feat: add router

This commit is contained in:
1ncounter 2024-03-21 16:00:24 +08:00
parent fb5de6441d
commit 03f7c76284
51 changed files with 1142 additions and 1425 deletions

View File

@ -1513,7 +1513,6 @@ webpack.config.js # 项目工程配置,包含插件配置及自定义 webpack
| -------------- | ---------------------------------- | ------ | ------ | ------ | ---------------------------------------------- |
| path | 当前解析后的路径 | String | - | - | 必填 |
| hash | 当前路径的 hash 值,以 # 开头 | String | - | - | 必填 |
| href | 当前的全部路径 | String | - | - | 必填 |
| params | 匹配到的路径参数 | Object | - | - | 必填 |
| query | 当前的路径 query 对象 | Object | - | - | 必填,代表当前地址的 search 属性的对象 |
| name | 匹配到的路由记录名 | String | - | - | 选填 |

View File

@ -6,8 +6,10 @@
"bugs": "https://github.com/alibaba/lowcode-engine/issues",
"homepage": "https://github.com/alibaba/lowcode-engine/#readme",
"license": "MIT",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"scripts": {
"build": "",
"build": "tsc",
"test": "vitest --run",
"test:watch": "vitest"
},

View File

@ -22,7 +22,7 @@ export interface AppBase {
*/
export interface AppContext {
schema: AppSchema;
config: Map<string, any>;
config: PlainObject;
appScope: CodeScope;
packageManager: PackageManager;
boosts: AppBoostsManager;
@ -35,14 +35,14 @@ type AppCreator<O, T extends AppBase> = (
export type App<T extends AppBase = AppBase> = {
schema: Project;
config: Map<string, any>;
config: PlainObject;
readonly boosts: AppBoosts;
use(plugin: Plugin): Promise<void>;
} & T;
/**
*
* createApp
* @param schema
* @param options
* @returns
@ -55,9 +55,9 @@ export function createAppFunction<O extends AppOptionsBase, T extends AppBase =
}
return async (options) => {
const { schema, appScopeValue = {} } = options;
const { schema, appScopeValue } = options;
const appSchema = createAppSchema(schema);
const appConfig = new Map<string, any>();
const appConfig = {};
const packageManager = createPackageManager();
const appScope = createScope({
...appScopeValue,

View File

@ -1,136 +1,42 @@
import { CreateContainerOptions, createContainer } from '../container';
import { createCodeRuntime, createScope } from '../code-runtime';
import { throwRuntimeError } from '../utils/error';
import { validateContainerSchema } from '../validator/schema';
import { type CreateContainerOptions, createContainer, type Container } from '../container';
import type { PlainObject, InstanceStateApi } from '../types';
export interface ComponentOptionsBase<C> {
componentsTree: RootSchema;
componentsRecord: Record<string, C | Package>;
dataSourceCreator: DataSourceCreator;
}
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;
export type CreateComponentBaseOptions<T extends string> = Omit<
CreateContainerOptions<T>,
'stateCreator'
>;
/**
* createComponent
* createComponent = createComponentFunction(() => component)
*/
export function createComponentFunction<
ComponentT,
InstanceT,
LifeCycleNameT extends string,
O extends CreateComponentBaseOptions<LifeCycleNameT>,
>(
stateCreator: (initState: PlainObject) => InstanceStateApi,
componentCreator: (
container: Container<InstanceT, LifeCycleNameT>,
componentOptions: O,
) => ComponentT,
): (componentOptions: O) => ComponentT {
return (componentOptions) => {
const finalOptions = Object.assign({}, defaultOptions, componentOptions);
const { supCodeScope, initScopeValue = {}, dataSourceCreator } = finalOptions;
const {
supCodeScope,
initScopeValue = {},
dataSourceCreator,
componentsTree,
} = componentOptions;
const codeRuntimeScope =
supCodeScope?.createSubScope(initScopeValue) ?? createScope(initScopeValue);
const codeRuntime = createCodeRuntime(codeRuntimeScope);
const container: Container = {
get codeScope() {
return codeRuntimeScope;
},
get codeRuntime() {
return codeRuntime;
},
createInstance(componentsTree, extraProps = {}) {
if (!validateContainerSchema(componentsTree)) {
throwRuntimeError('createComponent', 'componentsTree is not valid!');
}
const mapRefToComponentInstance: Map<string, C> = new Map();
const initialState = codeRuntime.parseExprOrFn(componentsTree.state ?? {});
const stateContext = stateCreator(initialState);
codeRuntimeScope.setValue(
Object.assign(
{
props: codeRuntime.parseExprOrFn({
...componentsTree.defaultProps,
...componentsTree.props,
...extraProps,
}),
$(ref: string) {
return mapRefToComponentInstance.get(ref);
},
},
stateContext,
dataSourceCreator
? dataSourceCreator(componentsTree.dataSource ?? ({ list: [] } as any), stateContext)
: {},
) as ContainerInstanceScope<C>,
true,
);
if (componentsTree.methods) {
for (const [key, fn] of Object.entries(componentsTree.methods)) {
const customMethod = codeRuntime.createFnBoundScope(fn.value);
if (customMethod) {
codeRuntimeScope.inject(key, customMethod);
}
}
}
triggerLifeCycle('constructor');
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(codeRuntime.getScope().value, args);
}
}
}
const instance: ContainerInstance<C> = {
get id() {
return componentsTree.id;
},
get cssText() {
return componentsTree.css;
},
get codeScope() {
return codeRuntimeScope;
},
triggerLifeCycle,
setRefInstance(ref, instance) {
mapRefToComponentInstance.set(ref, instance);
},
removeRefInstance(ref) {
mapRefToComponentInstance.delete(ref);
},
getComponentTreeNodes() {
const childNodes = componentsTree.children
? Array.isArray(componentsTree.children)
? componentsTree.children
: [componentsTree.children]
: [];
const treeNodes = childNodes.map((item) => {
return createComponentTreeNode(item, undefined);
});
return treeNodes;
},
destory() {
mapRefToComponentInstance.clear();
codeRuntimeScope.setValue({});
},
};
return instance;
},
};
const container = createContainer<InstanceT, LifeCycleNameT>({
supCodeScope,
initScopeValue,
stateCreator,
dataSourceCreator,
componentsTree,
});
return componentCreator(container, componentOptions);
};

View File

@ -76,17 +76,20 @@ export function createCodeRuntime(scopeOrValue: PlainObject = {}): CodeRuntime {
};
}
export interface CodeScope {
readonly value: PlainObject;
export interface CodeScope<T extends PlainObject = PlainObject, K extends keyof T = keyof T> {
readonly value: T;
inject(name: string, value: any, force?: boolean): void;
setValue(value: PlainObject, replace?: boolean): void;
createSubScope(initValue?: PlainObject): CodeScope;
inject(name: K, value: T[K], force?: boolean): void;
setValue(value: T, replace?: boolean): void;
createSubScope<O extends PlainObject = PlainObject>(initValue: O): CodeScope<T & O>;
}
export function createScope(initValue: PlainObject = {}): CodeScope {
export function createScope<T extends PlainObject = PlainObject, K extends keyof T = keyof T>(
initValue: T,
): CodeScope<T, K> {
const innerScope = { value: initValue };
const proxyValue = new Proxy(Object.create(null), {
const proxyValue: T = new Proxy(Object.create(null), {
set(target, p, newValue, receiver) {
return Reflect.set(target, p, newValue, receiver);
},
@ -104,29 +107,17 @@ export function createScope(initValue: PlainObject = {}): CodeScope {
},
});
function inject(name: string, value: any, force = false): void {
if (innerScope.value[name] && !force) {
console.warn(`${name} 已存在值`);
return;
}
innerScope.value[name] = value;
}
function createSubScope(initValue: PlainObject = {}) {
const childScope = createScope(initValue);
(childScope as any).__raw.__parent = innerScope;
return childScope;
}
const scope: CodeScope = {
const scope: CodeScope<T, K> = {
get value() {
// dev return value
return proxyValue;
},
inject,
inject(name, value, force = false): void {
if (innerScope.value[name] && !force) {
return;
}
innerScope.value[name] = value;
},
setValue(value, replace = false) {
if (replace) {
innerScope.value = { ...value };
@ -134,7 +125,13 @@ export function createScope(initValue: PlainObject = {}): CodeScope {
innerScope.value = Object.assign({}, innerScope.value, value);
}
},
createSubScope,
createSubScope<O extends PlainObject = PlainObject>(initValue: O) {
const childScope = createScope<O & T>(initValue);
(childScope as any).__raw.__parent = innerScope;
return childScope;
},
};
Object.defineProperty(scope, Symbol.for(SYMBOL_SIGN), { get: () => true });

View File

@ -4,6 +4,7 @@ import type {
ComponentTree,
InstanceDataSourceApi,
InstanceStateApi,
NodeType,
} from './types';
import { type CodeScope, type CodeRuntime, createCodeRuntime, createScope } from './code-runtime';
import { isJSFunction } from './utils/type-guard';
@ -48,7 +49,13 @@ export interface CreateContainerOptions<LifeCycleNameT extends string> {
export function createContainer<InstanceT, LifeCycleNameT extends string>(
options: CreateContainerOptions<LifeCycleNameT>,
): Container<InstanceT, LifeCycleNameT> {
const { componentsTree, supCodeScope, initScopeValue, stateCreator, dataSourceCreator } = options;
const {
componentsTree,
supCodeScope,
initScopeValue = {},
stateCreator,
dataSourceCreator,
} = options;
validContainerSchema(componentsTree);
@ -151,7 +158,6 @@ export function createContainer<InstanceT, LifeCycleNameT extends string>(
createWidgets<Element>() {
if (!componentsTree.children) return [];
return componentsTree.children.map((item) => createWidget<Element>(item));
},
};

View File

@ -5,11 +5,14 @@ export { createCodeRuntime, createScope } from './code-runtime';
export { definePlugin } from './plugin';
export { createWidget } from './widget';
export { createContainer } from './container';
export { createHookStore, useEvent } from './utils/hook';
export * from './utils/type-guard';
export * from './utils/value';
export * from './widget';
/* --------------- 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';
export type { Container, CreateContainerOptions } from './container';

View File

@ -24,9 +24,11 @@ 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 | LowCodeComponent>;
/** 通过组件名获取对应的组件 */
getComponent<C = unknown>(componentName: string): C | undefined;
getComponent<C = unknown>(componentName: string): C | LowCodeComponent | undefined;
/** 注册组件 */
registerComponentByName(componentName: string, Component: unknown): void;
}

View File

@ -11,9 +11,9 @@ export interface AppSchema {
addComponentsMap(componentName: ComponentMap): void;
removeComponentsMap(componentName: string): void;
getPages(): PageConfig[];
addPage(page: PageConfig): void;
removePage(id: string): void;
getPageConfigs(): PageConfig[];
addPageConfig(page: PageConfig): void;
removePageConfig(id: string): void;
getByKey<K extends keyof Project>(key: K): Project[K] | undefined;
updateByKey<K extends keyof Project>(
@ -55,14 +55,14 @@ export function createAppSchema(schema: Project): AppSchema {
removeArrayItem(schemaRef.componentsMap, 'componentName', componentName);
},
getPages() {
getPageConfigs() {
return schemaRef.pages ?? [];
},
addPage(page) {
addPageConfig(page) {
schemaRef.pages ??= [];
addArrayItem(schemaRef.pages, page, 'id');
},
removePage(id) {
removePageConfig(id) {
schemaRef.pages ??= [];
removeArrayItem(schemaRef.pages, 'id', id);
},
@ -72,8 +72,7 @@ export function createAppSchema(schema: Project): AppSchema {
},
updateByKey(key, updater) {
const value = schemaRef[key];
schemaRef[key] = typeof updater === 'function' ? updater(value) : updater;
schemaRef[key] = typeof updater === 'function' ? (updater as any)(value) : updater;
},
find(predicate) {

View File

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

View File

@ -189,20 +189,28 @@ export interface ComponentTreeNodeProps {
/** 组件内联样式 */
style?: JSONObject | JSExpression;
/** 组件 ref 名称 */
ref?: string | JSExpression;
ref?: string;
[key: string]: any;
}
export interface NPMUtil {
name: string;
type: 'npm';
content: Omit<ComponentMap, 'componentName'>;
}
export interface FunctionUtil {
name: string;
type: 'function';
content: JSFunction;
}
/**
* 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;
}
export type Util = NPMUtil | FunctionUtil;
/**
* 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
@ -307,7 +315,7 @@ export interface JSONObject {
*/
export interface JSSlot {
type: 'JSSlot';
value: 1;
value: ComponentTreeNode | ComponentTreeNode[];
params?: string[];
}

View File

@ -50,10 +50,16 @@ export interface InstanceDataSourceApi {
reloadDataSource: () => void;
}
/**
*
*/
export interface UtilsApi {
utils: Record<string, AnyFunction>;
}
/**
* API
*/
export interface IntlApi {
/**
*
@ -72,4 +78,106 @@ export interface IntlApi {
setLocale(locale: string): void;
}
export interface RouterApi {}
/**
* Router API HistoryLocation api
* HTML 5 History api SPA SPA
*/
export interface RouterApi {
/**
*
*/
getCurrentLocation(): RouteLocation;
/**
* `Route`
*/
push(location: RawRouteLocation): void | Promise<void>;
/**
* `push`
*/
replace(location: RawRouteLocation): void | Promise<void>;
/**
* `history.back`
*/
back(): void;
/**
* `history.forward`
*/
forward(): void;
/**
* `history.go`
* @param delta
*/
go(delta: number): void;
/**
*
*/
beforeRouteLeave(
guard: (to: RouteLocation, from: RouteLocation) => boolean | Promise<boolean>,
): () => void;
/**
*
*/
afterRouteChange(guard: (to: RouteLocation, from: RouteLocation) => any): () => void;
}
export interface RawLocationAsPath {
path: string;
}
export interface RawLocationAsRelative {
params?: Record<string, string | string[]>;
}
export interface RawLocationAsName {
name: string;
params?: Record<string, string | string[]>;
}
export type RawLocation = RawLocationAsPath | RawLocationAsRelative | RawLocationAsName;
export interface RawLocationOptions {
searchParams?: URLSearchParams;
hash?: string;
state?: History['state'];
}
/**
*
*/
export type RawRouteLocation = string | (RawLocation & RawLocationOptions);
/**
* window.location
*/
export interface RouteLocation {
/**
*
*/
name: string | undefined;
/**
*
*/
path: string;
/**
* hash #
*/
hash: string;
/**
*
*/
params: Record<string, string | string[]> | undefined;
/**
* URLSearchParams
*/
searchParams: URLSearchParams | undefined;
/**
* search hash
*/
fullPath: string;
/**
*
*/
meta: PlainObject | undefined;
/**
*
*/
redirectedFrom: RouteLocation | undefined;
}

View File

@ -0,0 +1,8 @@
let idStart = 0x0907;
/**
* Generate unique id
*/
export function guid(): number {
return idStart++;
}

View File

@ -1,4 +1,4 @@
import type { JSExpression, JSFunction, I18nNode } from '../types';
import type { JSExpression, JSFunction, JSSlot, I18nNode, LowCodeComponent } from '../types';
import { isPlainObject } from 'lodash-es';
export function isJSExpression(v: unknown): v is JSExpression {
@ -13,6 +13,14 @@ export function isJSFunction(v: unknown): v is JSFunction {
);
}
export function isJSSlot(v: unknown): v is JSSlot {
return isPlainObject(v) && (v as any).type === 'JSSlot' && (v as any).value;
}
export function isI18nNode(v: unknown): v is I18nNode {
return isPlainObject(v) && (v as any).type === 'i18n' && typeof (v as any).key === 'string';
}
export function isLowCodeComponentSchema(v: unknown): v is LowCodeComponent {
return isPlainObject(v) && (v as any).type === 'lowCode' && (v as any).schema;
}

View File

@ -14,14 +14,14 @@ export function someValue(obj: any, predicate: (data: any) => boolean) {
export function processValue(
obj: any,
predicate: (obj: any) => boolean,
processor: (node: any, paths: Array<string | number>) => any
processor: (node: any, paths: Array<string | number>) => any,
): any {
const innerProcess = (target: any, paths: Array<string | number>): any => {
if (Array.isArray(target)) {
return target.map((item, idx) => innerProcess(item, [...paths, idx]));
}
if (!isPlainObject(target) || isEmptyObject(target)) return target;
if (!isPlainObject(target) || isEmpty(target)) return target;
if (!someValue(target, predicate)) return target;
if (predicate(target)) {

View File

@ -1,35 +1,32 @@
import type { NodeType, ComponentTreeNode, ComponentTreeNodeProps } from './types';
import { isJSExpression, isI18nNode } from './utils/type-guard';
import { guid } from './utils/guid';
export class Widget<Data, Element> {
protected _raw: Data;
protected proxyElements: Element[] = [];
protected renderObject: Element | undefined;
constructor(data: Data) {
this._raw = data;
constructor(public raw: Data) {
this.init();
}
protected init() {}
get raw() {
return this._raw;
get key(): string {
return (this.raw as any)?.id ?? `${guid()}`;
}
setRenderObject(el: Element) {
this.renderObject = el;
}
getRenderObject() {
return this.renderObject;
mapRenderObject(mapper: (widget: Widget<Data, Element>) => Element | undefined) {
this.renderObject = mapper(this);
return this;
}
addProxyELements(el: Element) {
this.proxyElements.push(el);
this.proxyElements.unshift(el);
}
build(builder: (elements: Element[]) => Element) {
return builder(this.proxyElements);
build<C>(builder: (elements: Element[]) => C): C {
return builder(this.renderObject ? [...this.proxyElements, this.renderObject] : []);
}
}
@ -53,11 +50,11 @@ export class ComponentWidget<E = unknown> extends Widget<ComponentTreeNode, E> {
private _propsValue: ComponentTreeNodeProps = {};
protected init() {
if (this._raw.props) {
this._propsValue = this._raw.props;
if (this.raw.props) {
this._propsValue = this.raw.props;
}
if (this._raw.children) {
this._children = this._raw.children.map((child) => createWidget<E>(child));
if (this.raw.children) {
this._children = this.raw.children.map((child) => createWidget<E>(child));
}
}
@ -65,20 +62,20 @@ export class ComponentWidget<E = unknown> extends Widget<ComponentTreeNode, E> {
return this.raw.componentName;
}
get props() {
return this._propsValue;
return this._propsValue ?? {};
}
get condition() {
return this.raw.condition !== false;
}
get loop() {
return this.raw.loop;
}
get loopArgs() {
return this.raw.loopArgs ?? ['item', 'index'];
}
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) {

View File

@ -1,4 +1,7 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "./dist"
},
"include": ["src"]
}
}

View File

@ -6,8 +6,10 @@
"bugs": "https://github.com/alibaba/lowcode-engine/issues",
"homepage": "https://github.com/alibaba/lowcode-engine/#readme",
"license": "MIT",
"module": "dist/index.js",
"types": "dist/index.d.ts",
"scripts": {
"build": "",
"build": "tsc",
"test": "vitest"
},
"dependencies": {
@ -20,8 +22,11 @@
"react-dom": "^18.2.0"
},
"devDependencies": {
"@alilc/runtime-router": "1.0.0-beta.0",
"@testing-library/react": "^14.2.0",
"@types/lodash-es": "^4.17.12",
"@types/hoist-non-react-statics": "^3.3.5",
"@types/use-sync-external-store": "^0.0.6",
"@types/react": "^18.2.67",
"@types/react-dom": "^18.2.22",
"jsdom": "^24.0.0"

View File

@ -1,34 +1,47 @@
import {
type App,
type RenderBase,
type AppBase,
createAppFunction,
type AppOptionsBase,
} from '@alilc/renderer-core';
import { type ComponentType } from 'react';
import { type Root, createRoot } from 'react-dom/client';
import { createRouter } from '@alilc/runtime-router';
import { createRenderer } from '../renderer';
import AppComponent from '../components/app';
import { intlPlugin } from '../plugins/intl';
import { globalUtilsPlugin } from '../plugins/utils';
import { initRouter } from '../router';
import { createIntl } from '../runtime-api/intl';
import { createRuntimeUtils } from '../runtime-api/utils';
export interface AppOptions extends AppOptionsBase {
dataSourceCreator: DataSourceCreator;
dataSourceCreator: any;
faultComponent?: ComponentType<any>;
}
export interface ReactRender extends RenderBase {}
export interface ReactRender extends AppBase {}
export type ReactApp = App<ReactRender>;
export const createApp = createAppFunction<AppOptions, ReactRender>(async (context, options) => {
const renderer = createRenderer();
const appContext = { ...context, renderer };
const { schema, packageManager, appScope, boosts } = context;
initRouter(appContext);
// router
// todo: transform config
const router = createRouter(schema.getByKey('router') as any);
options.plugins ??= [];
options.plugins!.unshift(globalUtilsPlugin, intlPlugin);
appScope.inject('router', router);
// i18n
const i18nMessages = schema.getByKey('i18n') ?? {};
const defaultLocale = schema.getByPath('config.defaultLocale') ?? 'zh-CN';
const intl = createIntl(i18nMessages, defaultLocale);
appScope.inject('intl', intl);
// utils
const runtimeUtils = createRuntimeUtils(schema.getByKey('utils') ?? [], packageManager);
appScope.inject('utils', runtimeUtils.utils);
boosts.add('runtimeUtils', runtimeUtils);
// set config
if (options.faultComponent) {
@ -37,6 +50,8 @@ export const createApp = createAppFunction<AppOptions, ReactRender>(async (conte
context.config.set('dataSourceCreator', options.dataSourceCreator);
let root: Root | undefined;
const renderer = createRenderer();
const appContext = { ...context, renderer };
const reactRender: ReactRender = {
async mount(el) {
@ -56,7 +71,7 @@ export const createApp = createAppFunction<AppOptions, ReactRender>(async (conte
};
return {
renderBase: reactRender,
appBase: reactRender,
renderer,
};
});

View File

@ -0,0 +1,5 @@
import { createComponent as internalCreate, ComponentOptions } from '../component';
export function createComponent(options: ComponentOptions) {
return internalCreate(options);
}

View File

@ -1,471 +0,0 @@
import {
type StateContext,
type ComponentOptionsBase,
createComponentFunction,
type ComponentTreeNode,
type ComponentNode,
someValue,
type ContainerInstance,
processValue,
createNode,
type CodeRuntime,
createCodeRuntime,
} from '@alilc/runtime-core';
import { isPlainObject } from 'lodash-es';
import {
type AnyObject,
type Package,
type JSSlot,
type JSFunction,
isJsExpression,
isJsSlot,
isLowCodeComponentPackage,
isJsFunction,
type JSExpression,
} from '@alilc/runtime-shared';
import {
useEffect,
type ComponentType,
type ReactNode,
type ElementType,
forwardRef,
ForwardedRef,
useMemo,
createElement,
type CSSProperties,
useRef,
} from 'react';
import { createSignal, watch } from '../signals';
import { appendExternalStyle } from '../helper/element';
import { reactive } from '../helper/reactive';
function reactiveStateCreator(initState: AnyObject): StateContext {
const proxyState = createSignal(initState);
return {
get state() {
return proxyState.value;
},
setState(newState) {
if (!isPlainObject(newState)) {
throw Error('newState mush be a object');
}
for (const key of Object.keys(newState)) {
proxyState.value[key] = newState[key];
}
},
};
}
/**
* reactNode
*
*/
export interface ConvertedTreeNode {
type: ComponentTreeNode['type'];
/** 节点对应的值 */
raw: ComponentTreeNode;
/** 转换时所在的上下文 */
context: {
codeRuntime: CodeRuntime;
[key: string]: any;
};
/** 用于渲染的组件,只存在于树节点为组件节点的情况 */
rawComponent?: ComponentType<any> | undefined;
/** 获取节点对应的 reactNode被用于渲染 */
getReactNode(): ReactNode;
/** 设置节点对应的 reactNode */
setReactNode(element: ReactNode): void;
}
export interface CreateComponentOptions<C = ComponentType<any>> extends ComponentOptionsBase<C> {
displayName?: string;
beforeNodeCreateComponent?(convertedNode: ConvertedTreeNode): void;
nodeCreatedComponent?(convertedNode: ConvertedTreeNode): void;
nodeComponentRefAttached?(node: ComponentNode, instance: ElementType): void;
componentDidMount?(): void;
componentWillUnmount?(): void;
}
export interface LowCodeComponentProps {
id?: string;
/** CSS 类名 */
className?: string;
/** style */
style?: CSSProperties;
[key: string]: any;
}
export const createComponent = createComponentFunction<
ComponentType<LowCodeComponentProps>,
CreateComponentOptions
>({
stateCreator: reactiveStateCreator,
componentCreator: ({ codeRuntime, createInstance }, componentOptions) => {
const {
displayName = '__LowCodeComponent__',
componentsTree,
componentsRecord,
beforeNodeCreateComponent,
nodeCreatedComponent,
nodeComponentRefAttached,
componentDidMount,
componentWillUnmount,
...extraOptions
} = componentOptions;
const lowCodeComponentCache = new Map<string, ComponentType<any>>();
function getComponentByName(
componentName: string,
componentsRecord: Record<string, ComponentType<any> | Package>,
) {
const Component = componentsRecord[componentName];
if (!Component) {
return undefined;
}
if (isLowCodeComponentPackage(Component)) {
if (lowCodeComponentCache.has(componentName)) {
return lowCodeComponentCache.get(componentName);
}
const componentsTree = Component.schema as any;
const LowCodeComponent = createComponent({
...extraOptions,
displayName: componentsTree.componentName,
componentsRecord,
componentsTree,
});
lowCodeComponentCache.set(componentName, LowCodeComponent);
return LowCodeComponent;
}
return Component;
}
function createConvertedTreeNode(
rawNode: ComponentTreeNode,
codeRuntime: CodeRuntime,
): ConvertedTreeNode {
let elementValue: ReactNode = null;
const node: ConvertedTreeNode = {
type: rawNode.type,
raw: rawNode,
context: { codeRuntime },
getReactNode() {
return elementValue;
},
setReactNode(element) {
elementValue = element;
},
};
if (rawNode.type === 'component') {
node.rawComponent = getComponentByName(rawNode.data.componentName, componentsRecord);
}
return node;
}
function createReactElement(
node: ComponentTreeNode,
codeRuntime: CodeRuntime,
instance: ContainerInstance,
componentsRecord: Record<string, ComponentType<any> | Package>,
) {
const convertedNode = createConvertedTreeNode(node, codeRuntime);
beforeNodeCreateComponent?.(convertedNode);
if (!convertedNode.getReactNode()) {
if (convertedNode.type === 'string') {
convertedNode.setReactNode(convertedNode.raw.data as string);
} else if (convertedNode.type === 'expression') {
const rawValue = convertedNode.raw.data as JSExpression;
function Text(props: any) {
return props.text;
}
Text.displayName = 'Text';
const ReactivedText = reactive(Text, {
target: {
text: rawValue,
},
valueGetter: (node) => codeRuntime.parseExprOrFn(node),
});
convertedNode.setReactNode(<ReactivedText key={rawValue.value} />);
} else if (convertedNode.type === 'component') {
const createReactElementByNode = () => {
const Component = convertedNode.rawComponent;
if (!Component) return null;
const rawNode = convertedNode.raw as ComponentNode;
const {
id,
componentName,
condition = true,
loop,
loopArgs = ['item', 'index'],
props: nodeProps = {},
} = rawNode.data;
// condition为 Falsy 的情况下 不渲染
if (!condition) return null;
// loop 为数组且为空的情况下 不渲染
if (Array.isArray(loop) && loop.length === 0) return null;
function createElementWithProps(
Component: ComponentType<any>,
props: AnyObject,
codeRuntime: CodeRuntime,
key: string,
children: ReactNode[] = [],
) {
const { ref, ...componentProps } = props;
const refFunction = (ins: any) => {
if (ins) {
if (ref) instance.setRefInstance(ref as string, ins);
nodeComponentRefAttached?.(rawNode, ins);
}
};
// 先将 jsslot, jsFunction 对象转换
const finalProps = processValue(
componentProps,
(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),
);
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);
return nodes.map((n) =>
createReactElement(n, subCodeRuntime, instance, componentsRecord),
);
};
} else {
return nodes.map((n) =>
createReactElement(n, codeRuntime, instance, componentsRecord),
);
}
}
} else if (isJsFunction(node)) {
return codeRuntime.parseExprOrFn(node);
}
return null;
},
);
if (someValue(finalProps, isJsExpression)) {
function Props(props: any) {
return createElement(
Component,
{
...props,
key,
ref: refFunction,
},
children,
);
}
Props.displayName = 'Props';
const Reactived = reactive(Props, {
target: finalProps,
valueGetter: (node) => codeRuntime.parseExprOrFn(node),
});
return <Reactived key={key} />;
} else {
return createElement(
Component,
{
...finalProps,
key,
ref: refFunction,
},
children,
);
}
}
const currentComponentKey = id || componentName;
let element: any = createElementWithProps(
Component,
nodeProps,
codeRuntime,
currentComponentKey,
rawNode.children?.map((n) =>
createReactElement(n, codeRuntime, instance, componentsRecord),
),
);
if (loop) {
const genLoopElements = (loopData: any[]) => {
return loopData.map((item, idx) => {
const loopArgsItem = loopArgs[0] ?? 'item';
const loopArgsIndex = loopArgs[1] ?? 'index';
const subCodeScope = codeRuntime.getScope().createSubScope({
[loopArgsItem]: item,
[loopArgsIndex]: idx,
});
const subCodeRuntime = createCodeRuntime(subCodeScope);
return createElementWithProps(
Component,
nodeProps,
subCodeRuntime,
`loop-${currentComponentKey}-${idx}`,
rawNode.children?.map((n) =>
createReactElement(n, subCodeRuntime, instance, componentsRecord),
),
);
});
};
if (isJsExpression(loop)) {
function Loop(props: any) {
if (!Array.isArray(props.loop)) {
return null;
}
return genLoopElements(props.loop);
}
Loop.displayName = 'Loop';
const ReactivedLoop = reactive(Loop, {
target: {
loop,
},
valueGetter: (expr) => codeRuntime.parseExprOrFn(expr),
});
element = createElement(ReactivedLoop, {
key: currentComponentKey,
});
} else {
element = genLoopElements(loop as any[]);
}
}
if (isJsExpression(condition)) {
function Condition(props: any) {
if (props.condition) {
return element;
}
return null;
}
Condition.displayName = 'Condition';
const ReactivedCondition = reactive(Condition, {
target: {
condition,
},
valueGetter: (expr) => codeRuntime.parseExprOrFn(expr),
});
return createElement(ReactivedCondition, {
key: currentComponentKey,
});
}
return element;
};
convertedNode.setReactNode(createReactElementByNode());
}
}
nodeCreatedComponent?.(convertedNode);
const finalElement = convertedNode.getReactNode();
// if finalElement is null, todo..
return finalElement;
}
const LowCodeComponent = forwardRef(function (
props: LowCodeComponentProps,
ref: ForwardedRef<any>,
) {
const { id, className, style, ...extraProps } = props;
const isMounted = useRef(false);
const instance = useMemo(() => {
return createInstance(componentsTree, extraProps);
}, []);
useEffect(() => {
let styleEl: HTMLElement | undefined;
const scopeValue = instance.codeScope.value;
// init dataSource
scopeValue.reloadDataSource();
if (instance.cssText) {
appendExternalStyle(instance.cssText).then((el) => {
styleEl = el;
});
}
// trigger lifeCycles
componentDidMount?.();
instance.triggerLifeCycle('componentDidMount');
// 当 state 改变之后调用
const unwatch = watch(scopeValue.state, (_, oldVal) => {
if (isMounted.current) {
instance.triggerLifeCycle('componentDidUpdate', props, oldVal);
}
});
isMounted.current = true;
return () => {
styleEl?.parentNode?.removeChild(styleEl);
componentWillUnmount?.();
instance.triggerLifeCycle('componentWillUnmount');
unwatch();
isMounted.current = false;
};
}, [instance]);
return (
<div id={id} className={className} style={style} ref={ref}>
{instance
.getComponentTreeNodes()
.map((n) => createReactElement(n, codeRuntime, instance, componentsRecord))}
</div>
);
});
LowCodeComponent.displayName = displayName;
return LowCodeComponent;
},
});

View File

@ -0,0 +1,418 @@
import {
createComponentFunction,
isLowCodeComponentSchema,
createCodeRuntime,
TextWidget,
ComponentWidget,
isJSExpression,
processValue,
isJSFunction,
isJSSlot,
someValue,
} from '@alilc/renderer-core';
import { isPlainObject } from 'lodash-es';
import { forwardRef, useRef, useEffect, createElement, useMemo } from 'react';
import { createSignal, watch } from './signals';
import { appendExternalStyle } from './utils/element';
import { reactive } from './utils/reactive';
import type {
CreateComponentBaseOptions,
PlainObject,
InstanceStateApi,
LowCodeComponent as LowCodeComponentSchema,
CodeRuntime,
IntlApi,
JSSlot,
JSFunction,
I18nNode,
} from '@alilc/renderer-core';
import type {
ComponentType,
ReactInstance,
CSSProperties,
ForwardedRef,
ReactNode,
ReactElement,
} from 'react';
export type ReactComponentLifeCycle =
| 'constructor'
| 'render'
| 'componentDidMount'
| 'componentDidUpdate'
| 'componentWillUnmount'
| 'componentDidCatch';
export interface ComponentOptions<C = ComponentType<any>>
extends CreateComponentBaseOptions<ReactComponentLifeCycle> {
componentsRecord: Record<string, C | LowCodeComponentSchema>;
intl: IntlApi;
displayName?: string;
beforeElementCreate?(widget: TextWidget<C> | ComponentWidget<C>): void;
componentRefAttached?(widget: ComponentWidget<C>, instance: ReactInstance): void;
}
export interface LowCodeComponentProps {
id?: string;
/** CSS 类名 */
className?: string;
/** style */
style?: CSSProperties;
[key: string]: any;
}
export const createComponent = createComponentFunction<
ComponentType<any>,
ReactInstance,
ReactComponentLifeCycle,
ComponentOptions
>(reactiveStateCreator, (container, options) => {
const {
componentsRecord,
intl,
displayName = '__LowCodeComponent__',
beforeElementCreate,
componentRefAttached,
...extraOptions
} = options;
const lowCodeComponentCache = new Map<string, ComponentType<any>>();
function getComponentByName(componentName: string) {
const Component = componentsRecord[componentName];
if (!Component) {
return undefined;
}
if (isLowCodeComponentSchema(Component)) {
if (lowCodeComponentCache.has(componentName)) {
return lowCodeComponentCache.get(componentName);
}
const LowCodeComponent = createComponent({
...extraOptions,
intl,
displayName: Component.componentName,
componentsRecord,
componentsTree: Component.schema,
});
lowCodeComponentCache.set(componentName, LowCodeComponent);
return LowCodeComponent;
}
return Component;
}
function createReactElement(
widget: TextWidget<ComponentType<any>> | ComponentWidget<ComponentType<any>>,
codeRuntime: CodeRuntime,
) {
beforeElementCreate?.(widget);
return widget.build((elements) => {
if (elements.length > 0) {
const RenderObject = elements[elements.length - 1];
const Wrappers = elements.slice(0, elements.length - 1);
const buildRenderElement = () => {
if (widget instanceof TextWidget) {
if (widget.type === 'string') {
return createElement(RenderObject, { key: widget.key, text: widget.raw });
} else {
return createElement(
reactive(RenderObject, {
target:
widget.type === 'expression' ? { text: widget.raw } : (widget.raw as I18nNode),
valueGetter(expr) {
return codeRuntime.parseExprOrFn(expr);
},
}),
{ key: widget.key },
);
}
} else if (widget instanceof ComponentWidget) {
const { condition, loop, loopArgs } = widget;
// condition为 Falsy 的情况下 不渲染
if (!condition) return null;
// loop 为数组且为空的情况下 不渲染
if (Array.isArray(loop) && loop.length === 0) return null;
function createElementWithProps(
Component: ComponentType<any>,
widget: ComponentWidget<ComponentType<any>>,
codeRuntime: CodeRuntime,
key?: string,
): ReactElement {
const { ref, ...componentProps } = widget.props;
const componentKey = key ?? widget.key;
const attachRef = (ins: ReactInstance) => {
if (ins) {
if (ref) container.setInstance(ref as string, ins);
componentRefAttached?.(widget, ins);
} else {
if (ref) container.removeInstance(ref);
}
};
// 先将 jsslot, jsFunction 对象转换
const finalProps = processValue(
componentProps,
(node) => isJSFunction(node) || isJSSlot(node),
(node: JSSlot | JSFunction) => {
if (isJSSlot(node)) {
if (node.value) {
const widgets = (Array.isArray(node.value) ? node.value : [node.value]).map(
(v) => new ComponentWidget<ComponentType<any>>(v),
);
if (node.params?.length) {
return (...args: any[]) => {
const params = node.params!.reduce((prev, cur, idx) => {
return (prev[cur] = args[idx]);
}, {} as PlainObject);
const subCodeScope = codeRuntime.getScope().createSubScope(params);
const subCodeRuntime = createCodeRuntime(subCodeScope);
return widgets.map((n) => createReactElement(n, subCodeRuntime));
};
} else {
return widgets.map((n) => createReactElement(n, codeRuntime));
}
}
} else if (isJSFunction(node)) {
return codeRuntime.parseExprOrFn(node);
}
return null;
},
);
const childElements = widget.children.map((child) =>
createReactElement(child, codeRuntime),
);
if (someValue(finalProps, isJSExpression)) {
const PropsWrapper = (props: PlainObject) =>
createElement(
Component,
{
...props,
key: componentKey,
ref: attachRef,
},
childElements,
);
PropsWrapper.displayName = 'PropsWrapper';
return createElement(
reactive(PropsWrapper, {
target: finalProps,
valueGetter: (node) => codeRuntime.parseExprOrFn(node),
}),
{ key: componentKey },
);
} else {
return createElement(
Component,
{
...finalProps,
key: componentKey,
ref: attachRef,
},
childElements,
);
}
}
let element: ReactElement | ReactElement[] = createElementWithProps(
RenderObject,
widget,
codeRuntime,
);
if (loop) {
const genLoopElements = (loopData: any[]) => {
return loopData.map((item, idx) => {
const loopArgsItem = loopArgs[0] ?? 'item';
const loopArgsIndex = loopArgs[1] ?? 'index';
const subCodeScope = codeRuntime.getScope().createSubScope({
[loopArgsItem]: item,
[loopArgsIndex]: idx,
});
const subCodeRuntime = createCodeRuntime(subCodeScope);
return createElementWithProps(
RenderObject,
widget,
subCodeRuntime,
`loop-${widget.key}-${idx}`,
);
});
};
if (isJSExpression(loop)) {
function Loop(props: { loop: boolean }) {
if (!Array.isArray(props.loop)) {
return null;
}
return <>{genLoopElements(props.loop)}</>;
}
Loop.displayName = 'Loop';
const ReactivedLoop = reactive(Loop, {
target: {
loop,
},
valueGetter: (expr) => codeRuntime.parseExprOrFn(expr),
});
element = createElement(ReactivedLoop, {
key: widget.key,
});
} else {
element = genLoopElements(loop as any[]);
}
}
if (isJSExpression(condition)) {
function Condition(props: any) {
if (props.condition) {
return element;
}
return null;
}
Condition.displayName = 'Condition';
const ReactivedCondition = reactive(Condition, {
target: {
condition,
},
valueGetter: (expr) => codeRuntime.parseExprOrFn(expr),
});
element = createElement(ReactivedCondition, {
key: widget.key,
});
}
return element;
}
return null;
};
const element = buildRenderElement();
return Wrappers.reduce((prevElement, CurWrapper) => {
return createElement(CurWrapper, { key: widget.key }, prevElement);
}, element);
}
});
}
const LowCodeComponent = forwardRef(function (
props: LowCodeComponentProps,
ref: ForwardedRef<any>,
) {
const { id, className, style } = props;
const isMounted = useRef(false);
useEffect(() => {
const scopeValue = container.codeRuntime.getScope().value;
// init dataSource
scopeValue.reloadDataSource();
let styleEl: HTMLElement | undefined;
const cssText = container.getCssText();
if (cssText) {
appendExternalStyle(cssText).then((el) => {
styleEl = el;
});
}
// trigger lifeCycles
// componentDidMount?.();
container.triggerLifeCycle('componentDidMount');
// 当 state 改变之后调用
const unwatch = watch(scopeValue.state, (_, oldVal) => {
if (isMounted.current) {
container.triggerLifeCycle('componentDidUpdate', props, oldVal);
}
});
isMounted.current = true;
return () => {
// componentWillUnmount?.();
container.triggerLifeCycle('componentWillUnmount');
styleEl?.parentNode?.removeChild(styleEl);
unwatch();
isMounted.current = false;
};
}, []);
const widgets = useMemo(() => {
return container.createWidgets<ComponentType<any>>().map((widget) =>
widget.mapRenderObject((widget) => {
if (widget instanceof TextWidget) {
if (widget.type === 'i18n') {
function IntlText(props: { key: string; params: Record<string, string> }) {
return <>{intl.i18n(props.key, props.params)}</>;
}
IntlText.displayName = 'IntlText';
return IntlText;
}
function Text(props: { text: string }) {
return <>{props.text}</>;
}
Text.displayName = 'Text';
return Text;
} else if (widget instanceof ComponentWidget) {
return getComponentByName(widget.raw.componentName);
}
}),
);
}, []);
return (
<div id={id} className={className} style={style} ref={ref}>
{widgets.map((widget) => createReactElement(widget, container.codeRuntime))}
</div>
);
});
LowCodeComponent.displayName = displayName;
return LowCodeComponent;
});
function reactiveStateCreator(initState: PlainObject): InstanceStateApi {
const proxyState = createSignal(initState);
return {
get state() {
return proxyState.value;
},
setState(newState) {
if (!isPlainObject(newState)) {
throw Error('newState mush be a object');
}
proxyState.value = {
...proxyState.value,
...newState,
};
},
};
}

View File

@ -1,6 +1,7 @@
import { AppContext, type AppContextObject } from '../context/app';
import { createComponent } from '../api/createComponent';
import { createComponent } from '../component';
import Route from './route';
import { createRouterProvider } from './router-view';
export default function App({ context }: { context: AppContextObject }) {
const { schema, config, renderer, packageManager, appScope } = context;
@ -15,8 +16,7 @@ export default function App({ context }: { context: AppContextObject }) {
if (Component?.devMode === 'lowCode') {
const componentsMap = schema.getComponentsMaps();
const componentsRecord =
packageManager.getComponentsNameRecord<any>(componentsMap);
const componentsRecord = packageManager.getComponentsNameRecord<any>(componentsMap);
const Layout = createComponent({
componentsTree: Component.schema,
@ -24,6 +24,7 @@ export default function App({ context }: { context: AppContextObject }) {
dataSourceCreator: config.get('dataSourceCreator'),
supCodeScope: appScope,
intl: appScope.value.intl,
});
return Layout;
@ -54,5 +55,11 @@ export default function App({ context }: { context: AppContextObject }) {
}, element);
}
return <AppContext.Provider value={context}>{element}</AppContext.Provider>;
const RouterProvider = createRouterProvider(appScope.value.router);
return (
<AppContext.Provider value={context}>
<RouterProvider>{element}</RouterProvider>
</AppContext.Provider>
);
}

View File

@ -1,43 +1,28 @@
import type { PageSchema, PageContainerSchema } from '@alilc/runtime-shared';
import type { PageConfig, ComponentTree } from '@alilc/renderer-core';
import { useAppContext } from '../context/app';
import { createComponent } from '../api/createComponent';
import { PAGE_EVENTS } from '../events';
import { createComponent } from '../component';
export interface OutletProps {
pageSchema: PageSchema;
componentsTree?: PageContainerSchema | undefined;
pageConfig: PageConfig;
componentsTree?: ComponentTree | undefined;
[key: string]: any;
}
export default function Outlet({ pageSchema, componentsTree }: OutletProps) {
const { schema, config, packageManager, appScope, boosts } = useAppContext();
const { schema, config, packageManager, appScope } = useAppContext();
const { type = 'lowCode' } = pageSchema;
if (type === 'lowCode' && componentsTree) {
const componentsMap = schema.getComponentsMaps();
const componentsRecord =
packageManager.getComponentsNameRecord<any>(componentsMap);
const componentsRecord = packageManager.getComponentsNameRecord<any>(componentsMap);
const LowCodeComponent = createComponent({
supCodeScope: appScope,
dataSourceCreator: config.get('dataSourceCreator'),
componentsTree,
componentsRecord,
beforeNodeCreateComponent(node) {
boosts.hooks.call(PAGE_EVENTS.COMPONENT_BEFORE_NODE_CREATE, node);
},
nodeCreatedComponent(result) {
boosts.hooks.call(PAGE_EVENTS.COMPONENT_NODE_CREATED, result);
},
nodeComponentRefAttached(node, instance) {
boosts.hooks.call(
PAGE_EVENTS.COMPONENT_NODE_REF_ATTACHED,
node,
instance
);
},
intl: appScope.value.intl,
});
return <LowCodeComponent />;

View File

@ -1,28 +1,21 @@
import { usePageSchema } from '../context/router';
import { usePageConfig } from '../context/router';
import { useAppContext } from '../context/app';
import RouteOutlet from './outlet';
export default function Route(props: any) {
const { schema, renderer } = useAppContext();
const pageSchema = usePageSchema();
const Outlet = renderer.getOutlet();
const pageConfig = usePageConfig();
const Outlet = renderer.getOutlet() ?? RouteOutlet;
if (Outlet && pageSchema) {
if (Outlet && pageConfig) {
let componentsTree;
const { type = 'lowCode', treeId } = pageSchema;
const { type = 'lowCode', mappingId } = pageConfig;
if (type === 'lowCode') {
componentsTree = schema
.getComponentsTrees()
.find(item => item.id === treeId);
componentsTree = schema.getComponentsTrees().find((item) => item.id === mappingId);
}
return (
<Outlet
{...props}
pageSchema={pageSchema}
componentsTree={componentsTree}
/>
);
return <Outlet {...props} pageConfig={pageConfig} componentsTree={componentsTree} />;
}
return null;

View File

@ -1,28 +1,24 @@
import { type Router } from '@alilc/runtime-router';
import { useState, useLayoutEffect, useMemo, type ReactNode } from 'react';
import {
RouterContext,
RouteLocationContext,
PageSchemaContext,
} from '../context/router';
import { RouterContext, RouteLocationContext, PageConfigContext } from '../context/router';
import { useAppContext } from '../context/app';
export const createRouterProvider = (router: Router) => {
return function RouterProvider({ children }: { children?: ReactNode }) {
const { schema } = useAppContext();
const [location, setCurrentLocation] = useState(router.getCurrentRoute());
const [location, setCurrentLocation] = useState(router.getCurrentLocation());
useLayoutEffect(() => {
const remove = router.afterRouteChange(to => setCurrentLocation(to));
const remove = router.afterRouteChange((to) => setCurrentLocation(to));
return () => remove();
}, []);
const pageSchema = useMemo(() => {
const pages = schema.getPages();
const pages = schema.getPageConfigs();
const matched = location.matched[location.matched.length - 1];
if (matched) {
const page = pages.find(item => matched.page === item.id);
const page = pages.find((item) => matched.page === item.id);
return page;
}
@ -32,9 +28,7 @@ export const createRouterProvider = (router: Router) => {
return (
<RouterContext.Provider value={router}>
<RouteLocationContext.Provider value={location}>
<PageSchemaContext.Provider value={pageSchema}>
{children}
</PageSchemaContext.Provider>
<PageConfigContext.Provider value={pageSchema}>{children}</PageConfigContext.Provider>
</RouteLocationContext.Provider>
</RouterContext.Provider>
);

View File

@ -1,5 +1,5 @@
import { createContext, useContext } from 'react';
import { type AppContext as AppContextType } from '@alilc/runtime-core';
import { type AppContext as AppContextType } from '@alilc/renderer-core';
import { type ReactRenderer } from '../renderer';
export interface AppContextObject extends AppContextType {

View File

@ -1,5 +1,5 @@
import { type Router, type RouteLocation } from '@alilc/runtime-router';
import { type PageSchema } from '@alilc/runtime-shared';
import { type Router, type RouteLocationNormalized } from '@alilc/runtime-router';
import { type PageConfig } from '@alilc/renderer-core';
import { createContext, useContext } from 'react';
export const RouterContext = createContext<Router>({} as any);
@ -8,10 +8,10 @@ RouterContext.displayName = 'RouterContext';
export const useRouter = () => useContext(RouterContext);
export const RouteLocationContext = createContext<RouteLocation>({
export const RouteLocationContext = createContext<RouteLocationNormalized>({
name: undefined,
path: '/',
query: {},
searchParams: undefined,
params: {},
hash: '',
fullPath: '/',
@ -24,10 +24,8 @@ RouteLocationContext.displayName = 'RouteLocationContext';
export const useRouteLocation = () => useContext(RouteLocationContext);
export const PageSchemaContext = createContext<PageSchema | undefined>(
undefined
);
export const PageConfigContext = createContext<PageConfig | undefined>(undefined);
PageSchemaContext.displayName = 'PageContext';
PageConfigContext.displayName = 'PageConfigContext';
export const usePageSchema = () => useContext(PageSchemaContext);
export const usePageConfig = () => useContext(PageConfigContext);

View File

@ -0,0 +1,2 @@
export * from './api/app';
export * from './api/component';

View File

@ -1,57 +0,0 @@
import { type ReactNode, createElement } from 'react';
import { someValue } from '@alilc/runtime-core';
import { isJsExpression } from '@alilc/runtime-shared';
import { definePlugin } from '../../renderer';
import { PAGE_EVENTS } from '../../events';
import { reactive } from '../../utils/reactive';
import { createIntl } from './intl';
export { createIntl };
declare module '@alilc/renderer-core' {
interface AppBoosts {
intl: ReturnType<typeof createIntl>;
}
}
export const intlPlugin = definePlugin({
name: 'intl',
setup({ schema, appScope, boosts }) {
const i18nMessages = schema.getByKey('i18n') ?? {};
const defaultLocale = schema.getByPath('config.defaultLocale') ?? 'zh-CN';
const intl = createIntl(i18nMessages, defaultLocale);
appScope.setValue(intl);
boosts.add('intl', intl);
boosts.hooks.hook(PAGE_EVENTS.COMPONENT_BEFORE_NODE_CREATE, (node) => {
if (node.type === 'i18n') {
const { key, params } = node.raw.data;
let element: ReactNode;
if (someValue(params, isJsExpression)) {
function IntlText(props: any) {
return intl.i18n(key, props.params);
}
IntlText.displayName = 'IntlText';
const Reactived = reactive(IntlText, {
target: {
params,
},
valueGetter(expr) {
return node.context.codeRuntime.parseExprOrFn(expr);
},
});
element = createElement(Reactived, { key });
} else {
element = intl.i18n(key, params ?? {});
}
node.setReactNode(element);
}
});
},
});

View File

@ -1,98 +0,0 @@
import { createCodeRuntime, type PackageManager } from '@alilc/runtime-core';
import {
type UtilItem,
type InternalUtils,
type ExternalUtils,
type AnyFunction,
} from '@alilc/runtime-shared';
import { definePlugin } from '../../renderer';
declare module '@alilc/runtime-core' {
interface AppBoosts {
globalUtils: GlobalUtils;
}
}
export interface GlobalUtils {
getUtil(name: string): AnyFunction;
addUtil(utilItem: UtilItem): void;
addUtil(name: string, fn: AnyFunction): void;
addUtils(utils: Record<string, AnyFunction>): void;
}
export const globalUtilsPlugin = definePlugin({
name: 'globalUtils',
setup({ schema, appScope, packageManager, boosts }) {
const utils = schema.getByKey('utils') ?? [];
const globalUtils = createGlobalUtils(packageManager);
const utilsProxy = new Proxy(Object.create(null), {
get(_, p: string) {
return globalUtils.getUtil(p);
},
set() {
return false;
},
has(_, p: string) {
return Boolean(globalUtils.getUtil(p));
},
});
utils.forEach(globalUtils.addUtil);
appScope.inject('utils', utilsProxy);
boosts.add('globalUtils', globalUtils);
},
});
function createGlobalUtils(packageManager: PackageManager) {
const codeRuntime = createCodeRuntime();
const utilsMap: Record<string, AnyFunction> = {};
function addUtil(item: string | UtilItem, fn?: AnyFunction) {
if (typeof item === 'string') {
if (typeof fn === 'function') {
utilsMap[item] = fn;
}
} else {
const fn = parseUtil(item);
addUtil(item.name, fn);
}
}
const globalUtils: GlobalUtils = {
addUtil,
addUtils(utils) {
Object.keys(utils).forEach(key => addUtil(key, utils[key]));
},
getUtil(name) {
return utilsMap[name];
},
};
function parseUtil(utilItem: UtilItem) {
if (utilItem.type === 'function') {
const { content } = utilItem as InternalUtils;
return codeRuntime.createFnBoundScope(content.value);
} else {
const {
content: { package: packageName, destructuring, exportName, subName },
} = utilItem as ExternalUtils;
let library: any = packageManager.getLibraryByPackageName(packageName);
if (library) {
if (destructuring) {
const target = library[exportName!];
library = subName ? target[subName] : target;
}
return library;
}
}
}
return globalUtils;
}

View File

@ -2,7 +2,7 @@ import {
definePlugin as definePluginFn,
type Plugin,
type PluginSetupContext,
} from '@alilc/runtime-core';
} from '@alilc/renderer-core';
import { type ComponentType, type PropsWithChildren } from 'react';
import { type OutletProps } from './components/outlet';

View File

@ -1,29 +0,0 @@
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/renderer-core' {
interface AppBoosts {
router: Router;
}
}
const defaultRouterOptions: RouterOptions = {
historyMode: 'browser',
baseName: '/',
routes: [],
};
export function initRouter(context: ReactRendererSetupContext) {
const { schema, boosts, appScope, renderer } = context;
const router = createRouter(schema.getByKey('router') ?? defaultRouterOptions);
appScope.inject('router', router);
boosts.add('router', router);
const RouterProvider = createRouterProvider(router);
renderer.addAppWrapper(RouterProvider);
renderer.setOutlet(RouteOutlet);
}

View File

@ -3,13 +3,11 @@ import { createSignal, computed } from '../../signals';
export function createIntl(
messages: Record<string, Record<string, string>>,
defaultLocale: string
defaultLocale: string,
) {
const allMessages = createSignal(messages);
const currentLocale = createSignal(defaultLocale);
const currentMessages = computed(
() => allMessages.value[currentLocale.value]
);
const currentMessages = computed(() => allMessages.value[currentLocale.value]);
return {
i18n(key: string, params: Record<string, string>) {

View File

@ -0,0 +1,72 @@
import {
createCodeRuntime,
type PackageManager,
type AnyFunction,
type Util,
type UtilsApi,
} from '@alilc/renderer-core';
export interface RuntimeUtils extends UtilsApi {
addUtil(utilItem: Util): void;
addUtil(name: string, fn: AnyFunction): void;
}
export function createRuntimeUtils(
utilSchema: Util[],
packageManager: PackageManager,
): RuntimeUtils {
const codeRuntime = createCodeRuntime();
const utilsMap: Record<string, AnyFunction> = {};
function addUtil(item: string | Util, fn?: AnyFunction) {
if (typeof item === 'string') {
if (typeof fn === 'function') {
utilsMap[item] = fn;
}
} else {
const fn = parseUtil(item);
addUtil(item.name, fn);
}
}
function parseUtil(utilItem: Util) {
if (utilItem.type === 'function') {
const { content } = utilItem;
return codeRuntime.createFnBoundScope(content.value);
} else {
const {
content: { package: packageName, destructuring, exportName, subName },
} = utilItem;
let library: any = packageManager.getLibraryByPackageName(packageName!);
if (library) {
if (destructuring) {
const target = library[exportName!];
library = subName ? target[subName] : target;
}
return library;
}
}
}
utilSchema.forEach((item) => addUtil(item));
const utilsProxy = new Proxy(Object.create(null), {
get(_, p: string) {
return utilsMap[p];
},
set() {
return false;
},
has(_, p: string) {
return Boolean(utilsMap[p]);
},
});
return {
addUtil,
utils: utilsProxy,
};
}

View File

@ -1,3 +1,4 @@
import { type PlainObject } from '@alilc/renderer-core';
import {
ref,
computed,
@ -114,7 +115,7 @@ function traverse(value: unknown, depth?: number, currentDepth = 0, seen?: Set<u
});
} else if (isPlainObject(value)) {
for (const key in value) {
traverse(value[key], depth, currentDepth, seen);
traverse((value as PlainObject)[key], depth, currentDepth, seen);
}
}
return value;

View File

@ -1,32 +1,26 @@
import {
type AnyObject,
processValue,
type AnyFunction,
type PlainObject,
type JSExpression,
isJsExpression,
} from '@alilc/runtime-shared';
import { processValue } from '@alilc/runtime-core';
import {
type ComponentType,
memo,
forwardRef,
type ForwardRefRenderFunction,
type PropsWithChildren,
} from 'react';
isJSExpression,
} from '@alilc/renderer-core';
import { type ComponentType, memo, forwardRef, type PropsWithChildren, createElement } from 'react';
import { produce } from 'immer';
import hoistNonReactStatics from 'hoist-non-react-statics';
import { useSyncExternalStore } from 'use-sync-external-store/shim';
import { computed, watch } from '../signals';
export interface ReactiveStore<Snapshot = AnyObject> {
export interface ReactiveStore<Snapshot = PlainObject> {
value: Snapshot;
onStateChange: AnyFunction | null;
subscribe: (onStoreChange: () => void) => () => void;
getSnapshot: () => Snapshot;
}
function createReactiveStore<Snapshot = AnyObject>(
function createReactiveStore<Snapshot = PlainObject>(
target: Record<string, any>,
valueGetter: (expr: JSExpression) => any
valueGetter: (expr: JSExpression) => any,
): ReactiveStore<Snapshot> {
let isFlushing = false;
let isFlushPending = false;
@ -34,25 +28,21 @@ function createReactiveStore<Snapshot = AnyObject>(
const cleanups: Array<() => void> = [];
const waitPathToSetValueMap = new Map();
const initValue = processValue(
target,
isJsExpression,
(node: JSExpression, paths) => {
const computedValue = computed(() => valueGetter(node));
const unwatch = watch(computedValue, newValue => {
waitPathToSetValueMap.set(paths, newValue);
const initValue = processValue(target, isJSExpression, (node: JSExpression, paths) => {
const computedValue = computed(() => valueGetter(node));
const unwatch = watch(computedValue, (newValue) => {
waitPathToSetValueMap.set(paths, newValue);
if (!isFlushPending && !isFlushing) {
isFlushPending = true;
Promise.resolve().then(genValue);
}
});
if (!isFlushPending && !isFlushing) {
isFlushPending = true;
Promise.resolve().then(genValue);
}
});
cleanups.push(unwatch);
cleanups.push(unwatch);
return computedValue.value;
}
);
return computedValue.value;
});
const genValue = () => {
isFlushPending = false;
@ -89,7 +79,7 @@ function createReactiveStore<Snapshot = AnyObject>(
return () => {
store.onStateChange = null;
cleanups.forEach(c => c());
cleanups.forEach((c) => c());
cleanups.length = 0;
};
},
@ -102,35 +92,32 @@ function createReactiveStore<Snapshot = AnyObject>(
}
interface ReactiveOptions {
target: AnyObject;
target: PlainObject;
valueGetter: (expr: JSExpression) => any;
forwardRef?: boolean;
}
export function reactive<TProps extends AnyObject = AnyObject>(
WrappedComponent: ForwardRefRenderFunction<PropsWithChildren<TProps>>,
{ target, valueGetter, forwardRef: forwardRefOption = true }: ReactiveOptions
) {
export function reactive<TProps extends PlainObject = PlainObject>(
WrappedComponent: ComponentType<TProps>,
{ target, valueGetter, forwardRef: forwardRefOption = true }: ReactiveOptions,
): ComponentType<PlainObject> {
const store = createReactiveStore(target, valueGetter);
function WrapperComponent(props: any, ref: any) {
const actualProps = useSyncExternalStore(
store.subscribe,
store.getSnapshot
);
return <WrappedComponent {...props} {...actualProps} ref={ref} />;
const actualProps = useSyncExternalStore(store.subscribe, store.getSnapshot);
return createElement(WrappedComponent, {
...props,
...actualProps,
ref,
});
}
const componentName =
WrappedComponent.displayName || WrappedComponent.name || 'Component';
const componentName = WrappedComponent.displayName || WrappedComponent.name || 'Component';
const displayName = `Reactive(${componentName})`;
const _Reactived = forwardRefOption
? forwardRef(WrapperComponent)
: WrapperComponent;
const Reactived = memo(_Reactived) as unknown as ComponentType<
PropsWithChildren<TProps>
>;
const _Reactived = forwardRefOption ? forwardRef(WrapperComponent) : WrapperComponent;
const Reactived = memo(_Reactived) as unknown as ComponentType<PropsWithChildren<TProps>>;
Reactived.displayName = WrappedComponent.displayName = displayName;

View File

@ -1,8 +1,11 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "dist",
"paths": {
"@alilc/*": ["runtime/*/src"]
"@alilc/*": ["runtime/*"],
"@alilc/runtime-router": ["runtime/router"]
}
}
},
"include": ["src"]
}

View File

@ -6,11 +6,20 @@
"bugs": "https://github.com/alibaba/lowcode-engine/issues",
"homepage": "https://github.com/alibaba/lowcode-engine/#readme",
"license": "MIT",
"module": "dist/index.js",
"types": "dist/index.d.ts",
"scripts": {
"build": "",
"build": "tsc",
"test": "vitest"
},
"dependencies": {
"@alilc/renderer-core": "^2.0.0-beta.0"
"@alilc/renderer-core": "^2.0.0-beta.0",
"lodash-es": "^4.17.21",
"path-to-regexp": "^6.2.1",
"qs": "^6.12.0"
},
"devDependencies": {
"@types/lodash-es": "^4.17.12",
"@types/qs": "^6.9.13"
}
}

View File

@ -1,72 +1,38 @@
import {
type RouteLocation,
type RouteLocationRaw,
} from '@alilc/runtime-shared';
import { RawRouteLocation } from '@alilc/renderer-core';
import { type RouteLocationNormalized } from './types';
import { isRouteLocation } from './utils/helper';
export type NavigationHookAfter = (
to: RouteLocation,
from: RouteLocation
to: RouteLocationNormalized,
from: RouteLocationNormalized,
) => any;
export type NavigationGuardReturn =
| void
| Error
| RouteLocationRaw
| boolean
| NavigationGuardNextCallback;
export type NavigationGuardNextCallback = () => any;
export interface NavigationGuardNext {
(): void;
(error: Error): void;
(location: RouteLocationRaw): void;
(valid: boolean | undefined): void;
(cb: NavigationGuardNextCallback): void;
/**
* Allows to detect if `next` isn't called in a resolved guard. Used
* internally in DEV mode to emit a warning. Commented out to simplify
* typings.
* @internal
*/
_called?: boolean;
}
export type NavigationGuardReturn = undefined | Error | RawRouteLocation | boolean;
/**
* Navigation guard.
*/
export interface NavigationGuard {
(to: RouteLocation, from: RouteLocation, next: NavigationGuardNext):
| NavigationGuardReturn
| Promise<NavigationGuardReturn>;
(
to: RouteLocationNormalized,
from: RouteLocationNormalized,
): NavigationGuardReturn | Promise<NavigationGuardReturn>;
}
export function guardToPromiseFn(
guard: NavigationGuard,
to: RouteLocation,
from: RouteLocation
to: RouteLocationNormalized,
from: RouteLocationNormalized,
): () => Promise<void> {
return () =>
new Promise((resolve, reject) => {
const next: NavigationGuardNext = (
valid?: boolean | RouteLocationRaw | NavigationGuardNextCallback | Error
) => {
const next = (valid?: boolean | RawRouteLocation | Error) => {
if (valid === false) {
reject();
} else if (valid instanceof Error) {
reject(valid);
} else if (isRouteLocation(valid)) {
// todo
// reject(
// createRouterError<NavigationRedirectError>(
// ErrorTypes.NAVIGATION_GUARD_REDIRECT,
// {
// from: to,
// to: valid
// }
// )
// );
// todo reject (error)
reject();
} else {
resolve();
@ -74,55 +40,10 @@ export function guardToPromiseFn(
};
// 使用 Promise.resolve 包装允许它与异步和同步守卫一起工作
const guardReturn = guard.call(
null,
to,
from,
canOnlyBeCalledOnce(next, to, from)
);
let guardCall = Promise.resolve(guardReturn);
const guardReturn = guard.call(null, to, from);
if (guard.length <= 2) guardCall = guardCall.then(next);
if (guard.length > 2) {
const message = `The "next" callback was never called inside of ${
guard.name ? '"' + guard.name + '"' : ''
}:\n${guard.toString()}\n. If you are returning a value instead of calling "next", make sure to remove the "next" parameter from your function.`;
if (typeof guardReturn === 'object' && 'then' in guardReturn) {
guardCall = guardCall.then(resolvedValue => {
if (!next._called) {
console.warn(message);
return Promise.reject(new Error('Invalid navigation guard'));
}
return resolvedValue;
});
} else if (guardReturn !== undefined) {
if (!next._called) {
console.warn(message);
reject(new Error('Invalid navigation guard'));
return;
}
}
}
guardCall.catch(err => reject(err));
return Promise.resolve(guardReturn)
.then(next)
.catch((err) => reject(err));
});
}
function canOnlyBeCalledOnce(
next: NavigationGuardNext,
to: RouteLocation,
from: RouteLocation
): NavigationGuardNext {
let called = 0;
return function () {
if (called++ === 1) {
console.warn(
`The "next" callback was called more than once in one navigation guard when going from "${from.fullPath}" to "${to.fullPath}". It should be called exactly one time in each navigation guard. This will fail in production.`
);
}
next._called = true;
if (called === 1) {
next.apply(null, arguments as any);
}
};
}

View File

@ -1,4 +1,4 @@
import { useCallbacks } from '@alilc/runtime-shared';
import { useEvent } from '@alilc/renderer-core';
export type HistoryState = Record<string | number, any>;
export type HistoryLocation = string;
@ -23,7 +23,7 @@ export type NavigationInformation = {
export type NavigationCallback = (
to: HistoryLocation,
from: HistoryLocation,
info: NavigationInformation
info: NavigationInformation,
) => void;
/**
@ -94,7 +94,7 @@ function buildState(
current: HistoryLocation,
forward: HistoryLocation | null,
replaced = false,
position = window.history.length
position = window.history.length,
): RouterHistoryState {
return {
name: '__ROUTER_STATE__',
@ -110,25 +110,14 @@ export function createBrowserHistory(base?: string): RouterHistory {
const finalBase = normalizeBase(base);
const { history, location } = window;
let currentLocation: HistoryLocation = createCurrentLocation(
finalBase,
location
);
let currentLocation: HistoryLocation = createCurrentLocation(finalBase, location);
let historyState: RouterHistoryState = history.state;
if (!historyState) {
doDomHistoryEvent(
currentLocation,
buildState(null, currentLocation, null, true),
true
);
doDomHistoryEvent(currentLocation, buildState(null, currentLocation, null, true), true);
}
function doDomHistoryEvent(
to: HistoryLocation,
state: RouterHistoryState,
replace: boolean
) {
function doDomHistoryEvent(to: HistoryLocation, state: RouterHistoryState, replace: boolean) {
// 处理 hash 情况下的 url
const hashIndex = finalBase.indexOf('#');
const url =
@ -150,7 +139,7 @@ export function createBrowserHistory(base?: string): RouterHistory {
history.state,
buildState(historyState.back, to, historyState.forward, true),
data,
{ position: historyState.position }
{ position: historyState.position },
);
doDomHistoryEvent(to, state, true);
@ -158,14 +147,9 @@ export function createBrowserHistory(base?: string): RouterHistory {
}
function push(to: HistoryLocation, data?: HistoryState) {
const currentState: RouterHistoryState = Object.assign(
{},
historyState,
history.state,
{
forward: to,
}
);
const currentState: RouterHistoryState = Object.assign({}, historyState, history.state, {
forward: to,
});
// 防止当前浏览器的 state 被修改先 replace 一次
// 将上次的state 的 forward 修改为 to
@ -175,15 +159,15 @@ export function createBrowserHistory(base?: string): RouterHistory {
{},
buildState(currentLocation, to, null),
{ position: currentState.position + 1 },
data
data,
);
doDomHistoryEvent(to, state, false);
currentLocation = to;
}
let listeners = useCallbacks<NavigationCallback>();
let teardowns = useCallbacks<() => void>();
let listeners = useEvent<NavigationCallback>();
let teardowns = useEvent<() => void>();
let pauseState: HistoryLocation | null = null;
@ -293,8 +277,7 @@ function normalizeBase(base?: string) {
}
const TRAILING_SLASH_RE = /\/$/;
export const removeTrailingSlash = (path: string) =>
path.replace(TRAILING_SLASH_RE, '');
export const removeTrailingSlash = (path: string) => path.replace(TRAILING_SLASH_RE, '');
function createCurrentLocation(base: string, location: Location) {
const { pathname, search, hash } = location;
@ -302,9 +285,7 @@ function createCurrentLocation(base: string, location: Location) {
// hash bases like #, /#, #/, #!, #!/, /#!/, or even /folder#end
const hashPos = base.indexOf('#');
if (hashPos > -1) {
let slicePos = hash.includes(base.slice(hashPos))
? base.slice(hashPos).length
: 1;
let slicePos = hash.includes(base.slice(hashPos)) ? base.slice(hashPos).length : 1;
let pathFromHash = hash.slice(slicePos);
// prepend the starting slash to hash so the url starts with /#
if (pathFromHash[0] !== '/') pathFromHash = '/' + pathFromHash;
@ -339,10 +320,7 @@ export function createHashHistory(base?: string): RouterHistory {
if (!base.endsWith('#/') && !base.endsWith('#')) {
console.warn(
`A hash base must end with a "#":\n"${base}" should be "${base.replace(
/#.*$/,
'#'
)}".`
`A hash base must end with a "#":\n"${base}" should be "${base.replace(/#.*$/, '#')}".`,
);
}
@ -370,12 +348,12 @@ export function createMemoryHistory(base = ''): RouterHistory {
historyStack.push({ location, state });
}
const listeners = useCallbacks<NavigationCallback>();
const listeners = useEvent<NavigationCallback>();
function triggerListeners(
to: HistoryLocation,
from: HistoryLocation,
{ direction, delta }: Pick<NavigationInformation, 'direction' | 'delta'>
{ direction, delta }: Pick<NavigationInformation, 'direction' | 'delta'>,
): void {
const info: NavigationInformation = {
direction,
@ -411,7 +389,7 @@ export function createMemoryHistory(base = ''): RouterHistory {
current: to,
},
data,
{ position }
{ position },
);
// remove current entry and decrement position
@ -428,12 +406,9 @@ export function createMemoryHistory(base = ''): RouterHistory {
historyStack.splice(position, 1);
pushStack(prevState.current, prevState);
const currentState = Object.assign(
{},
buildState(prevState.current, to, null, false),
data,
{ position: ++position }
);
const currentState = Object.assign({}, buildState(prevState.current, to, null, false), data, {
position: ++position,
});
pushStack(to, currentState);
},
@ -442,10 +417,7 @@ export function createMemoryHistory(base = ''): RouterHistory {
const direction: NavigationDirection =
delta < 0 ? NavigationDirection.back : NavigationDirection.forward;
position = Math.max(
0,
Math.min(position + delta, historyStack.length - 1)
);
position = Math.max(0, Math.min(position + delta, historyStack.length - 1));
if (shouldTriggerListeners) {
triggerListeners(this.location, from, {
@ -459,9 +431,7 @@ export function createMemoryHistory(base = ''): RouterHistory {
destroy() {
listeners.clear();
position = 0;
historyStack = [
{ location: '', state: buildState(null, '', null, false, position) },
];
historyStack = [{ location: '', state: buildState(null, '', null, false, position) }];
},
};
}

View File

@ -1,16 +1,7 @@
export { createRouter } from './router';
export {
createBrowserHistory,
createHashHistory,
createMemoryHistory,
} from './history';
export { createBrowserHistory, createHashHistory, createMemoryHistory } from './history';
export type { RouterHistory } from './history';
export type { NavigationGuard, NavigationHookAfter } from './guard';
export type { Router, RouterOptions } from './router';
export type { RouteParams, LocationQuery, RouteRecord } from './types';
export type {
RouteLocation,
RouteLocationRaw,
RouteLocationOptions,
} from '@alilc/runtime-shared';
export * from './types';

View File

@ -1,76 +1,69 @@
import { type AnyObject, pick } from '@alilc/runtime-shared';
import type { RouteRecord, RouteParams } from './types';
import {
createRouteRecordMatcher,
type RouteRecordMatcher,
} from './utils/record-matcher';
import { type PlainObject, type RawLocation } from '@alilc/renderer-core';
import { pick } from 'lodash-es';
import { createRouteRecordMatcher, type RouteRecordMatcher } from './utils/record-matcher';
import { type PathParserOptions } from './utils/path-parser';
export interface MatcherLocationAsPath {
path: string;
}
export interface MatcherLocationAsRelative {
params?: Record<string, string | string[]>;
}
export interface MatcherLocationAsName {
name: string;
params?: RouteParams;
}
import type { RouteRecord, RouteParams, RouteLocationNormalized } from './types';
/**
*
*/
export type MatcherLocationRaw =
| MatcherLocationAsPath
| MatcherLocationAsName
| MatcherLocationAsRelative;
export type RouteRecordNormalized = Required<
Pick<RouteRecord, 'path' | 'page' | 'children'>
> & {
export interface RouteRecordNormalized {
/**
* {@link RouteRecord.name}
*/
name: string | undefined;
name: RouteRecord['name'];
path: RouteRecord['path'];
page: string;
meta: PlainObject;
/**
* {@link RouteRecord.redirect}
*/
redirect: RouteRecord['redirect'] | undefined;
};
export interface MatcherLocation {
name: string | undefined;
path: string;
params: RouteParams;
matched: RouteRecord[];
meta: AnyObject;
redirect: RouteRecord['redirect'];
children: RouteRecord[];
}
/**
* matcher location
*/
export type MatcherLocation = Pick<
RouteLocationNormalized,
'name' | 'path' | 'params' | 'matched' | 'meta'
>;
/**
*
*/
export interface RouterMatcher {
/**
*
*/
addRoute: (record: RouteRecord, parent?: RouteRecordMatcher) => void;
/**
*
*/
removeRoute: {
(matcher: RouteRecordMatcher): void;
(name: string): void;
};
getRoutes: () => RouteRecordMatcher[];
/**
*
*/
getRecordMatchers: () => RouteRecordMatcher[];
/**
*
*/
getRecordMatcher: (name: string) => RouteRecordMatcher | undefined;
/**
* Resolves a location.
* Gives access to the route record that corresponds to the actual path as well as filling the corresponding params objects
* 访 params
*
* @param location - MatcherLocationRaw to resolve to a url
* @param currentLocation - MatcherLocation of the current location
*/
resolve: (
location: MatcherLocationRaw,
currentLocation: MatcherLocation
) => MatcherLocation;
resolve: (location: RawLocation, currentLocation: MatcherLocation) => MatcherLocation;
}
export function createRouterMatcher(
records: RouteRecord[],
globalOptions: PathParserOptions
globalOptions: PathParserOptions,
): RouterMatcher {
const matchers: RouteRecordMatcher[] = [];
const matcherMap = new Map<string, RouteRecordMatcher>();
@ -80,7 +73,7 @@ export function createRouterMatcher(
const options: PathParserOptions = Object.assign(
{},
globalOptions,
pick(record, ['end', 'sensitive', 'strict'])
pick(record, ['end', 'sensitive', 'strict']),
);
// 如果子路由不是绝对路径,则构建嵌套路由的路径。
@ -88,10 +81,9 @@ export function createRouterMatcher(
const { path } = normalizedRecord;
if (parent && path[0] !== '/') {
const parentPath = parent.record.path;
const connectingSlash =
parentPath[parentPath.length - 1] === '/' ? '' : '/';
normalizedRecord.path =
parent.record.path + (path && connectingSlash + path);
const connectingSlash = parentPath[parentPath.length - 1] === '/' ? '' : '/';
normalizedRecord.path = parent.record.path + (path && connectingSlash + path);
}
const matcher = createRouteRecordMatcher(normalizedRecord, parent, options);
@ -130,18 +122,11 @@ export function createRouterMatcher(
}
}
function getRoutes() {
return matchers;
}
function getRecordMatcher(name: string) {
return matcherMap.get(name);
}
function resolve(
location: MatcherLocationRaw,
currentLocation: MatcherLocation
): MatcherLocation {
function resolve(location: RawLocation, currentLocation: MatcherLocation): MatcherLocation {
let matcher: RouteRecordMatcher | undefined;
let params: RouteParams = {};
let path: MatcherLocation['path'];
@ -151,34 +136,32 @@ export function createRouterMatcher(
matcher = matcherMap.get(location.name);
if (!matcher) {
throw new Error(
`Router error: no match for ${JSON.stringify(location)}`
);
throw new Error(`Router error: no match for ${JSON.stringify(location)}`);
}
name = matcher.record.name;
// 从当前路径与传入的参数中获取 params
params = Object.assign(
paramsFromLocation(
currentLocation.params,
currentLocation.params ?? {},
matcher.keys
.filter(k => {
.filter((k) => {
return !(k.modifier === '?' || k.modifier === '*');
})
.map(k => k.name)
.map((k) => k.name),
),
location.params
? paramsFromLocation(
location.params,
matcher.keys.map(k => k.name)
matcher.keys.map((k) => k.name),
)
: {}
: {},
);
// throws if cannot be stringified
path = matcher.stringify(params);
} else if ('path' in location) {
path = location.path;
matcher = matchers.find(m => m.re.test(path));
matcher = matchers.find((m) => m.re.test(path));
if (matcher) {
name = matcher.record.name;
@ -187,13 +170,11 @@ export function createRouterMatcher(
} else {
matcher = currentLocation.name
? matcherMap.get(currentLocation.name)
: matchers.find(m => m.re.test(currentLocation.path));
: matchers.find((m) => m.re.test(currentLocation.path));
if (!matcher) {
throw new Error(
`no match for ${JSON.stringify(location)}, ${JSON.stringify(
currentLocation
)}`
`no match for ${JSON.stringify(location)}, ${JSON.stringify(currentLocation)}`,
);
}
@ -218,24 +199,23 @@ export function createRouterMatcher(
};
}
records.forEach(r => addRoute(r));
records.forEach((r) => addRoute(r));
return {
resolve,
addRoute,
removeRoute,
getRoutes,
getRecordMatchers() {
return matchers;
},
getRecordMatcher,
};
}
function paramsFromLocation(
params: RouteParams,
keys: (string | number)[]
): RouteParams {
function paramsFromLocation(params: RouteParams, keys: (string | number)[]): RouteParams {
const newParams = {} as RouteParams;
for (const key of keys) {
if (key in params) newParams[key] = params[key];
}
@ -243,14 +223,13 @@ function paramsFromLocation(
return newParams;
}
export function normalizeRouteRecord(
record: RouteRecord
): RouteRecordNormalized {
export function normalizeRouteRecord(record: RouteRecord): RouteRecordNormalized {
return {
path: record.path,
redirect: record.redirect,
name: record.name,
page: record.page,
page: record.page || '',
meta: record['meta'] || {},
children: record.children || [],
};
}

View File

@ -1,11 +1,11 @@
import {
type RouterSchema,
useCallbacks,
type RouterApi,
type RouterConfig,
type RouteLocation,
type RouteLocationRaw,
type RouteLocationOptions,
noop,
} from '@alilc/runtime-shared';
useEvent,
type RawRouteLocation,
type RawLocationOptions,
} from '@alilc/renderer-core';
import {
createBrowserHistory,
createHashHistory,
@ -13,46 +13,42 @@ import {
type RouterHistory,
type HistoryState,
} from './history';
import { createRouterMatcher, type MatcherLocationRaw } from './matcher';
import { createRouterMatcher } from './matcher';
import { type PathParserOptions } from './utils/path-parser';
import { parseURL, stringifyURL } from './utils/url';
import { normalizeQuery } from './utils/query';
import { isSameRouteLocation } from './utils/helper';
import type { RouteParams, RouteRecord } from './types';
import {
type NavigationHookAfter,
type NavigationGuard,
guardToPromiseFn,
} from './guard';
import type { RouteParams, RouteRecord, RouteLocationNormalized } from './types';
import { type NavigationHookAfter, type NavigationGuard, guardToPromiseFn } from './guard';
export interface Router {
export interface RouterOptions extends RouterConfig, PathParserOptions {
routes: RouteRecord[];
}
export interface Router extends RouterApi {
readonly options: RouterOptions;
readonly history: RouterHistory;
getCurrentRoute: () => RouteLocation;
getCurrentLocation(): RouteLocationNormalized;
addRoute: {
(parentName: string, route: RouteRecord): void;
(route: RouteRecord): void;
};
resolve(
rawLocation: RawRouteLocation,
currentLocation?: RouteLocationNormalized,
): RouteLocationNormalized;
addRoute(route: RouteRecord): void;
removeRoute(name: string): void;
hasRoute(name: string): boolean;
getRoutes(): RouteRecord[];
push: (to: RouteLocationRaw) => void;
replace: (to: RouteLocationRaw) => void;
hasRoute(name: string): boolean;
beforeRouteLeave: (fn: NavigationGuard) => () => void;
afterRouteChange: (fn: NavigationHookAfter) => () => void;
}
export type RouterOptions = RouterSchema & PathParserOptions;
const START_LOCATION_NORMALIZED: RouteLocation = {
const START_LOCATION: RouteLocationNormalized = {
path: '/',
name: undefined,
params: {},
query: {},
searchParams: undefined,
hash: '',
fullPath: '/',
matched: [],
@ -60,54 +56,50 @@ const START_LOCATION_NORMALIZED: RouteLocation = {
redirectedFrom: undefined,
};
export function createRouter(options: RouterOptions): Router {
const {
baseName = '/',
historyMode = 'browser',
routes = [],
...globalOptions
} = options;
const defaultRouterOptions: RouterOptions = {
historyMode: 'browser',
baseName: '/',
routes: [],
};
export function createRouter(options: RouterOptions = defaultRouterOptions): Router {
const { baseName = '/', historyMode = 'browser', routes = [], ...globalOptions } = options;
const matcher = createRouterMatcher(routes, globalOptions);
const routerHistory =
historyMode === 'hash'
? createHashHistory(baseName)
: historyMode === 'memory'
? createMemoryHistory(baseName)
: createBrowserHistory(baseName);
? createMemoryHistory(baseName)
: createBrowserHistory(baseName);
const beforeGuards = useCallbacks<NavigationGuard>();
const afterGuards = useCallbacks<NavigationHookAfter>();
const beforeGuards = useEvent<NavigationGuard>();
const afterGuards = useEvent<NavigationHookAfter>();
let currentRoute: RouteLocation = START_LOCATION_NORMALIZED;
let pendingLocation = currentRoute;
let currentLocation: RouteLocationNormalized = START_LOCATION;
let pendingLocation = currentLocation;
function resolve(
rawLocation: RouteLocationRaw,
currentLocation?: RouteLocation
): RouteLocation & {
rawLocation: RawRouteLocation,
currentLocation?: RouteLocationNormalized,
): RouteLocationNormalized & {
href: string;
} {
currentLocation = Object.assign({}, currentLocation || currentRoute);
currentLocation = Object.assign({}, currentLocation || currentLocation);
if (typeof rawLocation === 'string') {
const locationNormalized = parseURL(rawLocation);
const matchedRoute = matcher.resolve(
{ path: locationNormalized.path },
currentLocation
);
const matchedRoute = matcher.resolve({ path: locationNormalized.path }, currentLocation);
const href = routerHistory.createHref(locationNormalized.fullPath);
return Object.assign(locationNormalized, matchedRoute, {
query: locationNormalized.query as any,
searchParams: locationNormalized.searchParams,
hash: decodeURIComponent(locationNormalized.hash),
redirectedFrom: undefined,
href,
});
}
let matcherLocation: MatcherLocationRaw;
let matcherLocation: RawRouteLocation;
if ('path' in rawLocation) {
matcherLocation = { ...rawLocation };
@ -140,13 +132,13 @@ export function createRouter(options: RouterOptions): Router {
{
fullPath,
hash,
query: normalizeQuery(rawLocation.query) as any,
searchParams: rawLocation.searchParams,
},
matchedRoute,
{
redirectedFrom: undefined,
href,
}
},
);
}
@ -169,34 +161,33 @@ export function createRouter(options: RouterOptions): Router {
}
}
function getRoutes() {
return matcher.getRoutes().map(item => item.record);
return matcher.getRecordMatchers().map((item) => item.record);
}
function hasRoute(name: string) {
return !!matcher.getRecordMatcher(name);
}
function push(to: RouteLocationRaw) {
function push(to: RawRouteLocation) {
return pushOrRedirect(to);
}
function replace(to: RouteLocationRaw) {
return pushOrRedirect({ ...locationAsObject(to), replace: true });
function replace(to: RawRouteLocation) {
return pushOrRedirect({ ...locationAsObject(to) }, true);
}
function locationAsObject(
to: RouteLocationRaw | RouteLocation
): Exclude<RouteLocationRaw, string> | RouteLocation {
to: RawRouteLocation | RouteLocation,
): Exclude<RawRouteLocation, string> | RouteLocation {
return typeof to === 'string' ? parseURL(to) : { ...to };
}
async function pushOrRedirect(
to: RouteLocationRaw | RouteLocation,
redirectedFrom?: RouteLocation
to: RawRouteLocation | RouteLocation,
replace = false,
redirectedFrom?: RouteLocation,
) {
const targetLocation = (pendingLocation = resolve(to));
const from = currentRoute;
const data: HistoryState | undefined = (to as RouteLocationOptions).state;
const force: boolean | undefined = (to as RouteLocationOptions).force;
const replace = (to as RouteLocationOptions).replace === true;
const from = currentLocation;
const data: HistoryState | undefined = (to as RawLocationOptions).state;
const shouldRedirect = getRedirectRecordIfShould(targetLocation);
if (shouldRedirect) {
@ -204,20 +195,17 @@ export function createRouter(options: RouterOptions): Router {
{
...shouldRedirect,
state: Object.assign({}, data, shouldRedirect.state),
force,
replace,
},
redirectedFrom || targetLocation
replace,
redirectedFrom || targetLocation,
);
}
const toLocation = targetLocation as RouteLocation;
const toLocation = targetLocation as RouteLocationNormalized;
toLocation.redirectedFrom = redirectedFrom;
if (!force && isSameRouteLocation(from, toLocation)) {
throw Error(
'路由错误:重复请求' + JSON.stringify({ to: toLocation, from })
);
if (isSameRouteLocation(from, toLocation)) {
throw Error('路由错误:重复请求' + JSON.stringify({ to: toLocation, from }));
}
return navigateTriggerBeforeGuards(toLocation, from)
@ -231,21 +219,21 @@ export function createRouter(options: RouterOptions): Router {
});
}
function getRedirectRecordIfShould(to: RouteLocation) {
function getRedirectRecordIfShould(
to: RouteLocationNormalized,
): Exclude<RawRouteLocation, string> | undefined {
const lastMatched = to.matched[to.matched.length - 1];
if (lastMatched?.redirect) {
const { redirect } = lastMatched;
let newTargetLocation =
typeof redirect === 'function' ? redirect(to) : redirect;
let newTargetLocation = typeof redirect === 'function' ? redirect(to) : redirect;
if (typeof newTargetLocation === 'string') {
newTargetLocation =
newTargetLocation.includes('?') || newTargetLocation.includes('#')
? locationAsObject(newTargetLocation)
: { path: newTargetLocation };
// @ts-expect-error 强制清空参数
newTargetLocation.params = {};
(newTargetLocation as any).params = {};
}
if (!('path' in newTargetLocation) && !('name' in newTargetLocation)) {
@ -254,27 +242,25 @@ export function createRouter(options: RouterOptions): Router {
return Object.assign(
{
query: to.query,
searchParams: to.searchParams,
hash: to.hash,
// path 存在的时候 清空 params
params: 'path' in newTargetLocation ? {} : to.params,
},
newTargetLocation
newTargetLocation,
);
}
}
async function navigateTriggerBeforeGuards(
to: RouteLocation,
from: RouteLocation
to: RouteLocationNormalized,
from: RouteLocationNormalized,
): Promise<any> {
let guards: ((...args: any[]) => Promise<any>)[] = [];
const canceledNavigationCheck = async (): Promise<any> => {
if (pendingLocation !== to) {
throw Error(
`路由错误重复导航from: ${from.fullPath}, to: ${to.fullPath}`
);
throw Error(`路由错误重复导航from: ${from.fullPath}, to: ${to.fullPath}`);
}
return Promise.resolve();
};
@ -288,31 +274,26 @@ export function createRouter(options: RouterOptions): Router {
}
if (beforeGuardsList.length > 0) guards.push(canceledNavigationCheck);
return guards.reduce(
(promise, guard) => promise.then(() => guard()),
Promise.resolve()
);
return guards.reduce((promise, guard) => promise.then(() => guard()), Promise.resolve());
} catch (err) {
throw err;
}
}
function finalizeNavigation(
toLocation: RouteLocation,
from: RouteLocation,
toLocation: RouteLocationNormalized,
from: RouteLocationNormalized,
isPush: boolean,
replace?: boolean,
data?: HistoryState
data?: HistoryState,
) {
// 重复导航
if (pendingLocation !== toLocation) {
throw Error(
`路由错误重复导航from: ${from.fullPath}, to: ${toLocation.fullPath}`
);
throw Error(`路由错误重复导航from: ${from.fullPath}, to: ${toLocation.fullPath}`);
}
// 如果不是第一次启动的话 只需要考虑 push
const isFirstNavigation = from === START_LOCATION_NORMALIZED;
const isFirstNavigation = from === START_LOCATION;
if (isPush) {
if (replace || isFirstNavigation) {
@ -322,7 +303,7 @@ export function createRouter(options: RouterOptions): Router {
}
}
currentRoute = toLocation;
currentLocation = toLocation;
// markAsReady();
}
@ -335,18 +316,15 @@ export function createRouter(options: RouterOptions): Router {
// 判断是否需要重定向
const shouldRedirect = getRedirectRecordIfShould(toLocation);
if (shouldRedirect) {
return pushOrRedirect(
Object.assign(shouldRedirect, { replace: true }),
toLocation
).catch(() => {});
return pushOrRedirect(shouldRedirect, true, toLocation).catch(() => {});
}
pendingLocation = toLocation;
const from = currentRoute;
const from = currentLocation;
// 触发路由守卫
navigateTriggerBeforeGuards(toLocation, from)
.catch(error => {
.catch((error) => {
if (info.delta) {
routerHistory.go(-info.delta, false);
}
@ -366,25 +344,30 @@ export function createRouter(options: RouterOptions): Router {
guard(toLocation, from);
}
})
.catch(noop);
.catch(() => {});
});
}
// init
setupListeners();
if (currentRoute === START_LOCATION_NORMALIZED) {
push(routerHistory.location).catch(err => {
if (currentLocation === START_LOCATION) {
push(routerHistory.location).catch((err) => {
console.warn('Unexpected error when starting the router:', err);
});
}
const go = (delta: number) => routerHistory.go(delta);
return {
options,
get options() {
return options;
},
get history() {
return routerHistory;
},
getCurrentLocation: () => currentLocation,
getCurrentRoute: () => currentRoute,
resolve,
addRoute,
removeRoute,
getRoutes,
@ -392,6 +375,9 @@ export function createRouter(options: RouterOptions): Router {
push,
replace,
back: () => go(-1),
forward: () => go(1),
go,
beforeRouteLeave: beforeGuards.add,
afterRouteChange: afterGuards.add,

View File

@ -1,9 +1,22 @@
import { type RouteSchema } from '@alilc/runtime-shared';
import { type ParsedQs } from 'qs';
import { type PathParserOptions } from './utils/path-parser';
import type {
RouteRecord as RouterRecordSpec,
RouteLocation,
PlainObject,
RawRouteLocation,
} from '@alilc/renderer-core';
import type { PathParserOptions } from './utils/path-parser';
export type RouteRecord = RouteSchema & PathParserOptions;
export interface RouteRecord extends RouterRecordSpec, PathParserOptions {
meta?: PlainObject;
redirect?:
| string
| RawRouteLocation
| ((to: RouteLocationNormalized) => string | RawRouteLocation);
children?: RouteRecord[];
}
export type LocationQuery = ParsedQs;
export interface RouteLocationNormalized extends RouteLocation {
matched: RouteRecord[];
}
export type RouteParams = Record<string, string | string[]>;

View File

@ -1,15 +1,13 @@
import {
type RouteLocation,
type RouteLocationRaw,
} from '@alilc/runtime-shared';
import type { RawRouteLocation } from '@alilc/renderer-core';
import type { RouteLocationNormalized } from '../types';
export function isRouteLocation(route: any): route is RouteLocationRaw {
export function isRouteLocation(route: any): route is RawRouteLocation {
return typeof route === 'string' || (route && typeof route === 'object');
}
export function isSameRouteLocation(
a: RouteLocation,
b: RouteLocation
a: RouteLocationNormalized,
b: RouteLocationNormalized,
): boolean {
const aLastIndex = a.matched.length - 1;
const bLastIndex = b.matched.length - 1;
@ -19,15 +17,18 @@ export function isSameRouteLocation(
aLastIndex === bLastIndex &&
a.matched[aLastIndex] === b.matched[bLastIndex] &&
isSameRouteLocationParams(a.params, b.params) &&
a.query?.toString() === b.query?.toString() &&
a.searchParams?.toString() === b.searchParams?.toString() &&
a.hash === b.hash
);
}
export function isSameRouteLocationParams(
a: RouteLocation['params'],
b: RouteLocation['params']
a: RouteLocationNormalized['params'],
b: RouteLocationNormalized['params'],
): boolean {
if (!a && !b) return true;
if (!a || !b) return false;
if (Object.keys(a).length !== Object.keys(b).length) return false;
for (const key in a) {
@ -38,14 +39,14 @@ export function isSameRouteLocationParams(
}
function isSameRouteLocationParamsValue(
a: string | readonly string[],
b: string | readonly string[]
a: undefined | string | string[],
b: undefined | string | string[],
): boolean {
return Array.isArray(a)
? isEquivalentArray(a, b)
: Array.isArray(b)
? isEquivalentArray(b, a)
: a === b;
? isEquivalentArray(b, a)
: a === b;
}
function isEquivalentArray<T>(a: readonly T[], b: readonly T[] | T): boolean {

View File

@ -1,28 +0,0 @@
import { type AnyObject } from '@alilc/runtime-shared';
import type { LocationQuery } from '../types';
/**
* casting numbers into strings, removing keys with an undefined value and replacing
* undefined with null in arrays
*
* undefined keys undefined null
*
* @param query - query object to normalize
* @returns a normalized query object
*/
export function normalizeQuery(query: AnyObject | undefined): LocationQuery {
const normalizedQuery: AnyObject = {};
for (const key in query) {
const value = query[key];
if (value !== undefined) {
normalizedQuery[key] = Array.isArray(value)
? value.map(v => (v == null ? null : '' + v))
: value == null
? value
: '' + value;
}
}
return normalizedQuery;
}

View File

@ -1,10 +1,10 @@
import { type AnyObject } from '@alilc/runtime-shared';
import { parse, stringify } from 'qs';
import { type LocationQuery } from '../types';
/**
* todo: replace to URL API
*/
export function parseURL(location: string) {
let path = '';
let query: LocationQuery = {};
let searchParams: URLSearchParams | undefined;
let searchString = '';
let hash = '';
@ -16,12 +16,9 @@ export function parseURL(location: string) {
if (searchPos > -1) {
path = location.slice(0, searchPos);
searchString = location.slice(
searchPos + 1,
hashPos > -1 ? hashPos : location.length
);
searchString = location.slice(searchPos + 1, hashPos > -1 ? hashPos : location.length);
query = parse(searchString);
searchParams = new URLSearchParams(searchString);
}
if (hashPos > -1) {
@ -35,16 +32,16 @@ export function parseURL(location: string) {
return {
fullPath: path + (searchString && '?') + searchString + hash,
path,
query,
searchParams,
hash,
};
}
export function stringifyURL(location: {
path: string;
query?: AnyObject;
searchParams?: URLSearchParams;
hash?: string;
}): string {
const query: string = location.query ? stringify(location.query) : '';
return location.path + (query && '?') + query + (location.hash || '');
const searchStr = location.searchParams ? location.searchParams.toString() : '';
return location.path + (searchStr && '?') + searchStr + (location.hash || '');
}

View File

@ -1,6 +1,10 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "dist"
}
"outDir": "dist",
"paths": {
"@alilc/*": ["runtime/*"]
}
},
"include": ["src"]
}

View File

@ -1,7 +1,7 @@
{
"compilerOptions": {
"declaration": true,
"lib": ["es2015", "dom"],
"lib": ["DOM", "ESNext", "DOM.Iterable"],
// Target latest version of ECMAScript.
"target": "esnext",
// Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'.