Skip to content

feat(bundler): enable @eggjs/* packages to be bundled by turbopack#5863

Open
killagu wants to merge 19 commits intoeggjs:nextfrom
killagu:next
Open

feat(bundler): enable @eggjs/* packages to be bundled by turbopack#5863
killagu wants to merge 19 commits intoeggjs:nextfrom
killagu:next

Conversation

@killagu
Copy link
Copy Markdown
Contributor

@killagu killagu commented Apr 12, 2026

Summary

  • ManifestStoresetBundleModuleLoader 改用 globalThis 存储状态,解决 bundled entry 和 external egg 框架之间的 module 实例分裂问题
  • onerror/development 插件模板内联为字符串常量,消除 import.meta.dirname + readFileSync 依赖
  • watcher 插件 event source 从路径引用改为直接 class import
  • ExternalsResolver 不再自动 external 所有 @eggjs/*
  • Bundler 新增 post-process 步骤修复 turbopack 的 import.meta.url/dirname throwing getter

cnpmcore E2E 验证:externals 从 76 → 12,curl http://127.0.0.1:7001/-/ping 返回 HTTP 200。

Changes

Package Change
@eggjs/core ManifestStore.#bundleStoreglobalThis.__EGG_BUNDLE_STORE__
@eggjs/utils _bundleModuleLoader / _snapshotModuleLoader → globalThis
@eggjs/onerror 模板内联到 onerror_page.tstemplatePath 默认空字符串
@eggjs/development loader trace 模板内联到 loader_trace_template.ts
@eggjs/watcher event source 从 path.join(import.meta.dirname, ...) 改为 class import
@eggjs/egg-bundler 移除 @eggjs/* auto-external 规则,新增 #patchImportMetaUrl post-process

Test plan

  • pnpm --filter=@eggjs/core test — ManifestStore globalThis
  • pnpm --filter=@eggjs/utils test — module loaders globalThis
  • pnpm --filter=@eggjs/onerror test — 模板内联 (36/36)
  • pnpm --filter=@eggjs/development test — loader trace 内联 (12/12)
  • pnpm --filter=@eggjs/watcher test — event source class import (4/4)
  • pnpm --filter=@eggjs/egg-bundler test — bundler 单元测试 (59/59)
  • cnpmcore E2E: bundle → start → HTTP 200

🤖 Generated with Claude Code

Summary by CodeRabbit

Release Notes

  • New Features

    • Added application bundler (bundle command) for creating optimized, self-contained application packages with integrated module resolution.
    • Optimized template loading by inlining error page and loader trace templates, reducing runtime filesystem access.
  • Infrastructure Improvements

    • Enhanced module loading system to support bundle-aware module resolution, enabling faster startup times and reduced filesystem scanning during boot.

killagu and others added 19 commits April 11, 2026 22:51
- `@eggjs/utils.setBundleModuleLoader` intercepts `importModule()` BEFORE
  `importResolve()` so bundled apps can redirect module lookups to an
  in-bundle map without the source files existing on disk. Reuses the
  existing `__esModule` double-default and `importDefaultOnly` handling.
- `@eggjs/core.ManifestStore.fromBundle(data, baseDir)` creates a store
  from pre-validated bundled manifest data, skipping the lockfile /
  config fingerprint checks that would fail for a frozen artifact shipped
  across environments.

Both entry points are required by the upcoming @eggjs/egg-bundler to
bootstrap a bundled Egg app without filesystem discovery.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
New `tools/egg-bundler/` workspace package that bundles an entire
Egg.js application into a deployable artifact using @utoo/pack.

Core components:
- `ExternalsResolver` — identifies packages that must not be inlined:
  native addons (scripts matching node-gyp/prebuild-install/napi-rs/
  node-pre-gyp/electron-rebuild, binding.gyp, prebuilds/, *.node files),
  ESM-only packages (`type: "module"` without a `require` export
  condition), plus hard-coded always-external entries (`egg`,
  `@eggjs/*`, `@swc/helpers`, peerDependencies). User `force`/`inline`
  overrides supported.
- `PackRunner` — thin wrapper over `@utoo/pack/cjs/commands/build.js`
  (CJS entry explicit — the ESM entry's extensionless relative imports
  fail under pnpm workspace links). Pre-writes `tsconfig.json` (with
  `experimentalDecorators`/`emitDecoratorMetadata`) and
  `package.json {type:"commonjs"}` into the output dir so SWC's
  decorator compilation and Node's CJS loader both work.
- `ManifestLoader` — loads `.egg/manifest.json`, forks a
  `scripts/generate-manifest.mjs` subprocess to produce the manifest
  on demand, then normalizes `fileDiscovery` / `resolveCache` /
  `extensions.tegg` keys from realpath form (e.g. `../../plugins/static`
  under workspace links, or `.pnpm/...` under real pnpm installs) into
  stable `node_modules/<pkg>/<sub>` form via longest-prefix matching
  against the app + framework dependency map.

Wiring:
- Added `tools/egg-bundler` to root `tsconfig.json` references.
- Added `@utoo/pack: ^1.2.7` to the workspace catalog.
- Root `tsdown.config.ts`: externalize `@utoo/pack` and any `.node`
  binary so rolldown doesn't attempt to analyse the NAPI-RS
  prebuilt binaries shipped by @utoo/pack and its transitive
  `domparser-rs`, which previously broke the workspace build.
- `packages/egg/package.json` auto-picked up the `./lib/snapshot`
  export from tsdown on rebuild.

Fixtures:
- `test/fixtures/apps/minimal-app/` — controller + service + middleware
  + context extension + router + boot class, exercising loader paths
  we want the bundler to cover.
- `test/fixtures/apps/empty-app/` — router-only edge case, proving
  the bundler doesn't fall apart on minimal inputs.

Placeholder test verifies the public `bundle()` entry point throws
until T8 orchestration lands.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Introduce the BundlerConfig type surface that T8 orchestration and the
egg-bin CLI wrapper will consume. bundle() still throws until T8 lands
its implementation.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
`ManifestStore.setBundleStore(store)` registers a pre-built store that
`ManifestStore.load()` returns unconditionally, bypassing the disk read
and invalidation checks. The bundler-generated entry calls this at
startup (before `new Application()`) so the loader uses the frozen
manifest inlined into the bundle instead of looking on disk — bundled
apps don't ship `.egg/manifest.json` as a separate file.

Paired with `ManifestStore.fromBundle()` (already added) to close the
runtime side of the bundler's manifest plumbing.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
EntryGenerator produces two synthetic entry files (`worker.entry.ts` +
`agent.entry.ts`) under `<baseDir>/.egg-bundle/entries/`. The worker
entry, at runtime:

- Pulls every manifest-referenced module into the @utoo/pack chunk via
  static `import * as __mN from '<relative path>'` statements, sorted
  deterministically by posix relKey so artifacts are reproducible and
  T17 snapshot-diffs cleanly.
- Inlines the normalized manifest as a literal `MANIFEST_DATA` constant
  and registers it via `ManifestStore.setBundleStore(fromBundle(...))`
  before `new Application()` runs, so the loader uses the frozen
  bundled data instead of reading `.egg/manifest.json` from disk.
- Builds a `BUNDLE_MAP` keyed by both the relKey form and the resolved
  absolute posix form pointing at the imported module namespace, then
  registers it via `setBundleModuleLoader`, so every `importModule()`
  from egg's loader is short-circuited to the bundled module regardless
  of which lookup form the loader uses internally.
- Calls `startEgg({ baseDir, mode: 'single' })` which builds the Agent
  in-process, so single-mode bundled apps don't need a separate agent
  worker. The generated `agent.entry.ts` is a no-op stub kept as a
  symmetric placeholder for future cluster-mode support.

Uses relative specifiers (not absolute paths) so bundled sources stay
portable across machines and cache-friendly.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Thin wrapper over @eggjs/egg-bundler's programmatic bundle() API. Exposes
--output/--manifest/--framework/--mode/--no-tegg plus
--force-external/--inline-external flag forwarding.

The command is also re-exported from src/index.ts so tsdown's unused-dep
check can see the @eggjs/egg-bundler dependency (commands are otherwise
only reachable at runtime via oclif's dist/commands discovery).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…T11)

T9: 13 tests covering default output paths, import collection + sort
determinism across fileDiscovery/resolveCache/tegg decoratedFiles,
null-resolveCache skip, dedup, runtime hooks (setBundleStore,
setBundleModuleLoader, startEgg), BUNDLE_MAP dual-keying, MANIFEST_DATA
inlining, empty-manifest edge case, agent no-op stub, custom outputDir,
custom framework spec, byte-identical output across independent baseDirs,
and a canonical worker.entry file snapshot.

T11: 11 tests with a mocked buildFunc covering tsconfig.json +
package.json pre-write, recursive outputDir creation, pack config shape
(entry[], target node 22, platform node, standalone output, externals,
treeShaking/minify off), mode default + override, rootPath default to
projectPath + distinct override, sorted file listing including
pre-written files, and error wrapping that names all entries and
preserves cause.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Wire ManifestLoader -> ExternalsResolver -> EntryGenerator -> PackRunner
into a single Bundler class. The public `bundle()` helper now delegates
to `new Bundler(config).run()` instead of throwing.

Each step is wrapped with a human-readable "[@eggjs/egg-bundler] X
failed: ..." prefix and the original error preserved as `cause`. On
success the bundler writes a reference `bundle-manifest.json` into the
output directory (version, mode, entries, externals, chunks) for
debugging and deterministic-bundle verification; the runtime does not
read it.

MVP scope: worker entry only. The agent entry exists as a no-op stub in
single-mode bundles (see EntryGenerator) and is not passed to
PackRunner.

Also adds `docs/output-structure.md` describing the produced layout and
how to run the bundle, and lifts `./lib/Bundler` + `./lib/EntryGenerator`
into the package exports for downstream consumers.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Covers Bundler orchestration end-to-end with a mocked @utoo/pack
buildFunc and a pre-written static StartupManifest fixture (workaround
for the known generate-manifest subprocess fork issue tracked as T8.1).
Verifies:
- BundleResult.outputDir / files / manifestPath shape
- bundle-manifest.json schema (version, mode, chunks, externals sorted)
- PackRunner pre-written tsconfig.json + package.json content visible
  to buildFunc at build time
- mode override and externals.force propagate to the written manifest
- Step error wrapping preserves the root cause across Bundler + PackRunner
- EntryGenerator worker.entry.ts is referenced from bundle-manifest.entries

Phase 2 (real @utoo/pack) and Phase 3 (scratch subprocess path) are
intentionally deferred until T8.1 lands.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The manifest-generate subprocess spawned by ManifestLoader forks a
plain Node process, which under a workspace dev link cannot import
egg's bare-TS sources — the transitive @ContextProto() decorator in
tegg trips Node 22's strip-types parser with SyntaxError.

Changes:
- Switch fork() to spawn() since fork()'s IPC hooks-worker conflicts
  with the tsx resolver under Node 22.
- ManifestLoader.#buildExecArgv() injects `--import=file://.../tsx/
  dist/esm/index.mjs` when no tsx loader is already present. A regex
  detects both bare and URL forms to prevent recursive duplication.
- ManifestLoader.#resolveFrameworkEntryUrl() reads the framework
  package.json's `exports['.']` and passes an explicit file:// URL
  as `frameworkEntry` to the child. Importing the absolute directory
  would bypass the exports field and fail on the workspace-linked
  ./src/index.ts entry.
- generate-manifest.mjs accepts `frameworkEntry` for the initial
  import but still forwards `framework` (absolute dir) to
  `framework.start()` so egg's resolveFrameworkClasses keeps working.
- tsx added as a runtime dependency via the workspace catalog.

Verified with a raw-node invocation of generate-manifest.mjs on the
minimal-app fixture: produces a v1 manifest with 70 fileDiscovery
entries. bundler vitest stays green on unrelated tests.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds throwaway smoke-verified tegg fixture for T14 covering a single
@SingletonProto service + @HTTPController module exposed at
GET /tegg/hello. Used by bundler integration tests to exercise the
tegg plugin path alongside the existing minimal-app/empty-app fixtures.
…, T17)

- Add no-filesystem-scan.test.ts (T13): assert ManifestStore.setBundleStore
  short-circuits disk reads, ManifestStore.globFiles skips its fallback
  on cache hit, and FileLoader with a bundled manifest ignores extra files
  present on disk but absent from fileDiscovery.
- Add deterministic.test.ts (T17): assert bundle() is byte-identical
  across runs (modulo bundle-manifest.generatedAt), EntryGenerator emits
  byte-identical worker.entry.ts from independent fixture clones, and
  no artifact leaks the outputDir absolute path.
- Add .gitignore for test/fixtures/apps/*/.egg/ and .egg-bundle/ so
  integration-test runtime output stops appearing in git status.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
tsdown's default entry `src/**/*.ts` doesn't emit non-TS assets, so
the published `@eggjs/egg-bundler/dist/` was missing
`scripts/generate-manifest.mjs`. ManifestLoader.#generate() resolves
the script via `new URL('../scripts/...', import.meta.url)`, which
works under workspace dev link (src path) but fails in any
tgz/npm-install form with ERR_MODULE_NOT_FOUND.

Add `copy` entry to tsdown.config so the script lands at
`dist/scripts/generate-manifest.mjs` alongside dist/lib/. No runtime
code changes.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Previous commit 6119c44 used `to: 'dist/scripts/generate-manifest.mjs'`
which tsdown treats as a directory, producing a nested
`dist/scripts/generate-manifest.mjs/generate-manifest.mjs/` path. Change
to `to: 'dist/scripts/'` (trailing slash, directory form) so the file
lands flat at `dist/scripts/generate-manifest.mjs` as intended.

