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