From cad913bc6fcf9e4b2ae83abe34253c19909af371 Mon Sep 17 00:00:00 2001 From: doxa-maps-88655c70 <88627970+doxa-maps-88655c70@users.noreply.github.com> Date: Mon, 21 Sep 2026 21:06:04 +0000 Subject: [PATCH] scaffold deploy-method 5: Tauri desktop shell for the cap-5 SPA Method 5 was the last aspirational entry in the 5-deploy-method model: 0 tauri.conf.json, 0 Cargo.toml, 0 .rs. It is now scaffolded against the v2 config schema, mirroring the Capacitor pattern that already works. frontendDist points at the SAME public/ tree Capacitor and the CDN serve, so one build still feeds every consumer - now four instead of three. The crate carries no application logic on purpose: the product is the web tree, this is a window around it. check:tauri is the analogue of cap:verify and exists for the same reason capacitor.config.json warns about in capitals: a wrong frontendDist or a window url that does not exist launches a BLANK WHITE WINDOW, not an error. It runs with no Rust toolchain and no Tauri CLI, so CI can run it. Negative-tested five ways - wrong frontendDist, missing window url, identifier drift from the Capacitor appId, empty windows array, missing crate file - each exits 1, and the config was restored byte-identical after every test. Status stays BUILT, NOT SHOT. rustc parses main.rs (it reaches line 14 and fails only on the uninstalled tauri crate, a dependency error not a syntax one), but producing a binary needs the Tauri CLI plus system webview libs. Also recorded the token/CORS caveat up front: a Tauri webview serves from tauri://localhost, so the token endpoint must send CORS for that origin - the same surprise Capacitor documents for https://localhost. --- internal/check-tauri-config.mjs | 96 +++++++++++++++++++++++++++++++++ package.json | 33 ++++++------ src-tauri/.gitignore | 3 ++ src-tauri/Cargo.toml | 25 +++++++++ src-tauri/build.rs | 5 ++ src-tauri/src/main.rs | 16 ++++++ tauri.conf.json | 80 +++++++++++++++++++++++++++ 7 files changed, 242 insertions(+), 16 deletions(-) create mode 100644 internal/check-tauri-config.mjs create mode 100644 src-tauri/.gitignore create mode 100644 src-tauri/Cargo.toml create mode 100644 src-tauri/build.rs create mode 100644 src-tauri/src/main.rs create mode 100644 tauri.conf.json diff --git a/internal/check-tauri-config.mjs b/internal/check-tauri-config.mjs new file mode 100644 index 000000000..d0ac79334 --- /dev/null +++ b/internal/check-tauri-config.mjs @@ -0,0 +1,96 @@ +#!/usr/bin/env node +/** + * check-tauri-config.mjs — the Tauri analogue of `cap:verify`. + * + * WHY THIS EXISTS: capacitor.config.json documents the failure mode in capitals — "a wrong + * webDir yields an EMPTY assets/public and a BLANK WHITE SCREEN at launch, NOT AN ERROR." + * Tauri has the identical shape: point `build.frontendDist` at the wrong place, or name a + * window `url` that does not exist in the built tree, and the app launches to a blank white + * window with no message. Neither `cargo build` nor any web gate catches it, because the + * path is only resolved by the webview at runtime. + * + * This check runs with NO Rust toolchain and NO Tauri CLI — it is pure config-vs-filesystem + * assertion, so it works in CI (which installs only bun) exactly as `check:mobile` does. + * What it CANNOT do is prove the binary builds or launches; that still needs + * `cargo tauri build` on a machine with the Tauri CLI and system webview libs. + * + * Exit 0 = the config points at files that exist. Exit 1 = a launch-blank-window defect. + */ +import { existsSync, readFileSync } from 'node:fs' +import { resolve, dirname, join } from 'node:path' +import { fileURLToPath } from 'node:url' + +const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..') +const CONF = join(ROOT, 'tauri.conf.json') +const fail = [] +const ok = [] + +if (!existsSync(CONF)) { + console.error('TAURI-CONFIG: FAIL — tauri.conf.json is missing at the repo root.') + process.exit(1) +} + +let conf +try { + conf = JSON.parse(readFileSync(CONF, 'utf8')) +} catch (e) { + console.error(`TAURI-CONFIG: FAIL — tauri.conf.json does not parse: ${e.message}`) + process.exit(1) +} + +// 1. identifier must match Capacitor's appId — one product, one reverse-DNS identity. +const capPath = join(ROOT, 'capacitor.config.json') +if (existsSync(capPath)) { + const appId = JSON.parse(readFileSync(capPath, 'utf8')).appId + if (conf.identifier !== appId) { + fail.push(`identifier "${conf.identifier}" != capacitor appId "${appId}" — one product should carry one identity`) + } else { + ok.push(`identifier matches capacitor appId (${appId})`) + } +} + +// 2. frontendDist must resolve to a real directory. Tauri resolves it RELATIVE TO src-tauri/, +// which is the single easiest thing to get wrong in this file. +const dist = conf?.build?.frontendDist +if (!dist) { + fail.push('build.frontendDist is unset — the webview would have nothing to load') +} else { + const resolved = resolve(ROOT, 'src-tauri', dist) + if (!existsSync(resolved)) { + fail.push(`build.frontendDist "${dist}" resolves to ${resolved}, which does not exist`) + } else { + ok.push(`frontendDist "${dist}" -> ${resolved.replace(ROOT + '/', '')}/`) + } +} + +// 3. EVERY window url must exist inside frontendDist. This is the blank-white-window check. +const windows = conf?.app?.windows || [] +if (!windows.length) { + fail.push('app.windows is empty — no window would open') +} +for (const w of windows) { + if (!w.url) { ok.push(`window "${w.title || '(untitled)'}" uses the default entry`); continue } + const page = resolve(ROOT, 'src-tauri', dist || '.', w.url) + if (!existsSync(page)) { + fail.push(`window url "${w.url}" does not exist in the built tree (${page.replace(ROOT + '/', '')}) — THIS LAUNCHES A BLANK WHITE WINDOW`) + } else { + const bytes = readFileSync(page, 'utf8').length + ok.push(`window url "${w.url}" exists (${bytes} bytes)`) + } +} + +// 4. The Rust crate the config depends on must be present. +for (const f of ['src-tauri/Cargo.toml', 'src-tauri/build.rs', 'src-tauri/src/main.rs']) { + if (!existsSync(join(ROOT, f))) fail.push(`${f} is missing — \`cargo tauri build\` would fail`) + else ok.push(`${f} present`) +} + +for (const line of ok) console.log(` · ${line}`) +if (fail.length) { + console.error(`\nTAURI-CONFIG: FAIL — ${fail.length} problem(s):`) + for (const f of fail) console.error(` ✗ ${f}`) + process.exit(1) +} +console.log('\nTAURI-CONFIG: PASS — config points at files that exist; no blank-window defect.') +console.log(' NOT PROVEN HERE: that the binary builds or launches. That needs the Tauri CLI') +console.log(' (`cargo install tauri-cli`) plus system webview libs, then `cargo tauri build`.') diff --git a/package.json b/package.json index f2989a449..3189f6b6b 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "version": "0.1.0", "license": "Apache-2.0", "private": true, - "description": "A portable bundler that generates embeddable JAMstack maps \u2014 one library, many map bundles that embed on any site, compose into a lazy-loaded SPA, or wrap a Capacitor Android/iOS app.", + "description": "A portable bundler that generates embeddable JAMstack maps — one library, many map bundles that embed on any site, compose into a lazy-loaded SPA, or wrap a Capacitor Android/iOS app.", "type": "module", "scripts": { "dev": "bun x vite", @@ -20,8 +20,9 @@ "verify:full": "node library/scripts/research/verify-all.mjs --full", "serve": "python3 -m http.server 4173 --bind 0.0.0.0 --directory public", "serve:shell": "python3 -m http.server 4174 --bind 0.0.0.0 --directory library/layouts/app-wrapper-material", - "cap:sync": "./node_modules/.bin/cap sync android # NOT `bunx` \u2014 bunx is not on PATH in this environment (command not found, exit 127), which broke cap:sync -> apk:debug -> phone at step one. The CLI IS installed locally (@capacitor/cli 7.6.9); invoke the local bin directly. The rest of package.json already uses the spaced `bun x` form.", + "cap:sync": "./node_modules/.bin/cap sync android # NOT `bunx` — bunx is not on PATH in this environment (command not found, exit 127), which broke cap:sync -> apk:debug -> phone at step one. The CLI IS installed locally (@capacitor/cli 7.6.9); invoke the local bin directly. The rest of package.json already uses the spaced `bun x` form.", "cap:verify": "test -f capacitor/android/app/src/main/assets/public/index.html && echo \"OK webDir copied\" || (echo \"FAIL: empty assets/public -> blank white screen. Check webDir.\" && exit 1)", + "check:tauri": "node internal/check-tauri-config.mjs", "apk:debug": "bun run build && bun run cap:sync && bun run cap:verify && cd capacitor/android && ./gradlew assembleDebug", "apk:install": "adb install -r capacitor/android/app/build/outputs/apk/debug/app-debug.apk", "phone": "bun run apk:debug && bun run apk:install", @@ -48,25 +49,25 @@ "check:template-literal": "node internal/template-literal-check.mjs", "verify:map-paint": "node internal/verify-map-paint.mjs", "check:sri": "node internal/sri-consistency-check.mjs", - "check:composite": "node internal/check-composite-freshness.mjs # a COMPOSITE is a COPY taken at its own build time: fails if a nested child's source is newer than the parent's bundle. NOT in build:unlocked on purpose \u2014 there everything was just built, so it could never catch the real case (a PARTIAL build). Run it before PUBLISH.", - "check:shots": "node internal/check-shot-freshness.mjs # a README screenshot that predates the bundle it shows is a picture telling a lie \u2014 and it looks IDENTICAL to a current one. Compares each shot's PNG against the BUILT BUNDLE it renders (not raw sources: an unbuilt edit is not yet in any leaf). Fix: node internal/readme-shots.mjs", - "check:library-index": "node internal/check-library-index.mjs # the DERIVED library menu must not fall behind library/. Compares the index against the newest library SOURCE (not a bundle \u2014 this generator scans library/ directly, with no build step between). Fix: bun run library-index", - "check:quoted-scripts": "node internal/check-quoted-scripts.mjs # docs/README-SECTION-3-cli-commands.md QUOTES the scripts block verbatim; it had silently drifted TWICE (a stale bunx cap:sync, and build:unlocked missing check:public-hygiene + check:label-joins). package.json is what RUNS \u2014 update the DOC.", - "capability-exemplars": "node internal/generate-capability-exemplars.mjs # regenerate docs/reference-capability-exemplars.md \u2014 the DERIVED answer to \"which bundle is the capability-N example?\", a question answered WRONGLY in three separate artifacts (a gate, the README, the /plugin-designer skill).", - "check:a11y-button-names": "node internal/check-a11y-button-names.mjs # every VISIBLE button must have an accessible name (aria-label / aria-labelledby / visible text \u2014 a `title` is a TOOLTIP and does NOT count). Measures the RENDERED DOM through shadow roots: a source grep cannot answer this (132 `