Skip to content

feat(snapshot): gate V8 snapshot restore to Node.js >= 24, add docs and cnpmcore e2e#6003

Merged
killagu merged 9 commits into
eggjs:nextfrom
killagu:worktree-snapshot-node-gate-docs-ci
Jun 28, 2026
Merged

feat(snapshot): gate V8 snapshot restore to Node.js >= 24, add docs and cnpmcore e2e#6003
killagu merged 9 commits into
eggjs:nextfrom
killagu:worktree-snapshot-node-gate-docs-ci

Conversation

@killagu

@killagu killagu commented Jun 27, 2026

Copy link
Copy Markdown
Contributor

Motivation

Restoring a V8 startup snapshot requires Node.js >= 24: Node.js 22 aborts during deserialization of a non-trivial Egg heap with the native fatal Check failed: current == end_slot_index (a V8 bug). Building a snapshot still works on Node.js >= 22. Today nothing enforces or documents this, and there is no regression coverage. This PR adds a runtime gate, documentation, and an e2e regression — without changing the snapshot mechanism itself.

Builds on the snapshot work already on next (#5998 entry/prelude + egg-bin snapshot command, #5999 lazy-external network stack, #6001 logger reopen, #6002 module-loader hooks).

Scope

Runtime gate (restore ≥ 24; build stays ≥ 22)

  • @eggjs/scripts: egg-scripts start --snapshot-blob refuses to launch on Node.js < 24 with a clear error before spawning, checking the major version of the resolved --node target binary (not just the egg-scripts runtime). Also adds allowNo: true to the sourcemap flag so --no-sourcemap is accepted.
  • @eggjs/egg-bundler: a defense-in-depth guard in the generated deserialize-main for direct node --snapshot-blob launches that manage to deserialize on an unsupported runtime.
  • @eggjs/bin: snapshot build prints a note that restoring needs Node.js >= 24.

Docs

  • Enrich site/docs/advanced/snapshot.md (EN + ZH): Node version requirements, the CLI workflow (egg-bin snapshot buildegg-scripts start --snapshot-blob), how it works (load module graph → run to configWillLoad → freeze; restore = didReady + listen), performance (~233ms vs ~942ms, ~4× on cnpmcore), and known limitations.
  • Wire the page into the VitePress sidebar (EN + ZH) — it existed but was unreachable.

CI

  • Add a blocking cnpmcore-snapshot ecosystem-ci e2e (Node 24): snapshot build → restore via egg-scripts start --snapshot-blobcurl /-/ping == 200 → stop. Wires repo.json, patch-project.ts, .gitignore.
  • Extract the shared health-check poll into ecosystem-ci/wait-health.sh, used by both the cnpmcore and cnpmcore-snapshot jobs.

Test evidence

  • pnpm --filter=@eggjs/scripts --filter=@eggjs/egg-bundler --filter=@eggjs/bin run typecheck — clean.
  • @eggjs/scripts snapshot-start.test.ts (4 tests, incl. a Node<24 gate test and a --no-sourcemap parse regression test) + start-unit.test.ts — pass.
  • @eggjs/bin snapshot.test.ts — pass.
  • @eggjs/egg-bundler EntryGenerator canonical snapshot regenerated for the new guard; the rest of the suite matches the pre-change baseline (a few pre-existing macOS/Node-22 path-resolution failures are unrelated).
  • A multi-agent diff review was run and all confirmed findings addressed (most notably: --no-sourcemap is now parseable via allowNo: true — without it the e2e job would have failed 100%).

Notes

  • The cnpmcore-snapshot job is correct-by-construction but could not be validated on the author's machine (Node 22, no MySQL+cnpmcore build); it relies on the supported path (lazy-external from feat(bundler): lazy-external network stack for V8 snapshots #5999, no manual stubs) and is validated by this PR's CI run.
  • The job is intentionally a separate matrix project (not folded into the existing cnpmcore job) for failure isolation.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added “V8 Startup Snapshot” documentation navigation.
    • Added support for snapshot project configuration (shared project-root patching).
  • Bug Fixes

    • Enforced Node.js version gating for V8 snapshot restore (requires Node.js ≥ 24) and improved snapshot restore safety.
    • Improved snapshot lazy-external behavior, including correct external named-export handling.
    • Updated the start command to allow --no-sourcemap.
  • Documentation

    • Expanded snapshot docs with requirements, workflow details, performance, and limitations.
  • Tests

    • Updated snapshot start/version-gating and lazy-external readiness assertions.
  • Chores

    • Improved E2E readiness checks with consistent polling, timeouts, and error log output.

…nd cnpmcore e2e

Restoring a V8 startup snapshot requires Node.js >= 24: Node.js 22 aborts during
deserialization of a non-trivial egg heap (V8 bug `Check failed: current ==
end_slot_index`). Building still works on Node.js >= 22.

- gate(scripts): egg-scripts `start --snapshot-blob` refuses Node.js < 24 before
  spawning, checking the resolved `--node` target binary's major version; add
  `allowNo: true` to the sourcemap flag so `--no-sourcemap` is accepted
- gate(egg-bundler): defense-in-depth guard in the generated deserialize-main
- gate(egg-bin): `snapshot build` notes the Node.js >= 24 restore requirement
- docs: enrich advanced/snapshot.md (EN + ZH) with version requirements, CLI
  workflow, principle, performance, limitations; wire into the sidebar
- ci: add a blocking cnpmcore-snapshot ecosystem-ci e2e (build snapshot -> restore
  via egg-scripts -> curl /-/ping == 200 on Node 24); share the health-check loop
  via ecosystem-ci/wait-health.sh

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

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 27, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

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

Use the following commands to manage reviews:

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

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds snapshot CI wiring, a shared health-wait script, Node.js 24 restore gates, bundler/runtime snapshot plumbing, worker entry generation, and expanded English and Chinese snapshot docs with sidebar links.

Changes

V8 startup snapshot rollout

Layer / File(s) Summary
Ecosystem CI root wiring
ecosystem-ci/repo.json, ecosystem-ci/patch-project.ts, .gitignore
Adds the cnpmcore-snapshot repository entry, patches project roots by project name, and ignores the generated snapshot workspace path.
Shared health polling in E2E
ecosystem-ci/wait-health.sh, .github/workflows/e2e-test.yml
Adds the polling script and rewires both cnpmcore E2E jobs to wait on HTTP health endpoints, stop the processes, and print failure logs.
Snapshot start gating and tests
tools/scripts/src/commands/start.ts, tools/scripts/test/snapshot-start.test.ts
Lets --sourcemap accept --no-sourcemap, resolves the target Node major version for --snapshot-blob, rejects restores below Node 24, and covers the new paths in tests.
Snapshot bundler runtime plumbing
tools/egg-bundler/src/lib/Bundler.ts, tools/egg-bundler/src/lib/PackRunner.ts, tools/egg-bundler/src/lib/prelude.ts, tools/egg-bundler/test/*
Passes external export names into the snapshot prelude, changes single-file externals handling, and updates the generated prelude and lazy-external tests for build-time export inspection and restore-time guards.
Snapshot build note
tools/egg-bin/src/commands/snapshot.ts
Adds a build-time log message that states the restored snapshot requires Node.js >= 24.
Snapshot worker entry generation
tools/egg-bundler/src/lib/EntryGenerator.ts
Adds decorated-file metadata repair, build-snapshot importer wiring, and the Node 24 restore guard in the generated worker entry.
Snapshot docs and navigation
site/.vitepress/config.mts, site/docs/advanced/snapshot.md, site/docs/zh-CN/advanced/snapshot.md
Adds the snapshot page to the Advanced sidebars and rewrites the English and Chinese snapshot docs with version requirements, workflow, lifecycle, performance, and limitation sections.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • eggjs/egg#5591: Shares the same tools/scripts/src/commands/start.ts command path and start-time option handling.
  • eggjs/egg#5739: Overlaps on the ecosystem-ci E2E harness and project-patching workflow used for cnpmcore-snapshot.
  • eggjs/egg#5999: Touches the same snapshot prelude and lazy-external bundler wiring changed here.

Suggested reviewers

  • jerryliang64

Poem

Hoppity hop, I shelled the run,
The health checks blinked, the work got done.
Snapshot seeds now wake with care,
And Node 24’s the key they wear. 🐇

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: gating snapshot restore to Node.js >= 24, plus docs and cnpmcore e2e updates.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

❤️ Share

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

@codecov

codecov Bot commented Jun 27, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 82.00%. Comparing base (5134d9c) to head (3b5dd96).

Additional details and impacted files
@@            Coverage Diff             @@
##             next    #6003      +/-   ##
==========================================
+ Coverage   81.99%   82.00%   +0.01%     
==========================================
  Files         676      676              
  Lines       20522    20534      +12     
  Branches     4060     4063       +3     
==========================================
+ Hits        16826    16839      +13     
+ Misses       3189     3187       -2     
- Partials      507      508       +1     

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

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

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This pull request introduces support for V8 Startup Snapshots, including CLI commands, documentation, and a new health check script. It enforces a Node.js version requirement of >= 24 for restoring snapshots. The review feedback highlights a potential TypeError in the test suite when mocking process.versions, suggests simplifying Node.js major version extraction using parseInt, and recommends adding an argument check in the new shell script to prevent crashes caused by unbound variables under set -u.

Important

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

Comment on lines +21 to +27
function pinNodeVersion(version: string): () => void {
const descriptor = Object.getOwnPropertyDescriptor(process.versions, 'node');
Object.defineProperty(process.versions, 'node', { value: version, configurable: true, writable: true });
return () => {
if (descriptor) Object.defineProperty(process.versions, 'node', descriptor);
};
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

In standard Node.js, process.versions is a frozen object, meaning that attempting to redefine properties on it directly via Object.defineProperty(process.versions, 'node', ...) will throw a TypeError in many environments. To mock the Node.js version safely and robustly, redefine the versions property on the process object itself instead.

function pinNodeVersion(version: string): () => void {
  const originalVersions = process.versions;
  Object.defineProperty(process, 'versions', {
    value: { ...originalVersions, node: version },
    configurable: true,
    writable: true,
  });
  return () => {
    Object.defineProperty(process, 'versions', {
      value: originalVersions,
      configurable: true,
      writable: true,
    });
  };
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done in a6417bbpinNodeVersion now redefines process.versions itself (a fresh object with the pinned node) rather than its node property, which is non-configurable in some runtimes.

Comment thread tools/scripts/src/commands/start.ts Outdated
*/
async #resolveSnapshotNodeMajor(command: string): Promise<number | undefined> {
if (command === 'node' || command === process.execPath) {
return Number(process.versions.node.split('.', 1)[0]);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

Using parseInt(process.versions.node, 10) is a simpler and more standard way to extract the major version of Node.js, avoiding unnecessary string splitting and array indexing.

Suggested change
return Number(process.versions.node.split('.', 1)[0]);
return parseInt(process.versions.node, 10);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done in a6417bb — switched to parseInt(process.versions.node, 10).

// (egg-scripts start --snapshot-blob) already gates this before spawning,
// so this is a defense-in-depth guard for a direct \`node --snapshot-blob\`
// launch that managed to deserialize on an unsupported runtime.
const __nodeMajor = Number(process.versions.node.split('.', 1)[0]);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

Using parseInt(process.versions.node, 10) is a simpler and more standard way to extract the major version of Node.js, avoiding unnecessary string splitting and array indexing.

Suggested change
const __nodeMajor = Number(process.versions.node.split('.', 1)[0]);
const __nodeMajor = parseInt(process.versions.node, 10);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done in a6417bb — the generated worker-entry restore guard now uses parseInt(process.versions.node, 10).

Comment on lines +12 to +13
URL="$1"
RESPONSE_FILE="$2"

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

Since set -u is enabled, running this script with fewer than 2 arguments will cause it to crash immediately with an unbound variable error. Adding an explicit argument count check provides a much friendlier and clearer usage error message.

Suggested change
URL="$1"
RESPONSE_FILE="$2"
if [ $# -lt 2 ]; then
echo "Usage: $0 <url> <response-file> [timeout-seconds] [sleep-seconds]" >&2
exit 1
fi
URL="$1"
RESPONSE_FILE="$2"

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done in a6417bb — added an explicit [ "$#" -lt 2 ] usage check with a friendly message before the set -u positional reads.

@socket-security

socket-security Bot commented Jun 27, 2026

Copy link
Copy Markdown

Dependency limit exceeded — report not shown.

This pull request scan exceeded the 10,000-dependency limit applied to this scan, so the results are incomplete and may be inaccurate. To avoid reporting false positives, Socket has not posted a report.

Upgrade your plan to raise the dependency limit and get complete reports, or view the partial scan in the dashboard.

Socket is always free for open source. If this is a non-commercial open source project, contact us to request a free Team account.

@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

🤖 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/scripts/src/commands/start.ts`:
- Around line 129-140: The `#resolveSnapshotNodeMajor` shortcut for bare node is
using process.versions.node, which can disagree with the binary actually
launched by spawn() when PATH is modified. Update this method to resolve the
command using this.env (or prefer the executable path resolution first) so the
version check matches the spawned binary, and keep the execFile fallback for
non-node commands.
🪄 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: 5d9c0a4c-2167-4fab-b9cd-5c92e5b61847

📥 Commits

Reviewing files that changed from the base of the PR and between 42f7a14 and 13a9738.

⛔ Files ignored due to path filters (1)
  • tools/egg-bundler/test/__snapshots__/EntryGenerator.worker.canonical.snap is excluded by !**/*.snap
📒 Files selected for processing (12)
  • .github/workflows/e2e-test.yml
  • .gitignore
  • ecosystem-ci/patch-project.ts
  • ecosystem-ci/repo.json
  • ecosystem-ci/wait-health.sh
  • site/.vitepress/config.mts
  • site/docs/advanced/snapshot.md
  • site/docs/zh-CN/advanced/snapshot.md
  • tools/egg-bin/src/commands/snapshot.ts
  • tools/egg-bundler/src/lib/EntryGenerator.ts
  • tools/scripts/src/commands/start.ts
  • tools/scripts/test/snapshot-start.test.ts

Comment thread tools/scripts/src/commands/start.ts
# V8 启动快照

Egg 内置了 V8 startup snapshot 的构建与恢复能力。对于需要在启动前预加载框架元数据、插件、Service、Router 和 tegg 模块的应用,这可以减少冷启动阶段的开销。
Egg 可以把一个完全加载好的应用固化成 [V8 启动快照](https://nodejs.org/api/cli.html#--build-snapshot),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

太厉害了,这个思路是不是可以用于 cli?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

cli 不太好搞,因为 cli 运行的 node 版本是不确定的。除非像 electron 那样把二进制一起打进去。

killagu and others added 4 commits June 27, 2026 23:42
Strengthen the V8-snapshot lazy-external mechanism so a real-world app
(loading many ESM/CJS deps) builds through the egg loader to V8 serialization:

- PackRunner: single-file externals use ExternalType `commonjs` (a direct
  require, surfaced as externalRequire) instead of the UMD form, which inside
  the single-file IIFE has no CommonJS module/exports and falls through to
  globalThis[name] = undefined, breaking every external.
- prelude: __makeLazyExt is now a member proxy that records the build-time
  access path (get/apply/construct + args) and replays it against the real
  module on restore, so `class X extends pkg.Klass` and
  `DataTypes.INTEGER(11).UNSIGNED` keep working. Web globals become no-op stub
  classes (constructable, for `class extends globalThis.Request`); node:buffer
  File/Blob are stubbed; http constants are read from the build Node instead of
  hand-written tables.
- prelude: the lazy strategy externalizes blacklisted network builtins (now
  incl. inspector, which egg core imports) OR any non-builtin package; builtin
  tool modules (path/fs/module) stay real.
- EntryGenerator: install __EGG_MODULE_IMPORTER__ for the build phase too, so
  the loader routes config/app imports through require() (dynamic import is
  unavailable under --build-snapshot) and gracefully skips a manifest-missing
  ESM config.
- Bundler: read each external's export names (in the bundler process) and inject
  __EXTERNAL_EXPORTS so the member proxy presents a full ESM namespace for
  `import { X } from 'pkg'`.

Tests updated for the new prelude shape and build-importer entry; the realbuild
test verifies build-time stub + restore-time real module via the member proxy.

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

A plain Object.defineProperty over Node's lazy web globals (specifically
`Headers`) makes Node eagerly initialize undici → http/http2 native bindings,
which a V8 startup snapshot cannot serialize (CheckGlobalAndEternalHandles
fatal). This was the root cause of snapshot build failing on a real-world tegg
app (cnpmcore): the prelude's OWN web-global stubbing triggered the very undici
load it was meant to prevent — not any application dependency.

Fix: two passes — delete each global first (removes Node's lazy getter WITHOUT
triggering it), then install the no-op stub class as a plain data property (now
nothing to trigger, and still constructable for `class extends globalThis.Request`).

With this, cnpmcore's snapshot builds to a ~63MB blob (http2 residual gone) and
deserializes on Node 24; no extra force-externals are needed beyond the genuine
non-bundlable packages.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
End-to-end fixes found by driving a real tegg + ORM app (cnpmcore) through
snapshot build → restore → /-/ping:

EntryGenerator — decorator filePath correction:
  tegg decorators capture a class's source path from the call stack at module
  eval (StackUtil.getCalleeFromStack at a hardcoded depth). In a bundle every
  user frame collapses onto worker.js + turbopack wrappers, so a decorator using
  a deeper index than the norm (notably @advice's depth-5 vs @SingletonProto's
  depth-4) captures "…/worker.js" instead of its source file. tegg then can't
  match that proto to its load unit and fails at restore with
  "Aop Advice(AsyncTimer) not found in loadUnits". Re-stamp the correct path
  (outputDir + relKey) on every decorated export from the manifest's tegg
  decoratedFiles, at build, so the snapshot carries correct paths.

prelude — member-proxy replay correctness (the lazy-external mechanism):
  - Reflect.apply/construct instead of v.apply(...): when an earlier step resolves
    to a callable proxy, typeof is 'function' but v.apply is undefined, throwing
    "v.apply is not a function" (leoric's createType introspecting a DataType).
  - object proxy target for call/construct RESULTS so `typeof member` is 'object'
    (an instance), matching libraries that branch on typeof (leoric: function ⇒
    DataType class, object ⇒ instance).
  - resolve member-proxy ARGS before applying: a captured arg can itself be a lazy
    export (e.g. `DataTypes.TEXT(LENGTH_VARIANTS.long)`); passing the proxy through
    makes the callee coerce it to a string and throw. Resolve via the __MR symbol.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Drive the cnpmcore V8-snapshot regression all the way to a served /-/ping:

- run under EGG_SERVER_ENV=prod (loads only config.default, DB from
  CNPMCORE_DATABASE_* env vars) instead of unittest, so the snapshot stays
  production-like and does not bake in the @eggjs/mock test plugin.
- set CNPMCORE_FORCE_LOCAL_FS=true so cnpmcore's NFSClientAdapter accepts the
  local-fs store under prod (the e2e only needs a working /-/ping, not OSS/S3).
- delete the .ts sources after tsc:prod + overlay so the bundler scans only .js;
  otherwise tegg globby picks up .ts AND .js of the same class as two modules
  ("duplicate proto"), which breaks decorated-class filePath correction on
  restore.
- drop the redundant `--force-external leoric` (auto-externalized via its native
  optional DB drivers) and keep undici/urllib external alongside
  @cnpmjs/packument and koa-onerror.

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

@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: 4

🤖 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/src/lib/EntryGenerator.ts`:
- Around line 440-448: The catch block in EntryGenerator’s __buildReq(fp)
wrapper is swallowing all MODULE_NOT_FOUND errors, which hides real
missing-dependency failures from nested imports. Update the try/catch around
__buildReq in EntryGenerator so it only returns undefined for the direct
missing-file case for fp itself, and rethrows MODULE_NOT_FOUND coming from
inside a config/module load. Resolve fp first or otherwise distinguish the
top-level file-not-found path from nested dependency resolution before
suppressing the error.
- Around line 435-437: The snapshot importer setup in EntryGenerator currently
calls process.getBuiltinModule directly, which breaks on Node 22.0–22.2. Update
the __EGG_MODULE_IMPORTER__ setup to use the same Node <22.3 fallback approach
as the restore branch, while keeping the createRequire base rooted at
__outputDir, so the fallback path works consistently with the existing restore
logic.

In `@tools/egg-bundler/src/lib/prelude.ts`:
- Around line 166-185: The snapshot restore path does not re-expose the real web
globals that `prelude.ts` stubs out, so live code can inherit the no-op
`WebGlobalStub` values after restore. Update the deserialize/restore flow that
currently installs `__RUNTIME_REQUIRE` to also restore the original
`globalThis.fetch`/`Headers`/`Request`/`Response`/`File`/`Blob` and
`process.getBuiltinModule('node:buffer').File`/`Blob`, using the existing
prelude restore mechanism or a new explicit restore hook. Add an e2e check
around the restore entrypoint to verify `globalThis.fetch` and
`process.getBuiltinModule('node:buffer').File` are real after restore.
- Around line 381-389: The export collection in collect only uses Object.keys,
so it can miss non-enumerable own properties and function-valued default exports
from external modules. Update collect to inspect own property names on both the
module object and its default export (while keeping the existing Set dedupe),
and make sure the merge logic still covers the CJS-via-ESM case referenced by
the default handling in prelude.ts.
🪄 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: 18aa3c9b-9f8f-418a-baa1-6194814fbf91

📥 Commits

Reviewing files that changed from the base of the PR and between 13a9738 and 73c76a2.

⛔ Files ignored due to path filters (1)
  • tools/egg-bundler/test/__snapshots__/EntryGenerator.worker.canonical.snap is excluded by !**/*.snap
📒 Files selected for processing (9)
  • .github/workflows/e2e-test.yml
  • tools/egg-bundler/src/lib/Bundler.ts
  • tools/egg-bundler/src/lib/EntryGenerator.ts
  • tools/egg-bundler/src/lib/PackRunner.ts
  • tools/egg-bundler/src/lib/prelude.ts
  • tools/egg-bundler/test/PackRunner.test.ts
  • tools/egg-bundler/test/snapshot-lazy-bundler.test.ts
  • tools/egg-bundler/test/snapshot-lazy-external.test.ts
  • tools/egg-bundler/test/snapshot-lazy.realbuild.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • .github/workflows/e2e-test.yml

Comment thread tools/egg-bundler/src/lib/EntryGenerator.ts Outdated
Comment thread tools/egg-bundler/src/lib/EntryGenerator.ts
Comment thread tools/egg-bundler/src/lib/prelude.ts
Comment thread tools/egg-bundler/src/lib/prelude.ts Outdated
- start.ts / EntryGenerator worker entry: use parseInt(process.versions.node, 10)
  to read the Node major instead of split('.')[0] (gemini-code-assist).
- snapshot-start.test pinNodeVersion: redefine process.versions itself rather
  than its `node` property, which is non-configurable in some runtimes and would
  throw on a direct defineProperty (gemini-code-assist).
- wait-health.sh: under `set -u`, fail with a clear usage message when called
  with fewer than 2 args instead of an unbound-variable crash (gemini-code-assist).

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

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

♻️ Duplicate comments (1)
tools/scripts/src/commands/start.ts (1)

129-135: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Probe the same node binary that spawn() will launch.

spawn(command, ..., { env: this.env }) resolves command against the rewritten this.env.PATH, but this helper still reads process.versions.node for bare node and probes custom commands with the parent env. If .node/bin / node_modules/.bin shadows node, or --node is a PATH-resolved alias, the gate can validate one binary and launch another, so unsupported Node 22 restores still slip through to the fatal V8 crash this guard is meant to avoid. Limit the shortcut to process.execPath and resolve/probe everything else with this.env.

🔧 Minimal fix
   async `#resolveSnapshotNodeMajor`(command: string): Promise<number | undefined> {
-    if (command === 'node' || command === process.execPath) {
+    if (command === process.execPath) {
       return parseInt(process.versions.node, 10);
     }
     try {
-      const { stdout } = await execFile(command, ['--version']);
+      const { stdout } = await execFile(command, ['--version'], { env: this.env });
       const match = /^v?(\d+)\./.exec(stdout.toString().trim());
       return match ? Number(match[1]) : undefined;
     } catch {
#!/bin/bash
set -euo pipefail

echo "=== PATH rewrite, version probe, and spawn env ==="
sed -n '121,141p;178,188p;264,293p;331,346p' tools/scripts/src/commands/start.ts

echo
echo "=== Snapshot-start tests touching the gate ==="
sed -n '1,220p' tools/scripts/test/snapshot-start.test.ts
🤖 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/scripts/src/commands/start.ts` around lines 129 - 135, The snapshot
version check in resolveSnapshotNodeMajor is probing the wrong Node binary
because it shortcuts bare node to process.versions.node and uses the parent
environment for execFile. Update this helper so only process.execPath uses the
current process version, and resolve/probe all other commands with this.env
(matching the spawn(command, ..., { env: this.env }) path) so the gate checks
the same binary that will actually launch.
🤖 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.

Duplicate comments:
In `@tools/scripts/src/commands/start.ts`:
- Around line 129-135: The snapshot version check in resolveSnapshotNodeMajor is
probing the wrong Node binary because it shortcuts bare node to
process.versions.node and uses the parent environment for execFile. Update this
helper so only process.execPath uses the current process version, and
resolve/probe all other commands with this.env (matching the spawn(command, ...,
{ env: this.env }) path) so the gate checks the same binary that will actually
launch.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 2d7b7722-3268-476c-84cd-9bc738d6297b

📥 Commits

Reviewing files that changed from the base of the PR and between 73c76a2 and a6417bb.

⛔ Files ignored due to path filters (1)
  • tools/egg-bundler/test/__snapshots__/EntryGenerator.worker.canonical.snap is excluded by !**/*.snap
📒 Files selected for processing (4)
  • ecosystem-ci/wait-health.sh
  • tools/egg-bundler/src/lib/EntryGenerator.ts
  • tools/scripts/src/commands/start.ts
  • tools/scripts/test/snapshot-start.test.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • ecosystem-ci/wait-health.sh
  • tools/scripts/test/snapshot-start.test.ts
  • tools/egg-bundler/src/lib/EntryGenerator.ts

killagu and others added 2 commits June 28, 2026 02:04
- build module importer (EntryGenerator): fall back to an eval'd `require` when
  process.getBuiltinModule is unavailable (Node 22.0–22.2), matching the restore
  branch; and resolve `fp` first so only a genuine "this file is missing" is
  skipped — a nested MODULE_NOT_FOUND from a real missing dependency now
  propagates instead of being silently swallowed (coderabbit).
- readExternalExports: collect names with Object.getOwnPropertyNames (not
  Object.keys) so non-enumerable named exports are included, matching the
  getOwnPropertyNames(raw) that @utoo/pack's interopEsm enumerates (coderabbit).
- docs(snapshot): document that the undici-backed web globals (fetch/Headers/…,
  Blob/File) remain no-op stubs in the restored process — Node's native lazy
  getters are not snapshot-serializable, so they can't be reinstated; apps should
  use a lazy-external HTTP client (urllib/undici) rather than globalThis.fetch.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds two snapshot-start cases exercising `#resolveSnapshotNodeMajor`'s execFile
path (a custom `--node /path`, which the gate version-checks via `<path> --version`
rather than process.versions): one where the binary reports an unsupported version
(< 24 → blocks before spawn) and one where the query fails (gate fails open and the
launch proceeds). Covers the previously-untested execFile/match/catch lines.

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

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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

@killagu killagu merged commit e086329 into eggjs:next Jun 28, 2026
22 checks passed
killagu added a commit that referenced this pull request Jun 28, 2026
## Motivation

Building a V8 startup snapshot serializes the whole heap, so any
dependency that
opens a socket, starts a timer, or initializes a native binding at
module-evaluation time can make the blob fail to build — or build and
then crash
on restore. There was no guide for finding which module is responsible
or how to
fix it.

## What

New dedicated page `advanced/snapshot-troubleshooting.md` (EN + zh-CN):

- **The serializability rule** — what cannot survive the round-trip
(native
bindings / libuv handles / lazy web-global getters) and when a
dependency
  trips it.
- **Failure surfaces** — build-time vs restore-time, with the exact
error
  strings each emits (`killed by signal SIGSEGV`, `no blob was written`,
  `Check failed: current == end_slot_index`, `Aop Advice not found`,
  `Cannot find module`, …).
- **Find the offending module** — `NODE_DEBUG` namespaces, a clean
`NODE_OPTIONS`, `--dry-run`, `--skip-bundle` bisecting,
`--force-external`
  confirmation.
- **Fixes** — `--force-external`, `egg.snapshot.lazyModules`, the
snapshot
lifecycle hooks, deferring work out of module scope, avoiding the web
globals.
- **Failure modes in detail** (tegg `@Advice` filePath, the
lazy-external member
  proxy, runtime-asset `ENOENT`) and a configuration reference table.

Also documents the previously-undocumented `egg.snapshot.lazyModules`
config in
`advanced/snapshot.md`, cross-links the new page from it, and wires the
page into
the English and Chinese advanced sidebars.

Docs-only; no runtime code change. Builds on the snapshot feature
already on
`next` (#5998 / #5999 / #6001 / #6003).

## Test evidence

- `vitepress build site` passes clean — VitePress's dead-link/anchor
check is
green, so both new pages render and every cross-file and in-page anchor
resolves (the two detail headings use explicit ASCII `{#…}` ids because
the
  VitePress slugifier retains CJK + fullwidth `「」`).
- Every technical claim was adversarially verified against the
`egg-bundler` / `egg-bin` / `egg-scripts` source — exact error strings,
flag
  names, debug namespaces, and the default lazy-module set.

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


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

## Summary by CodeRabbit

* **Documentation**
* Added a new Snapshot Troubleshooting guide in English and Chinese,
covering common build and restore failure symptoms, how to diagnose
them, and recommended fixes.
* Expanded the Snapshot guide with guidance on configuring additional
lazy-loaded modules, plus clearer troubleshooting links.
* Updated the sidebar navigation to include the new troubleshooting page
in both languages.

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

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
killagu added a commit to killagu/egg that referenced this pull request Jun 28, 2026
Add `undici` and `urllib` to DEFAULT_SNAPSHOT_LAZY_MODULES so an app gets a
serializable V8 startup snapshot without listing them in
`egg.snapshot.lazyModules`. Egg builds its HttpClient (urllib -> undici) during
boot, and undici instantiates an llhttp WebAssembly module (disabled under
--build-snapshot) + an HTTPParser that cannot be snapshot-serialized.

As npm packages, urllib/undici would otherwise be inlined into the bundle and
evaluated at build time; listing them here forces them external so the prelude's
member-proxy stub is used at build and the real module is required on restore.
The member-proxy (eggjs#6003) already replays the recorded access path, so egg's
`class HttpClient extends urllib.HttpClient` keeps working across the
build->restore boundary.

Adds a unit assertion for the default list and a real @utoo/pack build test that
exercises `class Sub extends pkg.Base` for a forced-external npm package across
the build-stub / restore-real boundary (upstream only covered the node:http
builtin).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
killagu added a commit to killagu/egg that referenced this pull request Jun 28, 2026
Add `undici` and `urllib` to DEFAULT_SNAPSHOT_LAZY_MODULES so an app gets a
serializable V8 startup snapshot without listing them in
`egg.snapshot.lazyModules`. Egg builds its HttpClient (urllib -> undici) during
boot, and undici instantiates an llhttp WebAssembly module (disabled under
--build-snapshot) + an HTTPParser that cannot be snapshot-serialized.

As npm packages, urllib/undici would otherwise be inlined into the bundle and
evaluated at build time; listing them here forces them external so the prelude's
member-proxy stub is used at build and the real module is required on restore.
The member-proxy (eggjs#6003) already replays the recorded access path, so egg's
`class HttpClient extends urllib.HttpClient` keeps working across the
build->restore boundary.

Adds a unit assertion for the default list and a real @utoo/pack build test that
exercises `class Sub extends pkg.Base` for a forced-external npm package across
the build-stub / restore-real boundary (upstream only covered the node:http
builtin).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

killagu commented Jun 28, 2026

Copy link
Copy Markdown
Contributor Author

Additional cnpmcore startup benchmark data, measured locally with readiness defined as the first HTTP 200 from /-/ping after process spawn.

Setup:

  • Node.js: v24.18.0
  • MySQL/Redis: local Docker services
  • Database: same initialized schema, cnpmcore_startup_bench
  • Workers: 1
  • 3 runs per mode
  • CNPMCORE_FORCE_LOCAL_FS=true
  • CNPMCORE_CONFIG_REGISTRY pointed at each local benchmark port
Boot mode 3 runs Median Relative to source dev
Source/dev boot: egg-bin dev 7674 / 6174 / 5878 ms 6174 ms 1x
Non-snapshot bundle: node dist-bundle/worker.js 1319 / 1044 / 1038 ms 1044 ms ~5.9x faster
V8 snapshot blob: node --snapshot-blob=snapshot.blob worker.js 370 / 370 / 372 ms 370 ms ~16.7x faster

Summary: on cnpmcore, V8 startup snapshot restore reaches healthy-ready in about 0.37s, around 16-17x faster than source/dev boot and about 2.8x faster than the non-snapshot bundle worker.

Node version caveat confirmed locally as well: the blob built with Node 24.18.0 fails to restore on Node 22.22.3 with Failed to load the startup snapshot because it was built with Node.js version 24.18.0..., so restore must use the matching/supported Node 24 runtime.

killagu added a commit to killagu/egg that referenced this pull request Jun 28, 2026
Add `undici` and `urllib` to DEFAULT_SNAPSHOT_LAZY_MODULES so an app gets a
serializable V8 startup snapshot without listing them in
`egg.snapshot.lazyModules`. Egg builds its HttpClient (urllib -> undici) during
boot, and undici instantiates an llhttp WebAssembly module (disabled under
--build-snapshot) + an HTTPParser that cannot be snapshot-serialized.

As npm packages, urllib/undici would otherwise be inlined into the bundle and
evaluated at build time; listing them here forces them external so the prelude's
member-proxy stub is used at build and the real module is required on restore.
The member-proxy (eggjs#6003) already replays the recorded access path, so egg's
`class HttpClient extends urllib.HttpClient` keeps working across the
build->restore boundary.

Adds a unit assertion for the default list and a real @utoo/pack build test that
exercises `class Sub extends pkg.Base` for a forced-external npm package across
the build-stub / restore-real boundary (upstream only covered the node:http
builtin).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
killagu added a commit to killagu/egg that referenced this pull request Jun 28, 2026
Add `undici` and `urllib` to DEFAULT_SNAPSHOT_LAZY_MODULES so an app gets a
serializable V8 startup snapshot without listing them in
`egg.snapshot.lazyModules`. Egg builds its HttpClient (urllib -> undici) during
boot, and undici instantiates an llhttp WebAssembly module (disabled under
--build-snapshot) + an HTTPParser that cannot be snapshot-serialized.

As npm packages, urllib/undici would otherwise be inlined into the bundle and
evaluated at build time; listing them here forces them external so the prelude's
member-proxy stub is used at build and the real module is required on restore.
The member-proxy (eggjs#6003) already replays the recorded access path, so egg's
`class HttpClient extends urllib.HttpClient` keeps working across the
build->restore boundary.

Adds a unit assertion for the default list and a real @utoo/pack build test that
exercises `class Sub extends pkg.Base` for a forced-external npm package across
the build-stub / restore-real boundary (upstream only covered the node:http
builtin).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
killagu added a commit that referenced this pull request Jun 28, 2026
…ots (#6011)

## Motivation

Under `snapshot: true`, the bundler keeps modules **lazy-external** so a
V8 startup snapshot stays serializable (member-proxy stub at build, real
module forwarded at restore via `globalThis.__RUNTIME_REQUIRE`).
`DEFAULT_SNAPSHOT_LAZY_MODULES` currently covers the Node network stack
+ `inspector`.

Egg builds its HttpClient (**urllib → undici**) during boot, and undici
instantiates an llhttp `WebAssembly` module (disabled under
`--build-snapshot`) + an `HTTPParser` that cannot be
snapshot-serialized. As npm packages, urllib/undici would otherwise be
**inlined** into the bundle and evaluated at build time. So every app
had to manually add them to `egg.snapshot.lazyModules` to get a working
snapshot.

This adds `undici` + `urllib` to the default list so they're **forced
external by default** — apps get a serializable snapshot for free.

## What changed

- `prelude.ts`: add `undici` + `urllib` to
`DEFAULT_SNAPSHOT_LAZY_MODULES` (+ JSDoc explaining the rationale and
the npm-package-vs-builtin distinction). `Bundler` adds lazy ids to the
externals map, so listing them forces them external; the member-proxy
from #6003 then stubs them at build and replays the access path against
the real module on restore — so egg's `class HttpClient extends
urllib.HttpClient` keeps working across the build→restore boundary.

## Tests

- Unit: asserts `undici`/`urllib` are in the default list and survive
`resolveSnapshotLazyModules`.
- Real `@utoo/pack` build: a **forced-external npm package** with `class
Sub extends pkg.Base` is a stub at build (no throw, real module never
loaded) and a real base instance after `__RUNTIME_REQUIRE` is installed
— `super(...)`, the inherited method, and the real instance field all
work. Upstream only realbuild-tested the `node:http` builtin, so this
fills the forced-external-npm gap.

28 lazy/realbuild tests green; `tsgo` + `oxlint` clean.

## Note on scope (rebase)

This PR originally (off the older `next`) shipped a bespoke per-export
forwarder inside `__makeLazyExt` to make `class HttpClient extends
urllib.HttpClient` survive the build→restore boundary. While it was
open, #6003 landed and rewrote `__makeLazyExt` into a general
access-path-recording member-proxy (`makeMember`) that already handles
`class X extends pkg.Klass` (plus `ownKeys`/`getOwnPropertyDescriptor`
via `__EXTERNAL_EXPORTS`, arg resolution, etc.). The PR has been
**rebased onto that and reduced to just the default-list addition** —
the forwarder is dropped as superseded. The earlier CodeRabbit/Gemini
review comments were on that now-removed code; replies posted inline.

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

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

* **Bug Fixes**
* Improved snapshot bundling to prevent V8 serialization failures in the
HTTP client module chain, ensuring correct restore-time loading.
* Expanded the default lazy-external module set to include `undici` and
`urllib` automatically (no extra configuration required).

* **Tests**
* Added coverage for default lazy-external resolution when package
metadata is missing.
* Added a real-build regression test validating forced-external behavior
with `class extends` and correct restore timing.

* **Documentation**
* Updated bundler docs and coding guidelines to explain lazy-external
snapshot behavior and the expanded defaults.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

Co-authored-by: Claude Opus 4.8 <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.

3 participants