تُرجمت هذه الصفحة آليًا وقد تحتوي على أخطاء. اقرأ الأصل الإنجليزي

التوثيق

خادم التطوير وتصحيح الأخطاء

أثناء التطوير، يبني NSH تطبيقك عند كل حفظ. يُبنى تطبيق Rust (يسمّي nsh.json الـ "root" الخاص به) في مشغّل خاص به يبدأه nsh run؛ وأي تغيير في الـ crate الخاص به يعيد بناءه ويعيد تشغيل التطبيق. أما تطبيق TSX فيُدفع إلى كل مضيف متصل، ويتحدّث في مكانه. يتولى ذلك أمران:

  • nsh run يبني التطبيق ويشغّله: نافذة Mac Catalyst أو AppKit، أو نافذة GTK على Linux، أو نافذة Win32 على Windows، أو محاكي iOS، أو جهاز Android أو محاكيه، أو صفحة في المتصفح. وفي حالة TSX يبدأ أيضًا خادم التطوير الذي يتصل به مضيف التطوير.
  • nsh dev --server-only يبدأ خادم التطوير لتطبيقات TSX، للمضيفات التي تشغّلها بنفسك: تطبيقك الخاص مع حزمة SDK للتطوير، أو جهاز على الشبكة، أو عدة مضيفات في وقت واحد.
# A Rust app, from its directory
nsh run                             # AppKit window (macOS); GTK window (Linux); Win32 window (Windows)
nsh run --target ios                # iOS simulator
nsh run --target android            # Android device or emulator

# A TSX app
nsh run App.tsx                     # AppKit window (macOS); GTK window (Linux); Win32 window (Windows)
nsh run App.tsx --target macos      # AppKit window (macOS)
nsh run App.tsx --target ios        # iOS simulator
nsh run App.tsx --target android    # Android device or emulator
nsh dev App.tsx --server-only       # server only

nsh dev يفتح أيضًا المضيف الأصلي افتراضيًا؛ وتستخدم المضيفات على الجوال --target ios أو --target android.

#nsh run

الخيارالوصف
--target <catalyst | ios | android | macos | linux | windows | gpui | web>مكان التشغيل. الافتراضي macos على macOS (AppKit)، وlinux على Linux، وwindows على Windows. يعمل تطبيق Rust على جميعها عدا gpui، الذي يضمّن جذره في تطبيق GPUI (GPUI).
--device <name or UDID>محاكي iOS المراد استخدامه. الافتراضي: محاكٍ قيد التشغيل، وإلا فأحدث iPhone (يُشغَّل لك).
--port <port>منفذ خادم التطوير. الافتراضي 8790؛ ويُستخدم أول منفذ حر تالٍ إذا كان مشغولًا.
--releaseتقديم إصدارات release من TSX (مصغّرة، بلا معلومات تصحيح، ولا تحديثات فورية).
--traceتسجيل زمن كل commit على المضيف (تطبيقات TSX).

ما يفعله كل هدف:

  • catalyst (macOS) يشغّل مضيف التطوير كتطبيق Mac. إيقاف nsh run (Ctrl-C، أو kill) يغلقه، وإغلاق النافذة ينهي nsh run.
  • macos (macOS) يشغّل مضيف التطوير AppKit، وهو تطبيق سطح مكتب في نافذة بحجم 1200×800، بالطريقة نفسها؛ ويعيد Cmd+R تحميل أحدث بناء. يتطلب macOS 14 أو أحدث (macOS).
  • linux (Linux) يشغّل مضيف التطوير GTK في نافذة بحجم 390×844، بالطريقة نفسها: إيقاف nsh run يغلقه، وإغلاق النافذة ينهي nsh run. يتطلب GTK 4.14 أو أحدث (Linux).
  • windows (Windows) يشغّل مضيف التطوير Win32 في نافذة بحجم 1200×800، بالطريقة نفسها؛ ولا يبقى المضيف حيًا بعد nsh run حتى لو أُغلقت وحدة التحكم. يتطلب Windows 10 الإصدار 1809 أو أحدث (Windows).
  • ios يثبّت مضيف التطوير في المحاكي، ويشغّله موجّهًا إلى خادم التطوير، ويواصل المراقبة. يشارك المحاكي شبكة Mac، ولذلك يصل المضيف إلى الخادم عبر 127.0.0.1. إيقاف nsh run (Ctrl-C) يغلق المضيف، وإلا انضم إلى الجلسة التالية. يتطلب Xcode.
  • android يثبّت APK مضيف التطوير (مرة أخرى فقط عند تغيّر وقت التشغيل)، ويشغّل adb reverse tcp:<port> tcp:<port> ليصل 127.0.0.1 الخاص بالجهاز إلى الخادم، ثم يبدأ المضيف. يعمل مع محاكٍ أو جهاز USB. يتطلب Android SDK (ANDROID_HOME). وعندما ينتهي nsh run (بما في ذلك عبر Ctrl-C) يوقف المضيف ويزيل إعادة توجيه المنفذ تلك، فلا ينضم المضيف إلى الجلسة التالية.

