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:
committed by
Facebook GitHub Bot
parent
3639feef61
commit
c69d102ca1
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
})();
|
||||
|
||||
@@ -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/*',
|
||||
'**/.*',
|
||||
],
|
||||
},
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user