feat(console-web): replace TanStack UI with Solid host pages
- move the management frontend under the wasmeld-console crate - use a persistent Host shell with disposable iframe page documents - version Host/Page postMessage state and command contracts - migrate runtime, service, WIT, invocation, and settings workflows - add Tailwind CSS v4, responsive layouts, and Host-owned dialogs - emit a dependency-free PWA worker with API cache exclusions - remove the superseded root TanStack Start application
This commit is contained in:
@@ -0,0 +1,88 @@
|
||||
import tailwindcss from "@tailwindcss/vite";
|
||||
import { createHash } from "node:crypto";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { resolve } from "node:path";
|
||||
import { defineConfig, type Plugin } from "vite";
|
||||
import solid from "vite-plugin-solid";
|
||||
|
||||
const outputDirectory = process.env.WASMELD_WEB_OUT_DIR ?? "dist";
|
||||
|
||||
export default defineConfig({
|
||||
server: {
|
||||
port: 3000,
|
||||
},
|
||||
plugins: [solid(), tailwindcss(), pwaWorker()],
|
||||
build: {
|
||||
outDir: outputDirectory,
|
||||
emptyOutDir: true,
|
||||
rollupOptions: {
|
||||
// Host and Page remain separate HTML documents. Dynamic page imports
|
||||
// then create one lazy chunk per primary management view.
|
||||
input: {
|
||||
host: resolve(import.meta.dirname, "index.html"),
|
||||
page: resolve(import.meta.dirname, "page.html"),
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
function pwaWorker(): Plugin {
|
||||
return {
|
||||
name: "wasmeld-pwa-worker",
|
||||
apply: "build",
|
||||
generateBundle(_, bundle) {
|
||||
// Derive the cache name from emitted filenames and unhashed public
|
||||
// assets. A changed build activates a new cache without another PWA
|
||||
// dependency or a generated source file in the repository.
|
||||
const publicFiles = ["icon.svg", "manifest.webmanifest", "og.png"];
|
||||
const generatedFiles = Object.keys(bundle).filter((file) =>
|
||||
/\.(?:html|js|css|svg|png|webmanifest)$/.test(file),
|
||||
);
|
||||
const files = [...new Set([...generatedFiles, ...publicFiles])].sort();
|
||||
const digest = createHash("sha256");
|
||||
for (const file of files) digest.update(file);
|
||||
for (const file of publicFiles) {
|
||||
digest.update(readFileSync(resolve(import.meta.dirname, "public", file)));
|
||||
}
|
||||
const cacheName = `wasmeld-${digest.digest("hex").slice(0, 16)}`;
|
||||
const urls = files.map((file) => `/${file}`);
|
||||
this.emitFile({
|
||||
type: "asset",
|
||||
fileName: "sw.js",
|
||||
source: serviceWorkerSource(cacheName, urls),
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function serviceWorkerSource(cacheName: string, urls: string[]): string {
|
||||
// Runtime state must never be satisfied from an old cache. Only documents
|
||||
// and build assets participate in offline behavior.
|
||||
return `const CACHE = ${JSON.stringify(cacheName)};
|
||||
const PRECACHE = ${JSON.stringify(urls)};
|
||||
self.addEventListener("install", (event) => {
|
||||
event.waitUntil(caches.open(CACHE).then((cache) => cache.addAll(PRECACHE)).then(() => self.skipWaiting()));
|
||||
});
|
||||
self.addEventListener("activate", (event) => {
|
||||
event.waitUntil(caches.keys().then((keys) => Promise.all(keys.filter((key) => key.startsWith("wasmeld-") && key !== CACHE).map((key) => caches.delete(key)))).then(() => self.clients.claim()));
|
||||
});
|
||||
self.addEventListener("fetch", (event) => {
|
||||
if (event.request.method !== "GET") return;
|
||||
const url = new URL(event.request.url);
|
||||
if (url.origin !== self.location.origin || url.pathname === "/healthz" || url.pathname.startsWith("/api/")) return;
|
||||
if (event.request.mode === "navigate") {
|
||||
event.respondWith(fetch(event.request).then((response) => {
|
||||
const copy = response.clone();
|
||||
caches.open(CACHE).then((cache) => cache.put(event.request, copy));
|
||||
return response;
|
||||
}).catch(async () => {
|
||||
const cached = await caches.match(event.request);
|
||||
if (cached) return cached;
|
||||
return caches.match(url.pathname === "/page.html" ? "/page.html" : "/index.html");
|
||||
}));
|
||||
return;
|
||||
}
|
||||
event.respondWith(caches.match(event.request).then((cached) => cached || fetch(event.request)));
|
||||
});
|
||||
`;
|
||||
}
|
||||
Reference in New Issue
Block a user