Des articles approfondis sur les technologies qui façonnent l'avenir.

Pourquoi Node.js a besoin d'un système de fichiers virtuel

Node.js est étroitement lié au système de fichiers réel. Une couche VFS faciliterait les tests, le sandboxing et le déploiement en edge.

Bras mécaniques saisissant des dossiers en papier, tandis qu'un dossier holographique fantomatique flotte à côté

Essayez de faire tourner une application Node.js sans système de fichiers réel et regardez à quelle vitesse tout s'effondre. require() lit des fichiers. fs.readFile() lit des fichiers. Les moteurs de templates lisent des fichiers. Les chargeurs de configuration lisent des fichiers. Les bibliothèques de logging écrivent des fichiers. Tout l'écosystème Node.js part du principe qu'un système de fichiers de type POSIX existe, est accessible en écriture et constitue le moyen principal d'accéder au code et aux données.

Cela allait très bien en 2010, quand Node.js tournait sur des serveurs avec des disques locaux. Ça pose de plus en plus problème aujourd'hui, alors qu'on veut exécuter du JavaScript dans des environnements où le système de fichiers n'existe pas, est en lecture seule ou ne doit pas être considéré comme fiable : runtimes edge, bacs à sable WebAssembly, fonctions serverless et environnements de développement dans le navigateur.

Là où l'hypothèse du système de fichiers se brise

Runtimes edge. Cloudflare Workers, Deno Deploy et Vercel Edge Functions exécutent du JavaScript en edge, sur des machines proches de l'utilisateur, avec une infrastructure minimale. Ces environnements ne fournissent souvent pas de système de fichiers accessible en écriture, ou seulement un système en mémoire limité qui ne persiste pas entre les requêtes. Les modules Node.js qui appellent fs.writeFileSync plantent. Ceux qui utilisent fs.existsSync pour vérifier la présence de fichiers de configuration se comportent de manière imprévisible.

WebAssembly. Faire tourner Node.js dans un bac à sable WebAssembly impose de faire correspondre les opérations sur le système de fichiers au système de fichiers virtuel du runtime Wasm (généralement WASI). Cette correspondance est imparfaite : les permissions de fichiers, les liens symboliques et les chemins propres à chaque plateforme ne se traduisent pas proprement. Une abstraction VFS rendrait cette correspondance explicite, au lieu de la laisser ad hoc.

Tests. Tester unitairement du code qui lit des fichiers de configuration, charge des templates ou écrit des logs impose soit de mocker le module fs (fragile, car les bibliothèques utilisent des API fs différentes), soit de créer des répertoires temporaires avec des fixtures (lent, instable, et qui laisse des déchets sur le disque). Un système de fichiers virtuel permettrait d'exécuter les tests entièrement en mémoire, de manière déterministe, sans toucher au système de fichiers réel.

Isolation de sécurité. Quand on exécute du code non fiable (plugins, scripts utilisateur, étapes de build CI/CD), on veut contrôler l'accès au système de fichiers. Aujourd'hui, cela demande une isolation au niveau du système d'exploitation (conteneurs, namespaces) ou du monkey-patching du module fs. Un VFS permettrait des politiques fines : ce code peut lire /app mais pas /etc, il peut écrire dans /tmp mais pas dans /app.

À quoi ressemblerait un VFS

Un système de fichiers virtuel n'est pas un concept nouveau. Les systèmes d'exploitation utilisent des couches VFS depuis les années 1980 : le VFS de Linux permet à ext4, NFS et procfs de coexister derrière la même API. L'idée pour Node.js est la même : découpler l'interface du système de fichiers (fs.readFile, require(), etc.) de son implémentation.

// Hypothetical VFS API
import { createVFS, MemoryFS, ReadOnlyFS, OverlayFS } from 'node:vfs';
// In-memory filesystem for testing
const testFS = new MemoryFS({
'/app/config.json': '{"port": 3000}',
'/app/templates/index.html': '<h1>Hello</h1>',
});
// Read-only view of the real filesystem
const readOnly = new ReadOnlyFS('/');
// Overlay: reads from real FS, writes go to memory
const sandbox = new OverlayFS(readOnly, new MemoryFS());
// Run code with a specific filesystem
const vfs = createVFS(sandbox);
vfs.run(() => {
// Inside this context:
// fs.readFileSync('/etc/hosts') → reads real file
// fs.writeFileSync('/tmp/log.txt') → writes to memory overlay
// require('./module') → resolves against the VFS
const app = require('./app');
app.start();
});

Le point clé est que le VFS intercepte tous les accès au système de fichiers, pas seulement les appels explicites à fs, mais aussi require(), import(), __dirname et process.cwd(). C'est ce qui le rend utile : on peut rediriger tous les accès au système de fichiers d'une application sans modifier l'application elle-même.

Le problème de la résolution des modules

L'imbrication la plus profonde entre Node.js et le système de fichiers concerne la résolution des modules. Quand on écrit require('express'), Node.js remonte l'arborescence des répertoires à la recherche de dossiers node_modules/express, lit des fichiers package.json, résout les liens symboliques et suit le champ main ou exports. Ce processus effectue des dizaines d'appels au système de fichiers pour un seul require.

Yarn PnP (Plug'n'Play) a partiellement résolu ce problème en remplaçant node_modules par un fichier manifeste qui associe les noms de modules à des archives zip. C'est plus rapide (pas de parcours de répertoires) et plus déterministe (pas de surprises liées au hoisting), mais cela a nécessité de faire du monkey-patching sur la résolution de modules de Node, ce qui a cassé des outils qui supposaient que node_modules existait.

