/** * 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; } }