mirror of
https://github.com/alibaba/lowcode-engine.git
synced 2025-12-14 04:43:14 +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",
|
||||
"use-sync-external-store": "^1.2.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0"
|
||||
"react-dom": "^18.2.0",
|
||||
"react-is": "^18.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@testing-library/react": "^14.2.0",
|
||||
@ -38,7 +39,8 @@
|
||||
"@types/hoist-non-react-statics": "^3.3.5",
|
||||
"@types/use-sync-external-store": "^0.0.6",
|
||||
"@types/react": "^18.2.67",
|
||||
"@types/react-dom": "^18.2.22"
|
||||
"@types/react-dom": "^18.2.22",
|
||||
"@types/react-is": "^18.3.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^18.2.0",
|
||||
|
||||
@ -1,42 +1,15 @@
|
||||
import { createRenderer, type AppOptions, type IRender } from '@alilc/lowcode-renderer-core';
|
||||
import { createRenderer, type AppOptions } from '@alilc/lowcode-renderer-core';
|
||||
import { type ComponentType } from 'react';
|
||||
import { type Root, createRoot } from 'react-dom/client';
|
||||
import { createRouter, type RouterOptions } from '@alilc/lowcode-renderer-router';
|
||||
import AppComponent from '../components/app';
|
||||
import { RendererContext } from '../context/render';
|
||||
import { createRouterProvider } from '../components/routerView';
|
||||
import { rendererExtends } from '../plugin';
|
||||
import { ApplicationView, RendererContext, extension } from '../app';
|
||||
|
||||
export interface ReactAppOptions extends AppOptions {
|
||||
faultComponent?: ComponentType<any>;
|
||||
}
|
||||
|
||||
const defaultRouterOptions: RouterOptions = {
|
||||
historyMode: 'browser',
|
||||
baseName: '/',
|
||||
routes: [],
|
||||
};
|
||||
|
||||
export const createApp = async (options: ReactAppOptions) => {
|
||||
const creator = createRenderer<IRender>(async (context) => {
|
||||
return createRenderer(async (context) => {
|
||||
const { schema, boostsManager } = context;
|
||||
const boosts = boostsManager.toExpose();
|
||||
|
||||
// router
|
||||
let routerConfig = defaultRouterOptions;
|
||||
|
||||
try {
|
||||
const routerSchema = schema.get('router');
|
||||
if (routerSchema) {
|
||||
routerConfig = boosts.codeRuntime.resolve(routerSchema);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(`schema's router config is resolve error: `, e);
|
||||
}
|
||||
|
||||
const router = createRouter(routerConfig);
|
||||
|
||||
boosts.codeRuntime.getScope().inject('router', router);
|
||||
|
||||
// set config
|
||||
// if (options.faultComponent) {
|
||||
@ -44,24 +17,21 @@ export const createApp = async (options: ReactAppOptions) => {
|
||||
// }
|
||||
|
||||
// extends boosts
|
||||
boostsManager.extend(rendererExtends);
|
||||
|
||||
const RouterProvider = createRouterProvider(router);
|
||||
extension.install(boostsManager);
|
||||
|
||||
let root: Root | undefined;
|
||||
|
||||
return {
|
||||
async mount(el) {
|
||||
if (root) {
|
||||
return;
|
||||
}
|
||||
async mount(containerOrId) {
|
||||
if (root) return;
|
||||
|
||||
root = createRoot(el);
|
||||
const defaultId = schema.get('config')?.targetRootID ?? 'app';
|
||||
const rootElement = normalizeContainer(containerOrId, defaultId);
|
||||
|
||||
root = createRoot(rootElement);
|
||||
root.render(
|
||||
<RendererContext.Provider value={context}>
|
||||
<RouterProvider>
|
||||
<AppComponent />
|
||||
</RouterProvider>
|
||||
<ApplicationView />
|
||||
</RendererContext.Provider>,
|
||||
);
|
||||
},
|
||||
@ -72,7 +42,23 @@ export const createApp = async (options: ReactAppOptions) => {
|
||||
}
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
return creator(options);
|
||||
})(options);
|
||||
};
|
||||
|
||||
function normalizeContainer(container: Element | string | undefined, defaultId: string): Element {
|
||||
let result: Element | undefined = undefined;
|
||||
|
||||
if (typeof container === 'string') {
|
||||
const el = document.getElementById(container);
|
||||
if (el) result = el;
|
||||
} else if (container instanceof window.Element) {
|
||||
result = container;
|
||||
}
|
||||
|
||||
if (!result) {
|
||||
result = document.createElement('div');
|
||||
result.id = defaultId;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { createRenderer, type AppOptions } from '@alilc/lowcode-renderer-core';
|
||||
import { FunctionComponent } from 'react';
|
||||
import { type LowCodeComponentProps, createComponentBySchema } from '../runtime';
|
||||
import { RendererContext } from '../context/render';
|
||||
import { type LowCodeComponentProps, createComponentBySchema } from '../runtime/component';
|
||||
import { RendererContext } from '../app/context';
|
||||
|
||||
interface Render {
|
||||
toComponent(): FunctionComponent<LowCodeComponentProps>;
|
||||
|
||||
@ -3,6 +3,6 @@ import { type RenderContext } from '@alilc/lowcode-renderer-core';
|
||||
|
||||
export const RendererContext = createContext<RenderContext>(undefined!);
|
||||
|
||||
RendererContext.displayName = 'RootContext';
|
||||
RendererContext.displayName = 'RendererContext';
|
||||
|
||||
export const useRenderContext = () => useContext(RendererContext);
|
||||
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/component';
|
||||
export { defineRendererPlugin } from './plugin';
|
||||
export * from './context/render';
|
||||
export * from './context/router';
|
||||
export { useRenderContext, defineRendererPlugin } from './app';
|
||||
export * from './router';
|
||||
|
||||
export type { Spec, ProCodeComponent, LowCodeComponent } from '@alilc/lowcode-shared';
|
||||
export type { PackageLoader, CodeScope, Plugin } from '@alilc/lowcode-renderer-core';
|
||||
export type { RendererExtends } from './plugin';
|
||||
export type { RendererExtends } from './app/extension';
|
||||
|
||||
@ -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 Spec } from '@alilc/lowcode-shared';
|
||||
import { createContext, useContext } from 'react';
|
||||
|
||||
export const RouterContext = createContext<Router>(undefined!);
|
||||
@ -13,9 +12,3 @@ export const RouteLocationContext = createContext<RouteLocationNormalized>(undef
|
||||
RouteLocationContext.displayName = 'RouteLocationContext';
|
||||
|
||||
export const useRouteLocation = () => useContext(RouteLocationContext);
|
||||
|
||||
export const PageConfigContext = createContext<Spec.PageConfig | undefined>(undefined);
|
||||
|
||||
PageConfigContext.displayName = 'PageConfigContext';
|
||||
|
||||
export const usePageConfig = () => useContext(PageConfigContext);
|
||||
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 { isPlainObject } from 'lodash-es';
|
||||
|
||||
export function reactiveStateCreator(initState: PlainObject): Spec.InstanceStateApi {
|
||||
export function reactiveStateFactory(initState: PlainObject): Spec.InstanceStateApi {
|
||||
const proxyState = signal(initState);
|
||||
|
||||
return {
|
||||
@ -13,10 +13,9 @@ export function reactiveStateCreator(initState: PlainObject): Spec.InstanceState
|
||||
throw Error('newState mush be a object');
|
||||
}
|
||||
|
||||
proxyState.value = {
|
||||
...proxyState.value,
|
||||
...newState,
|
||||
};
|
||||
Object.keys(newState as PlainObject).forEach((key) => {
|
||||
proxyState.value[key] = (newState as PlainObject)[key];
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
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 { ICodeScope, CodeScope } from '../../../src/parts/code-runtime';
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { CodeScope } from '../../../src/parts/code-runtime';
|
||||
|
||||
describe('codeScope', () => {
|
||||
let scope: ICodeScope;
|
||||
|
||||
beforeAll(() => {
|
||||
scope = new CodeScope({});
|
||||
describe('CodeScope', () => {
|
||||
it('should return initial values', () => {
|
||||
const initValue = { a: 1, b: 2 };
|
||||
const scope = new CodeScope(initValue);
|
||||
expect(scope.value.a).toBe(1);
|
||||
expect(scope.value.b).toBe(2);
|
||||
});
|
||||
|
||||
it('should inject a new value', () => {
|
||||
scope.inject('username', 'Alice');
|
||||
expect(scope.value).toEqual({ username: 'Alice' });
|
||||
it('inject should add new values', () => {
|
||||
const scope = new CodeScope({});
|
||||
scope.set('c', 3);
|
||||
expect(scope.value.c).toBe(3);
|
||||
});
|
||||
|
||||
it('should not overwrite an existing value without force', () => {
|
||||
scope.inject('username', 'Bob');
|
||||
expect(scope.value).toEqual({ username: 'Alice' });
|
||||
it('inject should not overwrite existing values without force', () => {
|
||||
const initValue = { a: 1 };
|
||||
const scope = new CodeScope(initValue);
|
||||
scope.set('a', 2);
|
||||
expect(scope.value.a).toBe(1);
|
||||
scope.set('a', 3, true);
|
||||
expect(scope.value.a).toBe(3);
|
||||
});
|
||||
|
||||
it('should overwrite an existing value with force', () => {
|
||||
scope.inject('username', 'Bob', true);
|
||||
expect(scope.value).toEqual({ username: 'Bob' });
|
||||
it('setValue should merge values by default', () => {
|
||||
const initValue = { a: 1 };
|
||||
const scope = new CodeScope(initValue);
|
||||
scope.setValue({ b: 2 });
|
||||
expect(scope.value.a).toBe(1);
|
||||
expect(scope.value.b).toBe(2);
|
||||
});
|
||||
|
||||
it('should set new value without replacing existing values', () => {
|
||||
scope.setValue({ age: 25 });
|
||||
expect(scope.value).toEqual({ username: 'Bob', age: 25 });
|
||||
it('setValue should replace values when replace is true', () => {
|
||||
const initValue = { a: 1 };
|
||||
const scope = new CodeScope(initValue);
|
||||
scope.setValue({ b: 2 }, true);
|
||||
expect(scope.value.a).toBeUndefined();
|
||||
expect(scope.value.b).toBe(2);
|
||||
});
|
||||
|
||||
it('should set new value and replace all existing values', () => {
|
||||
scope.setValue({ loggedIn: true }, true);
|
||||
expect(scope.value).toEqual({ loggedIn: true });
|
||||
});
|
||||
it('should create child scopes and respect scope hierarchy', () => {
|
||||
const parentValue = { a: 1, b: 2 };
|
||||
const childValue = { b: 3, c: 4 };
|
||||
|
||||
it('should create a child scope with initial values', () => {
|
||||
const childScope = scope.createChild({ sessionId: 'abc123' });
|
||||
expect(childScope.value).toEqual({ loggedIn: true, sessionId: 'abc123' });
|
||||
});
|
||||
const parentScope = new CodeScope(parentValue);
|
||||
const childScope = parentScope.createChild(childValue);
|
||||
|
||||
it('should set new values in the child scope without affecting the parent scope', () => {
|
||||
const childScope = scope.createChild({ theme: 'dark' });
|
||||
expect(childScope.value).toEqual({ loggedIn: true, sessionId: 'abc123', theme: 'dark' });
|
||||
expect(scope.value).toEqual({ loggedIn: true });
|
||||
expect(childScope.value.a).toBe(1); // Inherits from parent scope
|
||||
expect(childScope.value.b).toBe(3); // Overridden by child scope
|
||||
expect(childScope.value.c).toBe(4); // Unique to child scope
|
||||
expect(parentScope.value.c).toBeUndefined(); // Parent scope should not have child's properties
|
||||
});
|
||||
});
|
||||
|
||||
@ -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 -------------------- */
|
||||
export * from './apiCreate';
|
||||
export { definePackageLoader } from './parts/package';
|
||||
export { Widget } from './parts/widget';
|
||||
export { createRenderer } from './main';
|
||||
export { definePackageLoader } from './services/package';
|
||||
export { LifecyclePhase } from './services/lifeCycleService';
|
||||
export { Widget } from './services/widget';
|
||||
export * from './utils/node';
|
||||
export * from './utils/value';
|
||||
|
||||
/* --------------- types ---------------- */
|
||||
export type * from './types';
|
||||
export type {
|
||||
Plugin,
|
||||
IRender,
|
||||
IRenderObject,
|
||||
PluginContext,
|
||||
RenderAdapter,
|
||||
RenderContext,
|
||||
} from './parts/extension';
|
||||
export type * from './parts/code-runtime';
|
||||
export type * from './parts/component-tree-model';
|
||||
export type * from './parts/package';
|
||||
export type * from './parts/schema';
|
||||
export type * from './parts/widget';
|
||||
} from './services/extension';
|
||||
export type * from './services/code-runtime';
|
||||
export type * from './services/model';
|
||||
export type * from './services/package';
|
||||
export type * from './services/schema';
|
||||
export type * from './services/widget';
|
||||
export type * from './services/extension';
|
||||
|
||||
@ -1,91 +1,111 @@
|
||||
import { Injectable } from '@alilc/lowcode-shared';
|
||||
import { ICodeRuntimeService } from './parts/code-runtime';
|
||||
import { IExtensionHostService, type RenderAdapter } from './parts/extension';
|
||||
import { IPackageManagementService } from './parts/package';
|
||||
import { IRuntimeUtilService } from './parts/runtimeUtil';
|
||||
import { IRuntimeIntlService } from './parts/runtimeIntl';
|
||||
import { ISchemaService } from './parts/schema';
|
||||
|
||||
import { Injectable, invariant, InstantiationService } from '@alilc/lowcode-shared';
|
||||
import { ICodeRuntimeService } from './services/code-runtime';
|
||||
import {
|
||||
IBoostsService,
|
||||
IExtensionHostService,
|
||||
type RenderAdapter,
|
||||
type IRenderObject,
|
||||
} from './services/extension';
|
||||
import { IPackageManagementService } from './services/package';
|
||||
import { ISchemaService } from './services/schema';
|
||||
import { ILifeCycleService, LifecyclePhase } from './services/lifeCycleService';
|
||||
import { IComponentTreeModelService } from './services/model';
|
||||
import type { AppOptions, RendererApplication } from './types';
|
||||
|
||||
@Injectable()
|
||||
export class RendererMain {
|
||||
export class RendererMain<RenderObject> {
|
||||
private mode: 'development' | 'production' = 'production';
|
||||
|
||||
private initOptions: AppOptions;
|
||||
|
||||
private renderObject: RenderObject;
|
||||
|
||||
private adapter: RenderAdapter<RenderObject>;
|
||||
|
||||
constructor(
|
||||
@ICodeRuntimeService private codeRuntimeService: ICodeRuntimeService,
|
||||
@IPackageManagementService private packageManagementService: IPackageManagementService,
|
||||
@IRuntimeUtilService private runtimeUtilService: IRuntimeUtilService,
|
||||
@IRuntimeIntlService private runtimeIntlService: IRuntimeIntlService,
|
||||
@ISchemaService private schemaService: ISchemaService,
|
||||
@IExtensionHostService private extensionHostService: IExtensionHostService,
|
||||
) {}
|
||||
@IComponentTreeModelService private componentTreeModelService: IComponentTreeModelService,
|
||||
@IBoostsService private boostsService: IBoostsService,
|
||||
@ILifeCycleService private lifeCycleService: ILifeCycleService,
|
||||
) {
|
||||
this.lifeCycleService.when(LifecyclePhase.OptionsResolved).finally(async () => {
|
||||
const renderContext = {
|
||||
schema: this.schemaService,
|
||||
packageManager: this.packageManagementService,
|
||||
boostsManager: this.boostsService,
|
||||
componentTreeModel: this.componentTreeModelService,
|
||||
lifeCycle: this.lifeCycleService,
|
||||
};
|
||||
|
||||
async initialize(options: AppOptions) {
|
||||
const { schema, mode } = options;
|
||||
this.renderObject = await this.adapter(renderContext);
|
||||
|
||||
await this.packageManagementService.loadPackages(this.initOptions.packages ?? []);
|
||||
|
||||
this.lifeCycleService.phase = LifecyclePhase.Ready;
|
||||
});
|
||||
}
|
||||
|
||||
async main(options: AppOptions, adapter: RenderAdapter<RenderObject>) {
|
||||
const { schema, mode, plugins = [] } = options;
|
||||
|
||||
if (mode) this.mode = mode;
|
||||
this.initOptions = { ...options };
|
||||
this.adapter = adapter;
|
||||
|
||||
// valid schema
|
||||
this.schemaService.initialize(schema);
|
||||
|
||||
this.codeRuntimeService.initialize(options);
|
||||
this.codeRuntimeService.initialize(options.codeRuntime ?? {});
|
||||
|
||||
// init intl
|
||||
const finalLocale = options.locale ?? navigator.language;
|
||||
const i18nTranslations = this.schemaService.get('i18n') ?? {};
|
||||
this.extensionHostService.registerPlugin(plugins);
|
||||
|
||||
this.runtimeIntlService.initialize(finalLocale, i18nTranslations);
|
||||
this.lifeCycleService.phase = LifecyclePhase.OptionsResolved;
|
||||
}
|
||||
|
||||
async startup<Render>(adapter: RenderAdapter<Render>): Promise<RendererApplication<Render>> {
|
||||
const render = await this.extensionHostService.runRender<Render>(adapter);
|
||||
async getApp(): Promise<RendererApplication<RenderObject>> {
|
||||
await this.lifeCycleService.when(LifecyclePhase.Ready);
|
||||
|
||||
// construct application
|
||||
const app = Object.freeze<RendererApplication<Render>>({
|
||||
return Object.freeze<RendererApplication<RenderObject>>({
|
||||
// develop use
|
||||
__options: this.initOptions,
|
||||
|
||||
mode: this.mode,
|
||||
schema: this.schemaService,
|
||||
packageManager: this.packageManagementService,
|
||||
...render,
|
||||
...this.renderObject,
|
||||
|
||||
use: (plugin) => {
|
||||
return this.extensionHostService.registerPlugin(plugin);
|
||||
this.extensionHostService.registerPlugin(plugin);
|
||||
return this.extensionHostService.doSetupPlugin(plugin);
|
||||
},
|
||||
});
|
||||
|
||||
// setup plugins
|
||||
this.extensionHostService.initialize(app);
|
||||
await this.extensionHostService.registerPlugin(this.initOptions.plugins ?? []);
|
||||
|
||||
// load packages
|
||||
await this.packageManagementService.loadPackages(this.initOptions.packages ?? []);
|
||||
|
||||
// resolve component maps
|
||||
const componentsMaps = this.schemaService.get('componentsMap');
|
||||
this.packageManagementService.resolveComponentMaps(componentsMaps);
|
||||
|
||||
this.initGlobalScope();
|
||||
|
||||
return app;
|
||||
}
|
||||
|
||||
private initGlobalScope() {
|
||||
// init runtime uitls
|
||||
const utils = this.schemaService.get('utils') ?? [];
|
||||
for (const util of utils) {
|
||||
this.runtimeUtilService.add(util);
|
||||
}
|
||||
|
||||
const constants = this.schemaService.get('constants') ?? {};
|
||||
|
||||
const globalScope = this.codeRuntimeService.getScope();
|
||||
globalScope.setValue({
|
||||
constants,
|
||||
utils: this.runtimeUtilService.toExpose(),
|
||||
...this.runtimeIntlService.toExpose(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建 createRenderer 的辅助函数
|
||||
* @param schema
|
||||
* @param options
|
||||
* @returns
|
||||
*/
|
||||
export function createRenderer<RenderObject = IRenderObject>(
|
||||
renderAdapter: RenderAdapter<RenderObject>,
|
||||
): (options: AppOptions) => Promise<RendererApplication<RenderObject>> {
|
||||
invariant(typeof renderAdapter === 'function', 'The first parameter must be a function.');
|
||||
|
||||
const instantiationService = new InstantiationService({ defaultScope: 'Singleton' });
|
||||
instantiationService.bootstrapModules();
|
||||
|
||||
const rendererMain = instantiationService.createInstance(
|
||||
RendererMain,
|
||||
) as RendererMain<RenderObject>;
|
||||
|
||||
return async (options) => {
|
||||
await rendererMain.main(options, renderAdapter);
|
||||
return rendererMain.getApp();
|
||||
};
|
||||
}
|
||||
|
||||
@ -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 { isObject } from 'lodash-es';
|
||||
import { ICodeRuntimeService } from '../code-runtime';
|
||||
import { IRuntimeUtilService } from '../runtimeUtil';
|
||||
import { IRuntimeIntlService } from '../runtimeIntl';
|
||||
import { IRuntimeUtilService } from '../runtimeUtilService';
|
||||
import { IRuntimeIntlService } from '../runtimeIntlService';
|
||||
|
||||
export type IBoosts<Extends> = IBoostsApi & Extends;
|
||||
|
||||
@ -1,33 +1,23 @@
|
||||
import {
|
||||
invariant,
|
||||
createDecorator,
|
||||
Provide,
|
||||
EventEmitter,
|
||||
KeyValueStore,
|
||||
} from '@alilc/lowcode-shared';
|
||||
import { type Plugin } from './plugin';
|
||||
import { createDecorator, Provide, EventEmitter, KeyValueStore } from '@alilc/lowcode-shared';
|
||||
import { type Plugin, type PluginContext } from './plugin';
|
||||
import { IBoostsService } from './boosts';
|
||||
import { IPackageManagementService } from '../package';
|
||||
import { ISchemaService } from '../schema';
|
||||
import { type RenderAdapter } from './render';
|
||||
import { IComponentTreeModelService } from '../component-tree-model';
|
||||
import type { RendererApplication } from '../../types';
|
||||
import { IComponentTreeModelService } from '../model';
|
||||
import { ILifeCycleService, LifecyclePhase } from '../lifeCycleService';
|
||||
|
||||
interface IPluginRuntime extends Plugin {
|
||||
status: 'setup' | 'ready';
|
||||
}
|
||||
|
||||
export interface IExtensionHostService {
|
||||
initialize(app: RendererApplication): void;
|
||||
registerPlugin(plugin: Plugin | Plugin[]): void;
|
||||
|
||||
/* ========= plugin ============= */
|
||||
registerPlugin(plugin: Plugin | Plugin[]): Promise<void>;
|
||||
doSetupPlugin(plugin: Plugin): Promise<void>;
|
||||
|
||||
getPlugin(name: string): Plugin | undefined;
|
||||
|
||||
/* =========== render =============== */
|
||||
runRender<Render>(adapter: RenderAdapter<Render>): Promise<Render>;
|
||||
|
||||
dispose(): Promise<void>;
|
||||
}
|
||||
|
||||
@ -38,24 +28,37 @@ export const IExtensionHostService =
|
||||
export class ExtensionHostService implements IExtensionHostService {
|
||||
private pluginRuntimes: IPluginRuntime[] = [];
|
||||
|
||||
private app: RendererApplication;
|
||||
private eventEmitter: EventEmitter;
|
||||
|
||||
private eventEmitter = new EventEmitter();
|
||||
|
||||
private globalState = new KeyValueStore();
|
||||
private pluginSetupContext: PluginContext;
|
||||
|
||||
constructor(
|
||||
@IPackageManagementService private packageManagementService: IPackageManagementService,
|
||||
@IBoostsService private boostsService: IBoostsService,
|
||||
@ISchemaService private schemaService: ISchemaService,
|
||||
@IComponentTreeModelService private componentTreeModelService: IComponentTreeModelService,
|
||||
) {}
|
||||
@ILifeCycleService private lifeCycleService: ILifeCycleService,
|
||||
) {
|
||||
this.eventEmitter = new EventEmitter('ExtensionHost');
|
||||
this.pluginSetupContext = {
|
||||
eventEmitter: this.eventEmitter,
|
||||
globalState: new KeyValueStore(),
|
||||
boosts: this.boostsService.toExpose(),
|
||||
schema: this.schemaService,
|
||||
packageManager: this.packageManagementService,
|
||||
|
||||
initialize(app: RendererApplication) {
|
||||
this.app = app;
|
||||
whenLifeCylePhaseChange: (phase) => {
|
||||
return this.lifeCycleService.when(phase);
|
||||
},
|
||||
};
|
||||
|
||||
this.lifeCycleService.when(LifecyclePhase.OptionsResolved).then(async () => {
|
||||
for (const plugin of this.pluginRuntimes) {
|
||||
await this.doSetupPlugin(plugin);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async registerPlugin(plugins: Plugin | Plugin[]) {
|
||||
registerPlugin(plugins: Plugin | Plugin[]) {
|
||||
plugins = Array.isArray(plugins) ? plugins : [plugins];
|
||||
|
||||
for (const plugin of plugins) {
|
||||
@ -64,39 +67,18 @@ export class ExtensionHostService implements IExtensionHostService {
|
||||
continue;
|
||||
}
|
||||
|
||||
await this.doSetupPlugin(plugin);
|
||||
this.pluginRuntimes.push({
|
||||
...plugin,
|
||||
status: 'ready',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
getPlugin(name: string): Plugin | undefined {
|
||||
return this.pluginRuntimes.find((item) => item.name === name);
|
||||
}
|
||||
|
||||
async runRender<Render>(adapter: RenderAdapter<Render>): Promise<Render> {
|
||||
invariant(adapter, 'render adapter not settled', 'ExtensionHostService');
|
||||
|
||||
return adapter({
|
||||
schema: this.schemaService,
|
||||
packageManager: this.packageManagementService,
|
||||
boostsManager: this.boostsService,
|
||||
componentTreeModel: this.componentTreeModelService,
|
||||
});
|
||||
}
|
||||
|
||||
async dispose(): Promise<void> {
|
||||
for (const plugin of this.pluginRuntimes) {
|
||||
await plugin.destory?.();
|
||||
}
|
||||
}
|
||||
|
||||
private async doSetupPlugin(plugin: Plugin) {
|
||||
async doSetupPlugin(plugin: Plugin) {
|
||||
const pluginRuntime = plugin as IPluginRuntime;
|
||||
|
||||
if (!this.pluginRuntimes.some((item) => item.name !== pluginRuntime.name)) {
|
||||
this.pluginRuntimes.push({
|
||||
...pluginRuntime,
|
||||
status: 'ready',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const isSetup = (name: string) => {
|
||||
@ -108,11 +90,7 @@ export class ExtensionHostService implements IExtensionHostService {
|
||||
return;
|
||||
}
|
||||
|
||||
await pluginRuntime.setup(this.app, {
|
||||
eventEmitter: this.eventEmitter,
|
||||
globalState: this.globalState,
|
||||
boosts: this.boostsService.toExpose(),
|
||||
});
|
||||
await pluginRuntime.setup(this.pluginSetupContext);
|
||||
pluginRuntime.status = 'setup';
|
||||
|
||||
// 遍历未安装的插件 寻找 dependsOn 的插件已安装完的插件进行安装
|
||||
@ -122,4 +100,14 @@ export class ExtensionHostService implements IExtensionHostService {
|
||||
await this.doSetupPlugin(readyPlugin);
|
||||
}
|
||||
}
|
||||
|
||||
getPlugin(name: string): Plugin | undefined {
|
||||
return this.pluginRuntimes.find((item) => item.name === name);
|
||||
}
|
||||
|
||||
async dispose(): Promise<void> {
|
||||
for (const plugin of this.pluginRuntimes) {
|
||||
await plugin.destory?.();
|
||||
}
|
||||
}
|
||||
}
|
||||
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 { IBoostsService } from './boosts';
|
||||
import { ISchemaService } from '../schema';
|
||||
import { IComponentTreeModelService } from '../component-tree-model';
|
||||
import { IComponentTreeModelService } from '../model';
|
||||
import { ILifeCycleService } from '../lifeCycleService';
|
||||
|
||||
export interface IRender {
|
||||
mount: (el: HTMLElement) => void | Promise<void>;
|
||||
export interface IRenderObject {
|
||||
mount: (containerOrId?: string | HTMLElement) => void | Promise<void>;
|
||||
unmount: () => void | Promise<void>;
|
||||
}
|
||||
|
||||
@ -16,6 +17,8 @@ export interface RenderContext {
|
||||
readonly boostsManager: IBoostsService;
|
||||
|
||||
readonly componentTreeModel: IComponentTreeModelService;
|
||||
|
||||
readonly lifeCycle: ILifeCycleService;
|
||||
}
|
||||
|
||||
export interface RenderAdapter<Render> {
|
||||
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 { IWidget, Widget } from '../widget';
|
||||
|
||||
export interface NormalizedComponentNode extends Spec.ComponentNode {
|
||||
loopArgs: [string, string];
|
||||
props: Spec.ComponentNodeProps;
|
||||
}
|
||||
|
||||
export interface InitializeModelOptions {
|
||||
defaultProps?: PlainObject | undefined;
|
||||
|
||||
stateCreator: ModelScopeStateCreator;
|
||||
dataSourceCreator: ModelScopeDataSourceCreator;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据低代码搭建协议的容器组件描述生成的容器模型
|
||||
*/
|
||||
export interface IComponentTreeModel<Component, ComponentInstance = unknown> {
|
||||
readonly id: string;
|
||||
|
||||
readonly codeScope: ICodeScope;
|
||||
|
||||
readonly codeRuntime: ICodeRuntimeService;
|
||||
|
||||
readonly widgets: IWidget<Component, ComponentInstance>[];
|
||||
|
||||
initialize(options: InitializeModelOptions): void;
|
||||
|
||||
/**
|
||||
* 获取协议中的 css 内容
|
||||
*/
|
||||
@ -37,11 +60,6 @@ export interface IComponentTreeModel<Component, ComponentInstance = unknown> {
|
||||
export type ModelScopeStateCreator = (initalState: PlainObject) => Spec.InstanceStateApi;
|
||||
export type ModelScopeDataSourceCreator = (...args: any[]) => Spec.InstanceDataSourceApi;
|
||||
|
||||
export interface ComponentTreeModelOptions {
|
||||
stateCreator: ModelScopeStateCreator;
|
||||
dataSourceCreator: ModelScopeDataSourceCreator;
|
||||
}
|
||||
|
||||
const defaultDataSourceSchema: Spec.ComponentDataSource = {
|
||||
list: [],
|
||||
dataHandler: {
|
||||
@ -50,44 +68,60 @@ const defaultDataSourceSchema: Spec.ComponentDataSource = {
|
||||
},
|
||||
};
|
||||
|
||||
export interface ComponentTreeModelOptions {
|
||||
id?: string;
|
||||
metadata?: PlainObject;
|
||||
}
|
||||
|
||||
export class ComponentTreeModel<Component, ComponentInstance = unknown>
|
||||
implements IComponentTreeModel<Component, ComponentInstance>
|
||||
{
|
||||
private instanceMap = new Map<string, ComponentInstance[]>();
|
||||
|
||||
public id: string;
|
||||
|
||||
public codeScope: ICodeScope;
|
||||
|
||||
public widgets: IWidget<Component>[] = [];
|
||||
|
||||
public metadata: PlainObject = {};
|
||||
|
||||
constructor(
|
||||
public componentsTree: Spec.ComponentTree,
|
||||
public codeRuntime: ICodeRuntimeService,
|
||||
options: ComponentTreeModelOptions,
|
||||
options?: ComponentTreeModelOptions,
|
||||
) {
|
||||
invariant(componentsTree, 'componentsTree must to provide', 'ComponentTreeModel');
|
||||
|
||||
this.initModelScope(options.stateCreator, options.dataSourceCreator);
|
||||
this.id = options?.id ?? `model_${uniqueId()}`;
|
||||
if (options?.metadata) {
|
||||
this.metadata = options.metadata;
|
||||
}
|
||||
|
||||
if (componentsTree.children) {
|
||||
this.widgets = this.buildWidgets(componentsTree.children);
|
||||
}
|
||||
}
|
||||
|
||||
private initModelScope(
|
||||
stateCreator: ModelScopeStateCreator,
|
||||
dataSourceCreator: ModelScopeDataSourceCreator,
|
||||
) {
|
||||
initialize({ defaultProps, stateCreator, dataSourceCreator }: InitializeModelOptions) {
|
||||
const {
|
||||
state = {},
|
||||
defaultProps: defaultSchemaProps,
|
||||
props = {},
|
||||
dataSource = defaultDataSourceSchema,
|
||||
methods = {},
|
||||
} = this.componentsTree;
|
||||
|
||||
this.codeScope = this.codeRuntime.createChildScope({});
|
||||
this.codeScope = this.codeRuntime.createChildScope({
|
||||
props: {
|
||||
...props,
|
||||
...defaultSchemaProps,
|
||||
...defaultProps,
|
||||
},
|
||||
});
|
||||
|
||||
const initalState = this.codeRuntime.resolve(state, this.codeScope);
|
||||
const initalProps = this.codeRuntime.resolve(props, this.codeScope);
|
||||
const initalState = this.codeRuntime.resolve(state, { scope: this.codeScope });
|
||||
const initalProps = this.codeRuntime.resolve(props, { scope: this.codeScope });
|
||||
|
||||
const stateApi = stateCreator(initalState);
|
||||
const dataSourceApi = dataSourceCreator(dataSource, stateApi);
|
||||
@ -95,7 +129,7 @@ export class ComponentTreeModel<Component, ComponentInstance = unknown>
|
||||
this.codeScope.setValue(
|
||||
Object.assign(
|
||||
{
|
||||
props: initalProps,
|
||||
props: { ...defaultProps, ...initalProps },
|
||||
$: (ref: string) => {
|
||||
const insArr = this.instanceMap.get(ref);
|
||||
if (!insArr) return undefined;
|
||||
@ -111,9 +145,9 @@ export class ComponentTreeModel<Component, ComponentInstance = unknown>
|
||||
);
|
||||
|
||||
for (const [key, fn] of Object.entries(methods)) {
|
||||
const customMethod = this.codeRuntime.resolve(fn, this.codeScope);
|
||||
const customMethod = this.codeRuntime.resolve(fn, { scope: this.codeScope });
|
||||
if (typeof customMethod === 'function') {
|
||||
this.codeScope.inject(key, customMethod);
|
||||
this.codeScope.set(key, customMethod);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -133,7 +167,7 @@ export class ComponentTreeModel<Component, ComponentInstance = unknown>
|
||||
|
||||
const lifeCycleSchema = this.componentsTree.lifeCycles[lifeCycleName];
|
||||
|
||||
const lifeCycleFn = this.codeRuntime.resolve(lifeCycleSchema, this.codeScope);
|
||||
const lifeCycleFn = this.codeRuntime.resolve(lifeCycleSchema, { scope: this.codeScope });
|
||||
if (typeof lifeCycleFn === 'function') {
|
||||
lifeCycleFn.apply(this.codeScope.value, args);
|
||||
}
|
||||
@ -162,13 +196,31 @@ export class ComponentTreeModel<Component, ComponentInstance = unknown>
|
||||
|
||||
buildWidgets(nodes: Spec.NodeType[]): IWidget<Component>[] {
|
||||
return nodes.map((node) => {
|
||||
const widget = new Widget<Component, ComponentInstance>(node, this);
|
||||
if (isComponentNode(node)) {
|
||||
const normalized = normalizeComponentNode(node);
|
||||
const widget = new Widget<Component, ComponentInstance>(normalized, this);
|
||||
|
||||
if (isComponentNode(node) && node.children?.length) {
|
||||
widget.children = this.buildWidgets(node.children);
|
||||
if (normalized.children?.length) {
|
||||
widget.children = this.buildWidgets(normalized.children);
|
||||
}
|
||||
|
||||
return widget;
|
||||
} else {
|
||||
return new Widget<Component, ComponentInstance>(node, this);
|
||||
}
|
||||
|
||||
return widget;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function normalizeComponentNode(node: Spec.ComponentNode): NormalizedComponentNode {
|
||||
const [loopArgsOne, loopArgsTwo] = node.loopArgs ?? [];
|
||||
const { children, ...props } = node.props ?? {};
|
||||
|
||||
return {
|
||||
...node,
|
||||
loopArgs: [loopArgsOne || 'item', loopArgsTwo || 'index'],
|
||||
props,
|
||||
condition: node.condition || node.condition === false ? node.condition : true,
|
||||
children: node.children ?? children,
|
||||
};
|
||||
}
|
||||
@ -4,18 +4,18 @@ import {
|
||||
type IComponentTreeModel,
|
||||
ComponentTreeModel,
|
||||
type ComponentTreeModelOptions,
|
||||
} from './treeModel';
|
||||
} from './componentTreeModel';
|
||||
import { ISchemaService } from '../schema';
|
||||
|
||||
export interface IComponentTreeModelService {
|
||||
create<Component>(
|
||||
componentsTree: Spec.ComponentTree,
|
||||
options: ComponentTreeModelOptions,
|
||||
options?: ComponentTreeModelOptions,
|
||||
): IComponentTreeModel<Component>;
|
||||
|
||||
createById<Component>(
|
||||
id: string,
|
||||
options: ComponentTreeModelOptions,
|
||||
options?: ComponentTreeModelOptions,
|
||||
): IComponentTreeModel<Component>;
|
||||
}
|
||||
|
||||
@ -32,14 +32,14 @@ export class ComponentTreeModelService implements IComponentTreeModelService {
|
||||
|
||||
create<Component>(
|
||||
componentsTree: Spec.ComponentTree,
|
||||
options: ComponentTreeModelOptions,
|
||||
options?: ComponentTreeModelOptions,
|
||||
): IComponentTreeModel<Component> {
|
||||
return new ComponentTreeModel(componentsTree, this.codeRuntimeService, options);
|
||||
}
|
||||
|
||||
createById<Component>(
|
||||
id: string,
|
||||
options: ComponentTreeModelOptions,
|
||||
options?: ComponentTreeModelOptions,
|
||||
): IComponentTreeModel<Component> {
|
||||
const componentsTrees = this.schemaService.get('componentsTree');
|
||||
const componentsTree = componentsTrees.find((item) => item.id === id);
|
||||
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';
|
||||
import { get as lodashGet } from 'lodash-es';
|
||||
import { PackageLoader } from './loader';
|
||||
import { ISchemaService } from '../schema';
|
||||
import { ILifeCycleService, LifecyclePhase } from '../lifeCycleService';
|
||||
|
||||
export interface NormalizedPackage {
|
||||
id: string;
|
||||
@ -62,6 +64,16 @@ export class PackageManagementService implements IPackageManagementService {
|
||||
|
||||
private packageLoaders: PackageLoader[] = [];
|
||||
|
||||
constructor(
|
||||
@ISchemaService private schemaService: ISchemaService,
|
||||
@ILifeCycleService private lifeCycleService: ILifeCycleService,
|
||||
) {
|
||||
this.lifeCycleService.when(LifecyclePhase.Ready).then(() => {
|
||||
const componentsMaps = this.schemaService.get('componentsMap');
|
||||
this.resolveComponentMaps(componentsMaps);
|
||||
});
|
||||
}
|
||||
|
||||
async loadPackages(packages: Spec.Package[]) {
|
||||
for (const item of packages) {
|
||||
// low code component not need load
|
||||
@ -99,7 +111,7 @@ export class PackageManagementService implements IPackageManagementService {
|
||||
if (map.devMode === 'lowCode') {
|
||||
const packageInfo = this.lowCodeComponentPackages.get((map as LowCodeComponent).id);
|
||||
|
||||
if (packageInfo) {
|
||||
if (map.componentName && packageInfo) {
|
||||
this.componentsRecord[map.componentName] = packageInfo;
|
||||
}
|
||||
} else {
|
||||
@ -123,7 +135,7 @@ export class PackageManagementService implements IPackageManagementService {
|
||||
}
|
||||
|
||||
const recordName = map.componentName ?? map.exportName;
|
||||
this.componentsRecord[recordName] = result;
|
||||
if (recordName && result) this.componentsRecord[recordName] = result;
|
||||
}
|
||||
}
|
||||
}
|
||||
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 { IPackageManagementService } from './package';
|
||||
import { ICodeRuntimeService } from './code-runtime';
|
||||
import { ILifeCycleService, LifecyclePhase } from './lifeCycleService';
|
||||
import { ISchemaService } from './schema';
|
||||
|
||||
export interface IRuntimeUtilService {
|
||||
add(utilItem: Spec.Util): void;
|
||||
add(name: string, fn: AnyFunction): void;
|
||||
|
||||
remove(name: string): void;
|
||||
|
||||
toExpose(): Spec.UtilsApi;
|
||||
}
|
||||
|
||||
export const IRuntimeUtilService = createDecorator<IRuntimeUtilService>('rendererUtilService');
|
||||
@ -17,12 +17,21 @@ export const IRuntimeUtilService = createDecorator<IRuntimeUtilService>('rendere
|
||||
export class RuntimeUtilService implements IRuntimeUtilService {
|
||||
private utilsMap: Map<string, AnyFunction> = new Map();
|
||||
|
||||
private _expose: any;
|
||||
|
||||
constructor(
|
||||
@ICodeRuntimeService private codeRuntimeService: ICodeRuntimeService,
|
||||
@IPackageManagementService private packageManagementService: IPackageManagementService,
|
||||
) {}
|
||||
@ILifeCycleService private lifeCycleService: ILifeCycleService,
|
||||
@ISchemaService private schemaService: ISchemaService,
|
||||
) {
|
||||
this.lifeCycleService.when(LifecyclePhase.Ready).then(() => {
|
||||
const utils = this.schemaService.get('utils') ?? [];
|
||||
for (const util of utils) {
|
||||
this.add(util);
|
||||
}
|
||||
|
||||
this.toExpose();
|
||||
});
|
||||
}
|
||||
|
||||
add(utilItem: Spec.Util): void;
|
||||
add(name: string, fn: AnyFunction): void;
|
||||
@ -41,22 +50,20 @@ export class RuntimeUtilService implements IRuntimeUtilService {
|
||||
this.utilsMap.delete(name);
|
||||
}
|
||||
|
||||
toExpose(): Spec.UtilsApi {
|
||||
if (!this._expose) {
|
||||
this._expose = new Proxy(Object.create(null), {
|
||||
get: (_, p: string) => {
|
||||
return this.utilsMap.get(p);
|
||||
},
|
||||
set() {
|
||||
return false;
|
||||
},
|
||||
has: (_, p: string) => {
|
||||
return this.utilsMap.has(p);
|
||||
},
|
||||
});
|
||||
}
|
||||
private toExpose(): void {
|
||||
const exposed = new Proxy(Object.create(null), {
|
||||
get: (_, p: string) => {
|
||||
return this.utilsMap.get(p);
|
||||
},
|
||||
set() {
|
||||
return false;
|
||||
},
|
||||
has: (_, p: string) => {
|
||||
return this.utilsMap.has(p);
|
||||
},
|
||||
});
|
||||
|
||||
return this._expose;
|
||||
this.codeRuntimeService.getScope().set('utils', exposed);
|
||||
}
|
||||
|
||||
private parseUtil(utilItem: Spec.Util) {
|
||||
@ -2,11 +2,13 @@ import {
|
||||
type Spec,
|
||||
createDecorator,
|
||||
Provide,
|
||||
type IStore,
|
||||
KeyValueStore,
|
||||
type EventDisposable,
|
||||
} from '@alilc/lowcode-shared';
|
||||
import { isObject } from 'lodash-es';
|
||||
import { schemaValidation } from './validation';
|
||||
import { ILifeCycleService, LifecyclePhase } from '../lifeCycleService';
|
||||
import { ICodeRuntimeService } from '../code-runtime';
|
||||
|
||||
export interface NormalizedSchema extends Spec.Project {}
|
||||
|
||||
@ -18,22 +20,26 @@ export interface ISchemaService {
|
||||
get<K extends NormalizedSchemaKey>(key: K): NormalizedSchema[K];
|
||||
|
||||
set<K extends NormalizedSchemaKey>(key: K, value: NormalizedSchema[K]): void;
|
||||
|
||||
onValueChange<K extends NormalizedSchemaKey>(
|
||||
key: K,
|
||||
listener: (value: NormalizedSchema[K]) => void,
|
||||
): EventDisposable;
|
||||
}
|
||||
|
||||
export const ISchemaService = createDecorator<ISchemaService>('schemaService');
|
||||
|
||||
@Provide(ISchemaService)
|
||||
export class SchemaService implements ISchemaService {
|
||||
private store: KeyValueStore<NormalizedSchema, NormalizedSchemaKey>;
|
||||
private store: IStore<NormalizedSchema, NormalizedSchemaKey> = new KeyValueStore<
|
||||
NormalizedSchema,
|
||||
NormalizedSchemaKey
|
||||
>({
|
||||
setterValidation: schemaValidation,
|
||||
});
|
||||
|
||||
constructor() {
|
||||
this.store = new KeyValueStore<NormalizedSchema, NormalizedSchemaKey>(new Map(), {
|
||||
setterValidation: schemaValidation,
|
||||
constructor(
|
||||
@ILifeCycleService private lifeCycleService: ILifeCycleService,
|
||||
@ICodeRuntimeService private codeRuntimeService: ICodeRuntimeService,
|
||||
) {
|
||||
this.lifeCycleService.when(LifecyclePhase.Ready).then(() => {
|
||||
const constants = this.get('constants') ?? {};
|
||||
this.codeRuntimeService.getScope().set('constants', constants);
|
||||
});
|
||||
}
|
||||
|
||||
@ -55,11 +61,4 @@ export class SchemaService implements ISchemaService {
|
||||
get<K extends NormalizedSchemaKey>(key: K): NormalizedSchema[K] {
|
||||
return this.store.get(key) as NormalizedSchema[K];
|
||||
}
|
||||
|
||||
onValueChange<K extends NormalizedSchemaKey>(
|
||||
key: K,
|
||||
listener: (value: NormalizedSchema[K]) => void,
|
||||
): EventDisposable {
|
||||
return this.store.onValueChange(key, listener);
|
||||
}
|
||||
}
|
||||
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 Plugin } from './parts/extension';
|
||||
import { type ISchemaService } from './parts/schema';
|
||||
import { type IPackageManagementService } from './parts/package';
|
||||
import { type IExtensionHostService } from './parts/extension';
|
||||
import { type EvalCodeFunction } from './parts/code-runtime';
|
||||
import { type Plugin } from './services/extension';
|
||||
import { type ISchemaService } from './services/schema';
|
||||
import { type IPackageManagementService } from './services/package';
|
||||
import { type CodeRuntimeInitializeOptions } from './services/code-runtime';
|
||||
|
||||
export interface AppOptions {
|
||||
schema: Spec.Project;
|
||||
packages?: Spec.Package[];
|
||||
plugins?: Plugin[];
|
||||
|
||||
/**
|
||||
* 应用语言,默认值为浏览器当前语言 navigator.language
|
||||
*/
|
||||
locale?: string;
|
||||
|
||||
/**
|
||||
* 运行模式
|
||||
*/
|
||||
mode?: 'development' | 'production';
|
||||
|
||||
evalCodeFunction?: EvalCodeFunction;
|
||||
/**
|
||||
* code runtime 设置选项
|
||||
*/
|
||||
codeRuntime?: CodeRuntimeInitializeOptions;
|
||||
}
|
||||
|
||||
export type RendererApplication<Render = unknown> = {
|
||||
@ -30,5 +27,5 @@ export type RendererApplication<Render = unknown> = {
|
||||
|
||||
readonly packageManager: IPackageManagementService;
|
||||
|
||||
use: IExtensionHostService['registerPlugin'];
|
||||
use(plugin: Plugin): Promise<void>;
|
||||
} & Render;
|
||||
|
||||
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';
|
||||
|
||||
export function someValue(obj: any, predicate: (data: any) => boolean) {
|
||||
if (!isPlainObject(obj) || isEmpty(obj)) return false;
|
||||
if (predicate(obj)) return true;
|
||||
|
||||
for (const val of Object.values(obj)) {
|
||||
if (someValue(val, predicate)) return true;
|
||||
export function someValue(
|
||||
obj: PlainObject | PlainObject[],
|
||||
filter: (data: PlainObject) => boolean,
|
||||
): boolean {
|
||||
if (Array.isArray(obj)) {
|
||||
return obj.some((item) => someValue(item, filter));
|
||||
}
|
||||
|
||||
return false;
|
||||
if (!isPlainObject(obj) || isEmpty(obj)) return false;
|
||||
if (filter(obj)) return true;
|
||||
|
||||
return Object.values(obj).some((val) => someValue(val, filter));
|
||||
}
|
||||
|
||||
export function processValue(
|
||||
obj: any,
|
||||
predicate: (obj: any) => boolean,
|
||||
processor: (node: any, paths: Array<string | number>) => any,
|
||||
export function mapValue(
|
||||
obj: PlainObject,
|
||||
filter: (obj: PlainObject) => boolean,
|
||||
callback: (node: any, paths: Array<string | number>) => any,
|
||||
): any {
|
||||
const innerProcess = (target: any, paths: Array<string | number>): any => {
|
||||
if (!someValue(obj, filter)) return obj;
|
||||
|
||||
const mapping = (target: any, paths: Array<string | number>): any => {
|
||||
if (Array.isArray(target)) {
|
||||
return target.map((item, idx) => innerProcess(item, [...paths, idx]));
|
||||
return target.map((item, idx) => mapping(item, [...paths, idx]));
|
||||
}
|
||||
|
||||
if (!isPlainObject(target) || isEmpty(target)) return target;
|
||||
if (!someValue(target, predicate)) return target;
|
||||
|
||||
if (predicate(target)) {
|
||||
return processor(target, paths);
|
||||
} else {
|
||||
const result = {} as any;
|
||||
for (const [key, value] of Object.entries(target)) {
|
||||
result[key] = innerProcess(value, [...paths, key]);
|
||||
}
|
||||
|
||||
return result;
|
||||
if (filter(target)) {
|
||||
return callback(target, paths);
|
||||
}
|
||||
|
||||
const result: PlainObject = {};
|
||||
|
||||
for (const [key, value] of Object.entries(target)) {
|
||||
result[key] = mapping(value, [...paths, key]);
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
return innerProcess(obj, []);
|
||||
return mapping(obj, []);
|
||||
}
|
||||
|
||||
@ -42,6 +42,8 @@ export interface Router extends Spec.RouterApi {
|
||||
|
||||
beforeRouteLeave: (fn: NavigationGuard) => () => void;
|
||||
afterRouteChange: (fn: NavigationHookAfter) => () => void;
|
||||
|
||||
isReady(): Promise<void>;
|
||||
}
|
||||
|
||||
const START_LOCATION: RouteLocationNormalized = {
|
||||
@ -68,6 +70,7 @@ export function createRouter(options: RouterOptions): Router {
|
||||
|
||||
const beforeGuards = createCallback<NavigationGuard>();
|
||||
const afterGuards = createCallback<NavigationHookAfter>();
|
||||
const readyHandlers = createCallback<any>();
|
||||
|
||||
let currentLocation: RouteLocationNormalized = START_LOCATION;
|
||||
let pendingLocation = currentLocation;
|
||||
@ -203,7 +206,9 @@ export function createRouter(options: RouterOptions): Router {
|
||||
}
|
||||
|
||||
return navigateTriggerBeforeGuards(toLocation, from)
|
||||
.catch(() => {})
|
||||
.catch((error) => {
|
||||
return markAsReady(error);
|
||||
})
|
||||
.then(() => {
|
||||
finalizeNavigation(toLocation, from, true, replace, data);
|
||||
|
||||
@ -293,7 +298,7 @@ export function createRouter(options: RouterOptions): Router {
|
||||
}
|
||||
|
||||
currentLocation = toLocation;
|
||||
// markAsReady();
|
||||
markAsReady();
|
||||
}
|
||||
|
||||
let removeHistoryListener: undefined | null | (() => void);
|
||||
@ -337,6 +342,32 @@ export function createRouter(options: RouterOptions): Router {
|
||||
});
|
||||
}
|
||||
|
||||
let ready: boolean;
|
||||
|
||||
/**
|
||||
* Mark the router as ready, resolving the promised returned by isReady(). Can
|
||||
* only be called once, otherwise does nothing.
|
||||
* @param err - optional error
|
||||
*/
|
||||
function markAsReady<E = any>(err?: E): E | void {
|
||||
if (!ready) {
|
||||
// still not ready if an error happened
|
||||
ready = !err;
|
||||
setupListeners();
|
||||
readyHandlers.list().forEach(([resolve, reject]) => (err ? reject(err) : resolve()));
|
||||
readyHandlers.clear();
|
||||
}
|
||||
|
||||
return err;
|
||||
}
|
||||
|
||||
function isReady(): Promise<void> {
|
||||
if (ready && currentLocation !== START_LOCATION) return Promise.resolve();
|
||||
return new Promise((resolve, reject) => {
|
||||
readyHandlers.add([resolve, reject]);
|
||||
});
|
||||
}
|
||||
|
||||
// init
|
||||
setupListeners();
|
||||
if (currentLocation === START_LOCATION) {
|
||||
@ -370,5 +401,7 @@ export function createRouter(options: RouterOptions): Router {
|
||||
|
||||
beforeRouteLeave: beforeGuards.add,
|
||||
afterRouteChange: afterGuards.add,
|
||||
|
||||
isReady,
|
||||
};
|
||||
}
|
||||
|
||||
@ -15,7 +15,7 @@ export type Constructor<T = any> = new (...args: any[]) => T;
|
||||
export function createDecorator<T>(serviceId: string): ServiceIdentifier<T> {
|
||||
const id = <any>(
|
||||
function (target: Constructor, targetKey: string, indexOrPropertyDescriptor: any): any {
|
||||
return inject(serviceId)(target, targetKey, indexOrPropertyDescriptor);
|
||||
return set(serviceId)(target, targetKey, indexOrPropertyDescriptor);
|
||||
}
|
||||
);
|
||||
id.toString = () => serviceId;
|
||||
|
||||
@ -14,7 +14,7 @@ export class Intl {
|
||||
private currentMessage: ComputedSignal<Translations>;
|
||||
private intlShape: IntlFormatter;
|
||||
|
||||
constructor(defaultLocale?: string, messages: LocaleTranslationsRecord = {}) {
|
||||
constructor(defaultLocale: string = navigator.language, messages: LocaleTranslationsRecord = {}) {
|
||||
if (defaultLocale) {
|
||||
defaultLocale = nomarlizeLocale(defaultLocale);
|
||||
} else {
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
import { invariant } from '../utils';
|
||||
import { PlainObject } from '../types';
|
||||
|
||||
/**
|
||||
@ -20,23 +19,12 @@ export interface IStore<O, K extends keyof O> {
|
||||
/**
|
||||
* 统一存储接口
|
||||
*/
|
||||
export class KeyValueStore<O = PlainObject, K extends keyof O = keyof O> {
|
||||
export class KeyValueStore<O = PlainObject, K extends keyof O = keyof O> implements IStore<O, K> {
|
||||
private readonly store = new Map();
|
||||
|
||||
private setterValidation: ((key: K, value: O[K]) => boolean | string) | undefined;
|
||||
|
||||
private waits = new Map<
|
||||
K,
|
||||
{
|
||||
once?: boolean;
|
||||
resolve: (data: any) => void;
|
||||
}[]
|
||||
>();
|
||||
|
||||
constructor(
|
||||
private readonly store: IStore<O, K> = new Map(),
|
||||
options?: {
|
||||
setterValidation?: (key: K, value: O[K]) => boolean | string;
|
||||
},
|
||||
) {
|
||||
constructor(options?: { setterValidation?: (key: K, value: O[K]) => boolean | string }) {
|
||||
if (options?.setterValidation) {
|
||||
this.setterValidation = options.setterValidation;
|
||||
}
|
||||
@ -45,8 +33,8 @@ export class KeyValueStore<O = PlainObject, K extends keyof O = keyof O> {
|
||||
get(key: K, defaultValue: O[K]): O[K];
|
||||
get(key: K, defaultValue?: O[K] | undefined): O[K] | undefined;
|
||||
get(key: K, defaultValue?: O[K]): O[K] | undefined {
|
||||
const value = this.store.get(key, defaultValue);
|
||||
return value;
|
||||
const value = this.store.get(key);
|
||||
return value ?? defaultValue;
|
||||
}
|
||||
|
||||
set(key: K, value: O[K]): void {
|
||||
@ -60,7 +48,6 @@ export class KeyValueStore<O = PlainObject, K extends keyof O = keyof O> {
|
||||
}
|
||||
|
||||
this.store.set(key, value);
|
||||
this.dispatchValue(key);
|
||||
}
|
||||
|
||||
delete(key: K): void {
|
||||
@ -74,77 +61,4 @@ export class KeyValueStore<O = PlainObject, K extends keyof O = keyof O> {
|
||||
get size(): number {
|
||||
return this.store.size;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定 key 的值,若此时还未赋值,则等待,若已有值,则直接返回值
|
||||
* 注:此函数返回 Promise 实例,只会执行(fullfill)一次
|
||||
* @param key
|
||||
* @returns
|
||||
*/
|
||||
waitForValue(key: K) {
|
||||
const val = this.get(key);
|
||||
if (val !== undefined) {
|
||||
return Promise.resolve(val);
|
||||
}
|
||||
return new Promise((resolve) => {
|
||||
this.addWaiter(key, resolve, true);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定 key 的值,函数回调模式,若多次被赋值,回调会被多次调用
|
||||
* @param key
|
||||
* @param fn
|
||||
* @returns
|
||||
*/
|
||||
onValueChange<T extends K>(key: T, fn: (value: O[T]) => void): () => void {
|
||||
const val = this.get(key);
|
||||
if (val !== undefined) {
|
||||
// @ts-expect-error: val is not undefined
|
||||
fn(val);
|
||||
}
|
||||
this.addWaiter(key, fn as any);
|
||||
return () => {
|
||||
this.removeWaiter(key, fn as any);
|
||||
};
|
||||
}
|
||||
|
||||
private dispatchValue(key: K): void {
|
||||
const waits = this.waits.get(key);
|
||||
if (!waits) return;
|
||||
|
||||
for (let i = waits.length - 1; i >= 0; i--) {
|
||||
const waiter = waits[i];
|
||||
waiter.resolve(this.get(key)!);
|
||||
if (waiter.once) {
|
||||
waits.splice(i, 1); // Remove the waiter if it only waits once
|
||||
}
|
||||
}
|
||||
|
||||
if (waits.length === 0) {
|
||||
this.waits.delete(key); // No more waiters for the key
|
||||
}
|
||||
}
|
||||
|
||||
private addWaiter(key: K, resolve: (value: O[K]) => void, once?: boolean) {
|
||||
if (this.waits.has(key)) {
|
||||
this.waits.get(key)!.push({ resolve, once });
|
||||
} else {
|
||||
this.waits.set(key, [{ resolve, once }]);
|
||||
}
|
||||
}
|
||||
|
||||
private removeWaiter(key: K, resolve: (value: O[K]) => void) {
|
||||
const waits = this.waits.get(key);
|
||||
if (!waits) return;
|
||||
|
||||
this.waits.set(
|
||||
key,
|
||||
waits.filter((waiter) => waiter.resolve !== resolve),
|
||||
);
|
||||
|
||||
if (this.waits.get(key)!.length === 0) {
|
||||
this.waits.delete(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -9,19 +9,31 @@ import {
|
||||
ref,
|
||||
computed,
|
||||
ReactiveEffect,
|
||||
shallowRef,
|
||||
type ShallowRef,
|
||||
type ComputedRef,
|
||||
type Ref,
|
||||
getCurrentScope,
|
||||
isRef,
|
||||
isReactive,
|
||||
isShallow,
|
||||
EffectScheduler,
|
||||
readonly,
|
||||
type EffectScheduler,
|
||||
} from '@vue/reactivity';
|
||||
import { noop, isObject, isPlainObject, isSet, isMap, isFunction } from 'lodash-es';
|
||||
import { isPromise } from './utils';
|
||||
|
||||
export { ref as signal, computed, watchEffect as effect, watch as reaction, isRef as isSignal };
|
||||
export type { Ref as Signal, ComputedRef as ComputedSignal };
|
||||
export {
|
||||
ref as signal,
|
||||
shallowRef as shallowSignal,
|
||||
computed,
|
||||
watchEffect as effect,
|
||||
watch as reaction,
|
||||
isRef as isSignal,
|
||||
isShallow as isShallowSignal,
|
||||
readonly,
|
||||
};
|
||||
export type { Ref as Signal, ComputedRef as ComputedSignal, ShallowRef as ShallowSignal };
|
||||
|
||||
export type WatchSource<T = any> = Ref<T> | ComputedRef<T> | (() => T);
|
||||
export type WatchEffect = (onCleanup: OnCleanup) => void;
|
||||
|
||||
@ -40,7 +40,7 @@ export interface Project {
|
||||
/**
|
||||
* 当前应用配置信息
|
||||
*/
|
||||
config?: Record<string, JSONObject>;
|
||||
config?: ProjectConfig;
|
||||
/**
|
||||
* 当前应用元数据信息
|
||||
*/
|
||||
@ -60,6 +60,32 @@ export interface Project {
|
||||
pages?: PageConfig[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 当前应用配置信息
|
||||
*/
|
||||
export interface ProjectConfig {
|
||||
/**
|
||||
* 默认语言配置,不填则为 navagator.language
|
||||
*/
|
||||
defaultLocale?: string;
|
||||
/**
|
||||
* 布局组件配置
|
||||
*/
|
||||
layout?: {
|
||||
componentName: string;
|
||||
props: Record<string, JSONObject>;
|
||||
};
|
||||
/**
|
||||
* 默认挂载 dom 节点
|
||||
*/
|
||||
targetRootID?: string;
|
||||
|
||||
// todo
|
||||
theme?: any;
|
||||
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* https://lowcode-engine.cn/site/docs/specs/lowcode-spec#22-%E7%BB%84%E4%BB%B6%E6%98%A0%E5%B0%84%E5%85%B3%E7%B3%BBa
|
||||
* 协议中用于描述 componentName 到公域组件映射关系的规范。
|
||||
@ -259,7 +285,11 @@ export interface ComponentNode {
|
||||
*/
|
||||
componentName: string;
|
||||
/**
|
||||
* 组件属性对象
|
||||
* 默认 props
|
||||
*/
|
||||
defaultProps?: JSONObject;
|
||||
/**
|
||||
* 组件 props 对象
|
||||
*/
|
||||
props?: ComponentNodeProps;
|
||||
/**
|
||||
@ -415,11 +445,16 @@ export interface JSONObject {
|
||||
[key: string]: JSONValue | JSONObject | JSONObject[];
|
||||
}
|
||||
|
||||
export interface JSNode {
|
||||
type: string;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* 节点类型(A)
|
||||
* 通常用于描述组件的某一个属性为 Node 或 Function-Return-Node 的场景。
|
||||
*/
|
||||
export interface JSSlot {
|
||||
export interface JSSlot extends JSNode {
|
||||
type: 'JSSlot';
|
||||
value: ComponentNode | ComponentNode[];
|
||||
params?: string[];
|
||||
@ -430,7 +465,7 @@ export interface JSSlot {
|
||||
/**
|
||||
* 事件函数类型(A)
|
||||
*/
|
||||
export interface JSFunction {
|
||||
export interface JSFunction extends JSNode {
|
||||
type: 'JSFunction';
|
||||
value: string;
|
||||
|
||||
@ -440,7 +475,7 @@ export interface JSFunction {
|
||||
/**
|
||||
* 变量类型(A)
|
||||
*/
|
||||
export interface JSExpression {
|
||||
export interface JSExpression extends JSNode {
|
||||
type: 'JSExpression';
|
||||
value: string;
|
||||
|
||||
@ -450,7 +485,7 @@ export interface JSExpression {
|
||||
/**
|
||||
* 国际化多语言类型(AA)
|
||||
*/
|
||||
export interface JSI18n {
|
||||
export interface JSI18n extends JSNode {
|
||||
type: 'i18n';
|
||||
/**
|
||||
* i18n 结构中字段的 key 标识符
|
||||
@ -462,6 +497,4 @@ export interface JSI18n {
|
||||
params?: Record<string, string | number | JSExpression>;
|
||||
}
|
||||
|
||||
export type JSNode = JSSlot | JSExpression | JSExpression | JSI18n;
|
||||
|
||||
export type NodeType = string | JSExpression | JSI18n | ComponentNode;
|
||||
|
||||
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 './platform';
|
||||
export * from './callback';
|
||||
export * from './async';
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user