LOC-ENUS flagged any en-US format literal. Two of regions-dashboard's three
warns were the 'en-US' inside a CATCH, whose try formats with
`${locale}-u-nu-latn` -- a verbatim clone of DX-life's RegionDetail.vue:16,
Latin digits with the reader's grouping. The fallback exists because a bad
locale tag throws RangeError in toLocaleString and blanking the page is
worse than one wrong separator.
So the matrix's locale column said "ignores the locale" about code that
honours it, which is the kind of false warn that teaches people to stop
reading the column.
Measured before changing the rule: of the NINE LOC-ENUS sites in the tree,
exactly TWO are such fallbacks. The other seven are genuinely operative and
still flagged, so the tree goes 10 warns -> 8 and only those two move.
The exemption needs BOTH halves -- a catch AND a locale-derived primary --
because a lone catch must not excuse a format that never consulted the
locale. That is the one rule here that can widen by accident, so four
adversarial shapes are pinned as a self-test: the real pattern is skipped,
while a catch around a hardcoded primary, a catch whose primary never
consulted a locale, and a plain operative call are all still caught.
Loosening the exemption to a bare catch makes the self-test exit 3.
regions-dashboard keeps ONE warn: RegionCountryMap's popup, fixed en-US on
purpose, carded and evidenced by DX-life's own convention. I did not add an
ALLOW for it -- ALLOW is keyed per BUNDLE, so it would also blind regions to
a future operative en-US, and a visible warn on a decision already taken is
cheaper than a blind spot.
214 lines
14 KiB
JavaScript
214 lines
14 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* check-universal-props.mjs — every plugin honours the props the Plugin Studio header sets for all
|
|
* of them (documentation/reference-universal-props.md): map ENGINE, LOCALE (+dir), Mapbox TOKEN and
|
|
* token-service URL, and the people-group LIST (library/lists/). HEADLESS: no browser, no server. It reads what each bundle's source and SHIPPED
|
|
* bundle consume, and fails on a bypass: a literal or a private global where the library resolver
|
|
* belongs.
|
|
*
|
|
* WHY STATIC. A bypass is a line of code, and it is the same line whether or not anyone is looking
|
|
* at the map: a `setStyle('mapbox://…')` breaks the MapLibre engine switch, a `window.DOXA_TOKEN`
|
|
* read ignores the page's token, a `toLocaleString('en-US')` ignores the locale. Each was found in
|
|
* this tree by reading the source (the census behind the contract page). Rendering proves the
|
|
* happy path; this proves nobody went around it.
|
|
*
|
|
* RULES (id · prop · what fails):
|
|
* ENG-STYLE engine a `mapbox://` style literal in map.setStyle() or a raw `new gl.Map({style})`
|
|
* outside a resolveNamedStyle/registry fallback: MapLibre cannot load it
|
|
* (a style handed to useMapInstance is translated there, so it passes)
|
|
* ENG-GLOBAL engine `new mapboxgl.X` / `new window.mapboxgl.X` (build it with engineFor(map))
|
|
* ENG-PIN engine a literal provider:'mapbox'|'maplibre' handed to useMapInstance (ALLOW-listed
|
|
* where it is the plugin's design)
|
|
* TOK-GLOBAL token reading window.DOXA_TOKEN (nothing sets it on a built page; use the injected
|
|
* mapboxToken, fed by profile-config.tk and window.MAP_TOKEN)
|
|
* TOK-LITERAL token a `pk.ey…` token literal in code (comments excluded)
|
|
* LOC-ENUS locale toLocaleString('en-US') / Intl.NumberFormat('en-US') (use the active locale).
|
|
* WARN only: whether numerals follow the locale is a Driver decision
|
|
* LIST-FETCH list a file that fetches the people-group list (/api/people-groups/list, a
|
|
* source's endpoints.bulk, or the doxa-people-groups.json roster) without
|
|
* applyPeopleGroupList: that map would show DOXA's list whatever list was asked
|
|
* LIST-DSM list `new DataSourceManager(...)` without `profileConfig`: the manager applies the
|
|
* list itself, but without the embed's config only ?list= / window.MAP_LIST reach it
|
|
* LIST-KEYS list a composite's INHERITED_PAGE_KEYS without 'list': its panes never hear it
|
|
* Source = plugin-designs/<bundle>/**.{vue,js} minus index.html dev pages and scripts/. Shipped =
|
|
* public/<tree>/<bundle>/<bundle>.js, checked for the TOK-GLOBAL and TOK-LITERAL strings the
|
|
* minifier cannot rename.
|
|
*
|
|
* ALLOW: a documented exception per bundle+rule, with its reason. Nothing else passes.
|
|
* Self-test: every rule must fire on a synthetic violation, or the gate exits 3.
|
|
* Usage: node internal/check-universal-props.mjs [--only a,b] [--report]
|
|
* Exit: 0 conforms · 1 a bypass · 3 self-test could not see a planted bypass.
|
|
*/
|
|
import { readFileSync, readdirSync, statSync, existsSync } from 'node:fs'
|
|
import { resolve, dirname, join, relative } from 'node:path'
|
|
import { fileURLToPath } from 'node:url'
|
|
import { TREE_NAME } from './_tree-name.mjs'
|
|
|
|
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..')
|
|
const argv = process.argv.slice(2)
|
|
const ONLY = argv.includes('--only') ? argv[argv.indexOf('--only') + 1].split(',') : null
|
|
const REPORT = argv.includes('--report')
|
|
|
|
// The 7 production maps' bundles (PRODUCTION-MATRIX.md) and the Experimental shelf.
|
|
const BUNDLES = [
|
|
'dashboard-plus-grid', 'research-dashboard', 'research-map-plus-grid', 'doxa-research-map', 'people-groups-grid',
|
|
'regions-dashboard', 'my-upg-100-list', 'doxa-simple-map', 'cities-internet', 'gospel-resources',
|
|
'all-people-groups', 'country-posters', 'doxa-vision-card', 'pplr-data-maps', 'prayer-walk-planner',
|
|
'upg-neighbor', 'wall-of-people-groups', 'key-cities', 'gospel-internet',
|
|
]
|
|
const ALLOW = {
|
|
'prayer-walk-planner:ENG-PIN': 'keyless by design: always MapLibre, needs no token (index.js header)',
|
|
'all-people-groups:LIST-DSM': 'draws the IMB dataset (all 12,379 groups), not the DOXA list: a list there is Tier B (SPEC-PEOPLE-GROUP-LIST-PROP.md)',
|
|
}
|
|
|
|
const stripComments = (src) => src
|
|
.replace(/<!--[\s\S]*?-->/g, (m) => m.replace(/[^\n]/g, ' '))
|
|
.replace(/\/\*[\s\S]*?\*\//g, (m) => m.replace(/[^\n]/g, ' '))
|
|
.replace(/(^|[^:\\'"`])\/\/[^\n]*/g, (m, p) => p + ' '.repeat(m.length - p.length))
|
|
|
|
const RULES = [
|
|
{ id: 'ENG-STYLE', prop: 'engine', test(src) {
|
|
// useMapInstance swaps any non-http style for an OpenFreeMap one under MapLibre
|
|
// (maplibreStyle() in useMapInstance.js), so a mapbox:// default handed to IT is fine. What is
|
|
// not translated: a direct map.setStyle('mapbox://…') and a raw `new gl.Map({ style: 'mapbox://…' })`.
|
|
const out = []
|
|
const res = [/setStyle\s*\(([^;]{0,220}?)mapbox:\/\/styles\//g, /new\s+[\w.]+\.Map\s*\(\s*\{[^}]{0,400}?style\s*:([^,}]{0,160}?)mapbox:\/\/styles\//g]
|
|
for (const re of res) { let m; while ((m = re.exec(src))) {
|
|
const win = src.slice(Math.max(0, m.index - 160), m.index + m[0].length)
|
|
if (/resolveNamedStyle|registry|\.style\s*\|\|/.test(win)) continue // a fallback behind the registry
|
|
out.push(m.index)
|
|
} }
|
|
return out } },
|
|
{ id: 'ENG-GLOBAL', prop: 'engine', re: /new\s+(?:window\.)?mapboxgl\.(?:Marker|Popup|Map|NavigationControl|FullscreenControl|GeolocateControl|ScaleControl|AttributionControl)\b/g },
|
|
{ id: 'ENG-PIN', prop: 'engine', re: /useMapInstance\s*\(\s*\{[^}]{0,400}?provider\s*:\s*['"](?:mapbox|maplibre)['"]/g },
|
|
{ id: 'TOK-GLOBAL', prop: 'token', re: /window\.DOXA_TOKEN\b/g, shipped: 'DOXA_TOKEN' },
|
|
{ id: 'TOK-LITERAL', prop: 'token', re: /['"`]pk\.ey[A-Za-z0-9_-]{20,}/g, shipped: /pk\.eyJ[A-Za-z0-9_-]{20,}/ },
|
|
{ id: 'LOC-ENUS', prop: 'locale', warn: true, test(src) {
|
|
// ★ A CATCH-BLOCK FALLBACK IS NOT A FIXED FORMAT, and the bare regex was WARNING THE CORRECT
|
|
// PATTERN. regions formats with `${locale}-u-nu-latn` -- a verbatim clone of DX-life's
|
|
// RegionDetail.vue:16, Latin digits with the reader's grouping -- and falls back to 'en-US' only
|
|
// inside a catch, because a bad locale tag throws RangeError in toLocaleString and blanking the
|
|
// page is worse than one wrong separator. Flagging that told the matrix's locale column to say
|
|
// "ignores the locale" about code that honours it, which is the kind of false warn that teaches
|
|
// people to stop reading the column.
|
|
// MEASURED before changing the rule: of the NINE LOC-ENUS sites in the tree, exactly TWO are
|
|
// such fallbacks (region-dashboards.vue, regions-list.vue). The other seven are genuinely
|
|
// operative fixed en-US and are still flagged, including RegionCountryMap's popup, which is a
|
|
// HELD Driver decision with its own card rather than an oversight.
|
|
const out = []
|
|
const re = /(?:toLocaleString|NumberFormat|DateTimeFormat)\s*\(\s*['"]en(?:-US)?['"]/g
|
|
let m
|
|
while ((m = re.exec(src))) {
|
|
const before = src.slice(src.lastIndexOf('\n', m.index) + 1, m.index)
|
|
// both halves required: a catch, AND a locale-DERIVED format call it is the fallback for.
|
|
// A lone `catch` nearby must not excuse a hardcoded format that never consulted the locale.
|
|
const isFallback = /\bcatch\b/.test(before)
|
|
&& /(?:toLocaleString|NumberFormat|DateTimeFormat)\s*\(\s*`[^`]*\$\{/.test(before)
|
|
if (!isFallback) out.push(m.index)
|
|
}
|
|
return out
|
|
} },
|
|
// A whole-file rule: the fetch and the helper need not be on the same line, only in the same file.
|
|
{ id: 'LIST-FETCH', prop: 'list', test(src) {
|
|
const m = /\/api\/people-groups\/list|endpoints\?\.bulk|doxa-people-groups\.json/.exec(src)
|
|
return m && !/applyPeopleGroupList\s*\(/.test(src) ? [m.index] : [] } },
|
|
{ id: 'LIST-DSM', prop: 'list', re: /new\s+DataSourceManager\s*\((?![^)]*\bprofileConfig\b)[^)]*\)/g },
|
|
{ id: 'LIST-KEYS', prop: 'list', re: /INHERITED_PAGE_KEYS\s*=\s*\[(?![^\]]*['"]list['"])[^\]]*\]/g },
|
|
]
|
|
const run = (rule, src) => rule.test ? rule.test(src) : [...src.matchAll(rule.re)].map((m) => m.index)
|
|
const lineOf = (src, idx) => src.slice(0, idx).split('\n').length
|
|
|
|
// ── self-test: every rule must see a planted violation ─────────────────────────────────────────
|
|
const PLANTED = `
|
|
map.setStyle(isDark ? 'mapbox://styles/mapbox/dark-v11' : 'x')
|
|
const m2 = new gl.Map({ container: el, style: 'mapbox://styles/mapbox/light-v11' })
|
|
const p = new mapboxgl.Popup()
|
|
useMapInstance({ container: el, provider: 'mapbox' })
|
|
const tk = window.DOXA_TOKEN
|
|
const t = 'pk.eyJ1IjoiZXhhbXBsZSIsImEiOiJjbGFudGVkIn0'
|
|
n.toLocaleString('en-US')
|
|
const r = await fetch(base + '/api/people-groups/list?fields=slug')
|
|
const dsm = new DataSourceManager({ locale: 'en' })
|
|
const INHERITED_PAGE_KEYS = ['locale', 'provider']`
|
|
const missed = RULES.filter((r) => !run(r, stripComments(PLANTED)).length).map((r) => r.id)
|
|
const CLEAN = `map.setStyle(resolveNamedStyle('ink', p).style || 'mapbox://styles/mapbox/dark-v11')
|
|
useMapInstance({ container: el, style: cfg.style ?? 'mapbox://styles/mapbox/light-v11' })
|
|
// new mapboxgl.Popup() in a comment is not code
|
|
n.toLocaleString(locale.value)
|
|
try { return v.toLocaleString(\`\${locale.value || 'en'}-u-nu-latn\`) } catch (_) { return v.toLocaleString('en-US') }
|
|
const r2 = await applyPeopleGroupList((await fetch(base + '/api/people-groups/list')).posts, cfg)
|
|
const dsm2 = new DataSourceManager({ locale: 'en', profileConfig })
|
|
const INHERITED_PAGE_KEYS = ['locale', 'provider', 'list']`
|
|
const falsePos = RULES.filter((r) => run(r, stripComments(CLEAN)).length).map((r) => r.id)
|
|
|
|
// ── LOC-ENUS discrimination, tested on purpose ──────────────────────────────────────────────────
|
|
// The fallback exemption is the one rule here that can WIDEN by accident: loosen it and a hardcoded
|
|
// format with a catch anywhere near it stops being seen. So the three adversarial shapes are pinned.
|
|
const locRule = RULES.find((r) => r.id === 'LOC-ENUS')
|
|
const LOC_CASES = [
|
|
[false, "try { return v.toLocaleString(`${locale.value || 'en'}-u-nu-latn`) } catch (_) { return v.toLocaleString('en-US') }"],
|
|
[true, "try { return v.toLocaleString('en-US') } catch (_) { return '' }"],
|
|
[true, "try { return fmt(v) } catch (_) { return v.toLocaleString('en-US') }"],
|
|
[true, "const pop = Number(p.population).toLocaleString('en-US')"],
|
|
]
|
|
const locWrong = LOC_CASES.filter(([shouldFlag, line]) => (run(locRule, line).length > 0) !== shouldFlag)
|
|
if (locWrong.length) {
|
|
console.error(`UNIVERSAL-PROPS: SELF-TEST FAILED — LOC-ENUS misjudged ${locWrong.length} case(s); `
|
|
+ 'the catch-fallback exemption must skip ONLY a locale-derived primary: '
|
|
+ locWrong.map(([f, l]) => `${f ? 'missed' : 'false-flagged'}: ${l.slice(0, 56)}`).join(' | '))
|
|
process.exit(3)
|
|
}
|
|
if (missed.length || falsePos.length) {
|
|
console.error(`UNIVERSAL-PROPS: SELF-TEST FAILED — ${missed.length ? 'rules blind to a planted bypass: ' + missed.join(', ') : ''}${falsePos.length ? ' false positives on clean code: ' + falsePos.join(', ') : ''}`)
|
|
process.exit(3)
|
|
}
|
|
|
|
// ── scan ───────────────────────────────────────────────────────────────────────────────────────
|
|
function files(dir) {
|
|
const out = []
|
|
for (const n of readdirSync(dir)) {
|
|
const p = join(dir, n)
|
|
if (n === 'node_modules' || n === 'scripts' || n === 'data' || n === 'assets') continue
|
|
const st = statSync(p)
|
|
if (st.isDirectory()) out.push(...files(p))
|
|
else if (/\.(vue|js|mjs)$/.test(n)) out.push(p)
|
|
}
|
|
return out
|
|
}
|
|
const findings = []
|
|
const matrix = {}
|
|
for (const b of BUNDLES.filter((x) => !ONLY || ONLY.includes(x))) {
|
|
const dir = resolve(ROOT, 'plugin-designs', b)
|
|
if (!existsSync(dir)) continue
|
|
matrix[b] = { engine: 'ok', token: 'ok', locale: 'ok', list: 'ok' }
|
|
for (const f of files(dir)) {
|
|
const src = stripComments(readFileSync(f, 'utf8'))
|
|
for (const r of RULES) for (const idx of run(r, src)) {
|
|
const allowed = ALLOW[`${b}:${r.id}`]
|
|
if (allowed) { if (matrix[b][r.prop] === 'ok') matrix[b][r.prop] = 'allowed'; continue }
|
|
findings.push({ b, rule: r.id, prop: r.prop, warn: !!r.warn, where: `${relative(ROOT, f)}:${lineOf(src, idx)}`, text: src.slice(idx, idx + 70).replace(/\s+/g, ' ').trim() })
|
|
matrix[b][r.prop] = r.warn ? (matrix[b][r.prop] === 'BYPASS' ? 'BYPASS' : 'warn') : 'BYPASS'
|
|
}
|
|
}
|
|
const shipped = resolve(ROOT, 'public', TREE_NAME, b, `${b}.js`)
|
|
if (existsSync(shipped)) {
|
|
const js = readFileSync(shipped, 'utf8')
|
|
for (const r of RULES.filter((x) => x.shipped)) {
|
|
const hit = typeof r.shipped === 'string' ? js.includes(r.shipped) : r.shipped.test(js)
|
|
if (hit && !ALLOW[`${b}:${r.id}`]) { findings.push({ b, rule: r.id, prop: r.prop, where: relative(ROOT, shipped), text: '(in the shipped bundle)' }); matrix[b][r.prop] = 'BYPASS' }
|
|
}
|
|
}
|
|
}
|
|
|
|
if (REPORT) {
|
|
console.log('bundle'.padEnd(24) + 'engine'.padEnd(10) + 'token'.padEnd(10) + 'locale'.padEnd(10) + 'list')
|
|
for (const [b, m] of Object.entries(matrix)) console.log(b.padEnd(24) + m.engine.padEnd(10) + m.token.padEnd(10) + m.locale.padEnd(10) + m.list)
|
|
}
|
|
for (const f of findings) console.log(`${f.warn ? 'WARN ' : 'BYPASS '} ${f.b.padEnd(22)} ${f.rule.padEnd(11)} ${f.where} ${f.text}`)
|
|
const hard = findings.filter((f) => !f.warn)
|
|
if (findings.length - hard.length) console.log(`UNIVERSAL-PROPS: ${findings.length - hard.length} WARN(s): LOC-ENUS fixes numerals to en-US whatever the locale. Warn-only until the Driver decides the numerals policy.`)
|
|
if (hard.length) {
|
|
console.log(`UNIVERSAL-PROPS: FAIL — ${hard.length} bypass(es) in ${new Set(hard.map((f) => f.b)).size} bundle(s). Read the prop through the library (documentation/reference-universal-props.md), or ALLOW it here with the reason it is by design.`)
|
|
process.exit(1)
|
|
}
|
|
console.log(`UNIVERSAL-PROPS: PASS — ${Object.keys(matrix).length} bundles read engine, token, locale and list through the library (self-test: ${RULES.length} rules each caught a planted bypass).`)
|