diff --git a/CHANGELOG.md b/CHANGELOG.md index 6c0d96a39d41..c70e3106ed62 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,12 @@ ## main +### Features + +- `[jest-resolver]` Implement the `defaultAsyncResolver` ([#15679](https://github.com/jestjs/jest/pull/15679)) + ### Chore & Maintenance -- `[*]` Remove and deprecate `jest-repl` package ([15673](https://github.com/jestjs/jest/pull/15673)) +- `[*]` Remove and deprecate `jest-repl` package ([#15673](https://github.com/jestjs/jest/pull/15673)) ## 30.0.0 @@ -120,6 +124,7 @@ - `[*]` [**BREAKING**] Bundle all of Jest's modules into `index.js` ([#12348](https://github.com/jestjs/jest/pull/12348), [#14550](https://github.com/jestjs/jest/pull/14550) & [#14661](https://github.com/jestjs/jest/pull/14661)) - `[jest-haste-map]` Only spawn one process to check for `watchman` installation ([#14826](https://github.com/jestjs/jest/pull/14826)) - `[jest-runner]` Better cleanup `source-map-support` after test to resolve (minor) memory leak ([#15233](https://github.com/jestjs/jest/pull/15233)) +- `[jest-resolver]` Migrate `resolve` and `resolve.exports` to `unrs-resolver` ([#15619](https://github.com/jestjs/jest/pull/15619)) - `[jest-circus, jest-environment-node, jest-repl, jest-runner, jest-util]` Cleanup global variables on environment teardown to reduce memory leaks ([#15215](https://github.com/jestjs/jest/pull/15215) & [#15636](https://github.com/jestjs/jest/pull/15636) & [#15643](https://github.com/jestjs/jest/pull/15643)) ### Chore & Maintenance diff --git a/e2e/__tests__/__snapshots__/moduleNameMapper.test.ts.snap b/e2e/__tests__/__snapshots__/moduleNameMapper.test.ts.snap index 80065ab083fb..6e209e735703 100644 --- a/e2e/__tests__/__snapshots__/moduleNameMapper.test.ts.snap +++ b/e2e/__tests__/__snapshots__/moduleNameMapper.test.ts.snap @@ -41,7 +41,7 @@ exports[`moduleNameMapper wrong array configuration 1`] = ` 12 | module.exports = () => 'test'; 13 | - at createNoMappedModuleFoundError (../../packages/jest-resolve/build/index.js:1122:17) + at createNoMappedModuleFoundError (../../packages/jest-resolve/build/index.js:1129:17) at Object.require (index.js:10:1) at Object.require (__tests__/index.js:10:20)" `; @@ -71,7 +71,7 @@ exports[`moduleNameMapper wrong configuration 1`] = ` 12 | module.exports = () => 'test'; 13 | - at createNoMappedModuleFoundError (../../packages/jest-resolve/build/index.js:1122:17) + at createNoMappedModuleFoundError (../../packages/jest-resolve/build/index.js:1129:17) at Object.require (index.js:10:1) at Object.require (__tests__/index.js:10:20)" `; diff --git a/e2e/__tests__/__snapshots__/requireMissingExt.test.ts.snap b/e2e/__tests__/__snapshots__/requireMissingExt.test.ts.snap index 783e30a33ddc..9e8994d13f92 100644 --- a/e2e/__tests__/__snapshots__/requireMissingExt.test.ts.snap +++ b/e2e/__tests__/__snapshots__/requireMissingExt.test.ts.snap @@ -26,7 +26,7 @@ exports[`shows a proper error from deep requires 1`] = ` 12 | test('dummy', () => { 13 | expect(1).toBe(1); - at Resolver._throwModNotFoundError (../../packages/jest-resolve/build/index.js:868:11) + at Resolver._throwModNotFoundError (../../packages/jest-resolve/build/index.js:875:11) at Object. (node_modules/discord.js/src/index.js:21:12) at Object.require (__tests__/test.js:10:1)" `; diff --git a/e2e/__tests__/__snapshots__/resolveNoFileExtensions.test.ts.snap b/e2e/__tests__/__snapshots__/resolveNoFileExtensions.test.ts.snap index 90fadb428aa5..09d1d1400d46 100644 --- a/e2e/__tests__/__snapshots__/resolveNoFileExtensions.test.ts.snap +++ b/e2e/__tests__/__snapshots__/resolveNoFileExtensions.test.ts.snap @@ -37,7 +37,7 @@ exports[`show error message with matching files 1`] = ` | ^ 9 | - at Resolver._throwModNotFoundError (../../packages/jest-resolve/build/index.js:868:11) + at Resolver._throwModNotFoundError (../../packages/jest-resolve/build/index.js:875:11) at Object.require (index.js:8:18) at Object.require (__tests__/test.js:8:11)" `; diff --git a/packages/jest-resolve/src/__tests__/resolve.test.ts b/packages/jest-resolve/src/__tests__/resolve.test.ts index 81433f2c22d5..4400ce98bc58 100644 --- a/packages/jest-resolve/src/__tests__/resolve.test.ts +++ b/packages/jest-resolve/src/__tests__/resolve.test.ts @@ -13,7 +13,7 @@ import {pathToFileURL} from 'url'; import userResolver from '../__mocks__/userResolver'; import userResolverAsync from '../__mocks__/userResolverAsync'; -import defaultResolver from '../defaultResolver'; +import defaultResolver, {defaultAsyncResolver} from '../defaultResolver'; import nodeModulesPaths from '../nodeModulesPaths'; import Resolver from '../resolver'; import type {ResolverConfig} from '../types'; @@ -109,6 +109,7 @@ describe('findNodeModule', () => { expect(mockUserResolver.mock.calls[0][1]).toStrictEqual({ basedir: '/', conditions: ['conditions, woooo'], + defaultAsyncResolver, defaultResolver, extensions: ['js'], moduleDirectory: ['node_modules'], @@ -404,6 +405,7 @@ describe('findNodeModuleAsync', () => { expect(mockUserResolverAsync.async.mock.calls[0][1]).toStrictEqual({ basedir: '/', conditions: ['conditions, woooo'], + defaultAsyncResolver, defaultResolver, extensions: ['js'], moduleDirectory: ['node_modules'], diff --git a/packages/jest-resolve/src/defaultResolver.ts b/packages/jest-resolve/src/defaultResolver.ts index fe64aae69c73..440725585295 100644 --- a/packages/jest-resolve/src/defaultResolver.ts +++ b/packages/jest-resolve/src/defaultResolver.ts @@ -5,10 +5,10 @@ * LICENSE file in the root directory of this source tree. */ -import {resolve} from 'path'; import {fileURLToPath} from 'url'; import pnpResolver from 'jest-pnp-resolver'; import { + type ResolveResult, ResolverFactory, type NapiResolveOptions as UpstreamResolveOptions, } from 'unrs-resolver'; @@ -20,7 +20,9 @@ export interface ResolverOptions extends UpstreamResolveOptions { /** List of export conditions. */ conditions?: Array; /** Instance of default resolver. */ - defaultResolver: typeof defaultResolver; + defaultResolver: SyncResolver; + /** Instance of default async resolver. */ + defaultAsyncResolver: AsyncResolver; /** * List of directory names to be looked up for modules recursively. * @@ -53,7 +55,24 @@ export type AsyncResolver = ( export type Resolver = SyncResolver | AsyncResolver; -const defaultResolver: SyncResolver = (path, options) => { +const handleResolveResult = (result: ResolveResult) => { + if (result.error) { + throw new Error(result.error); + } + return result.path!; +}; + +function baseResolver(path: string, options: ResolverOptions): string; +function baseResolver( + path: string, + options: ResolverOptions, + async: true, +): Promise; +function baseResolver( + path: string, + options: ResolverOptions, + async?: true, +): string | Promise { if (process.versions.pnp && options.allowPnp !== false) { return pnpResolver(path, options); } @@ -76,9 +95,6 @@ const defaultResolver: SyncResolver = (path, options) => { /* eslint-enable prefer-const */ } = options; - // make sure that `basedir` is an absolute path - basedir = resolve(basedir); - modules = modules || moduleDirectory; const resolveOptions: UpstreamResolveOptions = { @@ -95,32 +111,50 @@ const defaultResolver: SyncResolver = (path, options) => { unrsResolver = unrsResolver.cloneWithOptions(resolveOptions); } else { unrsResolver = new ResolverFactory(resolveOptions); + setResolver(unrsResolver); } - setResolver(unrsResolver); - - let result = unrsResolver.sync(basedir, path); - - if (!result.path && paths?.length) { - const modulesArr = - modules == null || Array.isArray(modules) ? modules : [modules]; - if (modulesArr?.length) { - paths = paths.filter(p => !modulesArr.includes(p)); + const finalResolver = ( + resolve: ( + resolver: ResolverFactory, + ) => ResolveResult | Promise, + ) => { + const resolveWithPathsFallback = (result: ResolveResult) => { + if (!result.path && paths?.length) { + const modulesArr = + modules == null || Array.isArray(modules) ? modules : [modules]; + if (modulesArr?.length) { + paths = paths.filter(p => !modulesArr.includes(p)); + } + if (paths.length > 0) { + unrsResolver = unrsResolver!.cloneWithOptions({ + ...resolveOptions, + modules: paths, + }); + return resolve(unrsResolver); + } + } + return result; + }; + const result = resolve(unrsResolver!); + if ('then' in result) { + return result.then(resolveWithPathsFallback).then(handleResolveResult); } - if (paths.length > 0) { - unrsResolver = unrsResolver.cloneWithOptions({ - ...resolveOptions, - modules: paths, - }); - result = unrsResolver.sync(basedir, path); - } - } + return handleResolveResult( + resolveWithPathsFallback(result) as ResolveResult, + ); + }; - if (result.error) { - throw new Error(result.error); - } + return finalResolver((resolver: ResolverFactory) => + async ? resolver.async(basedir, path) : resolver.sync(basedir, path), + ); +} - return result.path!; -}; +export const defaultResolver: SyncResolver = baseResolver; + +export const defaultAsyncResolver: AsyncResolver = ( + path: string, + options: ResolverOptions, +) => baseResolver(path, options, true); export default defaultResolver; diff --git a/packages/jest-resolve/src/resolver.ts b/packages/jest-resolve/src/resolver.ts index 538cf1f31d34..7ac8a46b03f4 100644 --- a/packages/jest-resolve/src/resolver.ts +++ b/packages/jest-resolve/src/resolver.ts @@ -15,6 +15,7 @@ import defaultResolver, { type AsyncResolver, type Resolver as ResolverInterface, type SyncResolver, + defaultAsyncResolver, } from './defaultResolver'; import {clearFsCache} from './fileWalkers'; import isBuiltinModule from './isBuiltinModule'; @@ -122,6 +123,7 @@ export default class Resolver { return resolver(path, { basedir: options.basedir, conditions: options.conditions, + defaultAsyncResolver, defaultResolver, extensions: options.extensions, moduleDirectory: options.moduleDirectory, @@ -142,7 +144,7 @@ export default class Resolver { options: FindNodeModuleConfig, ): Promise { const resolverModule = loadResolver(options.resolver); - let resolver: ResolverInterface = defaultResolver; + let resolver: ResolverInterface = defaultAsyncResolver; if (typeof resolverModule === 'function') { resolver = resolverModule; @@ -165,6 +167,7 @@ export default class Resolver { const result = await resolver(path, { basedir: options.basedir, conditions: options.conditions, + defaultAsyncResolver, defaultResolver, extensions: options.extensions, moduleDirectory: options.moduleDirectory, diff --git a/website/versioned_docs/version-30.0/Configuration.md b/website/versioned_docs/version-30.0/Configuration.md index 3c9527f4fe76..3c876c92a579 100644 --- a/website/versioned_docs/version-30.0/Configuration.md +++ b/website/versioned_docs/version-30.0/Configuration.md @@ -1476,6 +1476,11 @@ type ResolverOptions = { conditions?: Array; /** Instance of default resolver. */ defaultResolver: (path: string, options: ResolverOptions) => string; + /** Instance of default async resolver. */ + defaultAsyncResolver: ( + path: string, + options: ResolverOptions, + ) => Promise; /** List of file extensions to search in order. */ extensions?: Array; /** List of directory names to be looked up for modules recursively. */ @@ -1491,6 +1496,8 @@ type ResolverOptions = { The `defaultResolver` passed as an option is the Jest default resolver which might be useful when you write your custom one. It takes the same arguments as your custom synchronous one, e.g. `(path, options)` and returns a string or throws. +Similarly, the `defaultAsyncResolver` is the default async resolver which takes the same arguments and returns a promise that resolves with a string or rejects with an error. + ::: For example, if you want to respect Browserify's [`"browser"` field](https://github.com/browserify/browserify-handbook/blob/master/readme.markdown#browser-field), you can use the following resolver: @@ -1522,25 +1529,6 @@ const config: Config = { export default config; ``` -By combining `defaultResolver` and `packageFilter` we can implement a `package.json` "pre-processor" that allows us to change how the default resolver will resolve modules. For example, imagine we want to use the field `"module"` if it is present, otherwise fallback to `"main"`: - -```js -module.exports = (path, options) => { - // Call the defaultResolver, so we leverage its cache, error handling, etc. - return options.defaultResolver(path, { - ...options, - // Use packageFilter to process parsed `package.json` before the resolution (see https://www.npmjs.com/package/resolve#resolveid-opts-cb) - packageFilter: pkg => { - return { - ...pkg, - // Alter the value of `main` before resolving the package - main: pkg.module || pkg.main, - }; - }, - }); -}; -``` - ### `restoreMocks` \[boolean] Default: `false`