refactor: refactor codes

This commit is contained in:
1ncounter 2024-06-16 20:47:53 +08:00
parent 0157bbd68f
commit 37d07f1db6
78 changed files with 2300 additions and 1817 deletions

View File

@ -1,36 +0,0 @@
import { it, describe, expect } from 'vitest';
import { lazyInject, provide, initInstantiation } from '../src/instantiation';
interface Warrior {
fight(): string;
}
interface Weapon {
hit(): string;
}
@provide(Katana)
class Katana implements Weapon {
public hit() {
return 'cut!';
}
}
@provide(Ninja)
class Ninja implements Warrior {
@lazyInject(Katana)
private _katana: Weapon;
public fight() {
return this._katana.hit();
}
}
initInstantiation();
describe('', () => {
it('works', () => {
const n = new Ninja();
expect(n.fight()).toBe('cut!');
});
});

View File

@ -30,7 +30,8 @@
"hoist-non-react-statics": "^3.3.2",
"use-sync-external-store": "^1.2.0",
"react": "^18.2.0",
"react-dom": "^18.2.0"
"react-dom": "^18.2.0",
"react-is": "^18.3.1"
},
"devDependencies": {
"@testing-library/react": "^14.2.0",
@ -38,7 +39,8 @@
"@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"
"@types/react-dom": "^18.2.22",
"@types/react-is": "^18.3.0"
},
"peerDependencies": {
"react": "^18.2.0",

View File

@ -1,42 +1,15 @@
import { createRenderer, type AppOptions, type IRender } from '@alilc/lowcode-renderer-core';
import { createRenderer, type AppOptions } from '@alilc/lowcode-renderer-core';
import { type ComponentType } from 'react';
import { type Root, createRoot } from 'react-dom/client';
import { createRouter, type RouterOptions } from '@alilc/lowcode-renderer-router';
import AppComponent from '../components/app';
import { RendererContext } from '../context/render';
import { createRouterProvider } from '../components/routerView';
import { rendererExtends } from '../plugin';
import { ApplicationView, RendererContext, extension } from '../app';
export interface ReactAppOptions extends AppOptions {
faultComponent?: ComponentType<any>;
}
const defaultRouterOptions: RouterOptions = {
historyMode: 'browser',
baseName: '/',
routes: [],
};
export const createApp = async (options: ReactAppOptions) => {
const creator = createRenderer<IRender>(async (context) => {
return createRenderer(async (context) => {
const { schema, boostsManager } = context;
const boosts = boostsManager.toExpose();
// router
let routerConfig = defaultRouterOptions;
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);
}
const router = createRouter(routerConfig);
boosts.codeRuntime.getScope().inject('router', router);
// set config
// if (options.faultComponent) {
@ -44,24 +17,21 @@ export const createApp = async (options: ReactAppOptions) => {
// }
// extends boosts
boostsManager.extend(rendererExtends);
const RouterProvider = createRouterProvider(router);
extension.install(boostsManager);
let root: Root | undefined;
return {
async mount(el) {
if (root) {
return;
}
async mount(containerOrId) {
if (root) return;
root = createRoot(el);
const defaultId = schema.get('config')?.targetRootID ?? 'app';
const rootElement = normalizeContainer(containerOrId, defaultId);
root = createRoot(rootElement);
root.render(
<RendererContext.Provider value={context}>
<RouterProvider>
<AppComponent />
</RouterProvider>
<ApplicationView />
</RendererContext.Provider>,
);
},
@ -72,7 +42,23 @@ export const createApp = async (options: ReactAppOptions) => {
}
},
};
});
return creator(options);
})(options);
};
function normalizeContainer(container: Element | string | undefined, defaultId: string): Element {
let result: Element | undefined = undefined;
if (typeof container === 'string') {
const el = document.getElementById(container);
if (el) result = el;
} else if (container instanceof window.Element) {
result = container;
}
if (!result) {
result = document.createElement('div');
result.id = defaultId;
}
return result;
}

View File

@ -1,7 +1,7 @@
import { createRenderer, type AppOptions } from '@alilc/lowcode-renderer-core';
import { FunctionComponent } from 'react';
import { type LowCodeComponentProps, createComponentBySchema } from '../runtime';
import { RendererContext } from '../context/render';
import { type LowCodeComponentProps, createComponentBySchema } from '../runtime/component';
import { RendererContext } from '../app/context';
interface Render {
toComponent(): FunctionComponent<LowCodeComponentProps>;

View File

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

View File

@ -0,0 +1,51 @@
import { type Plugin, type IBoostsService } from '@alilc/lowcode-renderer-core';
import { type ComponentType, type PropsWithChildren } from 'react';
export type WrapperComponent = ComponentType<PropsWithChildren<any>>;
export interface OutletProps {
[key: string]: any;
}
export type Outlet = ComponentType<OutletProps>;
export interface ReactRendererExtensionApi {
addAppWrapper(appWrapper: WrapperComponent): void;
setOutlet(outlet: Outlet): void;
}
class ReactRendererExtension {
private wrappers: WrapperComponent[] = [];
private outlet: Outlet | null = null;
getAppWrappers() {
return this.wrappers;
}
getOutlet() {
return this.outlet;
}
toExpose(): ReactRendererExtensionApi {
return {
addAppWrapper: (appWrapper) => {
if (appWrapper) this.wrappers.push(appWrapper);
},
setOutlet: (outletComponent) => {
if (outletComponent) this.outlet = outletComponent;
},
};
}
install(boostsService: IBoostsService) {
boostsService.extend(this.toExpose());
}
}
export const extension = new ReactRendererExtension();
export function defineRendererPlugin(plugin: Plugin<ReactRendererExtensionApi>) {
return plugin;
}

View File

@ -0,0 +1,3 @@
export * from './context';
export * from './extension';
export * from './view';

View File

@ -0,0 +1,34 @@
import { useRenderContext } from './context';
import { getComponentByName } from '../runtime/component';
import { extension } from './extension';
export function ApplicationView() {
const renderContext = useRenderContext();
const { schema } = renderContext;
const appWrappers = extension.getAppWrappers();
const Outlet = extension.getOutlet();
if (!Outlet) return null;
let element = <Outlet />;
const layoutConfig = schema.get('config')?.layout;
if (layoutConfig) {
const componentName = layoutConfig.componentName;
const Layout = getComponentByName(componentName, renderContext);
if (Layout) {
const layoutProps: any = layoutConfig.props ?? {};
element = <Layout {...layoutProps}>{element}</Layout>;
}
}
if (appWrappers.length > 0) {
element = appWrappers.reduce((preElement, CurrentWrapper) => {
return <CurrentWrapper>{preElement}</CurrentWrapper>;
}, element);
}
return element;
}

View File

@ -1,39 +0,0 @@
import { useRenderContext } from '../context/render';
import { getComponentByName } from '../runtime';
import Route from './route';
import { rendererExtends } from '../plugin';
export default function App() {
const renderContext = useRenderContext();
const { schema } = renderContext;
const appWrappers = rendererExtends.getAppWrappers();
const wrappers = rendererExtends.getRouteWrappers();
let element = <Route />;
if (wrappers.length > 0) {
element = wrappers.reduce((preElement, CurrentWrapper) => {
return <CurrentWrapper>{preElement}</CurrentWrapper>;
}, element);
}
const layoutConfig = schema.get('config')?.layout;
if (layoutConfig) {
const componentName = layoutConfig.componentName as string;
const Layout = getComponentByName(componentName, renderContext);
if (Layout) {
const layoutProps: any = layoutConfig.props ?? {};
element = <Layout {...layoutProps}>{element}</Layout>;
}
}
if (appWrappers.length > 0) {
element = appWrappers.reduce((preElement, CurrentWrapper) => {
return <CurrentWrapper>{preElement}</CurrentWrapper>;
}, element);
}
return element;
}

View File

@ -1,49 +0,0 @@
import { type Spec } from '@alilc/lowcode-shared';
import { useRenderContext } from '../context/render';
import { usePageConfig } from '../context/router';
import { rendererExtends } from '../plugin';
import { createComponentBySchema } from '../runtime';
export interface OutletProps {
pageConfig: Spec.PageConfig;
[key: string]: any;
}
export default function Route(props: any) {
const pageConfig = usePageConfig();
if (pageConfig) {
const Outlet = rendererExtends.getOutlet() ?? RouteOutlet;
return <Outlet {...props} pageConfig={pageConfig} />;
}
return null;
}
function RouteOutlet({ pageConfig }: OutletProps) {
const context = useRenderContext();
const { schema, packageManager } = context;
const { type = 'lowCode', mappingId } = pageConfig;
console.log(
'%c [ pageConfig ]-29',
'font-size:13px; background:pink; color:#bf2c9f;',
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,36 +0,0 @@
import { type Router } from '@alilc/lowcode-renderer-router';
import { useState, useLayoutEffect, useMemo, type ReactNode } from 'react';
import { RouterContext, RouteLocationContext, PageConfigContext } from '../context/router';
import { useRenderContext } from '../context/render';
export const createRouterProvider = (router: Router) => {
return function RouterProvider({ children }: { children?: ReactNode }) {
const { schema } = useRenderContext();
const [location, setCurrentLocation] = useState(router.getCurrentLocation());
useLayoutEffect(() => {
const remove = router.afterRouteChange((to) => setCurrentLocation(to));
return () => remove();
}, []);
const pageConfig = useMemo(() => {
const pages = schema.get('pages') ?? [];
const matched = location.matched[location.matched.length - 1];
if (matched) {
const page = pages.find((item) => matched.page === item.mappingId);
return page;
}
return undefined;
}, [location]);
return (
<RouterContext.Provider value={router}>
<RouteLocationContext.Provider value={location}>
<PageConfigContext.Provider value={pageConfig}>{children}</PageConfigContext.Provider>
</RouteLocationContext.Provider>
</RouterContext.Provider>
);
};
};

View File

@ -1,9 +1,8 @@
export * from './api/app';
export * from './api/component';
export { defineRendererPlugin } from './plugin';
export * from './context/render';
export * from './context/router';
export { useRenderContext, defineRendererPlugin } from './app';
export * from './router';
export type { Spec, ProCodeComponent, LowCodeComponent } from '@alilc/lowcode-shared';
export type { PackageLoader, CodeScope, Plugin } from '@alilc/lowcode-renderer-core';
export type { RendererExtends } from './plugin';
export type { RendererExtends } from './app/extension';

View File

@ -1,50 +0,0 @@
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 defineRendererPlugin(plugin: Plugin<RendererExtends>) {
return plugin;
}

View File

@ -1,5 +1,4 @@
import { type Router, type RouteLocationNormalized } from '@alilc/lowcode-renderer-router';
import { type Spec } from '@alilc/lowcode-shared';
import { createContext, useContext } from 'react';
export const RouterContext = createContext<Router>(undefined!);
@ -13,9 +12,3 @@ export const RouteLocationContext = createContext<RouteLocationNormalized>(undef
RouteLocationContext.displayName = 'RouteLocationContext';
export const useRouteLocation = () => useContext(RouteLocationContext);
export const PageConfigContext = createContext<Spec.PageConfig | undefined>(undefined);
PageConfigContext.displayName = 'PageConfigContext';
export const usePageConfig = () => useContext(PageConfigContext);

View File

@ -0,0 +1,2 @@
export * from './context';
export * from './plugin';

View File

@ -0,0 +1,42 @@
import { defineRendererPlugin } from '../app/extension';
import { LifecyclePhase } from '@alilc/lowcode-renderer-core';
import { createRouter, type RouterOptions } from '@alilc/lowcode-renderer-router';
import { createRouterView } from './routerView';
import { RouteOutlet } from './route';
const defaultRouterOptions: RouterOptions = {
historyMode: 'browser',
baseName: '/',
routes: [],
};
export const routerPlugin = defineRendererPlugin({
name: 'rendererRouter',
async setup(context) {
const { whenLifeCylePhaseChange, schema, boosts } = context;
let routerConfig = defaultRouterOptions;
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);
}
const router = createRouter(routerConfig);
boosts.codeRuntime.getScope().set('router', router);
const RouterView = createRouterView(router);
boosts.addAppWrapper(RouterView);
boosts.setOutlet(RouteOutlet);
whenLifeCylePhaseChange(LifecyclePhase.Ready).then(() => {
return router.isReady();
});
},
});

View File

@ -0,0 +1,41 @@
import { useMemo } from 'react';
import { useRenderContext } from '../app/context';
import { OutletProps } from '../app/extension';
import { useRouteLocation } from './context';
import { createComponentBySchema } from '../runtime/component';
export function RouteOutlet(props: OutletProps) {
const context = useRenderContext();
const location = useRouteLocation();
const { schema, packageManager } = context;
const pageConfig = useMemo(() => {
const pages = schema.get('pages') ?? [];
const matched = location.matched[location.matched.length - 1];
if (matched) {
const page = pages.find((item) => matched.page === item.mappingId);
return page;
}
return undefined;
}, [location]);
if (pageConfig?.type === 'lowCode') {
// 在页面渲染时重新获取 componentsMap
// 因为 componentsMap 可能在路由跳转之前懒加载新的页面 schema
const componentsMap = schema.get('componentsMap');
packageManager.resolveComponentMaps(componentsMap);
const LowCodeComponent = createComponentBySchema(pageConfig.mappingId, {
displayName: pageConfig.id,
modelOptions: {
metadata: pageConfig,
},
});
return <LowCodeComponent {...props} />;
}
return null;
}

View File

@ -0,0 +1,20 @@
import { type Router } from '@alilc/lowcode-renderer-router';
import { useState, useEffect, type ReactNode } from 'react';
import { RouterContext, RouteLocationContext } from './context';
export const createRouterView = (router: Router) => {
return function RouterView({ children }: { children?: ReactNode }) {
const [location, setCurrentLocation] = useState(router.getCurrentLocation());
useEffect(() => {
const remove = router.afterRouteChange((to) => setCurrentLocation(to));
return () => remove();
}, []);
return (
<RouterContext.Provider value={router}>
<RouteLocationContext.Provider value={location}>{children}</RouteLocationContext.Provider>
</RouterContext.Provider>
);
};
};

View File

@ -0,0 +1,164 @@
import { invariant, isLowCodeComponentPackage, type Spec } from '@alilc/lowcode-shared';
import { forwardRef, useRef, useEffect } from 'react';
import { isValidElementType } from 'react-is';
import { useRenderContext } from '../app/context';
import { reactiveStateFactory } from './reactiveState';
import { dataSourceCreator } from './dataSource';
import { type ReactComponent, type ReactWidget, createElementByWidget } from './render';
import { ModelContextProvider } from './context';
import { appendExternalStyle } from '../utils/element';
import type {
RenderContext,
IComponentTreeModel,
ComponentTreeModelOptions,
} from '@alilc/lowcode-renderer-core';
import type { ReactInstance, CSSProperties, ForwardedRef } from 'react';
export interface ComponentOptions {
displayName?: string;
modelOptions?: ComponentTreeModelOptions;
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>();
export function getComponentByName(
name: string,
{ packageManager, boostsManager }: RenderContext,
): ReactComponent {
const componentsRecord = packageManager.getComponentsNameRecord<ReactComponent>();
// read cache first
const result = lowCodeComponentsCache.get(name) || componentsRecord[name];
if (isLowCodeComponentPackage(result)) {
const { schema, ...metadata } = result;
const { componentsMap, componentsTree, utils, i18n } = schema;
if (componentsMap.length > 0) {
packageManager.resolveComponentMaps(componentsMap);
}
const boosts = boostsManager.toExpose();
utils?.forEach((util) => boosts.util.add(util));
if (i18n) {
Object.keys(i18n).forEach((locale) => {
boosts.intl.addTranslations(locale, i18n[locale]);
});
}
const lowCodeComponent = createComponentBySchema(componentsTree[0], {
displayName: name,
modelOptions: {
id: metadata.id,
metadata,
},
});
lowCodeComponentsCache.set(name, lowCodeComponent);
return lowCodeComponent;
}
invariant(isValidElementType(result), `${name} must be a React Component`);
return result;
}
export function createComponentBySchema(
schema: string | Spec.ComponentTreeRoot,
{ displayName = '__LowCodeComponent__', modelOptions }: 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, modelOptions);
} else {
modelRef.current = componentTreeModel.create(schema, modelOptions);
}
}
const model = modelRef.current!;
console.log('%c [ model ]-103', 'font-size:13px; background:pink; color:#bf2c9f;', model);
const isConstructed = useRef(false);
const isMounted = useRef(false);
if (!isConstructed.current) {
model.initialize({
defaultProps: props,
stateCreator: reactiveStateFactory,
dataSourceCreator,
});
model.triggerLifeCycle('constructor');
isConstructed.current = true;
const cssText = model.getCssText();
if (cssText) {
appendExternalStyle(cssText, { id: model.id });
}
}
useEffect(() => {
const scopeValue = model.codeScope.value;
// init dataSource
scopeValue.reloadDataSource?.();
// 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');
// unwatch();
isMounted.current = false;
};
}, []);
return (
<ModelContextProvider value={model}>
<div id={props.id} className={props.className} style={props.style} ref={ref}>
{model.widgets.map((w) => createElementByWidget(w, model.codeScope))}
</div>
</ModelContextProvider>
);
});
LowCodeComponent.displayName = displayName;
return LowCodeComponent;
}

