doxa-maps/internal/check-preset-config-merge.mjs
Doxa Maps 2e92a05b30 A preset keeps its profile's defaults: instance props merge over the meta config instead of replacing it
A preset page baked only the instance's props, so every default in the profile's
.meta.json config that the instance did not repeat was lost (go-int: upg-neighbor's
five presets dropped pinHint:false). instanceHtml now takes the meta config first
and the props over it. check:preset-config-merge (gates:post) reads every built
preset page and fails on a lost default; the rule is written into
PRESET-PLUGIN-STANDARD.md. Affected before the fix: upg-neighbor only; no
production preset has profile defaults to lose.
2026-09-28 03:12:45 +00:00

50 lines
2.7 KiB
JavaScript

#!/usr/bin/env node
/**
* check-preset-config-merge — a preset page keeps its profile's defaults. Headless.
*
* A preset (<bundle>/<profile>/<instance>/index.html) bakes a config made of the profile's
* <profile>.meta.json `config` with the instance's `props` MERGED over it. It used to REPLACE it: go-int's
* upg-neighbor presets lost the profile's pinHint:false. For every built preset page this asserts each
* meta `config` key is present, with the instance's own value where the instance sets one.
* Rule stated in documentation/PRESET-PLUGIN-STANDARD.md.
* EXIT 0 pass · 1 fail · 0 with SKIP when there is no built tree
*/
import { readFileSync, existsSync, readdirSync, statSync } from 'node:fs'
import { join, resolve, dirname } from 'node:path'
import { fileURLToPath } from 'node:url'
import { TREE_NAME } from './_tree-name.mjs'
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..')
const TREE = join(ROOT, 'public', TREE_NAME)
if (!existsSync(TREE)) { console.log('PRESET-CONFIG-MERGE: SKIP — no built tree'); process.exit(0) }
const json = (f) => { try { return JSON.parse(readFileSync(f, 'utf8')) } catch { return null } }
let pages = 0, bad = 0
for (const bundle of readdirSync(join(ROOT, 'plugin-designs'))) {
const dir = join(ROOT, 'plugin-designs', bundle)
if (!statSync(dir).isDirectory()) continue
for (const f of readdirSync(dir).filter((n) => n.endsWith('.instances.json'))) {
const raw = json(join(dir, f))
const list = Array.isArray(raw) ? raw : (raw && raw.instances) || []
for (const inst of list) {
const profile = inst.profile || f.replace(/\.instances\.json$/, '')
const cfg = (json(join(dir, `${profile}.meta.json`)) || {}).config || {}
const id = inst.id || inst.key || inst.name
const page = join(TREE, bundle, profile, String(id), 'index.html')
if (!id || !existsSync(page) || !Object.keys(cfg).length) continue
pages++
const m = readFileSync(page, 'utf8').match(/var BASE_CONFIG = (\{.*?\});?\n/)
const baked = m ? JSON.parse(m[1]) : {}
const props = inst.props || {}
for (const [k, v] of Object.entries(cfg)) {
const want = k in props ? props[k] : v
if (JSON.stringify(baked[k]) !== JSON.stringify(want)) {
bad++
console.log(` FAIL ${bundle}/${profile}/${id}: "${k}" is ${JSON.stringify(baked[k])}, want ${JSON.stringify(want)} (${k in props ? 'the instance' : 'the profile meta'})`)
}
}
}
}
}
if (bad) { console.log(`PRESET-CONFIG-MERGE: FAIL — ${bad} key(s) lost on ${pages} preset page(s) with profile defaults. Rebuild the tree (bun run tree).`); process.exit(1) }
console.log(`PRESET-CONFIG-MERGE: PASS — ${pages} preset page(s) with profile defaults keep every one, instance values winning.`)