packager: ResolutionRequest: unify asset resolution

Summary:
The existing resolution logic of assets:

* goes over all the files of the asset's directory for every resolution request;
* duplicates the parsing logic of `AssetPaths` by building up a custom regex for each resolution request.

This changeset proposes to tweak this by building an index for each particular directory in which we're looking for assets, so that we don't have to crawl a single directory twice, and so that it reuses the logic of `AssetPaths.tryParse()` for determining variants.

Reviewed By: davidaurelio

Differential Revision: D5062435

fbshipit-source-id: 708fc5612f57b14565499fad741701269438c806
This commit is contained in:
Jean Lauliac 2017-05-16 09:04:48 -07:00 committed by Facebook Github Bot
parent cb6bace9c6
commit 2482fa8956
5 changed files with 164 additions and 45 deletions

View File

@ -21,6 +21,7 @@ import type {
TransformedCodeFile, TransformedCodeFile,
} from '../types.flow'; } from '../types.flow';
const AssetResolutionCache = require('../../node-haste/AssetResolutionCache');
const DependencyGraphHelpers = require('../../node-haste/DependencyGraph/DependencyGraphHelpers'); const DependencyGraphHelpers = require('../../node-haste/DependencyGraph/DependencyGraphHelpers');
const FilesByDirNameIndex = require('../../node-haste/FilesByDirNameIndex'); const FilesByDirNameIndex = require('../../node-haste/FilesByDirNameIndex');
const HasteFS = require('./HasteFS'); const HasteFS = require('./HasteFS');
@ -110,6 +111,11 @@ exports.createResolveFn = function(options: ResolveOptions): ResolveFn {
const hasteMapBuilt = hasteMap.build(); const hasteMapBuilt = hasteMap.build();
const resolutionRequests = {}; const resolutionRequests = {};
const filesByDirNameIndex = new FilesByDirNameIndex(hasteMap.getAllFiles()); const filesByDirNameIndex = new FilesByDirNameIndex(hasteMap.getAllFiles());
const assetResolutionCache = new AssetResolutionCache({
assetExtensions: new Set(assetExts),
getDirFiles: dirPath => filesByDirNameIndex.getAllFiles(dirPath),
platforms,
});
return (id, source, platform, _, callback) => { return (id, source, platform, _, callback) => {
let resolutionRequest = resolutionRequests[platform]; let resolutionRequest = resolutionRequests[platform];
if (!resolutionRequest) { if (!resolutionRequest) {
@ -119,11 +125,12 @@ exports.createResolveFn = function(options: ResolveOptions): ResolveFn {
extraNodeModules, extraNodeModules,
hasteFS, hasteFS,
helpers, helpers,
matchFiles: filesByDirNameIndex.match.bind(filesByDirNameIndex),
moduleCache, moduleCache,
moduleMap: getFakeModuleMap(hasteMap), moduleMap: getFakeModuleMap(hasteMap),
platform, platform,
preferNativePlatform: true, preferNativePlatform: true,
resolveAsset: (dirPath, assetName) =>
assetResolutionCache.resolve(dirPath, assetName, platform),
sourceExts, sourceExts,
}); });
} }

View File

@ -0,0 +1,134 @@
/**
* 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 AssetPaths = require('./lib/AssetPaths');
const MapWithDefaults = require('./lib/MapWithDefaults');
import type {AssetData} from './lib/AssetPaths';
type Options = {|
/**
* Files that don't match these extensions are discarded. Assets always need
* an extension.
*/
+assetExtensions: Set<string>,
/**
* This should return all the files of the specified directory.
*/
+getDirFiles: (dirPath: string) => $ReadOnlyArray<string>,
/**
* All the valid platforms so as to support platform extensions, ex.
* `foo.ios.png`. A platform that's no in this set will be considered part of
* the asset name. Ex. `foo.smth.png`, if `smth` is not a valid platform, will
* be resolved by its full name `foo.smth.png`.
*/
+platforms: Set<string>,
|};
type AssetInfo = {platform: ?string, fileName: string};
type InfoByAssetName = Map<string, Array<AssetInfo>>;
const EMPTY_ARRAY = [];
/**
* Lazily build an index of assets for the directories in which we're looking
* for specific assets. For example if we're looking for `foo.png` in a `bar`
* directory, we'll look at all the files there and identify all the assets
* related to `foo.png`, for example `foo@2x.png` and `foo.ios.png`.
*/
class AssetResolutionCache {
_assetsByDirPath: MapWithDefaults<string, InfoByAssetName>;
_opts: Options;
constructor(options: Options) {
this._assetsByDirPath = new MapWithDefaults(this._findAssets);
this._opts = options;
}
/**
* The cache needs to be emptied if any file changes. This could be made more
* selective if performance demands it: for example, we could clear
* exclusively the directories in which files have changed. But that'd be
* more error-prone.
*/
clear() {
this._assetsByDirPath.clear();
}
/**
* Get the file paths of all the variants (resolutions, platforms, etc.) of a
* particular asset name, only looking at a specific directory. If needed this
* function could be changed to return pre-parsed information about the assets
* such as the resolution.
*/
resolve(
dirPath: string,
assetName: string,
platform: ?string,
): $ReadOnlyArray<string> {
const results = this._assetsByDirPath.get(dirPath);
const assets = results.get(assetName);
if (assets == null) {
return EMPTY_ARRAY;
}
return assets
.filter(asset => asset.platform == null || asset.platform === platform)
.map(asset => asset.fileName);
}
/**
* Build an index of assets for a particular directory. Several file can
* fulfill a single asset name, for example the different resolutions or
* platforms: ex. `foo.png` could contain `foo@2x.png`, `foo.ios.js`, etc.
*/
_findAssets = (dirPath: string) => {
const results = new Map();
const fileNames = this._opts.getDirFiles(dirPath);
for (let i = 0; i < fileNames.length; ++i) {
const fileName = fileNames[i];
const assetData = AssetPaths.tryParse(fileName, this._opts.platforms);
if (assetData == null || !this._isValidAsset(assetData)) {
continue;
}
getWithDefaultArray(results, assetData.assetName).push({
plaform: assetData.platform,
fileName,
});
}
return results;
};
_isValidAsset(assetData: AssetData): boolean {
return this._opts.assetExtensions.has(assetData.type);
}
}
/**
* Used instead of `MapWithDefaults` so that we don't create empty arrays
* anymore once the index is built.
*/
function getWithDefaultArray<TK, TV>(
map: Map<TK, Array<TV>>,
key: TK,
): Array<TV> {
let el = map.get(key);
if (el != null) {
return el;
}
el = [];
map.set(key, el);
return el;
}
module.exports = AssetResolutionCache;

View File

