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>
This commit is contained in:
2026-04-25 00:08:01 -04:00
parent 65b552bb08
commit 7c684a42cc
48450 changed files with 5679671 additions and 383 deletions

View File

@@ -0,0 +1,41 @@
/**
* 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
* @generated SignedSource<<ba8a5de14ca08c751a87bea6b356a670>>
*
* This file was translated from Flow by scripts/generateTypeScriptDefinitions.js
* Original file: packages/metro-file-map/src/watchers/AbstractWatcher.js
* To regenerate, run:
* js1 build metro-ts-defs (internal) OR
* yarn run build-ts-defs (OSS)
*/
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<string>;
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<void>;
stopWatching(): Promise<void>;
emitFileEvent(event: Omit<WatcherBackendChangeEvent, 'root'>): void;
emitError(error: Error): void;
getPauseReason(): null | undefined | string;
}

View File

@@ -0,0 +1,81 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true,
});
exports.AbstractWatcher = void 0;
var _common = require("./common");
var _events = _interopRequireDefault(require("events"));
var path = _interopRequireWildcard(require("path"));
function _interopRequireWildcard(e, t) {
if ("function" == typeof WeakMap)
var r = new WeakMap(),
n = new WeakMap();
return (_interopRequireWildcard = function (e, t) {
if (!t && e && e.__esModule) return e;
var o,
i,
f = { __proto__: null, default: e };
if (null === e || ("object" != typeof e && "function" != typeof e))
return f;
if ((o = t ? n : r)) {
if (o.has(e)) return o.get(e);
o.set(e, f);
}
for (const t in e)
"default" !== t &&
{}.hasOwnProperty.call(e, t) &&
((i =
(o = Object.defineProperty) &&
Object.getOwnPropertyDescriptor(e, t)) &&
(i.get || i.set)
? o(f, t, i)
: (f[t] = e[t]));
return f;
})(e, t);
}
function _interopRequireDefault(e) {
return e && e.__esModule ? e : { default: e };
}
class AbstractWatcher {
#emitter = new _events.default();
constructor(dir, opts) {
const { ignored, globs, dot } = opts;
this.dot = dot || false;
this.ignored = ignored;
this.globs = globs;
this.doIgnore = ignored
? (filePath) => (0, _common.posixPathMatchesPattern)(ignored, filePath)
: () => false;
this.root = path.resolve(dir);
}
onFileEvent(listener) {
this.#emitter.on("fileevent", listener);
return () => {
this.#emitter.removeListener("fileevent", listener);
};
}
onError(listener) {
this.#emitter.on("error", listener);
return () => {
this.#emitter.removeListener("error", listener);
};
}
async startWatching() {}
async stopWatching() {
this.#emitter.removeAllListeners();
}
emitFileEvent(event) {
this.#emitter.emit("fileevent", {
...event,
root: this.root,
});
}
emitError(error) {
this.#emitter.emit("error", error);
}
getPauseReason() {
return null;
}
}
exports.AbstractWatcher = AbstractWatcher;

View File

@@ -0,0 +1,85 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
*/
import type {
WatcherBackend,
WatcherBackendChangeEvent,
WatcherBackendOptions,
} from '../flow-types';
import {posixPathMatchesPattern} from './common';
import EventEmitter from 'events';
import * as path from 'path';
export type Listeners = Readonly<{
onFileEvent: (event: WatcherBackendChangeEvent) => void,
onError: (error: Error) => void,
}>;
export class AbstractWatcher implements WatcherBackend {
+root: string;
+ignored: ?RegExp;
+globs: ReadonlyArray<string>;
+dot: boolean;
+doIgnore: (path: string) => boolean;
#emitter: EventEmitter = new EventEmitter();
constructor(dir: string, opts: WatcherBackendOptions) {
const {ignored, globs, dot} = opts;
this.dot = dot || false;
this.ignored = ignored;
this.globs = globs;
this.doIgnore = ignored
? (filePath: string) => posixPathMatchesPattern(ignored, filePath)
: () => false;
this.root = path.resolve(dir);
}
onFileEvent(
listener: (event: WatcherBackendChangeEvent) => void,
): () => void {
this.#emitter.on('fileevent', listener);
return () => {
this.#emitter.removeListener('fileevent', listener);
};
}
onError(listener: (error: Error) => void): () => void {
this.#emitter.on('error', listener);
return () => {
this.#emitter.removeListener('error', listener);
};
}
async startWatching(): Promise<void> {
// Must be implemented by subclasses
}
async stopWatching(): Promise<void> {
this.#emitter.removeAllListeners();
}
emitFileEvent(event: Omit<WatcherBackendChangeEvent, 'root'>) {
this.#emitter.emit('fileevent', {
...event,
root: this.root,
});
}
emitError(error: Error) {
this.#emitter.emit('error', error);
}
getPauseReason(): ?string {
return null;
}
}

View File

@@ -0,0 +1,28 @@
/**
* 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
* @oncall react_native
* @generated SignedSource<<5152d1919d3373e4df611e0fca805e1c>>
*
* This file was translated from Flow by scripts/generateTypeScriptDefinitions.js
* Original file: packages/metro-file-map/src/watchers/FallbackWatcher.js
* To regenerate, run:
* js1 build metro-ts-defs (internal) OR
* yarn run build-ts-defs (OSS)
*/
import {AbstractWatcher} from './AbstractWatcher';
declare class FallbackWatcher extends AbstractWatcher {
startWatching(): Promise<void>;
/**
* End watching.
*/
stopWatching(): Promise<void>;
getPauseReason(): null | undefined | string;
}
export default FallbackWatcher;

View File

