diff --git a/.gitignore b/.gitignore index 104deff..8aea867 100644 --- a/.gitignore +++ b/.gitignore @@ -19,3 +19,4 @@ deprecated/go/bin/worker deprecated/go/dist/mtranserver-darwin-arm64 *.o src/generated/ +src/assets/ui.ts diff --git a/package.json b/package.json index 6cae59e..809c438 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/scripts/build.ts b/scripts/build.ts index f405ee4..c5636eb 100644 --- a/scripts/build.ts +++ b/scripts/build.ts @@ -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`; diff --git a/scripts/gen-ui-assets.ts b/scripts/gen-ui-assets.ts new file mode 100644 index 0000000..7ea7601 --- /dev/null +++ b/scripts/gen-ui-assets.ts @@ -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 { + 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 = { +${assetMap} +}; +`; + +await Bun.write(outputFile, code); +console.log(`Generated ${outputFile} with ${files.length} assets`); diff --git a/src/assets/index.ts b/src/assets/index.ts index 68dc04a..b8c6b12 100644 --- a/src/assets/index.ts +++ b/src/assets/index.ts @@ -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 { const binDir = path.join(configDir, 'bin') diff --git a/src/middleware/ui.ts b/src/middleware/ui.ts new file mode 100644 index 0000000..12ca34d --- /dev/null +++ b/src/middleware/ui.ts @@ -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 = { + '.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(); + } + } +}; diff --git a/src/server/index.ts b/src/server/index.ts index 79df6bd..5c469b7 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -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}`); }); diff --git a/scripts/stress_test.ts b/tests/stress-test.ts similarity index 100% rename from scripts/stress_test.ts rename to tests/stress-test.ts diff --git a/tsconfig.json b/tsconfig.json index 47e0a4f..370e9ef 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -26,5 +26,5 @@ } }, "include": ["src/**/*"], - "exclude": ["node_modules", "dist", "tests", "deprecated"] + "exclude": ["node_modules", "dist", "tests", "deprecated", "src/assets/ui.ts"] } diff --git a/ui/bun.lock b/ui/bun.lock index b9a8622..fafceeb 100644 --- a/ui/bun.lock +++ b/ui/bun.lock @@ -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=="], diff --git a/ui/vite.config.ts b/ui/vite.config.ts index e3a47ba..87b3d52 100644 --- a/ui/vite.config.ts +++ b/ui/vite.config.ts @@ -4,7 +4,7 @@ import path from "path" import tailwindcss from "@tailwindcss/vite" export default defineConfig({ - base: '/ui/', + base: '/', plugins: [react(), tailwindcss()], resolve: { alias: {