Зачем Node.js нужна виртуальная файловая система
Node.js жёстко привязан к реальной файловой системе. Виртуальная FS (VFS) открыла бы путь к лучшему тестированию, песочницам и edge-деплою.

Попробуйте запустить приложение на Node.js без настоящей файловой системы и увидите, как быстро всё развалится. require() читает файлы. fs.readFile() читает файлы. Шаблонизаторы читают файлы. Загрузчики конфигов читают файлы. Библиотеки логирования пишут файлы. Вся экосистема Node.js исходит из того, что POSIX-подобная файловая система существует, доступна для записи и является основным способом доступа к коду и данным.
В 2010 году, когда Node.js работал на серверах с локальными дисками, это было нормально. Сейчас проблема всё острее: мы хотим запускать JavaScript там, где файловой системы нет, она только для чтения или ей не стоит доверять. Это edge-рантаймы, песочницы WebAssembly, serverless-функции и браузерные среды разработки.
Где предположение о файловой системе ломается
Edge-рантаймы. Cloudflare Workers, Deno Deploy и Vercel Edge Functions выполняют JavaScript на периферии, на машинах рядом с пользователем, с минимальной инфраструктурой. Часто там нет доступной для записи файловой системы или есть ограниченная in-memory ФС, которая не сохраняется между запросами. Модули, которые вызывают fs.writeFileSync, падают. Модули, которые используют fs.existsSync для проверки конфигов, ведут себя непредсказуемо.
WebAssembly. Чтобы запустить Node.js внутри песочницы WebAssembly, файловые операции нужно сопоставить с виртуальной файловой системой Wasm-рантайма (обычно WASI). Это сопоставление несовершенно: права доступа к файлам, симлинки и платформенные пути переносятся нечисто. VFS-абстракция сделала бы это сопоставление явным, а не костыльным.
Тестирование. Юнит-тесты для кода, который читает конфиги, загружает шаблоны или пишет логи, требуют либо мокания модуля fs (хрупко, потому что разные библиотеки используют разные API fs), либо создания временных директорий с фикстурами (медленно, нестабильно и оставляет мусор на диске). Виртуальная файловая система позволила бы запускать тесты полностью в памяти, детерминированно и не трогая реальный диск.
Песочницы для безопасности. Когда запускаешь недоверенный код, будь то плагины, пользовательские скрипты или шаги CI/CD, хочется контролировать доступ к файловой системе. Сейчас для этого нужна изоляция на уровне ОС (контейнеры, namespaces) или monkey-patching модуля fs. VFS позволила бы задавать гранулярные политики: этот код может читать /app, но не /etc, и может писать в /tmp, но не в /app.
Как могла бы выглядеть VFS
Виртуальная файловая система не новая идея. Операционные системы используют VFS-слои с 1980-х: в Linux VFS позволяет ext4, NFS и procfs работать за одним API. Идея для Node.js та же: отделить интерфейс файловой системы (fs.readFile, require() и т. д.) от её реализации.
// 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();
});
Ключевая мысль в том, что VFS перехватывает весь доступ к файловой системе, а не только явные вызовы fs, но и require(), import(), __dirname и process.cwd(). Именно это делает её полезной: можно перенаправить весь файловый доступ приложения, не меняя само приложение.
Проблема разрешения модулей
Самая глубокая связка Node.js с файловой системой это разрешение модулей. Когда вы пишете require('express'), Node.js поднимается по дереву директорий в поисках папок node_modules/express, читает файлы package.json, разрешает симлинки и следует полю main или exports. Для одного require это десятки обращений к файловой системе.
Yarn PnP (Plug'n'Play) частично решил эту проблему, заменив node_modules файлом-манифестом, который сопоставляет имена модулей с zip-архивами. Это быстрее (нет обхода директорий) и предсказуемее (нет сюрпризов с hoisting), но потребовало monkey-patching механизма разрешения модулей Node, что сломало инструменты, рассчитывавшие на существование node_modules.
Полноценная VFS сделала бы подход Yarn PnP штатной возможностью. Разрешение модулей шло бы через VFS, которая могла бы реализовать любое сопоставление: zip-архивы, in-memory модули, удалённые URL или традиционный обход node_modules. Разные стратегии для разных окружений, один API для прикладного кода.
Что уже сделано в других системах
Другие рантаймы уже решили вариации этой задачи.
- Deno загружает модули по URL по умолчанию и кеширует их в управляемой директории. Нет
node_modulesи нет разрешения модулей через файловую систему. Рантайм сам контролирует, где и как хранятся модули. - Bun использует глобальный кеш модулей с жёсткими ссылками (hardlinks), что снижает нагрузку на файловую систему. Его разрешение модулей заметно оптимизировано по сравнению с Node.js.
- Интерфейс
io/fsв Go (появился в Go 1.16) определяет интерфейс файловой системы, который реализуют ОС-файловая система, встроенные файлы (embed.FS), zip-архивы и in-memory ФС. Функции стандартной библиотеки принимают интерфейсfs.FS, поэтому их можно тестировать без реальных файлов. - NIO
FileSystemв Java поддерживает подключаемых провайдеров файловых систем, включая in-memory реализации для тестов и ФС на основе zip. - Паттерн
IFileSystemв .NET широко используется в приложениях на .NET для тестируемости, хотя это скорее соглашение сообщества, чем возможность рантайма.
Подход Go особенно показателен. Определив небольшой интерфейс (Open, Read, Stat) и используя его по всей стандартной библиотеке, Go сделал абстракцию файловой системы тривиальной, не ломая существующий код. Node.js мог бы поступить так же: определить интерфейс VFS и постепенно перевести на него стандартную библиотеку и загрузчик модулей.
Проблема совместимости
Главное препятствие для VFS в Node.js это не реализация, а экосистема. В npm больше двух миллионов пакетов, и значительная их часть обращается к файловой системе, предполагая семантику POSIX, реальные пути и доступные для записи директории.
VFS, которая ломает существующие пакеты, мертворождена. Она должна быть opt-in и обратно совместимой: если VFS явно не создана, всё работает так же, как сегодня. Когда же VFS создана, она должна прозрачно перехватывать вызовы файловой системы, чтобы добросовестные пакеты работали без изменений.
Ключевое слово здесь «добросовестные». Пакеты, которые вызывают cp или rm, используют нативные аддоны, напрямую вызывающие open() из libc, или опираются на /proc и другие ОС-специфичные файловые системы, с VFS работать не будут. Это жёсткая граница: любая абстракция протекает, когда код лезет к нижележащей системе в обход неё.
Что на самом деле предлагается
Обсуждение VFS для Node.js не теория: в issue-трекере Node.js есть конкретные предложения. Текущее направление опирается на несколько ключевых идей.
Во-первых, интерфейс FileSystemProvider, которому делегирует модуль fs. Провайдером по умолчанию является реальная файловая система. Пользовательские провайдеры реализуют тот же интерфейс для in-memory, только для чтения, overlay или удалённых файловых систем.
Во-вторых, интеграция с загрузчиком модулей. require() и import() разрешали бы модули через VFS, что позволит использовать стратегии разрешения, не зависящие от директорий node_modules.
В-третьих, слой политик. VFS могла бы применять контроль доступа, не давая коду читать или писать пути за пределами заданной области. Это дало бы Node.js встроенную возможность песочницы, похожую на флаги --allow-read и --allow-write в Deno.
Выйдет ли это в Node.js 24, 26 или вообще когда-нибудь, зависит от ресурсов команды Node.js и готовности сообщества к ломающему изменению в работе с файловой системой. Но давление реальное: edge-рантаймы растут, цели развёртывания WebAssembly множатся, а разрыв между «JavaScript везде» и «Node.js именно на серверах с реальной файловой системой» становится конкурентным минусом.
Что можно сделать уже сегодня
Не обязательно ждать рантайм-VFS, чтобы выиграть от абстракции файловой системы. Для нового кода оберните доступ к файлам в интерфейс. Вместо прямых вызовов fs.readFile создайте интерфейс FileStore, от которого зависит ваш код. Реализуйте его поверх реальной файловой системы для продакшена и через in-memory map для тестов. Это стандартная инверсия зависимостей: незаметно, но эффективно и совместимо с любым рантаймом.
Для существующего кода библиотеки вроде memfs и unionfs предоставляют in-memory и overlay-реализации файловой системы, которые подменяют модуль fs Node. Они не идеальны: нативные аддоны и дочерние процессы их обходят. Но для чистого JavaScript они работают и заметно упрощают тестирование.
Файловая система последний крупный кусок инфраструктуры Node.js, у которого нет чистой границы абстракции. У стримов есть интерфейсы, у HTTP есть интерфейсы, даже у загрузчика модулей есть хуки. Когда файловая система получит такое же отношение, Node.js станет по-настоящему переносимым рантаймом, а не серверным. Это изменение стоит продвигать.


