From d632e7f7e6567d7541e36805d0b9aa1d1c9d280a Mon Sep 17 00:00:00 2001 From: 1ncounter <1ncounter.100@gmail.com> Date: Tue, 28 May 2024 11:07:01 +0800 Subject: [PATCH] refactor: render-core --- .npmrc | 2 +- package.json | 6 +- packages/core/package.json | 3 - .../config.ts => configuration.ts} | 86 ++-- packages/core/src/configuration/index.ts | 2 - packages/core/src/index.ts | 2 +- packages/core/src/instantiation/index.ts | 43 -- packages/core/src/intl.ts | 24 +- .../src/{configuration => }/preference.ts | 2 - packages/core/tsconfig.json | 2 +- .../src/models/document/document-model.ts | 4 +- packages/designer/src/models/node/node.ts | 6 +- packages/react-renderer/package.json | 4 +- packages/react-renderer/src/api/app.tsx | 117 ++--- packages/react-renderer/src/api/component.tsx | 35 +- packages/react-renderer/src/component.tsx | 425 ------------------ .../react-renderer/src/components/app.tsx | 48 +- .../react-renderer/src/components/outlet.tsx | 32 -- .../react-renderer/src/components/route.tsx | 44 +- .../{router-view.tsx => routerView.tsx} | 6 +- packages/react-renderer/src/context/app.ts | 13 - packages/react-renderer/src/context/render.ts | 8 + packages/react-renderer/src/context/router.ts | 18 +- packages/react-renderer/src/index.ts | 6 + packages/react-renderer/src/plugin.ts | 50 +++ packages/react-renderer/src/renderer.ts | 60 --- .../src/runtime-api/intl/index.tsx | 33 -- .../src/runtime-api/intl/parser.ts | 92 ---- .../react-renderer/src/runtime-api/utils.ts | 72 --- .../react-renderer/src/runtime/dataSource.ts | 1 + packages/react-renderer/src/runtime/index.tsx | 382 ++++++++++++++++ .../src/runtime/reactiveState.ts | 22 + packages/react-renderer/src/signals.ts | 122 ----- packages/react-renderer/src/utils/node.ts | 14 + .../react-renderer/src/utils/reactive.tsx | 27 +- packages/renderer-core/src/api/app.ts | 106 ----- packages/renderer-core/src/api/component.ts | 43 -- packages/renderer-core/src/apiCreate.ts | 27 ++ packages/renderer-core/src/boosts.ts | 42 -- packages/renderer-core/src/code-runtime.ts | 142 ------ packages/renderer-core/src/container.ts | 171 ------- packages/renderer-core/src/index.ts | 29 +- packages/renderer-core/src/main.ts | 86 ++++ packages/renderer-core/src/package.ts | 157 ------- .../parts/code-runtime/codeRuntimeService.ts | 86 ++++ .../src/parts/code-runtime/codeScope.ts | 74 +++ .../src/parts/code-runtime/index.ts | 2 + .../src/parts/component-tree-model/index.ts | 2 + .../parts/component-tree-model/treeModel.ts | 183 ++++++++ .../component-tree-model/treeModelService.ts | 51 +++ .../src/parts/extension/boosts.ts | 87 ++++ .../parts/extension/extensionHostService.ts | 125 ++++++ .../src/parts/extension/index.ts | 4 + .../src/parts/extension/plugin.ts | 20 + .../src/parts/extension/render.ts | 23 + .../renderer-core/src/parts/package/index.ts | 2 + .../renderer-core/src/parts/package/loader.ts | 12 + .../src/parts/package/managementService.ts | 146 ++++++ .../renderer-core/src/parts/runtimeIntl.ts | 84 ++++ .../renderer-core/src/parts/runtimeUtil.ts | 83 ++++ .../renderer-core/src/parts/schema/index.ts | 1 + .../src/parts/schema/schemaService.ts | 65 +++ .../src/parts/schema/validation.ts | 25 ++ .../renderer-core/src/parts/widget/index.ts | 1 + .../renderer-core/src/parts/widget/widget.ts | 68 +++ packages/renderer-core/src/plugin.ts | 60 --- packages/renderer-core/src/schema.ts | 106 ----- packages/renderer-core/src/types.ts | 31 ++ packages/renderer-core/src/utils/error.ts | 17 - packages/renderer-core/src/utils/guid.ts | 8 - packages/renderer-core/src/utils/hook.ts | 177 -------- .../src/utils/non-setter-proxy.ts | 13 - packages/renderer-core/src/widget.ts | 89 ---- packages/renderer-router/package.json | 5 +- packages/renderer-router/src/guard.ts | 3 +- packages/renderer-router/src/history.ts | 16 +- packages/renderer-router/src/index.ts | 7 +- packages/renderer-router/src/matcher.ts | 43 +- packages/renderer-router/src/router.ts | 34 +- packages/renderer-router/src/types.ts | 14 +- .../renderer-router/src/utils/callback.ts | 30 ++ packages/renderer-router/src/utils/helper.ts | 3 +- .../src/utils/path-parser/parser-ranker.ts | 57 +-- packages/shared/package.json | 34 +- packages/shared/src/abilities/event.ts | 100 +++++ packages/shared/src/abilities/index.ts | 5 + .../src/abilities/instantiation/index.ts | 59 +++ packages/shared/src/abilities/intl.ts | 108 +++++ .../shared/src/{parts => abilities}/logger.ts | 0 packages/shared/src/abilities/storage.ts | 151 +++++++ packages/shared/src/index.ts | 2 +- packages/shared/src/parts/event.ts | 74 --- packages/shared/src/parts/index.ts | 3 - packages/shared/src/parts/persistence.ts | 41 -- packages/shared/src/signals.ts | 6 +- packages/shared/src/types/base.ts | 5 - packages/shared/src/types/index.ts | 15 +- packages/shared/src/types/specs/index.ts | 4 + .../shared/src/types/specs/lowcode-spec.ts | 139 +++++- .../shared/src/types/specs/material-spec.ts | 6 +- .../specs/{runtime-api.ts => runtime.ts} | 38 +- packages/shared/src/utils/browser.ts | 8 - packages/shared/src/utils/callback.ts | 30 ++ packages/shared/src/utils/index.ts | 5 +- packages/shared/src/utils/invariant.ts | 2 +- .../shared/src/utils/type-guards/index.ts | 1 + .../src/utils/type-guards/spec.ts} | 14 +- playground/package.json | 4 + playground/renderer/src/index.ts | 14 + playground/test/index.html | 27 ++ playground/test/src/index.ts | 39 ++ scripts/rollup-dts.js | 3 +- 112 files changed, 2864 insertions(+), 2546 deletions(-) rename packages/core/src/{configuration/config.ts => configuration.ts} (54%) delete mode 100644 packages/core/src/configuration/index.ts delete mode 100644 packages/core/src/instantiation/index.ts rename packages/core/src/{configuration => }/preference.ts (97%) delete mode 100644 packages/react-renderer/src/component.tsx delete mode 100644 packages/react-renderer/src/components/outlet.tsx rename packages/react-renderer/src/components/{router-view.tsx => routerView.tsx} (89%) delete mode 100644 packages/react-renderer/src/context/app.ts create mode 100644 packages/react-renderer/src/context/render.ts create mode 100644 packages/react-renderer/src/plugin.ts delete mode 100644 packages/react-renderer/src/renderer.ts delete mode 100644 packages/react-renderer/src/runtime-api/intl/index.tsx delete mode 100644 packages/react-renderer/src/runtime-api/intl/parser.ts delete mode 100644 packages/react-renderer/src/runtime-api/utils.ts create mode 100644 packages/react-renderer/src/runtime/dataSource.ts create mode 100644 packages/react-renderer/src/runtime/index.tsx create mode 100644 packages/react-renderer/src/runtime/reactiveState.ts delete mode 100644 packages/react-renderer/src/signals.ts create mode 100644 packages/react-renderer/src/utils/node.ts delete mode 100644 packages/renderer-core/src/api/app.ts delete mode 100644 packages/renderer-core/src/api/component.ts create mode 100644 packages/renderer-core/src/apiCreate.ts delete mode 100644 packages/renderer-core/src/boosts.ts delete mode 100644 packages/renderer-core/src/code-runtime.ts delete mode 100644 packages/renderer-core/src/container.ts create mode 100644 packages/renderer-core/src/main.ts delete mode 100644 packages/renderer-core/src/package.ts create mode 100644 packages/renderer-core/src/parts/code-runtime/codeRuntimeService.ts create mode 100644 packages/renderer-core/src/parts/code-runtime/codeScope.ts create mode 100644 packages/renderer-core/src/parts/code-runtime/index.ts create mode 100644 packages/renderer-core/src/parts/component-tree-model/index.ts create mode 100644 packages/renderer-core/src/parts/component-tree-model/treeModel.ts create mode 100644 packages/renderer-core/src/parts/component-tree-model/treeModelService.ts create mode 100644 packages/renderer-core/src/parts/extension/boosts.ts create mode 100644 packages/renderer-core/src/parts/extension/extensionHostService.ts create mode 100644 packages/renderer-core/src/parts/extension/index.ts create mode 100644 packages/renderer-core/src/parts/extension/plugin.ts create mode 100644 packages/renderer-core/src/parts/extension/render.ts create mode 100644 packages/renderer-core/src/parts/package/index.ts create mode 100644 packages/renderer-core/src/parts/package/loader.ts create mode 100644 packages/renderer-core/src/parts/package/managementService.ts create mode 100644 packages/renderer-core/src/parts/runtimeIntl.ts create mode 100644 packages/renderer-core/src/parts/runtimeUtil.ts create mode 100644 packages/renderer-core/src/parts/schema/index.ts create mode 100644 packages/renderer-core/src/parts/schema/schemaService.ts create mode 100644 packages/renderer-core/src/parts/schema/validation.ts create mode 100644 packages/renderer-core/src/parts/widget/index.ts create mode 100644 packages/renderer-core/src/parts/widget/widget.ts delete mode 100644 packages/renderer-core/src/plugin.ts delete mode 100644 packages/renderer-core/src/schema.ts create mode 100644 packages/renderer-core/src/types.ts delete mode 100644 packages/renderer-core/src/utils/error.ts delete mode 100644 packages/renderer-core/src/utils/guid.ts delete mode 100644 packages/renderer-core/src/utils/hook.ts delete mode 100644 packages/renderer-core/src/utils/non-setter-proxy.ts delete mode 100644 packages/renderer-core/src/widget.ts create mode 100644 packages/renderer-router/src/utils/callback.ts create mode 100644 packages/shared/src/abilities/event.ts create mode 100644 packages/shared/src/abilities/index.ts create mode 100644 packages/shared/src/abilities/instantiation/index.ts create mode 100644 packages/shared/src/abilities/intl.ts rename packages/shared/src/{parts => abilities}/logger.ts (100%) create mode 100644 packages/shared/src/abilities/storage.ts delete mode 100644 packages/shared/src/parts/event.ts delete mode 100644 packages/shared/src/parts/index.ts delete mode 100644 packages/shared/src/parts/persistence.ts delete mode 100644 packages/shared/src/types/base.ts create mode 100644 packages/shared/src/types/specs/index.ts rename packages/shared/src/types/specs/{runtime-api.ts => runtime.ts} (85%) delete mode 100644 packages/shared/src/utils/browser.ts create mode 100644 packages/shared/src/utils/callback.ts create mode 100644 packages/shared/src/utils/type-guards/index.ts rename packages/{renderer-core/src/utils/type-guard.ts => shared/src/utils/type-guards/spec.ts} (58%) create mode 100644 playground/test/index.html create mode 100644 playground/test/src/index.ts diff --git a/.npmrc b/.npmrc index 125f6d0e1..a4be22c92 100644 --- a/.npmrc +++ b/.npmrc @@ -1 +1 @@ -git-checks=false \ No newline at end of file +git-checks=false diff --git a/package.json b/package.json index 376229389..3d3a7b816 100644 --- a/package.json +++ b/package.json @@ -21,8 +21,6 @@ "prepare": "husky" }, "devDependencies": { - "@alilc/build-plugin-lce": "^0.0.5", - "@alilc/lowcode-test-mate": "^1.0.1", "@changesets/cli": "^2.27.1", "@commitlint/cli": "^19.2.1", "@commitlint/config-conventional": "^19.1.0", @@ -30,7 +28,6 @@ "@microsoft/api-extractor": "^7.43.0", "@stylistic/eslint-plugin": "^1.7.0", "@types/node": "^20.11.30", - "@types/react-router": "5.1.18", "@vanilla-extract/vite-plugin": "^4.0.7", "@vitejs/plugin-react": "^4.2.1", "eslint": "^8.57.0", @@ -38,14 +35,13 @@ "eslint-plugin-react-hooks": "^4.6.0", "globals": "^15.0.0", "husky": "^9.0.11", - "less": "^4.2.0", "lint-staged": "^15.2.2", "prettier": "^3.2.5", "rimraf": "^5.0.2", "typescript": "^5.4.2", "typescript-eslint": "^7.5.0", "vite": "^5.2.9", - "vitest": "^1.5.0" + "vitest": "^1.6.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0", diff --git a/packages/core/package.json b/packages/core/package.json index 41e5bef02..cef3fa6e3 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -33,13 +33,10 @@ "test:cov": "" }, "dependencies": { - "@abraham/reflection": "^0.12.0", "@alilc/lowcode-shared": "workspace:*", "@alilc/lowcode-types": "workspace:*", "@alilc/lowcode-utils": "workspace:*", "@formatjs/intl": "^2.10.1", - "inversify": "^6.0.2", - "inversify-binding-decorators": "^4.0.0", "lodash-es": "^4.17.21", "react": "^18.2.0", "react-dom": "^18.2.0", diff --git a/packages/core/src/configuration/config.ts b/packages/core/src/configuration.ts similarity index 54% rename from packages/core/src/configuration/config.ts rename to packages/core/src/configuration.ts index 1afa8b150..0a2adc19c 100644 --- a/packages/core/src/configuration/config.ts +++ b/packages/core/src/configuration.ts @@ -1,23 +1,13 @@ -import { get as lodashGet, isPlainObject } from 'lodash-es'; -import { createLogger, type PlainObject, invariant } from '@alilc/lowcode-shared'; - -const logger = createLogger({ level: 'log', bizName: 'config' }); - -// this default behavior will be different later -const STRICT_PLUGIN_MODE_DEFAULT = true; - -interface ConfigurationOptions { - strictMode?: boolean; - setterValidator?: (key: K, value: Config[K]) => boolean | string; -} +import { get as lodashGet, isPlainObject, cloneDeep } from 'lodash-es'; +import { type PlainObject } from '@alilc/lowcode-shared/src/types'; +import { invariant } from '@alilc/lowcode-shared/src/utils'; export class Configuration { - #strictMode = STRICT_PLUGIN_MODE_DEFAULT; - #setterValidator: (key: K, value: Config[K]) => boolean | string = () => true; + private config: Config; - #config: Config = {} as Config; + private setterValidator: ((key: K, value: Config[K]) => boolean | string) | undefined; - #waits = new Map< + private waits = new Map< K, { once?: boolean; @@ -25,23 +15,18 @@ export class Configuration(); - constructor(config: Config, options?: ConfigurationOptions) { + constructor(config: Config, setterValidator?: (key: K, value: Config[K]) => boolean | string) { 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) { invariant( typeof setterValidator === 'function', 'setterValidator must be a function', 'Configuration', ); - this.#setterValidator = setterValidator; + this.setterValidator = setterValidator; } } @@ -50,38 +35,35 @@ export class Configuration(key: K, defaultValue?: T): T | undefined { + return lodashGet(this.config, key, defaultValue); } - /** * 设置指定 key 的值 * @param key * @param value */ set(key: K, value: any) { - if (this.#strictMode) { - const valid = this.#setterValidator(key, value); - if (valid === false || typeof valid === 'string') { - return logger.warn( - `failed to config ${key.toString()}, only predefined options can be set under strict mode, predefined options: `, - valid ? valid : '', - ); - } + if (this.setterValidator) { + const valid = this.setterValidator(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 : ''}`, + 'Configuration', + ); } - this.#config[key] = value; + this.config[key] = value; this.notifyGot(key); } - /** * 批量设值,set 的对象版本 * @param config @@ -93,7 +75,6 @@ export class Configuration void): () => void { - const val = this.#config[key]; + const val = this.config[key]; if (val !== undefined) { fn(val); } @@ -127,8 +107,8 @@ export class Configuration 0) { - this.#waits.set(key, waits); + this.waits.set(key, waits); } else { - this.#waits.delete(key); + this.waits.delete(key); } } - setWait(key: K, resolve: (data: any) => void, once?: boolean) { - const waits = this.#waits.get(key); + private setWait(key: K, resolve: (data: any) => void, once?: boolean) { + const waits = this.waits.get(key); if (waits) { waits.push({ resolve, once }); } else { - this.#waits.set(key, [{ resolve, once }]); + this.waits.set(key, [{ resolve, once }]); } } - delWait(key: K, fn: any) { - const waits = this.#waits.get(key); + private delWait(key: K, fn: any) { + const waits = this.waits.get(key); if (!waits) { return; } @@ -168,7 +148,7 @@ export class Configuration { - (...args: any[]): void; - type: T; -} - -type Constructor = new (...args: any[]) => T; - -export function createDecorator(serviceId: string): ServiceIdentifier { - const id = ( - 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(App: T) { - return iocContainer.resolve>(App); -} - -export function bootstrapModules() { - iocContainer.load(buildProviderModule()); -} diff --git a/packages/core/src/intl.ts b/packages/core/src/intl.ts index 75da7d3dd..952930060 100644 --- a/packages/core/src/intl.ts +++ b/packages/core/src/intl.ts @@ -3,8 +3,8 @@ import { computed, effect, createLogger, + type Spec, type Signal, - type I18nMap, type ComputedSignal, type PlainObject, } from '@alilc/lowcode-shared'; @@ -21,8 +21,8 @@ const logger = createLogger({ level: 'warn', bizName: 'globalLocale' }); const STORED_LOCALE_KEY = 'ali-lowcode-config'; export type Locale = string; -export type IntlMessage = I18nMap[Locale]; -export type IntlMessageRecord = I18nMap; +export type IntlMessage = Spec.I18nMap[Locale]; +export type IntlMessageRecord = Spec.I18nMap; export class Intl { #locale: Signal; @@ -34,7 +34,7 @@ export class Intl { if (defaultLocale) { defaultLocale = nomarlizeLocale(defaultLocale); } else { - defaultLocale = initializeLocale(); + defaultLocale = 'zh-CN'; } const messageStore = mapKeys(messages, (_, key) => { @@ -65,22 +65,6 @@ export class Intl { setLocale(locale: 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; } diff --git a/packages/core/src/configuration/preference.ts b/packages/core/src/preference.ts similarity index 97% rename from packages/core/src/configuration/preference.ts rename to packages/core/src/preference.ts index d62eb6d13..bf5076508 100644 --- a/packages/core/src/configuration/preference.ts +++ b/packages/core/src/preference.ts @@ -61,5 +61,3 @@ export class Preference { return !(result === undefined || result === null); } } - -export const userPreference = new Preference(); diff --git a/packages/core/tsconfig.json b/packages/core/tsconfig.json index 9110c0117..0590f5d80 100644 --- a/packages/core/tsconfig.json +++ b/packages/core/tsconfig.json @@ -3,5 +3,5 @@ "compilerOptions": { "outDir": "dist" }, - "include": ["src", "__tests__"] + "include": ["src", "__tests__", "src/configuration.ts"] } diff --git a/packages/designer/src/models/document/document-model.ts b/packages/designer/src/models/document/document-model.ts index e47264b27..641fb209e 100644 --- a/packages/designer/src/models/document/document-model.ts +++ b/packages/designer/src/models/document/document-model.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 { History } from './history'; -export interface DocumentSchema extends ComponentTreeRootNode { +export interface DocumentSchema extends Spec.ComponentTreeRoot { id: string; } diff --git a/packages/designer/src/models/node/node.ts b/packages/designer/src/models/node/node.ts index eceac848d..c1095189c 100644 --- a/packages/designer/src/models/node/node.ts +++ b/packages/designer/src/models/node/node.ts @@ -1,8 +1,8 @@ -import { ComponentTreeNode } from '@alilc/lowcode-shared'; +import { Spec } from '@alilc/lowcode-shared'; import { type ComponentMeta } from '../component-meta'; import { type Prop } from './prop'; -export interface Node { +export interface Node { /** * 节点 id * node id @@ -353,6 +353,6 @@ export interface Node { }; } -export function createNode(nodeSchema: Schema): Node { +export function createNode(nodeSchema: Schema): Node { return {}; } diff --git a/packages/react-renderer/package.json b/packages/react-renderer/package.json index 27f8ad0f3..17ee945e9 100644 --- a/packages/react-renderer/package.json +++ b/packages/react-renderer/package.json @@ -25,7 +25,6 @@ "@alilc/lowcode-shared": "workspace:*", "@alilc/lowcode-renderer-core": "workspace:*", "@alilc/lowcode-renderer-router": "workspace:*", - "@vue/reactivity": "^3.4.21", "lodash-es": "^4.17.21", "immer": "^10.0.4", "hoist-non-react-statics": "^3.3.2", @@ -39,8 +38,7 @@ "@types/hoist-non-react-statics": "^3.3.5", "@types/use-sync-external-store": "^0.0.6", "@types/react": "^18.2.67", - "@types/react-dom": "^18.2.22", - "jsdom": "^24.0.0" + "@types/react-dom": "^18.2.22" }, "peerDependencies": { "react": "^18.2.0", diff --git a/packages/react-renderer/src/api/app.tsx b/packages/react-renderer/src/api/app.tsx index f1596ce19..db58b8882 100644 --- a/packages/react-renderer/src/api/app.tsx +++ b/packages/react-renderer/src/api/app.tsx @@ -1,77 +1,78 @@ -import { - type App, - type AppBase, - createAppFunction, - type AppOptionsBase, -} from '@alilc/lowcode-renderer-core'; +import { createRenderer, type AppOptions, type IRender } from '@alilc/lowcode-renderer-core'; import { type ComponentType } from 'react'; import { type Root, createRoot } from 'react-dom/client'; -import { createRouter } from '@alilc/lowcode-renderer-router'; -import { createRenderer } from '../renderer'; +import { createRouter, type RouterOptions } from '@alilc/lowcode-renderer-router'; import AppComponent from '../components/app'; -import { createIntl } from '../runtime-api/intl'; -import { createRuntimeUtils } from '../runtime-api/utils'; +import { RendererContext } from '../context/render'; +import { createRouterProvider } from '../components/routerView'; +import { rendererExtends } from '../plugin'; -export interface AppOptions extends AppOptionsBase { - dataSourceCreator: any; +export interface ReactAppOptions extends AppOptions { faultComponent?: ComponentType; } -export interface ReactRender extends AppBase {} +const defaultRouterOptions: RouterOptions = { + historyMode: 'browser', + baseName: '/', + routes: [], +}; -export type ReactApp = App; +export const createApp = async (options: ReactAppOptions) => { + const creator = createRenderer(async (context) => { + const { schema, boostsManager } = context; + const boosts = boostsManager.toExpose(); -export const createApp = createAppFunction(async (context, options) => { - const { schema, packageManager, appScope, boosts } = context; + // router + let routerConfig = defaultRouterOptions; - // router - // todo: transform config - 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 i18nMessages = schema.getByKey('i18n') ?? {}; - const defaultLocale = schema.getByPath('config.defaultLocale') ?? 'zh-CN'; - const intl = createIntl(i18nMessages, defaultLocale); + const router = createRouter(routerConfig); - appScope.inject('intl', intl); + boosts.codeRuntime.getScope().inject('router', router); - // utils - const runtimeUtils = createRuntimeUtils(schema.getByKey('utils') ?? [], packageManager); + // set config + // if (options.faultComponent) { + // context.config.set('faultComponent', options.faultComponent); + // } - appScope.inject('utils', runtimeUtils.utils); - boosts.add('runtimeUtils', runtimeUtils); + // extends boosts + boostsManager.extend(rendererExtends); - // set config - if (options.faultComponent) { - context.config.set('faultComponent', options.faultComponent); - } - context.config.set('dataSourceCreator', options.dataSourceCreator); + const RouterProvider = createRouterProvider(router); - let root: Root | undefined; - const renderer = createRenderer(); - const appContext = { ...context, renderer }; + let root: Root | undefined; - const reactRender: ReactRender = { - async mount(el) { - if (root) { - return; - } + return { + async mount(el) { + if (root) { + return; + } - root = createRoot(el); - root.render(); - }, - unmount() { - if (root) { - root.unmount(); - root = undefined; - } - }, - }; + root = createRoot(el); + root.render( + + + + + , + ); + }, + unmount() { + if (root) { + root.unmount(); + root = undefined; + } + }, + }; + }); - return { - appBase: reactRender, - renderer, - }; -}); + return creator(options); +}; diff --git a/packages/react-renderer/src/api/component.tsx b/packages/react-renderer/src/api/component.tsx index 0416db721..c7cbccc2d 100644 --- a/packages/react-renderer/src/api/component.tsx +++ b/packages/react-renderer/src/api/component.tsx @@ -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) { - return internalCreate(options); +interface Render { + toComponent(): FunctionComponent; } -export type { ComponentOptions }; +export async function createComponent(options: AppOptions) { + const creator = createRenderer((context) => { + const { schema } = context; + + const LowCodeComponent = createComponentBySchema(schema.get('componentsTree')[0]); + + function Component(props: LowCodeComponentProps) { + return ( + + + + ); + } + + return { + toComponent() { + return Component; + }, + }; + }); + + const render = await creator(options); + + return render.toComponent(); +} diff --git a/packages/react-renderer/src/component.tsx b/packages/react-renderer/src/component.tsx deleted file mode 100644 index 6fc51ea24..000000000 --- a/packages/react-renderer/src/component.tsx +++ /dev/null @@ -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> - extends CreateComponentBaseOptions { - componentsRecord: Record; - intl: IntlApi; - displayName?: string; - - beforeElementCreate?(widget: TextWidget | ComponentWidget): void; - componentRefAttached?(widget: ComponentWidget, instance: ReactInstance): void; -} - -export interface LowCodeComponentProps { - id?: string; - /** CSS 类名 */ - className?: string; - /** style */ - style?: CSSProperties; - - [key: string]: any; -} - -export const createComponent = createComponentFunction< - ComponentType, - ReactInstance, - ReactComponentLifeCycle, - ComponentOptions ->(reactiveStateCreator, (container, options) => { - const { - componentsRecord, - intl, - displayName = '__LowCodeComponent__', - beforeElementCreate, - componentRefAttached, - - ...extraOptions - } = options; - const lowCodeComponentCache = new Map>(); - - 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> | ComponentWidget>, - 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, - widget: ComponentWidget>, - 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>(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, - ) { - 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>().map((widget) => - widget.mapRenderObject((widget) => { - if (widget instanceof TextWidget) { - if (widget.type === 'i18n') { - function IntlText(props: { key: string; params: Record }) { - 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 ( -
- {widgets.map((widget) => createReactElement(widget, container.codeRuntime))} -
- ); - }); - - 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, - }; - }, - }; -} diff --git a/packages/react-renderer/src/components/app.tsx b/packages/react-renderer/src/components/app.tsx index 63a072589..3131f8c4c 100644 --- a/packages/react-renderer/src/components/app.tsx +++ b/packages/react-renderer/src/components/app.tsx @@ -1,33 +1,25 @@ -import { AppContext, type AppContextObject } from '../context/app'; -import { createComponent } from '../component'; +import { isLowCodeComponentSchema } from '@alilc/lowcode-shared'; +import { useRenderContext } from '../context/render'; +import { createComponentBySchema, ReactComponent } from '../runtime'; import Route from './route'; -import { createRouterProvider } from './router-view'; +import { rendererExtends } from '../plugin'; -export default function App({ context }: { context: AppContextObject }) { - const { schema, config, renderer, packageManager, appScope } = context; - const appWrappers = renderer.getAppWrappers(); - const wrappers = renderer.getRouteWrappers(); +export default function App() { + const { schema, packageManager } = useRenderContext(); + const appWrappers = rendererExtends.getAppWrappers(); + const wrappers = rendererExtends.getRouteWrappers(); function getLayoutComponent() { - const layoutName = schema.getByPath('config.layout.componentName'); + const config = schema.get('config'); + const componentName = config?.layout?.componentName as string; - if (layoutName) { - const Component: any = packageManager.getComponent(layoutName); + if (componentName) { + const Component = packageManager.getComponent(componentName); - if (Component?.devMode === 'lowCode') { - const componentsMap = schema.getComponentsMaps(); - const componentsRecord = packageManager.getComponentsNameRecord(componentsMap); - - const Layout = createComponent({ - componentsTree: Component.schema, - componentsRecord, - - dataSourceCreator: config.get('dataSourceCreator'), - supCodeScope: appScope, - intl: appScope.value.intl, + if (isLowCodeComponentSchema(Component)) { + return createComponentBySchema(Component.schema, { + displayName: componentName, }); - - return Layout; } return Component; @@ -45,7 +37,7 @@ export default function App({ context }: { context: AppContextObject }) { } if (Layout) { - const layoutProps = schema.getByPath('config.layout.props') ?? {}; + const layoutProps: any = schema.get('config')?.layout?.props ?? {}; element = {element}; } @@ -55,11 +47,5 @@ export default function App({ context }: { context: AppContextObject }) { }, element); } - const RouterProvider = createRouterProvider(appScope.value.router); - - return ( - - {element} - - ); + return element; } diff --git a/packages/react-renderer/src/components/outlet.tsx b/packages/react-renderer/src/components/outlet.tsx deleted file mode 100644 index f62d3b8b6..000000000 --- a/packages/react-renderer/src/components/outlet.tsx +++ /dev/null @@ -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(componentsMap); - - const LowCodeComponent = createComponent({ - supCodeScope: appScope, - dataSourceCreator: config.get('dataSourceCreator'), - componentsTree, - componentsRecord, - intl: appScope.value.intl, - }); - - return ; - } - - return null; -} diff --git a/packages/react-renderer/src/components/route.tsx b/packages/react-renderer/src/components/route.tsx index 422a52527..c2c132cca 100644 --- a/packages/react-renderer/src/components/route.tsx +++ b/packages/react-renderer/src/components/route.tsx @@ -1,21 +1,43 @@ +import { type Spec } from '@alilc/lowcode-shared'; +import { useRenderContext } from '../context/render'; import { usePageConfig } from '../context/router'; -import { useAppContext } from '../context/app'; -import RouteOutlet from './outlet'; +import { rendererExtends } from '../plugin'; +import { createComponentBySchema } from '../runtime'; + +export interface OutletProps { + pageConfig: Spec.PageConfig; + + [key: string]: any; +} export default function Route(props: any) { - const { schema, renderer } = useAppContext(); const pageConfig = usePageConfig(); - const Outlet = renderer.getOutlet() ?? RouteOutlet; - if (Outlet && pageConfig) { - let componentsTree; - const { type = 'lowCode', mappingId } = pageConfig; + if (pageConfig) { + const Outlet = rendererExtends.getOutlet() ?? RouteOutlet; + + return ; + } + + return null; +} + +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); - if (type === 'lowCode') { - componentsTree = schema.getComponentsTrees().find((item) => item.id === mappingId); - } + const LowCodeComponent = createComponentBySchema(mappingId, { + displayName: pageConfig?.id, + }); - return ; + return ; } return null; diff --git a/packages/react-renderer/src/components/router-view.tsx b/packages/react-renderer/src/components/routerView.tsx similarity index 89% rename from packages/react-renderer/src/components/router-view.tsx rename to packages/react-renderer/src/components/routerView.tsx index 7032f1b01..4aaa90317 100644 --- a/packages/react-renderer/src/components/router-view.tsx +++ b/packages/react-renderer/src/components/routerView.tsx @@ -1,11 +1,11 @@ import { type Router } from '@alilc/lowcode-renderer-router'; import { useState, useLayoutEffect, useMemo, type ReactNode } from 'react'; import { RouterContext, RouteLocationContext, PageConfigContext } from '../context/router'; -import { useAppContext } from '../context/app'; +import { useRenderContext } from '../context/render'; export const createRouterProvider = (router: Router) => { return function RouterProvider({ children }: { children?: ReactNode }) { - const { schema } = useAppContext(); + const { schema } = useRenderContext(); const [location, setCurrentLocation] = useState(router.getCurrentLocation()); useLayoutEffect(() => { @@ -14,7 +14,7 @@ export const createRouterProvider = (router: Router) => { }, []); const pageSchema = useMemo(() => { - const pages = schema.getPageConfigs(); + const pages = schema.get('pages') ?? []; const matched = location.matched[location.matched.length - 1]; if (matched) { diff --git a/packages/react-renderer/src/context/app.ts b/packages/react-renderer/src/context/app.ts deleted file mode 100644 index e54836ebc..000000000 --- a/packages/react-renderer/src/context/app.ts +++ /dev/null @@ -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({} as any); - -AppContext.displayName = 'RootContext'; - -export const useAppContext = () => useContext(AppContext); diff --git a/packages/react-renderer/src/context/render.ts b/packages/react-renderer/src/context/render.ts new file mode 100644 index 000000000..59833b581 --- /dev/null +++ b/packages/react-renderer/src/context/render.ts @@ -0,0 +1,8 @@ +import { createContext, useContext } from 'react'; +import { type RenderContext } from '@alilc/lowcode-renderer-core'; + +export const RendererContext = createContext(undefined!); + +RendererContext.displayName = 'RootContext'; + +export const useRenderContext = () => useContext(RendererContext); diff --git a/packages/react-renderer/src/context/router.ts b/packages/react-renderer/src/context/router.ts index 376f78a04..d96402a28 100644 --- a/packages/react-renderer/src/context/router.ts +++ b/packages/react-renderer/src/context/router.ts @@ -1,30 +1,20 @@ 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'; -export const RouterContext = createContext({} as any); +export const RouterContext = createContext(undefined!); RouterContext.displayName = 'RouterContext'; export const useRouter = () => useContext(RouterContext); -export const RouteLocationContext = createContext({ - name: undefined, - path: '/', - searchParams: undefined, - params: {}, - hash: '', - fullPath: '/', - redirectedFrom: undefined, - matched: [], - meta: {}, -}); +export const RouteLocationContext = createContext(undefined!); RouteLocationContext.displayName = 'RouteLocationContext'; export const useRouteLocation = () => useContext(RouteLocationContext); -export const PageConfigContext = createContext(undefined); +export const PageConfigContext = createContext(undefined); PageConfigContext.displayName = 'PageConfigContext'; diff --git a/packages/react-renderer/src/index.ts b/packages/react-renderer/src/index.ts index 4063e8c56..97abc4027 100644 --- a/packages/react-renderer/src/index.ts +++ b/packages/react-renderer/src/index.ts @@ -1,2 +1,8 @@ export * from './api/app'; 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'; diff --git a/packages/react-renderer/src/plugin.ts b/packages/react-renderer/src/plugin.ts new file mode 100644 index 000000000..e4dfc440c --- /dev/null +++ b/packages/react-renderer/src/plugin.ts @@ -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>; + +export type Outlet = ComponentType; + +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) { + return plugin; +} diff --git a/packages/react-renderer/src/renderer.ts b/packages/react-renderer/src/renderer.ts deleted file mode 100644 index 45761779d..000000000 --- a/packages/react-renderer/src/renderer.ts +++ /dev/null @@ -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>; - -export type Outlet = ComponentType; - -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) { - return definePluginFn(plugin); -} diff --git a/packages/react-renderer/src/runtime-api/intl/index.tsx b/packages/react-renderer/src/runtime-api/intl/index.tsx deleted file mode 100644 index 742dba5c3..000000000 --- a/packages/react-renderer/src/runtime-api/intl/index.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import { parse, compile } from './parser'; -import { signal, computed } from '../../signals'; - -export function createIntl( - messages: Record>, - defaultLocale: string, -) { - const allMessages = signal(messages); - const currentLocale = signal(defaultLocale); - const currentMessages = computed(() => allMessages.value[currentLocale.value]); - - return { - i18n(key: string, params: Record) { - 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) { - allMessages.value[locale] = { - ...allMessages.value[locale], - ...messages, - }; - }, - }; -} diff --git a/packages/react-renderer/src/runtime-api/intl/parser.ts b/packages/react-renderer/src/runtime-api/intl/parser.ts deleted file mode 100644 index a2beba33e..000000000 --- a/packages/react-renderer/src/runtime-api/intl/parser.ts +++ /dev/null @@ -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 { - const tokens: Array = []; - 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 | 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)[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; -} diff --git a/packages/react-renderer/src/runtime-api/utils.ts b/packages/react-renderer/src/runtime-api/utils.ts deleted file mode 100644 index c3dee4078..000000000 --- a/packages/react-renderer/src/runtime-api/utils.ts +++ /dev/null @@ -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 = {}; - - 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, - }; -} diff --git a/packages/react-renderer/src/runtime/dataSource.ts b/packages/react-renderer/src/runtime/dataSource.ts new file mode 100644 index 000000000..eadc5050f --- /dev/null +++ b/packages/react-renderer/src/runtime/dataSource.ts @@ -0,0 +1 @@ +export const dataSourceCreator = () => ({}) as any; diff --git a/packages/react-renderer/src/runtime/index.tsx b/packages/react-renderer/src/runtime/index.tsx new file mode 100644 index 000000000..4a64f9beb --- /dev/null +++ b/packages/react-renderer/src/runtime/index.tsx @@ -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; +export type ReactWidget = IWidget; + +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(); + +function getComponentByName(name: string, { packageManager }: RenderContext): ReactComponent { + const componentsRecord = packageManager.getComponentsNameRecord(); + // 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, + ) { + const renderContext = useRenderContext(); + const { componentTreeModel } = renderContext; + + const modelRef = useRef>(); + + 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 ( +
+ {elements} +
+ ); + }); + + LowCodeComponent.displayName = displayName; + + return memo(LowCodeComponent); +} + +function Text(props: { text: string }) { + return <>{props.text}; +} + +Text.displayName = 'Text'; + +function createElementByWidget( + widget: IWidget, + codeScope: ICodeScope, + renderContext: RenderContext, + componentRefAttached?: ComponentOptions['componentRefAttached'], +) { + return widget.build((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; + }); +} diff --git a/packages/react-renderer/src/runtime/reactiveState.ts b/packages/react-renderer/src/runtime/reactiveState.ts new file mode 100644 index 000000000..6f4d4b5ac --- /dev/null +++ b/packages/react-renderer/src/runtime/reactiveState.ts @@ -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, + }; + }, + }; +} diff --git a/packages/react-renderer/src/signals.ts b/packages/react-renderer/src/signals.ts deleted file mode 100644 index 7f541fd8c..000000000 --- a/packages/react-renderer/src/signals.ts +++ /dev/null @@ -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( - source: Ref | ComputedRef | 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) { - 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; -} diff --git a/packages/react-renderer/src/utils/node.ts b/packages/react-renderer/src/utils/node.ts new file mode 100644 index 000000000..554374c22 --- /dev/null +++ b/packages/react-renderer/src/utils/node.ts @@ -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 ?? {}, + }; +} diff --git a/packages/react-renderer/src/utils/reactive.tsx b/packages/react-renderer/src/utils/reactive.tsx index afeab8c1f..26ca030d0 100644 --- a/packages/react-renderer/src/utils/reactive.tsx +++ b/packages/react-renderer/src/utils/reactive.tsx @@ -1,15 +1,15 @@ +import { processValue } from '@alilc/lowcode-renderer-core'; import { - processValue, type AnyFunction, type PlainObject, - type JSExpression, isJSExpression, -} from '@alilc/lowcode-renderer-core'; + computed, + watch, +} from '@alilc/lowcode-shared'; import { type ComponentType, memo, forwardRef, type PropsWithChildren, createElement } from 'react'; import { produce } from 'immer'; import hoistNonReactStatics from 'hoist-non-react-statics'; import { useSyncExternalStore } from 'use-sync-external-store/shim'; -import { computed, watch } from '../signals'; export interface ReactiveStore { value: Snapshot; @@ -20,7 +20,8 @@ export interface ReactiveStore { function createReactiveStore( target: Record, - valueGetter: (expr: JSExpression) => any, + predicate: (obj: any) => boolean, + valueGetter: (expr: any) => any, ): ReactiveStore { let isFlushing = false; let isFlushPending = false; @@ -28,7 +29,7 @@ function createReactiveStore( const cleanups: Array<() => void> = []; 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 unwatch = watch(computedValue, (newValue) => { waitPathToSetValueMap.set(paths, newValue); @@ -93,15 +94,21 @@ function createReactiveStore( interface ReactiveOptions { target: PlainObject; - valueGetter: (expr: JSExpression) => any; + valueGetter: (expr: any) => any; + predicate?: (obj: any) => boolean; forwardRef?: boolean; } export function reactive( WrappedComponent: ComponentType, - { target, valueGetter, forwardRef: forwardRefOption = true }: ReactiveOptions, -): ComponentType { - const store = createReactiveStore(target, valueGetter); + { + target, + valueGetter, + predicate = isJSExpression, + forwardRef: forwardRefOption = true, + }: ReactiveOptions, +): ComponentType> { + const store = createReactiveStore(target, predicate, valueGetter); function WrapperComponent(props: any, ref: any) { const actualProps = useSyncExternalStore(store.subscribe, store.getSnapshot); diff --git a/packages/renderer-core/src/api/app.ts b/packages/renderer-core/src/api/app.ts deleted file mode 100644 index b87ca6ec7..000000000 --- a/packages/renderer-core/src/api/app.ts +++ /dev/null @@ -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; - unmount: () => void | Promise; -} - -/** - * context for plugin or renderer - */ -export interface AppContext { - schema: AppSchema; - config: PlainObject; - appScope: CodeScope; - packageManager: PackageManager; - boosts: AppBoostsManager; -} - -type AppCreator = ( - appContext: Omit, - appOptions: O, -) => Promise<{ appBase: T; renderer?: any }>; - -export type App = { - schema: Project; - config: PlainObject; - readonly boosts: AppBoosts; - - use(plugin: Plugin): Promise; -} & T; - -/** - * 创建 createApp 的辅助函数 - * @param schema - * @param options - * @returns - */ -export function createAppFunction( - appCreator: AppCreator, -): (options: O) => Promise> { - 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, - ); - }; -} diff --git a/packages/renderer-core/src/api/component.ts b/packages/renderer-core/src/api/component.ts deleted file mode 100644 index 75a5a6aae..000000000 --- a/packages/renderer-core/src/api/component.ts +++ /dev/null @@ -1,43 +0,0 @@ -import type { PlainObject, InstanceStateApi } from '@alilc/lowcode-shared'; -import { type CreateContainerOptions, createContainer, type Container } from '../container'; - -export type CreateComponentBaseOptions = Omit< - CreateContainerOptions, - 'stateCreator' ->; - -/** - * 创建 createComponent 的辅助函数 - * createComponent = createComponentFunction(() => component) - */ -export function createComponentFunction< - ComponentT, - InstanceT, - LifeCycleNameT extends string, - O extends CreateComponentBaseOptions, ->( - stateCreator: (initState: PlainObject) => InstanceStateApi, - componentCreator: ( - container: Container, - componentOptions: O, - ) => ComponentT, -): (componentOptions: O) => ComponentT { - return (componentOptions) => { - const { - supCodeScope, - initScopeValue = {}, - dataSourceCreator, - componentsTree, - } = componentOptions; - - const container = createContainer({ - supCodeScope, - initScopeValue, - stateCreator, - dataSourceCreator, - componentsTree, - }); - - return componentCreator(container, componentOptions); - }; -} diff --git a/packages/renderer-core/src/apiCreate.ts b/packages/renderer-core/src/apiCreate.ts new file mode 100644 index 000000000..db090effa --- /dev/null +++ b/packages/renderer-core/src/apiCreate.ts @@ -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( + renderAdapter: RenderAdapter, +): (options: AppOptions) => Promise> { + 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(renderAdapter); + }; +} diff --git a/packages/renderer-core/src/boosts.ts b/packages/renderer-core/src/boosts.ts deleted file mode 100644 index ad4607c0b..000000000 --- a/packages/renderer-core/src/boosts.ts +++ /dev/null @@ -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; - - 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]; - } - }, -}; diff --git a/packages/renderer-core/src/code-runtime.ts b/packages/renderer-core/src/code-runtime.ts deleted file mode 100644 index 0d9a2e7cc..000000000 --- a/packages/renderer-core/src/code-runtime.ts +++ /dev/null @@ -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(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(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 { - readonly value: T; - - inject(name: K, value: T[K], force?: boolean): void; - setValue(value: T, replace?: boolean): void; - createSubScope(initValue: O): CodeScope; -} - -export function createScope( - initValue: T, -): CodeScope { - 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 = { - 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(initValue: O) { - const childScope = createScope(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; -} diff --git a/packages/renderer-core/src/container.ts b/packages/renderer-core/src/container.ts deleted file mode 100644 index 35953eaab..000000000 --- a/packages/renderer-core/src/container.ts +++ /dev/null @@ -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 { - readonly codeRuntime: CodeRuntime; - readonly instanceApiObject: InstanceApi; - - /** - * 获取协议中的 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(): (TextWidget | ComponentWidget)[]; -} - -export interface CreateContainerOptions { - supCodeScope?: CodeScope; - initScopeValue?: PlainObject; - componentsTree: ComponentTree; - stateCreator: (initalState: PlainObject) => InstanceStateApi; - // type todo - dataSourceCreator: (...args: any[]) => InstanceDataSourceApi; -} - -export function createContainer( - options: CreateContainerOptions, -): Container { - const { - componentsTree, - supCodeScope, - initScopeValue = {}, - stateCreator, - dataSourceCreator, - } = options; - - validContainerSchema(componentsTree); - - const instancesMap = new Map(); - 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 = 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; - }, - - getCssText() { - return componentsTree.css; - }, - triggerLifeCycle, - - setInstance: setInstanceByRef, - removeInstance: removeInstanceByRef, - - createWidgets() { - if (!componentsTree.children) return []; - return componentsTree.children.map((item) => createWidget(item)); - }, - }; -} - -const CONTAINTER_NAME = ['Page', 'Block', 'Component']; - -function validContainerSchema(schema: ComponentTree) { - if (!CONTAINTER_NAME.includes(schema.componentName)) { - throw Error('container schema not valid'); - } -} diff --git a/packages/renderer-core/src/index.ts b/packages/renderer-core/src/index.ts index fdf79866a..cd7e5403c 100644 --- a/packages/renderer-core/src/index.ts +++ b/packages/renderer-core/src/index.ts @@ -1,17 +1,20 @@ /* --------------- api -------------------- */ -export * from './api/app'; -export * from './api/component'; -export { createCodeRuntime, createScope } from './code-runtime'; -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 './apiCreate'; +export { definePackageLoader } from './parts/package'; +export { Widget } from './parts/widget'; export * from './utils/value'; -export * from './widget'; /* --------------- types ---------------- */ -export type { CodeRuntime, CodeScope } from './code-runtime'; -export type { Plugin, PluginSetupContext } from './plugin'; -export type { PackageManager, PackageLoader } from './package'; -export type { Container, CreateContainerOptions } from './container'; +export type * from './types'; +export type { + Plugin, + 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'; diff --git a/packages/renderer-core/src/main.ts b/packages/renderer-core/src/main.ts new file mode 100644 index 000000000..13038fc2d --- /dev/null +++ b/packages/renderer-core/src/main.ts @@ -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(adapter: RenderAdapter): Promise> { + const render = await this.extensionHostService.runRender(adapter); + + // construct application + const app = Object.freeze>({ + 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(), + }); + } +} diff --git a/packages/renderer-core/src/package.ts b/packages/renderer-core/src/package.ts deleted file mode 100644 index 2a16051ba..000000000 --- a/packages/renderer-core/src/package.ts +++ /dev/null @@ -1,157 +0,0 @@ -import { type Package, type ComponentMap, type LowCodeComponent } from './types'; - -const packageStore: Map = ((window as any).__PACKAGE_STORE__ ??= new Map()); - -export interface PackageLoader { - name?: string; - load(packageInfo: Package, thisManager: PackageManager): Promise; - active(packageInfo: Package): boolean; -} - -export interface PackageManager { - /** - * 新增资产包 - * @param packages - */ - addPackages(packages: Package[]): Promise; - /** 通过包名获取资产包信息 */ - 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( - componentMaps?: ComponentMap[], - ): Record; - /** 通过组件名获取对应的组件 */ - getComponent(componentName: string): C | LowCodeComponent | undefined; - /** 注册组件 */ - registerComponentByName(componentName: string, Component: unknown): void; -} - -export function createPackageManager(): PackageManager { - const packageLoaders: PackageLoader[] = []; - const componentsRecord: Record = {}; - - 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; -} diff --git a/packages/renderer-core/src/parts/code-runtime/codeRuntimeService.ts b/packages/renderer-core/src/parts/code-runtime/codeRuntimeService.ts new file mode 100644 index 000000000..1df8c2d68 --- /dev/null +++ b/packages/renderer-core/src/parts/code-runtime/codeRuntimeService.ts @@ -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(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('codeRuntimeService'); + +@Provide(ICodeRuntimeService) +export class CodeRuntimeService implements ICodeRuntimeService { + private codeScope: ICodeScope = new CodeScope({}); + + private callbacks = createCallback<(code: string) => string>(); + + getScope() { + return this.codeScope; + } + + run(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); + } +} diff --git a/packages/renderer-core/src/parts/code-runtime/codeScope.ts b/packages/renderer-core/src/parts/code-runtime/codeScope.ts new file mode 100644 index 000000000..3aa18eca6 --- /dev/null +++ b/packages/renderer-core/src/parts/code-runtime/codeScope.ts @@ -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; + } +} diff --git a/packages/renderer-core/src/parts/code-runtime/index.ts b/packages/renderer-core/src/parts/code-runtime/index.ts new file mode 100644 index 000000000..74b951ea1 --- /dev/null +++ b/packages/renderer-core/src/parts/code-runtime/index.ts @@ -0,0 +1,2 @@ +export * from './codeScope'; +export * from './codeRuntimeService'; diff --git a/packages/renderer-core/src/parts/component-tree-model/index.ts b/packages/renderer-core/src/parts/component-tree-model/index.ts new file mode 100644 index 000000000..4d5f98bc0 --- /dev/null +++ b/packages/renderer-core/src/parts/component-tree-model/index.ts @@ -0,0 +1,2 @@ +export * from './treeModel'; +export * from './treeModelService'; diff --git a/packages/renderer-core/src/parts/component-tree-model/treeModel.ts b/packages/renderer-core/src/parts/component-tree-model/treeModel.ts new file mode 100644 index 000000000..5590d1d70 --- /dev/null +++ b/packages/renderer-core/src/parts/component-tree-model/treeModel.ts @@ -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 { + readonly codeScope: ICodeScope; + + readonly codeRuntime: ICodeRuntimeService; + + readonly widgets: IWidget[]; + /** + * 获取协议中的 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[]; +} + +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 + implements IComponentTreeModel +{ + private instanceMap = new Map(); + + public codeScope: ICodeScope; + + public widgets: IWidget[] = []; + + 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(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[] { + return nodes.map((node) => { + const widget = new Widget(node, this); + + if (isComponentNode(node) && node.children?.length) { + widget.children = this.buildWidgets(node.children); + } + + return widget; + }); + } +} diff --git a/packages/renderer-core/src/parts/component-tree-model/treeModelService.ts b/packages/renderer-core/src/parts/component-tree-model/treeModelService.ts new file mode 100644 index 000000000..953ef1b25 --- /dev/null +++ b/packages/renderer-core/src/parts/component-tree-model/treeModelService.ts @@ -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( + componentsTree: Spec.ComponentTree, + options: ComponentTreeModelOptions, + ): IComponentTreeModel; + + createById( + id: string, + options: ComponentTreeModelOptions, + ): IComponentTreeModel; +} + +export const IComponentTreeModelService = createDecorator( + 'componentTreeModelService', +); + +@Provide(IComponentTreeModelService) +export class ComponentTreeModelService implements IComponentTreeModelService { + constructor( + @ISchemaService private schemaService: ISchemaService, + @ICodeRuntimeService private codeRuntimeService: ICodeRuntimeService, + ) {} + + create( + componentsTree: Spec.ComponentTree, + options: ComponentTreeModelOptions, + ): IComponentTreeModel { + return new ComponentTreeModel(componentsTree, this.codeRuntimeService, options); + } + + createById( + id: string, + options: ComponentTreeModelOptions, + ): IComponentTreeModel { + 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); + } +} diff --git a/packages/renderer-core/src/parts/extension/boosts.ts b/packages/renderer-core/src/parts/extension/boosts.ts new file mode 100644 index 000000000..e3707104c --- /dev/null +++ b/packages/renderer-core/src/parts/extension/boosts.ts @@ -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 = IBoostsApi & Extends; + +export interface IBoostsApi { + readonly codeRuntime: ICodeRuntimeService; + + readonly intl: Pick; + + readonly util: Pick; +} + +/** + * 提供了与运行时交互的接口 + */ +export interface IBoostsService { + extend(name: string, value: any, force?: boolean): void; + extend(value: PlainObject, force?: boolean): void; + + toExpose(): IBoosts; +} + +export const IBoostsService = createDecorator('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(): IBoosts { + 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; + } +} diff --git a/packages/renderer-core/src/parts/extension/extensionHostService.ts b/packages/renderer-core/src/parts/extension/extensionHostService.ts new file mode 100644 index 000000000..8d17f61e3 --- /dev/null +++ b/packages/renderer-core/src/parts/extension/extensionHostService.ts @@ -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; + + getPlugin(name: string): Plugin | undefined; + + /* =========== render =============== */ + runRender(adapter: RenderAdapter): Promise; + + dispose(): Promise; +} + +export const IExtensionHostService = + createDecorator('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(adapter: RenderAdapter): Promise { + invariant(adapter, 'render adapter not settled', 'ExtensionHostService'); + + return adapter({ + schema: this.schemaService, + packageManager: this.packageManagementService, + boostsManager: this.boostsService, + componentTreeModel: this.componentTreeModelService, + }); + } + + async dispose(): Promise { + 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); + } + } +} diff --git a/packages/renderer-core/src/parts/extension/index.ts b/packages/renderer-core/src/parts/extension/index.ts new file mode 100644 index 000000000..e5c9d94ff --- /dev/null +++ b/packages/renderer-core/src/parts/extension/index.ts @@ -0,0 +1,4 @@ +export * from './extensionHostService'; +export * from './plugin'; +export * from './boosts'; +export * from './render'; diff --git a/packages/renderer-core/src/parts/extension/plugin.ts b/packages/renderer-core/src/parts/extension/plugin.ts new file mode 100644 index 000000000..1a5d6fac2 --- /dev/null +++ b/packages/renderer-core/src/parts/extension/plugin.ts @@ -0,0 +1,20 @@ +import { EventEmitter, KeyValueStore } from '@alilc/lowcode-shared'; +import { type RendererApplication } from '../../types'; +import { IBoosts } from './boosts'; + +export interface PluginContext { + eventEmitter: EventEmitter; + globalState: KeyValueStore; + + boosts: IBoosts; +} + +export interface Plugin { + /** + * 插件的 name 作为唯一标识,并不可重复。 + */ + name: string; + setup(app: RendererApplication, context: PluginContext): void | Promise; + destory?(): void | Promise; + dependsOn?: string[]; +} diff --git a/packages/renderer-core/src/parts/extension/render.ts b/packages/renderer-core/src/parts/extension/render.ts new file mode 100644 index 000000000..d9c8b9a28 --- /dev/null +++ b/packages/renderer-core/src/parts/extension/render.ts @@ -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; + unmount: () => void | Promise; +} + +export interface RenderContext { + readonly schema: Omit; + + readonly packageManager: IPackageManagementService; + + readonly boostsManager: IBoostsService; + + readonly componentTreeModel: IComponentTreeModelService; +} + +export interface RenderAdapter { + (context: RenderContext): Render | Promise; +} diff --git a/packages/renderer-core/src/parts/package/index.ts b/packages/renderer-core/src/parts/package/index.ts new file mode 100644 index 000000000..9318ebf3d --- /dev/null +++ b/packages/renderer-core/src/parts/package/index.ts @@ -0,0 +1,2 @@ +export * from './loader'; +export * from './managementService'; diff --git a/packages/renderer-core/src/parts/package/loader.ts b/packages/renderer-core/src/parts/package/loader.ts new file mode 100644 index 000000000..2188cafe6 --- /dev/null +++ b/packages/renderer-core/src/parts/package/loader.ts @@ -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; + active(info: Spec.Package): boolean; +} + +export function definePackageLoader(loader: PackageLoader) { + return loader; +} diff --git a/packages/renderer-core/src/parts/package/managementService.ts b/packages/renderer-core/src/parts/package/managementService.ts new file mode 100644 index 000000000..7d815d7d0 --- /dev/null +++ b/packages/renderer-core/src/parts/package/managementService.ts @@ -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; + /** 通过包名获取资产包信息 */ + 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( + componentMaps?: Spec.ComponentMap[], + ): Record; + /** 通过组件名获取对应的组件 */ + getComponent(componentName: string): C | LowCodeComponent | undefined; + /** 注册组件 */ + registerComponentByName(componentName: string, Component: unknown): void; + + /** 新增资产包加载器 */ + addPackageLoader(loader: PackageLoader): void; +} + +export const IPackageManagementService = createDecorator( + 'packageManagementService', +); + +@Provide(IPackageManagementService) +export class PackageManagementService implements IPackageManagementService { + private componentsRecord: Record = {}; + + private packageStore: Map = ((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); + } + } +} diff --git a/packages/renderer-core/src/parts/runtimeIntl.ts b/packages/renderer-core/src/parts/runtimeIntl.ts new file mode 100644 index 000000000..825fea548 --- /dev/null +++ b/packages/renderer-core/src/parts/runtimeIntl.ts @@ -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; + 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'); + +@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({ + i18n: (key, params) => { + return this.t({ key, params }); + }, + getLocale: () => { + return this.getLocale(); + }, + setLocale: (locale) => { + this.setLocale(locale); + }, + }); + } + + return this._expose; + } +} diff --git a/packages/renderer-core/src/parts/runtimeUtil.ts b/packages/renderer-core/src/parts/runtimeUtil.ts new file mode 100644 index 000000000..789680168 --- /dev/null +++ b/packages/renderer-core/src/parts/runtimeUtil.ts @@ -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('rendererUtilService'); + +@Provide(IRuntimeUtilService) +export class RuntimeUtilService implements IRuntimeUtilService { + private utilsMap: Map = 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; + } + } + } +} diff --git a/packages/renderer-core/src/parts/schema/index.ts b/packages/renderer-core/src/parts/schema/index.ts new file mode 100644 index 000000000..b9ab40440 --- /dev/null +++ b/packages/renderer-core/src/parts/schema/index.ts @@ -0,0 +1 @@ +export * from './schemaService'; diff --git a/packages/renderer-core/src/parts/schema/schemaService.ts b/packages/renderer-core/src/parts/schema/schemaService.ts new file mode 100644 index 000000000..e7b873f50 --- /dev/null +++ b/packages/renderer-core/src/parts/schema/schemaService.ts @@ -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(key: K): NormalizedSchema[K]; + + set(key: K, value: NormalizedSchema[K]): void; + + onValueChange( + key: K, + listener: (value: NormalizedSchema[K]) => void, + ): EventDisposable; +} + +export const ISchemaService = createDecorator('schemaService'); + +@Provide(ISchemaService) +export class SchemaService implements ISchemaService { + private store: KeyValueStore; + + constructor() { + this.store = new KeyValueStore(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(key: K, value: NormalizedSchema[K]): void { + this.store.set(key, value); + } + + get(key: K): NormalizedSchema[K] { + return this.store.get(key) as NormalizedSchema[K]; + } + + onValueChange( + key: K, + listener: (value: NormalizedSchema[K]) => void, + ): EventDisposable { + return this.store.onValueChange(key, listener); + } +} diff --git a/packages/renderer-core/src/parts/schema/validation.ts b/packages/renderer-core/src/parts/schema/validation.ts new file mode 100644 index 000000000..c96b185fb --- /dev/null +++ b/packages/renderer-core/src/parts/schema/validation.ts @@ -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(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; +} diff --git a/packages/renderer-core/src/parts/widget/index.ts b/packages/renderer-core/src/parts/widget/index.ts new file mode 100644 index 000000000..3171f7e1f --- /dev/null +++ b/packages/renderer-core/src/parts/widget/index.ts @@ -0,0 +1 @@ +export * from './widget'; diff --git a/packages/renderer-core/src/parts/widget/widget.ts b/packages/renderer-core/src/parts/widget/widget.ts new file mode 100644 index 000000000..bdbf99ab3 --- /dev/null +++ b/packages/renderer-core/src/parts/widget/widget.ts @@ -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 { + key: string; + + node: Spec.NodeType; + + model: IComponentTreeModel; + + children?: IWidget[]; +} + +export interface IWidget { + readonly key: string; + + readonly node: Spec.NodeType; + + children?: IWidget[]; + + beforeBuild(beforeGuard: (node: T) => T): EventDisposable; + + build( + builder: (context: WidgetBuildContext) => Element, + ): Element; +} + +export class Widget + implements IWidget +{ + private beforeGuardCallbacks = createCallback(); + + public __raw: Spec.NodeType; + + public node: Spec.NodeType; + + public key: string; + + public children?: IWidget[] | undefined; + + constructor( + node: Spec.NodeType, + private model: IComponentTreeModel, + ) { + this.node = clone(node); + this.__raw = node; + this.key = (node as Spec.ComponentNode)?.id ?? uniqueId(); + } + + beforeBuild(beforeGuard: (node: T) => T): EventDisposable { + return this.beforeGuardCallbacks.add(beforeGuard); + } + + build( + builder: (context: WidgetBuildContext) => 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, + }); + } +} diff --git a/packages/renderer-core/src/plugin.ts b/packages/renderer-core/src/plugin.ts deleted file mode 100644 index 849b2a08d..000000000 --- a/packages/renderer-core/src/plugin.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { type AppContext } from './api/app'; -import { nonSetterProxy } from './utils/non-setter-proxy'; - -export interface Plugin { - name: string; // 插件的 name 作为唯一标识,并不可重复。 - setup(setupContext: C): void | Promise; - 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>(plugin: P) { - return plugin; -} diff --git a/packages/renderer-core/src/schema.ts b/packages/renderer-core/src/schema.ts deleted file mode 100644 index c63617042..000000000 --- a/packages/renderer-core/src/schema.ts +++ /dev/null @@ -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(key: K): Project[K] | undefined; - updateByKey( - 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>(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>( - target: T[], - comparison: string, - comparisonValue: any, -) { - const idx = target.findIndex((item) => item[comparison] === comparisonValue); - if (idx > -1) target.splice(idx, 1); -} diff --git a/packages/renderer-core/src/types.ts b/packages/renderer-core/src/types.ts new file mode 100644 index 000000000..95144eee5 --- /dev/null +++ b/packages/renderer-core/src/types.ts @@ -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 = { + readonly mode: 'development' | 'production'; + + readonly schema: Omit; + + readonly packageManager: IPackageManagementService; + + use: IExtensionHostService['registerPlugin']; +} & Render; diff --git a/packages/renderer-core/src/utils/error.ts b/packages/renderer-core/src/utils/error.ts deleted file mode 100644 index 759493c6a..000000000 --- a/packages/renderer-core/src/utils/error.ts +++ /dev/null @@ -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); -} diff --git a/packages/renderer-core/src/utils/guid.ts b/packages/renderer-core/src/utils/guid.ts deleted file mode 100644 index 7081041bf..000000000 --- a/packages/renderer-core/src/utils/guid.ts +++ /dev/null @@ -1,8 +0,0 @@ -let idStart = 0x0907; - -/** - * Generate unique id - */ -export function guid(): number { - return idStart++; -} diff --git a/packages/renderer-core/src/utils/hook.ts b/packages/renderer-core/src/utils/hook.ts deleted file mode 100644 index f68df026d..000000000 --- a/packages/renderer-core/src/utils/hook.ts +++ /dev/null @@ -1,177 +0,0 @@ -import type { AnyFunction } from '../types'; - -export type EventName = string | number | symbol; - -export function createEvent() { - 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 = ReturnType>; - -export type HookCallback = (...args: any) => Promise | any; - -type HookKeys = keyof T & PropertyKey; - -type InferCallback = 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: any>(fn: T) => ReturnType; - }; - } -} - -// https://developer.chrome.com/blog/devtools-modern-web-debugging/#linked-stack-traces -type CreateTask = typeof console.createTask; -const defaultTask: ReturnType = { run: (fn) => fn() }; -const _createTask: CreateTask = () => defaultTask; -const createTask = typeof console.createTask !== 'undefined' ? console.createTask : _createTask; - -export interface HookStore< - HooksT extends Record = Record, - HookNameT extends HookKeys = HookKeys, -> { - hook(name: NameT, fn: InferCallback): () => void; - - call( - name: NameT, - ...args: Parameters> - ): void; - callAsync( - name: NameT, - ...args: Parameters> - ): Promise; - callParallel( - name: NameT, - ...args: Parameters> - ): Promise; - - remove(name: NameT, fn?: InferCallback): void; - - clear(name?: NameT): void; - - getHooks(name: NameT): InferCallback[] | undefined; -} - -export function createHookStore< - HooksT extends Record = Record, - HookNameT extends HookKeys = HookKeys, ->(): HookStore { - const hooksMap = new Map>(); - - function hook(name: NameT, fn: InferCallback) { - 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( - name: NameT, - ...args: Parameters> - ) { - const hooks = hooksMap.get(name)?.list() ?? []; - - for (const hookFn of hooks) { - hookFn.call(null, ...args); - } - } - - function callAsync( - name: NameT, - ...args: Parameters> - ) { - 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( - name: NameT, - ...args: Parameters> - ) { - const hooks = hooksMap.get(name)?.list() ?? []; - const task = createTask(name.toString()); - return Promise.all(hooks.map((hook) => task.run(() => hook(...args)))); - } - - function remove(name: NameT, fn?: InferCallback) { - 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(name?: NameT) { - if (name) { - remove(name); - } else { - hooksMap.clear(); - } - } - - function getHooks( - name: NameT, - ): InferCallback[] | undefined { - return hooksMap.get(name)?.list() as InferCallback[] | undefined; - } - - return { - hook, - - call, - callAsync, - callParallel, - - remove, - clear, - - getHooks, - }; -} diff --git a/packages/renderer-core/src/utils/non-setter-proxy.ts b/packages/renderer-core/src/utils/non-setter-proxy.ts deleted file mode 100644 index 28314ee62..000000000 --- a/packages/renderer-core/src/utils/non-setter-proxy.ts +++ /dev/null @@ -1,13 +0,0 @@ -export function nonSetterProxy(target: T) { - return new Proxy(target, { - get(target, p, receiver) { - return Reflect.get(target, p, receiver); - }, - set() { - return false; - }, - has(target, p) { - return Reflect.has(target, p); - }, - }); -} diff --git a/packages/renderer-core/src/widget.ts b/packages/renderer-core/src/widget.ts deleted file mode 100644 index d49de3e57..000000000 --- a/packages/renderer-core/src/widget.ts +++ /dev/null @@ -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 { - 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) => Element | undefined) { - this.renderObject = mapper(this); - return this; - } - - addProxyELements(el: Element) { - this.proxyElements.unshift(el); - } - - build(builder: (elements: Element[]) => C): C { - return builder(this.renderObject ? [...this.proxyElements, this.renderObject] : []); - } -} - -export type TextWidgetData = Exclude; -export type TextWidgetType = 'string' | 'expression' | 'i18n'; - -export class TextWidget extends Widget { - type: TextWidgetType = 'string'; - - protected init() { - if (isJSExpression(this.raw)) { - this.type = 'expression'; - } else if (isI18nNode(this.raw)) { - this.type = 'i18n'; - } - } -} - -export class ComponentWidget extends Widget { - private _children: (TextWidget | ComponentWidget)[] = []; - 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(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(data: NodeType) { - if (typeof data === 'string' || isJSExpression(data) || isI18nNode(data)) { - return new TextWidget(data); - } else if (data.componentName) { - return new ComponentWidget(data); - } - - throw Error(`unknown node data: ${JSON.stringify(data)}`); -} diff --git a/packages/renderer-router/package.json b/packages/renderer-router/package.json index 64c9caefe..c6fb63628 100644 --- a/packages/renderer-router/package.json +++ b/packages/renderer-router/package.json @@ -22,7 +22,7 @@ "test": "vitest" }, "dependencies": { - "@alilc/lowcode-renderer-core": "workspace:*", + "@alilc/lowcode-shared": "workspace:*", "lodash-es": "^4.17.21", "path-to-regexp": "^6.2.1", "qs": "^6.12.0" @@ -31,9 +31,6 @@ "@types/lodash-es": "^4.17.12", "@types/qs": "^6.9.13" }, - "peerDependencies": { - "@alilc/lowcode-renderer-core": "workspace:*" - }, "publishConfig": { "access": "public", "registry": "https://registry.npmjs.org/" diff --git a/packages/renderer-router/src/guard.ts b/packages/renderer-router/src/guard.ts index 361c799ca..d42d389a4 100644 --- a/packages/renderer-router/src/guard.ts +++ b/packages/renderer-router/src/guard.ts @@ -1,5 +1,4 @@ -import { RawRouteLocation } from '@alilc/lowcode-renderer-core'; -import { type RouteLocationNormalized } from './types'; +import { type RouteLocationNormalized, type RawRouteLocation } from './types'; import { isRouteLocation } from './utils/helper'; export type NavigationHookAfter = ( diff --git a/packages/renderer-router/src/history.ts b/packages/renderer-router/src/history.ts index 34505c651..46f55882c 100644 --- a/packages/renderer-router/src/history.ts +++ b/packages/renderer-router/src/history.ts @@ -1,6 +1,12 @@ -import { createEvent } from '@alilc/lowcode-renderer-core'; +import { createCallback } from './utils/callback'; +/** + * history state + */ export type HistoryState = History['state']; +/** + * history locaiton + */ export type HistoryLocation = string; export enum NavigationType { @@ -166,8 +172,8 @@ export function createBrowserHistory(base?: string): RouterHistory { currentLocation = to; } - const listeners = createEvent(); - const teardowns = createEvent<() => void>(); + const listeners = createCallback(); + const teardowns = createCallback<() => void>(); let pauseState: HistoryLocation | null = null; @@ -266,7 +272,7 @@ export function createBrowserHistory(base?: string): RouterHistory { function normalizeBase(base?: string) { if (!base) { // strip full URL origin - base = document.baseURI.replace(/^\w+:\/\/[^\/]+/, ''); + base = document.baseURI.replace(/^\w+:\/\/[^/]+/, ''); } // 处理边界问题 确保是一个浏览器路径 如 /xxx #/xxx @@ -348,7 +354,7 @@ export function createMemoryHistory(base = ''): RouterHistory { historyStack.push({ location, state }); } - const listeners = createEvent(); + const listeners = createCallback(); function triggerListeners( to: HistoryLocation, diff --git a/packages/renderer-router/src/index.ts b/packages/renderer-router/src/index.ts index 35115c10c..02e72cb41 100644 --- a/packages/renderer-router/src/index.ts +++ b/packages/renderer-router/src/index.ts @@ -1,7 +1,8 @@ export { createRouter } from './router'; export { createBrowserHistory, createHashHistory, createMemoryHistory } from './history'; -export type { RouterHistory } from './history'; -export type { NavigationGuard, NavigationHookAfter } from './guard'; +export type * from './types'; +export type * from './history'; +export type { NavigationGuard, NavigationHookAfter, NavigationGuardReturn } from './guard'; export type { Router, RouterOptions } from './router'; -export * from './types'; +export type { PathParserOptions } from './utils/path-parser'; diff --git a/packages/renderer-router/src/matcher.ts b/packages/renderer-router/src/matcher.ts index 9c82f82b1..e5bac15bb 100644 --- a/packages/renderer-router/src/matcher.ts +++ b/packages/renderer-router/src/matcher.ts @@ -1,11 +1,15 @@ // 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 { 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 { /** @@ -60,9 +64,7 @@ export interface RouterMatcher { * @param location - MatcherLocationRaw to resolve to a url * @param currentLocation - MatcherLocation of the current location */ - resolve: ( - location: RawLocation, currentLocation: MatcherLocation - ) => MatcherLocation; + resolve: (location: RawLocation, currentLocation: MatcherLocation) => MatcherLocation; } export function createRouterMatcher( @@ -104,8 +106,7 @@ export function createRouterMatcher( while ( i < matchers.length && comparePathParserScore(matcher, matchers[i]) >= 0 && - (matcher.record.path !== matchers[i].record.path || - !isRecordChildOf(matcher, matchers[i])) + (matcher.record.path !== matchers[i].record.path || !isRecordChildOf(matcher, matchers[i])) ) { i++; } @@ -139,10 +140,7 @@ export function createRouterMatcher( return matcherMap.get(name); } - function resolve( - location: RawLocation, - currentLocation: MatcherLocation - ): MatcherLocation { + function resolve(location: RawLocation, currentLocation: MatcherLocation): MatcherLocation { let matcher: RouteRecordMatcher | undefined; let params: PathParams = {}; let path: MatcherLocation['path']; @@ -163,17 +161,15 @@ export function createRouterMatcher( paramsFromLocation( currentLocation.params ?? {}, matcher.keys - .filter(k => !k.optional) - .concat( - matcher.parent ? matcher.parent.keys.filter(k => k.optional) : [] - ) + .filter((k) => !k.optional) + .concat(matcher.parent ? matcher.parent.keys.filter((k) => k.optional) : []) .map((k) => k.name), ), location.params ? paramsFromLocation( - location.params, - matcher.keys.map((k) => k.name), - ) + location.params, + matcher.keys.map((k) => k.name), + ) : {}, ); @@ -253,11 +249,6 @@ export function normalizeRouteRecord(record: RouteRecord): RouteRecordNormalized }; } -function isRecordChildOf( - record: RouteRecordMatcher, - parent: RouteRecordMatcher -): boolean { - return parent.children.some( - child => child === record || isRecordChildOf(record, child) - ); +function isRecordChildOf(record: RouteRecordMatcher, parent: RouteRecordMatcher): boolean { + return parent.children.some((child) => child === record || isRecordChildOf(record, child)); } diff --git a/packages/renderer-router/src/router.ts b/packages/renderer-router/src/router.ts index 439b0832f..bde98fe8c 100644 --- a/packages/renderer-router/src/router.ts +++ b/packages/renderer-router/src/router.ts @@ -1,11 +1,4 @@ -import { - type RouterApi, - type RouterConfig, - type RouteLocation, - createEvent, - type RawRouteLocation, - type RawLocationOptions, -} from '@alilc/lowcode-renderer-core'; +import { type Spec } from '@alilc/lowcode-shared'; import { createBrowserHistory, createHashHistory, @@ -17,14 +10,21 @@ import { createRouterMatcher } from './matcher'; import { type PathParserOptions, type PathParams } from './utils/path-parser'; import { parseURL, stringifyURL } from './utils/url'; 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 { createCallback } from './utils/callback'; -export interface RouterOptions extends RouterConfig, PathParserOptions { +export interface RouterOptions extends Spec.RouterConfig, PathParserOptions { routes: RouteRecord[]; } -export interface Router extends RouterApi { +export interface Router extends Spec.RouterApi { readonly options: RouterOptions; readonly history: RouterHistory; @@ -56,13 +56,7 @@ const START_LOCATION: RouteLocationNormalized = { redirectedFrom: undefined, }; -const defaultRouterOptions: RouterOptions = { - historyMode: 'browser', - baseName: '/', - routes: [], -}; - -export function createRouter(options: RouterOptions = defaultRouterOptions): Router { +export function createRouter(options: RouterOptions): Router { const { baseName = '/', historyMode = 'browser', routes = [], ...globalOptions } = options; const matcher = createRouterMatcher(routes, globalOptions); const routerHistory = @@ -72,8 +66,8 @@ export function createRouter(options: RouterOptions = defaultRouterOptions): Rou ? createMemoryHistory(baseName) : createBrowserHistory(baseName); - const beforeGuards = createEvent(); - const afterGuards = createEvent(); + const beforeGuards = createCallback(); + const afterGuards = createCallback(); let currentLocation: RouteLocationNormalized = START_LOCATION; let pendingLocation = currentLocation; diff --git a/packages/renderer-router/src/types.ts b/packages/renderer-router/src/types.ts index d13253d28..bf694fa4b 100644 --- a/packages/renderer-router/src/types.ts +++ b/packages/renderer-router/src/types.ts @@ -1,12 +1,12 @@ -import type { - RouteRecord as RouterRecordSpec, - RouteLocation, - PlainObject, - RawRouteLocation, -} from '@alilc/lowcode-renderer-core'; +import type { Spec, PlainObject } from '@alilc/lowcode-shared'; 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; redirect?: | string diff --git a/packages/renderer-router/src/utils/callback.ts b/packages/renderer-router/src/utils/callback.ts new file mode 100644 index 000000000..490a6c6c4 --- /dev/null +++ b/packages/renderer-router/src/utils/callback.ts @@ -0,0 +1,30 @@ +import type { AnyFunction } from '@alilc/lowcode-shared'; + +export function createCallback() { + 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; + }, + }; +} diff --git a/packages/renderer-router/src/utils/helper.ts b/packages/renderer-router/src/utils/helper.ts index 46a611efd..b5bb3d630 100644 --- a/packages/renderer-router/src/utils/helper.ts +++ b/packages/renderer-router/src/utils/helper.ts @@ -1,5 +1,4 @@ -import type { RawRouteLocation } from '@alilc/lowcode-renderer-core'; -import type { RouteLocationNormalized } from '../types'; +import type { RouteLocationNormalized, RawRouteLocation } from '../types'; export function isRouteLocation(route: any): route is RawRouteLocation { return typeof route === 'string' || (route && typeof route === 'object'); diff --git a/packages/renderer-router/src/utils/path-parser/parser-ranker.ts b/packages/renderer-router/src/utils/path-parser/parser-ranker.ts index 12d2b16cd..cd453b5ea 100644 --- a/packages/renderer-router/src/utils/path-parser/parser-ranker.ts +++ b/packages/renderer-router/src/utils/path-parser/parser-ranker.ts @@ -8,26 +8,26 @@ export type PathParams = Record; * A param in a url like `/users/:id` */ interface PathParserParamKey { - name: string - repeatable: boolean - optional: boolean + name: string; + repeatable: boolean; + optional: boolean; } export interface PathParser { /** * The regexp used to match a url */ - re: RegExp + re: RegExp; /** * The score of the parser */ - score: Array + score: Array; /** * 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 * 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 * no match */ - parse(path: string): PathParams | null + parse(path: string): PathParams | null; /** * Creates a string version of the url @@ -45,26 +45,23 @@ export interface PathParser { * @param params - object of params * @returns a url */ - stringify(params: PathParams): string + stringify(params: PathParams): string; } -/** - * @internal - */ export interface _PathParserOptions { /** * Makes the RegExp case-sensitive. * * @defaultValue `false` */ - sensitive?: boolean + sensitive?: boolean; /** * Whether to disallow a trailing slash or not. * * @defaultValue `false` */ - strict?: boolean + strict?: boolean; /** * Should the RegExp match from the beginning by prepending a `^` to it. @@ -72,20 +69,17 @@ export interface _PathParserOptions { * * @defaultValue `true` */ - start?: boolean + start?: boolean; /** * Should the RegExp match until the end by appending a `$` to it. * * @defaultValue `true` */ - end?: boolean + end?: boolean; } -export type PathParserOptions = Pick< - _PathParserOptions, - 'end' | 'sensitive' | 'strict' ->; +export type PathParserOptions = Pick<_PathParserOptions, 'end' | 'sensitive' | 'strict'>; // default pattern for a param: non-greedy everything but / const BASE_PARAM_PATTERN = '[^/]+?'; @@ -126,7 +120,7 @@ const REGEX_CHARS_RE = /[.+*?^${}()[\]/\\]/g; */ export function tokensToParser( segments: Array, - extraOptions?: _PathParserOptions + extraOptions?: _PathParserOptions, ): PathParser { const options = Object.assign({}, BASE_PATH_PARSER_OPTIONS, extraOptions); @@ -147,8 +141,7 @@ export function tokensToParser( const token = segment[tokenIndex]; // resets the score if we are inside a sub-segment /:a-other-:b let subSegmentScore: number = - PathScore.Segment + - (options.sensitive ? PathScore.BonusCaseSensitive : 0); + PathScore.Segment + (options.sensitive ? PathScore.BonusCaseSensitive : 0); if (token.type === TokenType.Static) { // prepend the slash if we are starting a new segment @@ -171,8 +164,7 @@ export function tokensToParser( new RegExp(`(${re})`); } catch (err) { throw new Error( - `Invalid custom RegExp for param "${value}" (${re}): ` + - (err as Error).message + `Invalid custom RegExp for param "${value}" (${re}): ` + (err as Error).message, ); } } @@ -185,9 +177,7 @@ export function tokensToParser( subPattern = // avoid an optional / if there are more segments e.g. /:p?-static // or /:p?-:p2 - optional && segment.length < 2 - ? `(?:/${subPattern})` - : '/' + subPattern; + optional && segment.length < 2 ? `(?:/${subPattern})` : '/' + subPattern; if (optional) subPattern += '?'; pattern += subPattern; @@ -250,12 +240,11 @@ export function tokensToParser( path += token.value; } else if (token.type === TokenType.Param) { const { value, repeatable, optional } = token; - const param: string | readonly string[] = - value in params ? params[value] : ''; + const param: string | readonly string[] = value in params ? params[value] : ''; if (Array.isArray(param) && !repeatable) { 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 // otherwise sort the longest segment first if (a.length < b.length) { - return a.length === 1 && a[0] === PathScore.Static + PathScore.Segment - ? -1 - : 1; + return a.length === 1 && a[0] === PathScore.Static + PathScore.Segment ? -1 : 1; } else if (a.length > b.length) { - return b.length === 1 && b[0] === PathScore.Static + PathScore.Segment - ? 1 - : -1; + return b.length === 1 && b[0] === PathScore.Static + PathScore.Segment ? 1 : -1; } return 0; diff --git a/packages/shared/package.json b/packages/shared/package.json index fa19854a1..b2f17d027 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -1,11 +1,27 @@ { "name": "@alilc/lowcode-shared", - "version": "2.0.0-beta.0", - "private": true, + "version": "1.0.0-alpha.0", "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": { + "@abraham/reflection": "^0.12.0", + "@formatjs/intl": "^2.10.2", "@vue/reactivity": "^3.4.23", + "inversify": "^6.0.2", + "inversify-binding-decorators": "^4.0.0", "hookable": "^5.5.3", "lodash-es": "^4.17.21", "store": "^2.0.12" @@ -13,5 +29,15 @@ "devDependencies": { "@types/lodash-es": "^4.17.12", "@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" } diff --git a/packages/shared/src/abilities/event.ts b/packages/shared/src/abilities/event.ts new file mode 100644 index 000000000..7ef4ddf92 --- /dev/null +++ b/packages/shared/src/abilities/event.ts @@ -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 = Record, + HookNameT extends HookKeys = HookKeys, +> { + /** + * 监听事件 + * 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; + + /** + * 取消监听事件 + * 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 = Record, + HookNameT extends HookKeys = HookKeys, +> implements IEventEmitter +{ + private namespace: string | undefined; + private hooks = new Hookable(); + + 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>( + namespace?: string, +): EventEmitter { + return new EventEmitter(namespace); +} diff --git a/packages/shared/src/abilities/index.ts b/packages/shared/src/abilities/index.ts new file mode 100644 index 000000000..3d75c2d6e --- /dev/null +++ b/packages/shared/src/abilities/index.ts @@ -0,0 +1,5 @@ +export * from './event'; +export * from './logger'; +export * from './storage'; +export * from './intl'; +export * from './instantiation'; diff --git a/packages/shared/src/abilities/instantiation/index.ts b/packages/shared/src/abilities/instantiation/index.ts new file mode 100644 index 000000000..50196be9c --- /dev/null +++ b/packages/shared/src/abilities/instantiation/index.ts @@ -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 { + (...args: any[]): void; + type: T; +} + +export type Constructor = new (...args: any[]) => T; + +export function createDecorator(serviceId: string): ServiceIdentifier { + const id = ( + 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(serviceId: ServiceIdentifier, 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(serviceIdentifier: ServiceIdentifier) { + return this.container.get(serviceIdentifier); + } + + set(serviceIdentifier: ServiceIdentifier, constructor: Constructor) { + this.container.bind(serviceIdentifier).to(constructor); + } + + createInstance(App: T) { + return this.container.resolve>(App); + } + + bootstrapModules() { + this.container.load(buildProviderModule()); + } +} diff --git a/packages/shared/src/abilities/intl.ts b/packages/shared/src/abilities/intl.ts new file mode 100644 index 000000000..6362f62c3 --- /dev/null +++ b/packages/shared/src/abilities/intl.ts @@ -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; +export type LocaleTranslationsRecord = Record; + +export class Intl { + private locale: Signal; + private messageStore: Signal; + private currentMessage: ComputedSignal; + 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 = { + 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('-'); +} diff --git a/packages/shared/src/parts/logger.ts b/packages/shared/src/abilities/logger.ts similarity index 100% rename from packages/shared/src/parts/logger.ts rename to packages/shared/src/abilities/logger.ts diff --git a/packages/shared/src/abilities/storage.ts b/packages/shared/src/abilities/storage.ts new file mode 100644 index 000000000..bc811ffbe --- /dev/null +++ b/packages/shared/src/abilities/storage.ts @@ -0,0 +1,151 @@ +import { invariant } from '../utils'; +import { PlainObject } from '../types'; + +/** + * MapLike interface + */ +export interface IStore { + 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 { + 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 = 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(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); + } + } +} diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts index 7772bb8e8..6b3ff6d0c 100644 --- a/packages/shared/src/index.ts +++ b/packages/shared/src/index.ts @@ -1,4 +1,4 @@ export * from './types'; export * from './utils'; export * from './signals'; -export * from './parts'; +export * from './abilities'; diff --git a/packages/shared/src/parts/event.ts b/packages/shared/src/parts/event.ts deleted file mode 100644 index 3f2261b53..000000000 --- a/packages/shared/src/parts/event.ts +++ /dev/null @@ -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 = Record, - HookNameT extends HookKeys = HookKeys, -> extends Hookable { - #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>(namespace?: string): EventBus { - return new EventBus(namespace); -} diff --git a/packages/shared/src/parts/index.ts b/packages/shared/src/parts/index.ts deleted file mode 100644 index c8e07139d..000000000 --- a/packages/shared/src/parts/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './event'; -export * from './logger'; -export * from './persistence'; diff --git a/packages/shared/src/parts/persistence.ts b/packages/shared/src/parts/persistence.ts deleted file mode 100644 index c44d7b8d5..000000000 --- a/packages/shared/src/parts/persistence.ts +++ /dev/null @@ -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; - - 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(); - } -} diff --git a/packages/shared/src/signals.ts b/packages/shared/src/signals.ts index ff0e4c1b8..a87082fdb 100644 --- a/packages/shared/src/signals.ts +++ b/packages/shared/src/signals.ts @@ -32,7 +32,7 @@ export type WatchCallback = ( onCleanup: OnCleanup, ) => any; -type OnCleanup = (cleanupFn: () => void) => void; +export type OnCleanup = (cleanupFn: () => void) => void; export interface WatchOptions { immediate?: Immediate; @@ -42,7 +42,7 @@ export interface WatchOptions { const INITIAL_WATCHER_VALUE = {}; -type MultiWatchSources = (WatchSource | object)[]; +export type MultiWatchSources = (WatchSource | object)[]; export type WatchStopHandle = () => void; @@ -51,7 +51,7 @@ export function watchEffect(effect: WatchEffect): WatchStopHandle { return doWatch(effect, null); } -type MapSources = { +export type MapSources = { [K in keyof T]: T[K] extends WatchSource ? Immediate extends true ? V | undefined diff --git a/packages/shared/src/types/base.ts b/packages/shared/src/types/base.ts deleted file mode 100644 index fee7234fc..000000000 --- a/packages/shared/src/types/base.ts +++ /dev/null @@ -1,5 +0,0 @@ -export type VoidFunction = (...args: any[]) => void; - -export type AnyFunction = (...args: any[]) => any; - -export type PlainObject = Record; diff --git a/packages/shared/src/types/index.ts b/packages/shared/src/types/index.ts index 12ebf3016..3dac624f9 100644 --- a/packages/shared/src/types/index.ts +++ b/packages/shared/src/types/index.ts @@ -1,6 +1,11 @@ -export * from './base'; +import * as Spec from './specs'; + +export { Spec }; + export * from './material'; -export * from './specs/asset-spec'; -export * from './specs/lowcode-spec'; -export * from './specs/runtime-api'; -export * from './specs/material-spec'; + +export type VoidFunction = (...args: any[]) => void; + +export type AnyFunction = (...args: any[]) => any; + +export type PlainObject = Record; diff --git a/packages/shared/src/types/specs/index.ts b/packages/shared/src/types/specs/index.ts new file mode 100644 index 000000000..dd9956367 --- /dev/null +++ b/packages/shared/src/types/specs/index.ts @@ -0,0 +1,4 @@ +export * from './asset-spec'; +export * from './lowcode-spec'; +export * from './runtime'; +export * from './material-spec'; diff --git a/packages/shared/src/types/specs/lowcode-spec.ts b/packages/shared/src/types/specs/lowcode-spec.ts index 64bda3a00..ce878371b 100644 --- a/packages/shared/src/types/specs/lowcode-spec.ts +++ b/packages/shared/src/types/specs/lowcode-spec.ts @@ -27,7 +27,7 @@ export interface Project { /** * 国际化语料 */ - i18n?: I18nMap; + i18n?: LocaleTranslationsMap; /** * 应用范围内的全局常量 */ @@ -40,16 +40,16 @@ export interface Project { /** * 当前应用配置信息 */ - config?: Record; + config?: Record; /** * 当前应用元数据信息 */ - meta?: Record; + meta?: Record; /** * 当前应用的公共数据源 * @deprecated */ - dataSource?: never; + // dataSource?: never; /** * 当前应用的路由配置信息 */ @@ -103,15 +103,13 @@ export interface ComponentMap { * 组件树描述 * 协议中用于描述搭建出来的组件树结构的规范,整个组件树的描述由组件结构&容器结构两种结构嵌套构成。 */ -export type ComponentTree = - ComponentTreeRootNode; +export type ComponentTree = ComponentTreeRoot; /** * 根容器节点结构描述 (A) * 容器是一类特殊的组件,在组件能力基础上增加了对生命周期对象、自定义方法、样式文件、数据源等信息的描述。 */ -export interface ComponentTreeRootNode - extends ComponentTreeNode { +export interface ComponentTreeRoot extends ComponentNode { componentName: 'Page' | 'Block' | 'Component'; /** * 文件名称 @@ -129,7 +127,7 @@ export interface ComponentTreeRootNode * 生命周期对象 */ lifeCycles?: { - [name in LifeCycleNameT]: JSFunction; + [name in ComponentLifeCycle]: JSFunction; }; /** * 自定义方法对象 @@ -141,7 +139,7 @@ export interface ComponentTreeRootNode * 数据源对象 * type todo */ - dataSource?: any; + dataSource?: ComponentDataSource; // for useless loop: never; @@ -149,10 +147,109 @@ export interface ComponentTreeRootNode 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) */ -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 结构描述 */ -export interface ComponentTreeNodeProps { +export interface ComponentNodeProps { /** 组件 ID */ 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 * 国际化多语言支持 */ -export interface I18nMap { - [locale: string]: Record; +export interface I18nTranslations { + [key: string]: string; +} + +export interface LocaleTranslationsMap { + [locale: string]: I18nTranslations; } /** @@ -316,11 +417,11 @@ export interface JSONObject { /** * 节点类型(A) - * 通常用于描述组件的某一个属性为 ReactNode 或 Function-Return-ReactNode 的场景。 + * 通常用于描述组件的某一个属性为 Node 或 Function-Return-Node 的场景。 */ export interface JSSlot { type: 'JSSlot'; - value: ComponentTreeNode | ComponentTreeNode[]; + value: ComponentNode | ComponentNode[]; params?: string[]; } @@ -343,7 +444,7 @@ export interface JSExpression { /** * 国际化多语言类型(AA) */ -export interface I18nNode { +export interface JSI18n { type: 'i18n'; /** * i18n 结构中字段的 key 标识符 @@ -355,4 +456,4 @@ export interface I18nNode { params?: Record; } -export type NodeType = string | JSExpression | I18nNode | ComponentTreeNode; +export type NodeType = string | JSExpression | JSI18n | ComponentNode; diff --git a/packages/shared/src/types/specs/material-spec.ts b/packages/shared/src/types/specs/material-spec.ts index c5734f415..ce32121bc 100644 --- a/packages/shared/src/types/specs/material-spec.ts +++ b/packages/shared/src/types/specs/material-spec.ts @@ -3,8 +3,8 @@ * 对源码组件在低代码搭建平台中使用时所具备的配置能力和交互行为进行规范化描述,让不同平台对组件接入的实现保持一致, * 让组件针对不同的搭建平台接入时可以使用一份统一的描述内容,让组件在不同的业务中流通成为可能。 */ -import { ComponentTree, ComponentTreeNode } from './lowcode-spec'; -import { PlainObject } from '../base'; +import { ComponentTree, ComponentNode } from './lowcode-spec'; +import { PlainObject } from '../index'; export interface LowCodeComponentTree extends ComponentTree { componentName: 'Component'; @@ -230,5 +230,5 @@ export interface Snippet { /** * 待插入的 schema */ - schema?: ComponentTreeNode; + schema?: ComponentNode; } diff --git a/packages/shared/src/types/specs/runtime-api.ts b/packages/shared/src/types/specs/runtime.ts similarity index 85% rename from packages/shared/src/types/specs/runtime-api.ts rename to packages/shared/src/types/specs/runtime.ts index 762a57cd2..6b900d9e2 100644 --- a/packages/shared/src/types/specs/runtime-api.ts +++ b/packages/shared/src/types/specs/runtime.ts @@ -1,4 +1,5 @@ -import { AnyFunction, PlainObject } from '../base'; +import { AnyFunction, PlainObject } from '../index'; +import { JSExpression } from './lowcode-spec'; /** * 在上述事件类型描述和变量类型描述中,在函数或 JS 表达式内,均可以通过 this 对象获取当前组件所在容器的实例化对象 @@ -39,17 +40,48 @@ export interface InstanceStateApi { ): void; } +/** + * 数据源 api + */ export interface InstanceDataSourceApi { /** * 实例的数据源对象 Map */ - dataSourceMap: any; + dataSourceMap: Record; /** * 实例的初始化异步数据请求重载 */ reloadDataSource: () => void; } +/** + * 实例的单个数据源对象 + */ +export interface DataSourceMapItem { + /** + * 调用单个数据源 + * @param params 替换 ComponentDataSourceItemOptions 对象描述中的 params + */ + load(params: any): Promise; + /** + * 数据源请求的返回状态 + */ + 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 params 可选,是用来做模版字符串替换 */ - i18n(i18nKey: string, params?: Record): string; + i18n(key: string, params?: Record): string; /** * 返回当前环境语言 */ diff --git a/packages/shared/src/utils/browser.ts b/packages/shared/src/utils/browser.ts deleted file mode 100644 index 8c6f5ce8c..000000000 --- a/packages/shared/src/utils/browser.ts +++ /dev/null @@ -1,8 +0,0 @@ -const userAgent = navigator.userAgent; - -export const isFirefox = userAgent.indexOf('Firefox') >= 0; -export const isWebKit = userAgent.indexOf('AppleWebKit') >= 0; -export const isChrome = userAgent.indexOf('Chrome') >= 0; -export const isSafari = !isChrome && userAgent.indexOf('Safari') >= 0; -export const isWebkitWebView = !isChrome && !isSafari && isWebKit; -export const isAndroid = userAgent.indexOf('Android') >= 0; diff --git a/packages/shared/src/utils/callback.ts b/packages/shared/src/utils/callback.ts new file mode 100644 index 000000000..015b902d7 --- /dev/null +++ b/packages/shared/src/utils/callback.ts @@ -0,0 +1,30 @@ +import type { AnyFunction } from '../types'; + +export function createCallback() { + 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; + }, + }; +} diff --git a/packages/shared/src/utils/index.ts b/packages/shared/src/utils/index.ts index 73d09114a..d0dbb3b6a 100644 --- a/packages/shared/src/utils/index.ts +++ b/packages/shared/src/utils/index.ts @@ -1,5 +1,6 @@ export * from './invariant'; export * from './is-promise'; export * from './unique-id'; -export * as Browser from './browser'; -export * as Platform from './platform'; +export * from './type-guards'; +export * from './platform'; +export * from './callback'; diff --git a/packages/shared/src/utils/invariant.ts b/packages/shared/src/utils/invariant.ts index 9be50edb8..52a5bb911 100644 --- a/packages/shared/src/utils/invariant.ts +++ b/packages/shared/src/utils/invariant.ts @@ -1,4 +1,4 @@ -export function invariant(check: unknown, message: string, thing?: any) { +export function invariant(check: unknown, message: string, thing?: any): asserts check { if (!check) { throw new Error(`Invariant failed: ${message}${thing ? ` in '${thing}'` : ''}`); } diff --git a/packages/shared/src/utils/type-guards/index.ts b/packages/shared/src/utils/type-guards/index.ts new file mode 100644 index 000000000..97bec9d72 --- /dev/null +++ b/packages/shared/src/utils/type-guards/index.ts @@ -0,0 +1 @@ +export * from './spec'; diff --git a/packages/renderer-core/src/utils/type-guard.ts b/packages/shared/src/utils/type-guards/spec.ts similarity index 58% rename from packages/renderer-core/src/utils/type-guard.ts rename to packages/shared/src/utils/type-guards/spec.ts index 78795f000..61b65a648 100644 --- a/packages/renderer-core/src/utils/type-guard.ts +++ b/packages/shared/src/utils/type-guards/spec.ts @@ -1,26 +1,30 @@ -import type { JSExpression, JSFunction, JSSlot, I18nNode, LowCodeComponent } from '../types'; +import type { Spec, LowCodeComponent } from '../../types'; import { isPlainObject } from 'lodash-es'; -export function isJSExpression(v: unknown): v is JSExpression { +export function isJSExpression(v: unknown): v is Spec.JSExpression { return ( isPlainObject(v) && (v as any).type === 'JSExpression' && typeof (v as any).value === 'string' ); } -export function isJSFunction(v: unknown): v is JSFunction { +export function isJSFunction(v: unknown): v is Spec.JSFunction { return ( isPlainObject(v) && (v as any).type === 'JSFunction' && typeof (v as any).value === 'string' ); } -export function isJSSlot(v: unknown): v is JSSlot { +export function isJSSlot(v: unknown): v is Spec.JSSlot { return isPlainObject(v) && (v as any).type === 'JSSlot' && (v as any).value; } -export function isI18nNode(v: unknown): v is I18nNode { +export function isJSI18nNode(v: unknown): v is Spec.JSI18n { return isPlainObject(v) && (v as any).type === 'i18n' && typeof (v as any).key === 'string'; } +export function isComponentNode(v: unknown): v is Spec.ComponentNode { + return isPlainObject(v) && (v as any).componentName; +} + export function isLowCodeComponentSchema(v: unknown): v is LowCodeComponent { return isPlainObject(v) && (v as any).type === 'lowCode' && (v as any).schema; } diff --git a/playground/package.json b/playground/package.json index b3978a374..93bb53d3f 100644 --- a/playground/package.json +++ b/playground/package.json @@ -7,10 +7,14 @@ "dev": "vite" }, "dependencies": { + "@alilc/lowcode-shared": "workspace:*", "@alilc/lowcode-types": "workspace:*", + "@alilc/lowcode-core": "workspace:*", + "@alilc/lowcode-designer": "workspace:*", "@alilc/lowcode-engine": "workspace:*", "@alilc/lowcode-react-simulator-renderer": "workspace:*", "@alilc/lowcode-react-renderer": "workspace:*", + "@alilc/lowcode-renderer-core": "workspace:*", "@alifd/next": "^1.27.10", "react": "^18.2.0", "react-dom": "^18.2.0" diff --git a/playground/renderer/src/index.ts b/playground/renderer/src/index.ts index e69de29bb..e4c6feeda 100644 --- a/playground/renderer/src/index.ts +++ b/playground/renderer/src/index.ts @@ -0,0 +1,14 @@ +import { definePlugin, createApp } from '@alilc/lowcode-react-renderer'; + +const p = definePlugin({ + name: '', + setup(app, { boosts }) { + boosts.getAppWrappers; + }, +}); + +const app = createApp({}); + +app.then((app) => { + app.use(p); +}); diff --git a/playground/test/index.html b/playground/test/index.html new file mode 100644 index 000000000..0ba5ee847 --- /dev/null +++ b/playground/test/index.html @@ -0,0 +1,27 @@ + + + + + + Document + + +
+ + + + + + + + + + + + + + + + + + diff --git a/playground/test/src/index.ts b/playground/test/src/index.ts new file mode 100644 index 000000000..3bfbdf38d --- /dev/null +++ b/playground/test/src/index.ts @@ -0,0 +1,39 @@ +import { UAParser, createSignal, effect } from '@alilc/lowcode-shared'; +import { History, Hotkey } from '@alilc/lowcode-editor-core'; + +const parser = new UAParser().getResult(); +console.log('%c [ parser ]-5', 'font-size:13px; background:pink; color:#bf2c9f;', parser); + +const hotkey = new Hotkey(); +hotkey.mount(window); + +const signal = createSignal(0); + +effect(() => { + console.log('effect ', signal.value); +}); + +const history = new History(signal, (value) => { + signal.value = value; + console.log('redo', value); +}); + +const button1 = document.createElement('button'); +button1.innerHTML = 'click'; +button1.onclick = () => { + signal.value++; +}; + +document.getElementById('app')!.append(button1); + +hotkey.bind(['command+z'], () => { + console.log('undo'); + history.back(); + return false; +}); + +hotkey.bind(['command+shift+z'], () => { + console.log('redo'); + history.forward(); + return false; +}); diff --git a/scripts/rollup-dts.js b/scripts/rollup-dts.js index eb0ab5f4c..ea17bce51 100644 --- a/scripts/rollup-dts.js +++ b/scripts/rollup-dts.js @@ -36,7 +36,8 @@ async function run() { } else { console.error( '🚨类型声明文件生成失败:' + - +`\n\t${extractorResult.errorCount} errors``\n\tand ${extractorResult.warningCount} warnings`, + +`\n\t${extractorResult.errorCount} errors` + + `\n\tand ${extractorResult.warningCount} warnings`, ); exit(1); }