fix: lifeCycle events and router plugin

This commit is contained in:
1ncounter 2024-06-19 14:12:15 +08:00
parent 37d07f1db6
commit 61bc8e6c3d
34 changed files with 197 additions and 114 deletions

1
.gitignore vendored
View File

@ -13,7 +13,6 @@ pnpm-lock.yaml
deploy-space/packages
deploy-space/.env
# IDE
.vscode
.idea

View File

@ -1,7 +1,7 @@
import { createRenderer, type AppOptions } from '@alilc/lowcode-renderer-core';
import { type ComponentType } from 'react';
import { type Root, createRoot } from 'react-dom/client';
import { ApplicationView, RendererContext, extension } from '../app';
import { ApplicationView, RendererContext, boosts } from '../app';
export interface ReactAppOptions extends AppOptions {
faultComponent?: ComponentType<any>;
@ -17,7 +17,7 @@ export const createApp = async (options: ReactAppOptions) => {
// }
// extends boosts
extension.install(boostsManager);
boostsManager.extend(boosts.toExpose());
let root: Root | undefined;

View File

@ -1,6 +1,6 @@
import { createRenderer, type AppOptions } from '@alilc/lowcode-renderer-core';
import { FunctionComponent } from 'react';
import { type LowCodeComponentProps, createComponentBySchema } from '../runtime/component';
import { type LowCodeComponentProps, createComponentBySchema } from '../runtime/schema';
import { RendererContext } from '../app/context';
interface Render {

View File

@ -1,4 +1,4 @@
import { type Plugin, type IBoostsService } from '@alilc/lowcode-renderer-core';
import { type Plugin } from '@alilc/lowcode-renderer-core';
import { type ComponentType, type PropsWithChildren } from 'react';
export type WrapperComponent = ComponentType<PropsWithChildren<any>>;
@ -9,13 +9,13 @@ export interface OutletProps {
export type Outlet = ComponentType<OutletProps>;
export interface ReactRendererExtensionApi {
export interface ReactRendererBoostsApi {
addAppWrapper(appWrapper: WrapperComponent): void;
setOutlet(outlet: Outlet): void;
}
class ReactRendererExtension {
class ReactRendererBoosts {
private wrappers: WrapperComponent[] = [];
private outlet: Outlet | null = null;
@ -28,7 +28,7 @@ class ReactRendererExtension {
return this.outlet;
}
toExpose(): ReactRendererExtensionApi {
toExpose(): ReactRendererBoostsApi {
return {
addAppWrapper: (appWrapper) => {
if (appWrapper) this.wrappers.push(appWrapper);
@ -38,14 +38,10 @@ class ReactRendererExtension {
},
};
}
install(boostsService: IBoostsService) {
boostsService.extend(this.toExpose());
}
}
export const extension = new ReactRendererExtension();
export const boosts = new ReactRendererBoosts();
export function defineRendererPlugin(plugin: Plugin<ReactRendererExtensionApi>) {
export function defineRendererPlugin(plugin: Plugin<ReactRendererBoostsApi>) {
return plugin;
}

View File

@ -1,3 +1,3 @@
export * from './context';
export * from './extension';
export * from './boosts';
export * from './view';

View File

@ -1,12 +1,12 @@
import { useRenderContext } from './context';
import { getComponentByName } from '../runtime/component';
import { extension } from './extension';
import { getComponentByName } from '../runtime/schema';
import { boosts } from './boosts';
export function ApplicationView() {
const renderContext = useRenderContext();
const { schema } = renderContext;
const appWrappers = extension.getAppWrappers();
const Outlet = extension.getOutlet();
const appWrappers = boosts.getAppWrappers();
const Outlet = boosts.getOutlet();
if (!Outlet) return null;

View File

@ -5,4 +5,4 @@ export * from './router';
export type { Spec, ProCodeComponent, LowCodeComponent } from '@alilc/lowcode-shared';
export type { PackageLoader, CodeScope, Plugin } from '@alilc/lowcode-renderer-core';
export type { RendererExtends } from './app/extension';
export type { ReactRendererBoostsApi } from './app/boosts';

View File

@ -1,2 +1,3 @@
export * from './context';
export * from './plugin';
export type * from '@alilc/lowcode-renderer-router';

View File

@ -1,4 +1,4 @@
import { defineRendererPlugin } from '../app/extension';
import { defineRendererPlugin } from '../app/boosts';
import { LifecyclePhase } from '@alilc/lowcode-renderer-core';
import { createRouter, type RouterOptions } from '@alilc/lowcode-renderer-router';
import { createRouterView } from './routerView';
@ -29,13 +29,14 @@ export const routerPlugin = defineRendererPlugin({
const router = createRouter(routerConfig);
boosts.codeRuntime.getScope().set('router', router);
boosts.temporaryUse('router', router);
const RouterView = createRouterView(router);
boosts.addAppWrapper(RouterView);
boosts.setOutlet(RouteOutlet);
whenLifeCylePhaseChange(LifecyclePhase.Ready).then(() => {
whenLifeCylePhaseChange(LifecyclePhase.AfterInitPackageLoad).then(() => {
return router.isReady();
});
},

View File

@ -1,8 +1,8 @@
import { useMemo } from 'react';
import { useRenderContext } from '../app/context';
import { OutletProps } from '../app/extension';
import { OutletProps } from '../app/boosts';
import { useRouteLocation } from './context';
import { createComponentBySchema } from '../runtime/component';
import { createComponentBySchema } from '../runtime/schema';
export function RouteOutlet(props: OutletProps) {
const context = useRenderContext();

View File

@ -16,7 +16,7 @@ import { type ComponentType, type ReactInstance, useMemo, createElement } from '
import { useRenderContext } from '../app/context';
import { useReactiveStore } from './hooks/useReactiveStore';
import { useModel } from './context';
import { getComponentByName } from './component';
import { getComponentByName } from './schema';
export type ReactComponent = ComponentType<any>;
export type ReactWidget = IWidget<ReactComponent, ReactInstance>;

View File

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

View File

@ -1,2 +1,2 @@
export * from './component';
export * from './render';
export * from './schema';
export * from './components';

View File

@ -4,7 +4,7 @@ import { isValidElementType } from 'react-is';
import { useRenderContext } from '../app/context';
import { reactiveStateFactory } from './reactiveState';
import { dataSourceCreator } from './dataSource';
import { type ReactComponent, type ReactWidget, createElementByWidget } from './render';
import { type ReactComponent, type ReactWidget, createElementByWidget } from './components';
import { ModelContextProvider } from './context';
import { appendExternalStyle } from '../utils/element';

View File

@ -1,5 +1,8 @@
/**
* @vitest-environment jsdom
*/
import { describe, it, expect } from 'vitest';
import { CodeScope } from '../../../src/parts/code-runtime';
import { CodeScope } from '../../../src/services/code-runtime';
describe('CodeScope', () => {
it('should return initial values', () => {
@ -18,9 +21,8 @@ describe('CodeScope', () => {
it('inject should not overwrite existing values without force', () => {
const initValue = { a: 1 };
const scope = new CodeScope(initValue);
scope.set('a', 2);
expect(scope.value.a).toBe(1);
scope.set('a', 3, true);
expect(scope.value.a).not.toBe(2);
scope.set('a', 3);
expect(scope.value.a).toBe(3);
});

View File

@ -0,0 +1,36 @@
import { describe, it, expect } from 'vitest';
import { LifeCycleService, LifecyclePhase } from '../../src/services/lifeCycleService';
const sleep = () => new Promise((r) => setTimeout(r, 500));
describe('LifeCycleService', () => {
it('it works', async () => {
let result = '';
const lifeCycle = new LifeCycleService();
lifeCycle.when(LifecyclePhase.Ready).then(() => {
result += '1';
});
lifeCycle.when(LifecyclePhase.Ready).finally(() => {
result += '2';
});
lifeCycle.when(LifecyclePhase.AfterInitPackageLoad).then(() => {
result += '3';
});
lifeCycle.when(LifecyclePhase.AfterInitPackageLoad).finally(() => {
result += '4';
});
lifeCycle.phase = LifecyclePhase.Ready;
await sleep();
expect(result).toEqual('12');
lifeCycle.phase = LifecyclePhase.AfterInitPackageLoad;
await sleep();
expect(result).toEqual('1234');
});
});

View File

@ -31,7 +31,7 @@ export class RendererMain<RenderObject> {
@IBoostsService private boostsService: IBoostsService,
@ILifeCycleService private lifeCycleService: ILifeCycleService,
) {
this.lifeCycleService.when(LifecyclePhase.OptionsResolved).finally(async () => {
this.lifeCycleService.when(LifecyclePhase.OptionsResolved).then(async () => {
const renderContext = {
schema: this.schemaService,
packageManager: this.packageManagementService,
@ -42,8 +42,6 @@ export class RendererMain<RenderObject> {
this.renderObject = await this.adapter(renderContext);
await this.packageManagementService.loadPackages(this.initOptions.packages ?? []);
this.lifeCycleService.phase = LifecyclePhase.Ready;
});
}
@ -60,13 +58,19 @@ export class RendererMain<RenderObject> {
this.codeRuntimeService.initialize(options.codeRuntime ?? {});
this.extensionHostService.registerPlugin(plugins);
this.lifeCycleService.phase = LifecyclePhase.OptionsResolved;
await this.lifeCycleService.when(LifecyclePhase.Ready);
await this.extensionHostService.registerPlugin(plugins);
await this.packageManagementService.loadPackages(this.initOptions.packages ?? []);
this.lifeCycleService.phase = LifecyclePhase.AfterInitPackageLoad;
}
async getApp(): Promise<RendererApplication<RenderObject>> {
await this.lifeCycleService.when(LifecyclePhase.Ready);
await this.lifeCycleService.when(LifecyclePhase.AfterInitPackageLoad);
// construct application
return Object.freeze<RendererApplication<RenderObject>>({
@ -79,8 +83,7 @@ export class RendererMain<RenderObject> {
...this.renderObject,
use: (plugin) => {
this.extensionHostService.registerPlugin(plugin);
return this.extensionHostService.doSetupPlugin(plugin);
return this.extensionHostService.registerPlugin(plugin);
},
});
}

View File

@ -4,7 +4,7 @@ import { ICodeRuntimeService } from '../code-runtime';
import { IRuntimeUtilService } from '../runtimeUtilService';
import { IRuntimeIntlService } from '../runtimeIntlService';
export type IBoosts<Extends> = IBoostsApi & Extends;
export type IBoosts<Extends> = IBoostsApi & Extends & { [key: string]: any };
export interface IBoostsApi {
readonly codeRuntime: ICodeRuntimeService;
@ -12,6 +12,10 @@ export interface IBoostsApi {
readonly intl: Pick<IRuntimeIntlService, 't' | 'setLocale' | 'getLocale' | 'addTranslations'>;
readonly util: Pick<IRuntimeUtilService, 'add' | 'remove'>;
/**
* boosts 便使
*/
temporaryUse(name: string, value: any): void;
}
/**
@ -43,6 +47,9 @@ export class BoostsService implements IBoostsService {
codeRuntime: this.codeRuntimeService,
intl: this.runtimeIntlService,
util: this.runtimeUtilService,
temporaryUse: (name, value) => {
this.extend(name, value);
},
};
}
@ -55,6 +62,8 @@ export class BoostsService implements IBoostsService {
} else {
if (!this.extendsValue[name]) {
this.extendsValue[name] = value;
} else {
console.warn(`${name} is exist`);
}
}
} else if (isObject(name)) {

View File

@ -3,8 +3,6 @@ import { type Plugin, type PluginContext } from './plugin';
import { IBoostsService } from './boosts';
import { IPackageManagementService } from '../package';
import { ISchemaService } from '../schema';
import { type RenderAdapter } from './render';
import { IComponentTreeModelService } from '../model';
import { ILifeCycleService, LifecyclePhase } from '../lifeCycleService';
interface IPluginRuntime extends Plugin {
@ -12,9 +10,7 @@ interface IPluginRuntime extends Plugin {
}
export interface IExtensionHostService {
registerPlugin(plugin: Plugin | Plugin[]): void;
doSetupPlugin(plugin: Plugin): Promise<void>;
registerPlugin(plugin: Plugin | Plugin[]): Promise<void>;
getPlugin(name: string): Plugin | undefined;
@ -50,15 +46,9 @@ export class ExtensionHostService implements IExtensionHostService {
return this.lifeCycleService.when(phase);
},
};
this.lifeCycleService.when(LifecyclePhase.OptionsResolved).then(async () => {
for (const plugin of this.pluginRuntimes) {
await this.doSetupPlugin(plugin);
}
});
}
registerPlugin(plugins: Plugin | Plugin[]) {
async registerPlugin(plugins: Plugin | Plugin[]) {
plugins = Array.isArray(plugins) ? plugins : [plugins];
for (const plugin of plugins) {
@ -67,19 +57,17 @@ export class ExtensionHostService implements IExtensionHostService {
continue;
}
this.pluginRuntimes.push({
...plugin,
status: 'ready',
});
await this.doSetupPlugin(plugin);
}
}
async doSetupPlugin(plugin: Plugin) {
private async doSetupPlugin(plugin: Plugin) {
const pluginRuntime = plugin as IPluginRuntime;
if (!this.pluginRuntimes.some((item) => item.name !== pluginRuntime.name)) {
return;
}
this.pluginRuntimes.push({
...pluginRuntime,
status: 'ready',
});
const isSetup = (name: string) => {
const setupPlugins = this.pluginRuntimes.filter((item) => item.status === 'setup');

View File

@ -7,11 +7,7 @@ export const enum LifecyclePhase {
Ready = 3,
BeforeMount = 4,
Mounted = 5,
BeforeUnmount = 6,
AfterInitPackageLoad = 4,
}
export interface ILifeCycleService {
@ -40,7 +36,7 @@ export class LifeCycleService implements ILifeCycleService {
}
set phase(value: LifecyclePhase) {
if (value < this.phase) {
if (value < this._phase) {
throw new Error('Lifecycle cannot go backwards');
}

View File

@ -68,7 +68,7 @@ export class PackageManagementService implements IPackageManagementService {
@ISchemaService private schemaService: ISchemaService,
@ILifeCycleService private lifeCycleService: ILifeCycleService,
) {
this.lifeCycleService.when(LifecyclePhase.Ready).then(() => {
this.lifeCycleService.when(LifecyclePhase.AfterInitPackageLoad).then(() => {
const componentsMaps = this.schemaService.get('componentsMap');
this.resolveComponentMaps(componentsMaps);
});
@ -85,7 +85,6 @@ export class PackageManagementService implements IPackageManagementService {
if (!isProCodeComponentPackage(item)) continue;
const normalized = this.normalizePackage(item);
await this.loadPackageByNormalized(normalized);
}
}

View File

@ -5,6 +5,7 @@ import {
type Spec,
type Locale,
type Translations,
platformLocale,
} from '@alilc/lowcode-shared';
import { ILifeCycleService, LifecyclePhase } from './lifeCycleService';
import { ICodeRuntimeService } from './code-runtime';
@ -34,7 +35,7 @@ export const IRuntimeIntlService = createDecorator<IRuntimeIntlService>('IRuntim
export class RuntimeIntlService implements IRuntimeIntlService {
private intl: Intl = new Intl();
public locale: string = navigator.language;
public locale: string = platformLocale;
constructor(
@ILifeCycleService private lifeCycleService: ILifeCycleService,

View File

@ -23,12 +23,11 @@ export class RuntimeUtilService implements IRuntimeUtilService {
@ILifeCycleService private lifeCycleService: ILifeCycleService,
@ISchemaService private schemaService: ISchemaService,
) {
this.lifeCycleService.when(LifecyclePhase.Ready).then(() => {
this.lifeCycleService.when(LifecyclePhase.AfterInitPackageLoad).then(() => {
const utils = this.schemaService.get('utils') ?? [];
for (const util of utils) {
this.add(util);
}
this.toExpose();
});
}

View File

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

View File

@ -1,23 +0,0 @@
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { EventEmitter } from '../../src';
describe('hookable', () => {
let eventEmitter: EventEmitter;
beforeEach(() => {
eventEmitter = new EventEmitter();
});
it('on', async () => {
const spy = vi.fn();
eventEmitter.on('test', spy);
await eventEmitter.emit('test');
expect(spy).toBeCalled();
});
it('prependListener', () => {
// const spy = vi.fn();
// expect(spy).toC
});
});

View File

@ -0,0 +1,56 @@
import { describe, it, expect } from 'vitest';
import { Barrier } from '../../src';
describe('Barrier', () => {
it('waits for barrier to open', async () => {
const barrier = new Barrier();
setTimeout(() => {
barrier.open();
}, 500); // Simulate opening the barrier after 500ms
const start = Date.now();
await barrier.wait(); // Async operation should await here
const duration = Date.now() - start;
expect(barrier.isOpen()).toBeTruthy();
expect(duration).toBeGreaterThanOrEqual(500); // Ensures we waited for at least 500ms
});
it('mutiple', async () => {
let result = '';
const b1 = new Barrier();
const b2 = new Barrier();
async function run1() {
await b1.wait();
}
async function run2() {
await b2.wait();
}
run1().then(() => {
result += 1;
});
run1().finally(() => {
result += 2;
});
run2().then(() => {
result += 3;
});
run2().finally(() => {
result += 4;
});
b1.open();
await run1();
b2.open();
await run2();
expect(result).toBe('1234');
});
});

View File

@ -5,6 +5,13 @@
"main": "dist/low-code-shared.cjs",
"module": "dist/low-code-shared.js",
"types": "dist/index.d.ts",
"exports": {
".": {
"import": "./dist/low-code-shared.js",
"require": "./dist/low-code-shared.cjs",
"types": "./dist/index.d.ts"
}
},
"files": [
"dist",
"src",

View File

@ -15,7 +15,7 @@ export type Constructor<T = any> = new (...args: any[]) => T;
export function createDecorator<T>(serviceId: string): ServiceIdentifier<T> {
const id = <any>(
function (target: Constructor, targetKey: string, indexOrPropertyDescriptor: any): any {
return set(serviceId)(target, targetKey, indexOrPropertyDescriptor);
return inject(serviceId)(target, targetKey, indexOrPropertyDescriptor);
}
);
id.toString = () => serviceId;

View File

@ -1,6 +1,7 @@
import { createIntl, createIntlCache, type IntlShape as IntlFormatter } from '@formatjs/intl';
import { mapKeys } from 'lodash-es';
import { signal, computed, effect, type Signal, type ComputedSignal } from '../signals';
import { platformLocale } from '../utils';
export { IntlFormatter };
@ -14,7 +15,7 @@ export class Intl {
private currentMessage: ComputedSignal<Translations>;
private intlShape: IntlFormatter;
constructor(defaultLocale: string = navigator.language, messages: LocaleTranslationsRecord = {}) {
constructor(defaultLocale: string = platformLocale, messages: LocaleTranslationsRecord = {}) {
if (defaultLocale) {
defaultLocale = nomarlizeLocale(defaultLocale);
} else {

View File

@ -1,15 +1,15 @@
const userAgent: string = navigator.userAgent;
const userAgent: string = window.navigator.userAgent;
export const isWindows = userAgent.indexOf('Windows') >= 0;
export const isMacintosh = userAgent.indexOf('Macintosh') >= 0;
export const isLinux = userAgent.indexOf('Linux') >= 0;
export const isIOS =
(isMacintosh || userAgent.indexOf('iPad') >= 0 || userAgent.indexOf('iPhone') >= 0) &&
!!navigator.maxTouchPoints &&
navigator.maxTouchPoints > 0;
!!window.navigator.maxTouchPoints &&
window.navigator.maxTouchPoints > 0;
export const isMobile = userAgent?.indexOf('Mobi') >= 0;
export const locale: string | undefined = undefined;
export const platformLocale = navigator.language;
export const platformLocale = window.navigator.language;
export const enum Platform {
Web,

View File

@ -0,0 +1,3 @@
import { defineProject } from 'vitest/config';
export default defineProject({});

View File

@ -1,5 +1,5 @@
packages:
- 'packages/*'
- 'playground'
- 'playground/*'
- 'docs'
- 'scripts'

View File

@ -39,5 +39,11 @@
},
"types": ["vite/client", "vitest/globals", "node"]
},
"exclude": ["**/dist", "node_modules"]
"include": [
"packages/global.d.ts",
"packages/*/src",
"packages/*/__tests__",
"playground/*/src",
"scripts/*"
]
}

View File

@ -5,9 +5,12 @@ export default defineWorkspace([
{
test: {
include: ['**/__tests__/**/*.spec.{ts?(x),js?(x)}'],
alias: {
'@alilc/lowcode-shared': 'packages/shared',
},
name: 'unit test',
environment: 'jsdom',
globals: true
}
environmentMatchGlobs: [['packages/**', 'jsdom']],
globals: true,
},
},
])
]);