Skip to content

feat(egg-bundler): default to single-file (snapshot-eligible) bundle output - #5997

Merged
killagu merged 3 commits into
eggjs:nextfrom
killagu:egg-bundler-single-file
Jun 26, 2026
Merged

feat(egg-bundler): default to single-file (snapshot-eligible) bundle output#5997
killagu merged 3 commits into
eggjs:nextfrom
killagu:egg-bundler-single-file

Conversation

@killagu

@killagu killagu commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

Motivation

@eggjs/egg-bundler previously emitted @utoo/pack's standalone output: a tiny worker.js loader that does require("./_turbopack__runtime.js") and pulls in sibling chunks at runtime via R.c(...).

A V8 startup snapshot builder forbids user-land require of sibling chunks — the snapshot must be a single self-contained file. Since snapshot support is the direction egg-bundler is built for, this PR makes single-file output the default.

Scope

  • Single-file output is now the default. PackRunner sets two things on the @utoo/pack build config:

    • output.type: "export" (was "standalone")
    • each entry gets library: { name: "app" }

    With export + per-entry library, @utoo/pack inlines every module into one self-executing IIFE (((__UTOOPACK__)=>{...})([...modules])) — the emitted worker.js carries no sibling-chunk require and is snapshot-eligible. (A NAPI-RS loader helper index.*.js is normal.)

  • Opt-out preserved. Pass pack: { singleFile: false } (BundlerConfig.pack.singleFile) to fall back to the legacy multi-chunk standalone output.

Note: this package has no CLI bin today, so the toggle is the API option (pack.singleFile). A --bundle-mode=... flag can be added if/when a CLI lands.