Un VFS bien conçu ferait de l'approche de Yarn PnP une fonctionnalité de premier plan. La résolution des modules passerait par le VFS, qui pourrait implémenter n'importe quelle correspondance : archives zip, modules en mémoire, URL distantes, ou le parcours traditionnel du répertoire node_modules. Des stratégies différentes selon les environnements, la même API pour le code applicatif.

Les précédents

D'autres runtimes ont déjà résolu des variantes de ce problème.

  • Deno charge les modules depuis des URL par défaut et les met en cache dans un répertoire géré. Il n'y a ni node_modules ni résolution de modules basée sur le système de fichiers. Le runtime contrôle où et comment les modules sont stockés.
  • Bun utilise un cache global de modules avec des liens physiques (hardlinks), ce qui réduit la charge sur le système de fichiers. Sa résolution de modules est fortement optimisée par rapport à celle de Node.js.
  • L'interface io/fs de Go (ajoutée dans Go 1.16) définit une interface de système de fichiers implémentée par le système de fichiers du système d'exploitation, les fichiers embarqués (embed.FS), les archives zip et les systèmes en mémoire. Les fonctions de la bibliothèque standard acceptent l'interface fs.FS, ce qui les rend testables sans fichiers réels.
  • L'abstraction FileSystem de NIO en Java prend en charge des fournisseurs de système de fichiers enfichables, y compris des implémentations en mémoire pour les tests et des systèmes de fichiers basés sur zip.
  • Le modèle IFileSystem de .NET est largement utilisé dans les applications .NET pour la testabilité, même s'il s'agit d'une convention de la communauté plutôt que d'une fonctionnalité du runtime.

L'approche de Go est particulièrement instructive. En définissant une petite interface (Open, Read, Stat) et en l'utilisant dans toute la bibliothèque standard, Go a rendu l'abstraction du système de fichiers très simple, sans casser le code existant. Node.js pourrait faire de même en définissant une interface VFS, puis en migrant progressivement la bibliothèque standard et le chargeur de modules vers celle-ci.

Le défi de la compatibilité

Le principal obstacle à un VFS pour Node.js n'est pas l'implémentation, c'est l'écosystème. npm compte plus de deux millions de paquets, et une part importante d'entre eux accède au système de fichiers en supposant une sémantique POSIX, des chemins réels et des répertoires accessibles en écriture.

Un VFS qui casserait les paquets existants serait mort-né. Il doit être opt-in et rétrocompatible : si l'on ne crée pas explicitement de VFS, tout fonctionne exactement comme aujourd'hui. Lorsqu'on en crée un, il doit intercepter de manière transparente les appels au système de fichiers, afin que les paquets bien conçus fonctionnent sans modification.

« Bien conçu » est le mot-clé. Les paquets qui lancent cp ou rm en sous-processus, utilisent des addons natifs qui appellent directement open() de libc, ou dépendent de /proc ou d'autres systèmes de fichiers spécifiques à un OS ne fonctionneront pas avec un VFS. C'est une limite stricte : toute abstraction fuit dès que le code dépasse son périmètre pour atteindre le système sous-jacent.

Ce qui est réellement proposé

La discussion sur un VFS pour Node.js n'est pas théorique : il existe des propositions concrètes dans le gestionnaire de tickets de Node.js. La direction actuelle repose sur quelques idées clés.

D'abord, une interface FileSystemProvider vers laquelle le module fs délègue. Le fournisseur par défaut est le système de fichiers réel. Les fournisseurs personnalisés implémentent la même interface pour des systèmes en mémoire, en lecture seule, en overlay ou distants.

Ensuite, une intégration avec le chargeur de modules. require() et import() résoudraient les modules via le VFS, ce qui permettrait des stratégies de résolution qui ne dépendent pas des répertoires node_modules.

Troisièmement, une couche de politiques. Le VFS pourrait appliquer des contrôles d'accès, en empêchant le code de lire ou d'écrire des chemins en dehors d'un périmètre défini. Node.js disposerait ainsi d'une capacité de sandboxing intégrée, comparable aux options --allow-read et --allow-write de Deno.

Savoir si cela arrivera dans Node.js 24, 26, ou jamais, dépendra de la disponibilité de l'équipe Node.js et de l'appétit de la communauté pour un changement non rétrocompatible dans la manière d'accéder au système de fichiers. Mais la pression est réelle : les runtimes edge se multiplient, les cibles de déploiement WebAssembly aussi, et l'écart entre le « JavaScript partout » et « Node.js sur des serveurs avec un vrai système de fichiers » devient un handicap concurrentiel.

Ce que vous pouvez faire dès aujourd'hui

Inutile d'attendre un VFS dans le runtime pour profiter de l'abstraction du système de fichiers. Pour le nouveau code, encapsulez les accès au système de fichiers derrière une interface. Au lieu d'appeler fs.readFile directement, créez une interface FileStore dont votre code dépend. Implémentez-la avec le système de fichiers réel en production et avec une simple map en mémoire pour les tests. C'est de l'inversion de dépendances classique : pas glamour, efficace et compatible avec n'importe quel runtime.

Pour le code existant, des bibliothèques comme memfs et unionfs fournissent des implémentations en mémoire et en overlay qui patchent le module fs de Node. Elles ne sont pas parfaites (les addons natifs et les processus enfants les contournent), mais elles fonctionnent pour le code purement JavaScript et rendent les tests bien plus simples.

Le système de fichiers est la dernière grande brique d'infrastructure de Node.js qui n'a pas de frontière d'abstraction propre. Les streams ont des interfaces, HTTP aussi. Même le chargeur de modules dispose de hooks. Quand le système de fichiers bénéficiera du même traitement, Node.js deviendra un runtime véritablement portable plutôt qu'un runtime limité aux serveurs. Cela vaut la peine de se battre pour.