The release pipeline is tag-driven: pushing a vX.Y.Z tag triggers
.github/workflows/release.yml, which builds
three platform binaries and publishes four packages to npm.
End-user UX: npm install -g nbm → global nbm command.
Implementation: a single-file native executable produced by deno compile
is shipped over npm using the standard "binary via optionalDependencies"
pattern (esbuild / biome / swc / turbo all use this).
nbm ← the user installs this
bin/nbm.js 40-line Node launcher
optionalDependencies:
@nbm/cli-darwin-arm64 contains bin/nbm (Mach-O)
@nbm/cli-darwin-x64 contains bin/nbm (Mach-O)
@nbm/cli-linux-x64 contains bin/nbm (ELF)
Each platform package declares "os" + "cpu", so npm only installs the
matching one. The launcher resolves the installed platform package's binary
and spawnSyncs it.
- Deno runtime
apps/cli/source (compiled bydeno compile)packages/core/(as TS, via the deno.json import map; build-time only)apps/web/build/(Node-compatible SvelteKitadapter-nodeoutput, withpackages/corealready fused in by vite)- JSR deps
@cliffy/command,@std/assert
User has no Deno, no separate Node-running-our-code, no external runtime deps for nbm itself. (External notebook runtimes — python+marimo/jupyter, julia+Pluto — are still the user's responsibility; they're runtime deps of the notebooks.)
- User runs
nbm ui. - CLI calls
ensureUiRunning(port)in apps/cli/ui.ts. - That self-spawns the same compiled binary into a hidden subcommand:
Deno.execPath() __serve-ui --port N. - The hidden command in apps/cli/commands/__serve-ui.ts
extracts the embedded SvelteKit
build/to~/.nbm/web-build/<version>/on first run (workaround:createReadStreamfails against Deno's compiled virtual filesystem), then loadshandler.jsand serves it vianode:http. Printshttp://localhost:<port>on listen. - The parent scrapes the URL from the log file, persists state to
~/.nbm/ui.json, opens the browser, and (for foregroundnbm ui) blocks until Ctrl+C.
In dev mode (deno run apps/cli/main.ts) the same code path detects it's
not running from a compiled binary (via Deno.execPath() ending in
/deno) and spawns vite dev from the workspace instead, preserving hot
reload during development.
| Path | Purpose |
|---|---|
| apps/cli/main.ts | Cliffy entry, registers all commands incl. hidden __serve-ui. |
| apps/cli/version.ts | Single source of truth for VERSION. scripts/build-binary.ts string-replaces this literal at compile time. |
| apps/cli/ui.ts | UI lifecycle. Dual-mode dispatch (vite in dev, self-spawn in compiled). |
| apps/cli/commands/__serve-ui.ts | Hidden subcommand. Extracts the embedded SvelteKit build to ~/.nbm/web-build/<version>/ on first run, then loads handler.js and serves via node:http. |
| apps/cli/update-check.ts | Non-blocking npm registry check (24h cache). Prints a one-liner if outdated. Disabled by NBM_NO_UPDATE_CHECK=1, on --version/--help, on __serve-ui, and on -dev builds. |
| apps/web/ | SvelteKit app (adapter-node). No source changes for packaging — vite handles the workspace import of @nbm/core at build time. |
| packages/core/ | Shared workspace package. Private; never published. Bundled twice into the binary (once as TS via Deno's import map, once as JS via vite's web build). |
| scripts/build-binary.ts | One platform build. pnpm --dir apps/web build → bundle SvelteKit handler with esbuild → patch version.ts → deno compile --no-check --include apps/web/build → restore version.ts. |
| scripts/release.sh | Bump version across all four npm/*/package.jsons, commit, tag, push. |
| .github/workflows/release.yml | Tag-driven matrix (macos-14, macos-13, ubuntu-24.04) → upload artifacts → publish all four packages. |
| npm/nbm/ | User-facing launcher package. bin/nbm.js resolves the platform binary and spawnSyncs it. |
| npm/cli-darwin-arm64/, npm/cli-darwin-x64/, npm/cli-linux-x64/ | Platform-specific package.json templates declaring os/cpu. CI populates bin/nbm from build artifacts. |
The SvelteKit adapter-node build emits handler.js plus a tree of server
chunks under build/server/ that import real npm packages at runtime
(shiki for code preview, polka, sirv, cookie from the adapter
itself). If those imports remain, deno compile walks the workspace's pnpm
node_modules to resolve them — and pnpm's hoisting drags in 140 MB of
dev-only tooling (TypeScript, rolldown, vite plugins, etc.) along with
the real runtime deps. The binary explodes from ~88 MB to ~244 MB.
Workaround in scripts/build-binary.ts: after the
SvelteKit build, run esbuild to bundle handler.js + everything it imports
(including all npm deps) into a single self-contained file with node:* as
the only externals. Replace handler.js with the bundled output, delete the
now-redundant server/, env.js, shims.js, index.js. Then set
"nodeModulesDir": "none" in the workspace deno.json so deno compile
skips node_modules walking entirely.
SvelteKit's adapter-node build output (apps/web/build/handler.js) has
JSDoc @type comments referencing npm packages like polka, sirv,
@standard-schema/spec, @opentelemetry/api. These are type-only
references, not runtime imports — the polka/sirv code itself is bundled
inline by SvelteKit. deno check flags them anyway. --no-check skips the
check; runtime is unaffected.
deno compile's embedded virtual filesystem supports Deno.readFile and
fs.readFileSync against included assets, but not fs.createReadStream
("Failed to get OS file descriptor"). SvelteKit's adapter-node uses
createReadStream for static asset serving (via the bundled sirv).
Workaround in __serve-ui.ts: on first
run for a given binary version, walk the embedded apps/web/build/ tree
via Deno.readDir + Deno.readFile and write it out to
~/.nbm/web-build/<VERSION>/. Subsequent runs use the cached extraction.
- Reserve the unscoped name
nbmand the@nbmorg on npm. (If@nbmis taken, change every reference to@nbm/cli-*-*in npm/nbm/package.json, npm/nbm/bin/nbm.js, and the three npm/cli--/package.json files.) - Generate an npm token: https://www.npmjs.com/settings/~/tokens, type "Automation", scope "Publish".
- Add it to GitHub: repo → Settings → Secrets → Actions →
NPM_TOKEN = <token>.
./scripts/release.sh 0.1.0What happens:
- Sanity checks: clean working tree, on
main, up-to-date with origin, tag doesn't already exist. - Bumps version in
npm/nbm/package.jsonand the three platform packages (and theoptionalDependenciesreferences) to match. - Commits
release: v0.1.0, tagsv0.1.0, pushes the branch and tag. - Prints a link to the running GitHub Actions workflow.
CI then:
- Builds three binaries in parallel (macOS arm64 on
macos-14, macOS x64 onmacos-13, Linux x64 onubuntu-24.04). - Each job runs scripts/build-binary.ts, which
does
pnpm --dir apps/web buildthendeno compile. - Smoke-tests
--versionon the freshly built binary. - Uploads each binary as an artifact.
- The publish job downloads all three, populates
npm/cli-*-*/bin/nbm, and runsnpm publishon each platform package, then onnbm.
After it's green: npm install -g nbm@0.1.0 works for any user on a supported
platform. Total wall-clock time: ~5–10 minutes.
For testing changes before tagging:
deno run -A scripts/build-binary.ts darwin-arm64
# → dist/darwin-arm64/bin/nbmYou can move the resulting binary anywhere and run it; it has no runtime deps beyond the external notebook tools (python+marimo/jupyter, julia+Pluto).
Re-run the failed jobs from the GitHub UI: Actions → failed run → "Re-run failed jobs". The build is deterministic from the tagged commit.
Don't retag. The version that already published is locked. Fix the issue
and run ./scripts/release.sh 0.1.1 (or whatever next semver). On the next
release the launcher's optionalDependencies will reference the new version
of every package and everything realigns.
Within 72 hours of publish:
npm unpublish nbm@0.1.0 --force
npm unpublish @nbm/cli-darwin-arm64@0.1.0 --force
npm unpublish @nbm/cli-darwin-x64@0.1.0 --force
npm unpublish @nbm/cli-linux-x64@0.1.0 --forceAfter 72 hours, deprecate instead:
npm deprecate nbm@0.1.0 "broken; use 0.1.1 or later"Then ./scripts/release.sh 0.1.1.
After a release, sanity-check on a machine that doesn't have your dev environment:
npm install -g nbm@0.1.0
nbm --version
nbm ui # opens browser, dashboard loads
nbm start ~/some-notebook.pyIf something fails:
npm uninstall -g nbm
# fix bug in repo
./scripts/release.sh 0.1.1