diff --git a/CHANGELOG.md b/CHANGELOG.md index 66d8e321591e..93b233f5d465 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -45,6 +45,8 @@ - `[jest-resolve]` Support package self-reference ([#12682](https://github.com/facebook/jest/pull/12682)) - `[jest-resolve, jest-runtime]` Add support for `data:` URI import and mock ([#12392](https://github.com/facebook/jest/pull/12392)) - `[jest-resolve, jest-runtime]` Add support for async resolver ([#11540](https://github.com/facebook/jest/pull/11540)) +- `[jest-resolve]` [**BREAKING**] Remove `browser?: boolean` from resolver options, `conditions: ['browser']` should be used instead ([#12707](https://github.com/facebook/jest/pull/12707)) +- `[jest-resolve]` Expose `JestResolver`, `AsyncResolver` and `SyncResolver` types ([#12707](https://github.com/facebook/jest/pull/12707)) - `[jest-runner]` Allow `setupFiles` module to export an async function ([#12042](https://github.com/facebook/jest/pull/12042)) - `[jest-runner]` Allow passing `testEnvironmentOptions` via docblocks ([#12470](https://github.com/facebook/jest/pull/12470)) - `[jest-runner]` Exposing `CallbackTestRunner`, `EmittingTestRunner` abstract classes to help typing third party runners ([#12646](https://github.com/facebook/jest/pull/12646)) diff --git a/docs/Configuration.md b/docs/Configuration.md index 212e391955c1..8901f21b76c8 100644 --- a/docs/Configuration.md +++ b/docs/Configuration.md @@ -919,30 +919,41 @@ By default, each test file gets its own independent module registry. Enabling `r Default: `undefined` -This option allows the use of a custom resolver. This resolver must be a node module that exports _either_: +This option allows the use of a custom resolver. This resolver must be a module that exports _either_: 1. a function expecting a string as the first argument for the path to resolve and an options object as the second argument. The function should either return a path to the module that should be resolved or throw an error if the module can't be found. _or_ 2. an object containing `async` and/or `sync` properties. The `sync` property should be a function with the shape explained above, and the `async` property should also be a function that accepts the same arguments, but returns a promise which resolves with the path to the module or rejects with an error. The options object provided to resolvers has the shape: -```json -{ - "basedir": string, - "conditions": [string], - "defaultResolver": "function(request, options) -> string", - "extensions": [string], - "moduleDirectory": [string], - "paths": [string], - "packageFilter": "function(pkg, pkgdir)", - "pathFilter": "function(pkg, path, relativePath)", - "rootDir": [string] -} +```ts +type PackageJson = Record; + +type ResolverOptions = { + /** Directory to begin resolving from. */ + basedir: string; + /** List of export conditions. */ + conditions?: Array; + /** Instance of default resolver. */ + defaultResolver: (path: string, options: ResolverOptions) => string; + /** List of file extensions to search in order. */ + extensions?: Array; + /** List of directory names to be looked up for modules recursively. */ + moduleDirectory?: Array; + /** List of `require.paths` to use if nothing is found in `node_modules`. */ + paths?: Array; + /** Allows transforming parsed `package.json` contents. */ + packageFilter?: (pkg: PackageJson, file: string, dir: string) => PackageJson; + /** Allows transforms a path within a package. */ + pathFilter?: (pkg: PackageJson, path: string, relativePath: string) => string; + /** Current root directory. */ + rootDir?: string; +}; ``` :::tip -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. `(request, options)` and returns a string or throws. +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. ::: @@ -950,10 +961,7 @@ For example, if you want to respect Browserify's [`"browser"` field](https://git ```json { - ... - "jest": { - "resolver": "/resolver.js" - } + "resolver": "/resolver.js" } ``` @@ -965,19 +973,8 @@ module.exports = browserResolve.sync; 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) => { +module.exports = (path, options) => { // Call the defaultResolver, so we leverage its cache, error handling, etc. return options.defaultResolver(request, { ...options, diff --git a/e2e/__tests__/__snapshots__/moduleNameMapper.test.ts.snap b/e2e/__tests__/__snapshots__/moduleNameMapper.test.ts.snap index 912914fe7348..b9c10e68936f 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/resolver.js:901:17) + at createNoMappedModuleFoundError (../../packages/jest-resolve/build/resolver.js:899:17) at Object.require (index.js:10:1)" `; @@ -70,6 +70,6 @@ exports[`moduleNameMapper wrong configuration 1`] = ` 12 | module.exports = () => 'test'; 13 | - at createNoMappedModuleFoundError (../../packages/jest-resolve/build/resolver.js:901:17) + at createNoMappedModuleFoundError (../../packages/jest-resolve/build/resolver.js:899: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 a457d7551b13..8ee95a4ed134 100644 --- a/e2e/__tests__/__snapshots__/resolveNoFileExtensions.test.ts.snap +++ b/e2e/__tests__/__snapshots__/resolveNoFileExtensions.test.ts.snap @@ -37,6 +37,6 @@ exports[`show error message with matching files 1`] = ` | ^ 9 | - at Resolver._throwModNotFoundError (../../packages/jest-resolve/build/resolver.js:493:11) + at Resolver._throwModNotFoundError (../../packages/jest-resolve/build/resolver.js:491:11) at Object.require (index.js:8:18)" `; diff --git a/packages/jest-resolve/__typetests__/resolver.test.ts b/packages/jest-resolve/__typetests__/resolver.test.ts new file mode 100644 index 000000000000..a9cfc7ab3603 --- /dev/null +++ b/packages/jest-resolve/__typetests__/resolver.test.ts @@ -0,0 +1,70 @@ +/** + * 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 {expectAssignable, expectError, expectType} from 'tsd-lite'; +import type {AsyncResolver, JestResolver, SyncResolver} from 'jest-resolve'; + +type PackageJson = Record; +type PackageFilter = ( + pkg: PackageJson, + file: string, + dir: string, +) => PackageJson; +type PathFilter = ( + pkg: PackageJson, + path: string, + relativePath: string, +) => string; + +// AsyncResolver + +const asyncResolver: AsyncResolver = async (path, options) => { + expectType(path); + + expectType(options.basedir); + expectType | undefined>(options.conditions); + expectType(options.defaultResolver); + expectType | undefined>(options.extensions); + expectType | undefined>(options.moduleDirectory); + expectType(options.packageFilter); + expectType(options.pathFilter); + expectType | undefined>(options.paths); + expectType(options.rootDir); + + return path; +}; + +const notReturningAsyncResolver = async () => {}; +expectError(notReturningAsyncResolver()); + +// SyncResolver + +const syncResolver: SyncResolver = (path, options) => { + expectType(path); + + expectType(options.basedir); + expectType | undefined>(options.conditions); + expectType(options.defaultResolver); + expectType | undefined>(options.extensions); + expectType | undefined>(options.moduleDirectory); + expectType(options.packageFilter); + expectType(options.pathFilter); + expectType | undefined>(options.paths); + expectType(options.rootDir); + + return path; +}; + +const notReturningSyncResolver = () => {}; +expectError(notReturningSyncResolver()); + +// JestResolver + +expectAssignable({async: asyncResolver}); +expectAssignable({sync: syncResolver}); +expectAssignable({async: asyncResolver, sync: syncResolver}); +expectError({}); diff --git a/packages/jest-resolve/__typetests__/tsconfig.json b/packages/jest-resolve/__typetests__/tsconfig.json new file mode 100644 index 000000000000..165ba1343021 --- /dev/null +++ b/packages/jest-resolve/__typetests__/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "../../../tsconfig.json", + "compilerOptions": { + "composite": false, + "noUnusedLocals": false, + "noUnusedParameters": false, + "skipLibCheck": true, + + "types": [] + }, + "include": ["./**/*"] +} diff --git a/packages/jest-resolve/package.json b/packages/jest-resolve/package.json index 6d6120d83290..9bccf572115d 100644 --- a/packages/jest-resolve/package.json +++ b/packages/jest-resolve/package.json @@ -28,8 +28,10 @@ "slash": "^3.0.0" }, "devDependencies": { + "@tsd/typescript": "~4.6.2", "@types/graceful-fs": "^4.1.3", - "@types/resolve": "^1.20.0" + "@types/resolve": "^1.20.0", + "tsd-lite": "^0.5.1" }, "engines": { "node": "^12.13.0 || ^14.15.0 || ^16.13.0 || >=17.0.0" diff --git a/packages/jest-resolve/src/__tests__/resolve.test.ts b/packages/jest-resolve/src/__tests__/resolve.test.ts index 0b4b22a0647c..3143c9c45e6c 100644 --- a/packages/jest-resolve/src/__tests__/resolve.test.ts +++ b/packages/jest-resolve/src/__tests__/resolve.test.ts @@ -108,7 +108,6 @@ describe('findNodeModule', () => { const newPath = Resolver.findNodeModule('test', { basedir: '/', - browser: true, conditions: ['conditions, woooo'], extensions: ['js'], moduleDirectory: ['node_modules'], @@ -120,7 +119,6 @@ describe('findNodeModule', () => { expect(userResolver.mock.calls[0][0]).toBe('test'); expect(userResolver.mock.calls[0][1]).toStrictEqual({ basedir: '/', - browser: true, conditions: ['conditions, woooo'], defaultResolver, extensions: ['js'], @@ -315,7 +313,6 @@ describe('findNodeModuleAsync', () => { const newPath = await Resolver.findNodeModuleAsync('test', { basedir: '/', - browser: true, conditions: ['conditions, woooo'], extensions: ['js'], moduleDirectory: ['node_modules'], @@ -327,7 +324,6 @@ describe('findNodeModuleAsync', () => { expect(userResolverAsync.async.mock.calls[0][0]).toBe('test'); expect(userResolverAsync.async.mock.calls[0][1]).toStrictEqual({ basedir: '/', - browser: true, conditions: ['conditions, woooo'], defaultResolver, extensions: ['js'], diff --git a/packages/jest-resolve/src/defaultResolver.ts b/packages/jest-resolve/src/defaultResolver.ts index b5a834b32b3a..de53b3ccd939 100644 --- a/packages/jest-resolve/src/defaultResolver.ts +++ b/packages/jest-resolve/src/defaultResolver.ts @@ -13,7 +13,7 @@ import { resolve as resolveExports, } from 'resolve.exports'; import { - PkgJson, + PackageJson, findClosestPackageJson, isDirectory, isFile, @@ -21,20 +21,36 @@ import { realpathSync, } from './fileWalkers'; -// copy from `resolve`'s types so we don't have their types in our definition -// files -interface ResolverOptions { +type ResolverOptions = { + /** Directory to begin resolving from. */ basedir: string; - browser?: boolean; + /** List of export conditions. */ conditions?: Array; + /** Instance of default resolver. */ defaultResolver: typeof defaultResolver; + /** List of file extensions to search in order. */ extensions?: Array; + /** + * List of directory names to be looked up for modules recursively. + * + * @defaultValue + * The default is `['node_modules']`. + */ moduleDirectory?: Array; + /** + * List of `require.paths` to use if nothing is found in `node_modules`. + * + * @defaultValue + * The default is `undefined`. + */ paths?: Array; + /** Allows transforming parsed `package.json` contents. */ + packageFilter?: (pkg: PackageJson, file: string, dir: string) => PackageJson; + /** Allows transforms a path within a package. */ + pathFilter?: (pkg: PackageJson, path: string, relativePath: string) => string; + /** Current root directory. */ rootDir?: string; - packageFilter?: (pkg: PkgJson, dir: string) => PkgJson; - pathFilter?: (pkg: PkgJson, path: string, relativePath: string) => string; -} +}; type UpstreamResolveOptionsWithConditions = UpstreamResolveOptions & Pick; @@ -63,6 +79,7 @@ const defaultResolver: SyncResolver = (path, options) => { return pnpResolver(path, options); } + // @ts-expect-error: TODO remove after merging https://github.com/DefinitelyTyped/DefinitelyTyped/pull/59990 const resolveOptions: UpstreamResolveOptionsWithConditions = { ...options, isDirectory, @@ -91,7 +108,7 @@ export default defaultResolver; * helper functions */ -function readPackageSync(_: unknown, file: string): PkgJson { +function readPackageSync(_: unknown, file: string): PackageJson { return readPackageCached(file); } diff --git a/packages/jest-resolve/src/fileWalkers.ts b/packages/jest-resolve/src/fileWalkers.ts index 88c2110e9f72..1e57f6fc8251 100644 --- a/packages/jest-resolve/src/fileWalkers.ts +++ b/packages/jest-resolve/src/fileWalkers.ts @@ -71,17 +71,17 @@ function realpathCached(path: string): string { return result; } -export type PkgJson = Record; +export type PackageJson = Record; -const packageContents = new Map(); -export function readPackageCached(path: string): PkgJson { +const packageContents = new Map(); +export function readPackageCached(path: string): PackageJson { let result = packageContents.get(path); if (result != null) { return result; } - result = JSON.parse(fs.readFileSync(path, 'utf8')) as PkgJson; + result = JSON.parse(fs.readFileSync(path, 'utf8')) as PackageJson; packageContents.set(path, result); diff --git a/packages/jest-resolve/src/index.ts b/packages/jest-resolve/src/index.ts index 34600936314f..af1c1f2cb9ff 100644 --- a/packages/jest-resolve/src/index.ts +++ b/packages/jest-resolve/src/index.ts @@ -7,7 +7,12 @@ import Resolver from './resolver'; -export type {ResolveModuleConfig} from './resolver'; +export type {AsyncResolver, SyncResolver} from './defaultResolver'; +export type { + FindNodeModuleConfig, + ResolveModuleConfig, + ResolverObject as JestResolver, +} from './resolver'; export * from './utils'; export default Resolver; diff --git a/packages/jest-resolve/src/resolver.ts b/packages/jest-resolve/src/resolver.ts index 1ac833d5bf55..4c9f1a93cae3 100644 --- a/packages/jest-resolve/src/resolver.ts +++ b/packages/jest-resolve/src/resolver.ts @@ -24,9 +24,8 @@ import nodeModulesPaths from './nodeModulesPaths'; import shouldLoadAsEsm, {clearCachedLookups} from './shouldLoadAsEsm'; import type {ResolverConfig} from './types'; -type FindNodeModuleConfig = { +export type FindNodeModuleConfig = { basedir: string; - browser?: boolean; conditions?: Array; extensions?: Array; moduleDirectory?: Array; @@ -124,7 +123,6 @@ export default class Resolver { try { return resolver(path, { basedir: options.basedir, - browser: options.browser, conditions: options.conditions, defaultResolver, extensions: options.extensions, @@ -167,7 +165,6 @@ export default class Resolver { try { const result = await resolver(path, { basedir: options.basedir, - browser: options.browser, conditions: options.conditions, defaultResolver, extensions: options.extensions, @@ -860,7 +857,7 @@ Please check your configuration for these entries: type ResolverSyncObject = {sync: SyncResolver; async?: AsyncResolver}; type ResolverAsyncObject = {sync?: SyncResolver; async: AsyncResolver}; -type ResolverObject = ResolverSyncObject | ResolverAsyncObject; +export type ResolverObject = ResolverSyncObject | ResolverAsyncObject; function loadResolver( resolver: string | undefined | null, diff --git a/packages/jest-validate/src/__tests__/fixtures/jestConfig.ts b/packages/jest-validate/src/__tests__/fixtures/jestConfig.ts index c6055b1d740e..dfcc3a5a69c5 100644 --- a/packages/jest-validate/src/__tests__/fixtures/jestConfig.ts +++ b/packages/jest-validate/src/__tests__/fixtures/jestConfig.ts @@ -22,7 +22,6 @@ const NODE_MODULES_REGEXP = replacePathSepForRegex(NODE_MODULES); const defaultConfig = { automock: false, bail: 0, - browser: false, cacheDirectory: path.join(tmpdir(), 'jest'), clearMocks: false, coveragePathIgnorePatterns: [NODE_MODULES_REGEXP], @@ -59,7 +58,6 @@ const defaultConfig = { const validConfig = { automock: false, bail: 0, - browser: false, cache: true, cacheDirectory: '/tmp/user/jest', clearMocks: false, diff --git a/yarn.lock b/yarn.lock index 9f87e26b3026..7871974f8337 100644 --- a/yarn.lock +++ b/yarn.lock @@ -13444,6 +13444,7 @@ __metadata: version: 0.0.0-use.local resolution: "jest-resolve@workspace:packages/jest-resolve" dependencies: + "@tsd/typescript": ~4.6.2 "@types/graceful-fs": ^4.1.3 "@types/resolve": ^1.20.0 chalk: ^4.0.0 @@ -13455,6 +13456,7 @@ __metadata: resolve: ^1.20.0 resolve.exports: ^1.1.0 slash: ^3.0.0 + tsd-lite: ^0.5.1 languageName: unknown linkType: soft