From 7251b5cdc3de11ac414a93d0b1310158374cf293 Mon Sep 17 00:00:00 2001 From: Sacha STAFYNIAK Date: Tue, 2 Aug 2022 18:37:59 +0200 Subject: [PATCH] [vue-component-meta] fix: parse defineProps in script setup with option (#1665) Co-authored-by: johnsoncodehk --- packages/vue-component-meta/src/index.ts | 124 ++++++++++++------ .../vue-component-meta/tests/index.spec.ts | 82 ++++++++++-- .../component-js-setup.vue | 28 ++++ 3 files changed, 179 insertions(+), 55 deletions(-) create mode 100644 packages/vue-test-workspace/vue-component-meta/reference-type-props/component-js-setup.vue diff --git a/packages/vue-component-meta/src/index.ts b/packages/vue-component-meta/src/index.ts index c66cd5ff6..6063a2cae 100644 --- a/packages/vue-component-meta/src/index.ts +++ b/packages/vue-component-meta/src/index.ts @@ -111,7 +111,7 @@ export function createComponentMetaChecker(tsconfigPath: string, checkerOptions: function getMetaScriptContent(fileName: string) { return ` import * as Components from '${fileName.substring(0, fileName.length - '.meta.ts'.length)}'; - export default {} as { [K in keyof typeof Components]: InstanceType; };; + export default {} as { [K in keyof typeof Components]: InstanceType; }; `; } @@ -146,7 +146,7 @@ export function createComponentMetaChecker(tsconfigPath: string, checkerOptions: if ($props) { const type = typeChecker.getTypeOfSymbolAtLocation($props, symbolNode!); - const properties = type.getApparentProperties(); + const properties = type.getProperties(); result = properties .map((prop) => { @@ -165,9 +165,12 @@ export function createComponentMetaChecker(tsconfigPath: string, checkerOptions: } // fill defaults - const printer = ts.createPrinter(checkerOptions.printer); + const printer = checkerOptions.printer ? ts.createPrinter(checkerOptions.printer) : undefined; const snapshot = host.getScriptSnapshot(componentPath)!; - const vueDefaults = componentPath.endsWith('.vue') && exportName === 'default' ? readVueComponentDefaultProps(snapshot.getText(0, snapshot.getLength()), printer) : {}; + + const vueDefaults = componentPath.endsWith('.vue') && exportName === 'default' + ? readVueComponentDefaultProps(snapshot.getText(0, snapshot.getLength()), printer) + : {}; const tsDefaults = !componentPath.endsWith('.vue') ? readTsComponentDefaultProps( componentPath.substring(componentPath.lastIndexOf('.') + 1), // ts | js | tsx | jsx snapshot.getText(0, snapshot.getLength()), @@ -181,7 +184,15 @@ export function createComponentMetaChecker(tsconfigPath: string, checkerOptions: })) { const prop = result.find(p => p.name === propName); if (prop) { - prop.default = defaultExp; + prop.default = defaultExp.default; + + if (defaultExp.required !== undefined) { + prop.required = defaultExp.required; + } + + if (prop.default !== undefined) { + prop.required = false; // props with default are always optional + } } } @@ -200,7 +211,7 @@ export function createComponentMetaChecker(tsconfigPath: string, checkerOptions: const { resolveEventSignature, } = createSchemaResolvers(typeChecker, symbolNode!, checkerOptions.schema); - + return resolveEventSignature(call); }).filter(event => event.name); } @@ -437,12 +448,11 @@ function createSchemaResolvers(typeChecker: ts.TypeChecker, symbolNode: ts.Expre }; } -function readVueComponentDefaultProps(vueFileText: string, printer: ts.Printer) { - - const result: Record = {}; +function readVueComponentDefaultProps(vueFileText: string, printer: ts.Printer | undefined) { + let result: Record = {}; scriptSetupWorker(); - sciptWorker(); + scriptWorker(); return result; @@ -462,28 +472,43 @@ function readVueComponentDefaultProps(vueFileText: string, printer: ts.Printer) for (const prop of obj.properties) { if (ts.isPropertyAssignment(prop)) { const name = prop.name.getText(ast); - const exp = printer.printNode(ts.EmitHint.Expression, resolveDefaultOptionExpression(prop.initializer), ast);; - result[name] = exp; + const expNode = resolveDefaultOptionExpression(prop.initializer); + const expText = printer?.printNode(ts.EmitHint.Expression, expNode, ast) ?? expNode.getText(ast); + + result[name] = { + default: expText, + }; } } } + } else if (descriptor.scriptSetup && scriptSetupRanges?.propsRuntimeArg) { + const defaultsText = descriptor.scriptSetup.content.substring(scriptSetupRanges.propsRuntimeArg.start, scriptSetupRanges.propsRuntimeArg.end); + const ast = ts.createSourceFile('/tmp.' + descriptor.scriptSetup.lang, '(' + defaultsText + ')', ts.ScriptTarget.Latest); + const obj = findObjectLiteralExpression(ast); - function findObjectLiteralExpression(node: ts.Node) { - if (ts.isObjectLiteralExpression(node)) { - return node; - } - let result: ts.ObjectLiteralExpression | undefined; - node.forEachChild(child => { - if (!result) { - result = findObjectLiteralExpression(child); - } - }); - return result; + if (obj) { + result = { + ...result, + ...resolvePropsOption(ast, obj, printer), + }; + } + } + + function findObjectLiteralExpression(node: ts.Node) { + if (ts.isObjectLiteralExpression(node)) { + return node; } + let result: ts.ObjectLiteralExpression | undefined; + node.forEachChild(child => { + if (!result) { + result = findObjectLiteralExpression(child); + } + }); + return result; } } - function sciptWorker() { + function scriptWorker() { const vueSourceFile = vue.createSourceFile('/tmp.vue', vueFileText, {}, {}, ts); const descriptor = vueSourceFile.getDescriptor(); @@ -497,31 +522,16 @@ function readVueComponentDefaultProps(vueFileText: string, printer: ts.Printer) } } -function readTsComponentDefaultProps(lang: string, tsFileText: string, exportName: string, printer: ts.Printer) { +function readTsComponentDefaultProps(lang: string, tsFileText: string, exportName: string, printer: ts.Printer | undefined) { - const result: Record = {}; const ast = ts.createSourceFile('/tmp.' + lang, tsFileText, ts.ScriptTarget.Latest); const props = getPropsNode(); if (props) { - for (const prop of props.properties) { - if (ts.isPropertyAssignment(prop)) { - const name = prop.name?.getText(ast); - if (ts.isObjectLiteralExpression(prop.initializer)) { - for (const propOption of prop.initializer.properties) { - if (ts.isPropertyAssignment(propOption)) { - if (propOption.name?.getText(ast) === 'default') { - const _default = propOption.initializer; - result[name] = printer.printNode(ts.EmitHint.Expression, resolveDefaultOptionExpression(_default), ast); - } - } - } - } - } - } + return resolvePropsOption(ast, props, printer); } - return result; + return {}; function getComponentNode() { @@ -586,6 +596,36 @@ function readTsComponentDefaultProps(lang: string, tsFileText: string, exportNam } } +function resolvePropsOption(ast: ts.SourceFile, props: ts.ObjectLiteralExpression, printer: ts.Printer | undefined) { + + const result: Record = {}; + + for (const prop of props.properties) { + if (ts.isPropertyAssignment(prop)) { + const name = prop.name?.getText(ast); + if (ts.isObjectLiteralExpression(prop.initializer)) { + + const defaultProp = prop.initializer.properties.find(p => ts.isPropertyAssignment(p) && p.name.getText(ast) === 'default') as ts.PropertyAssignment | undefined; + const requiredProp = prop.initializer.properties.find(p => ts.isPropertyAssignment(p) && p.name.getText(ast) === 'required') as ts.PropertyAssignment | undefined; + + result[name] = {}; + + if (requiredProp) { + const exp = requiredProp.initializer.getText(ast); + result[name].required = exp === 'true'; + } + if (defaultProp) { + const expNode = resolveDefaultOptionExpression((defaultProp as any).initializer); + const expText = printer?.printNode(ts.EmitHint.Expression, expNode, ast) ?? expNode.getText(ast); + result[name].default = expText; + } + } + } + } + + return result; +} + function resolveDefaultOptionExpression(_default: ts.Expression) { if (ts.isArrowFunction(_default)) { if (ts.isBlock(_default.body)) { diff --git a/packages/vue-component-meta/tests/index.spec.ts b/packages/vue-component-meta/tests/index.spec.ts index bc0edbdd1..6e52486cd 100644 --- a/packages/vue-component-meta/tests/index.spec.ts +++ b/packages/vue-component-meta/tests/index.spec.ts @@ -7,12 +7,7 @@ describe(`vue-component-meta`, () => { const tsconfigPath = path.resolve(__dirname, '../../vue-test-workspace/vue-component-meta/tsconfig.json'); const checker = createComponentMetaChecker(tsconfigPath, { forceUseTs: true, - printer: { newLine: 1 }, - }); - const checker_schema = createComponentMetaChecker(tsconfigPath, { - schema: { - ignore: ['MyIgnoredNestedProps'], - }, + schema: { ignore: ['MyIgnoredNestedProps'] }, printer: { newLine: 1 }, }); @@ -41,7 +36,7 @@ describe(`vue-component-meta`, () => { test('reference-type-props', () => { const componentPath = path.resolve(__dirname, '../../vue-test-workspace/vue-component-meta/reference-type-props/component.vue'); - const meta = checker_schema.getComponentMeta(componentPath); + const meta = checker.getComponentMeta(componentPath); const foo = meta.props.find(prop => prop.name === 'foo'); const bar = meta.props.find(prop => prop.name === 'bar'); @@ -104,13 +99,21 @@ describe(`vue-component-meta`, () => { expect(baz?.required).toBeFalsy(); expect(baz?.type).toEqual('string[] | undefined'); expect(baz?.description).toEqual('string array baz'); - // expect(baz?.schema).toEqual({ - // kind: 'array', - // type: 'string[]', - // schema: ['string'] - // }); + expect(baz?.schema).toEqual({ + kind: 'enum', + type: 'string[] | undefined', + schema: [ + 'undefined', + { + kind: 'array', + type: 'string[]', + schema: ['string'] + } + ] + }); expect(union).toBeDefined(); + expect(union?.default).toBeUndefined(); expect(union?.required).toBeTruthy(); expect(union?.type).toEqual('string | number'); expect(union?.description).toEqual('required union type'); @@ -121,6 +124,7 @@ describe(`vue-component-meta`, () => { }); expect(unionOptional).toBeDefined(); + expect(unionOptional?.default).toBeUndefined(); expect(unionOptional?.required).toBeFalsy(); expect(unionOptional?.type).toEqual('string | number | undefined'); expect(unionOptional?.description).toEqual('optional union type'); @@ -131,6 +135,7 @@ describe(`vue-component-meta`, () => { }); expect(nested).toBeDefined(); + expect(nested?.default).toBeUndefined(); expect(nested?.required).toBeTruthy(); expect(nested?.type).toEqual('MyNestedProps'); expect(nested?.description).toEqual('required nested object'); @@ -151,6 +156,7 @@ describe(`vue-component-meta`, () => { }); expect(nestedIntersection).toBeDefined(); + expect(nestedIntersection?.default).toBeUndefined(); expect(nestedIntersection?.required).toBeTruthy(); expect(nestedIntersection?.type).toEqual('MyNestedProps & { additionalProp: string; }'); expect(nestedIntersection?.description).toEqual('required nested object with intersection'); @@ -180,6 +186,7 @@ describe(`vue-component-meta`, () => { }); expect(nestedOptional).toBeDefined(); + expect(nestedOptional?.default).toBeUndefined(); expect(nestedOptional?.required).toBeFalsy(); expect(nestedOptional?.type).toEqual('MyNestedProps | MyIgnoredNestedProps | undefined'); expect(nestedOptional?.description).toEqual('optional nested object'); @@ -208,6 +215,7 @@ describe(`vue-component-meta`, () => { }); expect(array).toBeDefined(); + expect(array?.default).toBeUndefined(); expect(array?.required).toBeTruthy(); expect(array?.type).toEqual('MyNestedProps[]'); expect(array?.description).toEqual('required array object'); @@ -234,6 +242,7 @@ describe(`vue-component-meta`, () => { }); expect(arrayOptional).toBeDefined(); + expect(arrayOptional?.default).toBeUndefined(); expect(arrayOptional?.required).toBeFalsy(); expect(arrayOptional?.type).toEqual('MyNestedProps[] | undefined'); expect(arrayOptional?.description).toEqual('optional array object'); @@ -267,6 +276,7 @@ describe(`vue-component-meta`, () => { }); expect(enumValue).toBeDefined(); + expect(enumValue?.default).toBeUndefined(); expect(enumValue?.required).toBeTruthy(); expect(enumValue?.type).toEqual('MyEnum'); expect(enumValue?.description).toEqual('enum value'); @@ -277,6 +287,7 @@ describe(`vue-component-meta`, () => { }); expect(inlined).toBeDefined(); + expect(inlined?.default).toBeUndefined(); expect(inlined?.required).toBeTruthy(); expect(inlined?.schema).toEqual({ kind: 'object', @@ -295,6 +306,7 @@ describe(`vue-component-meta`, () => { }); expect(literalFromContext).toBeDefined(); + expect(literalFromContext?.default).toBeUndefined(); expect(literalFromContext?.required).toBeTruthy(); expect(literalFromContext?.type).toEqual('"Uncategorized" | "Content" | "Interaction" | "Display" | "Forms" | "Addons"'); expect(literalFromContext?.description).toEqual('literal type alias that require context'); @@ -312,6 +324,7 @@ describe(`vue-component-meta`, () => { }); expect(recursive).toBeDefined(); + expect(recursive?.default).toBeUndefined(); expect(recursive?.required).toBeTruthy(); expect(recursive?.type).toEqual('MyNestedRecursiveProps'); expect(recursive?.schema).toEqual({ @@ -337,12 +350,55 @@ describe(`vue-component-meta`, () => { const foo = meta.props.find(prop => prop.name === 'foo'); expect(foo).toBeDefined(); + expect(foo?.default).toBeUndefined(); expect(foo?.required).toBeTruthy(); + expect(foo?.type).toEqual('string'); + }); + + test('reference-type-props-js-setup', () => { + const componentPath = path.resolve(__dirname, '../../vue-test-workspace/vue-component-meta/reference-type-props/component-js-setup.vue'); + const meta = checker.getComponentMeta(componentPath); + + const foo = meta.props.find(prop => prop.name === 'foo'); + const hello = meta.props.find(prop => prop.name === 'hello'); + const numberOrStringProp = meta.props.find(prop => prop.name === 'numberOrStringProp'); + const arrayProps = meta.props.find(prop => prop.name === 'arrayProps'); + + expect(foo).toBeDefined(); + expect(foo?.default).toBeUndefined(); + expect(foo?.required).toBeTruthy(); + // expect(foo?.type).toEqual('string | undefined'); // @todo should be 'string' + + expect(hello).toBeDefined(); + expect(hello?.default).toEqual('"Hello"'); + expect(hello?.type).toEqual('string | undefined'); + expect(hello?.required).toBeFalsy(); + + expect(numberOrStringProp).toBeDefined(); + expect(numberOrStringProp?.default).toEqual('42'); + expect(numberOrStringProp?.type).toEqual('string | number | undefined'); + expect(numberOrStringProp?.required).toBeFalsy(); + + expect(arrayProps).toBeDefined(); + // expect(arrayProps?.type).toEqual('unknown[] | undefined'); // @todo should be number[] + expect(arrayProps?.required).toBeFalsy(); + // expect(arrayProps?.schema).toEqual({ + // kind: 'enum', + // type: 'unknown[] | undefined', // @todo should be number[] + // schema: [ + // 'undefined', + // { + // kind: 'array', + // type: 'unknown[]', // @todo should be number[] + // schema: ['unknown'] // @todo should be number[] + // } + // ] + // }); }); test('reference-type-events', () => { const componentPath = path.resolve(__dirname, '../../vue-test-workspace/vue-component-meta/reference-type-events/component.vue'); - const meta = checker_schema.getComponentMeta(componentPath); + const meta = checker.getComponentMeta(componentPath); const onFoo = meta.events.find(event => event.name === 'foo'); const onBar = meta.events.find(event => event.name === 'bar'); diff --git a/packages/vue-test-workspace/vue-component-meta/reference-type-props/component-js-setup.vue b/packages/vue-test-workspace/vue-component-meta/reference-type-props/component-js-setup.vue new file mode 100644 index 000000000..8317c489c --- /dev/null +++ b/packages/vue-test-workspace/vue-component-meta/reference-type-props/component-js-setup.vue @@ -0,0 +1,28 @@ +