Skip to content

feat: Bun-first scaffold mode (--runtime bun) across all 3 templates#569

Merged
vivek7405 merged 12 commits into
mainfrom
feat/bun-runtime-scaffold
Jun 18, 2026
Merged

feat: Bun-first scaffold mode (--runtime bun) across all 3 templates#569
vivek7405 merged 12 commits into
mainfrom
feat/bun-runtime-scaffold

Conversation

@vivek7405

@vivek7405 vivek7405 commented Jun 18, 2026

Copy link
Copy Markdown
Collaborator

Closes #541

Adds a --runtime node|bun axis to webjs create, ORTHOGONAL to --template (the exactly-3-templates invariant is untouched). --runtime bun, and bun create webjs <name> (auto-detected from the invoking package manager), emit a Bun-flavored app across all three templates; --runtime node (the default) is byte-identical to today.

The load-bearing design decision: server on Bun, tooling on Node

The SERVER (the dev / start scripts) runs on Bun, because that is the app. The dev/build TOOLING (webjs test / db:* / check / typecheck) stays on Node, because two things break under Bun (both verified empirically):

  • webjs test spawns node --test; under Bun that becomes bun --test, which is invalid (Bun's runner is bun test).
  • webjs db migrate shells npx drizzle-kit, and a pure oven/bun image has no npx.

So: only dev / start force bun --bun (overriding the webjs bin's Node shebang); everything else stays plain webjs / bun run on Node.

What bun mode changes in the generated app

  • package.json scripts: dev / start become bun --bun webjs ...; tooling stays plain webjs ....
  • trustedDependencies: ["better-sqlite3"] (sqlite only) so its native-prebuild postinstall runs (Bun skips postinstalls). Pure-JS pg needs none.
  • Lockfile: bun install writes bun.lock.
  • Dockerfile: keeps the node:24-alpine base and COPIES in the Bun binary (so npx / drizzle-kit / the shebang bins work AND the server serves on Bun via CMD ["bun","--bun","run","start"]). A pure oven/bun base is deliberately NOT used (no npx). bun install, bun.lock, node -e healthcheck (the node base provides node).
  • CI workflow: keeps actions/setup-node (the tooling runs on node), ADDS oven-sh/setup-bun, installs with bun install, runs scripts with plain bun run.
  • compose.yaml: not transformed (builds from the Dockerfile, inherits the bun CMD; its node -e healthcheck works on the node base).
  • Agent-config markdown + starter-test header comments: bun command forms.

All bun output is DERIVED from the canonical node templates by pure string transforms (packages/cli/lib/runtime-rewrite.js), so there is no parallel bun template to drift.

Drive-by

Fixed stale Prisma references in templates/.cursorrules (missed in the #558 Prisma->Drizzle migration while the sibling agent configs were updated).

Note on the issue body

The issue predates the #558 Drizzle migration, so its Prisma references are adapted to Drizzle (trustedDependencies covers better-sqlite3, not the Prisma client). There is no README template to bun-ify (the scaffold generates none).

Tests + verification

  • packages/cli/test/runtime-rewrite/ (unit, the pure transforms) + test/scaffolds/scaffold-runtime.test.js (integration across all 3 templates, autodetect, override, node-unchanged, unknown-runtime rejection). Full CLI + scaffold suites: 40 pass.
  • A bun scaffold passes webjs check with only the 2 expected scaffold-placeholder violations (no transform-introduced violations); node and bun produce identical check output.
  • Dogfood: website / docs / ui-website boot 200 in prod mode (incl. the edited /docs pages), no broken preloads. The blog is not on this change's import path (scaffold-codegen only; the 4 apps import @webjsdev/core/server, not the cli create logic). Node-mode scaffold output is byte-identical to before.

Docs surfaces

  • Updated: root AGENTS.md (scaffolding section), packages/cli/AGENTS.md (module map + create row + invariant-1 orthogonality note), bin/webjs.js USAGE, docs site getting-started (both bun create webjs / bunx create-webjs@latest invocation styles + the npm -- separator note) and deployment (the generated node-base+bun-binary Dockerfile).
  • N/A: website/app/page.ts (makes no claim contradicted by this change; npm create webjs stays the canonical hero command and bun is auto-detected); MCP / editor plugins (no introspection surface or grammar changed); other docs topics.

Self-review

Ran the self-review loop to a clean round. Findings fixed along the way: the generated Bun-section doc framing, CI bun -e (later reverted with the node-base correction), the create-webjs arg parser reading a flag value as the app name, starter-test-comment bun-ification + npm i shorthand, --db forwarding through the wrapper, the server-on-bun/tooling-on-node correction (the bun --test + missing-npx discoveries), a saas-template comment, and a USAGE template-literal syntax bug.

@vivek7405 vivek7405 self-assigned this Jun 18, 2026
vivek7405 pushed a commit that referenced this pull request Jun 18, 2026
…pper arg parse)

Self-review findings on #569:
- The generated bun AGENTS.md 'Running on Bun' section framed Bun as opt-in
  ('force it with --bun'), but the bun app's dev/start scripts already embed
  --bun. Reframe it as the configured default.
- bunifyCi left the e2e Chromium-path step on 'node -e'; switch it to 'bun -e'
  so a fully-Bun CI does not rely on Node being implicitly present.
- create-webjs positional parsing read a value-taking flag's value as the app
  name (e.g. 'create-webjs --runtime bun my-app' picked 'bun'); skip flag
  values when collecting positionals.
vivek7405 pushed a commit that referenced this pull request Jun 18, 2026
Round-2 review findings on #569:
- The scaffolded starter tests (test/hello/browser, test/hello/e2e) carried
  'npx wtr' / 'npm i -D puppeteer-core' header comments copied verbatim, so a
  bun app shipped test files telling the user to run npm/npx. Add them to the
  prose-rewrite set and teach bunifyProse the 'npm i' shorthand.
- create-webjs forwarded template/runtime/install but dropped --db, so
  'bun create webjs my-app --db postgres' silently scaffolded sqlite while the
  docstring claimed behaviour matches 'webjs create' exactly. Forward --db
  (and skip its value in positional parsing).
@vivek7405

Copy link
Copy Markdown
Collaborator Author

Self-review: 5 rounds, last clean

Ran fresh-context review rounds over the whole change. What they caught and how it was resolved:

  • The generated bun AGENTS.md framed Bun as opt-in ("force it with --bun") when the dev/start scripts already embed it. Reframed as the configured default.
  • create-webjs --runtime bun my-app read bun as the app name (the positional parser did not skip a value-taking flag's value). Fixed, and --db is now forwarded too (it was silently dropped).
  • The starter test header comments shipped npx wtr / npm i -D into a bun app. Bun-ified them.
  • The big one: the first cut put the whole app on Bun. Two things break under Bun, both verified. webjs test spawns node --test, which becomes the invalid bun --test; and webjs db migrate shells npx drizzle-kit, absent from a pure oven/bun image. So the design changed to "server on Bun, tooling on Node": only dev/start force --bun, and the Dockerfile keeps the node:24-alpine base with the Bun binary copied in (the same pattern our in-repo apps deploy with).
  • A saas-template comment and a USAGE template-literal backtick bug (the latter would have stopped the CLI from loading; node --check caught it).

Generated output verified for both runtimes and all three templates, webjs check parity, and the three dogfood apps booting in prod mode.

@vivek7405 vivek7405 marked this pull request as ready for review June 18, 2026 06:49
t added 11 commits June 18, 2026 13:14
…rites)

Add a runtime axis orthogonal to --template (the 3-templates invariant is
untouched). `webjs create --runtime bun` (and `bun create webjs`, auto-detected
from the invoking PM) emits a Bun-flavored app:

- package.json: dev/start scripts force `bun --bun webjs ...` so the long-running
  server runs on Bun despite the webjs bin's node shebang (the gotcha); tooling
  scripts stay plain webjs (node:test/drizzle-kit/tsc are runtime-neutral).
  trustedDependencies=[better-sqlite3] so its native prebuild postinstall runs
  (Bun skips postinstalls by default).
- bun install (commits bun.lock) instead of npm/package-lock.
- Dockerfile -> oven/bun:1 base + bun install + bun -e healthcheck + bun start;
  compose healthcheck -> bun -e; CI -> setup-bun + bun install + bun --bun run.
- agent-config markdown -> bun command forms.

All derived from the canonical node templates by pure transforms (runtime-rewrite.js)
so there is no parallel bun template to drift. Node mode is byte-identical to
before. Tests + docs follow in subsequent commits.
templates/.cursorrules still described Prisma (prisma/schema.prisma,
lib/prisma.server.ts, @prisma/client, 'define a Prisma model'), missed in
the #558 Prisma->Drizzle migration while the sibling agent configs were
updated. Sync it to Drizzle (db/schema.server.ts, db/connection.server.ts,
db:generate + db:migrate, better-sqlite3/pg driver).

Also extend bunifyProse so the bun-mode AGENTS.md Dockerfile prose matches
the generated bun Dockerfile (node:24-alpine -> oven/bun:1 pin claim, and the
CMD [npm start] -> CMD [bun --bun run start] equivalence note).
Unit tests for the pure bunify* transforms (commands, registry-noun
survival, Dockerfile/compose/CI rewrites) and integration tests asserting a
bun scaffold across all 3 templates: dev/start force --bun, trustedDependencies
is sqlite-only, the deploy/CI files run on Bun, the agent markdown carries no
npm commands, bun is auto-detected from the invoking PM, the explicit flag
overrides detection, an unknown runtime is rejected, and node mode stays
byte-unchanged.
…d, deployment)

packages/cli/AGENTS.md: runtime-rewrite.js module-map entry, the create
command row, and an invariant-1 note that --runtime is orthogonal (not a 4th
template). Root AGENTS.md scaffolding section: the --runtime flag. Docs site:
a getting-started Bun-scaffold subsection (both bun create / bunx
create-webjs invocation styles + the npm -- separator note) and a deployment
bullet for the generated pure oven/bun Dockerfile.
…pper arg parse)

