@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/dist/webjs-core-browser.js +1 -1
- package/dist/webjs-core-browser.js.map +2 -2
- package/dist/webjs-core-testing.js +2 -2
- package/dist/webjs-core-testing.js.map +2 -2
- package/dist/webjs-core.js +2 -2
- package/dist/webjs-core.js.map +2 -2
- package/package.json +1 -1
- package/src/render-server.js +44 -1
- package/src/router-client.js +17 -2
- package/src/serialize.js +20 -8
package/package.json
CHANGED
package/src/render-server.js
CHANGED
|
@@ -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
|
-
|
|
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;
|
package/src/router-client.js
CHANGED
|
@@ -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
|
}
|