Skip to content

Commit 3b3e0aa

Browse files
robhoganfacebook-github-bot
authored andcommitted
Make ResolutionContext.fileSystemLookup stable and required
Summary: Promote `ResolutionContext.fileSystemLookup` to stable and (for providers of `ResolutionContext`) make it mandatory. `fileSystemLookup` has two main advantages over `doesFileExist`: - It allows nonempty directory existence checks, which is important for resolver performance. - It exposes `realPath`. Metro has no other API for using the in-memory file system to resolve a path to a real path. The latter is critical for correct implementation of custom resolvers (non-real paths can mean duplication in the module graph, and broken Fast Refresh, whereas use of `fs` obscures traversed symlinks, which interferes with incremental resolution). From now on, we will require custom resolvers to return absolute, *real*, paths, and suggest that they use this method to do it. `ResolutionContext.doesFileExist` is consequently deprecated as redundant and potentially misleading for implementers.. Changelog: ``` - **[Breaking]**: [Custom resolvers](https://metrobundler.dev/docs/configuration#resolverequest) must now return absolute, real paths for any successful resolution. - **[Feature]**: Expose [`ResolutionContext.fileSystemLookup`](https://metrobundler.dev/docs/resolution#resolution-context) for performing file and directory existence checks and resolving real paths. ``` Reviewed By: huntie Differential Revision: D62374241 fbshipit-source-id: 7760bd4278736e6db27f994a7598784b6578ab9c
1 parent fd7c3b1 commit 3b3e0aa

File tree

9 files changed

+78
-62
lines changed

9 files changed

+78
-62
lines changed

docs/Resolution.md