@@ -0,0 +1,379 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true,
});
exports.default = void 0;
var _AbstractWatcher = require("./AbstractWatcher");
var common = _interopRequireWildcard(require("./common"));
var _fs = _interopRequireDefault(require("fs"));
var _os = _interopRequireDefault(require("os"));
var _path = _interopRequireDefault(require("path"));
var _walker = _interopRequireDefault(require("walker"));
function _interopRequireDefault(e) {
return e && e.__esModule ? e : { default: e };
}
function _interopRequireWildcard(e, t) {
if ("function" == typeof WeakMap)
var r = new WeakMap(),
n = new WeakMap();
return (_interopRequireWildcard = function (e, t) {
if (!t && e && e.__esModule) return e;
var o,
i,
f = { __proto__: null, default: e };
if (null === e || ("object" != typeof e && "function" != typeof e))
return f;
if ((o = t ? n : r)) {
if (o.has(e)) return o.get(e);
o.set(e, f);
}
for (const t in e)
"default" !== t &&
{}.hasOwnProperty.call(e, t) &&
((i =
(o = Object.defineProperty) &&
Object.getOwnPropertyDescriptor(e, t)) &&
(i.get || i.set)
? o(f, t, i)
: (f[t] = e[t]));
return f;
})(e, t);
}
const platform = _os.default.platform();
const fsPromises = _fs.default.promises;
const TOUCH_EVENT = common.TOUCH_EVENT;
const DELETE_EVENT = common.DELETE_EVENT;
const DEBOUNCE_MS = 100;
class FallbackWatcher extends _AbstractWatcher.AbstractWatcher {
#changeTimers = new Map();
#dirRegistry = Object.create(null);
#watched = Object.create(null);
async startWatching() {
this.#watchdir(this.root);
await new Promise((resolve) => {
recReaddir(
this.root,
(dir) => {
this.#watchdir(dir);
},
(filename) => {
this.#register(filename, "f");
},
(symlink) => {
this.#register(symlink, "l");
},
() => {
resolve();
},
this.#checkedEmitError,
this.ignored,
);
});
}
#register(filepath, type) {
const dir = _path.default.dirname(filepath);
const filename = _path.default.basename(filepath);
if (this.#dirRegistry[dir] && this.#dirRegistry[dir][filename]) {
return false;
}
const relativePath = _path.default.relative(this.root, filepath);
if (
this.doIgnore(relativePath) ||
(type === "f" &&
!common.includedByGlob("f", this.globs, this.dot, relativePath))
) {
return false;
}
if (!this.#dirRegistry[dir]) {
this.#dirRegistry[dir] = Object.create(null);
}
this.#dirRegistry[dir][filename] = true;
return true;
}
#unregister(filepath) {
const dir = _path.default.dirname(filepath);
if (this.#dirRegistry[dir]) {
const filename = _path.default.basename(filepath);
delete this.#dirRegistry[dir][filename];
}
}
#unregisterDir(dirpath) {
const removedFiles = [];
for (const registeredDir of Object.keys(this.#dirRegistry)) {
if (
registeredDir === dirpath ||
registeredDir.startsWith(dirpath + _path.default.sep)
) {
for (const filename of Object.keys(this.#dirRegistry[registeredDir])) {
removedFiles.push(_path.default.join(registeredDir, filename));
}
delete this.#dirRegistry[registeredDir];
}
}
return removedFiles;
}
#registered(fullpath) {
const dir = _path.default.dirname(fullpath);
return !!(
this.#dirRegistry[fullpath] ||
(this.#dirRegistry[dir] &&
this.#dirRegistry[dir][_path.default.basename(fullpath)])
);
}
#checkedEmitError = (error) => {
if (!isIgnorableFileError(error)) {
this.emitError(error);
}
};
#watchdir = (dir) => {
if (this.#watched[dir]) {
return false;
}
const watcher = _fs.default.watch(
dir,
{
persistent: true,
},
(event, filename) => this.#normalizeChange(dir, event, filename),
);
this.#watched[dir] = watcher;
watcher.on("error", this.#checkedEmitError);
if (this.root !== dir) {
this.#register(dir, "d");
}
return true;
};
async #stopWatching(dir) {
if (this.#watched[dir]) {
await new Promise((resolve) => {
this.#watched[dir].once("close", () => process.nextTick(resolve));
this.#watched[dir].close();
delete this.#watched[dir];
});
}
}
async stopWatching() {
await super.stopWatching();
const promises = Object.keys(this.#watched).map((dir) =>
this.#stopWatching(dir),
);
await Promise.all(promises);
}
#detectChangedFile(dir, event, callback) {
if (!this.#dirRegistry[dir]) {
return;
}
let found = false;
let closest = null;
let c = 0;
Object.keys(this.#dirRegistry[dir]).forEach((file, i, arr) => {
_fs.default.lstat(_path.default.join(dir, file), (error, stat) => {
if (found) {
return;
}
if (error) {
if (isIgnorableFileError(error)) {
found = true;
callback(file);
} else {
this.emitError(error);
}
} else {
if (closest == null || stat.mtime > closest.mtime) {
closest = {
file,
mtime: stat.mtime,
};
}
if (arr.length === ++c) {
callback(closest.file);
}
}
});
});
}
#normalizeChange(dir, event, file) {
if (!file) {
this.#detectChangedFile(dir, event, (actualFile) => {
if (actualFile) {
this.#processChange(dir, event, actualFile).catch((error) =>
this.emitError(error),
);
}
});
} else {
this.#processChange(dir, event, _path.default.normalize(file)).catch(
(error) => this.emitError(error),
);
}
}
async #processChange(dir, event, file) {
const fullPath = _path.default.join(dir, file);
const relativePath = _path.default.join(
_path.default.relative(this.root, dir),
file,
);
const registered = this.#registered(fullPath);
try {
const stat = await fsPromises.lstat(fullPath);
if (stat.isDirectory()) {
if (event === "change") {
return;
}
if (
this.doIgnore(relativePath) ||
!common.includedByGlob("d", this.globs, this.dot, relativePath)
) {
return;
}
recReaddir(
_path.default.resolve(this.root, relativePath),
(dir, stats) => {
if (this.#watchdir(dir)) {
this.#emitEvent({
event: TOUCH_EVENT,
relativePath: _path.default.relative(this.root, dir),
metadata: {
modifiedTime: stats.mtime.getTime(),
size: stats.size,
type: "d",
},
});
}
},
(file, stats) => {
if (this.#register(file, "f")) {
this.#emitEvent({
event: TOUCH_EVENT,
relativePath: _path.default.relative(this.root, file),
metadata: {
modifiedTime: stats.mtime.getTime(),
size: stats.size,
type: "f",
},
});
}
},
(symlink, stats) => {
if (this.#register(symlink, "l")) {
this.emitFileEvent({
event: TOUCH_EVENT,
relativePath: _path.default.relative(this.root, symlink),
metadata: {
modifiedTime: stats.mtime.getTime(),
size: stats.size,
type: "l",
},
});
}
},
function endCallback() {},
this.#checkedEmitError,
this.ignored,
);
} else {
const type = common.typeFromStat(stat);
if (type == null) {
return;
}
const metadata = {
modifiedTime: stat.mtime.getTime(),
size: stat.size,
type,
};
if (registered) {
this.#emitEvent({
event: TOUCH_EVENT,
relativePath,
metadata,
});
} else {
if (this.#register(fullPath, type)) {
this.#emitEvent({
event: TOUCH_EVENT,
relativePath,
metadata,
});
}
}
}
} catch (error) {
if (!isIgnorableFileError(error)) {
this.emitError(error);
return;
}
this.#unregister(fullPath);
const removedFiles = this.#unregisterDir(fullPath);
for (const removedFile of removedFiles) {
this.#emitEvent({
event: DELETE_EVENT,
relativePath: _path.default.relative(this.root, removedFile),
});
}
if (registered) {
this.#emitEvent({
event: DELETE_EVENT,
relativePath,
});
}
await this.#stopWatching(fullPath);
}
}
#emitEvent(change) {
const { event, relativePath } = change;
const key = event + "-" + relativePath;
const existingTimer = this.#changeTimers.get(key);
if (existingTimer) {
clearTimeout(existingTimer);
}
this.#changeTimers.set(
key,
setTimeout(() => {
this.#changeTimers.delete(key);
this.emitFileEvent(change);
}, DEBOUNCE_MS),
);
}
getPauseReason() {
return null;
}
}
exports.default = FallbackWatcher;
function isIgnorableFileError(error) {
return (
error.code === "ENOENT" || (error.code === "EPERM" && platform === "win32")
);
}
function recReaddir(
dir,
dirCallback,
fileCallback,
symlinkCallback,
endCallback,
errorCallback,
ignored,
) {
const walk = (0, _walker.default)(dir);
if (ignored) {
walk.filterDir(
(currentDir) => !common.posixPathMatchesPattern(ignored, currentDir),
);
}
walk
.on("dir", normalizeProxy(dirCallback))
.on("file", normalizeProxy(fileCallback))
.on("symlink", normalizeProxy(symlinkCallback))
.on("error", errorCallback)
.on("end", () => {
if (platform === "win32") {
setTimeout(endCallback, 1000);
} else {
endCallback();
}
});
}
function normalizeProxy(callback) {
return (filepath, stats) =>
callback(_path.default.normalize(filepath), stats);
}

