Skip to content

Commit b5aec03

Browse files
authored
chore(resolver): reuse cached lookup of package.json files (#11969)
1 parent 696c472 commit b5aec03

10 files changed

+153
-111
lines changed

e2e/__tests__/__snapshots__/moduleNameMapper.test.ts.snap

+2-2
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ FAIL __tests__/index.js
4141
12 | module.exports = () => 'test';
4242
13 |
4343
44-
at createNoMappedModuleFoundError (../../packages/jest-resolve/build/resolver.js:577:17)
44+
at createNoMappedModuleFoundError (../../packages/jest-resolve/build/resolver.js:579:17)
4545
at Object.require (index.js:10:1)
4646
`;
4747

@@ -70,6 +70,6 @@ FAIL __tests__/index.js
7070
12 | module.exports = () => 'test';
7171
13 |
7272
73-
at createNoMappedModuleFoundError (../../packages/jest-resolve/build/resolver.js:577:17)
73+
at createNoMappedModuleFoundError (../../packages/jest-resolve/build/resolver.js:579:17)
7474
at Object.require (index.js:10:1)
7575
`;

e2e/__tests__/__snapshots__/resolveNoFileExtensions.test.ts.snap

+1-1
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,6 @@ FAIL __tests__/test.js
3737
| ^
3838
9 |
3939
40-
at Resolver.resolveModule (../../packages/jest-resolve/build/resolver.js:322:11)
40+
at Resolver.resolveModule (../../packages/jest-resolve/build/resolver.js:324:11)
4141
at Object.require (index.js:8:18)
4242
`;

packages/jest-core/src/runJest.ts

+5
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919
import type TestSequencer from '@jest/test-sequencer';
2020
import type {Config} from '@jest/types';
2121
import type {ChangedFiles, ChangedFilesPromise} from 'jest-changed-files';
22+
import Resolver from 'jest-resolve';
2223
import type {Context} from 'jest-runtime';
2324
import {requireOrImportModule, tryRealpath} from 'jest-util';
2425
import {JestHook, JestHookEmitter} from 'jest-watcher';
@@ -142,6 +143,10 @@ export default async function runJest({
142143
failedTestsCache?: FailedTestsCache;
143144
filter?: Filter;
144145
}): Promise<void> {
146+
// Clear cache for required modules - there might be different resolutions
147+
// from Jest's config loading to running the tests
148+
Resolver.clearDefaultResolverCache();
149+
145150
const Sequencer: typeof TestSequencer = await requireOrImportModule(
146151
globalConfig.testSequencer,
147152
);

packages/jest-core/src/watch.ts

-3
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ import type {
1616
default as HasteMap,
1717
} from 'jest-haste-map';
1818
import {formatExecError} from 'jest-message-util';
19-
import Resolver from 'jest-resolve';
2019
import type {Context} from 'jest-runtime';
2120
import {
2221
isInteractive,
@@ -294,8 +293,6 @@ export default async function watch(
294293
isRunning = true;
295294
const configs = contexts.map(context => context.config);
296295
const changedFilesPromise = getChangedFilesPromise(globalConfig, configs);
297-
// Clear cache for required modules
298-
Resolver.clearDefaultResolverCache();
299296

300297
return runJest({
301298
changedFilesPromise,

packages/jest-resolve/package.json

-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616
"dependencies": {
1717
"@jest/types": "^27.2.5",
1818
"chalk": "^4.0.0",
19-
"escalade": "^3.1.1",
2019
"graceful-fs": "^4.2.4",
2120
"jest-haste-map": "^27.2.5",
2221
"jest-pnp-resolver": "^1.2.2",

packages/jest-resolve/src/defaultResolver.ts

+7-91
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,16 @@
55
* LICENSE file in the root directory of this source tree.
66
*/
77

8-
import * as fs from 'graceful-fs';
98
import pnpResolver from 'jest-pnp-resolver';
109
import {Opts as ResolveOpts, sync as resolveSync} from 'resolve';
1110
import type {Config} from '@jest/types';
12-
import {tryRealpath} from 'jest-util';
11+
import {
12+
PkgJson,
13+
isDirectory,
14+
isFile,
15+
readPackageCached,
16+
realpathSync,
17+
} from './fileWalkers';
1318

1419
interface ResolverOptions extends ResolveOpts {
1520
basedir: Config.Path;
@@ -53,98 +58,9 @@ export default function defaultResolver(
5358
return realpathSync(result);
5459
}
5560

56-
export function clearDefaultResolverCache(): void {
57-
checkedPaths.clear();
58-
checkedRealpathPaths.clear();
59-
packageContents.clear();
60-
}
61-
62-
enum IPathType {
63-
FILE = 1,
64-
DIRECTORY = 2,
65-
OTHER = 3,
66-
}
67-
const checkedPaths = new Map<string, IPathType>();
68-
function statSyncCached(path: string): IPathType {
69-
const result = checkedPaths.get(path);
70-
if (result !== undefined) {
71-
return result;
72-
}
73-
74-
let stat;
75-
try {
76-
stat = fs.statSync(path);
77-
} catch (e: any) {
78-
if (!(e && (e.code === 'ENOENT' || e.code === 'ENOTDIR'))) {
79-
throw e;
80-
}
81-
}
82-
83-
if (stat) {
84-
if (stat.isFile() || stat.isFIFO()) {
85-
checkedPaths.set(path, IPathType.FILE);
86-
return IPathType.FILE;
87-
} else if (stat.isDirectory()) {
88-
checkedPaths.set(path, IPathType.DIRECTORY);
89-
return IPathType.DIRECTORY;
90-
}
91-
}
92-
93-
checkedPaths.set(path, IPathType.OTHER);
94-
return IPathType.OTHER;
95-
}
96-
97-
const checkedRealpathPaths = new Map<string, string>();
98-
function realpathCached(path: Config.Path): Config.Path {
99-
let result = checkedRealpathPaths.get(path);
100-
101-
if (result !== undefined) {
102-
return result;
103-
}
104-
105-
result = tryRealpath(path);
106-
107-
checkedRealpathPaths.set(path, result);
108-
109-
if (path !== result) {
110-
// also cache the result in case it's ever referenced directly - no reason to `realpath` that as well
111-
checkedRealpathPaths.set(result, result);
112-
}
113-
114-
return result;
115-
}
116-
117-
type PkgJson = Record<string, unknown>;
118-
119-
const packageContents = new Map<string, PkgJson>();
120-
function readPackageCached(path: Config.Path): PkgJson {
121-
let result = packageContents.get(path);
122-
123-
if (result !== undefined) {
124-
return result;
125-
}
126-
127-
result = JSON.parse(fs.readFileSync(path, 'utf8')) as PkgJson;
128-
129-
packageContents.set(path, result);
130-
131-
return result;
132-
}
133-
13461
/*
13562
* helper functions
13663
*/
137-
function isFile(file: Config.Path): boolean {
138-
return statSyncCached(file) === IPathType.FILE;
139-
}
140-
141-
function isDirectory(dir: Config.Path): boolean {
142-
return statSyncCached(dir) === IPathType.DIRECTORY;
143-
}
144-
145-
function realpathSync(file: Config.Path): Config.Path {
146-
return realpathCached(file);
147-
}
14864

14965
function readPackageSync(_: unknown, file: Config.Path): PkgJson {
15066
return readPackageCached(file);
+132
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
/**
2+
* Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
import {dirname, resolve} from 'path';
9+
import * as fs from 'graceful-fs';
10+
import type {Config} from '@jest/types';
11+
import {tryRealpath} from 'jest-util';
12+
13+
export function clearFsCache(): void {
14+
checkedPaths.clear();
15+
checkedRealpathPaths.clear();
16+
packageContents.clear();
17+
}
18+
19+
enum IPathType {
20+
FILE = 1,
21+
DIRECTORY = 2,
22+
OTHER = 3,
23+
}
24+
const checkedPaths = new Map<string, IPathType>();
25+
function statSyncCached(path: string): IPathType {
26+
const result = checkedPaths.get(path);
27+
if (result != null) {
28+
return result;
29+
}
30+
31+
let stat;
32+
try {
33+
stat = fs.statSync(path);
34+
} catch (e: any) {
35+
if (!(e && (e.code === 'ENOENT' || e.code === 'ENOTDIR'))) {
36+
throw e;
37+
}
38+
}
39+
40+
if (stat) {
41+
if (stat.isFile() || stat.isFIFO()) {
42+
checkedPaths.set(path, IPathType.FILE);
43+
return IPathType.FILE;
44+
} else if (stat.isDirectory()) {
45+
checkedPaths.set(path, IPathType.DIRECTORY);
46+
return IPathType.DIRECTORY;
47+
}
48+
}
49+
50+
checkedPaths.set(path, IPathType.OTHER);
51+
return IPathType.OTHER;
52+
}
53+
54+
const checkedRealpathPaths = new Map<string, string>();
55+
function realpathCached(path: Config.Path): Config.Path {
56+
let result = checkedRealpathPaths.get(path);
57+
58+
if (result != null) {
59+
return result;
60+
}
61+
62+
result = tryRealpath(path);
63+
64+
checkedRealpathPaths.set(path, result);
65+
66+
if (path !== result) {
67+
// also cache the result in case it's ever referenced directly - no reason to `realpath` that as well
68+
checkedRealpathPaths.set(result, result);
69+
}
70+
71+
return result;
72+
}
73+
74+
export type PkgJson = Record<string, unknown>;
75+
76+
const packageContents = new Map<string, PkgJson>();
77+
export function readPackageCached(path: Config.Path): PkgJson {
78+
let result = packageContents.get(path);
79+
80+
if (result != null) {
81+
return result;
82+
}
83+
84+
result = JSON.parse(fs.readFileSync(path, 'utf8')) as PkgJson;
85+
86+
packageContents.set(path, result);
87+
88+
return result;
89+
}
90+
91+
// adapted from
92+
// https://github.com/lukeed/escalade/blob/2477005062cdbd8407afc90d3f48f4930354252b/src/sync.js
93+
// to use cached `fs` calls
94+
export function findClosestPackageJson(
95+
start: Config.Path,
96+
): Config.Path | undefined {
97+
let dir = resolve('.', start);
98+
if (!isDirectory(dir)) {
99+
dir = dirname(dir);
100+
}
101+
102+
while (true) {
103+
const pkgJsonFile = resolve(dir, './package.json');
104+
const hasPackageJson = isFile(pkgJsonFile);
105+
106+
if (hasPackageJson) {
107+
return pkgJsonFile;
108+
}
109+
110+
const prevDir = dir;
111+
dir = dirname(dir);
112+
113+
if (prevDir === dir) {
114+
return undefined;
115+
}
116+
}
117+
}
118+
119+
/*
120+
* helper functions
121+
*/
122+
export function isFile(file: Config.Path): boolean {
123+
return statSyncCached(file) === IPathType.FILE;
124+
}
125+
126+
export function isDirectory(dir: Config.Path): boolean {
127+
return statSyncCached(dir) === IPathType.DIRECTORY;
128+
}
129+
130+
export function realpathSync(file: Config.Path): Config.Path {
131+
return realpathCached(file);
132+
}

packages/jest-resolve/src/resolver.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@ import type {Config} from '@jest/types';
1414
import type {IModuleMap} from 'jest-haste-map';
1515
import {tryRealpath} from 'jest-util';
1616
import ModuleNotFoundError from './ModuleNotFoundError';
17-
import defaultResolver, {clearDefaultResolverCache} from './defaultResolver';
17+
import defaultResolver from './defaultResolver';
18+
import {clearFsCache} from './fileWalkers';
1819
import isBuiltinModule from './isBuiltinModule';
1920
import nodeModulesPaths from './nodeModulesPaths';
2021
import shouldLoadAsEsm, {clearCachedLookups} from './shouldLoadAsEsm';
@@ -98,7 +99,7 @@ export default class Resolver {
9899
}
99100

100101
static clearDefaultResolverCache(): void {
101-
clearDefaultResolverCache();
102+
clearFsCache();
102103
clearCachedLookups();
103104
}
104105

packages/jest-resolve/src/shouldLoadAsEsm.ts

+3-10
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,8 @@
88
import {dirname, extname} from 'path';
99
// @ts-expect-error: experimental, not added to the types
1010
import {SyntheticModule} from 'vm';
11-
import escalade from 'escalade/sync';
12-
import {readFileSync} from 'graceful-fs';
1311
import type {Config} from '@jest/types';
12+
import {findClosestPackageJson, readPackageCached} from './fileWalkers';
1413

1514
const runtimeSupportsVmModules = typeof SyntheticModule === 'function';
1615

@@ -74,13 +73,7 @@ function shouldLoadAsEsm(
7473
}
7574

7675
function cachedPkgCheck(cwd: Config.Path): boolean {
77-
const pkgPath = escalade(cwd, (_dir, names) => {
78-
if (names.includes('package.json')) {
79-
// will be resolved into absolute
80-
return 'package.json';
81-
}
82-
return false;
83-
});
76+
const pkgPath = findClosestPackageJson(cwd);
8477
if (!pkgPath) {
8578
return false;
8679
}
@@ -91,7 +84,7 @@ function cachedPkgCheck(cwd: Config.Path): boolean {
9184
}
9285

9386
try {
94-
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
87+
const pkg = readPackageCached(pkgPath);
9588
hasModuleField = pkg.type === 'module';
9689
} catch {
9790
hasModuleField = false;

yarn.lock

-1
Original file line numberDiff line numberDiff line change
@@ -13020,7 +13020,6 @@ fsevents@^1.2.7:
1302013020
"@types/graceful-fs": ^4.1.3
1302113021
"@types/resolve": ^1.20.0
1302213022
chalk: ^4.0.0
13023-
escalade: ^3.1.1
1302413023
graceful-fs: ^4.2.4
1302513024
jest-haste-map: ^27.2.5
1302613025
jest-pnp-resolver: ^1.2.2

0 commit comments

Comments
 (0)