Skip to content

Commit bc3c921

Browse files
authored
feat: support . in exports field (#11919)
1 parent b5aec03 commit bc3c921

26 files changed

+140
-74
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
### Features
44

55
- `[jest-config]` Add `testEnvironmentOptions.html` to apply to jsdom input ([11950](https://github.com/facebook/jest/pull/11950))
6+
- `[jest-resolver]` Support default export (`.`) in `exports` field _if_ `main` is missing ([#11919](https://github.com/facebook/jest/pull/11919))
67

78
### Fixes
89

e2e/__tests__/resolveConditions.test.ts

-5
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,10 @@
77

88
import {resolve} from 'path';
99
import {onNodeVersions} from '@jest/test-utils';
10-
import {runYarnInstall} from '../Utils';
1110
import runJest from '../runJest';
1211

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

15-
beforeAll(() => {
16-
runYarnInstall(dir);
17-
});
18-
1914
// The versions where vm.Module exists and commonjs with "exports" is not broken
2015
onNodeVersions('>=12.16.0', () => {
2116
test('resolves package exports correctly with custom resolver', () => {

e2e/resolve-conditions/__tests__/browser.test.mjs

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
* @jest-environment <rootDir>/browser-env.js
88
*/
99

10-
import {fn} from '../fake-dual-dep';
10+
import {fn} from 'fake-dual-dep';
1111

1212
test('returns correct message', () => {
1313
expect(fn()).toEqual('hello from browser');

e2e/resolve-conditions/__tests__/node.test.mjs

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
* @jest-environment <rootDir>/node-env.js
88
*/
99

10-
import {fn} from '../fake-dual-dep';
10+
import {fn} from 'fake-dual-dep';
1111

1212
test('returns correct message', () => {
1313
expect(fn()).toEqual('hello from node');

e2e/resolve-conditions/__tests__/resolveCjs.test.cjs

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
* LICENSE file in the root directory of this source tree.
66
*/
77

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

1010
test('returns correct message', () => {
1111
expect(fn()).toEqual('hello from CJS');

e2e/resolve-conditions/__tests__/resolveEsm.test.mjs

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
* LICENSE file in the root directory of this source tree.
66
*/
77

8-
import {fn} from '../fake-dep';
8+
import {fn} from 'fake-dep';
99

1010
test('returns correct message', () => {
1111
expect(fn()).toEqual('hello from ESM');

e2e/resolve-conditions/package.json

-4
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,9 @@
66
"mjs",
77
"json"
88
],
9-
"resolver": "<rootDir>/resolver.js",
109
"testMatch": [
1110
"<rootDir>/**/*.test.*"
1211
],
1312
"transform": {}
14-
},
15-
"dependencies": {
16-
"resolve.exports": "^1.1.0"
1713
}
1814
}

e2e/resolve-conditions/resolver.js

-30
This file was deleted.

e2e/resolve-conditions/yarn.lock

-21
This file was deleted.

packages/jest-resolve/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
"jest-util": "^27.2.5",
2323
"jest-validate": "^27.2.5",
2424
"resolve": "^1.20.0",
25+
"resolve.exports": "^1.1.0",
2526
"slash": "^3.0.0"
2627
},
2728
"devDependencies": {

packages/jest-resolve/src/__mocks__/conditions/node_modules/import/file.js

Whitespace-only changes.

packages/jest-resolve/src/__mocks__/conditions/node_modules/import/package.json

+6
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/jest-resolve/src/__mocks__/conditions/node_modules/main/file.js

Whitespace-only changes.

packages/jest-resolve/src/__mocks__/conditions/node_modules/main/package.json

+4
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/jest-resolve/src/__mocks__/conditions/node_modules/require/file.js

Whitespace-only changes.

packages/jest-resolve/src/__mocks__/conditions/node_modules/require/package.json

+6
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"name": "__mocks__",
3+
"version": "1.0.0",
4+
"dependencies": {
5+
}
6+
}

packages/jest-resolve/src/__tests__/resolve.test.ts

+43-8
Original file line numberDiff line numberDiff line change
@@ -126,26 +126,61 @@ describe('findNodeModule', () => {
126126
});
127127
});
128128

129-
it('passes packageFilter to the resolve module when using the default resolver', () => {
129+
it('wraps passed packageFilter to the resolve module when using the default resolver', () => {
130130
const packageFilter = jest.fn();
131131

132132
// A resolver that delegates to defaultResolver with a packageFilter implementation
133133
userResolver.mockImplementation((request, opts) =>
134134
opts.defaultResolver(request, {...opts, packageFilter}),
135135
);
136136

137-
Resolver.findNodeModule('test', {
138-
basedir: '/',
137+
Resolver.findNodeModule('./test', {
138+
basedir: path.resolve(__dirname, '../__mocks__/'),
139139
resolver: require.resolve('../__mocks__/userResolver'),
140140
});
141141

142-
expect(mockResolveSync).toHaveBeenCalledWith(
143-
'test',
144-
expect.objectContaining({
145-
packageFilter,
146-
}),
142+
expect(packageFilter).toHaveBeenCalledWith(
143+
expect.objectContaining({name: '__mocks__'}),
144+
expect.any(String),
147145
);
148146
});
147+
148+
describe('conditions', () => {
149+
const conditionsRoot = path.resolve(__dirname, '../__mocks__/conditions');
150+
151+
test('resolves without exports, just main', () => {
152+
const result = Resolver.findNodeModule('main', {
153+
basedir: conditionsRoot,
154+
conditions: ['require'],
155+
});
156+
157+
expect(result).toEqual(
158+
path.resolve(conditionsRoot, './node_modules/main/file.js'),
159+
);
160+
});
161+
162+
test('resolves with import', () => {
163+
const result = Resolver.findNodeModule('import', {
164+
basedir: conditionsRoot,
165+
conditions: ['import'],
166+
});
167+
168+
expect(result).toEqual(
169+
path.resolve(conditionsRoot, './node_modules/import/file.js'),
170+
);
171+
});
172+
173+
test('resolves with require', () => {
174+
const result = Resolver.findNodeModule('require', {
175+
basedir: conditionsRoot,
176+
conditions: ['require'],
177+
});
178+
179+
expect(result).toEqual(
180+
path.resolve(conditionsRoot, './node_modules/require/file.js'),
181+
);
182+
});
183+
});
149184
});
150185

151186
describe('resolveModule', () => {

packages/jest-resolve/src/defaultResolver.ts

+61-2
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,13 @@
55
* LICENSE file in the root directory of this source tree.
66
*/
77

8+
import {resolve} from 'path';
89
import pnpResolver from 'jest-pnp-resolver';
9-
import {Opts as ResolveOpts, sync as resolveSync} from 'resolve';
10+
import {sync as resolveSync} from 'resolve';
11+
import {
12+
Options as ResolveExportsOptions,
13+
resolve as resolveExports,
14+
} from 'resolve.exports';
1015
import type {Config} from '@jest/types';
1116
import {
1217
PkgJson,
@@ -16,13 +21,19 @@ import {
1621
realpathSync,
1722
} from './fileWalkers';
1823

19-
interface ResolverOptions extends ResolveOpts {
24+
// copy from `resolve`'s types so we don't have their types in our definition
25+
// files
26+
interface ResolverOptions {
2027
basedir: Config.Path;
2128
browser?: boolean;
2229
conditions?: Array<string>;
2330
defaultResolver: typeof defaultResolver;
2431
extensions?: Array<string>;
32+
moduleDirectory?: Array<string>;
33+
paths?: Array<Config.Path>;
2534
rootDir?: Config.Path;
35+
packageFilter?: (pkg: PkgJson, dir: string) => PkgJson;
36+
pathFilter?: (pkg: PkgJson, path: string, relativePath: string) => string;
2637
}
2738

2839
// https://github.com/facebook/jest/pull/10617
@@ -48,6 +59,10 @@ export default function defaultResolver(
4859
...options,
4960
isDirectory,
5061
isFile,
62+
packageFilter: createPackageFilter(
63+
options.conditions,
64+
options.packageFilter,
65+
),
5166
preserveSymlinks: false,
5267
readPackageSync,
5368
realpathSync,
@@ -65,3 +80,47 @@ export default function defaultResolver(
6580
function readPackageSync(_: unknown, file: Config.Path): PkgJson {
6681
return readPackageCached(file);
6782
}
83+
84+
function createPackageFilter(
85+
conditions?: Array<string>,
86+
userFilter?: ResolverOptions['packageFilter'],
87+
): ResolverOptions['packageFilter'] {
88+
function attemptExportsFallback(pkg: PkgJson) {
89+
const options: ResolveExportsOptions = conditions
90+
? {conditions, unsafe: true}
91+
: // no conditions were passed - let's assume this is Jest internal and it should be `require`
92+
{browser: false, require: true};
93+
94+
try {
95+
return resolveExports(pkg, '.', options);
96+
} catch {
97+
return undefined;
98+
}
99+
}
100+
101+
return function packageFilter(pkg, packageDir) {
102+
let filteredPkg = pkg;
103+
104+
if (userFilter) {
105+
filteredPkg = userFilter(filteredPkg, packageDir);
106+
}
107+
108+
if (filteredPkg.main != null) {
109+
return filteredPkg;
110+
}
111+
112+
const indexInRoot = resolve(packageDir, './index.js');
113+
114+
// if the module contains an `index.js` file in root, `resolve` will request
115+
// that if there is no `main`. Since we don't wanna break that, add this
116+
// check
117+
if (isFile(indexInRoot)) {
118+
return filteredPkg;
119+
}
120+
121+
return {
122+
...filteredPkg,
123+
main: attemptExportsFallback(filteredPkg),
124+
};
125+
};
126+
}

yarn.lock

+8
Original file line numberDiff line numberDiff line change
@@ -13026,6 +13026,7 @@ fsevents@^1.2.7:
1302613026
jest-util: ^27.2.5
1302713027
jest-validate: ^27.2.5
1302813028
resolve: ^1.20.0
13029+
resolve.exports: ^1.1.0
1302913030
slash: ^3.0.0
1303013031
languageName: unknown
1303113032
linkType: soft
@@ -18944,6 +18945,13 @@ react-native@0.64.0:
1894418945
languageName: node
1894518946
linkType: hard
1894618947

18948+
"resolve.exports@npm:^1.1.0":
18949+
version: 1.1.0
18950+
resolution: "resolve.exports@npm:1.1.0"
18951+
checksum: d04d2ce651fac14fe6ba13b377690f790cbbe91e6211b8fbec97ee08282e278875c74073a9b6243143a64e33d95eefb479e1dd4965664edc73b28b712100b36c
18952+
languageName: node
18953+
linkType: hard
18954+
1894718955
"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":
1894818956
version: 1.20.0
1894918957
resolution: "resolve@npm:1.20.0"

0 commit comments

Comments
 (0)