Skip to content

feat(bundler): lazy-externalize undici + urllib by default for snapshots#6011

Merged
killagu merged 1 commit into
eggjs:nextfrom
killagu:feat/snapshot-default-lazy-undici-urllib
Jun 28, 2026
Merged

feat(bundler): lazy-externalize undici + urllib by default for snapshots#6011
killagu merged 1 commit into
eggjs:nextfrom
killagu:feat/snapshot-default-lazy-undici-urllib

Conversation

@killagu

@killagu killagu commented Jun 28, 2026

Copy link
Copy Markdown
Contributor

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 feat(snapshot): gate V8 snapshot restore to Node.js >= 24, add docs and cnpmcore e2e #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

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.

Copilot AI review requested due to automatic review settings June 28, 2026 02:38

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 28, 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 undici and urllib to the default snapshot lazy module list, updates snapshot prelude documentation, and adds tests plus wiki documentation covering lazy-external restore behavior.

Changes

Snapshot lazy-external defaults and validation

Layer / File(s) Summary
Prelude defaults and module docs
tools/egg-bundler/src/lib/prelude.ts, tools/egg-bundler/test/snapshot-lazy-external.test.ts
Updates the snapshot prelude docs, adds undici and urllib to DEFAULT_SNAPSHOT_LAZY_MODULES, and checks the default resolution path includes both ids.
Lazy-module resolution and real-build regression tests
tools/egg-bundler/test/snapshot-lazy.realbuild.test.ts
Adds a real-build snapshot regression that exercises lazy externals across build and restore execution paths for lazy-base in a class extends scenario.
Wiki changelog and package docs
AGENTS.md, wiki/log.md, wiki/packages/egg-bundler.md
Adds a coding-convention note, a changelog entry, and package documentation for snapshot lazy-external defaults and restore-time forwarding behavior.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • eggjs/egg#5999: Extends the snapshot lazy-external infrastructure that this PR builds on with new defaults and validation.
  • eggjs/egg#6012: Ties into the same snapshot prelude and restore flow for undici and urllib, which this PR adds to the default lazy set.

Suggested reviewers

  • jerryliang64

Poem

A rabbit packed the snapshot nest,
With undici and urllib now at rest.
Build-time sleeps, restore-time wakes,
And lazy modules hop through the breaks.
Hoppy docs and tests agree—
The bundle’s ready, 1, 2, 3!

🚥 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 and concisely describes the main change: default lazy-externalization of undici and urllib for snapshots.
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.

@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 configures the snapshot bundler to lazy-externalize the HTTP client stack (undici and urllib) by default, ensuring a serializable V8 startup snapshot. To support scenarios where a bundled module extends a lazy export at build time (e.g., class HttpClient extends urllib.HttpClient), a per-export forwarder is introduced to dynamically rebind the subclass to the real base class at restore time. The feedback identifies several gaps in the Proxy implementation of this forwarder—specifically regarding symbol handling at build time, and missing has, ownKeys, and getOwnPropertyDescriptor traps—and provides a robust implementation to ensure complete transparency and prevent potential runtime errors.

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/prelude.ts Outdated
Comment on lines +339 to +363
var fwd = new Proxy(function () {}, {
get: function (target, p) {
// \`Subclass.prototype.__proto__\` is wired to this at \`extends\` time.
if (p === 'prototype') return protoProxy;
var e = realExport();
if (e !== undefined && e !== null) return e[p];
if (p === 'then') return undefined;
if (p === 'name' || p === 'length') return Reflect.get(target, p);
return proxy; // build time: stay chainable on the well-tested module proxy
},
apply: function (target, thisArg, args) {
var e = realExport();
if (typeof e === 'function') return Reflect.apply(e, thisArg, args);
return undefined;
},
construct: function (target, args, newTarget) {
var e = realExport();
if (typeof e === 'function') {
// \`super(...)\` arrives with the subclass as newTarget; a direct
// \`new lazy.Export()\` arrives with \`fwd\` itself -> use the real base.
return Reflect.construct(e, args, newTarget === fwd ? e : newTarget);
}
return proxy; // build time: keep \`new lazy.Export().method()\` chainable
},
});

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

