Skip to content

Commit 6bb7820

Browse files
authored
fix(language-core): local name support for prop using runtime api (#4650)
1 parent 0b22735 commit 6bb7820

File tree

6 files changed

+230
-74
lines changed

6 files changed

+230
-74
lines changed

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

Lines changed: 69 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { ScriptSetupRanges } from '../../parsers/scriptSetupRanges';
2-
import type { Code, Sfc } from '../../types';
2+
import type { Code, Sfc, TextRange } from '../../types';
33
import { endOfLine, generateSfcBlockSection, newLine } from '../common';
44
import { generateComponent, generateEmitsOption } from './component';
55
import type { ScriptCodegenContext } from './context';
@@ -88,16 +88,16 @@ export function* generateScriptSetup(
8888

8989
if (ctx.scriptSetupGeneratedOffset !== undefined) {
9090
for (const defineProp of scriptSetupRanges.defineProp) {
91-
if (!defineProp.name) {
91+
if (!defineProp.localName) {
9292
continue;
9393
}
94-
const propName = scriptSetup.content.substring(defineProp.name.start, defineProp.name.end);
95-
const propMirror = definePropMirrors.get(propName);
94+
const [_, localName] = getPropAndLocalName(scriptSetup, defineProp);
95+
const propMirror = definePropMirrors.get(localName!);
9696
if (propMirror !== undefined) {
9797
options.linkedCodeMappings.push({
98-
sourceOffsets: [defineProp.name.start + ctx.scriptSetupGeneratedOffset],
98+
sourceOffsets: [defineProp.localName.start + ctx.scriptSetupGeneratedOffset],
9999
generatedOffsets: [propMirror],
100-
lengths: [defineProp.name.end - defineProp.name.start],
100+
lengths: [defineProp.localName.end - defineProp.localName.start],
101101
data: undefined,
102102
});
103103
}
@@ -302,14 +302,19 @@ function* generateComponentProps(
302302
yield `const __VLS_defaults = {${newLine}`;
303303
for (const defineProp of scriptSetupRanges.defineProp) {
304304
if (defineProp.defaultValue) {
305-
if (defineProp.name) {
306-
yield scriptSetup.content.substring(defineProp.name.start, defineProp.name.end);
305+
const [propName, localName] = getPropAndLocalName(scriptSetup, defineProp);
306+
307+
if (defineProp.name || defineProp.isModel) {
308+
yield propName!;
309+
}
310+
else if (defineProp.localName) {
311+
yield localName!;
307312
}
308313
else {
309-
yield `modelValue`;
314+
continue;
310315
}
311316
yield `: `;
312-
yield scriptSetup.content.substring(defineProp.defaultValue.start, defineProp.defaultValue.end);
317+
yield getRangeName(scriptSetup, defineProp.defaultValue);
313318
yield `,${newLine}`;
314319
}
315320
}
@@ -331,33 +336,35 @@ function* generateComponentProps(
331336
ctx.generatedPropsType = true;
332337
yield `{${newLine}`;
333338
for (const defineProp of scriptSetupRanges.defineProp) {
334-
let propName = 'modelValue';
335-
if (defineProp.name && defineProp.nameIsString) {
339+
const [propName, localName] = getPropAndLocalName(scriptSetup, defineProp);
340+
341+
if (defineProp.isModel && !defineProp.name) {
342+
yield propName!;
343+
}
344+
else if (defineProp.name) {
336345
// renaming support
337346
yield generateSfcBlockSection(scriptSetup, defineProp.name.start, defineProp.name.end, codeFeatures.navigation);
338-
propName = scriptSetup.content.substring(defineProp.name.start, defineProp.name.end);
339-
propName = propName.replace(/['"]+/g, '');
340347
}
341-
else if (defineProp.name) {
342-
propName = scriptSetup.content.substring(defineProp.name.start, defineProp.name.end);
343-
definePropMirrors.set(propName, options.getGeneratedLength());
344-
yield propName;
348+
else if (defineProp.localName) {
349+
definePropMirrors.set(localName!, options.getGeneratedLength());
350+
yield localName!;
345351
}
346352
else {
347-
yield propName;
353+
continue;
348354
}
355+
349356
yield defineProp.required
350357
? `: `
351358
: `?: `;
352-
yield* generateDefinePropType(scriptSetup, propName, defineProp);
359+
yield* generateDefinePropType(scriptSetup, propName, localName, defineProp);
353360
yield `,${newLine}`;
354361

355362
if (defineProp.modifierType) {
356363
let propModifierName = 'modelModifiers';
357364
if (defineProp.name) {
358-
propModifierName = `${scriptSetup.content.substring(defineProp.name.start + 1, defineProp.name.end - 1)}Modifiers`;
365+
propModifierName = `${getRangeName(scriptSetup, defineProp.name, true)}Modifiers`;
359366
}
360-
const modifierType = scriptSetup.content.substring(defineProp.modifierType.start, defineProp.modifierType.end);
367+
const modifierType = getRangeName(scriptSetup, defineProp.modifierType);
361368
definePropMirrors.set(propModifierName, options.getGeneratedLength());
362369
yield `${propModifierName}?: Record<${modifierType}, true>,${endOfLine}`;
363370
}
@@ -394,13 +401,10 @@ function* generateModelEmits(
394401
continue;
395402
}
396403

397-
let propName = 'modelValue';
398-
if (defineProp.name) {
399-
propName = scriptSetup.content.substring(defineProp.name.start, defineProp.name.end);
400-
propName = propName.replace(/['"]+/g, '');
401-
}
404+
const [propName, localName] = getPropAndLocalName(scriptSetup, defineProp);
405+
402406
yield `'update:${propName}': [${propName}:`;
403-
yield* generateDefinePropType(scriptSetup, propName, defineProp);
407+
yield* generateDefinePropType(scriptSetup, propName, localName, defineProp);
404408
yield `]${endOfLine}`;
405409
}
406410
yield `}`;
@@ -413,20 +417,52 @@ function* generateModelEmits(
413417
yield endOfLine;
414418
}
415419

416-
function* generateDefinePropType(scriptSetup: NonNullable<Sfc['scriptSetup']>, propName: string, defineProp: ScriptSetupRanges['defineProp'][number]) {
420+
function* generateDefinePropType(
421+
scriptSetup: NonNullable<Sfc['scriptSetup']>,
422+
propName: string | undefined,
423+
localName: string | undefined,
424+
defineProp: ScriptSetupRanges['defineProp'][number]
425+
) {
417426
if (defineProp.type) {
418427
// Infer from defineProp<T>
419-
yield scriptSetup.content.substring(defineProp.type.start, defineProp.type.end);
428+
yield getRangeName(scriptSetup, defineProp.type);
420429
}
421-
else if ((defineProp.name && defineProp.nameIsString) || !defineProp.nameIsString) {
430+
else if (defineProp.runtimeType && localName) {
422431
// Infer from actual prop declaration code
423-
yield `typeof ${propName}['value']`;
432+
yield `typeof ${localName}['value']`;
424433
}
425-
else if (defineProp.defaultValue) {
434+
else if (defineProp.defaultValue && propName) {
426435
// Infer from defineProp({default: T})
427436
yield `typeof __VLS_defaults['${propName}']`;
428437
}
429438
else {
430439
yield `any`;
431440
}
432441
}
442+
443+
function getPropAndLocalName(
444+
scriptSetup: NonNullable<Sfc['scriptSetup']>,
445+
defineProp: ScriptSetupRanges['defineProp'][number]
446+
) {
447+
const localName = defineProp.localName
448+
? getRangeName(scriptSetup, defineProp.localName)
449+
: undefined;
450+
let propName = defineProp.name
451+
? getRangeName(scriptSetup, defineProp.name)
452+
: defineProp.isModel
453+
? 'modelValue'
454+
: localName;
455+
if (defineProp.name) {
456+
propName = propName!.replace(/['"]+/g, '')
457+
}
458+
return [propName, localName];
459+
}
460+
461+
function getRangeName(
462+
scriptSetup: NonNullable<Sfc['scriptSetup']>,
463+
range: TextRange,
464+
unwrap = false
465+
) {
466+
const offset = unwrap ? 1 : 0;
467+
return scriptSetup.content.substring(range.start + offset, range.end - offset);
468+
}

packages/language-core/lib/parsers/scriptSetupRanges.ts

Lines changed: 97 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -50,10 +50,11 @@ export function parseScriptSetupRanges(
5050
const definePropProposalA = vueCompilerOptions.experimentalDefinePropProposal === 'kevinEdition' || ast.text.trimStart().startsWith('// @experimentalDefinePropProposal=kevinEdition');
5151
const definePropProposalB = vueCompilerOptions.experimentalDefinePropProposal === 'johnsonEdition' || ast.text.trimStart().startsWith('// @experimentalDefinePropProposal=johnsonEdition');
5252
const defineProp: {
53+
localName: TextRange | undefined;
5354
name: TextRange | undefined;
54-
nameIsString: boolean;
5555
type: TextRange | undefined;
5656
modifierType?: TextRange | undefined;
57+
runtimeType: TextRange | undefined;
5758
defaultValue: TextRange | undefined;
5859
required: boolean;
5960
isModel?: boolean;
@@ -134,81 +135,136 @@ export function parseScriptSetupRanges(
134135
) {
135136
const callText = getNodeText(ts, node.expression, ast);
136137
if (vueCompilerOptions.macros.defineModel.includes(callText)) {
137-
let name: TextRange | undefined;
138+
let localName: TextRange | undefined;
139+
let propName: TextRange | undefined;
138140
let options: ts.Node | undefined;
141+
142+
if (
143+
ts.isVariableDeclaration(parent) &&
144+
ts.isIdentifier(parent.name)
145+
) {
146+
localName = _getStartEnd(parent.name);
147+
}
148+
139149
if (node.arguments.length >= 2) {
140-
name = _getStartEnd(node.arguments[0]);
150+
propName = _getStartEnd(node.arguments[0]);
141151
options = node.arguments[1];
142152
}
143153
else if (node.arguments.length >= 1) {
144154
if (ts.isStringLiteral(node.arguments[0])) {
145-
name = _getStartEnd(node.arguments[0]);
155+
propName = _getStartEnd(node.arguments[0]);
146156
}
147157
else {
148158
options = node.arguments[0];
149159
}
150160
}
161+
162+
let runtimeType: TextRange | undefined;
163+
let defaultValue: TextRange | undefined;
151164
let required = false;
152165
if (options && ts.isObjectLiteralExpression(options)) {
153166
for (const property of options.properties) {
154-
if (ts.isPropertyAssignment(property) && ts.isIdentifier(property.name) && getNodeText(ts, property.name, ast) === 'required' && property.initializer.kind === ts.SyntaxKind.TrueKeyword) {
167+
if (!ts.isPropertyAssignment(property) || !ts.isIdentifier(property.name)) {
168+
continue;
169+
}
170+
const text = getNodeText(ts, property.name, ast);
171+
if (text === 'type') {
172+
runtimeType = _getStartEnd(property.initializer);
173+
}
174+
else if (text === 'default') {
175+
defaultValue = _getStartEnd(property.initializer);
176+
}
177+
else if (text === 'required' && property.initializer.kind === ts.SyntaxKind.TrueKeyword) {
155178
required = true;
156-
break;
157179
}
158180
}
159181
}
160182
defineProp.push({
161-
name,
162-
nameIsString: true,
183+
localName,
184+
name: propName,
163185
type: node.typeArguments?.length ? _getStartEnd(node.typeArguments[0]) : undefined,
164186
modifierType: node.typeArguments && node.typeArguments?.length >= 2 ? _getStartEnd(node.typeArguments[1]) : undefined,
165-
defaultValue: undefined,
187+
runtimeType,
188+
defaultValue,
166189
required,
167190
isModel: true,
168191
});
169192
}
170193
else if (callText === 'defineProp') {
194+
let localName: TextRange | undefined;
195+
let propName: TextRange | undefined;
196+
let options: ts.Node | undefined;
197+
198+
if (
199+
ts.isVariableDeclaration(parent) &&
200+
ts.isIdentifier(parent.name)
201+
) {
202+
localName = _getStartEnd(parent.name);
203+
}
204+
205+
let runtimeType: TextRange | undefined;
206+
let defaultValue: TextRange | undefined;
207+
let required = false;
171208
if (definePropProposalA) {
172-
let required = false;
173209
if (node.arguments.length >= 2) {
174-
const secondArg = node.arguments[1];
175-
if (ts.isObjectLiteralExpression(secondArg)) {
176-
for (const property of secondArg.properties) {
177-
if (ts.isPropertyAssignment(property) && ts.isIdentifier(property.name) && getNodeText(ts, property.name, ast) === 'required' && property.initializer.kind === ts.SyntaxKind.TrueKeyword) {
178-
required = true;
179-
break;
180-
}
210+
options = node.arguments[1];
211+
}
212+
if (node.arguments.length >= 1) {
213+
propName = _getStartEnd(node.arguments[0]);
214+
}
215+
216+
if (options && ts.isObjectLiteralExpression(options)) {
217+
for (const property of options.properties) {
218+
if (!ts.isPropertyAssignment(property) || !ts.isIdentifier(property.name)) {
219+
continue;
220+
}
221+
const text = getNodeText(ts, property.name, ast);
222+
if (text === 'type') {
223+
runtimeType = _getStartEnd(property.initializer);
224+
}
225+
else if (text === 'default') {
226+
defaultValue = _getStartEnd(property.initializer);
227+
}
228+
else if (text === 'required' && property.initializer.kind === ts.SyntaxKind.TrueKeyword) {
229+
required = true;
181230
}
182231
}
183232
}
233+
}
234+
else if (definePropProposalB) {
235+
if (node.arguments.length >= 3) {
236+
options = node.arguments[2];
237+
}
238+
if (node.arguments.length >= 2) {
239+
if (node.arguments[1].kind === ts.SyntaxKind.TrueKeyword) {
240+
required = true;
241+
}
242+
}
184243
if (node.arguments.length >= 1) {
185-
defineProp.push({
186-
name: _getStartEnd(node.arguments[0]),
187-
nameIsString: true,
188-
type: node.typeArguments?.length ? _getStartEnd(node.typeArguments[0]) : undefined,
189-
defaultValue: undefined,
190-
required,
191-
});
244+
defaultValue = _getStartEnd(node.arguments[0]);
192245
}
193-
else if (ts.isVariableDeclaration(parent)) {
194-
defineProp.push({
195-
name: _getStartEnd(parent.name),
196-
nameIsString: false,
197-
type: node.typeArguments?.length ? _getStartEnd(node.typeArguments[0]) : undefined,
198-
defaultValue: undefined,
199-
required,
200-
});
246+
247+
if (options && ts.isObjectLiteralExpression(options)) {
248+
for (const property of options.properties) {
249+
if (!ts.isPropertyAssignment(property) || !ts.isIdentifier(property.name)) {
250+
continue;
251+
}
252+
const text = getNodeText(ts, property.name, ast);
253+
if (text === 'type') {
254+
runtimeType = _getStartEnd(property.initializer);
255+
}
256+
}
201257
}
202258
}
203-
else if (definePropProposalB && ts.isVariableDeclaration(parent)) {
204-
defineProp.push({
205-
name: _getStartEnd(parent.name),
206-
nameIsString: false,
207-
defaultValue: node.arguments.length >= 1 ? _getStartEnd(node.arguments[0]) : undefined,
208-
type: node.typeArguments?.length ? _getStartEnd(node.typeArguments[0]) : undefined,
209-
required: node.arguments.length >= 2 && node.arguments[1].kind === ts.SyntaxKind.TrueKeyword,
210-
});
211-
}
259+
260+
defineProp.push({
261+
localName,
262+
name: propName,
263+
type: node.typeArguments?.length ? _getStartEnd(node.typeArguments[0]) : undefined,
264+
runtimeType,
265+
defaultValue,
266+
required,
267+
});
212268
}
213269
else if (vueCompilerOptions.macros.defineSlots.includes(callText)) {
214270
slots.define = parseDefineFunction(node);

test-workspace/tsc/passedFixtures/vue2/tsconfig.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
"../vue3/#4512",
2323
"../vue3/#4540",
2424
"../vue3/#4646",
25+
"../vue3/#4649",
2526
"../vue3/components",
2627
"../vue3/defineEmits",
2728
"../vue3/defineModel",
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<script lang="ts" setup>
2+
import ModelComp from './model-comp.vue';
3+
import PropComp from './prop-comp.vue';
4+
5+
const model = '1';
6+
const foo = '1';
7+
const bar = '1';
8+
const baz = '1';
9+
</script>
10+
11+
<template>
12+
<!-- @vue-expect-error -->
13+
<ModelComp v-model="model" />
14+
<!-- @vue-expect-error -->
15+
<ModelComp v-model:foo="foo" />
16+
<!-- @vue-expect-error -->
17+
<ModelComp v-model:bar="bar" />
18+
<!-- @vue-expect-error -->
19+
<PropComp :foo="foo" />
20+
<!-- @vue-expect-error -->
21+
<PropComp :bar="bar" />
22+
<!-- @vue-expect-error -->
23+
<PropComp :baz="baz" />
24+
</template>

0 commit comments

Comments
 (0)