---
title: Packages, bundles, budgets
description: npm and JSR packages vendored as source, without package.json or node_modules; compared with npm, Node, Deno and Bun, and the limitations.
section: Frontend
order: 8
---

# Packages, bundles, budgets

<p class="lead">There is no <code>node_modules</code> and no bundler to set up. <code>sluurp add</code> downloads a package into the app's <code>vendor/</code> folder and records it in <code>sluurp-deps.json</code> (import map, versions, and a hash of every file). The server bundles everything at startup.</p>

```sh title="Terminal"
# 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.json` is 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 `--offline` works 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](https://jsr.io) 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:

```tsx
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](/docs/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 add` vendors code that runs in pages and islands. A package written for Node doesn't work, whether it needs `fs`, `net`, `child_process` or `Buffer`, 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; `fetch` reaches 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](/docs/server-functions#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.json` workflow.** There are no `scripts`, no workspaces and no `npm 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 `+esm` build.
- **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](/docs/api) and tell Sluurp what happened with [events](/docs/hooks-and-events#events-from-outside).

## 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](/docs/hot-reload)).

## A budget

```json title="budget.json"
{ "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.
