fix(ui): serve

This commit is contained in:
xxnuo
2026-01-01 16:58:52 +08:00
parent 3db0cc3724
commit 62fa6d4ad1
11 changed files with 107 additions and 29 deletions

1
.gitignore vendored
View File

@@ -19,3 +19,4 @@ deprecated/go/bin/worker
deprecated/go/dist/mtranserver-darwin-arm64
*.o
src/generated/
src/assets/ui.ts

View File

@@ -14,7 +14,7 @@
"dev": "bun src/main.ts --log-level debug",
"build": "rm -rf dist && bun build src/main.ts --compile --outfile ./dist/mtranserver --minify --sourcemap",
"build:all": "bun scripts/build.ts",
"build:dev": "bun run gen && rm -rf dist && bun build src/main.ts --outdir dist --target node --format esm --sourcemap --external zstd-wasm-decoder --external express && tsc --emitDeclarationOnly --outDir dist",
"build:dev": "bun run gen && rm -rf dist && bun build src/main.ts --outdir dist --target node --format esm --sourcemap --external zstd-wasm-decoder --external express",
"start": "node dist/main.js",
"test": "bun test",
"typecheck": "tsc --noEmit",

View File

@@ -20,6 +20,12 @@ console.log("Cleaning dist...");
await $`rm -rf dist`;
await $`mkdir -p dist`;
console.log("Building UI...");
await $`cd ui && bun run build`;
console.log("Generating UI assets map...");
await $`bun run scripts/gen-ui-assets.ts`;
console.log("Generating routes and spec...");
await $`bun run gen`;

44
scripts/gen-ui-assets.ts Normal file
View File

@@ -0,0 +1,44 @@
import { $ } from "bun";
import fs from "fs";
import path from "path";
const distDir = "ui/dist";
const outputFile = "src/assets/ui.ts";
function* walkDir(dir: string): Generator<string> {
const entries = fs.readdirSync(dir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
yield* walkDir(fullPath);
} else {
yield fullPath;
}
}
}
const files = Array.from(walkDir(distDir));
const imports = files
.map((file, i) => {
const absolutePath = path.resolve(file);
return `import _asset${i} from "${absolutePath}" with { type: "file" };`;
})
.join("\n");
const assetMap = files
.map((file, i) => {
const urlPath = "/" + path.relative(distDir, file).replace(/\\/g, "/");
return ` "${urlPath}": _asset${i}`;
})
.join(",\n");
const code = `${imports}
export const assets: Record<string, string> = {
${assetMap}
};
`;
await Bun.write(outputFile, code);
console.log(`Generated ${outputFile} with ${files.length} assets`);

View File