Self-review findings on #569:
- The generated bun AGENTS.md 'Running on Bun' section framed Bun as opt-in
  ('force it with --bun'), but the bun app's dev/start scripts already embed
  --bun. Reframe it as the configured default.
- bunifyCi left the e2e Chromium-path step on 'node -e'; switch it to 'bun -e'
  so a fully-Bun CI does not rely on Node being implicitly present.
- create-webjs positional parsing read a value-taking flag's value as the app
  name (e.g. 'create-webjs --runtime bun my-app' picked 'bun'); skip flag
  values when collecting positionals.
Round-2 review findings on #569:
- The scaffolded starter tests (test/hello/browser, test/hello/e2e) carried
  'npx wtr' / 'npm i -D puppeteer-core' header comments copied verbatim, so a
  bun app shipped test files telling the user to run npm/npx. Add them to the
  prose-rewrite set and teach bunifyProse the 'npm i' shorthand.
- create-webjs forwarded template/runtime/install but dropped --db, so
  'bun create webjs my-app --db postgres' silently scaffolded sqlite while the
  docstring claimed behaviour matches 'webjs create' exactly. Forward --db
  (and skip its value in positional parsing).
Round-3 review exposed two correctness bugs in the bun runtime design,
both verified empirically:

1. webjs test spawns process.execPath --test. Under bun that is 'bun --test',
   which is INVALID (bun's runner is 'bun test'; 'bun --test' errors). So
   forcing --bun on the test scripts (bunifyCi did 'bun --bun run test:server')
   would fail CI. Fix: only the SERVER scripts (dev/start) force --bun; all
   tooling (test/db/check/typecheck) stays on Node via plain 'bun run' / the
   webjs bin's node shebang. bunifyProse and bunifyCi updated accordingly.

2. webjs db migrate shells 'npx drizzle-kit', and a pure oven/bun image has no
   npx (confirmed: oven/bun ships a node fallback shim but no npx). So a pure
   oven/bun Dockerfile base breaks migrate-at-boot. Fix: the bun Dockerfile
   KEEPS the node:24-alpine base and COPIES in the bun binary (npx + node
   toolchain present, server serves on bun via 'bun --bun run start'), the
   verified pattern the in-repo apps use. compose.yaml no longer needs a
   transform (inherits the Dockerfile CMD; its node -e healthcheck works on the
   node base).

Docs (root + cli AGENTS, getting-started, deployment) corrected to describe the
node-base+bun-binary Dockerfile and the node-tooling split. Tests updated.
…--bun run)

