Summary:
On Windows VM when "yarn start" is executed and compilation is in progress for some plugin, fs.watch randomly fires "changed" events for different files of other plugins. This leads to infinite attempts to rebuild the same plugin again and again, and this process never ends, so "yarn start" is almost unusable:
{F225467225}
I've tried to fix this by using watchman instead of fs.watch and on my tests with Windows build it works well:
{F225467508}
Also as watchman is more careful about opening file handles, hopefully this change will fix "too many files opened" problem as Michel suggested here https://fb.workplace.com/groups/flippersupport/permalink/764157990731528/ and here https://github.com/facebook/flipper/issues/699.
Reviewed By: mweststrate
Differential Revision: D19216026
fbshipit-source-id: acc53ae0d003a7936730e6423ac4dbca84f089c8
248 lines
7.0 KiB
JavaScript
248 lines
7.0 KiB
JavaScript
/**
|
|
* Copyright (c) Facebook, Inc. and its 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-local
|
|
*/
|
|
|
|
const path = require('path');
|
|
const fs = require('fs');
|
|
const Metro = require('metro');
|
|
const util = require('util');
|
|
const recursiveReaddir = require('recursive-readdir');
|
|
const expandTilde = require('expand-tilde');
|
|
const pMap = require('p-map');
|
|
const HOME_DIR = require('os').homedir();
|
|
const Watchman = require('./watchman');
|
|
|
|
const DEFAULT_COMPILE_OPTIONS = {
|
|
force: false,
|
|
failSilently: true,
|
|
};
|
|
|
|
module.exports = async (
|
|
reloadCallback,
|
|
pluginPaths,
|
|
pluginCache,
|
|
options = DEFAULT_COMPILE_OPTIONS,
|
|
) => {
|
|
const plugins = pluginEntryPoints(pluginPaths);
|
|
if (!fs.existsSync(pluginCache)) {
|
|
fs.mkdirSync(pluginCache);
|
|
}
|
|
watchChanges(plugins, reloadCallback, pluginCache, options);
|
|
const compilations = pMap(
|
|
Object.values(plugins),
|
|
plugin => {
|
|
const dynamicOptions = Object.assign(options, {force: false});
|
|
return compilePlugin(plugin, pluginCache, dynamicOptions);
|
|
},
|
|
{concurrency: 4},
|
|
);
|
|
|
|
const dynamicPlugins = (await compilations).filter(c => c != null);
|
|
console.log('✅ Compiled all plugins.');
|
|
return dynamicPlugins;
|
|
};
|
|
|
|
async function watchChanges(
|
|
plugins,
|
|
reloadCallback,
|
|
pluginCache,
|
|
options = DEFAULT_COMPILE_OPTIONS,
|
|
) {
|
|
// eslint-disable-next-line no-console
|
|
console.log('🕵️ Watching for plugin changes');
|
|
|
|
const delayedCompilation = {};
|
|
const kCompilationDelayMillis = 1000;
|
|
const rootDir = path.resolve(__dirname, '..');
|
|
const watchman = new Watchman(rootDir);
|
|
await watchman.initialize();
|
|
Object.values(plugins)
|
|
// no hot reloading for plugins in .flipper folder. This is to prevent
|
|
// Flipper from reloading, while we are doing changes on thirdparty plugins.
|
|
.filter(
|
|
plugin => !plugin.rootDir.startsWith(path.join(HOME_DIR, '.flipper')),
|
|
)
|
|
.map(plugin =>
|
|
watchman.startWatchFiles(
|
|
path.relative(rootDir, plugin.rootDir),
|
|
resp => {
|
|
// only recompile for changes in not hidden files. Watchman might create
|
|
// a file called .watchman-cookie
|
|
if (!delayedCompilation[plugin.name]) {
|
|
delayedCompilation[plugin.name] = setTimeout(() => {
|
|
delayedCompilation[plugin.name] = null;
|
|
// eslint-disable-next-line no-console
|
|
console.log(`🕵️ Detected changes in ${plugin.name}`);
|
|
const watchOptions = Object.assign(options, {force: true});
|
|
compilePlugin(plugin, pluginCache, watchOptions).then(
|
|
reloadCallback,
|
|
);
|
|
}, kCompilationDelayMillis);
|
|
}
|
|
},
|
|
{
|
|
excludes: ['**/__tests__/**/*', '**/node_modules/**/*', '**/.*'],
|
|
},
|
|
),
|
|
);
|
|
}
|
|
function hash(string) {
|
|
let hash = 0;
|
|
if (string.length === 0) {
|
|
return hash;
|
|
}
|
|
let chr;
|
|
for (let i = 0; i < string.length; i++) {
|
|
chr = string.charCodeAt(i);
|
|
hash = (hash << 5) - hash + chr;
|
|
hash |= 0;
|
|
}
|
|
return hash;
|
|
}
|
|
const fileToIdMap = new Map();
|
|
const createModuleIdFactory = () => filePath => {
|
|
if (filePath === '__prelude__') {
|
|
return 0;
|
|
}
|
|
let id = fileToIdMap.get(filePath);
|
|
if (typeof id !== 'number') {
|
|
id = hash(filePath);
|
|
fileToIdMap.set(filePath, id);
|
|
}
|
|
return id;
|
|
};
|
|
function pluginEntryPoints(additionalPaths = []) {
|
|
const defaultPluginPath = path.join(HOME_DIR, '.flipper', 'node_modules');
|
|
const entryPoints = entryPointForPluginFolder(defaultPluginPath);
|
|
if (typeof additionalPaths === 'string') {
|
|
additionalPaths = [additionalPaths];
|
|
}
|
|
additionalPaths.forEach(additionalPath => {
|
|
const additionalPlugins = entryPointForPluginFolder(additionalPath);
|
|
Object.keys(additionalPlugins).forEach(key => {
|
|
entryPoints[key] = additionalPlugins[key];
|
|
});
|
|
});
|
|
return entryPoints;
|
|
}
|
|
function entryPointForPluginFolder(pluginPath) {
|
|
pluginPath = expandTilde(pluginPath);
|
|
if (!fs.existsSync(pluginPath)) {
|
|
return {};
|
|
}
|
|
return fs
|
|
.readdirSync(pluginPath)
|
|
.filter(name => fs.lstatSync(path.join(pluginPath, name)).isDirectory())
|
|
.filter(Boolean)
|
|
.map(name => {
|
|
let packageJSON;
|
|
try {
|
|
packageJSON = fs
|
|
.readFileSync(path.join(pluginPath, name, 'package.json'))
|
|
.toString();
|
|
} catch (e) {}
|
|
if (packageJSON) {
|
|
try {
|
|
const pkg = JSON.parse(packageJSON);
|
|
return {
|
|
packageJSON: pkg,
|
|
name: pkg.name,
|
|
entry: path.join(pluginPath, name, pkg.main || 'index.js'),
|
|
rootDir: path.join(pluginPath, name),
|
|
};
|
|
} catch (e) {
|
|
console.error(
|
|
`Could not load plugin "${pluginPath}", because package.json is invalid.`,
|
|
);
|
|
console.error(e);
|
|
return null;
|
|
}
|
|
}
|
|
})
|
|
.filter(Boolean)
|
|
.reduce((acc, cv) => {
|
|
acc[cv.name] = cv;
|
|
return acc;
|
|
}, {});
|
|
}
|
|
function mostRecentlyChanged(dir) {
|
|
return util
|
|
.promisify(recursiveReaddir)(dir)
|
|
.then(files =>
|
|
files
|
|
.map(f => fs.lstatSync(f).ctime)
|
|
.reduce((a, b) => (a > b ? a : b), new Date(0)),
|
|
);
|
|
}
|
|
async function compilePlugin(
|
|
{rootDir, name, entry, packageJSON},
|
|
pluginCache,
|
|
options,
|
|
) {
|
|
const fileName = `${name}@${packageJSON.version || '0.0.0'}.js`;
|
|
const out = path.join(pluginCache, fileName);
|
|
const result = Object.assign({}, packageJSON, {rootDir, name, entry, out});
|
|
// check if plugin needs to be compiled
|
|
const rootDirCtime = await mostRecentlyChanged(rootDir);
|
|
if (
|
|
!options.force &&
|
|
fs.existsSync(out) &&
|
|
rootDirCtime < fs.lstatSync(out).ctime
|
|
) {
|
|
// eslint-disable-next-line no-console
|
|
console.log(`🥫 Using cached version of ${name}...`);
|
|
return result;
|
|
} else {
|
|
console.log(`⚙️ Compiling ${name}...`); // eslint-disable-line no-console
|
|
try {
|
|
await Metro.runBuild(
|
|
{
|
|
reporter: {update: () => {}},
|
|
projectRoot: rootDir,
|
|
watchFolders: [__dirname, rootDir],
|
|
serializer: {
|
|
getRunModuleStatement: moduleID =>
|
|
`module.exports = global.__r(${moduleID}).default;`,
|
|
createModuleIdFactory,
|
|
},
|
|
transformer: {
|
|
babelTransformerPath: path.join(
|
|
__dirname,
|
|
'transforms',
|
|
'index.js',
|
|
),
|
|
},
|
|
resolver: {
|
|
sourceExts: ['tsx', 'ts', 'js'],
|
|
blacklistRE: /(\/|\\)(sonar|flipper|flipper-public)(\/|\\)(dist|doctor)(\/|\\)|(\.native\.js$)/,
|
|
},
|
|
},
|
|
{
|
|
entry: entry.replace(rootDir, '.'),
|
|
out,
|
|
dev: false,
|
|
sourceMap: true,
|
|
minify: false,
|
|
},
|
|
);
|
|
} catch (e) {
|
|
if (options.failSilently) {
|
|
console.error(
|
|
`❌ Plugin ${name} is ignored, because it could not be compiled.`,
|
|
);
|
|
console.error(e);
|
|
return null;
|
|
} else {
|
|
throw e;
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
}
|