Skip to content

feat(tegg): declarative module plugin mechanism#6021

Open
gxkl wants to merge 41 commits into
nextfrom
feat/module-plugin-core
Open

feat(tegg): declarative module plugin mechanism#6021
gxkl wants to merge 41 commits into
nextfrom
feat/module-plugin-core

Conversation

@gxkl

@gxkl gxkl commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

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:

  • the same registration logic is duplicated between the egg plugin boot and the standalone Runner;
  • every new runtime shape (e.g. a service worker runtime) has to copy the whole assembly again;
  • modules are not self-contained.

This PR ports the declarative module plugin design from eggjs/tegg#325 (merged to the stale standalone-next branch, never reached master) to the next architecture, 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 #handleCompatibility TODO).

Design

A plain eggModule package can now provide framework extensions declaratively:

  • @InnerObjectProto — framework inner object (a SingletonProto with EGG_INNER_OBJECT_PROTO_IMPL_TYPE), diverted by the loader into ModuleDescriptor.innerObjectClazzList, never into business load units.
  • @EggLifecycleProto five 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 consumes registerBuildHook hooks once at its end (late registration is silently lost). Both hosts now boot as:

  1. scan modules → GlobalGraph.create (nodes only)
  2. create AND instantiate the InnerObjectLoadUnit (own topologically sorted proto graph, cycle detection, hard error on missing non-optional deps) — hooks register here, including graph build hooks from @LifecyclePostInject (see AopGraphHookRegistrar)
  3. build() / sort() → business load units → business instances
  4. destroy in reverse creation order (inner unit last)

Commits

commit scope
feat(core): add module plugin core mechanism decorators + metadata (EggInnerObjectPrototypeImpl, InjectObjectPrototypeFinder / ProtoGraphUtils extracted with a proto-name index replacing the O(n·m·n) scan) + runtime (EggInnerObjectImpl runs only decorator-declared self lifecycle; host-agnostic InnerObjectLoadUnit(Builder/Instance)) + loader diversion (manifest getDecoratedFiles covers the new list so bundle mode still re-imports those files)
feat(standalone): two-phase StandaloneApp with module plugin support BREAKING: Runner renamed to StandaloneApp (no alias; main()/preLoad() unchanged). Explicit boot phases, frameworkDeps option, bundle-mode manifest/loaderFS consumption + StandaloneApp.loadMetadata(), tegg#325 test suite ported
feat(tegg-plugin): instantiate InnerObjectLoadUnit in app mode ModuleHandler.init() phases via EggModuleLoader.initGraph()/load() split; the same module plugin package behaves identically under both hosts
feat(tegg): convert built-in framework hooks to module plugins AOP (incl. AopGraphHookRegistrar for the crossCut/pointCut build hooks), DAL (constructor args → @Inject of host-provided moduleConfigs/runtimeConfig/logger inner objects, PRIVATE on the egg host), ConfigSource; vestigial LoadUnitMultiInstanceProtoHook registration dropped (empty preCreate, 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 architecture concept page + log

Test evidence

  • New coverage: decorator metadata (7), loader diversion (3), InnerObjectLoadUnit integration 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).
  • Regression: 15 tegg projects (core + plugins + standalone) — 76 files / 296 tests green, MultiApp isolation green; full-repo run has zero failures related to this change (remaining ones verified as pre-existing/env: dns-cache needs a fresh ut install, multipart fails on clean next too, onerror/development are flaky-on-rerun).

Notes

  • Independent of fix(controller): register middlewareGraphHook before global graph build #6020 (no overlapping files; git merge-tree clean; combined-tree run of both test suites green).
  • TeggScope rules kept: no new process-global mutable state; lifecycle registration happens inside the host scope via the scope-aware utils; type-keyed creator registries stay global per the multi-app guidelines.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added support for inner objects and lifecycle-based hooks across app loading, runtime, and standalone mode.
    • Improved module/plugin bootstrapping so framework services and host-provided objects are discovered and initialized more reliably.
    • Standalone apps now use a unified app-based runner flow with better lifecycle handling.
  • Bug Fixes

    • Fixed several dependency and lifecycle wiring issues that could prevent hooks or injected objects from being registered correctly.
    • Improved handling for missing framework modules and optional dependencies during loading.
  • Documentation

    • Added concept documentation covering the new module plugin behavior and boot order.

@coderabbitai

coderabbitai Bot commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