وعندما ينتهي nsh run، أيًا كانت طريقة انتهائه، يوقف المضيف الذي بدأه: يُنهي تطبيق Catalyst أو يغلق مضيف macOS أو Linux أو Windows، أو ينهي مضيف التطوير في المحاكي، أو يفرض إيقافه على الجهاز.

في تطبيق Rust، يشغّل كل هدف المشغّل الخاص بالتطبيق، المبني مع الـ crate الخاص به: على أجهزة سطح المكتب بناء debug في مكانه مع إخراجه في الطرفية؛ وعلى iOS وAndroid حزمة منه تُثبَّت. أما في TSX فالمضيفات عامة: تشغّل أي تطبيق NSH يرسله الخادم، فلا شيء يُبنى لكل تطبيق (والتطبيق الذي له crate خاص به تُبنى له مضيفات معه).

#nsh dev

nsh dev App.tsx
✓ built App.tsx (3.2 KB js, 9.8 KB bytecode, 21ms)
→ dev server ws://127.0.0.1:8790/ws
  devices on this network: ws://192.168.1.20:8790/ws
الخيارالوصف
--port <port>منفذ خادم التطوير. الافتراضي 8790 (وأول منفذ حر تالٍ إذا كان مشغولًا).
--releaseتقديم إصدارات release.

صِل المضيفات بالعنوان المطبوع:

  • تطبيق iOS الخاص بك، المربوط مع NativeShellDev.xcframework: try app.connectDevServer(URL(string: "ws://192.168.1.20:8790/ws")!). راجع التضمين في تطبيقات iOS.
  • تطبيق Android الخاص بك، مع nsh-dev.aar: nsh.connectDevServer("ws://192.168.1.20:8790/ws")، أو adb reverse tcp:8790 tcp:8790 وws://127.0.0.1:8790/ws. راجع التضمين في تطبيقات Android.

يمكن توصيل أي عدد من المضيفات في وقت واحد؛ ويذهب كل بناء إليها جميعًا. والمضيف الذي يتصل لاحقًا يتلقى البناء الحالي فورًا. أما المضيف الذي لا يستطيع تشغيل التطبيق لأنه يفتقر إلى مجموعة UI يستوردها التطبيق (مضيف تطوير مبني لتطبيق آخر) فلا يتلقى بناءً: يعرض السبب، وتطبع الطرفية سطر warn مع الأمر الذي يثبّت مضيف التطبيق.

#تعديلات Rust

Rust لغة مُصرَّفة: أي تغيير في crate التطبيق، أو في crate يعتمد عليه بمسار، يعيد بناء التطبيق، ويعيد nsh run تشغيله بالشيفرة الجديدة. وتبدأ حالة التطبيق من جديد؛ فاحتفظ بما يجب أن يبقى بعد إعادة التشغيل في التخزين. يُطبع خطأ البناء في الطرفية، ويعمل التطبيق مجددًا عند أول حفظ يُصرَّف بنجاح. وفي تطبيق يجمع Rust وTSX، تبقى تعديلات TSX تحديثات فورية (أدناه) وتعيد تعديلات Rust تشغيل التطبيق.

#استبدال الوحدات الفوري (Hot module replacement)

تحتفظ إصدارات التطوير من TSX بوحدة واحدة لكل ملف مصدر. عند الحفظ، يعيد NSH البناء، ويقارن الوحدات بآخر بناء سليم، ويرسل رقعة تضم الوحدات التي تغيّرت فقط. يطبّقها المضيف في مكانها في أقل بكثير من ميلي ثانية؛ ويستغرق الأمر من حفظ الملف إلى رؤية التغيير نحو 120 ms.

