diff --git a/CHANGELOG.md b/CHANGELOG.md index 4090888c91cc..4366e726c58a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ - `[@jest/expect-utils]` New module exporting utils for `expect` ([#12323](https://github.com/facebook/jest/pull/12323)) - `[jest-jasmine2, jest-runtime]` [**BREAKING**] Use `Symbol` to pass `jest.setTimeout` value instead of `jasmine` specific logic ([#12124](https://github.com/facebook/jest/pull/12124)) - `[jest-jasmine2, jest-types]` [**BREAKING**] Move all `jasmine` specific types from `@jest/types` to its own package ([#12125](https://github.com/facebook/jest/pull/12125)) +- `[jest-resolver]` [**BREAKING**] Add support for `package.json` `exports` ([11961](https://github.com/facebook/jest/pull/11961)) - `[jest-snapshot]` [**BREAKING**] Migrate to ESM ([#12342](https://github.com/facebook/jest/pull/12342)) - `[jest-worker]` [**BREAKING**] Allow only absolute `workerPath` ([#12343](https://github.com/facebook/jest/pull/12343)) diff --git a/examples/angular/jest.config.js b/examples/angular/jest.config.js index 06e6c25e3075..18094f1ba955 100644 --- a/examples/angular/jest.config.js +++ b/examples/angular/jest.config.js @@ -1,7 +1,7 @@ module.exports = { moduleFileExtensions: ['ts', 'html', 'js', 'json'], setupFilesAfterEnv: ['/setupJest.js'], - testEnvironment: 'jsdom', + testEnvironment: '/test-env.js', transform: { '\\.[tj]s$': ['babel-jest', {configFile: require.resolve('./.babelrc')}], }, diff --git a/examples/angular/test-env.js b/examples/angular/test-env.js new file mode 100644 index 000000000000..542775fe3eb4 --- /dev/null +++ b/examples/angular/test-env.js @@ -0,0 +1,13 @@ +'use strict'; + +const { + TestEnvironment: JSDOMTestEnvironment, +} = require('jest-environment-jsdom'); + +module.exports = class AngularEnv extends JSDOMTestEnvironment { + exportConditions() { + // we need to include `node` as `rxjs` defines `node`, `es2015`, `default`, not `browser` or `require` + // https://github.com/ReactiveX/rxjs/pull/6821 + return super.exportConditions().concat('node'); + } +}; diff --git a/packages/jest-resolve/src/__mocks__/conditions/node_modules/import/file.js b/packages/jest-resolve/src/__mocks__/conditions/node_modules/exports/default.js similarity index 100% rename from packages/jest-resolve/src/__mocks__/conditions/node_modules/import/file.js rename to packages/jest-resolve/src/__mocks__/conditions/node_modules/exports/default.js diff --git a/packages/jest-resolve/src/__mocks__/conditions/node_modules/require/file.js b/packages/jest-resolve/src/__mocks__/conditions/node_modules/exports/import.js similarity index 100% rename from packages/jest-resolve/src/__mocks__/conditions/node_modules/require/file.js rename to packages/jest-resolve/src/__mocks__/conditions/node_modules/exports/import.js diff --git a/packages/jest-resolve/src/__mocks__/conditions/node_modules/exports/main.js b/packages/jest-resolve/src/__mocks__/conditions/node_modules/exports/main.js new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/packages/jest-resolve/src/__mocks__/conditions/node_modules/exports/nestedDefault.js b/packages/jest-resolve/src/__mocks__/conditions/node_modules/exports/nestedDefault.js new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/packages/jest-resolve/src/__mocks__/conditions/node_modules/exports/nestedRequire.js b/packages/jest-resolve/src/__mocks__/conditions/node_modules/exports/nestedRequire.js new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/packages/jest-resolve/src/__mocks__/conditions/node_modules/exports/other.js b/packages/jest-resolve/src/__mocks__/conditions/node_modules/exports/other.js new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/packages/jest-resolve/src/__mocks__/conditions/node_modules/exports/package.json b/packages/jest-resolve/src/__mocks__/conditions/node_modules/exports/package.json new file mode 100644 index 000000000000..7fd4bab2a661 --- /dev/null +++ b/packages/jest-resolve/src/__mocks__/conditions/node_modules/exports/package.json @@ -0,0 +1,16 @@ +{ + "name": "import", + "main": "main.js", + "exports": { + ".": { + "require": "./require.js", + "import": "./import.js", + "default": "./default.js" + }, + "./nested": "./nestedDefault.js", + "./deeplyNested" : { + "require": "./nestedRequire.js", + "default": "./nestedDefault.js" + } + } +} diff --git a/packages/jest-resolve/src/__mocks__/conditions/node_modules/exports/require.js b/packages/jest-resolve/src/__mocks__/conditions/node_modules/exports/require.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 deleted file mode 100644 index 24fc72b1cac7..000000000000 --- a/packages/jest-resolve/src/__mocks__/conditions/node_modules/import/package.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "name": "import", - "exports": { - "import": "./file.js" - } -} 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 deleted file mode 100644 index c42b33ecca86..000000000000 --- a/packages/jest-resolve/src/__mocks__/conditions/node_modules/require/package.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "name": "require", - "exports": { - "require": "./file.js" - } -} diff --git a/packages/jest-resolve/src/__tests__/resolve.test.ts b/packages/jest-resolve/src/__tests__/resolve.test.ts index e1d84f7c106e..ab1f19149ac4 100644 --- a/packages/jest-resolve/src/__tests__/resolve.test.ts +++ b/packages/jest-resolve/src/__tests__/resolve.test.ts @@ -168,24 +168,78 @@ describe('findNodeModule', () => { }); test('resolves with import', () => { - const result = Resolver.findNodeModule('import', { + const result = Resolver.findNodeModule('exports', { basedir: conditionsRoot, conditions: ['import'], }); expect(result).toEqual( - path.resolve(conditionsRoot, './node_modules/import/file.js'), + path.resolve(conditionsRoot, './node_modules/exports/import.js'), ); }); test('resolves with require', () => { - const result = Resolver.findNodeModule('require', { + const result = Resolver.findNodeModule('exports', { basedir: conditionsRoot, conditions: ['require'], }); expect(result).toEqual( - path.resolve(conditionsRoot, './node_modules/require/file.js'), + path.resolve(conditionsRoot, './node_modules/exports/require.js'), + ); + }); + + test('gets default when nothing is passed', () => { + const result = Resolver.findNodeModule('exports', { + basedir: conditionsRoot, + conditions: [], + }); + + expect(result).toEqual( + path.resolve(conditionsRoot, './node_modules/exports/default.js'), + ); + }); + + test('respects order in package.json, not conditions', () => { + const resultImport = Resolver.findNodeModule('exports', { + basedir: conditionsRoot, + conditions: ['import', 'require'], + }); + const resultRequire = Resolver.findNodeModule('exports', { + basedir: conditionsRoot, + conditions: ['require', 'import'], + }); + + expect(resultImport).toEqual(resultRequire); + }); + + test('supports nested paths', () => { + const result = Resolver.findNodeModule('exports/nested', { + basedir: conditionsRoot, + conditions: [], + }); + + expect(result).toEqual( + path.resolve(conditionsRoot, './node_modules/exports/nestedDefault.js'), + ); + }); + + test('supports nested conditions', () => { + const resultRequire = Resolver.findNodeModule('exports/deeplyNested', { + basedir: conditionsRoot, + conditions: ['require'], + }); + const resultDefault = Resolver.findNodeModule('exports/deeplyNested', { + basedir: conditionsRoot, + conditions: [], + }); + + expect(resultRequire).toEqual( + path.resolve(conditionsRoot, './node_modules/exports/nestedRequire.js'), + ); + + expect(resultDefault).toEqual( + path.resolve(conditionsRoot, './node_modules/exports/nestedDefault.js'), ); }); }); @@ -319,8 +373,8 @@ describe('resolveModule', () => { const src = require.resolve('../'); const resolved = resolver.resolveModule(src, 'mockJsDependency', { paths: [ - path.resolve(__dirname, '../../src/__tests__'), path.resolve(__dirname, '../../src/__mocks__'), + path.resolve(__dirname, '../../src/__tests__'), ], }); expect(resolved).toBe(require.resolve('../__mocks__/mockJsDependency.js')); diff --git a/packages/jest-resolve/src/defaultResolver.ts b/packages/jest-resolve/src/defaultResolver.ts index 5e0ca9c15dd1..217f5a225b21 100644 --- a/packages/jest-resolve/src/defaultResolver.ts +++ b/packages/jest-resolve/src/defaultResolver.ts @@ -5,13 +5,14 @@ * LICENSE file in the root directory of this source tree. */ -import {resolve} from 'path'; +import {isAbsolute} from 'path'; import pnpResolver from 'jest-pnp-resolver'; import resolveAsync = require('resolve'); import { Options as ResolveExportsOptions, resolve as resolveExports, } from 'resolve.exports'; +import slash = require('slash'); import type {Config} from '@jest/types'; import { PkgJson, @@ -106,10 +107,8 @@ function getSyncResolveOptions( ...options, isDirectory: isDirectorySync, isFile: isFileSync, - packageFilter: createPackageFilter( - options.conditions, - options.packageFilter, - ), + packageFilter: createPackageFilter(path, options.packageFilter), + pathFilter: createPathFilter(path, options.conditions, options.pathFilter), preserveSymlinks: false, readPackageSync, realpathSync, @@ -159,45 +158,68 @@ function readPackageAsync( } function createPackageFilter( - conditions?: Array, + originalPath: Config.Path, 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; - } + if (shouldIgnoreRequestForExports(originalPath)) { + return userFilter; } - return function packageFilter(pkg, packageDir) { + return function packageFilter(pkg, ...rest) { let filteredPkg = pkg; if (userFilter) { - filteredPkg = userFilter(filteredPkg, packageDir); + filteredPkg = userFilter(filteredPkg, ...rest); } - 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 (isFileSync(indexInRoot)) { + if (filteredPkg.exports == null) { return filteredPkg; } return { ...filteredPkg, - main: attemptExportsFallback(filteredPkg), + // remove `main` so `resolve` doesn't look at it and confuse the `.` + // loading in `pathFilter` + main: undefined, }; }; } + +function createPathFilter( + originalPath: Config.Path, + conditions?: Array, + userFilter?: ResolverOptions['pathFilter'], +): ResolverOptions['pathFilter'] { + if (shouldIgnoreRequestForExports(originalPath)) { + return userFilter; + } + + 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}; + + return function pathFilter(pkg, path, relativePath, ...rest) { + let pathToUse = relativePath; + + if (userFilter) { + pathToUse = userFilter(pkg, path, relativePath, ...rest); + } + + if (pkg.exports == null) { + return pathToUse; + } + + // this `index` thing can backfire, but `resolve` adds it: https://github.com/browserify/resolve/blob/f1b51848ecb7f56f77bfb823511d032489a13eab/lib/sync.js#L192 + const isRootRequire = + pathToUse === 'index' && !originalPath.endsWith('/index'); + + const newPath = isRootRequire ? '.' : slash(pathToUse); + + return resolveExports(pkg, newPath, options) || pathToUse; + }; +} + +// if it's a relative import or an absolute path, exports are ignored +const shouldIgnoreRequestForExports = (path: Config.Path) => + path.startsWith('.') || isAbsolute(path); diff --git a/packages/jest-runtime/src/__tests__/NODE_PATH_dir/package.json b/packages/jest-runtime/src/__tests__/NODE_PATH_dir/package.json new file mode 100644 index 000000000000..103175ed9050 --- /dev/null +++ b/packages/jest-runtime/src/__tests__/NODE_PATH_dir/package.json @@ -0,0 +1,6 @@ +{ + "name": "NODE_PATH_dir", + "version": "1.0.0", + "dependencies": { + } +} diff --git a/packages/jest-runtime/src/__tests__/test_root/package.json b/packages/jest-runtime/src/__tests__/test_root/package.json new file mode 100644 index 000000000000..48df13f673ca --- /dev/null +++ b/packages/jest-runtime/src/__tests__/test_root/package.json @@ -0,0 +1,6 @@ +{ + "name": "test_root", + "version": "1.0.0", + "dependencies": { + } +} diff --git a/packages/jest-runtime/src/index.ts b/packages/jest-runtime/src/index.ts index 22be30d419d4..3940af1822de 100644 --- a/packages/jest-runtime/src/index.ts +++ b/packages/jest-runtime/src/index.ts @@ -754,7 +754,7 @@ export default class Runtime { this._virtualMocks, from, moduleName, - isInternal ? undefined : {conditions: this.cjsConditions}, + {conditions: this.cjsConditions}, ); let modulePath: string | undefined; @@ -782,11 +782,9 @@ export default class Runtime { } if (!modulePath) { - modulePath = this._resolveModule( - from, - moduleName, - isInternal ? undefined : {conditions: this.cjsConditions}, - ); + modulePath = this._resolveModule(from, moduleName, { + conditions: this.cjsConditions, + }); } if (this.unstable_shouldLoadAsEsm(modulePath)) {