@koishijs/loader
Version:
Config Loader for Koishi
413 lines (411 loc) • 14.1 kB
JavaScript
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// src/shared.ts
var shared_exports = {};
__export(shared_exports, {
Loader: () => Loader,
default: () => shared_default,
unwrapExports: () => unwrapExports
});
module.exports = __toCommonJS(shared_exports);
var import_core = require("@koishijs/core");
var import_fs = require("fs");
var yaml = __toESM(require("js-yaml"));
var path = __toESM(require("path"));
function unwrapExports(module2) {
return module2?.default || module2;
}
__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() {
import_core.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 import_fs.promises.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 import_fs.promises.access(this.filename, import_fs.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 import_fs.promises.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 import_fs.promises.readFile(this.filename, "utf8"));
} else if (this.mime === "application/json") {
this.config = JSON.parse(await import_fs.promises.readFile(this.filename, "utf8"));
} else {
const module2 = require(this.filename);
this.config = module2.default || module2;
}
if (initial) await this.migrate();
if (this.writable) await this.writeConfig(true);
return new import_core.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 import_fs.promises.writeFile(this.filename + ".tmp", yaml.dump(this.config));
} else if (this.mime === "application/json") {
await import_fs.promises.writeFile(this.filename + ".tmp", JSON.stringify(this.config, null, 2));
}
await import_fs.promises.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 (0, import_core.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 (0, import_core.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-|^\/)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 ((0, import_core.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) && ((0, import_core.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 import_core.Logger("app").info("%C", `Koishi/${import_core.version}`);
const app = this.app = new import_core.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 !== import_core.Universal.Status.ONLINE) return;
dispose();
bot.sendMessage(channelId, content, guildId);
});
}
return app;
}
};
var shared_default = Loader;
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
Loader,
unwrapExports
});
//# sourceMappingURL=shared.js.map