Files
react-native/packager/react-packager/src/node-haste/DependencyGraph/ResolutionRequest.js
T
Christoph PojerandFacebook Github Bot 6554ad5983 Kill fastfs
Summary:
This kills fastfs in favor of Jest's hasteFS. It gets rid of a ton of code, including the mocking code in ResolutionRequest which we don't need any more. Next step after this is to rewrite HasteMap, ModuleCache, Module/Package. We are getting closer to a nicer and faster world! :)

Here is what I did:
* Use Jest's HasteFS instead of fastfs. A fresh instance is received every time something changes on the FS.
* HasteFS is not shared with everything any more. Only one reference is kept in DependencyGraph and there are a few smaller functions that are passed around (getClosestPackage and dirExists). Note: `dirExists` now does fs access instead of an offline check. This sucks but stat calls aren't slow and aren't going to be a bottleneck in ResolutionRequest, I promise! When it is time to tackle a ResolutionRequest rewrite with jest-resolve, this will go away. "It gets worse before it gets better" :) The ModuleGraph equivalent does *not* do fs access and retains the previous way of doing things because we shouldn't do online fs access there.
* Add flow annotations to ResolutionRequest. This required a few tiny hacks for now because of ModuleGraph's duck typing. I'll get rid of this soon.
* Updated ModuleGraph to work with the new code, also created a mock HasteFS instance there.
* I fixed a few tiny mock issues for `fs` to make the tests work; I had to add one tiny little internal update to `dgraph._hasteFS._files` because the file watching in the tests isn't real. It is instrumented through some function calls, therefore the hasteFS instance doesn't get automatically updated. One way to solve this is to add `JestHasteMap.emit('change', …)` for testing but I didn't want to cut a Jest release just for that. #movefast

(Note: I will likely land this in 1.5 weeks from now after my vacation and I have yet to fully test all the product flows. Please give me feedback so I can make sure this is solid!)

Reviewed By: davidaurelio

Differential Revision: D4204082

fbshipit-source-id: d6dc9fcb77ac224df4554a59f0fce241c01b0512
2016-11-30 04:28:32 -08:00

530 lines
16 KiB
JavaScript

