Node.js 一直以來都是以 I/O 為核心,處理串流、緩衝區、套接字和檔案系統。這個執行環境從一開始就設計為能夠快速在網路與檔案系統之間傳輸資料。但有一個長期困擾我的問題:你無法虛擬化檔案系統。

你無法匯入或 require() 一個只存在於記憶體中的模組。你無法在不修改標準函式庫的情況下,將資產打包成單一執行檔。你無法為多租戶平台沙盒化檔案存取,除非從頭重新實作 fs。

現在這些都將改變。我們宣布推出 @platformatic/vfs,一個用戶端的 Node.js 虛擬檔案系統,並且 node:vfs 模組即將合併進入 Node.js 核心。

當 Node.js 沒有 VFS 時,實務上會遇到以下問題:

將完整應用程式打包成單一執行檔時,你需要隨程式碼一起發佈設定檔、範本和靜態資產。這通常會額外增加 20 到 40 MB 的樣板程式碼來處理執行時的資產存取。Node.js 的單一執行檔(SEA)可以嵌入一個 blob,但應用程式碼仍然呼叫 fs.readFileSync() 並期待真實路徑,因此你最終會重複檔案或注入膨脹的膠水程式碼。

執行測試時不想觸碰磁碟,你需要一個隔離的記憶體檔案系統,避免測試遺留檔案或在 CI 中衝突。現在你只能用 memfs 這類工具模擬 fs,但這些模擬不會整合 import 或 require()。

在多租戶平台中沙盒化租戶的檔案存取,你必須限制每個租戶只能存取特定目錄,防止他們透過 ../ 路徑跳脫。你會寫出脆弱且容易出錯的路徑驗證邏輯。

載入執行時產生的程式碼。AI 代理、外掛系統和程式碼生成管線會產生需要匯入的 JavaScript。現在只能寫入暫存檔並希望能正確清理。

以上四種需求都需要同一個原始功能:一個能掛鉤 node:fs 與 Node.js 模組載入的虛擬檔案系統。生態系已有 memfs、unionfs、mock-fs 等近似方案,但它們都只能修改 fs,無法影響模組解析器。呼叫 import('./config.json') 的程式碼會完全繞過它們。

Daniel Lando 開啟的 VFS 掛鉤需求議題,和 Single Executable 工作組的 FS 掛鉤提案,清楚記錄了多年的需求。大家知道想要什麼,但沒人做出來。

我從 2025 年聖誕節開始著手實作 VFS。這個假期實驗變成了 PR #61478 :一個 node:vfs 模組,包含近 14,000 行程式碼,分布在 66 個檔案中。

老實說,這麼龐大的 PR 通常需要數月全職工作。但這次我用 Claude Code AI 協助,讓 AI 負責繁瑣的部分,例如實作所有 fs 方法變體(同步、回呼、Promise)、測試覆蓋和文件生成。我專注於架構、API 設計和逐行審查。沒有 AI,這個假期專案根本不可能完成。

這不是模擬。當你呼叫 myVfs.mount('/virtual') 時,VFS 會掛鉤到實際的 fs 模組和模組解析器。程式中任何讀取 /virtual 路徑下的程式碼(包含你的和第三方套件)都會從 VFS 取得內容。第三方函式庫不需要知道它的存在。express.static('/virtual/public') 也能正常運作。

VFS 有兩層架構:提供者層和掛載層。

提供者是儲存後端。MemoryProvider 是預設的:記憶體中、快速且程序結束即消失。SEAProvider 提供單一執行檔中嵌入資產的唯讀存取。VirtualProvider 是可擴充的基底類別,可用於自訂後端(資料庫、網路等)。

掛載是讓 VFS 對程序可見的方式。myVfs.mount('/virtual') 會讓 VFS 內容在該路徑前綴下可存取。process 物件會發出 vfs-mount 和 vfs-unmount 事件,方便追蹤狀態。

還有一種覆蓋模式,適合攔截特定檔案而不隱藏真實檔案系統:

只有 VFS 中存在的路徑會被攔截,其他路徑仍由真實檔案系統處理。這對測試非常理想,你可以覆寫少數檔案,其他保持不變。

VFS 不只是 fs 的子集。它涵蓋同步、回呼和 Promise 版本的讀寫、目錄、符號連結、檔案描述符、串流、監控和 glob。VirtualStats 與 fs.Stats 相符,錯誤代碼與 Node.js 一致(ENOENT、ENOTDIR、EISDIR、EEXIST)。能在真實檔案系統運作的程式碼,也能在 VFS 運作。