This PR introduces a host-agnostic "Tegg Module Plugin" mechanism: new InnerObjectProto/EggLifecycleProto decorators, prototype metadata/graph resolution updates, and a dedicated InnerObjectLoadUnit runtime. AOP, Config, and DAL plugins are migrated to this pattern, Tegg boot wiring is updated, and the standalone Runner is replaced with StandaloneApp.

Changes

Tegg Module Plugin Mechanism

Layer / File(s) Summary
Core InnerObjectProto/EggLifecycleProto decorators
tegg/core/core-decorator/src/decorator/*, tegg/core/core-decorator/src/util/PrototypeUtil.ts, tegg/core/types/src/core-decorator/*, tegg/core/types/package.json, tegg/core/core-decorator/test/inner-object-decorators.test.ts
Adds InnerObjectProto/EggLifecycleProto decorator factories, PrototypeUtil markers/metadata APIs, related type contracts, package exports, and unit tests.
Prototype metadata, inject resolution, dependency graph
tegg/core/metadata/src/impl/*, tegg/core/metadata/src/model/*, tegg/core/metadata/test/*
Adds EggInnerObjectPrototypeImpl/InjectObjectPrototypeFinder, refactors EggPrototypeBuilder, extends ModuleDescriptor/ProtoDescriptorHelper, and introduces ProtoGraphUtils for cached graph lookups in GlobalGraph.
Loader inner-object classification
tegg/core/loader/src/*, tegg/core/loader/test/*, tegg/core/test-util/src/LoaderUtil.ts
Classifies inner-object classes into innerObjectClazzList, extends glob exclusions, and rewrites test-util graph building.
Inner object runtime and load unit
tegg/core/runtime/src/impl/*, tegg/core/runtime/test/*
Implements EggInnerObjectImpl, InnerObjectLoadUnit/Builder/Instance, and ProvidedInnerObjectProto for host-provided objects.
AOP plugin migration
tegg/core/aop-*, tegg/plugin/aop/*
Applies new decorators to AOP hooks, adds AopGraphHookRegistrar, simplifies AopAppHook.
Config plugin migration
tegg/plugin/config/*
Converts ConfigSourceLoadUnitHook to lifecycle proto, resolves framework dir via getFrameworkPath, tolerates missing module directories.
DAL plugin migration
tegg/plugin/dal/*
Converts DAL hooks to decorator injection, adds MysqlDataSourceManagerObject, simplifies DalAppBootHook.
Tegg boot wiring and ModuleHandler
tegg/plugin/tegg/*
Adds graph init/manifest phases, ModuleHandler.instantiateInnerObjectLoadUnit, and new fixtures/tests.
Standalone Runner replaced by StandaloneApp
tegg/standalone/standalone/*
Removes Runner/StandaloneInnerObject*, adds StandaloneApp with full lifecycle orchestration and new fixtures/tests.
Wiki documentation
wiki/concepts/tegg-module-plugin.md, wiki/index.md, wiki/log.md
Documents the module plugin concept and records changelog.

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
Loading

Possibly related PRs

  • eggjs/egg#5846: Related module/decorated-file collection changes in ModuleDescriptor.ts align with manifest/precomputed file loading flow.
  • eggjs/egg#5993: Both PRs change when/how crossCutGraphHook/pointCutGraphHook build hooks are registered.

Suggested reviewers: jerryliang64, killagu, fengmk2

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is concise and accurately summarizes the PR’s main change: a declarative module plugin mechanism for Tegg.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/module-plugin-core

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jul 4, 2026

Copy link
Copy Markdown

Deploying egg with  Cloudflare Pages  Cloudflare Pages

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

View logs

@codecov

codecov Bot commented Jul 4, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 87.64881% with 83 lines in your changes missing coverage. Please review.
✅ Project coverage is 81.99%. Comparing base (d55efa3) to head (79594a6).

Files with missing lines Patch % Lines
tegg/core/runtime/src/impl/EggInnerObjectImpl.ts 57.77% 34 Missing and 4 partials ⚠️
tegg/standalone/standalone/src/StandaloneApp.ts 87.39% 15 Missing ⚠️
...e/metadata/src/impl/InjectObjectPrototypeFinder.ts 85.00% 4 Missing and 2 partials ⚠️
.../core/runtime/src/impl/ProvidedInnerObjectProto.ts 91.11% 4 Missing ⚠️
tegg/standalone/standalone/src/EggModuleLoader.ts 82.60% 4 Missing ⚠️
...e/metadata/src/impl/EggInnerObjectPrototypeImpl.ts 93.61% 3 Missing ⚠️
...g/core/metadata/src/model/graph/ProtoGraphUtils.ts 94.11% 2 Missing ⚠️
...ore/runtime/src/impl/InnerObjectLoadUnitBuilder.ts 95.34% 2 Missing ⚠️
tegg/plugin/tegg/src/lib/ModuleConfigLoader.ts 75.00% 2 Missing ⚠️
tegg/core/aop-runtime/src/AopGraphHookRegistrar.ts 87.50% 1 Missing ⚠️
... and 6 more
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.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@socket-security

socket-security Bot commented Jul 4, 2026

Copy link
Copy Markdown

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.

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jul 4, 2026

Copy link
Copy Markdown

Deploying egg-v3 with  Cloudflare Pages  Cloudflare Pages

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

View logs

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +5 to +6
import { crossCutGraphHook } from './CrossCutGraphHook.js';
import { pointCutGraphHook } from './PointCutGraphHook.js';

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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.

Suggested change
import { crossCutGraphHook } from './CrossCutGraphHook.js';
import { pointCutGraphHook } from './PointCutGraphHook.js';
import { crossCutGraphHook } from './CrossCutGraphHook.ts';
import { pointCutGraphHook } from './PointCutGraphHook.ts';
References
  1. 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.

Comment on lines +4 to +7
import { AopGraphHookRegistrar } from './AopGraphHookRegistrar.js';
import { EggObjectAopHook } from './EggObjectAopHook.js';
import { EggPrototypeCrossCutHook } from './EggPrototypeCrossCutHook.js';
import { LoadUnitAopHook } from './LoadUnitAopHook.js';

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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.

Suggested change
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
  1. 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.

Comment thread tegg/core/aop-runtime/src/index.ts Outdated
Comment on lines +7 to +8
export * from './AopGraphHookRegistrar.js';
export * from './AopInnerObjectClazzList.js';

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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.

Suggested change
export * from './AopGraphHookRegistrar.js';
export * from './AopInnerObjectClazzList.js';
export * from './AopGraphHookRegistrar.ts';
export * from './AopInnerObjectClazzList.ts';
References
  1. 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.

Comment on lines +321 to +324
ctx.destroy(lifecycle).catch((e) => {
e.message = `[tegg/standalone] destroy tegg context failed: ${e.message}`;
console.warn(e);
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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.

Suggested change
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
  1. 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.

Comment on lines +32 to 35
app.destroy().catch((e) => {
e.message = `[tegg/standalone] destroy tegg failed: ${e.message}`;
console.warn(e);
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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.

Suggested change
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
  1. 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.

@gxkl gxkl changed the title [WIP] feat(tegg): declarative module plugin mechanism feat(tegg): declarative module plugin mechanism Jul 6, 2026
@gxkl gxkl requested a review from killagu July 6, 2026 00:20
@gxkl gxkl marked this pull request as ready for review July 6, 2026 01:38
Copilot AI review requested due to automatic review settings July 6, 2026 01:38
gxkl and others added 18 commits July 6, 2026 09:39
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>
gxkl and others added 19 commits July 6, 2026 09:39
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>
@gxkl gxkl force-pushed the feat/module-plugin-core branch from 5001fb6 to 79594a6 Compare July 6, 2026 01:39

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 an InnerObjectLoadUnit instantiated before business load units.
  • Refactor standalone host: rename RunnerStandaloneApp, add bundle-manifest support, and update/expand module-plugin test coverage.
  • Update egg host boot wiring (ModuleHandler / EggModuleLoader) to instantiate the InnerObjectLoadUnit before loading business units; migrate built-in AOP/DAL/ConfigSource hooks to module-plugin form; document the architecture in wiki/.

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.

Comment on lines +10 to 13
export async function preLoad(cwd: string, dependencies?: StandaloneAppOptions['dependencies']): Promise<void> {
try {
await Runner.preLoad(cwd, dependencies);
await StandaloneApp.preLoad(cwd, dependencies);
} catch (e) {
Copilot AI review requested due to automatic review settings July 6, 2026 01:44

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 127 out of 127 changed files in this pull request and generated 1 comment.

Comment on lines +59 to 63
`"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)!))}`
: '') +
'}'

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 win

Give each module its own multiInstanceClazzList. Declaring the array outside the loop makes every ModuleDescriptor share the same list, so later modules mutate earlier descriptors and GlobalGraph.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 win

Forward frameworkDeps through preLoad
preLoad only accepts dependencies, so callers can’t warm the same framework module set that main/StandaloneApp.preLoad scan at boot. Add frameworkDeps here 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 value

Consider logging swallowed errors for diagnosability.

The catch-all silently discards any error from getFrameworkPath (not just "no framework resolvable" cases, e.g. malformed package.json). A debug(...) 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 win

Consider extracting the shared prototype-construction path EggInnerObjectPrototypeImpl.create() repeats the same field assembly and InjectObjectPrototypeFinder/IdenticalUtil.createProtoId flow as EggPrototypeBuilder.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 win

Literal union collapses to string, losing autocomplete for the five known lifecycle types.

TypeScript widens 'LoadUnit' | ... | string to just string, so IDEs won't suggest the five official lifecycle type names when authors declare custom EggLifecycleProto({ 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

getMetaData looks up metadata on a plain factory closure, which never carries decorator metadata.

this.objFactory is an arrow function created ad hoc by the builder (() => innerObject.obj), never a decorated class, so MetadataUtil.getMetaData here will effectively always resolve to undefined. If any consumer of EggPrototype.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 lift

Consider 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 like MultiApp.test.ts) to verify two concurrent apps do not cross-talk." Given ModuleHandler.init()/destroy() ordering was substantially reworked, a companion test booting two concurrent module-plugin-app-style apps would guard against state leaking across TeggScope bags.

🤖 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

📥 Commits

Reviewing files that changed from the base of the PR and between d55efa3 and 79594a6.

⛔ Files ignored due to path filters (8)
  • tegg/core/aop-runtime/test/__snapshots__/index.test.ts.snap is excluded by !**/*.snap
  • tegg/core/core-decorator/test/__snapshots__/index.test.ts.snap is excluded by !**/*.snap
  • tegg/core/metadata/test/__snapshots__/index.test.ts.snap is excluded by !**/*.snap
  • tegg/core/runtime/test/__snapshots__/index.test.ts.snap is excluded by !**/*.snap
  • tegg/core/tegg/test/__snapshots__/dal.test.ts.snap is excluded by !**/*.snap
  • tegg/core/tegg/test/__snapshots__/exports.test.ts.snap is excluded by !**/*.snap
  • tegg/core/tegg/test/__snapshots__/helper.test.ts.snap is excluded by !**/*.snap
  • tegg/core/types/test/__snapshots__/index.test.ts.snap is excluded by !**/*.snap
📒 Files selected for processing (119)
  • tegg/core/aop-decorator/src/CrosscutAdviceFactory.ts
  • tegg/core/aop-runtime/package.json
  • tegg/core/aop-runtime/src/AopGraphHookRegistrar.ts
  • tegg/core/aop-runtime/src/EggObjectAopHook.ts
  • tegg/core/aop-runtime/src/EggPrototypeCrossCutHook.ts
  • tegg/core/aop-runtime/src/LoadUnitAopHook.ts
  • tegg/core/aop-runtime/src/index.ts
  • tegg/core/aop-runtime/test/aop-runtime.test.ts
  • tegg/core/core-decorator/src/decorator/EggLifecycleProto.ts
  • tegg/core/core-decorator/src/decorator/InnerObjectProto.ts
  • tegg/core/core-decorator/src/decorator/index.ts
  • tegg/core/core-decorator/src/util/PrototypeUtil.ts
  • tegg/core/core-decorator/test/inner-object-decorators.test.ts
  • tegg/core/loader/src/LoaderFactory.ts
  • tegg/core/loader/src/LoaderUtil.ts
  • tegg/core/loader/test/LoaderInnerObject.test.ts
  • tegg/core/loader/test/fixtures/modules/module-with-inner-object/ControllerHook.ts
  • tegg/core/loader/test/fixtures/modules/module-with-inner-object/FetchRouter.ts
  • tegg/core/loader/test/fixtures/modules/module-with-inner-object/HelloService.ts
  • tegg/core/loader/test/fixtures/modules/module-with-inner-object/package.json
  • tegg/core/metadata/src/impl/EggInnerObjectPrototypeImpl.ts
  • tegg/core/metadata/src/impl/EggPrototypeBuilder.ts
  • tegg/core/metadata/src/impl/InjectObjectPrototypeFinder.ts
  • tegg/core/metadata/src/impl/index.ts
  • tegg/core/metadata/src/model/ModuleDescriptor.ts
  • tegg/core/metadata/src/model/ProtoDescriptorHelper.ts
  • tegg/core/metadata/src/model/graph/GlobalGraph.ts
  • tegg/core/metadata/src/model/graph/ProtoGraphUtils.ts
  • tegg/core/metadata/src/model/graph/index.ts
  • tegg/core/metadata/test/ModuleDescriptorDumper.test.ts
  • tegg/core/runtime/src/impl/EggInnerObjectImpl.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/ProvidedInnerObjectProto.ts
  • tegg/core/runtime/src/impl/index.ts
  • tegg/core/runtime/test/InnerObjectLoadUnit.test.ts
  • tegg/core/test-util/src/LoaderUtil.ts
  • tegg/core/types/package.json
  • tegg/core/types/src/core-decorator/EggLifecycleProto.ts
  • tegg/core/types/src/core-decorator/InnerObjectProto.ts
  • tegg/core/types/src/core-decorator/Prototype.ts
  • tegg/core/types/src/core-decorator/index.ts
  • tegg/core/types/src/core-decorator/model/EggLifecycleInfo.ts
  • tegg/core/types/src/core-decorator/model/index.ts
  • tegg/core/types/src/metadata/model/ProtoDescriptor.ts
  • tegg/plugin/aop/package.json
  • tegg/plugin/aop/src/InnerObjects.ts
  • tegg/plugin/aop/src/app.ts
  • tegg/plugin/aop/test/aop.test.ts
  • tegg/plugin/config/package.json
  • tegg/plugin/config/src/app.ts
  • tegg/plugin/config/src/lib/ConfigSourceLoadUnitHook.ts
  • tegg/plugin/config/src/lib/ModuleScanner.ts
  • tegg/plugin/config/test/ReadModule.test.ts
  • tegg/plugin/dal/package.json
  • tegg/plugin/dal/src/app.ts
  • tegg/plugin/dal/src/index.ts
  • tegg/plugin/dal/src/lib/DalModuleLoadUnitHook.ts
  • tegg/plugin/dal/src/lib/DalTableEggPrototypeHook.ts
  • tegg/plugin/dal/src/lib/MysqlDataSourceManagerObject.ts
  • tegg/plugin/dal/src/lib/TransactionPrototypeHook.ts
  • tegg/plugin/tegg/package.json
  • tegg/plugin/tegg/src/app.ts
  • tegg/plugin/tegg/src/lib/EggModuleLoader.ts
  • tegg/plugin/tegg/src/lib/ModuleConfigLoader.ts
  • tegg/plugin/tegg/src/lib/ModuleHandler.ts
  • tegg/plugin/tegg/test/BundledAppBoot.test.ts
  • tegg/plugin/tegg/test/ModulePlugin.test.ts
  • tegg/plugin/tegg/test/fixtures/apps/module-plugin-app/config/config.default.ts
  • tegg/plugin/tegg/test/fixtures/apps/module-plugin-app/config/module.json
  • tegg/plugin/tegg/test/fixtures/apps/module-plugin-app/config/plugin.ts
  • tegg/plugin/tegg/test/fixtures/apps/module-plugin-app/modules/plugin-module/HelloService.ts
  • tegg/plugin/tegg/test/fixtures/apps/module-plugin-app/modules/plugin-module/InnerRegistry.ts
  • tegg/plugin/tegg/test/fixtures/apps/module-plugin-app/modules/plugin-module/PluginHooks.ts
  • tegg/plugin/tegg/test/fixtures/apps/module-plugin-app/modules/plugin-module/package.json
  • tegg/plugin/tegg/test/fixtures/apps/module-plugin-app/package.json
  • tegg/standalone/standalone/package.json
  • tegg/standalone/standalone/src/ConfigSourceLoadUnitHook.ts
  • tegg/standalone/standalone/src/EggModuleLoader.ts
  • tegg/standalone/standalone/src/Runner.ts
  • tegg/standalone/standalone/src/StandaloneApp.ts
  • tegg/standalone/standalone/src/StandaloneInnerObject.ts
  • tegg/standalone/standalone/src/StandaloneInnerObjectProto.ts
  • tegg/standalone/standalone/src/StandaloneLoadUnit.ts
  • tegg/standalone/standalone/src/index.ts
  • tegg/standalone/standalone/src/main.ts
  • tegg/standalone/standalone/test/ModulePlugin.test.ts
  • tegg/standalone/standalone/test/fixtures/dal-manager-inject/foo.ts
  • tegg/standalone/standalone/test/fixtures/dal-manager-inject/package.json
  • tegg/standalone/standalone/test/fixtures/egg-context-lifecycle-proto/Foo.ts
  • tegg/standalone/standalone/test/fixtures/egg-context-lifecycle-proto/FooEggContextHook.ts
  • tegg/standalone/standalone/test/fixtures/egg-context-lifecycle-proto/package.json
  • tegg/standalone/standalone/test/fixtures/egg-object-lifecycle-proto/Foo.ts
  • tegg/standalone/standalone/test/fixtures/egg-object-lifecycle-proto/FooEggObjectHook.ts
  • tegg/standalone/standalone/test/fixtures/egg-object-lifecycle-proto/package.json
  • tegg/standalone/standalone/test/fixtures/egg-prototype-lifecycle-proto/Foo.ts
  • tegg/standalone/standalone/test/fixtures/egg-prototype-lifecycle-proto/FooEggPrototypeHook.ts
  • tegg/standalone/standalone/test/fixtures/egg-prototype-lifecycle-proto/package.json
  • tegg/standalone/standalone/test/fixtures/inner-object-proto/foo.ts
  • tegg/standalone/standalone/test/fixtures/inner-object-proto/innerBar.ts
  • tegg/standalone/standalone/test/fixtures/inner-object-proto/package.json
  • tegg/standalone/standalone/test/fixtures/invalid-inner-object-inject/foo.ts
  • tegg/standalone/standalone/test/fixtures/invalid-inner-object-inject/innerBar.ts
  • tegg/standalone/standalone/test/fixtures/invalid-inner-object-inject/package.json
  • tegg/standalone/standalone/test/fixtures/load-unit-instance-lifecycle-proto/Foo.ts
  • tegg/standalone/standalone/test/fixtures/load-unit-instance-lifecycle-proto/FooLoadUnitInstanceHook.ts
  • tegg/standalone/standalone/test/fixtures/load-unit-instance-lifecycle-proto/package.json
  • tegg/standalone/standalone/test/fixtures/load-unit-lifecycle-proto/Foo.ts
  • tegg/standalone/standalone/test/fixtures/load-unit-lifecycle-proto/FooLoadUnitHook.ts
  • tegg/standalone/standalone/test/fixtures/load-unit-lifecycle-proto/Runner.ts
  • tegg/standalone/standalone/test/fixtures/load-unit-lifecycle-proto/package.json
  • tegg/standalone/standalone/test/fixtures/logger-option/foo.ts
  • tegg/standalone/standalone/test/fixtures/logger-option/package.json
  • tegg/standalone/standalone/test/index.test.ts
  • tegg/standalone/standalone/tsdown.config.ts
  • wiki/concepts/tegg-module-plugin.md
  • wiki/index.md
  • wiki/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

Comment on lines +59 to +71
// 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;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 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.

Comment on lines 84 to 107
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;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 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.

Comment on lines +116 to +132
// 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;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 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.

Suggested change
// 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.

Comment on lines 40 to 47
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);
});
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 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.

Suggested change
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

Comment on lines +160 to +164
// 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'];
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 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

Comment on lines +5 to +13
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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 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 {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

直接删掉就好了?

this.app.eggPrototypeLifecycleUtil.deleteLifecycle(this.eggPrototypeCrossCutHook);
this.app.loadUnitLifecycleUtil.deleteLifecycle(this.loadUnitAopHook);
this.app.eggObjectLifecycleUtil.deleteLifecycle(this.eggObjectAopHook);
this.app.eggContextLifecycleUtil.deleteLifecycle(this.aopContextHook);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这行为什么还在?

@InnerObjectProto({ name: 'mysqlDataSourceManager', accessLevel: AccessLevel.PUBLIC })
export class MysqlDataSourceManagerObject {
constructor() {
return MysqlDataSourceManager.instance;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这个显得很奇怪了,应该可以改成直接继承而不是返回一个 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()) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这里行为变了?

@killagu

killagu commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Code Review: 声明式 module plugin 机制

整体架构清晰,happy path 测试充分。但本次评审最核心的主题是:hook 的投递方式从"无条件命令式注册"变成了"扫描 + 提升(promote)流水线",而这条流水线的每一种失败模式都是静默跳过 —— 若干现实部署形态会在没有任何报错的情况下丢失 @Transactional 和 AOP 织入。以下发现经过逐条对抗性验证(14 个候选中 12 个确认、2 个证伪)。

发现(按严重程度排序)

1. tegg/plugin/config/src/lib/ModuleScanner.ts:25 — 多层自定义框架会静默丢失所有内置 DAL/AOP hook

扫描器只解析一个 framework 目录(getFrameworkPath,不走继承链),且只读它 package.json 里的直接依赖。在 app → my-framework → egg 的分层结构下(@eggjs/dal-plugin/aop-plugin 是 egg 的依赖而非 my-framework 的),egg 的插件加载器仍会沿完整 eggPath 链启用这些插件,但 module 扫描永远找不到它们的 eggModule 包 —— 而插件 app.ts 里原有的无条件 registerLifecycle 已被删除。结果:@Transactional 静默失去事务、crosscut/pointcut 通知静默不织入,且没有任何断言能检测到 hook 缺失。现有唯一的自定义框架 fixture 把 module 直接声明为框架依赖,所以测试覆盖不到这条路径。

2. tegg/plugin/tegg/src/lib/EggModuleLoader.ts:62 — bundle 模式下框架 module plugin 永远无法被 promote,hook 被静默跳过

manifest 生成只跑 loadMetadata(不经过 didLoad 阶段的 promote 循环),框架引用以 optional: true 被固化进 manifest;restore 时 ref.path 指向不存在<outputDir>/node_modules/<pkg>,而 #findPackageRoot 对真实存在的目录做 realpath —— 字符串精确比较永远不可能相等,promote 静默落空,ModuleHandler.ts:48 直接 continue 跳过 teggAop/teggDal。打包后的应用原本必然获得的 AOP 织入现在会静默消失。建议:改用 module 名字匹配(两侧都有)而不是磁盘路径相等,并至少在静默 continue 处加一行 coreLogger 日志。(另有一个潜在隐患:plugin.path 指向的是入口目录(src//dist/),任何在该目录带嵌套 package.json stub 的包会让 #findPackageRoot 停在错误的根 —— 仓库内暂无包命中,但第三方 tshy 双构建布局会。)

3. tegg/plugin/tegg/src/lib/ModuleHandler.ts:48 — 被禁用插件的 hook 现在照样运行

该处只判断 moduleDescriptor.optional === true,没有任何 plugin enable 检查。插件直接声明在应用自身 package.json dependencies 里(常规安装方式)时,扫描产出的引用是非 optional(optionalundefined),于是配置了 dal: { enable: false } 之后 DalModuleLoadUnitHook 仍被实例化、其 preCreate 仍会 fork/创建数据源 —— 插件已禁用却照样开 DB 连接、照样织入 AOP。本 PR 之前注册在插件 app.ts 启动逻辑里,天然受 enable 门控。

4. tegg/core/runtime/src/impl/InnerObjectLoadUnitBuilder.ts:58 — inner object 的 proto id 跨模块冲突,同名即启动崩溃

所有 module plugin 的 inner object 共用 instanceModuleName = INNER_OBJECT_LOAD_UNIT_NAME 和相同的默认 qualifier,而 createProtoId 既不含 defineModuleName 也不含 accessLevel。两个互不相识的 module plugin 各自声明 @InnerObjectProto() class FooHook {}(即使都是 PRIVATE)会产生相同 id → 抛 duplicate inner object proto(业务 proto 依靠各自 module 的 LoadUnitName qualifier 可以共存,inner object 不行)。同一 id 机制也破坏了 line 138 的 provided-object 路径:standalone 调用方传 innerObjectHandlers: { mysqlDataSourceManager: [{obj: mock}] }(旧 Runner 上合法的覆盖用法)现在会抛 duplicate provided inner object,因为 dal-plugin 总会被扫描并注册该名字。且报错信息无法定位是哪两个模块冲突。

5. tegg/core/runtime/src/impl/InnerObjectLoadUnitInstance.ts:41 — 自定义 lifecycle type 被 API 接受但必然启动崩溃

装饰器参数类型是刻意开放的(type: 'LoadUnit' | ... | string),core-decorator 还有测试明确断言 'CustomLifecycle' 能正常装饰 —— 但运行时用硬编码的 5 键 map 分发,未命中直接抛 unknown lifecycle type,且没有任何注册 API 可扩展。建议:要么把类型收窄为 5 个字面量,要么提供 lifecycle type 注册表。

6. tegg/standalone/standalone/src/main.ts:63 — 已弃用但仍被支持的 RunnerOptions.innerObjects 被静默丢弃

旧 Runner 支持它(且优先级高于 innerObjectHandlers);新 main() 只转发 innerObjectHandlers,StandaloneAppOptions 没有 innerObjects 字段。JS 调用方的 main(cwd, { innerObjects: { httpClient } }) 对象静默消失 → @Inject() httpClient 启动即失败且毫无提示。PR 描述声称"main() 不变",与此矛盾 —— 要么继续支持,要么抛出弃用错误。

7. tegg/standalone/standalone/src/StandaloneApp.ts:143 — 覆盖优先级反转:框架占位符现在覆盖用户提供的 moduleConfigs/moduleConfig/runtimeConfig

旧 Runner 最后应用 options.innerObjectHandlers(用户 stub 获胜,例如测试里的假 runtimeConfig);新 #createInnerObjects 把框架三件套放在 Object.assign 最后一个参数,用户这几个 key 的条目被静默丢弃并从磁盘重新填充。代码注释表明这是有意为之,但对签名未变的公开 innerObjectHandlers 契约来说是静默的行为破坏 —— 至少应对被覆盖的 key 抛错或告警。

8. tegg/plugin/tegg/src/lib/ModuleHandler.ts:107 — 启动中途失败会泄漏已完全初始化的 inner LoadUnitInstance

init() 先创建 inner instance,但 this.loadUnitInstances 直到最后才赋值,instance 本身没有存到任何字段(只存了 LoadUnit)。若某个业务 module 启动失败,destroy()(经 beforeCloseapp.close() 时可达 —— egg-mock 断言启动失败后的标准 teardown 路径)只会销毁 LoadUnit,永远够不到 instance:lifecycle hook 留在 scope 级 util 里、@LifecycleDestroy 清理(如 DAL 关数据源)不执行、instance 滞留在 LoadUnitInstanceFactory.instanceMap。StandaloneApp 是增量 push 的,没有这个问题 —— 建议对齐该模式,或在 catch 里销毁 inner instance。

已确认但排在前 8 之外

  • wiki 与代码不符:wiki/concepts/tegg-module-plugin.md:52-60 描述的 moduleHandler.registerInnerObjectClazzList()AOP_INNER_OBJECT_CLAZZ_LIST/DAL_INNER_OBJECT_CLAZZ_LIST、"builder 按 class 去重"均不存在;builder 遇重复是抛错,内置 hook 完全经 eggModule 扫描进入。PR 描述也重复了去重的说法。需重写这两条(违反 AGENTS.md "every nontrivial wiki claim should be traceable to raw sources")。
  • 启动开销:framework 扫描现在对每个 app+agent 无条件执行(原先以 pkg.egg.framework 为门控)—— 对框架包递归 glob 加上对 egg 42 个依赖逐个 importResolve+读文件;standalone bundle 模式仍做全量多根磁盘 glob,尽管 buildTeggManifestData 写出的 manifest.moduleReferences 从未被读回(egg 插件侧是消费了的,见 tegg/plugin/config/src/app.ts:57)。
  • 重复代码:EggInnerObjectImplEggObjectImpl 约 220 行的拷贝,唯一差异是一条 hook 解析规则;EggInnerObjectPrototypeImplEggPrototypeImpl + 内联 builder 的逐字拷贝(空子类即可 —— EggObjectFactory 按精确 constructor 分发);standalone 的 buildTeggManifestData 与插件侧逐字节相同。
  • 缺失覆盖:tegg/CLAUDE.md 规则 7 要求 loader/runtime/lifecycle 变更附带多 app 回归覆盖;本 diff 未增加(未发现隔离 bug —— scoped util 与 per-app creator overlay 检查无误 —— 但本 PR 是该 overlay 的第一个真实消费者,而 MultiApp.test.ts 未覆盖 inner object)。

验证阶段被证伪的候选(供参考)

  • LoaderUtil.ts 新增的 !**/test/** 不是行为变化 —— 实测 globby 11 在旧的 !**/test 下就已剪掉 test/ 子树(新代码注释的说法与事实不符)。
  • 怀疑 teggAop barrel 重导出的 hook 类会缺席 bundle manifest 的问题不成立 —— loader 会把 FILE_PATH 重新盖章为被扫描的 barrel 文件(LoaderUtil.ts:134),getDecoratedFiles 因此包含它们;已用真实 LoaderFactory.loadApp 实测验证。

🤖 Generated with Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants