From bc3c921251193e88eca0d94ca6bd5ab92a137cbf Mon Sep 17 00:00:00 2001 From: Simen Bekkhus Date: Sun, 17 Oct 2021 20:02:07 +0200 Subject: [PATCH] feat: support `.` in `exports` field (#11919) --- CHANGELOG.md | 1 + e2e/__tests__/resolveConditions.test.ts | 5 -- .../__tests__/browser.test.mjs | 2 +- .../__tests__/node.test.mjs | 2 +- .../__tests__/resolveCjs.test.cjs | 2 +- .../__tests__/resolveEsm.test.mjs | 2 +- .../{ => node_modules}/fake-dep/module.cjs | 0 .../{ => node_modules}/fake-dep/module.mjs | 0 .../{ => node_modules}/fake-dep/package.json | 0 .../fake-dual-dep/browser.mjs | 0 .../{ => node_modules}/fake-dual-dep/node.mjs | 0 .../fake-dual-dep/package.json | 0 e2e/resolve-conditions/package.json | 4 -- e2e/resolve-conditions/resolver.js | 30 --------- e2e/resolve-conditions/yarn.lock | 21 ------- packages/jest-resolve/package.json | 1 + .../conditions/node_modules/import/file.js | 0 .../node_modules/import/package.json | 6 ++ .../conditions/node_modules/main/file.js | 0 .../conditions/node_modules/main/package.json | 4 ++ .../conditions/node_modules/require/file.js | 0 .../node_modules/require/package.json | 6 ++ .../jest-resolve/src/__mocks__/package.json | 6 ++ .../src/__tests__/resolve.test.ts | 51 ++++++++++++--- packages/jest-resolve/src/defaultResolver.ts | 63 ++++++++++++++++++- yarn.lock | 8 +++ 26 files changed, 140 insertions(+), 74 deletions(-) rename e2e/resolve-conditions/{ => node_modules}/fake-dep/module.cjs (100%) rename e2e/resolve-conditions/{ => node_modules}/fake-dep/module.mjs (100%) rename e2e/resolve-conditions/{ => node_modules}/fake-dep/package.json (100%) rename e2e/resolve-conditions/{ => node_modules}/fake-dual-dep/browser.mjs (100%) rename e2e/resolve-conditions/{ => node_modules}/fake-dual-dep/node.mjs (100%) rename e2e/resolve-conditions/{ => node_modules}/fake-dual-dep/package.json (100%) delete mode 100644 e2e/resolve-conditions/resolver.js delete mode 100644 e2e/resolve-conditions/yarn.lock create mode 100644 packages/jest-resolve/src/__mocks__/conditions/node_modules/import/file.js create mode 100644 packages/jest-resolve/src/__mocks__/conditions/node_modules/import/package.json create mode 100644 packages/jest-resolve/src/__mocks__/conditions/node_modules/main/file.js create mode 100644 packages/jest-resolve/src/__mocks__/conditions/node_modules/main/package.json create mode 100644 packages/jest-resolve/src/__mocks__/conditions/node_modules/require/file.js create mode 100644 packages/jest-resolve/src/__mocks__/conditions/node_modules/require/package.json create mode 100644 packages/jest-resolve/src/__mocks__/package.json diff --git a/CHANGELOG.md b/CHANGELOG.md index 48c812a51672..5356c4492d29 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ### Features - `[jest-config]` Add `testEnvironmentOptions.html` to apply to jsdom input ([11950](https://github.com/facebook/jest/pull/11950)) +- `[jest-resolver]` Support default export (`.`) in `exports` field _if_ `main` is missing ([#11919](https://github.com/facebook/jest/pull/11919)) ### Fixes diff --git a/e2e/__tests__/resolveConditions.test.ts b/e2e/__tests__/resolveConditions.test.ts index 375257ac7640..991a2b27e8b1 100644 --- a/e2e/__tests__/resolveConditions.test.ts +++ b/e2e/__tests__/resolveConditions.test.ts @@ -7,15 +7,10 @@ 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', () => { test('resolves package exports correctly with custom resolver', () => { diff --git a/e2e/resolve-conditions/__tests__/browser.test.mjs b/e2e/resolve-conditions/__tests__/browser.test.mjs index 12b397aa5afc..4edfaa7ffdaa 100644 --- a/e2e/resolve-conditions/__tests__/browser.test.mjs +++ b/e2e/resolve-conditions/__tests__/browser.test.mjs @@ -7,7 +7,7 @@ * @jest-environment /browser-env.js */ -import {fn} from '../fake-dual-dep'; +import {fn} from 'fake-dual-dep'; test('returns correct message', () => { expect(fn()).toEqual('hello from browser'); diff --git a/e2e/resolve-conditions/__tests__/node.test.mjs b/e2e/resolve-conditions/__tests__/node.test.mjs index fb86c2a8eeac..f71ed5a69e2e 100644 --- a/e2e/resolve-conditions/__tests__/node.test.mjs +++ b/e2e/resolve-conditions/__tests__/node.test.mjs @@ -7,7 +7,7 @@ * @jest-environment /node-env.js */ -import {fn} from '../fake-dual-dep'; +import {fn} from 'fake-dual-dep'; test('returns correct message', () => { expect(fn()).toEqual('hello from node'); diff --git a/e2e/resolve-conditions/__tests__/resolveCjs.test.cjs b/e2e/resolve-conditions/__tests__/resolveCjs.test.cjs index 5a9ba6b23b88..43409fed5939 100644 --- a/e2e/resolve-conditions/__tests__/resolveCjs.test.cjs +++ b/e2e/resolve-conditions/__tests__/resolveCjs.test.cjs @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -const {fn} = require('../fake-dep'); +const {fn} = require('fake-dep'); test('returns correct message', () => { expect(fn()).toEqual('hello from CJS'); diff --git a/e2e/resolve-conditions/__tests__/resolveEsm.test.mjs b/e2e/resolve-conditions/__tests__/resolveEsm.test.mjs index d46c700f2cf2..2303cef9e418 100644 --- a/e2e/resolve-conditions/__tests__/resolveEsm.test.mjs +++ b/e2e/resolve-conditions/__tests__/resolveEsm.test.mjs @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -import {fn} from '../fake-dep'; +import {fn} from 'fake-dep'; test('returns correct message', () => { expect(fn()).toEqual('hello from ESM'); diff --git a/e2e/resolve-conditions/fake-dep/module.cjs b/e2e/resolve-conditions/node_modules/fake-dep/module.cjs similarity index 100% rename from e2e/resolve-conditions/fake-dep/module.cjs rename to e2e/resolve-conditions/node_modules/fake-dep/module.cjs diff --git a/e2e/resolve-conditions/fake-dep/module.mjs b/e2e/resolve-conditions/node_modules/fake-dep/module.mjs similarity index 100% rename from e2e/resolve-conditions/fake-dep/module.mjs rename to e2e/resolve-conditions/node_modules/fake-dep/module.mjs diff --git a/e2e/resolve-conditions/fake-dep/package.json b/e2e/resolve-conditions/node_modules/fake-dep/package.json similarity index 100% rename from e2e/resolve-conditions/fake-dep/package.json rename to e2e/resolve-conditions/node_modules/fake-dep/package.json diff --git a/e2e/resolve-conditions/fake-dual-dep/browser.mjs b/e2e/resolve-conditions/node_modules/fake-dual-dep/browser.mjs similarity index 100% rename from e2e/resolve-conditions/fake-dual-dep/browser.mjs rename to e2e/resolve-conditions/node_modules/fake-dual-dep/browser.mjs diff --git a/e2e/resolve-conditions/fake-dual-dep/node.mjs b/e2e/resolve-conditions/node_modules/fake-dual-dep/node.mjs similarity index 100% rename from e2e/resolve-conditions/fake-dual-dep/node.mjs rename to e2e/resolve-conditions/node_modules/fake-dual-dep/node.mjs diff --git a/e2e/resolve-conditions/fake-dual-dep/package.json b/e2e/resolve-conditions/node_modules/fake-dual-dep/package.json similarity index 100% rename from e2e/resolve-conditions/fake-dual-dep/package.json rename to e2e/resolve-conditions/node_modules/fake-dual-dep/package.json diff --git a/e2e/resolve-conditions/package.json b/e2e/resolve-conditions/package.json index 33449adf2da3..6bfa1448215b 100644 --- a/e2e/resolve-conditions/package.json +++ b/e2e/resolve-conditions/package.json @@ -6,13 +6,9 @@ "mjs", "json" ], - "resolver": "/resolver.js", "testMatch": [ "/**/*.test.*" ], "transform": {} - }, - "dependencies": { - "resolve.exports": "^1.1.0" } } diff --git a/e2e/resolve-conditions/resolver.js b/e2e/resolve-conditions/resolver.js deleted file mode 100644 index 09cee4fb91a3..000000000000 --- a/e2e/resolve-conditions/resolver.js +++ /dev/null @@ -1,30 +0,0 @@ -/** - * 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, unsafe: true}) || relativePath - ); - }; -} diff --git a/e2e/resolve-conditions/yarn.lock b/e2e/resolve-conditions/yarn.lock deleted file mode 100644 index df2f59b137b1..000000000000 --- a/e2e/resolve-conditions/yarn.lock +++ /dev/null @@ -1,21 +0,0 @@ -# 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.1.0": - version: 1.1.0 - resolution: "resolve.exports@npm:1.1.0" - checksum: d04d2ce651fac14fe6ba13b377690f790cbbe91e6211b8fbec97ee08282e278875c74073a9b6243143a64e33d95eefb479e1dd4965664edc73b28b712100b36c - languageName: node - linkType: hard - -"root-workspace-0b6124@workspace:.": - version: 0.0.0-use.local - resolution: "root-workspace-0b6124@workspace:." - dependencies: - resolve.exports: ^1.1.0 - languageName: unknown - linkType: soft diff --git a/packages/jest-resolve/package.json b/packages/jest-resolve/package.json index 5da271e0508c..52ac535c8bf7 100644 --- a/packages/jest-resolve/package.json +++ b/packages/jest-resolve/package.json @@ -22,6 +22,7 @@ "jest-util": "^27.2.5", "jest-validate": "^27.2.5", "resolve": "^1.20.0", + "resolve.exports": "^1.1.0", "slash": "^3.0.0" }, "devDependencies": { diff --git a/packages/jest-resolve/src/__mocks__/conditions/node_modules/import/file.js b/packages/jest-resolve/src/__mocks__/conditions/node_modules/import/file.js new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/packages/jest-resolve/src/__mocks__/conditions/node_modules/import/package.json b/packages/jest-resolve/src/__mocks__/conditions/node_modules/import/package.json new file mode 100644 index 000000000000..24fc72b1cac7 --- /dev/null +++ b/packages/jest-resolve/src/__mocks__/conditions/node_modules/import/package.json @@ -0,0 +1,6 @@ +{ + "name": "import", + "exports": { + "import": "./file.js" + } +} diff --git a/packages/jest-resolve/src/__mocks__/conditions/node_modules/main/file.js b/packages/jest-resolve/src/__mocks__/conditions/node_modules/main/file.js new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/packages/jest-resolve/src/__mocks__/conditions/node_modules/main/package.json b/packages/jest-resolve/src/__mocks__/conditions/node_modules/main/package.json new file mode 100644 index 000000000000..18b48565f56a --- /dev/null +++ b/packages/jest-resolve/src/__mocks__/conditions/node_modules/main/package.json @@ -0,0 +1,4 @@ +{ + "name": "main", + "main": "./file.js" +} diff --git a/packages/jest-resolve/src/__mocks__/conditions/node_modules/require/file.js b/packages/jest-resolve/src/__mocks__/conditions/node_modules/require/file.js new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/packages/jest-resolve/src/__mocks__/conditions/node_modules/require/package.json b/packages/jest-resolve/src/__mocks__/conditions/node_modules/require/package.json new file mode 100644 index 000000000000..c42b33ecca86 --- /dev/null +++ b/packages/jest-resolve/src/__mocks__/conditions/node_modules/require/package.json @@ -0,0 +1,6 @@ +{ + "name": "require", + "exports": { + "require": "./file.js" + } +} diff --git a/packages/jest-resolve/src/__mocks__/package.json b/packages/jest-resolve/src/__mocks__/package.json new file mode 100644 index 000000000000..2807645e6dca --- /dev/null +++ b/packages/jest-resolve/src/__mocks__/package.json @@ -0,0 +1,6 @@ +{ + "name": "__mocks__", + "version": "1.0.0", + "dependencies": { + } +} diff --git a/packages/jest-resolve/src/__tests__/resolve.test.ts b/packages/jest-resolve/src/__tests__/resolve.test.ts index cfbe99bc841d..65b89a77b63d 100644 --- a/packages/jest-resolve/src/__tests__/resolve.test.ts +++ b/packages/jest-resolve/src/__tests__/resolve.test.ts @@ -126,7 +126,7 @@ describe('findNodeModule', () => { }); }); - it('passes packageFilter to the resolve module when using the default resolver', () => { + it('wraps passed packageFilter to the resolve module when using the default resolver', () => { const packageFilter = jest.fn(); // A resolver that delegates to defaultResolver with a packageFilter implementation @@ -134,18 +134,53 @@ describe('findNodeModule', () => { opts.defaultResolver(request, {...opts, packageFilter}), ); - Resolver.findNodeModule('test', { - basedir: '/', + Resolver.findNodeModule('./test', { + basedir: path.resolve(__dirname, '../__mocks__/'), resolver: require.resolve('../__mocks__/userResolver'), }); - expect(mockResolveSync).toHaveBeenCalledWith( - 'test', - expect.objectContaining({ - packageFilter, - }), + expect(packageFilter).toHaveBeenCalledWith( + expect.objectContaining({name: '__mocks__'}), + expect.any(String), ); }); + + describe('conditions', () => { + const conditionsRoot = path.resolve(__dirname, '../__mocks__/conditions'); + + test('resolves without exports, just main', () => { + const result = Resolver.findNodeModule('main', { + basedir: conditionsRoot, + conditions: ['require'], + }); + + expect(result).toEqual( + path.resolve(conditionsRoot, './node_modules/main/file.js'), + ); + }); + + test('resolves with import', () => { + const result = Resolver.findNodeModule('import', { + basedir: conditionsRoot, + conditions: ['import'], + }); + + expect(result).toEqual( + path.resolve(conditionsRoot, './node_modules/import/file.js'), + ); + }); + + test('resolves with require', () => { + const result = Resolver.findNodeModule('require', { + basedir: conditionsRoot, + conditions: ['require'], + }); + + expect(result).toEqual( + path.resolve(conditionsRoot, './node_modules/require/file.js'), + ); + }); + }); }); describe('resolveModule', () => { diff --git a/packages/jest-resolve/src/defaultResolver.ts b/packages/jest-resolve/src/defaultResolver.ts index bc0e5b64b5d9..1c461feab16e 100644 --- a/packages/jest-resolve/src/defaultResolver.ts +++ b/packages/jest-resolve/src/defaultResolver.ts @@ -5,8 +5,13 @@ * LICENSE file in the root directory of this source tree. */ +import {resolve} from 'path'; import pnpResolver from 'jest-pnp-resolver'; -import {Opts as ResolveOpts, sync as resolveSync} from 'resolve'; +import {sync as resolveSync} from 'resolve'; +import { + Options as ResolveExportsOptions, + resolve as resolveExports, +} from 'resolve.exports'; import type {Config} from '@jest/types'; import { PkgJson, @@ -16,13 +21,19 @@ import { realpathSync, } from './fileWalkers'; -interface ResolverOptions extends ResolveOpts { +// copy from `resolve`'s types so we don't have their types in our definition +// files +interface ResolverOptions { basedir: Config.Path; browser?: boolean; conditions?: Array; defaultResolver: typeof defaultResolver; extensions?: Array; + moduleDirectory?: Array; + paths?: Array; rootDir?: Config.Path; + packageFilter?: (pkg: PkgJson, dir: string) => PkgJson; + pathFilter?: (pkg: PkgJson, path: string, relativePath: string) => string; } // https://github.com/facebook/jest/pull/10617 @@ -48,6 +59,10 @@ export default function defaultResolver( ...options, isDirectory, isFile, + packageFilter: createPackageFilter( + options.conditions, + options.packageFilter, + ), preserveSymlinks: false, readPackageSync, realpathSync, @@ -65,3 +80,47 @@ export default function defaultResolver( function readPackageSync(_: unknown, file: Config.Path): PkgJson { return readPackageCached(file); } + +function createPackageFilter( + conditions?: Array, + userFilter?: ResolverOptions['packageFilter'], +): ResolverOptions['packageFilter'] { + function attemptExportsFallback(pkg: PkgJson) { + const options: ResolveExportsOptions = conditions + ? {conditions, unsafe: true} + : // no conditions were passed - let's assume this is Jest internal and it should be `require` + {browser: false, require: true}; + + try { + return resolveExports(pkg, '.', options); + } catch { + return undefined; + } + } + + return function packageFilter(pkg, packageDir) { + let filteredPkg = pkg; + + if (userFilter) { + filteredPkg = userFilter(filteredPkg, packageDir); + } + + if (filteredPkg.main != null) { + return filteredPkg; + } + + const indexInRoot = resolve(packageDir, './index.js'); + + // if the module contains an `index.js` file in root, `resolve` will request + // that if there is no `main`. Since we don't wanna break that, add this + // check + if (isFile(indexInRoot)) { + return filteredPkg; + } + + return { + ...filteredPkg, + main: attemptExportsFallback(filteredPkg), + }; + }; +} diff --git a/yarn.lock b/yarn.lock index 2bb1e0a824cb..4a9c62a8abc8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -13026,6 +13026,7 @@ fsevents@^1.2.7: jest-util: ^27.2.5 jest-validate: ^27.2.5 resolve: ^1.20.0 + resolve.exports: ^1.1.0 slash: ^3.0.0 languageName: unknown linkType: soft @@ -18944,6 +18945,13 @@ react-native@0.64.0: languageName: node linkType: hard +"resolve.exports@npm:^1.1.0": + version: 1.1.0 + resolution: "resolve.exports@npm:1.1.0" + checksum: d04d2ce651fac14fe6ba13b377690f790cbbe91e6211b8fbec97ee08282e278875c74073a9b6243143a64e33d95eefb479e1dd4965664edc73b28b712100b36c + languageName: node + linkType: hard + "resolve@^1.1.6, resolve@^1.10.0, resolve@^1.14.2, resolve@^1.15.0, resolve@^1.19.0, resolve@^1.20.0, resolve@^1.3.2": version: 1.20.0 resolution: "resolve@npm:1.20.0"