refactor: render-core

This commit is contained in:
1ncounter 2024-05-28 11:07:01 +08:00
parent 8510f998fe
commit d632e7f7e6
112 changed files with 2871 additions and 2553 deletions

2
.npmrc
View File

@ -1 +1 @@
git-checks=false
git-checks=false

View File

@ -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",

View File

@ -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",

View File

@ -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<Config extends PlainObject, K extends keyof Config = keyof Config> {
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<Config extends PlainObject, K extends keyof Config = keyof Config> {
#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<Config extends PlainObject, K extends keyof Config =
}[]
>();
constructor(config: Config, options?: ConfigurationOptions<Config>) {
constructor(config: Config, setterValidator?: (key: K, value: Config[K]) => boolean | string) {
invariant(config, 'config must exist', 'Configuration');
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<Config extends PlainObject, K extends keyof Config =
* @param key
*/
has(key: K): boolean {
return this.#config[key] !== undefined;
return this.config[key] !== undefined;
}
/**
* key
* @param key
* @param defaultValue
*/
get(key: K, defaultValue?: any): any {
return lodashGet(this.#config, key, defaultValue);
get<T = any>(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<Config extends PlainObject, K extends keyof Config =
});
}
}
/**
* key
* Promise fullfill
@ -101,7 +82,7 @@ export class Configuration<Config extends PlainObject, K extends keyof Config =
* @returns
*/
onceGot(key: K) {
const val = this.#config[key];
const val = this.get(key);
if (val !== undefined) {
return Promise.resolve(val);
}
@ -109,7 +90,6 @@ export class Configuration<Config extends PlainObject, K extends keyof Config =
this.setWait(key, resolve, true);
});
}
/**
* key
* @param key
@ -117,7 +97,7 @@ export class Configuration<Config extends PlainObject, K extends keyof Config =
* @returns
*/
onGot(key: K, fn: (data: Config[K]) => void): () => void {
const val = this.#config[key];
const val = this.config[key];
if (val !== undefined) {
fn(val);
}
@ -127,8 +107,8 @@ export class Configuration<Config extends PlainObject, K extends keyof Config =
};
}
notifyGot(key: K): void {
let waits = this.#waits.get(key);
private notifyGot(key: K): void {
let waits = this.waits.get(key);
if (!waits) {
return;
}
@ -141,23 +121,23 @@ export class Configuration<Config extends PlainObject, K extends keyof Config =
}
}
if (waits.length > 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<Config extends PlainObject, K extends keyof Config =
}
}
if (waits.length < 1) {
this.#waits.delete(key);
this.waits.delete(key);
}
}
}

View File

@ -1,2 +0,0 @@
export * from './config';
export { Preference, userPreference } from './preference';

View File

@ -1,4 +1,4 @@
export * from './configuration';
export * from './preference';
export * from './hotkey';
export * from './intl';
export * from './instantiation';

View File

@ -1,43 +0,0 @@
import '@abraham/reflection';
import { Container, inject } from 'inversify';
import { fluentProvide, buildProviderModule } from 'inversify-binding-decorators';
export const iocContainer = new Container();
/**
* Identifies a service of type `T`.
*/
export interface ServiceIdentifier<T> {
(...args: any[]): void;
type: T;
}
type Constructor<T = any> = new (...args: any[]) => T;
export function createDecorator<T>(serviceId: string): ServiceIdentifier<T> {
const id = <any>(
function (target: Constructor, targetKey: string, indexOrPropertyDescriptor: any): any {
return inject(serviceId)(target, targetKey, indexOrPropertyDescriptor);
}
);
id.toString = () => serviceId;
return id;
}
export function Provide(serviceId: string, isSingleTon?: boolean) {
const ret = fluentProvide(serviceId.toString());
if (isSingleTon) {
return ret.inSingletonScope().done();
}
return ret.done();
}
export function createInstance<T extends Constructor>(App: T) {
return iocContainer.resolve<InstanceType<T>>(App);
}
export function bootstrapModules() {
iocContainer.load(buildProviderModule());
}

View File

@ -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<Locale>;
@ -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;
}

View File

@ -61,5 +61,3 @@ export class Preference {
return !(result === undefined || result === null);
}
}
export const userPreference = new Preference();

View File

@ -3,5 +3,5 @@
"compilerOptions": {
"outDir": "dist"
},
"include": ["src", "__tests__"]
"include": ["src", "__tests__", "src/configuration.ts"]
}

View File

@ -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;
}

View File

@ -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<Schema extends ComponentTreeNode = ComponentTreeNode> {
export interface Node<Schema extends Spec.ComponentNode = Spec.ComponentNode> {
/**
* id
* node id
@ -353,6 +353,6 @@ export interface Node<Schema extends ComponentTreeNode = ComponentTreeNode> {
};
}
export function createNode<Schema extends ComponentTreeNode>(nodeSchema: Schema): Node<Schema> {
export function createNode<Schema extends Spec.ComponentNode>(nodeSchema: Schema): Node<Schema> {
return {};
}

View File

@ -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",

View File

@ -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<any>;
}
export interface ReactRender extends AppBase {}
const defaultRouterOptions: RouterOptions = {
historyMode: 'browser',
baseName: '/',
routes: [],
};
export type ReactApp = App<ReactRender>;
export const createApp = async (options: ReactAppOptions) => {
const creator = createRenderer<IRender>(async (context) => {
const { schema, boostsManager } = context;
const boosts = boostsManager.toExpose();
export const createApp = createAppFunction<AppOptions, ReactRender>(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);
// i18n
const i18nMessages = schema.getByKey('i18n') ?? {};
const defaultLocale = schema.getByPath('config.defaultLocale') ?? 'zh-CN';
const intl = createIntl(i18nMessages, defaultLocale);
appScope.inject('intl', intl);
// utils
const runtimeUtils = createRuntimeUtils(schema.getByKey('utils') ?? [], packageManager);
appScope.inject('utils', runtimeUtils.utils);
boosts.add('runtimeUtils', runtimeUtils);
// set config
if (options.faultComponent) {
context.config.set('faultComponent', options.faultComponent);
}
context.config.set('dataSourceCreator', options.dataSourceCreator);
let root: Root | undefined;
const renderer = createRenderer();
const appContext = { ...context, renderer };
const reactRender: ReactRender = {
async mount(el) {
if (root) {
return;
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);
}
root = createRoot(el);
root.render(<AppComponent context={appContext} />);
},
unmount() {
if (root) {
root.unmount();
root = undefined;
}
},
};
const router = createRouter(routerConfig);
return {
appBase: reactRender,
renderer,
};
});
boosts.codeRuntime.getScope().inject('router', router);
// set config
// if (options.faultComponent) {
// context.config.set('faultComponent', options.faultComponent);
// }
// extends boosts
boostsManager.extend(rendererExtends);
const RouterProvider = createRouterProvider(router);
let root: Root | undefined;
return {
async mount(el) {
if (root) {
return;
}
root = createRoot(el);
root.render(
<RendererContext.Provider value={context}>
<RouterProvider>
<AppComponent />
</RouterProvider>
</RendererContext.Provider>,
);
},
unmount() {
if (root) {
root.unmount();
root = undefined;
}
},
};
});
return creator(options);
};

View File

@ -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<LowCodeComponentProps>;
}
export type { ComponentOptions };
export async function createComponent(options: AppOptions) {
const creator = createRenderer<Render>((context) => {
const { schema } = context;
const LowCodeComponent = createComponentBySchema(schema.get('componentsTree')[0]);
function Component(props: LowCodeComponentProps) {
return (
<RendererContext.Provider value={context}>
<LowCodeComponent {...props} />
</RendererContext.Provider>
);
}
return {
toComponent() {
return Component;
},
};
});
const render = await creator(options);
return render.toComponent();
}

View File

@ -1,425 +0,0 @@
import {
createComponentFunction,
isLowCodeComponentSchema,
createCodeRuntime,
TextWidget,
ComponentWidget,
isJSExpression,
processValue,
isJSFunction,
isJSSlot,
someValue,
type CreateComponentBaseOptions,
type CodeRuntime,
} from '@alilc/lowcode-renderer-core';
import { isPlainObject } from 'lodash-es';
import { forwardRef, useRef, useEffect, createElement, useMemo } from 'react';
import { signal, watch } from './signals';
import { appendExternalStyle } from './utils/element';
import { reactive } from './utils/reactive';
import type {
PlainObject,
InstanceStateApi,
LowCodeComponent as LowCodeComponentSchema,
IntlApi,
JSSlot,
JSFunction,
I18nNode,
} from '@alilc/lowcode-shared';
import type {
ComponentType,
ReactInstance,
CSSProperties,
ForwardedRef,
ReactElement,
} from 'react';
export type ReactComponentLifeCycle =
| 'constructor'
| 'render'
| 'componentDidMount'
| 'componentDidUpdate'
| 'componentWillUnmount'
| 'componentDidCatch';
export interface ComponentOptions<C = ComponentType<any>>
extends CreateComponentBaseOptions<ReactComponentLifeCycle> {
componentsRecord: Record<string, C | LowCodeComponentSchema>;
intl: IntlApi;
displayName?: string;
beforeElementCreate?(widget: TextWidget<C> | ComponentWidget<C>): void;
componentRefAttached?(widget: ComponentWidget<C>, instance: ReactInstance): void;
}
export interface LowCodeComponentProps {
id?: string;
/** CSS 类名 */
className?: string;
/** style */
style?: CSSProperties;
[key: string]: any;
}
export const createComponent = createComponentFunction<
ComponentType<any>,
ReactInstance,
ReactComponentLifeCycle,
ComponentOptions
>(reactiveStateCreator, (container, options) => {
const {
componentsRecord,
intl,
displayName = '__LowCodeComponent__',
beforeElementCreate,
componentRefAttached,
...extraOptions
} = options;
const lowCodeComponentCache = new Map<string, ComponentType<any>>();
function getComponentByName(componentName: string) {
const Component = componentsRecord[componentName];
if (!Component) {
return undefined;
}
if (isLowCodeComponentSchema(Component)) {
if (lowCodeComponentCache.has(componentName)) {
return lowCodeComponentCache.get(componentName);
}
const LowCodeComponent = createComponent({
...extraOptions,
intl,
displayName: Component.componentName,
componentsRecord,
componentsTree: Component.schema,
});
lowCodeComponentCache.set(componentName, LowCodeComponent);
return LowCodeComponent;
}
return Component;
}
function createReactElement(
widget: TextWidget<ComponentType<any>> | ComponentWidget<ComponentType<any>>,
codeRuntime: CodeRuntime,
) {
beforeElementCreate?.(widget);
return widget.build((elements) => {
if (elements.length > 0) {
const RenderObject = elements[elements.length - 1];
const Wrappers = elements.slice(0, elements.length - 1);
const buildRenderElement = () => {
if (widget instanceof TextWidget) {
if (widget.type === 'string') {
return createElement(RenderObject, { key: widget.key, text: widget.raw });
} else {
return createElement(
reactive(RenderObject, {
target:
widget.type === 'expression' ? { text: widget.raw } : (widget.raw as I18nNode),
valueGetter(expr) {
return codeRuntime.parseExprOrFn(expr);
},
}),
{ key: widget.key },
);
}
} else if (widget instanceof ComponentWidget) {
const { condition, loop, loopArgs } = widget;
// condition为 Falsy 的情况下 不渲染
if (!condition) return null;
// loop 为数组且为空的情况下 不渲染
if (Array.isArray(loop) && loop.length === 0) return null;
function createElementWithProps(
Component: ComponentType<any>,
widget: ComponentWidget<ComponentType<any>>,
codeRuntime: CodeRuntime,
key?: string,
): ReactElement {
const { ref, ...componentProps } = widget.props;
const componentKey = key ?? widget.key;
const attachRef = (ins: ReactInstance) => {
if (ins) {
if (ref) container.setInstance(ref as string, ins);
componentRefAttached?.(widget, ins);
} else {
if (ref) container.removeInstance(ref);
}
};
// 先将 jsslot, jsFunction 对象转换
const finalProps = processValue(
componentProps,
(node) => isJSFunction(node) || isJSSlot(node),
(node: JSSlot | JSFunction) => {
if (isJSSlot(node)) {
const slot = node as JSSlot;
if (slot.value) {
const widgets = (Array.isArray(node.value) ? node.value : [node.value]).map(
(v) => new ComponentWidget<ComponentType<any>>(v),
);
if (slot.params?.length) {
return (...args: any[]) => {
const params = slot.params!.reduce((prev, cur, idx) => {
return (prev[cur] = args[idx]);
}, {} as PlainObject);
const subCodeScope = codeRuntime.getScope().createSubScope(params);
const subCodeRuntime = createCodeRuntime(subCodeScope);
return widgets.map((n) => createReactElement(n, subCodeRuntime));
};
} else {
return widgets.map((n) => createReactElement(n, codeRuntime));
}
}
} else if (isJSFunction(node)) {
return codeRuntime.parseExprOrFn(node);
}
return null;
},
);
const childElements = widget.children.map((child) =>
createReactElement(child, codeRuntime),
);
if (someValue(finalProps, isJSExpression)) {
const PropsWrapper = (props: PlainObject) =>
createElement(
Component,
{
...props,
key: componentKey,
ref: attachRef,
},
childElements,
);
PropsWrapper.displayName = 'PropsWrapper';
return createElement(
reactive(PropsWrapper, {
target: finalProps,
valueGetter: (node) => codeRuntime.parseExprOrFn(node),
}),
{ key: componentKey },
);
} else {
return createElement(
Component,
{
...finalProps,
key: componentKey,
ref: attachRef,
},
childElements,
);
}
}
let element: ReactElement | ReactElement[] = createElementWithProps(
RenderObject,
widget,
codeRuntime,
);
if (loop) {
const genLoopElements = (loopData: any[]) => {
return loopData.map((item, idx) => {
const loopArgsItem = loopArgs[0] ?? 'item';
const loopArgsIndex = loopArgs[1] ?? 'index';
const subCodeScope = codeRuntime.getScope().createSubScope({
[loopArgsItem]: item,
[loopArgsIndex]: idx,
});
const subCodeRuntime = createCodeRuntime(subCodeScope);
return createElementWithProps(
RenderObject,
widget,
subCodeRuntime,
`loop-${widget.key}-${idx}`,
);
});
};
if (isJSExpression(loop)) {
function Loop(props: { loop: boolean }) {
if (!Array.isArray(props.loop)) {
return null;
}
return <>{genLoopElements(props.loop)}</>;
}
Loop.displayName = 'Loop';
const ReactivedLoop = reactive(Loop, {
target: {
loop,
},
valueGetter: (expr) => codeRuntime.parseExprOrFn(expr),
});
element = createElement(ReactivedLoop, {
key: widget.key,
});
} else {
element = genLoopElements(loop as any[]);
}
}
if (isJSExpression(condition)) {
function Condition(props: any) {
if (props.condition) {
return element;
}
return null;
}
Condition.displayName = 'Condition';
const ReactivedCondition = reactive(Condition, {
target: {
condition,
},
valueGetter: (expr) => codeRuntime.parseExprOrFn(expr),
});
element = createElement(ReactivedCondition, {
key: widget.key,
});
}
return element;
}
return null;
};
const element = buildRenderElement();
return Wrappers.reduce((prevElement, CurWrapper) => {
return createElement(CurWrapper, { key: widget.key }, prevElement);
}, element);
}
});
}
const LowCodeComponent = forwardRef(function (
props: LowCodeComponentProps,
ref: ForwardedRef<any>,
) {
const { id, className, style } = props;
const isConstructed = useRef(false);
const isMounted = useRef(false);
if (!isConstructed.current) {
container.triggerLifeCycle('constructor');
isConstructed.current = true;
}
useEffect(() => {
const scopeValue = container.codeRuntime.getScope().value;
// init dataSource
scopeValue.reloadDataSource();
let styleEl: HTMLElement | undefined;
const cssText = container.getCssText();
if (cssText) {
appendExternalStyle(cssText).then((el) => {
styleEl = el;
});
}
// trigger lifeCycles
// componentDidMount?.();
container.triggerLifeCycle('componentDidMount');
// 当 state 改变之后调用
const unwatch = watch(scopeValue.state, (_, oldVal) => {
if (isMounted.current) {
container.triggerLifeCycle('componentDidUpdate', props, oldVal);
}
});
isMounted.current = true;
return () => {
// componentWillUnmount?.();
container.triggerLifeCycle('componentWillUnmount');
styleEl?.parentNode?.removeChild(styleEl);
unwatch();
isMounted.current = false;
};
}, []);
const widgets = useMemo(() => {
return container.createWidgets<ComponentType<any>>().map((widget) =>
widget.mapRenderObject((widget) => {
if (widget instanceof TextWidget) {
if (widget.type === 'i18n') {
function IntlText(props: { key: string; params: Record<string, string> }) {
return <>{intl.i18n(props.key, props.params)}</>;
}
IntlText.displayName = 'IntlText';
return IntlText;
}
function Text(props: { text: string }) {
return <>{props.text}</>;
}
Text.displayName = 'Text';
return Text;
} else if (widget instanceof ComponentWidget) {
return getComponentByName(widget.raw.componentName);
}
}),
);
}, []);
return (
<div id={id} className={className} style={style} ref={ref}>
{widgets.map((widget) => createReactElement(widget, container.codeRuntime))}
</div>
);
});
LowCodeComponent.displayName = displayName;
return LowCodeComponent;
});
function reactiveStateCreator(initState: PlainObject): InstanceStateApi {
const proxyState = signal(initState);
return {
get state() {
return proxyState.value;
},
setState(newState) {
if (!isPlainObject(newState)) {
throw Error('newState mush be a object');
}
proxyState.value = {
...proxyState.value,
...newState,
};
},
};
}

View File

@ -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<ReactComponent>(componentName);
if (Component?.devMode === 'lowCode') {
const componentsMap = schema.getComponentsMaps();
const componentsRecord = packageManager.getComponentsNameRecord<any>(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 = <Layout {...layoutProps}>{element}</Layout>;
}
@ -55,11 +47,5 @@ export default function App({ context }: { context: AppContextObject }) {
}, element);
}
const RouterProvider = createRouterProvider(appScope.value.router);
return (
<AppContext.Provider value={context}>
<RouterProvider>{element}</RouterProvider>
</AppContext.Provider>
);
return element;
}

