diff --git a/packages/compiler-core/__tests__/transforms/vBind.spec.ts b/packages/compiler-core/__tests__/transforms/vBind.spec.ts index be063b8a9d5..2221d926e52 100644 --- a/packages/compiler-core/__tests__/transforms/vBind.spec.ts +++ b/packages/compiler-core/__tests__/transforms/vBind.spec.ts @@ -17,6 +17,7 @@ import { helperNameMap, } from '../../src/runtimeHelpers' import { transformExpression } from '../../src/transforms/transformExpression' +import { transformVBindShorthand } from '../../src/transforms/transformVBindShorthand' function parseWithVBind( template: string, @@ -25,6 +26,7 @@ function parseWithVBind( const ast = parse(template) transform(ast, { nodeTransforms: [ + transformVBindShorthand, ...(options.prefixIdentifiers ? [transformExpression] : []), transformElement, ], diff --git a/packages/compiler-core/__tests__/transforms/vFor.spec.ts b/packages/compiler-core/__tests__/transforms/vFor.spec.ts index fead2476ac5..db872be460c 100644 --- a/packages/compiler-core/__tests__/transforms/vFor.spec.ts +++ b/packages/compiler-core/__tests__/transforms/vFor.spec.ts @@ -21,6 +21,7 @@ import { type CompilerOptions, generate } from '../../src' import { FRAGMENT, RENDER_LIST, RENDER_SLOT } from '../../src/runtimeHelpers' import { PatchFlags } from '@vue/shared' import { createObjectMatcher } from '../testUtils' +import { transformVBindShorthand } from '../../src/transforms/transformVBindShorthand' export function parseWithForTransform( template: string, @@ -32,6 +33,7 @@ export function parseWithForTransform( const ast = parse(template, options) transform(ast, { nodeTransforms: [ + transformVBindShorthand, transformIf, transformFor, ...(options.prefixIdentifiers ? [transformExpression] : []), diff --git a/packages/compiler-core/__tests__/transforms/vIf.spec.ts b/packages/compiler-core/__tests__/transforms/vIf.spec.ts index 2c2fedab0d5..734c0cf22b2 100644 --- a/packages/compiler-core/__tests__/transforms/vIf.spec.ts +++ b/packages/compiler-core/__tests__/transforms/vIf.spec.ts @@ -17,7 +17,12 @@ import { type VNodeCall, } from '../../src/ast' import { ErrorCodes } from '../../src/errors' -import { type CompilerOptions, TO_HANDLERS, generate } from '../../src' +import { + type CompilerOptions, + TO_HANDLERS, + generate, + transformVBindShorthand, +} from '../../src' import { CREATE_COMMENT, FRAGMENT, @@ -35,7 +40,12 @@ function parseWithIfTransform( ) { const ast = parse(template, options) transform(ast, { - nodeTransforms: [transformIf, transformSlotOutlet, transformElement], + nodeTransforms: [ + transformVBindShorthand, + transformIf, + transformSlotOutlet, + transformElement, + ], ...options, }) if (!options.onError) { @@ -209,6 +219,16 @@ describe('compiler: v-if', () => { content: `_ctx.ok`, }) }) + + //#11321 + test('v-if + :key shorthand', () => { + const { node } = parseWithIfTransform(`
`) + expect(node.type).toBe(NodeTypes.IF) + expect(node.branches[0].userKey).toMatchObject({ + arg: { content: 'key' }, + exp: { content: 'key' }, + }) + }) }) describe('errors', () => { diff --git a/packages/compiler-core/src/compile.ts b/packages/compiler-core/src/compile.ts index a697c9d22e6..eba00427289 100644 --- a/packages/compiler-core/src/compile.ts +++ b/packages/compiler-core/src/compile.ts @@ -22,6 +22,7 @@ import { transformModel } from './transforms/vModel' import { transformFilter } from './compat/transformFilter' import { ErrorCodes, createCompilerError, defaultOnError } from './errors' import { transformMemo } from './transforms/vMemo' +import { transformVBindShorthand } from './transforms/transformVBindShorthand' export type TransformPreset = [ NodeTransform[], @@ -33,6 +34,7 @@ export function getBaseTransformPreset( ): TransformPreset { return [ [ + transformVBindShorthand, transformOnce, transformIf, transformMemo, diff --git a/packages/compiler-core/src/index.ts b/packages/compiler-core/src/index.ts index 29e5f681300..f3e6454bd6e 100644 --- a/packages/compiler-core/src/index.ts +++ b/packages/compiler-core/src/index.ts @@ -66,6 +66,7 @@ export { buildDirectiveArgs, type PropsExpression, } from './transforms/transformElement' +export { transformVBindShorthand } from './transforms/transformVBindShorthand' export { processSlotOutlet } from './transforms/transformSlotOutlet' export { getConstantType } from './transforms/cacheStatic' export { generateCodeFrame } from '@vue/shared' diff --git a/packages/compiler-core/src/transforms/transformVBindShorthand.ts b/packages/compiler-core/src/transforms/transformVBindShorthand.ts new file mode 100644 index 00000000000..a2e5e9f9339 --- /dev/null +++ b/packages/compiler-core/src/transforms/transformVBindShorthand.ts @@ -0,0 +1,39 @@ +import { camelize } from '@vue/shared' +import { + NodeTypes, + type SimpleExpressionNode, + createSimpleExpression, +} from '../ast' +import type { NodeTransform } from '../transform' +import { ErrorCodes, createCompilerError } from '../errors' +import { validFirstIdentCharRE } from '../utils' + +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) + if (validFirstIdentCharRE.test(propName[0])) { + prop.exp = createSimpleExpression(propName, false, arg.loc) + } + } + } + } + } +} diff --git a/packages/compiler-core/src/transforms/vBind.ts b/packages/compiler-core/src/transforms/vBind.ts index 1e5e371418b..6977b21efe6 100644 --- a/packages/compiler-core/src/transforms/vBind.ts +++ b/packages/compiler-core/src/transforms/vBind.ts @@ -1,16 +1,13 @@ -import type { DirectiveTransform, TransformContext } from '../transform' +import type { DirectiveTransform } from '../transform' import { - type DirectiveNode, type ExpressionNode, NodeTypes, - type SimpleExpressionNode, createObjectProperty, createSimpleExpression, } from '../ast' import { ErrorCodes, createCompilerError } from '../errors' import { camelize } from '@vue/shared' import { CAMELIZE } from '../runtimeHelpers' -import { processExpression } from './transformExpression' // 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 @@ -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) { arg.children.unshift(`(`) arg.children.push(`) || ""`) @@ -92,20 +68,7 @@ export const transformBind: DirectiveTransform = (dir, _node, context) => { } return { - 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) + props: [createObjectProperty(arg, exp!)], } } diff --git a/packages/compiler-core/src/transforms/vFor.ts b/packages/compiler-core/src/transforms/vFor.ts index 0dca0ba9ab4..3e428528654 100644 --- a/packages/compiler-core/src/transforms/vFor.ts +++ b/packages/compiler-core/src/transforms/vFor.ts @@ -48,7 +48,6 @@ import { import { processExpression } from './transformExpression' import { validateBrowserExpression } from '../validateExpression' import { PatchFlags } from '@vue/shared' -import { transformBindShorthand } from './vBind' export const transformFor: NodeTransform = createStructuralDirectiveTransform( 'for', @@ -64,10 +63,6 @@ export const transformFor: NodeTransform = createStructuralDirectiveTransform( const memo = findDir(node, 'memo') const keyProp = findProp(node, `key`, false, true) const isDirKey = keyProp && keyProp.type === NodeTypes.DIRECTIVE - if (isDirKey && !keyProp.exp) { - // resolve :key shorthand #10882 - transformBindShorthand(keyProp, context) - } let keyExp = keyProp && (keyProp.type === NodeTypes.ATTRIBUTE diff --git a/packages/compiler-core/src/utils.ts b/packages/compiler-core/src/utils.ts index b49d70bb2fb..d2278992f85 100644 --- a/packages/compiler-core/src/utils.ts +++ b/packages/compiler-core/src/utils.ts @@ -74,7 +74,7 @@ enum MemberExpLexState { inString, } -const validFirstIdentCharRE = /[A-Za-z_$\xA0-\uFFFF]/ +export const validFirstIdentCharRE: RegExp = /[A-Za-z_$\xA0-\uFFFF]/ const validIdentCharRE = /[\.\?\w$\xA0-\uFFFF]/ const whitespaceRE = /\s+[.[]\s*|\s*[.[]\s+/g diff --git a/packages/compiler-dom/__tests__/transforms/__snapshots__/vModel.spec.ts.snap b/packages/compiler-dom/__tests__/transforms/__snapshots__/vModel.spec.ts.snap index 5b610745e41..b357ac15a1b 100644 --- a/packages/compiler-dom/__tests__/transforms/__snapshots__/vModel.spec.ts.snap +++ b/packages/compiler-dom/__tests__/transforms/__snapshots__/vModel.spec.ts.snap @@ -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`] = ` "const _Vue = Vue diff --git a/packages/compiler-dom/__tests__/transforms/vModel.spec.ts b/packages/compiler-dom/__tests__/transforms/vModel.spec.ts index 02d188f01b9..6fe39900ca8 100644 --- a/packages/compiler-dom/__tests__/transforms/vModel.spec.ts +++ b/packages/compiler-dom/__tests__/transforms/vModel.spec.ts @@ -3,6 +3,7 @@ import { generate, baseParse as parse, transform, + transformVBindShorthand, } from '@vue/compiler-core' import { transformModel } from '../../src/transforms/vModel' import { transformElement } from '../../../compiler-core/src/transforms/transformElement' @@ -18,7 +19,7 @@ import { function transformWithModel(template: string, options: CompilerOptions = {}) { const ast = parse(template) transform(ast, { - nodeTransforms: [transformElement], + nodeTransforms: [transformVBindShorthand, transformElement], directiveTransforms: { model: transformModel, }, @@ -63,6 +64,14 @@ describe('compiler: transform v-model', () => { expect(generate(root).code).toMatchSnapshot() }) + // #13169 + test('input with v-bind shorthand type after v-model should use dynamic model', () => { + const root = transformWithModel('') + + expect(root.helpers).toContain(V_MODEL_DYNAMIC) + expect(generate(root).code).toMatchSnapshot() + }) + test('input w/ dynamic v-bind', () => { const root = transformWithModel('') diff --git a/packages/compiler-ssr/__tests__/ssrTransitionGroup.spec.ts b/packages/compiler-ssr/__tests__/ssrTransitionGroup.spec.ts index 82122e621c7..abbb5f53e6e 100644 --- a/packages/compiler-ssr/__tests__/ssrTransitionGroup.spec.ts +++ b/packages/compiler-ssr/__tests__/ssrTransitionGroup.spec.ts @@ -101,6 +101,28 @@ describe('transition-group', () => { `) }) + test('with dynamic tag shorthand', () => { + expect( + compile( + `
`, + ).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(\`
\`) + }) + _push(\`\`) + }" + `) + }) + test('with multi fragments children', () => { expect( compile( diff --git a/packages/compiler-ssr/src/index.ts b/packages/compiler-ssr/src/index.ts index f8a686555e8..cc1658d783c 100644 --- a/packages/compiler-ssr/src/index.ts +++ b/packages/compiler-ssr/src/index.ts @@ -13,6 +13,7 @@ import { transformExpression, transformOn, transformStyle, + transformVBindShorthand, } from '@vue/compiler-dom' import { ssrCodegenTransform } from './ssrCodegenTransform' import { ssrTransformElement } from './transforms/ssrTransformElement' @@ -55,6 +56,7 @@ export function compile( ...options, hoistStatic: false, nodeTransforms: [ + transformVBindShorthand, ssrTransformIf, ssrTransformFor, trackVForSlotScopes,