diff --git a/packages/compiler-core/__tests__/__snapshots__/codegen.spec.ts.snap b/packages/compiler-core/__tests__/__snapshots__/codegen.spec.ts.snap index db268af4f9b..d1bbc6d6d0d 100644 --- a/packages/compiler-core/__tests__/__snapshots__/codegen.spec.ts.snap +++ b/packages/compiler-core/__tests__/__snapshots__/codegen.spec.ts.snap @@ -94,6 +94,15 @@ return function render(_ctx, _cache) { }" `; +exports[`compiler: codegen > empty interpolation 1`] = ` +" +return function render(_ctx, _cache) { + with (_ctx) { + return "" + } +}" +`; + exports[`compiler: codegen > forNode 1`] = ` " return function render(_ctx, _cache) { @@ -183,6 +192,15 @@ export function render(_ctx, _cache) { }" `; +exports[`compiler: codegen > static interpolation 1`] = ` +" +return function render(_ctx, _cache) { + with (_ctx) { + return "hello1falseundefinednullhi" + } +}" +`; + exports[`compiler: codegen > static text 1`] = ` " return function render(_ctx, _cache) { diff --git a/packages/compiler-core/__tests__/codegen.spec.ts b/packages/compiler-core/__tests__/codegen.spec.ts index 34386ce6930..3653be66df5 100644 --- a/packages/compiler-core/__tests__/codegen.spec.ts +++ b/packages/compiler-core/__tests__/codegen.spec.ts @@ -3,6 +3,7 @@ import { type DirectiveArguments, type ForCodegenNode, type IfConditionalExpression, + type InterpolationNode, NodeTypes, type RootNode, type VNodeCall, @@ -192,6 +193,40 @@ describe('compiler: codegen', () => { expect(code).toMatchSnapshot() }) + test('static interpolation', () => { + const codegenNode: InterpolationNode = { + type: NodeTypes.INTERPOLATION, + loc: locStub, + content: createSimpleExpression( + `"hello" + 1 + false + undefined + null + ${'`hi`'}`, + true, + locStub, + ), + } + const { code } = generate( + createRoot({ + codegenNode, + }), + ) + expect(code).toMatch(`return "hello1falseundefinednullhi"`) + expect(code).toMatchSnapshot() + }) + + test('empty interpolation', () => { + const codegenNode: InterpolationNode = { + type: NodeTypes.INTERPOLATION, + loc: locStub, + content: createSimpleExpression(``, true, locStub), + } + const { code } = generate( + createRoot({ + codegenNode, + }), + ) + expect(code).toMatch(`return ""`) + expect(code).toMatchSnapshot() + }) + test('comment', () => { const { code } = generate( createRoot({ diff --git a/packages/compiler-core/__tests__/transforms/__snapshots__/cacheStatic.spec.ts.snap b/packages/compiler-core/__tests__/transforms/__snapshots__/cacheStatic.spec.ts.snap index b8bef22c478..9ade20b23f6 100644 --- a/packages/compiler-core/__tests__/transforms/__snapshots__/cacheStatic.spec.ts.snap +++ b/packages/compiler-core/__tests__/transforms/__snapshots__/cacheStatic.spec.ts.snap @@ -148,7 +148,7 @@ return function render(_ctx, _cache) { const { toDisplayString: _toDisplayString, createElementVNode: _createElementVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = _Vue return (_openBlock(), _createElementBlock("div", null, _cache[0] || (_cache[0] = [ - _createElementVNode("span", null, "foo " + _toDisplayString(1) + " " + _toDisplayString(true), -1 /* CACHED */) + _createElementVNode("span", null, "foo " + "1" + " " + "true", -1 /* CACHED */) ]))) } }" @@ -162,7 +162,7 @@ return function render(_ctx, _cache) { const { toDisplayString: _toDisplayString, createElementVNode: _createElementVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = _Vue return (_openBlock(), _createElementBlock("div", null, _cache[0] || (_cache[0] = [ - _createElementVNode("span", { foo: 0 }, _toDisplayString(1), -1 /* CACHED */) + _createElementVNode("span", { foo: 0 }, "1", -1 /* CACHED */) ]))) } }" diff --git a/packages/compiler-core/src/codegen.ts b/packages/compiler-core/src/codegen.ts index 6b4559fabb2..8dc19eae0d2 100644 --- a/packages/compiler-core/src/codegen.ts +++ b/packages/compiler-core/src/codegen.ts @@ -7,6 +7,7 @@ import { type CommentNode, type CompoundExpressionNode, type ConditionalExpression, + ConstantTypes, type ExpressionNode, type FunctionExpression, type IfStatement, @@ -32,6 +33,7 @@ import { SourceMapGenerator } from 'source-map-js' import { advancePositionWithMutation, assert, + evaluateConstant, isSimpleIdentifier, toValidAssetId, } from './utils' @@ -41,6 +43,7 @@ import { isArray, isString, isSymbol, + toDisplayString, } from '@vue/shared' import { CREATE_COMMENT, @@ -760,6 +763,20 @@ function genExpression(node: SimpleExpressionNode, context: CodegenContext) { function genInterpolation(node: InterpolationNode, context: CodegenContext) { const { push, helper, pure } = context + + if ( + node.content.type === NodeTypes.SIMPLE_EXPRESSION && + node.content.constType === ConstantTypes.CAN_STRINGIFY + ) { + if (node.content.content) { + push(JSON.stringify(toDisplayString(evaluateConstant(node.content)))) + } else { + push(`""`) + } + + return + } + if (pure) push(PURE_ANNOTATION) push(`${helper(TO_DISPLAY_STRING)}(`) genNode(node.content, context) diff --git a/packages/compiler-core/src/transforms/transformExpression.ts b/packages/compiler-core/src/transforms/transformExpression.ts index 9ae8897e674..d13abff2db4 100644 --- a/packages/compiler-core/src/transforms/transformExpression.ts +++ b/packages/compiler-core/src/transforms/transformExpression.ts @@ -44,7 +44,9 @@ import { parseExpression } from '@babel/parser' import { IS_REF, UNREF } from '../runtimeHelpers' import { BindingTypes } from '../options' -const isLiteralWhitelisted = /*@__PURE__*/ makeMap('true,false,null,this') +const isLiteralWhitelisted = /*@__PURE__*/ makeMap( + 'true,false,null,undefined,this', +) export const transformExpression: NodeTransform = (node, context) => { if (node.type === NodeTypes.INTERPOLATION) { @@ -119,7 +121,14 @@ export function processExpression( return node } - if (!context.prefixIdentifiers || !node.content.trim()) { + if (!node.content.trim()) { + // This allows stringification to continue in the presence of empty + // interpolations. + node.constType = ConstantTypes.CAN_STRINGIFY + return node + } + + if (!context.prefixIdentifiers) { return node } diff --git a/packages/compiler-core/src/utils.ts b/packages/compiler-core/src/utils.ts index b49d70bb2fb..40e94c6669e 100644 --- a/packages/compiler-core/src/utils.ts +++ b/packages/compiler-core/src/utils.ts @@ -37,7 +37,13 @@ import { TO_HANDLERS, WITH_MEMO, } from './runtimeHelpers' -import { NOOP, isObject, isString } from '@vue/shared' +import { + NOOP, + isObject, + isString, + isSymbol, + toDisplayString, +} from '@vue/shared' import type { PropsExpression } from './transforms/transformElement' import { parseExpression } from '@babel/parser' import type { Expression, Node } from '@babel/types' @@ -564,3 +570,32 @@ export function getMemoedVNodeCall( } export const forAliasRE: RegExp = /([\s\S]*?)\s+(?:in|of)\s+(\S[\s\S]*)/ + +// __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/transforms/transformExpression) +export 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 += toDisplayString(evaluateConstant(c.content)) + } else { + res += evaluateConstant(c as ExpressionNode) + } + }) + return res + } +} diff --git a/packages/compiler-dom/__tests__/transforms/__snapshots__/stringifyStatic.spec.ts.snap b/packages/compiler-dom/__tests__/transforms/__snapshots__/stringifyStatic.spec.ts.snap index 5bc40d3fab5..8c6c3629e8c 100644 --- a/packages/compiler-dom/__tests__/transforms/__snapshots__/stringifyStatic.spec.ts.snap +++ b/packages/compiler-dom/__tests__/transforms/__snapshots__/stringifyStatic.spec.ts.snap @@ -158,6 +158,14 @@ return function render(_ctx, _cache) { }" `; +exports[`stringify static html > static interpolation 1`] = ` +"const { createElementVNode: _createElementVNode, createStaticVNode: _createStaticVNode } = Vue + +return function render(_ctx, _cache) { + return _cache[0] || (_cache[0] = _createStaticVNode("