@webjsdev/core 0.7.32 → 0.7.33

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webjsdev/core",
3
- "version": "0.7.32",
3
+ "version": "0.7.33",
4
4
  "type": "module",
5
5
  "description": "webjs core runtime - html/css tags, WebComponent base, isomorphic renderers",
6
6
  "types": "./index.d.ts",
package/src/component.js CHANGED
@@ -159,7 +159,7 @@ function defaultHasChanged(a, b) {
159
159
  * `attachInternals()` on hydration.
160
160
  *
161
161
  * Deliberate deviation from lit: lit's `checkValidity` / `reportValidity`
162
- * THROW on the server. webjs returns `true` instead, to keep SSR
162
+ * THROW on the server. WebJs returns `true` instead, to keep SSR
163
163
  * progressive-enhancement-safe (a stray validity call in a constructor must
164
164
  * not 500 the page); the browser does the real validation.
165
165
  * @returns {any}
package/src/context.js CHANGED
@@ -2,7 +2,7 @@
2
2
  * Context Protocol: cross-component data sharing without prop drilling.
3
3
  *
4
4
  * Implements the W3C Community Group Context Protocol so any web component
5
- * (not just webjs components) can participate as a provider or consumer.
5
+ * (not just WebJs components) can participate as a provider or consumer.
6
6
  *
7
7
  * ## When to use
8
8
  *
package/src/metadata.d.ts CHANGED
@@ -259,7 +259,7 @@ export interface Metadata {
259
259
  * `<link rel="preconnect">` hints: warm DNS + TLS + TCP to a cross-origin
260
260
  * the page is about to talk to (an API host, a font / image CDN). Each
261
261
  * entry is a URL string or `{ url, crossorigin? }` (a font CDN needs
262
- * `crossorigin`). A single value or an array. webjs ALSO auto-emits one
262
+ * `crossorigin`). A single value or an array. WebJs ALSO auto-emits one
263
263
  * preconnect to the resolved vendor CDN origin for an unpinned app (deduped
264
264
  * against an author-declared one).
265
265
  */
@@ -390,7 +390,7 @@ function isProd() {
390
390
  * renders an empty (silent, isolated) element so no internal detail leaks.
391
391
  *
392
392
  * The prod-silence signal is the SERVER's `dev` flag, threaded through the SSR
393
- * render context (#483). webjs keys prod on the CLI `dev` flag, not `NODE_ENV`,
393
+ * render context (#483). WebJs keys prod on the CLI `dev` flag, not `NODE_ENV`,
394
394
  * and `webjs start` does not export `NODE_ENV=production`, so a bare prod launch
395
395
  * would otherwise leak the message. When `dev` is undefined (a context-free
396
396
  * `renderToString` with no server signal, e.g. a bare unit test) it falls back
package/src/rich-fetch.js CHANGED
@@ -8,14 +8,14 @@ const RPC_CONTENT_TYPE = 'application/vnd.webjs+json';
8
8
  * `TypedArray`, `Blob`, `File`, `FormData`, cycles).
9
9
  *
10
10
  * - Adds `Accept: application/vnd.webjs+json` to the request so a
11
- * server-side `json(data)` helper encodes with the webjs serializer.
11
+ * server-side `json(data)` helper encodes with the WebJs serializer.
12
12
  * - If the response content-type matches, decodes with the webjs
13
13
  * serializer and returns the rich value.
14
14
  * - Otherwise decodes with plain `.json()` (so any route you haven't
15
15
  * opted in on still works).
16
16
  * - Request bodies: if `init.body` is a plain object (not a string,
17
17
  * FormData/Blob/ArrayBuffer/ReadableStream/URLSearchParams), it is
18
- * encoded with the webjs serializer and the request content-type is
18
+ * encoded with the WebJs serializer and the request content-type is
19
19
  * set to our vendor type.
20
20
  *
21
21
  * ```js
@@ -153,7 +153,7 @@ let currentPageUrl = null;
153
153
  * Previous value of `history.scrollRestoration` (so we can restore it
154
154
  * when the router is disabled). The browser's default behavior of
155
155
  * auto-restoring scroll on popstate races with the SPA's own scroll
156
- * restoration: disabled here so webjs is the sole authority on scroll
156
+ * restoration: disabled here so WebJs is the sole authority on scroll
157
157
  * during navigation. Same pattern as Turbo Drive's
158
158
  * `assumeControlOfScrollRestoration()` (turbo/src/core/drive/history.js).
159
159
  *
@@ -1189,7 +1189,7 @@ function prefetchSuppressed(anchor) {
1189
1189
 
1190
1190
  /**
1191
1191
  * Resolve the prefetch strategy for an anchor from a `data-prefetch`
1192
- * attribute. webjs has no Link component (links are plain `<a href>`), so
1192
+ * attribute. WebJs has no Link component (links are plain `<a href>`), so
1193
1193
  * the knob is a valid-HTML `data-*` attribute, the same shape SvelteKit
1194
1194
  * (`data-sveltekit-preload-data`) and Astro (`data-astro-prefetch`) use.
1195
1195
  * Next.js / Nuxt / Remix express the same choice as a component PROP
@@ -1284,9 +1284,34 @@ function prefetch(href) {
1284
1284
  if (!/^text\/html\b/i.test(ctype)) return;
1285
1285
  if (resp.status >= 400) return;
1286
1286
  const build = resp.headers.get('x-webjs-build');
1287
+ const src = resp.headers.get('x-webjs-src');
1288
+ // Deploy detected at PREFETCH time (#899). A prefetch fetch carries the
1289
+ // server's current build id AND app-source id. If EITHER differs from what
1290
+ // the page booted with, a deploy landed, so every earlier snapshot/prefetch
1291
+ // is pre-deploy and stale. Evict them here, well before the click (a
1292
+ // hover/viewport prefetch fires early), so a click on a previously-
1293
+ // prefetched link re-fetches fresh (then applySwap hard-reloads on a build
1294
+ // change or soft-applies on a src-only change). This shrinks the window
1295
+ // where a pre-deploy prefetch, whose stored ids equal the still-old page
1296
+ // ids so applySwap alone cannot tell it is stale, is served. Both ids of a
1297
+ // pair must be present: an empty id is the warmup "version unknown", never
1298
+ // a deploy signal.
1299
+ const pageTag = typeof document !== 'undefined' ? document.querySelector('script[type="importmap"]') : null;
1300
+ const pageBuild = pageTag ? pageTag.getAttribute('data-webjs-build') : null;
1301
+ const pageSrc = pageTag ? pageTag.getAttribute('data-webjs-src') : null;
1302
+ if ((build && pageBuild && build !== pageBuild) || (src && pageSrc && src !== pageSrc)) {
1303
+ snapshotCache.clear();
1304
+ prefetchCache.clear();
1305
+ // Deliberately do NOT advance the page's data-webjs-src here (only the
1306
+ // foreground `applySwap` does). A prefetch is speculative; leaving the
1307
+ // reference id on the old deploy keeps applySwap the single authority
1308
+ // that settles the page on the first real navigation. The cost is small:
1309
+ // repeated prefetches in the pre-first-nav window each re-clear the
1310
+ // (already tiny) caches, which converges the instant the user navigates.
1311
+ }
1287
1312
  const finalUrl = resp.redirected && resp.url ? resp.url : href;
1288
1313
  const html = await resp.text();
1289
- prefetchStore(key, { html, build, finalUrl, at: nowMs() });
1314
+ prefetchStore(key, { html, build, src, finalUrl, at: nowMs() });
1290
1315
  })
1291
1316
  .catch(() => { /* speculative: swallow */ })
1292
1317
  .finally(() => {
@@ -1608,6 +1633,7 @@ async function fetchAndApply(href, frameId, recordHistory, optimisticState, meth
1608
1633
  // shell swap. Null for a buffered (non-streaming) or prefetched response.
1609
1634
  let streamCtx = null;
1610
1635
  let incomingBuild = null;
1636
+ let incomingSrc = null;
1611
1637
  /** @type {number | null} */
1612
1638
  let respStatus = null;
1613
1639
  /** @type {boolean} */
@@ -1633,6 +1659,7 @@ async function fetchAndApply(href, frameId, recordHistory, optimisticState, meth
1633
1659
  if (prefetched) {
1634
1660
  html = prefetched.html;
1635
1661
  incomingBuild = prefetched.build;
1662
+ incomingSrc = prefetched.src;
1636
1663
  finalUrl = prefetched.finalUrl;
1637
1664
  // A consumed prefetch is a successful 200 GET fragment.
1638
1665
  respStatus = 200;
@@ -1725,6 +1752,7 @@ async function fetchAndApply(href, frameId, recordHistory, optimisticState, meth
1725
1752
  // tag to compare. The applySwap importmap-mismatch guard reads
1726
1753
  // this to detect deploys that bumped the vendor pin.
1727
1754
  incomingBuild = resp.headers.get('x-webjs-build');
1755
+ incomingSrc = resp.headers.get('x-webjs-src');
1728
1756
  // Progressive streaming (#473): read only up to the first streamed Suspense
1729
1757
  // boundary so the shell (with fallbacks) swaps in immediately; the rest
1730
1758
  // streams in after the swap. A body with no boundaries reads to completion,
@@ -1763,7 +1791,7 @@ async function fetchAndApply(href, frameId, recordHistory, optimisticState, meth
1763
1791
  // recover in place rather than a destructive full reload.
1764
1792
  if (!doc) { restoreOptimistic(optimisticState); handleNavigationError(href, null, new Error('navigation response did not parse as HTML')); return { ok: false, status: respStatus, aborted: false }; }
1765
1793
 
1766
- applySwap(doc, frameId, false, finalUrl, incomingBuild);
1794
+ applySwap(doc, frameId, false, finalUrl, incomingBuild, incomingSrc);
1767
1795
 
1768
1796
  if (recordHistory) history.pushState(null, '', finalUrl);
1769
1797
 
@@ -2234,7 +2262,7 @@ function upgradeCustomElementsInRange(range) {
2234
2262
  }
2235
2263
  }
2236
2264
 
2237
- function applySwap(doc, frameId, revalidating, href, incomingBuild) {
2265
+ function applySwap(doc, frameId, revalidating, href, incomingBuild, incomingSrc) {
2238
2266
  // SSR action seeding (#472): ingest any seed payload the incoming page
2239
2267
  // carries BEFORE its components are grafted into the live DOM and upgrade, so
2240
2268
  // a soft-navigated async component resolves from the seed instead of
@@ -2308,6 +2336,14 @@ function applySwap(doc, frameId, revalidating, href, incomingBuild) {
2308
2336
  if (currentSig !== incomingSig) mismatch = true;
2309
2337
  }
2310
2338
  if (mismatch && typeof location !== 'undefined') {
2339
+ // A detected cross-deploy mismatch means every URL-keyed snapshot and
2340
+ // speculative prefetch was captured on the OLD deploy, so it is stale
2341
+ // pre-deploy HTML (#899). Evict both caches so no stale entry is applied
2342
+ // on a later soft nav, even when the infinite-reload guard below bails to
2343
+ // a partial swap instead of a full reload (that partial swap must not then
2344
+ // pull a pre-deploy fragment out of the cache).
2345
+ snapshotCache.clear();
2346
+ prefetchCache.clear();
2311
2347
  // Infinite-reload guard: if the importmap appears to genuinely
2312
2348
  // change EVERY navigation (e.g. a developer is live-editing the
2313
2349
  // pin file in dev, or a misbehaving CDN returns different
@@ -2336,6 +2372,27 @@ function applySwap(doc, frameId, revalidating, href, incomingBuild) {
2336
2372
  return;
2337
2373
  }
2338
2374
  } else if (!mismatch) {
2375
+ // No importmap/build mismatch, so no hard reload. But the app-source
2376
+ // signal (#899) is the SECOND tier: if `data-webjs-src` differs, an
2377
+ // app-source or server-framework deploy changed the SSR output while the
2378
+ // running page's browser code is unchanged. A hard reload would be an
2379
+ // over-correction; instead EVICT the URL-keyed snapshot + prefetch caches
2380
+ // (all captured on the OLD deploy) so a later soft nav re-fetches fresh.
2381
+ // The current nav's already-fetched `doc` still applies normally. Both ids
2382
+ // must be present (an empty id is the warmup "unknown", never a signal),
2383
+ // exactly like the build guard.
2384
+ const currentSrc = currentTag ? currentTag.getAttribute('data-webjs-src') : null;
2385
+ if (incomingSrc && currentSrc && incomingSrc !== currentSrc) {
2386
+ snapshotCache.clear();
2387
+ prefetchCache.clear();
2388
+ // Advance the page's reference id. The importmap <script> is preserved
2389
+ // across soft navs (an importmap cannot be re-registered), so without
2390
+ // this the tag would keep its OLD id and EVERY later nav in the new
2391
+ // deploy would re-detect the same mismatch and evict again, defeating
2392
+ // the caches. Updating the attribute (not the importmap body) settles
2393
+ // the page onto the new deploy: evict once, then cache normally.
2394
+ if (currentTag) currentTag.setAttribute('data-webjs-src', incomingSrc);
2395
+ }
2339
2396
  // A clean swap (no importmap mismatch) means we're back to
2340
2397
  // matching client/server importmaps. Clear the reload flag so
2341
2398
  // a future LEGITIMATE mismatch (e.g. a later deploy) gets a
@@ -2839,7 +2896,7 @@ function getCspNonce() {
2839
2896
  // because it can be called against script/link elements (where
2840
2897
  // browsers DO expose `.nonce` and additionally clear the
2841
2898
  // `nonce` attribute on document load). The `<meta name="csp-nonce">`
2842
- // element webjs targets has no `.nonce` IDL (only script + link
2899
+ // element WebJs targets has no `.nonce` IDL (only script + link
2843
2900
  // elements do per HTML spec), so the only viable source is the
2844
2901
  // `content` attribute.
2845
2902
  return meta ? meta.getAttribute('content') || '' : '';
@@ -3220,7 +3277,9 @@ export {
3220
3277
  diffElementInPlace as _diffElementInPlace,
3221
3278
  reconcileChildren as _reconcileChildren,
3222
3279
  onPopState as _onPopState,
3280
+ applySwap as _applySwap,
3223
3281
  snapshotCache as _snapshotCache,
3282
+ prefetchCache as _prefetchCache,
3224
3283
  LIVE_ATTRS as _LIVE_ATTRS,
3225
3284
  blurOutgoingFocus as _blurOutgoingFocus,
3226
3285
  onSubmit as _onSubmit,
package/src/routes.d.ts CHANGED
@@ -3,7 +3,7 @@
3
3
  * opt-in, generated route union that types `navigate()` and catches bad
4
4
  * hrefs at tsserver time (#258).
5
5
  *
6
- * webjs identity holds: NO build step, NO runtime cost, types-only, opt-in
6
+ * WebJs identity holds: NO build step, NO runtime cost, types-only, opt-in
7
7
  * for JSDoc apps. The mechanism mirrors Next 15's `typedRoutes` but via
8
8
  * interface declaration-merging instead of a bundler.
9
9
  *
@@ -86,7 +86,7 @@ export type RouteParams<R extends string> = R extends keyof RouteParamMap
86
86
 
87
87
  /**
88
88
  * A value that is BOTH synchronously usable as `T` and `await`-able to `T`
89
- * (#848). The webjs runtime hands `params` / `searchParams` as a plain object
89
+ * (#848). The WebJs runtime hands `params` / `searchParams` as a plain object
90
90
  * carrying a non-enumerable `then`, so `params.id` and `await params` both
91
91
  * work; this type expresses that dual nature to TypeScript.
92
92
  */
@@ -2,7 +2,7 @@
2
2
  * Compile-time serializability typing for server actions (#488).
3
3
  *
4
4
  * A `'use server'` action's arguments and result cross the RPC wire through the
5
- * webjs serializer, which round-trips a SPECIFIC set of rich types (`Date`,
5
+ * WebJs serializer, which round-trips a SPECIFIC set of rich types (`Date`,
6
6
  * `Map`, `Set`, `BigInt`, `Error`, `RegExp`, `URL`, typed arrays, `ArrayBuffer`,
7
7
  * `Blob`, `File`, `FormData`, registered `Symbol`s, plain objects / arrays, and
8
8
  * cycles). It does NOT round-trip a FUNCTION or a class instance's METHODS: a
@@ -17,7 +17,7 @@
17
17
  * / `SerializableResult` apply it across an action's parameter tuple and its
18
18
  * (possibly promised) return.
19
19
  *
20
- * This is OPT-IN by design: webjs actions stay plain `export async function`s
20
+ * This is OPT-IN by design: WebJs actions stay plain `export async function`s
21
21
  * with no wrapper (the framework rewrites the client import to an RPC stub at
22
22
  * runtime, it does not wrap the authored function). An author who wants the
23
23
  * guard annotates the action with `SerializableActionFn`, e.g.
@@ -40,7 +40,7 @@ export type NonSerializable<Reason extends string = 'this value is not serializa
40
40
  type SerializablePrimitive = string | number | boolean | bigint | null | undefined;
41
41
 
42
42
  /**
43
- * Built-in rich types the webjs serializer round-trips as themselves (their
43
+ * Built-in rich types the WebJs serializer round-trips as themselves (their
44
44
  * identity / methods survive because the serializer reconstructs the instance).
45
45
  * `Map` / `Set` recurse into their members via the top-level mapper.
46
46
  */
@@ -1,5 +1,5 @@
1
1
  /**
2
- * The webjs wire serializer (rich-type round-trip: Date / Map / Set / BigInt /
2
+ * The WebJs wire serializer (rich-type round-trip: Date / Map / Set / BigInt /
3
3
  * Error / typed arrays / Blob / File / FormData / Symbols / cycles).
4
4
  */
5
5
  export function stringify(value: unknown): Promise<string>;
package/src/serialize.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * webjs serializer: JSON-friendly encoding for rich JS types.
2
+ * WebJs serializer: JSON-friendly encoding for rich JS types.
3
3
  *
4
4
  * Replaces superjson with a pure-ESM, dependency-free implementation
5
5
  * tuned to the wire surface that React Server Actions support, plus a
package/src/signal.js CHANGED
@@ -493,7 +493,7 @@ class SignalWatcher {
493
493
  * to the watcher's producer set automatically. Re-arms on every call.
494
494
  *
495
495
  * Used by the WebComponent integration and the `watch()` directive
496
- * because the natural webjs pattern is "watch everything this render
496
+ * because the natural WebJs pattern is "watch everything this render
497
497
  * touches" rather than enumerating signals up front.
498
498
  *
499
499
  * @param {() => unknown} fn
package/src/testing.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Testing utilities for webjs components.
2
+ * Testing utilities for WebJs components.
3
3
  *
4
4
  * **When to use (AI hint):** Import from `'@webjsdev/core/testing'` in test files
5
5
  * to create, render, and interact with web components in a Node.js test
@@ -1,5 +1,5 @@
1
1
  /**
2
- * TypeScript overlay for the `webjs` config block in a webjs app's
2
+ * TypeScript overlay for the `webjs` config block in a WebJs app's
3
3
  * package.json.
4
4
  *
5
5
  * // package.json is JSON, so author it there, but a typed reference
@@ -95,6 +95,17 @@ export interface WebjsDevTasks {
95
95
  * the parent and torn down on exit, so a watcher cannot leak past the server.
96
96
  */
97
97
  parallel?: string[];
98
+ /**
99
+ * Extra directories the dev live-reload watcher follows IN ADDITION to the
100
+ * appDir (#894). The dev server watches its appDir recursively, but an app
101
+ * that reads content from OUTSIDE its tree (e.g. blog markdown in a repo-root
102
+ * `blog/` dir, a sibling of the app) sees no reload when that content changes.
103
+ * Each entry is resolved relative to the app root and MAY escape it
104
+ * (`"../blog"`); a change under one triggers the same rebuild + browser reload
105
+ * as an in-tree edit. Missing / overlapping (ancestor or descendant of the
106
+ * appDir) entries are skipped. Read by the server (`readDevWatchPathsFromApp`).
107
+ */
108
+ watch?: string[];
98
109
  }
99
110
 
100
111
  /** Start task orchestration in `webjs.start` (#550). Read by the CLI, not the server. */
@@ -124,7 +135,7 @@ export interface WebjsCspConfig {
124
135
  }
125
136
 
126
137
  /**
127
- * The `webjs` object in a webjs app's package.json. Every key is
138
+ * The `webjs` object in a WebJs app's package.json. Every key is
128
139
  * optional. Mirrors what the server readers actually consume, NOT a
129
140
  * Next.js superset.
130
141
  */
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Tiny client-side WebSocket helper with automatic reconnection, JSON
3
- * parse/stringify, and the webjs CSRF-style matches-origin wss:// scheme
3
+ * parse/stringify, and the WebJs CSRF-style matches-origin wss:// scheme
4
4
  * rewriting.
5
5
  *
6
6
  * ```js