import { createHash } from "node:crypto"; import { cpSync, copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync, } from "node:fs"; import { basename, dirname, extname, resolve } from "node:path"; import { fileURLToPath } from "node:url"; import vue from "@vitejs/plugin-vue"; import { defineConfig } from "vite"; const webRoot = dirname(fileURLToPath(import.meta.url)); const outputRoot = resolve(webRoot, "dist"); const backendOrigin = "http://127.0.0.1:8000"; const runtimeDirectories = ["assets", "icons", "vendor"]; const runtimeFiles = [ "manifest.webmanifest", "service-worker.js", "speech-audio-worklet.js", "login.html", "login.css", "login.js", "hardware-control.html", "hardware-control.css", "hardware-control.js", "network.js", "app.js", "gesture-control.js", "pwa.js", "console-v2.js", "self-extension.js", "speech-input.js", "assistant-chat.js", "energy-monitor.js", "firmware-workbench-entry.js", "firmware-workbench-theme.js", "firmware-workbench.js", "firmware-workbench.html", "styles.css", "console-v2.css", "self-extension.css", "speech-input.css", "assistant-chat.css", "energy-monitor.css", "firmware-workbench-entry.css", "firmware-workbench.css", "ui-consistency.css", "codepen-glass-theme.css", "vue-ui.js" ]; const fingerprintedRuntimeFiles = runtimeFiles.filter((file) => ( /\.(?:css|js)$/.test(file) && !["service-worker.js", "speech-audio-worklet.js"].includes(file) )); const rewrittenHtmlFiles = [ "index.html", "login.html", "hardware-control.html", "firmware-workbench.html", ]; function contentHash(value) { return createHash("sha256").update(value).digest("hex").slice(0, 12); } function hashedRuntimeName(file, hash) { const extension = extname(file); return `${basename(file, extension)}.${hash}${extension}`; } function rewriteRuntimeReferences(value, manifest) { let rewritten = value; for (const [source, target] of Object.entries(manifest)) { const escaped = source.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); rewritten = rewritten.replace( new RegExp(`/${escaped}(?:\\?[^\\s\"'<>)]*)?`, "g"), `/${target}`, ); } return rewritten; } function fingerprintRuntimeAssets() { const manifest = {}; for (const file of fingerprintedRuntimeFiles) { const source = resolve(outputRoot, file); if (!existsSync(source)) continue; const contents = readFileSync(source); const hashedName = hashedRuntimeName(file, contentHash(contents)); writeFileSync(resolve(outputRoot, hashedName), contents); manifest[file] = hashedName; } for (const file of rewrittenHtmlFiles) { const target = resolve(outputRoot, file); if (!existsSync(target)) continue; const source = readFileSync(target, "utf8"); writeFileSync(target, rewriteRuntimeReferences(source, manifest), "utf8"); } const serviceWorkerPath = resolve(outputRoot, "service-worker.js"); const buildHash = contentHash(JSON.stringify(manifest)); if (existsSync(serviceWorkerPath)) { const serviceWorker = rewriteRuntimeReferences( readFileSync(serviceWorkerPath, "utf8"), manifest, ).replace( /const CACHE_VERSION = "[^"]+";/, `const CACHE_VERSION = "cognitive-agent-pad-build-${buildHash}";`, ); writeFileSync(serviceWorkerPath, serviceWorker, "utf8"); } writeFileSync( resolve(outputRoot, "runtime-assets.json"), `${JSON.stringify({ build_hash: buildHash, assets: manifest }, null, 2)}\n`, "utf8", ); } function copyRuntimeAssets() { return { name: "copy-n7-runtime-assets", apply: "build", closeBundle() { for (const directory of runtimeDirectories) { const source = resolve(webRoot, directory); if (existsSync(source)) { cpSync(source, resolve(outputRoot, directory), { recursive: true }); } } for (const file of runtimeFiles) { const source = resolve(webRoot, file); if (!existsSync(source)) continue; const target = resolve(outputRoot, file); mkdirSync(dirname(target), { recursive: true }); copyFileSync(source, target); } fingerprintRuntimeAssets(); } }; } function backendProxy() { return { target: backendOrigin, changeOrigin: true, configure(proxy) { proxy.on("proxyReq", (request) => request.setHeader("origin", backendOrigin)); } }; } export default defineConfig({ root: webRoot, publicDir: false, plugins: [vue(), copyRuntimeAssets()], server: { strictPort: true, fs: { allow: [webRoot] }, proxy: { "/api": backendProxy(), "/artifacts": backendProxy() } }, build: { outDir: outputRoot, emptyOutDir: true, assetsDir: "vite-assets", target: "es2022", sourcemap: true } });