refactor: renderer

This commit is contained in:
1ncounter 2024-08-01 20:28:50 +08:00
parent ddc2473f98
commit 39be950600
51 changed files with 827 additions and 883 deletions

View File

@ -1,8 +1,8 @@
import { signal, uniqueId, ComponentTreeRoot } from '@alilc/lowcode-shared';
import { Signals, uniqueId, ComponentTree } from '@alilc/lowcode-shared';
import { type Project } from '../project';
import { History } from './history';
export interface DocumentSchema extends ComponentTreeRoot {
export interface DocumentSchema extends ComponentTree {
id: string;
}
@ -26,7 +26,7 @@ export interface DocumentModel {
export function createDocumentModel(project: Project) {
const uid = uniqueId('doc');
const currentDocumentSchema = signal<DocumentSchema>({});
const currentDocumentSchema = Signals.signal<DocumentSchema>({});
const documentHistory = new History(currentDocumentSchema, () => {});

View File

@ -2,11 +2,3 @@ export * from './configuration';
export * from './extension/extension';
export * from './resource';
export * from './command';
// test
export * from './extension/registry';
export * from './main';
export * from './keybinding/keybindingRegistry';
export * from './keybinding/keybindingParser';
export * from './keybinding/keybindingResolver';
export * from './keybinding/keybindings';

View File

@ -0,0 +1,82 @@
import { createDecorator, Barrier } from '@alilc/lowcode-shared';
/**
*
*/
export const enum LifecyclePhase {
/**
*
*/
Starting = 1,
/**
*
*/
Ready = 2,
/**
*
*/
Destroying = 3,
}
export interface ILifeCycleService {
/**
* A flag indicating in what phase of the lifecycle we currently are.
*/
phase: LifecyclePhase;
setPhase(phase: LifecyclePhase): void;
/**
* Returns a promise that resolves when a certain lifecycle phase
* has started.
*/
when(phase: LifecyclePhase): Promise<void>;
onWillDestory(): void;
}
export const ILifeCycleService = createDecorator<ILifeCycleService>('lifeCycleService');
export class LifeCycleService implements ILifeCycleService {
private readonly phaseWhen = new Map<LifecyclePhase, Barrier>();
private _phase = LifecyclePhase.Starting;
get phase(): LifecyclePhase {
return this._phase;
}
setPhase(value: LifecyclePhase) {
if (value < this._phase) {
throw new Error('Lifecycle cannot go backwards');
}
if (this._phase === value) {
return;
}
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();
}
onWillDestory(): void {}
}

View File

@ -1,24 +1,31 @@
import { InstantiationService } from '@alilc/lowcode-shared';
import { IWorkbenchService } from './workbench';
import { IConfigurationService } from './configuration';
import { ConfigurationService, IConfigurationService } from './configuration';
class MainApplication {
class TestMainApplication {
constructor() {
console.log('main application');
}
async main() {
const instantiationService = new InstantiationService();
const configurationService = instantiationService.get(IConfigurationService);
const workbench = instantiationService.get(IWorkbenchService);
await configurationService.initialize();
workbench.initialize();
}
createServices() {
const instantiationService = new InstantiationService();
const configurationService = new ConfigurationService();
instantiationService.container.set(IConfigurationService, configurationService);
}
export async function createLowCodeEngineApp(): Promise<MainApplication> {
const app = new MainApplication();
initServices() {}
}
export async function createLowCodeEngineApp() {
const app = new TestMainApplication();
await app.main();

View File

@ -0,0 +1,5 @@
export interface ITheme {
type: string;
value: string;
}

View File

@ -0,0 +1,21 @@
import { Disposable, Events, type IDisposable, createDecorator } from '@alilc/lowcode-shared';
import { type ITheme } from './theme';
export interface IThemeService extends IDisposable {
getTheme(): ITheme;
onDidColorThemeChange: Events.Event<ITheme>;
}
export const IThemeService = createDecorator<IThemeService>('themeService');
export class ThemeService extends Disposable implements IThemeService {
private _activeTheme: ITheme;
private _onDidColorThemeChange = this._addDispose(new Events.Emitter<ITheme>());
onDidColorThemeChange = this._onDidColorThemeChange.event;
getTheme(): ITheme {
return this._activeTheme;
}
}

View File

@ -1,5 +1,5 @@
import { InstantiationService } from '@alilc/lowcode-shared';
import { IConfigurationService, IWorkspaceService } from '@alilc/lowcode-engine-core';
import { IConfigurationService } from '@alilc/lowcode-engine-core';
export class MainEngineApplication {
instantiationService = new InstantiationService();

View File

@ -1,27 +0,0 @@
import { type Event, type EventListener, createDecorator } from '@alilc/lowcode-shared';
export interface ITheme {
type: string;
value: string;
}
export interface IThemeService {
getTheme(): ITheme;
onDidColorThemeChange: Event<[ITheme]>;
}
export const IThemeService = createDecorator<IThemeService>('themeService');
export class ThemeService implements IThemeService {
private activeTheme: ITheme;
getTheme(): ITheme {
return this.activeTheme;
}
onDidColorThemeChange(listener: EventListener<[ITheme]>) {
return () => {};
}
}

View File

@ -1,60 +1,9 @@
import { createRenderer } from '@alilc/lowcode-renderer-core';
import { type Root, createRoot } from 'react-dom/client';
import { type IRendererContext, RendererContext, getRenderInstancesByAccessor } from './context';
import { ApplicationView, boosts } from '../app';
import { type ReactAppOptions } from './types';
import { App, type AppOptions } from '../app';
export const createApp = async (options: ReactAppOptions) => {
return createRenderer(async (service) => {
const contextValue: IRendererContext = service.invokeFunction((accessor) => {
return {
options,
...getRenderInstancesByAccessor(accessor),
export const createApp = async (options: AppOptions) => {
const app = new App(options);
await app.startup();
return app;
};
});
contextValue.boostsManager.extend(boosts.toExpose());
let root: Root | undefined;
return {
async mount(containerOrId) {
if (root) return;
const defaultId = contextValue.schema.get<string>('config.targetRootID', 'app');
const rootElement = normalizeContainer(containerOrId, defaultId);
root = createRoot(rootElement);
root.render(
<RendererContext.Provider value={contextValue}>
<ApplicationView />
</RendererContext.Provider>,
);
},
unmount() {
if (root) {
root.unmount();
root = undefined;
}
},
};
})(options);
};
function normalizeContainer(container: Element | string | undefined, defaultId: string): Element {
let result: Element | undefined = undefined;
if (typeof container === 'string') {
const el = document.getElementById(container);
if (el) result = el;
} else if (container instanceof window.Element) {
result = container;
}
if (!result) {
result = document.createElement('div');
result.id = defaultId;
}
return result;
}

View File

@ -1,53 +1,38 @@
import { createRenderer } from '@alilc/lowcode-renderer-core';
import { type ComponentTreeRoot } from '@alilc/lowcode-shared';
import { type FunctionComponent } from 'react';
import { type StringDictionary, type ComponentTree } from '@alilc/lowcode-shared';
import { CodeRuntime } from '@alilc/lowcode-renderer-core';
import { FunctionComponent, ComponentType } from 'react';
import {
type LowCodeComponentProps,
createComponent as createSchemaComponent,
} from '../runtime/createComponent';
import { type IRendererContext, RendererContext, getRenderInstancesByAccessor } from './context';
import { type ReactAppOptions } from './types';
type ComponentOptions as SchemaComponentOptions,
reactiveStateFactory,
} from '../runtime';
import { type ComponentsAccessor } from '../app';
interface Render {
toComponent(): FunctionComponent<LowCodeComponentProps>;
export interface ComponentOptions extends SchemaComponentOptions {
schema: ComponentTree;
componentsRecord: Record<string, ComponentType>;
codeScope?: StringDictionary;
}
export async function createComponent(options: ReactAppOptions) {
const creator = createRenderer<Render>((service) => {
const contextValue: IRendererContext = service.invokeFunction((accessor) => {
return {
options,
...getRenderInstancesByAccessor(accessor),
};
});
const componentsTree = contextValue.schema.get<ComponentTreeRoot>('componentsTree.0');
if (!componentsTree) {
throw new Error('componentsTree is required');
}
const LowCodeComponent = createSchemaComponent(componentsTree, {
displayName: componentsTree.componentName,
...options.component,
});
function Component(props: LowCodeComponentProps) {
return (
<RendererContext.Provider value={contextValue}>
<LowCodeComponent {...props} />
</RendererContext.Provider>
);
}
return {
toComponent() {
return Component;
export function createComponent(
options: ComponentOptions,
): FunctionComponent<LowCodeComponentProps> {
const { schema, componentsRecord, modelOptions, codeScope, ...componentOptions } = options;
const codeRuntime = new CodeRuntime(codeScope);
const components: ComponentsAccessor = {
getComponent(componentName) {
return componentsRecord[componentName] as any;
},
};
const LowCodeComponent = createSchemaComponent(schema, codeRuntime, components, {
...componentOptions,
modelOptions: {
...modelOptions,
stateCreator: modelOptions.stateCreator ?? reactiveStateFactory,
},
});
const render = await creator(options);
return render.toComponent();
return LowCodeComponent;
}

View File

@ -1,41 +0,0 @@
import {
IBoostsManager,
IComponentTreeModelService,
ILifeCycleService,
IPackageManagementService,
ISchemaService,
IExtensionHostService,
} from '@alilc/lowcode-renderer-core';
import { InstanceAccessor } from '@alilc/lowcode-shared';
import { createContext, useContext } from 'react';
import { type ReactAppOptions } from './types';
export interface IRendererContext {
readonly options: ReactAppOptions;
readonly schema: Omit<ISchemaService, 'initialize'>;
readonly packageManager: IPackageManagementService;
readonly boostsManager: IBoostsManager;
readonly componentTreeModel: IComponentTreeModelService;
readonly lifeCycle: ILifeCycleService;
}
export const getRenderInstancesByAccessor = (accessor: InstanceAccessor) => {
return {
schema: accessor.get(ISchemaService),
packageManager: accessor.get(IPackageManagementService),
boostsManager: accessor.get(IExtensionHostService).boostsManager,
componentTreeModel: accessor.get(IComponentTreeModelService),
lifeCycle: accessor.get(ILifeCycleService),
};
};
export const RendererContext = createContext<IRendererContext>(undefined!);
RendererContext.displayName = 'RendererContext';
export const useRendererContext = () => useContext(RendererContext);

View File

@ -1,11 +0,0 @@
import { type AppOptions } from '@alilc/lowcode-renderer-core';
import { type ComponentType } from 'react';
import { type ComponentOptions } from '../runtime/createComponent';
export interface ReactAppOptions extends AppOptions {
component?: Pick<
ComponentOptions,
'beforeElementCreate' | 'elementCreated' | 'componentRefAttached'
>;
faultComponent?: ComponentType<any>;
}

View File

@ -0,0 +1,279 @@
import {
InstantiationService,
BeanContainer,
CtorDescriptor,
Disposable,
} from '@alilc/lowcode-shared';
import {
CodeRuntimeService,
ICodeRuntimeService,
IExtensionHostService,
ExtensionHostService,
IPackageManagementService,
PackageManagementService,
ISchemaService,
SchemaService,
IRuntimeIntlService,
RuntimeIntlService,
IRuntimeUtilService,
RuntimeUtilService,
} from '@alilc/lowcode-renderer-core';
import { createRouter, type RouterOptions, type Router } from '@alilc/lowcode-renderer-router';
import { type ComponentType, createElement } from 'react';
import { type Root, createRoot } from 'react-dom/client';
import {
IReactRendererBoostsService,
ReactRendererBoostsService,
type ReactRendererBoostsApi,
} from './boosts';
import { createAppView } from './components/view';
import { ComponentOptions } from '../runtime';
import type { Project, Package } from '@alilc/lowcode-shared';
import type {
Plugin,
CodeRuntimeOptions,
ModelDataSourceCreator,
ModelStateCreator,
ICodeRuntime,
} from '@alilc/lowcode-renderer-core';
export type ComponentsAccessor = Pick<IPackageManagementService, 'getComponent'>;
export type SchemaAccessor = Pick<ISchemaService, 'get'>;
export interface IRendererApplication {
readonly options: AppOptions;
readonly mode: 'development' | 'production';
mount: (containerOrId?: string | HTMLElement) => void | Promise<void>;
unmount: () => void | Promise<void>;
use(plugin: Plugin<ReactRendererBoostsApi>): Promise<void>;
destroy(): void;
}
export interface AppOptions {
schema: Project;
packages?: Package[];
plugins?: Plugin[];
/**
* code runtime
*/
codeRuntime?: CodeRuntimeOptions;
component?: {
stateCreator?: ModelStateCreator;
/**
*
*/
dataSourceCreator?: ModelDataSourceCreator;
} & Pick<ComponentOptions, 'beforeElementCreate' | 'elementCreated' | 'componentRefAttached'>;
faultComponent?: ComponentType<any>;
}
export class App extends Disposable implements IRendererApplication {
private _root?: Root;
private instantiationService: InstantiationService;
get mode() {
return __DEV__ ? 'development' : 'production';
}
private _router: Router;
get router() {
return this._router;
}
private _components?: ComponentsAccessor;
get components() {
if (!this._components) {
this._components = this.instantiationService.invokeFunction((accessor) => {
const packageManager = accessor.get(IPackageManagementService);
return {
getComponent: packageManager.getComponent.bind(packageManager),
};
});
}
return this._components;
}
private _schema?: SchemaAccessor;
get schema() {
if (!this._schema) {
this._schema = this.instantiationService.invokeFunction((accessor) => {
const schemaService = accessor.get(ISchemaService);
return {
get: schemaService.get.bind(schemaService),
};
});
}
return this._schema;
}
private _codeRuntime?: ICodeRuntime;
get codeRuntime() {
if (!this._codeRuntime) {
return this.instantiationService.invokeFunction((accessor) => {
return accessor.get(ICodeRuntimeService).rootRuntime;
});
}
return this._codeRuntime;
}
constructor(public readonly options: AppOptions) {
super();
this._createServices();
}
async startup() {
await this._initServices();
await this._createRouter();
}
private _createServices() {
const container = new BeanContainer();
// create services
container.set(ISchemaService, new SchemaService());
container.set(IRuntimeIntlService, new RuntimeIntlService());
container.set(ICodeRuntimeService, new CtorDescriptor(CodeRuntimeService));
container.set(IPackageManagementService, new CtorDescriptor(PackageManagementService));
container.set(IExtensionHostService, new CtorDescriptor(ExtensionHostService));
container.set(IRuntimeUtilService, new CtorDescriptor(RuntimeUtilService));
container.set(IReactRendererBoostsService, new CtorDescriptor(ReactRendererBoostsService));
this.instantiationService = this._addDispose(new InstantiationService(container));
}
private async _initServices() {
const [
schemaService,
extensionHostService,
packageManagementService,
utilService,
intlService,
] = this.instantiationService.invokeFunction((accessor) => [
accessor.get(ISchemaService),
accessor.get(IExtensionHostService),
accessor.get(IPackageManagementService),
accessor.get(IRuntimeUtilService),
accessor.get(IRuntimeIntlService),
]);
// init services
schemaService.initialize(this.options.schema);
const defaultLocale = schemaService.get<string>('config.defaultLocale');
const i18ns = schemaService.get('i18n', {});
const [intlApi, utilApi] = [
intlService.initialize(defaultLocale, i18ns),
utilService.initialize(),
];
this.codeRuntime.getScope().set('intl', intlApi);
this.codeRuntime.getScope().set('utils', utilApi);
await extensionHostService.registerPlugin(this.options.plugins ?? []);
await packageManagementService.loadPackages(this.options.packages ?? []);
lifeCycleService.setPhase(LifecyclePhase.Ready);
}
private async _createRouter() {
const defaultRouterOptions: RouterOptions = {
historyMode: 'browser',
baseName: '/',
routes: [],
};
let routerConfig = defaultRouterOptions;
try {
const routerSchema = this.schema.get('router');
if (routerSchema) {
routerConfig = this.codeRuntime.resolve(routerSchema);
}
} catch (e) {
console.error(`schema's router config is resolve error: `, e);
}
this._router = createRouter(routerConfig);
this.codeRuntime.getScope().set('router', this._router);
await this._router.isReady();
}
async mount(containerOrId?: string | HTMLElement) {
this._throwIfDisposed(`this app has been destroyed`);
if (this._root) return;
const reactBoosts = this.instantiationService.invokeFunction((accessor) => {
return accessor.get(IReactRendererBoostsService);
});
const defaultId = this.schema.get<string>('config.targetRootID', 'app');
const rootElement = normalizeContainer(containerOrId, defaultId);
const AppView = createAppView(this, reactBoosts.getAppWrappers(), reactBoosts.getOutlet());
this._root = createRoot(rootElement);
this._root.render(createElement(AppView));
}
unmount() {
this._throwIfDisposed(`this app has been destroyed`);
if (this._root) {
this._root.unmount();
this._root = undefined;
}
}
use(plugin: Plugin<ReactRendererBoostsApi>) {
this._throwIfDisposed(`this app has been destroyed`);
const extensionHostService = this.instantiationService.invokeFunction((accessor) => {
return accessor.get(IExtensionHostService);
});
return extensionHostService.registerPlugin(plugin);
}
destroy() {
this.dispose();
}
}
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;
}
export function defineRendererPlugin(plugin: Plugin<ReactRendererBoostsApi>) {
return plugin;
}

View File

@ -1,4 +1,5 @@
import { type Plugin } from '@alilc/lowcode-renderer-core';
import { IExtensionHostService } from '@alilc/lowcode-renderer-core';
import { createDecorator } from '@alilc/lowcode-shared';
import { type ComponentType, type PropsWithChildren } from 'react';
export type WrapperComponent = ComponentType<PropsWithChildren<any>>;
@ -9,39 +10,58 @@ export interface OutletProps {
export type Outlet = ComponentType<OutletProps>;
export interface ReactRendererBoostsApi {
export interface IReactRendererBoostsService {
addAppWrapper(appWrapper: WrapperComponent): void;
getAppWrappers(): WrapperComponent[];
setOutlet(outlet: Outlet): void;
getOutlet(): Outlet | null;
}
class ReactRendererBoosts {
private wrappers: WrapperComponent[] = [];
export const IReactRendererBoostsService = createDecorator<IReactRendererBoostsService>(
'reactRendererBoostsService',
);
private outlet: Outlet | null = null;
export type ReactRendererBoostsApi = Pick<
IReactRendererBoostsService,
'addAppWrapper' | 'setOutlet'
>;
getAppWrappers() {
return this.wrappers;
export class ReactRendererBoostsService implements IReactRendererBoostsService {
private _wrappers: WrapperComponent[] = [];
private _outlet: Outlet | null = null;
constructor(@IExtensionHostService private extensionHostService: IExtensionHostService) {
this.extensionHostService.boostsManager.extend(this._toExpose());
}
getOutlet() {
return this.outlet;
getAppWrappers(): WrapperComponent[] {
return this._wrappers;
}
toExpose(): ReactRendererBoostsApi {
addAppWrapper(appWrapper: WrapperComponent): void {
if (appWrapper) this._wrappers.push(appWrapper);
}
setOutlet(outletComponent: Outlet): void {
if (outletComponent) this._outlet = outletComponent;
}
getOutlet(): Outlet | null {
return this._outlet;
}
private _toExpose(): ReactRendererBoostsApi {
return {
addAppWrapper: (appWrapper) => {
if (appWrapper) this.wrappers.push(appWrapper);
this.addAppWrapper(appWrapper);
},
setOutlet: (outletComponent) => {
if (outletComponent) this.outlet = outletComponent;
this.setOutlet(outletComponent);
},
};
}
}
export const boosts = new ReactRendererBoosts();
export function defineRendererPlugin(plugin: Plugin<ReactRendererBoostsApi>) {
return plugin;
}

View File

@ -0,0 +1,51 @@
import { type ComponentTree, type PageConfig } from '@alilc/lowcode-shared';
import { useMemo } from 'react';
import { useAppContext } from '../context';
import { OutletProps } from '../boosts';
import { useRouteLocation } from '../context';
import { createComponent } from '../../runtime/createComponent';
import { reactiveStateFactory } from '../../runtime/reactiveState';
export function RouteOutlet(props: OutletProps) {
const app = useAppContext();
const location = useRouteLocation();
const { schema, options } = app;
const pageConfig = useMemo(() => {
const pages = schema.get<PageConfig[]>('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') {
const componentsTrees = schema.get<ComponentTree[]>('componentsTree', []);
const target = componentsTrees.find((item) => item.id === pageConfig.mappingId);
if (!target) return null;
const {
stateCreator = reactiveStateFactory,
dataSourceCreator,
...otherOptions
} = options.component ?? {};
const LowCodeComponent = createComponent(target, app.codeRuntime, app.components, {
displayName: pageConfig.id,
modelOptions: {
metadata: pageConfig,
stateCreator,
dataSourceCreator,
},
...otherOptions,
});
return <LowCodeComponent {...props} />;
}
return null;
}

View File

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

View File

@ -0,0 +1,47 @@
import { AppContext } from '../context';
import { type App } from '../app';
import { getOrCreateComponent, reactiveStateFactory } from '../../runtime';
import { RouterView } from './routerView';
import { RouteOutlet } from './route';
import { type WrapperComponent, type Outlet } from '../boosts';
export const createAppView = (app: App, wrappers: WrapperComponent[], Outlet: Outlet | null) => {
return function ApplicationView() {
let element = (
<RouterView router={app.router}>{Outlet ? <Outlet /> : <RouteOutlet />}</RouterView>
);
const layoutConfig = app.schema.get<any>('config.layout');
if (layoutConfig) {
const componentName = layoutConfig.componentName;
const {
stateCreator = reactiveStateFactory,
dataSourceCreator,
...otherOptions
} = app.options.component ?? {};
const Layout = getOrCreateComponent(componentName, app.codeRuntime, app.components, {
displayName: componentName,
modelOptions: {
stateCreator,
dataSourceCreator,
},
...otherOptions,
});
if (Layout) {
const layoutProps: any = layoutConfig.props ?? {};
element = <Layout {...layoutProps}>{element}</Layout>;
}
}
if (wrappers.length > 0) {
element = wrappers.reduce((preElement, CurrentWrapper) => {
return <CurrentWrapper>{preElement}</CurrentWrapper>;
}, element);
}
return <AppContext.Provider value={app}>{element}</AppContext.Provider>;
};
};

View File

@ -1,5 +1,12 @@
import { type Router, type RouteLocationNormalized } from '@alilc/lowcode-renderer-router';
import { createContext, useContext } from 'react';
import { type App } from './app';
export const AppContext = createContext<App>(undefined!);
AppContext.displayName = 'AppContext';
export const useAppContext = () => useContext(AppContext);
export const RouterContext = createContext<Router>(undefined!);

View File

@ -1,2 +1,2 @@
export * from './boosts';
export * from './view';
export * from './app';
export * from './context';

View File

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

View File

@ -1,9 +1,6 @@
export * from './api/app';
export * from './api/component';
export * from './api/context';
export { defineRendererPlugin } from './app';
export * from './router';
export { LifecyclePhase } from '@alilc/lowcode-renderer-core';
export type { Package, ProCodeComponent, LowCodeComponent } from '@alilc/lowcode-shared';
export type {
@ -13,4 +10,4 @@ export type {
ModelDataSourceCreator,
ModelStateCreator,
} from '@alilc/lowcode-renderer-core';
export type { ReactRendererBoostsApi } from './app/boosts';
export type * from './app';

View File

@ -1,10 +0,0 @@
export * from './context';
export * from './plugin';
export type {
RouteLocation,
RawLocation,
Router,
RouterOptions,
RouteLocationNormalized,
RouterHistory,
} from '@alilc/lowcode-renderer-router';

View File

@ -1,39 +0,0 @@
import { defineRendererPlugin } from '../app/boosts';
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 { 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);
const RouterView = createRouterView(router);
boosts.addAppWrapper(RouterView);
boosts.setOutlet(RouteOutlet);
boosts.codeRuntime.getScope().set('router', router);
boosts.temporaryUse('router', router);
await router.isReady();
},
});

View File

@ -1,42 +0,0 @@
import { useMemo } from 'react';
import { useRendererContext } from '../api/context';
import { OutletProps } from '../app/boosts';
import { useRouteLocation } from './context';
import { createComponent } from '../runtime/createComponent';
export function RouteOutlet(props: OutletProps) {
const context = useRendererContext();
const location = useRouteLocation();
const { schema, packageManager, options } = 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 = createComponent(pageConfig.mappingId, {
displayName: pageConfig.id,
modelOptions: {
metadata: pageConfig,
},
...options.component,
});
return <LowCodeComponent {...props} />;
}
return null;
}

View File

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

View File

@ -1,20 +1,17 @@
import { invariant, specTypes, type ComponentTreeRoot } from '@alilc/lowcode-shared';
import { invariant, specTypes, type ComponentTree } from '@alilc/lowcode-shared';
import { type ComponentTreeModelOptions, ComponentTreeModel } from '@alilc/lowcode-renderer-core';
import { forwardRef, useRef, useEffect } from 'react';
import { isValidElementType } from 'react-is';
import { useRendererContext, IRendererContext } from '../api/context';
import { reactiveStateFactory } from './reactiveState';
import { type ReactComponent, type ReactWidget, createElementByWidget } from './elements';
import { appendExternalStyle } from '../utils/element';
import { type ComponentsAccessor } from '../app';
import type {
IComponentTreeModel,
CreateComponentTreeModelOptions,
} from '@alilc/lowcode-renderer-core';
import type { ICodeRuntime, IComponentTreeModel } from '@alilc/lowcode-renderer-core';
import type { ReactInstance, CSSProperties, ForwardedRef, ReactNode } from 'react';
export interface ComponentOptions {
displayName?: string;
modelOptions?: Pick<CreateComponentTreeModelOptions, 'id' | 'metadata'>;
modelOptions: ComponentTreeModelOptions;
beforeElementCreate?(widget: ReactWidget): ReactWidget;
elementCreated?(widget: ReactWidget, element: ReactNode): ReactNode;
@ -33,20 +30,22 @@ export interface LowCodeComponentProps {
const lowCodeComponentsCache = new Map<string, ReactComponent>();
export function getComponentByName(
export function getOrCreateComponent(
name: string,
{ packageManager }: IRendererContext,
componentOptions: ComponentOptions = {},
codeRuntime: ICodeRuntime,
components: ComponentsAccessor,
componentOptions: ComponentOptions,
): ReactComponent {
const result = lowCodeComponentsCache.get(name) || packageManager.getComponent(name);
const result = lowCodeComponentsCache.get(name) || components.getComponent(name);
if (specTypes.isLowCodeComponentPackage(result)) {
const { schema, ...metadata } = result;
const lowCodeComponent = createComponent(schema, {
const lowCodeComponent = createComponent(schema, codeRuntime, components, {
...componentOptions,
displayName: name,
modelOptions: {
...componentOptions.modelOptions,
id: metadata.id,
metadata,
},
@ -63,8 +62,10 @@ export function getComponentByName(
}
export function createComponent(
schema: string | ComponentTreeRoot,
componentOptions: ComponentOptions = {},
schema: ComponentTree,
codeRuntime: ICodeRuntime,
components: ComponentsAccessor,
componentOptions: ComponentOptions,
) {
const { displayName = '__LowCodeComponent__', modelOptions } = componentOptions;
@ -72,26 +73,17 @@ export function createComponent(
props: LowCodeComponentProps,
ref: ForwardedRef<any>,
) {
const context = useRendererContext();
const { options: globalOptions, componentTreeModel } = context;
const modelRef = useRef<IComponentTreeModel<ReactComponent, ReactInstance>>();
if (!modelRef.current) {
const finalOptions: CreateComponentTreeModelOptions = {
...modelOptions,
codeScopeValue: {
modelRef.current = new ComponentTreeModel(
schema,
codeRuntime.createChild({
props,
},
stateCreator: reactiveStateFactory,
dataSourceCreator: globalOptions.dataSourceCreator,
};
} as any),
modelOptions,
);
if (typeof schema === 'string') {
modelRef.current = componentTreeModel.createById(schema, finalOptions);
} else {
modelRef.current = componentTreeModel.create(schema, finalOptions);
}
console.log(
'%c [ model ]-103',
'font-size:13px; background:pink; color:#bf2c9f;',
@ -137,7 +129,14 @@ export function createComponent(
return (
<div id={props.id} className={props.className} style={props.style} ref={ref}>
{model.widgets.map((w) => createElementByWidget(w, w.model.codeRuntime, componentOptions))}
{model.widgets.map((w) =>
createElementByWidget({
widget: w,
codeRuntime: w.model.codeRuntime,
components,
options: componentOptions,
}),
)}
</div>
);
});

View File

@ -19,9 +19,9 @@ import {
createElement,
type ReactNode,
} from 'react';
import { useRendererContext } from '../api/context';
import { ComponentsAccessor } from '../app';
import { useReactiveStore } from './hooks/useReactiveStore';
import { getComponentByName, type ComponentOptions } from './createComponent';
import { getOrCreateComponent, type ComponentOptions } from './createComponent';
export type ReactComponent = ComponentType<any>;
export type ReactWidget = IWidget<ReactComponent, ReactInstance>;
@ -29,16 +29,13 @@ export type ReactWidget = IWidget<ReactComponent, ReactInstance>;
interface WidgetRendererProps {
widget: ReactWidget;
codeRuntime: ICodeRuntime;
components: ComponentsAccessor;
options: ComponentOptions;
[key: string]: any;
}
export function createElementByWidget(
widget: ReactWidget,
codeRuntime: ICodeRuntime,
options: ComponentOptions,
): ReactNode {
export function createElementByWidget(props: WidgetRendererProps): ReactNode {
const getElement = (widget: ReactWidget) => {
const { key, rawNode } = widget;
@ -47,11 +44,11 @@ export function createElementByWidget(
}
if (specTypes.isJSExpression(rawNode)) {
return <Text key={key} expr={rawNode} codeRuntime={codeRuntime} />;
return <Text key={key} expr={rawNode} codeRuntime={props.codeRuntime} />;
}
if (specTypes.isJSI18nNode(rawNode)) {
return <I18nText key={key} i18n={rawNode} codeRuntime={codeRuntime} />;
return <I18nText key={key} i18n={rawNode} codeRuntime={props.codeRuntime} />;
}
const { condition, loop } = widget.rawNode as NormalizedComponentNode;
@ -62,44 +59,32 @@ export function createElementByWidget(
if (Array.isArray(loop) && loop.length === 0) return null;
if (specTypes.isJSExpression(loop)) {
return (
<LoopWidgetRenderer
key={key}
loop={loop}
widget={widget}
codeRuntime={codeRuntime}
options={options}
/>
);
return <LoopWidgetRenderer key={key} loop={loop} {...props} />;
}
return (
<WidgetComponent key={key} widget={widget} codeRuntime={codeRuntime} options={options} />
);
return <WidgetComponent key={key} {...props} />;
};
if (options.beforeElementCreate) {
widget = options.beforeElementCreate(widget);
if (props.options.beforeElementCreate) {
props.widget = props.options.beforeElementCreate(props.widget);
}
const element = getElement(widget);
const element = getElement(props.widget);
if (options.elementCreated) {
return options.elementCreated(widget, element);
if (props.options.elementCreated) {
return props.options.elementCreated(props.widget, element);
}
return element;
}
export function WidgetComponent(props: WidgetRendererProps) {
const { widget, codeRuntime, options, ...otherProps } = props;
const { widget, codeRuntime, components, options, ...otherProps } = props;
const componentNode = widget.rawNode as NormalizedComponentNode;
const { ref, ...componentProps } = componentNode.props;
const context = useRendererContext();
const Component = useMemo(
() => getComponentByName(componentNode.componentName, context, options),
() => getOrCreateComponent(componentNode.componentName, codeRuntime, components, options),
[widget],
);
@ -123,15 +108,15 @@ export function WidgetComponent(props: WidgetRendererProps) {
}, {} as StringDictionary);
return widgets.map((n) =>
createElementByWidget(
n,
codeRuntime.createChild({ initScopeValue: params }),
options,
),
createElementByWidget({
...props,
widget: n,
codeRuntime: codeRuntime.createChild({ initScopeValue: params }),
}),
);
};
} else {
return widgets.map((n) => createElementByWidget(n, codeRuntime, options));
return widgets.map((n) => createElementByWidget({ ...props, widget: n }));
}
}
} else if (specTypes.isJSFunction(node)) {
@ -183,7 +168,7 @@ export function WidgetComponent(props: WidgetRendererProps) {
key: widget.key,
ref: attachRef,
},
widget.children?.map((item) => createElementByWidget(item, codeRuntime, options)) ?? [],
widget.children?.map((item) => createElementByWidget({ ...props, widget: item })) ?? [],
);
}
@ -213,19 +198,12 @@ function I18nText(props: { i18n: JSI18n; codeRuntime: ICodeRuntime }) {
I18nText.displayName = 'I18nText';
function LoopWidgetRenderer({
loop,
widget,
codeRuntime,
options,
...otherProps
}: {
interface LoopWidgetRendererProps extends WidgetRendererProps {
loop: JSExpression;
widget: ReactWidget;
codeRuntime: ICodeRuntime;
options: ComponentOptions;
[key: string]: any;
}) {
}
function LoopWidgetRenderer(props: LoopWidgetRendererProps) {
const { loop, widget, codeRuntime, ...otherProps } = props;
const { condition, loopArgs } = widget.rawNode as NormalizedComponentNode;
const state = useReactiveStore({
target: {
@ -252,7 +230,6 @@ function LoopWidgetRenderer({
key={`loop-${widget.key}-${idx}`}
widget={widget}
codeRuntime={childRuntime}
options={options}
/>
);
});

View File

@ -1,2 +1,3 @@
export * from './createComponent';
export * from './reactiveState';
export * from './elements';

View File

@ -18,18 +18,17 @@ export interface ICodeRuntimeService extends IDisposable {
export const ICodeRuntimeService = createDecorator<ICodeRuntimeService>('codeRuntimeService');
export class CodeRuntimeService extends Disposable implements ICodeRuntimeService {
private _rootRuntime: ICodeRuntime;
private _rootRuntime?: ICodeRuntime;
get rootRuntime() {
if (!this._rootRuntime) {
this._rootRuntime = this._addDispose(new CodeRuntime());
}
return this._rootRuntime;
}
constructor(
options: CodeRuntimeOptions = {},
@ISchemaService private schemaService: ISchemaService,
) {
constructor(@ISchemaService private schemaService: ISchemaService) {
super();
this._rootRuntime = this._addDispose(new CodeRuntime(options));
this._addDispose(
this.schemaService.onSchemaUpdate(({ key, data }) => {
if (key === 'constants') {
@ -39,6 +38,10 @@ export class CodeRuntimeService extends Disposable implements ICodeRuntimeServic
);
}
initialize(options: CodeRuntimeOptions): void {
this._rootRuntime = this._addDispose(new CodeRuntime(options));
}
createCodeRuntime<T extends StringDictionary = StringDictionary>(
options: CodeRuntimeOptions<T> = {},
): ICodeRuntime<T> {

View File

@ -0,0 +1,3 @@
export * from './componentTreeModel';
export type { IWidget } from './widget';

View File

@ -1,5 +1,5 @@
import { type NodeType, uniqueId, type ComponentNode } from '@alilc/lowcode-shared';
import { IComponentTreeModel } from '../services/model';
import { IComponentTreeModel } from './componentTreeModel';
export interface IWidget<Component, ComponentInstance = unknown> {
readonly key: string;

View File

@ -1,123 +0,0 @@
import {
invariant,
InstantiationService,
BeanContainer,
CtorDescriptor,
type Project,
type Package,
} from '@alilc/lowcode-shared';
import { CodeRuntimeService, ICodeRuntimeService, type CodeRuntimeOptions } from './code-runtime';
import {
IExtensionHostService,
type RenderAdapter,
type IRenderObject,
ExtensionHostService,
type Plugin,
} from './extension';
import { IPackageManagementService, PackageManagementService } from './package';
import { ISchemaService, SchemaService } from './schema';
import { ILifeCycleService, LifecyclePhase, LifeCycleService } from './life-cycle';
import { IRuntimeIntlService, RuntimeIntlService } from './intl';
import { IRuntimeUtilService, RuntimeUtilService } from './util';
import { type ModelDataSourceCreator } from './model';
export interface AppOptions {
schema: Project;
packages?: Package[];
plugins?: Plugin[];
/**
* code runtime
*/
codeRuntime?: CodeRuntimeOptions;
/**
*
*/
dataSourceCreator?: ModelDataSourceCreator;
}
export type RendererApplication<Render = unknown> = {
readonly mode: 'development' | 'production';
readonly schema: Omit<ISchemaService, 'initialize'>;
readonly packageManager: IPackageManagementService;
use(plugin: Plugin): Promise<void>;
destroy(): void;
} & Render;
export function createRenderer<RenderObject = IRenderObject>(
renderAdapter: RenderAdapter<RenderObject>,
): (options: AppOptions) => Promise<RendererApplication<RenderObject>> {
invariant(typeof renderAdapter === 'function', 'The first parameter must be a function.');
return async (options) => {
// create services
const container = new BeanContainer();
const lifeCycleService = new LifeCycleService();
container.set(ILifeCycleService, lifeCycleService);
const schemaService = new SchemaService(options.schema);
container.set(ISchemaService, schemaService);
container.set(
ICodeRuntimeService,
new CtorDescriptor(CodeRuntimeService, [options.codeRuntime]),
);
container.set(IPackageManagementService, new CtorDescriptor(PackageManagementService));
const utils = schemaService.get('utils');
container.set(IRuntimeUtilService, new CtorDescriptor(RuntimeUtilService, [utils]));
const defaultLocale = schemaService.get('config.defaultLocale');
const i18ns = schemaService.get('i18n', {});
container.set(
IRuntimeIntlService,
new CtorDescriptor(RuntimeIntlService, [defaultLocale, i18ns]),
);
container.set(IExtensionHostService, new CtorDescriptor(ExtensionHostService));
const instantiationService = new InstantiationService(container);
lifeCycleService.setPhase(LifecyclePhase.OptionsResolved);
const [extensionHostService, packageManagementService] = instantiationService.invokeFunction(
(accessor) => {
return [accessor.get(IExtensionHostService), accessor.get(IPackageManagementService)];
},
);
const renderObject = await renderAdapter(instantiationService);
await extensionHostService.registerPlugin(options.plugins ?? []);
await packageManagementService.loadPackages(options.packages ?? []);
lifeCycleService.setPhase(LifecyclePhase.Ready);
const app: RendererApplication<RenderObject> = {
get mode() {
return __DEV__ ? 'development' : 'production';
},
schema: schemaService,
packageManager: packageManagementService,
...renderObject,
use: (plugin) => {
return extensionHostService.registerPlugin(plugin);
},
destroy: () => {
lifeCycleService.setPhase(LifecyclePhase.Destroying);
instantiationService.dispose();
},
};
if (__DEV__) {
Object.defineProperty(app, '__options', { get: () => options });
}
return app;
};
}

View File

@ -1,8 +1,8 @@
import { type StringDictionary } from '@alilc/lowcode-shared';
import { isObject } from 'lodash-es';
import { ICodeRuntime, ICodeRuntimeService } from '../code-runtime';
import { IRuntimeUtilService } from '../util/utilService';
import { IRuntimeIntlService } from '../intlService';
import { IRuntimeUtilService } from '../util';
import { IRuntimeIntlService } from '../intl';
export type IBoosts<Extends> = IBoostsApi & Extends & { [key: string]: any };

View File

@ -3,7 +3,6 @@ import { type Plugin, type PluginContext } from './plugin';
import { BoostsManager } from './boosts';
import { IPackageManagementService } from '../package';
import { ISchemaService } from '../schema';
import { ILifeCycleService } from '../life-cycle/lifeCycleService';
import { ICodeRuntimeService } from '../code-runtime';
import { IRuntimeIntlService } from '../intl';
import { IRuntimeUtilService } from '../util';
@ -28,7 +27,6 @@ export class ExtensionHostService extends Disposable implements IExtensionHostSe
private _pluginSetupContext: PluginContext;
constructor(
@ILifeCycleService lifeCycleService: ILifeCycleService,
@IPackageManagementService packageManagementService: IPackageManagementService,
@ISchemaService schemaService: ISchemaService,
@ICodeRuntimeService codeRuntimeService: ICodeRuntimeService,
@ -48,10 +46,6 @@ export class ExtensionHostService extends Disposable implements IExtensionHostSe
boosts: this.boostsManager.toExpose(),
schema: schemaService,
packageManager: packageManagementService,
whenLifeCylePhaseChange: (phase) => {
return lifeCycleService.when(phase);
},
};
}
@ -101,9 +95,8 @@ export class ExtensionHostService extends Disposable implements IExtensionHostSe
private async _doSetupPlugin(plugin: Plugin) {
if (this._activePlugins.has(plugin.name)) return;
await plugin.setup(this._pluginSetupContext);
this._addDispose(await plugin.setup(this._pluginSetupContext));
this._activePlugins.add(plugin.name);
this._addDispose(plugin);
}
getPlugin(name: string): Plugin | undefined {

View File

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

View File

@ -1,23 +1,19 @@
import { type StringDictionary, type IDisposable } from '@alilc/lowcode-shared';
import { type IDisposable } from '@alilc/lowcode-shared';
import { type IBoosts } from './boosts';
import { ILifeCycleService } from '../life-cycle/lifeCycleService';
import { type ISchemaService } from '../schema';
import { type IPackageManagementService } from '../package';
import { type IStore } from '../../utils/store';
export interface PluginContext<BoostsExtends = object> {
globalState: IStore<StringDictionary, string>;
globalState: Map<string, any>;
boosts: IBoosts<BoostsExtends>;
schema: Pick<ISchemaService, 'get' | 'set'>;
packageManager: IPackageManagementService;
whenLifeCylePhaseChange: ILifeCycleService['when'];
}
export interface Plugin<BoostsExtends = object> extends IDisposable {
export interface Plugin<BoostsExtends = object> {
/**
* name
*/
@ -26,7 +22,7 @@ export interface Plugin<BoostsExtends = object> extends IDisposable {
*
* @param context
*/
setup(context: PluginContext<BoostsExtends>): void | Promise<void>;
setup(context: PluginContext<BoostsExtends>): IDisposable | Promise<IDisposable>;
/**
*
*/

View File

@ -1,10 +0,0 @@
import { type IInstantiationService } from '@alilc/lowcode-shared';
export interface IRenderObject {
mount: (containerOrId?: string | HTMLElement) => void | Promise<void>;
unmount: () => void | Promise<void>;
}
export interface RenderAdapter<Render> {
(instantiationService: IInstantiationService): Render | Promise<Render>;
}

View File

@ -1,20 +1,8 @@
/* --------------- api -------------------- */
export * from './createRenderer';
export { IExtensionHostService } from './extension';
export { definePackageLoader, IPackageManagementService } from './package';
export { LifecyclePhase, ILifeCycleService } from './life-cycle';
export { IComponentTreeModelService } from './model';
export { ICodeRuntimeService, mapValue, someValue } from './code-runtime';
export { IRuntimeIntlService } from './intl';
export { IRuntimeUtilService } from './util';
export { ISchemaService } from './schema';
export { Widget } from './widget';
/* --------------- types ---------------- */
export type * from './extension';
export type * from './code-runtime';
export type * from './model';
export type * from './package';
export type * from './schema';
export type * from './extension';
export type * from './widget';
export * from './extension';
export * from './code-runtime';
export * from './component-tree-model';
export * from './package';
export * from './schema';
export * from './extension';
export * from './intl';
export * from './util';

View File

@ -7,7 +7,6 @@ import {
type LocaleTranslationsMap,
Disposable,
} from '@alilc/lowcode-shared';
import { ICodeRuntimeService } from '../code-runtime';
export interface MessageDescriptor {
key: string;
@ -16,6 +15,8 @@ export interface MessageDescriptor {
}
export interface IRuntimeIntlService {
initialize(locale: string | undefined, i18nTranslations: LocaleTranslationsMap): IntlApi;
localize(descriptor: MessageDescriptor): string;
setLocale(locale: Locale): void;
@ -28,20 +29,31 @@ export interface IRuntimeIntlService {
export const IRuntimeIntlService = createDecorator<IRuntimeIntlService>('IRuntimeIntlService');
export class RuntimeIntlService extends Disposable implements IRuntimeIntlService {
private _intl: Intl;
private _intl: Intl = this._addDispose(new Intl());
constructor(
defaultLocale: string | undefined,
i18nTranslations: LocaleTranslationsMap,
@ICodeRuntimeService private codeRuntimeService: ICodeRuntimeService,
) {
constructor() {
super();
}
this._intl = this._addDispose(new Intl(defaultLocale));
initialize(locale: string | undefined, i18nTranslations: LocaleTranslationsMap): IntlApi {
if (locale) this._intl.setLocale(locale);
for (const key of Object.keys(i18nTranslations)) {
this._intl.addTranslations(key, i18nTranslations[key]);
}
this._injectScope();
const exposed: IntlApi = {
i18n: (key, params) => {
return this.localize({ key, params });
},
getLocale: () => {
return this.getLocale();
},
setLocale: (locale) => {
this.setLocale(locale);
},
};
return exposed;
}
localize(descriptor: MessageDescriptor): string {
@ -65,20 +77,4 @@ export class RuntimeIntlService extends Disposable implements IRuntimeIntlServic
addTranslations(locale: Locale, translations: Translations) {
this._intl.addTranslations(locale, translations);
}
private _injectScope(): void {
const exposed: IntlApi = {
i18n: (key, params) => {
return this.localize({ key, params });
},
getLocale: () => {
return this.getLocale();
},
setLocale: (locale) => {
this.setLocale(locale);
},
};
this.codeRuntimeService.rootRuntime.getScope().setValue(exposed);
}
}

View File

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

View File

@ -1,99 +0,0 @@
import { createDecorator, Barrier } from '@alilc/lowcode-shared';
/**
*
*/
export const enum LifecyclePhase {
/**
*
*/
Starting = 1,
/**
*
*/
OptionsResolved = 2,
/**
*
*/
Ready = 3,
/**
*
*/
Destroying = 4,
}
export interface ILifeCycleService {
/**
* A flag indicating in what phase of the lifecycle we currently are.
*/
phase: LifecyclePhase;
setPhase(phase: LifecyclePhase): void;
/**
* Returns a promise that resolves when a certain lifecycle phase
* has started.
*/
when(phase: LifecyclePhase): Promise<void>;
onWillDestory(): void;
}
export function LifecyclePhaseToString(phase: LifecyclePhase): string {
switch (phase) {
case LifecyclePhase.Starting:
return 'Starting';
case LifecyclePhase.OptionsResolved:
return 'OptionsResolved';
case LifecyclePhase.Ready:
return 'Ready';
case LifecyclePhase.Destroying:
return 'Destroying';
}
}
export const ILifeCycleService = createDecorator<ILifeCycleService>('lifeCycleService');
export class LifeCycleService implements ILifeCycleService {
private readonly phaseWhen = new Map<LifecyclePhase, Barrier>();
private _phase = LifecyclePhase.Starting;
get phase(): LifecyclePhase {
return this._phase;
}
setPhase(value: LifecyclePhase) {
if (value < this._phase) {
throw new Error('Lifecycle cannot go backwards');
}
if (this._phase === value) {
return;
}
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();
}
onWillDestory(): void {}
}

View File

@ -1,73 +0,0 @@
import {
createDecorator,
type IDisposable,
Disposable,
invariant,
type ComponentTree,
type StringDictionary,
} from '@alilc/lowcode-shared';
import { ICodeRuntimeService } from '../code-runtime';
import {
type IComponentTreeModel,
ComponentTreeModel,
type ComponentTreeModelOptions,
} from './componentTreeModel';
import { ISchemaService } from '../schema';
export interface CreateComponentTreeModelOptions extends ComponentTreeModelOptions {
codeScopeValue?: StringDictionary;
}
export interface IComponentTreeModelService extends IDisposable {
create<Component>(
componentsTree: ComponentTree,
options?: CreateComponentTreeModelOptions,
): IComponentTreeModel<Component>;
createById<Component>(
id: string,
options?: CreateComponentTreeModelOptions,
): IComponentTreeModel<Component>;
}
export const IComponentTreeModelService = createDecorator<IComponentTreeModelService>(
'componentTreeModelService',
);
export class ComponentTreeModelService extends Disposable implements IComponentTreeModelService {
constructor(
@ISchemaService private schemaService: ISchemaService,
@ICodeRuntimeService private codeRuntimeService: ICodeRuntimeService,
) {
super();
}
create<Component>(
componentsTree: ComponentTree,
options: CreateComponentTreeModelOptions,
): IComponentTreeModel<Component> {
this._throwIfDisposed(`ComponentTreeModelService has been disposed.`);
return this._addDispose(
new ComponentTreeModel(
componentsTree,
this.codeRuntimeService.createCodeRuntime({
initScopeValue: options?.codeScopeValue as any,
}),
options,
),
);
}
createById<Component>(
id: string,
options: CreateComponentTreeModelOptions,
): IComponentTreeModel<Component> {
const componentsTrees = this.schemaService.get<ComponentTree[]>('componentsTree', []);
const componentsTree = componentsTrees.find((item) => item.id === id);
invariant(componentsTree, 'componentsTree not found');
return this.create(componentsTree, options);
}
}

View File

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

View File

@ -11,6 +11,8 @@ export type SchemaUpdateEvent = { key: string; previous: any; data: any };
export interface ISchemaService {
readonly onSchemaUpdate: Events.Event<SchemaUpdateEvent>;
initialize(schema: unknown): void;
get<T>(key: string): T | undefined;
get<T>(key: string, defaultValue?: T): T;
@ -20,20 +22,22 @@ export interface ISchemaService {
export const ISchemaService = createDecorator<ISchemaService>('schemaService');
export class SchemaService extends Disposable implements ISchemaService {
private store: NormalizedSchema;
private _schema: NormalizedSchema;
private _observer = this._addDispose(new Events.Emitter<SchemaUpdateEvent>());
private _onSchemaUpdate = this._addDispose(new Events.Emitter<SchemaUpdateEvent>());
readonly onSchemaUpdate = this._observer.event;
readonly onSchemaUpdate = this._onSchemaUpdate.event;
constructor(schema: unknown) {
constructor() {
super();
}
initialize(schema: unknown) {
if (!isObject(schema)) {
throw Error('schema must a object');
}
this.store = {} as any;
this._schema = {} as any;
for (const key of Object.keys(schema)) {
const value = (schema as any)[key];
@ -52,12 +56,12 @@ export class SchemaService extends Disposable implements ISchemaService {
set(key: string, value: any): void {
const previous = this.get(key);
if (!isEqual(previous, value)) {
lodashSet(this.store, key, value);
this._observer.notify({ key, previous, data: value });
lodashSet(this._schema, key, value);
this._onSchemaUpdate.notify({ key, previous, data: value });
}
}
get<T>(key: string, defaultValue?: T): T {
return (lodashGet(this.store, key) ?? defaultValue) as T;
return (lodashGet(this._schema, key) ?? defaultValue) as T;
}
}

View File

@ -3,12 +3,17 @@ import {
type UtilDescription,
createDecorator,
type StringDictionary,
Disposable,
type UtilsApi,
} from '@alilc/lowcode-shared';
import { isPlainObject } from 'lodash-es';
import { IPackageManagementService } from '../package';
import { ICodeRuntimeService } from '../code-runtime';
import { ISchemaService } from '../schema';
export interface IRuntimeUtilService {
initialize(): UtilsApi;
add(utilItem: UtilDescription, force?: boolean): void;
add(name: string, target: AnyFunction | StringDictionary, force?: boolean): void;
@ -17,18 +22,41 @@ export interface IRuntimeUtilService {
export const IRuntimeUtilService = createDecorator<IRuntimeUtilService>('rendererUtilService');
export class RuntimeUtilService implements IRuntimeUtilService {
export class RuntimeUtilService extends Disposable implements IRuntimeUtilService {
private _utilsMap: Map<string, any> = new Map();
constructor(
utils: UtilDescription[] = [],
@ICodeRuntimeService private codeRuntimeService: ICodeRuntimeService,
@IPackageManagementService private packageManagementService: IPackageManagementService,
@ISchemaService private schemaService: ISchemaService,
) {
for (const util of utils) {
this.add(util);
super();
this._addDispose(
this.schemaService.onSchemaUpdate(({ key, data }) => {
if (key === 'utils') {
for (const item of data) {
this.add(item);
}
this._injectScope();
}
}),
);
}
initialize(): UtilsApi {
const exposed: UtilsApi = 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 exposed;
}
add(utilItem: UtilDescription, force?: boolean): void;
@ -93,20 +121,4 @@ export class RuntimeUtilService implements IRuntimeUtilService {
return this.packageManagementService.getModuleByReference(utilItem.content);
}
}
private _injectScope(): 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);
},
});
this.codeRuntimeService.rootRuntime.getScope().set('utils', exposed);
}
}

View File

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

View File

@ -47,7 +47,7 @@ export abstract class Disposable implements IDisposable {
/**
* Adds `o` to the collection of disposables managed by this object.
*/
protected addDispose<T extends IDisposable>(o: T): T {
protected _addDispose<T extends IDisposable>(o: T): T {
this._throwIfDisposed();
if ((o as unknown as Disposable) === this) {
throw new Error('Cannot register a disposable on itself!');

View File

@ -1,4 +1,4 @@
import { isDisposable } from '../disposable';
import { dispose, isDisposable } from '../disposable';
import { Graph, CyclicDependencyError } from '../graph';
import {
type BeanIdentifier,
@ -14,8 +14,6 @@ export interface InstanceAccessor {
}
export interface IInstantiationService {
readonly container: BeanContainer;
createInstance<T extends Constructor>(Ctor: T, ...args: any[]): InstanceType<T>;
invokeFunction<R, Args extends any[] = []>(
@ -23,6 +21,8 @@ export interface IInstantiationService {
...args: Args
): R;
createChild(container: BeanContainer): IInstantiationService;
dispose(): void;
}
@ -31,11 +31,47 @@ export const IInstantiationService = createDecorator<IInstantiationService>('ins
export class InstantiationService implements IInstantiationService {
private _activeInstantiations = new Set<BeanIdentifier<any>>();
private _children = new Set<InstantiationService>();
private _isDisposed = false;
private readonly _beansToMaybeDispose = new Set<any>();
constructor(public readonly container: BeanContainer = new BeanContainer()) {
this.container.set(IInstantiationService, this);
constructor(
private readonly _container: BeanContainer = new BeanContainer(),
private readonly _parent?: InstantiationService,
) {
this._container.set(IInstantiationService, this);
}
createChild(container: BeanContainer): IInstantiationService {
this._throwIfDisposed();
const that = this;
const result = new (class extends InstantiationService {
override dispose(): void {
that._children.delete(result);
super.dispose();
}
})(container, this);
this._children.add(result);
return result;
}
dispose(): void {
if (this._isDisposed) return;
// dispose all child services
dispose(this._children);
this._children.clear();
// dispose all services created by this service
for (const candidate of this._beansToMaybeDispose) {
if (isDisposable(candidate)) {
candidate.dispose();
}
}
this._beansToMaybeDispose.clear();
this._isDisposed = true;
}
/**
@ -92,7 +128,7 @@ export class InstantiationService implements IInstantiationService {
}
private _getOrCreateInstance<T>(id: BeanIdentifier<T>): T {
const thing = this.container.get(id);
const thing = this._container.get(id);
if (thing instanceof CtorDescriptor) {
return this._safeCreateAndCacheInstance<T>(id, thing);
} else {
@ -138,7 +174,7 @@ export class InstantiationService implements IInstantiationService {
// check all dependencies for existence and if they need to be created first
for (const dependency of getBeanDependecies(item.desc.ctor)) {
const instanceOrDesc = this.container.get(dependency.id);
const instanceOrDesc = this._container.get(dependency.id);
if (!instanceOrDesc) {
throw new Error(
`[createInstance] ${id} depends on ${dependency.id} which is NOT registered.`,
@ -171,35 +207,45 @@ export class InstantiationService implements IInstantiationService {
// Repeat the check for this still being a service sync descriptor. That's because
// instantiating a dependency might have side-effect and recursively trigger instantiation
// so that some dependencies are now fullfilled already.
const instanceOrDesc = this.container.get(data.id);
const instanceOrDesc = this._container.get(data.id);
if (instanceOrDesc instanceof CtorDescriptor) {
// create instance and overwrite the service collections
const instance = this.createInstance(
instanceOrDesc.ctor,
instanceOrDesc.staticArguments,
const instance = this._createServiceInstanceWithOwner(
data.id,
data.desc.ctor,
data.desc.staticArguments,
);
this._beansToMaybeDispose.add(instance);
this.container.set(data.id, instance);
this._setCreatedServiceInstance(data.id, instance);
}
graph.removeNode(data);
}
}
}
return this.container.get(id) as T;
return this._container.get(id) as T;
}
dispose(): void {
if (this._isDisposed) return;
private _createServiceInstanceWithOwner<T>(id: BeanIdentifier<T>, ctor: any, args: any[]): T {
if (this._container.get(id) instanceof CtorDescriptor) {
const instance = this.createInstance(ctor, args);
this._beansToMaybeDispose.add(instance);
// dispose all services created by this service
for (const candidate of this._beansToMaybeDispose) {
if (isDisposable(candidate)) {
candidate.dispose();
return instance;
} else if (this._parent) {
return this._parent._createServiceInstanceWithOwner(id, ctor, args);
} else {
throw new Error(`illegalState - creating UNKNOWN service instance ${ctor.name}`);
}
}
this._beansToMaybeDispose.clear();
this._isDisposed = true;
private _setCreatedServiceInstance<T>(id: BeanIdentifier<T>, instance: T): void {
if (this._container.get(id) instanceof CtorDescriptor) {
this._container.set(id, instance);
} else if (this._parent) {
this._parent._setCreatedServiceInstance(id, instance);
} else {
throw new Error('illegalState - setting UNKNOWN service instance');
}
}
private _throwIfDisposed(): void {

View File

@ -29,7 +29,7 @@ export class Intl extends Disposable {
return this._messageStore.value[this._locale.value] ?? {};
});
this.addDispose(
this._addDispose(
toDisposable(
effect(() => {
const cache = createIntlCache();

View File

@ -3,7 +3,7 @@
*
*/
import { StringDictionary } from '..';
import { ComponentTreeRoot } from './lowcode-spec';
import { ComponentTree } from './lowcode-spec';
import { ComponentMetaData, Reference } from './material-spec';
/**
@ -85,7 +85,7 @@ export interface Package {
/**
* schema
*/
schema?: ComponentTreeRoot;
schema?: ComponentTree;
/**
* id
*/