Skip to content

Commit

Permalink
[gem] Fixed #101
Browse files Browse the repository at this point in the history
  • Loading branch information
mantou132 committed Aug 30, 2024
1 parent 9f6253f commit 3184560
Show file tree
Hide file tree
Showing 9 changed files with 151 additions and 65 deletions.
42 changes: 35 additions & 7 deletions packages/gem-analyzer/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import type { SourceFile, ClassDeclaration, Project } from 'ts-morph';
import { camelToKebabCase } from '@mantou/gem/lib/utils';

import { getJsDoc } from './lib/utils';
import { getJsDoc, getTypeText, isGetter, isSetter } from './lib/utils';

interface StaticProperty {
getter?: boolean;
setter?: boolean;
name: string;
deprecated?: boolean;
slot?: string;
Expand All @@ -20,6 +22,8 @@ interface StaticMethod {
}

interface Property {
getter?: boolean;
setter?: boolean;
name: string;
reactive: boolean;
deprecated?: boolean;
Expand Down Expand Up @@ -50,6 +54,7 @@ interface ConstructorParam {
}

export interface ElementDetail {
relativePath?: string;
shadow: boolean;
name: string;
constructorName: string;
Expand All @@ -68,6 +73,16 @@ export interface ElementDetail {
extend?: ElementDetail;
}

export const getChain = (detail: ElementDetail) => {
let root = detail;
const result: ElementDetail[] = [root];
while (root.extend) {
root = root.extend;
result.push(root);
}
return result;
};

const shadowDecoratorName = ['shadow'];
const elementDecoratorName = ['customElement'];
const attrDecoratorName = ['attribute', 'boolattribute', 'numattribute'];
Expand All @@ -81,6 +96,7 @@ const globalEventDecoratorName = ['globalemitter'];
const lifecyclePopsOrMethods = ['state', 'willMount', 'render', 'mounted', 'shouldUpdate', 'updated', 'unmounted'];

async function getExtendsClassDetail(className: string, sourceFile: SourceFile, project?: Project) {
if (className === 'GemElement') return;
const currentFile = sourceFile.getFilePath();
const isAbsFilePath = currentFile.startsWith('/');
if (!isAbsFilePath || !project) return;
Expand All @@ -102,7 +118,10 @@ async function getExtendsClassDetail(className: string, sourceFile: SourceFile,
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);
if (!classDeclaration) return;
const detail = await parseElement(classDeclaration, file, project);
detail.relativePath = depPath;
return detail;
} catch {
return;
}
Expand Down Expand Up @@ -151,7 +170,7 @@ export const parseElement = async (declaration: ClassDeclaration, file: SourceFi
});
detail.constructorParams = constructor.getParameters().map((param) => ({
name: param.getName(),
type: param.getType().getText(),
type: getTypeText(param),
description: params[param.getName()],
}));
}
Expand All @@ -164,6 +183,8 @@ export const parseElement = async (declaration: ClassDeclaration, file: SourceFi
const prop: StaticProperty = {
name: staticPropName,
type: staticPropDeclaration.getType().getText(),
getter: isGetter(staticPropDeclaration),
setter: isSetter(staticPropDeclaration),
...getJsDoc(staticPropDeclaration),
};

Expand Down Expand Up @@ -208,7 +229,9 @@ export const parseElement = async (declaration: ClassDeclaration, file: SourceFi
const prop: Property = {
name: propName,
reactive: false,
type: propDeclaration.getType().getText(),
type: getTypeText(propDeclaration),
getter: isGetter(propDeclaration),
setter: isSetter(propDeclaration),
...getJsDoc(propDeclaration),
};
detail.properties.push(prop);
Expand Down Expand Up @@ -250,7 +273,7 @@ export const parseElement = async (declaration: ClassDeclaration, file: SourceFi
if (lifecyclePopsOrMethods.includes(methodName)) continue;
const method: Method = {
name: methodName,
type: methodDeclaration.getType().getText(),
type: getTypeText(methodDeclaration),
...getJsDoc(methodDeclaration),
};
detail.methods.push(method);
Expand All @@ -267,6 +290,13 @@ export const parseElement = async (declaration: ClassDeclaration, file: SourceFi
}
}
}

const elementDecorators = declaration.getDecorators();
const shadowDeclaration = elementDecorators.find((decorator) => shadowDecoratorName.includes(decorator.getName()));
if (shadowDeclaration) {
detail.shadow = true;
}

return detail;
};

Expand All @@ -278,7 +308,6 @@ export const getElements = async (file: SourceFile, project?: Project) => {
const elementDeclaration = elementDecorators.find((decorator) =>
elementDecoratorName.includes(decorator.getName()),
);
const shadowDeclaration = elementDecorators.find((decorator) => shadowDecoratorName.includes(decorator.getName()));
const elementTag =
elementDeclaration
?.getCallExpression()!
Expand All @@ -295,7 +324,6 @@ export const getElements = async (file: SourceFile, project?: Project) => {
const detail: ElementDetail = {
...(await parseElement(declaration, file, project)),
name: elementTag,
shadow: !!shadowDeclaration,
};
if (!detail.constructorName.startsWith('_')) {
result.push(detail);
Expand Down
27 changes: 26 additions & 1 deletion packages/gem-analyzer/src/lib/utils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import { ClassInstancePropertyTypes, MethodDeclaration, ExportedDeclarations } from 'ts-morph';
import {
ClassInstancePropertyTypes,
MethodDeclaration,
ExportedDeclarations,
ClassStaticPropertyTypes,
} from 'ts-morph';

export function getJsDoc(declaration: ClassInstancePropertyTypes | MethodDeclaration | ExportedDeclarations) {
if ('getJsDocs' in declaration) {
Expand All @@ -14,3 +19,23 @@ export function getJsDoc(declaration: ClassInstancePropertyTypes | MethodDeclara
};
}
}

export function getTypeText(declaration: ClassInstancePropertyTypes | MethodDeclaration) {
const structure = declaration.getStructure();
return (
('type' in structure && typeof structure.type === 'string' && structure.type) || declaration.getType().getText()
);
}

export function isGetter(
declaration: ClassInstancePropertyTypes | ClassStaticPropertyTypes,
kind: 'get' | 'set' = 'get',
) {
const first = declaration.getFirstChild();
const firstText = first?.getText();
return (firstText === 'static' ? first?.getNextSibling()?.getText() : firstText) === kind;
}

export function isSetter(declaration: ClassInstancePropertyTypes | ClassStaticPropertyTypes) {
return isGetter(declaration, 'set');
}
16 changes: 11 additions & 5 deletions packages/gem-book/src/plugins/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,9 +90,13 @@ class _GbpApiElement extends GemBookPluginElement {
return `${level} ${title} {#${hash}}\n\n`;
};

#renderCode = (s = '', deprecated?: boolean) => {
#renderCode = (s = '', deprecated?: boolean, prefix?: string) => {
if (!s) return '';
const code = `\`${s.replace(/\|/g, '\\|').replace(/\n/g, ' ')}\``;
const code = `\`${[prefix, s]
.filter((e) => e)
.join(' ')
.replace(/\|/g, '\\|')
.replace(/\n/g, ' ')}\``;
return deprecated ? `~~${code}~~` : code;
};

Expand Down Expand Up @@ -156,7 +160,8 @@ class _GbpApiElement extends GemBookPluginElement {
staticProperties,
['Property', 'Type'].concat(constructorParams.some((e) => e.description) ? 'Description' : []),
[
({ name, deprecated }) => this.#renderCode(name, deprecated),
({ name, deprecated, getter, setter }) =>
this.#renderCode(name, deprecated, getter ? 'get' : setter ? 'set' : ''),
({ type }) => this.#renderCode(type),
({ description = '' }) => description.replaceAll('\n', '<br>'),
],
Expand All @@ -182,8 +187,9 @@ class _GbpApiElement extends GemBookPluginElement {
innerWidth > 600 && constructorParams.some((e) => e.description) ? 'Description' : [],
),
[
({ name, attribute: attr, deprecated }) =>
this.#renderCode(name, deprecated) + (attr ? `(${this.#renderCode(attr, deprecated)})` : ''),
({ name, attribute: attr, deprecated, setter, getter }) =>
this.#renderCode(name, deprecated, getter ? 'get' : setter ? 'set' : '') +
(attr ? `(${this.#renderCode(attr, deprecated)})` : ''),
({ reactive }) => (reactive ? 'Yes' : ''),
({ type }) => this.#renderCode(type),
({ description = '' }) => description.replaceAll('\n', '<br>'),
Expand Down
19 changes: 12 additions & 7 deletions packages/gem-port/src/common.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import path from 'path';
import { readdirSync, readFileSync, statSync } from 'fs';
import { readdirSync, readFileSync, statSync, promises } from 'fs';

import { ElementDetail, getElements } from 'gem-analyzer';
import { Project } from 'ts-morph';
import { ElementDetail, getChain, getElements } from 'gem-analyzer';
import { Project, ts as morphTs } from 'ts-morph';
import * as ts from 'typescript';

const dev = process.env.MODE === 'dev';
Expand All @@ -26,13 +26,18 @@ export function getElementPathList(elementsDir: string) {
return paths;
}

const elementCache: Record<string, ElementDetail[] | undefined> = {};
const project = new Project({ useInMemoryFileSystem: true });
const elementCache: Record<string, [ElementDetail, ElementDetail[]][] | undefined> = {};
const project = new Project({
useInMemoryFileSystem: true,
compilerOptions: { target: morphTs.ScriptTarget.ESNext },
});
project.getFileSystem().readFile = (filePath) => promises.readFile(filePath, 'utf8');
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);
const file = project.getSourceFile(elementFilePath) || project.createSourceFile(elementFilePath, text);
const details = await getElements(file, project);
elementCache[elementFilePath] = details.map((detail) => [detail, getChain(detail)] as const);
}
return elementCache[elementFilePath];
}
Expand Down
13 changes: 7 additions & 6 deletions packages/gem-port/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ const cliOptions = {
svelteNs: '',
};

const timer = setTimeout(() => program.outputHelp());

program
.name(name)
.description(description)
Expand All @@ -23,12 +25,11 @@ program
.option('--svelte-ns <ns>', `specify svelte element namespace`, (ns: string) => (cliOptions.svelteNs = ns))
.arguments('<dir>')
.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),
]);
clearTimeout(timer);
await compileReact(dir, path.resolve(cliOptions.outDir, 'react'));
await generateVue(dir, path.resolve(cliOptions.outDir, 'vue'));
await compileSvelte(dir, path.resolve(cliOptions.outDir, 'svelte'), cliOptions.svelteNs);
process.exit(0);
});

