@webjsdev/core 0.7.35 → 0.7.36
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 +2 -2
- package/dist/webjs-core-browser.js.map +2 -2
- package/dist/webjs-core-testing.js +16 -16
- package/dist/webjs-core-testing.js.map +2 -2
- package/dist/webjs-core.js +3 -3
- package/dist/webjs-core.js.map +2 -2
- package/package.json +1 -1
- package/src/component.js +10 -0
- package/src/render-server.js +38 -0
- package/src/router-client.js +84 -3
package/package.json
CHANGED
package/src/component.js
CHANGED
|
@@ -779,6 +779,16 @@ class WebComponentBase extends Base {
|
|
|
779
779
|
// hydrated element (whose attribute already arrived from SSR) is unchanged.
|
|
780
780
|
this._reflectDeclaredAttributes();
|
|
781
781
|
const Ctor = /** @type any */ (this.constructor);
|
|
782
|
+
// Mark LIGHT-DOM component hosts so the framework default host rule
|
|
783
|
+
// (`@layer webjs-host { :where([data-wj-host]) { display: block } }`)
|
|
784
|
+
// applies. SSR already stamps this on server-rendered light hosts
|
|
785
|
+
// (idempotent here); this also covers a client-only light component (never
|
|
786
|
+
// SSR'd) so it does not collapse. Shadow hosts are NOT marked: a document
|
|
787
|
+
// rule would override the shadow author's own `:host { display: … }`, so
|
|
788
|
+
// shadow components control their host display via `:host` in `static styles`.
|
|
789
|
+
if (Ctor.shadow !== true && !this.hasAttribute('data-wj-host')) {
|
|
790
|
+
this.setAttribute('data-wj-host', '');
|
|
791
|
+
}
|
|
782
792
|
if (Ctor.shadow === true) {
|
|
783
793
|
const hadSSRShadow = !!this.shadowRoot;
|
|
784
794
|
if (!this.shadowRoot) {
|
package/src/render-server.js
CHANGED
|
@@ -502,6 +502,24 @@ async function injectDSD(html, ctx, ancestors = [], dev) {
|
|
|
502
502
|
// this.hidden on the host). Reading after render() captures all three.
|
|
503
503
|
// Appending keeps the original tag byte-identical when nothing changed.
|
|
504
504
|
opening = appendReflectedAttrs(opening, instance, presentAttrNames);
|
|
505
|
+
// Mark LIGHT-DOM component hosts so the framework default
|
|
506
|
+
// `@layer webjs-host { :where([data-wj-host]) { display: block } }`
|
|
507
|
+
// (injected once in the document head) applies at first paint. A custom
|
|
508
|
+
// element is `display:inline` by default, which collapses a component used
|
|
509
|
+
// as a block container (a board / card) until an author style intervenes.
|
|
510
|
+
// The low-priority `@layer` keeps it overridable by any author style,
|
|
511
|
+
// INCLUDING Tailwind's layered utilities (`class="flex"` wins). Emitted
|
|
512
|
+
// uniformly regardless of elision, so the elision on-vs-off differential is
|
|
513
|
+
// preserved.
|
|
514
|
+
//
|
|
515
|
+
// Shadow hosts are NOT marked: a document-level rule targeting the host
|
|
516
|
+
// beats the shadow tree's own `:host { display: … }` (the encapsulation-
|
|
517
|
+
// context criterion outranks both layer and specificity for normal
|
|
518
|
+
// declarations), so marking them would silently override the shadow
|
|
519
|
+
// author's `:host` display. Shadow components set their own host display
|
|
520
|
+
// via `:host` in `static styles` (the idiomatic mechanism), which the
|
|
521
|
+
// framework must not clobber.
|
|
522
|
+
if (!isShadow) opening = withHostMarker(opening);
|
|
505
523
|
// Render the template to HTML. injectDSD recurses on the result so
|
|
506
524
|
// nested custom elements (e.g. <theme-toggle> inside <blog-shell>)
|
|
507
525
|
// get their own DSD pass.
|
|
@@ -635,6 +653,14 @@ async function injectDSD(html, ctx, ancestors = [], dev) {
|
|
|
635
653
|
// path), not land in light DOM. Otherwise the client renders the error
|
|
636
654
|
// into the shadow root while the light error box lingers underneath.
|
|
637
655
|
const isShadowErr = /** @type any */ (Cls).shadow === true;
|
|
656
|
+
// Mark the LIGHT host here too, so a component whose SSR render() throws
|
|
657
|
+
// paints its error state as display:block (not the inline default),
|
|
658
|
+
// matching the success path. When an `async render()` rejects, it throws
|
|
659
|
+
// before the success-path withHostMarker (above) ran, so `opening` is still
|
|
660
|
+
// unmarked; when a later template render throws, the success marker already
|
|
661
|
+
// ran and this call is a no-op (withHostMarker is idempotent). Shadow hosts
|
|
662
|
+
// stay unmarked (their :host must win).
|
|
663
|
+
if (!isShadowErr) opening = withHostMarker(opening);
|
|
638
664
|
let text;
|
|
639
665
|
if (isShadowErr) {
|
|
640
666
|
const rawStyles = /** @type any */ (Cls).styles;
|
|
@@ -1032,6 +1058,18 @@ function seedServerAttrs(instance, attrs) {
|
|
|
1032
1058
|
* @param {Set<string>} presentAttrNames lowercased names already in the source tag
|
|
1033
1059
|
* @returns {string}
|
|
1034
1060
|
*/
|
|
1061
|
+
/**
|
|
1062
|
+
* Add the component host marker (`data-wj-host`) to an opening tag, unless it
|
|
1063
|
+
* is already present. Insert before the closing `>` the same way
|
|
1064
|
+
* `appendReflectedAttrs` does. Idempotent so a re-processed tag is unchanged.
|
|
1065
|
+
* @param {string} opening the element's opening tag, ending in `>`
|
|
1066
|
+
* @returns {string}
|
|
1067
|
+
*/
|
|
1068
|
+
function withHostMarker(opening) {
|
|
1069
|
+
if (/\sdata-wj-host(?=[\s>=])/i.test(opening)) return opening;
|
|
1070
|
+
return `${opening.slice(0, -1)} data-wj-host>`;
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1035
1073
|
function appendReflectedAttrs(opening, instance, presentAttrNames) {
|
|
1036
1074
|
if (!instance || typeof instance.getAttributeNames !== 'function') return opening;
|
|
1037
1075
|
let extra = '';
|
package/src/router-client.js
CHANGED
|
@@ -82,10 +82,40 @@ const STREAM_MIME = 'text/vnd.webjs-stream.html';
|
|
|
82
82
|
* Parse HTML into a Document. Prefers Document.parseHTMLUnsafe (processes
|
|
83
83
|
* Declarative Shadow DOM) over DOMParser (does NOT process DSD).
|
|
84
84
|
*
|
|
85
|
+
* A partial-nav response (#936) is an INNER fragment that BEGINS with the
|
|
86
|
+
* `<!--wj:children:<path>-->` layout marker and carries no `<!doctype>`/`<html>`.
|
|
87
|
+
* Parsing such a fragment as a DOCUMENT hoists that leading comment OUT of
|
|
88
|
+
* `<body>` (the HTML parser's "before html" insertion mode makes a leading
|
|
89
|
+
* comment a child of the document, before `<html>`), so
|
|
90
|
+
* `collectChildrenSlots(doc.body)` never sees the opening marker, finds no
|
|
91
|
+
* shared slot, and `applySwap` falls to the destructive full-body swap that
|
|
92
|
+
* strips the head stylesheets and the outer layout. So a fragment is parsed in
|
|
93
|
+
* BODY (fragment) context instead, keeping the marker with its content.
|
|
94
|
+
* `body.setHTMLUnsafe` also processes Declarative Shadow DOM, so a shadow
|
|
95
|
+
* component inside the swapped content still re-attaches its root; the
|
|
96
|
+
* `<template>` path is the fallback for browsers without it (markers preserved,
|
|
97
|
+
* DSD not, which matches the pre-`setHTMLUnsafe` baseline).
|
|
98
|
+
*
|
|
85
99
|
* @param {string} html
|
|
86
100
|
* @returns {Document | null}
|
|
87
101
|
*/
|
|
88
102
|
function parseHTML(html) {
|
|
103
|
+
const isFragment = !/^\s*(?:<!doctype|<html)/i.test(html);
|
|
104
|
+
if (isFragment && typeof document !== 'undefined' && document.implementation) {
|
|
105
|
+
try {
|
|
106
|
+
const doc = document.implementation.createHTMLDocument();
|
|
107
|
+
if (typeof doc.body.setHTMLUnsafe === 'function') {
|
|
108
|
+
doc.body.setHTMLUnsafe(html);
|
|
109
|
+
} else {
|
|
110
|
+
const t = doc.createElement('template');
|
|
111
|
+
t.innerHTML = html;
|
|
112
|
+
doc.body.appendChild(t.content);
|
|
113
|
+
}
|
|
114
|
+
return doc;
|
|
115
|
+
} catch {
|
|
116
|
+
// Fall through to a document parse (still functional, just the #936 path).
|
|
117
|
+
}
|
|
118
|
+
}
|
|
89
119
|
if (typeof Document !== 'undefined' && typeof Document.parseHTMLUnsafe === 'function') {
|
|
90
120
|
return Document.parseHTMLUnsafe(html);
|
|
91
121
|
}
|
|
@@ -1279,9 +1309,19 @@ function prefetch(href) {
|
|
|
1279
1309
|
return;
|
|
1280
1310
|
}
|
|
1281
1311
|
|
|
1312
|
+
const have = buildHaveHeader();
|
|
1313
|
+
// #936: while the document is still parsing, the closing `<!--/wj:children-->`
|
|
1314
|
+
// marker at the bottom of the body may not exist yet, so `buildHaveHeader()`
|
|
1315
|
+
// returns '' meaning "markers not parsed yet", NOT "this page has no layout".
|
|
1316
|
+
// A touch-device viewport prefetch fires early enough (mid-parse) to hit this
|
|
1317
|
+
// window on real Android Chrome. Caching that empty-`have` response (a full
|
|
1318
|
+
// page) would later drive the destructive full-body swap fallback. Skip the
|
|
1319
|
+
// speculative fetch; the click path re-fetches with a correct `have` once the
|
|
1320
|
+
// document has parsed (and applySwap now falls back to a full load anyway).
|
|
1321
|
+
if (!have && typeof document !== 'undefined' && document.readyState === 'loading') return;
|
|
1322
|
+
|
|
1282
1323
|
prefetchInflight.add(key);
|
|
1283
1324
|
const headers = { 'x-webjs-router': '1', 'x-webjs-prefetch': '1' };
|
|
1284
|
-
const have = buildHaveHeader();
|
|
1285
1325
|
if (have) headers['x-webjs-have'] = have;
|
|
1286
1326
|
|
|
1287
1327
|
fetch(href, { method: 'GET', headers, credentials: 'same-origin' })
|
|
@@ -2470,8 +2510,14 @@ function applySwap(doc, frameId, revalidating, href, incomingBuild, incomingSrc)
|
|
|
2470
2510
|
return;
|
|
2471
2511
|
}
|
|
2472
2512
|
|
|
2473
|
-
// 3. Full body swap fallback
|
|
2474
|
-
//
|
|
2513
|
+
// 3. Full body swap fallback: no shared layout marker (a genuine root-layout
|
|
2514
|
+
// change, or a same-layout nav whose markers could not be paired). Full head
|
|
2515
|
+
// merge, so a real root-layout change removes stale head elements. `mergeHead`
|
|
2516
|
+
// now PRESERVES stylesheets and `<style>` unconditionally (#936): even if the
|
|
2517
|
+
// client reaches this path with an incoming head that lacks the app's
|
|
2518
|
+
// `<link rel=stylesheet>` (a partial or mangled response, e.g. from a
|
|
2519
|
+
// mid-parse empty-`have` prefetch), the live CSS is never stripped, so the
|
|
2520
|
+
// swap can no longer leave the page unstyled.
|
|
2475
2521
|
mergeHead(doc.head);
|
|
2476
2522
|
// Persist permanent elements by node identity across the full-body
|
|
2477
2523
|
// swap: move each live [data-webjs-permanent][id] node into the matching
|
|
@@ -3214,6 +3260,31 @@ function addNewHeadElements(newHead) {
|
|
|
3214
3260
|
}
|
|
3215
3261
|
}
|
|
3216
3262
|
|
|
3263
|
+
/**
|
|
3264
|
+
* Is `el` a stylesheet the head merge must never remove: a `<style>` or a
|
|
3265
|
+
* `<link rel~="stylesheet">`. WebJs ALWAYS keeps these on a soft nav, with no
|
|
3266
|
+
* opt-out, and that is a deliberate divergence from Turbo. Turbo removes a
|
|
3267
|
+
* stylesheet absent from the new head when it is tagged
|
|
3268
|
+
* `data-turbo-track="dynamic"`, which is sound in Turbo because a Turbo visit
|
|
3269
|
+
* always compares a COMPLETE old head to a COMPLETE new head, so "absent" means
|
|
3270
|
+
* "this page removed it". WebJs's `X-Webjs-Have` optimization returns a REDUCED
|
|
3271
|
+
* head (the shared app stylesheet is omitted because the client already has it),
|
|
3272
|
+
* so "absent from the incoming head" means "optimized away", NOT "removed". A
|
|
3273
|
+
* dynamic-removal opt-out would therefore re-introduce #936 (it would strip a
|
|
3274
|
+
* still-needed sheet on any partial response), and WebJs is Tailwind-first (one
|
|
3275
|
+
* global sheet, no page-specific sheets to drop), so the knob would be unsafe
|
|
3276
|
+
* and unused. Keeping every stylesheet is correct here; a genuinely changed one
|
|
3277
|
+
* is dropped by the deploy-level hard reload (build-id mismatch), not a soft swap.
|
|
3278
|
+
*
|
|
3279
|
+
* @param {Element} el
|
|
3280
|
+
* @returns {boolean}
|
|
3281
|
+
*/
|
|
3282
|
+
function isPersistentHeadStyle(el) {
|
|
3283
|
+
if (el.tagName === 'STYLE') return true;
|
|
3284
|
+
return el.tagName === 'LINK' &&
|
|
3285
|
+
(el.getAttribute('rel') || '').toLowerCase().split(/\s+/).includes('stylesheet');
|
|
3286
|
+
}
|
|
3287
|
+
|
|
3217
3288
|
/** @param {HTMLHeadElement} newHead */
|
|
3218
3289
|
function mergeHead(newHead) {
|
|
3219
3290
|
const currentHead = document.head;
|
|
@@ -3239,6 +3310,14 @@ function mergeHead(newHead) {
|
|
|
3239
3310
|
if (el.tagName === 'SCRIPT' && el.getAttribute('type') === 'importmap') continue;
|
|
3240
3311
|
if (el.tagName === 'BASE') continue;
|
|
3241
3312
|
if (el.tagName === 'TITLE') continue;
|
|
3313
|
+
// #936: NEVER remove a stylesheet or a `<style>` on a soft nav (Turbo's
|
|
3314
|
+
// persistent-CSS model). The incoming head of a full-body-swap fallback can
|
|
3315
|
+
// legitimately lack the app's `<link rel=stylesheet>` (a partial or mangled
|
|
3316
|
+
// response, e.g. from a mid-parse empty-`have` prefetch): removing the live
|
|
3317
|
+
// one there leaves the whole page unstyled until a manual refresh, the
|
|
3318
|
+
// headline #936 symptom. Keeping it is safe: a genuinely stale sheet is
|
|
3319
|
+
// dropped by the deploy-level hard reload (build-id mismatch), not here.
|
|
3320
|
+
if (isPersistentHeadStyle(el)) continue;
|
|
3242
3321
|
if (!newSet.has(outerHTMLForDiff(el))) el.remove();
|
|
3243
3322
|
}
|
|
3244
3323
|
|
|
@@ -3473,11 +3552,13 @@ export {
|
|
|
3473
3552
|
clearFormBusy as _clearFormBusy,
|
|
3474
3553
|
collectChildrenSlots as _collectChildrenSlots,
|
|
3475
3554
|
longestSharedPath as _longestSharedPath,
|
|
3555
|
+
parseHTML as _parseHTML,
|
|
3476
3556
|
keyOf as _keyOf,
|
|
3477
3557
|
diffElementInPlace as _diffElementInPlace,
|
|
3478
3558
|
reconcileChildren as _reconcileChildren,
|
|
3479
3559
|
onPopState as _onPopState,
|
|
3480
3560
|
applySwap as _applySwap,
|
|
3561
|
+
buildHaveHeader as _buildHaveHeader,
|
|
3481
3562
|
snapshotCache as _snapshotCache,
|
|
3482
3563
|
prefetchCache as _prefetchCache,
|
|
3483
3564
|
LIVE_ATTRS as _LIVE_ATTRS,
|