Skip to content

Commit e303578

Browse files
robhoganfacebook-github-bot
authored andcommitted
Move package-relative path calculation under context.getPackageForModule (#1268)
Summary: Pull Request resolved: #1268 During resolution using `package.json#exports` or the `browser` spec, we need package-relative subpaths of resolution candidates to check against these export/redirect maps. Currently, we derive these using (pseudo code): ``` const absolutePathOfPackageRoot = context.getPackageForModule(absolutePathOfCandidate).rootPath; const packageRelativePath = path.relative(absolutePathOfPackageRoot, absolutePathOfCandidate); ``` However, this implicitly assumes that both paths are either real, or traverse the same set of symlinks, so that the former is a prefix of the latter. If `getPackageForModule` returned `rootPath: fs.realpath(packageRoot)`, this assumption would not hold. Eg: if `/workspace-root/pgk-a/foo.js` imports `pkg-b/bar.js`, we will try a candidate `/workspace-root/node_modules/pkg-b/bar.js`, but `node_modles/pkg-b` may be a symlink to `/workspace-root/pkg-b`, and `path.relative(realPackageRoot, candidate)` will give `../node_modules/pkg-b/bar.js`, which will not match against an export map, though it represents the same location on the filesystem. Instead, we move the calculation of `packageRelativePath` inside `getPackageForModule` and close to implementation of the package search, where we know that the `path.relative` call is safe under the current implementation, and allowing an alternative implementation to use an alternative mechanism. This is in preparation for a new implementation of `getClosestPackage`, which currently dominates resolution time, using `FileSystem`, which will natively return only *real* paths. Changelog: Internal Reviewed By: huntie Differential Revision: D56823091 fbshipit-source-id: 3007f1732ad2aaede5035d0c4be054304b328de9
1 parent ee6ccc3 commit e303578

File tree

12 files changed

+112
-53
lines changed

12 files changed

+112
-53
lines changed

docs/Resolution.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -210,9 +210,9 @@ The ordered list of fields in `package.json` that should be read to resolve a pa
210210

211211
Given the path to a `package.json` file, returns the parsed file contents.
212212

213-
#### `getPackageForModule: (modulePath: string) => ?PackageInfo` <div class="label deprecated">Deprecated</div>
213+
#### `getPackageForModule: (absoluteModulePath: string) => ?PackageInfo` <div class="label deprecated">Deprecated</div>
214214

215-
Given a module path that may exist under an npm package, locates and returns the package root path and parsed `package.json` contents.
215+
Given a candidate absolute module path that may exist under a package, locates and returns the closest package root (working upwards from the given path, stopping at the nearest `node_modules`), parsed `package.json` contents, and the package-relative path of the given path.
216216

217217
#### `resolveHasteModule: string => ?string`
218218

packages/metro-resolver/src/PackageExportsResolve.js

+5-5
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ export function resolvePackageTargetFromExports(
5151
* to a package-relative subpath for comparison.
5252
*/
5353
modulePath: string,
54+
packageRelativePath: string,
5455
exportsField: ExportsField,
5556
platform: string | null,
5657
): FileResolution {
@@ -61,13 +62,14 @@ export function resolvePackageTargetFromExports(
6162
});
6263
};
6364

64-
const subpath = getExportsSubpath(packagePath, modulePath);
65+
const subpath = getExportsSubpath(packageRelativePath);
6566
const exportMap = normalizeExportsField(exportsField, createConfigError);
6667

6768
if (!isSubpathDefinedInExports(exportMap, subpath)) {
6869
throw new PackagePathNotExportedError(
6970
`Attempted to import the module "${modulePath}" which is not listed ` +
70-
`in the "exports" of "${packagePath}".`,
71+
`in the "exports" of "${packagePath}" under the requested subpath ` +
72+
`"${subpath}".`,
7173
);
7274
}
7375

