حالة وجود نظام ملفات افتراضي (VFS) في Node.js
Node.js مرتبط بشكل وثيق بنظام الملفات الحقيقي. طبقة نظام ملفات افتراضي ستتيح اختبارًا أفضل، وعزلًا آمنًا، ونشرًا على الحافة (edge).

جرّب تشغيل تطبيق Node.js بدون نظام ملفات حقيقي وشاهد كيف ينهار بسرعة. require() يقرأ الملفات. fs.readFile() يقرأ الملفات. محركات القوالب تقرأ الملفات. محمّلات الإعدادات تقرأ الملفات. مكتبات السجلات تكتب الملفات. النظام البيئي لـ Node.js بالكامل يفترض وجود نظام ملفات شبيه بـ POSIX، وقابل للكتابة، وهو الطريقة الأساسية للوصول إلى الكود والبيانات.
كان هذا مقبولًا في 2010 حين كان Node.js يعمل على خوادم بأقراص محلية. أصبح الأمر أكثر إشكالية الآن مع رغبتنا في تشغيل JavaScript في أماكن لا يوجد فيها نظام ملفات، أو يكون للقراءة فقط، أو لا ينبغي الوثوق به: بيئات التشغيل على الحافة (edge runtimes)، وصناديق WebAssembly المعزولة، والدوال بدون خوادم (serverless)، وبيئات التطوير المعتمدة على المتصفح.
أين يفشل افتراض نظام الملفات
بيئات التشغيل على الحافة. تشغّل Cloudflare Workers وDeno Deploy وVercel Edge Functions كود JavaScript على الحافة، أي على أجهزة قريبة من المستخدم وببنية تحتية محدودة. غالبًا ما لا توفر هذه البيئات نظام ملفات قابل للكتابة، أو توفر نظام ملفات محدودًا في الذاكرة لا يحتفظ بالبيانات بين الطلبات. الوحدات التي تستدعي fs.writeFileSync تتعطل، والوحدات التي تستخدم fs.existsSync للتحقق من ملفات الإعدادات تتصرف بشكل غير متوقع.
WebAssembly. تشغيل Node.js داخل صندوق WebAssembly معزول يتطلب ربط عمليات نظام الملفات بنظام الملفات الافتراضي للبيئة Wasm (عادةً WASI). هذا الربط غير مثالي، فصلاحيات الملفات والروابط الرمزية (symlinks) والمسارات الخاصة بكل منصة لا تُترجم بشكل سليم. تجريد VFS سيجعل هذا الربط صريحًا بدلًا من أن يكون مرتجلًا.
الاختبار. اختبار الوحدات التي تقرأ ملفات الإعدادات أو تحمّل القوالب أو تكتب السجلات يتطلب إما محاكاة وحدة fs (وهي هشة، لأن المكتبات المختلفة تستخدم واجهات fs مختلفة)، أو إنشاء مجلدات مؤقتة مع بيانات اختبار (وهي بطيئة وغير مستقرة وتترك بقايا على القرص). نظام ملفات افتراضي سيتيح تشغيل الاختبارات بالكامل في الذاكرة، وبشكل حتمي، دون لمس نظام الملفات الحقيقي.
العزل الأمني. عند تشغيل كود غير موثوق، مثل الإضافات وسكربتات المستخدمين وخطوات البناء في CI/CD، تريد التحكم في الوصول إلى نظام الملفات. حاليًا يتطلب ذلك عزلًا على مستوى نظام التشغيل (الحاويات وnamespaces) أو تعديل وحدة fs برمجيًا (monkey-patching). يسمح VFS بسياسات دقيقة للملفات: يمكن لهذا الكود قراءة /app لكن لا /etc، ويمكنه الكتابة في /tmp لكن لا في /app.
كيف سيبدو نظام الملفات الافتراضي
نظام الملفات الافتراضي ليس مفهومًا جديدًا. استخدمت أنظمة التشغيل طبقات VFS منذ الثمانينيات، فنظام VFS في لينكس يتيح لـ ext4 وNFS وprocfs التعايش خلف الواجهة نفسها. الفكرة لـ 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(). وهذا ما يجعله مفيدًا: يمكنك توجيه كل وصول التطبيق لنظام الملفات دون تعديل التطبيق نفسه.
مشكلة تحليل الوحدات (Module Resolution)
أعمق تشابك بين Node.js ونظام الملفات يكمن في تحليل الوحدات. عندما تكتب require('express')، يصعد Node.js في شجرة المجلدات باحثًا عن مجلدات node_modules/express، ويقرأ ملفات package.json، ويحل الروابط الرمزية، ويتبع الحقل main أو exports. هذه العملية تنفذ عشرات استدعاءات نظام الملفات لاستدعاء require واحد.
حلّ Yarn PnP (Plug'n'Play) جزءًا من هذه المشكلة باستبدال node_modules بملف manifest يربط أسماء الوحدات بأرشيفات zip. وهو أسرع (بلا اجتياز للمجلدات) وأكثر حتمية (بلا مفاجآت الـ hoisting)، لكنه تطلب تعديل تحليل الوحدات في Node برمجيًا، وهذا كسر أدوات كانت تفترض وجود node_modules.
لو وُجد VFS حقيقي لجعل نهج Yarn PnP ميزة من الدرجة الأولى. سيمر تحليل الوحدات عبر VFS، الذي يمكنه تنفيذ أي تعيين: أرشيفات zip، أو وحدات في الذاكرة، أو روابط بعيدة، أو اجتياز مجلد node_modules التقليدي. استراتيجيات مختلفة لبيئات مختلفة، وواجهة واحدة لكود التطبيق.
الأعمال السابقة
حلّت بيئات تشغيل أخرى بالفعل نسخًا مختلفة من هذه المشكلة.
- Deno يحمّل الوحدات من الروابط افتراضيًا ويخزنها مؤقتًا في مجلد مُدار. لا يوجد
node_modulesولا تحليل وحدات يعتمد على نظام الملفات. بيئة التشغيل تتحكم في مكان تخزين الوحدات وكيفيته. - Bun يستخدم ذاكرة تخزين عامة للوحدات مع الروابط الصلبة (hardlinks)، مما يقلل من أعباء نظام الملفات. تحليل الوحدات لديه محسّن بشدة مقارنة بتحليل Node.js.
- واجهة
io/fsفي Go (أُضيفت في Go 1.16) تعرّف واجهة لنظام الملفات تنفذها ملفات النظام، والملفات المضمنة (embed.FS)، وأرشيفات zip، وأنظمة الملفات في الذاكرة. دوال المكتبة القياسية تقبل واجهةfs.FS، مما يجعلها قابلة للاختبار دون ملفات حقيقية. - تجريد NIO
FileSystemفي Java يدعم مزودي أنظمة ملفات قابلين للتوصيل، بما في ذلك التنفيذات في الذاكرة للاختبار، وأنظمة الملفات المعتمدة على zip. - نمط
IFileSystemفي .NET منتشر على نطاق واسع في تطبيقات .NET لتسهيل الاختبار، رغم أنه عُرف مجتمعي وليس ميزة في بيئة التشغيل.
نهج Go مفيد بشكل خاص. بتعريف واجهة صغيرة (Open وRead وStat) واستخدامها في المكتبة القياسية كلها، جعلت Go تجريد نظام الملفات سهلًا بلا كسر للكود الموجود. ويمكن لـ Node.js أن يفعل الشيء نفسه بتعريف واجهة VFS، ثم ترحيل المكتبة القياسية ومحمّل الوحدات تدريجيًا لاستخدامها.
تحدي التوافقية
أكبر عائق أمام VFS في Node.js ليس التنفيذ، بل النظام البيئي. يضم npm أكثر من مليوني حزمة، وجزء كبير منها يصل إلى نظام الملفات بطرق تفترض دلالات POSIX، ومسارات حقيقية، ومجلدات قابلة للكتابة.
نظام VFS يكسر الحزم الموجودة مآله الفشل من البداية. يجب أن يكون اختياريًا ومتوافقًا مع الإصدارات السابقة: إذا لم تُنشئ VFS صراحةً، يعمل كل شيء كما يعمل اليوم. وعند إنشائه، ينبغي أن يعترض استدعاءات نظام الملفات بشفافية، حتى تعمل الحزم السليمة دون تعديل.
«السليمة» هو القيد الأساسي. الحزم التي تنفذ أوامر shell مثل cp أو rm، أو تستخدم إضافات native تستدعي open() الخاصة بـ libc مباشرة، أو تعتمد على /proc أو أنظمة ملفات أخرى خاصة بنظام التشغيل، لن تعمل مع VFS. هذا حد صلب، فأي تجريد يتسرب عندما يتجاوزه الكود نحو النظام الأساسي.
ما الذي يُقترح فعلًا
نقاش VFS في Node.js ليس نظريًا، فهناك مقترحات ملموسة في متتبع المشكلات (issue tracker) الخاص بـ Node.js. يقوم الاتجاه الحالي على عدة أفكار رئيسية.
أولًا، واجهة FileSystemProvider تفوّض إليها وحدة fs العمل. المزود الافتراضي هو نظام الملفات الحقيقي. المزودون المخصصون ينفذون الواجهة نفسها للأنظمة في الذاكرة، أو للقراءة فقط، أو التراكبية (overlay)، أو البعيدة.
ثانيًا، التكامل مع محمّل الوحدات. سيمر require() وimport() عبر VFS للتحليل، مما يتيح استراتيجيات تحليل لا تعتمد على مجلدات node_modules.
ثالثًا، طبقة السياسات. يستطيع VFS فرض ضوابط وصول، فيمنع الكود من القراءة أو الكتابة خارج نطاق محدد. هذا سيمنح Node.js قدرة عزل مدمجة شبيهة بعلامتي --allow-read و--allow-write في Deno.
هل سيصدر هذا في Node.js 24 أو 26 أو لن يصدر أبدًا، يعتمد على إمكانات فريق Node.js وعلى رغبة المجتمع في تغيير يكسر التوافق في طريقة الوصول إلى نظام الملفات. لكن الضغط حقيقي: بيئات التشغيل على الحافة تتوسع، وأهداف نشر WebAssembly تتكاثر، والفجوة بين «JavaScript في كل مكان» و«Node.js تحديدًا على خوادم بأنظمة ملفات حقيقية» تتحول إلى عبء تنافسي.
ما الذي يمكنك فعله اليوم
لا تحتاج إلى انتظار VFS على مستوى بيئة التشغيل للاستفادة من تجريد نظام الملفات. في الكود الجديد، غلّف الوصول إلى نظام الملفات بواجهة. بدلًا من استدعاء fs.readFile مباشرة، أنشئ واجهة FileStore يعتمد عليها كودك. نفذها بنظام الملفات الحقيقي للإنتاج، وبخريطة (map) في الذاكرة للاختبارات. هذا عكس تبعية (dependency inversion) قياسي، غير لامع لكنه فعّال ويتوافق مع أي بيئة تشغيل.
أما للكود الموجود، فتوفر مكتبات مثل memfs وunionfs تنفيذات لأنظمة ملفات في الذاكرة وتراكبية تعدّل وحدة fs في Node برمجيًا. ليست مثالية، فالإضافات native والعمليات الفرعية (child processes) تتجاوزها، لكنها تعمل مع الكود القائم على JavaScript الصرف وتجعل الاختبار أسهل بكثير.
نظام الملفات هو آخر قطعة كبيرة في بنية Node.js التحتية لا تملك حدًا تجريديًا نظيفًا. الـ Streams لها واجهات، وHTTP له واجهات، وحتى محمّل الوحدات فيه hooks. عندما يحظى نظام الملفات بالمعاملة نفسها، سيصبح Node.js بيئة تشغيل محمولة حقًا لا بيئة مخصصة للخوادم. وهذا تغيير يستحق الدفع نحوه.


