Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions e2e/__tests__/__snapshots__/moduleNameMapper.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -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)"
`;
Expand Down Expand Up @@ -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)"
`;
2 changes: 1 addition & 1 deletion e2e/__tests__/__snapshots__/requireMissingExt.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -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.<anonymous> (node_modules/discord.js/src/index.js:21:12)
at Object.require (__tests__/test.js:10:1)"
`;
Original file line number Diff line number Diff line change
Expand Up @@ -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)"
`;
4 changes: 3 additions & 1 deletion packages/jest-resolve/src/__tests__/resolve.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -109,6 +109,7 @@ describe('findNodeModule', () => {
expect(mockUserResolver.mock.calls[0][1]).toStrictEqual({
basedir: '/',
conditions: ['conditions, woooo'],
defaultAsyncResolver,
defaultResolver,
extensions: ['js'],
moduleDirectory: ['node_modules'],
Expand Down Expand Up @@ -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'],
Expand Down
90 changes: 62 additions & 28 deletions packages/jest-resolve/src/defaultResolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -20,7 +20,9 @@
/** List of export conditions. */
conditions?: Array<string>;
/** 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.
*
Expand Down Expand Up @@ -53,7 +55,24 @@

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<string>;
function baseResolver(
path: string,
options: ResolverOptions,
async?: true,
): string | Promise<string> {
if (process.versions.pnp && options.allowPnp !== false) {
return pnpResolver(path, options);
}
Expand All @@ -76,9 +95,6 @@
/* eslint-enable prefer-const */
} = options;

// make sure that `basedir` is an absolute path
basedir = resolve(basedir);

modules = modules || moduleDirectory;

const resolveOptions: UpstreamResolveOptions = {
Expand All @@ -95,32 +111,50 @@
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<ResolveResult>,
) => {
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);

Check warning on line 141 in packages/jest-resolve/src/defaultResolver.ts

View check run for this annotation

Codecov / codecov/patch

packages/jest-resolve/src/defaultResolver.ts#L141

Added line #L141 was not covered by tests
}
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);

Check warning on line 158 in packages/jest-resolve/src/defaultResolver.ts

View check run for this annotation

Codecov / codecov/patch

packages/jest-resolve/src/defaultResolver.ts#L158

Added line #L158 was not covered by tests

export default defaultResolver;
5 changes: 4 additions & 1 deletion packages/jest-resolve/src/resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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,
Expand All @@ -142,7 +144,7 @@ export default class Resolver {
options: FindNodeModuleConfig,
): Promise<string | null> {
const resolverModule = loadResolver(options.resolver);
let resolver: ResolverInterface = defaultResolver;
let resolver: ResolverInterface = defaultAsyncResolver;

if (typeof resolverModule === 'function') {
resolver = resolverModule;
Expand All @@ -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,
Expand Down
26 changes: 7 additions & 19 deletions website/versioned_docs/version-30.0/Configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -1476,6 +1476,11 @@ type ResolverOptions = {
conditions?: Array<string>;
/** Instance of default resolver. */
defaultResolver: (path: string, options: ResolverOptions) => string;
/** Instance of default async resolver. */
defaultAsyncResolver: (
path: string,
options: ResolverOptions,
) => Promise<string>;
/** List of file extensions to search in order. */
extensions?: Array<string>;
/** List of directory names to be looked up for modules recursively. */
Expand All @@ -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:
Expand Down Expand Up @@ -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,
};
},
});
};
```

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is another way to do similar with new resolver:

module.exports = (path, options) => {
  // Call the defaultResolver, so we leverage its cache, error handling, etc.
  return options.defaultResolver(path, {
    ...options,
    // HACK!!!
    // this is option from unrs-resolver from https://github.com/unrs/unrs-resolver?tab=readme-ov-file#main-field
    // unrs-resolver used from jest-resolve now https://github.com/jestjs/jest/blob/v30.0.0/packages/jest-resolve/src/defaultResolver.ts#L84-L98
    // We use the fact that jest-resolve just pass extra options to resolver
    mainFields: ["react-native", "main"],
  });
};

Copy link
Member

@SimenB SimenB Jun 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that seems like a very reasonable way of doing it. should be added to the docs 😀

Copy link
Collaborator Author

@JounQin JounQin Jun 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@vovkasm Would you like to raise a PR to add it?

@SimenB I'm thinking that we can expose more options from unrs-resolver for example tsconfig or we can also detect tsconfig.json or jsconfig.json automatically, so that paths/references will be supported out of box.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@JounQin I'd love to do it. Today or tomorrow!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@JounQin see #15687

As for exposing options from unrs-resolver, it seems it is not necessary, interface ResolverOptions already extends UpstreamResolveOptions.

### `restoreMocks` \[boolean]

Default: `false`
Expand Down
Loading