Il caso per un File System Virtuale in Node.js
Node.js è strettamente legato al filesystem reale. Un layer di file system virtuale sbloccherebbe testing migliori, sandboxing e deploy sugli edge.

Prova a eseguire un'applicazione Node.js senza un filesystem reale e guarda quanto in fretta va in pezzi. require() legge file. fs.readFile() legge file. I template engine leggono file. I loader di configurazione leggono file. Le librerie di logging scrivono file. L'intero ecosistema Node.js presuppone che esista un filesystem in stile POSIX, scrivibile, e che sia il modo principale per accedere a codice e dati.
Questo andava bene nel 2010, quando Node.js girava su server con dischi locali. Oggi è sempre più problematico, perché vogliamo eseguire JavaScript in posti dove il filesystem non esiste, è di sola lettura o non è affidabile: edge runtime, sandbox WebAssembly, funzioni serverless e ambienti di sviluppo nel browser.
Dove l'assunzione sul filesystem si rompe
Edge runtime. Cloudflare Workers, Deno Deploy e Vercel Edge Functions eseguono JavaScript ai margini della rete, su macchine vicine all'utente, con un'infrastruttura minima. Questi ambienti spesso non offrono un filesystem scrivibile, oppure ne offrono uno limitato in memoria che non persiste tra una richiesta e l'altra. I moduli Node.js che chiamano fs.writeFileSync vanno in crash. I moduli che usano fs.existsSync per controllare la presenza di file di configurazione si comportano in modo imprevedibile.
WebAssembly. Eseguire Node.js dentro una sandbox WebAssembly richiede di mappare le operazioni sul filesystem sul filesystem virtuale del runtime Wasm (tipicamente WASI). Questa mappatura è imperfetta: permessi dei file, symlink e percorsi specifici della piattaforma non si traducono in modo pulito. Un'astrazione VFS renderebbe questa mappatura esplicita invece che improvvisata.
Testing. Testare unitariamente codice che legge file di configurazione, carica template o scrive log richiede di fare il mock del modulo fs (fragile, perché librerie diverse usano API fs diverse) oppure di creare directory temporanee con fixture di test (lento, instabile, lascia spazzatura sul disco). Un filesystem virtuale permetterebbe ai test di girare interamente in memoria, in modo deterministico, senza toccare il filesystem reale.
Sandboxing di sicurezza. Quando esegui codice non fidato (plugin, script utente, step di build CI/CD) vuoi controllare l'accesso al filesystem. Oggi questo richiede sandboxing a livello di sistema operativo (container, namespace) oppure monkey-patching del modulo fs. Un VFS permetterebbe policy di filesystem granulari: questo codice può leggere /app ma non /etc, può scrivere in /tmp ma non in /app.
Come sarebbe un VFS
Un file system virtuale non è un concetto nuovo. I sistemi operativi usano layer VFS dagli anni '80: il VFS di Linux permette a ext4, NFS e procfs di coesistere dietro la stessa API. L'idea per Node.js è la stessa: disaccoppiare l'interfaccia del filesystem (fs.readFile, require(), ecc.) dalla sua implementazione.
// 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();
});
La chiave sta nel fatto che il VFS intercetta tutti gli accessi al filesystem, non solo le chiamate esplicite a fs ma anche require(), import(), __dirname e process.cwd(). È questo che lo rende utile: puoi reindirizzare tutti gli accessi al filesystem di un'applicazione senza modificarla.
Il problema della risoluzione dei moduli
Il legame più profondo tra Node.js e il filesystem è la risoluzione dei moduli. Quando scrivi require('express'), Node.js risale l'albero delle directory cercando le cartelle node_modules/express, legge i file package.json, risolve i symlink e segue il campo main o exports. Questo processo genera decine di chiamate al filesystem per un singolo require.
Yarn PnP (Plug'n'Play) ha risolto in parte il problema sostituendo node_modules con un file manifest che mappa i nomi dei moduli su archivi zip. È più veloce (niente attraversamento delle directory) e più deterministico (niente sorprese dovute all'hoisting), ma ha richiesto il monkey-patching della risoluzione dei moduli di Node, che ha rotto gli strumenti che davano per scontata l'esistenza di node_modules.
Un VFS fatto bene renderebbe l'approccio di Yarn PnP una funzionalità di prima classe. La risoluzione dei moduli passerebbe dal VFS, che potrebbe implementare qualunque mapping: archivi zip, moduli in memoria, URL remoti o il classico attraversamento delle directory node_modules. Strategie diverse per ambienti diversi, stessa API per il codice applicativo.
Lavori precedenti
Altri runtime hanno già risolto varianti di questo problema.
- Deno carica i moduli da URL per impostazione predefinita e li mette in cache in una directory gestita. Non esiste
node_modulesné una risoluzione dei moduli basata sul filesystem. Il runtime controlla dove e come i moduli vengono memorizzati. - Bun usa una cache globale dei moduli con hardlink, riducendo l'overhead sul filesystem. La sua risoluzione dei moduli è molto ottimizzata rispetto a quella di Node.js.
- L'interfaccia
io/fsdi Go (introdotta in Go 1.16) definisce un'interfaccia per il filesystem implementata dal filesystem del sistema operativo, dai file incorporati (embed.FS), dagli archivi zip e dai filesystem in memoria. Le funzioni della libreria standard accettano l'interfacciafs.FS, rendendole testabili senza file reali. - L'astrazione
FileSystemdi Java NIO supporta provider di filesystem intercambiabili, incluse implementazioni in memoria per i test e filesystem basati su zip. - Il pattern
IFileSystemdi .NET è molto diffuso nelle applicazioni .NET per la testabilità, anche se è una convenzione della community più che una funzionalità del runtime.
L'approccio di Go è particolarmente istruttivo. Definendo una piccola interfaccia (Open, Read, Stat) e usandola in tutta la libreria standard, Go ha reso l'astrazione del filesystem banalmente semplice senza rompere il codice esistente. Node.js potrebbe fare lo stesso definendo un'interfaccia VFS e migrando gradualmente la libreria standard e il module loader per usarla.
La sfida della compatibilità
Il principale ostacolo per un VFS in Node.js non è l'implementazione, ma l'ecosistema. npm ha più di due milioni di pacchetti, e una parte significativa di essi accede al filesystem in modi che presuppongono semantica POSIX, percorsi reali e directory scrivibili.
Un VFS che rompe i pacchetti esistenti è morto prima di nascere. Deve essere opt-in e retrocompatibile: se non crei esplicitamente un VFS, tutto funziona esattamente come oggi. Quando ne crei uno, dovrebbe intercettare in modo trasparente le chiamate al filesystem, così che i pacchetti ben educati funzionino senza modifiche.
'Ben educati' è il qualificatore chiave. I pacchetti che richiamano cp o rm da shell, usano addon nativi che chiamano direttamente open() di libc, o si affidano a /proc o ad altri filesystem specifici del sistema operativo non funzioneranno con un VFS. È un limite netto: ogni astrazione perde quando il codice scavalca il livello e raggiunge il sistema sottostante.
Cosa si sta proponendo davvero
La discussione sul VFS di Node.js non è teorica: nel issue tracker di Node.js ci sono proposte concrete. La direzione attuale ruota attorno ad alcune idee chiave.
Primo, un'interfaccia FileSystemProvider a cui il modulo fs delega. Il provider predefinito è il filesystem reale. I provider personalizzati implementano la stessa interfaccia per filesystem in memoria, di sola lettura, overlay o remoti.
Secondo, l'integrazione con il module loader. require() e import() risolverebbero i moduli attraverso il VFS, abilitando strategie di risoluzione che non dipendono dalle directory node_modules.
Terzo, un layer di policy. Il VFS potrebbe applicare controlli di accesso, impedendo al codice di leggere o scrivere percorsi fuori da un ambito definito. Questo darebbe a Node.js una capacità di sandboxing integrata simile ai flag --allow-read e --allow-write di Deno.
Se questo arriverà in Node.js 24, 26 o mai dipende dalla disponibilità del team di Node.js e dalla voglia della community di affrontare un cambiamento breaking nel modo in cui funziona l'accesso al filesystem. Ma la pressione è reale: gli edge runtime stanno crescendo, i target di deploy WebAssembly si moltiplicano, e il divario tra 'JavaScript ovunque' e 'Node.js specificamente sui server con filesystem reali' sta diventando uno svantaggio competitivo.
Cosa puoi fare oggi
Non serve aspettare un VFS a livello di runtime per beneficiare di un'astrazione del filesystem. Per il codice nuovo, incapsula l'accesso al filesystem dietro un'interfaccia. Invece di chiamare direttamente fs.readFile, crea un'interfaccia FileStore da cui dipende il tuo codice. Implementala con il filesystem reale in produzione e con una mappa in memoria nei test. È la classica dependency inversion: poco scintillante, efficace e compatibile con qualunque runtime.
Per il codice esistente, librerie come memfs e unionfs forniscono implementazioni in memoria e overlay del filesystem che patchano il modulo fs di Node. Non sono perfette (addon nativi e processi figli le aggirano), ma funzionano per il codice puramente JavaScript e rendono i test molto più semplici.
Il filesystem è l'ultimo grande pezzo dell'infrastruttura di Node.js che non ha un confine di astrazione pulito. Gli stream hanno interfacce. HTTP ha interfacce. Persino il module loader ha degli hook. Quando anche il filesystem riceverà lo stesso trattamento, Node.js diventerà un runtime davvero portabile invece che specifico per il server. È un cambiamento per cui vale la pena battersi.


