@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 +5 -3
- package/lib/dev-reload.js +16 -3
- package/lib/dev-supervisor.js +65 -8
- package/package.json +2 -2
- package/templates/.agents/skills/webjs/references/auth-and-sessions.md +2 -0
- package/templates/.agents/skills/webjs/references/components.md +4 -0
- package/templates/.agents/skills/webjs/references/muscle-memory-gotchas.md +1 -1
- package/templates/.agents/skills/webjs/references/runtime.md +4 -4
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 (
|
|
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),
|
|
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 (
|
|
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.
|
package/lib/dev-supervisor.js
CHANGED
|
@@ -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**
|
|
19
|
-
* a
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
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
|
-
* @
|
|
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:
|
|
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.
|
|
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.
|
|
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`
|
|
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
|
|
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
|
|