refactor(compile): add independent transform for VBindShorthand

This commit is contained in:
daiwei 2025-06-05 14:52:12 +08:00
parent a47832e75e
commit fd7d6cf67e
12 changed files with 117 additions and 47 deletions

View File

@ -17,6 +17,7 @@ import {
helperNameMap, helperNameMap,
} from '../../src/runtimeHelpers' } from '../../src/runtimeHelpers'
import { transformExpression } from '../../src/transforms/transformExpression' import { transformExpression } from '../../src/transforms/transformExpression'
import { transformVBindShorthand } from '../../src/transforms/transformVBindShorthand'
function parseWithVBind( function parseWithVBind(
template: string, template: string,
@ -25,6 +26,7 @@ function parseWithVBind(
const ast = parse(template) const ast = parse(template)
transform(ast, { transform(ast, {
nodeTransforms: [ nodeTransforms: [
transformVBindShorthand,
...(options.prefixIdentifiers ? [transformExpression] : []), ...(options.prefixIdentifiers ? [transformExpression] : []),
transformElement, transformElement,
], ],

View File

@ -21,6 +21,7 @@ import { type CompilerOptions, generate } from '../../src'
import { FRAGMENT, RENDER_LIST, RENDER_SLOT } from '../../src/runtimeHelpers' import { FRAGMENT, RENDER_LIST, RENDER_SLOT } from '../../src/runtimeHelpers'
import { PatchFlags } from '@vue/shared' import { PatchFlags } from '@vue/shared'
import { createObjectMatcher } from '../testUtils' import { createObjectMatcher } from '../testUtils'
import { transformVBindShorthand } from '../../src/transforms/transformVBindShorthand'
export function parseWithForTransform( export function parseWithForTransform(
template: string, template: string,
@ -32,6 +33,7 @@ export function parseWithForTransform(
const ast = parse(template, options) const ast = parse(template, options)
transform(ast, { transform(ast, {
nodeTransforms: [ nodeTransforms: [
transformVBindShorthand,
transformIf, transformIf,
transformFor, transformFor,
...(options.prefixIdentifiers ? [transformExpression] : []), ...(options.prefixIdentifiers ? [transformExpression] : []),

View File

@ -17,7 +17,12 @@ import {
type VNodeCall, type VNodeCall,
} from '../../src/ast' } from '../../src/ast'
import { ErrorCodes } from '../../src/errors' import { ErrorCodes } from '../../src/errors'
import { type CompilerOptions, TO_HANDLERS, generate } from '../../src' import {
type CompilerOptions,
TO_HANDLERS,
generate,
transformVBindShorthand,
} from '../../src'
import { import {
CREATE_COMMENT, CREATE_COMMENT,
FRAGMENT, FRAGMENT,
@ -35,7 +40,12 @@ function parseWithIfTransform(
) { ) {
const ast = parse(template, options) const ast = parse(template, options)
transform(ast, { transform(ast, {
nodeTransforms: [transformIf, transformSlotOutlet, transformElement], nodeTransforms: [
transformVBindShorthand,
transformIf,
transformSlotOutlet,
transformElement,
],
...options, ...options,
}) })
if (!options.onError) { if (!options.onError) {
@ -209,6 +219,16 @@ describe('compiler: v-if', () => {
content: `_ctx.ok`, content: `_ctx.ok`,
}) })
}) })
//#11321
test('v-if + :key shorthand', () => {
const { node } = parseWithIfTransform(`<div v-if="ok" :key></div>`)
expect(node.type).toBe(NodeTypes.IF)
expect(node.branches[0].userKey).toMatchObject({
arg: { content: 'key' },
exp: { content: 'key' },
})
})
}) })
describe('errors', () => { describe('errors', () => {

View File

@ -22,6 +22,7 @@ import { transformModel } from './transforms/vModel'
import { transformFilter } from './compat/transformFilter' import { transformFilter } from './compat/transformFilter'
import { ErrorCodes, createCompilerError, defaultOnError } from './errors' import { ErrorCodes, createCompilerError, defaultOnError } from './errors'
import { transformMemo } from './transforms/vMemo' import { transformMemo } from './transforms/vMemo'
import { transformVBindShorthand } from './transforms/transformVBindShorthand'
export type TransformPreset = [ export type TransformPreset = [
NodeTransform[], NodeTransform[],
@ -33,6 +34,7 @@ export function getBaseTransformPreset(
): TransformPreset { ): TransformPreset {
return [ return [
[ [
transformVBindShorthand,
transformOnce, transformOnce,
transformIf, transformIf,
transformMemo, transformMemo,

View File

@ -66,6 +66,7 @@ export {
buildDirectiveArgs, buildDirectiveArgs,
type PropsExpression, type PropsExpression,
} from './transforms/transformElement' } from './transforms/transformElement'
export { transformVBindShorthand } from './transforms/transformVBindShorthand'
export { processSlotOutlet } from './transforms/transformSlotOutlet' export { processSlotOutlet } from './transforms/transformSlotOutlet'
export { getConstantType } from './transforms/cacheStatic' export { getConstantType } from './transforms/cacheStatic'
export { generateCodeFrame } from '@vue/shared' export { generateCodeFrame } from '@vue/shared'

View File

@ -0,0 +1,36 @@
import { camelize } from '@vue/shared'
import {
NodeTypes,
type SimpleExpressionNode,
createSimpleExpression,
} from '../ast'
import type { NodeTransform } from '../transform'
import { ErrorCodes, createCompilerError } from '../errors'
export const transformVBindShorthand: NodeTransform = (node, context) => {
if (node.type === NodeTypes.ELEMENT) {
for (const prop of node.props) {
// same-name shorthand - :arg is expanded to :arg="arg"
if (
prop.type === NodeTypes.DIRECTIVE &&
prop.name === 'bind' &&
!prop.exp
) {
const arg = prop.arg!
if (arg.type !== NodeTypes.SIMPLE_EXPRESSION || !arg.isStatic) {
// only simple expression is allowed for same-name shorthand
context.onError(
createCompilerError(
ErrorCodes.X_V_BIND_INVALID_SAME_NAME_ARGUMENT,
arg.loc,
),
)
prop.exp = createSimpleExpression('', true, arg.loc)
} else {
const propName = camelize((arg as SimpleExpressionNode).content)
prop.exp = createSimpleExpression(propName, false, arg.loc)
}
}
}
}
}

View File

@ -1,16 +1,13 @@
import type { DirectiveTransform, TransformContext } from '../transform' import type { DirectiveTransform } from '../transform'
import { import {
type DirectiveNode,
type ExpressionNode, type ExpressionNode,
NodeTypes, NodeTypes,
type SimpleExpressionNode,
createObjectProperty, createObjectProperty,
createSimpleExpression, createSimpleExpression,
} from '../ast' } from '../ast'
import { ErrorCodes, createCompilerError } from '../errors' import { ErrorCodes, createCompilerError } from '../errors'
import { camelize } from '@vue/shared' import { camelize } from '@vue/shared'
import { CAMELIZE } from '../runtimeHelpers' import { CAMELIZE } from '../runtimeHelpers'
import { processExpression } from './transformExpression'
// v-bind without arg is handled directly in ./transformElement.ts due to its affecting // v-bind without arg is handled directly in ./transformElement.ts due to its affecting
// codegen for the entire props object. This transform here is only for v-bind // codegen for the entire props object. This transform here is only for v-bind
@ -40,27 +37,6 @@ export const transformBind: DirectiveTransform = (dir, _node, context) => {
} }
} }
// same-name shorthand - :arg is expanded to :arg="arg"
if (!exp) {
if (arg.type !== NodeTypes.SIMPLE_EXPRESSION || !arg.isStatic) {
// only simple expression is allowed for same-name shorthand
context.onError(
createCompilerError(
ErrorCodes.X_V_BIND_INVALID_SAME_NAME_ARGUMENT,
arg.loc,
),
)
return {
props: [
createObjectProperty(arg, createSimpleExpression('', true, loc)),
],
}
}
transformBindShorthand(dir, context)
exp = dir.exp!
}
if (arg.type !== NodeTypes.SIMPLE_EXPRESSION) { if (arg.type !== NodeTypes.SIMPLE_EXPRESSION) {
arg.children.unshift(`(`) arg.children.unshift(`(`)
arg.children.push(`) || ""`) arg.children.push(`) || ""`)
@ -92,20 +68,7 @@ export const transformBind: DirectiveTransform = (dir, _node, context) => {
} }
return { return {
props: [createObjectProperty(arg, exp)], props: [createObjectProperty(arg, exp!)],
}
}
export const transformBindShorthand = (
dir: DirectiveNode,
context: TransformContext,
): void => {
const arg = dir.arg!
const propName = camelize((arg as SimpleExpressionNode).content)
dir.exp = createSimpleExpression(propName, false, arg.loc)
if (!__BROWSER__) {
dir.exp = processExpression(dir.exp, context)
} }
} }

View File

@ -48,7 +48,6 @@ import {
import { processExpression } from './transformExpression' import { processExpression } from './transformExpression'
import { validateBrowserExpression } from '../validateExpression' import { validateBrowserExpression } from '../validateExpression'
import { PatchFlags } from '@vue/shared' import { PatchFlags } from '@vue/shared'
import { transformBindShorthand } from './vBind'
export const transformFor: NodeTransform = createStructuralDirectiveTransform( export const transformFor: NodeTransform = createStructuralDirectiveTransform(
'for', 'for',
@ -64,10 +63,6 @@ export const transformFor: NodeTransform = createStructuralDirectiveTransform(
const memo = findDir(node, 'memo') const memo = findDir(node, 'memo')
const keyProp = findProp(node, `key`, false, true) const keyProp = findProp(node, `key`, false, true)
const isDirKey = keyProp && keyProp.type === NodeTypes.DIRECTIVE const isDirKey = keyProp && keyProp.type === NodeTypes.DIRECTIVE
if (isDirKey && !keyProp.exp) {
// resolve :key shorthand #10882
transformBindShorthand(keyProp, context)
}
let keyExp = let keyExp =
keyProp && keyProp &&
(keyProp.type === NodeTypes.ATTRIBUTE (keyProp.type === NodeTypes.ATTRIBUTE

View File

@ -48,6 +48,22 @@ return function render(_ctx, _cache) {
}" }"
`; `;
exports[`compiler: transform v-model > input with v-bind shorthand type after v-model should use dynamic model 1`] = `
"const _Vue = Vue
return function render(_ctx, _cache) {
with (_ctx) {
const { vModelDynamic: _vModelDynamic, withDirectives: _withDirectives, openBlock: _openBlock, createElementBlock: _createElementBlock } = _Vue
return _withDirectives((_openBlock(), _createElementBlock("input", {
"onUpdate:modelValue": $event => ((model) = $event)
}, null, 8 /* PROPS */, ["onUpdate:modelValue"])), [
[_vModelDynamic, model]
])
}
}"
`;
exports[`compiler: transform v-model > modifiers > .lazy 1`] = ` exports[`compiler: transform v-model > modifiers > .lazy 1`] = `
"const _Vue = Vue "const _Vue = Vue

View File

@ -3,6 +3,7 @@ import {
generate, generate,
baseParse as parse, baseParse as parse,
transform, transform,
transformVBindShorthand,
} from '@vue/compiler-core' } from '@vue/compiler-core'
import { transformModel } from '../../src/transforms/vModel' import { transformModel } from '../../src/transforms/vModel'
import { transformElement } from '../../../compiler-core/src/transforms/transformElement' import { transformElement } from '../../../compiler-core/src/transforms/transformElement'
@ -18,7 +19,7 @@ import {
function transformWithModel(template: string, options: CompilerOptions = {}) { function transformWithModel(template: string, options: CompilerOptions = {}) {
const ast = parse(template) const ast = parse(template)
transform(ast, { transform(ast, {
nodeTransforms: [transformElement], nodeTransforms: [transformVBindShorthand, transformElement],
directiveTransforms: { directiveTransforms: {
model: transformModel, model: transformModel,
}, },
@ -63,6 +64,14 @@ describe('compiler: transform v-model', () => {
expect(generate(root).code).toMatchSnapshot() expect(generate(root).code).toMatchSnapshot()
}) })
// #13169
test('input with v-bind shorthand type after v-model should use dynamic model', () => {
const root = transformWithModel('<input v-model="model" :type/>')
expect(root.helpers).toContain(V_MODEL_DYNAMIC)
expect(generate(root).code).toMatchSnapshot()
})
test('input w/ dynamic v-bind', () => { test('input w/ dynamic v-bind', () => {
const root = transformWithModel('<input v-bind="obj" v-model="model" />') const root = transformWithModel('<input v-bind="obj" v-model="model" />')

View File

@ -101,6 +101,28 @@ describe('transition-group', () => {
`) `)
}) })
test('with dynamic tag shorthand', () => {
expect(
compile(
`<transition-group :tag><div v-for="i in list"/></transition-group>`,
).code,
).toMatchInlineSnapshot(`
"const { ssrRenderAttrs: _ssrRenderAttrs, ssrRenderList: _ssrRenderList } = require("vue/server-renderer")
return function ssrRender(_ctx, _push, _parent, _attrs) {
_push(\`<\${
_ctx.tag
}\${
_ssrRenderAttrs(_attrs)
}>\`)
_ssrRenderList(_ctx.list, (i) => {
_push(\`<div></div>\`)
})
_push(\`</\${_ctx.tag}>\`)
}"
`)
})
test('with multi fragments children', () => { test('with multi fragments children', () => {
expect( expect(
compile( compile(

View File

@ -13,6 +13,7 @@ import {
transformExpression, transformExpression,
transformOn, transformOn,
transformStyle, transformStyle,
transformVBindShorthand,
} from '@vue/compiler-dom' } from '@vue/compiler-dom'
import { ssrCodegenTransform } from './ssrCodegenTransform' import { ssrCodegenTransform } from './ssrCodegenTransform'
import { ssrTransformElement } from './transforms/ssrTransformElement' import { ssrTransformElement } from './transforms/ssrTransformElement'
@ -55,6 +56,7 @@ export function compile(
...options, ...options,
hoistStatic: false, hoistStatic: false,
nodeTransforms: [ nodeTransforms: [
transformVBindShorthand,
ssrTransformIf, ssrTransformIf,
ssrTransformFor, ssrTransformFor,
trackVForSlotScopes, trackVForSlotScopes,