mirror of https://github.com/vuejs/core.git
feat(runtime-dom): Trusted Types compatibility (#10844)
This commit is contained in:
parent
998dca59f1
commit
6d4eb94853
|
@ -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",
|
||||
|
@ -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",
|
||||
|
|
|
@ -548,7 +548,7 @@ function installCompatMount(
|
|||
}
|
||||
|
||||
// clear content before mounting
|
||||
container.innerHTML = ''
|
||||
container.textContent = ''
|
||||
|
||||
// TODO hydration
|
||||
render(vnode, container, namespace)
|
||||
|
|
|
@ -53,5 +53,8 @@
|
|||
"@vue/runtime-core": "workspace:*",
|
||||
"@vue/reactivity": "workspace:*",
|
||||
"csstype": "^3.1.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/trusted-types": "^2.0.7"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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'
|
||||
|
@ -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') {
|
||||
|
|
|
@ -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>
|
|
@ -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,
|
||||
)
|
||||
})
|
|
@ -62,6 +62,9 @@ importers:
|
|||
'@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))
|
||||
|
@ -149,6 +152,9 @@ importers:
|
|||
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
|
||||
|
@ -325,6 +331,10 @@ importers:
|
|||
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:
|
||||
|
@ -1220,6 +1230,12 @@ packages:
|
|||
'@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==}
|
||||
|
||||
|
@ -4204,6 +4220,12 @@ snapshots:
|
|||
|
||||
'@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
|
||||
|
|
Loading…
Reference in New Issue