Skip to content

fix(bundle): boot a bundled tegg app (cnpmcore) end to end#5987

Merged
killagu merged 5 commits into
eggjs:nextfrom
killagu:fix/bundle-cnpmcore-boot
Jun 23, 2026
Merged

fix(bundle): boot a bundled tegg app (cnpmcore) end to end#5987
killagu merged 5 commits into
eggjs:nextfrom
killagu:fix/bundle-cnpmcore-boot

Conversation

@killagu

@killagu killagu commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

Motivation

Booting a real, complex multi-plugin egg/tegg app from an @utoo/pack bundle (egg-bin bundlenode worker.js) did not fully work. Using cnpmcore (a large DDD tegg app) as the target, the bundle built fine and booted through the entire egg framework, all plugins, and the egg lifecycle — but then failed inside tegg DI:

TEGG_EGG_PROTO_NOT_FOUND: Object httpClient not found in LOAD_UNIT:cnpmcoreCommon

This PR collects the bundle-compat fixes that get cnpmcore to boot and serve a full npm round-trip from a bundle.

Root cause of the tegg-DI blocker

In a bundle the module source files do not exist on disk and only the manifest knows the decorated files. EggModuleLoader.buildAppGraph already loads modules through the manifest's precomputed decoratedFiles, but EggModuleLoader.loadModule created its per-module load-unit loader via the globbing LoaderFactory.createLoader(MODULE), which returns 0 classes in a bundle.

That loader's load() is what the load-unit lifecycle hook EggQualifierProtoHook.preCreate(ctx) calls (ctx.loader.load()) to add the Egg qualifier to every inject. With 0 classes, the hook never runs, so an inject like cnpmcore's @Inject() httpClient: HttpClient (no explicit @EggQualifier) never gets its Egg=CONTEXT qualifier. Since httpClient exists as both an app and a context egg object, the inject becomes ambiguous and DI fails.

Verified by side-by-side bundle/non-bundle probes: non-bundle resolves the request [Egg=CONTEXT] to exactly 1 proto; the bundle issued [] (qualifier missing) and matched 0.

Changes

  • @eggjs/tegg-plugin EggModuleLoader.loadModule: in bundle mode, reuse the manifest's precomputed decoratedFiles (new ModuleLoader(path, { precomputedFiles, loaderFS })) instead of globbing, so lifecycle hooks see the real decorated classes. Gated on a loadedFromManifest flag — zero behavior change in non-bundle mode.
  • @eggjs/core loader getPluginPath: resolve falsy-path plugins via bundle resolution; #resolveBundlePluginPath falls back to package.json for entry-less plugins.
  • @eggjs/utils importResolve: feature-detect import.meta.resolve and forward paths to the require() fallback.
  • egg customEggPaths: use the rebased framework dir when import.meta.dirname is bundler-rewritten.

Tests

  • BundledAppBoot.test.ts: a fixture proto now carries an auto Egg-qualifier inject of an egg-compatible object (no explicit @EggQualifier), exercising the EggQualifierProtoHook path in manifest-consume mode.
  • The "Theme F" assertion now verifies tegg module discovery is fully manifest-served (no residual module-dir glob), which is exactly the mechanism the fix relies on — this assertion fails without the loadModule change.

End-to-end validation (cnpmcore)

Against cnpmcore@master with published @eggjs/egg-bundler@beta + this branch's packages:

  1. egg-bin bundle → 24-file artifact ✓
  2. node worker.js (MySQL + Redis) → server listening on port 7001
  3. Full npm round-trip against the bundled registry:
    • npm publish @cnpm/hello-bundle@1.0.0
    • npm view → 1.0.0 ✓
    • npm install + require() → returns the package ✓
    • tarball download → HTTP 200 ✓

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Improved bundled application handling, including embedded-manifest loading to reduce filesystem discovery work.
  • Bug Fixes

    • Fixed plugin path resolution in bundle mode, including cases where plugin paths are omitted.
    • Made bundle/framework directory detection more reliable when bundlers rewrite import paths.
    • Hardened import.meta.resolve detection and improved fallback resolution behavior when it’s unavailable.
  • Tests

    • Updated bundled boot assertions to ensure module discovery falls back as expected while first-party discovery avoids unnecessary real-fs globs.

Make a complex multi-plugin egg/tegg app (cnpmcore) boot from an
@utoo/pack bundle, where the module source files do not exist on disk and
framework/plugin paths are rewritten to the bundle output dir.

- core loader getPluginPath: resolve falsy-path plugins via bundle
  resolution; #resolveBundlePluginPath falls back to package.json for
  entry-less plugins
- utils importResolve: feature-detect import.meta.resolve and forward
  paths to the require() fallback
- egg customEggPaths: use the rebased framework dir when
  import.meta.dirname is bundler-rewritten
