Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(language-core): local name support for prop using runtime api #4650

Merged
merged 11 commits into from
Aug 25, 2024
102 changes: 69 additions & 33 deletions packages/language-core/lib/codegen/script/scriptSetup.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { ScriptSetupRanges } from '../../parsers/scriptSetupRanges';
import type { Code, Sfc } from '../../types';
import type { Code, Sfc, TextRange } from '../../types';
import { endOfLine, generateSfcBlockSection, newLine } from '../common';
import { generateComponent, generateEmitsOption } from './component';
import type { ScriptCodegenContext } from './context';
Expand Down Expand Up @@ -88,16 +88,16 @@ export function* generateScriptSetup(

if (ctx.scriptSetupGeneratedOffset !== undefined) {
for (const defineProp of scriptSetupRanges.defineProp) {
if (!defineProp.name) {
if (!defineProp.localName) {
continue;
}
const propName = scriptSetup.content.substring(defineProp.name.start, defineProp.name.end);
const propMirror = definePropMirrors.get(propName);
const [_, localName] = getPropAndLocalName(scriptSetup, defineProp);
const propMirror = definePropMirrors.get(localName!);
if (propMirror !== undefined) {
options.linkedCodeMappings.push({
sourceOffsets: [defineProp.name.start + ctx.scriptSetupGeneratedOffset],
sourceOffsets: [defineProp.localName.start + ctx.scriptSetupGeneratedOffset],
generatedOffsets: [propMirror],
lengths: [defineProp.name.end - defineProp.name.start],
lengths: [defineProp.localName.end - defineProp.localName.start],
data: undefined,
});
}
Expand Down Expand Up @@ -302,14 +302,19 @@ function* generateComponentProps(
yield `const __VLS_defaults = {${newLine}`;
for (const defineProp of scriptSetupRanges.defineProp) {
if (defineProp.defaultValue) {
if (defineProp.name) {
yield scriptSetup.content.substring(defineProp.name.start, defineProp.name.end);
const [propName, localName] = getPropAndLocalName(scriptSetup, defineProp);

if (defineProp.name || defineProp.isModel) {
yield propName!;
}
else if (defineProp.localName) {
yield localName!;
}
else {
yield `modelValue`;
continue;
}
yield `: `;
yield scriptSetup.content.substring(defineProp.defaultValue.start, defineProp.defaultValue.end);
yield getRangeName(scriptSetup, defineProp.defaultValue);
yield `,${newLine}`;
}
}
Expand All @@ -331,33 +336,35 @@ function* generateComponentProps(
ctx.generatedPropsType = true;
yield `{${newLine}`;
for (const defineProp of scriptSetupRanges.defineProp) {
let propName = 'modelValue';
if (defineProp.name && defineProp.nameIsString) {
const [propName, localName] = getPropAndLocalName(scriptSetup, defineProp);

if (defineProp.isModel && !defineProp.name) {
yield propName!;
}
else if (defineProp.name) {
// renaming support
yield generateSfcBlockSection(scriptSetup, defineProp.name.start, defineProp.name.end, codeFeatures.navigation);
propName = scriptSetup.content.substring(defineProp.name.start, defineProp.name.end);
propName = propName.replace(/['"]+/g, '');
}
else if (defineProp.name) {
propName = scriptSetup.content.substring(defineProp.name.start, defineProp.name.end);
definePropMirrors.set(propName, options.getGeneratedLength());
yield propName;
else if (defineProp.localName) {
definePropMirrors.set(localName!, options.getGeneratedLength());
yield localName!;
}
else {
yield propName;
continue;
}

yield defineProp.required
? `: `
: `?: `;
yield* generateDefinePropType(scriptSetup, propName, defineProp);
yield* generateDefinePropType(scriptSetup, propName, localName, defineProp);
yield `,${newLine}`;

if (defineProp.modifierType) {
let propModifierName = 'modelModifiers';
if (defineProp.name) {
propModifierName = `${scriptSetup.content.substring(defineProp.name.start + 1, defineProp.name.end - 1)}Modifiers`;
propModifierName = `${getRangeName(scriptSetup, defineProp.name, true)}Modifiers`;
}
const modifierType = scriptSetup.content.substring(defineProp.modifierType.start, defineProp.modifierType.end);
const modifierType = getRangeName(scriptSetup, defineProp.modifierType);
definePropMirrors.set(propModifierName, options.getGeneratedLength());
yield `${propModifierName}?: Record<${modifierType}, true>,${endOfLine}`;
}
Expand Down Expand Up @@ -394,13 +401,10 @@ function* generateModelEmits(
continue;
}

let propName = 'modelValue';
if (defineProp.name) {
propName = scriptSetup.content.substring(defineProp.name.start, defineProp.name.end);
propName = propName.replace(/['"]+/g, '');
}
const [propName, localName] = getPropAndLocalName(scriptSetup, defineProp);

yield `'update:${propName}': [${propName}:`;
yield* generateDefinePropType(scriptSetup, propName, defineProp);
yield* generateDefinePropType(scriptSetup, propName, localName, defineProp);
yield `]${endOfLine}`;
}
yield `}`;
Expand All @@ -413,20 +417,52 @@ function* generateModelEmits(
yield endOfLine;
}

function* generateDefinePropType(scriptSetup: NonNullable<Sfc['scriptSetup']>, propName: string, defineProp: ScriptSetupRanges['defineProp'][number]) {
function* generateDefinePropType(
scriptSetup: NonNullable<Sfc['scriptSetup']>,
propName: string | undefined,
localName: string | undefined,
defineProp: ScriptSetupRanges['defineProp'][number]
) {
if (defineProp.type) {
// Infer from defineProp<T>
yield scriptSetup.content.substring(defineProp.type.start, defineProp.type.end);
yield getRangeName(scriptSetup, defineProp.type);
}
else if ((defineProp.name && defineProp.nameIsString) || !defineProp.nameIsString) {
else if (defineProp.runtimeType && localName) {
// Infer from actual prop declaration code
yield `typeof ${propName}['value']`;
yield `typeof ${localName}['value']`;
}
else if (defineProp.defaultValue) {
else if (defineProp.defaultValue && propName) {
// Infer from defineProp({default: T})
yield `typeof __VLS_defaults['${propName}']`;
}
else {
yield `any`;
}
}

function getPropAndLocalName(
scriptSetup: NonNullable<Sfc['scriptSetup']>,
defineProp: ScriptSetupRanges['defineProp'][number]
) {
const localName = defineProp.localName
? getRangeName(scriptSetup, defineProp.localName)
: undefined;
let propName = defineProp.name
? getRangeName(scriptSetup, defineProp.name)
: defineProp.isModel
? 'modelValue'
: localName;
if (defineProp.name) {
propName = propName!.replace(/['"]+/g, '')
}
return [propName, localName];
}

function getRangeName(
scriptSetup: NonNullable<Sfc['scriptSetup']>,
range: TextRange,
unwrap = false
) {
const offset = unwrap ? 1 : 0;
return scriptSetup.content.substring(range.start + offset, range.end - offset);
}
138 changes: 97 additions & 41 deletions packages/language-core/lib/parsers/scriptSetupRanges.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,10 +50,11 @@ export function parseScriptSetupRanges(
const definePropProposalA = vueCompilerOptions.experimentalDefinePropProposal === 'kevinEdition' || ast.text.trimStart().startsWith('// @experimentalDefinePropProposal=kevinEdition');
const definePropProposalB = vueCompilerOptions.experimentalDefinePropProposal === 'johnsonEdition' || ast.text.trimStart().startsWith('// @experimentalDefinePropProposal=johnsonEdition');
const defineProp: {
localName: TextRange | undefined;
name: TextRange | undefined;
nameIsString: boolean;
type: TextRange | undefined;
modifierType?: TextRange | undefined;
runtimeType: TextRange | undefined;
defaultValue: TextRange | undefined;
required: boolean;
isModel?: boolean;
Expand Down Expand Up @@ -134,81 +135,136 @@ export function parseScriptSetupRanges(
) {
const callText = getNodeText(ts, node.expression, ast);
if (vueCompilerOptions.macros.defineModel.includes(callText)) {
let name: TextRange | undefined;
let localName: TextRange | undefined;
let propName: TextRange | undefined;
let options: ts.Node | undefined;

if (
ts.isVariableDeclaration(parent) &&
ts.isIdentifier(parent.name)
) {
localName = _getStartEnd(parent.name);
}

if (node.arguments.length >= 2) {
name = _getStartEnd(node.arguments[0]);
propName = _getStartEnd(node.arguments[0]);
options = node.arguments[1];
}
else if (node.arguments.length >= 1) {
if (ts.isStringLiteral(node.arguments[0])) {
name = _getStartEnd(node.arguments[0]);
propName = _getStartEnd(node.arguments[0]);
}
else {
options = node.arguments[0];
}
}

let runtimeType: TextRange | undefined;
let defaultValue: TextRange | undefined;
let required = false;
if (options && ts.isObjectLiteralExpression(options)) {
for (const property of options.properties) {
if (ts.isPropertyAssignment(property) && ts.isIdentifier(property.name) && getNodeText(ts, property.name, ast) === 'required' && property.initializer.kind === ts.SyntaxKind.TrueKeyword) {
if (!ts.isPropertyAssignment(property) || !ts.isIdentifier(property.name)) {
continue;
}
const text = getNodeText(ts, property.name, ast);
if (text === 'type') {
runtimeType = _getStartEnd(property.initializer);
}
else if (text === 'default') {
defaultValue = _getStartEnd(property.initializer);
}
else if (text === 'required' && property.initializer.kind === ts.SyntaxKind.TrueKeyword) {
required = true;
break;
}
}
}
defineProp.push({
name,
nameIsString: true,
localName,
name: propName,
type: node.typeArguments?.length ? _getStartEnd(node.typeArguments[0]) : undefined,
modifierType: node.typeArguments && node.typeArguments?.length >= 2 ? _getStartEnd(node.typeArguments[1]) : undefined,
defaultValue: undefined,
runtimeType,
defaultValue,
required,
isModel: true,
});
}
else if (callText === 'defineProp') {
let localName: TextRange | undefined;
let propName: TextRange | undefined;
let options: ts.Node | undefined;

if (
ts.isVariableDeclaration(parent) &&
ts.isIdentifier(parent.name)
) {
localName = _getStartEnd(parent.name);
}

let runtimeType: TextRange | undefined;
let defaultValue: TextRange | undefined;
let required = false;
if (definePropProposalA) {
let required = false;
if (node.arguments.length >= 2) {
const secondArg = node.arguments[1];
if (ts.isObjectLiteralExpression(secondArg)) {
for (const property of secondArg.properties) {
if (ts.isPropertyAssignment(property) && ts.isIdentifier(property.name) && getNodeText(ts, property.name, ast) === 'required' && property.initializer.kind === ts.SyntaxKind.TrueKeyword) {
required = true;
break;
}
options = node.arguments[1];
}
if (node.arguments.length >= 1) {
propName = _getStartEnd(node.arguments[0]);
}

if (options && ts.isObjectLiteralExpression(options)) {
for (const property of options.properties) {
if (!ts.isPropertyAssignment(property) || !ts.isIdentifier(property.name)) {
continue;
}
const text = getNodeText(ts, property.name, ast);
if (text === 'type') {
runtimeType = _getStartEnd(property.initializer);
}
else if (text === 'default') {
defaultValue = _getStartEnd(property.initializer);
}
else if (text === 'required' && property.initializer.kind === ts.SyntaxKind.TrueKeyword) {
required = true;
}
}
}
}
else if (definePropProposalB) {
if (node.arguments.length >= 3) {
options = node.arguments[2];
}
if (node.arguments.length >= 2) {
if (node.arguments[1].kind === ts.SyntaxKind.TrueKeyword) {
required = true;
}
}
if (node.arguments.length >= 1) {
defineProp.push({
name: _getStartEnd(node.arguments[0]),
nameIsString: true,
type: node.typeArguments?.length ? _getStartEnd(node.typeArguments[0]) : undefined,
defaultValue: undefined,
required,
});
defaultValue = _getStartEnd(node.arguments[0]);
}
else if (ts.isVariableDeclaration(parent)) {
defineProp.push({
name: _getStartEnd(parent.name),
nameIsString: false,
type: node.typeArguments?.length ? _getStartEnd(node.typeArguments[0]) : undefined,
defaultValue: undefined,
required,
});

if (options && ts.isObjectLiteralExpression(options)) {
for (const property of options.properties) {
if (!ts.isPropertyAssignment(property) || !ts.isIdentifier(property.name)) {
continue;
}
const text = getNodeText(ts, property.name, ast);
if (text === 'type') {
runtimeType = _getStartEnd(property.initializer);
}
}
}
}
else if (definePropProposalB && ts.isVariableDeclaration(parent)) {
defineProp.push({
name: _getStartEnd(parent.name),
nameIsString: false,
defaultValue: node.arguments.length >= 1 ? _getStartEnd(node.arguments[0]) : undefined,
type: node.typeArguments?.length ? _getStartEnd(node.typeArguments[0]) : undefined,
required: node.arguments.length >= 2 && node.arguments[1].kind === ts.SyntaxKind.TrueKeyword,
});
}

defineProp.push({
localName,
name: propName,
type: node.typeArguments?.length ? _getStartEnd(node.typeArguments[0]) : undefined,
runtimeType,
defaultValue,
required,
});
}
else if (vueCompilerOptions.macros.defineSlots.includes(callText)) {
slots.define = parseDefineFunction(node);
Expand Down
1 change: 1 addition & 0 deletions test-workspace/tsc/passedFixtures/vue2/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"../vue3/#4512",
"../vue3/#4540",
"../vue3/#4646",
"../vue3/#4649",
"../vue3/components",
"../vue3/defineEmits",
"../vue3/defineModel",
Expand Down
24 changes: 24 additions & 0 deletions test-workspace/tsc/passedFixtures/vue3/#4649/main.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<script lang="ts" setup>
import ModelComp from './model-comp.vue';
import PropComp from './prop-comp.vue';

const model = '1';
const foo = '1';
const bar = '1';
const baz = '1';
</script>

<template>
<!-- @vue-expect-error -->
<ModelComp v-model="model" />
<!-- @vue-expect-error -->
<ModelComp v-model:foo="foo" />
<!-- @vue-expect-error -->
<ModelComp v-model:bar="bar" />
<!-- @vue-expect-error -->
<PropComp :foo="foo" />
<!-- @vue-expect-error -->
<PropComp :bar="bar" />
<!-- @vue-expect-error -->
<PropComp :baz="baz" />
</template>
Loading