doxa-maps/internal/check-composite-srcs.mjs
doxa-maps b57aef457c
Some checks are pending
build-gate / build (push) Waiting to run
Add the mobile capability-4 composite: research map above the people-groups grid
The desktop composite pairs the research dashboard with the grid. This is its
mobile sibling: the monolithic 7-tab research map on top, the same grid beneath,
stacked to scroll. Which one a shell shows is an aspect-ratio choice made at the
SPA level, so neither plugin knows about the other and both stay embeddable.

The grid's own search and filters are ON here. The desktop composite turns them
off because the dashboard header owns those controls and drives the grid through
props; the research map has no such header, so switching them off would leave the
grid with no way to be searched at all.

Composes its children by tag and bundle URL rather than importing their .vue
files. Importing the research map directly would bypass its bundle entry, which
is where its three bundle-private colour strategies, its profile registry, its
Pinia and i18n pair and the RTL text plugin are installed — and losing those is a
runtime defect in a green build, because a thrown setup() removes the subtree
with an empty error.

Add check:composite-srcs, which resolves every nested src against the built tree
and asserts every nested tag is registered. A wrong relative depth is a 404, and
the stack layout swallows a failed section on purpose so one broken child cannot
take the whole stack down — which means the cost of that resilience is a silently
empty band. Negative-tested both ways: breaking the depth and renaming a tag each
fail with the file and path named; restoring returns to pass.
2026-09-22 07:34:02 +00:00

156 lines
7.2 KiB
JavaScript

