diff --git a/internal/check-view-address.mjs b/internal/check-view-address.mjs index d8f312f39..b554f5f47 100644 --- a/internal/check-view-address.mjs +++ b/internal/check-view-address.mjs @@ -22,7 +22,9 @@ // opt-in it never writes; a kept-alive column shown again claims its pane back (a replace), unless the address // names one. // G. THE WIRING in Regions: regions-mfe opts its column in, owns its leaf, and carries the place with its pane; -// region-dashboards claims ?place= (replace at first paint, push on a choice) and re-applies it on Back. +// region-dashboards claims ?place= (replace at first paint, push on a choice) and re-applies it on Back. And +// each shell (Plugin Studio, the Research Gallery, Posterverse) records its nested composites' choices +// (ownAddress) and releases the listener before any hot-update return. // --self-test: each poison below must FAIL its arm. Pure node: no Vite, no build, no browser. import { execFileSync } from 'node:child_process' import { readFileSync, writeFileSync, unlinkSync } from 'node:fs' @@ -331,6 +333,15 @@ async function arms({ va, sv, counts, pw, src }, { skipF = false } = {}) { if (!/claimPlace\(false\)/.test(d) || !/claimPlace\(true\)/.test(d)) fails.push('G: region-dashboards does not replace at first paint and push on a choice') if (!/onAddressChange\(onAddress\)/.test(d)) fails.push('G: region-dashboards does not re-apply ?place= on Back/Forward') if (!/new ResizeObserver\([\s\S]{0,400}else if \(!place\) claimPlace\(false\)/.test(d)) fails.push('G: a kept-alive region-dashboards shown again does not claim its place back') + // Each shell that owns a page records its nested composites' choices, and releases the listener on unmount + // BEFORE any hot-update early return (or a hot remount stacks a second one). + for (const [f, t] of Object.entries(src.shells || {})) { + if (!/this\._offOwn = ownAddress\(this\.\$el, \(\) => urlOwnerOf\(this\.cfg\)\)/.test(t)) { fails.push(`G: the shell ${f} does not record nested choices (ownAddress)`); continue } + const bu = t.indexOf('beforeUnmount()') + const rel = bu < 0 ? -1 : t.indexOf('this._offOwn()', bu) + const hot = bu < 0 ? -1 : t.indexOf('if (import.meta.hot) return', bu) + if (rel < 0 || (hot >= 0 && hot < rel)) fails.push(`G: the shell ${f} does not release ownAddress in beforeUnmount before any hot-update return`) + } } // ── D. the census ── @@ -346,7 +357,12 @@ const va = await import(join(ROOT, 'library/composables/useViewAddress.js')) const sv = await import(join(ROOT, 'library/composables/useShareableView.js')) const PW = 'library/layouts/pane-wrapper/pane-wrapper.js' const pw = await import(join(ROOT, PW)) -const src = { mfe: readFileSync(join(ROOT, 'plugin-designs/regions-dashboard/regions-mfe.vue'), 'utf8'), dash: readFileSync(join(ROOT, 'plugin-designs/regions-dashboard/region-dashboards.vue'), 'utf8') } +const SHELLS = ['plugin-studio/plugin-studio.vue', 'doxa-maps-spa/doxa-maps-spa.vue', 'posterverse/posterverse.vue'] +const src = { + mfe: readFileSync(join(ROOT, 'plugin-designs/regions-dashboard/regions-mfe.vue'), 'utf8'), + dash: readFileSync(join(ROOT, 'plugin-designs/regions-dashboard/region-dashboards.vue'), 'utf8'), + shells: Object.fromEntries(SHELLS.map((f) => [f, readFileSync(join(ROOT, 'plugin-designs', f), 'utf8')])), +} const counts = census() const real = { va, sv, counts, pw, src } // A pane-wrapper with one line changed, loaded from a sibling file so its relative import still resolves. @@ -402,6 +418,10 @@ if (process.argv.includes('--self-test')) { ['region-dashboards not claiming its place when shown again', { ...real, src: { ...src, dash: src.dash.replace('else if (!place) claimPlace(false)', 'else if (!place) void 0') } }, /^G: a kept-alive region-dashboards shown again/], ['regions-mfe opening the dashboard without its place', { ...real, src: { ...src, mfe: src.mfe.replace("go('dashboard', { place: detail.countrySlug || detail.regionSlug || '' })", "go('dashboard')") } }, /^G: regions-mfe opens the dashboard without its place/], ['regions-mfe without the address opt-in', { ...real, src: { ...src, mfe: src.mfe.replace('{ host: stage.value, address: { owner } }', '{ host: stage.value }') } }, /^G: regions-mfe does not opt its column in/], + ['Plugin Studio without ownAddress', { ...real, src: { ...src, shells: { ...src.shells, 'plugin-studio/plugin-studio.vue': src.shells['plugin-studio/plugin-studio.vue'].replace('this._offOwn = ownAddress(this.$el, () => urlOwnerOf(this.cfg))', 'this._offOwn = null') } } }, /^G: the shell plugin-studio\/plugin-studio\.vue does not record/], + ['Plugin Studio releasing ownAddress after its hot-update return', { ...real, src: { ...src, shells: { ...src.shells, 'plugin-studio/plugin-studio.vue': src.shells['plugin-studio/plugin-studio.vue'].replace(' if (this._offOwn) { this._offOwn(); this._offOwn = null }\n if (import.meta.hot) return', ' if (import.meta.hot) return\n if (this._offOwn) { this._offOwn(); this._offOwn = null }') } } }, /^G: the shell plugin-studio\/plugin-studio\.vue does not release/], + ['the Research Gallery without ownAddress', { ...real, src: { ...src, shells: { ...src.shells, 'doxa-maps-spa/doxa-maps-spa.vue': src.shells['doxa-maps-spa/doxa-maps-spa.vue'].replace('this._offOwn = ownAddress(', 'this._offOwn = (') } } }, /^G: the shell doxa-maps-spa/], + ['Posterverse never releasing ownAddress', { ...real, src: { ...src, shells: { ...src.shells, 'posterverse/posterverse.vue': src.shells['posterverse/posterverse.vue'].replace('if (this._offOwn) { this._offOwn(); this._offOwn = null }', 'void 0') } } }, /^G: the shell posterverse\/posterverse\.vue does not release/], ['region-dashboards not re-applying ?place= on Back', { ...real, src: { ...src, dash: src.dash.replace('onAddressChange(onAddress)', 'null') } }, /^G: region-dashboards does not re-apply/], ] for (const [name, inp, rx] of poisons) {