Skip to content

Commit dec7683

Browse files
authored
fix: correctly resolve remapped directories (#12373)
1 parent bb09565 commit dec7683

File tree

7 files changed

+83
-62
lines changed

7 files changed

+83
-62
lines changed

Diff for: CHANGELOG.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
- `[jest-environment-node]` [**BREAKING**] Add default `node` and `node-addon` conditions to `exportConditions` for `node` environment ([#11924](https://github.com/facebook/jest/pull/11924))
1313
- `[@jest/expect]` New module which extends `expect` with `jest-snapshot` matchers ([#12404](https://github.com/facebook/jest/pull/12404), [#12410](https://github.com/facebook/jest/pull/12410), [#12418](https://github.com/facebook/jest/pull/12418))
1414
- `[@jest/expect-utils]` New module exporting utils for `expect` ([#12323](https://github.com/facebook/jest/pull/12323))
15-
- `[jest-resolver]` [**BREAKING**] Add support for `package.json` `exports` ([11961](https://github.com/facebook/jest/pull/11961))
15+
- `[jest-resolve]` [**BREAKING**] Add support for `package.json` `exports` ([#11961](https://github.com/facebook/jest/pull/11961), [#12373](https://github.com/facebook/jest/pull/12373))
1616
- `[jest-resolve, jest-runtime]` Add support for `data:` URI import and mock ([#12392](https://github.com/facebook/jest/pull/12392))
1717
- `[@jest/schemas]` New module for JSON schemas for Jest's config ([#12384](https://github.com/facebook/jest/pull/12384))
1818
- `[jest-worker]` [**BREAKING**] Allow only absolute `workerPath` ([#12343](https://github.com/facebook/jest/pull/12343))

Diff for: e2e/runtime-internal-module-registry/__tests__/runtimeInternalModuleRegistry.test.js

+3-3
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,10 @@ describe('Runtime internal module registry', () => {
1313
it('behaves correctly when requiring a module that is used by jest internals', () => {
1414
const fs = require('fs');
1515

16-
// We require from this crazy path so that we can mimick Jest (and it's
17-
// transitive deps) being installed along side a projects deps (e.g. with an
16+
// We require from this crazy path so that we can mimick Jest (and its
17+
// transitive deps) being installed alongside a projects deps (e.g. with an
1818
// NPM3 flat dep tree)
19-
const jestUtil = require('../../../packages/jest-util');
19+
const jestUtil = require('jest-util');
2020

2121
// If FS is mocked correctly, this folder won't actually be created on the
2222
// filesystem

Diff for: packages/jest-resolve/src/__mocks__/conditions/node_modules/exports/package.json

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

Diff for: packages/jest-resolve/src/__mocks__/conditions/node_modules/exports/some-other-directory/file.js

Whitespace-only changes.

Diff for: packages/jest-resolve/src/__tests__/resolve.test.ts

+14
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,20 @@ describe('findNodeModule', () => {
234234
path.resolve(conditionsRoot, './node_modules/exports/nestedDefault.js'),
235235
);
236236
});
237+
238+
test('supports separate directory path', () => {
239+
const result = Resolver.findNodeModule('exports/directory/file.js', {
240+
basedir: conditionsRoot,
241+
conditions: [],
242+
});
243+
244+
expect(result).toEqual(
245+
path.resolve(
246+
conditionsRoot,
247+
'./node_modules/exports/some-other-directory/file.js',
248+
),
249+
);
250+
});
237251
});
238252
});
239253

Diff for: packages/jest-resolve/src/defaultResolver.ts

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

8-
import {isAbsolute} from 'path';
8+
import {dirname, isAbsolute, resolve as pathResolve} from 'path';
99
import pnpResolver from 'jest-pnp-resolver';
10-
import {sync as resolveSync} from 'resolve';
10+
import {SyncOpts as UpstreamResolveOptions, sync as resolveSync} from 'resolve';
1111
import {
1212
Options as ResolveExportsOptions,
1313
resolve as resolveExports,
1414
} from 'resolve.exports';
15-
import slash = require('slash');
1615
import {
1716
PkgJson,
1817
isDirectory,
@@ -36,6 +35,9 @@ interface ResolverOptions {
3635
pathFilter?: (pkg: PkgJson, path: string, relativePath: string) => string;
3736
}
3837

38+
type UpstreamResolveOptionsWithConditions = UpstreamResolveOptions &
39+
Pick<ResolverOptions, 'conditions'>;
40+
3941
// https://github.com/facebook/jest/pull/10617
4042
declare global {
4143
namespace NodeJS {
@@ -55,16 +57,22 @@ export default function defaultResolver(
5557
return pnpResolver(path, options);
5658
}
5759

58-
const result = resolveSync(path, {
60+
const resolveOptions: UpstreamResolveOptionsWithConditions = {
5961
...options,
6062
isDirectory,
6163
isFile,
62-
packageFilter: createPackageFilter(path, options.packageFilter),
63-
pathFilter: createPathFilter(path, options.conditions, options.pathFilter),
6464
preserveSymlinks: false,
6565
readPackageSync,
6666
realpathSync,
67-
});
67+
};
68+
69+
const pathToResolve = getPathInModule(path, resolveOptions);
70+
71+
const result =
72+
// if `getPathInModule` doesn't change the path, attempt to resolve it
73+
pathToResolve === path
74+
? resolveSync(pathToResolve, resolveOptions)
75+
: pathToResolve;
6876

6977
// Dereference symlinks to ensure we don't create a separate
7078
// module instance depending on how it was referenced.
@@ -79,67 +87,65 @@ function readPackageSync(_: unknown, file: string): PkgJson {
7987
return readPackageCached(file);
8088
}
8189

82-
function createPackageFilter(
83-
originalPath: string,
84-
userFilter?: ResolverOptions['packageFilter'],
85-
): ResolverOptions['packageFilter'] {
86-
if (shouldIgnoreRequestForExports(originalPath)) {
87-
return userFilter;
90+
function getPathInModule(
91+
path: string,
92+
options: UpstreamResolveOptionsWithConditions,
93+
): string {
94+
if (shouldIgnoreRequestForExports(path)) {
95+
return path;
8896
}
8997

90-
return function packageFilter(pkg, ...rest) {
91-
let filteredPkg = pkg;
98+
const segments = path.split('/');
9299

93-
if (userFilter) {
94-
filteredPkg = userFilter(filteredPkg, ...rest);
95-
}
100+
let moduleName = segments.shift();
96101

97-
if (filteredPkg.exports == null) {
98-
return filteredPkg;
102+
if (moduleName) {
103+
// TODO: handle `#` here: https://github.com/facebook/jest/issues/12270
104+
if (moduleName.startsWith('@')) {
105+
moduleName = `${moduleName}/${segments.shift()}`;
99106
}
100107

101-
return {
102-
...filteredPkg,
103-
// remove `main` so `resolve` doesn't look at it and confuse the `.`
104-
// loading in `pathFilter`
105-
main: undefined,
106-
};
107-
};
108-
}
108+
let packageJsonPath = '';
109109

110-
function createPathFilter(
111-
originalPath: string,
112-
conditions?: Array<string>,
113-
userFilter?: ResolverOptions['pathFilter'],
114-
): ResolverOptions['pathFilter'] {
115-
if (shouldIgnoreRequestForExports(originalPath)) {
116-
return userFilter;
117-
}
110+
try {
111+
packageJsonPath = resolveSync(`${moduleName}/package.json`, options);
112+
} catch {
113+
// ignore if package.json cannot be found
114+
}
118115

119-
const options: ResolveExportsOptions = conditions
120-
? {conditions, unsafe: true}
121-
: // no conditions were passed - let's assume this is Jest internal and it should be `require`
122-
{browser: false, require: true};
116+
if (packageJsonPath && isFile(packageJsonPath)) {
117+
const pkg = readPackageCached(packageJsonPath);
123118

124-
return function pathFilter(pkg, path, relativePath, ...rest) {
125-
let pathToUse = relativePath;
119+
if (pkg.exports) {
120+
// we need to make sure resolve ignores `main`
121+
delete pkg.main;
126122

127-
if (userFilter) {
128-
pathToUse = userFilter(pkg, path, relativePath, ...rest);
129-
}
123+
const subpath = segments.join('/') || '.';
130124

131-
if (pkg.exports == null) {
132-
return pathToUse;
133-
}
125+
const resolved = resolveExports(
126+
pkg,
127+
subpath,
128+
createResolveOptions(options.conditions),
129+
);
134130

135-
// this `index` thing can backfire, but `resolve` adds it: https://github.com/browserify/resolve/blob/f1b51848ecb7f56f77bfb823511d032489a13eab/lib/sync.js#L192
136-
const isRootRequire =
137-
pathToUse === 'index' && !originalPath.endsWith('/index');
131+
// TODO: should we throw if not?
132+
if (resolved) {
133+
return pathResolve(dirname(packageJsonPath), resolved);
134+
}
135+
}
136+
}
137+
}
138138

139-
const newPath = isRootRequire ? '.' : slash(pathToUse);
139+
return path;
140+
}
140141

141-
return resolveExports(pkg, newPath, options) || pathToUse;
142-
};
142+
function createResolveOptions(
143+
conditions: Array<string> | undefined,
144+
): ResolveExportsOptions {
145+
return conditions
146+
? {conditions, unsafe: true}
147+
: // no conditions were passed - let's assume this is Jest internal and it should be `require`
148+
{browser: false, require: true};
143149
}
144150

145151
// if it's a relative import or an absolute path, exports are ignored

Diff for: packages/jest-worker/src/__tests__/leak-integration.test.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import {tmpdir} from 'os';
99
import {join} from 'path';
1010
import {writeFileSync} from 'graceful-fs';
1111
import LeakDetector from 'jest-leak-detector';
12-
import {Worker} from '../..';
12+
import {Worker} from '../../build/index';
1313

1414
let workerFile!: string;
1515
beforeAll(() => {
@@ -30,10 +30,10 @@ afterEach(async () => {
3030

3131
it('does not retain arguments after a task finished', async () => {
3232
let leakDetector!: LeakDetector;
33-
await new Promise(resolve => {
33+
await new Promise((resolve, reject) => {
3434
const obj = {};
3535
leakDetector = new LeakDetector(obj);
36-
(worker as any).fn(obj).then(resolve);
36+
(worker as any).fn(obj).then(resolve, reject);
3737
});
3838

3939
expect(await leakDetector.isLeaking()).toBe(false);

0 commit comments

Comments
 (0)