@@ -135,9 +137,7 @@ export function resolvePackageTargetFromExports(
135137
* Convert a module path to the package-relative subpath key to attempt for
136138
* "exports" field lookup.
137139
*/
138-
function getExportsSubpath(packagePath: string, modulePath: string): string {
139-
const packageSubpath = path.relative(packagePath, modulePath);
140-
140+
function getExportsSubpath(packageSubpath: string): string {
141141
return packageSubpath === '' ? '.' : './' + toPosixPath(packageSubpath);
142142
}
143143

packages/metro-resolver/src/PackageResolve.js

+15-6
Original file line numberDiff line numberDiff line change
@@ -97,9 +97,10 @@ export function redirectModulePath(
9797
modulePath: string,
9898
): string | false {
9999
const {getPackageForModule, mainFields, originModulePath} = context;
100+
const isModulePathAbsolute = path.isAbsolute(modulePath);
100101

101102
const containingPackage = getPackageForModule(
102-
path.isAbsolute(modulePath) ? modulePath : originModulePath,
103+
isModulePathAbsolute ? modulePath : originModulePath,
103104
);
104105

105106
if (containingPackage == null) {
@@ -109,11 +110,19 @@ export function redirectModulePath(
109110

110111
let redirectedPath;
111112

112-
if (modulePath.startsWith('.') || path.isAbsolute(modulePath)) {
113-
const packageRelativeModulePath = path.relative(
114-
containingPackage.rootPath,
115-
path.resolve(path.dirname(originModulePath), modulePath),
116-
);
113+
if (modulePath.startsWith('.') || isModulePathAbsolute) {
114+
const packageRelativeModulePath = isModulePathAbsolute
115+
? // If the module path is absolute, containingPackage is relative to it
116+
// (see above).
117+
containingPackage.packageRelativePath
118+
: // Otherwise containingPackage is relative to the origin module.
119+
// Origin's package-relative directory joined with the target module's
120+
// origin-relative path gives us the module's package-relative path.
121+
path.join(
122+
path.dirname(containingPackage.packageRelativePath),
123+
modulePath,
124+
);
125+
117126
redirectedPath = matchSubpathFromMainFields(
118127
// Use prefixed POSIX path for lookup in package.json
119128
'./' + toPosixPath(packageRelativeModulePath),

packages/metro-resolver/src/__tests__/package-exports-test.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -306,7 +306,7 @@ describe('with package exports resolution enabled', () => {
306306
});
307307
expect(logWarning).toHaveBeenCalledTimes(1);
308308
expect(logWarning.mock.calls[0][0]).toMatchInlineSnapshot(
309-
`"Attempted to import the module \\"/root/node_modules/test-pkg/foo\\" which is not listed in the \\"exports\\" of \\"/root/node_modules/test-pkg\\". Falling back to file-based resolution. Consider updating the call site or asking the package maintainer(s) to expose this API."`,
309+
`"Attempted to import the module \\"/root/node_modules/test-pkg/foo\\" which is not listed in the \\"exports\\" of \\"/root/node_modules/test-pkg\\" under the requested subpath \\"./foo\\". Falling back to file-based resolution. Consider updating the call site or asking the package maintainer(s) to expose this API."`,
310310
);
311311
});
312312

@@ -454,7 +454,7 @@ describe('with package exports resolution enabled', () => {
454454
);
455455
expect(logWarning).toHaveBeenCalledTimes(1);
456456
expect(logWarning.mock.calls[0][0]).toMatchInlineSnapshot(
457-
`"Attempted to import the module \\"/root/node_modules/test-pkg/private/bar\\" which is not listed in the \\"exports\\" of \\"/root/node_modules/test-pkg\\". Falling back to file-based resolution. Consider updating the call site or asking the package maintainer(s) to expose this API."`,
457+
`"Attempted to import the module \\"/root/node_modules/test-pkg/private/bar\\" which is not listed in the \\"exports\\" of \\"/root/node_modules/test-pkg\\" under the requested subpath \\"./private/bar\\". Falling back to file-based resolution. Consider updating the call site or asking the package maintainer(s) to expose this API."`,
458458
);
459459
});
460460

packages/metro-resolver/src/__tests__/utils.js

+1
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@ export function createPackageAccessors(
113113
return {
114114
rootPath: dir,
115115
packageJson,
116+
packageRelativePath: path.relative(dir, modulePath),
116117
};
117118
}
118119

packages/metro-resolver/src/resolve.js

+1
Original file line numberDiff line numberDiff line change
@@ -279,6 +279,7 @@ function resolvePackage(
279279
{...context, unstable_conditionNames: conditionNamesOverride},
280280
pkg.rootPath,
281281
absoluteCandidatePath,
282+
pkg.packageRelativePath,
282283
exportsField,
283284
platform,
284285
);

packages/metro-resolver/src/types.js

+11-4
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,13 @@ export type PackageInfo = $ReadOnly<{
8686
rootPath: string,
8787
}>;
8888

89+
export type PackageForModule = $ReadOnly<{
90+
...PackageInfo,
91+
/* A system-separated subpath (with no './' prefix) that reflects the subpath
92+
of the given candidate relative to the returned rootPath. */
93+
packageRelativePath: string,
94+
}>;
95+
8996
/**
9097
* Check existence of a single file.
9198
*/
@@ -121,13 +128,13 @@ export type ResolutionContext = $ReadOnly<{
121128
getPackage: (packageJsonPath: string) => ?PackageJson,
122129

123130
/**
124-
* Get the closest package scope and parsed `package.json` for a given
125-
* absolute candidate path (which need not exist), or null if there is no
126-
* package.json closer than the nearest node_modules directory.
131+
* Get the closest package scope, parsed `package.json` and relative subpath
132+
* for a given absolute candidate path (which need not exist), or null if
133+
* there is no package.json closer than the nearest node_modules directory.
127134
*
128135
* @deprecated See https://github.com/facebook/metro/commit/29c77bff31e2475a086bc3f04073f485da8f9ff0
129136
*/
130-
getPackageForModule: (absoluteModulePath: string) => ?PackageInfo,
137+
getPackageForModule: (absoluteModulePath: string) => ?PackageForModule,
131138

132139
/**
133140
* The dependency descriptor, within the origin module, corresponding to the

packages/metro-resolver/types/types.d.ts

+10-4
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,12 @@ export interface PackageInfo {
6464
readonly rootPath: string;
6565
}
6666

67+
export interface PackageForModule extends PackageInfo {
68+
/* A system-separated subpath (with no './' prefix) that reflects the subpath
69+
of the given candidate relative to the returned rootPath. */
70+
readonly packageRelativePath: string;
71+
}
72+
6773
/**
6874
* Check existence of a single file.
6975
*/
@@ -97,15 +103,15 @@ export interface ResolutionContext {
97103
readonly getPackage: (packageJsonPath: string) => PackageJson | null;
98104

99105
/**
100-
* Get the closest package scope and parsed `package.json` for a given
101-
* absolute candidate path (which need not exist), or null if there is no
102-
* package.json closer than the nearest node_modules directory.
106+
* Get the closest package scope, parsed `package.json` and relative subpath
107+
* for a given absolute candidate path (which need not exist), or null if
108+
* there is no package.json closer than the nearest node_modules directory.
103109
*
104110
* @deprecated See https://github.com/facebook/metro/commit/29c77bff31e2475a086bc3f04073f485da8f9ff0
105111
*/
106112
readonly getPackageForModule: (
107113
absoluteModulePath: string,
108-
) => PackageInfo | null;
114+
) => PackageForModule | null;
109115

110116
/**
111117
* The dependency descriptor, within the origin module, corresponding to the

packages/metro/src/node-haste/DependencyGraph.js

+9-2
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,9 @@ class DependencyGraph extends EventEmitter {
152152
return self;
153153
}
154154

155-
_getClosestPackage(absoluteModulePath: string): ?string {
155+
_getClosestPackage(
156+
absoluteModulePath: string,
157+
): ?{packageJsonPath: string, packageRelativePath: string} {
156158
const parsedPath = path.parse(absoluteModulePath);
157159
const root = parsedPath.root;
158160
let dir = path.join(parsedPath.dir, parsedPath.base);
@@ -165,7 +167,12 @@ class DependencyGraph extends EventEmitter {
165167
}
166168
const candidate = path.join(dir, 'package.json');
167169
if (this._fileSystem.exists(candidate)) {
168-
return candidate;
170+
return {
171+
packageJsonPath: candidate,
172+
// Note that by construction, dir is a prefix of absoluteModulePath,
173+
// so this relative path has no indirections.
174+
packageRelativePath: path.relative(dir, absoluteModulePath),
175+
};
169176
}
170177
dir = path.dirname(dir);
171178
} while (dir !== '.' && dir !== root);

packages/metro/src/node-haste/DependencyGraph/ModuleResolution.js

+13-9
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ import type {
2525
Resolution,
2626
ResolveAsset,
2727
} from 'metro-resolver';
28-
import type {PackageInfo, PackageJson} from 'metro-resolver/src/types';
28+
import type {PackageForModule, PackageJson} from 'metro-resolver/src/types';
2929

3030
const {codeFrameColumns} = require('@babel/code-frame');
3131
const fs = require('fs');
@@ -53,7 +53,10 @@ export type ModuleishCache<TPackage> = interface {
5353
platform?: string,
5454
supportsNativePlatform?: boolean,
5555
): TPackage,
56-
getPackageOf(absolutePath: string): ?TPackage,
56+
getPackageOf(absolutePath: string): ?{
57+
pkg: TPackage,
58+
packageRelativePath: string,
59+
},
5760
};
5861

5962
type Options<TPackage> = $ReadOnly<{
@@ -95,7 +98,7 @@ class ModuleResolver<TPackage: Packageish> {
9598
this._projectRootFakeModule = {
9699
path: path.join(projectRoot, '_'),
97100
getPackage: () =>
98-
moduleCache.getPackageOf(this._projectRootFakeModule.path),
101+
moduleCache.getPackageOf(this._projectRootFakeModule.path)?.pkg,
99102
isHaste() {
100103
throw new Error('not implemented');
101104
},
@@ -249,20 +252,21 @@ class ModuleResolver<TPackage: Packageish> {
249252
return null;
250253
};
251254

252-
_getPackageForModule = (absolutePath: string): ?PackageInfo => {
253-
let pkg;
255+
_getPackageForModule = (absolutePath: string): ?PackageForModule => {
256+
let result;
254257

255258
try {
256-
pkg = this._options.moduleCache.getPackageOf(absolutePath);
259+
result = this._options.moduleCache.getPackageOf(absolutePath);
257260
} catch (e) {
258261
// Do nothing. The standard module cache does not trigger any error, but
259262
// the ModuleGraph one does, if the module does not exist.
260263
}
261264

262-
return pkg != null
265+
return result != null
263266
? {
264-
rootPath: path.dirname(pkg.path),
265-
packageJson: pkg.read(),
267+
rootPath: path.dirname(result.pkg.path),
268+
packageJson: result.pkg.read(),
269+
packageRelativePath: result.packageRelativePath,
266270
}
267271
: null;
268272
};

packages/metro/src/node-haste/Module.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ class Module {
3232
}
3333

3434
getPackage(): ?Package {
35-
return this._moduleCache.getPackageForModule(this);
35+
return this._moduleCache.getPackageForModule(this)?.pkg;
3636
}
3737

3838
invalidate() {}

packages/metro/src/node-haste/ModuleCache.js

+42-18
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,10 @@
1414
const Module = require('./Module');
1515
const Package = require('./Package');
1616

17-
type GetClosestPackageFn = (absoluteFilePath: string) => ?string;
17+
type GetClosestPackageFn = (absoluteFilePath: string) => ?{
18+
packageJsonPath: string,
19+
packageRelativePath: string,
20+
};
1821

1922
class ModuleCache {
2023
_getClosestPackage: GetClosestPackageFn;
@@ -29,8 +32,11 @@ class ModuleCache {
2932
...
3033
};
3134
// Cache for "closest package.json" queries by module path.
32-
_packagePathByModulePath: {
33-
[filePath: string]: string,
35+
_packagePathAndSubpathByModulePath: {
36+
[filePath: string]: ?{
37+
packageJsonPath: string,
38+
packageRelativePath: string,
39+
},
3440
__proto__: null,
3541
...
3642
};
@@ -45,7 +51,7 @@ class ModuleCache {
4551
this._getClosestPackage = options.getClosestPackage;
4652
this._moduleCache = Object.create(null);
4753
this._packageCache = Object.create(null);
48-
this._packagePathByModulePath = Object.create(null);
54+
this._packagePathAndSubpathByModulePath = Object.create(null);
4955
this._modulePathsByPackagePath = Object.create(null);
5056
}
5157

@@ -65,29 +71,45 @@ class ModuleCache {
6571
return this._packageCache[filePath];
6672
}
6773

68-
getPackageForModule(module: Module): ?Package {
74+
getPackageForModule(
75+
module: Module,
76+
): ?{pkg: Package, packageRelativePath: string} {
6977
return this.getPackageOf(module.path);
7078
}
7179

72-
getPackageOf(absoluteModulePath: string): ?Package {
73-
let packagePath: ?string =
74-
this._packagePathByModulePath[absoluteModulePath];
75-
if (packagePath && this._packageCache[packagePath]) {
76-
return this._packageCache[packagePath];
80+
getPackageOf(
81+
absoluteModulePath: string,
82+
): ?{pkg: Package, packageRelativePath: string} {
83+
let packagePathAndSubpath =
84+
this._packagePathAndSubpathByModulePath[absoluteModulePath];
85+
if (
86+
packagePathAndSubpath &&
87+
this._packageCache[packagePathAndSubpath.packageJsonPath]
88+
) {
89+
return {
90+
pkg: this._packageCache[packagePathAndSubpath.packageJsonPath],
91+
packageRelativePath: packagePathAndSubpath.packageRelativePath,
92+
};
7793
}
7894

79-
packagePath = this._getClosestPackage(absoluteModulePath);
80-
if (!packagePath) {
95+
packagePathAndSubpath = this._getClosestPackage(absoluteModulePath);
96+
if (!packagePathAndSubpath) {
8197
return null;
8298
}
8399

84-
this._packagePathByModulePath[absoluteModulePath] = packagePath;
100+
const packagePath = packagePathAndSubpath.packageJsonPath;
101+
102+
this._packagePathAndSubpathByModulePath[absoluteModulePath] =
103+
packagePathAndSubpath;
85104
const modulePaths =
86105
this._modulePathsByPackagePath[packagePath] ?? new Set();
87106
modulePaths.add(absoluteModulePath);
88107
this._modulePathsByPackagePath[packagePath] = modulePaths;
89108

90-
return this.getPackage(packagePath);
109+
return {
110+
pkg: this.getPackage(packagePath),
111+
packageRelativePath: packagePathAndSubpath.packageRelativePath,
112+
};
91113
}
92114

93115
invalidate(filePath: string) {
@@ -99,10 +121,12 @@ class ModuleCache {
99121
this._packageCache[filePath].invalidate();
100122
delete this._packageCache[filePath];
101123
}
102-
if (this._packagePathByModulePath[filePath]) {
124+
const packagePathAndSubpath =
125+
this._packagePathAndSubpathByModulePath[filePath];
126+
if (packagePathAndSubpath) {
103127
// filePath is a module inside a package.
104-
const packagePath = this._packagePathByModulePath[filePath];
105-
delete this._packagePathByModulePath[filePath];
128+
const packagePath = packagePathAndSubpath.packageJsonPath;
129+
delete this._packagePathAndSubpathByModulePath[filePath];
106130
// This change doesn't invalidate any cached "closest package.json"
107131
// queries for the package's other modules. Clean up only this module.
108132
const modulePaths = this._modulePathsByPackagePath[packagePath];
@@ -118,7 +142,7 @@ class ModuleCache {
118142
// package.json" queries for modules inside this package.
119143
const modulePaths = this._modulePathsByPackagePath[filePath];
120144
for (const modulePath of modulePaths) {
121-
delete this._packagePathByModulePath[modulePath];
145+
delete this._packagePathAndSubpathByModulePath[modulePath];
122146
}
123147
modulePaths.clear();
124148
delete this._modulePathsByPackagePath[filePath];

0 commit comments

Comments
 (0)