Verified dist layout + 58 passed / 1 skipped.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Bundler's local tsdown.config didn't inherit workspace defaults
(`unbundle: true`, `entry: 'src/**/*.ts'`, `fixedExtension: false`,
`external: [...]`), so dist was produced in bundled mode with `.mjs`
files, mismatching publishConfig exports which list unbundled
`./dist/index.js` + six `./lib/*` subpaths.

Explicitly set the workspace defaults in the local config on top of
the copy + unused.ignore additions, producing the standard unbundled
`.js` layout matching packages/core, plugins/*, etc. dist/lib/ now
contains Bundler.js, EntryGenerator.js, etc., and
dist/scripts/generate-manifest.mjs lands via the copy rule.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@utoo/pack's build() expects a wrapper argument with a `.config`
property (it internally reads `bundleOptions.config.entry`). The
previous DEFAULT_BUILD_FUNC unwrapped `wrapped.config` before
passing, so @utoo/pack saw `options.config === undefined` and
threw `TypeError: Cannot read properties of undefined (reading
'entry')` on real builds.

T11 unit tests used a mocked buildFunc and never exercised the
default path — a coverage gap. Ship the 1-line fix plus a
regression test that poisons the CJS require cache for
@utoo/pack's build.js and asserts DEFAULT_BUILD_FUNC passes the
wrapper shape through unchanged. (vi.mock cannot intercept
createRequire-based CJS resolution, hence the cache-poisoning
approach.)

Caught by T16 cnpmcore E2E on real @utoo/pack.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Three runtime blockers resolved to achieve a full T16 Phase E pass
(cnpmcore bundled app boots in 361ms, egg-ready + schedule start):

1. UMD-shaped externals: switch from plain commonjs to
   { commonjs: name, root: name } so @utoo/pack emits a dual
   require()/globalThis branch instead of globalThis-only.

2. External-aware entry generation: split manifest entries into
   internal (static import, bundled) vs external (dynamic
   createRequire at runtime), preventing @utoo/pack from bundling
   externalized package source files whose import.meta.dirname
   would hit turbopack's throwing getter.

3. Runtime baseDir via process.argv[1]: turbopack replaces __dirname
   with compile-time input path; use process.argv[1] to derive the
   actual output directory at runtime.

4. Output package.json name patch: framework reads project name from
   baseDir/package.json; Bundler now patches the output package.json
   with the source project's name after pack.

T16 cnpmcore result: HTTP 503 (server up, DB/Redis unavailable in
ecosystem-ci — expected). Full application lifecycle confirmed:
  application Start duration: 361ms
  app ready
  broadcast egg-ready
  schedule start

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
startEgg({ mode: 'single' }) only initializes Application + Agent but
does not create an HTTP server or bind to a port. The generated
worker.entry.ts now calls app.listen() after startEgg resolves, reading
the port from PORT env, app.config.cluster.listen.port, or defaulting
to 7001.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…led by turbopack

Previously all @eggjs/* packages (~72) had to be externalized when bundling
with @utoo/pack because of two root causes:

1. Module instance splitting — ManifestStore.setBundleStore() and
   setBundleModuleLoader() stored state in module-level variables,
   invisible across bundled/external module boundaries.

2. Plugin asset references — onerror, development, and watcher plugins
   used import.meta.dirname + readFileSync to load templates, which
   breaks when turbopack embeds modules in a single chunk.

This commit fixes both:

- ManifestStore (#bundleStore) and module loaders now use globalThis
  so bundled entry and external egg framework share the same state.
- onerror/development templates inlined as string constants.
- watcher event sources changed from path-based to direct class imports.
- ExternalsResolver no longer auto-externalizes @eggjs/* packages.
- Bundler post-processes turbopack output to fix import.meta.url/dirname.

Result: cnpmcore E2E externals reduced from 76 to 12, HTTP 200 confirmed.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings April 12, 2026 11:02
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 12, 2026

📝 Walkthrough

Walkthrough

This PR introduces a complete application bundling system for EggJS via a new @eggjs/egg-bundler package, including global bundle-store and module-loader registration mechanisms in core and utils, new bundler CLI command in egg-bin, plugin export updates, and comprehensive test coverage with fixture apps.

Changes

Cohort / File(s) Summary
Core Bundle Store Registration
packages/core/src/loader/manifest.ts
Added static setBundleStore(), getBundleStore(), and fromBundle() methods to register/retrieve bundle stores via globalThis. Updated load() to return registered bundle store immediately, bypassing disk reads.
Module Loader Registration
packages/utils/src/import.ts, packages/utils/test/bundle-import.test.ts
Added BundleModuleLoader type and setBundleModuleLoader() to register loaders on globalThis. Updated importModule() to consult bundle loader first, with default export normalization and optional importDefaultOnly handling. Added test coverage for bundle loader interception.
Bundler Package Implementation
tools/egg-bundler/src/lib/{Bundler,EntryGenerator,ExternalsResolver,ManifestLoader,PackRunner}.ts, tools/egg-bundler/src/scripts/generate-manifest.mjs, tools/egg-bundler/src/index.ts
Complete bundling pipeline: Bundler orchestrates the flow, ManifestLoader loads/caches framework manifests, ExternalsResolver computes external packages, EntryGenerator produces worker/agent entries with module maps, PackRunner invokes pack build. Generator script handles manifest generation via subprocess.
Bundler Package Configuration & Documentation
tools/egg-bundler/package.json, tools/egg-bundler/tsdown.config.ts, tools/egg-bundler/vitest.config.ts, tools/egg-bundler/docs/output-structure.md
Added package manifest with exports for bundler modules and dependencies (@utoo/pack, egg). Configured TypeScript bundling, Vitest, and documented output structure (worker entry, manifest, chunk files, and startup flow).
Bundler Testing
tools/egg-bundler/test/{index,integration,deterministic,no-filesystem-scan}.test.ts, tools/egg-bundler/test/{EntryGenerator,ExternalsResolver,PackRunner*}.test.ts
Comprehensive test suites covering entry generation, externals resolution, pack runner behavior, integration bundling, determinism checks, and filesystem-scan prevention via bundle store.
Test Fixture Apps
tools/egg-bundler/test/fixtures/apps/{empty-app,minimal-app,tegg-app}/**, tools/egg-bundler/test/fixtures/externals/basic-app/package.json
Added three fixture applications (empty, minimal with services/middleware, tegg-decorated) and basic-app with diverse dependency types for testing externals resolution and bundling workflows.
Bundler CLI Integration
tools/egg-bin/src/commands/bundle.ts, tools/egg-bin/src/index.ts, tools/egg-bin/package.json
Added Bundle command class with flags for output directory, manifest path, framework, mode, tegg collection, and external overrides. Command resolves paths and dynamically imports bundler. Exported from main index and added to package exports.
Plugin Export Updates
plugins/development/package.json, plugins/onerror/package.json
Added new package export entries: ./lib/snapshot (development plugin) and ./lib/onerror_page (onerror plugin).
Plugin Template Extraction
plugins/development/src/app/middleware/loader_trace_template.ts, plugins/development/src/app/middleware/egg_loader_trace.ts, plugins/onerror/src/lib/onerror_page.ts, plugins/onerror/src/app.ts, plugins/onerror/src/config/config.default.ts
Extracted HTML/template strings into static constants (LOADER_TRACE_TEMPLATE, ONERROR_PAGE_TEMPLATE) removing runtime filesystem reads. Updated middleware and config to use bundled templates with fallback behavior when no custom path provided.
Watcher Configuration Update
plugins/watcher/src/config/config.default.ts
Changed eventSources type from Record<string, string> to `Record<string, string
Workspace & Root Configuration
pnpm-workspace.yaml, tsconfig.json, tsdown.config.ts
Added @utoo/pack (^1.2.7) to pnpm catalog, added tools/egg-bundler as TypeScript project reference, and updated tsdown external config to exclude @utoo/pack and .node files.

Sequence Diagram

sequenceDiagram
    participant User as User/CLI
    participant Bundle as bundle()
    participant ML as ManifestLoader
    participant ER as ExternalsResolver
    participant EG as EntryGenerator
    participant PR as PackRunner
    participant FS as Filesystem

    User->>Bundle: Call bundle(config)
    Bundle->>ML: Load manifest
    ML->>FS: Read package.json & manifest
    ML-->>Bundle: Return StartupManifest

    Bundle->>ER: Resolve externals
    ER->>FS: Read dependencies
    ER-->>Bundle: Return ExternalsConfig

    Bundle->>EG: Generate entries
    EG->>ML: Get discovered files
    EG->>FS: Create output dir
    EG->>FS: Write worker.entry.ts
    EG-->>Bundle: Return GeneratedEntries

    Bundle->>PR: Run pack build
    PR->>FS: Write tsconfig.json
    PR->>FS: Write package.json
    PR->>PR: Invoke buildFunc(`@utoo/pack`)
    PR->>FS: Collect built files
    PR-->>Bundle: Return PackRunnerResult

    Bundle->>FS: Patch import.meta.url
    Bundle->>FS: Write bundle-manifest.json
    Bundle-->>User: Return BundleResult
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • jerryliang64
  • gxkl
  • fengmk2

Poem

🐰 Hop hop, the bundler arrives with glee,
Packing apps into one so tight and free,
No filesystem scans at boot, so fleet,
ManifestStore and loaders complete,
From tegg to eggs, all bundled sweet! 🥚

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 16.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The pull request title clearly and accurately summarizes the main objective: enabling @eggjs/* packages to be bundled by turbopack, which is the primary technical change across the changeset.

✏️ 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 and usage tips.

@codecov
Copy link
Copy Markdown

codecov bot commented Apr 12, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 86.03%. Comparing base (09222b7) to head (d3515c7).

Additional details and impacted files
@@            Coverage Diff             @@
##             next    #5863      +/-   ##
==========================================
+ Coverage   86.00%   86.03%   +0.02%     
==========================================
  Files         667      669       +2     
  Lines       18945    18969      +24     
  Branches     3652     3660       +8     
==========================================
+ Hits        16294    16320      +26     
+ Misses       2297     2294       -3     
- Partials      354      355       +1     

☔ View full report in Codecov by Sentry.
📢 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.

Copy link
Copy Markdown
Contributor

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

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 introduces @eggjs/egg-bundler, a new tool for bundling Egg.js applications into self-contained artifacts. It adds necessary infrastructure for bundle-time manifest loading, external package resolution, and entry point generation. Additionally, it updates the core loader and import utilities to support bundle-based execution. I have identified an issue in the package.json engine version specification that should be addressed.

"egg": "workspace:*"
},
"engines": {
"node": ">=22.18.0"
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

The specified Node.js engine version >=22.18.0 appears to be a typo, as this version does not exist. Please correct this to a valid Node.js version range, for example, >=20.0.0 or another appropriate version that your package supports. An incorrect engine version can cause issues for users trying to install the package.

Suggested change
"node": ">=22.18.0"
"node": ">=20.0.0"

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

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 bundling support (targeting turbopack / @utoo/pack) so Egg apps can bundle more @eggjs/* dependencies without runtime module-instance splits, while removing filesystem/template-path assumptions that break in bundled outputs.

Changes:

  • Move bundling-related shared state (ManifestStore bundle store + module loaders) onto globalThis to avoid duplicate instances across bundled/external module graphs.
  • Inline HTML templates previously loaded via import.meta.dirname + filesystem reads (onerror page + loader trace).
  • Add a new @eggjs/egg-bundler tool + egg-bin bundle command, including externals resolution and a post-process patch for turbopack import.meta shims.

Reviewed changes

Copilot reviewed 62 out of 76 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
tsdown.config.ts Keep @utoo/pack / .node external in builds
tsconfig.json Add tools/egg-bundler TS project reference
tools/egg-bundler/vitest.config.ts Add vitest project config for bundler tool
tools/egg-bundler/tsdown.config.ts Add tsdown build config for bundler tool
tools/egg-bundler/tsconfig.json Tool tsconfig extending repo root
tools/egg-bundler/test/PackRunner.test.ts Unit tests for PackRunner config/output
tools/egg-bundler/test/PackRunner.default-build.test.ts Test default build func via CJS require cache
tools/egg-bundler/test/no-filesystem-scan.test.ts Contract test: manifest prevents directory scans
tools/egg-bundler/test/integration.test.ts Minimal app integration for bundle()
tools/egg-bundler/test/index.test.ts Public surface test for exports
tools/egg-bundler/test/fixtures/externals/basic-app/package.json Externals resolver fixture app manifest
tools/egg-bundler/test/fixtures/externals/basic-app/node_modules/normal-js/package.json Fixture: plain CJS package
tools/egg-bundler/test/fixtures/externals/basic-app/node_modules/native-scripts/package.json Fixture: native install script
tools/egg-bundler/test/fixtures/externals/basic-app/node_modules/native-prebuilds/prebuilds/placeholder.txt Fixture: prebuilds marker
tools/egg-bundler/test/fixtures/externals/basic-app/node_modules/native-prebuilds/package.json Fixture: prebuilds package
tools/egg-bundler/test/fixtures/externals/basic-app/node_modules/native-dotnode/package.json Fixture: .node binary package
tools/egg-bundler/test/fixtures/externals/basic-app/node_modules/native-dotnode/addon.node Fixture: .node binary file
tools/egg-bundler/test/fixtures/externals/basic-app/node_modules/native-binding/package.json Fixture: binding.gyp package
tools/egg-bundler/test/fixtures/externals/basic-app/node_modules/native-binding/binding.gyp Fixture: binding.gyp marker
tools/egg-bundler/test/fixtures/externals/basic-app/node_modules/esm-only/package.json Fixture: ESM-only exports
tools/egg-bundler/test/fixtures/externals/basic-app/node_modules/esm-dual/package.json Fixture: dual ESM/CJS exports
tools/egg-bundler/test/fixtures/externals/basic-app/node_modules/@eggjs/some-plugin/package.json Fixture: @eggjs/* package
tools/egg-bundler/test/fixtures/apps/tegg-app/tsconfig.json Fixture: tegg app tsconfig
tools/egg-bundler/test/fixtures/apps/tegg-app/package.json Fixture: tegg app package.json
tools/egg-bundler/test/fixtures/apps/tegg-app/modules/foo/package.json Fixture: tegg module package.json
tools/egg-bundler/test/fixtures/apps/tegg-app/modules/foo/FooService.ts Fixture: tegg decorated service
tools/egg-bundler/test/fixtures/apps/tegg-app/modules/foo/FooController.ts Fixture: tegg decorated controller
tools/egg-bundler/test/fixtures/apps/tegg-app/config/plugin.ts Fixture: tegg plugins config
tools/egg-bundler/test/fixtures/apps/tegg-app/config/module.json Fixture: tegg module descriptor
tools/egg-bundler/test/fixtures/apps/tegg-app/config/config.default.ts Fixture: tegg config defaults
tools/egg-bundler/test/fixtures/apps/minimal-app/tsconfig.json Fixture: minimal app tsconfig
tools/egg-bundler/test/fixtures/apps/minimal-app/package.json Fixture: minimal app package.json
tools/egg-bundler/test/fixtures/apps/minimal-app/config/config.default.ts Fixture: minimal app config
tools/egg-bundler/test/fixtures/apps/minimal-app/app/service/user.ts Fixture: minimal app service
tools/egg-bundler/test/fixtures/apps/minimal-app/app/router.ts Fixture: minimal app router
tools/egg-bundler/test/fixtures/apps/minimal-app/app/public/.gitkeep Fixture: minimal app public dir
tools/egg-bundler/test/fixtures/apps/minimal-app/app/middleware/timing.ts Fixture: minimal app middleware
tools/egg-bundler/test/fixtures/apps/minimal-app/app/extend/context.ts Fixture: minimal app context extend
tools/egg-bundler/test/fixtures/apps/minimal-app/app/controller/home.ts Fixture: minimal app controller
tools/egg-bundler/test/fixtures/apps/minimal-app/app.ts Fixture: minimal app lifecycle boot
tools/egg-bundler/test/fixtures/apps/empty-app/tsconfig.json Fixture: empty app tsconfig
tools/egg-bundler/test/fixtures/apps/empty-app/package.json Fixture: empty app package.json
tools/egg-bundler/test/fixtures/apps/empty-app/config/config.default.ts Fixture: empty app config
tools/egg-bundler/test/fixtures/apps/empty-app/app/router.ts Fixture: empty app router
tools/egg-bundler/test/fixtures/apps/empty-app/app/public/.gitkeep Fixture: empty app public dir
tools/egg-bundler/test/ExternalsResolver.test.ts Externals resolver behavior tests
tools/egg-bundler/test/EntryGenerator.test.ts Entry generation tests + snapshot
tools/egg-bundler/test/deterministic.test.ts Determinism tests for bundle output
tools/egg-bundler/test/snapshots/EntryGenerator.worker.canonical.snap Canonical worker entry snapshot
tools/egg-bundler/src/scripts/generate-manifest.mjs Child script to generate .egg/manifest.json
tools/egg-bundler/src/lib/PackRunner.ts Pack runner writing tsconfig/package.json + build
tools/egg-bundler/src/lib/ManifestLoader.ts Load/generate/normalize startup manifest
tools/egg-bundler/src/lib/ExternalsResolver.ts Determine externals (native/peer/always/user)
tools/egg-bundler/src/lib/EntryGenerator.ts Generate worker/agent entry sources
tools/egg-bundler/src/lib/Bundler.ts Orchestrate bundling + patch turbopack import.meta
tools/egg-bundler/src/index.ts Public exports + bundle() helper
tools/egg-bundler/package.json New tool package metadata/scripts/exports
tools/egg-bundler/docs/output-structure.md Document bundle output schema/usage
tools/egg-bundler/.gitignore Ignore fixture runtime outputs
tools/egg-bin/src/index.ts Export new bundle command
tools/egg-bin/src/commands/bundle.ts New egg-bin bundle CLI command
tools/egg-bin/package.json Add @eggjs/egg-bundler dependency + export subpath
pnpm-workspace.yaml Add @utoo/pack to catalog
plugins/watcher/src/config/config.default.ts Event source config uses class imports (no dirname)
plugins/onerror/src/lib/onerror_page.ts Inline onerror mustache HTML template
plugins/onerror/src/config/config.default.ts Default templatePath now empty (use inlined template)
plugins/onerror/src/app.ts Use inlined onerror template when no path provided
plugins/onerror/package.json Export new onerror template module
plugins/development/src/app/middleware/loader_trace_template.ts Inline loader trace HTML template
plugins/development/src/app/middleware/egg_loader_trace.ts Use inlined loader trace template
plugins/development/package.json Export new loader trace template module
packages/utils/test/bundle-import.test.ts Tests for bundle module loader interception
packages/utils/test/snapshots/index.test.ts.snap Snapshot update for new export
packages/utils/src/import.ts Add globalThis bundle module loader + global snapshot loader
packages/egg/package.json Export ./lib/snapshot entrypoint
packages/core/src/loader/manifest.ts Add globalThis bundle store + fromBundle() constructor

Comment on lines +1 to +3
import type { BaseEventSource } from '../lib/event-sources/base.ts';
import DefaultEventSource from '../lib/event-sources/default.ts';
import DevelopmentEventSource from '../lib/event-sources/development.ts';
Comment on lines +406 to +411
* Register a bundle module loader. Uses globalThis so that bundled and
* external copies of @eggjs/utils share the same loader.
*/
export function setBundleModuleLoader(loader: BundleModuleLoader | undefined): void {
(globalThis as any).__EGG_BUNDLE_MODULE_LOADER__ = loader;
if (loader) isESM = false;
Comment on lines +50 to +56
pack,
} = this.#config;

