mirror of
https://github.com/alibaba/lowcode-engine.git
synced 2025-12-13 04:03:07 +00:00
feat: add renderer-core codes
This commit is contained in:
parent
3ba0926438
commit
fb5de6441d
File diff suppressed because it is too large
Load Diff
@ -57,6 +57,7 @@
|
||||
"lerna": "^4.0.0",
|
||||
"typescript": "^5.4.2",
|
||||
"yarn": "^1.22.17",
|
||||
"prettier": "^3.2.5",
|
||||
"rimraf": "^3.0.2",
|
||||
"rollup": "^4.13.0",
|
||||
"vite": "^5.1.6",
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@alilc/lowcode-types",
|
||||
"version": "1.3.2",
|
||||
"version": "1.3.3",
|
||||
"description": "Types for Ali lowCode engine",
|
||||
"files": [
|
||||
"es",
|
||||
|
||||
@ -1,16 +1,19 @@
|
||||
import { IPublicEnumContextMenuType } from '../enum';
|
||||
import { IPublicModelNode } from '../model';
|
||||
import { IPublicTypeI18nData } from './i8n-data';
|
||||
import { IPublicTypeI18nData } from './i18n-data';
|
||||
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;
|
||||
|
||||
items?: Omit<IPublicTypeContextMenuItem, 'items'>[];
|
||||
}
|
||||
|
||||
export interface IPublicTypeContextMenuAction {
|
||||
|
||||
/**
|
||||
* 动作的唯一标识符
|
||||
* Unique identifier for the action
|
||||
@ -41,7 +44,11 @@ export interface IPublicTypeContextMenuAction {
|
||||
* 子菜单项或生成子节点的函数,可选,仅支持两级
|
||||
* 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;
|
||||
}
|
||||
|
||||
|
||||
@ -24,7 +24,7 @@ export * from './widget-base-config';
|
||||
export * from './node-data';
|
||||
export * from './icon-type';
|
||||
export * from './transformed-component-metadata';
|
||||
export * from './i8n-data';
|
||||
export * from './i18n-data';
|
||||
export * from './npm-info';
|
||||
export * from './drag-node-data-object';
|
||||
export * from './drag-node-object';
|
||||
@ -93,4 +93,4 @@ export * from './scrollable';
|
||||
export * from './simulator-renderer';
|
||||
export * from './config-transducer';
|
||||
export * from './context-menu';
|
||||
export * from './command';
|
||||
export * from './command';
|
||||
|
||||
@ -12,5 +12,7 @@ export interface IPublicTypeLowCodeComponent {
|
||||
}
|
||||
|
||||
export type IPublicTypeProCodeComponent = IPublicTypeNpmInfo;
|
||||
export type IPublicTypeComponentMap = IPublicTypeProCodeComponent | IPublicTypeLowCodeComponent;
|
||||
export type IPublicTypeComponentMap =
|
||||
| IPublicTypeProCodeComponent
|
||||
| IPublicTypeLowCodeComponent;
|
||||
export type IPublicTypeComponentsMap = IPublicTypeComponentMap[];
|
||||
|
||||
7
rollup.config.mjs
Normal file
7
rollup.config.mjs
Normal file
@ -0,0 +1,7 @@
|
||||
export default {
|
||||
input: 'src/main.js',
|
||||
output: {
|
||||
file: 'bundle.js',
|
||||
format: 'cjs'
|
||||
}
|
||||
};
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
@ -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,
|
||||
};
|
||||
}
|
||||
);
|
||||
@ -1,3 +0,0 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json"
|
||||
}
|
||||
@ -5,6 +5,12 @@
|
||||
"type": "module",
|
||||
"bugs": "https://github.com/alibaba/lowcode-engine/issues",
|
||||
"homepage": "https://github.com/alibaba/lowcode-engine/#readme",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"build": "",
|
||||
"test": "vitest --run",
|
||||
"test:watch": "vitest"
|
||||
},
|
||||
"dependencies": {
|
||||
"@alilc/lowcode-types": "1.3.2",
|
||||
"lodash-es": "^4.17.21"
|
||||
|
||||
@ -1,26 +1,18 @@
|
||||
import {
|
||||
type ProjectSchema,
|
||||
type Package,
|
||||
type AnyObject,
|
||||
} from '@alilc/runtime-shared';
|
||||
import { type PackageManager, createPackageManager } from '../core/package';
|
||||
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';
|
||||
import type { Project, Package, PlainObject } from '../types';
|
||||
import { type PackageManager, createPackageManager } from '../package';
|
||||
import { createPluginManager, type Plugin } from '../plugin';
|
||||
import { createScope, type CodeScope } from '../code-runtime';
|
||||
import { appBoosts, type AppBoosts, type AppBoostsManager } from '../boosts';
|
||||
import { type AppSchema, createAppSchema } from '../schema';
|
||||
|
||||
export interface AppOptionsBase {
|
||||
schema: ProjectSchema;
|
||||
schema: Project;
|
||||
packages?: Package[];
|
||||
plugins?: Plugin[];
|
||||
appScopeValue?: AnyObject;
|
||||
appScopeValue?: PlainObject;
|
||||
}
|
||||
|
||||
export interface RenderBase {
|
||||
export interface AppBase {
|
||||
mount: (el: HTMLElement) => void | Promise<void>;
|
||||
unmount: () => void | Promise<void>;
|
||||
}
|
||||
@ -36,13 +28,13 @@ export interface AppContext {
|
||||
boosts: AppBoostsManager;
|
||||
}
|
||||
|
||||
type AppCreator<O, T> = (
|
||||
type AppCreator<O, T extends AppBase> = (
|
||||
appContext: Omit<AppContext, 'renderer'>,
|
||||
appOptions: O
|
||||
) => Promise<{ renderBase: T; renderer?: any }>;
|
||||
appOptions: O,
|
||||
) => Promise<{ appBase: T; renderer?: any }>;
|
||||
|
||||
export type App<T extends RenderBase = RenderBase> = {
|
||||
schema: ProjectSchema;
|
||||
export type App<T extends AppBase = AppBase> = {
|
||||
schema: Project;
|
||||
config: Map<string, any>;
|
||||
readonly boosts: AppBoosts;
|
||||
|
||||
@ -55,11 +47,14 @@ export type App<T extends RenderBase = RenderBase> = {
|
||||
* @param options
|
||||
* @returns
|
||||
*/
|
||||
export function createAppFunction<
|
||||
O extends AppOptionsBase,
|
||||
T extends RenderBase = RenderBase
|
||||
>(appCreator: AppCreator<O, T>): (options: O) => Promise<App<T>> {
|
||||
return async options => {
|
||||
export function createAppFunction<O extends AppOptionsBase, T extends AppBase = AppBase>(
|
||||
appCreator: AppCreator<O, T>,
|
||||
): (options: O) => Promise<App<T>> {
|
||||
if (typeof appCreator !== 'function') {
|
||||
throw Error('The first parameter must be a function.');
|
||||
}
|
||||
|
||||
return async (options) => {
|
||||
const { schema, appScopeValue = {} } = options;
|
||||
const appSchema = createAppSchema(schema);
|
||||
const appConfig = new Map<string, any>();
|
||||
@ -77,14 +72,19 @@ export function createAppFunction<
|
||||
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({
|
||||
...appContext,
|
||||
renderer,
|
||||
});
|
||||
|
||||
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) {
|
||||
@ -100,7 +100,7 @@ export function createAppFunction<
|
||||
return appBoosts.value;
|
||||
},
|
||||
},
|
||||
renderBase
|
||||
appBase,
|
||||
);
|
||||
};
|
||||
}
|
||||
@ -1,104 +1,27 @@
|
||||
import { isJsFunction } from '@alilc/runtime-shared';
|
||||
import {
|
||||
type CodeRuntime,
|
||||
createCodeRuntime,
|
||||
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;
|
||||
}
|
||||
import { CreateContainerOptions, createContainer } from '../container';
|
||||
import { createCodeRuntime, createScope } from '../code-runtime';
|
||||
import { throwRuntimeError } from '../utils/error';
|
||||
import { validateContainerSchema } from '../validator/schema';
|
||||
|
||||
export interface ComponentOptionsBase<C> {
|
||||
componentsTree: RootSchema;
|
||||
componentsRecord: Record<string, C | Package>;
|
||||
supCodeScope?: CodeScope;
|
||||
initScopeValue?: AnyObject;
|
||||
dataSourceCreator: DataSourceCreator;
|
||||
}
|
||||
|
||||
export function createComponentFunction<
|
||||
C,
|
||||
O extends ComponentOptionsBase<C>
|
||||
>(options: {
|
||||
export function createComponentFunction<C, O extends ComponentOptionsBase<C>>(options: {
|
||||
stateCreator: (initState: AnyObject) => StateContext;
|
||||
componentCreator: (container: Container, componentOptions: O) => C;
|
||||
defaultOptions?: Partial<O>;
|
||||
}): (componentOptions: O) => C {
|
||||
const { stateCreator, componentCreator, defaultOptions = {} } = options;
|
||||
|
||||
return componentOptions => {
|
||||
return (componentOptions) => {
|
||||
const finalOptions = Object.assign({}, defaultOptions, componentOptions);
|
||||
const {
|
||||
supCodeScope,
|
||||
initScopeValue = {},
|
||||
dataSourceCreator,
|
||||
} = finalOptions;
|
||||
const { supCodeScope, initScopeValue = {}, dataSourceCreator } = finalOptions;
|
||||
|
||||
const codeRuntimeScope =
|
||||
supCodeScope?.createSubScope(initScopeValue) ??
|
||||
createScope(initScopeValue);
|
||||
supCodeScope?.createSubScope(initScopeValue) ?? createScope(initScopeValue);
|
||||
const codeRuntime = createCodeRuntime(codeRuntimeScope);
|
||||
|
||||
const container: Container = {
|
||||
@ -116,9 +39,7 @@ export function createComponentFunction<
|
||||
|
||||
const mapRefToComponentInstance: Map<string, C> = new Map();
|
||||
|
||||
const initialState = codeRuntime.parseExprOrFn(
|
||||
componentsTree.state ?? {}
|
||||
);
|
||||
const initialState = codeRuntime.parseExprOrFn(componentsTree.state ?? {});
|
||||
const stateContext = stateCreator(initialState);
|
||||
|
||||
codeRuntimeScope.setValue(
|
||||
@ -135,13 +56,10 @@ export function createComponentFunction<
|
||||
},
|
||||
stateContext,
|
||||
dataSourceCreator
|
||||
? dataSourceCreator(
|
||||
componentsTree.dataSource ?? ({ list: [] } as any),
|
||||
stateContext
|
||||
)
|
||||
: {}
|
||||
? dataSourceCreator(componentsTree.dataSource ?? ({ list: [] } as any), stateContext)
|
||||
: {},
|
||||
) as ContainerInstanceScope<C>,
|
||||
true
|
||||
true,
|
||||
);
|
||||
|
||||
if (componentsTree.methods) {
|
||||
@ -155,10 +73,7 @@ export function createComponentFunction<
|
||||
|
||||
triggerLifeCycle('constructor');
|
||||
|
||||
function triggerLifeCycle(
|
||||
lifeCycleName: LifeCycleName,
|
||||
...args: any[]
|
||||
) {
|
||||
function triggerLifeCycle(lifeCycleName: LifeCycleNameT, ...args: any[]) {
|
||||
// keys 用来判断 lifeCycleName 存在于 schema 对象上,不获取原型链上的对象
|
||||
if (
|
||||
!componentsTree.lifeCycles ||
|
||||
@ -169,9 +84,7 @@ export function createComponentFunction<
|
||||
|
||||
const lifeCycleSchema = componentsTree.lifeCycles[lifeCycleName];
|
||||
if (isJsFunction(lifeCycleSchema)) {
|
||||
const lifeCycleFn = codeRuntime.createFnBoundScope(
|
||||
lifeCycleSchema.value
|
||||
);
|
||||
const lifeCycleFn = codeRuntime.createFnBoundScope(lifeCycleSchema.value);
|
||||
if (lifeCycleFn) {
|
||||
lifeCycleFn.apply(codeRuntime.getScope().value, args);
|
||||
}
|
||||
@ -202,8 +115,8 @@ export function createComponentFunction<
|
||||
? componentsTree.children
|
||||
: [componentsTree.children]
|
||||
: [];
|
||||
const treeNodes = childNodes.map(item => {
|
||||
return createNode(item, undefined);
|
||||
const treeNodes = childNodes.map((item) => {
|
||||
return createComponentTreeNode(item, undefined);
|
||||
});
|
||||
|
||||
return treeNodes;
|
||||
@ -1,8 +1,11 @@
|
||||
import { type AnyFunction } from '@alilc/runtime-shared';
|
||||
import { createHooks, type Hooks } from '../helper/hook';
|
||||
import { type RuntimeError } from './error';
|
||||
import { type AnyFunction } from './types';
|
||||
import { createHookStore, type HookStore } from './utils/hook';
|
||||
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 {
|
||||
'app:error': (error: RuntimeError) => void;
|
||||
@ -11,22 +14,29 @@ export interface RuntimeHooks {
|
||||
}
|
||||
|
||||
export interface AppBoostsManager {
|
||||
hooks: Hooks<RuntimeHooks>;
|
||||
hookStore: HookStore<RuntimeHooks>;
|
||||
|
||||
readonly value: AppBoosts;
|
||||
add(name: PropertyKey, value: any, force?: boolean): void;
|
||||
remove(name: PropertyKey): void;
|
||||
}
|
||||
|
||||
const boostsValue: AppBoosts = {};
|
||||
const proxyBoostsValue = nonSetterProxy(boostsValue);
|
||||
|
||||
export const appBoosts: AppBoostsManager = {
|
||||
hooks: createHooks(),
|
||||
hookStore: createHookStore(),
|
||||
|
||||
get value() {
|
||||
return boostsValue;
|
||||
return proxyBoostsValue;
|
||||
},
|
||||
add(name: PropertyKey, value: any, force = false) {
|
||||
if ((boostsValue as any)[name] && !force) return;
|
||||
(boostsValue as any)[name] = value;
|
||||
},
|
||||
remove(name) {
|
||||
if ((boostsValue as any)[name]) {
|
||||
delete (boostsValue as any)[name];
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
@ -1,24 +1,22 @@
|
||||
import {
|
||||
type AnyFunction,
|
||||
type AnyObject,
|
||||
JSExpression,
|
||||
JSFunction,
|
||||
isJsExpression,
|
||||
isJsFunction,
|
||||
} from '@alilc/runtime-shared';
|
||||
import { processValue } from '../utils/value';
|
||||
import type { AnyFunction, PlainObject, JSExpression, JSFunction } from './types';
|
||||
import { isJSExpression, isJSFunction } from './utils/type-guard';
|
||||
import { processValue } from './utils/value';
|
||||
|
||||
export interface CodeRuntime {
|
||||
run<T = unknown>(code: string): T | undefined;
|
||||
createFnBoundScope(code: string): AnyFunction | undefined;
|
||||
parseExprOrFn(value: AnyObject): any;
|
||||
parseExprOrFn(value: PlainObject): any;
|
||||
|
||||
bindingScope(scope: CodeScope): void;
|
||||
getScope(): CodeScope;
|
||||
}
|
||||
|
||||
export function createCodeRuntime(scope?: CodeScope): CodeRuntime {
|
||||
let runtimeScope = scope ?? createScope({});
|
||||
const SYMBOL_SIGN = '__code__scope';
|
||||
|
||||
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 {
|
||||
if (!code) return undefined;
|
||||
@ -26,16 +24,11 @@ export function createCodeRuntime(scope?: CodeScope): CodeRuntime {
|
||||
try {
|
||||
return new Function(
|
||||
'scope',
|
||||
`"use strict";return (function(){return (${code})}).bind(scope)();`
|
||||
`"use strict";return (function(){return (${code})}).bind(scope)();`,
|
||||
)(runtimeScope.value) as T;
|
||||
} catch (err) {
|
||||
console.log(
|
||||
'%c eval error',
|
||||
'font-size:13px; background:pink; color:#bf2c9f;',
|
||||
code,
|
||||
scope.value,
|
||||
err
|
||||
);
|
||||
// todo
|
||||
console.error('%c eval error', code, runtimeScope.value, err);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
@ -46,11 +39,11 @@ export function createCodeRuntime(scope?: CodeScope): CodeRuntime {
|
||||
return fn.bind(runtimeScope.value);
|
||||
}
|
||||
|
||||
function parseExprOrFn(value: AnyObject) {
|
||||
function parseExprOrFn(value: PlainObject) {
|
||||
return processValue(
|
||||
value,
|
||||
data => {
|
||||
return isJsExpression(data) || isJsFunction(data);
|
||||
(data) => {
|
||||
return isJSExpression(data) || isJSFunction(data);
|
||||
},
|
||||
(node: JSExpression | JSFunction) => {
|
||||
let v;
|
||||
@ -65,7 +58,7 @@ export function createCodeRuntime(scope?: CodeScope): CodeRuntime {
|
||||
return (node as any).mock;
|
||||
}
|
||||
return v;
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@ -84,14 +77,14 @@ export function createCodeRuntime(scope?: CodeScope): CodeRuntime {
|
||||
}
|
||||
|
||||
export interface CodeScope {
|
||||
readonly value: AnyObject;
|
||||
readonly value: PlainObject;
|
||||
|
||||
inject(name: string, value: any, force?: boolean): void;
|
||||
setValue(value: AnyObject, replace?: boolean): void;
|
||||
createSubScope(initValue: AnyObject): CodeScope;
|
||||
setValue(value: PlainObject, replace?: boolean): void;
|
||||
createSubScope(initValue?: PlainObject): CodeScope;
|
||||
}
|
||||
|
||||
export function createScope(initValue: AnyObject): CodeScope {
|
||||
export function createScope(initValue: PlainObject = {}): CodeScope {
|
||||
const innerScope = { value: initValue };
|
||||
const proxyValue = new Proxy(Object.create(null), {
|
||||
set(target, p, newValue, receiver) {
|
||||
@ -120,7 +113,7 @@ export function createScope(initValue: AnyObject): CodeScope {
|
||||
innerScope.value[name] = value;
|
||||
}
|
||||
|
||||
function createSubScope(initValue: AnyObject) {
|
||||
function createSubScope(initValue: PlainObject = {}) {
|
||||
const childScope = createScope(initValue);
|
||||
|
||||
(childScope as any).__raw.__parent = innerScope;
|
||||
@ -144,6 +137,8 @@ export function createScope(initValue: AnyObject): CodeScope {
|
||||
createSubScope,
|
||||
};
|
||||
|
||||
Object.defineProperty(scope, Symbol.for(SYMBOL_SIGN), { get: () => true });
|
||||
// development env
|
||||
Object.defineProperty(scope, '__raw', { get: () => innerScope });
|
||||
|
||||
return scope;
|
||||
|
||||
166
runtime/renderer-core/src/container.ts
Normal file
166
runtime/renderer-core/src/container.ts
Normal 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');
|
||||
}
|
||||
}
|
||||
@ -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';
|
||||
@ -1,13 +1,6 @@
|
||||
import {
|
||||
type Package,
|
||||
type ProCodeComponent,
|
||||
type ComponentMap,
|
||||
type LowCodeComponent,
|
||||
isLowCodeComponentPackage,
|
||||
} from '@alilc/runtime-shared';
|
||||
import { type Package, type ComponentMap, type LowCodeComponent } from './types';
|
||||
|
||||
const packageStore: Map<string, any> = ((window as any).__PACKAGE_STORE__ ??=
|
||||
new Map());
|
||||
const packageStore: Map<string, any> = ((window as any).__PACKAGE_STORE__ ??= new Map());
|
||||
|
||||
export interface PackageLoader {
|
||||
name?: string;
|
||||
@ -31,9 +24,7 @@ export interface PackageManager {
|
||||
/** 解析组件映射 */
|
||||
resolveComponentMaps(componentMaps: ComponentMap[]): void;
|
||||
/** 获取组件映射对象,key = componentName value = component */
|
||||
getComponentsNameRecord<C = unknown>(
|
||||
componentMaps?: ComponentMap[]
|
||||
): Record<string, C>;
|
||||
getComponentsNameRecord<C = unknown>(componentMaps?: ComponentMap[]): Record<string, C>;
|
||||
/** 通过组件名获取对应的组件 */
|
||||
getComponent<C = unknown>(componentName: string): C | undefined;
|
||||
/** 注册组件 */
|
||||
@ -48,8 +39,10 @@ export function createPackageManager(): PackageManager {
|
||||
|
||||
async function addPackages(packages: Package[]) {
|
||||
for (const item of packages) {
|
||||
const newId = item.package ?? item.id;
|
||||
const isExist = packagesRef.some(_ => {
|
||||
if (!item.package && !item.id) continue;
|
||||
|
||||
const newId = item.package ?? item.id!;
|
||||
const isExist = packagesRef.some((_) => {
|
||||
const itemId = _.package ?? _.id;
|
||||
return itemId === newId;
|
||||
});
|
||||
@ -58,7 +51,7 @@ export function createPackageManager(): PackageManager {
|
||||
packagesRef.push(item);
|
||||
|
||||
if (!packageStore.has(newId)) {
|
||||
const loader = packageLoaders.find(loader => loader.active(item));
|
||||
const loader = packageLoaders.find((loader) => loader.active(item));
|
||||
if (!loader) continue;
|
||||
|
||||
try {
|
||||
@ -73,14 +66,14 @@ export function createPackageManager(): PackageManager {
|
||||
}
|
||||
|
||||
function getPackageInfo(packageName: string) {
|
||||
return packagesRef.find(p => p.package === packageName);
|
||||
return packagesRef.find((p) => p.package === packageName);
|
||||
}
|
||||
|
||||
function getLibraryByPackageName(packageName: string) {
|
||||
const packageInfo = getPackageInfo(packageName);
|
||||
|
||||
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[]) {
|
||||
for (const map of componentMaps) {
|
||||
if ((map as LowCodeComponent).devMode === 'lowCode') {
|
||||
const packageInfo = packagesRef.find(_ => {
|
||||
if (map.devMode === 'lowCode') {
|
||||
const packageInfo = packagesRef.find((_) => {
|
||||
return _.id === (map as LowCodeComponent).id;
|
||||
});
|
||||
|
||||
if (isLowCodeComponentPackage(packageInfo)) {
|
||||
if (packageInfo) {
|
||||
componentsRecord[map.componentName] = packageInfo;
|
||||
}
|
||||
} else {
|
||||
const npmInfo = map as ProCodeComponent;
|
||||
|
||||
if (packageStore.has(npmInfo.package)) {
|
||||
const library = packageStore.get(npmInfo.package);
|
||||
if (packageStore.has(map.package!)) {
|
||||
const library = packageStore.get(map.package!);
|
||||
// export { exportName } from xxx exportName === global.libraryName.exportName
|
||||
// export exportName from xxx exportName === global.libraryName.default || global.libraryName
|
||||
// export { exportName as componentName } from package
|
||||
// if exportName == null exportName === componentName;
|
||||
// const componentName = exportName.subName, if exportName empty subName donot use
|
||||
const paths =
|
||||
npmInfo.exportName && npmInfo.subName
|
||||
? npmInfo.subName.split('.')
|
||||
: [];
|
||||
const exportName = npmInfo.exportName ?? npmInfo.componentName;
|
||||
const paths = map.exportName && map.subName ? map.subName.split('.') : [];
|
||||
const exportName = map.exportName ?? map.componentName;
|
||||
|
||||
if (npmInfo.destructuring) {
|
||||
if (map.destructuring) {
|
||||
paths.unshift(exportName);
|
||||
}
|
||||
|
||||
@ -123,7 +111,7 @@ export function createPackageManager(): PackageManager {
|
||||
result = result[path] || result;
|
||||
}
|
||||
|
||||
const recordName = npmInfo.componentName ?? npmInfo.exportName;
|
||||
const recordName = map.componentName ?? map.exportName;
|
||||
componentsRecord[recordName] = result;
|
||||
}
|
||||
}
|
||||
@ -152,7 +140,7 @@ export function createPackageManager(): PackageManager {
|
||||
getLibraryByPackageName,
|
||||
setLibraryByPackageName,
|
||||
addPackageLoader(loader) {
|
||||
if (!loader.name || !packageLoaders.some(_ => _.name === loader.name)) {
|
||||
if (!loader.name || !packageLoaders.some((_) => _.name === loader.name)) {
|
||||
packageLoaders.push(loader);
|
||||
}
|
||||
},
|
||||
|
||||
@ -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> {
|
||||
name: string; // 插件的 name 作为唯一标识,并不可重复。
|
||||
@ -14,24 +15,12 @@ export function createPluginManager(context: PluginSetupContext) {
|
||||
const installedPlugins: Plugin[] = [];
|
||||
let readyToInstallPlugins: Plugin[] = [];
|
||||
|
||||
const setupContext = new Proxy(context, {
|
||||
get(target, p, receiver) {
|
||||
return Reflect.get(target, p, receiver);
|
||||
},
|
||||
set() {
|
||||
return false;
|
||||
},
|
||||
has(target, p) {
|
||||
return Reflect.has(target, p);
|
||||
},
|
||||
});
|
||||
const setupContext = nonSetterProxy(context);
|
||||
|
||||
async function install(plugin: Plugin) {
|
||||
if (installedPlugins.some(p => p.name === plugin.name)) return;
|
||||
if (installedPlugins.some((p) => p.name === plugin.name)) return;
|
||||
|
||||
if (
|
||||
plugin.dependsOn?.some(dep => !installedPlugins.some(p => p.name === dep))
|
||||
) {
|
||||
if (plugin.dependsOn?.some((dep) => !installedPlugins.some((p) => p.name === dep))) {
|
||||
readyToInstallPlugins.push(plugin);
|
||||
return;
|
||||
}
|
||||
@ -41,24 +30,22 @@ export function createPluginManager(context: PluginSetupContext) {
|
||||
|
||||
// 遍历未安装的插件 寻找 dependsOn 的插件已安装完的插件进行安装
|
||||
for (const item of readyToInstallPlugins) {
|
||||
if (
|
||||
item.dependsOn?.every(dep => installedPlugins.some(p => p.name === dep))
|
||||
) {
|
||||
if (item.dependsOn?.every((dep) => installedPlugins.some((p) => p.name === dep))) {
|
||||
await item.setup(setupContext);
|
||||
installedPlugins.push(item);
|
||||
}
|
||||
}
|
||||
|
||||
if (readyToInstallPlugins.length) {
|
||||
readyToInstallPlugins = readyToInstallPlugins.filter(item =>
|
||||
installedPlugins.some(p => p.name === item.name)
|
||||
readyToInstallPlugins = readyToInstallPlugins.filter((item) =>
|
||||
installedPlugins.some((p) => p.name === item.name),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
async add(plugin: Plugin) {
|
||||
if (installedPlugins.find(item => item.name === plugin.name)) {
|
||||
if (installedPlugins.find((item) => item.name === plugin.name)) {
|
||||
console.warn('该插件已安装');
|
||||
return;
|
||||
}
|
||||
@ -68,8 +55,6 @@ export function createPluginManager(context: PluginSetupContext) {
|
||||
};
|
||||
}
|
||||
|
||||
export function definePlugin<C extends PluginSetupContext, P = Plugin<C>>(
|
||||
plugin: P
|
||||
) {
|
||||
export function definePlugin<C extends PluginSetupContext, P = Plugin<C>>(plugin: P) {
|
||||
return plugin;
|
||||
}
|
||||
|
||||
@ -1,43 +1,33 @@
|
||||
import type {
|
||||
ProjectSchema,
|
||||
RootSchema,
|
||||
ComponentMap,
|
||||
PageSchema,
|
||||
} from '@alilc/runtime-shared';
|
||||
import { throwRuntimeError } from './error';
|
||||
import type { Project, ComponentTree, ComponentMap, PageConfig } from './types';
|
||||
import { throwRuntimeError } from './utils/error';
|
||||
import { set, get } from 'lodash-es';
|
||||
|
||||
type AppSchemaType = ProjectSchema<RootSchema>;
|
||||
|
||||
export interface AppSchema {
|
||||
getComponentsTrees(): RootSchema[];
|
||||
addComponentsTree(tree: RootSchema): void;
|
||||
getComponentsTrees(): ComponentTree[];
|
||||
addComponentsTree(tree: ComponentTree): void;
|
||||
removeComponentsTree(id: string): void;
|
||||
|
||||
getComponentsMaps(): ComponentMap[];
|
||||
addComponentsMap(componentName: ComponentMap): void;
|
||||
removeComponentsMap(componentName: string): void;
|
||||
|
||||
getPages(): PageSchema[];
|
||||
addPage(page: PageSchema): void;
|
||||
getPages(): PageConfig[];
|
||||
addPage(page: PageConfig): void;
|
||||
removePage(id: string): void;
|
||||
|
||||
getByKey<K extends keyof AppSchemaType>(key: K): AppSchemaType[K] | undefined;
|
||||
updateByKey<K extends keyof AppSchemaType>(
|
||||
getByKey<K extends keyof Project>(key: K): Project[K] | undefined;
|
||||
updateByKey<K extends keyof Project>(
|
||||
key: K,
|
||||
updater: AppSchemaType[K] | ((value: AppSchemaType[K]) => AppSchemaType[K])
|
||||
updater: Project[K] | ((value: Project[K]) => Project[K]),
|
||||
): void;
|
||||
|
||||
getByPath(path: string | string[]): any;
|
||||
updateByPath(
|
||||
path: string | string[],
|
||||
updater: any | ((value: any) => any)
|
||||
): void;
|
||||
updateByPath(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.')) {
|
||||
throwRuntimeError('core', 'schema version must be 1.x.x');
|
||||
}
|
||||
@ -93,21 +83,13 @@ export function createAppSchema(schema: ProjectSchema): AppSchema {
|
||||
return get(schemaRef, path);
|
||||
},
|
||||
updateByPath(path, updater) {
|
||||
set(
|
||||
schemaRef,
|
||||
path,
|
||||
typeof updater === 'function' ? updater(this.getByPath(path)) : updater
|
||||
);
|
||||
set(schemaRef, path, typeof updater === 'function' ? updater(this.getByPath(path)) : updater);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function addArrayItem<T extends Record<string, any>>(
|
||||
target: T[],
|
||||
item: T,
|
||||
comparison: string
|
||||
) {
|
||||
const idx = target.findIndex(_ => _[comparison] === item[comparison]);
|
||||
function addArrayItem<T extends Record<string, any>>(target: T[], item: T, comparison: string) {
|
||||
const idx = target.findIndex((_) => _[comparison] === item[comparison]);
|
||||
if (idx > -1) {
|
||||
target.splice(idx, 1, item);
|
||||
} else {
|
||||
@ -118,8 +100,8 @@ function addArrayItem<T extends Record<string, any>>(
|
||||
function removeArrayItem<T extends Record<string, any>>(
|
||||
target: T[],
|
||||
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);
|
||||
}
|
||||
|
||||
3
runtime/renderer-core/src/types/common.ts
Normal file
3
runtime/renderer-core/src/types/common.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export type AnyFunction = (...args: any[]) => any;
|
||||
|
||||
export type PlainObject = Record<PropertyKey, any>;
|
||||
5
runtime/renderer-core/src/types/index.ts
Normal file
5
runtime/renderer-core/src/types/index.ts
Normal 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';
|
||||
14
runtime/renderer-core/src/types/material.ts
Normal file
14
runtime/renderer-core/src/types/material.ts
Normal 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;
|
||||
}
|
||||
104
runtime/renderer-core/src/types/specs/asset-spec.ts
Normal file
104
runtime/renderer-core/src/types/specs/asset-spec.ts
Normal 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';
|
||||
345
runtime/renderer-core/src/types/specs/lowcode-spec.ts
Normal file
345
runtime/renderer-core/src/types/specs/lowcode-spec.ts
Normal 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;
|
||||
/**
|
||||
* 路径对应的页面 ID,page 与 redirect 字段中必须要有有一个存在
|
||||
*/
|
||||
page?: string;
|
||||
/**
|
||||
* 此路径需要重定向到的路由信息,page 与 redirect 字段中必须要有有一个存在
|
||||
*/
|
||||
redirect?: string | object | JSFunction;
|
||||
/**
|
||||
* 子路由
|
||||
*/
|
||||
children?: RouteRecord[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 当前应用的页面信息(AA)
|
||||
* 用于描述当前应用的页面信息,比如页面对应的低代码搭建内容、页面标题、页面配置等。
|
||||
* 在一些比较复杂的场景下,声明一层页面映射关系,以支持页面声明更多信息与配置,同时能够支持不同类型的产物。
|
||||
*/
|
||||
export interface PageConfig {
|
||||
/**
|
||||
* 页面 id
|
||||
*/
|
||||
id: string;
|
||||
/**
|
||||
* 页面类型,如 componentsTree,package,默认为 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;
|
||||
75
runtime/renderer-core/src/types/specs/runtime-api.ts
Normal file
75
runtime/renderer-core/src/types/specs/runtime-api.ts
Normal 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 {}
|
||||
@ -1,11 +1,14 @@
|
||||
import { appBoosts } from './boosts';
|
||||
import { appBoosts } from '../boosts';
|
||||
|
||||
export type ErrorType = string;
|
||||
|
||||
export class RuntimeError extends Error {
|
||||
constructor(public type: ErrorType, message: string) {
|
||||
constructor(
|
||||
public type: ErrorType,
|
||||
message: string,
|
||||
) {
|
||||
super(message);
|
||||
appBoosts.hooks.call(`app:error`, this);
|
||||
appBoosts.hookStore.call(`app:error`, this);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 InferCallback<HT, HN extends keyof HT> = HT[HN] extends HookCallback
|
||||
? HT[HN]
|
||||
: never;
|
||||
type InferCallback<HT, HN extends keyof HT> = HT[HN] extends HookCallback ? HT[HN] : never;
|
||||
|
||||
declare global {
|
||||
interface Console {
|
||||
@ -18,19 +50,16 @@ declare global {
|
||||
|
||||
// https://developer.chrome.com/blog/devtools-modern-web-debugging/#linked-stack-traces
|
||||
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 =
|
||||
typeof console.createTask !== 'undefined' ? console.createTask : _createTask;
|
||||
const createTask = typeof console.createTask !== 'undefined' ? console.createTask : _createTask;
|
||||
|
||||
export interface Hooks<
|
||||
export interface HookStore<
|
||||
HooksT extends Record<PropertyKey, any> = Record<PropertyKey, HookCallback>,
|
||||
HookNameT extends HookKeys<HooksT> = HookKeys<HooksT>
|
||||
HookNameT extends HookKeys<HooksT> = HookKeys<HooksT>,
|
||||
> {
|
||||
hook<NameT extends HookNameT>(
|
||||
name: NameT,
|
||||
fn: InferCallback<HooksT, NameT>
|
||||
): () => void;
|
||||
hook<NameT extends HookNameT>(name: NameT, fn: InferCallback<HooksT, NameT>): () => void;
|
||||
|
||||
call<NameT extends HookNameT>(
|
||||
name: NameT,
|
||||
...args: Parameters<InferCallback<HooksT, NameT>>
|
||||
@ -43,29 +72,28 @@ export interface Hooks<
|
||||
name: NameT,
|
||||
...args: Parameters<InferCallback<HooksT, NameT>>
|
||||
): Promise<void[]>;
|
||||
remove<NameT extends HookNameT>(
|
||||
name: NameT,
|
||||
fn?: InferCallback<HooksT, NameT>
|
||||
): void;
|
||||
|
||||
remove<NameT extends HookNameT>(name: NameT, 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>,
|
||||
HookNameT extends HookKeys<HooksT> = HookKeys<HooksT>
|
||||
>(): Hooks<HooksT, HookNameT> {
|
||||
const hooksMap = new Map<HookNameT, Callback<HookCallback>>();
|
||||
HookNameT extends HookKeys<HooksT> = HookKeys<HooksT>,
|
||||
>(): HookStore<HooksT, HookNameT> {
|
||||
const hooksMap = new Map<HookNameT, Event<HookCallback>>();
|
||||
|
||||
function hook<NameT extends HookNameT>(
|
||||
name: NameT,
|
||||
fn: InferCallback<HooksT, NameT>
|
||||
) {
|
||||
function hook<NameT extends HookNameT>(name: NameT, fn: InferCallback<HooksT, NameT>) {
|
||||
if (!name || typeof fn !== 'function') {
|
||||
return () => {};
|
||||
}
|
||||
|
||||
let hooks = hooksMap.get(name);
|
||||
if (!hooks) {
|
||||
hooks = useCallbacks();
|
||||
hooks = useEvent();
|
||||
hooksMap.set(name, hooks);
|
||||
}
|
||||
|
||||
@ -92,9 +120,8 @@ export function createHooks<
|
||||
const task = createTask(name.toString());
|
||||
|
||||
return hooks.reduce(
|
||||
(promise, hookFunction) =>
|
||||
promise.then(() => task.run(() => hookFunction(...args))),
|
||||
Promise.resolve()
|
||||
(promise, hookFunction) => promise.then(() => task.run(() => hookFunction(...args))),
|
||||
Promise.resolve(),
|
||||
);
|
||||
}
|
||||
|
||||
@ -104,19 +131,16 @@ export function createHooks<
|
||||
) {
|
||||
const hooks = hooksMap.get(name)?.list() ?? [];
|
||||
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>(
|
||||
name: NameT,
|
||||
fn?: InferCallback<HooksT, NameT>
|
||||
) {
|
||||
function remove<NameT extends HookNameT>(name: NameT, fn?: InferCallback<HooksT, NameT>) {
|
||||
const hooks = hooksMap.get(name);
|
||||
if (!hooks) return;
|
||||
|
||||
if (fn) {
|
||||
hooks.remove(fn);
|
||||
if (hooks.list.length === 0) {
|
||||
if (hooks.list().length === 0) {
|
||||
hooksMap.delete(name);
|
||||
}
|
||||
} 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 {
|
||||
hook,
|
||||
|
||||
call,
|
||||
callAsync,
|
||||
callParallel,
|
||||
|
||||
remove,
|
||||
clear,
|
||||
|
||||
getHooks,
|
||||
};
|
||||
}
|
||||
|
||||
13
runtime/renderer-core/src/utils/non-setter-proxy.ts
Normal file
13
runtime/renderer-core/src/utils/non-setter-proxy.ts
Normal 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);
|
||||
},
|
||||
});
|
||||
}
|
||||
18
runtime/renderer-core/src/utils/type-guard.ts
Normal file
18
runtime/renderer-core/src/utils/type-guard.ts
Normal 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';
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
92
runtime/renderer-core/src/widget.ts
Normal file
92
runtime/renderer-core/src/widget.ts
Normal 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)}`);
|
||||
}
|
||||
50
runtime/renderer-core/tests/api/app.spec.ts
Normal file
50
runtime/renderer-core/tests/api/app.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
5
runtime/renderer-core/tests/api/component.spec.ts
Normal file
5
runtime/renderer-core/tests/api/component.spec.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
describe('createComponentFunction', () => {
|
||||
it('', () => {});
|
||||
});
|
||||
17
runtime/renderer-core/tests/boosts.spec.ts
Normal file
17
runtime/renderer-core/tests/boosts.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
1
runtime/renderer-core/tests/code-runtime.spec.ts
Normal file
1
runtime/renderer-core/tests/code-runtime.spec.ts
Normal file
@ -0,0 +1 @@
|
||||
import {} from 'vitest';
|
||||
2
runtime/renderer-core/tests/package.spec.ts
Normal file
2
runtime/renderer-core/tests/package.spec.ts
Normal file
@ -0,0 +1,2 @@
|
||||
import { expect } from 'vitest';
|
||||
import { createPackageManager } from '../src/package';
|
||||
12
runtime/renderer-core/tests/plugin.spec.ts
Normal file
12
runtime/renderer-core/tests/plugin.spec.ts
Normal 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', () => {});
|
||||
});
|
||||
169
runtime/renderer-core/tests/utils/hook.spec.ts
Normal file
169
runtime/renderer-core/tests/utils/hook.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
19
runtime/renderer-core/tests/utils/non-setter-proxy.spec.ts
Normal file
19
runtime/renderer-core/tests/utils/non-setter-proxy.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,7 @@
|
||||
import { defineConfig } from 'vitest/config'
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
include: ['tests/**/*.spec.ts']
|
||||
}
|
||||
})
|
||||
33
runtime/renderer-react/package.json
Normal file
33
runtime/renderer-react/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
62
runtime/renderer-react/src/api/create-app.tsx
Normal file
62
runtime/renderer-react/src/api/create-app.tsx
Normal 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,
|
||||
};
|
||||
});
|
||||
@ -11,12 +11,12 @@ import {
|
||||
type CodeRuntime,
|
||||
createCodeRuntime,
|
||||
} from '@alilc/runtime-core';
|
||||
import { isPlainObject } from 'lodash-es';
|
||||
import {
|
||||
type AnyObject,
|
||||
type Package,
|
||||
type JSSlot,
|
||||
type JSFunction,
|
||||
isPlainObject,
|
||||
isJsExpression,
|
||||
isJsSlot,
|
||||
isLowCodeComponentPackage,
|
||||
@ -79,8 +79,7 @@ export interface ConvertedTreeNode {
|
||||
setReactNode(element: ReactNode): void;
|
||||
}
|
||||
|
||||
export interface CreateComponentOptions<C = ComponentType<any>>
|
||||
extends ComponentOptionsBase<C> {
|
||||
export interface CreateComponentOptions<C = ComponentType<any>> extends ComponentOptionsBase<C> {
|
||||
displayName?: string;
|
||||
|
||||
beforeNodeCreateComponent?(convertedNode: ConvertedTreeNode): void;
|
||||
@ -124,7 +123,7 @@ export const createComponent = createComponentFunction<
|
||||
|
||||
function getComponentByName(
|
||||
componentName: string,
|
||||
componentsRecord: Record<string, ComponentType<any> | Package>
|
||||
componentsRecord: Record<string, ComponentType<any> | Package>,
|
||||
) {
|
||||
const Component = componentsRecord[componentName];
|
||||
if (!Component) {
|
||||
@ -154,7 +153,7 @@ export const createComponent = createComponentFunction<
|
||||
|
||||
function createConvertedTreeNode(
|
||||
rawNode: ComponentTreeNode,
|
||||
codeRuntime: CodeRuntime
|
||||
codeRuntime: CodeRuntime,
|
||||
): ConvertedTreeNode {
|
||||
let elementValue: ReactNode = null;
|
||||
|
||||
@ -172,10 +171,7 @@ export const createComponent = createComponentFunction<
|
||||
};
|
||||
|
||||
if (rawNode.type === 'component') {
|
||||
node.rawComponent = getComponentByName(
|
||||
rawNode.data.componentName,
|
||||
componentsRecord
|
||||
);
|
||||
node.rawComponent = getComponentByName(rawNode.data.componentName, componentsRecord);
|
||||
}
|
||||
|
||||
return node;
|
||||
@ -185,7 +181,7 @@ export const createComponent = createComponentFunction<
|
||||
node: ComponentTreeNode,
|
||||
codeRuntime: CodeRuntime,
|
||||
instance: ContainerInstance,
|
||||
componentsRecord: Record<string, ComponentType<any> | Package>
|
||||
componentsRecord: Record<string, ComponentType<any> | Package>,
|
||||
) {
|
||||
const convertedNode = createConvertedTreeNode(node, codeRuntime);
|
||||
|
||||
@ -206,7 +202,7 @@ export const createComponent = createComponentFunction<
|
||||
target: {
|
||||
text: rawValue,
|
||||
},
|
||||
valueGetter: node => codeRuntime.parseExprOrFn(node),
|
||||
valueGetter: (node) => codeRuntime.parseExprOrFn(node),
|
||||
});
|
||||
|
||||
convertedNode.setReactNode(<ReactivedText key={rawValue.value} />);
|
||||
@ -235,7 +231,7 @@ export const createComponent = createComponentFunction<
|
||||
props: AnyObject,
|
||||
codeRuntime: CodeRuntime,
|
||||
key: string,
|
||||
children: ReactNode[] = []
|
||||
children: ReactNode[] = [],
|
||||
) {
|
||||
const { ref, ...componentProps } = props;
|
||||
|
||||
@ -249,45 +245,29 @@ export const createComponent = createComponentFunction<
|
||||
// 先将 jsslot, jsFunction 对象转换
|
||||
const finalProps = processValue(
|
||||
componentProps,
|
||||
node => isJsSlot(node) || isJsFunction(node),
|
||||
(node) => isJsSlot(node) || isJsFunction(node),
|
||||
(node: JSSlot | JSFunction) => {
|
||||
if (isJsSlot(node)) {
|
||||
if (node.value) {
|
||||
const nodes = (
|
||||
Array.isArray(node.value) ? node.value : [node.value]
|
||||
).map(n => createNode(n, undefined));
|
||||
const nodes = (Array.isArray(node.value) ? node.value : [node.value]).map(
|
||||
(n) => createNode(n, undefined),
|
||||
);
|
||||
|
||||
if (node.params?.length) {
|
||||
return (...args: any[]) => {
|
||||
const params = node.params!.reduce(
|
||||
(prev, cur, idx) => {
|
||||
return (prev[cur] = args[idx]);
|
||||
},
|
||||
{} as AnyObject
|
||||
);
|
||||
const subCodeScope = codeRuntime
|
||||
.getScope()
|
||||
.createSubScope(params);
|
||||
const subCodeRuntime =
|
||||
createCodeRuntime(subCodeScope);
|
||||
const params = node.params!.reduce((prev, cur, idx) => {
|
||||
return (prev[cur] = args[idx]);
|
||||
}, {} as AnyObject);
|
||||
const subCodeScope = codeRuntime.getScope().createSubScope(params);
|
||||
const subCodeRuntime = createCodeRuntime(subCodeScope);
|
||||
|
||||
return nodes.map(n =>
|
||||
createReactElement(
|
||||
n,
|
||||
subCodeRuntime,
|
||||
instance,
|
||||
componentsRecord
|
||||
)
|
||||
return nodes.map((n) =>
|
||||
createReactElement(n, subCodeRuntime, instance, componentsRecord),
|
||||
);
|
||||
};
|
||||
} else {
|
||||
return nodes.map(n =>
|
||||
createReactElement(
|
||||
n,
|
||||
codeRuntime,
|
||||
instance,
|
||||
componentsRecord
|
||||
)
|
||||
return nodes.map((n) =>
|
||||
createReactElement(n, codeRuntime, instance, componentsRecord),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -296,7 +276,7 @@ export const createComponent = createComponentFunction<
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
if (someValue(finalProps, isJsExpression)) {
|
||||
@ -308,14 +288,14 @@ export const createComponent = createComponentFunction<
|
||||
key,
|
||||
ref: refFunction,
|
||||
},
|
||||
children
|
||||
children,
|
||||
);
|
||||
}
|
||||
Props.displayName = 'Props';
|
||||
|
||||
const Reactived = reactive(Props, {
|
||||
target: finalProps,
|
||||
valueGetter: node => codeRuntime.parseExprOrFn(node),
|
||||
valueGetter: (node) => codeRuntime.parseExprOrFn(node),
|
||||
});
|
||||
|
||||
return <Reactived key={key} />;
|
||||
@ -327,7 +307,7 @@ export const createComponent = createComponentFunction<
|
||||
key,
|
||||
ref: refFunction,
|
||||
},
|
||||
children
|
||||
children,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -339,9 +319,9 @@ export const createComponent = createComponentFunction<
|
||||
nodeProps,
|
||||
codeRuntime,
|
||||
currentComponentKey,
|
||||
rawNode.children?.map(n =>
|
||||
createReactElement(n, codeRuntime, instance, componentsRecord)
|
||||
)
|
||||
rawNode.children?.map((n) =>
|
||||
createReactElement(n, codeRuntime, instance, componentsRecord),
|
||||
),
|
||||
);
|
||||
|
||||
if (loop) {
|
||||
@ -360,14 +340,9 @@ export const createComponent = createComponentFunction<
|
||||
nodeProps,
|
||||
subCodeRuntime,
|
||||
`loop-${currentComponentKey}-${idx}`,
|
||||
rawNode.children?.map(n =>
|
||||
createReactElement(
|
||||
n,
|
||||
subCodeRuntime,
|
||||
instance,
|
||||
componentsRecord
|
||||
)
|
||||
)
|
||||
rawNode.children?.map((n) =>
|
||||
createReactElement(n, subCodeRuntime, instance, componentsRecord),
|
||||
),
|
||||
);
|
||||
});
|
||||
};
|
||||
@ -386,7 +361,7 @@ export const createComponent = createComponentFunction<
|
||||
target: {
|
||||
loop,
|
||||
},
|
||||
valueGetter: expr => codeRuntime.parseExprOrFn(expr),
|
||||
valueGetter: (expr) => codeRuntime.parseExprOrFn(expr),
|
||||
});
|
||||
|
||||
element = createElement(ReactivedLoop, {
|
||||
@ -410,7 +385,7 @@ export const createComponent = createComponentFunction<
|
||||
target: {
|
||||
condition,
|
||||
},
|
||||
valueGetter: expr => codeRuntime.parseExprOrFn(expr),
|
||||
valueGetter: (expr) => codeRuntime.parseExprOrFn(expr),
|
||||
});
|
||||
|
||||
return createElement(ReactivedCondition, {
|
||||
@ -434,7 +409,7 @@ export const createComponent = createComponentFunction<
|
||||
|
||||
const LowCodeComponent = forwardRef(function (
|
||||
props: LowCodeComponentProps,
|
||||
ref: ForwardedRef<any>
|
||||
ref: ForwardedRef<any>,
|
||||
) {
|
||||
const { id, className, style, ...extraProps } = props;
|
||||
const isMounted = useRef(false);
|
||||
@ -451,7 +426,7 @@ export const createComponent = createComponentFunction<
|
||||
scopeValue.reloadDataSource();
|
||||
|
||||
if (instance.cssText) {
|
||||
appendExternalStyle(instance.cssText).then(el => {
|
||||
appendExternalStyle(instance.cssText).then((el) => {
|
||||
styleEl = el;
|
||||
});
|
||||
}
|
||||
@ -484,9 +459,7 @@ export const createComponent = createComponentFunction<
|
||||
<div id={id} className={className} style={style} ref={ref}>
|
||||
{instance
|
||||
.getComponentTreeNodes()
|
||||
.map(n =>
|
||||
createReactElement(n, codeRuntime, instance, componentsRecord)
|
||||
)}
|
||||
.map((n) => createReactElement(n, codeRuntime, instance, componentsRecord))}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
13
runtime/renderer-react/src/context/app.ts
Normal file
13
runtime/renderer-react/src/context/app.ts
Normal 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);
|
||||
33
runtime/renderer-react/src/context/router.ts
Normal file
33
runtime/renderer-react/src/context/router.ts
Normal 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);
|
||||
@ -3,12 +3,12 @@ import { someValue } from '@alilc/runtime-core';
|
||||
import { isJsExpression } from '@alilc/runtime-shared';
|
||||
import { definePlugin } from '../../renderer';
|
||||
import { PAGE_EVENTS } from '../../events';
|
||||
import { reactive } from '../../helper/reactive';
|
||||
import { reactive } from '../../utils/reactive';
|
||||
import { createIntl } from './intl';
|
||||
|
||||
export { createIntl };
|
||||
|
||||
declare module '@alilc/runtime-core' {
|
||||
declare module '@alilc/renderer-core' {
|
||||
interface AppBoosts {
|
||||
intl: ReturnType<typeof createIntl>;
|
||||
}
|
||||
@ -24,7 +24,7 @@ export const intlPlugin = definePlugin({
|
||||
appScope.setValue(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') {
|
||||
const { key, params } = node.raw.data;
|
||||
|
||||
@ -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_NAMED_VALUE: RegExp = /^(?:\w)+/;
|
||||
@ -32,8 +32,8 @@ export function parse(format: string): Array<Token> {
|
||||
const type = RE_TOKEN_LIST_VALUE.test(sub)
|
||||
? 'list'
|
||||
: isClosed && RE_TOKEN_NAMED_VALUE.test(sub)
|
||||
? 'named'
|
||||
: 'unknown';
|
||||
? 'named'
|
||||
: 'unknown';
|
||||
tokens.push({ value: sub, type });
|
||||
} else if (char === '%') {
|
||||
// when found rails i18n syntax, skip text capture
|
||||
@ -50,18 +50,11 @@ export function parse(format: string): Array<Token> {
|
||||
return tokens;
|
||||
}
|
||||
|
||||
export function compile(
|
||||
tokens: Token[],
|
||||
values: Record<string, any> | any[] = {}
|
||||
): string[] {
|
||||
export function compile(tokens: Token[], values: Record<string, any> | any[] = {}): string[] {
|
||||
const compiled: string[] = [];
|
||||
let index: number = 0;
|
||||
|
||||
const mode: string = Array.isArray(values)
|
||||
? 'list'
|
||||
: isObject(values)
|
||||
? 'named'
|
||||
: 'unknown';
|
||||
const mode: string = Array.isArray(values) ? 'list' : isObject(values) ? 'named' : 'unknown';
|
||||
if (mode === 'unknown') {
|
||||
return compiled;
|
||||
}
|
||||
@ -81,7 +74,7 @@ export function compile(
|
||||
} else {
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
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!`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,13 +1,9 @@
|
||||
import {
|
||||
type Router,
|
||||
type RouterOptions,
|
||||
createRouter,
|
||||
} from '@alilc/runtime-router';
|
||||
import { type Router, type RouterOptions, createRouter } from '@alilc/runtime-router';
|
||||
import { createRouterProvider } from './components/router-view';
|
||||
import RouteOutlet from './components/outlet';
|
||||
import { type ReactRendererSetupContext } from './renderer';
|
||||
|
||||
declare module '@alilc/runtime-core' {
|
||||
declare module '@alilc/renderer-core' {
|
||||
interface AppBoosts {
|
||||
router: Router;
|
||||
}
|
||||
@ -21,9 +17,7 @@ const defaultRouterOptions: RouterOptions = {
|
||||
|
||||
export function initRouter(context: ReactRendererSetupContext) {
|
||||
const { schema, boosts, appScope, renderer } = context;
|
||||
const router = createRouter(
|
||||
schema.getByKey('router') ?? defaultRouterOptions
|
||||
);
|
||||
const router = createRouter(schema.getByKey('router') ?? defaultRouterOptions);
|
||||
|
||||
appScope.inject('router', router);
|
||||
boosts.add('router', router);
|
||||
@ -10,13 +10,7 @@ import {
|
||||
isReactive,
|
||||
isShallow,
|
||||
} from '@vue/reactivity';
|
||||
import {
|
||||
noop,
|
||||
isObject,
|
||||
isPlainObject,
|
||||
isSet,
|
||||
isMap,
|
||||
} from '@alilc/runtime-shared';
|
||||
import { noop, isObject, isPlainObject, isSet, isMap } from 'lodash-es';
|
||||
|
||||
export { ref as createSignal, computed, effect };
|
||||
export type { Ref as Signal, ComputedRef as ComputedSignal };
|
||||
@ -32,7 +26,7 @@ export function watch<T = any>(
|
||||
}: {
|
||||
deep?: boolean;
|
||||
immediate?: boolean;
|
||||
} = {}
|
||||
} = {},
|
||||
) {
|
||||
let getter: () => any;
|
||||
let forceTrigger = false;
|
||||
@ -42,9 +36,7 @@ export function watch<T = any>(
|
||||
forceTrigger = isShallow(source);
|
||||
} else if (isReactive(source)) {
|
||||
getter = () => {
|
||||
return deep === true
|
||||
? source
|
||||
: traverse(source, deep === false ? 1 : undefined);
|
||||
return deep === true ? source : traverse(source, deep === false ? 1 : undefined);
|
||||
};
|
||||
forceTrigger = true;
|
||||
} else {
|
||||
@ -93,12 +85,7 @@ export function watch<T = any>(
|
||||
return unwatch;
|
||||
}
|
||||
|
||||
function traverse(
|
||||
value: unknown,
|
||||
depth?: number,
|
||||
currentDepth = 0,
|
||||
seen?: Set<unknown>
|
||||
) {
|
||||
function traverse(value: unknown, depth?: number, currentDepth = 0, seen?: Set<unknown>) {
|
||||
if (!isObject(value)) {
|
||||
return value;
|
||||
}
|
||||
@ -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') {
|
||||
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(scripts.map(item => appendExternalScript(item)));
|
||||
await Promise.all(styles.map((item) => appendExternalCss(item)));
|
||||
await Promise.all(scripts.map((item) => appendExternalScript(item)));
|
||||
}
|
||||
|
||||
async function appendExternalScript(
|
||||
url: string,
|
||||
root: HTMLElement = document.body
|
||||
root: HTMLElement = document.body,
|
||||
): Promise<HTMLElement> {
|
||||
if (url) {
|
||||
const el = getIfExistAssetByUrl(url, 'script');
|
||||
@ -73,9 +75,9 @@ async function appendExternalScript(
|
||||
() => {
|
||||
resolve(scriptElement);
|
||||
},
|
||||
false
|
||||
false,
|
||||
);
|
||||
scriptElement.addEventListener('error', error => {
|
||||
scriptElement.addEventListener('error', (error) => {
|
||||
if (root.contains(scriptElement)) {
|
||||
root.removeChild(scriptElement);
|
||||
}
|
||||
@ -88,7 +90,7 @@ async function appendExternalScript(
|
||||
|
||||
async function appendExternalCss(
|
||||
url: string,
|
||||
root: HTMLElement = document.head
|
||||
root: HTMLElement = document.head,
|
||||
): Promise<HTMLElement> {
|
||||
if (url) {
|
||||
const el = getIfExistAssetByUrl(url, 'link');
|
||||
@ -105,9 +107,9 @@ async function appendExternalCss(
|
||||
() => {
|
||||
resolve(el);
|
||||
},
|
||||
false
|
||||
false,
|
||||
);
|
||||
el.addEventListener('error', error => {
|
||||
el.addEventListener('error', (error) => {
|
||||
reject(error);
|
||||
});
|
||||
|
||||
@ -117,7 +119,7 @@ async function appendExternalCss(
|
||||
|
||||
export async function appendExternalStyle(
|
||||
cssText: string,
|
||||
root: HTMLElement = document.head
|
||||
root: HTMLElement = document.head,
|
||||
): Promise<HTMLElement> {
|
||||
return new Promise((resolve, reject) => {
|
||||
let el: HTMLStyleElement = document.createElement('style');
|
||||
@ -128,9 +130,9 @@ export async function appendExternalStyle(
|
||||
() => {
|
||||
resolve(el);
|
||||
},
|
||||
false
|
||||
false,
|
||||
);
|
||||
el.addEventListener('error', error => {
|
||||
el.addEventListener('error', (error) => {
|
||||
reject(error);
|
||||
});
|
||||
|
||||
@ -140,11 +142,10 @@ export async function appendExternalStyle(
|
||||
|
||||
function getIfExistAssetByUrl(
|
||||
url: string,
|
||||
tag: 'link' | 'script'
|
||||
tag: 'link' | 'script',
|
||||
): HTMLLinkElement | HTMLScriptElement | undefined {
|
||||
return Array.from(document.getElementsByTagName(tag)).find(item => {
|
||||
const elUrl =
|
||||
(item as HTMLLinkElement).href || (item as HTMLScriptElement).src;
|
||||
return Array.from(document.getElementsByTagName(tag)).find((item) => {
|
||||
const elUrl = (item as HTMLLinkElement).href || (item as HTMLScriptElement).src;
|
||||
|
||||
if (/^(https?:)?\/\/([\w.]+\/?)\S*/gi.test(url)) {
|
||||
// if url === http://xxx.xxx
|
||||
8
runtime/renderer-react/tsconfig.json
Normal file
8
runtime/renderer-react/tsconfig.json
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"paths": {
|
||||
"@alilc/*": ["runtime/*/src"]
|
||||
}
|
||||
}
|
||||
}
|
||||
8
runtime/renderer-react/vitest.config.ts
Normal file
8
runtime/renderer-react/vitest.config.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import { defineConfig } from 'vitest/config'
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
include: ['tests/*.spec.ts'],
|
||||
environment: 'jsdom'
|
||||
}
|
||||
})
|
||||
@ -4,5 +4,13 @@
|
||||
"description": "",
|
||||
"type": "module",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,3 +1,6 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json"
|
||||
}
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "dist"
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user