Skip to content

Commit

Permalink
[vue-component-meta] fix: parse defineProps in script setup with opti…
Browse files Browse the repository at this point in the history
…on (#1665)

Co-authored-by: johnsoncodehk <johnsoncodehk@gmail.com>
  • Loading branch information
stafyniaksacha and johnsoncodehk committed Aug 2, 2022
1 parent a2e9f02 commit 7251b5c
Show file tree
Hide file tree
Showing 3 changed files with 179 additions and 55 deletions.
124 changes: 82 additions & 42 deletions packages/vue-component-meta/src/index.ts
Expand Up @@ -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<typeof Components[K]>; };;
export default {} as { [K in keyof typeof Components]: InstanceType<typeof Components[K]>; };
`;
}

Expand Down Expand Up @@ -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) => {
Expand All @@ -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()),
Expand All @@ -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
}
}
}

Expand All @@ -200,7 +211,7 @@ export function createComponentMetaChecker(tsconfigPath: string, checkerOptions:
const {
resolveEventSignature,
} = createSchemaResolvers(typeChecker, symbolNode!, checkerOptions.schema);

return resolveEventSignature(call);
}).filter(event => event.name);
}
Expand Down Expand Up @@ -437,12 +448,11 @@ function createSchemaResolvers(typeChecker: ts.TypeChecker, symbolNode: ts.Expre
};
}

function readVueComponentDefaultProps(vueFileText: string, printer: ts.Printer) {

const result: Record<string, string> = {};
function readVueComponentDefaultProps(vueFileText: string, printer: ts.Printer | undefined) {
let result: Record<string, { default?: string, required?: boolean; }> = {};

scriptSetupWorker();
sciptWorker();
scriptWorker();

return result;

Expand All @@ -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();
Expand All @@ -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<string, string> = {};
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() {

Expand Down Expand Up @@ -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<string, { default?: string, required?: boolean; }> = {};

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)) {
Expand Down
82 changes: 69 additions & 13 deletions packages/vue-component-meta/tests/index.spec.ts
Expand Up @@ -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 },
});

Expand Down Expand Up @@ -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');
Expand Down Expand Up @@ -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');
Expand All @@ -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');
Expand All @@ -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');
Expand All @@ -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');
Expand Down Expand Up @@ -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');
Expand Down Expand Up @@ -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');
Expand All @@ -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');
Expand Down Expand Up @@ -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');
Expand All @@ -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',
Expand All @@ -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');
Expand All @@ -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({
Expand All @@ -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');
Expand Down

0 comments on commit 7251b5c

Please sign in to comment.