ENbext.dev
DocumentationPRISM (natif bext)

Traduction automatique depuis l’anglais. Consulter l’original.

Sur cette page

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).

code
┌─────────────────────────────────────────────┐
│  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.

typescript
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.

typescript
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.

typescript
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.

typescript
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).

typescript
// 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.

typescript
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.

typescript
__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 :

typescript
import {
  readFile,
  fileExists,
  readDir,
  httpFetch,
  dbQuery,
  env,
  log,
} from "@bext-stack/framework/bridge";

readFichier (chemin)#

typescript
const content: string = readFile("./data/config.json");

fichierExiste(chemin)#

typescript
const exists: boolean = fileExists("./data/cache.json");

readDir(chemin)#

Renvoie un string[] analysé au lieu du JSON brut :

typescript
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é :

typescript
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 :

typescript
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é :

typescript
// 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é)#

typescript
const apiKey: string | null = env("API_KEY");
const port = env("PORT") ?? "3000";

journal (niveau, msg)#

typescript
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" :

typescript
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 :

typescript
// 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 /
Attention

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 :

typescript
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 :

typescript
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#

Modifier cette page ↗Besoin d’aide ? ↗
Rechercher dans la documentation

Saisissez un sujet, une commande ou une question.