UNPKG

@koishijs/plugin-console

Version:

Web User Interface for Koishi

269 lines (266 loc) 10.3 kB
var __defProp = Object.defineProperty; var __getOwnPropNames = Object.getOwnPropertyNames; 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'); }); var __commonJS = (cb, mod) => function __require2() { return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports; }; // src/node/locales/zh-CN.yml var require_zh_CN = __commonJS({ "src/node/locales/zh-CN.yml"(exports, module) { module.exports = { root: "前端页面的根目录。", uiPath: "前端页面呈现的路径。", apiPath: "后端 API 服务的路径。", selfUrl: "Koishi 服务暴露在公网的地址。", open: "在应用启动后自动在浏览器中打开控制台。", head: { $desc: "自定义页面头部信息。", $inner: { tag: { $desc: "标签名。", $inner: ["title", "link", "meta", "script", "style", "自定义"] }, attrs: "标签属性。", content: "标签内容。" } }, heartbeat: { interval: "心跳发送间隔 (单位毫秒)。", timeout: "心跳超时时间 (单位毫秒)。" }, devMode: "启用调试模式 (仅供开发者使用)。", cacheDir: "调试服务器缓存目录。" }; } }); // src/node/index.ts import { h, makeArray, noop, Schema, Time } from "koishi"; import { Console } from "@koishijs/console"; import { extname, resolve } from "path"; import { createReadStream, existsSync, promises as fs } from "fs"; import open from "open"; import { createRequire } from "module"; import { fileURLToPath, pathToFileURL } from "url"; export * from "@koishijs/console"; var NodeConsole = class extends Console { constructor(ctx, config) { super(ctx); this.ctx = ctx; this.config = config; this.layer = ctx.server.ws(config.apiPath, (socket, request) => { this.accept(socket, request); }); ctx.on("console/connection", () => { const loader = ctx.get("loader"); if (!loader) return; loader.envData.clientCount = this.layer.clients.size; }); const base = import.meta.url || pathToFileURL(__filename).href; const require2 = createRequire(base); this.root = config.devMode ? resolve(require2.resolve("@koishijs/client/package.json"), "../app") : fileURLToPath(new URL("../../dist", base)); } static { __name(this, "NodeConsole"); } static inject = { required: ["server"], optional: ["console"] }; // static inject = ['server'] // workaround for edge case (collision with @koishijs/plugin-config) _config; vite; root; layer; // @ts-ignore FIXME get config() { return this._config; } set config(value) { this._config = value; } createGlobal() { const global = {}; const { devMode, uiPath, apiPath, selfUrl, heartbeat } = this.config; global.devMode = devMode; global.uiPath = uiPath; global.heartbeat = heartbeat; global.endpoint = selfUrl + apiPath; const proxy = this.ctx.get("server.proxy"); if (proxy) global.proxyBase = proxy.config.path + "/"; return global; } async start() { if (this.config.devMode) await this.createVite(); this.serveAssets(); this.ctx.on("server/ready", () => { let { host, port } = this.ctx.server; if (["0.0.0.0", "::"].includes(host)) host = "127.0.0.1"; const target = `http://${host}:${port}${this.config.uiPath}`; if (this.config.open && !this.ctx.get("loader")?.envData.clientCount && !process.env.KOISHI_AGENT) { open(target); } this.ctx.logger.info("webui is available at %c", target); }); } getFiles(files) { if (typeof files === "string" || Array.isArray(files)) return files; if (!this.config.devMode) return files.prod; if (!existsSync(files.dev)) return files.prod; return files.dev; } resolveEntry(files, key) { const { devMode, uiPath } = this.config; const filenames = []; for (const local of makeArray(this.getFiles(files))) { const filename = devMode ? "/vite/@fs/" + local : uiPath + "/@plugin-" + key; if (extname(local)) { filenames.push(filename); } else { filenames.push(filename + "/index.js"); if (existsSync(local + "/style.css")) { filenames.push(filename + "/style.css"); } } } return filenames; } serveAssets() { const { uiPath } = this.config; this.ctx.server.get(uiPath + "(.*)", async (ctx, next) => { await next(); if (ctx.body || ctx.response.body) return; if (ctx.path === uiPath && !uiPath.endsWith("/")) { return ctx.redirect(ctx.path + "/"); } const name = ctx.path.slice(uiPath.length).replace(/^\/+/, ""); const sendFile = /* @__PURE__ */ __name((filename2) => { ctx.type = extname(filename2); return ctx.body = createReadStream(filename2); }, "sendFile"); if (name.startsWith("@plugin-")) { const [key] = name.slice(8).split("/", 1); if (this.entries[key]) { const files = makeArray(this.getFiles(this.entries[key].files)); const filename2 = files[0] + name.slice(8 + key.length); ctx.type = extname(filename2); if (this.config.devMode || ctx.type !== "application/javascript") { return sendFile(filename2); } const source = await fs.readFile(filename2, "utf8"); return ctx.body = await this.transformImport(source); } else { return ctx.status = 404; } } const filename = resolve(this.root, name); if (!filename.startsWith(this.root) && !filename.includes("node_modules")) { return ctx.status = 403; } const stats = await fs.stat(filename).catch(noop); if (stats?.isFile()) return sendFile(filename); const template = await fs.readFile(resolve(this.root, "index.html"), "utf8"); ctx.type = "html"; ctx.body = await this.transformHtml(template); }); } async transformImport(source) { let output = ""; let cap; while (cap = /((?:^|;)import\b[^'"]+\bfrom\s*)(['"])([^'"]+)\2;/m.exec(source)) { const [stmt, left, quote, path] = cap; output += source.slice(0, cap.index) + left + quote + ({ "vue": "../vue.js", "vue-router": "../vue-router.js", "@vueuse/core": "../vueuse.js", "@koishijs/client": "../client.js" }[path] ?? path) + quote + ";"; source = source.slice(cap.index + stmt.length); } return output + source; } async transformHtml(template) { const { uiPath, head = [] } = this.config; if (this.vite) { template = await this.vite.transformIndexHtml(uiPath, template); } else { template = template.replace(/(href|src)="(?=\/)/g, (_, $1) => `${$1}="${uiPath}`); } let headInjection = `<script>KOISHI_CONFIG = ${JSON.stringify(this.createGlobal())}</script>`; for (const { tag, attrs = {}, content } of head) { const attrString = Object.entries(attrs).map(([key, value]) => ` ${key}="${h.escape(value ?? "", true)}"`).join(""); headInjection += `<${tag}${attrString}>${content ?? ""}</${tag}>`; } return template.replace("<title>", headInjection + "<title>"); } async createVite() { const { cacheDir, dev } = this.config; const { createServer } = __require("@koishijs/client/lib"); this.vite = await createServer(this.ctx.baseDir, { cacheDir: resolve(this.ctx.baseDir, cacheDir), server: { fs: dev.fs } }); this.ctx.server.all("/vite(.*)", (ctx) => new Promise((resolve2) => { this.vite.middlewares(ctx.req, ctx.res, resolve2); })); this.ctx.on("dispose", () => this.vite.close()); } stop() { this.layer.close(); } }; ((NodeConsole2) => { NodeConsole2.Dev = Schema.object({ fs: Schema.object({ strict: Schema.boolean().default(true), allow: Schema.array(String).default(null), deny: Schema.array(String).default(null) }).hidden() }); NodeConsole2.Head = Schema.intersect([ Schema.object({ tag: Schema.union([ "title", "link", "meta", "script", "style", Schema.string() ]).required() }), Schema.union([ Schema.object({ tag: Schema.const("title").required(), content: Schema.string().role("textarea") }), Schema.object({ tag: Schema.const("link").required(), attrs: Schema.dict(Schema.string()).role("table") }), Schema.object({ tag: Schema.const("meta").required(), attrs: Schema.dict(Schema.string()).role("table") }), Schema.object({ tag: Schema.const("script").required(), attrs: Schema.dict(Schema.string()).role("table"), content: Schema.string().role("textarea") }), Schema.object({ tag: Schema.const("style").required(), attrs: Schema.dict(Schema.string()).role("table"), content: Schema.string().role("textarea") }), Schema.object({ tag: Schema.string().required(), attrs: Schema.dict(Schema.string()).role("table"), content: Schema.string().role("textarea") }) ]) ]); NodeConsole2.Config = Schema.intersect([ Schema.object({ uiPath: Schema.string().default(""), apiPath: Schema.string().default("/status"), selfUrl: Schema.string().role("link").default(""), open: Schema.boolean(), head: Schema.array(NodeConsole2.Head), heartbeat: Schema.object({ interval: Schema.number().default(Time.second * 30), timeout: Schema.number().default(Time.minute) }), devMode: Schema.boolean().default(process.env.NODE_ENV === "development").hidden(), cacheDir: Schema.string().default("cache/vite").hidden(), dev: NodeConsole2.Dev }) ]).i18n({ "zh-CN": require_zh_CN() }); })(NodeConsole || (NodeConsole = {})); var node_default = NodeConsole; export { node_default as default }; //# sourceMappingURL=index.mjs.map