const absBaseDir = path.resolve(baseDir);
const absOutputDir = path.resolve(absBaseDir, rawOutputDir);
debug('bundle start: baseDir=%s outputDir=%s framework=%s mode=%s', absBaseDir, absOutputDir, framework, mode);

Comment on lines +174 to +179
let totalPatches = 0;
const entries = await fs.readdir(outputDir);
for (const name of entries) {
if (!name.endsWith('.js')) continue;
const filepath = path.join(outputDir, name);
const content = await fs.readFile(filepath, 'utf8');
Comment on lines +120 to +142
async #isEsmOnly(pkgDir: string): Promise<boolean> {
const pkg = await this.#readPackageJson(pkgDir);
if (pkg.type !== 'module') return false;
const exportsField = pkg.exports;
if (!exportsField || typeof exportsField !== 'object') {
return false;
}
return !this.#hasRequireCondition(exportsField);
}

#hasRequireCondition(value: unknown): boolean {
if (!value || typeof value !== 'object') return false;
if (Array.isArray(value)) {
return value.some((v) => this.#hasRequireCondition(v));
}
const obj = value as Record<string, unknown>;
if ('require' in obj) return true;
for (const v of Object.values(obj)) {
if (this.#hasRequireCondition(v)) return true;
}
return false;
}

Comment on lines +63 to +67
ESM-only packages, peer dependencies, `@eggjs/*`, and the user's
`externals.force` list) are **not** inlined. They must be installed alongside
the bundle — typically by copying the app's `package.json` next to
`worker.js` and running `npm ci --production`, or by deploying the bundle
into an image that already has these dependencies on disk.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 11

🧹 Nitpick comments (14)
plugins/development/src/app/middleware/loader_trace_template.ts (1)

1-1: Rename this file to kebab-case for guideline compliance.

loader_trace_template.ts should be loader-trace-template.ts (and update its import/export subpaths) to match repository file-naming rules.

As per coding guidelines, "{packages,plugins}/**/*.{ts,tsx,js,mjs}: Name files in lowercase with hyphens (e.g. loader-context.ts)".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@plugins/development/src/app/middleware/loader_trace_template.ts` at line 1,
Rename the file loader_trace_template.ts to kebab-case loader-trace-template.ts
and update all imports/exports that reference loader_trace_template (search for
loader_trace_template, LoaderTraceTemplate, or any path segment containing
loader_trace_template) to use the new filename loader-trace-template; ensure
build/system imports, barrel exports, and any relative paths in
plugins/development/src/app/middleware are updated so the module resolves
correctly.
plugins/onerror/package.json (1)

31-31: Use a hyphenated public subpath before this becomes API.

Publishing ./lib/onerror_page cements the underscore-based filename into the package surface. Please rename the module/path to onerror-page and update the matching import/export specifiers before release.

As per coding guidelines, {packages,plugins}/**/*.{ts,tsx,js,mjs}: Name files in lowercase with hyphens (e.g. loader-context.ts).

Also applies to: 44-44

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@plugins/onerror/package.json` at line 31, Rename the published subpath string
"./lib/onerror_page" to "./lib/onerror-page" and update every matching
import/export specifier that references onerror_page to use onerror-page (e.g.,
change imports like import ... from "./lib/onerror_page" and any package.json
exports entries mapping "./lib/onerror_page" to instead use
"./lib/onerror-page"); ensure both the package.json export key/value and all
source import paths are updated so the hyphenated module name is published
consistently (also apply the same change to the second occurrence referenced in
the diff).
tools/egg-bundler/test/fixtures/apps/empty-app/app/router.ts (1)

3-3: Add JSDoc for the exported router initializer.

The default-exported function is missing API documentation.

Suggested patch
+/**
+ * Register fixture routes for bundler integration tests.
+ */
 export default (app: Application): void => {
   app.get('/', async (ctx) => {
     ctx.body = { ok: true };
   });
 };

As per coding guidelines "Document all exported functions, classes, and configuration properties with JSDoc comments."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tools/egg-bundler/test/fixtures/apps/empty-app/app/router.ts` at line 3, The
exported default router initializer lacks JSDoc; add a JSDoc block immediately
above the default export (the anonymous function defined as "export default
(app: Application): void => {") that describes the purpose of the initializer,
documents the parameter with "@param {Application} app - the Egg application
instance" and the return type with "@returns {void}", and include any notes
about side effects (e.g., registering routes/middleware) so the exported
initializer is properly documented for consumers and tooling.
tools/egg-bundler/test/fixtures/apps/tegg-app/config/config.default.ts (1)

6-6: Add JSDoc for this exported config factory.

Line 6 exports a public config function without JSDoc.

Proposed update
+/**
+ * Return default config for tegg-app fixture.
+ */
 export default (): TeggAppConfig => {
   return {
     keys: 'tegg-app-keys',
     security: {
       csrf: {
         enable: false,
       },
     },
   };
 };
As per coding guidelines, "Document all exported functions, classes, and configuration properties with JSDoc comments".
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tools/egg-bundler/test/fixtures/apps/tegg-app/config/config.default.ts` at
line 6, Add a JSDoc comment above the exported default config factory (the
function signature "export default (): TeggAppConfig => {") describing what the
configuration controls and include an `@returns` line specifying it returns a
TeggAppConfig; follow project style (brief description, parameter annotations if
any, and `@returns` TeggAppConfig) so the exported config function is properly
documented.
tools/egg-bundler/test/fixtures/apps/minimal-app/app.ts (1)

3-17: Refactor boot stage tracking to avoid repeated cast chains.

Lines 11-12 and Line 16 repeatedly cast Application through unknown; consider a small typed extension once and guard in both hooks.

Proposed refactor
 import type { Application, ILifecycleBoot } from 'egg';

