Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(jest-resolve): expose JestResolver, AsyncResolver and SyncResolver types #12707

Merged
merged 7 commits into from
Apr 21, 2022
Merged
Show file tree
Hide file tree
Changes from 3 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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
- `[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]` 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))
Expand Down
58 changes: 28 additions & 30 deletions docs/Configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -919,41 +919,50 @@ 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<string, unknown>;

type ResolverOptions = {
/** Directory to begin resolving from. */
basedir: string;
browser?: boolean;
/** List of export conditions. */
conditions?: Array<string>;
/** Instance of default resolver. */
defaultResolver: (path: string, options: ResolverOptions) => string;
/** List of file extensions to search in order. */
extensions?: Array<string>;
/** List of directory names to be looked up for modules recursively. */
moduleDirectory?: Array<string>;
/** List of `require.paths` to use if nothing is found in `node_modules`. */
paths?: Array<string>;
/** 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.

:::

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 configuration:

```json
{
...
"jest": {
"resolver": "<rootDir>/resolver.js"
}
"resolver": "<rootDir>/resolver.js"
}
```

Expand All @@ -965,19 +974,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,
Expand Down
68 changes: 68 additions & 0 deletions packages/jest-resolve/__typetests__/resolver.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/**
* 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<string, unknown>;
type PackageFilter = (pkg: PackageJson, dir: string) => PackageJson;
type PathFilter = (
pkg: PackageJson,
path: string,
relativePath: string,
) => string;

// AsyncResolver

const asyncResolver: AsyncResolver = async (path, options) => {
expectType<string>(path);

expectType<string>(options.basedir);
expectType<boolean | undefined>(options.browser);
expectType<Array<string> | undefined>(options.conditions);
expectType<SyncResolver>(options.defaultResolver);
expectType<Array<string> | undefined>(options.extensions);
expectType<Array<string> | undefined>(options.moduleDirectory);
expectType<PackageFilter | undefined>(options.packageFilter);
expectType<PathFilter | undefined>(options.pathFilter);
expectType<Array<string> | undefined>(options.paths);
expectType<string | undefined>(options.rootDir);

return path;
};

const notReturningAsyncResolver = async () => {};
expectError<AsyncResolver>(notReturningAsyncResolver());

// SyncResolver

const syncResolver: SyncResolver = (path, options) => {
expectType<string>(path);

expectType<string>(options.basedir);
expectType<boolean | undefined>(options.browser);
expectType<Array<string> | undefined>(options.conditions);
expectType<SyncResolver>(options.defaultResolver);
expectType<Array<string> | undefined>(options.extensions);
expectType<Array<string> | undefined>(options.moduleDirectory);
expectType<PackageFilter | undefined>(options.packageFilter);
expectType<PathFilter | undefined>(options.pathFilter);
expectType<Array<string> | undefined>(options.paths);
expectType<string | undefined>(options.rootDir);

return path;
};

const notReturningSyncResolver = () => {};
expectError<SyncResolver>(notReturningSyncResolver());

// JestResolver

expectAssignable<JestResolver>({async: asyncResolver});
expectAssignable<JestResolver>({sync: syncResolver});
expectAssignable<JestResolver>({async: asyncResolver, sync: syncResolver});
expectError<JestResolver>({});
12 changes: 12 additions & 0 deletions packages/jest-resolve/__typetests__/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"extends": "../../../tsconfig.json",
"compilerOptions": {
"composite": false,
"noUnusedLocals": false,
"noUnusedParameters": false,
"skipLibCheck": true,

"types": []
},
"include": ["./**/*"]
}
4 changes: 3 additions & 1 deletion packages/jest-resolve/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
34 changes: 26 additions & 8 deletions packages/jest-resolve/src/defaultResolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,28 +13,45 @@ import {
resolve as resolveExports,
} from 'resolve.exports';
import {
PkgJson,
PackageJson,
findClosestPackageJson,
isDirectory,
isFile,
readPackageCached,
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;
mrazauskas marked this conversation as resolved.
Show resolved Hide resolved
/** List of export conditions. */
conditions?: Array<string>;
/** Instance of default resolver. */
defaultResolver: typeof defaultResolver;
/** List of file extensions to search in order. */
extensions?: Array<string>;
/**
* List of directory names to be looked up for modules recursively.
*
* @defaultValue
* The default is `['node_modules']`.
*/
moduleDirectory?: Array<string>;
/**
* List of `require.paths` to use if nothing is found in `node_modules`.
*
* @defaultValue
* The default is `undefined`.
*/
paths?: Array<string>;
/** 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<ResolverOptions, 'basedir' | 'conditions'>;
Expand Down Expand Up @@ -63,6 +80,7 @@ const defaultResolver: SyncResolver = (path, options) => {
return pnpResolver(path, options);
}

// @ts-expect-error: TODO packageFilter typings should be fixed in @types/resolve
Copy link
Member

Choose a reason for hiding this comment

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

what's the error?

Copy link
Member

Choose a reason for hiding this comment

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

hah, commented before your comments showed up 🙈

Copy link
Contributor Author

Choose a reason for hiding this comment

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

packageFilter() takes three arguments, but @types/resolve defines just two.

Copy link
Member

Choose a reason for hiding this comment

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

👍 will you be sending a PR to DT?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sure (;

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Copy link
Member

Choose a reason for hiding this comment

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

const resolveOptions: UpstreamResolveOptionsWithConditions = {
...options,
isDirectory,
Expand Down Expand Up @@ -91,7 +109,7 @@ export default defaultResolver;
* helper functions
*/

function readPackageSync(_: unknown, file: string): PkgJson {
function readPackageSync(_: unknown, file: string): PackageJson {
return readPackageCached(file);
}

Expand Down
8 changes: 4 additions & 4 deletions packages/jest-resolve/src/fileWalkers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,17 +71,17 @@ function realpathCached(path: string): string {
return result;
}

export type PkgJson = Record<string, unknown>;
export type PackageJson = Record<string, unknown>;

const packageContents = new Map<string, PkgJson>();
export function readPackageCached(path: string): PkgJson {
const packageContents = new Map<string, PackageJson>();
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);

Expand Down
7 changes: 6 additions & 1 deletion packages/jest-resolve/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,12 @@

import Resolver from './resolver';

export type {ResolveModuleConfig} from './resolver';
export type {AsyncResolver, SyncResolver} from './defaultResolver';
export type {
FindNodeModuleConfig,
ResolveModuleConfig,
Comment on lines +12 to +13
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Typings for static methods of Resolver class.

ResolverObject as JestResolver,
} from './resolver';
export * from './utils';

export default Resolver;
4 changes: 2 additions & 2 deletions packages/jest-resolve/src/resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ 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<string>;
Expand Down Expand Up @@ -860,7 +860,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,
Expand Down
2 changes: 2 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand Down