diff --git a/packages/metro-file-map/src/Watcher.js b/packages/metro-file-map/src/Watcher.js index 0a6d81dff9..3afc117e6f 100644 --- a/packages/metro-file-map/src/Watcher.js +++ b/packages/metro-file-map/src/Watcher.js @@ -51,7 +51,7 @@ type WatcherOptions = { extensions: $ReadOnlyArray, forceNodeFilesystemAPI: boolean, healthCheckFilePrefix: string, - ignoreForCrawl: string => boolean, + ignoreForCrawl: (filePath: string) => boolean, ignorePatternForWatch: RegExp, previousState: CrawlerOptions['previousState'], perfLogger: ?PerfLogger, diff --git a/packages/metro-file-map/src/__tests__/index-test.js b/packages/metro-file-map/src/__tests__/index-test.js index 3056289340..d77cc66ff1 100644 --- a/packages/metro-file-map/src/__tests__/index-test.js +++ b/packages/metro-file-map/src/__tests__/index-test.js @@ -408,7 +408,7 @@ describe('FileMap', () => { }); test('exports constants', () => { - expect(FileMap.H).toBe(require('../constants')); + expect(FileMap.H).toBe(require('../constants').constants); }); test('ignores files given a pattern', async () => { diff --git a/packages/metro-file-map/src/__tests__/worker-test.js b/packages/metro-file-map/src/__tests__/worker-test.js index 5549e7d691..1d44f5ea14 100644 --- a/packages/metro-file-map/src/__tests__/worker-test.js +++ b/packages/metro-file-map/src/__tests__/worker-test.js @@ -315,6 +315,7 @@ describe('jest-worker interface', () => { beforeEach(() => { jest.resetModules(); + // Re-require the worker module to reset its internal state. workerModule = require('../worker'); }); diff --git a/packages/metro-file-map/src/cache/DiskCacheManager.js b/packages/metro-file-map/src/cache/DiskCacheManager.js index e15959a8a5..55305fab7f 100644 --- a/packages/metro-file-map/src/cache/DiskCacheManager.js +++ b/packages/metro-file-map/src/cache/DiskCacheManager.js @@ -51,9 +51,11 @@ export class DiskCacheManager implements CacheManager { #stopListening: ?() => void; constructor( - {buildParameters}: CacheManagerFactoryOptions, - {autoSave = {}, cacheDirectory, cacheFilePrefix}: DiskCacheConfig, + factoryOptions: CacheManagerFactoryOptions, + config: DiskCacheConfig, ) { + const {buildParameters} = factoryOptions; + const {autoSave = {}, cacheDirectory, cacheFilePrefix} = config; this.#cachePath = DiskCacheManager.getCacheFilePath( buildParameters, cacheFilePrefix, @@ -103,12 +105,9 @@ export class DiskCacheManager implements CacheManager { async write( getSnapshot: () => CacheData, - { - changedSinceCacheRead, - eventSource, - onWriteError, - }: CacheManagerWriteOptions, + writeOptions: CacheManagerWriteOptions, ): Promise { + const {changedSinceCacheRead, eventSource, onWriteError} = writeOptions; // Initialise a writer function using a promise queue to ensure writes are // sequenced. const tryWrite = (this.#tryWrite = () => { @@ -150,7 +149,7 @@ export class DiskCacheManager implements CacheManager { } } - async end() { + async end(): Promise { // Clear any timers if (this.#debounceTimeout) { clearTimeout(this.#debounceTimeout); diff --git a/packages/metro-file-map/src/constants.js b/packages/metro-file-map/src/constants.js index 78fd7a58a8..807444808c 100644 --- a/packages/metro-file-map/src/constants.js +++ b/packages/metro-file-map/src/constants.js @@ -18,14 +18,11 @@ * This constant key map allows to keep the map smaller without having to build * a custom serialization library. */ - -/*:: import type {HType} from './flow-types'; -*/ 'use strict'; -const constants/*: HType */ = { +export const constants: HType = { /* dependency serialization */ DEPENDENCY_DELIM: '\0', @@ -51,4 +48,4 @@ const constants/*: HType */ = { NATIVE_PLATFORM: 'native', }; -module.exports = constants; +export default constants; diff --git a/packages/metro-file-map/src/crawlers/watchman/index.js b/packages/metro-file-map/src/crawlers/watchman/index.js index 8e04181c0b..9ff8e8f9d0 100644 --- a/packages/metro-file-map/src/crawlers/watchman/index.js +++ b/packages/metro-file-map/src/crawlers/watchman/index.js @@ -46,22 +46,23 @@ function makeWatchmanError(error: Error): Error { return error; } -export default async function watchmanCrawl({ - abortSignal, - computeSha1, - extensions, - ignore, - includeSymlinks, - onStatus, - perfLogger, - previousState, - rootDir, - roots, -}: CrawlerOptions): Promise<{ +export default async function watchmanCrawl(options: CrawlerOptions): Promise<{ changedFiles: FileData, removedFiles: Set, clocks: WatchmanClocks, }> { + const { + abortSignal, + computeSha1, + extensions, + ignore, + includeSymlinks, + onStatus, + perfLogger, + previousState, + rootDir, + roots, + } = options; abortSignal?.throwIfAborted(); const client = new watchman.Client(); diff --git a/packages/metro-file-map/src/crawlers/watchman/planQuery.js b/packages/metro-file-map/src/crawlers/watchman/planQuery.js index 82f185cd19..2123939fb9 100644 --- a/packages/metro-file-map/src/crawlers/watchman/planQuery.js +++ b/packages/metro-file-map/src/crawlers/watchman/planQuery.js @@ -15,22 +15,20 @@ import type { WatchmanQuerySince, } from 'fb-watchman'; -export function planQuery({ - since, - directoryFilters, - extensions, - includeSha1, - includeSymlinks, -}: Readonly<{ +type PlanQueryArgs = Readonly<{ since: ?WatchmanQuerySince, - directoryFilters: $ReadOnlyArray, - extensions: $ReadOnlyArray, + directoryFilters: ReadonlyArray, + extensions: ReadonlyArray, includeSha1: boolean, includeSymlinks: boolean, -}>): { +}>; + +export function planQuery(args: PlanQueryArgs): { query: WatchmanQuery, queryGenerator: string, } { + const {since, directoryFilters, extensions, includeSha1, includeSymlinks} = + args; const fields = ['name', 'exists', 'mtime_ms', 'size']; if (includeSha1) { fields.push('content.sha1hex'); diff --git a/packages/metro-file-map/src/flow-types.js b/packages/metro-file-map/src/flow-types.js index b52fb34ff5..3c15d82ad4 100644 --- a/packages/metro-file-map/src/flow-types.js +++ b/packages/metro-file-map/src/flow-types.js @@ -85,7 +85,7 @@ export type CacheManagerFactoryOptions = Readonly<{ export type CacheManagerWriteOptions = Readonly<{ changedSinceCacheRead: boolean, eventSource: CacheManagerEventSource, - onWriteError: Error => void, + onWriteError: (error: Error) => void, }>; // A path that is @@ -427,7 +427,9 @@ export type HasteMapData = Map; export type HasteMapItem = { [platform: string]: HasteMapItemMetadata, + /*:: __proto__: null, + */ }; export type HasteMapItemMetadata = [/* path */ string, /* type */ number]; @@ -465,8 +467,8 @@ export type ReadOnlyRawMockMap = Readonly<{ export interface WatcherBackend { getPauseReason(): ?string; - onError((error: Error) => void): () => void; - onFileEvent((event: WatcherBackendChangeEvent) => void): () => void; + onError(listener: (error: Error) => void): () => void; + onFileEvent(listener: (event: WatcherBackendChangeEvent) => void): () => void; startWatching(): Promise; stopWatching(): Promise; } @@ -521,5 +523,5 @@ export type WorkerMetadata = Readonly<{ }>; export type WorkerSetupArgs = Readonly<{ - plugins?: $ReadOnlyArray, + plugins?: ReadonlyArray, }>; diff --git a/packages/metro-file-map/src/index.js b/packages/metro-file-map/src/index.js index eb75bc6545..320b4f53e8 100644 --- a/packages/metro-file-map/src/index.js +++ b/packages/metro-file-map/src/index.js @@ -238,7 +238,9 @@ export default class FileMap extends EventEmitter { _buildPromise: ?Promise; _canUseWatchmanPromise: Promise; _changeID: number; + /*:: _changeInterval: ?IntervalID; + */ _fileProcessor: FileProcessor; _console: Console; _options: InternalOptions; @@ -246,7 +248,9 @@ export default class FileMap extends EventEmitter { _watcher: ?Watcher; _cacheManager: CacheManager; _crawlerAbortController: AbortController; + /*:: _healthCheckInterval: ?IntervalID; + */ _startupPerfLogger: ?PerfLogger; #plugins: $ReadOnlyArray; diff --git a/packages/metro-file-map/src/lib/TreeFS.js b/packages/metro-file-map/src/lib/TreeFS.js index 2e7346a486..78e4c3b6c7 100644 --- a/packages/metro-file-map/src/lib/TreeFS.js +++ b/packages/metro-file-map/src/lib/TreeFS.js @@ -42,6 +42,49 @@ type NormalizedSymlinkTarget = { startOfBasenameIdx: number, }; +type DeserializedSnapshotInput = { + rootDir: string, + fileSystemData: DirectoryNode, + processFile: ProcessFileFunction, +}; + +type TreeFSOptions = { + rootDir: Path, + files?: FileData, + processFile: ProcessFileFunction, +}; + +type MatchFilesOptions = Readonly<{ + /* Filter relative paths against a pattern. */ + filter?: ?RegExp, + /* `filter` is applied against absolute paths, vs rootDir-relative. (default: false) */ + filterCompareAbsolute?: boolean, + /* `filter` is applied against posix-delimited paths, even on Windows. (default: false) */ + filterComparePosix?: boolean, + /* Follow symlinks when enumerating paths. (default: false) */ + follow?: boolean, + /* Should search for files recursively. (default: true) */ + recursive?: boolean, + /* Match files under a given root, or null for all files */ + rootDir?: ?Path, +}>; + +type MetadataIteratorOptions = Readonly<{ + includeSymlinks: boolean, + includeNodeModules: boolean, +}>; + +type PathIteratorOptions = Readonly<{ + alwaysYieldPosix: boolean, + canonicalPathOfRoot: string, + follow: boolean, + recursive: boolean, + subtreeOnly: boolean, +}>; + +type GetFileDataOptions = { + followLeaf: boolean, +}; /** * OVERVIEW: * @@ -99,15 +142,8 @@ export default class TreeFS implements MutableFileSystem { #pathUtils: RootPathUtils; #processFile: ProcessFileFunction; - constructor({ - rootDir, - files, - processFile, - }: { - rootDir: Path, - files?: FileData, - processFile: ProcessFileFunction, - }) { + constructor(opts: TreeFSOptions) { + const {rootDir, files, processFile} = opts; this.#rootDir = rootDir; this.#pathUtils = new RootPathUtils(rootDir); this.#processFile = processFile; @@ -120,15 +156,8 @@ export default class TreeFS implements MutableFileSystem { return this._cloneTree(this.#rootNode); } - static fromDeserializedSnapshot({ - rootDir, - fileSystemData, - processFile, - }: { - rootDir: string, - fileSystemData: DirectoryNode, - processFile: ProcessFileFunction, - }): TreeFS { + static fromDeserializedSnapshot(args: DeserializedSnapshotInput): TreeFS { + const {rootDir, fileSystemData, processFile} = args; const tfs = new TreeFS({processFile, rootDir}); tfs.#rootNode = fileSystemData; return tfs; @@ -302,27 +331,15 @@ export default class TreeFS implements MutableFileSystem { * The query matches against normalized paths which start with `./`, * for example: `a/b.js` -> `./a/b.js` */ - *matchFiles({ - filter = null, - filterCompareAbsolute = false, - filterComparePosix = false, - follow = false, - recursive = true, - rootDir = null, - }: Readonly<{ - /* Filter relative paths against a pattern. */ - filter?: ?RegExp, - /* `filter` is applied against absolute paths, vs rootDir-relative. (default: false) */ - filterCompareAbsolute?: boolean, - /* `filter` is applied against posix-delimited paths, even on Windows. (default: false) */ - filterComparePosix?: boolean, - /* Follow symlinks when enumerating paths. (default: false) */ - follow?: boolean, - /* Should search for files recursively. (default: true) */ - recursive?: boolean, - /* Match files under a given root, or null for all files */ - rootDir?: ?Path, - }>): Iterable { + *matchFiles(opts: MatchFilesOptions): Iterable { + const { + filter = null, + filterCompareAbsolute = false, + filterComparePosix = false, + follow = false, + recursive = true, + rootDir = null, + } = opts; const normalRoot = rootDir == null ? '' : this._normalizePath(rootDir); const contextRootResult = this._lookupByNormalPath(normalRoot); if (!contextRootResult.exists) { @@ -993,12 +1010,7 @@ export default class TreeFS implements MutableFileSystem { return null; } - *metadataIterator( - opts: Readonly<{ - includeSymlinks: boolean, - includeNodeModules: boolean, - }>, - ): Iterator<{ + *metadataIterator(opts: MetadataIteratorOptions): Iterator<{ baseName: string, canonicalPath: string, metadata: FileMetadata, @@ -1008,7 +1020,7 @@ export default class TreeFS implements MutableFileSystem { *_metadataIterator( rootNode: DirectoryNode, - opts: Readonly<{includeSymlinks: boolean, includeNodeModules: boolean}>, + opts: MetadataIteratorOptions, prefix: string = '', ): Iterable<{ baseName: string, @@ -1060,13 +1072,7 @@ export default class TreeFS implements MutableFileSystem { iterationRootNode: DirectoryNode, iterationRootParentNode: ?DirectoryNode, ancestorOfRootIdx: ?number, - opts: Readonly<{ - alwaysYieldPosix: boolean, - canonicalPathOfRoot: string, - follow: boolean, - recursive: boolean, - subtreeOnly: boolean, - }>, + opts: PathIteratorOptions, pathPrefix: string = '', followedLinks: ReadonlySet = new Set(), ): Iterable { @@ -1185,7 +1191,7 @@ export default class TreeFS implements MutableFileSystem { _getFileData( filePath: Path, - opts: {followLeaf: boolean} = {followLeaf: true}, + opts: GetFileDataOptions = {followLeaf: true}, ): ?FileMetadata { const normalPath = this._normalizePath(filePath); const result = this._lookupByNormalPath(normalPath, { diff --git a/packages/metro-file-map/src/lib/dependencyExtractor.js b/packages/metro-file-map/src/lib/dependencyExtractor.js index 4791cf21f8..014c58c955 100644 --- a/packages/metro-file-map/src/lib/dependencyExtractor.js +++ b/packages/metro-file-map/src/lib/dependencyExtractor.js @@ -14,7 +14,7 @@ 'use strict'; const NOT_A_DOT = '(? +const CAPTURE_STRING_LITERAL = (pos: number) => `([\`'"])([^'"\`]*?)(?:\\${pos})`; const WORD_SEPARATOR = '\\b'; const LEFT_PARENTHESIS = '\\('; @@ -22,18 +22,15 @@ const RIGHT_PARENTHESIS = '\\)'; const WHITESPACE = '\\s*'; const OPTIONAL_COMMA = '(:?,\\s*)?'; -function createRegExp( - parts /*: $ReadOnlyArray */, - flags /*: string */, -) { +function createRegExp(parts: $ReadOnlyArray, flags: string) { return new RegExp(parts.join(''), flags); } -function alternatives(...parts /*: $ReadOnlyArray */) { +function alternatives(...parts: $ReadOnlyArray) { return `(?:${parts.join('|')})`; } -function functionCallStart(...names /*: $ReadOnlyArray */) { +function functionCallStart(...names: $ReadOnlyArray) { return [ NOT_A_DOT, WORD_SEPARATOR, @@ -79,14 +76,10 @@ const JEST_EXTENSIONS_RE = createRegExp( 'g', ); -function extract(code /*: string */) /*: Set */ { - const dependencies /*: Set */ = new Set(); +export function extract(code: string): Set { + const dependencies: Set = new Set(); - const addDependency = ( - match /*: string */, - _ /*: string */, - dep /*: string */, - ) => { + const addDependency = (match: string, _: string, dep: string) => { dependencies.add(dep); return match; }; @@ -101,4 +94,4 @@ function extract(code /*: string */) /*: Set */ { return dependencies; } -module.exports = {extract}; +export default {extract}; diff --git a/packages/metro-file-map/src/lib/normalizePathSeparatorsToPosix.js b/packages/metro-file-map/src/lib/normalizePathSeparatorsToPosix.js index be55409b77..210316704c 100644 --- a/packages/metro-file-map/src/lib/normalizePathSeparatorsToPosix.js +++ b/packages/metro-file-map/src/lib/normalizePathSeparatorsToPosix.js @@ -18,4 +18,6 @@ if (path.sep === '/') { filePath.replace(/\\/g, '/'); } -export default normalizePathSeparatorsToPosix; +export default normalizePathSeparatorsToPosix as interface { + (filePath: string): string, +}; diff --git a/packages/metro-file-map/src/lib/normalizePathSeparatorsToSystem.js b/packages/metro-file-map/src/lib/normalizePathSeparatorsToSystem.js index bdee4fb75f..1b480317d3 100644 --- a/packages/metro-file-map/src/lib/normalizePathSeparatorsToSystem.js +++ b/packages/metro-file-map/src/lib/normalizePathSeparatorsToSystem.js @@ -18,4 +18,6 @@ if (path.sep === '/') { filePath.replace(/\//g, path.sep); } -export default normalizePathSeparatorsToSystem; +export default normalizePathSeparatorsToSystem as interface { + (filePath: string): string, +}; diff --git a/packages/metro-file-map/src/plugins/HastePlugin.js b/packages/metro-file-map/src/plugins/HastePlugin.js index aa730a45ed..ac47e43730 100644 --- a/packages/metro-file-map/src/plugins/HastePlugin.js +++ b/packages/metro-file-map/src/plugins/HastePlugin.js @@ -55,7 +55,7 @@ export type HasteMapOptions = Readonly<{ export default class HastePlugin implements HasteMap, FileMapPlugin { - +name = 'haste'; + +name: 'haste' = 'haste'; +#rootDir: Path; +#map: Map = new Map(); @@ -81,9 +81,10 @@ export default class HastePlugin this.#failValidationOnConflicts = options.failValidationOnConflicts; } - async initialize({ - files, - }: FileMapPluginInitOptions): Promise { + async initialize( + initOptions: FileMapPluginInitOptions, + ): Promise { + const {files} = initOptions; this.#perfLogger?.point('constructHasteMap_start'); let hasteFiles = 0; for (const { diff --git a/packages/metro-file-map/src/plugins/MockPlugin.js b/packages/metro-file-map/src/plugins/MockPlugin.js index cd7b92bdc3..2f3fb18d3c 100644 --- a/packages/metro-file-map/src/plugins/MockPlugin.js +++ b/packages/metro-file-map/src/plugins/MockPlugin.js @@ -37,7 +37,7 @@ export type MockMapOptions = Readonly<{ }>; export default class MockPlugin implements FileMapPlugin, IMockMap { - +name = 'mocks'; + +name: 'mocks' = 'mocks'; +#mocksPattern: RegExp; #raw: RawMockMap; @@ -68,10 +68,10 @@ export default class MockPlugin implements FileMapPlugin, IMockMap { this.#throwOnModuleCollision = throwOnModuleCollision; } - async initialize({ - files, - pluginState, - }: FileMapPluginInitOptions): Promise { + async initialize( + initOptions: FileMapPluginInitOptions, + ): Promise { + const {files, pluginState} = initOptions; if (pluginState != null && pluginState.version === this.#raw.version) { // Use cached state directly if available this.#raw = pluginState; diff --git a/packages/metro-file-map/src/plugins/haste/computeConflicts.js b/packages/metro-file-map/src/plugins/haste/computeConflicts.js index 9ea6f023f3..696df01048 100644 --- a/packages/metro-file-map/src/plugins/haste/computeConflicts.js +++ b/packages/metro-file-map/src/plugins/haste/computeConflicts.js @@ -21,18 +21,17 @@ type Conflict = { type: 'duplicate' | 'shadowing', }; -export function computeHasteConflicts({ - duplicates, - map, - rootDir, -}: Readonly<{ - duplicates: ReadonlyMap< - string, - ReadonlyMap>, - >, - map: ReadonlyMap, - rootDir: string, -}>): Array { +export function computeHasteConflicts( + options: Readonly<{ + duplicates: ReadonlyMap< + string, + ReadonlyMap>, + >, + map: ReadonlyMap, + rootDir: string, + }>, +): Array { + const {duplicates, map, rootDir} = options; const conflicts: Array = []; // Add duplicates reported by metro-file-map diff --git a/packages/metro-file-map/src/plugins/mocks/getMockName.js b/packages/metro-file-map/src/plugins/mocks/getMockName.js index 6a68868e25..fdd26c16ec 100644 --- a/packages/metro-file-map/src/plugins/mocks/getMockName.js +++ b/packages/metro-file-map/src/plugins/mocks/getMockName.js @@ -19,4 +19,6 @@ const getMockName = (filePath: string): string => { .replaceAll('\\', '/'); }; -export default getMockName; +export default getMockName as interface { + (filePath: string): string, +}; diff --git a/packages/metro-file-map/src/watchers/AbstractWatcher.js b/packages/metro-file-map/src/watchers/AbstractWatcher.js index 4856722447..8a279078ce 100644 --- a/packages/metro-file-map/src/watchers/AbstractWatcher.js +++ b/packages/metro-file-map/src/watchers/AbstractWatcher.js @@ -32,7 +32,8 @@ export class AbstractWatcher implements WatcherBackend { #emitter: EventEmitter = new EventEmitter(); - constructor(dir: string, {ignored, globs, dot}: WatcherBackendOptions) { + constructor(dir: string, opts: WatcherBackendOptions) { + const {ignored, globs, dot} = opts; this.dot = dot || false; this.ignored = ignored; this.globs = globs; @@ -63,7 +64,7 @@ export class AbstractWatcher implements WatcherBackend { // Must be implemented by subclasses } - async stopWatching() { + async stopWatching(): Promise { this.#emitter.removeAllListeners(); } diff --git a/packages/metro-file-map/src/watchers/FallbackWatcher.js b/packages/metro-file-map/src/watchers/FallbackWatcher.js index fc733ba042..06241ca85c 100644 --- a/packages/metro-file-map/src/watchers/FallbackWatcher.js +++ b/packages/metro-file-map/src/watchers/FallbackWatcher.js @@ -42,14 +42,28 @@ const DELETE_EVENT = common.DELETE_EVENT; const DEBOUNCE_MS = 100; export default class FallbackWatcher extends AbstractWatcher { + /*:: +_changeTimers: Map = new Map(); + */ +_dirRegistry: { - [directory: string]: {[file: string]: true, __proto__: null}, + [directory: string]: { + [file: string]: true, + /*:: + __proto__: null + */ + }, + /*:: __proto__: null, + */ + } = Object.create(null); + +watched: { + [key: string]: FSWatcher, + /*:: + __proto__: null + */ } = Object.create(null); - +watched: {[key: string]: FSWatcher, __proto__: null} = Object.create(null); - async startWatching() { + async startWatching(): Promise { this._watchdir(this.root); await new Promise(resolve => { @@ -156,7 +170,7 @@ export default class FallbackWatcher extends AbstractWatcher { /** * Watch a directory. */ - _watchdir: string => boolean = (dir: string) => { + _watchdir: (dir: string) => boolean = (dir: string) => { if (this.watched[dir]) { return false; } diff --git a/packages/metro-file-map/src/watchers/WatchmanWatcher.js b/packages/metro-file-map/src/watchers/WatchmanWatcher.js index 8f55d47627..240f700d5f 100644 --- a/packages/metro-file-map/src/watchers/WatchmanWatcher.js +++ b/packages/metro-file-map/src/watchers/WatchmanWatcher.js @@ -49,8 +49,9 @@ export default class WatchmanWatcher extends AbstractWatcher { +watchmanDeferStates: $ReadOnlyArray; #deferringStates: ?Set = null; - constructor(dir: string, {watchmanDeferStates, ...opts}: WatcherOptions) { - super(dir, opts); + constructor(dir: string, opts: WatcherOptions) { + const {watchmanDeferStates, ...baseOpts} = opts; + super(dir, baseOpts); this.watchmanDeferStates = watchmanDeferStates; @@ -62,7 +63,7 @@ export default class WatchmanWatcher extends AbstractWatcher { this.subscriptionName = `${SUB_PREFIX}-${process.pid}-${readablePath}-${watchKey}`; } - async startWatching() { + async startWatching(): Promise { await new Promise((resolve, reject) => this._init(resolve, reject)); } @@ -313,7 +314,7 @@ export default class WatchmanWatcher extends AbstractWatcher { /** * Closes the watcher. */ - async stopWatching() { + async stopWatching(): Promise { await super.stopWatching(); if (this.client) { this.client.removeAllListeners(); diff --git a/packages/metro-file-map/types/Watcher.d.ts b/packages/metro-file-map/types/Watcher.d.ts index 66d29e36f8..e76f8b439f 100644 --- a/packages/metro-file-map/types/Watcher.d.ts +++ b/packages/metro-file-map/types/Watcher.d.ts @@ -5,20 +5,75 @@ * LICENSE file in the root directory of this source tree. * * @format - * @oncall react_native + * */ +import type { + Console, + CrawlerOptions, + FileData, + Path, + PerfLogger, + WatcherBackend, + WatcherBackendChangeEvent, + WatchmanClocks, +} from './flow-types'; + +import EventEmitter from 'events'; + +type CrawlResult = { + changedFiles: FileData; + clocks?: WatchmanClocks; + removedFiles: Set; +}; +type WatcherOptions = { + abortSignal: AbortSignal; + computeSha1: boolean; + console: Console; + enableSymlinks: boolean; + extensions: ReadonlyArray; + forceNodeFilesystemAPI: boolean; + healthCheckFilePrefix: string; + ignoreForCrawl: (filePath: string) => boolean; + ignorePatternForWatch: RegExp; + previousState: CrawlerOptions['previousState']; + perfLogger: null | undefined | PerfLogger; + roots: ReadonlyArray; + rootDir: string; + useWatchman: boolean; + watch: boolean; + watchmanDeferStates: ReadonlyArray; +}; export type HealthCheckResult = - | {type: 'error'; timeout: number; error: Error; watcher: string | null} + | { + type: 'error'; + timeout: number; + error: Error; + watcher: null | undefined | string; + } | { type: 'success'; timeout: number; timeElapsed: number; - watcher: string | null; + watcher: null | undefined | string; } | { type: 'timeout'; timeout: number; - watcher: string | null; - pauseReason: string | null; + watcher: null | undefined | string; + pauseReason: null | undefined | string; }; +export declare class Watcher extends EventEmitter { + _options: WatcherOptions; + _backends: ReadonlyArray; + _instanceId: number; + _nextHealthCheckId: number; + _pendingHealthChecks: Map void>; + _activeWatcher: null | undefined | string; + constructor(options: WatcherOptions); + crawl(): Promise; + watch(onChange: (change: WatcherBackendChangeEvent) => void): void; + _handleHealthCheckObservation(basename: string): void; + close(): void; + checkHealth(timeout: number): Promise; +} diff --git a/packages/metro-file-map/types/cache/DiskCacheManager.d.ts b/packages/metro-file-map/types/cache/DiskCacheManager.d.ts index 0d21587936..1038bd0122 100644 --- a/packages/metro-file-map/types/cache/DiskCacheManager.d.ts +++ b/packages/metro-file-map/types/cache/DiskCacheManager.d.ts @@ -12,27 +12,31 @@ import type { BuildParameters, CacheData, CacheManager, + CacheManagerFactoryOptions, CacheManagerWriteOptions, } from '../flow-types'; -export interface DiskCacheConfig { - buildParameters: BuildParameters; - cacheFilePrefix?: string | null; - cacheDirectory?: string | null; -} - -export class DiskCacheManager implements CacheManager { - constructor(options: DiskCacheConfig); +type AutoSaveOptions = Readonly<{debounceMs: number}>; +type DiskCacheConfig = Readonly<{ + autoSave?: Partial | boolean; + cacheFilePrefix?: null | undefined | string; + cacheDirectory?: null | undefined | string; +}>; +export declare class DiskCacheManager implements CacheManager { + constructor( + factoryOptions: CacheManagerFactoryOptions, + config: DiskCacheConfig, + ); static getCacheFilePath( buildParameters: BuildParameters, - cacheFilePrefix?: string | null, - cacheDirectory?: string | null, + cacheFilePrefix?: null | undefined | string, + cacheDirectory?: null | undefined | string, ): string; getCacheFilePath(): string; - read(): Promise; + read(): Promise; write( getSnapshot: () => CacheData, - opts: CacheManagerWriteOptions, + writeOptions: CacheManagerWriteOptions, ): Promise; end(): Promise; } diff --git a/packages/metro-file-map/types/constants.d.ts b/packages/metro-file-map/types/constants.d.ts new file mode 100644 index 0000000000..1b38370148 --- /dev/null +++ b/packages/metro-file-map/types/constants.d.ts @@ -0,0 +1,17 @@ +/** + * 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. + * + * @noformat - Flow comment syntax + */ + +import type { HType } from "./flow-types"; + +export declare const constants: HType; +export declare type constants = typeof constants; +declare const $$EXPORT_DEFAULT_DECLARATION$$: typeof constants; +declare type $$EXPORT_DEFAULT_DECLARATION$$ = + typeof $$EXPORT_DEFAULT_DECLARATION$$; +export default $$EXPORT_DEFAULT_DECLARATION$$; diff --git a/packages/metro-file-map/types/crawlers/node/hasNativeFindSupport.d.ts b/packages/metro-file-map/types/crawlers/node/hasNativeFindSupport.d.ts new file mode 100644 index 0000000000..c4a13dde62 --- /dev/null +++ b/packages/metro-file-map/types/crawlers/node/hasNativeFindSupport.d.ts @@ -0,0 +1,12 @@ +/** + * 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. + * + * @format + * @oncall react_native + */ + +declare function hasNativeFindSupport(): Promise; +export default hasNativeFindSupport; diff --git a/packages/metro-file-map/types/crawlers/node/index.d.ts b/packages/metro-file-map/types/crawlers/node/index.d.ts new file mode 100644 index 0000000000..de9c996564 --- /dev/null +++ b/packages/metro-file-map/types/crawlers/node/index.d.ts @@ -0,0 +1,16 @@ +/** + * 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. + * + * @format + * @oncall react_native + */ + +import type {CanonicalPath, CrawlerOptions, FileData} from '../../flow-types'; + +declare function nodeCrawl( + options: CrawlerOptions, +): Promise<{removedFiles: Set; changedFiles: FileData}>; +export default nodeCrawl; diff --git a/packages/metro-file-map/types/crawlers/watchman/index.d.ts b/packages/metro-file-map/types/crawlers/watchman/index.d.ts new file mode 100644 index 0000000000..d6b9c4e5e8 --- /dev/null +++ b/packages/metro-file-map/types/crawlers/watchman/index.d.ts @@ -0,0 +1,23 @@ +/** + * 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. + * + * @format + * @oncall react_native + */ + +import type { + CanonicalPath, + CrawlerOptions, + FileData, + WatchmanClocks, +} from '../../flow-types'; + +declare function watchmanCrawl(options: CrawlerOptions): Promise<{ + changedFiles: FileData; + removedFiles: Set; + clocks: WatchmanClocks; +}>; +export default watchmanCrawl; diff --git a/packages/metro-file-map/types/crawlers/watchman/planQuery.d.ts b/packages/metro-file-map/types/crawlers/watchman/planQuery.d.ts new file mode 100644 index 0000000000..a2fd6a41bd --- /dev/null +++ b/packages/metro-file-map/types/crawlers/watchman/planQuery.d.ts @@ -0,0 +1,23 @@ +/** + * 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. + * + * @format + * + */ + +import type {WatchmanQuery, WatchmanQuerySince} from 'fb-watchman'; + +type PlanQueryArgs = Readonly<{ + since: null | undefined | WatchmanQuerySince; + directoryFilters: ReadonlyArray; + extensions: ReadonlyArray; + includeSha1: boolean; + includeSymlinks: boolean; +}>; +export declare function planQuery(args: PlanQueryArgs): { + query: WatchmanQuery; + queryGenerator: string; +}; diff --git a/packages/metro-file-map/types/flow-types.d.ts b/packages/metro-file-map/types/flow-types.d.ts index e302113397..f105062e6e 100644 --- a/packages/metro-file-map/types/flow-types.d.ts +++ b/packages/metro-file-map/types/flow-types.d.ts @@ -11,11 +11,6 @@ import type {PerfLogger, PerfLoggerFactory, RootPerfLogger} from 'metro-config'; export type {PerfLoggerFactory, PerfLogger}; - -/** - * These inputs affect the internal data collected for a given filesystem - * state, and changes may invalidate a cache. - */ export type BuildParameters = Readonly<{ computeDependencies: boolean; computeSha1: boolean; @@ -24,35 +19,31 @@ export type BuildParameters = Readonly<{ extensions: ReadonlyArray; forceNodeFilesystemAPI: boolean; ignorePattern: RegExp; - mocksPattern: RegExp | null; - platforms: ReadonlyArray; + plugins: ReadonlyArray; retainAllFiles: boolean; rootDir: string; roots: ReadonlyArray; - dependencyExtractor: string | null; - hasteImplModulePath: string | null; + dependencyExtractor: null | undefined | string; + hasteImplModulePath: null | undefined | string; cacheBreaker: string; }>; - -export interface BuildResult { +export type BuildResult = { fileSystem: FileSystem; hasteMap: HasteMap; -} - -export interface CacheData { - readonly clocks: WatchmanClocks; - readonly fileSystemData: unknown; - readonly plugins: ReadonlyMap; -} - + mockMap: null | undefined | MockMap; +}; +export type CacheData = Readonly<{ + clocks: WatchmanClocks; + fileSystemData: unknown; + plugins: ReadonlyMap; +}>; export interface CacheManager { /** * Called during startup to load initial state, if available. Provided to * a crawler, which will return the delta between the initial state and the * current file system state. */ - read(): Promise; - + read(): Promise; /** * Called when metro-file-map `build()` has applied changes returned by the * crawler - i.e. internal state reflects the current file system state. @@ -64,65 +55,62 @@ export interface CacheManager { getSnapshot: () => CacheData, opts: CacheManagerWriteOptions, ): Promise; - /** * The last call that will be made to this CacheManager. Any handles should * be closed by the time this settles. */ end(): Promise; } - export interface CacheManagerEventSource { - onChange(listener: () => void): () => void /* unsubscribe */; + onChange(listener: () => void): () => void; } - export type CacheManagerFactory = ( options: CacheManagerFactoryOptions, ) => CacheManager; - -export type CacheManagerFactoryOptions = { +export type CacheManagerFactoryOptions = Readonly<{ buildParameters: BuildParameters; -}; - -export type CacheManagerWriteOptions = { +}>; +export type CacheManagerWriteOptions = Readonly<{ changedSinceCacheRead: boolean; eventSource: CacheManagerEventSource; - onWriteError: (e: Error) => void; -}; - -export interface ChangeEvent { - logger: RootPerfLogger | null; + onWriteError: (error: Error) => void; +}>; +export type CanonicalPath = string; +export type ChangeEvent = { + logger: null | undefined | RootPerfLogger; eventsQueue: EventsQueue; -} - -export interface ChangeEventMetadata { - /** Epoch ms */ - modifiedTime: number | null; - /** Bytes */ - size: number | null; - /** Regular file / Directory / Symlink */ +}; +export type ChangeEventMetadata = { + modifiedTime: null | undefined | number; + size: null | undefined | number; type: 'f' | 'd' | 'l'; -} - +}; export type Console = typeof global.console; - -export interface CrawlerOptions { - abortSignal: AbortSignal | null; +export type CrawlerOptions = { + abortSignal: null | undefined | AbortSignal; computeSha1: boolean; + console: Console; extensions: ReadonlyArray; forceNodeFilesystemAPI: boolean; ignore: IgnoreMatcher; includeSymlinks: boolean; - perfLogger?: PerfLogger | null; + perfLogger?: null | undefined | PerfLogger; previousState: Readonly<{ - clocks: ReadonlyMap; - files: ReadonlyMap; + clocks: ReadonlyMap; + fileSystem: FileSystem; }>; rootDir: string; roots: ReadonlyArray; onStatus: (status: WatcherStatus) => void; -} - +}; +export type DependencyExtractor = { + extract: ( + content: string, + absoluteFilePath: string, + defaultExtractor?: DependencyExtractor['extract'], + ) => Set; + getCacheKey: () => string; +}; export type WatcherStatus = | { type: 'watchman_slow_command'; @@ -139,24 +127,54 @@ export type WatcherStatus = warning: unknown; command: 'watch-project' | 'query'; }; - -export type DuplicatesSet = Map; +export type DuplicatesSet = Map; export type DuplicatesIndex = Map>; - export type EventsQueue = Array<{ filePath: Path; - metadata?: ChangeEventMetadata | null; + metadata: ChangeEventMetadata; type: string; }>; - -export interface HType { - ID: 0; - MTIME: 1; - SIZE: 2; - VISITED: 3; - DEPENDENCIES: 4; - SHA1: 5; - SYMLINK: 6; +export type FileMapDelta = Readonly<{ + removed: Iterable<[CanonicalPath, FileMetadata]>; + addedOrModified: Iterable<[CanonicalPath, FileMetadata]>; +}>; +interface FileSystemState { + metadataIterator( + opts: Readonly<{includeNodeModules: boolean; includeSymlinks: boolean}>, + ): Iterable<{ + baseName: string; + canonicalPath: string; + metadata: FileMetadata; + }>; +} +export type FileMapPluginInitOptions = Readonly<{ + files: FileSystemState; + pluginState: null | undefined | SerializableState; +}>; +type V8Serializable = unknown; +export interface FileMapPlugin { + readonly name: string; + initialize( + initOptions: FileMapPluginInitOptions, + ): Promise; + assertValid(): void; + bulkUpdate(delta: FileMapDelta): Promise; + getSerializableSnapshot(): SerializableState; + onRemovedFile(relativeFilePath: string, fileMetadata: FileMetadata): void; + onNewOrModifiedFile( + relativeFilePath: string, + fileMetadata: FileMetadata, + ): void; + getCacheKey(): string; +} +export type HType = { + MTIME: 0; + SIZE: 1; + VISITED: 2; + DEPENDENCIES: 3; + SHA1: 4; + SYMLINK: 5; + ID: 6; PATH: 0; TYPE: 1; MODULE: 0; @@ -164,38 +182,38 @@ export interface HType { GENERIC_PLATFORM: 'g'; NATIVE_PLATFORM: 'native'; DEPENDENCY_DELIM: '\0'; -} - -type Values = T[keyof T]; -export type HTypeValue = Values; - +}; +export type HTypeValue = HType[keyof HType]; export type IgnoreMatcher = (item: string) => boolean; - -export type FileData = Map; - +export type FileData = Map; export type FileMetadata = [ - /* id */ string, - /* mtime */ number, - /* size */ number, - /* visited */ 0 | 1, - /* dependencies */ string, - /* sha1 */ string | null, - /* symlink */ 0 | 1 | string, // string specifies target, if known + null | undefined | number, + number, + 0 | 1, + string, + null | undefined | string, + 0 | 1 | string, + string, ]; - export type FileStats = Readonly<{ fileType: 'f' | 'l'; - modifiedTime: number; + modifiedTime: null | undefined | number; + size: null | undefined | number; }>; - export interface FileSystem { exists(file: Path): boolean; - getAllFiles(): Path[]; - getDependencies(file: Path): string[] | null; - getModuleName(file: Path): string | null; - getSerializableSnapshot(): FileData; - getSha1(file: Path): string | null; - + getAllFiles(): Array; + getDependencies(file: Path): null | undefined | Array; + getDifference(files: FileData): { + changedFiles: FileData; + removedFiles: Set; + }; + getModuleName(file: Path): null | undefined | string; + getSerializableSnapshot(): CacheData['fileSystemData']; + getSha1(file: Path): null | undefined | string; + getOrComputeSha1( + file: Path, + ): Promise; /** * Given a start path (which need not exist), a subpath and type, and * optionally a 'breakOnSegment', performs the following: @@ -223,130 +241,133 @@ export interface FileSystem { mixedStartPath: string, subpath: string, opts: { - breakOnSegment: string | null | undefined; - invalidatedBy: Set | null | undefined; + breakOnSegment: null | undefined | string; + invalidatedBy: null | undefined | Set; subpathType: 'f' | 'd'; }, - ): { - absolutePath: string; - containerRelativePath: string; - } | null; - + ): null | undefined | {absolutePath: string; containerRelativePath: string}; /** * Analogous to posix lstat. If the file at `file` is a symlink, return * information about the symlink without following it. */ - linkStats(file: Path): FileStats | null; - + linkStats(file: Path): null | undefined | FileStats; /** * Return information about the given path, whether a directory or file. * Always follow symlinks, and return a real path if it exists. */ lookup(mixedPath: Path): LookupResult; - matchFiles(opts: { - /* Filter relative paths against a pattern. */ filter?: RegExp | null; - /* `filter` is applied against absolute paths, vs rootDir-relative. (default: false) */ filterCompareAbsolute?: boolean; - /* `filter` is applied against posix-delimited paths, even on Windows. (default: false) */ filterComparePosix?: boolean; - /* Follow symlinks when enumerating paths. (default: false) */ follow?: boolean; - /* Should search for files recursively. (default: true) */ recursive?: boolean; - /* Match files under a given root, or null for all files */ rootDir?: Path | null; }): Iterable; } - export type Glob = string; - export type LookupResult = - | { - // The node is missing from the FileSystem implementation (note this - // could indicate an unwatched path, or a directory containing no watched - // files). - exists: false; - // The real, normal, absolute paths of any symlinks traversed. - links: ReadonlySet; - // The real, normal, absolute path of the first path segment - // encountered that does not exist, or cannot be navigated through. - missing: string; - } + | {exists: false; links: ReadonlySet; missing: string} | { exists: true; - // The real, normal, absolute paths of any symlinks traversed. links: ReadonlySet; - // The real, normal, absolute path of the file or directory. realPath: string; - // Currently lookup always follows symlinks, so can only return - // directories or regular files, but this may be extended. type: 'd' | 'f'; }; - +export interface MockMap { + getMockModule(name: string): null | undefined | Path; +} export type HasteConflict = { id: string; platform: string | null; absolutePaths: Array; type: 'duplicate' | 'shadowing'; }; - export interface HasteMap { getModule( name: string, - platform?: string | null, - supportsNativePlatform?: boolean | null, - type?: HTypeValue | null, - ): Path | null; - + platform?: null | undefined | string, + supportsNativePlatform?: null | undefined | boolean, + type?: null | undefined | HTypeValue, + ): null | undefined | Path; getPackage( name: string, - platform: string | null, - _supportsNativePlatform: boolean | null, - ): Path | null; - + platform: null | undefined | string, + _supportsNativePlatform: null | undefined | boolean, + ): null | undefined | Path; computeConflicts(): Array; } - -export type RawMockMap = { - readonly mocks: Map; - readonly duplicates: Map>; -}; - export type HasteMapData = Map; - -export interface HasteMapItem { - [platform: string]: HasteMapItemMetadata; -} -export type HasteMapItemMetadata = [/* path */ string, /* type */ number]; - +export type HasteMapItem = {[platform: string]: HasteMapItemMetadata}; +export type HasteMapItemMetadata = [string, number]; export interface MutableFileSystem extends FileSystem { - remove(filePath: Path): void; + remove(filePath: Path): null | undefined | FileMetadata; addOrModify(filePath: Path, fileMetadata: FileMetadata): void; bulkAddOrModify(addedOrModifiedFiles: FileData): void; } - export type Path = string; - +export type ProcessFileFunction = ( + absolutePath: string, + metadata: FileMetadata, + request: Readonly<{computeSha1: boolean}>, +) => null | undefined | Buffer; +export type RawMockMap = Readonly<{ + duplicates: Map>; + mocks: Map; + version: number; +}>; +export type ReadOnlyRawMockMap = Readonly<{ + duplicates: ReadonlyMap>; + mocks: ReadonlyMap; + version: number; +}>; +export interface WatcherBackend { + getPauseReason(): null | undefined | string; + onError(listener: (error: Error) => void): () => void; + onFileEvent(listener: (event: WatcherBackendChangeEvent) => void): () => void; + startWatching(): Promise; + stopWatching(): Promise; +} +export type ChangeEventClock = [string, string]; +export type WatcherBackendChangeEvent = + | Readonly<{ + event: 'touch'; + clock?: ChangeEventClock; + relativePath: string; + root: string; + metadata: ChangeEventMetadata; + }> + | Readonly<{ + event: 'delete'; + clock?: ChangeEventClock; + relativePath: string; + root: string; + metadata?: void; + }>; +export type WatcherBackendOptions = Readonly<{ + ignored: null | undefined | RegExp; + globs: ReadonlyArray; + dot: boolean; +}>; export type WatchmanClockSpec = | string | Readonly<{scm: Readonly<{'mergebase-with': string}>}>; export type WatchmanClocks = Map; - export type WorkerMessage = Readonly<{ computeDependencies: boolean; computeSha1: boolean; - dependencyExtractor?: string | null; + dependencyExtractor?: null | undefined | string; enableHastePackages: boolean; - rootDir: string; filePath: string; - hasteImplModulePath?: string | null; + hasteImplModulePath?: null | undefined | string; + maybeReturnContent: boolean; }>; - export type WorkerMetadata = Readonly<{ - dependencies?: ReadonlyArray; - id?: string | null; - module?: HasteMapItemMetadata | null; - sha1?: string | null; + dependencies?: null | undefined | ReadonlyArray; + id?: null | undefined | string; + sha1?: null | undefined | string; + content?: null | undefined | Buffer; }>; +export interface WorkerSetupArgs { + __future__?: false; +} diff --git a/packages/metro-file-map/types/index.d.ts b/packages/metro-file-map/types/index.d.ts index 22131b3f73..50c8729952 100644 --- a/packages/metro-file-map/types/index.d.ts +++ b/packages/metro-file-map/types/index.d.ts @@ -12,20 +12,35 @@ import type { BuildParameters, BuildResult, CacheData, + CacheManager, CacheManagerFactory, + CanonicalPath, + ChangeEventClock, ChangeEventMetadata, Console, + CrawlerOptions, FileData, + FileMapPlugin, + FileMetadata, FileSystem, - HasteConflict, HasteMapData, HasteMapItem, + HType, + MutableFileSystem, + Path, + PerfLogger, PerfLoggerFactory, + WatchmanClocks, } from './flow-types'; -import type {EventEmitter} from 'events'; + +import {FileProcessor} from './lib/FileProcessor'; +import {RootPathUtils} from './lib/RootPathUtils'; +import {Watcher} from './Watcher'; +import EventEmitter from 'events'; export type { BuildParameters, + BuildResult, CacheData, ChangeEventMetadata, FileData, @@ -34,63 +49,219 @@ export type { HasteMapData, HasteMapItem, }; - export type InputOptions = Readonly<{ - computeDependencies?: boolean | null; - computeSha1?: boolean | null; - enableSymlinks?: boolean | null; + computeDependencies?: null | undefined | boolean; + computeSha1?: null | undefined | boolean; + enableHastePackages?: boolean; + enableSymlinks?: null | undefined | boolean; + enableWorkerThreads?: null | undefined | boolean; extensions: ReadonlyArray; - forceNodeFilesystemAPI?: boolean | null; - ignorePattern?: RegExp | null; - mocksPattern?: string | null; + forceNodeFilesystemAPI?: null | undefined | boolean; + ignorePattern?: null | undefined | RegExp; + mocksPattern?: null | undefined | string; platforms: ReadonlyArray; + plugins?: ReadonlyArray; retainAllFiles: boolean; rootDir: string; roots: ReadonlyArray; - - /** Module paths that should export a 'getCacheKey' method */ - dependencyExtractor?: string | null; - hasteImplModulePath?: string | null; - - perfLoggerFactory?: PerfLoggerFactory | null; - resetCache?: boolean | null; - maxWorkers: number; - throwOnModuleCollision?: boolean | null; - useWatchman?: boolean | null; - watchmanDeferStates?: ReadonlyArray; - watch?: boolean | null; + dependencyExtractor?: null | undefined | string; + hasteImplModulePath?: null | undefined | string; + cacheManagerFactory?: null | undefined | CacheManagerFactory; console?: Console; - cacheManagerFactory?: CacheManagerFactory | null; - healthCheck: HealthCheckOptions; + maxFilesPerWorker?: null | undefined | number; + maxWorkers: number; + perfLoggerFactory?: null | undefined | PerfLoggerFactory; + resetCache?: null | undefined | boolean; + throwOnModuleCollision?: null | undefined | boolean; + useWatchman?: null | undefined | boolean; + watch?: null | undefined | boolean; + watchmanDeferStates?: ReadonlyArray; }>; - type HealthCheckOptions = Readonly<{ enabled: boolean; interval: number; timeout: number; filePrefix: string; }>; - +type InternalOptions = Readonly< + Omit< + BuildParameters, + keyof { + healthCheck: HealthCheckOptions; + perfLoggerFactory: null | undefined | PerfLoggerFactory; + resetCache: null | undefined | boolean; + useWatchman: boolean; + watch: boolean; + watchmanDeferStates: ReadonlyArray; + } + > & { + healthCheck: HealthCheckOptions; + perfLoggerFactory: null | undefined | PerfLoggerFactory; + resetCache: null | undefined | boolean; + useWatchman: boolean; + watch: boolean; + watchmanDeferStates: ReadonlyArray; + } +>; export {DiskCacheManager} from './cache/DiskCacheManager'; -export {DuplicateHasteCandidatesError} from './lib/DuplicateHasteCandidatesError'; +export {DuplicateHasteCandidatesError} from './plugins/haste/DuplicateHasteCandidatesError'; +export {HasteConflictsError} from './plugins/haste/HasteConflictsError'; +export {default as HastePlugin} from './plugins/HastePlugin'; export type {HasteMap} from './flow-types'; export type {HealthCheckResult} from './Watcher'; export type { CacheManager, CacheManagerFactory, + CacheManagerFactoryOptions, + CacheManagerWriteOptions, ChangeEvent, + DependencyExtractor, WatcherStatus, } from './flow-types'; - -export default class FileMap extends EventEmitter { +/** + * FileMap includes a JavaScript implementation of Facebook's haste module system. + * + * This implementation is inspired by https://github.com/facebook/node-haste + * and was built with for high-performance in large code repositories with + * hundreds of thousands of files. This implementation is scalable and provides + * predictable performance. + * + * Because the haste map creation and synchronization is critical to startup + * performance and most tasks are blocked by I/O this class makes heavy use of + * synchronous operations. It uses worker processes for parallelizing file + * access and metadata extraction. + * + * The data structures created by `metro-file-map` can be used directly from the + * cache without further processing. The metadata objects in the `files` and + * `map` objects contain cross-references: a metadata object from one can look + * up the corresponding metadata object in the other map. Note that in most + * projects, the number of files will be greater than the number of haste + * modules one module can refer to many files based on platform extensions. + * + * type CacheData = { + * clocks: WatchmanClocks, + * files: {[filepath: string]: FileMetadata}, + * map: {[id: string]: HasteMapItem}, + * mocks: {[id: string]: string}, + * } + * + * // Watchman clocks are used for query synchronization and file system deltas. + * type WatchmanClocks = {[filepath: string]: string}; + * + * type FileMetadata = { + * id: ?string, // used to look up module metadata objects in `map`. + * mtime: number, // check for outdated files. + * size: number, // size of the file in bytes. + * visited: boolean, // whether the file has been parsed or not. + * dependencies: Array, // all relative dependencies of this file. + * sha1: ?string, // SHA-1 of the file, if requested via options. + * symlink: ?(1 | 0 | string), // Truthy if symlink, string is target + * }; + * + * // Modules can be targeted to a specific platform based on the file name. + * // Example: platform.ios.js and Platform.android.js will both map to the same + * // `Platform` module. The platform should be specified during resolution. + * type HasteMapItem = {[platform: string]: ModuleMetadata}; + * + * // + * type ModuleMetadata = { + * path: string, // the path to look up the file object in `files`. + * type: string, // the module type (either `package` or `module`). + * }; + * + * Note that the data structures described above are conceptual only. The actual + * implementation uses arrays and constant keys for metadata storage. Instead of + * `{id: 'flatMap', mtime: 3421, size: 42, visited: true, dependencies: []}` the real + * representation is similar to `['flatMap', 3421, 42, 1, []]` to save storage space + * and reduce parse and write time of a big JSON blob. + * + * The HasteMap is created as follows: + * 1. read data from the cache or create an empty structure. + * + * 2. crawl the file system. + * * empty cache: crawl the entire file system. + * * cache available: + * * if watchman is available: get file system delta changes. + * * if watchman is unavailable: crawl the entire file system. + * * build metadata objects for every file. This builds the `files` part of + * the `HasteMap`. + * + * 3. parse and extract metadata from changed files. + * * this is done in parallel over worker processes to improve performance. + * * the worst case is to parse all files. + * * the best case is no file system access and retrieving all data from + * the cache. + * * the average case is a small number of changed files. + * + * 4. serialize the new `HasteMap` in a cache file. + * + */ +declare class FileMap extends EventEmitter { + _buildPromise: null | undefined | Promise; + canUseWatchmanPromise: Promise; + _changeID: number; + _fileProcessor: FileProcessor; + _console: Console; + _options: InternalOptions; + _pathUtils: RootPathUtils; + _watcher: null | undefined | Watcher; + _cacheManager: CacheManager; + _crawlerAbortController: AbortController; + _startupPerfLogger: null | undefined | PerfLogger; static create(options: InputOptions): FileMap; constructor(options: InputOptions); build(): Promise; - read(): Promise; -} - -export class HasteConflictsError extends Error { - constructor(conflicts: ReadonlyArray); - getDetailedMessage(pathsRelativeToRoot?: string): string; + /** + * 1. read data from the cache or create an empty structure. + */ + read(): Promise; + /** + * 2. crawl the file system. + */ + _buildFileDelta(previousState: CrawlerOptions['previousState']): Promise<{ + removedFiles: Set; + changedFiles: FileData; + clocks?: WatchmanClocks; + }>; + _maybeReadLink( + filePath: Path, + fileMetadata: FileMetadata, + ): null | undefined | Promise; + _applyFileDelta( + fileSystem: MutableFileSystem, + plugins: ReadonlyArray, + delta: Readonly<{ + changedFiles: FileData; + removedFiles: ReadonlySet; + clocks?: WatchmanClocks; + }>, + ): Promise; + /** + * 4. Serialize a snapshot of our raw data via the configured cache manager + */ + _takeSnapshotAndPersist( + fileSystem: FileSystem, + clocks: WatchmanClocks, + plugins: ReadonlyArray, + changed: FileData, + removed: Set, + ): void; + /** + * Watch mode + */ + _watch( + fileSystem: MutableFileSystem, + clocks: WatchmanClocks, + plugins: ReadonlyArray, + ): Promise; + end(): Promise; + _shouldUseWatchman(): Promise; + _getNextChangeID(): number; + _updateClock( + clocks: WatchmanClocks, + newClock?: null | undefined | ChangeEventClock, + ): void; + static H: HType; } +export default FileMap; diff --git a/packages/metro-file-map/types/lib/FileProcessor.d.ts b/packages/metro-file-map/types/lib/FileProcessor.d.ts new file mode 100644 index 0000000000..ba5b45c8ac --- /dev/null +++ b/packages/metro-file-map/types/lib/FileProcessor.d.ts @@ -0,0 +1,56 @@ +/** + * 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. + * + * @format + * @oncall react_native + */ + +import type {FileMetadata, PerfLogger} from '../flow-types'; + +type ProcessFileRequest = Readonly<{ + /** + * Populate metadata[H.SHA1] with the SHA1 of the file's contents. + */ + computeSha1: boolean; + /** + * Populate metadata[H.DEPENDENCIES] with unresolved dependency specifiers + * using the dependencyExtractor provided to the constructor. + */ + computeDependencies: boolean; + /** + * Only if processing has already required reading the file's contents, return + * the contents as a Buffer - null otherwise. Not supported for batches. + */ + maybeReturnContent: boolean; +}>; +interface MaybeCodedError extends Error { + code?: string; +} +export declare class FileProcessor { + constructor( + opts: Readonly<{ + dependencyExtractor: null | undefined | string; + enableHastePackages: boolean; + enableWorkerThreads: boolean; + hasteImplModulePath: null | undefined | string; + maxFilesPerWorker?: null | undefined | number; + maxWorkers: number; + perfLogger: null | undefined | PerfLogger; + }>, + ); + processBatch( + files: ReadonlyArray<[string, FileMetadata]>, + req: ProcessFileRequest, + ): Promise<{ + errors: Array<{absolutePath: string; error: MaybeCodedError}>; + }>; + processRegularFile( + absolutePath: string, + fileMetadata: FileMetadata, + req: ProcessFileRequest, + ): null | undefined | {content: null | undefined | Buffer}; + end(): Promise; +} diff --git a/packages/metro-file-map/types/lib/RootPathUtils.d.ts b/packages/metro-file-map/types/lib/RootPathUtils.d.ts new file mode 100644 index 0000000000..90741778b0 --- /dev/null +++ b/packages/metro-file-map/types/lib/RootPathUtils.d.ts @@ -0,0 +1,24 @@ +/** + * 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. + * + * @format + * + */ + +export declare class RootPathUtils { + constructor(rootDir: string); + getBasenameOfNthAncestor(n: number): string; + getParts(): ReadonlyArray; + absoluteToNormal(absolutePath: string): string; + normalToAbsolute(normalPath: string): string; + relativeToNormal(relativePath: string): string; + getAncestorOfRootIdx(normalPath: string): null | undefined | number; + joinNormalToRelative( + normalPath: string, + relativePath: string, + ): {normalPath: string; collapsedSegments: number}; + relative(from: string, to: string): string; +} diff --git a/packages/metro-file-map/types/lib/TreeFS.d.ts b/packages/metro-file-map/types/lib/TreeFS.d.ts new file mode 100644 index 0000000000..2d1d62f545 --- /dev/null +++ b/packages/metro-file-map/types/lib/TreeFS.d.ts @@ -0,0 +1,261 @@ +/** + * 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. + * + * @format + * + */ + +import type { + CacheData, + FileData, + FileMetadata, + FileStats, + LookupResult, + MutableFileSystem, + Path, + ProcessFileFunction, +} from '../flow-types'; + +type DirectoryNode = Map; +type FileNode = FileMetadata; +type MixedNode = FileNode | DirectoryNode; +type NormalizedSymlinkTarget = { + ancestorOfRootIdx: null | undefined | number; + normalPath: string; + startOfBasenameIdx: number; +}; +type DeserializedSnapshotInput = { + rootDir: string; + fileSystemData: DirectoryNode; + processFile: ProcessFileFunction; +}; +type TreeFSOptions = { + rootDir: Path; + files?: FileData; + processFile: ProcessFileFunction; +}; +type MatchFilesOptions = Readonly<{ + filter?: null | undefined | RegExp; + filterCompareAbsolute?: boolean; + filterComparePosix?: boolean; + follow?: boolean; + recursive?: boolean; + rootDir?: null | undefined | Path; +}>; +type MetadataIteratorOptions = Readonly<{ + includeSymlinks: boolean; + includeNodeModules: boolean; +}>; +type PathIteratorOptions = Readonly<{ + alwaysYieldPosix: boolean; + canonicalPathOfRoot: string; + follow: boolean; + recursive: boolean; + subtreeOnly: boolean; +}>; +type GetFileDataOptions = {followLeaf: boolean}; +/** + * OVERVIEW: + * + * TreeFS is Metro's in-memory representation of the file system. It is + * structured as a tree of non-empty maps and leaves (tuples), with the root + * node representing the given `rootDir`, typically Metro's _project root_ + * (not a filesystem root). Map keys are path segments, and branches outside + * the project root are accessed via `'..'`. + * + * EXAMPLE: + * + * For a root dir '/data/project', the file '/data/other/app/index.js' would + * have metadata at #rootNode.get('..').get('other').get('app').get('index.js') + * + * SERIALISATION: + * + * #rootNode is designed to be directly serialisable and directly portable (for + * a given project) between different root directories and operating systems. + * + * SYMLINKS: + * + * Symlinks are represented as nodes whose metadata contains their literal + * target. Literal targets are resolved to normal paths at runtime, and cached. + * If a symlink is encountered during traversal, we restart traversal at the + * root node targeting join(normal symlink target, remaining path suffix). + * + * NODE TYPES: + * + * - A directory (including a parent directory at '..') is represented by a + * `Map` of basenames to any other node type. + * - A file is represented by an `Array` (tuple) of metadata, of which: + * - A regular file has node[H.SYMLINK] === 0 + * - A symlink has node[H.SYMLINK] === 1 or + * typeof node[H.SYMLINK] === 'string', where a string is the literal + * content of the symlink (i.e. from readlink), if known. + * + * TERMINOLOGY: + * + * - mixedPath + * A root-relative or absolute path + * - relativePath + * A root-relative path + * - normalPath + * A root-relative, normalised path (no extraneous '.' or '..'), may have a + * single trailing slash + * - canonicalPath + * A root-relative, normalised, real path (no symlinks in dirname), never has + * a trailing slash + */ +declare class TreeFS implements MutableFileSystem { + constructor(opts: TreeFSOptions); + getSerializableSnapshot(): CacheData['fileSystemData']; + static fromDeserializedSnapshot(args: DeserializedSnapshotInput): TreeFS; + getModuleName(mixedPath: Path): null | undefined | string; + getSize(mixedPath: Path): null | undefined | number; + getDependencies(mixedPath: Path): null | undefined | Array; + getDifference(files: FileData): { + changedFiles: FileData; + removedFiles: Set; + }; + getSha1(mixedPath: Path): null | undefined | string; + getOrComputeSha1( + mixedPath: Path, + ): Promise; + exists(mixedPath: Path): boolean; + lookup(mixedPath: Path): LookupResult; + getAllFiles(): Array; + linkStats(mixedPath: Path): null | undefined | FileStats; + /** + * Given a search context, return a list of file paths matching the query. + * The query matches against normalized paths which start with `./`, + * for example: `a/b.js` -> `./a/b.js` + */ + matchFiles(opts: MatchFilesOptions): Iterable; + addOrModify(mixedPath: Path, metadata: FileMetadata): void; + bulkAddOrModify(addedOrModifiedFiles: FileData): void; + remove(mixedPath: Path): null | undefined | FileMetadata; + /** + * The core traversal algorithm of TreeFS - takes a normal path and traverses + * through a tree of maps keyed on path segments, returning the node, + * canonical path, and other metadata if successful, or the first missing + * segment otherwise. + * + * When a symlink is encountered, we set a new target of the symlink's + * normalised target path plus the remainder of the original target path. In + * this way, the eventual target path in a successful lookup has all symlinks + * resolved, and gives us the real path "for free". Similarly if a traversal + * fails, we automatically have the real path of the first non-existent node. + * + * Note that this code is extremely hot during resolution, being the most + * expensive part of a file existence check. Benchmark any modifications! + */ + _lookupByNormalPath( + requestedNormalPath: string, + opts?: { + collectAncestors?: Array<{ + ancestorOfRootIdx: null | undefined | number; + node: DirectoryNode; + normalPath: string; + segmentName: string; + }>; + collectLinkPaths?: null | undefined | Set; + followLeaf?: boolean; + makeDirectories?: boolean; + startPathIdx?: number; + startNode?: DirectoryNode; + start?: { + ancestorOfRootIdx: null | undefined | number; + node: DirectoryNode; + pathIdx: number; + }; + }, + ): + | { + ancestorOfRootIdx: null | undefined | number; + canonicalPath: string; + exists: true; + node: MixedNode; + parentNode: DirectoryNode; + } + | { + ancestorOfRootIdx: null | undefined | number; + canonicalPath: string; + exists: true; + node: DirectoryNode; + parentNode: null; + } + | { + canonicalMissingPath: string; + missingSegmentName: string; + exists: false; + }; + /** + * Given a start path (which need not exist), a subpath and type, and + * optionally a 'breakOnSegment', performs the following: + * + * X = mixedStartPath + * do + * if basename(X) === opts.breakOnSegment + * return null + * if X + subpath exists and has type opts.subpathType + * return { + * absolutePath: realpath(X + subpath) + * containerRelativePath: relative(mixedStartPath, X) + * } + * X = dirname(X) + * while X !== dirname(X) + * + * If opts.invalidatedBy is given, collects all absolute, real paths that if + * added or removed may invalidate this result. + * + * Useful for finding the closest package scope (subpath: package.json, + * type f, breakOnSegment: node_modules) or closest potential package root + * (subpath: node_modules/pkg, type: d) in Node.js resolution. + */ + hierarchicalLookup( + mixedStartPath: string, + subpath: string, + opts: { + breakOnSegment: null | undefined | string; + invalidatedBy: null | undefined | Set; + subpathType: 'f' | 'd'; + }, + ): null | undefined | {absolutePath: string; containerRelativePath: string}; + metadataIterator(opts: MetadataIteratorOptions): Iterable<{ + baseName: string; + canonicalPath: string; + metadata: FileMetadata; + }>; + _metadataIterator( + rootNode: DirectoryNode, + opts: MetadataIteratorOptions, + prefix?: string, + ): Iterable<{ + baseName: string; + canonicalPath: string; + metadata: FileMetadata; + }>; + _normalizePath(relativeOrAbsolutePath: Path): string; + /** + * Enumerate paths under a given node, including symlinks and through + * symlinks (if `follow` is enabled). + */ + _pathIterator( + iterationRootNode: DirectoryNode, + iterationRootParentNode: null | undefined | DirectoryNode, + ancestorOfRootIdx: null | undefined | number, + opts: PathIteratorOptions, + pathPrefix?: string, + followedLinks?: ReadonlySet, + ): Iterable; + _resolveSymlinkTargetToNormalPath( + symlinkNode: FileMetadata, + canonicalPathOfSymlink: Path, + ): NormalizedSymlinkTarget; + _getFileData( + filePath: Path, + opts?: GetFileDataOptions, + ): null | undefined | FileMetadata; + _cloneTree(root: DirectoryNode): DirectoryNode; +} +export default TreeFS; diff --git a/packages/metro-file-map/types/lib/checkWatchmanCapabilities.d.ts b/packages/metro-file-map/types/lib/checkWatchmanCapabilities.d.ts new file mode 100644 index 0000000000..b948ca101f --- /dev/null +++ b/packages/metro-file-map/types/lib/checkWatchmanCapabilities.d.ts @@ -0,0 +1,14 @@ +/** + * 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. + * + * @format + * + */ + +declare function checkWatchmanCapabilities( + requiredCapabilities: ReadonlyArray, +): Promise<{version: string}>; +export default checkWatchmanCapabilities; diff --git a/packages/metro-file-map/types/lib/dependencyExtractor.d.ts b/packages/metro-file-map/types/lib/dependencyExtractor.d.ts new file mode 100644 index 0000000000..8ab068a340 --- /dev/null +++ b/packages/metro-file-map/types/lib/dependencyExtractor.d.ts @@ -0,0 +1,15 @@ +/** + * 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. + * + * @format + * + */ + +export declare function extract(code: string): Set; +declare const $$EXPORT_DEFAULT_DECLARATION$$: {extract: typeof extract}; +declare type $$EXPORT_DEFAULT_DECLARATION$$ = + typeof $$EXPORT_DEFAULT_DECLARATION$$; +export default $$EXPORT_DEFAULT_DECLARATION$$; diff --git a/packages/metro-file-map/types/lib/normalizePathSeparatorsToPosix.d.ts b/packages/metro-file-map/types/lib/normalizePathSeparatorsToPosix.d.ts new file mode 100644 index 0000000000..7e118c07fd --- /dev/null +++ b/packages/metro-file-map/types/lib/normalizePathSeparatorsToPosix.d.ts @@ -0,0 +1,14 @@ +/** + * 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. + * + * @format + * + */ + +declare const $$EXPORT_DEFAULT_DECLARATION$$: {(filePath: string): string}; +declare type $$EXPORT_DEFAULT_DECLARATION$$ = + typeof $$EXPORT_DEFAULT_DECLARATION$$; +export default $$EXPORT_DEFAULT_DECLARATION$$; diff --git a/packages/metro-file-map/types/lib/normalizePathSeparatorsToSystem.d.ts b/packages/metro-file-map/types/lib/normalizePathSeparatorsToSystem.d.ts new file mode 100644 index 0000000000..7e118c07fd --- /dev/null +++ b/packages/metro-file-map/types/lib/normalizePathSeparatorsToSystem.d.ts @@ -0,0 +1,14 @@ +/** + * 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. + * + * @format + * + */ + +declare const $$EXPORT_DEFAULT_DECLARATION$$: {(filePath: string): string}; +declare type $$EXPORT_DEFAULT_DECLARATION$$ = + typeof $$EXPORT_DEFAULT_DECLARATION$$; +export default $$EXPORT_DEFAULT_DECLARATION$$; diff --git a/packages/metro-file-map/types/lib/rootRelativeCacheKeys.d.ts b/packages/metro-file-map/types/lib/rootRelativeCacheKeys.d.ts new file mode 100644 index 0000000000..8f82daada7 --- /dev/null +++ b/packages/metro-file-map/types/lib/rootRelativeCacheKeys.d.ts @@ -0,0 +1,17 @@ +/** + * 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. + * + * @format + * @oncall react_native + */ + +import type {BuildParameters} from '../flow-types'; + +declare function rootRelativeCacheKeys(buildParameters: BuildParameters): { + rootDirHash: string; + relativeConfigHash: string; +}; +export default rootRelativeCacheKeys; diff --git a/packages/metro-file-map/types/lib/sorting.d.ts b/packages/metro-file-map/types/lib/sorting.d.ts new file mode 100644 index 0000000000..e503c9aba4 --- /dev/null +++ b/packages/metro-file-map/types/lib/sorting.d.ts @@ -0,0 +1,16 @@ +/** + * 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. + * + * @format + */ + +export declare function compareStrings( + a: null | string, + b: null | string, +): number; +export declare function chainComparators( + ...comparators: Array<(a: T, b: T) => number> +): (a: T, b: T) => number; diff --git a/packages/metro-file-map/types/plugins/HastePlugin.d.ts b/packages/metro-file-map/types/plugins/HastePlugin.d.ts new file mode 100644 index 0000000000..a9c0cd64b7 --- /dev/null +++ b/packages/metro-file-map/types/plugins/HastePlugin.d.ts @@ -0,0 +1,89 @@ +/** + * 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. + * + * @format + * @oncall react_native + */ + +import type { + Console, + DuplicatesSet, + FileMapDelta, + FileMapPlugin, + FileMapPluginInitOptions, + FileMetadata, + HasteConflict, + HasteMap, + HasteMapItemMetadata, + HTypeValue, + Path, + PerfLogger, +} from '../flow-types'; + +type HasteMapOptions = Readonly<{ + console?: null | undefined | Console; + enableHastePackages: boolean; + perfLogger: null | undefined | PerfLogger; + platforms: ReadonlySet; + rootDir: Path; + failValidationOnConflicts: boolean; +}>; +declare class HastePlugin implements HasteMap, FileMapPlugin { + readonly name: 'haste'; + constructor(options: HasteMapOptions); + initialize(initOptions: FileMapPluginInitOptions): Promise; + getSerializableSnapshot(): null; + getModule( + name: string, + platform?: null | undefined | string, + supportsNativePlatform?: null | undefined | boolean, + type?: null | undefined | HTypeValue, + ): null | undefined | Path; + getPackage( + name: string, + platform: null | undefined | string, + _supportsNativePlatform?: null | undefined | boolean, + ): null | undefined | Path; + /** + * 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: null | undefined | string, + supportsNativePlatform: boolean, + ): HasteMapItemMetadata | null; + _assertNoDuplicates( + name: string, + platform: string, + supportsNativePlatform: boolean, + relativePathSet: null | undefined | DuplicatesSet, + ): void; + bulkUpdate(delta: FileMapDelta): Promise; + onNewOrModifiedFile( + relativeFilePath: string, + fileMetadata: FileMetadata, + ): void; + setModule(id: string, module: HasteMapItemMetadata): void; + onRemovedFile(relativeFilePath: string, fileMetadata: FileMetadata): void; + assertValid(): void; + /** + * 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): void; + computeConflicts(): Array; + getCacheKey(): string; +} +export default HastePlugin; diff --git a/packages/metro-file-map/types/plugins/MockPlugin.d.ts b/packages/metro-file-map/types/plugins/MockPlugin.d.ts new file mode 100644 index 0000000000..eaa9195464 --- /dev/null +++ b/packages/metro-file-map/types/plugins/MockPlugin.d.ts @@ -0,0 +1,42 @@ +/** + * 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. + * + * @format + * @oncall react_native + */ + +import type { + FileMapDelta, + FileMapPlugin, + FileMapPluginInitOptions, + MockMap as IMockMap, + Path, + RawMockMap, +} from '../flow-types'; + +export declare const CACHE_VERSION: 2; +export declare type CACHE_VERSION = typeof CACHE_VERSION; +declare class MockPlugin implements FileMapPlugin, IMockMap { + readonly name: 'mocks'; + constructor( + options: Readonly<{ + console: typeof console; + mocksPattern: RegExp; + rawMockMap?: RawMockMap; + rootDir: Path; + throwOnModuleCollision: boolean; + }>, + ); + initialize(initOptions: FileMapPluginInitOptions): Promise; + getMockModule(name: string): null | undefined | Path; + bulkUpdate(delta: FileMapDelta): Promise; + onNewOrModifiedFile(relativeFilePath: Path): void; + onRemovedFile(relativeFilePath: Path): void; + getSerializableSnapshot(): RawMockMap; + assertValid(): void; + getCacheKey(): string; +} +export default MockPlugin; diff --git a/packages/metro-file-map/types/lib/DuplicateHasteCandidatesError.d.ts b/packages/metro-file-map/types/plugins/haste/DuplicateHasteCandidatesError.d.ts similarity index 79% rename from packages/metro-file-map/types/lib/DuplicateHasteCandidatesError.d.ts rename to packages/metro-file-map/types/plugins/haste/DuplicateHasteCandidatesError.d.ts index dc99f23a02..f66d95da0e 100644 --- a/packages/metro-file-map/types/lib/DuplicateHasteCandidatesError.d.ts +++ b/packages/metro-file-map/types/plugins/haste/DuplicateHasteCandidatesError.d.ts @@ -8,9 +8,9 @@ * @oncall react_native */ -import type {DuplicatesSet} from '../flow-types'; +import type {DuplicatesSet} from '../../flow-types'; -export class DuplicateHasteCandidatesError extends Error { +export declare class DuplicateHasteCandidatesError extends Error { hasteName: string; platform: string | null; supportsNativePlatform: boolean; diff --git a/packages/metro-file-map/types/plugins/haste/HasteConflictsError.d.ts b/packages/metro-file-map/types/plugins/haste/HasteConflictsError.d.ts new file mode 100644 index 0000000000..fe78a34b60 --- /dev/null +++ b/packages/metro-file-map/types/plugins/haste/HasteConflictsError.d.ts @@ -0,0 +1,16 @@ +/** + * 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. + * + * @format + * @oncall react_native + */ + +import type {HasteConflict} from '../../flow-types'; + +export declare class HasteConflictsError extends Error { + constructor(conflicts: ReadonlyArray); + getDetailedMessage(pathsRelativeToRoot: null | undefined | string): string; +} diff --git a/packages/metro-file-map/types/plugins/haste/computeConflicts.d.ts b/packages/metro-file-map/types/plugins/haste/computeConflicts.d.ts new file mode 100644 index 0000000000..fcd4195fb3 --- /dev/null +++ b/packages/metro-file-map/types/plugins/haste/computeConflicts.d.ts @@ -0,0 +1,27 @@ +/** + * 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. + * + * @format + */ + +import type {HasteMapItem} from '../../flow-types'; + +type Conflict = { + id: string; + platform: string | null; + absolutePaths: Array; + type: 'duplicate' | 'shadowing'; +}; +export declare function computeHasteConflicts( + options: Readonly<{ + duplicates: ReadonlyMap< + string, + ReadonlyMap> + >; + map: ReadonlyMap; + rootDir: string; + }>, +): Array; diff --git a/packages/metro-file-map/types/plugins/haste/getPlatformExtension.d.ts b/packages/metro-file-map/types/plugins/haste/getPlatformExtension.d.ts new file mode 100644 index 0000000000..6c219aeb34 --- /dev/null +++ b/packages/metro-file-map/types/plugins/haste/getPlatformExtension.d.ts @@ -0,0 +1,15 @@ +/** + * 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. + * + * @format + * + */ + +declare function getPlatformExtension( + file: string, + platforms: ReadonlySet, +): null | undefined | string; +export default getPlatformExtension; diff --git a/packages/metro-file-map/types/plugins/mocks/getMockName.d.ts b/packages/metro-file-map/types/plugins/mocks/getMockName.d.ts new file mode 100644 index 0000000000..7e118c07fd --- /dev/null +++ b/packages/metro-file-map/types/plugins/mocks/getMockName.d.ts @@ -0,0 +1,14 @@ +/** + * 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. + * + * @format + * + */ + +declare const $$EXPORT_DEFAULT_DECLARATION$$: {(filePath: string): string}; +declare type $$EXPORT_DEFAULT_DECLARATION$$ = + typeof $$EXPORT_DEFAULT_DECLARATION$$; +export default $$EXPORT_DEFAULT_DECLARATION$$; diff --git a/packages/metro-file-map/types/watchers/AbstractWatcher.d.ts b/packages/metro-file-map/types/watchers/AbstractWatcher.d.ts new file mode 100644 index 0000000000..87eb65fbc6 --- /dev/null +++ b/packages/metro-file-map/types/watchers/AbstractWatcher.d.ts @@ -0,0 +1,35 @@ +/** + * 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. + * + * @format + * + */ + +import type { + WatcherBackend, + WatcherBackendChangeEvent, + WatcherBackendOptions, +} from '../flow-types'; + +export type Listeners = Readonly<{ + onFileEvent: (event: WatcherBackendChangeEvent) => void; + onError: (error: Error) => void; +}>; +export declare class AbstractWatcher implements WatcherBackend { + readonly root: string; + readonly ignored: null | undefined | RegExp; + readonly globs: ReadonlyArray; + readonly dot: boolean; + readonly doIgnore: (path: string) => boolean; + constructor(dir: string, opts: WatcherBackendOptions); + onFileEvent(listener: (event: WatcherBackendChangeEvent) => void): () => void; + onError(listener: (error: Error) => void): () => void; + startWatching(): Promise; + stopWatching(): Promise; + emitFileEvent(event: Omit): void; + emitError(error: Error): void; + getPauseReason(): null | undefined | string; +} diff --git a/packages/metro-file-map/types/watchers/FallbackWatcher.d.ts b/packages/metro-file-map/types/watchers/FallbackWatcher.d.ts new file mode 100644 index 0000000000..ea94a61093 --- /dev/null +++ b/packages/metro-file-map/types/watchers/FallbackWatcher.d.ts @@ -0,0 +1,98 @@ +/** + * 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. + * + * @format + * @oncall react_native + */ + +/** + * Originally vendored from https://github.com/amasad/sane/blob/64ff3a870c42e84f744086884bf55a4f9c22d376/src/node_watcher.js + */ + +import type { + ChangeEventMetadata, + WatcherBackendChangeEvent, +} from '../flow-types'; +import type {FSWatcher} from 'fs'; + +import {AbstractWatcher} from './AbstractWatcher'; + +declare class FallbackWatcher extends AbstractWatcher { + readonly _dirRegistry: {[directory: string]: {[file: string]: true}}; + readonly watched: {[key: string]: FSWatcher}; + startWatching(): Promise; + /** + * Register files that matches our globs to know what to type of event to + * emit in the future. + * + * Registry looks like the following: + * + * dirRegister => Map { + * dirpath => Map { + * filename => true + * } + * } + * + * Return false if ignored or already registered. + */ + _register(filepath: string, type: ChangeEventMetadata['type']): boolean; + /** + * Removes a file from the registry. + */ + _unregister(filepath: string): void; + /** + * Removes a dir from the registry. + */ + _unregisterDir(dirpath: string): void; + /** + * Checks if a file or directory exists in the registry. + */ + _registered(fullpath: string): boolean; + /** + * Emit "error" event if it's not an ignorable event + */ + _checkedEmitError: (error: Error) => void; + /** + * Watch a directory. + */ + _watchdir: (dir: string) => boolean; + /** + * Stop watching a directory. + */ + _stopWatching(dir: string): Promise; + /** + * End watching. + */ + stopWatching(): Promise; + /** + * On some platforms, as pointed out on the fs docs (most likely just win32) + * the file argument might be missing from the fs event. Try to detect what + * change by detecting if something was deleted or the most recent file change. + */ + _detectChangedFile( + dir: string, + event: string, + callback: (file: string) => void, + ): void; + /** + * Normalize fs events and pass it on to be processed. + */ + _normalizeChange(dir: string, event: string, file: string): void; + /** + * Process changes. + */ + _processChange(dir: string, event: string, file: string): void; + /** + * Emits the given event after debouncing, to emit only the latest + * information when we receive several events in quick succession. E.g., + * Linux emits two events for every new file. + * + * See also note above for DEBOUNCE_MS. + */ + _emitEvent(change: Omit): void; + getPauseReason(): null | undefined | string; +} +export default FallbackWatcher; diff --git a/packages/metro-file-map/types/watchers/NativeWatcher.d.ts b/packages/metro-file-map/types/watchers/NativeWatcher.d.ts new file mode 100644 index 0000000000..8cb5aff063 --- /dev/null +++ b/packages/metro-file-map/types/watchers/NativeWatcher.d.ts @@ -0,0 +1,49 @@ +/** + * 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. + * + * @format + * + */ + +import {AbstractWatcher} from './AbstractWatcher'; +/** + * NativeWatcher uses Node's native fs.watch API with recursive: true. + * + * Supported on macOS (and potentially Windows), because both natively have a + * concept of recurisve watching, via FSEvents and ReadDirectoryChangesW + * respectively. Notably Linux lacks this capability at the OS level. + * + * Node.js has at times supported the `recursive` option to fs.watch on Linux + * by walking the directory tree and creating a watcher on each directory, but + * this fits poorly with the synchronous `watch` API - either it must block for + * arbitrarily large IO, or it may drop changes after `watch` returns. See: + * https://github.com/nodejs/node/issues/48437 + * + * Therefore, we retain a fallback to our own application-level recursive + * FallbackWatcher for Linux, which has async `startWatching`. + * + * On Windows, this watcher could be used in principle, but needs work around + * some Windows-specific edge cases handled in FallbackWatcher, like + * deduping file change events, ignoring directory changes, and handling EPERM. + */ +declare class NativeWatcher extends AbstractWatcher { + static isSupported(): boolean; + constructor( + dir: string, + opts: Readonly<{ + ignored: null | undefined | RegExp; + globs: ReadonlyArray; + dot: boolean; + }>, + ); + startWatching(): Promise; + /** + * End watching. + */ + stopWatching(): Promise; + _handleEvent(relativePath: string): void; +} +export default NativeWatcher; diff --git a/packages/metro-file-map/types/watchers/RecrawlWarning.d.ts b/packages/metro-file-map/types/watchers/RecrawlWarning.d.ts new file mode 100644 index 0000000000..a106b22b43 --- /dev/null +++ b/packages/metro-file-map/types/watchers/RecrawlWarning.d.ts @@ -0,0 +1,25 @@ +/** + * 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. + * + * @format + * @oncall react_native + */ + +/** + * Originally vendored from + * https://github.com/amasad/sane/blob/64ff3a870c42e84f744086884bf55a4f9c22d376/src/utils/recrawl-warning-dedupe.js + */ + +declare class RecrawlWarning { + static RECRAWL_WARNINGS: Array; + static REGEXP: RegExp; + root: string; + count: number; + constructor(root: string, count: number); + static findByRoot(root: string): null | undefined | RecrawlWarning; + static isRecrawlWarningDupe(warningMessage: unknown): boolean; +} +export default RecrawlWarning; diff --git a/packages/metro-file-map/types/watchers/WatchmanWatcher.d.ts b/packages/metro-file-map/types/watchers/WatchmanWatcher.d.ts new file mode 100644 index 0000000000..83c1dd25e0 --- /dev/null +++ b/packages/metro-file-map/types/watchers/WatchmanWatcher.d.ts @@ -0,0 +1,53 @@ +/** + * 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. + * + * @format + * @oncall react_native + */ + +import type {WatcherOptions} from './common'; +import type { + Client, + WatchmanFileChange, + WatchmanSubscriptionEvent, +} from 'fb-watchman'; + +import {AbstractWatcher} from './AbstractWatcher'; +/** + * Watches `dir`. + */ +declare class WatchmanWatcher extends AbstractWatcher { + client: Client; + readonly subscriptionName: string; + watchProjectInfo: + | null + | undefined + | Readonly<{relativePath: string; root: string}>; + readonly watchmanDeferStates: ReadonlyArray; + constructor(dir: string, opts: WatcherOptions); + startWatching(): Promise; + /** + * Run the watchman `watch` command on the root and subscribe to changes. + */ + _init(onReady: () => void, onError: (error: Error) => void): void; + /** + * Handles a change event coming from the subscription. + */ + _handleChangeEvent(resp: WatchmanSubscriptionEvent): void; + /** + * Handles a single change event record. + */ + _handleFileChange( + changeDescriptor: WatchmanFileChange, + rawClock: WatchmanSubscriptionEvent['clock'], + ): void; + /** + * Closes the watcher. + */ + stopWatching(): Promise; + getPauseReason(): null | undefined | string; +} +export default WatchmanWatcher; diff --git a/packages/metro-file-map/types/watchers/common.d.ts b/packages/metro-file-map/types/watchers/common.d.ts new file mode 100644 index 0000000000..bdfa8d83df --- /dev/null +++ b/packages/metro-file-map/types/watchers/common.d.ts @@ -0,0 +1,61 @@ +/** + * 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. + * + * @format + * @oncall react_native + */ + +/** + * Originally vendored from + * https://github.com/amasad/sane/blob/64ff3a870c42e84f744086884bf55a4f9c22d376/src/common.js + */ + +import type {ChangeEventMetadata} from '../flow-types'; +import type {Stats} from 'fs'; +/** + * Constants + */ +export declare const DELETE_EVENT: 'delete'; +export declare type DELETE_EVENT = typeof DELETE_EVENT; +export declare const TOUCH_EVENT: 'touch'; +export declare type TOUCH_EVENT = typeof TOUCH_EVENT; +export declare const ALL_EVENT: 'all'; +export declare type ALL_EVENT = typeof ALL_EVENT; +export type WatcherOptions = Readonly<{ + globs: ReadonlyArray; + dot: boolean; + ignored: null | undefined | RegExp; + watchmanDeferStates: ReadonlyArray; + watchman?: unknown; + watchmanPath?: string; +}>; +/** + * Checks a file relative path against the globs array. + */ +export declare function includedByGlob( + type: null | undefined | ('f' | 'l' | 'd'), + globs: ReadonlyArray, + dot: boolean, + relativePath: string, +): boolean; +/** + * Whether the given filePath matches the given RegExp, after converting + * (on Windows only) system separators to posix separators. + * + * Conversion to posix is for backwards compatibility with the previous + * anymatch matcher, which normlises all inputs[1]. This may not be consistent + * with other parts of metro-file-map. + * + * [1]: https://github.com/micromatch/anymatch/blob/3.1.1/index.js#L50 + */ +export declare const posixPathMatchesPattern: ( + pattern: RegExp, + filePath: string, +) => boolean; +export declare type posixPathMatchesPattern = typeof posixPathMatchesPattern; +export declare function typeFromStat( + stat: Stats, +): null | undefined | ChangeEventMetadata['type']; diff --git a/packages/metro-file-map/types/workerExclusionList.d.ts b/packages/metro-file-map/types/workerExclusionList.d.ts new file mode 100644 index 0000000000..2bfc8ca2ca --- /dev/null +++ b/packages/metro-file-map/types/workerExclusionList.d.ts @@ -0,0 +1,16 @@ +/** + * 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. + * + * @format + * + */ + +export declare const extensions: ReadonlySet; +export declare type extensions = typeof extensions; +declare const $$EXPORT_DEFAULT_DECLARATION$$: typeof extensions; +declare type $$EXPORT_DEFAULT_DECLARATION$$ = + typeof $$EXPORT_DEFAULT_DECLARATION$$; +export default $$EXPORT_DEFAULT_DECLARATION$$; diff --git a/scripts/generateTypeScriptDefinitions.js b/scripts/generateTypeScriptDefinitions.js index 44efdfeb07..2f6c1c9f27 100644 --- a/scripts/generateTypeScriptDefinitions.js +++ b/scripts/generateTypeScriptDefinitions.js @@ -34,6 +34,7 @@ export const AUTO_GENERATED_PATTERNS: $ReadOnlyArray = [ 'packages/metro-resolver/**', 'packages/metro-source-map/**', 'packages/metro-transform-worker/**', + 'packages/metro-file-map/**', 'packages/ob1/**', ]; @@ -47,6 +48,7 @@ const IGNORED_PATTERNS = [ '**/node_modules/**', 'packages/metro-babel-register/**', 'packages/*/build/**', + 'packages/metro-file-map/src/worker.js', 'packages/metro/src/integration_tests/**', ];