doxa-maps/internal/check-no-cross-plugin-imports.mjs
Doxa Maps de5a3fdf80 check:no-cross-plugin-imports: a plugin may not reach into another plugin's private folder
research-dashboard and dashboard-plus-grid imported '../doxa-research-map/src/colors/…' — another plugin's
private code — which is how two copies of the same palette drifted (C1a moves them to library/colors).
The gate refuses any relative import that resolves into plugin-designs/<other>/ below that bundle's root;
a composite mounting another bundle's PUBLIC profile .vue (the capability-4 pattern) stays allowed. It
FAILs on main (the 4 palette imports C1a removes) and PASSes with C1a; --self-test in gates:post.
2026-09-26 08:39:53 +00:00

59 lines
3.9 KiB
JavaScript

#!/usr/bin/env node
/**
* check-no-cross-plugin-imports — a plugin imports another plugin only through library/, never by reaching
* into its folder.
*
* WHY (2026-09-26, regions' C1a): research-dashboard and dashboard-plus-grid imported
* '../doxa-research-map/src/…' — another plugin's PRIVATE folder. That couples two owners' code invisibly
* (a refactor inside doxa-research-map/src breaks a bundle its owner never builds), and it is how two
* private copies of the same palette drifted apart. Shared code belongs in library/ (@map/…).
*
* RULE: in plugin-designs/<bundle>/**, a relative import (static, dynamic, export-from, import.meta.glob)
* must not resolve into plugin-designs/<other>/ — except another bundle's PUBLIC profile component (a .vue at
* that bundle's root), which is how a capability-4 composite mounts it. Anything deeper (src/, utils) is refused.
* --self-test plants a crossing import and requires a FAIL.
* EXIT 0 pass · 1 fail
*/
import { readFileSync, readdirSync, statSync } from 'node:fs'
import { join, resolve, dirname, relative, sep } from 'node:path'
import { fileURLToPath } from 'node:url'
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..')
const PD = join(ROOT, 'plugin-designs')
const SPEC = /(?:import|export)\s[^'"`;]*?from\s*['"]([^'"]+)['"]|import\s*['"]([^'"]+)['"]|import\s*\(\s*['"]([^'"]+)['"]\s*\)|import\.meta\.glob\s*\(\s*['"]([^'"]+)['"]/g
/** [{file, spec, target}] for every relative import in `src` (at `file`) that lands in another bundle. */
export function crossings(file, src) {
const own = relative(PD, file).split(sep)[0]
const out = []
for (const m of src.matchAll(SPEC)) {
const spec = m[1] || m[2] || m[3] || m[4]
if (!spec || !spec.startsWith('.')) continue
const abs = resolve(dirname(file), spec)
const rel = relative(PD, abs)
if (rel.startsWith('..')) continue // left plugin-designs/ (library/, data/): fine
const parts = rel.split(sep)
const target = parts[0]
if (!target || target === own) continue
// A composite mounting another bundle's PUBLIC profile (a .vue at that bundle's root) is composition,
// the documented capability-4 pattern. Reaching any deeper (src/, utils, data) is the private coupling.
if (parts.length === 2 && /\.vue$/.test(parts[1])) continue
out.push({ file: relative(ROOT, file), spec, target })
}
return out
}
if (process.argv.includes('--self-test')) {
const f = join(PD, 'research-dashboard', 'x.vue')
const hits = crossings(f, "import A from '../doxa-research-map/src/a.js'\nimport B from './src/b.js'\nimport C from '../../library/c.js'\nimport D from '../people-groups-grid/people-groups-grid.vue'")
if (hits.length === 1 && hits[0].target === 'doxa-research-map') { console.log('NO-CROSS-PLUGIN-IMPORTS: SELF-TEST PASS — a reach into another plugin\'s src/ is caught; its own src/, library/ and another bundle\'s public profile .vue are not.'); process.exit(0) }
console.error('NO-CROSS-PLUGIN-IMPORTS: SELF-TEST FAIL — ' + JSON.stringify(hits)); process.exit(1)
}
const files = []
const walk = (d) => { for (const e of readdirSync(d)) { if (e === 'node_modules' || e.startsWith('.')) continue; const p = join(d, e); const st = statSync(p); if (st.isDirectory()) walk(p); else if (/\.(vue|m?js)$/.test(e) && !/\/(scripts|data)\//.test(p.replace(/\\/g, '/'))) files.push(p) } }
walk(PD)
const bad = files.flatMap((f) => crossings(f, readFileSync(f, 'utf8')))
for (const b of bad) console.log(` FAIL ${b.file}: imports '${b.spec}' from another plugin (${b.target}); move the shared code to library/ and import it as @map/…`)
if (bad.length) { console.log(`NO-CROSS-PLUGIN-IMPORTS: FAIL — ${bad.length} import(s) reach into another plugin's folder.`); process.exit(1) }
console.log(`NO-CROSS-PLUGIN-IMPORTS: PASS — ${files.length} plugin source file(s); none imports from another plugin's folder.`)