Files
flipper/static/compilePlugins.js
Daniel Büchele 3bea3c88e6 flipper-plugin-*
Summary: rename plugin packages from `sonar-plugin-*` to `flipper-plugin-*`.

Reviewed By: passy

Differential Revision: D9851211

fbshipit-source-id: bdac568ede0a4a9308138a6c3d60728bb6195a72
2018-09-18 07:01:16 -07:00

199 lines
5.8 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 = (reloadCallback, pluginPaths, pluginCache) => {
const plugins = pluginEntryPoints(pluginPaths);
if (!fs.existsSync(pluginCache)) {
fs.mkdirSync(pluginCache);
}
watchChanges(plugins, reloadCallback, pluginCache);
return Promise.all(
Object.values(plugins).map(plugin =>
compilePlugin(plugin, false, pluginCache),
),
)
.then(dynamicPlugins => {
// eslint-disable-next-line no-console
console.log('✅ Compiled all plugins.');
return dynamicPlugins;
})
.catch(console.error);
};
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 changeExport(path) {
let file = fs.readFileSync(path);
file = file
.toString()
.replace(
/\nrequire\((-?[0-9]+)\);\n*$/,
(_, moduleID) =>
`\nmodule.exports = global.require(${moduleID}).default;`,
);
fs.writeFileSync(path, file);
}
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)),
);
}
function compilePlugin(
{rootDir, name, entry, packageJSON},
force,
pluginCache,
) {
return new Promise((resolve, reject) => {
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
mostRecentlyChanged(rootDir).then(rootDirCtime => {
if (
!force &&
fs.existsSync(out) &&
rootDirCtime < fs.lstatSync(out).ctime
) {
// eslint-disable-next-line no-console
console.log(`🥫 Using cached version of ${name}...`);
resolve(result);
} else {
console.log(`⚙️ Compiling ${name}...`); // eslint-disable-line no-console
metro
.runBuild({
config: {
getProjectRoots: () => [rootDir, path.join(__dirname, '..')],
getTransformModulePath: () =>
path.join(__dirname, 'transforms', 'index.js'),
// a custom hash function is required, because by default metro starts
// numbering the modules by 1. This means all plugins would start at
// ID 1, which causes a clash. This is why we have a custom IDFactory.
createModuleIdFactory,
},
dev: false,
entry,
out,
})
.then(() => {
changeExport(out);
resolve(result);
})
.catch(err => {
console.error(
`❌ Plugin ${name} is ignored, because it could not be compiled.`,
);
console.error(err);
});
}
});
});
}