diff --git a/packages/compiler-core/src/transforms/transformExpression.ts b/packages/compiler-core/src/transforms/transformExpression.ts
index 12d8fc0c29c..b78daaf7226 100644
--- a/packages/compiler-core/src/transforms/transformExpression.ts
+++ b/packages/compiler-core/src/transforms/transformExpression.ts
@@ -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
diff --git a/packages/compiler-dom/__tests__/transforms/stringifyStatic.spec.ts b/packages/compiler-dom/__tests__/transforms/stringifyStatic.spec.ts
new file mode 100644
index 00000000000..d1095d3f425
--- /dev/null
+++ b/packages/compiler-dom/__tests__/transforms/stringifyStatic.spec.ts
@@ -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(
+ `
`
+ )
+ 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(
+ `${repeat(
+ ``,
+ StringifyThresholds.ELEMENT_WITH_BINDING_COUNT
+ )}
`
+ )
+ 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(
+ `${repeat(
+ ``,
+ StringifyThresholds.ELEMENT_WITH_BINDING_COUNT
+ )}
`
+ )
+ ]
+ })
+ })
+
+ test('should work on eligible content (elements > 20)', () => {
+ const { ast } = compileWithStringify(
+ `${repeat(
+ ``,
+ StringifyThresholds.NODE_COUNT
+ )}
`
+ )
+ 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(
+ `${repeat(
+ ``,
+ StringifyThresholds.NODE_COUNT
+ )}
`
+ )
+ ]
+ })
+ })
+
+ test('serliazing constant bindings', () => {
+ const { ast } = compileWithStringify(
+ `${repeat(
+ `{{ 1 }} + {{ false }}`,
+ StringifyThresholds.ELEMENT_WITH_BINDING_COUNT
+ )}
`
+ )
+ 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(
+ `${repeat(
+ `1 + false`,
+ StringifyThresholds.ELEMENT_WITH_BINDING_COUNT
+ )}
`
+ )
+ ]
+ })
+ })
+
+ test('escape', () => {
+ const { ast } = compileWithStringify(
+ `${repeat(
+ `{{ 1 }} + {{ '<' }}` +
+ `&`,
+ StringifyThresholds.ELEMENT_WITH_BINDING_COUNT
+ )}
`
+ )
+ 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(
+ `${repeat(
+ `1 + <` + `&`,
+ StringifyThresholds.ELEMENT_WITH_BINDING_COUNT
+ )}
`
+ )
+ ]
+ })
+ })
+})
diff --git a/packages/compiler-dom/src/index.ts b/packages/compiler-dom/src/index.ts
index ca305153818..b0202263661 100644
--- a/packages/compiler-dom/src/index.ts
+++ b/packages/compiler-dom/src/index.ts
@@ -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
diff --git a/packages/compiler-dom/src/stringifyStatic.ts b/packages/compiler-dom/src/transforms/stringifyStatic.ts
similarity index 64%
rename from packages/compiler-dom/src/stringifyStatic.ts
rename to packages/compiler-dom/src/transforms/stringifyStatic.ts
index 47e5321b441..a2efa56eec0 100644
--- a/packages/compiler-dom/src/stringifyStatic.ts
+++ b/packages/compiler-dom/src/transforms/stringifyStatic.ts
@@ -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(`bar
`)
+// 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 ``
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
+ }
+}