From d07c1f6431ab08503bce11ec625fe0db58185614 Mon Sep 17 00:00:00 2001 From: Sacha STAFYNIAK Date: Tue, 2 Aug 2022 17:50:08 +0200 Subject: [PATCH 1/2] fix: parse defineProps in script setup with option --- packages/vue-component-meta/src/index.ts | 90 ++++++++++++++----- .../vue-component-meta/tests/index.spec.ts | 79 +++++++++++++--- .../component-js-setup.vue | 28 ++++++ 3 files changed, 164 insertions(+), 33 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..56f176687 100644 --- a/packages/vue-component-meta/src/index.ts +++ b/packages/vue-component-meta/src/index.ts @@ -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) => { @@ -167,7 +167,10 @@ export function createComponentMetaChecker(tsconfigPath: string, checkerOptions: // fill defaults const printer = ts.createPrinter(checkerOptions.printer); 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, typeChecker) + : {}; 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 (typeof defaultExp.required !== 'undefined') { + prop.required = defaultExp.required; + } + + if (typeof 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, typeChecker: ts.TypeChecker) { + const result: Record = {}; scriptSetupWorker(); - sciptWorker(); + scriptWorker(); return result; @@ -463,27 +473,61 @@ function readVueComponentDefaultProps(vueFileText: string, printer: ts.Printer) if (ts.isPropertyAssignment(prop)) { const name = prop.name.getText(ast); const exp = printer.printNode(ts.EmitHint.Expression, resolveDefaultOptionExpression(prop.initializer), ast);; - result[name] = exp; + + result[name] = { + default: exp, + }; } } } + } 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); + if (obj) { + for (const prop of obj.properties) { + if (ts.isPropertyAssignment(prop)) { + const name = prop.name.getText(ast); + const resolved = resolveDefaultOptionExpression(prop.initializer); + + result[name] = { + required: false, + }; + + if (ts.isObjectLiteralExpression(resolved)) { + const defaultProp = resolved.properties.find(p => ts.isPropertyAssignment(p) && p.name.getText(ast) === 'default') as ts.PropertyAssignment | undefined; + const requiredProp = resolved.properties.find(p => ts.isPropertyAssignment(p) && p.name.getText(ast) === 'required') as ts.PropertyAssignment | undefined; + + if (requiredProp) { + const exp = printer.printNode(ts.EmitHint.Unspecified, requiredProp.initializer, ast); + result[name].required = exp === 'true'; + } + if (defaultProp) { + const exp = printer.printNode(ts.EmitHint.Expression, resolveDefaultOptionExpression((defaultProp as any).initializer), ast); + result[name].default = exp; + } + } } - }); - return result; + } + } + } + + 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(); @@ -499,7 +543,7 @@ function readVueComponentDefaultProps(vueFileText: string, printer: ts.Printer) function readTsComponentDefaultProps(lang: string, tsFileText: string, exportName: string, printer: ts.Printer) { - const result: Record = {}; + const result: Record = {}; const ast = ts.createSourceFile('/tmp.' + lang, tsFileText, ts.ScriptTarget.Latest); const props = getPropsNode(); @@ -512,7 +556,9 @@ function readTsComponentDefaultProps(lang: string, tsFileText: string, exportNam if (ts.isPropertyAssignment(propOption)) { if (propOption.name?.getText(ast) === 'default') { const _default = propOption.initializer; - result[name] = printer.printNode(ts.EmitHint.Expression, resolveDefaultOptionExpression(_default), ast); + result[name] = { + default: printer.printNode(ts.EmitHint.Expression, resolveDefaultOptionExpression(_default), ast) + }; } } } diff --git a/packages/vue-component-meta/tests/index.spec.ts b/packages/vue-component-meta/tests/index.spec.ts index bc0edbdd1..7fdd2f342 100644 --- a/packages/vue-component-meta/tests/index.spec.ts +++ b/packages/vue-component-meta/tests/index.spec.ts @@ -7,13 +7,9 @@ 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'], }, - printer: { newLine: 1 }, }); test('empty-component', () => { @@ -41,7 +37,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 +100,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 +125,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 +136,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 +157,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 +187,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 +216,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 +243,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 +277,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 +288,7 @@ describe(`vue-component-meta`, () => { }); expect(inlined).toBeDefined(); + expect(inlined?.default).toBeUndefined(); expect(inlined?.required).toBeTruthy(); expect(inlined?.schema).toEqual({ kind: 'object', @@ -295,6 +307,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 +325,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 +351,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 @@ + From 141f939154f3a6ff25b39e92a19a40be2006e596 Mon Sep 17 00:00:00 2001 From: johnsoncodehk Date: Wed, 3 Aug 2022 00:31:03 +0800 Subject: [PATCH 2/2] refactor: reuse props object resolver --- packages/vue-component-meta/src/index.ts | 100 ++++++++---------- .../vue-component-meta/tests/index.spec.ts | 5 +- 2 files changed, 49 insertions(+), 56 deletions(-) diff --git a/packages/vue-component-meta/src/index.ts b/packages/vue-component-meta/src/index.ts index 56f176687..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; }; `; } @@ -165,11 +165,11 @@ 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, typeChecker) + ? readVueComponentDefaultProps(snapshot.getText(0, snapshot.getLength()), printer) : {}; const tsDefaults = !componentPath.endsWith('.vue') ? readTsComponentDefaultProps( componentPath.substring(componentPath.lastIndexOf('.') + 1), // ts | js | tsx | jsx @@ -186,11 +186,11 @@ export function createComponentMetaChecker(tsconfigPath: string, checkerOptions: if (prop) { prop.default = defaultExp.default; - if (typeof defaultExp.required !== 'undefined') { + if (defaultExp.required !== undefined) { prop.required = defaultExp.required; } - if (typeof prop.default !== 'undefined') { + if (prop.default !== undefined) { prop.required = false; // props with default are always optional } } @@ -448,8 +448,8 @@ function createSchemaResolvers(typeChecker: ts.TypeChecker, symbolNode: ts.Expre }; } -function readVueComponentDefaultProps(vueFileText: string, printer: ts.Printer, typeChecker: ts.TypeChecker) { - const result: Record = {}; +function readVueComponentDefaultProps(vueFileText: string, printer: ts.Printer | undefined) { + let result: Record = {}; scriptSetupWorker(); scriptWorker(); @@ -472,10 +472,11 @@ 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);; + const expNode = resolveDefaultOptionExpression(prop.initializer); + const expText = printer?.printNode(ts.EmitHint.Expression, expNode, ast) ?? expNode.getText(ast); result[name] = { - default: exp, + default: expText, }; } } @@ -486,30 +487,10 @@ function readVueComponentDefaultProps(vueFileText: string, printer: ts.Printer, const obj = findObjectLiteralExpression(ast); if (obj) { - for (const prop of obj.properties) { - if (ts.isPropertyAssignment(prop)) { - const name = prop.name.getText(ast); - const resolved = resolveDefaultOptionExpression(prop.initializer); - - result[name] = { - required: false, - }; - - if (ts.isObjectLiteralExpression(resolved)) { - const defaultProp = resolved.properties.find(p => ts.isPropertyAssignment(p) && p.name.getText(ast) === 'default') as ts.PropertyAssignment | undefined; - const requiredProp = resolved.properties.find(p => ts.isPropertyAssignment(p) && p.name.getText(ast) === 'required') as ts.PropertyAssignment | undefined; - - if (requiredProp) { - const exp = printer.printNode(ts.EmitHint.Unspecified, requiredProp.initializer, ast); - result[name].required = exp === 'true'; - } - if (defaultProp) { - const exp = printer.printNode(ts.EmitHint.Expression, resolveDefaultOptionExpression((defaultProp as any).initializer), ast); - result[name].default = exp; - } - } - } - } + result = { + ...result, + ...resolvePropsOption(ast, obj, printer), + }; } } @@ -541,33 +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] = { - default: printer.printNode(ts.EmitHint.Expression, resolveDefaultOptionExpression(_default), ast) - }; - } - } - } - } - } - } + return resolvePropsOption(ast, props, printer); } - return result; + return {}; function getComponentNode() { @@ -632,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 7fdd2f342..6e52486cd 100644 --- a/packages/vue-component-meta/tests/index.spec.ts +++ b/packages/vue-component-meta/tests/index.spec.ts @@ -7,9 +7,8 @@ describe(`vue-component-meta`, () => { const tsconfigPath = path.resolve(__dirname, '../../vue-test-workspace/vue-component-meta/tsconfig.json'); const checker = createComponentMetaChecker(tsconfigPath, { forceUseTs: true, - schema: { - ignore: ['MyIgnoredNestedProps'], - }, + schema: { ignore: ['MyIgnoredNestedProps'] }, + printer: { newLine: 1 }, }); test('empty-component', () => {