View File

@ -0,0 +1,11 @@
import { IComponentTreeModel } from '@alilc/lowcode-renderer-core';
import { createContext, useContext, type ReactInstance } from 'react';
import { type ReactComponent } from './render';
export const ModelContext = createContext<IComponentTreeModel<ReactComponent, ReactInstance>>(
undefined!,
);
export const useModel = () => useContext(ModelContext);
export const ModelContextProvider = ModelContext.Provider;

View File

@ -0,0 +1,137 @@
import { mapValue } from '@alilc/lowcode-renderer-core';
import {
type AnyFunction,
type PlainObject,
isJSExpression,
computed,
watch,
invariant,
} from '@alilc/lowcode-shared';
import { useRef } from 'react';
import { produce } from 'immer';
import { useSyncExternalStore } from 'use-sync-external-store/shim';
interface ReactiveOptions {
target: PlainObject;
getter?: (obj: any) => any;
valueGetter?: (expr: any) => any;
filter?: (obj: any) => boolean;
}
export interface ReactiveStore<Snapshot = PlainObject> {
value: Snapshot | null;
onStateChange: AnyFunction | null;
subscribe: (onStoreChange: () => void) => () => void;
getSnapshot: () => Snapshot | null;
}
function createReactiveStore<Snapshot = PlainObject>(
options: ReactiveOptions,
): ReactiveStore<Snapshot> {
const { target, getter, filter = isJSExpression, valueGetter } = options;
invariant(
getter || valueGetter,
'Either the processor option or the valueGetter option must be provided.',
);
let isFlushing = false;
let isFlushPending = false;
const cleanups: Array<() => void> = [];
const waitPathToSetValueMap = new Map();
const store: ReactiveStore<Snapshot> = {
value: null,
onStateChange: null,
subscribe(callback: () => void) {
store.onStateChange = callback;
return () => {
store.onStateChange = null;
cleanups.forEach((c) => c());
cleanups.length = 0;
};
},
getSnapshot() {
return store.value;
},
};
if (getter) {
const computedValue = computed<any>(() => getter(target));
cleanups.push(
watch(
computedValue,
(newValue) => {
Promise.resolve().then(() => {
store.value = newValue;
store.onStateChange?.();
});
},
{ immediate: true },
),
);
} else if (valueGetter) {
const initValue = mapValue(target, filter, (node: any, paths) => {
const computedValue = computed(() => valueGetter(node));
const unwatch = watch(computedValue, (newValue) => {
waitPathToSetValueMap.set(paths, newValue);
if (!isFlushPending && !isFlushing) {
isFlushPending = true;
Promise.resolve().then(updateStoreValue);
}
});
cleanups.push(unwatch);
updateStoreValue();
return computedValue.value;
});
store.value = initValue;
}
function updateStoreValue() {
isFlushPending = false;
isFlushing = true;
if (waitPathToSetValueMap.size > 0) {
store.value = produce(store.value, (draft: any) => {
waitPathToSetValueMap.forEach((value, paths) => {
if (paths.length === 1) {
draft[paths[0]] = value;
} else if (paths.length > 1) {
let target = draft;
let i = 0;
for (; i < paths.length - 1; i++) {
target = draft[paths[i]];
}
target[paths[i]] = value;
}
waitPathToSetValueMap.delete(paths);
});
});
}
store.onStateChange?.();
isFlushing = false;
}
return store;
}
export function useReactiveStore(options: ReactiveOptions) {
const storeRef = useRef<ReactiveStore>();
if (!storeRef.current) {
storeRef.current = createReactiveStore(options);
}
const store = storeRef.current;
return useSyncExternalStore(store.subscribe, store.getSnapshot) as any;
}

View File

@ -0,0 +1,2 @@
export * from './component';
export * from './render';

View File

@ -1,412 +0,0 @@
import { processValue, someValue } from '@alilc/lowcode-renderer-core';
import {
isJSExpression,
isJSFunction,
isJSSlot,
invariant,
isLowCodeComponentPackage,
isJSI18nNode,
} from '@alilc/lowcode-shared';
import { forwardRef, useRef, useEffect, createElement, memo } from 'react';
import { appendExternalStyle } from '../../../../playground/renderer/src/plugin/remote/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>();
export function getComponentByName(
name: string,
{ packageManager, boostsManager }: 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 (isLowCodeComponentPackage(result)) {
const { componentsMap, componentsTree, utils, i18n } = result.schema;
if (componentsMap.length > 0) {
packageManager.resolveComponentMaps(componentsMap);
}
const boosts = boostsManager.toExpose();
utils?.forEach((util) => boosts.util.add(util));
if (i18n) {
Object.keys(i18n).forEach((locale) => {
boosts.intl.addTranslations(locale, i18n[locale]);
});
}
const lowCodeComponent = createComponentBySchema(componentsTree[0], {
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;
console.log('%c [ model ]-123', 'font-size:13px; background:pink; color:#bf2c9f;', model);
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 对象转换
let 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;
},
);
finalProps = processValue(
finalProps,
(value) => {
return value.type === 'JSSlot' && !value.value;
},
(node) => {
console.log('%c [ node ]-303', 'font-size:13px; background:pink; color:#bf2c9f;', node);
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

@ -1,7 +1,7 @@
import { signal, type PlainObject, type Spec } from '@alilc/lowcode-shared';
import { isPlainObject } from 'lodash-es';
export function reactiveStateCreator(initState: PlainObject): Spec.InstanceStateApi {
export function reactiveStateFactory(initState: PlainObject): Spec.InstanceStateApi {
const proxyState = signal(initState);
return {
@ -13,10 +13,9 @@ export function reactiveStateCreator(initState: PlainObject): Spec.InstanceState
throw Error('newState mush be a object');
}
proxyState.value = {
...proxyState.value,
...newState,
};
Object.keys(newState as PlainObject).forEach((key) => {
proxyState.value[key] = (newState as PlainObject)[key];
});
},
};
}

View File

@ -0,0 +1,222 @@
import {
type IWidget,
type ICodeScope,
type NormalizedComponentNode,
mapValue,
} from '@alilc/lowcode-renderer-core';
import {
type PlainObject,
isJSExpression,
isJSI18nNode,
isJSFunction,
isJSSlot,
type Spec,
} from '@alilc/lowcode-shared';
import { type ComponentType, type ReactInstance, useMemo, createElement } from 'react';
import { useRenderContext } from '../app/context';
import { useReactiveStore } from './hooks/useReactiveStore';
import { useModel } from './context';
import { getComponentByName } from './component';
export type ReactComponent = ComponentType<any>;
export type ReactWidget = IWidget<ReactComponent, ReactInstance>;
interface WidgetRendererProps {
widget: ReactWidget;
codeScope: ICodeScope;
[key: string]: any;
}
export function createElementByWidget(
widget: IWidget<ReactComponent, ReactInstance>,
codeScope: ICodeScope,
) {
const { key, node } = widget;
if (typeof node === 'string') {
return node;
}
if (isJSExpression(node)) {
return <Text key={key} expr={node} codeScope={codeScope} />;
}
if (isJSI18nNode(node)) {
return <I18nText key={key} i18n={node} codeScope={codeScope} />;
}
const { condition, loop } = widget.node as NormalizedComponentNode;
// condition为 Falsy 的情况下 不渲染
if (!condition) return null;
// loop 为数组且为空的情况下 不渲染
if (Array.isArray(loop) && loop.length === 0) return null;
if (isJSExpression(loop)) {
return <LoopWidgetRenderer key={key} loop={loop} widget={widget} codeScope={codeScope} />;
}
return <WidgetComponent key={key} widget={widget} codeScope={codeScope} />;
}
export function WidgetComponent(props: WidgetRendererProps) {
const { widget, codeScope, ...otherProps } = props;
const componentNode = widget.node as NormalizedComponentNode;
const { ref, ...componentProps } = componentNode.props;
const renderContext = useRenderContext();
const Component = useMemo(
() => getComponentByName(componentNode.componentName, renderContext),
[widget],
);
const state = useReactiveStore({
target: {
condition: componentNode.condition,
props: preprocessProps(componentProps, widget, codeScope),
},
valueGetter(expr) {
return widget.model.codeRuntime.resolve(expr, { scope: codeScope });
},
});
const attachRef = (ins: ReactInstance | null) => {
if (ins) {
if (ref) widget.model.setComponentRef(ref as string, ins);
} else {
if (ref) widget.model.removeComponentRef(ref);
}
};
if (!state.condition) {
return null;
}
return createElement(
Component,
{
...otherProps,
...state.props,
key: widget.key,
ref: attachRef,
},
widget.children?.map((item) => createElementByWidget(item, codeScope)) ?? [],
);
}
function preprocessProps(props: PlainObject, widget: ReactWidget, codeScope: ICodeScope) {
// 先将 jsslot, jsFunction 对象转换
const finalProps = mapValue(
props,
(node) => isJSFunction(node) || isJSSlot(node),
(node: Spec.JSSlot | Spec.JSFunction) => {
if (isJSSlot(node)) {
const slot = node as Spec.JSSlot;
if (slot.value) {
const widgets = widget.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)));
};
} else {
return widgets.map((n) => createElementByWidget(n, codeScope));
}
}
} else if (isJSFunction(node)) {
return widget.model.codeRuntime.resolve(node, { scope: codeScope });
}
return null;
},
);
if (process.env.NODE_ENV === 'development') {
// development 模式下 把 widget 的内容作为 prop ,便于排查问题
finalProps.widget = widget;
}
return finalProps;
}
function Text(props: { expr: Spec.JSExpression; codeScope: ICodeScope }) {
const model = useModel();
const text: string = useReactiveStore({
target: props.expr,
getter: (obj) => {
return model.codeRuntime.resolve(obj, { scope: props.codeScope });
},
});
return text;
}
Text.displayName = 'Text';
function I18nText(props: { i18n: Spec.JSI18n; codeScope: ICodeScope }) {
const model = useModel();
const text: string = useReactiveStore({
target: props.i18n,
getter: (obj) => {
return model.codeRuntime.resolve(obj, { scope: props.codeScope });
},
});
return text;
}
I18nText.displayName = 'I18nText';
function LoopWidgetRenderer({
loop,
widget,
codeScope,
...otherProps
}: {
loop: Spec.JSExpression;
widget: ReactWidget;
codeScope: ICodeScope;
[key: string]: any;
}) {
const { condition, loopArgs } = widget.node as NormalizedComponentNode;
const state = useReactiveStore({
target: {
loop,
condition,
},
valueGetter(expr) {
return widget.model.codeRuntime.resolve(expr, { scope: codeScope });
},
});
if (state.condition && Array.isArray(state.loop) && state.loop.length > 0) {
return state.loop.map((item: any, idx: number) => {
const childScope = codeScope.createChild({
[loopArgs[0]]: item,
[loopArgs[1]]: idx,
});
return (
<WidgetComponent
{...otherProps}
key={`loop-${widget.key}-${idx}`}
widget={widget}
codeScope={childScope}
/>
);
});
}
return null;
}

