Skip to content

Commit

Permalink
Encapsulate Haste map initialisation in MutableHasteMap
Browse files Browse the repository at this point in the history
Summary:
A bit of internal rearranging to move some Haste-specific logic out of the `FileMap` core class and encapsulate it within the `MutableHasteMap` implementation of `HasteMap`.

This provides a template for other plugins and sets up some further changes.

Changelog: Internal

Reviewed By: huntie

Differential Revision: D67763377

fbshipit-source-id: e7f0e422622ded5ee93d9e613b7d6b2d36e595ab
  • Loading branch information
robhogan authored and facebook-github-bot committed Jan 3, 2025
1 parent f9d330b commit b7b3cf4
Show file tree
Hide file tree
Showing 4 changed files with 128 additions and 29 deletions.
27 changes: 3 additions & 24 deletions packages/metro-file-map/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -146,9 +146,6 @@ export type {
const CACHE_BREAKER = '8';

const CHANGE_INTERVAL = 30;
// Periodically yield to the event loop to allow parallel I/O, etc.
// Based on 200k files taking up to 800ms => max 40ms between yields.
const YIELD_EVERY_NUM_HASTE_FILES = 10000;

const NODE_MODULES = path.sep + 'node_modules' + path.sep;
const PACKAGE_JSON = path.sep + 'package.json';
Expand Down Expand Up @@ -426,31 +423,13 @@ export default class FileMap extends EventEmitter {
this._startupPerfLogger?.point('constructHasteMap_start');
const hasteMap = new MutableHasteMap({
console: this._console,
enableHastePackages: this._options.enableHastePackages,
perfLogger: this._startupPerfLogger,
platforms: new Set(this._options.platforms),
rootDir: this._options.rootDir,
failValidationOnConflicts: this._options.throwOnModuleCollision,
});
let hasteFiles = 0;
for (const {
baseName,
canonicalPath,
metadata,
} of fileSystem.metadataIterator({
// Symlinks and node_modules are never Haste modules or packages.
includeNodeModules: false,
includeSymlinks: false,
})) {
if (metadata[H.ID]) {
hasteMap.setModule(metadata[H.ID], [
canonicalPath,
baseName === 'package.json' ? H.PACKAGE : H.MODULE,
]);
if (++hasteFiles % YIELD_EVERY_NUM_HASTE_FILES === 0) {
await new Promise(setImmediate);
}
}
}
this._startupPerfLogger?.annotate({int: {hasteFiles}});
await hasteMap.initialize(fileSystem);
this._startupPerfLogger?.point('constructHasteMap_end');
return hasteMap;
}
Expand Down
54 changes: 54 additions & 0 deletions packages/metro-file-map/src/lib/MutableHasteMap.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,14 @@ import type {
Console,
DuplicatesIndex,
DuplicatesSet,
FileMetaData,
HasteConflict,
HasteMap,
HasteMapItem,
HasteMapItemMetaData,
HTypeValue,
Path,
PerfLogger,
} from '../flow-types';

import H from '../constants';
Expand All @@ -32,31 +34,83 @@ import path from 'path';
const EMPTY_OBJ: $ReadOnly<{[string]: HasteMapItemMetaData}> = {};
const EMPTY_MAP: $ReadOnlyMap<string, DuplicatesSet> = new Map();

// Periodically yield to the event loop to allow parallel I/O, etc.
// Based on 200k files taking up to 800ms => max 40ms between yields.
const YIELD_EVERY_NUM_HASTE_FILES = 10000;

type HasteMapOptions = $ReadOnly<{
console?: ?Console,
enableHastePackages: boolean,
perfLogger: ?PerfLogger,
platforms: $ReadOnlySet<string>,
rootDir: Path,
failValidationOnConflicts: boolean,
}>;

/**
* Low-level, read-only access to the in-memory FileSystem.
*/
interface InitialState {
metadataIterator(
opts: $ReadOnly<{
includeNodeModules: boolean,
includeSymlinks: boolean,
}>,
): Iterable<{
baseName: string,
canonicalPath: string,
metadata: FileMetaData,
}>;
}

export default class MutableHasteMap implements HasteMap {
+#rootDir: Path;
+#map: Map<string, HasteMapItem> = new Map();
+#duplicates: DuplicatesIndex = new Map();

+#console: ?Console;
+#enableHastePackages: boolean;
+#perfLogger: ?PerfLogger;
+#pathUtils: RootPathUtils;
+#platforms: $ReadOnlySet<string>;
+#failValidationOnConflicts: boolean;

constructor(options: HasteMapOptions) {
this.#console = options.console ?? null;
this.#enableHastePackages = options.enableHastePackages;
this.#perfLogger = options.perfLogger;
this.#platforms = options.platforms;
this.#rootDir = options.rootDir;
this.#pathUtils = new RootPathUtils(options.rootDir);
this.#failValidationOnConflicts = options.failValidationOnConflicts;
}

async initialize(fileSystemState: InitialState): Promise<void> {
let hasteFiles = 0;
for (const {
baseName,
canonicalPath,
metadata,
} of fileSystemState.metadataIterator({
// Symlinks and node_modules are never Haste modules or packages.
includeNodeModules: false,
includeSymlinks: false,
})) {
if (metadata[H.ID]) {
this.setModule(metadata[H.ID], [
canonicalPath,
this.#enableHastePackages && baseName === 'package.json'
? H.PACKAGE
: H.MODULE,
]);
if (++hasteFiles % YIELD_EVERY_NUM_HASTE_FILES === 0) {
await new Promise(setImmediate);
}
}
}
this.#perfLogger?.annotate({int: {hasteFiles}});
}

getModule(
name: string,
platform?: ?string,
Expand Down
12 changes: 7 additions & 5 deletions packages/metro-file-map/src/lib/TreeFS.js
Original file line number Diff line number Diff line change
Expand Up @@ -945,10 +945,12 @@ export default class TreeFS implements MutableFileSystem {
return null;
}

*metadataIterator(opts: {
includeSymlinks: boolean,
includeNodeModules: boolean,
}): Iterable<{
*metadataIterator(
opts: $ReadOnly<{
includeSymlinks: boolean,
includeNodeModules: boolean,
}>,
): Iterable<{
baseName: string,
canonicalPath: string,
metadata: FileMetaData,
Expand All @@ -958,7 +960,7 @@ export default class TreeFS implements MutableFileSystem {

*_metadataIterator(
rootNode: DirectoryNode,
opts: {includeSymlinks: boolean, includeNodeModules: boolean},
opts: $ReadOnly<{includeSymlinks: boolean, includeNodeModules: boolean}>,
prefix: string = '',
): Iterable<{
baseName: string,
Expand Down
64 changes: 64 additions & 0 deletions packages/metro-file-map/src/lib/__tests__/MutableHasteMap-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/**
* 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
* @oncall react_native
*/

import type {FileMetaData} from '../../flow-types';
import type HasteMapType from '../MutableHasteMap';

let mockPathModule;
jest.mock('path', () => mockPathModule);

describe.each([['win32'], ['posix']])('MockMap on %s', platform => {
const p: string => string = filePath =>
platform === 'win32'
? filePath.replace(/\//g, '\\').replace(/^\\/, 'C:\\')
: filePath;

let HasteMap: Class<HasteMapType>;

const opts = {
enableHastePackages: false,
failValidationOnConflicts: false,
perfLogger: null,
platforms: new Set(['ios', 'android']),
rootDir: p('/root'),
};

beforeEach(() => {
jest.resetModules();
mockPathModule = jest.requireActual<{}>('path')[platform];
HasteMap = require('../MutableHasteMap').default;
jest.spyOn(console, 'warn').mockImplementation(() => {});
jest.clearAllMocks();
});

test('initialize', async () => {
const hasteMap = new HasteMap(opts);
const initialState = {
metadataIterator: jest.fn().mockReturnValue([
{
canonicalPath: p('project/Foo.js'),
baseName: 'Foo.js',
metadata: hasteMetadata('NameForFoo'),
},
]),
};
await hasteMap.initialize(initialState);
expect(initialState.metadataIterator).toHaveBeenCalledWith({
includeNodeModules: false,
includeSymlinks: false,
});
expect(hasteMap.getModule('NameForFoo')).toEqual(p('/root/project/Foo.js'));
});
});

function hasteMetadata(hasteName: string): FileMetaData {
return [hasteName, 0, 0, 0, '', '', 0];
}

0 comments on commit b7b3cf4

Please sign in to comment.