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/.jsxare fully supported as modules (import them freely for JSX views); they just can't be the entry. A JSX app needs a one-line.tsshim that re-exports it, e.g.src/index.ts→export { 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_urls →
dest=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"—/aboutfindsabout.htmlorabout/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 (theAppclass). To serve an SPA, set"single-page-application"(returns/index.htmlfor 404s); to serve a static error page, set"404-page"(walks up the tree looking for the nearest404.html).headers: undefined— no custom response headers are set or unset; the bundler only setsContent-TypeandETag.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.