@ -11,6 +11,7 @@
'use strict'; 'use strict';
const AssetResolutionCache = require('./AssetResolutionCache');
const DependencyGraphHelpers = require('./DependencyGraph/DependencyGraphHelpers'); const DependencyGraphHelpers = require('./DependencyGraph/DependencyGraphHelpers');
const FilesByDirNameIndex = require('./FilesByDirNameIndex'); const FilesByDirNameIndex = require('./FilesByDirNameIndex');
const JestHasteMap = require('jest-haste-map'); const JestHasteMap = require('jest-haste-map');
@ -70,6 +71,7 @@ const JEST_HASTE_MAP_CACHE_BREAKER = 1;
class DependencyGraph extends EventEmitter { class DependencyGraph extends EventEmitter {
_assetResolutionCache: AssetResolutionCache;
_opts: Options; _opts: Options;
_filesByDirNameIndex: FilesByDirNameIndex; _filesByDirNameIndex: FilesByDirNameIndex;
_haste: JestHasteMap; _haste: JestHasteMap;
@ -88,13 +90,17 @@ class DependencyGraph extends EventEmitter {
invariant(config.opts.maxWorkerCount >= 1, 'worker count must be greater or equal to 1'); invariant(config.opts.maxWorkerCount >= 1, 'worker count must be greater or equal to 1');
this._opts = config.opts; this._opts = config.opts;
this._filesByDirNameIndex = new FilesByDirNameIndex(config.initialHasteFS.getAllFiles()); this._filesByDirNameIndex = new FilesByDirNameIndex(config.initialHasteFS.getAllFiles());
this._assetResolutionCache = new AssetResolutionCache({
assetExtensions: new Set(config.opts.assetExts),
getDirFiles: dirPath => this._filesByDirNameIndex.getAllFiles(dirPath),
platforms: config.opts.platforms,
});
this._haste = config.haste; this._haste = config.haste;
this._hasteFS = config.initialHasteFS; this._hasteFS = config.initialHasteFS;
this._moduleMap = config.initialModuleMap; this._moduleMap = config.initialModuleMap;
this._helpers = new DependencyGraphHelpers(this._opts); this._helpers = new DependencyGraphHelpers(this._opts);
this._haste.on('change', this._onHasteChange.bind(this)); this._haste.on('change', this._onHasteChange.bind(this));
this._moduleCache = this._createModuleCache(); this._moduleCache = this._createModuleCache();
(this: any)._matchFilesByDirAndPattern = this._matchFilesByDirAndPattern.bind(this);
} }
static _createHaste(opts: Options): JestHasteMap { static _createHaste(opts: Options): JestHasteMap {
@ -148,6 +154,7 @@ class DependencyGraph extends EventEmitter {
_onHasteChange({eventsQueue, hasteFS, moduleMap}) { _onHasteChange({eventsQueue, hasteFS, moduleMap}) {
this._hasteFS = hasteFS; this._hasteFS = hasteFS;
this._filesByDirNameIndex = new FilesByDirNameIndex(hasteFS.getAllFiles()); this._filesByDirNameIndex = new FilesByDirNameIndex(hasteFS.getAllFiles());
this._assetResolutionCache.clear();
this._moduleMap = moduleMap; this._moduleMap = moduleMap;
eventsQueue.forEach(({type, filePath, stat}) => eventsQueue.forEach(({type, filePath, stat}) =>
this._moduleCache.processFileChange(type, filePath, stat) this._moduleCache.processFileChange(type, filePath, stat)
@ -225,11 +232,12 @@ class DependencyGraph extends EventEmitter {
extraNodeModules: this._opts.extraNodeModules, extraNodeModules: this._opts.extraNodeModules,
hasteFS: this._hasteFS, hasteFS: this._hasteFS,
helpers: this._helpers, helpers: this._helpers,
matchFiles: this._matchFilesByDirAndPattern,
moduleCache: this._moduleCache, moduleCache: this._moduleCache,
moduleMap: this._moduleMap, moduleMap: this._moduleMap,
platform, platform,
preferNativePlatform: this._opts.preferNativePlatform, preferNativePlatform: this._opts.preferNativePlatform,
resolveAsset: (dirPath, assetName) =>
this._assetResolutionCache.resolve(dirPath, assetName, platform),
sourceExts: this._opts.sourceExts, sourceExts: this._opts.sourceExts,
}); });
@ -243,10 +251,6 @@ class DependencyGraph extends EventEmitter {
}).then(() => response); }).then(() => response);
} }
_matchFilesByDirAndPattern(dirName: string, pattern: RegExp) {
return this._filesByDirNameIndex.match(dirName, pattern);
}
matchFilesByPattern(pattern: RegExp) { matchFilesByPattern(pattern: RegExp) {
return Promise.resolve(this._hasteFS.matchFiles(pattern)); return Promise.resolve(this._hasteFS.matchFiles(pattern));
} }

View File

