[WIP] feat(tegg): declarative module plugin mechanism#6021
Conversation
Introduce the declarative framework-extension (module plugin) core so a plain tegg module can provide lifecycle hooks and framework inner objects via decorators, instead of the host hand-registering every hook in its boot code. Ported from eggjs/tegg#325 (standalone-next) and adapted to the next-branch architecture. - core-decorator/types: @InnerObjectProto (framework inner object, a SingletonProto with EGG_INNER_OBJECT_PROTO_IMPL_TYPE) and @EggLifecycleProto with the five typed variants (LoadUnit/LoadUnitInstance/EggPrototype/EggObject/EggContext); PrototypeUtil metadata accessors. - loader: divert inner object classes into ModuleDescriptor.innerObjectClazzList; getDecoratedFiles now covers the new list so bundle/manifest mode still re-imports those files. - metadata: EggInnerObjectPrototypeImpl (resolves injects at create time); InjectObjectPrototypeFinder extracted from EggPrototypeBuilder (behavior unchanged); ProtoGraphUtils extracted from GlobalGraph with a proto-name index replacing the O(n*m*n) scan; ProtoDescriptorHelper.createByInstanceClazz supports define/instance module separation. - runtime: EggInnerObjectImpl runs only decorator-declared self lifecycle methods (hook-callback names never double as self lifecycle); host-agnostic InnerObjectLoadUnit(+Instance/Builder) instantiates inner objects on a dedicated topologically-sorted proto graph (cycle detection, hard error on missing non-optional deps) and auto registers/deregisters lifecycle protos by declared type via the scope-aware lifecycle utils. The InnerObjectLoadUnit is designed to be instantiated before the business GlobalGraph build so registered hooks (including future declarative graph build hooks) land inside their consumption windows; host wiring for standalone/app follows in separate PRs. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Rename Runner to StandaloneApp (no Runner alias kept — 4.x breaking change; main()/preLoad() entries are unchanged) and restructure boot into explicit phases so module plugins work end to end: 1. scan modules + create the business GlobalGraph (nodes only) 2. create AND instantiate the InnerObjectLoadUnit — host-provided inner objects plus every scanned @InnerObjectProto/@XxxLifecycleProto class (topologically ordered); lifecycle protos auto-register 3. build()/sort() the business graph and create module load units 4. instantiate module load units Deferring build/sort until after the inner load unit is instantiated restores the tegg#325 two-phase ordering: hooks registered by lifecycle protos (including graph build hooks registered in @LifecyclePostInject) land before their consumption windows. Destroy now runs in reverse creation order so lifecycle protos stay registered until every object they may hook is torn down. Also: - new frameworkDeps option: framework module plugins scanned ahead of the app's own modules (service worker runtime packages plug in here) - bundle-mode support: EggModuleLoader accepts a tegg manifest + loaderFS (reuses precomputed decorated files instead of globbing) and StandaloneApp.loadMetadata() provides the scan-only manifest generation entry for bundlers - replace StandaloneLoadUnit / StandaloneInnerObjectProto / StandaloneInnerObject with the host-agnostic core implementations (InnerObjectLoadUnit / ProvidedInnerObjectProto) - port the tegg#325 module plugin test suite (five lifecycle proto types, inner object DI, PUBLIC/PRIVATE access semantics) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Wire the module plugin mechanism into the egg host, the part tegg#325 left out (inner object / lifecycle protos were silently ignored in app mode). ModuleHandler.init() now runs in phases: 1. EggModuleLoader.initGraph(): scan modules, create the business GlobalGraph (nodes only) and flush buffered build hooks 2. create AND instantiate the InnerObjectLoadUnit from every scanned module's innerObjectClazzList — lifecycle protos auto-register with DI wired, before any business load unit exists 3. EggModuleLoader.load(): APP load unit + build()/sort() + module load units — declarative graph build hooks registered in step 2 land before build() consumes them 4. instantiate business load units (inner unit skips egg compatible mounting) destroy() runs in reverse creation order so lifecycle protos stay registered until every object they may hook is torn down. The same module plugin package now behaves identically under the egg host and the standalone host. Covered by a fixture app whose module provides @InnerObjectProto/@LoadUnitLifecycleProto/@EggObjectLifecycleProto classes; MultiApp isolation regression stays green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Complete the module plugin bootstrap (the #handleCompatibility TODO from tegg#325): every hook the standalone host used to hand-register is now a declarative module plugin class, shared verbatim by both hosts. - aop: LoadUnitAopHook / EggPrototypeCrossCutHook / EggObjectAopHook are @XxxLifecycleProto classes injecting an @InnerObjectProto CrosscutAdviceFactory (optional constructor kept for manual paths); the crossCut/pointCut graph build hooks are registered by the new AopGraphHookRegistrar from @LifecyclePostInject — instantiated after graph creation and before build(), the only valid window. Exported as AOP_INNER_OBJECT_CLAZZ_LIST. - dal: the three hooks are @XxxLifecycleProto classes injecting the host-provided moduleConfigs / runtimeConfig / logger inner objects instead of constructor args. Exported as DAL_INNER_OBJECT_CLAZZ_LIST. - config source: both ConfigSourceLoadUnitHook copies decorated with @LoadUnitLifecycleProto. - LoadUnitMultiInstanceProtoHook registration dropped on both hosts — its preCreate is empty and the static set has no consumers. Host wiring: - ModuleHandler.registerInnerObjectClazzList() buffers plugin-provided classes (aop/dal boots now register one list in configDidLoad instead of constructing and registering hooks); the egg host provides moduleConfigs/runtimeConfig/logger as PRIVATE inner objects so they stay visible to inner objects only and never pollute cross-unit resolution (InnerObject/ProvidedInnerObjectProto gain accessLevel). - StandaloneApp feeds the built-in lists, always provides a logger inner object, and drops all manual register/delete bookkeeping — lifecycle protos deregister with the InnerObjectLoadUnit. - InnerObjectLoadUnitBuilder dedupes by class: hosts hard-feed built-in lists while the owning package may also be scanned as an eggModule (e.g. @eggjs/dal-plugin as a module dependency) — first add wins. - test-util LoaderUtil mirrors the production loader and diverts inner object classes out of module load units. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Important Review skippedDraft detected. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Deploying egg with
|
| Latest commit: |
f4267df
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://5f0686fb.egg-cci.pages.dev |
| Branch Preview URL: | https://feat-module-plugin-core.egg-cci.pages.dev |
Codecov Report✅ All modified and coverable lines are covered by tests.
Additional details and impacted files@@ Coverage Diff @@
## next #6021 +/- ##
===========================================
- Coverage 81.95% 70.88% -11.08%
===========================================
Files 678 18 -660
Lines 20800 735 -20065
Branches 4147 181 -3966
===========================================
- Hits 17047 521 -16526
+ Misses 3235 181 -3054
+ Partials 518 33 -485 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
|
Dependency limit exceeded — report not shown. This pull request scan exceeded the 10,000-dependency limit applied to this scan, so the results are incomplete and may be inaccurate. To avoid reporting false positives, Socket has not posted a report. Upgrade your plan to raise the dependency limit and get complete reports, or view the partial scan in the dashboard. Socket is always free for open source. If this is a non-commercial open source project, contact us to request a free Team account. |
Deploying egg-v3 with
|
| Latest commit: |
f4267df
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://2fdfe456.egg-v3.pages.dev |
| Branch Preview URL: | https://feat-module-plugin-core.egg-v3.pages.dev |
There was a problem hiding this comment.
Code Review
This pull request implements a declarative module plugin mechanism using @InnerObjectProto and @EggLifecycleProto decorators, enabling framework extensions to be loaded into an InnerObjectLoadUnit before the business graph is built for both standalone and egg hosts. It also converts AOP, DAL, and ConfigSource hooks into module plugins and renames Runner to StandaloneApp. The review feedback suggests maintaining consistency by using .ts extensions instead of .js in import/export paths within aop-runtime, and defensively checking if caught exceptions are Error instances before accessing their message property in StandaloneApp.ts and main.ts to avoid runtime type errors.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| import { crossCutGraphHook } from './CrossCutGraphHook.js'; | ||
| import { pointCutGraphHook } from './PointCutGraphHook.js'; |
There was a problem hiding this comment.
According to the repository's general rules, TypeScript source file imports should use the .ts extension instead of .js to maintain consistency across the codebase.
| import { crossCutGraphHook } from './CrossCutGraphHook.js'; | |
| import { pointCutGraphHook } from './PointCutGraphHook.js'; | |
| import { crossCutGraphHook } from './CrossCutGraphHook.ts'; | |
| import { pointCutGraphHook } from './PointCutGraphHook.ts'; |
References
- In this repository, use '.ts' extensions in import/export paths for TypeScript source files to maintain consistency with the existing convention across source and test files.
| import { AopGraphHookRegistrar } from './AopGraphHookRegistrar.js'; | ||
| import { EggObjectAopHook } from './EggObjectAopHook.js'; | ||
| import { EggPrototypeCrossCutHook } from './EggPrototypeCrossCutHook.js'; | ||
| import { LoadUnitAopHook } from './LoadUnitAopHook.js'; |
There was a problem hiding this comment.
According to the repository's general rules, TypeScript source file imports should use the .ts extension instead of .js to maintain consistency across the codebase.
| import { AopGraphHookRegistrar } from './AopGraphHookRegistrar.js'; | |
| import { EggObjectAopHook } from './EggObjectAopHook.js'; | |
| import { EggPrototypeCrossCutHook } from './EggPrototypeCrossCutHook.js'; | |
| import { LoadUnitAopHook } from './LoadUnitAopHook.js'; | |
| import { AopGraphHookRegistrar } from './AopGraphHookRegistrar.ts'; | |
| import { EggObjectAopHook } from './EggObjectAopHook.ts'; | |
| import { EggPrototypeCrossCutHook } from './EggPrototypeCrossCutHook.ts'; | |
| import { LoadUnitAopHook } from './LoadUnitAopHook.ts'; |
References
- In this repository, use '.ts' extensions in import/export paths for TypeScript source files to maintain consistency with the existing convention across source and test files.
| export * from './AopGraphHookRegistrar.js'; | ||
| export * from './AopInnerObjectClazzList.js'; |
There was a problem hiding this comment.
According to the repository's general rules, TypeScript source file exports should use the .ts extension instead of .js to maintain consistency across the codebase.
| export * from './AopGraphHookRegistrar.js'; | |
| export * from './AopInnerObjectClazzList.js'; | |
| export * from './AopGraphHookRegistrar.ts'; | |
| export * from './AopInnerObjectClazzList.ts'; |
References
- In this repository, use '.ts' extensions in import/export paths for TypeScript source files to maintain consistency with the existing convention across source and test files.
| ctx.destroy(lifecycle).catch((e) => { | ||
| e.message = `[tegg/standalone] destroy tegg context failed: ${e.message}`; | ||
| console.warn(e); | ||
| }); |
There was a problem hiding this comment.
When handling a caught exception, defensively check if the caught value is an Error instance before accessing or mutating its message property. Mutating e.message directly can throw a TypeError in strict mode if e is a primitive value. Use String(e) as a fallback to safely format the error message.
| ctx.destroy(lifecycle).catch((e) => { | |
| e.message = `[tegg/standalone] destroy tegg context failed: ${e.message}`; | |
| console.warn(e); | |
| }); | |
| ctx.destroy(lifecycle).catch((e) => { | |
| const msg = e instanceof Error ? e.message : String(e); | |
| console.warn('[tegg/standalone] destroy tegg context failed: ' + msg); | |
| }); |
References
- When creating a new Error from a caught exception, check if the caught value is an 'Error' instance before accessing its 'message' property. Use 'String(err)' as a fallback for non-Error values to ensure a meaningful error message.
| app.destroy().catch((e) => { | ||
| e.message = `[tegg/standalone] destroy tegg failed: ${e.message}`; | ||
| console.warn(e); | ||
| }); |
There was a problem hiding this comment.
When handling a caught exception, defensively check if the caught value is an Error instance before accessing or mutating its message property. Mutating e.message directly can throw a TypeError in strict mode if e is a primitive value. Use String(e) as a fallback to safely format the error message.
| app.destroy().catch((e) => { | |
| e.message = `[tegg/standalone] destroy tegg failed: ${e.message}`; | |
| console.warn(e); | |
| }); | |
| app.destroy().catch((e) => { | |
| const msg = e instanceof Error ? e.message : String(e); | |
| console.warn('[tegg/standalone] destroy tegg failed: ' + msg); | |
| }); |
References
- When creating a new Error from a caught exception, check if the caught value is an 'Error' instance before accessing its 'message' property. Use 'String(err)' as a fallback for non-Error values to ensure a meaningful error message.
…inner unit Review follow-ups on the module plugin PR: - ConfigSourceLoadUnitHook existed twice (standalone + egg plugin copy) — exactly the duplication the module plugin mechanism is meant to end. Move the single host-agnostic class to @eggjs/metadata (hook/); both hosts now feed the same import into their inner-object clazz lists. - ModuleHandler tracked the inner-object load unit inside the business loadUnits list, forcing INNER_OBJECT_LOAD_UNIT_TYPE filters in the init loop and contextModuleCompatible. Track it in its own field: business loops need no filtering, destroy still tears it down last (its lifecycle protos must outlive every hooked object). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Motivation
Tegg's framework-level extensions (AOP, DAL, controller registration, config source, ...) all rely on the HOST hand-registering lifecycle hooks in its boot code. A module cannot declare "run this when a load unit is created" by itself, so:
Runner;This PR ports the declarative module plugin design from eggjs/tegg#325 (merged to the stale
standalone-nextbranch, never reached master) to thenextarchitecture, and completes the two parts #325 left open: egg-host wiring (inner object protos used to be silently ignored in app mode) and bootstrapping the built-in hooks themselves (the#handleCompatibilityTODO).Design
A plain eggModule package can now provide framework extensions declaratively:
@InnerObjectProto— framework inner object (a SingletonProto withEGG_INNER_OBJECT_PROTO_IMPL_TYPE), diverted by the loader intoModuleDescriptor.innerObjectClazzList, never into business load units.@EggLifecycleProtofive typed variants (LoadUnit/LoadUnitInstance/EggPrototype/EggObject/EggContext) — a DI-capable hook object, auto-registered into the matching scope-aware LifecycleUtil and deregistered symmetrically.Two-phase ordering (the load-bearing constraint).
GlobalGraph.create()only adds nodes;build()adds inject edges and consumesregisterBuildHookhooks once at its end (late registration is silently lost). Both hosts now boot as:GlobalGraph.create(nodes only)InnerObjectLoadUnit(own topologically sorted proto graph, cycle detection, hard error on missing non-optional deps) — hooks register here, including graph build hooks from@LifecyclePostInject(seeAopGraphHookRegistrar)build()/sort()→ business load units → business instancesCommits
feat(core): add module plugin core mechanismEggInnerObjectPrototypeImpl,InjectObjectPrototypeFinder/ProtoGraphUtilsextracted with a proto-name index replacing the O(n·m·n) scan) + runtime (EggInnerObjectImplruns only decorator-declared self lifecycle; host-agnosticInnerObjectLoadUnit(Builder/Instance)) + loader diversion (manifestgetDecoratedFilescovers the new list so bundle mode still re-imports those files)feat(standalone): two-phase StandaloneApp with module plugin supportRunnerrenamed toStandaloneApp(no alias;main()/preLoad()unchanged). Explicit boot phases,frameworkDepsoption, bundle-mode manifest/loaderFS consumption +StandaloneApp.loadMetadata(), tegg#325 test suite portedfeat(tegg-plugin): instantiate InnerObjectLoadUnit in app modeModuleHandler.init()phases viaEggModuleLoader.initGraph()/load()split; the same module plugin package behaves identically under both hostsfeat(tegg): convert built-in framework hooks to module pluginsAopGraphHookRegistrarfor the crossCut/pointCut build hooks), DAL (constructor args →@Injectof host-providedmoduleConfigs/runtimeConfig/loggerinner objects, PRIVATE on the egg host), ConfigSource; vestigialLoadUnitMultiInstanceProtoHookregistration dropped (emptypreCreate, unconsumed static set); builder dedupes by class since a package may be both hard-fed and scanned as an eggModule (e.g.@eggjs/dal-plugin)docs(wiki): record tegg module plugin architectureTest evidence
InnerObjectLoadUnitintegration incl. decorator-only-lifecycle semantics / auto register+deregister / cycle detection / missing-dep hard error (4), standalone module plugin suite ported from tegg#325 — five lifecycle proto types + inner object PUBLIC/PRIVATE semantics (7), app-mode module plugin fixture (2).MultiAppisolation green; full-repo run has zero failures related to this change (remaining ones verified as pre-existing/env: dns-cache needs a freshut install, multipart fails on cleannexttoo, onerror/development are flaky-on-rerun).Notes
git merge-treeclean; combined-tree run of both test suites green).🤖 Generated with Claude Code