@webjsdev/core 0.7.4 → 0.7.5

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.4",
3
+ "version": "0.7.5",
4
4
  "type": "module",
5
5
  "description": "webjs core runtime - html/css tags, WebComponent base, isomorphic renderers",
6
6
  "types": "./index.d.ts",
@@ -314,6 +314,41 @@ async function renderTemplate(tr, ctx) {
314
314
  return out;
315
315
  }
316
316
 
317
+ // Browser-only names whose absence during SSR produces a recognisable error.
318
+ // Mirrors the `no-browser-globals-in-render` webjs check rule, which catches
319
+ // these at edit time; this turns the runtime SSR crash into the same guidance.
320
+ const SSR_BROWSER_GLOBALS = new Set([
321
+ 'document', 'window', 'localStorage', 'sessionStorage', 'navigator',
322
+ 'matchMedia', 'requestAnimationFrame', 'getComputedStyle',
323
+ 'IntersectionObserver', 'MutationObserver', 'ResizeObserver',
324
+ ]);
325
+ const SSR_HTMLELEMENT_METHODS = new Set([
326
+ 'attachShadow', 'setAttribute', 'getAttribute', 'removeAttribute',
327
+ 'hasAttribute', 'dispatchEvent', 'querySelector', 'querySelectorAll',
328
+ 'getBoundingClientRect', 'focus', 'blur', 'scrollIntoView',
329
+ ]);
330
+
331
+ /**
332
+ * If `e` is the recognisable failure of touching a browser-only API during
333
+ * SSR (a `ReferenceError` for a browser global, or a `TypeError` calling an
334
+ * HTMLElement method that does not exist on the bare server-side instance),
335
+ * return an actionable, member-naming hint; otherwise null.
336
+ * @param {unknown} e
337
+ * @returns {string | null}
338
+ */
339
+ function browserMemberHint(e) {
340
+ const msg = e && typeof (/** @type any */ (e).message) === 'string' ? /** @type any */ (e).message : '';
341
+ let m = /^(\w+) is not defined$/.exec(msg);
342
+ if (e instanceof ReferenceError && m && SSR_BROWSER_GLOBALS.has(m[1])) {
343
+ return `\`${m[1]}\` is a browser-only global and is undefined during SSR.`;
344
+ }
345
+ m = /\.(\w+) is not a function$/.exec(msg);
346
+ if (e instanceof TypeError && m && SSR_HTMLELEMENT_METHODS.has(m[1])) {
347
+ return `\`${m[1]}\` is an HTMLElement method that does not exist on the server-side component instance during SSR.`;
348
+ }
349
+ return null;
350
+ }
351
+
317
352
  /**
318
353
  * Scan an HTML string for registered custom elements and inject
319
354
  * Declarative Shadow DOM (`<template shadowrootmode="open">`).
@@ -443,7 +478,15 @@ async function injectDSD(html, ctx) {
443
478
  });
444
479
  }
445
480
  } catch (e) {
446
- console.error(`[webjs] SSR failed for <${tag}>:`, e);
481
+ const hint = browserMemberHint(e);
482
+ if (hint) {
483
+ console.error(
484
+ `[webjs] SSR failed for <${tag}>: ${hint} It was touched in the component's constructor or render(), which run during SSR. Move browser-only work to connectedCallback() or a lifecycle hook (firstUpdated/updated), which SSR never calls; seed first-paint defaults in the constructor only from server-known inputs (attributes / props).`,
485
+ e,
486
+ );
487
+ } else {
488
+ console.error(`[webjs] SSR failed for <${tag}>:`, e);
489
+ }
447
490
  }
448
491
  }
449
492
  if (!edits.length) return html;
@@ -1003,7 +1003,17 @@ function drainPrefetchQueue() {
1003
1003
  }
1004
1004
 
1005
1005
  /**
1006
- * Store a speculative entry under LRU + cap.
1006
+ * Store a speculative entry under LRU + cap, then announce that the
1007
+ * fragment is cached and consumable.
1008
+ *
1009
+ * The `webjs:prefetch` event fires the instant a speculative fragment
1010
+ * becomes consumable (after the response body has been read), which is
1011
+ * strictly later than the prefetch request going out. App code can
1012
+ * listen to instrument prefetch hit rate, and tests can await it to know
1013
+ * a subsequent click will consume the cache rather than refetch. The
1014
+ * detail carries the cached URL and a `from: 'prefetch'` tag so a single
1015
+ * listener can disambiguate it from `webjs:navigate`.
1016
+ *
1007
1017
  * @param {string} key
1008
1018
  * @param {PrefetchEntry} entry
1009
1019
  */
