Bundling conventions for unfolder.space spaces

Builds run @cloudflare/worker-bundler's createApp inside the parent Worker. Inputs come from the space's Workspace at HEAD; .git/ is excluded automatically.

Entry point

  • Server entry is auto-detected. Default search: src/index.ts, index.ts, src/server.ts, src/worker.ts (and .js/.mts/.mjs).
  • The entry must be .ts/.js — NOT .tsx/.jsx. .tsx/.jsx are fully supported as modules (import them freely for JSX views); they just can't be the entry. A JSX app needs a one-line .ts shim that re-exports it, e.g. src/index.tsexport { App } from "./app";.

JSX runtime — use the classic hono/jsx pragma

createApp bundles with esbuild's classic JSX runtime and does not honor jsxImportSource (neither the tsconfig setting nor the automatic-runtime /** @jsxImportSource hono/jsx */ pragma). A JSX file configured for the automatic runtime builds cleanly but throws ReferenceError: React is not defined at request time — a green build, a 500 on first render. Every .tsx/.jsx file must carry the classic pragma pair + an explicit import:

/** @jsx jsx */
/** @jsxFrag Fragment */
import { jsx, Fragment } from "hono/jsx";

(The unfolder.hono prompt is a complete, building example.)

Durable Object class — name matters

If your worker exports a stateful class, name it App and export it named:

import { DurableObject } from "cloudflare:workers";

export class App extends DurableObject {
  async fetch(request: Request) { /* ... */ }
}

The facet host calls worker.getDurableObjectClass("App") directly and never reads export default — so don't add one. export default class App will fail with an opaque internal error. Always use a named export.

For purely-static sites you don't need any server code: drop your HTML/CSS/JS into the workspace and the supervisor will serve them via handleAssetRequest before forwarding any unmatched requests to the isolate.

Static assets

Anything that isn't .ts/.tsx/.js/.jsx/.json (or that lives under public/) is treated as a static asset.

Keep binary out of the workspace, though. public/ is for text assets (HTML/CSS/JS, an SVG, a robots.txt). Images, fonts, video, and audio are media — upload them to the media bucket (media.write / get_urlsdest=media); they serve at the site root just the same (/favicon.ico, /og.png) without bloating git. See media.

Headers, redirects, and asset behavior — config.setAssetConfig

Asset serving is configured on the supervisor, not via files in the workspace. Set it from inside run_code:

async () => {
  await config.setAssetConfig({
    headers: {
      "/*":       { set: { "X-Frame-Options": "DENY" } },
      "/assets/*": { set: { "Cache-Control": "public, max-age=31536000" } },
    },
    redirects: {
      static:  { "/old": { status: 301, to: "/new" } },
      dynamic: { "/old/*": { status: 301, to: "/new/:splat" } },
    },
    not_found_handling: "single-page-application", // or "404-page" | "none"
  });
};

The config is stored on the supervisor's KV — one source of truth shared between agent code (config.*) and the MCP layer. It takes effect on the next deploy. Pass null to clear.

Read the current config with config.getAssetConfig(). Returns null if none is set.

The full schema is AssetConfig from @cloudflare/worker-bundler: headers, redirects.static, redirects.dynamic, html_handling, not_found_handling. There are no _headers / _redirects files — don't write them; they are ignored.

Defaults (no config set)

When config.getAssetConfig() returns null, the bundler's defaults apply:

  • html_handling: "auto-trailing-slash"/about finds about.html or about/index.html; /about/ likewise. Trailing slash is auto-added or auto-dropped to find a match.
  • not_found_handling: "none" — unmatched requests fall through to your isolate (the App class). To serve an SPA, set "single-page-application" (returns /index.html for 404s); to serve a static error page, set "404-page" (walks up the tree looking for the nearest 404.html).
  • headers: undefined — no custom response headers are set or unset; the bundler only sets Content-Type and ETag.
  • redirects: undefined — no redirects applied.

In all cases the supervisor first tries the asset manifest (static files in the workspace), then redirects, then falls through to the isolate's fetch. A static-only site works with no config at all — just commit your files and deploy.

npm

package.json is honored. Add deps and they'll be resolved at build time.