+interface BootStageApplication extends Application {
+  bootStages?: string[];
+}
+
+/** Fixture boot hook used to verify lifecycle ordering in bundler tests. */
 export default class AppBoot implements ILifecycleBoot {
-  private readonly app: Application;
+  private readonly app: BootStageApplication;

-  constructor(app: Application) {
+  constructor(app: Application) {
-    this.app = app;
+    this.app = app as BootStageApplication;
   }

   async didLoad(): Promise<void> {
-    (this.app as unknown as { bootStages: string[] }).bootStages ??= [];
-    (this.app as unknown as { bootStages: string[] }).bootStages.push('didLoad');
+    this.app.bootStages ??= [];
+    this.app.bootStages.push('didLoad');
   }

   async willReady(): Promise<void> {
-    (this.app as unknown as { bootStages: string[] }).bootStages.push('willReady');
+    this.app.bootStages ??= [];
+    this.app.bootStages.push('willReady');
   }
 }
As per coding guidelines, "Document all exported functions, classes, and configuration properties with JSDoc comments".
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tools/egg-bundler/test/fixtures/apps/minimal-app/app.ts` around lines 3 - 17,
Refactor AppBoot to avoid repeating the (this.app as unknown as { bootStages:
string[] }) cast: create a local typed guard/alias once (e.g., const appWithBoot
= this.app as unknown as { bootStages?: string[] } or a type-guard function) and
use it in didLoad and willReady to initialize and push into bootStages, ensuring
you check/assign bootStages with ??= or an existence check before pushing; also
add a JSDoc comment for the exported class AppBoot describing its purpose and
exported lifecycle hooks (didLoad, willReady).
tools/egg-bundler/test/fixtures/apps/empty-app/config/config.default.ts (1)

5-5: Add JSDoc for the exported config factory.

Line 5 exports a public config function without JSDoc.

Proposed update
+/**
+ * Return default config for empty-app fixture.
+ */
 export default (): EmptyAppConfig => {
   return {
     keys: 'empty-app-keys',
   };
 };
As per coding guidelines, "Document all exported functions, classes, and configuration properties with JSDoc comments".
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tools/egg-bundler/test/fixtures/apps/empty-app/config/config.default.ts` at
line 5, Add a JSDoc comment above the exported config factory (the default
export function exported as "export default (): EmptyAppConfig => {") describing
the purpose of the config factory, documenting that it takes no parameters and
returns an EmptyAppConfig, and include a short description of what values or
defaults the returned config contains; place the comment immediately above the
export so tooling and consumers pick up the documentation.
packages/core/src/loader/manifest.ts (1)

65-67: Remove any casts from global bundle-store access.

Lines 66, 73, and 77 use (globalThis as any), which weakens type safety in a core runtime path.

Proposed refactor
+interface GlobalWithEggBundleStore {
+  __EGG_BUNDLE_STORE__?: ManifestStore;
+}
+
+const globalWithEggBundleStore = globalThis as unknown as GlobalWithEggBundleStore;
+
 static setBundleStore(store: ManifestStore | undefined): void {
-  (globalThis as any).__EGG_BUNDLE_STORE__ = store;
+  globalWithEggBundleStore.__EGG_BUNDLE_STORE__ = store;
 }
 
 static getBundleStore(): ManifestStore | undefined {
-  return (globalThis as any).__EGG_BUNDLE_STORE__;
+  return globalWithEggBundleStore.__EGG_BUNDLE_STORE__;
 }
 
 static load(baseDir: string, serverEnv: string, serverScope: string): ManifestStore | null {
-  const bundleStore: ManifestStore | undefined = (globalThis as any).__EGG_BUNDLE_STORE__;
+  const bundleStore = globalWithEggBundleStore.__EGG_BUNDLE_STORE__;
As per coding guidelines, "Avoid 'any' type in TypeScript; use 'unknown' when type is truly unknown".

Also applies to: 72-74, 77-77

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/core/src/loader/manifest.ts` around lines 65 - 67, Remove the unsafe
(globalThis as any) casts used when accessing __EGG_BUNDLE_STORE__ (e.g., in
setBundleStore and other accessors) by declaring a proper global augmentation or
a specific typed wrapper for that symbol; add a global interface (or a small
helper type) that defines __EGG_BUNDLE_STORE__?: ManifestStore and then replace
the any casts with that typed global reference so reads/writes use the
ManifestStore | undefined type rather than any.
tools/egg-bundler/src/lib/ExternalsResolver.ts (2)

65-66: Redundant check for 'egg'.

Line 66 checks name === 'egg' but 'egg' is already in ALWAYS_EXTERNAL_NAMES (line 21), so line 65 already returns true for it. Consider removing the redundant check.

Proposed fix
   async `#shouldExternalize`(name: string, peerDeps: ReadonlySet<string>): Promise<boolean> {
     if (peerDeps.has(name)) return true;
     if (ALWAYS_EXTERNAL_NAMES.has(name)) return true;
-    if (name === 'egg') return true;

     const pkgDir = await this.#findPackageDir(name);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tools/egg-bundler/src/lib/ExternalsResolver.ts` around lines 65 - 66, The
check "if (name === 'egg') return true;" is redundant because 'egg' is already
included in ALWAYS_EXTERNAL_NAMES; remove that conditional from
ExternalsResolver so the function relies on ALWAYS_EXTERNAL_NAMES.has(name)
only, and run/adjust any related tests to ensure behavior is unchanged (look for
the variable ALWAYS_EXTERNAL_NAMES and the function/method containing the two
conditionals to locate the code).

120-128: Remove unused #isEsmOnly method.

The static analysis correctly flags that #isEsmOnly is declared but never called. Based on the comment at lines 71-73, the decision was made to NOT externalize ESM-only packages, and this method appears to be leftover from an earlier design. Remove it to avoid confusion.

Proposed fix
-  async `#isEsmOnly`(pkgDir: string): Promise<boolean> {
-    const pkg = await this.#readPackageJson(pkgDir);
-    if (pkg.type !== 'module') return false;
-    const exportsField = pkg.exports;
-    if (!exportsField || typeof exportsField !== 'object') {
-      return false;
-    }
-    return !this.#hasRequireCondition(exportsField);
-  }
-
-  `#hasRequireCondition`(value: unknown): boolean {
-    if (!value || typeof value !== 'object') return false;
-    if (Array.isArray(value)) {
-      return value.some((v) => this.#hasRequireCondition(v));
-    }
-    const obj = value as Record<string, unknown>;
-    if ('require' in obj) return true;
-    for (const v of Object.values(obj)) {
-      if (this.#hasRequireCondition(v)) return true;
-    }
-    return false;
-  }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tools/egg-bundler/src/lib/ExternalsResolver.ts` around lines 120 - 128,
Delete the unused private method `#isEsmOnly` from the ExternalsResolver class:
remove its async definition and body that reads package.json via
`#readPackageJson` and calls `#hasRequireCondition`, since ESM-only detection is no
longer used; ensure no other code references `#isEsmOnly` after removal and leave
`#readPackageJson` and `#hasRequireCondition` intact if they are still used
elsewhere.
tools/egg-bundler/src/scripts/generate-manifest.mjs (1)

5-8: Consider adding argument validation.

If the script is invoked without argv[2], JSON.parse(undefined) throws a confusing SyntaxError. A guard would improve debuggability.

💡 Suggested validation
 async function main() {
   debug('argv: %o', process.argv);
+  if (!process.argv[2]) {
+    throw new Error('Usage: generate-manifest.mjs <options-json>');
+  }
   const options = JSON.parse(process.argv[2]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tools/egg-bundler/src/scripts/generate-manifest.mjs` around lines 5 - 8, The
main() function currently calls JSON.parse(process.argv[2]) which throws a
SyntaxError if argv[2] is missing or invalid; add argument validation around
process.argv[2] in generate-manifest.mjs: first check that process.argv[2] is
defined (and non-empty), and if not log a clear error via debug/processLogger
and exit with non-zero status; then wrap JSON.parse in a try/catch to catch
parse errors and log the invalid payload (including the raw argv[2]) before
exiting. Reference: main(), options, process.argv[2], JSON.parse.
tools/egg-bundler/test/deterministic.test.ts (1)

7-7: Use node:assert for the new Vitest suite.

These new bundler tests use expect throughout, but the repo convention for Vitest tests is the built-in assert module. Converting the suite now will keep this package aligned with the rest of the monorepo.

As per coding guidelines, "Use standard assertions with Node.js built-in 'assert' module in Vitest tests instead of external assertion libraries."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tools/egg-bundler/test/deterministic.test.ts` at line 7, The test suite
imports expect from Vitest but must use Node.js built-in assertions per repo
convention; update the import line to import assert from 'node:assert' (keeping
afterEach, beforeEach, describe, it) and replace all uses of expect(...) with
equivalent assert methods (e.g., assert.strictEqual, assert.deepStrictEqual,
assert.ok) throughout the tests in deterministic.test.ts so the suite uses the
standard assert API instead of Vitest's expect.
tools/egg-bundler/src/lib/EntryGenerator.ts (1)

218-221: process.argv[1] may be unreliable with custom loaders or launchers.

When the bundled app is invoked via a loader (e.g., node --import=... worker.js) or a process manager that modifies argv, process.argv[1] might not point to worker.js. Consider documenting this assumption or providing a fallback.

The comment explains why __dirname cannot be used, but process.argv[1] has its own edge cases. A possible alternative is to allow injecting __baseDir via an environment variable:

const __baseDir = process.env.EGG_BUNDLE_DIR ?? path.dirname(path.resolve(process.argv[1]));
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tools/egg-bundler/src/lib/EntryGenerator.ts` around lines 218 - 221, The
current derivation of __baseDir from process.argv[1] is brittle with custom
loaders or process managers; update EntryGenerator (around the __baseDir
definition) to accept an override via an environment variable (e.g.,
EGG_BUNDLE_DIR) and fall back to the existing
path.dirname(path.resolve(process.argv[1])) logic, and add a short comment
documenting this assumption and the fallback so callers can inject a stable base
dir when argv is modified by loaders/launchers.
tools/egg-bundler/package.json (1)

32-43: Consider adding types exports for library subpaths.

The publishConfig.exports provides JS entry points for subpaths (./lib/Bundler, etc.) but lacks corresponding .d.ts type exports. Consumers importing from subpaths won't get type information after publish.

💡 Suggested fix to add types exports
   "publishConfig": {
     "access": "public",
     "exports": {
-      ".": "./dist/index.js",
-      "./lib/Bundler": "./dist/lib/Bundler.js",
-      "./lib/EntryGenerator": "./dist/lib/EntryGenerator.js",
-      "./lib/ExternalsResolver": "./dist/lib/ExternalsResolver.js",
-      "./lib/ManifestLoader": "./dist/lib/ManifestLoader.js",
-      "./lib/PackRunner": "./dist/lib/PackRunner.js",
+      ".": {
+        "types": "./dist/index.d.ts",
+        "default": "./dist/index.js"
+      },
+      "./lib/Bundler": {
+        "types": "./dist/lib/Bundler.d.ts",
+        "default": "./dist/lib/Bundler.js"
+      },
+      "./lib/EntryGenerator": {
+        "types": "./dist/lib/EntryGenerator.d.ts",
+        "default": "./dist/lib/EntryGenerator.js"
+      },
+      "./lib/ExternalsResolver": {
+        "types": "./dist/lib/ExternalsResolver.d.ts",
+        "default": "./dist/lib/ExternalsResolver.js"
+      },
+      "./lib/ManifestLoader": {
+        "types": "./dist/lib/ManifestLoader.d.ts",
+        "default": "./dist/lib/ManifestLoader.js"
+      },
+      "./lib/PackRunner": {
+        "types": "./dist/lib/PackRunner.d.ts",
+        "default": "./dist/lib/PackRunner.js"
+      },
       "./package.json": "./package.json"
     }
   },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tools/egg-bundler/package.json` around lines 32 - 43, The
publishConfig.exports defines JS subpath entries like "./lib/Bundler",
"./lib/EntryGenerator", "./lib/ExternalsResolver", and "./lib/PackRunner" but
does not expose their TypeScript declaration files; add corresponding "types"
exports for each subpath (e.g. map "./lib/Bundler" to "./dist/lib/Bundler.d.ts",
etc.) and ensure the package-level "types" (or "typesVersions" if used) points
to "./dist/index.d.ts" so consumers importing from subpaths (Bundler,
EntryGenerator, ExternalsResolver, ManifestLoader, PackRunner) receive .d.ts
typings after publish.
tools/egg-bundler/test/integration.test.ts (1)

192-219: Redundant bundle() invocation for cause-chain verification.

The test calls bundle() twice with the same failing buildFunc: once via expect(...).rejects.toThrowError() (Line 198-204), and again inside a try/catch (Line 206-218) to inspect the cause chain. Consider combining into a single invocation.

💡 Consolidate to a single bundle() call
   it('wraps a buildFunc failure under the "pack build" step with an identifiable prefix and preserves cause', async () => {
     const original = new Error('synthetic pack failure');
     const buildFunc: BuildFunc = async () => {
       throw original;
     };

-    await expect(
-      bundle({
-        baseDir: FIXTURE_BASE,
-        outputDir: tmpOutput,
-        pack: { buildFunc },
-      }),
-    ).rejects.toThrowError(/\[`@eggjs`\/egg-bundler\] pack build failed/);
-
+    let caught: Error | undefined;
     try {
       await bundle({
         baseDir: FIXTURE_BASE,
         outputDir: tmpOutput,
         pack: { buildFunc },
       });
     } catch (err) {
-      // Bundler wraps with its own message; PackRunner wraps once inside.
-      // Walk the cause chain to find the synthetic root.
+      caught = err as Error;
+    }
+
+    expect(caught).toBeDefined();
+    expect(caught!.message).toMatch(/\[`@eggjs`\/egg-bundler\] pack build failed/);
+
+    // Walk the cause chain to find the synthetic root.
+    if (caught) {
       let cause: unknown = (err as Error).cause;
       while (cause && (cause as Error).cause) cause = (cause as Error).cause;
       expect(cause).toBe(original);
     }
   });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tools/egg-bundler/test/integration.test.ts` around lines 192 - 219, The test
currently calls bundle() twice to both assert the thrown message and inspect the
cause chain; change it to a single invocation by calling bundle({ baseDir:
FIXTURE_BASE, outputDir: tmpOutput, pack: { buildFunc } }) inside a try/catch,
assert the caught error's message matches /\[`@eggjs`\/egg-bundler\] pack build
failed/ (or use toThrow-like regex on err.message), then walk the err.cause
chain to find and expect the original Error; remove the prior
expect(...).rejects.toThrowError invocation so bundle() is only invoked once.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/core/src/loader/manifest.ts`:
- Around line 76-81: The current load method returns any global bundle store
unconditionally; change it to verify the registered bundleStore matches the
requested context before returning by checking its identifying properties
against the incoming baseDir, serverEnv and serverScope (e.g.,
bundleStore.baseDir, bundleStore.serverEnv, bundleStore.serverScope or a
provided bundleStore.scope/equals method) and only return it when all
identifiers match; if they don’t match, fall through to the normal
loading/validation path and emit a debug log stating the mismatch.

In `@plugins/development/src/app/middleware/egg_loader_trace.ts`:
- Line 16: The template injection currently uses JSON.stringify(data) when
setting ctx.body using LOADER_TRACE_TEMPLATE, which can break script contexts;
replace the JSON.stringify call with ctx.helper.sjson(data) so the JSON is
escaped for safe embedding in a <script> block, i.e. update the expression that
builds ctx.body to use ctx.helper.sjson(data) instead of JSON.stringify(data)
(ensure ctx and ctx.helper.sjson are available in the middleware).

In `@plugins/onerror/src/lib/onerror_page.ts`:
- Line 627: The error page currently loads its logo from an external URL in the
<img class="error-logo" src="https://zos.alipayobjects.com/..."> tag; change
this to a self-hosted asset or inline SVG to avoid external network calls and
CSP issues: locate the error page template in onerror_page.ts (the HTML/JSX
containing the img with class "error-logo") and either replace the src with an
imported local file (e.g., import logo from './logo.svg' and use <img src={logo}
...>) or paste the SVG markup inline where the <img> is (keeping
class="error-logo" and any sizing attributes), and ensure any build tooling will
bundle the asset.
- Line 2: The ONERROR_PAGE_TEMPLATE constant is inferred as a huge string
literal which bloats the .d.ts and fails with isolatedDeclarations; add an
explicit type annotation (export const ONERROR_PAGE_TEMPLATE: string) to prevent
embedding the entire template into declaration output, and remove the hard-coded
third-party asset URL found in the template (the SVG at the URL around the image
reference) by either inlining the asset content or replacing it with a
local/static asset reference so the error page has no external network
dependency; update the ONERROR_PAGE_TEMPLATE definition accordingly.

In `@tools/egg-bundler/docs/output-structure.md`:
- Around line 9-20: Add a language identifier to the unlabeled fenced code block
that starts with "<outputDir>/" in output-structure.md by changing the opening
fence from ``` to ```text so markdownlint stops flagging it; ensure only the
fence is updated and the block contents remain unchanged.
- Around line 62-67: The docs currently state that ExternalsResolver keeps
"ESM-only packages" and "@eggjs/*" external, but the new resolver behavior in
this PR bundles those by default unless another rule or externals.force override
applies; update the paragraph to remove ESM-only packages and "@eggjs/*" from
the list of guarantees that are not inlined, and instead explain that
ExternalsResolver treats native addons, peer dependencies and entries in
externals.force as external, while ESM-only packages and `@eggjs/`* are bundled
unless explicitly forced external (mention ExternalsResolver and externals.force
and the deployment flow around worker.js and package.json as necessary).

In `@tools/egg-bundler/src/lib/Bundler.ts`:
- Around line 43-78: The bundler currently ignores the config.tegg flag so the
CLI's --no-tegg is a no-op; extract tegg from this.#config alongside other
options (e.g. const { ..., tegg = true } = this.#config) and pass it into the
EntryGenerator constructor (new EntryGenerator({ baseDir: absBaseDir,
manifestLoader, framework, externals: new Set(...), tegg })) so EntryGenerator
can skip tegg-decorated files when tegg is false.

In `@tools/egg-bundler/src/lib/ManifestLoader.ts`:
- Around line 141-155: The JSON.parse in the private method `#readFromDisk` can
throw a SyntaxError without file context; wrap the parse in a try/catch and
rethrow a new Error that includes this.#manifestPath (and the original error
message or use the error as the cause) so callers know which manifest file
failed to parse; keep the existing version check (parsed.version vs
SUPPORTED_MANIFEST_VERSION) and return the parsed StartupManifest as before.

In `@tools/egg-bundler/test/ExternalsResolver.test.ts`:
- Around line 109-115: The test currently passes because `@eggjs/`* packages are
omitted by default; update the test to actually verify inline precedence by
either (a) using a package that is externalized by default (e.g., replace
'@eggjs/some-plugin' with a known-external package) and asserting that
result[package] becomes undefined when inline includes it, or (b) seed the
resolver's initial externals map to include '@eggjs/some-plugin' (or the target
package) before calling new ExternalsResolver(...).resolve() and then assert
that the seeded entry is removed when inline contains that package; locate the
logic around ExternalsResolver, its constructor options (inline) and the
resolve() result map to implement this change.

In `@tools/egg-bundler/test/index.test.ts`:
- Around line 1-10: Replace Vitest's expect usage with Node's assert/strict:
remove expect from the import line and add import assert from 'assert/strict';
change the three assertions to use assert.strictEqual(typeof bundle,
'function'), assert.strictEqual(typeof Bundler, 'function'), and assert.ok(new
Bundler({ baseDir: '/tmp', outputDir: '/tmp/out' }) instanceof Bundler). Keep
describe and it from 'vitest' unchanged and reference the bundle and Bundler
symbols in index.test.ts when making these replacements.

In `@tools/egg-bundler/tsdown.config.ts`:
- Around line 3-13: The unused configuration in the config object uses an
invalid literal 'warn' for the level field; update the unused.level value to
'warning' in the config defined by defineConfig (the config constant) so it
matches tsdown's allowed options ('error' or 'warning')—locate the unused block
on the config object and replace level: 'warn' with level: 'warning'.

---

Nitpick comments:
In `@packages/core/src/loader/manifest.ts`:
- Around line 65-67: Remove the unsafe (globalThis as any) casts used when
accessing __EGG_BUNDLE_STORE__ (e.g., in setBundleStore and other accessors) by
declaring a proper global augmentation or a specific typed wrapper for that
symbol; add a global interface (or a small helper type) that defines
__EGG_BUNDLE_STORE__?: ManifestStore and then replace the any casts with that
typed global reference so reads/writes use the ManifestStore | undefined type
rather than any.

In `@plugins/development/src/app/middleware/loader_trace_template.ts`:
- Line 1: Rename the file loader_trace_template.ts to kebab-case
loader-trace-template.ts and update all imports/exports that reference
loader_trace_template (search for loader_trace_template, LoaderTraceTemplate, or
any path segment containing loader_trace_template) to use the new filename
loader-trace-template; ensure build/system imports, barrel exports, and any
relative paths in plugins/development/src/app/middleware are updated so the
module resolves correctly.

In `@plugins/onerror/package.json`:
- Line 31: Rename the published subpath string "./lib/onerror_page" to
"./lib/onerror-page" and update every matching import/export specifier that
references onerror_page to use onerror-page (e.g., change imports like import
... from "./lib/onerror_page" and any package.json exports entries mapping
"./lib/onerror_page" to instead use "./lib/onerror-page"); ensure both the
package.json export key/value and all source import paths are updated so the
hyphenated module name is published consistently (also apply the same change to
the second occurrence referenced in the diff).

In `@tools/egg-bundler/package.json`:
- Around line 32-43: The publishConfig.exports defines JS subpath entries like
"./lib/Bundler", "./lib/EntryGenerator", "./lib/ExternalsResolver", and
"./lib/PackRunner" but does not expose their TypeScript declaration files; add
corresponding "types" exports for each subpath (e.g. map "./lib/Bundler" to
"./dist/lib/Bundler.d.ts", etc.) and ensure the package-level "types" (or
"typesVersions" if used) points to "./dist/index.d.ts" so consumers importing
from subpaths (Bundler, EntryGenerator, ExternalsResolver, ManifestLoader,
PackRunner) receive .d.ts typings after publish.

In `@tools/egg-bundler/src/lib/EntryGenerator.ts`:
- Around line 218-221: The current derivation of __baseDir from process.argv[1]
is brittle with custom loaders or process managers; update EntryGenerator
(around the __baseDir definition) to accept an override via an environment
variable (e.g., EGG_BUNDLE_DIR) and fall back to the existing
path.dirname(path.resolve(process.argv[1])) logic, and add a short comment
documenting this assumption and the fallback so callers can inject a stable base
dir when argv is modified by loaders/launchers.

In `@tools/egg-bundler/src/lib/ExternalsResolver.ts`:
- Around line 65-66: The check "if (name === 'egg') return true;" is redundant
because 'egg' is already included in ALWAYS_EXTERNAL_NAMES; remove that
conditional from ExternalsResolver so the function relies on
ALWAYS_EXTERNAL_NAMES.has(name) only, and run/adjust any related tests to ensure
behavior is unchanged (look for the variable ALWAYS_EXTERNAL_NAMES and the
function/method containing the two conditionals to locate the code).
- Around line 120-128: Delete the unused private method `#isEsmOnly` from the
ExternalsResolver class: remove its async definition and body that reads
package.json via `#readPackageJson` and calls `#hasRequireCondition`, since ESM-only
detection is no longer used; ensure no other code references `#isEsmOnly` after
removal and leave `#readPackageJson` and `#hasRequireCondition` intact if they are
still used elsewhere.

In `@tools/egg-bundler/src/scripts/generate-manifest.mjs`:
- Around line 5-8: The main() function currently calls
JSON.parse(process.argv[2]) which throws a SyntaxError if argv[2] is missing or
invalid; add argument validation around process.argv[2] in
generate-manifest.mjs: first check that process.argv[2] is defined (and
non-empty), and if not log a clear error via debug/processLogger and exit with
non-zero status; then wrap JSON.parse in a try/catch to catch parse errors and
log the invalid payload (including the raw argv[2]) before exiting. Reference:
main(), options, process.argv[2], JSON.parse.

In `@tools/egg-bundler/test/deterministic.test.ts`:
- Line 7: The test suite imports expect from Vitest but must use Node.js
built-in assertions per repo convention; update the import line to import assert
from 'node:assert' (keeping afterEach, beforeEach, describe, it) and replace all
uses of expect(...) with equivalent assert methods (e.g., assert.strictEqual,
assert.deepStrictEqual, assert.ok) throughout the tests in deterministic.test.ts
so the suite uses the standard assert API instead of Vitest's expect.

In `@tools/egg-bundler/test/fixtures/apps/empty-app/app/router.ts`:
- Line 3: The exported default router initializer lacks JSDoc; add a JSDoc block
immediately above the default export (the anonymous function defined as "export
default (app: Application): void => {") that describes the purpose of the
initializer, documents the parameter with "@param {Application} app - the Egg
application instance" and the return type with "@returns {void}", and include
any notes about side effects (e.g., registering routes/middleware) so the
exported initializer is properly documented for consumers and tooling.

In `@tools/egg-bundler/test/fixtures/apps/empty-app/config/config.default.ts`:
- Line 5: Add a JSDoc comment above the exported config factory (the default
export function exported as "export default (): EmptyAppConfig => {") describing
the purpose of the config factory, documenting that it takes no parameters and
returns an EmptyAppConfig, and include a short description of what values or
defaults the returned config contains; place the comment immediately above the
export so tooling and consumers pick up the documentation.

In `@tools/egg-bundler/test/fixtures/apps/minimal-app/app.ts`:
- Around line 3-17: Refactor AppBoot to avoid repeating the (this.app as unknown
as { bootStages: string[] }) cast: create a local typed guard/alias once (e.g.,
const appWithBoot = this.app as unknown as { bootStages?: string[] } or a
type-guard function) and use it in didLoad and willReady to initialize and push
into bootStages, ensuring you check/assign bootStages with ??= or an existence
check before pushing; also add a JSDoc comment for the exported class AppBoot
describing its purpose and exported lifecycle hooks (didLoad, willReady).

In `@tools/egg-bundler/test/fixtures/apps/tegg-app/config/config.default.ts`:
- Line 6: Add a JSDoc comment above the exported default config factory (the
function signature "export default (): TeggAppConfig => {") describing what the
configuration controls and include an `@returns` line specifying it returns a
TeggAppConfig; follow project style (brief description, parameter annotations if
any, and `@returns` TeggAppConfig) so the exported config function is properly
documented.

In `@tools/egg-bundler/test/integration.test.ts`:
- Around line 192-219: The test currently calls bundle() twice to both assert
the thrown message and inspect the cause chain; change it to a single invocation
by calling bundle({ baseDir: FIXTURE_BASE, outputDir: tmpOutput, pack: {
buildFunc } }) inside a try/catch, assert the caught error's message matches
/\[`@eggjs`\/egg-bundler\] pack build failed/ (or use toThrow-like regex on
err.message), then walk the err.cause chain to find and expect the original
Error; remove the prior expect(...).rejects.toThrowError invocation so bundle()
is only invoked once.
🪄 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: 47813087-d211-4f40-8b8f-6de71afd7874

📥 Commits

Reviewing files that changed from the base of the PR and between 09222b7 and d3515c7.

⛔ Files ignored due to path filters (13)
  • packages/utils/test/__snapshots__/index.test.ts.snap is excluded by !**/*.snap
  • tools/egg-bundler/test/__snapshots__/EntryGenerator.worker.canonical.snap is excluded by !**/*.snap
  • tools/egg-bundler/test/fixtures/externals/basic-app/node_modules/@eggjs/some-plugin/package.json is excluded by !**/node_modules/**
  • tools/egg-bundler/test/fixtures/externals/basic-app/node_modules/esm-dual/package.json is excluded by !**/node_modules/**
  • tools/egg-bundler/test/fixtures/externals/basic-app/node_modules/esm-only/package.json is excluded by !**/node_modules/**
  • tools/egg-bundler/test/fixtures/externals/basic-app/node_modules/native-binding/binding.gyp is excluded by !**/node_modules/**
  • tools/egg-bundler/test/fixtures/externals/basic-app/node_modules/native-binding/package.json is excluded by !**/node_modules/**
  • tools/egg-bundler/test/fixtures/externals/basic-app/node_modules/native-dotnode/addon.node is excluded by !**/node_modules/**
  • tools/egg-bundler/test/fixtures/externals/basic-app/node_modules/native-dotnode/package.json is excluded by !**/node_modules/**
  • tools/egg-bundler/test/fixtures/externals/basic-app/node_modules/native-prebuilds/package.json is excluded by !**/node_modules/**
  • tools/egg-bundler/test/fixtures/externals/basic-app/node_modules/native-prebuilds/prebuilds/placeholder.txt is excluded by !**/node_modules/**
  • tools/egg-bundler/test/fixtures/externals/basic-app/node_modules/native-scripts/package.json is excluded by !**/node_modules/**
  • tools/egg-bundler/test/fixtures/externals/basic-app/node_modules/normal-js/package.json is excluded by !**/node_modules/**
📒 Files selected for processing (63)
  • packages/core/src/loader/manifest.ts
  • packages/egg/package.json
  • packages/utils/src/import.ts
  • packages/utils/test/bundle-import.test.ts
  • plugins/development/package.json
  • plugins/development/src/app/middleware/egg_loader_trace.ts
  • plugins/development/src/app/middleware/loader_trace_template.ts
  • plugins/onerror/package.json
  • plugins/onerror/src/app.ts
  • plugins/onerror/src/config/config.default.ts
  • plugins/onerror/src/lib/onerror_page.ts
  • plugins/watcher/src/config/config.default.ts
  • pnpm-workspace.yaml
  • tools/egg-bin/package.json
  • tools/egg-bin/src/commands/bundle.ts
  • tools/egg-bin/src/index.ts
  • tools/egg-bundler/.gitignore
  • tools/egg-bundler/docs/output-structure.md
  • tools/egg-bundler/package.json
  • tools/egg-bundler/src/index.ts
  • tools/egg-bundler/src/lib/Bundler.ts
  • tools/egg-bundler/src/lib/EntryGenerator.ts
  • tools/egg-bundler/src/lib/ExternalsResolver.ts
  • tools/egg-bundler/src/lib/ManifestLoader.ts
  • tools/egg-bundler/src/lib/PackRunner.ts
  • tools/egg-bundler/src/scripts/generate-manifest.mjs
  • tools/egg-bundler/test/EntryGenerator.test.ts
  • tools/egg-bundler/test/ExternalsResolver.test.ts
  • tools/egg-bundler/test/PackRunner.default-build.test.ts
  • tools/egg-bundler/test/PackRunner.test.ts
  • tools/egg-bundler/test/deterministic.test.ts
  • tools/egg-bundler/test/fixtures/apps/empty-app/app/public/.gitkeep
  • tools/egg-bundler/test/fixtures/apps/empty-app/app/router.ts
  • tools/egg-bundler/test/fixtures/apps/empty-app/config/config.default.ts
  • tools/egg-bundler/test/fixtures/apps/empty-app/package.json
  • tools/egg-bundler/test/fixtures/apps/empty-app/tsconfig.json
  • tools/egg-bundler/test/fixtures/apps/minimal-app/app.ts
  • tools/egg-bundler/test/fixtures/apps/minimal-app/app/controller/home.ts
  • tools/egg-bundler/test/fixtures/apps/minimal-app/app/extend/context.ts
  • tools/egg-bundler/test/fixtures/apps/minimal-app/app/middleware/timing.ts
  • tools/egg-bundler/test/fixtures/apps/minimal-app/app/public/.gitkeep
  • tools/egg-bundler/test/fixtures/apps/minimal-app/app/router.ts
  • tools/egg-bundler/test/fixtures/apps/minimal-app/app/service/user.ts
  • tools/egg-bundler/test/fixtures/apps/minimal-app/config/config.default.ts
  • tools/egg-bundler/test/fixtures/apps/minimal-app/package.json
  • tools/egg-bundler/test/fixtures/apps/minimal-app/tsconfig.json
  • tools/egg-bundler/test/fixtures/apps/tegg-app/config/config.default.ts
  • tools/egg-bundler/test/fixtures/apps/tegg-app/config/module.json
  • tools/egg-bundler/test/fixtures/apps/tegg-app/config/plugin.ts
  • tools/egg-bundler/test/fixtures/apps/tegg-app/modules/foo/FooController.ts
  • tools/egg-bundler/test/fixtures/apps/tegg-app/modules/foo/FooService.ts
  • tools/egg-bundler/test/fixtures/apps/tegg-app/modules/foo/package.json
  • tools/egg-bundler/test/fixtures/apps/tegg-app/package.json
  • tools/egg-bundler/test/fixtures/apps/tegg-app/tsconfig.json
  • tools/egg-bundler/test/fixtures/externals/basic-app/package.json
  • tools/egg-bundler/test/index.test.ts
  • tools/egg-bundler/test/integration.test.ts
  • tools/egg-bundler/test/no-filesystem-scan.test.ts
  • tools/egg-bundler/tsconfig.json
  • tools/egg-bundler/tsdown.config.ts
  • tools/egg-bundler/vitest.config.ts
  • tsconfig.json
  • tsdown.config.ts

Comment on lines 76 to +81
static load(baseDir: string, serverEnv: string, serverScope: string): ManifestStore | null {
const bundleStore: ManifestStore | undefined = (globalThis as any).__EGG_BUNDLE_STORE__;
if (bundleStore) {
debug('load: returning registered bundle store');
return bundleStore;
}
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.

⚠️ Potential issue | 🟠 Major

Scope-check the registered bundle store before returning it.

Line 77 currently short-circuits load() for any non-empty global store. In multi-app or reused-process scenarios, this can return a manifest from a different baseDir/env/scope and bypass the normal validation path.

Proposed fix
 static load(baseDir: string, serverEnv: string, serverScope: string): ManifestStore | null {
-  const bundleStore: ManifestStore | undefined = (globalThis as any).__EGG_BUNDLE_STORE__;
-  if (bundleStore) {
+  const bundleStore = ManifestStore.getBundleStore();
+  if (
+    bundleStore &&
+    bundleStore.baseDir === baseDir &&
+    bundleStore.data.invalidation.serverEnv === serverEnv &&
+    bundleStore.data.invalidation.serverScope === serverScope
+  ) {
     debug('load: returning registered bundle store');
     return bundleStore;
   }
+  if (bundleStore) {
+    debug('load: ignore registered bundle store due context mismatch');
+  }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/core/src/loader/manifest.ts` around lines 76 - 81, The current load
method returns any global bundle store unconditionally; change it to verify the
registered bundleStore matches the requested context before returning by
checking its identifying properties against the incoming baseDir, serverEnv and
serverScope (e.g., bundleStore.baseDir, bundleStore.serverEnv,
bundleStore.serverScope or a provided bundleStore.scope/equals method) and only
return it when all identifiers match; if they don’t match, fall through to the
normal loading/validation path and emit a debug log stating the mismatch.

const template = await fs.readFile(templatePath, 'utf8');
const data = await loadTimingData(app);
ctx.body = template.replace('{{placeholder}}', JSON.stringify(data));
ctx.body = LOADER_TRACE_TEMPLATE.replace('{{placeholder}}', JSON.stringify(data));
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.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, locate and read the target file
cat -n plugins/development/src/app/middleware/egg_loader_trace.ts

Repository: eggjs/egg

Length of output: 1624


🏁 Script executed:

# Read the template file
cat -n plugins/development/src/app/middleware/loader_trace_template.ts

Repository: eggjs/egg

Length of output: 1585


🏁 Script executed:

# Search for ctx.helper or escape-related functions in the codebase
rg "helper\.escape|ctx\.helper" --type ts -A 2 -B 2 | head -50

Repository: eggjs/egg

Length of output: 4625


🏁 Script executed:

# Search for escape function in security or core context
rg "escape\s*\(" --type ts plugins/security/ -B 3 -A 5 | head -100

Repository: eggjs/egg

Length of output: 681


🏁 Script executed:

# Find the escape helper test
rg "escape" plugins/security/test/app/extends/helper.test.ts -B 5 -A 10

Repository: eggjs/egg

Length of output: 1540


🏁 Script executed:

# Search for escape helper implementation in security plugin
rg "escape.*=" plugins/security/src --type ts -B 2 -A 5 | head -80

Repository: eggjs/egg

Length of output: 1151


🏁 Script executed:

# Find escape function definition
fd ".*helper.*" plugins/security/src -type f | xargs grep -l "escape"

Repository: eggjs/egg

Length of output: 225


🏁 Script executed:

# Find escape function definition
find plugins/security/src -name "*.ts" -type f | xargs grep -l "escape"

Repository: eggjs/egg

Length of output: 355


🏁 Script executed:

# Read the escape helper
cat -n plugins/security/src/lib/helper/escape.ts

Repository: eggjs/egg

Length of output: 139


🏁 Script executed:

# Check sjson helper which might be for safe JSON in scripts
cat -n plugins/security/src/lib/helper/sjson.ts

Repository: eggjs/egg

Length of output: 1082


🏁 Script executed:

# Check sjs helper which is used by sjson
cat -n plugins/security/src/lib/helper/sjs.ts

Repository: eggjs/egg

Length of output: 1729


🏁 Script executed:

# Check the helper index to see what's exported
cat -n plugins/security/src/lib/helper/index.ts

Repository: eggjs/egg

Length of output: 1011


🏁 Script executed:

# Verify that sjson is available by checking core context extensions
rg "sjson" plugins/security/src -A 3 -B 1 | head -30

Repository: eggjs/egg

Length of output: 924


Use ctx.helper.sjson() instead of JSON.stringify() for JSON embedded in script context.

JSON.stringify(data) inserted directly into a <script> block (line 11 of the template) can break script context if the data contains characters like </script> or certain unicode sequences.

The framework provides ctx.helper.sjson(), designed specifically for "escape json for output in script". This helper properly escapes all JavaScript-sensitive characters and sanitizes object keys:

-    ctx.body = LOADER_TRACE_TEMPLATE.replace('{{placeholder}}', JSON.stringify(data));
+    ctx.body = LOADER_TRACE_TEMPLATE.replace('{{placeholder}}', ctx.helper.sjson(data));
📝 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
ctx.body = LOADER_TRACE_TEMPLATE.replace('{{placeholder}}', JSON.stringify(data));
ctx.body = LOADER_TRACE_TEMPLATE.replace('{{placeholder}}', ctx.helper.sjson(data));
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@plugins/development/src/app/middleware/egg_loader_trace.ts` at line 16, The
template injection currently uses JSON.stringify(data) when setting ctx.body
using LOADER_TRACE_TEMPLATE, which can break script contexts; replace the
JSON.stringify call with ctx.helper.sjson(data) so the JSON is escaped for safe
embedding in a <script> block, i.e. update the expression that builds ctx.body
to use ctx.helper.sjson(data) instead of JSON.stringify(data) (ensure ctx and
ctx.helper.sjson are available in the middleware).

@@ -0,0 +1,1336 @@
/** Error page template - inlined from onerror_page.mustache.html */
export const ONERROR_PAGE_TEMPLATE = `<!doctype html>
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.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

For TypeScript declaration emit, what .d.tsis produced forexport const x = `...`versusexport const x: string = `...`?

💡 Result:

Top web search findings:

  1. @type {const} produces namespace in .d.ts instead of normal object type · Issue #56789 · microsoft/TypeScript: @type {const} produces namespace in .d.ts instead of normal object type ... ### 🔎 Search Terms @type {const} const definition types ### 🕗 Version & Regression Information - This is a crash in all versions ### ⏯ Playground Link ... ts // Typescript file export const myConst = /** `@type` {const} */ { foo: 'bar', fuu: { foo: 'bar' } }; js // Javascript file export const myConst = /** `@type` {const} */ { foo: 'bar', fuu: { foo: 'bar' } }; ### 🙁 Actual behavior ts // .D.TS from Typescript file export declare const myConst: { foo: string; fuu: { foo: string; }; }; ts // .D.TS from Javascript file export namespace myConst { let foo: string; namespace fuu { let foo_1: string; export { foo_1 as foo }; } } ### 🙂 Expected behavior Same output in both cases. Typescript file output is the correct one. ### Additional information about the issue No response --- ### Timeline ... > The @type {const} doesn't seem to be relevant - the .js produces the same output whether it's present or not. > ... > I guess, same code in Typescript and Javascript should output same Javascript and same definition types files. And ... @RyanCavanaugh commented · Dec 21, 2023 at 9:35pm > In TS files, you have to use TS syntax for type annotations / assertions. Producing identical .d.ts output from .ts ... > Then, how can I annotate the JS file to get a constant declaration? I'm trying to expose types of a library to be ... RyanCavanaugh changed the title from "@type {const} emit different definition types from Javascript and Typescript files" to "@type {const} produces namespace in .d.ts instead of normal object type" · Dec 22, 2023 at ... RyanCavanaugh added this to milestone TypeScript 5.5.0 · Dec 22, 2023 at 8:54pm @RyanCavanaugh commented · Dec 22, 2023 at 8:56pm > The are sort of two issues being implied here: > * TS and JS .d.ts output should be identical -- again, this is not an invariant > * JS .d.ts is producing a weird namespace declaration when it could just be a const - this should be changed @sandersn commented · Dec 27, 2023 at 10:03pm > I think there are two smaller repro pairs that are relevant. > > Equivalence of 'as const': > > ts > export const a_ts = { foo: 'bar' } as const > export const a_js = /** `@type` {const} */({ foo: 'bar' }) > > > Both a_ts and a_js should have type { readonly foo: 'bar' } not { foo: string }. Quick info shows that they do, ... > Both b_ts and b_js should generate export declare const b_ts: { foo: string } But b_js generates > > ```ts ... > This is the bug; the rest of the export { foo_1 as foo } in the longer examples are renaming shenanigans that are ... > I'm pretty sure this is a result of `const x =
  2. inconsistent string template exports in type interface files · Issue #38055 · microsoft/TypeScript: # Issue: microsoft/TypeScript #38055 - Repository: microsoft/TypeScript | TypeScript is a superset of JavaScript that compiles to clean JavaScript output. | ... ## inconsistent string template exports in type interface files - Author: @aappddeevv - State: closed (completed) - Locked: true - Labels: Question - Created: 2020-04-20T12:00:48Z - Updated: 2025-10-21T23:03:38Z - Closed: 2020-04-25T00:00:01Z ... TypeScript Version: 3.8.3 Code ts export const strange = `no interpolated values` export const interpolated = `has interpolated ${some_value} values` Expected behavior: ts export declare const strange: string export declare const interpolated: string Actual behavior: ts export declare const strange = "no interpolated values" export declare const interpolated: string I could not determine if this was expected but it seemed inconsistent. --- ### Timeline aappddeevv mentioned this in issue #147: interpolated string in ts file seems to get translated into a complex typing that is not as string · Apr 20, 2020 at 12:03pm @MartinJohns commented · Apr 20, 2020 at 12:55pm > This is by design. See: #16592 > > The first version is a constant because it contains no interpolated values. That means a more strict type (the string literal type) can be inferred. @j-oliveras commented · Apr 20, 2020 at 1:39pm > As a workaround you can set the type explicitly: > > ts > export const strange: string = `no interpolated values` > RyanCavanaugh added label Question · Apr 20, 2020 at 8:44pm @typescript-bot commented · Apr 25, 2020 at 12am > This issue has been marked as 'Question' and has seen no recent activity. It has been automatically closed for ... typescript-bot closed this · Apr 25
  3. Typescript export const foo vs export default { foo }: Typescript export const foo vs export default { foo } ... - Asked on: Aug 2, 2021 - Last active: Aug 3, 2021 - License: CC BY-SA 4.0 --- ## Question Are the three export options exactly equivalent? // foo.ts export const foo = () => {} // foo.ts const foo = () => {} export default { foo } // foo.ts const foo = () => {} export { foo } Disclaimer: I know that import { foo } from './foo' will work just the same in all scenarios but are there any ... - By: user16435030 - Answered on: Aug 3, 2021 The short answer is no they're not the same, they have different syntax, but let's be serious. export const foo = () => {} ... "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.foo = void 0; var foo = function () { }; exports.foo = foo; Is what that compiles to using commonjs and es5 as target. const foo = () => {} export default { foo } ... "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); var foo = function () { }; exports.default = { foo: foo }; Is what that compiles to. const foo = () => {} export { foo } ... "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.foo = void 0; var foo = function () { }; exports.foo = foo; Is what that compiles to. Along with that it should also be noted that using: export { foo } Can only be imported using: import {foo} from './foo' While using: export default { foo } Can only be imported using: ``` import whatever from './foo' //
  4. string enum works from exported strings but not from strings exported as const · Issue #59187 · microsoft/TypeScript: ## string enum works from exported strings but not from strings exported as const - Author: @daniele-orlando - State: closed (not_planned) - Locked: true - Labels: Working as Intended - Created: 2024-07-08T20:37:08Z - Updated: 2025-10-22T03:05:06Z - Closed: 2024-07-13T01:31:04Z ... ### 🔎 Search Terms enum ### 🕗 Version & Regression Information TypeScript: 5.5.3 - This is the behavior in every version I tried ### ⏯ Playground Link Reproduction Project ### 💻 Code ts // FILE: ./case1.ts export const Case1_ImageJpegType = 'image/jpeg' ... // FILE: ./case2.ts export const Case2_ImageJpegType = 'image/jpeg' as const ... enum Case1 { // WORKS AS EXPECTED Png = Case1_ImagePngType, Jpeg = Case1_ImageJpegType, } ... // FAILS WITH: Type 'string' is not assignable to type 'number' as required for computed enum member values. Png = Case2_ImagePngType, Jpeg = Case2_ImageJpegType, } ### 🙁 Actual behavior Exporting a string as const breaks string enums. sh npx -p typescript@5.5.3 tsc --noEmit -p tsconfig.verbatime-false.json // FAILS with // Type 'string' is not assignable to type 'number' as required for computed enum member values. ### 🙂 Expected behavior Exporting a string with or without as const should not break string enums. sh npx -p typescript@5.5.3 tsc --noEmit -p tsconfig.verbatime-false.json // SHOULD NOT FAIL ts export const MyConst = 'MyConst' export const MyConst = 'MyConst' as const should behave the same when used as ts import {MyConst} from './something' ... > Looks like the generated typings are different with `as const`. > > `export declare const Case1_ImageJpegType = "image/jpeg";` > versus > `export declare const Case2_ImageJpegType: "image/jpeg";` > > Exactly why, I couldn't say, but I'm guessing the problem is related to the lack of an assigned value in the latter. ... > Exactly. It is a sneaky difference in the generated `.d.ts` file. I don't know the exact semantic difference between ... > > ts > export declare const A1 = 'A' > export declare const A2: 'A' > > > For mere curiosity, aside from the issue explained, I would be glad if someone could point me to a ... **@RyanCavanaugh** commented · Jul 9, 2024 at 11:44pm >ts > // Widening literal type > export declare const A1 = 'A' > // Nonwidening literal type > export declare const A2: 'A' > > > See also https://mariusschulz.com/blog/literal-type-widening-in-typescript > > The difference is basically that a reference to the unannotated one is preferentially `string` but can act as `"A"` in context, whereas the other is always just `"A"`. This difference needs to exist for idiomatic code like this: ... > > vs here with the nonwidening behavior, you get an error: > >ts > const DefaultUserName = "anonymous" as const; > const users = [DefaultUserName]; > // Error > users.push("bob"); ... > What I still find strange is the fact that the widened form export declare const A1 = 'A' can be used as value for ... > From what I understand from your explanation and from the article literal-type-widening-in-typescript, it ... > I understand the rationale for the difference with respect to how it affects typing now. Is the lack of a literal ... > This issue has been marked as "Working as Intended" and has seen no recent activity. It has been automatically closed ... > Given these two forms > > ```ts > export const Case
  5. TypeScript: Documentation - Modules - Reference: The TypeScript compiler recognizes standard ECMAScript module syntax in TypeScript and JavaScript files and many forms of CommonJS syntax in JavaScript files. ... Type aliases, interfaces, enums, and namespaces can be exported from a module with anexport modifier, like any standard JavaScript declaration: ts// Standard JavaScript syntax...export function f() {}// ...extended to type declarationsexport type SomeType = /* ... */;export interface SomeInterface { /* ... */ } ... tsexport { f, SomeType, SomeInterface }; ... import("./module.js")'let x: mod.SomeType; // Ok ... When emitting imports and exports to JavaScript, by default, TypeScript automatically elides (does not emit) imports that are only used in type positions and exports that only refer to types. Type-only imports and exports can be used to force this behavior and make the elision explicit. Import declarations written with`import type`, export declarations written with`export type { ... }`, and import or export specifiers prefixed with the`type` keyword are all guaranteed ... TypeScript provides a type syntax similar to JavaScript’s dynamic`import` for referencing the type of a module without ... ts// Access an exported type:type WriteFileOptions = import("fs").WriteFileOptions;// Access the type of an exported ... ts/** @type {import("webpack").Configuration} /module.exports = { // ...} ### export = and import = require() When emitting CommonJS modules, TypeScript files can use a direct analog of`module.exports = ...` and`const mod = ... ts// `@Filename`: main.tsimport fs = require("fs");export = fs.readFileSync("...");// `@Filename`: main.js"use ... This syntax was used over its JavaScript counterparts since variable declarations and property assignments could not refer to TypeScript types, whereas special TypeScript syntax could: ... ts// `@Filename`: a.tsinterface Options { /* ... */ }module.exports = Options; // Error: 'Options' only refers to a type, ... TypeScript supports a syntax in script (non-module) files for declaring a module that exists in the runtime but has no ... tsdeclare module "path" { export function normalize(p: string): string; export function join(...paths: any[]): ... tsdeclare module ".html" { const content: string; export default content;} ``` ## The module compiler option This section discusses the details of eachmodule compiler option value. See the Module output format theory section for more background on what the option is and how it fits into the overall compilation process. In brief, the`module` compiler option was historically only used to control the output module format of emitted JavaScript ... Node.js supports both CommonJS and ECMAScript modules, with specific rules for which format each file can be and how ... #### Module format detection - `.mts`/`.mjs`/`.d.mts` files are always ES modules. - `.cts`/`.cjs`/`.d.cts` files are always CommonJS modules. - `.ts`/`.tsx`/`.js`/`.jsx`/`.d.ts` files are ES modules if the nearest ancestor package.json file contains`"type": ... The detected module format of input`.ts`/`.tsx`/`.mts`/`.cts` files determines the module format of the emitted JavaScript files. So, for example, a project consisting entirely of`.ts` files will emit all CommonJS modules by ... - The`module.exports` of the CommonJS module is available as a default import to the ES module. ... The emit format of each file is determined by the detected module format of each file. ESM emit is similar to`--module ... ### preserve In`--module preserve`(added in TypeScript 5.4), ECMAScript imports and exports written in input files are preserved in the output, and CommonJS-style`import x = require("...")` and`export = ...` statements are emitted as CommonJS`require` and`module.exports`. In other words, the format of each individual import or export statement is preserved, rather than being coerced into a single format for the whole compilation (or even a whole file). #### ... ts// `@Filename`: main.tsimpor...

Citations:


🏁 Script executed:

find . -name "onerror_page.ts" -type f

Repository: eggjs/egg

Length of output: 94


🏁 Script executed:

wc -l ./plugins/onerror/src/lib/onerror_page.ts

Repository: eggjs/egg

Length of output: 99


🏁 Script executed:

head -50 ./plugins/onerror/src/lib/onerror_page.ts

Repository: eggjs/egg

Length of output: 1766


🏁 Script executed:

grep -n '\$' ./plugins/onerror/src/lib/onerror_page.ts | head -20

Repository: eggjs/egg

Length of output: 1532


🏁 Script executed:

grep -E '\$\{' ./plugins/onerror/src/lib/onerror_page.ts | head -10

Repository: eggjs/egg

Length of output: 35


🏁 Script executed:

sed -n '620,635p' ./plugins/onerror/src/lib/onerror_page.ts

Repository: eggjs/egg

Length of output: 703


🏁 Script executed:

cat ./plugins/onerror/src/lib/onerror_page.ts | grep -c '{{' && echo "Mustache templates found (not JS interpolations)"

Repository: eggjs/egg

Length of output: 104


🏁 Script executed:

cat ./plugins/onerror/src/lib/onerror_page.ts | grep -E '\$\{.*\}' | head -5

Repository: eggjs/egg

Length of output: 35


🏁 Script executed:

cat ./plugins/onerror/src/lib/onerror_page.ts | sed -n '1,10p; 1330,1336p'

Repository: eggjs/egg

Length of output: 606


🏁 Script executed:

cd ./plugins/onerror && cat tsconfig.json 2>/dev/null | head -30

Repository: eggjs/egg

Length of output: 91


🏁 Script executed:

cat ./tsconfig.json | grep -A 5 -B 5 'isolatedDeclarations'

Repository: eggjs/egg

Length of output: 429


Add explicit string type annotation to prevent bloat in declaration emit.

This 1336-line template has no JavaScript interpolations (uses Mustache {{ }} syntax), so TypeScript will infer the literal string type, embedding the entire template into the generated .d.ts. With isolatedDeclarations: true enabled, explicit type annotations are required anyway.

✂️ Suggested change
-export const ONERROR_PAGE_TEMPLATE = `<!doctype html>
+export const ONERROR_PAGE_TEMPLATE: string = `<!doctype html>

Additionally, line 627 contains a hard-coded third-party asset URL (https://zos.alipayobjects.com/rmsportal/JFKAMfmPehWfhBPdCjrw.svg) that creates a network dependency in the built-in error page.

📝 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
export const ONERROR_PAGE_TEMPLATE = `<!doctype html>
export const ONERROR_PAGE_TEMPLATE: string = `<!doctype html>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@plugins/onerror/src/lib/onerror_page.ts` at line 2, The ONERROR_PAGE_TEMPLATE
constant is inferred as a huge string literal which bloats the .d.ts and fails
with isolatedDeclarations; add an explicit type annotation (export const
ONERROR_PAGE_TEMPLATE: string) to prevent embedding the entire template into
declaration output, and remove the hard-coded third-party asset URL found in the
template (the SVG at the URL around the image reference) by either inlining the
asset content or replacing it with a local/static asset reference so the error
page has no external network dependency; update the ONERROR_PAGE_TEMPLATE
definition accordingly.

<div class="context">{{ message }}</div>
</div>

<img class="error-logo" src="https://zos.alipayobjects.com/rmsportal/JFKAMfmPehWfhBPdCjrw.svg" />
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.

⚠️ Potential issue | 🟠 Major

Avoid fetching the default logo from a third party.

The built-in error page still pulls an image from zos.alipayobjects.com. That leaks debug-page traffic to an external domain and breaks offline or strict-CSP environments. Please inline the asset or ship it locally.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@plugins/onerror/src/lib/onerror_page.ts` at line 627, The error page
currently loads its logo from an external URL in the <img class="error-logo"
src="https://zos.alipayobjects.com/..."> tag; change this to a self-hosted asset
or inline SVG to avoid external network calls and CSP issues: locate the error
page template in onerror_page.ts (the HTML/JSX containing the img with class
"error-logo") and either replace the src with an imported local file (e.g.,
import logo from './logo.svg' and use <img src={logo} ...>) or paste the SVG
markup inline where the <img> is (keeping class="error-logo" and any sizing
attributes), and ensure any build tooling will bundle the asset.

Comment on lines +9 to +20
```
<outputDir>/
├── worker.js # main entry chunk produced from the synthetic worker.entry.ts
├── worker.js.map # sourcemap for the worker entry
├── _root-of-the-server___<hash>.js # module graph chunk (@utoo/pack)
├── _root-of-the-server___<hash>.js.map
├── _turbopack__runtime.js # @utoo/pack runtime shim
├── _turbopack__runtime.js.map
├── tsconfig.json # written by PackRunner; SWC reads decorator options from here
├── package.json # written by PackRunner; `{ "type": "commonjs" }` so node parses *.js as CJS
└── bundle-manifest.json # written by Bundler; reference / debug metadata
```
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.

⚠️ Potential issue | 🟡 Minor

Add a language to the layout fence.

This unlabeled fenced block trips markdownlint. text is enough here.

🧰 Tools
🪛 markdownlint-cli2 (0.22.0)

[warning] 9-9: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tools/egg-bundler/docs/output-structure.md` around lines 9 - 20, Add a
language identifier to the unlabeled fenced code block that starts with
"<outputDir>/" in output-structure.md by changing the opening fence from ``` to
```text so markdownlint stops flagging it; ensure only the fence is updated and
the block contents remain unchanged.

Comment on lines +43 to +78
const {
baseDir,
outputDir: rawOutputDir,
manifestPath,
framework = 'egg',
mode = 'production',
externals,
pack,
} = this.#config;

const absBaseDir = path.resolve(baseDir);
const absOutputDir = path.resolve(absBaseDir, rawOutputDir);
debug('bundle start: baseDir=%s outputDir=%s framework=%s mode=%s', absBaseDir, absOutputDir, framework, mode);

const manifestLoader = new ManifestLoader({
baseDir: absBaseDir,
manifestPath,
framework,
});
await wrapStep('manifest load', () => manifestLoader.load());

const externalsResolver = new ExternalsResolver({
baseDir: absBaseDir,
force: externals?.force,
inline: externals?.inline,
});
const externalsMap = await wrapStep('externals resolve', () => externalsResolver.resolve());
debug('externals resolved: %d packages', Object.keys(externalsMap).length);

const entryGen = new EntryGenerator({
baseDir: absBaseDir,
manifestLoader,
framework,
externals: new Set(Object.keys(externalsMap)),
});
const entries = await wrapStep('entry generation', () => entryGen.generate());
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.

⚠️ Potential issue | 🟠 Major

Thread config.tegg into entry generation; --no-tegg is a no-op right now.

tools/egg-bin/src/commands/bundle.ts passes tegg, but run() never reads it here, so every bundle still includes tegg-decorated files. That makes the new CLI flag ineffective.

💡 Minimal propagation fix
     const {
       baseDir,
       outputDir: rawOutputDir,
       manifestPath,
       framework = 'egg',
       mode = 'production',
+      tegg = true,
       externals,
       pack,
     } = this.#config;
@@
     const entryGen = new EntryGenerator({
       baseDir: absBaseDir,
       manifestLoader,
       framework,
       externals: new Set(Object.keys(externalsMap)),
+      tegg,
     });
📝 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
const {
baseDir,
outputDir: rawOutputDir,
manifestPath,
framework = 'egg',
mode = 'production',
externals,
pack,
} = this.#config;
const absBaseDir = path.resolve(baseDir);
const absOutputDir = path.resolve(absBaseDir, rawOutputDir);
debug('bundle start: baseDir=%s outputDir=%s framework=%s mode=%s', absBaseDir, absOutputDir, framework, mode);
const manifestLoader = new ManifestLoader({
baseDir: absBaseDir,
manifestPath,
framework,
});
await wrapStep('manifest load', () => manifestLoader.load());
const externalsResolver = new ExternalsResolver({
baseDir: absBaseDir,
force: externals?.force,
inline: externals?.inline,
});
const externalsMap = await wrapStep('externals resolve', () => externalsResolver.resolve());
debug('externals resolved: %d packages', Object.keys(externalsMap).length);
const entryGen = new EntryGenerator({
baseDir: absBaseDir,
manifestLoader,
framework,
externals: new Set(Object.keys(externalsMap)),
});
const entries = await wrapStep('entry generation', () => entryGen.generate());
const {
baseDir,
outputDir: rawOutputDir,
manifestPath,
framework = 'egg',
mode = 'production',
tegg = true,
externals,
pack,
} = this.#config;
const absBaseDir = path.resolve(baseDir);
const absOutputDir = path.resolve(absBaseDir, rawOutputDir);
debug('bundle start: baseDir=%s outputDir=%s framework=%s mode=%s', absBaseDir, absOutputDir, framework, mode);
const manifestLoader = new ManifestLoader({
baseDir: absBaseDir,
manifestPath,
framework,
});
await wrapStep('manifest load', () => manifestLoader.load());
const externalsResolver = new ExternalsResolver({
baseDir: absBaseDir,
force: externals?.force,
inline: externals?.inline,
});
const externalsMap = await wrapStep('externals resolve', () => externalsResolver.resolve());
debug('externals resolved: %d packages', Object.keys(externalsMap).length);
const entryGen = new EntryGenerator({
baseDir: absBaseDir,
manifestLoader,
framework,
externals: new Set(Object.keys(externalsMap)),
tegg,
});
const entries = await wrapStep('entry generation', () => entryGen.generate());
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tools/egg-bundler/src/lib/Bundler.ts` around lines 43 - 78, The bundler
currently ignores the config.tegg flag so the CLI's --no-tegg is a no-op;
extract tegg from this.#config alongside other options (e.g. const { ..., tegg =
true } = this.#config) and pass it into the EntryGenerator constructor (new
EntryGenerator({ baseDir: absBaseDir, manifestLoader, framework, externals: new
Set(...), tegg })) so EntryGenerator can skip tegg-decorated files when tegg is
false.

Comment on lines +141 to +155
#readFromDisk(): StartupManifest | undefined {
let raw: string;
try {
raw = fs.readFileSync(this.#manifestPath, 'utf-8');
} catch {
return undefined;
}
const parsed = JSON.parse(raw) as StartupManifest;
if (parsed.version !== SUPPORTED_MANIFEST_VERSION) {
throw new Error(
`[@eggjs/egg-bundler] manifest version mismatch at ${this.#manifestPath}: expected ${SUPPORTED_MANIFEST_VERSION}, got ${parsed.version}`,
);
}
return parsed;
}
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.

⚠️ Potential issue | 🟡 Minor

JSON parse error is not wrapped with context.

If JSON.parse fails on an invalid manifest file (corrupted JSON), the raw SyntaxError is thrown without indicating which file caused the issue. Consider wrapping it.

🛡️ Proposed fix to wrap JSON parse error
   `#readFromDisk`(): StartupManifest | undefined {
     let raw: string;
     try {
       raw = fs.readFileSync(this.#manifestPath, 'utf-8');
     } catch {
       return undefined;
     }
-    const parsed = JSON.parse(raw) as StartupManifest;
+    let parsed: StartupManifest;
+    try {
+      parsed = JSON.parse(raw) as StartupManifest;
+    } catch (err) {
+      throw new Error(`[`@eggjs/egg-bundler`] failed to parse manifest at ${this.#manifestPath}`, { cause: err });
+    }
     if (parsed.version !== SUPPORTED_MANIFEST_VERSION) {
       throw new Error(
         `[`@eggjs/egg-bundler`] manifest version mismatch at ${this.#manifestPath}: expected ${SUPPORTED_MANIFEST_VERSION}, got ${parsed.version}`,
       );
     }
     return parsed;
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tools/egg-bundler/src/lib/ManifestLoader.ts` around lines 141 - 155, The
JSON.parse in the private method `#readFromDisk` can throw a SyntaxError without
file context; wrap the parse in a try/catch and rethrow a new Error that
includes this.#manifestPath (and the original error message or use the error as
the cause) so callers know which manifest file failed to parse; keep the
existing version check (parsed.version vs SUPPORTED_MANIFEST_VERSION) and return
the parsed StartupManifest as before.

Comment on lines +109 to +115
it('inline removes a hard-coded @eggjs/* package from externals', async () => {
const result = await new ExternalsResolver({
baseDir: basicApp,
inline: ['@eggjs/some-plugin'],
}).resolve();
expect(result['@eggjs/some-plugin']).toBeUndefined();
});
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.

⚠️ Potential issue | 🟡 Minor

This test does not verify inline handling anymore.

@eggjs/* is already expected to be absent from the externals map by default, so this still passes even if inline is ignored completely. To cover override behavior, use a package that is externalized by default or seed the same package into the map first and then assert the precedence you want.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tools/egg-bundler/test/ExternalsResolver.test.ts` around lines 109 - 115, The
test currently passes because `@eggjs/`* packages are omitted by default; update
the test to actually verify inline precedence by either (a) using a package that
is externalized by default (e.g., replace '@eggjs/some-plugin' with a
known-external package) and asserting that result[package] becomes undefined
when inline includes it, or (b) seed the resolver's initial externals map to
include '@eggjs/some-plugin' (or the target package) before calling new
ExternalsResolver(...).resolve() and then assert that the seeded entry is
removed when inline contains that package; locate the logic around
ExternalsResolver, its constructor options (inline) and the resolve() result map
to implement this change.

Comment on lines +1 to +10
import { describe, it, expect } from 'vitest';

import { bundle, Bundler } from '../src/index.ts';

describe('@eggjs/egg-bundler', () => {
it('exposes a Bundler class and a bundle() helper', () => {
expect(typeof bundle).toBe('function');
expect(typeof Bundler).toBe('function');
expect(new Bundler({ baseDir: '/tmp', outputDir: '/tmp/out' })).toBeInstanceOf(Bundler);
});
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.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify assertion style in this test file
rg -n "import .*expect.*from 'vitest'|\\bexpect\\(" tools/egg-bundler/test/index.test.ts

Repository: eggjs/egg

Length of output: 289


Replace expect assertions with Node assert module per repository testing standards.

This test imports and uses expect from Vitest. Repository guidelines require using Node.js built-in assert/strict for all Vitest tests.

Fix
-import { describe, it, expect } from 'vitest';
+import assert from 'node:assert/strict';
+import { describe, it } from 'vitest';
@@
   it('exposes a Bundler class and a bundle() helper', () => {
-    expect(typeof bundle).toBe('function');
-    expect(typeof Bundler).toBe('function');
-    expect(new Bundler({ baseDir: '/tmp', outputDir: '/tmp/out' })).toBeInstanceOf(Bundler);
+    assert.equal(typeof bundle, 'function');
+    assert.equal(typeof Bundler, 'function');
+    assert.ok(new Bundler({ baseDir: '/tmp', outputDir: '/tmp/out' }) instanceof Bundler);
   });
📝 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
import { describe, it, expect } from 'vitest';
import { bundle, Bundler } from '../src/index.ts';
describe('@eggjs/egg-bundler', () => {
it('exposes a Bundler class and a bundle() helper', () => {
expect(typeof bundle).toBe('function');
expect(typeof Bundler).toBe('function');
expect(new Bundler({ baseDir: '/tmp', outputDir: '/tmp/out' })).toBeInstanceOf(Bundler);
});
import assert from 'node:assert/strict';
import { describe, it } from 'vitest';
import { bundle, Bundler } from '../src/index.ts';
describe('@eggjs/egg-bundler', () => {
it('exposes a Bundler class and a bundle() helper', () => {
assert.equal(typeof bundle, 'function');
assert.equal(typeof Bundler, 'function');
assert.ok(new Bundler({ baseDir: '/tmp', outputDir: '/tmp/out' }) instanceof Bundler);
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tools/egg-bundler/test/index.test.ts` around lines 1 - 10, Replace Vitest's
expect usage with Node's assert/strict: remove expect from the import line and
add import assert from 'assert/strict'; change the three assertions to use
assert.strictEqual(typeof bundle, 'function'), assert.strictEqual(typeof
Bundler, 'function'), and assert.ok(new Bundler({ baseDir: '/tmp', outputDir:
'/tmp/out' }) instanceof Bundler). Keep describe and it from 'vitest' unchanged
and reference the bundle and Bundler symbols in index.test.ts when making these
replacements.

Comment on lines +3 to +13
const config: UserConfig = defineConfig({
entry: 'src/**/*.ts',
unbundle: true,
fixedExtension: false,
external: [/^@eggjs\//, 'egg', '@utoo/pack', /\.node$/],
copy: [{ from: 'src/scripts/generate-manifest.mjs', to: 'dist/scripts/' }],
unused: {
level: 'warn',
ignore: ['@utoo/pack', 'egg'],
},
});
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.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🌐 Web query:

tsdown unused option configuration API

💡 Result:

In tsdown, the unused option in the configuration API enables checks for unused dependencies using unplugin-unused (requires installation of unplugin-unused). It is disabled by default (false). When enabled, configure it as an object of type WithEnabled with these properties: - depKinds?: array of DepKind[] (default: ['dependencies', 'peerDependencies']) - exclude?: FilterPattern - ignore?: string[] | Partial<Record<DepKind, string[]>> - include?: FilterPattern - level?: 'error' | 'warning' (default: 'warning') - root?: string Example configuration in tsdown.config.ts: import { defineConfig } from 'tsdown' export default defineConfig({ unused: { level: 'error', ignore: ['some-package'] } }) This detects unused dependencies in dependencies and peerDependencies, fails the build on errors, and ignores some-package. CLI equivalent: --unused --unused.level error --unused.ignore some-package. Requires unplugin-unused to be installed as a dev dependency.

Citations:


🏁 Script executed:

find . -name "tsdown.config.ts" -path "*/egg-bundler/*" | head -5

Repository: eggjs/egg

Length of output: 89


🏁 Script executed:

git ls-files | grep -i "egg-bundler" | grep "tsdown.config.ts"

Repository: eggjs/egg

Length of output: 87


🏁 Script executed:

cat -n tools/egg-bundler/tsdown.config.ts

Repository: eggjs/egg

Length of output: 559


Change level value from 'warn' to 'warning' in the unused configuration.

The level option in tsdown's unused configuration only accepts 'error' or 'warning', but line 10 uses the invalid literal 'warn'. This causes the TypeScript error TS2769: No overload matches this call.

🧰 Tools
🪛 GitHub Check: typecheck

[failure] 3-13: typescript(TS2769)
No overload matches this call.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tools/egg-bundler/tsdown.config.ts` around lines 3 - 13, The unused
configuration in the config object uses an invalid literal 'warn' for the level
field; update the unused.level value to 'warning' in the config defined by
defineConfig (the config constant) so it matches tsdown's allowed options
('error' or 'warning')—locate the unused block on the config object and replace
level: 'warn' with level: 'warning'.

*/
export function setSnapshotModuleLoader(loader: SnapshotModuleLoader): void {
_snapshotModuleLoader = loader;
(globalThis as any).__EGG_SNAPSHOT_MODULE_LOADER__ = loader;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

可以给 globalThis 扩展一个类型定义,这样就不需要靠 any 了

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