@ -12,7 +12,6 @@
'use strict'; 'use strict';
const AssetPaths = require('../lib/AssetPaths');
const AsyncTaskGroup = require('../lib/AsyncTaskGroup'); const AsyncTaskGroup = require('../lib/AsyncTaskGroup');
const MapWithDefaults = require('../lib/MapWithDefaults'); const MapWithDefaults = require('../lib/MapWithDefaults');
@ -78,22 +77,17 @@ export type ModuleishCache<TModule, TPackage> = {
getAssetModule(path: string): TModule, getAssetModule(path: string): TModule,
}; };
type MatchFilesByDirAndPattern = (
dirName: string,
pattern: RegExp,
) => Array<string>;
type Options<TModule, TPackage> = {| type Options<TModule, TPackage> = {|
+dirExists: DirExistsFn, +dirExists: DirExistsFn,
+entryPath: string, +entryPath: string,
+extraNodeModules: ?Object, +extraNodeModules: ?Object,
+hasteFS: HasteFS, +hasteFS: HasteFS,
+helpers: DependencyGraphHelpers, +helpers: DependencyGraphHelpers,
+matchFiles: MatchFilesByDirAndPattern,
+moduleCache: ModuleishCache<TModule, TPackage>, +moduleCache: ModuleishCache<TModule, TPackage>,
+moduleMap: ModuleMap, +moduleMap: ModuleMap,
+platform: ?string, +platform: ?string,
+preferNativePlatform: boolean, +preferNativePlatform: boolean,
+resolveAsset: (dirPath: string, assetName: string) => $ReadOnlyArray<string>,
+sourceExts: Array<string>, +sourceExts: Array<string>,
|}; |};
@ -113,8 +107,6 @@ function tryResolveSync<T>(action: () => T, secondaryAction: () => T): T {
} }
} }
const EMPTY_SET = new Set();
class ResolutionRequest<TModule: Moduleish, TPackage: Packageish> { class ResolutionRequest<TModule: Moduleish, TPackage: Packageish> {
_doesFileExist = filePath => this._options.hasteFS.exists(filePath); _doesFileExist = filePath => this._options.hasteFS.exists(filePath);
_immediateResolutionCache: {[key: string]: TModule}; _immediateResolutionCache: {[key: string]: TModule};
@ -619,26 +611,18 @@ class ResolutionRequest<TModule: Moduleish, TPackage: Packageish> {
fromModule: TModule, fromModule: TModule,
toModule: string, toModule: string,
): TModule { ): TModule {
const {name, type} = AssetPaths.parse(potentialModulePath, EMPTY_SET); const dirPath = path.dirname(potentialModulePath);
const baseName = path.basename(potentialModulePath);
let pattern = '^' + name + '(@[\\d\\.]+x)?'; const assetNames = this._options.resolveAsset(dirPath, baseName);
if (this._options.platform != null) { const assetName = getArrayLowestItem(assetNames);
pattern += '(\\.' + this._options.platform + ')?'; if (assetName != null) {
} const assetPath = path.join(dirPath, assetName);
pattern += '\\.' + type + '$'; return this._options.moduleCache.getAssetModule(assetPath);
const dirname = path.dirname(potentialModulePath);
const assetFiles = this._options.matchFiles(dirname, new RegExp(pattern));
// We arbitrarly grab the lowest, because scale selection will happen
// somewhere else. Always the lowest so that it's stable between builds.
const assetFile = getArrayLowestItem(assetFiles);
if (assetFile) {
return this._options.moduleCache.getAssetModule(assetFile);
} }
throw new UnableToResolveError( throw new UnableToResolveError(
fromModule, fromModule,
toModule, toModule,
`Directory \`${dirname}' doesn't contain asset \`${name}'`, `Directory \`${dirPath}' doesn't contain asset \`${baseName}'`,
); );
} }
@ -804,7 +788,7 @@ function isRelativeImport(filePath) {
return /^[.][.]?(?:[/]|$)/.test(filePath); return /^[.][.]?(?:[/]|$)/.test(filePath);
} }
function getArrayLowestItem(a: Array<string>): string | void { function getArrayLowestItem(a: $ReadOnlyArray<string>): string | void {
if (a.length === 0) { if (a.length === 0) {
return undefined; return undefined;
} }

View File

@ -40,18 +40,8 @@ class FilesByDirNameIndex {
} }
} }
match(dirName: string, pattern: RegExp): Array<string> { getAllFiles(dirPath: string): $ReadOnlyArray<string> {
const results = []; return this._filesByDirName.get(dirPath) || [];
const dir = this._filesByDirName.get(dirName);
if (dir === undefined) {
return [];
}
for (let i = 0; i < dir.length; ++i) {
if (pattern.test(dir[i])) {
results.push(path.join(dirName, dir[i]));
}
}
return results;
} }
} }