Files
FrenoCorp/node_modules/metro-file-map/src/plugins/HastePlugin.js.flow
Michael Freno 7c684a42cc FRE-600: Fix code review blockers
- Consolidated duplicate UndoManagers to single instance
- Fixed connection promise to only resolve on 'connected' status
- Fixed WebSocketProvider import (WebsocketProvider)
- Added proper doc.destroy() cleanup
- Renamed isPresenceInitialized property to avoid conflict

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-25 00:08:01 -04:00

515 lines
15 KiB
Plaintext

/**
* 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 {
Console,
DuplicatesIndex,
DuplicatesSet,
FileMapPlugin,
FileMapPluginInitOptions,
FileMapPluginWorker,
HasteConflict,
HasteMap,
HasteMapItem,
HasteMapItemMetadata,
HTypeValue,
Path,
PerfLogger,
ReadonlyFileSystemChanges,
} from '../flow-types';
import H from '../constants';
import {RootPathUtils} from '../lib/RootPathUtils';
import {chainComparators, compareStrings} from '../lib/sorting';
import {DuplicateHasteCandidatesError} from './haste/DuplicateHasteCandidatesError';
import getPlatformExtension from './haste/getPlatformExtension';
import {HasteConflictsError} from './haste/HasteConflictsError';
import path from 'path';
const EMPTY_OBJ: Readonly<{[string]: HasteMapItemMetadata}> = {};
const EMPTY_MAP: ReadonlyMap<string, DuplicatesSet> = new Map();
const PACKAGE_JSON = /(?:[/\\]|^)package\.json$/;
// 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;
export type HasteMapOptions = Readonly<{
console?: ?Console,
enableHastePackages: boolean,
hasteImplModulePath: ?string,
perfLogger?: ?PerfLogger,
platforms: ReadonlySet<string>,
rootDir: Path,
failValidationOnConflicts: boolean,
}>;
export default class HastePlugin
implements HasteMap, FileMapPlugin<null, string | null>
{
+name: 'haste' = 'haste';
+#console: ?Console;
+#duplicates: DuplicatesIndex = new Map();
+#enableHastePackages: boolean;
+#failValidationOnConflicts: boolean;
#getModuleNameByPath: string => ?string;
+#hasteImplModulePath: ?string;
+#map: Map<string, HasteMapItem> = new Map();
+#pathUtils: RootPathUtils;
+#perfLogger: ?PerfLogger;
+#platforms: ReadonlySet<string>;
+#rootDir: Path;
constructor(options: HasteMapOptions) {
this.#console = options.console ?? global.console;
this.#enableHastePackages = options.enableHastePackages;
this.#hasteImplModulePath = options.hasteImplModulePath;
this.#perfLogger = options.perfLogger;
this.#platforms = options.platforms;
this.#rootDir = options.rootDir;
this.#pathUtils = new RootPathUtils(options.rootDir);
this.#failValidationOnConflicts = options.failValidationOnConflicts;
}
async initialize({
files,
}: FileMapPluginInitOptions<null, string | null>): Promise<void> {
this.#perfLogger?.point('constructHasteMap_start');
let hasteFiles = 0;
for (const {
baseName,
canonicalPath,
pluginData: hasteId,
} of files.fileIterator({
// Symlinks and node_modules are never Haste modules or packages.
includeNodeModules: false,
includeSymlinks: false,
})) {
if (hasteId == null) {
continue;
}
this.setModule(hasteId, [
canonicalPath,
this.#enableHastePackages && baseName === 'package.json'
? H.PACKAGE
: H.MODULE,
]);
if (++hasteFiles % YIELD_EVERY_NUM_HASTE_FILES === 0) {
await new Promise(setImmediate);
}
}
this.#getModuleNameByPath = mixedPath => {
const result = files.lookup(mixedPath);
return result.exists &&
result.type === 'f' &&
typeof result.pluginData === 'string'
? result.pluginData
: null;
};
this.#perfLogger?.point('constructHasteMap_end');
this.#perfLogger?.annotate({int: {hasteFiles}});
}
getSerializableSnapshot(): null {
// Haste is not serialised, but built from traversing the file metadata
// on each run. This turns out to have comparable performance to
// serialisation, at least when Haste is dense, and makes for a much
// smaller cache.
return null;
}
getModule(
name: string,
platform?: ?string,
supportsNativePlatform?: ?boolean,
type?: ?HTypeValue,
): ?Path {
const module = this.#getModuleMetadata(
name,
platform,
!!supportsNativePlatform,
);
/* $FlowFixMe[invalid-compare] Error discovered during Constant Condition
* roll out. See https://fburl.com/workplace/4oq3zi07. */
if (module && module[H.TYPE] === (type ?? H.MODULE)) {
const modulePath = module[H.PATH];
return modulePath && this.#pathUtils.normalToAbsolute(modulePath);
}
return null;
}
getModuleNameByPath(mixedPath: Path): ?string {
if (this.#getModuleNameByPath == null) {
throw new Error(
'HastePlugin has not been initialized before getModuleNameByPath',
);
}
return this.#getModuleNameByPath(mixedPath) ?? null;
}
getPackage(
name: string,
platform: ?string,
_supportsNativePlatform?: ?boolean,
): ?Path {
return this.getModule(name, platform, null, H.PACKAGE);
}
/**
* When looking up a module's data, we walk through each eligible platform for
* the query. For each platform, we want to check if there are known
* duplicates for that name+platform pair. The duplication logic normally
* removes elements from the `map` object, but we want to check upfront to be
* extra sure. If metadata exists both in the `duplicates` object and the
* `map`, this would be a bug.
*/
#getModuleMetadata(
name: string,
platform: ?string,
supportsNativePlatform: boolean,
): HasteMapItemMetadata | null {
const map = this.#map.get(name) || EMPTY_OBJ;
const dupMap = this.#duplicates.get(name) || EMPTY_MAP;
if (platform != null) {
this.#assertNoDuplicates(
name,
platform,
supportsNativePlatform,
dupMap.get(platform),
);
if (map[platform] != null) {
return map[platform];
}
}
if (supportsNativePlatform) {
this.#assertNoDuplicates(
name,
H.NATIVE_PLATFORM,
supportsNativePlatform,
dupMap.get(H.NATIVE_PLATFORM),
);
if (map[H.NATIVE_PLATFORM]) {
return map[H.NATIVE_PLATFORM];
}
}
this.#assertNoDuplicates(
name,
H.GENERIC_PLATFORM,
supportsNativePlatform,
dupMap.get(H.GENERIC_PLATFORM),
);
if (map[H.GENERIC_PLATFORM]) {
return map[H.GENERIC_PLATFORM];
}
return null;
}
#assertNoDuplicates(
name: string,
platform: string,
supportsNativePlatform: boolean,
relativePathSet: ?DuplicatesSet,
): void {
if (relativePathSet == null) {
return;
}
const duplicates = new Map<string, number>();
for (const [relativePath, type] of relativePathSet) {
const duplicatePath = this.#pathUtils.normalToAbsolute(relativePath);
duplicates.set(duplicatePath, type);
}
throw new DuplicateHasteCandidatesError(
name,
platform,
supportsNativePlatform,
duplicates,
);
}
onChanged(delta: ReadonlyFileSystemChanges<?string>): void {
// Process removals first so that moves aren't treated as duplicates.
for (const [canonicalPath, maybeHasteId] of delta.removedFiles) {
this.#onRemovedFile(canonicalPath, maybeHasteId);
}
for (const [canonicalPath, maybeHasteId] of delta.addedFiles) {
this.#onNewFile(canonicalPath, maybeHasteId);
}
}
#onNewFile(canonicalPath: string, id: ?string) {
if (id == null) {
// Not a Haste module or package
return;
}
const module: HasteMapItemMetadata = [
canonicalPath,
this.#enableHastePackages &&
path.basename(canonicalPath) === 'package.json'
? H.PACKAGE
: H.MODULE,
];
this.setModule(id, module);
}
setModule(id: string, module: HasteMapItemMetadata) {
let hasteMapItem = this.#map.get(id);
if (!hasteMapItem) {
// $FlowFixMe[unclear-type] - Add type coverage
hasteMapItem = Object.create(null) as any;
this.#map.set(id, hasteMapItem);
}
const platform =
getPlatformExtension(module[H.PATH], this.#platforms) ||
H.GENERIC_PLATFORM;
const existingModule = hasteMapItem[platform];
if (existingModule && existingModule[H.PATH] !== module[H.PATH]) {
if (this.#console) {
this.#console.warn(
[
'metro-file-map: Haste module naming collision: ' + id,
' The following files share their name; please adjust your hasteImpl:',
' * <rootDir>' + path.sep + existingModule[H.PATH],
' * <rootDir>' + path.sep + module[H.PATH],
'',
].join('\n'),
);
}
// We do NOT want consumers to use a module that is ambiguous.
delete hasteMapItem[platform];
if (Object.keys(hasteMapItem).length === 0) {
this.#map.delete(id);
}
let dupsByPlatform = this.#duplicates.get(id);
if (dupsByPlatform == null) {
dupsByPlatform = new Map();
this.#duplicates.set(id, dupsByPlatform);
}
const dups = new Map([
[module[H.PATH], module[H.TYPE]],
[existingModule[H.PATH], existingModule[H.TYPE]],
]);
dupsByPlatform.set(platform, dups);
return;
}
const dupsByPlatform = this.#duplicates.get(id);
if (dupsByPlatform != null) {
const dups = dupsByPlatform.get(platform);
if (dups != null) {
dups.set(module[H.PATH], module[H.TYPE]);
}
return;
}
hasteMapItem[platform] = module;
}
#onRemovedFile(canonicalPath: string, moduleName: ?string) {
if (moduleName == null) {
// Not a Haste module or package
return;
}
const platform =
getPlatformExtension(canonicalPath, this.#platforms) ||
H.GENERIC_PLATFORM;
const hasteMapItem = this.#map.get(moduleName);
if (hasteMapItem != null) {
delete hasteMapItem[platform];
if (Object.keys(hasteMapItem).length === 0) {
this.#map.delete(moduleName);
} else {
this.#map.set(moduleName, hasteMapItem);
}
}
this.#recoverDuplicates(moduleName, canonicalPath);
}
assertValid(): void {
if (!this.#failValidationOnConflicts) {
return;
}
const conflicts = this.computeConflicts();
if (conflicts.length > 0) {
throw new HasteConflictsError(conflicts);
}
}
/**
* This function should be called when the file under `filePath` is removed
* or changed. When that happens, we want to figure out if that file was
* part of a group of files that had the same ID. If it was, we want to
* remove it from the group. Furthermore, if there is only one file
* remaining in the group, then we want to restore that single file as the
* correct resolution for its ID, and cleanup the duplicates index.
*/
#recoverDuplicates(moduleName: string, relativeFilePath: string) {
let dupsByPlatform = this.#duplicates.get(moduleName);
if (dupsByPlatform == null) {
return;
}
const platform =
getPlatformExtension(relativeFilePath, this.#platforms) ||
H.GENERIC_PLATFORM;
let dups = dupsByPlatform.get(platform);
if (dups == null) {
return;
}
dupsByPlatform = new Map(dupsByPlatform);
this.#duplicates.set(moduleName, dupsByPlatform);
dups = new Map(dups);
dupsByPlatform.set(platform, dups);
dups.delete(relativeFilePath);
if (dups.size !== 1) {
return;
}
const uniqueModule = dups.entries().next().value;
if (!uniqueModule) {
return;
}
let dedupMap: ?HasteMapItem = this.#map.get(moduleName);
if (dedupMap == null) {
dedupMap = Object.create(null) as HasteMapItem;
this.#map.set(moduleName, dedupMap);
}
dedupMap[platform] = uniqueModule;
dupsByPlatform.delete(platform);
if (dupsByPlatform.size === 0) {
this.#duplicates.delete(moduleName);
}
}
computeConflicts(): Array<HasteConflict> {
const conflicts: Array<HasteConflict> = [];
// Add literal duplicates tracked in the #duplicates map
for (const [id, dupsByPlatform] of this.#duplicates.entries()) {
for (const [platform, conflictingModules] of dupsByPlatform) {
conflicts.push({
absolutePaths: [...conflictingModules.keys()]
.map(modulePath => this.#pathUtils.normalToAbsolute(modulePath))
// Sort for ease of testing
.sort(),
id,
platform: platform === H.GENERIC_PLATFORM ? null : platform,
type: 'duplicate',
});
}
}
// Add cases of "shadowing at a distance": a module with a platform suffix and
// a module with a lower priority platform suffix (or no suffix), in different
// directories.
for (const [id, data] of this.#map) {
const conflictPaths = new Set<string>();
const basePaths = [];
for (const basePlatform of [H.NATIVE_PLATFORM, H.GENERIC_PLATFORM]) {
if (data[basePlatform] == null) {
continue;
}
const basePath = data[basePlatform][0];
basePaths.push(basePath);
const basePathDir = path.dirname(basePath);
// Find all platforms that can shadow basePlatform
// Given that X.(specific platform).js > x.native.js > X.js
// and basePlatform is either 'native' or generic (no platform).
for (const platform of Object.keys(data)) {
if (
platform === basePlatform ||
platform === H.GENERIC_PLATFORM /* lowest priority */
) {
continue;
}
const platformPath = data[platform][0];
if (path.dirname(platformPath) !== basePathDir) {
conflictPaths.add(platformPath);
}
}
}
if (conflictPaths.size) {
conflicts.push({
absolutePaths: [...new Set([...conflictPaths, ...basePaths])]
.map(modulePath => this.#pathUtils.normalToAbsolute(modulePath))
// Sort for ease of testing
.sort(),
id,
platform: null,
type: 'shadowing',
});
}
}
// Sort for ease of testing
conflicts.sort(
chainComparators(
(a, b) => compareStrings(a.type, b.type),
(a, b) => compareStrings(a.id, b.id),
(a, b) => compareStrings(a.platform, b.platform),
),
);
return conflicts;
}
getCacheKey(): string {
return JSON.stringify([
this.#enableHastePackages,
this.#hasteImplModulePath != null
? // $FlowFixMe[unsupported-syntax] - dynamic require
require(this.#hasteImplModulePath).getCacheKey()
: null,
[...this.#platforms].sort(),
]);
}
getWorker(): FileMapPluginWorker {
return {
worker: {
modulePath: require.resolve('./haste/worker.js'),
setupArgs: {
hasteImplModulePath: this.#hasteImplModulePath ?? null,
},
},
filter: ({isNodeModules, normalPath}) => {
if (isNodeModules) {
return false;
}
if (PACKAGE_JSON.test(normalPath)) {
return this.#enableHastePackages;
}
return this.#hasteImplModulePath != null;
},
};
}
}