#!/usr/bin/env node
/**
* check-composite-srcs.mjs — every composite's nested `src:` must resolve to a real built bundle.
*
* WHY THIS EXISTS. A capability-4/5 composite nests its children by TAG + BUNDLE URL through
* StackLayout / VerticalTabsLayout:
*
* { key:'map', element:'doxa-research-map', src:'../../doxa-research-map/doxa-research-map.js' }
*
* That `src` is resolved BY THE BROWSER, relative to the composite's generated leaf page, which is
* served two levels deep (doxa-maps/<bundle>/<profile>/index.html). So a sibling bundle is reached
* with `../../<bundle>/<bundle>.js`. Get the depth wrong by one and you get a 404.
*
* ★ THE FAILURE IS SILENT, WHICH IS WHY IT NEEDS A GATE. StackLayout mounts each section inside a
* promise chain that ends in `.catch(() => {})` — a deliberate choice, so one broken child cannot
* take down the whole stack. The cost of that resilience is that a wrong path produces NO error,
* NO console warning and NO build failure: just a section that is silently EMPTY. The build is
* green, the element mounts, and a band of the product is missing. Exactly the shape that survives
* every check an author runs and is found only by a human looking at the page.
*
* WHAT IT CHECKS. For every `src:` string in plugin-designs/<bundle>/*.vue, resolve it the way the
* browser will — against `public/<treeName>/<bundle>/<profile>/` — and assert the file exists in
* the built tree. It also flags an `element:` whose tag no bundle registers, which is the other
* half of the same defect (a correct path to a bundle that defines a different tag).
*
* NOT A SUBSTITUTE FOR A BROWSER. This proves the URL resolves to a file on disk. It cannot prove
* the element renders. Pair it with the Playwright smoke.
*
* NEGATIVE-TESTED: changing a src's depth (../../ -> ../) makes this FAIL naming the bundle, the
* section and both the written and resolved paths; restoring it returns PASS. A tree with no
* composites at all FAILS (vacuous guard) rather than passing on an empty set.
*/
import { readFileSync, existsSync, readdirSync } from 'node:fs'
import { resolve, dirname, join } from 'node:path'
import { fileURLToPath } from 'node:url'
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..')
// The output tree name comes from the build config, the same source generate-tree uses. Defaulting
// silently would make this gate check a directory the build never writes.
let treeName = 'doxa-maps'
try {
const cfg = JSON.parse(readFileSync(join(ROOT, 'vite.1040-maps-build-config.json'), 'utf8'))
if (cfg?.name) treeName = cfg.name
} catch { /* fall through to the default */ }
const TREE = join(ROOT, 'public', treeName)
if (!existsSync(TREE)) {
console.log(`COMPOSITE-SRCS: SKIP — no built tree at public/${treeName}/. Run \`bun run build\` first.`)
console.log(' An absent tree is not evidence either way.')
process.exit(0)
}
const DESIGNS = join(ROOT, 'plugin-designs')
const bundles = readdirSync(DESIGNS, { withFileTypes: true })
.filter(d => d.isDirectory()).map(d => d.name)
// Collect every tag any bundle registers, so an `element:` pointing at a tag nobody defines is
// caught too. Read from the SOURCE entries (index.js), which is where customElements.define lives.
const definedTags = new Map() // tag -> bundle that defines it
for (const b of bundles) {
const entry = join(DESIGNS, b, 'index.js')
if (!existsSync(entry)) continue
const txt = readFileSync(entry, 'utf8')
for (const m of txt.matchAll(/customElements\.define\(\s*['"]([^'"]+)['"]/g)) {
if (!definedTags.has(m[1])) definedTags.set(m[1], b)
}
}
// A section literal is `{ ... element: 'tag' ... src: '...' ... }`. Rather than parse JS, scan each
// .vue for src/element string pairs within a small window — composites declare them adjacently.
const SRC_RE = /src:\s*['"]([^'"]+\.js)['"]/g
const EL_RE = /element:\s*['"]([^'"]+)['"]/g
const problems = []
let composites = 0
let checked = 0
for (const bundle of bundles) {
const dir = join(DESIGNS, bundle)
const vues = readdirSync(dir).filter(f => f.endsWith('.vue'))
for (const vue of vues) {
const text = readFileSync(join(dir, vue), 'utf8')
const srcs = [...text.matchAll(SRC_RE)].map(m => m[1])
const els = [...text.matchAll(EL_RE)].map(m => m[1])
if (!srcs.length && !els.length) continue
composites++
// The composite's generated leaf lives at public/<tree>/<bundle>/<profile>/index.html. Profiles
// are the bundle's own .vue basenames; use the real built dirs so this matches what shipped.
const bundleOut = join(TREE, bundle)
if (!existsSync(bundleOut)) {
problems.push({ bundle, vue, why: `no built output at public/${treeName}/${bundle}/ — did the build emit this bundle?` })
continue
}
const profileDirs = readdirSync(bundleOut, { withFileTypes: true })
.filter(d => d.isDirectory()).map(d => d.name)
if (!profileDirs.length) {
problems.push({ bundle, vue, why: `built bundle has no profile directory to resolve relative srcs from` })
continue
}
for (const src of srcs) {
checked++
// Resolve exactly as the browser would: against the leaf page's directory.
const leafDir = join(bundleOut, profileDirs[0])
const resolved = resolve(leafDir, src)
if (!existsSync(resolved)) {
problems.push({
bundle, vue, src,
why: `resolves to ${resolved.replace(ROOT + '/', '')} — WHICH DOES NOT EXIST`,
hint: `a sibling bundle is '../../<bundle>/<bundle>.js' from a leaf at <tree>/<bundle>/<profile>/`
})
}
}
for (const el of els) {
checked++
if (!definedTags.has(el)) {
problems.push({
bundle, vue, src: el,
why: `element tag '${el}' is not registered by ANY bundle's index.js`,
hint: `StackLayout waits for customElements.get('${el}') and gives up silently if it never appears`
})
}
}
}
}
console.log(`COMPOSITE-SRCS: ${composites} composite file(s), ${checked} nested reference(s) checked ` +
`against public/${treeName}/.`)
// Vacuous guard: "0 composites" must never read as "0 problems" — this repo ships several.
const MIN_COMPOSITES = 2
if (composites < MIN_COMPOSITES) {
console.error(`\nCOMPOSITE-SRCS: FAIL — only ${composites} composite(s) found (expected >= ${MIN_COMPOSITES}).`)
console.error(' The scan found almost nothing, which means the pattern changed or the scan broke.')
console.error(' Refusing to pass vacuously.')
process.exit(1)
}
if (problems.length) {
console.error(`\nCOMPOSITE-SRCS: FAIL — ${problems.length} nested reference(s) do not resolve:`)
for (const p of problems) {
console.error(` ✗ ${p.bundle}/${p.vue}${p.src ? ` ${p.src}` : ''}`)
console.error(` ${p.why}`)
if (p.hint) console.error(` ${p.hint}`)
}
console.error(
'\n A composite nests its children by URL, and StackLayout swallows a failed section on purpose\n' +
' (one broken child must not take down the stack). So a wrong path is NOT an error at runtime —\n' +
' it is a silently EMPTY band in a green build. Fix the path, or the product ships missing a piece.'
)
process.exit(1)
}
console.log('COMPOSITE-SRCS: PASS — every nested src resolves and every nested tag is registered.')