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`