"@types/hash-sum": "^1.0.2",
"@types/node": "^20.14.13",
"@types/semver": "^7.5.8",
+ "@types/serve-handler": "^6.1.4",
"@vitest/coverage-istanbul": "^1.6.0",
"@vue/consolidate": "1.0.0",
"conventional-changelog-cli": "^5.0.0",
"rollup-plugin-polyfill-node": "^0.13.0",
"semver": "^7.6.3",
"serve": "^14.2.3",
+ "serve-handler": "^6.1.5",
"simple-git-hooks": "^2.11.1",
"todomvc-app-css": "^2.4.3",
"tslib": "^2.6.3",
}
// clear content before mounting
- container.innerHTML = ''
+ container.textContent = ''
// TODO hydration
render(vnode, container, namespace)
"@vue/runtime-core": "workspace:*",
"@vue/reactivity": "workspace:*",
"csstype": "^3.1.3"
+ },
+ "devDependencies": {
+ "@types/trusted-types": "^2.0.7"
}
}
}
// clear content before mounting
- container.innerHTML = ''
+ container.textContent = ''
const proxy = mount(container, false, resolveRootNamespace(container))
if (container instanceof Element) {
container.removeAttribute('v-cloak')
+import { warn } from '@vue/runtime-core'
import type { RendererOptions } from '@vue/runtime-core'
+import type {
+ TrustedHTML,
+ TrustedTypePolicy,
+ TrustedTypesWindow,
+} from 'trusted-types/lib'
+
+let policy: Pick<TrustedTypePolicy, 'name' | 'createHTML'> | undefined =
+ undefined
+
+const tt =
+ typeof window !== 'undefined' &&
+ (window as unknown as TrustedTypesWindow).trustedTypes
+
+if (tt) {
+ try {
+ policy = /*#__PURE__*/ tt.createPolicy('vue', {
+ createHTML: val => val,
+ })
+ } catch (e: unknown) {
+ // `createPolicy` throws a TypeError if the name is a duplicate
+ // and the CSP trusted-types directive is not using `allow-duplicates`.
+ // So we have to catch that error.
+ __DEV__ && warn(`Error creating trusted types policy: ${e}`)
+ }
+}
+
+// __UNSAFE__
+// Reason: potentially setting innerHTML.
+// This function merely perform a type-level trusted type conversion
+// for use in `innerHTML` assignment, etc.
+// Be careful of whatever value passed to this function.
+const unsafeToTrustedHTML: (value: string) => TrustedHTML | string = policy
+ ? val => policy.createHTML(val)
+ : val => val
export const svgNS = 'http://www.w3.org/2000/svg'
export const mathmlNS = 'http://www.w3.org/1998/Math/MathML'
}
} else {
// fresh insert
- templateContainer.innerHTML =
+ templateContainer.innerHTML = unsafeToTrustedHTML(
namespace === 'svg'
? `<svg>${content}</svg>`
: namespace === 'mathml'
? `<math>${content}</math>`
- : content
+ : content,
+ ) as string
const template = templateContainer.content
if (namespace === 'svg' || namespace === 'mathml') {
--- /dev/null
+<!doctype html>
+<html lang="en">
+ <head>
+ <meta charset="UTF-8" />
+ <link rel="icon" href="data:;base64,iVBORw0KGgo=">
+ <meta
+ http-equiv="content-security-policy"
+ content="require-trusted-types-for 'script'"
+ />
+ <title>Vue App</title>
+ <script src="../../dist/vue.global.js"></script>
+ </head>
+
+ <body>
+ <div id="app"></div>
+ </body>
+</html>
--- /dev/null
+import { once } from 'node:events'
+import { createServer } from 'node:http'
+import path from 'node:path'
+import { beforeAll } from 'vitest'
+import serveHandler from 'serve-handler'
+
+import { E2E_TIMEOUT, setupPuppeteer } from './e2eUtils'
+
+// use the `vue` package root as the public directory
+// because we need to serve the Vue runtime for the tests
+const serverRoot = path.resolve(import.meta.dirname, '../../')
+const testPort = 9090
+const basePath = path.relative(
+ serverRoot,
+ path.resolve(import.meta.dirname, './trusted-types.html'),
+)
+const baseUrl = `http://localhost:${testPort}/${basePath}`
+
+const { page, html } = setupPuppeteer()
+
+let server: ReturnType<typeof createServer>
+beforeAll(async () => {
+ // sets up the static server
+ server = createServer((req, res) => {
+ return serveHandler(req, res, {
+ public: serverRoot,
+ cleanUrls: false,
+ })
+ })
+
+ server.listen(testPort)
+ await once(server, 'listening')
+})
+
+afterAll(async () => {
+ server.close()
+ await once(server, 'close')
+})
+
+describe('e2e: trusted types', () => {
+ beforeEach(async () => {
+ await page().goto(baseUrl)
+ await page().waitForSelector('#app')
+ })
+
+ test(
+ 'should render the hello world app',
+ async () => {
+ await page().evaluate(() => {
+ const { createApp, ref, h } = (window as any).Vue
+ createApp({
+ setup() {
+ const msg = ref('✅success: hello world')
+ return function render() {
+ return h('div', msg.value)
+ }
+ },
+ }).mount('#app')
+ })
+ expect(await html('#app')).toContain('<div>✅success: hello world</div>')
+ },
+ E2E_TIMEOUT,
+ )
+
+ test(
+ 'should render static vnode without error',
+ async () => {
+ await page().evaluate(() => {
+ const { createApp, createStaticVNode } = (window as any).Vue
+ createApp({
+ render() {
+ return createStaticVNode('<div>✅success: static vnode</div>')
+ },
+ }).mount('#app')
+ })
+ expect(await html('#app')).toContain('<div>✅success: static vnode</div>')
+ },
+ E2E_TIMEOUT,
+ )
+
+ test(
+ 'should accept v-html with custom policy',
+ async () => {
+ await page().evaluate(() => {
+ const testPolicy = (window as any).trustedTypes.createPolicy('test', {
+ createHTML: (input: string): string => input,
+ })
+
+ const { createApp, ref, h } = (window as any).Vue
+ createApp({
+ setup() {
+ const msg = ref('✅success: v-html')
+ return function render() {
+ return h('div', { innerHTML: testPolicy.createHTML(msg.value) })
+ }
+ },
+ }).mount('#app')
+ })
+ expect(await html('#app')).toContain('<div>✅success: v-html</div>')
+ },
+ E2E_TIMEOUT,
+ )
+})
'@types/semver':
specifier: ^7.5.8
version: 7.5.8
+ '@types/serve-handler':
+ specifier: ^6.1.4
+ version: 6.1.4
'@vitest/coverage-istanbul':
specifier: ^1.6.0
version: 1.6.0(vitest@1.6.0(@types/node@20.14.13)(jsdom@24.1.1)(sass@1.77.8)(terser@5.31.1))
serve:
specifier: ^14.2.3
version: 14.2.3
+ serve-handler:
+ specifier: ^6.1.5
+ version: 6.1.5
simple-git-hooks:
specifier: ^2.11.1
version: 2.11.1
csstype:
specifier: ^3.1.3
version: 3.1.3
+ devDependencies:
+ '@types/trusted-types':
+ specifier: ^2.0.7
+ version: 2.0.7
packages/runtime-test:
dependencies:
'@types/semver@7.5.8':
resolution: {integrity: sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==}
+ '@types/serve-handler@6.1.4':
+ resolution: {integrity: sha512-aXy58tNie0NkuSCY291xUxl0X+kGYy986l4kqW6Gi4kEXgr6Tx0fpSH7YwUSa5usPpG3s9DBeIR6hHcDtL2IvQ==}
+
+ '@types/trusted-types@2.0.7':
+ resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==}
+
'@types/yauzl@2.10.3':
resolution: {integrity: sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==}
'@types/semver@7.5.8': {}
+ '@types/serve-handler@6.1.4':
+ dependencies:
+ '@types/node': 20.14.13
+
+ '@types/trusted-types@2.0.7': {}
+
'@types/yauzl@2.10.3':
dependencies:
'@types/node': 20.14.13