doxa-maps/internal/set-app-start-page.mjs
doxa-maps-88655c70 79bd6d2ac3
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
public/ is one folder: the output tree plus a README, with the Plugin Studio as its front page
Driver decisions (cards BUILD-OUTPUT-IS-ONE-FOLDER, BUILD-PLUGIN-STUDIO-IS-THE-OUTPUT-FRONT-PAGE).

- public/ holds only <tree>/ and a generated README.md. The build no longer writes public/index.html,
  public/assets/ or public/__gallery/; `rm -rf public && bun run build` recreates exactly these two.
- The Plugin Studio is <tree>/index.html (tree-base "."), replacing the staging-forge chooser; nothing
  is written above the tree. check:staging-lang, which tested that chooser, is retired with it.
- Assets are gathered in a temp directory and the fetched allowlist staged into <tree>/assets/ as
  before. fonts/, images/, brand/ and legacy/ existed ONLY in public/assets: they are sources and move
  to data/ (git mv). Poster previews keep their tracked sources in plugin-designs/*/assets/previews
  (all 378 verified present).
- __gallery/ is archived out of public/ (a copy kept in the umbrella's _qa-cycle/archive) and out of
  the build; generate-gallery renders only on request, into a directory you name, never public/.
- Native shells load the output folder: capacitor webDir public/<tree> (cap copy needs an index.html
  there), Tauri frontendDist + window url relative to it. internal/sync-app-shells.mjs keeps both in
  step with the tree's name on every build, so the coming rename stays one config line; the Capacitor
  start page resolves its leaf tree-relative.
- dropin.include now lists only extras (README.md): the tree is always shipped and never named twice;
  excludes are tree-relative ({tree}/...). clean.mjs removes the old root entries from existing
  checkouts; the build lock and public/.gitignore's rules move out of public/.
- check:wp-embed read the BUILT manifest from gates:pre, before the build, so a from-scratch build
  could never pass; it runs in gates:post and reads the tree name from config.
- Stale bundles refreshed by the clean build (key-cities, template-bundle, wall-of-people-groups);
  the CLI doc's quoted scripts re-synced; capability exemplars regenerated; the '#' note leaves
  check:stale-sweep-claim's command (it was already in //scripts).

Proof: rm -rf public && bun run build -> public/ = README.md + doxa-maps/; check:sri, check:drop,
check:parse, check:public-hygiene, check:quoted-scripts, check:capability-exemplars PASS; all 57 pages
(the front page + 56 leaves) mount under /doxa-maps/ on this tree. check:facet-cascade is red as on main.
2026-09-25 17:41:56 +00:00

116 lines
6.1 KiB
JavaScript

#!/usr/bin/env node
/**
* set-app-start-page — the MOBILE APP opens the UPG-100 list, not the plugin studio.
*
* THE DEFECT, reported by repo: the Capacitor app launches public/index.html, which mounts
* <plugin-studio>. A recipient opening the phone app meets the DEV STUDIO rather than the map the
* README promises. RULED (Kingdom-agent, card DECISION-MOBILE-EXAMPLE-IS-UPG100-NOT-SIMPLE-
* RESEARCH): the ONE mobile-app showcase is the UPG-100 list.
*
* ⚠ THE CARD'S OWN BLUF SAYS "Simple Research" AND IS STALE. It predates that ruling, which is
* recorded as DONE in the same backlog and says Simple Research "was never mobile-friendly".
* Building to the card's wording would ship the app the ruling rejected.
*
* ★ WHY THIS RUNS AFTER `cap sync` AND WRITES ONLY INTO THE ANDROID ASSETS.
* public/index.html has THREE consumers (capacitor.config.json documents them): Railway/CDN,
* Capacitor, and local staging. The studio is CORRECT for the CDN root and for `bun run serve` —
* only the phone build wants a different landing page. Rewriting the shared root would fix the app
* by breaking the other two. `cap sync` copies webDir into
* deployment/capacitor/android/app/src/main/assets/public/, so overwriting the COPY changes what
* the app opens and leaves the served tree untouched.
*
* ★ WHY IT COPIES THE BUILT LEAF RATHER THAN HAND-WRITING A HOST PAGE.
* The leaf carries an SRI integrity hash for its bundle. A hand-written page needs its own correct
* hash, and a stale one makes the browser REFUSE the script with no console error at all: the
* element never upgrades and the app shows a blank screen. Copying the artifact the build already
* verified means there is no second copy of that hash to drift. Only the relative depth changes.
*
* Usage: node internal/set-app-start-page.mjs [--check]
* --check assert the app root is already the UPG-100 page; do not write. Exit 1 if not.
*/
import { readFileSync, writeFileSync, existsSync, copyFileSync } from 'node:fs'
import { resolve, dirname, join } from 'node:path'
import { fileURLToPath } from 'node:url'
import { TREE_NAME } from './_tree-name.mjs'
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..')
const CHECK = process.argv.includes('--check')
// The assets tree `cap sync` writes. NOT public/ — see the header.
const APP_PUBLIC = join(ROOT, 'deployment/capacitor/android/app/src/main/assets/public')
const APP_ROOT = join(APP_PUBLIC, 'index.html')
// The showcase leaf, inside the served tree.
const TREE = TREE_NAME
const LEAF_REL = `my-upg-100-list/upg-100-mfe/index.html` // the app's web root IS the tree (webDir public/<tree>)
const LEAF = join(APP_PUBLIC, LEAF_REL)
// A marker so --check can tell the app page apart from the studio page without parsing HTML.
const MARKER = `<!-- ${TREE_NAME}: mobile app start page (UPG-100 list) -->`
if (!existsSync(APP_PUBLIC)) {
console.log('APP-START-PAGE: SKIP — no synced assets tree.')
console.log(' Run `bun run cap:sync` first; this rewrites what that step copied.')
process.exit(0)
}
if (!existsSync(LEAF)) {
console.error(`APP-START-PAGE: FAIL — the showcase leaf is missing from the synced tree:`)
console.error(` ${LEAF_REL}`)
console.error(' The app would launch a 404. Build the tree and re-run cap:sync.')
process.exit(1)
}
if (CHECK) {
const cur = existsSync(APP_ROOT) ? readFileSync(APP_ROOT, 'utf8') : ''
if (cur.includes(MARKER)) {
console.log('APP-START-PAGE: PASS — the app opens the UPG-100 list.')
process.exit(0)
}
console.error('APP-START-PAGE: FAIL — the app root is NOT the UPG-100 page.')
console.error(cur.includes('<plugin-studio')
? ' It mounts <plugin-studio>: a recipient opening the app meets the dev studio.'
: ' No UPG-100 marker found.')
console.error(' Fix: node internal/set-app-start-page.mjs (after cap:sync).')
process.exit(1)
}
// ── REWRITE THE LEAF FOR ONE LEVEL UP ────────────────────────────────────────
// The leaf lives at <tree>/my-upg-100-list/upg-100-mfe/index.html and reaches its bundle with
// `../my-upg-100-list.js`. At the app root that same relative path resolves ABOVE the web root.
// Every leaf-relative reference gains the leaf's own directory prefix; absolute and protocol URLs
// (the Mapbox CDN tags) are left alone.
const LEAF_DIR = `my-upg-100-list/upg-100-mfe/`
const html = readFileSync(LEAF, 'utf8')
const rebased = html.replace(
/\b(src|href)=(["'])(?!https?:|data:|#|\/)([^"']+)\2/g,
(_m, attr, q, url) => {
// Resolve the leaf-relative url against the leaf's directory, from the web root.
const abs = new URL(url, `http://x/${LEAF_DIR}`).pathname.replace(/^\//, '')
return `${attr}=${q}${abs}${q}`
},
)
if (rebased === html) {
console.error('APP-START-PAGE: FAIL — rewrote nothing, so the paths were not what this expects.')
console.error(' Refusing to install a page whose bundle path would 404 at the app root.')
process.exit(1)
}
// Prove the rewrite points at files that EXIST, rather than trusting the regex. A start page
// whose bundle 404s is a blank white screen with no error, which is the failure mode this whole
// script exists to prevent.
const referenced = [...rebased.matchAll(/\b(?:src|href)=["']([^"':]+)["']/g)].map((m) => m[1])
const missing = referenced.filter((r) => !r.startsWith('data:') && !existsSync(join(APP_PUBLIC, r)))
if (missing.length) {
console.error('APP-START-PAGE: FAIL — the rewritten page references files that are not there:')
for (const m of missing) console.error(` ${m}`)
process.exit(1)
}
// Keep the studio page reachable rather than deleting it: a developer with the apk still wants it.
const STUDIO_KEEP = join(APP_PUBLIC, 'studio.html')
if (existsSync(APP_ROOT) && !existsSync(STUDIO_KEEP)) copyFileSync(APP_ROOT, STUDIO_KEEP)
writeFileSync(APP_ROOT, rebased.replace(/<head>/i, `<head>\n ${MARKER}`))
console.log('APP-START-PAGE: the app now opens the UPG-100 list.')
console.log(` from ${LEAF_REL}`)
console.log(` wrote assets/public/index.html (${referenced.length} reference(s) rebased, all present)`)
console.log(' the studio page is kept at assets/public/studio.html')