Skip to content

Commit 6d4eb94

Browse files
authoredAug 2, 2024··
feat(runtime-dom): Trusted Types compatibility (#10844)
1 parent 998dca5 commit 6d4eb94

File tree

8 files changed

+187
-4
lines changed

8 files changed

+187
-4
lines changed
 

‎package.json

+2
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@
7070
"@types/hash-sum": "^1.0.2",
7171
"@types/node": "^20.14.13",
7272
"@types/semver": "^7.5.8",
73+
"@types/serve-handler": "^6.1.4",
7374
"@vitest/coverage-istanbul": "^1.6.0",
7475
"@vue/consolidate": "1.0.0",
7576
"conventional-changelog-cli": "^5.0.0",
@@ -99,6 +100,7 @@
99100
"rollup-plugin-polyfill-node": "^0.13.0",
100101
"semver": "^7.6.3",
101102
"serve": "^14.2.3",
103+
"serve-handler": "^6.1.5",
102104
"simple-git-hooks": "^2.11.1",
103105
"todomvc-app-css": "^2.4.3",
104106
"tslib": "^2.6.3",

‎packages/runtime-core/src/compat/global.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -548,7 +548,7 @@ function installCompatMount(
548548
}
549549

550550
// clear content before mounting
551-
container.innerHTML = ''
551+
container.textContent = ''
552552

553553
// TODO hydration
554554
render(vnode, container, namespace)

‎packages/runtime-dom/package.json

+3
Original file line numberDiff line numberDiff line change
@@ -53,5 +53,8 @@
5353
"@vue/runtime-core": "workspace:*",
5454
"@vue/reactivity": "workspace:*",
5555
"csstype": "^3.1.3"
56+
},
57+
"devDependencies": {
58+
"@types/trusted-types": "^2.0.7"
5659
}
5760
}

