From b41801b0836dbf559c1b903d80a6468c8a4d4520 Mon Sep 17 00:00:00 2001 From: Guillaume Chau Date: Wed, 22 Jan 2025 08:35:37 +0100 Subject: [PATCH] feat(component-meta): collect destructured props defaults (#5101) --- packages/component-meta/lib/base.ts | 8 ++++++++ packages/component-meta/tests/index.spec.ts | 11 +++++++++++ packages/language-core/lib/codegen/utils/index.ts | 11 ++++++----- .../language-core/lib/parsers/scriptSetupRanges.ts | 8 ++++---- packages/language-core/lib/plugins/vue-tsx.ts | 2 +- .../language-service/lib/plugins/vue-inlayhints.ts | 8 ++++---- packages/tsc/tests/__snapshots__/dts.spec.ts.snap | 9 +++++++++ .../reference-type-props/component-destructure.vue | 11 +++++++++++ 8 files changed, 54 insertions(+), 14 deletions(-) create mode 100644 test-workspace/component-meta/reference-type-props/component-destructure.vue diff --git a/packages/component-meta/lib/base.ts b/packages/component-meta/lib/base.ts index 5d8ff7ec85..d0517916a0 100644 --- a/packages/component-meta/lib/base.ts +++ b/packages/component-meta/lib/base.ts @@ -754,6 +754,14 @@ function readVueComponentDefaultProps( ...resolvePropsOption(ast, obj, printer, ts), }; } + } else if (descriptor.scriptSetup && scriptSetupRanges?.defineProps?.destructured) { + const ast = descriptor.scriptSetup.ast; + for (const [prop, initializer] of scriptSetupRanges.defineProps.destructured) { + if (initializer) { + const expText = printer?.printNode(ts.EmitHint.Expression, initializer, ast) ?? initializer.getText(ast); + result[prop] = { default: expText }; + } + } } function findObjectLiteralExpression(node: ts.Node) { diff --git a/packages/component-meta/tests/index.spec.ts b/packages/component-meta/tests/index.spec.ts index 1ca3323775..5888bde78f 100644 --- a/packages/component-meta/tests/index.spec.ts +++ b/packages/component-meta/tests/index.spec.ts @@ -365,6 +365,17 @@ const worker = (checker: ComponentMetaChecker, withTsconfig: boolean) => describ }); }); + test('reference-type-props-destructured', () => { + const componentPath = path.resolve(__dirname, '../../../test-workspace/component-meta/reference-type-props/component-destructure.vue'); + const meta = checker.getComponentMeta(componentPath); + + expect(meta.type).toEqual(TypeMeta.Class); + + const text = meta.props.find(prop => prop.name === 'text'); + + expect(text?.default).toEqual('"foobar"'); + }) + test('reference-type-props-js', () => { const componentPath = path.resolve(__dirname, '../../../test-workspace/component-meta/reference-type-props/component-js.vue'); const meta = checker.getComponentMeta(componentPath); diff --git a/packages/language-core/lib/codegen/utils/index.ts b/packages/language-core/lib/codegen/utils/index.ts index a39915e108..d68dfd7c01 100644 --- a/packages/language-core/lib/codegen/utils/index.ts +++ b/packages/language-core/lib/codegen/utils/index.ts @@ -32,7 +32,7 @@ export function collectVars( results: string[] = [] ) { const identifiers = collectIdentifiers(ts, node, []); - for (const [id] of identifiers) { + for (const { id } of identifiers) { results.push(getNodeText(ts, id, ast)); } return results; @@ -41,15 +41,16 @@ export function collectVars( export function collectIdentifiers( ts: typeof import('typescript'), node: ts.Node, - results: [id: ts.Identifier, isRest: boolean][] = [], - isRest = false + results: Array<{ id: ts.Identifier, isRest: boolean, initializer: ts.Expression | undefined }> = [], + isRest = false, + initializer: ts.Expression | undefined = undefined ) { if (ts.isIdentifier(node)) { - results.push([node, isRest]); + results.push({ id: node, isRest, initializer }); } else if (ts.isObjectBindingPattern(node)) { for (const el of node.elements) { - collectIdentifiers(ts, el.name, results, !!el.dotDotDotToken); + collectIdentifiers(ts, el.name, results, !!el.dotDotDotToken, el.initializer); } } else if (ts.isArrayBindingPattern(node)) { diff --git a/packages/language-core/lib/parsers/scriptSetupRanges.ts b/packages/language-core/lib/parsers/scriptSetupRanges.ts index 4366454235..5ed7b21a7b 100644 --- a/packages/language-core/lib/parsers/scriptSetupRanges.ts +++ b/packages/language-core/lib/parsers/scriptSetupRanges.ts @@ -24,7 +24,7 @@ type DefineProp = { type DefineProps = CallExpressionRange & { name?: string; - destructured?: Set; + destructured?: Map; destructuredRest?: string; statement: TextRange; }; @@ -285,15 +285,15 @@ export function parseScriptSetupRanges( }; if (ts.isVariableDeclaration(parent)) { if (ts.isObjectBindingPattern(parent.name)) { - defineProps.destructured = new Set(); + defineProps.destructured = new Map(); const identifiers = collectIdentifiers(ts, parent.name, []); - for (const [id, isRest] of identifiers) { + for (const { id, isRest, initializer } of identifiers) { const name = _getNodeText(id); if (isRest) { defineProps.destructuredRest = name; } else { - defineProps.destructured.add(name); + defineProps.destructured.set(name, initializer); } } } diff --git a/packages/language-core/lib/plugins/vue-tsx.ts b/packages/language-core/lib/plugins/vue-tsx.ts index 7bf5e75dfc..ee669a8e5c 100644 --- a/packages/language-core/lib/plugins/vue-tsx.ts +++ b/packages/language-core/lib/plugins/vue-tsx.ts @@ -133,7 +133,7 @@ function createTsx( ); const destructuredPropNames = unstable.computedSet( computed(() => { - const newNames = new Set(scriptSetupRanges.get()?.defineProps?.destructured); + const newNames = new Set(scriptSetupRanges.get()?.defineProps?.destructured?.keys()); const rest = scriptSetupRanges.get()?.defineProps?.destructuredRest; if (rest) { newNames.add(rest); diff --git a/packages/language-service/lib/plugins/vue-inlayhints.ts b/packages/language-service/lib/plugins/vue-inlayhints.ts index 3cee77fb07..8b61f117a4 100644 --- a/packages/language-service/lib/plugins/vue-inlayhints.ts +++ b/packages/language-service/lib/plugins/vue-inlayhints.ts @@ -45,7 +45,7 @@ export function create(ts: typeof import('typescript')): LanguageServicePlugin { for (const [prop, isShorthand] of findDestructuredProps( ts, virtualCode._sfc.scriptSetup.ast, - scriptSetupRanges.defineProps.destructured + scriptSetupRanges.defineProps.destructured.keys() )) { const name = prop.text; const end = prop.getEnd(); @@ -117,7 +117,7 @@ type Scope = Record; export function findDestructuredProps( ts: typeof import('typescript'), ast: ts.SourceFile, - props: Set + props: MapIterator ) { const rootScope: Scope = Object.create(null); const scopeStack: Scope[] = [rootScope]; @@ -192,7 +192,7 @@ export function findDestructuredProps( && ts.isCallExpression(initializer) && initializer.expression.getText(ast) === 'defineProps'; - for (const [id] of collectIdentifiers(ts, name)) { + for (const { id } of collectIdentifiers(ts, name)) { if (isDefineProps) { excludedIds.add(id); } else { @@ -208,7 +208,7 @@ export function findDestructuredProps( } for (const p of parameters) { - for (const [id] of collectIdentifiers(ts, p)) { + for (const { id } of collectIdentifiers(ts, p)) { registerLocalBinding(id); } } diff --git a/packages/tsc/tests/__snapshots__/dts.spec.ts.snap b/packages/tsc/tests/__snapshots__/dts.spec.ts.snap index e321fe1512..8d42786314 100644 --- a/packages/tsc/tests/__snapshots__/dts.spec.ts.snap +++ b/packages/tsc/tests/__snapshots__/dts.spec.ts.snap @@ -335,6 +335,15 @@ export default _default; " `; +exports[`vue-tsc-dts > Input: reference-type-props/component-destructure.vue, Output: reference-type-props/component-destructure.vue.d.ts 1`] = ` +"type __VLS_Props = { + text: string; +}; +declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, HTMLDivElement>; +export default _default; +" +`; + exports[`vue-tsc-dts > Input: reference-type-props/component-js.vue, Output: reference-type-props/component-js.vue.d.ts 1`] = ` "declare const _default: import("vue").DefineComponent +const { + text = 'foobar' +} = defineProps<{ + text: string; +}>(); + + +