Дайджесты новостей
Архитектурная изометрическая модель подсистемы node:vfs в Node.js 26 с разделением резолвера путей и провайдеров хранилища в памяти и архивах на графитовом фоне.

Виртуальные файловые системы в Node.js: как устроен модуль node:vfs

Если ты хоть раз пробовал упаковать приложение на Node.js в переносимый архив, запустить автотесты без создания временных папок на диске или скормить сгенерированный код напрямую в рантайм, то прекрасно знаешь эту давнюю проблему. Файловый ввод-вывод в Node.js всегда был намертво привязан к физическому диску. Чтобы сымитировать файловую структуру в оперативной памяти или прочитать запакованный архив, разработчикам приходилось идти на сомнительные уловки: подменять глобальный объект fs сторонними библиотеками вроде memfs или mock-fs.

Обезьяньи патчи (monkey-patching) платформенных модулей неизбежно ломаются, как только в проекте появляются нативные аддоны, потоки воркеров или строгий загрузчик ECMAScript-модулей (ESM). Мы уже наблюдали развитие серверной платформы, когда разбирали ключевые этапы эволюции версий Node.js.

Но костылям пришел конец. В линейке Node.js 26 (начиная с экспериментального релиза 26.4.0 и до стабилизации в версиях 26.9.0–26.10.0) появилась фундаментальная подсистема — нативный встроенный модуль node:vfs (Virtual Filesystem).

Его разработку возглавил сооснователь Platformatic и член Node.js TSC Маттео Коллина (Matteo Collina) совместно с Джеймсом Снеллом (James Snell, модуль загрузчика) и Филиппом Дункелем (Philipp Dunkel, ZIP-провайдер и флаги запуска). Давай разберем архитектуру этой подсистемы и выясним, как она устроена «под капотом».

Почему патчинг fs зашел в тупик

Старый подход с переопределением методов fs.readFile или fs.stat окончательно исчерпал себя по трем причинам:

  1. Многопоточность: каждый поток worker_threads живет в отдельном изоляте V8. Мутация объекта fs в одном потоке не видна в соседних без сложной синхронизации через общую память.
  2. Загрузчик ESM: ядро Node.js обращается к файлам через системные вызовы libuv на уровне C++. Подмена методов fs в JS-коде игнорировалась загрузчиком: вызов import все равно шел на диск и завершался ошибкой ERR_MODULE_NOT_FOUND.
  3. Нативные аддоны .node: для их подключения системный загрузчик (dlopen в Linux/macOS или LoadLibrary в Windows) требует реальный дескриптор файла, чего виртуальные заглушки в памяти дать не могли.

Архитектура node:vfs: Резолвер и Провайдер

Чтобы устранить эти барьеры, архитектуру node:vfs разделили на два изолированных слоя: Резолвер путей (Resolver) и Провайдер хранилища (Storage Provider).

  • Резолвер (Resolver) работает внутри ядра платформы. Он интегрирован с подсистемой путей, правилами поиска зависимостей в package.json (поля exports, imports), а также хуками загрузчика CJS и ESM. Резолвер отвечает за логику путей, относительные переходы и правила разрешения импортов.
  • Провайдер хранилища (Storage Provider) — это чистая абстракция ввода-вывода. Провайдер ничего не знает об устройстве модулей Node.js. Его задача сводится к базовым бинарным операциям: прочитать массив байтов по ключу, записать блок данных, вернуть метаданные (размер, время) или отдать список файлов в папке.

Такое разделение позволяет подключать под единый интерфейс файловой системы любые бэкенды: оперативную память, сжатый ZIP-архив, локальную директорию на диске или облачное хранилище.

Изолированные пространства вместо глобальных точек монтирования

В привычных виртуальных ФС используется монтирование по пользовательскому пути вроде /virtual. Создатели node:vfs сознательно отказались от этой концепции. Глобальные пути приводили к коллизиям между независимыми библиотеками и вызывали опасный неявный фолбек на диск, если файл в виртуальной точке отсутствовал.