- tegg EggModuleLoader.loadModule: reuse the manifest's precomputed
  decorated files in bundle mode so load-unit lifecycle hooks
  (EggQualifierProtoHook) still see the real decorated classes via
  ctx.loader.load(). Without this, auto Egg-qualifier injects of
  egg-compatible objects (e.g. httpClient) are ambiguous and DI fails
  with EggPrototypeNotFound.
- test: bundle-mode regression for the auto Egg-qualifier inject and for
  manifest-served module discovery (no residual module-dir glob)

Verified end to end: cnpmcore bundles, boots, and serves a full npm
round-trip (publish/view/install) from the bundle.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings June 23, 2026 02:36

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@coderabbitai

coderabbitai Bot commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f1501b11-e2a7-4f6b-aae9-87c0bfd4c6b0

📥 Commits

Reviewing files that changed from the base of the PR and between ffbee4b and a356760.

📒 Files selected for processing (1)
  • packages/core/src/loader/egg_loader.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/core/src/loader/egg_loader.ts

📝 Walkthrough

Walkthrough

Bundle-mode resolution is hardened across four areas: importResolve gains a runtime capability check for import.meta.resolve and forwards paths in the CJS fallback; EggLoader introduces #isBundleModeForThisApp() to broaden plugin re-resolution and adds a package.json-based fallback; customEggPaths also rebases when import.meta.dirname is bundler-rewritten; EggModuleLoader adds a loadedFromManifest flag to drive precomputed ModuleLoader construction without filesystem globbing, and the corresponding test tightens the no-glob assertion to cover tegg module directories.

Changes

Bundle Mode Resolution and Manifest-Driven Loading

Layer / File(s) Summary
importResolve bundler-compatibility
packages/utils/src/import.ts
supportImportMetaResolve changes from a Node ≥ 18 version gate to a runtime capability check that also verifies import.meta.resolve is a function; the CJS fallback now forwards the caller's paths option to require.resolve.
Plugin path and framework rebasing in bundle mode
packages/core/src/loader/egg_loader.ts, packages/egg/src/lib/egg.ts
Extracts #isBundleModeForThisApp() to gate bundle-store ownership; broadens getPluginPath to re-resolve path-less plugins by package name; adds a package.json-based directory fallback in #resolveBundlePluginPath; extends customEggPaths rebasing to also fire when import.meta.dirname is detected as bundler-rewritten.
EggModuleLoader manifest-driven loading
tegg/plugin/tegg/src/lib/EggModuleLoader.ts
Adds loadedFromManifest flag set in buildAppGraph(); loadModule() reads TEGG_MANIFEST_KEY from manifest extensions to build a unitPath→decoratedFiles map and constructs ModuleLoader with precomputedFiles in bundle mode, falling back to LoaderFactory.createLoader otherwise.
Bundle mode no-glob test assertions
tegg/plugin/tegg/test/BundledAppBoot.test.ts
Theme F assertions now require zero real-fs fallback globs for both first-party app discovery and tegg module-directory discovery in manifest-consume mode, removing the prior allowance for residual globbing under the modules segment.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • eggjs/egg#5932: Wires up bundle-mode module loader through globalThis.__EGG_BUNDLE_MODULE_LOADER__ registration; directly connected to the importResolve capability gating and bundle-mode loader selection logic here.
  • eggjs/egg#5970: Modifies EggModuleLoader.ts to route tegg module discovery through a shared loaderFS, complementing the manifest-derived precomputedFiles loading introduced in this PR.
  • eggjs/egg#5972: Updates EggLoader#getPluginPath and bundle-store gating for plugin path re-resolution in the same code paths modified here.

Poem

🐇 Hop, hop through the bundle's maze,
Where import.meta hides in a haze!
No more disk-glob wandering astray,
The manifest guides the modules' way.
Precomputed paths, no filesystem fuss—
This rabbit's bundle runs without a ruckus! 🎉

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'fix(bundle): boot a bundled tegg app (cnpmcore) end to end' clearly describes the main change—fixing bundled tegg app booting with a specific example (cnpmcore). It directly reflects the PR's primary objective of enabling end-to-end bundled application boot.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@codecov

codecov Bot commented Jun 23, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 96.42857% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 85.94%. Comparing base (5b0d59f) to head (a356760).
⚠️ Report is 1 commits behind head on next.

Files with missing lines Patch % Lines
packages/utils/src/import.ts 75.00% 0 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             next    #5987      +/-   ##
==========================================
+ Coverage   85.91%   85.94%   +0.02%     
==========================================
  Files         669      669              
  Lines       19907    19925      +18     
  Branches     3949     3961      +12     
==========================================
+ Hits        17104    17124      +20     
+ Misses       2425     2423       -2     
  Partials      378      378              

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

