mirror of https://github.com/status-im/metro.git
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:
parent
cb6bace9c6
commit
2482fa8956
|
@ -21,6 +21,7 @@ import type {
|
|||
TransformedCodeFile,
|
||||
} from '../types.flow';
|
||||
|
||||
const AssetResolutionCache = require('../../node-haste/AssetResolutionCache');
|
||||
const DependencyGraphHelpers = require('../../node-haste/DependencyGraph/DependencyGraphHelpers');
|
||||
const FilesByDirNameIndex = require('../../node-haste/FilesByDirNameIndex');
|
||||
const HasteFS = require('./HasteFS');
|
||||
|
@ -110,6 +111,11 @@ exports.createResolveFn = function(options: ResolveOptions): ResolveFn {
|
|||
const hasteMapBuilt = hasteMap.build();
|
||||
const resolutionRequests = {};
|
||||
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) => {
|
||||
let resolutionRequest = resolutionRequests[platform];
|
||||
if (!resolutionRequest) {
|
||||
|
@ -119,11 +125,12 @@ exports.createResolveFn = function(options: ResolveOptions): ResolveFn {
|
|||
extraNodeModules,
|
||||
hasteFS,
|
||||
helpers,
|
||||
matchFiles: filesByDirNameIndex.match.bind(filesByDirNameIndex),
|
||||
moduleCache,
|
||||
moduleMap: getFakeModuleMap(hasteMap),
|
||||
platform,
|
||||
preferNativePlatform: true,
|
||||
resolveAsset: (dirPath, assetName) =>
|
||||
assetResolutionCache.resolve(dirPath, assetName, platform),
|
||||
sourceExts,
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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;
|
|
@ -11,6 +11,7 @@
|
|||
|
||||
'use strict';
|
||||
|
||||
const AssetResolutionCache = require('./AssetResolutionCache');
|
||||
const DependencyGraphHelpers = require('./DependencyGraph/DependencyGraphHelpers');
|
||||
const FilesByDirNameIndex = require('./FilesByDirNameIndex');
|
||||
const JestHasteMap = require('jest-haste-map');
|
||||
|
@ -70,6 +71,7 @@ const JEST_HASTE_MAP_CACHE_BREAKER = 1;
|
|||
|
||||
class DependencyGraph extends EventEmitter {
|
||||
|
||||
_assetResolutionCache: AssetResolutionCache;
|
||||
_opts: Options;
|
||||
_filesByDirNameIndex: FilesByDirNameIndex;
|
||||
_haste: JestHasteMap;
|
||||
|
@ -88,13 +90,17 @@ class DependencyGraph extends EventEmitter {
|
|||
invariant(config.opts.maxWorkerCount >= 1, 'worker count must be greater or equal to 1');
|
||||
this._opts = config.opts;
|
||||
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._hasteFS = config.initialHasteFS;
|
||||
this._moduleMap = config.initialModuleMap;
|
||||
this._helpers = new DependencyGraphHelpers(this._opts);
|
||||
this._haste.on('change', this._onHasteChange.bind(this));
|
||||
this._moduleCache = this._createModuleCache();
|
||||
(this: any)._matchFilesByDirAndPattern = this._matchFilesByDirAndPattern.bind(this);
|
||||
}
|
||||
|
||||
static _createHaste(opts: Options): JestHasteMap {
|
||||
|
@ -148,6 +154,7 @@ class DependencyGraph extends EventEmitter {
|
|||
_onHasteChange({eventsQueue, hasteFS, moduleMap}) {
|
||||
this._hasteFS = hasteFS;
|
||||
this._filesByDirNameIndex = new FilesByDirNameIndex(hasteFS.getAllFiles());
|
||||
this._assetResolutionCache.clear();
|
||||
this._moduleMap = moduleMap;
|
||||
eventsQueue.forEach(({type, filePath, stat}) =>
|
||||
this._moduleCache.processFileChange(type, filePath, stat)
|
||||
|
@ -225,11 +232,12 @@ class DependencyGraph extends EventEmitter {
|
|||
extraNodeModules: this._opts.extraNodeModules,
|
||||
hasteFS: this._hasteFS,
|
||||
helpers: this._helpers,
|
||||
matchFiles: this._matchFilesByDirAndPattern,
|
||||
moduleCache: this._moduleCache,
|
||||
moduleMap: this._moduleMap,
|
||||
platform,
|
||||
preferNativePlatform: this._opts.preferNativePlatform,
|
||||
resolveAsset: (dirPath, assetName) =>
|
||||
this._assetResolutionCache.resolve(dirPath, assetName, platform),
|
||||
sourceExts: this._opts.sourceExts,
|
||||
});
|
||||
|
||||
|
@ -243,10 +251,6 @@ class DependencyGraph extends EventEmitter {
|
|||
}).then(() => response);
|
||||
}
|
||||
|
||||
_matchFilesByDirAndPattern(dirName: string, pattern: RegExp) {
|
||||
return this._filesByDirNameIndex.match(dirName, pattern);
|
||||
}
|
||||
|
||||
matchFilesByPattern(pattern: RegExp) {
|
||||
return Promise.resolve(this._hasteFS.matchFiles(pattern));
|
||||
}
|
||||
|
|
|
@ -12,7 +12,6 @@
|
|||
|
||||
'use strict';
|
||||
|
||||
const AssetPaths = require('../lib/AssetPaths');
|
||||
const AsyncTaskGroup = require('../lib/AsyncTaskGroup');
|
||||
const MapWithDefaults = require('../lib/MapWithDefaults');
|
||||
|
||||
|
@ -78,22 +77,17 @@ export type ModuleishCache<TModule, TPackage> = {
|
|||
getAssetModule(path: string): TModule,
|
||||
};
|
||||
|
||||
type MatchFilesByDirAndPattern = (
|
||||
dirName: string,
|
||||
pattern: RegExp,
|
||||
) => Array<string>;
|
||||
|
||||
type Options<TModule, TPackage> = {|
|
||||
+dirExists: DirExistsFn,
|
||||
+entryPath: string,
|
||||
+extraNodeModules: ?Object,
|
||||
+hasteFS: HasteFS,
|
||||
+helpers: DependencyGraphHelpers,
|
||||
+matchFiles: MatchFilesByDirAndPattern,
|
||||
+moduleCache: ModuleishCache<TModule, TPackage>,
|
||||
+moduleMap: ModuleMap,
|
||||
+platform: ?string,
|
||||
+preferNativePlatform: boolean,
|
||||
+resolveAsset: (dirPath: string, assetName: string) => $ReadOnlyArray<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> {
|
||||
_doesFileExist = filePath => this._options.hasteFS.exists(filePath);
|
||||
_immediateResolutionCache: {[key: string]: TModule};
|
||||
|
@ -619,26 +611,18 @@ class ResolutionRequest<TModule: Moduleish, TPackage: Packageish> {
|
|||
fromModule: TModule,
|
||||
toModule: string,
|
||||
): TModule {
|
||||
const {name, type} = AssetPaths.parse(potentialModulePath, EMPTY_SET);
|
||||
|
||||
let pattern = '^' + name + '(@[\\d\\.]+x)?';
|
||||
if (this._options.platform != null) {
|
||||
pattern += '(\\.' + this._options.platform + ')?';
|
||||
}
|
||||
pattern += '\\.' + type + '$';
|
||||
|
||||
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);
|
||||
const dirPath = path.dirname(potentialModulePath);
|
||||
const baseName = path.basename(potentialModulePath);
|
||||
const assetNames = this._options.resolveAsset(dirPath, baseName);
|
||||
const assetName = getArrayLowestItem(assetNames);
|
||||
if (assetName != null) {
|
||||
const assetPath = path.join(dirPath, assetName);
|
||||
return this._options.moduleCache.getAssetModule(assetPath);
|
||||
}
|
||||
throw new UnableToResolveError(
|
||||
fromModule,
|
||||
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);
|
||||
}
|
||||
|
||||
function getArrayLowestItem(a: Array<string>): string | void {
|
||||
function getArrayLowestItem(a: $ReadOnlyArray<string>): string | void {
|
||||
if (a.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
|
|
@ -40,18 +40,8 @@ class FilesByDirNameIndex {
|
|||
}
|
||||
}
|
||||
|
||||
match(dirName: string, pattern: RegExp): Array<string> {
|
||||
const results = [];
|
||||
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;
|
||||
getAllFiles(dirPath: string): $ReadOnlyArray<string> {
|
||||
return this._filesByDirName.get(dirPath) || [];
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue