انتقل إلى المحتوى

الشيفرة المشتركة

منصّات المنظومة مختلفة من الخارج، واحدة من الداخل. يرى المستخدم لكلّ منصّة علامتها التجارية ونطاقها وخدماتها الخاصّة — ومع ذلك يأتي أكثر من 90% من الشيفرة من مصدر واحد.

تشرح هذه الصفحة كيف تُوزَّع تلك الشيفرة المشتركة.

لماذا صار ذلك ضروريًا

في البداية كانت كلّ منصّة تُنسَخ من القالب. والنتيجة أنّ إصلاحًا واحدًا كان يجب تكراره يدويًا في 8 مستودعات: يكفي أن يُنسى واحد حتى تتخلّف تلك المنصّة بصمت.

أظهر القياس أنّ ~95% من الواجهة الأمامية مشترك فعلًا؛ أمّا الاختلاف الحقيقي فكان نحو 40 سطرًا تحمل اسم العلامة التجارية. أي أنّ التكرار لم يكن متطلَّبًا تقنيًا، بل إرثًا من النسخ.

ثلاث آليات

تنتقل الشيفرة المشتركة بطرق مختلفة بحسب الطبقة. ولا واحدة منها «نسخ ولصق» — جميعها مُصدَّرة بإصدارات وقابلة للتراجع.

الطبقة الشكل الآلية
نواة الواجهة الخلفية وحدة Go تبعية في go.mod
طبقة الواجهة الأمامية حزمة npm تبعية في package.json
هيكل المنصّة تاريخ git git merge + مزامنة تلقائية يومية

1. نواة الواجهة الخلفية — وحدة Go

المصادقة، والتحكّم بالوصول حسب الأدوار (RBAC)، وبوّابة API، والتدقيق، وخطّ الذكاء الاصطناعي، وتكامل eID/SSO — كلّ ذلك في وحدة Go واحدة. وعادةً ما يقع ملفّ main.go للمنصّة في نحو 30 سطرًا: تشغيل النواة ثمّ إضافة المسارات الخاصّة بتلك المنصّة.

للنواة الآن طبقة مباشرة واحدة:

  • open-gerege-core — الأساس المفتوح الذي تستهلكه مباشرةً جميع خلفيات الخطّ الحكومي وخطّ Gerege.

حتى 2026-08-02 كانت private-gerege-core طبقة وسيطة. ولأنها لم تتضمن منطقًا إضافيًا أو ترحيلات، أزيلت من السلسلة وأُرشفت. يبقى المنطق التجاري في مستودع كل منتج.

2. طبقة الواجهة الأمامية — @gerege/ui-core

