diff --git a/standalone/standalone/package.json b/standalone/standalone/package.json index 0a0394f1..21dd1c1a 100644 --- a/standalone/standalone/package.json +++ b/standalone/standalone/package.json @@ -46,7 +46,8 @@ "@eggjs/tegg-lifecycle": "^3.17.0", "@eggjs/tegg-loader": "^3.17.0", "@eggjs/tegg-metadata": "^3.17.0", - "@eggjs/tegg-runtime": "^3.17.0" + "@eggjs/tegg-runtime": "^3.17.0", + "@eggjs/tegg-aop-runtime": "^3.17.0" }, "publishConfig": { "access": "public" diff --git a/standalone/standalone/src/ConfigSourceLoadUnitHook.ts b/standalone/standalone/src/ConfigSourceLoadUnitHook.ts index 794224af..ca83049f 100644 --- a/standalone/standalone/src/ConfigSourceLoadUnitHook.ts +++ b/standalone/standalone/src/ConfigSourceLoadUnitHook.ts @@ -5,6 +5,10 @@ import { } from '@eggjs/tegg'; import { ConfigSourceQualifier, ConfigSourceQualifierAttribute } from './ConfigSource'; +/** + * Hook for inject moduleConfig. + * Add default qualifier value is current module name. + */ export class ConfigSourceLoadUnitHook implements LifecycleHook { async preCreate(ctx: LoadUnitLifecycleContext, loadUnit: LoadUnit): Promise { const classList = ctx.loader.load(); diff --git a/standalone/standalone/src/LoadUnitInnerClassHook.ts b/standalone/standalone/src/LoadUnitInnerClassHook.ts new file mode 100644 index 00000000..6fddd4a5 --- /dev/null +++ b/standalone/standalone/src/LoadUnitInnerClassHook.ts @@ -0,0 +1,26 @@ +import { LifecycleHook } from '@eggjs/tegg-lifecycle'; +import { + EggPrototypeFactory, + LoadUnit, + LoadUnitLifecycleContext, + EggPrototypeCreatorFactory, +} from '@eggjs/tegg-metadata'; +import { EggProtoImplClass } from '@eggjs/tegg'; +import { EggObjectFactory } from '@eggjs/tegg-dynamic-inject-runtime'; + +const INNER_CLASS_LIST = [ + EggObjectFactory, +]; + +export class LoadUnitInnerClassHook implements LifecycleHook { + async postCreate(_: LoadUnitLifecycleContext, loadUnit: LoadUnit): Promise { + if (loadUnit.type === 'StandaloneLoadUnitType') { + for (const clazz of INNER_CLASS_LIST) { + const protos = await EggPrototypeCreatorFactory.createProto(clazz as EggProtoImplClass, loadUnit); + for (const proto of protos) { + EggPrototypeFactory.instance.registerPrototype(proto, loadUnit); + } + } + } + } +} diff --git a/standalone/standalone/src/Runner.ts b/standalone/standalone/src/Runner.ts index 65738146..52753732 100644 --- a/standalone/standalone/src/Runner.ts +++ b/standalone/standalone/src/Runner.ts @@ -1,19 +1,22 @@ import { ModuleConfigUtil, ModuleReference, RuntimeConfig } from '@eggjs/tegg-common-util'; import { - EggPrototype, + EggPrototype, EggPrototypeLifecycleUtil, LoadUnit, LoadUnitFactory, LoadUnitLifecycleUtil, } from '@eggjs/tegg-metadata'; import { ContextHandler, - EggContainerFactory, EggContext, + EggContainerFactory, EggContext, EggObjectLifecycleUtil, LoadUnitInstance, LoadUnitInstanceFactory, ModuleLoadUnitInstance, } from '@eggjs/tegg-runtime'; import { EggProtoImplClass, PrototypeUtil } from '@eggjs/tegg'; import { StandaloneUtil, MainRunner } from '@eggjs/tegg/standalone'; +import { CrosscutAdviceFactory } from '@eggjs/tegg/aop'; +import { EggObjectAopHook, EggPrototypeCrossCutHook, LoadUnitAopHook } from '@eggjs/tegg-aop-runtime'; + import { EggModuleLoader } from './EggModuleLoader'; import { InnerObject, StandaloneLoadUnit, StandaloneLoadUnitType } from './StandaloneLoadUnit'; import { StandaloneContext } from './StandaloneContext'; @@ -21,6 +24,7 @@ import { StandaloneContextHandler } from './StandaloneContextHandler'; import { ModuleConfigHolder, ModuleConfigs } from './ModuleConfigs'; import { ConfigSourceQualifierAttribute } from './ConfigSource'; import { ConfigSourceLoadUnitHook } from './ConfigSourceLoadUnitHook'; +import { LoadUnitInnerClassHook } from './LoadUnitInnerClassHook'; export interface RunnerOptions { /** @@ -40,6 +44,12 @@ export class Runner { private runnerProto: EggPrototype; private configSourceEggPrototypeHook: ConfigSourceLoadUnitHook; + private readonly loadUnitInnerClassHook: LoadUnitInnerClassHook; + private readonly crosscutAdviceFactory: CrosscutAdviceFactory; + private readonly loadUnitAopHook: LoadUnitAopHook; + private readonly eggPrototypeCrossCutHook: EggPrototypeCrossCutHook; + private readonly eggObjectAopHook: EggObjectAopHook; + loadUnits: LoadUnit[] = []; loadUnitInstances: LoadUnitInstance[] = []; innerObjects: Record; @@ -96,6 +106,20 @@ export class Runner { this.loadUnitLoader = new EggModuleLoader(this.moduleReferences); const configSourceEggPrototypeHook = new ConfigSourceLoadUnitHook(); LoadUnitLifecycleUtil.registerLifecycle(configSourceEggPrototypeHook); + + this.loadUnitInnerClassHook = new LoadUnitInnerClassHook(); + LoadUnitLifecycleUtil.registerLifecycle(this.loadUnitInnerClassHook); + + // TODO refactor with egg module + // aop runtime + this.crosscutAdviceFactory = new CrosscutAdviceFactory(); + this.loadUnitAopHook = new LoadUnitAopHook(this.crosscutAdviceFactory); + this.eggPrototypeCrossCutHook = new EggPrototypeCrossCutHook(this.crosscutAdviceFactory); + this.eggObjectAopHook = new EggObjectAopHook(); + + EggPrototypeLifecycleUtil.registerLifecycle(this.eggPrototypeCrossCutHook); + LoadUnitLifecycleUtil.registerLifecycle(this.loadUnitAopHook); + EggObjectLifecycleUtil.registerLifecycle(this.eggObjectAopHook); } async init() { @@ -165,5 +189,19 @@ export class Runner { if (this.configSourceEggPrototypeHook) { LoadUnitLifecycleUtil.deleteLifecycle(this.configSourceEggPrototypeHook); } + + if (this.loadUnitInnerClassHook) { + LoadUnitLifecycleUtil.deleteLifecycle(this.loadUnitInnerClassHook); + } + + if (this.eggPrototypeCrossCutHook) { + EggPrototypeLifecycleUtil.deleteLifecycle(this.eggPrototypeCrossCutHook); + } + if (this.loadUnitAopHook) { + LoadUnitLifecycleUtil.deleteLifecycle(this.loadUnitAopHook); + } + if (this.eggObjectAopHook) { + EggObjectLifecycleUtil.deleteLifecycle(this.eggObjectAopHook); + } } } diff --git a/standalone/standalone/test/fixtures/aop-module/Hello.ts b/standalone/standalone/test/fixtures/aop-module/Hello.ts new file mode 100644 index 00000000..08d16d83 --- /dev/null +++ b/standalone/standalone/test/fixtures/aop-module/Hello.ts @@ -0,0 +1,182 @@ +import { + ContextProto, + Inject, + SingletonProto, +} from '@eggjs/tegg'; +import { + Advice, + AdviceContext, + Crosscut, + IAdvice, + Pointcut, + PointcutType, +} from '@eggjs/tegg/aop'; +import assert from 'assert'; + +export interface CallTraceMsg { + className: string; + methodName: string; + id: number; + name: string; + result?: string; + adviceParams?: any; +} + +@SingletonProto() +export class CallTrace { + msgs: Array = []; + + addMsg(msg: CallTraceMsg) { + this.msgs.push(msg); + } +} + +export const pointcutAdviceParams = { + point: Math.random() + .toString(), + cut: Math.random() + .toString(), +}; + +@Advice() +export class PointcutAdvice implements IAdvice { + @Inject() + callTrace: CallTrace; + + async beforeCall(ctx: AdviceContext): Promise { + assert.ok(ctx.adviceParams); + assert.deepStrictEqual(ctx.adviceParams, pointcutAdviceParams); + this.callTrace.addMsg({ + className: PointcutAdvice.name, + methodName: 'beforeCall', + id: ctx.that.id, + name: ctx.args[0], + adviceParams: ctx.adviceParams, + }); + } + + async afterReturn(ctx: AdviceContext, result: any): Promise { + assert.ok(ctx.adviceParams); + assert.deepStrictEqual(ctx.adviceParams, pointcutAdviceParams); + this.callTrace.addMsg({ + className: PointcutAdvice.name, + methodName: 'afterReturn', + id: ctx.that.id, + name: ctx.args[0], + result, + adviceParams: ctx.adviceParams, + }); + } + + async afterThrow(ctx: AdviceContext, error: Error): Promise { + assert.ok(ctx.adviceParams); + assert.deepStrictEqual(ctx.adviceParams, pointcutAdviceParams); + this.callTrace.addMsg({ + className: PointcutAdvice.name, + methodName: 'afterThrow', + id: ctx.that.id, + name: ctx.args[0], + result: error.message, + adviceParams: ctx.adviceParams, + }); + } + + async afterFinally(ctx: AdviceContext): Promise { + assert.ok(ctx.adviceParams); + assert.deepStrictEqual(ctx.adviceParams, pointcutAdviceParams); + this.callTrace.addMsg({ + className: PointcutAdvice.name, + methodName: 'afterFinally', + id: ctx.that.id, + name: ctx.args[0], + adviceParams: ctx.adviceParams, + }); + } + + async around(ctx: AdviceContext, next: () => Promise): Promise { + assert.ok(ctx.adviceParams); + assert.deepStrictEqual(ctx.adviceParams, pointcutAdviceParams); + ctx.args[0] = `withPointAroundParam(${ctx.args[0]})`; + const result = await next(); + return `withPointAroundResult(${result}${JSON.stringify(pointcutAdviceParams)})`; + } +} + +@ContextProto() +export class Hello { + id = 233; + + @Pointcut(PointcutAdvice, { adviceParams: pointcutAdviceParams }) + async hello(name: string) { + return `hello ${name}`; + } + + @Pointcut(PointcutAdvice, { adviceParams: pointcutAdviceParams }) + async helloWithException(name: string) { + throw new Error(`ops, exception for ${name}`); + } + +} + +export const crosscutAdviceParams = { + cross: Math.random() + .toString(), + cut: Math.random() + .toString(), +}; + +@Crosscut({ + type: PointcutType.CLASS, + clazz: Hello, + methodName: 'hello', +}, { adviceParams: crosscutAdviceParams }) +@Advice() +export class CrosscutAdvice implements IAdvice { + @Inject() + callTrace: CallTrace; + + async beforeCall(ctx: AdviceContext): Promise { + assert.ok(ctx.adviceParams); + assert.deepStrictEqual(ctx.adviceParams, crosscutAdviceParams); + this.callTrace.addMsg({ + className: CrosscutAdvice.name, + methodName: 'beforeCall', + id: ctx.that.id, + name: ctx.args[0], + adviceParams: ctx.adviceParams, + }); + } + + async afterReturn(ctx: AdviceContext, result: any): Promise { + assert.ok(ctx.adviceParams); + assert.deepStrictEqual(ctx.adviceParams, crosscutAdviceParams); + this.callTrace.addMsg({ + className: CrosscutAdvice.name, + methodName: 'afterReturn', + id: ctx.that.id, + name: ctx.args[0], + result, + adviceParams: ctx.adviceParams, + }); + } + + async afterFinally(ctx: AdviceContext): Promise { + assert.ok(ctx.adviceParams); + assert.deepStrictEqual(ctx.adviceParams, crosscutAdviceParams); + this.callTrace.addMsg({ + className: CrosscutAdvice.name, + methodName: 'afterFinally', + id: ctx.that.id, + name: ctx.args[0], + adviceParams: ctx.adviceParams, + }); + } + + async around(ctx: AdviceContext, next: () => Promise): Promise { + assert.ok(ctx.adviceParams); + assert.deepStrictEqual(ctx.adviceParams, crosscutAdviceParams); + ctx.args[0] = `withCrosscutAroundParam(${ctx.args[0]})`; + const result = await next(); + return `withCrossAroundResult(${result}${JSON.stringify(ctx.adviceParams)})`; + } +} diff --git a/standalone/standalone/test/fixtures/aop-module/main.ts b/standalone/standalone/test/fixtures/aop-module/main.ts new file mode 100644 index 00000000..11d6fc34 --- /dev/null +++ b/standalone/standalone/test/fixtures/aop-module/main.ts @@ -0,0 +1,15 @@ +import { ContextProto, Inject } from '@eggjs/tegg'; +import { Runner, MainRunner } from '@eggjs/tegg/standalone'; +import { Hello } from './Hello'; + + +@Runner() +@ContextProto() +export class Foo implements MainRunner { + @Inject() + hello: Hello; + + async main(): Promise { + return await this.hello.hello('aop'); + } +} diff --git a/standalone/standalone/test/fixtures/aop-module/package.json b/standalone/standalone/test/fixtures/aop-module/package.json new file mode 100644 index 00000000..4dcd7bf5 --- /dev/null +++ b/standalone/standalone/test/fixtures/aop-module/package.json @@ -0,0 +1,6 @@ +{ + "name": "aop-module", + "eggModule": { + "name": "aopModule" + } +} diff --git a/standalone/standalone/test/fixtures/dynamic-inject-module/AbstractContextHello.ts b/standalone/standalone/test/fixtures/dynamic-inject-module/AbstractContextHello.ts new file mode 100644 index 00000000..7f52000d --- /dev/null +++ b/standalone/standalone/test/fixtures/dynamic-inject-module/AbstractContextHello.ts @@ -0,0 +1,3 @@ +export abstract class AbstractContextHello { + abstract hello(): string; +} diff --git a/standalone/standalone/test/fixtures/dynamic-inject-module/AbstractSingletonHello.ts b/standalone/standalone/test/fixtures/dynamic-inject-module/AbstractSingletonHello.ts new file mode 100644 index 00000000..28e17f29 --- /dev/null +++ b/standalone/standalone/test/fixtures/dynamic-inject-module/AbstractSingletonHello.ts @@ -0,0 +1,3 @@ +export abstract class AbstractSingletonHello { + abstract hello(): string; +} diff --git a/standalone/standalone/test/fixtures/dynamic-inject-module/FooType.ts b/standalone/standalone/test/fixtures/dynamic-inject-module/FooType.ts new file mode 100644 index 00000000..e5f322e3 --- /dev/null +++ b/standalone/standalone/test/fixtures/dynamic-inject-module/FooType.ts @@ -0,0 +1,9 @@ +export enum ContextHelloType { + FOO = 'FOO', + BAR = 'BAR', +} + +export enum SingletonHelloType { + FOO = 'FOO', + BAR = 'BAR', +} diff --git a/standalone/standalone/test/fixtures/dynamic-inject-module/HelloService.ts b/standalone/standalone/test/fixtures/dynamic-inject-module/HelloService.ts new file mode 100644 index 00000000..3f34ec1c --- /dev/null +++ b/standalone/standalone/test/fixtures/dynamic-inject-module/HelloService.ts @@ -0,0 +1,21 @@ +import { ContextProto, Inject, EggObjectFactory } from '@eggjs/tegg'; +import { ContextHelloType, SingletonHelloType } from './FooType'; +import { AbstractContextHello } from './AbstractContextHello'; +import { AbstractSingletonHello } from './AbstractSingletonHello'; + +@ContextProto() +export class HelloService { + @Inject() + private readonly eggObjectFactory: EggObjectFactory; + + async hello(): Promise { + const helloImpls = await Promise.all([ + this.eggObjectFactory.getEggObject(AbstractContextHello, ContextHelloType.FOO), + this.eggObjectFactory.getEggObject(AbstractContextHello, ContextHelloType.BAR), + this.eggObjectFactory.getEggObject(AbstractSingletonHello, SingletonHelloType.FOO), + this.eggObjectFactory.getEggObject(AbstractSingletonHello, SingletonHelloType.BAR), + ]); + const msgs = helloImpls.map(helloImpl => helloImpl.hello()); + return msgs; + } +} diff --git a/standalone/standalone/test/fixtures/dynamic-inject-module/decorator/ContextHello.ts b/standalone/standalone/test/fixtures/dynamic-inject-module/decorator/ContextHello.ts new file mode 100644 index 00000000..4fea049d --- /dev/null +++ b/standalone/standalone/test/fixtures/dynamic-inject-module/decorator/ContextHello.ts @@ -0,0 +1,9 @@ +import { ContextHelloType } from '../FooType'; +import { ImplDecorator, QualifierImplDecoratorUtil } from '@eggjs/tegg'; +import { AbstractContextHello } from '../AbstractContextHello'; + +export const CONTEXT_HELLO_ATTRIBUTE = 'CONTEXT_HELLO_ATTRIBUTE'; + +export const ContextHello: ImplDecorator = + QualifierImplDecoratorUtil.generatorDecorator(AbstractContextHello, CONTEXT_HELLO_ATTRIBUTE); + diff --git a/standalone/standalone/test/fixtures/dynamic-inject-module/decorator/SingletonHello.ts b/standalone/standalone/test/fixtures/dynamic-inject-module/decorator/SingletonHello.ts new file mode 100644 index 00000000..98a28121 --- /dev/null +++ b/standalone/standalone/test/fixtures/dynamic-inject-module/decorator/SingletonHello.ts @@ -0,0 +1,8 @@ +import { SingletonHelloType } from '../FooType'; +import { ImplDecorator, QualifierImplDecoratorUtil } from '@eggjs/tegg'; +import { AbstractSingletonHello } from '../AbstractSingletonHello'; + +export const SINGLETON_HELLO_ATTRIBUTE = 'SINGLETON_HELLO_ATTRIBUTE'; + +export const SingletonHello: ImplDecorator = + QualifierImplDecoratorUtil.generatorDecorator(AbstractSingletonHello, SINGLETON_HELLO_ATTRIBUTE); diff --git a/standalone/standalone/test/fixtures/dynamic-inject-module/impl/BarContextHello.ts b/standalone/standalone/test/fixtures/dynamic-inject-module/impl/BarContextHello.ts new file mode 100644 index 00000000..5f3d268c --- /dev/null +++ b/standalone/standalone/test/fixtures/dynamic-inject-module/impl/BarContextHello.ts @@ -0,0 +1,14 @@ +import { ContextProto } from '@eggjs/core-decorator'; +import { ContextHello } from '../decorator/ContextHello'; +import { ContextHelloType } from '../FooType'; +import { AbstractContextHello } from '../AbstractContextHello'; + +@ContextProto() +@ContextHello(ContextHelloType.BAR) +export class BarContextHello extends AbstractContextHello { + id = 0; + + hello(): string { + return `hello, bar(context:${this.id++})`; + } +} diff --git a/standalone/standalone/test/fixtures/dynamic-inject-module/impl/BarSingletonHello.ts b/standalone/standalone/test/fixtures/dynamic-inject-module/impl/BarSingletonHello.ts new file mode 100644 index 00000000..d0ec5e86 --- /dev/null +++ b/standalone/standalone/test/fixtures/dynamic-inject-module/impl/BarSingletonHello.ts @@ -0,0 +1,14 @@ +import { SingletonProto } from '@eggjs/core-decorator'; +import { SingletonHelloType } from '../FooType'; +import { SingletonHello } from '../decorator/SingletonHello'; +import { AbstractContextHello } from '../AbstractContextHello'; + +@SingletonProto() +@SingletonHello(SingletonHelloType.BAR) +export class BarSingletonHello extends AbstractContextHello { + id = 0; + + hello(): string { + return `hello, bar(singleton:${this.id++})`; + } +} diff --git a/standalone/standalone/test/fixtures/dynamic-inject-module/impl/FooContextHello.ts b/standalone/standalone/test/fixtures/dynamic-inject-module/impl/FooContextHello.ts new file mode 100644 index 00000000..e6ed0fd9 --- /dev/null +++ b/standalone/standalone/test/fixtures/dynamic-inject-module/impl/FooContextHello.ts @@ -0,0 +1,14 @@ +import { ContextProto } from '@eggjs/core-decorator'; +import { ContextHello } from '../decorator/ContextHello'; +import { ContextHelloType } from '../FooType'; +import { AbstractContextHello } from '../AbstractContextHello'; + +@ContextProto() +@ContextHello(ContextHelloType.FOO) +export class FooContextHello extends AbstractContextHello { + id = 0; + + hello(): string { + return `hello, foo(context:${this.id++})`; + } +} diff --git a/standalone/standalone/test/fixtures/dynamic-inject-module/impl/FooSingletonHello.ts b/standalone/standalone/test/fixtures/dynamic-inject-module/impl/FooSingletonHello.ts new file mode 100644 index 00000000..7deb20e2 --- /dev/null +++ b/standalone/standalone/test/fixtures/dynamic-inject-module/impl/FooSingletonHello.ts @@ -0,0 +1,14 @@ +import { SingletonProto } from '@eggjs/core-decorator'; +import { SingletonHelloType } from '../FooType'; +import { SingletonHello } from '../decorator/SingletonHello'; +import { AbstractContextHello } from '../AbstractContextHello'; + +@SingletonProto() +@SingletonHello(SingletonHelloType.FOO) +export class FooSingletonHello extends AbstractContextHello { + id = 0; + + hello(): string { + return `hello, foo(singleton:${this.id++})`; + } +} diff --git a/standalone/standalone/test/fixtures/dynamic-inject-module/main.ts b/standalone/standalone/test/fixtures/dynamic-inject-module/main.ts new file mode 100644 index 00000000..39383822 --- /dev/null +++ b/standalone/standalone/test/fixtures/dynamic-inject-module/main.ts @@ -0,0 +1,14 @@ +import { ContextProto, Inject } from '@eggjs/tegg'; +import { Runner, MainRunner } from '@eggjs/tegg/standalone'; +import { HelloService } from './HelloService'; + +@Runner() +@ContextProto() +export class Foo implements MainRunner { + @Inject() + helloService: HelloService; + + async main(): Promise { + return await this.helloService.hello(); + } +} diff --git a/standalone/standalone/test/fixtures/dynamic-inject-module/package.json b/standalone/standalone/test/fixtures/dynamic-inject-module/package.json new file mode 100644 index 00000000..876f583b --- /dev/null +++ b/standalone/standalone/test/fixtures/dynamic-inject-module/package.json @@ -0,0 +1,6 @@ +{ + "name": "dynamic-inject-module", + "eggModule": { + "name": "dynamicInjectModule" + } +} diff --git a/standalone/standalone/test/index.test.ts b/standalone/standalone/test/index.test.ts index e15235cf..78a8f877 100644 --- a/standalone/standalone/test/index.test.ts +++ b/standalone/standalone/test/index.test.ts @@ -4,6 +4,7 @@ import fs from 'node:fs/promises'; import { main, StandaloneContext, Runner } from '..'; import { ModuleConfigs } from '../src/ModuleConfigs'; import { ModuleConfig } from 'egg'; +import { crosscutAdviceParams, pointcutAdviceParams } from './fixtures/aop-module/Hello'; describe('test/index.test.ts', () => { describe('simple runner', () => { @@ -89,4 +90,28 @@ describe('test/index.test.ts', () => { assert(barContent.includes('hello, bar')); }); }); + + describe('dynamic inject', () => { + const fixturePath = path.join(__dirname, './fixtures/dynamic-inject-module'); + + it('should work', async () => { + const msgs = await main(fixturePath); + assert.deepStrictEqual(msgs, [ + 'hello, foo(context:0)', + 'hello, bar(context:0)', + 'hello, foo(singleton:0)', + 'hello, bar(singleton:0)', + ]); + }); + }); + + describe('aop runtime', () => { + const fixturePath = path.join(__dirname, './fixtures/aop-module'); + + it('should work', async () => { + const msg = await main(fixturePath); + assert.deepStrictEqual(msg, + `withCrossAroundResult(withPointAroundResult(hello withPointAroundParam(withCrosscutAroundParam(aop))${JSON.stringify(pointcutAdviceParams)})${JSON.stringify(crosscutAdviceParams)})`); + }); + }); });