Track plugin changes and notify frontend

Summary: Watch source plugin folders and notify frontend that any of them changed. In subsequent diffs, we will start reloading plugins that changed.

Reviewed By: lblasa

Differential Revision: D39539443

fbshipit-source-id: 726916c0bce336a2c0179558526bcb1b74e35b93
This commit is contained in:
Andrey Goncharov
2022-09-15 10:02:19 -07:00
committed by Facebook GitHub Bot
parent 3639feef61
commit c69d102ca1
10 changed files with 61 additions and 30 deletions

View File

@@ -511,17 +511,3 @@ function defaultResolve(...rest: any[]) {
...rest,
);
}
export async function rebuildPlugin(pluginPath: string) {
try {
await runBuild(pluginPath, true);
console.info(chalk.green('Rebuilt plugin'), pluginPath);
} catch (e) {
console.error(
chalk.red(
'Failed to compile a plugin, waiting for additional changes...',
),
e,
);
}
}

View File

@@ -21,7 +21,6 @@
"dotenv": "^14.2.0",
"electron-builder": "23.0.3",
"express": "^4.17.3",
"fb-watchman": "^2.0.1",
"flipper-common": "0.0.0",
"flipper-pkg-lib": "0.0.0",
"flipper-plugin-lib": "0.0.0",

View File

