feat(tegg): declarative module plugin mechanism#6021
Conversation
📝 WalkthroughWalkthroughThis PR introduces a host-agnostic "Tegg Module Plugin" mechanism: new ChangesTegg Module Plugin Mechanism
Estimated code review effort: 5 (Critical) | ~120 minutes Sequence Diagram(s)sequenceDiagram
participant App as StandaloneApp / ModuleHandler
participant Builder as InnerObjectLoadUnitBuilder
participant LoadUnit as InnerObjectLoadUnit
participant Instance as InnerObjectLoadUnitInstance
participant Business as BusinessLoadUnits
App->>App: initGraph()
App->>Builder: addInnerObjectClazzList / createLoadUnit
Builder->>LoadUnit: build proto graph and register
App->>Instance: instantiate inner load unit
Instance->>Instance: register lifecycle hooks
App->>Business: instantiate business load units
Business-->>App: readiness confirmed
Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 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: |
79594a6
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://702e5979.egg-cci.pages.dev |
| Branch Preview URL: | https://feat-module-plugin-core.egg-cci.pages.dev |
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## next #6021 +/- ##
==========================================
+ Coverage 81.96% 81.99% +0.03%
==========================================
Files 678 687 +9
Lines 20800 21102 +302
Branches 4147 4196 +49
==========================================
+ Hits 17048 17302 +254
- Misses 3235 3285 +50
+ Partials 517 515 -2 ☔ 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: |
79594a6
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://f4946e5d.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.
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>
…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>
Review follow-up: hand-fed inner-object class lists defeated the module plugin's purpose — the packages already declare eggModule metadata, so consume them AS modules: - StandaloneApp: aop-runtime/dal-plugin join the regular module scan as built-in framework modules (their @InnerObjectProto/@XxxLifecycleProto classes are diverted into the InnerObjectLoadUnit by loadApp); the AOP_/DAL_INNER_OBJECT_CLAZZ_LIST exports are gone. Module references now dedupe by path (an app may also depend on a built-in module directly). - egg host: moduleHandler.registerInnerObjectModule(path) — the aop/dal plugins register their package as a scanned module instead of a class list; manifest collection includes registered framework modules. - aop-runtime re-exports CrosscutAdviceFactory (decorated in aop-decorator) so the scan picks it up as a module member, and no longer needs the manual list file. - LoaderUtil.filePattern: '!**/test' does not exclude descendants — add '!**/test/**' (and coverage) so scanning workspace packages that ship test dirs stays safe; built-in references also exclude test fixture modules from the reference scan. - Drop the test-only optional constructor params from LoadUnitAopHook / EggPrototypeCrossCutHook; the aop-runtime tests wire the dependency via Reflect.set on the DI property instead. registerInnerObjectClazzList stays for host-boot wiring (e.g. the shared ConfigSourceLoadUnitHook) and edge hosts. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…the contract Second review round: - Drop registerInnerObjectModule (added one commit ago): a central register is exactly what module plugins should not need. The egg host resolves module plugins from what already exists: tegg-config's ModuleScanner picks eggModule-declaring packages up from app/framework dependencies, and EggModuleLoader.resolveModuleReferences() fills the remaining gap by auto-including every ENABLED plugin whose package declares eggModule (ModuleConfigUtil.hasEggModule). Enabling the plugin is the whole contract. - plugin/aop is now itself the AOP module (eggModule: teggAop) and re-exports the aop-runtime/aop-decorator hook classes for the scan; plugin/dal already declared eggModule - both app.ts registrations are gone. aop-runtime keeps its own eggModule for the standalone built-in. - InnerObjectLoadUnitBuilder: remove the #seenClazzSet dual-arrival dedupe - reference-level path dedupe covers the legitimate case now that hand-fed lists are gone; a genuine double registration fails loud. Host-provided instances now resolve through the SAME selectProto rules as graph protos (name + qualifiers + access level) instead of a bare name whitelist; they still skip the topological sort - an already-constructed instance has no construction order and no outgoing edges. - test-util buildGlobalGraph reuses LoaderFactory.loadApp for classification instead of re-implementing the inner-object diversion. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…in the graph Third review round: - Only plugins are modules: aop-runtime drops its eggModule declaration (pure library again); @eggjs/aop-plugin (eggModule: teggAop) is THE aop module for BOTH hosts - its egg imports are all type-only, so the standalone built-in reference now points at the plugin package, same as the egg host resolves it from the enabled-plugin list. One module name, one carrier. resolveModuleReferences locates the real package root from plugin.path (which points inside the package, through symlinks). - Host-provided inner objects are now REAL graph nodes: same descriptors, same vertices, same edge resolution and topological sort as hook protos (ProtoNode ids include qualifiers, so multi-entry names like per-module moduleConfig coexist). Only the instantiation list excludes them - the instances already exist. The selectProto fallback branch is gone; one resolution path. - aop-runtime tests no longer feed the package root as a module (hooks are registered manually there); fixture .egg manifest caches are stale-prone local artifacts, not repo files. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Restore the pre-refactor design (StandaloneInnerObjectProto) inside the new graph: a provided instance's descriptor is a regular ClassProtoDescriptor whose clazz is the factory `() => obj`, with protoImplType PROVIDED_INNER_OBJECT dispatched through the standard creator registry (the same polymorphism point as COMPATIBLE/INNER_OBJECT proto types). "Constructing" one returns the instance. One uniform channel: the builder feeds every descriptor - hooks and provided alike - through the same vertices, edge resolution, topological sort, createProtoByDescriptor loop and instantiation loop. The sort-output filter and the load unit's separate #innerObjects registration channel are both gone; InnerObjectLoadUnitOptions.innerObjects is removed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Vestige of the intermediate round where aop-runtime itself was the scanned module; plugin/aop (the actual aop module) re-exports CrosscutAdviceFactory from aop-decorator directly. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ed-plugin fallback
Fourth review round, closing the discovery question:
The enabled-plugin auto-include (resolveModuleReferences + hasEggModule)
was solving a fixture problem with a mechanism. The pre-existing contract
already covers plugins-as-modules on BOTH declaration styles:
- explicit config/module.json: an npm-form reference to the plugin package
(the dal fixture has carried `{"package": "../../../../"}` all along -
that is HOW its plugin module loaded before this PR);
- scan mode: eggModule-declaring packages in app/framework dependencies
(the standalone dal fixture declares `@eggjs/dal-plugin` in deps).
What broke was never the mechanism: the aop/dal/controller egg fixtures
enable plugins whose hooks now ride the module scan, without declaring the
plugin package as a module. Before the refactor that omission was invisible
(hooks were hand-registered). Fix the fixtures to follow the dal precedent
(`{"package": "@eggjs/aop-plugin"}` in module.json) and revert the
discovery machinery: EggModuleLoader is back to app.moduleReferences,
hasEggModule is gone.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
ProvidedInnerObjectProto stored the `() => obj` factory in a field named `clazz` typed EggProtoImplClass (inherited from the old StandaloneInnerObjectProto trick), making constructEggObject read like an instantiation. Rename to `objFactory: () => object` and call it directly - the only casts left sit at the EggPrototypeLifecycleContext boundary, whose `clazz` slot is the carrier, with comments saying so. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… compat Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Core tegg semantics with no declaring app: scan discovery would leak host internals into user module.json. One shared class, one line per host. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… the config module Review suggestion: the config-domain hook belongs to the config package, and that package should simply BE a module. This kills the last clazz-list registration and with it the whole registerInnerObjectClazzList API. - @eggjs/tegg-config declares eggModule (teggConfig) and hosts the hook in src/lib (host-safe: its egg imports are type-only). Both hosts consume it as a scanned module: standalone via the third built-in framework module reference; the egg host via the framework-dependency channel. - ModuleScanner now takes the RUNTIME framework dir (app.options.framework, what egg/mm actually resolved) over re-deriving from appPkg.egg.framework - the gap that made framework-deps discovery invisible in test harnesses. With it, packages/egg's own dependencies (aop/dal/config plugins) reach every fixture as OPTIONAL modules, promoted by the existing enabled loop; the fixture module.json declarations added earlier are retired. - ModuleHandler.registerInnerObjectClazzList and its buffer are gone - every hook now arrives through the module scan; ReadModule asserts the production shape (business refs exact, framework refs optional). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Follow-up from asking whether standalone can reuse ModuleScanner (it can't: the scanner's increment over readModuleReference is egg-only semantics - single frameworkDir + optional marking gated by plugin enablement, and standalone has no plugin system; the shared primitives already live in common-util): - Graph sort only gates optional modules' BUSINESS protos; their inner hooks were instantiated regardless, so a disabled plugin's hooks silently loaded. instantiateInnerObjectLoadUnit now skips unpromoted optional descriptors - symmetric gating. - That gate exposed a long-dormant bug: buildAppGraph's enabled-promotion matched plugin.path (inside the package, through symlinks) against reference paths (real package roots) and never fired. Resolve the real package root before matching. - StandaloneApp switches its hand-rolled path dedupe to the shared ModuleConfigUtil.deduplicateModules (same first-wins semantics, plus duplicate-name conflict detection). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Same pattern as moduleConfigs: the constructor registers an empty runtimeConfig object as the inner object, and loadConfigs (init phase) assigns its values. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
options.logger backs the logger inner object and the loadMetadata loader; an explicit innerObjectHandlers logger entry still wins, console remains the last resort. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Same contract as ServiceWorkerApp.init: a repeated call returns instead of re-pushing moduleConfig inner objects and re-creating load units. Matters more now that config loading runs inside init(). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
loadConfigs fills the constructor's own placeholder list instead of whatever sits under innerObjects.moduleConfig, so a host that overrides the key via innerObjectHandlers fully replaces the built-in entries — the pre-refactor replace-wins behavior. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… of use Align with the egg host's ModuleHandler: moduleConfigs/moduleConfig/ runtimeConfig/mysqlDataSourceManager/logger are assembled when the InnerObjectLoadUnit is created, not pre-baked in the constructor with placeholder back-fill. The constructor's innerObjects field now carries only the host contract (innerObjectHandlers plus additions between new and init), merged over the base objects so host entries win on name clash. Removes the runtimeConfig/moduleConfig placeholder fields and the constructor's scope-dependent work (no more try/catch or runInScope). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Constructor takes StandaloneAppInit (frameworkDeps/dump/innerObjects/ logger) — capability wiring only, with the runtimeConfig/moduleConfig(s) placeholders pre-created and registered as inner objects. App binding (name/env/baseDir, plus our dependencies/manifest/loaderFS extras) moves to init(opts): the module scan, config loading and placeholder fill all happen there, so runtimeConfig values come from init options, not the constructor. main(cwd, options) keeps its flat signature and delegates to the new appMain(options, init, ctx) entry, same as the upstream PR. Ref: eggjs/tegg#325 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…age.json Drop the hand-maintained builtinFrameworkModules list. The standalone package root joins the scan as the built-in framework root, so its own package.json dependencies that declare eggModule (aop/dal/config plugin packages) are discovered through the same node_modules convention as app dependencies. Adding a framework capability is now just adding the dependency. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…self DalModuleLoadUnitHook gains an EggObjectLifecycle destroy that clears MysqlDataSourceManager/SqlMapManager/TableModelManager when the InnerObjectLoadUnit instance goes down (after every business load unit) — the standalone counterpart of the dal plugin's egg-side beforeClose. StandaloneApp drops its dal-plugin import entirely: the unconsumed mysqlDataSourceManager inner object entry (no injector anywhere) and the host-side clear() calls are gone, and with no scope-resolved work left in the constructor the runInScope/try-catch wrapper goes too. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…e dal module The PUBLIC `@Inject() mysqlDataSourceManager` surface (consumed by business modules; no in-repo framework consumer) is now provided by the dal module itself via an @InnerObjectProto accessor whose constructor return-override hands out the per-app TeggScope singleton the dal hooks populate. Both hosts get the surface uniformly through the module scan; StandaloneApp drops its dal-plugin import again — this time for real. Adds a standalone regression test pinning the injection surface. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Inner objects never fall back to interface method names for self lifecycle (EggInnerObjectImpl#callObjectLifecycle guards against hook callback collisions), so the bare destroy() added earlier was never invoked. Decorate the renamed destroyManagers with @LifecycleDestroy and pin the behavior with a deterministic standalone test: the app's per-scope MysqlDataSourceManager holds the datasource after init and is empty after destroy. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The merged interface gives the wrapper the manager's full public type surface — matching what its constructor actually returns — so the unknown-cast goes away and typed usage is sound. Export it from the package entry. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The aop/dal/config plugin packages are consumed through the package.json-driven module scan, not code imports — being a dependency IS the declaration. The unused check cannot see that, so ignore them explicitly. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The dts build (rolldown-plugin-dts with --isolatedDeclarations) requires explicit return annotations on the new module-plugin decorators: EggLifecycleProto/InnerObjectProto return PrototypeDecorator like the existing proto decorators, and the five lifecycle decorator factories get a named factory type. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
cnpmcore E2E exposed the gap: apps on the base framework declare no
pkg.egg.framework (cnpmcore's egg section is just {typescript:true}), so
ModuleScanner skipped the framework scan entirely — aop/dal/config
module plugins were never discovered and their hooks silently did not
load (AsyncTimer @advice logs missing). Default the framework to 'egg'
per egg-core convention, tolerate unresolvable frameworks for bare
fixtures, and drop the egg.framework declarations that earlier review
rounds added to plugin test fixtures so the suites now cover the
cnpmcore shape. Also: update the @eggjs/tegg helper export snapshot for
PROVIDED_INNER_OBJECT_PROTO_IMPL_TYPE, sync generated exports maps for
files added/moved by this PR, and make ReadModule assertions robust to
framework-discovered optional references.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Delegate to the canonical @eggjs/utils convention: explicit pkg.egg.framework wins, default egg otherwise. Tried and rejected deriving from loader.eggPaths — the outermost chain entry is @eggjs/mock under tests, not the business framework. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
cnpmcore-snapshot E2E: framework plugin module references restored from a bundle manifest point at directories the bundler never materializes (their code ships externally), so loadModuleConfigs crashed the snapshot build boot reading package.json. Use the manifest-carried name and an empty config when the module dir is absent; disk stays authoritative when it exists. Also refresh the @eggjs/tegg dal export snapshot (hand-fed clazz list removed, MysqlDataSourceManagerObject added). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…g load - ModuleDescriptorDumper: JSON.stringify the string fields; raw interpolation of unitPath/filePath produced invalid JSON on Windows (backslash escapes) — exposed by the new LoaderInnerObject dump test. - ModuleConfigLoader: same tolerance as the config plugin for framework plugin modules restored from a bundle manifest whose directories are not materialized in the bundle output — use the manifest-carried name and skip module.yml loading when the dir is absent. Fixes the cnpmcore-snapshot restore boot (EggAppLoader.load crashed on ENOENT). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The tegg vitest projects do not inherit the root hookTimeout (extends is disabled), so app-boot beforeAll hooks run with the 10s default — windows-latest/node22 tipped over on the aop boot and the BundledAppBoot double boot while every test passed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
20s matches the root hookTimeout the tegg projects fail to inherit; BundledAppBoot gets 3x for its two boots plus manifest generation. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
5001fb6 to
79594a6
Compare
There was a problem hiding this comment.
Pull request overview
This PR introduces a declarative “module plugin” mechanism for Tegg so eggModule packages can contribute framework inner objects and lifecycle hooks without host boot-code registration, and updates both the standalone host and egg host to boot in a two-phase order that guarantees graph build hooks are registered before GlobalGraph.build() consumes them.
Changes:
- Add
@InnerObjectProto/@EggLifecycleProto(five lifecycle variants) and runtime support via anInnerObjectLoadUnitinstantiated before business load units. - Refactor standalone host: rename
Runner→StandaloneApp, add bundle-manifest support, and update/expand module-plugin test coverage. - Update egg host boot wiring (
ModuleHandler/EggModuleLoader) to instantiate theInnerObjectLoadUnitbefore loading business units; migrate built-in AOP/DAL/ConfigSource hooks to module-plugin form; document the architecture inwiki/.
Reviewed changes
Copilot reviewed 127 out of 127 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| wiki/log.md | Logs the new module-plugin architecture work and host ordering constraints. |
| wiki/index.md | Adds an index entry for the new module-plugin concept page. |
| wiki/concepts/tegg-module-plugin.md | Documents the declarative module-plugin design and two-phase boot ordering. |
| tegg/standalone/standalone/tsdown.config.ts | Configures unused-check ignore list for framework module-plugin deps. |
| tegg/standalone/standalone/test/ModulePlugin.test.ts | Adds standalone integration coverage for lifecycle protos + inner objects. |
| tegg/standalone/standalone/test/index.test.ts | Updates tests for StandaloneApp, adds logger option and DAL cleanup coverage. |
| tegg/standalone/standalone/test/fixtures/logger-option/package.json | Adds a fixture eggModule for logger injection tests. |
| tegg/standalone/standalone/test/fixtures/logger-option/foo.ts | Fixture runner validating injected logger inner object. |
| tegg/standalone/standalone/test/fixtures/load-unit-lifecycle-proto/Runner.ts | Fixture validating LoadUnit lifecycle proto timing + DI. |
| tegg/standalone/standalone/test/fixtures/load-unit-lifecycle-proto/package.json | Adds eggModule metadata for the lifecycle-proto fixture. |
| tegg/standalone/standalone/test/fixtures/load-unit-lifecycle-proto/FooLoadUnitHook.ts | Fixture hook that mutates business load unit protos during preCreate. |
| tegg/standalone/standalone/test/fixtures/load-unit-lifecycle-proto/Foo.ts | Fixture inner object proto used by lifecycle hook DI. |
| tegg/standalone/standalone/test/fixtures/load-unit-instance-lifecycle-proto/package.json | Adds eggModule metadata for LoadUnitInstance lifecycle fixture. |
| tegg/standalone/standalone/test/fixtures/load-unit-instance-lifecycle-proto/FooLoadUnitInstanceHook.ts | Fixture LoadUnitInstance hook mutating created objects. |
| tegg/standalone/standalone/test/fixtures/load-unit-instance-lifecycle-proto/Foo.ts | Fixture runner exposing mutation performed by instance hook. |
| tegg/standalone/standalone/test/fixtures/invalid-inner-object-inject/package.json | Adds eggModule metadata for private-inner-object injection failure test. |
| tegg/standalone/standalone/test/fixtures/invalid-inner-object-inject/innerBar.ts | Fixture private inner object proto. |
| tegg/standalone/standalone/test/fixtures/invalid-inner-object-inject/foo.ts | Fixture business proto that incorrectly injects private inner object. |
| tegg/standalone/standalone/test/fixtures/inner-object-proto/package.json | Adds eggModule metadata for inner-object injection fixture. |
| tegg/standalone/standalone/test/fixtures/inner-object-proto/innerBar.ts | Fixture PUBLIC inner object proto with DI dependency on another inner object. |
| tegg/standalone/standalone/test/fixtures/inner-object-proto/foo.ts | Fixture business proto injecting a PUBLIC inner object. |
| tegg/standalone/standalone/test/fixtures/egg-prototype-lifecycle-proto/package.json | Adds eggModule metadata for EggPrototype lifecycle fixture. |
| tegg/standalone/standalone/test/fixtures/egg-prototype-lifecycle-proto/FooEggPrototypeHook.ts | Fixture prototype hook writing metadata from prototype creation. |
| tegg/standalone/standalone/test/fixtures/egg-prototype-lifecycle-proto/Foo.ts | Fixture runner reading data set by prototype hook. |
| tegg/standalone/standalone/test/fixtures/egg-object-lifecycle-proto/package.json | Adds eggModule metadata for EggObject lifecycle fixture. |
| tegg/standalone/standalone/test/fixtures/egg-object-lifecycle-proto/FooEggObjectHook.ts | Fixture object hook mutating created object. |
| tegg/standalone/standalone/test/fixtures/egg-object-lifecycle-proto/Foo.ts | Fixture runner reading mutation from object hook. |
| tegg/standalone/standalone/test/fixtures/egg-context-lifecycle-proto/package.json | Adds eggModule metadata for EggContext lifecycle fixture. |
| tegg/standalone/standalone/test/fixtures/egg-context-lifecycle-proto/FooEggContextHook.ts | Fixture context hook initializing context state. |
| tegg/standalone/standalone/test/fixtures/egg-context-lifecycle-proto/Foo.ts | Fixture runner reading ctx state via ContextHandler. |
| tegg/standalone/standalone/test/fixtures/dal-manager-inject/package.json | Adds eggModule metadata for DAL manager injection fixture. |
| tegg/standalone/standalone/test/fixtures/dal-manager-inject/foo.ts | Fixture pinning PUBLIC mysqlDataSourceManager injection surface. |
| tegg/standalone/standalone/src/StandaloneLoadUnit.ts | Removes legacy standalone “inner object as a load unit” implementation. |
| tegg/standalone/standalone/src/StandaloneInnerObjectProto.ts | Removes legacy standalone inner-object prototype implementation. |
| tegg/standalone/standalone/src/StandaloneInnerObject.ts | Removes legacy standalone inner-object EggObject implementation. |
| tegg/standalone/standalone/src/StandaloneApp.ts | New standalone host implementing two-phase boot with InnerObjectLoadUnit. |
| tegg/standalone/standalone/src/Runner.ts | Removes old standalone host (replaced by StandaloneApp). |
| tegg/standalone/standalone/src/main.ts | Updates main entry to use StandaloneApp and introduces appMain. |
| tegg/standalone/standalone/src/index.ts | Updates exports to expose StandaloneApp instead of removed legacy types. |
| tegg/standalone/standalone/src/EggModuleLoader.ts | Adds manifest/loaderFS support and exposes moduleDescriptors for inner unit build. |
| tegg/standalone/standalone/src/ConfigSourceLoadUnitHook.ts | Removes standalone-specific ConfigSource hook (now host-agnostic module plugin). |
| tegg/standalone/standalone/package.json | Switches framework deps to module-plugin packages and adds loader-fs/tegg-config. |
| tegg/plugin/tegg/test/ModulePlugin.test.ts | Adds app-mode integration coverage for module-plugin lifecycle protos + inner objects. |
| tegg/plugin/tegg/test/fixtures/apps/module-plugin-app/package.json | Adds ESM test app fixture root. |
| tegg/plugin/tegg/test/fixtures/apps/module-plugin-app/modules/plugin-module/PluginHooks.ts | Fixture module plugin hooks (LoadUnit + EggObject lifecycle protos). |
| tegg/plugin/tegg/test/fixtures/apps/module-plugin-app/modules/plugin-module/package.json | Adds eggModule metadata for the fixture module. |
| tegg/plugin/tegg/test/fixtures/apps/module-plugin-app/modules/plugin-module/InnerRegistry.ts | Fixture inner objects with PRIVATE/PUBLIC semantics and DI. |
| tegg/plugin/tegg/test/fixtures/apps/module-plugin-app/modules/plugin-module/HelloService.ts | Fixture business singleton injecting a PUBLIC inner object. |
| tegg/plugin/tegg/test/fixtures/apps/module-plugin-app/config/plugin.ts | Enables teggConfig and tegg plugin for app-mode fixture. |
| tegg/plugin/tegg/test/fixtures/apps/module-plugin-app/config/module.json | Declares the fixture module path for scanning. |
| tegg/plugin/tegg/test/fixtures/apps/module-plugin-app/config/config.default.ts | Minimal keys config for egg mock app boot. |
| tegg/plugin/tegg/test/BundledAppBoot.test.ts | Extends timeout to accommodate additional boots/manifest generation. |
| tegg/plugin/tegg/src/lib/ModuleHandler.ts | Instantiates InnerObjectLoadUnit before business units; reverses teardown ordering. |
| tegg/plugin/tegg/src/lib/ModuleConfigLoader.ts | Adds bundle-mode tolerance when module dirs don’t exist on disk. |
| tegg/plugin/tegg/src/lib/EggModuleLoader.ts | Splits initGraph() vs load(), adds manifest collection changes, promotes enabled plugins reliably. |
| tegg/plugin/tegg/src/app.ts | Removes imperative registration for config-source + multi-instance hook (now module plugins / vestigial removed). |
| tegg/plugin/tegg/package.json | Removes export for deleted ConfigSourceLoadUnitHook. |
| tegg/plugin/dal/src/lib/TransactionPrototypeHook.ts | Converts DAL transaction hook to module-plugin lifecycle proto + DI-injected deps. |
| tegg/plugin/dal/src/lib/MysqlDataSourceManagerObject.ts | Adds PUBLIC inner-object wrapper exposing DAL manager to business modules. |
| tegg/plugin/dal/src/lib/DalTableEggPrototypeHook.ts | Converts DAL table hook to module-plugin lifecycle proto with DI logger. |
| tegg/plugin/dal/src/lib/DalModuleLoadUnitHook.ts | Converts DAL load-unit hook to module-plugin + adds @LifecycleDestroy cleanup. |
| tegg/plugin/dal/src/index.ts | Exports the new MysqlDataSourceManagerObject. |
| tegg/plugin/dal/src/app.ts | Removes imperative hook registration; retains egg-side manager cleanup in beforeClose. |
| tegg/plugin/dal/package.json | Adds export mapping for MysqlDataSourceManagerObject. |
| tegg/plugin/config/test/ReadModule.test.ts | Updates expectations to account for optional framework module references. |
| tegg/plugin/config/src/lib/ModuleScanner.ts | Resolves framework dir via getFrameworkPath and scans framework modules as optional. |
| tegg/plugin/config/src/lib/ConfigSourceLoadUnitHook.ts | Converts config-source hook into host-agnostic module-plugin lifecycle proto. |
| tegg/plugin/config/src/app.ts | Adds bundle-mode tolerance for missing external module dirs/configs. |
| tegg/plugin/config/package.json | Marks tegg-config as an eggModule and exports ConfigSourceLoadUnitHook. |
| tegg/plugin/aop/test/aop.test.ts | Increases timeout to avoid slow-runner failures. |
| tegg/plugin/aop/src/InnerObjects.ts | Re-exports AOP module-plugin classes for scanning under the aop plugin eggModule. |
| tegg/plugin/aop/src/app.ts | Removes imperative hook registration; documents which hooks remain host-registered. |
| tegg/plugin/aop/package.json | Marks aop plugin as an eggModule and exports InnerObjects entry. |
| tegg/core/types/test/snapshots/index.test.ts.snap | Snapshot updates for new exported constants/types. |
| tegg/core/types/src/metadata/model/ProtoDescriptor.ts | Extends InjectObjectDescriptor with optional?: boolean. |
| tegg/core/types/src/core-decorator/Prototype.ts | Adds EGG_INNER_OBJECT_PROTO_IMPL_TYPE constant. |
| tegg/core/types/src/core-decorator/model/index.ts | Exposes EggLifecycleInfo type. |
| tegg/core/types/src/core-decorator/model/EggLifecycleInfo.ts | Adds EggLifecycleInfo model type. |
| tegg/core/types/src/core-decorator/InnerObjectProto.ts | Adds InnerObjectProto params type. |
| tegg/core/types/src/core-decorator/index.ts | Exports EggLifecycleProto + InnerObjectProto types. |
| tegg/core/types/src/core-decorator/EggLifecycleProto.ts | Adds EggLifecycleProto param types. |
| tegg/core/types/package.json | Adds export map entries for new core-decorator typings. |
| tegg/core/test-util/src/LoaderUtil.ts | Reuses production loader classification to match inner-object diversion behavior in tests. |
| tegg/core/tegg/test/snapshots/helper.test.ts.snap | Snapshot updates for new exports. |
| tegg/core/tegg/test/snapshots/exports.test.ts.snap | Snapshot updates for new exports/decorators. |
| tegg/core/tegg/test/snapshots/dal.test.ts.snap | Snapshot updates for DAL exports. |
| tegg/core/runtime/test/InnerObjectLoadUnit.test.ts | Adds integration tests for inner objects, DI wiring, lifecycle proto registration, and error cases. |
| tegg/core/runtime/test/snapshots/index.test.ts.snap | Snapshot updates for new runtime exports. |
| tegg/core/runtime/src/impl/ProvidedInnerObjectProto.ts | Adds proto/object for host-provided inner objects (provided-instance factory). |
| tegg/core/runtime/src/impl/InnerObjectLoadUnitInstance.ts | Implements instantiation + auto register/deregister of lifecycle protos by type. |
| tegg/core/runtime/src/impl/InnerObjectLoadUnitBuilder.ts | Builds a dedicated inner-object proto graph (toposort/cycle detect/missing dep errors). |
| tegg/core/runtime/src/impl/InnerObjectLoadUnit.ts | Introduces the host-agnostic InnerObjectLoadUnit type. |
| tegg/core/runtime/src/impl/index.ts | Exposes new inner-object runtime types and EggInnerObjectImpl. |
| tegg/core/runtime/src/impl/EggInnerObjectImpl.ts | Adds inner-object EggObject implementation that only runs decorator-declared self lifecycle. |
| tegg/core/metadata/test/ModuleDescriptorDumper.test.ts | Updates dumper tests to include innerObjectClazzList. |
| tegg/core/metadata/test/snapshots/index.test.ts.snap | Snapshot updates for new metadata exports. |
| tegg/core/metadata/src/model/ProtoDescriptorHelper.ts | Supports define-vs-instance module/unit fields for diverted protos. |
| tegg/core/metadata/src/model/ModuleDescriptor.ts | Adds innerObjectClazzList and fixes JSON dumping on Windows path escaping. |
| tegg/core/metadata/src/model/graph/ProtoGraphUtils.ts | Extracts proto dependency resolution utilities + name index for perf. |
| tegg/core/metadata/src/model/graph/index.ts | Exports ProtoGraphUtils. |
| tegg/core/metadata/src/model/graph/GlobalGraph.ts | Uses ProtoGraphUtils + lazy name index to avoid O(n·m·n) scanning. |
| tegg/core/metadata/src/impl/InjectObjectPrototypeFinder.ts | Extracts inject-proto selection logic for reuse and optional handling. |
| tegg/core/metadata/src/impl/index.ts | Exports EggInnerObjectPrototypeImpl + InjectObjectPrototypeFinder. |
| tegg/core/metadata/src/impl/EggPrototypeBuilder.ts | Refactors prototype building to use InjectObjectPrototypeFinder. |
| tegg/core/metadata/src/impl/EggInnerObjectPrototypeImpl.ts | Adds proto impl type for inner objects built from metadata descriptors. |
| tegg/core/loader/test/LoaderInnerObject.test.ts | Adds coverage that inner-object classes are diverted and included in manifest decorated files. |
| tegg/core/loader/test/fixtures/modules/module-with-inner-object/package.json | Adds test module fixture with eggModule metadata. |
| tegg/core/loader/test/fixtures/modules/module-with-inner-object/HelloService.ts | Fixture business singleton proto. |
| tegg/core/loader/test/fixtures/modules/module-with-inner-object/FetchRouter.ts | Fixture inner object proto. |
| tegg/core/loader/test/fixtures/modules/module-with-inner-object/ControllerHook.ts | Fixture lifecycle proto injecting inner object. |
| tegg/core/loader/src/LoaderUtil.ts | Tightens glob excludes to also omit test/** and coverage/**. |
| tegg/core/loader/src/LoaderFactory.ts | Diverts inner-object protos into innerObjectClazzList ahead of business clazzList. |
| tegg/core/core-decorator/test/inner-object-decorators.test.ts | Adds tests for InnerObjectProto/EggLifecycleProto metadata semantics. |
| tegg/core/core-decorator/test/snapshots/index.test.ts.snap | Snapshot updates for new decorator exports. |
| tegg/core/core-decorator/src/util/PrototypeUtil.ts | Adds metadata flags for inner objects + lifecycle protos and stores lifecycle type metadata. |
| tegg/core/core-decorator/src/decorator/InnerObjectProto.ts | Adds InnerObjectProto decorator mapping to inner-object proto impl type + flagging. |
| tegg/core/core-decorator/src/decorator/index.ts | Exports new decorators. |
| tegg/core/core-decorator/src/decorator/EggLifecycleProto.ts | Adds EggLifecycleProto decorator + five typed variants. |
| tegg/core/aop-runtime/test/aop-runtime.test.ts | Updates tests to inject CrosscutAdviceFactory via DI field instead of constructor. |
| tegg/core/aop-runtime/test/snapshots/index.test.ts.snap | Snapshot updates for new AOP runtime export. |
| tegg/core/aop-runtime/src/LoadUnitAopHook.ts | Converts to module-plugin lifecycle proto with injected CrosscutAdviceFactory. |
| tegg/core/aop-runtime/src/index.ts | Exports AopGraphHookRegistrar. |
| tegg/core/aop-runtime/src/EggPrototypeCrossCutHook.ts | Converts to module-plugin lifecycle proto with injected CrosscutAdviceFactory. |
| tegg/core/aop-runtime/src/EggObjectAopHook.ts | Converts to module-plugin EggObject lifecycle proto. |
| tegg/core/aop-runtime/src/AopGraphHookRegistrar.ts | Adds declarative registration of GlobalGraph build hooks via @LifecyclePostInject. |
| tegg/core/aop-runtime/package.json | Adds lifecycle dep and removes eggModule declaration from aop-runtime package. |
| tegg/core/aop-decorator/src/CrosscutAdviceFactory.ts | Marks CrosscutAdviceFactory as an inner-object proto for module-plugin instantiation. |
| export async function preLoad(cwd: string, dependencies?: StandaloneAppOptions['dependencies']): Promise<void> { | ||
| try { | ||
| await Runner.preLoad(cwd, dependencies); | ||
| await StandaloneApp.preLoad(cwd, dependencies); | ||
| } catch (e) { |
| `"name": ${JSON.stringify(clazz.name)},` + | ||
| (PrototypeUtil.getFilePath(clazz) | ||
| ? `"filePath": "${path.relative(moduleDescriptor.unitPath, PrototypeUtil.getFilePath(clazz)!)}"` | ||
| ? `"filePath": ${JSON.stringify(path.relative(moduleDescriptor.unitPath, PrototypeUtil.getFilePath(clazz)!))}` | ||
| : '') + | ||
| '}' |
There was a problem hiding this comment.
Actionable comments posted: 6
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
tegg/core/loader/src/LoaderFactory.ts (1)
61-62: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick winGive each module its own
multiInstanceClazzList. Declaring the array outside the loop makes everyModuleDescriptorshare the same list, so later modules mutate earlier descriptors andGlobalGraph.create()attributes multi-instance classes to the wrong module.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@tegg/core/loader/src/LoaderFactory.ts` around lines 61 - 62, Each ModuleDescriptor is currently sharing the same multiInstanceClazzList, so later iterations can mutate earlier descriptors and confuse GlobalGraph.create() attribution. Move the multiInstanceClazzList initialization so it is created per module inside the LoaderFactory logic that builds each ModuleDescriptor, and make sure each descriptor receives its own fresh array before pushing multi-instance classes.tegg/standalone/standalone/src/main.ts (1)
10-19: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winForward
frameworkDepsthroughpreLoad
preLoadonly acceptsdependencies, so callers can’t warm the same framework module set thatmain/StandaloneApp.preLoadscan at boot. AddframeworkDepshere and pass it through.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@tegg/standalone/standalone/src/main.ts` around lines 10 - 19, The preLoad wrapper only forwards dependencies, so callers cannot pass the same frameworkDeps set that main and StandaloneApp.preLoad use during boot. Update preLoad to accept frameworkDeps alongside dependencies, and pass both through to StandaloneApp.preLoad so the same module set can be warmed consistently.
🧹 Nitpick comments (5)
tegg/plugin/config/src/lib/ModuleScanner.ts (1)
25-33: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueConsider logging swallowed errors for diagnosability.
The catch-all silently discards any error from
getFrameworkPath(not just "no framework resolvable" cases, e.g. malformedpackage.json). Adebug(...)call here would help diagnose unexpected misconfiguration without changing behavior.♻️ Suggested addition
private resolveFrameworkDir(): string | undefined { try { return getFrameworkPath({ baseDir: this.baseDir }); - } catch { + } catch (err) { + debug('resolveFrameworkDir failed: %o', err); // No package.json or no resolvable framework next to the app (e.g. // bare unit fixtures without node_modules) — app modules only. return undefined; } }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@tegg/plugin/config/src/lib/ModuleScanner.ts` around lines 25 - 33, The resolveFrameworkDir() method in ModuleScanner swallows all errors from getFrameworkPath(), so add a debug(...) log inside the catch before returning undefined. Use the same baseDir context from this.baseDir and keep the fallback behavior unchanged, but make sure unexpected failures (not just missing framework cases) are observable for diagnosis.tegg/core/metadata/src/impl/EggInnerObjectPrototypeImpl.ts (1)
99-141: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winConsider extracting the shared prototype-construction path
EggInnerObjectPrototypeImpl.create()repeats the same field assembly andInjectObjectPrototypeFinder/IdenticalUtil.createProtoIdflow asEggPrototypeBuilder.create()/build(). A shared helper would reduce the chance of the two implementations drifting apart.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@tegg/core/metadata/src/impl/EggInnerObjectPrototypeImpl.ts` around lines 99 - 141, EggInnerObjectPrototypeImpl.create() duplicates the prototype assembly logic already present in EggPrototypeBuilder.create()/build(), so refactor the shared construction path into a common helper and have both call it. Keep the existing behavior for filepath, qualifiers, inject type/objects, InjectObjectPrototypeFinder.findInjectObjectPrototypes, and IdenticalUtil.createProtoId, but centralize the field mapping so the two implementations cannot drift apart.tegg/core/types/src/core-decorator/EggLifecycleProto.ts (1)
3-5: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winLiteral union collapses to
string, losing autocomplete for the five known lifecycle types.TypeScript widens
'LoadUnit' | ... | stringto juststring, so IDEs won't suggest the five official lifecycle type names when authors declare customEggLifecycleProto({ type: ... })calls.♻️ Proposed fix using the `string & {}` trick to retain literal autocomplete
export interface CommonEggLifecycleProtoParams extends InnerObjectProtoParams { - type: 'LoadUnit' | 'LoadUnitInstance' | 'EggObject' | 'EggPrototype' | 'EggContext' | string; + type: 'LoadUnit' | 'LoadUnitInstance' | 'EggObject' | 'EggPrototype' | 'EggContext' | (string & {}); }Please confirm this project's TypeScript version supports this pattern as expected (it's a long-standing, TS-team-acknowledged trick, not an official feature).
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@tegg/core/types/src/core-decorator/EggLifecycleProto.ts` around lines 3 - 5, The CommonEggLifecycleProtoParams.type union in EggLifecycleProto is collapsing to plain string, which removes autocomplete for the known lifecycle names. Update the type definition to preserve the five literal options while still allowing custom strings, using the standard string & {}-style workaround in CommonEggLifecycleProtoParams. Keep the change localized to the type declaration so EggLifecycleProto and related EggLifecycleProto({ type: ... }) call sites retain IDE suggestions without changing runtime behavior.tegg/core/runtime/src/impl/ProvidedInnerObjectProto.ts (1)
78-80: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value
getMetaDatalooks up metadata on a plain factory closure, which never carries decorator metadata.
this.objFactoryis an arrow function created ad hoc by the builder (() => innerObject.obj), never a decorated class, soMetadataUtil.getMetaDatahere will effectively always resolve toundefined. If any consumer ofEggPrototype.getMetaData(e.g. AOP/crosscut logic) relies on this for provided inner objects, it will silently no-op instead of surfacing a clear "not supported" signal.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@tegg/core/runtime/src/impl/ProvidedInnerObjectProto.ts` around lines 78 - 80, getMetaData on ProvidedInnerObjectProto is reading decorator metadata from objFactory, but objFactory is just the builder’s plain arrow closure and will never have metadata. Update ProvidedInnerObjectProto.getMetaData to stop delegating to MetadataUtil.getMetaData on objFactory, and instead either return a clear unsupported result or throw a descriptive error for provided inner objects so EggPrototype consumers like AOP/crosscut logic do not silently no-op.tegg/plugin/tegg/test/ModulePlugin.test.ts (1)
9-51: 📐 Maintainability & Code Quality | 🔵 Trivial | 🏗️ Heavy liftConsider adding multi-app regression coverage for this lifecycle change.
This exercises the new module-plugin boot order in
ModuleHandler(inner-object load unit instantiated before business load units) with a single app instance. As per coding guidelines, "When changing loader, runtime, lifecycle, or eventbus behavior, add or update multi-app regression coverage (for example tests likeMultiApp.test.ts) to verify two concurrent apps do not cross-talk." GivenModuleHandler.init()/destroy()ordering was substantially reworked, a companion test booting two concurrentmodule-plugin-app-style apps would guard against state leaking acrossTeggScopebags.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@tegg/plugin/tegg/test/ModulePlugin.test.ts` around lines 9 - 51, Add multi-app regression coverage for the ModuleHandler lifecycle change by creating a test that boots two concurrent module-plugin-app instances and verifies their TeggScope state does not cross-talk. Use the existing ModulePlugin.test.ts setup and the ModuleHandler init/destroy behavior as reference points, and assert both apps independently load HelloService, initialize inner-object load units before business load units, and keep their createdLoadUnits and innerRegistry state isolated.Source: Coding guidelines
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@tegg/plugin/tegg/src/lib/ModuleConfigLoader.ts`:
- Around line 59-71: The module resolution tolerance logic is duplicated in
ModuleConfigLoader and is already drifting from the similar code in the config
plugin and StandaloneApp. Extract the shared “resolve path, check fs.existsSync,
then fall back to reference.name and skip config loading” flow into a common
helper on ModuleConfigUtil (or equivalent) and update ModuleConfigLoader to call
it, so the modulePath/moduleDirExists/moduleName/defaultConfig handling stays
consistent across all callers.
In `@tegg/plugin/tegg/src/lib/ModuleHandler.ts`:
- Around line 116-132: The teardown logic in ModuleHandler currently stops on
the first thrown error in the load unit destruction loops, which can skip later
cleanup including the final `#innerObjectLoadUnit` teardown. Update the destroy
path around the loadUnitInstances, loadUnits, and `#innerObjectLoadUnit` blocks to
isolate each destroyLoadUnitInstance/destroyLoadUnit call so one failure does
not abort the remaining cleanup. Collect any errors during the loops in
ModuleHandler and rethrow an aggregate after all teardown steps have been
attempted.
- Around line 84-107: The init flow in ModuleHandler.init only assigns
loadUnitInstances after all later steps succeed, which leaves the inner-object
instance untracked if a boot error happens after
instantiateInnerObjectLoadUnit(). Update ModuleHandler.init so the
innerObjectInstance is stored in this.loadUnitInstances immediately after it is
created, then append each LoadUnitInstance created in the loop; keep the later
CompatibleUtil and loadUnit handling unchanged.
In `@tegg/standalone/standalone/src/main.ts`:
- Around line 40-47: The success-path cleanup in main/appMain is inconsistent
because app.destroy() is not awaited before returning, which can let
StandaloneApp resolve before TeggScope teardown finishes; change the finally
block to await the destroy promise just like the init-failure path so scope
cleanup completes before exit. Also update the .catch in that same app.destroy()
call to follow the existing Error-guard pattern used in this file, so non-Error
rejections are handled safely without mutating e.message directly. Use the
app.destroy() cleanup logic and the appMain/main flow as the key locations, and
add or update a regression test covering sequential StandaloneApp/MultiApp-style
teardown ordering to confirm no scope cross-talk.
In `@tegg/standalone/standalone/src/StandaloneApp.ts`:
- Around line 160-164: The per-app config selection in StandaloneApp should not
mutate the process-global ModuleConfigUtil.configNames, since that breaks
isolation between concurrent StandaloneApp instances. Update `#initRuntime`,
doDestroy, and the ModuleConfigUtil.readModuleNameSync/loadModuleConfigSync path
so configNames is passed or stored via TeggScope-backed instance state instead
of a shared static, keeping each app’s env-specific module.default/module.<env>
lookup independent.
In `@wiki/concepts/tegg-module-plugin.md`:
- Around line 5-13: The source_files list is missing the actual host-entrypoint
sources that back the egg-host feeding claim. Update the wiki page’s
source_files in the tegg-module-plugin section to include the plugin boot
entrypoints that register the hard-fed inner-object lists, such as the app.ts
entrypoints in the AOP, config, and DAL plugins, alongside the existing runtime
and handler symbols. Keep the listed sources aligned with the claim’s
traceability requirements so the major code paths are represented.
---
Outside diff comments:
In `@tegg/core/loader/src/LoaderFactory.ts`:
- Around line 61-62: Each ModuleDescriptor is currently sharing the same
multiInstanceClazzList, so later iterations can mutate earlier descriptors and
confuse GlobalGraph.create() attribution. Move the multiInstanceClazzList
initialization so it is created per module inside the LoaderFactory logic that
builds each ModuleDescriptor, and make sure each descriptor receives its own
fresh array before pushing multi-instance classes.
In `@tegg/standalone/standalone/src/main.ts`:
- Around line 10-19: The preLoad wrapper only forwards dependencies, so callers
cannot pass the same frameworkDeps set that main and StandaloneApp.preLoad use
during boot. Update preLoad to accept frameworkDeps alongside dependencies, and
pass both through to StandaloneApp.preLoad so the same module set can be warmed
consistently.
---
Nitpick comments:
In `@tegg/core/metadata/src/impl/EggInnerObjectPrototypeImpl.ts`:
- Around line 99-141: EggInnerObjectPrototypeImpl.create() duplicates the
prototype assembly logic already present in
EggPrototypeBuilder.create()/build(), so refactor the shared construction path
into a common helper and have both call it. Keep the existing behavior for
filepath, qualifiers, inject type/objects,
InjectObjectPrototypeFinder.findInjectObjectPrototypes, and
IdenticalUtil.createProtoId, but centralize the field mapping so the two
implementations cannot drift apart.
In `@tegg/core/runtime/src/impl/ProvidedInnerObjectProto.ts`:
- Around line 78-80: getMetaData on ProvidedInnerObjectProto is reading
decorator metadata from objFactory, but objFactory is just the builder’s plain
arrow closure and will never have metadata. Update
ProvidedInnerObjectProto.getMetaData to stop delegating to
MetadataUtil.getMetaData on objFactory, and instead either return a clear
unsupported result or throw a descriptive error for provided inner objects so
EggPrototype consumers like AOP/crosscut logic do not silently no-op.
In `@tegg/core/types/src/core-decorator/EggLifecycleProto.ts`:
- Around line 3-5: The CommonEggLifecycleProtoParams.type union in
EggLifecycleProto is collapsing to plain string, which removes autocomplete for
the known lifecycle names. Update the type definition to preserve the five
literal options while still allowing custom strings, using the standard string &
{}-style workaround in CommonEggLifecycleProtoParams. Keep the change localized
to the type declaration so EggLifecycleProto and related EggLifecycleProto({
type: ... }) call sites retain IDE suggestions without changing runtime
behavior.
In `@tegg/plugin/config/src/lib/ModuleScanner.ts`:
- Around line 25-33: The resolveFrameworkDir() method in ModuleScanner swallows
all errors from getFrameworkPath(), so add a debug(...) log inside the catch
before returning undefined. Use the same baseDir context from this.baseDir and
keep the fallback behavior unchanged, but make sure unexpected failures (not
just missing framework cases) are observable for diagnosis.
In `@tegg/plugin/tegg/test/ModulePlugin.test.ts`:
- Around line 9-51: Add multi-app regression coverage for the ModuleHandler
lifecycle change by creating a test that boots two concurrent module-plugin-app
instances and verifies their TeggScope state does not cross-talk. Use the
existing ModulePlugin.test.ts setup and the ModuleHandler init/destroy behavior
as reference points, and assert both apps independently load HelloService,
initialize inner-object load units before business load units, and keep their
createdLoadUnits and innerRegistry state isolated.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: b5819719-d3f0-4e63-9f9f-28a26cccc964
⛔ Files ignored due to path filters (8)
tegg/core/aop-runtime/test/__snapshots__/index.test.ts.snapis excluded by!**/*.snaptegg/core/core-decorator/test/__snapshots__/index.test.ts.snapis excluded by!**/*.snaptegg/core/metadata/test/__snapshots__/index.test.ts.snapis excluded by!**/*.snaptegg/core/runtime/test/__snapshots__/index.test.ts.snapis excluded by!**/*.snaptegg/core/tegg/test/__snapshots__/dal.test.ts.snapis excluded by!**/*.snaptegg/core/tegg/test/__snapshots__/exports.test.ts.snapis excluded by!**/*.snaptegg/core/tegg/test/__snapshots__/helper.test.ts.snapis excluded by!**/*.snaptegg/core/types/test/__snapshots__/index.test.ts.snapis excluded by!**/*.snap
📒 Files selected for processing (119)
tegg/core/aop-decorator/src/CrosscutAdviceFactory.tstegg/core/aop-runtime/package.jsontegg/core/aop-runtime/src/AopGraphHookRegistrar.tstegg/core/aop-runtime/src/EggObjectAopHook.tstegg/core/aop-runtime/src/EggPrototypeCrossCutHook.tstegg/core/aop-runtime/src/LoadUnitAopHook.tstegg/core/aop-runtime/src/index.tstegg/core/aop-runtime/test/aop-runtime.test.tstegg/core/core-decorator/src/decorator/EggLifecycleProto.tstegg/core/core-decorator/src/decorator/InnerObjectProto.tstegg/core/core-decorator/src/decorator/index.tstegg/core/core-decorator/src/util/PrototypeUtil.tstegg/core/core-decorator/test/inner-object-decorators.test.tstegg/core/loader/src/LoaderFactory.tstegg/core/loader/src/LoaderUtil.tstegg/core/loader/test/LoaderInnerObject.test.tstegg/core/loader/test/fixtures/modules/module-with-inner-object/ControllerHook.tstegg/core/loader/test/fixtures/modules/module-with-inner-object/FetchRouter.tstegg/core/loader/test/fixtures/modules/module-with-inner-object/HelloService.tstegg/core/loader/test/fixtures/modules/module-with-inner-object/package.jsontegg/core/metadata/src/impl/EggInnerObjectPrototypeImpl.tstegg/core/metadata/src/impl/EggPrototypeBuilder.tstegg/core/metadata/src/impl/InjectObjectPrototypeFinder.tstegg/core/metadata/src/impl/index.tstegg/core/metadata/src/model/ModuleDescriptor.tstegg/core/metadata/src/model/ProtoDescriptorHelper.tstegg/core/metadata/src/model/graph/GlobalGraph.tstegg/core/metadata/src/model/graph/ProtoGraphUtils.tstegg/core/metadata/src/model/graph/index.tstegg/core/metadata/test/ModuleDescriptorDumper.test.tstegg/core/runtime/src/impl/EggInnerObjectImpl.tstegg/core/runtime/src/impl/InnerObjectLoadUnit.tstegg/core/runtime/src/impl/InnerObjectLoadUnitBuilder.tstegg/core/runtime/src/impl/InnerObjectLoadUnitInstance.tstegg/core/runtime/src/impl/ProvidedInnerObjectProto.tstegg/core/runtime/src/impl/index.tstegg/core/runtime/test/InnerObjectLoadUnit.test.tstegg/core/test-util/src/LoaderUtil.tstegg/core/types/package.jsontegg/core/types/src/core-decorator/EggLifecycleProto.tstegg/core/types/src/core-decorator/InnerObjectProto.tstegg/core/types/src/core-decorator/Prototype.tstegg/core/types/src/core-decorator/index.tstegg/core/types/src/core-decorator/model/EggLifecycleInfo.tstegg/core/types/src/core-decorator/model/index.tstegg/core/types/src/metadata/model/ProtoDescriptor.tstegg/plugin/aop/package.jsontegg/plugin/aop/src/InnerObjects.tstegg/plugin/aop/src/app.tstegg/plugin/aop/test/aop.test.tstegg/plugin/config/package.jsontegg/plugin/config/src/app.tstegg/plugin/config/src/lib/ConfigSourceLoadUnitHook.tstegg/plugin/config/src/lib/ModuleScanner.tstegg/plugin/config/test/ReadModule.test.tstegg/plugin/dal/package.jsontegg/plugin/dal/src/app.tstegg/plugin/dal/src/index.tstegg/plugin/dal/src/lib/DalModuleLoadUnitHook.tstegg/plugin/dal/src/lib/DalTableEggPrototypeHook.tstegg/plugin/dal/src/lib/MysqlDataSourceManagerObject.tstegg/plugin/dal/src/lib/TransactionPrototypeHook.tstegg/plugin/tegg/package.jsontegg/plugin/tegg/src/app.tstegg/plugin/tegg/src/lib/EggModuleLoader.tstegg/plugin/tegg/src/lib/ModuleConfigLoader.tstegg/plugin/tegg/src/lib/ModuleHandler.tstegg/plugin/tegg/test/BundledAppBoot.test.tstegg/plugin/tegg/test/ModulePlugin.test.tstegg/plugin/tegg/test/fixtures/apps/module-plugin-app/config/config.default.tstegg/plugin/tegg/test/fixtures/apps/module-plugin-app/config/module.jsontegg/plugin/tegg/test/fixtures/apps/module-plugin-app/config/plugin.tstegg/plugin/tegg/test/fixtures/apps/module-plugin-app/modules/plugin-module/HelloService.tstegg/plugin/tegg/test/fixtures/apps/module-plugin-app/modules/plugin-module/InnerRegistry.tstegg/plugin/tegg/test/fixtures/apps/module-plugin-app/modules/plugin-module/PluginHooks.tstegg/plugin/tegg/test/fixtures/apps/module-plugin-app/modules/plugin-module/package.jsontegg/plugin/tegg/test/fixtures/apps/module-plugin-app/package.jsontegg/standalone/standalone/package.jsontegg/standalone/standalone/src/ConfigSourceLoadUnitHook.tstegg/standalone/standalone/src/EggModuleLoader.tstegg/standalone/standalone/src/Runner.tstegg/standalone/standalone/src/StandaloneApp.tstegg/standalone/standalone/src/StandaloneInnerObject.tstegg/standalone/standalone/src/StandaloneInnerObjectProto.tstegg/standalone/standalone/src/StandaloneLoadUnit.tstegg/standalone/standalone/src/index.tstegg/standalone/standalone/src/main.tstegg/standalone/standalone/test/ModulePlugin.test.tstegg/standalone/standalone/test/fixtures/dal-manager-inject/foo.tstegg/standalone/standalone/test/fixtures/dal-manager-inject/package.jsontegg/standalone/standalone/test/fixtures/egg-context-lifecycle-proto/Foo.tstegg/standalone/standalone/test/fixtures/egg-context-lifecycle-proto/FooEggContextHook.tstegg/standalone/standalone/test/fixtures/egg-context-lifecycle-proto/package.jsontegg/standalone/standalone/test/fixtures/egg-object-lifecycle-proto/Foo.tstegg/standalone/standalone/test/fixtures/egg-object-lifecycle-proto/FooEggObjectHook.tstegg/standalone/standalone/test/fixtures/egg-object-lifecycle-proto/package.jsontegg/standalone/standalone/test/fixtures/egg-prototype-lifecycle-proto/Foo.tstegg/standalone/standalone/test/fixtures/egg-prototype-lifecycle-proto/FooEggPrototypeHook.tstegg/standalone/standalone/test/fixtures/egg-prototype-lifecycle-proto/package.jsontegg/standalone/standalone/test/fixtures/inner-object-proto/foo.tstegg/standalone/standalone/test/fixtures/inner-object-proto/innerBar.tstegg/standalone/standalone/test/fixtures/inner-object-proto/package.jsontegg/standalone/standalone/test/fixtures/invalid-inner-object-inject/foo.tstegg/standalone/standalone/test/fixtures/invalid-inner-object-inject/innerBar.tstegg/standalone/standalone/test/fixtures/invalid-inner-object-inject/package.jsontegg/standalone/standalone/test/fixtures/load-unit-instance-lifecycle-proto/Foo.tstegg/standalone/standalone/test/fixtures/load-unit-instance-lifecycle-proto/FooLoadUnitInstanceHook.tstegg/standalone/standalone/test/fixtures/load-unit-instance-lifecycle-proto/package.jsontegg/standalone/standalone/test/fixtures/load-unit-lifecycle-proto/Foo.tstegg/standalone/standalone/test/fixtures/load-unit-lifecycle-proto/FooLoadUnitHook.tstegg/standalone/standalone/test/fixtures/load-unit-lifecycle-proto/Runner.tstegg/standalone/standalone/test/fixtures/load-unit-lifecycle-proto/package.jsontegg/standalone/standalone/test/fixtures/logger-option/foo.tstegg/standalone/standalone/test/fixtures/logger-option/package.jsontegg/standalone/standalone/test/index.test.tstegg/standalone/standalone/tsdown.config.tswiki/concepts/tegg-module-plugin.mdwiki/index.mdwiki/log.md
💤 Files with no reviewable changes (6)
- tegg/standalone/standalone/src/ConfigSourceLoadUnitHook.ts
- tegg/standalone/standalone/src/StandaloneInnerObject.ts
- tegg/standalone/standalone/src/StandaloneLoadUnit.ts
- tegg/standalone/standalone/src/Runner.ts
- tegg/plugin/tegg/package.json
- tegg/standalone/standalone/src/StandaloneInnerObjectProto.ts
| // Same tolerance as the config plugin's loadModuleConfigs: framework | ||
| // plugin modules restored from a bundle manifest are NOT materialized | ||
| // inside the bundle output (their code ships externally), so read the | ||
| // manifest-carried name instead of a package.json that does not exist. | ||
| const modulePath = path.isAbsolute(reference.path) | ||
| ? reference.path | ||
| : path.resolve(this.app.baseDir, reference.path); | ||
| const moduleDirExists = fs.existsSync(modulePath); | ||
| const moduleName = | ||
| !moduleDirExists && reference.name ? reference.name : ModuleConfigUtil.readModuleNameSync(modulePath); | ||
| const defaultConfig = moduleDirExists | ||
| ? ModuleConfigUtil.loadModuleConfigSync(modulePath, undefined, this.app.config.env) | ||
| : undefined; |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win
Consider consolidating the duplicated "moduleDirExists" tolerance logic.
This exact pattern (resolve absolute path → fs.existsSync → fall back to reference.name / skip config read) is now duplicated in tegg/plugin/config/src/app.ts. A third copy in tegg/standalone/standalone/src/StandaloneApp.ts#loadModuleConfigs is missing this same tolerance entirely (see comment there) — evidence that keeping the logic inline invites drift. Extracting a shared ModuleConfigUtil helper (e.g. resolveModuleConfigTolerant(reference, baseDir)) would prevent this class of divergence.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@tegg/plugin/tegg/src/lib/ModuleConfigLoader.ts` around lines 59 - 71, The
module resolution tolerance logic is duplicated in ModuleConfigLoader and is
already drifting from the similar code in the config plugin and StandaloneApp.
Extract the shared “resolve path, check fs.existsSync, then fall back to
reference.name and skip config loading” flow into a common helper on
ModuleConfigUtil (or equivalent) and update ModuleConfigLoader to call it, so
the modulePath/moduleDirExists/moduleName/defaultConfig handling stays
consistent across all callers.
| async init(): Promise<void> { | ||
| try { | ||
| this.app.eggPrototypeCreatorFactory.registerPrototypeCreator( | ||
| COMPATIBLE_PROTO_IMPLE_TYPE, | ||
| EggCompatibleProtoImpl.create, | ||
| ); | ||
|
|
||
| await this.loadUnitLoader.initGraph(); | ||
| const innerObjectInstance = await this.instantiateInnerObjectLoadUnit(); | ||
| await this.loadUnitLoader.load(); | ||
| const instances: LoadUnitInstance[] = []; | ||
| const instances: LoadUnitInstance[] = [innerObjectInstance]; | ||
| this.app.module = {} as any; | ||
|
|
||
| const businessInstances: LoadUnitInstance[] = []; | ||
| for (const loadUnit of this.loadUnits) { | ||
| const instance = await LoadUnitInstanceFactory.createLoadUnitInstance(loadUnit); | ||
| if (instance.loadUnit.type !== EggLoadUnitType.APP) { | ||
| CompatibleUtil.appCompatible(this.app, instance); | ||
| } | ||
| instances.push(instance); | ||
| businessInstances.push(instance); | ||
| } | ||
| CompatibleUtil.contextModuleCompatible(this.app.context, instances); | ||
| CompatibleUtil.contextModuleCompatible(this.app.context, businessInstances); | ||
| this.loadUnitInstances = instances; |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Inspect the target file and related teardown logic.
git ls-files tegg/plugin/tegg/src/lib/ModuleHandler.ts tegg/plugin/tegg/src/lib | sed -n '1,120p'
wc -l tegg/plugin/tegg/src/lib/ModuleHandler.ts
cat -n tegg/plugin/tegg/src/lib/ModuleHandler.ts | sed -n '1,240p'
# Find destroy/load-unit teardown paths.
rg -n "destroyLoadUnitInstance|loadUnitInstances|destroy\\(" tegg/plugin/tegg/src/lib tegg/plugin/tegg/src -g '*.ts'Repository: eggjs/egg
Length of output: 10642
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Inspect the destroy behavior for load-unit instances and load-units.
cat -n tegg/plugin/tegg/src/lib/AppLoadUnit.ts | sed -n '1,220p'
echo '---'
cat -n tegg/plugin/tegg/src/lib/AppLoadUnitInstance.ts | sed -n '1,220p'
echo '---'
cat -n tegg/plugin/tegg/src/lib/ModuleHandler.ts | sed -n '1,220p'
echo '---'
rg -n "destroyLoadUnitInstance|createLoadUnitInstance|destroyLoadUnit|InnerObjectLoadUnitBuilder|LoadUnitInstanceFactory" tegg/plugin/tegg/src -g '*.ts'
echo '---'
cat -n tegg/plugin/tegg/src/app.ts | sed -n '1,180p'Repository: eggjs/egg
Length of output: 22002
Track the inner-object instance as soon as it’s created. A boot failure after instantiateInnerObjectLoadUnit() leaves destroy() with an empty loadUnitInstances array, so destroyLoadUnitInstance never runs for that already-created instance; LoadUnitFactory.destroyLoadUnit(this.#innerObjectLoadUnit) only tears down the container. Set this.loadUnitInstances = [innerObjectInstance] immediately, then append business instances as they’re created.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@tegg/plugin/tegg/src/lib/ModuleHandler.ts` around lines 84 - 107, The init
flow in ModuleHandler.init only assigns loadUnitInstances after all later steps
succeed, which leaves the inner-object instance untracked if a boot error
happens after instantiateInnerObjectLoadUnit(). Update ModuleHandler.init so the
innerObjectInstance is stored in this.loadUnitInstances immediately after it is
created, then append each LoadUnitInstance created in the loop; keep the later
CompatibleUtil and loadUnit handling unchanged.
| // Reverse creation order: business load units go down first, the | ||
| // InnerObjectLoadUnit last — its lifecycle protos stay registered until | ||
| // every object they may hook has been destroyed. | ||
| if (this.loadUnitInstances) { | ||
| for (const instance of this.loadUnitInstances) { | ||
| for (const instance of [...this.loadUnitInstances].reverse()) { | ||
| await LoadUnitInstanceFactory.destroyLoadUnitInstance(instance); | ||
| } | ||
| } | ||
| if (this.loadUnits) { | ||
| for (const loadUnit of this.loadUnits) { | ||
| for (const loadUnit of [...this.loadUnits].reverse()) { | ||
| await LoadUnitFactory.destroyLoadUnit(loadUnit); | ||
| } | ||
| } | ||
| if (this.#innerObjectLoadUnit) { | ||
| await LoadUnitFactory.destroyLoadUnit(this.#innerObjectLoadUnit); | ||
| this.#innerObjectLoadUnit = undefined; | ||
| } |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
Destroy loops abort entirely on the first failure, skipping later cleanup including the #innerObjectLoadUnit teardown.
None of the three teardown blocks contain error containment. If any destroyLoadUnitInstance/destroyLoadUnit call throws, the enclosing for loop stops and the function rejects immediately — remaining instances/load units in that loop, the subsequent business-loadUnits block, and the final #innerObjectLoadUnit cleanup never run. Since the inner-object load unit's lifecycle protos are meant to "stay registered until every object they may hook has been destroyed" (per the comment), a single failed teardown anywhere upstream now permanently leaks it.
Consider isolating each destroy call so a single failure doesn't prevent the rest of teardown from proceeding (collect errors and rethrow an aggregate at the end).
🔧 Proposed fix: contain failures per item
async destroy(): Promise<void> {
+ const errors: unknown[] = [];
if (this.loadUnitInstances) {
for (const instance of [...this.loadUnitInstances].reverse()) {
- await LoadUnitInstanceFactory.destroyLoadUnitInstance(instance);
+ await LoadUnitInstanceFactory.destroyLoadUnitInstance(instance).catch(e => errors.push(e));
}
}
if (this.loadUnits) {
for (const loadUnit of [...this.loadUnits].reverse()) {
- await LoadUnitFactory.destroyLoadUnit(loadUnit);
+ await LoadUnitFactory.destroyLoadUnit(loadUnit).catch(e => errors.push(e));
}
}
if (this.#innerObjectLoadUnit) {
- await LoadUnitFactory.destroyLoadUnit(this.#innerObjectLoadUnit);
+ await LoadUnitFactory.destroyLoadUnit(this.#innerObjectLoadUnit).catch(e => errors.push(e));
this.#innerObjectLoadUnit = undefined;
}
+ if (errors.length > 0) {
+ throw new AggregateError(errors, 'ModuleHandler#destroy encountered errors');
+ }
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| // Reverse creation order: business load units go down first, the | |
| // InnerObjectLoadUnit last — its lifecycle protos stay registered until | |
| // every object they may hook has been destroyed. | |
| if (this.loadUnitInstances) { | |
| for (const instance of this.loadUnitInstances) { | |
| for (const instance of [...this.loadUnitInstances].reverse()) { | |
| await LoadUnitInstanceFactory.destroyLoadUnitInstance(instance); | |
| } | |
| } | |
| if (this.loadUnits) { | |
| for (const loadUnit of this.loadUnits) { | |
| for (const loadUnit of [...this.loadUnits].reverse()) { | |
| await LoadUnitFactory.destroyLoadUnit(loadUnit); | |
| } | |
| } | |
| if (this.#innerObjectLoadUnit) { | |
| await LoadUnitFactory.destroyLoadUnit(this.#innerObjectLoadUnit); | |
| this.#innerObjectLoadUnit = undefined; | |
| } | |
| const errors: unknown[] = []; | |
| // Reverse creation order: business load units go down first, the | |
| // InnerObjectLoadUnit last — its lifecycle protos stay registered until | |
| // every object they may hook has been destroyed. | |
| if (this.loadUnitInstances) { | |
| for (const instance of [...this.loadUnitInstances].reverse()) { | |
| await LoadUnitInstanceFactory.destroyLoadUnitInstance(instance).catch(e => errors.push(e)); | |
| } | |
| } | |
| if (this.loadUnits) { | |
| for (const loadUnit of [...this.loadUnits].reverse()) { | |
| await LoadUnitFactory.destroyLoadUnit(loadUnit).catch(e => errors.push(e)); | |
| } | |
| } | |
| if (this.#innerObjectLoadUnit) { | |
| await LoadUnitFactory.destroyLoadUnit(this.#innerObjectLoadUnit).catch(e => errors.push(e)); | |
| this.#innerObjectLoadUnit = undefined; | |
| } | |
| if (errors.length > 0) { | |
| throw new AggregateError(errors, 'ModuleHandler#destroy encountered errors'); | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@tegg/plugin/tegg/src/lib/ModuleHandler.ts` around lines 116 - 132, The
teardown logic in ModuleHandler currently stops on the first thrown error in the
load unit destruction loops, which can skip later cleanup including the final
`#innerObjectLoadUnit` teardown. Update the destroy path around the
loadUnitInstances, loadUnits, and `#innerObjectLoadUnit` blocks to isolate each
destroyLoadUnitInstance/destroyLoadUnit call so one failure does not abort the
remaining cleanup. Collect any errors during the loops in ModuleHandler and
rethrow an aggregate after all teardown steps have been attempted.
| try { | ||
| return await runner.run<T>(); | ||
| return await app.run<T>(ctx); | ||
| } finally { | ||
| runner.destroy().catch((e) => { | ||
| app.destroy().catch((e) => { | ||
| e.message = `[tegg/standalone] destroy tegg failed: ${e.message}`; | ||
| console.warn(e); | ||
| }); | ||
| } |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
app.destroy() isn't awaited on the success path — scope-leak guard is inconsistent.
The comment above the init-failure branch explains that destroy() must run before returning so the app's TeggScope bag doesn't leak into liveScopeBags, and that path correctly does await app.destroy().catch(...). But in the success-path finally, app.destroy() is fired without await, so appMain/main can resolve and return to the caller before the scope is actually torn down — reintroducing the exact leak the other branch guards against (e.g. a caller immediately creating a second StandaloneApp could race with in-flight teardown).
Separately, this .catch handler mutates e.message without the instanceof Error guard used everywhere else in this file. If destroy() rejects with a non-Error value, e.message = ... throws inside the .catch handler itself (module code runs in strict mode), producing an unhandled rejection instead of a warning.
🔧 Proposed fix
try {
return await app.run<T>(ctx);
} finally {
- app.destroy().catch((e) => {
- e.message = `[tegg/standalone] destroy tegg failed: ${e.message}`;
- console.warn(e);
- });
+ await app.destroy().catch((e) => {
+ if (e instanceof Error) {
+ e.message = `[tegg/standalone] destroy tegg failed: ${e.message}`;
+ }
+ console.warn(e);
+ });
}Given the loader/lifecycle/scope changes in this PR, this is exactly the kind of hazard the multi-app regression coverage guideline calls out — worth adding/asserting sequential-app teardown ordering in a MultiApp-style test. As per coding guidelines, "add or update multi-app regression coverage (for example tests like MultiApp.test.ts) to verify two concurrent apps do not cross-talk."
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| try { | |
| return await runner.run<T>(); | |
| return await app.run<T>(ctx); | |
| } finally { | |
| runner.destroy().catch((e) => { | |
| app.destroy().catch((e) => { | |
| e.message = `[tegg/standalone] destroy tegg failed: ${e.message}`; | |
| console.warn(e); | |
| }); | |
| } | |
| try { | |
| return await app.run<T>(ctx); | |
| } finally { | |
| await app.destroy().catch((e) => { | |
| if (e instanceof Error) { | |
| e.message = `[tegg/standalone] destroy tegg failed: ${e.message}`; | |
| } | |
| console.warn(e); | |
| }); | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@tegg/standalone/standalone/src/main.ts` around lines 40 - 47, The
success-path cleanup in main/appMain is inconsistent because app.destroy() is
not awaited before returning, which can let StandaloneApp resolve before
TeggScope teardown finishes; change the finally block to await the destroy
promise just like the init-failure path so scope cleanup completes before exit.
Also update the .catch in that same app.destroy() call to follow the existing
Error-guard pattern used in this file, so non-Error rejections are handled
safely without mutating e.message directly. Use the app.destroy() cleanup logic
and the appMain/main flow as the key locations, and add or update a regression
test covering sequential StandaloneApp/MultiApp-style teardown ordering to
confirm no scope cross-talk.
Source: Coding guidelines
| // load module.yml and module.env.yml by default | ||
| // Always set configNames for this app invocation, since destroy() clears it | ||
| // asynchronously and may not have completed before the next app is created. | ||
| ModuleConfigUtil.configNames = opts.env ? ['module.default', `module.${opts.env}`] : ['module.default']; | ||
| } |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift
Mutating the process-global ModuleConfigUtil.configNames breaks the per-app isolation this file otherwise guarantees via TeggScope.
Every other piece of per-app mutable state in this class (#moduleConfigs, #runtimeConfig, #moduleReferences, loadUnits, scopeBag, ...) is instance-scoped, and the class comments explicitly promise "multiple StandaloneApps in one process stay isolated." ModuleConfigUtil.configNames, however, is set as a static/global value in #initRuntime and cleared globally in doDestroy via ModuleConfigUtil.setConfigNames(undefined). The comment on Line 162 itself concedes the race: "since destroy() clears it asynchronously and may not have completed before the next app is created." If two StandaloneApp instances are alive concurrently (e.g. two appMain() calls in flight, or app B booting while app A is still tearing down), one app's env-specific config file names (module.<env>) can silently leak into or get cleared out from under the other, causing the wrong module config to load — undermining the multi-app isolation this rewrite is meant to provide.
As per coding guidelines, "In Tegg source code, do not introduce new process-global mutable runtime state (for example static fields, Maps, or singletons that hold per-app data); use TeggScope-backed slots for per-app state instead." Consider threading configNames explicitly through ModuleConfigUtil.readModuleNameSync/loadModuleConfigSync calls (or backing it with a TeggScope slot resolved via this.scopeBag) instead of a shared static field.
Also applies to: 389-389
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@tegg/standalone/standalone/src/StandaloneApp.ts` around lines 160 - 164, The
per-app config selection in StandaloneApp should not mutate the process-global
ModuleConfigUtil.configNames, since that breaks isolation between concurrent
StandaloneApp instances. Update `#initRuntime`, doDestroy, and the
ModuleConfigUtil.readModuleNameSync/loadModuleConfigSync path so configNames is
passed or stored via TeggScope-backed instance state instead of a shared static,
keeping each app’s env-specific module.default/module.<env> lookup independent.
Source: Coding guidelines
| source_files: | ||
| - tegg/core/core-decorator/src/decorator/InnerObjectProto.ts | ||
| - tegg/core/core-decorator/src/decorator/EggLifecycleProto.ts | ||
| - tegg/core/runtime/src/impl/InnerObjectLoadUnit.ts | ||
| - tegg/core/runtime/src/impl/InnerObjectLoadUnitBuilder.ts | ||
| - tegg/core/runtime/src/impl/InnerObjectLoadUnitInstance.ts | ||
| - tegg/core/runtime/src/impl/EggInnerObjectImpl.ts | ||
| - tegg/standalone/standalone/src/StandaloneApp.ts | ||
| - tegg/plugin/tegg/src/lib/ModuleHandler.ts |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Add the missing host-entrypoint sources to source_files.
This page claims traceability for the egg-host feeding path, but the source list only points at decorator/runtime/handler internals. Please add the actual plugin boot entrypoints that register the hard-fed inner-object lists, such as tegg/plugin/aop/src/app.ts, tegg/plugin/config/src/app.ts, and tegg/plugin/dal/src/app.ts, so the host-feeding claim is backed by the listed sources. As per coding guidelines, nontrivial wiki claims should list the major source paths they depend on.
Also applies to: 53-58
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@wiki/concepts/tegg-module-plugin.md` around lines 5 - 13, The source_files
list is missing the actual host-entrypoint sources that back the egg-host
feeding claim. Update the wiki page’s source_files in the tegg-module-plugin
section to include the plugin boot entrypoints that register the hard-fed
inner-object lists, such as the app.ts entrypoints in the AOP, config, and DAL
plugins, alongside the existing runtime and handler symbols. Keep the listed
sources aligned with the claim’s traceability requirements so the major code
paths are represented.
Source: Coding guidelines
| this.eggObjectAopHook = new EggObjectAopHook(); | ||
| } | ||
|
|
||
| configDidLoad(): void { |
| this.app.eggPrototypeLifecycleUtil.deleteLifecycle(this.eggPrototypeCrossCutHook); | ||
| this.app.loadUnitLifecycleUtil.deleteLifecycle(this.loadUnitAopHook); | ||
| this.app.eggObjectLifecycleUtil.deleteLifecycle(this.eggObjectAopHook); | ||
| this.app.eggContextLifecycleUtil.deleteLifecycle(this.aopContextHook); |
| @InnerObjectProto({ name: 'mysqlDataSourceManager', accessLevel: AccessLevel.PUBLIC }) | ||
| export class MysqlDataSourceManagerObject { | ||
| constructor() { | ||
| return MysqlDataSourceManager.instance; |
There was a problem hiding this comment.
这个显得很奇怪了,应该可以改成直接继承而不是返回一个 instance?
| // every object they may hook has been destroyed. | ||
| if (this.loadUnitInstances) { | ||
| for (const instance of this.loadUnitInstances) { | ||
| for (const instance of [...this.loadUnitInstances].reverse()) { |
Code Review: 声明式 module plugin 机制整体架构清晰,happy path 测试充分。但本次评审最核心的主题是:hook 的投递方式从"无条件命令式注册"变成了"扫描 + 提升(promote)流水线",而这条流水线的每一种失败模式都是静默跳过 —— 若干现实部署形态会在没有任何报错的情况下丢失 发现(按严重程度排序)1.
|
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
Summary by CodeRabbit
New Features
Bug Fixes
Documentation