diff --git a/packages/gem-analyzer/src/index.ts b/packages/gem-analyzer/src/index.ts
index 99dd4c86..9dbe2161 100644
--- a/packages/gem-analyzer/src/index.ts
+++ b/packages/gem-analyzer/src/index.ts
@@ -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;
@@ -20,6 +22,8 @@ interface StaticMethod {
}
interface Property {
+ getter?: boolean;
+ setter?: boolean;
name: string;
reactive: boolean;
deprecated?: boolean;
@@ -50,6 +54,7 @@ interface ConstructorParam {
}
export interface ElementDetail {
+ relativePath?: string;
shadow: boolean;
name: string;
constructorName: string;
@@ -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'];
@@ -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;
@@ -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;
}
@@ -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()],
}));
}
@@ -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),
};
@@ -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);
@@ -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);
@@ -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;
};
@@ -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()!
@@ -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);
diff --git a/packages/gem-analyzer/src/lib/utils.ts b/packages/gem-analyzer/src/lib/utils.ts
index d196f7fa..25d73ebe 100644
--- a/packages/gem-analyzer/src/lib/utils.ts
+++ b/packages/gem-analyzer/src/lib/utils.ts
@@ -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) {
@@ -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');
+}
diff --git a/packages/gem-book/src/plugins/api.ts b/packages/gem-book/src/plugins/api.ts
index 6f76bbcd..05988d2c 100644
--- a/packages/gem-book/src/plugins/api.ts
+++ b/packages/gem-book/src/plugins/api.ts
@@ -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;
};
@@ -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', '
'),
],
@@ -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', '
'),
diff --git a/packages/gem-port/src/common.ts b/packages/gem-port/src/common.ts
index 75e8d405..3c8e92c2 100644
--- a/packages/gem-port/src/common.ts
+++ b/packages/gem-port/src/common.ts
@@ -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';
@@ -26,13 +26,18 @@ export function getElementPathList(elementsDir: string) {
return paths;
}
-const elementCache: Record = {};
-const project = new Project({ useInMemoryFileSystem: true });
+const elementCache: Record = {};
+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];
}
diff --git a/packages/gem-port/src/index.ts b/packages/gem-port/src/index.ts
index 56d3732a..bc363b60 100644
--- a/packages/gem-port/src/index.ts
+++ b/packages/gem-port/src/index.ts
@@ -15,6 +15,8 @@ const cliOptions = {
svelteNs: '',
};
+const timer = setTimeout(() => program.outputHelp());
+
program
.name(name)
.description(description)
@@ -23,12 +25,11 @@ program
.option('--svelte-ns ', `specify svelte element namespace`, (ns: string) => (cliOptions.svelteNs = ns))
.arguments('')
.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);
diff --git a/packages/gem-port/src/react.ts b/packages/gem-port/src/react.ts
index 87dd197e..825e56f6 100644
--- a/packages/gem-port/src/react.ts
+++ b/packages/gem-port/src/react.ts
@@ -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]) => {
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',
`
@@ -25,18 +26,26 @@ async function createReactSourceFile(elementFilePath: string, outDir: string) {
export type ${componentPropsName} = HTMLAttributes & RefAttributes<${constructorName}> & {
${properties
- .map(({ name, reactive, event, deprecated }) =>
+ .map(({ name, getter, event, deprecated }) =>
event
? [
getJsDocDescName(`'on${event}'`, deprecated),
`(event: CustomEvent[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 {
@@ -45,16 +54,8 @@ 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 & RefAttributes<${componentMethodsName}>> = forwardRef<${componentMethodsName}, ${componentPropsName}>(function (props, ref): JSX.Element {
+ export const ${componentName}: ForwardRefExoticComponent & RefAttributes<${componentExposeName}>> = forwardRef<${componentExposeName}, ${componentPropsName}>(function (props, ref): JSX.Element {
const elementRef = useRef<${constructorName}>(null);
useImperativeHandle(ref, () => {
return {
@@ -62,11 +63,20 @@ async function createReactSourceFile(elementFilePath: string, outDir: string) {
.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('')}
};
}, []);
diff --git a/packages/gem-port/src/svelte.ts b/packages/gem-port/src/svelte.ts
index 24e4d31c..e24d03b2 100644
--- a/packages/gem-port/src/svelte.ts
+++ b/packages/gem-port/src/svelte.ts
@@ -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);
@@ -31,13 +31,13 @@ export async function compileSvelte(elementsDir: string, outDir: string, ns = ''
interface ${componentPropsName} extends HTMLAttributes {
${properties
- .map(({ name, reactive, event, deprecated }) =>
+ .map(({ name, getter, event, deprecated }) =>
event
? [
getJsDocDescName(`'on:${event}'`, deprecated),
`(event: CustomEvent[0]>) => void`,
].join('?:')
- : reactive
+ : !getter
? [getJsDocDescName(name, deprecated), `${constructorName}['${name}']`].join('?:')
: '',
)
diff --git a/packages/gem-port/src/vue.ts b/packages/gem-port/src/vue.ts
index d611275b..127747b9 100644
--- a/packages/gem-port/src/vue.ts
+++ b/packages/gem-port/src/vue.ts
@@ -32,11 +32,12 @@ export async function generateVue(elementsDir: string, outDir: string) {
const processFile = async (elementFilePath: string) => {
const elements = await getFileElements(elementFilePath);
- elements.forEach(({ name: tag, properties, constructorName, methods, events }) => {
+ elements.forEach(([{ name: tag, properties, constructorName, methods, events }]) => {
const componentName = getComponentName(tag);
- const componentMethodsName = `${componentName}Methods`;
+ const componentExposeName = `${componentName}Expose`;
const relativePath = getRelativePath(elementFilePath, outDir);
- const reactiveProps = properties.filter(({ reactive }) => !!reactive);
+ const settableProperties = properties.filter((e) => !e.getter && !e.event);
+ const getters = properties.filter((e) => e.getter);
writeFileSync(
resolve(outDir, componentName + '.vue'),
`
@@ -46,19 +47,13 @@ export async function generateVue(elementsDir: string, outDir: string) {
const elementRef = ref<${constructorName}>();
- ${
- reactiveProps.length
- ? `
const props = defineProps<{
- ${reactiveProps
+ ${settableProperties
.map(({ name, deprecated }) =>
[getJsDocDescName(name, deprecated), `${constructorName}['${name}']`].join('?: '),
)
.join(',\n')}
}>();
- `
- : ''
- }
const emit = defineEmits<{
${properties
@@ -72,30 +67,39 @@ export async function generateVue(elementsDir: string, outDir: string) {
.join(',\n')}
}>()
- type ${componentMethodsName} = {
- ${methods
+ type ${componentExposeName} = {
+ ${[...methods, ...getters]
.map(({ name, deprecated }) =>
[getJsDocDescName(name, deprecated), `${constructorName}['${name}']`].join(': '),
)
.join(';\n')}
}
- defineExpose<${componentMethodsName}>({
+ defineExpose<${componentExposeName}>({
${methods
.map(
({ name }) => `
${name}(...args) {
- elementRef.value?.${name}(...args)
- }
+ return elementRef.value!.${name}(...args)
+ },
+ `,
+ )
+ .join('')}
+ ${getters
+ .map(
+ ({ name }) => `
+ get ${name}() {
+ return elementRef.value!.${name}
+ },
`,
)
- .join(',')}
+ .join('')}
})
// prop 可以用 :prop
onMounted(() => {
const element = elementRef.value!;
- ${properties.map(({ name, reactive }) => (reactive ? `element.${name} = props.${name}` : '')).join(';\n')}
+ ${settableProperties.map(({ name }) => `element.${name} = props.${name}`).join(';\n')}
})
@@ -108,8 +112,8 @@ export async function generateVue(elementsDir: string, outDir: string) {
- <${tag} ref="elementRef" ${reactiveProps.length ? 'v-bind="props"' : ''} ${events
- .map((event) => [`@${event}`, `"e => emit('change', e)"`].join('='))
+ <${tag} ref="elementRef" v-bind="props" ${events
+ .map((event) => [`@${event}`, `"e => emit('${event}', e)"`].join('='))
.join(' ')}>
${tag}>
diff --git a/packages/gem/src/helper/ssr-shim.ts b/packages/gem/src/helper/ssr-shim.ts
index f5a9b43f..c98916a7 100644
--- a/packages/gem/src/helper/ssr-shim.ts
+++ b/packages/gem/src/helper/ssr-shim.ts
@@ -26,7 +26,6 @@ if (typeof window === 'undefined') {
globalThis.document = any;
globalThis.addEventListener = any;
globalThis.history = any;
- globalThis.navigator = any;
globalThis.customElements = any;
// duoyun-ui
@@ -34,6 +33,14 @@ if (typeof window === 'undefined') {
globalThis.Document = any;
globalThis.Element = any;
globalThis.MutationObserver = any;
+
+ try {
+ // gem dist i18n
+ // https://nodejs.org/api/globals.html#navigator_1
+ globalThis.navigator = any;
+ } catch {
+ //
+ }
}
export {};