View File

@ -1,32 +0,0 @@
import type { PageConfig, ComponentTree } from '@alilc/lowcode-renderer-core';
import { useAppContext } from '../context/app';
import { createComponent } from '../component';
export interface OutletProps {
pageConfig: PageConfig;
componentsTree?: ComponentTree | undefined;
[key: string]: any;
}
export default function Outlet({ pageSchema, componentsTree }: OutletProps) {
const { schema, config, packageManager, appScope } = useAppContext();
const { type = 'lowCode' } = pageSchema;
if (type === 'lowCode' && componentsTree) {
const componentsMap = schema.getComponentsMaps();
const componentsRecord = packageManager.getComponentsNameRecord<any>(componentsMap);
const LowCodeComponent = createComponent({
supCodeScope: appScope,
dataSourceCreator: config.get('dataSourceCreator'),
componentsTree,
componentsRecord,
intl: appScope.value.intl,
});
return <LowCodeComponent />;
}
return null;
}

View File

@ -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;
if (type === 'lowCode') {
componentsTree = schema.getComponentsTrees().find((item) => item.id === mappingId);
}
return <Outlet {...props} pageConfig={pageConfig} componentsTree={componentsTree} />;
return <Outlet {...props} pageConfig={pageConfig} />;
}
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);
const LowCodeComponent = createComponentBySchema(mappingId, {
displayName: pageConfig?.id,
});
return <LowCodeComponent />;
}
return null;

View File

