Skip to content

Commit f76c5eb

Browse files
authored
fix(language-core, typescript-plugin): handle self-reference component correctly (#5102)
1 parent 7f93a97 commit f76c5eb

File tree

10 files changed

+98
-73
lines changed

10 files changed

+98
-73
lines changed

packages/language-core/lib/codegen/globalTypes.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,10 +41,11 @@ export function generateGlobalTypes(lib: string, target: number, strictTemplates
4141
type __VLS_IsAny<T> = 0 extends 1 & T ? true : false;
4242
type __VLS_PickNotAny<A, B> = __VLS_IsAny<A> extends true ? B : A;
4343
type __VLS_unknownDirective = (arg1: unknown, arg2: unknown, arg3: unknown, arg4: unknown) => void;
44-
type __VLS_WithComponent<N0 extends string, LocalComponents, N1 extends string, N2 extends string, N3 extends string> =
44+
type __VLS_WithComponent<N0 extends string, LocalComponents, Self, N1 extends string, N2 extends string, N3 extends string> =
4545
N1 extends keyof LocalComponents ? N1 extends N0 ? Pick<LocalComponents, N0 extends keyof LocalComponents ? N0 : never> : { [K in N0]: LocalComponents[N1] } :
4646
N2 extends keyof LocalComponents ? N2 extends N0 ? Pick<LocalComponents, N0 extends keyof LocalComponents ? N0 : never> : { [K in N0]: LocalComponents[N2] } :
4747
N3 extends keyof LocalComponents ? N3 extends N0 ? Pick<LocalComponents, N0 extends keyof LocalComponents ? N0 : never> : { [K in N0]: LocalComponents[N3] } :
48+
Self extends object ? { [K in N0]: Self } :
4849
N1 extends keyof __VLS_GlobalComponents ? N1 extends N0 ? Pick<__VLS_GlobalComponents, N0 extends keyof __VLS_GlobalComponents ? N0 : never> : { [K in N0]: __VLS_GlobalComponents[N1] } :
4950
N2 extends keyof __VLS_GlobalComponents ? N2 extends N0 ? Pick<__VLS_GlobalComponents, N0 extends keyof __VLS_GlobalComponents ? N0 : never> : { [K in N0]: __VLS_GlobalComponents[N2] } :
5051
N3 extends keyof __VLS_GlobalComponents ? N3 extends N0 ? Pick<__VLS_GlobalComponents, N0 extends keyof __VLS_GlobalComponents ? N0 : never> : { [K in N0]: __VLS_GlobalComponents[N3] } :

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

Lines changed: 1 addition & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
1-
import * as path from 'path-browserify';
21
import type { Code } from '../../types';
3-
import { getSlotsPropertyName, hyphenateTag } from '../../utils/shared';
2+
import { hyphenateTag } from '../../utils/shared';
43
import { TemplateCodegenContext, createTemplateCodegenContext } from '../template/context';
54
import { generateInterpolation } from '../template/interpolation';
65
import { generateStyleScopedClassReferences } from '../template/styleScopedClasses';
@@ -69,23 +68,6 @@ function* generateTemplateComponents(options: ScriptCodegenOptions): Generator<C
6968
types.push(`typeof __VLS_componentsOption`);
7069
}
7170

72-
let nameType: Code | undefined;
73-
if (options.sfc.script && options.scriptRanges?.exportDefault?.nameOption) {
74-
const { nameOption } = options.scriptRanges.exportDefault;
75-
nameType = options.sfc.script.content.slice(nameOption.start, nameOption.end);
76-
}
77-
else if (options.sfc.scriptSetup) {
78-
const baseName = path.basename(options.fileName);
79-
nameType = `'${options.scriptSetupRanges?.defineOptions?.name ?? baseName.slice(0, baseName.lastIndexOf('.'))}'`;
80-
}
81-
if (nameType) {
82-
types.push(
83-
`{ [K in ${nameType}]: typeof __VLS_self & (new () => { `
84-
+ getSlotsPropertyName(options.vueCompilerOptions.target)
85-
+ `: typeof ${options.scriptSetupRanges?.defineSlots?.name ?? `__VLS_slots`} }) }`
86-
);
87-
}
88-
8971
types.push(`typeof __VLS_ctx`);
9072

9173
yield `type __VLS_LocalComponents =`;

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

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import * as CompilerDOM from '@vue/compiler-dom';
22
import { camelize, capitalize } from '@vue/shared';
33
import type { Code, VueCodeInformation } from '../../types';
4-
import { hyphenateTag } from '../../utils/shared';
4+
import { getSlotsPropertyName, hyphenateTag } from '../../utils/shared';
55
import { createVBindShorthandInlayHintInfo } from '../inlayHints';
66
import { collectVars, createTsAst, endOfLine, newLine, normalizeAttributeValue, variableNameRegex, wrapWith } from '../utils';
77
import { generateCamelized } from '../utils/camelized';
@@ -151,6 +151,14 @@ export function* generateComponent(
151151
}
152152
else if (!isComponentTag) {
153153
yield `const ${var_originalComponent} = ({} as __VLS_WithComponent<'${getCanonicalComponentName(node.tag)}', __VLS_LocalComponents, `;
154+
if (options.selfComponentName && possibleOriginalNames.includes(options.selfComponentName)) {
155+
yield `typeof __VLS_self & (new () => { `
156+
+ getSlotsPropertyName(options.vueCompilerOptions.target)
157+
+ `: typeof ${options.slotsAssignName ?? `__VLS_slots`} }), `;
158+
}
159+
else {
160+
yield `void, `;
161+
}
154162
yield getPossibleOriginalComponentNames(node.tag, false)
155163
.map(name => `'${name}'`)
156164
.join(`, `);

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ export interface TemplateCodegenOptions {
2323
slotsAssignName?: string;
2424
propsAssignName?: string;
2525
inheritAttrs: boolean;
26+
selfComponentName?: string;
2627
}
2728

2829
export function* generateTemplate(options: TemplateCodegenOptions): Generator<Code, TemplateCodegenContext> {

packages/language-core/lib/plugins/vue-tsx.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import type { Mapping } from '@volar/language-core';
2+
import { camelize, capitalize } from '@vue/shared';
23
import { computed, unstable } from 'alien-signals';
4+
import * as path from 'path-browserify';
35
import { generateScript } from '../codegen/script';
46
import { generateTemplate } from '../codegen/template';
57
import { parseScriptRanges } from '../parsers/scriptRanges';
@@ -153,6 +155,19 @@ function createTsx(
153155
const value = scriptSetupRanges.get()?.defineOptions?.inheritAttrs ?? scriptRanges.get()?.exportDefault?.inheritAttrsOption;
154156
return value !== 'false';
155157
});
158+
const selfComponentName = computed(() => {
159+
const { exportDefault } = scriptRanges.get() ?? {};
160+
if (_sfc.script && exportDefault?.nameOption) {
161+
const { nameOption } = exportDefault;
162+
return _sfc.script.content.slice(nameOption.start + 1, nameOption.end - 1);
163+
}
164+
const { defineOptions } = scriptSetupRanges.get() ?? {};
165+
if (_sfc.scriptSetup && defineOptions?.name) {
166+
return defineOptions.name;
167+
}
168+
const baseName = path.basename(fileName);
169+
return capitalize(camelize(baseName.slice(0, baseName.lastIndexOf('.'))));
170+
});
156171
const generatedTemplate = computed(() => {
157172

158173
if (vueCompilerOptions.get().skipTemplateCodegen || !_sfc.template) {
@@ -174,6 +189,7 @@ function createTsx(
174189
slotsAssignName: slotsAssignName.get(),
175190
propsAssignName: propsAssignName.get(),
176191
inheritAttrs: inheritAttrs.get(),
192+
selfComponentName: selfComponentName.get()
177193
});
178194

179195
let current = codegen.next();

packages/language-server/tests/completions.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -187,8 +187,8 @@ describe('Completions', async () => {
187187
"component",
188188
"slot",
189189
"template",
190-
"fixture",
191190
"BaseTransition",
191+
"Fixture",
192192
]
193193
`);
194194
});

packages/typescript-plugin/lib/requests/componentInfos.ts

Lines changed: 51 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import * as vue from '@vue/language-core';
22
import { camelize, capitalize } from '@vue/shared';
3+
import * as path from 'node:path';
34
import type * as ts from 'typescript';
45
import type { RequestContext } from './types';
56

@@ -21,28 +22,11 @@ export function getComponentProps(
2122
return [];
2223
}
2324

24-
const name = tag.split('.');
25-
26-
let componentSymbol = components.type.getProperty(name[0])
27-
?? components.type.getProperty(camelize(name[0]))
28-
?? components.type.getProperty(capitalize(camelize(name[0])));
29-
30-
if (!componentSymbol) {
25+
const componentType = getComponentType(ts, languageService, vueCode, components, fileName, tag);
26+
if (!componentType) {
3127
return [];
3228
}
3329

34-
let componentType = checker.getTypeOfSymbolAtLocation(componentSymbol, components.node);
35-
36-
for (let i = 1; i < name.length; i++) {
37-
componentSymbol = componentType.getProperty(name[i]);
38-
if (componentSymbol) {
39-
componentType = checker.getTypeOfSymbolAtLocation(componentSymbol, components.node);
40-
}
41-
else {
42-
return [];
43-
}
44-
}
45-
4630
const result = new Map<string, {
4731
name: string;
4832
required?: true;
@@ -104,31 +88,11 @@ export function getComponentEvents(
10488
return [];
10589
}
10690

107-
const name = tag.split('.');
108-
109-
let componentSymbol = components.type.getProperty(name[0]);
110-
111-
if (!componentSymbol) {
112-
componentSymbol = components.type.getProperty(camelize(name[0]))
113-
?? components.type.getProperty(capitalize(camelize(name[0])));
114-
}
115-
116-
if (!componentSymbol) {
91+
const componentType = getComponentType(ts, languageService, vueCode, components, fileName, tag);
92+
if (!componentType) {
11793
return [];
11894
}
11995

120-
let componentType = checker.getTypeOfSymbolAtLocation(componentSymbol, components.node);
121-
122-
for (let i = 1; i < name.length; i++) {
123-
componentSymbol = componentType.getProperty(name[i]);
124-
if (componentSymbol) {
125-
componentType = checker.getTypeOfSymbolAtLocation(componentSymbol, components.node);
126-
}
127-
else {
128-
return [];
129-
}
130-
}
131-
13296
const result = new Set<string>();
13397

13498
// for (const sig of componentType.getCallSignatures()) {
@@ -185,26 +149,23 @@ export function getComponentNames(
185149
return;
186150
}
187151
const vueCode = volarFile.generated.root;
188-
189-
return getVariableType(ts, languageService, vueCode, '__VLS_components')
190-
?.type
191-
?.getProperties()
192-
.map(c => c.name)
193-
.filter(entry => !entry.includes('$') && !entry.startsWith('_'))
194-
?? [];
152+
return _getComponentNames(ts, languageService, vueCode);
195153
}
196154

197155
export function _getComponentNames(
198156
ts: typeof import('typescript'),
199157
tsLs: ts.LanguageService,
200158
vueCode: vue.VueVirtualCode
201159
) {
202-
return getVariableType(ts, tsLs, vueCode, '__VLS_components')
160+
const names = getVariableType(ts, tsLs, vueCode, '__VLS_components')
203161
?.type
204162
?.getProperties()
205163
.map(c => c.name)
206164
.filter(entry => !entry.includes('$') && !entry.startsWith('_'))
207165
?? [];
166+
167+
names.push(getSelfComponentName(vueCode.fileName));
168+
return names;
208169
}
209170

210171
export function getElementAttrs(
@@ -242,6 +203,42 @@ export function getElementAttrs(
242203
return [];
243204
}
244205

206+
function getComponentType(
207+
ts: typeof import('typescript'),
208+
languageService: ts.LanguageService,
209+
vueCode: vue.VueVirtualCode,
210+
components: NonNullable<ReturnType<typeof getVariableType>>,
211+
fileName: string,
212+
tag: string
213+
) {
214+
const program = languageService.getProgram()!;
215+
const checker = program.getTypeChecker();
216+
const name = tag.split('.');
217+
218+
let componentSymbol = components.type.getProperty(name[0])
219+
?? components.type.getProperty(camelize(name[0]))
220+
?? components.type.getProperty(capitalize(camelize(name[0])));
221+
let componentType: ts.Type | undefined;
222+
223+
if (!componentSymbol) {
224+
const name = getSelfComponentName(fileName);
225+
if (name === capitalize(camelize(tag))) {
226+
componentType = getVariableType(ts, languageService, vueCode, '__VLS_self')?.type;
227+
}
228+
}
229+
else {
230+
componentType = checker.getTypeOfSymbolAtLocation(componentSymbol, components.node);
231+
for (let i = 1; i < name.length; i++) {
232+
componentSymbol = componentType.getProperty(name[i]);
233+
if (componentSymbol) {
234+
componentType = checker.getTypeOfSymbolAtLocation(componentSymbol, components.node);
235+
}
236+
}
237+
}
238+
239+
return componentType;
240+
}
241+
245242
function getVariableType(
246243
ts: typeof import('typescript'),
247244
languageService: ts.LanguageService,
@@ -266,6 +263,11 @@ function getVariableType(
266263
}
267264
}
268265

266+
function getSelfComponentName(fileName: string) {
267+
const baseName = path.basename(fileName);
268+
return capitalize(camelize(baseName.slice(0, baseName.lastIndexOf('.'))));
269+
}
270+
269271
function searchVariableDeclarationNode(
270272
ts: typeof import('typescript'),
271273
sourceFile: ts.SourceFile,
@@ -298,7 +300,6 @@ function generateCommentMarkdown(parts: ts.SymbolDisplayPart[], jsDocTags: ts.JS
298300
return result;
299301
}
300302

301-
302303
function _symbolDisplayPartsToMarkdown(parts: ts.SymbolDisplayPart[]) {
303304
return parts.map(part => {
304305
switch (part.kind) {
@@ -312,7 +313,6 @@ function _symbolDisplayPartsToMarkdown(parts: ts.SymbolDisplayPart[]) {
312313
}).join('');
313314
}
314315

315-
316316
function _jsDocTagInfoToMarkdown(jsDocTags: ts.JSDocTagInfo[]) {
317317
return jsDocTags.map(tag => {
318318
const tagName = `*@${tag.name}*`;
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { default as Main } from './child.vue';
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<script setup lang="ts">
2+
defineProps<{
3+
bar: string;
4+
}>();
5+
</script>
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<script setup lang="ts">
2+
import { Main } from './child';
3+
4+
defineProps<{
5+
foo: string;
6+
}>();
7+
</script>
8+
9+
<template>
10+
<Main bar=""></Main>
11+
</template>

0 commit comments

Comments
 (0)