Skip to content

Commit 7bb7369

Browse files
committed
chore: migrate resolve and resolve.exports to unrs-resolver
close #15600
1 parent 321604a commit 7bb7369

File tree

5 files changed

+117
-308
lines changed

5 files changed

+117
-308
lines changed

packages/jest-resolve/package.json

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,17 +22,13 @@
2222
"chalk": "^4.0.0",
2323
"graceful-fs": "^4.2.9",
2424
"jest-haste-map": "workspace:*",
25-
"jest-pnp-resolver": "^1.2.2",
2625
"jest-util": "workspace:*",
2726
"jest-validate": "workspace:*",
28-
"resolve": "^1.20.0",
29-
"resolve.exports": "^2.0.0",
30-
"slash": "^3.0.0"
27+
"slash": "^3.0.0",
28+
"unrs-resolver": "^1.7.2"
3129
},
3230
"devDependencies": {
33-
"@types/graceful-fs": "^4.1.3",
34-
"@types/pnpapi": "^0.0.5",
35-
"@types/resolve": "^1.20.2"
31+
"@types/graceful-fs": "^4.1.3"
3632
},
3733
"engines": {
3834
"node": "^16.10.0 || ^18.12.0 || >=20.0.0"

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

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
*/
88

99
import * as path from 'path';
10-
import {fileURLToPath, pathToFileURL} from 'url';
10+
import {pathToFileURL} from 'url';
1111
import * as fs from 'graceful-fs';
1212
import {sync as resolveSync} from 'resolve';
1313
import {type IModuleMap, ModuleMap} from 'jest-haste-map';
@@ -20,18 +20,16 @@ import type {ResolverConfig} from '../types';
2020

2121
jest.mock('../__mocks__/userResolver').mock('../__mocks__/userResolverAsync');
2222

23-
// Do not fully mock `resolve` because it is used by Jest. Doing it will crash
23+
// Do not fully mock `unrs-resolver` because it is used by Jest. Doing it will crash
2424
// in very strange ways. Instead, just spy on it and its `sync` method.
25-
jest.mock('resolve', () => {
26-
const originalModule =
27-
jest.requireActual<typeof import('resolve')>('resolve');
28-
29-
const m = jest.fn<typeof import('resolve')>((...args) =>
30-
originalModule(...args),
31-
);
32-
Object.assign(m, originalModule);
33-
m.sync = jest.spyOn(originalModule, 'sync');
34-
25+
jest.mock('unrs-resolver', () => {
26+
const originalResolverFactory =
27+
jest.requireActual<typeof import('unrs-resolver')>(
28+
'unrs-resolver',
29+
).ResolverFactory;
30+
31+
const m = jest.fn<import('unrs-resolver').ResolverFactory['sync']>();
32+
jest.spyOn(originalResolverFactory.prototype, 'sync');
3533
return m;
3634
});
3735

packages/jest-resolve/src/defaultResolver.ts

Lines changed: 16 additions & 172 deletions
Original file line numberDiff line numberDiff line change
@@ -5,21 +5,10 @@
55
* LICENSE file in the root directory of this source tree.
66
*/
77

8-
import {dirname, isAbsolute, resolve as pathResolve} from 'path';
9-
import {fileURLToPath} from 'url';
10-
import pnpResolver from 'jest-pnp-resolver';
118
import {
12-
type SyncOpts as UpstreamResolveOptions,
13-
sync as resolveSync,
14-
} from 'resolve';
15-
import * as resolve from 'resolve.exports';
16-
import {
17-
findClosestPackageJson,
18-
isDirectory,
19-
isFile,
20-
readPackageCached,
21-
realpathSync,
22-
} from './fileWalkers';
9+
ResolverFactory,
10+
type NapiResolveOptions as UpstreamResolveOptions,
11+
} from 'unrs-resolver';
2312
import type {PackageJSON} from './types';
2413

2514
/**
@@ -34,7 +23,7 @@ import type {PackageJSON} from './types';
3423
export type PackageFilter = (
3524
pkg: PackageJSON,
3625
file: string,
37-
dir: string,
26+
dir?: string,
3827
) => PackageJSON;
3928

4029
/**
@@ -52,15 +41,13 @@ export type PathFilter = (
5241
relativePath: string,
5342
) => string;
5443

55-
export type ResolverOptions = {
44+
export interface ResolverOptions extends UpstreamResolveOptions {
5645
/** Directory to begin resolving from. */
5746
basedir: string;
5847
/** List of export conditions. */
5948
conditions?: Array<string>;
6049
/** Instance of default resolver. */
6150
defaultResolver: typeof defaultResolver;
62-
/** List of file extensions to search in order. */
63-
extensions?: Array<string>;
6451
/**
6552
* List of directory names to be looked up for modules recursively.
6653
*
@@ -79,12 +66,7 @@ export type ResolverOptions = {
7966
packageFilter?: PackageFilter;
8067
/** Allows transforms a path within a package. */
8168
pathFilter?: PathFilter;
82-
/** Current root directory. */
83-
rootDir?: string;
84-
};
85-
86-
type UpstreamResolveOptionsWithConditions = UpstreamResolveOptions &
87-
ResolverOptions;
69+
}
8870

