JSX intégré
bext inclut un runtime JSX intégré qui compile JSX en chaînes HTML. Il n'y a pas de DOM virtuel, pas de différence, pas de réconciliation. La fonction h() concatène les chaînes — la même chose que vous feriez avec les littéraux de modèle, mais avec la syntaxe JSX.
// This JSX:
<div className="card">
<h2>{title}</h2>
<p>{description}</p>
</div>
// Becomes this string:
// <div class="card"><h2>My Title</h2><p>My description</p></div>
Comment ça marche#
Le runtime JSX de bext exporte h() (hyperscript), jsx, jsxs et Fragment. Le mode react-jsx de TypeScript appelle jsx / jsxs ; les appelants directs peuvent utiliser h(). Au lieu de créer des nœuds DOM virtuels, les éléments intrinsèques renvoient SafeHtml, une sous-classe de marque String dont la valeur primitive est le fil HTML.
- Les composants de fonction reçoivent des accessoires et renvoient une chaîne
- Les balises HTML sont rendues sous forme de concaténation de chaînes
- Les enfants sont aplatis et joints
- Il n'y a pas de cycle de vie des composants, pas d'état, pas de hooks
Le runtime automatique réutilise l'objet props créé par le compilateur pour les éléments intrinsèques, car formatAttrs ignore déjà children. Pour les composants, il contourne la normalisation uniquement lorsque les enfants sont absents, scalaires ou dans un tableau dense déjà plat ; Les tableaux imbriqués, les valeurs nulles et les booléens conservent la sémantique d'aplatissement héritée.
Cela le rend idéal pour les pages rendues par le serveur où vous souhaitez bénéficier de l'ergonomie de JSX sans envoyer de code de framework au navigateur.
Configuration de tsconfig.json#
Configurez TypeScript pour utiliser le runtime JSX de bext :
{
"compilerOptions": {
"jsx": "react-jsx",
"jsxImportSource": "@bext-stack/framework",
"paths": {
"@bext-stack/framework/*": ["../shared/framework/*"],
"@bext-stack/framework": ["../shared/framework/src/index.ts"]
}
}
}
Avec cette configuration, TypeScript importe automatiquement le runtime JSX à partir de @bext-stack/framework/jsx-runtime. Aucune importation manuelle nécessaire.
Exemples de composants#
Composant simple#
function Header({ title, subtitle }: { title: string; subtitle?: string }) {
return (
<header>
<h1>{title}</h1>
{subtitle && <p className="subtitle">{subtitle}</p>}
</header>
);
}
// Returns: <header><h1>My Site</h1><p class="subtitle">Welcome</p></header>
const html = <Header title="My Site" subtitle="Welcome" />;
Composant de carte#
interface CardProps {
title: string;
body: string;
href?: string;
children?: string[];
}
function Card({ title, body, href, children }: CardProps) {
const Tag = href ? "a" : "div";
return (
<Tag className="card" href={href}>
<h3>{title}</h3>
<p>{body}</p>
{children}
</Tag>
);
}
const html = <Card title="Feature" body="Lightning fast" href="/features" />;
// <a class="card" href="/features"><h3>Feature</h3><p>Lightning fast</p></a>
Composant de mise en page#
function Layout({ title, children }: { title: string; children?: string[] }) {
return (
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>{title}</title>
<link rel="stylesheet" href="/styles.css" />
</head>
<body>
<nav>
<a href="/">Home</a>
<a href="/about">About</a>
</nav>
<main>{children}</main>
<footer>© 2026</footer>
</body>
</html>
);
}
Navigation avec état actif#
interface NavItem {
href: string;
label: string;
}
function Nav({ items, currentPath }: { items: NavItem[]; currentPath: string }) {
return (
<nav className="sidebar">
{items.map((item) => (
<a
href={item.href}
className={currentPath === item.href ? "active" : ""}
>
{item.label}
</a>
))}
</nav>
);
}
Mélange avec des chaînes de modèles#
bext JSX renvoie des chaînes simples, vous pouvez donc librement mélanger JSX et les littéraux de modèle :
import { h } from "@bext-stack/framework";
function renderPage(page: Page, ctx: RenderContext): string {
const nav = (
<nav>
<a href="/">Home</a>
<a href="/docs">Docs</a>
</nav>
);
// Mix JSX output with template strings
return `<!DOCTYPE html>
<html lang="en">
<head>
<title>${page.title}</title>
</head>
<body>
${nav}
<main>${page.html}</main>
<footer>Built with bext</footer>
</body>
</html>`;
}
Ceci est utile lorsque votre shell HTML externe est statique et que vous souhaitez uniquement JSX pour les composants dynamiques.
Prise en charge des fragments#
Utilisez Fragment (ou le raccourci <>...</>) pour renvoyer plusieurs éléments sans wrapper :
import { Fragment } from "@bext-stack/framework";
function MetaTags({ title, description }: { title: string; description: string }) {
return (
<>
<title>{title}</title>
<meta name="description" content={description} />
<meta property="og:title" content={title} />
</>
);
}
// Returns: <title>Hello</title><meta name="description" content="World"><meta property="og:title" content="Hello">
dangereusementSetInnerHTML#
Pour injecter du HTML brut sans s'échapper (pour une sortie markdown, un contenu utilisateur nettoyé, etc.) :
function MarkdownContent({ html }: { html: string }) {
return (
<article
className="prose"
dangerouslySetInnerHTML={{ __html: html }}
/>
);
}
// The html string is inserted directly without escaping
const output = <MarkdownContent html="<h1>Hello</h1><p>World</p>" />;
// <article class="prose"><h1>Hello</h1><p>World</p></article>
Avertissement : Utilisez uniquement
dangerouslySetInnerHTMLavec du contenu fiable. Ne transmettez jamais d’entrées utilisateur non nettoyées.
Objets de style#
Transmettez un objet JavaScript à style et il sera converti en chaîne CSS. Les propriétés camelCase sont converties en kebab-case :
function Badge({ color, label }: { color: string; label: string }) {
return (
<span
style={{
backgroundColor: color,
padding: "2px 8px",
borderRadius: "4px",
fontSize: "12px",
fontWeight: 600,
}}
>
{label}
</span>
);
}
// <span style="background-color:red;padding:2px 8px;border-radius:4px;font-size:12px;font-weight:600">New</span>
className vers l'alias de classe#
Le runtime JSX de bext convertit automatiquement les noms d'attributs de style React en leurs équivalents HTML :
| Attribut JSX | Sortie HTML |
|---|---|
className |
class |
htmlFor |
for |
httpEquiv |
http-equiv |
tabIndex |
tabindex |
crossOrigin |
crossorigin |
autoComplete |
autocomplete |
autoFocus |
autofocus |
<label htmlFor="email" className="form-label">Email</label>
// <label for="email" class="form-label">Email</label>
<meta httpEquiv="refresh" content="5" />
// <meta http-equiv="refresh" content="5">
Attributs booléens#
La valeur booléenne true restitue l'attribut sans valeur. false ou null l'omet :
<input type="checkbox" checked={true} disabled={false} />
// <input type="checkbox" checked>
<details open={isOpen}>
<summary>Click me</summary>
<p>Content</p>
</details>
Éléments vides#
Les éléments HTML à fermeture automatique ( <img>, <br>, <input>, <meta>, <link>, etc.) sont rendus correctement sans balises de fermeture :
<img src="/photo.jpg" alt="A photo" />
// <img src="/photo.jpg" alt="A photo">
<br />
// <br>
<link rel="stylesheet" href="/styles.css" />
// <link rel="stylesheet" href="/styles.css">
Comparaison avec React#
| Fonctionnalité | bext JSX | React |
|---|---|---|
| Sortir | Chaîne HTML | DOM virtuel |
| Taille du paquet | 0 Ko (chaînes) | ~140 Ko ( react + react -dom) |
| Gestion de l'État | Aucun (serveur uniquement) | useState, useReducer |
| Crochets de cycle de vie | Aucun | useEffect, useMemo, etc. |
| Hydratation des clients | Aucun (zéro JS) | Hydratation complète requise |
| Vitesse de rendu (appareil de 38,3 Ko) | 69,3 µs direct / 75,3 µs automatique | 1 823 µs |
| Cas d'utilisation | Pages rendues par le serveur | Applications client interactives |
Utiliser bext JSX lorsque :
- Vos pages sont rendues par le serveur sans interactivité client
- Vous souhaitez que zéro JavaScript soit envoyé au navigateur
- Vous avez besoin de performances de rendu maximales (au niveau de la microseconde)
- Vous créez des sites de contenu, des documents, des blogs, des pages marketing
Utiliser React lorsque :
- Vous avez besoin d'interactivité côté client (formulaires, animations, mises à jour en temps réel)
- Vous disposez d'une bibliothèque de composants React existante.
- Vous avez besoin de hooks, de contexte ou de gestion d'état
Vous pouvez également mélanger les deux : voir React, Preact et solide pour savoir comment intégrer des composants React dans les mises en page JSX bext.
En rapport#
- Cadre PRISM — le framework de serveur construit sur ce runtime JSX
- Passe de compilation PRISM — comment les appels JSX sont repliés en concats de chaînes au moment de la construction
- Architecture des îles — envoi de composants côté client aux côtés du serveur JSX
- Signaux — îlots réactifs sans React, utilisant la même syntaxe JSX
- CSS vent arrière — générer du CSS à partir de l'utilisation de JSX
className
Échappement HTML : ce qui est échappé et ce qui ne l'est pas#
Il s'agit de la note de sécurité la plus importante,, de ce document. Le texte et les attributs dynamiques sont sécurisés par défaut ; Le HTML brut reste un opt-in explicite.
| Position | Échappé? | Exemple |
|---|---|---|
| Texte statique dans JSX | Oui (temps de compilation) | <p>5 < 10</p> → <p>5 < 10</p> |
| Valeurs d'attribut | Oui (environnement d'exécution formatAttrs ) |
<a title={user.bio}> |
Enfants de chaîne d'expression {value} |
Oui (assistant d'exécution ou de compilation) | <p>{userInput}</p> |
| JSX rendu imbriqué | Transmis via SafeHtml |
<div>{<strong>safe</strong>}</div> |
dangerouslySetInnerHTML |
Non (intentionnel) | comme dans React |
Lorsque vous écrivez <p>{userInput}</p>, une chaîne simple passe par escapeHtml. Lorsque l'enfant est sorti d'un autre élément PRISM, il transporte le signal hors bande.
Marque SafeHtml et traverse sans double fuite. Les bundles compilés hérités qui portent toujours le marqueur réservé \x01 restent pris en charge, mais la nouvelle sortie du runtime ne met pas ce marqueur sur le fil.
Le texte dynamique normal ne nécessite aucun wrapper :
<p>{userInput}</p>
<input value={userInput} readOnly />
Si vous avez besoin d'une sortie HTML brute (markdown, texte riche épuré), utilisez
dangerouslySetInnerHTML={{ __html: ... }} exactement comme React. Le nom de l'accessoire est l'avertissement : utilisez-le uniquement avec du contenu fiable ou nettoyé.
dangerouslySetInnerHTML contourne la fuite. Transmettez uniquement du HTML fiable ou nettoyé séparément à __html ; ne l'utilisez pas pour une saisie utilisateur ordinaire.
escapeHtml est toujours exporté pour les contextes non-JSX et pour les appels d'aide à la compilation. Il utilise des expressions régulières sur des chaînes sans &<>". Les attributs de classe ajoutent un cache de valeurs exactes limité à deux entrées pour les lignes répétées du tableau ; le cache stocke le résultat déjà échappé, de sorte qu'une valeur malveillante répétée reste échappée.