mirror of
https://github.com/alibaba/lowcode-engine.git
synced 2026-04-18 11:28:06 +00:00
refactor: refactor codes
This commit is contained in:
parent
0157bbd68f
commit
37d07f1db6
@ -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!');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@ -30,7 +30,8 @@
|
|||||||
"hoist-non-react-statics": "^3.3.2",
|
"hoist-non-react-statics": "^3.3.2",
|
||||||
"use-sync-external-store": "^1.2.0",
|
"use-sync-external-store": "^1.2.0",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0"
|
"react-dom": "^18.2.0",
|
||||||
|
"react-is": "^18.3.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@testing-library/react": "^14.2.0",
|
"@testing-library/react": "^14.2.0",
|
||||||
@ -38,7 +39,8 @@
|
|||||||
"@types/hoist-non-react-statics": "^3.3.5",
|
"@types/hoist-non-react-statics": "^3.3.5",
|
||||||
"@types/use-sync-external-store": "^0.0.6",
|
"@types/use-sync-external-store": "^0.0.6",
|
||||||
"@types/react": "^18.2.67",
|
"@types/react": "^18.2.67",
|
||||||
"@types/react-dom": "^18.2.22"
|
"@types/react-dom": "^18.2.22",
|
||||||
|
"@types/react-is": "^18.3.0"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
|
|||||||
@ -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 ComponentType } from 'react';
|
||||||
import { type Root, createRoot } from 'react-dom/client';
|
import { type Root, createRoot } from 'react-dom/client';
|
||||||
import { createRouter, type RouterOptions } from '@alilc/lowcode-renderer-router';
|
import { ApplicationView, RendererContext, extension } from '../app';
|
||||||
import AppComponent from '../components/app';
|
|
||||||
import { RendererContext } from '../context/render';
|
|
||||||
import { createRouterProvider } from '../components/routerView';
|
|
||||||
import { rendererExtends } from '../plugin';
|
|
||||||
|
|
||||||
export interface ReactAppOptions extends AppOptions {
|
export interface ReactAppOptions extends AppOptions {
|
||||||
faultComponent?: ComponentType<any>;
|
faultComponent?: ComponentType<any>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultRouterOptions: RouterOptions = {
|
|
||||||
historyMode: 'browser',
|
|
||||||
baseName: '/',
|
|
||||||
routes: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
export const createApp = async (options: ReactAppOptions) => {
|
export const createApp = async (options: ReactAppOptions) => {
|
||||||
const creator = createRenderer<IRender>(async (context) => {
|
return createRenderer(async (context) => {
|
||||||
const { schema, boostsManager } = 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
|
// set config
|
||||||
// if (options.faultComponent) {
|
// if (options.faultComponent) {
|
||||||
@ -44,24 +17,21 @@ export const createApp = async (options: ReactAppOptions) => {
|
|||||||
// }
|
// }
|
||||||
|
|
||||||
// extends boosts
|
// extends boosts
|
||||||
boostsManager.extend(rendererExtends);
|
extension.install(boostsManager);
|
||||||
|
|
||||||
const RouterProvider = createRouterProvider(router);
|
|
||||||
|
|
||||||
let root: Root | undefined;
|
let root: Root | undefined;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
async mount(el) {
|
async mount(containerOrId) {
|
||||||
if (root) {
|
if (root) return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
root = createRoot(el);
|
const defaultId = schema.get('config')?.targetRootID ?? 'app';
|
||||||
|
const rootElement = normalizeContainer(containerOrId, defaultId);
|
||||||
|
|
||||||
|
root = createRoot(rootElement);
|
||||||
root.render(
|
root.render(
|
||||||
<RendererContext.Provider value={context}>
|
<RendererContext.Provider value={context}>
|
||||||
<RouterProvider>
|
<ApplicationView />
|
||||||
<AppComponent />
|
|
||||||
</RouterProvider>
|
|
||||||
</RendererContext.Provider>,
|
</RendererContext.Provider>,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@ -72,7 +42,23 @@ export const createApp = async (options: ReactAppOptions) => {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
});
|
})(options);
|
||||||
|
|
||||||
return creator(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;
|
||||||
|
}
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { createRenderer, type AppOptions } from '@alilc/lowcode-renderer-core';
|
import { createRenderer, type AppOptions } from '@alilc/lowcode-renderer-core';
|
||||||
import { FunctionComponent } from 'react';
|
import { FunctionComponent } from 'react';
|
||||||
import { type LowCodeComponentProps, createComponentBySchema } from '../runtime';
|
import { type LowCodeComponentProps, createComponentBySchema } from '../runtime/component';
|
||||||
import { RendererContext } from '../context/render';
|
import { RendererContext } from '../app/context';
|
||||||
|
|
||||||
interface Render {
|
interface Render {
|
||||||
toComponent(): FunctionComponent<LowCodeComponentProps>;
|
toComponent(): FunctionComponent<LowCodeComponentProps>;
|
||||||
|
|||||||
@ -3,6 +3,6 @@ import { type RenderContext } from '@alilc/lowcode-renderer-core';
|
|||||||
|
|
||||||
export const RendererContext = createContext<RenderContext>(undefined!);
|
export const RendererContext = createContext<RenderContext>(undefined!);
|
||||||
|
|
||||||
RendererContext.displayName = 'RootContext';
|
RendererContext.displayName = 'RendererContext';
|
||||||
|
|
||||||
export const useRenderContext = () => useContext(RendererContext);
|
export const useRenderContext = () => useContext(RendererContext);
|
||||||
51
packages/react-renderer/src/app/extension.ts
Normal file
51
packages/react-renderer/src/app/extension.ts
Normal 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;
|
||||||
|
}
|
||||||
3
packages/react-renderer/src/app/index.ts
Normal file
3
packages/react-renderer/src/app/index.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export * from './context';
|
||||||
|
export * from './extension';
|
||||||
|
export * from './view';
|
||||||
34
packages/react-renderer/src/app/view.tsx
Normal file
34
packages/react-renderer/src/app/view.tsx
Normal 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;
|
||||||
|
}
|
||||||
@ -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;
|
|
||||||
}
|
|
||||||
@ -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;
|
|
||||||
}
|
|
||||||
@ -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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@ -1,9 +1,8 @@
|
|||||||
export * from './api/app';
|
export * from './api/app';
|
||||||
export * from './api/component';
|
export * from './api/component';
|
||||||
export { defineRendererPlugin } from './plugin';
|
export { useRenderContext, defineRendererPlugin } from './app';
|
||||||
export * from './context/render';
|
export * from './router';
|
||||||
export * from './context/router';
|
|
||||||
|
|
||||||
export type { Spec, ProCodeComponent, LowCodeComponent } from '@alilc/lowcode-shared';
|
export type { Spec, ProCodeComponent, LowCodeComponent } from '@alilc/lowcode-shared';
|
||||||
export type { PackageLoader, CodeScope, Plugin } from '@alilc/lowcode-renderer-core';
|
export type { PackageLoader, CodeScope, Plugin } from '@alilc/lowcode-renderer-core';
|
||||||
export type { RendererExtends } from './plugin';
|
export type { RendererExtends } from './app/extension';
|
||||||
|
|||||||
@ -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;
|
|
||||||
}
|
|
||||||
@ -1,5 +1,4 @@
|
|||||||
import { type Router, type RouteLocationNormalized } from '@alilc/lowcode-renderer-router';
|
import { type Router, type RouteLocationNormalized } from '@alilc/lowcode-renderer-router';
|
||||||
import { type Spec } from '@alilc/lowcode-shared';
|
|
||||||
import { createContext, useContext } from 'react';
|
import { createContext, useContext } from 'react';
|
||||||
|
|
||||||
export const RouterContext = createContext<Router>(undefined!);
|
export const RouterContext = createContext<Router>(undefined!);
|
||||||
@ -13,9 +12,3 @@ export const RouteLocationContext = createContext<RouteLocationNormalized>(undef
|
|||||||
RouteLocationContext.displayName = 'RouteLocationContext';
|
RouteLocationContext.displayName = 'RouteLocationContext';
|
||||||
|
|
||||||
export const useRouteLocation = () => useContext(RouteLocationContext);
|
export const useRouteLocation = () => useContext(RouteLocationContext);
|
||||||
|
|
||||||
export const PageConfigContext = createContext<Spec.PageConfig | undefined>(undefined);
|
|
||||||
|
|
||||||
PageConfigContext.displayName = 'PageConfigContext';
|
|
||||||
|
|
||||||
export const usePageConfig = () => useContext(PageConfigContext);
|
|
||||||
2
packages/react-renderer/src/router/index.ts
Normal file
2
packages/react-renderer/src/router/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from './context';
|
||||||
|
export * from './plugin';
|
||||||
42
packages/react-renderer/src/router/plugin.ts
Normal file
42
packages/react-renderer/src/router/plugin.ts
Normal 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();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
41
packages/react-renderer/src/router/route.tsx
Normal file
41
packages/react-renderer/src/router/route.tsx
Normal 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;
|
||||||
|
}
|
||||||
20
packages/react-renderer/src/router/routerView.tsx
Normal file
20
packages/react-renderer/src/router/routerView.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
};
|
||||||
164
packages/react-renderer/src/runtime/component.tsx
Normal file
164
packages/react-renderer/src/runtime/component.tsx
Normal 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;
|
||||||
|
}
|
||||||
11
packages/react-renderer/src/runtime/context.ts
Normal file
11
packages/react-renderer/src/runtime/context.ts
Normal 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;
|
||||||
137
packages/react-renderer/src/runtime/hooks/useReactiveStore.tsx
Normal file
137
packages/react-renderer/src/runtime/hooks/useReactiveStore.tsx
Normal 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;
|
||||||
|
}
|
||||||
2
packages/react-renderer/src/runtime/index.ts
Normal file
2
packages/react-renderer/src/runtime/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from './component';
|
||||||
|
export * from './render';
|
||||||
@ -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;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@ -1,7 +1,7 @@
|
|||||||
import { signal, type PlainObject, type Spec } from '@alilc/lowcode-shared';
|
import { signal, type PlainObject, type Spec } from '@alilc/lowcode-shared';
|
||||||
import { isPlainObject } from 'lodash-es';
|
import { isPlainObject } from 'lodash-es';
|
||||||
|
|
||||||
export function reactiveStateCreator(initState: PlainObject): Spec.InstanceStateApi {
|
export function reactiveStateFactory(initState: PlainObject): Spec.InstanceStateApi {
|
||||||
const proxyState = signal(initState);
|
const proxyState = signal(initState);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -13,10 +13,9 @@ export function reactiveStateCreator(initState: PlainObject): Spec.InstanceState
|
|||||||
throw Error('newState mush be a object');
|
throw Error('newState mush be a object');
|
||||||
}
|
}
|
||||||
|
|
||||||
proxyState.value = {
|
Object.keys(newState as PlainObject).forEach((key) => {
|
||||||
...proxyState.value,
|
proxyState.value[key] = (newState as PlainObject)[key];
|
||||||
...newState,
|
});
|
||||||
};
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
222
packages/react-renderer/src/runtime/render.tsx
Normal file
222
packages/react-renderer/src/runtime/render.tsx
Normal 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;
|
||||||
|
}
|
||||||
106
packages/react-renderer/src/utils/element.ts
Normal file
106
packages/react-renderer/src/utils/element.ts
Normal 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);
|
||||||
|
});
|
||||||
|
}
|
||||||
@ -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,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@ -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);
|
|
||||||
}
|
|
||||||
@ -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();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@ -1,46 +1,55 @@
|
|||||||
import { describe, it, expect, beforeAll } from 'vitest';
|
import { describe, it, expect } from 'vitest';
|
||||||
import { ICodeScope, CodeScope } from '../../../src/parts/code-runtime';
|
import { CodeScope } from '../../../src/parts/code-runtime';
|
||||||
|
|
||||||
describe('codeScope', () => {
|
describe('CodeScope', () => {
|
||||||
let scope: ICodeScope;
|
it('should return initial values', () => {
|
||||||
|
const initValue = { a: 1, b: 2 };
|
||||||
beforeAll(() => {
|
const scope = new CodeScope(initValue);
|
||||||
scope = new CodeScope({});
|
expect(scope.value.a).toBe(1);
|
||||||
|
expect(scope.value.b).toBe(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should inject a new value', () => {
|
it('inject should add new values', () => {
|
||||||
scope.inject('username', 'Alice');
|
const scope = new CodeScope({});
|
||||||
expect(scope.value).toEqual({ username: 'Alice' });
|
scope.set('c', 3);
|
||||||
|
expect(scope.value.c).toBe(3);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not overwrite an existing value without force', () => {
|
it('inject should not overwrite existing values without force', () => {
|
||||||
scope.inject('username', 'Bob');
|
const initValue = { a: 1 };
|
||||||
expect(scope.value).toEqual({ username: 'Alice' });
|
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', () => {
|
it('setValue should merge values by default', () => {
|
||||||
scope.inject('username', 'Bob', true);
|
const initValue = { a: 1 };
|
||||||
expect(scope.value).toEqual({ username: 'Bob' });
|
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', () => {
|
it('setValue should replace values when replace is true', () => {
|
||||||
scope.setValue({ age: 25 });
|
const initValue = { a: 1 };
|
||||||
expect(scope.value).toEqual({ username: 'Bob', age: 25 });
|
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', () => {
|
it('should create child scopes and respect scope hierarchy', () => {
|
||||||
scope.setValue({ loggedIn: true }, true);
|
const parentValue = { a: 1, b: 2 };
|
||||||
expect(scope.value).toEqual({ loggedIn: true });
|
const childValue = { b: 3, c: 4 };
|
||||||
});
|
|
||||||
|
|
||||||
it('should create a child scope with initial values', () => {
|
const parentScope = new CodeScope(parentValue);
|
||||||
const childScope = scope.createChild({ sessionId: 'abc123' });
|
const childScope = parentScope.createChild(childValue);
|
||||||
expect(childScope.value).toEqual({ loggedIn: true, sessionId: 'abc123' });
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should set new values in the child scope without affecting the parent scope', () => {
|
expect(childScope.value.a).toBe(1); // Inherits from parent scope
|
||||||
const childScope = scope.createChild({ theme: 'dark' });
|
expect(childScope.value.b).toBe(3); // Overridden by child scope
|
||||||
expect(childScope.value).toEqual({ loggedIn: true, sessionId: 'abc123', theme: 'dark' });
|
expect(childScope.value.c).toBe(4); // Unique to child scope
|
||||||
expect(scope.value).toEqual({ loggedIn: true });
|
expect(parentScope.value.c).toBeUndefined(); // Parent scope should not have child's properties
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -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();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
79
packages/renderer-core/__tests__/utils/node.spec.ts
Normal file
79
packages/renderer-core/__tests__/utils/node.spec.ts
Normal 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));
|
||||||
|
});
|
||||||
|
});
|
||||||
98
packages/renderer-core/__tests__/utils/value.spec.ts
Normal file
98
packages/renderer-core/__tests__/utils/value.spec.ts
Normal 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');
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -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);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@ -1,20 +1,23 @@
|
|||||||
/* --------------- api -------------------- */
|
/* --------------- api -------------------- */
|
||||||
export * from './apiCreate';
|
export { createRenderer } from './main';
|
||||||
export { definePackageLoader } from './parts/package';
|
export { definePackageLoader } from './services/package';
|
||||||
export { Widget } from './parts/widget';
|
export { LifecyclePhase } from './services/lifeCycleService';
|
||||||
|
export { Widget } from './services/widget';
|
||||||
|
export * from './utils/node';
|
||||||
export * from './utils/value';
|
export * from './utils/value';
|
||||||
|
|
||||||
/* --------------- types ---------------- */
|
/* --------------- types ---------------- */
|
||||||
export type * from './types';
|
export type * from './types';
|
||||||
export type {
|
export type {
|
||||||
Plugin,
|
Plugin,
|
||||||
IRender,
|
IRenderObject,
|
||||||
PluginContext,
|
PluginContext,
|
||||||
RenderAdapter,
|
RenderAdapter,
|
||||||
RenderContext,
|
RenderContext,
|
||||||
} from './parts/extension';
|
} from './services/extension';
|
||||||
export type * from './parts/code-runtime';
|
export type * from './services/code-runtime';
|
||||||
export type * from './parts/component-tree-model';
|
export type * from './services/model';
|
||||||
export type * from './parts/package';
|
export type * from './services/package';
|
||||||
export type * from './parts/schema';
|
export type * from './services/schema';
|
||||||
export type * from './parts/widget';
|
export type * from './services/widget';
|
||||||
|
export type * from './services/extension';
|
||||||
|
|||||||
@ -1,91 +1,111 @@
|
|||||||
import { Injectable } from '@alilc/lowcode-shared';
|
import { Injectable, invariant, InstantiationService } from '@alilc/lowcode-shared';
|
||||||
import { ICodeRuntimeService } from './parts/code-runtime';
|
import { ICodeRuntimeService } from './services/code-runtime';
|
||||||
import { IExtensionHostService, type RenderAdapter } from './parts/extension';
|
import {
|
||||||
import { IPackageManagementService } from './parts/package';
|
IBoostsService,
|
||||||
import { IRuntimeUtilService } from './parts/runtimeUtil';
|
IExtensionHostService,
|
||||||
import { IRuntimeIntlService } from './parts/runtimeIntl';
|
type RenderAdapter,
|
||||||
import { ISchemaService } from './parts/schema';
|
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';
|
import type { AppOptions, RendererApplication } from './types';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class RendererMain {
|
export class RendererMain<RenderObject> {
|
||||||
private mode: 'development' | 'production' = 'production';
|
private mode: 'development' | 'production' = 'production';
|
||||||
|
|
||||||
private initOptions: AppOptions;
|
private initOptions: AppOptions;
|
||||||
|
|
||||||
|
private renderObject: RenderObject;
|
||||||
|
|
||||||
|
private adapter: RenderAdapter<RenderObject>;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@ICodeRuntimeService private codeRuntimeService: ICodeRuntimeService,
|
@ICodeRuntimeService private codeRuntimeService: ICodeRuntimeService,
|
||||||
@IPackageManagementService private packageManagementService: IPackageManagementService,
|
@IPackageManagementService private packageManagementService: IPackageManagementService,
|
||||||
@IRuntimeUtilService private runtimeUtilService: IRuntimeUtilService,
|
|
||||||
@IRuntimeIntlService private runtimeIntlService: IRuntimeIntlService,
|
|
||||||
@ISchemaService private schemaService: ISchemaService,
|
@ISchemaService private schemaService: ISchemaService,
|
||||||
@IExtensionHostService private extensionHostService: IExtensionHostService,
|
@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) {
|
this.renderObject = await this.adapter(renderContext);
|
||||||
const { schema, mode } = options;
|
|
||||||
|
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;
|
if (mode) this.mode = mode;
|
||||||
this.initOptions = { ...options };
|
this.initOptions = { ...options };
|
||||||
|
this.adapter = adapter;
|
||||||
|
|
||||||
// valid schema
|
// valid schema
|
||||||
this.schemaService.initialize(schema);
|
this.schemaService.initialize(schema);
|
||||||
|
|
||||||
this.codeRuntimeService.initialize(options);
|
this.codeRuntimeService.initialize(options.codeRuntime ?? {});
|
||||||
|
|
||||||
// init intl
|
this.extensionHostService.registerPlugin(plugins);
|
||||||
const finalLocale = options.locale ?? navigator.language;
|
|
||||||
const i18nTranslations = this.schemaService.get('i18n') ?? {};
|
|
||||||
|
|
||||||
this.runtimeIntlService.initialize(finalLocale, i18nTranslations);
|
this.lifeCycleService.phase = LifecyclePhase.OptionsResolved;
|
||||||
}
|
}
|
||||||
|
|
||||||
async startup<Render>(adapter: RenderAdapter<Render>): Promise<RendererApplication<Render>> {
|
async getApp(): Promise<RendererApplication<RenderObject>> {
|
||||||
const render = await this.extensionHostService.runRender<Render>(adapter);
|
await this.lifeCycleService.when(LifecyclePhase.Ready);
|
||||||
|
|
||||||
// construct application
|
// construct application
|
||||||
const app = Object.freeze<RendererApplication<Render>>({
|
return Object.freeze<RendererApplication<RenderObject>>({
|
||||||
|
// develop use
|
||||||
|
__options: this.initOptions,
|
||||||
|
|
||||||
mode: this.mode,
|
mode: this.mode,
|
||||||
schema: this.schemaService,
|
schema: this.schemaService,
|
||||||
packageManager: this.packageManagementService,
|
packageManager: this.packageManagementService,
|
||||||
...render,
|
...this.renderObject,
|
||||||
|
|
||||||
use: (plugin) => {
|
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();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
@ -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,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,2 +0,0 @@
|
|||||||
export * from './treeModel';
|
|
||||||
export * from './treeModelService';
|
|
||||||
@ -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[];
|
|
||||||
}
|
|
||||||
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,8 +1,8 @@
|
|||||||
import { createDecorator, Provide, type PlainObject } from '@alilc/lowcode-shared';
|
import { createDecorator, Provide, type PlainObject } from '@alilc/lowcode-shared';
|
||||||
import { isObject } from 'lodash-es';
|
import { isObject } from 'lodash-es';
|
||||||
import { ICodeRuntimeService } from '../code-runtime';
|
import { ICodeRuntimeService } from '../code-runtime';
|
||||||
import { IRuntimeUtilService } from '../runtimeUtil';
|
import { IRuntimeUtilService } from '../runtimeUtilService';
|
||||||
import { IRuntimeIntlService } from '../runtimeIntl';
|
import { IRuntimeIntlService } from '../runtimeIntlService';
|
||||||
|
|
||||||
export type IBoosts<Extends> = IBoostsApi & Extends;
|
export type IBoosts<Extends> = IBoostsApi & Extends;
|
||||||
|
|
||||||
@ -1,33 +1,23 @@
|
|||||||
import {
|
import { createDecorator, Provide, EventEmitter, KeyValueStore } from '@alilc/lowcode-shared';
|
||||||
invariant,
|
import { type Plugin, type PluginContext } from './plugin';
|
||||||
createDecorator,
|
|
||||||
Provide,
|
|
||||||
EventEmitter,
|
|
||||||
KeyValueStore,
|
|
||||||
} from '@alilc/lowcode-shared';
|
|
||||||
import { type Plugin } from './plugin';
|
|
||||||
import { IBoostsService } from './boosts';
|
import { IBoostsService } from './boosts';
|
||||||
import { IPackageManagementService } from '../package';
|
import { IPackageManagementService } from '../package';
|
||||||
import { ISchemaService } from '../schema';
|
import { ISchemaService } from '../schema';
|
||||||
import { type RenderAdapter } from './render';
|
import { type RenderAdapter } from './render';
|
||||||
import { IComponentTreeModelService } from '../component-tree-model';
|
import { IComponentTreeModelService } from '../model';
|
||||||
import type { RendererApplication } from '../../types';
|
import { ILifeCycleService, LifecyclePhase } from '../lifeCycleService';
|
||||||
|
|
||||||
interface IPluginRuntime extends Plugin {
|
interface IPluginRuntime extends Plugin {
|
||||||
status: 'setup' | 'ready';
|
status: 'setup' | 'ready';
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IExtensionHostService {
|
export interface IExtensionHostService {
|
||||||
initialize(app: RendererApplication): void;
|
registerPlugin(plugin: Plugin | Plugin[]): void;
|
||||||
|
|
||||||
/* ========= plugin ============= */
|
doSetupPlugin(plugin: Plugin): Promise<void>;
|
||||||
registerPlugin(plugin: Plugin | Plugin[]): Promise<void>;
|
|
||||||
|
|
||||||
getPlugin(name: string): Plugin | undefined;
|
getPlugin(name: string): Plugin | undefined;
|
||||||
|
|
||||||
/* =========== render =============== */
|
|
||||||
runRender<Render>(adapter: RenderAdapter<Render>): Promise<Render>;
|
|
||||||
|
|
||||||
dispose(): Promise<void>;
|
dispose(): Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -38,24 +28,37 @@ export const IExtensionHostService =
|
|||||||
export class ExtensionHostService implements IExtensionHostService {
|
export class ExtensionHostService implements IExtensionHostService {
|
||||||
private pluginRuntimes: IPluginRuntime[] = [];
|
private pluginRuntimes: IPluginRuntime[] = [];
|
||||||
|
|
||||||
private app: RendererApplication;
|
private eventEmitter: EventEmitter;
|
||||||
|
|
||||||
private eventEmitter = new EventEmitter();
|
private pluginSetupContext: PluginContext;
|
||||||
|
|
||||||
private globalState = new KeyValueStore();
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@IPackageManagementService private packageManagementService: IPackageManagementService,
|
@IPackageManagementService private packageManagementService: IPackageManagementService,
|
||||||
@IBoostsService private boostsService: IBoostsService,
|
@IBoostsService private boostsService: IBoostsService,
|
||||||
@ISchemaService private schemaService: ISchemaService,
|
@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) {
|
whenLifeCylePhaseChange: (phase) => {
|
||||||
this.app = app;
|
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];
|
plugins = Array.isArray(plugins) ? plugins : [plugins];
|
||||||
|
|
||||||
for (const plugin of plugins) {
|
for (const plugin of plugins) {
|
||||||
@ -64,39 +67,18 @@ export class ExtensionHostService implements IExtensionHostService {
|
|||||||
continue;
|
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;
|
const pluginRuntime = plugin as IPluginRuntime;
|
||||||
|
|
||||||
if (!this.pluginRuntimes.some((item) => item.name !== pluginRuntime.name)) {
|
if (!this.pluginRuntimes.some((item) => item.name !== pluginRuntime.name)) {
|
||||||
this.pluginRuntimes.push({
|
return;
|
||||||
...pluginRuntime,
|
|
||||||
status: 'ready',
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const isSetup = (name: string) => {
|
const isSetup = (name: string) => {
|
||||||
@ -108,11 +90,7 @@ export class ExtensionHostService implements IExtensionHostService {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await pluginRuntime.setup(this.app, {
|
await pluginRuntime.setup(this.pluginSetupContext);
|
||||||
eventEmitter: this.eventEmitter,
|
|
||||||
globalState: this.globalState,
|
|
||||||
boosts: this.boostsService.toExpose(),
|
|
||||||
});
|
|
||||||
pluginRuntime.status = 'setup';
|
pluginRuntime.status = 'setup';
|
||||||
|
|
||||||
// 遍历未安装的插件 寻找 dependsOn 的插件已安装完的插件进行安装
|
// 遍历未安装的插件 寻找 dependsOn 的插件已安装完的插件进行安装
|
||||||
@ -122,4 +100,14 @@ export class ExtensionHostService implements IExtensionHostService {
|
|||||||
await this.doSetupPlugin(readyPlugin);
|
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?.();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
27
packages/renderer-core/src/services/extension/plugin.ts
Normal file
27
packages/renderer-core/src/services/extension/plugin.ts
Normal 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[];
|
||||||
|
}
|
||||||
@ -1,10 +1,11 @@
|
|||||||
import { IPackageManagementService } from '../package';
|
import { IPackageManagementService } from '../package';
|
||||||
import { IBoostsService } from './boosts';
|
import { IBoostsService } from './boosts';
|
||||||
import { ISchemaService } from '../schema';
|
import { ISchemaService } from '../schema';
|
||||||
import { IComponentTreeModelService } from '../component-tree-model';
|
import { IComponentTreeModelService } from '../model';
|
||||||
|
import { ILifeCycleService } from '../lifeCycleService';
|
||||||
|
|
||||||
export interface IRender {
|
export interface IRenderObject {
|
||||||
mount: (el: HTMLElement) => void | Promise<void>;
|
mount: (containerOrId?: string | HTMLElement) => void | Promise<void>;
|
||||||
unmount: () => void | Promise<void>;
|
unmount: () => void | Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -16,6 +17,8 @@ export interface RenderContext {
|
|||||||
readonly boostsManager: IBoostsService;
|
readonly boostsManager: IBoostsService;
|
||||||
|
|
||||||
readonly componentTreeModel: IComponentTreeModelService;
|
readonly componentTreeModel: IComponentTreeModelService;
|
||||||
|
|
||||||
|
readonly lifeCycle: ILifeCycleService;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RenderAdapter<Render> {
|
export interface RenderAdapter<Render> {
|
||||||
75
packages/renderer-core/src/services/lifeCycleService.ts
Normal file
75
packages/renderer-core/src/services/lifeCycleService.ts
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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 { type ICodeScope, type ICodeRuntimeService } from '../code-runtime';
|
||||||
import { IWidget, Widget } from '../widget';
|
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> {
|
export interface IComponentTreeModel<Component, ComponentInstance = unknown> {
|
||||||
|
readonly id: string;
|
||||||
|
|
||||||
readonly codeScope: ICodeScope;
|
readonly codeScope: ICodeScope;
|
||||||
|
|
||||||
readonly codeRuntime: ICodeRuntimeService;
|
readonly codeRuntime: ICodeRuntimeService;
|
||||||
|
|
||||||
readonly widgets: IWidget<Component, ComponentInstance>[];
|
readonly widgets: IWidget<Component, ComponentInstance>[];
|
||||||
|
|
||||||
|
initialize(options: InitializeModelOptions): void;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取协议中的 css 内容
|
* 获取协议中的 css 内容
|
||||||
*/
|
*/
|
||||||
@ -37,11 +60,6 @@ export interface IComponentTreeModel<Component, ComponentInstance = unknown> {
|
|||||||
export type ModelScopeStateCreator = (initalState: PlainObject) => Spec.InstanceStateApi;
|
export type ModelScopeStateCreator = (initalState: PlainObject) => Spec.InstanceStateApi;
|
||||||
export type ModelScopeDataSourceCreator = (...args: any[]) => Spec.InstanceDataSourceApi;
|
export type ModelScopeDataSourceCreator = (...args: any[]) => Spec.InstanceDataSourceApi;
|
||||||
|
|
||||||
export interface ComponentTreeModelOptions {
|
|
||||||
stateCreator: ModelScopeStateCreator;
|
|
||||||
dataSourceCreator: ModelScopeDataSourceCreator;
|
|
||||||
}
|
|
||||||
|
|
||||||
const defaultDataSourceSchema: Spec.ComponentDataSource = {
|
const defaultDataSourceSchema: Spec.ComponentDataSource = {
|
||||||
list: [],
|
list: [],
|
||||||
dataHandler: {
|
dataHandler: {
|
||||||
@ -50,44 +68,60 @@ const defaultDataSourceSchema: Spec.ComponentDataSource = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export interface ComponentTreeModelOptions {
|
||||||
|
id?: string;
|
||||||
|
metadata?: PlainObject;
|
||||||
|
}
|
||||||
|
|
||||||
export class ComponentTreeModel<Component, ComponentInstance = unknown>
|
export class ComponentTreeModel<Component, ComponentInstance = unknown>
|
||||||
implements IComponentTreeModel<Component, ComponentInstance>
|
implements IComponentTreeModel<Component, ComponentInstance>
|
||||||
{
|
{
|
||||||
private instanceMap = new Map<string, ComponentInstance[]>();
|
private instanceMap = new Map<string, ComponentInstance[]>();
|
||||||
|
|
||||||
|
public id: string;
|
||||||
|
|
||||||
public codeScope: ICodeScope;
|
public codeScope: ICodeScope;
|
||||||
|
|
||||||
public widgets: IWidget<Component>[] = [];
|
public widgets: IWidget<Component>[] = [];
|
||||||
|
|
||||||
|
public metadata: PlainObject = {};
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
public componentsTree: Spec.ComponentTree,
|
public componentsTree: Spec.ComponentTree,
|
||||||
public codeRuntime: ICodeRuntimeService,
|
public codeRuntime: ICodeRuntimeService,
|
||||||
options: ComponentTreeModelOptions,
|
options?: ComponentTreeModelOptions,
|
||||||
) {
|
) {
|
||||||
invariant(componentsTree, 'componentsTree must to provide', 'ComponentTreeModel');
|
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) {
|
if (componentsTree.children) {
|
||||||
this.widgets = this.buildWidgets(componentsTree.children);
|
this.widgets = this.buildWidgets(componentsTree.children);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private initModelScope(
|
initialize({ defaultProps, stateCreator, dataSourceCreator }: InitializeModelOptions) {
|
||||||
stateCreator: ModelScopeStateCreator,
|
|
||||||
dataSourceCreator: ModelScopeDataSourceCreator,
|
|
||||||
) {
|
|
||||||
const {
|
const {
|
||||||
state = {},
|
state = {},
|
||||||
|
defaultProps: defaultSchemaProps,
|
||||||
props = {},
|
props = {},
|
||||||
dataSource = defaultDataSourceSchema,
|
dataSource = defaultDataSourceSchema,
|
||||||
methods = {},
|
methods = {},
|
||||||
} = this.componentsTree;
|
} = 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 initalState = this.codeRuntime.resolve(state, { scope: this.codeScope });
|
||||||
const initalProps = this.codeRuntime.resolve(props, this.codeScope);
|
const initalProps = this.codeRuntime.resolve(props, { scope: this.codeScope });
|
||||||
|
|
||||||
const stateApi = stateCreator(initalState);
|
const stateApi = stateCreator(initalState);
|
||||||
const dataSourceApi = dataSourceCreator(dataSource, stateApi);
|
const dataSourceApi = dataSourceCreator(dataSource, stateApi);
|
||||||
@ -95,7 +129,7 @@ export class ComponentTreeModel<Component, ComponentInstance = unknown>
|
|||||||
this.codeScope.setValue(
|
this.codeScope.setValue(
|
||||||
Object.assign(
|
Object.assign(
|
||||||
{
|
{
|
||||||
props: initalProps,
|
props: { ...defaultProps, ...initalProps },
|
||||||
$: (ref: string) => {
|
$: (ref: string) => {
|
||||||
const insArr = this.instanceMap.get(ref);
|
const insArr = this.instanceMap.get(ref);
|
||||||
if (!insArr) return undefined;
|
if (!insArr) return undefined;
|
||||||
@ -111,9 +145,9 @@ export class ComponentTreeModel<Component, ComponentInstance = unknown>
|
|||||||
);
|
);
|
||||||
|
|
||||||
for (const [key, fn] of Object.entries(methods)) {
|
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') {
|
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 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') {
|
if (typeof lifeCycleFn === 'function') {
|
||||||
lifeCycleFn.apply(this.codeScope.value, args);
|
lifeCycleFn.apply(this.codeScope.value, args);
|
||||||
}
|
}
|
||||||
@ -162,13 +196,31 @@ export class ComponentTreeModel<Component, ComponentInstance = unknown>
|
|||||||
|
|
||||||
buildWidgets(nodes: Spec.NodeType[]): IWidget<Component>[] {
|
buildWidgets(nodes: Spec.NodeType[]): IWidget<Component>[] {
|
||||||
return nodes.map((node) => {
|
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) {
|
if (normalized.children?.length) {
|
||||||
widget.children = this.buildWidgets(node.children);
|
widget.children = this.buildWidgets(normalized.children);
|
||||||
}
|
}
|
||||||
|
|
||||||
return widget;
|
return widget;
|
||||||
|
} else {
|
||||||
|
return new Widget<Component, ComponentInstance>(node, this);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
}
|
||||||
@ -4,18 +4,18 @@ import {
|
|||||||
type IComponentTreeModel,
|
type IComponentTreeModel,
|
||||||
ComponentTreeModel,
|
ComponentTreeModel,
|
||||||
type ComponentTreeModelOptions,
|
type ComponentTreeModelOptions,
|
||||||
} from './treeModel';
|
} from './componentTreeModel';
|
||||||
import { ISchemaService } from '../schema';
|
import { ISchemaService } from '../schema';
|
||||||
|
|
||||||
export interface IComponentTreeModelService {
|
export interface IComponentTreeModelService {
|
||||||
create<Component>(
|
create<Component>(
|
||||||
componentsTree: Spec.ComponentTree,
|
componentsTree: Spec.ComponentTree,
|
||||||
options: ComponentTreeModelOptions,
|
options?: ComponentTreeModelOptions,
|
||||||
): IComponentTreeModel<Component>;
|
): IComponentTreeModel<Component>;
|
||||||
|
|
||||||
createById<Component>(
|
createById<Component>(
|
||||||
id: string,
|
id: string,
|
||||||
options: ComponentTreeModelOptions,
|
options?: ComponentTreeModelOptions,
|
||||||
): IComponentTreeModel<Component>;
|
): IComponentTreeModel<Component>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -32,14 +32,14 @@ export class ComponentTreeModelService implements IComponentTreeModelService {
|
|||||||
|
|
||||||
create<Component>(
|
create<Component>(
|
||||||
componentsTree: Spec.ComponentTree,
|
componentsTree: Spec.ComponentTree,
|
||||||
options: ComponentTreeModelOptions,
|
options?: ComponentTreeModelOptions,
|
||||||
): IComponentTreeModel<Component> {
|
): IComponentTreeModel<Component> {
|
||||||
return new ComponentTreeModel(componentsTree, this.codeRuntimeService, options);
|
return new ComponentTreeModel(componentsTree, this.codeRuntimeService, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
createById<Component>(
|
createById<Component>(
|
||||||
id: string,
|
id: string,
|
||||||
options: ComponentTreeModelOptions,
|
options?: ComponentTreeModelOptions,
|
||||||
): IComponentTreeModel<Component> {
|
): IComponentTreeModel<Component> {
|
||||||
const componentsTrees = this.schemaService.get('componentsTree');
|
const componentsTrees = this.schemaService.get('componentsTree');
|
||||||
const componentsTree = componentsTrees.find((item) => item.id === id);
|
const componentsTree = componentsTrees.find((item) => item.id === id);
|
||||||
2
packages/renderer-core/src/services/model/index.ts
Normal file
2
packages/renderer-core/src/services/model/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from './componentTreeModel';
|
||||||
|
export * from './componentTreeModelService';
|
||||||
@ -9,6 +9,8 @@ import {
|
|||||||
} from '@alilc/lowcode-shared';
|
} from '@alilc/lowcode-shared';
|
||||||
import { get as lodashGet } from 'lodash-es';
|
import { get as lodashGet } from 'lodash-es';
|
||||||
import { PackageLoader } from './loader';
|
import { PackageLoader } from './loader';
|
||||||
|
import { ISchemaService } from '../schema';
|
||||||
|
import { ILifeCycleService, LifecyclePhase } from '../lifeCycleService';
|
||||||
|
|
||||||
export interface NormalizedPackage {
|
export interface NormalizedPackage {
|
||||||
id: string;
|
id: string;
|
||||||
@ -62,6 +64,16 @@ export class PackageManagementService implements IPackageManagementService {
|
|||||||
|
|
||||||
private packageLoaders: PackageLoader[] = [];
|
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[]) {
|
async loadPackages(packages: Spec.Package[]) {
|
||||||
for (const item of packages) {
|
for (const item of packages) {
|
||||||
// low code component not need load
|
// low code component not need load
|
||||||
@ -99,7 +111,7 @@ export class PackageManagementService implements IPackageManagementService {
|
|||||||
if (map.devMode === 'lowCode') {
|
if (map.devMode === 'lowCode') {
|
||||||
const packageInfo = this.lowCodeComponentPackages.get((map as LowCodeComponent).id);
|
const packageInfo = this.lowCodeComponentPackages.get((map as LowCodeComponent).id);
|
||||||
|
|
||||||
if (packageInfo) {
|
if (map.componentName && packageInfo) {
|
||||||
this.componentsRecord[map.componentName] = packageInfo;
|
this.componentsRecord[map.componentName] = packageInfo;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -123,7 +135,7 @@ export class PackageManagementService implements IPackageManagementService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const recordName = map.componentName ?? map.exportName;
|
const recordName = map.componentName ?? map.exportName;
|
||||||
this.componentsRecord[recordName] = result;
|
if (recordName && result) this.componentsRecord[recordName] = result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
102
packages/renderer-core/src/services/runtimeIntlService.ts
Normal file
102
packages/renderer-core/src/services/runtimeIntlService.ts
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,14 +1,14 @@
|
|||||||
import { type AnyFunction, type Spec, createDecorator, Provide } from '@alilc/lowcode-shared';
|
import { type AnyFunction, type Spec, createDecorator, Provide } from '@alilc/lowcode-shared';
|
||||||
import { IPackageManagementService } from './package';
|
import { IPackageManagementService } from './package';
|
||||||
import { ICodeRuntimeService } from './code-runtime';
|
import { ICodeRuntimeService } from './code-runtime';
|
||||||
|
import { ILifeCycleService, LifecyclePhase } from './lifeCycleService';
|
||||||
|
import { ISchemaService } from './schema';
|
||||||
|
|
||||||
export interface IRuntimeUtilService {
|
export interface IRuntimeUtilService {
|
||||||
add(utilItem: Spec.Util): void;
|
add(utilItem: Spec.Util): void;
|
||||||
add(name: string, fn: AnyFunction): void;
|
add(name: string, fn: AnyFunction): void;
|
||||||
|
|
||||||
remove(name: string): void;
|
remove(name: string): void;
|
||||||
|
|
||||||
toExpose(): Spec.UtilsApi;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const IRuntimeUtilService = createDecorator<IRuntimeUtilService>('rendererUtilService');
|
export const IRuntimeUtilService = createDecorator<IRuntimeUtilService>('rendererUtilService');
|
||||||
@ -17,12 +17,21 @@ export const IRuntimeUtilService = createDecorator<IRuntimeUtilService>('rendere
|
|||||||
export class RuntimeUtilService implements IRuntimeUtilService {
|
export class RuntimeUtilService implements IRuntimeUtilService {
|
||||||
private utilsMap: Map<string, AnyFunction> = new Map();
|
private utilsMap: Map<string, AnyFunction> = new Map();
|
||||||
|
|
||||||
private _expose: any;
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@ICodeRuntimeService private codeRuntimeService: ICodeRuntimeService,
|
@ICodeRuntimeService private codeRuntimeService: ICodeRuntimeService,
|
||||||
@IPackageManagementService private packageManagementService: IPackageManagementService,
|
@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(utilItem: Spec.Util): void;
|
||||||
add(name: string, fn: AnyFunction): void;
|
add(name: string, fn: AnyFunction): void;
|
||||||
@ -41,9 +50,8 @@ export class RuntimeUtilService implements IRuntimeUtilService {
|
|||||||
this.utilsMap.delete(name);
|
this.utilsMap.delete(name);
|
||||||
}
|
}
|
||||||
|
|
||||||
toExpose(): Spec.UtilsApi {
|
private toExpose(): void {
|
||||||
if (!this._expose) {
|
const exposed = new Proxy(Object.create(null), {
|
||||||
this._expose = new Proxy(Object.create(null), {
|
|
||||||
get: (_, p: string) => {
|
get: (_, p: string) => {
|
||||||
return this.utilsMap.get(p);
|
return this.utilsMap.get(p);
|
||||||
},
|
},
|
||||||
@ -54,9 +62,8 @@ export class RuntimeUtilService implements IRuntimeUtilService {
|
|||||||
return this.utilsMap.has(p);
|
return this.utilsMap.has(p);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
return this._expose;
|
this.codeRuntimeService.getScope().set('utils', exposed);
|
||||||
}
|
}
|
||||||
|
|
||||||
private parseUtil(utilItem: Spec.Util) {
|
private parseUtil(utilItem: Spec.Util) {
|
||||||
@ -2,11 +2,13 @@ import {
|
|||||||
type Spec,
|
type Spec,
|
||||||
createDecorator,
|
createDecorator,
|
||||||
Provide,
|
Provide,
|
||||||
|
type IStore,
|
||||||
KeyValueStore,
|
KeyValueStore,
|
||||||
type EventDisposable,
|
|
||||||
} from '@alilc/lowcode-shared';
|
} from '@alilc/lowcode-shared';
|
||||||
import { isObject } from 'lodash-es';
|
import { isObject } from 'lodash-es';
|
||||||
import { schemaValidation } from './validation';
|
import { schemaValidation } from './validation';
|
||||||
|
import { ILifeCycleService, LifecyclePhase } from '../lifeCycleService';
|
||||||
|
import { ICodeRuntimeService } from '../code-runtime';
|
||||||
|
|
||||||
export interface NormalizedSchema extends Spec.Project {}
|
export interface NormalizedSchema extends Spec.Project {}
|
||||||
|
|
||||||
@ -18,23 +20,27 @@ export interface ISchemaService {
|
|||||||
get<K extends NormalizedSchemaKey>(key: K): NormalizedSchema[K];
|
get<K extends NormalizedSchemaKey>(key: K): NormalizedSchema[K];
|
||||||
|
|
||||||
set<K extends NormalizedSchemaKey>(key: K, value: NormalizedSchema[K]): void;
|
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');
|
export const ISchemaService = createDecorator<ISchemaService>('schemaService');
|
||||||
|
|
||||||
@Provide(ISchemaService)
|
@Provide(ISchemaService)
|
||||||
export class SchemaService implements ISchemaService {
|
export class SchemaService implements ISchemaService {
|
||||||
private store: KeyValueStore<NormalizedSchema, NormalizedSchemaKey>;
|
private store: IStore<NormalizedSchema, NormalizedSchemaKey> = new KeyValueStore<
|
||||||
|
NormalizedSchema,
|
||||||
constructor() {
|
NormalizedSchemaKey
|
||||||
this.store = new KeyValueStore<NormalizedSchema, NormalizedSchemaKey>(new Map(), {
|
>({
|
||||||
setterValidation: schemaValidation,
|
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);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
initialize(schema: unknown): void {
|
initialize(schema: unknown): void {
|
||||||
@ -55,11 +61,4 @@ export class SchemaService implements ISchemaService {
|
|||||||
get<K extends NormalizedSchemaKey>(key: K): NormalizedSchema[K] {
|
get<K extends NormalizedSchemaKey>(key: K): NormalizedSchema[K] {
|
||||||
return this.store.get(key) as 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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
34
packages/renderer-core/src/services/widget/widget.ts
Normal file
34
packages/renderer-core/src/services/widget/widget.ts
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,26 +1,23 @@
|
|||||||
import { type Spec } from '@alilc/lowcode-shared';
|
import { type Spec } from '@alilc/lowcode-shared';
|
||||||
import { type Plugin } from './parts/extension';
|
import { type Plugin } from './services/extension';
|
||||||
import { type ISchemaService } from './parts/schema';
|
import { type ISchemaService } from './services/schema';
|
||||||
import { type IPackageManagementService } from './parts/package';
|
import { type IPackageManagementService } from './services/package';
|
||||||
import { type IExtensionHostService } from './parts/extension';
|
import { type CodeRuntimeInitializeOptions } from './services/code-runtime';
|
||||||
import { type EvalCodeFunction } from './parts/code-runtime';
|
|
||||||
|
|
||||||
export interface AppOptions {
|
export interface AppOptions {
|
||||||
schema: Spec.Project;
|
schema: Spec.Project;
|
||||||
packages?: Spec.Package[];
|
packages?: Spec.Package[];
|
||||||
plugins?: Plugin[];
|
plugins?: Plugin[];
|
||||||
|
|
||||||
/**
|
|
||||||
* 应用语言,默认值为浏览器当前语言 navigator.language
|
|
||||||
*/
|
|
||||||
locale?: string;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 运行模式
|
* 运行模式
|
||||||
*/
|
*/
|
||||||
mode?: 'development' | 'production';
|
mode?: 'development' | 'production';
|
||||||
|
|
||||||
evalCodeFunction?: EvalCodeFunction;
|
/**
|
||||||
|
* code runtime 设置选项
|
||||||
|
*/
|
||||||
|
codeRuntime?: CodeRuntimeInitializeOptions;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type RendererApplication<Render = unknown> = {
|
export type RendererApplication<Render = unknown> = {
|
||||||
@ -30,5 +27,5 @@ export type RendererApplication<Render = unknown> = {
|
|||||||
|
|
||||||
readonly packageManager: IPackageManagementService;
|
readonly packageManager: IPackageManagementService;
|
||||||
|
|
||||||
use: IExtensionHostService['registerPlugin'];
|
use(plugin: Plugin): Promise<void>;
|
||||||
} & Render;
|
} & Render;
|
||||||
|
|||||||
5
packages/renderer-core/src/utils/evaluate.ts
Normal file
5
packages/renderer-core/src/utils/evaluate.ts
Normal 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,
|
||||||
|
);
|
||||||
|
}
|
||||||
75
packages/renderer-core/src/utils/globals-es2015.ts
Normal file
75
packages/renderer-core/src/utils/globals-es2015.ts
Normal 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',
|
||||||
|
];
|
||||||
191
packages/renderer-core/src/utils/node.ts
Normal file
191
packages/renderer-core/src/utils/node.ts
Normal 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);
|
||||||
|
}
|
||||||
@ -1,40 +1,46 @@
|
|||||||
|
import { type PlainObject } from '@alilc/lowcode-shared';
|
||||||
import { isPlainObject, isEmpty } from 'lodash-es';
|
import { isPlainObject, isEmpty } from 'lodash-es';
|
||||||
|
|
||||||
export function someValue(obj: any, predicate: (data: any) => boolean) {
|
export function someValue(
|
||||||
|
obj: PlainObject | PlainObject[],
|
||||||
|
filter: (data: PlainObject) => boolean,
|
||||||
|
): boolean {
|
||||||
|
if (Array.isArray(obj)) {
|
||||||
|
return obj.some((item) => someValue(item, filter));
|
||||||
|
}
|
||||||
|
|
||||||
if (!isPlainObject(obj) || isEmpty(obj)) return false;
|
if (!isPlainObject(obj) || isEmpty(obj)) return false;
|
||||||
if (predicate(obj)) return true;
|
if (filter(obj)) return true;
|
||||||
|
|
||||||
for (const val of Object.values(obj)) {
|
return Object.values(obj).some((val) => someValue(val, filter));
|
||||||
if (someValue(val, predicate)) return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
export function mapValue(
|
||||||
}
|
obj: PlainObject,
|
||||||
|
filter: (obj: PlainObject) => boolean,
|
||||||
export function processValue(
|
callback: (node: any, paths: Array<string | number>) => any,
|
||||||
obj: any,
|
|
||||||
predicate: (obj: any) => boolean,
|
|
||||||
processor: (node: any, paths: Array<string | number>) => any,
|
|
||||||
): 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)) {
|
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 (!isPlainObject(target) || isEmpty(target)) return target;
|
||||||
if (!someValue(target, predicate)) return target;
|
|
||||||
|
|
||||||
if (predicate(target)) {
|
if (filter(target)) {
|
||||||
return processor(target, paths);
|
return callback(target, paths);
|
||||||
} else {
|
}
|
||||||
const result = {} as any;
|
|
||||||
|
const result: PlainObject = {};
|
||||||
|
|
||||||
for (const [key, value] of Object.entries(target)) {
|
for (const [key, value] of Object.entries(target)) {
|
||||||
result[key] = innerProcess(value, [...paths, key]);
|
result[key] = mapping(value, [...paths, key]);
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return innerProcess(obj, []);
|
return mapping(obj, []);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -42,6 +42,8 @@ export interface Router extends Spec.RouterApi {
|
|||||||
|
|
||||||
beforeRouteLeave: (fn: NavigationGuard) => () => void;
|
beforeRouteLeave: (fn: NavigationGuard) => () => void;
|
||||||
afterRouteChange: (fn: NavigationHookAfter) => () => void;
|
afterRouteChange: (fn: NavigationHookAfter) => () => void;
|
||||||
|
|
||||||
|
isReady(): Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const START_LOCATION: RouteLocationNormalized = {
|
const START_LOCATION: RouteLocationNormalized = {
|
||||||
@ -68,6 +70,7 @@ export function createRouter(options: RouterOptions): Router {
|
|||||||
|
|
||||||
const beforeGuards = createCallback<NavigationGuard>();
|
const beforeGuards = createCallback<NavigationGuard>();
|
||||||
const afterGuards = createCallback<NavigationHookAfter>();
|
const afterGuards = createCallback<NavigationHookAfter>();
|
||||||
|
const readyHandlers = createCallback<any>();
|
||||||
|
|
||||||
let currentLocation: RouteLocationNormalized = START_LOCATION;
|
let currentLocation: RouteLocationNormalized = START_LOCATION;
|
||||||
let pendingLocation = currentLocation;
|
let pendingLocation = currentLocation;
|
||||||
@ -203,7 +206,9 @@ export function createRouter(options: RouterOptions): Router {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return navigateTriggerBeforeGuards(toLocation, from)
|
return navigateTriggerBeforeGuards(toLocation, from)
|
||||||
.catch(() => {})
|
.catch((error) => {
|
||||||
|
return markAsReady(error);
|
||||||
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
finalizeNavigation(toLocation, from, true, replace, data);
|
finalizeNavigation(toLocation, from, true, replace, data);
|
||||||
|
|
||||||
@ -293,7 +298,7 @@ export function createRouter(options: RouterOptions): Router {
|
|||||||
}
|
}
|
||||||
|
|
||||||
currentLocation = toLocation;
|
currentLocation = toLocation;
|
||||||
// markAsReady();
|
markAsReady();
|
||||||
}
|
}
|
||||||
|
|
||||||
let removeHistoryListener: undefined | null | (() => void);
|
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
|
// init
|
||||||
setupListeners();
|
setupListeners();
|
||||||
if (currentLocation === START_LOCATION) {
|
if (currentLocation === START_LOCATION) {
|
||||||
@ -370,5 +401,7 @@ export function createRouter(options: RouterOptions): Router {
|
|||||||
|
|
||||||
beforeRouteLeave: beforeGuards.add,
|
beforeRouteLeave: beforeGuards.add,
|
||||||
afterRouteChange: afterGuards.add,
|
afterRouteChange: afterGuards.add,
|
||||||
|
|
||||||
|
isReady,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -15,7 +15,7 @@ export type Constructor<T = any> = new (...args: any[]) => T;
|
|||||||
export function createDecorator<T>(serviceId: string): ServiceIdentifier<T> {
|
export function createDecorator<T>(serviceId: string): ServiceIdentifier<T> {
|
||||||
const id = <any>(
|
const id = <any>(
|
||||||
function (target: Constructor, targetKey: string, indexOrPropertyDescriptor: any): any {
|
function (target: Constructor, targetKey: string, indexOrPropertyDescriptor: any): any {
|
||||||
return inject(serviceId)(target, targetKey, indexOrPropertyDescriptor);
|
return set(serviceId)(target, targetKey, indexOrPropertyDescriptor);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
id.toString = () => serviceId;
|
id.toString = () => serviceId;
|
||||||
|
|||||||
@ -14,7 +14,7 @@ export class Intl {
|
|||||||
private currentMessage: ComputedSignal<Translations>;
|
private currentMessage: ComputedSignal<Translations>;
|
||||||
private intlShape: IntlFormatter;
|
private intlShape: IntlFormatter;
|
||||||
|
|
||||||
constructor(defaultLocale?: string, messages: LocaleTranslationsRecord = {}) {
|
constructor(defaultLocale: string = navigator.language, messages: LocaleTranslationsRecord = {}) {
|
||||||
if (defaultLocale) {
|
if (defaultLocale) {
|
||||||
defaultLocale = nomarlizeLocale(defaultLocale);
|
defaultLocale = nomarlizeLocale(defaultLocale);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
import { invariant } from '../utils';
|
|
||||||
import { PlainObject } from '../types';
|
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 setterValidation: ((key: K, value: O[K]) => boolean | string) | undefined;
|
||||||
|
|
||||||
private waits = new Map<
|
constructor(options?: { setterValidation?: (key: K, value: O[K]) => boolean | string }) {
|
||||||
K,
|
|
||||||
{
|
|
||||||
once?: boolean;
|
|
||||||
resolve: (data: any) => void;
|
|
||||||
}[]
|
|
||||||
>();
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
private readonly store: IStore<O, K> = new Map(),
|
|
||||||
options?: {
|
|
||||||
setterValidation?: (key: K, value: O[K]) => boolean | string;
|
|
||||||
},
|
|
||||||
) {
|
|
||||||
if (options?.setterValidation) {
|
if (options?.setterValidation) {
|
||||||
this.setterValidation = 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]): O[K];
|
||||||
get(key: K, defaultValue?: O[K] | undefined): O[K] | undefined;
|
get(key: K, defaultValue?: O[K] | undefined): O[K] | undefined;
|
||||||
get(key: K, defaultValue?: O[K]): O[K] | undefined {
|
get(key: K, defaultValue?: O[K]): O[K] | undefined {
|
||||||
const value = this.store.get(key, defaultValue);
|
const value = this.store.get(key);
|
||||||
return value;
|
return value ?? defaultValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
set(key: K, value: O[K]): void {
|
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.store.set(key, value);
|
||||||
this.dispatchValue(key);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
delete(key: K): void {
|
delete(key: K): void {
|
||||||
@ -74,77 +61,4 @@ export class KeyValueStore<O = PlainObject, K extends keyof O = keyof O> {
|
|||||||
get size(): number {
|
get size(): number {
|
||||||
return this.store.size;
|
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,19 +9,31 @@ import {
|
|||||||
ref,
|
ref,
|
||||||
computed,
|
computed,
|
||||||
ReactiveEffect,
|
ReactiveEffect,
|
||||||
|
shallowRef,
|
||||||
|
type ShallowRef,
|
||||||
type ComputedRef,
|
type ComputedRef,
|
||||||
type Ref,
|
type Ref,
|
||||||
getCurrentScope,
|
getCurrentScope,
|
||||||
isRef,
|
isRef,
|
||||||
isReactive,
|
isReactive,
|
||||||
isShallow,
|
isShallow,
|
||||||
EffectScheduler,
|
readonly,
|
||||||
|
type EffectScheduler,
|
||||||
} from '@vue/reactivity';
|
} from '@vue/reactivity';
|
||||||
import { noop, isObject, isPlainObject, isSet, isMap, isFunction } from 'lodash-es';
|
import { noop, isObject, isPlainObject, isSet, isMap, isFunction } from 'lodash-es';
|
||||||
import { isPromise } from './utils';
|
import { isPromise } from './utils';
|
||||||
|
|
||||||
export { ref as signal, computed, watchEffect as effect, watch as reaction, isRef as isSignal };
|
export {
|
||||||
export type { Ref as Signal, ComputedRef as ComputedSignal };
|
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 WatchSource<T = any> = Ref<T> | ComputedRef<T> | (() => T);
|
||||||
export type WatchEffect = (onCleanup: OnCleanup) => void;
|
export type WatchEffect = (onCleanup: OnCleanup) => void;
|
||||||
|
|||||||
@ -40,7 +40,7 @@ export interface Project {
|
|||||||
/**
|
/**
|
||||||
* 当前应用配置信息
|
* 当前应用配置信息
|
||||||
*/
|
*/
|
||||||
config?: Record<string, JSONObject>;
|
config?: ProjectConfig;
|
||||||
/**
|
/**
|
||||||
* 当前应用元数据信息
|
* 当前应用元数据信息
|
||||||
*/
|
*/
|
||||||
@ -60,6 +60,32 @@ export interface Project {
|
|||||||
pages?: PageConfig[];
|
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
|
* 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 到公域组件映射关系的规范。
|
* 协议中用于描述 componentName 到公域组件映射关系的规范。
|
||||||
@ -259,7 +285,11 @@ export interface ComponentNode {
|
|||||||
*/
|
*/
|
||||||
componentName: string;
|
componentName: string;
|
||||||
/**
|
/**
|
||||||
* 组件属性对象
|
* 默认 props
|
||||||
|
*/
|
||||||
|
defaultProps?: JSONObject;
|
||||||
|
/**
|
||||||
|
* 组件 props 对象
|
||||||
*/
|
*/
|
||||||
props?: ComponentNodeProps;
|
props?: ComponentNodeProps;
|
||||||
/**
|
/**
|
||||||
@ -415,11 +445,16 @@ export interface JSONObject {
|
|||||||
[key: string]: JSONValue | JSONObject | JSONObject[];
|
[key: string]: JSONValue | JSONObject | JSONObject[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface JSNode {
|
||||||
|
type: string;
|
||||||
|
[key: string]: any;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 节点类型(A)
|
* 节点类型(A)
|
||||||
* 通常用于描述组件的某一个属性为 Node 或 Function-Return-Node 的场景。
|
* 通常用于描述组件的某一个属性为 Node 或 Function-Return-Node 的场景。
|
||||||
*/
|
*/
|
||||||
export interface JSSlot {
|
export interface JSSlot extends JSNode {
|
||||||
type: 'JSSlot';
|
type: 'JSSlot';
|
||||||
value: ComponentNode | ComponentNode[];
|
value: ComponentNode | ComponentNode[];
|
||||||
params?: string[];
|
params?: string[];
|
||||||
@ -430,7 +465,7 @@ export interface JSSlot {
|
|||||||
/**
|
/**
|
||||||
* 事件函数类型(A)
|
* 事件函数类型(A)
|
||||||
*/
|
*/
|
||||||
export interface JSFunction {
|
export interface JSFunction extends JSNode {
|
||||||
type: 'JSFunction';
|
type: 'JSFunction';
|
||||||
value: string;
|
value: string;
|
||||||
|
|
||||||
@ -440,7 +475,7 @@ export interface JSFunction {
|
|||||||
/**
|
/**
|
||||||
* 变量类型(A)
|
* 变量类型(A)
|
||||||
*/
|
*/
|
||||||
export interface JSExpression {
|
export interface JSExpression extends JSNode {
|
||||||
type: 'JSExpression';
|
type: 'JSExpression';
|
||||||
value: string;
|
value: string;
|
||||||
|
|
||||||
@ -450,7 +485,7 @@ export interface JSExpression {
|
|||||||
/**
|
/**
|
||||||
* 国际化多语言类型(AA)
|
* 国际化多语言类型(AA)
|
||||||
*/
|
*/
|
||||||
export interface JSI18n {
|
export interface JSI18n extends JSNode {
|
||||||
type: 'i18n';
|
type: 'i18n';
|
||||||
/**
|
/**
|
||||||
* i18n 结构中字段的 key 标识符
|
* i18n 结构中字段的 key 标识符
|
||||||
@ -462,6 +497,4 @@ export interface JSI18n {
|
|||||||
params?: Record<string, string | number | JSExpression>;
|
params?: Record<string, string | number | JSExpression>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type JSNode = JSSlot | JSExpression | JSExpression | JSI18n;
|
|
||||||
|
|
||||||
export type NodeType = string | JSExpression | JSI18n | ComponentNode;
|
export type NodeType = string | JSExpression | JSI18n | ComponentNode;
|
||||||
|
|||||||
46
packages/shared/src/utils/async.ts
Normal file
46
packages/shared/src/utils/async.ts
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -4,3 +4,4 @@ export * from './unique-id';
|
|||||||
export * from './type-guards';
|
export * from './type-guards';
|
||||||
export * from './platform';
|
export * from './platform';
|
||||||
export * from './callback';
|
export * from './callback';
|
||||||
|
export * from './async';
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user