@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 improves bundle-mode compatibility and robustness across @eggjs packages. Key changes include updating plugin path resolution to handle missing or rewritten paths, falling back to package.json resolution for older plugins, feature-detecting import.meta.resolve capability, and reusing precomputed decoratedFiles from the manifest in EggModuleLoader to avoid re-globbing module directories. The review feedback highlights two potential runtime crashes: a TypeError when accessing import.meta.resolve if import.meta is undefined, and another TypeError when calling path.resolve with an undefined import.meta.dirname.

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 thread packages/utils/src/import.ts Outdated
Comment on lines +70 to +71
const supportImportMetaResolve =
nodeMajorVersion >= 18 && typeof (import.meta as { resolve?: unknown }).resolve === 'function';

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.

high

If import.meta is undefined (which is common in CommonJS environments or when bundled to CJS without a shim), attempting to access (import.meta as any).resolve will throw a TypeError: Cannot read properties of undefined (reading 'resolve') at runtime. We should safely guard the existence of import.meta before checking the type of its resolve property.

const supportImportMetaResolve =
  nodeMajorVersion >= 18 &&
  typeof import.meta !== 'undefined' &&
  !!import.meta &&
  typeof (import.meta as { resolve?: unknown }).resolve === 'function';

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good catch — fixed in 5997a06 (guarded the undefined case; no-op when defined).

Comment thread packages/egg/src/lib/egg.ts Outdated
killagu and others added 2 commits June 23, 2026 10:47
Address review feedback: avoid TypeErrors when `import.meta` / its
members are absent.

- utils supportImportMetaResolve: guard `typeof import.meta !== 'undefined'`
  before reading `.resolve`, matching the file's existing CJS handling
- egg customEggPaths: short-circuit `importMetaRewritten` when
  `import.meta.dirname` is undefined (bundler-rewritten to undefined or
  pre-20.11 Node) so `path.resolve(undefined)` cannot throw

Both are no-ops when the values are defined.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Theme F assertion required *zero* non-module fallback globs, but a
plugin whose `app/service`/`app/middleware` dir is empty produces an
empty-result glob that the manifest does not cache, so it legitimately
falls back to a real-fs glob. This only materializes in the pnpm/CI
layout (where those dirs sit under `<app>/node_modules/@eggjs/*`), not in
a local run, so CI failed while local passed.

Assert the app's own (first-party) discovery is manifest-served and
exclude third-party `node_modules` globs, which are environment-dependent
and orthogonal to what this test covers.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings June 23, 2026 03:07

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

killagu and others added 2 commits June 23, 2026 11:20
`app.getEggObject` is not available on the single-mode mock app in CI, and
this case did not actually catch the bug anyway (the fixture's module
source files exist on disk, so the pre-fix globbing loader still finds
them). The Theme F assertion — that loadModule no longer globs module dirs
in consume mode — is the real regression for this fix and fails when the
fix is reverted. Revert the now-unused EggTypeService fixture inject.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…absent

#resolveBundlePluginPath rebases a plugin's resolved dir under the output
baseDir's node_modules (e.g. <baseDir>/node_modules/@eggjs/tegg-plugin/dist).
In a real bundle the externals are installed there, so it exists and holds
the files the manifest keyed. But when a bundle store is registered over a
source checkout — an integration test consuming a normally-collected
manifest, or a dev bundle whose externals still resolve from the workspace —
that rebased dir does not exist, so the plugin's config/extend/app files
(e.g. tegg-plugin's app/extend that provides app.getEggObject) fail to load.

Guard the rebase on fs.existsSync; otherwise return the real resolved dir.
No-op for real bundles (rebased dir exists); fixes source/dev consumption.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings June 23, 2026 03:53

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@killagu killagu merged commit fd5358b into eggjs:next Jun 23, 2026
21 checks passed
killagu added a commit that referenced this pull request Jun 23, 2026
Adds a **Bundle Deployment** guide to the Core docs (EN + zh-CN),
documenting the `egg-bin bundle` / `@eggjs/egg-bundler` workflow:

- **Build** — `egg-bin bundle`, options, auto-externals, `module.yml`
config
- **Output** — artifact layout and `bundle-manifest.json`
- **Run** — install externals next to `worker.js` (`npm ci --omit=dev`),
single-process boot
- **Tegg applications** — module discovery is served from the manifest's
`decoratedFiles` (no on-disk glob), keeping the tegg DI graph intact for
auto-resolved egg-compatible injects (`httpClient`/`logger`/`config`)
- **Limitations** — single-process only, native addons external

Registered in the Core sidebar next to the Startup Manifest guide it
builds on. Pairs with the bundle-boot fix in #5987.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Documentation**
* Added a new “Bundle Deployment / 打包部署” documentation section,
including the `egg-bin bundle` workflow, key CLI options, and how build
outputs are structured in `dist-bundle/`.
* Expanded runtime instructions (worker setup and single-process mode),
plus `module.yml` configuration examples and documented limitations.
* Updated both English and Chinese site navigation to include the new
bundle page.
* Refreshed the English bundle page to focus on Egg bundle deployment
and remove tegg application coverage.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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.

2 participants