العقبات والحلول أثناء نقل pnpm monorepo إلى Nub
2026年7月27日
0
Computing/SoftwareComments (0)
Log in to leave a comment
No posts yet
Log in to leave a comment
No posts yet
منظومة Node.js تعاني من الإرهاق. فبيئة البناء التي تعتمد على طبقات متراكمة من tsx وdotenv-cli وnvm وpnpm أصبحت ثقيلة، وكلما أردت تحديث شيء واحد، تتشابك البرامج النصية (scripts). وعندما ظهرت Nub — وهي مجموعة أدوات متكاملة مكتوبة بلغة Rust لدمج هذه الفوضى من الحزم في ملف تنفيذي ثنائي واحد (single binary) — أكون صريحًا: سعدتُ بالفكرة ولكن خالطني بعض الشك.
وعندما قمتُ بالفعل بتحويل pnpm-workspace monorepo المستخدم في بيئة الإنتاج إلى Nub، وجدتها جذابة للغاية. لكن بالطبع، لم تكن العملية سلسة بدون عقبات؛ حيث ظهرت مشاكل مباشرة في بيئة العمل الحقيقية بدءًا من تعارض الاعتمادات (dependencies)، وسياسة القفل الأمني لمدة 24 ساعة، وحتى مشكلة فصل التخزين المؤقت (cache) في CI.
يتعرف محرك الحزم الخاص بـ Nub والمسمى aube على pnpm-workspace.yaml الحالي وworkspaces في package.json بشكل مباشر. وكما يُذكر في وصفه بأنه متوافق على مستوى البايت مع مخطط pnpm-lock.yaml v9، فإنه يستغل هيكل المتجر الافتراضي الحالي (node_modules/.store/) بشكل أساسي.
المشكلة تكمن في الأدوات القديمة التي تفترض ضمنيًا وجود هيكل node_modules مسطح (hoisted). فعند تشغيلها مباشرة بعد عملية الهجرة، تفشل وتخرج بخطأ MODULE_NOT_FOUND. ولجعل الخدمة تعمل فورًا، عليك البدء بفرد شجرة الحزم باستخدام الخيار --node-linker hoisted.
بناءً على التثبيت الدافئ (Warm install) مع تفعيل التخزين المؤقت، يستغرق وضع GVS الافتراضي 346 مللي ثانية. أما وضع Hoisted فيستغرق 1461 مللي ثانية، وهو أبطأ بأكثر من 4 أضعاف، ولكنه يظل أسرع بأكثر من مرتين مقارنة بـ 3453 مللي ثانية في pnpm v10+. في البداية، من الأسهل لك ضمان التوافق باستخدام وضع Hoisted، ثم الانتقال إلى وضع GVS الافتراضي بعد التخلص من الاعتمادات على الأدوات القديمة.
`bash
#!/usr/bin/env bash
set -euo pipefail
echo "==> [1/4] التحقق من ثنائي Nub"
if ! command -v nub &> /dev/null; then
echo "Error: Nub غير موجود. يرجى تشغيل 'npm i -g @nubjs/nub' أولاً."
exit 1
fi
echo "==> [2/4] التحقق من مخطط pnpm-lock.yaml"
if [ -f "pnpm-lock.yaml" ]; then
nub pm use nub
fi
echo "==> [3/4] تحديد محرك التشغيل في package.json"
node -e '
const fs = require("fs");
const pkg = JSON.parse(fs.readFileSync("package.json", "utf8"));
pkg.devEngines = pkg.devEngines || {};
pkg.devEngines.packageManager = {
name: "nub",
version: "^0.4.0",
onFail: "warn"
};
fs.writeFileSync("package.json", JSON.stringify(pkg, null, 2) + "\n");
'
echo "==> [4/4] إنشاء Lockfile"
nub install --frozen-lockfile=false
`
باستخدام هذا البرنامج النصي لتحويل pnpm-lock.yaml إلى nub.lock، يمكنك توفير أكثر من ساعتين أسبوعيًا من الوقت الذي كان يضيع في إصلاح الأخطاء الناتجة عن تعديل السكريبتات في كل مرة.
تتميز Nub بسياسة أمنية صارمة للغاية؛ حيث تحظر سكريبتات دورة الحياة افتراضيًا، وترمي الخطأ ERR_NUB_TRUST_DOWNGRADE إذا لم يكن هناك توقيع npm Provenance. والشيء الأكثر إرباكًا هو الخيار minimumReleaseAge؛ حيث يعتبر إصدارات الحزم التي لم يمر على نشرها 24 ساعة في حالة انتظار للتحقق الأمني، ويمنع تثبيتها.
في الأوقات العادية، تعد هذه درعًا واقيًا ممتازًا، ولكن عندما تظهر ثغرة يوم الصفر (zero-day) وتحتاج إلى نشر إصدار إصلاحي صدر قبل ساعة واحدة فقط، تصبح هذه السياسة عائقًا كبيرًا. في هذه الحالة، يجب عليك تحديد الحزم المسموح ببرامج البناء الخاصة بها في package.json الرئيسي وتجاوز التقييد عبر الأوامر.
`json
{
"name": "@org/monorepo-root",
"private": true,
"allowBuilds": {
"esbuild": true,
"sharp": true,
"@fast-cve/patch-pkg": true
}
}
`
تدفق العمل لتجاوز المحظر وتطبيق حزمة الإصلاح العاجل يكون كالتالي:
`bash
nub add --allow-build=@fast-cve/patch-pkg @fast-cve/patch-pkg@1.0.1-hotfix
nub approve-builds
`
عند تشغيل nub approve-builds، يتم رفع الحظر ومتابعة عملية البناء فورًا. أما الحزم المسجلة في قاعدة بيانات OSV كبرمجيات خبيثة (MAL-*)، فلن يتم تجاوزها حتى بهذا الأمر، وفي تلك الحالة يتعين عليك البحث عن إصدار بديل أعلى.
لرفع سرعة CI/CD، تحتاج إلى معرفة مسارات التخزين المؤقت لـ Nub. يتم تخزين ملفات Node الثنائية في ~/.cache/nub/node/<version>/، بينما ينقسم متجر الحزم القابل للعنونة بالمحتوى (CAS) بين ~/.cache/nub/ وnode_modules/.store/.
في بناء Docker متعدد المراحل، يجب استخدام نقطة تثبيت التخزين المؤقت (cache mount) الخاصة بـ BuildKit لعزل طبقة التثبيت تمامًا للاستفادة من التخزين المؤقت.
`dockerfile
FROM ghcr.io/nubjs/nub:latest AS base
WORKDIR /app
FROM base AS dependencies
COPY package.json nub.lock pnpm-workspace.yaml ./
COPY packages/core/package.json ./packages/core/
COPY packages/api/package.json ./packages/api/
RUN --mount=type=cache,target=/root/.cache/nub
nub ci --prefer-offline
FROM dependencies AS builder
COPY . .
RUN nub run build --filter=@org/api
FROM node:22-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
COPY --from=builder /app/packages/api/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
EXPOSE 3000
CMD ["node", "dist/index.js"]
`
في GitHub Actions، يتم استخدام nubjs/setup-nub@v0 بدلاً من actions/setup-node.
`yaml
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: nubjs/setup-nub@v0
with:
cache: true
- run: nub ci
- run: nub -r run build
- run: nub -r run test
`
تصل سرعة nub ci عند دمج setup-nub مع التخزين المؤقت إلى حوالي 346 مللي ثانية. مقارنة بـ pnpm التقليدي (3453 مللي ثانية)، ينخفض وقت تثبيت الحزم في أنبوب التوصيل (pipeline) بشكل كبير، مما يقلل إجمالي وقت بناء CI إلى النصف. وبالتأكيد ستشعر بالرضا عند الاطلاع على فاتورة البنية التحتية السحابية في نهاية الشهر.
يتضمن Nub مترجمًا مدمجًا للذاكرة (in-memory transpiler) يعتمد على oxc، مما يسمح بتشغيل كود TypeScript مباشرة دون الحاجة إلى tsx أو ts-node. كما أنه يقرأ ملفات .env تلقائيًا. ومع اختفاء حمل التمهيد الإضافي (bootstrapping overhead) لعملية Node.js الذي كان يظهر في كل مرة عند استخدام pnpm run (442.7 مللي ثانية)، ينخفض وقت تنفيذ السكريبت مع nub run إلى 14.7 مللي ثانية، وهو فارق ملموس للغاية.
ولمنع تشتت إصدارات Node.js بين أعضاء الفريق، يكفي وضع ملف .node-version في المجلد الرئيسي.
`bash
echo "22.15.0" > .node-version
nub src/index.ts
`
في Node 22.15.0 وما فوق، يختفي تأخير التشغيل البارد (cold start) تمامًا بفضل module.registerHooks() التزامني. وعند انضمام عضو جديد للفريق، فإن إعداد سكريبت أتمتة واحد هو أطرق طريقة لضبط البيئة دفعة واحدة دون الحاجة لشروحات معقدة.
`bash
#!/usr/bin/env bash
set -euo pipefail
echo "==> بدء إعداد بيئة التطوير"
if ! command -v nub &> /dev/null; then
if command -v brew &> /dev/null; then
brew install nubjs/tap/nub
else
npm install -g --ignore-scripts=false @nubjs/nub
fi
fi
nub pm shim
nub node install
nub install
if [ ! -f ".env.local" ] && [ -f ".env.example" ]; then
cp .env.example .env.local
fi
echo "==> اكتمل الإعداد. قم بالتجميع عبر 'nub run dev'."
`
بمجرد تشغيل nub pm shim، حتى لو قمت بكتابة pnpm install أو npm run بحكم العادة، سيتم اعتراض الأمر ومعالجته عبر مشغل Nub. وبالإضافة إلى تقليل الحمل الإضافي لتنفيذ CLI من pnpm exec (191 مللي ثانية) إلى nubx (11 مللي ثانية)، فإن هذا يوفر عليك ساعتين أسبوعيًا على الأقل من وقت تصحيح الأخطاء واستكشاف الأعطال الناتجة عن مقولات مثل "الأمر لا يعمل على جهوزي".