'use strict'; const fs = require('fs').promises; const hooks = require('./hooks'); const log4js = require('log4js'); const path = require('path'); const runCmd = require('../../../node/utils/run_cmd'); const tsort = require('./tsort'); const pluginUtils = require('./shared'); const defs = require('./plugin_defs'); const logger = log4js.getLogger('plugins'); // Log the version of npm at startup. let loggedVersion = false; (async () => { if (loggedVersion) return; loggedVersion = true; const p = runCmd(['npm', '--version'], {stdoutLogger: null}); const chunks = []; await Promise.all([ (async () => { for await (const chunk of p.stdout) chunks.push(chunk); })(), p, // Await in parallel to avoid unhandled rejection if p rejects during chunk read. ]); const version = Buffer.concat(chunks).toString().replace(/\n+$/g, ''); logger.info(`npm --version: ${version}`); })().catch((err) => { logger.error(`Failed to get npm version: ${err.stack || err}`); // This isn't a fatal error so don't re-throw. }); exports.prefix = 'ep_'; exports.formatPlugins = () => Object.keys(defs.plugins).join(', '); exports.formatParts = () => defs.parts.map((part) => part.full_name).join('\n'); exports.formatHooks = (hookSetName) => { const res = []; const hooks = pluginUtils.extractHooks(defs.parts, hookSetName || 'hooks'); for (const registeredHooks of Object.values(hooks)) { for (const hook of registeredHooks) { res.push(`
${hook.hook_name}
${hook.hook_fn_name} ` + `from ${hook.part.full_name}
`); } } return `
${res.join('\n')}
`; }; const callInit = async () => { await Promise.all(Object.keys(defs.plugins).map(async (pluginName) => { const plugin = defs.plugins[pluginName]; const epInit = path.join(plugin.package.path, '.ep_initialized'); try { await fs.stat(epInit); } catch (err) { await fs.writeFile(epInit, 'done'); await hooks.aCallAll(`init_${pluginName}`, {}); } })); }; exports.pathNormalization = (part, hookFnName, hookName) => { const tmp = hookFnName.split(':'); // hookFnName might be something like 'C:\\foo.js:myFunc'. // If there is a single colon assume it's 'filename:funcname' not 'C:\\filename'. const functionName = (tmp.length > 1 ? tmp.pop() : null) || hookName; const moduleName = tmp.join(':') || part.plugin; const packageDir = path.dirname(defs.plugins[part.plugin].package.path); const fileName = path.join(packageDir, moduleName); return `${fileName}:${functionName}`; }; exports.update = async () => { const packages = await exports.getPackages(); const parts = {}; // Key is full name. sortParts converts this into a topologically sorted array. const plugins = {}; // Load plugin metadata ep.json await Promise.all(Object.keys(packages).map(async (pluginName) => { logger.info(`Loading plugin ${pluginName}...`); await loadPlugin(packages, pluginName, plugins, parts); })); logger.info(`Loaded ${Object.keys(packages).length} plugins`); defs.plugins = plugins; defs.parts = sortParts(parts); defs.hooks = pluginUtils.extractHooks(defs.parts, 'hooks', exports.pathNormalization); defs.loaded = true; await callInit(); }; exports.getPackages = async () => { logger.info('Running npm to get a list of installed plugins...'); // Notes: // * Do not pass `--prod` otherwise `npm ls` will fail if there is no `package.json`. // * The `--no-production` flag is required (or the `NODE_ENV` environment variable must be // unset or set to `development`) because otherwise `npm ls` will not mention any packages // that are not included in `package.json` (which is expected to not exist). const p = runCmd(['npm', 'ls', '--long', '--json', '--depth=0', '--no-production'], { stdoutLogger: null, // We want to capture stdout, so don't attempt to log it. }); const chunks = []; await Promise.all([ (async () => { for await (const chunk of p.stdout) chunks.push(chunk); })(), p, // Await in parallel to avoid unhandled rejection if p rejects during chunk read. ]); const {dependencies = {}} = JSON.parse(Buffer.concat(chunks).toString()); await Promise.all(Object.entries(dependencies).map(async ([pkg, info]) => { if (!pkg.startsWith(exports.prefix)) { delete dependencies[pkg]; return; } info.realPath = await fs.realpath(info.path); })); return dependencies; }; const loadPlugin = async (packages, pluginName, plugins, parts) => { const pluginPath = path.resolve(packages[pluginName].path, 'ep.json'); try { const data = await fs.readFile(pluginPath); try { const plugin = JSON.parse(data); plugin.package = packages[pluginName]; plugins[pluginName] = plugin; for (const part of plugin.parts) { part.plugin = pluginName; part.full_name = `${pluginName}/${part.name}`; parts[part.full_name] = part; } } catch (err) { logger.error(`Unable to parse plugin definition file ${pluginPath}: ${err.stack || err}`); } } catch (err) { logger.error(`Unable to load plugin definition file ${pluginPath}: ${err.stack || err}`); } }; const partsToParentChildList = (parts) => { const res = []; for (const name of Object.keys(parts)) { for (const childName of parts[name].post || []) { res.push([name, childName]); } for (const parentName of parts[name].pre || []) { res.push([parentName, name]); } if (!parts[name].pre && !parts[name].post) { res.push([name, `:${name}`]); // Include apps with no dependency info } } return res; }; // Used only in Node, so no need for _ const sortParts = (parts) => tsort(partsToParentChildList(parts)) .filter((name) => parts[name] !== undefined) .map((name) => parts[name]);