feat: add renderer-core codes

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

@ -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;
}

View File

@ -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';

View File

@ -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
View File

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

View File

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

View File

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

View File

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

View File

@ -5,6 +5,12 @@
"type": "module",
"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"

View File

@ -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,
);
};
}

View File

@ -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;

View File

@ -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];
}
},
};

View File

@ -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;

View File

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

View File

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

View File

@ -1,13 +1,6 @@
import {
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);
}
},

View File

@ -1,4 +1,5 @@
import { type AppContext } from '../api/create-app-function';
import { type AppContext } from './api/app';
import { nonSetterProxy } from './utils/non-setter-proxy';
export interface Plugin<C extends PluginSetupContext = PluginSetupContext> {
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;
}

View File

@ -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);
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,11 +1,14 @@
import { appBoosts } from './boosts';
import { appBoosts } from '../boosts';
export type ErrorType = string;
export 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);
}
}

View File

@ -1,11 +1,43 @@
import { useCallbacks, type Callback } from '@alilc/runtime-shared';
import type { AnyFunction } from '../types';
export type EventName = string | number | symbol;
export function useEvent<T = AnyFunction>() {
let events: T[] = [];
function add(fn: T) {
events.push(fn);
return () => {
events = events.filter((e) => e !== fn);
};
}
function remove(fn: T) {
events = events.filter((f) => fn !== f);
}
function list() {
return [...events];
}
return {
add,
remove,
list,
clear() {
events.length = 0;
},
};
}
export type Event<F = AnyFunction> = ReturnType<typeof useEvent<F>>;
export type HookCallback = (...args: any) => Promise<any> | any;
export type HookCallback = (...args: any) => Promise<void> | void;
type HookKeys<T> = keyof T & PropertyKey;
type 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,
};
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -11,12 +11,12 @@ import {
type CodeRuntime,
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>
);
});

View File

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

View File

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

View File

@ -3,12 +3,12 @@ import { someValue } from '@alilc/runtime-core';
import { isJsExpression } from '@alilc/runtime-shared';
import { 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;

View File

@ -1,4 +1,4 @@
import { isObject } from '@alilc/runtime-shared';
import { isObject } from 'lodash-es';
const RE_TOKEN_LIST_VALUE: RegExp = /^(?:\d)+/;
const RE_TOKEN_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!`,
);
}
}

View File

@ -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);

View File

@ -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;
}

View File

@ -1,4 +1,6 @@
import { addLeadingSlash } from '@alilc/runtime-shared';
const addLeadingSlash = (path: string): string => {
return path.charAt(0) === '/' ? path : `/${path}`;
};
export function getElementById(id: string, tag: string = 'div') {
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

View File

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

View File

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

View File

@ -4,5 +4,13 @@
"description": "",
"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"
}
}

View File

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