The per-export forwarder fwd has a few subtle gaps in its Proxy implementation compared to the main proxy:

  1. Symbol Handling at Build Time: If p is a symbol (e.g., Symbol.toStringTag, Symbol.toPrimitive, Symbol.iterator) and the real module is not loaded yet (build time), the get trap will fall back to returning proxy. Returning a function/Proxy for standard symbols can break JS engine/library checks (e.g., causing TypeError during coercion or infinite loops during iteration checks). It should return undefined for symbols at build time, matching the main proxy's behavior.
  2. Missing has Trap: Without a has trap, checking property existence on the forwarder (e.g., 'someStaticMethod' in urllib.HttpClient) at restore time will check the dummy target function () {} and return false, even if the property exists on the real export.
  3. Missing ownKeys and getOwnPropertyDescriptor Traps: Without these traps, structural reflection (like Object.keys(), Object.getOwnPropertyNames(), or object spread) on the forwarded export at restore time will only see the properties of the dummy target, completely hiding the real export's static properties and methods.

Adding these traps ensures complete transparency for the forwarded exports at restore time.

      var fwd = new Proxy(function () {}, {
        get: function (target, p) {
          // Subclass.prototype.__proto__ is wired to this at extends time.
          if (p === 'prototype') return protoProxy;
          var e = realExport();
          if (e !== undefined && e !== null) return e[p];
          if (p === 'then') return undefined;
          if (p === 'name' || p === 'length') return Reflect.get(target, p);
          if (typeof p === 'symbol') return undefined;
          return proxy; // build time: stay chainable on the well-tested module proxy
        },
        apply: function (target, thisArg, args) {
          var e = realExport();
          if (typeof e === 'function') return Reflect.apply(e, thisArg, args);
          return undefined;
        },
        construct: function (target, args, newTarget) {
          var e = realExport();
          if (typeof e === 'function') {
            // super(...) arrives with the subclass as newTarget; a direct
            // new lazy.Export() arrives with fwd itself -> use the real base.
            return Reflect.construct(e, args, newTarget === fwd ? e : newTarget);
          }
          return proxy; // build time: keep new lazy.Export().method() chainable
        },
        has: function (target, p) {
          var e = realExport();
          if (e !== undefined && e !== null) return p in e;
          return Reflect.has(target, p);
        },
        ownKeys: function (target) {
          var e = realExport();
          if (e === undefined || e === null || (typeof e !== 'object' && typeof e !== 'function')) return Reflect.ownKeys(target);
          var keys = Reflect.ownKeys(e);
          var targetKeys = Reflect.ownKeys(target);
          for (var i = 0; i < targetKeys.length; i++) {
            if (keys.indexOf(targetKeys[i]) === -1) keys.push(targetKeys[i]);
          }
          return keys;
        },
        getOwnPropertyDescriptor: function (target, p) {
          var targetDesc = Reflect.getOwnPropertyDescriptor(target, p);
          if (targetDesc && !targetDesc.configurable) return targetDesc;
          var e = realExport();
          if (e === undefined || e === null || (typeof e !== 'object' && typeof e !== 'function')) return targetDesc;
          var desc = Reflect.getOwnPropertyDescriptor(e, p);
          if (desc) desc.configurable = true;
          return desc;
        },
      });
References
  1. When implementing a JavaScript Proxy that wraps a dummy target but delegates to a real object at runtime, ensure structural traps like ownKeys and getOwnPropertyDescriptor correctly merge or delegate to the real object. To avoid Proxy invariant violations (e.g., reporting a non-configurable property that is absent from the target), force configurable: true on descriptors retrieved from the real object, while still faithfully reporting the target's own non-configurable properties.

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.

Thanks. This PR was rebased onto the latest next; #6003 landed in the meantime and replaced this per-export forwarder with a general member-proxy (makeMember), so the code this comment was on no longer exists. The PR is now reduced to adding undici/urllib to the default lazy list.