View File

@ -0,0 +1,106 @@
export const addLeadingSlash = (path: string): string => {
return path.charAt(0) === '/' ? path : `/${path}`;
};
export interface ExternalElementOptions {
id?: string;
root?: HTMLElement;
}
export async function appendExternalScript(
url: string,
{ id, root = document.body }: ExternalElementOptions = {},
): Promise<HTMLElement> {
if (id) {
const el = document.getElementById(id);
if (el) return el;
}
return new Promise((resolve, reject) => {
const scriptElement = document.createElement('script');
// scriptElement.type = moduleType === 'module' ? 'module' : 'text/javascript';
/**
* `async=false` is required to make sure all js resources execute sequentially.
*/
scriptElement.async = false;
scriptElement.crossOrigin = 'anonymous';
scriptElement.src = url;
if (id) scriptElement.id = id;
scriptElement.addEventListener(
'load',
() => {
resolve(scriptElement);
},
false,
);
scriptElement.addEventListener('error', (error) => {
if (root.contains(scriptElement)) {
root.removeChild(scriptElement);
}
reject(error);
});
root.appendChild(scriptElement);
});
}
export async function appendExternalCss(
url: string,
{ id, root = document.head }: ExternalElementOptions = {},
): Promise<HTMLElement> {
if (id) {
const el = document.getElementById(id);
if (el) return el;
}
return new Promise((resolve, reject) => {
const el: HTMLLinkElement = document.createElement('link');
el.rel = 'stylesheet';
el.href = url;
if (id) el.id = id;
el.addEventListener(
'load',
() => {
resolve(el);
},
false,
);
el.addEventListener('error', (error) => {
reject(error);
});
root.appendChild(el);
});
}
export async function appendExternalStyle(
cssText: string,
{ id, root = document.head }: ExternalElementOptions = {},
): Promise<HTMLElement> {
if (id) {
const el = document.getElementById(id);
if (el) return el;
}
return new Promise((resolve, reject) => {
const el: HTMLStyleElement = document.createElement('style');
el.innerText = cssText;
if (id) el.id = id;
el.addEventListener(
'load',
() => {
resolve(el);
},
false,
);
el.addEventListener('error', (error) => {
reject(error);
});
root.appendChild(el);
});
}

View File

@ -1,15 +0,0 @@
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 ?? {},
condition: node.condition || node.condition === false ? node.condition : true,
};
}

View File

@ -1,132 +0,0 @@
import { processValue } from '@alilc/lowcode-renderer-core';
import {
type AnyFunction,
type PlainObject,
isJSExpression,
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';
export interface ReactiveStore<Snapshot = PlainObject> {
value: Snapshot;
onStateChange: AnyFunction | null;
subscribe: (onStoreChange: () => void) => () => void;
getSnapshot: () => Snapshot;
}
function createReactiveStore<Snapshot = PlainObject>(
target: Record<string, any>,
predicate: (obj: any) => boolean,
valueGetter: (expr: any) => any,
): ReactiveStore<Snapshot> {
let isFlushing = false;
let isFlushPending = false;
const cleanups: Array<() => void> = [];
const waitPathToSetValueMap = new Map();
const initValue = processValue(target, predicate, (node: any, paths) => {
const computedValue = computed(() => valueGetter(node));
const unwatch = watch(computedValue, (newValue) => {
waitPathToSetValueMap.set(paths, newValue);
if (!isFlushPending && !isFlushing) {
isFlushPending = true;
Promise.resolve().then(genValue);
}
});
cleanups.push(unwatch);
return computedValue.value;
});
const genValue = () => {
isFlushPending = false;
isFlushing = true;
if (waitPathToSetValueMap.size > 0) {
store.value = produce(store.value, (draft: any) => {
waitPathToSetValueMap.forEach((value, paths) => {
if (paths.length === 1) {
draft[paths[0]] = value;
} else if (paths.length > 1) {
let target = draft;
let i = 0;
for (; i < paths.length - 1; i++) {
target = draft[paths[i]];
}
target[paths[i]] = value;
}
});
});
}
waitPathToSetValueMap.clear();
store.onStateChange?.();
isFlushing = false;
};
const store: ReactiveStore<Snapshot> = {
value: initValue,
onStateChange: null,
subscribe(callback: () => void) {
store.onStateChange = callback;
return () => {
store.onStateChange = null;
cleanups.forEach((c) => c());
cleanups.length = 0;
};
},
getSnapshot() {
return store.value;
},
};
return store;
}
interface ReactiveOptions {
target: PlainObject;
valueGetter: (expr: any) => any;
predicate?: (obj: any) => boolean;
forwardRef?: boolean;
}
export function reactive<TProps extends PlainObject = PlainObject>(
WrappedComponent: ComponentType<TProps>,
{
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);
return createElement(WrappedComponent, {
...props,
...actualProps,
ref,
});
}
const componentName = WrappedComponent.displayName || WrappedComponent.name || 'Component';
const displayName = `Reactive(${componentName})`;
const _Reactived = forwardRefOption ? forwardRef(WrapperComponent) : WrapperComponent;
const Reactived = memo(_Reactived) as unknown as ComponentType<PropsWithChildren<TProps>>;
Reactived.displayName = WrappedComponent.displayName = displayName;
return hoistNonReactStatics(Reactived, WrappedComponent);
}

View File

@ -1,50 +0,0 @@
import { describe, it, expect, vi } from 'vitest';
import { createAppFunction } from '../../src/api/app';
import { definePlugin } from '../../src/plugin';
describe('createAppFunction', () => {
it('should require a function argument that returns an render object.', () => {
expect(() => createAppFunction(undefined as any)).rejects.toThrowError();
});
it('should return a function', () => {
const createApp = createAppFunction(async () => {
return {
appBase: {
mount(el) {},
unmount() {},
},
};
});
expect({ createApp }).toEqual({ createApp: expect.any(Function) });
});
it('should construct app object', () => {
expect('').toBe('');
});
it('should plugin inited when app created', async () => {
const plugin = definePlugin({
name: 'test',
setup() {},
});
const spy = vi.spyOn(plugin, 'setup');
const createApp = createAppFunction(async () => {
return {
appBase: {
mount(el) {},
unmount() {},
},
};
});
await createApp({
schema: {},
plugins: [plugin],
});
expect(spy).toHaveBeenCalled();
});
});

View File

