diff --git a/packager/src/ModuleGraph/node-haste/node-haste.js b/packager/src/ModuleGraph/node-haste/node-haste.js index 26c008f3c..39c0ee070 100644 --- a/packager/src/ModuleGraph/node-haste/node-haste.js +++ b/packager/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/packager/src/node-haste/AssetResolutionCache.js b/packager/src/node-haste/AssetResolutionCache.js new file mode 100644 index 000000000..e96a7c15b --- /dev/null +++ b/packager/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/packager/src/node-haste/DependencyGraph.js b/packager/src/node-haste/DependencyGraph.js index b4f6a7fc8..9eecbd8c7 100644 --- a/packager/src/node-haste/DependencyGraph.js +++ b/packager/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/packager/src/node-haste/DependencyGraph/ResolutionRequest.js b/packager/src/node-haste/DependencyGraph/ResolutionRequest.js index 21ac79123..4a973778d 100644 --- a/packager/src/node-haste/DependencyGraph/ResolutionRequest.js +++ b/packager/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/packager/src/node-haste/FilesByDirNameIndex.js b/packager/src/node-haste/FilesByDirNameIndex.js index 10af60cda..440a8e09f 100644 --- a/packager/src/node-haste/FilesByDirNameIndex.js +++ b/packager/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) || []; } }