View File

@@ -0,0 +1,469 @@
/**
* 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
*/
/**
* Originally vendored from https://github.com/amasad/sane/blob/64ff3a870c42e84f744086884bf55a4f9c22d376/src/node_watcher.js
*/
import type {
ChangeEventMetadata,
WatcherBackendChangeEvent,
} from '../flow-types';
import type {FSWatcher, Stats} from 'fs';
import {AbstractWatcher} from './AbstractWatcher';
import * as common from './common';
import fs from 'fs';
import os from 'os';
import path from 'path';
// $FlowFixMe[untyped-import] - Write libdefs for `walker`
import walker from 'walker';
const platform = os.platform();
const fsPromises = fs.promises;
const TOUCH_EVENT = common.TOUCH_EVENT;
const DELETE_EVENT = common.DELETE_EVENT;
/**
* This setting delays all events. It suppresses 'change' events that
* immediately follow an 'add', and debounces successive 'change' events to
* only emit the latest.
*/
const DEBOUNCE_MS = 100;
export default class FallbackWatcher extends AbstractWatcher {
+#changeTimers: Map<string, TimeoutID> = new Map();
+#dirRegistry: {
[directory: string]: {[file: string]: true, __proto__: null},
__proto__: null,
} = Object.create(null);
+#watched: {[key: string]: FSWatcher, __proto__: null} = Object.create(null);
async startWatching(): Promise<void> {
this.#watchdir(this.root);
await new Promise(resolve => {
recReaddir(
this.root,
dir => {
this.#watchdir(dir);
},
filename => {
this.#register(filename, 'f');
},
symlink => {
this.#register(symlink, 'l');
},
() => {
resolve();
},
this.#checkedEmitError,
this.ignored,
);
});
}
/**
* 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 {
const dir = path.dirname(filepath);
const filename = path.basename(filepath);
if (this.#dirRegistry[dir] && this.#dirRegistry[dir][filename]) {
return false;
}
const relativePath = path.relative(this.root, filepath);
if (
this.doIgnore(relativePath) ||
(type === 'f' &&
!common.includedByGlob('f', this.globs, this.dot, relativePath))
) {
return false;
}
if (!this.#dirRegistry[dir]) {
this.#dirRegistry[dir] = Object.create(null);
}
this.#dirRegistry[dir][filename] = true;
return true;
}
/**
* Removes a file from the registry.
*/
#unregister(filepath: string) {
const dir = path.dirname(filepath);
if (this.#dirRegistry[dir]) {
const filename = path.basename(filepath);
delete this.#dirRegistry[dir][filename];
}
}
/**
* Removes a dir from the registry, returning all files that were registered
* under it (recursively).
*/
#unregisterDir(dirpath: string): Array<string> {
const removedFiles: Array<string> = [];
// Find and remove all entries under this directory
for (const registeredDir of Object.keys(this.#dirRegistry)) {
if (
registeredDir === dirpath ||
registeredDir.startsWith(dirpath + path.sep)
) {
// Collect all files in this directory
for (const filename of Object.keys(this.#dirRegistry[registeredDir])) {
removedFiles.push(path.join(registeredDir, filename));
}
delete this.#dirRegistry[registeredDir];
}
}
return removedFiles;
}
/**
* Checks if a file or directory exists in the registry.
*/
#registered(fullpath: string): boolean {
const dir = path.dirname(fullpath);
return !!(
this.#dirRegistry[fullpath] ||
(this.#dirRegistry[dir] &&
this.#dirRegistry[dir][path.basename(fullpath)])
);
}
/**
* Emit "error" event if it's not an ignorable event
*/
#checkedEmitError: (error: Error) => void = error => {
if (!isIgnorableFileError(error)) {
this.emitError(error);
}
};
/**
* Watch a directory.
*/
#watchdir: (dir: string) => boolean = (dir: string) => {
if (this.#watched[dir]) {
return false;
}
const watcher = fs.watch(dir, {persistent: true}, (event, filename) =>
this.#normalizeChange(dir, event, filename),
);
this.#watched[dir] = watcher;
watcher.on('error', this.#checkedEmitError);
if (this.root !== dir) {
this.#register(dir, 'd');
}
return true;
};
/**
* Stop watching a directory.
*/
async #stopWatching(dir: string): Promise<void> {
if (this.#watched[dir]) {
await new Promise(resolve => {
this.#watched[dir].once('close', () => process.nextTick(resolve));
this.#watched[dir].close();
delete this.#watched[dir];
});
}
}
/**
* End watching.
*/
async stopWatching(): Promise<void> {
await super.stopWatching();
const promises = Object.keys(this.#watched).map(dir =>
this.#stopWatching(dir),
);
await Promise.all(promises);
}
/**
* 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,
) {
if (!this.#dirRegistry[dir]) {
return;
}
let found = false;
let closest: ?Readonly<{file: string, mtime: Stats['mtime']}> = null;
let c = 0;
Object.keys(this.#dirRegistry[dir]).forEach((file, i, arr) => {
fs.lstat(path.join(dir, file), (error, stat) => {
if (found) {
return;
}
if (error) {
if (isIgnorableFileError(error)) {
found = true;
callback(file);
} else {
this.emitError(error);
}
} else {
if (closest == null || stat.mtime > closest.mtime) {
closest = {file, mtime: stat.mtime};
}
if (arr.length === ++c) {
callback(closest.file);
}
}
});
});
}
/**
* Normalize fs events and pass it on to be processed.
*/
#normalizeChange(dir: string, event: string, file: string) {
if (!file) {
this.#detectChangedFile(dir, event, actualFile => {
if (actualFile) {
this.#processChange(dir, event, actualFile).catch(error =>
this.emitError(error),
);
}
});
} else {
this.#processChange(dir, event, path.normalize(file)).catch(error =>
this.emitError(error),
);
}
}
/**
* Process changes.
*/
async #processChange(dir: string, event: string, file: string) {
const fullPath = path.join(dir, file);
const relativePath = path.join(path.relative(this.root, dir), file);
const registered = this.#registered(fullPath);
try {
const stat = await fsPromises.lstat(fullPath);
if (stat.isDirectory()) {
// win32 emits usless change events on dirs.
if (event === 'change') {
return;
}
if (
this.doIgnore(relativePath) ||
!common.includedByGlob('d', this.globs, this.dot, relativePath)
) {
return;
}
recReaddir(
path.resolve(this.root, relativePath),
(dir, stats) => {
if (this.#watchdir(dir)) {
this.#emitEvent({
event: TOUCH_EVENT,
relativePath: path.relative(this.root, dir),
metadata: {
modifiedTime: stats.mtime.getTime(),
size: stats.size,
type: 'd',
},
});
}
},
(file, stats) => {
if (this.#register(file, 'f')) {
this.#emitEvent({
event: TOUCH_EVENT,
relativePath: path.relative(this.root, file),
metadata: {
modifiedTime: stats.mtime.getTime(),
size: stats.size,
type: 'f',
},
});
}
},
(symlink, stats) => {
if (this.#register(symlink, 'l')) {
this.emitFileEvent({
event: TOUCH_EVENT,
relativePath: path.relative(this.root, symlink),
metadata: {
modifiedTime: stats.mtime.getTime(),
size: stats.size,
type: 'l',
},
});
}
},
function endCallback() {},
this.#checkedEmitError,
this.ignored,
);
} else {
const type = common.typeFromStat(stat);
if (type == null) {
return;
}
const metadata: ChangeEventMetadata = {
modifiedTime: stat.mtime.getTime(),
size: stat.size,
type,
};
if (registered) {
this.#emitEvent({event: TOUCH_EVENT, relativePath, metadata});
} else {
if (this.#register(fullPath, type)) {
this.#emitEvent({event: TOUCH_EVENT, relativePath, metadata});
}
}
}
} catch (error) {
if (!isIgnorableFileError(error)) {
this.emitError(error);
return;
}
this.#unregister(fullPath);
// When a directory is deleted, emit delete events for all files we
// knew about under that directory
const removedFiles = this.#unregisterDir(fullPath);
for (const removedFile of removedFiles) {
this.#emitEvent({
event: DELETE_EVENT,
relativePath: path.relative(this.root, removedFile),
});
}
if (registered) {
this.#emitEvent({event: DELETE_EVENT, relativePath});
}
await this.#stopWatching(fullPath);
}
}
/**
* 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<WatcherBackendChangeEvent, 'root'>) {
const {event, relativePath} = change;
const key = event + '-' + relativePath;
const existingTimer = this.#changeTimers.get(key);
if (existingTimer) {
clearTimeout(existingTimer);
}
this.#changeTimers.set(
key,
setTimeout(() => {
this.#changeTimers.delete(key);
this.emitFileEvent(change);
}, DEBOUNCE_MS),
);
}
getPauseReason(): ?string {
return null;
}
}
/**
* Determine if a given FS error can be ignored
*/
function isIgnorableFileError(error: Error | {code: string}) {
return (
error.code === 'ENOENT' ||
// Workaround Windows EPERM on watched folder deletion, and when
// reading locked files (pending further writes or pending deletion).
// In such cases, we'll receive a subsequent event when the file is
// deleted or ready to read.
// https://github.com/facebook/metro/issues/1001
// https://github.com/nodejs/node-v0.x-archive/issues/4337
(error.code === 'EPERM' && platform === 'win32')
);
}
/**
* Traverse a directory recursively calling `callback` on every directory.
*/
function recReaddir(
dir: string,
dirCallback: (string, Stats) => void,
fileCallback: (string, Stats) => void,
symlinkCallback: (string, Stats) => void,
endCallback: () => void,
errorCallback: Error => void,
ignored: ?RegExp,
) {
const walk = walker(dir);
if (ignored) {
walk.filterDir(
(currentDir: string) =>
!common.posixPathMatchesPattern(ignored, currentDir),
);
}
walk
.on('dir', normalizeProxy(dirCallback))
.on('file', normalizeProxy(fileCallback))
.on('symlink', normalizeProxy(symlinkCallback))
.on('error', errorCallback)
.on('end', () => {
if (platform === 'win32') {
setTimeout(endCallback, 1000);
} else {
endCallback();
}
});
}
/**
* Returns a callback that when called will normalize a path and call the
* original callback
*/
function normalizeProxy<T>(
callback: (filepath: string, stats: Stats) => T,
): (string, Stats) => T {
return (filepath: string, stats: Stats) =>
callback(path.normalize(filepath), stats);
}

View File

@@ -0,0 +1,55 @@
/**
* 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
* @generated SignedSource<<b68c5620efd3f5bec83279059d0d1b4e>>
*
* This file was translated from Flow by scripts/generateTypeScriptDefinitions.js
* Original file: packages/metro-file-map/src/watchers/NativeWatcher.js
* To regenerate, run:
* js1 build metro-ts-defs (internal) OR
* yarn run build-ts-defs (OSS)
*/
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<string>;
dot: boolean;
}>,
);
startWatching(): Promise<void>;
/**
* End watching.
*/
stopWatching(): Promise<void>;
_handleEvent(event: string, relativePath: string): void;
}
export default NativeWatcher;

View File

@@ -0,0 +1,135 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true,
});
exports.default = void 0;
var _AbstractWatcher = require("./AbstractWatcher");
var _common = require("./common");
var _fs = require("fs");
var _os = require("os");
var path = _interopRequireWildcard(require("path"));
function _interopRequireWildcard(e, t) {
if ("function" == typeof WeakMap)
var r = new WeakMap(),
n = new WeakMap();
return (_interopRequireWildcard = function (e, t) {
if (!t && e && e.__esModule) return e;
var o,
i,
f = { __proto__: null, default: e };
if (null === e || ("object" != typeof e && "function" != typeof e))
return f;
if ((o = t ? n : r)) {
if (o.has(e)) return o.get(e);
o.set(e, f);
}
for (const t in e)
"default" !== t &&
{}.hasOwnProperty.call(e, t) &&
((i =
(o = Object.defineProperty) &&
Object.getOwnPropertyDescriptor(e, t)) &&
(i.get || i.set)
? o(f, t, i)
: (f[t] = e[t]));
return f;
})(e, t);
}
const debug = require("debug")("Metro:NativeWatcher");
const TOUCH_EVENT = "touch";
const DELETE_EVENT = "delete";
const RECRAWL_EVENT = "recrawl";
class NativeWatcher extends _AbstractWatcher.AbstractWatcher {
#fsWatcher;
static isSupported() {
return (0, _os.platform)() === "darwin";
}
constructor(dir, opts) {
if (!NativeWatcher.isSupported) {
throw new Error("This watcher can only be used on macOS");
}
super(dir, opts);
}
async startWatching() {
this.#fsWatcher = (0, _fs.watch)(
this.root,
{
persistent: false,
recursive: true,
},
(event, relativePath) => {
this._handleEvent(event, relativePath).catch((error) => {
this.emitError(error);
});
},
);
debug("Watching %s", this.root);
}
async stopWatching() {
await super.stopWatching();
if (this.#fsWatcher) {
this.#fsWatcher.close();
}
}
async _handleEvent(event, relativePath) {
const absolutePath = path.resolve(this.root, relativePath);
if (this.doIgnore(relativePath)) {
debug(
'Ignoring event "%s" on %s (root: %s)',
event,
relativePath,
this.root,
);
return;
}
debug(
'Handling event "%s" on %s (root: %s)',
event,
relativePath,
this.root,
);
try {
const stat = await _fs.promises.lstat(absolutePath);
const type = (0, _common.typeFromStat)(stat);
if (!type) {
return;
}
if (
!(0, _common.includedByGlob)(type, this.globs, this.dot, relativePath)
) {
return;
}
if (type === "d" && event === "rename") {
debug(
"Directory rename detected on %s, requesting recrawl",
relativePath,
);
this.emitFileEvent({
event: RECRAWL_EVENT,
relativePath,
});
return;
}
this.emitFileEvent({
event: TOUCH_EVENT,
relativePath,
metadata: {
type,
modifiedTime: stat.mtime.getTime(),
size: stat.size,
},
});
} catch (error) {
if (error?.code !== "ENOENT") {
this.emitError(error);
return;
}
this.emitFileEvent({
event: DELETE_EVENT,
relativePath,
});
}
}
}
exports.default = NativeWatcher;

View File

