@koishijs/plugin-console
Version:
Web User Interface for Koishi
269 lines (266 loc) • 10.3 kB
JavaScript
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