@@ -21,7 +21,6 @@ import path from 'path';
import fs from 'fs-extra';
import {hostname} from 'os';
import {compileMain, prepareDefaultPlugins} from './build-utils';
import Watchman from './watchman';
// @ts-ignore no typings for metro
import Metro from 'metro';
import {staticDir, babelTransformationsDir, rootDir} from './paths';
@@ -29,8 +28,8 @@ import isFB from './isFB';
import getAppWatchFolders from './get-app-watch-folders';
import {getPluginSourceFolders} from 'flipper-plugin-lib';
import ensurePluginFoldersWatchable from './ensurePluginFoldersWatchable';
import startWatchPlugins from './startWatchPlugins';
import yargs from 'yargs';
import {startWatchPlugins, Watchman} from 'flipper-pkg-lib';
const argv = yargs
.usage('yarn start [args]')
@@ -445,8 +444,8 @@ function checkDevServer() {
await startMetroServer(app, server);
outputScreen(socket);
await compileMain();
await startWatchPlugins(() => {
socket.emit('refresh');
await startWatchPlugins((changedPlugins) => {
socket.emit('plugins-source-updated', changedPlugins);
});
if (dotenv && dotenv.parsed) {
console.log('✅ Loaded env vars from .env file: ', dotenv.parsed);

View File

@@ -15,11 +15,10 @@ import {
launchServer,
prepareDefaultPlugins,
} from './build-utils';
import Watchman from './watchman';
import isFB from './isFB';
import yargs from 'yargs';
import ensurePluginFoldersWatchable from './ensurePluginFoldersWatchable';
import startWatchPlugins from './startWatchPlugins';
import {Watchman} from 'flipper-pkg-lib';
const argv = yargs
.usage('yarn flipper-server [args]')
@@ -195,5 +194,4 @@ async function startWatchChanges() {
await restartServer();
// watch
await startWatchChanges();
await startWatchPlugins();
})();

View File

@@ -1,89 +0,0 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
*/
import Watchman from './watchman';
import {getPluginSourceFolders, isPluginDir} from 'flipper-plugin-lib';
import path from 'path';
import chalk from 'chalk';
import {rebuildPlugin} from './build-utils';
export default async function startWatchPlugins(
onChanged?: () => void | Promise<void>,
) {
// eslint-disable-next-line no-console
console.log('🕵️‍ Watching for plugin changes');
let delayedCompilation: NodeJS.Timeout | undefined;
const kCompilationDelayMillis = 1000;
const onPluginChangeDetected = (root: string, files: string[]) => {
if (!delayedCompilation) {
delayedCompilation = setTimeout(async () => {
delayedCompilation = undefined;
// eslint-disable-next-line no-console
console.log(`🕵️‍ Detected plugin change`);
await Promise.all(
// https://facebook.github.io/watchman/docs/nodejs.html#subscribing-to-changes
files.map(async (file: string) => {
const filePathAbs = path.resolve(root, file);
let dirPath = path.dirname(filePathAbs);
while (
// Stop when we reach plugin root
!(await isPluginDir(dirPath))
) {
const relative = path.relative(root, dirPath);
// Stop when we reach desktop/plugins folder
if (!relative || relative.startsWith('..')) {
console.warn(
chalk.yellow('Failed to find a plugin root for path'),
filePathAbs,
);
return;
}
dirPath = path.resolve(dirPath, '..');
}
await rebuildPlugin(dirPath);
}),
);
onChanged?.();
}, kCompilationDelayMillis);
}
};
try {
await startWatchingPluginsUsingWatchman(onPluginChangeDetected);
} catch (err) {
console.error(
'Failed to start watching plugin files using Watchman, continue without hot reloading',
err,
);
}
}
async function startWatchingPluginsUsingWatchman(
onChange: (root: string, files: string[]) => void,
) {
const pluginFolders = await getPluginSourceFolders();
await Promise.all(
pluginFolders.map(async (pluginFolder) => {
const watchman = new Watchman(pluginFolder);
await watchman.initialize();
await watchman.startWatchFiles(
'.',
({files}) => onChange(pluginFolder, files),
{
excludes: [
'**/__tests__/**/*',
'**/node_modules/**/*',
'**/dist/*',
'**/.*',
],
},
);
}),
);
}

View File

@@ -1,126 +0,0 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
*/
import {Client} from 'fb-watchman';
import {v4 as uuid} from 'uuid';
import path from 'path';
const watchmanTimeout = 60 * 1000;
export default class Watchman {
constructor(private rootDir: string) {}
private client?: Client;
private watch?: any;
private relativeRoot?: string;
async initialize(): Promise<void> {
if (this.client) {
return;
}
this.client = new Client();
this.client.setMaxListeners(250);
await new Promise<void>((resolve, reject) => {
const onError = (err: Error) => {
this.client!.removeAllListeners('error');
reject(err);
this.client!.end();
delete this.client;
};
const timeouthandle = setTimeout(() => {
onError(new Error('Timeout when trying to start Watchman'));
}, watchmanTimeout);
this.client!.once('error', onError);
this.client!.capabilityCheck(
{optional: [], required: ['relative_root']},
(error) => {
if (error) {
onError(error);
return;
}
this.client!.command(
['watch-project', this.rootDir],
(error, resp) => {
if (error) {
onError(error);
return;
}
if ('warning' in resp) {
console.warn(resp.warning);
}
this.watch = resp.watch;
this.relativeRoot = resp.relative_path;
clearTimeout(timeouthandle);
resolve();
},
);
},
);
});
}
async startWatchFiles(
relativeDir: string,
handler: (resp: any) => void,
options: {excludes: string[]},
): Promise<void> {
if (!this.watch) {
throw new Error(
'Watchman is not initialized, please call "initialize" function and wait for the returned promise completion before calling "startWatchFiles".',
);
}
options = Object.assign({excludes: []}, options);
return new Promise((resolve, reject) => {
this.client!.command(['clock', this.watch], (error, resp) => {
if (error) {
return reject(error);
}
try {
const {clock} = resp;
const sub = {
expression: [
'allof',
['not', ['type', 'd']],
...options!.excludes.map((e) => [
'not',
['match', e, 'wholename'],
]),
],
fields: ['name'],
since: clock,
relative_root: this.relativeRoot
? path.join(this.relativeRoot, relativeDir)
: relativeDir,
};
const id = uuid();
this.client!.command(['subscribe', this.watch, id, sub], (error) => {
if (error) {
return reject(error);
}
this.client!.on('subscription', (resp) => {
if (resp.subscription !== id || !resp.files) {
return;
}
handler(resp);
});
resolve();
});
} catch (err) {
reject(err);
}
});
});
}
}