Both library layouts load nested bundles through the shared embedScript, which fixes VerticalTabsLayout on the dev server

VerticalTabsLayout kept a private script loader without the dev-server ES-module
rule, so a bundle nested through it under bun run dev loaded as a classic script,
reported success, and never defined its tag. It and StackLayout now call
library/utils/embedScript.js loadScriptOnce; StackLayout keeps its own
resolveBundleSrc step first. loadScriptOnce gains an empty-url no-op (a
<script src=""> re-fetched the page) and carries StackLayout's stack-demo
measurement beside the gallery's. One copy of the pattern and one cache.
This commit is contained in:
Doxa Maps 2026-09-28 06:47:24 +00:00
parent d0939c0492
commit 4ce8604d42
3 changed files with 17 additions and 41 deletions

View file

@ -80,10 +80,8 @@
import { defineComponent } from 'vue' import { defineComponent } from 'vue'
import { useShadowStyles } from '@map/composables/useShadowStyles.js' import { useShadowStyles } from '@map/composables/useShadowStyles.js'
import { bundleTreeRoot } from '@map/utils/embedBase.js' import { bundleTreeRoot } from '@map/utils/embedBase.js'
import { loadScriptOnce } from '@map/utils/embedScript.js'
// Inject a bundle script exactly once per URL (deduped across all layout instances). Same helper the
// tabs layout + the SPA use — resolves after the IIFE's customElements.define has run.
const _stlLoaded = {}
// ★ ANCHOR A SIBLING-BUNDLE src TO THE TREE ROOT, NOT TO A GUESSED DEPTH. // ★ ANCHOR A SIBLING-BUNDLE src TO THE TREE ROOT, NOT TO A GUESSED DEPTH.
// THE BUG (Driver-reported 2026-09-23, verbatim console text): // THE BUG (Driver-reported 2026-09-23, verbatim console text):
// Could not load <doxa-research-map> from ../../doxa-research-map/doxa-research-map.js // Could not load <doxa-research-map> from ../../doxa-research-map/doxa-research-map.js
@ -134,30 +132,11 @@ function resolveBundleSrc(url) {
return url return url
} }
// Inject a bundle script exactly once per URL, through the ONE shared loader (library/utils/embedScript.js:
// the dedupe cache, the dev-server ES-module rule, the empty-url no-op). Only the src resolution is this
// layout's own: a sibling-bundle src is anchored to the tree root first (resolveBundleSrc above).
function injectScriptOnce(rawUrl) { function injectScriptOnce(rawUrl) {
const url = resolveBundleSrc(rawUrl) return loadScriptOnce(resolveBundleSrc(rawUrl))
if (!url) return Promise.resolve()
if (_stlLoaded[url]) return _stlLoaded[url]
_stlLoaded[url] = new Promise((ok, fail) => {
const s = document.createElement('script')
s.src = url
// ★ A DEV ENTRY IS AN ES MODULE, AND A CLASSIC <script> LIES ABOUT LOADING IT.
// Pointing a classic script at /plugin-designs/<bundle>/index.js fires onload SUCCESSFULLY and
// only THEN throws "Cannot use import statement outside a module", asynchronously. So this
// promise resolved, waitForDefine() below timed out, the tag never registered, and the section
// rendered empty — with NO failed request and NO console error at the injection site. onload is
// structurally incapable of detecting this.
// MEASURED on stack-demo's dev leaf 2026-09-22: 0 failed requests, and yet
// customElements.get('research-dashboard-map') === false with 0 rows rendered.
// Same rule as plugin-studio.vue:255, which hit this first. It belongs HERE because StackLayout
// is the SHARED layout — dashboard-plus-grid and research-map-plus-grid inject through this same
// function, so one fix covers all three consumers instead of becoming a fourth private copy.
if (/^\/plugin-designs\//.test(url) || /\/index\.js(\?|$)/.test(url)) s.type = 'module'
s.onload = () => ok()
s.onerror = () => { delete _stlLoaded[url]; fail(new Error('failed to load ' + url)) }
document.head.appendChild(s)
})
return _stlLoaded[url]
} }
function waitForDefine(tag, timeoutMs = 5000) { function waitForDefine(tag, timeoutMs = 5000) {

View file

@ -246,22 +246,12 @@ import { tStatic } from '../i18n/index.js'
const tr = (key, en) => tStatic(key) || en const tr = (key, en) => tStatic(key) || en
import { useShadowStyles } from '@map/composables/useShadowStyles.js' import { useShadowStyles } from '@map/composables/useShadowStyles.js'
import { icon as localIcon } from '@map/layouts/app-wrapper-material/icons.js' import { icon as localIcon } from '@map/layouts/app-wrapper-material/icons.js'
import { loadScriptOnce } from '@map/utils/embedScript.js'
// Inject a bundle script exactly once per URL (deduped across all layout instances). Resolves after // Inject a bundle script exactly once per URL, through the ONE shared loader (library/utils/embedScript.js).
// the IIFE has executed (so its customElements.define has run). Mirrors the SPA/studio's once(). // This layout's private copy lacked the dev-server ES-module rule, so a bundle nested through it under
const _vtlLoaded = {} // bun run dev loaded as a classic script, reported success, and never defined its tag.
function injectScriptOnce(url) { const injectScriptOnce = (url) => loadScriptOnce(url)
if (!url) return Promise.resolve()
if (_vtlLoaded[url]) return _vtlLoaded[url]
_vtlLoaded[url] = new Promise((ok, fail) => {
const s = document.createElement('script')
s.src = url
s.onload = () => ok()
s.onerror = () => { delete _vtlLoaded[url]; fail(new Error('failed to load ' + url)) }
document.head.appendChild(s)
})
return _vtlLoaded[url]
}
// Wait until a custom element tag is defined (up to ~5s). If a src bundle was injected, its // Wait until a custom element tag is defined (up to ~5s). If a src bundle was injected, its
// define() has already run by onload; this covers elements registered async after script exec. // define() has already run by onload; this covers elements registered async after script exec.

View file

@ -29,11 +29,18 @@
* The built tree ships IIFEs, where a classic tag is correct, so this only ever bites under the dev * The built tree ships IIFEs, where a classic tag is correct, so this only ever bites under the dev
* server — exactly the surface a developer works on. Rule: a rooted /plugin-designs/ entry (or any * server — exactly the surface a developer works on. Rule: a rooted /plugin-designs/ entry (or any
* .../index.js) is a dev ES module; everything else is a built IIFE. * .../index.js) is a dev ES module; everything else is a built IIFE.
* MEASURED a second time, independently, on stack-demo's dev leaf 2026-09-22 (StackLayout, before it
* used this file): 0 failed requests, and yet customElements.get('research-dashboard-map') === false
* with 0 rows rendered. VerticalTabsLayout's private copy LACKED this rule until it moved here, so a
* bundle nested through it on the dev server never defined its tag.
*
* An empty url is a no-op: a <script src=""> would re-fetch the page itself.
* *
* SRI is optional (attrs.integrity). * SRI is optional (attrs.integrity).
*/ */
const _loaded = {} const _loaded = {}
export function loadScriptOnce(url, attrs) { export function loadScriptOnce(url, attrs) {
if (!url) return Promise.resolve()
if (_loaded[url]) return _loaded[url] if (_loaded[url]) return _loaded[url]
_loaded[url] = new Promise((ok, fail) => { _loaded[url] = new Promise((ok, fail) => {
const s = document.createElement('script') const s = document.createElement('script')