Artigos aprofundados sobre a tecnologia que molda o que vem a seguir.

O Caso para um Sistema de Arquivos Virtual no Node.js

O Node.js está fortemente acoplado ao sistema de arquivos real. Uma camada de VFS traria testes, sandboxing e deploy na edge bem melhores.

Braços mecânicos pegando pastas de papel enquanto uma pasta holográfica fantasmagórica flutua por perto

Tente rodar uma aplicação Node.js sem um sistema de arquivos real e veja como ela desmorona rapidamente. require() lê arquivos. fs.readFile() lê arquivos. Os template engines lêem arquivos. Os carregadores de configuração lêem arquivos. Bibliotecas de logging gravam arquivos. Todo o ecossistema do Node.js assume que existe um sistema de arquivos estilo POSIX, que ele é gravável e que é a forma principal de acessar código e dados.

Isso era tranquilo em 2010, quando o Node.js rodava em servidores com discos locais. Hoje, porém, é cada vez mais problemático, já que queremos rodar JavaScript em lugares onde o sistema de arquivos não existe, é somente leitura ou não deve ser confiável: edge runtimes, sandboxes WebAssembly, funções serverless e ambientes de desenvolvimento no navegador.

Onde a Premissa do Sistema de Arquivos Quebra

Edge runtimes. Cloudflare Workers, Deno Deploy e Vercel Edge Functions rodam JavaScript na edge, em máquinas próximas ao usuário, com infraestrutura mínima. Esses ambientes frequentemente não oferecem um sistema de arquivos gravável, ou oferecem um sistema em memória limitado que não persiste entre requisições. Módulos do Node.js que chamam fs.writeFileSync simplesmente quebram. Módulos que usam fs.existsSync para verificar arquivos de configuração se comportam de forma imprevisível.

WebAssembly. Rodar o Node.js dentro de um sandbox WebAssembly exige mapear as operações de arquivo para o sistema de arquivos virtual do runtime Wasm (normalmente WASI). Esse mapeamento é imperfeito: permissões de arquivo, links simbólicos e caminhos específicos de plataforma não se traduzem bem. Uma abstração de VFS tornaria esse mapeamento explícito, em vez de ad hoc.

Testes. Testar unitariamente código que lê arquivos de configuração, carrega templates ou grava logs exige mockar o módulo fs (frágil, porque bibliotecas diferentes usam APIs diferentes de fs) ou criar diretórios temporários com fixtures (lento, instável e deixa lixo no disco). Um sistema de arquivos virtual permitiria que os testes rodassem inteiramente em memória, de forma determinística, sem tocar no sistema de arquivos real.

Isolamento de segurança. Ao executar código não confiável, como plugins, scripts de usuários ou etapas de build em CI/CD, você quer controlar o acesso ao sistema de arquivos. Hoje, isso exige sandboxing no nível do SO (containers, namespaces) ou monkey-patching do módulo fs. Um VFS permitiria políticas de acesso granulares: este código pode ler /app, mas não /etc; pode escrever em /tmp, mas não em /app.

Como Seria um VFS

Um sistema de arquivos virtual não é um conceito novo. Sistemas operacionais usam camadas de VFS desde os anos 1980. O VFS do Linux permite que ext4, NFS e procfs coexistam atrás da mesma API. A ideia para o Node.js é a mesma: desacoplar a interface do sistema de arquivos (fs.readFile, require() etc.) da implementação dele.

// 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();
});

O ponto-chave é que o VFS intercepta todo acesso ao sistema de arquivos, não apenas as chamadas explícitas a fs, mas também require(), import(), __dirname e process.cwd(). É isso que o torna útil: você pode redirecionar todo o acesso ao sistema de arquivos de uma aplicação sem modificá-la.

O Problema da Resolução de Módulos

O vínculo mais profundo entre o Node.js e o sistema de arquivos é a resolução de módulos. Quando você escreve require('express'), o Node.js sobe na árvore de diretórios procurando pastas node_modules/express, lendo arquivos package.json, resolvendo links simbólicos e seguindo o campo main ou exports. Esse processo faz dezenas de chamadas ao sistema de arquivos para um único require.

O Yarn PnP (Plug'n'Play) resolveu parcialmente isso ao substituir a pasta node_modules por um arquivo de manifesto que mapeia nomes de módulos para arquivos zip. É mais rápido (sem percorrer diretórios) e mais determinístico (sem surpresas de hoisting), mas exigiu monkey-patching da resolução de módulos do Node, o que quebrou ferramentas que faziam suposições sobre a existência de node_modules.

Um VFS bem projetado tornaria a abordagem do Yarn PnP um recurso de primeira classe. A resolução de módulos passaria pelo VFS, que poderia implementar qualquer mapeamento: arquivos zip, módulos em memória, URLs remotas ou a tradicional varredura de diretórios node_modules. Estratégias diferentes para ambientes diferentes, mesma API para o código da aplicação.

Soluções Já Existentes