В node:vfs рантайм сам генерирует изолированное пространство имен для каждой смонтированной системы. На POSIX-системах создается путь вида /dev/null/vfs/0, на Windows — нуль-устройство. Виртуальное дерево полностью изолировано от физического диска, исключая случайные конфликты.

Встроенные провайдеры: память, диск и ZIP-архивы

Node.js 26 включает три штатных провайдера хранилищ:

  • MemoryProvider (по умолчанию): динамическое файловое дерево в оперативной памяти с поддержкой создания директорий, ссылок и событий fs.watch. Идеально для тестов и генерации кода на лету.
  • RealFSProvider: мост к реальной папке на диске для проброса файлов внутрь виртуального контекста.
  • ZipProvider: монтирование сжатых ZIP-архивов через модуль node:zlib (ZipFile, ZipBuffer) с прозрачной декомпрессией файлов при чтении.

Вот как выглядит создание виртуальной файловой системы в памяти и запуск сгенерированного модуля:

// demo-vfs-memory.mjs — Создание VFS в памяти и импорт модуля
import { createVFS, MemoryProvider } from 'node:vfs';
import fs from 'node:fs/promises';

// 1. Инициализируем VFS с провайдером памяти
const vfs = await createVFS({
  provider: new MemoryProvider(),
});

// 2. Монтируем изолированное дерево
const mount = await vfs.mount();
console.log('Точка монтирования:', mount.path);

// 3. Записываем код модуля прямо в память
const moduleCode = `
  export function computePayload(data) {
    return data.map(item => item * 2);
  }
`;

await fs.writeFile(`${mount.path}/processor.js`, moduleCode, 'utf8');

// 4. Импортируем модуль напрямую из памяти без обращения к диску
const { computePayload } = await import(`${mount.path}/processor.js`);
console.log('Результат:', computePayload([10, 20, 30]));

Запуск из архивов и Single Executable Applications (SEA)

Главное прикладное преимущество node:vfs — удобство дистрибуции. Микросервис со всеми зависимостями node_modules и ассетами можно собрать в один ZIP-архив и запускать без предварительной распаковки:

Изометрическая взрыв-схема монолитного ZIP-архива Single Executable Application в Node.js 26 с модулем node:vfs и загрузкой C++ аддона через memfd_create без дисковой распаковки.

# Запуск приложения на Node.js 26 напрямую из упакованного ZIP-архива
node --experimental-vfs --vfs-load=application-bundle.zip

Кроме того, node:vfs встроен в технологию сборки бинарных файлов Single Executable Applications (SEA). Флаг "useVfs": true в конфиге сборки помещает ресурсы приложения в виртуальную файловую систему, позволяя коду читать файлы через привычный вызов fs.readFileSync('./assets/config.json').

Для нативных C++ аддонов (.node) разработчики задействовали вызов memfd_create в ядре Linux. Рантайм создает анонимный дескриптор в памяти с исполняемыми правами и передает его системному загрузчику dlopen.

Контекст кодинг-агентов и изоляция

Особое значение node:vfs приобретает для автономных AI-агентов. Когда агент экспериментирует с генерацией кода или собирает временные тестовые стенды, запись на физический диск создает задержки и засоряет проект. В виртуальной ФС агент разворачивает окружение за миллисекунды, прогоняет тесты и мгновенно освобождает память. Это напоминает уроки, которые мы извлекли из постмортема ИИ-агента Smith: надежность автоматизации напрямую зависит от управляемости окружения.

Границы безопасности: VFS — это не песочница

Модуль node:vfs является абстракцией хранения, а не изолирующей песочницей (sandbox). Код внутри виртуальной ФС имеет доступ к сетевым сокетам и системным процессам. При включенной модели разрешений Node.js (Permission Model) монтирование VFS требует явного флага --allow-fs-vfs.

Ограничения первой версии

Синхронное чтение сжатых файлов из ZipProvider блокирует Event Loop на время распаковки — для высоконагруженных задач используйте потоки. Также опция SEA useVfs пока не поддерживает совместную работу со снимками кучи useSnapshot.

Появление node:vfs дает экосистеме Node.js долгожданный стандарт виртуализации ввода-вывода, избавляя разработчиков от шатких monkey-patch библиотек.