program.parse(process.argv).outputHelp();
program.parse(process.argv);
42 changes: 26 additions & 16 deletions packages/gem-port/src/react.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,12 @@ import {
async function createReactSourceFile(elementFilePath: string, outDir: string) {
const elementDetailList = await getFileElements(elementFilePath);
return Object.fromEntries(
elementDetailList.map(({ name: tag, constructorName, properties, methods }) => {
elementDetailList.map(([{ name: tag, constructorName, properties, methods }, chain]) => {

Check warning on line 13 in packages/gem-port/src/react.ts

View workflow job for this annotation

GitHub Actions / lint

'chain' is defined but never used. Allowed unused args must match /^_/u
const componentName = getComponentName(tag);
const componentPropsName = `${componentName}Props`;
const componentMethodsName = `${componentName}Methods`;
const componentExposeName = `${componentName}Expose`;
const relativePath = getRelativePath(elementFilePath, outDir);
const getters = properties.filter((e) => e.getter);
return [
componentName + '.tsx',
`
Expand All @@ -25,18 +26,26 @@ async function createReactSourceFile(elementFilePath: string, outDir: string) {
export type ${componentPropsName} = HTMLAttributes<HTMLDivElement> & RefAttributes<${constructorName}> & {
${properties
.map(({ name, reactive, event, deprecated }) =>
.map(({ name, getter, event, deprecated }) =>
event
? [
getJsDocDescName(`'on${event}'`, deprecated),
`(event: CustomEvent<Parameters<${constructorName}['${name}']>[0]>) => void`,
].join('?:')
: reactive
: !getter
? [getJsDocDescName(name, deprecated), `${constructorName}['${name}']`].join('?:')
: '',
)
.join(';\n')}
};
export type ${componentExposeName} = {
${[...methods, ...getters]
.map(({ name, deprecated }) =>
[getJsDocDescName(name, deprecated), `${constructorName}['${name}']`].join(': '),
)
.join(';\n')}
}
declare global {
namespace JSX {
Expand All @@ -45,28 +54,29 @@ async function createReactSourceFile(elementFilePath: string, outDir: string) {
}
}
}
export type ${componentMethodsName} = {
${methods
.map(({ name, deprecated }) =>
[getJsDocDescName(name, deprecated), `${constructorName}['${name}']`].join(': '),
)
.join(';\n')}
}
export const ${componentName}: ForwardRefExoticComponent<Omit<${componentPropsName}, "ref"> & RefAttributes<${componentMethodsName}>> = forwardRef<${componentMethodsName}, ${componentPropsName}>(function (props, ref): JSX.Element {
export const ${componentName}: ForwardRefExoticComponent<Omit<${componentPropsName}, "ref"> & RefAttributes<${componentExposeName}>> = forwardRef<${componentExposeName}, ${componentPropsName}>(function (props, ref): JSX.Element {
const elementRef = useRef<${constructorName}>(null);
useImperativeHandle(ref, () => {
return {
${methods
.map(
({ name }) => `
${name}(...args) {
elementRef.current?.${name}(...args)
}
return elementRef.current!.${name}(...args)
},
`,
)
.join(',')}
.join('')}
${getters
.map(
({ name }) => `
get ${name}() {
return elementRef.current!.${name}
},
`,
)
.join('')}
};
}, []);
Expand Down
6 changes: 3 additions & 3 deletions packages/gem-port/src/svelte.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export async function compileSvelte(elementsDir: string, outDir: string, ns = ''
Object.assign(
fileSystem,
Object.fromEntries(
elementDetailList.map(({ name: tag, constructorName, properties }) => {
elementDetailList.map(([{ name: tag, constructorName, properties }]) => {
const componentName = getComponentName(tag);
const componentPropsName = `${componentName}Props`;
const relativePath = getRelativePath(elementFilePath, outDir);
Expand All @@ -31,13 +31,13 @@ export async function compileSvelte(elementsDir: string, outDir: string, ns = ''
interface ${componentPropsName} extends HTMLAttributes<HTMLElement> {
${properties
.map(({ name, reactive, event, deprecated }) =>
.map(({ name, getter, event, deprecated }) =>
event
? [
getJsDocDescName(`'on:${event}'`, deprecated),
`(event: CustomEvent<Parameters<${constructorName}['${name}']>[0]>) => void`,
].join('?:')
: reactive
: !getter
? [getJsDocDescName(name, deprecated), `${constructorName}['${name}']`].join('?:')
: '',
)
Expand Down
Loading

0 comments on commit 3184560

Please sign in to comment.