Skip to content

Commit

Permalink
Include additional build parameters in file map cache key
Browse files Browse the repository at this point in the history
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 c7fc436 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
  • Loading branch information
robhogan authored and facebook-github-bot committed Feb 27, 2023
1 parent 2303c10 commit eeb211f
Show file tree
Hide file tree
Showing 2 changed files with 160 additions and 18 deletions.
@@ -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 = <T: $Keys<typeof simpleParameters>>(
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<string, number>();
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);
}
});
52 changes: 34 additions & 18 deletions packages/metro-file-map/src/lib/rootRelativeCacheKeys.js
Expand Up @@ -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 {
Expand Down

0 comments on commit eeb211f

Please sign in to comment.