fix: renderer bugs fix

This commit is contained in:
1ncounter 2024-07-01 19:03:35 +08:00
parent a855c05d67
commit 450d08e500
26 changed files with 472 additions and 376 deletions

View File

@ -1,7 +1,8 @@
import { createRenderer } from '@alilc/lowcode-renderer-core';
import { type Root, createRoot } from 'react-dom/client';
import { type ReactAppOptions, RendererContext } from './context';
import { RendererContext } from './context';
import { ApplicationView, boosts } from '../app';
import { type ReactAppOptions } from './types';
export const createApp = async (options: ReactAppOptions) => {
return createRenderer(async (context) => {

View File

@ -1,17 +1,26 @@
import { createRenderer, type AppOptions } from '@alilc/lowcode-renderer-core';
import { createRenderer } from '@alilc/lowcode-renderer-core';
import { FunctionComponent } from 'react';
import { type LowCodeComponentProps, createComponentBySchema } from '../runtime/schema';
import { RendererContext } from '../api/context';
import {
type LowCodeComponentProps,
createComponent as createSchemaComponent,
} from '../runtime/createComponent';
import { RendererContext } from './context';
import { type ReactAppOptions } from './types';
interface Render {
toComponent(): FunctionComponent<LowCodeComponentProps>;
}
export async function createComponent(options: AppOptions) {
export async function createComponent(options: ReactAppOptions) {
const creator = createRenderer<Render>((context) => {
const { schema } = context;
const componentsTree = schema.get('componentsTree')[0];
const LowCodeComponent = createSchemaComponent(componentsTree, {
displayName: componentsTree.componentName,
...options.component,
});
const LowCodeComponent = createComponentBySchema(schema.get('componentsTree')[0]);
const contextValue = { ...context, options };
function Component(props: LowCodeComponentProps) {

View File

@ -1,9 +1,6 @@
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>;
}
import { createContext, useContext } from 'react';
import { type RenderContext } from '@alilc/lowcode-renderer-core';
import { type ReactAppOptions } from './types';
export const RendererContext = createContext<RenderContext & { options: ReactAppOptions }>(
undefined!,

View File

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

@ -1,10 +1,10 @@
import { useRendererContext } from '../api/context';
import { getComponentByName } from '../runtime/schema';
import { getComponentByName } from '../runtime/createComponent';
import { boosts } from './boosts';
export function ApplicationView() {
const rendererContext = useRendererContext();
const { schema } = rendererContext;
const { schema, options } = 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, rendererContext);
const Layout = getComponentByName(componentName, rendererContext, options.component);
if (Layout) {
const layoutProps: any = layoutConfig.props ?? {};

View File

@ -6,5 +6,11 @@ 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';
export type {
PackageLoader,
CodeScope,
Plugin,
ModelDataSourceCreator,
ModelStateCreator,
} from '@alilc/lowcode-renderer-core';
export type { ReactRendererBoostsApi } from './app/boosts';

View File

@ -2,12 +2,12 @@ import { useMemo } from 'react';
import { useRendererContext } from '../api/context';
import { OutletProps } from '../app/boosts';
import { useRouteLocation } from './context';
import { createComponentBySchema } from '../runtime/schema';
import { createComponent } from '../runtime/createComponent';
export function RouteOutlet(props: OutletProps) {
const context = useRendererContext();
const location = useRouteLocation();
const { schema, packageManager } = context;
const { schema, packageManager, options } = context;
const pageConfig = useMemo(() => {
const pages = schema.get('pages') ?? [];
@ -27,11 +27,12 @@ export function RouteOutlet(props: OutletProps) {
const componentsMap = schema.get('componentsMap');
packageManager.resolveComponentMaps(componentsMap);
const LowCodeComponent = createComponentBySchema(pageConfig.mappingId, {
const LowCodeComponent = createComponent(pageConfig.mappingId, {
displayName: pageConfig.id,
modelOptions: {
metadata: pageConfig,
},
...options.component,
});
return <LowCodeComponent {...props} />;

View File

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

View File

@ -3,23 +3,23 @@ import { forwardRef, useRef, useEffect } from 'react';
import { isValidElementType } from 'react-is';
import { useRendererContext } from '../api/context';
import { reactiveStateFactory } from './reactiveState';
import { type ReactComponent, type ReactWidget, createElementByWidget } from './components';
import { ModelContextProvider } from './context';
import { type ReactComponent, type ReactWidget, createElementByWidget } from './elements';
import { appendExternalStyle } from '../utils/element';
import type {
RenderContext,
IComponentTreeModel,
ComponentTreeModelOptions,
CreateComponentTreeModelOptions,
} from '@alilc/lowcode-renderer-core';
import type { ReactInstance, CSSProperties, ForwardedRef } from 'react';
import type { ReactInstance, CSSProperties, ForwardedRef, ReactNode } from 'react';
export interface ComponentOptions {
displayName?: string;
modelOptions?: ComponentTreeModelOptions;
modelOptions?: Pick<CreateComponentTreeModelOptions, 'id' | 'metadata'>;
widgetCreated?(widget: ReactWidget): void;
componentRefAttached?(widget: ReactWidget, instance: ReactInstance): void;
beforeElementCreate?(widget: ReactWidget): ReactWidget;
elementCreated?(widget: ReactWidget, element: ReactNode): ReactNode;
componentRefAttached?(widget: ReactWidget, instance: ReactInstance | null): void;
}
export interface LowCodeComponentProps {
@ -37,6 +37,7 @@ const lowCodeComponentsCache = new Map<string, ReactComponent>();
export function getComponentByName(
name: string,
{ packageManager, boostsManager }: RenderContext,
componentOptions: ComponentOptions = {},
): ReactComponent {
const result = lowCodeComponentsCache.get(name) || packageManager.getComponent(name);
@ -58,7 +59,8 @@ export function getComponentByName(
});
}
const lowCodeComponent = createComponentBySchema(componentsTree[0], {
const lowCodeComponent = createComponent(componentsTree[0], {
...componentOptions,
displayName: name,
modelOptions: {
id: metadata.id,
@ -76,40 +78,49 @@ export function getComponentByName(
return result;
}
export function createComponentBySchema(
export function createComponent(
schema: string | Spec.ComponentTreeRoot,
{ displayName = '__LowCodeComponent__', modelOptions }: ComponentOptions = {},
componentOptions: ComponentOptions = {},
) {
const { displayName = '__LowCodeComponent__', modelOptions } = componentOptions;
const LowCodeComponent = forwardRef(function (
props: LowCodeComponentProps,
ref: ForwardedRef<any>,
) {
const renderContext = useRendererContext();
const { options, componentTreeModel } = renderContext;
const context = useRendererContext();
const { options: globalOptions, componentTreeModel } = context;
const modelRef = useRef<IComponentTreeModel<ReactComponent, ReactInstance>>();
if (!modelRef.current) {
const finalOptions: CreateComponentTreeModelOptions = {
...modelOptions,
codeScopeValue: {
props,
},
stateCreator: reactiveStateFactory,
dataSourceCreator: globalOptions.dataSourceCreator,
};
if (typeof schema === 'string') {
modelRef.current = componentTreeModel.createById(schema, modelOptions);
modelRef.current = componentTreeModel.createById(schema, finalOptions);
} else {
modelRef.current = componentTreeModel.create(schema, modelOptions);
modelRef.current = componentTreeModel.create(schema, finalOptions);
}
console.log(
'%c [ model ]-103',
'font-size:13px; background:pink; color:#bf2c9f;',
modelRef.current,
);
}
const model = modelRef.current!;
console.log('%c [ model ]-103', 'font-size:13px; background:pink; color:#bf2c9f;', model);
const isConstructed = useRef(false);
const isMounted = useRef(false);
if (!isConstructed.current) {
model.initialize({
defaultProps: props,
stateCreator: reactiveStateFactory,
dataSourceCreator: options.dataSourceCreator,
});
model.triggerLifeCycle('constructor');
isConstructed.current = true;
@ -142,11 +153,9 @@ export function createComponentBySchema(
}, []);
return (
<ModelContextProvider value={model}>
<div id={props.id} className={props.className} style={props.style} ref={ref}>
{model.widgets.map((w) => createElementByWidget(w, model.codeScope))}
{model.widgets.map((w) => createElementByWidget(w, w.model.codeRuntime, componentOptions))}
</div>
</ModelContextProvider>
);
});

View File

@ -1,6 +1,6 @@
import {
type IWidget,
type ICodeScope,
type ICodeRuntime,
type NormalizedComponentNode,
mapValue,
} from '@alilc/lowcode-renderer-core';
@ -15,38 +15,40 @@ import {
import { type ComponentType, type ReactInstance, useMemo, createElement } from 'react';
import { useRendererContext } from '../api/context';
import { useReactiveStore } from './hooks/useReactiveStore';
import { useModel } from './context';
import { getComponentByName } from './schema';
import { getComponentByName, type ComponentOptions } from './createComponent';
export type ReactComponent = ComponentType<any>;
export type ReactWidget = IWidget<ReactComponent, ReactInstance>;
interface WidgetRendererProps {
widget: ReactWidget;
codeScope: ICodeScope;
codeRuntime: ICodeRuntime;
options: ComponentOptions;
[key: string]: any;
}
export function createElementByWidget(
widget: IWidget<ReactComponent, ReactInstance>,
codeScope: ICodeScope,
widget: ReactWidget,
codeRuntime: ICodeRuntime,
options: ComponentOptions,
) {
const { key, node } = widget;
const getElement = (widget: ReactWidget) => {
const { key, rawNode } = widget;
if (typeof node === 'string') {
return node;
if (typeof rawNode === 'string') {
return rawNode;
}
if (isJSExpression(node)) {
return <Text key={key} expr={node} codeScope={codeScope} />;
if (isJSExpression(rawNode)) {
return <Text key={key} expr={rawNode} codeRuntime={codeRuntime} />;
}
if (isJSI18nNode(node)) {
return <I18nText key={key} i18n={node} codeScope={codeScope} />;
if (isJSI18nNode(rawNode)) {
return <I18nText key={key} i18n={rawNode} codeRuntime={codeRuntime} />;
}
const { condition, loop } = widget.node as NormalizedComponentNode;
const { condition, loop } = widget.rawNode as NormalizedComponentNode;
// condition为 Falsy 的情况下 不渲染
if (!condition) return null;
@ -54,31 +56,98 @@ export function createElementByWidget(
if (Array.isArray(loop) && loop.length === 0) return null;
if (isJSExpression(loop)) {
return <LoopWidgetRenderer key={key} loop={loop} widget={widget} codeScope={codeScope} />;
return (
<LoopWidgetRenderer
key={key}
loop={loop}
widget={widget}
codeRuntime={codeRuntime}
options={options}
/>
);
}
return <WidgetComponent key={key} widget={widget} codeScope={codeScope} />;
return (
<WidgetComponent key={key} widget={widget} codeRuntime={codeRuntime} options={options} />
);
};
if (options.beforeElementCreate) {
widget = options.beforeElementCreate(widget);
}
const element = getElement(widget);
if (options.elementCreated) {
return options.elementCreated(widget, element);
}
return element;
}
export function WidgetComponent(props: WidgetRendererProps) {
const { widget, codeScope, ...otherProps } = props;
const componentNode = widget.node as NormalizedComponentNode;
const { widget, codeRuntime, options, ...otherProps } = props;
const componentNode = widget.rawNode as NormalizedComponentNode;
const { ref, ...componentProps } = componentNode.props;
const rendererContext = useRendererContext();
const context = useRendererContext();
const Component = useMemo(
() => getComponentByName(componentNode.componentName, rendererContext),
() => getComponentByName(componentNode.componentName, context, options),
[widget],
);
// 先将 jsslot, jsFunction 对象转换
const processedProps = mapValue(
componentProps,
(node) => isJSFunction(node) || isJSSlot(node),
(node: Spec.JSSlot | Spec.JSFunction) => {
if (isJSSlot(node)) {
const slot = node as Spec.JSSlot;
if (slot.value) {
const widgets = widget.model.buildWidgets(
Array.isArray(node.value) ? node.value : [node.value],
);
if (slot.params?.length) {
return (...args: any[]) => {
const params = slot.params!.reduce((prev, cur, idx) => {
return (prev[cur] = args[idx]);
}, {} as PlainObject);
return widgets.map((n) =>
createElementByWidget(
n,
codeRuntime.createChild({ initScopeValue: params }),
options,
),
);
};
} else {
return widgets.map((n) => createElementByWidget(n, codeRuntime, options));
}
}
} else if (isJSFunction(node)) {
return widget.model.codeRuntime.resolve(node);
}
return null;
},
);
if (process.env.NODE_ENV === 'development') {
// development 模式下 把 widget 的内容作为 prop ,便于排查问题
processedProps.widget = widget;
}
const state = useReactiveStore({
target: {
condition: componentNode.condition,
props: preprocessProps(componentProps, widget, codeScope),
props: processedProps,
},
valueGetter(expr) {
return widget.model.codeRuntime.resolve(expr, { scope: codeScope });
return codeRuntime.resolve(expr);
},
});
@ -88,6 +157,8 @@ export function WidgetComponent(props: WidgetRendererProps) {
} else {
if (ref) widget.model.removeComponentRef(ref);
}
options.componentRefAttached?.(widget, ins);
};
if (!state.condition) {
@ -107,58 +178,15 @@ export function WidgetComponent(props: WidgetRendererProps) {
key: widget.key,
ref: attachRef,
},
widget.children?.map((item) => createElementByWidget(item, codeScope)) ?? [],
widget.children?.map((item) => createElementByWidget(item, codeRuntime, options)) ?? [],
);
}
function preprocessProps(props: PlainObject, widget: ReactWidget, codeScope: ICodeScope) {
// 先将 jsslot, jsFunction 对象转换
const finalProps = mapValue(
props,
(node) => isJSFunction(node) || isJSSlot(node),
(node: Spec.JSSlot | Spec.JSFunction) => {
if (isJSSlot(node)) {
const slot = node as Spec.JSSlot;
if (slot.value) {
const widgets = widget.model.buildWidgets(
Array.isArray(node.value) ? node.value : [node.value],
);
if (slot.params?.length) {
return (...args: any[]) => {
const params = slot.params!.reduce((prev, cur, idx) => {
return (prev[cur] = args[idx]);
}, {} as PlainObject);
return widgets.map((n) => createElementByWidget(n, codeScope.createChild(params)));
};
} else {
return widgets.map((n) => createElementByWidget(n, codeScope));
}
}
} else if (isJSFunction(node)) {
return widget.model.codeRuntime.resolve(node, { scope: codeScope });
}
return null;
},
);
if (process.env.NODE_ENV === 'development') {
// development 模式下 把 widget 的内容作为 prop ,便于排查问题
finalProps.widget = widget;
}
return finalProps;
}
function Text(props: { expr: Spec.JSExpression; codeScope: ICodeScope }) {
const model = useModel();
function Text(props: { expr: Spec.JSExpression; codeRuntime: ICodeRuntime }) {
const text: string = useReactiveStore({
target: props.expr,
getter: (obj) => {
return model.codeRuntime.resolve(obj, { scope: props.codeScope });
return props.codeRuntime.resolve(obj);
},
});
@ -167,12 +195,11 @@ function Text(props: { expr: Spec.JSExpression; codeScope: ICodeScope }) {
Text.displayName = 'Text';
function I18nText(props: { i18n: Spec.JSI18n; codeScope: ICodeScope }) {
const model = useModel();
function I18nText(props: { i18n: Spec.JSI18n; codeRuntime: ICodeRuntime }) {
const text: string = useReactiveStore({
target: props.i18n,
getter: (obj) => {
return model.codeRuntime.resolve(obj, { scope: props.codeScope });
return props.codeRuntime.resolve(obj);
},
});
@ -184,32 +211,34 @@ I18nText.displayName = 'I18nText';
function LoopWidgetRenderer({
loop,
widget,
codeScope,
codeRuntime,
options,
...otherProps
}: {
loop: Spec.JSExpression;
widget: ReactWidget;
codeScope: ICodeScope;
codeRuntime: ICodeRuntime;
options: ComponentOptions;
[key: string]: any;
}) {
const { condition, loopArgs } = widget.node as NormalizedComponentNode;
const { condition, loopArgs } = widget.rawNode as NormalizedComponentNode;
const state = useReactiveStore({
target: {
loop,
condition,
},
valueGetter(expr) {
return widget.model.codeRuntime.resolve(expr, { scope: codeScope });
return codeRuntime.resolve(expr);
},
});
if (state.condition && Array.isArray(state.loop) && state.loop.length > 0) {
return state.loop.map((item: any, idx: number) => {
const childScope = codeScope.createChild({
const childRuntime = codeRuntime.createChild({
initScopeValue: {
[loopArgs[0]]: item,
[loopArgs[1]]: idx,
},
});
return (
@ -217,7 +246,8 @@ function LoopWidgetRenderer({
{...otherProps}
key={`loop-${widget.key}-${idx}`}
widget={widget}
codeScope={childScope}
codeRuntime={childRuntime}
options={options}
/>
);
});

View File

@ -1,2 +1,2 @@
export * from './schema';
export * from './components';
export * from './createComponent';
export * from './elements';

View File

@ -0,0 +1,128 @@
import {
type PlainObject,
type Spec,
type EventDisposable,
isJSExpression,
isJSFunction,
} from '@alilc/lowcode-shared';
import { type ICodeScope, CodeScope } from './codeScope';
import { isNode } from '../../utils/node';
import { mapValue } from '../../utils/value';
import { evaluate } from './evaluate';
export interface CodeRuntimeOptions<T extends PlainObject = PlainObject> {
initScopeValue?: Partial<T>;
parentScope?: ICodeScope;
evalCodeFunction?: EvalCodeFunction;
}
export interface ICodeRuntime<T extends PlainObject = PlainObject> {
getScope(): ICodeScope<T>;
run<R = unknown>(code: string, scope?: ICodeScope): R | undefined;
resolve(value: PlainObject): any;
onResolve(handler: NodeResolverHandler): EventDisposable;
createChild<V extends PlainObject = PlainObject>(
options: Omit<CodeRuntimeOptions<V>, 'parentScope'>,
): ICodeRuntime<V>;
}
export type NodeResolverHandler = (node: Spec.JSNode) => Spec.JSNode | false | undefined;
let onResolveHandlers: NodeResolverHandler[] = [];
export type EvalCodeFunction = (code: string, scope: any) => any;
export class CodeRuntime<T extends PlainObject = PlainObject> implements ICodeRuntime<T> {
private codeScope: ICodeScope<T>;
private evalCodeFunction: EvalCodeFunction = evaluate;
constructor(options: CodeRuntimeOptions<T> = {}) {
if (options.evalCodeFunction) this.evalCodeFunction = options.evalCodeFunction;
if (options.parentScope) {
this.codeScope = options.parentScope.createChild<T>(options.initScopeValue ?? {});
} else {
this.codeScope = new CodeScope(options.initScopeValue ?? {});
}
}
getScope() {
return this.codeScope;
}
run<R = unknown>(code: string): R | undefined {
if (!code) return undefined;
try {
const result = this.evalCodeFunction(code, this.codeScope.value);
return result as R;
} catch (err) {
// todo replace logger
console.error('eval error', code, this.codeScope.value, err);
return undefined;
}
}
resolve(data: PlainObject): any {
if (onResolveHandlers.length > 0) {
data = mapValue(data, isNode, (node: Spec.JSNode) => {
let newNode: Spec.JSNode | false | undefined = node;
for (const handler of onResolveHandlers) {
newNode = handler(newNode as Spec.JSNode);
if (newNode === false || typeof newNode === 'undefined') {
break;
}
}
return newNode;
});
}
return mapValue(
data,
(data) => {
return isJSExpression(data) || isJSFunction(data);
},
(node: Spec.JSExpression | Spec.JSFunction) => {
return this.resolveExprOrFunction(node);
},
);
}
private resolveExprOrFunction(node: Spec.JSExpression | Spec.JSFunction) {
const v = this.run(node.value) as any;
if (typeof v === 'undefined' && node.mock) {
return this.resolve(node.mock);
}
return v;
}
/**
* handler
*/
onResolve(handler: NodeResolverHandler): EventDisposable {
onResolveHandlers.push(handler);
return () => {
onResolveHandlers = onResolveHandlers.filter((h) => h !== handler);
};
}
createChild<V extends PlainObject = PlainObject>(
options?: Omit<CodeRuntimeOptions<V>, 'parentScope'>,
): ICodeRuntime<V> {
return new CodeRuntime({
initScopeValue: options?.initScopeValue,
parentScope: this.codeScope,
evalCodeFunction: options?.evalCodeFunction ?? this.evalCodeFunction,
});
}
}

View File

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

View File

@ -9,27 +9,28 @@ const unscopables = trustedGlobals.reduce((acc, key) => ({ ...acc, [key]: true }
__proto__: null,
});
export interface ICodeScope {
readonly value: PlainObject;
set(name: string, value: any): void;
setValue(value: PlainObject, replace?: boolean): void;
createChild(initValue: PlainObject): ICodeScope;
export interface ICodeScope<T extends PlainObject = PlainObject> {
readonly value: T;
set(name: keyof T, value: any): void;
setValue(value: Partial<T>, replace?: boolean): void;
createChild<V extends PlainObject = PlainObject>(initValue: Partial<V>): ICodeScope<V>;
}
/**
*
*/
interface IScopeNode {
parent?: IScopeNode;
current: PlainObject;
interface IScopeNode<T extends PlainObject> {
parent?: IScopeNode<PlainObject>;
current: Partial<T>;
}
export class CodeScope implements ICodeScope {
__node: IScopeNode;
export class CodeScope<T extends PlainObject = PlainObject> implements ICodeScope<T> {
__node: IScopeNode<T>;
private proxyValue: PlainObject;
private proxyValue: T;
constructor(initValue: PlainObject) {
constructor(initValue: Partial<T>) {
this.__node = {
current: initValue,
};
@ -37,15 +38,15 @@ export class CodeScope implements ICodeScope {
this.proxyValue = this.createProxy();
}
get value() {
get value(): T {
return this.proxyValue;
}
set(name: string, value: any): void {
set(name: keyof T, value: any): void {
this.__node.current[name] = value;
}
setValue(value: PlainObject, replace = false) {
setValue(value: Partial<T>, replace = false) {
if (replace) {
this.__node.current = { ...value };
} else {
@ -53,15 +54,15 @@ export class CodeScope implements ICodeScope {
}
}
createChild(initValue: PlainObject): ICodeScope {
createChild<V extends PlainObject = PlainObject>(initValue: Partial<V>): ICodeScope<V> {
const childScope = new CodeScope(initValue);
childScope.__node.parent = this.__node;
return childScope;
}
private createProxy(): PlainObject {
return new Proxy(Object.create(null) as PlainObject, {
private createProxy(): T {
return new Proxy(Object.create(null) as T, {
set: (target, p, newValue) => {
this.set(p as string, newValue);
return true;
@ -74,7 +75,7 @@ export class CodeScope implements ICodeScope {
private findValue(prop: PropertyKey) {
if (prop === Symbol.unscopables) return unscopables;
let node: IScopeNode | undefined = this.__node;
let node: IScopeNode<PlainObject> | undefined = this.__node;
while (node) {
if (Object.hasOwnProperty.call(node.current, prop)) {
return node.current[prop as string];
@ -86,7 +87,7 @@ export class CodeScope implements ICodeScope {
private hasProperty(prop: PropertyKey): boolean {
if (prop in unscopables) return true;
let node: IScopeNode | undefined = this.__node;
let node: IScopeNode<PlainObject> | undefined = this.__node;
while (node) {
if (prop in node.current) {
return true;

View File

@ -0,0 +1,7 @@
import { type EvalCodeFunction } from './codeRuntime';
export const evaluate: EvalCodeFunction = (code: string, scope: any) => {
return new Function('scope', `"use strict";return (function(){return (${code})}).bind(scope)();`)(
scope,
);
};

View File

@ -1,2 +1,3 @@
export * from './codeScope';
export * from './codeRuntimeService';
export * from './codeRuntime';

View File

@ -1,13 +1,13 @@
import { createDecorator, Provide, type PlainObject } from '@alilc/lowcode-shared';
import { isObject } from 'lodash-es';
import { ICodeRuntimeService } from '../code-runtime';
import { ICodeRuntime, ICodeRuntimeService } from '../code-runtime';
import { IRuntimeUtilService } from '../runtimeUtilService';
import { IRuntimeIntlService } from '../runtimeIntlService';
export type IBoosts<Extends> = IBoostsApi & Extends & { [key: string]: any };
export interface IBoostsApi {
readonly codeRuntime: ICodeRuntimeService;
readonly codeRuntime: ICodeRuntime;
readonly intl: Pick<IRuntimeIntlService, 't' | 'setLocale' | 'getLocale' | 'addTranslations'>;
@ -39,12 +39,14 @@ export class BoostsService implements IBoostsService {
private _expose: any;
constructor(
@ICodeRuntimeService private codeRuntimeService: ICodeRuntimeService,
@ICodeRuntimeService codeRuntimeService: ICodeRuntimeService,
@IRuntimeIntlService private runtimeIntlService: IRuntimeIntlService,
@IRuntimeUtilService private runtimeUtilService: IRuntimeUtilService,
) {
this.builtInApis = {
codeRuntime: this.codeRuntimeService,
get codeRuntime() {
return codeRuntimeService.rootRuntime;
},
intl: this.runtimeIntlService,
util: this.runtimeUtilService,
temporaryUse: (name, value) => {
@ -75,7 +77,7 @@ export class BoostsService implements IBoostsService {
toExpose<Extends>(): IBoosts<Extends> {
if (!this._expose) {
this._expose = new Proxy(Object.create(null), {
this._expose = new Proxy(this.builtInApis, {
get: (_, p, receiver) => {
return (
Reflect.get(this.builtInApis, p, receiver) ||

View File

@ -25,6 +25,19 @@ export interface ILifeCycleService {
when(phase: LifecyclePhase, listener: () => void | Promise<void>): EventDisposable;
}
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');
@Provide(ILifeCycleService)
@ -55,18 +68,3 @@ export class LifeCycleService implements ILifeCycleService {
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

@ -5,7 +5,7 @@ import {
invariant,
uniqueId,
} from '@alilc/lowcode-shared';
import { type ICodeScope, type ICodeRuntimeService } from '../code-runtime';
import { type ICodeRuntime } from '../code-runtime';
import { IWidget, Widget } from '../widget';
export interface NormalizedComponentNode extends Spec.ComponentNode {
@ -13,26 +13,16 @@ export interface NormalizedComponentNode extends Spec.ComponentNode {
props: Spec.ComponentNodeProps;
}
export interface InitializeModelOptions {
defaultProps?: PlainObject | undefined;
stateCreator: ModelScopeStateCreator;
dataSourceCreator?: ModelScopeDataSourceCreator;
}
/**
*
*/
export interface IComponentTreeModel<Component, ComponentInstance = unknown> {
readonly id: string;
readonly codeScope: ICodeScope;
readonly codeRuntime: ICodeRuntimeService;
readonly codeRuntime: ICodeRuntime;
readonly widgets: IWidget<Component, ComponentInstance>[];
initialize(options: InitializeModelOptions): void;
/**
* css
*/
@ -56,12 +46,18 @@ export interface IComponentTreeModel<Component, ComponentInstance = unknown> {
buildWidgets(nodes: Spec.NodeType[]): IWidget<Component, ComponentInstance>[];
}
export type ModelScopeStateCreator = (initalState: PlainObject) => Spec.InstanceStateApi;
export type ModelScopeDataSourceCreator = (...args: any[]) => Spec.InstanceDataSourceApi;
export type ModelStateCreator = (initalState: PlainObject) => Spec.InstanceStateApi;
export type ModelDataSourceCreator = (
dataSourceSchema: Spec.ComponentDataSource,
codeRuntime: ICodeRuntime<Spec.InstanceApi>,
) => Spec.InstanceDataSourceApi;
export interface ComponentTreeModelOptions {
id?: string;
metadata?: PlainObject;
stateCreator: ModelStateCreator;
dataSourceCreator?: ModelDataSourceCreator;
}
export class ComponentTreeModel<Component, ComponentInstance = unknown>
@ -71,16 +67,14 @@ export class ComponentTreeModel<Component, ComponentInstance = unknown>
public id: string;
public codeScope: ICodeScope;
public widgets: IWidget<Component>[] = [];
public metadata: PlainObject = {};
constructor(
public componentsTree: Spec.ComponentTree,
public codeRuntime: ICodeRuntimeService,
options?: ComponentTreeModelOptions,
public codeRuntime: ICodeRuntime<Spec.InstanceApi>,
options: ComponentTreeModelOptions,
) {
invariant(componentsTree, 'componentsTree must to provide', 'ComponentTreeModel');
@ -92,39 +86,28 @@ export class ComponentTreeModel<Component, ComponentInstance = unknown>
if (componentsTree.children) {
this.widgets = this.buildWidgets(componentsTree.children);
}
this.initialize(options);
}
initialize({ defaultProps, stateCreator, dataSourceCreator }: InitializeModelOptions) {
const {
state = {},
defaultProps: defaultSchemaProps,
props = {},
dataSource,
methods = {},
} = this.componentsTree;
private initialize({ stateCreator, dataSourceCreator }: ComponentTreeModelOptions) {
const { state = {}, defaultProps, props = {}, dataSource, methods = {} } = this.componentsTree;
const codeScope = this.codeRuntime.getScope();
this.codeScope = this.codeRuntime.createChildScope({
props: {
...props,
...defaultSchemaProps,
...defaultProps,
},
});
const initalProps = this.codeRuntime.resolve(props);
codeScope.setValue({ props: { ...defaultProps, ...codeScope.value.props, ...initalProps } });
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 initalState = this.codeRuntime.resolve(state);
const stateApi = stateCreator(initalState);
this.codeScope.setValue(stateApi);
codeScope.setValue(stateApi);
let dataSourceApi: Spec.InstanceDataSourceApi | undefined;
if (dataSource && dataSourceCreator) {
const dataSourceProps = this.codeRuntime.resolve(dataSource, { scope: this.codeScope });
dataSourceApi = dataSourceCreator(dataSourceProps, stateApi);
const dataSourceProps = this.codeRuntime.resolve(dataSource);
dataSourceApi = dataSourceCreator(dataSourceProps, this.codeRuntime);
}
this.codeScope.setValue(
codeScope.setValue(
Object.assign(
{
$: (ref: string) => {
@ -141,9 +124,9 @@ export class ComponentTreeModel<Component, ComponentInstance = unknown>
);
for (const [key, fn] of Object.entries(methods)) {
const customMethod = this.codeRuntime.resolve(fn, { scope: this.codeScope });
const customMethod = this.codeRuntime.resolve(fn);
if (typeof customMethod === 'function') {
this.codeScope.set(key, customMethod);
codeScope.set(key, customMethod);
}
}
}
@ -163,9 +146,9 @@ export class ComponentTreeModel<Component, ComponentInstance = unknown>
const lifeCycleSchema = this.componentsTree.lifeCycles[lifeCycleName];
const lifeCycleFn = this.codeRuntime.resolve(lifeCycleSchema, { scope: this.codeScope });
const lifeCycleFn = this.codeRuntime.resolve(lifeCycleSchema);
if (typeof lifeCycleFn === 'function') {
lifeCycleFn.apply(this.codeScope.value, args);
lifeCycleFn.apply(this.codeRuntime.getScope().value, args);
}
}

View File

@ -1,4 +1,10 @@
import { createDecorator, Provide, invariant, type Spec } from '@alilc/lowcode-shared';
import {
createDecorator,
Provide,
invariant,
type Spec,
type PlainObject,
} from '@alilc/lowcode-shared';
import { ICodeRuntimeService } from '../code-runtime';
import {
type IComponentTreeModel,
@ -7,15 +13,19 @@ import {
} from './componentTreeModel';
import { ISchemaService } from '../schema';
export interface CreateComponentTreeModelOptions extends ComponentTreeModelOptions {
codeScopeValue?: PlainObject;
}
export interface IComponentTreeModelService {
create<Component>(
componentsTree: Spec.ComponentTree,
options?: ComponentTreeModelOptions,
options?: CreateComponentTreeModelOptions,
): IComponentTreeModel<Component>;
createById<Component>(
id: string,
options?: ComponentTreeModelOptions,
options?: CreateComponentTreeModelOptions,
): IComponentTreeModel<Component>;
}
@ -32,20 +42,32 @@ export class ComponentTreeModelService implements IComponentTreeModelService {
create<Component>(
componentsTree: Spec.ComponentTree,
options?: ComponentTreeModelOptions,
options: CreateComponentTreeModelOptions,
): IComponentTreeModel<Component> {
return new ComponentTreeModel(componentsTree, this.codeRuntimeService, options);
return new ComponentTreeModel(
componentsTree,
this.codeRuntimeService.createCodeRuntime({
initScopeValue: options?.codeScopeValue,
}),
options,
);
}
createById<Component>(
id: string,
options?: ComponentTreeModelOptions,
options: CreateComponentTreeModelOptions,
): IComponentTreeModel<Component> {
const componentsTrees = this.schemaService.get('componentsTree');
const componentsTree = componentsTrees.find((item) => item.id === id);
invariant(componentsTree, 'componentsTree not found');
return new ComponentTreeModel(componentsTree, this.codeRuntimeService, options);
return new ComponentTreeModel(
componentsTree,
this.codeRuntimeService.createCodeRuntime({
initScopeValue: options?.codeScopeValue,
}),
options,
);
}
}

View File

@ -42,8 +42,6 @@ export class RuntimeIntlService implements IRuntimeIntlService {
@ICodeRuntimeService private codeRuntimeService: ICodeRuntimeService,
@ISchemaService private schemaService: ISchemaService,
) {
this.injectScope();
this.lifeCycleService.when(LifecyclePhase.OptionsResolved, () => {
const config = this.schemaService.get('config');
const i18nTranslations = this.schemaService.get('i18n');
@ -56,6 +54,8 @@ export class RuntimeIntlService implements IRuntimeIntlService {
this.addTranslations(key, i18nTranslations[key]);
});
}
this.injectScope();
});
}
@ -96,6 +96,6 @@ export class RuntimeIntlService implements IRuntimeIntlService {
},
};
this.codeRuntimeService.getScope().setValue(exposed);
this.codeRuntimeService.rootRuntime.getScope().setValue(exposed);
}
}

View File

@ -9,6 +9,7 @@ import { isPlainObject } from 'lodash-es';
import { IPackageManagementService } from './package';
import { ICodeRuntimeService } from './code-runtime';
import { ISchemaService } from './schema';
import { ILifeCycleService, LifecyclePhase } from './lifeCycleService';
export interface IRuntimeUtilService {
add(utilItem: Spec.Util, force?: boolean): void;
@ -27,8 +28,11 @@ export class RuntimeUtilService implements IRuntimeUtilService {
@ICodeRuntimeService private codeRuntimeService: ICodeRuntimeService,
@IPackageManagementService private packageManagementService: IPackageManagementService,
@ISchemaService private schemaService: ISchemaService,
@ILifeCycleService private lifeCycleService: ILifeCycleService,
) {
this.lifeCycleService.when(LifecyclePhase.OptionsResolved, () => {
this.injectScope();
});
this.schemaService.onChange('utils', (utils = []) => {
for (const util of utils) {
@ -93,7 +97,7 @@ export class RuntimeUtilService implements IRuntimeUtilService {
const { content } = utilItem;
return {
key: utilItem.name,
value: this.codeRuntimeService.run(content.value),
value: this.codeRuntimeService.rootRuntime.run(content.value),
};
} else {
return this.packageManagementService.getLibraryByComponentMap(utilItem.content);
@ -113,6 +117,6 @@ export class RuntimeUtilService implements IRuntimeUtilService {
},
});
this.codeRuntimeService.getScope().set('utils', exposed);
this.codeRuntimeService.rootRuntime.getScope().set('utils', exposed);
}
}

View File

@ -1,11 +1,10 @@
import { type Spec, uniqueId } from '@alilc/lowcode-shared';
import { clone } from 'lodash-es';
import { IComponentTreeModel } from '../model';
export interface IWidget<Component, ComponentInstance = unknown> {
readonly key: string;
readonly node: Spec.NodeType;
readonly rawNode: Spec.NodeType;
model: IComponentTreeModel<Component, ComponentInstance>;
@ -15,9 +14,7 @@ export interface IWidget<Component, ComponentInstance = unknown> {
export class Widget<Component, ComponentInstance = unknown>
implements IWidget<Component, ComponentInstance>
{
public __raw: Spec.NodeType;
public node: Spec.NodeType;
public rawNode: Spec.NodeType;
public key: string;
@ -27,8 +24,7 @@ export class Widget<Component, ComponentInstance = unknown>
node: Spec.NodeType,
public model: IComponentTreeModel<Component, ComponentInstance>,
) {
this.node = clone(node);
this.__raw = node;
this.rawNode = node;
this.key = (node as Spec.ComponentNode)?.id ?? uniqueId();
}
}

View File

@ -2,8 +2,8 @@ import { type Spec } from '@alilc/lowcode-shared';
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';
import { type CodeRuntimeOptions } from './services/code-runtime';
import { type ModelDataSourceCreator } from './services/model';
export interface AppOptions {
schema: Spec.Project;
@ -16,11 +16,11 @@ export interface AppOptions {
/**
* code runtime
*/
codeRuntime?: CodeRuntimeInitializeOptions;
codeRuntime?: CodeRuntimeOptions;
/**
*
*/
dataSourceCreator?: ModelScopeDataSourceCreator;
dataSourceCreator?: ModelDataSourceCreator;
}
export type RendererApplication<Render = unknown> = {

View File

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

View File

@ -1,5 +1,4 @@
import { AnyFunction, PlainObject } from '../index';
import { JSExpression } from './lowcode-spec';
/**
* JS this
@ -62,7 +61,7 @@ export interface DataSourceMapItem<T = any> {
*
* @param params ComponentDataSourceItemOptions params
*/
load(params: any): Promise<T>;
load(params?: any): Promise<T>;
/**
*
*/