Let the map classifier see maps nested below a profile root or in another bundle
Some checks are pending
build-gate / build (push) Waiting to run
Some checks are pending
build-gate / build (push) Waiting to run
Two defects in generate-tree.mjs left the only two map-owning composite leaves marked TOKEN_FREE, so they mounted with no token in profile-config and survived only on a window.MAP_TOKEN fallback that yields to any pre-existing global. - isMapLeaf scans one flat file. research-dashboard.vue has zero engine references and is the only .vue in its folder, while the real map lives in src/components/dashboards/DashboardMapPanel.vue. New ownsAMapDeeply walks src/ for .vue/.js/.mjs and is wired into both the composite path and the non-composite branch. - composes entries dropped the bundle segment, so a child from a sibling bundle was looked up in the wrong directory and silently missed. Both fixes were needed; resolving cross-bundle alone still reports map=false because the map is a directory deeper. Negative-tested before wiring in: across every bundle, research-dashboard is the only profile whose answer changes. Post-build check confirms the seven genuinely map-free profiles stay token-free. embed suite 40/40, exit 0.
This commit is contained in:
parent
7f08d09edc
commit
8846be5b47
6 changed files with 105 additions and 19 deletions
|
|
@ -205,6 +205,49 @@ function isMapLeaf(filePath) {
|
|||
catch { return false }
|
||||
}
|
||||
|
||||
/**
|
||||
* Does a profile own a map ANYWHERE beneath its folder, not just in its own root .vue?
|
||||
*
|
||||
* WHY THIS EXISTS (measured 2026-09-22). isMapLeaf scans ONE FLAT FILE. That is correct for a
|
||||
* simple leaf, and wrong for a profile that splits itself into components. The case that proved
|
||||
* it: `research-dashboard.vue` has ZERO engine references (grep count 0) and is the only .vue in
|
||||
* its folder — yet the profile owns a real Mapbox map at
|
||||
* `src/components/dashboards/DashboardMapPanel.vue`, one directory deeper. So every existing
|
||||
* resolution step reported map=False and the generator emitted TOKEN_FREE for it.
|
||||
*
|
||||
* THE MEASURED CONSEQUENCE, and why it is worth fixing rather than documenting. A TOKEN_FREE leaf
|
||||
* calls `mount('')`, so the `tk: token` branch never runs and the element's profile-config carries
|
||||
* no token. The map then survives only on the leaf's `window.MAP_TOKEN = BAKED_TOKEN` fallback —
|
||||
* which the generated page itself guards with `if (BAKED_TOKEN && !window.MAP_TOKEN)`, i.e. it
|
||||
* YIELDS to any pre-existing global. A negative control confirmed the asymmetry: blanking
|
||||
* window.MAP_TOKEN broke `dashboard-plus-grid` ALONE; its eight siblings were untouched because
|
||||
* they inject `tk` directly. One bundle was a single rung short of every other.
|
||||
*
|
||||
* BOUNDED ON PURPOSE. Only `src/` is walked, and only .vue/.js/.mjs are read. A profile's map
|
||||
* lives in its component tree; walking the whole folder would drag in data/, fixtures and built
|
||||
* output for no signal and real cost on every build.
|
||||
*/
|
||||
const NESTED_SCAN_DIRS = ['src']
|
||||
const NESTED_SCAN_EXT = /\.(vue|js|mjs)$/
|
||||
function ownsAMapDeeply(profileDir) {
|
||||
for (const sub of NESTED_SCAN_DIRS) {
|
||||
const root = join(profileDir, sub)
|
||||
if (!existsSync(root)) continue
|
||||
const stack = [root]
|
||||
while (stack.length) {
|
||||
const cur = stack.pop()
|
||||
let entries
|
||||
try { entries = readdirSync(cur, { withFileTypes: true }) } catch { continue }
|
||||
for (const e of entries) {
|
||||
const p = join(cur, e.name)
|
||||
if (e.isDirectory()) { stack.push(p); continue }
|
||||
if (NESTED_SCAN_EXT.test(e.name) && isMapLeaf(p)) return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Does a cap-4 composite (`nested-elements-mfe`) compose a child that paints a map?
|
||||
*
|
||||
|
|
@ -234,19 +277,49 @@ function composesAMapLeaf(dir, profileName, file) {
|
|||
if (existsSync(metaPath)) {
|
||||
const meta = JSON.parse(readFileSync(metaPath, 'utf8'))
|
||||
const composes = Array.isArray(meta?.composes) ? meta.composes : []
|
||||
// Entries are "<bundle>/<profile>" or a bare "<profile>"; only the last segment
|
||||
// names the .vue, and a composite composes within its own bundle.
|
||||
const named = composes
|
||||
.map((c) => String(c).split('/').pop())
|
||||
.filter(Boolean)
|
||||
// Entries are "<bundle>/<profile>" or a bare "<profile>".
|
||||
//
|
||||
// CORRECTED 2026-09-22 — this previously did `.split('/').pop()`, DISCARDING the bundle
|
||||
// segment, then looked for the .vue inside THIS bundle's dir. For a composite that nests a
|
||||
// child from ANOTHER bundle that lookup silently missed: the file does not exist there, so
|
||||
// isMapLeaf returned false and the composite was classified map-free. Measured case:
|
||||
// dashboard-plus-grid composes "research-dashboard", which lives in its own sibling bundle
|
||||
// directory, not in dashboard-plus-grid/.
|
||||
//
|
||||
// Two fixes, and BOTH were needed — neither alone is sufficient:
|
||||
// (a) resolve cross-bundle: try the sibling bundle dir before falling back to this one;
|
||||
// (b) scan the resolved profile DEEPLY (ownsAMapDeeply), because research-dashboard's own
|
||||
// .vue has zero engine refs while its map sits in src/components/dashboards/.
|
||||
// Fixing only (a) still yields map=false — verified by simulation before writing this.
|
||||
const bundlesRoot = dirname(dir)
|
||||
const resolveChild = (entry) => {
|
||||
const parts = String(entry).split('/').filter(Boolean)
|
||||
const profile = parts[parts.length - 1]
|
||||
const bundle = parts.length > 1 ? parts[parts.length - 2] : null
|
||||
// Candidate profile DIRECTORIES, most-specific first.
|
||||
const dirs = []
|
||||
if (bundle) dirs.push(join(bundlesRoot, bundle))
|
||||
dirs.push(dir) // same-bundle sibling (the historical assumption)
|
||||
dirs.push(join(bundlesRoot, profile))// a bare entry naming a bundle of the same name
|
||||
for (const d of dirs) {
|
||||
if (!existsSync(d)) continue
|
||||
if (isMapLeaf(join(d, `${profile}.vue`))) return true
|
||||
if (ownsAMapDeeply(d)) return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
const named = composes.map((c) => String(c)).filter(Boolean)
|
||||
if (named.length) {
|
||||
return named.some((p) => isMapLeaf(join(dir, `${p}.vue`)))
|
||||
return named.some(resolveChild)
|
||||
}
|
||||
}
|
||||
} catch (_) { /* malformed meta → fall through to the source scans */ }
|
||||
|
||||
// 2. the composite's own source
|
||||
// 2. the composite's own source — flat file first, then its own component tree. The deep scan
|
||||
// is what catches a profile whose root .vue is a pure layout and whose map lives in
|
||||
// src/components/ (the research-dashboard shape).
|
||||
if (isMapLeaf(join(dir, file))) return true
|
||||
if (ownsAMapDeeply(dir)) return true
|
||||
|
||||
// 3. any sibling profile in the same bundle
|
||||
try {
|
||||
|
|
@ -947,11 +1020,21 @@ function scanBundles() {
|
|||
// derived from meta.json `composes` (and falls back to scanning the bundle's own
|
||||
// profiles), never from a per-leaf special case — the next author who composes a map
|
||||
// gets a working peer without knowing this rule exists.
|
||||
// A NON-composite profile is `|| ownsAMapDeeply(dir)` for the same reason a composite is:
|
||||
// a profile whose root .vue is a pure layout can still own a map in its own component
|
||||
// tree. research-dashboard is exactly that — zero engine refs in research-dashboard.vue,
|
||||
// a real Mapbox map in src/components/dashboards/DashboardMapPanel.vue — so the flat scan
|
||||
// alone marked its standalone leaf TOKEN_FREE and the page mounted with no token.
|
||||
//
|
||||
// The deep scan is bounded to src/ and to .vue/.js/.mjs, and it was negative-tested across
|
||||
// every bundle before being wired in: research-dashboard is the ONLY profile whose
|
||||
// classification it changes (flat=false → deep=true). Every other map bundle already
|
||||
// matched flat, and no genuinely map-free profile was flipped.
|
||||
const isMap = profileType === 'nested-mfe-spa'
|
||||
? false
|
||||
: profileType === 'nested-elements-mfe'
|
||||
? composesAMapLeaf(dir, profileName, file)
|
||||
: isMapLeaf(join(dir, file))
|
||||
: (isMapLeaf(join(dir, file)) || ownsAMapDeeply(dir))
|
||||
|
||||
return {
|
||||
profile: profileName,
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
pid=186659 started=2026-09-22T01:39:13Z by=lk10x
|
||||
pid=299775 started=2026-09-22T01:59:04Z by=lk10x
|
||||
|
|
|
|||
2
public/doxa-maps/index.html
generated
2
public/doxa-maps/index.html
generated
File diff suppressed because one or more lines are too long
4
public/doxa-maps/manifest.json
generated
4
public/doxa-maps/manifest.json
generated
|
|
@ -200,7 +200,7 @@
|
|||
"description": "Both children are IMPORTED DIRECTLY (compiled into this ONE bundle — no runtime bundle",
|
||||
"element": "dashboard-plus-grid",
|
||||
"type": "nested-elements-mfe",
|
||||
"isMap": false,
|
||||
"isMap": true,
|
||||
"iframeUrl": "./dashboard-plus-grid/dashboard-plus-grid/index.html",
|
||||
"scriptUrl": "./dashboard-plus-grid/dashboard-plus-grid.js",
|
||||
"embed": {
|
||||
|
|
@ -750,7 +750,7 @@
|
|||
"description": "Coordinated-Multiple-Views research dashboard for 10/40-window UPGs (live pray.doxa.life): docked facet rail + map-as-hero + linked table/charts, brushing-and-linking over one in-memory store.",
|
||||
"element": "research-dashboard-map",
|
||||
"type": "monolithic",
|
||||
"isMap": false,
|
||||
"isMap": true,
|
||||
"iframeUrl": "./research-dashboard/research-dashboard/index.html",
|
||||
"scriptUrl": "./research-dashboard/research-dashboard.js",
|
||||
"embed": {
|
||||
|
|
|
|||
2
public/doxa-maps/research-dashboard/index.html
generated
2
public/doxa-maps/research-dashboard/index.html
generated
|
|
@ -130,7 +130,7 @@ header.top .sub { margin-top: 3px; font: 12px/1.4 var(--sans); color: var(--mut)
|
|||
|
||||
<script>window.FORGE = {"mode":"built","scope":"bundle","manifestUrl":"../manifest.json","bundle":"research-dashboard","hrefBase":"../"};
|
||||
/* file:// contract: manifest inlined at build — no fetch needed to render. */
|
||||
window.FORGE.manifest = {"schema":"doxa-maps-manifest@1","generatedFrom":"plugin-designs/*/*.vue","embedBase":".","bundleCount":17,"profileCount":26,"bundles":[{"bundle":"research-dashboard","scriptUrl":"./research-dashboard/research-dashboard.js","output":"doxa-maps/research-dashboard/research-dashboard.js","integrity":"sha384-nHubFqtdQYTHB6pk9pg5pUrrciRtghiBDLJ1j2JiJVpRonHyJvY0cJ1W+P9UrHx9","elements":["research-dashboard-map"],"profiles":[{"profile":"research-dashboard","file":"research-dashboard.vue","description":"Coordinated-Multiple-Views research dashboard for 10/40-window UPGs (live pray.doxa.life): docked facet rail + map-as-hero + linked table/charts, brushing-and-linking over one in-memory store.","element":"research-dashboard-map","type":"monolithic","isMap":false,"iframeUrl":"./research-dashboard/research-dashboard/index.html","scriptUrl":"./research-dashboard/research-dashboard.js","embed":{"iframe":"\u003ciframe src=\"./research-dashboard/research-dashboard/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen>\u003c/iframe>","script":"\u003cscript>window.MAP_APP_API_URL=\"https://pray.doxa.life\";\u003c/script>\n\u003cscript src=\"./research-dashboard/research-dashboard.js\" integrity=\"sha384-nHubFqtdQYTHB6pk9pg5pUrrciRtghiBDLJ1j2JiJVpRonHyJvY0cJ1W+P9UrHx9\" crossorigin=\"anonymous\">\u003c/script>\n\u003cresearch-dashboard-map profile-config='{\"profile\":\"research-dashboard\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'>\u003c/research-dashboard-map>"},"bundle":"research-dashboard"}]}]};</script>
|
||||
window.FORGE.manifest = {"schema":"doxa-maps-manifest@1","generatedFrom":"plugin-designs/*/*.vue","embedBase":".","bundleCount":17,"profileCount":26,"bundles":[{"bundle":"research-dashboard","scriptUrl":"./research-dashboard/research-dashboard.js","output":"doxa-maps/research-dashboard/research-dashboard.js","integrity":"sha384-nHubFqtdQYTHB6pk9pg5pUrrciRtghiBDLJ1j2JiJVpRonHyJvY0cJ1W+P9UrHx9","elements":["research-dashboard-map"],"profiles":[{"profile":"research-dashboard","file":"research-dashboard.vue","description":"Coordinated-Multiple-Views research dashboard for 10/40-window UPGs (live pray.doxa.life): docked facet rail + map-as-hero + linked table/charts, brushing-and-linking over one in-memory store.","element":"research-dashboard-map","type":"monolithic","isMap":true,"iframeUrl":"./research-dashboard/research-dashboard/index.html","scriptUrl":"./research-dashboard/research-dashboard.js","embed":{"iframe":"\u003ciframe src=\"./research-dashboard/research-dashboard/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen>\u003c/iframe>","script":"\u003cscript>window.MAP_APP_API_URL=\"https://pray.doxa.life\";\u003c/script>\n\u003cscript src=\"./research-dashboard/research-dashboard.js\" integrity=\"sha384-nHubFqtdQYTHB6pk9pg5pUrrciRtghiBDLJ1j2JiJVpRonHyJvY0cJ1W+P9UrHx9\" crossorigin=\"anonymous\">\u003c/script>\n\u003cresearch-dashboard-map profile-config='{\"profile\":\"research-dashboard\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'>\u003c/research-dashboard-map>"},"bundle":"research-dashboard"}]}]};</script>
|
||||
<script>/**
|
||||
* staging-forge.js — the ONE-PAGE semantic staging environment (root + bundle pages).
|
||||
*
|
||||
|
|
|
|||
|
|
@ -5,10 +5,13 @@
|
|||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>research-dashboard · research-dashboard</title>
|
||||
<link rel="icon" href="data:,">
|
||||
<!-- NON-MAP leaf (app-shell / composite / grid / card / chart-table): NO map peer.
|
||||
This element draws no map tiles, so it needs no Mapbox/MapLibre engine and MUST NOT
|
||||
gate on a token. Any nested MAP (in a composite/shell) renders in its own iframe leaf,
|
||||
which loads its own map peer + token. A peer here would be dead weight + imply a gate. -->
|
||||
<!-- Mapbox peer loaded ONCE here; the map custom element mounts in this page and
|
||||
uses window.mapboxgl directly (same realm → no cross-realm container errors). -->
|
||||
<link rel="preconnect" href="https://api.mapbox.com" crossorigin />
|
||||
<link href="https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.css" rel="stylesheet" />
|
||||
<link href="https://api.mapbox.com/mapbox-gl-js/plugins/mapbox-gl-geocoder/v5.0.0/mapbox-gl-geocoder.css" rel="stylesheet" />
|
||||
<script src="https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js"></script>
|
||||
<script src="https://api.mapbox.com/mapbox-gl-js/plugins/mapbox-gl-geocoder/v5.0.0/mapbox-gl-geocoder.min.js"></script>
|
||||
<style>
|
||||
/* BARE full-page map — no chrome. The custom element IS the page, so the
|
||||
browser URL shares the map itself. */
|
||||
|
|
@ -22,7 +25,7 @@
|
|||
</head>
|
||||
<body>
|
||||
<div id="slot"></div>
|
||||
<div id="status">loading…</div>
|
||||
<div id="status">loading token…</div>
|
||||
|
||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||
<!-- Bundle IIFE — registers the <research-dashboard-map> custom element. -->
|
||||
|
|
@ -45,7 +48,7 @@
|
|||
var BASE_CONFIG = {"profile":"research-dashboard","dataSource":"pray-tools","instanceId":"research-dashboard-research-dashboard","assetsBase":"../../assets"};
|
||||
// MapLibre map pages AND shell pages (app-shell / nested-mfe-spa) need NO token:
|
||||
// the ladder below is skipped entirely and the element mounts immediately.
|
||||
var TOKEN_FREE = true;
|
||||
var TOKEN_FREE = false;
|
||||
var TOKEN_SRC = "https://doxa.life/api/maps/token";
|
||||
var BAKED_TOKEN = "pk.eyJ1IjoiZG94YWxpZmUiLCJhIjoiY211YXkxa25yMDI2YTJ3cTV6ZXhydTZzcyJ9.fBMmvu0Y0S9u8tnHOyVhxg";
|
||||
// Expose the baked token as window.MAP_TOKEN so a BAKED composite (a compiled element that mounts
|
||||
|
|
|
|||
Loading…
Reference in a new issue