From 48fce4512e99259ec26a9b032bfcc9f4046ad235 Mon Sep 17 00:00:00 2001 From: killa Date: Thu, 10 Oct 2024 12:12:16 +0800 Subject: [PATCH] feat: impl GlobalGraph build hook (#246) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ##### Checklist - [ ] `npm test` passes - [ ] tests and/or benchmarks are included - [ ] documentation is changed or added - [ ] commit message follows commit guidelines ##### Affected core subsystem(s) ##### Description of change ## Summary by CodeRabbit - **New Features** - Added `CrossCutGraphHook` and `PointCutGraphHook` to enhance the module's capabilities. - Introduced functions for managing cross-cutting concerns and linking pointcut advice within a global graph structure. - **Bug Fixes** - Improved test coverage for cross-cutting concerns, ensuring robust functionality. - **Documentation** - New `package.json` files created for `hello-cross-cut` and `hello-point-cut` modules to define metadata. - **Refactor** - Restructured the `Hello` class to utilize pointcut advice effectively. - **Chores** - Updated utility classes to support new build hooks for enhanced functionality. --- core/aop-decorator/src/AspectMetaBuilder.ts | 2 +- core/aop-runtime/index.ts | 3 + core/aop-runtime/src/CrossCutGraphHook.ts | 63 +++++++ core/aop-runtime/src/PointCutGraphHook.ts | 54 ++++++ core/aop-runtime/test/aop-runtime.test.ts | 12 +- .../modules/hello_cross_cut/CallTrace.ts | 21 +++ .../modules/hello_cross_cut/HelloCrossCut.ts | 69 ++++++++ .../modules/hello_cross_cut/package.json | 6 + .../modules/hello_point_cut/HelloPointCut.ts | 82 +++++++++ .../modules/hello_point_cut/package.json | 6 + .../fixtures/modules/hello_succeed/Hello.ts | 160 +----------------- core/metadata/src/model/graph/GlobalGraph.ts | 74 +++++--- core/test-util/CoreTestHelper.ts | 16 +- core/test-util/LoaderUtil.ts | 7 +- .../test/fixtures/aop-module/Hello.ts | 2 +- 15 files changed, 385 insertions(+), 192 deletions(-) create mode 100644 core/aop-runtime/src/CrossCutGraphHook.ts create mode 100644 core/aop-runtime/src/PointCutGraphHook.ts create mode 100644 core/aop-runtime/test/fixtures/modules/hello_cross_cut/CallTrace.ts create mode 100644 core/aop-runtime/test/fixtures/modules/hello_cross_cut/HelloCrossCut.ts create mode 100644 core/aop-runtime/test/fixtures/modules/hello_cross_cut/package.json create mode 100644 core/aop-runtime/test/fixtures/modules/hello_point_cut/HelloPointCut.ts create mode 100644 core/aop-runtime/test/fixtures/modules/hello_point_cut/package.json diff --git a/core/aop-decorator/src/AspectMetaBuilder.ts b/core/aop-decorator/src/AspectMetaBuilder.ts index 16776442..d7ecf849 100644 --- a/core/aop-decorator/src/AspectMetaBuilder.ts +++ b/core/aop-decorator/src/AspectMetaBuilder.ts @@ -26,7 +26,7 @@ export class AspectMetaBuilder { return aspectList; } - private static getAllMethods(clazz): PropertyKey[] { + static getAllMethods(clazz): PropertyKey[] { const methodSet = new Set(); function getMethods(obj) { if (obj) { diff --git a/core/aop-runtime/index.ts b/core/aop-runtime/index.ts index 41a6f799..55d1f4b3 100644 --- a/core/aop-runtime/index.ts +++ b/core/aop-runtime/index.ts @@ -1,3 +1,6 @@ export * from './src/EggPrototypeCrossCutHook'; export * from './src/EggObjectAopHook'; export * from './src/LoadUnitAopHook'; + +export * from './src/CrossCutGraphHook'; +export * from './src/PointCutGraphHook'; diff --git a/core/aop-runtime/src/CrossCutGraphHook.ts b/core/aop-runtime/src/CrossCutGraphHook.ts new file mode 100644 index 00000000..4eab8d3d --- /dev/null +++ b/core/aop-runtime/src/CrossCutGraphHook.ts @@ -0,0 +1,63 @@ +import { AspectMetaBuilder, CrosscutInfo, CrosscutInfoUtil } from '@eggjs/aop-decorator'; +import { GraphNode } from '@eggjs/tegg-common-util'; +import { + ClassProtoDescriptor, + GlobalGraph, + ProtoDependencyMeta, + ProtoNode, +} from '@eggjs/tegg-metadata'; + +export function crossCutGraphHook(globalGraph: GlobalGraph) { + for (const moduleNode of globalGraph.moduleGraph.nodes.values()) { + for (const crossCutProtoNode of moduleNode.val.protos) { + const protoNodes = findCrossCuttedClazz(globalGraph, crossCutProtoNode); + if (!protoNodes) continue; + for (const crossCuttedProtoNode of protoNodes) { + const crossCuttedModuleNode = globalGraph.findModuleNode(crossCuttedProtoNode.val.proto.instanceModuleName); + if (!crossCuttedModuleNode) continue; + globalGraph.addInject( + crossCuttedModuleNode, + crossCuttedProtoNode, + crossCutProtoNode, + crossCutProtoNode.val.proto.name); + } + } + } +} + +function findCrossCuttedClazz(globalGraph: GlobalGraph, protoNode: GraphNode) { + const proto = protoNode.val.proto; + if (!ClassProtoDescriptor.isClassProtoDescriptor(proto)) { + return; + } + if (!CrosscutInfoUtil.isCrosscutAdvice(proto.clazz)) { + return; + } + const crosscutInfoList = CrosscutInfoUtil.getCrosscutInfoList(proto.clazz); + const result: GraphNode[] = []; + // eslint-disable-next-line no-labels + crosscut: for (const crosscutInfo of crosscutInfoList) { + for (const protoNode of globalGraph.protoGraph.nodes.values()) { + if (checkClazzMatchCrossCut(protoNode, crosscutInfo)) { + result.push(protoNode); + // eslint-disable-next-line no-labels + break crosscut; + } + } + } + return result; +} + +function checkClazzMatchCrossCut(protoNode: GraphNode, crosscutInfo: CrosscutInfo) { + const proto = protoNode.val.proto; + if (!ClassProtoDescriptor.isClassProtoDescriptor(proto)) { + return; + } + const allMethods = AspectMetaBuilder.getAllMethods(proto.clazz); + for (const method of allMethods) { + if (crosscutInfo.pointcutInfo.match(proto.clazz, method)) { + return true; + } + } + return false; +} diff --git a/core/aop-runtime/src/PointCutGraphHook.ts b/core/aop-runtime/src/PointCutGraphHook.ts new file mode 100644 index 00000000..7c229e8b --- /dev/null +++ b/core/aop-runtime/src/PointCutGraphHook.ts @@ -0,0 +1,54 @@ +import { AspectMetaBuilder, PointcutAdviceInfoUtil } from '@eggjs/aop-decorator'; +import { PrototypeUtil, QualifierUtil } from '@eggjs/core-decorator'; +import { GraphNode } from '@eggjs/tegg-common-util'; +import { + ClassProtoDescriptor, + GlobalGraph, + ProtoDependencyMeta, + ProtoNode, +} from '@eggjs/tegg-metadata'; +import assert from 'node:assert'; + +export function pointCutGraphHook(globalGraph: GlobalGraph) { + for (const moduleNode of globalGraph.moduleGraph.nodes.values()) { + for (const pointCuttedProtoNode of moduleNode.val.protos) { + const pointCutAdviceProtoList = findPointCutAdvice(globalGraph, pointCuttedProtoNode); + if (!pointCutAdviceProtoList) continue; + for (const pointCutAdviceProto of pointCutAdviceProtoList) { + globalGraph.addInject( + moduleNode, + pointCuttedProtoNode, + pointCutAdviceProto, + pointCutAdviceProto.val.proto.name); + } + } + } +} + +function findPointCutAdvice(globalGraph: GlobalGraph, protoNode: GraphNode) { + const proto = protoNode.val.proto; + if (!ClassProtoDescriptor.isClassProtoDescriptor(proto)) { + return; + } + const result: Set> = new Set(); + const allMethods = AspectMetaBuilder.getAllMethods(proto.clazz); + for (const method of allMethods) { + const adviceInfoList = PointcutAdviceInfoUtil.getPointcutAdviceInfoList(proto.clazz, method); + for (const { clazz } of adviceInfoList) { + const property = PrototypeUtil.getProperty(clazz); + assert(property, 'not found property'); + const injectProto = globalGraph.findDependencyProtoNode(protoNode.val.proto, { + objName: property.name, + refName: property.name, + qualifiers: [ + ...property?.qualifiers ?? [], + ...QualifierUtil.getProtoQualifiers(clazz), + ], + }); + if (injectProto) { + result.add(injectProto); + } + } + } + return Array.from(result); +} diff --git a/core/aop-runtime/test/aop-runtime.test.ts b/core/aop-runtime/test/aop-runtime.test.ts index 03a966e1..0ce26599 100644 --- a/core/aop-runtime/test/aop-runtime.test.ts +++ b/core/aop-runtime/test/aop-runtime.test.ts @@ -6,10 +6,15 @@ import { EggPrototypeLifecycleUtil, LoadUnitFactory, LoadUnitLifecycleUtil } fro import type { LoadUnitInstance } from '@eggjs/tegg-types'; import { CrosscutAdviceFactory } from '@eggjs/aop-decorator'; import { CoreTestHelper, EggTestContext } from '../../test-util'; -import { CallTrace, Hello, crosscutAdviceParams, pointcutAdviceParams } from './fixtures/modules/hello_succeed/Hello'; +import { Hello } from './fixtures/modules/hello_succeed/Hello'; +import { crosscutAdviceParams } from './fixtures/modules/hello_cross_cut/HelloCrossCut'; +import { pointcutAdviceParams } from './fixtures/modules/hello_point_cut/HelloPointCut'; import { EggObjectAopHook } from '../src/EggObjectAopHook'; import { LoadUnitAopHook } from '../src/LoadUnitAopHook'; import { EggPrototypeCrossCutHook } from '../src/EggPrototypeCrossCutHook'; +import { crossCutGraphHook } from '../src/CrossCutGraphHook'; +import { pointCutGraphHook } from '../src/PointCutGraphHook'; +import { CallTrace } from './fixtures/modules/hello_cross_cut/CallTrace'; describe('test/aop-runtime.test.ts', () => { describe('succeed call', () => { @@ -31,6 +36,11 @@ describe('test/aop-runtime.test.ts', () => { modules = await CoreTestHelper.prepareModules([ path.join(__dirname, '..'), path.join(__dirname, 'fixtures/modules/hello_succeed'), + path.join(__dirname, 'fixtures/modules/hello_point_cut'), + path.join(__dirname, 'fixtures/modules/hello_cross_cut'), + ], [ + crossCutGraphHook, + pointCutGraphHook, ]); }); diff --git a/core/aop-runtime/test/fixtures/modules/hello_cross_cut/CallTrace.ts b/core/aop-runtime/test/fixtures/modules/hello_cross_cut/CallTrace.ts new file mode 100644 index 00000000..f94ca2b7 --- /dev/null +++ b/core/aop-runtime/test/fixtures/modules/hello_cross_cut/CallTrace.ts @@ -0,0 +1,21 @@ +import { AccessLevel, SingletonProto } from "@eggjs/core-decorator"; + +export interface CallTraceMsg { + className: string; + methodName: string; + id: number; + name: string; + result?: string; + adviceParams?: any; +} + +@SingletonProto({ + accessLevel: AccessLevel.PUBLIC, +}) +export class CallTrace { + msgs: Array = []; + + addMsg(msg: CallTraceMsg) { + this.msgs.push(msg); + } +} diff --git a/core/aop-runtime/test/fixtures/modules/hello_cross_cut/HelloCrossCut.ts b/core/aop-runtime/test/fixtures/modules/hello_cross_cut/HelloCrossCut.ts new file mode 100644 index 00000000..9a31ae90 --- /dev/null +++ b/core/aop-runtime/test/fixtures/modules/hello_cross_cut/HelloCrossCut.ts @@ -0,0 +1,69 @@ +import assert from 'node:assert'; +import { AccessLevel, Inject } from '@eggjs/core-decorator'; +import { Advice, Crosscut } from '@eggjs/aop-decorator'; +import { AdviceContext, IAdvice, PointcutType } from '@eggjs/tegg-types'; +import { Hello } from '../hello_succeed/Hello'; +import { CallTrace } from './CallTrace'; + +export const crosscutAdviceParams = { + cross: Math.random().toString(), + cut: Math.random().toString(), +}; + +@Crosscut({ + type: PointcutType.CLASS, + clazz: Hello, + methodName: 'hello', +}, { adviceParams: crosscutAdviceParams }) +@Advice({ + accessLevel: AccessLevel.PUBLIC, +}) +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/core/aop-runtime/test/fixtures/modules/hello_cross_cut/package.json b/core/aop-runtime/test/fixtures/modules/hello_cross_cut/package.json new file mode 100644 index 00000000..f4705e1f --- /dev/null +++ b/core/aop-runtime/test/fixtures/modules/hello_cross_cut/package.json @@ -0,0 +1,6 @@ +{ + "name": "hello-cross-cut", + "eggModule": { + "name": "helloCrossCut" + } +} diff --git a/core/aop-runtime/test/fixtures/modules/hello_point_cut/HelloPointCut.ts b/core/aop-runtime/test/fixtures/modules/hello_point_cut/HelloPointCut.ts new file mode 100644 index 00000000..8312e21a --- /dev/null +++ b/core/aop-runtime/test/fixtures/modules/hello_point_cut/HelloPointCut.ts @@ -0,0 +1,82 @@ +import assert from 'node:assert'; +import { AccessLevel, Inject } from '@eggjs/core-decorator'; +import { Advice } from '@eggjs/aop-decorator'; +import { AdviceContext, IAdvice } from '@eggjs/tegg-types'; +import { Hello } from '../hello_succeed/Hello'; +import { CallTrace } from '../hello_cross_cut/CallTrace'; + +export const pointcutAdviceParams = { + point: Math.random().toString(), + cut: Math.random().toString(), +}; + +// 测试aop修改ctx的args的值 +const TEST_CTX_ARGS_VALUE = 123; + +@Advice({ + accessLevel: AccessLevel.PUBLIC, +}) +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, + }); + ctx.args = [...ctx.args, TEST_CTX_ARGS_VALUE]; + } + + async afterReturn(ctx: AdviceContext, result: any): Promise { + assert.ok(ctx.adviceParams); + assert.deepStrictEqual(ctx.adviceParams, pointcutAdviceParams); + assert.deepStrictEqual(ctx.args[ctx.args.length - 1], TEST_CTX_ARGS_VALUE); + 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)})`; + } +} diff --git a/core/aop-runtime/test/fixtures/modules/hello_point_cut/package.json b/core/aop-runtime/test/fixtures/modules/hello_point_cut/package.json new file mode 100644 index 00000000..2bb45ff4 --- /dev/null +++ b/core/aop-runtime/test/fixtures/modules/hello_point_cut/package.json @@ -0,0 +1,6 @@ +{ + "name": "hello-point-cut", + "eggModule": { + "name": "helloPointCut" + } +} diff --git a/core/aop-runtime/test/fixtures/modules/hello_succeed/Hello.ts b/core/aop-runtime/test/fixtures/modules/hello_succeed/Hello.ts index ef49d93f..16b45a36 100644 --- a/core/aop-runtime/test/fixtures/modules/hello_succeed/Hello.ts +++ b/core/aop-runtime/test/fixtures/modules/hello_succeed/Hello.ts @@ -1,99 +1,6 @@ -import assert from 'node:assert'; -import { ContextProto, Inject, SingletonProto } from '@eggjs/core-decorator'; -import { Advice, Crosscut, Pointcut } from '@eggjs/aop-decorator'; -import { AdviceContext, IAdvice, PointcutType } from '@eggjs/tegg-types'; - -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(), -}; - -// 测试aop修改ctx的args的值 -const TEST_CTX_ARGS_VALUE = 123; - -@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, - }); - ctx.args = [...ctx.args, TEST_CTX_ARGS_VALUE]; - } - - async afterReturn(ctx: AdviceContext, result: any): Promise { - assert.ok(ctx.adviceParams); - assert.deepStrictEqual(ctx.adviceParams, pointcutAdviceParams); - assert.deepStrictEqual(ctx.args[ctx.args.length - 1], TEST_CTX_ARGS_VALUE); - 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)})`; - } -} +import { ContextProto } from '@eggjs/core-decorator'; +import { Pointcut } from '@eggjs/aop-decorator'; +import { PointcutAdvice, pointcutAdviceParams } from '../hello_point_cut/HelloPointCut'; @ContextProto() export class Hello { @@ -110,64 +17,3 @@ export class Hello { } } - -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/core/metadata/src/model/graph/GlobalGraph.ts b/core/metadata/src/model/graph/GlobalGraph.ts index e2ed6e14..ff767a3a 100644 --- a/core/metadata/src/model/graph/GlobalGraph.ts +++ b/core/metadata/src/model/graph/GlobalGraph.ts @@ -19,6 +19,8 @@ export interface GlobalGraphOptions { strict?: boolean; } +export type GlobalGraphBuildHook = (globalGraph: GlobalGraph) => void; + /** * Sort all prototypes and modules in app. * - 1. LoaderFactory.loadApp: get ModuleDescriptors @@ -38,13 +40,13 @@ export class GlobalGraph { * Edge: ModuleDependencyMeta, prototype and it's inject object * @private */ - private moduleGraph: Graph; + moduleGraph: Graph; /** * Vertex: ProtoNode, collect all prototypes in app * Edge: ProtoDependencyMeta, inject object * @private */ - private protoGraph: Graph; + protoGraph: Graph; /** * The order of the moduleConfigList is the order in which they are instantiated */ @@ -55,6 +57,7 @@ export class GlobalGraph { */ moduleProtoDescriptorMap: Map; strict: boolean; + private buildHooks: GlobalGraphBuildHook[]; /** * The global instance used in ModuleLoadUnit @@ -66,6 +69,11 @@ export class GlobalGraph { this.protoGraph = new Graph(); this.strict = options?.strict ?? false; this.moduleProtoDescriptorMap = new Map(); + this.buildHooks = []; + } + + registerBuildHook(hook: GlobalGraphBuildHook) { + this.buildHooks.push(hook); } addModuleNode(moduleNode: GlobalModuleNode) { @@ -83,29 +91,45 @@ export class GlobalGraph { for (const moduleNode of this.moduleGraph.nodes.values()) { for (const protoNode of moduleNode.val.protos) { for (const injectObj of protoNode.val.proto.injectObjects) { - const injectProto = this.#findDependencyProtoNode(protoNode.val.proto, injectObj); - if (!injectProto) { - if (!this.strict) { - continue; - } - throw FrameworkErrorFormater.formatError(new EggPrototypeNotFound(injectObj.objName, protoNode.val.proto.instanceModuleName)); - } - this.protoGraph.addEdge(protoNode, injectProto, new ProtoDependencyMeta({ - injectObj: injectObj.objName, - })); - const injectModule = this.#findModuleNode(injectProto.val.proto.instanceModuleName); - if (!injectModule) { - if (!this.strict) { - continue; - } - throw new Error(`not found module ${injectProto.val.proto.instanceModuleName}`); - } - if (moduleNode.val.id !== injectModule.val.id) { - this.moduleGraph.addEdge(moduleNode, injectModule, new ModuleDependencyMeta(protoNode.val.proto, injectObj.objName)); - } + this.buildInjectEdge(moduleNode, protoNode, injectObj); } } } + for (const buildHook of this.buildHooks) { + buildHook(this); + } + } + + buildInjectEdge(moduleNode: GraphNode, protoNode: GraphNode, injectObj: InjectObjectDescriptor) { + const injectProto = this.findDependencyProtoNode(protoNode.val.proto, injectObj); + if (!injectProto) { + if (!this.strict) { + return; + } + throw FrameworkErrorFormater.formatError(new EggPrototypeNotFound(injectObj.objName, protoNode.val.proto.instanceModuleName)); + } + this.addInject(moduleNode, protoNode, injectProto, injectObj.objName); + } + + addInject( + moduleNode: GraphNode, + protoNode: GraphNode, + injectNode: GraphNode, + injectName: PropertyKey, + ) { + this.protoGraph.addEdge(protoNode, injectNode, new ProtoDependencyMeta({ + injectObj: injectName, + })); + const injectModule = this.findModuleNode(injectNode.val.proto.instanceModuleName); + if (!injectModule) { + if (!this.strict) { + return; + } + throw new Error(`not found module ${injectNode.val.proto.instanceModuleName}`); + } + if (moduleNode.val.id !== injectModule.val.id) { + this.moduleGraph.addEdge(moduleNode, injectModule, new ModuleDependencyMeta(protoNode.val.proto, injectName)); + } } findInjectProto(proto: ProtoDescriptor, injectObject: InjectObjectDescriptor): ProtoDescriptor | undefined { @@ -133,7 +157,7 @@ export class GlobalGraph { return result; } - #findDependencyProtoNode(proto: ProtoDescriptor, injectObject: InjectObjectDescriptor): GraphNode | undefined { + findDependencyProtoNode(proto: ProtoDescriptor, injectObject: InjectObjectDescriptor): GraphNode | undefined { // 1. find proto with request // 2. try to add Context qualifier to find // 3. try to add self init type qualifier to find @@ -163,7 +187,7 @@ export class GlobalGraph { } const loadUnitQualifier = injectObject.qualifiers.find(t => t.attribute === LoadUnitNameQualifierAttribute); if (!loadUnitQualifier) { - return this.#findDependencyProtoNode(proto, { + return this.findDependencyProtoNode(proto, { ...injectObject, qualifiers: [ ...injectObject.qualifiers, @@ -177,7 +201,7 @@ export class GlobalGraph { throw FrameworkErrorFormater.formatError(new MultiPrototypeFound(injectObject.objName, injectObject.qualifiers)); } - #findModuleNode(moduleName: string) { + findModuleNode(moduleName: string) { for (const node of this.moduleGraph.nodes.values()) { if (node.val.name === moduleName) { return node; diff --git a/core/test-util/CoreTestHelper.ts b/core/test-util/CoreTestHelper.ts index 195af394..b8ae0102 100644 --- a/core/test-util/CoreTestHelper.ts +++ b/core/test-util/CoreTestHelper.ts @@ -5,7 +5,13 @@ import { LoadUnitInstance, LoadUnitInstanceFactory, } from '@eggjs/tegg-runtime'; -import { EggLoadUnitType, EggPrototype, LoadUnitFactory } from '@eggjs/tegg-metadata'; +import { + EggLoadUnitType, + EggPrototype, + GlobalGraph, + GlobalGraphBuildHook, + LoadUnitFactory, +} from '@eggjs/tegg-metadata'; import { LoaderFactory } from '@eggjs/tegg-loader'; import { EggProtoImplClass, PrototypeUtil } from '@eggjs/core-decorator'; import { AsyncLocalStorage } from 'async_hooks'; @@ -32,12 +38,12 @@ export class CoreTestHelper { const loadUnit = await LoadUnitFactory.createLoadUnit(moduleDir, EggLoadUnitType.MODULE, loader); return await LoadUnitInstanceFactory.createLoadUnitInstance(loadUnit); } - static async prepareModules(moduleDirs: string[]): Promise> { - LoaderUtil.buildGlobalGraph(moduleDirs); + static async prepareModules(moduleDirs: string[], hooks?: GlobalGraphBuildHook[]): Promise> { + LoaderUtil.buildGlobalGraph(moduleDirs, hooks); EggContextStorage.register(); const instances: Array = []; - for (const moduleDir of moduleDirs) { - instances.push(await CoreTestHelper.getLoadUnitInstance(moduleDir)); + for (const { path } of GlobalGraph.instance!.moduleConfigList) { + instances.push(await CoreTestHelper.getLoadUnitInstance(path)); } return instances; } diff --git a/core/test-util/LoaderUtil.ts b/core/test-util/LoaderUtil.ts index 08fbc34a..11d09715 100644 --- a/core/test-util/LoaderUtil.ts +++ b/core/test-util/LoaderUtil.ts @@ -1,5 +1,5 @@ import { EggProtoImplClass, PrototypeUtil } from '@eggjs/core-decorator'; -import { EggLoadUnitType, GlobalGraph, GlobalModuleNodeBuilder } from '@eggjs/tegg-metadata'; +import { EggLoadUnitType, GlobalGraph, GlobalGraphBuildHook, GlobalModuleNodeBuilder } from '@eggjs/tegg-metadata'; import { ModuleConfigUtil } from '@eggjs/tegg-common-util'; import { LoaderFactory } from '@eggjs/tegg-loader'; @@ -34,8 +34,11 @@ export class LoaderUtil { return builder.build(); } - static buildGlobalGraph(modulePaths: string[]) { + static buildGlobalGraph(modulePaths: string[], hooks?: GlobalGraphBuildHook[]) { GlobalGraph.instance = new GlobalGraph(); + for (const hook of hooks ?? []) { + GlobalGraph.instance.registerBuildHook(hook); + } const multiInstanceEggProtoClass: { clazz: any; unitPath: string; diff --git a/standalone/standalone/test/fixtures/aop-module/Hello.ts b/standalone/standalone/test/fixtures/aop-module/Hello.ts index 08d16d83..25aaaa8e 100644 --- a/standalone/standalone/test/fixtures/aop-module/Hello.ts +++ b/standalone/standalone/test/fixtures/aop-module/Hello.ts @@ -131,7 +131,7 @@ export const crosscutAdviceParams = { methodName: 'hello', }, { adviceParams: crosscutAdviceParams }) @Advice() -export class CrosscutAdvice implements IAdvice { +export class CrosscutAdvice implements IAdvice { @Inject() callTrace: CallTrace;