metro-resolver: simplify API with exceptions

Reviewed By: mjesun

Differential Revision: D7098249

fbshipit-source-id: 5eea3dfdc54a98d295b756296398fd3a1dd3890b
This commit is contained in:
Jean Lauliac 2018-02-28 02:44:38 -08:00 committed by Facebook Github Bot
parent a66a231dfc
commit 50356b7408
7 changed files with 163 additions and 97 deletions

View File

@ -0,0 +1,38 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
* @format
*/
'use strict';
const path = require('path');
class FailedToResolveNameError extends Error {
dirPaths: $ReadOnlyArray<string>;
extraPaths: $ReadOnlyArray<string>;
constructor(
dirPaths: $ReadOnlyArray<string>,
extraPaths: $ReadOnlyArray<string>,
) {
const displayDirPaths = dirPaths.concat(extraPaths);
const hint = displayDirPaths.length ? ' or in these directories:' : '';
super(
`Module does not exist in the Haste module map${hint}\n` +
displayDirPaths
.map(dirPath => ` ${path.dirname(dirPath)}\n`)
.join(', ') +
'\n',
);
this.dirPaths = dirPaths;
this.extraPaths = extraPaths;
}
}
module.exports = FailedToResolveNameError;

View File

@ -0,0 +1,30 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
* @format
*/
'use strict';
const formatFileCandidates = require('./formatFileCandidates');
import type {FileAndDirCandidates} from './types';
class FailedToResolvePathError extends Error {
candidates: FileAndDirCandidates;
constructor(candidates: FileAndDirCandidates) {
super(
`The module could not be resolved because none of these files exist:\n\n` +
` * \`${formatFileCandidates(candidates.file)}\`\n` +
` * \`${formatFileCandidates(candidates.dir)}\``,
);
this.candidates = candidates;
}
}
module.exports = FailedToResolvePathError;

View File

