Files
flipper/static/compilePlugins.js
Daniel Büchele 6d18701d6a disable ASAR
Summary:
Metro's new version using `jest-haste-map` wants to access files inside our ASAR bundle, not using electron's require function. This fails, because it can not read from inside the ASAR bundle. For this reason we are disabeling ASAR for now.

Additionally, we are disabling reloads when a hidden file changes. This is because watchman creates a `.watchman-cookie` which shoudln't triggers a reload. But in general I think it's safe to not reload when hidden files are changed, as they are unlikely to have actual code in them.

Reviewed By: passy

Differential Revision: D10426715

fbshipit-source-id: 6ad9dcf88c62d5b65a9736eff28aadaf89c6af7a
2018-10-18 02:56:17 -07:00

193 lines
5.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
*/
const path = require('path');
const fs = require('fs');
const Metro = require('metro');
const util = require('util');
const recursiveReaddir = require('recursive-readdir');
const HOME_DIR = require('os').homedir();
module.exports = async (reloadCallback, pluginPaths, pluginCache) => {
const plugins = pluginEntryPoints(pluginPaths);
if (!fs.existsSync(pluginCache)) {
fs.mkdirSync(pluginCache);
}
watchChanges(plugins, reloadCallback, pluginCache);
const dynamicPlugins = [];
for (let plugin of Object.values(plugins)) {
const compiledPlugin = await compilePlugin(plugin, false, pluginCache);
if (compiledPlugin) {
dynamicPlugins.push(compiledPlugin);
}
}
console.log('✅ Compiled all plugins.');
return dynamicPlugins;
};
function watchChanges(plugins, reloadCallback, pluginCache) {
// eslint-disable-next-line no-console
console.log('🕵️‍ Watching for plugin changes');
Object.values(plugins).map(plugin =>
fs.watch(plugin.rootDir, (eventType, filename) => {
// only recompile for changes in not hidden files. Watchman might create
// a file called .watchman-cookie
if (!filename.startsWith('.')) {
// eslint-disable-next-line no-console
console.log(`🕵️‍ Detected changes in ${plugin.name}`);
compilePlugin(plugin, true, pluginCache).then(reloadCallback);
}
}),
);
}
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 = pluginPath.replace('~', HOME_DIR);
if (!fs.existsSync(pluginPath)) {
return {};
}
return fs
.readdirSync(pluginPath)
.filter(name =>
/*name.startsWith('flipper-plugin') && */ 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},
force,
pluginCache,
) {
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 (!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',
),
},
},
{
entry: entry.replace(rootDir, '.'),
out,
dev: false,
sourceMap: true,
minify: false,
},
);
} catch (e) {
console.error(
`❌ Plugin ${name} is ignored, because it could not be compiled.`,
);
console.error(e);
return null;
}
return result;
}
}