UNPKG

hfs

Version:
700 lines (699 loc) 31.9 kB
"use strict"; // 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 }; })); }