123 lines
7 KiB
Markdown
123 lines
7 KiB
Markdown
# `deployment/src-tauri/` — the desktop shell (deploy method 5)
|
||
|
||
A Tauri window around the **same `public/` tree** the CDN, Capacitor and local staging already
|
||
serve. This crate deliberately contains **no application logic**: the product is the web tree in
|
||
`plugin-designs/` → `public/`, and this is a window around it — the desktop counterpart of
|
||
`deployment/capacitor/android`.
|
||
|
||
## Why this file exists instead of comments in the config
|
||
|
||
`capacitor.config.json` documents itself with `"//key": [...]` comment entries, because Capacitor
|
||
ignores unknown keys. **Tauri does not.** Its config parser is strict and rejects unknown fields:
|
||
|
||
```
|
||
unknown field `//THE_TOKEN_CAVEAT`, expected one of `$schema`, `product-name`, `productName`,
|
||
`main-binary-name`, `mainBinaryName`, `version`, `identifier`, `app`, `build`, `bundle`, `plugins`
|
||
```
|
||
|
||
That is a hard build failure, found after `cargo check` compiled 228 crates. So the rationale
|
||
lives here and `tauri.conf.json` stays schema-pure. **Do not add `//` keys to it.**
|
||
|
||
## The two placement facts that cost a build each
|
||
|
||
1. **`tauri.conf.json` must live in `deployment/src-tauri/`, not the repo root.** `tauri-build` reads it from
|
||
the crate directory; at the root it is invisible and the build dies with *"unable to read Tauri
|
||
config file … because entity not found"*. `check:tauri` now asserts the canonical location and
|
||
fails on a stray root copy.
|
||
2. **`frontendDist` is relative to this directory**, hence `../public`. It is a POINTER, NOT A
|
||
COPY — one `bun run build` feeds four consumers:
|
||
|
||
| # | Consumer | Reads |
|
||
|---|---|---|
|
||
| 1 | Railway / CDN | `public/` served statically |
|
||
| 2 | Capacitor | `webDir: "public"` → copied into the Android assets |
|
||
| 3 | local staging | `bun run serve` (public/ on :4173) |
|
||
| 4 | **Tauri** | `build.frontendDist: "../public"` |
|
||
|
||
`beforeBuildCommand` is deliberately **empty**. `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 tree — the concurrent-clean race the build lock exists to
|
||
prevent. **Build first, then package.**
|
||
|
||
## The token / CORS caveat — read before debugging blank basemaps
|
||
|
||
A Tauri webview serves the app from a **custom origin** (`tauri://localhost`, or
|
||
`http://tauri.localhost` on Windows), *not* from your token host. Every map bundle fetches its
|
||
Mapbox token from the configured `tokenUrl`, so that endpoint **must send CORS headers permitting
|
||
the Tauri origin** — exactly the surprise `capacitor.config.json` documents for `https://localhost`.
|
||
|
||
**The fix is a response header on the token host, not a change in `tauri.conf.json`.**
|
||
|
||
Measured 2026-09-21: `https://doxa.life/api/maps/token` → **200**;
|
||
`https://nuxtbase.app/api/maps/token` → **404**. A desktop build pointed at nuxtbase would launch
|
||
with blank basemaps even though the window opens and the element mounts correctly. Separately, the
|
||
Mapbox token is **URL-restricted**: with a `nuxtbase.app` Referer the style document returns 200 but
|
||
every **tile 403s**, while the same token with a `doxa.life` Referer returns 200. Verify the token
|
||
path before blaming the shell.
|
||
|
||
## Why the window opens on the cap-5 SPA
|
||
|
||
`app.windows[0].url` points at `doxa-maps/doxa-maps-spa/doxa-maps-spa/index.html` — the flagship
|
||
capability-5 app, not the gallery index. 1440×900 is the viewport the SPA is verified at (its rail
|
||
renders 11 rows there: 6 curated + 5 auto-discovered).
|
||
|
||
`security.csp` is `null` so the page inherits whatever it declares when served, matching CDN
|
||
behaviour. Do not tighten it without re-running the mount check: the leaf pages load their map
|
||
engine from a **CDN** — `api.mapbox.com/mapbox-gl-js/…` for Mapbox and `cdn.jsdelivr.net/npm/
|
||
maplibre-gl@5/…` for MapLibre (see the `ENGINES` registry in
|
||
`library/composables/useMapInstance.js`). A naive `script-src 'self'` would break every map.
|
||
|
||
> **Corrected 2026-09-21.** This previously claimed the bundles "self-host `mapbox-gl` from
|
||
> `public/assets/vendor/` (the no-CDN rule)". That is **false** — measured: `vendor/` contains only
|
||
> `tabulator`, and the leaves carry `src="https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js"`.
|
||
> A CSP written from the old sentence would have been wrong in exactly the way that produces a
|
||
> blank map, so the error mattered.
|
||
|
||
## Cargo.lock is committed — on purpose
|
||
|
||
`Cargo.lock` was originally gitignored here, grouped with `/target` and `/gen`. Those two are
|
||
genuinely regenerated; a lockfile is not. It is the **resolution record** — the only artifact that
|
||
makes `cargo build` reproducible. Cargo's rule splits on crate type: **commit it for a binary,
|
||
ignore it for a library.** `cargo metadata` reports this crate as `kinds = ['bin']`.
|
||
|
||
The stakes are concrete: `Cargo.toml` pins nothing tighter than `tauri = { version = "2" }`, while
|
||
the lockfile pins **430 packages**. Without it, an inheritor building next month resolves a different
|
||
dependency tree than the one that produced the shipped 64 MB binary. Verified 2026-09-22 that the
|
||
committed lockfile is consistent with the manifest (`cargo metadata --locked --offline` succeeds) and
|
||
contains no local paths or credentials.
|
||
|
||
## Commands
|
||
|
||
```sh
|
||
bun run check:tauri # config contract — NO Rust, NO Tauri CLI needed (CI-safe)
|
||
cargo check # typechecks the crate (needs Rust + webkit2gtk)
|
||
bun run tauri:build # the real binary + .deb (CLI is a devDependency: @tauri-apps/cli)
|
||
```
|
||
|
||
## `bundle.targets` is `["deb"]`, not `"all"` — on purpose
|
||
|
||
`"all"` attempts **deb + rpm + appimage**. Measured on this box: `dpkg-deb` present, but
|
||
`rpmbuild` and `linuxdeploy` (AppImage) **absent**, so `"all"` would fail on two of three
|
||
formats after a full release compile. Narrowed to what the machine can actually produce.
|
||
Add targets back when the packaging tools exist — the failure comes at the very END of a
|
||
~6-minute release build, which is the expensive place to discover it.
|
||
|
||
## Status: BUILT — binary + .deb emitted; LAUNCH still unverified
|
||
|
||
Verified 2026-09-21 with the real toolchain (`tauri info`: webkit2gtk 2.50.6, rustc/cargo
|
||
1.96.1, frontendDist `../public`, framework Vue.js, bundler Vite — all ✔):
|
||
|
||
| Artifact | Result |
|
||
|---|---|
|
||
| `target/release/doxa-maps-desktop` | **65 MB ELF 64-bit executable** |
|
||
| `target/release/bundle/deb/*.deb` | **58 MB**, `Package: doxa-maps`, `Depends: libwebkit2gtk-4.1-0, libgtk-3-0` |
|
||
|
||
The web tree is **compiled INTO the binary** — not shipped as loose files, which is why the
|
||
`.deb` has only 6 entries. Confirmed by 19 distinct embedded `doxa-maps/...` asset paths in the
|
||
ELF (all bundle dirs + `assets/`), and by size: a logic-free shell is 65 MB only because the
|
||
132 MB `public/` tree is compressed inside it.
|
||
|
||
**What is still NOT proven: that the window opens and renders.** This VPS is headless — running
|
||
the binary panics in `tao` with *"Failed to initialize GTK"*, which is the absence of a display,
|
||
not a defect. No Xvfb is installed. A human on a desktop machine (or a CI job with a virtual
|
||
display) must run it once to move method 5 to *shot*.
|