Skip to content

Commit 2034d5c

Browse files
authored
feat(language-core): enhance single root nodes collection (vuejs#4819)
1 parent 41d670d commit 2034d5c

File tree

10 files changed

+80
-18
lines changed

10 files changed

+80
-18
lines changed

packages/language-core/lib/codegen/script/component.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ export function* generateComponent(
4747
if (
4848
options.vueCompilerOptions.target >= 3.5
4949
&& options.vueCompilerOptions.inferComponentDollarEl
50-
&& options.templateCodegen?.singleRootElType
50+
&& options.templateCodegen?.singleRootElTypes.length
5151
) {
5252
yield `__typeEl: {} as __VLS_RootEl,${newLine}`;
5353
}

packages/language-core/lib/codegen/template/context.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -97,8 +97,8 @@ export function createTemplateCodegenContext(options: Pick<TemplateCodegenOption
9797
ctxVar: string;
9898
used: boolean;
9999
} | undefined,
100-
singleRootElType: undefined as string | undefined,
101-
singleRootNode: undefined as CompilerDOM.ElementNode | undefined,
100+
singleRootElTypes: [] as string[],
101+
singleRootNodes: new Set<CompilerDOM.ElementNode | null>(),
102102
accessExternalVariable(name: string, offset?: number) {
103103
let arr = accessExternalVariables.get(name);
104104
if (!arr) {

packages/language-core/lib/codegen/template/element.ts

+7-6
Original file line numberDiff line numberDiff line change
@@ -257,7 +257,8 @@ export function* generateComponent(
257257
yield* generateElementDirectives(options, ctx, node);
258258

259259
const [refName, offset] = yield* generateElementReference(options, ctx, node);
260-
const isRootNode = node === ctx.singleRootNode;
260+
const tag = hyphenateTag(node.tag);
261+
const isRootNode = ctx.singleRootNodes.has(node) && !options.vueCompilerOptions.fallthroughComponentNames.includes(tag);
261262

262263
if (refName || isRootNode) {
263264
const componentInstanceVar = ctx.getInternalVariable();
@@ -276,14 +277,14 @@ export function* generateComponent(
276277
});
277278
}
278279
if (isRootNode) {
279-
ctx.singleRootElType = `NonNullable<typeof ${componentInstanceVar}>['$el']`;
280+
ctx.singleRootElTypes.push(`NonNullable<typeof ${componentInstanceVar}>['$el']`);
280281
}
281282
}
282283

283284
if (hasVBindAttrs(options, ctx, node)) {
284285
const attrsVar = ctx.getInternalVariable();
285-
ctx.inheritedAttrVars.add(attrsVar);
286286
yield `let ${attrsVar}!: Parameters<typeof ${componentFunctionalVar}>[0]${endOfLine}`;
287+
ctx.inheritedAttrVars.add(attrsVar);
287288
}
288289

289290
collectStyleScopedClassReferences(options, ctx, node);
@@ -366,8 +367,8 @@ export function* generateElement(
366367
offset
367368
});
368369
}
369-
if (ctx.singleRootNode === node) {
370-
ctx.singleRootElType = `__VLS_NativeElements['${node.tag}']`;
370+
if (ctx.singleRootNodes.has(node)) {
371+
ctx.singleRootElTypes.push(`__VLS_NativeElements['${node.tag}']`);
371372
}
372373

373374
if (hasVBindAttrs(options, ctx, node)) {
@@ -496,7 +497,7 @@ function hasVBindAttrs(
496497
node: CompilerDOM.ElementNode
497498
) {
498499
return options.vueCompilerOptions.fallthroughAttributes && (
499-
node === ctx.singleRootNode ||
500+
(options.inheritAttrs && ctx.singleRootNodes.has(node)) ||
500501
node.props.some(prop =>
501502
prop.type === CompilerDOM.NodeTypes.DIRECTIVE
502503
&& prop.name === 'bind'

packages/language-core/lib/codegen/template/index.ts

+8-1
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,14 @@ function* generateRootEl(
160160
ctx: TemplateCodegenContext
161161
): Generator<Code> {
162162
yield `type __VLS_RootEl = `;
163-
yield ctx.singleRootElType ?? `any`;
163+
if (ctx.singleRootElTypes.length && !ctx.singleRootNodes.has(null)) {
164+
for (const type of ctx.singleRootElTypes) {
165+
yield `${newLine}| ${type}`;
166+
}
167+
}
168+
else {
169+
yield `any`;
170+
}
164171
yield endOfLine;
165172
return `__VLS_RootEl`;
166173
}

packages/language-core/lib/codegen/template/templateChild.ts

+34-5
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import * as CompilerDOM from '@vue/compiler-dom';
22
import type { Code } from '../../types';
3+
import { hyphenateTag } from '../../utils/shared';
34
import { endOfLine, newLine } from '../utils';
45
import type { TemplateCodegenContext } from './context';
56
import { generateComponent, generateElement } from './element';
@@ -66,18 +67,16 @@ export function* generateTemplateChild(
6667
}
6768
}
6869

69-
const shouldInheritRootNodeAttrs = options.inheritAttrs;
70-
7170
const cur = node as CompilerDOM.ElementNode | CompilerDOM.IfNode | CompilerDOM.ForNode;
7271
if (cur.codegenNode?.type === CompilerDOM.NodeTypes.JS_CACHE_EXPRESSION) {
7372
cur.codegenNode = cur.codegenNode.value as any;
7473
}
7574

7675
if (node.type === CompilerDOM.NodeTypes.ROOT) {
77-
let prev: CompilerDOM.TemplateChildNode | undefined;
78-
if (shouldInheritRootNodeAttrs && node.children.length === 1 && node.children[0].type === CompilerDOM.NodeTypes.ELEMENT) {
79-
ctx.singleRootNode = node.children[0];
76+
for (const item of collectSingleRootNodes(options, node.children)) {
77+
ctx.singleRootNodes.add(item);
8078
}
79+
let prev: CompilerDOM.TemplateChildNode | undefined;
8180
for (const childNode of node.children) {
8281
yield* generateTemplateChild(options, ctx, childNode, prev);
8382
prev = childNode;
@@ -159,6 +158,36 @@ export function* generateTemplateChild(
159158
}
160159
}
161160

161+
function* collectSingleRootNodes(
162+
options: TemplateCodegenOptions,
163+
children: CompilerDOM.TemplateChildNode[]
164+
): Generator<CompilerDOM.ElementNode | null> {
165+
if (children.length !== 1) {
166+
// "null" is used to determine whether the component is not always has a single root
167+
if (children.length > 1) {
168+
yield null;
169+
}
170+
return;
171+
}
172+
173+
const child = children[0];
174+
if (child.type === CompilerDOM.NodeTypes.IF) {
175+
for (const branch of child.branches) {
176+
yield* collectSingleRootNodes(options, branch.children);
177+
}
178+
return;
179+
}
180+
else if (child.type !== CompilerDOM.NodeTypes.ELEMENT) {
181+
return;
182+
}
183+
yield child;
184+
185+
const tag = hyphenateTag(child.tag);
186+
if (options.vueCompilerOptions.fallthroughComponentNames.includes(tag)) {
187+
yield* collectSingleRootNodes(options, child.children);
188+
}
189+
}
190+
162191
// TODO: track https://github.com/vuejs/vue-next/issues/3498
163192
export function getVForNode(node: CompilerDOM.ElementNode) {
164193
const forDirective = node.props.find(

packages/language-core/lib/types.ts

+1
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ export interface VueCompilerOptions {
4040
inferTemplateDollarSlots: boolean;
4141
skipTemplateCodegen: boolean;
4242
fallthroughAttributes: boolean;
43+
fallthroughComponentNames: string[];
4344
dataAttributes: string[];
4445
htmlAttributes: string[];
4546
optionsWrapper: [string, string] | [];

packages/language-core/lib/utils/ts.ts

+11
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import type * as ts from 'typescript';
44
import { generateGlobalTypes, getGlobalTypesFileName } from '../codegen/globalTypes';
55
import { getAllExtensions } from '../languagePlugin';
66
import type { RawVueCompilerOptions, VueCompilerOptions, VueLanguagePlugin } from '../types';
7+
import { hyphenateTag } from './shared';
78

89
export type ParsedCommandLine = ts.ParsedCommandLine & {
910
vueOptions: VueCompilerOptions;
@@ -219,6 +220,10 @@ export class CompilerOptionsResolver {
219220
...defaults.composables,
220221
...this.options.composables,
221222
},
223+
fallthroughComponentNames: [
224+
...defaults.fallthroughComponentNames,
225+
...this.options.fallthroughComponentNames ?? []
226+
].map(hyphenateTag),
222227
// https://github.com/vuejs/vue-next/blob/master/packages/compiler-dom/src/transforms/vModel.ts#L49-L51
223228
// https://vuejs.org/guide/essentials/forms.html#form-input-bindings
224229
experimentalModelPropName: Object.fromEntries(Object.entries(
@@ -274,6 +279,12 @@ export function getDefaultCompilerOptions(target = 99, lib = 'vue', strictTempla
274279
inferTemplateDollarSlots: false,
275280
skipTemplateCodegen: false,
276281
fallthroughAttributes: false,
282+
fallthroughComponentNames: [
283+
'Transition',
284+
'KeepAlive',
285+
'Teleport',
286+
'Suspense',
287+
],
277288
dataAttributes: [],
278289
htmlAttributes: ['aria-*'],
279290
optionsWrapper: target >= 2.7

packages/language-core/schemas/vue-tsconfig.schema.json

+10
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,16 @@
105105
"default": false,
106106
"markdownDescription": "Enable to support typed fallthrough attributes. Please note that enabling may significantly slow down type checking."
107107
},
108+
"fallthroughComponentNames": {
109+
"type": "array",
110+
"default": [
111+
"Transition",
112+
"KeepAlive",
113+
"Teleport",
114+
"Suspense"
115+
],
116+
"markdownDescription": "Component names that will be transparent when collecting single root child nodes and fallthrough attributes."
117+
},
108118
"dataAttributes": {
109119
"type": "array",
110120
"default": [ ],

test-workspace/tsc/passedFixtures/vue3/rootEl/child.vue

+5-2
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,10 @@ import Base from './base.vue';
77
</script>
88

99
<template>
10-
<Base>
11-
{{ exactType($el, {} as HTMLAnchorElement) }}
10+
<Base v-if="true">
11+
{{ exactType($el, {} as HTMLAnchorElement | HTMLImageElement) }}
1212
</Base>
13+
<Transition v-else>
14+
<img />
15+
</Transition>
1316
</template>

test-workspace/tsc/passedFixtures/vue3/rootEl/main.vue

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,6 @@ import Child from './child.vue';
77

88
<template>
99
<Child>
10-
{{ exactType($el, {} as HTMLAnchorElement) }}
10+
{{ exactType($el, {} as HTMLAnchorElement | HTMLImageElement) }}
1111
</Child>
1212
</template>

0 commit comments

Comments
 (0)