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

12 files changed

Lines changed: 112 additions & 53 deletions

File tree

docs/Resolution.md

Lines changed: 2 additions & 2 deletions
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

Lines changed: 5 additions & 5 deletions
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

Lines changed: 15 additions & 6 deletions
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

Lines changed: 2 additions & 2 deletions
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

Lines changed: 1 addition & 0 deletions
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

Lines changed: 1 addition & 0 deletions
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

Lines changed: 11 additions & 4 deletions
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

Lines changed: 10 additions & 4 deletions
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

Lines changed: 9 additions & 2 deletions
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

Lines changed: 13 additions & 9 deletions
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
};

0 commit comments

Comments
 (0)