mirror of
https://github.com/status-im/metro.git
synced 2025-02-18 14:07:15 +00:00
ResolutionRequest: extract module resolution
Reviewed By: davidaurelio Differential Revision: D5218015 fbshipit-source-id: 6e34df5913d96a0b518f9403309658ea0b559730
This commit is contained in:
parent
b559412a08
commit
d3195fa528
@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
@ -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,
|
||||
};
|
@ -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;
|
||||
|
@ -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({
|
||||
|
Loading…
x
Reference in New Issue
Block a user