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 @@
"use strict";

View File

@@ -0,0 +1,10 @@
/**
* 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
*/

View File

@@ -0,0 +1 @@
"use strict";

View File

@@ -0,0 +1,10 @@
/**
* 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
*/

View File

@@ -0,0 +1 @@
"use strict";

View File

@@ -0,0 +1,10 @@
/**
* 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
*/

View File

@@ -0,0 +1 @@
"use strict";

View File

@@ -0,0 +1,10 @@
/**
* 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
*/

View File

@@ -0,0 +1 @@
"use strict";

View File

@@ -0,0 +1,10 @@
/**
* 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
*/

View File

@@ -0,0 +1,19 @@
/**
* 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<<8b6ff8a24f9156cd7991006c72edd296>>
*
* This file was translated from Flow by scripts/generateTypeScriptDefinitions.js
* Original file: packages/metro-file-map/src/crawlers/node/hasNativeFindSupport.js
* To regenerate, run:
* js1 build metro-ts-defs (internal) OR
* yarn run build-ts-defs (OSS)
*/
declare function hasNativeFindSupport(): Promise<boolean>;
export default hasNativeFindSupport;

View File

@@ -0,0 +1,36 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true,
});
exports.default = hasNativeFindSupport;
var _child_process = require("child_process");
async function hasNativeFindSupport() {
try {
return await new Promise((resolve) => {
const args = [
".",
"-type",
"f",
"(",
"-iname",
"*.ts",
"-o",
"-iname",
"*.js",
")",
];
const child = (0, _child_process.spawn)("find", args, {
cwd: __dirname,
});
child.on("error", () => {
resolve(false);
});
child.on("exit", (code) => {
resolve(code === 0);
});
});
} catch {
return false;
}
}

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.
*
* @flow strict-local
* @format
* @oncall react_native
*/
import {spawn} from 'child_process';
export default async function hasNativeFindSupport(): Promise<boolean> {
try {
return await new Promise(resolve => {
// Check the find binary supports the non-POSIX -iname parameter wrapped in parens.
const args = [
'.',
'-type',
'f',
'(',
'-iname',
'*.ts',
'-o',
'-iname',
'*.js',
')',
];
const child = spawn('find', args, {cwd: __dirname});
child.on('error', () => {
resolve(false);
});
child.on('exit', code => {
resolve(code === 0);
});
});
} catch {
return false;
}
}

View File

@@ -0,0 +1,21 @@
/**
* 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<<27109494e4956802ba89ac6fd22aa277>>
*
* This file was translated from Flow by scripts/generateTypeScriptDefinitions.js
* Original file: packages/metro-file-map/src/crawlers/node/index.js
* To regenerate, run:
* js1 build metro-ts-defs (internal) OR
* yarn run build-ts-defs (OSS)
*/
import type {CrawlerOptions, CrawlResult} from '../../flow-types';
declare function nodeCrawl(options: CrawlerOptions): Promise<CrawlResult>;
export default nodeCrawl;

230
node_modules/metro-file-map/src/crawlers/node/index.js generated vendored Normal file
View File

@@ -0,0 +1,230 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true,
});
exports.default = nodeCrawl;
var _RootPathUtils = require("../../lib/RootPathUtils");
var _hasNativeFindSupport = _interopRequireDefault(
require("./hasNativeFindSupport"),
);
var _child_process = require("child_process");
var fs = _interopRequireWildcard(require("graceful-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);
}
function _interopRequireDefault(e) {
return e && e.__esModule ? e : { default: e };
}
const debug = require("debug")("Metro:NodeCrawler");
function find(
roots,
extensions,
ignore,
includeSymlinks,
rootDir,
console,
callback,
) {
const result = new Map();
let activeCalls = 0;
const pathUtils = new _RootPathUtils.RootPathUtils(rootDir);
function search(directory) {
activeCalls++;
fs.readdir(
directory,
{
withFileTypes: true,
},
(err, entries) => {
activeCalls--;
if (err) {
console.warn(
`Error "${err.code ?? err.message}" reading contents of "${directory}", skipping. Add this directory to your ignore list to exclude it.`,
);
} else {
entries.forEach((entry) => {
const file = path.join(directory, entry.name.toString());
if (ignore(file)) {
return;
}
if (entry.isSymbolicLink() && !includeSymlinks) {
return;
}
if (entry.isDirectory()) {
search(file);
return;
}
activeCalls++;
fs.lstat(file, (err, stat) => {
activeCalls--;
if (!err && stat) {
const ext = path.extname(file).substr(1);
if (stat.isSymbolicLink() || extensions.includes(ext)) {
result.set(pathUtils.absoluteToNormal(file), [
stat.mtime.getTime(),
stat.size,
0,
null,
stat.isSymbolicLink() ? 1 : 0,
null,
]);
}
}
if (activeCalls === 0) {
callback(result);
}
});
});
}
if (activeCalls === 0) {
callback(result);
}
},
);
}
if (roots.length > 0) {
roots.forEach(search);
} else {
callback(result);
}
}
function findNative(
roots,
extensions,
ignore,
includeSymlinks,
rootDir,
console,
callback,
) {
const extensionClause = extensions.length
? `( ${extensions.map((ext) => `-iname *.${ext}`).join(" -o ")} )`
: "";
const expression = `( ( -type f ${extensionClause} ) ${includeSymlinks ? "-o -type l " : ""})`;
const pathUtils = new _RootPathUtils.RootPathUtils(rootDir);
const child = (0, _child_process.spawn)(
"find",
roots.concat(expression.split(" ")),
);
let stdout = "";
if (child.stdout == null) {
throw new Error(
"stdout is null - this should never happen. Please open up an issue at https://github.com/facebook/metro",
);
}
child.stdout.setEncoding("utf-8");
child.stdout.on("data", (data) => (stdout += data));
child.stdout.on("close", () => {
const lines = stdout
.trim()
.split("\n")
.filter((x) => !ignore(x));
const result = new Map();
let count = lines.length;
if (!count) {
callback(new Map());
} else {
lines.forEach((path) => {
fs.lstat(path, (err, stat) => {
if (!err && stat) {
result.set(pathUtils.absoluteToNormal(path), [
stat.mtime.getTime(),
stat.size,
0,
null,
stat.isSymbolicLink() ? 1 : 0,
null,
]);
}
if (--count === 0) {
callback(result);
}
});
});
}
});
}
async function nodeCrawl(options) {
const {
console,
previousState,
extensions,
forceNodeFilesystemAPI,
ignore,
rootDir,
includeSymlinks,
perfLogger,
roots,
abortSignal,
subpath,
} = options;
abortSignal?.throwIfAborted();
perfLogger?.point("nodeCrawl_start");
const useNativeFind =
!forceNodeFilesystemAPI &&
(0, _os.platform)() !== "win32" &&
(await (0, _hasNativeFindSupport.default)());
debug("Using system find: %s", useNativeFind);
return new Promise((resolve, reject) => {
const callback = (fileData) => {
const difference = previousState.fileSystem.getDifference(fileData, {
subpath,
});
perfLogger?.point("nodeCrawl_end");
try {
abortSignal?.throwIfAborted();
} catch (e) {
reject(e);
}
resolve(difference);
};
if (useNativeFind) {
findNative(
roots,
extensions,
ignore,
includeSymlinks,
rootDir,
console,
callback,
);
} else {
find(
roots,
extensions,
ignore,
includeSymlinks,
rootDir,
console,
callback,
);
}
});
}

View File

@@ -0,0 +1,239 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
* @oncall react_native
*/
import type {
Console,
CrawlerOptions,
CrawlResult,
FileData,
IgnoreMatcher,
} from '../../flow-types';
import {RootPathUtils} from '../../lib/RootPathUtils';
import hasNativeFindSupport from './hasNativeFindSupport';
import {spawn} from 'child_process';
import * as fs from 'graceful-fs';
import {platform} from 'os';
import * as path from 'path';
// eslint-disable-next-line import/no-commonjs
const debug = require('debug')('Metro:NodeCrawler');
type Callback = (result: FileData) => void;
function find(
roots: ReadonlyArray<string>,
extensions: ReadonlyArray<string>,
ignore: IgnoreMatcher,
includeSymlinks: boolean,
rootDir: string,
console: Console,
callback: Callback,
): void {
const result: FileData = new Map();
let activeCalls = 0;
const pathUtils = new RootPathUtils(rootDir);
function search(directory: string): void {
activeCalls++;
fs.readdir(directory, {withFileTypes: true}, (err, entries) => {
activeCalls--;
if (err) {
console.warn(
`Error "${err.code ?? err.message}" reading contents of "${directory}", skipping. Add this directory to your ignore list to exclude it.`,
);
} else {
entries.forEach((entry: fs.Dirent) => {
const file = path.join(directory, entry.name.toString());
if (ignore(file)) {
return;
}
if (entry.isSymbolicLink() && !includeSymlinks) {
return;
}
if (entry.isDirectory()) {
search(file);
return;
}
activeCalls++;
fs.lstat(file, (err, stat) => {
activeCalls--;
if (!err && stat) {
const ext = path.extname(file).substr(1);
if (stat.isSymbolicLink() || extensions.includes(ext)) {
result.set(pathUtils.absoluteToNormal(file), [
stat.mtime.getTime(),
stat.size,
0,
null,
stat.isSymbolicLink() ? 1 : 0,
null,
]);
}
}
if (activeCalls === 0) {
callback(result);
}
});
});
}
if (activeCalls === 0) {
callback(result);
}
});
}
if (roots.length > 0) {
roots.forEach(search);
} else {
callback(result);
}
}
function findNative(
roots: ReadonlyArray<string>,
extensions: ReadonlyArray<string>,
ignore: IgnoreMatcher,
includeSymlinks: boolean,
rootDir: string,
console: Console,
callback: Callback,
): void {
// Examples:
// ( ( -type f ( -iname *.js ) ) )
// ( ( -type f ( -iname *.js -o -iname *.ts ) ) )
// ( ( -type f ( -iname *.js ) ) -o -type l )
// ( ( -type f ) -o -type l )
const extensionClause = extensions.length
? `( ${extensions.map(ext => `-iname *.${ext}`).join(' -o ')} )`
: ''; // Empty inner expressions eg "( )" are not allowed
const expression = `( ( -type f ${extensionClause} ) ${
includeSymlinks ? '-o -type l ' : ''
})`;
const pathUtils = new RootPathUtils(rootDir);
const child = spawn('find', roots.concat(expression.split(' ')));
let stdout = '';
if (child.stdout == null) {
throw new Error(
'stdout is null - this should never happen. Please open up an issue at https://github.com/facebook/metro',
);
}
child.stdout.setEncoding('utf-8');
child.stdout.on('data', data => (stdout += data));
child.stdout.on('close', () => {
const lines = stdout
.trim()
.split('\n')
.filter(x => !ignore(x));
const result: FileData = new Map();
let count = lines.length;
if (!count) {
callback(new Map());
} else {
lines.forEach(path => {
fs.lstat(path, (err, stat) => {
if (!err && stat) {
result.set(pathUtils.absoluteToNormal(path), [
stat.mtime.getTime(),
stat.size,
0,
null,
stat.isSymbolicLink() ? 1 : 0,
null,
]);
}
if (--count === 0) {
callback(result);
}
});
});
}
});
}
export default async function nodeCrawl(
options: CrawlerOptions,
): Promise<CrawlResult> {
const {
console,
previousState,
extensions,
forceNodeFilesystemAPI,
ignore,
rootDir,
includeSymlinks,
perfLogger,
roots,
abortSignal,
subpath,
} = options;
abortSignal?.throwIfAborted();
perfLogger?.point('nodeCrawl_start');
const useNativeFind =
!forceNodeFilesystemAPI &&
platform() !== 'win32' &&
(await hasNativeFindSupport());
debug('Using system find: %s', useNativeFind);
return new Promise((resolve, reject) => {
const callback: Callback = fileData => {
const difference = previousState.fileSystem.getDifference(fileData, {
subpath,
});
perfLogger?.point('nodeCrawl_end');
try {
// TODO: Use AbortSignal.reason directly when Flow supports it
abortSignal?.throwIfAborted();
} catch (e) {
reject(e);
}
resolve(difference);
};
if (useNativeFind) {
findNative(
roots,
extensions,
ignore,
includeSymlinks,
rootDir,
console,
callback,
);
} else {
find(
roots,
extensions,
ignore,
includeSymlinks,
rootDir,
console,
callback,
);
}
});
}

View File

@@ -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.
*
* @noformat
* @oncall react_native
* @generated SignedSource<<bcfb58810773510450845bc00a93beae>>
*
* This file was translated from Flow by scripts/generateTypeScriptDefinitions.js
* Original file: packages/metro-file-map/src/crawlers/watchman/index.js
* To regenerate, run:
* js1 build metro-ts-defs (internal) OR
* yarn run build-ts-defs (OSS)
*/
import type {CrawlerOptions, CrawlResult} from '../../flow-types';
declare function watchmanCrawl(
$$PARAM_0$$: CrawlerOptions,
): Promise<CrawlResult>;
export default watchmanCrawl;

View File

@@ -0,0 +1,300 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true,
});
exports.default = watchmanCrawl;
var _normalizePathSeparatorsToPosix = _interopRequireDefault(
require("../../lib/normalizePathSeparatorsToPosix"),
);
var _normalizePathSeparatorsToSystem = _interopRequireDefault(
require("../../lib/normalizePathSeparatorsToSystem"),
);
var _RootPathUtils = require("../../lib/RootPathUtils");
var _planQuery = require("./planQuery");
var _fbWatchman = _interopRequireDefault(require("fb-watchman"));
var _invariant = _interopRequireDefault(require("invariant"));
var path = _interopRequireWildcard(require("path"));
var _perf_hooks = require("perf_hooks");
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 WATCHMAN_WARNING_INITIAL_DELAY_MILLISECONDS = 10000;
const WATCHMAN_WARNING_INTERVAL_MILLISECONDS = 20000;
const watchmanURL = "https://facebook.github.io/watchman/docs/troubleshooting";
function makeWatchmanError(error) {
error.message =
`Watchman error: ${error.message.trim()}. Make sure watchman ` +
`is running for this project. See ${watchmanURL}.`;
return error;
}
async function watchmanCrawl({
abortSignal,
computeSha1,
extensions,
ignore,
includeSymlinks,
onStatus,
perfLogger,
previousState,
rootDir,
roots,
}) {
abortSignal?.throwIfAborted();
const client = new _fbWatchman.default.Client();
const pathUtils = new _RootPathUtils.RootPathUtils(rootDir);
abortSignal?.addEventListener("abort", () => client.end());
perfLogger?.point("watchmanCrawl_start");
const newClocks = new Map();
let clientError;
client.on("error", (error) => {
clientError = makeWatchmanError(error);
});
const cmd = async (command, ...args) => {
let didLogWatchmanWaitMessage = false;
const startTime = _perf_hooks.performance.now();
const logWatchmanWaitMessage = () => {
didLogWatchmanWaitMessage = true;
onStatus({
type: "watchman_slow_command",
timeElapsed: _perf_hooks.performance.now() - startTime,
command,
});
};
let intervalOrTimeoutId = setTimeout(() => {
logWatchmanWaitMessage();
intervalOrTimeoutId = setInterval(
logWatchmanWaitMessage,
WATCHMAN_WARNING_INTERVAL_MILLISECONDS,
);
}, WATCHMAN_WARNING_INITIAL_DELAY_MILLISECONDS);
try {
const response = await new Promise((resolve, reject) =>
client.command([command, ...args], (error, result) =>
error ? reject(makeWatchmanError(error)) : resolve(result),
),
);
if ("warning" in response) {
onStatus({
type: "watchman_warning",
warning: response.warning,
command,
});
}
return response;
} finally {
clearInterval(intervalOrTimeoutId);
if (didLogWatchmanWaitMessage) {
onStatus({
type: "watchman_slow_command_complete",
timeElapsed: _perf_hooks.performance.now() - startTime,
command,
});
}
}
};
async function getWatchmanRoots(roots) {
perfLogger?.point("watchmanCrawl/getWatchmanRoots_start");
const watchmanRoots = new Map();
await Promise.all(
roots.map(async (root, index) => {
perfLogger?.point(`watchmanCrawl/watchProject_${index}_start`);
const response = await cmd("watch-project", root);
perfLogger?.point(`watchmanCrawl/watchProject_${index}_end`);
const existing = watchmanRoots.get(response.watch);
const canBeFiltered = !existing || existing.directoryFilters.length > 0;
if (canBeFiltered) {
if (response.relative_path) {
watchmanRoots.set(response.watch, {
watcher: response.watcher,
directoryFilters: (existing?.directoryFilters || []).concat(
response.relative_path,
),
});
} else {
watchmanRoots.set(response.watch, {
watcher: response.watcher,
directoryFilters: [],
});
}
}
}),
);
perfLogger?.point("watchmanCrawl/getWatchmanRoots_end");
return watchmanRoots;
}
async function queryWatchmanForDirs(rootProjectDirMappings) {
perfLogger?.point("watchmanCrawl/queryWatchmanForDirs_start");
const results = new Map();
let isFresh = false;
await Promise.all(
Array.from(rootProjectDirMappings).map(
async ([posixSeparatedRoot, { directoryFilters, watcher }], index) => {
const since = previousState.clocks.get(
(0, _normalizePathSeparatorsToPosix.default)(
pathUtils.absoluteToNormal(
(0, _normalizePathSeparatorsToSystem.default)(
posixSeparatedRoot,
),
),
),
);
perfLogger?.annotate({
bool: {
[`watchmanCrawl/query_${index}_has_clock`]: since != null,
},
});
const { query, queryGenerator } = (0, _planQuery.planQuery)({
since,
extensions,
directoryFilters,
includeSha1: computeSha1,
includeSymlinks,
});
perfLogger?.annotate({
string: {
[`watchmanCrawl/query_${index}_watcher`]: watcher ?? "unknown",
[`watchmanCrawl/query_${index}_generator`]: queryGenerator,
},
});
perfLogger?.point(`watchmanCrawl/query_${index}_start`);
const response = await cmd("query", posixSeparatedRoot, query);
perfLogger?.point(`watchmanCrawl/query_${index}_end`);
const isSourceControlQuery =
typeof since !== "string" && since?.scm?.["mergebase-with"] != null;
if (!isSourceControlQuery) {
isFresh = isFresh || response.is_fresh_instance;
}
results.set(posixSeparatedRoot, response);
},
),
);
perfLogger?.point("watchmanCrawl/queryWatchmanForDirs_end");
return {
isFresh,
results,
};
}
let removedFiles = new Set();
let changedFiles = new Map();
let results;
let isFresh = false;
let queryError;
try {
const watchmanRoots = await getWatchmanRoots(roots);
const watchmanFileResults = await queryWatchmanForDirs(watchmanRoots);
results = watchmanFileResults.results;
isFresh = watchmanFileResults.isFresh;
} catch (e) {
queryError = e;
}
client.end();
if (results == null) {
if (clientError) {
perfLogger?.annotate({
string: {
"watchmanCrawl/client_error":
clientError.message ?? "[message missing]",
},
});
}
if (queryError) {
perfLogger?.annotate({
string: {
"watchmanCrawl/query_error":
queryError.message ?? "[message missing]",
},
});
}
perfLogger?.point("watchmanCrawl_end");
abortSignal?.throwIfAborted();
throw (
queryError ?? clientError ?? new Error("Watchman file results missing")
);
}
perfLogger?.point("watchmanCrawl/processResults_start");
const freshFileData = new Map();
for (const [watchRoot, response] of results) {
const fsRoot = (0, _normalizePathSeparatorsToSystem.default)(watchRoot);
const relativeFsRoot = pathUtils.absoluteToNormal(fsRoot);
newClocks.set(
(0, _normalizePathSeparatorsToPosix.default)(relativeFsRoot),
typeof response.clock === "string"
? response.clock
: response.clock.clock,
);
for (const fileData of response.files) {
const filePath =
fsRoot +
path.sep +
(0, _normalizePathSeparatorsToSystem.default)(fileData.name);
const relativeFilePath = pathUtils.absoluteToNormal(filePath);
if (!fileData.exists) {
if (!isFresh) {
removedFiles.add(relativeFilePath);
}
} else if (!ignore(filePath)) {
const { mtime_ms, size } = fileData;
(0, _invariant.default)(
mtime_ms != null && size != null,
"missing file data in watchman response",
);
const mtime =
typeof mtime_ms === "number" ? mtime_ms : mtime_ms.toNumber();
let sha1hex = fileData["content.sha1hex"];
if (typeof sha1hex !== "string" || sha1hex.length !== 40) {
sha1hex = undefined;
}
let symlinkInfo = 0;
if (fileData.type === "l") {
symlinkInfo = fileData["symlink_target"] ?? 1;
}
const nextData = [mtime, size, 0, sha1hex ?? null, symlinkInfo, null];
if (isFresh) {
freshFileData.set(relativeFilePath, nextData);
} else {
changedFiles.set(relativeFilePath, nextData);
}
}
}
}
if (isFresh) {
({ changedFiles, removedFiles } =
previousState.fileSystem.getDifference(freshFileData));
}
perfLogger?.point("watchmanCrawl/processResults_end");
perfLogger?.point("watchmanCrawl_end");
abortSignal?.throwIfAborted();
return {
changedFiles,
removedFiles,
clocks: newClocks,
};
}

View File

@@ -0,0 +1,364 @@
/**
* 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 {WatchmanClockSpec} from '../../flow-types';
import type {
CanonicalPath,
CrawlerOptions,
CrawlResult,
FileData,
FileMetadata,
Path,
} from '../../flow-types';
import type {WatchmanQueryResponse, WatchmanWatchResponse} from 'fb-watchman';
import normalizePathSeparatorsToPosix from '../../lib/normalizePathSeparatorsToPosix';
import normalizePathSeparatorsToSystem from '../../lib/normalizePathSeparatorsToSystem';
import {RootPathUtils} from '../../lib/RootPathUtils';
import {planQuery} from './planQuery';
import watchman from 'fb-watchman';
import invariant from 'invariant';
import * as path from 'path';
import {performance} from 'perf_hooks';
type WatchmanRoots = Map<
string, // Posix-separated absolute path
Readonly<{directoryFilters: Array<string>, watcher: string}>,
>;
const WATCHMAN_WARNING_INITIAL_DELAY_MILLISECONDS = 10000;
const WATCHMAN_WARNING_INTERVAL_MILLISECONDS = 20000;
const watchmanURL = 'https://facebook.github.io/watchman/docs/troubleshooting';
function makeWatchmanError(error: Error): Error {
error.message =
`Watchman error: ${error.message.trim()}. Make sure watchman ` +
`is running for this project. See ${watchmanURL}.`;
return error;
}
export default async function watchmanCrawl({
abortSignal,
computeSha1,
extensions,
ignore,
includeSymlinks,
onStatus,
perfLogger,
previousState,
rootDir,
roots,
}: CrawlerOptions): Promise<CrawlResult> {
abortSignal?.throwIfAborted();
const client = new watchman.Client();
const pathUtils = new RootPathUtils(rootDir);
abortSignal?.addEventListener('abort', () => client.end());
perfLogger?.point('watchmanCrawl_start');
const newClocks = new Map<Path, WatchmanClockSpec>();
let clientError;
client.on('error', error => {
clientError = makeWatchmanError(error);
});
const cmd = async <T>(
command: 'watch-project' | 'query',
// $FlowFixMe[unclear-type] - Fix to use fb-watchman types
...args: Array<any>
): Promise<T> => {
let didLogWatchmanWaitMessage = false;
const startTime = performance.now();
const logWatchmanWaitMessage = () => {
didLogWatchmanWaitMessage = true;
onStatus({
type: 'watchman_slow_command',
timeElapsed: performance.now() - startTime,
command,
});
};
let intervalOrTimeoutId: TimeoutID | IntervalID = setTimeout(() => {
logWatchmanWaitMessage();
intervalOrTimeoutId = setInterval(
logWatchmanWaitMessage,
WATCHMAN_WARNING_INTERVAL_MILLISECONDS,
);
}, WATCHMAN_WARNING_INITIAL_DELAY_MILLISECONDS);
try {
const response = await new Promise<WatchmanQueryResponse>(
(resolve, reject) =>
// $FlowFixMe[incompatible-type] - dynamic call of command
client.command(
[command, ...args],
(error: ?Error, result: WatchmanQueryResponse) =>
error ? reject(makeWatchmanError(error)) : resolve(result),
),
);
if ('warning' in response) {
onStatus({
type: 'watchman_warning',
warning: response.warning,
command,
});
}
// $FlowFixMe[incompatible-type]
return response;
} finally {
// $FlowFixMe[incompatible-type] clearInterval / clearTimeout are interchangeable
clearInterval(intervalOrTimeoutId);
if (didLogWatchmanWaitMessage) {
onStatus({
type: 'watchman_slow_command_complete',
timeElapsed: performance.now() - startTime,
command,
});
}
}
};
async function getWatchmanRoots(
roots: ReadonlyArray<Path>,
): Promise<WatchmanRoots> {
perfLogger?.point('watchmanCrawl/getWatchmanRoots_start');
const watchmanRoots: WatchmanRoots = new Map();
await Promise.all(
roots.map(async (root, index) => {
perfLogger?.point(`watchmanCrawl/watchProject_${index}_start`);
const response = await cmd<WatchmanWatchResponse>(
'watch-project',
root,
);
perfLogger?.point(`watchmanCrawl/watchProject_${index}_end`);
const existing = watchmanRoots.get(response.watch);
// A root can only be filtered if it was never seen with a
// relative_path before.
const canBeFiltered = !existing || existing.directoryFilters.length > 0;
if (canBeFiltered) {
if (response.relative_path) {
watchmanRoots.set(response.watch, {
watcher: response.watcher,
directoryFilters: (existing?.directoryFilters || []).concat(
response.relative_path,
),
});
} else {
// Make the filter directories an empty array to signal that this
// root was already seen and needs to be watched for all files or
// directories.
watchmanRoots.set(response.watch, {
watcher: response.watcher,
directoryFilters: [],
});
}
}
}),
);
perfLogger?.point('watchmanCrawl/getWatchmanRoots_end');
return watchmanRoots;
}
async function queryWatchmanForDirs(rootProjectDirMappings: WatchmanRoots) {
perfLogger?.point('watchmanCrawl/queryWatchmanForDirs_start');
const results = new Map<string, WatchmanQueryResponse>();
let isFresh = false;
await Promise.all(
Array.from(rootProjectDirMappings).map(
async ([posixSeparatedRoot, {directoryFilters, watcher}], index) => {
// Jest is only going to store one type of clock; a string that
// represents a local clock. However, the Watchman crawler supports
// a second type of clock that can be written by automation outside of
// Jest, called an "scm query", which fetches changed files based on
// source control mergebases. The reason this is necessary is because
// local clocks are not portable across systems, but scm queries are.
// By using scm queries, we can create the haste map on a different
// system and import it, transforming the clock into a local clock.
const since = previousState.clocks.get(
normalizePathSeparatorsToPosix(
pathUtils.absoluteToNormal(
normalizePathSeparatorsToSystem(posixSeparatedRoot),
),
),
);
perfLogger?.annotate({
bool: {
[`watchmanCrawl/query_${index}_has_clock`]: since != null,
},
});
const {query, queryGenerator} = planQuery({
since,
extensions,
directoryFilters,
includeSha1: computeSha1,
includeSymlinks,
});
perfLogger?.annotate({
string: {
[`watchmanCrawl/query_${index}_watcher`]: watcher ?? 'unknown',
[`watchmanCrawl/query_${index}_generator`]: queryGenerator,
},
});
perfLogger?.point(`watchmanCrawl/query_${index}_start`);
const response = await cmd<WatchmanQueryResponse>(
'query',
posixSeparatedRoot,
query,
);
perfLogger?.point(`watchmanCrawl/query_${index}_end`);
// When a source-control query is used, we ignore the "is fresh"
// response from Watchman because it will be true despite the query
// being incremental.
const isSourceControlQuery =
typeof since !== 'string' && since?.scm?.['mergebase-with'] != null;
if (!isSourceControlQuery) {
isFresh = isFresh || response.is_fresh_instance;
}
results.set(posixSeparatedRoot, response);
},
),
);
perfLogger?.point('watchmanCrawl/queryWatchmanForDirs_end');
return {
isFresh,
results,
};
}
let removedFiles: Set<CanonicalPath> = new Set();
let changedFiles: FileData = new Map();
let results: Map<string, WatchmanQueryResponse>;
let isFresh = false;
let queryError: ?Error;
try {
const watchmanRoots = await getWatchmanRoots(roots);
const watchmanFileResults = await queryWatchmanForDirs(watchmanRoots);
results = watchmanFileResults.results;
isFresh = watchmanFileResults.isFresh;
} catch (e) {
queryError = e;
}
client.end();
if (results == null) {
if (clientError) {
perfLogger?.annotate({
string: {
'watchmanCrawl/client_error':
clientError.message ?? '[message missing]',
},
});
}
if (queryError) {
perfLogger?.annotate({
string: {
'watchmanCrawl/query_error':
queryError.message ?? '[message missing]',
},
});
}
perfLogger?.point('watchmanCrawl_end');
abortSignal?.throwIfAborted();
throw (
queryError ?? clientError ?? new Error('Watchman file results missing')
);
}
perfLogger?.point('watchmanCrawl/processResults_start');
const freshFileData: FileData = new Map();
for (const [watchRoot, response] of results) {
const fsRoot = normalizePathSeparatorsToSystem(watchRoot);
const relativeFsRoot = pathUtils.absoluteToNormal(fsRoot);
newClocks.set(
normalizePathSeparatorsToPosix(relativeFsRoot),
// Ensure we persist only the local clock.
typeof response.clock === 'string'
? response.clock
: response.clock.clock,
);
for (const fileData of response.files) {
const filePath =
fsRoot + path.sep + normalizePathSeparatorsToSystem(fileData.name);
const relativeFilePath = pathUtils.absoluteToNormal(filePath);
if (!fileData.exists) {
if (!isFresh) {
removedFiles.add(relativeFilePath);
}
// Whether watchman can return exists: false in a fresh instance
// response is unknown, but there's nothing we need to do in that case.
} else if (!ignore(filePath)) {
const {mtime_ms, size} = fileData;
invariant(
mtime_ms != null && size != null,
'missing file data in watchman response',
);
const mtime =
typeof mtime_ms === 'number' ? mtime_ms : mtime_ms.toNumber();
let sha1hex = fileData['content.sha1hex'];
if (typeof sha1hex !== 'string' || sha1hex.length !== 40) {
sha1hex = undefined;
}
let symlinkInfo: 0 | 1 | string = 0;
if (fileData.type === 'l') {
symlinkInfo = fileData['symlink_target'] ?? 1;
}
const nextData: FileMetadata = [
mtime,
size,
0,
sha1hex ?? null,
symlinkInfo,
null,
];
// If watchman is fresh, the removed files map starts with all files
// and we remove them as we verify they still exist.
if (isFresh) {
freshFileData.set(relativeFilePath, nextData);
} else {
changedFiles.set(relativeFilePath, nextData);
}
}
}
}
if (isFresh) {
({changedFiles, removedFiles} =
previousState.fileSystem.getDifference(freshFileData));
}
perfLogger?.point('watchmanCrawl/processResults_end');
perfLogger?.point('watchmanCrawl_end');
abortSignal?.throwIfAborted();
return {
changedFiles,
removedFiles,
clocks: newClocks,
};
}

View File

@@ -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
*/
type WatchmanQuery = {[key: string]: unknown};
type WatchmanQuerySince = unknown;
export declare function planQuery(
args: Readonly<{
since: WatchmanQuerySince;
directoryFilters: ReadonlyArray<string>;
extensions: ReadonlyArray<string>;
includeSha1: boolean;
includeSymlinks: boolean;
}>,
): {
query: WatchmanQuery;
queryGenerator: string;
};

View File

@@ -0,0 +1,62 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true,
});
exports.planQuery = planQuery;
function planQuery({
since,
directoryFilters,
extensions,
includeSha1,
includeSymlinks,
}) {
const fields = ["name", "exists", "mtime_ms", "size"];
if (includeSha1) {
fields.push("content.sha1hex");
}
if (includeSymlinks) {
fields.push("type");
}
const allOfTerms = includeSymlinks
? [
[
"anyof",
["allof", ["type", "f"], ["suffix", extensions]],
["type", "l"],
],
]
: [["type", "f"]];
const query = {
fields,
};
let queryGenerator;
if (since != null) {
query.since = since;
queryGenerator = "since";
if (directoryFilters.length > 0) {
allOfTerms.push([
"anyof",
...directoryFilters.map((dir) => ["dirname", dir]),
]);
}
} else if (directoryFilters.length > 0) {
query.glob = directoryFilters.map((directory) => `${directory}/**`);
query.glob_includedotfiles = true;
queryGenerator = "glob";
} else if (!includeSymlinks) {
query.suffix = extensions;
queryGenerator = "suffix";
} else {
queryGenerator = "all";
}
if (!includeSymlinks && queryGenerator !== "suffix") {
allOfTerms.push(["suffix", extensions]);
}
query.expression =
allOfTerms.length === 1 ? allOfTerms[0] : ["allof", ...allOfTerms];
return {
query,
queryGenerator,
};
}

View File

@@ -0,0 +1,136 @@
/**
* 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
* @flow strict
*/
import type {
WatchmanDirnameExpression,
WatchmanExpression,
WatchmanQuery,
WatchmanQuerySince,
} from 'fb-watchman';
export function planQuery({
since,
directoryFilters,
extensions,
includeSha1,
includeSymlinks,
}: Readonly<{
since: ?WatchmanQuerySince,
directoryFilters: ReadonlyArray<string>,
extensions: ReadonlyArray<string>,
includeSha1: boolean,
includeSymlinks: boolean,
}>): {
query: WatchmanQuery,
queryGenerator: string,
} {
const fields = ['name', 'exists', 'mtime_ms', 'size'];
if (includeSha1) {
fields.push('content.sha1hex');
}
/**
* Note on symlink_target:
*
* Watchman supports requesting the symlink_target field, which is
* *potentially* more efficient if targets can be read from metadata without
* reading/materialising files. However, at the time of writing, Watchman has
* issues reporting symlink_target on some backends[1]. Additionally, though
* the Eden watcher is known to work, it reads links serially[2] on demand[3]
* - less efficiently than we can do ourselves.
* [1] https://github.com/facebook/watchman/issues/1084
* [2] https://github.com/facebook/watchman/blob/v2023.01.02.00/watchman/watcher/eden.cpp#L476-L485
* [3] https://github.com/facebook/watchman/blob/v2023.01.02.00/watchman/watcher/eden.cpp#L433-L434
*/
if (includeSymlinks) {
fields.push('type');
}
const allOfTerms: Array<WatchmanExpression> = includeSymlinks
? [
[
'anyof',
['allof', ['type', 'f'], ['suffix', extensions]],
['type', 'l'],
],
]
: [['type', 'f']];
const query: WatchmanQuery = {fields};
/**
* Watchman "query planner".
*
* Watchman file queries consist of 1 or more generators that feed
* files through the expression evaluator.
*
* Strategy:
* 1. Select the narrowest possible generator so that the expression
* evaluator has fewer candidates to process.
* 2. Evaluate expressions from narrowest to broadest.
* 3. Don't use an expression to recheck a condition that the
* generator already guarantees.
* 4. Compose expressions to avoid combinatorial explosions in the
* number of terms.
*
* The ordering of generators/filters, from narrow to broad, is:
* - since = O(changes)
* - glob / dirname = O(files in a subtree of the repo)
* - suffix = O(files in the repo)
*
* We assume that file extensions are ~uniformly distributed in the
* repo but Haste map projects are focused on a handful of
* directories. Therefore `glob` < `suffix`.
*/
let queryGenerator: ?string;
if (since != null) {
// Prefer the since generator whenever we have a clock
query.since = since;
queryGenerator = 'since';
// Filter on directories using an anyof expression
if (directoryFilters.length > 0) {
allOfTerms.push([
'anyof',
...directoryFilters.map(
dir => ['dirname', dir] as WatchmanDirnameExpression,
),
]);
}
} else if (directoryFilters.length > 0) {
// Use the `glob` generator and filter only by extension.
query.glob = directoryFilters.map(directory => `${directory}/**`);
query.glob_includedotfiles = true;
queryGenerator = 'glob';
} else if (!includeSymlinks) {
// Use the `suffix` generator with no path/extension filtering, as long
// as we don't need (suffixless) directory symlinks.
query.suffix = extensions;
queryGenerator = 'suffix';
} else {
// Fall back to `all` if we need symlinks and don't have a clock or
// directory filters.
queryGenerator = 'all';
}
// `includeSymlinks` implies we need (suffixless) directory links. In the
// case of the `suffix` generator, a suffix expression would be redundant.
if (!includeSymlinks && queryGenerator !== 'suffix') {
allOfTerms.push(['suffix', extensions]);
}
// If we only have one "all of" expression we can use it directly, otherwise
// wrap in ['allof', ...expressions]. By construction we should never have
// length 0.
query.expression =
allOfTerms.length === 1 ? allOfTerms[0] : ['allof', ...allOfTerms];
return {query, queryGenerator};
}