@webjsdev/cli 0.10.27 → 0.10.28

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -33,7 +33,7 @@ npx @webjsdev/cli create my-app
33
33
  cd my-app && npm run dev
34
34
  ```
35
35
 
36
- `webjs create` installs dependencies in the new directory by default on **Node** (it needs `node_modules` to run). On **Bun** it **skips** the install (zero-install: `bun run dev` resolves deps on the fly). Pass `--install` to force the install, or `--no-install` to skip it, on either runtime.
36
+ Both `webjs create` and `create-webjs-app` auto-install dependencies in the new directory using your detected package manager (npm / pnpm / yarn / bun). Pass `--no-install` to opt out.
37
37
 
38
38
  ## Commands
39
39
 
package/bin/webjs.js CHANGED
@@ -6,7 +6,6 @@ import { resolveBin } from '../lib/resolve-bin.js';
6
6
  import { checkNodeInline, nodeInlineMessage } from '../lib/node-preflight.js';
7
7
  import { loadAppEnv, resolvePort } from '../lib/port.js';
8
8
  import { planDevSupervisor } from '../lib/dev-supervisor.js';
9
- import { importWebjsdev } from '../lib/import-webjsdev.js';
10
9
 
11
10
  const __dirname = dirname(fileURLToPath(import.meta.url));
12
11
  const [cmd, ...rest] = process.argv.slice(2);
@@ -51,12 +50,12 @@ const USAGE = `webjs commands:
51
50
  webjs doctor Verify project health (Node, tsconfig, env, vendor pins, importmap coherence, @webjsdev versions, git hook, page/layout elision)
52
51
  webjs types Generate .webjs/routes.d.ts (typed Route union + per-route params)
53
52
  webjs typecheck [tsc args...] Type-check the app with the project's tsc --noEmit (non-zero on errors)
54
- webjs create <name> [--template full-stack|api|saas] [--db sqlite|postgres] [--runtime node|bun] [--install|--no-install] Scaffold a new webjs app
53
+ webjs create <name> [--template full-stack|api|saas] [--db sqlite|postgres] [--runtime node|bun] [--no-install] Scaffold a new webjs app
55
54
  (only 3 templates exist. default: full-stack, Drizzle, --db sqlite, --runtime node)
56
55
  --runtime bun emits a Bun-flavored app (bun.lock, bun Dockerfile/CI, bun docs);
57
56
  also auto-detected when run via "bun create webjs".
58
- Install default is per runtime: Node installs; Bun skips (zero-install,
59
- "bun run dev" resolves deps on the fly). --install / --no-install override.
57
+ Auto-runs the detected package manager's install in the new dir
58
+ unless --no-install is passed.
60
59
  webjs db generate Generate a SQL migration from the schema (drizzle-kit generate)
61
60
  webjs db migrate Apply pending migrations (drizzle-kit migrate)
62
61
  webjs db push Push the schema straight to the dev DB (drizzle-kit push)
