Adapting a SPA to an app-shell
Este conteúdo não está disponível em sua língua ainda.
Concrete patterns for taking a client-only SPA (originally Vite/nginx/UnoCSS) and running it as a Buntime gateway app-shell worker. Distilled from porting a real client SPA. Apply these when “the SPA renders locally but breaks as a Buntime worker”.
1. Assets must live at single-segment paths
Section titled “1. Assets must live at single-segment paths”The gateway routes a request to the shell worker only when isDocument || (isRootPath && !isFrameEmbedding), where isRootPath means a single path segment (!pathname.slice(1).includes("/")). See the gateway plugin for request-type detection.
- Bun’s HTML bundler emits flat root assets (
/chunk-abc.js) → served fine. - A multi-segment asset (
/assets/x.js,/workers/x.js) is never handed to the shell worker → 404 (or mis-routed if a proxy rule matches the prefix). - Fix: emit such assets to the dist root. Example: a SharedWorker built to
/workers/session.worker.jsfailed; emitting it to/session.worker.jsfixed it.
2. Per-environment config via window.__config (not the build)
Section titled “2. Per-environment config via window.__config (not the build)”Don’t bake env-specific config (auth realm/url, API bases) into the bundle — the same artifact runs in multiple environments. Make the SPA a serverless worker (entrypoint: index.ts) that injects config server-side:
// index.ts — serves dist/ and injects window.__config into the HTML <head>const AUTH = Bun.env.AUTH_CONFIG ? JSON.parse(Bun.env.AUTH_CONFIG) : undefined; // per-env manifest value// else fall back to fetching CONFIG_API/config/auth (open in-cluster endpoint)const injected = `<script>window.__config=${JSON.stringify({ auth })}</script>`;html.replace("</head>", injected + "</head>");- Prod reads
window.__config. KeepPUBLIC_AUTH_CONFIG(→window.__env__) as a DEV-only fallback. AUTH_CONFIG(manifest env, server-only) is the simplest source when the backend config endpoint is gated (e.g. a gateway returning 401 on/api/config/auth).- Single-segment asset rule (1) still applies — the worker serves
dist/files, so flat chunks “just work”; nested ones don’t.
3. Bundle web/shared workers separately, reference a single-segment URL
Section titled “3. Bundle web/shared workers separately, reference a single-segment URL”new SharedWorker(new URL("../workers/x.ts", import.meta.url)) does not get bundled by an HTML-entrypoint build, and resolves to an unserved path. Add a second Bun.build for the worker, emit to dist root, and reference the served URL:
// build: Bun.build({ entrypoints: ["./src/workers/session.worker.ts"], naming: "[name].[ext]", outdir })new SharedWorker("/session.worker.js", { type: "module" });4. UnoCSS to Tailwind class gaps (silent no-ops)
Section titled “4. UnoCSS to Tailwind class gaps (silent no-ops)”A UnoCSS SPA ported to Bun’s bun-plugin-tailwind loses classes Tailwind doesn’t define — they vanish with no error:
| UnoCSS (works) | Tailwind (no-op) | Fix |
|---|---|---|
font-600, font-300 | not generated | font-semibold, font-light |
max-w-none + HTML height="24" | preflight img{height:auto} overrides the attr; max-w-none drops the clamp → intrinsic size | size in CSS: max-h-6 max-w-full object-contain |
Grep the source for font-\d00 and bare HTML height=/width= on <img> when a logo/icon renders huge or unstyled. Verify a class survived with grep font-semibold dist/*.css after building.
5. Defensive rendering of backend-driven data
Section titled “5. Defensive rendering of backend-driven data”Backend data may not match the SPA’s old assumptions. Guard before calling:
// app.icon may be undefined → calling it throws "icon is not a function",// which the router error boundary renders as a generic "unknown app" page.typeof icon === "function" ? icon({...}) : <img src={typeof icon === "string" ? icon : fallback} />When migrating an expand/collapse tree off a CSS-checkbox mechanism to React state, port the search-expand too: force groups open while a query is present (const isOpen = query.trim() ? true : expanded) — otherwise search filters but leaves branches collapsed.
Packaging and deploy
Section titled “Packaging and deploy”Package dist/ + index.ts + manifest.yaml as a .tgz (top dir package/) and upload via POST /_/api/workers/upload. For the broader picture of how the gateway composes app-shells and micro-frontends, see Micro-frontend.