Skip to content

Commit

Permalink
fix(language-core): local name support for prop using runtime api (#4650
Browse files Browse the repository at this point in the history
)
  • Loading branch information
KazariEX authored Aug 25, 2024
1 parent 0b22735 commit 6bb7820
Show file tree
Hide file tree
Showing 6 changed files with 230 additions and 74 deletions.
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

0 comments on commit 6bb7820

Please sign in to comment.