Test evidence

  • test/PackRunner.test.ts: the default config now asserts export output with a per-entry library; a dedicated test asserts singleFile: false produces the legacy standalone output with no library.
  • test/single-file.realbuild.test.ts: a real @utoo/pack build through bundle() with no pack option (i.e. the default) asserts the output dir holds exactly one worker.js (no _turbopack__runtime.js sibling), worker.js matches neither _turbopack__runtime nor R.c(, node --check worker.js passes, and the inlined module runs.

Manual verification of the acceptance checks on a default bundle:

  • dist/ contains only worker.js (one worker*.js, no _turbopack__runtime.js)
  • grep -c "_turbopack__runtime\|R.c(" worker.js0
  • node --check worker.js → PASS, and direct execution prints the inlined module's value

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added an optional single-file build mode for worker bundles.
    • By default, bundles are emitted as a single self-contained worker.js suitable for V8 startup snapshots.
  • Bug Fixes

    • Updated worker packaging to pass through and apply the single-file setting correctly, ensuring the output is runnable.
  • Tests

    • Expanded unit tests to cover both default behavior and explicit singleFile: false.
    • Added a real-build regression test to validate the generated worker output and runtime behavior.

The default `standalone` output emits a tiny `worker.js` loader that does
`require("./_turbopack__runtime.js")` and pulls in sibling chunks via `R.c(...)`
at runtime. A V8 startup snapshot builder forbids that user-land require of
sibling chunks, so a single self-contained file is needed.

Add an opt-in single-file mode (default stays `standalone`):

- `PackRunner` gains a `singleFile` option. When enabled it switches the
  turbopack build to `output.type: "export"` and attaches `library: { name: "app" }`
  to every entry, so @utoo/pack inlines all modules into one self-executing IIFE
  (`((__UTOOPACK__)=>{...})([...modules])`) with no sibling-chunk require.
- Exposed through `BundlerConfig.pack.singleFile`, threaded by `Bundler`.

Verified by a real @utoo/pack build: the bundle emits a single `worker.js`
(no `_turbopack__runtime`/`R.c(`), passes `node --check`, and runs.

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

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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

@coderabbitai

coderabbitai Bot commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Adds a singleFile option to egg-bundler config, threads it into PackRunner, changes pack output mode and entry shape when enabled, and adds unit and real-build coverage for the resulting worker bundle.

Changes

Single-file bundler output

Layer / File(s) Summary
Config plumbing
tools/egg-bundler/src/index.ts, tools/egg-bundler/src/lib/Bundler.ts
Adds singleFile to BundlerPackConfig and passes mergedPack?.singleFile into PackRunner.
PackRunner output branching
tools/egg-bundler/src/lib/PackRunner.ts
Adds singleFile to PackRunnerOptions, defaults it to true, and switches pack output between standalone and export.
PackRunner tests
tools/egg-bundler/test/PackRunner.test.ts
Extends the test helper and adds assertions for default single-file output and explicit false output.
Single-file build regression
tools/egg-bundler/test/single-file.realbuild.test.ts
Adds a real build test that mocks loaders and generators, builds with default single-file output, and checks the emitted worker bundle.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Suggested reviewers

  • fengmk2
  • elrrrrrrr

Poem

A bunny hopped through build-land bright,
One worker file, all tucked in tight. 🐰
No sibling chunks to munch or hide,
Just export glow and rabbit pride.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly captures the main change: defaulting egg-bundler to snapshot-eligible single-file bundle output.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a singleFile option to the egg-bundler, enabling the generation of a single self-contained worker file with all modules inlined to support V8 startup snapshots. Feedback suggests simplifying the entry mapping configuration in PackRunner.ts by using object spreading to conditionally apply the library property, which reduces code duplication.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread tools/egg-bundler/src/lib/PackRunner.ts Outdated
Comment on lines +156 to +158
entry: singleFile
? entries.map((e) => ({ name: e.name, import: e.filepath, library: { name: 'app' } }))
: entries.map((e) => ({ name: e.name, import: e.filepath })),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

We can simplify the entry mapping to avoid duplicating the .map() call and the entry object structure. This improves maintainability and readability.

Suggested change
entry: singleFile
? entries.map((e) => ({ name: e.name, import: e.filepath, library: { name: 'app' } }))
: entries.map((e) => ({ name: e.name, import: e.filepath })),
entry: entries.map((e) => ({
name: e.name,
import: e.filepath,
...(singleFile ? { library: { name: 'app' } } : {}),
})),

@codecov

codecov Bot commented Jun 26, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 84.89%. Comparing base (753e045) to head (3cdfb12).
⚠️ Report is 1 commits behind head on next.

Additional details and impacted files
@@           Coverage Diff           @@
##             next    #5997   +/-   ##
=======================================
  Coverage   84.89%   84.89%           
=======================================
  Files         669      669           
  Lines       19942    19942           
  Branches     3964     3964           
=======================================
  Hits        16929    16929           
  Misses       2588     2588           
  Partials      425      425           

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
tools/egg-bundler/src/lib/PackRunner.ts (1)

156-158: 🚀 Performance & Scalability | 🔵 Trivial

Remove hardcoded library: { name: 'app' } to prevent global namespace collisions

Hardcoding the library name causes all entries in singleFile mode to overwrite one another on the global scope. @utoo/pack documentation confirms that library.name defines the global variable key for each bundle; distinct entries require distinct names to avoid collisions when exposed to window or globalThis.

♻️ Use the entry name as the library name
       entry: singleFile
-        ? entries.map((e) => ({ name: e.name, import: e.filepath, library: { name: 'app' } }))
+        ? entries.map((e) => ({ name: e.name, import: e.filepath, library: { name: e.name } }))
         : entries.map((e) => ({ name: e.name, import: e.filepath })),
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tools/egg-bundler/src/lib/PackRunner.ts` around lines 156 - 158, The
single-file entry mapping in PackRunner is hardcoding library.name to app, which
causes global collisions between bundled entries. Update the entries map used in
PackRunner so each bundle uses the entry’s own name for library.name instead of
a fixed value, keeping the singleFile path and the non-singleFile path aligned
with distinct names.

Source: Linters/SAST tools

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tools/egg-bundler/test/singleFile.realbuild.test.ts`:
- Line 1: The test file name uses camelCase instead of the repository’s
lowercase-with-hyphens convention. Rename the `singleFile.realbuild.test.ts`
test file to a hyphenated lowercase name such as
`single-file.realbuild.test.ts`, and update any references or imports that
depend on the old filename if applicable.

---

Nitpick comments:
In `@tools/egg-bundler/src/lib/PackRunner.ts`:
- Around line 156-158: The single-file entry mapping in PackRunner is hardcoding
library.name to app, which causes global collisions between bundled entries.
Update the entries map used in PackRunner so each bundle uses the entry’s own
name for library.name instead of a fixed value, keeping the singleFile path and
the non-singleFile path aligned with distinct names.
🪄 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: 85030aec-cbe0-47e4-b44d-efeb35923654

📥 Commits

Reviewing files that changed from the base of the PR and between 753e045 and d2c3007.

📒 Files selected for processing (5)
  • tools/egg-bundler/src/index.ts
  • tools/egg-bundler/src/lib/Bundler.ts
  • tools/egg-bundler/src/lib/PackRunner.ts
  • tools/egg-bundler/test/PackRunner.test.ts
  • tools/egg-bundler/test/singleFile.realbuild.test.ts

Comment thread tools/egg-bundler/test/single-file.realbuild.test.ts
- Collapse the `entry` ternary into a single map that spreads `library`
  conditionally (Gemini / CodeRabbit): avoids duplicating the entry-shape
  builder across the standalone/single-file branches.
- Rename test to `single-file.realbuild.test.ts` to follow the repo's
  lowercase-with-hyphens file naming convention (CodeRabbit).

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

killagu commented Jun 26, 2026

Copy link
Copy Markdown
Contributor Author

Addressed both review comments in ec708d6:

  • @gemini-code-assist / @coderabbitai (PackRunner.ts entry mapping): collapsed the entry ternary into a single entries.map(...) that spreads library conditionally, removing the duplicated entry-shape builder.
  • @coderabbitai (filename convention): renamed singleFile.realbuild.test.tssingle-file.realbuild.test.ts to follow the lowercase-with-hyphens rule (matches the sibling cjs-esm-interop.realbuild.test.ts).

Tests still green (18 passed).

@gemini-code-assist

Copy link
Copy Markdown
Contributor

Thanks for the update, @killagu. The changes look great and address the feedback effectively. Collapsing the entry mapping logic is much cleaner, and the filename adjustment for the test file ensures consistency with the existing naming conventions. Everything looks good to go.

@coderabbitai

coderabbitai Bot commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

@killagu Thanks — the cleanup in PackRunner.ts and the rename to single-file.realbuild.test.ts both sound good, and great to hear the test suite is still green with 18 passing.

🐇✨


If you found this review helpful, would you consider giving us a shout-out on X?

Thank you for using CodeRabbit!

✅ Action performed

Comments resolved. Approval is disabled; enable reviews.request_changes_workflow to allow explicit top-level @coderabbitai resolve or @coderabbitai approve commands.

Single-file (`output.type: "export"` + per-entry `library`) is now the
default output mode, since it is what V8 startup snapshots require (a
snapshot builder forbids the user-land `require` of sibling chunks that
the legacy standalone output relies on).

- PackRunner `singleFile` now defaults to `true`.
- Pass `pack: { singleFile: false }` to opt back into the legacy
  multi-chunk standalone output.
- Update docs and tests: the default config now asserts export output
  with a per-entry library; a dedicated test covers the standalone
  opt-out; the real-build test exercises the default (no explicit flag).

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

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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

@killagu killagu changed the title feat(egg-bundler): add single-file output mode for V8 snapshots feat(egg-bundler): default to single-file (snapshot-eligible) bundle output Jun 26, 2026
@killagu

killagu commented Jun 26, 2026

Copy link
Copy Markdown
Contributor Author

Review against PR1 (egg-bundler single-file output) — ✅ 符合,一处需确认的偏差

机制完全对齐 PR1:

  • PackRunneroutput.type: 'export' + 每个 entry library: { name: 'app' },正是 PR1 要求的两处改动。✅
  • 通过 API 选项 pack.singleFile 开关(PR1 允许 "或 Bundler config pack.singleFile";无 CLI bin 故暂不加 --bundle-mode,合理)。✅
  • 范围干净,只动输出类型 + 选项透传 + 测试,无 snapshot 逻辑,符合 PR1 边界。✅

验收点齐全(single-file.realbuild.test.ts 是真实 @utoo/pack build,仅 stub 了 ManifestLoader/ExternalsResolver/EntryGenerator):

  • 仅一个 worker.js(无 worker.<hash>.js / _turbopack__runtime.js 兄弟)✅
  • worker 不匹配 _turbopack__runtime / R.c((即 grep 计数为 0)✅
  • node --check 通过 ✅
  • 额外:直接执行产物、内联模块值可达(运行时证明)✅ — 比 PR1 验收更强。

一处与 PR1 描述的偏差,需 reviewer 自觉接受:
PR1 写的是"用选项 gate(默认仍 standalone),single-file opt-in"。本 PR 把 single-file 设为默认(opt-out pack.singleFile: false)。PR 描述给了理由(snapshot 是 egg-bundler 的方向)。这是合理设计,但它改变了所有现有消费者的产物形态(多 chunk → 单文件),不是纯新增。建议:要么确认接受这个默认翻转,要么在 changelog/迁移说明里显式标注此 breaking-ish 行为变化。

结论:作为 PR1 可合入,机制与测试均达标;唯一决策点是默认值翻转。

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
tools/egg-bundler/test/single-file.realbuild.test.ts (1)

88-107: 🩺 Stability & Availability | 🟠 Major

Ensure test execution environment meets the bundle's node 22 target requirement.

The bundled output targets Node 22 (confirmed in PackRunner.test.ts), but the test at line 106 executes the result using the ambient node binary. If a developer runs this on Node < 22, the test will fail due to runtime incompatibility rather than a bundler regression.

While CI runs on Node 24 (compatible), local environments may drift. Guard the test execution or add a clear requirement:

// tools/egg-bundler/test/single-file.realbuild.test.ts
import { version } from 'node:process';
const [major] = version.replace('v', '').split('.').map(Number);
if (major < 22) {
  // Skip test if the runner's Node version < target
  return;
}

Alternatively, update the test description to explicitly state the Node >= 22 requirement.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tools/egg-bundler/test/single-file.realbuild.test.ts` around lines 88 - 107,
The single-file bundler test is running the generated worker.js with the ambient
node binary without ensuring it matches the Node 22 runtime target. Update the
test in single-file.realbuild.test.ts around the bundle() execution so it either
skips/guards when process.version is below 22 or otherwise enforces a Node >= 22
runner, using the existing bundle test setup and assertions to keep the check
aligned with the PackRunner Node 22 target.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@tools/egg-bundler/test/single-file.realbuild.test.ts`:
- Around line 88-107: The single-file bundler test is running the generated
worker.js with the ambient node binary without ensuring it matches the Node 22
runtime target. Update the test in single-file.realbuild.test.ts around the
bundle() execution so it either skips/guards when process.version is below 22 or
otherwise enforces a Node >= 22 runner, using the existing bundle test setup and
assertions to keep the check aligned with the PackRunner Node 22 target.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 9d47d066-a60b-4fba-a3e6-4197659b42f3

📥 Commits

Reviewing files that changed from the base of the PR and between ec708d6 and 3cdfb12.

📒 Files selected for processing (4)
  • tools/egg-bundler/src/index.ts
  • tools/egg-bundler/src/lib/PackRunner.ts
  • tools/egg-bundler/test/PackRunner.test.ts
  • tools/egg-bundler/test/single-file.realbuild.test.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • tools/egg-bundler/src/lib/PackRunner.ts
  • tools/egg-bundler/src/index.ts

@killagu
killagu merged commit 003c456 into eggjs:next Jun 26, 2026
19 of 20 checks passed
killagu added a commit that referenced this pull request Jun 26, 2026
… command

Add the V8 startup snapshot entry-generation mechanism to @eggjs/egg-bundler
(building on the single-file output default from #5997) plus egg-bin commands.

- EntryGenerator emits a 3-mode worker entry driven by EGG_BUNDLE_SNAPSHOT:
  - normal: startEgg + listen (unchanged)
  - snapshot-build (EGG_BUNDLE_SNAPSHOT=build): startEgg({snapshot:true}),
    run snapshotWillSerialize hooks, register v8 setDeserializeMainFunction
  - restore main: setImmediate-deferred (ESM loader not ready at deserialize),
    installs require-based __EGG_MODULE_IMPORTER__/__RUNTIME_REQUIRE hooks so
    the egg loader avoids the missing dynamic import() callback, then resumes
    snapshotDidDeserialize and listens
- Add a snapshot prelude generator; Bundler prepends it before the bundle IIFE
  in snapshot mode (skeleton placeholder; PR3 fills the lazy/stub mechanism)
- BundlerConfig.snapshot forces single-file output and prelude prepend
- egg-bin: add `snapshot build` (bundle + node --build-snapshot) and
  `snapshot start` (node --snapshot-blob); spawn (not fork) to avoid an IPC
  handle that would break --build-snapshot
- Declare __RUNTIME_REQUIRE in @eggjs/typings global

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
killagu added a commit that referenced this pull request Jun 26, 2026
… command

Add the V8 startup snapshot entry-generation mechanism to @eggjs/egg-bundler
(building on the single-file output default from #5997) plus egg-bin commands.

- EntryGenerator emits a 3-mode worker entry driven by EGG_BUNDLE_SNAPSHOT:
  - normal: startEgg + listen (unchanged)
  - snapshot-build (EGG_BUNDLE_SNAPSHOT=build): startEgg({snapshot:true}),
    run snapshotWillSerialize hooks, register v8 setDeserializeMainFunction
  - restore main: setImmediate-deferred (ESM loader not ready at deserialize),
    installs require-based __EGG_MODULE_IMPORTER__/__RUNTIME_REQUIRE hooks so
    the egg loader avoids the missing dynamic import() callback, then resumes
    snapshotDidDeserialize and listens
- Add a snapshot prelude generator; Bundler prepends it before the bundle IIFE
  in snapshot mode (skeleton placeholder; PR3 fills the lazy/stub mechanism)
- BundlerConfig.snapshot forces single-file output and prelude prepend
- egg-bin: add `snapshot build` (bundle + node --build-snapshot) and
  `snapshot start` (node --snapshot-blob); spawn (not fork) to avoid an IPC
  handle that would break --build-snapshot
- Declare __RUNTIME_REQUIRE in @eggjs/typings global

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
killagu added a commit that referenced this pull request Jun 26, 2026
… command

Add the V8 startup snapshot entry-generation mechanism to @eggjs/egg-bundler
(building on the single-file output default from #5997) plus egg-bin commands.

- EntryGenerator emits a 3-mode worker entry driven by EGG_BUNDLE_SNAPSHOT:
  - normal: startEgg + listen (unchanged)
  - snapshot-build (EGG_BUNDLE_SNAPSHOT=build): startEgg({snapshot:true}),
    run snapshotWillSerialize hooks, register v8 setDeserializeMainFunction
  - restore main: setImmediate-deferred (ESM loader not ready at deserialize),
    installs require-based __EGG_MODULE_IMPORTER__/__RUNTIME_REQUIRE hooks so
    the egg loader avoids the missing dynamic import() callback, then resumes
    snapshotDidDeserialize and listens
- Add a snapshot prelude generator; Bundler prepends it before the bundle IIFE
  in snapshot mode (skeleton placeholder; PR3 fills the lazy/stub mechanism)
- BundlerConfig.snapshot forces single-file output and prelude prepend
- egg-bin: add `snapshot build` (bundle + node --build-snapshot) and
  `snapshot start` (node --snapshot-blob); spawn (not fork) to avoid an IPC
  handle that would break --build-snapshot
- Declare __RUNTIME_REQUIRE in @eggjs/typings global

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
killagu added a commit that referenced this pull request Jun 26, 2026
… command

Add the V8 startup snapshot entry-generation mechanism to @eggjs/egg-bundler
(building on the single-file output default from #5997) plus egg-bin commands.

- EntryGenerator emits a 3-mode worker entry driven by EGG_BUNDLE_SNAPSHOT:
  - normal: startEgg + listen (unchanged)
  - snapshot-build (EGG_BUNDLE_SNAPSHOT=build): startEgg({snapshot:true}),
    run snapshotWillSerialize hooks, register v8 setDeserializeMainFunction
  - restore main: setImmediate-deferred (ESM loader not ready at deserialize),
    installs require-based __EGG_MODULE_IMPORTER__/__RUNTIME_REQUIRE hooks so
    the egg loader avoids the missing dynamic import() callback, then resumes
    snapshotDidDeserialize and listens
- Add a snapshot prelude generator; Bundler prepends it before the bundle IIFE
  in snapshot mode (skeleton placeholder; PR3 fills the lazy/stub mechanism)
- BundlerConfig.snapshot forces single-file output and prelude prepend
- egg-bin: add `snapshot build` (bundle + node --build-snapshot) and
  `snapshot start` (node --snapshot-blob); spawn (not fork) to avoid an IPC
  handle that would break --build-snapshot
- Declare __RUNTIME_REQUIRE in @eggjs/typings global

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
killagu added a commit that referenced this pull request Jun 26, 2026
… command

Add the V8 startup snapshot entry-generation mechanism to @eggjs/egg-bundler
(building on the single-file output default from #5997) plus egg-bin commands.

- EntryGenerator emits a 3-mode worker entry driven by EGG_BUNDLE_SNAPSHOT:
  - normal: startEgg + listen (unchanged)
  - snapshot-build (EGG_BUNDLE_SNAPSHOT=build): startEgg({snapshot:true}),
    run snapshotWillSerialize hooks, register v8 setDeserializeMainFunction
  - restore main: setImmediate-deferred (ESM loader not ready at deserialize),
    installs require-based __EGG_MODULE_IMPORTER__/__RUNTIME_REQUIRE hooks so
    the egg loader avoids the missing dynamic import() callback, then resumes
    snapshotDidDeserialize and listens
- Add a snapshot prelude generator; Bundler prepends it before the bundle IIFE
  in snapshot mode (skeleton placeholder; PR3 fills the lazy/stub mechanism)
- BundlerConfig.snapshot forces single-file output and prelude prepend
- egg-bin: add `snapshot build` (bundle + node --build-snapshot) and
  `snapshot start` (node --snapshot-blob); spawn (not fork) to avoid an IPC
  handle that would break --build-snapshot
- Declare __RUNTIME_REQUIRE in @eggjs/typings global

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
killagu added a commit that referenced this pull request Jun 26, 2026
… command

Add the V8 startup snapshot entry-generation mechanism to @eggjs/egg-bundler
(building on the single-file output default from #5997) plus egg-bin commands.

- EntryGenerator emits a 3-mode worker entry driven by EGG_BUNDLE_SNAPSHOT:
  - normal: startEgg + listen (unchanged)
  - snapshot-build (EGG_BUNDLE_SNAPSHOT=build): startEgg({snapshot:true}),
    run snapshotWillSerialize hooks, register v8 setDeserializeMainFunction
  - restore main: setImmediate-deferred (ESM loader not ready at deserialize),
    installs require-based __EGG_MODULE_IMPORTER__/__RUNTIME_REQUIRE hooks so
    the egg loader avoids the missing dynamic import() callback, then resumes
    snapshotDidDeserialize and listens
- Add a snapshot prelude generator; Bundler prepends it before the bundle IIFE
  in snapshot mode (skeleton placeholder; PR3 fills the lazy/stub mechanism)
- BundlerConfig.snapshot forces single-file output and prelude prepend
- egg-bin: add `snapshot build` (bundle + node --build-snapshot) and
  `snapshot start` (node --snapshot-blob); spawn (not fork) to avoid an IPC
  handle that would break --build-snapshot
- Declare __RUNTIME_REQUIRE in @eggjs/typings global

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
killagu added a commit that referenced this pull request Jun 26, 2026
… command (#5998)

## Motivation

`@eggjs/egg-bundler` now defaults to single-file (snapshot-eligible)
output (#5997). This PR adds the mechanism that turns that bundle into a
**V8 startup snapshot**: a 3-mode generated entry, an auto-prepended
runtime prelude, an `egg-bin snapshot build` command, and `egg-scripts
start --snapshot-blob` to boot from the blob.

egg core already has the snapshot lifecycle
(`buildSnapshot`/`restoreSnapshot`,
`snapshotWillSerialize`/`snapshotDidDeserialize`, `snapshot:true`
stopping at `configWillLoad`). This wires egg-bundler + egg-bin +
egg-scripts to drive it.

## Scope

- **3-mode worker entry** (`EntryGenerator`), selected at runtime by
`EGG_BUNDLE_SNAPSHOT`:
  - **normal** — `startEgg` + `listen` (unchanged behavior).
- **snapshot-build** (`EGG_BUNDLE_SNAPSHOT=build`) — `startEgg({
snapshot: true })`, run `snapshotWillSerialize` hooks, then
`v8.startupSnapshot.setDeserializeMainFunction(...)`.
- **restore main** (inside the deserialize callback) — deferred via
`setImmediate` (the Node ESM loader isn't ready when the callback runs),
installs `require`-based `__EGG_MODULE_IMPORTER__` / `__RUNTIME_REQUIRE`
hooks (via `process.getBuiltinModule('node:module')`, with an
eval-require fallback for Node < 22.3) so the egg loader avoids the
missing dynamic `import()` callback, resumes `snapshotDidDeserialize`,
`listen`s, and emits an `egg-ready` IPC message for daemon readiness.
- **Snapshot prelude** (`prelude.ts`) — `Bundler` prepends it before the
bundle IIFE in snapshot mode so it runs before any module loads.
Skeleton/placeholder; PR3 fills the lazy / native-binding-stub
mechanism.
- **`BundlerConfig.snapshot`** — forces single-file output (even if
`pack.singleFile: false`) and triggers the prelude prepend.
- **`egg-bin snapshot build`** — bundle in snapshot mode, then `node
--snapshot-blob X --build-snapshot worker.js` (clean env so the ts-node
loader isn't applied to the bundle; verifies the blob exists). Uses
`spawn` (not `fork`) so no IPC handle breaks `--build-snapshot`.
- **`egg-scripts start --snapshot-blob <blob>`** — boots the single
self-contained snapshot process (no egg-cluster), reusing the existing
daemon / stdout-stderr / signal lifecycle. `egg-scripts stop` recognizes
snapshot processes by `--snapshot-blob` + `--title`.
- `__RUNTIME_REQUIRE` declared in `@eggjs/typings` global; shared
bundle-option helpers extracted to `bundleOptions.ts` (deduped from
`bundle.ts`).

> `snapshot start` lives in `@eggjs/scripts` (the production launcher),
not `egg-bin` (dev/build tooling): a deployed app that ships a blob has
egg-scripts available but usually not egg-bin.

## Boundary

This PR is the entry/prelude skeleton + commands. The lazy/stub
mechanism that converges non-serializable native bindings lands in PR3,
so a live `egg-scripts start --snapshot-blob` may still hit
native-binding errors after `setDeserializeMainFunction` until then. The
egg-scripts daemon/stop path is unit-tested at the spawn-arg /
process-match level; end-to-end daemon boot is verifiable once restore
works (PR3).

## Test evidence

- `tools/egg-bundler`: `prelude.test.ts`; `Bundler.test.ts`
snapshot-mode cases (forces single-file even with
`pack.singleFile:false`, prepends prelude, fails fast on missing
worker.js); `EntryGenerator.test.ts` 3-mode + egg-ready assertions +
regenerated canonical snapshot.
- `tools/egg-bin`: `snapshot.test.ts` (build bundles + spawns
`--build-snapshot` with clean env; `--skip-bundle`; custom `--blob`;
`--dry-run`; non-zero exit / missing-blob rejections);
`bundleOptions.test.ts`; existing `bundle.test.ts` green after the
helper extraction.
- `tools/scripts`: `snapshot-start.test.ts` (boots via `node
--snapshot-blob`, no cluster bin; `--port` → PORT env);
`snapshot-stop.test.ts` (stop matches snapshot processes by title).
- typecheck + oxlint clean on all three packages.

Pre-existing, unrelated to this PR: a few
`EntryGenerator`/`ManifestLoader` tests fail locally on macOS due to
`/var`→`/private/var` realpath (identical on the base branch).

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

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

## Summary by CodeRabbit

* **New Features**
* Added V8 startup-snapshot workflow: `snapshot build`, and snapshot
boot support for the start/stop flow.
* Implemented snapshot prelude injection for single-file worker bundles
and added a runtime require helper for snapshot restores.
* Exposed additional command/option entry points and shared bundle
options (mode/framework selection, `--pack-alias`).

* **Bug Fixes**
  * Fixed port handling so `PORT=0` is treated as an explicit value.
* Improved validation and error reporting when expected snapshot
artifacts are missing.

* **Tests**
* Added and expanded unit/integration tests for snapshot commands,
bundler/prelude/entry generation, and bundle options.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants