feat(smart-app): implement complete mobile app MVP

- App.tsx: full navigation (Auth stack + Main tabs with 5 screens)
- Auth: LoginScreen, RegisterScreen, ForgotPasswordScreen
- HomeScreen: dashboard with IoT metrics, weather widget, alerts, quick actions, sensors
- MapScreen: interactive map with layer toggles (6 layers)
- MarketplaceScreen: categories (6), products (5), search
- ChatScreen: AI chat with quick prompts (4), bot responses
- ProfileScreen: user info, stats, menu (9 items), logout
- AlertsScreen: alert list with severity, acknowledge
- SensorsScreen: sensor list with type filters (6 types), search
- ZonesScreen: zone cards with stats
- SettingsScreen: language picker (FR/EN/ES/DE), privacy, about
- Stores: iotStore (sensors, zones, alerts), notificationStore, uiStore + i18n
- Hooks: useSensors, useAlerts, useNotifications, useLocation
- Components: Card, Button, LoadingSpinner, ErrorBoundary, Header
- Services: iotService, notificationService (with axios API client)
- Utils: formatters (temp, AQI, noise, dates), validators (email, password, IBAN)
- Theme: colors.ts with full design system (Blue Ocean palette)
- Ditto: fixed MongoDB connection, new JWT secrets, official gateway image
This commit is contained in:
Eric FELIXINE
2026-06-01 18:00:35 -04:00
parent 08ca495bde
commit e30ae8ed09
35578 changed files with 3703534 additions and 43 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,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,242 @@
"use strict";
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 _getRequireWildcardCache(nodeInterop) {
if (typeof WeakMap !== "function") return null;
var cacheBabelInterop = new WeakMap();
var cacheNodeInterop = new WeakMap();
return (_getRequireWildcardCache = function (nodeInterop) {
return nodeInterop ? cacheNodeInterop : cacheBabelInterop;
})(nodeInterop);
}
function _interopRequireWildcard(obj, nodeInterop) {
if (!nodeInterop && obj && obj.__esModule) {
return obj;
}
if (obj === null || (typeof obj !== "object" && typeof obj !== "function")) {
return { default: obj };
}
var cache = _getRequireWildcardCache(nodeInterop);
if (cache && cache.has(obj)) {
return cache.get(obj);
}
var newObj = {};
var hasPropertyDescriptor =
Object.defineProperty && Object.getOwnPropertyDescriptor;
for (var key in obj) {
if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) {
var desc = hasPropertyDescriptor
? Object.getOwnPropertyDescriptor(obj, key)
: null;
if (desc && (desc.get || desc.set)) {
Object.defineProperty(newObj, key, desc);
} else {
newObj[key] = obj[key];
}
}
}
newObj.default = obj;
if (cache) {
cache.set(obj, newObj);
}
return newObj;
}
function _interopRequireDefault(obj) {
return obj && obj.__esModule ? obj : { default: obj };
}
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,
]);
}
}
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,
]);
}
if (--count === 0) {
callback(result);
}
});
});
}
});
}
module.exports = async function nodeCrawl(options) {
const {
console,
previousState,
extensions,
forceNodeFilesystemAPI,
ignore,
rootDir,
includeSymlinks,
perfLogger,
roots,
abortSignal,
} = 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);
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,238 @@
/**
* 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 {
CanonicalPath,
Console,
CrawlerOptions,
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';
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,
]);
}
}
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,
]);
}
if (--count === 0) {
callback(result);
}
});
});
}
});
}
module.exports = async function nodeCrawl(options: CrawlerOptions): Promise<{
removedFiles: Set<CanonicalPath>,
changedFiles: FileData,
}> {
const {
console,
previousState,
extensions,
forceNodeFilesystemAPI,
ignore,
rootDir,
includeSymlinks,
perfLogger,
roots,
abortSignal,
} = 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);
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,307 @@
"use strict";
var _normalizePathSeparatorsToPosix = _interopRequireDefault(
require("../../lib/normalizePathSeparatorsToPosix")
);
var _normalizePathSeparatorsToSystem = _interopRequireDefault(
require("../../lib/normalizePathSeparatorsToSystem")
);
var _RootPathUtils = require("../../lib/RootPathUtils");
var _planQuery = require("./planQuery");
var _invariant = _interopRequireDefault(require("invariant"));
var path = _interopRequireWildcard(require("path"));
var _perf_hooks = require("perf_hooks");
function _getRequireWildcardCache(nodeInterop) {
if (typeof WeakMap !== "function") return null;
var cacheBabelInterop = new WeakMap();
var cacheNodeInterop = new WeakMap();
return (_getRequireWildcardCache = function (nodeInterop) {
return nodeInterop ? cacheNodeInterop : cacheBabelInterop;
})(nodeInterop);
}
function _interopRequireWildcard(obj, nodeInterop) {
if (!nodeInterop && obj && obj.__esModule) {
return obj;
}
if (obj === null || (typeof obj !== "object" && typeof obj !== "function")) {
return { default: obj };
}
var cache = _getRequireWildcardCache(nodeInterop);
if (cache && cache.has(obj)) {
return cache.get(obj);
}
var newObj = {};
var hasPropertyDescriptor =
Object.defineProperty && Object.getOwnPropertyDescriptor;
for (var key in obj) {
if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) {
var desc = hasPropertyDescriptor
? Object.getOwnPropertyDescriptor(obj, key)
: null;
if (desc && (desc.get || desc.set)) {
Object.defineProperty(newObj, key, desc);
} else {
newObj[key] = obj[key];
}
}
}
newObj.default = obj;
if (cache) {
cache.set(obj, newObj);
}
return newObj;
}
function _interopRequireDefault(obj) {
return obj && obj.__esModule ? obj : { default: obj };
}
const watchman = require("fb-watchman");
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;
}
module.exports = async function watchmanCrawl({
abortSignal,
computeSha1,
extensions,
ignore,
includeSymlinks,
onStatus,
perfLogger,
previousState,
rootDir,
roots,
}) {
abortSignal?.throwIfAborted();
const client = new watchman.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];
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,370 @@
/**
* 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,
FileData,
FileMetaData,
Path,
WatchmanClocks,
} 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 invariant from 'invariant';
import * as path from 'path';
import {performance} from 'perf_hooks';
const watchman = require('fb-watchman');
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;
}
module.exports = async function watchmanCrawl({
abortSignal,
computeSha1,
extensions,
ignore,
includeSymlinks,
onStatus,
perfLogger,
previousState,
rootDir,
roots,
}: CrawlerOptions): Promise<{
changedFiles: FileData,
removedFiles: Set<CanonicalPath>,
clocks: WatchmanClocks,
}> {
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-call] - 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-return]
return response;
} finally {
// $FlowFixMe[incompatible-call] 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,
];
// 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,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]: 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};
}