From 2482fa89561f3142fd49d916c1e1882841e0fe4e Mon Sep 17 00:00:00 2001 From: Jean Lauliac Date: Tue, 16 May 2017 09:04:48 -0700 Subject: [PATCH] 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 --- .../src/ModuleGraph/node-haste/node-haste.js | 9 +- .../src/node-haste/AssetResolutionCache.js | 134 ++++++++++++++++++ .../src/node-haste/DependencyGraph.js | 16 ++- .../DependencyGraph/ResolutionRequest.js | 36 ++--- .../src/node-haste/FilesByDirNameIndex.js | 14 +- 5 files changed, 164 insertions(+), 45 deletions(-) create mode 100644 packages/metro-bundler/src/node-haste/AssetResolutionCache.js diff --git a/packages/metro-bundler/src/ModuleGraph/node-haste/node-haste.js b/packages/metro-bundler/src/ModuleGraph/node-haste/node-haste.js index 26c008f3..39c0ee07 100644 --- a/packages/metro-bundler/src/ModuleGraph/node-haste/node-haste.js +++ b/packages/metro-bundler/src/ModuleGraph/node-haste/node-haste.js @@ -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, }); } diff --git a/packages/metro-bundler/src/node-haste/AssetResolutionCache.js b/packages/metro-bundler/src/node-haste/AssetResolutionCache.js new file mode 100644 index 00000000..e96a7c15 --- /dev/null +++ b/packages/metro-bundler/src/node-haste/AssetResolutionCache.js @@ -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, + /** + * This should return all the files of the specified directory. + */ + +getDirFiles: (dirPath: string) => $ReadOnlyArray, + /** + * 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, +|}; + +type AssetInfo = {platform: ?string, fileName: string}; +type InfoByAssetName = Map>; + +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; + _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 { + 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( + map: Map>, + key: TK, +): Array { + let el = map.get(key); + if (el != null) { + return el; + } + el = []; + map.set(key, el); + return el; +} + +module.exports = AssetResolutionCache; diff --git a/packages/metro-bundler/src/node-haste/DependencyGraph.js b/packages/metro-bundler/src/node-haste/DependencyGraph.js index b4f6a7fc..9eecbd8c 100644 --- a/packages/metro-bundler/src/node-haste/DependencyGraph.js +++ b/packages/metro-bundler/src/node-haste/DependencyGraph.js @@ -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)); } diff --git a/packages/metro-bundler/src/node-haste/DependencyGraph/ResolutionRequest.js b/packages/metro-bundler/src/node-haste/DependencyGraph/ResolutionRequest.js index 21ac7912..4a973778 100644 --- a/packages/metro-bundler/src/node-haste/DependencyGraph/ResolutionRequest.js +++ b/packages/metro-bundler/src/node-haste/DependencyGraph/ResolutionRequest.js @@ -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 = { getAssetModule(path: string): TModule, }; -type MatchFilesByDirAndPattern = ( - dirName: string, - pattern: RegExp, -) => Array; - type Options = {| +dirExists: DirExistsFn, +entryPath: string, +extraNodeModules: ?Object, +hasteFS: HasteFS, +helpers: DependencyGraphHelpers, - +matchFiles: MatchFilesByDirAndPattern, +moduleCache: ModuleishCache, +moduleMap: ModuleMap, +platform: ?string, +preferNativePlatform: boolean, + +resolveAsset: (dirPath: string, assetName: string) => $ReadOnlyArray, +sourceExts: Array, |}; @@ -113,8 +107,6 @@ function tryResolveSync(action: () => T, secondaryAction: () => T): T { } } -const EMPTY_SET = new Set(); - class ResolutionRequest { _doesFileExist = filePath => this._options.hasteFS.exists(filePath); _immediateResolutionCache: {[key: string]: TModule}; @@ -619,26 +611,18 @@ class ResolutionRequest { 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 | void { +function getArrayLowestItem(a: $ReadOnlyArray): string | void { if (a.length === 0) { return undefined; } diff --git a/packages/metro-bundler/src/node-haste/FilesByDirNameIndex.js b/packages/metro-bundler/src/node-haste/FilesByDirNameIndex.js index 10af60cd..440a8e09 100644 --- a/packages/metro-bundler/src/node-haste/FilesByDirNameIndex.js +++ b/packages/metro-bundler/src/node-haste/FilesByDirNameIndex.js @@ -40,18 +40,8 @@ class FilesByDirNameIndex { } } - match(dirName: string, pattern: RegExp): Array { - 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 { + return this._filesByDirName.get(dirPath) || []; } }