From e4cba8e2e2b3754c5fbce5cbb64bf5427e89a1ab Mon Sep 17 00:00:00 2001 From: Jean Lauliac Date: Sun, 24 Dec 2017 03:06:04 -0800 Subject: [PATCH] metro: ModuleResolution: break down functions out of the class Reviewed By: davidaurelio Differential Revision: D6610568 fbshipit-source-id: b9f9c6803026442e557d6422fe0589fdeacc8311 --- .../src/ModuleGraph/node-haste/node-haste.js | 2 +- .../metro/src/node-haste/DependencyGraph.js | 4 +- .../DependencyGraph/ModuleResolution.js | 310 ++++++++++-------- 3 files changed, 178 insertions(+), 138 deletions(-) diff --git a/packages/metro/src/ModuleGraph/node-haste/node-haste.js b/packages/metro/src/ModuleGraph/node-haste/node-haste.js index 6cca131d..1eee0446 100644 --- a/packages/metro/src/ModuleGraph/node-haste/node-haste.js +++ b/packages/metro/src/ModuleGraph/node-haste/node-haste.js @@ -141,7 +141,7 @@ exports.createResolveFn = function(options: ResolveOptions): ResolveFn { dirExists: filePath => hasteFS.dirExists(filePath), doesFileExist: filePath => hasteFS.exists(filePath), extraNodeModules, - helpers, + isAssetFile: filePath => helpers.isAssetFile(filePath), moduleCache, moduleMap: new ModuleMap({ duplicates: Object.create(null), diff --git a/packages/metro/src/node-haste/DependencyGraph.js b/packages/metro/src/node-haste/DependencyGraph.js index 039f5156..92d1ec7f 100644 --- a/packages/metro/src/node-haste/DependencyGraph.js +++ b/packages/metro/src/node-haste/DependencyGraph.js @@ -180,7 +180,7 @@ class DependencyGraph extends EventEmitter { }, doesFileExist: this._doesFileExist, extraNodeModules: this._opts.extraNodeModules, - helpers: this._helpers, + isAssetFile: filePath => this._helpers.isAssetFile(filePath), moduleCache: this._moduleCache, moduleMap: this._moduleMap, preferNativePlatform: this._opts.preferNativePlatform, @@ -232,7 +232,7 @@ class DependencyGraph extends EventEmitter { } getModuleForPath(entryFile: string) { - if (this._moduleResolver.isAssetFile(entryFile)) { + if (this._helpers.isAssetFile(entryFile)) { return this._moduleCache.getAssetModule(entryFile); } diff --git a/packages/metro/src/node-haste/DependencyGraph/ModuleResolution.js b/packages/metro/src/node-haste/DependencyGraph/ModuleResolution.js index e0bb96a7..b4f4de47 100644 --- a/packages/metro/src/node-haste/DependencyGraph/ModuleResolution.js +++ b/packages/metro/src/node-haste/DependencyGraph/ModuleResolution.js @@ -58,19 +58,34 @@ export type ModuleishCache = { getAssetModule(path: string): TModule, }; +/** + * Given a directory path and the base asset name, return a list of all the + * asset file names that match the given base name in that directory. Return + * null if there's no such named asset. `platform` is used to identify + * platform-specific assets, ex. `foo.ios.js` instead of a generic `foo.js`. + */ +type ResolveAsset = ( + dirPath: string, + assetName: string, + platform: string | null, +) => ?$ReadOnlyArray; + +/** + * Check existence of a single file. + */ +type DoesFileExist = (filePath: string) => boolean; + +type IsAssetFile = (fileName: string) => boolean; + type Options = {| +dirExists: DirExistsFn, - +doesFileExist: (filePath: string) => boolean, + +doesFileExist: DoesFileExist, +extraNodeModules: ?Object, - +helpers: DependencyGraphHelpers, + +isAssetFile: IsAssetFile, +moduleCache: ModuleishCache, +preferNativePlatform: boolean, +moduleMap: ModuleMap, - +resolveAsset: ( - dirPath: string, - assetName: string, - platform: string | null, - ) => ?$ReadOnlyArray, + +resolveAsset: ResolveAsset, +sourceExts: Array, |}; @@ -376,7 +391,8 @@ class ModuleResolver { ): Result { const dirPath = path.dirname(potentialModulePath); const fileNameHint = path.basename(potentialModulePath); - const fileResult = this._loadAsFile(dirPath, fileNameHint, platform); + const {_options} = this; + const fileResult = resolveFile(_options, dirPath, fileNameHint, platform); if (fileResult.type === 'resolved') { return fileResult; } @@ -387,131 +403,6 @@ class ModuleResolver { return failedFor({file: fileResult.candidates, dir: dirResult.candidates}); } - isAssetFile(filename: string): boolean { - return this._options.helpers.isAssetFile(filename); - } - - _loadAsFile( - dirPath: string, - fileNameHint: string, - platform: string | null, - ): Result { - if (this.isAssetFile(fileNameHint)) { - const result = this._loadAsAssetFile(dirPath, fileNameHint, platform); - return mapResult(result, filePaths => ({type: 'assetFiles', filePaths})); - } - const candidateExts = []; - const filePathPrefix = path.join(dirPath, fileNameHint); - const context = {filePathPrefix, candidateExts}; - const filePath = this._tryToResolveSourceFile(context, platform); - if (filePath != null) { - return resolvedAs({type: 'sourceFile', filePath}); - } - return failedFor({type: 'sourceFile', candidateExts}); - } - - _loadAsAssetFile( - dirPath: string, - fileNameHint: string, - platform: string | null, - ): Result { - const {resolveAsset} = this._options; - const assetNames = resolveAsset(dirPath, fileNameHint, platform); - if (assetNames != null) { - return resolvedAs( - assetNames.map(assetName => { - return path.join(dirPath, assetName); - }), - ); - } - return failedFor({type: 'asset', name: fileNameHint}); - } - - /** - * A particular 'base path' can resolve to a number of possibilities depending - * on the context. For example `foo/bar` could resolve to `foo/bar.ios.js`, or - * to `foo/bar.js`. If can also resolve to the bare path `foo/bar` itself, as - * supported by Node.js resolution. On the other hand it doesn't support - * `foo/bar.ios`, for historical reasons. - * - * Return the full path of the resolved module, `null` if no resolution could - * be found. - */ - _tryToResolveSourceFile( - context: {| - +candidateExts: Array, - +filePathPrefix: string, - |}, - platform: ?string, - ): ?string { - let filePath = this._tryToResolveFileForExt(context, ''); - if (filePath) { - return filePath; - } - const {sourceExts} = this._options; - for (let i = 0; i < sourceExts.length; i++) { - const ext = `.${sourceExts[i]}`; - filePath = this._tryToResolveFileForExts(context, ext, platform); - if (filePath != null) { - return filePath; - } - } - return null; - } - - /** - * For a particular extension, ex. `js`, we want to try a few possibilities, - * such as `foo.ios.js`, `foo.native.js`, and of course `foo.js`. - * - * Return the full path of the resolved module, `null` if no resolution could - * be found. - */ - _tryToResolveFileForExts( - context: {| - +candidateExts: Array, - +filePathPrefix: string, - |}, - sourceExt: string, - platform: ?string, - ): ?string { - const {preferNativePlatform} = this._options; - if (platform != null) { - const platExt = `.${platform}${sourceExt}`; - const filePath = this._tryToResolveFileForExt(context, platExt); - if (filePath) { - return filePath; - } - } - if (preferNativePlatform) { - const nativeExt = `.native${sourceExt}`; - const filePath = this._tryToResolveFileForExt(context, nativeExt); - if (filePath) { - return filePath; - } - } - const filePath = this._tryToResolveFileForExt(context, sourceExt); - return filePath; - } - - /** - * We try to resolve a single possible extension. If it doesn't exist, then - * we make sure to add the extension to a list of candidates for reporting. - */ - _tryToResolveFileForExt( - context: {| - +candidateExts: Array, - +filePathPrefix: string, - |}, - extension: string, - ): ?string { - const filePath = `${context.filePathPrefix}${extension}`; - if (this._options.doesFileExist(filePath)) { - return filePath; - } - context.candidateExts.push(filePath); - return null; - } - _getEmptyModule(fromModule: TModule, toModuleName: string): TModule { const {moduleCache} = this._options; const module = moduleCache.getModule(ModuleResolver.EMPTY_MODULE); @@ -543,7 +434,8 @@ class ModuleResolver { if (this._options.doesFileExist(packageJsonPath)) { return this._loadAsPackage(packageJsonPath, platform); } - const result = this._loadAsFile(potentialDirPath, 'index', platform); + const opts = this._options; + const result = resolveFile(opts, potentialDirPath, 'index', platform); if (result.type === 'resolved') { return result; } @@ -565,7 +457,8 @@ class ModuleResolver { const mainPrefixPath = package_.getMain(); const dirPath = path.dirname(mainPrefixPath); const prefixName = path.basename(mainPrefixPath); - const fileResult = this._loadAsFile(dirPath, prefixName, platform); + const opts = this._options; + const fileResult = resolveFile(opts, dirPath, prefixName, platform); if (fileResult.type === 'resolved') { return fileResult; } @@ -581,6 +474,153 @@ class ModuleResolver { } } +type FileContext = { + +doesFileExist: DoesFileExist, + +isAssetFile: IsAssetFile, + +preferNativePlatform: boolean, + +resolveAsset: ResolveAsset, + +sourceExts: $ReadOnlyArray, +}; + +/** + * Given a file name for a particular directory, return a resolution result + * depending on whether or not we found the corresponding module as a file. For + * example, we might ask for `foo.png`, that resolves to + * `['/js/beep/foo.ios.png']`. Or we may ask for `boop`, that resolves to + * `/js/boop.android.ts`. On the other hand this function does not resolve + * directory-based module names: for example `boop` will not resolve to + * `/js/boop/index.js` (see `_loadAsDir` for that). + */ +function resolveFile( + context: FileContext, + dirPath: string, + fileNameHint: string, + platform: string | null, +): Result { + const {isAssetFile, resolveAsset} = context; + if (isAssetFile(fileNameHint)) { + const result = resolveAssetFiles( + resolveAsset, + dirPath, + fileNameHint, + platform, + ); + return mapResult(result, filePaths => ({type: 'assetFiles', filePaths})); + } + const candidateExts = []; + const filePathPrefix = path.join(dirPath, fileNameHint); + const sfContext = {...context, candidateExts, filePathPrefix}; + const filePath = resolveSourceFile(sfContext, platform); + if (filePath != null) { + return resolvedAs({type: 'sourceFile', filePath}); + } + return failedFor({type: 'sourceFile', candidateExts}); +} + +type SourceFileContext = SourceFileForAllExtsContext & { + +sourceExts: $ReadOnlyArray, +}; + +/** + * A particular 'base path' can resolve to a number of possibilities depending + * on the context. For example `foo/bar` could resolve to `foo/bar.ios.js`, or + * to `foo/bar.js`. If can also resolve to the bare path `foo/bar` itself, as + * supported by Node.js resolution. On the other hand it doesn't support + * `foo/bar.ios`, for historical reasons. + * + * Return the full path of the resolved module, `null` if no resolution could + * be found. + */ +function resolveSourceFile( + context: SourceFileContext, + platform: ?string, +): ?string { + let filePath = resolveSourceFileForAllExts(context, ''); + if (filePath) { + return filePath; + } + const {sourceExts} = context; + for (let i = 0; i < sourceExts.length; i++) { + const ext = `.${sourceExts[i]}`; + filePath = resolveSourceFileForAllExts(context, ext, platform); + if (filePath != null) { + return filePath; + } + } + return null; +} + +type SourceFileForAllExtsContext = SourceFileForExtContext & { + +preferNativePlatform: boolean, +}; + +/** + * For a particular extension, ex. `js`, we want to try a few possibilities, + * such as `foo.ios.js`, `foo.native.js`, and of course `foo.js`. Return the + * full path of the resolved module, `null` if no resolution could be found. + */ +function resolveSourceFileForAllExts( + context: SourceFileForAllExtsContext, + sourceExt: string, + platform: ?string, +): ?string { + if (platform != null) { + const ext = `.${platform}${sourceExt}`; + const filePath = resolveSourceFileForExt(context, ext); + if (filePath) { + return filePath; + } + } + if (context.preferNativePlatform) { + const filePath = resolveSourceFileForExt(context, `.native${sourceExt}`); + if (filePath) { + return filePath; + } + } + const filePath = resolveSourceFileForExt(context, sourceExt); + return filePath; +} + +type SourceFileForExtContext = { + +candidateExts: Array, + +doesFileExist: DoesFileExist, + +filePathPrefix: string, +}; + +/** + * We try to resolve a single possible extension. If it doesn't exist, then + * we make sure to add the extension to a list of candidates for reporting. + */ +function resolveSourceFileForExt( + context: SourceFileForExtContext, + extension: string, +): ?string { + const filePath = `${context.filePathPrefix}${extension}`; + if (context.doesFileExist(filePath)) { + return filePath; + } + context.candidateExts.push(filePath); + return null; +} + +/** + * Find all the asset files corresponding to the file base name, and return + * it wrapped as a resolution result. + */ +function resolveAssetFiles( + resolveAsset: ResolveAsset, + dirPath: string, + fileNameHint: string, + platform: string | null, +): Result { + const assetNames = resolveAsset(dirPath, fileNameHint, platform); + if (assetNames != null) { + const res = assetNames.map(assetName => path.join(dirPath, assetName)); + return resolvedAs(res); + } + return failedFor({type: 'asset', name: fileNameHint}); +} + // HasteFS stores paths with backslashes on Windows, this ensures the path is in // the proper format. Will also add drive letter if not present so `/root` will // resolve to `C:\root`. Noop on other platforms.