@@ -0,0 +1,164 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
*/
import type {FSWatcher} from 'fs';
import {AbstractWatcher} from './AbstractWatcher';
import {includedByGlob, typeFromStat} from './common';
import {promises as fsPromises, watch} from 'fs';
import {platform} from 'os';
import * as path from 'path';
// eslint-disable-next-line import/no-commonjs
const debug = require('debug')('Metro:NativeWatcher');
const TOUCH_EVENT = 'touch';
const DELETE_EVENT = 'delete';
const RECRAWL_EVENT = 'recrawl';
/**
* 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.
*/
export default class NativeWatcher extends AbstractWatcher {
#fsWatcher: ?FSWatcher;
static isSupported(): boolean {
return platform() === 'darwin';
}
constructor(
dir: string,
opts: Readonly<{
ignored: ?RegExp,
globs: ReadonlyArray<string>,
dot: boolean,
...
}>,
) {
if (!NativeWatcher.isSupported) {
throw new Error('This watcher can only be used on macOS');
}
super(dir, opts);
}
async startWatching(): Promise<void> {
this.#fsWatcher = watch(
this.root,
{
// Don't hold the process open if we forget to close()
persistent: false,
// FSEvents or ReadDirectoryChangesW should mean this is cheap and
// ~instant on macOS or Windows.
recursive: true,
},
(event, relativePath) => {
this._handleEvent(event, relativePath).catch(error => {
this.emitError(error);
});
},
);
debug('Watching %s', this.root);
}
/**
* End watching.
*/
async stopWatching(): Promise<void> {
await super.stopWatching();
if (this.#fsWatcher) {
this.#fsWatcher.close();
}
}
async _handleEvent(event: string, relativePath: string) {
const absolutePath = path.resolve(this.root, relativePath);
if (this.doIgnore(relativePath)) {
debug(
'Ignoring event "%s" on %s (root: %s)',
event,
relativePath,
this.root,
);
return;
}
debug(
'Handling event "%s" on %s (root: %s)',
event,
relativePath,
this.root,
);
try {
const stat = await fsPromises.lstat(absolutePath);
const type = typeFromStat(stat);
// Ignore files of an unrecognized type
if (!type) {
return;
}
if (!includedByGlob(type, this.globs, this.dot, relativePath)) {
return;
}
// For directory "rename" events, notify that we need a recrawl since we
// wont' receive events for unmodified files underneath a moved (or
// cloned) directory. Renames are fired by the OS on moves, clones, and
// creations. We ignore "change" events because they indiciate a change
// to directory metadata, rather than its path or existence.
if (type === 'd' && event === 'rename') {
debug(
'Directory rename detected on %s, requesting recrawl',
relativePath,
);
this.emitFileEvent({
event: RECRAWL_EVENT,
relativePath,
});
return;
}
this.emitFileEvent({
event: TOUCH_EVENT,
relativePath,
metadata: {
type,
modifiedTime: stat.mtime.getTime(),
size: stat.size,
},
});
} catch (error) {
if (error?.code !== 'ENOENT') {
this.emitError(error);
return;
}
this.emitFileEvent({event: DELETE_EVENT, relativePath});
}
}
}

View File

@@ -0,0 +1,32 @@
/**
* 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
* @oncall react_native
* @generated SignedSource<<dc063c7e351d5c09a5ad65d09b5b6b2a>>
*
* This file was translated from Flow by scripts/generateTypeScriptDefinitions.js
* Original file: packages/metro-file-map/src/watchers/RecrawlWarning.js
* To regenerate, run:
* js1 build metro-ts-defs (internal) OR
* yarn run build-ts-defs (OSS)
*/
/**
* Originally vendored from
* https://github.com/amasad/sane/blob/64ff3a870c42e84f744086884bf55a4f9c22d376/src/utils/recrawl-warning-dedupe.js
*/
declare class RecrawlWarning {
static RECRAWL_WARNINGS: Array<RecrawlWarning>;
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;

View File

@@ -0,0 +1,48 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true,
});
exports.default = void 0;
class RecrawlWarning {
static RECRAWL_WARNINGS = [];
static REGEXP =
/Recrawled this watch (\d+) times?, most recently because:\n([^:]+)/;
constructor(root, count) {
this.root = root;
this.count = count;
}
static findByRoot(root) {
for (let i = 0; i < this.RECRAWL_WARNINGS.length; i++) {
const warning = this.RECRAWL_WARNINGS[i];
if (warning.root === root) {
return warning;
}
}
return undefined;
}
static isRecrawlWarningDupe(warningMessage) {
if (typeof warningMessage !== "string") {
return false;
}
const match = warningMessage.match(this.REGEXP);
if (!match) {
return false;
}
const count = Number(match[1]);
const root = match[2];
const warning = this.findByRoot(root);
if (warning) {
if (warning.count >= count) {
return true;
} else {
warning.count = count;
return false;
}
} else {
this.RECRAWL_WARNINGS.push(new RecrawlWarning(root, count));
return false;
}
}
}
exports.default = RecrawlWarning;

View File

@@ -0,0 +1,69 @@
/**
* 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
* @format
* @oncall react_native
*/
/**
* Originally vendored from
* https://github.com/amasad/sane/blob/64ff3a870c42e84f744086884bf55a4f9c22d376/src/utils/recrawl-warning-dedupe.js
*/
export default class RecrawlWarning {
static RECRAWL_WARNINGS: Array<RecrawlWarning> = [];
static REGEXP: RegExp =
/Recrawled this watch (\d+) times?, most recently because:\n([^:]+)/;
root: string;
count: number;
constructor(root: string, count: number) {
this.root = root;
this.count = count;
}
static findByRoot(root: string): ?RecrawlWarning {
for (let i = 0; i < this.RECRAWL_WARNINGS.length; i++) {
const warning = this.RECRAWL_WARNINGS[i];
if (warning.root === root) {
return warning;
}
}
return undefined;
}
static isRecrawlWarningDupe(warningMessage: unknown): boolean {
if (typeof warningMessage !== 'string') {
return false;
}
const match = warningMessage.match(this.REGEXP);
if (!match) {
return false;
}
const count = Number(match[1]);
const root = match[2];
const warning = this.findByRoot(root);
if (warning) {
// only keep the highest count, assume count to either stay the same or
// increase.
if (warning.count >= count) {
return true;
} else {
// update the existing warning to the latest (highest) count
warning.count = count;
return false;
}
} else {
this.RECRAWL_WARNINGS.push(new RecrawlWarning(root, count));
return false;
}
}
}

View File

@@ -0,0 +1,34 @@
/**
* 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
* @oncall react_native
* @generated SignedSource<<b8358b8822835bcef505207f90b02c66>>
*
* This file was translated from Flow by scripts/generateTypeScriptDefinitions.js
* Original file: packages/metro-file-map/src/watchers/WatchmanWatcher.js
* To regenerate, run:
* js1 build metro-ts-defs (internal) OR
* yarn run build-ts-defs (OSS)
*/
import type {WatcherOptions} from './common';
import {AbstractWatcher} from './AbstractWatcher';
/**
* Watches `dir`.
*/
declare class WatchmanWatcher extends AbstractWatcher {
readonly subscriptionName: string;
constructor(dir: string, opts: WatcherOptions);
startWatching(): Promise<void>;
/**
* Closes the watcher.
*/
stopWatching(): Promise<void>;
getPauseReason(): null | undefined | string;
}
export default WatchmanWatcher;

