fix: fix renderer some bugs

This commit is contained in:
1ncounter 2024-06-28 18:04:01 +08:00
parent ac8aa2c5a4
commit a855c05d67
33 changed files with 257 additions and 390 deletions

View File

@ -34,13 +34,7 @@
},
"dependencies": {
"@alilc/lowcode-shared": "workspace:*",
"@alilc/lowcode-types": "workspace:*",
"@alilc/lowcode-utils": "workspace:*",
"@formatjs/intl": "^2.10.1",
"lodash-es": "^4.17.21",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"events": "^3.3.0"
"lodash-es": "^4.17.21"
},
"devDependencies": {
"@types/lodash-es": "^4.17.12",
@ -49,8 +43,6 @@
},
"peerDependencies": {
"@alilc/lowcode-shared": "workspace:*",
"@alilc/lowcode-types": "workspace:*",
"@alilc/lowcode-utils": "workspace:*",
"react": "^18.2.0",
"react-dom": "^18.2.0"
},

View File

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

View File

@ -1,154 +0,0 @@
import {
signal,
computed,
effect,
createLogger,
type Spec,
type Signal,
type ComputedSignal,
type PlainObject,
} from '@alilc/lowcode-shared';
import { createIntl, createIntlCache, type IntlShape as IntlFormatter } from '@formatjs/intl';
import { mapKeys } from 'lodash-es';
export { IntlFormatter };
const logger = createLogger({ level: 'warn', bizName: 'globalLocale' });
/**
* todo: key
*/
const STORED_LOCALE_KEY = 'ali-lowcode-config';
export type Locale = string;
export type IntlMessage = Spec.I18nMap[Locale];
export type IntlMessageRecord = Spec.I18nMap;
export class Intl {
#locale: Signal<Locale>;
#messageStore: Signal<IntlMessageRecord>;
#currentMessage: ComputedSignal<IntlMessage>;
#intlShape: IntlFormatter;
constructor(defaultLocale?: string, messages: IntlMessageRecord = {}) {
if (defaultLocale) {
defaultLocale = nomarlizeLocale(defaultLocale);
} else {
defaultLocale = 'zh-CN';
}
const messageStore = mapKeys(messages, (_, key) => {
return nomarlizeLocale(key);
});
this.#locale = signal(defaultLocale);
this.#messageStore = signal(messageStore);
this.#currentMessage = computed(() => {
return this.#messageStore.value[this.#locale.value] ?? {};
});
effect(() => {
const cache = createIntlCache();
this.#intlShape = createIntl(
{
locale: this.#locale.value,
messages: this.#currentMessage.value,
},
cache,
);
});
}
getLocale() {
return this.#locale.value;
}
setLocale(locale: Locale) {
const nomarlizedLocale = nomarlizeLocale(locale);
this.#locale.value = nomarlizedLocale;
}
addMessages(locale: Locale, messages: IntlMessage) {
locale = nomarlizeLocale(locale);
const original = this.#messageStore.value[locale];
this.#messageStore.value[locale] = Object.assign(original, messages);
}
getFormatter(): IntlFormatter {
return this.#intlShape;
}
}
function initializeLocale() {
let result: Locale | undefined;
let config: PlainObject = {};
try {
// store 1: config from storage
config = JSON.parse(localStorage.getItem(STORED_LOCALE_KEY) || '');
} catch {
// ignore;
}
if (config?.locale) {
result = (config.locale || '').replace('_', '-');
logger.debug(`getting locale from localStorage: ${result}`);
}
if (!result && navigator.language) {
// store 2: config from system
result = nomarlizeLocale(navigator.language);
}
if (!result) {
logger.warn(
'something when wrong when trying to get locale, use zh-CN as default, please check it out!',
);
result = 'zh-CN';
}
return result;
}
const navigatorLanguageMapping: Record<string, string> = {
en: 'en-US',
zh: 'zh-CN',
zt: 'zh-TW',
es: 'es-ES',
pt: 'pt-PT',
fr: 'fr-FR',
de: 'de-DE',
it: 'it-IT',
ru: 'ru-RU',
ja: 'ja-JP',
ko: 'ko-KR',
ar: 'ar-SA',
tr: 'tr-TR',
th: 'th-TH',
vi: 'vi-VN',
nl: 'nl-NL',
he: 'iw-IL',
id: 'in-ID',
pl: 'pl-PL',
hi: 'hi-IN',
uk: 'uk-UA',
ms: 'ms-MY',
tl: 'tl-PH',
};
/**
* nomarlize navigator.language or user input's locale
* eg: zh -> zh-CN, zh_CN -> zh-CN, zh-cn -> zh-CN
* @param target
*/
function nomarlizeLocale(target: Locale) {
if (navigatorLanguageMapping[target]) {
return navigatorLanguageMapping[target];
}
const replaced = target.replace('_', '-');
const splited = replaced.split('-').slice(0, 2);
splited[1] = splited[1].toUpperCase();
return splited.join('-');
}

