diff --git a/core/lifecycle/src/LifycycleUtil.ts b/core/lifecycle/src/LifycycleUtil.ts index ee859a0a..2388d81a 100644 --- a/core/lifecycle/src/LifycycleUtil.ts +++ b/core/lifecycle/src/LifycycleUtil.ts @@ -98,4 +98,9 @@ export class LifecycleUtil(LIFECYCLE_HOOK); } + + static getStaticLifecycleHook(hookName: LifecycleHookName, clazz: EggProtoImplClass) { + const LIFECYCLE_HOOK = Symbol.for(`EggPrototype#Lifecycle${hookName}`); + return MetadataUtil.getMetaData(LIFECYCLE_HOOK, clazz); + } } diff --git a/core/lifecycle/src/decorator/index.ts b/core/lifecycle/src/decorator/index.ts index 3529287c..84d8eba5 100644 --- a/core/lifecycle/src/decorator/index.ts +++ b/core/lifecycle/src/decorator/index.ts @@ -10,9 +10,22 @@ function createLifecycle(hookName: LifecycleHookName) { }; } +function createStaticLifecycle(hookName: LifecycleHookName) { + return () => { + return function(target: EggProtoImplClass, methodName: string) { + if (typeof target !== 'function') { + throw new Error(`${hookName} must be a static function`); + } + LifecycleUtil.setLifecycleHook(methodName, hookName, target); + }; + }; +} + + export const LifecyclePostConstruct = createLifecycle('postConstruct'); export const LifecyclePreInject = createLifecycle('preInject'); export const LifecyclePostInject = createLifecycle('postInject'); export const LifecycleInit = createLifecycle('init'); export const LifecyclePreDestroy = createLifecycle('preDestroy'); export const LifecycleDestroy = createLifecycle('destroy'); +export const LifecyclePreLoad = createStaticLifecycle('preLoad'); diff --git a/core/metadata/src/impl/ModuleLoadUnit.ts b/core/metadata/src/impl/ModuleLoadUnit.ts index b9816e3d..4b724ea6 100644 --- a/core/metadata/src/impl/ModuleLoadUnit.ts +++ b/core/metadata/src/impl/ModuleLoadUnit.ts @@ -19,7 +19,7 @@ import type { import { Graph, GraphNode, MapUtil } from '@eggjs/tegg-common-util'; import { PrototypeUtil, QualifierUtil } from '@eggjs/core-decorator'; import { FrameworkErrorFormater } from 'egg-errors'; -import { IdenticalUtil } from '@eggjs/tegg-lifecycle'; +import { IdenticalUtil, LifecycleUtil } from '@eggjs/tegg-lifecycle'; import { EggPrototypeFactory } from '../factory/EggPrototypeFactory'; import { LoadUnitFactory } from '../factory/LoadUnitFactory'; import { EggPrototypeCreatorFactory } from '../factory/EggPrototypeCreatorFactory'; @@ -182,6 +182,16 @@ export class ModuleLoadUnit implements LoadUnit { return clazzList; } + async preLoad() { + const clazzList = this.loader.load(); + for (const protoClass of clazzList) { + const fnName = LifecycleUtil.getStaticLifecycleHook('preLoad', protoClass); + if (fnName) { + await protoClass[fnName]?.(); + } + } + } + async init() { const clazzList = this.loadClazz(); const protoGraph = new ModuleGraph(clazzList, this.unitPath, this.name); diff --git a/core/types/lifecycle/EggObjectLifecycle.ts b/core/types/lifecycle/EggObjectLifecycle.ts index 7db36e47..a2db352a 100644 --- a/core/types/lifecycle/EggObjectLifecycle.ts +++ b/core/types/lifecycle/EggObjectLifecycle.ts @@ -4,6 +4,10 @@ import type { EggObject, EggObjectLifeCycleContext } from '@eggjs/tegg-runtime'; * lifecycle hook interface for egg object */ export interface EggObjectLifecycle { + /** + * call before project load + */ + preLoad?(ctx: EggObjectLifeCycleContext): Promise; /** * call after construct */ diff --git a/core/types/lifecycle/LifecycleHook.ts b/core/types/lifecycle/LifecycleHook.ts index 3cac803c..44d6aa49 100644 --- a/core/types/lifecycle/LifecycleHook.ts +++ b/core/types/lifecycle/LifecycleHook.ts @@ -4,6 +4,7 @@ export interface LifecycleContext { } export interface LifecycleObject extends IdenticalObject { + preLoad?(): Promise; init?(ctx: T): Promise; destroy?(ctx: T): Promise; } diff --git a/standalone/standalone/src/EggModuleLoader.ts b/standalone/standalone/src/EggModuleLoader.ts index c35b8b88..ba2e23b8 100644 --- a/standalone/standalone/src/EggModuleLoader.ts +++ b/standalone/standalone/src/EggModuleLoader.ts @@ -10,9 +10,9 @@ export class EggModuleLoader { this.moduleReferences = moduleReferences; } - private buildAppGraph(loaderCache: Map) { + private static generateAppGraph(loaderCache: Map, moduleReferences: readonly ModuleReference[]) { const appGraph = new AppGraph(); - for (const moduleConfig of this.moduleReferences) { + for (const moduleConfig of moduleReferences) { const modulePath = moduleConfig.path; const moduleNode = new ModuleNode(moduleConfig); const loader = LoaderFactory.createLoader(modulePath, EggLoadUnitType.MODULE); @@ -27,10 +27,10 @@ export class EggModuleLoader { return appGraph; } - async load(): Promise { + private static async generateLoadUnits(moduleReferences: readonly ModuleReference[]) { const loadUnits: LoadUnit[] = []; const loaderCache = new Map(); - const appGraph = this.buildAppGraph(loaderCache); + const appGraph = EggModuleLoader.generateAppGraph(loaderCache, moduleReferences); appGraph.sort(); const moduleConfigList = appGraph.moduleConfigList; for (const moduleConfig of moduleConfigList) { @@ -40,6 +40,16 @@ export class EggModuleLoader { loadUnits.push(loadUnit); } return loadUnits; - // return loadUnits; + } + + async load(): Promise { + return await EggModuleLoader.generateLoadUnits(this.moduleReferences); + } + + static async preLoad(moduleReferences: readonly ModuleReference[]): Promise { + const loads = await EggModuleLoader.generateLoadUnits(moduleReferences); + for (const load of loads) { + await load.preLoad?.(); + } } } diff --git a/standalone/standalone/src/Runner.ts b/standalone/standalone/src/Runner.ts index 22c14f91..8c57d155 100644 --- a/standalone/standalone/src/Runner.ts +++ b/standalone/standalone/src/Runner.ts @@ -85,11 +85,7 @@ export class Runner { this.cwd = cwd; this.env = options?.env; this.name = options?.name; - const moduleDirs = (options?.dependencies || []).concat(this.cwd); - this.moduleReferences = moduleDirs.reduce((list, baseDir) => { - const module = typeof baseDir === 'string' ? { baseDir } : baseDir; - return list.concat(...ModuleConfigUtil.readModuleReference(module.baseDir, module)); - }, [] as readonly ModuleReference[]); + this.moduleReferences = Runner.getModuleReferences(this.cwd, options?.dependencies); this.moduleConfigs = {}; this.innerObjects = { moduleConfigs: [{ @@ -189,6 +185,19 @@ export class Runner { return [ standaloneLoadUnit, ...loadUnits ]; } + static getModuleReferences(cwd: string, dependencies?: RunnerOptions['dependencies']) { + const moduleDirs = (dependencies || []).concat(cwd); + return moduleDirs.reduce((list, baseDir) => { + const module = typeof baseDir === 'string' ? { baseDir } : baseDir; + return list.concat(...ModuleConfigUtil.readModuleReference(module.baseDir, module)); + }, [] as readonly ModuleReference[]); + } + + static async preLoad(cwd: string, dependencies?: RunnerOptions['dependencies']) { + const moduleReferences = Runner.getModuleReferences(cwd, dependencies); + await EggModuleLoader.preLoad(moduleReferences); + } + async init() { this.loadUnits = await this.load(); const instances: LoadUnitInstance[] = []; diff --git a/standalone/standalone/src/main.ts b/standalone/standalone/src/main.ts index 7db7e781..6b6a3404 100644 --- a/standalone/standalone/src/main.ts +++ b/standalone/standalone/src/main.ts @@ -1,5 +1,14 @@ import { Runner, RunnerOptions } from './Runner'; +export async function preLoad(cwd: string, dependencies?: RunnerOptions['dependencies']) { + try { + await Runner.preLoad(cwd, dependencies); + } catch (e) { + e.message = `[tegg/standalone] bootstrap standalone preLoad failed: ${e.message}`; + throw e; + } +} + export async function main(cwd: string, options?: RunnerOptions): Promise { const runner = new Runner(cwd, options); try { diff --git a/standalone/standalone/test/fixtures/lifecycle/foo.ts b/standalone/standalone/test/fixtures/lifecycle/foo.ts new file mode 100644 index 00000000..ac5403cc --- /dev/null +++ b/standalone/standalone/test/fixtures/lifecycle/foo.ts @@ -0,0 +1,69 @@ +import { + SingletonProto, + LifecyclePreLoad, + LifecyclePostConstruct, + LifecyclePreInject, + LifecyclePostInject, + LifecycleInit, + LifecyclePreDestroy, + LifecycleDestroy, +} from '@eggjs/tegg'; +import { Runner, MainRunner } from '@eggjs/tegg/standalone'; + +@Runner() +@SingletonProto() +export class Foo implements MainRunner { + + static staticCalled: string[] = []; + + getLifecycleCalled() { + return Foo.staticCalled; + } + + @LifecyclePreLoad() + static async _preLoad() { + Foo.staticCalled.push('preLoad'); + } + + constructor() { + Foo.staticCalled.push('construct'); + } + + @LifecyclePostConstruct() + protected async _postConstruct() { + Foo.staticCalled.push('postConstruct'); + } + + @LifecyclePreInject() + protected async _preInject() { + Foo.staticCalled.push('preInject'); + } + + @LifecyclePostInject() + protected async _postInject() { + Foo.staticCalled.push('postInject'); + } + + protected async init() { + Foo.staticCalled.push('init should not called'); + } + + @LifecycleInit() + protected async _init() { + Foo.staticCalled.push('init'); + } + + @LifecyclePreDestroy() + protected async _preDestroy() { + Foo.staticCalled.push('preDestroy'); + } + + @LifecycleDestroy() + protected async _destroy() { + Foo.staticCalled.push('destroy'); + } + + async main(): Promise { + return Foo.staticCalled; + } +} diff --git a/standalone/standalone/test/fixtures/lifecycle/package.json b/standalone/standalone/test/fixtures/lifecycle/package.json new file mode 100644 index 00000000..75761a44 --- /dev/null +++ b/standalone/standalone/test/fixtures/lifecycle/package.json @@ -0,0 +1,6 @@ +{ + "name": "lifecycle", + "eggModule": { + "name": "lifecycle" + } +} diff --git a/standalone/standalone/test/index.test.ts b/standalone/standalone/test/index.test.ts index 1552693c..eff49a5e 100644 --- a/standalone/standalone/test/index.test.ts +++ b/standalone/standalone/test/index.test.ts @@ -1,8 +1,8 @@ -import { strict as assert } from 'node:assert'; +import { strict as assert, deepStrictEqual } from 'node:assert'; import path from 'node:path'; import fs from 'node:fs/promises'; import { ModuleConfig, ModuleConfigs } from '@eggjs/tegg/helper'; -import { main, StandaloneContext, Runner } from '..'; +import { main, StandaloneContext, Runner, preLoad } from '..'; import { crosscutAdviceParams, pointcutAdviceParams } from './fixtures/aop-module/Hello'; import { Foo } from './fixtures/dal-module/src/Foo'; @@ -276,4 +276,28 @@ describe('standalone/standalone/test/index.test.ts', () => { assert.equal(result, '{"body":{"fullname":"mock fullname","skipDependencies":true,"registryName":"ok"}}'); }); }); + + describe('lifecycle', () => { + const fixturePath = path.join(__dirname, './fixtures/lifecycle'); + let Foo; + + beforeEach(() => { + delete require.cache[require.resolve(path.join(fixturePath, './foo'))]; + // eslint-disable-next-line @typescript-eslint/no-var-requires + Foo = require(path.join(fixturePath, './foo')).Foo; + }); + + it('should work', async () => { + await preLoad(fixturePath); + await main(fixturePath); + deepStrictEqual(Foo.staticCalled, [ + 'preLoad', + 'construct', + 'postConstruct', + 'preInject', + 'postInject', + 'init', + ]); + }); + }); });