diff --git a/internal/check-shared-component-reached-bundles.mjs b/internal/check-shared-component-reached-bundles.mjs index afa5c211b..a81bce70c 100644 --- a/internal/check-shared-component-reached-bundles.mjs +++ b/internal/check-shared-component-reached-bundles.mjs @@ -72,6 +72,13 @@ const MARKERS = [ component: 'library/composables/useMapInstance.js', sourceRe: /if \(!element\) return\n\s*const root = element\.getRootNode\(\)/, builtRe: /if\(!([\w$]+)\)return;const ([\w$]+)=\1\.getRootNode\(\);if\(!\(\2 instanceof ShadowRoot\)\)return/, + // ★ NOT EVERY CONSUMER COMPILES THE GUARDED FUNCTION. ensureEngineCSS is only called by + // initializeMap; a bundle that imports pieces of useMapInstance (ensureEngineScript, engineGlobal…) + // but never initializeMap gets it TREE-SHAKEN AWAY, so the guard can never appear there — and the + // race cannot happen there either. Reported by upg100 (my-upg-100-list) 2026-09-25: 3 of 12 + // bundles were false negatives. `staleRe` is the UNGUARDED shape: a bundle is MISSING the fix only + // if it still compiles ensureEngineCSS without the guard; with neither shape it is NOT AFFECTED. + staleRe: /const ([\w$]+)=([\w$]+)\.getRootNode\(\);if\(!\(\1 instanceof ShadowRoot\)\)return/g, marker: 'if (!element) return (ensureEngineCSS)', landed: 'studio, 2026-09-25', why: 'a map whose pane unmounts mid-initialise no longer throws "reading getRootNode" (check-map-unmount-mid-init)', @@ -80,6 +87,13 @@ const MARKERS = [ // A marker is a literal (`marker`, matched with includes) unless the entry supplies regexes. const inSource = (m, src) => (m.sourceRe ? m.sourceRe.test(src) : src.includes(m.marker)) const inBuilt = (m, js) => (m.builtRe ? m.builtRe.test(js) : js.includes(m.marker)) +// With a staleRe: does the bundle compile the OLD shape — an occurrence NOT immediately preceded by +// the guard? (The guarded form contains the old shape as a substring, hence the look-behind.) +const hasStale = (m, js) => { + if (!m.staleRe) return true + for (const x of js.matchAll(new RegExp(m.staleRe.source, 'g'))) if (!/return;$/.test(js.slice(Math.max(0, x.index - 24), x.index))) return true + return false +} if (!existsSync(TREE)) { console.log(`SHARED-REACHED-BUNDLES: SKIP — no built tree at ${TREE}.`) @@ -158,19 +172,23 @@ for (const m of MARKERS) { const have = [] const missing = [] const unbuilt = [] + const unaffected = [] for (const bundle of consumers) { const built = join(TREE, bundle, `${bundle}.js`) if (!existsSync(built)) { unbuilt.push(bundle); continue } const js = readFileSync(built, 'utf8') - ;(inBuilt(m, js) ? have : missing).push(bundle) + if (inBuilt(m, js)) have.push(bundle) + else if (hasStale(m, js)) missing.push(bundle) + else unaffected.push(bundle) } console.log(` ${m.id} — ${m.why}`) console.log(` landed ${m.landed}; ${have.length} of ${have.length + missing.length} built ` - + `consumer(s) carry it${unbuilt.length ? `, ${unbuilt.length} not built` : ''}`) + + `consumer(s) carry it${unaffected.length ? ` (${unaffected.length} not affected)` : ''}${unbuilt.length ? `, ${unbuilt.length} not built` : ''}`) if (have.length) console.log(` ✓ ${have.join(', ')}`) if (missing.length) console.log(` ✗ ${missing.join(', ')}`) if (unbuilt.length) console.log(` · not built: ${unbuilt.join(', ')}`) + if (unaffected.length) console.log(` – not affected (the changed code is not compiled in): ${unaffected.join(', ')}`) if (missing.length) { fails.push(`${m.id}: ${missing.length} bundle(s) do NOT carry "${m.why}" — ${missing.join(', ')}`)