The comment claimed the bun-ified auth-test reads 'bun --bun run db:*'; the
actual (correct) output is plain 'bun run db:*' since db is Node tooling. The
generated output was already right; only the code comment was wrong.
The --runtime USAGE line used backticks around 'bun create webjs' inside the
backtick-delimited USAGE template literal, which closed it and made
bin/webjs.js a SyntaxError (the CLI would not load). Tests missed it because
they import lib/create.js, not the bin. Use double quotes instead. Verified
with node --check + running the bin.
…default)

test/preload-subset.test.mjs boots all four in-repo apps and probes their
modulepreloads (#204); the website boot's jspm vendor resolution tips just
over bun test's 5s per-test default on the CI runner (~5.1s locally). Same
app-boot + vendor-resolution class as the already-denylisted differential-elision
and test/docs. The invariant is covered on the Node path; a real bun app boot is
covered by test/bun/listener.mjs. Pre-existing flake (#204), surfaced on the
#541 PR's bun matrix; unrelated to the scaffold change.
…ebase

After rebasing on #570 (npx-free webjs db/test), a pure oven/bun scaffold
Dockerfile is technically viable, but scaffolds pin @webjsdev/cli: latest, so
it is only safe once cli with #570 is PUBLISHED (an old installed cli still
shells npx, which a pure oven/bun image lacks). So #541 ships the node-base +
bun-binary Dockerfile (works with any cli version); align the cli AGENTS and
deployment docs to that, noting a pure oven/bun image becomes the default in a
future scaffold once cli@#570 is the published latest.
@vivek7405 vivek7405 force-pushed the feat/bun-runtime-scaffold branch from b209bdc to 3cb7315 Compare June 18, 2026 07:51
Post-#570, webjs db migrate is npx-free, so the old justification ('keep the
node base because webjs db migrate needs npx') is stale and was a same-file
self-contradiction in packages/cli/AGENTS.md (the runtime-rewrite entry vs the
#570 webjs db row), plus it shipped into every generated bun Dockerfile comment.

The real reason the scaffold stays node-base: a scaffolded app pins
@webjsdev/cli: latest, and until the npx-free #570 build is the published
latest, an installed CLI may still shell npx for the boot webjs db migrate,
which a pure oven/bun image lacks. The node base works with ANY installed CLI;
a future scaffold can drop to a pure oven/bun base once #570 is published.
Rewrote the rationale in runtime-rewrite.js (incl. the baked Dockerfile
comment), packages/cli/AGENTS.md, root AGENTS.md, and getting-started.
@vivek7405 vivek7405 merged commit 92d544c into main Jun 18, 2026
9 checks passed
@vivek7405 vivek7405 deleted the feat/bun-runtime-scaffold branch June 18, 2026 08:15
vivek7405 pushed a commit that referenced this pull request Jun 18, 2026
Ship the unreleased @webjsdev/cli feat work: runtime-native webjs db/test (#571,
the npx-free CLI that makes a pure oven/bun image viable) and the Bun-first
scaffold --runtime bun (#569). Patch bump; dependents pin ^0.10.0, satisfied.
vivek7405 added a commit that referenced this pull request Jun 18, 2026
Ship the unreleased @webjsdev/cli feat work: runtime-native webjs db/test (#571,
the npx-free CLI that makes a pure oven/bun image viable) and the Bun-first
scaffold --runtime bun (#569). Patch bump; dependents pin ^0.10.0, satisfied.

Co-authored-by: t <t@t>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add a Bun-first scaffold mode (--runtime bun) across all 3 templates

1 participant