/**
* 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
*/
'use strict';
const AsyncTaskGroup = require('../lib/AsyncTaskGroup');
const MapWithDefaults = require('../lib/MapWithDefaults');
const debug = require('debug')('ReactNativePackager:DependencyGraph');
const util = require('util');
const path = require('path');
const realPath = require('path');
const isAbsolutePath = require('absolute-path');
const getAssetDataFromName = require('../lib/getAssetDataFromName');
import type {HasteFS} from '../types';
import type DependencyGraphHelpers from './DependencyGraphHelpers';
import type HasteMap from './HasteMap';
import type Module from '../Module';
import type ModuleCache from '../ModuleCache';
import type ResolutionResponse from './ResolutionResponse';
const emptyModule = require.resolve('./assets/empty-module.js');
type DirExistsFn = (filePath: string) => boolean;
type Options = {
dirExists: DirExistsFn,
entryPath: string,
extraNodeModules: Object,
hasteFS: HasteFS,
hasteMap: HasteMap,
helpers: DependencyGraphHelpers,
// TODO(cpojer): Remove 'any' type. This is used for ModuleGraph/node-haste
moduleCache: ModuleCache | any,
platform: string,
platforms: Set<string>,
preferNativePlatform: boolean,
shouldThrowOnUnresolvedErrors: () => boolean,
};
class ResolutionRequest {
_dirExists: DirExistsFn;
_entryPath: string;
_extraNodeModules: Object;
_hasteFS: HasteFS;
_hasteMap: HasteMap;
_helpers: DependencyGraphHelpers;
_immediateResolutionCache: {[key: string]: string};
_moduleCache: ModuleCache;
_platform: string;
_platforms: Set<string>;
_preferNativePlatform: boolean;
_shouldThrowOnUnresolvedErrors: () => boolean;
constructor({
dirExists,
entryPath,
extraNodeModules,
hasteFS,
hasteMap,
helpers,
moduleCache,
platform,
platforms,
preferNativePlatform,
shouldThrowOnUnresolvedErrors,
}: Options) {
this._dirExists = dirExists;
this._entryPath = entryPath;
this._extraNodeModules = extraNodeModules;
this._hasteFS = hasteFS;
this._hasteMap = hasteMap;
this._helpers = helpers;
this._moduleCache = moduleCache;
this._platform = platform;
this._platforms = platforms;
this._preferNativePlatform = preferNativePlatform;
this._shouldThrowOnUnresolvedErrors = shouldThrowOnUnresolvedErrors;
this._resetResolutionCache();
}
_tryResolve(action, secondaryAction) {
return action().catch((error) => {
if (error.type !== 'UnableToResolveError') {
throw error;
}
return secondaryAction();
});
}
// TODO(cpojer): Remove 'any' type. This is used for ModuleGraph/node-haste
resolveDependency(fromModule: Module | any, toModuleName: string) {
const resHash = resolutionHash(fromModule.path, toModuleName);
if (this._immediateResolutionCache[resHash]) {
return Promise.resolve(this._immediateResolutionCache[resHash]);
}
const cacheResult = (result) => {
this._immediateResolutionCache[resHash] = result;
return result;
};
const forgive = (error) => {
if (
error.type !== 'UnableToResolveError' ||
this._shouldThrowOnUnresolvedErrors(this._entryPath, this._platform)
) {
throw error;
}
debug(
'Unable to resolve module %s from %s',
toModuleName,
fromModule.path
);
return null;
};
if (!this._helpers.isNodeModulesDir(fromModule.path)
&& !(isRelativeImport(toModuleName) || isAbsolutePath(toModuleName))) {
return this._tryResolve(
() => this._resolveHasteDependency(fromModule, toModuleName),
() => this._resolveNodeDependency(fromModule, toModuleName)
).then(
cacheResult,
forgive,
);
}
return this._resolveNodeDependency(fromModule, toModuleName)
.then(
cacheResult,
forgive,
);
}
getOrderedDependencies({
response,
transformOptions,
onProgress,
recursive = true,
}: {
response: ResolutionResponse,
transformOptions: Object,
onProgress: () => void,
recursive: boolean,
}) {
const entry = this._moduleCache.getModule(this._entryPath);
response.pushDependency(entry);
let totalModules = 1;
let finishedModules = 0;
const resolveDependencies = module =>
module.getDependencies(transformOptions)
.then(dependencyNames =>
Promise.all(
dependencyNames.map(name => this.resolveDependency(module, name))
).then(dependencies => [dependencyNames, dependencies])
);
const collectedDependencies = new MapWithDefaults(module => collect(module));
const crawlDependencies = (mod, [depNames, dependencies]) => {
const filteredPairs = [];
dependencies.forEach((modDep, i) => {
const name = depNames[i];
if (modDep == null) {
debug(
'WARNING: Cannot find required module `%s` from module `%s`',
name,
mod.path
);
return false;
}
return filteredPairs.push([name, modDep]);
});
response.setResolvedDependencyPairs(mod, filteredPairs);
const dependencyModules = filteredPairs.map(([, m]) => m);
const newDependencies =
dependencyModules.filter(m => !collectedDependencies.has(m));
if (onProgress) {
finishedModules += 1;
totalModules += newDependencies.length;
onProgress(finishedModules, totalModules);
}
if (recursive) {
// doesn't block the return of this function invocation, but defers
// the resulution of collectionsInProgress.done.then(...)
dependencyModules
.forEach(dependency => collectedDependencies.get(dependency));
}
return dependencyModules;
};
const collectionsInProgress = new AsyncTaskGroup();
function collect(module) {
collectionsInProgress.start(module);
const result = resolveDependencies(module)
.then(deps => crawlDependencies(module, deps));
const end = () => collectionsInProgress.end(module);
result.then(end, end);
return result;
}
return Promise.all([
// kicks off recursive dependency discovery, but doesn't block until it's done
collectedDependencies.get(entry),
// resolves when there are no more modules resolving dependencies
collectionsInProgress.done,
]).then(([rootDependencies]) => {
return Promise.all(
Array.from(collectedDependencies, resolveKeyWithPromise)
).then(moduleToDependenciesPairs =>
[rootDependencies, new MapWithDefaults(() => [], moduleToDependenciesPairs)]
);
}).then(([rootDependencies, moduleDependencies]) => {
// serialize dependencies, and make sure that every single one is only
// included once
const seen = new Set([entry]);
function traverse(dependencies) {
dependencies.forEach(dependency => {
if (seen.has(dependency)) { return; }
seen.add(dependency);
response.pushDependency(dependency);
traverse(moduleDependencies.get(dependency));
});
}
traverse(rootDependencies);
});
}
_resolveHasteDependency(fromModule, toModuleName) {
toModuleName = normalizePath(toModuleName);
let p = fromModule.getPackage();
if (p) {
p = p.redirectRequire(toModuleName);
} else {
p = Promise.resolve(toModuleName);
}
return p.then((realModuleName) => {
let dep = this._hasteMap.getModule(realModuleName, this._platform);
if (dep && dep.type === 'Module') {
return dep;
}
let packageName = realModuleName;
while (packageName && packageName !== '.') {
dep = this._hasteMap.getModule(packageName, this._platform);
if (dep && dep.type === 'Package') {
break;
}
packageName = path.dirname(packageName);
}
if (dep && dep.type === 'Package') {
const potentialModulePath = path.join(
dep.root,
path.relative(packageName, realModuleName)
);
return this._tryResolve(
() => this._loadAsFile(
potentialModulePath,
fromModule,
toModuleName,
),
() => this._loadAsDir(potentialModulePath, fromModule, toModuleName),
);
}
throw new UnableToResolveError(
fromModule,
toModuleName,
'Unable to resolve dependency',
);
});
}
_redirectRequire(fromModule, modulePath) {
return Promise.resolve(fromModule.getPackage()).then(p => {
if (p) {
return p.redirectRequire(modulePath);
}
return modulePath;
});
}
_resolveFileOrDir(fromModule, toModuleName) {
const potentialModulePath = isAbsolutePath(toModuleName) ?
toModuleName :
path.join(path.dirname(fromModule.path), toModuleName);
return this._redirectRequire(fromModule, potentialModulePath).then(
realModuleName => {
if (realModuleName === false) {
return this._loadAsFile(emptyModule, fromModule, toModuleName);
}
return this._tryResolve(
() => this._loadAsFile(realModuleName, fromModule, toModuleName),
() => this._loadAsDir(realModuleName, fromModule, toModuleName)
);
}
);
}
_resolveNodeDependency(fromModule, toModuleName) {
if (isRelativeImport(toModuleName) || isAbsolutePath(toModuleName)) {
return this._resolveFileOrDir(fromModule, toModuleName);
} else {
return this._redirectRequire(fromModule, toModuleName).then(
realModuleName => {
// exclude
if (realModuleName === false) {
return this._loadAsFile(emptyModule, fromModule, toModuleName);
}
if (isRelativeImport(realModuleName) || isAbsolutePath(realModuleName)) {
// derive absolute path /.../node_modules/fromModuleDir/realModuleName
const fromModuleParentIdx = fromModule.path.lastIndexOf('node_modules/') + 13;
const fromModuleDir = fromModule.path.slice(
0,
fromModule.path.indexOf('/', 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');
if (this._dirExists(searchPath)) {
searchQueue.push(
path.join(searchPath, realModuleName)
);
}
}
if (this._extraNodeModules) {
const bits = toModuleName.split('/');
const packageName = bits[0];
if (this._extraNodeModules[packageName]) {
bits[0] = this._extraNodeModules[packageName];
searchQueue.push(path.join.apply(path, bits));
}
}
let p = Promise.reject(new UnableToResolveError(fromModule, toModuleName));
searchQueue.forEach(potentialModulePath => {
p = this._tryResolve(
() => this._tryResolve(
() => p,
() => this._loadAsFile(potentialModulePath, fromModule, toModuleName),
),
() => this._loadAsDir(potentialModulePath, fromModule, toModuleName)
);
});
return p.catch(error => {
if (error.type !== 'UnableToResolveError') {
throw error;
}
const hint = searchQueue.length ? ' or in these directories:' : '';
throw new UnableToResolveError(
fromModule,
toModuleName,
`Module does not exist in the module map${hint}\n` +
searchQueue.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`.'
);
});
});
}
}
_loadAsFile(potentialModulePath, fromModule, toModule) {
return Promise.resolve().then(() => {
if (this._helpers.isAssetFile(potentialModulePath)) {
const dirname = path.dirname(potentialModulePath);
if (!this._dirExists(dirname)) {
throw new UnableToResolveError(
fromModule,
toModule,
`Directory ${dirname} doesn't exist`,
);
}
const {name, type} = getAssetDataFromName(potentialModulePath, this._platforms);
let pattern = name + '(@[\\d\\.]+x)?';
if (this._platform != null) {
pattern += '(\\.' + this._platform + ')?';
}
pattern += '\\.' + type;
// We arbitrarly grab the first one, because scale selection
// will happen somewhere
const [assetFile] = this._hasteFS.matchFiles(
new RegExp(dirname + '(\/|\\\\)' + pattern)
);
if (assetFile) {
return this._moduleCache.getAssetModule(assetFile);
}
}
let file;
if (this._hasteFS.exists(potentialModulePath)) {
file = potentialModulePath;
} else if (this._platform != null &&
this._hasteFS.exists(potentialModulePath + '.' + this._platform + '.js')) {
file = potentialModulePath + '.' + this._platform + '.js';
} else if (this._preferNativePlatform &&
this._hasteFS.exists(potentialModulePath + '.native.js')) {
file = potentialModulePath + '.native.js';
} else if (this._hasteFS.exists(potentialModulePath + '.js')) {
file = potentialModulePath + '.js';
} else if (this._hasteFS.exists(potentialModulePath + '.json')) {
file = potentialModulePath + '.json';
} else {
throw new UnableToResolveError(
fromModule,
toModule,
`File ${potentialModulePath} doesn't exist`,
);
}
return this._moduleCache.getModule(file);
});
}
_loadAsDir(potentialDirPath, fromModule, toModule) {
return Promise.resolve().then(() => {
if (!this._dirExists(potentialDirPath)) {
throw new UnableToResolveError(
fromModule,
toModule,
`Directory ${potentialDirPath} doesn't exist`,
);
}
const packageJsonPath = path.join(potentialDirPath, 'package.json');
if (this._hasteFS.exists(packageJsonPath)) {
return this._moduleCache.getPackage(packageJsonPath)
.getMain().then(
(main) => this._tryResolve(
() => this._loadAsFile(main, fromModule, toModule),
() => this._loadAsDir(main, fromModule, toModule)
)
);
}
return this._loadAsFile(
path.join(potentialDirPath, 'index'),
fromModule,
toModule,
);
});
}
_resetResolutionCache() {
this._immediateResolutionCache = Object.create(null);
}
}
function resolutionHash(modulePath, depName) {
return `${path.resolve(modulePath)}:${depName}`;
}
class UnableToResolveError extends Error {
type: string;
constructor(fromModule, toModule, message) {
super();
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(/\/$/, '');
}
function resolveKeyWithPromise([key, promise]) {
return promise.then(value => [key, value]);
}
function isRelativeImport(filePath) {
return /^[.][.]?(?:[/]|$)/.test(filePath);
}
module.exports = ResolutionRequest;