भविष्य को आकार देने वाली तकनीक पर गहन लेख।

Node.js में Virtual File System की ज़रूरत क्यों है

Node.js असली फ़ाइलसिस्टम से गहराई से जुड़ा है। Virtual file system layer से बेहतर टेस्टिंग, सैंडबॉक्सिंग और edge deployment संभव होगा।

रोबोटिक हाथ कागज़ के फ़ोल्डर उठा रहे हैं, और पास में एक भूतिया होलोग्राफ़िक फ़ोल्डर तैर रहा है

बिना असली फ़ाइलसिस्टम के Node.js application चलाकर देखिए, कितनी जल्दी सब बिखर जाता है। require() फ़ाइलें पढ़ता है। fs.readFile() फ़ाइलें पढ़ता है। Template engines फ़ाइलें पढ़ते हैं। Config loaders फ़ाइलें पढ़ते हैं। Logging libraries फ़ाइलों में लिखती हैं। पूरा Node.js ecosystem मानकर चलता है कि एक POSIX-जैसा filesystem मौजूद है, लिखने लायक है, और code व data तक पहुँचने का मुख्य तरीका यही है।

यह 2010 में ठीक था, जब Node.js स्थानीय disks वाले servers पर चलता था। अब यह दिक्कत बढ़ रही है, क्योंकि हम JavaScript को ऐसी जगहों पर चलाना चाहते हैं जहाँ filesystem होता ही नहीं, या read-only होता है, या जिस पर भरोसा नहीं किया जा सकता: edge runtimes, WebAssembly sandboxes, serverless functions और browser-based development environments।

जहाँ filesystem वाली मान्यता टूटती है

Edge runtimes. Cloudflare Workers, Deno Deploy और Vercel Edge Functions JavaScript को edge पर चलाते हैं, यानी उन machines पर जो user के करीब हैं और जिनका infrastructure बेहद सीमित है। इन environments में अक्सर लिखने योग्य filesystem नहीं मिलता, या सिर्फ़ एक सीमित in-memory filesystem मिलता है जो requests के बीच बचा नहीं रहता। ऐसे Node.js modules जो fs.writeFileSync call करते हैं, crash हो जाते हैं। जो modules config फ़ाइलें ढूँढने के लिए fs.existsSync इस्तेमाल करते हैं, उनका व्यवहार अनिश्चित हो जाता है।

WebAssembly. Node.js को WebAssembly sandbox के अंदर चलाने के लिए filesystem operations को Wasm runtime के virtual filesystem (आम तौर पर WASI) पर map करना पड़ता है। यह mapping पूरी तरह सटीक नहीं होती। File permissions, symlinks और platform-specific paths ठीक से translate नहीं होते। VFS abstraction इस mapping को ad hoc तरीके के बजाय साफ़ और स्पष्ट बना सकता है।

Testing. ऐसा code जो config फ़ाइलें पढ़ता है, templates load करता है या logs लिखता है, उसका unit test लिखने के लिए दो में से एक रास्ता चुनना पड़ता है: fs module को mock करना (जो नाज़ुक होता है, क्योंकि अलग-अलग libraries अलग-अलग fs APIs इस्तेमाल करती हैं), या temporary directories में test fixtures बनाना (जो धीमा है, flaky है और disk पर कचरा छोड़ जाता है)। Virtual filesystem से tests पूरी तरह memory में, deterministic तरीके से चल सकेंगे, और असली filesystem को छुए बिना।

Security sandboxing. जब untrusted code चलाना हो, जैसे plugins, user scripts या CI/CD build steps, तो filesystem access पर नियंत्रण चाहिए। अभी इसके लिए OS-level sandboxing (containers, namespaces) लेना पड़ता है या fs module को monkey-patch करना पड़ता है। VFS से बारीक filesystem policies बन सकेंगी: यह code /app पढ़ सकता है पर /etc नहीं, /tmp में लिख सकता है पर /app में नहीं।

VFS कैसा दिखेगा

