Créer votre première application

Ce parcours construit une application bext PRISM minimale à partir de zéro : la générer, examiner les deux fichiers de configuration, ajouter un layout racine et une page dont le loader lit la session et appelle le SDK de la plateforme, puis l'exécuter localement avec rechargement à chaud. Quand vous serez prêt à la mettre sur un domaine, poursuivez avec Déployer un site PRISM.

Générer le squelette

Créez une nouvelle application à partir du modèle de démarrage PRISM :

bext new my-app --template @bext/starter-prism
cd my-app
bun install

Le répertoire my-app ne doit pas déjà exister. Cela met en place un petit projet : bext.config.toml, tsconfig.json, package.json, .gitignore, README.md et une arborescence de routes src/app/ avec un layout, une page d'accueil et une route /api/hello. La commande initialise aussi un dépôt git puis affiche les étapes suivantes (bun install, puis bext dev) ; avec --no-post-install, elle saute l'étape git et affiche seulement les étapes suivantes. D'autres modèles de démarrage existent (@bext/starter-blog, -docs, -saas, -api…) : voir la Référence CLI.

Les fichiers de configuration

bext.config.toml

Le modèle de démarrage fournit une configuration minimale. Son cœur (extrait ci-dessous ; le fichier généré contient aussi [build.css] et [compile]) déclare une application PRISM qui surveille ses répertoires source pour le rechargement à chaud et effectue le rendu avec l'ISR :

[server]
app_dir = "."

[framework]
type = "prism"

[build]
watch_dirs = ["src/app", "src/components"]
live_reload = true

[rendering]
mode = "isr"
revalidate = 60

Voir la Référence de configuration pour chaque section.

tsconfig.json

PRISM compile le JSX directement en chaînes de caractères, la configuration TypeScript pointe donc le JSX vers le framework (extrait) :

{
  "compilerOptions": {
    "jsx": "react-jsx",
    "jsxImportSource": "@bext-stack/framework",
    "moduleResolution": "bundler",
    "paths": { "@/*": ["./src/*"] }
  }
}

Le layout racine

Le layout.tsx racine reçoit la page en tant que children. Il peut posséder le document <html>, comme ci-dessous, ou transmettre children tel quel et laisser chaque page rendre son propre document (c'est le choix du modèle de démarrage). Le tsconfig.json ci-dessus pointe déjà le JSX vers le framework ; vous pouvez aussi l'indiquer fichier par fichier avec le pragma :

/** @jsxImportSource @bext-stack/framework */

export default function RootLayout({ children }) {
  return (
    <html lang="en">
      <head>
        <meta charSet="utf-8" />
        <title>My App</title>
      </head>
      <body>{children}</body>
    </html>
  );
}

Un composant est simplement une fonction qui renvoie du HTML sous forme de chaîne (ou une promesse de chaîne) ; aucun JavaScript n'est envoyé au navigateur tant que vous n'ajoutez pas d'island.

Une page avec un loader

Un page.tsx effectue le rendu de la route. Placez le chargement des données dans un loader : le composant de page ne voit jamais request, les cookies et les en-têtes se lisent donc dans le loader. Ici, le loader lit la session et incrémente un compteur dans le magasin KV via le SDK de la plateforme.

La session vient de createSession (point d'entrée @bext-stack/framework/auth), un cookie signé que vous déclarez une fois dans src/lib/session.ts : voir Loaders et actions pour sa création et la connexion.

/** @jsxImportSource @bext-stack/framework */
import { createSdk } from "@bext-stack/framework/sdk";
import { session } from "@/lib/session"; // createSession<{ name: string }>(…)

const sdk = createSdk("my-app");

export async function loader({ request }) {
  const user = session.read(request); // null si absente, altérée ou expirée
  const visits = Number((await sdk.kv.get("visits")) ?? 0) + 1;
  await sdk.kv.set("visits", visits, 3600); // valeur + TTL (secondes)
  return { user, visits };
}

export default function Page({ data }) {
  return (
    <main>
      <h1>Hello{data.user ? `, ${data.user.name}` : ""}</h1>
      <p>Page views: {data.visits}</p>
    </main>
  );
}

La valeur de retour du loader arrive sur la page en tant que props.data. Voir Loaders et actions pour le cycle de vie complet, y compris action pour les mutations, qui s'exécute avant le loader sur POST/PUT/PATCH/DELETE.

Note

createSdk("my-app") connecte votre application au SDK de la plateforme. Le SDK authentifie votre application automatiquement et cantonne toutes les données à son app id ; vous ne transmettez aucun token.

Appeler l'API de la plateforme depuis un loader

Un loader peut aussi appeler l'API de plateforme tRPC. Les requêtes sont en GET (avec une enveloppe d'entrée superjson quand la procédure prend une entrée), et le résultat que vous voulez est imbriqué à result.data.json. Les cookies de votre application ne sont pas envoyés à manage.inklura.fr : authentifiez-vous donc avec un jeton Bearer rangé dans les secrets de votre application. auth.getSession ne prend aucune entrée et demande la permission auth:read :

export async function loader() {
  const token = await sdk.secrets.get("INKLURA_TOKEN");
  const res = await fetch("https://manage.inklura.fr/api/trpc/auth.getSession", {
    headers: { Authorization: `Bearer ${token}` },
  });
  if (!res.ok) return { me: null }; // par exemple 401 si le jeton est invalide ou expiré
  const body = await res.json();
  return { me: body.result.data.json };
}

Le jeton a une date d'expiration. Un jeton invalide ou expiré vaut absence d'identifiants : l'appel s'exécute alors comme un visiteur anonyme, ce qui échoue en général en UNAUTHORIZED (401). Renouvelez le secret quand c'est le cas.

Pour un client autonome complet (@trpc/client typé, pagination et gestion des erreurs), voir Appeler l'API depuis un script.

Exécutez-la

Démarrez le serveur de développement avec rechargement à chaud :

bext dev

Il écoute sur l'interface locale, sur le port de [server] listen (3061 s'il n'est pas défini) ; si ce port est occupé, il en choisit un libre à proximité et l'indique. Le fichier généré contient aussi port = 3000 sous [server], mais le serveur ne lit pas cette clé : pour servir sur 3000, écrivez listen = "127.0.0.1:3000" ou lancez bext dev --port 3000. Options utiles : --port N, --listen host:port (par exemple 0.0.0.0:3000 pour l'exposer sur votre réseau), --open, --no-hmr. Modifiez un fichier sous watch_dirs et le changement est pris en compte en une ou deux secondes. Pour voir les routes découvertes :

bext routes

Une note sur les paquets npm

Attention

Les pages s'exécutent dans un isolate, pas dans Node : les modules natifs comme node:crypto y sont absents ou seulement partiellement simulés, une bibliothèque qui en dépend ne fonctionnera donc pas de façon fiable au moment du rendu. La méthode sûre consiste à vendoriser une build ESM en pur JS sous forme de fichiers .js dans src/ et à l'importer par chemin relatif : voir la Vue d'ensemble du SDK.

Étapes suivantes

Page Ce qu'elle couvre
Vue d'ensemble du SDK Le framework et le SDK de la plateforme
Loaders et actions Chargement des données, mutations, accès à la session
Déployer un site PRISM Mettre l'application sur un domaine avec TLS automatique