From cc65f40c1ee6b10855040234db828301a1a70ae0 Mon Sep 17 00:00:00 2001 From: Doxa Maps Date: Tue, 29 Sep 2026 18:35:37 +0000 Subject: [PATCH] Gate hygiene: three gates:post reds on development, and two things that hid failures - check:no-orphan-gates stopped at internal/check-grid-merge-defaults.mjs, which had no script. It asserts defaults of three grid props that are not on development (the grid merge has not landed), so it is registered ON DEMAND with that reason, not chained red. Past that first arm, its chain arm found check:grid-image-loading and check:retry-fetch run by no chain; both depend only on code and pass in under 300 ms, so they join gates:pre. - A tracked vite.config.js.timestamp-*.mjs (395 KB, eight absolute paths) failed check:no-abs-paths. Removed, and ignored at the source: vite writes one beside its config while it bundles it. - check:library-index: documentation/reference-library-index.md regenerated for retryFetch.js. - run-parallel printed a failed step's output only with --verbose: the unbraced else bound to the inner if. Braced; a failing step now prints why. - The Pages workflow ran bun run build without BUILD_ALLOW=1, so build-lock refused it with exit 75 and the workflow ran no gate at all. A CI runner is the only builder on its machine; it opts in. --- .github/workflows/pages.yml | 3 + .gitignore | 3 + documentation/reference-library-index.md | 2 + internal/fixtures/on-demand-checks.json | 1 + internal/run-parallel.mjs | 9 +- package.json | 4 +- ....timestamp-1790698770542-9b99b49958ff6.mjs | 1982 ----------------- 7 files changed, 19 insertions(+), 1985 deletions(-) delete mode 100644 vite.config.js.timestamp-1790698770542-9b99b49958ff6.mjs diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml index a363d49a8..8c4ea8181 100644 --- a/.github/workflows/pages.yml +++ b/.github/workflows/pages.yml @@ -43,8 +43,11 @@ jobs: # (drop-portability, inline-parse, bundles-shipped and the rest). A guard # failure fails the deploy, which is the point of having them. - name: Build + # BUILD_ALLOW=1: build-lock.sh refuses an unannounced build (exit 75) so two agents on one box + # cannot build at once; a CI runner is the only builder on its machine, so it opts in. env: MAPBOX_TOKEN: ${{ secrets.MAPBOX_TOKEN }} + BUILD_ALLOW: '1' run: bun run build # Fail loudly rather than publishing a site whose maps cannot draw. diff --git a/.gitignore b/.gitignore index d7a3a3d5c..4333a745b 100644 --- a/.gitignore +++ b/.gitignore @@ -155,3 +155,6 @@ data-private/ # gated bundles (gatedProfiles) build here, never into public/ (vite.config.js) /.gated/ /deployment/src-tauri/binaries/ + +# vite writes these beside the config while it bundles it; never commit one (it embeds absolute paths) +vite.config.js.timestamp-* diff --git a/documentation/reference-library-index.md b/documentation/reference-library-index.md index ef15162cd..63f8ad174 100644 --- a/documentation/reference-library-index.md +++ b/documentation/reference-library-index.md @@ -141,6 +141,8 @@ and a change here can break other maps. `import … from '@map/composables/poster/usePosterLayout.js'` - **printInPlace** — printInPlace.js — SHARED (@map/composables/printInPlace.js). `import … from '@map/composables/printInPlace.js'` +- **retryFetch** — retryFetch.js — fetch(), with the same transient-blip retry the lazy components already get. + `import … from '@map/composables/retryFetch.js'` - **retryableAsyncComponent** — retryableAsyncComponent.js — SHARED (@map/composables/retryableAsyncComponent.js). `import … from '@map/composables/retryableAsyncComponent.js'` - **useAvatarGenerator** — Avatar Generator Composable diff --git a/internal/fixtures/on-demand-checks.json b/internal/fixtures/on-demand-checks.json index 7d41ec173..812c94b91 100644 --- a/internal/fixtures/on-demand-checks.json +++ b/internal/fixtures/on-demand-checks.json @@ -9,6 +9,7 @@ "check:shot-fonts": "checks that the MACHINE taking README screenshots can render emoji (fc-list): a property of that box, not of the product, so it runs before a screenshot session, not in the gates", "check:coverage-consistent": "the full run includes clause L, the age of the oldest link verification (45 days), which depends on the CALENDAR: chained, it would turn gates red on a date (first 2026-11-08) with no code change. Run it with the link re-verification; every other clause runs in gates:pre as check:coverage-consistent:chained", "check:snippets-draw": "AWAITING ITS FIRST BROWSER RUN (rega): the browser suite lists a gate only after it has run. Moves into verify-browser-all's GATES, and out of this list, once that run is recorded.", + "check:grid-merge-defaults": "WAITS FOR ITS CODE: it asserts the defaults of three grid props (filtersToggle, backdrop, backdropFloor) that are not on development, because the grid merge (b1e839a33) has not landed. Chain it in gates:pre in the same landing as that merge.", "check:snippets-draw:mapbox": "AWAITING ITS FIRST BROWSER RUN (rega): the browser suite lists a gate only after it has run. Moves into verify-browser-all's GATES, and out of this list, once that run is recorded. Needs MAP_TOKEN." } } diff --git a/internal/run-parallel.mjs b/internal/run-parallel.mjs index 482cbf934..4bc8544eb 100644 --- a/internal/run-parallel.mjs +++ b/internal/run-parallel.mjs @@ -253,10 +253,15 @@ async function main() { const serial = results.reduce((a, r) => a + r.ms, 0) const failed = results.filter((r) => r.bucket === 'FAIL') - if (VERBOSE) for (const r of results) if (r.out.trim()) console.log(`\n───── ${r.name} ─────\n${r.out}`) // A failure must be readable in full: these gates explain themselves in their output, and // a truncated explanation is how a real defect gets re-diagnosed as a flake. - else for (const r of failed) console.log(`\n───── FAILED: ${r.name} (exit ${r.code}) ─────\n${r.out}`) + // ★ BRACED ON PURPOSE. Unbraced, this `else` bound to the INNER `if (r.out.trim())`, so without + // --verbose a failed gate printed only its name and exit code, never why. + if (VERBOSE) { + for (const r of results) if (r.out.trim()) console.log(`\n───── ${r.name} ─────\n${r.out}`) + } else { + for (const r of failed) console.log(`\n───── FAILED: ${r.name} (exit ${r.code}) ─────\n${r.out}`) + } // COUNT THE SKIPS IN THE SUMMARY, because the summary is the line a reader quotes and // the one that lands in a report. "35/35 passed" was true of a wave where a third of the diff --git a/package.json b/package.json index f091e9ded..529944b65 100644 --- a/package.json +++ b/package.json @@ -285,6 +285,7 @@ "check:embed-url-absolute": "node internal/check-embed-url-absolute.mjs", "check:grid-image-loading": "node internal/check-grid-image-loading.mjs", "check:retry-fetch": "node internal/check-retry-fetch.mjs", + "check:grid-merge-defaults": "node internal/check-grid-merge-defaults.mjs", "check:setup-order": "node internal/check-setup-order.mjs", "check:sfc-parse": "node internal/check-sfc-script-parses.mjs", "check:shadow-css-parses": "node internal/check-shadow-css-parses.mjs", @@ -339,7 +340,7 @@ "fix:sri": "node internal/fix-sri.mjs", "gallery": "node internal/generate-gallery.mjs", "gates:post": "node internal/run-parallel.mjs --label=post -- check:capability-derived check:drop check:parse verify check:sri check:public-hygiene check:label-joins check:facet-cascade check:facet-one-field check:heartbeat check:kpi-unit check:composite-reg check:descriptions check:no-abs-paths check:no-codename check:dep-names check:composite-srcs check:embed-targets check:nested-token check:i18n-parity check:tabs-localized check:packets-all check:packet-strings-parity check:zone-census check:zoning-declared check:share-policy check:library-index check:csv-shape check:capability-citations check:catalog-identity check:no-orphan-gates check:quoted-scripts check:done-evidence check:script-flags check:backlog-status check:capability-exemplars check:packet-fixture check:packet-browser check:provider-switch check:locale-reachable check:url-resolver check:manifest-covers check:one-palette check:pin-dim-slider check:legend-source check:backlog-refs check:clustering-modes check:composite-embed-knobs check:composite-locale-leaks check:linked-vocabulary check:wp-embed check:dashboard-geocoder-provider check:leaf-engine-per-provider check:auto-engine check:engine-contract check:map-props-unit check:composites-mount check:composite-interop check:no-free-identifiers check:map-fly-reduced-motion check:mst-clustering check:list-prop-unit check:profiles-mount check:cdn-pins check:no-eval check:popup-escaping check:i18n-treeshake check:pinned-umd check:one-palette:self-test check:no-cross-plugin-imports check:no-cross-plugin-imports:self-test check:map-attribution check:studio-rail check:approx-pin-note check:embedder-keys check:provider-for check:legend-logical-insets check:legend-logical-insets:self-test check:optional-t-equivalence check:text-highlight check:religion-label-not-value check:i18n-stopped-scope check:no-silent-engine check:no-silent-engine:self-test check:regions-uses-library check:shadow-css-parses check:square-map-controls check:preset-config-merge check:instance-config-keys check:frozen-labels check:plural-forms check:region-fill-repaint check:region-paint-single-source check:poster-print-fallback check:posters-exportable check:country-names check:reduced-motion check:no-dead-library check:universal-props check:readme-size check:tree-relative check:leaf-hints check:snippet-engine check:snippet-engine:self-test check:embed-base check:link-coverage check:no-public-binds check:shared-reached check:tree-weight", - "gates:pre": "node internal/run-parallel.mjs --label=pre -- check:template-literal check:mjs-parse check:sfc-parse check:tab-radius check:synced-css check:doc-scripts check:setup-order check:package-json-unique-keys check:no-repo-pb-hooks check:no-credentials-omit check:no-credentials-omit:self-test check:shadow-reduced-motion check:poster-action-print check:targets check:targets:self-test check:browser-gates-guarded check:pin-selection check:facet-shape check:hardcoded-english:self-test check:list-selection check:selector-fields check:upg-zone-joins check:gospel-set-charts check:gospel-set-fit check:describe-group check:one-scroll-container check:packet-charts check:composite-token-url check:language-tree-layout check:i18n-keys-exist check:i18n-never-translated check:packet-variants check:profile-table check:element-bootstrap check:embed-url-absolute check:frame-target check:language-tree-model check:map-menu-anchor check:selection-kinds check:tiered-points check:whiteboard-projection check:mercator-slice check:ring-vocabulary check:map-props-unit check:mobile-gating check:provide-shadowing check:legend-top-offset check:cdn-cache check:plural-forms check:select-zone-race check:script-registry check:map-control-overlap check:poster-attribution check:board-map-options check:gospel-whiteboard check:packet-map-missing-loud check:poster-layout-optin check:ported-globals-identical check:ring-bands check:packet-a1-structure check:zone-title-tier-a check:evangelical-share check:map-labels check:packet-offline check:help-reachable check:gated-not-in-tree check:window-globals-released check:legend-data-contract check:no-new-driver-references check:poster-fetch-guard check:people-group-records check:sentence-slots check:popup-href-escaped check:popup-pan-controls check:viewport-meta check:viewport-meta:self-test check:label-key-coverage check:poster-browser-equiv check:poster-render-pinned check:host-locale-fetch check:print-frame-reclaim check:click-sheet check:coverage-consistent:chained check:coverage-consistent:poison check:dashboard-filter-a11y check:filter-multivalue check:hover-channel check:kit-options-reachable check:layout-key-namespaced check:legend-mirrors check:map-unmount-race check:no-conflict-markers check:series-isolation check:snapshot-guard check:stale-sweep-claim check:surfaces-read-the-tier check:translator-contacts check:dxlife check:wall-wysiwyg check:wall-print-watch check:wall-no-bg-photos check:template-i18n check:list-pattern-doc check:shadow-style-labels-unique", + "gates:pre": "node internal/run-parallel.mjs --label=pre -- check:template-literal check:mjs-parse check:sfc-parse check:tab-radius check:synced-css check:doc-scripts check:setup-order check:package-json-unique-keys check:no-repo-pb-hooks check:no-credentials-omit check:no-credentials-omit:self-test check:shadow-reduced-motion check:poster-action-print check:targets check:targets:self-test check:browser-gates-guarded check:pin-selection check:facet-shape check:hardcoded-english:self-test check:list-selection check:selector-fields check:upg-zone-joins check:gospel-set-charts check:gospel-set-fit check:describe-group check:one-scroll-container check:packet-charts check:composite-token-url check:language-tree-layout check:i18n-keys-exist check:i18n-never-translated check:packet-variants check:profile-table check:element-bootstrap check:embed-url-absolute check:frame-target check:language-tree-model check:map-menu-anchor check:selection-kinds check:tiered-points check:whiteboard-projection check:mercator-slice check:ring-vocabulary check:map-props-unit check:mobile-gating check:provide-shadowing check:legend-top-offset check:cdn-cache check:plural-forms check:select-zone-race check:script-registry check:map-control-overlap check:poster-attribution check:board-map-options check:gospel-whiteboard check:packet-map-missing-loud check:poster-layout-optin check:ported-globals-identical check:ring-bands check:packet-a1-structure check:zone-title-tier-a check:evangelical-share check:map-labels check:packet-offline check:help-reachable check:gated-not-in-tree check:window-globals-released check:legend-data-contract check:no-new-driver-references check:poster-fetch-guard check:people-group-records check:sentence-slots check:popup-href-escaped check:popup-pan-controls check:viewport-meta check:viewport-meta:self-test check:label-key-coverage check:poster-browser-equiv check:poster-render-pinned check:host-locale-fetch check:print-frame-reclaim check:click-sheet check:coverage-consistent:chained check:coverage-consistent:poison check:dashboard-filter-a11y check:filter-multivalue check:hover-channel check:kit-options-reachable check:layout-key-namespaced check:legend-mirrors check:map-unmount-race check:no-conflict-markers check:series-isolation check:snapshot-guard check:stale-sweep-claim check:surfaces-read-the-tier check:translator-contacts check:dxlife check:wall-wysiwyg check:wall-print-watch check:wall-no-bg-photos check:template-i18n check:list-pattern-doc check:shadow-style-labels-unique check:grid-image-loading check:retry-fetch", "gen:iso3-to-alpha2": "node library/scripts/generate-iso3-to-alpha2.mjs", "internet:adm1": "node plugin-designs/cities-internet/scripts/build-adm1-internet.mjs", "library-index": "node internal/generate-library-index.mjs", @@ -521,6 +522,7 @@ "check:mobile": "deploy-2 CONTRACT check: needs NO JDK/SDK/device. cap sync is a file copy + plugin wiring, so it runs anywhere; cap:verify then asserts assets/public/index.html exists. Guards the SOFT FAILURE capacitor.config.json warns about: a wrong webDir yields an EMPTY assets/public and a BLANK WHITE SCREEN at launch, not an error.", "check:mst-clustering": "the MST clustering mode draws a true minimum spanning tree/forest (same edge count and length as a reference Kruskal) and 2,107 pins take under 3 s on the main thread. Headless.", "check:nav-reachable": "a cap-5 shell must have ONE on-screen control that opens its tab rail at phone width (needs `bun run serve` on 4173; browser gate, not in build:unlocked)", + "check:grid-merge-defaults": "the people-group grid's merge props (filtersToggle, backdrop, backdropFloor) must DEFAULT to today's grid, read from the SFC's defineProps by parser. ON DEMAND: those props are not on development yet (the grid merge, b1e839a33, is not landed), so it would be red in any chain; it chains with that merge.", "check:no-abs-paths": "no /home/… or /Users/… in any TRACKED file (a clone must work on any machine)", "check:no-codename": "the development-era INTERNAL CODE NAME may not ship in any TRACKED file. Same hand-off family as check:no-abs-paths: meaningless to the Doxa team who inherit this, and invisible to every other gate (none read for vocabulary). Deliberately does NOT fail on the same letters appearing as the unix USERNAME inside a /home/… path — that is a portability defect check:no-abs-paths already reports better, and two gates disagreeing about one string teaches everyone to ignore both.", "check:no-conflict-markers": "Nothing tracked may carry an unresolved merge. Exists because a merge from this worktree committed a BUILT bundle with 21 conflict markers - both sides concatenated, 1.13MB - and every gospel leaf rendered dead while the SRI hash matched and every request returned 200. The cause was a landing script that resolved one expected file then ran \"git add -A public\", staging every other file including the ones still holding markers. For a built file: take either side, REBUILD, commit the rebuild, then run this before pushing. --poison for the negative test.", diff --git a/vite.config.js.timestamp-1790698770542-9b99b49958ff6.mjs b/vite.config.js.timestamp-1790698770542-9b99b49958ff6.mjs deleted file mode 100644 index 95bde7fd7..000000000 --- a/vite.config.js.timestamp-1790698770542-9b99b49958ff6.mjs +++ /dev/null @@ -1,1982 +0,0 @@ -var __defProp = Object.defineProperty; -var __getOwnPropNames = Object.getOwnPropertyNames; -var __esm = (fn, res) => function __init() { - return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res; -}; -var __export = (target, all) => { - for (var name in all) - __defProp(target, name, { get: all[name], enumerable: true }); -}; - -// internal/i18n-areas.mjs -var i18n_areas_exports = {}; -__export(i18n_areas_exports, { - allAreas: () => allAreas, - areasForBundle: () => areasForBundle, - bundleGraph: () => bundleGraph -}); -import { readFileSync as readFileSync6, existsSync as existsSync5, readdirSync as readdirSync4, statSync as statSync3 } from "node:fs"; -import { resolve as resolve7, dirname as dirname5, join as join3, relative } from "node:path"; -function allAreas(root) { - return readdirSync4(join3(root, "library/i18n/locales/en")).filter((f) => f.endsWith(".json")).map((f) => f.replace(/\.json$/, "")).sort(); -} -function resolveFile(p) { - for (const cand of [p, p + ".js", p + ".mjs", p + ".vue", p + ".json", join3(p, "index.js")]) { - try { - if (existsSync5(cand) && statSync3(cand).isFile()) return cand; - } catch { - } - } - return null; -} -function expandGlob(pattern, fromDir) { - const abs = resolve7(fromDir, pattern); - const parts = abs.split("/"); - const out = []; - const walk = (i, dir) => { - if (i === parts.length) return; - const seg = parts[i]; - if (i === 0) return walk(1, "/"); - if (!seg.includes("*")) { - const next = join3(dir, seg); - if (i === parts.length - 1) { - if (existsSync5(next)) out.push(next); - } else walk(i + 1, next); - return; - } - const re = new RegExp("^" + seg.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*\*/g, ".*").replace(/\*/g, "[^/]*") + "$"); - let entries = []; - try { - entries = readdirSync4(dir); - } catch { - return; - } - for (const e of entries) if (re.test(e)) { - if (i === parts.length - 1) out.push(join3(dir, e)); - else walk(i + 1, join3(dir, e)); - } - }; - walk(0, "/"); - return out; -} -function bundleGraph(root, bundle) { - const entry = join3(root, "plugin-designs", bundle, "index.js"); - const seen = /* @__PURE__ */ new Set(); - const queue = [entry]; - const LOCALES = join3(root, "library/i18n/locales"); - while (queue.length) { - const f = queue.pop(); - if (!f || seen.has(f) || f.startsWith(LOCALES) || f.includes("/node_modules/")) continue; - seen.add(f); - let src2 = ""; - try { - src2 = readFileSync6(f, "utf8"); - } catch { - continue; - } - const dir = dirname5(f); - const add = (spec) => { - if (!spec) return; - let p = null; - if (spec.startsWith("@map/")) p = join3(root, "library", spec.slice(5)); - else if (spec.startsWith(".") || spec.startsWith("/")) p = spec.startsWith("/") ? join3(root, spec) : resolve7(dir, spec); - else return; - p = p.replace(/\?.*$/, ""); - const r = resolveFile(p); - if (r) queue.push(r); - }; - for (const m of src2.matchAll(IMPORT_RE)) add(m[1] || m[2] || m[3]); - for (const m of src2.matchAll(GLOB_RE)) { - const pats = m[1].startsWith("[") ? [...m[1].matchAll(/['"]([^'"]+)['"]/g)].map((x) => x[1]) : [m[1].slice(1, -1)]; - for (const pat of pats) { - if (pat.startsWith("!")) continue; - const base = pat.startsWith("@map/") ? join3(root, "library") : dir; - for (const hit of expandGlob(pat.replace(/^@map\//, ""), base)) queue.push(hit); - } - } - } - return [...seen]; -} -function areasForBundle(root, bundle, { extra = [] } = {}) { - const areas = allAreas(root); - const files = bundleGraph(root, bundle); - const text = files.map((f) => { - try { - return readFileSync6(f, "utf8"); - } catch { - return ""; - } - }).join("\n"); - const used = new Set(extra); - const assemblesUnknownKeys = /\b(?:t|te|tm|tStatic|T)\(\s*`\$\{/.test(text); - for (const a of areas) { - const esc = a.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); - if (new RegExp(`['"\`]${esc}\\.`).test(text)) { - used.add(a); - continue; - } - if (new RegExp(`\\b(?:useOptionalT|useOptionalI18n|qualify|tm|t|te|tStatic|rt)\\(\\s*['"\`]${esc}['"\`]`).test(text)) { - used.add(a); - continue; - } - if (assemblesUnknownKeys && new RegExp(`['"\`]${esc}['"\`]`).test(text)) used.add(a); - } - return { areas: [...used].filter((a) => areas.includes(a)).sort(), files: files.map((f) => relative(root, f)) }; -} -var IMPORT_RE, GLOB_RE; -var init_i18n_areas = __esm({ - "internal/i18n-areas.mjs"() { - IMPORT_RE = /(?:import|export)\s[^'"`;]*?from\s*['"]([^'"]+)['"]|import\s*['"]([^'"]+)['"]|import\s*\(\s*['"]([^'"]+)['"]\s*\)/g; - GLOB_RE = /import\.meta\.glob\s*\(\s*(\[[^\]]*\]|['"][^'"]+['"])/g; - } -}); - -// vite.config.js -import { defineConfig, loadEnv } from "file:///home/lk10x/Workspace/eco-dx/root-doxa-maps/doxa-maps-plugin-studio/node_modules/vite/dist/node/index.js"; -import vue from "file:///home/lk10x/Workspace/eco-dx/root-doxa-maps/doxa-maps-plugin-studio/node_modules/@vitejs/plugin-vue/dist/index.mjs"; -import cssInjectedByJsPlugin from "file:///home/lk10x/Workspace/eco-dx/root-doxa-maps/doxa-maps-plugin-studio/node_modules/vite-plugin-css-injected-by-js/dist/esm/index.js"; - -// internal/plugin-gallery/dev-plugin.mjs -import { resolve as resolve4, extname } from "node:path"; -import { spawnSync } from "node:child_process"; -import { readdirSync as readdirSync3, statSync as statSync2, existsSync as existsSync3, readFileSync as readFileSync5 } from "node:fs"; - -// internal/plugin-gallery/discover.mjs -import { resolve, dirname } from "node:path"; -import { fileURLToPath } from "node:url"; -import { readdirSync, statSync, existsSync, readFileSync } from "node:fs"; -var __vite_injected_original_import_meta_url = "file:///home/lk10x/Workspace/eco-dx/root-doxa-maps/wt-hf/internal/plugin-gallery/discover.mjs"; -var REPO_ROOT = resolve(dirname(fileURLToPath(__vite_injected_original_import_meta_url)), "..", ".."); -var PROFILE_TIER = { - "nested-mfe-spa": 0, - "nested-elements-mfe": 1, - "monolithic": 2, - "parameterized": 3, - "single": 4 -}; -var TYPE_DISPLAY = { - "single": "single-element", - "parameterized": "preset-element", - "monolithic": "multi-view-element", - "nested-elements-mfe": "composite-element", - "nested-mfe-spa": "app-shell" -}; -var TYPE_ALIASES = Object.fromEntries(Object.entries(TYPE_DISPLAY).map(([k, v]) => [v, k])); -var normalizeType = (t) => t && PROFILE_TIER[t] != null ? t : TYPE_ALIASES[t] || null; -function readJsonSafe(p) { - try { - return JSON.parse(readFileSync(p, "utf8")); - } catch { - return null; - } -} -function detectProfileType(dir, profile, ownTag, allTags, hasInstances) { - const meta = readJsonSafe(resolve(dir, `${profile}.meta.json`)); - const declared = meta && normalizeType(meta.type); - if (declared) return { type: declared, label: meta.label || profile }; - const type = hasInstances ? "parameterized" : "single"; - try { - const childVue = countChildVue(dir); - if (childVue >= 5) { - console.warn( - `[discover] ${profile}: type INFERRED as "${type}" but the bundle has ${childVue} child .vue components. Tier 3+ is never inferred \u2014 if this nests multiple views internally it is "monolithic"; if its children are independently embeddable it is "nested-elements-mfe". Declare it in ${profile}.meta.json {"type": ...} or this ships understated.` - ); - } - } catch { - } - return { type, label: meta && meta.label || profile }; -} -function countChildVue(dir) { - let n = 0; - const walk = (d, depth) => { - for (const e of readdirSync(d, { withFileTypes: true })) { - if (e.name.startsWith(".") || e.name === "node_modules") continue; - const p = resolve(d, e.name); - if (e.isDirectory()) walk(p, depth + 1); - else if (depth > 0 && e.name.endsWith(".vue")) n++; - } - }; - walk(dir, 0); - return n; -} -function discoverSpaRegistry(root = REPO_ROOT) { - const APP_DIR = resolve(root, "plugin-designs"); - const bundles = []; - if (!existsSync(APP_DIR)) return bundles; - const dirs = readdirSync(APP_DIR).sort().filter((n) => { - const d = resolve(APP_DIR, n); - return statSync(d).isDirectory() && n !== "template-bundle" && existsSync(resolve(d, "index.js")); - }); - const tagOf = {}; - for (const n of dirs) { - try { - const m = readFileSync(resolve(APP_DIR, n, "index.js"), "utf8").match(/customElements\.define\(\s*['"]([^'"]+)['"]/); - if (m) tagOf[n] = m[1]; - } catch { - } - } - const allTags = Object.values(tagOf); - for (const n of dirs) { - const dir = resolve(APP_DIR, n), tag = tagOf[n]; - if (!tag) continue; - const files = readdirSync(dir); - const instFile = files.find((f) => /\.instances\.json$/i.test(f)); - const rawInst = instFile ? readJsonSafe(resolve(dir, instFile)) : null; - const instances = Array.isArray(rawInst) ? rawInst : rawInst && Array.isArray(rawInst.instances) ? rawInst.instances : null; - const hasInstances = !!(instances && instances.length); - const instApi = rawInst && !Array.isArray(rawInst) && typeof rawInst.apiBaseUrl === "string" ? rawInst.apiBaseUrl : ""; - const items = []; - for (const profile of files.filter((f) => f.endsWith(".vue")).map((f) => f.replace(/\.vue$/, "")).sort()) { - const t = detectProfileType(dir, profile, tag, allTags, hasInstances); - if (t.type === "parameterized" && hasInstances) { - for (const inst of instances) { - items.push({ profile, id: inst.id || inst.title || profile, label: inst.title || inst.id || profile, type: "parameterized", props: inst.props || {}, apiBaseUrl: instApi }); - } - } else { - items.push({ profile, id: profile, label: t.label, type: t.type, props: null }); - } - } - if (!items.length) continue; - items.sort((a, b) => (PROFILE_TIER[a.type] ?? 9) - (PROFILE_TIER[b.type] ?? 9)); - const bestTier = Math.min(...items.map((i) => PROFILE_TIER[i.type] ?? 9)); - bundles.push({ bundle: n, tag, items, bestTier }); - } - bundles.sort((a, b) => a.bestTier - b.bestTier || a.bundle.localeCompare(b.bundle)); - return bundles; -} - -// internal/generate-gallery.mjs -import { readFileSync as readFileSync3, writeFileSync, existsSync as existsSync2, readdirSync as readdirSync2, mkdirSync } from "node:fs"; -import { join, resolve as resolve3, dirname as dirname3 } from "node:path"; -import { fileURLToPath as fileURLToPath3, pathToFileURL } from "node:url"; - -// internal/_tree-name.mjs -import { readFileSync as readFileSync2 } from "node:fs"; -import { resolve as resolve2, dirname as dirname2 } from "node:path"; -import { fileURLToPath as fileURLToPath2 } from "node:url"; -var __vite_injected_original_import_meta_url2 = "file:///home/lk10x/Workspace/eco-dx/root-doxa-maps/wt-hf/internal/_tree-name.mjs"; -var ROOT = resolve2(dirname2(fileURLToPath2(__vite_injected_original_import_meta_url2)), ".."); -var BUILD_CONFIG_FILE = resolve2(ROOT, "vite.doxa-maps-build-config.json"); -var FALLBACK = "doxa-maps"; -function read() { - let name = ""; - try { - const cfg = JSON.parse(readFileSync2(BUILD_CONFIG_FILE, "utf8")); - if (cfg && typeof cfg.name === "string") name = cfg.name.trim(); - } catch { - } - if (!name) return FALLBACK; - if (!/^[a-z0-9][a-z0-9._-]*$/i.test(name) || name.includes("..")) { - throw new Error(`[_tree-name] build config "name" must be a single URL-safe path segment, got ${JSON.stringify(name)}`); - } - return name; -} -var TREE_NAME = read(); -function readGated() { - try { - const cfg = JSON.parse(readFileSync2(BUILD_CONFIG_FILE, "utf8")); - return Array.isArray(cfg.gatedProfiles) ? cfg.gatedProfiles.filter((x) => typeof x === "string" && x.trim()).map((x) => x.trim()) : []; - } catch { - return []; - } -} -var GATED_PROFILES = readGated(); -var TREE_NAME_RE = TREE_NAME.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); -if (process.argv[1] && resolve2(process.argv[1]) === fileURLToPath2(__vite_injected_original_import_meta_url2)) process.stdout.write(TREE_NAME + "\n"); - -// internal/generate-gallery.mjs -var __vite_injected_original_import_meta_url3 = "file:///home/lk10x/Workspace/eco-dx/root-doxa-maps/wt-hf/internal/generate-gallery.mjs"; -var ROOT2 = resolve3(dirname3(fileURLToPath3(__vite_injected_original_import_meta_url3)), ".."); -function readSourceTiers(appDir = join(ROOT2, "plugin-designs")) { - const out = {}; - if (!existsSync2(appDir)) return out; - let dirs = []; - try { - dirs = readdirSync2(appDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name); - } catch { - return out; - } - for (const bundle of dirs) { - const dir = join(appDir, bundle); - let files = []; - try { - files = readdirSync2(dir); - } catch { - continue; - } - const hasInstances = files.some((f) => /\.instances\.json$/i.test(f)); - for (const f of files) { - if (!f.endsWith(".vue")) continue; - const profile = f.replace(/\.vue$/, ""); - try { - const t = detectProfileType(dir, profile, "", [], hasInstances); - out[bundle + "/" + profile] = t.type; - } catch { - } - } - } - return out; -} -function readBuildConfig() { - let cfg = {}; - try { - cfg = JSON.parse(readFileSync3(join(ROOT2, "vite.doxa-maps-build-config.json"), "utf8")); - } catch { - } - const str = (k) => typeof cfg[k] === "string" ? cfg[k].trim() : ""; - let tokenUrl = str("tokenUrl"); - if (!tokenUrl) { - try { - const src2 = JSON.parse(readFileSync3(join(ROOT2, "library/api/sources.json"), "utf8")); - const v = src2?._meta?.mapboxTokenUrl || src2?.mapboxTokenUrl; - if (typeof v === "string" && v.trim()) tokenUrl = v.trim(); - } catch { - } - } - return { - name: str("name") || TREE_NAME, - title: str("displayTitle") || TREE_NAME, - tokenUrl: tokenUrl || "window.MAP_TOKEN_URL", - token: str("token"), - apiBaseUrl: str("apiBaseUrl").replace(/\/+$/, ""), - assetsBaseUrl: str("assetsBaseUrl").replace(/\/+$/, ""), - qaWidget: cfg.qaWidget !== false - }; -} -function snip(b, p, x) { - var u = (x || p).u, tag = x && x.e || p.e || b.e[0] || b.b; - var r = { iframe: '' }; - if (!x) r.script = '\n<" + tag + " profile-config='" + JSON.stringify({ profile: p.p, tk: "YOUR_MAPBOX_TOKEN", utmSource: "YOUR_SOURCE" }) + "'>"; - return r; -} -function compact(m, lean, tierMap) { - tierMap = tierMap || {}; - var PT = { "nested-mfe-spa": 0, "nested-elements-mfe": 1, "monolithic": 2, "parameterized": 3, "single": 4 }; - var capFor = function(t) { - return PT[t] != null ? 5 - PT[t] : 1; - }; - return (m.bundles || []).map(function(src2) { - var b = { b: src2.bundle, s: src2.scriptUrl || "", i: src2.integrity || "", e: src2.elements || [], p: [] }; - b.p = (src2.profiles || []).map(function(sp) { - var type = sp.type || tierMap[src2.bundle + "/" + sp.profile] || "single"; - var c = capFor(type); - var p = { p: sp.profile, e: sp.element || "", u: sp.iframeUrl || "", d: sp.description || "", c }; - var em = sp.embed || {}, d = snip(b, p, null); - if (!lean || em.iframe !== d.iframe || em.script !== d.script) p.em = em; - var ins = (sp.instances || []).map(function(sx) { - var x = { id: sx.id, t: sx.title || sx.id, a: sx.apiBaseUrl || "", pr: sx.props || {}, e: sx.element || "", u: sx.iframeUrl || "" }; - var xe = sx.embed || {}, xd = snip(b, p, x); - if (!lean || xe.iframe !== xd.iframe) x.em = xe; - return x; - }); - if (ins.length) p.in = ins; - return p; - }); - return b; - }); -} -var src = (fn) => fn.toString().replace(/<\/script/gi, "<\\/script"); -var LADDER_CSS = String.raw`:root{--acc:#1a73e8}@media(prefers-color-scheme:dark){:root{--acc:#64b5f6}} -.ladder{position:relative;margin-left:auto;display:flex;align-items:center;gap:6px;white-space:nowrap} -.rung{display:inline-flex;align-items:center;gap:4px}.rname{font:500 12px/1 ui-monospace,Menlo,monospace} -.arrow{color:var(--mut);font-size:12px;padding:0 1px} -.q{width:18px;height:18px;padding:0;border-radius:50%;font:700 11px/1 system-ui,sans-serif;color:var(--mut);background:var(--bg);display:inline-grid;place-items:center} -.start{font:600 10.5px/1 system-ui,sans-serif;letter-spacing:.04em;text-transform:uppercase;color:var(--acc);background:var(--bg);border-radius:999px;padding:4px 9px;margin-right:4px} -.q:hover,.q[aria-expanded=true],.start:hover,.start[aria-expanded=true]{color:var(--acc);border-color:var(--acc);background:var(--bg)} -.explain{position:absolute;top:calc(100% + 8px);right:0;width:420px;max-width:calc(100vw - 32px);max-height:70vh;overflow:auto;z-index:40;background:var(--bg);border:1px solid var(--bd);border-radius:10px;box-shadow:0 4px 16px rgba(0,0,0,.14);padding:12px 14px;font-size:13px;line-height:1.45;text-align:left;white-space:normal} -.explain h3{margin:0 0 5px;font-size:14px;display:flex;align-items:center;gap:8px;flex-wrap:wrap}.explain h4{margin:8px 0 3px;font-size:13px} -.explain .tier{font:700 10px/1 system-ui,sans-serif;letter-spacing:.6px;text-transform:uppercase;color:var(--mut)} -.explain p{margin:0 0 6px}.explain .aka{font-size:11.5px;color:var(--mut);margin:-2px 0 6px} -.explain .how{margin:0 0 6px;font-size:12.5px}.explain .how summary{cursor:pointer;color:var(--acc);font-weight:600;font-size:12px}.explain .how p{margin:6px 0 0;color:var(--mut)} -.explain .ex{color:var(--mut);font-size:12px} -@media(max-width:1280px){.ladder{display:none}} -`; -var LADDER_HTML = ''; -var LADDER_JS = String.raw` -`; -function renderGallery(model, opts = {}) { - const { mode = "build", title = TREE_NAME, tree = TREE_NAME, tokenUrl = "window.MAP_TOKEN_URL", token = "", apiBaseUrl = "", assetsBaseUrl = "", qaWidget = true, tierMap = {} } = opts; - const dev = mode === "dev"; - const baked = compact(model, true, tierMap); - const json = (v) => JSON.stringify(v).replace(/ String(s).replace(/&/g, "&").replace(/ - - - - -${esc(title)} - - - - - -
-
- ${esc(title)}{{ countLabel }}${dev ? LADDER_HTML : ""} - -
-
- -
-
-
{{ ttl }}{{ sub }}
- Open page - -
-
- - -
-
-
-
Choose a map from the list.
-
-
-
- -${dev ? LADDER_JS : ""} - -`; -} -function main() { - const CFG = readBuildConfig(); - const manifestPath = join(ROOT2, "public", CFG.name, "manifest.json"); - if (!existsSync2(manifestPath)) { - console.error(`[generate-gallery] missing ${manifestPath} \u2014 run \`bun run tree\` first`); - process.exit(1); - } - const manifest = JSON.parse(readFileSync3(manifestPath, "utf8")); - const tierMap = readSourceTiers(); - const html = renderGallery(manifest, { mode: "build", title: CFG.title, tree: "../" + CFG.name, tokenUrl: CFG.tokenUrl, token: CFG.token, apiBaseUrl: CFG.apiBaseUrl, assetsBaseUrl: CFG.assetsBaseUrl, qaWidget: CFG.qaWidget, tierMap }); - const baked = compact(manifest, true, tierMap); - const nProfiles = baked.reduce((n, b) => n + b.p.length, 0); - const nInstances = baked.reduce((n, b) => n + b.p.reduce((k, p) => k + (p.in ? p.in.length : 0), 0), 0); - const nOverrides = baked.reduce((n, b) => n + b.p.reduce((k, p) => k + (p.em ? 1 : 0) + (p.in || []).filter((x) => x.em).length, 0), 0); - const oi = process.argv.indexOf("--out"); - const outDir = oi >= 0 && process.argv[oi + 1] ? resolve3(process.argv[oi + 1]) : null; - if (!outDir) { - console.log("[generate-gallery] ARCHIVED \u2014 not written. The gallery is no longer part of the build or of public/;"); - console.log(" the Plugin Studio (/index.html) is the front page. To render it anyway: bun run gallery -- --out "); - return; - } - if (outDir === resolve3(ROOT2, "public") || outDir.startsWith(resolve3(ROOT2, "public") + "/")) { - console.error("[generate-gallery] refusing to write into public/ \u2014 it holds only the output folder."); - process.exit(1); - } - const dir = outDir; - mkdirSync(dir, { recursive: true }); - writeFileSync(join(dir, "index.html"), html); - console.log(`[generate-gallery] ${dir}/index.html (ARCHIVED rendering) \u2014 ${(html.length / 1024).toFixed(1)} KB \xB7 ${baked.length} bundle(s) \xB7 ${nProfiles} profile(s) \xB7 ${nInstances} instance(s) \xB7 ${nOverrides} embed snippet(s) baked verbatim (the rest re-derive)`); -} -if (process.argv[1] && __vite_injected_original_import_meta_url3 === pathToFileURL(process.argv[1]).href) main(); - -// internal/profile-meta.mjs -import { readFileSync as readFileSync4 } from "node:fs"; -import { join as join2 } from "node:path"; -var CORNERS = ["bottom-right", "bottom-left", "top-right", "top-left"]; -var VISIBILITY = ["team", "public"]; -var MOBILE = ["desktop-only", "larger-screens"]; -function readProfileMeta(dir, profileName) { - try { - const meta = JSON.parse(readFileSync4(join2(dir, `${profileName}.meta.json`), "utf8")); - const corner = meta?.widget?.corner; - const name = [meta?.label, meta?.title, meta?.displayName].find((v) => typeof v === "string" && v.trim()); - return { - stage: meta?.stage === "production" ? "production" : "experimental", - heavy: meta?.heavy === true, - widget: CORNERS.includes(corner) ? { corner } : null, - label: name ? name.trim() : "", - // Where the plugin runs ("web", "desktop"…); absent = web. A desktop-only plugin is never a gallery tab. - targets: Array.isArray(meta?.targets) ? meta.targets.filter((t) => typeof t === "string") : null, - visibility: VISIBILITY.includes(meta?.visibility) ? meta.visibility : "team", - // The declared-but-unrecognised value, kept so the generator can name it in the failure. - // null when the key is absent (the supported way to say "team") or already valid. - visibilityInvalid: meta?.visibility === void 0 || VISIBILITY.includes(meta?.visibility) ? null : String(meta.visibility), - mobile: MOBILE.includes(meta?.mobile) ? meta.mobile : null - }; - } catch { - return { stage: "experimental", heavy: false, widget: null, label: "", targets: null, visibility: "team", visibilityInvalid: null, mobile: null }; - } -} -function profileMetaFields(dir, profileName) { - const m = readProfileMeta(dir, profileName); - return { stage: m.stage, ...m.visibility === "public" ? { visibility: "public" } : {}, ...m.label ? { label: m.label } : {}, ...m.targets ? { targets: m.targets } : {}, ...m.heavy ? { heavy: true } : {}, ...m.widget ? { widget: m.widget } : {}, ...m.mobile ? { mobile: m.mobile } : {} }; -} -var bundleStage = (profiles) => profiles.some((p) => p.stage === "production") ? "production" : "experimental"; -var bundleVisibility = (profiles) => profiles.some((p) => p.visibility === "public") ? "public" : "team"; - -// internal/plugin-gallery/dev-plugin.mjs -var DEV_ASSET_MIME = { - ".json": "application/json; charset=utf-8", - ".geojson": "application/geo+json; charset=utf-8", - ".js": "text/javascript; charset=utf-8", - ".css": "text/css; charset=utf-8", - ".csv": "text/csv; charset=utf-8", - ".txt": "text/plain; charset=utf-8", - ".html": "text/html; charset=utf-8", - ".svg": "image/svg+xml", - ".png": "image/png", - ".webp": "image/webp", - ".jpg": "image/jpeg", - ".jpeg": "image/jpeg", - ".gif": "image/gif", - ".woff2": "font/woff2", - ".woff": "font/woff", - ".pdf": "application/pdf" -}; -function resolveDevAsset(rel, root = REPO_ROOT) { - const isFile = (p) => existsSync3(p) && statSync2(p).isFile(); - const source = resolve4(root, "data", rel); - if (isFile(source)) return source; - const shipped = resolve4(root, "public", TREE_NAME, "assets", rel); - if (isFile(shipped)) return shipped; - const profiles = resolve4(root, "plugin-designs"); - for (const name of readdirSync3(profiles)) { - if (name.startsWith("_") || name.startsWith(".")) continue; - const cand = resolve4(profiles, name, "data", rel); - if (isFile(cand)) return cand; - } - return null; -} -function readComposes(bundle, profile) { - try { - const meta = JSON.parse(readFileSync5(resolve4(REPO_ROOT, "plugin-designs", bundle, `${profile}.meta.json`), "utf8")); - return Array.isArray(meta?.composes) ? meta.composes.map(String).filter(Boolean) : []; - } catch { - return []; - } -} -function galleryModel(reg) { - const iframe = (u) => ``; - const link = (...parts) => "/__leaf/" + parts.map(encodeURIComponent).join("/"); - let gated = /* @__PURE__ */ new Set(); - try { - const cfg = JSON.parse(readFileSync5(resolve4(REPO_ROOT, "vite.doxa-maps-build-config.json"), "utf8")); - if (Array.isArray(cfg.gatedProfiles)) gated = new Set(cfg.gatedProfiles.map(String)); - } catch { - } - const bundles = [...reg].filter((b) => !gated.has(b.bundle)).sort((a, b) => a.bundle.localeCompare(b.bundle)).map((b) => { - const scriptUrl = `/plugin-designs/${b.bundle}/index.js`; - const presets = /* @__PURE__ */ new Map(); - const typeOf = /* @__PURE__ */ new Map(); - for (const it of b.items) { - if (!presets.has(it.profile)) presets.set(it.profile, []); - if (!typeOf.has(it.profile)) typeOf.set(it.profile, it.type); - if (it.type === "parameterized") presets.get(it.profile).push(it); - } - const profiles = [...presets.keys()].sort().map((profile) => { - const p = { - profile, - file: `${profile}.vue`, - description: "", - element: b.tag, - type: typeOf.get(profile) || "single", - ...profileMetaFields(resolve4(REPO_ROOT, "plugin-designs", b.bundle), profile), - // same as the built manifest - iframeUrl: link(b.bundle, profile), - scriptUrl, - embed: { - iframe: iframe(`./${b.bundle}/${profile}/index.html`), - script: ` -<${b.tag} profile-config='${JSON.stringify({ profile, tk: "YOUR_MAPBOX_TOKEN", utmSource: "YOUR_SOURCE" })}'>` - } - }; - const instances = presets.get(profile).map((it) => ({ - id: it.id, - title: it.label, - apiBaseUrl: it.apiBaseUrl || "", - props: { profile, ...it.props || {} }, - element: b.tag, - iframeUrl: link(b.bundle, profile, it.id), - embed: { iframe: iframe(`./${b.bundle}/${profile}/${it.id}/index.html`) } - })); - if (instances.length) p.instances = instances; - return p; - }); - const composes = [...new Set([...presets.keys()].flatMap((profile) => readComposes(b.bundle, profile)))]; - return { bundle: b.bundle, scriptUrl, integrity: "", elements: [b.tag], stage: bundleStage(profiles), ...bundleVisibility(profiles) === "public" ? { visibility: "public" } : {}, ...composes.length ? { composes } : {}, profiles }; - }); - return { - schema: "doxa-maps-manifest@1", - generatedFrom: "plugin-designs/*/*.vue (dev discovery)", - embedBase: ".", - bundleCount: bundles.length, - profileCount: bundles.reduce((n, b) => n + b.profiles.length, 0), - bundles - }; -} -function leafRewrite(path, treeName = TREE_NAME) { - if (!path.endsWith("/index.html")) return ""; - let segs = path.slice(0, -"/index.html".length).split("/").filter(Boolean); - if (segs[0] === treeName) segs = segs.slice(1); - if (segs.length < 2 || segs.length > 3) return ""; - for (const s of segs) { - let d = s; - try { - d = decodeURIComponent(s); - } catch { - return ""; - } - if (!d || d === "." || d === ".." || d.includes("/") || d.includes("\\")) return ""; - } - return "/__leaf/" + segs.join("/"); -} -function autoSpaDevPlugin(token = "") { - const page = () => { - const cfg = readBuildConfig(); - return renderGallery(galleryModel(discoverSpaRegistry()), { - // ★ `/__plugin-gallery`, ABSOLUTE — the SAME move, and the same fix, as assetsBaseUrl below. - // `tree` is the folder the live manifest is fetched from and that relative refs resolve - // against. It was '__plugin-gallery' (relative), correct only while the gallery WAS the root - // page. The gallery now lives at /__gallery, so the relative form resolves one level deeper. - // MEASURED 2026-09-22 on the running dev server: - // /__plugin-gallery/manifest.json ............. 200 - // /__gallery/__plugin-gallery/manifest.json ... 404 - // Naming it absolutely makes the value independent of the page depth the gallery sits at — - // which is the whole reason assetsBaseUrl was made absolute in the same move. The built tree - // passes '../' for the same reason; both now resolve to the real tree root. - mode: "dev", - title: cfg.title, - tree: "/__plugin-gallery", - tokenUrl: cfg.tokenUrl, - token: token || cfg.token, - // ★ ABSOLUTE, NOT RELATIVE — the gallery no longer lives at the root. - // This was '' (relative), which was correct while the gallery WAS the root page: a map asked - // for assets/tables/x.json and got /assets/tables/x.json. The gallery has since moved to - // /__gallery so the studio can own /, and the same relative path now resolves one level deeper - // to /__gallery/assets/... — which 404s. MEASURED 2026-09-22 on the running dev server: - // /assets/tables/internet-by-country.json ............. 200 - // /__gallery/assets/tables/internet-by-country.json ... 404 - // Nothing failed at move time; the breakage only appears when a map FETCHES, and it surfaces as - // "JSON.parse: unexpected end of data" rather than as a 404, because Vite answers an unknown - // path with HTML. The dev server serves the asset tree at /assets/ (see resolveDevAsset), so - // naming it absolutely makes the value independent of whatever page depth the gallery sits at. - apiBaseUrl: cfg.apiBaseUrl, - assetsBaseUrl: "/assets", - qaWidget: cfg.qaWidget - }); - }; - const json = (res, body, status = 200) => { - res.statusCode = status; - res.setHeader("Content-Type", "application/json; charset=utf-8"); - res.setHeader("Cache-Control", "no-cache"); - res.end(JSON.stringify(body)); - }; - return { - name: "doxa-auto-spa", - apply: "serve", - configureServer(server) { - server.middlewares.use((req, res, next) => { - const rawPath = (req.url || "").split("?")[0]; - let _decoded = rawPath; - try { - _decoded = decodeURIComponent(rawPath); - } catch { - } - if (/(^|\/)\.(git|env)(\/|$)|(^|\/)\.(merge-queue|main-capture)(\/|$)/i.test(_decoded)) { - res.statusCode = 404; - return res.end(); - } - let treeName = TREE_NAME; - try { - treeName = readBuildConfig()?.name || treeName; - } catch { - } - const rewritten = leafRewrite(rawPath, treeName); - const path = rewritten || rawPath; - if (rewritten) req.url = rewritten + (req.url || "").slice(rawPath.length); - const iife = /^\/(?:([^/]+)\/)?([a-z0-9][a-z0-9-]*)\/([a-z0-9][a-z0-9-]*)\.js$/.exec(rawPath); - if (iife && iife[2] === iife[3] && (!iife[1] || iife[1] === treeName)) { - const file = resolve4(REPO_ROOT, "public", treeName, iife[2], iife[2] + ".js"); - if (existsSync3(file)) { - res.setHeader("Content-Type", "text/javascript; charset=utf-8"); - res.setHeader("Cache-Control", "no-cache"); - return res.end(readFileSync5(file)); - } - if (existsSync3(resolve4(REPO_ROOT, "plugin-designs", iife[2]))) { - res.statusCode = 404; - res.setHeader("Content-Type", "text/plain; charset=utf-8"); - return res.end(`${iife[2]}.js is only produced by a build: run \`bun run build\` (or BUNDLE=${iife[2]} bun x vite build), then reload.`); - } - } - const leafAsset = /^\/__leaf\/(?:[^/]+\/){0,3}assets\//.exec(path); - const assetPrefix = path.startsWith("/assets/") ? "/assets/" : path.startsWith("/__plugin-gallery/assets/") ? "/__plugin-gallery/assets/" : path.startsWith(`/${treeName}/assets/`) ? `/${treeName}/assets/` : leafAsset ? leafAsset[0] : ""; - if (assetPrefix) { - let rel = ""; - try { - rel = decodeURIComponent(path.slice(assetPrefix.length)); - } catch { - rel = ""; - } - if (!rel || rel.split("/").includes("..")) { - res.statusCode = 400; - return res.end("bad asset path"); - } - const file = resolveDevAsset(rel); - if (!file) return json(res, { error: "asset not found", asset: rel, looked: [`public/${TREE_NAME}/assets/`, "data/", "plugin-designs/*/data/"] }, 404); - res.statusCode = 200; - res.setHeader("Content-Type", DEV_ASSET_MIME[extname(file).toLowerCase()] || "application/octet-stream"); - res.setHeader("Cache-Control", "no-cache"); - return res.end(readFileSync5(file)); - } - if (path === "/__dev-sha") { - const git2 = (...a) => spawnSync("git", a, { cwd: REPO_ROOT, encoding: "utf8" }).stdout.trim(); - const sha = git2("rev-parse", "HEAD"); - const dirty = git2("status", "--porcelain", "--untracked-files=no", "--", "library", "plugin-designs", "internal", "vite.config.js", "package.json") ? "+dirty" : ""; - res.statusCode = sha ? 200 : 500; - res.setHeader("Content-Type", "text/plain; charset=utf-8"); - res.setHeader("Cache-Control", "no-store"); - return res.end((sha || "unknown") + dirty + "\n"); - } - if (path === "/__plugin-gallery/registry.json" || path === "/__plugin-gallery/manifest.json") { - let reg; - try { - reg = discoverSpaRegistry(); - } catch (e) { - return json(res, { error: String(e) }, 500); - } - return json(res, path.endsWith("/manifest.json") ? galleryModel(reg) : reg); - } - if (path.startsWith("/__leaf/")) { - const parts = path.slice("/__leaf/".length).split("/").filter(Boolean).map((s) => { - try { - return decodeURIComponent(s); - } catch { - return ""; - } - }); - const [bundle, profile, instance] = parts; - if (!bundle || !profile || parts.some((s) => !s || s.includes("..") || s.includes("/"))) { - res.statusCode = 400; - return res.end("bad leaf path"); - } - let reg; - try { - reg = discoverSpaRegistry(); - } catch (e) { - return json(res, { error: String(e) }, 500); - } - const entry = reg.find((b) => b.bundle === bundle); - if (!entry) { - const named = reg.find((b) => b.bundle === profile); - const hint = named ? `this plugin's own page is /__leaf/${named.bundle}/${named.items?.[0]?.profile || named.bundle}` : "a plugin's own page is /__leaf//; every plugin is listed at /__plugin-gallery/manifest.json"; - return json(res, { error: "unknown bundle", bundle, hint }, 404); - } - const tag = entry.tag; - let leafCfg = {}; - try { - leafCfg = readBuildConfig() || {}; - } catch { - leafCfg = {}; - } - const leafTok = token || leafCfg.token || ""; - const apiBase = leafCfg.apiBaseUrl || ""; - const pc = JSON.stringify(instance ? { profile, instanceId: instance, assetsBase: "/assets" } : { profile, assetsBase: "/assets" }); - const scrollPage = (entry.items || []).some( - (it) => it.profile === profile && it.type === "nested-elements-mfe" - ); - res.statusCode = 200; - res.setHeader("Content-Type", "text/html; charset=utf-8"); - res.setHeader("Cache-Control", "no-cache"); - return res.end([ - "", - `${bundle} \xB7 ${profile}`, - '', - '', - leafTok ? `` : "", - // ★ MAP_APP_API_URL IS NOT OPTIONAL — without it every dashboard renders EMPTY in dev. - // The BUILT leaves bake `window.MAP_APP_API_URL = "https://pray.doxa.life"`; these dev - // pages baked only the Mapbox token, so the dashboards fell back to a RELATIVE - // `/api/people-groups/list?...`. Vite answers an unknown path with SPA-fallback HTML at - // HTTP 200, so the fetch "succeeds", JSON.parse fails or yields nothing, and the facet - // rail renders ZERO rows with no console error and no error card. Measured 2026-09-22: - // 8 such 404/fallback requests during one studio sweep. That reads as a broken component - // and is really a missing global — the worst kind of dev-only divergence, and exactly the - // four-surface class (see REFERENCE-the-four-surface-contract.md). - apiBase ? `` : "", - // ── FIXED-VIEWPORT LEAF vs SCROLL-PAGE LEAF ──────────────────────────────────────── - // The BUILT tree makes exactly this distinction (internal/generate-tree.mjs, `scrollPage`) - // and this dev leaf did NOT — it served the fixed-viewport style to EVERY bundle. That is - // the whole of BUG-CANNOT-SCROLL-DOWN-TO-THE-GRID-UNDER-THE-DASHBOARD: the three-file fix - // for BUG-DASHBOARD-CANT-SCROLL-DOWN landed in the built tree only, so the dev URL that - // actually gets tested was never covered and still traps the composite. - // - // WHY `height:100%` ON BODY IS THE TRAP, and why scrollHeight LIES about it. A definite - // height + `overflow:visible` lets content SPILL (so scrollHeight inflates — measured 3531 - // against a 900 viewport) WITHOUT creating a scrollport, so the viewport has nothing to - // scroll. The research-dashboard's own inner `overflow:auto` regions then swallow every - // wheel event. MEASURED 2026-09-22 on this leaf: 12 wheel events of 600px moved the page - // 0px while 2,631px of content sat below the fold. A geometry check calls that "scrollable"; - // only a real wheel exposes it. - // - // A cap-4 composite (nested-elements-mfe) is a StackLayout scroll-stack, so the DOCUMENT - // must own the scroll: `min-height` (a floor, not a cage) + explicit `overflow-y:auto`, - // and the element takes its NATURAL height so the page grows and the grid below is - // reachable. Every other tier keeps the fixed-viewport leaf — a map IS the page. - scrollPage ? `` : ``, - "", - // tree-base ON THE LEAF TOO — cap-5 shells nested here fetch a manifest of their own. - // doxa-maps-spa (like plugin-studio) discovers its tabs from the tree's manifest.json and - // resolves it RELATIVE to its own page when no base is given. On a /__leaf/ URL that - // resolved to /__leaf/manifest.json and 400'd. Harmless (both shells carry a baked - // fallback list) but it silently degrades the tab list to the fallback instead of the - // live 19-bundle tree. Passing the base makes a nested shell see exactly what the top - // level sees. Elements that ignore tree-base simply drop an unknown attribute. - `<${tag} tree-base="/__plugin-gallery" profile-config='${pc}'>`, - ``, - "" - ].filter(Boolean).join("\n")); - } - if (path === "/" || path === "/index.html") { - let devCfg = {}; - try { - devCfg = readBuildConfig() || {}; - } catch { - devCfg = {}; - } - const devTok = token || devCfg.token || ""; - const devApiBase = devCfg.apiBaseUrl || ""; - res.statusCode = 200; - res.setHeader("Content-Type", "text/html; charset=utf-8"); - res.setHeader("Cache-Control", "no-cache"); - return res.end([ - "", - '', - '', - `${TREE_NAME} plugin studio`, - '', - // The token ladder's baked rung, exactly as the built leaves receive it, so maps paint - // in dev without anyone appending ?tk=. - devTok ? `` : "", - // Same reason as the leaf route above: without MAP_APP_API_URL the dashboards nested in - // the studio fetch a relative /api/... that Vite answers with HTML at 200, so they mount - // and then show nothing. The value comes from the SAME build config the built leaves use, - // so dev and built agree by construction rather than by a second hard-coded copy. - devApiBase ? `` : "", - // No chrome, no prose: the element fills the viewport. "Without a lot of text garb - // that takes up so much space" is a layout requirement, so the host page contributes - // ZERO text — every pixel belongs to the studio element. - "", - "", - // tree-base IS REQUIRED IN DEV — without it the studio mounts EMPTY. - // The studio builds its tab list at runtime by fetching `manifest.json`, and with - // no base it falls back to '../manifest.json' RELATIVE TO ITS SCRIPT URL. In dev the - // script is /plugin-designs/plugin-studio/index.js, so that resolves to - // /plugin-designs/manifest.json — which does not exist. The shell then renders with zero - // rows: 31 shadow descendants, no nested plugin tags. (Measured exactly that before this - // line existed; the symptom reads as "no elements in the studio", not as a failed fetch.) - // The dev server's manifest lives on its own route instead, serving the SAME - // manifest-shaped model the built tree bakes (19 bundles / 30 profiles), so pointing the - // studio at it gives dev the same tab list production has. - '', - '', - "" - ].filter(Boolean).join("\n")); - } - if (path !== "/__gallery" && path !== "/__gallery/") return next(); - let html; - try { - html = page(); - } catch (e) { - console.error(`[plugin-gallery] ${e && e.stack ? e.stack : e}`); - return next(); - } - res.statusCode = 200; - res.setHeader("Content-Type", "text/html; charset=utf-8"); - res.end(html); - }); - } - }; -} - -// internal/vite-plugin-strip-shadow-css-comments.mjs -import { resolve as resolve5, dirname as dirname4, sep } from "node:path"; -import { fileURLToPath as fileURLToPath4 } from "node:url"; -var __vite_injected_original_import_meta_url4 = "file:///home/lk10x/Workspace/eco-dx/root-doxa-maps/wt-hf/internal/vite-plugin-strip-shadow-css-comments.mjs"; -var ROOT3 = resolve5(dirname4(fileURLToPath4(__vite_injected_original_import_meta_url4)), ".."); -var SOURCE_ROOTS = ["library", "plugin-designs"].map((d) => resolve5(ROOT3, d) + sep); -var TAG = "[strip-shadow-css-comments]"; -var CALL = /\buseShadowStyles\(\s*(?:[A-Za-z_$][\w$]*\s*\+\s*)*`|\b(?:export\s+)?const\s+[A-Z][A-Z0-9_]*(?:_CSS|_STYLES)\s*=\s*`/g; -function findTemplateEnd(code, from) { - for (let i = from; i < code.length; i++) { - const c = code[i]; - if (c === "\\") { - i++; - continue; - } - if (c === "`") return { end: i }; - if (c === "$" && code[i + 1] === "{") return { reason: "contains a ${\u2026} substitution", at: i }; - } - return { reason: "template literal never closes", at: code.length }; -} -function stripCssComments(css) { - const n = css.length; - let out = ""; - let comments = 0; - let i = 0; - while (i < n) { - const c = css[i]; - if (c === '"' || c === "'") { - let j = i + 1; - while (j < n && css[j] !== c && css[j] !== "\n") { - if (css[j] === "\\") j++; - j++; - } - if (j >= n) return { reason: "a CSS string never closes" }; - if (css[j] === "\n") { - out += css.slice(i, j); - i = j; - continue; - } - out += css.slice(i, j + 1); - i = j + 1; - continue; - } - if (c === "/" && css[i + 1] === "*") { - const j = css.indexOf("*/", i + 2); - if (j < 0) return { reason: "a CSS comment never closes" }; - comments++; - i = j + 2; - continue; - } - if ((c === "u" || c === "U") && /^url\(/i.test(css.slice(i, i + 4)) && (i === 0 || !/[\w-]/.test(css[i - 1]))) { - let j = i + 4; - while (j < n && (css[j] === " " || css[j] === " ")) j++; - if (css[j] === '"' || css[j] === "'") { - out += css.slice(i, j); - i = j; - continue; - } - const k = css.indexOf(")", j); - if (k < 0) return { reason: "an unquoted url() never closes" }; - out += css.slice(i, k + 1); - i = k + 1; - continue; - } - out += c; - i++; - } - if (comments > 0) { - out = out.replace(/[ \t]+(?=\r?\n|$)/g, ""); - out = out.replace(/\r?\n(?:[ \t]*\r?\n)+/g, "\n"); - } - return { css: out, comments }; -} -function stripShadowStyleComments(code) { - const result = { code, changed: false, literals: 0, comments: 0, bytes: 0, skipped: [] }; - let out = ""; - let last = 0; - CALL.lastIndex = 0; - let m; - while (m = CALL.exec(code)) { - const open = m.index + m[0].length; - const lit = findTemplateEnd(code, open); - if (lit.reason) { - result.skipped.push({ line: lineOf(code, m.index), reason: lit.reason }); - CALL.lastIndex = lit.at; - continue; - } - result.literals++; - const original = code.slice(open, lit.end); - const stripped = stripCssComments(original); - if (stripped.reason) { - result.skipped.push({ line: lineOf(code, m.index), reason: stripped.reason }); - } else if (stripped.css !== original) { - out += code.slice(last, open) + stripped.css; - last = lit.end; - result.comments += stripped.comments; - result.bytes += original.length - stripped.css.length; - result.changed = true; - } - CALL.lastIndex = lit.end + 1; - } - if (result.changed) result.code = out + code.slice(last); - return result; -} -function lineOf(code, index) { - let line = 1; - for (let i = 0; i < index; i++) if (code[i] === "\n") line++; - return line; -} -function isSourceModule(id) { - const q = id.indexOf("?"); - if (q >= 0) return false; - if (!(id.endsWith(".vue") || id.endsWith(".js"))) return false; - return SOURCE_ROOTS.some((root) => id.startsWith(root)); -} -function stripShadowCssCommentsPlugin() { - let logger = console; - const totals = { files: 0, literals: 0, comments: 0, bytes: 0, skipped: 0 }; - return { - name: "doxa:strip-shadow-css-comments", - apply: "build", - enforce: "pre", - configResolved(config) { - logger = config.logger; - }, - transform(code, id) { - if (!isSourceModule(id)) return null; - const r = stripShadowStyleComments(code); - for (const s of r.skipped) { - totals.skipped++; - logger.warn(`${TAG} left untouched ${id.slice(ROOT3.length + 1)}:${s.line} \u2014 ${s.reason}`); - } - if (!r.changed) return null; - totals.files++; - totals.literals += r.literals; - totals.comments += r.comments; - totals.bytes += r.bytes; - return { code: r.code, map: null }; - }, - buildEnd() { - logger.info(`${TAG} removed ${totals.comments} CSS comments (${totals.bytes} bytes) from ${totals.literals} shadow-style literals in ${totals.files} files` + (totals.skipped ? `; ${totals.skipped} literal(s) left untouched` : "")); - } - }; -} - -// internal/backend-dev-plugin.mjs -import { spawn } from "node:child_process"; -import { existsSync as existsSync4, mkdirSync as mkdirSync2, writeFileSync as writeFileSync2, rmSync, chmodSync, renameSync, createWriteStream } from "node:fs"; -import { createHash } from "node:crypto"; -import { connect } from "node:net"; -import { inflateRawSync } from "node:zlib"; -import { resolve as resolve6 } from "node:path"; -var PB_VERSION = "0.40.4"; -var PB_SHA256 = { - linux_amd64: "9042ec818570e79c3628dadcd0a756c1496d9e1173918ec409d133c02f82e5fa", - linux_arm64: "86095bf8ed9345954f0d2bf0a5fb9b57584ae60b77ebf3b6cd23a8003a3fd418", - darwin_amd64: "052906521f09f6f23405cd804c930d2b7a1f8eee06d39b85723b5054b2acb4e4", - darwin_arm64: "eeb619ea4f8a06421daedb946d133bed269fea334a760941d147f76befc25ebc" -}; -function devBackends(env = process.env) { - const pbPort = Number(env.DX_PB_PORT || 8090); - return [ - { - name: "rudder-engine", - bin: "backend/bin/rudder-engine", - build: "cd backend/rudder-engine && go build -o ../bin/rudder-engine .", - // What a fresh clone is told at startup when this backend is not built: it is OPTIONAL (desktop voice). - optional: "optional: Rudder (desktop voice-to-text) is not built, which is fine for the maps. It needs Go 1.22+; see documentation/HOWTO-build-the-desktop-app.md.", - route: "/api/rudder", - port: Number(process.env.RUDDER_PORT || 5193), - env: { RUDDER_PORT: String(process.env.RUDDER_PORT || 5193), RUDDER_NEMO_PORT: String(process.env.RUDDER_NEMO_PORT || 5194) }, - // This server is SUPPOSED to run the engine (a server deployment with the engine switched on): while it - // is down, the plugin says "not running yet" instead of "runs on your own computer". - expected: () => process.env.RUDDER_ENGINE === "1" || process.env.RUDDER_AUDIO === "browser", - // RUDDER_ENGINE=0 (a server where Rudder is laptop-only, 2026-09-27): never start it, even if a - // binary built by an earlier engine-on deploy is still there — it would offer to record a server's mic. - disabled: () => process.env.RUDDER_ENGINE === "0" - }, - { - // Hooks off (a hooks dir that never exists): only the migrations in git shape the schema, as in the cloud. - // DX_PB_URL points this laptop at a shared PocketBase instead (e.g. the cloud pb-dev); none starts here. - name: "pocketbase", - bin: "backend/bin/pocketbase", - args: ["serve", "--http", `127.0.0.1:${pbPort}`, "--dir", ".pb/dev", "--migrationsDir", "backend/pocketbase/pb_migrations", "--hooksDir", ".pb/hooks-off"], - build: "it downloads on the next `bun run dev` (or set DX_PB_URL)", - fetch: fetchPocketBase, - portEnv: "DX_PB_PORT", - route: "/api/pb", - rewrite: (p) => p.replace(/^\/api\/pb/, "") || "/", - port: pbPort, - remote: env.DX_PB_URL || "", - offline: (res) => { - res.writeHead(503, { "Content-Type": "application/json" }); - res.end(JSON.stringify({ status: 503, message: "PocketBase is not running on this computer (yet). See the dev server log." })); - } - } - ]; -} -async function fetchPocketBase(root, logger) { - const plat = { "linux-x64": "linux_amd64", "linux-arm64": "linux_arm64", "darwin-x64": "darwin_amd64", "darwin-arm64": "darwin_arm64" }[`${process.platform}-${process.arch}`]; - if (!plat) { - logger.warn(`[pocketbase] no pinned build for ${process.platform}-${process.arch}: put PocketBase ${PB_VERSION} at backend/bin/pocketbase, or set DX_PB_URL`); - return false; - } - const name = `pocketbase_${PB_VERSION}_${plat}.zip`; - const dir = resolve6(root, "backend/bin"); - const part = resolve6(dir, `.pocketbase.part`); - logger.info(`[pocketbase] downloading ${name} (first run only)`); - try { - mkdirSync2(dir, { recursive: true }); - const r = await fetch(`https://github.com/pocketbase/pocketbase/releases/download/v${PB_VERSION}/${name}`); - if (!r.ok) throw new Error(`HTTP ${r.status}`); - const buf = Buffer.from(await r.arrayBuffer()); - const sha = createHash("sha256").update(buf).digest("hex"); - if (sha !== PB_SHA256[plat]) throw new Error(`checksum mismatch (${sha}): refusing to use it`); - writeFileSync2(part, unzipOne(buf, "pocketbase")); - chmodSync(part, 493); - renameSync(part, resolve6(dir, "pocketbase")); - return true; - } catch (e) { - logger.warn(`[pocketbase] download failed (${e.message}); /api/pb answers 503 until the next \`bun run dev\``); - return false; - } finally { - rmSync(part, { force: true }); - } -} -function unzipOne(buf, want) { - const end = buf.lastIndexOf(Buffer.from([80, 75, 5, 6])); - if (end < 0) throw new Error("not a zip"); - for (let i = 0, p = buf.readUInt32LE(end + 16); i < buf.readUInt16LE(end + 10); i++) { - const method = buf.readUInt16LE(p + 10), size = buf.readUInt32LE(p + 20); - const nameLen = buf.readUInt16LE(p + 28), local = buf.readUInt32LE(p + 42); - if (buf.toString("utf8", p + 46, p + 46 + nameLen) === want) { - const start2 = local + 30 + buf.readUInt16LE(local + 26) + buf.readUInt16LE(local + 28); - const data = buf.subarray(start2, start2 + size); - if (method === 8) return inflateRawSync(data); - if (method === 0) return data; - throw new Error(`unsupported zip method ${method}`); - } - p += 46 + nameLen + buf.readUInt16LE(p + 30) + buf.readUInt16LE(p + 32); - } - throw new Error(`${want} not in the zip`); -} -var portInUse = (port) => new Promise((ok) => { - const s = connect({ host: "127.0.0.1", port }, () => { - s.destroy(); - ok(true); - }); - s.on("error", () => ok(false)); -}); -function devBackendProxy(env = process.env) { - return Object.fromEntries(devBackends(env).map((b) => [b.route, { - target: b.remote || `http://127.0.0.1:${b.port}`, - changeOrigin: !!b.remote, - ws: true, - ...b.rewrite ? { rewrite: b.rewrite } : {} - }])); -} -var REG = globalThis.__dxDevBackends ||= { procs: /* @__PURE__ */ new Map(), exitHooked: false, stopping: false }; -var isAlive = (name) => { - const e = REG.procs.get(name); - return !!e && e.child.exitCode === null && e.child.signalCode === null; -}; -function stopAll() { - REG.stopping = true; - for (const { child } of REG.procs.values()) { - try { - child.kill("SIGTERM"); - } catch { - } - } -} -function start(root, b, logger, restarts = 0) { - const tag = `[${b.name}]`; - const child = spawn(resolve6(root, b.bin), b.args || [], { cwd: root, env: { ...process.env, ...b.env }, stdio: ["ignore", "pipe", "pipe"] }); - const entry = { child, restarts, logger }; - REG.procs.set(b.name, entry); - const logDir = resolve6(root, ".pb", "logs"); - mkdirSync2(logDir, { recursive: true }); - const sink = createWriteStream(resolve6(logDir, `${b.name}.log`), { flags: "a" }); - const LOUD = /\b(error|fail(ed|ure)?|panic|fatal|warn(ing)?|refused|denied|in use|cannot|unable|superuser|pbinstall?)\b/i; - const out = (d) => { - sink.write(d); - String(d).trim().split("\n").forEach((l) => l && LOUD.test(l) && entry.logger.info(`${tag} ${l}`)); - }; - child.stdout.on("data", out); - child.stderr.on("data", out); - child.on("exit", () => sink.end()); - child.on("exit", (code) => { - if (REG.procs.get(b.name) !== entry) return; - REG.procs.delete(b.name); - if (REG.stopping) return; - entry.logger.warn(`${tag} exited (${code})`); - if (entry.restarts < 5) { - setTimeout(() => { - if (REG.stopping || REG.procs.has(b.name)) return; - entry.logger.warn(`${tag} restarting (${entry.restarts + 1}/5)`); - start(root, b, entry.logger, entry.restarts + 1); - }, 2e3); - } - }); - logger.info(`${tag} started \u2192 ${b.route} on 127.0.0.1:${b.port} (full log: .pb/logs/${b.name}.log)`); -} -function backendDevPlugin(root, env = process.env) { - const backends = devBackends(env); - return { - name: "dx-backend-dev", - apply: "serve", - configureServer(server) { - for (const b of backends) { - server.middlewares.use(b.route, (req, res, next) => { - if (b.remote || isAlive(b.name)) return next(); - if (b.offline) return b.offline(res); - const status = { engine: "offline", expected: !!(b.expected && b.expected()), error: "The desktop engine is not running." }; - if ((req.url || "").startsWith("/events")) { - res.writeHead(200, { "Content-Type": "text/event-stream", "Cache-Control": "no-cache", Connection: "keep-alive" }); - res.write(`data: ${JSON.stringify({ type: "status", status })} - -`); - return; - } - if ((req.url || "").startsWith("/status")) { - res.writeHead(200, { "Content-Type": "application/json" }); - res.end(JSON.stringify(status)); - return; - } - res.writeHead(200, { "Content-Type": "application/json" }); - res.end("[]"); - }); - } - const logger = server.config.logger; - if (!REG.exitHooked) { - REG.exitHooked = true; - process.once("exit", stopAll); - process.once("SIGINT", () => { - stopAll(); - process.exit(0); - }); - process.once("SIGTERM", () => { - stopAll(); - process.exit(0); - }); - } - for (const b of backends) { - if (b.remote) { - logger.info(`[${b.name}] not started here: ${b.route} \u2192 ${b.remote}`); - continue; - } - if (isAlive(b.name)) { - REG.procs.get(b.name).logger = logger; - logger.info(`[${b.name}] still running \u2192 ${b.route} (kept across the dev-server restart)`); - continue; - } - if (b.disabled && b.disabled()) { - logger.info(`[backend] ${b.name} switched off here (${b.name === "rudder-engine" ? "RUDDER_ENGINE=0" : "disabled"})`); - continue; - } - const launch = async () => { - if (b.portEnv && await portInUse(b.port)) { - logger.warn(`[${b.name}] port ${b.port} is already in use (another \`bun run dev\`?): not started. Set ${b.portEnv} to use another port.`); - return; - } - if (!REG.stopping && !isAlive(b.name)) start(root, b, logger); - }; - if (!existsSync4(resolve6(root, b.bin))) { - if (b.fetch) { - REG.fetching ||= /* @__PURE__ */ new Set(); - if (REG.fetching.has(b.name)) continue; - REG.fetching.add(b.name); - b.fetch(root, logger).then((ok) => { - REG.fetching.delete(b.name); - if (ok) launch(); - }); - continue; - } - if (b.optional) logger.info(`[backend] ${b.optional}`); - else logger.warn(`[backend] ${b.name} not built \u2014 its plugin will show "engine offline". Build: ${b.build}`); - continue; - } - launch(); - } - } - }; -} - -// internal/dev-sync-plugin.mjs -import { execFile } from "node:child_process"; -var git = (root, args) => new Promise((ok) => execFile( - "git", - args, - { cwd: root, timeout: 6e4, env: { ...process.env, GIT_TERMINAL_PROMPT: "0" } }, - (e, out, err) => { - const all = String(err || e?.message || "").trim(); - ok({ ok: !e, out: String(out || "").trim(), err: all.split("\n")[0], errAll: all }); - } -)); -var AUTH_RE = /unauthori[sz]ed|authentication failed|could not read (username|password)|terminal prompts disabled|permission denied \(publickey|\b40[13]\b/i; -var shown = (remote) => remote.replace(/\/\/[^/@]*@/, "//"); -var label = (remote, branch) => remote.includes("/") ? `${branch} of ${shown(remote)}` : `${remote}/${branch}`; -var REG2 = globalThis.__dxDevSync ||= { timer: null, running: false, status: { enabled: false } }; -async function syncOnce(root, remote, branch, log = { info() { -}, warn() { -} }) { - if (REG2.running) return REG2.status; - REG2.running = true; - try { - const src2 = label(remote, branch); - const s = { enabled: true, remote: shown(remote), branch, checkedAt: (/* @__PURE__ */ new Date()).toISOString() }; - const f = await git(root, ["fetch", "-q", remote, branch]); - if (!f.ok) { - const auth = AUTH_RE.test(f.errAll); - const state = auth ? "needs-credentials" : "fetch-failed"; - const message = auth ? `dev-sync needs git credentials for ${shown(remote)} (a credential helper, or an SSH remote your key can read); git said: ${shown(f.err)}` : `could not fetch ${src2}: ${shown(f.err)}`; - if (REG2.status.state !== state || REG2.status.message !== message) log.warn(`[dev-sync] ${message}`); - return REG2.status = { ...s, state, message }; - } - const up = (await git(root, ["rev-parse", "FETCH_HEAD"])).out; - const head = (await git(root, ["rev-parse", "HEAD"])).out; - const cur = (await git(root, ["symbolic-ref", "-q", "--short", "HEAD"])).out; - const behind = Number((await git(root, ["rev-list", "--count", `HEAD..${up}`])).out || 0); - const ahead = Number((await git(root, ["rev-list", "--count", `${up}..HEAD`])).out || 0); - const dirty = (await git(root, ["status", "--porcelain", "--untracked-files=no"])).out !== ""; - Object.assign(s, { upstream: up, behind, ahead, dirty, currentBranch: cur || "(detached)" }); - if (behind === 0) return REG2.status = { ...s, state: "up-to-date", message: `up to date with ${src2}` }; - const why = cur !== branch ? `this checkout is on ${cur || "a detached HEAD"}, not ${branch}` : dirty ? "you have local edits" : ahead ? `you have ${ahead} local commit(s) that are not on ${src2}` : ""; - const m = why ? null : await git(root, ["merge", "--ff-only", "-q", up]); - if (why || !m.ok) { - const message = `${branch} is ${behind} commit(s) ahead; ${why || `fast-forward refused (${m.err})`}, so nothing was merged. Pull when ready.`; - if (REG2.status.upstream !== up || REG2.status.state !== "behind") log.warn(`[dev-sync] ${message}`); - return REG2.status = { ...s, state: "behind", message }; - } - log.info(`[dev-sync] fast-forwarded ${head.slice(0, 8)} \u2192 ${up.slice(0, 8)} (${behind} commit(s) from ${src2})`); - return REG2.status = { ...s, behind: 0, state: "up-to-date", lastMerge: s.checkedAt, message: `fast-forwarded ${behind} commit(s) from ${src2}` }; - } finally { - REG2.running = false; - } -} -function devSyncPlugin(root, env = process.env) { - const remote = env.DX_SYNC_REMOTE || ""; - const branch = env.DX_SYNC_BRANCH || "development"; - const every = Math.max(2, Number(env.DX_SYNC_INTERVAL) || 5); - return { - name: "dx-dev-sync", - apply: "serve", - configureServer(server) { - server.middlewares.use("/api/dev-sync", (req, res) => { - res.writeHead(200, { "Content-Type": "application/json", "Cache-Control": "no-store" }); - res.end(JSON.stringify(REG2.status)); - }); - clearInterval(REG2.timer); - if (!remote) { - REG2.status = { enabled: false, message: "off: set DX_SYNC_REMOTE (a remote name like origin, or a repo URL) in .env.local to follow a shared branch" }; - return; - } - const log = server.config.logger; - log.info(`[dev-sync] following ${label(remote, branch)} every ${every}s (fast-forward only, never over local edits)`); - const tick = () => syncOnce(root, remote, branch, log); - REG2.timer = setInterval(tick, every * 1e3); - REG2.timer.unref?.(); - tick(); - } - }; -} - -// vite.config.js -import { fileURLToPath as fileURLToPath5 } from "node:url"; -import { resolve as resolve8, dirname as dirname6, join as join4 } from "node:path"; -import { readdirSync as readdirSync5, statSync as statSync4, existsSync as existsSync6, readFileSync as readFileSync7 } from "node:fs"; -import { createHash as createHash2 } from "node:crypto"; -var __vite_injected_original_import_meta_url5 = "file:///home/lk10x/Workspace/eco-dx/root-doxa-maps/wt-hf/vite.config.js"; -var __dirname = dirname6(fileURLToPath5(__vite_injected_original_import_meta_url5)); -var SHIP_ONE_LOCALE = process.env.DX_SHIP_ONE_LOCALE === "1"; -var profilesDir = resolve8(__dirname, "plugin-designs"); -var MFE_NAME = TREE_NAME; -var OUT_ROOT = process.env.DOXA_OUT_ROOT ? resolve8(process.env.DOXA_OUT_ROOT) : GATED_PROFILES.includes(process.env.BUNDLE) ? resolve8(__dirname, ".gated", MFE_NAME) : resolve8(__dirname, "public", MFE_NAME); -function discoverBundles() { - if (!existsSync6(profilesDir)) return {}; - const entries = {}; - for (const name of readdirSync5(profilesDir)) { - if (name.startsWith("_") || name.startsWith(".")) continue; - const dir = resolve8(profilesDir, name); - if (!statSync4(dir).isDirectory()) continue; - const html = resolve8(dir, "index.html"); - const js = resolve8(dir, "index.js"); - if (!existsSync6(html) || !existsSync6(js)) continue; - entries[name] = { html, js }; - } - return entries; -} -var localeDirs = () => readdirSync5(join4(__dirname, "library/i18n/locales"), { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name).sort(); -function localeTree(areas, lc) { - const root = join4(__dirname, "library/i18n/locales"); - const tree = {}; - for (const a of areas) { - const p = join4(root, lc, `${a}.json`); - if (!existsSync6(p)) continue; - try { - tree[a] = JSON.parse(readFileSync7(p, "utf8")); - } catch { - } - } - return Object.keys(tree).length ? JSON.stringify(tree) : null; -} -function i18nAreasPlugin(bundle) { - let areas = null; - return { - name: "doxa-i18n-areas", - enforce: "pre", - async buildStart() { - const { areasForBundle: areasForBundle2 } = await Promise.resolve().then(() => (init_i18n_areas(), i18n_areas_exports)); - areas = areasForBundle2(__dirname, bundle).areas; - this.info?.(`i18n areas for ${bundle}: ${areas.join(", ") || "(none)"}`); - }, - transform(code, id) { - if (!areas || !id.replace(/\\/g, "/").endsWith("/library/i18n/index.js")) return null; - const GLOB = "import.meta.glob('./locales/*/*.json'"; - if (!code.includes(GLOB)) this.error("i18n catalogue glob not found in library/i18n/index.js; cannot narrow it"); - const loc = SHIP_ONE_LOCALE ? "en" : "*"; - const list = areas.length ? "[" + areas.map((a) => `'./locales/${loc}/${a}.json'`).join(", ") + "]" : "['./locales/*/__none__.json']"; - let out = code.replace(GLOB, "import.meta.glob(" + list); - if (SHIP_ONE_LOCALE) { - const sri = {}; - for (const lc of localeDirs()) { - if (lc === "en") continue; - const json = localeTree(areas, lc); - if (json) sri[lc] = "sha384-" + createHash2("sha384").update(json).digest("base64"); - } - out = `const __DX_LOCALE_SRI__ = ${JSON.stringify(sri)}; -` + out; - } - return { code: out, map: null }; - }, - // Emit /locales/.json for every locale the IIFE no longer carries, holding exactly - // the areas this bundle uses — the same areasForBundle() answer the glob above was narrowed - // with, so the two dimensions cannot drift apart. Emitted from THIS plugin on purpose: the - // composite-freshness, landing-consumers, fix:sri and .gated/ steps already walk the build's - // own output, so assets that appear here are covered with no new pipeline step. - generateBundle() { - if (!SHIP_ONE_LOCALE || !areas?.length) return; - for (const lc of localeDirs()) { - if (lc === "en") continue; - const json = localeTree(areas, lc); - if (json) this.emitFile({ type: "asset", fileName: `locales/${lc}.json`, source: json }); - } - } - }; -} -var vite_config_default = defineConfig(({ command, mode }) => { - const env = loadEnv(mode, __dirname, ""); - const all = discoverBundles(); - const allNames = Object.keys(all); - const target = process.env.BUNDLE; - const isBuild = command === "build"; - if (isBuild && !target) { - throw new Error( - `[${MFE_NAME}] Set BUNDLE= for build, or use \`npm run build\` (loops all). -Available: ${allNames.join(", ") || "(none)"}` - ); - } - if (isBuild && !all[target]) { - throw new Error( - `[${MFE_NAME}] Bundle "${target}" is not buildable: plugin-designs/${target}/ is missing index.html and/or index.js (the custom-element entry pair every bundle needs). -Buildable bundles: ${allNames.join(", ") || "(none)"}` - ); - } - const buildEntries = isBuild ? { [target]: all[target].js } : Object.fromEntries(allNames.map((n) => [n, all[n].html])); - return { - // COMPILE-TIME QA GATE (release governance): the QA corner widget references - // the maintainer's internal platform and must NOT exist in public - // artifacts. Default FALSE strips it from every bundle (dead-branch + - // treeshake); an explicitly-flagged platform build sets DOXA_QA=1. - // The dev server listens on THIS MACHINE ONLY. A reachable one serves the source tree, node_modules - // and the token baked into /; a remote viewer reaches it through an ssh tunnel (ssh -L), never a public - // port. `--host` on the command line still overrides this for a deliberate LAN session. - // Local plugin backends (backend/, e.g. rudder-engine) are started by backendDevPlugin and reached - // through these same-origin routes, so a plugin calls /api//… in dev exactly as on desktop. - // NOT WATCHING public/ (the first laptop run with DX_SYNC_REMOTE, 2026-09-28): a landing rewrites - // dozens of built leaf index.html files there, and Vite answers each changed .html with a FULL page reload - // (measured: one edited leaf page -> one full-reload broadcast), so a pull fired a burst of reloads while - // git was still writing and the page went white. The dev server never serves public/ through its module - // graph (the script-embed route reads it on request), so nothing there needs watching. - server: { host: "127.0.0.1", proxy: devBackendProxy(env), watch: { ignored: ["**/public/**"] } }, - define: { - // ★ NO 'unsafe-eval' (CRITIC-4, 2026-09-26). vue-i18n's default message compiler builds each message - // with `new Function`, so a partner with a strict Content-Security-Policy saw every translated string - // fail. The JIT compiler (vue-i18n >= 9.3) compiles messages to an AST and evaluates it without eval; - // defining the flag lets the bundler drop the eval path entirely (check:no-eval asserts it). - __INTLIFY_JIT_COMPILATION__: "true", - __INTLIFY_DROP_MESSAGE_COMPILER__: "false", - __DOXA_QA__: JSON.stringify(process.env.DOXA_QA === "1"), - // The output tree's name (vite.doxa-maps-build-config.json `name`, the same value OUT_ROOT uses), - // for the few runtime paths that must name the tree — so renaming the tree needs no source edit. - __DOXA_TREE_NAME__: JSON.stringify(MFE_NAME) - }, - // Build only: shadow-DOM CSS is injected from useShadowStyles(`…`) literals that - // ship verbatim, so their block comments are stripped before the Vue compiler - // runs (enforce: 'pre'). Dev keeps the comments. - plugins: [ - // ★ ONLY THE TRANSLATION AREAS THIS BUNDLE USES (CRITIC-10). library/i18n/index.js globs every - // locales/*/*.json; in a BUNDLE build the glob is narrowed to the areas internal/i18n-areas.mjs finds - // in this bundle's source graph. Dev (no BUNDLE) keeps every area. check:i18n-treeshake verifies it. - isBuild && target ? i18nAreasPlugin(target) : null, - stripShadowCssCommentsPlugin(), - // ─── STRUCTURAL shadow-DOM CSS fix (customElement mode) ────────────────── - // WHY: every bundle here is a `defineCustomElement(...)` that renders inside a - // SHADOW ROOT. In @vitejs/plugin-vue's DEFAULT mode, each SFC