+12-2
Original file line numberDiff line numberDiff line change
@@ -168,11 +168,19 @@ The set of file extensions used to identify asset files. Defaults to [`resolver.
168168

169169
`true` if the resolution is for a development bundle, or `false` otherwise.
170170

171-
#### `doesFileExist: string => boolean`
171+
#### `doesFileExist: string => boolean` <div class="label deprecated">Deprecated</div>
172172

173173
Returns `true` if the file with the given path exists, or `false` otherwise.
174174

175-
By default, Metro implements this by consulting an in-memory map of the filesystem that has been prepared in advance. This approach avoids disk I/O during module resolution.
175+
The default implementation wraps [`fileSystemLookup`](#filesystemlookup-string--exists-true-type-fd-realpath-string--exists-false) - prefer using that directly.
176+
177+
#### `fileSystemLookup: string => {exists: true, type: 'f'|'d', realPath: string} | {exists: false}`
178+
179+
*Added in v0.81.0*
180+
181+
Return information about the given absolute or project-relative path, following symlinks. A file "exists" if and only if it is watched, and a directory must be non-empty. The returned `realPath` is real and absolute.
182+
183+
By default, Metro implements this by consulting an in-memory map of the filesystem that has been prepared in advance. This approach avoids disk I/O during module resolution and performs realpath resolution at negligible additional cost.
176184

177185
#### `nodeModulesPaths: $ReadOnlyArray<string>`
178186

@@ -267,6 +275,8 @@ type Resolution =
267275
| {type: 'assetFiles', filePaths: $ReadOnlyArray<string>};
268276
```
269277

278+
Returned paths (`filePath` and `filePaths`) must be *absolute* and *real*, such as the `realPath` returned by [`fileSystemLookup`](#filesystemlookup-string--exists-true-type-fd-realpath-string--exists-false).
279+
270280
When calling the default resolver with a non-null `resolveRequest` function, it represents a custom resolver and will always be called, fully replacing the default resolution logic.
271281

272282
Inside a custom resolver, `resolveRequest` is set to the default resolver function, for easy chaining and customization.

packages/metro-resolver/src/PackageExportsResolve.js

+3-10
Original file line numberDiff line numberDiff line change
@@ -110,18 +110,11 @@ export function resolvePackageTargetFromExports(
110110
}
111111
}
112112

113-
if (context.unstable_fileSystemLookup != null) {
114-
const lookupResult = context.unstable_fileSystemLookup(filePath);
115-
if (lookupResult.exists && lookupResult.type === 'f') {
116-
return {
117-
type: 'sourceFile',
118-
filePath: lookupResult.realPath,
119-
};
120-
}
121-
} else if (context.doesFileExist(filePath)) {
113+
const lookupResult = context.fileSystemLookup(filePath);
114+
if (lookupResult.exists && lookupResult.type === 'f') {
122115
return {
123116
type: 'sourceFile',
124-
filePath,
117+
filePath: lookupResult.realPath,
125118
};
126119
}
127120

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

-1
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,6 @@ describe('with package exports resolution disabled', () => {
3434
}),
3535
originModulePath: '/root/src/main.js',
3636
unstable_enablePackageExports: false,
37-
unstable_fileSystemLookup: null,
3837
};
3938

4039
expect(Resolver.resolve(context, 'test-pkg', null)).toEqual({

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

+14-14
Original file line numberDiff line numberDiff line change
@@ -54,20 +54,7 @@ export function createResolutionContext(
5454
(typeof fileMap[filePath] === 'string' ||
5555
typeof fileMap[filePath].realPath === 'string'),
5656
extraNodeModules: null,
57-
mainFields: ['browser', 'main'],
58-
nodeModulesPaths: [],
59-
preferNativePlatform: false,
60-
redirectModulePath: (filePath: string) => filePath,
61-
resolveAsset: (filePath: string) => null,
62-
resolveHasteModule: (name: string) => null,
63-
resolveHastePackage: (name: string) => null,
64-
sourceExts: ['js', 'jsx', 'json', 'ts', 'tsx'],
65-
unstable_conditionNames: ['require'],
66-
unstable_conditionsByPlatform: {
67-
web: ['browser'],
68-
},
69-
unstable_enablePackageExports: false,
70-
unstable_fileSystemLookup: inputPath => {
57+
fileSystemLookup: inputPath => {
7158
// Normalise and remove any trailing slash.
7259
const filePath = path.resolve(inputPath);
7360
const candidate = fileMap[filePath];
@@ -89,6 +76,19 @@ export function createResolutionContext(
8976
realPath: candidate.realPath,
9077
};
9178
},
79+
mainFields: ['browser', 'main'],
80+
nodeModulesPaths: [],
81+
preferNativePlatform: false,
82+
redirectModulePath: (filePath: string) => filePath,
83+
resolveAsset: (filePath: string) => null,
84+
resolveHasteModule: (name: string) => null,
85+
resolveHastePackage: (name: string) => null,
86+
sourceExts: ['js', 'jsx', 'json', 'ts', 'tsx'],
87+
unstable_conditionNames: ['require'],
88+
unstable_conditionsByPlatform: {
89+
web: ['browser'],
90+
},
91+
unstable_enablePackageExports: false,
9292
unstable_logWarning: () => {},
9393
...createPackageAccessors(fileMap),
9494
};

packages/metro-resolver/src/resolve.js

+19-29
Original file line numberDiff line numberDiff line change
@@ -135,22 +135,20 @@ function resolve(
135135
const allDirPaths = nodeModulesPaths
136136
.map(nodeModulePath => {
137137
let lookupResult = null;
138-
if (context.unstable_fileSystemLookup) {
139-
// Insight: The module can only exist if there is a `node_modules` at
140-
// this path. Redirections cannot succeed, because we will never look
141-
// beyond a node_modules path segment for finding the closest
142-
// package.json. Moreover, if the specifier contains a '/' separator,
143-
// the first part *must* be a real directory, because it is the
144-
// shallowest path that can possibly contain a redirecting package.json.
145-
const mustBeDirectory =
146-
parsedSpecifier.posixSubpath !== '.' ||
147-
parsedSpecifier.packageName.length > parsedSpecifier.firstPart.length
148-
? nodeModulePath + path.sep + parsedSpecifier.firstPart
149-
: nodeModulePath;
150-
lookupResult = context.unstable_fileSystemLookup(mustBeDirectory);
151-
if (!lookupResult.exists || lookupResult.type !== 'd') {
152-
return null;
153-
}
138+
// Insight: The module can only exist if there is a `node_modules` at
139+
// this path. Redirections cannot succeed, because we will never look
140+
// beyond a node_modules path segment for finding the closest
141+
// package.json. Moreover, if the specifier contains a '/' separator,
142+
// the first part *must* be a real directory, because it is the
143+
// shallowest path that can possibly contain a redirecting package.json.
144+
const mustBeDirectory =
145+
parsedSpecifier.posixSubpath !== '.' ||
146+
parsedSpecifier.packageName.length > parsedSpecifier.firstPart.length
147+
? nodeModulePath + path.sep + parsedSpecifier.firstPart
148+
: nodeModulePath;
149+
lookupResult = context.fileSystemLookup(mustBeDirectory);
150+
if (!lookupResult.exists || lookupResult.type !== 'd') {
151+
return null;
154152
}
155153
return path.join(nodeModulePath, realModuleName);
156154
})
@@ -398,12 +396,8 @@ function resolvePackageEntryPoint(
398396
packagePath: string,
399397
platform: string | null,
400398
): Result<Resolution, FileCandidates> {
401-
const dirLookup = context.unstable_fileSystemLookup?.(packagePath);
402-
403-
if (
404-
dirLookup != null &&
405-
(dirLookup.exists === false || dirLookup.type !== 'd')
406-
) {
399+
const dirLookup = context.fileSystemLookup(packagePath);
400+
if (dirLookup.exists == false || dirLookup.type !== 'd') {
407401
return failedFor({
408402
type: 'sourceFile',
409403
filePathPrefix: packagePath,
@@ -575,13 +569,9 @@ function resolveSourceFileForExt(
575569
if (redirectedPath === false) {
576570
return {type: 'empty'};
577571
}
578-
if (context.unstable_fileSystemLookup) {
579-
const lookupResult = context.unstable_fileSystemLookup(redirectedPath);
580-
if (lookupResult.exists && lookupResult.type === 'f') {
581-
return lookupResult.realPath;
582-
}
583-
} else if (context.doesFileExist(redirectedPath)) {
584-
return redirectedPath;
572+
const lookupResult = context.fileSystemLookup(redirectedPath);
573+
if (lookupResult.exists && lookupResult.type === 'f') {
574+
return lookupResult.realPath;
585575
}
586576
context.candidateExts.push(extension);
587577
return null;

packages/metro-resolver/src/types.js

+13-1
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,12 @@ export type ResolutionContext = $ReadOnly<{
124124
assetExts: $ReadOnlySet<string>,
125125
customResolverOptions: CustomResolverOptions,
126126
disableHierarchicalLookup: boolean,
127+
128+
/**
129+
* Determine whether a regular file exists at the given path.
130+
*
131+
* @deprecated, prefer `fileSystemLookup`
132+
*/
127133
doesFileExist: DoesFileExist,
128134
extraNodeModules: ?{[string]: string, ...},
129135

@@ -151,6 +157,13 @@ export type ResolutionContext = $ReadOnly<{
151157
*/
152158
dependency?: TransformResultDependency,
153159

160+
/**
161+
* Synchonously returns information about a given absolute path, including
162+
* whether it exists, whether it is a file or directory, and its absolute
163+
* real path.
164+
*/
165+
fileSystemLookup: FileSystemLookup,
166+
154167
/**
155168
* The ordered list of fields to read in `package.json` to resolve a main
156169
* entry point based on the "browser" field spec.
@@ -189,7 +202,6 @@ export type ResolutionContext = $ReadOnly<{
189202
[platform: string]: $ReadOnlyArray<string>,
190203
}>,
191204
unstable_enablePackageExports: boolean,
192-
unstable_fileSystemLookup?: ?FileSystemLookup,
193205
unstable_logWarning: (message: string) => void,
194206
}>;
195207

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

+13-1
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,12 @@ export interface ResolutionContext {
101101
readonly allowHaste: boolean;
102102
readonly customResolverOptions: CustomResolverOptions;
103103
readonly disableHierarchicalLookup: boolean;
104+
105+
/**
106+
* Determine whether a regular file exists at the given path.
107+
*
108+
* @deprecated, prefer `fileSystemLookup`
109+
*/
104110
readonly doesFileExist: DoesFileExist;
105111
readonly extraNodeModules?: {[key: string]: string};
106112

@@ -127,6 +133,13 @@ export interface ResolutionContext {
127133
*/
128134
readonly dependency?: TransformResultDependency;
129135

136+
/**
137+
* Synchonously returns information about a given absolute path, including
138+
* whether it exists, whether it is a file or directory, and its absolute
139+
* real path.
140+
*/
141+
readonly fileSystemLookup: FileSystemLookup;
142+
130143
/**
131144
* The ordered list of fields to read in `package.json` to resolve a main
132145
* entry point based on the "browser" field spec.
@@ -165,7 +178,6 @@ export interface ResolutionContext {
165178
[platform: string]: ReadonlyArray<string>;
166179
}>;
167180
unstable_enablePackageExports: boolean;
168-
unstable_fileSystemLookup?: FileSystemLookup | null;
169181
unstable_logWarning: (message: string) => void;
170182
}
171183

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,7 @@ class DependencyGraph extends EventEmitter {
189189
doesFileExist: this._doesFileExist,
190190
emptyModulePath: this._config.resolver.emptyModulePath,
191191
extraNodeModules: this._config.resolver.extraNodeModules,
192+
fileSystemLookup,
192193
getHasteModulePath: (name, platform) =>
193194
this._hasteMap.getModule(name, platform, true),
194195
getHastePackagePath: (name, platform) =>
@@ -219,7 +220,6 @@ class DependencyGraph extends EventEmitter {
219220
this._config.resolver.unstable_conditionsByPlatform,
220221
unstable_enablePackageExports:
221222
this._config.resolver.unstable_enablePackageExports,
222-
unstable_fileSystemLookup: fileSystemLookup,
223223
});
224224
}
225225

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

+3-3
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ type Options<TPackage> = $ReadOnly<{
6666
doesFileExist: DoesFileExist,
6767
emptyModulePath: string,
6868
extraNodeModules: ?Object,
69+
fileSystemLookup: FileSystemLookup,
6970
getHasteModulePath: (name: string, platform: ?string) => ?string,
7071
getHastePackagePath: (name: string, platform: ?string) => ?string,
7172
mainFields: $ReadOnlyArray<string>,
@@ -82,7 +83,6 @@ type Options<TPackage> = $ReadOnly<{
8283
[platform: string]: $ReadOnlyArray<string>,
8384
}>,
8485
unstable_enablePackageExports: boolean,
85-
unstable_fileSystemLookup: ?FileSystemLookup,
8686
}>;
8787

8888
class ModuleResolver<TPackage: Packageish> {
@@ -142,6 +142,7 @@ class ModuleResolver<TPackage: Packageish> {
142142
disableHierarchicalLookup,
143143
doesFileExist,
144144
extraNodeModules,
145+
fileSystemLookup,
145146
mainFields,
146147
nodeModulesPaths,
147148
preferNativePlatform,
@@ -151,7 +152,6 @@ class ModuleResolver<TPackage: Packageish> {
151152
unstable_conditionNames,
152153
unstable_conditionsByPlatform,
153154
unstable_enablePackageExports,
154-
unstable_fileSystemLookup,
155155
} = this._options;
156156

157157
try {
@@ -164,6 +164,7 @@ class ModuleResolver<TPackage: Packageish> {
164164
disableHierarchicalLookup,
165165
doesFileExist,
166166
extraNodeModules,
167+
fileSystemLookup,
167168
mainFields,
168169
nodeModulesPaths,
169170
preferNativePlatform,
@@ -173,7 +174,6 @@ class ModuleResolver<TPackage: Packageish> {
173174
unstable_conditionNames,
174175
unstable_conditionsByPlatform,
175176
unstable_enablePackageExports,
176-
unstable_fileSystemLookup,
177177
unstable_logWarning: this._logWarning,
178178
customResolverOptions: resolverOptions.customResolverOptions ?? {},
179179
originModulePath: fromModule.path,

0 commit comments

Comments
 (0)