Skip to content

Commit

Permalink
feat(runtime-dom): Trusted Types compatibility (#10844)
Browse files Browse the repository at this point in the history
  • Loading branch information
haoqunjiang committed Aug 2, 2024
1 parent 998dca5 commit 6d4eb94
Show file tree
Hide file tree
Showing 8 changed files with 187 additions and 4 deletions.
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@
"@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",
Expand Down Expand Up @@ -99,6 +100,7 @@
"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",
Expand Down
2 changes: 1 addition & 1 deletion packages/runtime-core/src/compat/global.ts
Original file line number Diff line number Diff line change
Expand Up @@ -548,7 +548,7 @@ function installCompatMount(
}

// clear content before mounting
container.innerHTML = ''
container.textContent = ''

// TODO hydration
render(vnode, container, namespace)
Expand Down
3 changes: 3 additions & 0 deletions packages/runtime-dom/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,5 +53,8 @@
"@vue/runtime-core": "workspace:*",
"@vue/reactivity": "workspace:*",
"csstype": "^3.1.3"
},
"devDependencies": {
"@types/trusted-types": "^2.0.7"
}
}
2 changes: 1 addition & 1 deletion packages/runtime-dom/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ export const createApp = ((...args) => {
}

// clear content before mounting
container.innerHTML = ''
container.textContent = ''
const proxy = mount(container, false, resolveRootNamespace(container))
if (container instanceof Element) {
container.removeAttribute('v-cloak')
Expand Down
40 changes: 38 additions & 2 deletions packages/runtime-dom/src/nodeOps.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,39 @@
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'
Expand Down Expand Up @@ -76,12 +111,13 @@ export const nodeOps: Omit<RendererOptions<Node, Element>, 'patchProp'> = {
}
} 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') {
Expand Down
17 changes: 17 additions & 0 deletions packages/vue/__tests__/e2e/trusted-types.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<!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>
103 changes: 103 additions & 0 deletions packages/vue/__tests__/e2e/trusted-types.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
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,
)
})
22 changes: 22 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 6d4eb94

Please sign in to comment.