8971
export type SyncResolver = (path: string, options: ResolverOptions) => string;
9072
export type AsyncResolver = (
@@ -95,159 +77,21 @@ export type AsyncResolver = (
9577
export type Resolver = SyncResolver | AsyncResolver;
9678

9779
const defaultResolver: SyncResolver = (path, options) => {
98-
// Yarn 2 adds support to `resolve` automatically so the pnpResolver is only
99-
// needed for Yarn 1 which implements version 1 of the pnp spec
100-
if (process.versions.pnp === '1') {
101-
return pnpResolver(path, options);
102-
}
103-
104-
const resolveOptions: UpstreamResolveOptionsWithConditions = {
80+
const resolveOptions: UpstreamResolveOptions = {
10581
...options,
106-
isDirectory,
107-
isFile,
108-
preserveSymlinks: false,
109-
readPackageSync,
110-
realpathSync,
82+
conditionNames: options.conditionNames || options.conditions,
83+
modules: options.modules || options.moduleDirectory,
11184
};
11285

113-
const pathToResolve = getPathInModule(path, resolveOptions);
114-
115-
// resolveSync dereferences symlinks to ensure we don't create a separate
116-
// module instance depending on how it was referenced.
117-
const result = resolveSync(pathToResolve, resolveOptions);
118-
119-
return result;
120-
};
121-
122-
export default defaultResolver;
123-
124-
/*
125-
* helper functions
126-
*/
127-
128-
function readPackageSync(_: unknown, file: string): PackageJSON {
129-
return readPackageCached(file);
130-
}
86+
const unrsResolver = new ResolverFactory(resolveOptions);
13187

132-
function getPathInModule(
133-
path: string,
134-
options: UpstreamResolveOptionsWithConditions,
135-
): string {
136-
if (path.startsWith('file://')) {
137-
path = fileURLToPath(path);
138-
}
88+
const result = unrsResolver.sync(options.basedir, path);
13989

140-
if (shouldIgnoreRequestForExports(path)) {
141-
return path;
90+
if (result.error) {
91+
throw new Error(result.error);
14292
}
14393

144-
if (path.startsWith('#')) {
145-
const closestPackageJson = findClosestPackageJson(options.basedir);
146-
147-
if (!closestPackageJson) {
148-
throw new Error(
149-
`Jest: unable to locate closest package.json from ${options.basedir} when resolving import "${path}"`,
150-
);
151-
}
152-
153-
const pkg = readPackageCached(closestPackageJson);
154-
155-
const resolved = resolve.imports(
156-
pkg,
157-
path as resolve.Imports.Entry,
158-
createResolveOptions(options.conditions),
159-
);
160-
161-
if (resolved) {
162-
const target = resolved[0];
163-
return target.startsWith('.')
164-
? // internal relative filepath
165-
pathResolve(dirname(closestPackageJson), target)
166-
: // this is an external module, re-resolve it
167-
defaultResolver(target, options);
168-
}
169-
170-
if (pkg.imports) {
171-
throw new Error(
172-
'`imports` exists, but no results - this is a bug in Jest. Please report an issue',
173-
);
174-
}
175-
}
176-
177-
const segments = path.split('/');
178-
179-
let moduleName = segments.shift();
180-
181-
if (moduleName) {
182-
if (moduleName.startsWith('@')) {
183-
moduleName = `${moduleName}/${segments.shift()}`;
184-
}
185-
186-
// self-reference
187-
const closestPackageJson = findClosestPackageJson(options.basedir);
188-
if (closestPackageJson) {
189-
const pkg = readPackageCached(closestPackageJson);
190-
191-
if (pkg.name === moduleName) {
192-
const resolved = resolve.exports(
193-
pkg,
194-
(segments.join('/') || '.') as resolve.Exports.Entry,
195-
createResolveOptions(options.conditions),
196-
);
197-
198-
if (resolved) {
199-
return pathResolve(dirname(closestPackageJson), resolved[0]);
200-
}
201-
202-
if (pkg.exports) {
203-
throw new Error(
204-
'`exports` exists, but no results - this is a bug in Jest. Please report an issue',
205-
);
206-
}
207-
}
208-
}
209-
210-
let packageJsonPath = '';
211-
212-
try {
213-
packageJsonPath = resolveSync(`${moduleName}/package.json`, options);
214-
} catch {
215-
// ignore if package.json cannot be found
216-
}
217-
218-
if (packageJsonPath && isFile(packageJsonPath)) {
219-
const pkg = readPackageCached(packageJsonPath);
220-
221-
const resolved = resolve.exports(
222-
pkg,
223-
(segments.join('/') || '.') as resolve.Exports.Entry,
224-
createResolveOptions(options.conditions),
225-
);
226-
227-
if (resolved) {
228-
return pathResolve(dirname(packageJsonPath), resolved[0]);
229-
}
230-
231-
if (pkg.exports) {
232-
throw new Error(
233-
'`exports` exists, but no results - this is a bug in Jest. Please report an issue',
234-
);
235-
}
236-
}
237-
}
238-
239-
return path;
240-
}
241-
242-
function createResolveOptions(
243-
conditions: Array<string> | undefined,
244-
): resolve.Options {
245-
return conditions
246-
? {conditions, unsafe: true}
247-
: // no conditions were passed - let's assume this is Jest internal and it should be `require`
248-
{browser: false, require: true};
249-
}
94+
return result.path!;
95+
};
25096

251-
// if it's a relative import or an absolute path, imports/exports are ignored
252-
const shouldIgnoreRequestForExports = (path: string) =>
253-
path.startsWith('.') || isAbsolute(path);
97+
export default defaultResolver;

packages/jest-resolve/src/resolver.ts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,6 @@ export default class Resolver {
126126
extensions: options.extensions,
127127
moduleDirectory: options.moduleDirectory,
128128
paths: paths ? [...(nodePaths || []), ...paths] : nodePaths,
129-
rootDir: options.rootDir,
130129
});
131130
} catch (error) {
132131
// we always wanna throw if it's an internal import
@@ -169,7 +168,6 @@ export default class Resolver {
169168
extensions: options.extensions,
170169
moduleDirectory: options.moduleDirectory,
171170
paths: paths ? [...(nodePaths || []), ...paths] : nodePaths,
172-
rootDir: options.rootDir,
173171
});
174172
return result;
175173
} catch (error: unknown) {
@@ -341,7 +339,7 @@ export default class Resolver {
341339
resolveModule(
342340
from: string,
343341
moduleName: string,
344-
options: ResolveModuleConfig,
342+
options?: ResolveModuleConfig,
345343
): string {
346344
const dirname = path.dirname(from);
347345
const module =
@@ -485,7 +483,7 @@ export default class Resolver {
485483
getMockModule(
486484
from: string,
487485
name: string,
488-
options: Pick<ResolveModuleConfig, 'conditions'>,
486+
options?: Pick<ResolveModuleConfig, 'conditions'>,
489487
): string | null {
490488
const mock = this._moduleMap.getMockModule(name);
491489
if (mock) {
@@ -731,7 +729,7 @@ export default class Resolver {
731729
resolveStubModuleName(
732730
from: string,
733731
moduleName: string,
734-
options: Pick<ResolveModuleConfig, 'conditions'>,
732+
options?: Pick<ResolveModuleConfig, 'conditions'>,
735733
): string | null {
736734
const dirname = path.dirname(from);
737735

0 commit comments

Comments
 (0)