From eeb211fdcfdcb9e7f8a51721bd0f48bc7d0d211f Mon Sep 17 00:00:00 2001 From: Rob Hogan Date: Mon, 27 Feb 2023 12:27:15 -0800 Subject: [PATCH] Include additional build parameters in file map cache key Summary: We inherited cache key components from `jest-haste-map` in https://github.com/facebook/metro/blob/13f06bd14da0d307eea1599f5d2f86c0a30f711e/packages/metro-file-map/src/index.js#L334-L355, and I did some reorganising in https://github.com/facebook/metro/commit/c7fc43661e26f02efa3ff5ab9fde10bf1096433a to attempt to separate "build parameters" - config that would change the built output - from other options. There are a few old options that were in build parameters but never part of the cache key - they should've been. This diff adds them. - `enableSymlinks` (determines whether crawlers include symlinks) - `retainAllFiles` (determines whether `node_modules` files are ignored) - `skipPackageJson` (determines whether `package.json` files are "processed" for Haste packages) Of these, only `enableSymlinks` is configurable via Metro config, through `resolver.unstable_enableSymlinks`. The other two are internal-only. Additionally: - Refactor `rootRelativeCacheKeys` so that Flow will alert us to any missed keys. - Add a test verifying that we get a different hash for any variation in config. Changelog: * **Fix:** Include `resolver.unstable_enableSymlinks` in file map cache key. Reviewed By: motiz88 Differential Revision: D43624928 fbshipit-source-id: bca285bda21796fea4a8664a6f71f92591412c4c --- .../__tests__/rootRelativeCacheKeys-test.js | 126 ++++++++++++++++++ .../src/lib/rootRelativeCacheKeys.js | 52 +++++--- 2 files changed, 160 insertions(+), 18 deletions(-) create mode 100644 packages/metro-file-map/src/lib/__tests__/rootRelativeCacheKeys-test.js diff --git a/packages/metro-file-map/src/lib/__tests__/rootRelativeCacheKeys-test.js b/packages/metro-file-map/src/lib/__tests__/rootRelativeCacheKeys-test.js new file mode 100644 index 0000000000..d217449616 --- /dev/null +++ b/packages/metro-file-map/src/lib/__tests__/rootRelativeCacheKeys-test.js @@ -0,0 +1,126 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow strict-local + * @format + */ + +import type {BuildParameters} from '../../flow-types'; + +import rootRelativeCacheKeys from '../rootRelativeCacheKeys'; + +const buildParameters: BuildParameters = { + computeDependencies: false, + computeSha1: false, + dependencyExtractor: null, + enableSymlinks: false, + extensions: ['a'], + forceNodeFilesystemAPI: false, + hasteImplModulePath: null, + ignorePattern: /a/, + mocksPattern: /a/, + platforms: ['a'], + retainAllFiles: false, + rootDir: '/root', + roots: ['a', 'b'], + skipPackageJson: false, + cacheBreaker: 'a', +}; + +jest.mock( + '/haste/1', + () => ({ + getCacheKey: () => 'haste/1', + }), + {virtual: true}, +); +jest.mock( + '/haste/2', + () => ({ + getCacheKey: () => 'haste/2', + }), + {virtual: true}, +); +jest.mock( + '/extractor/1', + () => ({ + getCacheKey: () => 'extractor/1', + }), + {virtual: true}, +); +jest.mock( + '/extractor/2', + () => ({ + getCacheKey: () => 'extractor/2', + }), + {virtual: true}, +); + +it('returns a distinct cache key for any change', () => { + const { + hasteImplModulePath: _, + dependencyExtractor: __, + rootDir: ___, + ...simpleParameters + } = buildParameters; + + const varyDefault = >( + key: T, + newVal: BuildParameters[T], + ): BuildParameters => { + // $FlowFixMe[invalid-computed-prop] Can't use a union for a computed prop + return {...buildParameters, [key]: newVal}; + }; + + const configs = Object.keys(simpleParameters).map(key => { + switch (key) { + // Boolean + case 'computeDependencies': + case 'computeSha1': + case 'enableSymlinks': + case 'forceNodeFilesystemAPI': + case 'retainAllFiles': + case 'skipPackageJson': + return varyDefault(key, !buildParameters[key]); + // Strings + case 'cacheBreaker': + return varyDefault(key, 'foo'); + // String arrays + case 'extensions': + case 'platforms': + case 'roots': + return varyDefault(key, ['foo']); + // Regexp + case 'mocksPattern': + case 'ignorePattern': + return varyDefault(key, /foo/); + default: + (key: empty); + throw new Error('Unrecognised key in build parameters: ' + key); + } + }); + configs.push(buildParameters); + configs.push({...buildParameters, dependencyExtractor: '/extractor/1'}); + configs.push({...buildParameters, dependencyExtractor: '/extractor/2'}); + configs.push({...buildParameters, hasteImplModulePath: '/haste/1'}); + configs.push({...buildParameters, hasteImplModulePath: '/haste/2'}); + + // Generate hashes for each config + const configHashes = configs.map( + config => rootRelativeCacheKeys(config).relativeConfigHash, + ); + + // We expect them all to have distinct hashes + const seen = new Map(); + for (const [i, configHash] of configHashes.entries()) { + const seenIndex = seen.get(configHash); + if (seenIndex != null) { + // Two configs have the same hash - let Jest print the differences + expect(configs[seenIndex]).toEqual(configs[i]); + } + seen.set(configHash, i); + } +}); diff --git a/packages/metro-file-map/src/lib/rootRelativeCacheKeys.js b/packages/metro-file-map/src/lib/rootRelativeCacheKeys.js index d986c5f144..5eda3bc36d 100644 --- a/packages/metro-file-map/src/lib/rootRelativeCacheKeys.js +++ b/packages/metro-file-map/src/lib/rootRelativeCacheKeys.js @@ -36,29 +36,45 @@ export default function rootRelativeCacheKeys( rootDirHash: string, relativeConfigHash: string, } { - const rootDirHash = createHash('md5') - .update(buildParameters.rootDir) - .digest('hex'); + const {rootDir, ...otherParameters} = buildParameters; + const rootDirHash = createHash('md5').update(rootDir).digest('hex'); + + const cacheComponents = Object.keys(otherParameters) + .sort() + .map(key => { + switch (key) { + case 'roots': + return buildParameters[key].map(root => + fastPath.relative(rootDir, root), + ); + case 'cacheBreaker': + case 'extensions': + case 'computeDependencies': + case 'computeSha1': + case 'enableSymlinks': + case 'forceNodeFilesystemAPI': + case 'platforms': + case 'retainAllFiles': + case 'skipPackageJson': + return buildParameters[key] ?? null; + case 'mocksPattern': + return buildParameters[key]?.toString() ?? null; + case 'ignorePattern': + return buildParameters[key].toString(); + case 'hasteImplModulePath': + case 'dependencyExtractor': + return moduleCacheKey(buildParameters[key]); + default: + (key: empty); + throw new Error('Unrecognised key in build parameters: ' + key); + } + }); // JSON.stringify is stable here because we only deal in (nested) arrays of // primitives. Use a different approach if this is expanded to include // objects/Sets/Maps, etc. - const serializedConfig = JSON.stringify([ - buildParameters.roots.map(root => - fastPath.relative(buildParameters.rootDir, root), - ), - buildParameters.extensions, - buildParameters.platforms, - buildParameters.computeSha1, - buildParameters.mocksPattern?.toString() ?? null, - buildParameters.ignorePattern.toString(), - moduleCacheKey(buildParameters.hasteImplModulePath), - moduleCacheKey(buildParameters.dependencyExtractor), - buildParameters.computeDependencies, - buildParameters.cacheBreaker, - ]); const relativeConfigHash = createHash('md5') - .update(serializedConfig) + .update(JSON.stringify(cacheComponents)) .digest('hex'); return {