stoneware

Documentation

What gets generated

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

A new project is twelve 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.
│
├── islands/                   The only place client JS originates.
│   └── Counter.tsx            Hydrates on load. Gets its own bundle.
│
├── lib/                       Behavior functions and shared utilities.
│                              Ships JS only if an island imports it.
│
├── public/                    Served as-is, at the URL root.
│   └── styles.css             -> GET /styles.css
│
├── stoneware.config.ts             Port, CSP override, CSRF settings.
├── tsconfig.json              jsx: "react-jsx", jsxImportSource: "stoneware"
├── package.json               scripts: dev / build / start
│
├── .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.
│                           Routes are inlined, so no transpiling per request.
│
├── islands.json            Island name -> public chunk URL.
│                           { "Counter": "/_stoneware/Counter-jzp1gax8.js" }
│
├── static/                 Served under /_stoneware/*, immutable (content-hashed).
│   ├── Counter-jzp1gax8.js     One entry chunk per island.
│   ├── chunk-gcapcpwn.js       Shared runtime: signals + hydrate.
│   └── styles-4kq2n7wd.css     Every .css found beside your code.
│
└── entries/                Generated island entry points. Build input.
stoneware build
routes/ must still exist at runtime. Route modules are inlined into server.js, but path matching reads the directory for its filenames — never for its contents.

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.