Outros runtimes já resolveram variações desse problema.

  • Deno carrega módulos a partir de URLs por padrão e os armazena em cache em um diretório gerenciado. Não existe node_modules nem resolução de módulos baseada em sistema de arquivos. O runtime controla onde e como os módulos são armazenados.
  • Bun usa um cache global de módulos com hardlinks, reduzindo a sobrecarga no sistema de arquivos. Sua resolução de módulos é bem otimizada em comparação com a do Node.js.
  • A interface io/fs do Go (adicionada no Go 1.16) define uma interface de sistema de arquivos implementada pelo sistema de arquivos do SO, por arquivos embutidos (embed.FS), arquivos zip e sistemas de arquivos em memória. As funções da biblioteca padrão aceitam a interface fs.FS, tornando-as testáveis sem arquivos reais.
  • A abstração FileSystem do NIO do Java suporta provedores de sistema de arquivos plugáveis, incluindo implementações em memória para testes e sistemas de arquivos baseados em zip.
  • O padrão IFileSystem do .NET é amplamente usado em aplicações .NET para testabilidade, embora seja uma convenção da comunidade e não um recurso do runtime.

A abordagem do Go é especialmente instrutiva. Ao definir uma interface pequena (Open, Read, Stat) e usá-la em toda a biblioteca padrão, o Go tornou a abstração do sistema de arquivos trivialmente fácil sem quebrar código existente. O Node.js poderia fazer o mesmo: definir uma interface de VFS e migrar gradualmente a biblioteca padrão e o carregador de módulos para usá-la.

O Desafio da Compatibilidade

O maior obstáculo para um VFS no Node.js não é a implementação, e sim o ecossistema. O npm tem mais de dois milhões de pacotes, e uma parcela significativa deles acessa o sistema de arquivos de maneiras que assumem semântica POSIX, caminhos reais e diretórios graváveis.

Um VFS que quebre pacotes existentes já nasce morto. Ele precisa ser opt-in e retrocompatível: se você não criar explicitamente um VFS, tudo funciona exatamente como hoje. Quando você criar um, ele deve interceptar as chamadas ao sistema de arquivos de forma transparente, para que pacotes bem-comportados funcionem sem modificação.

'Bem-comportado' é o qualificador-chave. Pacotes que chamam cp ou rm via shell, usam addons nativos que chamam diretamente o open() da libc ou dependem de /proc ou de outros sistemas de arquivos específicos do SO não vão funcionar com um VFS. Essa é uma fronteira rígida: toda abstração vaza quando o código ultrapassa essa fronteira e acessa o sistema subjacente.

O Que Está Sendo Proposto de Fato

A discussão sobre um VFS no Node.js não é teórica. Existem propostas concretas no issue tracker do Node.js. A direção atual envolve algumas ideias-chave.

Primeiro, uma interface FileSystemProvider para a qual o módulo fs delega. O provedor padrão é o sistema de arquivos real. Provedores customizados implementam a mesma interface para sistemas em memória, somente leitura, overlay ou remotos.

Segundo, integração com o carregador de módulos. require() e import() passariam a resolver módulos pelo VFS, permitindo estratégias de resolução que não dependam de diretórios node_modules.

Terceiro, uma camada de políticas. O VFS poderia aplicar controles de acesso, impedindo que o código leia ou escreva caminhos fora de um escopo definido. Isso daria ao Node.js uma capacidade de sandboxing embutida, semelhante às flags --allow-read e --allow-write do Deno.

Se isso será lançado no Node.js 24, 26 ou algum dia, depende da disponibilidade da equipe do Node.js e do apetite da comunidade por uma mudança que quebra compatibilidade na forma como o acesso ao sistema de arquivos funciona. Mas a pressão é real: edge runtimes estão crescendo, os alvos de deploy WebAssembly se multiplicam, e a distância entre 'JavaScript em todo lugar' e 'Node.js especificamente em servidores com sistemas de arquivos reais' está se tornando uma desvantagem competitiva.

O Que Você Pode Fazer Hoje

Você não precisa esperar por um VFS no runtime para se beneficiar de uma abstração do sistema de arquivos. Para código novo, encapsule o acesso ao sistema de arquivos atrás de uma interface. Em vez de chamar fs.readFile diretamente, crie uma interface FileStore da qual seu código depende. Implemente-a com o sistema de arquivos real para produção e com um mapa em memória para os testes. É inversão de dependência padrão: sem glamour, eficaz e compatível com qualquer runtime.

Para código existente, bibliotecas como memfs e unionfs oferecem implementações em memória e overlay que fazem patch no módulo fs do Node. Elas não são perfeitas, já que addons nativos e processos filhos contornam essas implementações, mas funcionam para código JavaScript puro e tornam os testes muito mais fáceis.

O sistema de arquivos é a última grande peça da infraestrutura do Node.js que ainda não tem uma fronteira de abstração limpa. Streams têm interfaces. HTTP tem interfaces. Até o carregador de módulos tem hooks. Quando o sistema de arquivos receber o mesmo tratamento, o Node.js será um runtime realmente portável, e não apenas um voltado a servidores. Essa é uma mudança que vale a pena defender.