Documentation
v0.1.4 & v0.1.5
The two deploy releases — why a build would not run where it was not built.
Both published 15 August 2026, hours apart, because 0.1.4 did not finish the job it set out to do. They belong together: if you are deploying to a platform that bundles your app into a function, 0.1.4 alone still fails. Both are on npm.
0.1.5
Published hours after 0.1.4, for the half of the problem 0.1.4 left standing.
The rest of the build now travels with the bundle
0.1.4 0.1.5 ────────────────────────── ────────────────────────── server.mjs arrived server.mjs arrived islands.json left behind islands.json inlined stylesheet.txt left behind stylesheet.txt inlined Island manifest not found serves
0.1.4 stopped the bundle recording its build path and stopped it rescanning routes/, which is what made a copied build 404 every path. It did not stop the server reading .stoneware/islands.json at boot — and that read computed its path at runtime.
A platform that builds a function by tracing imports cannot follow a path computed at runtime. It followed the static import of the server bundle and carried that across; it never saw the manifest. So the function started with the bundle intact, threw before its first request, and reported a bare 500.
Error: Island manifest not found at /var/task/.stoneware/islands.json
at rebuildIslands (/var/task/.stoneware/server.mjs:1462:14)
at async createApp (/var/task/.stoneware/server.mjs:1482:8)The build now writes both the island manifest and the stylesheet URL into the generated entry as values. Nothing about serving a request touches the filesystem for them, so there is nothing left for a bundler to miss.
The same mistake twice, one layer apart: a path assembled at runtime is invisible to a tool reasoning about imports. First it was routes/, then it was islands.json. A test now asserts the manifest is still inlined, because the way this regresses is silent until someone deploys.
Also in 0.1.5
- stoneware doctor warns on 0.1.4 as well as on 0.1.0-0.1.3, naming which of the two deploy failures each version has.
- stoneware preview no longer claims an exported site has no Content-Security-Policy. It stopped being true when 0.1.4 started embedding one, and the message had not caught up.
0.1.4
Published 15 August 2026. One theme: a build should run somewhere other than the machine that produced it. That sounds obvious, and it was not true — which is why deploying to a platform that bundles your app failed in a way that looked like a routing bug.
Builds that run where they were not built
before now
────────────────────────── ──────────────────────────
the bundle recorded the the root is derived from
absolute path it was built the bundle's own location
at, and rescanned routes/
on every request a route manifest ships
inside it, so routes/ is a
→ 404 for every path build input, not a runtime
somewhere else dependencyA production build inlined every route and island, then matched paths by scanning routes/ on disk anyway — and hardcoded the build machine's project root. Both are invisible locally, because the directory you build in is the directory you serve from. Move the output and every request 404s while the process reports a clean start.
This is what made Vercel fail. It is not Vercel-specific: a container that builds in one path and runs in another, a CI artifact handed to a deploy step, and a serverless function unpacked into a scratch directory all hit it.
A second bug fell out of testing the first. With islands/ absent, the island registry was rebuilt by rescanning it, so every island quietly degraded to plain markup — no hydration marker, no chunk, nothing logged. Pages looked fine and shipped no JavaScript. An empty registry is indistinguishable from a page that genuinely has no islands, which is what kept it silent.
Deploying to Vercel
server.js import "./.stoneware/server.js";
the Bun preset detects the Bun.serve()
call inside it and routes every request
vercel.json framework + bunVersion + buildCommand
written only if you do not have oneVercel runs Bun as a first-class function runtime, and its Bun framework preset wants exactly one thing: a root entrypoint that calls Bun.serve() at module startup. A built Stoneware server already does that, so the target emits a re-export rather than an adapter — there is no request translation and nothing to keep in sync with the pipeline. See deploying.
An existing vercel.json is never rewritten. It is hand-maintained configuration that may carry regions, headers or redirects, so anything missing is reported instead — including a functions block, which fails the preset build outright because those patterns only match an api/ directory.
Dev no longer shares a port by accident
before now
────────────────────────── ──────────────────────────
dev binds ::1, start binds dev asks whether anything
0.0.0.0 — different sockets, answers on the port first,
so neither errors across both loopback
families, and steps past
both report success; who
answers depends on the start still fails loudly:
client's IPv4/IPv6 order it must have its own port0.1.3 already walked to the next free port when a bind failed. This is the case where nothing fails: two servers hold the same port on different addresses, both log success, and requests land on whichever one the client's address preference picks. Asking whether the port answers catches it; asking whether the bind failed cannot.
An exported site keeps its policy
before now
────────────────────────── ──────────────────────────
no Content-Security-Policy _headers, read by Netlify
at all — it is a response and Cloudflare Pages, with
header, and static files the full policy
carry none
<meta http-equiv> in every
the framework's strongest page for every other host,
default, silently absent minus the three directives
a meta tag cannot carryThe claim that the CSP is never silently absent held for stoneware start and quietly failed for stoneware export. Both files are written now, because neither covers every host alone. frame-ancestors, report-uri and sandbox are stripped from the meta tag rather than emitted — browsers ignore them there, and listing a directive you do not enforce is worse than omitting it. The export names what a header-less host gives up. See security.
Three new commands
preview serves an export with its own conventions —
<path>/index.html, 404.html for a miss. Previously
the only way to check an export was to deploy it.
routes the compiled table in match order, so you can see
which pattern a URL actually reaches.
doctor setup problems a running server cannot report:
tsconfig JSX settings, Bun version, .gitignore.doctor's most useful check is the tsconfig one. JSX pointed at React's runtime compiles cleanly and then fails during a render, as a TypeError about an object, blaming a template that is perfectly correct. Also new: stoneware --version prints both the framework and Bun versions, and stoneware dev --open launches a browser.
Two things that used to fail quietly
- A style attribute under the default CSP is emitted and then ignored by the browser. Development now says so, naming the element and the fix, and stays silent if your policy permits inline styles.
- Islands that were built but never registered rendered as inert markup — correct-looking HTML shipping no JavaScript. The server now says so at boot rather than serving pages that look right and do nothing.
Also in 0.1.4
- Path matching no longer goes through Bun.FileSystemRouter. That removes the workaround for a Bun 1.3.14 native panic on any path containing %, which made GET /%41 a remote denial of service — an abort, not a catchable exception.
- Route patterns are matched from a table rather than the filesystem, so dev and production resolve paths through exactly the same code.
- A route's default export may be async, and the type now says so. It always worked — the server awaits that call — but Component was declared synchronous, so a database query in a route worked at runtime and failed to typecheck. PageComponent is the route-level type; Component stays synchronous because islands and nested components genuinely are.
- The build reports each island's chunk size. JavaScript being opt-in is only checkable if the cost is shown where it is incurred.
Either side of these two
0.1.6 is on what's new. 0.1.3 and 0.1.2 are on past releases. These two have a page of their own because they are one story told twice, and because the version you are on decides which of the two deploy failures you still have.
Something wrong in the framework itself rather than the page? Open an issue on GitHub.