ما يحدث لتطبيقك:

  • تُعاد معالجة المكوّنات التي عدّلتها فقط. كل مكوّن من المستوى الأعلى (دالة تبدأ بحرف كبير وتُرجع JSX) يُغلَّف بوكيل ثابت. وعندما تتغير شيفرته (أو بقية شيفرة وحدته التي قد يستخدمها)، تُعاد معالجة نسخه بالشيفرة الجديدة؛ أما المكوّنات التي لم تتغير شيفرتها فتحتفظ بنسخها، ومعها إشاراتها (signals) وحالتها. عدّل نمط زر وستبقى الشاشة حيث كانت، بالنص المُدخل نفسه وموضع التمرير نفسه.
  • تعمل الوحدات التي تغيّرت مرة أخرى، وكذلك الوحدات التي تستورد وحدة متغيّرة ولا تستطيع قبول التحديث بنفسها، وصولًا إلى نقطة الدخول. ولذلك تعمل شيفرة مستوى الوحدة مرة أخرى، باستثناء ما يحتفظ بحالته:
  • تحتفظ الإشارات ومكدسات التنقل من المستوى الأعلى بحالتها: يحتفظ const الذي أنشأه signal(…) أو createNavStack(…) في أعلى الوحدة بقيمته (والمكدس بصفحاته) عبر التعديلات، ما دام مُهيّئه نفسه لم يتغير؛ غيّر المُهيّئ (signal(0) إلى signal(10)) لتبدأ من جديد.
  • تحتفظ كائنات context() من المستوى الأعلى بهويتها، وتأخذ القيمة الافتراضية الجديدة، فيبقى المزوّدون والمستهلكون في وحدات مختلفة متطابقين.
  • تعديل نقطة الدخول يُبقي التطبيق مركّبًا. إذا عمل mount(App) (أو mount(() => <App />)) مرة أخرى بالمكوّن نفسه، فإنه يحتفظ بما ركّبه: تُعاد معالجة المكوّنات التي عدّلتها فقط. وإذا عدّلت App نفسه أُعيدت معالجته مع ما يعرضه (وتبدأ حالة هذه المكوّنات من جديد)، لكن المتنقّل (navigator) المُنشأ في أعلى الوحدة يحتفظ بمساراته، فتبقى الشاشة المدفوعة مدفوعة. أما استدعاء mount تغيّر (مكوّن آخر، أو props) فيركّب التطبيق من جديد.
  • المكوّن الذي يرمي استثناءً يبلّغ عن الخطأ ولا يعرض شيئًا حتى تصلحه؛ ويواصل بقية التطبيق العمل. ويتعافى عند أول حفظ سليم.

يعيد المضيف تحميل التطبيق بالكامل بدل الترقيع عندما:

  • يستورد التغيير وحدة NSH لم يستوردها التطبيق العامل بعد (مثل أول import … from "nsh")؛
  • يتغير capabilities الخاص بالتطبيق في nsh.json؛
  • لا يشغّل المضيف البناء الذي تستند إليه الرقعة (اتصل للتو، أو فشل تحميل سابق)؛
  • تتعذّر إمكانية تطبيق الرقعة.

يقدّم --release إصدارات release دون تحديثات فورية: كل حفظ يعيد تحميل التطبيق. استخدمه لفحص البناء المصغّر. وأي شيء يُحمَّل من خادم تطوير، سواء --release أم لا، يتجاوز تحصين وقت التشغيل (ويبقى eval متاحًا)؛ راجع العزل والصلاحيات.

#السجلات

يسجّل تطبيق Rust باستخدام crate ‏log (log::info!، log::warn!، log::error!): يعرض nsh run الأسطر في طرفيته، مع إخراج التطبيق، على أجهزة سطح المكتب وفي محاكي iOS.

في TSX، تذهب console.log وconsole.info وconsole.debug وconsole.warn وconsole.error إلى السجل الأصلي مع الهدف js. وما دام مضيف متصلًا، تُرسل أسطر سجله إلى الطرفية التي تشغّل nsh run / nsh dev: أسطر console.log كما هي (مسبوقة باسم الجهاز عند اتصال عدة أجهزة)، والتحذيرات والأخطاء بعد اسم الجهاز:

• iPhone 16 Pro connected, build 1
loading more after 1000
warn iPhone 16 Pro: deprecated option
error iPhone 16 Pro: uncaught exception: TypeError: cannot read property 'id' of undefined
          at Row (demos/Photos.tsx:18:22)

يُنسّق console.log القيم كما تفعل وحدة تحكم المتصفح: السلاسل النصية كما هي، والأخطاء مع مكدسها، والدوال باسمها، والكائنات والمصفوفات بصيغة JSON. وعندما لا يكون المضيف متصلًا، تذهب السجلات إلى سجل المنصة (وحدة تحكم Xcode / السجل الموحّد على iOS، وadb logcat على Android).

ويُسجَّل الوعد (promise) المرفوض الذي ما يزال بلا معالج بعد انتهاء العمل الذي رفضه (أي بعد تشغيل microtasks الخاصة به) على أنه خطأ أيضًا، كما يبلّغ عنه المتصفح: دالة async ترمي استثناءً بينما لا ينتظرها أحد، أو Promise.reject(…) لا يلتقطه أحد، أو استدعاء إلى Rust يفشل دون أن ينتظره شيء. يذهب السطر إلى حيث يذهب console.error (الطرفية، أو سجل المنصة، أو وحدة تحكم المتصفح على الويب)، ومكدسه يشير إلى مصادرك:

