diff --git a/packages/gem-analyzer/src/index.ts b/packages/gem-analyzer/src/index.ts index fa0dcd98..99dd4c86 100644 --- a/packages/gem-analyzer/src/index.ts +++ b/packages/gem-analyzer/src/index.ts @@ -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'; @@ -65,6 +65,7 @@ export interface ElementDetail { slots: string[]; parts: string[]; cssStates: string[]; + extend?: ElementDetail; } const shadowDecoratorName = ['shadow']; @@ -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, @@ -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) { @@ -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? @@ -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, }; @@ -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()) { diff --git a/packages/gem-book/src/plugins/api.ts b/packages/gem-book/src/plugins/api.ts index 8dcfb761..6f76bbcd 100644 --- a/packages/gem-book/src/plugins/api.ts +++ b/packages/gem-book/src/plugins/api.ts @@ -20,6 +20,8 @@ const { adoptedStyle, BoundaryCSSState, effect, + kebabToCamelCase, + camelToKebabCase, } = Gem; const styles = createCSSSheet(css` @@ -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) => { @@ -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, @@ -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) { @@ -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' : []), @@ -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' : []), @@ -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' : []), @@ -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( @@ -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' : []), @@ -193,7 +211,7 @@ 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'], @@ -201,7 +219,11 @@ class _GbpApiElement extends GemBookPluginElement { ); } - return Utils.parseMarkdown(text); + if (extend) { + text += this.#renderElement(extend, detail) + '\n\n'; + } + + return text; }; #renderExports = (exports: ExportDetail[]) => { @@ -249,6 +271,6 @@ class _GbpApiElement extends GemBookPluginElement { if (!elements) return html`
API Loading...
`; 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)))}`; }; } diff --git a/packages/gem-port/src/common.ts b/packages/gem-port/src/common.ts index abda5f20..75e8d405 100644 --- a/packages/gem-port/src/common.ts +++ b/packages/gem-port/src/common.ts @@ -28,13 +28,13 @@ export function getElementPathList(elementsDir: string) { const elementCache: Record = {}; 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) { diff --git a/packages/gem-port/src/index.ts b/packages/gem-port/src/index.ts index df346262..56d3732a 100644 --- a/packages/gem-port/src/index.ts +++ b/packages/gem-port/src/index.ts @@ -22,10 +22,12 @@ program .option('-o, --outdir ', `specify out dir`, (outdir: string) => (cliOptions.outDir = outdir)) .option('--svelte-ns ', `specify svelte element namespace`, (ns: string) => (cliOptions.svelteNs = ns)) .arguments('') - .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); }); diff --git a/packages/gem-port/src/react.ts b/packages/gem-port/src/react.ts index d03239b1..87dd197e 100644 --- a/packages/gem-port/src/react.ts +++ b/packages/gem-port/src/react.ts @@ -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); @@ -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 = {}; - 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); } diff --git a/packages/gem-port/src/svelte.ts b/packages/gem-port/src/svelte.ts index 22a8199a..24e4d31c 100644 --- a/packages/gem-port/src/svelte.ts +++ b/packages/gem-port/src/svelte.ts @@ -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 = {}; - getElementPathList(elementsDir).forEach((elementFilePath) => { - const elementDetailList = getFileElements(elementFilePath); + + const processFile = async (elementFilePath: string) => { + const elementDetailList = await getFileElements(elementFilePath); Object.assign( fileSystem, Object.fromEntries( @@ -53,6 +54,8 @@ export function compileSvelte(elementsDir: string, outDir: string, ns = ''): voi }), ), ); - }); + }; + + await Promise.all(getElementPathList(elementsDir).map(processFile)); compile(outDir, fileSystem); } diff --git a/packages/gem-port/src/vue.ts b/packages/gem-port/src/vue.ts index def26635..d611275b 100644 --- a/packages/gem-port/src/vue.ts +++ b/packages/gem-port/src/vue.ts @@ -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); @@ -115,5 +117,7 @@ export function generateVue(elementsDir: string, outDir: string): void { `, ); }); - }); + }; + + await Promise.all(getElementPathList(elementsDir).map(processFile)); }