@@ -1014,6 +1024,11 @@ function prefetchStore(key, entry) {
1014
1024
  const oldest = prefetchCache.keys().next().value;
1015
1025
  prefetchCache.delete(oldest);
1016
1026
  }
1027
+ if (typeof document !== 'undefined') {
1028
+ document.dispatchEvent(new CustomEvent('webjs:prefetch', {
1029
+ detail: { url: entry.finalUrl, key, from: 'prefetch' },
1030
+ }));
1031
+ }
1017
1032
  }
1018
1033
 
1019
1034
  /**
@@ -1267,7 +1282,7 @@ async function fetchAndApply(href, frameId, recordHistory, optimisticState, meth
1267
1282
  }
1268
1283
  }
1269
1284
 
1270
- document.dispatchEvent(new CustomEvent('webjs:navigate', { detail: { url: finalUrl, frameId } }));
1285
+ document.dispatchEvent(new CustomEvent('webjs:navigate', { detail: { url: finalUrl, frameId, from: 'navigate' } }));
1271
1286
  }
1272
1287
 
1273
1288
  /**
package/src/serialize.js CHANGED
@@ -321,15 +321,26 @@ function decode(v, ctx) {
321
321
 
322
322
  function decodeTagged(v, tag, ctx) {
323
323
  const id = v[ID_KEY];
324
+ // Register the decoded value under its `_id` so a later `Ref` to it
325
+ // resolves. Container tags (Map/Set/Arr/Error/object) register BEFORE
326
+ // decoding children (so a cycle through the container resolves); leaf
327
+ // rich types (Date, typed arrays, Blob, ...) have no children and register
328
+ // through this helper at return. Without it, a SHARED Date or typed array
329
+ // (the same instance appearing twice) decodes the first copy but throws
330
+ // "Dangling reference" on the second.
331
+ const reg = (value) => {
332
+ if (typeof id === 'number') ctx.refs.set(id, value);
333
+ return value;
334
+ };
324
335
  switch (tag) {
325
336
  case 'undef': return undefined;
326
337
  case 'NaN': return NaN;
327
338
  case 'Inf': return Infinity;
328
339
  case '-Inf': return -Infinity;
329
340
  case '-0': return -0;
330
- case 'BigInt': return BigInt(v.v);
331
- case 'Sym': return Symbol.for(v.v);
332
- case 'Date': return v.v == null ? new Date(NaN) : new Date(v.v);
341
+ case 'BigInt': return reg(BigInt(v.v));
342
+ case 'Sym': return reg(Symbol.for(v.v));
343
+ case 'Date': return reg(v.v == null ? new Date(NaN) : new Date(v.v));
333
344
  case 'Error': {
334
345
  const e = new Error(v.v.message);
335
346
  e.name = v.v.name;
@@ -365,16 +376,17 @@ function decodeTagged(v, tag, ctx) {
365
376
  case 'Blob': {
366
377
  if (typeof Blob === 'undefined') throw new TypeError('Blob is not available in this environment.');
367
378
  const bytes = b64ToBytes(v.v);
368
- return new Blob([bytes], { type: v.t || '' });
379
+ return reg(new Blob([bytes], { type: v.t || '' }));
369
380
  }
370
381
  case 'File': {
371
382
  if (typeof File === 'undefined') throw new TypeError('File is not available in this environment.');
372
383
  const bytes = b64ToBytes(v.v);
373
- return new File([bytes], v.n, { type: v.t || '', lastModified: v.m });
384
+ return reg(new File([bytes], v.n, { type: v.t || '', lastModified: v.m }));
374
385
  }
375
386
  case 'FD': {
376
387
  if (typeof FormData === 'undefined') throw new TypeError('FormData is not available in this environment.');
377
388
  const fd = new FormData();
389
+ reg(fd);
378
390
  for (const [k, val] of v.v) {
379
391
  const decoded = decode(val, ctx);
380
392
  fd.append(k, decoded);
@@ -385,17 +397,17 @@ function decodeTagged(v, tag, ctx) {
385
397
  // Typed arrays + binary buffers
386
398
  if (tag === 'buf') {
387
399
  const bytes = b64ToBytes(v.v);
388
- return bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
400
+ return reg(bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength));
389
401
  }
390
402
  if (tag === 'dv') {
391
403
  const bytes = b64ToBytes(v.v);
392
- return new DataView(bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength));
404
+ return reg(new DataView(bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength)));
393
405
  }
394
406
  const Ctor = TAG_TO_TYPED_ARRAY[tag];
395
407
  if (Ctor) {
396
408
  const bytes = b64ToBytes(v.v);
397
409
  const sliced = bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
398
- return new Ctor(sliced);
410
+ return reg(new Ctor(sliced));
399
411
  }
400
412
  throw new TypeError(`Unknown serialization tag: ${tag}`);
401
413
  }