On the specific gaps: the new top-level __makeLazyExt proxy already implements has/ownKeys/getOwnPropertyDescriptor (the last two backed by __EXTERNAL_EXPORTS so @utoo/pack's interopEsm enumerates the real named exports) and returns undefined for symbols at build time. The inner makeMember proxy intentionally records an access path (get/apply/construct) rather than mirroring structure. So these are handled upstream rather than in this PR.

@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/egg-bundler/src/lib/prelude.ts`:
- Around line 340-344: The proxy get trap in prelude.ts is rebinding static
accessors to the real export instead of preserving the subclass receiver. Update
the get handler in the lazy export proxy so that when realExport() returns a
value, it uses Reflect.get on the export with the provided recv instead of
direct property access, keeping class Sub extends lazy.Base static getters bound
to Sub rather than RealBase.
🪄 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: 56273d23-cd9f-44e4-9194-877e2421be50

📥 Commits

Reviewing files that changed from the base of the PR and between ff17536 and 44a2789.

📒 Files selected for processing (5)
  • tools/egg-bundler/src/lib/prelude.ts
  • tools/egg-bundler/test/snapshot-lazy-external.test.ts
  • tools/egg-bundler/test/snapshot-lazy.realbuild.test.ts
  • wiki/log.md
  • wiki/packages/egg-bundler.md

Comment thread tools/egg-bundler/src/lib/prelude.ts Outdated
@killagu killagu force-pushed the feat/snapshot-default-lazy-undici-urllib branch from 44a2789 to 58d5d56 Compare June 28, 2026 03:00
@codecov

codecov Bot commented Jun 28, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 81.94%. Comparing base (b39c019) to head (d428e29).

Additional details and impacted files
@@            Coverage Diff             @@
##             next    #6011      +/-   ##
==========================================
- Coverage   81.95%   81.94%   -0.01%     
==========================================
  Files         677      677              
  Lines       20651    20651              
  Branches     4099     4099              
==========================================
- Hits        16924    16922       -2     
- Misses       3214     3215       +1     
- Partials      513      514       +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.

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

🤖 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/prelude.ts`:
- Around line 274-287: Update the proxy forwarding in prelude.ts so property
access goes through Reflect.get with the correct receiver instead of direct
indexing. In the member proxy’s get trap and protoProxy’s get trap, forward
reads to the resolved export/prototype using Reflect.get(..., recv) so accessors
and inherited/static getters observe the right this when the proxy is used as a
subclass or prototype. Keep the existing special cases for __MR, prototype,
then, and symbols, but replace the raw r[p] / r.prototype[p] lookup in the
relevant proxy handlers.

In `@tools/egg-bundler/test/snapshot-lazy-external.test.ts`:
- Around line 120-126: The snapshot test is hard-coding the expected header-size
value, which makes it brittle across Node versions. Update the assertion in the
`renderSnapshotPrelude()` test to compare against `String(http.maxHeaderSize)`
instead of the literal `16384`, while keeping the existing checks for the other
inlined HTTP constants. This should be done in the
`snapshot-lazy-external.test.ts` test case that verifies the
`globalThis.__HTTP_CONSTS` prelude output.

In `@tools/egg-bundler/test/snapshot-lazy.realbuild.test.ts`:
- Around line 221-265: The restore-path test currently can succeed via native
module resolution instead of the lazy hook, and it does not verify the promised
instanceof behavior. Update the restore runner around `__RUNTIME_REQUIRE` and
`__makeSub` so it blocks direct `lazy-base` resolution in `baseDir/dist`,
forcing the instantiation path to go through the runtime require hook. Then
extend the `restoreRunner` probe to assert the constructed instance is an
`instanceof Base`, alongside the existing `tag`, `greet`, and `describe` checks.
🪄 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: d9eda364-71f7-4d06-b98b-3f193b0e7710

📥 Commits

Reviewing files that changed from the base of the PR and between 44a2789 and 58d5d56.

📒 Files selected for processing (5)
  • tools/egg-bundler/src/lib/prelude.ts
  • tools/egg-bundler/test/snapshot-lazy-external.test.ts
  • tools/egg-bundler/test/snapshot-lazy.realbuild.test.ts
  • wiki/log.md
  • wiki/packages/egg-bundler.md
✅ Files skipped from review due to trivial changes (1)
  • wiki/packages/egg-bundler.md

@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

Inline review comments failed to post. This is likely due to GitHub's internal server error or limits when posting large numbers of comments. If you are seeing this consistently it is likely a permissions issue. Please check "Moderation" -> "Code review limits" under your organization settings.

Actionable comments posted: 3

🤖 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/prelude.ts`:
- Around line 274-287: Update the proxy forwarding in prelude.ts so property
access goes through Reflect.get with the correct receiver instead of direct
indexing. In the member proxy’s get trap and protoProxy’s get trap, forward
reads to the resolved export/prototype using Reflect.get(..., recv) so accessors
and inherited/static getters observe the right this when the proxy is used as a
subclass or prototype. Keep the existing special cases for __MR, prototype,
then, and symbols, but replace the raw r[p] / r.prototype[p] lookup in the
relevant proxy handlers.

In `@tools/egg-bundler/test/snapshot-lazy-external.test.ts`:
- Around line 120-126: The snapshot test is hard-coding the expected header-size
value, which makes it brittle across Node versions. Update the assertion in the
`renderSnapshotPrelude()` test to compare against `String(http.maxHeaderSize)`
instead of the literal `16384`, while keeping the existing checks for the other
inlined HTTP constants. This should be done in the
`snapshot-lazy-external.test.ts` test case that verifies the
`globalThis.__HTTP_CONSTS` prelude output.

In `@tools/egg-bundler/test/snapshot-lazy.realbuild.test.ts`:
- Around line 221-265: The restore-path test currently can succeed via native
module resolution instead of the lazy hook, and it does not verify the promised
instanceof behavior. Update the restore runner around `__RUNTIME_REQUIRE` and
`__makeSub` so it blocks direct `lazy-base` resolution in `baseDir/dist`,
forcing the instantiation path to go through the runtime require hook. Then
extend the `restoreRunner` probe to assert the constructed instance is an
`instanceof Base`, alongside the existing `tag`, `greet`, and `describe` checks.
🪄 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: d9eda364-71f7-4d06-b98b-3f193b0e7710

📥 Commits

Reviewing files that changed from the base of the PR and between 44a2789 and 58d5d56.

📒 Files selected for processing (5)
  • tools/egg-bundler/src/lib/prelude.ts
  • tools/egg-bundler/test/snapshot-lazy-external.test.ts
  • tools/egg-bundler/test/snapshot-lazy.realbuild.test.ts
  • wiki/log.md
  • wiki/packages/egg-bundler.md
✅ Files skipped from review due to trivial changes (1)
  • wiki/packages/egg-bundler.md
🛑 Comments failed to post (3)
tools/egg-bundler/src/lib/prelude.ts (1)

274-287: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Forward through Reflect.get(..., recv) here. r[p] and r.prototype[p] bind accessors to the resolved export/prototype, so inherited or static getters still see the wrong this when the proxy is used as a subclass or prototype.

🤖 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/prelude.ts` around lines 274 - 287, Update the
proxy forwarding in prelude.ts so property access goes through Reflect.get with
the correct receiver instead of direct indexing. In the member proxy’s get trap
and protoProxy’s get trap, forward reads to the resolved export/prototype using
Reflect.get(..., recv) so accessors and inherited/static getters observe the
right this when the proxy is used as a subclass or prototype. Keep the existing
special cases for __MR, prototype, then, and symbols, but replace the raw r[p] /
r.prototype[p] lookup in the relevant proxy handlers.
tools/egg-bundler/test/snapshot-lazy-external.test.ts (1)

120-126: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Use http.maxHeaderSize instead of a hard-coded value. renderSnapshotPrelude() serializes the build Node’s header-size default, so asserting 16384 makes this test brittle across Node runtimes. Compare against String(http.maxHeaderSize) instead.

🤖 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/snapshot-lazy-external.test.ts` around lines 120 -
126, The snapshot test is hard-coding the expected header-size value, which
makes it brittle across Node versions. Update the assertion in the
`renderSnapshotPrelude()` test to compare against `String(http.maxHeaderSize)`
instead of the literal `16384`, while keeping the existing checks for the other
inlined HTTP constants. This should be done in the
`snapshot-lazy-external.test.ts` test case that verifies the
`globalThis.__HTTP_CONSTS` prelude output.
tools/egg-bundler/test/snapshot-lazy.realbuild.test.ts (1)

221-265: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

git ls-files tools/egg-bundler/test/snapshot-lazy.realbuild.test.ts tools/egg-bundler/test | sed -n '1,120p'
echo '--- FILE OUTLINE ---'
ast-grep outline tools/egg-bundler/test/snapshot-lazy.realbuild.test.ts --view expanded
echo '--- RELEVANT LINES ---'
sed -n '180,290p' tools/egg-bundler/test/snapshot-lazy.realbuild.test.ts
echo '--- SEARCH FOR INSTANCEOF / RUNTIME_REQUIRE / lazy-base ---'
rg -n 'instanceof|__RUNTIME_REQUIRE|lazy-base|Base' tools/egg-bundler/test/snapshot-lazy.realbuild.test.ts tools/egg-bundler/test -S

Repository: eggjs/egg

Length of output: 21404


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- snapshot-lazy-external restore behavior ---'
sed -n '220,290p' tools/egg-bundler/test/snapshot-lazy-external.test.ts

echo '--- snapshot-lazy-bundler lazy proxy behavior ---'
sed -n '100,170p' tools/egg-bundler/test/snapshot-lazy-bundler.test.ts

echo '--- any existing instanceof assertions in egg-bundler tests ---'
rg -n 'instanceof\s+Base|instanceof\s+.*Base|instanceof' tools/egg-bundler/test -S

Repository: eggjs/egg

Length of output: 6095


Make the restore runner exercise __RUNTIME_REQUIRE and assert instanceof. Since the runner sits in baseDir/dist, require('lazy-base') can still resolve natively from baseDir/node_modules, so this test can pass even if the restore path never uses the lazy hook. It also never checks the advertised instanceof contract. Block native resolution in the runner and add an instanceof Base assertion.

🧰 Tools
🪛 ast-grep (0.44.0)

[warning] Importing child_process exposes a command-execution surface; ensure any command/argument built from input is validated, and prefer execFile/spawn with an argument array over exec.
Context: import { execFile } from 'node:child_process';
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').

(detect-child-process-typescript)


[warning] 226-239: Filesystem path is not a string literal; a request-/variable-derived path can enable path traversal. Validate and normalize the path before use.
Context: fs.writeFile(
buildRunner,
[
'const probe = { phase: "build" };',
'try {',
' require("./worker.js");',
' globalThis.__makeSub(7);',
' probe.restored = !!globalThis.__RUNTIME_REQUIRE;',
' probe.threw = false;',
'} catch (e) { probe.threw = true; probe.err = e.message; }',
'process.stdout.write("\n" + JSON.stringify(probe));',
'',
].join('\n'),
)
Note: [CWE-22] Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal').

(detect-non-literal-fs-filename-typescript)


[warning] 247-262: Filesystem path is not a string literal; a request-/variable-derived path can enable path traversal. Validate and normalize the path before use.
Context: fs.writeFile(
restoreRunner,
[
'require("./worker.js");',
globalThis.__RUNTIME_REQUIRE = (id) => id === "lazy-base" ? require(${basePathLiteral}) : require(id);,
'const inst = globalThis.__makeSub(7);',
'const probe = {',
' restored: true,',
' tag: inst.tag,',
' greet: inst.greet(),',
' describe: inst.describe(),',
'};',
'process.stdout.write("\n" + JSON.stringify(probe));',
'',
].join('\n'),
)
Note: [CWE-22] Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal').

(detect-non-literal-fs-filename-typescript)

🤖 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/snapshot-lazy.realbuild.test.ts` around lines 221 -
265, The restore-path test currently can succeed via native module resolution
instead of the lazy hook, and it does not verify the promised instanceof
behavior. Update the restore runner around `__RUNTIME_REQUIRE` and `__makeSub`
so it blocks direct `lazy-base` resolution in `baseDir/dist`, forcing the
instantiation path to go through the runtime require hook. Then extend the
`restoreRunner` probe to assert the constructed instance is an `instanceof
Base`, alongside the existing `tag`, `greet`, and `describe` checks.

@killagu killagu force-pushed the feat/snapshot-default-lazy-undici-urllib branch from 58d5d56 to 1779d3f Compare June 28, 2026 04:10
Copilot AI review requested due to automatic review settings June 28, 2026 04:10

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 commented Jun 28, 2026

Copy link
Copy Markdown
Contributor Author

Re: CodeRabbit re-review (3 items; inline comments failed to post)

  1. prelude.ts:274-287Reflect.get(..., recv) in makeMember/protoProxy. Out of scope here: that's the general access-path member-proxy from feat(snapshot): gate V8 snapshot restore to Node.js >= 24, add docs and cnpmcore e2e #6003, not touched by this PR (which only adds undici/urllib to the default list). It's a valid receiver-preservation nit but only affects static/inherited accessors that read this on a snapshot-frozen subclass (urllib/undici don't have those). Happy to do it as a separate follow-up against the feat(snapshot): gate V8 snapshot restore to Node.js >= 24, add docs and cnpmcore e2e #6003 code rather than widen this PR.

  2. snapshot-lazy-external.test.ts:120-126 — hard-coded 16384. Also pre-existing feat(snapshot): gate V8 snapshot restore to Node.js >= 24, add docs and cnpmcore e2e #6003 code (the __HTTP_CONSTS test), not added/changed by this PR. Leaving it to keep this PR scoped.

  3. snapshot-lazy.realbuild.test.ts restore test — addressed (1779d3fd). Added a module-eval load counter to lazy-base so the runners now prove the lazy-hook path: at build the real package is never loaded (realLoads === 0); at restore it loads exactly once and only when the member-proxy resolves through __RUNTIME_REQUIRE (loadedBeforeMakeSub === 0, loadedAfterMakeSub === 1) — not via native resolution. I deliberately did not assert instanceof Base: makeMember's protoProxy exposes only a get trap (no getPrototypeOf), so inherited methods forward to the real prototype but the frozen subclass's chain doesn't literally contain Base.prototype; identity-by-prototype is a known non-goal of the upstream member-proxy, so that assertion would fail by design (documented in the test).

Re: "do other framework built-in packages need force-external?" (incl. the MCP SDK)

Audited egg core + default plugins + tegg/core|plugin. No additional package needs adding. Notably the MCP SDK does not: the default controller plugin imports it at boot (tegg/plugin/controller/src/app.ts:19MCPControllerRegister → SDK server/streamableHttp.js), and that transitively does a top-level require("http2") via @hono/node-server plus class extends globalThis.Request — but both are already covered (http2 is in the lazy list; globalThis.Request is stubbed by the prelude), and no MCP transport/server is instantiated at boot (only thunks/maps). No gRPC (@grpc/grpc-js)/ws server or fs-watcher native handle exists in the framework boot path. The opt-in mcp-client/mcp-proxy/langchain plugins pull SDK client transports (eventsource/cross-spawn/pkce-challenge) — those are an app-opt-in concern via egg.snapshot.lazyModules, not a framework default.

@killagu killagu force-pushed the feat/snapshot-default-lazy-undici-urllib branch from 1779d3f to 6f44b93 Compare June 28, 2026 05:20
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 killagu force-pushed the feat/snapshot-default-lazy-undici-urllib branch from 6f44b93 to d428e29 Compare June 28, 2026 06:03
Copilot AI review requested due to automatic review settings June 28, 2026 06: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 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.

🧹 Nitpick comments (1)
tools/egg-bundler/test/snapshot-lazy.realbuild.test.ts (1)

295-296: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Avoid pinning this test to the helper’s exact emitted syntax.

/function\s+externalRequire\s*\(/ couples the test to a specific codegen shape. A harmless refactor to const externalRequire = ... would fail this test even if snapshot lazy-restore still works. The behavioral checks below already cover the contract this PR cares about.

Suggested change
     expect(worker).toContain(SNAPSHOT_PRELUDE_MARKER);
-    expect(worker).toMatch(/function\s+externalRequire\s*\(/);
     await execFileAsync(process.execPath, ['--check', workerPath]);
🤖 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/snapshot-lazy.realbuild.test.ts` around lines 295 -
296, The snapshot-lazy restore test is over-coupled to the exact emitted syntax
of externalRequire, so remove the regex assertion from
snapshot-lazy.realbuild.test.ts and keep the behavioral checks around
SNAPSHOT_PRELUDE_MARKER and the lazy-restore contract instead. Locate the
assertion in the test that matches externalRequire and replace it with a less
implementation-specific check so harmless codegen refactors do not break the
test.
🤖 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.

Nitpick comments:
In `@tools/egg-bundler/test/snapshot-lazy.realbuild.test.ts`:
- Around line 295-296: The snapshot-lazy restore test is over-coupled to the
exact emitted syntax of externalRequire, so remove the regex assertion from
snapshot-lazy.realbuild.test.ts and keep the behavioral checks around
SNAPSHOT_PRELUDE_MARKER and the lazy-restore contract instead. Locate the
assertion in the test that matches externalRequire and replace it with a less
implementation-specific check so harmless codegen refactors do not break the
test.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f1d8a13b-faf7-4071-ac64-8360832bab91

📥 Commits

Reviewing files that changed from the base of the PR and between 6f44b93 and d428e29.

📒 Files selected for processing (6)
  • AGENTS.md
  • tools/egg-bundler/src/lib/prelude.ts
  • tools/egg-bundler/test/snapshot-lazy-external.test.ts
  • tools/egg-bundler/test/snapshot-lazy.realbuild.test.ts
  • wiki/log.md
  • wiki/packages/egg-bundler.md
✅ Files skipped from review due to trivial changes (4)
  • AGENTS.md
  • wiki/log.md
  • tools/egg-bundler/test/snapshot-lazy-external.test.ts
  • wiki/packages/egg-bundler.md
🚧 Files skipped from review as they are similar to previous changes (1)
  • tools/egg-bundler/src/lib/prelude.ts

@killagu killagu merged commit 9e8abce into eggjs:next Jun 28, 2026
21 of 22 checks passed
killagu added a commit that referenced this pull request Jun 28, 2026
…rt readiness) (#6015)

Two unrelated **flaky test** fixes surfaced while stabilizing CI for the
snapshot work (#6011). Test-only; no `src` changes.

## 1. `@eggjs/multipart` — `ENOTEMPTY` teardown race

`file-mode.test.ts` flaked on macOS Node 24 (passing on every other
matrix job):

```
Error: ENOTEMPTY: directory not empty, rmdir '.../egg-multipart-tmp/multipart-file-mode-demo/2026/06/28/05'
```

`fs.rm(tmpdir, { force: true, recursive: true })` defaults to
`maxRetries: 0`, so a transient `ENOTEMPTY` (a file in a date-bucket dir
mid recursive-removal — async per-request cleanup draining / APFS
latency) throws and fails the suite. The production `clean_tmpdir`
schedule already swallows these; only the 7 test teardowns are fragile.
**Fix:** add `maxRetries: 3` to all 7.

## 2. `@eggjs/scripts` — flaky `stop.test.ts` start readiness

`Test scripts (ubuntu-22)` failed in `stop.test.ts`:

```
AssertionError: expected 'Starting custom-framework application…' to match /custom-framework started on http:\/\/…/
```

`stop.test.ts` waited a fixed **1s** for a 2-worker egg app to boot
before asserting its `started on http://…` log — while every sibling
`start-without-demon-*.test.ts` waits **10s**. On a loaded runner 1s
isn't enough. **Fix:** replace the fixed `scheduler.wait(waitTime)`
before each startup/shutdown assertion with a `waitFor(getText, pattern,
timeout)` poll (`test/utils.ts`) that returns as soon as the expected
output appears (up to 10s) — race-free and faster than a fixed delay.
Windows keeps the fixed settle for the signal-driven shutdown checks
(SIGTERM isn't handled there).

## Verification
- multipart: the 7 teardowns now retry; affected tests pass locally.
- scripts: `stop.test.ts` passes locally after build (8 passed, 1
skipped).

Re Gemini's review on the multipart hunks: it claimed Vitest runs
`afterAll` in registration order — that's incorrect (default
`sequence.hooks: 'stack'` → reverse; verified empirically on v4.1.9), so
the current teardown already removes the tmpdir **last** (after
`server.close()`/`app.close()`); the suggested reorder would move
`fs.rm` first and reintroduce the race. Replied inline on each thread.

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

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

## Summary by CodeRabbit

* **Tests**
* Improved cleanup reliability across multipart-related test suites by
adding additional retry behavior when removing temporary directories
during teardown.
* Updated process lifecycle checks in test runs to wait for specific
startup/shutdown output instead of relying on fixed timing, reducing
flaky boot/shutdown timing.
* Introduced a reusable polling helper to detect expected log output
within a timeout.

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

2 participants