Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[vue-component-meta] fix: recursive schema parsing #1660

Merged
merged 1 commit into from Aug 1, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
122 changes: 70 additions & 52 deletions packages/vue-component-meta/src/index.ts
Expand Up @@ -130,12 +130,6 @@ export function createComponentMetaChecker(tsconfigPath: string, checkerOptions:

const componentType = typeChecker.getTypeOfSymbolAtLocation(_export, symbolNode!);
const symbolProperties = componentType.getProperties() ?? [];
const {
resolveNestedProperties,
resolveEventSignature,
resolveExposedProperties,
resolveSlotProperties,
} = createSchemaResolvers(typeChecker, symbolNode!, checkerOptions.schema);

return {
props: getProps(),
Expand All @@ -155,7 +149,13 @@ export function createComponentMetaChecker(tsconfigPath: string, checkerOptions:
const properties = type.getApparentProperties();

result = properties
.map(resolveNestedProperties)
.map((prop) => {
const {
resolveNestedProperties,
} = createSchemaResolvers(typeChecker, symbolNode!, checkerOptions.schema);

return resolveNestedProperties(prop);
})
.filter((prop) => !prop.name.match(propEventRegex));
}

Expand Down Expand Up @@ -189,14 +189,20 @@ export function createComponentMetaChecker(tsconfigPath: string, checkerOptions:
}

function getEvents() {

const $emit = symbolProperties.find(prop => prop.escapedName === '$emit');

if ($emit) {
const type = typeChecker.getTypeOfSymbolAtLocation($emit, symbolNode!);
const calls = type.getCallSignatures();

return calls.map(resolveEventSignature).filter(event => event.name);
return calls.map((call) => {

const {
resolveEventSignature,
} = createSchemaResolvers(typeChecker, symbolNode!, checkerOptions.schema);

return resolveEventSignature(call);
}).filter(event => event.name);
}

return [];
Expand All @@ -211,20 +217,33 @@ export function createComponentMetaChecker(tsconfigPath: string, checkerOptions:
const type = typeChecker.getTypeOfSymbolAtLocation($slots, symbolNode!);
const properties = type.getProperties();

return properties.map(resolveSlotProperties);
return properties.map((prop) => {
const {
resolveSlotProperties,
} = createSchemaResolvers(typeChecker, symbolNode!, checkerOptions.schema);

return resolveSlotProperties(prop);
});
}

return [];
}

function getExposed() {

const exposed = symbolProperties.filter(prop =>
// only exposed props will have a syntheticOrigin
Boolean((prop as any).syntheticOrigin)
);

if (exposed.length) {
return exposed.map(resolveExposedProperties);
return exposed.map((prop) => {
const {
resolveExposedProperties,
} = createSchemaResolvers(typeChecker, symbolNode!, checkerOptions.schema);

return resolveExposedProperties(prop);
});
}

return [];
Expand Down Expand Up @@ -272,7 +291,7 @@ export function createComponentMetaChecker(tsconfigPath: string, checkerOptions:

function createSchemaResolvers(typeChecker: ts.TypeChecker, symbolNode: ts.Expression, options: MetaCheckerSchemaOptions = false) {
const enabled = !!options;
const ignore = typeof options === 'object' ? options.ignore ?? [] : [];
const ignore = typeof options === 'object' ? [...options?.ignore ?? []] : [];

function shouldIgnore(subtype: ts.Type) {
const type = typeChecker.typeToString(subtype);
Expand All @@ -287,6 +306,11 @@ function createSchemaResolvers(typeChecker: ts.TypeChecker, symbolNode: ts.Expre
return ignore.includes(type);
}

function setVisited(subtype: ts.Type) {
const type = typeChecker.typeToString(subtype);
ignore.push(type);
}

function reducer(acc: any, cur: any) {
acc[cur.name] = cur;
return acc;
Expand Down Expand Up @@ -358,63 +382,57 @@ function createSchemaResolvers(typeChecker: ts.TypeChecker, symbolNode: ts.Expre
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 (
function resolveSchema(subtype: ts.Type): PropertyMetaSchema {
const type = typeChecker.typeToString(subtype);
let schema: PropertyMetaSchema = type;

if (shouldIgnore(subtype)) {
return type;
}

setVisited(subtype);

if (subtype.isUnion()) {
schema = {
kind: 'enum',
type,
schema: subtype.types.map(resolveSchema)
};
}

// @ts-ignore - typescript internal, isArrayLikeType exists
else if (typeChecker.isArrayLikeType(subtype)) {
schema = {
kind: 'array',
type,
schema: typeChecker.getTypeArguments(subtype as ts.TypeReference).map(resolveSchema)
};
}

else 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 {
// setVisited(subtype);
schema = {
kind: 'object',
type: typeChecker.typeToString(subtype),
type,
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)
};
else if (subtype.getCallSignatures().length === 1) {
schema = resolveCallbackSchema(subtype.getCallSignatures()[0]);
}

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 schema;
}

return {
resolveNestedProperties,
resolveSlotProperties,
resolveEventSignature,
resolveExposedProperties,
resolveCallbackSchema,
resolveEventSchema,
resolveNestedSchema,
resolveArraySchema,
resolveSchema,
};
}
Expand Down
41 changes: 19 additions & 22 deletions packages/vue-component-meta/tests/index.spec.ts
Expand Up @@ -56,7 +56,7 @@ describe(`vue-component-meta`, () => {
const enumValue = meta.props.find(prop => prop.name === 'enumValue');
const literalFromContext = meta.props.find(prop => prop.name === 'literalFromContext');
const inlined = meta.props.find(prop => prop.name === 'inlined');
// const onEvent = meta.props.find(prop => prop.name === 'onEvent');
const recursive = meta.props.find(prop => prop.name === 'recursive');

expect(foo).toBeDefined();
expect(foo?.required).toBeTruthy();
Expand Down Expand Up @@ -311,27 +311,24 @@ describe(`vue-component-meta`, () => {
]
});

// expect(onEvent).toBeDefined();
// // expect(onEvent?.required).toBeFalsy()
// expect(onEvent?.type).toEqual('((...args: any[]) => any) | undefined');
// expect(onEvent?.schema).toEqual({
// kind: 'enum',
// type: '((...args: any[]) => any) | undefined',
// schema: [
// 'undefined',
// {
// kind: 'event',
// type: '(...args: any[]): any',
// schema: [
// {
// kind: 'array',
// type: 'any',
// schema: [],
// }
// ]
// }
// ]
// });
expect(recursive).toBeDefined();
expect(recursive?.required).toBeTruthy();
expect(recursive?.type).toEqual('MyNestedRecursiveProps');
expect(recursive?.schema).toEqual({
kind: 'object',
type: 'MyNestedRecursiveProps',
schema: {
recursive: {
name: 'recursive',
description: '',
tags: [],
global: false,
required: true,
type: 'MyNestedRecursiveProps',
schema: 'MyNestedRecursiveProps'
}
}
});
});

test('reference-type-props-js', () => {
Expand Down
Expand Up @@ -9,6 +9,10 @@ export interface MyIgnoredNestedProps {
nestedProp: string;
}

export interface MyNestedRecursiveProps {
recursive: MyNestedRecursiveProps
}

enum MyEnum {
Small,
Medium,
Expand Down Expand Up @@ -91,4 +95,5 @@ export interface MyProps {
*/
literalFromContext: MyCategories,
inlined: { foo: string; },
recursive: MyNestedRecursiveProps
}