From b5aec031393f465b3513bd221d7ff6ab17a3e2d9 Mon Sep 17 00:00:00 2001 From: Simen Bekkhus Date: Sun, 17 Oct 2021 19:22:33 +0200 Subject: [PATCH] chore(resolver): reuse cached lookup of package.json files (#11969) --- .../moduleNameMapper.test.ts.snap | 4 +- .../resolveNoFileExtensions.test.ts.snap | 2 +- packages/jest-core/src/runJest.ts | 5 + packages/jest-core/src/watch.ts | 3 - packages/jest-resolve/package.json | 1 - packages/jest-resolve/src/defaultResolver.ts | 98 +------------ packages/jest-resolve/src/fileWalkers.ts | 132 ++++++++++++++++++ packages/jest-resolve/src/resolver.ts | 5 +- packages/jest-resolve/src/shouldLoadAsEsm.ts | 13 +- yarn.lock | 1 - 10 files changed, 153 insertions(+), 111 deletions(-) create mode 100644 packages/jest-resolve/src/fileWalkers.ts diff --git a/e2e/__tests__/__snapshots__/moduleNameMapper.test.ts.snap b/e2e/__tests__/__snapshots__/moduleNameMapper.test.ts.snap index c73d54ca2d2e..b2a3571bf7a8 100644 --- a/e2e/__tests__/__snapshots__/moduleNameMapper.test.ts.snap +++ b/e2e/__tests__/__snapshots__/moduleNameMapper.test.ts.snap @@ -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) `; @@ -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) `; diff --git a/e2e/__tests__/__snapshots__/resolveNoFileExtensions.test.ts.snap b/e2e/__tests__/__snapshots__/resolveNoFileExtensions.test.ts.snap index 0abfba5f3a54..46f5c889b68c 100644 --- a/e2e/__tests__/__snapshots__/resolveNoFileExtensions.test.ts.snap +++ b/e2e/__tests__/__snapshots__/resolveNoFileExtensions.test.ts.snap @@ -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) `; diff --git a/packages/jest-core/src/runJest.ts b/packages/jest-core/src/runJest.ts index 40d2970b22c9..636c28993385 100644 --- a/packages/jest-core/src/runJest.ts +++ b/packages/jest-core/src/runJest.ts @@ -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'; @@ -142,6 +143,10 @@ export default async function runJest({ failedTestsCache?: FailedTestsCache; filter?: Filter; }): Promise { + // 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, ); diff --git a/packages/jest-core/src/watch.ts b/packages/jest-core/src/watch.ts index f1ccf4fce949..3042630d9c04 100644 --- a/packages/jest-core/src/watch.ts +++ b/packages/jest-core/src/watch.ts @@ -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, @@ -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, diff --git a/packages/jest-resolve/package.json b/packages/jest-resolve/package.json index 5dbb38901a16..5da271e0508c 100644 --- a/packages/jest-resolve/package.json +++ b/packages/jest-resolve/package.json @@ -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", diff --git a/packages/jest-resolve/src/defaultResolver.ts b/packages/jest-resolve/src/defaultResolver.ts index 0100b5e6236f..bc0e5b64b5d9 100644 --- a/packages/jest-resolve/src/defaultResolver.ts +++ b/packages/jest-resolve/src/defaultResolver.ts @@ -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; @@ -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(); -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(); -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; - -const packageContents = new Map(); -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); diff --git a/packages/jest-resolve/src/fileWalkers.ts b/packages/jest-resolve/src/fileWalkers.ts new file mode 100644 index 000000000000..5834f3fad495 --- /dev/null +++ b/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(); +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(); +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; + +const packageContents = new Map(); +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); +} diff --git a/packages/jest-resolve/src/resolver.ts b/packages/jest-resolve/src/resolver.ts index f5c668b85b43..f21bf9a4570c 100644 --- a/packages/jest-resolve/src/resolver.ts +++ b/packages/jest-resolve/src/resolver.ts @@ -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'; @@ -98,7 +99,7 @@ export default class Resolver { } static clearDefaultResolverCache(): void { - clearDefaultResolverCache(); + clearFsCache(); clearCachedLookups(); } diff --git a/packages/jest-resolve/src/shouldLoadAsEsm.ts b/packages/jest-resolve/src/shouldLoadAsEsm.ts index 5f9759cca251..aadca23c4d01 100644 --- a/packages/jest-resolve/src/shouldLoadAsEsm.ts +++ b/packages/jest-resolve/src/shouldLoadAsEsm.ts @@ -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'; @@ -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; } @@ -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; diff --git a/yarn.lock b/yarn.lock index ffef44d4cd16..2bb1e0a824cb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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