@webjsdev/cli 0.10.16 → 0.10.17

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/bin/webjs.js CHANGED
@@ -4,6 +4,7 @@ import { spawn } from 'node:child_process';
4
4
  import { fileURLToPath } from 'node:url';
5
5
  import { checkNodeInline, nodeInlineMessage } from '../lib/node-preflight.js';
6
6
  import { loadAppEnv, resolvePort } from '../lib/port.js';
7
+ import { planDevSupervisor } from '../lib/dev-supervisor.js';
7
8
 
8
9
  const __dirname = dirname(fileURLToPath(import.meta.url));
9
10
  const [cmd, ...rest] = process.argv.slice(2);
@@ -39,7 +40,8 @@ if (cmd !== 'help' && cmd !== undefined) {
39
40
  const TEMPLATES = ['full-stack', 'api', 'saas'];
40
41
 
41
42
  const USAGE = `webjs commands:
42
- webjs dev [--port 8080] Start dev server with live reload
43
+ webjs dev [--port 8080] [--no-hot] Start dev server with live reload
44
+ (--no-hot: run in-process, no hot-reload supervisor)
43
45
  webjs start [--port 8080] Start production server (serves source directly, no build step)
44
46
  webjs test [--server|--browser] Run server + browser tests
45
47
  webjs check [--json] Run correctness checks on the app (--json emits structured violations)
@@ -83,7 +85,8 @@ async function main() {
83
85
  }
84
86
  switch (cmd) {
85
87
  case 'dev': {
86
- // If we're already inside the --watch child, start the server directly.
88
+ // If we're already inside the reload child (node --watch or bun --hot),
89
+ // start the server directly.
87
90
  if (process.env.__WEBJS_DEV_CHILD === '1') {
88
91
  const { startServer } = await import('@webjsdev/server');
89
92
  // Load `.env` BEFORE resolving the port so a `PORT` set there is in
@@ -104,36 +107,31 @@ async function main() {
104
107
  const hint = prismaDevHint(process.cwd());
105
108
  if (hint) console.error(hint);
106
109
 
107
- // Otherwise, spawn ourselves as a child with node --watch.
108
- // This restarts the process on file changes, guaranteeing a fresh
109
- // Node ESM module cache. Without this, edits to transitively-imported
110
- // modules (actions, queries, components, utils) don't take effect
111
- // because Node caches ESM by URL with no public invalidation API.
112
- // Build watch paths from directories that exist in the project.
110
+ // Decide how to run: in-process (`--no-hot`), or re-exec'd under the host
111
+ // runtime's hot-reload supervisor (`node --watch` on Node, `bun --hot` on
112
+ // Bun, #514). The branch logic lives in the pure `planDevSupervisor` so it
113
+ // is unit-testable without spawning a process.
113
114
  const { existsSync } = await import('node:fs');
114
- const watchPaths = [];
115
- for (const dir of ['app', 'components', 'modules', 'lib', 'actions']) {
116
- if (existsSync(dir)) watchPaths.push('--watch-path', dir);
117
- }
118
- // Watch root middleware/config if present
119
- for (const f of ['middleware.ts', 'middleware.js']) {
120
- if (existsSync(f)) watchPaths.push('--watch-path', f);
115
+ const plan = planDevSupervisor({
116
+ isBun: !!process.versions.bun,
117
+ argv: process.argv.slice(1),
118
+ noHot: rest.includes('--no-hot'),
119
+ exists: (p) => existsSync(p),
120
+ });
121
+
122
+ if (plan.mode === 'inline') {
123
+ const { startServer } = await import('@webjsdev/server');
124
+ loadAppEnv(process.cwd());
125
+ const port = resolvePort(flag(rest, '--port'));
126
+ await startServer({ appDir: process.cwd(), port, dev: true });
127
+ break;
121
128
  }
122
129
 
123
- const child = spawn(
124
- process.execPath,
125
- [
126
- '--watch',
127
- '--watch-preserve-output',
128
- ...watchPaths,
129
- ...process.argv.slice(1),
130
- ],
131
- {
132
- stdio: 'inherit',
133
- cwd: process.cwd(),
134
- env: { ...process.env, __WEBJS_DEV_CHILD: '1' },
135
- },
136
- );
130
+ const child = spawn(process.execPath, plan.args, {
131
+ stdio: 'inherit',
132
+ cwd: process.cwd(),
133
+ env: { ...process.env, __WEBJS_DEV_CHILD: '1' },
134
+ });
137
135
  child.on('exit', (code) => process.exit(code ?? 0));
138
136
  break;
139
137
  }
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Dev-server reload supervisor planning for `webjs dev` (issue #514).
3
+ *
4
+ * `webjs dev` re-execs itself under the host runtime's hot-reload supervisor so
5
+ * an edit to a transitively-imported module (an action, query, component, util)
6
+ * takes effect without a manual restart. Both runtimes cache ES modules by
7
+ * resolved URL with no public invalidation API, so the dev re-import in
8
+ * `@webjsdev/server`'s `dev.js` relies on the runtime's own file-watching cache
9
+ * invalidation:
10
+ *
11
+ * - **Node** has no in-place module-cache eviction, so it re-execs under
12
+ * `node --watch`, which RESTARTS the process on a file change (a fresh ESM
13
+ * cache each time). The dev re-import additionally appends a `?t=` cache-bust
14
+ * query that Node honours between restarts.
15
+ * - **Bun** keys its module cache by path and IGNORES that `?t=` query, so the
16
+ * `node --watch` model does not transfer: without help a re-imported module
17
+ * stays STALE on Bun (the #514 bug). Bun's `--hot` invalidates loaded modules
18
+ * on a file change WITHOUT restarting the process, which is exactly what the
19
+ * dev re-import needs; `Bun.serve` is reused across hot reloads, so the
20
+ * listener is not duplicated. `--hot` auto-watches every loaded file, so the
21
+ * node `--watch-path` flags do not apply (and are not Bun flags).
22
+ *
23
+ * This pure planner returns the spawn decision so the bin stays a thin shell and
24
+ * the branch logic is unit-testable without spawning a process.
25
+ */
26
+
27
+ /**
28
+ * Plan how `webjs dev` runs its server.
29
+ *
30
+ * @param {object} opts
31
+ * @param {boolean} opts.isBun Whether the host runtime is Bun (`process.versions.bun`).
32
+ * @param {string[]} opts.argv `process.argv.slice(1)` (the script path followed by its args), forwarded to the child verbatim.
33
+ * @param {boolean} opts.noHot Whether `--no-hot` was passed (opt out of the supervisor entirely).
34
+ * @param {(path: string) => boolean} opts.exists Existence check for the Node `--watch-path` targets (relative to cwd). Unused on Bun.
35
+ * @returns {{ mode: 'inline' } | { mode: 'spawn', args: string[] }}
36
+ * `inline` runs the server in this process (no reload watcher); `spawn`
37
+ * re-execs `process.execPath` with `args` and `__WEBJS_DEV_CHILD=1`.
38
+ */
39
+ export function planDevSupervisor({ isBun, argv, noHot, exists }) {
40
+ // `--no-hot` opts out of the reload supervisor on either runtime: run the dev
41
+ // server in THIS process with no watcher. Degraded dev (a deep-import edit
42
+ // needs a manual restart) but useful under an external process manager or a
43
+ // debugger that wants a single, un-re-exec'd process.
44
+ if (noHot) return { mode: 'inline' };
45
+
46
+ if (isBun) return { mode: 'spawn', args: ['--hot', ...argv] };
47
+
48
+ // Node: re-exec under `node --watch`, watching the project dirs/files that
49
+ // exist. `--watch-preserve-output` keeps prior logs across a restart.
50
+ const watchPaths = [];
51
+ for (const dir of ['app', 'components', 'modules', 'lib', 'actions']) {
52
+ if (exists(dir)) watchPaths.push('--watch-path', dir);
53
+ }
54
+ for (const f of ['middleware.ts', 'middleware.js']) {
55
+ if (exists(f)) watchPaths.push('--watch-path', f);
56
+ }
57
+ return {
58
+ mode: 'spawn',
59
+ args: ['--watch', '--watch-preserve-output', ...watchPaths, ...argv],
60
+ };
61
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webjsdev/cli",
3
- "version": "0.10.16",
3
+ "version": "0.10.17",
4
4
  "type": "module",
5
5
  "description": "webjs CLI - dev, start, create, db",
6
6
  "bin": {