diff --git a/packages/compiler-core/src/transforms/hoistStatic.ts b/packages/compiler-core/src/transforms/hoistStatic.ts index 920ba8d4a4b..215792ed403 100644 --- a/packages/compiler-core/src/transforms/hoistStatic.ts +++ b/packages/compiler-core/src/transforms/hoistStatic.ts @@ -52,16 +52,6 @@ function walk( context: TransformContext, doNotHoistNode: boolean = false ) { - // Some transforms, e.g. transformAssetUrls from @vue/compiler-sfc, replaces - // static bindings with expressions. These expressions are guaranteed to be - // constant so they are still eligible for hoisting, but they are only - // available at runtime and therefore cannot be evaluated ahead of time. - // This is only a concern for pre-stringification (via transformHoist by - // @vue/compiler-dom), but doing it here allows us to perform only one full - // walk of the AST and allow `stringifyStatic` to stop walking as soon as its - // stringification threshold is met. - let canStringify = true - const { children } = node const originalCount = children.length let hoistedCount = 0 @@ -77,9 +67,6 @@ function walk( ? ConstantTypes.NOT_CONSTANT : getConstantType(child, context) if (constantType > ConstantTypes.NOT_CONSTANT) { - if (constantType < ConstantTypes.CAN_STRINGIFY) { - canStringify = false - } if (constantType >= ConstantTypes.CAN_HOIST) { ;(child.codegenNode as VNodeCall).patchFlag = PatchFlags.HOISTED + (__DEV__ ? ` /* HOISTED */` : ``) @@ -110,17 +97,12 @@ function walk( } } } - } else if (child.type === NodeTypes.TEXT_CALL) { - const contentType = getConstantType(child.content, context) - if (contentType > 0) { - if (contentType < ConstantTypes.CAN_STRINGIFY) { - canStringify = false - } - if (contentType >= ConstantTypes.CAN_HOIST) { - child.codegenNode = context.hoist(child.codegenNode) - hoistedCount++ - } - } + } else if ( + child.type === NodeTypes.TEXT_CALL && + getConstantType(child.content, context) >= ConstantTypes.CAN_HOIST + ) { + child.codegenNode = context.hoist(child.codegenNode) + hoistedCount++ } // walk further @@ -148,7 +130,7 @@ function walk( } } - if (canStringify && hoistedCount && context.transformHoist) { + if (hoistedCount && context.transformHoist) { context.transformHoist(children, context, node) } diff --git a/packages/compiler-dom/__tests__/transforms/__snapshots__/stringifyStatic.spec.ts.snap b/packages/compiler-dom/__tests__/transforms/__snapshots__/stringifyStatic.spec.ts.snap new file mode 100644 index 00000000000..efa75df3f47 --- /dev/null +++ b/packages/compiler-dom/__tests__/transforms/__snapshots__/stringifyStatic.spec.ts.snap @@ -0,0 +1,34 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`stringify static html should bail on bindings that are hoisted but not stringifiable 1`] = ` +"const { createElementVNode: _createElementVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = Vue + +const _hoisted_1 = /*#__PURE__*/_createElementVNode(\\"div\\", null, [ + /*#__PURE__*/_createElementVNode(\\"span\\", { class: \\"foo\\" }, \\"foo\\"), + /*#__PURE__*/_createElementVNode(\\"span\\", { class: \\"foo\\" }, \\"foo\\"), + /*#__PURE__*/_createElementVNode(\\"span\\", { class: \\"foo\\" }, \\"foo\\"), + /*#__PURE__*/_createElementVNode(\\"span\\", { class: \\"foo\\" }, \\"foo\\"), + /*#__PURE__*/_createElementVNode(\\"span\\", { class: \\"foo\\" }, \\"foo\\"), + /*#__PURE__*/_createElementVNode(\\"img\\", { src: _imports_0_ }) +], -1 /* HOISTED */) +const _hoisted_2 = [ + _hoisted_1 +] + +return function render(_ctx, _cache) { + return (_openBlock(), _createElementBlock(\\"div\\", null, _hoisted_2)) +}" +`; + +exports[`stringify static html should work with bindings that are non-static but stringifiable 1`] = ` +"const { createElementVNode: _createElementVNode, createStaticVNode: _createStaticVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = Vue + +const _hoisted_1 = /*#__PURE__*/_createStaticVNode(\\"
foofoofoofoofoo
\\", 1) +const _hoisted_2 = [ + _hoisted_1 +] + +return function render(_ctx, _cache) { + return (_openBlock(), _createElementBlock(\\"div\\", null, _hoisted_2)) +}" +`; diff --git a/packages/compiler-dom/__tests__/transforms/stringifyStatic.spec.ts b/packages/compiler-dom/__tests__/transforms/stringifyStatic.spec.ts index 0923f0816c5..0beb42585b4 100644 --- a/packages/compiler-dom/__tests__/transforms/stringifyStatic.spec.ts +++ b/packages/compiler-dom/__tests__/transforms/stringifyStatic.spec.ts @@ -181,8 +181,8 @@ describe('stringify static html', () => { ]) }) - test('should bail on runtime constant v-bind bindings', () => { - const { ast } = compile( + test('should bail on bindings that are hoisted but not stringifiable', () => { + const { ast, code } = compile( `
${repeat( `foo`, StringifyThresholds.ELEMENT_WITH_BINDING_COUNT @@ -216,13 +216,62 @@ describe('stringify static html', () => { expect(ast.hoists).toMatchObject([ { // the expression and the tree are still hoistable - // but if it's stringified it will be NodeTypes.CALL_EXPRESSION + // but should stay NodeTypes.VNODE_CALL + // if it's stringified it will be NodeTypes.JS_CALL_EXPRESSION type: NodeTypes.VNODE_CALL }, { type: NodeTypes.JS_ARRAY_EXPRESSION } ]) + expect(code).toMatchSnapshot() + }) + + test('should work with bindings that are non-static but stringifiable', () => { + // if a binding is non-static but marked as CAN_STRINGIFY, it means it's + // a known reference to a constant string. + const { ast, code } = 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, + ConstantTypes.CAN_STRINGIFY + ) + node.props[0] = { + type: NodeTypes.DIRECTIVE, + name: 'bind', + arg: createSimpleExpression('src', true), + exp, + modifiers: [], + loc: node.loc + } + } + } + ] + } + ) + expect(ast.hoists).toMatchObject([ + { + // the hoisted node should be NodeTypes.JS_CALL_EXPRESSION + // of `createStaticVNode()` instead of dynamic NodeTypes.VNODE_CALL + type: NodeTypes.JS_CALL_EXPRESSION + }, + { + type: NodeTypes.JS_ARRAY_EXPRESSION + } + ]) + expect(code).toMatchSnapshot() }) // #1128 diff --git a/packages/compiler-dom/src/transforms/stringifyStatic.ts b/packages/compiler-dom/src/transforms/stringifyStatic.ts index 877c4d011c2..e257e3254fe 100644 --- a/packages/compiler-dom/src/transforms/stringifyStatic.ts +++ b/packages/compiler-dom/src/transforms/stringifyStatic.ts @@ -14,7 +14,8 @@ import { ElementTypes, PlainElementNode, JSChildNode, - TextCallNode + TextCallNode, + ConstantTypes } from '@vue/compiler-core' import { isVoidTag, @@ -171,7 +172,7 @@ const isNonStringifiable = /*#__PURE__*/ makeMap( /** * for a hoisted node, analyze it and return: - * - false: bailed (contains runtime constant) + * - false: bailed (contains non-stringifiable props or runtime constant) * - [nc, ec] where * - nc is the number of nodes inside * - ec is the number of element with bindings inside @@ -216,6 +217,13 @@ function analyzeNode(node: StringifiableNode): [number, number] | false { ) { return bail() } + if ( + p.exp && + (p.exp.type === NodeTypes.COMPOUND_EXPRESSION || + p.exp.constType < ConstantTypes.CAN_STRINGIFY) + ) { + return bail() + } } } for (let i = 0; i < node.children.length; i++) { diff --git a/packages/compiler-sfc/__tests__/templateTransformAssetUrl.spec.ts b/packages/compiler-sfc/__tests__/templateTransformAssetUrl.spec.ts index 15aff00bfea..44ef092aa22 100644 --- a/packages/compiler-sfc/__tests__/templateTransformAssetUrl.spec.ts +++ b/packages/compiler-sfc/__tests__/templateTransformAssetUrl.spec.ts @@ -160,7 +160,6 @@ describe('compiler sfc: transform asset url', () => { transformHoist: stringifyStatic } ) - console.log(code) expect(code).toMatch(`_createStaticVNode`) expect(code).toMatchSnapshot() })