Skip to content

Commit

Permalink
feat(component-meta): allow documenting components in .ts files (#1634)
Browse files Browse the repository at this point in the history
Co-authored-by: johnsoncodehk <[email protected]>
  • Loading branch information
elevatebart and johnsoncodehk authored Jul 27, 2022
1 parent 9d70df7 commit 23c2500
Show file tree
Hide file tree
Showing 5 changed files with 113 additions and 42 deletions.
100 changes: 63 additions & 37 deletions packages/vue-component-meta/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,13 +133,10 @@ export function createComponentMetaChecker(tsconfigPath: string) {
useCaseSensitiveFileNames: () => ts.sys.useCaseSensitiveFileNames,
getCompilationSettings: () => parsedCommandLine.options,
getScriptFileNames: () => {
const result = [...parsedCommandLine.fileNames];
for (const fileName of parsedCommandLine.fileNames) {
if (fileName.endsWith('.vue')) {
result.push(fileName + '.meta.ts');
}
}
return result;
return [
...parsedCommandLine.fileNames,
...parsedCommandLine.fileNames.map(getMetaFileName),
];
},
getProjectReferences: () => parsedCommandLine.projectReferences,
getScriptVersion: (fileName) => '0',
Expand All @@ -160,47 +157,36 @@ export function createComponentMetaChecker(tsconfigPath: string) {
const typeChecker = program.getTypeChecker();

return {
getExportNames,
getComponentMeta,
};

function getMetaFileName(fileName: string) {
return (fileName.endsWith('.vue') ? fileName : fileName.substring(0, fileName.lastIndexOf('.'))) + '.meta.ts';
}

function getMetaScriptContent(fileName: string) {
return `
import Component from '${fileName.substring(0, fileName.length - '.meta.ts'.length)}';
export default new Component();
import * as Components from '${fileName.substring(0, fileName.length - '.meta.ts'.length)}';
export default {} as { [K in keyof typeof Components]: InstanceType<typeof Components[K]>; };;
`;
}

function getComponentMeta(componentPath: string) {

const sourceFile = program?.getSourceFile(componentPath + '.meta.ts');
if (!sourceFile) {
throw 'Could not find main source file';
}

const moduleSymbol = typeChecker.getSymbolAtLocation(sourceFile);
if (!moduleSymbol) {
throw 'Could not find module symbol';
}

const exportedSymbols = typeChecker.getExportsOfModule(moduleSymbol);

let symbolNode: ts.Expression | undefined;
function getExportNames(componentPath: string) {
return _getExports(componentPath).exports.map(e => e.getName());
}

for (const symbol of exportedSymbols) {
function getComponentMeta(componentPath: string, exportName = 'default') {

const [declaration] = symbol.getDeclarations() ?? [];
const { symbolNode, exports } = _getExports(componentPath);
const _export = exports.find((property) => property.getName() === exportName);

if (ts.isExportAssignment(declaration)) {
symbolNode = declaration.expression;
}
if (!_export) {
throw `Could not find export ${exportName}`;
}

if (!symbolNode) {
throw 'Could not find symbol node';
}

const symbolType = typeChecker.getTypeAtLocation(symbolNode);
const symbolProperties = symbolType.getProperties();
const componentType = typeChecker.getTypeOfSymbolAtLocation(_export, symbolNode!);
const symbolProperties = componentType.getProperties() ?? [];

return {
props: getProps(),
Expand All @@ -223,6 +209,7 @@ export function createComponentMetaChecker(tsconfigPath: string) {

return [];
}

function getEvents() {

const $emit = symbolProperties.find(prop => prop.escapedName === '$emit');
Expand All @@ -242,6 +229,7 @@ export function createComponentMetaChecker(tsconfigPath: string) {

return [];
}

function getSlots() {

const propertyName = (parsedCommandLine.vueOptions.target ?? 3) < 3 ? '$scopedSlots' : '$slots';
Expand All @@ -251,7 +239,7 @@ export function createComponentMetaChecker(tsconfigPath: string) {
const type = typeChecker.getTypeOfSymbolAtLocation($slots, symbolNode!);
const properties = type.getProperties();
return properties.map(prop => ({
name: prop.escapedName as string,
name: prop.getName(),
propsType: typeChecker.typeToString(typeChecker.getTypeOfSymbolAtLocation(typeChecker.getTypeOfSymbolAtLocation(prop, symbolNode!).getCallSignatures()[0].parameters[0], symbolNode!)),
// props: {}, // TODO
description: ts.displayPartsToString(prop.getDocumentationComment(typeChecker)),
Expand All @@ -270,7 +258,7 @@ export function createComponentMetaChecker(tsconfigPath: string) {

if (exposed.length) {
return exposed.map(expose => ({
name: expose.escapedName as string,
name: expose.getName(),
type: typeChecker.typeToString(typeChecker.getTypeOfSymbolAtLocation(expose, symbolNode!)),
description: ts.displayPartsToString(expose.getDocumentationComment(typeChecker)),
}));
Expand All @@ -279,4 +267,42 @@ export function createComponentMetaChecker(tsconfigPath: string) {
return [];
}
}

function _getExports(componentPath: string) {

const sourceFile = program?.getSourceFile(getMetaFileName(componentPath));
if (!sourceFile) {
throw 'Could not find main source file';
}

const moduleSymbol = typeChecker.getSymbolAtLocation(sourceFile);
if (!moduleSymbol) {
throw 'Could not find module symbol';
}

const exportedSymbols = typeChecker.getExportsOfModule(moduleSymbol);

let symbolNode: ts.Expression | undefined;

for (const symbol of exportedSymbols) {

const [declaration] = symbol.getDeclarations() ?? [];

if (ts.isExportAssignment(declaration)) {
symbolNode = declaration.expression;
}
}

if (!symbolNode) {
throw 'Could not find symbol node';
}

const exportDefaultType = typeChecker.getTypeAtLocation(symbolNode);
const exports = exportDefaultType.getProperties();

return {
symbolNode,
exports,
};
}
}
34 changes: 29 additions & 5 deletions packages/vue-component-meta/tests/index.spec.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import * as path from 'path';
import { describe, expect, it, test } from 'vitest';
import { describe, expect, test } from 'vitest';
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);

it('reference-type-props', () => {
test('reference-type-props', () => {

const componentPath = path.resolve(__dirname, '../../vue-test-workspace/vue-component-meta/reference-type-props/component.vue');
const meta = checker.getComponentMeta(componentPath);
Expand Down Expand Up @@ -290,7 +290,7 @@ describe(`vue-component-meta`, () => {
});
});

it('reference-type-events', () => {
test('reference-type-events', () => {

const componentPath = path.resolve(__dirname, '../../vue-test-workspace/vue-component-meta/reference-type-events/component.vue');
const meta = checker.getComponentMeta(componentPath);
Expand Down Expand Up @@ -326,7 +326,7 @@ describe(`vue-component-meta`, () => {
expect(onBaz?.schema).toEqual([]);
});

it('template-slots', () => {
test('template-slots', () => {

const componentPath = path.resolve(__dirname, '../../vue-test-workspace/vue-component-meta/template-slots/component.vue');
const meta = checker.getComponentMeta(componentPath);
Expand All @@ -349,7 +349,7 @@ describe(`vue-component-meta`, () => {
expect(c).toBeDefined();
});

it('class-slots', () => {
test('class-slots', () => {

const componentPath = path.resolve(__dirname, '../../vue-test-workspace/vue-component-meta/class-slots/component.vue');
const meta = checker.getComponentMeta(componentPath);
Expand Down Expand Up @@ -380,4 +380,28 @@ describe(`vue-component-meta`, () => {

expect(counter).toBeDefined();
});

test('ts-named-exports', () => {

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');
const Bar = checker.getComponentMeta(componentPath, 'Bar');

expect(exportNames).toEqual(['Foo', 'Bar']);

const a = Foo.props.find(prop =>
prop.name === 'foo'
&& prop.required === true
&& prop.type === 'string'
);
const b = Bar.props.find(prop =>
prop.name === 'bar'
&& prop.required === false
&& prop.type === 'number | undefined'
);

expect(a).toBeDefined();
expect(b).toBeDefined();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export interface MyProps {
/**
* string foo
*/
foo: string,
/**
* optional number bar
*/
bar?: number,
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { h, defineComponent } from "vue";
import { MyProps } from "./PropDefinitions";

export default defineComponent((props: MyProps) => {
return h('pre', JSON.stringify(props, null, 2));
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { defineComponent } from "vue";

export const Foo = defineComponent((_: { foo: string; }) => { });

export const Bar = defineComponent((_: { bar?: number; }) => { });

0 comments on commit 23c2500

Please sign in to comment.