@platformatic/vfs 證明了 API 可行,但也顯示用戶端實作永遠有妥協。以下是用戶端實作的限制:

模組解析邏輯重複。用戶端套件包含 960 多行模組解析程式碼,處理 node_modules 樹狀結構、package.json exports 欄位、索引檔案和條件匯出。這些在 Node.js 核心已有實作。

核心模組直接掛鉤解析器,用戶端只能重寫並盼望涵蓋所有邊緣案例。

私有 API。23.5 版前的 Node.js 沒有公開 API 掛鉤模組解析。用戶端套件只能修改 Module._resolveFilename 和 Module._extensions,這些是私有且不保證穩定的內部實作,可能被小版本破壞。

核心模組將 VFS 整合在解析器中,而非外掛式修改。

全域 fs 攔截脆弱。用戶端套件會替換 fs.readFileSync、fs.statSync 等核心函式。如果程式碼在 VFS 掛載前取得 fs.readFileSync 的參考,該參考會繞過 VFS。

核心模組在公開 API 之下攔截,捕獲的參考仍有效。

原生模組無法運作。dlopen() 需要真實檔案路徑。

用戶端 VFS 無法教原生模組載入器從記憶體讀取 .node 檔案,核心模組可以。

模組快取清理不可能。卸載 VFS 時,從 VFS require() 的模組仍留在 require.cache。

用戶端套件無法區分 VFS 載入模組與真實模組,無法清理。核心模組可追蹤並在卸載時失效。

這些限制不是用戶端套件的錯,而是執行環境外的根本限制。用戶端套件是橋樑,現在可用,未來可切換到 node:vfs。

PR 正在開放審查,功能將以實驗性形式釋出。

Igalia 的 Joyee Cheung 是最嚴謹的審查者,她針對 mount() 的安全模型提出建議,指出 internalModuleStat 不應公開,並參考 Single Executable 工作組四年收集的需求文件。她的回饋讓實作大幅改進。

James Snell 和 Paolo Insogna 已批准 PR。Stephen Belanger 提出關於全域 mount() 劫持的安全疑慮,建議整合權限模型。Ethan Arrowood 詳審文件與測試。Aviv Keller 指出可用 node:path 簡化的程式碼。Richard Lau 和 Tierney Cyren 提供文件結構回饋。

感謝所有參與者,審查 14,000 行 PR 是大工程。

我們不想等核心 PR 合併。

Vercel CTO Malte Ubl 看到 PR 後推文表示非常興奮,並考慮將其回溯到用戶端發佈 npm。

我們和 Vercel 團隊都有同樣想法,他們發佈了 node-vfs-polyfill。兩隊獨立將相同 API 推向用戶端,代表設計穩健。

我們的版本是 @platformatic/vfs,支援 Node.js 22 以上。

API 與 node:vfs 提案一致,未來核心模組釋出後,切換只需一行改動:將 '@platformatic/vfs' 換成 'node:vfs'。

用戶端套件提供兩個核心 PR 沒有的提供者。SqliteProvider 以 node:sqlite 支援持久化 VFS,檔案可跨程序重啟保存,適合快取編譯資產或跨部署保存生成代碼。

RealFSProvider 是沙盒化的真實檔案系統存取,將 VFS 路徑映射到真實目錄,防止路徑跳脫。

Node.js 單一執行檔可嵌入資產,但存取一直困難。使用 VFS 後,SEA 資產會自動掛載,可透過標準 fs 呼叫、import 和 require() 存取,應用程式碼無需知道自己在 SEA 環境。

你可以為每個測試建立隔離檔案系統,無需清理暫存目錄,避免平行測試衝突。

AI 代理生成的程式碼需要執行,寫入暫存檔慢且清理麻煩且有安全風險。使用 VFS,生成代碼保留在記憶體中,可直接 import。

node:vfs 和 @platformatic/vfs 都是實驗性功能。測試覆蓋良好,但虛擬檔案系統掛鉤模組載入和 node:fs 涉及龐大範圍,仍會有錯誤、未遇過的邊緣案例和第三方程式碼互動問題。

若遇問題,請回報。用戶端套件請在 platformatic/vfs 開 issue,核心模組請在 nodejs/node PR 或 issue 回報。每個錯誤回報都很重要。

node:vfs 核心合併後,我們會同步 @platformatic/vfs 的 API 變更,並最終以核心模組取代它。

同時,歡迎試用並告訴我們你的使用心得。

此功能修正了 Daniel Lando 提出的 #60021 問題。