Files
flipper/static/compilePlugins.js
Pascal Hartig 2a6462641b Limit concurrency during plugin compilation
Summary:
My MBP just crashed again during startup. I would like to
understand why every plugin compilation appears to start
up a new electron process, but until that's understood
and hopefully fixed, it's probably best to limit the number
of processes we spawn by setting an upper bound here.

N.B. My Linux box doesn't mind this at all.

Reviewed By: jknoxville, danielbuechele

Differential Revision: D17419289

fbshipit-source-id: a11562a21a984059dc35e826eb20d869df218546
2019-09-17 12:21:15 -07:00

236 lines
6.5 KiB
JavaScript

/**
* Copyright 2018-present Facebook.
* 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();
/* eslint-disable prettier/prettier */
/*::
type CompileOptions = {|
force: boolean,
failSilently: boolean,
|};
*/
const DEFAULT_COMPILE_OPTIONS /*: CompileOptions */ = {
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;
};
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;
Object.values(plugins).map(plugin =>
fs.watch(plugin.rootDir, {recursive: true}, (eventType, filename) => {
// only recompile for changes in not hidden files. Watchman might create
// a file called .watchman-cookie
if (!filename.startsWith('.') && !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);
}
}),
);
}
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/*: CompileOptions */,
) {
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: {
blacklistRE: /\/(sonar|flipper-public)\/dist\//,
},
},
{
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;
}
}