UNPKG

@koishijs/loader

Version:
386 lines (384 loc) 12.6 kB
var __defProp = Object.defineProperty; var __name = (target, value) => __defProp(target, "name", { value, configurable: true }); var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, { get: (a, b) => (typeof require !== "undefined" ? require : a)[b] }) : x)(function(x) { if (typeof require !== "undefined") return require.apply(this, arguments); throw Error('Dynamic require of "' + x + '" is not supported'); }); // src/shared.ts import { Context, interpolate, isNullable, Logger, Universal, valueMap, version } from "@koishijs/core"; import { constants, promises as fs } from "fs"; import * as yaml from "js-yaml"; import * as path from "path"; function unwrapExports(module) { return module?.default || module; } __name(unwrapExports, "unwrapExports"); function separate(source, isGroup = false) { const config = {}, meta = {}; for (const [key, value] of Object.entries(source || {})) { if (key.startsWith("$")) { meta[key] = value; } else { config[key] = value; } } return [isGroup ? source : config, meta]; } __name(separate, "separate"); var kUpdate = Symbol("update"); var group = { name: "group", reusable: true, apply(ctx, plugins) { ctx.scope[Loader.kRecord] ||= /* @__PURE__ */ Object.create(null); for (const name in plugins || {}) { if (name.startsWith("~") || name.startsWith("$")) continue; ctx.loader.reload(ctx, name, plugins[name]); } ctx.accept((neo) => { const old = ctx.scope.config; for (const key in { ...old, ...neo }) { if (key.startsWith("~") || key.startsWith("$")) continue; const fork = ctx.scope[Loader.kRecord][key]; if (!fork) { ctx.loader.reload(ctx, key, neo[key]); } else if (!(key in neo)) { ctx.loader.unload(ctx, key); } else { ctx.loader.reload(ctx, key, neo[key] || {}); } } }, { passive: true }); } }; function insertKey(object, temp, rest) { for (const key of rest) { temp[key] = object[key]; delete object[key]; } Object.assign(object, temp); } __name(insertKey, "insertKey"); function rename(object, old, neo, value) { const keys = Object.keys(object); const index = keys.findIndex((key) => key === old || key === "~" + old); const rest = index < 0 ? [] : keys.slice(index + 1); const temp = { [neo]: value }; delete object[old]; delete object["~" + old]; insertKey(object, temp, rest); } __name(rename, "rename"); var writable = { ".json": "application/json", ".yaml": "application/yaml", ".yml": "application/yaml" }; var Loader = class _Loader { static { __name(this, "Loader"); } static kRecord = Symbol.for("koishi.loader.record"); static exitCode = 51; static extensions = new Set(Object.keys(writable)); // process baseDir = process.cwd(); envData = process.env.KOISHI_SHARED ? JSON.parse(process.env.KOISHI_SHARED) : { startTime: Date.now() }; params = { env: process.env }; app; config; entry; suspend = false; writable = false; mime; filename; envFiles; names = /* @__PURE__ */ new Set(); cache = /* @__PURE__ */ Object.create(null); prolog = []; store = /* @__PURE__ */ new WeakMap(); _writeTask; _writeSlient = true; constructor() { Logger.targets.push({ colors: 3, record: /* @__PURE__ */ __name((record) => { this.prolog.push(record); this.prolog = this.prolog.slice(-1e3); }, "record") }); } async init(filename) { if (filename) { filename = path.resolve(this.baseDir, filename); const stats = await fs.stat(filename); if (stats.isFile()) { this.filename = filename; this.baseDir = path.dirname(filename); const extname2 = path.extname(filename); this.mime = writable[extname2]; if (!_Loader.extensions.has(extname2)) { throw new Error(`extension "${extname2}" not supported`); } } else { this.baseDir = filename; await this.findConfig(); } } else { await this.findConfig(); } if (this.mime) { try { await fs.access(this.filename, constants.W_OK); this.writable = true; } catch { } } this.envFiles = [ path.resolve(this.baseDir, ".env"), path.resolve(this.baseDir, ".env.local") ]; } async findConfig() { const files = await fs.readdir(this.baseDir); for (const basename of ["koishi.config", "koishi"]) { for (const extname2 of _Loader.extensions) { if (files.includes(basename + extname2)) { this.mime = writable[extname2]; this.filename = path.resolve(this.baseDir, basename + extname2); return; } } } throw new Error("config file not found"); } migrateEntry(name, config) { if (name !== "group") return; const backup = { ...config }; for (const key in backup) delete config[key]; for (let key in backup) { if (key.startsWith("$")) { config[key] = backup[key]; continue; } const [prefix] = key.split(":", 1); const name2 = prefix.replace(/^~/, ""); const value = this.migrateEntry(name2, backup[key]) ?? backup[key]; let ident = key.slice(prefix.length + 1); if (!ident || this.names.has(ident)) { ident = Math.random().toString(36).slice(2, 8); key = `${prefix}:${ident}`; } this.names.add(ident); config[key] = value; } } async migrate() { this.migrateEntry("group", this.config.plugins); } async readConfig(initial = false) { if (this.mime === "application/yaml") { this.config = yaml.load(await fs.readFile(this.filename, "utf8")); } else if (this.mime === "application/json") { this.config = JSON.parse(await fs.readFile(this.filename, "utf8")); } else { const module = __require(this.filename); this.config = module.default || module; } if (initial) await this.migrate(); if (this.writable) await this.writeConfig(true); return new Context.Config(this.interpolate(this.config)); } async _writeConfig(silent = false) { this.suspend = true; if (!this.writable) { throw new Error(`cannot overwrite readonly config`); } if (this.mime === "application/yaml") { await fs.writeFile(this.filename + ".tmp", yaml.dump(this.config)); } else if (this.mime === "application/json") { await fs.writeFile(this.filename + ".tmp", JSON.stringify(this.config, null, 2)); } await fs.rename(this.filename + ".tmp", this.filename); if (!silent) this.app.emit("config"); } writeConfig(silent = false) { this._writeSlient &&= silent; if (this._writeTask) return this._writeTask; return this._writeTask = new Promise((resolve2, reject) => { setTimeout(() => { this._writeSlient = true; this._writeTask = void 0; this._writeConfig(silent).then(resolve2, reject); }, 0); }); } interpolate(source) { if (typeof source === "string") { return interpolate(source, this.params, /\$\{\{(.+?)\}\}/g); } else if (!source || typeof source !== "object") { return source; } else if (Array.isArray(source)) { return source.map((item) => this.interpolate(item)); } else { return valueMap(source, (item) => this.interpolate(item)); } } async resolve(name) { const plugin = unwrapExports(await this.import(name)); if (plugin) this.store.set(this.app.registry.resolve(plugin), name); return plugin; } keyFor(plugin) { const name = this.store.get(this.app.registry.resolve(plugin)); if (name) return name.replace(/(koishi-|^@koishijs\/)plugin-/, ""); } replace(oldKey, newKey) { oldKey = this.app.registry.resolve(oldKey); newKey = this.app.registry.resolve(newKey); const name = this.store.get(oldKey); if (!name) return; this.store.set(newKey, name); this.store.delete(oldKey); } async forkPlugin(name, config, parent) { const plugin = await this.resolve(name); if (!plugin) return; return parent.plugin(plugin, this.interpolate(config)); } isTruthyLike(expr) { if (isNullable(expr)) return true; return !!this.interpolate(`\${{ ${expr} }}`); } logUpdate(type, parent, key) { this.app.logger("loader").info("%s plugin %c", type, key); } async reload(parent, key, source) { let fork = parent.scope[_Loader.kRecord][key]; const name = key.split(":", 1)[0]; const [config, meta] = separate(source, name === "group"); if (fork) { if (!this.isTruthyLike(meta.$if)) { this.unload(parent, key); return; } fork[kUpdate] = true; fork.update(config); } else { if (!this.isTruthyLike(meta.$if)) return; this.logUpdate("apply", parent, key); const ctx = parent.extend(); if (name === "group") { fork = ctx.plugin(group, config); } else { fork = await this.forkPlugin(name, config, ctx); } if (!fork) return; fork.key = key.slice(name.length + 1); parent.scope[_Loader.kRecord][key] = fork; } const filter = this.interpolate(meta.$filter); fork.parent.filter = (session) => { return parent.filter(session) && (isNullable(filter) || session.resolve(filter)); }; return fork; } unload(ctx, key) { const fork = ctx.scope[_Loader.kRecord][key]; if (fork) fork.dispose(); } getRefName(fork) { const record = fork.parent.scope[_Loader.kRecord]; if (!record) return; for (const name in record) { if (record[name] !== fork) continue; return name; } } /** @deprecated */ resolvePlugin(name) { return this.resolve(name); } /** @deprecated */ reloadPlugin(ctx, key, source) { return this.reload(ctx, key, source); } /** @deprecated */ unloadPlugin(ctx, key) { return this.unload(ctx, key); } paths(scope) { if (scope === scope.parent.scope) return []; if (scope.runtime === scope) { return [].concat(...scope.runtime.children.map((child) => this.paths(child))); } if (scope.key) return [scope.key]; return this.paths(scope.parent.scope); } async createApp() { new Logger("app").info("%C", `Koishi/${version}`); const app = this.app = new Context(this.interpolate(this.config)); app.provide("loader", this, true); app.provide("baseDir", this.baseDir, true); app.scope[_Loader.kRecord] = /* @__PURE__ */ Object.create(null); const fork = await this.reload(app, "group:entry", this.config.plugins); this.entry = fork.ctx; app.accept((config) => { app.koishi.config = config; }); app.accept(["plugins"], (config) => { this.reload(app, "group:entry", config.plugins); }, { passive: true }); app.on("dispose", () => { this.fullReload(); }); app.on("internal/fork", (fork2) => { if (fork2.uid || !fork2.parent.scope[_Loader.kRecord]) return; const key = Object.keys(fork2.parent.scope[_Loader.kRecord]).find((key2) => { return fork2.parent.scope[_Loader.kRecord][key2] === fork2; }); if (!key) return; this.logUpdate("unload", fork2.parent, key); delete fork2.parent.scope[_Loader.kRecord][key]; if (!app.registry.has(fork2.runtime.plugin)) return; rename(fork2.parent.scope.config, key, "~" + key, fork2.parent.scope.config[key]); this.writeConfig(); }); app.on("internal/update", (fork2) => { const key = this.getRefName(fork2); if (key) this.logUpdate("reload", fork2.parent, key); }); app.on("internal/before-update", (fork2, config) => { if (fork2[kUpdate]) return delete fork2[kUpdate]; const name = this.getRefName(fork2); if (!name) return; const { schema } = fork2.runtime; fork2.parent.scope.config[name] = { ...separate(fork2.parent.scope.config[name])[1], ...schema ? schema.simplify(config) : config }; this.writeConfig(); }); if (this.envData.message) { const { sid, channelId, guildId, content } = this.envData.message; this.envData.message = null; const dispose = app.on("bot-status-updated", (bot) => { if (bot.sid !== sid || bot.status !== Universal.Status.ONLINE) return; dispose(); bot.sendMessage(channelId, content, guildId); }); } return app; } }; var shared_default = Loader; export { Loader, shared_default as default, unwrapExports }; //# sourceMappingURL=shared.mjs.map