diff --git a/Dockerfile b/Dockerfile index 86df468..841f595 100644 --- a/Dockerfile +++ b/Dockerfile @@ -113,12 +113,15 @@ RUN if [ -f dist-clients/BackstageCapture.x64.dll ]; then \ # Keep production build phases separate so BuildKit reports the exact slow or # failing phase and can cache each completed phase independently. RUN bun run build:css +RUN bun run build:web:prod RUN bun run vendor RUN MINIFY_CONCURRENCY=4 bun run minify RUN bun run build:bundle RUN test "$(wc -l < ./public/index.html)" -lt 20 \ && test "$(wc -l < ./public/assets/main.js)" -lt 50 \ + && test -s ./public/assets/generated/shared-ui-settings.js \ + && test ! -e ./public/assets/generated/shared-ui-settings.js.map \ && test -s ./public/assets/tailwind.css \ && test -d ./public/vendor/fontawesome \ && test -s ./dist/index.js \ diff --git a/Overlord-Server/package.json b/Overlord-Server/package.json index dc2583e..f096932 100644 --- a/Overlord-Server/package.json +++ b/Overlord-Server/package.json @@ -11,9 +11,10 @@ "dev": "bun run scripts/dev.ts", "start": "bun run build && bun dist/index.js", "build": "bun run build:css && bun run build:web && bun run vendor && bun run build:bundle", - "build:public:prod": "bun run build:css && bun run build:web && bun run vendor && bun run minify", + "build:public:prod": "bun run build:css && bun run build:web:prod && bun run vendor && bun run minify", "build:bundle": "bun build --production src/index.ts --target bun --outfile dist/index.js && bun build --production src/server/plugin-runtime/worker-host.ts --target bun --outfile dist/server/plugin-runtime/worker-host.js", "build:web": "bun build src/web/shared-ui-settings.ts --outdir public/assets/generated --target browser --format esm --sourcemap=external", + "build:web:prod": "bun -e \"import { rm } from 'node:fs/promises'; await rm('public/assets/generated', { recursive: true, force: true });\" && bun build --production src/web/shared-ui-settings.ts --outdir public/assets/generated --target browser --format esm", "watch:web": "bun run build:web --watch", "vendor": "bun run scripts/vendor.ts", "build:prod:win": "bun run build:public:prod && bun build --production src/index.ts --compile --target bun-windows-x64 --outfile dist/overlord-server.exe", diff --git a/Overlord-Server/scripts/minify-public.ts b/Overlord-Server/scripts/minify-public.ts index 3db107c..41bde1e 100644 --- a/Overlord-Server/scripts/minify-public.ts +++ b/Overlord-Server/scripts/minify-public.ts @@ -1,6 +1,7 @@ #!/usr/bin/env bun /** - * Minify all public JS, CSS, and HTML assets in-place. + * Minify first-party public assets and copied vendor assets that do not + * already ship minified. * Uses terser (JS), clean-css (CSS), html-minifier-terser (HTML). * * Usage: bun run scripts/minify-public.ts [--dir public] @@ -55,13 +56,13 @@ async function collectFiles(dir: string, exts: Set): Promise { return results; } -async function minifyJS(filePath: string) { +async function minifyJS(filePath: string, isModule = true) { const src = await Bun.file(filePath).text(); const result = await terserMinify(src, { compress: { passes: 2, drop_console: false, ecma: 2020 }, mangle: { toplevel: false }, format: { ecma: 2020 }, - module: true, + module: isModule, }); if (result.code && result.code.length < src.length) { stats.savedBytes += src.length - result.code.length; @@ -108,19 +109,60 @@ async function runPool( await Promise.all(workers); } -// Collect all files +const vendorDir = path.join(publicDir, "vendor"); +const vendorRoots = [ + "msgpackr", + "codemirror", + "chart.js", + "gridstack", + "hotwired", + "xterm", + "inter", + "jetbrains-mono", +]; + +async function collectVendorFiles(exts: Set): Promise { + const results: string[] = []; + for (const root of vendorRoots) { + const dir = path.join(vendorDir, root); + try { + results.push(...(await collectFiles(dir, exts))); + } catch (error: any) { + if (error?.code !== "ENOENT") throw error; + } + } + return results; +} + +function isVendorModule(filePath: string): boolean { + const normalized = filePath.replaceAll("\\", "/"); + return normalized.endsWith(".mjs") || + normalized.includes("/hotwired/") || + normalized.endsWith("/chart.esm.js"); +} + +// Collect all files. const jsFiles = await collectFiles(path.join(publicDir, "assets"), new Set([".js"])); const cssFiles = await collectFiles(path.join(publicDir, "assets"), new Set([".css"])); const htmlFiles = await collectFiles(publicDir, new Set([".html"])); +const vendorJsFiles = await collectVendorFiles(new Set([".js", ".mjs"])); +const vendorCssFiles = await collectVendorFiles(new Set([".css"])); console.log( - `Minifying ${jsFiles.length} JS, ${cssFiles.length} CSS, ${htmlFiles.length} HTML files ` + + `Minifying ${jsFiles.length + vendorJsFiles.length} JS, ` + + `${cssFiles.length + vendorCssFiles.length} CSS, ${htmlFiles.length} HTML files ` + `(concurrency=${minifyConcurrency})...`, ); const jobs = [ ...jsFiles.map((file) => ({ kind: "JS", file, minify: minifyJS })), + ...vendorJsFiles.map((file) => ({ + kind: "JS", + file, + minify: (filePath: string) => minifyJS(filePath, isVendorModule(filePath)), + })), ...cssFiles.map((file) => ({ kind: "CSS", file, minify: minifyCSS })), + ...vendorCssFiles.map((file) => ({ kind: "CSS", file, minify: minifyCSS })), ...htmlFiles.map((file) => ({ kind: "HTML", file, minify: minifyHTML })), ]; await runPool(jobs, minifyConcurrency, async ({ kind, file, minify }) => {