@webjsdev/server 0.8.91 → 0.10.0
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 +4 -1
- package/index.d.ts +461 -102
- package/index.js +9 -4
- package/package.json +3 -3
- package/src/action-flash.js +185 -0
- package/src/action-input.js +208 -0
- package/src/action-params.js +156 -0
- package/src/action-route.js +8 -9
- package/src/action-seed.js +44 -5
- package/src/actions.js +112 -15
- package/src/api.js +67 -10
- package/src/app-layout.js +133 -0
- package/src/asset-hash.js +82 -49
- package/src/auth.js +612 -108
- package/src/browser-entries.js +2 -17
- package/src/build-epoch.js +1 -1
- package/src/cache-tags.js +5 -1
- package/src/check/helpers.js +85 -0
- package/src/check/rules-actions.js +37 -1
- package/src/check/rules-components.js +133 -28
- package/src/check/rules-imports.js +175 -4
- package/src/check/rules-route-modules.js +200 -0
- package/src/check/rules-routes.js +55 -0
- package/src/check/rules-routing.js +143 -95
- package/src/check/rules-typescript.js +24 -2
- package/src/check/rules.js +109 -9
- package/src/check/runner-support.js +53 -171
- package/src/check/runner.js +24 -6
- package/src/client-signals.js +171 -0
- package/src/component-scanner.js +22 -16
- package/src/context.js +152 -16
- package/src/cookie-store.js +262 -0
- package/src/core-exports.js +190 -0
- package/src/dev/bun-app-source.js +95 -5
- package/src/dev/config.js +7 -51
- package/src/dev/dev-service-worker.js +48 -0
- package/src/dev/dev-status.js +122 -0
- package/src/dev/esm-hot-hooks.js +373 -0
- package/src/dev/handler.js +632 -155
- package/src/dev/helpers.js +127 -96
- package/src/dev/hot-host.js +33 -6
- package/src/dev/poke-state.js +110 -0
- package/src/dev/serve.js +191 -133
- package/src/dev/server.js +109 -37
- package/src/dev/specifier-versions.js +125 -0
- package/src/dev/watch-ignore.js +49 -4
- package/src/dev/watch-poll.js +78 -0
- package/src/dev/watch-tree.js +59 -22
- package/src/dev-classify.js +59 -26
- package/src/dev-error.js +104 -3
- package/src/dev-hot-classify.js +291 -0
- package/src/dev-hot-client.js +245 -0
- package/src/dev-import.js +22 -5
- package/src/dev-overlay.js +26 -0
- package/src/dev-ready-probe.js +69 -0
- package/src/dev-regenerate.js +322 -78
- package/src/dev-reload-hold.js +49 -20
- package/src/dev-reload-reason.js +61 -0
- package/src/dev-reload-worker.js +62 -13
- package/src/dev-secret.js +73 -0
- package/src/dev-source-locations-hook.js +4 -1
- package/src/dev-source-locations.js +33 -2
- package/src/dev-styles.js +64 -6
- package/src/dev.js +0 -1
- package/src/env-files.js +124 -0
- package/src/file-storage.js +2 -2
- package/src/foreign-imports.js +228 -0
- package/src/form-action-identity.js +52 -25
- package/src/form-dispatch.js +150 -69
- package/src/html-cache.js +35 -4
- package/src/importmap.js +2 -2
- package/src/instrumentation.js +59 -3
- package/src/js-scan.js +5 -4
- package/src/jwe.js +155 -0
- package/src/lazy-components.js +166 -0
- package/src/listener-core.js +4 -2
- package/src/lit-hints.js +172 -0
- package/src/middleware-load.js +248 -0
- package/src/module-graph.js +73 -47
- package/src/module-side-effect.js +284 -0
- package/src/public-files.js +73 -0
- package/src/redirect-location.js +19 -0
- package/src/redirects.js +9 -1
- package/src/route-frontier.js +151 -0
- package/src/route-report.js +130 -0
- package/src/router.js +120 -13
- package/src/session.js +32 -10
- package/src/ssr/client-router-flag.js +1 -1
- package/src/ssr/document.js +49 -17
- package/src/ssr/head.js +228 -25
- package/src/ssr/preloads.js +31 -45
- package/src/ssr/render.js +243 -55
- package/src/ssr/responses.js +51 -4
- package/src/ssr/shipped-modules.js +52 -16
- package/src/testing.d.ts +31 -2
- package/src/testing.js +85 -17
- package/src/thenable-params.js +26 -0
- package/src/vendor/jspm.js +12 -1
- package/src/vendor/pins.js +9 -8
- package/src/vendor/scanner.js +14 -9
- package/src/vendor.js +5 -3
- package/webjs-config.schema.json +3 -8
- package/src/component-elision.js +0 -1852
- package/src/elision-differential.js +0 -101
- package/src/elision-report.js +0 -193
package/src/dev/helpers.js
CHANGED
|
@@ -203,8 +203,8 @@ export const MIME = {
|
|
|
203
203
|
* The transformed bytes are cached per request handler in `state.tsCache`
|
|
204
204
|
* (a `Map<string, { mtimeMs, code, map }>`), bounded to `TS_CACHE_MAX`
|
|
205
205
|
* entries. The cache is per-handler rather than module-global because the
|
|
206
|
-
* cached code bakes in that handler's
|
|
207
|
-
* the same app
|
|
206
|
+
* cached code bakes in that handler's lazy-component set, so two handlers for
|
|
207
|
+
* the same app at different moments must not share it.
|
|
208
208
|
*/
|
|
209
209
|
export const TS_CACHE_MAX = 500;
|
|
210
210
|
|
|
@@ -245,42 +245,6 @@ export function shouldAccessLog(pathname) {
|
|
|
245
245
|
return !pathname.startsWith('/__webjs/');
|
|
246
246
|
}
|
|
247
247
|
|
|
248
|
-
/**
|
|
249
|
-
* Auto-load `<appDir>/.env` into `process.env` once at boot. Mirrors
|
|
250
|
-
* what Rails / Next / Astro do out of the box: a scaffolded app with
|
|
251
|
-
* a committed `.env.example` and a developer-copied `.env` should
|
|
252
|
-
* "just work" without the user having to add a dotenv import or set
|
|
253
|
-
* the file path on the CLI.
|
|
254
|
-
*
|
|
255
|
-
* Uses Node 24+'s built-in `process.loadEnvFile`, which is dotenv-
|
|
256
|
-
* compatible and DOES NOT override pre-existing `process.env` values.
|
|
257
|
-
* Calls that hit a missing file or parse error are silenced; the
|
|
258
|
-
* server should still come up cleanly when there's no `.env`.
|
|
259
|
-
*
|
|
260
|
-
* Idempotent: re-running is a no-op for any env var the user already
|
|
261
|
-
* exported (e.g. via the host shell or a process manager). That
|
|
262
|
-
* keeps the "shell-set wins over file" precedence Rails users
|
|
263
|
-
* expect.
|
|
264
|
-
*
|
|
265
|
-
* Must run before any server-only module is loaded by
|
|
266
|
-
* buildActionIndex, since module-init code in `lib/*.server.ts`
|
|
267
|
-
* (e.g. `createAuth({ secret: process.env.AUTH_SECRET })`) reads
|
|
268
|
-
* process.env at import time. createRequestHandler is the
|
|
269
|
-
* single entry point where this is guaranteed.
|
|
270
|
-
*
|
|
271
|
-
* @param {string} appDir
|
|
272
|
-
*/
|
|
273
|
-
export function loadAppEnv(appDir) {
|
|
274
|
-
try {
|
|
275
|
-
if (typeof process.loadEnvFile === 'function') {
|
|
276
|
-
process.loadEnvFile(join(appDir, '.env'));
|
|
277
|
-
}
|
|
278
|
-
} catch {
|
|
279
|
-
// No .env file, malformed file, or Node version without
|
|
280
|
-
// loadEnvFile. Either way, fall through silently: the user
|
|
281
|
-
// may not need any env vars, or they may set them via shell.
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
248
|
|
|
285
249
|
/**
|
|
286
250
|
* Walk the route table + component scanner to collect every file the
|
|
@@ -317,25 +281,6 @@ export function loadAppEnv(appDir) {
|
|
|
317
281
|
* @param {string} appDir
|
|
318
282
|
* @returns {Set<string>}
|
|
319
283
|
*/
|
|
320
|
-
/**
|
|
321
|
-
* Collect every page + layout file across the route table. These are the
|
|
322
|
-
* modules the client boot script imports, and thus the candidates for
|
|
323
|
-
* inert-route elision (dropping a module that does no client work).
|
|
324
|
-
* `route.{js,ts}` / middleware / metadata are excluded: they never ship.
|
|
325
|
-
*
|
|
326
|
-
* @param {Awaited<ReturnType<typeof buildRouteTable>>} routeTable
|
|
327
|
-
* @returns {string[]}
|
|
328
|
-
*/
|
|
329
|
-
export function collectRouteModules(routeTable) {
|
|
330
|
-
/** @type {Set<string>} */
|
|
331
|
-
const mods = new Set();
|
|
332
|
-
for (const page of routeTable.pages || []) {
|
|
333
|
-
if (page.file) mods.add(page.file);
|
|
334
|
-
for (const f of page.layouts || []) mods.add(f);
|
|
335
|
-
}
|
|
336
|
-
return [...mods];
|
|
337
|
-
}
|
|
338
|
-
|
|
339
284
|
/**
|
|
340
285
|
* List the app's top-level source directory names, for expanding a `#*`
|
|
341
286
|
* catch-all import alias into one browser importmap prefix scope per dir (#555).
|
|
@@ -421,6 +366,21 @@ const RELOAD_WORKER_SRC = readFileSync(new URL('../dev-reload-worker.js', import
|
|
|
421
366
|
const DEV_STYLES_SRC = readFileSync(new URL('../dev-styles.js', import.meta.url), 'utf8')
|
|
422
367
|
.replace(/^export /gm, '');
|
|
423
368
|
|
|
369
|
+
// The component hot swap (#1689), same shared-source rule. It installs the
|
|
370
|
+
// template interner and the class patcher core reads, so the reload client
|
|
371
|
+
// must evaluate before the page's boot script (`ssr/head.js` orders them).
|
|
372
|
+
const DEV_HOT_CLIENT_SRC = readFileSync(new URL('../dev-hot-client.js', import.meta.url), 'utf8')
|
|
373
|
+
.replace(/^export /gm, '');
|
|
374
|
+
|
|
375
|
+
// The readiness probe every reload waits on (#893, #1689), same shared-source
|
|
376
|
+
// rule.
|
|
377
|
+
const DEV_READY_PROBE_SRC = readFileSync(new URL('../dev-ready-probe.js', import.meta.url), 'utf8')
|
|
378
|
+
.replace(/^export /gm, '');
|
|
379
|
+
|
|
380
|
+
// Why a full reload happened, printed after it (#1689), same shared-source rule.
|
|
381
|
+
const DEV_RELOAD_REASON_SRC = readFileSync(new URL('../dev-reload-reason.js', import.meta.url), 'utf8')
|
|
382
|
+
.replace(/^export /gm, '');
|
|
383
|
+
|
|
424
384
|
// The embed host's reload hold (#1532), same shared-source rule.
|
|
425
385
|
const DEV_RELOAD_HOLD_SRC = readFileSync(new URL('../dev-reload-hold.js', import.meta.url), 'utf8')
|
|
426
386
|
.replace(/^export /gm, '');
|
|
@@ -481,10 +441,20 @@ export function reloadClientJs(bp) {
|
|
|
481
441
|
const workerUrl = JSON.stringify(withBasePath('/__webjs/reload-worker.js', bp));
|
|
482
442
|
const versionUrl = JSON.stringify(withBasePath('/__webjs/version', bp));
|
|
483
443
|
return `// webjs dev reload client
|
|
444
|
+
${DEV_HOT_CLIENT_SRC}
|
|
445
|
+
var __webjsHot = installHotClient(globalThis);
|
|
484
446
|
${DEV_OVERLAY_SRC}
|
|
485
447
|
${RELOAD_WORKER_SRC}
|
|
486
448
|
${DEV_STYLES_SRC}
|
|
487
449
|
${DEV_RELOAD_HOLD_SRC}
|
|
450
|
+
${DEV_READY_PROBE_SRC}
|
|
451
|
+
${DEV_RELOAD_REASON_SRC}
|
|
452
|
+
// Every full reload goes through here, so the reloaded page can say why
|
|
453
|
+
// (#1689); the reason a previous page stored is printed once, now.
|
|
454
|
+
function __webjsStore() { try { return globalThis.sessionStorage || null; } catch (_) { return null; } }
|
|
455
|
+
function __webjsInfo(msg) { console.info(msg); }
|
|
456
|
+
function __webjsReload(verdict) { reloadWithReason(verdict, location, __webjsStore(), __webjsInfo); }
|
|
457
|
+
printPendingReason(__webjsStore(), __webjsInfo);
|
|
488
458
|
// A framing host holds reloads while its agent builds (#1532): the embed bridge
|
|
489
459
|
// sets __webjsEmbedHold, every signal below is kept (strongest verdict wins),
|
|
490
460
|
// and releasing the hold applies that one reload, or none if nothing changed.
|
|
@@ -499,54 +469,66 @@ function __webjsApplyError(data) {
|
|
|
499
469
|
// frame that arrived before the URL advanced goes up once it does. The gate
|
|
500
470
|
// itself lives inside renderDevOverlay, so __webjsApplyError needs no change.
|
|
501
471
|
installDevOverlayNavSync();
|
|
502
|
-
// Never reload INTO a server that is still restarting (
|
|
503
|
-
//
|
|
504
|
-
//
|
|
505
|
-
//
|
|
506
|
-
//
|
|
507
|
-
//
|
|
508
|
-
//
|
|
509
|
-
//
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
}
|
|
517
|
-
function again() { if (++tries > 100) location.reload(); else setTimeout(attempt, 100); }
|
|
518
|
-
attempt();
|
|
472
|
+
// Never reload INTO a server that is still restarting (a restart file's edit,
|
|
473
|
+
// a crash), which would paint a style-less, half-rendered page (#893). Probe
|
|
474
|
+
// the lightweight /__webjs/version endpoint until it answers, THEN apply.
|
|
475
|
+
// Under an in-process reload the server is up, so the first probe passes and
|
|
476
|
+
// the apply is instant; under a restart the old page stays put until the new
|
|
477
|
+
// server is ready, however long that takes (#1689: it used to give up after
|
|
478
|
+
// about 10 seconds and reload into the browser's error page). One probe loop
|
|
479
|
+
// at a time, so a burst of signals during a restart joins it.
|
|
480
|
+
var __webjsWhenReady = createReadyProbe(function (u, o) { return fetch(u, o); }, ${versionUrl});
|
|
481
|
+
// A relay message's verdict as an object (#1689). A SharedWorker another tab
|
|
482
|
+
// started before an upgrade can still be alive and posts the bare verdict name,
|
|
483
|
+
// so a string becomes { v }, and anything unreadable is a full reload.
|
|
484
|
+
function __webjsVerdictOf(v) {
|
|
485
|
+
if (typeof v === 'string') return { v: v, by: '', why: '', modules: [], css: false };
|
|
486
|
+
return v && typeof v === 'object' ? v : { v: 'reload', by: '', why: 'unreadable-frame', modules: [], css: false };
|
|
519
487
|
}
|
|
520
488
|
// Apply a reload signal at the lightest correct weight (#1398). A page or
|
|
521
489
|
// layout edit never changes browser-bound JS, so the server's re-render is the
|
|
522
490
|
// complete truth and the client router can swap it in place: scroll and (for a
|
|
523
491
|
// page edit) the hydrated state of components outside the changed region
|
|
524
|
-
// survive.
|
|
525
|
-
//
|
|
492
|
+
// survive. A \`component\` verdict (#1689) re-imports the versioned component
|
|
493
|
+
// modules it lists, which patch the registered classes in place, so the live
|
|
494
|
+
// elements keep their state. A \`css\` verdict re-requests the stylesheets and
|
|
495
|
+
// swaps them in, with no re-render at all. Anything else, including every signal
|
|
496
|
+
// the server could not classify, is the full reload this always was.
|
|
526
497
|
function __webjsApplyReload(verdict) {
|
|
527
498
|
if (__webjsHold.offer(verdict)) return;
|
|
528
499
|
__webjsWhenReady(function () {
|
|
500
|
+
var mods = verdict.modules || [];
|
|
501
|
+
if (verdict.v !== 'reload' && mods.length) { __webjsApplyHot(verdict, mods); return; }
|
|
502
|
+
if (verdict.v === 'css') {
|
|
503
|
+
// Every same-origin sheet, not just the edited file, so an @import-ed
|
|
504
|
+
// partial and a webjs.dev.regenerate output (Tailwind) come out right.
|
|
505
|
+
// The clone-swap keeps the working sheet when a re-request fails.
|
|
506
|
+
preloadStyles().then(function (styles) {
|
|
507
|
+
styles.commit();
|
|
508
|
+
console.info('[webjs] stylesheets refreshed' + (verdict.by ? ' (' + verdict.by + ')' : ''));
|
|
509
|
+
}).then(null, function () { __webjsReload({ why: 'styles-failed', by: verdict.by }); });
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
529
512
|
// Runtime feature detection, never an assumption. The refresh entry is
|
|
530
513
|
// published by enableClientRouter and removed by disableClientRouter, so its
|
|
531
514
|
// ABSENCE covers both no-router cases at once: an app that opted out with
|
|
532
515
|
// \`webjs.clientRouter: false\`, and a page that ships no component at all so
|
|
533
516
|
// @webjsdev/core never loads in the browser and the module never runs.
|
|
534
517
|
var refresh = globalThis.__webjsRefreshPage;
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
//
|
|
538
|
-
//
|
|
539
|
-
//
|
|
540
|
-
//
|
|
518
|
+
var mode = verdict.v;
|
|
519
|
+
if ((mode === 'page' || mode === 'shell') && typeof refresh === 'function') {
|
|
520
|
+
// Take any error overlay down BEFORE re-rendering. The overlay's own
|
|
521
|
+
// teardown is keyed on the URL CHANGING, which a same-url refresh never
|
|
522
|
+
// does, so a refresh has no exit for a live overlay the way a full reload
|
|
523
|
+
// did (it replaced the document).
|
|
541
524
|
//
|
|
542
|
-
//
|
|
543
|
-
//
|
|
544
|
-
//
|
|
545
|
-
//
|
|
546
|
-
//
|
|
547
|
-
//
|
|
548
|
-
//
|
|
549
|
-
// which can coexist with a page that still renders and so still refreshes.
|
|
525
|
+
// A page-render error is the main case (#1689). Its response is marked
|
|
526
|
+
// \`x-webjs-dev-error\`, so the refresh that hits it leaves the last good
|
|
527
|
+
// page on screen under the overlay instead of applying the error page,
|
|
528
|
+
// and the edit that FIXES it refreshes in place, which is when this
|
|
529
|
+
// takes the overlay down. An UNSCOPED frame (a \`rebuild\` or
|
|
530
|
+
// \`ts-strip\` failure), which the nav sync deliberately never clears, is
|
|
531
|
+
// the other.
|
|
550
532
|
//
|
|
551
533
|
// Before rather than after, so the ordering is self-correcting: if this
|
|
552
534
|
// render also fails, the server pushes a fresh frame DURING it and the
|
|
@@ -558,16 +540,65 @@ function __webjsApplyReload(verdict) {
|
|
|
558
540
|
// seconds, and markup swapped in before it lands paints a new class with
|
|
559
541
|
// no rule. The old sheets stay applied until the swap, then go.
|
|
560
542
|
preloadStyles().then(function (styles) {
|
|
561
|
-
return refresh(
|
|
562
|
-
if (!
|
|
543
|
+
return refresh(mode).then(function (outcome) {
|
|
544
|
+
if (!outcome || outcome === 'failed') { __webjsReload({ why: 'refresh-failed', by: verdict.by }); return; }
|
|
563
545
|
styles.commit();
|
|
546
|
+
__webjsLogRefresh(outcome, mode, verdict.by);
|
|
564
547
|
});
|
|
565
|
-
}).then(null, function () {
|
|
548
|
+
}).then(null, function () { __webjsReload({ why: 'refresh-failed', by: verdict.by }); });
|
|
566
549
|
return;
|
|
567
550
|
}
|
|
568
|
-
|
|
551
|
+
// The reloaded page says why (#1689): the server's reason (a component
|
|
552
|
+
// edit the hot swap refused names its guard, such as constructor-changed),
|
|
553
|
+
// or that this page has no client router to apply a refresh with.
|
|
554
|
+
__webjsReload(mode === 'page' || mode === 'shell' ? { why: 'no-client-router', by: verdict.by } : verdict);
|
|
569
555
|
});
|
|
570
556
|
}
|
|
557
|
+
// Apply a verdict that carries component modules (#1689): load the rebuilt
|
|
558
|
+
// stylesheets, re-import the modules (each re-define patches its registered
|
|
559
|
+
// class and re-renders the live elements), then re-render the page for a
|
|
560
|
+
// \`page\` / \`shell\` verdict in the same batch, then swap the sheets. A
|
|
561
|
+
// refused swap (a guard the hot client checks, a module that fails to load)
|
|
562
|
+
// reloads and says why.
|
|
563
|
+
function __webjsApplyHot(verdict, mods) {
|
|
564
|
+
var refresh = globalThis.__webjsRefreshPage;
|
|
565
|
+
var mode = verdict.v;
|
|
566
|
+
var by = verdict.by ? ' (' + verdict.by + ')' : '';
|
|
567
|
+
function reload(why) { __webjsReload({ why: why, by: verdict.by }); }
|
|
568
|
+
if ((mode === 'page' || mode === 'shell') && typeof refresh !== 'function') { reload('no-client-router'); return; }
|
|
569
|
+
// A page / shell refresh re-raises an error that still stands (see the
|
|
570
|
+
// refresh path above), so it may dismiss first. A component-only swap
|
|
571
|
+
// re-renders nothing on the server, so it takes down only an overlay the
|
|
572
|
+
// swap settled: the edited file, a re-imported module, or a rebuild error.
|
|
573
|
+
if (mode === 'page' || mode === 'shell') dismissDevOverlay();
|
|
574
|
+
else {
|
|
575
|
+
var bp = ${JSON.stringify(bp || '')};
|
|
576
|
+
dismissDevOverlayCoveredBy(mods.map(function (u) {
|
|
577
|
+
var p = String(u).split('?')[0];
|
|
578
|
+
return bp && p.indexOf(bp + '/') === 0 ? p.slice(bp.length) : p;
|
|
579
|
+
}).concat(verdict.by ? ['/' + verdict.by] : []));
|
|
580
|
+
}
|
|
581
|
+
preloadStyles().then(function (styles) {
|
|
582
|
+
return __webjsHot.applyModules(mods).then(function (r) {
|
|
583
|
+
if (!r.ok) { reload(r.why); return; }
|
|
584
|
+
var after = mode === 'page' || mode === 'shell' ? refresh(mode) : Promise.resolve('applied');
|
|
585
|
+
return after.then(function (outcome) {
|
|
586
|
+
if (!outcome || outcome === 'failed') { reload('refresh-failed'); return; }
|
|
587
|
+
styles.commit();
|
|
588
|
+
console.info('[webjs] hot-swapped ' + (r.tags.length ? r.tags.map(function (t) { return '<' + t + '>'; }).join(', ') : 'nothing') + by);
|
|
589
|
+
if (outcome === 'dev-error') __webjsLogRefresh(outcome, mode, verdict.by);
|
|
590
|
+
});
|
|
591
|
+
});
|
|
592
|
+
}).then(null, function (e) { reload(String((e && e.message) || e)); });
|
|
593
|
+
}
|
|
594
|
+
// The one console line an in-place refresh leaves (#1689). A refresh the dev
|
|
595
|
+
// server answered with a render error applied nothing, so it must not claim the
|
|
596
|
+
// page was updated: the last good render is still on screen under the overlay.
|
|
597
|
+
function __webjsLogRefresh(outcome, mode, by) {
|
|
598
|
+
var file = by ? ' (' + by + ')' : '';
|
|
599
|
+
if (outcome === 'dev-error') { console.info('[webjs] the page did not render, so the last good version stays under the error overlay' + file); return; }
|
|
600
|
+
console.info('[webjs] ' + (mode === 'shell' ? 'layout' : 'page') + ' updated in place' + file);
|
|
601
|
+
}
|
|
571
602
|
function __webjsDirectEvents() {
|
|
572
603
|
// No SharedWorker (Chrome for Android has none) or its construction threw (a
|
|
573
604
|
// strict dev CSP with no worker-src). Run the SAME relay here in the tab over
|
|
@@ -594,7 +625,7 @@ function __webjsDirectEvents() {
|
|
|
594
625
|
// dev-overlay bug leaves no trace, which would be a new behaviour rather
|
|
595
626
|
// than a restored one.
|
|
596
627
|
try {
|
|
597
|
-
if (m.type === 'reload') __webjsApplyReload(m.verdict);
|
|
628
|
+
if (m.type === 'reload') __webjsApplyReload(__webjsVerdictOf(m.verdict));
|
|
598
629
|
else if (m.type === 'webjs-error') __webjsApplyError(m.data);
|
|
599
630
|
} catch (_) {
|
|
600
631
|
console.error('[webjs] dev reload handler threw', _);
|
|
@@ -657,7 +688,7 @@ try {
|
|
|
657
688
|
const w = new SharedWorker(${workerUrl});
|
|
658
689
|
w.port.onmessage = (e) => {
|
|
659
690
|
const m = e.data || {};
|
|
660
|
-
if (m.type === 'reload') __webjsApplyReload(m.verdict);
|
|
691
|
+
if (m.type === 'reload') __webjsApplyReload(__webjsVerdictOf(m.verdict));
|
|
661
692
|
else if (m.type === 'webjs-error') __webjsApplyError(m.data);
|
|
662
693
|
};
|
|
663
694
|
// The worker's SCRIPT can fail to load where the page itself loaded: a
|
package/src/dev/hot-host.js
CHANGED
|
@@ -36,12 +36,37 @@ import { pathToFileURL } from 'node:url';
|
|
|
36
36
|
const HOSTS = Symbol.for('webjs.dev.hotHosts');
|
|
37
37
|
|
|
38
38
|
/**
|
|
39
|
-
* Sent to the `webjs dev` supervisor over IPC once the
|
|
40
|
-
*
|
|
41
|
-
*
|
|
39
|
+
* Sent to the `webjs dev` supervisor over IPC once the server is up and able
|
|
40
|
+
* to reload app edits in place: the Bun hot host (plugin-served edits, #1575),
|
|
41
|
+
* or a Node server with the module resolve hook (`esm-hot-hooks.js`, #1689).
|
|
42
|
+
* The supervisor then restarts only for its restart files. Mirrors
|
|
43
|
+
* `HOT_IN_PLACE_MESSAGE` in `@webjsdev/cli`'s `dev-reload.js`.
|
|
42
44
|
*/
|
|
43
45
|
export const HOT_IN_PLACE_MESSAGE = 'hot-reload-in-place';
|
|
44
46
|
|
|
47
|
+
/**
|
|
48
|
+
* Sent to the `webjs dev` supervisor to restart this server for an edit it
|
|
49
|
+
* cannot apply in place (#1689): a module the CommonJS loader holds, which no
|
|
50
|
+
* version reaches. The restart is the reload. Mirrors `HOT_RESTART_MESSAGE` in
|
|
51
|
+
* `@webjsdev/cli`'s `dev-reload.js`.
|
|
52
|
+
*/
|
|
53
|
+
export const HOT_RESTART_MESSAGE = 'hot-reload-restart';
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Ask the supervisor (when there is one) to restart for `path`, app-relative.
|
|
57
|
+
* @param {string} path
|
|
58
|
+
* @returns {boolean} whether the request was sent
|
|
59
|
+
*/
|
|
60
|
+
export function requestSupervisorRestart(path) {
|
|
61
|
+
try {
|
|
62
|
+
if (typeof process.send === 'function' && process.connected) {
|
|
63
|
+
process.send({ webjs: HOT_RESTART_MESSAGE, path });
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
} catch { /* no supervisor, or it is gone */ }
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
|
|
45
70
|
/** Tell the supervisor (when there is one) that edits reload in place. */
|
|
46
71
|
export function announceHotInPlace() {
|
|
47
72
|
try {
|
|
@@ -157,9 +182,11 @@ const INERT_ASSET = /\.(?:css|md|mdx|txt|log|map|png|jpe?g|gif|webp|avif|ico|svg
|
|
|
157
182
|
* plugin, the rest through `dev/bun-app-source.js` or the source-locations
|
|
158
183
|
* plugin), which `bun --hot` never watches.
|
|
159
184
|
*
|
|
160
|
-
* `isRouteOnly` answers whether a module path
|
|
161
|
-
*
|
|
162
|
-
*
|
|
185
|
+
* `isRouteOnly` answers whether a module path needs no reset (see
|
|
186
|
+
* `needsRegistryReset` on the request handler): a module the dev plugins
|
|
187
|
+
* already served, which the module versions re-evaluate in place with its
|
|
188
|
+
* importers (#1689), or a module only the router loads, whose dev re-import is
|
|
189
|
+
* keyed by content. A new or deleted module still resets. Anything the
|
|
163
190
|
* dev server cannot place, such as the extensionless temp file `sed -i`
|
|
164
191
|
* renames over the real one, gets the reset, because the event may name the
|
|
165
192
|
* temp file rather than the module it replaced.
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { mergeVerdicts } from '../dev-classify.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The edits a `bun --hot` registry reset has yet to apply (#1575, #1689).
|
|
5
|
+
*
|
|
6
|
+
* Under `bun --hot` an edit to a module some other module imports cannot be
|
|
7
|
+
* rebuilt straight away: the module registry has to be reset first, or the next
|
|
8
|
+
* request would still import the stale module. So the watcher POKES the re-run
|
|
9
|
+
* sentinel, keeps the batch here, and the re-run rebuilds with it. If the
|
|
10
|
+
* re-run does not arrive within `fallbackMs`, the fallback rebuilds anyway, so
|
|
11
|
+
* an edit is never lost to a sentinel `bun --hot` did not see.
|
|
12
|
+
*
|
|
13
|
+
* The race this owns: a large edit can make the re-run arrive AFTER the
|
|
14
|
+
* fallback. The fallback rebuilt and notified against the PRE-reset registry,
|
|
15
|
+
* so the browser applied the edit against stale modules, and the late re-run
|
|
16
|
+
* found nothing pending and rebuilt silently, so the tab never learned the
|
|
17
|
+
* fresh modules were there. The fallback's batch is therefore kept as `fired`,
|
|
18
|
+
* and a re-run with nothing pending rebuilds and notifies again with it.
|
|
19
|
+
*
|
|
20
|
+
* `fired` lives only `firedMs` (twice the fallback by default) past the
|
|
21
|
+
* fallback. A re-run that late is not the one the batch was waiting for: the
|
|
22
|
+
* reset was genuinely lost, and replaying the batch into a later, unrelated
|
|
23
|
+
* edit would merge its old verdict in (an old `reload` turning a page morph
|
|
24
|
+
* into a full reload) and bump its files' versions a second time, dropping
|
|
25
|
+
* server module state for a chain nobody touched.
|
|
26
|
+
*
|
|
27
|
+
* @param {{
|
|
28
|
+
* setTimeout?: (fn: () => void, ms: number) => unknown,
|
|
29
|
+
* clearTimeout?: (t: unknown) => void,
|
|
30
|
+
* fallbackMs?: number,
|
|
31
|
+
* firedMs?: number,
|
|
32
|
+
* }} [opts]
|
|
33
|
+
*/
|
|
34
|
+
export function createPokeState(opts = {}) {
|
|
35
|
+
const set = opts.setTimeout || ((fn, ms) => setTimeout(fn, ms));
|
|
36
|
+
const clear = opts.clearTimeout || ((t) => clearTimeout(/** @type {any} */ (t)));
|
|
37
|
+
const fallbackMs = opts.fallbackMs ?? 1500;
|
|
38
|
+
const firedMs = opts.firedMs ?? fallbackMs * 2;
|
|
39
|
+
/** @type {import('../dev-classify.js').ReloadVerdict | null} */
|
|
40
|
+
let verdict = null;
|
|
41
|
+
/** @type {Set<string>} */
|
|
42
|
+
let changed = new Set();
|
|
43
|
+
/** @type {{ verdict: import('../dev-classify.js').ReloadVerdict | null, changed: string[] } | null} */
|
|
44
|
+
let fired = null;
|
|
45
|
+
/** @type {unknown} */
|
|
46
|
+
let timer = null;
|
|
47
|
+
/** @type {unknown} */
|
|
48
|
+
let firedTimer = null;
|
|
49
|
+
const dropFired = () => {
|
|
50
|
+
clear(firedTimer);
|
|
51
|
+
firedTimer = null;
|
|
52
|
+
fired = null;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
/**
|
|
57
|
+
* Keep a batch for the coming re-run and (re)arm the fallback.
|
|
58
|
+
* @param {import('../dev-classify.js').ReloadVerdict | null | undefined} v
|
|
59
|
+
* @param {Iterable<string>} files
|
|
60
|
+
* @param {(verdict: import('../dev-classify.js').ReloadVerdict | null, changed: string[]) => void} onFallback
|
|
61
|
+
*/
|
|
62
|
+
poke: function (v, files, onFallback) {
|
|
63
|
+
verdict = mergeVerdicts(verdict, v || null);
|
|
64
|
+
for (const f of files) changed.add(f);
|
|
65
|
+
// A fresh poke inside the fired batch's window supersedes it: the
|
|
66
|
+
// coming re-run carries both, so the browser hears once, at the end.
|
|
67
|
+
if (fired) {
|
|
68
|
+
verdict = mergeVerdicts(fired.verdict, verdict);
|
|
69
|
+
for (const f of fired.changed) changed.add(f);
|
|
70
|
+
dropFired();
|
|
71
|
+
}
|
|
72
|
+
clear(timer);
|
|
73
|
+
timer = set(() => {
|
|
74
|
+
timer = null;
|
|
75
|
+
const batch = { verdict, changed: [...changed] };
|
|
76
|
+
verdict = null;
|
|
77
|
+
changed = new Set();
|
|
78
|
+
dropFired();
|
|
79
|
+
fired = batch;
|
|
80
|
+
firedTimer = set(dropFired, firedMs);
|
|
81
|
+
onFallback(batch.verdict, batch.changed);
|
|
82
|
+
}, fallbackMs);
|
|
83
|
+
},
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* What a registry-reset re-run should rebuild with: the pending batch when
|
|
87
|
+
* there is one, else the batch the fallback already applied against the old
|
|
88
|
+
* registry (notify again so the tab picks up the fresh modules), else
|
|
89
|
+
* nothing, a silent re-derive.
|
|
90
|
+
* @returns {{ verdict: import('../dev-classify.js').ReloadVerdict | null, changed: string[], notify: boolean }}
|
|
91
|
+
*/
|
|
92
|
+
takeForRerun: function () {
|
|
93
|
+
clear(timer);
|
|
94
|
+
timer = null;
|
|
95
|
+
if (verdict || changed.size) {
|
|
96
|
+
const out = { verdict, changed: [...changed], notify: !!verdict };
|
|
97
|
+
verdict = null;
|
|
98
|
+
changed = new Set();
|
|
99
|
+
dropFired();
|
|
100
|
+
return out;
|
|
101
|
+
}
|
|
102
|
+
if (fired) {
|
|
103
|
+
const out = { verdict: fired.verdict, changed: fired.changed, notify: !!fired.verdict };
|
|
104
|
+
dropFired();
|
|
105
|
+
return out;
|
|
106
|
+
}
|
|
107
|
+
return { verdict: null, changed: [], notify: false };
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
}
|