slurp-dev serves a built theme on 127.0.0.1, watches it, and pushes a reload
to the browser when a file changes. It can answer the theme’s API calls from a
JSON file, so a page that fetches data renders with nothing else running.
http://localhost:3000.
Installation
slurp dev is a launcher. The server itself is a separate binary from a
separate crate, and the launcher looks for it next to the slurp binary, then
on PATH:
cargo build -p slurp-dev-server puts slurp-dev next to slurp in
target/debug or target/release, which is where the launcher looks first.
Run modes
- Serve a build
- Watch sources and rebuild
slurp dev supports directly. Reloads fire on any watched file
changing, but nothing recompiles templates: rebuild them yourself.Flags
These areslurp-dev’s own flags. slurp dev forwards --port and
--fixtures, plus a --bind that is accepted and ignored.
number
default:"3000"
TCP port for the HTTP server and the reload WebSocket. They share one port.
path
default:"."
The directory to serve. Pages come from
pages/*.html inside it, and
everything else is served as a static asset.path
Template sources. Setting it makes the server run
slurp build into
--theme-dir at startup and again after every .slurp change, and enables
server-side rendering for dynamic [param] routes.path
A JSON file, or a directory holding
fixtures.json, mapping a request path to
a canned response. See Fixtures.url
Proxy
/api/* and /auth/* here, and resolve relative {fetch} URLs against
it. Combines with --fixtures: a fixture wins, and a miss falls through to the
backend.path
A JSON file seeded into every SSR render context, the same file you would pass
to
slurp build --globals.string
A CSS build command to run in watch mode alongside the server, killed when the
server exits. It is run through the platform shell, so
PATH and
node_modules/.bin resolve as they would in a terminal.host
An extra hostname the server will answer to. Repeatable. See
Security posture.
--globals seeds the SSR context only. It is not passed through to the
slurp build that --source-dir runs, so a value used by a statically built
page still renders empty in this mode. Pass it to your own slurp build as
well if a page depends on it.How reload works
It is a full page reload, not module-level hot swapping. There is no state preservation and nothing is patched in place. The server injects three lines before</head> (falling back to </body>, then
to the end of the document) into every HTML page it serves:
/_slurp/ws and reconnects every second if it
drops. Three message types come back:
What triggers them: the watcher looks at
.slurp, .css, .js, .ts,
.json, .png, .jpg, .jpeg, .svg and .webp files under --theme-dir,
debounced 50 ms so one save is one rebuild. A change to a non-template file
broadcasts pages: ["*"], meaning everything. A change to a .slurp file is
walked through an import graph first, so editing a component reloads the pages
that import it, not just the component.
A parse error in a watched .slurp file pushes the overlay rather than reloading
into a broken page, and fixing the file clears it.
Fixtures
A fixtures file is a flat JSON object mapping a request path to the response to return for it:fixtures.json
/api/* and /auth/* requests the page makes
from the browser, and {fetch} URLs during SSR. A lookup tries the exact string
first, then the path with any query string stripped, so /api/products?limit=10
still matches a /api/products fixture.
Slurp knows nothing about what the paths mean. It replays whatever JSON the file
records.
A miss with no --backend-url is a 404 that names the path so you can add it:
fixtures.json inside it. A file
that does not parse, or that is not a JSON object, exits 1 at startup rather
than starting up half-configured.
Dynamic routes
With--source-dir set, a request for an extensionless path that no built page
matches is tried against a bracket file: pages/[slug].html catches
/some-product, matching from the deepest segment first.
The server then re-compiles the corresponding .slurp source with the route
parameter in the context, resolves the template’s {fetch} URLs (from fixtures
first, then the backend, forwarding the request’s Cookie header), and renders
in development mode. If that fails it falls back to serving the pre-built bracket
file as-is, which is usually a skeleton.
Security posture
It binds127.0.0.1 unconditionally. Binding loopback does not stop an open web
page from talking to it, so there are two further checks:
- Host. A request whose
Hostheader names anything butlocalhost,127.0.0.1or[::1]is refused with a 403 explaining why. DNS rebinding fails on this check: an attacker pointing their own domain at127.0.0.1makes the browser treat their page as same-origin with the dev server, and theHostheader is the only place that lie is visible. - Origin. A present but foreign
Originis refused on every route. Without it, a cross-site simple request could drive the backend proxy with the user’s cookies attached. An absentOriginis allowed, because a normal navigation and a plaincurlboth send none.
null Origin is refused there too, since
a real browser handshake always carries one. Without that check any page on the
internet could open a socket to ws://localhost:3000/_slurp/ws and read the
build errors, which carry absolute local filesystem paths.
If you point a hosts-file entry at the server, opt it in by name:
mytheme.test and nothing else. Path traversal is refused in every
encoding, including through the dynamic-route and SSR branches, and the proxy
does not relay the upstream’s access-control-allow-origin header: a permissive
dev backend answering * must not hand a foreign page read access to responses
fetched with the user’s cookies.
Status
slurp-dev-server is 0.1.0 and its CLI surface is unstable. slurp dev
forwards a small part of it, and the flags it does not forward may still move.
Next
CLI
Building and validating from the shell.
Editor setup
Highlighting and formatting while you write.
Pages
Routing, and how a page becomes a file.
Common mistakes
Failures that survive a clean reload.