@webjsdev/cli 0.10.62 → 0.10.63

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
@@ -7,7 +7,7 @@ import { resolveBin } from '../lib/resolve-bin.js';
7
7
  import { dbGenerateTtyHint } from '../lib/db-hints.js';
8
8
  import { checkNodeInline, nodeInlineMessage } from '../lib/node-preflight.js';
9
9
  import { loadAppEnv, resolvePort, failFastOnPortInUse } from '../lib/port.js';
10
- import { planDevSupervisor } from '../lib/dev-supervisor.js';
10
+ import { planDevSupervisor, readDevSourceLocations, sourceLocationsOn } from '../lib/dev-supervisor.js';
11
11
  import { checkAppName, appNameErrorMessage } from '../lib/app-name.js';
12
12
  import { findCheckTarget, notAnAppMessage, notAnAppJson, findServeTarget, notAnAppServeMessage } from '../lib/check-target.js';
13
13
 
@@ -513,7 +513,7 @@ async function main() {
513
513
  }
514
514
  switch (cmd) {
515
515
  case 'dev': {
516
- // If we're already inside the reload child (node --watch or bun --hot),
516
+ // If we're already inside the reload child (under the supervisor),
517
517
  // start the server directly.
518
518
  if (process.env.__WEBJS_DEV_CHILD === '1') {
519
519
  const { startServer } = await import('@webjsdev/server');
@@ -553,13 +553,15 @@ async function main() {
553
553
 
554
554
  // Decide how to run: in-process (`--no-hot`), or in a child under WebJs's
555
555
  // reload supervisor (#1521), which restarts it on a change on Node and
556
- // runs it under `bun --hot` on Bun (#514), and brings a crashed child
556
+ // runs it under `bun --hot` on Bun (#514), restarting it there only for an
557
+ // edit to a module a Bun.plugin serves (#1550), and brings a crashed child
557
558
  // back on either. The branch logic lives in the pure `planDevSupervisor`
558
559
  // so it is unit-testable without spawning a process.
559
560
  const plan = planDevSupervisor({
560
561
  isBun: !!process.versions.bun,
561
562
  argv: process.argv.slice(1),
562
563
  noHot: rest.includes('--no-hot'),
564
+ sourceLocations: sourceLocationsOn(process.env, readDevSourceLocations(process.cwd())),
563
565
  });
564
566
 
565
567
  if (plan.mode === 'inline') {
package/lib/dev-reload.js CHANGED
@@ -205,11 +205,16 @@ export function watchRestartPaths(cwd, { dirs, files, ignore, onChange, onError,
205
205
  * killTimeoutMs?: number,
206
206
  * finalExitCodes?: number[],
207
207
  * onFinal?: (code: number) => void,
208
+ * restartFor?: (path: string) => boolean,
208
209
  * }} opts
209
210
  */
210
211
  export function createSupervisor({
211
212
  spawnChild,
212
213
  restartOnChange,
214
+ // Which changed paths restart a live child (default: all). On Bun only the
215
+ // paths `bun --hot` cannot reload in place do (#1550); the rest it reloads
216
+ // itself. A dead child is started by any change either way.
217
+ restartFor = () => true,
213
218
  log = () => {},
214
219
  timers = globalThis,
215
220
  now = Date.now,
@@ -230,6 +235,8 @@ export function createSupervisor({
230
235
  let crashes = 0;
231
236
  /** @type {string | null} */
232
237
  let pendingPath = null;
238
+ // Whether any change in the current debounce window asks for a restart.
239
+ let pendingRestart = false;
233
240
  /** @type {any} */ let debounceTimer = null;
234
241
  /** @type {any} */ let backoffTimer = null;
235
242
  /** @type {any} */ let killTimer = null;
@@ -289,7 +296,9 @@ export function createSupervisor({
289
296
  const flush = () => {
290
297
  debounceTimer = null;
291
298
  const path = pendingPath;
299
+ const wantsRestart = pendingRestart;
292
300
  pendingPath = null;
301
+ pendingRestart = false;
293
302
  if (stopping) return;
294
303
  if (!child) {
295
304
  // Not running (crashed, or waiting out a backoff): a change is the cue.
@@ -297,7 +306,7 @@ export function createSupervisor({
297
306
  launch();
298
307
  return;
299
308
  }
300
- if (!restartOnChange || restarting) return;
309
+ if (!restartOnChange || restarting || !wantsRestart) return;
301
310
  restarting = true;
302
311
  if (path) log(`${path} changed, restarting the dev server`);
303
312
  terminate(child);
@@ -308,7 +317,10 @@ export function createSupervisor({
308
317
  /** @param {string} path */
309
318
  change(path) {
310
319
  if (stopping) return;
311
- if (pendingPath === null) pendingPath = path;
320
+ if (restartFor(path)) {
321
+ if (!pendingRestart) pendingPath = path;
322
+ pendingRestart = true;
323
+ } else if (pendingPath === null) pendingPath = path;
312
324
  debounceTimer = clear(debounceTimer);
313
325
  debounceTimer = timers.setTimeout(flush, debounceMs);
314
326
  },
@@ -331,7 +343,7 @@ export function createSupervisor({
331
343
  *
332
344
  * @param {{
333
345
  * cwd: string,
334
- * plan: { args: string[], restartOnChange: boolean, watchDirs: string[], watchFiles: string[] },
346
+ * plan: { args: string[], restartOnChange: boolean, restartFor?: (path: string) => boolean, watchDirs: string[], watchFiles: string[] },
335
347
  * env: NodeJS.ProcessEnv,
336
348
  * onExit: (code: number) => void,
337
349
  * }} opts
@@ -356,6 +368,7 @@ export function superviseDevServer({ cwd, plan, env, onExit }) {
356
368
 
357
369
  const sup = createSupervisor({
358
370
  restartOnChange: plan.restartOnChange,
371
+ ...(plan.restartFor ? { restartFor: plan.restartFor } : {}),
359
372
  log,
360
373
  // The child already printed why (the port and its holder); stop with its
361
374
  // code rather than restarting a server that can never bind.
@@ -15,11 +15,18 @@
15
15
  * vanished mid-scan, #1521) and takes the preview down for good. WebJs's own
16
16
  * supervisor (`lib/dev-reload.js`) watches the same paths with every watcher
17
17
  * error handled, and restarts the child faster.
18
- * - **Bun** keys its module cache by path and IGNORES the `?t=` cache-bust, so
19
- * a restart-per-edit model is not needed: `bun --hot` invalidates loaded
20
- * modules on a file change WITHOUT restarting the process, and `Bun.serve` is
21
- * reused across hot reloads. The parent still supervises it, but only to
22
- * bring a CRASHED child back (`restartOnChange: false`).
18
+ * - **Bun** runs the child under `bun --hot`, which invalidates loaded modules
19
+ * on a file change WITHOUT restarting the process (so a page edit can be
20
+ * refreshed in place, #1398). But `bun --hot` does not watch a file whose
21
+ * contents a `Bun.plugin` `onLoad` returned (#1550), and two plugins serve
22
+ * app modules on Bun: the `'use server'` seed plugin (every `*.server.*`
23
+ * module, always on) and the SSR source-locations plugin (every other app
24
+ * JS/TS module when source locations are on). An edit to one of THOSE
25
+ * restarts the child, like Node; every other edit is left to `bun --hot`.
26
+ * A module the server re-imports directly (a page, an action module) is fresh
27
+ * even before the restart lands, because the dev cache-bust import rides a
28
+ * plain path on Bun (`devImportSpecifier` in `@webjsdev/server`; Bun drops a
29
+ * `file://` specifier's query).
23
30
  *
24
31
  * On both runtimes a child that exits on its own (a crash) is restarted on the
25
32
  * next file change, and after a short backoff even with no change.
@@ -27,6 +34,8 @@
27
34
  * This pure planner returns the spawn decision so the bin stays a thin shell and
28
35
  * the branch logic is unit-testable without spawning a process.
29
36
  */
37
+ import { readFileSync } from 'node:fs';
38
+ import { join } from 'node:path';
30
39
 
31
40
  /** Project directories whose changes restart the dev server on Node. */
32
41
  export const WATCH_DIRS = ['app', 'components', 'modules', 'lib', 'actions'];
@@ -46,14 +55,15 @@ export const WATCH_FILES = ['middleware.ts', 'middleware.js', 'middleware.mts',
46
55
  * @param {boolean} opts.isBun Whether the host runtime is Bun (`process.versions.bun`).
47
56
  * @param {string[]} opts.argv `process.argv.slice(1)` (the script path followed by its args), forwarded to the child verbatim.
48
57
  * @param {boolean} opts.noHot Whether `--no-hot` was passed (opt out of the supervisor entirely).
49
- * @returns {{ mode: 'inline' } | { mode: 'supervise', args: string[], restartOnChange: boolean, watchDirs: string[], watchFiles: string[] }}
58
+ * @param {boolean} [opts.sourceLocations] Whether dev source locations are on (`WEBJS_SOURCE_LOCATIONS` / `webjs.dev.sourceLocations`), which puts every app module behind a Bun plugin.
59
+ * @returns {{ mode: 'inline' } | { mode: 'supervise', args: string[], restartOnChange: boolean, restartFor?: (path: string) => boolean, watchDirs: string[], watchFiles: string[] }}
50
60
  * `inline` runs the server in this process (no reload watcher); `supervise`
51
61
  * spawns `process.execPath` with `args` and `__WEBJS_DEV_CHILD=1` under the
52
62
  * supervisor, which watches `watchDirs` (recursively) and `watchFiles` (at
53
63
  * the app root). The directories need not exist yet: one created later is
54
64
  * picked up.
55
65
  */
56
- export function planDevSupervisor({ isBun, argv, noHot }) {
66
+ export function planDevSupervisor({ isBun, argv, noHot, sourceLocations = false }) {
57
67
  // `--no-hot` opts out of the reload supervisor on either runtime: run the dev
58
68
  // server in THIS process with no watcher. Degraded dev (a deep-import edit
59
69
  // needs a manual restart) but useful under an external process manager or a
@@ -61,6 +71,53 @@ export function planDevSupervisor({ isBun, argv, noHot }) {
61
71
  if (noHot) return { mode: 'inline' };
62
72
 
63
73
  const watch = { watchDirs: [...WATCH_DIRS], watchFiles: [...WATCH_FILES] };
64
- if (isBun) return { mode: 'supervise', args: ['--hot', ...argv], restartOnChange: false, ...watch };
74
+ if (isBun) return { mode: 'supervise', args: ['--hot', ...argv], restartOnChange: true, restartFor: bunPluginServed(sourceLocations), ...watch };
65
75
  return { mode: 'supervise', args: [...argv], restartOnChange: true, ...watch };
66
76
  }
77
+
78
+ const SERVER_MODULE = /\.server\.m?[jt]s$/;
79
+ const APP_MODULE = /\.m?[jt]s$/;
80
+
81
+ /**
82
+ * The changed paths `bun --hot` cannot reload because a `Bun.plugin` serves
83
+ * them (#1550): every `*.server.*` module (the `'use server'` seed plugin), and
84
+ * with source locations on every JS/TS app module (the source-locations
85
+ * plugin). Paths are app-relative, as the supervisor's watcher reports them.
86
+ *
87
+ * @param {boolean} sourceLocations
88
+ * @returns {(path: string) => boolean}
89
+ */
90
+ export function bunPluginServed(sourceLocations) {
91
+ return (path) => SERVER_MODULE.test(path) || (sourceLocations && APP_MODULE.test(path));
92
+ }
93
+
94
+ /**
95
+ * Whether dev source locations are on, the same rule the server applies
96
+ * (`sourceLocationsRequested` in `@webjsdev/server`): `WEBJS_SOURCE_LOCATIONS`
97
+ * set to 1/true or 0/false wins, otherwise `webjs.dev.sourceLocations`.
98
+ *
99
+ * @param {NodeJS.ProcessEnv} env
100
+ * @param {unknown} configured the app's `webjs.dev.sourceLocations`
101
+ * @returns {boolean}
102
+ */
103
+ export function sourceLocationsOn(env, configured) {
104
+ const v = String(env.WEBJS_SOURCE_LOCATIONS || '').trim().toLowerCase();
105
+ if (v === '1' || v === 'true') return true;
106
+ if (v === '0' || v === 'false') return false;
107
+ return configured === true;
108
+ }
109
+
110
+ /**
111
+ * The app's `webjs.dev.sourceLocations` from `<appDir>/package.json`, or
112
+ * undefined when the file or the key is missing or unreadable.
113
+ *
114
+ * @param {string} appDir
115
+ * @returns {unknown}
116
+ */
117
+ export function readDevSourceLocations(appDir) {
118
+ try {
119
+ return JSON.parse(readFileSync(join(appDir, 'package.json'), 'utf8'))?.webjs?.dev?.sourceLocations;
120
+ } catch {
121
+ return undefined;
122
+ }
123
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webjsdev/cli",
3
- "version": "0.10.62",
3
+ "version": "0.10.63",
4
4
  "type": "module",
5
5
  "description": "The CLI for WebJs, a full-stack JavaScript framework built on web components with server-side rendering and no build step. Runs the dev and production servers, scaffolds apps, validates conventions, and drives the database. Node 24+ or Bun.",
6
6
  "bin": {
@@ -18,7 +18,7 @@
18
18
  ],
19
19
  "dependencies": {
20
20
  "@webjsdev/mcp": "^0.1.0",
21
- "@webjsdev/server": "^0.8.73",
21
+ "@webjsdev/server": "^0.8.79",
22
22
  "@webjsdev/ui": "^0.3.15"
23
23
  },
24
24
  "publishConfig": {
@@ -101,6 +101,8 @@ export const GET = handlers.GET;
101
101
  export const POST = handlers.POST;
102
102
  ```
103
103
 
104
+ **Cookie names.** `createAuth` sets `webjs.auth` (session) and, for OAuth, `webjs.auth.state` and `webjs.auth.redirect` in development. In production (`NODE_ENV=production`) each carries the `__Host-` prefix (`__Host-webjs.auth`, ...), which a browser stores only when `Secure`, `Path=/` and without `Domain`, so an app on a sibling subdomain cannot toss its own session cookie into yours. Production reads only the prefixed names; a test or tool that hand-sets the cookie in production must use the prefixed name.
105
+
104
106
  **The no-JS sign-in / sign-out flow is plain forms** (progressive-enhancement-safe). Sign in by POSTing to `/api/auth/signin/credentials` with a hidden `redirectTo`, and read `?error` (mapped from `pages.error`) for feedback; sign out by POSTing to `/api/auth/signout`:
105
107
 
106
108
  ```html
@@ -390,6 +390,10 @@ every hole position (a child, a plain attribute, a `?bool`, a `.prop`). So
390
390
  was previously resolved only in a child hole, which served `open=""` and let
391
391
  hydration close the element a moment later (#1443).
392
392
 
393
+ ## Lazy components: load on first sight
394
+
395
+ `static lazy = true` defers a component's module until an element with its tag is first visible: scrolled within 200px of the viewport, or shown when a `hidden` tab panel or a closed `<dialog>` around it opens. Reach for it for heavy panes and dialogs a first screen does not show (an editor behind a Code tab, a data browser, a settings dialog). It stays lazy when the component that renders it imports it (`import './code-pane.ts'`): the server keeps that import for SSR, and the browser copy of the importer gets an `observeLazy` registration in its place, with no preload for the lazy subtree (#1524). Only a SIDE-EFFECT import defers; a binding import (`import { CodePane } from ...`) stays eager, and one anywhere in the app makes that component eager everywhere. Until the module arrives the element is not upgraded, so a parent calling into it uses optional calls (`this.#pane.value?.save?.()`). The loader scans light DOM only, so do not render a lazy tag inside a shadow root. Browser tests (`webjs test --browser`) get every import as written, so a test that imports a lazy component has it defined at once. A component opened by a window event rather than by becoming visible (a panel that renders nothing until an `open` event) does not fit `static lazy`: dynamically `import()` its module in the code that dispatches the event, then dispatch.
396
+
393
397
  ## Display-only elision
394
398
 
395
399
  A component that does no client-side work renders the same SSR'd HTML with or without its JS, so WebJs strips its import from the served source (and any vendor reachable only through it). This is automatic and conservative. A component stays elidable while it has NONE of:
@@ -206,7 +206,7 @@ The file stays `middleware.ts`, NOT Next 16's renamed `proxy.ts`. WebJs middlewa
206
206
 
207
207
  ### No `<Link>`, no `next/navigation`, no `next/*` libraries
208
208
 
209
- Navigation is automatic. The client router auto-enables when `@webjsdev/core` loads (any page with a component), so a plain `<a href>` gets soft navigation for free. There is no `<Link>` to import and no `useRouter`. For programmatic navigation import `navigate()` / `revalidate()` from `@webjsdev/core`. There is no `next/image`, `next/font`, `next/script`, or `next/dynamic`. WebJs is no-build: use a plain `<img>`, a `<link>` / `@font-face`, a component's `static lazy = true` for viewport lazy-loading, and a dynamic `import()` where code should load lazily.
209
+ Navigation is automatic. The client router auto-enables when `@webjsdev/core` loads (any page with a component), so a plain `<a href>` gets soft navigation for free. There is no `<Link>` to import and no `useRouter`. For programmatic navigation import `navigate()` / `revalidate()` from `@webjsdev/core`. There is no `next/image`, `next/font`, `next/script`, or `next/dynamic`. WebJs is no-build: use a plain `<img>`, a `<link>` / `@font-face`, a component's `static lazy = true` to load it when it is first visible (scrolled near, or its hidden tab panel or dialog opened, and it stays lazy when another component imports it), and a dynamic `import()` where code should load lazily.
210
210
 
211
211
  ### No `<ScrollRestoration>`, and no scroll restore of your own
212
212
 
@@ -32,21 +32,21 @@ Three seams pick a runtime-specific implementation, all inside the framework, no
32
32
  | Listener | `node:http` shell | native `Bun.serve` (faster on the listening path only, not end-to-end, because SSR render dominates a real page) |
33
33
  | TS strip | built-in `module.stripTypeScriptTypes` | `amaro` (byte-identical, position-preserving) |
34
34
  | SQLite | built-in `node:sqlite` + `drizzle-orm/node-sqlite` | built-in `bun:sqlite` + `drizzle-orm/bun-sqlite` |
35
- | Hot reload | restart on change (the `webjs dev` supervisor, #1521) | `bun --hot` |
35
+ | Hot reload | restart on change (the `webjs dev` supervisor, #1521) | `bun --hot`, plus a restart for a `*.server.*` edit and, with source locations on, any app module edit (#1550) |
36
36
  | WebSocket | the `ws` library | native `Bun.serve` + a bridge adapter |
37
37
  | 103 Early Hints | yes | no (`Bun.serve` has no informational-response API) |
38
- | Dev edit to a page / layout | full reload (the dev restart replaces the process) | refreshes IN PLACE, no reload (#1398) |
38
+ | Dev edit to a page / layout | full reload (the dev restart replaces the process) | refreshes IN PLACE, no reload (#1398), unless source locations are on |
39
39
  | Reverse-proxy headers | `X-Forwarded-Proto` / `X-Forwarded-Host` honored | same |
40
40
 
41
41
  **`webjs dev` lets an idle host sleep (#1507).** The live-reload stream sends nothing between edits (no keepalive) and is held open only while a tab showing the app is visible, so a sandbox or preview host that suspends on network quiet can suspend with a backgrounded dev tab open. Showing the tab reconnects, and an edit made meanwhile reloads the page on return. A host that counts an OPEN request as activity (a sandbox that suspends on idle) also needs `"webjs": { "dev": { "reloadIdle": 20 } }` (or `WEBJS_DEV_RELOAD_IDLE=20`): after that many seconds with no edit and no interaction the stream closes, and the next interaction, a tab showing, or an embed-bridge host command (`{ source: 'webjs-embed-host', type: 'resume' }`) reopens it. Off by default. Every reconnect also compares the server's state with the state the page on screen was rendered at, so an edit whose reload signal was lost while the stream was being replaced (a host that closes a held stream when it wakes) still reloads the page.
42
42
 
43
43
  **The in-place dev refresh (#1398) needs the server process to SURVIVE the edit,** which is the whole of the Node-versus-Bun difference in that row. A page or layout never hydrates, so a freshly rendered page is the complete truth for it and the client router can swap it in without a reload, keeping scroll and (for a page edit) the hydrated state of components outside the changed region. The server classifies the changed file and puts the verdict on the live-reload event, so this needs a process that is still alive to do the classifying.
44
44
 
45
- Bun's `bun --hot` invalidates modules in place without restarting, so it gets the refresh. On Node the `webjs dev` supervisor (it replaced `node --watch` in #1521) RESTARTS the server process on a change under `app`, `components`, `modules`, `lib`, or `actions`, or to a root `middleware.{ts,js,mts,mjs}`, and a fresh process holds no record of what changed, so those edits are always a full reload. Two Node cases still refresh in place: an edit OUTSIDE that watched set (`db/schema.server.ts`, a `webjs.dev.watch` content dir), and running `npm run dev -- --no-hot`, which keeps the server in one process on either runtime. An in-place refresh loads the rebuilt stylesheets (a `webjs.dev.regenerate` compile runs on that request) BEFORE it swaps the new markup in, and drops the old sheets only after, so an element that gained a utility class never paints without its rule (#1535; a full reload never had the gap, since a head stylesheet is render-blocking). A component edit is a full reload everywhere by design, because `customElements.define` is once-per-tag and swapping fresh markup onto the old class would be worse than the reload.
45
+ Bun's `bun --hot` invalidates modules in place without restarting, so it gets the refresh. It cannot reload a module a `Bun.plugin` serves, though (#1550): every `'use server'` `*.server.*` module (the action-seeding plugin) and, when dev source locations are on (`WEBJS_SOURCE_LOCATIONS=1`), every app module (the source-locations plugin). The supervisor restarts the server for an edit to one of those, so on Bun with source locations on every app edit is a full reload, as on Node. On Node the `webjs dev` supervisor (it replaced `node --watch` in #1521) RESTARTS the server process on a change under `app`, `components`, `modules`, `lib`, or `actions`, or to a root `middleware.{ts,js,mts,mjs}`, and a fresh process holds no record of what changed, so those edits are always a full reload. Two Node cases still refresh in place: an edit OUTSIDE that watched set (`db/schema.server.ts`, a `webjs.dev.watch` content dir), and running `npm run dev -- --no-hot`, which keeps the server in one process on either runtime. An in-place refresh loads the rebuilt stylesheets (a `webjs.dev.regenerate` compile runs on that request) BEFORE it swaps the new markup in, and drops the old sheets only after, so an element that gained a utility class never paints without its rule (#1535; a full reload never had the gap, since a head stylesheet is render-blocking). A component edit is a full reload everywhere by design, because `customElements.define` is once-per-tag and swapping fresh markup onto the old class would be worse than the reload.
46
46
 
47
47
  **`webjs dev` and `webjs start` serve the directory they are started in, and refuse anywhere else (#1526).** Run them in the app directory, the one holding `app/`. In a workspace (`apps/web` under a root `package.json` with `workspaces`) that is the member, even when the CLI is hoisted to the root `node_modules`: the hoisted bin and the Bun `--hot` child both keep the directory they were started in. Started where there is no `app/` (the workspace root, or a subdirectory such as `app/` itself), both exit 1 before any `before` step runs, naming the app to start (`cd apps/web && webjs dev`), instead of booting a server that answers 404 for every route.
48
48
 
49
- **`webjs dev` does not stay down (#1521).** The supervisor and the server's own watcher handle every watcher error: a file in a watched dir the dev server cannot read or watch (the 0600 temp file `sed -i` creates when another user runs it, a file removed mid-scan) logs one `file watcher skipped <path> (EACCES)` warning and both keep running, where `node --watch` used to crash and leave the preview dead. Edits that REPLACE a file (`sed -i`, an editor saving through a temp file, an atomic write) are heard every time, however often the same file is replaced (#1529: on Linux under Node the watchers watch each directory, since Node 24's own recursive watcher stopped hearing a file after its first replacement). A server process that crashes is started again on the next file change, or by itself after a backoff of 0.5s growing to 10s for repeated crashes. On Bun the supervisor does only the crash recovery, since `bun --hot` reloads edits in place. Stopping `webjs dev` (Ctrl-C, SIGTERM) stops the server child too, and a child whose supervisor was killed outright exits on its own, so nothing is left holding the port.
49
+ **`webjs dev` does not stay down (#1521).** The supervisor and the server's own watcher handle every watcher error: a file in a watched dir the dev server cannot read or watch (the 0600 temp file `sed -i` creates when another user runs it, a file removed mid-scan) logs one `file watcher skipped <path> (EACCES)` warning and both keep running, where `node --watch` used to crash and leave the preview dead. Edits that REPLACE a file (`sed -i`, an editor saving through a temp file, an atomic write) are heard every time, however often the same file is replaced (#1529: on Linux under Node the watchers watch each directory, since Node 24's own recursive watcher stopped hearing a file after its first replacement). A server process that crashes is started again on the next file change, or by itself after a backoff of 0.5s growing to 10s for repeated crashes. On Bun the supervisor restarts the server only for an edit `bun --hot` cannot reload in place (a `*.server.*` module, or with source locations on any app module, #1550), and otherwise does only the crash recovery. Stopping `webjs dev` (Ctrl-C, SIGTERM) stops the server child too, and a child whose supervisor was killed outright exits on its own, so nothing is left holding the port.
50
50
 
51
51
  The 103 Early Hints gap costs only a small first-load latency edge where an edge proxy forwards the 103, never correctness. The `modulepreload` hints still ship in the document head on both runtimes.
52
52