stoneware

Documentation

What gets generated

Every file create-stoneware writes, what it is for, and what a build adds.

A new project is sixteen files. There is no hidden state, no lockfile-adjacent cache to understand, and nothing generated that you are not meant to read.

my-site/
│
├── routes/                    Server-only. Never ships JavaScript.
│   ├── index.tsx              A page. Maps to "/"
│   ├── _404.tsx               Shown for any path that does not match.
│   │                          Leading _ means it is not itself a page.
│   ├── robots.txt.ts          A route that returns text rather than HTML,
│   │                          so it is written at its literal path.
│   └── sitemap.xml.ts         Built with sitemap(). Add the pages you
│                              publish to the list it exports.
│
├── islands/                   The only place client JS originates.
│   └── Counter.tsx            Hydrates on load. Gets its own bundle.
│
├── lib/                       Behavior functions and shared utilities.
│   └── site.ts                SITE_URL, for canonical links and the sitemap.
│                              Ships JS only if an island imports it.
│
├── public/                    Served as-is, at the URL root.
│   ├── styles.css             -> GET /styles.css
│   ├── favicon.ico            -> GET /favicon.ico
│   └── mark.svg               -> GET /mark.svg
│
├── stoneware.config.ts        Port, CSP override, CSRF settings.
├── tsconfig.json              jsx: "react-jsx", jsxImportSource: "stoneware"
├── package.json               scripts: dev / build / start
├── README.md                  The commands, and where the docs live.
│
├── .env                       STONEWARE_CSRF_SECRET, generated unique. Gitignored.
├── .env.example               Tracked template, no value.
└── .gitignore                 node_modules/ .stoneware/ .env
bunx create-stoneware my-site

The two directories that matter

routes/ and islands/ are not a style preference. They are the mechanism behind the framework's central claim, and the difference is enforced by the build rather than by convention.

routes/**          islands/**
    │                  │
    │                  └──► entry point for Bun.build (browser target)
    │                             │
    │                             └──► shipped to the client
    │
    └──► never passed to the bundler at all
              │
              └──► cannot reach the client, by construction
why the split is structural

Nothing scans your route files for a directive and decides. Server-only code is not excluded by a heuristic that might get it wrong — it is simply never handed to the bundler.

What a build adds

stoneware build writes everything into .stoneware/, which is gitignored. Deleting it is always safe.

my-site/.stoneware/
│
├── server.js               One bundle: framework + every route + every island,
│                           plus the pattern table used to match paths.
├── server.js.map           Source map, so a stack trace still names your file.
├── server-entry.ts         The generated entry the bundle was built from.
│
├── islands.json            Island name -> public chunk URL.
│                           { "Counter": "/_stoneware/Counter-rzesgezg.js",
│                             "@runtime": "/_stoneware/stoneware-runtime-2pweh7fr.js" }
│
├── static/                 Served under /_stoneware/*, immutable (content-hashed).
│   ├── Counter-rzesgezg.js          One entry chunk per island.
│   ├── chunk-y24dm8pf.js            Shared code, hoisted out of the entries.
│   ├── stoneware-runtime-2pw...js   Lazy hydration. Always emitted; a few hundred bytes.
│   └── styles-4kq2n7wd.css          Every .css found beside your code.
│
└── entries/                Generated island entry points. Build input.
stoneware build
Since 0.1.4 the source tree is a build input, not a runtime dependency. Routes and islands are inlined into server.js and paths are matched against a pattern table written beside them, so a built server runs with routes/ and islands/ deleted — which is what makes a minimal container image possible.

The shared chunk is why a page with three islands does not download signals three times. Each island entry is small; the runtime they have in common is hoisted out once.

The stylesheet only appears if the project has a .css file under routes/, islands/ or lib/. A new project styles itself from public/styles.css instead, at a fixed URL — both work, and the co-located route is the one that scales past a single file.

Something wrong in the framework itself rather than the page? Open an issue on GitHub.