@ -1,46 +1,55 @@
import { describe, it, expect, beforeAll } from 'vitest';
import { ICodeScope, CodeScope } from '../../../src/parts/code-runtime';
import { describe, it, expect } from 'vitest';
import { CodeScope } from '../../../src/parts/code-runtime';
describe('codeScope', () => {
let scope: ICodeScope;
beforeAll(() => {
scope = new CodeScope({});
describe('CodeScope', () => {
it('should return initial values', () => {
const initValue = { a: 1, b: 2 };
const scope = new CodeScope(initValue);
expect(scope.value.a).toBe(1);
expect(scope.value.b).toBe(2);
});
it('should inject a new value', () => {
scope.inject('username', 'Alice');
expect(scope.value).toEqual({ username: 'Alice' });
it('inject should add new values', () => {
const scope = new CodeScope({});
scope.set('c', 3);
expect(scope.value.c).toBe(3);
});
it('should not overwrite an existing value without force', () => {
scope.inject('username', 'Bob');
expect(scope.value).toEqual({ username: 'Alice' });
it('inject should not overwrite existing values without force', () => {
const initValue = { a: 1 };
const scope = new CodeScope(initValue);
scope.set('a', 2);
expect(scope.value.a).toBe(1);
scope.set('a', 3, true);
expect(scope.value.a).toBe(3);
});
it('should overwrite an existing value with force', () => {
scope.inject('username', 'Bob', true);
expect(scope.value).toEqual({ username: 'Bob' });
it('setValue should merge values by default', () => {
const initValue = { a: 1 };
const scope = new CodeScope(initValue);
scope.setValue({ b: 2 });
expect(scope.value.a).toBe(1);
expect(scope.value.b).toBe(2);
});
it('should set new value without replacing existing values', () => {
scope.setValue({ age: 25 });
expect(scope.value).toEqual({ username: 'Bob', age: 25 });
it('setValue should replace values when replace is true', () => {
const initValue = { a: 1 };
const scope = new CodeScope(initValue);
scope.setValue({ b: 2 }, true);
expect(scope.value.a).toBeUndefined();
expect(scope.value.b).toBe(2);
});
it('should set new value and replace all existing values', () => {
scope.setValue({ loggedIn: true }, true);
expect(scope.value).toEqual({ loggedIn: true });
});
it('should create child scopes and respect scope hierarchy', () => {
const parentValue = { a: 1, b: 2 };
const childValue = { b: 3, c: 4 };
it('should create a child scope with initial values', () => {
const childScope = scope.createChild({ sessionId: 'abc123' });
expect(childScope.value).toEqual({ loggedIn: true, sessionId: 'abc123' });
});
const parentScope = new CodeScope(parentValue);
const childScope = parentScope.createChild(childValue);
it('should set new values in the child scope without affecting the parent scope', () => {
const childScope = scope.createChild({ theme: 'dark' });
expect(childScope.value).toEqual({ loggedIn: true, sessionId: 'abc123', theme: 'dark' });
expect(scope.value).toEqual({ loggedIn: true });
expect(childScope.value.a).toBe(1); // Inherits from parent scope
expect(childScope.value.b).toBe(3); // Overridden by child scope
expect(childScope.value.c).toBe(4); // Unique to child scope
expect(parentScope.value.c).toBeUndefined(); // Parent scope should not have child's properties
});
});

View File

@ -1,169 +0,0 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { createEvent, createHookStore, type HookStore } from '../../src/utils/hook';
describe('event', () => {
it('event\'s listener ops', () => {
const event = createEvent();
const fn = () => {};
event.add(fn);
expect(event.list().includes(fn)).toBeTruthy();
event.remove(fn);
expect(event.list().includes(fn)).toBeFalsy();
event.add(fn);
expect(event.list().includes(fn)).toBeTruthy();
event.clear();
expect(event.list().includes(fn)).toBeFalsy();
});
});
describe('hooks', () => {
let hookStore: HookStore;
beforeEach(() => {
hookStore = createHookStore();
});
it('should register hook successfully', () => {
const fn = () => {};
hookStore.hook('test', fn);
expect(hookStore.getHooks('test')).toContain(fn);
});
it('should ignore empty hook', () => {
hookStore.hook('', () => {});
hookStore.hook(undefined as any, () => {});
expect(hookStore.getHooks('')).toBeUndefined();
expect(hookStore.getHooks(undefined as any)).toBeUndefined();
});
it('should ignore not function hook', () => {
hookStore.hook('test', 1 as any);
hookStore.hook('test', undefined as any);
expect(hookStore.getHooks('test')).toBeUndefined();
});
it('should call registered hook', () => {
const spy = vi.fn();
hookStore.hook('test', spy);
hookStore.call('test');
expect(spy).toHaveBeenCalled();
});
it('callAsync: should sequential call registered async hook', async () => {
let count = 0;
const counts: number[] = [];
const fn = async () => {
counts.push(count++);
};
hookStore.hook('test', fn);
hookStore.hook('test', fn);
await hookStore.callAsync('test');
expect(counts).toEqual([0, 1]);
});
it('callParallel: should parallel call registered async hook', async () => {
let count = 0;
const sleep = (delay: number) => {
return new Promise((resolve) => {
setTimeout(resolve, delay);
});
};
hookStore.hook('test', () => {
count++;
});
hookStore.hook('test', async () => {
await sleep(500);
count++;
});
hookStore.hook('test', async () => {
await sleep(1000);
expect(count).toBe(2);
});
await hookStore.callParallel('test');
});
it('should throw hook error', async () => {
const error = new Error('Hook Error');
hookStore.hook('test', () => {
throw error;
});
expect(() => hookStore.call('test')).toThrow(error);
});
it('should return a self-removal function', async () => {
const spy = vi.fn();
const remove = hookStore.hook('test', spy);
hookStore.call('test');
expect(spy).toBeCalledTimes(1);
remove();
hookStore.call('test');
expect(spy).toBeCalledTimes(1);
});
it('should clear removed hooks', () => {
const result: number[] = [];
const fn1 = () => result.push(1);
const fn2 = () => result.push(2);
hookStore.hook('test', fn1);
hookStore.hook('test', fn2);
hookStore.call('test');
expect(result).toHaveLength(2);
expect(result).toEqual([1, 2]);
hookStore.remove('test', fn1);
hookStore.call('test');
expect(result).toHaveLength(3);
expect(result).toEqual([1, 2, 2]);
hookStore.remove('test');
hookStore.call('test');
expect(result).toHaveLength(3);
expect(result).toEqual([1, 2, 2]);
});
it('should clear ops works', () => {
hookStore.hook('test1', () => {});
hookStore.hook('test2', () => {});
expect(hookStore.getHooks('test1')).toHaveLength(1);
expect(hookStore.getHooks('test2')).toHaveLength(1);
hookStore.clear('test1');
expect(hookStore.getHooks('test1')).toBeUndefined();
expect(hookStore.getHooks('test2')).toHaveLength(1);
hookStore.clear();
expect(hookStore.getHooks('test1')).toBeUndefined();
expect(hookStore.getHooks('test2')).toBeUndefined();
});
});

View File

@ -0,0 +1,79 @@
import { describe, it, expect } from 'vitest';
import { walk } from '../../src/utils/node';
describe('sync walker', () => {
it('down', () => {
const ast = {
hasMask: true,
visible: false,
footer: false,
cancelProps: {
text: false,
type: 'normal',
},
confirmState: '确定',
confirmStyle: 'primary',
footerActions: 'cancel,ok',
className: 'dialog_lkz6xvcv',
confirmText: {
en_US: 'Confirm',
use: '',
zh_CN: '确定',
type: 'JSExpression',
value: `({"en_US":"OK","key":"i18n-xgse6q6a","type":"i18n","zh_CN":"确定"})[this.utils.getLocale()]`,
key: 'i18n-xgse6q6a',
extType: 'i18n',
},
autoFocus: true,
title: {
mock: {
en_US: 'Dialog Title',
use: '',
zh_CN: 'Dialog标题',
type: 'JSExpression',
value: `({"en_US":"Dialog Title","key":"i18n-0m3kaceq","type":"i18n","zh_CN":"Dialog标题"})[this.utils.getLocale()]`,
key: 'i18n-0m3kaceq',
extType: 'i18n',
},
type: 'JSExpression',
value: 'state.dialogInfo && state.dialogInfo.title',
},
closeable: [
'esc',
'mask',
{
type: 'JSExpression',
value: '1',
},
],
cancelText: {
en_US: 'Cancel',
use: '',
zh_CN: '取消',
type: 'JSExpression',
value: `({"en_US":"Cancel","key":"i18n-wtq23279","type":"i18n","zh_CN":"取消"})[this.utils.getLocale()]`,
key: 'i18n-wtq23279',
extType: 'i18n',
},
width: '800px',
footerAlign: 'right',
popupOutDialog: true,
__style__: ':root {}',
fieldId: 'dialog_case',
height: '500px',
};
const newAst = walk(ast, {
enter(node, parent, key, index) {
if (node.type === 'JSExpression') {
this.replace({
type: '1',
value: '2',
});
}
},
});
console.log(newAst, Object.is(newAst, ast));
});
});

View File

@ -0,0 +1,98 @@
import { describe, it, expect } from 'vitest';
import { someValue, mapValue } from '../../src/utils/value';
describe('someValue', () => {
it('should return false for non-plain objects or empty objects', () => {
expect(someValue([], (val) => val > 10)).toBe(false);
expect(someValue({}, (val) => val > 10)).toBe(false);
expect(someValue(null, (val) => val > 10)).toBe(false);
expect(someValue(undefined, (val) => val > 10)).toBe(false);
});
it('should return true if the predicate matches object value', () => {
const obj = { a: 5, b: { c: 15 } };
expect(someValue(obj, (val) => val.c > 10)).toBe(true);
});
it('should return true if the predicate matches nested array element', () => {
const obj = { a: [1, 2, { d: 14 }] };
expect(someValue(obj, (val) => val.d > 10)).toBe(true);
});
it('should return false if the predicate does not match any value', () => {
const obj = { a: 5, b: { c: 9 } };
expect(someValue(obj, (val) => val.c > 10)).toBe(false);
});
it('should handle primitives in object values', () => {
const obj = { a: 1, b: 'string', c: true };
const strPredicate = (val: any) => typeof val.b === 'string';
expect(someValue(obj, strPredicate)).toBe(true);
const boolPredicate = (val: any) => typeof val.c === 'boolean';
expect(someValue(obj, boolPredicate)).toBe(true);
});
it('should handle deep nesting with mixed arrays and objects', () => {
const complexObj = { a: { b: [1, 2, { c: 3 }, [{ d: 4 }]] } };
expect(someValue(complexObj, (val) => val.d === 4)).toBe(true);
});
it('should handle functions and undefined values', () => {
const objWithFunc = { a: () => {}, b: undefined };
const funcPredicate = (val: any) => typeof val.a === 'function';
expect(someValue(objWithFunc, funcPredicate)).toBe(true);
const undefinedPredicate = (val: any) => val.b === undefined;
expect(someValue(objWithFunc, undefinedPredicate)).toBe(true);
});
});
describe('mapValue', () => {
const predicate = (obj: any) => obj && obj.process;
const processor = (obj: any, paths: any[]) => ({ ...obj, processed: true, paths });
it('should not process object if it does not match the predicate', () => {
const obj = { a: 3, b: { c: 4 } };
expect(mapValue(obj, predicate, processor)).toEqual(obj);
});
it('should process object that matches the predicate', () => {
const obj = { a: { process: true } };
expect(mapValue(obj, predicate, processor)).toEqual({
a: { process: true, processed: true, paths: ['a'] },
});
});
it('should handle nested objects and arrays with various types of predicates', () => {
const complexObj = {
a: { key: 'value' },
b: [{ key: 'value' }, undefined, null, 0, false],
c: () => {},
};
const truthyPredicate = (obj: any) => 'key' in obj && obj.key === 'value';
const falsePredicate = (obj: any) => false;
expect(mapValue(complexObj, truthyPredicate, processor)).toEqual({
a: { key: 'value', processed: true, paths: ['a'] },
b: [{ key: 'value', processed: true, paths: ['b', 0] }, undefined, null, 0, false],
c: complexObj.c,
});
expect(mapValue(complexObj, falsePredicate, processor)).toEqual(complexObj);
});
it('should process nested object and arrays that match the predicate', () => {
const nestedObj = {
a: { key: 'value', nested: { key: 'value' } },
};
const predicate = (obj: any) => 'key' in obj;
const result = mapValue(nestedObj, predicate, processor);
expect(result).toEqual({
a: { key: 'value', processed: true, paths: ['a'], nested: { key: 'value' } },
});
expect(result.a.nested).not.toHaveProperty('processed');
});
});

View File

@ -1,27 +0,0 @@
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) => {
await rendererMain.initialize(options);
return rendererMain.startup<Render>(renderAdapter);
};
}

View File

@ -1,20 +1,23 @@
/* --------------- api -------------------- */
export * from './apiCreate';
export { definePackageLoader } from './parts/package';
export { Widget } from './parts/widget';
export { createRenderer } from './main';
export { definePackageLoader } from './services/package';
export { LifecyclePhase } from './services/lifeCycleService';
export { Widget } from './services/widget';
export * from './utils/node';
export * from './utils/value';
/* --------------- types ---------------- */
export type * from './types';
export type {
Plugin,
IRender,
IRenderObject,
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';
} from './services/extension';
export type * from './services/code-runtime';
export type * from './services/model';
export type * from './services/package';
export type * from './services/schema';
export type * from './services/widget';
export type * from './services/extension';

View File

@ -1,91 +1,111 @@
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 { Injectable, invariant, InstantiationService } from '@alilc/lowcode-shared';
import { ICodeRuntimeService } from './services/code-runtime';
import {
IBoostsService,
IExtensionHostService,
type RenderAdapter,
type IRenderObject,
} from './services/extension';
import { IPackageManagementService } from './services/package';
import { ISchemaService } from './services/schema';
import { ILifeCycleService, LifecyclePhase } from './services/lifeCycleService';
import { IComponentTreeModelService } from './services/model';
import type { AppOptions, RendererApplication } from './types';
@Injectable()
export class RendererMain {
export class RendererMain<RenderObject> {
private mode: 'development' | 'production' = 'production';
private initOptions: AppOptions;
private renderObject: RenderObject;
private adapter: RenderAdapter<RenderObject>;
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,
) {}
@IComponentTreeModelService private componentTreeModelService: IComponentTreeModelService,
@IBoostsService private boostsService: IBoostsService,
@ILifeCycleService private lifeCycleService: ILifeCycleService,
) {
this.lifeCycleService.when(LifecyclePhase.OptionsResolved).finally(async () => {
const renderContext = {
schema: this.schemaService,
packageManager: this.packageManagementService,
boostsManager: this.boostsService,
componentTreeModel: this.componentTreeModelService,
lifeCycle: this.lifeCycleService,
};
async initialize(options: AppOptions) {
const { schema, mode } = options;
this.renderObject = await this.adapter(renderContext);
await this.packageManagementService.loadPackages(this.initOptions.packages ?? []);
this.lifeCycleService.phase = LifecyclePhase.Ready;
});
}
async main(options: AppOptions, adapter: RenderAdapter<RenderObject>) {
const { schema, mode, plugins = [] } = options;
if (mode) this.mode = mode;
this.initOptions = { ...options };
this.adapter = adapter;
// valid schema
this.schemaService.initialize(schema);
this.codeRuntimeService.initialize(options);
this.codeRuntimeService.initialize(options.codeRuntime ?? {});
// init intl
const finalLocale = options.locale ?? navigator.language;
const i18nTranslations = this.schemaService.get('i18n') ?? {};
this.extensionHostService.registerPlugin(plugins);
this.runtimeIntlService.initialize(finalLocale, i18nTranslations);
this.lifeCycleService.phase = LifecyclePhase.OptionsResolved;
}
async startup<Render>(adapter: RenderAdapter<Render>): Promise<RendererApplication<Render>> {
const render = await this.extensionHostService.runRender<Render>(adapter);
async getApp(): Promise<RendererApplication<RenderObject>> {
await this.lifeCycleService.when(LifecyclePhase.Ready);
// construct application
const app = Object.freeze<RendererApplication<Render>>({
return Object.freeze<RendererApplication<RenderObject>>({
// develop use
__options: this.initOptions,
mode: this.mode,
schema: this.schemaService,
packageManager: this.packageManagementService,
...render,
...this.renderObject,
use: (plugin) => {
return this.extensionHostService.registerPlugin(plugin);
this.extensionHostService.registerPlugin(plugin);
return this.extensionHostService.doSetupPlugin(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 constants = this.schemaService.get('constants') ?? {};
const globalScope = this.codeRuntimeService.getScope();
globalScope.setValue({
constants,
utils: this.runtimeUtilService.toExpose(),
...this.runtimeIntlService.toExpose(),
});
}
}
/**
* createRenderer
* @param schema
* @param options
* @returns
*/
export function createRenderer<RenderObject = IRenderObject>(
renderAdapter: RenderAdapter<RenderObject>,
): (options: AppOptions) => Promise<RendererApplication<RenderObject>> {
invariant(typeof renderAdapter === 'function', 'The first parameter must be a function.');
const instantiationService = new InstantiationService({ defaultScope: 'Singleton' });
instantiationService.bootstrapModules();
const rendererMain = instantiationService.createInstance(
RendererMain,
) as RendererMain<RenderObject>;
return async (options) => {
await rendererMain.main(options, renderAdapter);
return rendererMain.getApp();
};
}

View File

@ -1,107 +0,0 @@
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';
type BeforeResolveCb = (
code: Spec.JSExpression | Spec.JSFunction,
) => Spec.JSExpression | Spec.JSFunction;
export interface ICodeRuntimeService {
initialize({ evalCodeFunction }: CodeRuntimeInitializeOptions): void;
getScope(): ICodeScope;
run<R = unknown>(code: string, scope?: ICodeScope): R | undefined;
resolve(value: PlainObject, scope?: ICodeScope): any;
beforeResolve(fn: BeforeResolveCb): EventDisposable;
createChildScope(value: PlainObject): ICodeScope;
}
export const ICodeRuntimeService = createDecorator<ICodeRuntimeService>('codeRuntimeService');
export type EvalCodeFunction = (code: string, scope: any) => any;
export interface CodeRuntimeInitializeOptions {
evalCodeFunction?: EvalCodeFunction;
}
@Provide(ICodeRuntimeService)
export class CodeRuntimeService implements ICodeRuntimeService {
private codeScope: ICodeScope = new CodeScope({});
private callbacks = createCallback<BeforeResolveCb>();
private evalCodeFunction: EvalCodeFunction = evaluate;
initialize({ evalCodeFunction }: CodeRuntimeInitializeOptions) {
if (evalCodeFunction) this.evalCodeFunction = evalCodeFunction;
}
getScope() {
return this.codeScope;
}
run<R = unknown>(code: string, scope: ICodeScope = this.codeScope): R | undefined {
if (!code) return undefined;
try {
let result = this.evalCodeFunction(code, 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 cbs = this.callbacks.list();
const finalNode = cbs.reduce((node, cb) => cb(node), node);
const v = this.run(finalNode.value, scope);
if (typeof v === 'undefined' && finalNode.mock) {
return this.resolve(finalNode.mock, scope);
}
return v;
},
);
}
beforeResolve(fn: BeforeResolveCb): EventDisposable {
return this.callbacks.add(fn);
}
createChildScope(value: PlainObject): ICodeScope {
return this.codeScope.createChild(value);
}
}
function evaluate(code: string, scope: any) {
return new Function('scope', `"use strict";return (function(){return (${code})}).bind(scope)();`)(
scope,
);
}

View File

@ -1,86 +0,0 @@
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 = valueTarget.prev;
}
return Reflect.get(target, p, receiver);
},
has: (target, p) => {
let valueTarget: IScopeNode | undefined = this.__node;
while (valueTarget) {
if (Reflect.has(valueTarget.current, p)) {
return true;
}
valueTarget = valueTarget.prev;
}
return Reflect.has(target, p);
},
});
}
get value() {
return this.proxyValue;
}
inject(name: string, value: any, force = false): void {
if (this.__node.current[name] && !force) {
return;
}
this.__node.current[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

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

View File

@ -1,20 +0,0 @@
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

@ -1,84 +0,0 @@
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

@ -1,68 +0,0 @@
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

@ -0,0 +1,129 @@
import {
type PlainObject,
type Spec,
type EventDisposable,
createDecorator,
Provide,
isJSExpression,
isJSFunction,
} from '@alilc/lowcode-shared';
import { type ICodeScope, CodeScope } from './codeScope';
import { evaluate } from '../../utils/evaluate';
import { isNode } from '../../utils/node';
import { mapValue } from '../../utils/value';
export interface ResolveOptions {
scope?: ICodeScope;
}
export type NodeResolverHandler = (node: Spec.JSNode) => Spec.JSNode | false | undefined;
export interface ICodeRuntimeService {
initialize(options: CodeRuntimeInitializeOptions): void;
getScope(): ICodeScope;
run<R = unknown>(code: string, scope?: ICodeScope): R | undefined;
resolve(value: PlainObject, options?: ResolveOptions): any;
onResolve(handler: NodeResolverHandler): EventDisposable;
createChildScope(value: PlainObject): ICodeScope;
}
export const ICodeRuntimeService = createDecorator<ICodeRuntimeService>('codeRuntimeService');
export interface CodeRuntimeInitializeOptions {
evalCodeFunction?: (code: string, scope: any) => any;
}
@Provide(ICodeRuntimeService)
export class CodeRuntimeService implements ICodeRuntimeService {
private codeScope: ICodeScope = new CodeScope({});
private evalCodeFunction = evaluate;
private onResolveHandlers: NodeResolverHandler[] = [];
initialize(options: CodeRuntimeInitializeOptions) {
if (options.evalCodeFunction) this.evalCodeFunction = options.evalCodeFunction;
}
getScope() {
return this.codeScope;
}
run<R = unknown>(code: string, scope: ICodeScope = this.codeScope): R | undefined {
if (!code) return undefined;
try {
let result = this.evalCodeFunction(code, scope.value);
if (typeof result === 'function') {
result = result.bind(scope.value);
}
return result as R;
} catch (err) {
// todo replace logger
console.error('eval error', code, scope.value, err);
return undefined;
}
}
resolve(data: PlainObject, options: ResolveOptions = {}): any {
const handlers = this.onResolveHandlers;
if (handlers.length > 0) {
data = mapValue(data, isNode, (node: Spec.JSNode) => {
let newNode: Spec.JSNode | false | undefined = node;
for (const handler of handlers) {
newNode = handler(newNode as Spec.JSNode);
if (newNode === false || typeof newNode === 'undefined') {
break;
}
}
return newNode;
});
}
return mapValue(
data,
(data) => {
return isJSExpression(data) || isJSFunction(data);
},
(node: Spec.JSExpression | Spec.JSFunction) => {
return this.resolveExprOrFunction(node, options);
},
);
}
private resolveExprOrFunction(
node: Spec.JSExpression | Spec.JSFunction,
options: ResolveOptions,
) {
const v = this.run(node.value, options.scope || this.codeScope);
if (typeof v === 'undefined' && node.mock) {
return this.resolve(node.mock, options);
}
return v;
}
/**
* handler
*/
onResolve(handler: NodeResolverHandler): EventDisposable {
this.onResolveHandlers.push(handler);
return () => {
this.onResolveHandlers = this.onResolveHandlers.filter((h) => h !== handler);
};
}
createChildScope(value: PlainObject): ICodeScope {
return this.codeScope.createChild(value);
}
}

View File

@ -0,0 +1,99 @@
import { type PlainObject } from '@alilc/lowcode-shared';
import { trustedGlobals } from '../../utils/globals-es2015';
/*
* variables who are impossible to be overwritten need to be escaped from proxy scope for performance reasons
* see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol/unscopables
*/
const unscopables = trustedGlobals.reduce((acc, key) => ({ ...acc, [key]: true }), {
__proto__: null,
});
export interface ICodeScope {
readonly value: PlainObject;
set(name: string, value: any): void;
setValue(value: PlainObject, replace?: boolean): void;
createChild(initValue: PlainObject): ICodeScope;
}
/**
*
*/
interface IScopeNode {
parent?: IScopeNode;
current: PlainObject;
}
export class CodeScope implements ICodeScope {
__node: IScopeNode;
private proxyValue: PlainObject;
constructor(initValue: PlainObject) {
this.__node = {
current: initValue,
};
this.proxyValue = this.createProxy();
}
get value() {
return this.proxyValue;
}
set(name: string, value: any): void {
this.__node.current[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 childScope = new CodeScope(initValue);
childScope.__node.parent = this.__node;
return childScope;
}
private createProxy(): PlainObject {
return new Proxy(Object.create(null) as PlainObject, {
set: (target, p, newValue) => {
this.set(p as string, newValue, true);
return true;
},
get: (_, p) => this.findValue(p) ?? undefined,
has: (_, p) => this.hasProperty(p),
});
}
private findValue(prop: PropertyKey) {
if (prop === Symbol.unscopables) return unscopables;
let node: IScopeNode | undefined = this.__node;
while (node) {
if (Object.hasOwnProperty.call(node.current, prop)) {
return node.current[prop as string];
}
node = node.parent;
}
}
private hasProperty(prop: PropertyKey): boolean {
if (prop in unscopables) return true;
let node: IScopeNode | undefined = this.__node;
while (node) {
if (prop in node.current) {
return true;
}
node = node.parent;
}
return false;
}
}

View File

@ -1,8 +1,8 @@
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';
import { IRuntimeUtilService } from '../runtimeUtilService';
import { IRuntimeIntlService } from '../runtimeIntlService';
export type IBoosts<Extends> = IBoostsApi & Extends;

View File

@ -1,33 +1,23 @@
import {
invariant,
createDecorator,
Provide,
EventEmitter,
KeyValueStore,
} from '@alilc/lowcode-shared';
import { type Plugin } from './plugin';
import { createDecorator, Provide, EventEmitter, KeyValueStore } from '@alilc/lowcode-shared';
import { type Plugin, type PluginContext } 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';
import { IComponentTreeModelService } from '../model';
import { ILifeCycleService, LifecyclePhase } from '../lifeCycleService';
interface IPluginRuntime extends Plugin {
status: 'setup' | 'ready';
}
export interface IExtensionHostService {
initialize(app: RendererApplication): void;
registerPlugin(plugin: Plugin | Plugin[]): void;
/* ========= plugin ============= */
registerPlugin(plugin: Plugin | Plugin[]): Promise<void>;
doSetupPlugin(plugin: Plugin): Promise<void>;
getPlugin(name: string): Plugin | undefined;
/* =========== render =============== */
runRender<Render>(adapter: RenderAdapter<Render>): Promise<Render>;
dispose(): Promise<void>;
}
@ -38,24 +28,37 @@ export const IExtensionHostService =
export class ExtensionHostService implements IExtensionHostService {
private pluginRuntimes: IPluginRuntime[] = [];
private app: RendererApplication;
private eventEmitter: EventEmitter;
private eventEmitter = new EventEmitter();
private globalState = new KeyValueStore();
private pluginSetupContext: PluginContext;
constructor(
@IPackageManagementService private packageManagementService: IPackageManagementService,
@IBoostsService private boostsService: IBoostsService,
@ISchemaService private schemaService: ISchemaService,
@IComponentTreeModelService private componentTreeModelService: IComponentTreeModelService,
) {}
@ILifeCycleService private lifeCycleService: ILifeCycleService,
) {
this.eventEmitter = new EventEmitter('ExtensionHost');
this.pluginSetupContext = {
eventEmitter: this.eventEmitter,
globalState: new KeyValueStore(),
boosts: this.boostsService.toExpose(),
schema: this.schemaService,
packageManager: this.packageManagementService,
initialize(app: RendererApplication) {
this.app = app;
whenLifeCylePhaseChange: (phase) => {
return this.lifeCycleService.when(phase);
},
};
this.lifeCycleService.when(LifecyclePhase.OptionsResolved).then(async () => {
for (const plugin of this.pluginRuntimes) {
await this.doSetupPlugin(plugin);
}
});
}
async registerPlugin(plugins: Plugin | Plugin[]) {
registerPlugin(plugins: Plugin | Plugin[]) {
plugins = Array.isArray(plugins) ? plugins : [plugins];
for (const plugin of plugins) {
@ -64,39 +67,18 @@ export class ExtensionHostService implements IExtensionHostService {
continue;
}
await this.doSetupPlugin(plugin);
this.pluginRuntimes.push({
...plugin,
status: 'ready',
});
}
}
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) {
async doSetupPlugin(plugin: Plugin) {
const pluginRuntime = plugin as IPluginRuntime;
if (!this.pluginRuntimes.some((item) => item.name !== pluginRuntime.name)) {
this.pluginRuntimes.push({
...pluginRuntime,
status: 'ready',
});
return;
}
const isSetup = (name: string) => {
@ -108,11 +90,7 @@ export class ExtensionHostService implements IExtensionHostService {
return;
}
await pluginRuntime.setup(this.app, {
eventEmitter: this.eventEmitter,
globalState: this.globalState,
boosts: this.boostsService.toExpose(),
});
await pluginRuntime.setup(this.pluginSetupContext);
pluginRuntime.status = 'setup';
// 遍历未安装的插件 寻找 dependsOn 的插件已安装完的插件进行安装
@ -122,4 +100,14 @@ export class ExtensionHostService implements IExtensionHostService {
await this.doSetupPlugin(readyPlugin);
}
}
getPlugin(name: string): Plugin | undefined {
return this.pluginRuntimes.find((item) => item.name === name);
}
async dispose(): Promise<void> {
for (const plugin of this.pluginRuntimes) {
await plugin.destory?.();
}
}
}

View File

@ -0,0 +1,27 @@
import { type EventEmitter, type IStore, type PlainObject } from '@alilc/lowcode-shared';
import { type IBoosts } from './boosts';
import { LifecyclePhase } from '../lifeCycleService';
import { type ISchemaService } from '../schema';
import { type IPackageManagementService } from '../package';
export interface PluginContext<BoostsExtends = object> {
eventEmitter: EventEmitter;
globalState: IStore<PlainObject, string>;
boosts: IBoosts<BoostsExtends>;
schema: ISchemaService;
packageManager: IPackageManagementService;
/**
*
*/
whenLifeCylePhaseChange(phase: LifecyclePhase): Promise<void>;
}
export interface Plugin<BoostsExtends = object> {
/**
* name
*/
name: string;
setup(context: PluginContext<BoostsExtends>): void | Promise<void>;
destory?(): void | Promise<void>;
dependsOn?: string[];
}

View File

@ -1,10 +1,11 @@
import { IPackageManagementService } from '../package';
import { IBoostsService } from './boosts';
import { ISchemaService } from '../schema';
import { IComponentTreeModelService } from '../component-tree-model';
import { IComponentTreeModelService } from '../model';
import { ILifeCycleService } from '../lifeCycleService';
export interface IRender {
mount: (el: HTMLElement) => void | Promise<void>;
export interface IRenderObject {
mount: (containerOrId?: string | HTMLElement) => void | Promise<void>;
unmount: () => void | Promise<void>;
}
@ -16,6 +17,8 @@ export interface RenderContext {
readonly boostsManager: IBoostsService;
readonly componentTreeModel: IComponentTreeModelService;
readonly lifeCycle: ILifeCycleService;
}
export interface RenderAdapter<Render> {

View File

@ -0,0 +1,75 @@
import { Provide, createDecorator, Barrier } from '@alilc/lowcode-shared';
export const enum LifecyclePhase {
Starting = 1,
OptionsResolved = 2,
Ready = 3,
BeforeMount = 4,
Mounted = 5,
BeforeUnmount = 6,
}
export interface ILifeCycleService {
/**
* A flag indicating in what phase of the lifecycle we currently are.
*/
phase: LifecyclePhase;
/**
* Returns a promise that resolves when a certain lifecycle phase
* has started.
*/
when(phase: LifecyclePhase): Promise<void>;
}
export const ILifeCycleService = createDecorator<ILifeCycleService>('lifeCycleService');
@Provide(ILifeCycleService)
export class LifeCycleService implements ILifeCycleService {
private readonly phaseWhen = new Map<LifecyclePhase, Barrier>();
private _phase = LifecyclePhase.Starting;
get phase(): LifecyclePhase {
return this._phase;
}
set phase(value: LifecyclePhase) {
if (value < this.phase) {
throw new Error('Lifecycle cannot go backwards');
}
if (this._phase === value) {
return;
}
// this.logService.trace(`lifecycle: phase changed (value: ${value})`);
this._phase = value;
const barrier = this.phaseWhen.get(this._phase);
if (barrier) {
barrier.open();
this.phaseWhen.delete(this._phase);
}
}
async when(phase: LifecyclePhase): Promise<void> {
if (phase <= this._phase) {
return;
}
let barrier = this.phaseWhen.get(phase);
if (!barrier) {
barrier = new Barrier();
this.phaseWhen.set(phase, barrier);
}
await barrier.wait();
}
}

View File

@ -1,16 +1,39 @@
import { type Spec, type PlainObject, isComponentNode, invariant } from '@alilc/lowcode-shared';
import {
type Spec,
type PlainObject,
isComponentNode,
invariant,
uniqueId,
} from '@alilc/lowcode-shared';
import { type ICodeScope, type ICodeRuntimeService } from '../code-runtime';
import { IWidget, Widget } from '../widget';
export interface NormalizedComponentNode extends Spec.ComponentNode {
loopArgs: [string, string];
props: Spec.ComponentNodeProps;
}
export interface InitializeModelOptions {
defaultProps?: PlainObject | undefined;
stateCreator: ModelScopeStateCreator;
dataSourceCreator: ModelScopeDataSourceCreator;
}
/**
*
*/
export interface IComponentTreeModel<Component, ComponentInstance = unknown> {
readonly id: string;
readonly codeScope: ICodeScope;
readonly codeRuntime: ICodeRuntimeService;
readonly widgets: IWidget<Component, ComponentInstance>[];
initialize(options: InitializeModelOptions): void;
/**
* css
*/
@ -37,11 +60,6 @@ export interface IComponentTreeModel<Component, ComponentInstance = unknown> {
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: {
@ -50,44 +68,60 @@ const defaultDataSourceSchema: Spec.ComponentDataSource = {
},
};
export interface ComponentTreeModelOptions {
id?: string;
metadata?: PlainObject;
}
export class ComponentTreeModel<Component, ComponentInstance = unknown>
implements IComponentTreeModel<Component, ComponentInstance>
{
private instanceMap = new Map<string, ComponentInstance[]>();
public id: string;
public codeScope: ICodeScope;
public widgets: IWidget<Component>[] = [];
public metadata: PlainObject = {};
constructor(
public componentsTree: Spec.ComponentTree,
public codeRuntime: ICodeRuntimeService,
options: ComponentTreeModelOptions,
options?: ComponentTreeModelOptions,
) {
invariant(componentsTree, 'componentsTree must to provide', 'ComponentTreeModel');
this.initModelScope(options.stateCreator, options.dataSourceCreator);
this.id = options?.id ?? `model_${uniqueId()}`;
if (options?.metadata) {
this.metadata = options.metadata;
}
if (componentsTree.children) {
this.widgets = this.buildWidgets(componentsTree.children);
}
}
private initModelScope(
stateCreator: ModelScopeStateCreator,
dataSourceCreator: ModelScopeDataSourceCreator,
) {
initialize({ defaultProps, stateCreator, dataSourceCreator }: InitializeModelOptions) {
const {
state = {},
defaultProps: defaultSchemaProps,
props = {},
dataSource = defaultDataSourceSchema,
methods = {},
} = this.componentsTree;
this.codeScope = this.codeRuntime.createChildScope({});
this.codeScope = this.codeRuntime.createChildScope({
props: {
...props,
...defaultSchemaProps,
...defaultProps,
},
});
const initalState = this.codeRuntime.resolve(state, this.codeScope);
const initalProps = this.codeRuntime.resolve(props, this.codeScope);
const initalState = this.codeRuntime.resolve(state, { scope: this.codeScope });
const initalProps = this.codeRuntime.resolve(props, { scope: this.codeScope });
const stateApi = stateCreator(initalState);
const dataSourceApi = dataSourceCreator(dataSource, stateApi);
@ -95,7 +129,7 @@ export class ComponentTreeModel<Component, ComponentInstance = unknown>
this.codeScope.setValue(
Object.assign(
{
props: initalProps,
props: { ...defaultProps, ...initalProps },
$: (ref: string) => {
const insArr = this.instanceMap.get(ref);
if (!insArr) return undefined;
@ -111,9 +145,9 @@ export class ComponentTreeModel<Component, ComponentInstance = unknown>
);
for (const [key, fn] of Object.entries(methods)) {
const customMethod = this.codeRuntime.resolve(fn, this.codeScope);
const customMethod = this.codeRuntime.resolve(fn, { scope: this.codeScope });
if (typeof customMethod === 'function') {
this.codeScope.inject(key, customMethod);
this.codeScope.set(key, customMethod);
}
}
}
@ -133,7 +167,7 @@ export class ComponentTreeModel<Component, ComponentInstance = unknown>
const lifeCycleSchema = this.componentsTree.lifeCycles[lifeCycleName];
const lifeCycleFn = this.codeRuntime.resolve(lifeCycleSchema, this.codeScope);
const lifeCycleFn = this.codeRuntime.resolve(lifeCycleSchema, { scope: this.codeScope });
if (typeof lifeCycleFn === 'function') {
lifeCycleFn.apply(this.codeScope.value, args);
}
@ -162,13 +196,31 @@ export class ComponentTreeModel<Component, ComponentInstance = unknown>
buildWidgets(nodes: Spec.NodeType[]): IWidget<Component>[] {
return nodes.map((node) => {
const widget = new Widget<Component, ComponentInstance>(node, this);
if (isComponentNode(node)) {
const normalized = normalizeComponentNode(node);
const widget = new Widget<Component, ComponentInstance>(normalized, this);
if (isComponentNode(node) && node.children?.length) {
widget.children = this.buildWidgets(node.children);
if (normalized.children?.length) {
widget.children = this.buildWidgets(normalized.children);
}
return widget;
} else {
return new Widget<Component, ComponentInstance>(node, this);
}
return widget;
});
}
}
export function normalizeComponentNode(node: Spec.ComponentNode): NormalizedComponentNode {
const [loopArgsOne, loopArgsTwo] = node.loopArgs ?? [];
const { children, ...props } = node.props ?? {};
return {
...node,
loopArgs: [loopArgsOne || 'item', loopArgsTwo || 'index'],
props,
condition: node.condition || node.condition === false ? node.condition : true,
children: node.children ?? children,
};
}

View File

@ -4,18 +4,18 @@ import {
type IComponentTreeModel,
ComponentTreeModel,
type ComponentTreeModelOptions,
} from './treeModel';
} from './componentTreeModel';
import { ISchemaService } from '../schema';
export interface IComponentTreeModelService {
create<Component>(
componentsTree: Spec.ComponentTree,
options: ComponentTreeModelOptions,
options?: ComponentTreeModelOptions,
): IComponentTreeModel<Component>;
createById<Component>(
id: string,
options: ComponentTreeModelOptions,
options?: ComponentTreeModelOptions,
): IComponentTreeModel<Component>;
}
@ -32,14 +32,14 @@ export class ComponentTreeModelService implements IComponentTreeModelService {
create<Component>(
componentsTree: Spec.ComponentTree,
options: ComponentTreeModelOptions,
options?: ComponentTreeModelOptions,
): IComponentTreeModel<Component> {
return new ComponentTreeModel(componentsTree, this.codeRuntimeService, options);
}
createById<Component>(
id: string,
options: ComponentTreeModelOptions,
options?: ComponentTreeModelOptions,
): IComponentTreeModel<Component> {
const componentsTrees = this.schemaService.get('componentsTree');
const componentsTree = componentsTrees.find((item) => item.id === id);

View File

@ -0,0 +1,2 @@
export * from './componentTreeModel';
export * from './componentTreeModelService';

View File

@ -9,6 +9,8 @@ import {
} from '@alilc/lowcode-shared';
import { get as lodashGet } from 'lodash-es';
import { PackageLoader } from './loader';
import { ISchemaService } from '../schema';
import { ILifeCycleService, LifecyclePhase } from '../lifeCycleService';
export interface NormalizedPackage {
id: string;
@ -62,6 +64,16 @@ export class PackageManagementService implements IPackageManagementService {
private packageLoaders: PackageLoader[] = [];
constructor(
@ISchemaService private schemaService: ISchemaService,
@ILifeCycleService private lifeCycleService: ILifeCycleService,
) {
this.lifeCycleService.when(LifecyclePhase.Ready).then(() => {
const componentsMaps = this.schemaService.get('componentsMap');
this.resolveComponentMaps(componentsMaps);
});
}
async loadPackages(packages: Spec.Package[]) {
for (const item of packages) {
// low code component not need load
@ -99,7 +111,7 @@ export class PackageManagementService implements IPackageManagementService {
if (map.devMode === 'lowCode') {
const packageInfo = this.lowCodeComponentPackages.get((map as LowCodeComponent).id);
if (packageInfo) {
if (map.componentName && packageInfo) {
this.componentsRecord[map.componentName] = packageInfo;
}
} else {
@ -123,7 +135,7 @@ export class PackageManagementService implements IPackageManagementService {
}
const recordName = map.componentName ?? map.exportName;
this.componentsRecord[recordName] = result;
if (recordName && result) this.componentsRecord[recordName] = result;
}
}
}

View File

@ -0,0 +1,102 @@
import {
createDecorator,
Provide,
Intl,
type Spec,
type Locale,
type Translations,
} from '@alilc/lowcode-shared';
import { ILifeCycleService, LifecyclePhase } from './lifeCycleService';
import { ICodeRuntimeService } from './code-runtime';
import { ISchemaService } from './schema';
export interface MessageDescriptor {
key: string;
params?: Record<string, string>;
fallback?: string;
}
export interface IRuntimeIntlService {
locale: string;
t(descriptor: MessageDescriptor): string;
setLocale(locale: Locale): void;
getLocale(): Locale;
addTranslations(locale: Locale, translations: Translations): void;
}
export const IRuntimeIntlService = createDecorator<IRuntimeIntlService>('IRuntimeIntlService');
@Provide(IRuntimeIntlService)
export class RuntimeIntlService implements IRuntimeIntlService {
private intl: Intl = new Intl();
public locale: string = navigator.language;
constructor(
@ILifeCycleService private lifeCycleService: ILifeCycleService,
@ICodeRuntimeService private codeRuntimeService: ICodeRuntimeService,
@ISchemaService private schemaService: ISchemaService,
) {
this.lifeCycleService.when(LifecyclePhase.OptionsResolved).then(() => {
const config = this.schemaService.get('config');
const i18nTranslations = this.schemaService.get('i18n');
if (config?.defaultLocale) {
this.setLocale(config.defaultLocale);
}
if (i18nTranslations) {
Object.keys(i18nTranslations).forEach((key) => {
this.addTranslations(key, i18nTranslations[key]);
});
}
});
this.lifeCycleService.when(LifecyclePhase.Ready).then(() => {
this.toExpose();
});
}
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);
}
private toExpose(): void {
const exposed: Spec.IntlApi = {
i18n: (key, params) => {
return this.t({ key, params });
},
getLocale: () => {
return this.getLocale();
},
setLocale: (locale) => {
this.setLocale(locale);
},
};
this.codeRuntimeService.getScope().setValue(exposed);
}
}

View File

@ -1,14 +1,14 @@
import { type AnyFunction, type Spec, createDecorator, Provide } from '@alilc/lowcode-shared';
import { IPackageManagementService } from './package';
import { ICodeRuntimeService } from './code-runtime';
import { ILifeCycleService, LifecyclePhase } from './lifeCycleService';
import { ISchemaService } from './schema';
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');
@ -17,12 +17,21 @@ export const IRuntimeUtilService = createDecorator<IRuntimeUtilService>('rendere
export class RuntimeUtilService implements IRuntimeUtilService {
private utilsMap: Map<string, AnyFunction> = new Map();
private _expose: any;
constructor(
@ICodeRuntimeService private codeRuntimeService: ICodeRuntimeService,
@IPackageManagementService private packageManagementService: IPackageManagementService,
) {}
@ILifeCycleService private lifeCycleService: ILifeCycleService,
@ISchemaService private schemaService: ISchemaService,
) {
this.lifeCycleService.when(LifecyclePhase.Ready).then(() => {
const utils = this.schemaService.get('utils') ?? [];
for (const util of utils) {
this.add(util);
}
this.toExpose();
});
}
add(utilItem: Spec.Util): void;
add(name: string, fn: AnyFunction): void;
@ -41,22 +50,20 @@ export class RuntimeUtilService implements IRuntimeUtilService {
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);
},
});
}
private toExpose(): void {
const exposed = 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;
this.codeRuntimeService.getScope().set('utils', exposed);
}
private parseUtil(utilItem: Spec.Util) {

View File

@ -2,11 +2,13 @@ import {
type Spec,
createDecorator,
Provide,
type IStore,
KeyValueStore,
type EventDisposable,
} from '@alilc/lowcode-shared';
import { isObject } from 'lodash-es';
import { schemaValidation } from './validation';
import { ILifeCycleService, LifecyclePhase } from '../lifeCycleService';
import { ICodeRuntimeService } from '../code-runtime';
export interface NormalizedSchema extends Spec.Project {}
@ -18,22 +20,26 @@ export interface ISchemaService {
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>;
private store: IStore<NormalizedSchema, NormalizedSchemaKey> = new KeyValueStore<
NormalizedSchema,
NormalizedSchemaKey
>({
setterValidation: schemaValidation,
});
constructor() {
this.store = new KeyValueStore<NormalizedSchema, NormalizedSchemaKey>(new Map(), {
setterValidation: schemaValidation,
constructor(
@ILifeCycleService private lifeCycleService: ILifeCycleService,
@ICodeRuntimeService private codeRuntimeService: ICodeRuntimeService,
) {
this.lifeCycleService.when(LifecyclePhase.Ready).then(() => {
const constants = this.get('constants') ?? {};
this.codeRuntimeService.getScope().set('constants', constants);
});
}
@ -55,11 +61,4 @@ export class SchemaService implements ISchemaService {
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,34 @@
import { type Spec, uniqueId } from '@alilc/lowcode-shared';
import { clone } from 'lodash-es';
import { IComponentTreeModel } from '../model';
export interface IWidget<Component, ComponentInstance = unknown> {
readonly key: string;
readonly node: Spec.NodeType;
model: IComponentTreeModel<Component, ComponentInstance>;
children?: IWidget<Component, ComponentInstance>[];
}
export class Widget<Component, ComponentInstance = unknown>
implements IWidget<Component, ComponentInstance>
{
public __raw: Spec.NodeType;
public node: Spec.NodeType;
public key: string;
public children?: IWidget<Component, ComponentInstance>[] | undefined;
constructor(
node: Spec.NodeType,
public model: IComponentTreeModel<Component, ComponentInstance>,
) {
this.node = clone(node);
this.__raw = node;
this.key = (node as Spec.ComponentNode)?.id ?? uniqueId();
}
}

View File

@ -1,26 +1,23 @@
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';
import { type EvalCodeFunction } from './parts/code-runtime';
import { type Plugin } from './services/extension';
import { type ISchemaService } from './services/schema';
import { type IPackageManagementService } from './services/package';
import { type CodeRuntimeInitializeOptions } from './services/code-runtime';
export interface AppOptions {
schema: Spec.Project;
packages?: Spec.Package[];
plugins?: Plugin[];
/**
* navigator.language
*/
locale?: string;
/**
*
*/
mode?: 'development' | 'production';
evalCodeFunction?: EvalCodeFunction;
/**
* code runtime
*/
codeRuntime?: CodeRuntimeInitializeOptions;
}
export type RendererApplication<Render = unknown> = {
@ -30,5 +27,5 @@ export type RendererApplication<Render = unknown> = {
readonly packageManager: IPackageManagementService;
use: IExtensionHostService['registerPlugin'];
use(plugin: Plugin): Promise<void>;
} & Render;

View File

@ -0,0 +1,5 @@
export function evaluate(code: string, scope: any) {
return new Function('scope', `"use strict";return (function(){return (${code})}).bind(scope)();`)(
scope,
);
}

View File

@ -0,0 +1,75 @@
// generated from https://github.com/sindresorhus/globals/blob/main/globals.json es2015 part
export const globals = [
'Array',
'ArrayBuffer',
'Boolean',
'constructor',
'DataView',
'Date',
'decodeURI',
'decodeURIComponent',
'encodeURI',
'encodeURIComponent',
'Error',
'escape',
'eval',
'EvalError',
'Float32Array',
'Float64Array',
'Function',
'hasOwnProperty',
'Infinity',
'Int16Array',
'Int32Array',
'Int8Array',
'isFinite',
'isNaN',
'isPrototypeOf',
'JSON',
'Map',
'Math',
'NaN',
'Number',
'Object',
'parseFloat',
'parseInt',
'Promise',
'propertyIsEnumerable',
'Proxy',
'RangeError',
'ReferenceError',
'Reflect',
'RegExp',
'Set',
'String',
'Symbol',
'SyntaxError',
'toLocaleString',
'toString',
'TypeError',
'Uint16Array',
'Uint32Array',
'Uint8Array',
'Uint8ClampedArray',
'undefined',
'unescape',
'URIError',
'valueOf',
'WeakMap',
'WeakSet',
];
const spiedGlobals = [
'window',
'self',
'globalThis',
'top',
'parent',
'hasOwnProperty',
'document',
'eval',
];
export const trustedGlobals = [
...globals.filter((item) => spiedGlobals.includes(item)),
'requestAnimationFrame',
];

View File

@ -0,0 +1,191 @@
/**
* TypeScript-ization estree-walker
* fork from: https://github.com/Rich-Harris/estree-walker
*/
import { type PlainObject, type Spec } from '@alilc/lowcode-shared';
type Node = Spec.JSNode;
interface WalkerContext {
skip: () => void;
remove: () => void;
replace: (node: Node) => void;
}
class WalkerBase {
should_skip: boolean = false;
should_remove: boolean = false;
replacement: Node | null = null;
context: WalkerContext;
constructor() {
this.context = {
skip: () => (this.should_skip = true),
remove: () => (this.should_remove = true),
replace: (node) => (this.replacement = node),
};
}
replace(
parent: Node | null,
prop: keyof Node | null | undefined,
index: number | null | undefined,
node: Node,
) {
if (parent && prop) {
if (index != null) {
parent[prop][index] = node;
} else {
parent[prop] = node;
}
}
}
remove(
parent: Node | null | undefined,
prop: keyof Node | null | undefined,
index: number | null | undefined,
) {
if (parent && prop) {
if (index !== null && index !== undefined) {
parent[prop].splice(index, 1);
} else {
delete parent[prop];
}
}
}
}
export type SyncWalkerHandler = (
this: WalkerContext,
node: Node,
parent: Node | null,
key: PropertyKey | undefined,
index: number | undefined,
) => void;
export class SyncWalker extends WalkerBase {
enter: SyncWalkerHandler | undefined;
leave: SyncWalkerHandler | undefined;
constructor(enter?: SyncWalkerHandler | undefined, leave?: SyncWalkerHandler | undefined) {
super();
this.enter = enter;
this.leave = leave;
}
visit(
node: Node,
parent: Node | null,
prop?: keyof Node | undefined,
index?: number | undefined,
): Node | null {
if (node) {
if (this.enter) {
const _should_skip = this.should_skip;
const _should_remove = this.should_remove;
const _replacement = this.replacement;
this.should_skip = false;
this.should_remove = false;
this.replacement = null;
this.enter.call(this.context, node, parent, prop, index);
if (this.replacement) {
node = this.replacement;
this.replace(parent, prop, index, node);
}
if (this.should_remove) {
this.remove(parent, prop, index);
}
const skipped = this.should_skip;
const removed = this.should_remove;
this.should_skip = _should_skip;
this.should_remove = _should_remove;
this.replacement = _replacement;
if (skipped) return node;
if (removed) return null;
}
let key: keyof Node;
for (key in node) {
const value = node[key] as unknown;
if (value && typeof value === 'object') {
if (Array.isArray(value)) {
const nodes = value as unknown[];
for (let i = 0; i < nodes.length; i += 1) {
const item = nodes[i];
if (isNode(item)) {
if (!this.visit(item, node, key, i)) {
// removed
i--;
}
}
}
} else if (isNode(value)) {
this.visit(value, node, key, undefined);
}
}
}
if (this.leave) {
const _replacement = this.replacement;
const _should_remove = this.should_remove;
this.replacement = null;
this.should_remove = false;
this.leave.call(this.context, node, parent, prop, index);
if (this.replacement) {
node = this.replacement;
this.replace(parent, prop, index, node);
}
if (this.should_remove) {
this.remove(parent, prop, index);
}
const removed = this.should_remove;
this.replacement = _replacement;
this.should_remove = _should_remove;
if (removed) return null;
}
}
return node;
}
}
/**
* Ducktype a node.
*
* @param {unknown} value
* @returns {value is Node}
*/
export function isNode(value: unknown): value is Node {
return (
value !== null && typeof value === 'object' && 'type' in value && typeof value.type === 'string'
);
}
export function walk(
ast: PlainObject,
{ enter, leave }: { enter?: SyncWalkerHandler; leave?: SyncWalkerHandler } = {},
) {
const instance = new SyncWalker(enter, leave);
return instance.visit(ast as any, null);
}

View File

@ -1,40 +1,46 @@
import { type PlainObject } from '@alilc/lowcode-shared';
import { isPlainObject, isEmpty } from 'lodash-es';
export function someValue(obj: any, predicate: (data: any) => boolean) {
if (!isPlainObject(obj) || isEmpty(obj)) return false;
if (predicate(obj)) return true;
for (const val of Object.values(obj)) {
if (someValue(val, predicate)) return true;
export function someValue(
obj: PlainObject | PlainObject[],
filter: (data: PlainObject) => boolean,
): boolean {
if (Array.isArray(obj)) {
return obj.some((item) => someValue(item, filter));
}
return false;
if (!isPlainObject(obj) || isEmpty(obj)) return false;
if (filter(obj)) return true;
return Object.values(obj).some((val) => someValue(val, filter));
}
export function processValue(
obj: any,
predicate: (obj: any) => boolean,
processor: (node: any, paths: Array<string | number>) => any,
export function mapValue(
obj: PlainObject,
filter: (obj: PlainObject) => boolean,
callback: (node: any, paths: Array<string | number>) => any,
): any {
const innerProcess = (target: any, paths: Array<string | number>): any => {
if (!someValue(obj, filter)) return obj;
const mapping = (target: any, paths: Array<string | number>): any => {
if (Array.isArray(target)) {
return target.map((item, idx) => innerProcess(item, [...paths, idx]));
return target.map((item, idx) => mapping(item, [...paths, idx]));
}
if (!isPlainObject(target) || isEmpty(target)) return target;
if (!someValue(target, predicate)) return target;
if (predicate(target)) {
return processor(target, paths);
} else {
const result = {} as any;
for (const [key, value] of Object.entries(target)) {
result[key] = innerProcess(value, [...paths, key]);
}
return result;
if (filter(target)) {
return callback(target, paths);
}
const result: PlainObject = {};
for (const [key, value] of Object.entries(target)) {
result[key] = mapping(value, [...paths, key]);
}
return result;
};
return innerProcess(obj, []);
return mapping(obj, []);
}

View File

@ -42,6 +42,8 @@ export interface Router extends Spec.RouterApi {
beforeRouteLeave: (fn: NavigationGuard) => () => void;
afterRouteChange: (fn: NavigationHookAfter) => () => void;
isReady(): Promise<void>;
}
const START_LOCATION: RouteLocationNormalized = {
@ -68,6 +70,7 @@ export function createRouter(options: RouterOptions): Router {
const beforeGuards = createCallback<NavigationGuard>();
const afterGuards = createCallback<NavigationHookAfter>();
const readyHandlers = createCallback<any>();
let currentLocation: RouteLocationNormalized = START_LOCATION;
let pendingLocation = currentLocation;
@ -203,7 +206,9 @@ export function createRouter(options: RouterOptions): Router {
}
return navigateTriggerBeforeGuards(toLocation, from)
.catch(() => {})
.catch((error) => {
return markAsReady(error);
})
.then(() => {
finalizeNavigation(toLocation, from, true, replace, data);
@ -293,7 +298,7 @@ export function createRouter(options: RouterOptions): Router {
}
currentLocation = toLocation;
// markAsReady();
markAsReady();
}
let removeHistoryListener: undefined | null | (() => void);
@ -337,6 +342,32 @@ export function createRouter(options: RouterOptions): Router {
});
}
let ready: boolean;
/**
* Mark the router as ready, resolving the promised returned by isReady(). Can
* only be called once, otherwise does nothing.
* @param err - optional error
*/
function markAsReady<E = any>(err?: E): E | void {
if (!ready) {
// still not ready if an error happened
ready = !err;
setupListeners();
readyHandlers.list().forEach(([resolve, reject]) => (err ? reject(err) : resolve()));
readyHandlers.clear();
}
return err;
}
function isReady(): Promise<void> {
if (ready && currentLocation !== START_LOCATION) return Promise.resolve();
return new Promise((resolve, reject) => {
readyHandlers.add([resolve, reject]);
});
}
// init
setupListeners();
if (currentLocation === START_LOCATION) {
@ -370,5 +401,7 @@ export function createRouter(options: RouterOptions): Router {
beforeRouteLeave: beforeGuards.add,
afterRouteChange: afterGuards.add,
isReady,
};
}

View File

@ -15,7 +15,7 @@ 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);
return set(serviceId)(target, targetKey, indexOrPropertyDescriptor);
}
);
id.toString = () => serviceId;

View File

@ -14,7 +14,7 @@ export class Intl {
private currentMessage: ComputedSignal<Translations>;
private intlShape: IntlFormatter;
constructor(defaultLocale?: string, messages: LocaleTranslationsRecord = {}) {
constructor(defaultLocale: string = navigator.language, messages: LocaleTranslationsRecord = {}) {
if (defaultLocale) {
defaultLocale = nomarlizeLocale(defaultLocale);
} else {

View File

@ -1,4 +1,3 @@
import { invariant } from '../utils';
import { PlainObject } from '../types';
/**
@ -20,23 +19,12 @@ export interface IStore<O, K extends keyof O> {
/**
*
*/
export class KeyValueStore<O = PlainObject, K extends keyof O = keyof O> {
export class KeyValueStore<O = PlainObject, K extends keyof O = keyof O> implements IStore<O, K> {
private readonly store = new Map();
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;
},
) {
constructor(options?: { setterValidation?: (key: K, value: O[K]) => boolean | string }) {
if (options?.setterValidation) {
this.setterValidation = options.setterValidation;
}
@ -45,8 +33,8 @@ export class KeyValueStore<O = PlainObject, K extends keyof O = keyof O> {
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;
const value = this.store.get(key);
return value ?? defaultValue;
}
set(key: K, value: O[K]): void {
@ -60,7 +48,6 @@ export class KeyValueStore<O = PlainObject, K extends keyof O = keyof O> {
}
this.store.set(key, value);
this.dispatchValue(key);
}
delete(key: K): void {
@ -74,77 +61,4 @@ export class KeyValueStore<O = PlainObject, K extends keyof O = keyof O> {
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

@ -9,19 +9,31 @@ import {
ref,
computed,
ReactiveEffect,
shallowRef,
type ShallowRef,
type ComputedRef,
type Ref,
getCurrentScope,
isRef,
isReactive,
isShallow,
EffectScheduler,
readonly,
type EffectScheduler,
} from '@vue/reactivity';
import { noop, isObject, isPlainObject, isSet, isMap, isFunction } from 'lodash-es';
import { isPromise } from './utils';
export { ref as signal, computed, watchEffect as effect, watch as reaction, isRef as isSignal };
export type { Ref as Signal, ComputedRef as ComputedSignal };
export {
ref as signal,
shallowRef as shallowSignal,
computed,
watchEffect as effect,
watch as reaction,
isRef as isSignal,
isShallow as isShallowSignal,
readonly,
};
export type { Ref as Signal, ComputedRef as ComputedSignal, ShallowRef as ShallowSignal };
export type WatchSource<T = any> = Ref<T> | ComputedRef<T> | (() => T);
export type WatchEffect = (onCleanup: OnCleanup) => void;

View File

@ -40,7 +40,7 @@ export interface Project {
/**
*
*/
config?: Record<string, JSONObject>;
config?: ProjectConfig;
/**
*
*/
@ -60,6 +60,32 @@ export interface Project {
pages?: PageConfig[];
}
/**
*
*/
export interface ProjectConfig {
/**
* navagator.language
*/
defaultLocale?: string;
/**
*
*/
layout?: {
componentName: string;
props: Record<string, JSONObject>;
};
/**
* dom
*/
targetRootID?: string;
// todo
theme?: any;
[key: string]: any;
}
/**
* https://lowcode-engine.cn/site/docs/specs/lowcode-spec#22-%E7%BB%84%E4%BB%B6%E6%98%A0%E5%B0%84%E5%85%B3%E7%B3%BBa
* componentName
@ -259,7 +285,11 @@ export interface ComponentNode {
*/
componentName: string;
/**
*
* props
*/
defaultProps?: JSONObject;
/**
* props
*/
props?: ComponentNodeProps;
/**
@ -415,11 +445,16 @@ export interface JSONObject {
[key: string]: JSONValue | JSONObject | JSONObject[];
}
export interface JSNode {
type: string;
[key: string]: any;
}
/**
* A
* Node Function-Return-Node
*/
export interface JSSlot {
export interface JSSlot extends JSNode {
type: 'JSSlot';
value: ComponentNode | ComponentNode[];
params?: string[];
@ -430,7 +465,7 @@ export interface JSSlot {
/**
* A
*/
export interface JSFunction {
export interface JSFunction extends JSNode {
type: 'JSFunction';
value: string;
@ -440,7 +475,7 @@ export interface JSFunction {
/**
* A
*/
export interface JSExpression {
export interface JSExpression extends JSNode {
type: 'JSExpression';
value: string;
@ -450,7 +485,7 @@ export interface JSExpression {
/**
* AA
*/
export interface JSI18n {
export interface JSI18n extends JSNode {
type: 'i18n';
/**
* i18n key
@ -462,6 +497,4 @@ export interface JSI18n {
params?: Record<string, string | number | JSExpression>;
}
export type JSNode = JSSlot | JSExpression | JSExpression | JSI18n;
export type NodeType = string | JSExpression | JSI18n | ComponentNode;

View File

@ -0,0 +1,46 @@
/**
* A barrier that is initially closed and then becomes opened permanently.
*/
export class Barrier {
private _isOpen: boolean;
private _promise: Promise<boolean>;
private _completePromise!: (v: boolean) => void;
constructor() {
this._isOpen = false;
this._promise = new Promise<boolean>((c) => {
this._completePromise = c;
});
}
isOpen(): boolean {
return this._isOpen;
}
open(): void {
this._isOpen = true;
this._completePromise(true);
}
wait(): Promise<boolean> {
return this._promise;
}
}
/**
* A barrier that is initially closed and then becomes opened permanently after a certain period of
* time or when open is called explicitly
*/
export class AutoOpenBarrier extends Barrier {
private readonly _timeout: any;
constructor(autoOpenTimeMs: number) {
super();
this._timeout = setTimeout(() => this.open(), autoOpenTimeMs);
}
override open(): void {
clearTimeout(this._timeout);
super.open();
}
}

View File

@ -4,3 +4,4 @@ export * from './unique-id';
export * from './type-guards';
export * from './platform';
export * from './callback';
export * from './async';