View File

@ -1,22 +1,12 @@
import { createRenderer, type AppOptions } from '@alilc/lowcode-renderer-core';
import { type ComponentType } from 'react';
import { createRenderer } from '@alilc/lowcode-renderer-core';
import { type Root, createRoot } from 'react-dom/client';
import { ApplicationView, RendererContext, boosts } from '../app';
export interface ReactAppOptions extends AppOptions {
faultComponent?: ComponentType<any>;
}
import { type ReactAppOptions, RendererContext } from './context';
import { ApplicationView, boosts } from '../app';
export const createApp = async (options: ReactAppOptions) => {
return createRenderer(async (context) => {
const { schema, boostsManager } = context;
// set config
// if (options.faultComponent) {
// context.config.set('faultComponent', options.faultComponent);
// }
// extends boosts
boostsManager.extend(boosts.toExpose());
let root: Root | undefined;
@ -27,10 +17,11 @@ export const createApp = async (options: ReactAppOptions) => {
const defaultId = schema.get('config')?.targetRootID ?? 'app';
const rootElement = normalizeContainer(containerOrId, defaultId);
const contextValue = { ...context, options };
root = createRoot(rootElement);
root.render(
<RendererContext.Provider value={context}>
<RendererContext.Provider value={contextValue}>
<ApplicationView />
</RendererContext.Provider>,
);

View File

@ -1,7 +1,7 @@
import { createRenderer, type AppOptions } from '@alilc/lowcode-renderer-core';
import { FunctionComponent } from 'react';
import { type LowCodeComponentProps, createComponentBySchema } from '../runtime/schema';
import { RendererContext } from '../app/context';
import { RendererContext } from '../api/context';
interface Render {
toComponent(): FunctionComponent<LowCodeComponentProps>;
@ -12,10 +12,11 @@ export async function createComponent(options: AppOptions) {
const { schema } = context;
const LowCodeComponent = createComponentBySchema(schema.get('componentsTree')[0]);
const contextValue = { ...context, options };
function Component(props: LowCodeComponentProps) {
return (
<RendererContext.Provider value={context}>
<RendererContext.Provider value={contextValue}>
<LowCodeComponent {...props} />
</RendererContext.Provider>
);

View File

@ -0,0 +1,14 @@
import { type ComponentType, createContext, useContext } from 'react';
import { type AppOptions, type RenderContext } from '@alilc/lowcode-renderer-core';
export interface ReactAppOptions extends AppOptions {
faultComponent?: ComponentType<any>;
}
export const RendererContext = createContext<RenderContext & { options: ReactAppOptions }>(
undefined!,
);
RendererContext.displayName = 'RendererContext';
export const useRendererContext = () => useContext(RendererContext);

View File

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

View File

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

View File

@ -1,10 +1,10 @@
import { useRenderContext } from './context';
import { useRendererContext } from '../api/context';
import { getComponentByName } from '../runtime/schema';
import { boosts } from './boosts';
export function ApplicationView() {
const renderContext = useRenderContext();
const { schema } = renderContext;
const rendererContext = useRendererContext();
const { schema } = rendererContext;
const appWrappers = boosts.getAppWrappers();
const Outlet = boosts.getOutlet();
@ -16,7 +16,7 @@ export function ApplicationView() {
if (layoutConfig) {
const componentName = layoutConfig.componentName;
const Layout = getComponentByName(componentName, renderContext);
const Layout = getComponentByName(componentName, rendererContext);
if (Layout) {
const layoutProps: any = layoutConfig.props ?? {};

View File

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

View File

@ -1,3 +1,3 @@
export * from './context';
export * from './plugin';
export type { Router, RouterHistory } from '@alilc/lowcode-renderer-router';
export type * from '@alilc/lowcode-renderer-router';

View File

@ -1,5 +1,4 @@
import { defineRendererPlugin } from '../app/boosts';
import { LifecyclePhase } from '@alilc/lowcode-renderer-core';
import { createRouter, type RouterOptions } from '@alilc/lowcode-renderer-router';
import { createRouterView } from './routerView';
import { RouteOutlet } from './route';
@ -13,7 +12,7 @@ const defaultRouterOptions: RouterOptions = {
export const routerPlugin = defineRendererPlugin({
name: 'rendererRouter',
async setup(context) {
const { whenLifeCylePhaseChange, schema, boosts } = context;
const { schema, boosts } = context;
let routerConfig = defaultRouterOptions;
@ -27,17 +26,14 @@ export const routerPlugin = defineRendererPlugin({
}
const router = createRouter(routerConfig);
boosts.codeRuntime.getScope().set('router', router);
boosts.temporaryUse('router', router);
const RouterView = createRouterView(router);
boosts.addAppWrapper(RouterView);
boosts.setOutlet(RouteOutlet);
whenLifeCylePhaseChange(LifecyclePhase.AfterInitPackageLoad).then(() => {
return router.isReady();
});
boosts.codeRuntime.getScope().set('router', router);
boosts.temporaryUse('router', router);
await router.isReady();
},
});

View File

@ -1,11 +1,11 @@
import { useMemo } from 'react';
import { useRenderContext } from '../app/context';
import { useRendererContext } from '../api/context';
import { OutletProps } from '../app/boosts';
import { useRouteLocation } from './context';
import { createComponentBySchema } from '../runtime/schema';
export function RouteOutlet(props: OutletProps) {
const context = useRenderContext();
const context = useRendererContext();
const location = useRouteLocation();
const { schema, packageManager } = context;

View File

@ -13,7 +13,7 @@ import {
type Spec,
} from '@alilc/lowcode-shared';
import { type ComponentType, type ReactInstance, useMemo, createElement } from 'react';
import { useRenderContext } from '../app/context';
import { useRendererContext } from '../api/context';
import { useReactiveStore } from './hooks/useReactiveStore';
import { useModel } from './context';
import { getComponentByName } from './schema';
@ -65,10 +65,10 @@ export function WidgetComponent(props: WidgetRendererProps) {
const componentNode = widget.node as NormalizedComponentNode;
const { ref, ...componentProps } = componentNode.props;
const renderContext = useRenderContext();
const rendererContext = useRendererContext();
const Component = useMemo(
() => getComponentByName(componentNode.componentName, renderContext),
() => getComponentByName(componentNode.componentName, rendererContext),
[widget],
);
@ -94,11 +94,16 @@ export function WidgetComponent(props: WidgetRendererProps) {
return null;
}
const finalProps = {
...otherProps,
...state.props,
};
return createElement(
Component,
{
...otherProps,
...state.props,
...finalProps,
id: finalProps.id ? finalProps.id : undefined,
key: widget.key,
ref: attachRef,
},

View File

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

View File

@ -1,9 +1,8 @@
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 { useRendererContext } from '../api/context';
import { reactiveStateFactory } from './reactiveState';
import { dataSourceCreator } from './dataSource';
import { type ReactComponent, type ReactWidget, createElementByWidget } from './components';
import { ModelContextProvider } from './context';
import { appendExternalStyle } from '../utils/element';
@ -39,9 +38,7 @@ export function getComponentByName(
name: string,
{ packageManager, boostsManager }: RenderContext,
): ReactComponent {
const componentsRecord = packageManager.getComponentsNameRecord<ReactComponent>();
// read cache first
const result = lowCodeComponentsCache.get(name) || componentsRecord[name];
const result = lowCodeComponentsCache.get(name) || packageManager.getComponent(name);
if (isLowCodeComponentPackage(result)) {
const { schema, ...metadata } = result;
@ -87,8 +84,8 @@ export function createComponentBySchema(
props: LowCodeComponentProps,
ref: ForwardedRef<any>,
) {
const renderContext = useRenderContext();
const { componentTreeModel } = renderContext;
const renderContext = useRendererContext();
const { options, componentTreeModel } = renderContext;
const modelRef = useRef<IComponentTreeModel<ReactComponent, ReactInstance>>();
@ -110,7 +107,7 @@ export function createComponentBySchema(
model.initialize({
defaultProps: props,
stateCreator: reactiveStateFactory,
dataSourceCreator,
dataSourceCreator: options.dataSourceCreator,
});
model.triggerLifeCycle('constructor');
@ -123,11 +120,6 @@ export function createComponentBySchema(
}
useEffect(() => {
const scopeValue = model.codeScope.value;
// init dataSource
scopeValue.reloadDataSource?.();
// trigger lifeCycles
// componentDidMount?.();
model.triggerLifeCycle('componentDidMount');

View File

@ -1,7 +1,3 @@
export const addLeadingSlash = (path: string): string => {
return path.charAt(0) === '/' ? path : `/${path}`;
};
export interface ExternalElementOptions {
id?: string;
root?: HTMLElement;

View File

@ -14,10 +14,10 @@ describe('LifeCycleService', () => {
lifeCycle.when(LifecyclePhase.Ready).finally(() => {
result += '2';
});
lifeCycle.when(LifecyclePhase.AfterInitPackageLoad).then(() => {
lifeCycle.when(LifecyclePhase.Inited).then(() => {
result += '3';
});
lifeCycle.when(LifecyclePhase.AfterInitPackageLoad).finally(() => {
lifeCycle.when(LifecyclePhase.Inited).finally(() => {
result += '4';
});
@ -27,7 +27,7 @@ describe('LifeCycleService', () => {
expect(result).toEqual('12');
lifeCycle.phase = LifecyclePhase.AfterInitPackageLoad;
lifeCycle.phase = LifecyclePhase.Inited;
await sleep();

View File

@ -30,21 +30,7 @@ export class RendererMain<RenderObject> {
@IComponentTreeModelService private componentTreeModelService: IComponentTreeModelService,
@IBoostsService private boostsService: IBoostsService,
@ILifeCycleService private lifeCycleService: ILifeCycleService,
) {
this.lifeCycleService.when(LifecyclePhase.OptionsResolved).then(async () => {
const renderContext = {
schema: this.schemaService,
packageManager: this.packageManagementService,
boostsManager: this.boostsService,
componentTreeModel: this.componentTreeModelService,
lifeCycle: this.lifeCycleService,
};
this.renderObject = await this.adapter(renderContext);
this.lifeCycleService.phase = LifecyclePhase.Ready;
});
}
) {}
async main(options: AppOptions, adapter: RenderAdapter<RenderObject>) {
const { schema, mode, plugins = [] } = options;
@ -58,20 +44,26 @@ export class RendererMain<RenderObject> {
this.codeRuntimeService.initialize(options.codeRuntime ?? {});
this.lifeCycleService.phase = LifecyclePhase.OptionsResolved;
await this.lifeCycleService.setPhase(LifecyclePhase.OptionsResolved);
await this.lifeCycleService.when(LifecyclePhase.Ready);
const renderContext = {
schema: this.schemaService,
packageManager: this.packageManagementService,
boostsManager: this.boostsService,
componentTreeModel: this.componentTreeModelService,
lifeCycle: this.lifeCycleService,
};
this.renderObject = await this.adapter(renderContext);
await this.extensionHostService.registerPlugin(plugins);
// 先加载插件提供 package loader
await this.packageManagementService.loadPackages(this.initOptions.packages ?? []);
this.lifeCycleService.phase = LifecyclePhase.AfterInitPackageLoad;
await this.lifeCycleService.setPhase(LifecyclePhase.Ready);
}
async getApp(): Promise<RendererApplication<RenderObject>> {
await this.lifeCycleService.when(LifecyclePhase.AfterInitPackageLoad);
getApp(): RendererApplication<RenderObject> {
// construct application
return Object.freeze<RendererApplication<RenderObject>>({
// develop use
@ -85,6 +77,9 @@ export class RendererMain<RenderObject> {
use: (plugin) => {
return this.extensionHostService.registerPlugin(plugin);
},
destroy: async () => {
return this.lifeCycleService.setPhase(LifecyclePhase.Destroying);
},
});
}
}

View File

@ -58,11 +58,7 @@ export class CodeRuntimeService implements ICodeRuntimeService {
if (!code) return undefined;
try {
let result = this.evalCodeFunction(code, scope.value);
if (typeof result === 'function') {
result = result.bind(scope.value);
}
const result = this.evalCodeFunction(code, scope.value);
return result as R;
} catch (err) {
@ -105,7 +101,8 @@ export class CodeRuntimeService implements ICodeRuntimeService {
node: Spec.JSExpression | Spec.JSFunction,
options: ResolveOptions,
) {
const v = this.run(node.value, options.scope || this.codeScope);
const scope = options.scope || this.codeScope;
const v = this.run(node.value, scope) as any;
if (typeof v === 'undefined' && node.mock) {
return this.resolve(node.mock, options);

View File

@ -63,7 +63,7 @@ export class CodeScope implements ICodeScope {
private createProxy(): PlainObject {
return new Proxy(Object.create(null) as PlainObject, {
set: (target, p, newValue) => {
this.set(p as string, newValue, true);
this.set(p as string, newValue);
return true;
},
get: (_, p) => this.findValue(p) ?? undefined,

View File

@ -3,7 +3,7 @@ import { type Plugin, type PluginContext } from './plugin';
import { IBoostsService } from './boosts';
import { IPackageManagementService } from '../package';
import { ISchemaService } from '../schema';
import { ILifeCycleService, LifecyclePhase } from '../lifeCycleService';
import { ILifeCycleService } from '../lifeCycleService';
interface IPluginRuntime extends Plugin {
status: 'setup' | 'ready';
@ -42,8 +42,8 @@ export class ExtensionHostService implements IExtensionHostService {
schema: this.schemaService,
packageManager: this.packageManagementService,
whenLifeCylePhaseChange: (phase) => {
return this.lifeCycleService.when(phase);
whenLifeCylePhaseChange: (phase, listener) => {
return this.lifeCycleService.when(phase, listener);
},
};
}

View File

@ -1,6 +1,6 @@
import { type EventEmitter, type IStore, type PlainObject } from '@alilc/lowcode-shared';
import { type IBoosts } from './boosts';
import { LifecyclePhase } from '../lifeCycleService';
import { ILifeCycleService } from '../lifeCycleService';
import { type ISchemaService } from '../schema';
import { type IPackageManagementService } from '../package';
@ -8,12 +8,12 @@ export interface PluginContext<BoostsExtends = object> {
eventEmitter: EventEmitter;
globalState: IStore<PlainObject, string>;
boosts: IBoosts<BoostsExtends>;
schema: ISchemaService;
schema: Pick<ISchemaService, 'get' | 'set'>;
packageManager: IPackageManagementService;
/**
*
*/
whenLifeCylePhaseChange(phase: LifecyclePhase): Promise<void>;
whenLifeCylePhaseChange: ILifeCycleService['when'];
}
export interface Plugin<BoostsExtends = object> {

View File

@ -1,4 +1,4 @@
import { Provide, createDecorator, Barrier } from '@alilc/lowcode-shared';
import { Provide, createDecorator, EventEmitter, EventDisposable } from '@alilc/lowcode-shared';
export const enum LifecyclePhase {
Starting = 1,
@ -7,7 +7,7 @@ export const enum LifecyclePhase {
Ready = 3,
AfterInitPackageLoad = 4,
Destroying = 4,
}
export interface ILifeCycleService {
@ -16,18 +16,20 @@ export interface ILifeCycleService {
*/
phase: LifecyclePhase;
setPhase(phase: LifecyclePhase): Promise<void>;
/**
* Returns a promise that resolves when a certain lifecycle phase
* has started.
*/
when(phase: LifecyclePhase): Promise<void>;
when(phase: LifecyclePhase, listener: () => void | Promise<void>): EventDisposable;
}
export const ILifeCycleService = createDecorator<ILifeCycleService>('lifeCycleService');
@Provide(ILifeCycleService)
export class LifeCycleService implements ILifeCycleService {
private readonly phaseWhen = new Map<LifecyclePhase, Barrier>();
private readonly phaseWhen = new EventEmitter();
private _phase = LifecyclePhase.Starting;
@ -35,7 +37,7 @@ export class LifeCycleService implements ILifeCycleService {
return this._phase;
}
set phase(value: LifecyclePhase) {
async setPhase(value: LifecyclePhase) {
if (value < this._phase) {
throw new Error('Lifecycle cannot go backwards');
}
@ -44,28 +46,27 @@ export class LifeCycleService implements ILifeCycleService {
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);
}
await this.phaseWhen.emit(LifecyclePhaseToString(value));
}
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();
when(phase: LifecyclePhase, listener: () => void | Promise<void>) {
return this.phaseWhen.on(LifecyclePhaseToString(phase), listener);
}
}
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.Inited:
return 'Inited';
case LifecyclePhase.Destroying:
return 'Destroying';
}
}

View File

@ -15,9 +15,8 @@ export interface NormalizedComponentNode extends Spec.ComponentNode {
export interface InitializeModelOptions {
defaultProps?: PlainObject | undefined;
stateCreator: ModelScopeStateCreator;
dataSourceCreator: ModelScopeDataSourceCreator;
dataSourceCreator?: ModelScopeDataSourceCreator;
}
/**
@ -60,14 +59,6 @@ export interface IComponentTreeModel<Component, ComponentInstance = unknown> {
export type ModelScopeStateCreator = (initalState: PlainObject) => Spec.InstanceStateApi;
export type ModelScopeDataSourceCreator = (...args: any[]) => Spec.InstanceDataSourceApi;
const defaultDataSourceSchema: Spec.ComponentDataSource = {
list: [],
dataHandler: {
type: 'JSFunction',
value: '() => {}',
},
};
export interface ComponentTreeModelOptions {
id?: string;
metadata?: PlainObject;
@ -108,7 +99,7 @@ export class ComponentTreeModel<Component, ComponentInstance = unknown>
state = {},
defaultProps: defaultSchemaProps,
props = {},
dataSource = defaultDataSourceSchema,
dataSource,
methods = {},
} = this.componentsTree;
@ -120,16 +111,22 @@ export class ComponentTreeModel<Component, ComponentInstance = unknown>
},
});
const initalState = this.codeRuntime.resolve(state, { scope: this.codeScope });
const initalProps = this.codeRuntime.resolve(props, { scope: this.codeScope });
this.codeScope.setValue({ props: { ...defaultProps, ...initalProps } });
const initalState = this.codeRuntime.resolve(state, { scope: this.codeScope });
const stateApi = stateCreator(initalState);
const dataSourceApi = dataSourceCreator(dataSource, stateApi);
this.codeScope.setValue(stateApi);
let dataSourceApi: Spec.InstanceDataSourceApi | undefined;
if (dataSource && dataSourceCreator) {
const dataSourceProps = this.codeRuntime.resolve(dataSource, { scope: this.codeScope });
dataSourceApi = dataSourceCreator(dataSourceProps, stateApi);
}
this.codeScope.setValue(
Object.assign(
{
props: { ...defaultProps, ...initalProps },
$: (ref: string) => {
const insArr = this.instanceMap.get(ref);
if (!insArr) return undefined;
@ -139,7 +136,6 @@ export class ComponentTreeModel<Component, ComponentInstance = unknown>
return this.instanceMap.get(ref) ?? [];
},
},
stateApi,
dataSourceApi,
),
);

View File

@ -10,7 +10,7 @@ import {
import { get as lodashGet } from 'lodash-es';
import { PackageLoader } from './loader';
import { ISchemaService } from '../schema';
import { ILifeCycleService, LifecyclePhase } from '../lifeCycleService';
import { ILifeCycleService } from '../lifeCycleService';
export interface NormalizedPackage {
id: string;
@ -33,16 +33,13 @@ export interface IPackageManagementService {
setLibraryByPackageName(packageName: string, library: any): void;
getLibraryByComponentMap(componentMap: Spec.ComponentMap): any;
getLibraryByComponentMap(
componentMap: Spec.ComponentMap,
): { key: string; value: any } | undefined;
/** 解析组件映射 */
resolveComponentMaps(componentMaps: Spec.ComponentMap[]): void;
/** 获取组件映射对象key = componentName value = component */
getComponentsNameRecord<C = unknown>(
componentMaps?: Spec.ComponentMap[],
): Record<string, C | LowCodeComponent>;
/** 通过组件名获取对应的组件 */
getComponent<C = unknown>(componentName: string): C | LowCodeComponent | undefined;
/** 注册组件 */
@ -72,8 +69,7 @@ export class PackageManagementService implements IPackageManagementService {
@ISchemaService private schemaService: ISchemaService,
@ILifeCycleService private lifeCycleService: ILifeCycleService,
) {
this.lifeCycleService.when(LifecyclePhase.AfterInitPackageLoad).then(() => {
const componentsMaps = this.schemaService.get('componentsMap');
this.schemaService.onChange('componentsMap', (componentsMaps) => {
this.resolveComponentMaps(componentsMaps);
});
}
@ -109,19 +105,23 @@ export class PackageManagementService implements IPackageManagementService {
this.packageStore.set(packageName, library);
}
getLibraryByComponentMap(componentMap: Spec.ComponentMap) {
if (this.packageStore.has(componentMap.package!)) {
getLibraryByComponentMap(
componentMap: Spec.ComponentMap,
): { key: string; value: any } | undefined {
if (!componentMap.componentName && !componentMap.exportName) return;
if (this.packageStore.has(componentMap.package)) {
const library = this.packageStore.get(componentMap.package!);
// export { exportName } from xxx exportName === global.libraryName.exportName
// export exportName from xxx exportName === global.libraryName.default || global.libraryName
// export { exportName as componentName } from package
// if exportName == null exportName === componentName;
// const componentName = exportName.subName, if exportName empty subName donot use
const paths =
componentMap.exportName && componentMap.subName ? componentMap.subName.split('.') : [];
// if exportName === nil, exportName === componentName;
const exportName = componentMap.exportName ?? componentMap.componentName;
if (componentMap.destructuring) {
if (exportName && componentMap.destructuring) {
paths.unshift(exportName);
}
@ -130,10 +130,12 @@ export class PackageManagementService implements IPackageManagementService {
result = result[path] || result;
}
return result;
// export { exportName as componentName } from package
return {
key: componentMap.componentName ?? componentMap.exportName!,
value: result,
};
}
return undefined;
}
resolveComponentMaps(componentMaps: Spec.ComponentMap[]) {
@ -146,22 +148,15 @@ export class PackageManagementService implements IPackageManagementService {
}
} else {
const result = this.getLibraryByComponentMap(map);
if (map.componentName && result) this.componentsRecord[map.componentName] = result;
if (result) {
this.componentsRecord[result.key] = result.value;
}
}
}
}
getComponentsNameRecord(componentMaps?: Spec.ComponentMap[]) {
if (componentMaps) {
const newMaps = componentMaps.filter((item) => !this.componentsRecord[item.componentName]);
this.resolveComponentMaps(newMaps);
}
return { ...this.componentsRecord };
}
getComponent(componentName: string) {
return this.componentsRecord[componentName];
return lodashGet(this.componentsRecord, componentName);
}
registerComponentByName(componentName: string, Component: unknown) {

View File

@ -42,7 +42,9 @@ export class RuntimeIntlService implements IRuntimeIntlService {
@ICodeRuntimeService private codeRuntimeService: ICodeRuntimeService,
@ISchemaService private schemaService: ISchemaService,
) {
this.lifeCycleService.when(LifecyclePhase.OptionsResolved).then(() => {
this.injectScope();
this.lifeCycleService.when(LifecyclePhase.OptionsResolved, () => {
const config = this.schemaService.get('config');
const i18nTranslations = this.schemaService.get('i18n');
@ -55,10 +57,6 @@ export class RuntimeIntlService implements IRuntimeIntlService {
});
}
});
this.lifeCycleService.when(LifecyclePhase.Ready).then(() => {
this.toExpose();
});
}
t(descriptor: MessageDescriptor): string {
@ -85,7 +83,7 @@ export class RuntimeIntlService implements IRuntimeIntlService {
this.intl.addTranslations(locale, translations);
}
private toExpose(): void {
private injectScope(): void {
const exposed: Spec.IntlApi = {
i18n: (key, params) => {
return this.t({ key, params });

View File

@ -8,12 +8,11 @@ import {
import { isPlainObject } from 'lodash-es';
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, target: AnyFunction | PlainObject): void;
add(utilItem: Spec.Util, force?: boolean): void;
add(name: string, target: AnyFunction | PlainObject, force?: boolean): void;
remove(name: string): void;
}
@ -27,38 +26,61 @@ export class RuntimeUtilService implements IRuntimeUtilService {
constructor(
@ICodeRuntimeService private codeRuntimeService: ICodeRuntimeService,
@IPackageManagementService private packageManagementService: IPackageManagementService,
@ILifeCycleService private lifeCycleService: ILifeCycleService,
@ISchemaService private schemaService: ISchemaService,
) {
this.lifeCycleService.when(LifecyclePhase.AfterInitPackageLoad).then(() => {
const utils = this.schemaService.get('utils') ?? [];
this.injectScope();
this.schemaService.onChange('utils', (utils = []) => {
for (const util of utils) {
this.add(util);
}
this.toExpose();
});
}
add(utilItem: Spec.Util): void;
add(name: string, fn: AnyFunction | PlainObject): void;
add(name: Spec.Util | string, fn?: AnyFunction | PlainObject): void {
if (typeof name === 'string') {
if (fn) {
if (isPlainObject(fn)) {
if ((fn as PlainObject).destructuring) {
for (const key of Object.keys(fn)) {
this.add(key, (fn as PlainObject)[key]);
}
} else {
this.utilsMap.set(name, fn);
}
} else if (typeof fn === 'function') {
this.utilsMap.set(name, fn);
}
}
add(utilItem: Spec.Util, force?: boolean): void;
add(name: string, fn: AnyFunction | PlainObject, force?: boolean): void;
add(util: Spec.Util | string, fn?: AnyFunction | PlainObject | boolean, force?: boolean): void {
let name: string;
let utilObj: AnyFunction | PlainObject | Spec.Util;
if (typeof util === 'string') {
if (!fn) return;
name = util;
utilObj = fn as AnyFunction | PlainObject;
} else {
const util = this.parseUtil(name);
if (util) this.add(name.name, util);
if (!util) return;
name = util.name;
utilObj = util;
force = fn as boolean;
}
this.addUtilByName(name, utilObj, force);
}
private addUtilByName(
name: string,
fn: AnyFunction | PlainObject | Spec.Util,
force?: boolean,
): void {
if (this.utilsMap.has(name) && !force) return;
if (isPlainObject(fn)) {
if ((fn as Spec.Util).type === 'function' || (fn as Spec.Util).type === 'npm') {
const utilFn = this.parseUtil(fn as Spec.Util);
if (utilFn) {
this.addUtilByName(utilFn.key, utilFn.value, force);
}
} else if ((fn as PlainObject).destructuring) {
for (const key of Object.keys(fn)) {
this.addUtilByName(key, (fn as PlainObject)[key], force);
}
} else {
this.utilsMap.set(name, fn);
}
} else if (typeof fn === 'function') {
this.utilsMap.set(name, fn);
}
}
@ -69,13 +91,16 @@ export class RuntimeUtilService implements IRuntimeUtilService {
private parseUtil(utilItem: Spec.Util) {
if (utilItem.type === 'function') {
const { content } = utilItem;
return this.codeRuntimeService.run(content.value);
return {
key: utilItem.name,
value: this.codeRuntimeService.run(content.value),
};
} else {
return this.packageManagementService.getLibraryByComponentMap(utilItem.content);
}
}
private toExpose(): void {
private injectScope(): void {
const exposed = new Proxy(Object.create(null), {
get: (_, p: string) => {
return this.utilsMap.get(p);

View File

@ -4,6 +4,8 @@ import {
Provide,
type IStore,
KeyValueStore,
EventEmitter,
type EventDisposable,
} from '@alilc/lowcode-shared';
import { isObject } from 'lodash-es';
import { schemaValidation } from './validation';
@ -19,7 +21,12 @@ export interface ISchemaService {
get<K extends NormalizedSchemaKey>(key: K): NormalizedSchema[K];
set<K extends NormalizedSchemaKey>(key: K, value: NormalizedSchema[K]): void;
set<K extends NormalizedSchemaKey>(key: K, value: NormalizedSchema[K]): Promise<void>;
onChange<K extends NormalizedSchemaKey>(
key: K,
listener: (v: NormalizedSchema[K]) => void,
): EventDisposable;
}
export const ISchemaService = createDecorator<ISchemaService>('schemaService');
@ -33,13 +40,18 @@ export class SchemaService implements ISchemaService {
setterValidation: schemaValidation,
});
private notifyEmiiter = new EventEmitter();
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);
this.onChange('constants', (value = {}) => {
this.codeRuntimeService.getScope().set('constants', value);
});
this.lifeCycleService.when(LifecyclePhase.Destroying, () => {
this.notifyEmiiter.removeAll();
});
}
@ -54,11 +66,21 @@ export class SchemaService implements ISchemaService {
});
}
set<K extends NormalizedSchemaKey>(key: K, value: NormalizedSchema[K]): void {
this.store.set(key, value);
async set<K extends NormalizedSchemaKey>(key: K, value: NormalizedSchema[K]): Promise<void> {
if (value !== this.get(key)) {
this.store.set(key, value);
await this.notifyEmiiter.emit(key, value);
}
}
get<K extends NormalizedSchemaKey>(key: K): NormalizedSchema[K] {
return this.store.get(key) as NormalizedSchema[K];
}
onChange<K extends keyof NormalizedSchema>(
key: K,
listener: (v: NormalizedSchema[K]) => void | Promise<void>,
): EventDisposable {
return this.notifyEmiiter.on(key, listener);
}
}

View File

@ -3,21 +3,24 @@ 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';
import { type ModelScopeDataSourceCreator } from './services/model';
export interface AppOptions {
schema: Spec.Project;
packages?: Spec.Package[];
plugins?: Plugin[];
/**
*
*/
mode?: 'development' | 'production';
/**
* code runtime
*/
codeRuntime?: CodeRuntimeInitializeOptions;
/**
*
*/
dataSourceCreator?: ModelScopeDataSourceCreator;
}
export type RendererApplication<Render = unknown> = {
@ -28,4 +31,6 @@ export type RendererApplication<Render = unknown> = {
readonly packageManager: IPackageManagementService;
use(plugin: Plugin): Promise<void>;
destroy(): Promise<void>;
} & Render;

View File

@ -8,7 +8,7 @@ import {
} from './history';
import { createRouterMatcher } from './matcher';
import { type PathParserOptions, type PathParams } from './utils/path-parser';
import { parseURL, stringifyURL } from './utils/url';
import { mergeSearchParams, parseURL, stringifyURL } from './utils/url';
import { isSameRouteLocation } from './utils/helper';
import type {
RouteRecord,
@ -77,11 +77,11 @@ export function createRouter(options: RouterOptions): Router {
function resolve(
rawLocation: RawRouteLocation,
currentLocation?: RouteLocationNormalized,
current?: RouteLocationNormalized,
): RouteLocationNormalized & {
href: string;
} {
currentLocation = Object.assign({}, currentLocation || currentLocation);
currentLocation = Object.assign({}, current || currentLocation);
if (typeof rawLocation === 'string') {
const locationNormalized = parseURL(rawLocation);
@ -89,7 +89,10 @@ export function createRouter(options: RouterOptions): Router {
const href = routerHistory.createHref(locationNormalized.fullPath);
return Object.assign(locationNormalized, matchedRoute, {
searchParams: locationNormalized.searchParams,
searchParams: mergeSearchParams(
currentLocation.searchParams,
locationNormalized.searchParams,
),
hash: decodeURIComponent(locationNormalized.hash),
redirectedFrom: undefined,
href,

View File

@ -45,3 +45,14 @@ export function stringifyURL(location: {
const searchStr = location.searchParams ? location.searchParams.toString() : '';
return location.path + (searchStr && '?') + searchStr + (location.hash || '');
}
export function mergeSearchParams(
a: URLSearchParams | undefined,
b: URLSearchParams | undefined,
): URLSearchParams {
if (!a && !b) return new URLSearchParams();
if (!a) return b!;
if (!b) return a;
return new URLSearchParams([...Array.from(a.entries()), ...Array.from(b.entries())]);
}

View File

@ -44,7 +44,7 @@ export interface Project {
/**
*
*/
meta?: Record<string, JSONObject>;
meta?: Record<string, JSONValue | JSONObject>;
/**
*
* @deprecated
@ -92,13 +92,13 @@ export interface ProjectConfig {
*/
export interface ComponentMap {
/**
* JS
* JS
*/
componentName: string;
componentName?: string;
/**
* npm package name
* npm package name
*/
package?: string;
package: string;
/**
* package version
*/