@webjsdev/core 0.7.1 → 0.7.2
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 +34 -2
- package/dist/webjs-core-browser.js +4 -0
- package/dist/webjs-core-browser.js.map +6 -0
- package/dist/webjs-core-lazy-loader.js +2 -0
- package/dist/webjs-core-lazy-loader.js.map +6 -0
- package/dist/webjs-core-testing.js +3 -0
- package/dist/webjs-core-testing.js.map +6 -0
- package/dist/webjs-core.js +4 -0
- package/dist/webjs-core.js.map +6 -0
- package/index-browser.js +58 -0
- package/index.js +1 -0
- package/package.json +41 -7
- package/src/component.js +8 -0
- package/src/csp-nonce.js +50 -0
- package/src/registry.js +1 -1
- package/src/render-server.js +10 -1
- package/src/router-client.js +307 -37
package/src/router-client.js
CHANGED
|
@@ -596,7 +596,7 @@ async function performNavigation(href, isPopState, frameId) {
|
|
|
596
596
|
if (cached) {
|
|
597
597
|
const cachedDoc = parseHTML(cached.html);
|
|
598
598
|
if (cachedDoc) {
|
|
599
|
-
applySwap(cachedDoc, frameId, /* revalidating */ true);
|
|
599
|
+
applySwap(cachedDoc, frameId, /* revalidating */ true, /* href */ null);
|
|
600
600
|
// Restore window scroll to where the user left it.
|
|
601
601
|
if (typeof window !== 'undefined') {
|
|
602
602
|
window.scrollTo(cached.scrollX, cached.scrollY);
|
|
@@ -734,6 +734,7 @@ async function fetchAndApply(href, frameId, recordHistory, optimisticState, meth
|
|
|
734
734
|
method = method || 'GET';
|
|
735
735
|
const myToken = typeof token === 'number' ? token : currentNavigationToken;
|
|
736
736
|
let html;
|
|
737
|
+
let incomingBuild = null;
|
|
737
738
|
/** @type {string} */
|
|
738
739
|
let finalUrl = href;
|
|
739
740
|
try {
|
|
@@ -782,6 +783,12 @@ async function fetchAndApply(href, frameId, recordHistory, optimisticState, meth
|
|
|
782
783
|
// (turbo/src/core/drive/navigator.js:92-107). Critical for the
|
|
783
784
|
// standard server-rendered validation pattern: 422 + re-rendered
|
|
784
785
|
// form with errors keeps the user's typed input and shows context.
|
|
786
|
+
// Capture the server's build hash header BEFORE reading the body.
|
|
787
|
+
// The header is set on every SSR response, including X-Webjs-Have
|
|
788
|
+
// partial responses where the body has no head and no importmap
|
|
789
|
+
// tag to compare. The applySwap importmap-mismatch guard reads
|
|
790
|
+
// this to detect deploys that bumped the vendor pin.
|
|
791
|
+
incomingBuild = resp.headers.get('x-webjs-build');
|
|
785
792
|
html = await resp.text();
|
|
786
793
|
} catch (err) {
|
|
787
794
|
// Aborted by a newer navigation: let it run, don't fall back.
|
|
@@ -801,7 +808,7 @@ async function fetchAndApply(href, frameId, recordHistory, optimisticState, meth
|
|
|
801
808
|
const doc = parseHTML(html);
|
|
802
809
|
if (!doc) { location.href = href; return; }
|
|
803
810
|
|
|
804
|
-
applySwap(doc, frameId, false);
|
|
811
|
+
applySwap(doc, frameId, false, finalUrl, incomingBuild);
|
|
805
812
|
|
|
806
813
|
if (recordHistory) history.pushState(null, '', finalUrl);
|
|
807
814
|
|
|
@@ -834,11 +841,173 @@ async function fetchAndApply(href, frameId, recordHistory, optimisticState, meth
|
|
|
834
841
|
* Picks the most-scoped match: explicit webjs-frame > deepest shared
|
|
835
842
|
* layout marker > full body swap.
|
|
836
843
|
*
|
|
844
|
+
* If the incoming page carries a different importmap from the current
|
|
845
|
+
* page (typical after a deploy that bumped a vendor pin), partial swap
|
|
846
|
+
* is unsafe: importmaps are immutable once applied, so the new page
|
|
847
|
+
* would resolve modules against the stale URLs. We fall back to a full
|
|
848
|
+
* page load via `location.assign(href)`. Mirrors Turbo's
|
|
849
|
+
* `tracked_element_mismatch` reload, applied specifically to
|
|
850
|
+
* importmaps. Called with `href = null` for revalidation flows (which
|
|
851
|
+
* never trigger a hard reload).
|
|
852
|
+
*
|
|
853
|
+
* Detection uses the `X-Webjs-Build` response header (read by the
|
|
854
|
+
* fetch path and passed in as `incomingBuild`), compared against the
|
|
855
|
+
* current page's `data-webjs-build`. The header is set on EVERY SSR
|
|
856
|
+
* response, including X-Webjs-Have partial responses that omit the
|
|
857
|
+
* head and importmap entirely, and it carries the PUBLISHED build id,
|
|
858
|
+
* which the server advertises only once the importmap is final. A hard
|
|
859
|
+
* reload fires only when both ids are present and differ (a real
|
|
860
|
+
* cross-deploy). An empty / absent id on either side means "version
|
|
861
|
+
* unknown" (a warming runtime-first-boot server, or a response that
|
|
862
|
+
* predates the header) and never triggers a reload, so the warmup
|
|
863
|
+
* window cannot wipe a half-filled form.
|
|
864
|
+
*
|
|
837
865
|
* @param {Document} doc
|
|
838
866
|
* @param {string | null} frameId
|
|
839
|
-
* @param {boolean} revalidating Restore from cache
|
|
867
|
+
* @param {boolean} revalidating Restore from cache; already-matched markers may stomp inflight state, signal helps loading templates skip.
|
|
868
|
+
* @param {string | null} [href] Target URL for hard-reload fallback on importmap mismatch.
|
|
869
|
+
* @param {string | null} [incomingBuild] X-Webjs-Build header from the response, or null.
|
|
870
|
+
*/
|
|
871
|
+
/**
|
|
872
|
+
* Compute the signature of all `data-webjs-track="reload"` elements
|
|
873
|
+
* in the head of `root`. Returns the concatenation of each element's
|
|
874
|
+
* `outerHTML`, in document order. Two documents with identical
|
|
875
|
+
* tracked-element sets produce identical signatures; any change in
|
|
876
|
+
* attributes, content, or set membership produces a different one.
|
|
877
|
+
*
|
|
878
|
+
* Mirrors hotwired/turbo's `head_snapshot.js` `trackedElementSignature`
|
|
879
|
+
* (the data-turbo-track="reload" mechanism). Used by applySwap as a
|
|
880
|
+
* generic opt-in next to the importmap-specific build hash.
|
|
881
|
+
*
|
|
882
|
+
* Returns the empty string when `root` has no head (e.g. an
|
|
883
|
+
* X-Webjs-Have partial response) or when no elements opt in.
|
|
884
|
+
*
|
|
885
|
+
* @param {Document | undefined} root
|
|
886
|
+
* @returns {string}
|
|
840
887
|
*/
|
|
841
|
-
function
|
|
888
|
+
function trackedReloadSignature(root) {
|
|
889
|
+
if (!root || !root.head) return '';
|
|
890
|
+
const tracked = root.head.querySelectorAll('[data-webjs-track="reload"]');
|
|
891
|
+
if (!tracked.length) return '';
|
|
892
|
+
// Use outerHTMLForDiff so the CSP nonce (which rotates per
|
|
893
|
+
// request) is stripped before signature comparison. Without this,
|
|
894
|
+
// a nonced tracked script like `<script nonce="${cspNonce()}"
|
|
895
|
+
// data-webjs-track="reload" src="/build.js?v=42">` would mismatch
|
|
896
|
+
// every navigation and infinite-reload. Matches Turbo's
|
|
897
|
+
// head_snapshot.js elementWithoutNonce posture.
|
|
898
|
+
let sig = '';
|
|
899
|
+
for (const el of tracked) sig += outerHTMLForDiff(el);
|
|
900
|
+
return sig;
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
function applySwap(doc, frameId, revalidating, href, incomingBuild) {
|
|
904
|
+
// Any clean swap (no importmap mismatch, including cache restores
|
|
905
|
+
// and frame swaps where we don't even run the mismatch check) is a
|
|
906
|
+
// signal that the user successfully navigated, so clear the reload
|
|
907
|
+
// flag. Otherwise a sequence "reload because of mismatch → Back to
|
|
908
|
+
// a cache restore → Forward to a deploy-bumped URL" would find the
|
|
909
|
+
// stale flag still set and suppress the second legitimate reload.
|
|
910
|
+
try {
|
|
911
|
+
if (typeof sessionStorage !== 'undefined' && (!href || frameId || revalidating)) {
|
|
912
|
+
sessionStorage.removeItem('webjs:importmap-reload');
|
|
913
|
+
}
|
|
914
|
+
} catch { /* ignore */ }
|
|
915
|
+
|
|
916
|
+
// Importmap-mismatch guard. Only fires for foreground navs (href
|
|
917
|
+
// present); revalidation passes href=null to keep cache restores
|
|
918
|
+
// soft. Skipped if a <webjs-frame> escape hatch is in play (frame
|
|
919
|
+
// swaps are intra-page and don't change the importmap).
|
|
920
|
+
if (href && !frameId && !revalidating) {
|
|
921
|
+
const currentTag = document.querySelector('script[type="importmap"]');
|
|
922
|
+
const currentBuild = currentTag ? currentTag.getAttribute('data-webjs-build') : null;
|
|
923
|
+
let mismatch = false;
|
|
924
|
+
if (incomingBuild && currentBuild) {
|
|
925
|
+
// Preferred path: compare per-response build id. Works even
|
|
926
|
+
// when the response body has no importmap (partial swap).
|
|
927
|
+
mismatch = incomingBuild !== currentBuild;
|
|
928
|
+
}
|
|
929
|
+
// An empty / absent build id on EITHER side means "version unknown":
|
|
930
|
+
// the server has not published an authoritative importmap yet (the
|
|
931
|
+
// warmup window, where a runtime-first-boot app resolves its vendor
|
|
932
|
+
// map over the first request), or the response predates the build
|
|
933
|
+
// header. In that state a hard reload is unsafe and destructive: it
|
|
934
|
+
// would fire repeatedly as the warming server's id flips from empty
|
|
935
|
+
// to its final value, wiping any half-filled form on the page. So we
|
|
936
|
+
// never hard-reload against an unknown id and leave `mismatch` false;
|
|
937
|
+
// the soft swap proceeds and the page settles once the server is
|
|
938
|
+
// warm. A real cross-deploy reload still fires, because both sides
|
|
939
|
+
// then carry non-empty, differing ids. (No importmap-textContent
|
|
940
|
+
// fallback: the published-id contract above supersedes it, and the
|
|
941
|
+
// textContent of a warming map drifts for the same reason the id does.)
|
|
942
|
+
// Generic `data-webjs-track="reload"` opt-in. ANY element in the
|
|
943
|
+
// head that the user marks gets included in the tracked-element
|
|
944
|
+
// signature. If the signature differs between current and incoming
|
|
945
|
+
// documents, hard-reload. Mirrors hotwired/turbo's
|
|
946
|
+
// data-turbo-track="reload" semantics (head_snapshot.js
|
|
947
|
+
// trackedElementSignature). Lets app authors tag arbitrary
|
|
948
|
+
// version-sensitive elements (CSS bundle <link>, deploy meta tag)
|
|
949
|
+
// for cross-deploy reload, not just the importmap.
|
|
950
|
+
//
|
|
951
|
+
// Importmap-specific data-webjs-build / X-Webjs-Build remain the
|
|
952
|
+
// primary mechanism because they ALSO work on partial responses
|
|
953
|
+
// (no head in the body). data-webjs-track is for elements that
|
|
954
|
+
// can't ride the build hash.
|
|
955
|
+
//
|
|
956
|
+
// Skip the check when the incoming response has no head content
|
|
957
|
+
// (X-Webjs-Have partial-fragment response). Without this guard
|
|
958
|
+
// a partial response would always mismatch any current tracked
|
|
959
|
+
// signature and falsely reload. With the guard, a partial
|
|
960
|
+
// response means "trust the build hash; don't decide based on
|
|
961
|
+
// missing head info." Comparing on full responses also catches
|
|
962
|
+
// added/removed track markers because empty `incomingSig`
|
|
963
|
+
// would correctly differ from a non-empty `currentSig`.
|
|
964
|
+
if (!mismatch && doc.head && doc.head.children.length > 0) {
|
|
965
|
+
const currentSig = trackedReloadSignature(document);
|
|
966
|
+
const incomingSig = trackedReloadSignature(doc);
|
|
967
|
+
if (currentSig !== incomingSig) mismatch = true;
|
|
968
|
+
}
|
|
969
|
+
if (mismatch && typeof location !== 'undefined') {
|
|
970
|
+
// Infinite-reload guard: if the importmap appears to genuinely
|
|
971
|
+
// change EVERY navigation (e.g. a developer is live-editing the
|
|
972
|
+
// pin file in dev, or a misbehaving CDN returns different
|
|
973
|
+
// jspm.io URLs each request), the user would experience a hard
|
|
974
|
+
// reload on every click. Use a one-shot sessionStorage flag:
|
|
975
|
+
// set before the first reload, cleared by the next successful
|
|
976
|
+
// swap. Two reloads BACK-TO-BACK (without an intervening clean
|
|
977
|
+
// nav) trip the guard.
|
|
978
|
+
try {
|
|
979
|
+
const flag = 'webjs:importmap-reload';
|
|
980
|
+
if (sessionStorage && sessionStorage.getItem(flag)) {
|
|
981
|
+
// Already reloaded once for an importmap mismatch and the
|
|
982
|
+
// next nav STILL mismatches: bail to the partial swap. The
|
|
983
|
+
// user is on a stale importmap but at least the page
|
|
984
|
+
// renders.
|
|
985
|
+
sessionStorage.removeItem(flag);
|
|
986
|
+
} else {
|
|
987
|
+
if (sessionStorage) sessionStorage.setItem(flag, '1');
|
|
988
|
+
location.href = href;
|
|
989
|
+
return;
|
|
990
|
+
}
|
|
991
|
+
} catch {
|
|
992
|
+
// sessionStorage unavailable (private mode w/ quota etc.):
|
|
993
|
+
// fall through to a single reload like before.
|
|
994
|
+
location.href = href;
|
|
995
|
+
return;
|
|
996
|
+
}
|
|
997
|
+
} else if (!mismatch) {
|
|
998
|
+
// A clean swap (no importmap mismatch) means we're back to
|
|
999
|
+
// matching client/server importmaps. Clear the reload flag so
|
|
1000
|
+
// a future LEGITIMATE mismatch (e.g. a later deploy) gets a
|
|
1001
|
+
// fresh single-shot reload instead of being suppressed by a
|
|
1002
|
+
// stale flag from an unrelated earlier reload.
|
|
1003
|
+
try {
|
|
1004
|
+
if (typeof sessionStorage !== 'undefined') {
|
|
1005
|
+
sessionStorage.removeItem('webjs:importmap-reload');
|
|
1006
|
+
}
|
|
1007
|
+
} catch { /* ignore */ }
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
|
|
842
1011
|
// 1. webjs-frame escape hatch.
|
|
843
1012
|
if (frameId) {
|
|
844
1013
|
const target = document.querySelector(`webjs-frame#${CSS.escape(frameId)}`);
|
|
@@ -1256,41 +1425,146 @@ function diffChildren(dst, src) {
|
|
|
1256
1425
|
*
|
|
1257
1426
|
* @param {HTMLHeadElement} newHead
|
|
1258
1427
|
*/
|
|
1428
|
+
|
|
1429
|
+
/**
|
|
1430
|
+
* Read the CSP nonce that the original page load published via
|
|
1431
|
+
* `<meta name="csp-nonce" content="...">`. Returns empty string when
|
|
1432
|
+
* no meta tag is present (apps without strict CSP).
|
|
1433
|
+
*
|
|
1434
|
+
* The meta tag is the contract: server emits it once at SSR time,
|
|
1435
|
+
* client reads it for every dynamically-created script. The browser
|
|
1436
|
+
* enforces CSP against the nonce the original page declared, NOT the
|
|
1437
|
+
* per-request nonce on subsequent navigations. So we always apply
|
|
1438
|
+
* THIS nonce, not the source-page nonce that arrived with the new
|
|
1439
|
+
* head fragment.
|
|
1440
|
+
*
|
|
1441
|
+
* Mirrors hotwired/turbo's `getCspNonce` in src/util.js. Not cached:
|
|
1442
|
+
* a single querySelector on document.head is cheap, and caching
|
|
1443
|
+
* would break if the user (or a test) inserted the meta tag late.
|
|
1444
|
+
*
|
|
1445
|
+
* @returns {string}
|
|
1446
|
+
*/
|
|
1447
|
+
function getCspNonce() {
|
|
1448
|
+
if (typeof document === 'undefined') return '';
|
|
1449
|
+
const meta = document.querySelector('meta[name="csp-nonce"]');
|
|
1450
|
+
// Read the `content` attribute, not the `.nonce` IDL property.
|
|
1451
|
+
// Turbo's getCspNonce in src/util.js falls back to `.nonce` first
|
|
1452
|
+
// because it can be called against script/link elements (where
|
|
1453
|
+
// browsers DO expose `.nonce` and additionally clear the
|
|
1454
|
+
// `nonce` attribute on document load). The `<meta name="csp-nonce">`
|
|
1455
|
+
// element webjs targets has no `.nonce` IDL (only script + link
|
|
1456
|
+
// elements do per HTML spec), so the only viable source is the
|
|
1457
|
+
// `content` attribute.
|
|
1458
|
+
return meta ? meta.getAttribute('content') || '' : '';
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1461
|
+
/**
|
|
1462
|
+
* Create a `<script>` clone of `source` that's safe to insert into the
|
|
1463
|
+
* live document under strict CSP. Copies every attribute EXCEPT
|
|
1464
|
+
* nonce (the source's nonce is from the new page's per-request token,
|
|
1465
|
+
* which the browser's CSP cache from the original page load will
|
|
1466
|
+
* reject), then applies the cached nonce from the meta tag. Re-emits
|
|
1467
|
+
* textContent so inline scripts execute as if first-loaded.
|
|
1468
|
+
*
|
|
1469
|
+
* @param {HTMLScriptElement} source
|
|
1470
|
+
* @returns {HTMLScriptElement}
|
|
1471
|
+
*/
|
|
1472
|
+
function cloneScriptWithCorrectNonce(source) {
|
|
1473
|
+
const script = document.createElement('script');
|
|
1474
|
+
for (const attr of source.attributes) {
|
|
1475
|
+
if (attr.name === 'nonce') continue;
|
|
1476
|
+
script.setAttribute(attr.name, attr.value);
|
|
1477
|
+
}
|
|
1478
|
+
const nonce = getCspNonce();
|
|
1479
|
+
if (nonce) {
|
|
1480
|
+
// Use setAttribute so the attribute is queryable
|
|
1481
|
+
// (`getAttribute('nonce')`, outerHTML serialization, etc.).
|
|
1482
|
+
// Per CSP3 the .nonce IDL property is the authoritative source
|
|
1483
|
+
// for the CSP check, but real browsers reflect setAttribute into
|
|
1484
|
+
// .nonce automatically. Test environments (linkedom) reflect only
|
|
1485
|
+
// one direction, so we set the attribute.
|
|
1486
|
+
script.setAttribute('nonce', nonce);
|
|
1487
|
+
}
|
|
1488
|
+
script.textContent = source.textContent;
|
|
1489
|
+
return script;
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1492
|
+
/**
|
|
1493
|
+
* Clone any head element while substituting the page-load CSP nonce
|
|
1494
|
+
* for the source's per-request nonce. Used for `<link rel="modulepreload"
|
|
1495
|
+
* nonce="...">` and any other nonce-carrying head element: browsers
|
|
1496
|
+
* gate cross-origin module preload by script-src nonce too, so the
|
|
1497
|
+
* per-request nonce from the new page's head would be blocked by the
|
|
1498
|
+
* browser's CSP cache from the original page load.
|
|
1499
|
+
*
|
|
1500
|
+
* Returns a cloneNode(true) for elements without a nonce attribute,
|
|
1501
|
+
* so non-CSP cases stay zero-cost.
|
|
1502
|
+
*
|
|
1503
|
+
* @param {Element} source
|
|
1504
|
+
* @returns {Element}
|
|
1505
|
+
*/
|
|
1506
|
+
function cloneElementWithCorrectNonce(source) {
|
|
1507
|
+
if (!source.hasAttribute('nonce')) return source.cloneNode(true);
|
|
1508
|
+
const clone = /** @type {Element} */ (source.cloneNode(true));
|
|
1509
|
+
const nonce = getCspNonce();
|
|
1510
|
+
if (nonce) {
|
|
1511
|
+
clone.setAttribute('nonce', nonce);
|
|
1512
|
+
} else {
|
|
1513
|
+
clone.removeAttribute('nonce');
|
|
1514
|
+
}
|
|
1515
|
+
return clone;
|
|
1516
|
+
}
|
|
1517
|
+
|
|
1518
|
+
/**
|
|
1519
|
+
* Return an `outerHTML` string suitable for head-diff comparison: strip
|
|
1520
|
+
* any nonce attribute so per-request nonces don't cause every script in
|
|
1521
|
+
* the head to look "changed" on every navigation. The original element
|
|
1522
|
+
* is left untouched (we clone first).
|
|
1523
|
+
*
|
|
1524
|
+
* Mirrors hotwired/turbo's `elementWithoutNonce` pattern in
|
|
1525
|
+
* src/core/drive/head_snapshot.js.
|
|
1526
|
+
*
|
|
1527
|
+
* @param {Element} el
|
|
1528
|
+
* @returns {string}
|
|
1529
|
+
*/
|
|
1530
|
+
function outerHTMLForDiff(el) {
|
|
1531
|
+
// Strip nonce from ANY element type. SCRIPT obviously, but also LINK
|
|
1532
|
+
// (modulepreload tags carry nonce per the recent CSP fix). Without
|
|
1533
|
+
// this, per-request nonces on link tags would cause the diff to
|
|
1534
|
+
// treat every preload as "changed", duplicating preloads on every
|
|
1535
|
+
// navigation.
|
|
1536
|
+
if (!el.hasAttribute('nonce')) return el.outerHTML;
|
|
1537
|
+
const clone = /** @type {Element} */ (el.cloneNode(true));
|
|
1538
|
+
clone.removeAttribute('nonce');
|
|
1539
|
+
return clone.outerHTML;
|
|
1540
|
+
}
|
|
1541
|
+
|
|
1259
1542
|
function addNewHeadElements(newHead) {
|
|
1260
1543
|
const newTitle = newHead.querySelector('title');
|
|
1261
1544
|
if (newTitle) document.title = newTitle.textContent || '';
|
|
1262
1545
|
|
|
1263
1546
|
const currentSet = new Set();
|
|
1264
|
-
for (const el of document.head.children) currentSet.add(el
|
|
1547
|
+
for (const el of document.head.children) currentSet.add(outerHTMLForDiff(el));
|
|
1265
1548
|
|
|
1266
1549
|
for (const el of newHead.children) {
|
|
1267
1550
|
if (el.tagName === 'SCRIPT' && el.getAttribute('type') === 'importmap') {
|
|
1268
1551
|
// Skip: partial swaps keep the outer layout mounted, so the
|
|
1269
|
-
// existing importmap stays authoritative.
|
|
1270
|
-
//
|
|
1271
|
-
//
|
|
1272
|
-
//
|
|
1273
|
-
//
|
|
1274
|
-
const incoming = (el.textContent || '').trim();
|
|
1275
|
-
if (incoming) {
|
|
1276
|
-
const current = document.querySelector('script[type="importmap"]');
|
|
1277
|
-
const currentText = current ? (current.textContent || '').trim() : '';
|
|
1278
|
-
if (currentText && currentText !== incoming && typeof console !== 'undefined') {
|
|
1279
|
-
console.warn('[webjs] incoming page has a different importmap; keeping the current page\'s map. Cross-layout module aliasing may be inconsistent.');
|
|
1280
|
-
}
|
|
1281
|
-
}
|
|
1552
|
+
// existing importmap stays authoritative. Importmaps are
|
|
1553
|
+
// immutable once a script has run (modern browsers ignore
|
|
1554
|
+
// subsequent `<script type=importmap>`). Importmap-mismatch
|
|
1555
|
+
// detection lives at the applySwap entry: a mismatch there
|
|
1556
|
+
// triggers a full reload before we ever reach this loop.
|
|
1282
1557
|
continue;
|
|
1283
1558
|
}
|
|
1284
1559
|
if (el.tagName === 'BASE') continue;
|
|
1285
1560
|
if (el.tagName === 'TITLE') continue;
|
|
1286
|
-
if (!currentSet.has(el
|
|
1561
|
+
if (!currentSet.has(outerHTMLForDiff(el))) {
|
|
1287
1562
|
if (el.tagName === 'SCRIPT') {
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
document.head.appendChild(script);
|
|
1563
|
+
document.head.appendChild(
|
|
1564
|
+
cloneScriptWithCorrectNonce(/** @type {HTMLScriptElement} */ (el)),
|
|
1565
|
+
);
|
|
1292
1566
|
} else {
|
|
1293
|
-
document.head.appendChild(el
|
|
1567
|
+
document.head.appendChild(cloneElementWithCorrectNonce(el));
|
|
1294
1568
|
}
|
|
1295
1569
|
}
|
|
1296
1570
|
}
|
|
@@ -1307,35 +1581,34 @@ function mergeHead(newHead) {
|
|
|
1307
1581
|
for (const el of currentHead.children) {
|
|
1308
1582
|
if (el.tagName === 'SCRIPT' && el.getAttribute('type') === 'importmap') continue;
|
|
1309
1583
|
if (el.tagName === 'BASE') continue;
|
|
1310
|
-
currentSet.add(el
|
|
1584
|
+
currentSet.add(outerHTMLForDiff(el));
|
|
1311
1585
|
}
|
|
1312
1586
|
|
|
1313
1587
|
const newSet = new Set();
|
|
1314
1588
|
for (const el of newHead.children) {
|
|
1315
1589
|
if (el.tagName === 'SCRIPT' && el.getAttribute('type') === 'importmap') continue;
|
|
1316
1590
|
if (el.tagName === 'BASE') continue;
|
|
1317
|
-
newSet.add(el
|
|
1591
|
+
newSet.add(outerHTMLForDiff(el));
|
|
1318
1592
|
}
|
|
1319
1593
|
|
|
1320
1594
|
for (const el of [...currentHead.children]) {
|
|
1321
1595
|
if (el.tagName === 'SCRIPT' && el.getAttribute('type') === 'importmap') continue;
|
|
1322
1596
|
if (el.tagName === 'BASE') continue;
|
|
1323
1597
|
if (el.tagName === 'TITLE') continue;
|
|
1324
|
-
if (!newSet.has(el
|
|
1598
|
+
if (!newSet.has(outerHTMLForDiff(el))) el.remove();
|
|
1325
1599
|
}
|
|
1326
1600
|
|
|
1327
1601
|
for (const el of newHead.children) {
|
|
1328
1602
|
if (el.tagName === 'SCRIPT' && el.getAttribute('type') === 'importmap') continue;
|
|
1329
1603
|
if (el.tagName === 'BASE') continue;
|
|
1330
1604
|
if (el.tagName === 'TITLE') continue;
|
|
1331
|
-
if (!currentSet.has(el
|
|
1605
|
+
if (!currentSet.has(outerHTMLForDiff(el))) {
|
|
1332
1606
|
if (el.tagName === 'SCRIPT') {
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
currentHead.appendChild(script);
|
|
1607
|
+
currentHead.appendChild(
|
|
1608
|
+
cloneScriptWithCorrectNonce(/** @type {HTMLScriptElement} */ (el)),
|
|
1609
|
+
);
|
|
1337
1610
|
} else {
|
|
1338
|
-
currentHead.appendChild(el
|
|
1611
|
+
currentHead.appendChild(cloneElementWithCorrectNonce(el));
|
|
1339
1612
|
}
|
|
1340
1613
|
}
|
|
1341
1614
|
}
|
|
@@ -1380,10 +1653,7 @@ function forwardSuspenseResolvers(fetchedBody) {
|
|
|
1380
1653
|
/** @param {Element} container */
|
|
1381
1654
|
function reactivateScripts(container) {
|
|
1382
1655
|
for (const old of container.querySelectorAll('script')) {
|
|
1383
|
-
|
|
1384
|
-
for (const attr of old.attributes) script.setAttribute(attr.name, attr.value);
|
|
1385
|
-
script.textContent = old.textContent;
|
|
1386
|
-
old.replaceWith(script);
|
|
1656
|
+
old.replaceWith(cloneScriptWithCorrectNonce(/** @type {HTMLScriptElement} */ (old)));
|
|
1387
1657
|
}
|
|
1388
1658
|
}
|
|
1389
1659
|
|