JSR هو مستودع حزم جديد يقدمه فريق Deno ويهدف إلى حل العديد من المشكلات في نظام Javascript البيئي. لقد تمت دعوتي للمشاركة في الوصول المبكر إليه وأريد مشاركة انطباعاتي.
القليل من التاريخ
لقد كنت مساهمًا مبكرًا جدًا في Deno والذي انتهى بي الأمر في النهاية بالانضمام إلى Deno عندما حصل Ry وBert على التمويل الأولي.لقد غادرت بعد عامين لمتابعة العمل الذي كان يقع في أستراليا.
خلال فترة وجودي في مجتمع Deno الذي قمت بإنشائه بلوط، إطار عمل وسيط يشبه Express مكتوب خصيصًا لـ Deno. ولا يزال حتى يومنا هذا إطارًا شائعًا للغاية. أثناء وجودي في Deno، غالبًا ما استخدمناه كأرضية اختبار لأشياء، مثل إنشاء الوثائق تلقائيًا، والتأكد من عملها نشر دينووغيرها من التغييرات في النظام البيئي.
أعتقد أن استمرار شعبيتها واستخدامها كان أحد الأسباب التي جعلت فريق Deno يتواصل معي ليمنحني وصولًا مبكرًا جدًا إلى JSR حتى أتمكن من رؤية كيفية عمله مع شيء مثل خشب البلوط.
ما هو JSR؟
JSR هو سجل Javascript/TypeScript. إنها لا مدير الحزم. ومن الواضح أن npm كان أول مدير حزم وتسجيل قابلين للتطبيق لنظام جافا سكريبت البيئي. منذ ذلك الحين شهدنا الكثير من الابتكارات في جانب مدير الحزم من المعادلةغزل, com.pnpmوحتى أوقات التشغيل التي تدمج مدير الحزم مثل كعكة.
كان هذا كله ابتكارًا في جانب إدارة الحزم. لم يكن هناك سوى القليل من التغيير أو الابتكار في جانب التسجيل الذي لا يزال يحتوي على سجل npm في جوهره. كانت هناك حلول مؤسسية تتعامل مع أشياء مثل المستودعات الخاصة والحزم المعتمدة “للإدراج المسموح به”، وكانت هناك حلول لتعزيز سجل npm، مثل unpkg وesm.shوعلى الرغم من كونها مفيدة، إلا أنها لا تزال مرتبطة بشكل جوهري بالنظام البيئي npm.
لقد حاولنا في Deno إجراء عملية تسجيل أقل من مدير الحزم باستخدامdeno.land/x والتي عملت بشكل جيد إلى حد ما مع Deno التي ركزت على “إدارة الحزم كرمز”، ولكنها واجهت تحديات. تلك التي كنا نتحدث عنها قبل أن أغادر دينو.
يبدو أن JSR يحاول الابتكار في جانب تسجيل الحزمة هذا. لقد قطعت Javascript شوطًا طويلًا منذ الأيام الأولى لـ npm. تم تصميم سجل npm ومدير الحزم خصيصًا لخدمة وقت تشغيل واحد (Node.js) والذي كان يقدم للعالم مفهوم وحدات Javascript التي جاءت نتيجة لجهود المجتمع لتوسيع نطاق JavaScript. نحن نتذكر فقط CommonJS بسبب بناء جملة الوحدة الخاص به الذي اعتمدته Node.js، ولكنه كان في الأصل أكثر من ذلك بكثير.
الآن أصبح نظام جافا سكريبت البيئي مختلفًا كثيرًا. لدينا لغة حولت نفسها، ولدينا معيار للوحدات النمطية التي تخدم كلاً من أوقات تشغيل الخادم ومتصفحات العملاء، ولدينا عالم تهيمن فيه TypeScript على طريقة تأليف كود Javascript مثل Javascript بمفردها، ولدينا نطاق واسع من أوقات تشغيل الخادم المحلي، وأوقات التشغيل بدون خادم، والمتصفحات.
ما الذي يحاول حله؟
لم أتلق ملخصًا من فريق Deno حول JSR، أو خريطة الطريق الخاصة به، لذلك تم تشكيل الكثير من هذا مما رأيته باستخدام JSR.
أول شيء يبدو أننا نحاول حله هو التحديات التي واجهناها deno.land/x
ومدير الحزم كنهج تعليمات برمجية لإدارة التبعيات. عندما ينجح الأمر، تكون تجربة تطوير رائعة، ما عليك سوى التركيز على تأليف التعليمات البرمجية. ثم تأتي المشكلة في محاولة التعبير عن تبعيات الإصدار. كل تبعية خارجية عليه deno.land/x
تم “تثبيته” على الإصدار الدقيق. يعد هذا أمرًا رائعًا من خلال طريقة “الاستقرار”، ولكنه يصبح أكثر صعوبة عند مشاركة المكتبات المشتركة عبر الحزم. يدعم JSR التعبير البسيط عن التبعيات. على الرغم من أن هذا كان موجودًا في عالم npm منذ البداية، إلا أنه بالنسبة لمستخدمي Deno، يتيح هذا المرونة والاستفادة من مدير الحزم كرمز، ولكن مع فوائد عدم التثبيت دائمًا على إصدار معين.
المشكلة الأخرى التي يبدو أن JSR يحاول حلها هي يدعم TypeScript بشكل كامل إلى جانب Javascript. لم يكن TypeScript موجودًا مع إنشاء TypeScript، وقد توصل المجتمع إلى حلول حول TypeScript للتغلب على التحديات. إنه أمر مربك لمشرفي الحزم، “هل أقوم بشحن كل من كود TypeScript المنقول والمصدر؟ إذا قمت بشحن كليهما، فكيف أساعد المستخدمين على استهلاك الحزمة؟” إنها فوضى حقيقية. في JSR، تقوم بنشر المصدر الخاص بك، سواء كان TypeScript أو Javascript، ويتأكد السجل من أن المستخدمين يستخدمون الإصدار الصحيح من التعليمات البرمجية.
فضلاً عن ذلك، الكود المنشور “مضغوط” (الذي يبدو أنه يُسمى في الأصل FastCheck bit يتم ترحيله إلى Zap، وأظن أن ذلك بسبب مكتبات الاختبار التي تتعارض مع “الفحص السريع”). يبدو أن هذا هو مستوى التحقق من نوع TypeScript الذي يمكن تحقيقه دون تحليل تدفق التحكم، بالنظر إلى سطح واجهة برمجة التطبيقات العامة للحزمة. الجانب الإيجابي هو أن Zap سريع للغاية، ودليلي المضاد للفطريات على خشب البلوط هو أنه أسرع بحوالي 10x – 15x. كما يضمن أيضًا إمكانية توثيق الكود المنشور بالكامل عبر إنشاء المستندات تلقائيًا.
لحزمة المؤلف JSR يطمح إليها اكتب مرة واحدة، واركض في كل مكان. على الرغم من أنه لم يكن موجودًا في الإصدارات الأولى التي لعبت بها وما زال أمامه طريق طويل، فقد حصلت مؤخرًا على القدرة على تحميل حزم JSR عبر Node.js وnpm. يدرك السجل وقت التشغيل حيث أن منشئ الحزمة ينشر إصدارًا واحدًا ويتعامل السجل مع تقديم إصدار خاص بوقت التشغيل المستهدف. في الوقت الحالي، من المتوقع أن يكون الكود “على دراية” بالاختلافات في وقت التشغيل، لكنني أظن أنه في المستقبل القريب ستكون هناك طرق لتمكين طرق مختلفة للتعامل مع هذا الأمر. تم إنشاء فريق دينوdnt منذ فترة ونأمل أن نرى بعض الميزات والأفكار التي تم الاستفادة منها هناك تشق طريقها إلى JSR.
إثراء تجربة تطوير الحزمة هو هدف واضح. عندما كنت في Deno، عملنا على تحسين تجربة التطوير deno.land/x
بحيث يكون لدى مشرفي الحزم مجموعة كاملة من الإمكانات لتحسين تجربة تطوير المستخدمين النهائيين. لقد ابتكر النظام البيئي npm الأكبر في ذلك، و npmjs.org لقد أثرت البيانات إلى درجة تعتمد على هذه الابتكارات، لكنها لا تمثل الحل الكامل. واحدة من الأشياء الكبيرة التي قمنا بها deno.land/x
الذي تم إحضاره إلى JSR، هو الإنشاء التلقائي للوثائق. يتم استخدام نفس الآليات التي يستخدمها مؤلفو الحزم لإثراء تجربة IDE من خلال التعليقات التوضيحية من نوع JSDoc وTypeScript لإنشاء وثائق غنية عبر الإنترنت لكل حزمة.
كانت إحدى مزايا Deno دائمًا هي القدرة على استخدام كود TypeScript كمواطن من الدرجة الأولى. هذا يعني أنه باعتباري منشئ الحزمة، لا يتعين علي اكتشاف طريقة لتوزيع معلومات النوع الخاصة بي جنبًا إلى جنب مع كود وقت التشغيل الخاص بي (أو جعل مستهلك الحزمة يفعل ذلك). أدى هذا التحدي الذي يواجه النظام البيئي غير دينو إلى تخلي بعض مؤلفي الحزم عن التطوير في TypeScript لصالح التأليف في JavaScript “الأنواع كتعليقات”. ويأتي هذا النهج مع تعقيداته الخاصة. لدي معلومات جيدة أنه على الرغم من أنها ليست جزءًا من JSR في الوقت الحالي، إلا أنها في وقت قريب جدًا سيقوم JSR بإنشاء تعريفات النوع مع نقل كود Javascript عند استهداف النظام البيئي npm. هذا يعني أن مؤلفي الحزم يمكنهم الكتابة باستخدام TypeScript وسيتمكن المستخدمون من استهلاك الحزم دون الحاجة إلى القلق بشأن النقل ولكن لديهم القدرة على الحفاظ على تحسين تجربة IDE الكاملة والتحقق من النوع الذي يأتي من استخدام TypeScript.
القدرات والقيود الذكية
يمكن أن يعتمد الكود الذي يتم نشره على JSR فقط على حزم JSR الأخرى، أو حزم npm، أو Node.js المضمنة. هذا حديقة مسورة يبدو أنه قيد/قيد متعمد. وهذا يضمن احتواء النظام البيئي للتسجيل وإمكانية التحكم فيه.
لكن الوصول إلى النظام البيئي npm الأوسع يعد أمرًا شفافًا لكل من مؤلف الحزمة وكذلك المستخدم النهائي، ويستخدم أسلوب الإدارة “package-manager as code”. على سبيل المثال، أنا استخدم العظيمالمسار إلى regexp مكتبة في البلوط. ل deno.land/x
الإصدار الذي أنشره أستخدم الإصدار الذي نشره شخص ماdeno.land/x
. في حديقة JSR المسورة، يجب نشر الإصدار على JSR (وأنا متأكد من أنه من غير المرجح أن يهتم مشرف الحزمة في أي وقت قريب)، أو يمكنني فقط استخدام الإصدار على npm. هذا ما اخترته عند النشر على npm. أقوم بإعادة تصدير جميع التبعيات الخاصة بي من ملف ./deps.ts
ملف في البلوط وهذا ما يبدو هناك:
export { compile, type Key, match as pathMatch, parse as pathParse, type ParseOptions, pathToRegexp, type TokensToRegexpOptions,} from "npm:path-to-regexp@6.2.1";
هناك قيد مقصود آخر وهو أن جميع الحزم المنشورة يجب أن تحتوي على أسماء وحدات مؤهلة بالكامل للتعليمات البرمجية الداخلية. وذلك لتجنب التحدي الذي كان على Node.js وبقية النظام البيئي محاولة التخلص منه مع كيفية تفسير الوحدات دون حل واضح.
أحد الأشياء التي كان npm ضعيفًا فيها هو الاعتماد الكبير على اتفاقية خدمة API العامة للحزمة. تمت معالجة ذلك ببطء من خلال إضافة "exports"
ل package.json
. مع JSR، هذا واضح أيضًا. باعتبارك مؤلفًا، يتعين عليك إما توفير وحدة تصدير “رئيسية” فقط، أو خريطة لمختلف عمليات التصدير المسماة ووحدتها. بالنسبة للبلوط، أقوم بتصدير كل شيء، لكن أضفت عمليات تصدير مسماة لمختلف الأجزاء العامة من البلوط. يبدو مثل هذا في deno.json
(وهو ملف البيانات التعريفية الوحيد لحزم JSR):
{ "name": "@oak/oak", "version": "13.1.0", "exports": { ".": "./mod.ts", "./application": "./application.ts", "./body": "./body.ts", "./context": "./context.ts", "./helpers": "./helpers.ts", "./etag": "./etag.ts", "./form_data": "./form_data.ts", "./http_server_native": "./http_server_native.ts", "./proxy": "./middleware/proxy.ts", "./range": "./range.ts", "./request": "./request.ts", "./response": "./response.ts", "./router": "./router.ts", "./send": "./send.ts", "./serve": "./middleware/serve.ts", "./testing": "./testing.ts" }}
وكما ترون مما سبق، هناك الآن "name"
و "version"
الحقول التي تدخل deno.json
والتي يتم استخدامها بعد ذلك للنشر أيضًا.
Deno CLI هو أيضًا وسيلة النشر للحزم إلى JSR. أنه يحتوي على كل ما تحتاجه كمؤلف الحزمة. لنشر نسخة، ما عليك سوى القيام بذلك deno publish
وتذهب بعيدا. ستقوم واجهة سطر الأوامر (CLI) بإجراء جميع عمليات التحقق وما شابه ذلك، بل وستتضمن أيضًا --dry-run
خيار للتأكد من أنه يتصرف. من الواضح أن الواجهة، رغم أنها كافية، لا تزال في مرحلة النضج.
واجهة التسجيل
واجهة الويب للتسجيل غنية جدًا بالفعل. من الواضح أنه يعتمد على الكثير مما فعلناه من أجله deno.land/x
ولكن من الواضح أنه يأخذ كل شيء إلى المستوى التالي. لأولئك منكم على دراية deno.land/x
، سيكون الكثير مألوفًا، ولكن بالنسبة لأولئك الذين ليسوا على دراية، يمكنك أن ترى كيف أن كمية المعلومات المتاحة تتجاوز ما نحصل عليه حاليًا من خلال النظام البيئي npm، كل ذلك في مكان واحد.
هذا ما تبدو عليه الصفحة المقصودة لحزمة خشب البلوط على JSR:
لأن لدي كتلة JSDoc للوحدة النمطية في نقطة الدخول الرئيسية لـ oak، فهي تعرض ذلك باعتباره المستند الرئيسي مقابل README. إذا لم يكن موجودًا، فسيتم عرض الملف التمهيدي (README) بدلاً من ذلك.
توجد تعليمات حول كيفية تثبيت حزمة ضمن npm (أو مديري حزم النظام البيئي npm الآخرين):
التعليمات تعمل، لكن المشكلة هي أن الكود المنشور على oak لا يدعم Node.js حتى الآن. بينما أقوم بالنشرالبلوط إلى npm مباشرة في الوقت الحالي، يعمل هذا الإصدار ويتم اختباره ضمن Node.js، نفس الحلول لتوفير هذه الإمكانية ليست مدمجة في JSR. لقد قدمت للفريق تعليقاتي حول هذا الأمر ونأمل أن نرى تحسينات في هذا المجال. سأرى أيضًا ما إذا كان بإمكاني إيجاد طريقة لدعم كلا وقتي التشغيل ضمن إصدار JSR الفردي.
من الأشياء الرائعة التي جربناها deno.land/x
ولكن واجهت صعوبة في الحصول على ما هو صحيح، يبدو أنه تم دمجها مباشرة في JSR، القدرة على البحث عن الرموز:
فيما يلي عرض لتبعيات oak على JSR، حيث يمكنك التعبير عن جميع تبعياتك كرمز بما في ذلك قيود semver التي يتم تحليلها تلقائيًا عند النشر:
من الواضح أن هناك فرصة هنا لزيادة إثراء هذا ببيانات أخرى مثل ما إذا كانت التبعية حالية، أو ما هي التغييرات التي حلها semver بالفعل.
يمكنك أيضًا الحصول على عرض للإصدارات المنشورة في سجل الحزمة، لأنني مالك الحزمة، ولدي أيضًا القدرة على “سحب” الإصدار:
كان الإنشاء التلقائي للوثائق أحد أكثر الأشياء المجزية التي عملت عليها deno.land/x
وأنا أحب أن يستمر حتى JSR:
وعندما يتم تبني هذا الأمر، فإنه يمثل دفعة كبيرة لإنتاجية المطورين. كما أنه يجعل حياة مشرفي الحزم أسهل بكثير، حيث تقلق بشأن كتابة التعليمات البرمجية الخاصة بك وتصبح نفس الآلية التي تعمل على تحسين تجربة المطور في IDE هي آلية توثيق الحزمة الخاصة بك. حتى بدون جهد من مؤلف الحزمة، فإنك تقدم الوثائق الصحيحة والمحدثة كخط أساس.
الاستنتاجات
JSR لا يزال في المراحل المبكرة. كانت انطباعاتي الأولية منذ شهر مضى هي أن ترحيل خشب البلوط إلى JSR كان يتطلب قدرًا لا بأس به من العمل. كان بعض ذلك في المراحل الأولى وكنت أقع في مشاكل تم حلها الآن وما زال بعضها قيد الحل.
في البداية كنت أفتقد “نعم، فماذا في ذلك”، ولكن مع بدء المزيد من الرؤية في الكشف عن نفسها، أصبحت أكثر حماسًا.
فهو يحل الكثير من المشاكل التي deno.land/x
كان، ولكن بطريقة ما يحافظ على جميع المزايا التي توفرها “إدارة الحزم كرمز”. أضف إلى ذلك أن القدرة على التفاعل مع بقية نظام جافا سكريبت البيئي تحل في الواقع الكثير من المشكلات الحقيقية.
على أقل تقدير، يعد وجود سجل يجعل TypeScript على قدم المساواة مع Javascript أمرًا جوهريًا. إنها ميزة نتمتع بها في أرض Deno منذ سنوات، ولكنها ليست شيئًا يمكن الوصول إليه بسهولة لبقية نظام Javascript البيئي حتى الآن.
لذلك، في حين أن الوقت لا يزال مبكرًا جدًا، ومن الواضح أن النظام البيئي سيصوت بأقدامه، إلا أنني أرى طريقة ما لما كان ظاهريًا مجرد جنون منذ فترة قصيرة.
مثال البلوط باستخدام JSR
فيما يلي مثال على استخدام إصدار من خشب البلوط من JSR. يجب أن يكون هذا قابلاً للتشغيل بالكامل مع الإصدارات الحديثة من Deno CLI. (لا يدعم Deno Deploy حاليًا JSR، ولكني سمعت أن ذلك سيأتي قريبًا جدًا.)
import { Application, Context, isHttpError, Router, RouterContext, Status,} from "jsr:@oak/oak@13";interface Book { id: string; title: string; author: string;}const books = new Map<string, Book>();books.set("1234", { id: "1234", title: "The Hound of the Baskervilles", author: "Conan Doyle, Author",});function notFound(context: Context) { context.response.status = Status.NotFound; context.response.body = `404 - Not Found
Path
${context.request.url} not found.`;}const router = new Router();router .get("/", (context) => { context.response.body = "Hello world!"; }) .get("/book", (context) => { context.response.body = Array.from(books.values()); }) .get("/book/:id", (context) => { if (context.params && books.has(context.params.id)) { context.response.body = books.get(context.params.id); } else { return notFound(context); } });const app = new Application();app.use(async (context, next) => { await next(); const rt = context.response.headers.get("X-Response-Time"); console.log( `%c${context.request.method} %c${ decodeURIComponent(context.request.url.pathname) }%c - %c${rt}`, "color:green", "color:cyan", "color:none", "font-weight: bold", );});app.use(async (context, next) => { const start = Date.now(); await next(); const ms = Date.now() - start; context.response.headers.set("X-Response-Time", `${ms}ms`);});app.use(async (context, next) => { try { await next(); } catch (err) { if (isHttpError(err)) { context.response.status = err.status; const { message, status, stack } = err; if (context.request.accepts("json")) { context.response.body = { message, status, stack }; context.response.type = "json"; } else { context.response.body = `${status} ${message}nn${stack ?? ""}`; context.response.type = "text/plain"; } } else { console.log(err); throw err; } }});app.use(router.routes());app.use(router.allowedMethods());app.use(notFound);app.addEventListener("listen", ({ hostname, port, serverType }) => { console.log( `%cStart listening on %c${hostname}:${port}`, "font-weight:bold", "color:yellow", ); console.log(` using HTTP server: %c${serverType}`, "color:yellow");});await app.listen();