View File

@@ -0,0 +1,302 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true,
});
exports.default = void 0;
var _normalizePathSeparatorsToSystem = _interopRequireDefault(
require("../lib/normalizePathSeparatorsToSystem"),
);
var _AbstractWatcher = require("./AbstractWatcher");
var common = _interopRequireWildcard(require("./common"));
var _RecrawlWarning = _interopRequireDefault(require("./RecrawlWarning"));
var _assert = _interopRequireDefault(require("assert"));
var _crypto = require("crypto");
var _fbWatchman = _interopRequireDefault(require("fb-watchman"));
var _invariant = _interopRequireDefault(require("invariant"));
function _interopRequireWildcard(e, t) {
if ("function" == typeof WeakMap)
var r = new WeakMap(),
n = new WeakMap();
return (_interopRequireWildcard = function (e, t) {
if (!t && e && e.__esModule) return e;
var o,
i,
f = { __proto__: null, default: e };
if (null === e || ("object" != typeof e && "function" != typeof e))
return f;
if ((o = t ? n : r)) {
if (o.has(e)) return o.get(e);
o.set(e, f);
}
for (const t in e)
"default" !== t &&
{}.hasOwnProperty.call(e, t) &&
((i =
(o = Object.defineProperty) &&
Object.getOwnPropertyDescriptor(e, t)) &&
(i.get || i.set)
? o(f, t, i)
: (f[t] = e[t]));
return f;
})(e, t);
}
function _interopRequireDefault(e) {
return e && e.__esModule ? e : { default: e };
}
const debug = require("debug")("Metro:WatchmanWatcher");
const DELETE_EVENT = common.DELETE_EVENT;
const TOUCH_EVENT = common.TOUCH_EVENT;
const SUB_PREFIX = "metro-file-map";
class WatchmanWatcher extends _AbstractWatcher.AbstractWatcher {
#client;
#watchProjectInfo;
#watchmanDeferStates;
#deferringStates = null;
constructor(dir, opts) {
const { watchmanDeferStates, ...baseOpts } = opts;
super(dir, baseOpts);
this.#watchmanDeferStates = watchmanDeferStates;
const watchKey = (0, _crypto.createHash)("md5")
.update(this.root)
.digest("hex");
const readablePath = this.root
.replace(/[\/\\]/g, "-")
.replace(/[^\-\w]/g, "");
this.subscriptionName = `${SUB_PREFIX}-${process.pid}-${readablePath}-${watchKey}`;
}
async startWatching() {
await new Promise((resolve, reject) => this.#init(resolve, reject));
}
#init(onReady, onError) {
if (this.#client) {
this.#client.removeAllListeners();
}
const self = this;
this.#client = new _fbWatchman.default.Client();
this.#client.on("error", (error) => {
this.emitError(error);
});
this.#client.on("subscription", (changeEvent) =>
this.#handleChangeEvent(changeEvent),
);
this.#client.on("end", () => {
console.warn(
"[metro-file-map] Warning: Lost connection to Watchman, reconnecting..",
);
self.#init(
() => {},
(error) => self.emitError(error),
);
});
this.#watchProjectInfo = null;
function getWatchRoot() {
return self.#watchProjectInfo ? self.#watchProjectInfo.root : self.root;
}
function onWatchProject(error, resp) {
if (error) {
onError(error);
return;
}
debug("Received watch-project response: %s", resp.relative_path);
handleWarning(resp);
self.#watchProjectInfo = {
relativePath: resp.relative_path
? (0, _normalizePathSeparatorsToSystem.default)(resp.relative_path)
: "",
root: (0, _normalizePathSeparatorsToSystem.default)(resp.watch),
};
self.#client.command(["clock", getWatchRoot()], onClock);
}
function onClock(error, resp) {
if (error) {
onError(error);
return;
}
debug("Received clock response: %s", resp.clock);
const watchProjectInfo = self.#watchProjectInfo;
(0, _invariant.default)(
watchProjectInfo != null,
"watch-project response should have been set before clock response",
);
handleWarning(resp);
const options = {
fields: ["name", "exists", "new", "type", "size", "mtime_ms"],
since: resp.clock,
defer: self.#watchmanDeferStates,
relative_root: watchProjectInfo.relativePath,
};
if (self.globs.length === 0 && !self.dot) {
options.expression = [
"match",
"**",
"wholename",
{
includedotfiles: false,
},
];
}
self.#client.command(
["subscribe", getWatchRoot(), self.subscriptionName, options],
onSubscribe,
);
}
const onSubscribe = (error, resp) => {
if (error) {
onError(error);
return;
}
debug("Received subscribe response: %s", resp.subscribe);
handleWarning(resp);
if (resp["asserted-states"] != null) {
this.#deferringStates = new Set(resp["asserted-states"]);
}
onReady();
};
self.#client.command(["watch-project", getWatchRoot()], onWatchProject);
}
#handleChangeEvent(resp) {
debug(
"Received subscription response: %s (fresh: %s, files: %s, enter: %s, leave: %s, clock: %s)",
resp.subscription,
resp.is_fresh_instance,
resp.files?.length,
resp["state-enter"],
resp["state-leave"],
resp.clock,
);
_assert.default.equal(
resp.subscription,
this.subscriptionName,
"Invalid subscription event.",
);
if (Array.isArray(resp.files)) {
resp.files.forEach((change) =>
this.#handleFileChange(change, resp.clock),
);
}
const { "state-enter": stateEnter, "state-leave": stateLeave } = resp;
if (
stateEnter != null &&
(this.#watchmanDeferStates ?? []).includes(stateEnter)
) {
this.#deferringStates?.add(stateEnter);
debug(
'Watchman reports "%s" just started. Filesystem notifications are paused.',
stateEnter,
);
}
if (
stateLeave != null &&
(this.#watchmanDeferStates ?? []).includes(stateLeave)
) {
this.#deferringStates?.delete(stateLeave);
debug(
'Watchman reports "%s" ended. Filesystem notifications resumed.',
stateLeave,
);
}
}
#handleFileChange(changeDescriptor, rawClock) {
const self = this;
const watchProjectInfo = self.#watchProjectInfo;
(0, _invariant.default)(
watchProjectInfo != null,
"watch-project response should have been set before receiving subscription events",
);
const {
name: relativePosixPath,
new: isNew = false,
exists = false,
type,
mtime_ms,
size,
} = changeDescriptor;
const relativePath = (0, _normalizePathSeparatorsToSystem.default)(
relativePosixPath,
);
debug(
"Handling change to: %s (new: %s, exists: %s, type: %s)",
relativePath,
isNew,
exists,
type,
);
if (type != null && !(type === "f" || type === "d" || type === "l")) {
return;
}
if (
this.doIgnore(relativePath) ||
!common.includedByGlob(type, this.globs, this.dot, relativePath)
) {
return;
}
const clock =
typeof rawClock === "string" && this.#watchProjectInfo != null
? [this.#watchProjectInfo.root, rawClock]
: undefined;
if (!exists) {
self.emitFileEvent({
event: DELETE_EVENT,
clock,
relativePath,
});
} else {
(0, _invariant.default)(
type != null && mtime_ms != null && size != null,
'Watchman file change event for "%s" missing some requested metadata. ' +
"Got type: %s, mtime_ms: %s, size: %s",
relativePath,
type,
mtime_ms,
size,
);
if (!(type === "d" && !isNew)) {
const mtime = Number(mtime_ms);
self.emitFileEvent({
event: TOUCH_EVENT,
clock,
relativePath,
metadata: {
modifiedTime: mtime !== 0 ? mtime : null,
size,
type,
},
});
}
}
}
async stopWatching() {
await super.stopWatching();
if (this.#client) {
this.#client.removeAllListeners();
this.#client.end();
}
this.#deferringStates = null;
}
getPauseReason() {
if (this.#deferringStates == null || this.#deferringStates.size === 0) {
return null;
}
const states = [...this.#deferringStates];
if (states.length === 1) {
return `The watch is in the '${states[0]}' state.`;
}
return `The watch is in the ${states
.slice(0, -1)
.map((s) => `'${s}'`)
.join(", ")} and '${states[states.length - 1]}' states.`;
}
}
exports.default = WatchmanWatcher;
function handleWarning(resp) {
if ("warning" in resp) {
if (_RecrawlWarning.default.isRecrawlWarningDupe(resp.warning)) {
return true;
}
console.warn(resp.warning);
return true;
} else {
return false;
}
}

View File

@@ -0,0 +1,354 @@
/**
* 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 {WatcherOptions} from './common';
import type {
Client,
WatchmanClockResponse,
WatchmanFileChange,
WatchmanQuery,
WatchmanSubscribeResponse,
WatchmanSubscriptionEvent,
WatchmanWatchResponse,
} from 'fb-watchman';
import normalizePathSeparatorsToSystem from '../lib/normalizePathSeparatorsToSystem';
import {AbstractWatcher} from './AbstractWatcher';
import * as common from './common';
import RecrawlWarning from './RecrawlWarning';
import assert from 'assert';
import {createHash} from 'crypto';
import watchman from 'fb-watchman';
import invariant from 'invariant';
// eslint-disable-next-line import/no-commonjs
const debug = require('debug')('Metro:WatchmanWatcher');
const DELETE_EVENT = common.DELETE_EVENT;
const TOUCH_EVENT = common.TOUCH_EVENT;
const SUB_PREFIX = 'metro-file-map';
/**
* Watches `dir`.
*/
export default class WatchmanWatcher extends AbstractWatcher {
#client: Client;
+subscriptionName: string;
#watchProjectInfo: ?Readonly<{
relativePath: string,
root: string,
}>;
+#watchmanDeferStates: ReadonlyArray<string>;
#deferringStates: ?Set<string> = null;
constructor(dir: string, opts: WatcherOptions) {
const {watchmanDeferStates, ...baseOpts} = opts;
super(dir, baseOpts);
this.#watchmanDeferStates = watchmanDeferStates;
// Use a unique subscription name per process per watched directory
const watchKey = createHash('md5').update(this.root).digest('hex');
const readablePath = this.root
.replace(/[\/\\]/g, '-') // \ and / to -
.replace(/[^\-\w]/g, ''); // Remove non-word/hyphen
this.subscriptionName = `${SUB_PREFIX}-${process.pid}-${readablePath}-${watchKey}`;
}
async startWatching(): Promise<void> {
await new Promise((resolve, reject) => this.#init(resolve, reject));
}
/**
* Run the watchman `watch` command on the root and subscribe to changes.
*/
#init(onReady: () => void, onError: (error: Error) => void) {
if (this.#client) {
this.#client.removeAllListeners();
}
const self = this;
this.#client = new watchman.Client();
this.#client.on('error', error => {
this.emitError(error);
});
this.#client.on('subscription', changeEvent =>
this.#handleChangeEvent(changeEvent),
);
this.#client.on('end', () => {
console.warn(
'[metro-file-map] Warning: Lost connection to Watchman, reconnecting..',
);
self.#init(
() => {},
error => self.emitError(error),
);
});
this.#watchProjectInfo = null;
function getWatchRoot() {
return self.#watchProjectInfo ? self.#watchProjectInfo.root : self.root;
}
function onWatchProject(error: ?Error, resp: WatchmanWatchResponse) {
if (error) {
onError(error);
return;
}
debug('Received watch-project response: %s', resp.relative_path);
handleWarning(resp);
// NB: Watchman outputs posix-separated paths even on Windows, convert
// them to system-native separators.
self.#watchProjectInfo = {
relativePath: resp.relative_path
? normalizePathSeparatorsToSystem(resp.relative_path)
: '',
root: normalizePathSeparatorsToSystem(resp.watch),
};
self.#client.command(['clock', getWatchRoot()], onClock);
}
function onClock(error: ?Error, resp: WatchmanClockResponse) {
if (error) {
onError(error);
return;
}
debug('Received clock response: %s', resp.clock);
const watchProjectInfo = self.#watchProjectInfo;
invariant(
watchProjectInfo != null,
'watch-project response should have been set before clock response',
);
handleWarning(resp);
const options: WatchmanQuery = {
fields: ['name', 'exists', 'new', 'type', 'size', 'mtime_ms'],
since: resp.clock,
defer: self.#watchmanDeferStates,
relative_root: watchProjectInfo.relativePath,
};
// Make sure we honor the dot option if even we're not using globs.
if (self.globs.length === 0 && !self.dot) {
options.expression = [
'match',
'**',
'wholename',
{
includedotfiles: false,
},
];
}
self.#client.command(
['subscribe', getWatchRoot(), self.subscriptionName, options],
onSubscribe,
);
}
const onSubscribe = (error: ?Error, resp: WatchmanSubscribeResponse) => {
if (error) {
onError(error);
return;
}
debug('Received subscribe response: %s', resp.subscribe);
handleWarning(resp);
if (resp['asserted-states'] != null) {
this.#deferringStates = new Set(resp['asserted-states']);
}
onReady();
};
self.#client.command(['watch-project', getWatchRoot()], onWatchProject);
}
/**
* Handles a change event coming from the subscription.
*/
#handleChangeEvent(resp: WatchmanSubscriptionEvent) {
debug(
'Received subscription response: %s (fresh: %s, files: %s, enter: %s, leave: %s, clock: %s)',
resp.subscription,
resp.is_fresh_instance,
resp.files?.length,
resp['state-enter'],
resp['state-leave'],
resp.clock,
);
assert.equal(
resp.subscription,
this.subscriptionName,
'Invalid subscription event.',
);
if (Array.isArray(resp.files)) {
resp.files.forEach(change => this.#handleFileChange(change, resp.clock));
}
const {'state-enter': stateEnter, 'state-leave': stateLeave} = resp;
if (
stateEnter != null &&
(this.#watchmanDeferStates ?? []).includes(stateEnter)
) {
this.#deferringStates?.add(stateEnter);
debug(
'Watchman reports "%s" just started. Filesystem notifications are paused.',
stateEnter,
);
}
if (
stateLeave != null &&
(this.#watchmanDeferStates ?? []).includes(stateLeave)
) {
this.#deferringStates?.delete(stateLeave);
debug(
'Watchman reports "%s" ended. Filesystem notifications resumed.',
stateLeave,
);
}
}
/**
* Handles a single change event record.
*/
#handleFileChange(
changeDescriptor: WatchmanFileChange,
rawClock: WatchmanSubscriptionEvent['clock'],
) {
const self = this;
const watchProjectInfo = self.#watchProjectInfo;
invariant(
watchProjectInfo != null,
'watch-project response should have been set before receiving subscription events',
);
const {
name: relativePosixPath,
new: isNew = false,
exists = false,
type,
mtime_ms,
size,
} = changeDescriptor;
// Watchman emits posix-separated paths on Windows, which is inconsistent
// with other watchers. Normalize to system-native separators.
const relativePath = normalizePathSeparatorsToSystem(relativePosixPath);
debug(
'Handling change to: %s (new: %s, exists: %s, type: %s)',
relativePath,
isNew,
exists,
type,
);
// Ignore files of an unrecognized type
if (type != null && !(type === 'f' || type === 'd' || type === 'l')) {
return;
}
if (
this.doIgnore(relativePath) ||
!common.includedByGlob(type, this.globs, this.dot, relativePath)
) {
return;
}
const clock =
typeof rawClock === 'string' && this.#watchProjectInfo != null
? ([this.#watchProjectInfo.root, rawClock] as [string, string])
: undefined;
if (!exists) {
self.emitFileEvent({event: DELETE_EVENT, clock, relativePath});
} else {
invariant(
type != null && mtime_ms != null && size != null,
'Watchman file change event for "%s" missing some requested metadata. ' +
'Got type: %s, mtime_ms: %s, size: %s',
relativePath,
type,
mtime_ms,
size,
);
if (
// Change event on dirs are mostly useless.
!(type === 'd' && !isNew)
) {
const mtime = Number(mtime_ms);
self.emitFileEvent({
event: TOUCH_EVENT,
clock,
relativePath,
metadata: {
modifiedTime: mtime !== 0 ? mtime : null,
size,
type,
},
});
}
}
}
/**
* Closes the watcher.
*/
async stopWatching(): Promise<void> {
await super.stopWatching();
if (this.#client) {
this.#client.removeAllListeners();
this.#client.end();
}
this.#deferringStates = null;
}
getPauseReason(): ?string {
if (this.#deferringStates == null || this.#deferringStates.size === 0) {
return null;
}
const states = [...this.#deferringStates];
if (states.length === 1) {
return `The watch is in the '${states[0]}' state.`;
}
return `The watch is in the ${states
.slice(0, -1)
.map(s => `'${s}'`)
.join(', ')} and '${states[states.length - 1]}' states.`;
}
}
/**
* Handles a warning in the watchman resp object.
*/
function handleWarning(resp: Readonly<{warning?: unknown, ...}>) {
if ('warning' in resp) {
if (RecrawlWarning.isRecrawlWarningDupe(resp.warning)) {
return true;
}
console.warn(resp.warning);
return true;
} else {
return false;
}
}

70
node_modules/metro-file-map/src/watchers/common.d.ts generated vendored Normal file
View File

@@ -0,0 +1,70 @@
/**
* 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
* @oncall react_native
* @generated SignedSource<<ebebfbca9d43e034fde8489e1d9f2dbb>>
*
* This file was translated from Flow by scripts/generateTypeScriptDefinitions.js
* Original file: packages/metro-file-map/src/watchers/common.js
* To regenerate, run:
* js1 build metro-ts-defs (internal) OR
* yarn run build-ts-defs (OSS)
*/
/**
* 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 RECRAWL_EVENT: 'recrawl';
export declare type RECRAWL_EVENT = typeof RECRAWL_EVENT;
export declare const ALL_EVENT: 'all';
export declare type ALL_EVENT = typeof ALL_EVENT;
export type WatcherOptions = Readonly<{
globs: ReadonlyArray<string>;
dot: boolean;
ignored: null | undefined | RegExp;
watchmanDeferStates: ReadonlyArray<string>;
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<string>,
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'];

47
node_modules/metro-file-map/src/watchers/common.js generated vendored Normal file
View File

@@ -0,0 +1,47 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true,
});
exports.TOUCH_EVENT =
exports.RECRAWL_EVENT =
exports.DELETE_EVENT =
exports.ALL_EVENT =
void 0;
exports.includedByGlob = includedByGlob;
exports.posixPathMatchesPattern = void 0;
exports.typeFromStat = typeFromStat;
var _micromatch = _interopRequireDefault(require("micromatch"));
var _path = _interopRequireDefault(require("path"));
function _interopRequireDefault(e) {
return e && e.__esModule ? e : { default: e };
}
const DELETE_EVENT = (exports.DELETE_EVENT = "delete");
const TOUCH_EVENT = (exports.TOUCH_EVENT = "touch");
const RECRAWL_EVENT = (exports.RECRAWL_EVENT = "recrawl");
const ALL_EVENT = (exports.ALL_EVENT = "all");
function includedByGlob(type, globs, dot, relativePath) {
if (globs.length === 0 || type !== "f") {
return dot || _micromatch.default.some(relativePath, "**/*");
}
return _micromatch.default.some(relativePath, globs, {
dot,
});
}
const posixPathMatchesPattern = (exports.posixPathMatchesPattern =
_path.default.sep === "/"
? (pattern, filePath) => pattern.test(filePath)
: (pattern, filePath) =>
pattern.test(filePath.replaceAll(_path.default.sep, "/")));
function typeFromStat(stat) {
if (stat.isSymbolicLink()) {
return "l";
}
if (stat.isDirectory()) {
return "d";
}
if (stat.isFile()) {
return "f";
}
return null;
}

View File

@@ -0,0 +1,88 @@
/**
* 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
*/
/**
* Originally vendored from
* https://github.com/amasad/sane/blob/64ff3a870c42e84f744086884bf55a4f9c22d376/src/common.js
*/
import type {ChangeEventMetadata} from '../flow-types';
import type {Stats} from 'fs';
// $FlowFixMe[untyped-import] - Write libdefs for `micromatch`
import micromatch from 'micromatch';
import path from 'path';
/**
* Constants
*/
export const DELETE_EVENT = 'delete';
export const TOUCH_EVENT = 'touch';
export const RECRAWL_EVENT = 'recrawl';
export const ALL_EVENT = 'all';
export type WatcherOptions = Readonly<{
globs: ReadonlyArray<string>,
dot: boolean,
ignored: ?RegExp,
watchmanDeferStates: ReadonlyArray<string>,
watchman?: unknown,
watchmanPath?: string,
}>;
/**
* Checks a file relative path against the globs array.
*/
export function includedByGlob(
type: ?('f' | 'l' | 'd'),
globs: ReadonlyArray<string>,
dot: boolean,
relativePath: string,
): boolean {
// For non-regular files or if there are no glob matchers, just respect the
// `dot` option to filter dotfiles if dot === false.
if (globs.length === 0 || type !== 'f') {
return dot || micromatch.some(relativePath, '**/*');
}
return micromatch.some(relativePath, globs, {dot});
}
/**
* 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 const posixPathMatchesPattern: (
pattern: RegExp,
filePath: string,
) => boolean =
path.sep === '/'
? (pattern, filePath) => pattern.test(filePath)
: (pattern, filePath) => pattern.test(filePath.replaceAll(path.sep, '/'));
export function typeFromStat(stat: Stats): ?ChangeEventMetadata['type'] {
// Note: These tests are not mutually exclusive - a symlink passes isFile
if (stat.isSymbolicLink()) {
return 'l';
}
if (stat.isDirectory()) {
return 'd';
}
if (stat.isFile()) {
return 'f'; // "Regular" file
}
return null;
}