From 60faff1a4893df36f9a52e911b64bae19208b994 Mon Sep 17 00:00:00 2001 From: Doxa Maps Date: Sat, 26 Sep 2026 03:49:03 +0000 Subject: [PATCH] PPLR data maps: loading d3 no longer writes into the host page's own d3 4c45d76c put the host's window.d3 back after loading the pinned d3, but d3's UMD build writes its exports INTO an existing window.d3 ((t = globalThis).d3 = t.d3 || {}). By the time the host's object was restored, ours had been written into it: rega measured 577 added keys and version '7.9.0' on a host's own d3. A host on d3 v6 would get v7 functions. While our script executes (document.currentScript === the element we appended), window.d3 now reads and writes a fresh slot of ours. Any other code, at every moment, reads and writes the host's own value, so nothing is blanked while the file loads. The host's value is then restored as a plain data property. A non-configurable global (a page-level `var d3`) cannot be trapped, so it is emptied for the load instead. Headless, with the real d3@7.9.0 file (its sha384 matches the pinned SRI), run in a vm realm with document.currentScript set as a browser does: before host owns window.d3: keys 2 -> 579, version -> 7.9.0 (rega's finding) after host owns window.d3: keys 2 -> 2, version host-7.0.0, same object; PPLR gets its own d3 7.9.0; the host sees its own d3 during the load after no host d3: window.d3 absent afterwards, as before The vm realm makes `var d3` configurable, so the non-configurable fallback was not exercised there. The probe is kept at _qa-cycle/exp-shepherd/d3-probe/. --- .../pplr-data-maps/src/utils/pplrData.js | 48 +++++++++++++++---- 1 file changed, 40 insertions(+), 8 deletions(-) diff --git a/plugin-designs/pplr-data-maps/src/utils/pplrData.js b/plugin-designs/pplr-data-maps/src/utils/pplrData.js index fd8346b20..78ae2c223 100644 --- a/plugin-designs/pplr-data-maps/src/utils/pplrData.js +++ b/plugin-designs/pplr-data-maps/src/utils/pplrData.js @@ -24,19 +24,51 @@ export function ensureD3() { if (d3Lib) return Promise.resolve(d3Lib) if (typeof window === 'undefined') return Promise.reject(new Error('no window')) if (!d3Promise) d3Promise = new Promise((res, rej) => { - const hadOwn = Object.prototype.hasOwnProperty.call(window, 'd3') - const hostD3 = window.d3 const s = document.createElement('script') s.src = D3_URL s.integrity = D3_SRI s.crossOrigin = 'anonymous' - s.onload = () => { - d3Lib = window.d3 - if (hadOwn) window.d3 = hostD3 - else { try { delete window.d3 } catch (_) { window.d3 = undefined } } - res(d3Lib) + // ★ PUTTING THE HOST'S window.d3 BACK IS NOT ENOUGH. d3's UMD build writes its exports INTO an + // existing window.d3 — `(t = globalThis).d3 = t.d3 || {}` — so a host that owns a d3 had ours + // written over it before we restored it: rega measured 577 added keys and version '7.9.0' on a + // host's own object (a host on d3 v6 would get v7 functions). So while OUR script executes + // (document.currentScript === s), window.d3 reads and writes a fresh slot of ours; any other code, + // at every moment, reads and writes the host's own value. Nothing is blanked while the file loads. + const own = Object.getOwnPropertyDescriptor(window, 'd3') + let hostD3 = window.d3 + let ours + let trapped = false + if (!own || (own.configurable && 'value' in own)) { + Object.defineProperty(window, 'd3', { + configurable: true, enumerable: own ? own.enumerable : true, + get: () => (document.currentScript === s ? ours : hostD3), + set: (v) => { if (document.currentScript === s) ours = v; else hostD3 = v }, + }) + trapped = true + } else if (own.writable) { + // A `var d3` global is non-configurable, so it cannot be trapped: empty it for the load instead + // (the host reads undefined until onload), which still keeps our exports out of its object. + window.d3 = undefined } - s.onerror = () => { d3Promise = null; rej(new Error('d3 failed to load (network or integrity)')) } + const restore = () => { + if (trapped) { + delete window.d3 + if (own || hostD3 !== undefined) { + Object.defineProperty(window, 'd3', { value: hostD3, writable: own ? own.writable : true, + configurable: true, enumerable: own ? own.enumerable : true }) + } + } else if (own && own.writable) { + window.d3 = hostD3 + } + } + s.onload = () => { + const lib = trapped ? ours : window.d3 + restore() + d3Lib = lib + if (d3Lib) res(d3Lib) + else { d3Promise = null; rej(new Error('d3 loaded but exported nothing (an AMD loader on the page?)')) } + } + s.onerror = () => { restore(); d3Promise = null; rej(new Error('d3 failed to load (network or integrity)')) } document.head.appendChild(s) }) return d3Promise