diff --git a/CHANGELOG.md b/CHANGELOG.md index 093d3cc6a965..ea522cebb042 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ### Features +- `[jest-resolve]` Add support for `packageFilter` on custom resolver ([#10393](https://github.com/facebook/jest/pull/10393)) + ### Fixes ### Chore & Maintenance diff --git a/docs/Configuration.md b/docs/Configuration.md index 406f48fa493f..0a43c9b9412f 100644 --- a/docs/Configuration.md +++ b/docs/Configuration.md @@ -693,6 +693,7 @@ This option allows the use of a custom resolver. This resolver must be a node mo "extensions": [string], "moduleDirectory": [string], "paths": [string], + "packageFilter": "function(pkg, pkgdir)", "rootDir": [string] } ``` @@ -712,6 +713,36 @@ For example, if you want to respect Browserify's [`"browser"` field](https://git } ``` +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"`: + +```json +{ + ... + "jest": { + "resolver": "my-module-resolve" + } +} +``` + +```js +// my-module-resolve package + +module.exports = (request, options) => { + // Call the defaultResolver, so we leverage its cache, error handling, etc. + return options.defaultResolver(request, { + ...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` diff --git a/packages/jest-resolve/src/__tests__/resolve.test.ts b/packages/jest-resolve/src/__tests__/resolve.test.ts index 75f1125697fb..822cdede4d2d 100644 --- a/packages/jest-resolve/src/__tests__/resolve.test.ts +++ b/packages/jest-resolve/src/__tests__/resolve.test.ts @@ -9,6 +9,7 @@ import * as path from 'path'; import * as fs from 'graceful-fs'; import {ModuleMap} from 'jest-haste-map'; +import {sync as resolveSync} from 'resolve'; import Resolver = require('../'); import userResolver from '../__mocks__/userResolver'; import nodeModulesPaths from '../nodeModulesPaths'; @@ -17,8 +18,23 @@ import type {ResolverConfig} from '../types'; jest.mock('../__mocks__/userResolver'); +// Do not fully mock `resolve` because it is used by Jest. Doing it will crash +// in very strange ways. Instead just spy on the method `sync`. +jest.mock('resolve', () => { + const originalModule = jest.requireActual('resolve'); + return { + ...originalModule, + sync: jest.spyOn(originalModule, 'sync'), + }; +}); + +const mockResolveSync = < + jest.Mock, Parameters> +>resolveSync; + beforeEach(() => { userResolver.mockClear(); + mockResolveSync.mockClear(); }); describe('isCoreModule', () => { @@ -93,6 +109,27 @@ describe('findNodeModule', () => { rootDir: undefined, }); }); + + it('passes packageFilter to the resolve module when using the default resolver', () => { + const packageFilter = jest.fn(); + + // A resolver that delegates to defaultResolver with a packageFilter implementation + userResolver.mockImplementation((request, opts) => + opts.defaultResolver(request, {...opts, packageFilter}), + ); + + Resolver.findNodeModule('test', { + basedir: '/', + resolver: require.resolve('../__mocks__/userResolver'), + }); + + expect(mockResolveSync).toHaveBeenCalledWith( + 'test', + expect.objectContaining({ + packageFilter, + }), + ); + }); }); describe('resolveModule', () => { diff --git a/packages/jest-resolve/src/defaultResolver.ts b/packages/jest-resolve/src/defaultResolver.ts index 6febcba33d3b..c925ae8fe3b0 100644 --- a/packages/jest-resolve/src/defaultResolver.ts +++ b/packages/jest-resolve/src/defaultResolver.ts @@ -6,7 +6,7 @@ */ import * as fs from 'graceful-fs'; -import {sync as resolveSync} from 'resolve'; +import {Opts as ResolveOpts, sync as resolveSync} from 'resolve'; import pnpResolver from 'jest-pnp-resolver'; import {tryRealpath} from 'jest-util'; import type {Config} from '@jest/types'; @@ -20,6 +20,7 @@ type ResolverOptions = { moduleDirectory?: Array; paths?: Array; rootDir?: Config.Path; + packageFilter?: ResolveOpts['packageFilter']; }; declare global { @@ -45,6 +46,7 @@ export default function defaultResolver( isDirectory, isFile, moduleDirectory: options.moduleDirectory, + packageFilter: options.packageFilter, paths: options.paths, preserveSymlinks: false, realpathSync,