ResolutionRequest: extract module resolution

Reviewed By: davidaurelio

Differential Revision: D5218015

fbshipit-source-id: 6e34df5913d96a0b518f9403309658ea0b559730
This commit is contained in:
Jean Lauliac 2017-06-13 07:15:49 -07:00 committed by Facebook Github Bot
parent b559412a08
commit d3195fa528
6 changed files with 735 additions and 588 deletions

View File

@ -32,6 +32,8 @@ const ResolutionRequest = require('../../node-haste/DependencyGraph/ResolutionRe
const defaults = require('../../defaults');
const {ModuleResolver} = require('../../node-haste/DependencyGraph/ModuleResolution');
import type {Moduleish, Packageish} from '../../node-haste/DependencyGraph/ResolutionRequest';
type ResolveOptions = {|
@ -116,22 +118,28 @@ exports.createResolveFn = function(options: ResolveOptions): ResolveFn {
getDirFiles: dirPath => filesByDirNameIndex.getAllFiles(dirPath),
platforms,
});
const moduleResolver = new ModuleResolver({
dirExists: filePath => hasteFS.dirExists(filePath),
doesFileExist: filePath => hasteFS.exists(filePath),
extraNodeModules,
helpers,
moduleCache,
moduleMap: getFakeModuleMap(hasteMap),
preferNativePlatform: true,
resolveAsset: (dirPath, assetName, platform) =>
assetResolutionCache.resolve(dirPath, assetName, platform),
sourceExts,
});
return (id, source, platform, _, callback) => {
let resolutionRequest = resolutionRequests[platform];
if (!resolutionRequest) {
resolutionRequest = resolutionRequests[platform] = new ResolutionRequest({
dirExists: filePath => hasteFS.dirExists(filePath),
doesFileExist: filePath => hasteFS.exists(filePath),
moduleResolver,
entryPath: '',
extraNodeModules,
helpers,
moduleCache,
moduleMap: getFakeModuleMap(hasteMap),
platform,
preferNativePlatform: true,
resolveAsset: (dirPath, assetName) =>
assetResolutionCache.resolve(dirPath, assetName, platform),
sourceExts,
moduleCache,
});
}

View File

@ -75,7 +75,7 @@ class AssetResolutionCache {
resolve(
dirPath: string,
assetName: string,
platform: ?string,
platform: string | null,
): $ReadOnlyArray<string> {
const results = this._assetsByDirPath.get(dirPath);
const assets = results.get(assetName);

View File

@ -33,14 +33,16 @@ const {
createActionStartEntry,
log,
} = require('../Logger');
const {ModuleResolver} = require('./DependencyGraph/ModuleResolution');
const {EventEmitter} = require('events');
import type {Options as JSTransformerOptions} from '../JSTransformer/worker';
import type {GlobalTransformCache} from '../lib/GlobalTransformCache';
import type {GetTransformCacheKey} from '../lib/TransformCaching';
import type {Reporter} from '../lib/reporting';
import type {ModuleMap} from './DependencyGraph/ResolutionRequest';
import type {ModuleMap} from './DependencyGraph/ModuleResolution';
import type {Options as ModuleOptions, TransformCode} from './Module';
import type Package from './Package';
import type {HasteFS} from './types';
type Options = {|
@ -75,6 +77,7 @@ class DependencyGraph extends EventEmitter {
_helpers: DependencyGraphHelpers;
_moduleCache: ModuleCache;
_moduleMap: ModuleMap;
_moduleResolver: ModuleResolver<Module, Package>;
_opts: Options;
constructor(config: {|
@ -103,6 +106,7 @@ class DependencyGraph extends EventEmitter {
this._helpers = new DependencyGraphHelpers(this._opts);
this._haste.on('change', this._onHasteChange.bind(this));
this._moduleCache = this._createModuleCache();
this._createModuleResolver();
}
static _createHaste(opts: Options): JestHasteMap {
@ -162,9 +166,30 @@ class DependencyGraph extends EventEmitter {
eventsQueue.forEach(({type, filePath}) =>
this._moduleCache.processFileChange(type, filePath),
);
this._createModuleResolver();
this.emit('change');
}
_createModuleResolver() {
this._moduleResolver = new ModuleResolver({
dirExists: filePath => {
try {
return fs.lstatSync(filePath).isDirectory();
} catch (e) {}
return false;
},
doesFileExist: this._doesFileExist,
extraNodeModules: this._opts.extraNodeModules,
helpers: this._helpers,
moduleCache: this._moduleCache,
moduleMap: this._moduleMap,
preferNativePlatform: this._opts.preferNativePlatform,
resolveAsset: (dirPath, assetName, platform) =>
this._assetResolutionCache.resolve(dirPath, assetName, platform),
sourceExts: this._opts.sourceExts,
});
}
_createModuleCache() {
const {_opts} = this;
return new ModuleCache(
@ -226,25 +251,12 @@ class DependencyGraph extends EventEmitter {
}): Promise<ResolutionResponse<Module, T>> {
platform = this._getRequestPlatform(entryPath, platform);
const absPath = this._getAbsolutePath(entryPath);
const dirExists = filePath => {
try {
return fs.lstatSync(filePath).isDirectory();
} catch (e) {}
return false;
};
const req = new ResolutionRequest({
dirExists,
doesFileExist: this._doesFileExist,
moduleResolver: this._moduleResolver,
entryPath: absPath,
extraNodeModules: this._opts.extraNodeModules,
helpers: this._helpers,
platform: platform != null ? platform : null,
moduleCache: this._moduleCache,
moduleMap: this._moduleMap,
platform,
preferNativePlatform: this._opts.preferNativePlatform,
resolveAsset: (dirPath, assetName) =>
this._assetResolutionCache.resolve(dirPath, assetName, platform),
sourceExts: this._opts.sourceExts,
});
const response = new ResolutionResponse(options);

View File

@ -0,0 +1,668 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @flow
* @format
*/
'use strict';
const FileNameResolver = require('./FileNameResolver');
const invariant = require('fbjs/lib/invariant');
const isAbsolutePath = require('absolute-path');
const path = require('path');
const util = require('util');
import type DependencyGraphHelpers from './DependencyGraphHelpers';
export type DirExistsFn = (filePath: string) => boolean;
/**
* `jest-haste-map`'s interface for ModuleMap.
*/
export type ModuleMap = {
getModule(
name: string,
platform: string | null,
supportsNativePlatform: boolean,
): ?string,
getPackage(
name: string,
platform: string | null,
supportsNativePlatform: boolean,
): ?string,
};
export type Packageish = {
redirectRequire(toModuleName: string): string | false,
getMain(): string,
+root: string,
};
export type Moduleish = {
+path: string,
getPackage(): ?Packageish,
};
export type ModuleishCache<TModule, TPackage> = {
getPackage(
name: string,
platform?: string,
supportsNativePlatform?: boolean,
): TPackage,
getModule(path: string): TModule,
getAssetModule(path: string): TModule,
};
type Options<TModule, TPackage> = {|
+dirExists: DirExistsFn,
+doesFileExist: (filePath: string) => boolean,
+extraNodeModules: ?Object,
+helpers: DependencyGraphHelpers,
+moduleCache: ModuleishCache<TModule, TPackage>,
+preferNativePlatform: boolean,
+moduleMap: ModuleMap,
+resolveAsset: (
dirPath: string,
assetName: string,
platform: string | null,
) => $ReadOnlyArray<string>,
+sourceExts: Array<string>,
|};
/**
* This is a way to describe what files we tried to look for when resolving
* a module name as file. This is mainly used for error reporting, so that
* we can explain why we cannot resolve a module.
*/
type FileCandidates =
// We only tried to resolve a specific asset.
| {|+type: 'asset', +name: string|}
// We attempted to resolve a name as being a source file (ex. JavaScript,
// JSON...), in which case there can be several variants we tried, for
// example `foo.ios.js`, `foo.js`, etc.
| {|+type: 'sources', +fileNames: $ReadOnlyArray<string>|};
/**
* This is a way to describe what files we tried to look for when resolving
* a module name as directory.
*/
type DirCandidates =
| {|+type: 'package', +dir: DirCandidates, +file: FileCandidates|}
| {|+type: 'index', +file: FileCandidates|};
type Resolution<TModule, TCandidates> =
| {|+type: 'resolved', +module: TModule|}
| {|+type: 'failed', +candidates: TCandidates|};
/**
* It may not be a great pattern to leverage exception just for "trying" things
* out, notably for performance. We should consider replacing these functions
* to be nullable-returning, or being better stucture to the algorithm.
*/
function tryResolveSync<T>(action: () => T, secondaryAction: () => T): T {
try {
return action();
} catch (error) {
if (error.type !== 'UnableToResolveError') {
throw error;
}
return secondaryAction();
}
}
class ModuleResolver<TModule: Moduleish, TPackage: Packageish> {
_options: Options<TModule, TPackage>;
static EMPTY_MODULE: string = require.resolve('./assets/empty-module.js');
constructor(options: Options<TModule, TPackage>) {
this._options = options;
}
resolveHasteDependency(
fromModule: TModule,
toModuleName: string,
platform: string | null,
): TModule {
toModuleName = normalizePath(toModuleName);
const pck = fromModule.getPackage();
let realModuleName;
if (pck) {
/* $FlowFixMe: redirectRequire can actually return `false` for
exclusions*/
realModuleName = (pck.redirectRequire(toModuleName): string);
} else {
realModuleName = toModuleName;
}
const modulePath = this._options.moduleMap.getModule(
realModuleName,
platform,
/* supportsNativePlatform */ true,
);
if (modulePath != null) {
const module = this._options.moduleCache.getModule(modulePath);
/* temporary until we strengthen the typing */
invariant(module.type === 'Module', 'expected Module type');
return module;
}
let packageName = realModuleName;
let packagePath;
while (packageName && packageName !== '.') {
packagePath = this._options.moduleMap.getPackage(
packageName,
platform,
/* supportsNativePlatform */ true,
);
if (packagePath != null) {
break;
}
packageName = path.dirname(packageName);
}
if (packagePath != null) {
const package_ = this._options.moduleCache.getPackage(packagePath);
/* temporary until we strengthen the typing */
invariant(package_.type === 'Package', 'expected Package type');
const potentialModulePath = path.join(
package_.root,
path.relative(packageName, realModuleName),
);
return tryResolveSync(
() =>
this._loadAsFileOrThrow(
potentialModulePath,
fromModule,
toModuleName,
platform,
),
() =>
this._loadAsDirOrThrow(
potentialModulePath,
fromModule,
toModuleName,
platform,
),
);
}
throw new UnableToResolveError(
fromModule,
toModuleName,
'Unable to resolve dependency',
);
}
_redirectRequire(fromModule: TModule, modulePath: string): string | false {
const pck = fromModule.getPackage();
if (pck) {
return pck.redirectRequire(modulePath);
}
return modulePath;
}
_resolveFileOrDir(
fromModule: TModule,
toModuleName: string,
platform: string | null,
): TModule {
const potentialModulePath = isAbsolutePath(toModuleName)
? resolveWindowsPath(toModuleName)
: path.join(path.dirname(fromModule.path), toModuleName);
const realModuleName = this._redirectRequire(
fromModule,
potentialModulePath,
);
if (realModuleName === false) {
return this._getEmptyModule(fromModule, toModuleName);
}
return tryResolveSync(
() =>
this._loadAsFileOrThrow(
realModuleName,
fromModule,
toModuleName,
platform,
),
() =>
this._loadAsDirOrThrow(
realModuleName,
fromModule,
toModuleName,
platform,
),
);
}
resolveNodeDependency(
fromModule: TModule,
toModuleName: string,
platform: string | null,
): TModule {
if (isRelativeImport(toModuleName) || isAbsolutePath(toModuleName)) {
return this._resolveFileOrDir(fromModule, toModuleName, platform);
}
const realModuleName = this._redirectRequire(fromModule, toModuleName);
// exclude
if (realModuleName === false) {
return this._getEmptyModule(fromModule, toModuleName);
}
if (isRelativeImport(realModuleName) || isAbsolutePath(realModuleName)) {
// derive absolute path /.../node_modules/fromModuleDir/realModuleName
const fromModuleParentIdx =
fromModule.path.lastIndexOf('node_modules' + path.sep) + 13;
const fromModuleDir = fromModule.path.slice(
0,
fromModule.path.indexOf(path.sep, fromModuleParentIdx),
);
const absPath = path.join(fromModuleDir, realModuleName);
return this._resolveFileOrDir(fromModule, absPath, platform);
}
const searchQueue = [];
for (
let currDir = path.dirname(fromModule.path);
currDir !== '.' && currDir !== path.parse(fromModule.path).root;
currDir = path.dirname(currDir)
) {
const searchPath = path.join(currDir, 'node_modules');
searchQueue.push(path.join(searchPath, realModuleName));
}
const extraSearchQueue = [];
if (this._options.extraNodeModules) {
const {extraNodeModules} = this._options;
const bits = toModuleName.split(path.sep);
const packageName = bits[0];
if (extraNodeModules[packageName]) {
bits[0] = extraNodeModules[packageName];
extraSearchQueue.push(path.join.apply(path, bits));
}
}
const fullSearchQueue = searchQueue.concat(extraSearchQueue);
for (let i = 0; i < fullSearchQueue.length; ++i) {
const resolvedModule = this._tryResolveNodeDep(
fullSearchQueue[i],
fromModule,
toModuleName,
platform,
);
if (resolvedModule != null) {
return resolvedModule;
}
}
const displaySearchQueue = searchQueue
.filter(dirPath => this._options.dirExists(dirPath))
.concat(extraSearchQueue);
const hint = displaySearchQueue.length ? ' or in these directories:' : '';
throw new UnableToResolveError(
fromModule,
toModuleName,
`Module does not exist in the module map${hint}\n` +
displaySearchQueue
.map(searchPath => ` ${path.dirname(searchPath)}\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 packager cache: `rm -fr $TMPDIR/react-*` or `npm start -- --reset-cache`.',
);
}
/**
* This is written as a separate function because "try..catch" blocks cause
* the entire surrounding function to be deoptimized.
*/
_tryResolveNodeDep(
searchPath: string,
fromModule: TModule,
toModuleName: string,
platform: string | null,
): ?TModule {
try {
return tryResolveSync(
() =>
this._loadAsFileOrThrow(
searchPath,
fromModule,
toModuleName,
platform,
),
() =>
this._loadAsDirOrThrow(
searchPath,
fromModule,
toModuleName,
platform,
),
);
} catch (error) {
if (error.type !== 'UnableToResolveError') {
throw error;
}
return null;
}
}
/**
* Eventually we'd like to remove all the exception being throw in the middle
* of the resolution algorithm, instead keeping track of tentatives in a
* specific data structure, and building a proper error at the top-level.
* This function is meant to be a temporary proxy for _loadAsFile until
* the callsites switch to that tracking structure.
*/
_loadAsFileOrThrow(
basePath: string,
fromModule: TModule,
toModule: string,
platform: string | null,
): TModule {
const dirPath = path.dirname(basePath);
const fileNameHint = path.basename(basePath);
const result = this._loadAsFile(dirPath, fileNameHint, platform);
if (result.type === 'resolved') {
return result.module;
}
if (result.candidates.type === 'asset') {
const msg =
`Directory \`${dirPath}' doesn't contain asset ` +
`\`${result.candidates.name}'`;
throw new UnableToResolveError(fromModule, toModule, msg);
}
invariant(result.candidates.type === 'sources', 'invalid candidate type');
const msg =
`Could not resolve the base path \`${basePath}' into a module. The ` +
`folder \`${dirPath}' was searched for one of these files: ` +
result.candidates.fileNames.map(filePath => `\`${filePath}'`).join(', ') +
'.';
throw new UnableToResolveError(fromModule, toModule, msg);
}
_loadAsFile(
dirPath: string,
fileNameHint: string,
platform: string | null,
): Resolution<TModule, FileCandidates> {
if (this._options.helpers.isAssetFile(fileNameHint)) {
return this._loadAsAssetFile(dirPath, fileNameHint, platform);
}
const {doesFileExist} = this._options;
const resolver = new FileNameResolver({doesFileExist, dirPath});
const fileName = this._tryToResolveAllFileNames(
resolver,
fileNameHint,
platform,
);
if (fileName != null) {
const filePath = path.join(dirPath, fileName);
const module = this._options.moduleCache.getModule(filePath);
return {type: 'resolved', module};
}
const fileNames = resolver.getTentativeFileNames();
return {type: 'failed', candidates: {type: 'sources', fileNames}};
}
_loadAsAssetFile(
dirPath: string,
fileNameHint: string,
platform: string | null,
): Resolution<TModule, FileCandidates> {
const {resolveAsset} = this._options;
const assetNames = resolveAsset(dirPath, fileNameHint, platform);
const assetName = getArrayLowestItem(assetNames);
if (assetName != null) {
const assetPath = path.join(dirPath, assetName);
return {
type: 'resolved',
module: this._options.moduleCache.getAssetModule(assetPath),
};
}
return {
type: 'failed',
candidates: {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.
*/
_tryToResolveAllFileNames(
resolver: FileNameResolver,
fileNamePrefix: string,
platform: ?string,
): ?string {
if (resolver.tryToResolveFileName(fileNamePrefix)) {
return fileNamePrefix;
}
const {sourceExts} = this._options;
for (let i = 0; i < sourceExts.length; i++) {
const fileName = this._tryToResolveFileNamesForExt(
fileNamePrefix,
resolver,
sourceExts[i],
platform,
);
if (fileName != null) {
return fileName;
}
}
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`.
*/
_tryToResolveFileNamesForExt(
fileNamePrefix: string,
resolver: FileNameResolver,
ext: string,
platform: ?string,
): ?string {
const {preferNativePlatform} = this._options;
if (platform != null) {
const fileName = `${fileNamePrefix}.${platform}.${ext}`;
if (resolver.tryToResolveFileName(fileName)) {
return fileName;
}
}
if (preferNativePlatform) {
const fileName = `${fileNamePrefix}.native.${ext}`;
if (resolver.tryToResolveFileName(fileName)) {
return fileName;
}
}
const fileName = `${fileNamePrefix}.${ext}`;
return resolver.tryToResolveFileName(fileName) ? fileName : null;
}
_getEmptyModule(fromModule: TModule, toModuleName: string): TModule {
const {moduleCache} = this._options;
const module = moduleCache.getModule(ModuleResolver.EMPTY_MODULE);
if (module != null) {
return module;
}
throw new UnableToResolveError(
fromModule,
toModuleName,
"could not resolve `${ModuleResolver.EMPTY_MODULE}'",
);
}
/**
* Same as `_loadAsDir`, but throws instead of returning candidates in case of
* failure. We want to migrate all the callsites to `_loadAsDir` eventually.
*/
_loadAsDirOrThrow(
potentialDirPath: string,
fromModule: TModule,
toModuleName: string,
platform: string | null,
): TModule {
const result = this._loadAsDir(potentialDirPath, platform);
if (result.type === 'resolved') {
return result.module;
}
if (result.candidates.type === 'package') {
throw new UnableToResolveError(
fromModule,
toModuleName,
`could not resolve \`${potentialDirPath}' as a folder: it contained ` +
'a package, but its "main" could not be resolved',
);
}
invariant(result.candidates.type === 'index', 'invalid candidate type');
throw new UnableToResolveError(
fromModule,
toModuleName,
`could not resolve \`${potentialDirPath}' as a folder: it did not ` +
'contain a package, nor an index file',
);
}
/**
* Try to resolve a potential path as if it was a directory-based module.
* Either this is a directory that contains a package, or that the directory
* contains an index file. If it fails to resolve these options, it returns
* `null` and fills the array of `candidates` that were tried.
*
* For example we could try to resolve `/foo/bar`, that would eventually
* resolve to `/foo/bar/lib/index.ios.js` if we're on platform iOS and that
* `bar` contains a package which entry point is `./lib/index` (or `./lib`).
*/
_loadAsDir(
potentialDirPath: string,
platform: string | null,
): Resolution<TModule, DirCandidates> {
const packageJsonPath = path.join(potentialDirPath, 'package.json');
if (this._options.doesFileExist(packageJsonPath)) {
return this._loadAsPackage(packageJsonPath, platform);
}
const result = this._loadAsFile(potentialDirPath, 'index', platform);
if (result.type === 'resolved') {
return result;
}
return {
type: 'failed',
candidates: {type: 'index', file: result.candidates},
};
}
/**
* Right now we just consider it a failure to resolve if we couldn't find the
* file corresponding to the `main` indicated by a package. Argument can be
* made this should be changed so that failing to find the `main` is not a
* resolution failure, but identified instead as a corrupted or invalid
* package (or that a package only supports a specific platform, etc.)
*/
_loadAsPackage(
packageJsonPath: string,
platform: string | null,
): Resolution<TModule, DirCandidates> {
const package_ = this._options.moduleCache.getPackage(packageJsonPath);
const mainPrefixPath = package_.getMain();
const dirPath = path.dirname(mainPrefixPath);
const prefixName = path.basename(mainPrefixPath);
const fileResult = this._loadAsFile(dirPath, prefixName, platform);
if (fileResult.type === 'resolved') {
return fileResult;
}
const dirResult = this._loadAsDir(mainPrefixPath, platform);
if (dirResult.type === 'resolved') {
return dirResult;
}
return {
type: 'failed',
candidates: {
type: 'package',
dir: dirResult.candidates,
file: fileResult.candidates,
},
};
}
}
// 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.
function resolveWindowsPath(modulePath) {
if (path.sep !== '\\') {
return modulePath;
}
return path.resolve(modulePath);
}
function isRelativeImport(filePath: string) {
return /^[.][.]?(?:[/]|$)/.test(filePath);
}
function normalizePath(modulePath) {
if (path.sep === '/') {
modulePath = path.normalize(modulePath);
} else if (path.posix) {
modulePath = path.posix.normalize(modulePath);
}
return modulePath.replace(/\/$/, '');
}
function getArrayLowestItem(a: $ReadOnlyArray<string>): string | void {
if (a.length === 0) {
return undefined;
}
let lowest = a[0];
for (let i = 1; i < a.length; ++i) {
if (a[i] < lowest) {
lowest = a[i];
}
}
return lowest;
}
class UnableToResolveError<TModule: Moduleish> extends Error {
type: string;
from: string;
to: string;
constructor(fromModule: TModule, toModule: string, message: string) {
super();
this.from = fromModule.path;
this.to = toModule;
this.message = util.format(
'Unable to resolve module `%s` from `%s`: %s',
toModule,
fromModule.path,
message,
);
this.type = this.name = 'UnableToResolveError';
}
}
module.exports = {
UnableToResolveError,
ModuleResolver,
isRelativeImport,
tryResolveSync,
};

View File

@ -13,15 +13,12 @@
'use strict';
const AsyncTaskGroup = require('../lib/AsyncTaskGroup');
const FileNameResolver = require('./FileNameResolver');
const MapWithDefaults = require('../lib/MapWithDefaults');
const ModuleResolution = require('./ModuleResolution');
const debug = require('debug')('RNP:DependencyGraph');
const util = require('util');
const path = require('path');
const realPath = require('path');
const invariant = require('fbjs/lib/invariant');
const isAbsolutePath = require('absolute-path');
const path = require('path');
import type DependencyGraphHelpers from './DependencyGraphHelpers';
import type ResolutionResponse from './ResolutionResponse';
@ -29,24 +26,9 @@ import type {
Options as TransformWorkerOptions,
} from '../../JSTransformer/worker';
import type {ReadResult, CachedReadResult} from '../Module';
import type {ModuleResolver} from './ModuleResolution';
type DirExistsFn = (filePath: string) => boolean;
/**
* `jest-haste-map`'s interface for ModuleMap.
*/
export type ModuleMap = {
getModule(
name: string,
platform: ?string,
supportsNativePlatform: boolean,
): ?string,
getPackage(
name: string,
platform: ?string,
supportsNativePlatform: boolean,
): ?string,
};
const {UnableToResolveError, isRelativeImport} = ModuleResolution;
export type Packageish = {
isHaste(): boolean,
@ -78,83 +60,22 @@ export type ModuleishCache<TModule, TPackage> = {
};
type Options<TModule, TPackage> = {|
+dirExists: DirExistsFn,
+doesFileExist: (filePath: string) => boolean,
+entryPath: string,
+extraNodeModules: ?Object,
+helpers: DependencyGraphHelpers,
+moduleCache: ModuleishCache<TModule, TPackage>,
+moduleMap: ModuleMap,
+platform: ?string,
+preferNativePlatform: boolean,
+resolveAsset: (dirPath: string, assetName: string) => $ReadOnlyArray<string>,
+sourceExts: Array<string>,
+moduleResolver: ModuleResolver<TModule, TPackage>,
+platform: string | null,
|};
/**
* This is a way to describe what files we tried to look for when resolving
* a module name as file. This is mainly used for error reporting, so that
* we can explain why we cannot resolve a module.
*/
type FileCandidates =
// We only tried to resolve a specific asset.
| {|+type: 'asset', +name: string|}
// We attempted to resolve a name as being a source file (ex. JavaScript,
// JSON...), in which case there can be several variants we tried, for
// example `foo.ios.js`, `foo.js`, etc.
| {|+type: 'sources', +fileNames: $ReadOnlyArray<string>|};
/**
* This is a way to describe what files we tried to look for when resolving
* a module name as directory.
*/
type DirCandidates =
| {|+type: 'package', +dir: DirCandidates, +file: FileCandidates|}
| {|+type: 'index', +file: FileCandidates|};
type Resolution<TModule, TCandidates> =
| {|+type: 'resolved', +module: TModule|}
| {|+type: 'failed', +candidates: TCandidates|};
/**
* It may not be a great pattern to leverage exception just for "trying" things
* out, notably for performance. We should consider replacing these functions
* to be nullable-returning, or being better stucture to the algorithm.
*/
function tryResolveSync<T>(action: () => T, secondaryAction: () => T): T {
try {
return action();
} catch (error) {
if (error.type !== 'UnableToResolveError') {
throw error;
}
return secondaryAction();
}
}
class ResolutionRequest<TModule: Moduleish, TPackage: Packageish> {
_immediateResolutionCache: {[key: string]: TModule};
_options: Options<TModule, TPackage>;
static EMPTY_MODULE: string = require.resolve('./assets/empty-module.js');
constructor(options: Options<TModule, TPackage>) {
this._options = options;
this._resetResolutionCache();
}
_tryResolve<T>(
action: () => Promise<T>,
secondaryAction: () => ?Promise<T>,
): Promise<T> {
return action().catch(error => {
if (error.type !== 'UnableToResolveError') {
throw error;
}
return secondaryAction();
});
}
resolveDependency(fromModule: TModule, toModuleName: string): TModule {
const resHash = resolutionHash(fromModule.path, toModuleName);
@ -168,18 +89,25 @@ class ResolutionRequest<TModule: Moduleish, TPackage: Packageish> {
return result;
};
const resolver = this._options.moduleResolver;
const platform = this._options.platform;
if (
!this._options.helpers.isNodeModulesDir(fromModule.path) &&
!(isRelativeImport(toModuleName) || isAbsolutePath(toModuleName))
) {
const result = tryResolveSync(
() => this._resolveHasteDependency(fromModule, toModuleName),
() => this._resolveNodeDependency(fromModule, toModuleName),
const result = ModuleResolution.tryResolveSync(
() =>
resolver.resolveHasteDependency(fromModule, toModuleName, platform),
() =>
resolver.resolveNodeDependency(fromModule, toModuleName, platform),
);
return cacheResult(result);
}
return cacheResult(this._resolveNodeDependency(fromModule, toModuleName));
return cacheResult(
resolver.resolveNodeDependency(fromModule, toModuleName, platform),
);
}
resolveModuleDependencies(
@ -414,421 +342,6 @@ class ResolutionRequest<TModule: Moduleish, TPackage: Packageish> {
return result;
}
_resolveHasteDependency(fromModule: TModule, toModuleName: string): TModule {
toModuleName = normalizePath(toModuleName);
const pck = fromModule.getPackage();
let realModuleName;
if (pck) {
/* $FlowFixMe: redirectRequire can actually return `false` for
exclusions*/
realModuleName = (pck.redirectRequire(toModuleName): string);
} else {
realModuleName = toModuleName;
}
const modulePath = this._options.moduleMap.getModule(
realModuleName,
this._options.platform,
/* supportsNativePlatform */ true,
);
if (modulePath != null) {
const module = this._options.moduleCache.getModule(modulePath);
/* temporary until we strengthen the typing */
invariant(module.type === 'Module', 'expected Module type');
return module;
}
let packageName = realModuleName;
let packagePath;
while (packageName && packageName !== '.') {
packagePath = this._options.moduleMap.getPackage(
packageName,
this._options.platform,
/* supportsNativePlatform */ true,
);
if (packagePath != null) {
break;
}
packageName = path.dirname(packageName);
}
if (packagePath != null) {
const package_ = this._options.moduleCache.getPackage(packagePath);
/* temporary until we strengthen the typing */
invariant(package_.type === 'Package', 'expected Package type');
const potentialModulePath = path.join(
package_.root,
path.relative(packageName, realModuleName),
);
return tryResolveSync(
() =>
this._loadAsFileOrThrow(
potentialModulePath,
fromModule,
toModuleName,
),
() =>
this._loadAsDirOrThrow(potentialModulePath, fromModule, toModuleName),
);
}
throw new UnableToResolveError(
fromModule,
toModuleName,
'Unable to resolve dependency',
);
}
_redirectRequire(fromModule: TModule, modulePath: string): string | false {
const pck = fromModule.getPackage();
if (pck) {
return pck.redirectRequire(modulePath);
}
return modulePath;
}
_resolveFileOrDir(fromModule: TModule, toModuleName: string): TModule {
const potentialModulePath = isAbsolutePath(toModuleName)
? resolveWindowsPath(toModuleName)
: path.join(path.dirname(fromModule.path), toModuleName);
const realModuleName = this._redirectRequire(
fromModule,
potentialModulePath,
);
if (realModuleName === false) {
return this._getEmptyModule(fromModule, toModuleName);
}
return tryResolveSync(
() => this._loadAsFileOrThrow(realModuleName, fromModule, toModuleName),
() => this._loadAsDirOrThrow(realModuleName, fromModule, toModuleName),
);
}
_resolveNodeDependency(fromModule: TModule, toModuleName: string): TModule {
if (isRelativeImport(toModuleName) || isAbsolutePath(toModuleName)) {
return this._resolveFileOrDir(fromModule, toModuleName);
}
const realModuleName = this._redirectRequire(fromModule, toModuleName);
// exclude
if (realModuleName === false) {
return this._getEmptyModule(fromModule, toModuleName);
}
if (isRelativeImport(realModuleName) || isAbsolutePath(realModuleName)) {
// derive absolute path /.../node_modules/fromModuleDir/realModuleName
const fromModuleParentIdx =
fromModule.path.lastIndexOf('node_modules' + path.sep) + 13;
const fromModuleDir = fromModule.path.slice(
0,
fromModule.path.indexOf(path.sep, fromModuleParentIdx),
);
const absPath = path.join(fromModuleDir, realModuleName);
return this._resolveFileOrDir(fromModule, absPath);
}
const searchQueue = [];
for (
let currDir = path.dirname(fromModule.path);
currDir !== '.' && currDir !== realPath.parse(fromModule.path).root;
currDir = path.dirname(currDir)
) {
const searchPath = path.join(currDir, 'node_modules');
searchQueue.push(path.join(searchPath, realModuleName));
}
const extraSearchQueue = [];
if (this._options.extraNodeModules) {
const {extraNodeModules} = this._options;
const bits = toModuleName.split(path.sep);
const packageName = bits[0];
if (extraNodeModules[packageName]) {
bits[0] = extraNodeModules[packageName];
extraSearchQueue.push(path.join.apply(path, bits));
}
}
const fullSearchQueue = searchQueue.concat(extraSearchQueue);
for (let i = 0; i < fullSearchQueue.length; ++i) {
const resolvedModule = this._tryResolveNodeDep(
fullSearchQueue[i],
fromModule,
toModuleName,
);
if (resolvedModule != null) {
return resolvedModule;
}
}
const displaySearchQueue = searchQueue
.filter(dirPath => this._options.dirExists(dirPath))
.concat(extraSearchQueue);
const hint = displaySearchQueue.length ? ' or in these directories:' : '';
throw new UnableToResolveError(
fromModule,
toModuleName,
`Module does not exist in the module map${hint}\n` +
displaySearchQueue
.map(searchPath => ` ${path.dirname(searchPath)}\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 packager cache: `rm -fr $TMPDIR/react-*` or `npm start -- --reset-cache`.',
);
}
/**
* This is written as a separate function because "try..catch" blocks cause
* the entire surrounding function to be deoptimized.
*/
_tryResolveNodeDep(
searchPath: string,
fromModule: TModule,
toModuleName: string,
): ?TModule {
try {
return tryResolveSync(
() => this._loadAsFileOrThrow(searchPath, fromModule, toModuleName),
() => this._loadAsDirOrThrow(searchPath, fromModule, toModuleName),
);
} catch (error) {
if (error.type !== 'UnableToResolveError') {
throw error;
}
return null;
}
}
/**
* Eventually we'd like to remove all the exception being throw in the middle
* of the resolution algorithm, instead keeping track of tentatives in a
* specific data structure, and building a proper error at the top-level.
* This function is meant to be a temporary proxy for _loadAsFile until
* the callsites switch to that tracking structure.
*/
_loadAsFileOrThrow(
basePath: string,
fromModule: TModule,
toModule: string,
): TModule {
const dirPath = path.dirname(basePath);
const fileNameHint = path.basename(basePath);
const result = this._loadAsFile(dirPath, fileNameHint);
if (result.type === 'resolved') {
return result.module;
}
if (result.candidates.type === 'asset') {
const msg =
`Directory \`${dirPath}' doesn't contain asset ` +
`\`${result.candidates.name}'`;
throw new UnableToResolveError(fromModule, toModule, msg);
}
invariant(result.candidates.type === 'sources', 'invalid candidate type');
const msg =
`Could not resolve the base path \`${basePath}' into a module. The ` +
`folder \`${dirPath}' was searched for one of these files: ` +
result.candidates.fileNames.map(filePath => `\`${filePath}'`).join(', ') +
'.';
throw new UnableToResolveError(fromModule, toModule, msg);
}
_loadAsFile(
dirPath: string,
fileNameHint: string,
): Resolution<TModule, FileCandidates> {
if (this._options.helpers.isAssetFile(fileNameHint)) {
return this._loadAsAssetFile(dirPath, fileNameHint);
}
const {doesFileExist} = this._options;
const resolver = new FileNameResolver({doesFileExist, dirPath});
const fileName = this._tryToResolveAllFileNames(resolver, fileNameHint);
if (fileName != null) {
const filePath = path.join(dirPath, fileName);
const module = this._options.moduleCache.getModule(filePath);
return {type: 'resolved', module};
}
const fileNames = resolver.getTentativeFileNames();
return {type: 'failed', candidates: {type: 'sources', fileNames}};
}
_loadAsAssetFile(
dirPath: string,
fileNameHint: string,
): Resolution<TModule, FileCandidates> {
const assetNames = this._options.resolveAsset(dirPath, fileNameHint);
const assetName = getArrayLowestItem(assetNames);
if (assetName != null) {
const assetPath = path.join(dirPath, assetName);
return {
type: 'resolved',
module: this._options.moduleCache.getAssetModule(assetPath),
};
}
return {
type: 'failed',
candidates: {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.
*/
_tryToResolveAllFileNames(
resolver: FileNameResolver,
fileNamePrefix: string,
): ?string {
if (resolver.tryToResolveFileName(fileNamePrefix)) {
return fileNamePrefix;
}
const {sourceExts} = this._options;
for (let i = 0; i < sourceExts.length; i++) {
const fileName = this._tryToResolveFileNamesForExt(
fileNamePrefix,
resolver,
sourceExts[i],
);
if (fileName != null) {
return fileName;
}
}
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`.
*/
_tryToResolveFileNamesForExt(
fileNamePrefix: string,
resolver: FileNameResolver,
ext: string,
): ?string {
const {platform, preferNativePlatform} = this._options;
if (platform != null) {
const fileName = `${fileNamePrefix}.${platform}.${ext}`;
if (resolver.tryToResolveFileName(fileName)) {
return fileName;
}
}
if (preferNativePlatform) {
const fileName = `${fileNamePrefix}.native.${ext}`;
if (resolver.tryToResolveFileName(fileName)) {
return fileName;
}
}
const fileName = `${fileNamePrefix}.${ext}`;
return resolver.tryToResolveFileName(fileName) ? fileName : null;
}
_getEmptyModule(fromModule: TModule, toModuleName: string): TModule {
const {moduleCache} = this._options;
const module = moduleCache.getModule(ResolutionRequest.EMPTY_MODULE);
if (module != null) {
return module;
}
throw new UnableToResolveError(
fromModule,
toModuleName,
"could not resolve `${ResolutionRequest.EMPTY_MODULE}'",
);
}
/**
* Same as `_loadAsDir`, but throws instead of returning candidates in case of
* failure. We want to migrate all the callsites to `_loadAsDir` eventually.
*/
_loadAsDirOrThrow(
potentialDirPath: string,
fromModule: TModule,
toModuleName: string,
): TModule {
const result = this._loadAsDir(potentialDirPath);
if (result.type === 'resolved') {
return result.module;
}
if (result.candidates.type === 'package') {
throw new UnableToResolveError(
fromModule,
toModuleName,
`could not resolve \`${potentialDirPath}' as a folder: it contained ` +
'a package, but its "main" could not be resolved',
);
}
invariant(result.candidates.type === 'index', 'invalid candidate type');
throw new UnableToResolveError(
fromModule,
toModuleName,
`could not resolve \`${potentialDirPath}' as a folder: it did not ` +
'contain a package, nor an index file',
);
}
/**
* Try to resolve a potential path as if it was a directory-based module.
* Either this is a directory that contains a package, or that the directory
* contains an index file. If it fails to resolve these options, it returns
* `null` and fills the array of `candidates` that were tried.
*
* For example we could try to resolve `/foo/bar`, that would eventually
* resolve to `/foo/bar/lib/index.ios.js` if we're on platform iOS and that
* `bar` contains a package which entry point is `./lib/index` (or `./lib`).
*/
_loadAsDir(potentialDirPath: string): Resolution<TModule, DirCandidates> {
const packageJsonPath = path.join(potentialDirPath, 'package.json');
if (this._options.doesFileExist(packageJsonPath)) {
return this._loadAsPackage(packageJsonPath);
}
const result = this._loadAsFile(potentialDirPath, 'index');
if (result.type === 'resolved') {
return result;
}
return {
type: 'failed',
candidates: {type: 'index', file: result.candidates},
};
}
/**
* Right now we just consider it a failure to resolve if we couldn't find the
* file corresponding to the `main` indicated by a package. Argument can be
* made this should be changed so that failing to find the `main` is not a
* resolution failure, but identified instead as a corrupted or invalid
* package (or that a package only supports a specific platform, etc.)
*/
_loadAsPackage(packageJsonPath: string): Resolution<TModule, DirCandidates> {
const package_ = this._options.moduleCache.getPackage(packageJsonPath);
const mainPrefixPath = package_.getMain();
const dirPath = path.dirname(mainPrefixPath);
const prefixName = path.basename(mainPrefixPath);
const fileResult = this._loadAsFile(dirPath, prefixName);
if (fileResult.type === 'resolved') {
return fileResult;
}
const dirResult = this._loadAsDir(mainPrefixPath);
if (dirResult.type === 'resolved') {
return dirResult;
}
return {
type: 'failed',
candidates: {
type: 'package',
dir: dirResult.candidates,
file: fileResult.candidates,
},
};
}
_resetResolutionCache() {
this._immediateResolutionCache = Object.create(null);
}
@ -838,60 +351,4 @@ function resolutionHash(modulePath, depName) {
return `${path.resolve(modulePath)}:${depName}`;
}
class UnableToResolveError extends Error {
type: string;
from: string;
to: string;
constructor(fromModule, toModule, message) {
super();
this.from = fromModule.path;
this.to = toModule;
this.message = util.format(
'Unable to resolve module `%s` from `%s`: %s',
toModule,
fromModule.path,
message,
);
this.type = this.name = 'UnableToResolveError';
}
}
function normalizePath(modulePath) {
if (path.sep === '/') {
modulePath = path.normalize(modulePath);
} else if (path.posix) {
modulePath = path.posix.normalize(modulePath);
}
return modulePath.replace(/\/$/, '');
}
// 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.
function resolveWindowsPath(modulePath) {
if (path.sep !== '\\') {
return modulePath;
}
return path.resolve(modulePath);
}
function isRelativeImport(filePath) {
return /^[.][.]?(?:[/]|$)/.test(filePath);
}
function getArrayLowestItem(a: $ReadOnlyArray<string>): string | void {
if (a.length === 0) {
return undefined;
}
let lowest = a[0];
for (let i = 1; i < a.length; ++i) {
if (a[i] < lowest) {
lowest = a[i];
}
}
return lowest;
}
module.exports = ResolutionRequest;

View File

@ -1942,7 +1942,8 @@ describe('DependencyGraph', function() {
it(
'should support browser exclude of a package ("' + fieldName + '")',
function() {
ResolutionRequest.EMPTY_MODULE = '/root/emptyModule.js';
require('../DependencyGraph/ModuleResolution').ModuleResolver.EMPTY_MODULE =
'/root/emptyModule.js';
var root = '/root';
setMockFileSystem({
root: {
@ -2020,7 +2021,8 @@ describe('DependencyGraph', function() {
it(
'should support browser exclude of a file ("' + fieldName + '")',
function() {
ResolutionRequest.EMPTY_MODULE = '/root/emptyModule.js';
require('../DependencyGraph/ModuleResolution').ModuleResolver.EMPTY_MODULE =
'/root/emptyModule.js';
var root = '/root';
setMockFileSystem({