‎packages/runtime-dom/src/index.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ export const createApp = ((...args) => {
123123
}
124124

125125
// clear content before mounting
126-
container.innerHTML = ''
126+
container.textContent = ''
127127
const proxy = mount(container, false, resolveRootNamespace(container))
128128
if (container instanceof Element) {
129129
container.removeAttribute('v-cloak')

‎packages/runtime-dom/src/nodeOps.ts

+38-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,39 @@
1+
import { warn } from '@vue/runtime-core'
12
import type { RendererOptions } from '@vue/runtime-core'
3+
import type {
4+
TrustedHTML,
5+
TrustedTypePolicy,
6+
TrustedTypesWindow,
7+
} from 'trusted-types/lib'
8+
9+
let policy: Pick<TrustedTypePolicy, 'name' | 'createHTML'> | undefined =
10+
undefined
11+
12+
const tt =
13+
typeof window !== 'undefined' &&
14+
(window as unknown as TrustedTypesWindow).trustedTypes
15+
16+
if (tt) {
17+
try {
18+
policy = /*#__PURE__*/ tt.createPolicy('vue', {
19+
createHTML: val => val,
20+
})
21+
} catch (e: unknown) {
22+
// `createPolicy` throws a TypeError if the name is a duplicate
23+
// and the CSP trusted-types directive is not using `allow-duplicates`.
24+
// So we have to catch that error.
25+
__DEV__ && warn(`Error creating trusted types policy: ${e}`)
26+
}
27+
}
28+
29+
// __UNSAFE__
30+
// Reason: potentially setting innerHTML.
31+
// This function merely perform a type-level trusted type conversion
32+
// for use in `innerHTML` assignment, etc.
33+
// Be careful of whatever value passed to this function.
34+
const unsafeToTrustedHTML: (value: string) => TrustedHTML | string = policy
35+
? val => policy.createHTML(val)
36+
: val => val
237

338
export const svgNS = 'http://www.w3.org/2000/svg'
439
export const mathmlNS = 'http://www.w3.org/1998/Math/MathML'
@@ -76,12 +111,13 @@ export const nodeOps: Omit<RendererOptions<Node, Element>, 'patchProp'> = {
76111
}
77112
} else {
78113
// fresh insert
79-
templateContainer.innerHTML =
114+
templateContainer.innerHTML = unsafeToTrustedHTML(
80115
namespace === 'svg'
81116
? `<svg>${content}</svg>`
82117
: namespace === 'mathml'
83118
? `<math>${content}</math>`
84-
: content
119+
: content,
120+
) as string
85121

86122
const template = templateContainer.content
87123
if (namespace === 'svg' || namespace === 'mathml') {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<!doctype html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8" />
5+
<link rel="icon" href="data:;base64,iVBORw0KGgo=">
6+
<meta
7+
http-equiv="content-security-policy"
8+
content="require-trusted-types-for 'script'"
9+
/>
10+
<title>Vue App</title>
11+
<script src="../../dist/vue.global.js"></script>
12+
</head>
13+
14+
<body>
15+
<div id="app"></div>
16+
</body>
17+
</html>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import { once } from 'node:events'
2+
import { createServer } from 'node:http'
3+
import path from 'node:path'
4+
import { beforeAll } from 'vitest'
5+
import serveHandler from 'serve-handler'
6+
7+
import { E2E_TIMEOUT, setupPuppeteer } from './e2eUtils'
8+
9+
// use the `vue` package root as the public directory
10+
// because we need to serve the Vue runtime for the tests
11+
const serverRoot = path.resolve(import.meta.dirname, '../../')
12+
const testPort = 9090
13+
const basePath = path.relative(
14+
serverRoot,
15+
path.resolve(import.meta.dirname, './trusted-types.html'),
16+
)
17+
const baseUrl = `http://localhost:${testPort}/${basePath}`
18+
19+
const { page, html } = setupPuppeteer()
20+
21+
let server: ReturnType<typeof createServer>
22+
beforeAll(async () => {
23+
// sets up the static server
24+
server = createServer((req, res) => {
25+
return serveHandler(req, res, {
26+
public: serverRoot,
27+
cleanUrls: false,
28+
})
29+
})
30+
31+
server.listen(testPort)
32+
await once(server, 'listening')
33+
})
34+
35+
afterAll(async () => {
36+
server.close()
37+
await once(server, 'close')
38+
})
39+
40+
describe('e2e: trusted types', () => {
41+
beforeEach(async () => {
42+
await page().goto(baseUrl)
43+
await page().waitForSelector('#app')
44+
})
45+
46+
test(
47+
'should render the hello world app',
48+
async () => {
49+
await page().evaluate(() => {
50+
const { createApp, ref, h } = (window as any).Vue
51+
createApp({
52+
setup() {
53+
const msg = ref('✅success: hello world')
54+
return function render() {
55+
return h('div', msg.value)
56+
}
57+
},
58+
}).mount('#app')
59+
})
60+
expect(await html('#app')).toContain('<div>✅success: hello world</div>')
61+
},
62+
E2E_TIMEOUT,
63+
)
64+
65+
test(
66+
'should render static vnode without error',
67+
async () => {
68+
await page().evaluate(() => {
69+
const { createApp, createStaticVNode } = (window as any).Vue
70+
createApp({
71+
render() {
72+
return createStaticVNode('<div>✅success: static vnode</div>')
73+
},
74+
}).mount('#app')
75+
})
76+
expect(await html('#app')).toContain('<div>✅success: static vnode</div>')
77+
},
78+
E2E_TIMEOUT,
79+
)
80+
81+
test(
82+
'should accept v-html with custom policy',
83+
async () => {
84+
await page().evaluate(() => {
85+
const testPolicy = (window as any).trustedTypes.createPolicy('test', {
86+
createHTML: (input: string): string => input,
87+
})
88+
89+
const { createApp, ref, h } = (window as any).Vue
90+
createApp({
91+
setup() {
92+
const msg = ref('✅success: v-html')
93+
return function render() {
94+
return h('div', { innerHTML: testPolicy.createHTML(msg.value) })
95+
}
96+
},
97+
}).mount('#app')
98+
})
99+
expect(await html('#app')).toContain('<div>✅success: v-html</div>')
100+
},
101+
E2E_TIMEOUT,
102+
)
103+
})

‎pnpm-lock.yaml

+22
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)
Please sign in to comment.