feat(bundler): enable @eggjs/* packages to be bundled by turbopack#5863
feat(bundler): enable @eggjs/* packages to be bundled by turbopack#5863killagu wants to merge 19 commits intoeggjs:nextfrom
Conversation
- `@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>
📝 WalkthroughWalkthroughThis PR introduces a complete application bundling system for EggJS via a new Changes
Sequence DiagramsequenceDiagram
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Codecov Report✅ All modified and coverable lines are covered by tests. 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. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
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" |
There was a problem hiding this comment.
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.
| "node": ">=22.18.0" | |
| "node": ">=20.0.0" |
There was a problem hiding this comment.
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 (
ManifestStorebundle store + module loaders) ontoglobalThisto 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-bundlertool +egg-bin bundlecommand, including externals resolution and a post-process patch for turbopackimport.metashims.
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 |
| 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'; |
| * 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; |
| 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); | ||
|
|
| 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'); |
| 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; | ||
| } | ||
|
|
| 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. |
There was a problem hiding this comment.
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.tsshould beloader-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_pagecements the underscore-based filename into the package surface. Please rename the module/path toonerror-pageand 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.
As per coding guidelines, "Document all exported functions, classes, and configuration properties with JSDoc comments".Proposed update
+/** + * Return default config for tegg-app fixture. + */ export default (): TeggAppConfig => { return { keys: 'tegg-app-keys', security: { csrf: { enable: false, }, }, }; };🤖 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
Applicationthroughunknown; consider a small typed extension once and guard in both hooks.As per coding guidelines, "Document all exported functions, classes, and configuration properties with JSDoc comments".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'); } }🤖 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.
As per coding guidelines, "Document all exported functions, classes, and configuration properties with JSDoc comments".Proposed update
+/** + * Return default config for empty-app fixture. + */ export default (): EmptyAppConfig => { return { keys: 'empty-app-keys', }; };🤖 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: Removeanycasts from global bundle-store access.Lines 66, 73, and 77 use
(globalThis as any), which weakens type safety in a core runtime path.As per coding guidelines, "Avoid 'any' type in TypeScript; use 'unknown' when type is truly unknown".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__;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 inALWAYS_EXTERNAL_NAMES(line 21), so line 65 already returnstruefor 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#isEsmOnlymethod.The static analysis correctly flags that
#isEsmOnlyis 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 confusingSyntaxError. 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: Usenode:assertfor the new Vitest suite.These new bundler tests use
expectthroughout, but the repo convention for Vitest tests is the built-inassertmodule. 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 modifiesargv,process.argv[1]might not point toworker.js. Consider documenting this assumption or providing a fallback.The comment explains why
__dirnamecannot be used, butprocess.argv[1]has its own edge cases. A possible alternative is to allow injecting__baseDirvia 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 addingtypesexports for library subpaths.The
publishConfig.exportsprovides JS entry points for subpaths (./lib/Bundler, etc.) but lacks corresponding.d.tstype 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: Redundantbundle()invocation for cause-chain verification.The test calls
bundle()twice with the same failingbuildFunc: once viaexpect(...).rejects.toThrowError()(Line 198-204), and again inside atry/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
⛔ Files ignored due to path filters (13)
packages/utils/test/__snapshots__/index.test.ts.snapis excluded by!**/*.snaptools/egg-bundler/test/__snapshots__/EntryGenerator.worker.canonical.snapis excluded by!**/*.snaptools/egg-bundler/test/fixtures/externals/basic-app/node_modules/@eggjs/some-plugin/package.jsonis excluded by!**/node_modules/**tools/egg-bundler/test/fixtures/externals/basic-app/node_modules/esm-dual/package.jsonis excluded by!**/node_modules/**tools/egg-bundler/test/fixtures/externals/basic-app/node_modules/esm-only/package.jsonis excluded by!**/node_modules/**tools/egg-bundler/test/fixtures/externals/basic-app/node_modules/native-binding/binding.gypis excluded by!**/node_modules/**tools/egg-bundler/test/fixtures/externals/basic-app/node_modules/native-binding/package.jsonis excluded by!**/node_modules/**tools/egg-bundler/test/fixtures/externals/basic-app/node_modules/native-dotnode/addon.nodeis excluded by!**/node_modules/**tools/egg-bundler/test/fixtures/externals/basic-app/node_modules/native-dotnode/package.jsonis excluded by!**/node_modules/**tools/egg-bundler/test/fixtures/externals/basic-app/node_modules/native-prebuilds/package.jsonis excluded by!**/node_modules/**tools/egg-bundler/test/fixtures/externals/basic-app/node_modules/native-prebuilds/prebuilds/placeholder.txtis excluded by!**/node_modules/**tools/egg-bundler/test/fixtures/externals/basic-app/node_modules/native-scripts/package.jsonis excluded by!**/node_modules/**tools/egg-bundler/test/fixtures/externals/basic-app/node_modules/normal-js/package.jsonis excluded by!**/node_modules/**
📒 Files selected for processing (63)
packages/core/src/loader/manifest.tspackages/egg/package.jsonpackages/utils/src/import.tspackages/utils/test/bundle-import.test.tsplugins/development/package.jsonplugins/development/src/app/middleware/egg_loader_trace.tsplugins/development/src/app/middleware/loader_trace_template.tsplugins/onerror/package.jsonplugins/onerror/src/app.tsplugins/onerror/src/config/config.default.tsplugins/onerror/src/lib/onerror_page.tsplugins/watcher/src/config/config.default.tspnpm-workspace.yamltools/egg-bin/package.jsontools/egg-bin/src/commands/bundle.tstools/egg-bin/src/index.tstools/egg-bundler/.gitignoretools/egg-bundler/docs/output-structure.mdtools/egg-bundler/package.jsontools/egg-bundler/src/index.tstools/egg-bundler/src/lib/Bundler.tstools/egg-bundler/src/lib/EntryGenerator.tstools/egg-bundler/src/lib/ExternalsResolver.tstools/egg-bundler/src/lib/ManifestLoader.tstools/egg-bundler/src/lib/PackRunner.tstools/egg-bundler/src/scripts/generate-manifest.mjstools/egg-bundler/test/EntryGenerator.test.tstools/egg-bundler/test/ExternalsResolver.test.tstools/egg-bundler/test/PackRunner.default-build.test.tstools/egg-bundler/test/PackRunner.test.tstools/egg-bundler/test/deterministic.test.tstools/egg-bundler/test/fixtures/apps/empty-app/app/public/.gitkeeptools/egg-bundler/test/fixtures/apps/empty-app/app/router.tstools/egg-bundler/test/fixtures/apps/empty-app/config/config.default.tstools/egg-bundler/test/fixtures/apps/empty-app/package.jsontools/egg-bundler/test/fixtures/apps/empty-app/tsconfig.jsontools/egg-bundler/test/fixtures/apps/minimal-app/app.tstools/egg-bundler/test/fixtures/apps/minimal-app/app/controller/home.tstools/egg-bundler/test/fixtures/apps/minimal-app/app/extend/context.tstools/egg-bundler/test/fixtures/apps/minimal-app/app/middleware/timing.tstools/egg-bundler/test/fixtures/apps/minimal-app/app/public/.gitkeeptools/egg-bundler/test/fixtures/apps/minimal-app/app/router.tstools/egg-bundler/test/fixtures/apps/minimal-app/app/service/user.tstools/egg-bundler/test/fixtures/apps/minimal-app/config/config.default.tstools/egg-bundler/test/fixtures/apps/minimal-app/package.jsontools/egg-bundler/test/fixtures/apps/minimal-app/tsconfig.jsontools/egg-bundler/test/fixtures/apps/tegg-app/config/config.default.tstools/egg-bundler/test/fixtures/apps/tegg-app/config/module.jsontools/egg-bundler/test/fixtures/apps/tegg-app/config/plugin.tstools/egg-bundler/test/fixtures/apps/tegg-app/modules/foo/FooController.tstools/egg-bundler/test/fixtures/apps/tegg-app/modules/foo/FooService.tstools/egg-bundler/test/fixtures/apps/tegg-app/modules/foo/package.jsontools/egg-bundler/test/fixtures/apps/tegg-app/package.jsontools/egg-bundler/test/fixtures/apps/tegg-app/tsconfig.jsontools/egg-bundler/test/fixtures/externals/basic-app/package.jsontools/egg-bundler/test/index.test.tstools/egg-bundler/test/integration.test.tstools/egg-bundler/test/no-filesystem-scan.test.tstools/egg-bundler/tsconfig.jsontools/egg-bundler/tsdown.config.tstools/egg-bundler/vitest.config.tstsconfig.jsontsdown.config.ts
| 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; | ||
| } |
There was a problem hiding this comment.
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)); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# First, locate and read the target file
cat -n plugins/development/src/app/middleware/egg_loader_trace.tsRepository: eggjs/egg
Length of output: 1624
🏁 Script executed:
# Read the template file
cat -n plugins/development/src/app/middleware/loader_trace_template.tsRepository: 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 -50Repository: 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 -100Repository: 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 10Repository: 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 -80Repository: 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.tsRepository: 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.tsRepository: eggjs/egg
Length of output: 1082
🏁 Script executed:
# Check sjs helper which is used by sjson
cat -n plugins/security/src/lib/helper/sjs.tsRepository: 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.tsRepository: 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 -30Repository: 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.
| 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> | |||
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
For TypeScript declaration emit, what .d.tsis produced forexport const x = `...`versusexport const x: string = `...`?
💡 Result:
Top web search findings:
@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 behaviorts // .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.jsproduces 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 weirdnamespacedeclaration 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 generateexport declare const b_ts: { foo: string }But b_js generates > > ```ts ... > This is the bug; the rest of theexport { foo_1 as foo }in the longer examples are renaming shenanigans that are ... > I'm pretty sure this is a result of `const x =- 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 Codets 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: stringActual behavior:ts export declare const strange = "no interpolated values" export declare const interpolated: stringI 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 labelQuestion· 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 - 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 thatimport { 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' // - 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 ### 💻 Codets // 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 stringas constbreaks 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 withoutas constshould not break string enums.sh npx -p typescript@5.5.3 tsc --noEmit -p tsconfig.verbatime-false.json // SHOULD NOT FAILts export const MyConst = 'MyConst' export const MyConst = 'MyConst' as constshould behave the same when used asts 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 formexport 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 - 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 an
exportmodifier, 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 eachmodulecompiler 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:
- 1:
@type {const}produces namespace in .d.ts instead of normal object type microsoft/TypeScript#56789 - 2: inconsistent string template exports in type interface files microsoft/TypeScript#38055
- 3: https://stackoverflow.com/questions/68625798/typescript-export-const-foo-vs-export-default-foo
- 4: string enum works from exported strings but not from strings exported as const microsoft/TypeScript#59187
- 5: https://www.typescriptlang.org/docs/handbook/modules/reference.html
🏁 Script executed:
find . -name "onerror_page.ts" -type fRepository: eggjs/egg
Length of output: 94
🏁 Script executed:
wc -l ./plugins/onerror/src/lib/onerror_page.tsRepository: eggjs/egg
Length of output: 99
🏁 Script executed:
head -50 ./plugins/onerror/src/lib/onerror_page.tsRepository: eggjs/egg
Length of output: 1766
🏁 Script executed:
grep -n '\$' ./plugins/onerror/src/lib/onerror_page.ts | head -20Repository: eggjs/egg
Length of output: 1532
🏁 Script executed:
grep -E '\$\{' ./plugins/onerror/src/lib/onerror_page.ts | head -10Repository: eggjs/egg
Length of output: 35
🏁 Script executed:
sed -n '620,635p' ./plugins/onerror/src/lib/onerror_page.tsRepository: 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 -5Repository: 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 -30Repository: 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.
| 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" /> |
There was a problem hiding this comment.
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.
| ``` | ||
| <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 | ||
| ``` |
There was a problem hiding this comment.
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.
| 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()); |
There was a problem hiding this comment.
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.
| 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.
| #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; | ||
| } |
There was a problem hiding this comment.
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.
| 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(); | ||
| }); |
There was a problem hiding this comment.
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.
| 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); | ||
| }); |
There was a problem hiding this comment.
🧩 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.tsRepository: 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.
| 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.
| 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'], | ||
| }, | ||
| }); |
There was a problem hiding this comment.
🧩 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:
- 1: https://tsdown.dev/reference/api/interface.unusedoptions
- 2: https://tsdown.dev/reference/api/Interface.UserConfig
- 3: configure unplugin-unused rolldown/tsdown#255
- 4: https://main.tsdown.dev/reference/api/Interface.UserConfig
🏁 Script executed:
find . -name "tsdown.config.ts" -path "*/egg-bundler/*" | head -5Repository: 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.tsRepository: 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; |
There was a problem hiding this comment.
可以给 globalThis 扩展一个类型定义,这样就不需要靠 any 了
Summary
globalThis存储状态,解决 bundled entry 和 external egg 框架之间的 module 实例分裂问题import.meta.dirname+readFileSync依赖@eggjs/*包import.meta.url/dirnamethrowing gettercnpmcore E2E 验证:externals 从 76 → 12,
curl http://127.0.0.1:7001/-/ping返回 HTTP 200。Changes
@eggjs/coreManifestStore.#bundleStore→globalThis.__EGG_BUNDLE_STORE__@eggjs/utils_bundleModuleLoader/_snapshotModuleLoader→ globalThis@eggjs/onerroronerror_page.ts,templatePath默认空字符串@eggjs/developmentloader_trace_template.ts@eggjs/watcherpath.join(import.meta.dirname, ...)改为 class import@eggjs/egg-bundler@eggjs/*auto-external 规则,新增#patchImportMetaUrlpost-processTest plan
pnpm --filter=@eggjs/core test— ManifestStore globalThispnpm --filter=@eggjs/utils test— module loaders globalThispnpm --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)🤖 Generated with Claude Code
Summary by CodeRabbit
Release Notes
New Features
bundlecommand) for creating optimized, self-contained application packages with integrated module resolution.Infrastructure Improvements