@@ -1,14 +1,5 @@
import fs from 'fs/promises'
import path from 'path'
import { fileURLToPath } from 'url'
const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
export function getEmbeddedAssetPath(filename: string): string {
const assetsDir = path.resolve(__dirname, '../../assets')
return path.join(assetsDir, filename)
}
export async function cleanupLegacyBin(configDir: string): Promise<void> {
const binDir = path.join(configDir, 'bin')

48
src/middleware/ui.ts Normal file
View File

@@ -0,0 +1,48 @@
import { Request, Response, NextFunction, RequestHandler } from 'express';
import { readFile } from 'fs/promises';
import { assets } from '@/assets/ui.js';
const mimeTypes: Record<string, string> = {
'.html': 'text/html',
'.css': 'text/css',
'.js': 'application/javascript',
'.json': 'application/json',
'.png': 'image/png',
'.jpg': 'image/jpeg',
'.jpeg': 'image/jpeg',
'.gif': 'image/gif',
'.svg': 'image/svg+xml',
'.ico': 'image/x-icon',
};
export const UI: RequestHandler = async (req: Request, res: Response, next: NextFunction) => {
let filePath = req.path;
if (filePath.startsWith('/ui/')) {
filePath = filePath.substring(3);
}
if (filePath === '/' || filePath === '') {
filePath = '/index.html';
}
const assetPath = assets[filePath];
if (assetPath) {
const ext = filePath.substring(filePath.lastIndexOf('.'));
const mimeType = mimeTypes[ext] || 'application/octet-stream';
res.setHeader('Content-Type', mimeType);
const buffer = await readFile(assetPath);
res.send(buffer);
} else {
const indexPath = assets['/index.html'];
if (indexPath) {
res.setHeader('Content-Type', 'text/html');
const buffer = await readFile(indexPath);
res.send(buffer);
} else {
next();
}
}
};

View File

@@ -1,7 +1,5 @@
import express from 'express';
import fs from 'fs/promises';
import path from 'path';
import { fileURLToPath } from 'url';
import swaggerUi from 'swagger-ui-express';
import { getConfig } from '@/config/index.js';
import * as logger from '@/logger/index.js';
@@ -11,9 +9,7 @@ import { cleanupLegacyBin } from '@/assets/index.js';
import { requestId, errorHandler, cors } from '@/middleware/index.js';
import { RegisterRoutes } from '@/generated/routes.js';
import swaggerDocument from '@/generated/swagger.json';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
import { UI } from '@/middleware/ui.js';
export async function run() {
const config = getConfig();
@@ -34,22 +30,16 @@ export async function run() {
app.use(express.json());
app.use(cors());
const uiPath = path.resolve(__dirname, '../../ui/dist');
app.use('/ui', express.static(uiPath));
app.get('/', (_req, res) => {
res.redirect('/ui');
});
RegisterRoutes(app);
app.use('/docs', swaggerUi.serve, swaggerUi.setup(swaggerDocument));
app.use(UI);
app.use(errorHandler());
const server = app.listen(parseInt(config.port), config.host, () => {
logger.info(`HTTP Service URL: http://${config.host}:${config.port}`);
logger.info(`Web UI: http://${config.host}:${config.port}/ui`);
logger.info(`Swagger Docs: http://${config.host}:${config.port}/docs`);
logger.info(`Log level set to: ${config.logLevel}`);
});

View File

@@ -26,5 +26,5 @@
}
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "tests", "deprecated"]
"exclude": ["node_modules", "dist", "tests", "deprecated", "src/assets/ui.ts"]
}

View File

@@ -816,9 +816,9 @@
"@radix-ui/react-tooltip/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "https://registry.npmmirror.com/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.7.1", "https://registry.npmmirror.com/@emnapi/core/-/core-1.7.1.tgz", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg=="],
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.8.0", "https://registry.npmmirror.com/@emnapi/core/-/core-1.8.0.tgz", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-ryJnSmj4UhrGLZZPJ6PKVb4wNPAIkW6iyLy+0TRwazd3L1u0wzMe8RfqevAh2HbcSkoeLiSYnOVDOys4JSGYyg=="],
"@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.7.1", "https://registry.npmmirror.com/@emnapi/runtime/-/runtime-1.7.1.tgz", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA=="],
"@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.8.0", "https://registry.npmmirror.com/@emnapi/runtime/-/runtime-1.8.0.tgz", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-Z82FDl1ByxqPEPrAYYeTQVlx2FSHPe1qwX465c+96IRS3fTdSYRoJcRxg3g2fEG5I69z1dSEWQlNRRr0/677mg=="],
"@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.1.0", "https://registry.npmmirror.com/@emnapi/wasi-threads/-/wasi-threads-1.1.0.tgz", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ=="],
@@ -832,8 +832,6 @@
"@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "https://registry.npmmirror.com/minimatch/-/minimatch-9.0.5.tgz", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
"cmdk/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.4", "https://registry.npmmirror.com/@radix-ui/react-primitive/-/react-primitive-2.1.4.tgz", { "dependencies": { "@radix-ui/react-slot": "1.2.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg=="],
"prop-types/react-is": ["react-is@16.13.1", "https://registry.npmmirror.com/react-is/-/react-is-16.13.1.tgz", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="],
"@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.2", "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-2.0.2.tgz", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="],

View File

@@ -4,7 +4,7 @@ import path from "path"
import tailwindcss from "@tailwindcss/vite"
export default defineConfig({
base: '/ui/',
base: '/',
plugins: [react(), tailwindcss()],
resolve: {
alias: {