Skip to content

Commit 334edef

Browse files
authored
fix!: don't emit localStorage warnings on Node 26, fail gracefully when worker fails to start (#10293)
1 parent bd9cc9d commit 334edef

12 files changed

Lines changed: 77 additions & 15 deletions

File tree

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ jobs:
9999
strategy:
100100
matrix:
101101
os: [ubuntu-latest]
102-
node_version: [22, 24]
102+
node_version: [22, 24, 26]
103103
include:
104104
- os: macos-latest
105105
node_version: 24

docs/guide/environment.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -96,9 +96,9 @@ interface PopulateOptions {
9696
interface PopulateResult {
9797
// a list of all keys that were copied, even if value doesn't exist on original object
9898
keys: Set<string>
99-
// a map of original object that might have been overridden with keys
100-
// you can return these values inside `teardown` function
101-
originals: Map<string | symbol, any>
99+
// a map of property descriptors for keys that might have been overridden
100+
// you can restore them with `Object.defineProperty` inside `teardown`
101+
originals: Map<string | symbol, PropertyDescriptor>
102102
}
103103

104104
export function populateGlobal(global: any, original: any, options: PopulateOptions): PopulateResult

docs/guide/migration.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -328,6 +328,17 @@ $ cd subdir && vitest --config ../vitest.config.ts # [!code ++]
328328

329329
Assignments to properties on `globalThis` or `window` in `jsdom` and `happy-dom` environments are now propagated to the underlying DOM implementation. Mutable properties such as `innerWidth` can affect APIs implemented by the DOM environment, for example `happy-dom`'s `matchMedia`.
330330

331+
### `populateGlobal` Returns Descriptors in `originals`
332+
333+
The `originals` map returned by [`populateGlobal`](/guide/environment#custom-environment) now holds [property descriptors](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/getOwnPropertyDescriptor) instead of plain values. This avoids invoking native lazy getters (such as Node's `localStorage`) while capturing the original, and restores them faithfully on teardown.
334+
335+
If you restore them manually in a custom environment, use `Object.defineProperty` instead of an assignment:
336+
337+
```ts
338+
originals.forEach((value, key) => (global[key] = value)) // [!code --]
339+
originals.forEach((descriptor, key) => Object.defineProperty(global, key, descriptor)) // [!code ++]
340+
```
341+
331342
### Browser Orchestrator URL Requires a Session
332343

333344
Vitest no longer serves the browser orchestrator UI from a bare `/__vitest_test__/` URL. Browser runner URLs are now session-bound and must include the `sessionId` generated by Vitest, for example `/__vitest_test__/?sessionId=...`.

packages/vitest/src/integrations/env/edge-runtime.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ export default <Environment>{
4343
return {
4444
teardown(global) {
4545
keys.forEach(key => delete global[key])
46-
originals.forEach((v, k) => (global[k] = v))
46+
originals.forEach((d, k) => Object.defineProperty(global, k, d))
4747
},
4848
}
4949
},

packages/vitest/src/integrations/env/happy-dom.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ export default <Environment>{
8282
async teardown(global) {
8383
await teardownWindow(win)
8484
keys.forEach(key => delete global[key])
85-
originals.forEach((v, k) => (global[k] = v))
85+
originals.forEach((d, k) => Object.defineProperty(global, k, d))
8686
},
8787
}
8888
},

packages/vitest/src/integrations/env/jsdom-keys.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,7 @@ const OTHER_KEYS = [
209209
'innerHeight',
210210
'innerWidth',
211211
'length',
212+
'localStorage',
212213
'location',
213214
'matchMedia',
214215
'moveBy',
@@ -241,6 +242,7 @@ const OTHER_KEYS = [
241242
'scrollX',
242243
'scrollY',
243244
'self',
245+
'sessionStorage',
244246
/* 'setInterval', */
245247
/* 'setTimeout', */
246248
'stop',

packages/vitest/src/integrations/env/jsdom.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -225,7 +225,7 @@ export default <Environment>{
225225
dom.window.close()
226226
delete global.jsdom
227227
keys.forEach(key => delete global[key])
228-
originals.forEach((v, k) => (global[k] = v))
228+
originals.forEach((d, k) => Object.defineProperty(global, k, d))
229229
},
230230
}
231231
},

packages/vitest/src/integrations/env/utils.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,12 +45,12 @@ export function populateGlobal(
4545
): {
4646
keys: Set<string>
4747
skipKeys: string[]
48-
originals: Map<string | symbol, any>
48+
originals: Map<string | symbol, PropertyDescriptor>
4949
} {
5050
const { bindFunctions = false } = options
5151
const keys = getWindowKeys(global, win, options.additionalKeys)
5252

53-
const originals = new Map<string | symbol, any>()
53+
const originals = new Map<string | symbol, PropertyDescriptor>()
5454

5555
const overriddenKeys = new Set([...KEYS, ...options.additionalKeys || []])
5656

@@ -63,7 +63,12 @@ export function populateGlobal(
6363
&& win[key].bind(win)
6464

6565
if (overriddenKeys.has(key) && key in global) {
66-
originals.set(key, global[key])
66+
// capture the descriptor instead of the value to avoid invoking native
67+
// lazy getters such as Node's `localStorage`, which warns when accessed
68+
// without `--localstorage-file`
69+
const descriptor = Object.getOwnPropertyDescriptor(global, key)
70+
?? { value: global[key], configurable: true, writable: true, enumerable: true }
71+
originals.set(key, descriptor)
6772
}
6873

6974
Object.defineProperty(global, key, {

packages/vitest/src/node/pools/poolRunner.ts

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -384,9 +384,14 @@ export class PoolRunner {
384384

385385
private waitForStart() {
386386
return new Promise<void>((resolve, reject) => {
387-
const onStart = (message: WorkerResponse) => {
387+
const cleanup = () => {
388+
this.off('message', onStart)
389+
this.off('error', onError)
390+
}
391+
392+
function onStart(message: WorkerResponse) {
388393
if (message.type === 'started') {
389-
this.off('message', onStart)
394+
cleanup()
390395
if (message.error) {
391396
reject(message.error)
392397
}
@@ -396,7 +401,16 @@ export class PoolRunner {
396401
}
397402
}
398403

404+
// The worker can die before it ever reports back (e.g. an invalid
405+
// `execArgv` makes Node exit immediately). Reject as soon as that
406+
// happens instead of waiting for the start timeout to elapse.
407+
function onError(error: Error) {
408+
cleanup()
409+
reject(error)
410+
}
411+
399412
this.on('message', onStart)
413+
this.on('error', onError)
400414
})
401415
}
402416

test/e2e/test/no-module-runner.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -712,7 +712,7 @@ test('2+2=4', () => {
712712
`)
713713
})
714714

715-
test('with --experimental-transform-types is supported', async () => {
715+
test.runIf(process.allowedNodeEnvironmentFlags.has('--experimental-transform-types'))('with --experimental-transform-types is supported', async () => {
716716
const { errorTree } = await runNoViteModuleRunnerTests(
717717
{
718718
'add.cts': /* ts */`

0 commit comments

Comments
 (0)