Skip to content

Commit

Permalink
metro-file-map: Extract mocks handling into nullable MockMap (#1402)
Browse files Browse the repository at this point in the history
Summary:
Extract almost all mock handling logic out of `FileMap` and into `MockMap`.

Mocks are obviously a Jest concept inherited from `jest-haste-map`, but we're keeping them around in `metro-file-map` for planned re-use of `metro-file-map` as a custom `hasteMapModulePath` in Jest, at least at Meta.

This is a step towards making `MockMap` into a *plugin* or subscriber to `FileMap`, so that we don't need to keep it in `metro-file-map`'s codebase and can inject it where needed.

Changelog: Internal

(No Metro-user-facing change, `metro-file-map`'s API is [explicitly experimental](https://github.com/facebook/metro/tree/main/packages/metro-file-map#experimental-metro-file-map))


Test Plan:
- New unit tests
 - `mocksPattern` is always `null` in Metro, so the new code isn't reached right now, except by tests.
 - We'll exercise this code next half when we use it in a Jest context.

Differential Revision: D67056483

Pulled By: robhogan
  • Loading branch information
robhogan authored and facebook-github-bot committed Dec 11, 2024
1 parent d1343fc commit 664c5d6
Show file tree
Hide file tree
Showing 4 changed files with 127 additions and 63 deletions.
30 changes: 30 additions & 0 deletions packages/metro-file-map/src/__tests__/index-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -742,6 +742,36 @@ describe('FileMap', () => {
expect(fs.readFileSync.mock.calls.length).toBe(5);
});

test('builds a mock map if mocksPattern is non-null', async () => {
const pathToMock = path.join(
'/',
'project',
'fruits1',
'__mocks__',
'Blueberry.js',
);
mockFs[pathToMock] = '/* empty */';

const {mockMap} = await new FileMap({
mocksPattern: '__mocks__',
throwOnModuleCollision: true,
...defaultConfig,
}).build();

expect(mockMap).not.toBeNull();
expect(mockMap.getMockModule('Blueberry')).toEqual(pathToMock);
});

test('returns null mockMap if mocksPattern is empty', async () => {
const {mockMap} = await new FileMap({
mocksPattern: '',
throwOnModuleCollision: true,
...defaultConfig,
}).build();

expect(mockMap).toBeNull();
});

test('warns on duplicate mock files', async () => {
expect.assertions(1);

Expand Down
2 changes: 1 addition & 1 deletion packages/metro-file-map/src/flow-types.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ export type BuildParameters = $ReadOnly<{
export type BuildResult = {
fileSystem: FileSystem,
hasteMap: HasteMap,
mockMap: MockMap,
mockMap: ?MockMap,
};

export type CacheData = $ReadOnly<{
Expand Down
87 changes: 26 additions & 61 deletions packages/metro-file-map/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,18 +31,14 @@ import type {
Path,
PerfLogger,
PerfLoggerFactory,
RawMockMap,
ReadOnlyRawMockMap,
WatchmanClocks,
WorkerMetadata,
} from './flow-types';
import type {IJestWorker} from 'jest-worker';

import {DiskCacheManager} from './cache/DiskCacheManager';
import H from './constants';
import getMockName from './getMockName';
import checkWatchmanCapabilities from './lib/checkWatchmanCapabilities';
import {DuplicateError} from './lib/DuplicateError';
import MockMapImpl from './lib/MockMap';
import MutableHasteMap from './lib/MutableHasteMap';
import normalizePathSeparatorsToSystem from './lib/normalizePathSeparatorsToSystem';
Expand Down Expand Up @@ -367,7 +363,6 @@ export default class FileMap extends EventEmitter {
})
: new TreeFS({rootDir});
this._startupPerfLogger?.point('constructFileSystem_end');
const mocks = initialData?.mocks ?? new Map();

// Construct the Haste map from the cached file system state while
// crawling to build a diff of current state vs cached. `fileSystem`
Expand All @@ -380,13 +375,24 @@ export default class FileMap extends EventEmitter {
this._constructHasteMap(fileSystem),
]);

const mockMap =
this._options.mocksPattern != null
? new MockMapImpl({
console: this._console,
mocksPattern: this._options.mocksPattern,
rawMockMap: initialData?.mocks ?? new Map(),
rootDir,
throwOnModuleCollision: this._options.throwOnModuleCollision,
})
: null;

// Update `fileSystem`, `hasteMap` and `mocks` based on the file delta.
await this._applyFileDelta(fileSystem, hasteMap, mocks, fileDelta);
await this._applyFileDelta(fileSystem, hasteMap, mockMap, fileDelta);

await this._takeSnapshotAndPersist(
fileSystem,
fileDelta.clocks ?? new Map(),
mocks,
mockMap,
fileDelta.changedFiles,
fileDelta.removedFiles,
);
Expand All @@ -396,11 +402,11 @@ export default class FileMap extends EventEmitter {
fileDelta.removedFiles.size,
);

await this._watch(fileSystem, hasteMap, mocks);
await this._watch(fileSystem, hasteMap, mockMap);
return {
fileSystem,
hasteMap,
mockMap: new MockMapImpl({rootDir, rawMockMap: mocks}),
mockMap,
};
})();
}
Expand Down Expand Up @@ -521,7 +527,7 @@ export default class FileMap extends EventEmitter {
*/
_processFile(
hasteMap: MutableHasteMap,
mockMap: RawMockMap,
mockMap: ?MockMapImpl,
filePath: Path,
fileMetadata: FileMetaData,
workerOptions?: {forceInBand?: ?boolean, perfLogger?: ?PerfLogger},
Expand All @@ -542,8 +548,6 @@ export default class FileMap extends EventEmitter {

const rootDir = this._options.rootDir;

const relativeFilePath = this._pathUtils.absoluteToNormal(filePath);

const computeSha1 =
this._options.computeSha1 && fileMetadata[H.SHA1] == null;

Expand Down Expand Up @@ -607,38 +611,7 @@ export default class FileMap extends EventEmitter {
return null;
}

if (
this._options.mocksPattern &&
this._options.mocksPattern.test(filePath)
) {
const mockPath = getMockName(filePath);
const existingMockPath = mockMap.get(mockPath);

if (existingMockPath != null) {
const secondMockPath = this._pathUtils.absoluteToNormal(filePath);
if (existingMockPath !== secondMockPath) {
const method = this._options.throwOnModuleCollision
? 'error'
: 'warn';

this._console[method](
[
'metro-file-map: duplicate manual mock found: ' + mockPath,
' The following files share their name; please delete one of them:',
' * <rootDir>' + path.sep + existingMockPath,
' * <rootDir>' + path.sep + secondMockPath,
'',
].join('\n'),
);

if (this._options.throwOnModuleCollision) {
throw new DuplicateError(existingMockPath, secondMockPath);
}
}
}

mockMap.set(mockPath, relativeFilePath);
}
mockMap?.onNewOrModifiedFile(filePath);

return this._getWorker(workerOptions)
.worker({
Expand All @@ -656,7 +629,7 @@ export default class FileMap extends EventEmitter {
async _applyFileDelta(
fileSystem: MutableFileSystem,
hasteMap: MutableHasteMap,
mockMap: RawMockMap,
mockMap: ?MockMapImpl,
delta: $ReadOnly<{
changedFiles: FileData,
removedFiles: $ReadOnlySet<CanonicalPath>,
Expand Down Expand Up @@ -758,7 +731,7 @@ export default class FileMap extends EventEmitter {
async _takeSnapshotAndPersist(
fileSystem: FileSystem,
clocks: WatchmanClocks,
mockMap: ReadOnlyRawMockMap,
mockMap: ?MockMapImpl,
changed: FileData,
removed: Set<CanonicalPath>,
) {
Expand All @@ -767,7 +740,7 @@ export default class FileMap extends EventEmitter {
{
fileSystemData: fileSystem.getSerializableSnapshot(),
clocks: new Map(clocks),
mocks: new Map(mockMap),
mocks: mockMap ? mockMap.getSerializableSnapshot() : new Map(),
},
{changed, removed},
);
Expand Down Expand Up @@ -809,7 +782,7 @@ export default class FileMap extends EventEmitter {
_removeIfExists(
fileSystem: MutableFileSystem,
hasteMap: MutableHasteMap,
mockMap: RawMockMap,
mockMap: ?MockMapImpl,
relativeFilePath: Path,
) {
const fileMetadata = fileSystem.remove(relativeFilePath);
Expand All @@ -823,18 +796,10 @@ export default class FileMap extends EventEmitter {

hasteMap.removeModule(moduleName, relativeFilePath);

if (this._options.mocksPattern) {
const absoluteFilePath = path.join(
this._options.rootDir,
normalizePathSeparatorsToSystem(relativeFilePath),
if (mockMap) {
mockMap?.onRemovedFile(
this._pathUtils.normalToAbsolute(relativeFilePath),
);
if (
this._options.mocksPattern &&
this._options.mocksPattern.test(absoluteFilePath)
) {
const mockName = getMockName(absoluteFilePath);
mockMap.delete(mockName);
}
}
}

Expand All @@ -844,7 +809,7 @@ export default class FileMap extends EventEmitter {
async _watch(
fileSystem: MutableFileSystem,
hasteMap: MutableHasteMap,
mockMap: RawMockMap,
mockMap: ?MockMapImpl,
): Promise<void> {
this._startupPerfLogger?.point('watch_start');
if (!this._options.watch) {
Expand All @@ -854,8 +819,8 @@ export default class FileMap extends EventEmitter {

// In watch mode, we'll only warn about module collisions and we'll retain
// all files, even changes to node_modules.
this._options.throwOnModuleCollision = false;
hasteMap.setThrowOnModuleCollision(false);
mockMap?.setThrowOnModuleCollision(false);
this._options.retainAllFiles = true;

const hasWatchedExtension = (filePath: string) =>
Expand Down
71 changes: 70 additions & 1 deletion packages/metro-file-map/src/lib/MockMap.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,21 +11,90 @@

import type {MockMap as IMockMap, Path, RawMockMap} from '../flow-types';

import getMockName from '../getMockName';
import {DuplicateError} from './DuplicateError';
import {RootPathUtils} from './RootPathUtils';
import path from 'path';

export default class MockMap implements IMockMap {
+#mocksPattern: RegExp;
+#raw: RawMockMap;
+#rootDir: Path;
+#pathUtils: RootPathUtils;
+#console: typeof console;
#throwOnModuleCollision: boolean;

constructor({rawMockMap, rootDir}: {rawMockMap: RawMockMap, rootDir: Path}) {
constructor({
console,
mocksPattern,
rawMockMap,
rootDir,
throwOnModuleCollision,
}: {
console: typeof console,
mocksPattern: RegExp,
rawMockMap: RawMockMap,
rootDir: Path,
throwOnModuleCollision: boolean,
}) {
this.#mocksPattern = mocksPattern;
this.#raw = rawMockMap;
this.#rootDir = rootDir;
this.#console = console;
this.#pathUtils = new RootPathUtils(rootDir);
this.#throwOnModuleCollision = throwOnModuleCollision;
}

getMockModule(name: string): ?Path {
const mockPath = this.#raw.get(name) || this.#raw.get(name + '/index');
return mockPath != null ? this.#pathUtils.normalToAbsolute(mockPath) : null;
}

onNewOrModifiedFile(absoluteFilePath: Path): void {
if (!this.#mocksPattern.test(absoluteFilePath)) {
return;
}

const mockName = getMockName(absoluteFilePath);
const existingMockPath = this.#raw.get(mockName);
const newMockPath = this.#pathUtils.absoluteToNormal(absoluteFilePath);

if (existingMockPath != null) {
if (existingMockPath !== newMockPath) {
const method = this.#throwOnModuleCollision ? 'error' : 'warn';

this.#console[method](
[
'metro-file-map: duplicate manual mock found: ' + mockName,
' The following files share their name; please delete one of them:',
' * <rootDir>' + path.sep + existingMockPath,
' * <rootDir>' + path.sep + newMockPath,
'',
].join('\n'),
);

if (this.#throwOnModuleCollision) {
throw new DuplicateError(existingMockPath, newMockPath);
}
}
}

this.#raw.set(mockName, newMockPath);
}

onRemovedFile(absoluteFilePath: Path): void {
if (!this.#mocksPattern.test(absoluteFilePath)) {
return;
}
const mockName = getMockName(absoluteFilePath);
this.#raw.delete(mockName);
}

setThrowOnModuleCollision(throwOnModuleCollision: boolean): void {
this.#throwOnModuleCollision = throwOnModuleCollision;
}

getSerializableSnapshot(): RawMockMap {
return new Map(this.#raw);
}
}

0 comments on commit 664c5d6

Please sign in to comment.