error iPhone 16 Pro: unhandled rejection: Error: HTTP 404
          at loadProfile (App.tsx:12:11)

المعالج الذي يُرفق لاحقًا في الدورة نفسها (const p = load(); …; p.catch(…)) يُحتسب معالجةً للرفض. استخدم await مع الوعد أو أرفق به .catch حيث يهمّ الفشل.

أما بقية الطرفية فسطر واحد لكل خطوة (✓ اكتمل، → مكان الاتصال، • ما فعله جهاز)؛ ولا يُطبع إخراج الأدوات التي يشغّلها NSH (cargo والمضيفات نفسها) إلا عند فشل إحداها. ويطبع nsh run -v (أو NSH_VERBOSE=1) كل شيء: كل سطر سجل مع مستواه ومصدره، وإخراج cargo والمضيفات نفسها.

ويضيف --trace كلفة كل commit:

perf commit: 10 ops, 151µs (list js 2µs), 8 mount items, 27 nodes

راجع الاختبار لمعرفة ما تعنيه هذه الأرقام.

#شاشة الأخطاء

تُظهر هذه الأخطاء طبقة حمراء فوق التطبيق على المضيف، مع الرسالة والمكدس:

  • فشل في البناء (خطأ في الصياغة، أو عنصر غير معروف، أو استيراد مفقود)، مع إطار شيفرة يشير إلى السطر؛
  • خطأ أثناء تقييم الحزمة (bundle)؛
  • استثناء غير ملتقط في معالج حدث أو مؤقّت أو effect.

اضغط على الطبقة لإغلاقها. وتختفي أيضًا عند أول إعادة تحميل ناجحة. وتُطبع أخطاء البناء في الطرفية كذلك:

error building App.tsx failed
App.tsx

  x <Buton> names nothing in scope
   ,-[3:11]
 2 | function App() {
 3 |   return <Buton />;
   :           ^^^^^
 4 | }

يزيل المجمّع (bundler) أنواع TypeScript دون فحصها؛ شغّل nsh check (المصرّف الأصلي لـ TypeScript مع كتابات tsconfig.json nsh init؛ ولا حاجة إلى Node) أو اعتمد على محررك لأخطاء الأنواع.

في إصدارات release (تطبيق مُضمِّن مع SDK الخاص بـ release، أو تطبيق مُحزَّم)، تُسجَّل الأخطاء وتُمرَّر إلى معالج الأخطاء في التطبيق المضيف (NshAppView.onError، NshView.setErrorListener) ولكن لا تُعرض فوق التطبيق.

#خرائط المصدر ومكدسات الاستدعاء

ينتج التجميع خريطة مصدر (source map) تركّب كل خطوة (TypeScript ← JSX ← الحزمة)، وتُرسل مع كل بناء. وقبل طباعة مكدس الاستدعاء أو عرضه، يعيد وقت التشغيل كتابة مواضع الحزمة إلى مصادرك، فتشير المكدسات إلى App.tsx:2:28 لا إلى الحزمة:

error Mac Catalyst: uncaught exception: Error: tap boom
          at <anonymous> (App.tsx:2:58)

يكتب nsh build الخريطة باسم bundle.js.map؛ والتطبيق المُضمِّن الذي يحمّل حزمة من مجلد يقرؤها من جوار الحزمة، وتحملها الحزم الموقّعة أيضًا، ما لم تُحفظ بجوار الحزمة وتُطبَّق باستخدام nsh symbolicate (أخطاء الحزم المنشورة).

#نصائح لتصحيح الأخطاء

  • لا شيء يتحدّث بعد تعديل: تحقق من الطرفية بحثًا عن خطأ في البناء؛ وتعرضه الطبقة الحمراء على المضيف أيضًا.
  • يحتفظ التطبيق بحالة قديمة كنت تتوقع إعادة تهيئتها: يحتفظ الاستبدال الفوري عمدًا بحالة المكوّنات غير المتغيّرة والإشارات والمتنقّلات من المستوى الأعلى. أعد التشغيل بـ Ctrl-C ثم nsh run، أو غيّر مُهيّئ الحالة.
  • يفشل طلب في بناء مُحزَّم أو مُضمَّن فقط: تحقق من صلاحيات التطبيق؛ إذ تُسجَّل حالات الرفض مع المفتاح الذي ينبغي إضافته.
  • شيء بطيء: أعد إنتاجه دون واجهة باستخدام nsh test --trace، الذي يطبع كل عملية على عرض (view) أصلي، أو شغّل تطبيق TSX باستخدام --trace.
  • أخطاء الأنواع في المحرر: يُبقي nsh run وnsh dev الملف .nsh/types محدّثًا؛ راجع بنية المشروع.

#انظر أيضًا