fix(compiler-sfc): make asset url imports stringifiable

This commit is contained in:
Evan You 2021-12-06 01:19:06 +08:00
parent 3e5ed6c1fc
commit 87c73e99d6
7 changed files with 136 additions and 16 deletions

View File

@ -38,6 +38,12 @@ export const enum StringifyThresholds {
type StringifiableNode = PlainElementNode | TextCallNode type StringifiableNode = PlainElementNode | TextCallNode
/**
* Regex for replacing placeholders for embedded constant variables
* (e.g. import URL string constants generated by compiler-sfc)
*/
const expReplaceRE = /__VUE_EXP_START__(.*?)__VUE_EXP_END__/g
/** /**
* Turn eligible hoisted static trees into stringified static nodes, e.g. * Turn eligible hoisted static trees into stringified static nodes, e.g.
* *
@ -80,7 +86,7 @@ export const stringifyStatic: HoistTransform = (children, context, parent) => {
const staticCall = createCallExpression(context.helper(CREATE_STATIC), [ const staticCall = createCallExpression(context.helper(CREATE_STATIC), [
JSON.stringify( JSON.stringify(
currentChunk.map(node => stringifyNode(node, context)).join('') currentChunk.map(node => stringifyNode(node, context)).join('')
), ).replace(expReplaceRE, `" + $1 + "`),
// the 2nd argument indicates the number of DOM nodes this static vnode // the 2nd argument indicates the number of DOM nodes this static vnode
// will insert / hydrate // will insert / hydrate
String(currentChunk.length) String(currentChunk.length)
@ -273,8 +279,17 @@ function stringifyElement(
res += `="${escapeHtml(p.value.content)}"` res += `="${escapeHtml(p.value.content)}"`
} }
} else if (p.type === NodeTypes.DIRECTIVE && p.name === 'bind') { } else if (p.type === NodeTypes.DIRECTIVE && p.name === 'bind') {
const exp = p.exp as SimpleExpressionNode
if (exp.content[0] === '_') {
// internally generated string constant references
// e.g. imported URL strings via compiler-sfc transformAssetUrl plugin
res += ` ${(p.arg as SimpleExpressionNode).content}="__VUE_EXP_START__${
exp.content
}__VUE_EXP_END__"`
continue
}
// constant v-bind, e.g. :foo="1" // constant v-bind, e.g. :foo="1"
let evaluated = evaluateConstant(p.exp as SimpleExpressionNode) let evaluated = evaluateConstant(exp)
if (evaluated != null) { if (evaluated != null) {
const arg = p.arg && (p.arg as SimpleExpressionNode).content const arg = p.arg && (p.arg as SimpleExpressionNode).content
if (arg === 'class') { if (arg === 'class') {

View File

@ -74,6 +74,22 @@ export function render(_ctx, _cache) {
}" }"
`; `;
exports[`compiler sfc: transform asset url transform with stringify 1`] = `
"import { createElementVNode as _createElementVNode, createStaticVNode as _createStaticVNode, openBlock as _openBlock, createElementBlock as _createElementBlock } from \\"vue\\"
import _imports_0 from './bar.png'
import _imports_1 from '/bar.png'
const _hoisted_1 = /*#__PURE__*/_createStaticVNode(\\"<img src=\\\\\\"\\" + _imports_0 + \\"\\\\\\"><img src=\\\\\\"\\" + _imports_1 + \\"\\\\\\"><img src=\\\\\\"https://foo.bar/baz.png\\\\\\"><img src=\\\\\\"//foo.bar/baz.png\\\\\\"><img src=\\\\\\"\\" + _imports_0 + \\"\\\\\\">\\", 5)
const _hoisted_6 = [
_hoisted_1
]
export function render(_ctx, _cache) {
return (_openBlock(), _createElementBlock(\\"div\\", null, _hoisted_6))
}"
`;
exports[`compiler sfc: transform asset url with explicit base 1`] = ` exports[`compiler sfc: transform asset url with explicit base 1`] = `
"import { createElementVNode as _createElementVNode, Fragment as _Fragment, openBlock as _openBlock, createElementBlock as _createElementBlock } from \\"vue\\" "import { createElementVNode as _createElementVNode, Fragment as _Fragment, openBlock as _openBlock, createElementBlock as _createElementBlock } from \\"vue\\"
import _imports_0 from 'bar.png' import _imports_0 from 'bar.png'

View File

@ -194,3 +194,28 @@ export function render(_ctx, _cache) {
], 64 /* STABLE_FRAGMENT */)) ], 64 /* STABLE_FRAGMENT */))
}" }"
`; `;
exports[`compiler sfc: transform srcset transform srcset w/ stringify 1`] = `
"import { createElementVNode as _createElementVNode, createStaticVNode as _createStaticVNode, openBlock as _openBlock, createElementBlock as _createElementBlock } from \\"vue\\"
import _imports_0 from './logo.png'
import _imports_1 from '/logo.png'
const _hoisted_1 = _imports_0
const _hoisted_2 = _imports_0 + ' 2x'
const _hoisted_3 = _imports_0 + ' 2x'
const _hoisted_4 = _imports_0 + ', ' + _imports_0 + ' 2x'
const _hoisted_5 = _imports_0 + ' 2x, ' + _imports_0
const _hoisted_6 = _imports_0 + ' 2x, ' + _imports_0 + ' 3x'
const _hoisted_7 = _imports_0 + ', ' + _imports_0 + ' 2x, ' + _imports_0 + ' 3x'
const _hoisted_8 = _imports_1 + ', ' + _imports_1 + ' 2x'
const _hoisted_9 = _imports_1 + ', ' + _imports_0 + ' 2x'
const _hoisted_10 = /*#__PURE__*/_createStaticVNode(\\"<img src=\\\\\\"./logo.png\\\\\\" srcset=\\\\\\"\\\\\\"><img src=\\\\\\"./logo.png\\\\\\" srcset=\\\\\\"\\" + _hoisted_1 + \\"\\\\\\"><img src=\\\\\\"./logo.png\\\\\\" srcset=\\\\\\"\\" + _hoisted_2 + \\"\\\\\\"><img src=\\\\\\"./logo.png\\\\\\" srcset=\\\\\\"\\" + _hoisted_3 + \\"\\\\\\"><img src=\\\\\\"./logo.png\\\\\\" srcset=\\\\\\"\\" + _hoisted_4 + \\"\\\\\\"><img src=\\\\\\"./logo.png\\\\\\" srcset=\\\\\\"\\" + _hoisted_5 + \\"\\\\\\"><img src=\\\\\\"./logo.png\\\\\\" srcset=\\\\\\"\\" + _hoisted_6 + \\"\\\\\\"><img src=\\\\\\"./logo.png\\\\\\" srcset=\\\\\\"\\" + _hoisted_7 + \\"\\\\\\"><img src=\\\\\\"/logo.png\\\\\\" srcset=\\\\\\"\\" + _hoisted_8 + \\"\\\\\\"><img src=\\\\\\"https://example.com/logo.png\\\\\\" srcset=\\\\\\"https://example.com/logo.png, https://example.com/logo.png 2x\\\\\\"><img src=\\\\\\"/logo.png\\\\\\" srcset=\\\\\\"\\" + _hoisted_9 + \\"\\\\\\"><img src=\\\\\\"data:image/png;base64,i\\\\\\" srcset=\\\\\\"data:image/png;base64,i 1x, data:image/png;base64,i 2x\\\\\\">\\", 12)
const _hoisted_22 = [
_hoisted_10
]
export function render(_ctx, _cache) {
return (_openBlock(), _createElementBlock(\\"div\\", null, _hoisted_22))
}"
`;

View File

@ -1,4 +1,9 @@
import { generate, baseParse, transform } from '@vue/compiler-core' import {
generate,
baseParse,
transform,
TransformOptions
} from '@vue/compiler-core'
import { import {
transformAssetUrl, transformAssetUrl,
createAssetUrlTransformWithOptions, createAssetUrlTransformWithOptions,
@ -7,8 +12,13 @@ import {
} from '../src/templateTransformAssetUrl' } from '../src/templateTransformAssetUrl'
import { transformElement } from '../../compiler-core/src/transforms/transformElement' import { transformElement } from '../../compiler-core/src/transforms/transformElement'
import { transformBind } from '../../compiler-core/src/transforms/vBind' import { transformBind } from '../../compiler-core/src/transforms/vBind'
import { stringifyStatic } from '../../compiler-dom/src/transforms/stringifyStatic'
function compileWithAssetUrls(template: string, options?: AssetURLOptions) { function compileWithAssetUrls(
template: string,
options?: AssetURLOptions,
transformOptions?: TransformOptions
) {
const ast = baseParse(template) const ast = baseParse(template)
const t = options const t = options
? createAssetUrlTransformWithOptions(normalizeOptions(options)) ? createAssetUrlTransformWithOptions(normalizeOptions(options))
@ -17,7 +27,8 @@ function compileWithAssetUrls(template: string, options?: AssetURLOptions) {
nodeTransforms: [t, transformElement], nodeTransforms: [t, transformElement],
directiveTransforms: { directiveTransforms: {
bind: transformBind bind: transformBind
} },
...transformOptions
}) })
return generate(ast, { mode: 'module' }) return generate(ast, { mode: 'module' })
} }
@ -131,4 +142,26 @@ describe('compiler sfc: transform asset url', () => {
expect(code).toMatchSnapshot() expect(code).toMatchSnapshot()
}) })
test('transform with stringify', () => {
const { code } = compileWithAssetUrls(
`<div>` +
`<img src="./bar.png"/>` +
`<img src="/bar.png"/>` +
`<img src="https://foo.bar/baz.png"/>` +
`<img src="//foo.bar/baz.png"/>` +
`<img src="./bar.png"/>` +
`</div>`,
{
includeAbsolute: true
},
{
hoistStatic: true,
transformHoist: stringifyStatic
}
)
console.log(code)
expect(code).toMatch(`_createStaticVNode`)
expect(code).toMatchSnapshot()
})
}) })

View File

@ -1,4 +1,9 @@
import { generate, baseParse, transform } from '@vue/compiler-core' import {
generate,
baseParse,
transform,
TransformOptions
} from '@vue/compiler-core'
import { import {
transformSrcset, transformSrcset,
createSrcsetTransformWithOptions createSrcsetTransformWithOptions
@ -9,8 +14,13 @@ import {
AssetURLOptions, AssetURLOptions,
normalizeOptions normalizeOptions
} from '../src/templateTransformAssetUrl' } from '../src/templateTransformAssetUrl'
import { stringifyStatic } from '../../compiler-dom/src/transforms/stringifyStatic'
function compileWithSrcset(template: string, options?: AssetURLOptions) { function compileWithSrcset(
template: string,
options?: AssetURLOptions,
transformOptions?: TransformOptions
) {
const ast = baseParse(template) const ast = baseParse(template)
const srcsetTransform = options const srcsetTransform = options
? createSrcsetTransformWithOptions(normalizeOptions(options)) ? createSrcsetTransformWithOptions(normalizeOptions(options))
@ -19,7 +29,8 @@ function compileWithSrcset(template: string, options?: AssetURLOptions) {
nodeTransforms: [srcsetTransform, transformElement], nodeTransforms: [srcsetTransform, transformElement],
directiveTransforms: { directiveTransforms: {
bind: transformBind bind: transformBind
} },
...transformOptions
}) })
return generate(ast, { mode: 'module' }) return generate(ast, { mode: 'module' })
} }
@ -59,4 +70,19 @@ describe('compiler sfc: transform srcset', () => {
}).code }).code
).toMatchSnapshot() ).toMatchSnapshot()
}) })
test('transform srcset w/ stringify', () => {
const code = compileWithSrcset(
`<div>${src}</div>`,
{
includeAbsolute: true
},
{
hoistStatic: true,
transformHoist: stringifyStatic
}
).code
expect(code).toMatch(`_createStaticVNode`)
expect(code).toMatchSnapshot()
})
}) })

View File

@ -162,7 +162,12 @@ function getImportsExpressionExp(
exp = context.imports[existingIndex].exp as SimpleExpressionNode exp = context.imports[existingIndex].exp as SimpleExpressionNode
} else { } else {
name = `_imports_${context.imports.length}` name = `_imports_${context.imports.length}`
exp = createSimpleExpression(name, false, loc, ConstantTypes.CAN_HOIST) exp = createSimpleExpression(
name,
false,
loc,
ConstantTypes.CAN_STRINGIFY
)
context.imports.push({ exp, path }) context.imports.push({ exp, path })
} }
@ -184,13 +189,13 @@ function getImportsExpressionExp(
`_hoisted_${existingHoistIndex + 1}`, `_hoisted_${existingHoistIndex + 1}`,
false, false,
loc, loc,
ConstantTypes.CAN_HOIST ConstantTypes.CAN_STRINGIFY
) )
} }
return context.hoist( return context.hoist(
createSimpleExpression(hashExp, false, loc, ConstantTypes.CAN_HOIST) createSimpleExpression(hashExp, false, loc, ConstantTypes.CAN_STRINGIFY)
) )
} else { } else {
return createSimpleExpression(`''`, false, loc, ConstantTypes.CAN_HOIST) return createSimpleExpression(`''`, false, loc, ConstantTypes.CAN_STRINGIFY)
} }
} }

View File

@ -113,14 +113,14 @@ export const transformSrcset: NodeTransform = (
`_imports_${existingImportsIndex}`, `_imports_${existingImportsIndex}`,
false, false,
attr.loc, attr.loc,
ConstantTypes.CAN_HOIST ConstantTypes.CAN_STRINGIFY
) )
} else { } else {
exp = createSimpleExpression( exp = createSimpleExpression(
`_imports_${context.imports.length}`, `_imports_${context.imports.length}`,
false, false,
attr.loc, attr.loc,
ConstantTypes.CAN_HOIST ConstantTypes.CAN_STRINGIFY
) )
context.imports.push({ exp, path }) context.imports.push({ exp, path })
} }
@ -131,7 +131,7 @@ export const transformSrcset: NodeTransform = (
`"${url}"`, `"${url}"`,
false, false,
attr.loc, attr.loc,
ConstantTypes.CAN_HOIST ConstantTypes.CAN_STRINGIFY
) )
compoundExpression.children.push(exp) compoundExpression.children.push(exp)
} }
@ -146,7 +146,7 @@ export const transformSrcset: NodeTransform = (
}) })
const hoisted = context.hoist(compoundExpression) const hoisted = context.hoist(compoundExpression)
hoisted.constType = ConstantTypes.CAN_HOIST hoisted.constType = ConstantTypes.CAN_STRINGIFY
node.props[index] = { node.props[index] = {
type: NodeTypes.DIRECTIVE, type: NodeTypes.DIRECTIVE,