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.
This commit is contained in:
parent
c8242064e6
commit
cad913bc6f
7 changed files with 242 additions and 16 deletions
96
internal/check-tauri-config.mjs
Normal file
96
internal/check-tauri-config.mjs
Normal file
|
|
@ -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`.')
|
||||
33
package.json
33
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 `<button>` hits, most with visible text). Needs a browser, so NOT in build:unlocked and NOT in CI. ALL=1 sweeps every profile.",
|
||||
"check:catalog-identity": "node internal/check-catalog-identity.mjs # proves the LIVE server serves the CATALOG\u2019s exact bytes, by hashing each live bundle and comparing sha384 against the manifest\u2019s own integrity field. An HTTP 200 cannot do this: /app/doxa-maps/ returns 200 with a 2872-byte HTML SIGN-IN PAGE. Network-dependent, so NOT in build:unlocked and NOT in CI; SKIPs cleanly when unreachable. CATALOG=<dir> also requires the live manifest to match a catalog checkout.",
|
||||
"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 — 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 — 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 — 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 — update the DOC.",
|
||||
"capability-exemplars": "node internal/generate-capability-exemplars.mjs # regenerate docs/reference-capability-exemplars.md — 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 — a `title` is a TOOLTIP and does NOT count). Measures the RENDERED DOM through shadow roots: a source grep cannot answer this (132 `<button>` hits, most with visible text). Needs a browser, so NOT in build:unlocked and NOT in CI. ALL=1 sweeps every profile.",
|
||||
"check:catalog-identity": "node internal/check-catalog-identity.mjs # proves the LIVE server serves the CATALOG’s exact bytes, by hashing each live bundle and comparing sha384 against the manifest’s own integrity field. An HTTP 200 cannot do this: /app/doxa-maps/ returns 200 with a 2872-byte HTML SIGN-IN PAGE. Network-dependent, so NOT in build:unlocked and NOT in CI; SKIPs cleanly when unreachable. CATALOG=<dir> also requires the live manifest to match a catalog checkout.",
|
||||
"check:capability-exemplars": "node internal/check-capability-exemplars.mjs # REGENERATES and content-compares (not mtimes: a full build rewrites manifest.json every time, so an mtime rule would cry wolf after every build and get switched off).",
|
||||
"check:capability-citations": "node internal/check-capability-citations.mjs # a doc that names a bundle as \"capability N\" must match manifest.json, which is what the BUILD computed. README captioned doxa-vision-card as cap-1; it ships 2 instances so it is parameterized (cap-2). The SAME error was fixed once in verify-5-capabilities.mjs and never reached the README \u2014 two copies of one fact, nothing forcing agreement.",
|
||||
"check:shot-distinctness": "node internal/check-shot-distinctness.mjs # two README slots must not show the SAME view. README shipped \"List \u2014 page 1\" and \"page 2\" as the same picture: the shot chased a Next control and a scroll that DO NOT EXIST, every attempt inside try/catch so the miss was silent, and the PNGs differed by 0.16% render noise so no checksum caught them. Keys on url+VIEWPORT \u2014 same url at a different vp (deploy2 mobile) is intended.",
|
||||
"check:shot-fidelity": "node internal/check-shot-fidelity.mjs # warns when THIS machine cannot RENDER what the shots must SHOW. `bun run shots` printed 13x OK while every emoji rendered as a tofu box \u2014 a glyph fallback loads perfectly (0 console errors, 0 HTTP>=400), so the generator cannot see it. Reports by default; --strict to block once an emoji font is installed.",
|
||||
"shots": "node internal/readme-shots.mjs # regenerate docs/readme-images/*.png (fetches its own Mapbox token SERVER-SIDE, so the browser CORS block does not apply). Was UNREACHABLE \u2014 present but invoked by no script, which is how the 12 PNGs silently rotted a day behind the UI.",
|
||||
"library-index": "node internal/generate-library-index.mjs # regenerate docs/reference-library-index.md \u2014 the DERIVED menu of the 84 reusable library pieces an author can import instead of rebuilding. Was UNREACHABLE, and the index did not exist at all: its header promised 'can't drift from the code' while nothing ever ran it.",
|
||||
"check:done-evidence": "node internal/check-done-evidence.mjs # a card may not reach status:done with NO bluf AND NO fields \u2014 the Doxa team inherits this board and an evidence-less 'done' cannot be reconciled against the code. 10 pre-existing are baselined by id; the list should only ever SHRINK.",
|
||||
"check:capability-citations": "node internal/check-capability-citations.mjs # a doc that names a bundle as \"capability N\" must match manifest.json, which is what the BUILD computed. README captioned doxa-vision-card as cap-1; it ships 2 instances so it is parameterized (cap-2). The SAME error was fixed once in verify-5-capabilities.mjs and never reached the README — two copies of one fact, nothing forcing agreement.",
|
||||
"check:shot-distinctness": "node internal/check-shot-distinctness.mjs # two README slots must not show the SAME view. README shipped \"List — page 1\" and \"page 2\" as the same picture: the shot chased a Next control and a scroll that DO NOT EXIST, every attempt inside try/catch so the miss was silent, and the PNGs differed by 0.16% render noise so no checksum caught them. Keys on url+VIEWPORT — same url at a different vp (deploy2 mobile) is intended.",
|
||||
"check:shot-fidelity": "node internal/check-shot-fidelity.mjs # warns when THIS machine cannot RENDER what the shots must SHOW. `bun run shots` printed 13x OK while every emoji rendered as a tofu box — a glyph fallback loads perfectly (0 console errors, 0 HTTP>=400), so the generator cannot see it. Reports by default; --strict to block once an emoji font is installed.",
|
||||
"shots": "node internal/readme-shots.mjs # regenerate docs/readme-images/*.png (fetches its own Mapbox token SERVER-SIDE, so the browser CORS block does not apply). Was UNREACHABLE — present but invoked by no script, which is how the 12 PNGs silently rotted a day behind the UI.",
|
||||
"library-index": "node internal/generate-library-index.mjs # regenerate docs/reference-library-index.md — the DERIVED menu of the 84 reusable library pieces an author can import instead of rebuilding. Was UNREACHABLE, and the index did not exist at all: its header promised 'can't drift from the code' while nothing ever ran it.",
|
||||
"check:done-evidence": "node internal/check-done-evidence.mjs # a card may not reach status:done with NO bluf AND NO fields — the Doxa team inherits this board and an evidence-less 'done' cannot be reconciled against the code. 10 pre-existing are baselined by id; the list should only ever SHRINK.",
|
||||
"check:wp-embed": "php wordpress/tests/wp-embed-contract-test.php",
|
||||
"check:mobile": "bun run cap:sync && bun run cap:verify # deploy-2 CONTRACT check: needs NO JDK/SDK/device. cap sync is a file copy + plugin wiring, so it runs anywhere; cap:verify then asserts assets/public/index.html exists. Guards the SOFT FAILURE capacitor.config.json warns about: a wrong webDir yields an EMPTY assets/public and a BLANK WHITE SCREEN at launch, not an error.",
|
||||
"check:settled": "node internal/settled-tree-gate.mjs",
|
||||
"verify:5-caps": "node internal/verify-5-capabilities.mjs",
|
||||
"verify:gate1": "node internal/acceptance/big-picture-acceptance.mjs # the Gate-1 chain end-to-end against LIVE data: header emits research-filter -> composite passes props down -> the REAL cap-2 grid re-filters -> count reflected back up. Asserts the NEGATIVE (a filter that must return 0 does) and RESET. Was an ORPHAN \u2014 present in internal/acceptance/ but invoked by NO script, so it could never report anything."
|
||||
"verify:gate1": "node internal/acceptance/big-picture-acceptance.mjs # the Gate-1 chain end-to-end against LIVE data: header emits research-filter -> composite passes props down -> the REAL cap-2 grid re-filters -> count reflected back up. Asserts the NEGATIVE (a filter that must return 0 does) and RESET. Was an ORPHAN — present in internal/acceptance/ but invoked by NO script, so it could never report anything."
|
||||
},
|
||||
"devDependencies": {
|
||||
"@capacitor/android": "7",
|
||||
|
|
|
|||
3
src-tauri/.gitignore
vendored
Normal file
3
src-tauri/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
# Cargo build output. The committed source is Cargo.toml + build.rs + src/ only,
|
||||
# mirroring capacitor/android where only the generated assets/public/ is ignored.
|
||||
/target
|
||||
25
src-tauri/Cargo.toml
Normal file
25
src-tauri/Cargo.toml
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
# Cargo.toml — the desktop shell for the cap-5 doxa-maps SPA (deploy method 5).
|
||||
#
|
||||
# THIS CRATE INTENTIONALLY CONTAINS NO APPLICATION LOGIC. The product is the web tree in
|
||||
# public/; this is a window around it, the desktop counterpart of capacitor/android.
|
||||
# Anything that belongs to the maps belongs in plugin-designs/, never here.
|
||||
#
|
||||
# Versions are left to the lockfile on first `cargo build` rather than pinned by hand —
|
||||
# pinning a version this file has never resolved would be a guess presented as a fact.
|
||||
|
||||
[package]
|
||||
name = "doxa-maps-desktop"
|
||||
version = "0.1.0"
|
||||
description = "Doxa Maps — desktop shell for the capability-5 maps SPA"
|
||||
edition = "2021"
|
||||
rust-version = "1.77"
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { version = "2", features = [] }
|
||||
|
||||
[dependencies]
|
||||
tauri = { version = "2", features = [] }
|
||||
|
||||
[features]
|
||||
# Tauri's standard custom-protocol feature: used for production bundles.
|
||||
custom-protocol = ["tauri/custom-protocol"]
|
||||
5
src-tauri/build.rs
Normal file
5
src-tauri/build.rs
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
// build.rs — Tauri's standard build script. It reads ../tauri.conf.json and generates the
|
||||
// context (window config, bundle metadata) the binary embeds. Nothing project-specific.
|
||||
fn main() {
|
||||
tauri_build::build()
|
||||
}
|
||||
16
src-tauri/src/main.rs
Normal file
16
src-tauri/src/main.rs
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
// main.rs — the whole desktop shell.
|
||||
//
|
||||
// A Tauri app around the SAME public/ tree Capacitor and the CDN serve. The window, its
|
||||
// size and the entry page all come from ../tauri.conf.json (app.windows[0]) so there is
|
||||
// ONE place to change them and no value is duplicated between config and code.
|
||||
//
|
||||
// No commands, no IPC, no state. The maps are a compiled Vue custom element loaded by the
|
||||
// webview exactly as a browser loads them; giving this shell logic would fork behaviour
|
||||
// between desktop and web, which is the opposite of what one-build-four-consumers buys us.
|
||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||
|
||||
fn main() {
|
||||
tauri::Builder::default()
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running the Doxa Maps desktop shell");
|
||||
}
|
||||
80
tauri.conf.json
Normal file
80
tauri.conf.json
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "Doxa Maps",
|
||||
"version": "0.1.0",
|
||||
"identifier": "life.doxa.maps",
|
||||
|
||||
"//identifier": [
|
||||
"DELIBERATELY THE SAME ID AS CAPACITOR (capacitor.config.json appId). One product, two native",
|
||||
"shells: Android via Capacitor, desktop via Tauri. Same reverse-DNS identity, same web payload."
|
||||
],
|
||||
|
||||
"//frontendDist": [
|
||||
"frontendDist = ../public — THE SAME WEB-ROOT CAPACITOR USES (capacitor.config.json webDir: 'public').",
|
||||
"It is a POINTER, NOT A COPY: public/ is the single served surface, so ONE `bun run build` feeds every",
|
||||
"consumer. This file lives at the repo root and Tauri resolves frontendDist RELATIVE TO src-tauri/,",
|
||||
"hence '../public' rather than 'public'.",
|
||||
"ONE build, FOUR consumers, all reading the same root:",
|
||||
" 1. Railway / CDN — serve public/ statically",
|
||||
" 2. Capacitor — webDir; `cap sync` copies public/ into the Android assets",
|
||||
" 3. local staging — `bun run serve` (public/ on :4173)",
|
||||
" 4. Tauri — THIS frontendDist; the webview loads it from the bundled resources",
|
||||
"⚠️ SAME SOFT FAILURE AS CAPACITOR: a wrong frontendDist yields a BLANK WHITE WINDOW at launch, not an",
|
||||
"error. Assert the entry page exists before shipping (see `check:tauri` in package.json)."
|
||||
],
|
||||
"build": {
|
||||
"frontendDist": "../public",
|
||||
"//beforeBuildCommand": [
|
||||
"Left EMPTY on purpose. `bun run build` is the authoritative single-writer of public/ (CI runs it as",
|
||||
"build:unlocked). Wiring it here would make a desktop build a SECOND writer of the shared output tree —",
|
||||
"exactly the concurrent-clean race the build lock exists to prevent. Build first, then package."
|
||||
],
|
||||
"beforeBuildCommand": ""
|
||||
},
|
||||
|
||||
"app": {
|
||||
"//windows": [
|
||||
"1440x900 matches the viewport the cap-5 SPA is verified at (the rail renders 11 rows there).",
|
||||
"The window opens on the cap-5 SPA's leaf page — the flagship app, not the gallery index."
|
||||
],
|
||||
"windows": [
|
||||
{
|
||||
"title": "Doxa Maps",
|
||||
"width": 1440,
|
||||
"height": 900,
|
||||
"minWidth": 900,
|
||||
"minHeight": 600,
|
||||
"resizable": true,
|
||||
"url": "doxa-maps/doxa-maps-spa/doxa-maps-spa/index.html"
|
||||
}
|
||||
],
|
||||
"//security": [
|
||||
"CSP null = inherit whatever the served page declares, matching how the same tree behaves on the CDN.",
|
||||
"Do NOT tighten this without re-running the mount check: the bundles self-host mapbox-gl from",
|
||||
"public/assets/vendor/ (the no-CDN rule), so a naive script-src would break every map."
|
||||
],
|
||||
"security": { "csp": null }
|
||||
},
|
||||
|
||||
"//THE_TOKEN_CAVEAT": [
|
||||
"★ THE ONE RUNTIME SURPRISE, stated up front because Capacitor already hit it.",
|
||||
"A Tauri webview serves the app from a CUSTOM ORIGIN (tauri://localhost, or http://tauri.localhost on",
|
||||
"Windows) — NOT from doxa.life. Every map bundle fetches its Mapbox token from the configured tokenUrl,",
|
||||
"so that endpoint MUST send CORS headers permitting the Tauri origin, exactly as capacitor.config.json",
|
||||
"documents for https://localhost.",
|
||||
"THE FIX IS A RESPONSE HEADER ON THE TOKEN HOST, NOT A CHANGE IN THIS FILE.",
|
||||
"Measured 2026-09-21: https://doxa.life/api/maps/token returns 200; https://nuxtbase.app/api/maps/token",
|
||||
"returns 404 — so a desktop build pointed at nuxtbase would launch with blank basemaps even though the",
|
||||
"window and the element mount correctly. Verify the token endpoint before blaming the shell."
|
||||
],
|
||||
|
||||
"bundle": {
|
||||
"active": true,
|
||||
"targets": "all",
|
||||
"//icon": [
|
||||
"No icon set is committed yet. `cargo tauri icon <png>` generates the platform set into src-tauri/icons/.",
|
||||
"Left out rather than pointing at a path that does not exist — a config that references a missing file",
|
||||
"fails at BUILD time with a confusing error."
|
||||
]
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue