mirror of
https://github.com/alibaba/lowcode-engine.git
synced 2026-04-18 03:18:06 +00:00
refactor: render-core
This commit is contained in:
parent
8510f998fe
commit
d632e7f7e6
@ -21,8 +21,6 @@
|
|||||||
"prepare": "husky"
|
"prepare": "husky"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@alilc/build-plugin-lce": "^0.0.5",
|
|
||||||
"@alilc/lowcode-test-mate": "^1.0.1",
|
|
||||||
"@changesets/cli": "^2.27.1",
|
"@changesets/cli": "^2.27.1",
|
||||||
"@commitlint/cli": "^19.2.1",
|
"@commitlint/cli": "^19.2.1",
|
||||||
"@commitlint/config-conventional": "^19.1.0",
|
"@commitlint/config-conventional": "^19.1.0",
|
||||||
@ -30,7 +28,6 @@
|
|||||||
"@microsoft/api-extractor": "^7.43.0",
|
"@microsoft/api-extractor": "^7.43.0",
|
||||||
"@stylistic/eslint-plugin": "^1.7.0",
|
"@stylistic/eslint-plugin": "^1.7.0",
|
||||||
"@types/node": "^20.11.30",
|
"@types/node": "^20.11.30",
|
||||||
"@types/react-router": "5.1.18",
|
|
||||||
"@vanilla-extract/vite-plugin": "^4.0.7",
|
"@vanilla-extract/vite-plugin": "^4.0.7",
|
||||||
"@vitejs/plugin-react": "^4.2.1",
|
"@vitejs/plugin-react": "^4.2.1",
|
||||||
"eslint": "^8.57.0",
|
"eslint": "^8.57.0",
|
||||||
@ -38,14 +35,13 @@
|
|||||||
"eslint-plugin-react-hooks": "^4.6.0",
|
"eslint-plugin-react-hooks": "^4.6.0",
|
||||||
"globals": "^15.0.0",
|
"globals": "^15.0.0",
|
||||||
"husky": "^9.0.11",
|
"husky": "^9.0.11",
|
||||||
"less": "^4.2.0",
|
|
||||||
"lint-staged": "^15.2.2",
|
"lint-staged": "^15.2.2",
|
||||||
"prettier": "^3.2.5",
|
"prettier": "^3.2.5",
|
||||||
"rimraf": "^5.0.2",
|
"rimraf": "^5.0.2",
|
||||||
"typescript": "^5.4.2",
|
"typescript": "^5.4.2",
|
||||||
"typescript-eslint": "^7.5.0",
|
"typescript-eslint": "^7.5.0",
|
||||||
"vite": "^5.2.9",
|
"vite": "^5.2.9",
|
||||||
"vitest": "^1.5.0"
|
"vitest": "^1.6.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0",
|
"node": "^18.18.0 || ^20.9.0 || >=21.1.0",
|
||||||
|
|||||||
@ -33,13 +33,10 @@
|
|||||||
"test:cov": ""
|
"test:cov": ""
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@abraham/reflection": "^0.12.0",
|
|
||||||
"@alilc/lowcode-shared": "workspace:*",
|
"@alilc/lowcode-shared": "workspace:*",
|
||||||
"@alilc/lowcode-types": "workspace:*",
|
"@alilc/lowcode-types": "workspace:*",
|
||||||
"@alilc/lowcode-utils": "workspace:*",
|
"@alilc/lowcode-utils": "workspace:*",
|
||||||
"@formatjs/intl": "^2.10.1",
|
"@formatjs/intl": "^2.10.1",
|
||||||
"inversify": "^6.0.2",
|
|
||||||
"inversify-binding-decorators": "^4.0.0",
|
|
||||||
"lodash-es": "^4.17.21",
|
"lodash-es": "^4.17.21",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
|
|||||||
@ -1,23 +1,13 @@
|
|||||||
import { get as lodashGet, isPlainObject } from 'lodash-es';
|
import { get as lodashGet, isPlainObject, cloneDeep } from 'lodash-es';
|
||||||
import { createLogger, type PlainObject, invariant } from '@alilc/lowcode-shared';
|
import { type PlainObject } from '@alilc/lowcode-shared/src/types';
|
||||||
|
import { invariant } from '@alilc/lowcode-shared/src/utils';
|
||||||
const logger = createLogger({ level: 'log', bizName: 'config' });
|
|
||||||
|
|
||||||
// this default behavior will be different later
|
|
||||||
const STRICT_PLUGIN_MODE_DEFAULT = true;
|
|
||||||
|
|
||||||
interface ConfigurationOptions<Config extends PlainObject, K extends keyof Config = keyof Config> {
|
|
||||||
strictMode?: boolean;
|
|
||||||
setterValidator?: (key: K, value: Config[K]) => boolean | string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class Configuration<Config extends PlainObject, K extends keyof Config = keyof Config> {
|
export class Configuration<Config extends PlainObject, K extends keyof Config = keyof Config> {
|
||||||
#strictMode = STRICT_PLUGIN_MODE_DEFAULT;
|
private config: Config;
|
||||||
#setterValidator: (key: K, value: Config[K]) => boolean | string = () => true;
|
|
||||||
|
|
||||||
#config: Config = {} as Config;
|
private setterValidator: ((key: K, value: Config[K]) => boolean | string) | undefined;
|
||||||
|
|
||||||
#waits = new Map<
|
private waits = new Map<
|
||||||
K,
|
K,
|
||||||
{
|
{
|
||||||
once?: boolean;
|
once?: boolean;
|
||||||
@ -25,23 +15,18 @@ export class Configuration<Config extends PlainObject, K extends keyof Config =
|
|||||||
}[]
|
}[]
|
||||||
>();
|
>();
|
||||||
|
|
||||||
constructor(config: Config, options?: ConfigurationOptions<Config>) {
|
constructor(config: Config, setterValidator?: (key: K, value: Config[K]) => boolean | string) {
|
||||||
invariant(config, 'config must exist', 'Configuration');
|
invariant(config, 'config must exist', 'Configuration');
|
||||||
|
|
||||||
this.#config = config;
|
this.config = cloneDeep(config);
|
||||||
|
|
||||||
const { strictMode, setterValidator } = options ?? {};
|
|
||||||
|
|
||||||
if (strictMode === false) {
|
|
||||||
this.#strictMode = false;
|
|
||||||
}
|
|
||||||
if (setterValidator) {
|
if (setterValidator) {
|
||||||
invariant(
|
invariant(
|
||||||
typeof setterValidator === 'function',
|
typeof setterValidator === 'function',
|
||||||
'setterValidator must be a function',
|
'setterValidator must be a function',
|
||||||
'Configuration',
|
'Configuration',
|
||||||
);
|
);
|
||||||
this.#setterValidator = setterValidator;
|
this.setterValidator = setterValidator;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -50,38 +35,35 @@ export class Configuration<Config extends PlainObject, K extends keyof Config =
|
|||||||
* @param key
|
* @param key
|
||||||
*/
|
*/
|
||||||
has(key: K): boolean {
|
has(key: K): boolean {
|
||||||
return this.#config[key] !== undefined;
|
return this.config[key] !== undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取指定 key 的值
|
* 获取指定 key 的值
|
||||||
* @param key
|
* @param key
|
||||||
* @param defaultValue
|
* @param defaultValue
|
||||||
*/
|
*/
|
||||||
get(key: K, defaultValue?: any): any {
|
get<T = any>(key: K, defaultValue?: T): T | undefined {
|
||||||
return lodashGet(this.#config, key, defaultValue);
|
return lodashGet(this.config, key, defaultValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 设置指定 key 的值
|
* 设置指定 key 的值
|
||||||
* @param key
|
* @param key
|
||||||
* @param value
|
* @param value
|
||||||
*/
|
*/
|
||||||
set(key: K, value: any) {
|
set(key: K, value: any) {
|
||||||
if (this.#strictMode) {
|
if (this.setterValidator) {
|
||||||
const valid = this.#setterValidator(key, value);
|
const valid = this.setterValidator(key, value);
|
||||||
if (valid === false || typeof valid === 'string') {
|
|
||||||
return logger.warn(
|
invariant(
|
||||||
`failed to config ${key.toString()}, only predefined options can be set under strict mode, predefined options: `,
|
valid === false || typeof valid === 'string',
|
||||||
valid ? valid : '',
|
`failed to config ${key.toString()}, only predefined options can be set under strict mode, predefined options: ${valid ? valid : ''}`,
|
||||||
|
'Configuration',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
this.#config[key] = value;
|
this.config[key] = value;
|
||||||
this.notifyGot(key);
|
this.notifyGot(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 批量设值,set 的对象版本
|
* 批量设值,set 的对象版本
|
||||||
* @param config
|
* @param config
|
||||||
@ -93,7 +75,6 @@ export class Configuration<Config extends PlainObject, K extends keyof Config =
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取指定 key 的值,若此时还未赋值,则等待,若已有值,则直接返回值
|
* 获取指定 key 的值,若此时还未赋值,则等待,若已有值,则直接返回值
|
||||||
* 注:此函数返回 Promise 实例,只会执行(fullfill)一次
|
* 注:此函数返回 Promise 实例,只会执行(fullfill)一次
|
||||||
@ -101,7 +82,7 @@ export class Configuration<Config extends PlainObject, K extends keyof Config =
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
onceGot(key: K) {
|
onceGot(key: K) {
|
||||||
const val = this.#config[key];
|
const val = this.get(key);
|
||||||
if (val !== undefined) {
|
if (val !== undefined) {
|
||||||
return Promise.resolve(val);
|
return Promise.resolve(val);
|
||||||
}
|
}
|
||||||
@ -109,7 +90,6 @@ export class Configuration<Config extends PlainObject, K extends keyof Config =
|
|||||||
this.setWait(key, resolve, true);
|
this.setWait(key, resolve, true);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取指定 key 的值,函数回调模式,若多次被赋值,回调会被多次调用
|
* 获取指定 key 的值,函数回调模式,若多次被赋值,回调会被多次调用
|
||||||
* @param key
|
* @param key
|
||||||
@ -117,7 +97,7 @@ export class Configuration<Config extends PlainObject, K extends keyof Config =
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
onGot(key: K, fn: (data: Config[K]) => void): () => void {
|
onGot(key: K, fn: (data: Config[K]) => void): () => void {
|
||||||
const val = this.#config[key];
|
const val = this.config[key];
|
||||||
if (val !== undefined) {
|
if (val !== undefined) {
|
||||||
fn(val);
|
fn(val);
|
||||||
}
|
}
|
||||||
@ -127,8 +107,8 @@ export class Configuration<Config extends PlainObject, K extends keyof Config =
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
notifyGot(key: K): void {
|
private notifyGot(key: K): void {
|
||||||
let waits = this.#waits.get(key);
|
let waits = this.waits.get(key);
|
||||||
if (!waits) {
|
if (!waits) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -141,23 +121,23 @@ export class Configuration<Config extends PlainObject, K extends keyof Config =
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (waits.length > 0) {
|
if (waits.length > 0) {
|
||||||
this.#waits.set(key, waits);
|
this.waits.set(key, waits);
|
||||||
} else {
|
} else {
|
||||||
this.#waits.delete(key);
|
this.waits.delete(key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setWait(key: K, resolve: (data: any) => void, once?: boolean) {
|
private setWait(key: K, resolve: (data: any) => void, once?: boolean) {
|
||||||
const waits = this.#waits.get(key);
|
const waits = this.waits.get(key);
|
||||||
if (waits) {
|
if (waits) {
|
||||||
waits.push({ resolve, once });
|
waits.push({ resolve, once });
|
||||||
} else {
|
} else {
|
||||||
this.#waits.set(key, [{ resolve, once }]);
|
this.waits.set(key, [{ resolve, once }]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
delWait(key: K, fn: any) {
|
private delWait(key: K, fn: any) {
|
||||||
const waits = this.#waits.get(key);
|
const waits = this.waits.get(key);
|
||||||
if (!waits) {
|
if (!waits) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -168,7 +148,7 @@ export class Configuration<Config extends PlainObject, K extends keyof Config =
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (waits.length < 1) {
|
if (waits.length < 1) {
|
||||||
this.#waits.delete(key);
|
this.waits.delete(key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1,2 +0,0 @@
|
|||||||
export * from './config';
|
|
||||||
export { Preference, userPreference } from './preference';
|
|
||||||
@ -1,4 +1,4 @@
|
|||||||
export * from './configuration';
|
export * from './preference';
|
||||||
export * from './hotkey';
|
export * from './hotkey';
|
||||||
export * from './intl';
|
export * from './intl';
|
||||||
export * from './instantiation';
|
export * from './instantiation';
|
||||||
|
|||||||
@ -1,43 +0,0 @@
|
|||||||
import '@abraham/reflection';
|
|
||||||
import { Container, inject } from 'inversify';
|
|
||||||
import { fluentProvide, buildProviderModule } from 'inversify-binding-decorators';
|
|
||||||
|
|
||||||
export const iocContainer = new Container();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Identifies a service of type `T`.
|
|
||||||
*/
|
|
||||||
export interface ServiceIdentifier<T> {
|
|
||||||
(...args: any[]): void;
|
|
||||||
type: T;
|
|
||||||
}
|
|
||||||
|
|
||||||
type Constructor<T = any> = new (...args: any[]) => T;
|
|
||||||
|
|
||||||
export function createDecorator<T>(serviceId: string): ServiceIdentifier<T> {
|
|
||||||
const id = <any>(
|
|
||||||
function (target: Constructor, targetKey: string, indexOrPropertyDescriptor: any): any {
|
|
||||||
return inject(serviceId)(target, targetKey, indexOrPropertyDescriptor);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
id.toString = () => serviceId;
|
|
||||||
|
|
||||||
return id;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function Provide(serviceId: string, isSingleTon?: boolean) {
|
|
||||||
const ret = fluentProvide(serviceId.toString());
|
|
||||||
|
|
||||||
if (isSingleTon) {
|
|
||||||
return ret.inSingletonScope().done();
|
|
||||||
}
|
|
||||||
return ret.done();
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createInstance<T extends Constructor>(App: T) {
|
|
||||||
return iocContainer.resolve<InstanceType<T>>(App);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function bootstrapModules() {
|
|
||||||
iocContainer.load(buildProviderModule());
|
|
||||||
}
|
|
||||||
@ -3,8 +3,8 @@ import {
|
|||||||
computed,
|
computed,
|
||||||
effect,
|
effect,
|
||||||
createLogger,
|
createLogger,
|
||||||
|
type Spec,
|
||||||
type Signal,
|
type Signal,
|
||||||
type I18nMap,
|
|
||||||
type ComputedSignal,
|
type ComputedSignal,
|
||||||
type PlainObject,
|
type PlainObject,
|
||||||
} from '@alilc/lowcode-shared';
|
} from '@alilc/lowcode-shared';
|
||||||
@ -21,8 +21,8 @@ const logger = createLogger({ level: 'warn', bizName: 'globalLocale' });
|
|||||||
const STORED_LOCALE_KEY = 'ali-lowcode-config';
|
const STORED_LOCALE_KEY = 'ali-lowcode-config';
|
||||||
|
|
||||||
export type Locale = string;
|
export type Locale = string;
|
||||||
export type IntlMessage = I18nMap[Locale];
|
export type IntlMessage = Spec.I18nMap[Locale];
|
||||||
export type IntlMessageRecord = I18nMap;
|
export type IntlMessageRecord = Spec.I18nMap;
|
||||||
|
|
||||||
export class Intl {
|
export class Intl {
|
||||||
#locale: Signal<Locale>;
|
#locale: Signal<Locale>;
|
||||||
@ -34,7 +34,7 @@ export class Intl {
|
|||||||
if (defaultLocale) {
|
if (defaultLocale) {
|
||||||
defaultLocale = nomarlizeLocale(defaultLocale);
|
defaultLocale = nomarlizeLocale(defaultLocale);
|
||||||
} else {
|
} else {
|
||||||
defaultLocale = initializeLocale();
|
defaultLocale = 'zh-CN';
|
||||||
}
|
}
|
||||||
|
|
||||||
const messageStore = mapKeys(messages, (_, key) => {
|
const messageStore = mapKeys(messages, (_, key) => {
|
||||||
@ -65,22 +65,6 @@ export class Intl {
|
|||||||
|
|
||||||
setLocale(locale: Locale) {
|
setLocale(locale: Locale) {
|
||||||
const nomarlizedLocale = nomarlizeLocale(locale);
|
const nomarlizedLocale = nomarlizeLocale(locale);
|
||||||
|
|
||||||
try {
|
|
||||||
// store storage
|
|
||||||
let config = JSON.parse(localStorage.getItem(STORED_LOCALE_KEY) || '');
|
|
||||||
|
|
||||||
if (config && typeof config === 'object') {
|
|
||||||
config.locale = locale;
|
|
||||||
} else {
|
|
||||||
config = { locale };
|
|
||||||
}
|
|
||||||
|
|
||||||
localStorage.setItem(STORED_LOCALE_KEY, JSON.stringify(config));
|
|
||||||
} catch {
|
|
||||||
// ignore;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.#locale.value = nomarlizedLocale;
|
this.#locale.value = nomarlizedLocale;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -61,5 +61,3 @@ export class Preference {
|
|||||||
return !(result === undefined || result === null);
|
return !(result === undefined || result === null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const userPreference = new Preference();
|
|
||||||
@ -3,5 +3,5 @@
|
|||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"outDir": "dist"
|
"outDir": "dist"
|
||||||
},
|
},
|
||||||
"include": ["src", "__tests__"]
|
"include": ["src", "__tests__", "src/configuration.ts"]
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
import { signal, uniqueId, type ComponentTreeRootNode } from '@alilc/lowcode-shared';
|
import { signal, uniqueId, type Spec } from '@alilc/lowcode-shared';
|
||||||
import { type Project } from '../project';
|
import { type Project } from '../project';
|
||||||
import { History } from './history';
|
import { History } from './history';
|
||||||
|
|
||||||
export interface DocumentSchema extends ComponentTreeRootNode {
|
export interface DocumentSchema extends Spec.ComponentTreeRoot {
|
||||||
id: string;
|
id: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
import { ComponentTreeNode } from '@alilc/lowcode-shared';
|
import { Spec } from '@alilc/lowcode-shared';
|
||||||
import { type ComponentMeta } from '../component-meta';
|
import { type ComponentMeta } from '../component-meta';
|
||||||
import { type Prop } from './prop';
|
import { type Prop } from './prop';
|
||||||
|
|
||||||
export interface Node<Schema extends ComponentTreeNode = ComponentTreeNode> {
|
export interface Node<Schema extends Spec.ComponentNode = Spec.ComponentNode> {
|
||||||
/**
|
/**
|
||||||
* 节点 id
|
* 节点 id
|
||||||
* node id
|
* node id
|
||||||
@ -353,6 +353,6 @@ export interface Node<Schema extends ComponentTreeNode = ComponentTreeNode> {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createNode<Schema extends ComponentTreeNode>(nodeSchema: Schema): Node<Schema> {
|
export function createNode<Schema extends Spec.ComponentNode>(nodeSchema: Schema): Node<Schema> {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -25,7 +25,6 @@
|
|||||||
"@alilc/lowcode-shared": "workspace:*",
|
"@alilc/lowcode-shared": "workspace:*",
|
||||||
"@alilc/lowcode-renderer-core": "workspace:*",
|
"@alilc/lowcode-renderer-core": "workspace:*",
|
||||||
"@alilc/lowcode-renderer-router": "workspace:*",
|
"@alilc/lowcode-renderer-router": "workspace:*",
|
||||||
"@vue/reactivity": "^3.4.21",
|
|
||||||
"lodash-es": "^4.17.21",
|
"lodash-es": "^4.17.21",
|
||||||
"immer": "^10.0.4",
|
"immer": "^10.0.4",
|
||||||
"hoist-non-react-statics": "^3.3.2",
|
"hoist-non-react-statics": "^3.3.2",
|
||||||
@ -39,8 +38,7 @@
|
|||||||
"@types/hoist-non-react-statics": "^3.3.5",
|
"@types/hoist-non-react-statics": "^3.3.5",
|
||||||
"@types/use-sync-external-store": "^0.0.6",
|
"@types/use-sync-external-store": "^0.0.6",
|
||||||
"@types/react": "^18.2.67",
|
"@types/react": "^18.2.67",
|
||||||
"@types/react-dom": "^18.2.22",
|
"@types/react-dom": "^18.2.22"
|
||||||
"jsdom": "^24.0.0"
|
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
|
|||||||
@ -1,66 +1,69 @@
|
|||||||
import {
|
import { createRenderer, type AppOptions, type IRender } from '@alilc/lowcode-renderer-core';
|
||||||
type App,
|
|
||||||
type AppBase,
|
|
||||||
createAppFunction,
|
|
||||||
type AppOptionsBase,
|
|
||||||
} from '@alilc/lowcode-renderer-core';
|
|
||||||
import { type ComponentType } from 'react';
|
import { type ComponentType } from 'react';
|
||||||
import { type Root, createRoot } from 'react-dom/client';
|
import { type Root, createRoot } from 'react-dom/client';
|
||||||
import { createRouter } from '@alilc/lowcode-renderer-router';
|
import { createRouter, type RouterOptions } from '@alilc/lowcode-renderer-router';
|
||||||
import { createRenderer } from '../renderer';
|
|
||||||
import AppComponent from '../components/app';
|
import AppComponent from '../components/app';
|
||||||
import { createIntl } from '../runtime-api/intl';
|
import { RendererContext } from '../context/render';
|
||||||
import { createRuntimeUtils } from '../runtime-api/utils';
|
import { createRouterProvider } from '../components/routerView';
|
||||||
|
import { rendererExtends } from '../plugin';
|
||||||
|
|
||||||
export interface AppOptions extends AppOptionsBase {
|
export interface ReactAppOptions extends AppOptions {
|
||||||
dataSourceCreator: any;
|
|
||||||
faultComponent?: ComponentType<any>;
|
faultComponent?: ComponentType<any>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ReactRender extends AppBase {}
|
const defaultRouterOptions: RouterOptions = {
|
||||||
|
historyMode: 'browser',
|
||||||
|
baseName: '/',
|
||||||
|
routes: [],
|
||||||
|
};
|
||||||
|
|
||||||
export type ReactApp = App<ReactRender>;
|
export const createApp = async (options: ReactAppOptions) => {
|
||||||
|
const creator = createRenderer<IRender>(async (context) => {
|
||||||
export const createApp = createAppFunction<AppOptions, ReactRender>(async (context, options) => {
|
const { schema, boostsManager } = context;
|
||||||
const { schema, packageManager, appScope, boosts } = context;
|
const boosts = boostsManager.toExpose();
|
||||||
|
|
||||||
// router
|
// router
|
||||||
// todo: transform config
|
let routerConfig = defaultRouterOptions;
|
||||||
const router = createRouter(schema.getByKey('router') as any);
|
|
||||||
|
|
||||||
appScope.inject('router', router);
|
try {
|
||||||
|
const routerSchema = schema.get('router');
|
||||||
|
if (routerSchema) {
|
||||||
|
routerConfig = boosts.codeRuntime.resolve(routerSchema);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`schema's router config is resolve error: `, e);
|
||||||
|
}
|
||||||
|
|
||||||
// i18n
|
const router = createRouter(routerConfig);
|
||||||
const i18nMessages = schema.getByKey('i18n') ?? {};
|
|
||||||
const defaultLocale = schema.getByPath('config.defaultLocale') ?? 'zh-CN';
|
|
||||||
const intl = createIntl(i18nMessages, defaultLocale);
|
|
||||||
|
|
||||||
appScope.inject('intl', intl);
|
boosts.codeRuntime.getScope().inject('router', router);
|
||||||
|
|
||||||
// utils
|
|
||||||
const runtimeUtils = createRuntimeUtils(schema.getByKey('utils') ?? [], packageManager);
|
|
||||||
|
|
||||||
appScope.inject('utils', runtimeUtils.utils);
|
|
||||||
boosts.add('runtimeUtils', runtimeUtils);
|
|
||||||
|
|
||||||
// set config
|
// set config
|
||||||
if (options.faultComponent) {
|
// if (options.faultComponent) {
|
||||||
context.config.set('faultComponent', options.faultComponent);
|
// context.config.set('faultComponent', options.faultComponent);
|
||||||
}
|
// }
|
||||||
context.config.set('dataSourceCreator', options.dataSourceCreator);
|
|
||||||
|
// extends boosts
|
||||||
|
boostsManager.extend(rendererExtends);
|
||||||
|
|
||||||
|
const RouterProvider = createRouterProvider(router);
|
||||||
|
|
||||||
let root: Root | undefined;
|
let root: Root | undefined;
|
||||||
const renderer = createRenderer();
|
|
||||||
const appContext = { ...context, renderer };
|
|
||||||
|
|
||||||
const reactRender: ReactRender = {
|
return {
|
||||||
async mount(el) {
|
async mount(el) {
|
||||||
if (root) {
|
if (root) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
root = createRoot(el);
|
root = createRoot(el);
|
||||||
root.render(<AppComponent context={appContext} />);
|
root.render(
|
||||||
|
<RendererContext.Provider value={context}>
|
||||||
|
<RouterProvider>
|
||||||
|
<AppComponent />
|
||||||
|
</RouterProvider>
|
||||||
|
</RendererContext.Provider>,
|
||||||
|
);
|
||||||
},
|
},
|
||||||
unmount() {
|
unmount() {
|
||||||
if (root) {
|
if (root) {
|
||||||
@ -69,9 +72,7 @@ export const createApp = createAppFunction<AppOptions, ReactRender>(async (conte
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return creator(options);
|
||||||
appBase: reactRender,
|
};
|
||||||
renderer,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|||||||
@ -1,7 +1,34 @@
|
|||||||
import { createComponent as internalCreate, ComponentOptions } from '../component';
|
import { createRenderer, type AppOptions } from '@alilc/lowcode-renderer-core';
|
||||||
|
import { FunctionComponent } from 'react';
|
||||||
|
import { type LowCodeComponentProps, createComponentBySchema } from '../runtime';
|
||||||
|
import { RendererContext } from '../context/render';
|
||||||
|
|
||||||
export function createComponent(options: ComponentOptions) {
|
interface Render {
|
||||||
return internalCreate(options);
|
toComponent(): FunctionComponent<LowCodeComponentProps>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type { ComponentOptions };
|
export async function createComponent(options: AppOptions) {
|
||||||
|
const creator = createRenderer<Render>((context) => {
|
||||||
|
const { schema } = context;
|
||||||
|
|
||||||
|
const LowCodeComponent = createComponentBySchema(schema.get('componentsTree')[0]);
|
||||||
|
|
||||||
|
function Component(props: LowCodeComponentProps) {
|
||||||
|
return (
|
||||||
|
<RendererContext.Provider value={context}>
|
||||||
|
<LowCodeComponent {...props} />
|
||||||
|
</RendererContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
toComponent() {
|
||||||
|
return Component;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const render = await creator(options);
|
||||||
|
|
||||||
|
return render.toComponent();
|
||||||
|
}
|
||||||
|
|||||||
@ -1,425 +0,0 @@
|
|||||||
import {
|
|
||||||
createComponentFunction,
|
|
||||||
isLowCodeComponentSchema,
|
|
||||||
createCodeRuntime,
|
|
||||||
TextWidget,
|
|
||||||
ComponentWidget,
|
|
||||||
isJSExpression,
|
|
||||||
processValue,
|
|
||||||
isJSFunction,
|
|
||||||
isJSSlot,
|
|
||||||
someValue,
|
|
||||||
type CreateComponentBaseOptions,
|
|
||||||
type CodeRuntime,
|
|
||||||
} from '@alilc/lowcode-renderer-core';
|
|
||||||
import { isPlainObject } from 'lodash-es';
|
|
||||||
import { forwardRef, useRef, useEffect, createElement, useMemo } from 'react';
|
|
||||||
import { signal, watch } from './signals';
|
|
||||||
import { appendExternalStyle } from './utils/element';
|
|
||||||
import { reactive } from './utils/reactive';
|
|
||||||
|
|
||||||
import type {
|
|
||||||
PlainObject,
|
|
||||||
InstanceStateApi,
|
|
||||||
LowCodeComponent as LowCodeComponentSchema,
|
|
||||||
IntlApi,
|
|
||||||
JSSlot,
|
|
||||||
JSFunction,
|
|
||||||
I18nNode,
|
|
||||||
} from '@alilc/lowcode-shared';
|
|
||||||
import type {
|
|
||||||
ComponentType,
|
|
||||||
ReactInstance,
|
|
||||||
CSSProperties,
|
|
||||||
ForwardedRef,
|
|
||||||
ReactElement,
|
|
||||||
} from 'react';
|
|
||||||
|
|
||||||
export type ReactComponentLifeCycle =
|
|
||||||
| 'constructor'
|
|
||||||
| 'render'
|
|
||||||
| 'componentDidMount'
|
|
||||||
| 'componentDidUpdate'
|
|
||||||
| 'componentWillUnmount'
|
|
||||||
| 'componentDidCatch';
|
|
||||||
|
|
||||||
export interface ComponentOptions<C = ComponentType<any>>
|
|
||||||
extends CreateComponentBaseOptions<ReactComponentLifeCycle> {
|
|
||||||
componentsRecord: Record<string, C | LowCodeComponentSchema>;
|
|
||||||
intl: IntlApi;
|
|
||||||
displayName?: string;
|
|
||||||
|
|
||||||
beforeElementCreate?(widget: TextWidget<C> | ComponentWidget<C>): void;
|
|
||||||
componentRefAttached?(widget: ComponentWidget<C>, instance: ReactInstance): void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface LowCodeComponentProps {
|
|
||||||
id?: string;
|
|
||||||
/** CSS 类名 */
|
|
||||||
className?: string;
|
|
||||||
/** style */
|
|
||||||
style?: CSSProperties;
|
|
||||||
|
|
||||||
[key: string]: any;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const createComponent = createComponentFunction<
|
|
||||||
ComponentType<any>,
|
|
||||||
ReactInstance,
|
|
||||||
ReactComponentLifeCycle,
|
|
||||||
ComponentOptions
|
|
||||||
>(reactiveStateCreator, (container, options) => {
|
|
||||||
const {
|
|
||||||
componentsRecord,
|
|
||||||
intl,
|
|
||||||
displayName = '__LowCodeComponent__',
|
|
||||||
beforeElementCreate,
|
|
||||||
componentRefAttached,
|
|
||||||
|
|
||||||
...extraOptions
|
|
||||||
} = options;
|
|
||||||
const lowCodeComponentCache = new Map<string, ComponentType<any>>();
|
|
||||||
|
|
||||||
function getComponentByName(componentName: string) {
|
|
||||||
const Component = componentsRecord[componentName];
|
|
||||||
if (!Component) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isLowCodeComponentSchema(Component)) {
|
|
||||||
if (lowCodeComponentCache.has(componentName)) {
|
|
||||||
return lowCodeComponentCache.get(componentName);
|
|
||||||
}
|
|
||||||
|
|
||||||
const LowCodeComponent = createComponent({
|
|
||||||
...extraOptions,
|
|
||||||
intl,
|
|
||||||
displayName: Component.componentName,
|
|
||||||
componentsRecord,
|
|
||||||
componentsTree: Component.schema,
|
|
||||||
});
|
|
||||||
|
|
||||||
lowCodeComponentCache.set(componentName, LowCodeComponent);
|
|
||||||
|
|
||||||
return LowCodeComponent;
|
|
||||||
}
|
|
||||||
|
|
||||||
return Component;
|
|
||||||
}
|
|
||||||
|
|
||||||
function createReactElement(
|
|
||||||
widget: TextWidget<ComponentType<any>> | ComponentWidget<ComponentType<any>>,
|
|
||||||
codeRuntime: CodeRuntime,
|
|
||||||
) {
|
|
||||||
beforeElementCreate?.(widget);
|
|
||||||
|
|
||||||
return widget.build((elements) => {
|
|
||||||
if (elements.length > 0) {
|
|
||||||
const RenderObject = elements[elements.length - 1];
|
|
||||||
const Wrappers = elements.slice(0, elements.length - 1);
|
|
||||||
|
|
||||||
const buildRenderElement = () => {
|
|
||||||
if (widget instanceof TextWidget) {
|
|
||||||
if (widget.type === 'string') {
|
|
||||||
return createElement(RenderObject, { key: widget.key, text: widget.raw });
|
|
||||||
} else {
|
|
||||||
return createElement(
|
|
||||||
reactive(RenderObject, {
|
|
||||||
target:
|
|
||||||
widget.type === 'expression' ? { text: widget.raw } : (widget.raw as I18nNode),
|
|
||||||
valueGetter(expr) {
|
|
||||||
return codeRuntime.parseExprOrFn(expr);
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
{ key: widget.key },
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else if (widget instanceof ComponentWidget) {
|
|
||||||
const { condition, loop, loopArgs } = widget;
|
|
||||||
|
|
||||||
// condition为 Falsy 的情况下 不渲染
|
|
||||||
if (!condition) return null;
|
|
||||||
// loop 为数组且为空的情况下 不渲染
|
|
||||||
if (Array.isArray(loop) && loop.length === 0) return null;
|
|
||||||
|
|
||||||
function createElementWithProps(
|
|
||||||
Component: ComponentType<any>,
|
|
||||||
widget: ComponentWidget<ComponentType<any>>,
|
|
||||||
codeRuntime: CodeRuntime,
|
|
||||||
key?: string,
|
|
||||||
): ReactElement {
|
|
||||||
const { ref, ...componentProps } = widget.props;
|
|
||||||
const componentKey = key ?? widget.key;
|
|
||||||
|
|
||||||
const attachRef = (ins: ReactInstance) => {
|
|
||||||
if (ins) {
|
|
||||||
if (ref) container.setInstance(ref as string, ins);
|
|
||||||
componentRefAttached?.(widget, ins);
|
|
||||||
} else {
|
|
||||||
if (ref) container.removeInstance(ref);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 先将 jsslot, jsFunction 对象转换
|
|
||||||
const finalProps = processValue(
|
|
||||||
componentProps,
|
|
||||||
(node) => isJSFunction(node) || isJSSlot(node),
|
|
||||||
(node: JSSlot | JSFunction) => {
|
|
||||||
if (isJSSlot(node)) {
|
|
||||||
const slot = node as JSSlot;
|
|
||||||
|
|
||||||
if (slot.value) {
|
|
||||||
const widgets = (Array.isArray(node.value) ? node.value : [node.value]).map(
|
|
||||||
(v) => new ComponentWidget<ComponentType<any>>(v),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (slot.params?.length) {
|
|
||||||
return (...args: any[]) => {
|
|
||||||
const params = slot.params!.reduce((prev, cur, idx) => {
|
|
||||||
return (prev[cur] = args[idx]);
|
|
||||||
}, {} as PlainObject);
|
|
||||||
const subCodeScope = codeRuntime.getScope().createSubScope(params);
|
|
||||||
const subCodeRuntime = createCodeRuntime(subCodeScope);
|
|
||||||
|
|
||||||
return widgets.map((n) => createReactElement(n, subCodeRuntime));
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
return widgets.map((n) => createReactElement(n, codeRuntime));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (isJSFunction(node)) {
|
|
||||||
return codeRuntime.parseExprOrFn(node);
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const childElements = widget.children.map((child) =>
|
|
||||||
createReactElement(child, codeRuntime),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (someValue(finalProps, isJSExpression)) {
|
|
||||||
const PropsWrapper = (props: PlainObject) =>
|
|
||||||
createElement(
|
|
||||||
Component,
|
|
||||||
{
|
|
||||||
...props,
|
|
||||||
key: componentKey,
|
|
||||||
ref: attachRef,
|
|
||||||
},
|
|
||||||
childElements,
|
|
||||||
);
|
|
||||||
|
|
||||||
PropsWrapper.displayName = 'PropsWrapper';
|
|
||||||
|
|
||||||
return createElement(
|
|
||||||
reactive(PropsWrapper, {
|
|
||||||
target: finalProps,
|
|
||||||
valueGetter: (node) => codeRuntime.parseExprOrFn(node),
|
|
||||||
}),
|
|
||||||
{ key: componentKey },
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return createElement(
|
|
||||||
Component,
|
|
||||||
{
|
|
||||||
...finalProps,
|
|
||||||
key: componentKey,
|
|
||||||
ref: attachRef,
|
|
||||||
},
|
|
||||||
childElements,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let element: ReactElement | ReactElement[] = createElementWithProps(
|
|
||||||
RenderObject,
|
|
||||||
widget,
|
|
||||||
codeRuntime,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (loop) {
|
|
||||||
const genLoopElements = (loopData: any[]) => {
|
|
||||||
return loopData.map((item, idx) => {
|
|
||||||
const loopArgsItem = loopArgs[0] ?? 'item';
|
|
||||||
const loopArgsIndex = loopArgs[1] ?? 'index';
|
|
||||||
const subCodeScope = codeRuntime.getScope().createSubScope({
|
|
||||||
[loopArgsItem]: item,
|
|
||||||
[loopArgsIndex]: idx,
|
|
||||||
});
|
|
||||||
const subCodeRuntime = createCodeRuntime(subCodeScope);
|
|
||||||
|
|
||||||
return createElementWithProps(
|
|
||||||
RenderObject,
|
|
||||||
widget,
|
|
||||||
subCodeRuntime,
|
|
||||||
`loop-${widget.key}-${idx}`,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
if (isJSExpression(loop)) {
|
|
||||||
function Loop(props: { loop: boolean }) {
|
|
||||||
if (!Array.isArray(props.loop)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return <>{genLoopElements(props.loop)}</>;
|
|
||||||
}
|
|
||||||
Loop.displayName = 'Loop';
|
|
||||||
|
|
||||||
const ReactivedLoop = reactive(Loop, {
|
|
||||||
target: {
|
|
||||||
loop,
|
|
||||||
},
|
|
||||||
valueGetter: (expr) => codeRuntime.parseExprOrFn(expr),
|
|
||||||
});
|
|
||||||
|
|
||||||
element = createElement(ReactivedLoop, {
|
|
||||||
key: widget.key,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
element = genLoopElements(loop as any[]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isJSExpression(condition)) {
|
|
||||||
function Condition(props: any) {
|
|
||||||
if (props.condition) {
|
|
||||||
return element;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
Condition.displayName = 'Condition';
|
|
||||||
|
|
||||||
const ReactivedCondition = reactive(Condition, {
|
|
||||||
target: {
|
|
||||||
condition,
|
|
||||||
},
|
|
||||||
valueGetter: (expr) => codeRuntime.parseExprOrFn(expr),
|
|
||||||
});
|
|
||||||
|
|
||||||
element = createElement(ReactivedCondition, {
|
|
||||||
key: widget.key,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return element;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
||||||
const element = buildRenderElement();
|
|
||||||
|
|
||||||
return Wrappers.reduce((prevElement, CurWrapper) => {
|
|
||||||
return createElement(CurWrapper, { key: widget.key }, prevElement);
|
|
||||||
}, element);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const LowCodeComponent = forwardRef(function (
|
|
||||||
props: LowCodeComponentProps,
|
|
||||||
ref: ForwardedRef<any>,
|
|
||||||
) {
|
|
||||||
const { id, className, style } = props;
|
|
||||||
const isConstructed = useRef(false);
|
|
||||||
const isMounted = useRef(false);
|
|
||||||
|
|
||||||
if (!isConstructed.current) {
|
|
||||||
container.triggerLifeCycle('constructor');
|
|
||||||
isConstructed.current = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const scopeValue = container.codeRuntime.getScope().value;
|
|
||||||
|
|
||||||
// init dataSource
|
|
||||||
scopeValue.reloadDataSource();
|
|
||||||
|
|
||||||
let styleEl: HTMLElement | undefined;
|
|
||||||
const cssText = container.getCssText();
|
|
||||||
if (cssText) {
|
|
||||||
appendExternalStyle(cssText).then((el) => {
|
|
||||||
styleEl = el;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// trigger lifeCycles
|
|
||||||
// componentDidMount?.();
|
|
||||||
container.triggerLifeCycle('componentDidMount');
|
|
||||||
|
|
||||||
// 当 state 改变之后调用
|
|
||||||
const unwatch = watch(scopeValue.state, (_, oldVal) => {
|
|
||||||
if (isMounted.current) {
|
|
||||||
container.triggerLifeCycle('componentDidUpdate', props, oldVal);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
isMounted.current = true;
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
// componentWillUnmount?.();
|
|
||||||
container.triggerLifeCycle('componentWillUnmount');
|
|
||||||
styleEl?.parentNode?.removeChild(styleEl);
|
|
||||||
unwatch();
|
|
||||||
isMounted.current = false;
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const widgets = useMemo(() => {
|
|
||||||
return container.createWidgets<ComponentType<any>>().map((widget) =>
|
|
||||||
widget.mapRenderObject((widget) => {
|
|
||||||
if (widget instanceof TextWidget) {
|
|
||||||
if (widget.type === 'i18n') {
|
|
||||||
function IntlText(props: { key: string; params: Record<string, string> }) {
|
|
||||||
return <>{intl.i18n(props.key, props.params)}</>;
|
|
||||||
}
|
|
||||||
IntlText.displayName = 'IntlText';
|
|
||||||
return IntlText;
|
|
||||||
}
|
|
||||||
|
|
||||||
function Text(props: { text: string }) {
|
|
||||||
return <>{props.text}</>;
|
|
||||||
}
|
|
||||||
Text.displayName = 'Text';
|
|
||||||
return Text;
|
|
||||||
} else if (widget instanceof ComponentWidget) {
|
|
||||||
return getComponentByName(widget.raw.componentName);
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div id={id} className={className} style={style} ref={ref}>
|
|
||||||
{widgets.map((widget) => createReactElement(widget, container.codeRuntime))}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
LowCodeComponent.displayName = displayName;
|
|
||||||
|
|
||||||
return LowCodeComponent;
|
|
||||||
});
|
|
||||||
|
|
||||||
function reactiveStateCreator(initState: PlainObject): InstanceStateApi {
|
|
||||||
const proxyState = signal(initState);
|
|
||||||
|
|
||||||
return {
|
|
||||||
get state() {
|
|
||||||
return proxyState.value;
|
|
||||||
},
|
|
||||||
setState(newState) {
|
|
||||||
if (!isPlainObject(newState)) {
|
|
||||||
throw Error('newState mush be a object');
|
|
||||||
}
|
|
||||||
|
|
||||||
proxyState.value = {
|
|
||||||
...proxyState.value,
|
|
||||||
...newState,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@ -1,33 +1,25 @@
|
|||||||
import { AppContext, type AppContextObject } from '../context/app';
|
import { isLowCodeComponentSchema } from '@alilc/lowcode-shared';
|
||||||
import { createComponent } from '../component';
|
import { useRenderContext } from '../context/render';
|
||||||
|
import { createComponentBySchema, ReactComponent } from '../runtime';
|
||||||
import Route from './route';
|
import Route from './route';
|
||||||
import { createRouterProvider } from './router-view';
|
import { rendererExtends } from '../plugin';
|
||||||
|
|
||||||
export default function App({ context }: { context: AppContextObject }) {
|
export default function App() {
|
||||||
const { schema, config, renderer, packageManager, appScope } = context;
|
const { schema, packageManager } = useRenderContext();
|
||||||
const appWrappers = renderer.getAppWrappers();
|
const appWrappers = rendererExtends.getAppWrappers();
|
||||||
const wrappers = renderer.getRouteWrappers();
|
const wrappers = rendererExtends.getRouteWrappers();
|
||||||
|
|
||||||
function getLayoutComponent() {
|
function getLayoutComponent() {
|
||||||
const layoutName = schema.getByPath('config.layout.componentName');
|
const config = schema.get('config');
|
||||||
|
const componentName = config?.layout?.componentName as string;
|
||||||
|
|
||||||
if (layoutName) {
|
if (componentName) {
|
||||||
const Component: any = packageManager.getComponent(layoutName);
|
const Component = packageManager.getComponent<ReactComponent>(componentName);
|
||||||
|
|
||||||
if (Component?.devMode === 'lowCode') {
|
if (isLowCodeComponentSchema(Component)) {
|
||||||
const componentsMap = schema.getComponentsMaps();
|
return createComponentBySchema(Component.schema, {
|
||||||
const componentsRecord = packageManager.getComponentsNameRecord<any>(componentsMap);
|
displayName: componentName,
|
||||||
|
|
||||||
const Layout = createComponent({
|
|
||||||
componentsTree: Component.schema,
|
|
||||||
componentsRecord,
|
|
||||||
|
|
||||||
dataSourceCreator: config.get('dataSourceCreator'),
|
|
||||||
supCodeScope: appScope,
|
|
||||||
intl: appScope.value.intl,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return Layout;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return Component;
|
return Component;
|
||||||
@ -45,7 +37,7 @@ export default function App({ context }: { context: AppContextObject }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (Layout) {
|
if (Layout) {
|
||||||
const layoutProps = schema.getByPath('config.layout.props') ?? {};
|
const layoutProps: any = schema.get('config')?.layout?.props ?? {};
|
||||||
element = <Layout {...layoutProps}>{element}</Layout>;
|
element = <Layout {...layoutProps}>{element}</Layout>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -55,11 +47,5 @@ export default function App({ context }: { context: AppContextObject }) {
|
|||||||
}, element);
|
}, element);
|
||||||
}
|
}
|
||||||
|
|
||||||
const RouterProvider = createRouterProvider(appScope.value.router);
|
return element;
|
||||||
|
|
||||||
return (
|
|
||||||
<AppContext.Provider value={context}>
|
|
||||||
<RouterProvider>{element}</RouterProvider>
|
|
||||||
</AppContext.Provider>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,32 +0,0 @@
|
|||||||
import type { PageConfig, ComponentTree } from '@alilc/lowcode-renderer-core';
|
|
||||||
import { useAppContext } from '../context/app';
|
|
||||||
import { createComponent } from '../component';
|
|
||||||
|
|
||||||
export interface OutletProps {
|
|
||||||
pageConfig: PageConfig;
|
|
||||||
componentsTree?: ComponentTree | undefined;
|
|
||||||
|
|
||||||
[key: string]: any;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function Outlet({ pageSchema, componentsTree }: OutletProps) {
|
|
||||||
const { schema, config, packageManager, appScope } = useAppContext();
|
|
||||||
const { type = 'lowCode' } = pageSchema;
|
|
||||||
|
|
||||||
if (type === 'lowCode' && componentsTree) {
|
|
||||||
const componentsMap = schema.getComponentsMaps();
|
|
||||||
const componentsRecord = packageManager.getComponentsNameRecord<any>(componentsMap);
|
|
||||||
|
|
||||||
const LowCodeComponent = createComponent({
|
|
||||||
supCodeScope: appScope,
|
|
||||||
dataSourceCreator: config.get('dataSourceCreator'),
|
|
||||||
componentsTree,
|
|
||||||
componentsRecord,
|
|
||||||
intl: appScope.value.intl,
|
|
||||||
});
|
|
||||||
|
|
||||||
return <LowCodeComponent />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
@ -1,21 +1,43 @@
|
|||||||
|
import { type Spec } from '@alilc/lowcode-shared';
|
||||||
|
import { useRenderContext } from '../context/render';
|
||||||
import { usePageConfig } from '../context/router';
|
import { usePageConfig } from '../context/router';
|
||||||
import { useAppContext } from '../context/app';
|
import { rendererExtends } from '../plugin';
|
||||||
import RouteOutlet from './outlet';
|
import { createComponentBySchema } from '../runtime';
|
||||||
|
|
||||||
|
export interface OutletProps {
|
||||||
|
pageConfig: Spec.PageConfig;
|
||||||
|
|
||||||
|
[key: string]: any;
|
||||||
|
}
|
||||||
|
|
||||||
export default function Route(props: any) {
|
export default function Route(props: any) {
|
||||||
const { schema, renderer } = useAppContext();
|
|
||||||
const pageConfig = usePageConfig();
|
const pageConfig = usePageConfig();
|
||||||
const Outlet = renderer.getOutlet() ?? RouteOutlet;
|
|
||||||
|
|
||||||
if (Outlet && pageConfig) {
|
if (pageConfig) {
|
||||||
let componentsTree;
|
const Outlet = rendererExtends.getOutlet() ?? RouteOutlet;
|
||||||
const { type = 'lowCode', mappingId } = pageConfig;
|
|
||||||
|
|
||||||
if (type === 'lowCode') {
|
return <Outlet {...props} pageConfig={pageConfig} />;
|
||||||
componentsTree = schema.getComponentsTrees().find((item) => item.id === mappingId);
|
}
|
||||||
}
|
|
||||||
|
return null;
|
||||||
return <Outlet {...props} pageConfig={pageConfig} componentsTree={componentsTree} />;
|
}
|
||||||
|
|
||||||
|
function RouteOutlet({ pageConfig }: OutletProps) {
|
||||||
|
const context = useRenderContext();
|
||||||
|
const { schema, packageManager } = context;
|
||||||
|
const { type = 'lowCode', mappingId } = pageConfig;
|
||||||
|
|
||||||
|
if (type === 'lowCode') {
|
||||||
|
// 在页面渲染时重新获取 componentsMap
|
||||||
|
// 因为 componentsMap 可能在路由跳转之前懒加载新的页面 schema
|
||||||
|
const componentsMap = schema.get('componentsMap');
|
||||||
|
packageManager.resolveComponentMaps(componentsMap);
|
||||||
|
|
||||||
|
const LowCodeComponent = createComponentBySchema(mappingId, {
|
||||||
|
displayName: pageConfig?.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
return <LowCodeComponent />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@ -1,11 +1,11 @@
|
|||||||
import { type Router } from '@alilc/lowcode-renderer-router';
|
import { type Router } from '@alilc/lowcode-renderer-router';
|
||||||
import { useState, useLayoutEffect, useMemo, type ReactNode } from 'react';
|
import { useState, useLayoutEffect, useMemo, type ReactNode } from 'react';
|
||||||
import { RouterContext, RouteLocationContext, PageConfigContext } from '../context/router';
|
import { RouterContext, RouteLocationContext, PageConfigContext } from '../context/router';
|
||||||
import { useAppContext } from '../context/app';
|
import { useRenderContext } from '../context/render';
|
||||||
|
|
||||||
export const createRouterProvider = (router: Router) => {
|
export const createRouterProvider = (router: Router) => {
|
||||||
return function RouterProvider({ children }: { children?: ReactNode }) {
|
return function RouterProvider({ children }: { children?: ReactNode }) {
|
||||||
const { schema } = useAppContext();
|
const { schema } = useRenderContext();
|
||||||
const [location, setCurrentLocation] = useState(router.getCurrentLocation());
|
const [location, setCurrentLocation] = useState(router.getCurrentLocation());
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
@ -14,7 +14,7 @@ export const createRouterProvider = (router: Router) => {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const pageSchema = useMemo(() => {
|
const pageSchema = useMemo(() => {
|
||||||
const pages = schema.getPageConfigs();
|
const pages = schema.get('pages') ?? [];
|
||||||
const matched = location.matched[location.matched.length - 1];
|
const matched = location.matched[location.matched.length - 1];
|
||||||
|
|
||||||
if (matched) {
|
if (matched) {
|
||||||
@ -1,13 +0,0 @@
|
|||||||
import { createContext, useContext } from 'react';
|
|
||||||
import { type AppContext as AppContextType } from '@alilc/lowcode-renderer-core';
|
|
||||||
import { type ReactRenderer } from '../renderer';
|
|
||||||
|
|
||||||
export interface AppContextObject extends AppContextType {
|
|
||||||
renderer: ReactRenderer;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const AppContext = createContext<AppContextObject>({} as any);
|
|
||||||
|
|
||||||
AppContext.displayName = 'RootContext';
|
|
||||||
|
|
||||||
export const useAppContext = () => useContext(AppContext);
|
|
||||||
8
packages/react-renderer/src/context/render.ts
Normal file
8
packages/react-renderer/src/context/render.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import { createContext, useContext } from 'react';
|
||||||
|
import { type RenderContext } from '@alilc/lowcode-renderer-core';
|
||||||
|
|
||||||
|
export const RendererContext = createContext<RenderContext>(undefined!);
|
||||||
|
|
||||||
|
RendererContext.displayName = 'RootContext';
|
||||||
|
|
||||||
|
export const useRenderContext = () => useContext(RendererContext);
|
||||||
@ -1,30 +1,20 @@
|
|||||||
import { type Router, type RouteLocationNormalized } from '@alilc/lowcode-renderer-router';
|
import { type Router, type RouteLocationNormalized } from '@alilc/lowcode-renderer-router';
|
||||||
import { type PageConfig } from '@alilc/lowcode-renderer-core';
|
import { type Spec } from '@alilc/lowcode-shared';
|
||||||
import { createContext, useContext } from 'react';
|
import { createContext, useContext } from 'react';
|
||||||
|
|
||||||
export const RouterContext = createContext<Router>({} as any);
|
export const RouterContext = createContext<Router>(undefined!);
|
||||||
|
|
||||||
RouterContext.displayName = 'RouterContext';
|
RouterContext.displayName = 'RouterContext';
|
||||||
|
|
||||||
export const useRouter = () => useContext(RouterContext);
|
export const useRouter = () => useContext(RouterContext);
|
||||||
|
|
||||||
export const RouteLocationContext = createContext<RouteLocationNormalized>({
|
export const RouteLocationContext = createContext<RouteLocationNormalized>(undefined!);
|
||||||
name: undefined,
|
|
||||||
path: '/',
|
|
||||||
searchParams: undefined,
|
|
||||||
params: {},
|
|
||||||
hash: '',
|
|
||||||
fullPath: '/',
|
|
||||||
redirectedFrom: undefined,
|
|
||||||
matched: [],
|
|
||||||
meta: {},
|
|
||||||
});
|
|
||||||
|
|
||||||
RouteLocationContext.displayName = 'RouteLocationContext';
|
RouteLocationContext.displayName = 'RouteLocationContext';
|
||||||
|
|
||||||
export const useRouteLocation = () => useContext(RouteLocationContext);
|
export const useRouteLocation = () => useContext(RouteLocationContext);
|
||||||
|
|
||||||
export const PageConfigContext = createContext<PageConfig | undefined>(undefined);
|
export const PageConfigContext = createContext<Spec.PageConfig | undefined>(undefined);
|
||||||
|
|
||||||
PageConfigContext.displayName = 'PageConfigContext';
|
PageConfigContext.displayName = 'PageConfigContext';
|
||||||
|
|
||||||
|
|||||||
@ -1,2 +1,8 @@
|
|||||||
export * from './api/app';
|
export * from './api/app';
|
||||||
export * from './api/component';
|
export * from './api/component';
|
||||||
|
export { definePlugin } from './plugin';
|
||||||
|
export * from './context/render';
|
||||||
|
export * from './context/router';
|
||||||
|
|
||||||
|
export type { PackageLoader, CodeScope, Plugin } from '@alilc/lowcode-renderer-core';
|
||||||
|
export type { RendererExtends } from './plugin';
|
||||||
|
|||||||
50
packages/react-renderer/src/plugin.ts
Normal file
50
packages/react-renderer/src/plugin.ts
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
import { Plugin } from '@alilc/lowcode-renderer-core';
|
||||||
|
import { type ComponentType, type PropsWithChildren } from 'react';
|
||||||
|
import { type OutletProps } from './components/route';
|
||||||
|
|
||||||
|
export type WrapperComponent = ComponentType<PropsWithChildren<any>>;
|
||||||
|
|
||||||
|
export type Outlet = ComponentType<OutletProps>;
|
||||||
|
|
||||||
|
export interface RendererExtends {
|
||||||
|
addAppWrapper(appWrapper: WrapperComponent): void;
|
||||||
|
getAppWrappers(): WrapperComponent[];
|
||||||
|
|
||||||
|
addRouteWrapper(wrapper: WrapperComponent): void;
|
||||||
|
getRouteWrappers(): WrapperComponent[];
|
||||||
|
|
||||||
|
setOutlet(outlet: Outlet): void;
|
||||||
|
getOutlet(): Outlet | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const appWrappers: WrapperComponent[] = [];
|
||||||
|
const wrappers: WrapperComponent[] = [];
|
||||||
|
|
||||||
|
let outlet: Outlet | null = null;
|
||||||
|
|
||||||
|
export const rendererExtends: RendererExtends = {
|
||||||
|
addAppWrapper(appWrapper) {
|
||||||
|
if (appWrapper) appWrappers.push(appWrapper);
|
||||||
|
},
|
||||||
|
getAppWrappers() {
|
||||||
|
return appWrappers;
|
||||||
|
},
|
||||||
|
|
||||||
|
addRouteWrapper(wrapper) {
|
||||||
|
if (wrapper) wrappers.push(wrapper);
|
||||||
|
},
|
||||||
|
getRouteWrappers() {
|
||||||
|
return wrappers;
|
||||||
|
},
|
||||||
|
|
||||||
|
setOutlet(outletComponent) {
|
||||||
|
if (outletComponent) outlet = outletComponent;
|
||||||
|
},
|
||||||
|
getOutlet() {
|
||||||
|
return outlet;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export function definePlugin(plugin: Plugin<RendererExtends>) {
|
||||||
|
return plugin;
|
||||||
|
}
|
||||||
@ -1,60 +0,0 @@
|
|||||||
import {
|
|
||||||
definePlugin as definePluginFn,
|
|
||||||
type Plugin,
|
|
||||||
type PluginSetupContext,
|
|
||||||
} from '@alilc/lowcode-renderer-core';
|
|
||||||
import { type ComponentType, type PropsWithChildren } from 'react';
|
|
||||||
import { type OutletProps } from './components/outlet';
|
|
||||||
|
|
||||||
export type WrapperComponent = ComponentType<PropsWithChildren<{}>>;
|
|
||||||
|
|
||||||
export type Outlet = ComponentType<OutletProps>;
|
|
||||||
|
|
||||||
export interface ReactRenderer {
|
|
||||||
addAppWrapper(appWrapper: WrapperComponent): void;
|
|
||||||
getAppWrappers(): WrapperComponent[];
|
|
||||||
|
|
||||||
addRouteWrapper(wrapper: WrapperComponent): void;
|
|
||||||
getRouteWrappers(): WrapperComponent[];
|
|
||||||
|
|
||||||
setOutlet(outlet: Outlet): void;
|
|
||||||
getOutlet(): Outlet | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createRenderer(): ReactRenderer {
|
|
||||||
const appWrappers: WrapperComponent[] = [];
|
|
||||||
const wrappers: WrapperComponent[] = [];
|
|
||||||
|
|
||||||
let outlet: Outlet | null = null;
|
|
||||||
|
|
||||||
return {
|
|
||||||
addAppWrapper(appWrapper) {
|
|
||||||
if (appWrapper) appWrappers.push(appWrapper);
|
|
||||||
},
|
|
||||||
getAppWrappers() {
|
|
||||||
return appWrappers;
|
|
||||||
},
|
|
||||||
|
|
||||||
addRouteWrapper(wrapper) {
|
|
||||||
if (wrapper) wrappers.push(wrapper);
|
|
||||||
},
|
|
||||||
getRouteWrappers() {
|
|
||||||
return wrappers;
|
|
||||||
},
|
|
||||||
|
|
||||||
setOutlet(outletComponent) {
|
|
||||||
if (outletComponent) outlet = outletComponent;
|
|
||||||
},
|
|
||||||
getOutlet() {
|
|
||||||
return outlet;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ReactRendererSetupContext extends PluginSetupContext {
|
|
||||||
renderer: ReactRenderer;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function definePlugin(plugin: Plugin<ReactRendererSetupContext>) {
|
|
||||||
return definePluginFn(plugin);
|
|
||||||
}
|
|
||||||
@ -1,33 +0,0 @@
|
|||||||
import { parse, compile } from './parser';
|
|
||||||
import { signal, computed } from '../../signals';
|
|
||||||
|
|
||||||
export function createIntl(
|
|
||||||
messages: Record<string, Record<string, string>>,
|
|
||||||
defaultLocale: string,
|
|
||||||
) {
|
|
||||||
const allMessages = signal(messages);
|
|
||||||
const currentLocale = signal(defaultLocale);
|
|
||||||
const currentMessages = computed(() => allMessages.value[currentLocale.value]);
|
|
||||||
|
|
||||||
return {
|
|
||||||
i18n(key: string, params: Record<string, string>) {
|
|
||||||
const message = currentMessages.value[key];
|
|
||||||
const result = compile(parse(message), params).join('');
|
|
||||||
|
|
||||||
return result;
|
|
||||||
},
|
|
||||||
getLocale() {
|
|
||||||
return currentLocale.value;
|
|
||||||
},
|
|
||||||
setLocale(locale: string) {
|
|
||||||
currentLocale.value = locale;
|
|
||||||
},
|
|
||||||
|
|
||||||
addMessages(locale: string, messages: Record<string, string>) {
|
|
||||||
allMessages.value[locale] = {
|
|
||||||
...allMessages.value[locale],
|
|
||||||
...messages,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@ -1,92 +0,0 @@
|
|||||||
import { isObject } from 'lodash-es';
|
|
||||||
|
|
||||||
const RE_TOKEN_LIST_VALUE: RegExp = /^(?:\d)+/;
|
|
||||||
const RE_TOKEN_NAMED_VALUE: RegExp = /^(?:\w)+/;
|
|
||||||
|
|
||||||
type Token = {
|
|
||||||
type: 'text' | 'named' | 'list' | 'unknown';
|
|
||||||
value: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function parse(format: string): Array<Token> {
|
|
||||||
const tokens: Array<Token> = [];
|
|
||||||
let position: number = 0;
|
|
||||||
|
|
||||||
let text: string = '';
|
|
||||||
while (position < format.length) {
|
|
||||||
let char: string = format[position++];
|
|
||||||
if (char === '{') {
|
|
||||||
if (text) {
|
|
||||||
tokens.push({ type: 'text', value: text });
|
|
||||||
}
|
|
||||||
|
|
||||||
text = '';
|
|
||||||
let sub: string = '';
|
|
||||||
char = format[position++];
|
|
||||||
while (char !== undefined && char !== '}') {
|
|
||||||
sub += char;
|
|
||||||
char = format[position++];
|
|
||||||
}
|
|
||||||
const isClosed = char === '}';
|
|
||||||
|
|
||||||
const type = RE_TOKEN_LIST_VALUE.test(sub)
|
|
||||||
? 'list'
|
|
||||||
: isClosed && RE_TOKEN_NAMED_VALUE.test(sub)
|
|
||||||
? 'named'
|
|
||||||
: 'unknown';
|
|
||||||
tokens.push({ value: sub, type });
|
|
||||||
} else if (char === '%') {
|
|
||||||
// when found rails i18n syntax, skip text capture
|
|
||||||
if (format[position] !== '{') {
|
|
||||||
text += char;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
text += char;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
text && tokens.push({ type: 'text', value: text });
|
|
||||||
|
|
||||||
return tokens;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function compile(tokens: Token[], values: Record<string, any> | any[] = {}): string[] {
|
|
||||||
const compiled: string[] = [];
|
|
||||||
let index: number = 0;
|
|
||||||
|
|
||||||
const mode: string = Array.isArray(values) ? 'list' : isObject(values) ? 'named' : 'unknown';
|
|
||||||
if (mode === 'unknown') {
|
|
||||||
return compiled;
|
|
||||||
}
|
|
||||||
|
|
||||||
while (index < tokens.length) {
|
|
||||||
const token: Token = tokens[index];
|
|
||||||
switch (token.type) {
|
|
||||||
case 'text':
|
|
||||||
compiled.push(token.value);
|
|
||||||
break;
|
|
||||||
case 'list':
|
|
||||||
compiled.push((values as any[])[parseInt(token.value, 10)]);
|
|
||||||
break;
|
|
||||||
case 'named':
|
|
||||||
if (mode === 'named') {
|
|
||||||
compiled.push((values as Record<string, any>)[token.value]);
|
|
||||||
} else {
|
|
||||||
if (process.env.NODE_ENV !== 'production') {
|
|
||||||
console.warn(
|
|
||||||
`Type of token '${token.type}' and format of value '${mode}' don't match!`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'unknown':
|
|
||||||
if (process.env.NODE_ENV !== 'production') {
|
|
||||||
console.warn('Detect \'unknown\' type of token!');
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
index++;
|
|
||||||
}
|
|
||||||
|
|
||||||
return compiled;
|
|
||||||
}
|
|
||||||
@ -1,72 +0,0 @@
|
|||||||
import {
|
|
||||||
createCodeRuntime,
|
|
||||||
type PackageManager,
|
|
||||||
type AnyFunction,
|
|
||||||
type Util,
|
|
||||||
type UtilsApi,
|
|
||||||
} from '@alilc/lowcode-renderer-core';
|
|
||||||
|
|
||||||
export interface RuntimeUtils extends UtilsApi {
|
|
||||||
addUtil(utilItem: Util): void;
|
|
||||||
addUtil(name: string, fn: AnyFunction): void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createRuntimeUtils(
|
|
||||||
utilSchema: Util[],
|
|
||||||
packageManager: PackageManager,
|
|
||||||
): RuntimeUtils {
|
|
||||||
const codeRuntime = createCodeRuntime();
|
|
||||||
const utilsMap: Record<string, AnyFunction> = {};
|
|
||||||
|
|
||||||
function addUtil(item: string | Util, fn?: AnyFunction) {
|
|
||||||
if (typeof item === 'string') {
|
|
||||||
if (typeof fn === 'function') {
|
|
||||||
utilsMap[item] = fn;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const fn = parseUtil(item);
|
|
||||||
addUtil(item.name, fn);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseUtil(utilItem: Util) {
|
|
||||||
if (utilItem.type === 'function') {
|
|
||||||
const { content } = utilItem;
|
|
||||||
|
|
||||||
return codeRuntime.createFnBoundScope(content.value);
|
|
||||||
} else {
|
|
||||||
const {
|
|
||||||
content: { package: packageName, destructuring, exportName, subName },
|
|
||||||
} = utilItem;
|
|
||||||
let library: any = packageManager.getLibraryByPackageName(packageName!);
|
|
||||||
|
|
||||||
if (library) {
|
|
||||||
if (destructuring) {
|
|
||||||
const target = library[exportName!];
|
|
||||||
library = subName ? target[subName] : target;
|
|
||||||
}
|
|
||||||
|
|
||||||
return library;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
utilSchema.forEach((item) => addUtil(item));
|
|
||||||
|
|
||||||
const utilsProxy = new Proxy(Object.create(null), {
|
|
||||||
get(_, p: string) {
|
|
||||||
return utilsMap[p];
|
|
||||||
},
|
|
||||||
set() {
|
|
||||||
return false;
|
|
||||||
},
|
|
||||||
has(_, p: string) {
|
|
||||||
return Boolean(utilsMap[p]);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
addUtil,
|
|
||||||
utils: utilsProxy,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
1
packages/react-renderer/src/runtime/dataSource.ts
Normal file
1
packages/react-renderer/src/runtime/dataSource.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export const dataSourceCreator = () => ({}) as any;
|
||||||
382
packages/react-renderer/src/runtime/index.tsx
Normal file
382
packages/react-renderer/src/runtime/index.tsx
Normal file
@ -0,0 +1,382 @@
|
|||||||
|
import { processValue, someValue } from '@alilc/lowcode-renderer-core';
|
||||||
|
import {
|
||||||
|
watch,
|
||||||
|
isJSExpression,
|
||||||
|
isJSFunction,
|
||||||
|
isJSSlot,
|
||||||
|
invariant,
|
||||||
|
isLowCodeComponentSchema,
|
||||||
|
isJSI18nNode,
|
||||||
|
} from '@alilc/lowcode-shared';
|
||||||
|
import { forwardRef, useRef, useEffect, createElement, memo } from 'react';
|
||||||
|
import { appendExternalStyle } from '../utils/element';
|
||||||
|
import { reactive } from '../utils/reactive';
|
||||||
|
import { useRenderContext } from '../context/render';
|
||||||
|
import { reactiveStateCreator } from './reactiveState';
|
||||||
|
import { dataSourceCreator } from './dataSource';
|
||||||
|
import { normalizeComponentNode, type NormalizedComponentNode } from '../utils/node';
|
||||||
|
|
||||||
|
import type { PlainObject, Spec } from '@alilc/lowcode-shared';
|
||||||
|
import type {
|
||||||
|
IWidget,
|
||||||
|
RenderContext,
|
||||||
|
ICodeScope,
|
||||||
|
IComponentTreeModel,
|
||||||
|
} from '@alilc/lowcode-renderer-core';
|
||||||
|
import type {
|
||||||
|
ComponentType,
|
||||||
|
ReactInstance,
|
||||||
|
CSSProperties,
|
||||||
|
ForwardedRef,
|
||||||
|
ReactElement,
|
||||||
|
} from 'react';
|
||||||
|
|
||||||
|
export type ReactComponent = ComponentType<any>;
|
||||||
|
export type ReactWidget = IWidget<ReactComponent, ReactInstance>;
|
||||||
|
|
||||||
|
export interface ComponentOptions {
|
||||||
|
displayName?: string;
|
||||||
|
|
||||||
|
widgetCreated?(widget: ReactWidget): void;
|
||||||
|
componentRefAttached?(widget: ReactWidget, instance: ReactInstance): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LowCodeComponentProps {
|
||||||
|
id?: string;
|
||||||
|
/** CSS 类名 */
|
||||||
|
className?: string;
|
||||||
|
/** style */
|
||||||
|
style?: CSSProperties;
|
||||||
|
|
||||||
|
[key: string]: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
const lowCodeComponentsCache = new Map<string, ReactComponent>();
|
||||||
|
|
||||||
|
function getComponentByName(name: string, { packageManager }: RenderContext): ReactComponent {
|
||||||
|
const componentsRecord = packageManager.getComponentsNameRecord<ReactComponent>();
|
||||||
|
// read cache first
|
||||||
|
const result = lowCodeComponentsCache.get(name) || componentsRecord[name];
|
||||||
|
|
||||||
|
invariant(result, `${name} component not found in componentsRecord`);
|
||||||
|
|
||||||
|
if (isLowCodeComponentSchema(result)) {
|
||||||
|
const lowCodeComponent = createComponentBySchema(result.schema, {
|
||||||
|
displayName: name,
|
||||||
|
});
|
||||||
|
|
||||||
|
lowCodeComponentsCache.set(name, lowCodeComponent);
|
||||||
|
|
||||||
|
return lowCodeComponent;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createComponentBySchema(
|
||||||
|
schema: string | Spec.ComponentTreeRoot,
|
||||||
|
{ displayName = '__LowCodeComponent__', componentRefAttached }: ComponentOptions = {},
|
||||||
|
) {
|
||||||
|
const LowCodeComponent = forwardRef(function (
|
||||||
|
props: LowCodeComponentProps,
|
||||||
|
ref: ForwardedRef<any>,
|
||||||
|
) {
|
||||||
|
const renderContext = useRenderContext();
|
||||||
|
const { componentTreeModel } = renderContext;
|
||||||
|
|
||||||
|
const modelRef = useRef<IComponentTreeModel<ReactComponent, ReactInstance>>();
|
||||||
|
|
||||||
|
if (!modelRef.current) {
|
||||||
|
if (typeof schema === 'string') {
|
||||||
|
modelRef.current = componentTreeModel.createById(schema, {
|
||||||
|
stateCreator: reactiveStateCreator,
|
||||||
|
dataSourceCreator,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
modelRef.current = componentTreeModel.create(schema, {
|
||||||
|
stateCreator: reactiveStateCreator,
|
||||||
|
dataSourceCreator,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const model = modelRef.current;
|
||||||
|
|
||||||
|
const isConstructed = useRef(false);
|
||||||
|
const isMounted = useRef(false);
|
||||||
|
|
||||||
|
if (!isConstructed.current) {
|
||||||
|
model.triggerLifeCycle('constructor');
|
||||||
|
isConstructed.current = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const scopeValue = model.codeScope.value;
|
||||||
|
|
||||||
|
// init dataSource
|
||||||
|
scopeValue.reloadDataSource();
|
||||||
|
|
||||||
|
let styleEl: HTMLElement | undefined;
|
||||||
|
const cssText = model.getCssText();
|
||||||
|
if (cssText) {
|
||||||
|
appendExternalStyle(cssText).then((el) => {
|
||||||
|
styleEl = el;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// trigger lifeCycles
|
||||||
|
// componentDidMount?.();
|
||||||
|
model.triggerLifeCycle('componentDidMount');
|
||||||
|
|
||||||
|
// 当 state 改变之后调用
|
||||||
|
const unwatch = watch(scopeValue.state, (_, oldVal) => {
|
||||||
|
if (isMounted.current) {
|
||||||
|
model.triggerLifeCycle('componentDidUpdate', props, oldVal);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
isMounted.current = true;
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
// componentWillUnmount?.();
|
||||||
|
model.triggerLifeCycle('componentWillUnmount');
|
||||||
|
styleEl?.parentNode?.removeChild(styleEl);
|
||||||
|
unwatch();
|
||||||
|
isMounted.current = false;
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const elements = model.widgets.map((widget) => {
|
||||||
|
return createElementByWidget(widget, model.codeScope, renderContext, componentRefAttached);
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div id={props.id} className={props.className} style={props.style} ref={ref}>
|
||||||
|
{elements}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
LowCodeComponent.displayName = displayName;
|
||||||
|
|
||||||
|
return memo(LowCodeComponent);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Text(props: { text: string }) {
|
||||||
|
return <>{props.text}</>;
|
||||||
|
}
|
||||||
|
|
||||||
|
Text.displayName = 'Text';
|
||||||
|
|
||||||
|
function createElementByWidget(
|
||||||
|
widget: IWidget<ReactComponent, ReactInstance>,
|
||||||
|
codeScope: ICodeScope,
|
||||||
|
renderContext: RenderContext,
|
||||||
|
componentRefAttached?: ComponentOptions['componentRefAttached'],
|
||||||
|
) {
|
||||||
|
return widget.build<ReactElement | ReactElement[] | null>((ctx) => {
|
||||||
|
const { key, node, model, children } = ctx;
|
||||||
|
const boosts = renderContext.boostsManager.toExpose();
|
||||||
|
|
||||||
|
if (typeof node === 'string') {
|
||||||
|
return createElement(Text, { key, text: node });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isJSExpression(node)) {
|
||||||
|
return createElement(
|
||||||
|
reactive(Text, {
|
||||||
|
target: { text: node },
|
||||||
|
valueGetter(expr) {
|
||||||
|
return model.codeRuntime.resolve(expr, codeScope);
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
{ key },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isJSI18nNode(node)) {
|
||||||
|
return createElement(
|
||||||
|
reactive(Text, {
|
||||||
|
target: { text: node },
|
||||||
|
predicate: isJSI18nNode,
|
||||||
|
valueGetter: (node: Spec.JSI18n) => {
|
||||||
|
return boosts.intl.t({
|
||||||
|
key: node.key,
|
||||||
|
params: node.params ? model.codeRuntime.resolve(node.params, codeScope) : undefined,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
{ key },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function createElementWithProps(
|
||||||
|
node: NormalizedComponentNode,
|
||||||
|
codeScope: ICodeScope,
|
||||||
|
key: string,
|
||||||
|
): ReactElement {
|
||||||
|
const { ref, ...componentProps } = node.props;
|
||||||
|
const Component = getComponentByName(node.componentName, renderContext);
|
||||||
|
|
||||||
|
const attachRef = (ins: ReactInstance | null) => {
|
||||||
|
if (ins) {
|
||||||
|
if (ref) model.setComponentRef(ref as string, ins);
|
||||||
|
componentRefAttached?.(widget, ins);
|
||||||
|
} else {
|
||||||
|
if (ref) model.removeComponentRef(ref);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 先将 jsslot, jsFunction 对象转换
|
||||||
|
const finalProps = processValue(
|
||||||
|
componentProps,
|
||||||
|
(node) => isJSFunction(node) || isJSSlot(node),
|
||||||
|
(node: Spec.JSSlot | Spec.JSFunction) => {
|
||||||
|
if (isJSSlot(node)) {
|
||||||
|
const slot = node as Spec.JSSlot;
|
||||||
|
|
||||||
|
if (slot.value) {
|
||||||
|
const widgets = model.buildWidgets(
|
||||||
|
Array.isArray(node.value) ? node.value : [node.value],
|
||||||
|
);
|
||||||
|
|
||||||
|
if (slot.params?.length) {
|
||||||
|
return (...args: any[]) => {
|
||||||
|
const params = slot.params!.reduce((prev, cur, idx) => {
|
||||||
|
return (prev[cur] = args[idx]);
|
||||||
|
}, {} as PlainObject);
|
||||||
|
|
||||||
|
return widgets.map((n) =>
|
||||||
|
createElementByWidget(
|
||||||
|
n,
|
||||||
|
codeScope.createChild(params),
|
||||||
|
renderContext,
|
||||||
|
componentRefAttached,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return widgets.map((n) =>
|
||||||
|
createElementByWidget(n, codeScope, renderContext, componentRefAttached),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (isJSFunction(node)) {
|
||||||
|
return model.codeRuntime.resolve(node, codeScope);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const childElements = children?.map((child) =>
|
||||||
|
createElementByWidget(child, codeScope, renderContext, componentRefAttached),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (someValue(finalProps, isJSExpression)) {
|
||||||
|
const PropsWrapper = (props: PlainObject) =>
|
||||||
|
createElement(
|
||||||
|
Component,
|
||||||
|
{
|
||||||
|
...props,
|
||||||
|
key,
|
||||||
|
ref: attachRef,
|
||||||
|
},
|
||||||
|
childElements,
|
||||||
|
);
|
||||||
|
|
||||||
|
PropsWrapper.displayName = 'PropsWrapper';
|
||||||
|
|
||||||
|
return createElement(
|
||||||
|
reactive(PropsWrapper, {
|
||||||
|
target: finalProps,
|
||||||
|
valueGetter: (node) => model.codeRuntime.resolve(node, codeScope),
|
||||||
|
}),
|
||||||
|
{ key },
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return createElement(
|
||||||
|
Component,
|
||||||
|
{
|
||||||
|
...finalProps,
|
||||||
|
key,
|
||||||
|
ref: attachRef,
|
||||||
|
},
|
||||||
|
childElements,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizedNode = normalizeComponentNode(node);
|
||||||
|
const { condition, loop, loopArgs } = normalizedNode;
|
||||||
|
|
||||||
|
// condition为 Falsy 的情况下 不渲染
|
||||||
|
if (!condition) return null;
|
||||||
|
// loop 为数组且为空的情况下 不渲染
|
||||||
|
if (Array.isArray(loop) && loop.length === 0) return null;
|
||||||
|
|
||||||
|
let element: ReactElement | ReactElement[] | null = null;
|
||||||
|
|
||||||
|
if (loop) {
|
||||||
|
const genLoopElements = (loopData: any[]) => {
|
||||||
|
return loopData.map((item, idx) => {
|
||||||
|
const loopArgsItem = loopArgs[0] ?? 'item';
|
||||||
|
const loopArgsIndex = loopArgs[1] ?? 'index';
|
||||||
|
|
||||||
|
return createElementWithProps(
|
||||||
|
normalizedNode,
|
||||||
|
codeScope.createChild({
|
||||||
|
[loopArgsItem]: item,
|
||||||
|
[loopArgsIndex]: idx,
|
||||||
|
}),
|
||||||
|
`loop-${key}-${idx}`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isJSExpression(loop)) {
|
||||||
|
function Loop(props: { loop: boolean }) {
|
||||||
|
if (!Array.isArray(props.loop)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return <>{genLoopElements(props.loop)}</>;
|
||||||
|
}
|
||||||
|
Loop.displayName = 'Loop';
|
||||||
|
|
||||||
|
const ReactivedLoop = reactive(Loop, {
|
||||||
|
target: { loop },
|
||||||
|
valueGetter: (expr) => model.codeRuntime.resolve(expr, codeScope),
|
||||||
|
});
|
||||||
|
|
||||||
|
element = createElement(ReactivedLoop, { key });
|
||||||
|
} else {
|
||||||
|
element = genLoopElements(loop as any[]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isJSExpression(condition)) {
|
||||||
|
function Condition(props: any) {
|
||||||
|
if (props.condition) {
|
||||||
|
return element;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
Condition.displayName = 'Condition';
|
||||||
|
|
||||||
|
const ReactivedCondition = reactive(Condition, {
|
||||||
|
target: { condition },
|
||||||
|
valueGetter: (expr) => model.codeRuntime.resolve(expr, codeScope),
|
||||||
|
});
|
||||||
|
|
||||||
|
element = createElement(ReactivedCondition, {
|
||||||
|
key,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!element) {
|
||||||
|
element = createElementWithProps(normalizedNode, codeScope, key);
|
||||||
|
}
|
||||||
|
|
||||||
|
return element;
|
||||||
|
});
|
||||||
|
}
|
||||||
22
packages/react-renderer/src/runtime/reactiveState.ts
Normal file
22
packages/react-renderer/src/runtime/reactiveState.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import { signal, type PlainObject, type Spec } from '@alilc/lowcode-shared';
|
||||||
|
import { isPlainObject } from 'lodash-es';
|
||||||
|
|
||||||
|
export function reactiveStateCreator(initState: PlainObject): Spec.InstanceStateApi {
|
||||||
|
const proxyState = signal(initState);
|
||||||
|
|
||||||
|
return {
|
||||||
|
get state() {
|
||||||
|
return proxyState.value;
|
||||||
|
},
|
||||||
|
setState(newState) {
|
||||||
|
if (!isPlainObject(newState)) {
|
||||||
|
throw Error('newState mush be a object');
|
||||||
|
}
|
||||||
|
|
||||||
|
proxyState.value = {
|
||||||
|
...proxyState.value,
|
||||||
|
...newState,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
@ -1,122 +0,0 @@
|
|||||||
import { type PlainObject } from '@alilc/lowcode-renderer-core';
|
|
||||||
import {
|
|
||||||
ref,
|
|
||||||
computed,
|
|
||||||
effect,
|
|
||||||
ReactiveEffect,
|
|
||||||
type ComputedRef,
|
|
||||||
type Ref,
|
|
||||||
getCurrentScope,
|
|
||||||
isRef,
|
|
||||||
isReactive,
|
|
||||||
isShallow,
|
|
||||||
} from '@vue/reactivity';
|
|
||||||
import { noop, isObject, isPlainObject, isSet, isMap } from 'lodash-es';
|
|
||||||
|
|
||||||
export { ref as signal, computed, effect };
|
|
||||||
export type { Ref as Signal, ComputedRef as ComputedSignal };
|
|
||||||
|
|
||||||
const INITIAL_WATCHER_VALUE = {};
|
|
||||||
|
|
||||||
export function watch<T = any>(
|
|
||||||
source: Ref<T> | ComputedRef<T> | object,
|
|
||||||
cb: (value: any, oldValue: any) => any,
|
|
||||||
{
|
|
||||||
deep,
|
|
||||||
immediate,
|
|
||||||
}: {
|
|
||||||
deep?: boolean;
|
|
||||||
immediate?: boolean;
|
|
||||||
} = {},
|
|
||||||
) {
|
|
||||||
let getter: () => any;
|
|
||||||
let forceTrigger = false;
|
|
||||||
|
|
||||||
if (isRef(source)) {
|
|
||||||
getter = () => source.value;
|
|
||||||
forceTrigger = isShallow(source);
|
|
||||||
} else if (isReactive(source)) {
|
|
||||||
getter = () => {
|
|
||||||
return deep === true ? source : traverse(source, deep === false ? 1 : undefined);
|
|
||||||
};
|
|
||||||
forceTrigger = true;
|
|
||||||
} else {
|
|
||||||
getter = () => {};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (deep) {
|
|
||||||
const baseGetter = getter;
|
|
||||||
getter = () => traverse(baseGetter());
|
|
||||||
}
|
|
||||||
|
|
||||||
let oldValue = INITIAL_WATCHER_VALUE;
|
|
||||||
const job = () => {
|
|
||||||
if (!effect.active || !effect.dirty) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const newValue = effect.run();
|
|
||||||
|
|
||||||
if (deep || forceTrigger || !Object.is(newValue, oldValue)) {
|
|
||||||
cb(newValue, oldValue === INITIAL_WATCHER_VALUE ? undefined : oldValue);
|
|
||||||
oldValue = newValue;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const effect = new ReactiveEffect(getter, noop, job);
|
|
||||||
|
|
||||||
const scope = getCurrentScope();
|
|
||||||
const unwatch = () => {
|
|
||||||
effect.stop();
|
|
||||||
if (scope) {
|
|
||||||
const i = (scope as any).effects.indexOf(effect);
|
|
||||||
if (i > -1) {
|
|
||||||
(scope as any).effects.splice(i, 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// initial run
|
|
||||||
if (immediate) {
|
|
||||||
job();
|
|
||||||
} else {
|
|
||||||
oldValue = effect.run();
|
|
||||||
}
|
|
||||||
|
|
||||||
return unwatch;
|
|
||||||
}
|
|
||||||
|
|
||||||
function traverse(value: unknown, depth?: number, currentDepth = 0, seen?: Set<unknown>) {
|
|
||||||
if (!isObject(value)) {
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (depth && depth > 0) {
|
|
||||||
if (currentDepth >= depth) {
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
currentDepth++;
|
|
||||||
}
|
|
||||||
|
|
||||||
seen = seen || new Set();
|
|
||||||
if (seen.has(value)) {
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
seen.add(value);
|
|
||||||
if (isRef(value)) {
|
|
||||||
traverse(value.value, depth, currentDepth, seen);
|
|
||||||
} else if (Array.isArray(value)) {
|
|
||||||
for (let i = 0; i < value.length; i++) {
|
|
||||||
traverse(value[i], depth, currentDepth, seen);
|
|
||||||
}
|
|
||||||
} else if (isSet(value) || isMap(value)) {
|
|
||||||
value.forEach((v: any) => {
|
|
||||||
traverse(v, depth, currentDepth, seen);
|
|
||||||
});
|
|
||||||
} else if (isPlainObject(value)) {
|
|
||||||
for (const key in value) {
|
|
||||||
traverse((value as PlainObject)[key], depth, currentDepth, seen);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
14
packages/react-renderer/src/utils/node.ts
Normal file
14
packages/react-renderer/src/utils/node.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import { Spec } from '@alilc/lowcode-shared';
|
||||||
|
|
||||||
|
export interface NormalizedComponentNode extends Spec.ComponentNode {
|
||||||
|
loopArgs: [string, string];
|
||||||
|
props: Spec.ComponentNodeProps;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function normalizeComponentNode(node: Spec.ComponentNode): NormalizedComponentNode {
|
||||||
|
return {
|
||||||
|
...node,
|
||||||
|
loopArgs: node.loopArgs ?? ['item', 'index'],
|
||||||
|
props: node.props ?? {},
|
||||||
|
};
|
||||||
|
}
|
||||||
@ -1,15 +1,15 @@
|
|||||||
|
import { processValue } from '@alilc/lowcode-renderer-core';
|
||||||
import {
|
import {
|
||||||
processValue,
|
|
||||||
type AnyFunction,
|
type AnyFunction,
|
||||||
type PlainObject,
|
type PlainObject,
|
||||||
type JSExpression,
|
|
||||||
isJSExpression,
|
isJSExpression,
|
||||||
} from '@alilc/lowcode-renderer-core';
|
computed,
|
||||||
|
watch,
|
||||||
|
} from '@alilc/lowcode-shared';
|
||||||
import { type ComponentType, memo, forwardRef, type PropsWithChildren, createElement } from 'react';
|
import { type ComponentType, memo, forwardRef, type PropsWithChildren, createElement } from 'react';
|
||||||
import { produce } from 'immer';
|
import { produce } from 'immer';
|
||||||
import hoistNonReactStatics from 'hoist-non-react-statics';
|
import hoistNonReactStatics from 'hoist-non-react-statics';
|
||||||
import { useSyncExternalStore } from 'use-sync-external-store/shim';
|
import { useSyncExternalStore } from 'use-sync-external-store/shim';
|
||||||
import { computed, watch } from '../signals';
|
|
||||||
|
|
||||||
export interface ReactiveStore<Snapshot = PlainObject> {
|
export interface ReactiveStore<Snapshot = PlainObject> {
|
||||||
value: Snapshot;
|
value: Snapshot;
|
||||||
@ -20,7 +20,8 @@ export interface ReactiveStore<Snapshot = PlainObject> {
|
|||||||
|
|
||||||
function createReactiveStore<Snapshot = PlainObject>(
|
function createReactiveStore<Snapshot = PlainObject>(
|
||||||
target: Record<string, any>,
|
target: Record<string, any>,
|
||||||
valueGetter: (expr: JSExpression) => any,
|
predicate: (obj: any) => boolean,
|
||||||
|
valueGetter: (expr: any) => any,
|
||||||
): ReactiveStore<Snapshot> {
|
): ReactiveStore<Snapshot> {
|
||||||
let isFlushing = false;
|
let isFlushing = false;
|
||||||
let isFlushPending = false;
|
let isFlushPending = false;
|
||||||
@ -28,7 +29,7 @@ function createReactiveStore<Snapshot = PlainObject>(
|
|||||||
const cleanups: Array<() => void> = [];
|
const cleanups: Array<() => void> = [];
|
||||||
const waitPathToSetValueMap = new Map();
|
const waitPathToSetValueMap = new Map();
|
||||||
|
|
||||||
const initValue = processValue(target, isJSExpression, (node: JSExpression, paths) => {
|
const initValue = processValue(target, predicate, (node: any, paths) => {
|
||||||
const computedValue = computed(() => valueGetter(node));
|
const computedValue = computed(() => valueGetter(node));
|
||||||
const unwatch = watch(computedValue, (newValue) => {
|
const unwatch = watch(computedValue, (newValue) => {
|
||||||
waitPathToSetValueMap.set(paths, newValue);
|
waitPathToSetValueMap.set(paths, newValue);
|
||||||
@ -93,15 +94,21 @@ function createReactiveStore<Snapshot = PlainObject>(
|
|||||||
|
|
||||||
interface ReactiveOptions {
|
interface ReactiveOptions {
|
||||||
target: PlainObject;
|
target: PlainObject;
|
||||||
valueGetter: (expr: JSExpression) => any;
|
valueGetter: (expr: any) => any;
|
||||||
|
predicate?: (obj: any) => boolean;
|
||||||
forwardRef?: boolean;
|
forwardRef?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function reactive<TProps extends PlainObject = PlainObject>(
|
export function reactive<TProps extends PlainObject = PlainObject>(
|
||||||
WrappedComponent: ComponentType<TProps>,
|
WrappedComponent: ComponentType<TProps>,
|
||||||
{ target, valueGetter, forwardRef: forwardRefOption = true }: ReactiveOptions,
|
{
|
||||||
): ComponentType<PlainObject> {
|
target,
|
||||||
const store = createReactiveStore(target, valueGetter);
|
valueGetter,
|
||||||
|
predicate = isJSExpression,
|
||||||
|
forwardRef: forwardRefOption = true,
|
||||||
|
}: ReactiveOptions,
|
||||||
|
): ComponentType<PropsWithChildren<any>> {
|
||||||
|
const store = createReactiveStore<TProps>(target, predicate, valueGetter);
|
||||||
|
|
||||||
function WrapperComponent(props: any, ref: any) {
|
function WrapperComponent(props: any, ref: any) {
|
||||||
const actualProps = useSyncExternalStore(store.subscribe, store.getSnapshot);
|
const actualProps = useSyncExternalStore(store.subscribe, store.getSnapshot);
|
||||||
|
|||||||
@ -1,106 +0,0 @@
|
|||||||
import type { Project, Package, PlainObject } from '@alilc/lowcode-shared';
|
|
||||||
import { type PackageManager, createPackageManager } from '../package';
|
|
||||||
import { createPluginManager, type Plugin } from '../plugin';
|
|
||||||
import { createScope, type CodeScope } from '../code-runtime';
|
|
||||||
import { appBoosts, type AppBoosts, type AppBoostsManager } from '../boosts';
|
|
||||||
import { type AppSchema, createAppSchema } from '../schema';
|
|
||||||
|
|
||||||
export interface AppOptionsBase {
|
|
||||||
schema: Project;
|
|
||||||
packages?: Package[];
|
|
||||||
plugins?: Plugin[];
|
|
||||||
appScopeValue?: PlainObject;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AppBase {
|
|
||||||
mount: (el: HTMLElement) => void | Promise<void>;
|
|
||||||
unmount: () => void | Promise<void>;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* context for plugin or renderer
|
|
||||||
*/
|
|
||||||
export interface AppContext {
|
|
||||||
schema: AppSchema;
|
|
||||||
config: PlainObject;
|
|
||||||
appScope: CodeScope;
|
|
||||||
packageManager: PackageManager;
|
|
||||||
boosts: AppBoostsManager;
|
|
||||||
}
|
|
||||||
|
|
||||||
type AppCreator<O, T extends AppBase> = (
|
|
||||||
appContext: Omit<AppContext, 'renderer'>,
|
|
||||||
appOptions: O,
|
|
||||||
) => Promise<{ appBase: T; renderer?: any }>;
|
|
||||||
|
|
||||||
export type App<T extends AppBase = AppBase> = {
|
|
||||||
schema: Project;
|
|
||||||
config: PlainObject;
|
|
||||||
readonly boosts: AppBoosts;
|
|
||||||
|
|
||||||
use(plugin: Plugin): Promise<void>;
|
|
||||||
} & T;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 创建 createApp 的辅助函数
|
|
||||||
* @param schema
|
|
||||||
* @param options
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export function createAppFunction<O extends AppOptionsBase, T extends AppBase = AppBase>(
|
|
||||||
appCreator: AppCreator<O, T>,
|
|
||||||
): (options: O) => Promise<App<T>> {
|
|
||||||
if (typeof appCreator !== 'function') {
|
|
||||||
throw Error('The first parameter must be a function.');
|
|
||||||
}
|
|
||||||
|
|
||||||
return async (options) => {
|
|
||||||
const { schema, appScopeValue } = options;
|
|
||||||
const appSchema = createAppSchema(schema);
|
|
||||||
const appConfig = {};
|
|
||||||
const packageManager = createPackageManager();
|
|
||||||
const appScope = createScope({
|
|
||||||
...appScopeValue,
|
|
||||||
constants: schema.constants ?? {},
|
|
||||||
});
|
|
||||||
|
|
||||||
const appContext = {
|
|
||||||
schema: appSchema,
|
|
||||||
config: appConfig,
|
|
||||||
appScope,
|
|
||||||
packageManager,
|
|
||||||
boosts: appBoosts,
|
|
||||||
};
|
|
||||||
|
|
||||||
const { appBase, renderer } = await appCreator(appContext, options);
|
|
||||||
|
|
||||||
if (!('mount' in appBase) || !('unmount' in appBase)) {
|
|
||||||
throw Error('appBase 必须返回 mount 和 unmount 方法');
|
|
||||||
}
|
|
||||||
|
|
||||||
const pluginManager = createPluginManager({
|
|
||||||
...appContext,
|
|
||||||
renderer,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (options.plugins?.length) {
|
|
||||||
await Promise.all(options.plugins.map((p) => pluginManager.add(p)));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options.packages?.length) {
|
|
||||||
await packageManager.addPackages(options.packages);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Object.assign(
|
|
||||||
{
|
|
||||||
schema,
|
|
||||||
config: appConfig,
|
|
||||||
use: pluginManager.add,
|
|
||||||
get boosts() {
|
|
||||||
return appBoosts.value;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
appBase,
|
|
||||||
);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@ -1,43 +0,0 @@
|
|||||||
import type { PlainObject, InstanceStateApi } from '@alilc/lowcode-shared';
|
|
||||||
import { type CreateContainerOptions, createContainer, type Container } from '../container';
|
|
||||||
|
|
||||||
export type CreateComponentBaseOptions<T extends string> = Omit<
|
|
||||||
CreateContainerOptions<T>,
|
|
||||||
'stateCreator'
|
|
||||||
>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 创建 createComponent 的辅助函数
|
|
||||||
* createComponent = createComponentFunction(() => component)
|
|
||||||
*/
|
|
||||||
export function createComponentFunction<
|
|
||||||
ComponentT,
|
|
||||||
InstanceT,
|
|
||||||
LifeCycleNameT extends string,
|
|
||||||
O extends CreateComponentBaseOptions<LifeCycleNameT>,
|
|
||||||
>(
|
|
||||||
stateCreator: (initState: PlainObject) => InstanceStateApi,
|
|
||||||
componentCreator: (
|
|
||||||
container: Container<InstanceT, LifeCycleNameT>,
|
|
||||||
componentOptions: O,
|
|
||||||
) => ComponentT,
|
|
||||||
): (componentOptions: O) => ComponentT {
|
|
||||||
return (componentOptions) => {
|
|
||||||
const {
|
|
||||||
supCodeScope,
|
|
||||||
initScopeValue = {},
|
|
||||||
dataSourceCreator,
|
|
||||||
componentsTree,
|
|
||||||
} = componentOptions;
|
|
||||||
|
|
||||||
const container = createContainer<InstanceT, LifeCycleNameT>({
|
|
||||||
supCodeScope,
|
|
||||||
initScopeValue,
|
|
||||||
stateCreator,
|
|
||||||
dataSourceCreator,
|
|
||||||
componentsTree,
|
|
||||||
});
|
|
||||||
|
|
||||||
return componentCreator(container, componentOptions);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
27
packages/renderer-core/src/apiCreate.ts
Normal file
27
packages/renderer-core/src/apiCreate.ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import { invariant, InstantiationService } from '@alilc/lowcode-shared';
|
||||||
|
import { RendererMain } from './main';
|
||||||
|
import { type IRender, type RenderAdapter } from './parts/extension';
|
||||||
|
import type { RendererApplication, AppOptions } from './types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建 createRenderer 的辅助函数
|
||||||
|
* @param schema
|
||||||
|
* @param options
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export function createRenderer<Render = IRender>(
|
||||||
|
renderAdapter: RenderAdapter<Render>,
|
||||||
|
): (options: AppOptions) => Promise<RendererApplication<Render>> {
|
||||||
|
invariant(typeof renderAdapter === 'function', 'The first parameter must be a function.');
|
||||||
|
|
||||||
|
const instantiationService = new InstantiationService({ defaultScope: 'Singleton' });
|
||||||
|
instantiationService.bootstrapModules();
|
||||||
|
|
||||||
|
const rendererMain = instantiationService.createInstance(RendererMain);
|
||||||
|
|
||||||
|
return async (options) => {
|
||||||
|
rendererMain.initialize(options);
|
||||||
|
|
||||||
|
return rendererMain.startup<Render>(renderAdapter);
|
||||||
|
};
|
||||||
|
}
|
||||||
@ -1,42 +0,0 @@
|
|||||||
import { type AnyFunction } from './types';
|
|
||||||
import { createHookStore, type HookStore } from './utils/hook';
|
|
||||||
import { nonSetterProxy } from './utils/non-setter-proxy';
|
|
||||||
import { type RuntimeError } from './utils/error';
|
|
||||||
|
|
||||||
export interface AppBoosts {
|
|
||||||
[key: string]: any;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface RuntimeHooks {
|
|
||||||
'app:error': (error: RuntimeError) => void;
|
|
||||||
|
|
||||||
[key: PropertyKey]: AnyFunction;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AppBoostsManager {
|
|
||||||
hookStore: HookStore<RuntimeHooks>;
|
|
||||||
|
|
||||||
readonly value: AppBoosts;
|
|
||||||
add(name: PropertyKey, value: any, force?: boolean): void;
|
|
||||||
remove(name: PropertyKey): void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const boostsValue: AppBoosts = {};
|
|
||||||
const proxyBoostsValue = nonSetterProxy(boostsValue);
|
|
||||||
|
|
||||||
export const appBoosts: AppBoostsManager = {
|
|
||||||
hookStore: createHookStore(),
|
|
||||||
|
|
||||||
get value() {
|
|
||||||
return proxyBoostsValue;
|
|
||||||
},
|
|
||||||
add(name: PropertyKey, value: any, force = false) {
|
|
||||||
if ((boostsValue as any)[name] && !force) return;
|
|
||||||
(boostsValue as any)[name] = value;
|
|
||||||
},
|
|
||||||
remove(name) {
|
|
||||||
if ((boostsValue as any)[name]) {
|
|
||||||
delete (boostsValue as any)[name];
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@ -1,142 +0,0 @@
|
|||||||
import type { AnyFunction, PlainObject, JSExpression, JSFunction } from './types';
|
|
||||||
import { isJSExpression, isJSFunction } from './utils/type-guard';
|
|
||||||
import { processValue } from './utils/value';
|
|
||||||
|
|
||||||
export interface CodeRuntime {
|
|
||||||
run<T = unknown>(code: string): T | undefined;
|
|
||||||
createFnBoundScope(code: string): AnyFunction | undefined;
|
|
||||||
parseExprOrFn(value: PlainObject): any;
|
|
||||||
|
|
||||||
bindingScope(scope: CodeScope): void;
|
|
||||||
getScope(): CodeScope;
|
|
||||||
}
|
|
||||||
|
|
||||||
const SYMBOL_SIGN = '__code__scope';
|
|
||||||
|
|
||||||
export function createCodeRuntime(scopeOrValue: PlainObject = {}): CodeRuntime {
|
|
||||||
let runtimeScope = scopeOrValue[Symbol.for(SYMBOL_SIGN)]
|
|
||||||
? (scopeOrValue as CodeScope)
|
|
||||||
: createScope(scopeOrValue);
|
|
||||||
|
|
||||||
function run<T = unknown>(code: string): T | undefined {
|
|
||||||
if (!code) return undefined;
|
|
||||||
|
|
||||||
try {
|
|
||||||
return new Function(
|
|
||||||
'scope',
|
|
||||||
`"use strict";return (function(){return (${code})}).bind(scope)();`,
|
|
||||||
)(runtimeScope.value) as T;
|
|
||||||
} catch (err) {
|
|
||||||
// todo
|
|
||||||
console.error('%c eval error', code, runtimeScope.value, err);
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function createFnBoundScope(code: string) {
|
|
||||||
const fn = run(code);
|
|
||||||
if (typeof fn !== 'function') return undefined;
|
|
||||||
return fn.bind(runtimeScope.value);
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseExprOrFn(value: PlainObject) {
|
|
||||||
return processValue(
|
|
||||||
value,
|
|
||||||
(data) => {
|
|
||||||
return isJSExpression(data) || isJSFunction(data);
|
|
||||||
},
|
|
||||||
(node: JSExpression | JSFunction) => {
|
|
||||||
let v;
|
|
||||||
|
|
||||||
if (node.type === 'JSExpression') {
|
|
||||||
v = run(node.value);
|
|
||||||
} else if (node.type === 'JSFunction') {
|
|
||||||
v = createFnBoundScope(node.value);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof v === 'undefined' && (node as any).mock) {
|
|
||||||
return (node as any).mock;
|
|
||||||
}
|
|
||||||
return v;
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
run,
|
|
||||||
createFnBoundScope,
|
|
||||||
parseExprOrFn,
|
|
||||||
|
|
||||||
bindingScope(nextScope) {
|
|
||||||
runtimeScope = nextScope;
|
|
||||||
},
|
|
||||||
getScope() {
|
|
||||||
return runtimeScope;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface CodeScope<T extends PlainObject = PlainObject, K extends keyof T = keyof T> {
|
|
||||||
readonly value: T;
|
|
||||||
|
|
||||||
inject(name: K, value: T[K], force?: boolean): void;
|
|
||||||
setValue(value: T, replace?: boolean): void;
|
|
||||||
createSubScope<O extends PlainObject = PlainObject>(initValue: O): CodeScope<T & O>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createScope<T extends PlainObject = PlainObject, K extends keyof T = keyof T>(
|
|
||||||
initValue: T,
|
|
||||||
): CodeScope<T, K> {
|
|
||||||
const innerScope = { value: initValue };
|
|
||||||
|
|
||||||
const proxyValue: T = new Proxy(Object.create(null), {
|
|
||||||
set(target, p, newValue, receiver) {
|
|
||||||
return Reflect.set(target, p, newValue, receiver);
|
|
||||||
},
|
|
||||||
get(target, p, receiver) {
|
|
||||||
let valueTarget = innerScope;
|
|
||||||
|
|
||||||
while (valueTarget) {
|
|
||||||
if (Reflect.has(valueTarget.value, p)) {
|
|
||||||
return Reflect.get(valueTarget.value, p, receiver);
|
|
||||||
}
|
|
||||||
valueTarget = (valueTarget as any).__parent;
|
|
||||||
}
|
|
||||||
|
|
||||||
return Reflect.get(target, p, receiver);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const scope: CodeScope<T, K> = {
|
|
||||||
get value() {
|
|
||||||
// dev return value
|
|
||||||
return proxyValue;
|
|
||||||
},
|
|
||||||
inject(name, value, force = false): void {
|
|
||||||
if (innerScope.value[name] && !force) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
innerScope.value[name] = value;
|
|
||||||
},
|
|
||||||
setValue(value, replace = false) {
|
|
||||||
if (replace) {
|
|
||||||
innerScope.value = { ...value };
|
|
||||||
} else {
|
|
||||||
innerScope.value = Object.assign({}, innerScope.value, value);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
createSubScope<O extends PlainObject = PlainObject>(initValue: O) {
|
|
||||||
const childScope = createScope<O & T>(initValue);
|
|
||||||
|
|
||||||
(childScope as any).__raw.__parent = innerScope;
|
|
||||||
|
|
||||||
return childScope;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
Object.defineProperty(scope, Symbol.for(SYMBOL_SIGN), { get: () => true });
|
|
||||||
// development env
|
|
||||||
Object.defineProperty(scope, '__raw', { get: () => innerScope });
|
|
||||||
|
|
||||||
return scope;
|
|
||||||
}
|
|
||||||
@ -1,171 +0,0 @@
|
|||||||
import type {
|
|
||||||
InstanceApi,
|
|
||||||
PlainObject,
|
|
||||||
ComponentTree,
|
|
||||||
InstanceDataSourceApi,
|
|
||||||
InstanceStateApi,
|
|
||||||
} from '@alilc/lowcode-shared';
|
|
||||||
import { type CodeScope, type CodeRuntime, createCodeRuntime, createScope } from './code-runtime';
|
|
||||||
import { isJSFunction } from './utils/type-guard';
|
|
||||||
import { type TextWidget, type ComponentWidget, createWidget } from './widget';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 根据低代码搭建协议的容器组件描述生成的容器实例
|
|
||||||
*/
|
|
||||||
export interface Container<InstanceT = unknown, LifeCycleNameT extends string = string> {
|
|
||||||
readonly codeRuntime: CodeRuntime;
|
|
||||||
readonly instanceApiObject: InstanceApi<InstanceT>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取协议中的 css 内容
|
|
||||||
*/
|
|
||||||
getCssText(): string | undefined;
|
|
||||||
/**
|
|
||||||
* 调用生命周期方法
|
|
||||||
*/
|
|
||||||
triggerLifeCycle(lifeCycleName: LifeCycleNameT, ...args: any[]): void;
|
|
||||||
/**
|
|
||||||
* 设置 ref 对应的组件实例, 提供给 scope.$() 方式使用
|
|
||||||
*/
|
|
||||||
setInstance(ref: string, instance: InstanceT): void;
|
|
||||||
/**
|
|
||||||
* 移除 ref 对应的组件实例
|
|
||||||
*/
|
|
||||||
removeInstance(ref: string, instance?: InstanceT): void;
|
|
||||||
|
|
||||||
createWidgets<Element>(): (TextWidget<Element> | ComponentWidget<Element>)[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface CreateContainerOptions<LifeCycleNameT extends string> {
|
|
||||||
supCodeScope?: CodeScope;
|
|
||||||
initScopeValue?: PlainObject;
|
|
||||||
componentsTree: ComponentTree<LifeCycleNameT>;
|
|
||||||
stateCreator: (initalState: PlainObject) => InstanceStateApi;
|
|
||||||
// type todo
|
|
||||||
dataSourceCreator: (...args: any[]) => InstanceDataSourceApi;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createContainer<InstanceT, LifeCycleNameT extends string>(
|
|
||||||
options: CreateContainerOptions<LifeCycleNameT>,
|
|
||||||
): Container<InstanceT, LifeCycleNameT> {
|
|
||||||
const {
|
|
||||||
componentsTree,
|
|
||||||
supCodeScope,
|
|
||||||
initScopeValue = {},
|
|
||||||
stateCreator,
|
|
||||||
dataSourceCreator,
|
|
||||||
} = options;
|
|
||||||
|
|
||||||
validContainerSchema(componentsTree);
|
|
||||||
|
|
||||||
const instancesMap = new Map<string, InstanceT[]>();
|
|
||||||
const subScope = supCodeScope
|
|
||||||
? supCodeScope.createSubScope(initScopeValue)
|
|
||||||
: createScope(initScopeValue);
|
|
||||||
const codeRuntime = createCodeRuntime(subScope);
|
|
||||||
|
|
||||||
const initalState = codeRuntime.parseExprOrFn(componentsTree.state ?? {});
|
|
||||||
const initalProps = codeRuntime.parseExprOrFn(componentsTree.props ?? {});
|
|
||||||
|
|
||||||
const stateApi = stateCreator(initalState);
|
|
||||||
const dataSourceApi = dataSourceCreator(componentsTree.dataSource, stateApi);
|
|
||||||
|
|
||||||
const instanceApiObject: InstanceApi<InstanceT> = Object.assign(
|
|
||||||
{
|
|
||||||
props: initalProps,
|
|
||||||
$(ref: string) {
|
|
||||||
const insArr = instancesMap.get(ref);
|
|
||||||
if (!insArr) return undefined;
|
|
||||||
|
|
||||||
return insArr[0];
|
|
||||||
},
|
|
||||||
$$(ref: string) {
|
|
||||||
return instancesMap.get(ref) ?? [];
|
|
||||||
},
|
|
||||||
},
|
|
||||||
stateApi,
|
|
||||||
dataSourceApi,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (componentsTree.methods) {
|
|
||||||
for (const [key, fn] of Object.entries(componentsTree.methods)) {
|
|
||||||
const customMethod = codeRuntime.createFnBoundScope(fn.value);
|
|
||||||
if (customMethod) {
|
|
||||||
instanceApiObject[key] = customMethod;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const containerCodeScope = subScope.createSubScope(instanceApiObject);
|
|
||||||
|
|
||||||
codeRuntime.bindingScope(containerCodeScope);
|
|
||||||
|
|
||||||
function setInstanceByRef(ref: string, ins: InstanceT) {
|
|
||||||
let insArr = instancesMap.get(ref);
|
|
||||||
if (!insArr) {
|
|
||||||
insArr = [];
|
|
||||||
instancesMap.set(ref, insArr);
|
|
||||||
}
|
|
||||||
insArr!.push(ins);
|
|
||||||
}
|
|
||||||
|
|
||||||
function removeInstanceByRef(ref: string, ins?: InstanceT) {
|
|
||||||
const insArr = instancesMap.get(ref);
|
|
||||||
if (insArr) {
|
|
||||||
if (ins) {
|
|
||||||
const idx = insArr.indexOf(ins);
|
|
||||||
if (idx > 0) insArr.splice(idx, 1);
|
|
||||||
} else {
|
|
||||||
instancesMap.delete(ref);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function triggerLifeCycle(lifeCycleName: LifeCycleNameT, ...args: any[]) {
|
|
||||||
// keys 用来判断 lifeCycleName 存在于 schema 对象上,不获取原型链上的对象
|
|
||||||
if (
|
|
||||||
!componentsTree.lifeCycles ||
|
|
||||||
!Object.keys(componentsTree.lifeCycles).includes(lifeCycleName)
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const lifeCycleSchema = componentsTree.lifeCycles[lifeCycleName];
|
|
||||||
if (isJSFunction(lifeCycleSchema)) {
|
|
||||||
const lifeCycleFn = codeRuntime.createFnBoundScope(lifeCycleSchema.value);
|
|
||||||
if (lifeCycleFn) {
|
|
||||||
lifeCycleFn.apply(containerCodeScope.value, args);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
get codeRuntime() {
|
|
||||||
return codeRuntime;
|
|
||||||
},
|
|
||||||
get instanceApiObject() {
|
|
||||||
return containerCodeScope.value as InstanceApi<InstanceT>;
|
|
||||||
},
|
|
||||||
|
|
||||||
getCssText() {
|
|
||||||
return componentsTree.css;
|
|
||||||
},
|
|
||||||
triggerLifeCycle,
|
|
||||||
|
|
||||||
setInstance: setInstanceByRef,
|
|
||||||
removeInstance: removeInstanceByRef,
|
|
||||||
|
|
||||||
createWidgets<Element>() {
|
|
||||||
if (!componentsTree.children) return [];
|
|
||||||
return componentsTree.children.map((item) => createWidget<Element>(item));
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const CONTAINTER_NAME = ['Page', 'Block', 'Component'];
|
|
||||||
|
|
||||||
function validContainerSchema(schema: ComponentTree) {
|
|
||||||
if (!CONTAINTER_NAME.includes(schema.componentName)) {
|
|
||||||
throw Error('container schema not valid');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,17 +1,20 @@
|
|||||||
/* --------------- api -------------------- */
|
/* --------------- api -------------------- */
|
||||||
export * from './api/app';
|
export * from './apiCreate';
|
||||||
export * from './api/component';
|
export { definePackageLoader } from './parts/package';
|
||||||
export { createCodeRuntime, createScope } from './code-runtime';
|
export { Widget } from './parts/widget';
|
||||||
export { definePlugin } from './plugin';
|
|
||||||
export { createWidget } from './widget';
|
|
||||||
export { createContainer } from './container';
|
|
||||||
export { createHookStore, createEvent } from './utils/hook';
|
|
||||||
export * from './utils/type-guard';
|
|
||||||
export * from './utils/value';
|
export * from './utils/value';
|
||||||
export * from './widget';
|
|
||||||
|
|
||||||
/* --------------- types ---------------- */
|
/* --------------- types ---------------- */
|
||||||
export type { CodeRuntime, CodeScope } from './code-runtime';
|
export type * from './types';
|
||||||
export type { Plugin, PluginSetupContext } from './plugin';
|
export type {
|
||||||
export type { PackageManager, PackageLoader } from './package';
|
Plugin,
|
||||||
export type { Container, CreateContainerOptions } from './container';
|
IRender,
|
||||||
|
PluginContext,
|
||||||
|
RenderAdapter,
|
||||||
|
RenderContext,
|
||||||
|
} from './parts/extension';
|
||||||
|
export type * from './parts/code-runtime';
|
||||||
|
export type * from './parts/component-tree-model';
|
||||||
|
export type * from './parts/package';
|
||||||
|
export type * from './parts/schema';
|
||||||
|
export type * from './parts/widget';
|
||||||
|
|||||||
86
packages/renderer-core/src/main.ts
Normal file
86
packages/renderer-core/src/main.ts
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
import { Injectable } from '@alilc/lowcode-shared';
|
||||||
|
import { ICodeRuntimeService } from './parts/code-runtime';
|
||||||
|
import { IExtensionHostService, type RenderAdapter } from './parts/extension';
|
||||||
|
import { IPackageManagementService } from './parts/package';
|
||||||
|
import { IRuntimeUtilService } from './parts/runtimeUtil';
|
||||||
|
import { IRuntimeIntlService } from './parts/runtimeIntl';
|
||||||
|
import { ISchemaService } from './parts/schema';
|
||||||
|
|
||||||
|
import type { AppOptions, RendererApplication } from './types';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class RendererMain {
|
||||||
|
private mode: 'development' | 'production' = 'production';
|
||||||
|
|
||||||
|
private initOptions: AppOptions;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
@ICodeRuntimeService private codeRuntimeService: ICodeRuntimeService,
|
||||||
|
@IPackageManagementService private packageManagementService: IPackageManagementService,
|
||||||
|
@IRuntimeUtilService private runtimeUtilService: IRuntimeUtilService,
|
||||||
|
@IRuntimeIntlService private runtimeIntlService: IRuntimeIntlService,
|
||||||
|
@ISchemaService private schemaService: ISchemaService,
|
||||||
|
@IExtensionHostService private extensionHostService: IExtensionHostService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async initialize(options: AppOptions) {
|
||||||
|
const { schema, mode } = options;
|
||||||
|
|
||||||
|
if (mode) this.mode = mode;
|
||||||
|
this.initOptions = { ...options };
|
||||||
|
|
||||||
|
// valid schema
|
||||||
|
this.schemaService.initialize(schema);
|
||||||
|
|
||||||
|
// init intl
|
||||||
|
const finalLocale = options.locale ?? navigator.language;
|
||||||
|
const i18nTranslations = this.schemaService.get('i18n') ?? {};
|
||||||
|
|
||||||
|
this.runtimeIntlService.initialize(finalLocale, i18nTranslations);
|
||||||
|
}
|
||||||
|
|
||||||
|
async startup<Render>(adapter: RenderAdapter<Render>): Promise<RendererApplication<Render>> {
|
||||||
|
const render = await this.extensionHostService.runRender<Render>(adapter);
|
||||||
|
|
||||||
|
// construct application
|
||||||
|
const app = Object.freeze<RendererApplication<Render>>({
|
||||||
|
mode: this.mode,
|
||||||
|
schema: this.schemaService,
|
||||||
|
packageManager: this.packageManagementService,
|
||||||
|
...render,
|
||||||
|
|
||||||
|
use: (plugin) => {
|
||||||
|
return this.extensionHostService.registerPlugin(plugin);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// setup plugins
|
||||||
|
this.extensionHostService.initialize(app);
|
||||||
|
await this.extensionHostService.registerPlugin(this.initOptions.plugins ?? []);
|
||||||
|
|
||||||
|
// load packages
|
||||||
|
await this.packageManagementService.loadPackages(this.initOptions.packages ?? []);
|
||||||
|
|
||||||
|
// resolve component maps
|
||||||
|
const componentsMaps = this.schemaService.get('componentsMap');
|
||||||
|
this.packageManagementService.resolveComponentMaps(componentsMaps);
|
||||||
|
|
||||||
|
this.initGlobalScope();
|
||||||
|
|
||||||
|
return app;
|
||||||
|
}
|
||||||
|
|
||||||
|
private initGlobalScope() {
|
||||||
|
// init runtime uitls
|
||||||
|
const utils = this.schemaService.get('utils') ?? [];
|
||||||
|
for (const util of utils) {
|
||||||
|
this.runtimeUtilService.add(util);
|
||||||
|
}
|
||||||
|
|
||||||
|
const globalScope = this.codeRuntimeService.getScope();
|
||||||
|
globalScope.setValue({
|
||||||
|
utils: this.runtimeUtilService.toExpose(),
|
||||||
|
...this.runtimeIntlService.toExpose(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,157 +0,0 @@
|
|||||||
import { type Package, type ComponentMap, type LowCodeComponent } from './types';
|
|
||||||
|
|
||||||
const packageStore: Map<string, any> = ((window as any).__PACKAGE_STORE__ ??= new Map());
|
|
||||||
|
|
||||||
export interface PackageLoader {
|
|
||||||
name?: string;
|
|
||||||
load(packageInfo: Package, thisManager: PackageManager): Promise<any>;
|
|
||||||
active(packageInfo: Package): boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface PackageManager {
|
|
||||||
/**
|
|
||||||
* 新增资产包
|
|
||||||
* @param packages
|
|
||||||
*/
|
|
||||||
addPackages(packages: Package[]): Promise<void>;
|
|
||||||
/** 通过包名获取资产包信息 */
|
|
||||||
getPackageInfo(packageName: string): Package | undefined;
|
|
||||||
getLibraryByPackageName(packageName: string): any;
|
|
||||||
setLibraryByPackageName(packageName: string, library: any): void;
|
|
||||||
/** 新增资产包加载器 */
|
|
||||||
addPackageLoader(loader: PackageLoader): void;
|
|
||||||
|
|
||||||
/** 解析组件映射 */
|
|
||||||
resolveComponentMaps(componentMaps: ComponentMap[]): void;
|
|
||||||
/** 获取组件映射对象,key = componentName value = component */
|
|
||||||
getComponentsNameRecord<C = unknown>(
|
|
||||||
componentMaps?: ComponentMap[],
|
|
||||||
): Record<string, C | LowCodeComponent>;
|
|
||||||
/** 通过组件名获取对应的组件 */
|
|
||||||
getComponent<C = unknown>(componentName: string): C | LowCodeComponent | undefined;
|
|
||||||
/** 注册组件 */
|
|
||||||
registerComponentByName(componentName: string, Component: unknown): void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createPackageManager(): PackageManager {
|
|
||||||
const packageLoaders: PackageLoader[] = [];
|
|
||||||
const componentsRecord: Record<string, any> = {};
|
|
||||||
|
|
||||||
const packagesRef: Package[] = [];
|
|
||||||
|
|
||||||
async function addPackages(packages: Package[]) {
|
|
||||||
for (const item of packages) {
|
|
||||||
if (!item.package && !item.id) continue;
|
|
||||||
|
|
||||||
const newId = item.package ?? item.id!;
|
|
||||||
const isExist = packagesRef.some((_) => {
|
|
||||||
const itemId = _.package ?? _.id;
|
|
||||||
return itemId === newId;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!isExist) {
|
|
||||||
packagesRef.push(item);
|
|
||||||
|
|
||||||
if (!packageStore.has(newId)) {
|
|
||||||
const loader = packageLoaders.find((loader) => loader.active(item));
|
|
||||||
if (!loader) continue;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const result = await loader.load(item, manager);
|
|
||||||
if (result) packageStore.set(newId, result);
|
|
||||||
} catch (e) {
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getPackageInfo(packageName: string) {
|
|
||||||
return packagesRef.find((p) => p.package === packageName);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getLibraryByPackageName(packageName: string) {
|
|
||||||
const packageInfo = getPackageInfo(packageName);
|
|
||||||
|
|
||||||
if (packageInfo) {
|
|
||||||
return packageStore.get(packageInfo.package ?? packageInfo.id!);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function setLibraryByPackageName(packageName: string, library: any) {
|
|
||||||
packageStore.set(packageName, library);
|
|
||||||
}
|
|
||||||
|
|
||||||
function resolveComponentMaps(componentMaps: ComponentMap[]) {
|
|
||||||
for (const map of componentMaps) {
|
|
||||||
if (map.devMode === 'lowCode') {
|
|
||||||
const packageInfo = packagesRef.find((_) => {
|
|
||||||
return _.id === (map as LowCodeComponent).id;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (packageInfo) {
|
|
||||||
componentsRecord[map.componentName] = packageInfo;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (packageStore.has(map.package!)) {
|
|
||||||
const library = packageStore.get(map.package!);
|
|
||||||
// export { exportName } from xxx exportName === global.libraryName.exportName
|
|
||||||
// export exportName from xxx exportName === global.libraryName.default || global.libraryName
|
|
||||||
// export { exportName as componentName } from package
|
|
||||||
// if exportName == null exportName === componentName;
|
|
||||||
// const componentName = exportName.subName, if exportName empty subName donot use
|
|
||||||
const paths = map.exportName && map.subName ? map.subName.split('.') : [];
|
|
||||||
const exportName = map.exportName ?? map.componentName;
|
|
||||||
|
|
||||||
if (map.destructuring) {
|
|
||||||
paths.unshift(exportName);
|
|
||||||
}
|
|
||||||
|
|
||||||
let result = library;
|
|
||||||
for (const path of paths) {
|
|
||||||
result = result[path] || result;
|
|
||||||
}
|
|
||||||
|
|
||||||
const recordName = map.componentName ?? map.exportName;
|
|
||||||
componentsRecord[recordName] = result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getComponentsNameRecord(componentMaps?: ComponentMap[]) {
|
|
||||||
if (componentMaps) {
|
|
||||||
resolveComponentMaps(componentMaps);
|
|
||||||
}
|
|
||||||
|
|
||||||
return { ...componentsRecord };
|
|
||||||
}
|
|
||||||
|
|
||||||
function getComponent(componentName: string) {
|
|
||||||
return componentsRecord[componentName];
|
|
||||||
}
|
|
||||||
|
|
||||||
function registerComponentByName(componentName: string, Component: unknown) {
|
|
||||||
componentsRecord[componentName] = Component;
|
|
||||||
}
|
|
||||||
|
|
||||||
const manager: PackageManager = {
|
|
||||||
addPackages,
|
|
||||||
getPackageInfo,
|
|
||||||
getLibraryByPackageName,
|
|
||||||
setLibraryByPackageName,
|
|
||||||
addPackageLoader(loader) {
|
|
||||||
if (!loader.name || !packageLoaders.some((_) => _.name === loader.name)) {
|
|
||||||
packageLoaders.push(loader);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
resolveComponentMaps,
|
|
||||||
getComponentsNameRecord,
|
|
||||||
getComponent,
|
|
||||||
registerComponentByName,
|
|
||||||
};
|
|
||||||
|
|
||||||
return manager;
|
|
||||||
}
|
|
||||||
@ -0,0 +1,86 @@
|
|||||||
|
import {
|
||||||
|
type PlainObject,
|
||||||
|
type Spec,
|
||||||
|
isJSFunction,
|
||||||
|
isJSExpression,
|
||||||
|
createCallback,
|
||||||
|
EventDisposable,
|
||||||
|
createDecorator,
|
||||||
|
Provide,
|
||||||
|
} from '@alilc/lowcode-shared';
|
||||||
|
import { type ICodeScope, CodeScope } from './codeScope';
|
||||||
|
import { processValue } from '../../utils/value';
|
||||||
|
|
||||||
|
export interface ICodeRuntimeService {
|
||||||
|
getScope(): ICodeScope;
|
||||||
|
|
||||||
|
run<R = unknown>(code: string, scope?: ICodeScope): R | undefined;
|
||||||
|
|
||||||
|
resolve(value: PlainObject, scope?: ICodeScope): any;
|
||||||
|
|
||||||
|
beforeRun(fn: (code: string) => string): EventDisposable;
|
||||||
|
|
||||||
|
createChildScope(value: PlainObject): ICodeScope;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ICodeRuntimeService = createDecorator<ICodeRuntimeService>('codeRuntimeService');
|
||||||
|
|
||||||
|
@Provide(ICodeRuntimeService)
|
||||||
|
export class CodeRuntimeService implements ICodeRuntimeService {
|
||||||
|
private codeScope: ICodeScope = new CodeScope({});
|
||||||
|
|
||||||
|
private callbacks = createCallback<(code: string) => string>();
|
||||||
|
|
||||||
|
getScope() {
|
||||||
|
return this.codeScope;
|
||||||
|
}
|
||||||
|
|
||||||
|
run<R = unknown>(code: string, scope: ICodeScope = this.codeScope): R | undefined {
|
||||||
|
if (!code) return undefined;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const cbs = this.callbacks.list();
|
||||||
|
const finalCode = cbs.reduce((code, cb) => cb(code), code);
|
||||||
|
|
||||||
|
let result = new Function(
|
||||||
|
'scope',
|
||||||
|
`"use strict";return (function(){return (${finalCode})}).bind(scope)();`,
|
||||||
|
)(scope.value);
|
||||||
|
|
||||||
|
if (typeof result === 'function') {
|
||||||
|
result = result.bind(scope.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result as R;
|
||||||
|
} catch (err) {
|
||||||
|
// todo replace logger
|
||||||
|
console.error('%c eval error', code, scope.value, err);
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve(value: PlainObject, scope: ICodeScope = this.codeScope) {
|
||||||
|
return processValue(
|
||||||
|
value,
|
||||||
|
(data) => {
|
||||||
|
return isJSExpression(data) || isJSFunction(data);
|
||||||
|
},
|
||||||
|
(node: Spec.JSExpression | Spec.JSFunction) => {
|
||||||
|
const v = this.run(node.value, scope);
|
||||||
|
|
||||||
|
if (typeof v === 'undefined' && (node as any).mock) {
|
||||||
|
return (node as any).mock;
|
||||||
|
}
|
||||||
|
return v;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeRun(fn: (code: string) => string): EventDisposable {
|
||||||
|
return this.callbacks.add(fn);
|
||||||
|
}
|
||||||
|
|
||||||
|
createChildScope(value: PlainObject): ICodeScope {
|
||||||
|
return this.codeScope.createChild(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
74
packages/renderer-core/src/parts/code-runtime/codeScope.ts
Normal file
74
packages/renderer-core/src/parts/code-runtime/codeScope.ts
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
import { type PlainObject } from '@alilc/lowcode-shared';
|
||||||
|
|
||||||
|
export interface ICodeScope {
|
||||||
|
readonly value: PlainObject;
|
||||||
|
|
||||||
|
inject(name: string, value: any, force?: boolean): void;
|
||||||
|
setValue(value: PlainObject, replace?: boolean): void;
|
||||||
|
createChild(initValue: PlainObject): ICodeScope;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 双链表实现父域值的获取
|
||||||
|
*/
|
||||||
|
interface IScopeNode {
|
||||||
|
prev?: IScopeNode;
|
||||||
|
current: PlainObject;
|
||||||
|
next?: IScopeNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class CodeScope implements ICodeScope {
|
||||||
|
__node: IScopeNode;
|
||||||
|
|
||||||
|
private proxyValue: PlainObject;
|
||||||
|
|
||||||
|
constructor(initValue: PlainObject) {
|
||||||
|
this.__node = {
|
||||||
|
current: initValue,
|
||||||
|
};
|
||||||
|
|
||||||
|
this.proxyValue = new Proxy(Object.create(null) as PlainObject, {
|
||||||
|
set(target, p, newValue, receiver) {
|
||||||
|
return Reflect.set(target, p, newValue, receiver);
|
||||||
|
},
|
||||||
|
get: (target, p, receiver) => {
|
||||||
|
let valueTarget: IScopeNode | undefined = this.__node;
|
||||||
|
|
||||||
|
while (valueTarget) {
|
||||||
|
if (Reflect.has(valueTarget.current, p)) {
|
||||||
|
return Reflect.get(valueTarget.current, p, receiver);
|
||||||
|
}
|
||||||
|
valueTarget = this.__node.prev;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Reflect.get(target, p, receiver);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
get value() {
|
||||||
|
return this.proxyValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
inject(name: string, value: any, force = false): void {
|
||||||
|
if (this.__node.current[name] && !force) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.__node.current.value[name] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
setValue(value: PlainObject, replace = false) {
|
||||||
|
if (replace) {
|
||||||
|
this.__node.current = { ...value };
|
||||||
|
} else {
|
||||||
|
this.__node.current = Object.assign({}, this.__node.current, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
createChild(initValue: PlainObject): ICodeScope {
|
||||||
|
const subScope = new CodeScope(initValue);
|
||||||
|
subScope.__node.prev = this.__node;
|
||||||
|
|
||||||
|
return subScope as ICodeScope;
|
||||||
|
}
|
||||||
|
}
|
||||||
2
packages/renderer-core/src/parts/code-runtime/index.ts
Normal file
2
packages/renderer-core/src/parts/code-runtime/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from './codeScope';
|
||||||
|
export * from './codeRuntimeService';
|
||||||
@ -0,0 +1,2 @@
|
|||||||
|
export * from './treeModel';
|
||||||
|
export * from './treeModelService';
|
||||||
@ -0,0 +1,183 @@
|
|||||||
|
import {
|
||||||
|
type Spec,
|
||||||
|
type PlainObject,
|
||||||
|
isJSFunction,
|
||||||
|
isComponentNode,
|
||||||
|
invariant,
|
||||||
|
type AnyFunction,
|
||||||
|
} from '@alilc/lowcode-shared';
|
||||||
|
import { type ICodeScope, type ICodeRuntimeService } from '../code-runtime';
|
||||||
|
import { IWidget, Widget } from '../widget';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据低代码搭建协议的容器组件描述生成的容器模型
|
||||||
|
*/
|
||||||
|
export interface IComponentTreeModel<Component, ComponentInstance = unknown> {
|
||||||
|
readonly codeScope: ICodeScope;
|
||||||
|
|
||||||
|
readonly codeRuntime: ICodeRuntimeService;
|
||||||
|
|
||||||
|
readonly widgets: IWidget<Component, ComponentInstance>[];
|
||||||
|
/**
|
||||||
|
* 获取协议中的 css 内容
|
||||||
|
*/
|
||||||
|
getCssText(): string | undefined;
|
||||||
|
/**
|
||||||
|
* 调用生命周期方法
|
||||||
|
*/
|
||||||
|
triggerLifeCycle(lifeCycleName: Spec.ComponentLifeCycle, ...args: any[]): void;
|
||||||
|
/**
|
||||||
|
* 设置 ref 对应的组件实例, 提供给 scope.$() 方式使用
|
||||||
|
*/
|
||||||
|
setComponentRef(ref: string, component: ComponentInstance): void;
|
||||||
|
/**
|
||||||
|
* 移除 ref 对应的组件实例
|
||||||
|
*/
|
||||||
|
removeComponentRef(ref: string, component?: ComponentInstance): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据 compoonentsTree.children 构建 widget 渲染对象
|
||||||
|
*/
|
||||||
|
buildWidgets(nodes: Spec.NodeType[]): IWidget<Component, ComponentInstance>[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ModelScopeStateCreator = (initalState: PlainObject) => Spec.InstanceStateApi;
|
||||||
|
export type ModelScopeDataSourceCreator = (...args: any[]) => Spec.InstanceDataSourceApi;
|
||||||
|
|
||||||
|
export interface ComponentTreeModelOptions {
|
||||||
|
stateCreator: ModelScopeStateCreator;
|
||||||
|
dataSourceCreator: ModelScopeDataSourceCreator;
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultDataSourceSchema: Spec.ComponentDataSource = {
|
||||||
|
list: [],
|
||||||
|
dataHandler: {
|
||||||
|
type: 'JSFunction',
|
||||||
|
value: '() => {}',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export class ComponentTreeModel<Component, ComponentInstance = unknown>
|
||||||
|
implements IComponentTreeModel<Component, ComponentInstance>
|
||||||
|
{
|
||||||
|
private instanceMap = new Map<string, ComponentInstance[]>();
|
||||||
|
|
||||||
|
public codeScope: ICodeScope;
|
||||||
|
|
||||||
|
public widgets: IWidget<Component>[] = [];
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
public componentsTree: Spec.ComponentTree,
|
||||||
|
public codeRuntime: ICodeRuntimeService,
|
||||||
|
options: ComponentTreeModelOptions,
|
||||||
|
) {
|
||||||
|
invariant(componentsTree, 'componentsTree must to provide', 'ComponentTreeModel');
|
||||||
|
|
||||||
|
this.initModelScope(options.stateCreator, options.dataSourceCreator);
|
||||||
|
|
||||||
|
if (componentsTree.children) {
|
||||||
|
this.widgets = this.buildWidgets(componentsTree.children);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private initModelScope(
|
||||||
|
stateCreator: ModelScopeStateCreator,
|
||||||
|
dataSourceCreator: ModelScopeDataSourceCreator,
|
||||||
|
) {
|
||||||
|
const {
|
||||||
|
state = {},
|
||||||
|
props = {},
|
||||||
|
dataSource = defaultDataSourceSchema,
|
||||||
|
methods = {},
|
||||||
|
} = this.componentsTree;
|
||||||
|
|
||||||
|
this.codeScope = this.codeRuntime.createChildScope({});
|
||||||
|
|
||||||
|
const initalState = this.codeRuntime.resolve(state, this.codeScope);
|
||||||
|
const initalProps = this.codeRuntime.resolve(props, this.codeScope);
|
||||||
|
|
||||||
|
const stateApi = stateCreator(initalState);
|
||||||
|
const dataSourceApi = dataSourceCreator(dataSource, stateApi);
|
||||||
|
|
||||||
|
this.codeScope.setValue(
|
||||||
|
Object.assign(
|
||||||
|
{
|
||||||
|
props: initalProps,
|
||||||
|
$: (ref: string) => {
|
||||||
|
const insArr = this.instanceMap.get(ref);
|
||||||
|
if (!insArr) return undefined;
|
||||||
|
return insArr[0];
|
||||||
|
},
|
||||||
|
$$: (ref: string) => {
|
||||||
|
return this.instanceMap.get(ref) ?? [];
|
||||||
|
},
|
||||||
|
},
|
||||||
|
stateApi,
|
||||||
|
dataSourceApi,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const [key, fn] of Object.entries(methods)) {
|
||||||
|
const customMethod = this.codeRuntime.run(fn.value, this.codeScope);
|
||||||
|
if (customMethod) {
|
||||||
|
this.codeScope.inject(key, customMethod);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getCssText(): string | undefined {
|
||||||
|
return this.componentsTree.css;
|
||||||
|
}
|
||||||
|
|
||||||
|
triggerLifeCycle(lifeCycleName: Spec.ComponentLifeCycle, ...args: any[]) {
|
||||||
|
// keys 用来判断 lifeCycleName 存在于 schema 对象上,不获取原型链上的对象
|
||||||
|
if (
|
||||||
|
!this.componentsTree.lifeCycles ||
|
||||||
|
!Object.keys(this.componentsTree.lifeCycles).includes(lifeCycleName)
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const lifeCycleSchema = this.componentsTree.lifeCycles[lifeCycleName];
|
||||||
|
|
||||||
|
if (isJSFunction(lifeCycleSchema)) {
|
||||||
|
const lifeCycleFn = this.codeRuntime.run<AnyFunction>(lifeCycleSchema.value, this.codeScope);
|
||||||
|
if (lifeCycleFn) {
|
||||||
|
lifeCycleFn.apply(this.codeScope.value, args);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setComponentRef(ref: string, ins: ComponentInstance) {
|
||||||
|
let insArr = this.instanceMap.get(ref);
|
||||||
|
if (!insArr) {
|
||||||
|
insArr = [];
|
||||||
|
this.instanceMap.set(ref, insArr);
|
||||||
|
}
|
||||||
|
insArr!.push(ins);
|
||||||
|
}
|
||||||
|
|
||||||
|
removeComponentRef(ref: string, ins?: ComponentInstance) {
|
||||||
|
const insArr = this.instanceMap.get(ref);
|
||||||
|
if (insArr) {
|
||||||
|
if (ins) {
|
||||||
|
const idx = insArr.indexOf(ins);
|
||||||
|
if (idx > 0) insArr.splice(idx, 1);
|
||||||
|
} else {
|
||||||
|
this.instanceMap.delete(ref);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
buildWidgets(nodes: Spec.NodeType[]): IWidget<Component>[] {
|
||||||
|
return nodes.map((node) => {
|
||||||
|
const widget = new Widget<Component, ComponentInstance>(node, this);
|
||||||
|
|
||||||
|
if (isComponentNode(node) && node.children?.length) {
|
||||||
|
widget.children = this.buildWidgets(node.children);
|
||||||
|
}
|
||||||
|
|
||||||
|
return widget;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,51 @@
|
|||||||
|
import { createDecorator, Provide, invariant, type Spec } from '@alilc/lowcode-shared';
|
||||||
|
import { ICodeRuntimeService } from '../code-runtime';
|
||||||
|
import {
|
||||||
|
type IComponentTreeModel,
|
||||||
|
ComponentTreeModel,
|
||||||
|
type ComponentTreeModelOptions,
|
||||||
|
} from './treeModel';
|
||||||
|
import { ISchemaService } from '../schema';
|
||||||
|
|
||||||
|
export interface IComponentTreeModelService {
|
||||||
|
create<Component>(
|
||||||
|
componentsTree: Spec.ComponentTree,
|
||||||
|
options: ComponentTreeModelOptions,
|
||||||
|
): IComponentTreeModel<Component>;
|
||||||
|
|
||||||
|
createById<Component>(
|
||||||
|
id: string,
|
||||||
|
options: ComponentTreeModelOptions,
|
||||||
|
): IComponentTreeModel<Component>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const IComponentTreeModelService = createDecorator<IComponentTreeModelService>(
|
||||||
|
'componentTreeModelService',
|
||||||
|
);
|
||||||
|
|
||||||
|
@Provide(IComponentTreeModelService)
|
||||||
|
export class ComponentTreeModelService implements IComponentTreeModelService {
|
||||||
|
constructor(
|
||||||
|
@ISchemaService private schemaService: ISchemaService,
|
||||||
|
@ICodeRuntimeService private codeRuntimeService: ICodeRuntimeService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
create<Component>(
|
||||||
|
componentsTree: Spec.ComponentTree,
|
||||||
|
options: ComponentTreeModelOptions,
|
||||||
|
): IComponentTreeModel<Component> {
|
||||||
|
return new ComponentTreeModel(componentsTree, this.codeRuntimeService, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
createById<Component>(
|
||||||
|
id: string,
|
||||||
|
options: ComponentTreeModelOptions,
|
||||||
|
): IComponentTreeModel<Component> {
|
||||||
|
const componentsTrees = this.schemaService.get('componentsTree');
|
||||||
|
const componentsTree = componentsTrees.find((item) => item.id === id);
|
||||||
|
|
||||||
|
invariant(componentsTree, 'componentsTree not found');
|
||||||
|
|
||||||
|
return new ComponentTreeModel(componentsTree, this.codeRuntimeService, options);
|
||||||
|
}
|
||||||
|
}
|
||||||
87
packages/renderer-core/src/parts/extension/boosts.ts
Normal file
87
packages/renderer-core/src/parts/extension/boosts.ts
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
import { createDecorator, Provide, type PlainObject } from '@alilc/lowcode-shared';
|
||||||
|
import { isObject } from 'lodash-es';
|
||||||
|
import { ICodeRuntimeService } from '../code-runtime';
|
||||||
|
import { IRuntimeUtilService } from '../runtimeUtil';
|
||||||
|
import { IRuntimeIntlService } from '../runtimeIntl';
|
||||||
|
|
||||||
|
export type IBoosts<Extends> = IBoostsApi & Extends;
|
||||||
|
|
||||||
|
export interface IBoostsApi {
|
||||||
|
readonly codeRuntime: ICodeRuntimeService;
|
||||||
|
|
||||||
|
readonly intl: Pick<IRuntimeIntlService, 't' | 'setLocale' | 'getLocale' | 'addTranslations'>;
|
||||||
|
|
||||||
|
readonly util: Pick<IRuntimeUtilService, 'add' | 'remove'>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 提供了与运行时交互的接口
|
||||||
|
*/
|
||||||
|
export interface IBoostsService {
|
||||||
|
extend(name: string, value: any, force?: boolean): void;
|
||||||
|
extend(value: PlainObject, force?: boolean): void;
|
||||||
|
|
||||||
|
toExpose<Extends>(): IBoosts<Extends>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const IBoostsService = createDecorator<IBoostsService>('boostsService');
|
||||||
|
|
||||||
|
@Provide(IBoostsService)
|
||||||
|
export class BoostsService implements IBoostsService {
|
||||||
|
private builtInApis: IBoostsApi;
|
||||||
|
|
||||||
|
private extendsValue: PlainObject = {};
|
||||||
|
|
||||||
|
private _expose: any;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
@ICodeRuntimeService private codeRuntimeService: ICodeRuntimeService,
|
||||||
|
@IRuntimeIntlService private runtimeIntlService: IRuntimeIntlService,
|
||||||
|
@IRuntimeUtilService private runtimeUtilService: IRuntimeUtilService,
|
||||||
|
) {
|
||||||
|
this.builtInApis = {
|
||||||
|
codeRuntime: this.codeRuntimeService,
|
||||||
|
intl: this.runtimeIntlService,
|
||||||
|
util: this.runtimeUtilService,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
extend(name: string, value: any, force?: boolean | undefined): void;
|
||||||
|
extend(value: PlainObject, force?: boolean | undefined): void;
|
||||||
|
extend(name: string | PlainObject, value?: any, force?: boolean | undefined): void {
|
||||||
|
if (typeof name === 'string') {
|
||||||
|
if (force) {
|
||||||
|
this.extendsValue[name] = value;
|
||||||
|
} else {
|
||||||
|
if (!this.extendsValue[name]) {
|
||||||
|
this.extendsValue[name] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (isObject(name)) {
|
||||||
|
Object.keys(name).forEach((key) => {
|
||||||
|
this.extend(key, name[key], value);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
toExpose<Extends>(): IBoosts<Extends> {
|
||||||
|
if (!this._expose) {
|
||||||
|
this._expose = new Proxy(Object.create(null), {
|
||||||
|
get: (_, p, receiver) => {
|
||||||
|
return (
|
||||||
|
Reflect.get(this.builtInApis, p, receiver) ||
|
||||||
|
Reflect.get(this.extendsValue, p, receiver)
|
||||||
|
);
|
||||||
|
},
|
||||||
|
set() {
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
has: (_, p) => {
|
||||||
|
return Reflect.has(this.builtInApis, p) || Reflect.has(this.extendsValue, p);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return this._expose;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,125 @@
|
|||||||
|
import {
|
||||||
|
invariant,
|
||||||
|
createDecorator,
|
||||||
|
Provide,
|
||||||
|
EventEmitter,
|
||||||
|
KeyValueStore,
|
||||||
|
} from '@alilc/lowcode-shared';
|
||||||
|
import { type Plugin } from './plugin';
|
||||||
|
import { IBoostsService } from './boosts';
|
||||||
|
import { IPackageManagementService } from '../package';
|
||||||
|
import { ISchemaService } from '../schema';
|
||||||
|
import { type RenderAdapter } from './render';
|
||||||
|
import { IComponentTreeModelService } from '../component-tree-model';
|
||||||
|
import type { RendererApplication } from '../../types';
|
||||||
|
|
||||||
|
interface IPluginRuntime extends Plugin {
|
||||||
|
status: 'setup' | 'ready';
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IExtensionHostService {
|
||||||
|
initialize(app: RendererApplication): void;
|
||||||
|
|
||||||
|
/* ========= plugin ============= */
|
||||||
|
registerPlugin(plugin: Plugin | Plugin[]): Promise<void>;
|
||||||
|
|
||||||
|
getPlugin(name: string): Plugin | undefined;
|
||||||
|
|
||||||
|
/* =========== render =============== */
|
||||||
|
runRender<Render>(adapter: RenderAdapter<Render>): Promise<Render>;
|
||||||
|
|
||||||
|
dispose(): Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const IExtensionHostService =
|
||||||
|
createDecorator<IExtensionHostService>('pluginManagementService');
|
||||||
|
|
||||||
|
@Provide(IExtensionHostService)
|
||||||
|
export class ExtensionHostService implements IExtensionHostService {
|
||||||
|
private pluginRuntimes: IPluginRuntime[] = [];
|
||||||
|
|
||||||
|
private app: RendererApplication;
|
||||||
|
|
||||||
|
private eventEmitter = new EventEmitter();
|
||||||
|
|
||||||
|
private globalState = new KeyValueStore();
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
@IPackageManagementService private packageManagementService: IPackageManagementService,
|
||||||
|
@IBoostsService private boostsService: IBoostsService,
|
||||||
|
@ISchemaService private schemaService: ISchemaService,
|
||||||
|
@IComponentTreeModelService private componentTreeModelService: IComponentTreeModelService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
initialize(app: RendererApplication) {
|
||||||
|
this.app = app;
|
||||||
|
}
|
||||||
|
|
||||||
|
async registerPlugin(plugins: Plugin | Plugin[]) {
|
||||||
|
plugins = Array.isArray(plugins) ? plugins : [plugins];
|
||||||
|
|
||||||
|
for (const plugin of plugins) {
|
||||||
|
if (this.pluginRuntimes.find((item) => item.name === plugin.name)) {
|
||||||
|
console.warn(`${plugin.name} 插件已注册`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.doSetupPlugin(plugin);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getPlugin(name: string): Plugin | undefined {
|
||||||
|
return this.pluginRuntimes.find((item) => item.name === name);
|
||||||
|
}
|
||||||
|
|
||||||
|
async runRender<Render>(adapter: RenderAdapter<Render>): Promise<Render> {
|
||||||
|
invariant(adapter, 'render adapter not settled', 'ExtensionHostService');
|
||||||
|
|
||||||
|
return adapter({
|
||||||
|
schema: this.schemaService,
|
||||||
|
packageManager: this.packageManagementService,
|
||||||
|
boostsManager: this.boostsService,
|
||||||
|
componentTreeModel: this.componentTreeModelService,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async dispose(): Promise<void> {
|
||||||
|
for (const plugin of this.pluginRuntimes) {
|
||||||
|
await plugin.destory?.();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async doSetupPlugin(plugin: Plugin) {
|
||||||
|
const pluginRuntime = plugin as IPluginRuntime;
|
||||||
|
|
||||||
|
if (!this.pluginRuntimes.some((item) => item.name !== pluginRuntime.name)) {
|
||||||
|
this.pluginRuntimes.push({
|
||||||
|
...pluginRuntime,
|
||||||
|
status: 'ready',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const isSetup = (name: string) => {
|
||||||
|
const setupPlugins = this.pluginRuntimes.filter((item) => item.status === 'setup');
|
||||||
|
return setupPlugins.some((p) => p.name === name);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (pluginRuntime.dependsOn?.some((dep) => !isSetup(dep))) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await pluginRuntime.setup(this.app, {
|
||||||
|
eventEmitter: this.eventEmitter,
|
||||||
|
globalState: this.globalState,
|
||||||
|
boosts: this.boostsService.toExpose(),
|
||||||
|
});
|
||||||
|
pluginRuntime.status = 'setup';
|
||||||
|
|
||||||
|
// 遍历未安装的插件 寻找 dependsOn 的插件已安装完的插件进行安装
|
||||||
|
const readyPlugins = this.pluginRuntimes.filter((item) => item.status === 'ready');
|
||||||
|
const readyPlugin = readyPlugins.find((item) => item.dependsOn?.every((dep) => isSetup(dep)));
|
||||||
|
if (readyPlugin) {
|
||||||
|
await this.doSetupPlugin(readyPlugin);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
4
packages/renderer-core/src/parts/extension/index.ts
Normal file
4
packages/renderer-core/src/parts/extension/index.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export * from './extensionHostService';
|
||||||
|
export * from './plugin';
|
||||||
|
export * from './boosts';
|
||||||
|
export * from './render';
|
||||||
20
packages/renderer-core/src/parts/extension/plugin.ts
Normal file
20
packages/renderer-core/src/parts/extension/plugin.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import { EventEmitter, KeyValueStore } from '@alilc/lowcode-shared';
|
||||||
|
import { type RendererApplication } from '../../types';
|
||||||
|
import { IBoosts } from './boosts';
|
||||||
|
|
||||||
|
export interface PluginContext<BoostsExtends = object> {
|
||||||
|
eventEmitter: EventEmitter;
|
||||||
|
globalState: KeyValueStore;
|
||||||
|
|
||||||
|
boosts: IBoosts<BoostsExtends>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Plugin<BoostsExtends = object> {
|
||||||
|
/**
|
||||||
|
* 插件的 name 作为唯一标识,并不可重复。
|
||||||
|
*/
|
||||||
|
name: string;
|
||||||
|
setup(app: RendererApplication, context: PluginContext<BoostsExtends>): void | Promise<void>;
|
||||||
|
destory?(): void | Promise<void>;
|
||||||
|
dependsOn?: string[];
|
||||||
|
}
|
||||||
23
packages/renderer-core/src/parts/extension/render.ts
Normal file
23
packages/renderer-core/src/parts/extension/render.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import { IPackageManagementService } from '../package';
|
||||||
|
import { IBoostsService } from './boosts';
|
||||||
|
import { ISchemaService } from '../schema';
|
||||||
|
import { IComponentTreeModelService } from '../component-tree-model';
|
||||||
|
|
||||||
|
export interface IRender {
|
||||||
|
mount: (el: HTMLElement) => void | Promise<void>;
|
||||||
|
unmount: () => void | Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RenderContext {
|
||||||
|
readonly schema: Omit<ISchemaService, 'initialize'>;
|
||||||
|
|
||||||
|
readonly packageManager: IPackageManagementService;
|
||||||
|
|
||||||
|
readonly boostsManager: IBoostsService;
|
||||||
|
|
||||||
|
readonly componentTreeModel: IComponentTreeModelService;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RenderAdapter<Render> {
|
||||||
|
(context: RenderContext): Render | Promise<Render>;
|
||||||
|
}
|
||||||
2
packages/renderer-core/src/parts/package/index.ts
Normal file
2
packages/renderer-core/src/parts/package/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from './loader';
|
||||||
|
export * from './managementService';
|
||||||
12
packages/renderer-core/src/parts/package/loader.ts
Normal file
12
packages/renderer-core/src/parts/package/loader.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { type Spec } from '@alilc/lowcode-shared';
|
||||||
|
import { type IPackageManagementService } from './managementService';
|
||||||
|
|
||||||
|
export interface PackageLoader {
|
||||||
|
name?: string;
|
||||||
|
load(this: IPackageManagementService, info: Spec.Package): Promise<any>;
|
||||||
|
active(info: Spec.Package): boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function definePackageLoader(loader: PackageLoader) {
|
||||||
|
return loader;
|
||||||
|
}
|
||||||
146
packages/renderer-core/src/parts/package/managementService.ts
Normal file
146
packages/renderer-core/src/parts/package/managementService.ts
Normal file
@ -0,0 +1,146 @@
|
|||||||
|
import { type Spec, type LowCodeComponent, createDecorator, Provide } from '@alilc/lowcode-shared';
|
||||||
|
import { PackageLoader } from './loader';
|
||||||
|
|
||||||
|
export interface IPackageManagementService {
|
||||||
|
/**
|
||||||
|
* 新增资产包
|
||||||
|
* @param packages
|
||||||
|
*/
|
||||||
|
loadPackages(packages: Spec.Package[]): Promise<void>;
|
||||||
|
/** 通过包名获取资产包信息 */
|
||||||
|
getPackageInfo(packageName: string): Spec.Package | undefined;
|
||||||
|
|
||||||
|
getLibraryByPackageName(packageName: string): any;
|
||||||
|
|
||||||
|
setLibraryByPackageName(packageName: string, library: any): void;
|
||||||
|
|
||||||
|
/** 解析组件映射 */
|
||||||
|
resolveComponentMaps(componentMaps: Spec.ComponentMap[]): void;
|
||||||
|
/** 获取组件映射对象,key = componentName value = component */
|
||||||
|
getComponentsNameRecord<C = unknown>(
|
||||||
|
componentMaps?: Spec.ComponentMap[],
|
||||||
|
): Record<string, C | LowCodeComponent>;
|
||||||
|
/** 通过组件名获取对应的组件 */
|
||||||
|
getComponent<C = unknown>(componentName: string): C | LowCodeComponent | undefined;
|
||||||
|
/** 注册组件 */
|
||||||
|
registerComponentByName(componentName: string, Component: unknown): void;
|
||||||
|
|
||||||
|
/** 新增资产包加载器 */
|
||||||
|
addPackageLoader(loader: PackageLoader): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const IPackageManagementService = createDecorator<IPackageManagementService>(
|
||||||
|
'packageManagementService',
|
||||||
|
);
|
||||||
|
|
||||||
|
@Provide(IPackageManagementService)
|
||||||
|
export class PackageManagementService implements IPackageManagementService {
|
||||||
|
private componentsRecord: Record<string, any> = {};
|
||||||
|
|
||||||
|
private packageStore: Map<string, any> = ((window as any).__PACKAGE_STORE__ ??= new Map());
|
||||||
|
|
||||||
|
private packagesRef: Spec.Package[] = [];
|
||||||
|
|
||||||
|
private packageLoaders: PackageLoader[] = [];
|
||||||
|
|
||||||
|
async loadPackages(packages: Spec.Package[]) {
|
||||||
|
for (const item of packages) {
|
||||||
|
if (!item.package && !item.id) continue;
|
||||||
|
|
||||||
|
const newId = item.package ?? item.id!;
|
||||||
|
const isExist = this.packagesRef.some((_) => {
|
||||||
|
const itemId = _.package ?? _.id;
|
||||||
|
return itemId === newId;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!isExist) {
|
||||||
|
this.packagesRef.push(item);
|
||||||
|
|
||||||
|
if (!this.packageStore.has(newId)) {
|
||||||
|
const loader = this.packageLoaders.find((loader) => loader.active(item));
|
||||||
|
if (!loader) continue;
|
||||||
|
|
||||||
|
const result = await loader.load.call(this, item);
|
||||||
|
if (result) this.packageStore.set(newId, result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getPackageInfo(packageName: string) {
|
||||||
|
return this.packagesRef.find((p) => p.package === packageName);
|
||||||
|
}
|
||||||
|
|
||||||
|
getLibraryByPackageName(packageName: string) {
|
||||||
|
const packageInfo = this.getPackageInfo(packageName);
|
||||||
|
|
||||||
|
if (packageInfo) {
|
||||||
|
return this.packageStore.get(packageInfo.package ?? packageInfo.id!);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setLibraryByPackageName(packageName: string, library: any) {
|
||||||
|
this.packageStore.set(packageName, library);
|
||||||
|
}
|
||||||
|
|
||||||
|
resolveComponentMaps(componentMaps: Spec.ComponentMap[]) {
|
||||||
|
for (const map of componentMaps) {
|
||||||
|
if (map.devMode === 'lowCode') {
|
||||||
|
const packageInfo = this.packagesRef.find((_) => {
|
||||||
|
return _.id === (map as LowCodeComponent).id;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (packageInfo) {
|
||||||
|
this.componentsRecord[map.componentName] = packageInfo;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (this.packageStore.has(map.package!)) {
|
||||||
|
const library = this.packageStore.get(map.package!);
|
||||||
|
// export { exportName } from xxx exportName === global.libraryName.exportName
|
||||||
|
// export exportName from xxx exportName === global.libraryName.default || global.libraryName
|
||||||
|
// export { exportName as componentName } from package
|
||||||
|
// if exportName == null exportName === componentName;
|
||||||
|
// const componentName = exportName.subName, if exportName empty subName donot use
|
||||||
|
const paths = map.exportName && map.subName ? map.subName.split('.') : [];
|
||||||
|
const exportName = map.exportName ?? map.componentName;
|
||||||
|
|
||||||
|
if (map.destructuring) {
|
||||||
|
paths.unshift(exportName);
|
||||||
|
}
|
||||||
|
|
||||||
|
let result = library;
|
||||||
|
for (const path of paths) {
|
||||||
|
result = result[path] || result;
|
||||||
|
}
|
||||||
|
|
||||||
|
const recordName = map.componentName ?? map.exportName;
|
||||||
|
this.componentsRecord[recordName] = result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getComponentsNameRecord(componentMaps?: Spec.ComponentMap[]) {
|
||||||
|
if (componentMaps) {
|
||||||
|
const newMaps = componentMaps.filter((item) => !this.componentsRecord[item.componentName]);
|
||||||
|
|
||||||
|
this.resolveComponentMaps(newMaps);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { ...this.componentsRecord };
|
||||||
|
}
|
||||||
|
|
||||||
|
getComponent(componentName: string) {
|
||||||
|
return this.componentsRecord[componentName];
|
||||||
|
}
|
||||||
|
|
||||||
|
registerComponentByName(componentName: string, Component: unknown) {
|
||||||
|
this.componentsRecord[componentName] = Component;
|
||||||
|
}
|
||||||
|
|
||||||
|
addPackageLoader(loader: PackageLoader) {
|
||||||
|
if (!loader.name || !this.packageLoaders.some((_) => _.name === loader.name)) {
|
||||||
|
this.packageLoaders.push(loader);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
84
packages/renderer-core/src/parts/runtimeIntl.ts
Normal file
84
packages/renderer-core/src/parts/runtimeIntl.ts
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
import {
|
||||||
|
createDecorator,
|
||||||
|
Provide,
|
||||||
|
Intl,
|
||||||
|
type Spec,
|
||||||
|
type Locale,
|
||||||
|
type LocaleTranslationsRecord,
|
||||||
|
type Translations,
|
||||||
|
} from '@alilc/lowcode-shared';
|
||||||
|
|
||||||
|
export interface MessageDescriptor {
|
||||||
|
key: string;
|
||||||
|
params?: Record<string, string>;
|
||||||
|
fallback?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IRuntimeIntlService {
|
||||||
|
initialize(locale: Locale, messages: LocaleTranslationsRecord): void;
|
||||||
|
|
||||||
|
t(descriptor: MessageDescriptor): string;
|
||||||
|
|
||||||
|
setLocale(locale: Locale): void;
|
||||||
|
|
||||||
|
getLocale(): Locale;
|
||||||
|
|
||||||
|
addTranslations(locale: Locale, translations: Translations): void;
|
||||||
|
|
||||||
|
toExpose(): Spec.IntlApi;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const IRuntimeIntlService = createDecorator<IRuntimeIntlService>('IRuntimeIntlService');
|
||||||
|
|
||||||
|
@Provide(IRuntimeIntlService)
|
||||||
|
export class RuntimeIntlService implements IRuntimeIntlService {
|
||||||
|
private intl: Intl;
|
||||||
|
|
||||||
|
private _expose: any;
|
||||||
|
|
||||||
|
initialize(locale: Locale, messages: LocaleTranslationsRecord) {
|
||||||
|
this.intl = new Intl(locale, messages);
|
||||||
|
}
|
||||||
|
|
||||||
|
t(descriptor: MessageDescriptor): string {
|
||||||
|
const formatter = this.intl.getFormatter();
|
||||||
|
|
||||||
|
return formatter.$t(
|
||||||
|
{
|
||||||
|
id: descriptor.key,
|
||||||
|
defaultMessage: descriptor.fallback,
|
||||||
|
},
|
||||||
|
descriptor.params,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
setLocale(locale: string): void {
|
||||||
|
this.intl.setLocale(locale);
|
||||||
|
}
|
||||||
|
|
||||||
|
getLocale(): string {
|
||||||
|
return this.intl.getLocale();
|
||||||
|
}
|
||||||
|
|
||||||
|
addTranslations(locale: Locale, translations: Translations) {
|
||||||
|
this.intl.addTranslations(locale, translations);
|
||||||
|
}
|
||||||
|
|
||||||
|
toExpose(): Spec.IntlApi {
|
||||||
|
if (!this._expose) {
|
||||||
|
this._expose = Object.freeze<Spec.IntlApi>({
|
||||||
|
i18n: (key, params) => {
|
||||||
|
return this.t({ key, params });
|
||||||
|
},
|
||||||
|
getLocale: () => {
|
||||||
|
return this.getLocale();
|
||||||
|
},
|
||||||
|
setLocale: (locale) => {
|
||||||
|
this.setLocale(locale);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return this._expose;
|
||||||
|
}
|
||||||
|
}
|
||||||
83
packages/renderer-core/src/parts/runtimeUtil.ts
Normal file
83
packages/renderer-core/src/parts/runtimeUtil.ts
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
import { type AnyFunction, type Spec, createDecorator, Provide } from '@alilc/lowcode-shared';
|
||||||
|
import { IPackageManagementService } from './package';
|
||||||
|
import { ICodeRuntimeService } from './code-runtime';
|
||||||
|
|
||||||
|
export interface IRuntimeUtilService {
|
||||||
|
add(utilItem: Spec.Util): void;
|
||||||
|
add(name: string, fn: AnyFunction): void;
|
||||||
|
|
||||||
|
remove(name: string): void;
|
||||||
|
|
||||||
|
toExpose(): Spec.UtilsApi;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const IRuntimeUtilService = createDecorator<IRuntimeUtilService>('rendererUtilService');
|
||||||
|
|
||||||
|
@Provide(IRuntimeUtilService)
|
||||||
|
export class RuntimeUtilService implements IRuntimeUtilService {
|
||||||
|
private utilsMap: Map<string, AnyFunction> = new Map();
|
||||||
|
|
||||||
|
private _expose: any;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
@ICodeRuntimeService private codeRuntimeService: ICodeRuntimeService,
|
||||||
|
@IPackageManagementService private packageManagementService: IPackageManagementService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
add(utilItem: Spec.Util): void;
|
||||||
|
add(name: string, fn: AnyFunction): void;
|
||||||
|
add(name: Spec.Util | string, fn?: AnyFunction): void {
|
||||||
|
if (typeof name === 'string') {
|
||||||
|
if (typeof fn === 'function') {
|
||||||
|
this.utilsMap.set(name, fn as AnyFunction);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const fn = this.parseUtil(name);
|
||||||
|
this.utilsMap.set(name.name, fn);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
remove(name: string): void {
|
||||||
|
this.utilsMap.delete(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
toExpose(): Spec.UtilsApi {
|
||||||
|
if (!this._expose) {
|
||||||
|
this._expose = new Proxy(Object.create(null), {
|
||||||
|
get: (_, p: string) => {
|
||||||
|
return this.utilsMap.get(p);
|
||||||
|
},
|
||||||
|
set() {
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
has: (_, p: string) => {
|
||||||
|
return this.utilsMap.has(p);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return this._expose;
|
||||||
|
}
|
||||||
|
|
||||||
|
private parseUtil(utilItem: Spec.Util) {
|
||||||
|
if (utilItem.type === 'function') {
|
||||||
|
const { content } = utilItem;
|
||||||
|
|
||||||
|
return this.codeRuntimeService.run(content.value);
|
||||||
|
} else {
|
||||||
|
const {
|
||||||
|
content: { package: packageName, destructuring, exportName, subName },
|
||||||
|
} = utilItem;
|
||||||
|
let library: any = this.packageManagementService.getLibraryByPackageName(packageName!);
|
||||||
|
|
||||||
|
if (library) {
|
||||||
|
if (destructuring) {
|
||||||
|
const target = library[exportName!];
|
||||||
|
library = subName ? target[subName] : target;
|
||||||
|
}
|
||||||
|
|
||||||
|
return library;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
1
packages/renderer-core/src/parts/schema/index.ts
Normal file
1
packages/renderer-core/src/parts/schema/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './schemaService';
|
||||||
65
packages/renderer-core/src/parts/schema/schemaService.ts
Normal file
65
packages/renderer-core/src/parts/schema/schemaService.ts
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
import {
|
||||||
|
type Spec,
|
||||||
|
createDecorator,
|
||||||
|
Provide,
|
||||||
|
KeyValueStore,
|
||||||
|
type EventDisposable,
|
||||||
|
} from '@alilc/lowcode-shared';
|
||||||
|
import { isObject } from 'lodash-es';
|
||||||
|
import { schemaValidation } from './validation';
|
||||||
|
|
||||||
|
export interface NormalizedSchema extends Spec.Project {}
|
||||||
|
|
||||||
|
export type NormalizedSchemaKey = keyof NormalizedSchema;
|
||||||
|
|
||||||
|
export interface ISchemaService {
|
||||||
|
initialize(schema: Spec.Project): void;
|
||||||
|
|
||||||
|
get<K extends NormalizedSchemaKey>(key: K): NormalizedSchema[K];
|
||||||
|
|
||||||
|
set<K extends NormalizedSchemaKey>(key: K, value: NormalizedSchema[K]): void;
|
||||||
|
|
||||||
|
onValueChange<K extends NormalizedSchemaKey>(
|
||||||
|
key: K,
|
||||||
|
listener: (value: NormalizedSchema[K]) => void,
|
||||||
|
): EventDisposable;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ISchemaService = createDecorator<ISchemaService>('schemaService');
|
||||||
|
|
||||||
|
@Provide(ISchemaService)
|
||||||
|
export class SchemaService implements ISchemaService {
|
||||||
|
private store: KeyValueStore<NormalizedSchema, NormalizedSchemaKey>;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.store = new KeyValueStore<NormalizedSchema, NormalizedSchemaKey>(new Map(), {
|
||||||
|
setterValidation: schemaValidation,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
initialize(schema: unknown): void {
|
||||||
|
if (!isObject(schema)) {
|
||||||
|
throw Error('schema muse a object');
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.keys(schema).forEach((key) => {
|
||||||
|
// @ts-expect-error: ignore initialization
|
||||||
|
this.set(key, schema[key]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
set<K extends NormalizedSchemaKey>(key: K, value: NormalizedSchema[K]): void {
|
||||||
|
this.store.set(key, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
get<K extends NormalizedSchemaKey>(key: K): NormalizedSchema[K] {
|
||||||
|
return this.store.get(key) as NormalizedSchema[K];
|
||||||
|
}
|
||||||
|
|
||||||
|
onValueChange<K extends NormalizedSchemaKey>(
|
||||||
|
key: K,
|
||||||
|
listener: (value: NormalizedSchema[K]) => void,
|
||||||
|
): EventDisposable {
|
||||||
|
return this.store.onValueChange(key, listener);
|
||||||
|
}
|
||||||
|
}
|
||||||
25
packages/renderer-core/src/parts/schema/validation.ts
Normal file
25
packages/renderer-core/src/parts/schema/validation.ts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import { type Spec } from '@alilc/lowcode-shared';
|
||||||
|
|
||||||
|
const SCHEMA_VALIDATIONS_OPTIONS: Partial<
|
||||||
|
Record<
|
||||||
|
keyof Spec.Project,
|
||||||
|
{
|
||||||
|
valid: (value: any) => boolean;
|
||||||
|
description: string;
|
||||||
|
}
|
||||||
|
>
|
||||||
|
> = {};
|
||||||
|
|
||||||
|
export function schemaValidation<K extends keyof Spec.Project>(key: K, value: Spec.Project[K]) {
|
||||||
|
const validOption = SCHEMA_VALIDATIONS_OPTIONS[key];
|
||||||
|
|
||||||
|
if (validOption) {
|
||||||
|
const result = validOption.valid(value);
|
||||||
|
|
||||||
|
if (!result) {
|
||||||
|
throw Error(validOption.description);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
1
packages/renderer-core/src/parts/widget/index.ts
Normal file
1
packages/renderer-core/src/parts/widget/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './widget';
|
||||||
68
packages/renderer-core/src/parts/widget/widget.ts
Normal file
68
packages/renderer-core/src/parts/widget/widget.ts
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
import { type Spec, uniqueId, EventDisposable, createCallback } from '@alilc/lowcode-shared';
|
||||||
|
import { clone } from 'lodash-es';
|
||||||
|
import { IComponentTreeModel } from '../component-tree-model';
|
||||||
|
|
||||||
|
export interface WidgetBuildContext<Component, ComponentInstance = unknown> {
|
||||||
|
key: string;
|
||||||
|
|
||||||
|
node: Spec.NodeType;
|
||||||
|
|
||||||
|
model: IComponentTreeModel<Component, ComponentInstance>;
|
||||||
|
|
||||||
|
children?: IWidget<Component, ComponentInstance>[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IWidget<Component, ComponentInstance = unknown> {
|
||||||
|
readonly key: string;
|
||||||
|
|
||||||
|
readonly node: Spec.NodeType;
|
||||||
|
|
||||||
|
children?: IWidget<Component, ComponentInstance>[];
|
||||||
|
|
||||||
|
beforeBuild<T extends Spec.NodeType>(beforeGuard: (node: T) => T): EventDisposable;
|
||||||
|
|
||||||
|
build<Element>(
|
||||||
|
builder: (context: WidgetBuildContext<Component, ComponentInstance>) => Element,
|
||||||
|
): Element;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Widget<Component, ComponentInstance = unknown>
|
||||||
|
implements IWidget<Component, ComponentInstance>
|
||||||
|
{
|
||||||
|
private beforeGuardCallbacks = createCallback();
|
||||||
|
|
||||||
|
public __raw: Spec.NodeType;
|
||||||
|
|
||||||
|
public node: Spec.NodeType;
|
||||||
|
|
||||||
|
public key: string;
|
||||||
|
|
||||||
|
public children?: IWidget<Component, ComponentInstance>[] | undefined;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
node: Spec.NodeType,
|
||||||
|
private model: IComponentTreeModel<Component, ComponentInstance>,
|
||||||
|
) {
|
||||||
|
this.node = clone(node);
|
||||||
|
this.__raw = node;
|
||||||
|
this.key = (node as Spec.ComponentNode)?.id ?? uniqueId();
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeBuild<T extends Spec.NodeType>(beforeGuard: (node: T) => T): EventDisposable {
|
||||||
|
return this.beforeGuardCallbacks.add(beforeGuard);
|
||||||
|
}
|
||||||
|
|
||||||
|
build<Element>(
|
||||||
|
builder: (context: WidgetBuildContext<Component, ComponentInstance>) => Element,
|
||||||
|
): Element {
|
||||||
|
const beforeGuards = this.beforeGuardCallbacks.list();
|
||||||
|
const finalNode = beforeGuards.reduce((prev, cb) => cb(prev), this.node);
|
||||||
|
|
||||||
|
return builder({
|
||||||
|
key: this.key,
|
||||||
|
node: finalNode,
|
||||||
|
model: this.model,
|
||||||
|
children: this.children,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,60 +0,0 @@
|
|||||||
import { type AppContext } from './api/app';
|
|
||||||
import { nonSetterProxy } from './utils/non-setter-proxy';
|
|
||||||
|
|
||||||
export interface Plugin<C extends PluginSetupContext = PluginSetupContext> {
|
|
||||||
name: string; // 插件的 name 作为唯一标识,并不可重复。
|
|
||||||
setup(setupContext: C): void | Promise<void>;
|
|
||||||
dependsOn?: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface PluginSetupContext extends AppContext {
|
|
||||||
[key: string]: any;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createPluginManager(context: PluginSetupContext) {
|
|
||||||
const installedPlugins: Plugin[] = [];
|
|
||||||
let readyToInstallPlugins: Plugin[] = [];
|
|
||||||
|
|
||||||
const setupContext = nonSetterProxy(context);
|
|
||||||
|
|
||||||
async function install(plugin: Plugin) {
|
|
||||||
if (installedPlugins.some((p) => p.name === plugin.name)) return;
|
|
||||||
|
|
||||||
if (plugin.dependsOn?.some((dep) => !installedPlugins.some((p) => p.name === dep))) {
|
|
||||||
readyToInstallPlugins.push(plugin);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await plugin.setup(setupContext);
|
|
||||||
installedPlugins.push(plugin);
|
|
||||||
|
|
||||||
// 遍历未安装的插件 寻找 dependsOn 的插件已安装完的插件进行安装
|
|
||||||
for (const item of readyToInstallPlugins) {
|
|
||||||
if (item.dependsOn?.every((dep) => installedPlugins.some((p) => p.name === dep))) {
|
|
||||||
await item.setup(setupContext);
|
|
||||||
installedPlugins.push(item);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (readyToInstallPlugins.length) {
|
|
||||||
readyToInstallPlugins = readyToInstallPlugins.filter((item) =>
|
|
||||||
installedPlugins.some((p) => p.name === item.name),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
async add(plugin: Plugin) {
|
|
||||||
if (installedPlugins.find((item) => item.name === plugin.name)) {
|
|
||||||
console.warn('该插件已安装');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await install(plugin);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function definePlugin<C extends PluginSetupContext, P = Plugin<C>>(plugin: P) {
|
|
||||||
return plugin;
|
|
||||||
}
|
|
||||||
@ -1,106 +0,0 @@
|
|||||||
import type { Project, ComponentTree, ComponentMap, PageConfig } from './types';
|
|
||||||
import { throwRuntimeError } from './utils/error';
|
|
||||||
import { set, get } from 'lodash-es';
|
|
||||||
|
|
||||||
export interface AppSchema {
|
|
||||||
getComponentsTrees(): ComponentTree[];
|
|
||||||
addComponentsTree(tree: ComponentTree): void;
|
|
||||||
removeComponentsTree(id: string): void;
|
|
||||||
|
|
||||||
getComponentsMaps(): ComponentMap[];
|
|
||||||
addComponentsMap(componentName: ComponentMap): void;
|
|
||||||
removeComponentsMap(componentName: string): void;
|
|
||||||
|
|
||||||
getPageConfigs(): PageConfig[];
|
|
||||||
addPageConfig(page: PageConfig): void;
|
|
||||||
removePageConfig(id: string): void;
|
|
||||||
|
|
||||||
getByKey<K extends keyof Project>(key: K): Project[K] | undefined;
|
|
||||||
updateByKey<K extends keyof Project>(
|
|
||||||
key: K,
|
|
||||||
updater: Project[K] | ((value: Project[K]) => Project[K]),
|
|
||||||
): void;
|
|
||||||
|
|
||||||
getByPath(path: string | string[]): any;
|
|
||||||
updateByPath(path: string | string[], updater: any | ((value: any) => any)): void;
|
|
||||||
|
|
||||||
find(predicate: (schema: Project) => any): any;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createAppSchema(schema: Project): AppSchema {
|
|
||||||
if (!schema.version.startsWith('1.')) {
|
|
||||||
throwRuntimeError('core', 'schema version must be 1.x.x');
|
|
||||||
}
|
|
||||||
|
|
||||||
const schemaRef = structuredClone(schema);
|
|
||||||
|
|
||||||
return {
|
|
||||||
getComponentsTrees() {
|
|
||||||
return schemaRef.componentsTree;
|
|
||||||
},
|
|
||||||
addComponentsTree(tree) {
|
|
||||||
addArrayItem(schemaRef.componentsTree, tree, 'id');
|
|
||||||
},
|
|
||||||
removeComponentsTree(id) {
|
|
||||||
removeArrayItem(schemaRef.componentsTree, 'id', id);
|
|
||||||
},
|
|
||||||
|
|
||||||
getComponentsMaps() {
|
|
||||||
return schemaRef.componentsMap;
|
|
||||||
},
|
|
||||||
addComponentsMap(componentsMap) {
|
|
||||||
addArrayItem(schemaRef.componentsMap, componentsMap, 'componentName');
|
|
||||||
},
|
|
||||||
removeComponentsMap(componentName) {
|
|
||||||
removeArrayItem(schemaRef.componentsMap, 'componentName', componentName);
|
|
||||||
},
|
|
||||||
|
|
||||||
getPageConfigs() {
|
|
||||||
return schemaRef.pages ?? [];
|
|
||||||
},
|
|
||||||
addPageConfig(page) {
|
|
||||||
schemaRef.pages ??= [];
|
|
||||||
addArrayItem(schemaRef.pages, page, 'id');
|
|
||||||
},
|
|
||||||
removePageConfig(id) {
|
|
||||||
schemaRef.pages ??= [];
|
|
||||||
removeArrayItem(schemaRef.pages, 'id', id);
|
|
||||||
},
|
|
||||||
|
|
||||||
getByKey(key) {
|
|
||||||
return schemaRef[key];
|
|
||||||
},
|
|
||||||
updateByKey(key, updater) {
|
|
||||||
const value = schemaRef[key];
|
|
||||||
schemaRef[key] = typeof updater === 'function' ? (updater as any)(value) : updater;
|
|
||||||
},
|
|
||||||
|
|
||||||
find(predicate) {
|
|
||||||
return predicate(schemaRef);
|
|
||||||
},
|
|
||||||
getByPath(path) {
|
|
||||||
return get(schemaRef, path);
|
|
||||||
},
|
|
||||||
updateByPath(path, updater) {
|
|
||||||
set(schemaRef, path, typeof updater === 'function' ? updater(this.getByPath(path)) : updater);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function addArrayItem<T extends Record<string, any>>(target: T[], item: T, comparison: string) {
|
|
||||||
const idx = target.findIndex((_) => _[comparison] === item[comparison]);
|
|
||||||
if (idx > -1) {
|
|
||||||
target.splice(idx, 1, item);
|
|
||||||
} else {
|
|
||||||
target.push(item);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function removeArrayItem<T extends Record<string, any>>(
|
|
||||||
target: T[],
|
|
||||||
comparison: string,
|
|
||||||
comparisonValue: any,
|
|
||||||
) {
|
|
||||||
const idx = target.findIndex((item) => item[comparison] === comparisonValue);
|
|
||||||
if (idx > -1) target.splice(idx, 1);
|
|
||||||
}
|
|
||||||
31
packages/renderer-core/src/types.ts
Normal file
31
packages/renderer-core/src/types.ts
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import { type Spec } from '@alilc/lowcode-shared';
|
||||||
|
import { type Plugin } from './parts/extension';
|
||||||
|
import { type ISchemaService } from './parts/schema';
|
||||||
|
import { type IPackageManagementService } from './parts/package';
|
||||||
|
import { type IExtensionHostService } from './parts/extension';
|
||||||
|
|
||||||
|
export interface AppOptions {
|
||||||
|
schema: Spec.Project;
|
||||||
|
packages?: Spec.Package[];
|
||||||
|
plugins?: Plugin[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 应用语言,默认值为浏览器当前语言 navigator.language
|
||||||
|
*/
|
||||||
|
locale?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 运行模式
|
||||||
|
*/
|
||||||
|
mode?: 'development' | 'production';
|
||||||
|
}
|
||||||
|
|
||||||
|
export type RendererApplication<Render = unknown> = {
|
||||||
|
readonly mode: 'development' | 'production';
|
||||||
|
|
||||||
|
readonly schema: Omit<ISchemaService, 'initialize'>;
|
||||||
|
|
||||||
|
readonly packageManager: IPackageManagementService;
|
||||||
|
|
||||||
|
use: IExtensionHostService['registerPlugin'];
|
||||||
|
} & Render;
|
||||||
@ -1,17 +0,0 @@
|
|||||||
import { appBoosts } from '../boosts';
|
|
||||||
|
|
||||||
export type ErrorType = string;
|
|
||||||
|
|
||||||
export class RuntimeError extends Error {
|
|
||||||
constructor(
|
|
||||||
public type: ErrorType,
|
|
||||||
message: string,
|
|
||||||
) {
|
|
||||||
super(message);
|
|
||||||
appBoosts.hookStore.call('app:error', this);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function throwRuntimeError(errorType: ErrorType, message: string) {
|
|
||||||
return new RuntimeError(errorType, message);
|
|
||||||
}
|
|
||||||
@ -1,8 +0,0 @@
|
|||||||
let idStart = 0x0907;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generate unique id
|
|
||||||
*/
|
|
||||||
export function guid(): number {
|
|
||||||
return idStart++;
|
|
||||||
}
|
|
||||||
@ -1,177 +0,0 @@
|
|||||||
import type { AnyFunction } from '../types';
|
|
||||||
|
|
||||||
export type EventName = string | number | symbol;
|
|
||||||
|
|
||||||
export function createEvent<T = AnyFunction>() {
|
|
||||||
let events: T[] = [];
|
|
||||||
|
|
||||||
function add(fn: T) {
|
|
||||||
events.push(fn);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
events = events.filter((e) => e !== fn);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function remove(fn: T) {
|
|
||||||
events = events.filter((f) => fn !== f);
|
|
||||||
}
|
|
||||||
|
|
||||||
function list() {
|
|
||||||
return [...events];
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
add,
|
|
||||||
remove,
|
|
||||||
list,
|
|
||||||
clear() {
|
|
||||||
events.length = 0;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export type Event<F = AnyFunction> = ReturnType<typeof createEvent<F>>;
|
|
||||||
|
|
||||||
export type HookCallback = (...args: any) => Promise<any> | any;
|
|
||||||
|
|
||||||
type HookKeys<T> = keyof T & PropertyKey;
|
|
||||||
|
|
||||||
type InferCallback<HT, HN extends keyof HT> = HT[HN] extends HookCallback ? HT[HN] : never;
|
|
||||||
|
|
||||||
declare global {
|
|
||||||
interface Console {
|
|
||||||
// https://developer.chrome.com/blog/devtools-modern-web-debugging/#linked-stack-traces
|
|
||||||
createTask(name: string): {
|
|
||||||
run: <T extends () => any>(fn: T) => ReturnType<T>;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// https://developer.chrome.com/blog/devtools-modern-web-debugging/#linked-stack-traces
|
|
||||||
type CreateTask = typeof console.createTask;
|
|
||||||
const defaultTask: ReturnType<CreateTask> = { run: (fn) => fn() };
|
|
||||||
const _createTask: CreateTask = () => defaultTask;
|
|
||||||
const createTask = typeof console.createTask !== 'undefined' ? console.createTask : _createTask;
|
|
||||||
|
|
||||||
export interface HookStore<
|
|
||||||
HooksT extends Record<PropertyKey, any> = Record<PropertyKey, HookCallback>,
|
|
||||||
HookNameT extends HookKeys<HooksT> = HookKeys<HooksT>,
|
|
||||||
> {
|
|
||||||
hook<NameT extends HookNameT>(name: NameT, fn: InferCallback<HooksT, NameT>): () => void;
|
|
||||||
|
|
||||||
call<NameT extends HookNameT>(
|
|
||||||
name: NameT,
|
|
||||||
...args: Parameters<InferCallback<HooksT, NameT>>
|
|
||||||
): void;
|
|
||||||
callAsync<NameT extends HookNameT>(
|
|
||||||
name: NameT,
|
|
||||||
...args: Parameters<InferCallback<HooksT, NameT>>
|
|
||||||
): Promise<void>;
|
|
||||||
callParallel<NameT extends HookNameT>(
|
|
||||||
name: NameT,
|
|
||||||
...args: Parameters<InferCallback<HooksT, NameT>>
|
|
||||||
): Promise<void[]>;
|
|
||||||
|
|
||||||
remove<NameT extends HookNameT>(name: NameT, fn?: InferCallback<HooksT, NameT>): void;
|
|
||||||
|
|
||||||
clear<NameT extends HookNameT>(name?: NameT): void;
|
|
||||||
|
|
||||||
getHooks<NameT extends HookNameT>(name: NameT): InferCallback<HooksT, NameT>[] | undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createHookStore<
|
|
||||||
HooksT extends Record<PropertyKey, any> = Record<PropertyKey, HookCallback>,
|
|
||||||
HookNameT extends HookKeys<HooksT> = HookKeys<HooksT>,
|
|
||||||
>(): HookStore<HooksT, HookNameT> {
|
|
||||||
const hooksMap = new Map<HookNameT, Event<HookCallback>>();
|
|
||||||
|
|
||||||
function hook<NameT extends HookNameT>(name: NameT, fn: InferCallback<HooksT, NameT>) {
|
|
||||||
if (!name || typeof fn !== 'function') {
|
|
||||||
return () => {};
|
|
||||||
}
|
|
||||||
|
|
||||||
let hooks = hooksMap.get(name);
|
|
||||||
if (!hooks) {
|
|
||||||
hooks = createEvent();
|
|
||||||
hooksMap.set(name, hooks);
|
|
||||||
}
|
|
||||||
|
|
||||||
hooks.add(fn);
|
|
||||||
return () => remove(name, fn);
|
|
||||||
}
|
|
||||||
|
|
||||||
function call<NameT extends HookNameT>(
|
|
||||||
name: NameT,
|
|
||||||
...args: Parameters<InferCallback<HooksT, NameT>>
|
|
||||||
) {
|
|
||||||
const hooks = hooksMap.get(name)?.list() ?? [];
|
|
||||||
|
|
||||||
for (const hookFn of hooks) {
|
|
||||||
hookFn.call(null, ...args);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function callAsync<NameT extends HookNameT>(
|
|
||||||
name: NameT,
|
|
||||||
...args: Parameters<InferCallback<HooksT, NameT>>
|
|
||||||
) {
|
|
||||||
const hooks = hooksMap.get(name)?.list() ?? [];
|
|
||||||
const task = createTask(name.toString());
|
|
||||||
|
|
||||||
return hooks.reduce(
|
|
||||||
(promise, hookFunction) => promise.then(() => task.run(() => hookFunction(...args))),
|
|
||||||
Promise.resolve(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function callParallel<NameT extends HookNameT>(
|
|
||||||
name: NameT,
|
|
||||||
...args: Parameters<InferCallback<HooksT, NameT>>
|
|
||||||
) {
|
|
||||||
const hooks = hooksMap.get(name)?.list() ?? [];
|
|
||||||
const task = createTask(name.toString());
|
|
||||||
return Promise.all(hooks.map((hook) => task.run(() => hook(...args))));
|
|
||||||
}
|
|
||||||
|
|
||||||
function remove<NameT extends HookNameT>(name: NameT, fn?: InferCallback<HooksT, NameT>) {
|
|
||||||
const hooks = hooksMap.get(name);
|
|
||||||
if (!hooks) return;
|
|
||||||
|
|
||||||
if (fn) {
|
|
||||||
hooks.remove(fn);
|
|
||||||
if (hooks.list().length === 0) {
|
|
||||||
hooksMap.delete(name);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
hooksMap.delete(name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function clear<NameT extends HookNameT>(name?: NameT) {
|
|
||||||
if (name) {
|
|
||||||
remove(name);
|
|
||||||
} else {
|
|
||||||
hooksMap.clear();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getHooks<NameT extends HookNameT>(
|
|
||||||
name: NameT,
|
|
||||||
): InferCallback<HooksT, NameT>[] | undefined {
|
|
||||||
return hooksMap.get(name)?.list() as InferCallback<HooksT, NameT>[] | undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
hook,
|
|
||||||
|
|
||||||
call,
|
|
||||||
callAsync,
|
|
||||||
callParallel,
|
|
||||||
|
|
||||||
remove,
|
|
||||||
clear,
|
|
||||||
|
|
||||||
getHooks,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@ -1,13 +0,0 @@
|
|||||||
export function nonSetterProxy<T extends object>(target: T) {
|
|
||||||
return new Proxy<T>(target, {
|
|
||||||
get(target, p, receiver) {
|
|
||||||
return Reflect.get(target, p, receiver);
|
|
||||||
},
|
|
||||||
set() {
|
|
||||||
return false;
|
|
||||||
},
|
|
||||||
has(target, p) {
|
|
||||||
return Reflect.has(target, p);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@ -1,89 +0,0 @@
|
|||||||
import type { NodeType, ComponentTreeNode, ComponentTreeNodeProps, JSExpression } from './types';
|
|
||||||
import { isJSExpression, isI18nNode } from './utils/type-guard';
|
|
||||||
import { guid } from './utils/guid';
|
|
||||||
|
|
||||||
export class Widget<Data, Element> {
|
|
||||||
protected proxyElements: Element[] = [];
|
|
||||||
protected renderObject: Element | undefined;
|
|
||||||
|
|
||||||
constructor(public raw: Data) {
|
|
||||||
this.init();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected init() {}
|
|
||||||
|
|
||||||
get key(): string {
|
|
||||||
return (this.raw as any)?.id ?? `${guid()}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
mapRenderObject(mapper: (widget: Widget<Data, Element>) => Element | undefined) {
|
|
||||||
this.renderObject = mapper(this);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
addProxyELements(el: Element) {
|
|
||||||
this.proxyElements.unshift(el);
|
|
||||||
}
|
|
||||||
|
|
||||||
build<C>(builder: (elements: Element[]) => C): C {
|
|
||||||
return builder(this.renderObject ? [...this.proxyElements, this.renderObject] : []);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export type TextWidgetData = Exclude<NodeType, ComponentTreeNode>;
|
|
||||||
export type TextWidgetType = 'string' | 'expression' | 'i18n';
|
|
||||||
|
|
||||||
export class TextWidget<E = unknown> extends Widget<TextWidgetData, E> {
|
|
||||||
type: TextWidgetType = 'string';
|
|
||||||
|
|
||||||
protected init() {
|
|
||||||
if (isJSExpression(this.raw)) {
|
|
||||||
this.type = 'expression';
|
|
||||||
} else if (isI18nNode(this.raw)) {
|
|
||||||
this.type = 'i18n';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class ComponentWidget<E = unknown> extends Widget<ComponentTreeNode, E> {
|
|
||||||
private _children: (TextWidget<E> | ComponentWidget<E>)[] = [];
|
|
||||||
private _propsValue: ComponentTreeNodeProps = {};
|
|
||||||
|
|
||||||
protected init() {
|
|
||||||
if (this.raw.props) {
|
|
||||||
this._propsValue = this.raw.props;
|
|
||||||
}
|
|
||||||
if (this.raw.children) {
|
|
||||||
this._children = this.raw.children.map((child) => createWidget<E>(child));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
get componentName() {
|
|
||||||
return this.raw.componentName;
|
|
||||||
}
|
|
||||||
get props() {
|
|
||||||
return this._propsValue ?? {};
|
|
||||||
}
|
|
||||||
get condition() {
|
|
||||||
return this.raw.condition !== false;
|
|
||||||
}
|
|
||||||
get loop(): unknown[] | JSExpression | undefined {
|
|
||||||
return this.raw.loop;
|
|
||||||
}
|
|
||||||
get loopArgs() {
|
|
||||||
return this.raw.loopArgs ?? ['item', 'index'];
|
|
||||||
}
|
|
||||||
get children() {
|
|
||||||
return this._children;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createWidget<E = unknown>(data: NodeType) {
|
|
||||||
if (typeof data === 'string' || isJSExpression(data) || isI18nNode(data)) {
|
|
||||||
return new TextWidget<E>(data);
|
|
||||||
} else if (data.componentName) {
|
|
||||||
return new ComponentWidget<E>(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
throw Error(`unknown node data: ${JSON.stringify(data)}`);
|
|
||||||
}
|
|
||||||
@ -22,7 +22,7 @@
|
|||||||
"test": "vitest"
|
"test": "vitest"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@alilc/lowcode-renderer-core": "workspace:*",
|
"@alilc/lowcode-shared": "workspace:*",
|
||||||
"lodash-es": "^4.17.21",
|
"lodash-es": "^4.17.21",
|
||||||
"path-to-regexp": "^6.2.1",
|
"path-to-regexp": "^6.2.1",
|
||||||
"qs": "^6.12.0"
|
"qs": "^6.12.0"
|
||||||
@ -31,9 +31,6 @@
|
|||||||
"@types/lodash-es": "^4.17.12",
|
"@types/lodash-es": "^4.17.12",
|
||||||
"@types/qs": "^6.9.13"
|
"@types/qs": "^6.9.13"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
|
||||||
"@alilc/lowcode-renderer-core": "workspace:*"
|
|
||||||
},
|
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
"access": "public",
|
"access": "public",
|
||||||
"registry": "https://registry.npmjs.org/"
|
"registry": "https://registry.npmjs.org/"
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
import { RawRouteLocation } from '@alilc/lowcode-renderer-core';
|
import { type RouteLocationNormalized, type RawRouteLocation } from './types';
|
||||||
import { type RouteLocationNormalized } from './types';
|
|
||||||
import { isRouteLocation } from './utils/helper';
|
import { isRouteLocation } from './utils/helper';
|
||||||
|
|
||||||
export type NavigationHookAfter = (
|
export type NavigationHookAfter = (
|
||||||
|
|||||||
@ -1,6 +1,12 @@
|
|||||||
import { createEvent } from '@alilc/lowcode-renderer-core';
|
import { createCallback } from './utils/callback';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* history state
|
||||||
|
*/
|
||||||
export type HistoryState = History['state'];
|
export type HistoryState = History['state'];
|
||||||
|
/**
|
||||||
|
* history locaiton
|
||||||
|
*/
|
||||||
export type HistoryLocation = string;
|
export type HistoryLocation = string;
|
||||||
|
|
||||||
export enum NavigationType {
|
export enum NavigationType {
|
||||||
@ -166,8 +172,8 @@ export function createBrowserHistory(base?: string): RouterHistory {
|
|||||||
currentLocation = to;
|
currentLocation = to;
|
||||||
}
|
}
|
||||||
|
|
||||||
const listeners = createEvent<NavigationCallback>();
|
const listeners = createCallback<NavigationCallback>();
|
||||||
const teardowns = createEvent<() => void>();
|
const teardowns = createCallback<() => void>();
|
||||||
|
|
||||||
let pauseState: HistoryLocation | null = null;
|
let pauseState: HistoryLocation | null = null;
|
||||||
|
|
||||||
@ -266,7 +272,7 @@ export function createBrowserHistory(base?: string): RouterHistory {
|
|||||||
function normalizeBase(base?: string) {
|
function normalizeBase(base?: string) {
|
||||||
if (!base) {
|
if (!base) {
|
||||||
// strip full URL origin
|
// strip full URL origin
|
||||||
base = document.baseURI.replace(/^\w+:\/\/[^\/]+/, '');
|
base = document.baseURI.replace(/^\w+:\/\/[^/]+/, '');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理边界问题 确保是一个浏览器路径 如 /xxx #/xxx
|
// 处理边界问题 确保是一个浏览器路径 如 /xxx #/xxx
|
||||||
@ -348,7 +354,7 @@ export function createMemoryHistory(base = ''): RouterHistory {
|
|||||||
historyStack.push({ location, state });
|
historyStack.push({ location, state });
|
||||||
}
|
}
|
||||||
|
|
||||||
const listeners = createEvent<NavigationCallback>();
|
const listeners = createCallback<NavigationCallback>();
|
||||||
|
|
||||||
function triggerListeners(
|
function triggerListeners(
|
||||||
to: HistoryLocation,
|
to: HistoryLocation,
|
||||||
|
|||||||
@ -1,7 +1,8 @@
|
|||||||
export { createRouter } from './router';
|
export { createRouter } from './router';
|
||||||
export { createBrowserHistory, createHashHistory, createMemoryHistory } from './history';
|
export { createBrowserHistory, createHashHistory, createMemoryHistory } from './history';
|
||||||
|
|
||||||
export type { RouterHistory } from './history';
|
export type * from './types';
|
||||||
export type { NavigationGuard, NavigationHookAfter } from './guard';
|
export type * from './history';
|
||||||
|
export type { NavigationGuard, NavigationHookAfter, NavigationGuardReturn } from './guard';
|
||||||
export type { Router, RouterOptions } from './router';
|
export type { Router, RouterOptions } from './router';
|
||||||
export * from './types';
|
export type { PathParserOptions } from './utils/path-parser';
|
||||||
|
|||||||
@ -1,11 +1,15 @@
|
|||||||
// refer from https://github.com/vuejs/router/blob/main/packages/router/src/matcher/index.ts
|
// refer from https://github.com/vuejs/router/blob/main/packages/router/src/matcher/index.ts
|
||||||
|
|
||||||
import { type PlainObject, type RawLocation } from '@alilc/lowcode-renderer-core';
|
import { type PlainObject } from '@alilc/lowcode-shared';
|
||||||
import { pick } from 'lodash-es';
|
import { pick } from 'lodash-es';
|
||||||
import { createRouteRecordMatcher, type RouteRecordMatcher } from './utils/record-matcher';
|
import { createRouteRecordMatcher, type RouteRecordMatcher } from './utils/record-matcher';
|
||||||
import { type PathParserOptions, type PathParams, comparePathParserScore } from './utils/path-parser';
|
import {
|
||||||
|
type PathParserOptions,
|
||||||
|
type PathParams,
|
||||||
|
comparePathParserScore,
|
||||||
|
} from './utils/path-parser';
|
||||||
|
|
||||||
import type { RouteRecord, RouteLocationNormalized } from './types';
|
import type { RouteRecord, RouteLocationNormalized, RawLocation } from './types';
|
||||||
|
|
||||||
export interface RouteRecordNormalized {
|
export interface RouteRecordNormalized {
|
||||||
/**
|
/**
|
||||||
@ -60,9 +64,7 @@ export interface RouterMatcher {
|
|||||||
* @param location - MatcherLocationRaw to resolve to a url
|
* @param location - MatcherLocationRaw to resolve to a url
|
||||||
* @param currentLocation - MatcherLocation of the current location
|
* @param currentLocation - MatcherLocation of the current location
|
||||||
*/
|
*/
|
||||||
resolve: (
|
resolve: (location: RawLocation, currentLocation: MatcherLocation) => MatcherLocation;
|
||||||
location: RawLocation, currentLocation: MatcherLocation
|
|
||||||
) => MatcherLocation;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createRouterMatcher(
|
export function createRouterMatcher(
|
||||||
@ -104,8 +106,7 @@ export function createRouterMatcher(
|
|||||||
while (
|
while (
|
||||||
i < matchers.length &&
|
i < matchers.length &&
|
||||||
comparePathParserScore(matcher, matchers[i]) >= 0 &&
|
comparePathParserScore(matcher, matchers[i]) >= 0 &&
|
||||||
(matcher.record.path !== matchers[i].record.path ||
|
(matcher.record.path !== matchers[i].record.path || !isRecordChildOf(matcher, matchers[i]))
|
||||||
!isRecordChildOf(matcher, matchers[i]))
|
|
||||||
) {
|
) {
|
||||||
i++;
|
i++;
|
||||||
}
|
}
|
||||||
@ -139,10 +140,7 @@ export function createRouterMatcher(
|
|||||||
return matcherMap.get(name);
|
return matcherMap.get(name);
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolve(
|
function resolve(location: RawLocation, currentLocation: MatcherLocation): MatcherLocation {
|
||||||
location: RawLocation,
|
|
||||||
currentLocation: MatcherLocation
|
|
||||||
): MatcherLocation {
|
|
||||||
let matcher: RouteRecordMatcher | undefined;
|
let matcher: RouteRecordMatcher | undefined;
|
||||||
let params: PathParams = {};
|
let params: PathParams = {};
|
||||||
let path: MatcherLocation['path'];
|
let path: MatcherLocation['path'];
|
||||||
@ -163,10 +161,8 @@ export function createRouterMatcher(
|
|||||||
paramsFromLocation(
|
paramsFromLocation(
|
||||||
currentLocation.params ?? {},
|
currentLocation.params ?? {},
|
||||||
matcher.keys
|
matcher.keys
|
||||||
.filter(k => !k.optional)
|
.filter((k) => !k.optional)
|
||||||
.concat(
|
.concat(matcher.parent ? matcher.parent.keys.filter((k) => k.optional) : [])
|
||||||
matcher.parent ? matcher.parent.keys.filter(k => k.optional) : []
|
|
||||||
)
|
|
||||||
.map((k) => k.name),
|
.map((k) => k.name),
|
||||||
),
|
),
|
||||||
location.params
|
location.params
|
||||||
@ -253,11 +249,6 @@ export function normalizeRouteRecord(record: RouteRecord): RouteRecordNormalized
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function isRecordChildOf(
|
function isRecordChildOf(record: RouteRecordMatcher, parent: RouteRecordMatcher): boolean {
|
||||||
record: RouteRecordMatcher,
|
return parent.children.some((child) => child === record || isRecordChildOf(record, child));
|
||||||
parent: RouteRecordMatcher
|
|
||||||
): boolean {
|
|
||||||
return parent.children.some(
|
|
||||||
child => child === record || isRecordChildOf(record, child)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,11 +1,4 @@
|
|||||||
import {
|
import { type Spec } from '@alilc/lowcode-shared';
|
||||||
type RouterApi,
|
|
||||||
type RouterConfig,
|
|
||||||
type RouteLocation,
|
|
||||||
createEvent,
|
|
||||||
type RawRouteLocation,
|
|
||||||
type RawLocationOptions,
|
|
||||||
} from '@alilc/lowcode-renderer-core';
|
|
||||||
import {
|
import {
|
||||||
createBrowserHistory,
|
createBrowserHistory,
|
||||||
createHashHistory,
|
createHashHistory,
|
||||||
@ -17,14 +10,21 @@ import { createRouterMatcher } from './matcher';
|
|||||||
import { type PathParserOptions, type PathParams } from './utils/path-parser';
|
import { type PathParserOptions, type PathParams } from './utils/path-parser';
|
||||||
import { parseURL, stringifyURL } from './utils/url';
|
import { parseURL, stringifyURL } from './utils/url';
|
||||||
import { isSameRouteLocation } from './utils/helper';
|
import { isSameRouteLocation } from './utils/helper';
|
||||||
import type { RouteRecord, RouteLocationNormalized } from './types';
|
import type {
|
||||||
|
RouteRecord,
|
||||||
|
RouteLocationNormalized,
|
||||||
|
RawRouteLocation,
|
||||||
|
RouteLocation,
|
||||||
|
RawLocationOptions,
|
||||||
|
} from './types';
|
||||||
import { type NavigationHookAfter, type NavigationGuard, guardToPromiseFn } from './guard';
|
import { type NavigationHookAfter, type NavigationGuard, guardToPromiseFn } from './guard';
|
||||||
|
import { createCallback } from './utils/callback';
|
||||||
|
|
||||||
export interface RouterOptions extends RouterConfig, PathParserOptions {
|
export interface RouterOptions extends Spec.RouterConfig, PathParserOptions {
|
||||||
routes: RouteRecord[];
|
routes: RouteRecord[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Router extends RouterApi {
|
export interface Router extends Spec.RouterApi {
|
||||||
readonly options: RouterOptions;
|
readonly options: RouterOptions;
|
||||||
readonly history: RouterHistory;
|
readonly history: RouterHistory;
|
||||||
|
|
||||||
@ -56,13 +56,7 @@ const START_LOCATION: RouteLocationNormalized = {
|
|||||||
redirectedFrom: undefined,
|
redirectedFrom: undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
const defaultRouterOptions: RouterOptions = {
|
export function createRouter(options: RouterOptions): Router {
|
||||||
historyMode: 'browser',
|
|
||||||
baseName: '/',
|
|
||||||
routes: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
export function createRouter(options: RouterOptions = defaultRouterOptions): Router {
|
|
||||||
const { baseName = '/', historyMode = 'browser', routes = [], ...globalOptions } = options;
|
const { baseName = '/', historyMode = 'browser', routes = [], ...globalOptions } = options;
|
||||||
const matcher = createRouterMatcher(routes, globalOptions);
|
const matcher = createRouterMatcher(routes, globalOptions);
|
||||||
const routerHistory =
|
const routerHistory =
|
||||||
@ -72,8 +66,8 @@ export function createRouter(options: RouterOptions = defaultRouterOptions): Rou
|
|||||||
? createMemoryHistory(baseName)
|
? createMemoryHistory(baseName)
|
||||||
: createBrowserHistory(baseName);
|
: createBrowserHistory(baseName);
|
||||||
|
|
||||||
const beforeGuards = createEvent<NavigationGuard>();
|
const beforeGuards = createCallback<NavigationGuard>();
|
||||||
const afterGuards = createEvent<NavigationHookAfter>();
|
const afterGuards = createCallback<NavigationHookAfter>();
|
||||||
|
|
||||||
let currentLocation: RouteLocationNormalized = START_LOCATION;
|
let currentLocation: RouteLocationNormalized = START_LOCATION;
|
||||||
let pendingLocation = currentLocation;
|
let pendingLocation = currentLocation;
|
||||||
|
|||||||
@ -1,12 +1,12 @@
|
|||||||
import type {
|
import type { Spec, PlainObject } from '@alilc/lowcode-shared';
|
||||||
RouteRecord as RouterRecordSpec,
|
|
||||||
RouteLocation,
|
|
||||||
PlainObject,
|
|
||||||
RawRouteLocation,
|
|
||||||
} from '@alilc/lowcode-renderer-core';
|
|
||||||
import type { PathParserOptions } from './utils/path-parser';
|
import type { PathParserOptions } from './utils/path-parser';
|
||||||
|
|
||||||
export interface RouteRecord extends RouterRecordSpec, PathParserOptions {
|
export type RawRouteLocation = Spec.RawRouteLocation;
|
||||||
|
export type RouteLocation = Spec.RouteLocation;
|
||||||
|
export type RawLocation = Spec.RawLocation;
|
||||||
|
export type RawLocationOptions = Spec.RawLocationOptions;
|
||||||
|
|
||||||
|
export interface RouteRecord extends Spec.RouteRecord, PathParserOptions {
|
||||||
meta?: PlainObject;
|
meta?: PlainObject;
|
||||||
redirect?:
|
redirect?:
|
||||||
| string
|
| string
|
||||||
|
|||||||
30
packages/renderer-router/src/utils/callback.ts
Normal file
30
packages/renderer-router/src/utils/callback.ts
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import type { AnyFunction } from '@alilc/lowcode-shared';
|
||||||
|
|
||||||
|
export function createCallback<T = AnyFunction>() {
|
||||||
|
let events: T[] = [];
|
||||||
|
|
||||||
|
function add(fn: T) {
|
||||||
|
events.push(fn);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
events = events.filter((e) => e !== fn);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function remove(fn: T) {
|
||||||
|
events = events.filter((f) => fn !== f);
|
||||||
|
}
|
||||||
|
|
||||||
|
function list() {
|
||||||
|
return [...events];
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
add,
|
||||||
|
remove,
|
||||||
|
list,
|
||||||
|
clear() {
|
||||||
|
events.length = 0;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
@ -1,5 +1,4 @@
|
|||||||
import type { RawRouteLocation } from '@alilc/lowcode-renderer-core';
|
import type { RouteLocationNormalized, RawRouteLocation } from '../types';
|
||||||
import type { RouteLocationNormalized } from '../types';
|
|
||||||
|
|
||||||
export function isRouteLocation(route: any): route is RawRouteLocation {
|
export function isRouteLocation(route: any): route is RawRouteLocation {
|
||||||
return typeof route === 'string' || (route && typeof route === 'object');
|
return typeof route === 'string' || (route && typeof route === 'object');
|
||||||
|
|||||||
@ -8,26 +8,26 @@ export type PathParams = Record<string, string | string[]>;
|
|||||||
* A param in a url like `/users/:id`
|
* A param in a url like `/users/:id`
|
||||||
*/
|
*/
|
||||||
interface PathParserParamKey {
|
interface PathParserParamKey {
|
||||||
name: string
|
name: string;
|
||||||
repeatable: boolean
|
repeatable: boolean;
|
||||||
optional: boolean
|
optional: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PathParser {
|
export interface PathParser {
|
||||||
/**
|
/**
|
||||||
* The regexp used to match a url
|
* The regexp used to match a url
|
||||||
*/
|
*/
|
||||||
re: RegExp
|
re: RegExp;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The score of the parser
|
* The score of the parser
|
||||||
*/
|
*/
|
||||||
score: Array<number[]>
|
score: Array<number[]>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Keys that appeared in the path
|
* Keys that appeared in the path
|
||||||
*/
|
*/
|
||||||
keys: PathParserParamKey[]
|
keys: PathParserParamKey[];
|
||||||
/**
|
/**
|
||||||
* Parses a url and returns the matched params or null if it doesn't match. An
|
* Parses a url and returns the matched params or null if it doesn't match. An
|
||||||
* optional param that isn't preset will be an empty string. A repeatable
|
* optional param that isn't preset will be an empty string. A repeatable
|
||||||
@ -37,7 +37,7 @@ export interface PathParser {
|
|||||||
* @returns a Params object, empty if there are no params. `null` if there is
|
* @returns a Params object, empty if there are no params. `null` if there is
|
||||||
* no match
|
* no match
|
||||||
*/
|
*/
|
||||||
parse(path: string): PathParams | null
|
parse(path: string): PathParams | null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a string version of the url
|
* Creates a string version of the url
|
||||||
@ -45,26 +45,23 @@ export interface PathParser {
|
|||||||
* @param params - object of params
|
* @param params - object of params
|
||||||
* @returns a url
|
* @returns a url
|
||||||
*/
|
*/
|
||||||
stringify(params: PathParams): string
|
stringify(params: PathParams): string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @internal
|
|
||||||
*/
|
|
||||||
export interface _PathParserOptions {
|
export interface _PathParserOptions {
|
||||||
/**
|
/**
|
||||||
* Makes the RegExp case-sensitive.
|
* Makes the RegExp case-sensitive.
|
||||||
*
|
*
|
||||||
* @defaultValue `false`
|
* @defaultValue `false`
|
||||||
*/
|
*/
|
||||||
sensitive?: boolean
|
sensitive?: boolean;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether to disallow a trailing slash or not.
|
* Whether to disallow a trailing slash or not.
|
||||||
*
|
*
|
||||||
* @defaultValue `false`
|
* @defaultValue `false`
|
||||||
*/
|
*/
|
||||||
strict?: boolean
|
strict?: boolean;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Should the RegExp match from the beginning by prepending a `^` to it.
|
* Should the RegExp match from the beginning by prepending a `^` to it.
|
||||||
@ -72,20 +69,17 @@ export interface _PathParserOptions {
|
|||||||
*
|
*
|
||||||
* @defaultValue `true`
|
* @defaultValue `true`
|
||||||
*/
|
*/
|
||||||
start?: boolean
|
start?: boolean;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Should the RegExp match until the end by appending a `$` to it.
|
* Should the RegExp match until the end by appending a `$` to it.
|
||||||
*
|
*
|
||||||
* @defaultValue `true`
|
* @defaultValue `true`
|
||||||
*/
|
*/
|
||||||
end?: boolean
|
end?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type PathParserOptions = Pick<
|
export type PathParserOptions = Pick<_PathParserOptions, 'end' | 'sensitive' | 'strict'>;
|
||||||
_PathParserOptions,
|
|
||||||
'end' | 'sensitive' | 'strict'
|
|
||||||
>;
|
|
||||||
|
|
||||||
// default pattern for a param: non-greedy everything but /
|
// default pattern for a param: non-greedy everything but /
|
||||||
const BASE_PARAM_PATTERN = '[^/]+?';
|
const BASE_PARAM_PATTERN = '[^/]+?';
|
||||||
@ -126,7 +120,7 @@ const REGEX_CHARS_RE = /[.+*?^${}()[\]/\\]/g;
|
|||||||
*/
|
*/
|
||||||
export function tokensToParser(
|
export function tokensToParser(
|
||||||
segments: Array<Token[]>,
|
segments: Array<Token[]>,
|
||||||
extraOptions?: _PathParserOptions
|
extraOptions?: _PathParserOptions,
|
||||||
): PathParser {
|
): PathParser {
|
||||||
const options = Object.assign({}, BASE_PATH_PARSER_OPTIONS, extraOptions);
|
const options = Object.assign({}, BASE_PATH_PARSER_OPTIONS, extraOptions);
|
||||||
|
|
||||||
@ -147,8 +141,7 @@ export function tokensToParser(
|
|||||||
const token = segment[tokenIndex];
|
const token = segment[tokenIndex];
|
||||||
// resets the score if we are inside a sub-segment /:a-other-:b
|
// resets the score if we are inside a sub-segment /:a-other-:b
|
||||||
let subSegmentScore: number =
|
let subSegmentScore: number =
|
||||||
PathScore.Segment +
|
PathScore.Segment + (options.sensitive ? PathScore.BonusCaseSensitive : 0);
|
||||||
(options.sensitive ? PathScore.BonusCaseSensitive : 0);
|
|
||||||
|
|
||||||
if (token.type === TokenType.Static) {
|
if (token.type === TokenType.Static) {
|
||||||
// prepend the slash if we are starting a new segment
|
// prepend the slash if we are starting a new segment
|
||||||
@ -171,8 +164,7 @@ export function tokensToParser(
|
|||||||
new RegExp(`(${re})`);
|
new RegExp(`(${re})`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Invalid custom RegExp for param "${value}" (${re}): ` +
|
`Invalid custom RegExp for param "${value}" (${re}): ` + (err as Error).message,
|
||||||
(err as Error).message
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -185,9 +177,7 @@ export function tokensToParser(
|
|||||||
subPattern =
|
subPattern =
|
||||||
// avoid an optional / if there are more segments e.g. /:p?-static
|
// avoid an optional / if there are more segments e.g. /:p?-static
|
||||||
// or /:p?-:p2
|
// or /:p?-:p2
|
||||||
optional && segment.length < 2
|
optional && segment.length < 2 ? `(?:/${subPattern})` : '/' + subPattern;
|
||||||
? `(?:/${subPattern})`
|
|
||||||
: '/' + subPattern;
|
|
||||||
if (optional) subPattern += '?';
|
if (optional) subPattern += '?';
|
||||||
|
|
||||||
pattern += subPattern;
|
pattern += subPattern;
|
||||||
@ -250,12 +240,11 @@ export function tokensToParser(
|
|||||||
path += token.value;
|
path += token.value;
|
||||||
} else if (token.type === TokenType.Param) {
|
} else if (token.type === TokenType.Param) {
|
||||||
const { value, repeatable, optional } = token;
|
const { value, repeatable, optional } = token;
|
||||||
const param: string | readonly string[] =
|
const param: string | readonly string[] = value in params ? params[value] : '';
|
||||||
value in params ? params[value] : '';
|
|
||||||
|
|
||||||
if (Array.isArray(param) && !repeatable) {
|
if (Array.isArray(param) && !repeatable) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Provided param "${value}" is an array but it is not repeatable (* or + modifiers)`
|
`Provided param "${value}" is an array but it is not repeatable (* or + modifiers)`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -313,13 +302,9 @@ function compareScoreArray(a: number[], b: number[]): number {
|
|||||||
// if the last subsegment was Static, the shorter segments should be sorted first
|
// if the last subsegment was Static, the shorter segments should be sorted first
|
||||||
// otherwise sort the longest segment first
|
// otherwise sort the longest segment first
|
||||||
if (a.length < b.length) {
|
if (a.length < b.length) {
|
||||||
return a.length === 1 && a[0] === PathScore.Static + PathScore.Segment
|
return a.length === 1 && a[0] === PathScore.Static + PathScore.Segment ? -1 : 1;
|
||||||
? -1
|
|
||||||
: 1;
|
|
||||||
} else if (a.length > b.length) {
|
} else if (a.length > b.length) {
|
||||||
return b.length === 1 && b[0] === PathScore.Static + PathScore.Segment
|
return b.length === 1 && b[0] === PathScore.Static + PathScore.Segment ? 1 : -1;
|
||||||
? 1
|
|
||||||
: -1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
|
|||||||
@ -1,11 +1,27 @@
|
|||||||
{
|
{
|
||||||
"name": "@alilc/lowcode-shared",
|
"name": "@alilc/lowcode-shared",
|
||||||
"version": "2.0.0-beta.0",
|
"version": "1.0.0-alpha.0",
|
||||||
"private": true,
|
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"module": "src/index.ts",
|
"main": "dist/low-code-shared.js",
|
||||||
|
"module": "dist/low-code-shared.js",
|
||||||
|
"types": "dist/index.d.ts",
|
||||||
|
"files": [
|
||||||
|
"dist",
|
||||||
|
"src",
|
||||||
|
"package.json"
|
||||||
|
],
|
||||||
|
"scripts": {
|
||||||
|
"build:target": "vite build",
|
||||||
|
"build:dts": "tsc -p tsconfig.declaration.json && node ../../scripts/rollup-dts.js",
|
||||||
|
"test": "vitest --run",
|
||||||
|
"test:watch": "vitest"
|
||||||
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@abraham/reflection": "^0.12.0",
|
||||||
|
"@formatjs/intl": "^2.10.2",
|
||||||
"@vue/reactivity": "^3.4.23",
|
"@vue/reactivity": "^3.4.23",
|
||||||
|
"inversify": "^6.0.2",
|
||||||
|
"inversify-binding-decorators": "^4.0.0",
|
||||||
"hookable": "^5.5.3",
|
"hookable": "^5.5.3",
|
||||||
"lodash-es": "^4.17.21",
|
"lodash-es": "^4.17.21",
|
||||||
"store": "^2.0.12"
|
"store": "^2.0.12"
|
||||||
@ -13,5 +29,15 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/lodash-es": "^4.17.12",
|
"@types/lodash-es": "^4.17.12",
|
||||||
"@types/store": "^2.0.2"
|
"@types/store": "^2.0.2"
|
||||||
}
|
},
|
||||||
|
"publishConfig": {
|
||||||
|
"access": "public",
|
||||||
|
"registry": "https://registry.npmjs.org/"
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"type": "http",
|
||||||
|
"url": "https://github.com/alibaba/lowcode-engine/tree/main/packages/shared"
|
||||||
|
},
|
||||||
|
"bugs": "https://github.com/alibaba/lowcode-engine/issues",
|
||||||
|
"homepage": "https://github.com/alibaba/lowcode-engine/#readme"
|
||||||
}
|
}
|
||||||
|
|||||||
100
packages/shared/src/abilities/event.ts
Normal file
100
packages/shared/src/abilities/event.ts
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
import { Hookable, type HookKeys, type HookCallback } from 'hookable';
|
||||||
|
|
||||||
|
export type EventListener = HookCallback;
|
||||||
|
export type EventDisposable = () => void;
|
||||||
|
|
||||||
|
export interface IEventEmitter<
|
||||||
|
HooksT extends Record<string, any> = Record<string, HookCallback>,
|
||||||
|
HookNameT extends HookKeys<HooksT> = HookKeys<HooksT>,
|
||||||
|
> {
|
||||||
|
/**
|
||||||
|
* 监听事件
|
||||||
|
* add monitor to a event
|
||||||
|
* @param event 事件名称
|
||||||
|
* @param listener 事件回调
|
||||||
|
*/
|
||||||
|
on(event: HookNameT, listener: HooksT[HookNameT]): EventDisposable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加只运行一次的监听事件
|
||||||
|
* @param event 事件名称
|
||||||
|
* @param listener 事件回调
|
||||||
|
*/
|
||||||
|
once(event: HookNameT, listener: HooksT[HookNameT]): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 触发事件
|
||||||
|
* emit a message for a event
|
||||||
|
* @param event 事件名称
|
||||||
|
* @param args 事件参数
|
||||||
|
*/
|
||||||
|
emit(event: HookNameT, ...args: any): Promise<any>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 取消监听事件
|
||||||
|
* cancel a monitor from a event
|
||||||
|
* @param event 事件名称
|
||||||
|
* @param listener 事件回调
|
||||||
|
*/
|
||||||
|
off(event: HookNameT, listener: HooksT[HookNameT]): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 监听事件,会在其他回调函数之前执行
|
||||||
|
* @param event 事件名称
|
||||||
|
* @param listener 事件回调
|
||||||
|
*/
|
||||||
|
prependListener(event: HookNameT, listener: HooksT[HookNameT]): EventDisposable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清除所有事件监听
|
||||||
|
*/
|
||||||
|
removeAll(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class EventEmitter<
|
||||||
|
HooksT extends Record<string, any> = Record<string, HookCallback>,
|
||||||
|
HookNameT extends HookKeys<HooksT> = HookKeys<HooksT>,
|
||||||
|
> implements IEventEmitter<HooksT, HookNameT>
|
||||||
|
{
|
||||||
|
private namespace: string | undefined;
|
||||||
|
private hooks = new Hookable<HooksT, HookNameT>();
|
||||||
|
|
||||||
|
constructor(namespace?: string) {
|
||||||
|
this.namespace = namespace;
|
||||||
|
}
|
||||||
|
|
||||||
|
on(event: HookNameT, listener: HooksT[HookNameT]): EventDisposable {
|
||||||
|
return this.hooks.hook(event, listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
once(event: HookNameT, listener: HooksT[HookNameT]): void {
|
||||||
|
this.hooks.hookOnce(event, listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
async emit(event: HookNameT, ...args: any) {
|
||||||
|
return this.hooks.callHook(event, ...args);
|
||||||
|
}
|
||||||
|
|
||||||
|
off(event: HookNameT, listener: HooksT[HookNameT]): void {
|
||||||
|
this.hooks.removeHook(event, listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 监听事件,会在其他回调函数之前执行
|
||||||
|
* @param event 事件名称
|
||||||
|
* @param listener 事件回调
|
||||||
|
*/
|
||||||
|
prependListener(event: HookNameT, listener: HooksT[HookNameT]): EventDisposable {
|
||||||
|
return this.hooks.hook(`${event}:before` as HookNameT, listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
removeAll(): void {
|
||||||
|
this.hooks.removeAllHooks();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createEventEmitter<T extends Record<string, any>>(
|
||||||
|
namespace?: string,
|
||||||
|
): EventEmitter<T> {
|
||||||
|
return new EventEmitter<T>(namespace);
|
||||||
|
}
|
||||||
5
packages/shared/src/abilities/index.ts
Normal file
5
packages/shared/src/abilities/index.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
export * from './event';
|
||||||
|
export * from './logger';
|
||||||
|
export * from './storage';
|
||||||
|
export * from './intl';
|
||||||
|
export * from './instantiation';
|
||||||
59
packages/shared/src/abilities/instantiation/index.ts
Normal file
59
packages/shared/src/abilities/instantiation/index.ts
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
import '@abraham/reflection';
|
||||||
|
import { Container, inject, interfaces, injectable } from 'inversify';
|
||||||
|
import { fluentProvide, buildProviderModule } from 'inversify-binding-decorators';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Identifies a service of type `T`.
|
||||||
|
*/
|
||||||
|
export interface ServiceIdentifier<T> {
|
||||||
|
(...args: any[]): void;
|
||||||
|
type: T;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Constructor<T = any> = new (...args: any[]) => T;
|
||||||
|
|
||||||
|
export function createDecorator<T>(serviceId: string): ServiceIdentifier<T> {
|
||||||
|
const id = <any>(
|
||||||
|
function (target: Constructor, targetKey: string, indexOrPropertyDescriptor: any): any {
|
||||||
|
return inject(serviceId)(target, targetKey, indexOrPropertyDescriptor);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
id.toString = () => serviceId;
|
||||||
|
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Injectable = injectable;
|
||||||
|
|
||||||
|
export function Provide<T>(serviceId: ServiceIdentifier<T>, isSingleTon?: boolean) {
|
||||||
|
const ret = fluentProvide(serviceId.toString());
|
||||||
|
|
||||||
|
if (isSingleTon) {
|
||||||
|
return ret.inSingletonScope().done();
|
||||||
|
}
|
||||||
|
return ret.done();
|
||||||
|
}
|
||||||
|
|
||||||
|
export class InstantiationService {
|
||||||
|
private container: Container;
|
||||||
|
|
||||||
|
constructor(options?: interfaces.ContainerOptions) {
|
||||||
|
this.container = new Container(options);
|
||||||
|
}
|
||||||
|
|
||||||
|
get<T>(serviceIdentifier: ServiceIdentifier<T>) {
|
||||||
|
return this.container.get<T>(serviceIdentifier);
|
||||||
|
}
|
||||||
|
|
||||||
|
set<T>(serviceIdentifier: ServiceIdentifier<T>, constructor: Constructor<T>) {
|
||||||
|
this.container.bind<T>(serviceIdentifier).to(constructor);
|
||||||
|
}
|
||||||
|
|
||||||
|
createInstance<T extends Constructor>(App: T) {
|
||||||
|
return this.container.resolve<InstanceType<T>>(App);
|
||||||
|
}
|
||||||
|
|
||||||
|
bootstrapModules() {
|
||||||
|
this.container.load(buildProviderModule());
|
||||||
|
}
|
||||||
|
}
|
||||||
108
packages/shared/src/abilities/intl.ts
Normal file
108
packages/shared/src/abilities/intl.ts
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
import { createIntl, createIntlCache, type IntlShape as IntlFormatter } from '@formatjs/intl';
|
||||||
|
import { mapKeys } from 'lodash-es';
|
||||||
|
import { signal, computed, effect, type Signal, type ComputedSignal } from '../signals';
|
||||||
|
|
||||||
|
export { IntlFormatter };
|
||||||
|
|
||||||
|
export type Locale = string;
|
||||||
|
export type Translations = Record<string, string>;
|
||||||
|
export type LocaleTranslationsRecord = Record<Locale, Translations>;
|
||||||
|
|
||||||
|
export class Intl {
|
||||||
|
private locale: Signal<Locale>;
|
||||||
|
private messageStore: Signal<LocaleTranslationsRecord>;
|
||||||
|
private currentMessage: ComputedSignal<Translations>;
|
||||||
|
private intlShape: IntlFormatter;
|
||||||
|
|
||||||
|
constructor(defaultLocale?: string, messages: LocaleTranslationsRecord = {}) {
|
||||||
|
if (defaultLocale) {
|
||||||
|
defaultLocale = nomarlizeLocale(defaultLocale);
|
||||||
|
} else {
|
||||||
|
defaultLocale = 'zh-CN';
|
||||||
|
}
|
||||||
|
|
||||||
|
const messageStore = mapKeys(messages, (_, key) => {
|
||||||
|
return nomarlizeLocale(key);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.locale = signal(defaultLocale);
|
||||||
|
this.messageStore = signal(messageStore);
|
||||||
|
this.currentMessage = computed(() => {
|
||||||
|
return this.messageStore.value[this.locale.value] ?? {};
|
||||||
|
});
|
||||||
|
|
||||||
|
effect(() => {
|
||||||
|
const cache = createIntlCache();
|
||||||
|
this.intlShape = createIntl(
|
||||||
|
{
|
||||||
|
locale: this.locale.value,
|
||||||
|
messages: this.currentMessage.value,
|
||||||
|
},
|
||||||
|
cache,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getLocale() {
|
||||||
|
return this.locale.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
setLocale(locale: Locale) {
|
||||||
|
const nomarlizedLocale = nomarlizeLocale(locale);
|
||||||
|
this.locale.value = nomarlizedLocale;
|
||||||
|
}
|
||||||
|
|
||||||
|
addTranslations(locale: Locale, messages: Translations) {
|
||||||
|
locale = nomarlizeLocale(locale);
|
||||||
|
const original = this.messageStore.value[locale];
|
||||||
|
|
||||||
|
this.messageStore.value[locale] = Object.assign(original, messages);
|
||||||
|
}
|
||||||
|
|
||||||
|
getFormatter(): IntlFormatter {
|
||||||
|
return this.intlShape;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const navigatorLanguageMapping: Record<string, string> = {
|
||||||
|
en: 'en-US',
|
||||||
|
zh: 'zh-CN',
|
||||||
|
zt: 'zh-TW',
|
||||||
|
es: 'es-ES',
|
||||||
|
pt: 'pt-PT',
|
||||||
|
fr: 'fr-FR',
|
||||||
|
de: 'de-DE',
|
||||||
|
it: 'it-IT',
|
||||||
|
ru: 'ru-RU',
|
||||||
|
ja: 'ja-JP',
|
||||||
|
ko: 'ko-KR',
|
||||||
|
ar: 'ar-SA',
|
||||||
|
tr: 'tr-TR',
|
||||||
|
th: 'th-TH',
|
||||||
|
vi: 'vi-VN',
|
||||||
|
nl: 'nl-NL',
|
||||||
|
he: 'iw-IL',
|
||||||
|
id: 'in-ID',
|
||||||
|
pl: 'pl-PL',
|
||||||
|
hi: 'hi-IN',
|
||||||
|
uk: 'uk-UA',
|
||||||
|
ms: 'ms-MY',
|
||||||
|
tl: 'tl-PH',
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* nomarlize navigator.language or user input's locale
|
||||||
|
* eg: zh -> zh-CN, zh_CN -> zh-CN, zh-cn -> zh-CN
|
||||||
|
* @param target
|
||||||
|
*/
|
||||||
|
function nomarlizeLocale(target: Locale) {
|
||||||
|
if (navigatorLanguageMapping[target]) {
|
||||||
|
return navigatorLanguageMapping[target];
|
||||||
|
}
|
||||||
|
|
||||||
|
const replaced = target.replace('_', '-');
|
||||||
|
const splited = replaced.split('-').slice(0, 2);
|
||||||
|
splited[1] = splited[1].toUpperCase();
|
||||||
|
|
||||||
|
return splited.join('-');
|
||||||
|
}
|
||||||
151
packages/shared/src/abilities/storage.ts
Normal file
151
packages/shared/src/abilities/storage.ts
Normal file
@ -0,0 +1,151 @@
|
|||||||
|
import { invariant } from '../utils';
|
||||||
|
import { PlainObject } from '../types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MapLike interface
|
||||||
|
*/
|
||||||
|
export interface IStore<O, K extends keyof O> {
|
||||||
|
readonly size: number;
|
||||||
|
|
||||||
|
get(key: K, defaultValue: O[K]): O[K];
|
||||||
|
get(key: K, defaultValue?: O[K]): O[K] | undefined;
|
||||||
|
|
||||||
|
set(key: K, value: O[K]): void;
|
||||||
|
|
||||||
|
delete(key: K): void;
|
||||||
|
|
||||||
|
clear(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 统一存储接口
|
||||||
|
*/
|
||||||
|
export class KeyValueStore<O = PlainObject, K extends keyof O = keyof O> {
|
||||||
|
private setterValidation: ((key: K, value: O[K]) => boolean | string) | undefined;
|
||||||
|
|
||||||
|
private waits = new Map<
|
||||||
|
K,
|
||||||
|
{
|
||||||
|
once?: boolean;
|
||||||
|
resolve: (data: any) => void;
|
||||||
|
}[]
|
||||||
|
>();
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly store: IStore<O, K> = new Map(),
|
||||||
|
options?: {
|
||||||
|
setterValidation?: (key: K, value: O[K]) => boolean | string;
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
if (options?.setterValidation) {
|
||||||
|
this.setterValidation = options.setterValidation;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get(key: K, defaultValue: O[K]): O[K];
|
||||||
|
get(key: K, defaultValue?: O[K] | undefined): O[K] | undefined;
|
||||||
|
get(key: K, defaultValue?: O[K]): O[K] | undefined {
|
||||||
|
const value = this.store.get(key, defaultValue);
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
set(key: K, value: O[K]): void {
|
||||||
|
if (this.setterValidation) {
|
||||||
|
const valid = this.setterValidation(key, value);
|
||||||
|
|
||||||
|
invariant(
|
||||||
|
valid === false || typeof valid === 'string',
|
||||||
|
`failed to config ${key.toString()}, only predefined options can be set under strict mode, predefined options: ${valid ? valid : ''}`,
|
||||||
|
'KeyValueStore',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.store.set(key, value);
|
||||||
|
this.dispatchValue(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
delete(key: K): void {
|
||||||
|
this.store.delete(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
clear(): void {
|
||||||
|
this.store.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
get size(): number {
|
||||||
|
return this.store.size;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取指定 key 的值,若此时还未赋值,则等待,若已有值,则直接返回值
|
||||||
|
* 注:此函数返回 Promise 实例,只会执行(fullfill)一次
|
||||||
|
* @param key
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
waitForValue(key: K) {
|
||||||
|
const val = this.get(key);
|
||||||
|
if (val !== undefined) {
|
||||||
|
return Promise.resolve(val);
|
||||||
|
}
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
this.addWaiter(key, resolve, true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取指定 key 的值,函数回调模式,若多次被赋值,回调会被多次调用
|
||||||
|
* @param key
|
||||||
|
* @param fn
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
onValueChange<T extends K>(key: T, fn: (value: O[T]) => void): () => void {
|
||||||
|
const val = this.get(key);
|
||||||
|
if (val !== undefined) {
|
||||||
|
// @ts-expect-error: val is not undefined
|
||||||
|
fn(val);
|
||||||
|
}
|
||||||
|
this.addWaiter(key, fn as any);
|
||||||
|
return () => {
|
||||||
|
this.removeWaiter(key, fn as any);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private dispatchValue(key: K): void {
|
||||||
|
const waits = this.waits.get(key);
|
||||||
|
if (!waits) return;
|
||||||
|
|
||||||
|
for (let i = waits.length - 1; i >= 0; i--) {
|
||||||
|
const waiter = waits[i];
|
||||||
|
waiter.resolve(this.get(key)!);
|
||||||
|
if (waiter.once) {
|
||||||
|
waits.splice(i, 1); // Remove the waiter if it only waits once
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (waits.length === 0) {
|
||||||
|
this.waits.delete(key); // No more waiters for the key
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private addWaiter(key: K, resolve: (value: O[K]) => void, once?: boolean) {
|
||||||
|
if (this.waits.has(key)) {
|
||||||
|
this.waits.get(key)!.push({ resolve, once });
|
||||||
|
} else {
|
||||||
|
this.waits.set(key, [{ resolve, once }]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private removeWaiter(key: K, resolve: (value: O[K]) => void) {
|
||||||
|
const waits = this.waits.get(key);
|
||||||
|
if (!waits) return;
|
||||||
|
|
||||||
|
this.waits.set(
|
||||||
|
key,
|
||||||
|
waits.filter((waiter) => waiter.resolve !== resolve),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (this.waits.get(key)!.length === 0) {
|
||||||
|
this.waits.delete(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,4 +1,4 @@
|
|||||||
export * from './types';
|
export * from './types';
|
||||||
export * from './utils';
|
export * from './utils';
|
||||||
export * from './signals';
|
export * from './signals';
|
||||||
export * from './parts';
|
export * from './abilities';
|
||||||
|
|||||||
@ -1,74 +0,0 @@
|
|||||||
import { Hookable, type HookKeys, type HookCallback } from 'hookable';
|
|
||||||
|
|
||||||
export type EventListener = HookCallback;
|
|
||||||
export type EventDisposable = () => void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* todo: logger
|
|
||||||
*/
|
|
||||||
export class EventEmitter<
|
|
||||||
HooksT extends Record<string, any> = Record<string, HookCallback>,
|
|
||||||
HookNameT extends HookKeys<HooksT> = HookKeys<HooksT>,
|
|
||||||
> extends Hookable<HooksT, HookNameT> {
|
|
||||||
#namespace: string | undefined;
|
|
||||||
|
|
||||||
constructor(namespace?: string) {
|
|
||||||
super();
|
|
||||||
this.#namespace = namespace;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 监听事件
|
|
||||||
* add monitor to a event
|
|
||||||
* @param event 事件名称
|
|
||||||
* @param listener 事件回调
|
|
||||||
*/
|
|
||||||
on(event: HookNameT, listener: HooksT[HookNameT]): EventDisposable {
|
|
||||||
return this.hook(event, listener);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 触发事件
|
|
||||||
* emit a message for a event
|
|
||||||
* @param event 事件名称
|
|
||||||
* @param args 事件参数
|
|
||||||
*/
|
|
||||||
async emit(event: HookNameT, ...args: any) {
|
|
||||||
return this.callHook(event, ...args);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 取消监听事件
|
|
||||||
* cancel a monitor from a event
|
|
||||||
* @param event 事件名称
|
|
||||||
* @param listener 事件回调
|
|
||||||
*/
|
|
||||||
off(event: HookNameT, listener: HooksT[HookNameT]): void {
|
|
||||||
this.removeHook(event, listener);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 监听事件,会在其他回调函数之前执行
|
|
||||||
* @param event 事件名称
|
|
||||||
* @param listener 事件回调
|
|
||||||
*/
|
|
||||||
prependListener(event: HookNameT, listener: HooksT[HookNameT]): EventDisposable {
|
|
||||||
const _hooks = (this as any)._hooks;
|
|
||||||
const hooks = _hooks[event];
|
|
||||||
|
|
||||||
if (Array.isArray(hooks)) {
|
|
||||||
hooks.unshift(listener);
|
|
||||||
return () => {
|
|
||||||
if (listener) {
|
|
||||||
this.removeHook(event, listener);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
return this.hook(event, listener);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createEventBus<T extends Record<string, any>>(namespace?: string): EventBus<T> {
|
|
||||||
return new EventBus<T>(namespace);
|
|
||||||
}
|
|
||||||
@ -1,3 +0,0 @@
|
|||||||
export * from './event';
|
|
||||||
export * from './logger';
|
|
||||||
export * from './persistence';
|
|
||||||
@ -1,41 +0,0 @@
|
|||||||
import { createStore } from 'store';
|
|
||||||
|
|
||||||
export type StorageValue = string | boolean | number | undefined | null | object;
|
|
||||||
|
|
||||||
export interface IPersistence {
|
|
||||||
get(key: string, fallbackValue: string): string;
|
|
||||||
get(key: string, fallbackValue?: string): string | undefined;
|
|
||||||
|
|
||||||
set(key: string, value: StorageValue): void;
|
|
||||||
|
|
||||||
delete(key: string): void;
|
|
||||||
|
|
||||||
clear(): void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class PersistenceStore implements IPersistence {
|
|
||||||
#store: ReturnType<typeof createStore>;
|
|
||||||
|
|
||||||
constructor(namespace?: string) {
|
|
||||||
this.#store = store.createStore([], namespace);
|
|
||||||
}
|
|
||||||
|
|
||||||
get(key: string, fallbackValue: string): string;
|
|
||||||
get(key: string, fallbackValue?: string | undefined): string | undefined;
|
|
||||||
get(key: string, fallbackValue?: unknown): string | undefined {
|
|
||||||
const value = store.get(key, fallbackValue);
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
set(key: string, value: StorageValue): void {
|
|
||||||
this.#store.set(key, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
delete(key: string): void {
|
|
||||||
this.#store.remove(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
clear(): void {
|
|
||||||
this.#store.clearAll();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -32,7 +32,7 @@ export type WatchCallback<V = any, OV = any> = (
|
|||||||
onCleanup: OnCleanup,
|
onCleanup: OnCleanup,
|
||||||
) => any;
|
) => any;
|
||||||
|
|
||||||
type OnCleanup = (cleanupFn: () => void) => void;
|
export type OnCleanup = (cleanupFn: () => void) => void;
|
||||||
|
|
||||||
export interface WatchOptions<Immediate = boolean> {
|
export interface WatchOptions<Immediate = boolean> {
|
||||||
immediate?: Immediate;
|
immediate?: Immediate;
|
||||||
@ -42,7 +42,7 @@ export interface WatchOptions<Immediate = boolean> {
|
|||||||
|
|
||||||
const INITIAL_WATCHER_VALUE = {};
|
const INITIAL_WATCHER_VALUE = {};
|
||||||
|
|
||||||
type MultiWatchSources = (WatchSource<unknown> | object)[];
|
export type MultiWatchSources = (WatchSource<unknown> | object)[];
|
||||||
|
|
||||||
export type WatchStopHandle = () => void;
|
export type WatchStopHandle = () => void;
|
||||||
|
|
||||||
@ -51,7 +51,7 @@ export function watchEffect(effect: WatchEffect): WatchStopHandle {
|
|||||||
return doWatch(effect, null);
|
return doWatch(effect, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
type MapSources<T, Immediate> = {
|
export type MapSources<T, Immediate> = {
|
||||||
[K in keyof T]: T[K] extends WatchSource<infer V>
|
[K in keyof T]: T[K] extends WatchSource<infer V>
|
||||||
? Immediate extends true
|
? Immediate extends true
|
||||||
? V | undefined
|
? V | undefined
|
||||||
|
|||||||
@ -1,5 +0,0 @@
|
|||||||
export type VoidFunction = (...args: any[]) => void;
|
|
||||||
|
|
||||||
export type AnyFunction = (...args: any[]) => any;
|
|
||||||
|
|
||||||
export type PlainObject = Record<string, any>;
|
|
||||||
@ -1,6 +1,11 @@
|
|||||||
export * from './base';
|
import * as Spec from './specs';
|
||||||
|
|
||||||
|
export { Spec };
|
||||||
|
|
||||||
export * from './material';
|
export * from './material';
|
||||||
export * from './specs/asset-spec';
|
|
||||||
export * from './specs/lowcode-spec';
|
export type VoidFunction = (...args: any[]) => void;
|
||||||
export * from './specs/runtime-api';
|
|
||||||
export * from './specs/material-spec';
|
export type AnyFunction = (...args: any[]) => any;
|
||||||
|
|
||||||
|
export type PlainObject = Record<string, any>;
|
||||||
|
|||||||
4
packages/shared/src/types/specs/index.ts
Normal file
4
packages/shared/src/types/specs/index.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export * from './asset-spec';
|
||||||
|
export * from './lowcode-spec';
|
||||||
|
export * from './runtime';
|
||||||
|
export * from './material-spec';
|
||||||
@ -27,7 +27,7 @@ export interface Project {
|
|||||||
/**
|
/**
|
||||||
* 国际化语料
|
* 国际化语料
|
||||||
*/
|
*/
|
||||||
i18n?: I18nMap;
|
i18n?: LocaleTranslationsMap;
|
||||||
/**
|
/**
|
||||||
* 应用范围内的全局常量
|
* 应用范围内的全局常量
|
||||||
*/
|
*/
|
||||||
@ -40,16 +40,16 @@ export interface Project {
|
|||||||
/**
|
/**
|
||||||
* 当前应用配置信息
|
* 当前应用配置信息
|
||||||
*/
|
*/
|
||||||
config?: Record<string, JSONValue>;
|
config?: Record<string, JSONObject>;
|
||||||
/**
|
/**
|
||||||
* 当前应用元数据信息
|
* 当前应用元数据信息
|
||||||
*/
|
*/
|
||||||
meta?: Record<string, JSONValue>;
|
meta?: Record<string, JSONObject>;
|
||||||
/**
|
/**
|
||||||
* 当前应用的公共数据源
|
* 当前应用的公共数据源
|
||||||
* @deprecated
|
* @deprecated
|
||||||
*/
|
*/
|
||||||
dataSource?: never;
|
// dataSource?: never;
|
||||||
/**
|
/**
|
||||||
* 当前应用的路由配置信息
|
* 当前应用的路由配置信息
|
||||||
*/
|
*/
|
||||||
@ -103,15 +103,13 @@ export interface ComponentMap {
|
|||||||
* 组件树描述
|
* 组件树描述
|
||||||
* 协议中用于描述搭建出来的组件树结构的规范,整个组件树的描述由组件结构&容器结构两种结构嵌套构成。
|
* 协议中用于描述搭建出来的组件树结构的规范,整个组件树的描述由组件结构&容器结构两种结构嵌套构成。
|
||||||
*/
|
*/
|
||||||
export type ComponentTree<LifeCycleNameT extends string = string> =
|
export type ComponentTree = ComponentTreeRoot;
|
||||||
ComponentTreeRootNode<LifeCycleNameT>;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 根容器节点结构描述 (A)
|
* 根容器节点结构描述 (A)
|
||||||
* 容器是一类特殊的组件,在组件能力基础上增加了对生命周期对象、自定义方法、样式文件、数据源等信息的描述。
|
* 容器是一类特殊的组件,在组件能力基础上增加了对生命周期对象、自定义方法、样式文件、数据源等信息的描述。
|
||||||
*/
|
*/
|
||||||
export interface ComponentTreeRootNode<LifeCycleNameT extends string = string>
|
export interface ComponentTreeRoot extends ComponentNode {
|
||||||
extends ComponentTreeNode {
|
|
||||||
componentName: 'Page' | 'Block' | 'Component';
|
componentName: 'Page' | 'Block' | 'Component';
|
||||||
/**
|
/**
|
||||||
* 文件名称
|
* 文件名称
|
||||||
@ -129,7 +127,7 @@ export interface ComponentTreeRootNode<LifeCycleNameT extends string = string>
|
|||||||
* 生命周期对象
|
* 生命周期对象
|
||||||
*/
|
*/
|
||||||
lifeCycles?: {
|
lifeCycles?: {
|
||||||
[name in LifeCycleNameT]: JSFunction;
|
[name in ComponentLifeCycle]: JSFunction;
|
||||||
};
|
};
|
||||||
/**
|
/**
|
||||||
* 自定义方法对象
|
* 自定义方法对象
|
||||||
@ -141,7 +139,7 @@ export interface ComponentTreeRootNode<LifeCycleNameT extends string = string>
|
|||||||
* 数据源对象
|
* 数据源对象
|
||||||
* type todo
|
* type todo
|
||||||
*/
|
*/
|
||||||
dataSource?: any;
|
dataSource?: ComponentDataSource;
|
||||||
|
|
||||||
// for useless
|
// for useless
|
||||||
loop: never;
|
loop: never;
|
||||||
@ -149,10 +147,109 @@ export interface ComponentTreeRootNode<LifeCycleNameT extends string = string>
|
|||||||
condition: never;
|
condition: never;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type ComponentLifeCycle =
|
||||||
|
| 'constructor'
|
||||||
|
| 'render'
|
||||||
|
| 'componentDidMount'
|
||||||
|
| 'componentDidUpdate'
|
||||||
|
| 'componentWillUnmount'
|
||||||
|
| 'componentDidCatch';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 组件数据源描述
|
||||||
|
*/
|
||||||
|
export interface ComponentDataSource {
|
||||||
|
/**
|
||||||
|
* 数据源列表
|
||||||
|
*/
|
||||||
|
list: ComponentDataSourceItem[];
|
||||||
|
/**
|
||||||
|
* 所有请求数据的处理函数
|
||||||
|
*/
|
||||||
|
dataHandler: JSFunction;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 请求配置
|
||||||
|
*/
|
||||||
|
export interface ComponentDataSourceItem {
|
||||||
|
/**
|
||||||
|
* 数据请求 ID 标识
|
||||||
|
*/
|
||||||
|
id: string;
|
||||||
|
/**
|
||||||
|
* 是否为初始数据
|
||||||
|
* 值为 true 时,将在组件初始化渲染时自动发送当前数据请求
|
||||||
|
*/
|
||||||
|
isInit: boolean | JSExpression;
|
||||||
|
/**
|
||||||
|
* 是否需要串行执行
|
||||||
|
* 值为 true 时,当前请求将被串行执行
|
||||||
|
*/
|
||||||
|
isSync: boolean | JSExpression;
|
||||||
|
/**
|
||||||
|
* 数据请求类型
|
||||||
|
*/
|
||||||
|
type: string;
|
||||||
|
/**
|
||||||
|
* 自定义扩展的外部请求处理器
|
||||||
|
*/
|
||||||
|
requestHandler?: JSFunction;
|
||||||
|
/**
|
||||||
|
* request 成功后的回调函数
|
||||||
|
* 参数为请求成功后 promise 的 value 值
|
||||||
|
*/
|
||||||
|
dataHandler?: JSFunction;
|
||||||
|
/**
|
||||||
|
* request 失败后的回调函数
|
||||||
|
* 参数为请求出错 promise 的 error 内容
|
||||||
|
*/
|
||||||
|
errorHandler?: JSFunction;
|
||||||
|
/**
|
||||||
|
* 请求配置参数
|
||||||
|
*/
|
||||||
|
options?: ComponentDataSourceItemOptions;
|
||||||
|
|
||||||
|
[otherKey: string]: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 请求配置参数
|
||||||
|
*/
|
||||||
|
export interface ComponentDataSourceItemOptions {
|
||||||
|
/**
|
||||||
|
* 请求地址
|
||||||
|
*/
|
||||||
|
uri: string | JSExpression;
|
||||||
|
/**
|
||||||
|
* 请求参数
|
||||||
|
*/
|
||||||
|
params?: JSONObject | JSExpression;
|
||||||
|
/**
|
||||||
|
* 请求方法
|
||||||
|
*/
|
||||||
|
method?: string | JSExpression;
|
||||||
|
/**
|
||||||
|
* 是否支持跨域
|
||||||
|
* 对应 credentials = 'include'
|
||||||
|
*/
|
||||||
|
isCors?: boolean | JSExpression;
|
||||||
|
/**
|
||||||
|
* 超时时长
|
||||||
|
*/
|
||||||
|
timeout?: number | JSExpression;
|
||||||
|
/**
|
||||||
|
* 请求头信息
|
||||||
|
*/
|
||||||
|
headers?: JSONObject | JSExpression;
|
||||||
|
|
||||||
|
[option: string]: any;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 组件结构描述(A)
|
* 组件结构描述(A)
|
||||||
*/
|
*/
|
||||||
export interface ComponentTreeNode {
|
export interface ComponentNode {
|
||||||
/**
|
/**
|
||||||
* 组件唯一标识
|
* 组件唯一标识
|
||||||
*/
|
*/
|
||||||
@ -164,7 +261,7 @@ export interface ComponentTreeNode {
|
|||||||
/**
|
/**
|
||||||
* 组件属性对象
|
* 组件属性对象
|
||||||
*/
|
*/
|
||||||
props?: ComponentTreeNodeProps;
|
props?: ComponentNodeProps;
|
||||||
/**
|
/**
|
||||||
* 选填,根据表达式结果判断是否渲染物料;
|
* 选填,根据表达式结果判断是否渲染物料;
|
||||||
*/
|
*/
|
||||||
@ -186,7 +283,7 @@ export interface ComponentTreeNode {
|
|||||||
/**
|
/**
|
||||||
* Props 结构描述
|
* Props 结构描述
|
||||||
*/
|
*/
|
||||||
export interface ComponentTreeNodeProps {
|
export interface ComponentNodeProps {
|
||||||
/** 组件 ID */
|
/** 组件 ID */
|
||||||
id?: string | JSExpression;
|
id?: string | JSExpression;
|
||||||
/** 组件样式类名 */
|
/** 组件样式类名 */
|
||||||
@ -221,8 +318,12 @@ export type Util = NPMUtil | FunctionUtil;
|
|||||||
* https://lowcode-engine.cn/site/docs/specs/lowcode-spec#25-%E5%9B%BD%E9%99%85%E5%8C%96%E5%A4%9A%E8%AF%AD%E8%A8%80%E6%94%AF%E6%8C%81aa
|
* https://lowcode-engine.cn/site/docs/specs/lowcode-spec#25-%E5%9B%BD%E9%99%85%E5%8C%96%E5%A4%9A%E8%AF%AD%E8%A8%80%E6%94%AF%E6%8C%81aa
|
||||||
* 国际化多语言支持
|
* 国际化多语言支持
|
||||||
*/
|
*/
|
||||||
export interface I18nMap {
|
export interface I18nTranslations {
|
||||||
[locale: string]: Record<string, string>;
|
[key: string]: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LocaleTranslationsMap {
|
||||||
|
[locale: string]: I18nTranslations;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -316,11 +417,11 @@ export interface JSONObject {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 节点类型(A)
|
* 节点类型(A)
|
||||||
* 通常用于描述组件的某一个属性为 ReactNode 或 Function-Return-ReactNode 的场景。
|
* 通常用于描述组件的某一个属性为 Node 或 Function-Return-Node 的场景。
|
||||||
*/
|
*/
|
||||||
export interface JSSlot {
|
export interface JSSlot {
|
||||||
type: 'JSSlot';
|
type: 'JSSlot';
|
||||||
value: ComponentTreeNode | ComponentTreeNode[];
|
value: ComponentNode | ComponentNode[];
|
||||||
params?: string[];
|
params?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -343,7 +444,7 @@ export interface JSExpression {
|
|||||||
/**
|
/**
|
||||||
* 国际化多语言类型(AA)
|
* 国际化多语言类型(AA)
|
||||||
*/
|
*/
|
||||||
export interface I18nNode {
|
export interface JSI18n {
|
||||||
type: 'i18n';
|
type: 'i18n';
|
||||||
/**
|
/**
|
||||||
* i18n 结构中字段的 key 标识符
|
* i18n 结构中字段的 key 标识符
|
||||||
@ -355,4 +456,4 @@ export interface I18nNode {
|
|||||||
params?: Record<string, string | number | JSExpression>;
|
params?: Record<string, string | number | JSExpression>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type NodeType = string | JSExpression | I18nNode | ComponentTreeNode;
|
export type NodeType = string | JSExpression | JSI18n | ComponentNode;
|
||||||
|
|||||||
@ -3,8 +3,8 @@
|
|||||||
* 对源码组件在低代码搭建平台中使用时所具备的配置能力和交互行为进行规范化描述,让不同平台对组件接入的实现保持一致,
|
* 对源码组件在低代码搭建平台中使用时所具备的配置能力和交互行为进行规范化描述,让不同平台对组件接入的实现保持一致,
|
||||||
* 让组件针对不同的搭建平台接入时可以使用一份统一的描述内容,让组件在不同的业务中流通成为可能。
|
* 让组件针对不同的搭建平台接入时可以使用一份统一的描述内容,让组件在不同的业务中流通成为可能。
|
||||||
*/
|
*/
|
||||||
import { ComponentTree, ComponentTreeNode } from './lowcode-spec';
|
import { ComponentTree, ComponentNode } from './lowcode-spec';
|
||||||
import { PlainObject } from '../base';
|
import { PlainObject } from '../index';
|
||||||
|
|
||||||
export interface LowCodeComponentTree extends ComponentTree {
|
export interface LowCodeComponentTree extends ComponentTree {
|
||||||
componentName: 'Component';
|
componentName: 'Component';
|
||||||
@ -230,5 +230,5 @@ export interface Snippet {
|
|||||||
/**
|
/**
|
||||||
* 待插入的 schema
|
* 待插入的 schema
|
||||||
*/
|
*/
|
||||||
schema?: ComponentTreeNode;
|
schema?: ComponentNode;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import { AnyFunction, PlainObject } from '../base';
|
import { AnyFunction, PlainObject } from '../index';
|
||||||
|
import { JSExpression } from './lowcode-spec';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 在上述事件类型描述和变量类型描述中,在函数或 JS 表达式内,均可以通过 this 对象获取当前组件所在容器的实例化对象
|
* 在上述事件类型描述和变量类型描述中,在函数或 JS 表达式内,均可以通过 this 对象获取当前组件所在容器的实例化对象
|
||||||
@ -39,17 +40,48 @@ export interface InstanceStateApi<S = PlainObject> {
|
|||||||
): void;
|
): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 数据源 api
|
||||||
|
*/
|
||||||
export interface InstanceDataSourceApi {
|
export interface InstanceDataSourceApi {
|
||||||
/**
|
/**
|
||||||
* 实例的数据源对象 Map
|
* 实例的数据源对象 Map
|
||||||
*/
|
*/
|
||||||
dataSourceMap: any;
|
dataSourceMap: Record<string, DataSourceMapItem>;
|
||||||
/**
|
/**
|
||||||
* 实例的初始化异步数据请求重载
|
* 实例的初始化异步数据请求重载
|
||||||
*/
|
*/
|
||||||
reloadDataSource: () => void;
|
reloadDataSource: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 实例的单个数据源对象
|
||||||
|
*/
|
||||||
|
export interface DataSourceMapItem<T = any> {
|
||||||
|
/**
|
||||||
|
* 调用单个数据源
|
||||||
|
* @param params 替换 ComponentDataSourceItemOptions 对象描述中的 params
|
||||||
|
*/
|
||||||
|
load(params: any): Promise<T>;
|
||||||
|
/**
|
||||||
|
* 数据源请求的返回状态
|
||||||
|
*/
|
||||||
|
status: DataSourceMapItemStatus;
|
||||||
|
/**
|
||||||
|
* 请求成功后的返回数据
|
||||||
|
*/
|
||||||
|
data: T | undefined;
|
||||||
|
/**
|
||||||
|
* 请求失败的错误对象
|
||||||
|
*/
|
||||||
|
error: Error | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 数据源请求的返回状态
|
||||||
|
*/
|
||||||
|
export type DataSourceMapItemStatus = 'loading' | 'loaded' | 'error' | 'init';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 应用级别的公共函数或第三方扩展
|
* 应用级别的公共函数或第三方扩展
|
||||||
*/
|
*/
|
||||||
@ -66,7 +98,7 @@ export interface IntlApi {
|
|||||||
* @param i18nKey 语料的标识符
|
* @param i18nKey 语料的标识符
|
||||||
* @param params 可选,是用来做模版字符串替换
|
* @param params 可选,是用来做模版字符串替换
|
||||||
*/
|
*/
|
||||||
i18n(i18nKey: string, params?: Record<string, string>): string;
|
i18n(key: string, params?: Record<string, string>): string;
|
||||||
/**
|
/**
|
||||||
* 返回当前环境语言
|
* 返回当前环境语言
|
||||||
*/
|
*/
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user