Virtual file system कोई नया विचार नहीं है। Operating systems 1980 के दशक से VFS layers इस्तेमाल कर रहे हैं। Linux का VFS ext4, NFS और procfs को एक ही API के पीछे साथ-साथ चलने देता है। Node.js के लिए विचार वही है: filesystem interface (fs.readFile, require() आदि) को filesystem implementation से अलग कर देना।

// 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 calls को ही नहीं, बल्कि require(), import(), __dirname और process.cwd() को भी intercept करता है। यही इसे उपयोगी बनाता है: application के code में बदलाव किए बिना पूरे application की filesystem access को redirect किया जा सकता है।

Module Resolution की समस्या

Node.js और filesystem के बीच सबसे गहरा उलझाव module resolution में है। जब आप require('express') लिखते हैं, तो Node.js directory tree में ऊपर की ओर जाकर node_modules/express directories खोजता है, package.json फ़ाइलें पढ़ता है, symlinks resolve करता है और main या exports field को फ़ॉलो करता है। एक ही require के लिए यह प्रक्रिया दर्जनों filesystem calls करती है।

Yarn PnP (Plug'n'Play) ने इसे आंशिक रूप से हल किया। इसने node_modules की जगह एक manifest file रखी, जो module names को zip archives से map करती है। यह तेज़ है (directory traversal नहीं होता) और ज़्यादा deterministic भी (hoisting के आश्चर्य नहीं)। लेकिन इसके लिए Node के module resolution को monkey-patch करना पड़ा, और इससे वे tools टूट गए जो मानकर चलते थे कि node_modules मौजूद है।

सही VFS में Yarn PnP का तरीका एक first-class feature बन सकता है। Module resolution VFS के ज़रिए होगा, जो कोई भी mapping लागू कर सके: zip archives, in-memory modules, remote URLs, या पारंपरिक node_modules directory walk। अलग-अलग environments के लिए अलग strategies, और application code के लिए एक ही API।

पहले से मौजूद समाधान

दूसरे runtimes इस समस्या के कुछ रूप पहले ही हल कर चुके हैं।

  • Deno डिफ़ॉल्ट रूप से modules को URLs से लोड करता है और उन्हें एक managed directory में cache करता है। यहाँ node_modules नहीं है और filesystem-आधारित module resolution भी नहीं। Runtime तय करता है कि modules कहाँ और कैसे store होंगे।
  • Bun hardlinks के साथ एक global module cache इस्तेमाल करता है, जिससे filesystem का overhead घटता है। Node.js की तुलना में इसकी module resolution काफ़ी optimized है।
  • Go का io/fs interface (Go 1.16 में जोड़ा गया) एक filesystem interface तय करता है, जिसे OS filesystem, embedded files (embed.FS), zip archives और in-memory filesystems लागू करते हैं। Standard library के functions fs.FS interface स्वीकार करते हैं, इसलिए वे असली फ़ाइलों के बिना भी testable रहते हैं।
  • Java का NIO FileSystem pluggable filesystem providers को सपोर्ट करता है, जिनमें testing के लिए in-memory implementations और zip-based filesystems शामिल हैं।
  • .NET का IFileSystem pattern testability के लिए .NET applications में बड़े पैमाने पर इस्तेमाल होता है, हालाँकि यह runtime फ़ीचर नहीं बल्कि community की convention है।

Go का तरीका खास तौर पर सीखने लायक है। एक छोटा interface (Open, Read, Stat) तय करके और उसे पूरी standard library में इस्तेमाल करके Go ने existing code तोड़े बिना filesystem abstraction को बेहद आसान बना दिया। Node.js भी यही कर सकता है: एक VFS interface तय करे और standard library व module loader को धीरे-धीरे उसका इस्तेमाल करने के लिए migrate करे।

Compatibility की चुनौती

Node.js VFS की सबसे बड़ी बाधा implementation नहीं, बल्कि ecosystem है। npm पर दो मिलियन से ज़्यादा packages हैं, और उनमें से बड़ा हिस्सा filesystem को ऐसे तरीकों से access करता है जो POSIX semantics, असली paths और लिखने योग्य directories मानकर चलते हैं।