@@ -122,7 +121,7 @@ async function main() {
122
121
  // instead of crashing cryptically later. `help` is exempt so a user on an
123
122
  // old Node can still read usage.
124
123
  if (cmd !== 'help' && cmd !== undefined) {
125
- const { assertNodeVersion } = await importWebjsdev('@webjsdev/server');
124
+ const { assertNodeVersion } = await import('@webjsdev/server');
126
125
  assertNodeVersion({ onFail: 'exit' });
127
126
  }
128
127
  switch (cmd) {
@@ -130,7 +129,7 @@ async function main() {
130
129
  // If we're already inside the reload child (node --watch or bun --hot),
131
130
  // start the server directly.
132
131
  if (process.env.__WEBJS_DEV_CHILD === '1') {
133
- const { startServer } = await importWebjsdev('@webjsdev/server');
132
+ const { startServer } = await import('@webjsdev/server');
134
133
  // Load `.env` BEFORE resolving the port so a `PORT` set there is in
135
134
  // process.env at resolution time (#447). The server loads `.env`
136
135
  // too, but that runs too late to affect the port the CLI computes.
@@ -166,7 +165,7 @@ async function main() {
166
165
  });
167
166
 
168
167
  if (plan.mode === 'inline') {
169
- const { startServer } = await importWebjsdev('@webjsdev/server');
168
+ const { startServer } = await import('@webjsdev/server');
170
169
  loadAppEnv(process.cwd());
171
170
  const port = resolvePort(flag(rest, '--port'));
172
171
  await startServer({ appDir: process.cwd(), port, dev: true });
@@ -183,7 +182,7 @@ async function main() {
183
182
  break;
184
183
  }
185
184
  case 'start': {
186
- const { startServer } = await importWebjsdev('@webjsdev/server');
185
+ const { startServer } = await import('@webjsdev/server');
187
186
  // Load `.env` BEFORE resolving the port so a `PORT` set there wins over
188
187
  // the 8080 default (#447), same as for `dev`.
189
188
  loadAppEnv(process.cwd());
@@ -365,7 +364,7 @@ async function main() {
365
364
  break;
366
365
  }
367
366
  case 'check': {
368
- const { checkConventions, RULES } = await importWebjsdev('@webjsdev/server/check');
367
+ const { checkConventions, RULES } = await import('@webjsdev/server/check');
369
368
 
370
369
  if (rest.includes('--rules')) {
371
370
  console.log('webjs check, correctness rules:');
@@ -391,7 +390,7 @@ async function main() {
391
390
  if (rest.includes('--json')) {
392
391
  // The projector lives in @webjsdev/mcp (the MCP `check` tool's home),
393
392
  // so `check --json` and the MCP tool stay byte-identical (#415).
394
- const { projectCheck } = await importWebjsdev('@webjsdev/mcp/check-report');
393
+ const { projectCheck } = await import('@webjsdev/mcp/check-report');
395
394
  console.log(JSON.stringify(projectCheck(violations)));
396
395
  if (violations.length > 0) process.exit(1);
397
396
  break;
@@ -448,7 +447,7 @@ async function main() {
448
447
  // narrowing the @webjsdev/core `Route` href union + per-route `params`.
449
448
  // Opt-in codegen: the static types in @webjsdev/core work without it
450
449
  // (un-generated apps see `Route = string`).
451
- const { generateRouteTypes } = await importWebjsdev('@webjsdev/server');
450
+ const { generateRouteTypes } = await import('@webjsdev/server');
452
451
  const { mkdir, writeFile } = await import('node:fs/promises');
453
452
  const appDir = process.cwd();
454
453
  const text = await generateRouteTypes(appDir);
@@ -522,11 +521,7 @@ files.
522
521
  Full docs: https://docs.webjs.com`);
523
522
  process.exit(1);
524
523
  }
525
- // Install policy (#682). Default per runtime: Node installs (needs
526
- // node_modules to run); Bun skips (zero-install, `bun run dev` resolves
527
- // on the fly). `--install` / `--no-install` override either way.
528
524
  const noInstall = rest.includes('--no-install');
529
- const explicitInstall = rest.includes('--install');
530
525
  // --db picks the database dialect: sqlite (default) or postgres.
531
526
  const db = flag(rest, '--db', 'sqlite');
532
527
  // --runtime picks the target runtime: node (default) or bun. Orthogonal
@@ -537,16 +532,15 @@ Full docs: https://docs.webjs.com`);
537
532
  console.error(`Error: unknown --runtime '${runtime}'. Only node / bun are supported.`);
538
533
  process.exit(1);
539
534
  }
540
- const { scaffoldApp, resolveCreateInstall } = await import('../lib/create.js');
541
- const install = resolveCreateInstall({ runtime, explicitInstall, noInstall });
542
- await scaffoldApp(name, process.cwd(), { template, db, runtime, install });
535
+ const { scaffoldApp } = await import('../lib/create.js');
536
+ await scaffoldApp(name, process.cwd(), { template, db, runtime, install: !noInstall });
543
537
  break;
544
538
  }
545
539
  case 'vendor': {
546
540
  const sub = rest[0];
547
541
  const args = rest.slice(1);
548
542
  const appDir = process.cwd();
549
- const { pinAll, unpinPackage, listPinned, auditPinned, findOutdated, updatePinned, readPinFile, ensureVendorCommittable, SUPPORTED_PROVIDERS } = await importWebjsdev('@webjsdev/server');
543
+ const { pinAll, unpinPackage, listPinned, auditPinned, findOutdated, updatePinned, readPinFile, ensureVendorCommittable, SUPPORTED_PROVIDERS } = await import('@webjsdev/server');
550
544
 
551
545
  // Parse `--from <provider>` once at the top so subcommands share it.
552
546
  // Mirrors importmap-rails's `bin/importmap pin foo --from jsdelivr`.
@@ -809,7 +803,7 @@ Full docs: https://docs.webjs.com`);
809
803
  // runnable directly as `npx @webjsdev/mcp`); `webjs mcp` delegates to it
810
804
  // for back-compat. The version advertised in the initialize handshake is
811
805
  // @webjsdev/mcp's own, resolved by its bin, so this passes none.
812
- const { runMcpServer } = await importWebjsdev('@webjsdev/mcp');
806
+ const { runMcpServer } = await import('@webjsdev/mcp');
813
807
  const { createRequire } = await import('node:module');
814
808
  const require = createRequire(import.meta.url);
815
809
  let version = '0.0.0';
package/lib/create.js CHANGED
@@ -14,7 +14,7 @@
14
14
  import { mkdir, writeFile, readFile, cp } from 'node:fs/promises';
15
15
  import { join, resolve, dirname } from 'node:path';
16
16
  import { fileURLToPath } from 'node:url';
17
- import { existsSync, readFileSync } from 'node:fs';
17
+ import { existsSync } from 'node:fs';
18
18
  import { createRequire } from 'node:module';
19
19
  import { spawnSync } from 'node:child_process';
20
20
  import { bunifyProse, bunifyDockerfile, bunifyCompose, bunifyCi } from './runtime-rewrite.js';
@@ -35,86 +35,6 @@ function detectPackageManager() {
35
35
  return 'npm';
36
36
  }
37
37
 
38
- /**
39
- * Decide whether `webjs create` runs the post-scaffold install, per target
40
- * runtime (#682). Node installs by default (it needs `node_modules` to run);
41
- * Bun SKIPS by default (zero-install: `bun run dev` resolves deps on the fly,
42
- * #675). Under Bun zero-install webjs pins a declared dep to its package.json
43
- * version via the #685/#698 onLoad rewrite (a caret range now resolves the
44
- * highest match, not absolute latest), so `bun install` is the path to a frozen
45
- * lockfile, not a correctness fix. Explicit flags win: `--install` forces it
46
- * on, `--no-install` forces it off. The CLI entry points call this and pass an
47
- * explicit boolean to `scaffoldApp`, so the library default (no install unless
48
- * `install: true`) is unchanged for programmatic callers.
49
- *
50
- * @param {{ runtime?: string, explicitInstall?: boolean, noInstall?: boolean }} [o]
51
- * @returns {boolean}
52
- */
53
- export function resolveCreateInstall({ runtime, explicitInstall, noInstall } = {}) {
54
- if (explicitInstall) return true;
55
- if (noInstall) return false;
56
- const isBun = runtime === 'bun' || (!runtime && detectPackageManager() === 'bun');
57
- return !isBun;
58
- }
59
-
60
- /**
61
- * Read the EXACT version of `@webjsdev/<pkg>` the scaffolding CLI ships with.
62
- * `webjsdevRange` carets over this for the generated `package.json` (#700), so a
63
- * fresh app tracks the line the CLI shipped. Walks the `require.resolve` node_modules search paths
64
- * and fs-reads `<pkg>/package.json` directly: `@webjsdev/server` (and `ui`) hide
65
- * `./package.json` behind `exports`, so a bare `require('<pkg>/package.json')`
66
- * fails (same constraint #687 hit). Falls back to `'latest'` when the package is
67
- * not resolvable (defensive; the CLI's own dependency closure is normally
68
- * present), which keeps the scaffold working rather than emitting a bad pin.
69
- * @param {string} pkg e.g. 'cli', 'core', 'server'
70
- * @returns {string} an exact version, or 'latest'
71
- */
72
- function webjsdevVersion(pkg) {
73
- const req = createRequire(import.meta.url);
74
- for (const base of (req.resolve.paths(`@webjsdev/${pkg}`) || [])) {
75
- const pj = join(base, '@webjsdev', pkg, 'package.json');
76
- if (existsSync(pj)) {
77
- try {
78
- const v = JSON.parse(readFileSync(pj, 'utf8')).version;
79
- if (v) return v;
80
- } catch { /* unreadable; keep looking, then fall back */ }
81
- }
82
- }
83
- return 'latest';
84
- }
85
-
86
- /**
87
- * The `@webjsdev/<pkg>` specifier for the generated `package.json` (#700): a
88
- * caret range over the version the scaffolding CLI ships with, so a fresh app
89
- * picks up patch updates the way an npm user expects (a `^0.x` caret stays
90
- * within the minor). Bun zero-install resolves a normal caret range correctly
91
- * since #698, so this no longer diverges from npm. Falls back to `'latest'` when
92
- * the version is not resolvable (keeps `^latest` from ever being emitted).
93
- * @param {string} pkg e.g. 'cli', 'core', 'server'
94
- * @returns {string}
95
- */
96
- function webjsdevRange(pkg) {
97
- const v = webjsdevVersion(pkg);
98
- return v === 'latest' ? 'latest' : '^' + v;
99
- }
100
-
101
- /**
102
- * Third-party dep specifiers the scaffold ships (#700). These are template deps
103
- * the CLI does NOT itself depend on, so they cannot be read from the CLI's
104
- * closure (unlike `@webjsdev/*`). Since #698, Bun zero-install resolves a normal
105
- * caret range correctly (highest match, not absolute latest), so `pg` is an
106
- * idiomatic `^` range. Drizzle is PINNED to an exact RC: its 1.0 line is a
107
- * PRERELEASE (`1.0.0-rc.3`), and Bun zero-install ENOENTs on a caret-prerelease
108
- * inline specifier (`drizzle-orm@^1.0.0-rc.3`, verified on Bun 1.3.14) while the
109
- * exact prerelease resolves, so drizzle must stay exact until the 1.0 stable
110
- * ships. Refresh on a deliberate bump.
111
- */
112
- const SCAFFOLD_DEP_VERSIONS = {
113
- 'drizzle-orm': '1.0.0-rc.3',
114
- 'drizzle-kit': '1.0.0-rc.3',
115
- pg: '^8.22.0',
116
- };
117
-
118
38
  /**
119
39
  * Run `<pm> install` inside the scaffolded app. Returns true on success.
120
40
  * Inherits stdio so the user sees the install progress live. Caller decides
@@ -353,10 +273,6 @@ export async function scaffoldApp(name, cwd, opts = {}) {
353
273
  throw new Error(`Unknown --runtime '${runtime}'. Only ${VALID_RUNTIMES.join(' / ')} are supported.`);
354
274
  }
355
275
  const isBun = runtime === 'bun';
356
- // Zero-install Bun entry (#675): the app-local `webjs-bun.mjs` bootstrap, run
357
- // under `bun --bun`, so the server resolves the CLI + deps via Bun auto-install
358
- // (no `bun install` required). App-local so it is resolvable with no node_modules.
359
- const bunBoot = 'bun --bun webjs-bun.mjs';
360
276
  const appDir = join(cwd, name);
361
277
  if (existsSync(appDir)) {
362
278
  console.error(`Error: directory '${name}' already exists.`);
@@ -402,19 +318,18 @@ export async function scaffoldApp(name, cwd, opts = {}) {
402
318
  // so `npm run start` (a thin alias) behaves identically. Drizzle has no
403
319
  // codegen, so there is no dev `before` step.
404
320
  //
405
- // Bun runtime (#541, zero-install #675): the server + DB scripts run via
406
- // the `webjs-bun.mjs` bootstrap under `bun --bun`. `--bun` overrides the
407
- // `webjs` bin's `#!/usr/bin/env node` shebang (without it `bun run dev`
321
+ // Bun runtime (#541): the long-running server scripts (`dev` / `start`)
322
+ // are prefixed `bun --bun` so the app SERVES on Bun. The `--bun` overrides
323
+ // the `webjs` bin's `#!/usr/bin/env node` shebang (without it `bun run dev`
408
324
  // would exec webjs under Node, silently running the "bun" app on Node).
409
- // Routing through the bootstrap file (which imports the CLI by bare
410
- // specifier) instead of the `webjs` bin means Bun's auto-install resolves
411
- // `@webjsdev/*` and your deps ON DEMAND, so `bun run dev` / `start` work
412
- // with NO `bun install` (install becomes optional, for editor types /
413
- // offline). The runtime-neutral tooling scripts (test / check / typecheck
325
+ // Baking it into the script body means a plain `bun run dev` (or even
326
+ // `npm run dev`) starts on Bun, so a user never has to remember the flag.
327
+ // The runtime-neutral tooling scripts below (test / db / check / typecheck
414
328
  // / doctor) stay plain `webjs ...`: they spawn node tooling (`node --test`,
415
- // tsc), which needs an install, so they are not part of the zero-install path.
416
- dev: isBun ? `${bunBoot} dev` : 'webjs dev',
417
- start: isBun ? `${bunBoot} start` : 'webjs start',
329
+ // drizzle-kit, tsc) and forcing `--bun` there buys nothing (and `webjs
330
+ // test` shells `node --test`, which a `bun --test` would not be).
331
+ dev: isBun ? 'bun --bun webjs dev' : 'webjs dev',
332
+ start: isBun ? 'bun --bun webjs start' : 'webjs start',
418
333
  test: 'webjs test',
419
334
  'test:server': 'webjs test --server',
420
335
  'test:browser': 'webjs test --browser',
@@ -425,31 +340,25 @@ export async function scaffoldApp(name, cwd, opts = {}) {
425
340
  // vendor pins, @webjsdev versions, git hook). Local tool, NOT a CI gate
426
341
  // (its env-drift + network pin-freshness checks would make CI flaky).
427
342
  doctor: 'webjs doctor',
428
- 'db:generate': isBun ? `${bunBoot} db generate` : 'webjs db generate',
429
- 'db:migrate': isBun ? `${bunBoot} db migrate` : 'webjs db migrate',
430
- 'db:push': isBun ? `${bunBoot} db push` : 'webjs db push',
431
- 'db:studio': isBun ? `${bunBoot} db studio` : 'webjs db studio',
432
- 'db:seed': isBun ? `${bunBoot} db seed` : 'webjs db seed',
343
+ 'db:generate': 'webjs db generate',
344
+ 'db:migrate': 'webjs db migrate',
345
+ 'db:push': 'webjs db push',
346
+ 'db:studio': 'webjs db studio',
347
+ 'db:seed': 'webjs db seed',
433
348
  },
434
349
  dependencies: {
435
350
  // Drizzle ORM (no codegen, no engine binary). Pinned to the 1.0 line
436
351
  // for relations v2. SQLite needs NO driver dependency: the connection
437
352
  // uses the built-in node:sqlite (Node) / bun:sqlite (Bun) via Drizzle's
438
353
  // node-sqlite / bun-sqlite adapters. Postgres still needs the pg driver.
439
- // Since #698 a normal caret range resolves correctly under bun
440
- // zero-install, so @webjsdev/* and pg are idiomatic `^` ranges (#700).
441
- // Drizzle stays EXACT: its 1.0 line is a prerelease RC, and bun ENOENTs
442
- // on a caret-prerelease inline specifier, so a range would break it.
443
- 'drizzle-orm': SCAFFOLD_DEP_VERSIONS['drizzle-orm'],
444
- ...(dialect === 'postgres' ? { pg: SCAFFOLD_DEP_VERSIONS.pg } : {}),
445
- '@webjsdev/cli': webjsdevRange('cli'),
446
- '@webjsdev/core': webjsdevRange('core'),
447
- '@webjsdev/server': webjsdevRange('server'),
354
+ 'drizzle-orm': '^1.0.0-rc.3',
355
+ ...(dialect === 'postgres' ? { pg: '^8.13.0' } : {}),
356
+ '@webjsdev/cli': 'latest',
357
+ '@webjsdev/core': 'latest',
358
+ '@webjsdev/server': 'latest',
448
359
  },
449
360
  devDependencies: {
450
- // Exact pin: drizzle-kit shares drizzle-orm's prerelease 1.0 RC, which bun
451
- // cannot resolve as a caret-prerelease range, so it stays exact (#700).
452
- 'drizzle-kit': SCAFFOLD_DEP_VERSIONS['drizzle-kit'],
361
+ 'drizzle-kit': '^1.0.0-rc.3',
453
362
  ...(dialect === 'postgres' ? { '@types/pg': '^8.11.0' } : {}),
454
363
  // The TypeScript compiler, for `npm run typecheck` (webjs typecheck runs
455
364
  // tsc --noEmit). Not needed at runtime (Node strips types in place), only
@@ -487,28 +396,10 @@ export async function scaffoldApp(name, cwd, opts = {}) {
487
396
  webjs: {
488
397
  // Drizzle has no codegen, so there is no dev `before` step. Production
489
398
  // applies pending migrations at boot via `webjs db migrate` (drizzle-kit).
490
- // On Bun this runs through the same zero-install bootstrap as `start`, so
491
- // the boot-time migrate needs no `webjs` bin in node_modules (#675).
492
- start: { before: [isBun ? `${bunBoot} db migrate` : 'webjs db migrate'] },
399
+ start: { before: ['webjs db migrate'] },
493
400
  },
494
401
  }, null, 2) + '\n');
495
402
 
496
- // The zero-install Bun entry (#675). `bun run dev` / `start` invoke this via
497
- // `bun --bun` (see the scripts above). Importing the webjs CLI by bare
498
- // specifier lets Bun auto-install resolve `@webjsdev/*` and your deps on
499
- // demand, so a fresh app serves with NO `bun install`. The CLI reads its
500
- // command (dev / start / db ...) and flags straight from argv. Node apps do
501
- // not get this file; they run the `webjs` bin directly.
502
- if (isBun) {
503
- await writeFile(join(appDir, 'webjs-bun.mjs'),
504
- '// Zero-install Bun entry (webjs #675). Run via `bun --bun webjs-bun.mjs <cmd>`\n' +
505
- '// (the dev / start / db npm scripts do this). Importing the CLI by bare\n' +
506
- '// specifier lets Bun auto-install resolve @webjsdev/* and your deps on\n' +
507
- '// demand, so the app serves with no `bun install` (install stays optional,\n' +
508
- '// for editor types and offline runs). Args pass through to the CLI.\n' +
509
- "await import('@webjsdev/cli/bin/webjs.js');\n");
510
- }
511
-
512
403
  await writeFile(join(appDir, 'tsconfig.json'), JSON.stringify({
513
404
  compilerOptions: {
514
405
  target: 'ES2022',
@@ -1490,13 +1381,6 @@ For AI agents, read this before editing scaffolded files:
1490
1381
  if (!installed) {
1491
1382
  console.log(`\n[warn] ${pm} install failed. Run '${pm} install' manually in ${name}/ to finish setup.\n`);
1492
1383
  }
1493
- } else if (isBun) {
1494
- // Bun zero-install (#675): no install needed; `bun run dev` resolves deps on
1495
- // the fly. Since #698 they resolve to their package.json versions (a caret
1496
- // range to its highest match), so point at `bun install` for a frozen
1497
- // lockfile and editor type intelligence, not a correctness fix.
1498
- console.log(`Skipped install. Bun resolves dependencies on the fly, so 'bun run dev' and 'bun run start' work as-is (no node_modules).`);
1499
- console.log(`These resolve to the versions in package.json. Run 'bun install' in ${name}/ to freeze a lockfile and get editor type intelligence.\n`);
1500
1384
  }
1501
1385
 
1502
1386
  // Next-steps banner prints LAST so the actionable command is the
@@ -1508,10 +1392,7 @@ For AI agents, read this before editing scaffolded files:
1508
1392
  // generate + migrate before the first run (the example User model wants
1509
1393
  // its table to exist). Drizzle splits Prisma's `migrate dev` into
1510
1394
  // `db:generate` (schema to SQL) then `db:migrate` (apply).
1511
- // Omit the install step from the next-steps line for a deliberate Bun
1512
- // zero-install (#682): `bun run dev` resolves deps on the fly, so it works
1513
- // without an install. Otherwise (Node, or an install that did not run) keep it.
1514
- const installSegment = (installed || (isBun && !shouldInstall)) ? '' : `${pm} install && `;
1395
+ const installSegment = installed ? '' : `${pm} install && `;
1515
1396
  const dbSegment = isSaas ? `${pm} run db:generate && ${pm} run db:migrate && ` : '';
1516
1397
  const runCommand = `cd ${name} && ${installSegment}${dbSegment}${pm} run dev`;
1517
1398
  // Use `npx webjsdev ui ...` here, not `npx webjs ui ...`. The bare
@@ -58,10 +58,14 @@ export function bunifyProse(s) {
58
58
  'Dockerfile pins `node:24-alpine` (the same Node major CI uses), installs\ndeps (no build step, since Drizzle has no codegen), and starts via',
59
59
  'Dockerfile is a pure `oven/bun:1` image (no Node, since `webjs db migrate`\nresolves drizzle-kit and runs under Bun with no `npx`, #570), installs deps\nwith `bun install` (no build step, since Drizzle has no codegen), and starts via',
60
60
  )
61
- // The "Running on Bun" section heading is reframed for a bun-flavored app.
62
- // Its body already describes the webjs-bun.mjs bootstrap + zero-install in the
63
- // template (#675), so only the heading needs the runtime reframe.
61
+ // The "Running on Bun" section frames Bun as opt-in ("force it with --bun").
62
+ // In a bun-flavored app the dev/start scripts ALREADY embed --bun, so reframe
63
+ // it as the configured default.
64
64
  .replaceAll('### Running on Bun instead of Node', '### Runtime: this app runs on Bun')
65
+ .replaceAll(
66
+ 'The same `package.json` scripts work on\neither; to run under Bun, force it with `--bun` so the server executes on Bun\nrather than the `webjs` bin\'s Node shebang:',
67
+ 'This app is configured for Bun. Its `dev` / `start` scripts already force\n`--bun` (which overrides the `webjs` bin\'s Node shebang), so a plain `bun run dev`\nserves on Bun. The other scripts (test / db / check) run on Node, the runtime\nthe `webjs` tooling targets:',
68
+ )
65
69
  // Invocation styles first, so "npm create webjs@latest" does not get
66
70
  // mangled by the generic "npm <x>" rules below.
67
71
  .replaceAll('npm create webjs@latest', 'bun create webjs')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webjsdev/cli",
3
- "version": "0.10.27",
3
+ "version": "0.10.28",
4
4
  "type": "module",
5
5
  "description": "webjs CLI - dev, start, create, db",
6
6
  "bin": {
@@ -404,29 +404,15 @@ an npm `prestart` hook.
404
404
 
405
405
  ### Running on Bun instead of Node
406
406
 
407
- webjs runs on **Node 24+ or Bun**. A `--runtime bun` app routes its `dev` /
408
- `start` / `db` scripts through a `webjs-bun.mjs` bootstrap under `bun --bun`
409
- (which overrides the `webjs` bin's Node shebang so the server runs on Bun). The
410
- bootstrap imports the CLI by bare specifier, so Bun auto-install resolves deps on
411
- demand and **no `bun install` is needed**:
407
+ webjs runs on **Node 24+ or Bun**. The same `package.json` scripts work on
408
+ either; to run under Bun, force it with `--bun` so the server executes on Bun
409
+ rather than the `webjs` bin's Node shebang:
412
410
 
413
411
  ```sh
414
- bun run dev # or: bun run start (no install step required)
412
+ bun install
413
+ bun --bun run dev # or: bun --bun run start
415
414
  ```
416
415
 
417
- `bun create` does not run an install on Bun, so a fresh app serves immediately.
418
- Under zero-install, Bun's runtime auto-install resolves a BARE import to LATEST
419
- (it ignores `package.json` and `bun.lock`), so webjs rewrites each declared dep's
420
- specifier to an inline-versioned one via an `onLoad` transform. The version is the
421
- `bun.lock` exact when present, else the `package.json` value when it is an
422
- inline-safe semver (an exact pin, or a caret / tilde / comparator range, which
423
- Bun resolves to the highest match). A protocol range (`workspace:`, `file:`), a
424
- wildcard (`*`), and a dist-tag (`latest`) stay at latest. Run `bun install` when
425
- you want versions frozen identically across machines (it materializes
426
- `node_modules` from the lockfile) or editor type intelligence. (To run a
427
- Node-flavored app on Bun instead, force `bun --bun run dev`, which still expects
428
- an install.)
429
-
430
416
  On Node the `.ts` type-stripping is the built-in `module.stripTypeScriptTypes`;
431
417
  on Bun (which has no built-in) it comes from `amaro` automatically, so the same
432
418
  source serves identically. SSR action-result seeding (an internal hydration
@@ -1,99 +0,0 @@
1
- // Pin `@webjsdev/*` imports under Bun zero-install (#709).
2
- //
3
- // Under Bun zero-install a bare `import('@webjsdev/server')` from the cli (run
4
- // out of the global cache) ENOENTs: Bun's runtime auto-install ignores the cli's
5
- // declared range and flakily fetches latest, which fails. An INLINE-versioned
6
- // specifier (`@webjsdev/server@^0.8.0`) resolves reliably. So we read the version
7
- // the APP declares (the scaffold adds `@webjsdev/*` to its deps) and retry inline
8
- // only when the bare import fails, so Node and installed apps are unaffected.
9
-
10
- import { readFileSync } from 'node:fs';
11
- import { join, dirname } from 'node:path';
12
- import { fileURLToPath } from 'node:url';
13
-
14
- // The cli's OWN package.json (one level up from this lib), used as the fallback
15
- // pin source for `@webjsdev/*` packages the app does not declare (mcp, ui).
16
- const CLI_PKG = join(dirname(fileURLToPath(import.meta.url)), '..', 'package.json');
17
-
18
- /**
19
- * Whether a declared version is a Bun inline-safe specifier: an exact version or
20
- * a single caret / tilde / comparator range, but NOT a range over a prerelease
21
- * (`^1.0.0-rc.3`, which Bun ENOENTs on, #703), a wildcard, a multi-token range,
22
- * or a protocol range (`workspace:` / `file:`).
23
- * @param {unknown} v
24
- * @returns {boolean}
25
- */
26
- export function inlineSafeVersion(v) {
27
- if (typeof v !== 'string') return false;
28
- const m = /^(>=|<=|>|<|=|\^|~)?(\d+(?:\.\d+){0,2})([-+][0-9A-Za-z.-]+)?$/.exec(v);
29
- return !!m && !(m[1] && m[3]);
30
- }
31
-
32
- /**
33
- * The version a `package.json` at `pkgPath` declares for `name`, when it is
34
- * inline-safe; else null.
35
- * @param {string} name
36
- * @param {string} pkgPath absolute path to a package.json
37
- * @returns {string | null}
38
- */
39
- function declaredIn(name, pkgPath) {
40
- try {
41
- const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
42
- const v = { ...pkg.dependencies, ...pkg.devDependencies }[name];
43
- if (inlineSafeVersion(v)) return v;
44
- } catch { /* no readable package.json */ }
45
- return null;
46
- }
47
-
48
- /**
49
- * The inline-safe version to pin `name` to: the app's `package.json` (in `cwd`)
50
- * first (it declares `@webjsdev/server` etc.), else the cli's OWN package.json
51
- * (for `@webjsdev/*` the app does not declare, like `mcp` / `ui`). Null if
52
- * neither has an inline-safe declaration.
53
- * @param {string} name
54
- * @param {string} [cwd]
55
- * @returns {string | null}
56
- */
57
- export function appDeclaredVersion(name, cwd = process.cwd()) {
58
- return declaredIn(name, join(cwd, 'package.json')) || declaredIn(name, CLI_PKG);
59
- }
60
-
61
- /**
62
- * Whether an import error is a resolution / not-found failure (so a version
63
- * retry is warranted), vs a genuine load-time throw from the module's own code
64
- * (which we must NOT retry, to avoid masking it and re-running side effects).
65
- * @param {unknown} err
66
- * @returns {boolean}
67
- */
68
- function isResolutionError(err) {
69
- if (err && /** @type {any} */ (err).code === 'ERR_MODULE_NOT_FOUND') return true;
70
- const msg = err && String(/** @type {any} */ (err).message || err);
71
- // Narrow on purpose: Node sets the code above; Bun's auto-install miss is
72
- // `ENOENT while resolving package '...'`. A broad "cannot find" would match
73
- // ordinary runtime errors ("Cannot find user") and wrongly trigger a retry.
74
- return !!msg && /ENOENT|resolving package|module not found/i.test(msg);
75
- }
76
-
77
- /**
78
- * Import an `@webjsdev/*` module, pinning the version under Bun zero-install. On
79
- * Node or an installed app the bare specifier resolves from `node_modules` (no
80
- * retry). On a RESOLUTION failure (Bun zero-install), retry with the app's (else
81
- * the cli's own) declared version inline. A real load-time throw is rethrown,
82
- * not retried. A subpath (`/check`) is preserved across the rewrite.
83
- * @param {string} spec e.g. `@webjsdev/server` or `@webjsdev/server/check`
84
- * @param {(s: string) => Promise<any>} [importer] injectable for tests
85
- * @returns {Promise<any>}
86
- */
87
- export async function importWebjsdev(spec, importer = (s) => import(s)) {
88
- try {
89
- return await importer(spec);
90
- } catch (err) {
91
- if (!isResolutionError(err)) throw err;
92
- const m = /^(@webjsdev\/[^/]+)(\/.*)?$/.exec(spec);
93
- const pkg = m && m[1];
94
- const sub = (m && m[2]) || '';
95
- const v = pkg && appDeclaredVersion(pkg);
96
- if (v) return await importer(pkg + '@' + v + sub);
97
- throw err;
98
- }
99
- }