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: pass conditions when resolving modules #11859

Merged
merged 11 commits into from
Sep 13, 2021
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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

### Features

- `[jest-resolver, jest-runtime]` Pass `conditions` to custom resolvers to enable them to implement support for package.json `exports` field ([#11859](https://github.com/facebook/jest/pull/11859))

### Fixes

- `[@jest/reporters]` Use async transform if available to transform files with no coverage ([#11852](https://github.com/facebook/jest/pull/11852))
Expand Down
4 changes: 4 additions & 0 deletions docs/Configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -788,11 +788,13 @@ This option allows the use of a custom resolver. This resolver must be a node mo
```json
{
"basedir": string,
"conditions": [string],
"defaultResolver": "function(request, options)",
"extensions": [string],
"moduleDirectory": [string],
"paths": [string],
"packageFilter": "function(pkg, pkgdir)",
"pathFilter": "function(pkg, path, relativePath)",
"rootDir": [string]
}
```
Expand Down Expand Up @@ -849,6 +851,8 @@ module.exports = (request, options) => {
};
```

While Jest does not support [package `exports`](https://nodejs.org/api/packages.html#packages_package_entry_points) (beyond `main`), Jest will provide `conditions` as an option when calling custom resolvers, which can be used to implement support for `exports` in userland. Jest will pass `['import', 'default']` when running a test in ESM mode, and `['require', 'default']` when running with CJS.

### `restoreMocks` \[boolean]

Default: `false`
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 @@ FAIL __tests__/index.js
12 | module.exports = () => 'test';
13 |

at createNoMappedModuleFoundError (../../packages/jest-resolve/build/resolver.js:566:17)
at createNoMappedModuleFoundError (../../packages/jest-resolve/build/resolver.js:577:17)
at Object.require (index.js:10:1)
`;

Expand Down Expand Up @@ -70,6 +70,6 @@ FAIL __tests__/index.js
12 | module.exports = () => 'test';
13 |

at createNoMappedModuleFoundError (../../packages/jest-resolve/build/resolver.js:566:17)
at createNoMappedModuleFoundError (../../packages/jest-resolve/build/resolver.js:577:17)
at Object.require (index.js:10:1)
`;
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,6 @@ FAIL __tests__/test.js
| ^
9 |

at Resolver.resolveModule (../../packages/jest-resolve/build/resolver.js:318:11)
at Resolver.resolveModule (../../packages/jest-resolve/build/resolver.js:322:11)
at Object.require (index.js:8:18)
`;
35 changes: 35 additions & 0 deletions e2e/__tests__/resolveConditions.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/**
* 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 {resolve} from 'path';
import {onNodeVersions} from '@jest/test-utils';
import {runYarnInstall} from '../Utils';
import runJest from '../runJest';

const dir = resolve(__dirname, '..', 'resolve-conditions');

beforeAll(() => {
runYarnInstall(dir);
});

// The versions where vm.Module exists and commonjs with "exports" is not broken
onNodeVersions('^12.16.0 || >=13.7.0', () => {
test('resolves package exports correctly with custom resolver', () => {
// run multiple times to ensure there are no caching errors
for (let i = 0; i < 5; i++) {
const {exitCode} = runJest(dir, [], {
nodeOptions: '--experimental-vm-modules',
});
try {
expect(exitCode).toBe(0);
} catch (error) {
console.log(`Test failed on iteration ${i + 1}`);
throw error;
}
}
});
});
12 changes: 12 additions & 0 deletions e2e/resolve-conditions/__tests__/resolveCjs.test.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/**
* 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.
*/

const {fn} = require('../fake-dep');

test('returns correct message', () => {
expect(fn()).toEqual('hello from CJS');
});
12 changes: 12 additions & 0 deletions e2e/resolve-conditions/__tests__/resolveEsm.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/**
* 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 {fn} from '../fake-dep';

test('returns correct message', () => {
expect(fn()).toEqual('hello from ESM');
});
10 changes: 10 additions & 0 deletions e2e/resolve-conditions/fake-dep/module.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/**
* 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.
*/

module.exports = {
fn: () => 'hello from CJS',
};
10 changes: 10 additions & 0 deletions e2e/resolve-conditions/fake-dep/module.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/**
* 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.
*/

export function fn() {
return 'hello from ESM';
}
10 changes: 10 additions & 0 deletions e2e/resolve-conditions/fake-dep/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"name": "fake-dep",
"version": "1.0.0",
"exports": {
".": {
"import": "./module.mjs",
"require": "./module.cjs"
}
}
}
18 changes: 18 additions & 0 deletions e2e/resolve-conditions/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"jest": {
"moduleFileExtensions": [
"js",
"cjs",
"mjs",
"json"
],
"resolver": "<rootDir>/resolver.js",
"testMatch": [
"<rootDir>/**/*.test.*"
],
"transform": {}
},
"dependencies": {
"resolve.exports": "^1.0.2"
}
}
34 changes: 34 additions & 0 deletions e2e/resolve-conditions/resolver.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/**
* 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.
*/

'use strict';

const {resolve: resolveExports} = require('resolve.exports');

module.exports = (path, options) => {
return options.defaultResolver(path, {
...options,
pathFilter: options.conditions
? createPathFilter(options.conditions)
: undefined,
});
};

function createPathFilter(conditions) {
return function pathFilter(pkg, _path, relativePath) {
// this `index` thing can backfire, but `resolve` adds it: https://github.com/browserify/resolve/blob/f1b51848ecb7f56f77bfb823511d032489a13eab/lib/sync.js#L192
const path = relativePath === 'index' ? '.' : relativePath;

return (
resolveExports(pkg, path, {
conditions,
// `resolve.exports adds `import` unless `require` is `false`, so let's add this ugly thing
require: !conditions.includes('import'),
}) || relativePath
);
};
}
21 changes: 21 additions & 0 deletions e2e/resolve-conditions/yarn.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# This file is generated by running "yarn install" inside your project.
# Manual changes might be lost - proceed with caution!

__metadata:
version: 4
cacheKey: 7

"resolve.exports@npm:^1.0.2":
version: 1.0.2
resolution: "resolve.exports@npm:1.0.2"
checksum: 012a46e3ae41c53762abf5b50ea1b4adf2de617bbea1dbc7bf6e609c1ceaedee7782acbc92d443951d5dd0c3a8fb1090ce73285a9ccc24b530e33b5e09ae196f
languageName: node
linkType: hard

"root-workspace-0b6124@workspace:.":
version: 0.0.0-use.local
resolution: "root-workspace-0b6124@workspace:."
dependencies:
resolve.exports: ^1.0.2
languageName: unknown
linkType: soft
2 changes: 2 additions & 0 deletions packages/jest-resolve/src/__tests__/resolve.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ describe('findNodeModule', () => {
const newPath = Resolver.findNodeModule('test', {
basedir: '/',
browser: true,
conditions: ['conditions, woooo'],
extensions: ['js'],
moduleDirectory: ['node_modules'],
paths: ['/something'],
Expand All @@ -116,6 +117,7 @@ describe('findNodeModule', () => {
expect(userResolver.mock.calls[0][1]).toStrictEqual({
basedir: '/',
browser: true,
conditions: ['conditions, woooo'],
defaultResolver,
extensions: ['js'],
moduleDirectory: ['node_modules'],
Expand Down
12 changes: 11 additions & 1 deletion packages/jest-resolve/src/defaultResolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,16 @@ type ResolverOptions = {
moduleDirectory?: Array<string>;
paths?: Array<Config.Path>;
rootDir?: Config.Path;
packageFilter?: (pkg: any, pkgfile: string) => any;
packageFilter?: (
pkg: Record<string, unknown>,
pkgfile: string,
) => Record<string, unknown>;
pathFilter?: (
pkg: Record<string, unknown>,
path: string,
relativePath: string,
) => string;
conditions?: Array<string>;
};

// https://github.com/facebook/jest/pull/10617
Expand Down Expand Up @@ -48,6 +57,7 @@ export default function defaultResolver(
isFile,
moduleDirectory: options.moduleDirectory,
packageFilter: options.packageFilter,
pathFilter: options.pathFilter,
paths: options.paths,
preserveSymlinks: false,
readPackageSync,
Expand Down
27 changes: 21 additions & 6 deletions packages/jest-resolve/src/resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import type {ResolverConfig} from './types';
type FindNodeModuleConfig = {
basedir: Config.Path;
browser?: boolean;
conditions?: Array<string>;
extensions?: Array<string>;
moduleDirectory?: Array<string>;
paths?: Array<Config.Path>;
Expand All @@ -32,6 +33,7 @@ type FindNodeModuleConfig = {
};

export type ResolveModuleConfig = {
conditions?: Array<string>;
skipNodeResolution?: boolean;
paths?: Array<Config.Path>;
};
Expand Down Expand Up @@ -113,6 +115,7 @@ export default class Resolver {
return resolver(path, {
basedir: options.basedir,
browser: options.browser,
conditions: options.conditions,
defaultResolver,
extensions: options.extensions,
moduleDirectory: options.moduleDirectory,
Expand All @@ -137,7 +140,8 @@ export default class Resolver {
): Config.Path | null {
const paths = options?.paths || this._options.modulePaths;
const moduleDirectory = this._options.moduleDirectories;
const key = dirname + path.delimiter + moduleName;
const stringifiedOptions = options ? JSON.stringify(options) : '';
const key = dirname + path.delimiter + moduleName + stringifiedOptions;
const defaultPlatform = this._options.defaultPlatform;
const extensions = this._options.extensions.slice();
let module;
Expand Down Expand Up @@ -183,6 +187,7 @@ export default class Resolver {

return Resolver.findNodeModule(name, {
basedir: dirname,
conditions: options?.conditions,
extensions,
moduleDirectory,
paths,
Expand Down Expand Up @@ -321,23 +326,31 @@ export default class Resolver {
virtualMocks: Map<string, boolean>,
from: Config.Path,
moduleName = '',
options?: ResolveModuleConfig,
): string {
const key = from + path.delimiter + moduleName;
const stringifiedOptions = options ? JSON.stringify(options) : '';
const key = from + path.delimiter + moduleName + stringifiedOptions;
const cachedModuleID = this._moduleIDCache.get(key);
if (cachedModuleID) {
return cachedModuleID;
}

const moduleType = this._getModuleType(moduleName);
const absolutePath = this._getAbsolutePath(virtualMocks, from, moduleName);
const absolutePath = this._getAbsolutePath(
virtualMocks,
from,
moduleName,
options,
);
const mockPath = this._getMockPath(from, moduleName);

const sep = path.delimiter;
const id =
moduleType +
sep +
(absolutePath ? absolutePath + sep : '') +
(mockPath ? mockPath + sep : '');
(mockPath ? mockPath + sep : '') +
(stringifiedOptions ? stringifiedOptions + sep : '');

this._moduleIDCache.set(key, id);
return id;
Expand All @@ -351,13 +364,14 @@ export default class Resolver {
virtualMocks: Map<string, boolean>,
from: Config.Path,
moduleName: string,
options?: ResolveModuleConfig,
): Config.Path | null {
if (this.isCoreModule(moduleName)) {
return moduleName;
}
return this._isModuleResolved(from, moduleName)
? this.getModule(moduleName)
: this._getVirtualMockPath(virtualMocks, from, moduleName);
: this._getVirtualMockPath(virtualMocks, from, moduleName, options);
}

private _getMockPath(
Expand All @@ -373,12 +387,13 @@ export default class Resolver {
virtualMocks: Map<string, boolean>,
from: Config.Path,
moduleName: string,
options?: ResolveModuleConfig,
): Config.Path {
const virtualMockPath = this.getModulePath(from, moduleName);
return virtualMocks.get(virtualMockPath)
? virtualMockPath
: moduleName
? this.resolveModule(from, moduleName)
? this.resolveModule(from, moduleName, options)
: from;
}

Expand Down
Loading