Files
flipper/static/compilePlugins.js
Daniel Büchele 36d00bce4c add source map support
Summary: Use inline source maps for plugins and main bundle, both in production and development.

Reviewed By: passy

Differential Revision: D9967235

fbshipit-source-id: 245e65c6fea94b93dc34a65ae572b7fc98ad56e1
2018-09-25 04:02:28 -07:00

191 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) => {
// 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,
// due to a bug in metro inline source maps are only created when
// sourceMapUrl is truthy: https://github.com/facebook/metro/pull/260
sourceMapUrl: 'inline',
},
);
} catch (e) {
console.error(
`❌ Plugin ${name} is ignored, because it could not be compiled.`,
);
console.error(e);
return null;
}
return result;
}
}