hfs
Version:
HTTP File Server
700 lines (699 loc) • 31.9 kB
JavaScript
// This file is part of HFS - Copyright 2021-2023, Massimo Melina <a@rejetto.com> - License https://www.gnu.org/licenses/gpl-3.0.txt
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.pluginsConfig = exports.suspendPlugins = exports.enablePlugins = exports.pluginsWatcher = exports.SERVER_CODE_ID = exports.Plugin = exports.pluginsMiddleware = exports.STORAGE_FOLDER = exports.DELETE_ME_SUFFIX = exports.DISABLING_SUFFIX = exports.PATH = void 0;
exports.isPluginRunning = isPluginRunning;
exports.isPluginEnabled = isPluginEnabled;
exports.enablePlugin = enablePlugin;
exports.stopPlugin = stopPlugin;
exports.startPlugin = startPlugin;
exports.setPluginConfig = setPluginConfig;
exports.getPluginInfo = getPluginInfo;
exports.findPluginByRepo = findPluginByRepo;
exports.getPluginConfigFields = getPluginConfigFields;
exports.mapPlugins = mapPlugins;
exports.firstPlugin = firstPlugin;
exports.getAvailablePlugins = getAvailablePlugins;
exports.rescan = rescan;
exports.parsePluginSource = parsePluginSource;
exports.getMissingDependencies = getMissingDependencies;
const fast_glob_1 = __importDefault(require("fast-glob"));
const watchLoad_1 = require("./watchLoad");
const lodash_1 = __importDefault(require("lodash"));
const const_1 = require("./const");
const Const = __importStar(require("./const"));
const misc_1 = require("./misc");
const misc = __importStar(require("./misc"));
const config_1 = require("./config");
const serveFile_1 = require("./serveFile");
const events_1 = __importDefault(require("./events"));
const promises_1 = require("fs/promises");
const fs_1 = require("fs");
const connections_1 = require("./connections");
const path_1 = require("path");
const customHtml_1 = require("./customHtml");
const kvstorage_1 = require("@rejetto/kvstorage");
const first_1 = require("./first");
const frontEndApis_1 = require("./frontEndApis");
const index_1 = require("./index");
const block_1 = require("./block");
const lang_1 = require("./lang");
const i18n_1 = require("./i18n");
const perm_1 = require("./perm");
const auth_1 = require("./auth");
const icons_1 = require("./icons");
exports.PATH = 'plugins';
exports.DISABLING_SUFFIX = '-disabled';
exports.DELETE_ME_SUFFIX = '-delete_me' + exports.DISABLING_SUFFIX;
exports.STORAGE_FOLDER = 'storage';
setTimeout(async () => {
for (const x of await (0, promises_1.readdir)(exports.PATH))
if (x.endsWith(exports.DELETE_ME_SUFFIX))
await (0, promises_1.rm)((0, path_1.join)(exports.PATH, x), { recursive: true, force: true }).catch(() => { });
}, 1000);
const plugins = new Map(); // now that we care about the order, a simple object wouldn't do, because numbers are always at the beginning
function isPluginRunning(id) {
var _a;
return Boolean((_a = plugins.get(id)) === null || _a === void 0 ? void 0 : _a.started);
}
function isPluginEnabled(id, considerSuspension = false) {
return (!considerSuspension || !exports.suspendPlugins.get()) && exports.enablePlugins.get().includes(id);
}
function enablePlugin(id, state = true) {
if (state && !getPluginInfo(id))
throw Error('miss');
exports.enablePlugins.set(arr => {
if (arr.includes(id) === state)
return arr;
console.log("switching plugin", id, state ? "on" : "off");
return arr.includes(id) === state ? arr
: state ? [...arr, id]
: arr.filter((x) => x !== id);
});
}
async function stopPlugin(id) {
enablePlugin(id, false);
await waitRunning(id, false);
}
async function startPlugin(id) {
var _a;
if ((_a = getPluginInfo(id)) === null || _a === void 0 ? void 0 : _a.isTheme)
await Promise.all(mapPlugins((pl, id) => pl.isTheme && stopPlugin(id)));
enablePlugin(id);
await waitRunning(id);
}
async function waitRunning(id, state = true) {
while (isPluginRunning(id) !== state) {
await (0, misc_1.wait)(500);
const error = getError(id);
if (error)
throw Error(error);
}
}
// nullish values are equivalent to defaultValues
function setPluginConfig(id, changes) {
exports.pluginsConfig.set(allConfigs => {
const fields = getPluginConfigFields(id);
const oldConfig = allConfigs[id];
const newConfig = changes && lodash_1.default.pickBy({ ...oldConfig, ...changes }, (v, k) => { var _a; return v != null && !(0, misc_1.same)(v, (_a = fields === null || fields === void 0 ? void 0 : fields[k]) === null || _a === void 0 ? void 0 : _a.defaultValue); });
return { ...allConfigs, [id]: lodash_1.default.isEmpty(newConfig) ? undefined : newConfig };
});
}
function getPluginInfo(id) {
const running = plugins.get(id);
return running && { ...running.getData(), ...running } || availablePlugins[id];
}
function findPluginByRepo(repo) {
for (const pl of plugins.values())
if (match(pl.getData()))
return pl;
return lodash_1.default.find(availablePlugins, match);
function match(rec) {
var _a, _b;
return repo === ((_b = (_a = rec === null || rec === void 0 ? void 0 : rec.repo) === null || _a === void 0 ? void 0 : _a.main) !== null && _b !== void 0 ? _b : rec === null || rec === void 0 ? void 0 : rec.repo);
}
}
function getPluginConfigFields(id) {
var _a;
return (_a = plugins.get(id)) === null || _a === void 0 ? void 0 : _a.getData().config;
}
async function initPlugin(pl, morePassedToInit) {
var _a;
const undoEvents = [];
const timeouts = [];
const res = await ((_a = pl.init) === null || _a === void 0 ? void 0 : _a.call(pl, {
Const,
require,
getConnections: connections_1.getConnections,
// intercept all subscriptions, so to be able to undo them on unload
events: Object.create(events_1.default, (0, misc_1.objFromKeys)(['on', 'once', 'multi'], k => ({
value() {
const ret = events_1.default[k](...arguments);
undoEvents.push(ret);
return ret;
}
}))),
log: console.log,
setError(msg) { setError((morePassedToInit === null || morePassedToInit === void 0 ? void 0 : morePassedToInit.id) || 'server_code', msg); },
getHfsConfig: config_1.getConfig,
setInterval() {
const ret = setInterval(...arguments);
timeouts.push(ret); // intervals can be canceled by clearTimeout (source: MDN)
return ret;
},
setTimeout() {
const ret = setTimeout(...arguments);
timeouts.push(ret);
return ret;
},
customApiCall,
notifyClient: frontEndApis_1.notifyClient,
addBlock: block_1.addBlock,
misc,
_: lodash_1.default,
ctxBelongsTo: perm_1.ctxBelongsTo,
getCurrentUsername: auth_1.getCurrentUsername,
getAccount: perm_1.getAccount, getUsernames: perm_1.getUsernames, addAccount: perm_1.addAccount, delAccount: perm_1.delAccount, updateAccount: perm_1.updateAccount, renameAccount: perm_1.renameAccount,
...morePassedToInit
}));
Object.assign(pl, typeof res === 'function' ? { unload: res } : res);
(0, misc_1.patchKey)(pl, 'unload', was => () => {
for (const x of timeouts)
clearTimeout(x);
for (const cb of undoEvents)
cb();
if (typeof was === 'function')
return was(...arguments);
});
events_1.default.emit('pluginInitialized', pl);
return pl;
}
const already = new Set();
function warnOnce(msg) {
if (already.has(msg))
return;
already.add(msg);
console.log('Warning: ' + msg);
}
const pluginsMiddleware = async (ctx, next) => {
var _a;
const after = {};
// run middleware plugins
let lastStatus = ctx.status;
let lastBody = ctx.body;
await Promise.all(mapPlugins(async (pl, id) => {
var _a;
try {
const res = await ((_a = pl.middleware) === null || _a === void 0 ? void 0 : _a.call(pl, ctx));
if (lastStatus !== ctx.status || lastBody !== ctx.body) {
console.debug("plugin changed response", id);
lastStatus = ctx.status;
lastBody = ctx.body;
}
if (res === true && !ctx.isStopped) { //legacy pre-0.53
ctx.stop();
warnOnce(`plugin ${id} is using deprecated API (return true on middleware) and may not work with future versions (check for an update to "${id}")`);
}
// don't just check ctx.isStopped, as the async plugin that called ctx.stop will reach here after sync ones
if (ctx.isStopped && !ctx.pluginBlockedRequest)
console.debug("plugin blocked request", ctx.pluginBlockedRequest = id);
if (typeof res === 'function')
after[id] = res;
}
catch (e) {
printError(id, e);
}
}));
// expose public plugins' files`
if (!ctx.isStopped) {
const { path } = ctx;
if (path.startsWith(const_1.PLUGINS_PUB_URI)) {
const a = path.substring(const_1.PLUGINS_PUB_URI.length).split('/');
const name = a.shift();
if (plugins.has(name)) { // do it only if the plugin is loaded
if ((_a = ctx.get('referer')) === null || _a === void 0 ? void 0 : _a.endsWith('/'))
ctx.state.considerAsGui = true;
await (0, serveFile_1.serveFile)(ctx, plugins.get(name).folder + '/public/' + a.join('/'), const_1.MIME_AUTO);
}
return;
}
if (ctx.body === undefined && ctx.status === const_1.HTTP_NOT_FOUND) // no response was provided by plugins, so we'll do
await next();
}
for (const [id, f] of Object.entries(after))
try {
await f();
}
catch (e) {
printError(id, e);
}
function printError(id, e) {
console.log(`error middleware plugin ${id}: ${(e === null || e === void 0 ? void 0 : e.message) || e}`);
console.debug(e);
}
};
exports.pluginsMiddleware = pluginsMiddleware;
events_1.default.once('app', () => Object.assign(index_1.app.context, {
isStopped: false,
stop() { return this.isStopped = true; }
}));
class Plugin {
constructor(id, folder, data, onUnload) {
this.id = id;
this.folder = folder;
this.data = data;
this.onUnload = onUnload;
this.started = new Date();
if (!data)
throw 'invalid data';
this.log = [];
this.data = data = { ...data }; // clone to make object modifiable. Objects coming from import are not.
// some validation
for (const k of ['frontend_css', 'frontend_js']) {
const v = data[k];
if (typeof v === 'string')
data[k] = [v];
else if (v && !Array.isArray(v)) {
delete data[k];
console.warn('invalid', k);
}
}
plugins.set(id, this);
const keys = Array.from(plugins.keys());
const idx = keys.indexOf(id);
const moveDown = (0, misc_1.onlyTruthy)(mapPlugins(((pl, plId, plIdx) => pl.afterPlugin === id && plIdx < idx && plId)));
const { beforePlugin, afterPlugin } = data; // or this plugin that wants to be considered before another
if (afterPlugin && keys.indexOf(afterPlugin) > idx)
moveDown.push(id);
if (beforePlugin && keys.indexOf(beforePlugin) < idx)
moveDown.push(beforePlugin);
for (const k of moveDown) {
const temp = plugins.get(k);
if (!temp)
continue;
plugins.delete(k);
plugins.set(k, temp);
}
}
get version() { var _a; return (_a = this.data) === null || _a === void 0 ? void 0 : _a.version; }
get description() { var _a; return (_a = this.data) === null || _a === void 0 ? void 0 : _a.description; }
get apiRequired() { var _a; return (_a = this.data) === null || _a === void 0 ? void 0 : _a.apiRequired; }
get isTheme() { var _a; return (_a = this.data) === null || _a === void 0 ? void 0 : _a.isTheme; }
get repo() { var _a; return (_a = this.data) === null || _a === void 0 ? void 0 : _a.repo; }
get depend() { var _a; return (_a = this.data) === null || _a === void 0 ? void 0 : _a.depend; }
get afterPlugin() { var _a; return (_a = this.data) === null || _a === void 0 ? void 0 : _a.afterPlugin; }
get beforePlugin() { var _a; return (_a = this.data) === null || _a === void 0 ? void 0 : _a.beforePlugin; }
get middleware() {
var _a;
return (_a = this.data) === null || _a === void 0 ? void 0 : _a.middleware;
}
get frontend_css() {
var _a;
return (_a = this.data) === null || _a === void 0 ? void 0 : _a.frontend_css;
}
get frontend_js() {
var _a;
return (_a = this.data) === null || _a === void 0 ? void 0 : _a.frontend_js;
}
get onDirEntry() {
var _a;
return (_a = this.data) === null || _a === void 0 ? void 0 : _a.onDirEntry;
}
getData() {
return this.data;
}
async unload(reloading = false) {
var _a, _b;
if (!this.started)
return;
this.started = null;
const { id } = this;
try {
await ((_b = (_a = this.data) === null || _a === void 0 ? void 0 : _a.unload) === null || _b === void 0 ? void 0 : _b.call(_a));
}
catch (e) {
console.log('error unloading plugin', id, String(e));
}
await this.onUnload();
if (!reloading && id !== exports.SERVER_CODE_ID) // we already printed 'reloading'
console.log('unloaded plugin', id);
if (this.data)
this.data.unload = undefined;
}
}
exports.Plugin = Plugin;
exports.SERVER_CODE_ID = '.'; // a name that will surely be not found among plugin folders
const serverCode = (0, config_1.defineConfig)('server_code', '', async (script, { k }) => {
var _a;
try {
(_a = (await serverCode.compiled())) === null || _a === void 0 ? void 0 : _a.unload();
}
catch (_b) { }
const res = {};
try {
new Function('exports', script)(res); // parse
await initPlugin(res);
res.getCustomHtml = () => (0, misc_1.callable)(res.customHtml) || {};
return new Plugin(exports.SERVER_CODE_ID, '', res, lodash_1.default.noop);
}
catch (e) {
return console.error(k + ':', e.message || String(e));
}
});
function mapPlugins(cb, includeServerCode = true) {
let i = 0;
return Array.from(plugins).map(([plName, pl]) => {
if (!includeServerCode && plName === exports.SERVER_CODE_ID)
return;
try {
return cb(pl, plName, i++);
}
catch (e) {
console.log('plugin error', plName, String(e));
}
}).filter(x => x !== undefined);
}
function firstPlugin(cb, includeServerCode = true) {
for (const [plName, pl] of plugins.entries()) {
if (!includeServerCode && plName === exports.SERVER_CODE_ID)
continue;
try {
const ret = cb(pl, plName);
if (ret !== undefined)
return ret;
}
catch (e) {
console.log('plugin error', plName, String(e));
}
}
}
let availablePlugins = {};
function getAvailablePlugins() {
return Object.values(availablePlugins);
}
const rescanAsap = (0, misc_1.debounceAsync)(rescan, { wait: 1000 });
if (!(0, fs_1.existsSync)(exports.PATH))
try {
(0, fs_1.mkdirSync)(exports.PATH);
}
catch (_a) { }
exports.pluginsWatcher = (0, misc_1.watchDir)(exports.PATH, rescanAsap);
exports.enablePlugins = (0, config_1.defineConfig)('enable_plugins', ['antibrute']);
exports.enablePlugins.sub(rescanAsap);
exports.suspendPlugins = (0, config_1.defineConfig)(misc_1.CFG.suspend_plugins, false);
exports.pluginsConfig = (0, config_1.defineConfig)('plugins_config', {});
const pluginWatchers = new Map();
async function rescan() {
console.debug('scanning plugins');
const patterns = [exports.PATH + '/*'];
if (const_1.APP_PATH !== process.cwd())
patterns.unshift((0, misc_1.adjustStaticPathForGlob)(const_1.APP_PATH) + '/' + patterns[0]); // first search bundled plugins, because otherwise they won't be loaded because of the folders with same name in .hfs/plugins (used for storage)
const met = [];
for (const { path, dirent } of await (0, fast_glob_1.default)(patterns, { onlyFiles: false, suppressErrors: true, objectMode: true })) {
if (!dirent.isDirectory() || path.endsWith(exports.DISABLING_SUFFIX))
continue;
const id = path.split('/').slice(-1)[0];
met.push(id);
if (!pluginWatchers.has(id))
pluginWatchers.set(id, watchPlugin(id, (0, path_1.join)(path, 'plugin.js')));
}
for (const [id, cancelWatcher] of pluginWatchers.entries())
if (!met.includes(id)) {
enablePlugin(id, false);
cancelWatcher();
pluginWatchers.delete(id);
}
}
function watchPlugin(id, path) {
console.debug('plugin watch', id);
const module = (0, path_1.resolve)(path);
let starting;
const unsub = exports.enablePlugins.sub(() => getPluginInfo(id) && considerStart()); // only after it has been loaded
const unsub2 = exports.suspendPlugins.sub(() => getPluginInfo(id) && considerStart());
function considerStart() {
const should = isPluginEnabled(id, true);
if (should === isPluginRunning(id))
return;
if (should) {
start();
return true;
}
stop();
}
const { unwatch } = (0, watchLoad_1.watchLoad)(module, async (source) => {
const notRunning = availablePlugins[id];
if (!source)
return onUninstalled();
if (isPluginEnabled(id, true))
return start();
const p = parsePluginSource(id, source);
if ((0, misc_1.same)(notRunning, p))
return;
availablePlugins[id] = p;
events_1.default.emit(notRunning ? 'pluginUpdated' : 'pluginInstalled', p);
});
return () => {
console.debug('plugin unwatch', id);
unsub();
unsub2();
unwatch();
return onUninstalled();
};
async function onUninstalled() {
await stop();
if (!getPluginInfo(id))
return; // already missing
delete availablePlugins[id];
events_1.default.emit('pluginUninstalled', id);
}
async function markItAvailable() {
plugins.delete(id);
availablePlugins[id] = await parsePlugin();
}
async function parsePlugin() {
return parsePluginSource(id, await (0, promises_1.readFile)(module, 'utf8'));
}
async function stop() {
await starting;
const p = plugins.get(id);
if (!p)
return;
await p.unload();
await markItAvailable().catch(() => events_1.default.emit('pluginUninstalled', id)); // when a running plugin is deleted, avoid error and report
events_1.default.emit('pluginStopped', p);
}
async function start() {
var _a;
if (starting)
return;
try {
starting = (0, misc_1.pendingPromise)();
// if dependencies are not ready right now, we give some time. Not super-solid but good enough for now.
const info = await parsePlugin();
if (!await (0, misc_1.waitFor)(() => lodash_1.default.isEmpty(getMissingDependencies(info)), { timeout: 5000 }))
throw Error("plugin missing dependencies: " + lodash_1.default.map(getMissingDependencies(info), x => x.repo).join(', '));
if (getPluginInfo(id))
setError(id, '');
const alreadyRunning = plugins.get(id);
console.log(alreadyRunning ? "reloading plugin" : "loading plugin", id);
const pluginData = require(module);
deleteModule(require.resolve(module)); // avoid caching at next import
calculateBadApi(pluginData);
if (pluginData.badApi)
throw Error(pluginData.badApi);
await (alreadyRunning === null || alreadyRunning === void 0 ? void 0 : alreadyRunning.unload(true));
console.debug("starting plugin", id);
const storageDir = (0, path_1.resolve)(exports.PATH, id, exports.STORAGE_FOLDER) + (const_1.IS_WINDOWS ? '\\' : '/');
if (!module.startsWith(process.cwd())) //legacy pre-0.53.0, bundled plugins' storageDir was not under cwd
await (0, promises_1.rename)((0, path_1.resolve)(module, '..', exports.STORAGE_FOLDER), storageDir).catch(() => { });
await (0, promises_1.mkdir)(storageDir, { recursive: true });
const openDbs = [];
const subbedConfigs = [];
const pluginReady = (0, misc_1.pendingPromise)();
const MAX_LOG = 100;
await initPlugin(pluginData, {
id,
srcDir: __dirname,
storageDir,
async openDb(filename, options) {
if (!filename)
throw Error("missing filename");
const db = new kvstorage_1.KvStorage(options);
await db.open((0, path_1.join)(storageDir, filename));
openDbs.push(db);
return db;
},
log(...args) {
console.log('plugin', id + ':', ...args);
pluginReady.then(() => {
if (!plugin)
return;
const msg = { ts: new Date, msg: args.map(x => x && typeof x === 'object' ? JSON.stringify(x) : String(x)).join(' ') };
plugin.log.push(msg);
if (plugin.log.length > MAX_LOG)
plugin.log.splice(0, 10); // truncate
events_1.default.emit('pluginLog:' + id, msg);
events_1.default.emit('pluginLog', id, msg);
});
},
getConfig(cfgKey) {
var _a, _b, _c, _d;
const cur = (_a = exports.pluginsConfig.get()) === null || _a === void 0 ? void 0 : _a[id];
return cfgKey ? (_b = cur === null || cur === void 0 ? void 0 : cur[cfgKey]) !== null && _b !== void 0 ? _b : (_d = (_c = pluginData.config) === null || _c === void 0 ? void 0 : _c[cfgKey]) === null || _d === void 0 ? void 0 : _d.defaultValue
: lodash_1.default.defaults(cur, (0, misc_1.objSameKeys)(pluginData.config, x => x.defaultValue));
},
setConfig: (cfgKey, value) => setPluginConfig(id, { [cfgKey]: value }),
subscribeConfig(cfgKey, cb) {
const get = () => Array.isArray(cfgKey) ? (0, misc_1.objFromKeys)(cfgKey, k => this.getConfig(k))
: this.getConfig(cfgKey);
let last = get();
cb(last);
const ret = exports.pluginsConfig.sub(() => {
const now = get();
if ((0, misc_1.same)(now, last))
return;
try {
cb(last = now);
}
catch (e) {
this.log(String(e));
}
});
subbedConfigs.push(ret);
return ret;
},
async i18n(ctx) {
return (0, i18n_1.i18nFromTranslations)(await (0, lang_1.getLangData)(ctx));
},
});
const folder = (0, path_1.dirname)(module);
const { sections, unwatch } = (0, customHtml_1.watchLoadCustomHtml)(folder);
pluginData.getCustomHtml = () => Object.assign(Object.fromEntries(sections), (0, misc_1.callable)(pluginData.customHtml) || {});
const unwatchIcons = (0, icons_1.watchIconsFolder)(folder, v => plugin.icons = v);
const plugin = new Plugin(id, folder, pluginData, async () => {
unwatchIcons();
unwatch();
for (const x of subbedConfigs)
x();
await Promise.allSettled(openDbs.map(x => x.close()));
openDbs.length = 0;
});
pluginReady.resolve();
if (alreadyRunning)
events_1.default.emit('pluginUpdated', Object.assign(lodash_1.default.pick(plugin, 'started'), getPluginInfo(id)));
else {
const wasInstalled = availablePlugins[id];
if (wasInstalled)
delete availablePlugins[id];
events_1.default.emit(wasInstalled ? 'pluginStarted' : 'pluginInstalled', plugin);
}
events_1.default.emit('pluginStarted:' + id);
}
catch (e) {
await markItAvailable();
const parsed = (_a = e.stack) === null || _a === void 0 ? void 0 : _a.split('\n\n'); // this form is used by syntax-errors inside the plugin, which is useful to show
const where = (parsed === null || parsed === void 0 ? void 0 : parsed.length) > 1 ? `\n${parsed[0]}` : '';
e = (0, misc_1.prefix)('', e.message, where) || String(e);
setError(id, e);
}
finally {
starting === null || starting === void 0 ? void 0 : starting.resolve();
starting = undefined;
}
}
}
function customApiCall(method, ...params) {
return mapPlugins(pl => { var _a, _b; return (_b = (_a = pl.getData().customApi) === null || _a === void 0 ? void 0 : _a[method]) === null || _b === void 0 ? void 0 : _b.call(_a, ...params); });
}
function getError(id) {
var _a;
return (_a = getPluginInfo(id)) === null || _a === void 0 ? void 0 : _a.error;
}
// returns true if there's an error, and it has changed
function setError(id, error) {
const info = getPluginInfo(id);
if (!info)
return;
if (info.error === error)
return;
info.error = error;
events_1.default.emit('pluginUpdated', info);
if (!error)
return;
console.warn(`plugin error: ${id}:`, error);
return true;
}
function deleteModule(id) {
var _a;
var _b;
const { cache } = require;
// build reversed map of dependencies
const requiredBy = { '.': ['.'] }; // don't touch main entry
for (const k in cache)
if (k !== id)
for (const child of (0, misc_1.wantArray)((_a = cache[k]) === null || _a === void 0 ? void 0 : _a.children))
(requiredBy[_b = child.id] || (requiredBy[_b] = [])).push(k);
const deleted = [];
(function deleteCache(id) {
const mod = cache[id];
if (!mod)
return;
delete cache[id];
deleted.push(id);
for (const child of mod.children)
if (!lodash_1.default.difference(requiredBy[child.id], deleted).length)
deleteCache(child.id);
})(id);
}
(0, first_1.onProcessExit)(() => Promise.allSettled(mapPlugins(pl => pl.unload())));
function parsePluginSource(id, source) {
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o;
const pl = { id };
pl.description = (0, misc_1.tryJson)((_a = /exports.description *= *(".*")/.exec(source)) === null || _a === void 0 ? void 0 : _a[1]);
pl.repo = (0, misc_1.tryJson)((_b = /exports.repo *= *(.*);? *$/m.exec(source)) === null || _b === void 0 ? void 0 : _b[1]);
pl.version = (_d = Number((_c = /exports.version *= *(\d*\.?\d+)/.exec(source)) === null || _c === void 0 ? void 0 : _c[1])) !== null && _d !== void 0 ? _d : undefined;
pl.apiRequired = (_f = (0, misc_1.tryJson)((_e = /exports.apiRequired *= *([ \d.,[\]]+)/.exec(source)) === null || _e === void 0 ? void 0 : _e[1])) !== null && _f !== void 0 ? _f : undefined;
pl.isTheme = (_h = (0, misc_1.tryJson)((_g = /exports.isTheme *= *(true|false|"light"|"dark")/.exec(source)) === null || _g === void 0 ? void 0 : _g[1])) !== null && _h !== void 0 ? _h : (id.endsWith('-theme') || undefined);
pl.preview = (_k = (0, misc_1.tryJson)((_j = /exports.preview *= *(.+)/.exec(source)) === null || _j === void 0 ? void 0 : _j[1])) !== null && _k !== void 0 ? _k : undefined;
pl.depend = (_m = (0, misc_1.tryJson)((_l = /exports.depend *= *(\[[\s\S]*?])/m.exec(source)) === null || _l === void 0 ? void 0 : _l[1])) === null || _m === void 0 ? void 0 : _m.filter((x) => typeof x.repo === 'string' && x.version === undefined || typeof x.version === 'number'
|| console.warn("plugin dependency discarded", x));
pl.changelog = (0, misc_1.tryJson)((_o = /exports.changelog *= *(\[[\s\S]*?])/m.exec(source)) === null || _o === void 0 ? void 0 : _o[1]);
if (Array.isArray(pl.apiRequired) && (pl.apiRequired.length !== 2 || !pl.apiRequired.every(lodash_1.default.isFinite))) // validate [from,to] form
pl.apiRequired = undefined;
calculateBadApi(pl);
return pl;
}
function calculateBadApi(data) {
const r = data.apiRequired;
const [min, max] = Array.isArray(r) ? r : [r, r]; // normalize data type
data.badApi = min > const_1.API_VERSION ? "may not work correctly as it is designed for a newer version of HFS - check for updates"
: max < const_1.COMPATIBLE_API_VERSION ? "may not work correctly as it is designed for an older version of HFS - check for updates"
: undefined;
}
function getMissingDependencies(plugin) {
return (0, misc_1.onlyTruthy)(((plugin === null || plugin === void 0 ? void 0 : plugin.depend) || []).map((dep) => {
const res = findPluginByRepo(dep.repo);
const error = !res ? 'missing'
: (res.version || 0) < dep.version ? 'version'
: !isPluginEnabled(res.id) ? 'disabled'
: !isPluginRunning(res.id) ? 'stopped'
: '';
return error && { repo: dep.repo, error, id: res === null || res === void 0 ? void 0 : res.id };
}));
}
;