Skip to content

Commit 937253b

Browse files
committed
jestjs#15638 Fix destruction of core nodejs modules that was introduced in jestjs#15215
1 parent 76632c6 commit 937253b

File tree

10 files changed

+113
-23
lines changed

10 files changed

+113
-23
lines changed

.github/workflows/test-nightly.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ jobs:
3232
- name: Get number of CPU cores
3333
id: cpu-cores
3434
uses: SimenB/github-actions-cpu-cores@97ba232459a8e02ff6121db9362b09661c875ab8 # v2.0.0
35+
- name: run node-env tests
36+
run: yarn test-node-env
3537
- name: run tests
3638
uses: nick-fields/retry@ce71cc2ab81d554ebbe88c79ab5975992d79ba08 # v3.0.2
3739
with:

.github/workflows/test.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ jobs:
3535
- name: Get number of CPU cores
3636
id: cpu-cores
3737
uses: SimenB/github-actions-cpu-cores@97ba232459a8e02ff6121db9362b09661c875ab8 # v2.0.0
38+
- name: run node-env tests
39+
run: yarn test-node-env
3840
- name: run tests
3941
uses: nick-fields/retry@ce71cc2ab81d554ebbe88c79ab5975992d79ba08 # v3.0.2
4042
with:

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@
112112
"test-ts": "yarn jest --config jest.config.ts.mjs",
113113
"test-types": "yarn tstyche",
114114
"test-with-type-info": "yarn jest e2e/__tests__/jest.config.ts.test.ts",
115+
"test-node-env": "yarn jest packages/jest-environment-node/src/__tests__",
115116
"test": "yarn lint && yarn jest",
116117
"typecheck": "yarn typecheck:examples && yarn typecheck:tests",
117118
"typecheck:examples": "tsc -p examples/expect-extend && tsc -p examples/typescript",

