Skip to content

Commit

Permalink
chore(resolver): reuse cached lookup of package.json files (#11969)
Browse files Browse the repository at this point in the history
  • Loading branch information
SimenB committed Oct 17, 2021
1 parent 696c472 commit b5aec03
Show file tree
Hide file tree
Showing 10 changed files with 153 additions and 111 deletions.
4 changes: 2 additions & 2 deletions e2e/__tests__/__snapshots__/moduleNameMapper.test.ts.snap
Expand Up @@ -41,7 +41,7 @@ FAIL __tests__/index.js
12 | module.exports = () => 'test';
13 |
at createNoMappedModuleFoundError (../../packages/jest-resolve/build/resolver.js:577:17)
at createNoMappedModuleFoundError (../../packages/jest-resolve/build/resolver.js:579:17)
at Object.require (index.js:10:1)
`;

Expand Down Expand Up @@ -70,6 +70,6 @@ FAIL __tests__/index.js
12 | module.exports = () => 'test';
13 |
at createNoMappedModuleFoundError (../../packages/jest-resolve/build/resolver.js:577:17)
at createNoMappedModuleFoundError (../../packages/jest-resolve/build/resolver.js:579:17)
at Object.require (index.js:10:1)
`;
Expand Up @@ -37,6 +37,6 @@ FAIL __tests__/test.js
| ^
9 |
at Resolver.resolveModule (../../packages/jest-resolve/build/resolver.js:322:11)
at Resolver.resolveModule (../../packages/jest-resolve/build/resolver.js:324:11)
at Object.require (index.js:8:18)
`;
5 changes: 5 additions & 0 deletions packages/jest-core/src/runJest.ts
Expand Up @@ -19,6 +19,7 @@ import {
import type TestSequencer from '@jest/test-sequencer';
import type {Config} from '@jest/types';
import type {ChangedFiles, ChangedFilesPromise} from 'jest-changed-files';
import Resolver from 'jest-resolve';
import type {Context} from 'jest-runtime';
import {requireOrImportModule, tryRealpath} from 'jest-util';
import {JestHook, JestHookEmitter} from 'jest-watcher';
Expand Down Expand Up @@ -142,6 +143,10 @@ export default async function runJest({
failedTestsCache?: FailedTestsCache;
filter?: Filter;
}): Promise<void> {
// Clear cache for required modules - there might be different resolutions
// from Jest's config loading to running the tests
Resolver.clearDefaultResolverCache();

const Sequencer: typeof TestSequencer = await requireOrImportModule(
globalConfig.testSequencer,
);
Expand Down
3 changes: 0 additions & 3 deletions packages/jest-core/src/watch.ts
Expand Up @@ -16,7 +16,6 @@ import type {
default as HasteMap,
} from 'jest-haste-map';
import {formatExecError} from 'jest-message-util';
import Resolver from 'jest-resolve';
import type {Context} from 'jest-runtime';
import {
isInteractive,
Expand Down Expand Up @@ -294,8 +293,6 @@ export default async function watch(
isRunning = true;
const configs = contexts.map(context => context.config);
const changedFilesPromise = getChangedFilesPromise(globalConfig, configs);
// Clear cache for required modules
Resolver.clearDefaultResolverCache();

return runJest({
changedFilesPromise,
Expand Down
1 change: 0 additions & 1 deletion packages/jest-resolve/package.json
Expand Up @@ -16,7 +16,6 @@
"dependencies": {
"@jest/types": "^27.2.5",
"chalk": "^4.0.0",
"escalade": "^3.1.1",
"graceful-fs": "^4.2.4",
"jest-haste-map": "^27.2.5",
"jest-pnp-resolver": "^1.2.2",
Expand Down
98 changes: 7 additions & 91 deletions packages/jest-resolve/src/defaultResolver.ts
Expand Up @@ -5,11 +5,16 @@
* LICENSE file in the root directory of this source tree.
*/

import * as fs from 'graceful-fs';
import pnpResolver from 'jest-pnp-resolver';
import {Opts as ResolveOpts, sync as resolveSync} from 'resolve';
import type {Config} from '@jest/types';
import {tryRealpath} from 'jest-util';
import {
PkgJson,
isDirectory,
isFile,
readPackageCached,
realpathSync,
} from './fileWalkers';

interface ResolverOptions extends ResolveOpts {
basedir: Config.Path;
Expand Down Expand Up @@ -53,98 +58,9 @@ export default function defaultResolver(
return realpathSync(result);
}

export function clearDefaultResolverCache(): void {
checkedPaths.clear();
checkedRealpathPaths.clear();
packageContents.clear();
}

enum IPathType {
FILE = 1,
DIRECTORY = 2,
OTHER = 3,
}
const checkedPaths = new Map<string, IPathType>();
function statSyncCached(path: string): IPathType {
const result = checkedPaths.get(path);
if (result !== undefined) {
return result;
}

let stat;
try {
stat = fs.statSync(path);
} catch (e: any) {
if (!(e && (e.code === 'ENOENT' || e.code === 'ENOTDIR'))) {
throw e;
}
}

if (stat) {
if (stat.isFile() || stat.isFIFO()) {
checkedPaths.set(path, IPathType.FILE);
return IPathType.FILE;
} else if (stat.isDirectory()) {
checkedPaths.set(path, IPathType.DIRECTORY);
return IPathType.DIRECTORY;
}
}

checkedPaths.set(path, IPathType.OTHER);
return IPathType.OTHER;
}

const checkedRealpathPaths = new Map<string, string>();
function realpathCached(path: Config.Path): Config.Path {
let result = checkedRealpathPaths.get(path);

if (result !== undefined) {
return result;
}

result = tryRealpath(path);

checkedRealpathPaths.set(path, result);

if (path !== result) {
// also cache the result in case it's ever referenced directly - no reason to `realpath` that as well
checkedRealpathPaths.set(result, result);
}

return result;
}

type PkgJson = Record<string, unknown>;

const packageContents = new Map<string, PkgJson>();
function readPackageCached(path: Config.Path): PkgJson {
let result = packageContents.get(path);

if (result !== undefined) {
return result;
}

result = JSON.parse(fs.readFileSync(path, 'utf8')) as PkgJson;

packageContents.set(path, result);

return result;
}

/*
* helper functions
*/
function isFile(file: Config.Path): boolean {
return statSyncCached(file) === IPathType.FILE;
}

function isDirectory(dir: Config.Path): boolean {
return statSyncCached(dir) === IPathType.DIRECTORY;
}

function realpathSync(file: Config.Path): Config.Path {
return realpathCached(file);
}

function readPackageSync(_: unknown, file: Config.Path): PkgJson {
return readPackageCached(file);
Expand Down
132 changes: 132 additions & 0 deletions packages/jest-resolve/src/fileWalkers.ts
@@ -0,0 +1,132 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

import {dirname, resolve} from 'path';
import * as fs from 'graceful-fs';
import type {Config} from '@jest/types';
import {tryRealpath} from 'jest-util';

export function clearFsCache(): void {
checkedPaths.clear();
checkedRealpathPaths.clear();
packageContents.clear();
}

enum IPathType {
FILE = 1,
DIRECTORY = 2,
OTHER = 3,
}
const checkedPaths = new Map<string, IPathType>();
function statSyncCached(path: string): IPathType {
const result = checkedPaths.get(path);
if (result != null) {
return result;
}

let stat;
try {
stat = fs.statSync(path);
} catch (e: any) {
if (!(e && (e.code === 'ENOENT' || e.code === 'ENOTDIR'))) {
throw e;
}
}

if (stat) {
if (stat.isFile() || stat.isFIFO()) {
checkedPaths.set(path, IPathType.FILE);
return IPathType.FILE;
} else if (stat.isDirectory()) {
checkedPaths.set(path, IPathType.DIRECTORY);
return IPathType.DIRECTORY;
}
}

checkedPaths.set(path, IPathType.OTHER);
return IPathType.OTHER;
}

const checkedRealpathPaths = new Map<string, string>();
function realpathCached(path: Config.Path): Config.Path {
let result = checkedRealpathPaths.get(path);

if (result != null) {
return result;
}

result = tryRealpath(path);

checkedRealpathPaths.set(path, result);

if (path !== result) {
// also cache the result in case it's ever referenced directly - no reason to `realpath` that as well
checkedRealpathPaths.set(result, result);
}

return result;
}

export type PkgJson = Record<string, unknown>;

const packageContents = new Map<string, PkgJson>();
export function readPackageCached(path: Config.Path): PkgJson {
let result = packageContents.get(path);

if (result != null) {
return result;
}

result = JSON.parse(fs.readFileSync(path, 'utf8')) as PkgJson;

packageContents.set(path, result);

return result;
}

// adapted from
// https://github.com/lukeed/escalade/blob/2477005062cdbd8407afc90d3f48f4930354252b/src/sync.js
// to use cached `fs` calls
export function findClosestPackageJson(
start: Config.Path,
): Config.Path | undefined {
let dir = resolve('.', start);
if (!isDirectory(dir)) {
dir = dirname(dir);
}

while (true) {
const pkgJsonFile = resolve(dir, './package.json');
const hasPackageJson = isFile(pkgJsonFile);

if (hasPackageJson) {
return pkgJsonFile;
}

const prevDir = dir;
dir = dirname(dir);

if (prevDir === dir) {
return undefined;
}
}
}

/*
* helper functions
*/
export function isFile(file: Config.Path): boolean {
return statSyncCached(file) === IPathType.FILE;
}

export function isDirectory(dir: Config.Path): boolean {
return statSyncCached(dir) === IPathType.DIRECTORY;
}

export function realpathSync(file: Config.Path): Config.Path {
return realpathCached(file);
}
5 changes: 3 additions & 2 deletions packages/jest-resolve/src/resolver.ts
Expand Up @@ -14,7 +14,8 @@ import type {Config} from '@jest/types';
import type {IModuleMap} from 'jest-haste-map';
import {tryRealpath} from 'jest-util';
import ModuleNotFoundError from './ModuleNotFoundError';
import defaultResolver, {clearDefaultResolverCache} from './defaultResolver';
import defaultResolver from './defaultResolver';
import {clearFsCache} from './fileWalkers';
import isBuiltinModule from './isBuiltinModule';
import nodeModulesPaths from './nodeModulesPaths';
import shouldLoadAsEsm, {clearCachedLookups} from './shouldLoadAsEsm';
Expand Down Expand Up @@ -98,7 +99,7 @@ export default class Resolver {
}

static clearDefaultResolverCache(): void {
clearDefaultResolverCache();
clearFsCache();
clearCachedLookups();
}

Expand Down
13 changes: 3 additions & 10 deletions packages/jest-resolve/src/shouldLoadAsEsm.ts
Expand Up @@ -8,9 +8,8 @@
import {dirname, extname} from 'path';
// @ts-expect-error: experimental, not added to the types
import {SyntheticModule} from 'vm';
import escalade from 'escalade/sync';
import {readFileSync} from 'graceful-fs';
import type {Config} from '@jest/types';
import {findClosestPackageJson, readPackageCached} from './fileWalkers';

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

Expand Down Expand Up @@ -74,13 +73,7 @@ function shouldLoadAsEsm(
}

function cachedPkgCheck(cwd: Config.Path): boolean {
const pkgPath = escalade(cwd, (_dir, names) => {
if (names.includes('package.json')) {
// will be resolved into absolute
return 'package.json';
}
return false;
});
const pkgPath = findClosestPackageJson(cwd);
if (!pkgPath) {
return false;
}
Expand All @@ -91,7 +84,7 @@ function cachedPkgCheck(cwd: Config.Path): boolean {
}

try {
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
const pkg = readPackageCached(pkgPath);
hasModuleField = pkg.type === 'module';
} catch {
hasModuleField = false;
Expand Down
1 change: 0 additions & 1 deletion yarn.lock
Expand Up @@ -13020,7 +13020,6 @@ fsevents@^1.2.7:
"@types/graceful-fs": ^4.1.3
"@types/resolve": ^1.20.0
chalk: ^4.0.0
escalade: ^3.1.1
graceful-fs: ^4.2.4
jest-haste-map: ^27.2.5
jest-pnp-resolver: ^1.2.2
Expand Down

0 comments on commit b5aec03

Please sign in to comment.