diff --git a/packages/compiler-core/src/ast.ts b/packages/compiler-core/src/ast.ts index 5b5e9e83ef4..7adfc8cba58 100644 --- a/packages/compiler-core/src/ast.ts +++ b/packages/compiler-core/src/ast.ts @@ -195,6 +195,9 @@ export interface SimpleExpressionNode extends Node { // an expression parsed as the params of a function will track // the identifiers declared inside the function body. identifiers?: string[] + // some expressions (e.g. transformAssetUrls import identifiers) are constant, + // but cannot be stringified because they must be first evaluated at runtime. + isRuntimeConstant?: boolean } export interface InterpolationNode extends Node { diff --git a/packages/compiler-dom/__tests__/transforms/stringifyStatic.spec.ts b/packages/compiler-dom/__tests__/transforms/stringifyStatic.spec.ts index 5e942c17af7..3dca19a0e8d 100644 --- a/packages/compiler-dom/__tests__/transforms/stringifyStatic.spec.ts +++ b/packages/compiler-dom/__tests__/transforms/stringifyStatic.spec.ts @@ -1,4 +1,9 @@ -import { compile, NodeTypes, CREATE_STATIC } from '../../src' +import { + compile, + NodeTypes, + CREATE_STATIC, + createSimpleExpression +} from '../../src' import { stringifyStatic, StringifyThresholds @@ -121,4 +126,46 @@ describe('stringify static html', () => { ] }) }) + + test('should bail on runtime constant v-bind bindings', () => { + const { ast } = compile( + `
${repeat( + `foo`, + StringifyThresholds.ELEMENT_WITH_BINDING_COUNT + )}
`, + { + hoistStatic: true, + prefixIdentifiers: true, + transformHoist: stringifyStatic, + nodeTransforms: [ + node => { + if (node.type === NodeTypes.ELEMENT && node.tag === 'img') { + const exp = createSimpleExpression( + '_imports_0_', + false, + node.loc, + true + ) + exp.isRuntimeConstant = true + node.props[0] = { + type: NodeTypes.DIRECTIVE, + name: 'bind', + arg: createSimpleExpression('src', true), + exp, + modifiers: [], + loc: node.loc + } + } + } + ] + } + ) + // the expression and the tree are still hoistable + expect(ast.hoists.length).toBe(1) + // ...but the hoisted tree should not be stringified + expect(ast.hoists[0]).toMatchObject({ + // if it's stringified it will be NodeTypes.CALL_EXPRESSION + type: NodeTypes.VNODE_CALL + }) + }) }) diff --git a/packages/compiler-dom/src/transforms/stringifyStatic.ts b/packages/compiler-dom/src/transforms/stringifyStatic.ts index a4aa7745f8e..f648257e489 100644 --- a/packages/compiler-dom/src/transforms/stringifyStatic.ts +++ b/packages/compiler-dom/src/transforms/stringifyStatic.ts @@ -47,12 +47,30 @@ export const enum StringifyThresholds { function shouldOptimize(node: ElementNode): boolean { let bindingThreshold = StringifyThresholds.ELEMENT_WITH_BINDING_COUNT let nodeThreshold = StringifyThresholds.NODE_COUNT + let bail = false // TODO: check for cases where using innerHTML will result in different // output compared to imperative node insertions. // probably only need to check for most common case // i.e. non-phrasing-content tags inside `

` function walk(node: ElementNode) { + // some transforms, e.g. `transformAssetUrls` in `@vue/compiler-sfc` may + // convert static attributes into a v-bind with a constnat expresion. + // Such constant bindings are eligible for hoisting but not for static + // stringification because they cannot be pre-evaluated. + for (let i = 0; i < node.props.length; i++) { + const p = node.props[i] + if ( + p.type === NodeTypes.DIRECTIVE && + p.name === 'bind' && + p.exp && + p.exp.type !== NodeTypes.COMPOUND_EXPRESSION && + p.exp.isRuntimeConstant + ) { + bail = true + return false + } + } for (let i = 0; i < node.children.length; i++) { if (--nodeThreshold === 0) { return true @@ -65,6 +83,9 @@ function shouldOptimize(node: ElementNode): boolean { if (walk(child)) { return true } + if (bail) { + return false + } } } return false diff --git a/packages/compiler-sfc/src/templateTransformAssetUrl.ts b/packages/compiler-sfc/src/templateTransformAssetUrl.ts index 44ebe36af0a..40189be86af 100644 --- a/packages/compiler-sfc/src/templateTransformAssetUrl.ts +++ b/packages/compiler-sfc/src/templateTransformAssetUrl.ts @@ -126,11 +126,14 @@ function getImportsExpressionExp( } const name = `_imports_${importsArray.length}` const exp = createSimpleExpression(name, false, loc, true) + exp.isRuntimeConstant = true context.imports.add({ exp, path }) if (hash && path) { - return context.hoist( + const ret = context.hoist( createSimpleExpression(`${name} + '${hash}'`, false, loc, true) ) + ret.isRuntimeConstant = true + return ret } else { return exp } diff --git a/packages/compiler-sfc/src/templateTransformSrcset.ts b/packages/compiler-sfc/src/templateTransformSrcset.ts index dccebd9e5c6..06b16ffd67c 100644 --- a/packages/compiler-sfc/src/templateTransformSrcset.ts +++ b/packages/compiler-sfc/src/templateTransformSrcset.ts @@ -86,11 +86,14 @@ export const transformSrcset: NodeTransform = (node, context) => { } }) + const hoisted = context.hoist(compoundExpression) + hoisted.isRuntimeConstant = true + node.props[index] = { type: NodeTypes.DIRECTIVE, name: 'bind', arg: createSimpleExpression('srcset', true, attr.loc), - exp: context.hoist(compoundExpression), + exp: hoisted, modifiers: [], loc: attr.loc }