Skip to content

Commit

Permalink
[gem-book] API plugin support inherit class
Browse files Browse the repository at this point in the history
  • Loading branch information
mantou132 committed Aug 25, 2024
1 parent f98e975 commit c768dcf
Show file tree
Hide file tree
Showing 7 changed files with 110 additions and 46 deletions.
40 changes: 35 additions & 5 deletions packages/gem-analyzer/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { SourceFile, ClassDeclaration } from 'ts-morph';
import type { SourceFile, ClassDeclaration, Project } from 'ts-morph';
import { camelToKebabCase } from '@mantou/gem/lib/utils';

import { getJsDoc } from './lib/utils';
Expand Down Expand Up @@ -65,6 +65,7 @@ export interface ElementDetail {
slots: string[];
parts: string[];
cssStates: string[];
extend?: ElementDetail;
}

const shadowDecoratorName = ['shadow'];
Expand All @@ -79,7 +80,35 @@ const eventDecoratorName = ['emitter', 'globalemitter'];
const globalEventDecoratorName = ['globalemitter'];
const lifecyclePopsOrMethods = ['state', 'willMount', 'render', 'mounted', 'shouldUpdate', 'updated', 'unmounted'];

export const parseElement = (declaration: ClassDeclaration) => {
async function getExtendsClassDetail(className: string, sourceFile: SourceFile, project?: Project) {
const currentFile = sourceFile.getFilePath();
const isAbsFilePath = currentFile.startsWith('/');
if (!isAbsFilePath || !project) return;
const fileSystem = project.getFileSystem();
const importDeclaration = sourceFile.getImportDeclaration(
(desc) =>
!!desc
.getNamedImports()
.map((e) => e.getText())
.find((e) => e.includes(className)),
);
if (!importDeclaration) return;
const depPath = importDeclaration.getModuleSpecifierValue();
if (!depPath.startsWith('.')) return;
const { pathname } = new URL(`${depPath}.ts`, `gem:${currentFile}`);
if (project.getSourceFile(pathname)) return;
project.createSourceFile(pathname, '');
try {
const text = await fileSystem.readFile(pathname);
const file = project.createSourceFile(pathname, text, { overwrite: true });
const classDeclaration = file.getClass(className);
return classDeclaration && parseElement(classDeclaration, file, project);
} catch {
return;
}
}

export const parseElement = async (declaration: ClassDeclaration, file: SourceFile, project?: Project) => {
const detail: ElementDetail = {
name: '',
shadow: false,
Expand All @@ -104,6 +133,7 @@ export const parseElement = (declaration: ClassDeclaration) => {
declaration.getJsDocs().forEach((jsDoc) => appendElementDesc(jsDoc.getCommentText()));

if (className && constructorExtendsName) {
detail.extend = await getExtendsClassDetail(constructorExtendsName, file, project);
detail.constructorName = className;
detail.constructorExtendsName = constructorExtendsName;
if (constructor) {
Expand Down Expand Up @@ -240,7 +270,7 @@ export const parseElement = (declaration: ClassDeclaration) => {
return detail;
};

export const getElements = (file: SourceFile) => {
export const getElements = async (file: SourceFile, project?: Project) => {
const result: ElementDetail[] = [];
for (const declaration of file.getClasses()) {
// need support other decorators?
Expand All @@ -263,7 +293,7 @@ export const getElements = (file: SourceFile) => {
?.getCommentText();
if (elementTag) {
const detail: ElementDetail = {
...parseElement(declaration),
...(await parseElement(declaration, file, project)),
name: elementTag,
shadow: !!shadowDeclaration,
};
Expand All @@ -283,7 +313,7 @@ export interface ExportDetail {
description?: string;
}

export const getExports = (file: SourceFile) => {
export const getExports = async (file: SourceFile) => {
const result: ExportDetail[] = [];

for (const [name, declarations] of file.getExportedDeclarations()) {
Expand Down
54 changes: 38 additions & 16 deletions packages/gem-book/src/plugins/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ const {
adoptedStyle,
BoundaryCSSState,
effect,
kebabToCamelCase,
camelToKebabCase,
} = Gem;

const styles = createCSSSheet(css`
Expand Down Expand Up @@ -66,15 +68,26 @@ class _GbpApiElement extends GemBookPluginElement {
useInMemoryFileSystem: true,
compilerOptions: { target: ts.ScriptTarget.ESNext },
});
const fileSystem = project.getFileSystem();
fileSystem.readFile = async (filePath: string) => {
const url = Utils.getRemoteURL(filePath);
return (await fetch(url)).text();
};
const file = project.createSourceFile(this.src, text);
return { elements: getElements(file), exports: getExports(file) };
return { elements: await getElements(file, project), exports: await getExports(file) };
};

#renderHeader = (headingLevel: number, text: string, name: string) => {
return `${'#'.repeat(headingLevel + this.#headingLevel - 1)} ${text} {#${`${name}-${text}`.replaceAll(
' ',
'-',
)}}\n\n`;
#renderHeader = (text: string, extText: string, root?: ElementDetail) => {
const baseLevel = root?.extend ? 2 : 1;
const headingLevel = extText === text ? baseLevel - 1 : baseLevel;
const level = '#'.repeat(headingLevel + this.#headingLevel - 1);
const title = root?.extend ? `\`${extText}\` ${text}` : text;
const hash = `${[extText === text ? '' : extText, text]
.map((e) => kebabToCamelCase(e).replaceAll(' ', ''))
.map((e) => camelToKebabCase(e).replace('-', ''))
.filter((e) => e)
.join('-')}`;
return `${level} ${title} {#${hash}}\n\n`;
};

#renderCode = (s = '', deprecated?: boolean) => {
Expand All @@ -93,10 +106,9 @@ class _GbpApiElement extends GemBookPluginElement {
return text;
};

#renderElement = (detail: ElementDetail) => {
#renderElement = (detail: ElementDetail, root = detail): string => {
const {
shadow,
name: eleName,
description: eleDescription = '',
constructorName,
constructorExtendsName,
Expand All @@ -109,9 +121,15 @@ class _GbpApiElement extends GemBookPluginElement {
cssStates,
parts,
slots,
extend,
} = detail;

let text = '';

if (root.extend) {
text += this.#renderHeader(constructorName, constructorName);
}

text += eleDescription + '\n\n';
if (constructorExtendsName) {
if (shadow) {
Expand All @@ -121,7 +139,7 @@ class _GbpApiElement extends GemBookPluginElement {
}

if (constructorName && constructorParams.length) {
text += this.#renderHeader(1, `Constructor \`${constructorName}()\``, eleName);
text += this.#renderHeader(`Constructor \`${constructorName}()\``, constructorName, root);
text += this.#renderTable(
constructorParams,
['Params', 'Type'].concat(constructorParams.some((e) => e.description) ? 'Description' : []),
Expand All @@ -133,7 +151,7 @@ class _GbpApiElement extends GemBookPluginElement {
);
}
if (staticProperties.length) {
text += this.#renderHeader(1, 'Static Properties', eleName);
text += this.#renderHeader('Static Properties', constructorName, root);
text += this.#renderTable(
staticProperties,
['Property', 'Type'].concat(constructorParams.some((e) => e.description) ? 'Description' : []),
Expand All @@ -145,7 +163,7 @@ class _GbpApiElement extends GemBookPluginElement {
);
}
if (staticMethods.length) {
text += this.#renderHeader(1, 'Static Methods', eleName);
text += this.#renderHeader('Static Methods', constructorName, root);
text += this.#renderTable(
staticMethods,
['Method', 'Type'].concat(constructorParams.some((e) => e.description) ? 'Description' : []),
Expand All @@ -157,7 +175,7 @@ class _GbpApiElement extends GemBookPluginElement {
);
}
if (properties.length) {
text += this.#renderHeader(1, 'Instance Properties', eleName);
text += this.#renderHeader('Instance Properties', constructorName, root);
text += this.#renderTable(
properties.filter(({ slot, cssState, part, isRef }) => !slot && !cssState && !part && !isRef),
['Property(Attribute)', 'Reactive', 'Type'].concat(
Expand All @@ -174,7 +192,7 @@ class _GbpApiElement extends GemBookPluginElement {
}

if (methods.length) {
text += this.#renderHeader(1, 'Instance Methods', eleName);
text += this.#renderHeader('Instance Methods', constructorName, root);
text += this.#renderTable(
methods.filter(({ event }) => !event),
['Method', 'Type'].concat(constructorParams.some((e) => e.description) ? 'Description' : []),
Expand All @@ -193,15 +211,19 @@ class _GbpApiElement extends GemBookPluginElement {
{ type: 'CSS State', value: cssStates },
];
if (otherRows.some(({ value }) => !!value.length)) {
text += this.#renderHeader(1, 'Other', eleName);
text += this.#renderHeader('Other', constructorName, root);
text += this.#renderTable(
otherRows.filter(({ value }) => !!value.length),
['Type', 'Value'],
[({ type }) => type, ({ value }) => value.map((e) => this.#renderCode(e)).join(', ')],
);
}

return Utils.parseMarkdown(text);
if (extend) {
text += this.#renderElement(extend, detail) + '\n\n';
}

return text;
};

#renderExports = (exports: ExportDetail[]) => {
Expand Down Expand Up @@ -249,6 +271,6 @@ class _GbpApiElement extends GemBookPluginElement {
if (!elements) return html`<div class="loading">API Loading...</div>`;
const renderElements = this.name ? elements.filter(({ name }) => this.name === name) : elements;
if (!renderElements.length && exports) return html`${this.#renderExports(exports)}`;
return html`${renderElements.map(this.#renderElement)}`;
return html`${renderElements.map((detail) => Utils.parseMarkdown(this.#renderElement(detail)))}`;
};
}
14 changes: 7 additions & 7 deletions packages/gem-port/src/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,13 @@ export function getElementPathList(elementsDir: string) {

const elementCache: Record<string, ElementDetail[] | undefined> = {};
const project = new Project({ useInMemoryFileSystem: true });
export function getFileElements(elementFilePath: string) {
return (
elementCache[elementFilePath] ||
(elementCache[elementFilePath] = getElements(
project.createSourceFile(elementFilePath, readFileSync(elementFilePath, { encoding: 'utf-8' })),
))
);
export async function getFileElements(elementFilePath: string) {
if (!elementCache[elementFilePath]) {
const text = readFileSync(elementFilePath, { encoding: 'utf-8' });
const file = project.createSourceFile(elementFilePath, text);
elementCache[elementFilePath] = await getElements(file);
}
return elementCache[elementFilePath];
}

export function getComponentName(tag: string) {
Expand Down
10 changes: 6 additions & 4 deletions packages/gem-port/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,12 @@ program
.option('-o, --outdir <path>', `specify out dir`, (outdir: string) => (cliOptions.outDir = outdir))
.option('--svelte-ns <ns>', `specify svelte element namespace`, (ns: string) => (cliOptions.svelteNs = ns))
.arguments('<dir>')
.action((dir: string) => {
compileReact(dir, path.resolve(cliOptions.outDir, 'react'));
generateVue(dir, path.resolve(cliOptions.outDir, 'vue'));
compileSvelte(dir, path.resolve(cliOptions.outDir, 'svelte'), cliOptions.svelteNs);
.action(async (dir: string) => {
await Promise.all([
compileReact(dir, path.resolve(cliOptions.outDir, 'react')),
generateVue(dir, path.resolve(cliOptions.outDir, 'vue')),
compileSvelte(dir, path.resolve(cliOptions.outDir, 'svelte'), cliOptions.svelteNs),
]);
process.exit(0);
});

Expand Down
15 changes: 9 additions & 6 deletions packages/gem-port/src/react.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ import {
getJsDocDescName,
} from './common';

function createReactSourceFile(elementFilePath: string, outDir: string) {
const elementDetailList = getFileElements(elementFilePath);
async function createReactSourceFile(elementFilePath: string, outDir: string) {
const elementDetailList = await getFileElements(elementFilePath);
return Object.fromEntries(
elementDetailList.map(({ name: tag, constructorName, properties, methods }) => {
const componentName = getComponentName(tag);
Expand Down Expand Up @@ -80,10 +80,13 @@ function createReactSourceFile(elementFilePath: string, outDir: string) {
);
}

export function compileReact(elementsDir: string, outDir: string): void {
export async function compileReact(elementsDir: string, outDir: string) {
const fileSystem: Record<string, string> = {};
getElementPathList(elementsDir).forEach((elementFilePath) => {
Object.assign(fileSystem, createReactSourceFile(elementFilePath, outDir));
});

const processFile = async (elementFilePath: string) => {
Object.assign(fileSystem, await createReactSourceFile(elementFilePath, outDir));
};

await Promise.all(getElementPathList(elementsDir).map(processFile));
compile(outDir, fileSystem);
}
11 changes: 7 additions & 4 deletions packages/gem-port/src/svelte.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,11 @@ import {
getRelativePath,
} from './common';

export function compileSvelte(elementsDir: string, outDir: string, ns = ''): void {
export async function compileSvelte(elementsDir: string, outDir: string, ns = '') {
const fileSystem: Record<string, string> = {};
getElementPathList(elementsDir).forEach((elementFilePath) => {
const elementDetailList = getFileElements(elementFilePath);

const processFile = async (elementFilePath: string) => {
const elementDetailList = await getFileElements(elementFilePath);
Object.assign(
fileSystem,
Object.fromEntries(
Expand Down Expand Up @@ -53,6 +54,8 @@ export function compileSvelte(elementsDir: string, outDir: string, ns = ''): voi
}),
),
);
});
};

await Promise.all(getElementPathList(elementsDir).map(processFile));
compile(outDir, fileSystem);
}
12 changes: 8 additions & 4 deletions packages/gem-port/src/vue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,12 @@ declare module '@vue/runtime-dom' {
}
*/

export function generateVue(elementsDir: string, outDir: string): void {
export async function generateVue(elementsDir: string, outDir: string) {
mkdirSync(outDir, { recursive: true });
getElementPathList(elementsDir).forEach((elementFilePath) => {
getFileElements(elementFilePath).forEach(({ name: tag, properties, constructorName, methods, events }) => {

const processFile = async (elementFilePath: string) => {
const elements = await getFileElements(elementFilePath);
elements.forEach(({ name: tag, properties, constructorName, methods, events }) => {
const componentName = getComponentName(tag);
const componentMethodsName = `${componentName}Methods`;
const relativePath = getRelativePath(elementFilePath, outDir);
Expand Down Expand Up @@ -115,5 +117,7 @@ export function generateVue(elementsDir: string, outDir: string): void {
`,
);
});
});
};

await Promise.all(getElementPathList(elementsDir).map(processFile));
}

0 comments on commit c768dcf

Please sign in to comment.