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: parse defineProps in script setup with option #1665

Merged
merged 2 commits into from Aug 2, 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
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