doxa-maps/internal/check-targets.mjs
Doxa Maps 6451a2b158 bun run desktop:build <plugin> packages a desktop plugin as an app; check:targets keeps non-web plugins out of the web build
A plugin's <profile>.meta.json targets (web, mobile, desktop, widget; absent = web)
now decide where it is built. internal/plugin-targets.mjs is the one reading.
desktop:build builds a desktop plugin into .desktop/<plugin>/ with a page that
names its engine, builds its backend as a Tauri sidecar for this machine, and runs
tauri build with a --config override; tauri.conf.json and public/ are untouched.
The shell starts the bundled rudder-engine (RUDDER_AUDIO=browser, RUDDER_HOME in the
app data folder) and stops it on exit. A plugin whose targets leave out web
(Rudder: desktop + widget) is no longer built for the web tree: run-parallel,
generate-tree and check-composite-freshness skip it, and its public/ folder is
removed; the dev server still gives the Studio its corner. check:targets
(gates:pre, with a self-test of five planted violations) validates the values,
widget corners and backend folders and fails if a non-web plugin reaches the built
tree. Measured: desktop:build rudder-voice on Linux produced Rudder_0.1.0_amd64.deb
(8.8 MB) with the app and the engine side by side; the app has not been launched.
2026-09-28 06:44:14 +00:00

75 lines
4.3 KiB
JavaScript

#!/usr/bin/env node
/**
* check-targets — where each plugin runs, as its <profile>.meta.json "targets" says. Headless.
*
* Rules (plugin-targets.mjs holds the shared reading):
* - targets, when declared, is a non-empty list of known values: web, mobile, desktop, widget.
* Absent = web: a meta never has to declare it.
* - "widget" in targets and a meta "widget": { "corner" } go together (the Studio mounts a widget by its corner).
* - "backend": "backend/<name>" names an existing folder (desktop:build packages it as the app's sidecar).
* - A plugin that is not built for the web (targets without "web") is NOT in the built web tree: no
* public/<tree>/<bundle>/ folder and no manifest entry, so neither the /cdn export nor the Research
* Gallery can carry it. desktop:build packages it instead.
* --self-test plants each violation and confirms it is caught.
* EXIT 0 pass · 1 fail
*/
import { readFileSync, readdirSync, existsSync, statSync } from 'node:fs'
import { join, resolve, dirname } from 'node:path'
import { fileURLToPath } from 'node:url'
import { KNOWN_TARGETS, profileTargets, isWebBuilt } from './plugin-targets.mjs'
import { TREE_NAME } from './_tree-name.mjs'
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..')
// One profile's meta against the rules; returns the problems.
export function metaProblems(where, meta, backendExists = (p) => existsSync(join(ROOT, p))) {
const out = []
const t = meta.targets
if (t !== undefined) {
if (!Array.isArray(t) || !t.length) out.push(`${where}: "targets" must be a non-empty list`)
else for (const v of t) if (!KNOWN_TARGETS.includes(v)) out.push(`${where}: unknown target "${v}" (known: ${KNOWN_TARGETS.join(', ')})`)
if (Array.isArray(t) && t.includes('widget') && !meta.widget?.corner) out.push(`${where}: targets include "widget" but there is no "widget": { "corner" }`)
if (Array.isArray(t) && meta.widget && !t.includes('widget')) out.push(`${where}: has a "widget" corner but its targets leave out "widget"`)
}
if (meta.backend !== undefined && !(typeof meta.backend === 'string' && backendExists(meta.backend))) out.push(`${where}: "backend" names no folder ${meta.backend}`)
return out
}
if (process.argv.includes('--self-test')) {
const planted = [
['unknown value', { targets: ['tv'] }],
['empty list', { targets: [] }],
['widget without corner', { targets: ['widget'] }],
['corner without widget target', { targets: ['desktop'], widget: { corner: 'bottom-right' } }],
['missing backend', { targets: ['desktop'], backend: 'backend/no-such-engine' }],
]
let missed = 0
for (const [name, meta] of planted) if (!metaProblems('planted', meta, () => false).length) { missed++; console.log(` MISSED ${name}`) }
if (metaProblems('ok', { targets: ['desktop', 'widget'], widget: { corner: 'bottom-right' }, backend: 'backend/x' }, () => true).length) { missed++; console.log(' FALSE ALARM on a valid meta') }
if (missed) { console.log(`TARGETS self-test: FAIL — ${missed}`); process.exit(1) }
console.log(`TARGETS self-test: PASS — ${planted.length} planted violations caught, a valid meta passes.`)
process.exit(0)
}
const problems = []
const DESIGNS = join(ROOT, 'plugin-designs')
const TREE = join(ROOT, 'public', TREE_NAME)
let manifest = null
try { manifest = JSON.parse(readFileSync(join(TREE, 'manifest.json'), 'utf8')) } catch { /* no built tree: tree rules skipped */ }
let declared = 0, notWeb = 0
for (const b of readdirSync(DESIGNS)) {
const dir = join(DESIGNS, b)
if (!statSync(dir).isDirectory()) continue
for (const p of profileTargets(dir)) {
if (p.targets) declared++
problems.push(...metaProblems(`${b}/${p.profile}.meta.json`, p.meta))
}
if (!isWebBuilt(dir)) {
notWeb++
if (existsSync(join(TREE, b))) problems.push(`${b} is not a web plugin but public/${TREE_NAME}/${b}/ exists (it would ship in the /cdn export)`)
if (manifest && (manifest.bundles || []).some((x) => x.bundle === b)) problems.push(`${b} is not a web plugin but the built manifest lists it (the gallery would offer it)`)
}
}
for (const p of problems) console.log(' FAIL ' + p)
if (problems.length) { console.log(`TARGETS: FAIL — ${problems.length} problem(s).`); process.exit(1) }
console.log(`TARGETS: PASS — ${declared} profile(s) declare targets (absent = web); ${notWeb} non-web plugin(s), none in the built web tree.`)