diff --git a/packages/language-core/lib/codegen/template/element.ts b/packages/language-core/lib/codegen/template/element.ts index d82cdb796c..e0c57f02f0 100644 --- a/packages/language-core/lib/codegen/template/element.ts +++ b/packages/language-core/lib/codegen/template/element.ts @@ -9,7 +9,6 @@ import { endOfLine, identifierRegex, newLine } from '../utils'; import { endBoundary, startBoundary } from '../utils/boundary'; import { generateCamelized } from '../utils/camelized'; import type { TemplateCodegenContext } from './context'; -import { generateElementChildren } from './elementChildren'; import { generateElementDirectives } from './elementDirectives'; import { generateElementEvents } from './elementEvents'; import { type FailedPropExpression, generateElementProps } from './elementProps'; @@ -17,6 +16,7 @@ import type { TemplateCodegenOptions } from './index'; import { generateInterpolation } from './interpolation'; import { generatePropertyAccess } from './propertyAccess'; import { collectStyleScopedClassReferences } from './styleScopedClasses'; +import { generateTemplateChild } from './templateChild'; import { generateVSlot } from './vSlot'; const colonReg = /:/g; @@ -344,7 +344,9 @@ export function* generateElement( const { currentComponent } = ctx; ctx.currentComponent = undefined; - yield* generateElementChildren(options, ctx, node.children); + for (const child of node.children) { + yield* generateTemplateChild(options, ctx, child); + } ctx.currentComponent = currentComponent; } diff --git a/packages/language-core/lib/codegen/template/elementChildren.ts b/packages/language-core/lib/codegen/template/elementChildren.ts deleted file mode 100644 index 154a5543a2..0000000000 --- a/packages/language-core/lib/codegen/template/elementChildren.ts +++ /dev/null @@ -1,99 +0,0 @@ -import * as CompilerDOM from '@vue/compiler-dom'; -import type { Code } from '../../types'; -import type { TemplateCodegenContext } from './context'; -import type { TemplateCodegenOptions } from './index'; -import { generateTemplateChild, getVIfNode } from './templateChild'; - -export function* generateElementChildren( - options: TemplateCodegenOptions, - ctx: TemplateCodegenContext, - children: CompilerDOM.TemplateChildNode[], - enterNode = true, -): Generator { - const endScope = ctx.startScope(); - for (let i = 0; i < children.length; i++) { - let current = children[i]; - [current, i] = normalizeIfBranch(children, i); - yield* generateTemplateChild(options, ctx, current, enterNode); - } - yield* endScope(); -} - -function normalizeIfBranch( - children: CompilerDOM.TemplateChildNode[], - start: number, -): [node: typeof children[number], end: number] { - const first = children[start]!; - if (first.type !== CompilerDOM.NodeTypes.ELEMENT) { - return [first, start]; - } - - const ifNode = getVIfNode(first); - if (!ifNode) { - return [first, start]; - } - - let end = start; - let comments: CompilerDOM.CommentNode[] = []; - - for (let i = start + 1; i < children.length; i++) { - const sibling = children[i]!; - if (sibling.type === CompilerDOM.NodeTypes.COMMENT) { - comments.push(sibling); - continue; - } - if (sibling.type === CompilerDOM.NodeTypes.TEXT && !sibling.content.trim()) { - continue; - } - const elseBranch = getVElseDirective(sibling); - if (elseBranch) { - const branchNode: CompilerDOM.ElementNode = { - ...elseBranch.element, - props: elseBranch.element.props.filter(prop => prop !== elseBranch.directive), - }; - - const branch = createIfBranch(branchNode, elseBranch.directive); - if (comments.length) { - branch.children = [...comments, ...branch.children]; - } - - ifNode.branches.push(branch); - comments = []; - end = i; - continue; - } - break; - } - - return [ifNode, end]; -} - -// source: https://github.com/vuejs/core/blob/25ebe3a42cd80ac0256355c2740a0258cdd7419d/packages/compiler-core/src/transforms/vIf.ts#L207 -function createIfBranch(node: CompilerDOM.ElementNode, dir: CompilerDOM.DirectiveNode): CompilerDOM.IfBranchNode { - const isTemplateIf = node.tagType === CompilerDOM.ElementTypes.TEMPLATE; - return { - type: CompilerDOM.NodeTypes.IF_BRANCH, - loc: node.loc, - condition: dir.name === 'else' ? undefined : dir.exp, - children: isTemplateIf && !CompilerDOM.findDir(node, 'for') ? node.children : [node], - userKey: CompilerDOM.findProp(node, 'key'), - isTemplateIf, - }; -} - -function getVElseDirective(node: CompilerDOM.TemplateChildNode) { - if (node.type !== CompilerDOM.NodeTypes.ELEMENT) { - return; - } - const directive = node.props.find( - (prop): prop is CompilerDOM.DirectiveNode => - prop.type === CompilerDOM.NodeTypes.DIRECTIVE - && (prop.name === 'else-if' || prop.name === 'else'), - ); - if (directive) { - return { - element: node, - directive, - }; - } -} diff --git a/packages/language-core/lib/codegen/template/slotOutlet.ts b/packages/language-core/lib/codegen/template/slotOutlet.ts index 67078e6fd6..dd5250d4ae 100644 --- a/packages/language-core/lib/codegen/template/slotOutlet.ts +++ b/packages/language-core/lib/codegen/template/slotOutlet.ts @@ -6,11 +6,11 @@ import { createVBindShorthandInlayHintInfo } from '../inlayHints'; import { endOfLine, newLine } from '../utils'; import { endBoundary, startBoundary } from '../utils/boundary'; import type { TemplateCodegenContext } from './context'; -import { generateElementChildren } from './elementChildren'; import { generateElementProps, generatePropExp } from './elementProps'; import type { TemplateCodegenOptions } from './index'; import { generateInterpolation } from './interpolation'; import { generatePropertyAccess } from './propertyAccess'; +import { generateTemplateChild } from './templateChild'; export function* generateSlotOutlet( options: TemplateCodegenOptions, @@ -148,5 +148,7 @@ export function* generateSlotOutlet( }); } } - yield* generateElementChildren(options, ctx, node.children); + for (const child of node.children) { + yield* generateTemplateChild(options, ctx, child); + } } diff --git a/packages/language-core/lib/codegen/template/templateChild.ts b/packages/language-core/lib/codegen/template/templateChild.ts index 71bc021a2e..ded28342b5 100644 --- a/packages/language-core/lib/codegen/template/templateChild.ts +++ b/packages/language-core/lib/codegen/template/templateChild.ts @@ -5,7 +5,6 @@ import { codeFeatures } from '../codeFeatures'; import { endOfLine } from '../utils'; import type { TemplateCodegenContext } from './context'; import { generateComponent, generateElement } from './element'; -import { generateElementChildren } from './elementChildren'; import type { TemplateCodegenOptions } from './index'; import { generateInterpolation } from './interpolation'; import { generateSlotOutlet } from './slotOutlet'; @@ -13,27 +12,11 @@ import { generateVFor } from './vFor'; import { generateVIf } from './vIf'; import { generateVSlot } from './vSlot'; -// @ts-ignore -const transformContext: CompilerDOM.TransformContext = { - onError: () => {}, - helperString: str => str.toString(), - replaceNode: () => {}, - cacheHandlers: false, - prefixIdentifiers: false, - scopes: { - vFor: 0, - vOnce: 0, - vPre: 0, - vSlot: 0, - }, - expressionPlugins: ['typescript'], -}; - export function* generateTemplateChild( options: TemplateCodegenOptions, ctx: TemplateCodegenContext, node: CompilerDOM.RootNode | CompilerDOM.TemplateChildNode | CompilerDOM.SimpleExpressionNode, - enterNode: boolean = true, + enterNode = true, ): Generator { if (enterNode && !ctx.enter(node)) { return; @@ -48,14 +31,12 @@ export function* generateTemplateChild( for (const item of collectSingleRootNodes(options, node.children)) { ctx.singleRootNodes.add(item); } - yield* generateElementChildren(options, ctx, node.children); + for (const child of node.children) { + yield* generateTemplateChild(options, ctx, child); + } } else if (node.type === CompilerDOM.NodeTypes.ELEMENT) { - const vForNode = getVForNode(node); - if (vForNode) { - yield* generateVFor(options, ctx, vForNode); - } - else if (node.tagType === CompilerDOM.ElementTypes.SLOT) { + if (node.tagType === CompilerDOM.ElementTypes.SLOT) { yield* generateSlotOutlet(options, ctx, node); } else { @@ -159,53 +140,6 @@ function* collectSingleRootNodes( } } -// TODO: track https://github.com/vuejs/vue-next/issues/3498 -export function getVForNode(node: CompilerDOM.ElementNode) { - const forDirective = node.props.find( - (prop): prop is CompilerDOM.DirectiveNode => - prop.type === CompilerDOM.NodeTypes.DIRECTIVE - && prop.name === 'for', - ); - if (forDirective) { - let forNode: CompilerDOM.ForNode | undefined; - CompilerDOM.processFor(node, forDirective, transformContext, _forNode => { - forNode = { ..._forNode }; - return undefined; - }); - if (forNode) { - forNode.children = [{ - ...node, - props: node.props.filter(prop => prop !== forDirective), - }]; - return forNode; - } - } -} - -export function getVIfNode(node: CompilerDOM.ElementNode) { - const ifDirective = node.props.find( - (prop): prop is CompilerDOM.DirectiveNode => - prop.type === CompilerDOM.NodeTypes.DIRECTIVE - && prop.name === 'if', - ); - if (ifDirective) { - let ifNode: CompilerDOM.IfNode | undefined; - CompilerDOM.processIf(node, ifDirective, transformContext, _ifNode => { - ifNode = { ..._ifNode }; - return undefined; - }); - if (ifNode) { - for (const branch of ifNode.branches) { - branch.children = [{ - ...node, - props: node.props.filter(prop => prop !== ifDirective), - }]; - } - return ifNode; - } - } -} - export function parseInterpolationNode(node: CompilerDOM.InterpolationNode, template: string) { let start = node.content.loc.start.offset; let end = node.content.loc.end.offset; diff --git a/packages/language-core/lib/codegen/template/vFor.ts b/packages/language-core/lib/codegen/template/vFor.ts index 36de03b24c..4b9c72ebfa 100644 --- a/packages/language-core/lib/codegen/template/vFor.ts +++ b/packages/language-core/lib/codegen/template/vFor.ts @@ -4,9 +4,9 @@ import { collectBindingNames } from '../../utils/collectBindings'; import { codeFeatures } from '../codeFeatures'; import { endOfLine, getTypeScriptAST, newLine } from '../utils'; import type { TemplateCodegenContext } from './context'; -import { generateElementChildren } from './elementChildren'; import type { TemplateCodegenOptions } from './index'; import { generateInterpolation } from './interpolation'; +import { generateTemplateChild } from './templateChild'; export function* generateVFor( options: TemplateCodegenOptions, @@ -82,7 +82,9 @@ export function* generateVFor( const { inVFor } = ctx; ctx.inVFor = true; - yield* generateElementChildren(options, ctx, node.children, isFragment); + for (const child of node.children) { + yield* generateTemplateChild(options, ctx, child, isFragment); + } ctx.inVFor = inVFor; yield* endScope(); diff --git a/packages/language-core/lib/codegen/template/vIf.ts b/packages/language-core/lib/codegen/template/vIf.ts index 353cc8d478..9270113534 100644 --- a/packages/language-core/lib/codegen/template/vIf.ts +++ b/packages/language-core/lib/codegen/template/vIf.ts @@ -4,9 +4,9 @@ import type { Code } from '../../types'; import { codeFeatures } from '../codeFeatures'; import { newLine } from '../utils'; import type { TemplateCodegenContext } from './context'; -import { generateElementChildren } from './elementChildren'; import type { TemplateCodegenOptions } from './index'; import { generateInterpolation } from './interpolation'; +import { generateTemplateChild } from './templateChild'; export function* generateVIf( options: TemplateCodegenOptions, @@ -52,7 +52,9 @@ export function* generateVIf( } yield `{${newLine}`; - yield* generateElementChildren(options, ctx, branch.children, i !== 0 || isFragment); + for (const child of branch.children) { + yield* generateTemplateChild(options, ctx, child, i !== 0 || isFragment); + } yield `}${newLine}`; if (addedBlockCondition) { diff --git a/packages/language-core/lib/codegen/template/vSlot.ts b/packages/language-core/lib/codegen/template/vSlot.ts index 769fcec777..3d0e7d28c9 100644 --- a/packages/language-core/lib/codegen/template/vSlot.ts +++ b/packages/language-core/lib/codegen/template/vSlot.ts @@ -7,10 +7,10 @@ import { codeFeatures } from '../codeFeatures'; import { endOfLine, getTypeScriptAST, newLine } from '../utils'; import { endBoundary, startBoundary } from '../utils/boundary'; import type { TemplateCodegenContext } from './context'; -import { generateElementChildren } from './elementChildren'; import type { TemplateCodegenOptions } from './index'; import { generateInterpolation } from './interpolation'; import { generateObjectProperty } from './objectProperty'; +import { generateTemplateChild } from './templateChild'; export function* generateVSlot( options: TemplateCodegenOptions, @@ -66,7 +66,9 @@ export function* generateVSlot( yield* generateSlotParameters(options, ctx, slotAst, slotDir.exp, slotVar); ctx.declare(...collectBindingNames(options.ts, slotAst, slotAst)); } - yield* generateElementChildren(options, ctx, node.children); + for (const child of node.children) { + yield* generateTemplateChild(options, ctx, child); + } yield* endScope(); if (slotDir) { diff --git a/packages/language-core/lib/compilerOptions.ts b/packages/language-core/lib/compilerOptions.ts index 6e0b22ceb4..b69f370f20 100644 --- a/packages/language-core/lib/compilerOptions.ts +++ b/packages/language-core/lib/compilerOptions.ts @@ -188,7 +188,7 @@ export class CompilerOptionsResolver { ...defaults.fallthroughComponentNames, ...this.options.fallthroughComponentNames ?? [], ].map(hyphenateTag), - // https://github.com/vuejs/vue-next/blob/master/packages/compiler-dom/src/transforms/vModel.ts#L49-L51 + // https://github.com/vuejs/core/blob/master/packages/compiler-dom/src/transforms/vModel.ts#L49-L51 // https://vuejs.org/guide/essentials/forms.html#form-input-bindings experimentalModelPropName: Object.fromEntries( Object.entries( diff --git a/packages/language-core/lib/virtualCode/ir.ts b/packages/language-core/lib/virtualCode/ir.ts index c20bbd2bf9..25a774d7ad 100644 --- a/packages/language-core/lib/virtualCode/ir.ts +++ b/packages/language-core/lib/virtualCode/ir.ts @@ -4,6 +4,7 @@ import { computed, setActiveSub } from 'alien-signals'; import type * as ts from 'typescript'; import type { Sfc, SfcBlock, SfcBlockAttr, VueLanguagePluginReturn } from '../types'; import { computedArray, computedItems } from '../utils/signals'; +import { normalizeTemplateAST } from './normalize'; export function useIR( ts: typeof import('typescript'), @@ -275,6 +276,7 @@ export function useIR( try { const result = plugin.compileSFCTemplate?.(base.lang, base.content, options); if (result) { + normalizeTemplateAST(result.ast); return { snapshot: getUntrackedSnapshot(), template: base.content, diff --git a/packages/language-core/lib/virtualCode/normalize.ts b/packages/language-core/lib/virtualCode/normalize.ts new file mode 100644 index 0000000000..e378066d98 --- /dev/null +++ b/packages/language-core/lib/virtualCode/normalize.ts @@ -0,0 +1,157 @@ +import * as CompilerDOM from '@vue/compiler-dom'; +import { forEachElementNode } from '../utils/forEachTemplateNode'; + +// See https://github.com/vuejs/core/issues/3498 +export function normalizeTemplateAST(root: CompilerDOM.RootNode) { + // @ts-ignore + const transformContext: CompilerDOM.TransformContext = { + onError: () => {}, + helperString: str => str.toString(), + replaceNode: () => {}, + cacheHandlers: false, + prefixIdentifiers: false, + scopes: { + vFor: 0, + vOnce: 0, + vPre: 0, + vSlot: 0, + }, + expressionPlugins: ['typescript'], + }; + + for (const { children } of forEachElementNode(root)) { + for (let i = 0; i < children.length; i++) { + const child = children[i]!; + if (child.type !== CompilerDOM.NodeTypes.ELEMENT) { + continue; + } + const forNode = getVForNode(child, transformContext); + if (forNode) { + children[i] = forNode; + continue; + } + const ifNode = getVIfNode(child, transformContext); + if (ifNode) { + const normalized = normalizeIfBranch(ifNode, children, i); + children.splice(i, normalized.end - i + 1, normalized.node); + continue; + } + } + } +} + +function normalizeIfBranch( + ifNode: CompilerDOM.IfNode, + children: CompilerDOM.TemplateChildNode[], + start: number, +): { node: typeof children[number]; end: number } { + let end = start; + let comments: CompilerDOM.CommentNode[] = []; + + for (let i = start + 1; i < children.length; i++) { + const sibling = children[i]!; + if (sibling.type === CompilerDOM.NodeTypes.COMMENT) { + comments.push(sibling); + continue; + } + if (sibling.type === CompilerDOM.NodeTypes.TEXT && !sibling.content.trim()) { + continue; + } + const elseBranch = getVElseDirective(sibling); + if (elseBranch) { + const branchNode: CompilerDOM.ElementNode = { + ...elseBranch.element, + props: elseBranch.element.props.filter(prop => prop !== elseBranch.directive), + }; + + const branch = createIfBranch(branchNode, elseBranch.directive); + if (comments.length) { + branch.children = [...comments, ...branch.children]; + } + + ifNode.branches.push(branch); + comments = []; + end = i; + continue; + } + break; + } + + return { node: ifNode, end }; +} + +// source: https://github.com/vuejs/core/blob/25ebe3a42cd80ac0256355c2740a0258cdd7419d/packages/compiler-core/src/transforms/vIf.ts#L207 +function createIfBranch(node: CompilerDOM.ElementNode, dir: CompilerDOM.DirectiveNode): CompilerDOM.IfBranchNode { + const isTemplateIf = node.tagType === CompilerDOM.ElementTypes.TEMPLATE; + return { + type: CompilerDOM.NodeTypes.IF_BRANCH, + loc: node.loc, + condition: dir.name === 'else' ? undefined : dir.exp, + children: isTemplateIf && !CompilerDOM.findDir(node, 'for') ? node.children : [node], + userKey: CompilerDOM.findProp(node, 'key'), + isTemplateIf, + }; +} + +function getVElseDirective(node: CompilerDOM.TemplateChildNode) { + if (node.type !== CompilerDOM.NodeTypes.ELEMENT) { + return; + } + const directive = node.props.find( + (prop): prop is CompilerDOM.DirectiveNode => + prop.type === CompilerDOM.NodeTypes.DIRECTIVE + && (prop.name === 'else-if' || prop.name === 'else'), + ); + if (directive) { + return { + element: node, + directive, + }; + } +} + +function getVForNode(node: CompilerDOM.ElementNode, transformContext: CompilerDOM.TransformContext) { + const forDirective = node.props.find( + (prop): prop is CompilerDOM.DirectiveNode => + prop.type === CompilerDOM.NodeTypes.DIRECTIVE + && prop.name === 'for', + ); + if (forDirective) { + let forNode: CompilerDOM.ForNode | undefined; + CompilerDOM.processFor(node, forDirective, transformContext, _forNode => { + forNode = { ..._forNode }; + return undefined; + }); + if (forNode) { + forNode.children = [{ + ...node, + props: node.props.filter(prop => prop !== forDirective), + }]; + return forNode; + } + } +} + +function getVIfNode(node: CompilerDOM.ElementNode, transformContext: CompilerDOM.TransformContext) { + const ifDirective = node.props.find( + (prop): prop is CompilerDOM.DirectiveNode => + prop.type === CompilerDOM.NodeTypes.DIRECTIVE + && prop.name === 'if', + ); + if (ifDirective) { + let ifNode: CompilerDOM.IfNode | undefined; + CompilerDOM.processIf(node, ifDirective, transformContext, _ifNode => { + ifNode = { ..._ifNode }; + return undefined; + }); + if (ifNode) { + for (const branch of ifNode.branches) { + branch.children = [{ + ...node, + props: node.props.filter(prop => prop !== ifDirective), + }]; + } + return ifNode; + } + } +}