Packages, bundles, budgets
There is no node_modules and no bundler to set up. sluurp add downloads a package into the app's vendor/ folder and records it in sluurp-deps.json (import map, versions, and a hash of every file). The server bundles everything at startup.
# add a package and its dependencies to vendor/
sluurp add npm:d3-force@3
# a JSR package, through JSR's npm registry
sluurp add jsr:@std/path@^1
# show wanted and latest versions, like pnpm
sluurp outdated
# upgrade, and drop files only the old version used
sluurp update --latest
# check every file against npm's tarballs
sluurp vendor verify
sluurp remove d3-force
# show the shared cache location and size; `clear` empties it
sluurp cache- Packages are vendored as source, not as a CDN’s build. CommonJS is converted to ES modules with rolldown at vendoring time.
- Types are vendored too (the package’s own or DefinitelyTyped’s), and
tsconfig.jsonis set up so the editor finds them. - A shared, content-addressed cache makes adding the same package to a second app instant. npm’s and pnpm’s caches are used when they have the file, and
--offlineworks from caches alone.
What you can add
| Spec | What it is |
|---|---|
npm:date-fns, npm:date-fns@4 | An npm package from its own sources, file by file, as the registry has it. Its dependencies come too. |
jsr:@std/path@^1 | A JSR package, through JSR’s npm registry. |
canvas-confetti, three@0.170 | An npm package as one browser-ready module per package, from jsDelivr’s +esm build. |
https://esm.sh/d3@7 | Any URL, from any CDN. |
font:inter:400,600 | A font, from Fontsource, with its CSS. |
Then import it by name, as you would with npm:
import { format } from "date-fns";
import confetti from "canvas-confetti";The name is resolved by the import map in sluurp-deps.json. A package’s own imports of other packages are resolved there too, each in its own scope, so two versions of one package can live side by side.
Compared with npm and package.json
npm with package.json | Sluurp | |
|---|---|---|
| The list of what you use | package.json | sluurp-deps.json: the import map, versions, and a hash of every file |
| The lock file | package-lock.json, pnpm-lock.yaml | the same sluurp-deps.json |
| Where the code lives | node_modules/, installed on each machine, not committed | vendor/, committed with your app |
| What’s kept | the whole tarball of every dependency | only the files your pages can reach |
| Installing after a clone | npm install, which needs the network | nothing: the files are already there |
| Running it | a bundler (Vite, webpack) turns it into browser code | the browser imports the files; the server bundles them when it starts |
| CommonJS | the bundler converts it on every build | converted to ES modules once, when the package is added |
| Install scripts | postinstall runs code on your machine | never run |
| Checking the files | npm audit checks versions | sluurp vendor verify checks every file against the registry’s tarball |
The idea is Deno’s: import by name, with no node_modules and no install step. The difference is that the files are kept with your app. A clone runs as it is, offline, and what runs in production is byte for byte what you tested.
Compared with Node, Deno and Bun
Node, Deno and Bun are JavaScript runtimes: you write the server in JavaScript and they run it. Sluurp is the server, written in Rust. Your JavaScript is the app: its pages and components, and the small pieces of server logic it needs.
| Node, Deno, Bun | Sluurp | |
|---|---|---|
| What you write | the whole server: routing, database access, sign-in, uploads | your app; the database, API, sign-in, files, realtime, jobs and admin are built in |
| Server code | runs in V8 or JavaScriptCore, with the runtime’s APIs | server functions, hooks and jobs run in a fresh QuickJS sandbox per call |
| What server code can reach | the file system, the network, process, native modules | ctx: your data, with its rules; the web’s standard globals, fetch to public addresses among them; no file system, no process |
| TypeScript | Deno and Bun run it; Node strips types | compiled as it’s served, with no configuration |
| Packages | npm, and JSR in Deno | npm and JSR, vendored for the browser |
| Deploying | the runtime, your code, node_modules and a database | one binary and your app folder |
Limitations
- Packages are for the browser.
sluurp addvendors code that runs in pages and islands. A package written for Node doesn’t work, whether it needsfs,net,child_processorBuffer, or is a native addon. - Server code isn’t Node. Server functions, hooks and jobs run in QuickJS, with 64 MB and 5 seconds per call. There’s no Node API, no file system and no WebAssembly;
fetchreaches public addresses only. QuickJS interprets JavaScript rather than compiling it, so heavy computation is several times slower than in V8. See QuickJS: small, safe, fast enough. - No install scripts. A package that builds something when it’s installed (
postinstall,node-gyp) won’t have what it built. - No
package.jsonworkflow. There are noscripts, no workspaces and nonpm run. Sluurp’s commands do the work:serve,test,static,push. - Some packages resist ES modules. A CommonJS package that loads its code at run time, with
require(variable), can’t be converted fully. Use its ES module build, or a CDN’s+esmbuild. - Packages come from the public registries. npm and JSR by name, and anything else by URL; there’s no private registry support yet.
For code the browser shouldn’t carry and QuickJS can’t run, keep it as a program of its own, in whatever runtime suits it. It can read and write your data through the REST API and tell Sluurp what happened with events.
In production
At startup the server bundles each page’s modules with rolldown. Bundles are code-split and minified, and include workers and new URL(…) assets. The result is cached on disk by content and precompressed with brotli and gzip. In development, your own modules are served unbundled; libraries are pre-bundled (see Hot reload).
A budget
{ "first-load-kb": 320, "pages": { "/signup.html": 140 } }/_sluurp/bundle/report.json shows how much each page downloads (compressed) before it can run, and flags pages over budget. Load code that only one page needs with import() from that page.