المشكلة نفسها التي حلّتها النواة في الخلفية، حُلَّت من جديد في الواجهة الأمامية. تحتوي الحزمة على:

  • lib/** — عميل API، ومساعدات BFF، وقاموس التعدّد اللغوي، والسمة، والجلسة،
  • components/** — الهيكل، ولوحة الإدارة، ومنطقة المستخدم، وeID، والبوّابة،
  • api/** — منطق 158 مسار BFF.

تُنشر الحزمة بصيغة شيفرة TypeScript المصدرية (غير مبنيّة)، فيبنيها التطبيق المستهلك عبر transpilePackages في Next.js. أمّا التوزيع فعبر حزمة tarball مفتوحة على HTTPS: لا تتطلّب مصادقة، وتعمل داخل بناء Docker أيضًا.

لماذا تحتفظ مسارات BFF بغلاف

يسجّل Next.js المسارات عبر نظام الملفّات، لذا يحتفظ كلّ تطبيق بإعادة تصدير من سطر واحد لكلّ مسار:

// src/app/api/org/[id]/route.ts
export { GET, PUT, DELETE } from '@gerege/ui-core/api/org/[id]';
export const dynamic = 'force-dynamic';

يمكن اختزال الملفّات الـ158 في مسار شامل واحد [...path]، لكنّ ذلك سيُلغي قائمة سماح أمنية: فقائمة المسارات هي التي تحدّد أيّ مسارات الخلفية يمكن للمتصفّح بلوغها أصلًا. الغلاف ثمن مقصود.

3. هيكل المنصّة — الوراثة عبر git

ما لا ينتمي إلى الحزمة (بنية الصفحات، وglobals.css، وإعداد النشر) يُورَث من القالب عبر git merge. وتجلب مزامنة يومية تغييرات القالب الأعلى وتفتح طلب سحب في مستودعات التطبيقات — فكلّ تغيير يصل إلى الإنتاج يمرّ بمراجعة بشرية.

أمّا الملفّات التي يجب أن تبقى ملكًا لكلّ منصّة (العلامة، والنشر، والتكامل المستمر، والتوثيق) فمحميّة بـ merge=ours في .gitattributes.

merge=ours لا يحمي من التغييرات أحادية الجانب

فذلك المشغّل يحلّ التعارضات فقط. وإذا حذف القالب الأعلى ملفًّا، تبِعه الدمج — ولا يُستدعى المشغّل إطلاقًا. الحماية الحقيقية أن يكون لكلّ ملفّ علامة أو إعداد محتوى مختلف على الجانبين.

ما يبقى ملكًا للمنصّة

في الحزمة / النواة ملك المنصّة
lib/**، components/**، منطق BFF brand.config.ts — الاسم والنطاق والألوان وعنوان التوثيق
المصادقة، وRBAC، والبوّابة، والتدقيق components/landing/** — النصّ التسويقي
تكامل eID / SSO app/**/page.tsx — تسجيل المسارات (أغلفة رفيعة)
قاموس التعدّد اللغوي المشترك (846 مفتاحًا × 7 لغات) lib/<platform>I18n.ts — مصطلحات المنصّة
بنية القائمة (AppShell) nav.config.ts — أيّ الأقسام تخدمها المنصّة
app/globals.css — رموز ألوان العلامة
deploy/**، .github/** — النشر والتكامل المستمر

لماذا تبقى مصطلحات المنصّة داخل التطبيق

القاعدة: القاموس المشترك لا يعرف إلّا السطح المشترك. فالكلمات التي تخصّ منصّة بعينها — مفردات IBAN وكشوف الحساب في المحفظة، وكتالوج API في بوّابة المطوّرين، ومصطلحات عمليات الأعمال في Ring — مكانها قاموس التطبيق نفسه.

والسبب هو الكلفة: لو وُضعت مصطلحات Ring الـ1,104 في القاموس المشترك لحملها kiosk وPOS والمحفظة جميعًا، ومع كلّ لغة تُضاف تتضاعف تلك الكلفة سبع مرّات.

ويتّبع التنفيذ النمط نفسه في كلّ مستودع:

// lib/walletI18n.ts — 15 مصطلحًا للمحفظة × 4 لغات
export function useWalletT() {  }   // ارتداد إلى الإنجليزية في اللغات غير المترجَمة

وإذا كان المكوّن يمرّر T إلى المكوّنات الفرعية بوصفه خاصيّة (prop)، فإنّ تقسيمه إلى دالّتين (T + wt) يعني تقسيم كلّ خاصيّة كذلك. وفي تلك الحالة يُكتب محلّل واحد: إن كان المفتاح خاصًّا بالمنصّة أُخذ من قاموسها، وإلّا فمن الحزمة (lib/lang.ts في ring-dgov).

القائمة — البنية مشتركة والخدمة خاصّة بالمنصّة

تنظيم AppShell واحد في كلّ المنصّات (مشرف أعلى · مشرف · مدير · مواطن)، غير أنّ كلّ منصّة لا تنفّذ منه إلّا مجموعة جزئية: فالمحفظة بلا وحدات gateway أو relay أو السجلّات.

الإعداد الغرض
navRoutes المسارات التي يخدمها التطبيق فعليًا؛ وبها تُرشَّح القائمة
navSystemLabels أسماء الأنظمة على الشريط الجانبي (me → «المحفظة»)
navExtra عناصر القائمة الموجودة في تلك المنصّة وحدها (21 عنصر BPM في Ring)

navExtra يأتي من مكوّن CLIENT

UiCoreProvider مكوّن client يُستدعى من root layout على الخادم. أمّا أيقونات القائمة (مكوّنات React) ودوالّ التسميات فلا تعبر حدّ server→client. لذلك ينشئ التطبيق غلافًا رفيعًا من نوع client ويمرّرها من داخله:

// src/nav.config.tsx
'use client';
export default function AppNav({ children }) {
  return <UiCoreProvider navExtra={NAV_EXTRA}>{children}</UiCoreProvider>;
}

وإذا وُسم في الحزمة عنصر قائمة لا تخدمه إلّا منصّات قليلة بـ optIn: true، لم يظهر إلّا على المنصّات التي ذكرته صراحةً في navRoutes.

ثلاث بوّابات آليّة

تولّد الشيفرة المشتركة ثلاثة أنواع مختلفة من التبعية. ويظهر كلّ نوع منها بصورة مختلفة عند العطب، ولذلك فالبوّابات ثلاث كذلك:

التبعية البوّابة ماذا يحدث عند العطب
شيفرة الحزمة ← شيفرة التطبيق tsc يفشل البناء — ويظهر فورًا
مسار الحزمة ← غلاف BFF في التطبيق check-routes تختفي نقطة النهاية بصمت
صنف الحزمة ← CSS التطبيق check-styles تفقد الشاشة تنسيقها بصمت
  • check-brand — يفشل البناء إذا ظهر اسم منصّة في الشيفرة خارج brand.config.ts. واسم المنصّة نفسها يُقرأ من brand.config.ts، فلا تتقادم القائمة يدويًا.

    ماذا التقطت البوّابة

    كانت صفحة الدخول تعرض تسجيل الدخول عبر «Gerege SSO (sso.gerege.mn)» بينما منصّات الخطّ الحكومي تحوّل فعليًا إلى sso.dgov.mn. وصار المضيف الآن يُقرأ من SSO_ISSUER في الواجهة الخلفية.

  • check-routes — تشترط وجود غلاف في التطبيق لكلّ مسار في الحزمة. وبدونه ستختفي أيّ نقطة نهاية جديدة في الحزمة بصمت على تلك المنصّة (فالمنطق غير مرئي داخل التطبيق، ولا يبدو شيء معطوبًا).

    البوّابة لا تُثبت صحّة EXCLUDE

    إذا قُصد ألّا يُفتح مسار ما كُتب في EXCLUDE — فيصير الفارق ظاهرًا. لكنّ البوّابة لا تلتقط المُدخَل الخاطئ. وهكذا أُغلق public/languages على إحدى المنصّات ففرغ مبدّل اللغات: كلّ البوّابات خضراء والشاشة معطوبة.

  • check-styles — الحزمة لا تتضمّن CSS: فالتنسيق يقع في globals.css بكلّ مستودع. وحين تسمّي الحزمة صنفًا جديدًا، أو يتقادم CSS المستودع، يفقد المكوّن تنسيقه بصمت — فيبقى الزرّ بزخرفة المتصفّح الرمادية الافتراضية ويفقد الجدول حدوده. وهذه البوّابة تقابل قيم className في الحزمة بـ CSS المستودع.

إدارة الإصدارات

تتبع الآليات الثلاث semver. وعند صدور إصدار جديد يفتح Dependabot طلب سحب في المستودعات المستهلِكة؛ أمّا التحديث نفسه فتغيير من سطر واحد في go.mod أو package.json.

رفع إصدار القالب لا يكفي

لمّا كانت كلّ منصّة ترث من القالب، يُظنّ أنّ «رفع القالب ينشر التحديث إلى الجميع». والواقع أنّ نوعَي التبعية يعملان بصورة مختلفة:

الملفّ merge=ours؟ هل ينتشر من القالب
backend/go.mod نعم ❌ أبدًا
frontend/package.json لا ✅ ينتشر

وحماية go.mod متطلَّب بنيوي: فسطر module يختلف في كلّ مستودع (…/gerege-app-mn/backend مقابل …/wallet-gerege-mn/backend)، فيتعارض كلّ دمج عند السطر الأوّل. ولذلك فرفع إصدار نواة الواجهة الخلفية في القالب لا ينشر شيئًا — بل يلزم طلب سحب في كلّ مستودع على حدة.

كما أنّ شجرة الوراثة من ثلاث درجات (public template → private template → التطبيق)، فحتى الملفّ القادر على الانتشار يقطع عدّة دورات مزامنة قبل أن يبلغ الأوراق.

التغيير الكاسر يوقف طلبات سحب التبعيات

توسيع القاموس من أربع لغات إلى سبع كسر كلّ موضع كُتب فيه Record<Lang, …>. فصار كلّ طلب سحب من Dependabot يفشل في tsc، ولم يدمجه أحد، وتراكم التالي فوقه — فتفرّق الأسطول من v0.4.0 إلى v0.10.2.

ولذلك عند إجراء تغيير كاسر في الحزمة: (أ) اكتب دليل الترحيل في ملاحظات الإصدار، (ب) أصدر الإصلاح في المستودعات المستهلِكة في الوقت نفسه. فالتحديث الآلي يحتاج خطوة يدوية مع التغييرات الكاسرة.

التخلّف يحدث بصمت

إذا تراكمت طلبات سحب التبعيات تفرّقت المنصّات على إصدارات مختلفة، وانكسر الوعد بأنّ «إصلاحًا واحدًا يصل إلى الجميع». وإغلاق تلك الطلبات بانتظام شرط تشغيلي لهذه البنية، لا رفاهية اختيارية.

ذات صلة