Skip to content

Commit b39c019

Browse files
killaguclaude
andauthored
fix(bundler): re-install web globals after a V8 snapshot restore (#6012)
## Motivation In a bundled V8 startup snapshot, the prelude replaces Node's undici-backed web globals (`fetch`/`Headers`/`Request`/`Response`/`FormData`/`WebSocket`/`EventSource`/`MessageEvent`/`CloseEvent`) with constructable stubs at build time — touching them pulls in undici's native `HTTPParser`/http2 bindings, which a snapshot can't serialize. But **the restored process kept the stubs**, so `globalThis.fetch(...)` was a dead no-op after restore. (This is the "Web globals are no-op stubs after restore" known limitation from the docs.) ## What changed - The generated snapshot-restore entry now installs `__RUNTIME_REQUIRE` (now carrying `.resolve`) and then calls a new `globalThis.__installWebGlobalsLazy` (defined by the prelude, serialized into the blob). It re-installs the web globals as **lazy accessors**: - the fetch family from the app's real `undici` — kept external (e.g. `--force-external undici urllib`, as the cnpmcore e2e already does), so it loads for real on restore; resolved directly, or through `urllib` under pnpm where undici isn't hoisted; - `Blob`/`File` from `node:buffer`. - The lazy accessor must **not cache `undefined`**: undici reads `globalThis.Headers` re-entrantly while its own `require()` is still in flight, so caching there would poison `Headers`. - **`Blob`/`File` are no longer stubbed on `node:buffer`.** They are `node:buffer`-backed (not undici), serialize into a snapshot fine, and stay real — the previous buffer-getter stub made them unrecoverable at restore. Verified the cnpmcore snapshot still serializes with them un-stubbed. Net effect: `globalThis.fetch(...)` and the related constructors work normally after a restore. Docs updated (EN + ZH): the limitation becomes a "works after restore" description, with the one remaining caveat (a web global captured at build time — `const f = fetch`, or `class X extends globalThis.Request` — freezes the stub; reference web globals at call time). ## Test evidence - New/updated unit + real-`@utoo/pack`-build tests in `snapshot-lazy-external.test.ts` and `snapshot-lazy.realbuild.test.ts`: lazy re-install from undici/`node:buffer`, re-entrancy, replace-stub-but-keep-genuine, non-configurable skip, and a real build where the prelude stubs the globals and the installer brings back real undici-backed `fetch`/`Headers`/`Blob`. - Verified end-to-end with a real `node --build-snapshot` + restore on **Node 24**: all web globals work — real `fetch` round-trip + `new Headers` + `new Blob`. - Verified against the **cnpmcore** snapshot (`next`'s recipe: `--force-external undici urllib @cnpmjs/packument koa-onerror`, no manual stubs): build → restore → `GET /-/ping` HTTP 200, with the real ORM/HTTP client live at restore. - `egg-bundler` suite green except pre-existing macOS-only env failures; lint / format / typecheck clean. 🤖 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** * Snapshot restores now lazily reinstall web globals after startup via `__installWebGlobalsLazy` (including `fetch`/`Headers`/`Request`/`Response`), with `Blob`/`File` restored from `node:buffer`. * Snapshot module loading now supports `__RUNTIME_REQUIRE.resolve` for resolver lookups. * **Bug Fixes** * Build-time snapshot stubbing is safer and no longer interferes with `node:buffer` web types. * **Documentation** * Updated “Web globals” known-limits guidance (EN and zh-CN) to emphasize using globals at call time. * **Tests** * Expanded coverage for lazy install, re-entrancy, and real undici-backed restore behavior. <!-- end of auto-generated comment: release notes by coderabbit.ai --> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent ff17536 commit b39c019

9 files changed

Lines changed: 296 additions & 46 deletions

File tree

packages/typings/src/global.ts

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,24 @@ declare global {
99
* Synchronous require bound to the bundle output directory, installed by the
1010
* snapshot restore main function. The snapshot prelude / lazy mechanism uses it
1111
* to pull in modules through `require()` (Node 22+ can `require()` ESM) because a
12-
* deserialized snapshot process has no dynamic `import()` callback.
12+
* deserialized snapshot process has no dynamic `import()` callback. It also carries
13+
* a `resolve` (the underlying `createRequire(...).resolve`) so the web globals
14+
* re-installer can locate `undici` through the app dependency tree.
1315
*/
1416
// eslint-disable-next-line no-var
15-
var __RUNTIME_REQUIRE: ((id: string) => unknown) | undefined;
17+
var __RUNTIME_REQUIRE:
18+
| (((id: string) => unknown) & { resolve?: (id: string, options?: { paths?: string[] }) => string })
19+
| undefined;
20+
/**
21+
* Re-install the web globals (`fetch`/`Headers`/.../`Blob`/`File`) that the snapshot
22+
* prelude replaces with stubs at build time, as lazy accessors backed by `undici`
23+
* (the fetch family) and `node:buffer` (`Blob`/`File`). Defined by the snapshot
24+
* prelude and serialized into the blob; the snapshot restore main calls it once
25+
* `__RUNTIME_REQUIRE` is installed, so an app that uses `globalThis.fetch` keeps
26+
* working after a restore.
27+
*/
28+
// eslint-disable-next-line no-var
29+
var __installWebGlobalsLazy: (() => void) | undefined;
1630
}
1731

1832
export {};

site/docs/advanced/snapshot.md

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -203,15 +203,13 @@ which is exactly the cost a snapshot front-loads into build time.
203203
be kept external (`--force-external`) or implement the snapshot lifecycle hooks
204204
so its state is released before serialization and rebuilt after restore. Not
205205
every package is snapshot-safe out of the box.
206-
- **Web globals are no-op stubs after restore**: at build the prelude replaces the
207-
undici-backed globals (`fetch`/`Headers`/`Request`/`Response`/`FormData`/`WebSocket`/
208-
`EventSource`/`MessageEvent`/`CloseEvent`, plus `Blob`/`File`) with no-op stubs,
209-
because touching them at build time pulls in
210-
Node's built-in undici and its native http/http2 bindings (which a snapshot cannot
211-
serialize). Node's native lazy getters are themselves not snapshot-serializable, so
212-
they cannot be reinstated on restore. **In the restored process those globals stay
213-
stubs** — a snapshotted app should use a lazy-loaded HTTP client (e.g. `urllib`/`undici`
214-
via an external, loaded for real on restore) rather than `globalThis.fetch` directly.
206+
- **Web globals must be referenced at call time**: the undici-backed globals
207+
(`fetch`/`Headers`/`Request`/`Response`/`FormData`/`WebSocket`/...) are stubbed at
208+
build time (touching them pulls in undici's non-serializable native bindings) and
209+
re-installed on restore, so call-time use works. But a reference captured at
210+
module-evaluation time — `const f = fetch`, or `class X extends globalThis.Request`
211+
freezes the build-time stub into the blob and is not upgraded. Reference web globals
212+
where you use them, not at module top level.
215213

216214
The supported surface is still evolving; the full list of known limitations and
217215
the design rationale are tracked in the project's V8 snapshot RFC.

site/docs/zh-CN/advanced/snapshot.md

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -183,13 +183,11 @@ module.exports = AppBootHook;
183183
(打开的 socket、原生 HTTP/2 绑定、后台 timer、文件句柄)都必须要么保持 external
184184
`--force-external`),要么实现快照生命周期钩子,在序列化前释放、在恢复后重建。并不是
185185
每个包都开箱即可被快照化。
186-
- **Web 全局对象在恢复后是惰性桩**:构建期 prelude 会把 undici 支撑的全局对象
187-
`fetch`/`Headers`/`Request`/`Response`/`FormData`/`WebSocket`/`EventSource`/`MessageEvent`/
188-
`CloseEvent`、以及 `Blob`/`File`)替换为空操作的桩,否则在构建期触碰它们会拉起 Node 内建
189-
undici 的原生 http/http2 绑定(无法被快照
190-
序列化)。Node 的原生惰性 getter 本身不可被快照序列化,因此恢复后无法把它们还原。**恢复后的
191-
进程里这些全局对象仍是桩**——快照化的应用应使用按需懒加载的 HTTP 客户端(如 `urllib`/`undici`
192-
通过 external 在恢复期加载真实模块),而不要直接依赖 `globalThis.fetch`
186+
- **Web 全局对象必须在调用处引用**:undici 支撑的全局对象
187+
`fetch`/`Headers`/`Request`/`Response`/`FormData`/`WebSocket`/……)会在构建期被替换为桩
188+
(触碰它们会拉起 undici 不可序列化的原生绑定),并在恢复时重新安装,因此在调用处使用时可正常工作。
189+
但在模块求值期捕获的绑定——`const f = fetch`,或 `class X extends globalThis.Request`——会把构建期的
190+
桩固化进 blob 且不会被升级。请在使用处引用 Web 全局对象,不要在模块顶层捕获。
193191