@ -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) {

View File

@ -1,13 +0,0 @@
import { createContext, useContext } from 'react';
import { type AppContext as AppContextType } from '@alilc/lowcode-renderer-core';
import { type ReactRenderer } from '../renderer';
export interface AppContextObject extends AppContextType {
renderer: ReactRenderer;
}
export const AppContext = createContext<AppContextObject>({} as any);
AppContext.displayName = 'RootContext';
export const useAppContext = () => useContext(AppContext);

View File

@ -0,0 +1,8 @@
import { createContext, useContext } from 'react';
import { type RenderContext } from '@alilc/lowcode-renderer-core';
export const RendererContext = createContext<RenderContext>(undefined!);
RendererContext.displayName = 'RootContext';
export const useRenderContext = () => useContext(RendererContext);

View File

@ -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<Router>({} as any);
export const RouterContext = createContext<Router>(undefined!);
RouterContext.displayName = 'RouterContext';
export const useRouter = () => useContext(RouterContext);
export const RouteLocationContext = createContext<RouteLocationNormalized>({
name: undefined,
path: '/',
searchParams: undefined,
params: {},
hash: '',
fullPath: '/',
redirectedFrom: undefined,
matched: [],
meta: {},
});
export const RouteLocationContext = createContext<RouteLocationNormalized>(undefined!);
RouteLocationContext.displayName = 'RouteLocationContext';
export const useRouteLocation = () => useContext(RouteLocationContext);
export const PageConfigContext = createContext<PageConfig | undefined>(undefined);
export const PageConfigContext = createContext<Spec.PageConfig | undefined>(undefined);
PageConfigContext.displayName = 'PageConfigContext';

View File

@ -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';

View File

@ -0,0 +1,50 @@
import { Plugin } from '@alilc/lowcode-renderer-core';
import { type ComponentType, type PropsWithChildren } from 'react';
import { type OutletProps } from './components/route';
export type WrapperComponent = ComponentType<PropsWithChildren<any>>;
export type Outlet = ComponentType<OutletProps>;
export interface RendererExtends {
addAppWrapper(appWrapper: WrapperComponent): void;
getAppWrappers(): WrapperComponent[];
addRouteWrapper(wrapper: WrapperComponent): void;
getRouteWrappers(): WrapperComponent[];
setOutlet(outlet: Outlet): void;
getOutlet(): Outlet | null;
}
const appWrappers: WrapperComponent[] = [];
const wrappers: WrapperComponent[] = [];
let outlet: Outlet | null = null;
export const rendererExtends: RendererExtends = {
addAppWrapper(appWrapper) {
if (appWrapper) appWrappers.push(appWrapper);
},
getAppWrappers() {
return appWrappers;
},
addRouteWrapper(wrapper) {
if (wrapper) wrappers.push(wrapper);
},
getRouteWrappers() {
return wrappers;
},
setOutlet(outletComponent) {
if (outletComponent) outlet = outletComponent;
},
getOutlet() {
return outlet;
},
};
export function definePlugin(plugin: Plugin<RendererExtends>) {
return plugin;
}

View File

@ -1,60 +0,0 @@
import {
definePlugin as definePluginFn,
type Plugin,
type PluginSetupContext,
} from '@alilc/lowcode-renderer-core';
import { type ComponentType, type PropsWithChildren } from 'react';
import { type OutletProps } from './components/outlet';
export type WrapperComponent = ComponentType<PropsWithChildren<{}>>;
export type Outlet = ComponentType<OutletProps>;
export interface ReactRenderer {
addAppWrapper(appWrapper: WrapperComponent): void;
getAppWrappers(): WrapperComponent[];
addRouteWrapper(wrapper: WrapperComponent): void;
getRouteWrappers(): WrapperComponent[];
setOutlet(outlet: Outlet): void;
getOutlet(): Outlet | null;
}
export function createRenderer(): ReactRenderer {
const appWrappers: WrapperComponent[] = [];
const wrappers: WrapperComponent[] = [];
let outlet: Outlet | null = null;
return {
addAppWrapper(appWrapper) {
if (appWrapper) appWrappers.push(appWrapper);
},
getAppWrappers() {
return appWrappers;
},
addRouteWrapper(wrapper) {
if (wrapper) wrappers.push(wrapper);
},
getRouteWrappers() {
return wrappers;
},
setOutlet(outletComponent) {
if (outletComponent) outlet = outletComponent;
},
getOutlet() {
return outlet;
},
};
}
export interface ReactRendererSetupContext extends PluginSetupContext {
renderer: ReactRenderer;
}
export function definePlugin(plugin: Plugin<ReactRendererSetupContext>) {
return definePluginFn(plugin);
}

View File

@ -1,33 +0,0 @@
import { parse, compile } from './parser';
import { signal, computed } from '../../signals';
export function createIntl(
messages: Record<string, Record<string, string>>,
defaultLocale: string,
) {
const allMessages = signal(messages);
const currentLocale = signal(defaultLocale);
const currentMessages = computed(() => allMessages.value[currentLocale.value]);
return {
i18n(key: string, params: Record<string, string>) {
const message = currentMessages.value[key];
const result = compile(parse(message), params).join('');
return result;
},
getLocale() {
return currentLocale.value;
},
setLocale(locale: string) {
currentLocale.value = locale;
},
addMessages(locale: string, messages: Record<string, string>) {
allMessages.value[locale] = {
...allMessages.value[locale],
...messages,
};
},
};
}

View File

@ -1,92 +0,0 @@
import { isObject } from 'lodash-es';
const RE_TOKEN_LIST_VALUE: RegExp = /^(?:\d)+/;
const RE_TOKEN_NAMED_VALUE: RegExp = /^(?:\w)+/;
type Token = {
type: 'text' | 'named' | 'list' | 'unknown';
value: string;
};
export function parse(format: string): Array<Token> {
const tokens: Array<Token> = [];
let position: number = 0;
let text: string = '';
while (position < format.length) {
let char: string = format[position++];
if (char === '{') {
if (text) {
tokens.push({ type: 'text', value: text });
}
text = '';
let sub: string = '';
char = format[position++];
while (char !== undefined && char !== '}') {
sub += char;
char = format[position++];
}
const isClosed = char === '}';
const type = RE_TOKEN_LIST_VALUE.test(sub)
? 'list'
: isClosed && RE_TOKEN_NAMED_VALUE.test(sub)
? 'named'
: 'unknown';
tokens.push({ value: sub, type });
} else if (char === '%') {
// when found rails i18n syntax, skip text capture
if (format[position] !== '{') {
text += char;
}
} else {
text += char;
}
}
text && tokens.push({ type: 'text', value: text });
return tokens;
}
export function compile(tokens: Token[], values: Record<string, any> | any[] = {}): string[] {
const compiled: string[] = [];
let index: number = 0;
const mode: string = Array.isArray(values) ? 'list' : isObject(values) ? 'named' : 'unknown';
if (mode === 'unknown') {
return compiled;
}
while (index < tokens.length) {
const token: Token = tokens[index];
switch (token.type) {
case 'text':
compiled.push(token.value);
break;
case 'list':
compiled.push((values as any[])[parseInt(token.value, 10)]);
break;
case 'named':
if (mode === 'named') {
compiled.push((values as Record<string, any>)[token.value]);
} else {
if (process.env.NODE_ENV !== 'production') {
console.warn(
`Type of token '${token.type}' and format of value '${mode}' don't match!`,
);
}
}
break;
case 'unknown':
if (process.env.NODE_ENV !== 'production') {
console.warn('Detect \'unknown\' type of token!');
}
break;
}
index++;
}
return compiled;
}

View File

@ -1,72 +0,0 @@
import {
createCodeRuntime,
type PackageManager,
type AnyFunction,
type Util,
type UtilsApi,
} from '@alilc/lowcode-renderer-core';
export interface RuntimeUtils extends UtilsApi {
addUtil(utilItem: Util): void;
addUtil(name: string, fn: AnyFunction): void;
}
export function createRuntimeUtils(
utilSchema: Util[],
packageManager: PackageManager,
): RuntimeUtils {
const codeRuntime = createCodeRuntime();
const utilsMap: Record<string, AnyFunction> = {};
function addUtil(item: string | Util, fn?: AnyFunction) {
if (typeof item === 'string') {
if (typeof fn === 'function') {
utilsMap[item] = fn;
}
} else {
const fn = parseUtil(item);
addUtil(item.name, fn);
}
}
function parseUtil(utilItem: Util) {
if (utilItem.type === 'function') {
const { content } = utilItem;
return codeRuntime.createFnBoundScope(content.value);
} else {
const {
content: { package: packageName, destructuring, exportName, subName },
} = utilItem;
let library: any = packageManager.getLibraryByPackageName(packageName!);
if (library) {
if (destructuring) {
const target = library[exportName!];
library = subName ? target[subName] : target;
}
return library;
}
}
}
utilSchema.forEach((item) => addUtil(item));
const utilsProxy = new Proxy(Object.create(null), {
get(_, p: string) {
return utilsMap[p];
},
set() {
return false;
},
has(_, p: string) {
return Boolean(utilsMap[p]);
},
});
return {
addUtil,
utils: utilsProxy,
};
}

View File

@ -0,0 +1 @@
export const dataSourceCreator = () => ({}) as any;

View File

@ -0,0 +1,382 @@
import { processValue, someValue } from '@alilc/lowcode-renderer-core';
import {
watch,
isJSExpression,
isJSFunction,
isJSSlot,
invariant,
isLowCodeComponentSchema,
isJSI18nNode,
} from '@alilc/lowcode-shared';
import { forwardRef, useRef, useEffect, createElement, memo } from 'react';
import { appendExternalStyle } from '../utils/element';
import { reactive } from '../utils/reactive';
import { useRenderContext } from '../context/render';
import { reactiveStateCreator } from './reactiveState';
import { dataSourceCreator } from './dataSource';
import { normalizeComponentNode, type NormalizedComponentNode } from '../utils/node';
import type { PlainObject, Spec } from '@alilc/lowcode-shared';
import type {
IWidget,
RenderContext,
ICodeScope,
IComponentTreeModel,
} from '@alilc/lowcode-renderer-core';
import type {
ComponentType,
ReactInstance,
CSSProperties,
ForwardedRef,
ReactElement,
} from 'react';
export type ReactComponent = ComponentType<any>;
export type ReactWidget = IWidget<ReactComponent, ReactInstance>;
export interface ComponentOptions {
displayName?: string;
widgetCreated?(widget: ReactWidget): void;
componentRefAttached?(widget: ReactWidget, instance: ReactInstance): void;
}
export interface LowCodeComponentProps {
id?: string;
/** CSS 类名 */
className?: string;
/** style */
style?: CSSProperties;
[key: string]: any;
}
const lowCodeComponentsCache = new Map<string, ReactComponent>();
function getComponentByName(name: string, { packageManager }: RenderContext): ReactComponent {
const componentsRecord = packageManager.getComponentsNameRecord<ReactComponent>();
// read cache first
const result = lowCodeComponentsCache.get(name) || componentsRecord[name];
invariant(result, `${name} component not found in componentsRecord`);
if (isLowCodeComponentSchema(result)) {
const lowCodeComponent = createComponentBySchema(result.schema, {
displayName: name,
});
lowCodeComponentsCache.set(name, lowCodeComponent);
return lowCodeComponent;
}
return result;
}
export function createComponentBySchema(
schema: string | Spec.ComponentTreeRoot,
{ displayName = '__LowCodeComponent__', componentRefAttached }: ComponentOptions = {},
) {
const LowCodeComponent = forwardRef(function (
props: LowCodeComponentProps,
ref: ForwardedRef<any>,
) {
const renderContext = useRenderContext();
const { componentTreeModel } = renderContext;
const modelRef = useRef<IComponentTreeModel<ReactComponent, ReactInstance>>();
if (!modelRef.current) {
if (typeof schema === 'string') {
modelRef.current = componentTreeModel.createById(schema, {
stateCreator: reactiveStateCreator,
dataSourceCreator,
});
} else {
modelRef.current = componentTreeModel.create(schema, {
stateCreator: reactiveStateCreator,
dataSourceCreator,
});
}
}
const model = modelRef.current;
const isConstructed = useRef(false);
const isMounted = useRef(false);
if (!isConstructed.current) {
model.triggerLifeCycle('constructor');
isConstructed.current = true;
}
useEffect(() => {
const scopeValue = model.codeScope.value;
// init dataSource
scopeValue.reloadDataSource();
let styleEl: HTMLElement | undefined;
const cssText = model.getCssText();
if (cssText) {
appendExternalStyle(cssText).then((el) => {
styleEl = el;
});
}
// trigger lifeCycles
// componentDidMount?.();
model.triggerLifeCycle('componentDidMount');
// 当 state 改变之后调用
const unwatch = watch(scopeValue.state, (_, oldVal) => {
if (isMounted.current) {
model.triggerLifeCycle('componentDidUpdate', props, oldVal);
}
});
isMounted.current = true;
return () => {
// componentWillUnmount?.();
model.triggerLifeCycle('componentWillUnmount');
styleEl?.parentNode?.removeChild(styleEl);
unwatch();
isMounted.current = false;
};
}, []);
const elements = model.widgets.map((widget) => {
return createElementByWidget(widget, model.codeScope, renderContext, componentRefAttached);
});
return (
<div id={props.id} className={props.className} style={props.style} ref={ref}>
{elements}
</div>
);
});
LowCodeComponent.displayName = displayName;
return memo(LowCodeComponent);
}
function Text(props: { text: string }) {
return <>{props.text}</>;
}
Text.displayName = 'Text';
function createElementByWidget(
widget: IWidget<ReactComponent, ReactInstance>,
codeScope: ICodeScope,
renderContext: RenderContext,
componentRefAttached?: ComponentOptions['componentRefAttached'],
) {
return widget.build<ReactElement | ReactElement[] | null>((ctx) => {
const { key, node, model, children } = ctx;
const boosts = renderContext.boostsManager.toExpose();
if (typeof node === 'string') {
return createElement(Text, { key, text: node });
}
if (isJSExpression(node)) {
return createElement(
reactive(Text, {
target: { text: node },
valueGetter(expr) {
return model.codeRuntime.resolve(expr, codeScope);
},
}),
{ key },
);
}
if (isJSI18nNode(node)) {
return createElement(
reactive(Text, {
target: { text: node },
predicate: isJSI18nNode,
valueGetter: (node: Spec.JSI18n) => {
return boosts.intl.t({
key: node.key,
params: node.params ? model.codeRuntime.resolve(node.params, codeScope) : undefined,
});
},
}),
{ key },
);
}
function createElementWithProps(
node: NormalizedComponentNode,
codeScope: ICodeScope,
key: string,
): ReactElement {
const { ref, ...componentProps } = node.props;
const Component = getComponentByName(node.componentName, renderContext);
const attachRef = (ins: ReactInstance | null) => {
if (ins) {
if (ref) model.setComponentRef(ref as string, ins);
componentRefAttached?.(widget, ins);
} else {
if (ref) model.removeComponentRef(ref);
}
};
// 先将 jsslot, jsFunction 对象转换
const finalProps = processValue(
componentProps,
(node) => isJSFunction(node) || isJSSlot(node),
(node: Spec.JSSlot | Spec.JSFunction) => {
if (isJSSlot(node)) {
const slot = node as Spec.JSSlot;
if (slot.value) {
const widgets = model.buildWidgets(
Array.isArray(node.value) ? node.value : [node.value],
);
if (slot.params?.length) {
return (...args: any[]) => {
const params = slot.params!.reduce((prev, cur, idx) => {
return (prev[cur] = args[idx]);
}, {} as PlainObject);
return widgets.map((n) =>
createElementByWidget(
n,
codeScope.createChild(params),
renderContext,
componentRefAttached,
),
);
};
} else {
return widgets.map((n) =>
createElementByWidget(n, codeScope, renderContext, componentRefAttached),
);
}
}
} else if (isJSFunction(node)) {
return model.codeRuntime.resolve(node, codeScope);
}
return null;
},
);
const childElements = children?.map((child) =>
createElementByWidget(child, codeScope, renderContext, componentRefAttached),
);
if (someValue(finalProps, isJSExpression)) {
const PropsWrapper = (props: PlainObject) =>
createElement(
Component,
{
...props,
key,
ref: attachRef,
},
childElements,
);
PropsWrapper.displayName = 'PropsWrapper';
return createElement(
reactive(PropsWrapper, {
target: finalProps,
valueGetter: (node) => model.codeRuntime.resolve(node, codeScope),
}),
{ key },
);
} else {
return createElement(
Component,
{
...finalProps,
key,
ref: attachRef,
},
childElements,
);
}
}
const normalizedNode = normalizeComponentNode(node);
const { condition, loop, loopArgs } = normalizedNode;
// condition为 Falsy 的情况下 不渲染
if (!condition) return null;
// loop 为数组且为空的情况下 不渲染
if (Array.isArray(loop) && loop.length === 0) return null;
let element: ReactElement | ReactElement[] | null = null;
if (loop) {
const genLoopElements = (loopData: any[]) => {
return loopData.map((item, idx) => {
const loopArgsItem = loopArgs[0] ?? 'item';
const loopArgsIndex = loopArgs[1] ?? 'index';
return createElementWithProps(
normalizedNode,
codeScope.createChild({
[loopArgsItem]: item,
[loopArgsIndex]: idx,
}),
`loop-${key}-${idx}`,
);
});
};
if (isJSExpression(loop)) {
function Loop(props: { loop: boolean }) {
if (!Array.isArray(props.loop)) {
return null;
}
return <>{genLoopElements(props.loop)}</>;
}
Loop.displayName = 'Loop';
const ReactivedLoop = reactive(Loop, {
target: { loop },
valueGetter: (expr) => model.codeRuntime.resolve(expr, codeScope),
});
element = createElement(ReactivedLoop, { key });
} else {
element = genLoopElements(loop as any[]);
}
}
if (isJSExpression(condition)) {
function Condition(props: any) {
if (props.condition) {
return element;
}
return null;
}
Condition.displayName = 'Condition';
const ReactivedCondition = reactive(Condition, {
target: { condition },
valueGetter: (expr) => model.codeRuntime.resolve(expr, codeScope),
});
element = createElement(ReactivedCondition, {
key,
});
}
if (!element) {
element = createElementWithProps(normalizedNode, codeScope, key);
}
return element;
});
}

View File

@ -0,0 +1,22 @@
import { signal, type PlainObject, type Spec } from '@alilc/lowcode-shared';
import { isPlainObject } from 'lodash-es';
export function reactiveStateCreator(initState: PlainObject): Spec.InstanceStateApi {
const proxyState = signal(initState);
return {
get state() {
return proxyState.value;
},
setState(newState) {
if (!isPlainObject(newState)) {
throw Error('newState mush be a object');
}
proxyState.value = {
...proxyState.value,
...newState,
};
},
};
}

View File

@ -1,122 +0,0 @@
import { type PlainObject } from '@alilc/lowcode-renderer-core';
import {
ref,
computed,
effect,
ReactiveEffect,
type ComputedRef,
type Ref,
getCurrentScope,
isRef,
isReactive,
isShallow,
} from '@vue/reactivity';
import { noop, isObject, isPlainObject, isSet, isMap } from 'lodash-es';
export { ref as signal, computed, effect };
export type { Ref as Signal, ComputedRef as ComputedSignal };
const INITIAL_WATCHER_VALUE = {};
export function watch<T = any>(
source: Ref<T> | ComputedRef<T> | object,
cb: (value: any, oldValue: any) => any,
{
deep,
immediate,
}: {
deep?: boolean;
immediate?: boolean;
} = {},
) {
let getter: () => any;
let forceTrigger = false;
if (isRef(source)) {
getter = () => source.value;
forceTrigger = isShallow(source);
} else if (isReactive(source)) {
getter = () => {
return deep === true ? source : traverse(source, deep === false ? 1 : undefined);
};
forceTrigger = true;
} else {
getter = () => {};
}
if (deep) {
const baseGetter = getter;
getter = () => traverse(baseGetter());
}
let oldValue = INITIAL_WATCHER_VALUE;
const job = () => {
if (!effect.active || !effect.dirty) {
return;
}
const newValue = effect.run();
if (deep || forceTrigger || !Object.is(newValue, oldValue)) {
cb(newValue, oldValue === INITIAL_WATCHER_VALUE ? undefined : oldValue);
oldValue = newValue;
}
};
const effect = new ReactiveEffect(getter, noop, job);
const scope = getCurrentScope();
const unwatch = () => {
effect.stop();
if (scope) {
const i = (scope as any).effects.indexOf(effect);
if (i > -1) {
(scope as any).effects.splice(i, 1);
}
}
};
// initial run
if (immediate) {
job();
} else {
oldValue = effect.run();
}
return unwatch;
}
function traverse(value: unknown, depth?: number, currentDepth = 0, seen?: Set<unknown>) {
if (!isObject(value)) {
return value;
}
if (depth && depth > 0) {
if (currentDepth >= depth) {
return value;
}
currentDepth++;
}
seen = seen || new Set();
if (seen.has(value)) {
return value;
}
seen.add(value);
if (isRef(value)) {
traverse(value.value, depth, currentDepth, seen);
} else if (Array.isArray(value)) {
for (let i = 0; i < value.length; i++) {
traverse(value[i], depth, currentDepth, seen);
}
} else if (isSet(value) || isMap(value)) {
value.forEach((v: any) => {
traverse(v, depth, currentDepth, seen);
});
} else if (isPlainObject(value)) {
for (const key in value) {
traverse((value as PlainObject)[key], depth, currentDepth, seen);
}
}
return value;
}

View File

@ -0,0 +1,14 @@
import { Spec } from '@alilc/lowcode-shared';
export interface NormalizedComponentNode extends Spec.ComponentNode {
loopArgs: [string, string];
props: Spec.ComponentNodeProps;
}
export function normalizeComponentNode(node: Spec.ComponentNode): NormalizedComponentNode {
return {
...node,
loopArgs: node.loopArgs ?? ['item', 'index'],
props: node.props ?? {},
};
}

View File

@ -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<Snapshot = PlainObject> {
value: Snapshot;
@ -20,7 +20,8 @@ export interface ReactiveStore<Snapshot = PlainObject> {
function createReactiveStore<Snapshot = PlainObject>(
target: Record<string, any>,
valueGetter: (expr: JSExpression) => any,
predicate: (obj: any) => boolean,
valueGetter: (expr: any) => any,
): ReactiveStore<Snapshot> {
let isFlushing = false;
let isFlushPending = false;
@ -28,7 +29,7 @@ function createReactiveStore<Snapshot = PlainObject>(
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<Snapshot = PlainObject>(
interface ReactiveOptions {
target: PlainObject;
valueGetter: (expr: JSExpression) => any;
valueGetter: (expr: any) => any;
predicate?: (obj: any) => boolean;
forwardRef?: boolean;
}
export function reactive<TProps extends PlainObject = PlainObject>(
WrappedComponent: ComponentType<TProps>,
{ target, valueGetter, forwardRef: forwardRefOption = true }: ReactiveOptions,
): ComponentType<PlainObject> {
const store = createReactiveStore(target, valueGetter);
{
target,
valueGetter,
predicate = isJSExpression,
forwardRef: forwardRefOption = true,
}: ReactiveOptions,
): ComponentType<PropsWithChildren<any>> {
const store = createReactiveStore<TProps>(target, predicate, valueGetter);
function WrapperComponent(props: any, ref: any) {
const actualProps = useSyncExternalStore(store.subscribe, store.getSnapshot);

View File

@ -1,106 +0,0 @@
import type { Project, Package, PlainObject } from '@alilc/lowcode-shared';
import { type PackageManager, createPackageManager } from '../package';
import { createPluginManager, type Plugin } from '../plugin';
import { createScope, type CodeScope } from '../code-runtime';
import { appBoosts, type AppBoosts, type AppBoostsManager } from '../boosts';
import { type AppSchema, createAppSchema } from '../schema';
export interface AppOptionsBase {
schema: Project;
packages?: Package[];
plugins?: Plugin[];
appScopeValue?: PlainObject;
}
export interface AppBase {
mount: (el: HTMLElement) => void | Promise<void>;
unmount: () => void | Promise<void>;
}
/**
* context for plugin or renderer
*/
export interface AppContext {
schema: AppSchema;
config: PlainObject;
appScope: CodeScope;
packageManager: PackageManager;
boosts: AppBoostsManager;
}
type AppCreator<O, T extends AppBase> = (
appContext: Omit<AppContext, 'renderer'>,
appOptions: O,
) => Promise<{ appBase: T; renderer?: any }>;
export type App<T extends AppBase = AppBase> = {
schema: Project;
config: PlainObject;
readonly boosts: AppBoosts;
use(plugin: Plugin): Promise<void>;
} & T;
/**
* createApp
* @param schema
* @param options
* @returns
*/
export function createAppFunction<O extends AppOptionsBase, T extends AppBase = AppBase>(
appCreator: AppCreator<O, T>,
): (options: O) => Promise<App<T>> {
if (typeof appCreator !== 'function') {
throw Error('The first parameter must be a function.');
}
return async (options) => {
const { schema, appScopeValue } = options;
const appSchema = createAppSchema(schema);
const appConfig = {};
const packageManager = createPackageManager();
const appScope = createScope({
...appScopeValue,
constants: schema.constants ?? {},
});
const appContext = {
schema: appSchema,
config: appConfig,
appScope,
packageManager,
boosts: appBoosts,
};
const { appBase, renderer } = await appCreator(appContext, options);
if (!('mount' in appBase) || !('unmount' in appBase)) {
throw Error('appBase 必须返回 mount 和 unmount 方法');
}
const pluginManager = createPluginManager({
...appContext,
renderer,
});
if (options.plugins?.length) {
await Promise.all(options.plugins.map((p) => pluginManager.add(p)));
}
if (options.packages?.length) {
await packageManager.addPackages(options.packages);
}
return Object.assign(
{
schema,
config: appConfig,
use: pluginManager.add,
get boosts() {
return appBoosts.value;
},
},
appBase,
);
};
}

View File

@ -1,43 +0,0 @@
import type { PlainObject, InstanceStateApi } from '@alilc/lowcode-shared';
import { type CreateContainerOptions, createContainer, type Container } from '../container';
export type CreateComponentBaseOptions<T extends string> = Omit<
CreateContainerOptions<T>,
'stateCreator'
>;
/**
* createComponent
* createComponent = createComponentFunction(() => component)
*/
export function createComponentFunction<
ComponentT,
InstanceT,
LifeCycleNameT extends string,
O extends CreateComponentBaseOptions<LifeCycleNameT>,
>(
stateCreator: (initState: PlainObject) => InstanceStateApi,
componentCreator: (
container: Container<InstanceT, LifeCycleNameT>,
componentOptions: O,
) => ComponentT,
): (componentOptions: O) => ComponentT {
return (componentOptions) => {
const {
supCodeScope,
initScopeValue = {},
dataSourceCreator,
componentsTree,
} = componentOptions;
const container = createContainer<InstanceT, LifeCycleNameT>({
supCodeScope,
initScopeValue,
stateCreator,
dataSourceCreator,
componentsTree,
});
return componentCreator(container, componentOptions);
};
}

View File

@ -0,0 +1,27 @@
import { invariant, InstantiationService } from '@alilc/lowcode-shared';
import { RendererMain } from './main';
import { type IRender, type RenderAdapter } from './parts/extension';
import type { RendererApplication, AppOptions } from './types';
/**
* createRenderer
* @param schema
* @param options
* @returns
*/
export function createRenderer<Render = IRender>(
renderAdapter: RenderAdapter<Render>,
): (options: AppOptions) => Promise<RendererApplication<Render>> {
invariant(typeof renderAdapter === 'function', 'The first parameter must be a function.');
const instantiationService = new InstantiationService({ defaultScope: 'Singleton' });
instantiationService.bootstrapModules();
const rendererMain = instantiationService.createInstance(RendererMain);
return async (options) => {
rendererMain.initialize(options);
return rendererMain.startup<Render>(renderAdapter);
};
}

View File

@ -1,42 +0,0 @@
import { type AnyFunction } from './types';
import { createHookStore, type HookStore } from './utils/hook';
import { nonSetterProxy } from './utils/non-setter-proxy';
import { type RuntimeError } from './utils/error';
export interface AppBoosts {
[key: string]: any;
}
export interface RuntimeHooks {
'app:error': (error: RuntimeError) => void;
[key: PropertyKey]: AnyFunction;
}
export interface AppBoostsManager {
hookStore: HookStore<RuntimeHooks>;
readonly value: AppBoosts;
add(name: PropertyKey, value: any, force?: boolean): void;
remove(name: PropertyKey): void;
}
const boostsValue: AppBoosts = {};
const proxyBoostsValue = nonSetterProxy(boostsValue);
export const appBoosts: AppBoostsManager = {
hookStore: createHookStore(),
get value() {
return proxyBoostsValue;
},
add(name: PropertyKey, value: any, force = false) {
if ((boostsValue as any)[name] && !force) return;
(boostsValue as any)[name] = value;
},
remove(name) {
if ((boostsValue as any)[name]) {
delete (boostsValue as any)[name];
}
},
};

View File

@ -1,142 +0,0 @@
import type { AnyFunction, PlainObject, JSExpression, JSFunction } from './types';
import { isJSExpression, isJSFunction } from './utils/type-guard';
import { processValue } from './utils/value';
export interface CodeRuntime {
run<T = unknown>(code: string): T | undefined;
createFnBoundScope(code: string): AnyFunction | undefined;
parseExprOrFn(value: PlainObject): any;
bindingScope(scope: CodeScope): void;
getScope(): CodeScope;
}
const SYMBOL_SIGN = '__code__scope';
export function createCodeRuntime(scopeOrValue: PlainObject = {}): CodeRuntime {
let runtimeScope = scopeOrValue[Symbol.for(SYMBOL_SIGN)]
? (scopeOrValue as CodeScope)
: createScope(scopeOrValue);
function run<T = unknown>(code: string): T | undefined {
if (!code) return undefined;
try {
return new Function(
'scope',
`"use strict";return (function(){return (${code})}).bind(scope)();`,
)(runtimeScope.value) as T;
} catch (err) {
// todo
console.error('%c eval error', code, runtimeScope.value, err);
return undefined;
}
}
function createFnBoundScope(code: string) {
const fn = run(code);
if (typeof fn !== 'function') return undefined;
return fn.bind(runtimeScope.value);
}
function parseExprOrFn(value: PlainObject) {
return processValue(
value,
(data) => {
return isJSExpression(data) || isJSFunction(data);
},
(node: JSExpression | JSFunction) => {
let v;
if (node.type === 'JSExpression') {
v = run(node.value);
} else if (node.type === 'JSFunction') {
v = createFnBoundScope(node.value);
}
if (typeof v === 'undefined' && (node as any).mock) {
return (node as any).mock;
}
return v;
},
);
}
return {
run,
createFnBoundScope,
parseExprOrFn,
bindingScope(nextScope) {
runtimeScope = nextScope;
},
getScope() {
return runtimeScope;
},
};
}
export interface CodeScope<T extends PlainObject = PlainObject, K extends keyof T = keyof T> {
readonly value: T;
inject(name: K, value: T[K], force?: boolean): void;
setValue(value: T, replace?: boolean): void;
createSubScope<O extends PlainObject = PlainObject>(initValue: O): CodeScope<T & O>;
}
export function createScope<T extends PlainObject = PlainObject, K extends keyof T = keyof T>(
initValue: T,
): CodeScope<T, K> {
const innerScope = { value: initValue };
const proxyValue: T = new Proxy(Object.create(null), {
set(target, p, newValue, receiver) {
return Reflect.set(target, p, newValue, receiver);
},
get(target, p, receiver) {
let valueTarget = innerScope;
while (valueTarget) {
if (Reflect.has(valueTarget.value, p)) {
return Reflect.get(valueTarget.value, p, receiver);
}
valueTarget = (valueTarget as any).__parent;
}
return Reflect.get(target, p, receiver);
},
});
const scope: CodeScope<T, K> = {
get value() {
// dev return value
return proxyValue;
},
inject(name, value, force = false): void {
if (innerScope.value[name] && !force) {
return;
}
innerScope.value[name] = value;
},
setValue(value, replace = false) {
if (replace) {
innerScope.value = { ...value };
} else {
innerScope.value = Object.assign({}, innerScope.value, value);
}
},
createSubScope<O extends PlainObject = PlainObject>(initValue: O) {
const childScope = createScope<O & T>(initValue);
(childScope as any).__raw.__parent = innerScope;
return childScope;
},
};
Object.defineProperty(scope, Symbol.for(SYMBOL_SIGN), { get: () => true });
// development env
Object.defineProperty(scope, '__raw', { get: () => innerScope });
return scope;
}

View File

@ -1,171 +0,0 @@
import type {
InstanceApi,
PlainObject,
ComponentTree,
InstanceDataSourceApi,
InstanceStateApi,
} from '@alilc/lowcode-shared';
import { type CodeScope, type CodeRuntime, createCodeRuntime, createScope } from './code-runtime';
import { isJSFunction } from './utils/type-guard';
import { type TextWidget, type ComponentWidget, createWidget } from './widget';
/**
*
*/
export interface Container<InstanceT = unknown, LifeCycleNameT extends string = string> {
readonly codeRuntime: CodeRuntime;
readonly instanceApiObject: InstanceApi<InstanceT>;
/**
* css
*/
getCssText(): string | undefined;
/**
*
*/
triggerLifeCycle(lifeCycleName: LifeCycleNameT, ...args: any[]): void;
/**
* ref , scope.$() 使
*/
setInstance(ref: string, instance: InstanceT): void;
/**
* ref
*/
removeInstance(ref: string, instance?: InstanceT): void;
createWidgets<Element>(): (TextWidget<Element> | ComponentWidget<Element>)[];
}
export interface CreateContainerOptions<LifeCycleNameT extends string> {
supCodeScope?: CodeScope;
initScopeValue?: PlainObject;
componentsTree: ComponentTree<LifeCycleNameT>;
stateCreator: (initalState: PlainObject) => InstanceStateApi;
// type todo
dataSourceCreator: (...args: any[]) => InstanceDataSourceApi;
}
export function createContainer<InstanceT, LifeCycleNameT extends string>(
options: CreateContainerOptions<LifeCycleNameT>,
): Container<InstanceT, LifeCycleNameT> {
const {
componentsTree,
supCodeScope,
initScopeValue = {},
stateCreator,
dataSourceCreator,
} = options;
validContainerSchema(componentsTree);
const instancesMap = new Map<string, InstanceT[]>();
const subScope = supCodeScope
? supCodeScope.createSubScope(initScopeValue)
: createScope(initScopeValue);
const codeRuntime = createCodeRuntime(subScope);
const initalState = codeRuntime.parseExprOrFn(componentsTree.state ?? {});
const initalProps = codeRuntime.parseExprOrFn(componentsTree.props ?? {});
const stateApi = stateCreator(initalState);
const dataSourceApi = dataSourceCreator(componentsTree.dataSource, stateApi);
const instanceApiObject: InstanceApi<InstanceT> = Object.assign(
{
props: initalProps,
$(ref: string) {
const insArr = instancesMap.get(ref);
if (!insArr) return undefined;
return insArr[0];
},
$$(ref: string) {
return instancesMap.get(ref) ?? [];
},
},
stateApi,
dataSourceApi,
);
if (componentsTree.methods) {
for (const [key, fn] of Object.entries(componentsTree.methods)) {
const customMethod = codeRuntime.createFnBoundScope(fn.value);
if (customMethod) {
instanceApiObject[key] = customMethod;
}
}
}
const containerCodeScope = subScope.createSubScope(instanceApiObject);
codeRuntime.bindingScope(containerCodeScope);
function setInstanceByRef(ref: string, ins: InstanceT) {
let insArr = instancesMap.get(ref);
if (!insArr) {
insArr = [];
instancesMap.set(ref, insArr);
}
insArr!.push(ins);
}
function removeInstanceByRef(ref: string, ins?: InstanceT) {
const insArr = instancesMap.get(ref);
if (insArr) {
if (ins) {
const idx = insArr.indexOf(ins);
if (idx > 0) insArr.splice(idx, 1);
} else {
instancesMap.delete(ref);
}
}
}
function triggerLifeCycle(lifeCycleName: LifeCycleNameT, ...args: any[]) {
// keys 用来判断 lifeCycleName 存在于 schema 对象上,不获取原型链上的对象
if (
!componentsTree.lifeCycles ||
!Object.keys(componentsTree.lifeCycles).includes(lifeCycleName)
) {
return;
}
const lifeCycleSchema = componentsTree.lifeCycles[lifeCycleName];
if (isJSFunction(lifeCycleSchema)) {
const lifeCycleFn = codeRuntime.createFnBoundScope(lifeCycleSchema.value);
if (lifeCycleFn) {
lifeCycleFn.apply(containerCodeScope.value, args);
}
}
}
return {
get codeRuntime() {
return codeRuntime;
},
get instanceApiObject() {
return containerCodeScope.value as InstanceApi<InstanceT>;
},
getCssText() {
return componentsTree.css;
},
triggerLifeCycle,
setInstance: setInstanceByRef,
removeInstance: removeInstanceByRef,
createWidgets<Element>() {
if (!componentsTree.children) return [];
return componentsTree.children.map((item) => createWidget<Element>(item));
},
};
}
const CONTAINTER_NAME = ['Page', 'Block', 'Component'];
function validContainerSchema(schema: ComponentTree) {
if (!CONTAINTER_NAME.includes(schema.componentName)) {
throw Error('container schema not valid');
}
}

View File

@ -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';

View File

@ -0,0 +1,86 @@
import { Injectable } from '@alilc/lowcode-shared';
import { ICodeRuntimeService } from './parts/code-runtime';
import { IExtensionHostService, type RenderAdapter } from './parts/extension';
import { IPackageManagementService } from './parts/package';
import { IRuntimeUtilService } from './parts/runtimeUtil';
import { IRuntimeIntlService } from './parts/runtimeIntl';
import { ISchemaService } from './parts/schema';
import type { AppOptions, RendererApplication } from './types';
@Injectable()
export class RendererMain {
private mode: 'development' | 'production' = 'production';
private initOptions: AppOptions;
constructor(
@ICodeRuntimeService private codeRuntimeService: ICodeRuntimeService,
@IPackageManagementService private packageManagementService: IPackageManagementService,
@IRuntimeUtilService private runtimeUtilService: IRuntimeUtilService,
@IRuntimeIntlService private runtimeIntlService: IRuntimeIntlService,
@ISchemaService private schemaService: ISchemaService,
@IExtensionHostService private extensionHostService: IExtensionHostService,
) {}
async initialize(options: AppOptions) {
const { schema, mode } = options;
if (mode) this.mode = mode;
this.initOptions = { ...options };
// valid schema
this.schemaService.initialize(schema);
// init intl
const finalLocale = options.locale ?? navigator.language;
const i18nTranslations = this.schemaService.get('i18n') ?? {};
this.runtimeIntlService.initialize(finalLocale, i18nTranslations);
}
async startup<Render>(adapter: RenderAdapter<Render>): Promise<RendererApplication<Render>> {
const render = await this.extensionHostService.runRender<Render>(adapter);
// construct application
const app = Object.freeze<RendererApplication<Render>>({
mode: this.mode,
schema: this.schemaService,
packageManager: this.packageManagementService,
...render,
use: (plugin) => {
return this.extensionHostService.registerPlugin(plugin);
},
});
// setup plugins
this.extensionHostService.initialize(app);
await this.extensionHostService.registerPlugin(this.initOptions.plugins ?? []);
// load packages
await this.packageManagementService.loadPackages(this.initOptions.packages ?? []);
// resolve component maps
const componentsMaps = this.schemaService.get('componentsMap');
this.packageManagementService.resolveComponentMaps(componentsMaps);
this.initGlobalScope();
return app;
}
private initGlobalScope() {
// init runtime uitls
const utils = this.schemaService.get('utils') ?? [];
for (const util of utils) {
this.runtimeUtilService.add(util);
}
const globalScope = this.codeRuntimeService.getScope();
globalScope.setValue({
utils: this.runtimeUtilService.toExpose(),
...this.runtimeIntlService.toExpose(),
});
}
}

View File

@ -1,157 +0,0 @@
import { type Package, type ComponentMap, type LowCodeComponent } from './types';
const packageStore: Map<string, any> = ((window as any).__PACKAGE_STORE__ ??= new Map());
export interface PackageLoader {
name?: string;
load(packageInfo: Package, thisManager: PackageManager): Promise<any>;
active(packageInfo: Package): boolean;
}
export interface PackageManager {
/**
*
* @param packages
*/
addPackages(packages: Package[]): Promise<void>;
/** 通过包名获取资产包信息 */
getPackageInfo(packageName: string): Package | undefined;
getLibraryByPackageName(packageName: string): any;
setLibraryByPackageName(packageName: string, library: any): void;
/** 新增资产包加载器 */
addPackageLoader(loader: PackageLoader): void;
/** 解析组件映射 */
resolveComponentMaps(componentMaps: ComponentMap[]): void;
/** 获取组件映射对象key = componentName value = component */
getComponentsNameRecord<C = unknown>(
componentMaps?: ComponentMap[],
): Record<string, C | LowCodeComponent>;
/** 通过组件名获取对应的组件 */
getComponent<C = unknown>(componentName: string): C | LowCodeComponent | undefined;
/** 注册组件 */
registerComponentByName(componentName: string, Component: unknown): void;
}
export function createPackageManager(): PackageManager {
const packageLoaders: PackageLoader[] = [];
const componentsRecord: Record<string, any> = {};
const packagesRef: Package[] = [];
async function addPackages(packages: Package[]) {
for (const item of packages) {
if (!item.package && !item.id) continue;
const newId = item.package ?? item.id!;
const isExist = packagesRef.some((_) => {
const itemId = _.package ?? _.id;
return itemId === newId;
});
if (!isExist) {
packagesRef.push(item);
if (!packageStore.has(newId)) {
const loader = packageLoaders.find((loader) => loader.active(item));
if (!loader) continue;
try {
const result = await loader.load(item, manager);
if (result) packageStore.set(newId, result);
} catch (e) {
throw e;
}
}
}
}
}
function getPackageInfo(packageName: string) {
return packagesRef.find((p) => p.package === packageName);
}
function getLibraryByPackageName(packageName: string) {
const packageInfo = getPackageInfo(packageName);
if (packageInfo) {
return packageStore.get(packageInfo.package ?? packageInfo.id!);
}
}
function setLibraryByPackageName(packageName: string, library: any) {
packageStore.set(packageName, library);
}
function resolveComponentMaps(componentMaps: ComponentMap[]) {
for (const map of componentMaps) {
if (map.devMode === 'lowCode') {
const packageInfo = packagesRef.find((_) => {
return _.id === (map as LowCodeComponent).id;
});
if (packageInfo) {
componentsRecord[map.componentName] = packageInfo;
}
} else {
if (packageStore.has(map.package!)) {
const library = packageStore.get(map.package!);
// export { exportName } from xxx exportName === global.libraryName.exportName
// export exportName from xxx exportName === global.libraryName.default || global.libraryName
// export { exportName as componentName } from package
// if exportName == null exportName === componentName;
// const componentName = exportName.subName, if exportName empty subName donot use
const paths = map.exportName && map.subName ? map.subName.split('.') : [];
const exportName = map.exportName ?? map.componentName;
if (map.destructuring) {
paths.unshift(exportName);
}
let result = library;
for (const path of paths) {
result = result[path] || result;
}
const recordName = map.componentName ?? map.exportName;
componentsRecord[recordName] = result;
}
}
}
}
function getComponentsNameRecord(componentMaps?: ComponentMap[]) {
if (componentMaps) {
resolveComponentMaps(componentMaps);
}
return { ...componentsRecord };
}
function getComponent(componentName: string) {
return componentsRecord[componentName];
}
function registerComponentByName(componentName: string, Component: unknown) {
componentsRecord[componentName] = Component;
}
const manager: PackageManager = {
addPackages,
getPackageInfo,
getLibraryByPackageName,
setLibraryByPackageName,
addPackageLoader(loader) {
if (!loader.name || !packageLoaders.some((_) => _.name === loader.name)) {
packageLoaders.push(loader);
}
},
resolveComponentMaps,
getComponentsNameRecord,
getComponent,
registerComponentByName,
};
return manager;
}

View File

@ -0,0 +1,86 @@
import {
type PlainObject,
type Spec,
isJSFunction,
isJSExpression,
createCallback,
EventDisposable,
createDecorator,
Provide,
} from '@alilc/lowcode-shared';
import { type ICodeScope, CodeScope } from './codeScope';
import { processValue } from '../../utils/value';
export interface ICodeRuntimeService {
getScope(): ICodeScope;
run<R = unknown>(code: string, scope?: ICodeScope): R | undefined;
resolve(value: PlainObject, scope?: ICodeScope): any;
beforeRun(fn: (code: string) => string): EventDisposable;
createChildScope(value: PlainObject): ICodeScope;
}
export const ICodeRuntimeService = createDecorator<ICodeRuntimeService>('codeRuntimeService');
@Provide(ICodeRuntimeService)
export class CodeRuntimeService implements ICodeRuntimeService {
private codeScope: ICodeScope = new CodeScope({});
private callbacks = createCallback<(code: string) => string>();
getScope() {
return this.codeScope;
}
run<R = unknown>(code: string, scope: ICodeScope = this.codeScope): R | undefined {
if (!code) return undefined;
try {
const cbs = this.callbacks.list();
const finalCode = cbs.reduce((code, cb) => cb(code), code);
let result = new Function(
'scope',
`"use strict";return (function(){return (${finalCode})}).bind(scope)();`,
)(scope.value);
if (typeof result === 'function') {
result = result.bind(scope.value);
}
return result as R;
} catch (err) {
// todo replace logger
console.error('%c eval error', code, scope.value, err);
return undefined;
}
}
resolve(value: PlainObject, scope: ICodeScope = this.codeScope) {
return processValue(
value,
(data) => {
return isJSExpression(data) || isJSFunction(data);
},
(node: Spec.JSExpression | Spec.JSFunction) => {
const v = this.run(node.value, scope);
if (typeof v === 'undefined' && (node as any).mock) {
return (node as any).mock;
}
return v;
},
);
}
beforeRun(fn: (code: string) => string): EventDisposable {
return this.callbacks.add(fn);
}
createChildScope(value: PlainObject): ICodeScope {
return this.codeScope.createChild(value);
}
}

View File

@ -0,0 +1,74 @@
import { type PlainObject } from '@alilc/lowcode-shared';
export interface ICodeScope {
readonly value: PlainObject;
inject(name: string, value: any, force?: boolean): void;
setValue(value: PlainObject, replace?: boolean): void;
createChild(initValue: PlainObject): ICodeScope;
}
/**
*
*/
interface IScopeNode {
prev?: IScopeNode;
current: PlainObject;
next?: IScopeNode;
}
export class CodeScope implements ICodeScope {
__node: IScopeNode;
private proxyValue: PlainObject;
constructor(initValue: PlainObject) {
this.__node = {
current: initValue,
};
this.proxyValue = new Proxy(Object.create(null) as PlainObject, {
set(target, p, newValue, receiver) {
return Reflect.set(target, p, newValue, receiver);
},
get: (target, p, receiver) => {
let valueTarget: IScopeNode | undefined = this.__node;
while (valueTarget) {
if (Reflect.has(valueTarget.current, p)) {
return Reflect.get(valueTarget.current, p, receiver);
}
valueTarget = this.__node.prev;
}
return Reflect.get(target, p, receiver);
},
});
}
get value() {
return this.proxyValue;
}
inject(name: string, value: any, force = false): void {
if (this.__node.current[name] && !force) {
return;
}
this.__node.current.value[name] = value;
}
setValue(value: PlainObject, replace = false) {
if (replace) {
this.__node.current = { ...value };
} else {
this.__node.current = Object.assign({}, this.__node.current, value);
}
}
createChild(initValue: PlainObject): ICodeScope {
const subScope = new CodeScope(initValue);
subScope.__node.prev = this.__node;
return subScope as ICodeScope;
}
}

View File

@ -0,0 +1,2 @@
export * from './codeScope';
export * from './codeRuntimeService';

View File

@ -0,0 +1,2 @@
export * from './treeModel';
export * from './treeModelService';

View File

@ -0,0 +1,183 @@
import {
type Spec,
type PlainObject,
isJSFunction,
isComponentNode,
invariant,
type AnyFunction,
} from '@alilc/lowcode-shared';
import { type ICodeScope, type ICodeRuntimeService } from '../code-runtime';
import { IWidget, Widget } from '../widget';
/**
*
*/
export interface IComponentTreeModel<Component, ComponentInstance = unknown> {
readonly codeScope: ICodeScope;
readonly codeRuntime: ICodeRuntimeService;
readonly widgets: IWidget<Component, ComponentInstance>[];
/**
* css
*/
getCssText(): string | undefined;
/**
*
*/
triggerLifeCycle(lifeCycleName: Spec.ComponentLifeCycle, ...args: any[]): void;
/**
* ref , scope.$() 使
*/
setComponentRef(ref: string, component: ComponentInstance): void;
/**
* ref
*/
removeComponentRef(ref: string, component?: ComponentInstance): void;
/**
* compoonentsTree.children widget
*/
buildWidgets(nodes: Spec.NodeType[]): IWidget<Component, ComponentInstance>[];
}
export type ModelScopeStateCreator = (initalState: PlainObject) => Spec.InstanceStateApi;
export type ModelScopeDataSourceCreator = (...args: any[]) => Spec.InstanceDataSourceApi;
export interface ComponentTreeModelOptions {
stateCreator: ModelScopeStateCreator;
dataSourceCreator: ModelScopeDataSourceCreator;
}
const defaultDataSourceSchema: Spec.ComponentDataSource = {
list: [],
dataHandler: {
type: 'JSFunction',
value: '() => {}',
},
};
export class ComponentTreeModel<Component, ComponentInstance = unknown>
implements IComponentTreeModel<Component, ComponentInstance>
{
private instanceMap = new Map<string, ComponentInstance[]>();
public codeScope: ICodeScope;
public widgets: IWidget<Component>[] = [];
constructor(
public componentsTree: Spec.ComponentTree,
public codeRuntime: ICodeRuntimeService,
options: ComponentTreeModelOptions,
) {
invariant(componentsTree, 'componentsTree must to provide', 'ComponentTreeModel');
this.initModelScope(options.stateCreator, options.dataSourceCreator);
if (componentsTree.children) {
this.widgets = this.buildWidgets(componentsTree.children);
}
}
private initModelScope(
stateCreator: ModelScopeStateCreator,
dataSourceCreator: ModelScopeDataSourceCreator,
) {
const {
state = {},
props = {},
dataSource = defaultDataSourceSchema,
methods = {},
} = this.componentsTree;
this.codeScope = this.codeRuntime.createChildScope({});
const initalState = this.codeRuntime.resolve(state, this.codeScope);
const initalProps = this.codeRuntime.resolve(props, this.codeScope);
const stateApi = stateCreator(initalState);
const dataSourceApi = dataSourceCreator(dataSource, stateApi);
this.codeScope.setValue(
Object.assign(
{
props: initalProps,
$: (ref: string) => {
const insArr = this.instanceMap.get(ref);
if (!insArr) return undefined;
return insArr[0];
},
$$: (ref: string) => {
return this.instanceMap.get(ref) ?? [];
},
},
stateApi,
dataSourceApi,
),
);
for (const [key, fn] of Object.entries(methods)) {
const customMethod = this.codeRuntime.run(fn.value, this.codeScope);
if (customMethod) {
this.codeScope.inject(key, customMethod);
}
}
}
getCssText(): string | undefined {
return this.componentsTree.css;
}
triggerLifeCycle(lifeCycleName: Spec.ComponentLifeCycle, ...args: any[]) {
// keys 用来判断 lifeCycleName 存在于 schema 对象上,不获取原型链上的对象
if (
!this.componentsTree.lifeCycles ||
!Object.keys(this.componentsTree.lifeCycles).includes(lifeCycleName)
) {
return;
}
const lifeCycleSchema = this.componentsTree.lifeCycles[lifeCycleName];
if (isJSFunction(lifeCycleSchema)) {
const lifeCycleFn = this.codeRuntime.run<AnyFunction>(lifeCycleSchema.value, this.codeScope);
if (lifeCycleFn) {
lifeCycleFn.apply(this.codeScope.value, args);
}
}
}
setComponentRef(ref: string, ins: ComponentInstance) {
let insArr = this.instanceMap.get(ref);
if (!insArr) {
insArr = [];
this.instanceMap.set(ref, insArr);
}
insArr!.push(ins);
}
removeComponentRef(ref: string, ins?: ComponentInstance) {
const insArr = this.instanceMap.get(ref);
if (insArr) {
if (ins) {
const idx = insArr.indexOf(ins);
if (idx > 0) insArr.splice(idx, 1);
} else {
this.instanceMap.delete(ref);
}
}
}
buildWidgets(nodes: Spec.NodeType[]): IWidget<Component>[] {
return nodes.map((node) => {
const widget = new Widget<Component, ComponentInstance>(node, this);
if (isComponentNode(node) && node.children?.length) {
widget.children = this.buildWidgets(node.children);
}
return widget;
});
}
}

View File

@ -0,0 +1,51 @@
import { createDecorator, Provide, invariant, type Spec } from '@alilc/lowcode-shared';
import { ICodeRuntimeService } from '../code-runtime';
import {
type IComponentTreeModel,
ComponentTreeModel,
type ComponentTreeModelOptions,
} from './treeModel';
import { ISchemaService } from '../schema';
export interface IComponentTreeModelService {
create<Component>(
componentsTree: Spec.ComponentTree,
options: ComponentTreeModelOptions,
): IComponentTreeModel<Component>;
createById<Component>(
id: string,
options: ComponentTreeModelOptions,
): IComponentTreeModel<Component>;
}
export const IComponentTreeModelService = createDecorator<IComponentTreeModelService>(
'componentTreeModelService',
);
@Provide(IComponentTreeModelService)
export class ComponentTreeModelService implements IComponentTreeModelService {
constructor(
@ISchemaService private schemaService: ISchemaService,
@ICodeRuntimeService private codeRuntimeService: ICodeRuntimeService,
) {}
create<Component>(
componentsTree: Spec.ComponentTree,
options: ComponentTreeModelOptions,
): IComponentTreeModel<Component> {
return new ComponentTreeModel(componentsTree, this.codeRuntimeService, options);
}
createById<Component>(
id: string,
options: ComponentTreeModelOptions,
): IComponentTreeModel<Component> {
const componentsTrees = this.schemaService.get('componentsTree');
const componentsTree = componentsTrees.find((item) => item.id === id);
invariant(componentsTree, 'componentsTree not found');
return new ComponentTreeModel(componentsTree, this.codeRuntimeService, options);
}
}

View File

@ -0,0 +1,87 @@
import { createDecorator, Provide, type PlainObject } from '@alilc/lowcode-shared';
import { isObject } from 'lodash-es';
import { ICodeRuntimeService } from '../code-runtime';
import { IRuntimeUtilService } from '../runtimeUtil';
import { IRuntimeIntlService } from '../runtimeIntl';
export type IBoosts<Extends> = IBoostsApi & Extends;
export interface IBoostsApi {
readonly codeRuntime: ICodeRuntimeService;
readonly intl: Pick<IRuntimeIntlService, 't' | 'setLocale' | 'getLocale' | 'addTranslations'>;
readonly util: Pick<IRuntimeUtilService, 'add' | 'remove'>;
}
/**
*
*/
export interface IBoostsService {
extend(name: string, value: any, force?: boolean): void;
extend(value: PlainObject, force?: boolean): void;
toExpose<Extends>(): IBoosts<Extends>;
}
export const IBoostsService = createDecorator<IBoostsService>('boostsService');
@Provide(IBoostsService)
export class BoostsService implements IBoostsService {
private builtInApis: IBoostsApi;
private extendsValue: PlainObject = {};
private _expose: any;
constructor(
@ICodeRuntimeService private codeRuntimeService: ICodeRuntimeService,
@IRuntimeIntlService private runtimeIntlService: IRuntimeIntlService,
@IRuntimeUtilService private runtimeUtilService: IRuntimeUtilService,
) {
this.builtInApis = {
codeRuntime: this.codeRuntimeService,
intl: this.runtimeIntlService,
util: this.runtimeUtilService,
};
}
extend(name: string, value: any, force?: boolean | undefined): void;
extend(value: PlainObject, force?: boolean | undefined): void;
extend(name: string | PlainObject, value?: any, force?: boolean | undefined): void {
if (typeof name === 'string') {
if (force) {
this.extendsValue[name] = value;
} else {
if (!this.extendsValue[name]) {
this.extendsValue[name] = value;
}
}
} else if (isObject(name)) {
Object.keys(name).forEach((key) => {
this.extend(key, name[key], value);
});
}
}
toExpose<Extends>(): IBoosts<Extends> {
if (!this._expose) {
this._expose = new Proxy(Object.create(null), {
get: (_, p, receiver) => {
return (
Reflect.get(this.builtInApis, p, receiver) ||
Reflect.get(this.extendsValue, p, receiver)
);
},
set() {
return false;
},
has: (_, p) => {
return Reflect.has(this.builtInApis, p) || Reflect.has(this.extendsValue, p);
},
});
}
return this._expose;
}
}

View File

@ -0,0 +1,125 @@
import {
invariant,
createDecorator,
Provide,
EventEmitter,
KeyValueStore,
} from '@alilc/lowcode-shared';
import { type Plugin } from './plugin';
import { IBoostsService } from './boosts';
import { IPackageManagementService } from '../package';
import { ISchemaService } from '../schema';
import { type RenderAdapter } from './render';
import { IComponentTreeModelService } from '../component-tree-model';
import type { RendererApplication } from '../../types';
interface IPluginRuntime extends Plugin {
status: 'setup' | 'ready';
}
export interface IExtensionHostService {
initialize(app: RendererApplication): void;
/* ========= plugin ============= */
registerPlugin(plugin: Plugin | Plugin[]): Promise<void>;
getPlugin(name: string): Plugin | undefined;
/* =========== render =============== */
runRender<Render>(adapter: RenderAdapter<Render>): Promise<Render>;
dispose(): Promise<void>;
}
export const IExtensionHostService =
createDecorator<IExtensionHostService>('pluginManagementService');
@Provide(IExtensionHostService)
export class ExtensionHostService implements IExtensionHostService {
private pluginRuntimes: IPluginRuntime[] = [];
private app: RendererApplication;
private eventEmitter = new EventEmitter();
private globalState = new KeyValueStore();
constructor(
@IPackageManagementService private packageManagementService: IPackageManagementService,
@IBoostsService private boostsService: IBoostsService,
@ISchemaService private schemaService: ISchemaService,
@IComponentTreeModelService private componentTreeModelService: IComponentTreeModelService,
) {}
initialize(app: RendererApplication) {
this.app = app;
}
async registerPlugin(plugins: Plugin | Plugin[]) {
plugins = Array.isArray(plugins) ? plugins : [plugins];
for (const plugin of plugins) {
if (this.pluginRuntimes.find((item) => item.name === plugin.name)) {
console.warn(`${plugin.name} 插件已注册`);
continue;
}
await this.doSetupPlugin(plugin);
}
}
getPlugin(name: string): Plugin | undefined {
return this.pluginRuntimes.find((item) => item.name === name);
}
async runRender<Render>(adapter: RenderAdapter<Render>): Promise<Render> {
invariant(adapter, 'render adapter not settled', 'ExtensionHostService');
return adapter({
schema: this.schemaService,
packageManager: this.packageManagementService,
boostsManager: this.boostsService,
componentTreeModel: this.componentTreeModelService,
});
}
async dispose(): Promise<void> {
for (const plugin of this.pluginRuntimes) {
await plugin.destory?.();
}
}
private async doSetupPlugin(plugin: Plugin) {
const pluginRuntime = plugin as IPluginRuntime;
if (!this.pluginRuntimes.some((item) => item.name !== pluginRuntime.name)) {
this.pluginRuntimes.push({
...pluginRuntime,
status: 'ready',
});
}
const isSetup = (name: string) => {
const setupPlugins = this.pluginRuntimes.filter((item) => item.status === 'setup');
return setupPlugins.some((p) => p.name === name);
};
if (pluginRuntime.dependsOn?.some((dep) => !isSetup(dep))) {
return;
}
await pluginRuntime.setup(this.app, {
eventEmitter: this.eventEmitter,
globalState: this.globalState,
boosts: this.boostsService.toExpose(),
});
pluginRuntime.status = 'setup';
// 遍历未安装的插件 寻找 dependsOn 的插件已安装完的插件进行安装
const readyPlugins = this.pluginRuntimes.filter((item) => item.status === 'ready');
const readyPlugin = readyPlugins.find((item) => item.dependsOn?.every((dep) => isSetup(dep)));
if (readyPlugin) {
await this.doSetupPlugin(readyPlugin);
}
}
}

View File

@ -0,0 +1,4 @@
export * from './extensionHostService';
export * from './plugin';
export * from './boosts';
export * from './render';

View File

@ -0,0 +1,20 @@
import { EventEmitter, KeyValueStore } from '@alilc/lowcode-shared';
import { type RendererApplication } from '../../types';
import { IBoosts } from './boosts';
export interface PluginContext<BoostsExtends = object> {
eventEmitter: EventEmitter;
globalState: KeyValueStore;
boosts: IBoosts<BoostsExtends>;
}
export interface Plugin<BoostsExtends = object> {
/**
* name
*/
name: string;
setup(app: RendererApplication, context: PluginContext<BoostsExtends>): void | Promise<void>;
destory?(): void | Promise<void>;
dependsOn?: string[];
}

View File

@ -0,0 +1,23 @@
import { IPackageManagementService } from '../package';
import { IBoostsService } from './boosts';
import { ISchemaService } from '../schema';
import { IComponentTreeModelService } from '../component-tree-model';
export interface IRender {
mount: (el: HTMLElement) => void | Promise<void>;
unmount: () => void | Promise<void>;
}
export interface RenderContext {
readonly schema: Omit<ISchemaService, 'initialize'>;
readonly packageManager: IPackageManagementService;
readonly boostsManager: IBoostsService;
readonly componentTreeModel: IComponentTreeModelService;
}
export interface RenderAdapter<Render> {
(context: RenderContext): Render | Promise<Render>;
}

View File

@ -0,0 +1,2 @@
export * from './loader';
export * from './managementService';

View File

@ -0,0 +1,12 @@
import { type Spec } from '@alilc/lowcode-shared';
import { type IPackageManagementService } from './managementService';
export interface PackageLoader {
name?: string;
load(this: IPackageManagementService, info: Spec.Package): Promise<any>;
active(info: Spec.Package): boolean;
}
export function definePackageLoader(loader: PackageLoader) {
return loader;
}

View File

@ -0,0 +1,146 @@
import { type Spec, type LowCodeComponent, createDecorator, Provide } from '@alilc/lowcode-shared';
import { PackageLoader } from './loader';
export interface IPackageManagementService {
/**
*
* @param packages
*/
loadPackages(packages: Spec.Package[]): Promise<void>;
/** 通过包名获取资产包信息 */
getPackageInfo(packageName: string): Spec.Package | undefined;
getLibraryByPackageName(packageName: string): any;
setLibraryByPackageName(packageName: string, library: any): void;
/** 解析组件映射 */
resolveComponentMaps(componentMaps: Spec.ComponentMap[]): void;
/** 获取组件映射对象key = componentName value = component */
getComponentsNameRecord<C = unknown>(
componentMaps?: Spec.ComponentMap[],
): Record<string, C | LowCodeComponent>;
/** 通过组件名获取对应的组件 */
getComponent<C = unknown>(componentName: string): C | LowCodeComponent | undefined;
/** 注册组件 */
registerComponentByName(componentName: string, Component: unknown): void;
/** 新增资产包加载器 */
addPackageLoader(loader: PackageLoader): void;
}
export const IPackageManagementService = createDecorator<IPackageManagementService>(
'packageManagementService',
);
@Provide(IPackageManagementService)
export class PackageManagementService implements IPackageManagementService {
private componentsRecord: Record<string, any> = {};
private packageStore: Map<string, any> = ((window as any).__PACKAGE_STORE__ ??= new Map());
private packagesRef: Spec.Package[] = [];
private packageLoaders: PackageLoader[] = [];
async loadPackages(packages: Spec.Package[]) {
for (const item of packages) {
if (!item.package && !item.id) continue;
const newId = item.package ?? item.id!;
const isExist = this.packagesRef.some((_) => {
const itemId = _.package ?? _.id;
return itemId === newId;
});
if (!isExist) {
this.packagesRef.push(item);
if (!this.packageStore.has(newId)) {
const loader = this.packageLoaders.find((loader) => loader.active(item));
if (!loader) continue;
const result = await loader.load.call(this, item);
if (result) this.packageStore.set(newId, result);
}
}
}
}
getPackageInfo(packageName: string) {
return this.packagesRef.find((p) => p.package === packageName);
}
getLibraryByPackageName(packageName: string) {
const packageInfo = this.getPackageInfo(packageName);
if (packageInfo) {
return this.packageStore.get(packageInfo.package ?? packageInfo.id!);
}
}
setLibraryByPackageName(packageName: string, library: any) {
this.packageStore.set(packageName, library);
}
resolveComponentMaps(componentMaps: Spec.ComponentMap[]) {
for (const map of componentMaps) {
if (map.devMode === 'lowCode') {
const packageInfo = this.packagesRef.find((_) => {
return _.id === (map as LowCodeComponent).id;
});
if (packageInfo) {
this.componentsRecord[map.componentName] = packageInfo;
}
} else {
if (this.packageStore.has(map.package!)) {
const library = this.packageStore.get(map.package!);
// export { exportName } from xxx exportName === global.libraryName.exportName
// export exportName from xxx exportName === global.libraryName.default || global.libraryName
// export { exportName as componentName } from package
// if exportName == null exportName === componentName;
// const componentName = exportName.subName, if exportName empty subName donot use
const paths = map.exportName && map.subName ? map.subName.split('.') : [];
const exportName = map.exportName ?? map.componentName;
if (map.destructuring) {
paths.unshift(exportName);
}
let result = library;
for (const path of paths) {
result = result[path] || result;
}
const recordName = map.componentName ?? map.exportName;
this.componentsRecord[recordName] = result;
}
}
}
}
getComponentsNameRecord(componentMaps?: Spec.ComponentMap[]) {
if (componentMaps) {
const newMaps = componentMaps.filter((item) => !this.componentsRecord[item.componentName]);
this.resolveComponentMaps(newMaps);
}
return { ...this.componentsRecord };
}
getComponent(componentName: string) {
return this.componentsRecord[componentName];
}
registerComponentByName(componentName: string, Component: unknown) {
this.componentsRecord[componentName] = Component;
}
addPackageLoader(loader: PackageLoader) {
if (!loader.name || !this.packageLoaders.some((_) => _.name === loader.name)) {
this.packageLoaders.push(loader);
}
}
}

View File

@ -0,0 +1,84 @@
import {
createDecorator,
Provide,
Intl,
type Spec,
type Locale,
type LocaleTranslationsRecord,
type Translations,
} from '@alilc/lowcode-shared';
export interface MessageDescriptor {
key: string;
params?: Record<string, string>;
fallback?: string;
}
export interface IRuntimeIntlService {
initialize(locale: Locale, messages: LocaleTranslationsRecord): void;
t(descriptor: MessageDescriptor): string;
setLocale(locale: Locale): void;
getLocale(): Locale;
addTranslations(locale: Locale, translations: Translations): void;
toExpose(): Spec.IntlApi;
}
export const IRuntimeIntlService = createDecorator<IRuntimeIntlService>('IRuntimeIntlService');
@Provide(IRuntimeIntlService)
export class RuntimeIntlService implements IRuntimeIntlService {
private intl: Intl;
private _expose: any;
initialize(locale: Locale, messages: LocaleTranslationsRecord) {
this.intl = new Intl(locale, messages);
}
t(descriptor: MessageDescriptor): string {
const formatter = this.intl.getFormatter();
return formatter.$t(
{
id: descriptor.key,
defaultMessage: descriptor.fallback,
},
descriptor.params,
);
}
setLocale(locale: string): void {
this.intl.setLocale(locale);
}
getLocale(): string {
return this.intl.getLocale();
}
addTranslations(locale: Locale, translations: Translations) {
this.intl.addTranslations(locale, translations);
}
toExpose(): Spec.IntlApi {
if (!this._expose) {
this._expose = Object.freeze<Spec.IntlApi>({
i18n: (key, params) => {
return this.t({ key, params });
},
getLocale: () => {
return this.getLocale();
},
setLocale: (locale) => {
this.setLocale(locale);
},
});
}
return this._expose;
}
}

View File

@ -0,0 +1,83 @@
import { type AnyFunction, type Spec, createDecorator, Provide } from '@alilc/lowcode-shared';
import { IPackageManagementService } from './package';
import { ICodeRuntimeService } from './code-runtime';
export interface IRuntimeUtilService {
add(utilItem: Spec.Util): void;
add(name: string, fn: AnyFunction): void;
remove(name: string): void;
toExpose(): Spec.UtilsApi;
}
export const IRuntimeUtilService = createDecorator<IRuntimeUtilService>('rendererUtilService');
@Provide(IRuntimeUtilService)
export class RuntimeUtilService implements IRuntimeUtilService {
private utilsMap: Map<string, AnyFunction> = new Map();
private _expose: any;
constructor(
@ICodeRuntimeService private codeRuntimeService: ICodeRuntimeService,
@IPackageManagementService private packageManagementService: IPackageManagementService,
) {}
add(utilItem: Spec.Util): void;
add(name: string, fn: AnyFunction): void;
add(name: Spec.Util | string, fn?: AnyFunction): void {
if (typeof name === 'string') {
if (typeof fn === 'function') {
this.utilsMap.set(name, fn as AnyFunction);
}
} else {
const fn = this.parseUtil(name);
this.utilsMap.set(name.name, fn);
}
}
remove(name: string): void {
this.utilsMap.delete(name);
}
toExpose(): Spec.UtilsApi {
if (!this._expose) {
this._expose = new Proxy(Object.create(null), {
get: (_, p: string) => {
return this.utilsMap.get(p);
},
set() {
return false;
},
has: (_, p: string) => {
return this.utilsMap.has(p);
},
});
}
return this._expose;
}
private parseUtil(utilItem: Spec.Util) {
if (utilItem.type === 'function') {
const { content } = utilItem;
return this.codeRuntimeService.run(content.value);
} else {
const {
content: { package: packageName, destructuring, exportName, subName },
} = utilItem;
let library: any = this.packageManagementService.getLibraryByPackageName(packageName!);
if (library) {
if (destructuring) {
const target = library[exportName!];
library = subName ? target[subName] : target;
}
return library;
}
}
}
}

View File

@ -0,0 +1 @@
export * from './schemaService';

View File

@ -0,0 +1,65 @@
import {
type Spec,
createDecorator,
Provide,
KeyValueStore,
type EventDisposable,
} from '@alilc/lowcode-shared';
import { isObject } from 'lodash-es';
import { schemaValidation } from './validation';
export interface NormalizedSchema extends Spec.Project {}
export type NormalizedSchemaKey = keyof NormalizedSchema;
export interface ISchemaService {
initialize(schema: Spec.Project): void;
get<K extends NormalizedSchemaKey>(key: K): NormalizedSchema[K];
set<K extends NormalizedSchemaKey>(key: K, value: NormalizedSchema[K]): void;
onValueChange<K extends NormalizedSchemaKey>(
key: K,
listener: (value: NormalizedSchema[K]) => void,
): EventDisposable;
}
export const ISchemaService = createDecorator<ISchemaService>('schemaService');
@Provide(ISchemaService)
export class SchemaService implements ISchemaService {
private store: KeyValueStore<NormalizedSchema, NormalizedSchemaKey>;
constructor() {
this.store = new KeyValueStore<NormalizedSchema, NormalizedSchemaKey>(new Map(), {
setterValidation: schemaValidation,
});
}
initialize(schema: unknown): void {
if (!isObject(schema)) {
throw Error('schema muse a object');
}
Object.keys(schema).forEach((key) => {
// @ts-expect-error: ignore initialization
this.set(key, schema[key]);
});
}
set<K extends NormalizedSchemaKey>(key: K, value: NormalizedSchema[K]): void {
this.store.set(key, value);
}
get<K extends NormalizedSchemaKey>(key: K): NormalizedSchema[K] {
return this.store.get(key) as NormalizedSchema[K];
}
onValueChange<K extends NormalizedSchemaKey>(
key: K,
listener: (value: NormalizedSchema[K]) => void,
): EventDisposable {
return this.store.onValueChange(key, listener);
}
}

View File

@ -0,0 +1,25 @@
import { type Spec } from '@alilc/lowcode-shared';
const SCHEMA_VALIDATIONS_OPTIONS: Partial<
Record<
keyof Spec.Project,
{
valid: (value: any) => boolean;
description: string;
}
>
> = {};
export function schemaValidation<K extends keyof Spec.Project>(key: K, value: Spec.Project[K]) {
const validOption = SCHEMA_VALIDATIONS_OPTIONS[key];
if (validOption) {
const result = validOption.valid(value);
if (!result) {
throw Error(validOption.description);
}
}
return true;
}

View File

@ -0,0 +1 @@
export * from './widget';

View File

@ -0,0 +1,68 @@
import { type Spec, uniqueId, EventDisposable, createCallback } from '@alilc/lowcode-shared';
import { clone } from 'lodash-es';
import { IComponentTreeModel } from '../component-tree-model';
export interface WidgetBuildContext<Component, ComponentInstance = unknown> {
key: string;
node: Spec.NodeType;
model: IComponentTreeModel<Component, ComponentInstance>;
children?: IWidget<Component, ComponentInstance>[];
}
export interface IWidget<Component, ComponentInstance = unknown> {
readonly key: string;
readonly node: Spec.NodeType;
children?: IWidget<Component, ComponentInstance>[];
beforeBuild<T extends Spec.NodeType>(beforeGuard: (node: T) => T): EventDisposable;
build<Element>(
builder: (context: WidgetBuildContext<Component, ComponentInstance>) => Element,
): Element;
}
export class Widget<Component, ComponentInstance = unknown>
implements IWidget<Component, ComponentInstance>
{
private beforeGuardCallbacks = createCallback();
public __raw: Spec.NodeType;
public node: Spec.NodeType;
public key: string;
public children?: IWidget<Component, ComponentInstance>[] | undefined;
constructor(
node: Spec.NodeType,
private model: IComponentTreeModel<Component, ComponentInstance>,
) {
this.node = clone(node);
this.__raw = node;
this.key = (node as Spec.ComponentNode)?.id ?? uniqueId();
}
beforeBuild<T extends Spec.NodeType>(beforeGuard: (node: T) => T): EventDisposable {
return this.beforeGuardCallbacks.add(beforeGuard);
}
build<Element>(
builder: (context: WidgetBuildContext<Component, ComponentInstance>) => Element,
): Element {
const beforeGuards = this.beforeGuardCallbacks.list();
const finalNode = beforeGuards.reduce((prev, cb) => cb(prev), this.node);
return builder({
key: this.key,
node: finalNode,
model: this.model,
children: this.children,
});
}
}

View File

@ -1,60 +0,0 @@
import { type AppContext } from './api/app';
import { nonSetterProxy } from './utils/non-setter-proxy';
export interface Plugin<C extends PluginSetupContext = PluginSetupContext> {
name: string; // 插件的 name 作为唯一标识,并不可重复。
setup(setupContext: C): void | Promise<void>;
dependsOn?: string[];
}
export interface PluginSetupContext extends AppContext {
[key: string]: any;
}
export function createPluginManager(context: PluginSetupContext) {
const installedPlugins: Plugin[] = [];
let readyToInstallPlugins: Plugin[] = [];
const setupContext = nonSetterProxy(context);
async function install(plugin: Plugin) {
if (installedPlugins.some((p) => p.name === plugin.name)) return;
if (plugin.dependsOn?.some((dep) => !installedPlugins.some((p) => p.name === dep))) {
readyToInstallPlugins.push(plugin);
return;
}
await plugin.setup(setupContext);
installedPlugins.push(plugin);
// 遍历未安装的插件 寻找 dependsOn 的插件已安装完的插件进行安装
for (const item of readyToInstallPlugins) {
if (item.dependsOn?.every((dep) => installedPlugins.some((p) => p.name === dep))) {
await item.setup(setupContext);
installedPlugins.push(item);
}
}
if (readyToInstallPlugins.length) {
readyToInstallPlugins = readyToInstallPlugins.filter((item) =>
installedPlugins.some((p) => p.name === item.name),
);
}
}
return {
async add(plugin: Plugin) {
if (installedPlugins.find((item) => item.name === plugin.name)) {
console.warn('该插件已安装');
return;
}
await install(plugin);
},
};
}
export function definePlugin<C extends PluginSetupContext, P = Plugin<C>>(plugin: P) {
return plugin;
}

View File

@ -1,106 +0,0 @@
import type { Project, ComponentTree, ComponentMap, PageConfig } from './types';
import { throwRuntimeError } from './utils/error';
import { set, get } from 'lodash-es';
export interface AppSchema {
getComponentsTrees(): ComponentTree[];
addComponentsTree(tree: ComponentTree): void;
removeComponentsTree(id: string): void;
getComponentsMaps(): ComponentMap[];
addComponentsMap(componentName: ComponentMap): void;
removeComponentsMap(componentName: string): void;
getPageConfigs(): PageConfig[];
addPageConfig(page: PageConfig): void;
removePageConfig(id: string): void;
getByKey<K extends keyof Project>(key: K): Project[K] | undefined;
updateByKey<K extends keyof Project>(
key: K,
updater: Project[K] | ((value: Project[K]) => Project[K]),
): void;
getByPath(path: string | string[]): any;
updateByPath(path: string | string[], updater: any | ((value: any) => any)): void;
find(predicate: (schema: Project) => any): any;
}
export function createAppSchema(schema: Project): AppSchema {
if (!schema.version.startsWith('1.')) {
throwRuntimeError('core', 'schema version must be 1.x.x');
}
const schemaRef = structuredClone(schema);
return {
getComponentsTrees() {
return schemaRef.componentsTree;
},
addComponentsTree(tree) {
addArrayItem(schemaRef.componentsTree, tree, 'id');
},
removeComponentsTree(id) {
removeArrayItem(schemaRef.componentsTree, 'id', id);
},
getComponentsMaps() {
return schemaRef.componentsMap;
},
addComponentsMap(componentsMap) {
addArrayItem(schemaRef.componentsMap, componentsMap, 'componentName');
},
removeComponentsMap(componentName) {
removeArrayItem(schemaRef.componentsMap, 'componentName', componentName);
},
getPageConfigs() {
return schemaRef.pages ?? [];
},
addPageConfig(page) {
schemaRef.pages ??= [];
addArrayItem(schemaRef.pages, page, 'id');
},
removePageConfig(id) {
schemaRef.pages ??= [];
removeArrayItem(schemaRef.pages, 'id', id);
},
getByKey(key) {
return schemaRef[key];
},
updateByKey(key, updater) {
const value = schemaRef[key];
schemaRef[key] = typeof updater === 'function' ? (updater as any)(value) : updater;
},
find(predicate) {
return predicate(schemaRef);
},
getByPath(path) {
return get(schemaRef, path);
},
updateByPath(path, updater) {
set(schemaRef, path, typeof updater === 'function' ? updater(this.getByPath(path)) : updater);
},
};
}
function addArrayItem<T extends Record<string, any>>(target: T[], item: T, comparison: string) {
const idx = target.findIndex((_) => _[comparison] === item[comparison]);
if (idx > -1) {
target.splice(idx, 1, item);
} else {
target.push(item);
}
}
function removeArrayItem<T extends Record<string, any>>(
target: T[],
comparison: string,
comparisonValue: any,
) {
const idx = target.findIndex((item) => item[comparison] === comparisonValue);
if (idx > -1) target.splice(idx, 1);
}

View File

@ -0,0 +1,31 @@
import { type Spec } from '@alilc/lowcode-shared';
import { type Plugin } from './parts/extension';
import { type ISchemaService } from './parts/schema';
import { type IPackageManagementService } from './parts/package';
import { type IExtensionHostService } from './parts/extension';
export interface AppOptions {
schema: Spec.Project;
packages?: Spec.Package[];
plugins?: Plugin[];
/**
* navigator.language
*/
locale?: string;
/**
*
*/
mode?: 'development' | 'production';
}
export type RendererApplication<Render = unknown> = {
readonly mode: 'development' | 'production';
readonly schema: Omit<ISchemaService, 'initialize'>;
readonly packageManager: IPackageManagementService;
use: IExtensionHostService['registerPlugin'];
} & Render;

View File

@ -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);
}

View File

@ -1,8 +0,0 @@
let idStart = 0x0907;
/**
* Generate unique id
*/
export function guid(): number {
return idStart++;
}

View File

@ -1,177 +0,0 @@
import type { AnyFunction } from '../types';
export type EventName = string | number | symbol;
export function createEvent<T = AnyFunction>() {
let events: T[] = [];
function add(fn: T) {
events.push(fn);
return () => {
events = events.filter((e) => e !== fn);
};
}
function remove(fn: T) {
events = events.filter((f) => fn !== f);
}
function list() {
return [...events];
}
return {
add,
remove,
list,
clear() {
events.length = 0;
},
};
}
export type Event<F = AnyFunction> = ReturnType<typeof createEvent<F>>;
export type HookCallback = (...args: any) => Promise<any> | any;
type HookKeys<T> = keyof T & PropertyKey;
type InferCallback<HT, HN extends keyof HT> = HT[HN] extends HookCallback ? HT[HN] : never;
declare global {
interface Console {
// https://developer.chrome.com/blog/devtools-modern-web-debugging/#linked-stack-traces
createTask(name: string): {
run: <T extends () => any>(fn: T) => ReturnType<T>;
};
}
}
// https://developer.chrome.com/blog/devtools-modern-web-debugging/#linked-stack-traces
type CreateTask = typeof console.createTask;
const defaultTask: ReturnType<CreateTask> = { run: (fn) => fn() };
const _createTask: CreateTask = () => defaultTask;
const createTask = typeof console.createTask !== 'undefined' ? console.createTask : _createTask;
export interface HookStore<
HooksT extends Record<PropertyKey, any> = Record<PropertyKey, HookCallback>,
HookNameT extends HookKeys<HooksT> = HookKeys<HooksT>,
> {
hook<NameT extends HookNameT>(name: NameT, fn: InferCallback<HooksT, NameT>): () => void;
call<NameT extends HookNameT>(
name: NameT,
...args: Parameters<InferCallback<HooksT, NameT>>
): void;
callAsync<NameT extends HookNameT>(
name: NameT,
...args: Parameters<InferCallback<HooksT, NameT>>
): Promise<void>;
callParallel<NameT extends HookNameT>(
name: NameT,
...args: Parameters<InferCallback<HooksT, NameT>>
): Promise<void[]>;
remove<NameT extends HookNameT>(name: NameT, fn?: InferCallback<HooksT, NameT>): void;
clear<NameT extends HookNameT>(name?: NameT): void;
getHooks<NameT extends HookNameT>(name: NameT): InferCallback<HooksT, NameT>[] | undefined;
}
export function createHookStore<
HooksT extends Record<PropertyKey, any> = Record<PropertyKey, HookCallback>,
HookNameT extends HookKeys<HooksT> = HookKeys<HooksT>,
>(): HookStore<HooksT, HookNameT> {
const hooksMap = new Map<HookNameT, Event<HookCallback>>();
function hook<NameT extends HookNameT>(name: NameT, fn: InferCallback<HooksT, NameT>) {
if (!name || typeof fn !== 'function') {
return () => {};
}
let hooks = hooksMap.get(name);
if (!hooks) {
hooks = createEvent();
hooksMap.set(name, hooks);
}
hooks.add(fn);
return () => remove(name, fn);
}
function call<NameT extends HookNameT>(
name: NameT,
...args: Parameters<InferCallback<HooksT, NameT>>
) {
const hooks = hooksMap.get(name)?.list() ?? [];
for (const hookFn of hooks) {
hookFn.call(null, ...args);
}
}
function callAsync<NameT extends HookNameT>(
name: NameT,
...args: Parameters<InferCallback<HooksT, NameT>>
) {
const hooks = hooksMap.get(name)?.list() ?? [];
const task = createTask(name.toString());
return hooks.reduce(
(promise, hookFunction) => promise.then(() => task.run(() => hookFunction(...args))),
Promise.resolve(),
);
}
function callParallel<NameT extends HookNameT>(
name: NameT,
...args: Parameters<InferCallback<HooksT, NameT>>
) {
const hooks = hooksMap.get(name)?.list() ?? [];
const task = createTask(name.toString());
return Promise.all(hooks.map((hook) => task.run(() => hook(...args))));
}
function remove<NameT extends HookNameT>(name: NameT, fn?: InferCallback<HooksT, NameT>) {
const hooks = hooksMap.get(name);
if (!hooks) return;
if (fn) {
hooks.remove(fn);
if (hooks.list().length === 0) {
hooksMap.delete(name);
}
} else {
hooksMap.delete(name);
}
}
function clear<NameT extends HookNameT>(name?: NameT) {
if (name) {
remove(name);
} else {
hooksMap.clear();
}
}
function getHooks<NameT extends HookNameT>(
name: NameT,
): InferCallback<HooksT, NameT>[] | undefined {
return hooksMap.get(name)?.list() as InferCallback<HooksT, NameT>[] | undefined;
}
return {
hook,
call,
callAsync,
callParallel,
remove,
clear,
getHooks,
};
}

View File

@ -1,13 +0,0 @@
export function nonSetterProxy<T extends object>(target: T) {
return new Proxy<T>(target, {
get(target, p, receiver) {
return Reflect.get(target, p, receiver);
},
set() {
return false;
},
has(target, p) {
return Reflect.has(target, p);
},
});
}

View File

@ -1,89 +0,0 @@
import type { NodeType, ComponentTreeNode, ComponentTreeNodeProps, JSExpression } from './types';
import { isJSExpression, isI18nNode } from './utils/type-guard';
import { guid } from './utils/guid';
export class Widget<Data, Element> {
protected proxyElements: Element[] = [];
protected renderObject: Element | undefined;
constructor(public raw: Data) {
this.init();
}
protected init() {}
get key(): string {
return (this.raw as any)?.id ?? `${guid()}`;
}
mapRenderObject(mapper: (widget: Widget<Data, Element>) => Element | undefined) {
this.renderObject = mapper(this);
return this;
}
addProxyELements(el: Element) {
this.proxyElements.unshift(el);
}
build<C>(builder: (elements: Element[]) => C): C {
return builder(this.renderObject ? [...this.proxyElements, this.renderObject] : []);
}
}
export type TextWidgetData = Exclude<NodeType, ComponentTreeNode>;
export type TextWidgetType = 'string' | 'expression' | 'i18n';
export class TextWidget<E = unknown> extends Widget<TextWidgetData, E> {
type: TextWidgetType = 'string';
protected init() {
if (isJSExpression(this.raw)) {
this.type = 'expression';
} else if (isI18nNode(this.raw)) {
this.type = 'i18n';
}
}
}
export class ComponentWidget<E = unknown> extends Widget<ComponentTreeNode, E> {
private _children: (TextWidget<E> | ComponentWidget<E>)[] = [];
private _propsValue: ComponentTreeNodeProps = {};
protected init() {
if (this.raw.props) {
this._propsValue = this.raw.props;
}
if (this.raw.children) {
this._children = this.raw.children.map((child) => createWidget<E>(child));
}
}
get componentName() {
return this.raw.componentName;
}
get props() {
return this._propsValue ?? {};
}
get condition() {
return this.raw.condition !== false;
}
get loop(): unknown[] | JSExpression | undefined {
return this.raw.loop;
}
get loopArgs() {
return this.raw.loopArgs ?? ['item', 'index'];
}
get children() {
return this._children;
}
}
export function createWidget<E = unknown>(data: NodeType) {
if (typeof data === 'string' || isJSExpression(data) || isI18nNode(data)) {
return new TextWidget<E>(data);
} else if (data.componentName) {
return new ComponentWidget<E>(data);
}
throw Error(`unknown node data: ${JSON.stringify(data)}`);
}

View File

@ -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/"

View File

@ -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 = (

View File

@ -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<NavigationCallback>();
const teardowns = createEvent<() => void>();
const listeners = createCallback<NavigationCallback>();
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<NavigationCallback>();
const listeners = createCallback<NavigationCallback>();
function triggerListeners(
to: HistoryLocation,

View File

@ -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';

View File

@ -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));
}

View File

@ -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<NavigationGuard>();
const afterGuards = createEvent<NavigationHookAfter>();
const beforeGuards = createCallback<NavigationGuard>();
const afterGuards = createCallback<NavigationHookAfter>();
let currentLocation: RouteLocationNormalized = START_LOCATION;
let pendingLocation = currentLocation;

View File

@ -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

View File

@ -0,0 +1,30 @@
import type { AnyFunction } from '@alilc/lowcode-shared';
export function createCallback<T = AnyFunction>() {
let events: T[] = [];
function add(fn: T) {
events.push(fn);
return () => {
events = events.filter((e) => e !== fn);
};
}
function remove(fn: T) {
events = events.filter((f) => fn !== f);
}
function list() {
return [...events];
}
return {
add,
remove,
list,
clear() {
events.length = 0;
},
};
}

View File

@ -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');

View File

@ -8,26 +8,26 @@ export type PathParams = Record<string, string | string[]>;
* 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<number[]>
score: Array<number[]>;
/**
* 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<Token[]>,
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;

View File

@ -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"
}

View File

@ -0,0 +1,100 @@
import { Hookable, type HookKeys, type HookCallback } from 'hookable';
export type EventListener = HookCallback;
export type EventDisposable = () => void;
export interface IEventEmitter<
HooksT extends Record<string, any> = Record<string, HookCallback>,
HookNameT extends HookKeys<HooksT> = HookKeys<HooksT>,
> {
/**
*
* add monitor to a event
* @param event
* @param listener
*/
on(event: HookNameT, listener: HooksT[HookNameT]): EventDisposable;
/**
*
* @param event
* @param listener
*/
once(event: HookNameT, listener: HooksT[HookNameT]): void;
/**
*
* emit a message for a event
* @param event
* @param args
*/
emit(event: HookNameT, ...args: any): Promise<any>;
/**
*
* cancel a monitor from a event
* @param event
* @param listener
*/
off(event: HookNameT, listener: HooksT[HookNameT]): void;
/**
*
* @param event
* @param listener
*/
prependListener(event: HookNameT, listener: HooksT[HookNameT]): EventDisposable;
/**
*
*/
removeAll(): void;
}
export class EventEmitter<
HooksT extends Record<string, any> = Record<string, HookCallback>,
HookNameT extends HookKeys<HooksT> = HookKeys<HooksT>,
> implements IEventEmitter<HooksT, HookNameT>
{
private namespace: string | undefined;
private hooks = new Hookable<HooksT, HookNameT>();
constructor(namespace?: string) {
this.namespace = namespace;
}
on(event: HookNameT, listener: HooksT[HookNameT]): EventDisposable {
return this.hooks.hook(event, listener);
}
once(event: HookNameT, listener: HooksT[HookNameT]): void {
this.hooks.hookOnce(event, listener);
}
async emit(event: HookNameT, ...args: any) {
return this.hooks.callHook(event, ...args);
}
off(event: HookNameT, listener: HooksT[HookNameT]): void {
this.hooks.removeHook(event, listener);
}
/**
*
* @param event
* @param listener
*/
prependListener(event: HookNameT, listener: HooksT[HookNameT]): EventDisposable {
return this.hooks.hook(`${event}:before` as HookNameT, listener);
}
removeAll(): void {
this.hooks.removeAllHooks();
}
}
export function createEventEmitter<T extends Record<string, any>>(
namespace?: string,
): EventEmitter<T> {
return new EventEmitter<T>(namespace);
}

View File

@ -0,0 +1,5 @@
export * from './event';
export * from './logger';
export * from './storage';
export * from './intl';
export * from './instantiation';

View File

@ -0,0 +1,59 @@
import '@abraham/reflection';
import { Container, inject, interfaces, injectable } from 'inversify';
import { fluentProvide, buildProviderModule } from 'inversify-binding-decorators';
/**
* Identifies a service of type `T`.
*/
export interface ServiceIdentifier<T> {
(...args: any[]): void;
type: T;
}
export type Constructor<T = any> = new (...args: any[]) => T;
export function createDecorator<T>(serviceId: string): ServiceIdentifier<T> {
const id = <any>(
function (target: Constructor, targetKey: string, indexOrPropertyDescriptor: any): any {
return inject(serviceId)(target, targetKey, indexOrPropertyDescriptor);
}
);
id.toString = () => serviceId;
return id;
}
export const Injectable = injectable;
export function Provide<T>(serviceId: ServiceIdentifier<T>, isSingleTon?: boolean) {
const ret = fluentProvide(serviceId.toString());
if (isSingleTon) {
return ret.inSingletonScope().done();
}
return ret.done();
}
export class InstantiationService {
private container: Container;
constructor(options?: interfaces.ContainerOptions) {
this.container = new Container(options);
}
get<T>(serviceIdentifier: ServiceIdentifier<T>) {
return this.container.get<T>(serviceIdentifier);
}
set<T>(serviceIdentifier: ServiceIdentifier<T>, constructor: Constructor<T>) {
this.container.bind<T>(serviceIdentifier).to(constructor);
}
createInstance<T extends Constructor>(App: T) {
return this.container.resolve<InstanceType<T>>(App);
}
bootstrapModules() {
this.container.load(buildProviderModule());
}
}

View File

@ -0,0 +1,108 @@
import { createIntl, createIntlCache, type IntlShape as IntlFormatter } from '@formatjs/intl';
import { mapKeys } from 'lodash-es';
import { signal, computed, effect, type Signal, type ComputedSignal } from '../signals';
export { IntlFormatter };
export type Locale = string;
export type Translations = Record<string, string>;
export type LocaleTranslationsRecord = Record<Locale, Translations>;
export class Intl {
private locale: Signal<Locale>;
private messageStore: Signal<LocaleTranslationsRecord>;
private currentMessage: ComputedSignal<Translations>;
private intlShape: IntlFormatter;
constructor(defaultLocale?: string, messages: LocaleTranslationsRecord = {}) {
if (defaultLocale) {
defaultLocale = nomarlizeLocale(defaultLocale);
} else {
defaultLocale = 'zh-CN';
}
const messageStore = mapKeys(messages, (_, key) => {
return nomarlizeLocale(key);
});
this.locale = signal(defaultLocale);
this.messageStore = signal(messageStore);
this.currentMessage = computed(() => {
return this.messageStore.value[this.locale.value] ?? {};
});
effect(() => {
const cache = createIntlCache();
this.intlShape = createIntl(
{
locale: this.locale.value,
messages: this.currentMessage.value,
},
cache,
);
});
}
getLocale() {
return this.locale.value;
}
setLocale(locale: Locale) {
const nomarlizedLocale = nomarlizeLocale(locale);
this.locale.value = nomarlizedLocale;
}
addTranslations(locale: Locale, messages: Translations) {
locale = nomarlizeLocale(locale);
const original = this.messageStore.value[locale];
this.messageStore.value[locale] = Object.assign(original, messages);
}
getFormatter(): IntlFormatter {
return this.intlShape;
}
}
const navigatorLanguageMapping: Record<string, string> = {
en: 'en-US',
zh: 'zh-CN',
zt: 'zh-TW',
es: 'es-ES',
pt: 'pt-PT',
fr: 'fr-FR',
de: 'de-DE',
it: 'it-IT',
ru: 'ru-RU',
ja: 'ja-JP',
ko: 'ko-KR',
ar: 'ar-SA',
tr: 'tr-TR',
th: 'th-TH',
vi: 'vi-VN',
nl: 'nl-NL',
he: 'iw-IL',
id: 'in-ID',
pl: 'pl-PL',
hi: 'hi-IN',
uk: 'uk-UA',
ms: 'ms-MY',
tl: 'tl-PH',
};
/**
* nomarlize navigator.language or user input's locale
* eg: zh -> zh-CN, zh_CN -> zh-CN, zh-cn -> zh-CN
* @param target
*/
function nomarlizeLocale(target: Locale) {
if (navigatorLanguageMapping[target]) {
return navigatorLanguageMapping[target];
}
const replaced = target.replace('_', '-');
const splited = replaced.split('-').slice(0, 2);
splited[1] = splited[1].toUpperCase();
return splited.join('-');
}

View File

@ -0,0 +1,151 @@
import { invariant } from '../utils';
import { PlainObject } from '../types';
/**
* MapLike interface
*/
export interface IStore<O, K extends keyof O> {
readonly size: number;
get(key: K, defaultValue: O[K]): O[K];
get(key: K, defaultValue?: O[K]): O[K] | undefined;
set(key: K, value: O[K]): void;
delete(key: K): void;
clear(): void;
}
/**
*
*/
export class KeyValueStore<O = PlainObject, K extends keyof O = keyof O> {
private setterValidation: ((key: K, value: O[K]) => boolean | string) | undefined;
private waits = new Map<
K,
{
once?: boolean;
resolve: (data: any) => void;
}[]
>();
constructor(
private readonly store: IStore<O, K> = new Map(),
options?: {
setterValidation?: (key: K, value: O[K]) => boolean | string;
},
) {
if (options?.setterValidation) {
this.setterValidation = options.setterValidation;
}
}
get(key: K, defaultValue: O[K]): O[K];
get(key: K, defaultValue?: O[K] | undefined): O[K] | undefined;
get(key: K, defaultValue?: O[K]): O[K] | undefined {
const value = this.store.get(key, defaultValue);
return value;
}
set(key: K, value: O[K]): void {
if (this.setterValidation) {
const valid = this.setterValidation(key, value);
invariant(
valid === false || typeof valid === 'string',
`failed to config ${key.toString()}, only predefined options can be set under strict mode, predefined options: ${valid ? valid : ''}`,
'KeyValueStore',
);
}
this.store.set(key, value);
this.dispatchValue(key);
}
delete(key: K): void {
this.store.delete(key);
}
clear(): void {
this.store.clear();
}
get size(): number {
return this.store.size;
}
/**
* key
* Promise fullfill
* @param key
* @returns
*/
waitForValue(key: K) {
const val = this.get(key);
if (val !== undefined) {
return Promise.resolve(val);
}
return new Promise((resolve) => {
this.addWaiter(key, resolve, true);
});
}
/**
* key
* @param key
* @param fn
* @returns
*/
onValueChange<T extends K>(key: T, fn: (value: O[T]) => void): () => void {
const val = this.get(key);
if (val !== undefined) {
// @ts-expect-error: val is not undefined
fn(val);
}
this.addWaiter(key, fn as any);
return () => {
this.removeWaiter(key, fn as any);
};
}
private dispatchValue(key: K): void {
const waits = this.waits.get(key);
if (!waits) return;
for (let i = waits.length - 1; i >= 0; i--) {
const waiter = waits[i];
waiter.resolve(this.get(key)!);
if (waiter.once) {
waits.splice(i, 1); // Remove the waiter if it only waits once
}
}
if (waits.length === 0) {
this.waits.delete(key); // No more waiters for the key
}
}
private addWaiter(key: K, resolve: (value: O[K]) => void, once?: boolean) {
if (this.waits.has(key)) {
this.waits.get(key)!.push({ resolve, once });
} else {
this.waits.set(key, [{ resolve, once }]);
}
}
private removeWaiter(key: K, resolve: (value: O[K]) => void) {
const waits = this.waits.get(key);
if (!waits) return;
this.waits.set(
key,
waits.filter((waiter) => waiter.resolve !== resolve),
);
if (this.waits.get(key)!.length === 0) {
this.waits.delete(key);
}
}
}

View File

@ -1,4 +1,4 @@
export * from './types';
export * from './utils';
export * from './signals';
export * from './parts';
export * from './abilities';

View File

@ -1,74 +0,0 @@
import { Hookable, type HookKeys, type HookCallback } from 'hookable';
export type EventListener = HookCallback;
export type EventDisposable = () => void;
/**
* todo: logger
*/
export class EventEmitter<
HooksT extends Record<string, any> = Record<string, HookCallback>,
HookNameT extends HookKeys<HooksT> = HookKeys<HooksT>,
> extends Hookable<HooksT, HookNameT> {
#namespace: string | undefined;
constructor(namespace?: string) {
super();
this.#namespace = namespace;
}
/**
*
* add monitor to a event
* @param event
* @param listener
*/
on(event: HookNameT, listener: HooksT[HookNameT]): EventDisposable {
return this.hook(event, listener);
}
/**
*
* emit a message for a event
* @param event
* @param args
*/
async emit(event: HookNameT, ...args: any) {
return this.callHook(event, ...args);
}
/**
*
* cancel a monitor from a event
* @param event
* @param listener
*/
off(event: HookNameT, listener: HooksT[HookNameT]): void {
this.removeHook(event, listener);
}
/**
*
* @param event
* @param listener
*/
prependListener(event: HookNameT, listener: HooksT[HookNameT]): EventDisposable {
const _hooks = (this as any)._hooks;
const hooks = _hooks[event];
if (Array.isArray(hooks)) {
hooks.unshift(listener);
return () => {
if (listener) {
this.removeHook(event, listener);
}
};
} else {
return this.hook(event, listener);
}
}
}
export function createEventBus<T extends Record<string, any>>(namespace?: string): EventBus<T> {
return new EventBus<T>(namespace);
}

View File

@ -1,3 +0,0 @@
export * from './event';
export * from './logger';
export * from './persistence';

View File

@ -1,41 +0,0 @@
import { createStore } from 'store';
export type StorageValue = string | boolean | number | undefined | null | object;
export interface IPersistence {
get(key: string, fallbackValue: string): string;
get(key: string, fallbackValue?: string): string | undefined;
set(key: string, value: StorageValue): void;
delete(key: string): void;
clear(): void;
}
export class PersistenceStore implements IPersistence {
#store: ReturnType<typeof createStore>;
constructor(namespace?: string) {
this.#store = store.createStore([], namespace);
}
get(key: string, fallbackValue: string): string;
get(key: string, fallbackValue?: string | undefined): string | undefined;
get(key: string, fallbackValue?: unknown): string | undefined {
const value = store.get(key, fallbackValue);
return value;
}
set(key: string, value: StorageValue): void {
this.#store.set(key, value);
}
delete(key: string): void {
this.#store.remove(key);
}
clear(): void {
this.#store.clearAll();
}
}

View File

@ -32,7 +32,7 @@ export type WatchCallback<V = any, OV = any> = (
onCleanup: OnCleanup,
) => any;
type OnCleanup = (cleanupFn: () => void) => void;
export type OnCleanup = (cleanupFn: () => void) => void;
export interface WatchOptions<Immediate = boolean> {
immediate?: Immediate;
@ -42,7 +42,7 @@ export interface WatchOptions<Immediate = boolean> {
const INITIAL_WATCHER_VALUE = {};
type MultiWatchSources = (WatchSource<unknown> | object)[];
export type MultiWatchSources = (WatchSource<unknown> | object)[];
export type WatchStopHandle = () => void;
@ -51,7 +51,7 @@ export function watchEffect(effect: WatchEffect): WatchStopHandle {
return doWatch(effect, null);
}
type MapSources<T, Immediate> = {
export type MapSources<T, Immediate> = {
[K in keyof T]: T[K] extends WatchSource<infer V>
? Immediate extends true
? V | undefined

View File

@ -1,5 +0,0 @@
export type VoidFunction = (...args: any[]) => void;
export type AnyFunction = (...args: any[]) => any;
export type PlainObject = Record<string, any>;

View File

@ -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<string, any>;

View File

@ -0,0 +1,4 @@
export * from './asset-spec';
export * from './lowcode-spec';
export * from './runtime';
export * from './material-spec';

View File

@ -27,7 +27,7 @@ export interface Project {
/**
*
*/
i18n?: I18nMap;
i18n?: LocaleTranslationsMap;
/**
*
*/
@ -40,16 +40,16 @@ export interface Project {
/**
*
*/
config?: Record<string, JSONValue>;
config?: Record<string, JSONObject>;
/**
*
*/
meta?: Record<string, JSONValue>;
meta?: Record<string, JSONObject>;
/**
*
* @deprecated
*/
dataSource?: never;
// dataSource?: never;
/**
*
*/
@ -103,15 +103,13 @@ export interface ComponentMap {
*
* &
*/
export type ComponentTree<LifeCycleNameT extends string = string> =
ComponentTreeRootNode<LifeCycleNameT>;
export type ComponentTree = ComponentTreeRoot;
/**
* (A)
*
*/
export interface ComponentTreeRootNode<LifeCycleNameT extends string = string>
extends ComponentTreeNode {
export interface ComponentTreeRoot extends ComponentNode {
componentName: 'Page' | 'Block' | 'Component';
/**
*
@ -129,7 +127,7 @@ export interface ComponentTreeRootNode<LifeCycleNameT extends string = string>
*
*/
lifeCycles?: {
[name in LifeCycleNameT]: JSFunction;
[name in ComponentLifeCycle]: JSFunction;
};
/**
*
@ -141,7 +139,7 @@ export interface ComponentTreeRootNode<LifeCycleNameT extends string = string>
*
* type todo
*/
dataSource?: any;
dataSource?: ComponentDataSource;
// for useless
loop: never;
@ -149,10 +147,109 @@ export interface ComponentTreeRootNode<LifeCycleNameT extends string = string>
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<string, string>;
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<string, string | number | JSExpression>;
}
export type NodeType = string | JSExpression | I18nNode | ComponentTreeNode;
export type NodeType = string | JSExpression | JSI18n | ComponentNode;

View File

@ -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;
}

Some files were not shown because too many files have changed in this diff Show More