@ -11,6 +11,7 @@
'use strict'; 'use strict';
const FailedToResolvePathError = require('../FailedToResolvePathError');
const Resolver = require('../index'); const Resolver = require('../index');
const path = require('path'); const path = require('path');
@ -69,47 +70,46 @@ const CONTEXT: ResolutionContext = (() => {
it('resolves relative path', () => { it('resolves relative path', () => {
expect(Resolver.resolve(CONTEXT, './bar', null)).toEqual({ expect(Resolver.resolve(CONTEXT, './bar', null)).toEqual({
type: 'resolved', type: 'sourceFile',
resolution: {type: 'sourceFile', filePath: '/root/project/bar.js'}, filePath: '/root/project/bar.js',
}); });
}); });
it('resolves relative path in another folder', () => { it('resolves relative path in another folder', () => {
expect(Resolver.resolve(CONTEXT, '../smth/beep', null)).toEqual({ expect(Resolver.resolve(CONTEXT, '../smth/beep', null)).toEqual({
type: 'resolved', type: 'sourceFile',
resolution: {type: 'sourceFile', filePath: '/root/smth/beep.js'}, filePath: '/root/smth/beep.js',
}); });
}); });
it('resolves a simple node_modules', () => { it('resolves a simple node_modules', () => {
expect(Resolver.resolve(CONTEXT, 'tadam', null)).toEqual({ expect(Resolver.resolve(CONTEXT, 'tadam', null)).toEqual({
type: 'resolved', type: 'sourceFile',
resolution: { filePath: '/root/node_modules/tadam/main.js',
type: 'sourceFile',
filePath: '/root/node_modules/tadam/main.js',
},
}); });
}); });
it('fails to resolve relative path', () => { it('fails to resolve relative path', () => {
expect(Resolver.resolve(CONTEXT, './tadam', null)).toEqual({ try {
type: 'failed', Resolver.resolve(CONTEXT, './tadam', null);
candidates: { throw new Error('should not reach');
type: 'modulePath', } catch (error) {
which: { if (!(error instanceof FailedToResolvePathError)) {
dir: { throw error;
candidateExts: ['', '.js'], }
filePathPrefix: '/root/project/tadam/index', expect(error.candidates).toEqual({
type: 'sourceFile', dir: {
}, candidateExts: ['', '.js'],
file: { filePathPrefix: '/root/project/tadam/index',
candidateExts: ['', '.js'], type: 'sourceFile',
filePathPrefix: '/root/project/tadam',
type: 'sourceFile',
},
}, },
}, file: {
}); candidateExts: ['', '.js'],
filePathPrefix: '/root/project/tadam',
type: 'sourceFile',
},
});
}
}); });
it('throws on invalid node package', () => { it('throws on invalid node package', () => {

View File

@ -18,7 +18,6 @@ export type {
} from './resolve'; } from './resolve';
export type { export type {
AssetFileResolution, AssetFileResolution,
Candidates,
FileAndDirCandidates, FileAndDirCandidates,
FileCandidates, FileCandidates,
FileResolution, FileResolution,
@ -27,9 +26,11 @@ export type {
} from './types'; } from './types';
const Resolver = { const Resolver = {
resolve: require('./resolve'), FailedToResolveNameError: require('./FailedToResolveNameError'),
InvalidPackageError: require('./InvalidPackageError'), FailedToResolvePathError: require('./FailedToResolvePathError'),
formatFileCandidates: require('./formatFileCandidates'), formatFileCandidates: require('./formatFileCandidates'),
InvalidPackageError: require('./InvalidPackageError'),
resolve: require('./resolve'),
}; };
module.exports = Resolver; module.exports = Resolver;

View File

@ -10,6 +10,8 @@
'use strict'; 'use strict';
const FailedToResolveNameError = require('./FailedToResolveNameError');
const FailedToResolvePathError = require('./FailedToResolvePathError');
const InvalidPackageError = require('./InvalidPackageError'); const InvalidPackageError = require('./InvalidPackageError');
const formatFileCandidates = require('./formatFileCandidates'); const formatFileCandidates = require('./formatFileCandidates');
@ -18,7 +20,6 @@ const path = require('path');
import type { import type {
AssetFileResolution, AssetFileResolution,
Candidates,
FileAndDirCandidates, FileAndDirCandidates,
FileCandidates, FileCandidates,
FileResolution, FileResolution,
@ -37,14 +38,14 @@ function resolve(
context: ResolutionContext, context: ResolutionContext,
moduleName: string, moduleName: string,
platform: string | null, platform: string | null,
): Result<Resolution, Candidates> { ): Resolution {
if (isRelativeImport(moduleName) || isAbsolutePath(moduleName)) { if (isRelativeImport(moduleName) || isAbsolutePath(moduleName)) {
return resolveModulePath(context, moduleName, platform); return resolveModulePath(context, moduleName, platform);
} }
const realModuleName = context.redirectModulePath(moduleName); const realModuleName = context.redirectModulePath(moduleName);
// exclude // exclude
if (realModuleName === false) { if (realModuleName === false) {
return resolvedAs({type: 'empty'}); return {type: 'empty'};
} }
const {originModulePath} = context; const {originModulePath} = context;
@ -66,7 +67,7 @@ function resolve(
const normalizedName = normalizePath(realModuleName); const normalizedName = normalizePath(realModuleName);
const result = resolveHasteName(context, normalizedName, platform); const result = resolveHasteName(context, normalizedName, platform);
if (result.type === 'resolved') { if (result.type === 'resolved') {
return result; return result.resolution;
} }
} }
@ -95,10 +96,10 @@ function resolve(
for (let i = 0; i < allDirPaths.length; ++i) { for (let i = 0; i < allDirPaths.length; ++i) {
const result = resolveFileOrDir(context, allDirPaths[i], platform); const result = resolveFileOrDir(context, allDirPaths[i], platform);
if (result.type === 'resolved') { if (result.type === 'resolved') {
return result; return result.resolution;
} }
} }
return failedFor({type: 'moduleName', dirPaths, extraPaths}); throw new FailedToResolveNameError(dirPaths, extraPaths);
} }
type ModulePathContext = FileOrDirContext & { type ModulePathContext = FileOrDirContext & {
@ -125,19 +126,19 @@ function resolveModulePath(
context: ModulePathContext, context: ModulePathContext,
toModuleName: string, toModuleName: string,
platform: string | null, platform: string | null,
): Result<Resolution, Candidates> { ): Resolution {
const modulePath = isAbsolutePath(toModuleName) const modulePath = isAbsolutePath(toModuleName)
? resolveWindowsPath(toModuleName) ? resolveWindowsPath(toModuleName)
: path.join(path.dirname(context.originModulePath), toModuleName); : path.join(path.dirname(context.originModulePath), toModuleName);
const redirectedPath = context.redirectModulePath(modulePath); const redirectedPath = context.redirectModulePath(modulePath);
if (redirectedPath === false) { if (redirectedPath === false) {
return resolvedAs({type: 'empty'}); return {type: 'empty'};
} }
const result = resolveFileOrDir(context, redirectedPath, platform); const result = resolveFileOrDir(context, redirectedPath, platform);
if (result.type === 'resolved') { if (result.type === 'resolved') {
return result; return result.resolution;
} }
return failedFor({type: 'modulePath', which: result.candidates}); throw new FailedToResolvePathError(result.candidates);
} }
type HasteContext = FileOrDirContext & { type HasteContext = FileOrDirContext & {

View File

@ -15,13 +15,6 @@ export type Result<+TResolution, +TCandidates> =
| {|+type: 'failed', +candidates: TCandidates|}; | {|+type: 'failed', +candidates: TCandidates|};
export type Resolution = FileResolution | {|+type: 'empty'|}; export type Resolution = FileResolution | {|+type: 'empty'|};
export type Candidates =
| {|+type: 'modulePath', +which: FileAndDirCandidates|}
| {|
+type: 'moduleName',
+dirPaths: $ReadOnlyArray<string>,
+extraPaths: $ReadOnlyArray<string>,
|};
export type AssetFileResolution = $ReadOnlyArray<string>; export type AssetFileResolution = $ReadOnlyArray<string>;
export type FileResolution = export type FileResolution =

View File

@ -96,60 +96,63 @@ class ModuleResolver<TModule: Moduleish, TPackage: Packageish> {
allowHaste: boolean, allowHaste: boolean,
platform: string | null, platform: string | null,
): TModule { ): TModule {
const result = Resolver.resolve( try {
{ const result = Resolver.resolve(
...this._options, {
originModulePath: fromModule.path, ...this._options,
redirectModulePath: modulePath => originModulePath: fromModule.path,
this._redirectRequire(fromModule, modulePath), redirectModulePath: modulePath =>
allowHaste, this._redirectRequire(fromModule, modulePath),
platform, allowHaste,
resolveHasteModule: name => platform,
this._options.moduleMap.getModule(name, platform, true), resolveHasteModule: name =>
resolveHastePackage: name => this._options.moduleMap.getModule(name, platform, true),
this._options.moduleMap.getPackage(name, platform, true), resolveHastePackage: name =>
getPackageMainPath: this._getPackageMainPath, this._options.moduleMap.getPackage(name, platform, true),
}, getPackageMainPath: this._getPackageMainPath,
moduleName, },
platform,
);
if (result.type === 'resolved') {
return this._getFileResolvedModule(result.resolution);
}
if (result.candidates.type === 'modulePath') {
const {which} = result.candidates;
throw new UnableToResolveError(
fromModule.path,
moduleName, moduleName,
`The module \`${moduleName}\` could not be found ` + platform,
`from \`${fromModule.path}\`. ` +
`Indeed, none of these files exist:\n\n` +
` * \`${Resolver.formatFileCandidates(which.file)}\`\n` +
` * \`${Resolver.formatFileCandidates(which.dir)}\``,
); );
return this._getFileResolvedModule(result);
} catch (error) {
if (error instanceof Resolver.FailedToResolvePathError) {
const {candidates} = error;
throw new UnableToResolveError(
fromModule.path,
moduleName,
`The module \`${moduleName}\` could not be found ` +
`from \`${fromModule.path}\`. ` +
`Indeed, none of these files exist:\n\n` +
` * \`${Resolver.formatFileCandidates(candidates.file)}\`\n` +
` * \`${Resolver.formatFileCandidates(candidates.dir)}\``,
);
}
if (error instanceof Resolver.FailedToResolveNameError) {
const {dirPaths, extraPaths} = error;
const displayDirPaths = dirPaths
.filter(dirPath => this._options.dirExists(dirPath))
.concat(extraPaths);
const hint = displayDirPaths.length ? ' or in these directories:' : '';
throw new UnableToResolveError(
fromModule.path,
moduleName,
`Module \`${moduleName}\` does not exist in the Haste module map${hint}\n` +
displayDirPaths
.map(dirPath => ` ${path.dirname(dirPath)}\n`)
.join(', ') +
'\n' +
`This might be related to https://github.com/facebook/react-native/issues/4968\n` +
`To resolve try the following:\n` +
` 1. Clear watchman watches: \`watchman watch-del-all\`.\n` +
` 2. Delete the \`node_modules\` folder: \`rm -rf node_modules && npm install\`.\n` +
' 3. Reset Metro Bundler cache: `rm -rf /tmp/metro-bundler-cache-*` or `npm start -- --reset-cache`.' +
' 4. Remove haste cache: `rm -rf /tmp/haste-map-react-native-packager-*`.',
);
}
throw error;
} }
const {dirPaths, extraPaths} = result.candidates;
const displayDirPaths = dirPaths
.filter(dirPath => this._options.dirExists(dirPath))
.concat(extraPaths);
const hint = displayDirPaths.length ? ' or in these directories:' : '';
throw new UnableToResolveError(
fromModule.path,
moduleName,
`Module does not exist in the module map${hint}\n` +
displayDirPaths
.map(dirPath => ` ${path.dirname(dirPath)}\n`)
.join(', ') +
'\n' +
`This might be related to https://github.com/facebook/react-native/issues/4968\n` +
`To resolve try the following:\n` +
` 1. Clear watchman watches: \`watchman watch-del-all\`.\n` +
` 2. Delete the \`node_modules\` folder: \`rm -rf node_modules && npm install\`.\n` +
' 3. Reset Metro Bundler cache: `rm -rf /tmp/metro-bundler-cache-*` or `npm start -- --reset-cache`.' +
' 4. Remove haste cache: `rm -rf /tmp/haste-map-react-native-packager-*`.',
);
} }
_getPackageMainPath = (packageJsonPath: string): string => { _getPackageMainPath = (packageJsonPath: string): string => {