2021-01-19 16:37:12 +00:00
|
|
|
'use strict';
|
|
|
|
|
2020-11-13 13:38:31 -05:00
|
|
|
const fs = require('fs').promises;
|
2020-11-13 13:41:08 -05:00
|
|
|
const hooks = require('./hooks');
|
2021-02-10 01:34:38 -05:00
|
|
|
const log4js = require('log4js');
|
2020-11-23 13:24:19 -05:00
|
|
|
const path = require('path');
|
2021-02-17 19:54:49 -05:00
|
|
|
const runCmd = require('../../../node/utils/run_cmd');
|
2020-11-23 13:24:19 -05:00
|
|
|
const tsort = require('./tsort');
|
|
|
|
const pluginUtils = require('./shared');
|
|
|
|
const defs = require('./plugin_defs');
|
2012-03-01 18:45:02 +01:00
|
|
|
|
2021-02-10 01:34:38 -05:00
|
|
|
const logger = log4js.getLogger('plugins');
|
|
|
|
|
2021-02-17 19:54:49 -05:00
|
|
|
// Log the version of npm at startup.
|
|
|
|
(async () => {
|
2021-02-18 01:26:16 -05:00
|
|
|
try {
|
2023-10-02 19:06:25 +02:00
|
|
|
const version = await runCmd(['bun', '--version'], {stdio: [null, 'string']});
|
|
|
|
logger.info(`bun --version: ${version}`);
|
2021-02-18 01:26:16 -05:00
|
|
|
} catch (err) {
|
2023-10-02 19:06:25 +02:00
|
|
|
logger.error(`Failed to get bun version: ${err.stack || err}`);
|
2021-02-18 01:26:16 -05:00
|
|
|
// This isn't a fatal error so don't re-throw.
|
|
|
|
}
|
|
|
|
})();
|
2021-02-17 19:54:49 -05:00
|
|
|
|
2012-03-01 18:45:02 +01:00
|
|
|
exports.prefix = 'ep_';
|
|
|
|
|
2021-01-19 16:37:12 +00:00
|
|
|
exports.formatPlugins = () => Object.keys(defs.plugins).join(', ');
|
2012-03-01 18:45:02 +01:00
|
|
|
|
2021-02-03 18:31:42 -05:00
|
|
|
exports.formatParts = () => defs.parts.map((part) => part.full_name).join('\n');
|
2012-03-01 18:45:02 +01:00
|
|
|
|
2021-03-03 03:14:15 -05:00
|
|
|
exports.formatHooks = (hookSetName, html) => {
|
|
|
|
let hooks = new Map();
|
|
|
|
for (const [pluginName, def] of Object.entries(defs.plugins)) {
|
|
|
|
for (const part of def.parts) {
|
|
|
|
for (const [hookName, hookFnName] of Object.entries(part[hookSetName] || {})) {
|
|
|
|
let hookEntry = hooks.get(hookName);
|
|
|
|
if (!hookEntry) {
|
|
|
|
hookEntry = new Map();
|
|
|
|
hooks.set(hookName, hookEntry);
|
|
|
|
}
|
|
|
|
let pluginEntry = hookEntry.get(pluginName);
|
|
|
|
if (!pluginEntry) {
|
|
|
|
pluginEntry = new Map();
|
|
|
|
hookEntry.set(pluginName, pluginEntry);
|
|
|
|
}
|
|
|
|
pluginEntry.set(part.name, hookFnName);
|
|
|
|
}
|
2021-02-03 18:31:42 -05:00
|
|
|
}
|
|
|
|
}
|
2021-03-03 03:14:15 -05:00
|
|
|
const lines = [];
|
|
|
|
const sortStringKeys = (a, b) => String(a[0]).localeCompare(b[0]);
|
|
|
|
if (html) lines.push('<dl>');
|
|
|
|
hooks = new Map([...hooks].sort(sortStringKeys));
|
|
|
|
for (const [hookName, hookEntry] of hooks) {
|
|
|
|
lines.push(html ? ` <dt>${hookName}:</dt><dd><dl>` : ` ${hookName}:`);
|
|
|
|
const sortedHookEntry = new Map([...hookEntry].sort(sortStringKeys));
|
|
|
|
hooks.set(hookName, sortedHookEntry);
|
|
|
|
for (const [pluginName, pluginEntry] of sortedHookEntry) {
|
|
|
|
lines.push(html ? ` <dt>${pluginName}:</dt><dd><dl>` : ` ${pluginName}:`);
|
|
|
|
const sortedPluginEntry = new Map([...pluginEntry].sort(sortStringKeys));
|
|
|
|
sortedHookEntry.set(pluginName, sortedPluginEntry);
|
|
|
|
for (const [partName, hookFnName] of sortedPluginEntry) {
|
|
|
|
lines.push(html
|
|
|
|
? ` <dt>${partName}:</dt><dd>${hookFnName}</dd>`
|
|
|
|
: ` ${partName}: ${hookFnName}`);
|
|
|
|
}
|
|
|
|
if (html) lines.push(' </dl></dd>');
|
|
|
|
}
|
|
|
|
if (html) lines.push(' </dl></dd>');
|
|
|
|
}
|
|
|
|
if (html) lines.push('</dl>');
|
|
|
|
return lines.join('\n');
|
2012-04-04 09:51:46 +02:00
|
|
|
};
|
2012-03-01 18:45:02 +01:00
|
|
|
|
2021-02-03 18:31:42 -05:00
|
|
|
exports.pathNormalization = (part, hookFnName, hookName) => {
|
|
|
|
const tmp = hookFnName.split(':'); // hookFnName might be something like 'C:\\foo.js:myFunc'.
|
2020-11-13 14:33:44 -05:00
|
|
|
// If there is a single colon assume it's 'filename:funcname' not 'C:\\filename'.
|
2021-02-03 18:31:42 -05:00
|
|
|
const functionName = (tmp.length > 1 ? tmp.pop() : null) || hookName;
|
2020-11-13 14:33:44 -05:00
|
|
|
const moduleName = tmp.join(':') || part.plugin;
|
2020-09-06 15:27:18 -04:00
|
|
|
const packageDir = path.dirname(defs.plugins[part.plugin].package.path);
|
2021-02-10 01:12:43 -05:00
|
|
|
const fileName = path.join(packageDir, moduleName);
|
2020-11-13 14:33:44 -05:00
|
|
|
return `${fileName}:${functionName}`;
|
2020-11-23 13:24:19 -05:00
|
|
|
};
|
2012-05-28 18:33:03 -07:00
|
|
|
|
2021-01-19 16:37:12 +00:00
|
|
|
exports.update = async () => {
|
2020-11-23 13:24:19 -05:00
|
|
|
const packages = await exports.getPackages();
|
|
|
|
const parts = {}; // Key is full name. sortParts converts this into a topologically sorted array.
|
|
|
|
const plugins = {};
|
2019-01-18 13:52:37 +00:00
|
|
|
|
|
|
|
// Load plugin metadata ep.json
|
2021-02-10 01:34:38 -05:00
|
|
|
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`);
|
2012-03-01 18:45:02 +01:00
|
|
|
|
2020-11-13 13:59:20 -05:00
|
|
|
defs.plugins = plugins;
|
|
|
|
defs.parts = sortParts(parts);
|
|
|
|
defs.hooks = pluginUtils.extractHooks(defs.parts, 'hooks', exports.pathNormalization);
|
|
|
|
defs.loaded = true;
|
2022-02-21 03:25:32 -05:00
|
|
|
await Promise.all(Object.keys(defs.plugins).map(async (p) => {
|
|
|
|
const logger = log4js.getLogger(`plugin:${p}`);
|
|
|
|
await hooks.aCallAll(`init_${p}`, {logger});
|
|
|
|
}));
|
2020-11-23 13:24:19 -05:00
|
|
|
};
|
2019-01-18 13:52:37 +00:00
|
|
|
|
2021-01-19 16:37:12 +00:00
|
|
|
exports.getPackages = async () => {
|
2021-02-10 01:34:38 -05:00
|
|
|
logger.info('Running npm to get a list of installed plugins...');
|
2021-02-10 03:04:28 -05:00
|
|
|
// 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).
|
2023-10-02 19:46:59 +02:00
|
|
|
const cmd = ['bun', 'ls', '--long', '--json', '--depth=0', '--no-production'];
|
2021-02-18 01:26:16 -05:00
|
|
|
const {dependencies = {}} = JSON.parse(await runCmd(cmd, {stdio: [null, 'string']}));
|
2021-02-08 19:19:49 -05:00
|
|
|
await Promise.all(Object.entries(dependencies).map(async ([pkg, info]) => {
|
|
|
|
if (!pkg.startsWith(exports.prefix)) {
|
|
|
|
delete dependencies[pkg];
|
|
|
|
return;
|
2021-02-03 18:31:42 -05:00
|
|
|
}
|
2021-02-08 19:19:49 -05:00
|
|
|
info.realPath = await fs.realpath(info.path);
|
|
|
|
}));
|
|
|
|
return dependencies;
|
2012-05-28 17:46:14 -07:00
|
|
|
};
|
2012-03-01 18:45:02 +01:00
|
|
|
|
2021-02-03 18:31:42 -05:00
|
|
|
const loadPlugin = async (packages, pluginName, plugins, parts) => {
|
|
|
|
const pluginPath = path.resolve(packages[pluginName].path, 'ep.json');
|
2019-01-18 13:52:37 +00:00
|
|
|
try {
|
2021-02-03 18:31:42 -05:00
|
|
|
const data = await fs.readFile(pluginPath);
|
2019-01-18 13:52:37 +00:00
|
|
|
try {
|
2020-11-23 13:24:19 -05:00
|
|
|
const plugin = JSON.parse(data);
|
2021-02-03 18:31:42 -05:00
|
|
|
plugin.package = packages[pluginName];
|
|
|
|
plugins[pluginName] = plugin;
|
|
|
|
for (const part of plugin.parts) {
|
|
|
|
part.plugin = pluginName;
|
|
|
|
part.full_name = `${pluginName}/${part.name}`;
|
2019-01-18 13:52:37 +00:00
|
|
|
parts[part.full_name] = part;
|
2021-02-03 18:31:42 -05:00
|
|
|
}
|
2021-02-10 01:34:38 -05:00
|
|
|
} catch (err) {
|
|
|
|
logger.error(`Unable to parse plugin definition file ${pluginPath}: ${err.stack || err}`);
|
2012-03-01 18:45:02 +01:00
|
|
|
}
|
2021-02-10 01:34:38 -05:00
|
|
|
} catch (err) {
|
|
|
|
logger.error(`Unable to load plugin definition file ${pluginPath}: ${err.stack || err}`);
|
2019-01-18 13:52:37 +00:00
|
|
|
}
|
2021-01-19 16:37:12 +00:00
|
|
|
};
|
2012-03-01 18:45:02 +01:00
|
|
|
|
2021-01-19 16:37:12 +00:00
|
|
|
const partsToParentChildList = (parts) => {
|
2020-11-23 13:24:19 -05:00
|
|
|
const res = [];
|
2021-02-03 18:31:42 -05:00
|
|
|
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]);
|
|
|
|
}
|
2012-03-01 18:45:02 +01:00
|
|
|
if (!parts[name].pre && !parts[name].post) {
|
2020-11-23 13:24:19 -05:00
|
|
|
res.push([name, `:${name}`]); // Include apps with no dependency info
|
2012-03-01 18:45:02 +01:00
|
|
|
}
|
2021-02-03 18:31:42 -05:00
|
|
|
}
|
2012-03-01 18:45:02 +01:00
|
|
|
return res;
|
2021-01-19 16:37:12 +00:00
|
|
|
};
|
2012-03-17 13:36:42 +01:00
|
|
|
|
|
|
|
// Used only in Node, so no need for _
|
2021-01-19 16:37:12 +00:00
|
|
|
const sortParts = (parts) => tsort(partsToParentChildList(parts))
|
|
|
|
.filter((name) => parts[name] !== undefined)
|
|
|
|
.map((name) => parts[name]);
|