Pont d'E/S natif
Le runtime V8 de bext fournit des capacités d'E/S natives via des fonctions de pont global. Ceux-ci sont enregistrés par le serveur Rust avant que votre bundle ne soit évalué, donnant à votre code JavaScript un accès synchrone au système de fichiers, HTTP, SQLite, aux variables d'environnement et au système de journalisation.
Pourquoi un pont ?#
V8 est un moteur JavaScript en bac à sable : il n'a pas de système de fichiers, de réseau ou d'accès à la base de données intégré. Les fonctions de pont sont des globales implémentées par Rust que bext injecte dans l'isolat V8. Ils sont synchrones car les agents de rendu traitent une requête à la fois (le parallélisme vient du pool de travailleurs, et non des E/S asynchrones).
┌─────────────────────────────────────────────┐
│ Your JavaScript (V8 Worker) │
│ │
│ __readFile("./data.json") │
│ │ │
│ ▼ │
│ Rust Bridge (native function) │
│ │ │
│ ▼ │
│ Kernel (actual file read) │
│ │ │
│ ▼ │
│ Returns UTF-8 string to JS │
└─────────────────────────────────────────────┘
Globales brutes#
Ceux-ci sont disponibles directement sur globalThis dans n'importe quel bundle bext V8 :
__readFile (chemin)#
Lit le contenu d'un fichier sous forme de chaîne UTF-8. Lance si le fichier n'existe pas.
const content = __readFile("./data/products.json");
const products = JSON.parse(content);
__readFileExists (chemin)#
Vérifiez si un fichier ou un répertoire existe. Renvoie true ou false.
if (__readFileExists("./data/cache.json")) {
const cached = JSON.parse(__readFile("./data/cache.json"));
}
__readDir(chemin)#
Répertoriez le contenu du répertoire. Renvoie un tableau de noms de fichiers codés en JSON.
const files: string[] = JSON.parse(__readDir("./content/posts"));
// ["2026-01-hello.md", "2026-02-world.md"]
__httpFetch(url, optsJson ?)#
Récupération HTTP synchrone. Renvoie un objet de réponse codé en JSON.
const response = JSON.parse(__httpFetch("https://api.example.com/data"));
// { ok: true, status: 200, body: "{...}" }
// With options
const response = JSON.parse(__httpFetch("https://api.example.com/data", JSON.stringify({
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ key: "value" }),
timeout_ms: 5000,
})));
__dbQuery (dbPath, sql, paramsJson ?)#
Exécutez une requête SQLite. Renvoie JSON avec des lignes (pour SELECT) ou des modifications (pour INSERT/UPDATE/DELETE).
// SELECT query
const result = JSON.parse(__dbQuery("./data.db", "SELECT * FROM products WHERE active = ?", "[true]"));
// { rows: [{ id: 1, name: "Widget", active: true }], columns: ["id", "name", "active"] }
// INSERT query
const result = JSON.parse(__dbQuery("./data.db", "INSERT INTO products (name) VALUES (?)", '["New Product"]'));
// { changes: 1, last_insert_rowid: 42 }
__environnement (clé)#
Lire une variable d'environnement. Renvoie la valeur sous forme de chaîne, ou null si elle n'est pas définie.
const apiKey = __env("API_KEY");
const nodeEnv = __env("NODE_ENV"); // null if not set
Journal __(niveau, msg)#
Enregistrez un message via le système de traçage de Rust. Les messages apparaissent dans la sortie du journal structuré de bext.
__log("info", "Processing request for /api/products");
__log("error", "Failed to connect to database");
__log("debug", "Cache miss for key: products:123");
__log("warn", "Rate limit approaching for client 192.168.1.1");
Accès tapé via @ bext -stack/framework/bridge#
Le module @bext-stack/framework/bridge encapsule les globales brutes avec les types TypeScript et l'analyse JSON :
import {
readFile,
fileExists,
readDir,
httpFetch,
dbQuery,
env,
log,
} from "@bext-stack/framework/bridge";
readFichier (chemin)#
const content: string = readFile("./data/config.json");
fichierExiste(chemin)#
const exists: boolean = fileExists("./data/cache.json");
readDir(chemin)#
Renvoie un string[] analysé au lieu du JSON brut :
const files: string[] = readDir("./content/posts");
files.forEach((file) => {
const content = readFile(`./content/posts/${file}`);
// process each file
});
httpFetch(url, opte ?)#
Renvoie un objet de réponse analysé :
const response = httpFetch("https://api.github.com/repos/benfavre/bext", {
headers: { "Accept": "application/json" },
timeout_ms: 3000,
});
if (response.ok) {
const repo = JSON.parse(response.body);
console.log(repo.stargazers_count);
} else {
log("error", `GitHub API error: ${response.status}`);
}
Type d'options complètes :
interface FetchOptions {
method?: string; // GET, POST, PUT, DELETE, etc.
headers?: Record<string, string>; // Request headers
body?: string; // Request body
timeout_ms?: number; // Timeout in milliseconds
}
interface FetchResponse {
ok: boolean; // true if status 200-299
status: number; // HTTP status code
body: string; // Response body as string
error?: string; // Error message if request failed
}
dbQuery (dbPath, sql, paramètres ?)#
Renvoie un objet de résultat analysé :
// SELECT
const { rows, columns } = dbQuery("./app.db", "SELECT * FROM users WHERE role = ?", ["admin"]);
// rows: [{ id: 1, name: "Alice", role: "admin" }, ...]
// columns: ["id", "name", "role"]
// INSERT
const { changes, last_insert_rowid } = dbQuery(
"./app.db",
"INSERT INTO users (name, role) VALUES (?, ?)",
["Bob", "user"]
);
// changes: 1, last_insert_rowid: 5
// UPDATE
const { changes } = dbQuery("./app.db", "UPDATE users SET role = ? WHERE id = ?", ["admin", 5]);
// changes: 1
// CREATE TABLE
dbQuery("./app.db", `
CREATE TABLE IF NOT EXISTS products (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
price REAL NOT NULL,
created_at TEXT DEFAULT CURRENT_TIMESTAMP
)
`);
env(clé)#
const apiKey: string | null = env("API_KEY");
const port = env("PORT") ?? "3000";
journal (niveau, msg)#
log("info", "Server initialized");
log("warn", `Slow query: ${elapsed}ms`);
log("error", `Database connection failed: ${err.message}`);
log("debug", `Cache lookup: key=${key}, hit=${!!cached}`);
Niveaux valides : "debug", "info", "warn", "error".
Cale de nœud fs#
Le module @bext-stack/framework/fs fournit un sous-ensemble compatible Node.js de fs et path à l'aide des globaux de pont. Ceci est utile lorsque vous avez du code existant qui importe depuis "fs" :
import { readFileSync, readdirSync, existsSync, join } from "@bext-stack/framework/fs";
// Same API as Node's fs module
const content = readFileSync("./data/config.json");
const files = readdirSync("./content/posts");
const exists = existsSync("./data/cache.json");
const fullPath = join("./content", "posts", "hello.md");
Aliasing dans votre build#
Vous pouvez créer un alias "fs" et "path" vers le cale bext dans votre configuration de build Bun afin que les bibliothèques existantes fonctionnent sans modification :
// build.ts
await Bun.build({
entrypoints: ["server/entry.ts"],
outdir: "dist",
plugins: [{
name: "fs-shim",
setup(build) {
build.onResolve({ filter: /^(fs|path)$/ }, () => ({
path: require.resolve("@bext-stack/framework/fs"),
}));
},
}],
});
Fonctions disponibles#
| Fonction | Description |
|---|---|
readFileSync(path) |
Lire le contenu du fichier sous forme de chaîne UTF-8 |
readdirSync(path) |
Répertorier le contenu du répertoire sous forme de tableau de chaînes |
existsSync(path) |
Vérifiez si le chemin existe |
join(...parts) |
Joindre des segments de chemin avec / |
Les fonctions de pont ( __readFile, __dbQuery, etc.) font et non les chemins de fichier sandbox eux-mêmes - elles sont résolues par rapport au répertoire de travail du processus. Validez toujours les segments de chemin fournis par l'utilisateur avant de les transmettre aux fonctions de pont pour empêcher la traversée du chemin.
Sécurité : protection contre la traversée du chemin#
Les fonctions de pont Rust de bext n'effectuent pas elles-mêmes le sandboxing des chemins - les chemins de fichiers sont résolus par rapport au répertoire de travail du processus. Cependant, le WAF (Web Application Firewall) de bext bloque les attaques par traversée de chemin au niveau de la couche HTTP avant que les requêtes n'atteignent votre JavaScript :
- Les URL contenant
../ou..\\sont rejetées avec 403 - Les octets nuls dans les chemins sont rejetés
- Le serveur de fichiers statique nettoie indépendamment les chemins
Pour vos propres gestionnaires de routes, validez les chemins fournis par l'utilisateur avant de les transmettre aux fonctions de pont :
import { readFile, fileExists } from "@bext-stack/framework/bridge";
function loadDocument(slug: string): string | null {
// Validate: only allow alphanumeric, hyphens, and forward slashes
if (!/^[a-zA-Z0-9\-\/]+$/.test(slug)) return null;
// Prevent traversal
if (slug.includes("..")) return null;
const path = `./content/docs/${slug}.md`;
if (!fileExists(path)) return null;
return readFile(path);
}
Exemple complet : blog avec SQLite#
Un blog qui lit les articles des fichiers markdown et stocke le nombre de vues dans SQLite :
import { createSite } from "@bext-stack/framework/entry";
import { json } from "@bext-stack/framework";
import { readFile, readDir, fileExists, dbQuery, log } from "@bext-stack/framework/bridge";
import type { RouteHandler, Page } from "@bext-stack/framework";
// Initialize database
dbQuery("./blog.db", `
CREATE TABLE IF NOT EXISTS views (
path TEXT PRIMARY KEY,
count INTEGER DEFAULT 0
)
`);
// Load all posts from markdown files
function loadPosts(): Page[] {
const files = readDir("./content/posts").filter((f) => f.endsWith(".md"));
return files.map((file) => {
const raw = readFile(`./content/posts/${file}`);
const title = raw.match(/^#\s+(.+)/m)?.[1] ?? file;
return {
title,
description: "",
html: markdownToHtml(raw),
};
});
}
// Track page views
const trackViews: RouteHandler = (ctx) => {
if (ctx.request.method !== "GET") return null;
if (ctx.path.startsWith("/api/")) return null;
dbQuery("./blog.db",
"INSERT INTO views (path, count) VALUES (?, 1) ON CONFLICT(path) DO UPDATE SET count = count + 1",
[ctx.path]
);
return null; // Continue to next handler
};
// View count API
const viewsApi: RouteHandler = (ctx) => {
if (ctx.path !== "/api/views") return null;
const { rows } = dbQuery("./blog.db", "SELECT path, count FROM views ORDER BY count DESC LIMIT 20");
return json(rows);
};
createSite({
pages: { getPage: (path) => /* ... */, getPagePaths: () => /* ... */ },
template: { renderPage: (page, ctx) => /* ... */ },
hostname: "blog.bext.dev",
routes: [trackViews, viewsApi],
});
log("info", "Blog initialized with SQLite view tracking");
En rapport#
- Cadre de modèle —
createSite()et gestionnaires d'itinéraires qui appellent des fonctions de pont - Cadre PRISM — Les pages PRISM peuvent appeler des ponts globaux directement pendant SSR
- Conventions de données PRISM — modèle de chargeur/action pour récupérer les données avant le rendu
- Piscine de rendu V8 — les fonctions de pont de pool d'isolation sont injectées dans
- Capacités : Stockage — SDK de stockage KV/objet de niveau supérieur au-delà du SQLite brut