diff --git a/changelog.d/fixed-iterator-polyfill-followups.md b/changelog.d/fixed-iterator-polyfill-followups.md new file mode 100644 index 0000000000..7d6ca38c0c --- /dev/null +++ b/changelog.d/fixed-iterator-polyfill-followups.md @@ -0,0 +1 @@ +Fixed the Node 20 `Iterator` polyfill so vitest passes again on the prow image: the polyfill is now mirrored into the vm contexts jsdom 30 creates per window, and `NODE_OPTIONS` is rewritten to the polyfill's absolute path so child `node` processes spawned by script tests from tmpdir cwds no longer die with MODULE_NOT_FOUND. diff --git a/scripts/node-iterator-polyfill.cjs b/scripts/node-iterator-polyfill.cjs index 4c218ba25c..23f514b245 100644 --- a/scripts/node-iterator-polyfill.cjs +++ b/scripts/node-iterator-polyfill.cjs @@ -13,4 +13,49 @@ // prototype methods; on Node >= 22 the native implementation is kept. if (typeof globalThis.Iterator === 'undefined') { require('core-js/actual/iterator') + + // jsdom >= 30 also reads every JS global out of the fresh vm context it + // creates per window (lib/jsdom/browser/Window.js: vm.runInContext(name, + // window) for each host global it found, which now includes our polyfilled + // `Iterator`). A fresh context on Node 20 has no native Iterator, so window + // creation throws "ReferenceError: Iterator is not defined" from inside + // runInContext. Mirror the polyfilled Iterator into every new context. + const vm = require('vm') + const originalCreateContext = vm.createContext + vm.createContext = function (...args) { + const context = originalCreateContext.apply(this, args) + // createContext returns the contextified object (or the context's global + // object with DONT_CONTEXTIFY); property sets on it reflect as globals + // inside the context. + const globalObject = + context !== null && typeof context === 'object' ? context : args[0] + try { + if ( + globalObject && + vm.runInContext('typeof Iterator', globalObject) === 'undefined' + ) { + globalObject.Iterator = globalThis.Iterator + } + } catch { + // Non-contextified or frozen targets: leave untouched. + } + return context + } +} + +// The `test` script preloads this file with a cwd-relative path +// (`--require ./scripts/node-iterator-polyfill.cjs`). Node resolves that +// against the cwd of EACH process, so a child `node` process a test spawns +// with a tmpdir cwd dies at startup with MODULE_NOT_FOUND before running +// anything. Rewrite NODE_OPTIONS to this file's absolute path so inherited +// environments keep working from any cwd. +const RELATIVE_SPEC = './scripts/node-iterator-polyfill.cjs' +if ( + process.env.NODE_OPTIONS && + process.env.NODE_OPTIONS.includes(RELATIVE_SPEC) && + !__filename.includes(' ') +) { + process.env.NODE_OPTIONS = process.env.NODE_OPTIONS + .split(RELATIVE_SPEC) + .join(__filename) }