194192
支持范围仍在演进中;完整的已知限制与设计取舍记录在项目的 V8 快照 RFC 中。
195193

tools/egg-bundler/src/lib/EntryGenerator.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -506,9 +506,19 @@ if (process.env.EGG_BUNDLE_SNAPSHOT === 'build') {
506506
? process.getBuiltinModule('node:module')
507507
: (0, eval)('require')('node:module');
508508
const __req = createRequire(__outputDir + '/');
509-
globalThis.__RUNTIME_REQUIRE = (id: string) => __req(id);
509+
const __runtimeRequire: any = (id: string) => __req(id);
510+
// Expose resolve so the web-globals re-installer can locate undici through the
511+
// app dependency tree (e.g. via urllib under pnpm).
512+
__runtimeRequire.resolve = (id: string, options?: any) => __req.resolve(id, options);
513+
globalThis.__RUNTIME_REQUIRE = __runtimeRequire;
510514
globalThis.__EGG_MODULE_IMPORTER__ = async (fp: string) => __req(fp);
511515
516+
// Re-install the web globals (fetch/Headers/.../Blob/File) the snapshot prelude
517+
// replaced with stubs at build time, so an app using globalThis.fetch keeps
518+
// working after a restore. They become lazy accessors backed by undici (the
519+
// fetch family) and node:buffer (Blob/File), loaded on first use.
520+
globalThis.__installWebGlobalsLazy?.();
521+
512522
(async () => {
513523
if (app.agent) {
514524
await app.agent.triggerSnapshotDidDeserialize();

tools/egg-bundler/src/lib/prelude.ts

Lines changed: 69 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ export const DEFAULT_SNAPSHOT_LAZY_MODULES: readonly string[] = [
7474
* stubs (NOT `delete`d — a deleted global throws ReferenceError when a bundled
7575
* module references it; a stub is referencable and harmless at build time).
7676
*/
77-
const WEB_GLOBALS: readonly string[] = [
77+
const UNDICI_WEB_GLOBALS: readonly string[] = [
7878
'fetch',
7979
'Headers',
8080
'Request',
@@ -84,17 +84,12 @@ const WEB_GLOBALS: readonly string[] = [
8484
'EventSource',
8585
'MessageEvent',
8686
'CloseEvent',
87-
'File',
88-
'Blob',
8987
];
9088

91-
/**
92-
* `node:buffer` is a real, serializable builtin (Buffer is needed at build time),
93-
* but reading its `File`/`Blob` getters lazily initializes Node's built-in undici
94-
* (→ http/http2 native bindings). Those two property getters are stubbed at build;
95-
* the live process re-exposes the real ones on restore.
96-
*/
97-
const BUFFER_DANGEROUS_PROPS: readonly string[] = ['File', 'Blob'];
89+
/** Web globals provided by `node:buffer`; re-installed from that builtin on restore. */
90+
const BUFFER_WEB_GLOBALS: readonly string[] = ['File', 'Blob'];
91+
92+
const WEB_GLOBALS: readonly string[] = [...UNDICI_WEB_GLOBALS, ...BUFFER_WEB_GLOBALS];
9893

9994
interface AppPackageJson {
10095
readonly egg?: {
@@ -149,7 +144,8 @@ export function renderSnapshotPrelude(
149144
): string {
150145
const lazyJson = JSON.stringify([...lazyModules]);
151146
const webJson = JSON.stringify([...WEB_GLOBALS]);
152-
const bufDangerJson = JSON.stringify([...BUFFER_DANGEROUS_PROPS]);
147+
const undiciGlobalsJson = JSON.stringify([...UNDICI_WEB_GLOBALS]);
148+
const bufferGlobalsJson = JSON.stringify([...BUFFER_WEB_GLOBALS]);
153149
const externalExportsJson = JSON.stringify(externalExports);
154150
const httpConstsJson = JSON.stringify({
155151
METHODS: http.METHODS,
@@ -199,18 +195,6 @@ export function renderSnapshotPrelude(
199195
// namespace and \`import { X } from 'pkg'\` resolves to a member-proxy (not undefined).
200196
globalThis.__EXTERNAL_EXPORTS = ${externalExportsJson};
201197
202-
// node:buffer is real at build (Buffer is needed) but its File/Blob getters
203-
// trigger Node's built-in undici. Stub just those two; restore re-exposes them.
204-
(function () {
205-
try {
206-
var __buf = process.getBuiltinModule('node:buffer');
207-
var __bd = ${bufDangerJson};
208-
for (var __j = 0; __j < __bd.length; __j++) {
209-
try { Object.defineProperty(__buf, __bd[__j], { value: function BufStub(){}, configurable: true, writable: true }); } catch (e) {}
210-
}
211-
} catch (e) {}
212-
})();
213-
214198
// isBuiltin: tells builtin "tool" modules (path/fs/module — load for real at
215199
// build) apart from non-builtin packages (lazy-stubbed at build).
216200
globalThis.__isBuiltin = (function () {
@@ -318,6 +302,68 @@ export function renderSnapshotPrelude(
318302
});
319303
return proxy;
320304
};
305+
306+
// Restore-time re-installer for the web globals the build replaced with stubs.
307+
// Serialized into the blob and called by the generated snapshot-restore entry AFTER
308+
// globalThis.__RUNTIME_REQUIRE is set. Without it globalThis.fetch (etc.) stays the
309+
// build-time WebGlobalStub in the live process. The fetch family comes from the
310+
// app's real \`undici\` (kept external, so it loads for real at restore — resolved
311+
// directly, or through \`urllib\` for pnpm layouts where undici is not hoisted);
312+
// File/Blob come from \`node:buffer\`. Each becomes a lazy accessor so the real
313+
// module only loads on first access.
314+
var __UNDICI_GLOBALS = ${undiciGlobalsJson};
315+
var __BUFFER_GLOBALS = ${bufferGlobalsJson};
316+
globalThis.__installWebGlobalsLazy = function () {
317+
var rt = globalThis.__RUNTIME_REQUIRE;
318+
var getBuiltin = function (id) {
319+
try { return process.getBuiltinModule(id); } catch (e) { return typeof rt === 'function' ? rt(id) : undefined; }
320+
};
321+
var __undici;
322+
var __undiciTried = false;
323+
var loadUndici = function () {
324+
if (__undiciTried) return __undici;
325+
__undiciTried = true;
326+
if (typeof rt !== 'function') return (__undici = undefined);
327+
try { __undici = rt('undici'); return __undici; } catch (e) {}
328+
try {
329+
var mod = getBuiltin('node:module');
330+
if (mod && typeof rt.resolve === 'function') __undici = mod.createRequire(rt.resolve('urllib'))('undici');
331+
} catch (e2) { __undici = undefined; }
332+
return __undici;
333+
};
334+
var loadBuffer = function () { return getBuiltin('node:buffer'); };
335+
var install = function (name, getSource) {
336+
// Replace the build stub (a function named WebGlobalStub) or an absent slot;
337+
// never clobber a non-configurable global or a genuine value.
338+
var existing = Object.getOwnPropertyDescriptor(globalThis, name);
339+
if (existing) {
340+
if (existing.configurable === false) return;
341+
if (!('value' in existing)) return;
342+
var cur = existing.value;
343+
if (cur !== undefined && !(cur && cur.name === 'WebGlobalStub')) return;
344+
}
345+
var define = function (value) {
346+
Object.defineProperty(globalThis, name, { value: value, writable: true, enumerable: false, configurable: true });
347+
return value;
348+
};
349+
Object.defineProperty(globalThis, name, {
350+
configurable: true,
351+
enumerable: false,
352+
get: function () {
353+
var value;
354+
try { var src = getSource(); value = src ? src[name] : undefined; } catch (e) { value = undefined; }
355+
// Do not cache undefined: the source module may still be loading when this
356+
// fires re-entrantly (undici reads globalThis.Headers while its own require()
357+
// is in flight), so leave the accessor in place for a later read to resolve.
358+
if (value !== undefined) return define(value);
359+
return undefined;
360+
},
361+
set: function (value) { define(value); }
362+
});
363+
};
364+
for (var __u = 0; __u < __UNDICI_GLOBALS.length; __u++) install(__UNDICI_GLOBALS[__u], loadUndici);
365+
for (var __b = 0; __b < __BUFFER_GLOBALS.length; __b++) install(__BUFFER_GLOBALS[__b], loadBuffer);
366+
};
321367
})();
322368
`;
323369
}

tools/egg-bundler/test/EntryGenerator.test.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -295,6 +295,9 @@ describe('EntryGenerator', () => {
295295
expect(worker).toContain("(0, eval)('require')('node:module')");
296296
expect(worker).toContain('globalThis.__RUNTIME_REQUIRE =');
297297
expect(worker).toContain('globalThis.__EGG_MODULE_IMPORTER__ = async (fp: string) => __req(fp)');
298+
// re-install the web globals (fetch/Headers/.../Blob/File) the prelude stubbed
299+
expect(worker).toContain('globalThis.__installWebGlobalsLazy?.()');
300+
expect(worker).toContain('__runtimeRequire.resolve =');
298301
expect(worker).toContain('app.triggerSnapshotDidDeserialize()');
299302
// daemon readiness over IPC for `egg-scripts start --snapshot-blob`
300303
expect(worker).toContain("process.send({ action: 'egg-ready'");

tools/egg-bundler/test/__snapshots__/EntryGenerator.worker.canonical.snap

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -267,9 +267,19 @@ if (process.env.EGG_BUNDLE_SNAPSHOT === 'build') {
267267
? process.getBuiltinModule('node:module')
268268
: (0, eval)('require')('node:module');
269269
const __req = createRequire(__outputDir + '/');
270-
globalThis.__RUNTIME_REQUIRE = (id: string) => __req(id);
270+
const __runtimeRequire: any = (id: string) => __req(id);
271+
// Expose resolve so the web-globals re-installer can locate undici through the
272+
// app dependency tree (e.g. via urllib under pnpm).
273+
__runtimeRequire.resolve = (id: string, options?: any) => __req.resolve(id, options);
274+
globalThis.__RUNTIME_REQUIRE = __runtimeRequire;
271275
globalThis.__EGG_MODULE_IMPORTER__ = async (fp: string) => __req(fp);
272276

277+
// Re-install the web globals (fetch/Headers/.../Blob/File) the snapshot prelude
278+
// replaced with stubs at build time, so an app using globalThis.fetch keeps
279+
// working after a restore. They become lazy accessors backed by undici (the
280+
// fetch family) and node:buffer (Blob/File), loaded on first use.
281+
globalThis.__installWebGlobalsLazy?.();
282+
273283
(async () => {
274284
if (app.agent) {
275285
await app.agent.triggerSnapshotDidDeserialize();

tools/egg-bundler/test/snapshot-lazy-external.test.ts

Lines changed: 100 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -91,10 +91,15 @@ describe('snapshot lazy-external', () => {
9191
}
9292
});
9393

94-
it('stubs node:buffer File/Blob (which would otherwise pull in undici)', () => {
94+
it('defines __installWebGlobalsLazy sourcing the fetch family from undici and File/Blob from node:buffer', () => {
9595
const prelude = renderSnapshotPrelude();
96-
expect(prelude).toContain("process.getBuiltinModule('node:buffer')");
97-
expect(prelude).toContain('["File","Blob"]');
96+
expect(prelude).toContain('globalThis.__installWebGlobalsLazy = function');
97+
expect(prelude).toContain("rt('undici')");
98+
// pnpm fallback: resolve undici through urllib (egg's direct dependency).
99+
expect(prelude).toContain("rt.resolve('urllib')");
100+
expect(prelude).toContain("getBuiltin('node:buffer')");
101+
// node:buffer is NOT stubbed: File/Blob serialize fine and are re-installed from it.
102+
expect(prelude).not.toContain('BufStub');
98103
});
99104

100105
it('installs __LAZY_EXT with every lazy id and the __makeLazyExt factory', () => {
@@ -258,4 +263,96 @@ describe('snapshot lazy-external', () => {
258263
expect('prototype' in tls).toBe(true);
259264
});
260265
});
266+
267+
describe('runtime __installWebGlobalsLazy behavior (prelude evaluated in a vm)', () => {
268+
// The vm sandbox has no `process`, so the installer's getBuiltin falls back to
269+
// __RUNTIME_REQUIRE — which the tests supply, standing in for node:buffer/undici.
270+
function makeRestoreContext() {
271+
const sandbox: Record<string, any> = {};
272+
vm.createContext(sandbox);
273+
vm.runInContext(renderSnapshotPrelude(['http']), sandbox);
274+
return sandbox;
275+
}
276+
277+
function makeFakeUndici() {
278+
return {
279+
fetch: () => 'fetched',
280+
Headers: class Headers {},
281+
Request: class Request {},
282+
Response: class Response {},
283+
FormData: class FormData {},
284+
WebSocket: class WebSocket {},
285+
EventSource: class EventSource {},
286+
MessageEvent: class MessageEvent {},
287+
CloseEvent: class CloseEvent {},
288+
};
289+
}
290+
291+
it('re-installs the fetch family from undici and File/Blob from node:buffer, lazily', () => {
292+
const sandbox = makeRestoreContext();
293+
const fakeUndici = makeFakeUndici();
294+
const fakeBuffer = { File: class File {}, Blob: class Blob {} };
295+
let undiciLoads = 0;
296+
sandbox.__RUNTIME_REQUIRE = (id: string) => {
297+
if (id === 'undici') {
298+
undiciLoads++;
299+
return fakeUndici;
300+
}
301+
if (id === 'node:buffer') return fakeBuffer;
302+
return undefined;
303+
};
304+
305+
sandbox.__installWebGlobalsLazy();
306+
307+
expect(undiciLoads).toBe(0); // lazy: undici not required until first access
308+
expect(sandbox.fetch).toBe(fakeUndici.fetch);
309+
expect(undiciLoads).toBe(1);
310+
expect(sandbox.Headers).toBe(fakeUndici.Headers);
311+
void sandbox.Request;
312+
expect(undiciLoads).toBe(1); // cached
313+
expect(sandbox.Blob).toBe(fakeBuffer.Blob);
314+
expect(sandbox.File).toBe(fakeBuffer.File);
315+
});
316+
317+
it('resolves a web global accessed re-entrantly while undici is still loading', () => {
318+
const sandbox = makeRestoreContext();
319+
const FakeHeaders = class Headers {};
320+
sandbox.__RUNTIME_REQUIRE = (id: string) => {
321+
if (id === 'undici') {
322+
void sandbox.Headers; // re-entrant access mid-load -> getSource() is undefined
323+
return { fetch: () => 'F', Headers: FakeHeaders };
324+
}
325+
return undefined;
326+
};
327+
328+
sandbox.__installWebGlobalsLazy();
329+
330+
expect(typeof sandbox.fetch).toBe('function'); // triggers the re-entrant load
331+
expect(sandbox.Headers).toBe(FakeHeaders); // resolved once undici finished
332+
});
333+
334+
it('replaces the build-time WebGlobalStub but keeps a genuine value', () => {
335+
const sandbox = makeRestoreContext();
336+
// A genuine value (not a WebGlobalStub) must be preserved.
337+
const genuine = function userHeaders() {};
338+
sandbox.Headers = genuine;
339+
sandbox.__RUNTIME_REQUIRE = (id: string) =>
340+
id === 'undici' ? makeFakeUndici() : id === 'node:buffer' ? { File: class {}, Blob: class {} } : undefined;
341+
342+
sandbox.__installWebGlobalsLazy();
343+
344+
expect(sandbox.Headers).toBe(genuine); // kept
345+
expect(sandbox.fetch()).toBe('fetched'); // the WebGlobalStub was replaced
346+
});
347+
348+
it('skips a non-configurable global instead of throwing', () => {
349+
const sandbox = makeRestoreContext();
350+
const frozen = function frozenFetch() {};
351+
Object.defineProperty(sandbox, 'fetch', { value: frozen, configurable: false, writable: false });
352+
sandbox.__RUNTIME_REQUIRE = (id: string) => (id === 'undici' ? makeFakeUndici() : undefined);
353+
354+
expect(() => sandbox.__installWebGlobalsLazy()).not.toThrow();
355+
expect(sandbox.fetch).toBe(frozen);
356+
});
357+
});
261358
});

0 commit comments

Comments
 (0)