feat: add renderer-core codes

This commit is contained in:
1ncounter 2024-03-19 10:47:13 +08:00
parent 3ba0926438
commit fb5de6441d
66 changed files with 2001 additions and 918 deletions

File diff suppressed because it is too large Load Diff

View File

@ -57,6 +57,7 @@
"lerna": "^4.0.0", "lerna": "^4.0.0",
"typescript": "^5.4.2", "typescript": "^5.4.2",
"yarn": "^1.22.17", "yarn": "^1.22.17",
"prettier": "^3.2.5",
"rimraf": "^3.0.2", "rimraf": "^3.0.2",
"rollup": "^4.13.0", "rollup": "^4.13.0",
"vite": "^5.1.6", "vite": "^5.1.6",

View File

@ -1,6 +1,6 @@
{ {
"name": "@alilc/lowcode-types", "name": "@alilc/lowcode-types",
"version": "1.3.2", "version": "1.3.3",
"description": "Types for Ali lowCode engine", "description": "Types for Ali lowCode engine",
"files": [ "files": [
"es", "es",

View File

@ -1,16 +1,19 @@
import { IPublicEnumContextMenuType } from '../enum'; import { IPublicEnumContextMenuType } from '../enum';
import { IPublicModelNode } from '../model'; import { IPublicModelNode } from '../model';
import { IPublicTypeI18nData } from './i8n-data'; import { IPublicTypeI18nData } from './i18n-data';
import { IPublicTypeHelpTipConfig } from './widget-base-config'; import { IPublicTypeHelpTipConfig } from './widget-base-config';
export interface IPublicTypeContextMenuItem extends Omit<IPublicTypeContextMenuAction, 'condition' | 'disabled' | 'items'> { export interface IPublicTypeContextMenuItem
extends Omit<
IPublicTypeContextMenuAction,
'condition' | 'disabled' | 'items'
> {
disabled?: boolean; disabled?: boolean;
items?: Omit<IPublicTypeContextMenuItem, 'items'>[]; items?: Omit<IPublicTypeContextMenuItem, 'items'>[];
} }
export interface IPublicTypeContextMenuAction { export interface IPublicTypeContextMenuAction {
/** /**
* *
* Unique identifier for the action * Unique identifier for the action
@ -41,7 +44,11 @@ export interface IPublicTypeContextMenuAction {
* *
* Sub-menu items or function to generate child node, optional * Sub-menu items or function to generate child node, optional
*/ */
items?: Omit<IPublicTypeContextMenuAction, 'items'>[] | ((nodes?: IPublicModelNode[]) => Omit<IPublicTypeContextMenuAction, 'items'>[]); items?:
| Omit<IPublicTypeContextMenuAction, 'items'>[]
| ((
nodes?: IPublicModelNode[],
) => Omit<IPublicTypeContextMenuAction, 'items'>[]);
/** /**
* *
@ -60,4 +67,3 @@ export interface IPublicTypeContextMenuAction {
*/ */
help?: IPublicTypeHelpTipConfig; help?: IPublicTypeHelpTipConfig;
} }

View File

@ -24,7 +24,7 @@ export * from './widget-base-config';
export * from './node-data'; export * from './node-data';
export * from './icon-type'; export * from './icon-type';
export * from './transformed-component-metadata'; export * from './transformed-component-metadata';
export * from './i8n-data'; export * from './i18n-data';
export * from './npm-info'; export * from './npm-info';
export * from './drag-node-data-object'; export * from './drag-node-data-object';
export * from './drag-node-object'; export * from './drag-node-object';
@ -93,4 +93,4 @@ export * from './scrollable';
export * from './simulator-renderer'; export * from './simulator-renderer';
export * from './config-transducer'; export * from './config-transducer';
export * from './context-menu'; export * from './context-menu';
export * from './command'; export * from './command';

View File

@ -12,5 +12,7 @@ export interface IPublicTypeLowCodeComponent {
} }
export type IPublicTypeProCodeComponent = IPublicTypeNpmInfo; export type IPublicTypeProCodeComponent = IPublicTypeNpmInfo;
export type IPublicTypeComponentMap = IPublicTypeProCodeComponent | IPublicTypeLowCodeComponent; export type IPublicTypeComponentMap =
| IPublicTypeProCodeComponent
| IPublicTypeLowCodeComponent;
export type IPublicTypeComponentsMap = IPublicTypeComponentMap[]; export type IPublicTypeComponentsMap = IPublicTypeComponentMap[];

7
rollup.config.mjs Normal file
View File

@ -0,0 +1,7 @@
export default {
input: 'src/main.js',
output: {
file: 'bundle.js',
format: 'cjs'
}
};

View File

@ -1,11 +0,0 @@
{
"name": "@alilc/react-renderer",
"version": "2.0.0-beta.0",
"description": "",
"type": "module",
"bugs": "https://github.com/alibaba/lowcode-engine/issues",
"homepage": "https://github.com/alibaba/lowcode-engine/#readme",
"dependencies": {
"@vue/reactivity": "^3.4.21"
}
}

View File

@ -1,65 +0,0 @@
import {
type App,
type RenderBase,
createAppFunction,
type AppOptionsBase,
} from '@alilc/runtime-core';
import { type DataSourceCreator } from '@alilc/runtime-shared';
import { type ComponentType } from 'react';
import { type Root, createRoot } from 'react-dom/client';
import { createRenderer } from '../renderer';
import AppComponent from '../components/app';
import { intlPlugin } from '../plugins/intl';
import { globalUtilsPlugin } from '../plugins/utils';
import { initRouter } from '../router';
export interface AppOptions extends AppOptionsBase {
dataSourceCreator: DataSourceCreator;
faultComponent?: ComponentType<any>;
}
export interface ReactRender extends RenderBase {}
export type ReactApp = App<ReactRender>;
export const createApp = createAppFunction<AppOptions, ReactRender>(
async (context, options) => {
const renderer = createRenderer();
const appContext = { ...context, renderer };
initRouter(appContext);
options.plugins ??= [];
options.plugins!.unshift(globalUtilsPlugin, intlPlugin);
// set config
if (options.faultComponent) {
context.config.set('faultComponent', options.faultComponent);
}
context.config.set('dataSourceCreator', options.dataSourceCreator);
let root: Root | undefined;
const reactRender: ReactRender = {
async mount(el) {
if (root) {
return;
}
root = createRoot(el);
root.render(<AppComponent context={appContext} />);
},
unmount() {
if (root) {
root.unmount();
root = undefined;
}
},
};
return {
renderBase: reactRender,
renderer,
};
}
);

View File

@ -1,3 +0,0 @@
{
"extends": "../../tsconfig.json"
}

View File

@ -5,6 +5,12 @@
"type": "module", "type": "module",
"bugs": "https://github.com/alibaba/lowcode-engine/issues", "bugs": "https://github.com/alibaba/lowcode-engine/issues",
"homepage": "https://github.com/alibaba/lowcode-engine/#readme", "homepage": "https://github.com/alibaba/lowcode-engine/#readme",
"license": "MIT",
"scripts": {
"build": "",
"test": "vitest --run",
"test:watch": "vitest"
},
"dependencies": { "dependencies": {
"@alilc/lowcode-types": "1.3.2", "@alilc/lowcode-types": "1.3.2",
"lodash-es": "^4.17.21" "lodash-es": "^4.17.21"

View File

@ -1,26 +1,18 @@
import { import type { Project, Package, PlainObject } from '../types';
type ProjectSchema, import { type PackageManager, createPackageManager } from '../package';
type Package, import { createPluginManager, type Plugin } from '../plugin';
type AnyObject, import { createScope, type CodeScope } from '../code-runtime';
} from '@alilc/runtime-shared'; import { appBoosts, type AppBoosts, type AppBoostsManager } from '../boosts';
import { type PackageManager, createPackageManager } from '../core/package'; import { type AppSchema, createAppSchema } from '../schema';
import { createPluginManager, type Plugin } from '../core/plugin';
import { createScope, type CodeScope } from '../core/codeRuntime';
import {
appBoosts,
type AppBoosts,
type AppBoostsManager,
} from '../core/boosts';
import { type AppSchema, createAppSchema } from '../core/schema';
export interface AppOptionsBase { export interface AppOptionsBase {
schema: ProjectSchema; schema: Project;
packages?: Package[]; packages?: Package[];
plugins?: Plugin[]; plugins?: Plugin[];
appScopeValue?: AnyObject; appScopeValue?: PlainObject;
} }
export interface RenderBase { export interface AppBase {
mount: (el: HTMLElement) => void | Promise<void>; mount: (el: HTMLElement) => void | Promise<void>;
unmount: () => void | Promise<void>; unmount: () => void | Promise<void>;
} }
@ -36,13 +28,13 @@ export interface AppContext {
boosts: AppBoostsManager; boosts: AppBoostsManager;
} }
type AppCreator<O, T> = ( type AppCreator<O, T extends AppBase> = (
appContext: Omit<AppContext, 'renderer'>, appContext: Omit<AppContext, 'renderer'>,
appOptions: O appOptions: O,
) => Promise<{ renderBase: T; renderer?: any }>; ) => Promise<{ appBase: T; renderer?: any }>;
export type App<T extends RenderBase = RenderBase> = { export type App<T extends AppBase = AppBase> = {
schema: ProjectSchema; schema: Project;
config: Map<string, any>; config: Map<string, any>;
readonly boosts: AppBoosts; readonly boosts: AppBoosts;
@ -55,11 +47,14 @@ export type App<T extends RenderBase = RenderBase> = {
* @param options * @param options
* @returns * @returns
*/ */
export function createAppFunction< export function createAppFunction<O extends AppOptionsBase, T extends AppBase = AppBase>(
O extends AppOptionsBase, appCreator: AppCreator<O, T>,
T extends RenderBase = RenderBase ): (options: O) => Promise<App<T>> {
>(appCreator: AppCreator<O, T>): (options: O) => Promise<App<T>> { if (typeof appCreator !== 'function') {
return async options => { throw Error('The first parameter must be a function.');
}
return async (options) => {
const { schema, appScopeValue = {} } = options; const { schema, appScopeValue = {} } = options;
const appSchema = createAppSchema(schema); const appSchema = createAppSchema(schema);
const appConfig = new Map<string, any>(); const appConfig = new Map<string, any>();
@ -77,14 +72,19 @@ export function createAppFunction<
boosts: appBoosts, boosts: appBoosts,
}; };
const { renderBase, renderer } = await appCreator(appContext, options); const { appBase, renderer } = await appCreator(appContext, options);
if (!('mount' in appBase) || !('unmount' in appBase)) {
throw Error('appBase 必须返回 mount 和 unmount 方法');
}
const pluginManager = createPluginManager({ const pluginManager = createPluginManager({
...appContext, ...appContext,
renderer, renderer,
}); });
if (options.plugins?.length) { if (options.plugins?.length) {
await Promise.all(options.plugins.map(p => pluginManager.add(p))); await Promise.all(options.plugins.map((p) => pluginManager.add(p)));
} }
if (options.packages?.length) { if (options.packages?.length) {
@ -100,7 +100,7 @@ export function createAppFunction<
return appBoosts.value; return appBoosts.value;
}, },
}, },
renderBase appBase,
); );
}; };
} }

View File

@ -1,104 +1,27 @@
import { isJsFunction } from '@alilc/runtime-shared'; import { CreateContainerOptions, createContainer } from '../container';
import { import { createCodeRuntime, createScope } from '../code-runtime';
type CodeRuntime, import { throwRuntimeError } from '../utils/error';
createCodeRuntime, import { validateContainerSchema } from '../validator/schema';
type CodeScope,
createScope,
} from '../core/codeRuntime';
import { throwRuntimeError } from '../core/error';
import { type ComponentTreeNode, createNode } from '../helper/treeNode';
import { validateContainerSchema } from '../helper/validator';
import type {
RootSchema,
DataSourceEngine,
DataSourceCreator,
AnyObject,
Package,
} from '@alilc/runtime-shared';
export interface StateContext {
/** 组件状态 */
readonly state: AnyObject;
/** 状态设置方法 */
setState: (newState: AnyObject) => void;
}
interface ContainerInstanceScope<C = any>
extends StateContext,
DataSourceEngine {
readonly props: AnyObject | undefined;
$(ref: string): C | undefined;
[key: string]: any;
}
type LifeCycleName =
| 'constructor'
| 'render'
| 'componentDidMount'
| 'componentDidUpdate'
| 'componentWillUnmount'
| 'componentDidCatch';
export interface ContainerInstance<C = any> {
readonly id?: string;
readonly cssText: string | undefined;
readonly codeScope: CodeScope;
/** 调用生命周期方法 */
triggerLifeCycle(lifeCycleName: LifeCycleName, ...args: any[]): void;
/**
* ref , scope.$() 使
*/
setRefInstance(ref: string, instance: C): void;
removeRefInstance(ref: string, instance?: C): void;
/** 获取子节点内容 渲染使用 */
getComponentTreeNodes(): ComponentTreeNode[];
destory(): void;
}
export interface Container {
readonly codeScope: CodeScope;
readonly codeRuntime: CodeRuntime;
createInstance(
componentsTree: RootSchema,
extraProps?: AnyObject
): ContainerInstance;
}
export interface ComponentOptionsBase<C> { export interface ComponentOptionsBase<C> {
componentsTree: RootSchema; componentsTree: RootSchema;
componentsRecord: Record<string, C | Package>; componentsRecord: Record<string, C | Package>;
supCodeScope?: CodeScope;
initScopeValue?: AnyObject;
dataSourceCreator: DataSourceCreator; dataSourceCreator: DataSourceCreator;
} }
export function createComponentFunction< export function createComponentFunction<C, O extends ComponentOptionsBase<C>>(options: {
C,
O extends ComponentOptionsBase<C>
>(options: {
stateCreator: (initState: AnyObject) => StateContext; stateCreator: (initState: AnyObject) => StateContext;
componentCreator: (container: Container, componentOptions: O) => C; componentCreator: (container: Container, componentOptions: O) => C;
defaultOptions?: Partial<O>; defaultOptions?: Partial<O>;
}): (componentOptions: O) => C { }): (componentOptions: O) => C {
const { stateCreator, componentCreator, defaultOptions = {} } = options; const { stateCreator, componentCreator, defaultOptions = {} } = options;
return componentOptions => { return (componentOptions) => {
const finalOptions = Object.assign({}, defaultOptions, componentOptions); const finalOptions = Object.assign({}, defaultOptions, componentOptions);
const { const { supCodeScope, initScopeValue = {}, dataSourceCreator } = finalOptions;
supCodeScope,
initScopeValue = {},
dataSourceCreator,
} = finalOptions;
const codeRuntimeScope = const codeRuntimeScope =
supCodeScope?.createSubScope(initScopeValue) ?? supCodeScope?.createSubScope(initScopeValue) ?? createScope(initScopeValue);
createScope(initScopeValue);
const codeRuntime = createCodeRuntime(codeRuntimeScope); const codeRuntime = createCodeRuntime(codeRuntimeScope);
const container: Container = { const container: Container = {
@ -116,9 +39,7 @@ export function createComponentFunction<
const mapRefToComponentInstance: Map<string, C> = new Map(); const mapRefToComponentInstance: Map<string, C> = new Map();
const initialState = codeRuntime.parseExprOrFn( const initialState = codeRuntime.parseExprOrFn(componentsTree.state ?? {});
componentsTree.state ?? {}
);
const stateContext = stateCreator(initialState); const stateContext = stateCreator(initialState);
codeRuntimeScope.setValue( codeRuntimeScope.setValue(
@ -135,13 +56,10 @@ export function createComponentFunction<
}, },
stateContext, stateContext,
dataSourceCreator dataSourceCreator
? dataSourceCreator( ? dataSourceCreator(componentsTree.dataSource ?? ({ list: [] } as any), stateContext)
componentsTree.dataSource ?? ({ list: [] } as any), : {},
stateContext
)
: {}
) as ContainerInstanceScope<C>, ) as ContainerInstanceScope<C>,
true true,
); );
if (componentsTree.methods) { if (componentsTree.methods) {
@ -155,10 +73,7 @@ export function createComponentFunction<
triggerLifeCycle('constructor'); triggerLifeCycle('constructor');
function triggerLifeCycle( function triggerLifeCycle(lifeCycleName: LifeCycleNameT, ...args: any[]) {
lifeCycleName: LifeCycleName,
...args: any[]
) {
// keys 用来判断 lifeCycleName 存在于 schema 对象上,不获取原型链上的对象 // keys 用来判断 lifeCycleName 存在于 schema 对象上,不获取原型链上的对象
if ( if (
!componentsTree.lifeCycles || !componentsTree.lifeCycles ||
@ -169,9 +84,7 @@ export function createComponentFunction<
const lifeCycleSchema = componentsTree.lifeCycles[lifeCycleName]; const lifeCycleSchema = componentsTree.lifeCycles[lifeCycleName];
if (isJsFunction(lifeCycleSchema)) { if (isJsFunction(lifeCycleSchema)) {
const lifeCycleFn = codeRuntime.createFnBoundScope( const lifeCycleFn = codeRuntime.createFnBoundScope(lifeCycleSchema.value);
lifeCycleSchema.value
);
if (lifeCycleFn) { if (lifeCycleFn) {
lifeCycleFn.apply(codeRuntime.getScope().value, args); lifeCycleFn.apply(codeRuntime.getScope().value, args);
} }
@ -202,8 +115,8 @@ export function createComponentFunction<
? componentsTree.children ? componentsTree.children
: [componentsTree.children] : [componentsTree.children]
: []; : [];
const treeNodes = childNodes.map(item => { const treeNodes = childNodes.map((item) => {
return createNode(item, undefined); return createComponentTreeNode(item, undefined);
}); });
return treeNodes; return treeNodes;

View File

@ -1,8 +1,11 @@
import { type AnyFunction } from '@alilc/runtime-shared'; import { type AnyFunction } from './types';
import { createHooks, type Hooks } from '../helper/hook'; import { createHookStore, type HookStore } from './utils/hook';
import { type RuntimeError } from './error'; import { nonSetterProxy } from './utils/non-setter-proxy';
import { type RuntimeError } from './utils/error';
export interface AppBoosts {} export interface AppBoosts {
[key: string]: any;
}
export interface RuntimeHooks { export interface RuntimeHooks {
'app:error': (error: RuntimeError) => void; 'app:error': (error: RuntimeError) => void;
@ -11,22 +14,29 @@ export interface RuntimeHooks {
} }
export interface AppBoostsManager { export interface AppBoostsManager {
hooks: Hooks<RuntimeHooks>; hookStore: HookStore<RuntimeHooks>;
readonly value: AppBoosts; readonly value: AppBoosts;
add(name: PropertyKey, value: any, force?: boolean): void; add(name: PropertyKey, value: any, force?: boolean): void;
remove(name: PropertyKey): void;
} }
const boostsValue: AppBoosts = {}; const boostsValue: AppBoosts = {};
const proxyBoostsValue = nonSetterProxy(boostsValue);
export const appBoosts: AppBoostsManager = { export const appBoosts: AppBoostsManager = {
hooks: createHooks(), hookStore: createHookStore(),
get value() { get value() {
return boostsValue; return proxyBoostsValue;
}, },
add(name: PropertyKey, value: any, force = false) { add(name: PropertyKey, value: any, force = false) {
if ((boostsValue as any)[name] && !force) return; if ((boostsValue as any)[name] && !force) return;
(boostsValue as any)[name] = value; (boostsValue as any)[name] = value;
}, },
remove(name) {
if ((boostsValue as any)[name]) {
delete (boostsValue as any)[name];
}
},
}; };

View File

@ -1,24 +1,22 @@
import { import type { AnyFunction, PlainObject, JSExpression, JSFunction } from './types';
type AnyFunction, import { isJSExpression, isJSFunction } from './utils/type-guard';
type AnyObject, import { processValue } from './utils/value';
JSExpression,
JSFunction,
isJsExpression,
isJsFunction,
} from '@alilc/runtime-shared';
import { processValue } from '../utils/value';
export interface CodeRuntime { export interface CodeRuntime {
run<T = unknown>(code: string): T | undefined; run<T = unknown>(code: string): T | undefined;
createFnBoundScope(code: string): AnyFunction | undefined; createFnBoundScope(code: string): AnyFunction | undefined;
parseExprOrFn(value: AnyObject): any; parseExprOrFn(value: PlainObject): any;
bindingScope(scope: CodeScope): void; bindingScope(scope: CodeScope): void;
getScope(): CodeScope; getScope(): CodeScope;
} }
export function createCodeRuntime(scope?: CodeScope): CodeRuntime { const SYMBOL_SIGN = '__code__scope';
let runtimeScope = scope ?? createScope({});
export function createCodeRuntime(scopeOrValue: PlainObject = {}): CodeRuntime {
let runtimeScope = scopeOrValue[Symbol.for(SYMBOL_SIGN)]
? (scopeOrValue as CodeScope)
: createScope(scopeOrValue);
function run<T = unknown>(code: string): T | undefined { function run<T = unknown>(code: string): T | undefined {
if (!code) return undefined; if (!code) return undefined;
@ -26,16 +24,11 @@ export function createCodeRuntime(scope?: CodeScope): CodeRuntime {
try { try {
return new Function( return new Function(
'scope', 'scope',
`"use strict";return (function(){return (${code})}).bind(scope)();` `"use strict";return (function(){return (${code})}).bind(scope)();`,
)(runtimeScope.value) as T; )(runtimeScope.value) as T;
} catch (err) { } catch (err) {
console.log( // todo
'%c eval error', console.error('%c eval error', code, runtimeScope.value, err);
'font-size:13px; background:pink; color:#bf2c9f;',
code,
scope.value,
err
);
return undefined; return undefined;
} }
} }
@ -46,11 +39,11 @@ export function createCodeRuntime(scope?: CodeScope): CodeRuntime {
return fn.bind(runtimeScope.value); return fn.bind(runtimeScope.value);
} }
function parseExprOrFn(value: AnyObject) { function parseExprOrFn(value: PlainObject) {
return processValue( return processValue(
value, value,
data => { (data) => {
return isJsExpression(data) || isJsFunction(data); return isJSExpression(data) || isJSFunction(data);
}, },
(node: JSExpression | JSFunction) => { (node: JSExpression | JSFunction) => {
let v; let v;
@ -65,7 +58,7 @@ export function createCodeRuntime(scope?: CodeScope): CodeRuntime {
return (node as any).mock; return (node as any).mock;
} }
return v; return v;
} },
); );
} }
@ -84,14 +77,14 @@ export function createCodeRuntime(scope?: CodeScope): CodeRuntime {
} }
export interface CodeScope { export interface CodeScope {
readonly value: AnyObject; readonly value: PlainObject;
inject(name: string, value: any, force?: boolean): void; inject(name: string, value: any, force?: boolean): void;
setValue(value: AnyObject, replace?: boolean): void; setValue(value: PlainObject, replace?: boolean): void;
createSubScope(initValue: AnyObject): CodeScope; createSubScope(initValue?: PlainObject): CodeScope;
} }
export function createScope(initValue: AnyObject): CodeScope { export function createScope(initValue: PlainObject = {}): CodeScope {
const innerScope = { value: initValue }; const innerScope = { value: initValue };
const proxyValue = new Proxy(Object.create(null), { const proxyValue = new Proxy(Object.create(null), {
set(target, p, newValue, receiver) { set(target, p, newValue, receiver) {
@ -120,7 +113,7 @@ export function createScope(initValue: AnyObject): CodeScope {
innerScope.value[name] = value; innerScope.value[name] = value;
} }
function createSubScope(initValue: AnyObject) { function createSubScope(initValue: PlainObject = {}) {
const childScope = createScope(initValue); const childScope = createScope(initValue);
(childScope as any).__raw.__parent = innerScope; (childScope as any).__raw.__parent = innerScope;
@ -144,6 +137,8 @@ export function createScope(initValue: AnyObject): CodeScope {
createSubScope, createSubScope,
}; };
Object.defineProperty(scope, Symbol.for(SYMBOL_SIGN), { get: () => true });
// development env
Object.defineProperty(scope, '__raw', { get: () => innerScope }); Object.defineProperty(scope, '__raw', { get: () => innerScope });
return scope; return scope;

View File

@ -0,0 +1,166 @@
import type {
InstanceApi,
PlainObject,
ComponentTree,
InstanceDataSourceApi,
InstanceStateApi,
} from './types';
import { type CodeScope, type CodeRuntime, createCodeRuntime, createScope } from './code-runtime';
import { isJSFunction } from './utils/type-guard';
import { type TextWidget, type ComponentWidget, createWidget } from './widget';
/**
*
*/
export interface Container<InstanceT = unknown, LifeCycleNameT extends string = string> {
readonly codeRuntime: CodeRuntime;
readonly instanceApiObject: InstanceApi<InstanceT>;
/**
* css
*/
getCssText(): string | undefined;
/**
*
*/
triggerLifeCycle(lifeCycleName: LifeCycleNameT, ...args: any[]): void;
/**
* ref , scope.$() 使
*/
setInstance(ref: string, instance: InstanceT): void;
/**
* ref
*/
removeInstance(ref: string, instance?: InstanceT): void;
createWidgets<Element>(): (TextWidget<Element> | ComponentWidget<Element>)[];
}
export interface CreateContainerOptions<LifeCycleNameT extends string> {
supCodeScope?: CodeScope;
initScopeValue?: PlainObject;
componentsTree: ComponentTree<LifeCycleNameT>;
stateCreator: (initalState: PlainObject) => InstanceStateApi;
// type todo
dataSourceCreator: (...args: any[]) => InstanceDataSourceApi;
}
export function createContainer<InstanceT, LifeCycleNameT extends string>(
options: CreateContainerOptions<LifeCycleNameT>,
): Container<InstanceT, LifeCycleNameT> {
const { componentsTree, supCodeScope, initScopeValue, stateCreator, dataSourceCreator } = options;
validContainerSchema(componentsTree);
const instancesMap = new Map<string, InstanceT[]>();
const subScope = supCodeScope
? supCodeScope.createSubScope(initScopeValue)
: createScope(initScopeValue);
const codeRuntime = createCodeRuntime(subScope);
const initalState = codeRuntime.parseExprOrFn(componentsTree.state ?? {});
const initalProps = codeRuntime.parseExprOrFn(componentsTree.props ?? {});
const stateApi = stateCreator(initalState);
const dataSourceApi = dataSourceCreator(componentsTree.dataSource, stateApi);
const instanceApiObject: InstanceApi<InstanceT> = Object.assign(
{
props: initalProps,
$(ref: string) {
const insArr = instancesMap.get(ref);
if (!insArr) return undefined;
return insArr[0];
},
$$(ref: string) {
return instancesMap.get(ref) ?? [];
},
},
stateApi,
dataSourceApi,
);
if (componentsTree.methods) {
for (const [key, fn] of Object.entries(componentsTree.methods)) {
const customMethod = codeRuntime.createFnBoundScope(fn.value);
if (customMethod) {
instanceApiObject[key] = customMethod;
}
}
}
const containerCodeScope = subScope.createSubScope(instanceApiObject);
codeRuntime.bindingScope(containerCodeScope);
function setInstanceByRef(ref: string, ins: InstanceT) {
let insArr = instancesMap.get(ref);
if (!insArr) {
insArr = [];
instancesMap.set(ref, insArr);
}
insArr!.push(ins);
}
function removeInstanceByRef(ref: string, ins?: InstanceT) {
const insArr = instancesMap.get(ref);
if (insArr) {
if (ins) {
const idx = insArr.indexOf(ins);
if (idx > 0) insArr.splice(idx, 1);
} else {
instancesMap.delete(ref);
}
}
}
function triggerLifeCycle(lifeCycleName: LifeCycleNameT, ...args: any[]) {
// keys 用来判断 lifeCycleName 存在于 schema 对象上,不获取原型链上的对象
if (
!componentsTree.lifeCycles ||
!Object.keys(componentsTree.lifeCycles).includes(lifeCycleName)
) {
return;
}
const lifeCycleSchema = componentsTree.lifeCycles[lifeCycleName];
if (isJSFunction(lifeCycleSchema)) {
const lifeCycleFn = codeRuntime.createFnBoundScope(lifeCycleSchema.value);
if (lifeCycleFn) {
lifeCycleFn.apply(containerCodeScope.value, args);
}
}
}
return {
get codeRuntime() {
return codeRuntime;
},
get instanceApiObject() {
return containerCodeScope.value as InstanceApi<InstanceT>;
},
getCssText() {
return componentsTree.css;
},
triggerLifeCycle,
setInstance: setInstanceByRef,
removeInstance: removeInstanceByRef,
createWidgets<Element>() {
if (!componentsTree.children) return [];
return componentsTree.children.map((item) => createWidget<Element>(item));
},
};
}
const CONTAINTER_NAME = ['Page', 'Block', 'Component'];
function validContainerSchema(schema: ComponentTree) {
if (!CONTAINTER_NAME.includes(schema.componentName)) {
throw Error('container schema not valid');
}
}

View File

@ -0,0 +1,15 @@
/* --------------- api -------------------- */
export * from './api/app';
export * from './api/component';
export { createCodeRuntime, createScope } from './code-runtime';
export { definePlugin } from './plugin';
export { createWidget } from './widget';
export { createContainer } from './container';
/* --------------- types ---------------- */
export * from './types';
export type { CodeRuntime, CodeScope } from './code-runtime';
export type { Plugin, PluginSetupContext } from './plugin';
export type { PackageManager, PackageLoader } from './package';
export type { Container } from './container';
export type { Widget, TextWidget, ComponentWidget } from './widget';

View File

@ -1,13 +1,6 @@
import { import { type Package, type ComponentMap, type LowCodeComponent } from './types';
type Package,
type ProCodeComponent,
type ComponentMap,
type LowCodeComponent,
isLowCodeComponentPackage,
} from '@alilc/runtime-shared';
const packageStore: Map<string, any> = ((window as any).__PACKAGE_STORE__ ??= const packageStore: Map<string, any> = ((window as any).__PACKAGE_STORE__ ??= new Map());
new Map());
export interface PackageLoader { export interface PackageLoader {
name?: string; name?: string;
@ -31,9 +24,7 @@ export interface PackageManager {
/** 解析组件映射 */ /** 解析组件映射 */
resolveComponentMaps(componentMaps: ComponentMap[]): void; resolveComponentMaps(componentMaps: ComponentMap[]): void;
/** 获取组件映射对象key = componentName value = component */ /** 获取组件映射对象key = componentName value = component */
getComponentsNameRecord<C = unknown>( getComponentsNameRecord<C = unknown>(componentMaps?: ComponentMap[]): Record<string, C>;
componentMaps?: ComponentMap[]
): Record<string, C>;
/** 通过组件名获取对应的组件 */ /** 通过组件名获取对应的组件 */
getComponent<C = unknown>(componentName: string): C | undefined; getComponent<C = unknown>(componentName: string): C | undefined;
/** 注册组件 */ /** 注册组件 */
@ -48,8 +39,10 @@ export function createPackageManager(): PackageManager {
async function addPackages(packages: Package[]) { async function addPackages(packages: Package[]) {
for (const item of packages) { for (const item of packages) {
const newId = item.package ?? item.id; if (!item.package && !item.id) continue;
const isExist = packagesRef.some(_ => {
const newId = item.package ?? item.id!;
const isExist = packagesRef.some((_) => {
const itemId = _.package ?? _.id; const itemId = _.package ?? _.id;
return itemId === newId; return itemId === newId;
}); });
@ -58,7 +51,7 @@ export function createPackageManager(): PackageManager {
packagesRef.push(item); packagesRef.push(item);
if (!packageStore.has(newId)) { if (!packageStore.has(newId)) {
const loader = packageLoaders.find(loader => loader.active(item)); const loader = packageLoaders.find((loader) => loader.active(item));
if (!loader) continue; if (!loader) continue;
try { try {
@ -73,14 +66,14 @@ export function createPackageManager(): PackageManager {
} }
function getPackageInfo(packageName: string) { function getPackageInfo(packageName: string) {
return packagesRef.find(p => p.package === packageName); return packagesRef.find((p) => p.package === packageName);
} }
function getLibraryByPackageName(packageName: string) { function getLibraryByPackageName(packageName: string) {
const packageInfo = getPackageInfo(packageName); const packageInfo = getPackageInfo(packageName);
if (packageInfo) { if (packageInfo) {
return packageStore.get(packageInfo.package ?? packageInfo.id); return packageStore.get(packageInfo.package ?? packageInfo.id!);
} }
} }
@ -90,31 +83,26 @@ export function createPackageManager(): PackageManager {
function resolveComponentMaps(componentMaps: ComponentMap[]) { function resolveComponentMaps(componentMaps: ComponentMap[]) {
for (const map of componentMaps) { for (const map of componentMaps) {
if ((map as LowCodeComponent).devMode === 'lowCode') { if (map.devMode === 'lowCode') {
const packageInfo = packagesRef.find(_ => { const packageInfo = packagesRef.find((_) => {
return _.id === (map as LowCodeComponent).id; return _.id === (map as LowCodeComponent).id;
}); });
if (isLowCodeComponentPackage(packageInfo)) { if (packageInfo) {
componentsRecord[map.componentName] = packageInfo; componentsRecord[map.componentName] = packageInfo;
} }
} else { } else {
const npmInfo = map as ProCodeComponent; if (packageStore.has(map.package!)) {
const library = packageStore.get(map.package!);
if (packageStore.has(npmInfo.package)) {
const library = packageStore.get(npmInfo.package);
// export { exportName } from xxx exportName === global.libraryName.exportName // export { exportName } from xxx exportName === global.libraryName.exportName
// export exportName from xxx exportName === global.libraryName.default || global.libraryName // export exportName from xxx exportName === global.libraryName.default || global.libraryName
// export { exportName as componentName } from package // export { exportName as componentName } from package
// if exportName == null exportName === componentName; // if exportName == null exportName === componentName;
// const componentName = exportName.subName, if exportName empty subName donot use // const componentName = exportName.subName, if exportName empty subName donot use
const paths = const paths = map.exportName && map.subName ? map.subName.split('.') : [];
npmInfo.exportName && npmInfo.subName const exportName = map.exportName ?? map.componentName;
? npmInfo.subName.split('.')
: [];
const exportName = npmInfo.exportName ?? npmInfo.componentName;
if (npmInfo.destructuring) { if (map.destructuring) {
paths.unshift(exportName); paths.unshift(exportName);
} }
@ -123,7 +111,7 @@ export function createPackageManager(): PackageManager {
result = result[path] || result; result = result[path] || result;
} }
const recordName = npmInfo.componentName ?? npmInfo.exportName; const recordName = map.componentName ?? map.exportName;
componentsRecord[recordName] = result; componentsRecord[recordName] = result;
} }
} }
@ -152,7 +140,7 @@ export function createPackageManager(): PackageManager {
getLibraryByPackageName, getLibraryByPackageName,
setLibraryByPackageName, setLibraryByPackageName,
addPackageLoader(loader) { addPackageLoader(loader) {
if (!loader.name || !packageLoaders.some(_ => _.name === loader.name)) { if (!loader.name || !packageLoaders.some((_) => _.name === loader.name)) {
packageLoaders.push(loader); packageLoaders.push(loader);
} }
}, },

View File

@ -1,4 +1,5 @@
import { type AppContext } from '../api/create-app-function'; import { type AppContext } from './api/app';
import { nonSetterProxy } from './utils/non-setter-proxy';
export interface Plugin<C extends PluginSetupContext = PluginSetupContext> { export interface Plugin<C extends PluginSetupContext = PluginSetupContext> {
name: string; // 插件的 name 作为唯一标识,并不可重复。 name: string; // 插件的 name 作为唯一标识,并不可重复。
@ -14,24 +15,12 @@ export function createPluginManager(context: PluginSetupContext) {
const installedPlugins: Plugin[] = []; const installedPlugins: Plugin[] = [];
let readyToInstallPlugins: Plugin[] = []; let readyToInstallPlugins: Plugin[] = [];
const setupContext = new Proxy(context, { const setupContext = nonSetterProxy(context);
get(target, p, receiver) {
return Reflect.get(target, p, receiver);
},
set() {
return false;
},
has(target, p) {
return Reflect.has(target, p);
},
});
async function install(plugin: Plugin) { async function install(plugin: Plugin) {
if (installedPlugins.some(p => p.name === plugin.name)) return; if (installedPlugins.some((p) => p.name === plugin.name)) return;
if ( if (plugin.dependsOn?.some((dep) => !installedPlugins.some((p) => p.name === dep))) {
plugin.dependsOn?.some(dep => !installedPlugins.some(p => p.name === dep))
) {
readyToInstallPlugins.push(plugin); readyToInstallPlugins.push(plugin);
return; return;
} }
@ -41,24 +30,22 @@ export function createPluginManager(context: PluginSetupContext) {
// 遍历未安装的插件 寻找 dependsOn 的插件已安装完的插件进行安装 // 遍历未安装的插件 寻找 dependsOn 的插件已安装完的插件进行安装
for (const item of readyToInstallPlugins) { for (const item of readyToInstallPlugins) {
if ( if (item.dependsOn?.every((dep) => installedPlugins.some((p) => p.name === dep))) {
item.dependsOn?.every(dep => installedPlugins.some(p => p.name === dep))
) {
await item.setup(setupContext); await item.setup(setupContext);
installedPlugins.push(item); installedPlugins.push(item);
} }
} }
if (readyToInstallPlugins.length) { if (readyToInstallPlugins.length) {
readyToInstallPlugins = readyToInstallPlugins.filter(item => readyToInstallPlugins = readyToInstallPlugins.filter((item) =>
installedPlugins.some(p => p.name === item.name) installedPlugins.some((p) => p.name === item.name),
); );
} }
} }
return { return {
async add(plugin: Plugin) { async add(plugin: Plugin) {
if (installedPlugins.find(item => item.name === plugin.name)) { if (installedPlugins.find((item) => item.name === plugin.name)) {
console.warn('该插件已安装'); console.warn('该插件已安装');
return; return;
} }
@ -68,8 +55,6 @@ export function createPluginManager(context: PluginSetupContext) {
}; };
} }
export function definePlugin<C extends PluginSetupContext, P = Plugin<C>>( export function definePlugin<C extends PluginSetupContext, P = Plugin<C>>(plugin: P) {
plugin: P
) {
return plugin; return plugin;
} }

View File

@ -1,43 +1,33 @@
import type { import type { Project, ComponentTree, ComponentMap, PageConfig } from './types';
ProjectSchema, import { throwRuntimeError } from './utils/error';
RootSchema,
ComponentMap,
PageSchema,
} from '@alilc/runtime-shared';
import { throwRuntimeError } from './error';
import { set, get } from 'lodash-es'; import { set, get } from 'lodash-es';
type AppSchemaType = ProjectSchema<RootSchema>;
export interface AppSchema { export interface AppSchema {
getComponentsTrees(): RootSchema[]; getComponentsTrees(): ComponentTree[];
addComponentsTree(tree: RootSchema): void; addComponentsTree(tree: ComponentTree): void;
removeComponentsTree(id: string): void; removeComponentsTree(id: string): void;
getComponentsMaps(): ComponentMap[]; getComponentsMaps(): ComponentMap[];
addComponentsMap(componentName: ComponentMap): void; addComponentsMap(componentName: ComponentMap): void;
removeComponentsMap(componentName: string): void; removeComponentsMap(componentName: string): void;
getPages(): PageSchema[]; getPages(): PageConfig[];
addPage(page: PageSchema): void; addPage(page: PageConfig): void;
removePage(id: string): void; removePage(id: string): void;
getByKey<K extends keyof AppSchemaType>(key: K): AppSchemaType[K] | undefined; getByKey<K extends keyof Project>(key: K): Project[K] | undefined;
updateByKey<K extends keyof AppSchemaType>( updateByKey<K extends keyof Project>(
key: K, key: K,
updater: AppSchemaType[K] | ((value: AppSchemaType[K]) => AppSchemaType[K]) updater: Project[K] | ((value: Project[K]) => Project[K]),
): void; ): void;
getByPath(path: string | string[]): any; getByPath(path: string | string[]): any;
updateByPath( updateByPath(path: string | string[], updater: any | ((value: any) => any)): void;
path: string | string[],
updater: any | ((value: any) => any)
): void;
find(predicate: (schema: AppSchemaType) => any): any; find(predicate: (schema: Project) => any): any;
} }
export function createAppSchema(schema: ProjectSchema): AppSchema { export function createAppSchema(schema: Project): AppSchema {
if (!schema.version.startsWith('1.')) { if (!schema.version.startsWith('1.')) {
throwRuntimeError('core', 'schema version must be 1.x.x'); throwRuntimeError('core', 'schema version must be 1.x.x');
} }
@ -93,21 +83,13 @@ export function createAppSchema(schema: ProjectSchema): AppSchema {
return get(schemaRef, path); return get(schemaRef, path);
}, },
updateByPath(path, updater) { updateByPath(path, updater) {
set( set(schemaRef, path, typeof updater === 'function' ? updater(this.getByPath(path)) : updater);
schemaRef,
path,
typeof updater === 'function' ? updater(this.getByPath(path)) : updater
);
}, },
}; };
} }
function addArrayItem<T extends Record<string, any>>( function addArrayItem<T extends Record<string, any>>(target: T[], item: T, comparison: string) {
target: T[], const idx = target.findIndex((_) => _[comparison] === item[comparison]);
item: T,
comparison: string
) {
const idx = target.findIndex(_ => _[comparison] === item[comparison]);
if (idx > -1) { if (idx > -1) {
target.splice(idx, 1, item); target.splice(idx, 1, item);
} else { } else {
@ -118,8 +100,8 @@ function addArrayItem<T extends Record<string, any>>(
function removeArrayItem<T extends Record<string, any>>( function removeArrayItem<T extends Record<string, any>>(
target: T[], target: T[],
comparison: string, comparison: string,
comparisonValue: any comparisonValue: any,
) { ) {
const idx = target.findIndex(item => item[comparison] === comparisonValue); const idx = target.findIndex((item) => item[comparison] === comparisonValue);
if (idx > -1) target.splice(idx, 1); if (idx > -1) target.splice(idx, 1);
} }

View File

@ -0,0 +1,3 @@
export type AnyFunction = (...args: any[]) => any;
export type PlainObject = Record<PropertyKey, any>;

View File

@ -0,0 +1,5 @@
export * from './common';
export * from './material';
export * from './specs/asset-spec';
export * from './specs/lowcode-spec';
export * from './specs/runtime-api';

View File

@ -0,0 +1,14 @@
import { Package } from './specs/asset-spec';
import { Project, ComponentMap } from './specs/lowcode-spec';
export interface ProCodeComponent extends Package {
package: string;
type: 'proCode';
}
export interface LowCodeComponent extends Package {
id: string;
type: 'lowCode';
componentName: string;
schema: Project;
}

View File

@ -0,0 +1,104 @@
/**
* https://lowcode-engine.cn/site/docs/specs/assets-spec
* for runtime
*/
import { Project } from './lowcode-spec';
export interface Package {
/**
* package package
*/
id?: string;
/**
* npm id
*/
package?: string;
/**
*
*/
version: string;
/**
* proCode
*/
type?: 'proCode' | 'lowCode';
/**
* CDN url js css
*/
urls?: string[];
/**
* CDN url js css urls
*/
advancedUrls?: ComplexUrls;
/**
* CDN url js css
*/
editUrls?: string[];
/**
* CDN url js css editUrls
*/
advancedEditUrls?: ComplexUrls;
/**
* schema
*/
schema?: Project;
/**
* id
*/
deps?: string[];
/**
*
*/
loadEnv?: LoadEnv[];
/**
* external
*/
external?: boolean;
/**
* webpack output.library
*/
library: string;
/**
* window[exportName] Object
*/
exportName?: string;
/**
* package window.library
*/
async?: boolean;
/**
* package package
*/
exportMode?: string;
/**
* package package
*/
exportSourceId?: string;
/**
* package window
*/
exportSourceLibrary?: string;
}
/**
* urls
*/
export type ComplexUrls = string[] | MultiModeUrls;
/**
*
*/
export interface MultiModeUrls {
/**
* url
*/
default: string[];
/**
* url
*/
[mode: string]: string[];
}
/**
*
*/
export type LoadEnv = 'design' | 'runtime';

View File

@ -0,0 +1,345 @@
/**
* https://lowcode-engine.cn/site/docs/specs/lowcode-spec
*
*/
/**
* https://lowcode-engine.cn/site/docs/specs/lowcode-spec#2-%E5%8D%8F%E8%AE%AE%E7%BB%93%E6%9E%84
*
*/
export interface Project {
/**
*
*/
version: string;
/**
*
*/
componentsMap: ComponentMap[];
/**
* ///
*/
componentsTree: ComponentTree[];
/**
*
*/
utils?: Util[];
/**
*
*/
i18n?: I18nMap;
/**
*
*/
constants?: ConstantsMap;
/**
*
* reset.css
*/
css?: string;
/**
*
*/
config?: Record<string, JSONValue>;
/**
*
*/
meta?: Record<string, JSONValue>;
/**
*
* @deprecated
*/
dataSource?: unknown;
/**
*
*/
router?: RouterConfig;
/**
*
*/
pages?: PageConfig[];
}
/**
* https://lowcode-engine.cn/site/docs/specs/lowcode-spec#22-%E7%BB%84%E4%BB%B6%E6%98%A0%E5%B0%84%E5%85%B3%E7%B3%BBa
* componentName
*/
export interface ComponentMap {
/**
* JS
*/
componentName: string;
/**
* npm package name
*/
package?: string;
/**
* package version
*/
version?: string;
/**
* 使
*/
destructuring?: boolean;
/**
*
*/
exportName?: string;
/**
*
*/
subName?: string;
/**
*
*/
main?: string;
/**
* proCode or lowCode
*/
devMode?: string;
}
/**
*
* &
*/
export type ComponentTree<LifeCycleNameT extends string = string> =
ComponentTreeContainer<LifeCycleNameT>;
/**
* (A)
*
*/
export interface ComponentTreeContainer<LifeCycleNameT extends string>
extends Omit<ComponentTreeNode, 'loop' | 'loopArgs' | 'condition'> {
componentName: 'Page' | 'Block' | 'Component';
/**
*
*/
fileName: string;
/**
*
*/
state?: Record<string, JSONValue | JSExpression>;
/**
*
*/
css?: string;
/**
*
*/
lifeCycles?: {
[name in LifeCycleNameT]: JSFunction;
};
/**
*
*/
methods?: {
[name: string]: JSFunction;
};
/**
*
* type todo
*/
dataSource?: any;
}
/**
* A
*/
export interface ComponentTreeNode {
/**
*
*/
id?: string;
/**
*
*/
componentName: string;
/**
*
*/
props?: ComponentTreeNodeProps;
/**
*
*/
condition?: boolean | JSExpression;
/**
*
*/
loop?: any[] | JSExpression;
/**
* ["item", "index"]
*/
loopArgs?: [string, string];
/**
*
*/
children?: NodeType[];
}
/**
* Props
*/
export interface ComponentTreeNodeProps {
/** 组件 ID */
id?: string | JSExpression;
/** 组件样式类名 */
className?: string;
/** 组件内联样式 */
style?: JSONObject | JSExpression;
/** 组件 ref 名称 */
ref?: string | JSExpression;
[key: string]: any;
}
/**
* https://lowcode-engine.cn/site/docs/specs/lowcode-spec#24-%E5%B7%A5%E5%85%B7%E7%B1%BB%E6%89%A9%E5%B1%95%E6%8F%8F%E8%BF%B0aa
* lodash moment API
*/
export interface Util {
name: string;
type: 'npm' | 'function';
content: ComponentMap | JSFunction;
}
/**
* https://lowcode-engine.cn/site/docs/specs/lowcode-spec#25-%E5%9B%BD%E9%99%85%E5%8C%96%E5%A4%9A%E8%AF%AD%E8%A8%80%E6%94%AF%E6%8C%81aa
*
*/
export interface I18nMap {
[locale: string]: Record<string, string>;
}
/**
* AA
* API
*/
export interface ConstantsMap {
[key: string]: JSONValue;
}
/**
* https://lowcode-engine.cn/site/docs/specs/lowcode-spec#211-%E5%BD%93%E5%89%8D%E5%BA%94%E7%94%A8%E7%9A%84%E8%B7%AF%E7%94%B1%E4%BF%A1%E6%81%AFaa
* AA
* -
*/
export interface RouterConfig {
/**
*
*/
baseName: string;
/**
* history
*/
historyMode: 'browser' | 'hash' | 'memory';
/**
*
*/
routes: RouteRecord[];
}
/**
* Route
* Route
*/
export interface RouteRecord {
/**
*
*/
name?: string;
/**
*
*/
path: string;
/**
* IDpage redirect
*/
page?: string;
/**
* page redirect
*/
redirect?: string | object | JSFunction;
/**
*
*/
children?: RouteRecord[];
}
/**
* AA
*
*
*/
export interface PageConfig {
/**
* id
*/
id: string;
/**
* componentsTreepackage componentsTree
*/
type?: string;
/**
* id type id
*/
mappingId: string;
/**
*
*/
meta?: JSONObject;
/**
*
*/
config?: JSONObject;
}
export type JSONValue = number | string | boolean | null;
export interface JSONObject {
[key: string]: JSONValue | JSONObject | JSONObject[];
}
/**
* A
* ReactNode Function-Return-ReactNode
*/
export interface JSSlot {
type: 'JSSlot';
value: 1;
params?: string[];
}
/**
* A
*/
export interface JSFunction {
type: 'JSFunction';
value: string;
}
/**
* A
*/
export interface JSExpression {
type: 'JSExpression';
value: string;
}
/**
* AA
*/
export interface I18nNode {
type: 'i18n';
/**
* i18n key
*/
key: string;
/**
*
*/
params?: Record<string, string | number | JSExpression>;
}
export type NodeType = string | JSExpression | I18nNode | ComponentTreeNode;

View File

@ -0,0 +1,75 @@
import { AnyFunction, PlainObject } from '../common';
/**
* JS this
* this API
*
*/
export interface InstanceApi<InstanceT = unknown> extends InstanceStateApi, InstanceDataSourceApi {
/**
* props
*/
props?: PlainObject;
/**
* ref ref
* @param ref
*/
$(ref: string): InstanceT | undefined;
/**
* ref ref ref
* @param ref
*/
$$(ref: string): InstanceT[];
[methodName: string]: any;
}
export interface InstanceStateApi<S = PlainObject> {
/**
* state
*/
state: Readonly<S>;
/**
*
* like React.Component.setState
*/
setState<K extends keyof S>(
newState: ((prevState: Readonly<S>) => Pick<S, K> | S | null) | (Pick<S, K> | S | null),
callback?: () => void,
): void;
}
export interface InstanceDataSourceApi {
/**
* Map
*/
dataSourceMap: any;
/**
*
*/
reloadDataSource: () => void;
}
export interface UtilsApi {
utils: Record<string, AnyFunction>;
}
export interface IntlApi {
/**
*
* @param i18nKey
* @param params
*/
i18n(i18nKey: string, params?: Record<string, string>): string;
/**
*
*/
getLocale(): string;
/**
*
* @param locale
*/
setLocale(locale: string): void;
}
export interface RouterApi {}

View File

@ -1,11 +1,14 @@
import { appBoosts } from './boosts'; import { appBoosts } from '../boosts';
export type ErrorType = string; export type ErrorType = string;
export class RuntimeError extends Error { export class RuntimeError extends Error {
constructor(public type: ErrorType, message: string) { constructor(
public type: ErrorType,
message: string,
) {
super(message); super(message);
appBoosts.hooks.call(`app:error`, this); appBoosts.hookStore.call(`app:error`, this);
} }
} }

View File

@ -1,11 +1,43 @@
import { useCallbacks, type Callback } from '@alilc/runtime-shared'; import type { AnyFunction } from '../types';
export type EventName = string | number | symbol;
export function useEvent<T = AnyFunction>() {
let events: T[] = [];
function add(fn: T) {
events.push(fn);
return () => {
events = events.filter((e) => e !== fn);
};
}
function remove(fn: T) {
events = events.filter((f) => fn !== f);
}
function list() {
return [...events];
}
return {
add,
remove,
list,
clear() {
events.length = 0;
},
};
}
export type Event<F = AnyFunction> = ReturnType<typeof useEvent<F>>;
export type HookCallback = (...args: any) => Promise<any> | any;
export type HookCallback = (...args: any) => Promise<void> | void;
type HookKeys<T> = keyof T & PropertyKey; type HookKeys<T> = keyof T & PropertyKey;
type InferCallback<HT, HN extends keyof HT> = HT[HN] extends HookCallback type InferCallback<HT, HN extends keyof HT> = HT[HN] extends HookCallback ? HT[HN] : never;
? HT[HN]
: never;
declare global { declare global {
interface Console { interface Console {
@ -18,19 +50,16 @@ declare global {
// https://developer.chrome.com/blog/devtools-modern-web-debugging/#linked-stack-traces // https://developer.chrome.com/blog/devtools-modern-web-debugging/#linked-stack-traces
type CreateTask = typeof console.createTask; type CreateTask = typeof console.createTask;
const defaultTask: ReturnType<CreateTask> = { run: fn => fn() }; const defaultTask: ReturnType<CreateTask> = { run: (fn) => fn() };
const _createTask: CreateTask = () => defaultTask; const _createTask: CreateTask = () => defaultTask;
const createTask = const createTask = typeof console.createTask !== 'undefined' ? console.createTask : _createTask;
typeof console.createTask !== 'undefined' ? console.createTask : _createTask;
export interface Hooks< export interface HookStore<
HooksT extends Record<PropertyKey, any> = Record<PropertyKey, HookCallback>, HooksT extends Record<PropertyKey, any> = Record<PropertyKey, HookCallback>,
HookNameT extends HookKeys<HooksT> = HookKeys<HooksT> HookNameT extends HookKeys<HooksT> = HookKeys<HooksT>,
> { > {
hook<NameT extends HookNameT>( hook<NameT extends HookNameT>(name: NameT, fn: InferCallback<HooksT, NameT>): () => void;
name: NameT,
fn: InferCallback<HooksT, NameT>
): () => void;
call<NameT extends HookNameT>( call<NameT extends HookNameT>(
name: NameT, name: NameT,
...args: Parameters<InferCallback<HooksT, NameT>> ...args: Parameters<InferCallback<HooksT, NameT>>
@ -43,29 +72,28 @@ export interface Hooks<
name: NameT, name: NameT,
...args: Parameters<InferCallback<HooksT, NameT>> ...args: Parameters<InferCallback<HooksT, NameT>>
): Promise<void[]>; ): Promise<void[]>;
remove<NameT extends HookNameT>(
name: NameT, remove<NameT extends HookNameT>(name: NameT, fn?: InferCallback<HooksT, NameT>): void;
fn?: InferCallback<HooksT, NameT>
): void; clear<NameT extends HookNameT>(name?: NameT): void;
getHooks<NameT extends HookNameT>(name: NameT): InferCallback<HooksT, NameT>[] | undefined;
} }
export function createHooks< export function createHookStore<
HooksT extends Record<PropertyKey, any> = Record<PropertyKey, HookCallback>, HooksT extends Record<PropertyKey, any> = Record<PropertyKey, HookCallback>,
HookNameT extends HookKeys<HooksT> = HookKeys<HooksT> HookNameT extends HookKeys<HooksT> = HookKeys<HooksT>,
>(): Hooks<HooksT, HookNameT> { >(): HookStore<HooksT, HookNameT> {
const hooksMap = new Map<HookNameT, Callback<HookCallback>>(); const hooksMap = new Map<HookNameT, Event<HookCallback>>();
function hook<NameT extends HookNameT>( function hook<NameT extends HookNameT>(name: NameT, fn: InferCallback<HooksT, NameT>) {
name: NameT,
fn: InferCallback<HooksT, NameT>
) {
if (!name || typeof fn !== 'function') { if (!name || typeof fn !== 'function') {
return () => {}; return () => {};
} }
let hooks = hooksMap.get(name); let hooks = hooksMap.get(name);
if (!hooks) { if (!hooks) {
hooks = useCallbacks(); hooks = useEvent();
hooksMap.set(name, hooks); hooksMap.set(name, hooks);
} }
@ -92,9 +120,8 @@ export function createHooks<
const task = createTask(name.toString()); const task = createTask(name.toString());
return hooks.reduce( return hooks.reduce(
(promise, hookFunction) => (promise, hookFunction) => promise.then(() => task.run(() => hookFunction(...args))),
promise.then(() => task.run(() => hookFunction(...args))), Promise.resolve(),
Promise.resolve()
); );
} }
@ -104,19 +131,16 @@ export function createHooks<
) { ) {
const hooks = hooksMap.get(name)?.list() ?? []; const hooks = hooksMap.get(name)?.list() ?? [];
const task = createTask(name.toString()); const task = createTask(name.toString());
return Promise.all(hooks.map(hook => task.run(() => hook(...args)))); return Promise.all(hooks.map((hook) => task.run(() => hook(...args))));
} }
function remove<NameT extends HookNameT>( function remove<NameT extends HookNameT>(name: NameT, fn?: InferCallback<HooksT, NameT>) {
name: NameT,
fn?: InferCallback<HooksT, NameT>
) {
const hooks = hooksMap.get(name); const hooks = hooksMap.get(name);
if (!hooks) return; if (!hooks) return;
if (fn) { if (fn) {
hooks.remove(fn); hooks.remove(fn);
if (hooks.list.length === 0) { if (hooks.list().length === 0) {
hooksMap.delete(name); hooksMap.delete(name);
} }
} else { } else {
@ -124,11 +148,30 @@ export function createHooks<
} }
} }
function clear<NameT extends HookNameT>(name?: NameT) {
if (name) {
remove(name);
} else {
hooksMap.clear();
}
}
function getHooks<NameT extends HookNameT>(
name: NameT,
): InferCallback<HooksT, NameT>[] | undefined {
return hooksMap.get(name)?.list() as InferCallback<HooksT, NameT>[] | undefined;
}
return { return {
hook, hook,
call, call,
callAsync, callAsync,
callParallel, callParallel,
remove, remove,
clear,
getHooks,
}; };
} }

View File

@ -0,0 +1,13 @@
export function nonSetterProxy<T extends object>(target: T) {
return new Proxy<T>(target, {
get(target, p, receiver) {
return Reflect.get(target, p, receiver);
},
set() {
return false;
},
has(target, p) {
return Reflect.has(target, p);
},
});
}

View File

@ -0,0 +1,18 @@
import type { JSExpression, JSFunction, I18nNode } from '../types';
import { isPlainObject } from 'lodash-es';
export function isJSExpression(v: unknown): v is JSExpression {
return (
isPlainObject(v) && (v as any).type === 'JSExpression' && typeof (v as any).value === 'string'
);
}
export function isJSFunction(v: unknown): v is JSFunction {
return (
isPlainObject(v) && (v as any).type === 'JSFunction' && typeof (v as any).value === 'string'
);
}
export function isI18nNode(v: unknown): v is I18nNode {
return isPlainObject(v) && (v as any).type === 'i18n' && typeof (v as any).key === 'string';
}

View File

@ -1,11 +0,0 @@
import { type RootSchema } from '@alilc/runtime-shared';
const CONTAINTER_NAME = ['Page', 'Block', 'Component'];
export function validateContainerSchema(schema: RootSchema): boolean {
if (!CONTAINTER_NAME.includes(schema.componentName)) {
return false;
}
return true;
}

View File

@ -0,0 +1,92 @@
import type { NodeType, ComponentTreeNode, ComponentTreeNodeProps } from './types';
import { isJSExpression, isI18nNode } from './utils/type-guard';
export class Widget<Data, Element> {
protected _raw: Data;
protected proxyElements: Element[] = [];
protected renderObject: Element | undefined;
constructor(data: Data) {
this._raw = data;
this.init();
}
protected init() {}
get raw() {
return this._raw;
}
setRenderObject(el: Element) {
this.renderObject = el;
}
getRenderObject() {
return this.renderObject;
}
addProxyELements(el: Element) {
this.proxyElements.push(el);
}
build(builder: (elements: Element[]) => Element) {
return builder(this.proxyElements);
}
}
export type TextWidgetData = Exclude<NodeType, ComponentTreeNode>;
export type TextWidgetType = 'string' | 'expression' | 'i18n';
export class TextWidget<E = unknown> extends Widget<TextWidgetData, E> {
type: TextWidgetType = 'string';
protected init() {
if (isJSExpression(this.raw)) {
this.type = 'expression';
} else if (isI18nNode(this.raw)) {
this.type = 'i18n';
}
}
}
export class ComponentWidget<E = unknown> extends Widget<ComponentTreeNode, E> {
private _children: (TextWidget<E> | ComponentWidget<E>)[] = [];
private _propsValue: ComponentTreeNodeProps = {};
protected init() {
if (this._raw.props) {
this._propsValue = this._raw.props;
}
if (this._raw.children) {
this._children = this._raw.children.map((child) => createWidget<E>(child));
}
}
get componentName() {
return this.raw.componentName;
}
get props() {
return this._propsValue;
}
get children() {
return this._children;
}
get condition() {
return this._raw.condition ?? true;
}
get loop() {
return this._raw.loop;
}
get loopArgs() {
return this._raw.loopArgs ?? ['item', 'index'];
}
}
export function createWidget<E = unknown>(data: NodeType) {
if (typeof data === 'string' || isJSExpression(data) || isI18nNode(data)) {
return new TextWidget<E>(data);
} else if (data.componentName) {
return new ComponentWidget<E>(data);
}
throw Error(`unknown node data: ${JSON.stringify(data)}`);
}

View File

@ -0,0 +1,50 @@
import { describe, it, expect, vi } from 'vitest';
import { createAppFunction } from '../../src/api/app';
import { definePlugin } from '../../src/plugin';
describe('createAppFunction', () => {
it('should require a function argument that returns an render object.', () => {
expect(() => createAppFunction(undefined as any)).rejects.toThrowError();
});
it('should return a function', () => {
const createApp = createAppFunction(async () => {
return {
appBase: {
mount(el) {},
unmount() {},
},
};
});
expect({ createApp }).toEqual({ createApp: expect.any(Function) });
});
it('should construct app object', () => {
expect('').toBe('');
});
it('should plugin inited when app created', async () => {
const plugin = definePlugin({
name: 'test',
setup() {},
});
const spy = vi.spyOn(plugin, 'setup');
const createApp = createAppFunction(async () => {
return {
appBase: {
mount(el) {},
unmount() {},
},
};
});
await createApp({
schema: {},
plugins: [plugin],
});
expect(spy).toHaveBeenCalled();
});
});

View File

@ -0,0 +1,5 @@
import { describe, it, expect } from 'vitest';
describe('createComponentFunction', () => {
it('', () => {});
});

View File

@ -0,0 +1,17 @@
import { describe, expect, it } from 'vitest';
import { appBoosts } from '../src/boosts';
describe('appBoosts', () => {
it('should add value successfully', () => {
appBoosts.add('test', 1);
expect(appBoosts.value.test).toBe(1);
});
it('should clear removed value', () => {
appBoosts.add('test', 1);
expect(appBoosts.value.test).toBe(1);
appBoosts.remove('test');
expect(appBoosts.value.test).toBeUndefined();
});
});

View File

@ -0,0 +1 @@
import {} from 'vitest';

View File

@ -0,0 +1,2 @@
import { expect } from 'vitest';
import { createPackageManager } from '../src/package';

View File

@ -0,0 +1,12 @@
import { describe, it, expect, expectTypeOf } from 'vitest';
import { definePlugin, type Plugin, createPluginManager } from '../src/plugin';
describe('createPluginManager', () => {
it('should install plugin successfully', () => {});
it('should install plugins when deps installed', () => {});
});
describe('definePlugin', () => {
it('should return a plugin', () => {});
});

View File

@ -0,0 +1,169 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { useEvent, createHookStore, type HookStore } from '../../src/utils/hook';
describe('event', () => {
it("event's listener ops", () => {
const event = useEvent();
const fn = () => {};
event.add(fn);
expect(event.list().includes(fn)).toBeTruthy();
event.remove(fn);
expect(event.list().includes(fn)).toBeFalsy();
event.add(fn);
expect(event.list().includes(fn)).toBeTruthy();
event.clear();
expect(event.list().includes(fn)).toBeFalsy();
});
});
describe('hooks', () => {
let hookStore: HookStore;
beforeEach(() => {
hookStore = createHookStore();
});
it('should register hook successfully', () => {
const fn = () => {};
hookStore.hook('test', fn);
expect(hookStore.getHooks('test')).toContain(fn);
});
it('should ignore empty hook', () => {
hookStore.hook('', () => {});
hookStore.hook(undefined as any, () => {});
expect(hookStore.getHooks('')).toBeUndefined();
expect(hookStore.getHooks(undefined as any)).toBeUndefined();
});
it('should ignore not function hook', () => {
hookStore.hook('test', 1 as any);
hookStore.hook('test', undefined as any);
expect(hookStore.getHooks('test')).toBeUndefined();
});
it('should call registered hook', () => {
const spy = vi.fn();
hookStore.hook('test', spy);
hookStore.call('test');
expect(spy).toHaveBeenCalled();
});
it('callAsync: should sequential call registered async hook', async () => {
let count = 0;
const counts: number[] = [];
const fn = async () => {
counts.push(count++);
};
hookStore.hook('test', fn);
hookStore.hook('test', fn);
await hookStore.callAsync('test');
expect(counts).toEqual([0, 1]);
});
it('callParallel: should parallel call registered async hook', async () => {
let count = 0;
const sleep = (delay: number) => {
return new Promise((resolve) => {
setTimeout(resolve, delay);
});
};
hookStore.hook('test', () => {
count++;
});
hookStore.hook('test', async () => {
await sleep(500);
count++;
});
hookStore.hook('test', async () => {
await sleep(1000);
expect(count).toBe(2);
});
await hookStore.callParallel('test');
});
it('should throw hook error', async () => {
const error = new Error('Hook Error');
hookStore.hook('test', () => {
throw error;
});
expect(() => hookStore.call('test')).toThrow(error);
});
it('should return a self-removal function', async () => {
const spy = vi.fn();
const remove = hookStore.hook('test', spy);
hookStore.call('test');
expect(spy).toBeCalledTimes(1);
remove();
hookStore.call('test');
expect(spy).toBeCalledTimes(1);
});
it('should clear removed hooks', () => {
const result: number[] = [];
const fn1 = () => result.push(1);
const fn2 = () => result.push(2);
hookStore.hook('test', fn1);
hookStore.hook('test', fn2);
hookStore.call('test');
expect(result).toHaveLength(2);
expect(result).toEqual([1, 2]);
hookStore.remove('test', fn1);
hookStore.call('test');
expect(result).toHaveLength(3);
expect(result).toEqual([1, 2, 2]);
hookStore.remove('test');
hookStore.call('test');
expect(result).toHaveLength(3);
expect(result).toEqual([1, 2, 2]);
});
it('should clear ops works', () => {
hookStore.hook('test1', () => {});
hookStore.hook('test2', () => {});
expect(hookStore.getHooks('test1')).toHaveLength(1);
expect(hookStore.getHooks('test2')).toHaveLength(1);
hookStore.clear('test1');
expect(hookStore.getHooks('test1')).toBeUndefined();
expect(hookStore.getHooks('test2')).toHaveLength(1);
hookStore.clear();
expect(hookStore.getHooks('test1')).toBeUndefined();
expect(hookStore.getHooks('test2')).toBeUndefined();
});
});

View File

@ -0,0 +1,19 @@
import { describe, it, expect } from 'vitest';
import { nonSetterProxy } from '../../src/utils/non-setter-proxy';
describe('nonSetterProxy', () => {
it('should non setter on proxy', () => {
const target = { a: 1 };
const proxy = nonSetterProxy(target);
expect(() => ((proxy as any).b = 1)).toThrowError(/trap returned falsish for property 'b'/);
});
it('should correct value when getter', () => {
const target = { a: 1 };
const proxy = nonSetterProxy(target);
expect(proxy.a).toBe(1);
expect('a' in proxy).toBeTruthy();
});
});

View File

@ -0,0 +1,7 @@
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
include: ['tests/**/*.spec.ts']
}
})

View File

@ -0,0 +1,33 @@
{
"name": "@alilc/renderer-react",
"version": "2.0.0-beta.0",
"description": "react renderer for ali lowcode engine",
"type": "module",
"bugs": "https://github.com/alibaba/lowcode-engine/issues",
"homepage": "https://github.com/alibaba/lowcode-engine/#readme",
"license": "MIT",
"scripts": {
"build": "",
"test": "vitest"
},
"dependencies": {
"@vue/reactivity": "^3.4.21",
"@alilc/renderer-core": "^2.0.0-beta.0",
"lodash-es": "^4.17.21",
"hoist-non-react-statics": "^3.3.2",
"use-sync-external-store": "^1.2.0",
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
"@testing-library/react": "^14.2.0",
"@types/lodash-es": "^4.17.12",
"@types/react": "^18.2.67",
"@types/react-dom": "^18.2.22",
"jsdom": "^24.0.0"
},
"peerDependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0"
}
}

View File

@ -0,0 +1,62 @@
import {
type App,
type RenderBase,
createAppFunction,
type AppOptionsBase,
} from '@alilc/renderer-core';
import { type ComponentType } from 'react';
import { type Root, createRoot } from 'react-dom/client';
import { createRenderer } from '../renderer';
import AppComponent from '../components/app';
import { intlPlugin } from '../plugins/intl';
import { globalUtilsPlugin } from '../plugins/utils';
import { initRouter } from '../router';
export interface AppOptions extends AppOptionsBase {
dataSourceCreator: DataSourceCreator;
faultComponent?: ComponentType<any>;
}
export interface ReactRender extends RenderBase {}
export type ReactApp = App<ReactRender>;
export const createApp = createAppFunction<AppOptions, ReactRender>(async (context, options) => {
const renderer = createRenderer();
const appContext = { ...context, renderer };
initRouter(appContext);
options.plugins ??= [];
options.plugins!.unshift(globalUtilsPlugin, intlPlugin);
// set config
if (options.faultComponent) {
context.config.set('faultComponent', options.faultComponent);
}
context.config.set('dataSourceCreator', options.dataSourceCreator);
let root: Root | undefined;
const reactRender: ReactRender = {
async mount(el) {
if (root) {
return;
}
root = createRoot(el);
root.render(<AppComponent context={appContext} />);
},
unmount() {
if (root) {
root.unmount();
root = undefined;
}
},
};
return {
renderBase: reactRender,
renderer,
};
});

View File

@ -11,12 +11,12 @@ import {
type CodeRuntime, type CodeRuntime,
createCodeRuntime, createCodeRuntime,
} from '@alilc/runtime-core'; } from '@alilc/runtime-core';
import { isPlainObject } from 'lodash-es';
import { import {
type AnyObject, type AnyObject,
type Package, type Package,
type JSSlot, type JSSlot,
type JSFunction, type JSFunction,
isPlainObject,
isJsExpression, isJsExpression,
isJsSlot, isJsSlot,
isLowCodeComponentPackage, isLowCodeComponentPackage,
@ -79,8 +79,7 @@ export interface ConvertedTreeNode {
setReactNode(element: ReactNode): void; setReactNode(element: ReactNode): void;
} }
export interface CreateComponentOptions<C = ComponentType<any>> export interface CreateComponentOptions<C = ComponentType<any>> extends ComponentOptionsBase<C> {
extends ComponentOptionsBase<C> {
displayName?: string; displayName?: string;
beforeNodeCreateComponent?(convertedNode: ConvertedTreeNode): void; beforeNodeCreateComponent?(convertedNode: ConvertedTreeNode): void;
@ -124,7 +123,7 @@ export const createComponent = createComponentFunction<
function getComponentByName( function getComponentByName(
componentName: string, componentName: string,
componentsRecord: Record<string, ComponentType<any> | Package> componentsRecord: Record<string, ComponentType<any> | Package>,
) { ) {
const Component = componentsRecord[componentName]; const Component = componentsRecord[componentName];
if (!Component) { if (!Component) {
@ -154,7 +153,7 @@ export const createComponent = createComponentFunction<
function createConvertedTreeNode( function createConvertedTreeNode(
rawNode: ComponentTreeNode, rawNode: ComponentTreeNode,
codeRuntime: CodeRuntime codeRuntime: CodeRuntime,
): ConvertedTreeNode { ): ConvertedTreeNode {
let elementValue: ReactNode = null; let elementValue: ReactNode = null;
@ -172,10 +171,7 @@ export const createComponent = createComponentFunction<
}; };
if (rawNode.type === 'component') { if (rawNode.type === 'component') {
node.rawComponent = getComponentByName( node.rawComponent = getComponentByName(rawNode.data.componentName, componentsRecord);
rawNode.data.componentName,
componentsRecord
);
} }
return node; return node;
@ -185,7 +181,7 @@ export const createComponent = createComponentFunction<
node: ComponentTreeNode, node: ComponentTreeNode,
codeRuntime: CodeRuntime, codeRuntime: CodeRuntime,
instance: ContainerInstance, instance: ContainerInstance,
componentsRecord: Record<string, ComponentType<any> | Package> componentsRecord: Record<string, ComponentType<any> | Package>,
) { ) {
const convertedNode = createConvertedTreeNode(node, codeRuntime); const convertedNode = createConvertedTreeNode(node, codeRuntime);
@ -206,7 +202,7 @@ export const createComponent = createComponentFunction<
target: { target: {
text: rawValue, text: rawValue,
}, },
valueGetter: node => codeRuntime.parseExprOrFn(node), valueGetter: (node) => codeRuntime.parseExprOrFn(node),
}); });
convertedNode.setReactNode(<ReactivedText key={rawValue.value} />); convertedNode.setReactNode(<ReactivedText key={rawValue.value} />);
@ -235,7 +231,7 @@ export const createComponent = createComponentFunction<
props: AnyObject, props: AnyObject,
codeRuntime: CodeRuntime, codeRuntime: CodeRuntime,
key: string, key: string,
children: ReactNode[] = [] children: ReactNode[] = [],
) { ) {
const { ref, ...componentProps } = props; const { ref, ...componentProps } = props;
@ -249,45 +245,29 @@ export const createComponent = createComponentFunction<
// 先将 jsslot, jsFunction 对象转换 // 先将 jsslot, jsFunction 对象转换
const finalProps = processValue( const finalProps = processValue(
componentProps, componentProps,
node => isJsSlot(node) || isJsFunction(node), (node) => isJsSlot(node) || isJsFunction(node),
(node: JSSlot | JSFunction) => { (node: JSSlot | JSFunction) => {
if (isJsSlot(node)) { if (isJsSlot(node)) {
if (node.value) { if (node.value) {
const nodes = ( const nodes = (Array.isArray(node.value) ? node.value : [node.value]).map(
Array.isArray(node.value) ? node.value : [node.value] (n) => createNode(n, undefined),
).map(n => createNode(n, undefined)); );
if (node.params?.length) { if (node.params?.length) {
return (...args: any[]) => { return (...args: any[]) => {
const params = node.params!.reduce( const params = node.params!.reduce((prev, cur, idx) => {
(prev, cur, idx) => { return (prev[cur] = args[idx]);
return (prev[cur] = args[idx]); }, {} as AnyObject);
}, const subCodeScope = codeRuntime.getScope().createSubScope(params);
{} as AnyObject const subCodeRuntime = createCodeRuntime(subCodeScope);
);
const subCodeScope = codeRuntime
.getScope()
.createSubScope(params);
const subCodeRuntime =
createCodeRuntime(subCodeScope);
return nodes.map(n => return nodes.map((n) =>
createReactElement( createReactElement(n, subCodeRuntime, instance, componentsRecord),
n,
subCodeRuntime,
instance,
componentsRecord
)
); );
}; };
} else { } else {
return nodes.map(n => return nodes.map((n) =>
createReactElement( createReactElement(n, codeRuntime, instance, componentsRecord),
n,
codeRuntime,
instance,
componentsRecord
)
); );
} }
} }
@ -296,7 +276,7 @@ export const createComponent = createComponentFunction<
} }
return null; return null;
} },
); );
if (someValue(finalProps, isJsExpression)) { if (someValue(finalProps, isJsExpression)) {
@ -308,14 +288,14 @@ export const createComponent = createComponentFunction<
key, key,
ref: refFunction, ref: refFunction,
}, },
children children,
); );
} }
Props.displayName = 'Props'; Props.displayName = 'Props';
const Reactived = reactive(Props, { const Reactived = reactive(Props, {
target: finalProps, target: finalProps,
valueGetter: node => codeRuntime.parseExprOrFn(node), valueGetter: (node) => codeRuntime.parseExprOrFn(node),
}); });
return <Reactived key={key} />; return <Reactived key={key} />;
@ -327,7 +307,7 @@ export const createComponent = createComponentFunction<
key, key,
ref: refFunction, ref: refFunction,
}, },
children children,
); );
} }
} }
@ -339,9 +319,9 @@ export const createComponent = createComponentFunction<
nodeProps, nodeProps,
codeRuntime, codeRuntime,
currentComponentKey, currentComponentKey,
rawNode.children?.map(n => rawNode.children?.map((n) =>
createReactElement(n, codeRuntime, instance, componentsRecord) createReactElement(n, codeRuntime, instance, componentsRecord),
) ),
); );
if (loop) { if (loop) {
@ -360,14 +340,9 @@ export const createComponent = createComponentFunction<
nodeProps, nodeProps,
subCodeRuntime, subCodeRuntime,
`loop-${currentComponentKey}-${idx}`, `loop-${currentComponentKey}-${idx}`,
rawNode.children?.map(n => rawNode.children?.map((n) =>
createReactElement( createReactElement(n, subCodeRuntime, instance, componentsRecord),
n, ),
subCodeRuntime,
instance,
componentsRecord
)
)
); );
}); });
}; };
@ -386,7 +361,7 @@ export const createComponent = createComponentFunction<
target: { target: {
loop, loop,
}, },
valueGetter: expr => codeRuntime.parseExprOrFn(expr), valueGetter: (expr) => codeRuntime.parseExprOrFn(expr),
}); });
element = createElement(ReactivedLoop, { element = createElement(ReactivedLoop, {
@ -410,7 +385,7 @@ export const createComponent = createComponentFunction<
target: { target: {
condition, condition,
}, },
valueGetter: expr => codeRuntime.parseExprOrFn(expr), valueGetter: (expr) => codeRuntime.parseExprOrFn(expr),
}); });
return createElement(ReactivedCondition, { return createElement(ReactivedCondition, {
@ -434,7 +409,7 @@ export const createComponent = createComponentFunction<
const LowCodeComponent = forwardRef(function ( const LowCodeComponent = forwardRef(function (
props: LowCodeComponentProps, props: LowCodeComponentProps,
ref: ForwardedRef<any> ref: ForwardedRef<any>,
) { ) {
const { id, className, style, ...extraProps } = props; const { id, className, style, ...extraProps } = props;
const isMounted = useRef(false); const isMounted = useRef(false);
@ -451,7 +426,7 @@ export const createComponent = createComponentFunction<
scopeValue.reloadDataSource(); scopeValue.reloadDataSource();
if (instance.cssText) { if (instance.cssText) {
appendExternalStyle(instance.cssText).then(el => { appendExternalStyle(instance.cssText).then((el) => {
styleEl = el; styleEl = el;
}); });
} }
@ -484,9 +459,7 @@ export const createComponent = createComponentFunction<
<div id={id} className={className} style={style} ref={ref}> <div id={id} className={className} style={style} ref={ref}>
{instance {instance
.getComponentTreeNodes() .getComponentTreeNodes()
.map(n => .map((n) => createReactElement(n, codeRuntime, instance, componentsRecord))}
createReactElement(n, codeRuntime, instance, componentsRecord)
)}
</div> </div>
); );
}); });

View File

@ -0,0 +1,13 @@
import { createContext, useContext } from 'react';
import { type AppContext as AppContextType } from '@alilc/runtime-core';
import { type ReactRenderer } from '../renderer';
export interface AppContextObject extends AppContextType {
renderer: ReactRenderer;
}
export const AppContext = createContext<AppContextObject>({} as any);
AppContext.displayName = 'RootContext';
export const useAppContext = () => useContext(AppContext);

View File

@ -0,0 +1,33 @@
import { type Router, type RouteLocation } from '@alilc/runtime-router';
import { type PageSchema } from '@alilc/runtime-shared';
import { createContext, useContext } from 'react';
export const RouterContext = createContext<Router>({} as any);
RouterContext.displayName = 'RouterContext';
export const useRouter = () => useContext(RouterContext);
export const RouteLocationContext = createContext<RouteLocation>({
name: undefined,
path: '/',
query: {},
params: {},
hash: '',
fullPath: '/',
redirectedFrom: undefined,
matched: [],
meta: {},
});
RouteLocationContext.displayName = 'RouteLocationContext';
export const useRouteLocation = () => useContext(RouteLocationContext);
export const PageSchemaContext = createContext<PageSchema | undefined>(
undefined
);
PageSchemaContext.displayName = 'PageContext';
export const usePageSchema = () => useContext(PageSchemaContext);

View File

@ -3,12 +3,12 @@ import { someValue } from '@alilc/runtime-core';
import { isJsExpression } from '@alilc/runtime-shared'; import { isJsExpression } from '@alilc/runtime-shared';
import { definePlugin } from '../../renderer'; import { definePlugin } from '../../renderer';
import { PAGE_EVENTS } from '../../events'; import { PAGE_EVENTS } from '../../events';
import { reactive } from '../../helper/reactive'; import { reactive } from '../../utils/reactive';
import { createIntl } from './intl'; import { createIntl } from './intl';
export { createIntl }; export { createIntl };
declare module '@alilc/runtime-core' { declare module '@alilc/renderer-core' {
interface AppBoosts { interface AppBoosts {
intl: ReturnType<typeof createIntl>; intl: ReturnType<typeof createIntl>;
} }
@ -24,7 +24,7 @@ export const intlPlugin = definePlugin({
appScope.setValue(intl); appScope.setValue(intl);
boosts.add('intl', intl); boosts.add('intl', intl);
boosts.hooks.hook(PAGE_EVENTS.COMPONENT_BEFORE_NODE_CREATE, node => { boosts.hooks.hook(PAGE_EVENTS.COMPONENT_BEFORE_NODE_CREATE, (node) => {
if (node.type === 'i18n') { if (node.type === 'i18n') {
const { key, params } = node.raw.data; const { key, params } = node.raw.data;

View File

@ -1,4 +1,4 @@
import { isObject } from '@alilc/runtime-shared'; import { isObject } from 'lodash-es';
const RE_TOKEN_LIST_VALUE: RegExp = /^(?:\d)+/; const RE_TOKEN_LIST_VALUE: RegExp = /^(?:\d)+/;
const RE_TOKEN_NAMED_VALUE: RegExp = /^(?:\w)+/; const RE_TOKEN_NAMED_VALUE: RegExp = /^(?:\w)+/;
@ -32,8 +32,8 @@ export function parse(format: string): Array<Token> {
const type = RE_TOKEN_LIST_VALUE.test(sub) const type = RE_TOKEN_LIST_VALUE.test(sub)
? 'list' ? 'list'
: isClosed && RE_TOKEN_NAMED_VALUE.test(sub) : isClosed && RE_TOKEN_NAMED_VALUE.test(sub)
? 'named' ? 'named'
: 'unknown'; : 'unknown';
tokens.push({ value: sub, type }); tokens.push({ value: sub, type });
} else if (char === '%') { } else if (char === '%') {
// when found rails i18n syntax, skip text capture // when found rails i18n syntax, skip text capture
@ -50,18 +50,11 @@ export function parse(format: string): Array<Token> {
return tokens; return tokens;
} }
export function compile( export function compile(tokens: Token[], values: Record<string, any> | any[] = {}): string[] {
tokens: Token[],
values: Record<string, any> | any[] = {}
): string[] {
const compiled: string[] = []; const compiled: string[] = [];
let index: number = 0; let index: number = 0;
const mode: string = Array.isArray(values) const mode: string = Array.isArray(values) ? 'list' : isObject(values) ? 'named' : 'unknown';
? 'list'
: isObject(values)
? 'named'
: 'unknown';
if (mode === 'unknown') { if (mode === 'unknown') {
return compiled; return compiled;
} }
@ -81,7 +74,7 @@ export function compile(
} else { } else {
if (process.env.NODE_ENV !== 'production') { if (process.env.NODE_ENV !== 'production') {
console.warn( console.warn(
`Type of token '${token.type}' and format of value '${mode}' don't match!` `Type of token '${token.type}' and format of value '${mode}' don't match!`,
); );
} }
} }

View File

@ -1,13 +1,9 @@
import { import { type Router, type RouterOptions, createRouter } from '@alilc/runtime-router';
type Router,
type RouterOptions,
createRouter,
} from '@alilc/runtime-router';
import { createRouterProvider } from './components/router-view'; import { createRouterProvider } from './components/router-view';
import RouteOutlet from './components/outlet'; import RouteOutlet from './components/outlet';
import { type ReactRendererSetupContext } from './renderer'; import { type ReactRendererSetupContext } from './renderer';
declare module '@alilc/runtime-core' { declare module '@alilc/renderer-core' {
interface AppBoosts { interface AppBoosts {
router: Router; router: Router;
} }
@ -21,9 +17,7 @@ const defaultRouterOptions: RouterOptions = {
export function initRouter(context: ReactRendererSetupContext) { export function initRouter(context: ReactRendererSetupContext) {
const { schema, boosts, appScope, renderer } = context; const { schema, boosts, appScope, renderer } = context;
const router = createRouter( const router = createRouter(schema.getByKey('router') ?? defaultRouterOptions);
schema.getByKey('router') ?? defaultRouterOptions
);
appScope.inject('router', router); appScope.inject('router', router);
boosts.add('router', router); boosts.add('router', router);

View File

@ -10,13 +10,7 @@ import {
isReactive, isReactive,
isShallow, isShallow,
} from '@vue/reactivity'; } from '@vue/reactivity';
import { import { noop, isObject, isPlainObject, isSet, isMap } from 'lodash-es';
noop,
isObject,
isPlainObject,
isSet,
isMap,
} from '@alilc/runtime-shared';
export { ref as createSignal, computed, effect }; export { ref as createSignal, computed, effect };
export type { Ref as Signal, ComputedRef as ComputedSignal }; export type { Ref as Signal, ComputedRef as ComputedSignal };
@ -32,7 +26,7 @@ export function watch<T = any>(
}: { }: {
deep?: boolean; deep?: boolean;
immediate?: boolean; immediate?: boolean;
} = {} } = {},
) { ) {
let getter: () => any; let getter: () => any;
let forceTrigger = false; let forceTrigger = false;
@ -42,9 +36,7 @@ export function watch<T = any>(
forceTrigger = isShallow(source); forceTrigger = isShallow(source);
} else if (isReactive(source)) { } else if (isReactive(source)) {
getter = () => { getter = () => {
return deep === true return deep === true ? source : traverse(source, deep === false ? 1 : undefined);
? source
: traverse(source, deep === false ? 1 : undefined);
}; };
forceTrigger = true; forceTrigger = true;
} else { } else {
@ -93,12 +85,7 @@ export function watch<T = any>(
return unwatch; return unwatch;
} }
function traverse( function traverse(value: unknown, depth?: number, currentDepth = 0, seen?: Set<unknown>) {
value: unknown,
depth?: number,
currentDepth = 0,
seen?: Set<unknown>
) {
if (!isObject(value)) { if (!isObject(value)) {
return value; return value;
} }

View File

@ -1,4 +1,6 @@
import { addLeadingSlash } from '@alilc/runtime-shared'; const addLeadingSlash = (path: string): string => {
return path.charAt(0) === '/' ? path : `/${path}`;
};
export function getElementById(id: string, tag: string = 'div') { export function getElementById(id: string, tag: string = 'div') {
let el = document.getElementById(id); let el = document.getElementById(id);
@ -44,13 +46,13 @@ export async function loadPackageUrls(urls: string[]) {
} }
} }
await Promise.all(styles.map(item => appendExternalCss(item))); await Promise.all(styles.map((item) => appendExternalCss(item)));
await Promise.all(scripts.map(item => appendExternalScript(item))); await Promise.all(scripts.map((item) => appendExternalScript(item)));
} }
async function appendExternalScript( async function appendExternalScript(
url: string, url: string,
root: HTMLElement = document.body root: HTMLElement = document.body,
): Promise<HTMLElement> { ): Promise<HTMLElement> {
if (url) { if (url) {
const el = getIfExistAssetByUrl(url, 'script'); const el = getIfExistAssetByUrl(url, 'script');
@ -73,9 +75,9 @@ async function appendExternalScript(
() => { () => {
resolve(scriptElement); resolve(scriptElement);
}, },
false false,
); );
scriptElement.addEventListener('error', error => { scriptElement.addEventListener('error', (error) => {
if (root.contains(scriptElement)) { if (root.contains(scriptElement)) {
root.removeChild(scriptElement); root.removeChild(scriptElement);
} }
@ -88,7 +90,7 @@ async function appendExternalScript(
async function appendExternalCss( async function appendExternalCss(
url: string, url: string,
root: HTMLElement = document.head root: HTMLElement = document.head,
): Promise<HTMLElement> { ): Promise<HTMLElement> {
if (url) { if (url) {
const el = getIfExistAssetByUrl(url, 'link'); const el = getIfExistAssetByUrl(url, 'link');
@ -105,9 +107,9 @@ async function appendExternalCss(
() => { () => {
resolve(el); resolve(el);
}, },
false false,
); );
el.addEventListener('error', error => { el.addEventListener('error', (error) => {
reject(error); reject(error);
}); });
@ -117,7 +119,7 @@ async function appendExternalCss(
export async function appendExternalStyle( export async function appendExternalStyle(
cssText: string, cssText: string,
root: HTMLElement = document.head root: HTMLElement = document.head,
): Promise<HTMLElement> { ): Promise<HTMLElement> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
let el: HTMLStyleElement = document.createElement('style'); let el: HTMLStyleElement = document.createElement('style');
@ -128,9 +130,9 @@ export async function appendExternalStyle(
() => { () => {
resolve(el); resolve(el);
}, },
false false,
); );
el.addEventListener('error', error => { el.addEventListener('error', (error) => {
reject(error); reject(error);
}); });
@ -140,11 +142,10 @@ export async function appendExternalStyle(
function getIfExistAssetByUrl( function getIfExistAssetByUrl(
url: string, url: string,
tag: 'link' | 'script' tag: 'link' | 'script',
): HTMLLinkElement | HTMLScriptElement | undefined { ): HTMLLinkElement | HTMLScriptElement | undefined {
return Array.from(document.getElementsByTagName(tag)).find(item => { return Array.from(document.getElementsByTagName(tag)).find((item) => {
const elUrl = const elUrl = (item as HTMLLinkElement).href || (item as HTMLScriptElement).src;
(item as HTMLLinkElement).href || (item as HTMLScriptElement).src;
if (/^(https?:)?\/\/([\w.]+\/?)\S*/gi.test(url)) { if (/^(https?:)?\/\/([\w.]+\/?)\S*/gi.test(url)) {
// if url === http://xxx.xxx // if url === http://xxx.xxx

View File

@ -0,0 +1,8 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"paths": {
"@alilc/*": ["runtime/*/src"]
}
}
}

View File

@ -0,0 +1,8 @@
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
include: ['tests/*.spec.ts'],
environment: 'jsdom'
}
})

View File

@ -4,5 +4,13 @@
"description": "", "description": "",
"type": "module", "type": "module",
"bugs": "https://github.com/alibaba/lowcode-engine/issues", "bugs": "https://github.com/alibaba/lowcode-engine/issues",
"homepage": "https://github.com/alibaba/lowcode-engine/#readme" "homepage": "https://github.com/alibaba/lowcode-engine/#readme",
} "license": "MIT",
"scripts": {
"build": "",
"test": "vitest"
},
"dependencies": {
"@alilc/renderer-core": "^2.0.0-beta.0"
}
}

View File

@ -1,3 +1,6 @@
{ {
"extends": "../../tsconfig.json" "extends": "../../tsconfig.json",
} "compilerOptions": {
"outDir": "dist"
}
}