feat(compiler-dom): handle constant expressions when stringifying static content
This commit is contained in:
parent
1389d7b88c
commit
8b7c162125
@ -90,6 +90,8 @@ export function processExpression(
|
||||
|
||||
// fast path if expression is a simple identifier.
|
||||
const rawExp = node.content
|
||||
// bail on parens to prevent any possible function invocations.
|
||||
const bailConstant = rawExp.indexOf(`(`) > -1
|
||||
if (isSimpleIdentifier(rawExp)) {
|
||||
if (
|
||||
!asParams &&
|
||||
@ -98,7 +100,7 @@ export function processExpression(
|
||||
!isLiteralWhitelisted(rawExp)
|
||||
) {
|
||||
node.content = `_ctx.${rawExp}`
|
||||
} else if (!context.identifiers[rawExp]) {
|
||||
} else if (!context.identifiers[rawExp] && !bailConstant) {
|
||||
// mark node constant for hoisting unless it's referring a scope variable
|
||||
node.isConstant = true
|
||||
}
|
||||
@ -139,12 +141,13 @@ export function processExpression(
|
||||
node.prefix = `${node.name}: `
|
||||
}
|
||||
node.name = `_ctx.${node.name}`
|
||||
node.isConstant = false
|
||||
ids.push(node)
|
||||
} else if (!isStaticPropertyKey(node, parent)) {
|
||||
// The identifier is considered constant unless it's pointing to a
|
||||
// scope variable (a v-for alias, or a v-slot prop)
|
||||
node.isConstant = !(needPrefix && knownIds[node.name])
|
||||
if (!(needPrefix && knownIds[node.name]) && !bailConstant) {
|
||||
node.isConstant = true
|
||||
}
|
||||
// also generate sub-expressions for other identifiers for better
|
||||
// source map support. (except for property keys which are static)
|
||||
ids.push(node)
|
||||
@ -234,7 +237,7 @@ export function processExpression(
|
||||
ret = createCompoundExpression(children, node.loc)
|
||||
} else {
|
||||
ret = node
|
||||
ret.isConstant = true
|
||||
ret.isConstant = !bailConstant
|
||||
}
|
||||
ret.identifiers = Object.keys(knownIds)
|
||||
return ret
|
||||
|
@ -0,0 +1,124 @@
|
||||
import { compile, NodeTypes, CREATE_STATIC } from '../../src'
|
||||
import {
|
||||
stringifyStatic,
|
||||
StringifyThresholds
|
||||
} from '../../src/transforms/stringifyStatic'
|
||||
|
||||
describe('stringify static html', () => {
|
||||
function compileWithStringify(template: string) {
|
||||
return compile(template, {
|
||||
hoistStatic: true,
|
||||
prefixIdentifiers: true,
|
||||
transformHoist: stringifyStatic
|
||||
})
|
||||
}
|
||||
|
||||
function repeat(code: string, n: number): string {
|
||||
return new Array(n)
|
||||
.fill(0)
|
||||
.map(() => code)
|
||||
.join('')
|
||||
}
|
||||
|
||||
test('should bail on non-eligible static trees', () => {
|
||||
const { ast } = compileWithStringify(
|
||||
`<div><div><div>hello</div><div>hello</div></div></div>`
|
||||
)
|
||||
expect(ast.hoists.length).toBe(1)
|
||||
// should be a normal vnode call
|
||||
expect(ast.hoists[0].type).toBe(NodeTypes.VNODE_CALL)
|
||||
})
|
||||
|
||||
test('should work on eligible content (elements with binding > 5)', () => {
|
||||
const { ast } = compileWithStringify(
|
||||
`<div><div>${repeat(
|
||||
`<span class="foo"/>`,
|
||||
StringifyThresholds.ELEMENT_WITH_BINDING_COUNT
|
||||
)}</div></div>`
|
||||
)
|
||||
expect(ast.hoists.length).toBe(1)
|
||||
// should be optimized now
|
||||
expect(ast.hoists[0]).toMatchObject({
|
||||
type: NodeTypes.JS_CALL_EXPRESSION,
|
||||
callee: CREATE_STATIC,
|
||||
arguments: [
|
||||
JSON.stringify(
|
||||
`<div>${repeat(
|
||||
`<span class="foo"></span>`,
|
||||
StringifyThresholds.ELEMENT_WITH_BINDING_COUNT
|
||||
)}</div>`
|
||||
)
|
||||
]
|
||||
})
|
||||
})
|
||||
|
||||
test('should work on eligible content (elements > 20)', () => {
|
||||
const { ast } = compileWithStringify(
|
||||
`<div><div>${repeat(
|
||||
`<span/>`,
|
||||
StringifyThresholds.NODE_COUNT
|
||||
)}</div></div>`
|
||||
)
|
||||
expect(ast.hoists.length).toBe(1)
|
||||
// should be optimized now
|
||||
expect(ast.hoists[0]).toMatchObject({
|
||||
type: NodeTypes.JS_CALL_EXPRESSION,
|
||||
callee: CREATE_STATIC,
|
||||
arguments: [
|
||||
JSON.stringify(
|
||||
`<div>${repeat(
|
||||
`<span></span>`,
|
||||
StringifyThresholds.NODE_COUNT
|
||||
)}</div>`
|
||||
)
|
||||
]
|
||||
})
|
||||
})
|
||||
|
||||
test('serliazing constant bindings', () => {
|
||||
const { ast } = compileWithStringify(
|
||||
`<div><div>${repeat(
|
||||
`<span :class="'foo' + 'bar'">{{ 1 }} + {{ false }}</span>`,
|
||||
StringifyThresholds.ELEMENT_WITH_BINDING_COUNT
|
||||
)}</div></div>`
|
||||
)
|
||||
expect(ast.hoists.length).toBe(1)
|
||||
// should be optimized now
|
||||
expect(ast.hoists[0]).toMatchObject({
|
||||
type: NodeTypes.JS_CALL_EXPRESSION,
|
||||
callee: CREATE_STATIC,
|
||||
arguments: [
|
||||
JSON.stringify(
|
||||
`<div>${repeat(
|
||||
`<span class="foobar">1 + false</span>`,
|
||||
StringifyThresholds.ELEMENT_WITH_BINDING_COUNT
|
||||
)}</div>`
|
||||
)
|
||||
]
|
||||
})
|
||||
})
|
||||
|
||||
test('escape', () => {
|
||||
const { ast } = compileWithStringify(
|
||||
`<div><div>${repeat(
|
||||
`<span :class="'foo' + '>ar'">{{ 1 }} + {{ '<' }}</span>` +
|
||||
`<span>&</span>`,
|
||||
StringifyThresholds.ELEMENT_WITH_BINDING_COUNT
|
||||
)}</div></div>`
|
||||
)
|
||||
expect(ast.hoists.length).toBe(1)
|
||||
// should be optimized now
|
||||
expect(ast.hoists[0]).toMatchObject({
|
||||
type: NodeTypes.JS_CALL_EXPRESSION,
|
||||
callee: CREATE_STATIC,
|
||||
arguments: [
|
||||
JSON.stringify(
|
||||
`<div>${repeat(
|
||||
`<span class="foo>ar">1 + <</span>` + `<span>&</span>`,
|
||||
StringifyThresholds.ELEMENT_WITH_BINDING_COUNT
|
||||
)}</div>`
|
||||
)
|
||||
]
|
||||
})
|
||||
})
|
||||
})
|
@ -18,7 +18,7 @@ import { transformModel } from './transforms/vModel'
|
||||
import { transformOn } from './transforms/vOn'
|
||||
import { transformShow } from './transforms/vShow'
|
||||
import { warnTransitionChildren } from './transforms/warnTransitionChildren'
|
||||
import { stringifyStatic } from './stringifyStatic'
|
||||
import { stringifyStatic } from './transforms/stringifyStatic'
|
||||
|
||||
export const parserOptions = __BROWSER__
|
||||
? parserOptionsMinimal
|
||||
|
@ -6,12 +6,20 @@ import {
|
||||
SimpleExpressionNode,
|
||||
createCallExpression,
|
||||
HoistTransform,
|
||||
CREATE_STATIC
|
||||
CREATE_STATIC,
|
||||
ExpressionNode
|
||||
} from '@vue/compiler-core'
|
||||
import { isVoidTag, isString, isSymbol, escapeHtml } from '@vue/shared'
|
||||
import {
|
||||
isVoidTag,
|
||||
isString,
|
||||
isSymbol,
|
||||
escapeHtml,
|
||||
toDisplayString
|
||||
} from '@vue/shared'
|
||||
|
||||
// Turn eligible hoisted static trees into stringied static nodes, e.g.
|
||||
// const _hoisted_1 = createStaticVNode(`<div class="foo">bar</div>`)
|
||||
// This is only performed in non-in-browser compilations.
|
||||
export const stringifyStatic: HoistTransform = (node, context) => {
|
||||
if (shouldOptimize(node)) {
|
||||
return createCallExpression(context.helper(CREATE_STATIC), [
|
||||
@ -22,6 +30,11 @@ export const stringifyStatic: HoistTransform = (node, context) => {
|
||||
}
|
||||
}
|
||||
|
||||
export const enum StringifyThresholds {
|
||||
ELEMENT_WITH_BINDING_COUNT = 5,
|
||||
NODE_COUNT = 20
|
||||
}
|
||||
|
||||
// Opt-in heuristics based on:
|
||||
// 1. number of elements with attributes > 5.
|
||||
// 2. OR: number of total nodes > 20
|
||||
@ -29,8 +42,8 @@ export const stringifyStatic: HoistTransform = (node, context) => {
|
||||
// it is only worth it when the tree is complex enough
|
||||
// (e.g. big piece of static content)
|
||||
function shouldOptimize(node: ElementNode): boolean {
|
||||
let bindingThreshold = 5
|
||||
let nodeThreshold = 20
|
||||
let bindingThreshold = StringifyThresholds.ELEMENT_WITH_BINDING_COUNT
|
||||
let nodeThreshold = StringifyThresholds.NODE_COUNT
|
||||
|
||||
// TODO: check for cases where using innerHTML will result in different
|
||||
// output compared to imperative node insertions.
|
||||
@ -67,11 +80,13 @@ function stringifyElement(
|
||||
if (p.type === NodeTypes.ATTRIBUTE) {
|
||||
res += ` ${p.name}`
|
||||
if (p.value) {
|
||||
res += `="${p.value.content}"`
|
||||
res += `="${escapeHtml(p.value.content)}"`
|
||||
}
|
||||
} else if (p.type === NodeTypes.DIRECTIVE && p.name === 'bind') {
|
||||
// constant v-bind, e.g. :foo="1"
|
||||
// TODO
|
||||
res += ` ${(p.arg as SimpleExpressionNode).content}="${escapeHtml(
|
||||
evaluateConstant(p.exp as ExpressionNode)
|
||||
)}"`
|
||||
}
|
||||
}
|
||||
if (context.scopeId) {
|
||||
@ -105,12 +120,9 @@ function stringifyNode(
|
||||
case NodeTypes.COMMENT:
|
||||
return `<!--${escapeHtml(node.content)}-->`
|
||||
case NodeTypes.INTERPOLATION:
|
||||
// constants
|
||||
// TODO check eval
|
||||
return (node.content as SimpleExpressionNode).content
|
||||
return escapeHtml(toDisplayString(evaluateConstant(node.content)))
|
||||
case NodeTypes.COMPOUND_EXPRESSION:
|
||||
// TODO proper handling
|
||||
return node.children.map((c: any) => stringifyNode(c, context)).join('')
|
||||
return escapeHtml(evaluateConstant(node))
|
||||
case NodeTypes.TEXT_CALL:
|
||||
return stringifyNode(node.content, context)
|
||||
default:
|
||||
@ -118,3 +130,32 @@ function stringifyNode(
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
// __UNSAFE__
|
||||
// Reason: eval.
|
||||
// It's technically safe to eval because only constant expressions are possible
|
||||
// here, e.g. `{{ 1 }}` or `{{ 'foo' }}`
|
||||
// in addition, constant exps bail on presence of parens so you can't even
|
||||
// run JSFuck in here. But we mark it unsafe for security review purposes.
|
||||
// (see compiler-core/src/transformExpressions)
|
||||
function evaluateConstant(exp: ExpressionNode): string {
|
||||
if (exp.type === NodeTypes.SIMPLE_EXPRESSION) {
|
||||
return new Function(`return ${exp.content}`)()
|
||||
} else {
|
||||
// compound
|
||||
let res = ``
|
||||
exp.children.forEach(c => {
|
||||
if (isString(c) || isSymbol(c)) {
|
||||
return
|
||||
}
|
||||
if (c.type === NodeTypes.TEXT) {
|
||||
res += c.content
|
||||
} else if (c.type === NodeTypes.INTERPOLATION) {
|
||||
res += evaluateConstant(c.content)
|
||||
} else {
|
||||
res += evaluateConstant(c)
|
||||
}
|
||||
})
|
||||
return res
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user