18 — Applications desktop et mobiles
Statut : fait · Phase : transverse · Mis à jour : 29/10/2026.
Décidé le 29/10/2026. Tout site construit avec Récit doit pouvoir devenir une application de bureau
(Linux, macOS, Windows) puis mobile (Android, iOS), sans code par site : une coquille Tauri 2
commune, configurée par une section app: du recit.config, qui embarque le site prérendu et le
sert hors ligne. Le modèle est la mise en place de Tauri du projet sass
(/home/deploy/work/sass), reprise là où elle s'applique et retournée là où Récit diffère : sass
est le client d'un serveur, un site Récit est un dossier de fichiers.
1. Objectifs et non-objectifs
Objectifs.
- Un site Récit (
sites/*,examples/site-*, le site d'un utilisateur) se construit en application native avec une commande :recit app build, à partir du mêmedist/querecit-site build. - Hors ligne complet : pages, recherche Pagefind, polices, récits publiés (
publish:), diagrammes. - Une seule coquille Rust, versionnée dans un paquet publié, jamais copiée dans les sites.
- La configuration est typée dans
@recit/core(zod,z.infer), comme toute section derecit.config, et présente dans le JSON Schemaconfig. - Les lots qui n'exigent pas la chaîne Rust (contrat, bundle, paquet, CLI, e2e web, docs) se font et se vérifient sur cette machine ; les lots natifs et mobiles sont marqués toolchain ou CI.
Non-objectifs.
- Pas de serveur embarqué, pas de SSR dans l'application : le mode
--experimental-ssrdu framework reste une affaire d'hébergement. - Pas d'authentification, de notifications, de stockage sécurisé ni de lien profond au départ :
sassen a besoin pour son SaaS, un site de contenu non. Le lien profond reste une option deapp:(§4.4) pour plus tard. - Pas de publication sur les stores dans ce plan : la question des comptes est ouverte (§10).
recit render(MP4) reste une chaîne Node + Chromium, hors application.
2. Ce que fait sass, et ce qui se transfère
2.1 Constats, avec les fichiers
| Mécanisme | Où | Constat |
|---|---|---|
| Tauri 2 | apps/desktop/src-tauri/tauri.conf.json:2, Cargo.toml:16-20 | $schema …/config/2, tauri = "2", plugins notification, deep-link, store, http |
| Frontend distant | tauri.conf.json:6-10 | devUrl: http://localhost:3000, frontendDist: "https://app.tonsaas.com" : l'app est un client de l'app Next déployée, qui a besoin d'un serveur (routes API, SQLite) |
| Permissions pour pages distantes | capabilities/default.json:6-8, :9-15 | remote.urls liste les origines qui ont le droit d'appeler les plugins ; sans cela l'app s'ouvre mais les appels sont refusés |
| Code Rust | src/lib.rs, src/main.rs, build.rs | 4 plugins, 2 commandes de démonstration (greet, get_system_info), #[cfg_attr(mobile, tauri::mobile_entry_point)] ; crate-type = ["staticlib","cdylib","rlib"] (Cargo.toml:10), prêt pour le mobile |
| Une coquille, N applications | packages/sites/scripts/app.ts:42-61, :63-66, :73 | un overlay JSON par site (productName, identifier, build.devUrl/frontendDist, fenêtre, capabilities) écrit dans apps/desktop/.site-configs/<site>.json (ignoré par git, apps/desktop/.gitignore:1) et passé à tauri dev|build --config |
| Réglages par site | app.ts:20-24, sites/engravingcol/app.json, sites/sass/app.json | app.json facultatif : productName, identifier, url ; identifiant par défaut com.tonsaas.<sous-domaine> sans tirets (:44) |
| Réutilisation du serveur de dev | app.ts:29-40, :45 | sonde du port 3000 ; s'il répond, beforeDevCommand: "" et l'app se branche dessus |
| Pont JavaScript | packages/native/src/bridge.ts, useTauri.ts, client.ts | isTauriEnvironment() (__TAURI_INTERNALS__ dans window), imports dynamiques des plugins avec repli web ; utilisé par aucun écran (apps/web/src n'importe pas @saas/native) |
| Mobile | icons/android, icons/ios ; apps/docs/pages/sites/application-desktop-et-mobile.md § Mobile | icônes générées par tauri icon, mais aucune cible initialisée (pas de gen/) ; Xcode et SDK Android absents de la machine de dev ; rien de vérifié |
| Stores | apps/docs/pages/projet/questions-ouvertes.md:76-78 (Q12) | qui publie, avec quel compte : non tranché ; recommandation de ne proposer le mobile qu'à la demande |
| Lien profond | apps/docs/pages/securite/authentification.md:47-49 | tonsaas://auth/callback?token= prévu, jamais branché |
| Mise à jour, signature, CI | — | aucune (pas de .github/, pas d'updater, pas de clé) |
| Commandes | package.json:13-14, Makefile:38-45 | desktop:dev, desktop:build, desktop-check (cargo check) |
2.2 Ce qui se transfère
Mécanisme de sass | Dans Récit |
|---|---|
Tauri 2, crate-type et point d'entrée mobile | tel quel dans la coquille de @recit/app (§4.3) |
Une coquille, N apps par overlay --config généré | tel quel, l'overlay est généré depuis app: du recit.config (§4.4) et non depuis un app.json |
| Réutilisation d'un serveur de dev déjà lancé | tel quel dans recit app dev (§4.7) |
Icônes de toutes les plateformes par tauri icon | tel quel, depuis l'icône du site (§4.8) |
frontendDist distant, remote.urls | sans objet : le bundle prérendu est embarqué, la capability ne nomme aucune origine distante |
Plugins notification, store, http, deep-link | sans objet au départ ; un seul plugin, opener, pour les liens externes (§4.6) ; deep-link reste une option de app: |
Pont JavaScript @saas/native | sans objet : la garde de navigation est côté Rust, zéro code client, @recit/site n'est pas touché |
Makefile et scripts desktop:* | adapté : sous-commandes recit app …, et un script app:build dans le package.json des sites qui le veulent |
3. Prérequis machine
La machine de travail actuelle (Debian, noyau 6.1 cloud, sans affichage) a node 22.22 et pnpm
11.6, et n'a ni rustc, cargo, rustup, ni Java, ni SDK/NDK Android, ni adb, ni les
bibliothèques de développement WebKitGTK ; build-essential est présent.
| Besoin | Niveau | Comment | Sert à |
|---|---|---|---|
Rust stable (rustc, cargo) | utilisateur | curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y, puis rustup default stable | cargo check, tout tauri build |
| Cibles Android | utilisateur | rustup target add aarch64-linux-android armv7-linux-androideabi i686-linux-android x86_64-linux-android | APP7 |
| Cibles iOS | utilisateur (Mac) | rustup target add aarch64-apple-ios aarch64-apple-ios-sim | APP8 |
| CLI Tauri | utilisateur (pnpm) | @tauri-apps/cli en dépendance de @recit/app, épinglé dans le catalogue de pnpm-workspace.yaml | APP1 et suivants |
| WebKitGTK et GTK (Linux) | apt, sudo | libwebkit2gtk-4.1-dev libgtk-3-dev libayatana-appindicator3-dev librsvg2-dev libssl-dev libxdo-dev pkg-config file curl wget | APP5 sur cette machine ; sinon CI (APP6) |
| JDK 17 | apt ou utilisateur | openjdk-17-jdk (sudo) ou une archive Temurin dans ~/.jdks (sans sudo), JAVA_HOME | APP7 |
| SDK Android (cmdline-tools, platform-tools, platforms 34, build-tools) et NDK ≥ 26 | utilisateur | sdkmanager dans ~/Android/Sdk, ANDROID_HOME, NDK_HOME | APP7 |
| Xcode complet, CocoaPods | Mac | App Store puis xcode-select --install, brew install cocoapods | APP8 |
| Un affichage | — | tauri build lie sans affichage ; tauri dev et un test de fumée demandent Xvfb (xvfb-run) ou un poste avec écran | APP5, APP6 |
Conséquence sur l'ordre : APP0 à APP4 ne touchent pas à Rust et s'exécutent ici ; APP5 demande
rustup et la ligne apt ; APP6 porte tout ce que la machine ne peut pas faire sur des runners
GitHub ; APP7 et APP8 sont CI ou poste équipé.
4. Architecture cible
4.1 Vue d'ensemble
Code
Trois règles de 03-packaging.md §8 fixent la place de chaque chose :
@recit/appportescope:tool, comme@recit/paged: Node uniquement, importé par la CLI (scope:distribution) et par personne d'autre. Un outil peut dépendre des fondations ; il n'a besoin que de@recit/core. Aucune nouvelle arête dansscripts/check-package-boundaries.js.- Un paquet publié n'importe jamais
@recit/site.@recit/applancerecit-site build(le binaire est une dépendance de chaque site) et lit ledist/; il n'importe rien du framework. - Une seule stack :
@tauri-apps/cliest la seule dépendance nouvelle, dans@recit/appseulement ;recitla charge à la demande comme@recit/motion-render(packages/cli/src/commands/motion.ts:200-213,peerDependenciesMetadepackages/cli/package.json). Un utilisateur qui n'en veut pas ne la télécharge pas.
4.2 Le paquet @recit/app
packages/tools/app, publié (ajouté à release.projects de nx.json, vérifié par
scripts/__tests__/release-workflow.spec.ts). Forme d'un paquet publié de 03-packaging.md §8 :
exports à trois conditions (types, @recit/source, default), construit par tsdown comme
packages/formats/motion-render.
API, le contrat que APP2 et APP3 consomment (les types de configuration viennent de @recit/core) :
TypeScript
4.3 La coquille
packages/tools/app/shell/, copiée telle quelle dans <site>/.recit/app/ par materializeShell
(dossier ignoré par git, comme .site-configs/ de sass ; la ligne .recit/ s'ajoute au
.gitignore racine) :
| Fichier | Rôle |
|---|---|
Cargo.toml | tauri = "2", tauri-plugin-opener = "2", serde, serde_json ; [lib] name = "recit_app_lib", crate-type = ["staticlib", "cdylib", "rlib"] |
build.rs | tauri_build::build() |
src/main.rs | recit_app_lib::run() avec windows_subsystem = "windows" hors debug (comme sass) |
src/lib.rs | #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() : plugin opener, garde on_navigation (§4.6), fenêtre montrée après on_page_load |
capabilities/default.json | windows: ["*"], permissions: ["core:default", "opener:default"], sans remote |
tauri.conf.json | généré par tauriConfig() à chaque appel, jamais édité à la main |
icons/ | générés par tauri icon (§4.8) |
.gitignore | target/, gen/, icons/ |
Le dossier target/ de Cargo vit dans <site>/.recit/app/target par défaut ;
RECIT_APP_TARGET_DIR le déplace pour partager le cache entre les sites d'un monorepo.
Le tauri.conf.json généré, pour un site :
JSON
bundle.android.minSdkVersion, bundle.iOS.developmentTeam, plugins.deep-link et
plugins.updater n'apparaissent que si la section app: les renseigne.
4.4 La section app: du recit.config
Dans packages/core/src/config/validate.ts, à côté de DeckConfigSchema et PublishConfigSchema,
et ajoutée à RecitConfigSchema (app: z.optional(AppConfigSchema)). C'est une section Récit :
defineConfig la retire des options du site et la range sous RECIT_CONFIG
(packages/core/src/config/define.ts:47-57), donc elle n'entre pas dans SITE_CONFIG_KEYS et le
test packages/integrations/site-stories/src/__tests__/site-config.spec.ts (« Récit sections never
collide with site options ») reste vert sans changement. Le JSON Schema config suit tout seul
(packages/core/src/schema/json-schema.ts:25).
TypeScript
resolveAppConfig(config, site: { dir, packageVersion }) rend un ResolvedAppConfig où chaque
champ a sa valeur, plus des Diagnostic[] (type de @recit/core) :
identifier: au moins deux segments, chacun[a-zA-Z][a-zA-Z0-9_]*(règle Android, pas de tiret ;sassles supprime,app.ts:44). Sans valeur nicanonicalUrlOrigin: erreur, le message donne la règle.basePathrenseigné : erreur. Tauri sertfrontendDistà la racine, or le routeur (createBrowserRouter,basename,packages/site/src/app/entry.client.tsx:68-70) et Pagefind (PagefindSearch.tsx:37) attendent le préfixe. Les sites Récit n'en ont pas.cdnUrlrenseigné : erreur, les assets viendraient du réseau.search.type: "inkeep": avertissement, la recherche est distante.appabsent : erreur derecit app, « ajoutez une sectionapp:», avec l'exemple minimal.
4.5 Le bundle statique
recit-site build produit déjà ce qu'il faut, sans option nouvelle :
- une page par route en
<chemin>.html(packages/site/src/vite/prerender/worker.ts:33),index.htmlpour/, et un fichier de 85 octets par redirection ; - des assets référencés en relatif (
renderBuiltUrlrend{ relative: true }sans CDN,packages/site/src/vite/config.ts:196-206) ; - les polices embarquées (
@fontsource-variable/geist,packages/site/src/app/main.css:1-2), Shiki, Mermaid, KaTeX dans le bundle ; - l'index Pagefind dans
dist/pagefind/(prerender.ts:216-218), chargé par import dynamique de/pagefind/pagefind.js(PagefindSearch.tsx:36-37) ;pagefind.jscharge son.wasmet ses fragments relativement à lui-même ; - le dossier
server/supprimé après prérendu (packages/site/src/vite/build.ts:236).
Le protocole de Tauri (tauri://localhost sur macOS, Linux et iOS, http://tauri.localhost sur
Windows et Android) résout une requête /a/b en essayant a/b, a/b.html, a/b/index.html puis
index.html en repli : un rechargement sur une page profonde trouve son .html, un chemin
inconnu tombe sur la route 404 du routeur (packages/site/src/app/notFoundRoute.tsx), le .wasm
est servi avec son type MIME. serveLikeTauri() reproduit exactement cette chaîne pour la vérifier
en e2e sans Rust (APP3) et pour recit app preview.
Ce qui reste dans le bundle et ne sert à rien dans l'app (sitemap, llms.txt, copies .md de
publishMarkdown) pèse quelques centaines de kilo-octets : on ne filtre pas.
4.6 Liens externes et navigation
Un site Récit renvoie vers les autres sites (header.navigation de
packages/tools/site-kit/src/config.tsx:100-109), GitHub, et les liens des pages. Dans la fenêtre
native, ces navigations doivent partir dans le navigateur du système, pas remplacer l'application.
Deux mécanismes, tous deux côté Rust :
tauri::Builder::on_navigation: autorise l'origine du bundle,devUrlen dev etapp.allowedOrigins; pour toute autre URL, appelletauri_plugin_opener::open_urlet refuse la navigation.window.openettarget="_blank"ne passent pas paron_navigation: un script d'initialisation (initialization_script) de dix lignes remplacewindow.openpar un appel àplugin:opener|open_url. À vérifier en APP5 sur les trois systèmes.
Rien n'est ajouté au framework ni aux composants : le site ne sait pas qu'il tourne dans une
application. Si un jour une page doit le savoir, "__TAURI_INTERNALS__" in window suffit (sass,
bridge.ts:3-5), sans dépendance.
4.7 La boucle de développement
recit app dev [dir] :
- lit
app:et le port du site (portdu config, sinon celui derecit-site dev) ; - sonde le port (
isPortInUse) ; s'il répond, réutilise le serveur, sinon lancerecit-site dev --port <p>dans le dossier du site ; - matérialise la coquille avec
devUrl: http://localhost:<p>et lancetauri dev --config.
Le serveur de dev du framework accepte tout nom d'hôte depuis le 29/10 (allowedHosts: true,
packages/site/src/vite/config.ts:130) et écoute sur toutes les interfaces
(packages/site/src/vite/dev-server.ts:288) : tauri android dev et tauri ios dev réécrivent
devUrl vers l'adresse LAN de la machine pour l'émulateur ou l'appareil, et le serveur répond. Le
HMR passe par la même origine que devUrl, il fonctionne dans la webview.
4.8 Icônes et écran de lancement
materializeShell lance tauri icon <source> -o icons/ quand la source est plus récente que
icons/icon.png. La source est app.icon, sinon metadata.favicon du site (SVG accepté ; pour les
sites Récit c'est brand/logo/q07-icon.svg). Cela produit les tailles desktop, Android (mipmap-*)
et iOS (AppIcon-*), comme dans sass (icons/android, icons/ios).
Pas d'écran de lancement sur desktop : la fenêtre naît visible: false et s'affiche à
on_page_load, ce qui évite le blanc. Sur Android 12+, le système dérive le splash de l'icône ; sur
iOS, gen/apple fournit un LaunchScreen.storyboard par défaut : on le laisse.
4.9 Mise à jour et distribution
- Desktop :
tauri-plugin-updater+tauri-plugin-process, cléstauri signer generate,TAURI_SIGNING_PRIVATE_KEYen secret CI,latest.jsonet les binaires sur la GitHub Release de l'application. Opt-in parapp.updater; la coquille n'ajoute les plugins que si la section est présente. Lot APP9. - Mobile : les stores assurent la mise à jour ; l'updater est ignoré.
- Signature : macOS (Developer ID, notarisation) et Windows (certificat) sont des secrets CI ; sans eux, APP6 produit des binaires non signés, suffisants pour tester.
4.10 Decks et films
- Un deck publié par
publish:est déjà dansdist/: il est dans l'application sans rien faire. La télécommande smartphone (--remote, WebSocket du serveur de dev,packages/cli/src/commands/dev.ts:312) n'a pas de serveur dans l'app : hors périmètre. La console présentateur ouvre une seconde fenêtre parwindow.open: traitée par le script de §4.6, à vérifier en APP5. - Un film (
StoryEmbed format=motion) se joue dans la webview par@recit/motion-blocks, déjà dans le bundle.recit renderreste hors application. - Un deck seul (
recit app build slides.md) : la sortie derecit buildest déjà relative (base = "./",packages/cli/src/render/build-deck.ts:43), la même coquille la prend commefrontendDist. Lot APP10, optionnel, une fois la coquille stable.
5. Lots
Chaque lot nomme ses fichiers possédés (deux agents ne touchent jamais le même fichier), ce dont il dépend, ce qu'il fournit aux autres, et la vérification qui le déclare fini. Les tailles sont S, M, L. Toolchain : exige Rust et la ligne apt de §3 ; CI : s'exécute sur un runner GitHub.
État au 29/10/2026
Exécuté le jour même par des agents en parallèle (notes détaillées dans journal.md, une par lot) :
| Lot | État |
|---|---|
| APP0 | Fait et vérifié : section app:, résolveur, 14 tests, golden du JSON Schema |
| APP1 | Fait : paquet @recit/app, coquille compilée ici, 94 tests |
| APP2 | Fait : recit app build|dev|preview|android|ios|icon, 105 tests avec APP10 et APP11a |
| APP3 | Fait : 5 e2e sur le bundle servi comme Tauri, aucune requête externe |
| APP4 | Fait : trois pages de doc, opt-in de site-embed, plans 03 et 06 |
| APP5 | Fait ici : binaires .deb/.rpm/.AppImage, garde de navigation, fenêtre, cache partagé |
| APP6 | Écrit, pas encore exécuté : aucun push depuis la machine de travail |
| APP7 | Fait ici sauf appareil et émulateur : .apk signé 21,7 Mo, .aab 15,2 Mo |
| APP8 | Écrit (code, workflow, doc), non vérifié faute de Mac |
| APP9 | Fait ici : updater signé, cycle 0.1.0 → 0.1.1 prouvé en local, app-release.yaml écrit |
| APP10 | Fait : deck seul, --remote refusé ; APP10.2 : seconde fenêtre native et BroadcastChannel prouvés |
| APP11a, APP11b | Faits : intégration CLI, @recit/app, cœur, CI, doc, d'après les constats des lots |
APP0 — Le contrat de configuration (S), sans Rust
Dépend de : rien. Parallèle avec : rien, il ouvre la voie.
Fichiers possédés : packages/core/src/config/validate.ts, packages/core/src/config/app.ts
(nouveau : resolveAppConfig, ResolvedAppConfig), packages/core/src/config/index.ts (une ligne
d'export), packages/core/src/__tests__/app-config.spec.ts (nouveau), et le golden ou instantané du
JSON Schema config s'il en existe un.
- APP0.1 —
AppConfigSchemaetAppConfigde §4.4,app: z.optional(AppConfigSchema)dansRecitConfigSchema. - APP0.2 —
resolveAppConfig()avec les défauts et les diagnostics de §4.4 ; aucune exception, desDiagnostic[]. - APP0.3 — tests : défauts, identifiant inversé depuis
canonicalUrlOrigin(docs.recit.dev→dev.recit.docs), identifiant invalide,basePath,cdnUrl, section absente ;defineConfigrangeappsousRECIT_CONFIGet pas dans les options du site.
Fournit : AppConfigSchema, AppConfig, ResolvedAppConfig, resolveAppConfig depuis
@recit/core/config. Les lots suivants ne redéfinissent aucun de ces types.
Vérification :
Bash
APP1 — Le paquet @recit/app (M), sans Rust
Dépend de : APP0. Parallèle avec : APP4.
Fichiers possédés : packages/tools/app/** (nouveau), pnpm-workspace.yaml (ligne de catalogue
@tauri-apps/cli), nx.json (release.projects), .nx/version-plans/version-plan-app.md
(nouveau), .gitignore racine (ligne .recit/).
- APP1.1 — squelette sur le modèle de
packages/formats/motion-render:package.json(scope:tool,exportsà trois conditions,tsdown),tsconfig.json,tsdown.config.ts,LICENSE.md,README.mdcourt. - APP1.2 —
tauriConfig()et ses instantanés : desktop, Android, iOS, avec et sansdeepLink,updater,devUrl. - APP1.3 —
shell/(§4.3) : Rust écrit et relu, non compilé ici ;materializeShell()copie, génèretauri.conf.json, appelletauri iconsi la source a changé ; test sur un dossier temporaire (sans lancertauri icon: injecté). - APP1.4 —
serveLikeTauri(): la chaîne de résolution de §4.5, types MIME (.wasm), test unitaire sur undist/factice (a.html,b/index.html, repliindex.html). - APP1.5 —
runTauri()etisPortInUse(). - APP1.6 — version plan
prerelease(§8),release.projects, catalogue.
Fournit : l'API de §4.2, figée.
Vérification :
Bash
APP2 — La commande recit app (M), sans Rust
Dépend de : APP1. Parallèle avec : APP3.
Fichiers possédés : packages/cli/src/commands/app.ts (nouveau),
packages/cli/src/__tests__/app.spec.ts (nouveau), packages/cli/src/index.ts (seulement les
lignes d'enregistrement de la commande), packages/cli/package.json (@recit/app en peer
optionnel, comme @recit/motion-render).
- APP2.1 —
recit app build [dir] [--target desktop|android|ios] [--skip-site-build]:resolveAppConfig,recit-site build(spawn dans le dossier du site),materializeShell,tauri build --config. Diagnostics imprimés comme ceux derecit build. - APP2.2 —
recit app dev [dir](§4.7). - APP2.3 —
recit app preview [dir] [--port]:serveLikeTauri(dist), imprime l'URL. - APP2.4 —
recit app android <init|dev|build> [-- …]etrecit app ios <init|dev|build> [-- …]: passe-plat verstauri android …ettauri ios …dans la coquille, les arguments après--transmis tels quels (--aab,--export-method), avec le message des prérequis de §3 quand l'outil manque. APP7 et APP8 n'ont donc rien à ajouter à la CLI. - APP2.5 —
recit app icon [source]: régénère les icônes. - APP2.6 —
@recit/appabsent : message « installez @recit/app » (motif demotion.ts:200-213). - APP2.7 — tests avec
@recit/appinjecté (commeloadRenderer) : enchaînement des étapes, options, codes de sortie.
Fournit : les noms de commandes ci-dessus, que APP4 documente.
Vérification :
Bash
APP3 — Le bundle vérifié en e2e (S), sans Rust
Dépend de : APP1. Parallèle avec : APP2.
Fichiers possédés : e2e/site-app-bundle.spec.ts (nouveau), e2e/ports.ts (une constante
APP_BUNDLE_PORT, hors de E2E_PORTS dont chaque clé lance recit-site preview,
playwright.config.ts:33), playwright.config.ts (une entrée webServer de plus),
e2e/package.json (dépendance @recit/app), e2e/support.ts si un helper est nécessaire.
- APP3.1 —
examples/site-embed/distservi parserveLikeTaurisurAPP_BUNDLE_PORT. - APP3.2 — tests : la page d'accueil, une navigation client vers
/stories/lorem-ipsum, un rechargement sur cette adresse profonde (fichier.html), une recherche Pagefind qui rend des résultats,/nulle-partqui affiche la page 404 par repliindex.html. - APP3.3 — hors ligne : toutes les requêtes de la session (
page.on("request")) ont l'origine du serveur ; une seule requête externe fait échouer le test.
Vérification :
Bash
APP4 — Documentation, plans et opt-in de l'exemple (S), sans Rust
Dépend de : APP0 (la clé app doit exister pour le typecheck de l'exemple). Parallèle avec : APP1.
Les noms de commandes viennent de ce plan (§5, APP2), pas du code.
Fichiers possédés : examples/site-embed/recit.config.ts (section app:),
sites/docs/pages/documentation/configuration/app.mdx, app-android.mdx et app-ios.mdx
(nouveaux) et les lignes qui les ajoutent dans sites/docs/sidebar.ts,
sites/plans/pages/03-packaging.md (app dans l'arborescence §2, la liste §6 et la table des tags
§8), sites/plans/pages/06-roadmap.md (une ligne dans P5 renvoyant ici). NOTICE.md n'est pas
touché : la coquille est le gabarit de Tauri, pas du code de sass.
- APP4.1 —
app: { productName: "Lorem Ipsum Docs", identifier: "com.example.loremipsum" }dans l'exemple ; le round-trip des récits ne lit pas la config, rien d'autre ne bouge. - APP4.2 — la page de doc : installer
@recit/app, la sectionapp:champ par champ, les commandes, les prérequis desktop, ce qui marche hors ligne, les limites (basePath,cdnUrl, télécommande). Deux pages sœurs,app-android.mdxetapp-ios.mdx, créées avec les prérequis de §3 et la mention « en cours » : APP7 et APP8 les remplissent sans toucher au sidebar. - APP4.3 — plans : table des tags, roadmap, et le lien depuis
index.mdx(fait avec ce plan).
Vérification :
Bash
APP5 — La coquille compilée et le premier binaire (M), toolchain
Dépend de : APP1, APP2. Parallèle avec : rien ; si la machine ne compile pas, APP6 le remplace et APP5 devient la lecture des journaux du run (§6).
Fichiers possédés : packages/tools/app/shell/** (reprend la propriété après APP1),
packages/tools/app/src/lib-rs.spec.ts ou équivalent si un test lit le Rust.
- APP5.1 —
cargo checkde la coquille matérialisée pourexamples/site-embed. - APP5.2 —
recit app buildsur Linux :.debet.AppImage; l'app s'ouvre (sousxvfb-runsi la machine n'a pas d'écran) sur la page d'accueil, navigue, recharge, cherche. - APP5.3 — garde de navigation (§4.6) : un lien vers GitHub ouvre le navigateur, la fenêtre
reste sur le site ;
target="_blank"et la console présentateur d'un deck publié. - APP5.4 — fenêtre cachée jusqu'à
on_page_load, titre et taille deapp:. - APP5.5 —
RECIT_APP_TARGET_DIRpartagé entre deux sites : le second build ne recompile pas Tauri.
Vérification :
Bash
APP6 — CI desktop sur trois systèmes (M), CI
Dépend de : APP2, et APP5 quand la machine compile. Parallèle avec : APP9, APP10 quand APP5 a tourné localement.
Fichiers possédés : .github/workflows/app.yaml (nouveau). Rien d'autre :
scripts/__tests__/release-workflow.spec.ts lit déjà tout .github/workflows et exige chaque
uses: épinglé par SHA (release-workflow.spec.ts:80-90).
- APP6.1 —
workflow_dispatch, matriceubuntu-24.04(apt de §3),macos-15,windows-2025;dtolnay/rust-toolchainetSwatinem/rust-cacheépinglés ;pnpm install, build du site,recit app build,actions/upload-artifact. - APP6.2 — un test de fumée par système : le binaire démarre et quitte (
xvfb-runsur Linux). - APP6.3 — secrets optionnels de signature (macOS, Windows) : le job passe sans eux.
Vérification : un run vert avec trois artefacts téléchargeables ;
pnpm exec vitest run scripts/__tests__/release-workflow.spec.ts.
APP7 — Android (L), toolchain / CI
Dépend de : APP6. Parallèle avec : APP8.
Fichiers possédés : packages/tools/app/src/android.ts (nouveau : keystore depuis l'environnement,
gen/android/keystore.properties jamais commité), .github/workflows/app-android.yaml (nouveau),
sites/docs/pages/documentation/configuration/app-android.mdx (créée par APP4).
- APP7.1 —
recit app android initsur un poste équipé ou en CI (android-actions/setup-androidépinglé) ;minSdkVersiondepuisapp.android. - APP7.2 —
recit app android devcontrerecit-site devsur le LAN (§4.7), émulateur. - APP7.3 —
recit app android build -- --aabsigné par un keystore de test ; l'.apks'installe et le site tourne hors ligne (mode avion). - APP7.4 — tailles : poids de l'
.apkd'examples/site-embednoté dans le journal.
Vérification : run CI vert avec .apk et .aab ; installation manuelle vérifiée une fois.
APP8 — iOS (L), Mac / CI
Dépend de : APP6. Parallèle avec : APP7.
Fichiers possédés : packages/tools/app/src/ios.ts (nouveau : developmentTeam depuis app.ios ou
APPLE_DEVELOPMENT_TEAM), .github/workflows/app-ios.yaml (nouveau),
sites/docs/pages/documentation/configuration/app-ios.mdx (créée par APP4).
- APP8.1 —
recit app ios initsur Mac ; simulateur avecrecit app ios dev. - APP8.2 —
recit app ios build -- --export-method app-store-connectavec une équipe de test. - APP8.3 —
LaunchScreenet icônes vérifiés ; hors ligne en mode avion.
Vérification : run CI macOS vert avec un .ipa ; TestFlight une fois les comptes tranchés (§10).
APP9 — Mise à jour automatique desktop (M), toolchain, optionnel
Dépend de : une coquille compilée (APP5 ou APP6). Parallèle avec : APP7, APP8, APP10.
Fichiers possédés : packages/tools/app/shell/Cargo.toml et shell/src/lib.rs (sections updater,
conditionnelles ; tauriConfig() sait déjà écrire plugins.updater depuis APP1.2),
.github/workflows/app-release.yaml (nouveau : latest.json, signature, GitHub Release), sans
toucher à app.yaml.
- APP9.1 —
tauri signer generate, clé publique dansapp.updater.pubkey, privée en secret. - APP9.2 — vérification au lancement, dialogue natif, redémarrage (
tauri-plugin-process). - APP9.3 — un cycle complet entre deux versions de test.
Vérification : une app en version n se met à jour en n+1 depuis une GitHub Release de test.
APP10 — Deck seul, console présentateur (M), toolchain, optionnel
Dépend de : une coquille compilée (APP5 ou APP6). Parallèle avec : APP7 à APP9.
Fichiers possédés : packages/cli/src/commands/app.ts (reprend la propriété après APP2 : branche «
fichier de récit » de recit app build), packages/tools/app/src/deck.ts (nouveau), les tests
correspondants.
- APP10.1 —
recit app build slides.md:recit buildpuis la coquille avecfrontendDistsur la sortie du deck ;app:lue dans lerecit.configdu deck (même schéma). - APP10.2 — console présentateur en seconde fenêtre native.
- APP10.3 —
--remote: diagnostic clair, « pas de télécommande dans l'application ».
Vérification : pnpm exec vitest run packages/cli packages/tools/app ; un deck d'examples/ en
.AppImage.
6. Exécution multi-agent
Le plan s'exécute par plusieurs agents en parallèle sur le même arbre de travail, et d'autres sessions peuvent éditer des fichiers en même temps. Règles pour chaque agent :
- Relire avant d'écrire. Chaque fichier est relu juste avant d'être modifié ; si son contenu a
changé entre la lecture et l'écriture, relire et refaire la modification sur le nouveau contenu.
Dans
journal.mdetindex.mdx, on ajoute ses lignes, on ne réordonne ni ne réécrit rien, et on ne rétablit jamais un changement qu'on n'a pas fait. - Un lot, un agent, ses fichiers. La liste « fichiers possédés » de §5 est exhaustive : un
fichier qui manque se demande au coordinateur avant d'y toucher. Les fichiers partagés
(
packages/cli/src/index.ts,nx.json,pnpm-workspace.yaml,.gitignore,e2e/ports.ts,playwright.config.ts) ont chacun un propriétaire. - Jamais de git (
commit,push,add,stash, branche, worktree), jamaispnpm fixnioxfmtsur tout le dépôt : les sources Markdown desites/*/pagesetexamples/**en sont exclues et une mise en forme les casse.pnpm checket les vérifications de son lot seulement. - Une note dans
journal.mdà la fin du lot, ajoutée à la fin du fichier sous un titre### APPn — <titre> — <date>: ce qui est fait, ce qui est mesuré (commandes et résultats), ce qui reste, et tout piège rencontré. Le prochain agent commence par la lire. - Le contrat ne bouge pas : un lot qui a besoin d'un champ de
app:ou d'une fonction de@recit/appqui n'existe pas l'écrit dans le journal et s'arrête, il ne modifie pas APP0 ni APP1.
Les vagues :
| Vague | Lots en parallèle | Condition d'entrée | Machine |
|---|---|---|---|
| 0 | APP0 | — | ici |
| 1 | APP1, APP4 | APP0 vérifié (vitest core + site-config.spec.ts, typecheck) | ici |
| 2 | APP2, APP3 | APP1 vérifié | ici |
| 3 | APP5, ou APP6 | APP2 vérifié ; APP5 si rustup et la ligne apt sont là, sinon APP6 d'emblée | toolchain, ou CI |
| 4 | APP6, APP9, APP10 | une coquille compilée (APP5 fait ici, ou le premier run vert de APP6) | CI / toolchain |
| 5 | APP7, APP8 | APP6 vert | CI / poste équipé |
Si la chaîne Rust n'est pas installée à la vague 3, APP6 compile la coquille pour la première fois sur le runner, et APP5 se réduit à lire les journaux du run et à corriger la coquille ; la vague 4 ne contient plus que APP9 et APP10.
7. Tests et vérification
| Niveau | Quoi | Où | Rust |
|---|---|---|---|
| Unitaire | schéma, défauts, diagnostics de app: | packages/core/src/__tests__/app-config.spec.ts | non |
| Unitaire | tauriConfig par instantané, materializeShell, serveLikeTauri | packages/tools/app/src/__tests__/ | non |
| Unitaire | enchaînement de recit app … avec @recit/app injecté | packages/cli/src/__tests__/app.spec.ts | non |
| e2e | bundle servi comme Tauri : navigation, rechargement profond, recherche, 404, aucune requête externe | e2e/site-app-bundle.spec.ts | non |
| Compilation | cargo check de la coquille matérialisée | APP5, puis chaque job de APP6 | oui |
| Fumée | le binaire démarre et quitte, sur trois systèmes | APP6 | oui |
| Manuel | liens externes, window.open, mode avion sur mobile | APP5, APP7, APP8, notés dans le journal | oui |
pnpm verify reste la porte : APP0 à APP4 ne la ralentissent que de leurs tests unitaires ; aucun
lot n'ajoute Rust à pnpm verify ni à build.yaml. La compilation native vit dans app.yaml,
lancé à la main.
8. Version plan
@recit/app devient un paquet publié : il entre dans release.projects de nx.json (vérifié par
scripts/__tests__/release-workflow.spec.ts) et prend un version plan. Tous les paquets partagent
une version (projectsRelationship: "fixed") ; en 0.x seul preminor (première release d'une
phase) ou prerelease est accepté (CONTRIBUTING.md §5, scripts/check-version-plans.js). Ce plan
ne crée pas de phase : .nx/version-plans/version-plan-app.md en prerelease, pour
@recit/core (la section app:), @recit/cli (recit app) et @recit/app, écrit par APP1. APP10
ajoute un second plan prerelease si APP1 est déjà publié.
9. Risques
| Risque | Parade |
|---|---|
Pagefind sous le protocole tauri:// : fetch du .wasm et des fragments depuis un schéma personnalisé | APP3 le prouve sous serveLikeTauri (HTTP) ; APP5 le vérifie sous le vrai protocole sur Linux (WebKitGTK) et APP6 sur macOS (WKWebView) et Windows (WebView2) ; en dernier recours, un serveur local embarqué (tauri-plugin-localhost), qui coûte une dépendance |
window.open et target="_blank" hors de on_navigation | script d'initialisation (§4.6) ; test manuel APP5.3 |
Un site avec basePath ou cdnUrl | refusé avec diagnostic dès APP0 ; un support ultérieur passerait par un build du site avec basePath forcé à "" |
| Poids des binaires (Mermaid, Shiki, KaTeX, polices) | mesuré en APP5.2 et APP7.4, noté au journal ; les langages Shiki sont déjà une liste par site (site-kit/src/config.tsx:111-123) |
@tauri-apps/cli : binaire natif par plateforme, lourd | dépendance de @recit/app seulement, chargé à la demande par recit ; pnpm-workspace.yaml l'épingle |
| Cache Cargo (gigaoctets) dans chaque site | RECIT_APP_TARGET_DIR (§4.3), ignoré par git |
| Pas d'écran, pas de Rust sur la machine de travail | §3 et l'ordre des vagues ; APP6 porte la compilation sur GitHub |
Les sites Récit renvoient entre eux par des URL absolues (siteUrl) | ouverts dans le navigateur par la garde ; une version « tout en un » (plusieurs sites dans une app) n'est pas prévue |
| Stores : identifiants figés, comptes, revues | identifier documenté comme définitif ; question ouverte Q2 |
10. Questions ouvertes
- Quel site en premier ? Proposé :
examples/site-embedpour les tests,sites/docspour la première application distribuée (une doc hors ligne a un sens).plansest interne. - Les stores. Compte Apple Developer et Google Play : ceux de Récit ? Pour quels sites ? La
recommandation de
sass(Q12) tient : le mobile à la demande, le site est déjà responsive. Décider avant APP7. - Le nom du paquet :
@recit/app(proposé) ou@recit/native; le dossier généré.recit/app/. - La chaîne Rust ici. La ligne apt de §3 demande sudo : l'installer sur cette machine, ou réserver la compilation à la CI (APP6) ?
- Place dans la feuille de route. Proposé : APP0 à APP6 pendant P5 et P6 en
prerelease; le mobile (APP7, APP8) après la 1.0 sauf demande.06-roadmap.mdreçoit une ligne en P5 (APP4). - Une app par site, ou une app Récit qui embarque plusieurs sites ? Ce plan fait une app par
site, comme
sass; l'autre forme demanderait un routeur multi-sites qui n'existe pas.