/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
* @format
*/
'use strict';
const AssetResolutionCache = require('./AssetResolutionCache');
const DependencyGraphHelpers = require('./DependencyGraph/DependencyGraphHelpers');
const JestHasteMap = require('jest-haste-map');
const Module = require('./Module');
const ModuleCache = require('./ModuleCache');
const ResolutionRequest = require('./DependencyGraph/ResolutionRequest');
const fs = require('fs');
const path = require('path');
const {ModuleResolver} = require('./DependencyGraph/ModuleResolution');
const {EventEmitter} = require('events');
const {
Logger: {createActionStartEntry, createActionEndEntry, log},
} = require('metro-core');
import type {ModuleMap} from './DependencyGraph/ModuleResolution';
import type Package from './Package';
import type {HasteFS} from './types';
import type {ConfigT} from 'metro-config/src/configTypes.flow';
const JEST_HASTE_MAP_CACHE_BREAKER = 4;
class DependencyGraph extends EventEmitter {
_assetResolutionCache: AssetResolutionCache;
_config: ConfigT;
_haste: JestHasteMap;
_hasteFS: HasteFS;
_helpers: DependencyGraphHelpers;
_moduleCache: ModuleCache;
_moduleMap: ModuleMap;
_moduleResolver: ModuleResolver<Module, Package>;
constructor({
config,
haste,
initialHasteFS,
initialModuleMap,
}: {|
+config: ConfigT,
+haste: JestHasteMap,
+initialHasteFS: HasteFS,
+initialModuleMap: ModuleMap,
|}) {
super();
this._config = config;
this._assetResolutionCache = new AssetResolutionCache({
assetExtensions: new Set(config.resolver.assetExts),
getDirFiles: dirPath => fs.readdirSync(dirPath),
platforms: new Set(config.resolver.platforms),
});
this._haste = haste;
this._hasteFS = initialHasteFS;
this._moduleMap = initialModuleMap;
this._helpers = new DependencyGraphHelpers({
assetExts: config.resolver.assetExts,
providesModuleNodeModules: config.resolver.providesModuleNodeModules,
});
this._haste.on('change', this._onHasteChange.bind(this));
this._moduleCache = this._createModuleCache();
this._createModuleResolver();
}
static _createHaste(config: ConfigT): JestHasteMap {
return new JestHasteMap({
computeDependencies: false,
computeSha1: true,
extensions: config.resolver.sourceExts.concat(config.resolver.assetExts),
forceNodeFilesystemAPI: !config.resolver.useWatchman,
hasteImplModulePath: config.resolver.hasteImplModulePath,
ignorePattern: config.resolver.blacklistRE || / ^/,
mapper: config.resolver.virtualMapper,
maxWorkers: config.maxWorkers,
mocksPattern: '',
name: 'metro-' + JEST_HASTE_MAP_CACHE_BREAKER,
platforms: config.resolver.platforms,
providesModuleNodeModules: config.resolver.providesModuleNodeModules,
retainAllFiles: true,
resetCache: config.resetCache,
rootDir: config.projectRoot,
roots: config.watchFolders,
throwOnModuleCollision: true,
useWatchman: config.resolver.useWatchman,
watch: true,
});
}
static async load(config: ConfigT): Promise<DependencyGraph> {
const initializingMetroLogEntry = log(
createActionStartEntry('Initializing Metro'),
);
config.reporter.update({type: 'dep_graph_loading'});
const haste = DependencyGraph._createHaste(config);
const {hasteFS, moduleMap} = await haste.build();
log(createActionEndEntry(initializingMetroLogEntry));
config.reporter.update({type: 'dep_graph_loaded'});
return new DependencyGraph({
haste,
initialHasteFS: hasteFS,
initialModuleMap: moduleMap,
config,
});
}
_getClosestPackage(filePath: string): ?string {
const parsedPath = path.parse(filePath);
const root = parsedPath.root;
let dir = parsedPath.dir;
do {
const candidate = path.join(dir, 'package.json');
if (this._hasteFS.exists(candidate)) {
return candidate;
}
dir = path.dirname(dir);
} while (dir !== '.' && dir !== root);
return null;
}
_onHasteChange({eventsQueue, hasteFS, moduleMap}) {
this._hasteFS = hasteFS;
this._assetResolutionCache.clear();
this._moduleMap = moduleMap;
eventsQueue.forEach(({type, filePath}) =>
this._moduleCache.processFileChange(type, filePath),
);
this._createModuleResolver();
this.emit('change');
}
_createModuleResolver() {
this._moduleResolver = new ModuleResolver({
allowPnp: this._config.resolver.allowPnp,
dirExists: filePath => {
try {
return fs.lstatSync(filePath).isDirectory();
} catch (e) {}
return false;
},
doesFileExist: this._doesFileExist,
extraNodeModules: this._config.resolver.extraNodeModules,
isAssetFile: filePath => this._helpers.isAssetFile(filePath),
mainFields: this._config.resolver.resolverMainFields,
moduleCache: this._moduleCache,
moduleMap: this._moduleMap,
preferNativePlatform: true,
resolveAsset: (dirPath, assetName, platform) =>
this._assetResolutionCache.resolve(dirPath, assetName, platform),
resolveRequest: this._config.resolver.resolveRequest,
sourceExts: this._config.resolver.sourceExts,
});
}
_createModuleCache() {
return new ModuleCache({
getClosestPackage: this._getClosestPackage.bind(this),
});
}
getSha1(filename: string): string {
// TODO If it looks like we're trying to get the sha1 from a file located
// within a Zip archive, then we instead compute the sha1 for what looks
// like the Zip archive itself.
const splitIndex = filename.indexOf('.zip/');
const containerName =
splitIndex !== -1 ? filename.slice(0, splitIndex + 4) : filename;
// TODO Calling realpath allows us to get a hash for a given path even when
// it's a symlink to a file, which prevents Metro from crashing in such a
// case. However, it doesn't allow Metro to track changes to the target file
// of the symlink. We should fix this by implementing a symlink map into
// Metro (or maybe by implementing those "extra transformation sources" we've
// been talking about for stuff like CSS or WASM).
const resolvedPath = fs.realpathSync(containerName);
const sha1 = this._hasteFS.getSha1(resolvedPath);
if (!sha1) {
throw new ReferenceError(
`SHA-1 for file ${filename} (${resolvedPath}) is not computed`,
);
}
return sha1;
}
getWatcher() {
return this._haste;
}
end() {
this._haste.end();
}
resolveDependency(from: string, to: string, platform: ?string): string {
const req = new ResolutionRequest({
moduleResolver: this._moduleResolver,
entryPath: from,
helpers: this._helpers,
platform: platform || null,
moduleCache: this._moduleCache,
});
return req.resolveDependency(this._moduleCache.getModule(from), to).path;
}
_doesFileExist = (filePath: string): boolean => {
return this._hasteFS.exists(filePath);
};
getHasteName(filePath: string): string {
const hasteName = this._hasteFS.getModuleName(filePath);
if (hasteName) {
return hasteName;
}
return path.relative(this._config.projectRoot, filePath);
}
}
module.exports = DependencyGraph;