जो VFS मौजूदा packages तोड़ दे, वह पैदा होते ही मर जाएगा। उसे opt-in और backwards-compatible होना होगा: अगर आप साफ़ तौर पर VFS नहीं बनाते, तो सब कुछ आज की तरह ही चलेगा। और जब आप VFS बनाते हैं, तो उसे filesystem calls को पारदर्शी रूप से intercept करना चाहिए, ताकि अच्छे व्यवहार वाले packages बिना बदलाव के काम करें।

'अच्छा व्यवहार' ही असली शर्त है। जो packages cp या rm को shell out करते हैं, libc का open() सीधे call करने वाले native addons इस्तेमाल करते हैं, या /proc या दूसरे OS-specific filesystems पर निर्भर हैं, वे VFS के साथ काम नहीं करेंगे। यह एक कठोर सीमा है: जब code उस सीमा से आगे जाकर नीचे के system तक पहुँचता है, तो हर abstraction लीक होता है।

असल में क्या प्रस्तावित है

Node.js VFS की चर्चा सिर्फ़ सैद्धांतिक नहीं है। Node.js के issue tracker में ठोस प्रस्ताव मौजूद हैं। मौजूदा दिशा में कुछ मुख्य विचार शामिल हैं।

पहला, एक FileSystemProvider interface, जिसे fs module delegate करता है। डिफ़ॉल्ट provider असली filesystem है। Custom providers इसी interface को in-memory, read-only, overlay या remote filesystems के लिए लागू करते हैं।

दूसरा, module loader के साथ integration। require() और import() modules को VFS के ज़रिए resolve करेंगे, जिससे ऐसी module resolution strategies संभव होंगी जो node_modules directories पर निर्भर न हों।

तीसरा, एक policy layer। VFS access controls लागू कर सकता है, यानी code को एक तय दायरे के बाहर के paths पढ़ने या लिखने से रोक सकता है। इससे Node.js में Deno के --allow-read और --allow-write flags जैसी built-in sandboxing क्षमता आ सकती है।

यह Node.js 24 में आएगा, 26 में, या कभी आएगा भी, यह Node.js team की क्षमता और filesystem access के तरीके में breaking change के लिए community की रुचि पर निर्भर करता है। पर दबाव असली है: edge runtimes बढ़ रहे हैं, WebAssembly deployment targets बढ़ रहे हैं, और 'हर जगह JavaScript' और 'असली filesystem वाले servers पर Node.js' के बीच का अंतर प्रतिस्पर्धा में नुकसान बनता जा रहा है।

आज आप क्या कर सकते हैं

Runtime VFS का इंतज़ार किए बिना भी filesystem abstraction का फ़ायदा लिया जा सकता है। नए code के लिए filesystem access को एक interface के पीछे रखिए। सीधे fs.readFile call करने के बजाय एक FileStore interface बनाइए, जिस पर आपका code निर्भर करे। Production के लिए उसे असली filesystem से लागू कीजिए और tests के लिए in-memory map से। यह standard dependency inversion है: साधारण सा, पर असरदार, और किसी भी runtime के साथ compatible।

मौजूदा code के लिए memfs और unionfs जैसी libraries in-memory और overlay filesystem implementations देती हैं, जो Node के fs module को patch करती हैं। ये पूरी तरह सही नहीं हैं, native addons और child processes इन्हें बायपास कर देते हैं, पर pure-JavaScript code के लिए ये काम करती हैं और testing को काफ़ी आसान बना देती हैं।

Filesystem Node.js infrastructure का आख़िरी बड़ा हिस्सा है जिसकी कोई साफ़ abstraction boundary नहीं है। Streams के पास interfaces हैं। HTTP के पास interfaces हैं। Module loader के पास भी hooks हैं। जब filesystem को भी यही दर्जा मिलेगा, तब Node.js सच में portable runtime बनेगा, सिर्फ़ servers तक सीमित नहीं। यह बदलाव माँगने लायक है।