Skip to content

Commit 4bde3e1

Browse files
authored
feat(language-core): type support of slot children (#5137)
1 parent 638b949 commit 4bde3e1

File tree

14 files changed

+140
-33
lines changed

14 files changed

+140
-33
lines changed

Diff for: packages/language-core/lib/codegen/globalTypes.ts

+28-20
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ export function generateGlobalTypes({
2424
checkUnknownEvents,
2525
checkUnknownComponents,
2626
}: VueCompilerOptions) {
27-
const fnPropsType = `(K extends { $props: infer Props } ? Props : any)${checkUnknownProps ? '' : ' & Record<string, unknown>'}`;
27+
const fnPropsType = `(T extends { $props: infer Props } ? Props : {})${checkUnknownProps ? '' : ' & Record<string, unknown>'}`;
2828
let text = ``;
2929
if (target < 3.5) {
3030
text += `
@@ -68,10 +68,31 @@ export function generateGlobalTypes({
6868
N2 extends keyof __VLS_GlobalComponents ? N2 extends N0 ? Pick<__VLS_GlobalComponents, N0 extends keyof __VLS_GlobalComponents ? N0 : never> : { [K in N0]: __VLS_GlobalComponents[N2] } :
6969
N3 extends keyof __VLS_GlobalComponents ? N3 extends N0 ? Pick<__VLS_GlobalComponents, N0 extends keyof __VLS_GlobalComponents ? N0 : never> : { [K in N0]: __VLS_GlobalComponents[N3] } :
7070
${checkUnknownComponents ? '{}' : '{ [K in N0]: unknown }'};
71-
type __VLS_FunctionalComponentProps<T, K> =
72-
'__ctx' extends keyof __VLS_PickNotAny<K, {}> ? K extends { __ctx?: { props?: infer P } } ? NonNullable<P> : never
73-
: T extends (props: infer P, ...args: any) => any ? P :
74-
{};
71+
type __VLS_FunctionalComponentCtx<T, K> = __VLS_PickNotAny<'__ctx' extends keyof __VLS_PickNotAny<K, {}>
72+
? K extends { __ctx?: infer Ctx } ? NonNullable<Ctx> : never : any
73+
, T extends (props: any, ctx: infer Ctx) => any ? Ctx : any
74+
>;
75+
type __VLS_FunctionalComponentProps<T, K> = '__ctx' extends keyof __VLS_PickNotAny<K, {}>
76+
? K extends { __ctx?: { props?: infer P } } ? NonNullable<P> : never
77+
: T extends (props: infer P, ...args: any) => any ? P
78+
: {};
79+
type __VLS_FunctionalComponent<T> = (props: ${fnPropsType}, ctx?: any) => __VLS_Element & {
80+
__ctx?: {
81+
attrs?: any,
82+
slots?: T extends { ${getSlotsPropertyName(target)}: infer Slots } ? Slots : Record<string, any>,
83+
emit?: T extends { $emit: infer Emit } ? Emit : {},
84+
props?: ${fnPropsType},
85+
expose?: (exposed: T) => void,
86+
}
87+
};
88+
type __VLS_NormalizeSlotReturns<S, R = NonNullable<S> extends (...args: any) => infer K ? K : any> = R extends any[] ? {
89+
[K in keyof R]: R[K] extends infer V
90+
? V extends Element ? V
91+
: V extends new (...args: any) => infer R ? ReturnType<__VLS_FunctionalComponent<R>>
92+
: V extends (...args: any) => infer R ? R
93+
: any
94+
: never
95+
} : R;
7596
type __VLS_IsFunction<T, K> = K extends keyof T
7697
? __VLS_IsAny<T[K]> extends false
7798
? unknown extends T[K]
@@ -118,10 +139,6 @@ export function generateGlobalTypes({
118139
NormalizedEmits = __VLS_NormalizeEmits<Emits> extends infer E ? string extends keyof E ? {} : E : never,
119140
> = __VLS_SpreadMerge<NormalizedEmits, TypeEmits>;
120141
type __VLS_PrettifyGlobal<T> = { [K in keyof T]: T[K]; } & {};
121-
type __VLS_PickFunctionalComponentCtx<T, K> = NonNullable<__VLS_PickNotAny<
122-
'__ctx' extends keyof __VLS_PickNotAny<K, {}> ? K extends { __ctx?: infer Ctx } ? Ctx : never : any
123-
, T extends (props: any, ctx: infer Ctx) => any ? Ctx : any
124-
>>;
125142
type __VLS_UseTemplateRef<T> = Readonly<import('${lib}').ShallowRef<T | null>>;
126143
127144
function __VLS_getVForSourceType<T extends number | string | any[] | Iterable<any>>(source: T): [
@@ -146,19 +163,10 @@ export function generateGlobalTypes({
146163
: (arg1: unknown, arg2: unknown, arg3: unknown, arg4: unknown) => void;
147164
function __VLS_makeOptional<T>(t: T): { [K in keyof T]?: T[K] };
148165
function __VLS_asFunctionalComponent<T, K = T extends new (...args: any) => any ? InstanceType<T> : unknown>(t: T, instance?: K):
149-
T extends new (...args: any) => any
150-
? (props: ${fnPropsType}, ctx?: any) => __VLS_Element & {
151-
__ctx?: {
152-
attrs?: any;
153-
slots?: K extends { ${getSlotsPropertyName(target)}: infer Slots } ? Slots : any;
154-
emit?: K extends { $emit: infer Emit } ? Emit : any;
155-
expose?(exposed: K): void;
156-
props?: ${fnPropsType};
157-
}
158-
}
166+
T extends new (...args: any) => any ? __VLS_FunctionalComponent<K>
159167
: T extends () => any ? (props: {}, ctx?: any) => ReturnType<T>
160168
: T extends (...args: any) => any ? T
161-
: (_: {}${checkUnknownProps ? '' : ' & Record<string, unknown>'}, ctx?: any) => { __ctx?: { attrs?: any, expose?: any, slots?: any, emit?: any, props?: {}${checkUnknownProps ? '' : ' & Record<string, unknown>'} } };
169+
: __VLS_FunctionalComponent<{}>;
162170
function __VLS_functionalComponentArgsRest<T extends (...args: any) => any>(t: T): 2 extends Parameters<T>['length'] ? [any] : [];
163171
function __VLS_asFunctionalElement<T>(tag: T, endTag?: T): (attrs: T${checkUnknownComponents ? '' : ' & Record<string, unknown>'}) => void;
164172
function __VLS_asFunctionalSlot<S>(slot: S): S extends () => infer R ? (props: {}) => R : NonNullable<S>;

Diff for: packages/language-core/lib/codegen/template/context.ts

+1
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,7 @@ export function createTemplateCodegenContext(options: Pick<TemplateCodegenOption
208208
templateRefs,
209209
currentComponent: undefined as {
210210
ctxVar: string;
211+
childTypes: string[];
211212
used: boolean;
212213
} | undefined,
213214
singleRootElTypes: [] as string[],

Diff for: packages/language-core/lib/codegen/template/element.ts

+9-2
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,11 @@ export function* generateComponent(
4141
const componentCtxVar = ctx.getInternalVariable();
4242
const isComponentTag = node.tag.toLowerCase() === 'component';
4343

44+
ctx.currentComponent?.childTypes.push(`typeof ${componentVNodeVar}`);
4445
ctx.currentComponent = {
4546
ctxVar: componentCtxVar,
46-
used: false
47+
childTypes: [],
48+
used: false,
4749
};
4850

4951
let props = node.props;
@@ -282,7 +284,7 @@ export function* generateComponent(
282284
yield* generateVSlot(options, ctx, node, slotDir);
283285

284286
if (ctx.currentComponent.used) {
285-
yield `var ${componentCtxVar}!: __VLS_PickFunctionalComponentCtx<typeof ${componentOriginalVar}, typeof ${componentVNodeVar}>${endOfLine}`;
287+
yield `var ${componentCtxVar}!: __VLS_FunctionalComponentCtx<typeof ${componentOriginalVar}, typeof ${componentVNodeVar}>${endOfLine}`;
286288
}
287289
}
288290

@@ -297,6 +299,8 @@ export function* generateElement(
297299
: undefined;
298300
const failedPropExps: FailedPropExpression[] = [];
299301

302+
ctx.currentComponent?.childTypes.push(`__VLS_NativeElements['${node.tag}']`);
303+
300304
yield `__VLS_asFunctionalElement(__VLS_elements`;
301305
yield* generatePropertyAccess(
302306
options,
@@ -355,7 +359,10 @@ export function* generateElement(
355359

356360
collectStyleScopedClassReferences(options, ctx, node);
357361

362+
const { currentComponent } = ctx;
363+
ctx.currentComponent = undefined;
358364
yield* generateElementChildren(options, ctx, node.children);
365+
ctx.currentComponent = currentComponent;
359366
}
360367

361368
function* generateFailedPropExps(

Diff for: packages/language-core/lib/codegen/template/vSlot.ts

+43-3
Original file line numberDiff line numberDiff line change
@@ -53,17 +53,17 @@ export function* generateVSlot(
5353
}
5454
else {
5555
// #932: reference for implicit default slot
56+
const { start, end } = getElementInnerLoc(options, node);
5657
yield* wrapWith(
57-
node.children[0].loc.start.offset,
58-
node.children.at(-1)!.loc.end.offset,
58+
start,
59+
end,
5960
ctx.codeFeatures.navigation,
6061
`default`
6162
);
6263
}
6364
yield `: ${slotVar} } = ${ctx.currentComponent.ctxVar}.slots!${endOfLine}`;
6465
}
6566

66-
6767
if (slotDir?.exp?.type === CompilerDOM.NodeTypes.SIMPLE_EXPRESSION) {
6868
const slotAst = createTsAst(options.ts, slotDir, `(${slotDir.exp.content}) => {}`);
6969
collectVars(options.ts, slotAst, slotAst, slotBlockVars);
@@ -80,6 +80,20 @@ export function* generateVSlot(
8080
ctx.removeLocalVariable(varName);
8181
}
8282

83+
if (options.vueCompilerOptions.strictSlotChildren && node.children.length) {
84+
const { start, end } = getElementInnerLoc(options, node);
85+
yield `(): __VLS_NormalizeSlotReturns<typeof ${slotVar}> => (`;
86+
yield* wrapWith(
87+
start,
88+
end,
89+
ctx.codeFeatures.verification,
90+
`{} as [`,
91+
...ctx.currentComponent.childTypes.map(name => `${name}, `),
92+
`]`
93+
);
94+
yield `)${endOfLine}`;
95+
}
96+
8397
if (slotDir) {
8498
let isStatic = true;
8599
if (slotDir.arg?.type === CompilerDOM.NodeTypes.SIMPLE_EXPRESSION) {
@@ -170,3 +184,29 @@ function* generateSlotParameters(
170184
];
171185
}
172186
}
187+
188+
function getElementInnerLoc(
189+
options: TemplateCodegenOptions,
190+
node: CompilerDOM.ElementNode
191+
) {
192+
if (node.children.length) {
193+
let start = node.children[0].loc.start.offset;
194+
let end = node.children.at(-1)!.loc.end.offset;
195+
while (options.template.content[start - 1] !== '>') {
196+
start--;
197+
}
198+
while (options.template.content[end] !== '<') {
199+
end++;
200+
}
201+
return {
202+
start,
203+
end,
204+
};
205+
}
206+
else {
207+
return {
208+
start: node.loc.start.offset,
209+
end: node.loc.end.offset,
210+
};
211+
}
212+
}

Diff for: packages/language-core/lib/types.ts

+1
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ export interface VueCompilerOptions {
2929
vitePressExtensions: string[];
3030
petiteVueExtensions: string[];
3131
jsxSlots: boolean;
32+
strictSlotChildren: boolean;
3233
strictVModel: boolean;
3334
checkUnknownProps: boolean;
3435
checkUnknownEvents: boolean;

Diff for: packages/language-core/lib/utils/ts.ts

+1
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,7 @@ export function getDefaultCompilerOptions(target = 99, lib = 'vue', strictTempla
267267
vitePressExtensions: [],
268268
petiteVueExtensions: [],
269269
jsxSlots: false,
270+
strictSlotChildren: strictTemplates,
270271
strictVModel: strictTemplates,
271272
checkUnknownProps: strictTemplates,
272273
checkUnknownEvents: strictTemplates,

Diff for: packages/language-core/schemas/vue-tsconfig.schema.json

+5
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,11 @@
4747
"default": false,
4848
"markdownDescription": "Enables strict templates. When set to `true`, all `checkUnknown*` options will be enabled."
4949
},
50+
"strictSlotChildren": {
51+
"type": "boolean",
52+
"default": false,
53+
"markdownDescription": "Strict type constraints of slot children. If not set, uses the 'strictTemplates' value."
54+
},
5055
"strictVModel": {
5156
"type": "boolean",
5257
"default": false,

Diff for: packages/language-server/tests/references.spec.ts

+5-5
Original file line numberDiff line numberDiff line change
@@ -40,16 +40,16 @@ test('Default slot', async () => {
4040
},
4141
{
4242
"end": {
43-
"line": 8,
44-
"offset": 16,
43+
"line": 9,
44+
"offset": 4,
4545
},
4646
"file": "\${testWorkspacePath}/tsconfigProject/foo.vue",
4747
"isDefinition": false,
4848
"isWriteAccess": false,
49-
"lineText": " <div></div>",
49+
"lineText": " <Fixture>",
5050
"start": {
51-
"line": 8,
52-
"offset": 5,
51+
"line": 7,
52+
"offset": 13,
5353
},
5454
},
5555
],

Diff for: test-workspace/tsc/passedFixtures/vue2/tsconfig.json

+1
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
"../vue3/events",
4040
"../vue3/no-script-block",
4141
"../vue3/rootEl",
42+
"../vue3/slot-children",
4243
"../vue3/slots",
4344
"../vue3/templateRef",
4445
"../vue3/templateRef_native",

Diff for: test-workspace/tsc/passedFixtures/vue3/#4668/child.vue

+2-2
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
<script lang="ts" setup>
44
defineSlots<{
5-
action1: (props: { a: string }) => void
6-
action2: (props: { a: string }) => void
5+
action1: (props: { a: string }) => any
6+
action2: (props: { a: string }) => any
77
}>()
88
</script>

Diff for: test-workspace/tsc/passedFixtures/vue3/#4668/main.vue

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,5 +12,5 @@
1212
import { exactType } from '../../shared';
1313
import Child from './child.vue'
1414
15-
const n = 1 as const
15+
const n = 1
1616
</script>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<script setup lang="ts" generic="T">
2+
defineProps<{
3+
foo: T;
4+
}>();
5+
</script>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<script lang="ts" setup>
2+
import Child from './child.vue';
3+
import Parent from './parent.vue';
4+
5+
const foo = {} as 'a' | 'b';
6+
</script>
7+
8+
<template>
9+
<Parent :foo>
10+
<Child :foo="(`a` as const)" />
11+
<Child :foo="(`b` as const)" />
12+
</Parent>
13+
14+
<!-- @vue-expect-error -->
15+
<Parent :foo>
16+
<Child :foo="(`a` as const)" />
17+
<Child :foo="(`c` as const)" />
18+
</Parent>
19+
20+
<Parent :foo>
21+
<a></a>
22+
</Parent>
23+
24+
<!-- @vue-expect-error -->
25+
<Parent :foo>
26+
<img />
27+
</Parent>
28+
</template>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<script setup lang="ts" generic="T">
2+
import Child from './child.vue';
3+
4+
defineProps<{
5+
foo: T;
6+
}>();
7+
defineSlots<{
8+
default?: () => (typeof Child<T> | HTMLAnchorElement)[];
9+
}>();
10+
</script>

0 commit comments

Comments
 (0)