packages/jest-environment-node/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,8 @@
2727
"jest-util": "workspace:*"
2828
},
2929
"devDependencies": {
30-
"@jest/test-utils": "workspace:*"
30+
"@jest/test-utils": "workspace:*",
31+
"clsx": "^2.0.0"
3132
},
3233
"engines": {
3334
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"

packages/jest-environment-node/src/__tests__/node_environment.test.ts

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,14 @@
66
*/
77

88
import type {EnvironmentContext} from '@jest/environment';
9-
import {makeGlobalConfig, makeProjectConfig} from '@jest/test-utils';
9+
import {
10+
makeGlobalConfig,
11+
makeProjectConfig,
12+
onNodeVersions,
13+
} from '@jest/test-utils';
1014
import NodeEnvironment from '../';
15+
import {AsyncLocalStorage, createHook} from 'async_hooks';
16+
import {clsx} from 'clsx';
1117

1218
const context: EnvironmentContext = {
1319
console,
@@ -91,4 +97,24 @@ describe('NodeEnvironment', () => {
9197
test('dispatch event', () => {
9298
new EventTarget().dispatchEvent(new Event('foo'));
9399
});
100+
101+
test('set modules on global', () => {
102+
(globalThis as any).async_hooks = require('async_hooks');
103+
(globalThis as any).AsyncLocalStorage =
104+
require('async_hooks').AsyncLocalStorage;
105+
(globalThis as any).createHook = require('async_hooks').createHook;
106+
(globalThis as any).clsx = require('clsx');
107+
expect(AsyncLocalStorage).toBeDefined();
108+
expect(clsx).toBeDefined();
109+
expect(createHook).toBeDefined();
110+
expect(createHook({})).toBeDefined();
111+
expect(clsx()).toBeDefined();
112+
});
113+
114+
onNodeVersions('>=19.8.0', () => {
115+
test('use static function from core module set on global', () => {
116+
expect(AsyncLocalStorage.snapshot).toBeDefined();
117+
expect(AsyncLocalStorage.snapshot()).toBeDefined();
118+
});
119+
});
94120
});

packages/jest-environment-node/src/__tests__/node_environment_2.test.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,32 @@
55
* LICENSE file in the root directory of this source tree.
66
*/
77

8+
import {AsyncLocalStorage, createHook} from 'async_hooks';
9+
import {clsx} from 'clsx';
10+
import {onNodeVersions} from '@jest/test-utils';
11+
812
describe('NodeEnvironment 2', () => {
913
test('dispatch event', () => {
1014
new EventTarget().dispatchEvent(new Event('foo'));
1115
});
16+
17+
test('set modules on global', () => {
18+
(globalThis as any).async_hooks = require('async_hooks');
19+
(globalThis as any).AsyncLocalStorage =
20+
require('async_hooks').AsyncLocalStorage;
21+
(globalThis as any).createHook = require('async_hooks').createHook;
22+
(globalThis as any).clsx = require('clsx');
23+
expect(AsyncLocalStorage).toBeDefined();
24+
expect(clsx).toBeDefined();
25+
expect(createHook).toBeDefined();
26+
expect(createHook({})).toBeDefined();
27+
expect(clsx()).toBeDefined();
28+
});
29+
30+
onNodeVersions('>=19.8.0', () => {
31+
test('use static function from core module set on global', () => {
32+
expect(AsyncLocalStorage.snapshot).toBeDefined();
33+
expect(AsyncLocalStorage.snapshot()).toBeDefined();
34+
});
35+
});
1236
});

packages/jest-environment-node/src/index.ts

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -270,20 +270,14 @@ class GlobalProxy implements ProxyHandler<typeof globalThis> {
270270
* 2. Properties protected by {@link #protectProperties}.
271271
*/
272272
clear(): void {
273-
for (const {property, value} of [
273+
for (const {value} of [
274274
...[...this.propertyToValue.entries()].map(([property, value]) => ({
275275
property,
276276
value,
277277
})),
278278
...this.leftovers,
279279
]) {
280-
/*
281-
* React Native's test setup invokes their custom `performance` property after env teardown.
282-
* Once they start using `protectProperties`, we can get rid of this.
283-
*/
284-
if (property !== 'performance') {
285-
deleteProperties(value);
286-
}
280+
deleteProperties(value);
287281
}
288282
this.propertyToValue.clear();
289283
this.leftovers = [];

packages/jest-runtime/src/index.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ import {
5555
deepCyclicCopy,
5656
invariant,
5757
isNonNullable,
58+
protectProperties,
5859
} from 'jest-util';
5960
import {
6061
createOutsideJestVmPath,
@@ -1767,7 +1768,9 @@ export default class Runtime {
17671768
return this._getMockedNativeModule();
17681769
}
17691770

1770-
return require(moduleName);
1771+
const coreModule = require(moduleName);
1772+
protectProperties(coreModule);
1773+
return coreModule;
17711774
}
17721775

17731776
private _importCoreModule(moduleName: string, context: VMContext) {

packages/jest-util/src/garbage-collection-utils.ts

Lines changed: 48 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
* LICENSE file in the root directory of this source tree.
66
*/
77

8-
const PROTECT_PROPERTY = Symbol.for('$$jest-protect-from-deletion');
8+
const PROTECT_SYMBOL = Symbol.for('$$jest-protect-from-deletion');
99

1010
/**
1111
* Deletes all the properties from the given value (if it's an object),
@@ -15,12 +15,13 @@ const PROTECT_PROPERTY = Symbol.for('$$jest-protect-from-deletion');
1515
*/
1616
export function deleteProperties(value: unknown): void {
1717
if (canDeleteProperties(value)) {
18-
const protectedProperties = Reflect.get(value, PROTECT_PROPERTY);
19-
if (!Array.isArray(protectedProperties) || protectedProperties.length > 0) {
20-
for (const key of Reflect.ownKeys(value)) {
21-
if (!protectedProperties?.includes(key)) {
22-
Reflect.deleteProperty(value, key);
23-
}
18+
const protectedKeys = getProtectedKeys(
19+
value,
20+
Reflect.get(value, PROTECT_SYMBOL),
21+
);
22+
for (const key of Reflect.ownKeys(value)) {
23+
if (!protectedKeys.includes(key) && key !== PROTECT_SYMBOL) {
24+
Reflect.deleteProperty(value, key);
2425
}
2526
}
2627
}
@@ -31,15 +32,38 @@ export function deleteProperties(value: unknown): void {
3132
*
3233
* @param value The given value.
3334
* @param properties If the array contains any property,
34-
* then only these properties will not be deleted; otherwise if the array is empty,
35-
* all properties will not be deleted.
35+
* then only these properties will be protected; otherwise if the array is empty,
36+
* all properties will be protected.
37+
* @param depth Determines how "deep" the protection should be.
38+
* A value of 0 means that only the top-most properties will be protected,
39+
* while a value larger than 0 means that deeper levels of nesting will be protected as well.
3640
*/
37-
export function protectProperties<T extends object>(
41+
export function protectProperties<T>(
3842
value: T,
3943
properties: Array<keyof T> = [],
44+
depth = 2,
4045
): boolean {
41-
if (canDeleteProperties(value)) {
42-
return Reflect.set(value, PROTECT_PROPERTY, properties);
46+
if (
47+
depth >= 0 &&
48+
canDeleteProperties(value) &&
49+
!Reflect.has(value, PROTECT_SYMBOL)
50+
) {
51+
const result = Reflect.set(value, PROTECT_SYMBOL, properties);
52+
for (const key of getProtectedKeys(value, properties)) {
53+
const originalDeprecationMode = process.noDeprecation;
54+
try {
55+
// Reflect.get may cause deprecation warnings, so we disable them temporarily
56+
process.noDeprecation = true;
57+
const nested = Reflect.get(value, key);
58+
protectProperties(nested, [], depth - 1);
59+
} catch {
60+
// Reflect.get might fail in certain edge-cases
61+
// Instead of failing the entire process, we will skip the property.
62+
} finally {
63+
process.noDeprecation = originalDeprecationMode;
64+
}
65+
}
66+
return result;
4367
}
4468
return false;
4569
}
@@ -57,3 +81,15 @@ export function canDeleteProperties(value: unknown): value is object {
5781

5882
return false;
5983
}
84+
85+
function getProtectedKeys<T extends object>(
86+
value: T,
87+
properties: Array<keyof T> | undefined,
88+
): Array<string | symbol | number> {
89+
if (properties === undefined) {
90+
return [];
91+
}
92+
const protectedKeys =
93+
properties.length > 0 ? properties : Reflect.ownKeys(value);
94+
return protectedKeys.filter(key => PROTECT_SYMBOL !== key);
95+
}

yarn.lock

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13869,6 +13869,7 @@ __metadata:
1386913869
"@jest/test-utils": "workspace:*"
1387013870
"@jest/types": "workspace:*"
1387113871
"@types/node": "*"
13872+
clsx: ^2.0.0
1387213873
jest-mock: "workspace:*"
1387313874
jest-util: "workspace:*"
1387413875
languageName: unknown

0 commit comments

Comments
 (0)