myst-cli
Version:
Command line tools for MyST
108 lines (107 loc) • 4.84 kB
JavaScript
import fs from 'node:fs';
import { pathToFileURL } from 'node:url';
import { RuleId, plural } from 'myst-common';
import { addWarningForFile } from './utils/addWarningForFile.js';
import { loadExecutablePlugin } from './executablePlugin.js';
/**
* Load user-defined plugin modules declared in the project frontmatter
*
* @param session session with logging
*/
export async function loadPlugins(session, plugins) {
var _a;
const loadedPlugins = (_a = session.plugins) !== null && _a !== void 0 ? _a : {
directives: [],
roles: [],
transforms: [],
paths: [],
};
// Deduplicate by path...
const newPlugins = [...new Map(plugins.map((info) => [info.path, info])).values()].filter(
// ...and filter out already loaded plugins
({ path }) => !loadedPlugins.paths.includes(path));
if (newPlugins.length === 0) {
return loadedPlugins;
}
session.log.debug(`Loading plugins: "${newPlugins.map((info) => `${info.path} (${info.type})`).join('", "')}"`);
const modules = await Promise.all(newPlugins.map(async (info) => {
var _a, _b;
const { type, path } = info;
switch (type) {
case 'executable': {
// Ensure the plugin is a file
if (!((_a = fs.statSync(path, { throwIfNoEntry: false })) === null || _a === void 0 ? void 0 : _a.isFile())) {
addWarningForFile(session, path, `Unknown plugin "${path}", it must be an executable file`, 'error', {
ruleId: RuleId.pluginLoads,
});
return null;
}
// Ensure the plugin is executable
try {
fs.accessSync(path, fs.constants.X_OK);
}
catch (err) {
addWarningForFile(session, path, `Plugin "${path}" is not executable`, 'error', {
ruleId: RuleId.pluginLoads,
});
return null;
}
const plugin = await loadExecutablePlugin(session, info.path);
if (plugin === undefined) {
addWarningForFile(session, path, `Non-zero exit code after querying executable "${path}" for plugin specification`, 'error', {
ruleId: RuleId.pluginLoads,
});
return null;
}
return { path, module: { plugin } };
}
case 'javascript': {
if (!((_b = fs.statSync(path, { throwIfNoEntry: false })) === null || _b === void 0 ? void 0 : _b.isFile()) || !path.endsWith('.mjs')) {
addWarningForFile(session, path, `Unknown plugin "${path}", it must be an mjs file`, 'error', {
ruleId: RuleId.pluginLoads,
});
return null;
}
let module;
const pathURL = pathToFileURL(path);
try {
module = await import(pathURL.toString());
}
catch (error) {
session.log.debug(`\n\n${error === null || error === void 0 ? void 0 : error.stack}\n\n`);
addWarningForFile(session, path, `Error reading plugin: ${error}`, 'error', {
ruleId: RuleId.pluginLoads,
});
return null;
}
return { path, module };
}
}
}));
modules.forEach((pluginLoader) => {
var _a;
if (!pluginLoader)
return;
const plugin = pluginLoader.module.default || pluginLoader.module.plugin;
const directives = plugin.directives || pluginLoader.module.directives;
const roles = plugin.roles || pluginLoader.module.roles;
const transforms = plugin.transforms || pluginLoader.module.transforms;
session.log.info(`🔌 ${(_a = plugin === null || plugin === void 0 ? void 0 : plugin.name) !== null && _a !== void 0 ? _a : 'Unnamed Plugin'} (${pluginLoader.path}) loaded: ${plural('%s directive(s)', directives)}, ${plural('%s role(s)', roles)}, ${plural('%s transform(s)', transforms)}`);
if (directives) {
// TODO: validate each directive
loadedPlugins.directives.push(...directives);
}
if (roles) {
// TODO: validate each role
loadedPlugins.roles.push(...roles);
}
if (transforms) {
// TODO: validate each transform
loadedPlugins.transforms.push(...transforms);
}
loadedPlugins.paths.push(pluginLoader.path);
});
session.plugins = loadedPlugins;
session.log.debug('Plugins loaded');
return loadedPlugins;
}