From ad6f42cbd959c60f36cd4154881978cd16163695 Mon Sep 17 00:00:00 2001 From: Sacha STAFYNIAK Date: Sat, 30 Jul 2022 00:38:37 +0200 Subject: [PATCH 01/13] feat: export component meta types --- packages/vue-component-meta/src/index.ts | 32 ++++++++++++++++++++---- 1 file changed, 27 insertions(+), 5 deletions(-) diff --git a/packages/vue-component-meta/src/index.ts b/packages/vue-component-meta/src/index.ts index 156f42d10..d12f72119 100644 --- a/packages/vue-component-meta/src/index.ts +++ b/packages/vue-component-meta/src/index.ts @@ -1,7 +1,13 @@ import * as vue from '@volar/vue-language-core'; import * as ts from 'typescript/lib/tsserverlibrary'; -export type PropertyMeta = { +export interface ComponentMeta { + props: PropertyMeta[] + events: EventMeta[] + slots: SlotMeta[] + exposed: ExposeMeta[] +} +export interface PropertyMeta { name: string; default?: string; description: string; @@ -10,6 +16,22 @@ export type PropertyMeta = { tags: { name: string, text?: string; }[]; schema: PropertyMetaSchema; }; +export interface EventMeta { + name: string; + type: string; + signature: string; + schema: PropertyMetaSchema[]; +} +export interface SlotMeta { + name: string; + type: string; + description: string; +} +export interface ExposeMeta { + name: string; + type: string; + description: string; +} export type PropertyMetaSchema = string | { kind: 'enum', type: string, schema: PropertyMetaSchema[]; } @@ -185,7 +207,7 @@ export function createComponentMetaChecker(tsconfigPath: string) { return _getExports(componentPath).exports.map(e => e.getName()); } - function getComponentMeta(componentPath: string, exportName = 'default') { + function getComponentMeta(componentPath: string, exportName = 'default'): ComponentMeta { const { symbolNode, exports } = _getExports(componentPath); const _export = exports.find((property) => property.getName() === exportName); @@ -246,7 +268,7 @@ export function createComponentMetaChecker(tsconfigPath: string) { type: typeChecker.typeToString(typeChecker.getTypeOfSymbolAtLocation(call.parameters[1], symbolNode!)), signature: typeChecker.signatureToString(call), schema: typeChecker.getTypeArguments(typeChecker.getTypeOfSymbolAtLocation(call.parameters[1], symbolNode!) as ts.TypeReference).map(resolveSchema), - })); + } as EventMeta)); } return []; @@ -264,7 +286,7 @@ export function createComponentMetaChecker(tsconfigPath: string) { name: prop.getName(), type: typeChecker.typeToString(typeChecker.getTypeOfSymbolAtLocation(typeChecker.getTypeOfSymbolAtLocation(prop, symbolNode!).getCallSignatures()[0].parameters[0], symbolNode!)), description: ts.displayPartsToString(prop.getDocumentationComment(typeChecker)), - })); + } as SlotMeta)); } return []; @@ -282,7 +304,7 @@ export function createComponentMetaChecker(tsconfigPath: string) { name: expose.getName(), type: typeChecker.typeToString(typeChecker.getTypeOfSymbolAtLocation(expose, symbolNode!)), description: ts.displayPartsToString(expose.getDocumentationComment(typeChecker)), - })); + } as ExposeMeta)); } return []; From a3e10027269f267dc34fa73535a9a25f775e7225 Mon Sep 17 00:00:00 2001 From: Sacha STAFYNIAK Date: Sat, 30 Jul 2022 00:42:07 +0200 Subject: [PATCH 02/13] chose: meta types signature --- packages/vue-component-meta/src/index.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/vue-component-meta/src/index.ts b/packages/vue-component-meta/src/index.ts index d12f72119..03ae12edc 100644 --- a/packages/vue-component-meta/src/index.ts +++ b/packages/vue-component-meta/src/index.ts @@ -226,7 +226,7 @@ export function createComponentMetaChecker(tsconfigPath: string) { exposed: getExposed(), }; - function getProps() { + function getProps(): PropertyMeta[] { const $props = symbolProperties.find(prop => prop.escapedName === '$props'); let result: PropertyMeta[] = []; @@ -254,7 +254,7 @@ export function createComponentMetaChecker(tsconfigPath: string) { return result; } - function getEvents() { + function getEvents(): EventMeta[] { const $emit = symbolProperties.find(prop => prop.escapedName === '$emit'); @@ -268,13 +268,13 @@ export function createComponentMetaChecker(tsconfigPath: string) { type: typeChecker.typeToString(typeChecker.getTypeOfSymbolAtLocation(call.parameters[1], symbolNode!)), signature: typeChecker.signatureToString(call), schema: typeChecker.getTypeArguments(typeChecker.getTypeOfSymbolAtLocation(call.parameters[1], symbolNode!) as ts.TypeReference).map(resolveSchema), - } as EventMeta)); + })); } return []; } - function getSlots() { + function getSlots(): SlotMeta[] { const propertyName = (parsedCommandLine.vueOptions.target ?? 3) < 3 ? '$scopedSlots' : '$slots'; const $slots = symbolProperties.find(prop => prop.escapedName === propertyName); @@ -286,13 +286,13 @@ export function createComponentMetaChecker(tsconfigPath: string) { name: prop.getName(), type: typeChecker.typeToString(typeChecker.getTypeOfSymbolAtLocation(typeChecker.getTypeOfSymbolAtLocation(prop, symbolNode!).getCallSignatures()[0].parameters[0], symbolNode!)), description: ts.displayPartsToString(prop.getDocumentationComment(typeChecker)), - } as SlotMeta)); + })); } return []; } - function getExposed() { + function getExposed(): ExposeMeta[] { const exposed = symbolProperties.filter(prop => // only exposed props will have a syntheticOrigin @@ -304,7 +304,7 @@ export function createComponentMetaChecker(tsconfigPath: string) { name: expose.getName(), type: typeChecker.typeToString(typeChecker.getTypeOfSymbolAtLocation(expose, symbolNode!)), description: ts.displayPartsToString(expose.getDocumentationComment(typeChecker)), - } as ExposeMeta)); + })); } return []; From e5b071b2437bf3bc9bf2b4b4cda1cb75f9081f7f Mon Sep 17 00:00:00 2001 From: Sacha STAFYNIAK Date: Sat, 30 Jul 2022 16:39:29 +0200 Subject: [PATCH 03/13] feat: add schema extraction option + resolve inline objects --- packages/vue-component-meta/src/index.ts | 320 ++++++++++-------- packages/vue-component-meta/src/types.ts | 49 +++ .../vue-component-meta/tests/index.spec.ts | 91 +++-- .../reference-type-props/my-props.ts | 8 +- 4 files changed, 304 insertions(+), 164 deletions(-) create mode 100644 packages/vue-component-meta/src/types.ts diff --git a/packages/vue-component-meta/src/index.ts b/packages/vue-component-meta/src/index.ts index 03ae12edc..24c959150 100644 --- a/packages/vue-component-meta/src/index.ts +++ b/packages/vue-component-meta/src/index.ts @@ -1,119 +1,29 @@ import * as vue from '@volar/vue-language-core'; import * as ts from 'typescript/lib/tsserverlibrary'; -export interface ComponentMeta { - props: PropertyMeta[] - events: EventMeta[] - slots: SlotMeta[] - exposed: ExposeMeta[] +import type { + MetaCheckerOptions, + ComponentMeta, + EventMeta, + ExposeMeta, + MetaCheckerSchemaOptions, + PropertyMeta, + PropertyMetaSchema, + SlotMeta +} from './types'; + +export type { + MetaCheckerOptions, + ComponentMeta, + EventMeta, + ExposeMeta, + MetaCheckerSchemaOptions, + PropertyMeta, + PropertyMetaSchema, + SlotMeta } -export interface PropertyMeta { - name: string; - default?: string; - description: string; - required: boolean; - type: string; - tags: { name: string, text?: string; }[]; - schema: PropertyMetaSchema; -}; -export interface EventMeta { - name: string; - type: string; - signature: string; - schema: PropertyMetaSchema[]; -} -export interface SlotMeta { - name: string; - type: string; - description: string; -} -export interface ExposeMeta { - name: string; - type: string; - description: string; -} - -export type PropertyMetaSchema = string - | { kind: 'enum', type: string, schema: PropertyMetaSchema[]; } - | { kind: 'array', type: string, schema: PropertyMetaSchema[]; } - | { kind: 'event', type: string, schema: PropertyMetaSchema[]; } - | { kind: 'object', type: string, schema: Record; }; - -function createSchemaResolvers(typeChecker: ts.TypeChecker, symbolNode: ts.Expression) { - function reducer(acc: any, cur: any) { - acc[cur.name] = cur; - return acc; - } - function resolveSymbolSchema(prop: ts.Symbol): PropertyMeta { - const subtype = typeChecker.getTypeOfSymbolAtLocation(prop, symbolNode!); - typeChecker.getDefaultFromTypeParameter(subtype); - - return { - name: prop.getEscapedName().toString(), - description: ts.displayPartsToString(prop.getDocumentationComment(typeChecker)), - tags: prop.getJsDocTags(typeChecker).map(tag => ({ - name: tag.name, - text: tag.text?.map(part => part.text).join(''), - })), - required: !Boolean((prop.declarations?.[0] as ts.ParameterDeclaration)?.questionToken ?? false), - type: typeChecker.typeToString(subtype), - schema: resolveSchema(subtype), - }; - } - function resolveCallbackSchema(signature: ts.Signature): PropertyMetaSchema { - return { - kind: 'event', - type: typeChecker.signatureToString(signature), - schema: typeChecker.getTypeArguments(typeChecker.getTypeOfSymbolAtLocation(signature.parameters[0], symbolNode) as ts.TypeReference).map(resolveSchema) - }; - } - function resolveEventSchema(subtype: ts.Type): PropertyMetaSchema { - return (subtype.getCallSignatures().length === 1) - ? resolveCallbackSchema(subtype.getCallSignatures()[0]) - : typeChecker.typeToString(subtype); - } - function resolveNestedSchema(subtype: ts.Type): PropertyMetaSchema { - // !!(subtype.flags & ts.TypeFlags.Object) - return (subtype.isClassOrInterface() || subtype.isIntersection()) - ? { - kind: 'object', - type: typeChecker.typeToString(subtype), - schema: subtype.getProperties().map(resolveSymbolSchema).reduce(reducer, {}) - } - : resolveEventSchema(subtype); - } - function resolveArraySchema(subtype: ts.Type): PropertyMetaSchema { - // @ts-ignore - typescript internal, isArrayLikeType exists - return typeChecker.isArrayLikeType(subtype) - ? { - kind: 'array', - type: typeChecker.typeToString(subtype), - schema: typeChecker.getTypeArguments(subtype as ts.TypeReference).map(resolveSchema) - } - : resolveNestedSchema(subtype); - } - function resolveSchema(subtype: ts.Type): PropertyMetaSchema { - return subtype.isUnion() - ? { - kind: 'enum', - type: typeChecker.typeToString(subtype), - schema: subtype.types.map(resolveArraySchema) - } - : resolveArraySchema(subtype); - } - - return { - resolveSymbolSchema, - resolveCallbackSchema, - resolveEventSchema, - resolveNestedSchema, - resolveArraySchema, - resolveSchema, - }; -} - -export function createComponentMetaChecker(tsconfigPath: string) { +export function createComponentMetaChecker(tsconfigPath: string, checkerOptions: MetaCheckerOptions = {}) { const parsedCommandLine = vue.tsShared.createParsedCommandLine(ts, { useCaseSensitiveFileNames: ts.sys.useCaseSensitiveFileNames, readDirectory: (path, extensions, exclude, include, depth) => { @@ -218,6 +128,12 @@ export function createComponentMetaChecker(tsconfigPath: string) { const componentType = typeChecker.getTypeOfSymbolAtLocation(_export, symbolNode!); const symbolProperties = componentType.getProperties() ?? []; + const { + resolveNestedProperties, + resolveEventSignature, + resolveExposedProperties, + resolveSlotProperties, + } = createSchemaResolvers(typeChecker, symbolNode!, checkerOptions.schema); return { props: getProps(), @@ -226,17 +142,19 @@ export function createComponentMetaChecker(tsconfigPath: string) { exposed: getExposed(), }; - function getProps(): PropertyMeta[] { + function getProps() { const $props = symbolProperties.find(prop => prop.escapedName === '$props'); + const propEventRegex = /^(on[A-Z])/; let result: PropertyMeta[] = []; if ($props) { const type = typeChecker.getTypeOfSymbolAtLocation($props, symbolNode!); const properties = type.getApparentProperties(); - const { resolveSymbolSchema } = createSchemaResolvers(typeChecker, symbolNode!); - result = properties.map(resolveSymbolSchema); + result = properties + .map(resolveNestedProperties) + .filter((prop) => !prop.name.match(propEventRegex)); } // fill defaults @@ -254,21 +172,15 @@ export function createComponentMetaChecker(tsconfigPath: string) { return result; } - function getEvents(): EventMeta[] { + function getEvents() { const $emit = symbolProperties.find(prop => prop.escapedName === '$emit'); if ($emit) { const type = typeChecker.getTypeOfSymbolAtLocation($emit, symbolNode!); const calls = type.getCallSignatures(); - const { resolveSchema } = createSchemaResolvers(typeChecker, symbolNode!); - - return calls.map(call => ({ - name: (typeChecker.getTypeOfSymbolAtLocation(call.parameters[0], symbolNode!) as ts.StringLiteralType).value, - type: typeChecker.typeToString(typeChecker.getTypeOfSymbolAtLocation(call.parameters[1], symbolNode!)), - signature: typeChecker.signatureToString(call), - schema: typeChecker.getTypeArguments(typeChecker.getTypeOfSymbolAtLocation(call.parameters[1], symbolNode!) as ts.TypeReference).map(resolveSchema), - })); + + return calls.map(resolveEventSignature).filter(event => event.name); } return []; @@ -282,29 +194,21 @@ export function createComponentMetaChecker(tsconfigPath: string) { if ($slots) { const type = typeChecker.getTypeOfSymbolAtLocation($slots, symbolNode!); const properties = type.getProperties(); - return properties.map(prop => ({ - name: prop.getName(), - type: typeChecker.typeToString(typeChecker.getTypeOfSymbolAtLocation(typeChecker.getTypeOfSymbolAtLocation(prop, symbolNode!).getCallSignatures()[0].parameters[0], symbolNode!)), - description: ts.displayPartsToString(prop.getDocumentationComment(typeChecker)), - })); + + return properties.map(resolveSlotProperties); } return []; } function getExposed(): ExposeMeta[] { - const exposed = symbolProperties.filter(prop => // only exposed props will have a syntheticOrigin Boolean((prop as any).syntheticOrigin) ); - + if (exposed.length) { - return exposed.map(expose => ({ - name: expose.getName(), - type: typeChecker.typeToString(typeChecker.getTypeOfSymbolAtLocation(expose, symbolNode!)), - description: ts.displayPartsToString(expose.getDocumentationComment(typeChecker)), - })); + return exposed.map(resolveExposedProperties); } return []; @@ -350,6 +254,152 @@ export function createComponentMetaChecker(tsconfigPath: string) { } } +function createSchemaResolvers(typeChecker: ts.TypeChecker, symbolNode: ts.Expression, options: MetaCheckerSchemaOptions = {}) { + const ignore = options.ignore ?? []; + const enabled = options.enabled ?? false; + + function shouldIgnore(subtype: ts.Type) { + if (ignore.length === 0) { + return false + } + + const type = typeChecker.typeToString(subtype); + return type === 'any' || ignore.includes(type) + } + + function reducer(acc: any, cur: any) { + acc[cur.name] = cur; + return acc; + } + + function resolveNestedProperties(prop: ts.Symbol): PropertyMeta { + const subtype = typeChecker.getTypeOfSymbolAtLocation(prop, symbolNode!); + const schema = enabled ? resolveSchema(subtype) : undefined + + return { + name: prop.getEscapedName().toString(), + description: ts.displayPartsToString(prop.getDocumentationComment(typeChecker)), + tags: prop.getJsDocTags(typeChecker).map(tag => ({ + name: tag.name, + text: tag.text?.map(part => part.text).join(''), + })), + required: !Boolean((prop.declarations?.[0] as ts.ParameterDeclaration)?.questionToken ?? false), + type: typeChecker.typeToString(subtype), + schema, + }; + } + function resolveSlotProperties(prop: ts.Symbol): SlotMeta { + const subtype = typeChecker.getTypeOfSymbolAtLocation(typeChecker.getTypeOfSymbolAtLocation(prop, symbolNode!).getCallSignatures()[0].parameters[0], symbolNode!) + const schema = enabled ? resolveSchema(subtype) : undefined + + return { + name: prop.getName(), + type: typeChecker.typeToString(subtype), + description: ts.displayPartsToString(prop.getDocumentationComment(typeChecker)), + schema, + } + } + function resolveExposedProperties(expose: ts.Symbol): ExposeMeta { + const subtype = typeChecker.getTypeOfSymbolAtLocation(expose, symbolNode!) + const schema = enabled ? resolveSchema(subtype) : undefined + + return { + name: expose.getName(), + type: typeChecker.typeToString(subtype), + description: ts.displayPartsToString(expose.getDocumentationComment(typeChecker)), + schema, + } + } + function resolveEventSignature(call: ts.Signature): EventMeta { + const schema = enabled + ? resolveSchema( + typeChecker + .getTypeArguments(typeChecker.getTypeOfSymbolAtLocation(call.parameters[1], symbolNode!) as ts.TypeReference)[0] + ) + : undefined + + return { + name: (typeChecker.getTypeOfSymbolAtLocation(call.parameters[0], symbolNode!) as ts.StringLiteralType).value, + type: typeChecker.typeToString(typeChecker.getTypeOfSymbolAtLocation(call.parameters[1], symbolNode!)), + signature: typeChecker.signatureToString(call), + schema, + } + } + + function resolveCallbackSchema(signature: ts.Signature): PropertyMetaSchema { + const schema = enabled + ? typeChecker + .getTypeArguments(typeChecker.getTypeOfSymbolAtLocation(signature.parameters[0], symbolNode) as ts.TypeReference) + .map(resolveSchema) + : undefined + + return { + kind: 'event', + type: typeChecker.signatureToString(signature), + schema, + }; + } + function resolveEventSchema(subtype: ts.Type): PropertyMetaSchema { + return (subtype.getCallSignatures().length === 1) + ? resolveCallbackSchema(subtype.getCallSignatures()[0]) + : typeChecker.typeToString(subtype); + } + function resolveNestedSchema(subtype: ts.Type): PropertyMetaSchema { + if ( + subtype.getCallSignatures().length === 0 && + (subtype.isClassOrInterface() || subtype.isIntersection() || (subtype as ts.ObjectType).objectFlags & ts.ObjectFlags.Anonymous) + ) { + if (shouldIgnore(subtype)) { + return typeChecker.typeToString(subtype) + } + + return { + kind: 'object', + type: typeChecker.typeToString(subtype), + schema: subtype.getProperties().map(resolveNestedProperties).reduce(reducer, {}) + } + } + return resolveEventSchema(subtype); + } + function resolveArraySchema(subtype: ts.Type): PropertyMetaSchema { + // @ts-ignore - typescript internal, isArrayLikeType exists + if (typeChecker.isArrayLikeType(subtype)) { + if (shouldIgnore(subtype)) { + return typeChecker.typeToString(subtype) + } + + return { + kind: 'array', + type: typeChecker.typeToString(subtype), + schema: typeChecker.getTypeArguments(subtype as ts.TypeReference).map(resolveSchema) + } + } + + return resolveNestedSchema(subtype); + } + function resolveSchema(subtype: ts.Type): PropertyMetaSchema { + return subtype.isUnion() + ? { + kind: 'enum', + type: typeChecker.typeToString(subtype), + schema: subtype.types.map(resolveArraySchema) + } + : resolveArraySchema(subtype); + } + + return { + resolveNestedProperties, + resolveSlotProperties, + resolveEventSignature, + resolveExposedProperties, + resolveCallbackSchema, + resolveEventSchema, + resolveNestedSchema, + resolveArraySchema, + resolveSchema, + }; +} + function readCmponentDefaultProps(fileText: string) { const vueSourceFile = vue.createSourceFile('/tmp.vue', fileText, {}, {}, ts); diff --git a/packages/vue-component-meta/src/types.ts b/packages/vue-component-meta/src/types.ts new file mode 100644 index 000000000..5c221442e --- /dev/null +++ b/packages/vue-component-meta/src/types.ts @@ -0,0 +1,49 @@ + + +export interface ComponentMeta { + props: PropertyMeta[] + events: EventMeta[] + slots: SlotMeta[] + exposed: ExposeMeta[] +} +export interface PropertyMeta { + name: string; + default?: string; + description: string; + required: boolean; + type: string; + tags: { name: string, text?: string; }[]; + schema?: PropertyMetaSchema; +}; +export interface EventMeta { + name: string; + type: string; + signature: string; + schema?: PropertyMetaSchema; +} +export interface SlotMeta { + name: string; + type: string; + description: string; + schema?: PropertyMetaSchema; +} +export interface ExposeMeta { + name: string; + description: string; + type: string; + schema?: PropertyMetaSchema; +} + +export type PropertyMetaSchema = string + | { kind: 'enum', type: string, schema?: PropertyMetaSchema[]; } + | { kind: 'array', type: string, schema?: PropertyMetaSchema[]; } + | { kind: 'event', type: string, schema?: PropertyMetaSchema[]; } + | { kind: 'object', type: string, schema?: Record; }; + +export interface MetaCheckerSchemaOptions { + enabled?: boolean; + ignore?: string[]; +} +export interface MetaCheckerOptions { + schema?: MetaCheckerSchemaOptions +} \ No newline at end of file diff --git a/packages/vue-component-meta/tests/index.spec.ts b/packages/vue-component-meta/tests/index.spec.ts index bfd9c35ab..77a557e33 100644 --- a/packages/vue-component-meta/tests/index.spec.ts +++ b/packages/vue-component-meta/tests/index.spec.ts @@ -5,7 +5,12 @@ import * as metaChecker from '..'; describe(`vue-component-meta`, () => { const tsconfigPath = path.resolve(__dirname, '../../vue-test-workspace/vue-component-meta/tsconfig.json'); - const checker = metaChecker.createComponentMetaChecker(tsconfigPath); + const checker = metaChecker.createComponentMetaChecker(tsconfigPath, { + schema: { + enabled: true, + ignore: ['MyIgnoredNestedProps', 'VNode', 'VNodeMountHook', 'RendererNode', 'RendererElement'] + } + }); test('global-props', () => { @@ -44,7 +49,7 @@ describe(`vue-component-meta`, () => { const arrayOptional = meta.props.find(prop => prop.name === 'arrayOptional'); const enumValue = meta.props.find(prop => prop.name === 'enumValue'); const literalFromContext = meta.props.find(prop => prop.name === 'literalFromContext'); - const literal = meta.props.find(prop => prop.name === 'literal'); + const inlined = meta.props.find(prop => prop.name === 'inlined'); // const onEvent = meta.props.find(prop => prop.name === 'onEvent'); expect(foo).toBeDefined(); @@ -162,11 +167,11 @@ describe(`vue-component-meta`, () => { expect(nestedOptional).toBeDefined(); expect(nestedOptional?.required).toBeFalsy(); - expect(nestedOptional?.type).toEqual('MyNestedProps | undefined'); + expect(nestedOptional?.type).toEqual('MyNestedProps | MyIgnoredNestedProps | undefined'); expect(nestedOptional?.description).toEqual('optional nested object'); expect(nestedOptional?.schema).toEqual({ kind: 'enum', - type: 'MyNestedProps | undefined', + type: 'MyNestedProps | MyIgnoredNestedProps | undefined', schema: [ 'undefined', { @@ -182,7 +187,8 @@ describe(`vue-component-meta`, () => { schema: 'string' } } - } + }, + 'MyIgnoredNestedProps', ] }); @@ -253,25 +259,22 @@ describe(`vue-component-meta`, () => { schema: ['MyEnum.Small', 'MyEnum.Medium', 'MyEnum.Large'] }); - expect(literal).toBeDefined(); - expect(literal?.required).toBeTruthy(); - expect(literal?.type).toEqual('{ foo: string; }'); - - // todo: this should be resolved to a type alias - // expect(literal?.schema).toEqual({ - // kind: 'object', - // type: '{ foo: string; }', - // schema: { - // foo: { - // name: 'foo', - // description: '', - // tags: [], - // required: true, - // type: 'string', - // schema: 'string' - // } - // } - // }) + expect(inlined).toBeDefined(); + expect(inlined?.required).toBeTruthy(); + expect(inlined?.schema).toEqual({ + kind: 'object', + type: '{ foo: string; }', + schema: { + foo: { + name: 'foo', + description: '', + tags: [], + required: true, + type: 'string', + schema: 'string' + } + } + }) expect(literalFromContext).toBeDefined(); expect(literalFromContext?.required).toBeTruthy(); @@ -331,7 +334,20 @@ describe(`vue-component-meta`, () => { type: '{ foo: string; } | undefined', schema: [ 'undefined', - '{ foo: string; }' // todo: this should be resolved to a type alias + { + kind: 'object', + type: '{ foo: string; }', + schema: { + foo: { + name: 'foo', + description: '', + tags: [], + required: true, + type: 'string', + schema: 'string' + } + } + } ], } ]); @@ -340,7 +356,28 @@ describe(`vue-component-meta`, () => { expect(onBar?.type).toEqual('[value: { arg1: number; arg2?: any; }]'); expect(onBar?.signature).toEqual('(event: "bar", value: { arg1: number; arg2?: any; }): void'); expect(onBar?.schema).toEqual([ - '{ arg1: number; arg2?: any; }' // todo: this should be resolved to a type alias + { + kind: 'object', + type: '{ arg1: number; arg2?: any; }', + schema: { + arg1: { + name: 'arg1', + description: '', + tags: [], + required: true, + type: 'number', + schema: 'number' + }, + arg2: { + name: 'arg2', + description: '', + tags: [], + required: false, + type: 'any', + schema: 'any' + }, + } + } ]); expect(onBaz).toBeDefined(); @@ -447,4 +484,4 @@ describe(`vue-component-meta`, () => { expect(a).toBeDefined(); expect(b).toBeDefined(); }); -}); +}); \ No newline at end of file diff --git a/packages/vue-test-workspace/vue-component-meta/reference-type-props/my-props.ts b/packages/vue-test-workspace/vue-component-meta/reference-type-props/my-props.ts index 0b77e324c..53bf591c8 100644 --- a/packages/vue-test-workspace/vue-component-meta/reference-type-props/my-props.ts +++ b/packages/vue-test-workspace/vue-component-meta/reference-type-props/my-props.ts @@ -5,6 +5,10 @@ export interface MyNestedProps { nestedProp: string; } +export interface MyIgnoredNestedProps { + nestedProp: string; +} + enum MyEnum { Small, Medium, @@ -69,7 +73,7 @@ export interface MyProps { /** * optional nested object */ - nestedOptional?: MyNestedProps, + nestedOptional?: MyNestedProps | MyIgnoredNestedProps, /** * required array object */ @@ -86,5 +90,5 @@ export interface MyProps { * literal type alias that require context */ literalFromContext: MyCategories, - literal: { foo: string }, + inlined: { foo: string }, } From 3e5a4452326808c59f4f916ab8f1a5519ac30bf4 Mon Sep 17 00:00:00 2001 From: Sacha STAFYNIAK Date: Sat, 30 Jul 2022 16:56:01 +0200 Subject: [PATCH 04/13] fix: EventMeta schema --- packages/vue-component-meta/src/index.ts | 12 +++++------- packages/vue-component-meta/src/types.ts | 2 +- packages/vue-component-meta/tests/index.spec.ts | 6 ------ 3 files changed, 6 insertions(+), 14 deletions(-) diff --git a/packages/vue-component-meta/src/index.ts b/packages/vue-component-meta/src/index.ts index 24c959150..7e720a5b5 100644 --- a/packages/vue-component-meta/src/index.ts +++ b/packages/vue-component-meta/src/index.ts @@ -311,23 +311,21 @@ function createSchemaResolvers(typeChecker: ts.TypeChecker, symbolNode: ts.Expre } } function resolveEventSignature(call: ts.Signature): EventMeta { + const subtype = typeChecker.getTypeOfSymbolAtLocation(call.parameters[1], symbolNode!) const schema = enabled - ? resolveSchema( - typeChecker - .getTypeArguments(typeChecker.getTypeOfSymbolAtLocation(call.parameters[1], symbolNode!) as ts.TypeReference)[0] - ) - : undefined + ? typeChecker.getTypeArguments(subtype as ts.TypeReference).map(resolveSchema) + : undefined return { name: (typeChecker.getTypeOfSymbolAtLocation(call.parameters[0], symbolNode!) as ts.StringLiteralType).value, - type: typeChecker.typeToString(typeChecker.getTypeOfSymbolAtLocation(call.parameters[1], symbolNode!)), + type: typeChecker.typeToString(subtype), signature: typeChecker.signatureToString(call), schema, } } function resolveCallbackSchema(signature: ts.Signature): PropertyMetaSchema { - const schema = enabled + const schema = enabled && signature.parameters.length > 0 ? typeChecker .getTypeArguments(typeChecker.getTypeOfSymbolAtLocation(signature.parameters[0], symbolNode) as ts.TypeReference) .map(resolveSchema) diff --git a/packages/vue-component-meta/src/types.ts b/packages/vue-component-meta/src/types.ts index 5c221442e..989463c37 100644 --- a/packages/vue-component-meta/src/types.ts +++ b/packages/vue-component-meta/src/types.ts @@ -19,7 +19,7 @@ export interface EventMeta { name: string; type: string; signature: string; - schema?: PropertyMetaSchema; + schema?: PropertyMetaSchema[]; } export interface SlotMeta { name: string; diff --git a/packages/vue-component-meta/tests/index.spec.ts b/packages/vue-component-meta/tests/index.spec.ts index 77a557e33..a182c583b 100644 --- a/packages/vue-component-meta/tests/index.spec.ts +++ b/packages/vue-component-meta/tests/index.spec.ts @@ -21,12 +21,6 @@ describe(`vue-component-meta`, () => { 'ref', 'ref_for', 'ref_key', - 'onVnodeBeforeMount', - 'onVnodeMounted', - 'onVnodeBeforeUpdate', - 'onVnodeUpdated', - 'onVnodeBeforeUnmount', - 'onVnodeUnmounted', 'class', 'style', ]); From a2d3b323b850b29f70ea5067646ad30b35239bf3 Mon Sep 17 00:00:00 2001 From: Sacha STAFYNIAK Date: Sat, 30 Jul 2022 16:57:29 +0200 Subject: [PATCH 05/13] chore: remove return type --- packages/vue-component-meta/src/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/vue-component-meta/src/index.ts b/packages/vue-component-meta/src/index.ts index 7e720a5b5..08223b43b 100644 --- a/packages/vue-component-meta/src/index.ts +++ b/packages/vue-component-meta/src/index.ts @@ -186,7 +186,7 @@ export function createComponentMetaChecker(tsconfigPath: string, checkerOptions: return []; } - function getSlots(): SlotMeta[] { + function getSlots() { const propertyName = (parsedCommandLine.vueOptions.target ?? 3) < 3 ? '$scopedSlots' : '$slots'; const $slots = symbolProperties.find(prop => prop.escapedName === propertyName); @@ -201,7 +201,7 @@ export function createComponentMetaChecker(tsconfigPath: string, checkerOptions: return []; } - function getExposed(): ExposeMeta[] { + function getExposed() { const exposed = symbolProperties.filter(prop => // only exposed props will have a syntheticOrigin Boolean((prop as any).syntheticOrigin) From 61b4e7d5e707f6520a11926af8755705cd69a44e Mon Sep 17 00:00:00 2001 From: Sacha STAFYNIAK Date: Sat, 30 Jul 2022 16:59:05 +0200 Subject: [PATCH 06/13] chore: remove return type --- packages/vue-component-meta/src/types.ts | 2 +- packages/vue-component-meta/tests/index.spec.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/vue-component-meta/src/types.ts b/packages/vue-component-meta/src/types.ts index 989463c37..ee5a40eb5 100644 --- a/packages/vue-component-meta/src/types.ts +++ b/packages/vue-component-meta/src/types.ts @@ -46,4 +46,4 @@ export interface MetaCheckerSchemaOptions { } export interface MetaCheckerOptions { schema?: MetaCheckerSchemaOptions -} \ No newline at end of file +} diff --git a/packages/vue-component-meta/tests/index.spec.ts b/packages/vue-component-meta/tests/index.spec.ts index a182c583b..96371d21b 100644 --- a/packages/vue-component-meta/tests/index.spec.ts +++ b/packages/vue-component-meta/tests/index.spec.ts @@ -478,4 +478,4 @@ describe(`vue-component-meta`, () => { expect(a).toBeDefined(); expect(b).toBeDefined(); }); -}); \ No newline at end of file +}); From ba845f448c5df486498f74751fab92a069c428fe Mon Sep 17 00:00:00 2001 From: Sacha STAFYNIAK Date: Sat, 30 Jul 2022 17:38:07 +0200 Subject: [PATCH 07/13] chore: add test empty component --- packages/vue-component-meta/tests/index.spec.ts | 15 +++++++++++++++ .../empty-component/component.vue | 3 +++ 2 files changed, 18 insertions(+) create mode 100644 packages/vue-test-workspace/vue-component-meta/empty-component/component.vue diff --git a/packages/vue-component-meta/tests/index.spec.ts b/packages/vue-component-meta/tests/index.spec.ts index 96371d21b..e5a6b1b2a 100644 --- a/packages/vue-component-meta/tests/index.spec.ts +++ b/packages/vue-component-meta/tests/index.spec.ts @@ -26,6 +26,21 @@ describe(`vue-component-meta`, () => { ]); }); + test('empty-component', () => { + const componentPath = path.resolve(__dirname, '../../vue-test-workspace/vue-component-meta/empty-component/component.vue'); + const meta = checker.getComponentMeta(componentPath); + const globalPropNames = checker.getGlobalPropNames(); + + meta.props = meta.props.filter(prop => !globalPropNames.includes(prop.name)) + + expect(meta).toEqual({ + props: [], + events: [], + slots: [], + exposed: [], + }) + }); + test('reference-type-props', () => { const componentPath = path.resolve(__dirname, '../../vue-test-workspace/vue-component-meta/reference-type-props/component.vue'); diff --git a/packages/vue-test-workspace/vue-component-meta/empty-component/component.vue b/packages/vue-test-workspace/vue-component-meta/empty-component/component.vue new file mode 100644 index 000000000..6efc99a1f --- /dev/null +++ b/packages/vue-test-workspace/vue-component-meta/empty-component/component.vue @@ -0,0 +1,3 @@ + \ No newline at end of file From d5df704dfbbe9ba1cfdb219c38e086eaea1835c0 Mon Sep 17 00:00:00 2001 From: Sacha STAFYNIAK Date: Sat, 30 Jul 2022 19:51:05 +0200 Subject: [PATCH 08/13] feat: allow to load jscomponents --- packages/vue-component-meta/src/index.ts | 5 +++++ packages/vue-component-meta/tests/index.spec.ts | 10 ++++++++++ .../vue-component-meta/empty-component/component.vue | 6 +++++- .../reference-type-props/component-js.vue | 12 ++++++++++++ .../vue-component-meta/tsconfig.json | 3 ++- 5 files changed, 34 insertions(+), 2 deletions(-) create mode 100644 packages/vue-test-workspace/vue-component-meta/reference-type-props/component-js.vue diff --git a/packages/vue-component-meta/src/index.ts b/packages/vue-component-meta/src/index.ts index 08223b43b..49fd4be85 100644 --- a/packages/vue-component-meta/src/index.ts +++ b/packages/vue-component-meta/src/index.ts @@ -65,6 +65,11 @@ export function createComponentMetaChecker(tsconfigPath: string, checkerOptions: fileText = ts.sys.readFile(fileName); } if (fileText !== undefined) { + // force typescript to parse the file + if (fileName.endsWith('.vue') && fileText.includes(' { // }); }); + test('reference-type-props-js', () => { + const componentPath = path.resolve(__dirname, '../../vue-test-workspace/vue-component-meta/reference-type-props/component-js.vue'); + const meta = checker.getComponentMeta(componentPath); + + const foo = meta.props.find(prop => prop.name === 'foo'); + expect(foo).toBeDefined(); + expect(foo?.required).toBeTruthy(); + expect(foo?.schema).toEqual('string'); + }) + test('reference-type-events', () => { const componentPath = path.resolve(__dirname, '../../vue-test-workspace/vue-component-meta/reference-type-events/component.vue'); diff --git a/packages/vue-test-workspace/vue-component-meta/empty-component/component.vue b/packages/vue-test-workspace/vue-component-meta/empty-component/component.vue index 6efc99a1f..3a7f79e12 100644 --- a/packages/vue-test-workspace/vue-component-meta/empty-component/component.vue +++ b/packages/vue-test-workspace/vue-component-meta/empty-component/component.vue @@ -1,3 +1,7 @@ \ No newline at end of file + + + \ No newline at end of file diff --git a/packages/vue-test-workspace/vue-component-meta/reference-type-props/component-js.vue b/packages/vue-test-workspace/vue-component-meta/reference-type-props/component-js.vue new file mode 100644 index 000000000..e808e05f2 --- /dev/null +++ b/packages/vue-test-workspace/vue-component-meta/reference-type-props/component-js.vue @@ -0,0 +1,12 @@ + diff --git a/packages/vue-test-workspace/vue-component-meta/tsconfig.json b/packages/vue-test-workspace/vue-component-meta/tsconfig.json index e1b85270d..2a8d3bfd3 100644 --- a/packages/vue-test-workspace/vue-component-meta/tsconfig.json +++ b/packages/vue-test-workspace/vue-component-meta/tsconfig.json @@ -2,5 +2,6 @@ "extends": "../tsconfig.json", "include": [ "**/*", - ] + ], + "allowJs": true, } \ No newline at end of file From 65423652ea324326b925717a48a702b33135fc6c Mon Sep 17 00:00:00 2001 From: Sacha STAFYNIAK Date: Sat, 30 Jul 2022 20:21:51 +0200 Subject: [PATCH 09/13] fix: tests optional schema --- packages/vue-component-meta/src/index.ts | 8 +++-- .../vue-component-meta/tests/index.spec.ts | 31 ++++++++++++------- 2 files changed, 25 insertions(+), 14 deletions(-) diff --git a/packages/vue-component-meta/src/index.ts b/packages/vue-component-meta/src/index.ts index 49fd4be85..6d589f07d 100644 --- a/packages/vue-component-meta/src/index.ts +++ b/packages/vue-component-meta/src/index.ts @@ -264,12 +264,16 @@ function createSchemaResolvers(typeChecker: ts.TypeChecker, symbolNode: ts.Expre const enabled = options.enabled ?? false; function shouldIgnore(subtype: ts.Type) { + const type = typeChecker.typeToString(subtype); + if (type === 'any') { + return true + } + if (ignore.length === 0) { return false } - const type = typeChecker.typeToString(subtype); - return type === 'any' || ignore.includes(type) + return ignore.includes(type) } function reducer(acc: any, cur: any) { diff --git a/packages/vue-component-meta/tests/index.spec.ts b/packages/vue-component-meta/tests/index.spec.ts index 8a7191113..656ff56b9 100644 --- a/packages/vue-component-meta/tests/index.spec.ts +++ b/packages/vue-component-meta/tests/index.spec.ts @@ -5,15 +5,10 @@ import * as metaChecker from '..'; describe(`vue-component-meta`, () => { const tsconfigPath = path.resolve(__dirname, '../../vue-test-workspace/vue-component-meta/tsconfig.json'); - const checker = metaChecker.createComponentMetaChecker(tsconfigPath, { - schema: { - enabled: true, - ignore: ['MyIgnoredNestedProps', 'VNode', 'VNodeMountHook', 'RendererNode', 'RendererElement'] - } - }); test('global-props', () => { + const checker = metaChecker.createComponentMetaChecker(tsconfigPath); const globalProps = checker.getGlobalPropNames(); expect(globalProps).toEqual([ @@ -27,6 +22,7 @@ describe(`vue-component-meta`, () => { }); test('empty-component', () => { + const checker = metaChecker.createComponentMetaChecker(tsconfigPath); const componentPath = path.resolve(__dirname, '../../vue-test-workspace/vue-component-meta/empty-component/component.vue'); const meta = checker.getComponentMeta(componentPath); const globalPropNames = checker.getGlobalPropNames(); @@ -42,6 +38,12 @@ describe(`vue-component-meta`, () => { }); test('reference-type-props', () => { + const checker = metaChecker.createComponentMetaChecker(tsconfigPath, { + schema: { + enabled: true, + ignore: ['MyIgnoredNestedProps'] + } + }); const componentPath = path.resolve(__dirname, '../../vue-test-workspace/vue-component-meta/reference-type-props/component.vue'); const meta = checker.getComponentMeta(componentPath); @@ -326,16 +328,21 @@ describe(`vue-component-meta`, () => { }); test('reference-type-props-js', () => { + const checker = metaChecker.createComponentMetaChecker(tsconfigPath); const componentPath = path.resolve(__dirname, '../../vue-test-workspace/vue-component-meta/reference-type-props/component-js.vue'); const meta = checker.getComponentMeta(componentPath); const foo = meta.props.find(prop => prop.name === 'foo'); expect(foo).toBeDefined(); expect(foo?.required).toBeTruthy(); - expect(foo?.schema).toEqual('string'); }) test('reference-type-events', () => { + const checker = metaChecker.createComponentMetaChecker(tsconfigPath, { + schema: { + enabled: true, + } + }); const componentPath = path.resolve(__dirname, '../../vue-test-workspace/vue-component-meta/reference-type-events/component.vue'); const meta = checker.getComponentMeta(componentPath); @@ -406,7 +413,7 @@ describe(`vue-component-meta`, () => { }); test('template-slots', () => { - + const checker = metaChecker.createComponentMetaChecker(tsconfigPath); const componentPath = path.resolve(__dirname, '../../vue-test-workspace/vue-component-meta/template-slots/component.vue'); const meta = checker.getComponentMeta(componentPath); @@ -429,7 +436,7 @@ describe(`vue-component-meta`, () => { }); test('class-slots', () => { - + const checker = metaChecker.createComponentMetaChecker(tsconfigPath); const componentPath = path.resolve(__dirname, '../../vue-test-workspace/vue-component-meta/class-slots/component.vue'); const meta = checker.getComponentMeta(componentPath); @@ -447,7 +454,7 @@ describe(`vue-component-meta`, () => { }); test('exposed', () => { - + const checker = metaChecker.createComponentMetaChecker(tsconfigPath); const componentPath = path.resolve(__dirname, '../../vue-test-workspace/vue-component-meta/reference-type-exposed/component.vue'); const meta = checker.getComponentMeta(componentPath); @@ -461,7 +468,7 @@ describe(`vue-component-meta`, () => { }); test('ts-component', () => { - + const checker = metaChecker.createComponentMetaChecker(tsconfigPath); const componentPath = path.resolve(__dirname, '../../vue-test-workspace/vue-component-meta/ts-component/component.ts'); const meta = checker.getComponentMeta(componentPath); @@ -481,7 +488,7 @@ describe(`vue-component-meta`, () => { }); test('ts-named-exports', () => { - + const checker = metaChecker.createComponentMetaChecker(tsconfigPath); const componentPath = path.resolve(__dirname, '../../vue-test-workspace/vue-component-meta/ts-named-export/component.ts'); const exportNames = checker.getExportNames(componentPath); const Foo = checker.getComponentMeta(componentPath, 'Foo'); From 8003fd2e3dc2360b4d18250cbb37c77a23674faa Mon Sep 17 00:00:00 2001 From: johnsoncodehk Date: Sun, 31 Jul 2022 08:54:25 +0800 Subject: [PATCH 10/13] chore: format --- packages/vue-component-meta/src/index.ts | 54 +++++++++---------- packages/vue-component-meta/src/types.ts | 12 ++--- .../vue-component-meta/tests/index.spec.ts | 14 ++--- .../empty-component/component.vue | 4 +- 4 files changed, 42 insertions(+), 42 deletions(-) diff --git a/packages/vue-component-meta/src/index.ts b/packages/vue-component-meta/src/index.ts index 6d589f07d..a6385a242 100644 --- a/packages/vue-component-meta/src/index.ts +++ b/packages/vue-component-meta/src/index.ts @@ -21,7 +21,7 @@ export type { PropertyMeta, PropertyMetaSchema, SlotMeta -} +}; export function createComponentMetaChecker(tsconfigPath: string, checkerOptions: MetaCheckerOptions = {}) { const parsedCommandLine = vue.tsShared.createParsedCommandLine(ts, { @@ -133,7 +133,7 @@ export function createComponentMetaChecker(tsconfigPath: string, checkerOptions: const componentType = typeChecker.getTypeOfSymbolAtLocation(_export, symbolNode!); const symbolProperties = componentType.getProperties() ?? []; - const { + const { resolveNestedProperties, resolveEventSignature, resolveExposedProperties, @@ -211,7 +211,7 @@ export function createComponentMetaChecker(tsconfigPath: string, checkerOptions: // only exposed props will have a syntheticOrigin Boolean((prop as any).syntheticOrigin) ); - + if (exposed.length) { return exposed.map(resolveExposedProperties); } @@ -266,16 +266,16 @@ function createSchemaResolvers(typeChecker: ts.TypeChecker, symbolNode: ts.Expre function shouldIgnore(subtype: ts.Type) { const type = typeChecker.typeToString(subtype); if (type === 'any') { - return true + return true; } - + if (ignore.length === 0) { - return false + return false; } - return ignore.includes(type) + return ignore.includes(type); } - + function reducer(acc: any, cur: any) { acc[cur.name] = cur; return acc; @@ -283,7 +283,7 @@ function createSchemaResolvers(typeChecker: ts.TypeChecker, symbolNode: ts.Expre function resolveNestedProperties(prop: ts.Symbol): PropertyMeta { const subtype = typeChecker.getTypeOfSymbolAtLocation(prop, symbolNode!); - const schema = enabled ? resolveSchema(subtype) : undefined + const schema = enabled ? resolveSchema(subtype) : undefined; return { name: prop.getEscapedName().toString(), @@ -298,39 +298,39 @@ function createSchemaResolvers(typeChecker: ts.TypeChecker, symbolNode: ts.Expre }; } function resolveSlotProperties(prop: ts.Symbol): SlotMeta { - const subtype = typeChecker.getTypeOfSymbolAtLocation(typeChecker.getTypeOfSymbolAtLocation(prop, symbolNode!).getCallSignatures()[0].parameters[0], symbolNode!) - const schema = enabled ? resolveSchema(subtype) : undefined + const subtype = typeChecker.getTypeOfSymbolAtLocation(typeChecker.getTypeOfSymbolAtLocation(prop, symbolNode!).getCallSignatures()[0].parameters[0], symbolNode!); + const schema = enabled ? resolveSchema(subtype) : undefined; return { name: prop.getName(), type: typeChecker.typeToString(subtype), description: ts.displayPartsToString(prop.getDocumentationComment(typeChecker)), schema, - } + }; } function resolveExposedProperties(expose: ts.Symbol): ExposeMeta { - const subtype = typeChecker.getTypeOfSymbolAtLocation(expose, symbolNode!) - const schema = enabled ? resolveSchema(subtype) : undefined + const subtype = typeChecker.getTypeOfSymbolAtLocation(expose, symbolNode!); + const schema = enabled ? resolveSchema(subtype) : undefined; return { name: expose.getName(), type: typeChecker.typeToString(subtype), description: ts.displayPartsToString(expose.getDocumentationComment(typeChecker)), schema, - } + }; } function resolveEventSignature(call: ts.Signature): EventMeta { - const subtype = typeChecker.getTypeOfSymbolAtLocation(call.parameters[1], symbolNode!) + const subtype = typeChecker.getTypeOfSymbolAtLocation(call.parameters[1], symbolNode!); const schema = enabled - ? typeChecker.getTypeArguments(subtype as ts.TypeReference).map(resolveSchema) - : undefined - + ? typeChecker.getTypeArguments(subtype as ts.TypeReference).map(resolveSchema) + : undefined; + return { name: (typeChecker.getTypeOfSymbolAtLocation(call.parameters[0], symbolNode!) as ts.StringLiteralType).value, type: typeChecker.typeToString(subtype), signature: typeChecker.signatureToString(call), schema, - } + }; } function resolveCallbackSchema(signature: ts.Signature): PropertyMetaSchema { @@ -338,8 +338,8 @@ function createSchemaResolvers(typeChecker: ts.TypeChecker, symbolNode: ts.Expre ? typeChecker .getTypeArguments(typeChecker.getTypeOfSymbolAtLocation(signature.parameters[0], symbolNode) as ts.TypeReference) .map(resolveSchema) - : undefined - + : undefined; + return { kind: 'event', type: typeChecker.signatureToString(signature), @@ -357,29 +357,29 @@ function createSchemaResolvers(typeChecker: ts.TypeChecker, symbolNode: ts.Expre (subtype.isClassOrInterface() || subtype.isIntersection() || (subtype as ts.ObjectType).objectFlags & ts.ObjectFlags.Anonymous) ) { if (shouldIgnore(subtype)) { - return typeChecker.typeToString(subtype) + return typeChecker.typeToString(subtype); } return { kind: 'object', type: typeChecker.typeToString(subtype), schema: subtype.getProperties().map(resolveNestedProperties).reduce(reducer, {}) - } + }; } - return resolveEventSchema(subtype); + return resolveEventSchema(subtype); } function resolveArraySchema(subtype: ts.Type): PropertyMetaSchema { // @ts-ignore - typescript internal, isArrayLikeType exists if (typeChecker.isArrayLikeType(subtype)) { if (shouldIgnore(subtype)) { - return typeChecker.typeToString(subtype) + return typeChecker.typeToString(subtype); } return { kind: 'array', type: typeChecker.typeToString(subtype), schema: typeChecker.getTypeArguments(subtype as ts.TypeReference).map(resolveSchema) - } + }; } return resolveNestedSchema(subtype); diff --git a/packages/vue-component-meta/src/types.ts b/packages/vue-component-meta/src/types.ts index ee5a40eb5..2dfecaf8a 100644 --- a/packages/vue-component-meta/src/types.ts +++ b/packages/vue-component-meta/src/types.ts @@ -1,10 +1,10 @@ export interface ComponentMeta { - props: PropertyMeta[] - events: EventMeta[] - slots: SlotMeta[] - exposed: ExposeMeta[] + props: PropertyMeta[]; + events: EventMeta[]; + slots: SlotMeta[]; + exposed: ExposeMeta[]; } export interface PropertyMeta { name: string; @@ -39,11 +39,11 @@ export type PropertyMetaSchema = string | { kind: 'array', type: string, schema?: PropertyMetaSchema[]; } | { kind: 'event', type: string, schema?: PropertyMetaSchema[]; } | { kind: 'object', type: string, schema?: Record; }; - + export interface MetaCheckerSchemaOptions { enabled?: boolean; ignore?: string[]; } export interface MetaCheckerOptions { - schema?: MetaCheckerSchemaOptions + schema?: MetaCheckerSchemaOptions; } diff --git a/packages/vue-component-meta/tests/index.spec.ts b/packages/vue-component-meta/tests/index.spec.ts index 656ff56b9..6a2ef2dc4 100644 --- a/packages/vue-component-meta/tests/index.spec.ts +++ b/packages/vue-component-meta/tests/index.spec.ts @@ -27,14 +27,14 @@ describe(`vue-component-meta`, () => { const meta = checker.getComponentMeta(componentPath); const globalPropNames = checker.getGlobalPropNames(); - meta.props = meta.props.filter(prop => !globalPropNames.includes(prop.name)) + meta.props = meta.props.filter(prop => !globalPropNames.includes(prop.name)); expect(meta).toEqual({ props: [], events: [], slots: [], exposed: [], - }) + }); }); test('reference-type-props', () => { @@ -272,10 +272,10 @@ describe(`vue-component-meta`, () => { expect(inlined).toBeDefined(); expect(inlined?.required).toBeTruthy(); - expect(inlined?.schema).toEqual({ + expect(inlined?.schema).toEqual({ kind: 'object', - type: '{ foo: string; }', - schema: { + type: '{ foo: string; }', + schema: { foo: { name: 'foo', description: '', @@ -285,7 +285,7 @@ describe(`vue-component-meta`, () => { schema: 'string' } } - }) + }); expect(literalFromContext).toBeDefined(); expect(literalFromContext?.required).toBeTruthy(); @@ -335,7 +335,7 @@ describe(`vue-component-meta`, () => { const foo = meta.props.find(prop => prop.name === 'foo'); expect(foo).toBeDefined(); expect(foo?.required).toBeTruthy(); - }) + }); test('reference-type-events', () => { const checker = metaChecker.createComponentMetaChecker(tsconfigPath, { diff --git a/packages/vue-test-workspace/vue-component-meta/empty-component/component.vue b/packages/vue-test-workspace/vue-component-meta/empty-component/component.vue index 3a7f79e12..622b5c0a7 100644 --- a/packages/vue-test-workspace/vue-component-meta/empty-component/component.vue +++ b/packages/vue-test-workspace/vue-component-meta/empty-component/component.vue @@ -1,7 +1,7 @@ \ No newline at end of file + From 357ee060406ff059478879e0795dbcaae6ca605a Mon Sep 17 00:00:00 2001 From: johnsoncodehk Date: Sun, 31 Jul 2022 09:07:06 +0800 Subject: [PATCH 11/13] perf: reuse checker for faster test --- .../vue-component-meta/tests/index.spec.ts | 34 ++++++------------- 1 file changed, 10 insertions(+), 24 deletions(-) diff --git a/packages/vue-component-meta/tests/index.spec.ts b/packages/vue-component-meta/tests/index.spec.ts index 6a2ef2dc4..057fa2dc0 100644 --- a/packages/vue-component-meta/tests/index.spec.ts +++ b/packages/vue-component-meta/tests/index.spec.ts @@ -1,14 +1,20 @@ import * as path from 'path'; import { describe, expect, test } from 'vitest'; -import * as metaChecker from '..'; +import { createComponentMetaChecker } from '..'; describe(`vue-component-meta`, () => { const tsconfigPath = path.resolve(__dirname, '../../vue-test-workspace/vue-component-meta/tsconfig.json'); + const checker = createComponentMetaChecker(tsconfigPath); + const checker_schema = createComponentMetaChecker(tsconfigPath, { + schema: { + enabled: true, + ignore: ['MyIgnoredNestedProps'], + } + }); test('global-props', () => { - const checker = metaChecker.createComponentMetaChecker(tsconfigPath); const globalProps = checker.getGlobalPropNames(); expect(globalProps).toEqual([ @@ -22,7 +28,6 @@ describe(`vue-component-meta`, () => { }); test('empty-component', () => { - const checker = metaChecker.createComponentMetaChecker(tsconfigPath); const componentPath = path.resolve(__dirname, '../../vue-test-workspace/vue-component-meta/empty-component/component.vue'); const meta = checker.getComponentMeta(componentPath); const globalPropNames = checker.getGlobalPropNames(); @@ -38,15 +43,8 @@ describe(`vue-component-meta`, () => { }); test('reference-type-props', () => { - const checker = metaChecker.createComponentMetaChecker(tsconfigPath, { - schema: { - enabled: true, - ignore: ['MyIgnoredNestedProps'] - } - }); - const componentPath = path.resolve(__dirname, '../../vue-test-workspace/vue-component-meta/reference-type-props/component.vue'); - const meta = checker.getComponentMeta(componentPath); + const meta = checker_schema.getComponentMeta(componentPath); const foo = meta.props.find(prop => prop.name === 'foo'); const bar = meta.props.find(prop => prop.name === 'bar'); @@ -328,7 +326,6 @@ describe(`vue-component-meta`, () => { }); test('reference-type-props-js', () => { - const checker = metaChecker.createComponentMetaChecker(tsconfigPath); const componentPath = path.resolve(__dirname, '../../vue-test-workspace/vue-component-meta/reference-type-props/component-js.vue'); const meta = checker.getComponentMeta(componentPath); @@ -338,14 +335,8 @@ describe(`vue-component-meta`, () => { }); test('reference-type-events', () => { - const checker = metaChecker.createComponentMetaChecker(tsconfigPath, { - schema: { - enabled: true, - } - }); - const componentPath = path.resolve(__dirname, '../../vue-test-workspace/vue-component-meta/reference-type-events/component.vue'); - const meta = checker.getComponentMeta(componentPath); + const meta = checker_schema.getComponentMeta(componentPath); const onFoo = meta.events.find(event => event.name === 'foo'); const onBar = meta.events.find(event => event.name === 'bar'); @@ -413,7 +404,6 @@ describe(`vue-component-meta`, () => { }); test('template-slots', () => { - const checker = metaChecker.createComponentMetaChecker(tsconfigPath); const componentPath = path.resolve(__dirname, '../../vue-test-workspace/vue-component-meta/template-slots/component.vue'); const meta = checker.getComponentMeta(componentPath); @@ -436,7 +426,6 @@ describe(`vue-component-meta`, () => { }); test('class-slots', () => { - const checker = metaChecker.createComponentMetaChecker(tsconfigPath); const componentPath = path.resolve(__dirname, '../../vue-test-workspace/vue-component-meta/class-slots/component.vue'); const meta = checker.getComponentMeta(componentPath); @@ -454,7 +443,6 @@ describe(`vue-component-meta`, () => { }); test('exposed', () => { - const checker = metaChecker.createComponentMetaChecker(tsconfigPath); const componentPath = path.resolve(__dirname, '../../vue-test-workspace/vue-component-meta/reference-type-exposed/component.vue'); const meta = checker.getComponentMeta(componentPath); @@ -468,7 +456,6 @@ describe(`vue-component-meta`, () => { }); test('ts-component', () => { - const checker = metaChecker.createComponentMetaChecker(tsconfigPath); const componentPath = path.resolve(__dirname, '../../vue-test-workspace/vue-component-meta/ts-component/component.ts'); const meta = checker.getComponentMeta(componentPath); @@ -488,7 +475,6 @@ describe(`vue-component-meta`, () => { }); test('ts-named-exports', () => { - const checker = metaChecker.createComponentMetaChecker(tsconfigPath); const componentPath = path.resolve(__dirname, '../../vue-test-workspace/vue-component-meta/ts-named-export/component.ts'); const exportNames = checker.getExportNames(componentPath); const Foo = checker.getComponentMeta(componentPath, 'Foo'); From e6b274884f93620de954d79db02fd6b132af6720 Mon Sep 17 00:00:00 2001 From: johnsoncodehk Date: Sun, 31 Jul 2022 09:51:09 +0800 Subject: [PATCH 12/13] feat: make `lang="ts"` replace as a option --- packages/vue-component-meta/src/index.ts | 44 ++++++++++++++++--- packages/vue-component-meta/src/types.ts | 1 + .../vue-component-meta/tests/index.spec.ts | 4 +- 3 files changed, 42 insertions(+), 7 deletions(-) diff --git a/packages/vue-component-meta/src/index.ts b/packages/vue-component-meta/src/index.ts index a6385a242..d55c284f8 100644 --- a/packages/vue-component-meta/src/index.ts +++ b/packages/vue-component-meta/src/index.ts @@ -65,11 +65,6 @@ export function createComponentMetaChecker(tsconfigPath: string, checkerOptions: fileText = ts.sys.readFile(fileName); } if (fileText !== undefined) { - // force typescript to parse the file - if (fileName.endsWith('.vue') && fileText.includes(' parsedCommandLine.vueOptions, }; const core = vue.createLanguageContext(host); - const tsLs = ts.createLanguageService(core.typescriptLanguageServiceHost); + const fileNameMap: Record = {}; + const proxyApis: Partial = checkerOptions.forceUseTs ? { + getScriptFileNames: () => core.typescriptLanguageServiceHost.getScriptFileNames().map(vueCoreFileNameToTsLsFileName), + getScriptVersion: (fileName) => { + fileName = tsLsFileNameToVueCoreFileName(fileName); + return core.typescriptLanguageServiceHost.getScriptVersion(fileName); + }, + getScriptSnapshot: (fileName) => { + fileName = tsLsFileNameToVueCoreFileName(fileName); + return core.typescriptLanguageServiceHost.getScriptSnapshot(fileName); + }, + } : {}; + const proxyHost = new Proxy(core.typescriptLanguageServiceHost, { + get(target, propKey: keyof ts.LanguageServiceHost) { + if (propKey in proxyApis) { + return proxyApis[propKey]; + } + return target[propKey]; + } + }); + const tsLs = ts.createLanguageService(proxyHost); const program = tsLs.getProgram()!; const typeChecker = program.getTypeChecker(); @@ -89,6 +104,23 @@ export function createComponentMetaChecker(tsconfigPath: string, checkerOptions: getComponentMeta, }; + function vueCoreFileNameToTsLsFileName(fileName: string) { + let newFileName = fileName; + if (fileName.endsWith('.vue.js')) { + newFileName = fileName.substring(0, fileName.length - '.js'.length) + '.ts'; + fileNameMap[newFileName] = fileName; + } + else if (fileName.endsWith('.vue.jsx')) { + newFileName = fileName.substring(0, fileName.length - '.jsx'.length) + '.tsx'; + fileNameMap[newFileName] = fileName; + } + return newFileName; + } + + function tsLsFileNameToVueCoreFileName(fileName: string) { + return fileNameMap[fileName] ?? fileName; + } + /** * Get helper array to map internal properties added by vue to any components * diff --git a/packages/vue-component-meta/src/types.ts b/packages/vue-component-meta/src/types.ts index 2dfecaf8a..d37d0cea3 100644 --- a/packages/vue-component-meta/src/types.ts +++ b/packages/vue-component-meta/src/types.ts @@ -46,4 +46,5 @@ export interface MetaCheckerSchemaOptions { } export interface MetaCheckerOptions { schema?: MetaCheckerSchemaOptions; + forceUseTs?: boolean; } diff --git a/packages/vue-component-meta/tests/index.spec.ts b/packages/vue-component-meta/tests/index.spec.ts index 057fa2dc0..850847171 100644 --- a/packages/vue-component-meta/tests/index.spec.ts +++ b/packages/vue-component-meta/tests/index.spec.ts @@ -5,7 +5,9 @@ import { createComponentMetaChecker } from '..'; describe(`vue-component-meta`, () => { const tsconfigPath = path.resolve(__dirname, '../../vue-test-workspace/vue-component-meta/tsconfig.json'); - const checker = createComponentMetaChecker(tsconfigPath); + const checker = createComponentMetaChecker(tsconfigPath, { + forceUseTs: true, + }); const checker_schema = createComponentMetaChecker(tsconfigPath, { schema: { enabled: true, From 6d7255dfef814937f43033d3fd118c0604c0b472 Mon Sep 17 00:00:00 2001 From: johnsoncodehk Date: Sun, 31 Jul 2022 09:58:38 +0800 Subject: [PATCH 13/13] refactor: proxy getScriptKind instead of getScriptFileNames --- packages/vue-component-meta/src/index.ts | 34 ++++++----------------- packages/vue-test-workspace/tsconfig.json | 1 + 2 files changed, 9 insertions(+), 26 deletions(-) diff --git a/packages/vue-component-meta/src/index.ts b/packages/vue-component-meta/src/index.ts index d55c284f8..c133997f8 100644 --- a/packages/vue-component-meta/src/index.ts +++ b/packages/vue-component-meta/src/index.ts @@ -74,16 +74,15 @@ export function createComponentMetaChecker(tsconfigPath: string, checkerOptions: getVueCompilationSettings: () => parsedCommandLine.vueOptions, }; const core = vue.createLanguageContext(host); - const fileNameMap: Record = {}; const proxyApis: Partial = checkerOptions.forceUseTs ? { - getScriptFileNames: () => core.typescriptLanguageServiceHost.getScriptFileNames().map(vueCoreFileNameToTsLsFileName), - getScriptVersion: (fileName) => { - fileName = tsLsFileNameToVueCoreFileName(fileName); - return core.typescriptLanguageServiceHost.getScriptVersion(fileName); - }, - getScriptSnapshot: (fileName) => { - fileName = tsLsFileNameToVueCoreFileName(fileName); - return core.typescriptLanguageServiceHost.getScriptSnapshot(fileName); + getScriptKind: (fileName) => { + if (fileName.endsWith('.vue.js')) { + return ts.ScriptKind.TS; + } + if (fileName.endsWith('.vue.jsx')) { + return ts.ScriptKind.TSX; + } + return core.typescriptLanguageServiceHost.getScriptKind!(fileName); }, } : {}; const proxyHost = new Proxy(core.typescriptLanguageServiceHost, { @@ -104,23 +103,6 @@ export function createComponentMetaChecker(tsconfigPath: string, checkerOptions: getComponentMeta, }; - function vueCoreFileNameToTsLsFileName(fileName: string) { - let newFileName = fileName; - if (fileName.endsWith('.vue.js')) { - newFileName = fileName.substring(0, fileName.length - '.js'.length) + '.ts'; - fileNameMap[newFileName] = fileName; - } - else if (fileName.endsWith('.vue.jsx')) { - newFileName = fileName.substring(0, fileName.length - '.jsx'.length) + '.tsx'; - fileNameMap[newFileName] = fileName; - } - return newFileName; - } - - function tsLsFileNameToVueCoreFileName(fileName: string) { - return fileNameMap[fileName] ?? fileName; - } - /** * Get helper array to map internal properties added by vue to any components * diff --git a/packages/vue-test-workspace/tsconfig.json b/packages/vue-test-workspace/tsconfig.json index f401addb1..208c9aa74 100644 --- a/packages/vue-test-workspace/tsconfig.json +++ b/packages/vue-test-workspace/tsconfig.json @@ -9,6 +9,7 @@ "noUnusedLocals": true, "noUnusedParameters": true, "skipLibCheck": true, + "allowJs": true, "jsx": "preserve", "baseUrl": ".", "paths": {