اكتب رمزًا لإعادة كتابة الرمز الخاص بك: jscodeshift
نشرت: 2022-03-11كودمود مع jscodeshift
كم مرة استخدمت وظيفة البحث والاستبدال عبر دليل لإجراء تغييرات على ملفات مصدر JavaScript؟ إذا كنت جيدًا ، فقد أصبحت خياليًا واستخدمت التعبيرات العادية مع مجموعات الالتقاط ، لأنه يستحق الجهد إذا كانت قاعدة التعليمات البرمجية الخاصة بك كبيرة. بالرغم من ذلك ، فإن Regex لها حدود. بالنسبة للتغييرات غير التافهة ، فأنت بحاجة إلى مطور يفهم الكود في السياق ويرغب أيضًا في القيام بعملية طويلة ومملة ومعرضة للخطأ.
هذا هو المكان الذي تأتي فيه "codemods".
Codemods هي نصوص تستخدم لإعادة كتابة نصوص أخرى. فكر فيها على أنها وظيفة بحث واستبدال يمكنها قراءة التعليمات البرمجية وكتابتها. يمكنك استخدامها لتحديث التعليمات البرمجية المصدر لتلائم اصطلاحات الترميز الخاصة بالفريق ، أو إجراء تغييرات واسعة النطاق عند تعديل واجهة برمجة التطبيقات ، أو حتى الإصلاح التلقائي للرمز الموجود عندما تقوم الحزمة العامة بإجراء تغيير مفاجئ.
في هذه المقالة ، سنستكشف مجموعة أدوات لوحدات الترميز تسمى "jscodeshift" أثناء إنشاء ثلاثة نماذج كود لزيادة التعقيد. بحلول النهاية ، سيكون لديك تعرض واسع للجوانب المهمة لـ jscodeshift وستكون جاهزًا لبدء كتابة طرق الشفرة الخاصة بك. سوف نمر بثلاثة تمارين تغطي بعض الاستخدامات الأساسية ، ولكن الرائعة ، لوحدات الترميز ، ويمكنك عرض الكود المصدري لهذه التمارين في مشروع جيثب الخاص بي.
ما هو jscodeshift؟
تسمح لك مجموعة أدوات jscodeshift بضخ مجموعة من الملفات المصدر من خلال تحويل واستبدالها بما يخرج من الطرف الآخر. داخل التحويل ، تقوم بتحليل المصدر إلى شجرة بناء جملة مجردة (AST) ، وتتجول لإجراء تغييراتك ، ثم تُجدد المصدر من AST المعدَّل.
الواجهة التي يوفرها jscodeshift عبارة عن غلاف حول حزم recast
الصياغة ast-types
. تتعامل recast
مع التحويل من المصدر إلى AST والعكس بينما تتعامل ast-types
مع التفاعل منخفض المستوى مع عُقد AST.
اقامة
للبدء ، قم بتثبيت jscodeshift عالميًا من npm.
npm i -g jscodeshift
هناك خيارات عداء يمكنك استخدامها وإعداد اختبار عنيد يجعل تشغيل مجموعة من الاختبارات عبر Jest (إطار عمل اختبار JavaScript مفتوح المصدر) أمرًا سهلاً حقًا ، لكننا سنتجاوز ذلك الآن لصالح البساطة:
jscodeshift -t some-transform.js input-file.js -d -p
سيؤدي هذا إلى تشغيل input-file.js
من خلال التحويل some-transform.js
وطباعة النتائج دون تغيير الملف.
قبل القفز ، من المهم فهم ثلاثة أنواع رئيسية من الكائنات التي تتعامل معها واجهة برمجة تطبيقات jscodeshift: العقد ومسارات العقد والمجموعات.
العقد
العقد هي لبنات البناء الأساسية لـ AST ، وغالبًا ما يشار إليها باسم "عُقد AST". هذا ما تراه عند استكشاف التعليمات البرمجية الخاصة بك باستخدام AST Explorer. إنها كائنات بسيطة ولا توفر أي طرق.
مسارات العقدة
مسارات العقدة عبارة عن أغلفة حول عقدة AST يتم توفيرها بواسطة ast-types
كطريقة لاجتياز شجرة بناء الجملة المجردة (AST ، تذكر؟). بشكل منفصل ، لا تحتوي العقد على أي معلومات حول أصلها أو نطاقها ، لذلك تهتم مسارات العقد بذلك. يمكنك الوصول إلى العقدة الملتفة عبر خاصية node
وهناك عدة طرق متاحة لتغيير العقدة الأساسية. غالبًا ما يُشار إلى مسارات العقد على أنها مجرد "مسارات".
المجموعات
المجموعات عبارة عن مجموعات من صفر أو أكثر من مسارات العقد التي تُرجعها واجهة برمجة تطبيقات jscodeshift عند الاستعلام عن AST. لديهم كل أنواع الأساليب المفيدة ، والتي سنستكشف بعضها.
تحتوي المجموعات على مسارات العقد ، وتحتوي مسارات العقد على عقد ، والعقد هي ما يتكون منه AST. ضع ذلك في الاعتبار وسيكون من السهل فهم واجهة برمجة تطبيقات استعلام jscodeshift.
قد يكون من الصعب تتبع الاختلافات بين هذه الكائنات وإمكانيات واجهة برمجة التطبيقات الخاصة بكل منها ، لذلك هناك أداة أنيقة تسمى jscodeshift-helper تسجل نوع الكائن وتوفر معلومات أساسية أخرى.
التمرين 1: إزالة المكالمات إلى وحدة التحكم
لتبليل أقدامنا ، لنبدأ بإزالة المكالمات لجميع طرق وحدة التحكم في قاعدة التعليمات البرمجية الخاصة بنا. بينما يمكنك القيام بذلك من خلال البحث والاستبدال والقليل من التعبير العادي ، فإنه يبدأ في التعقيد مع العبارات متعددة الأسطر ، والقوالب الحرفية ، والمكالمات الأكثر تعقيدًا ، لذا فهو مثال مثالي للبدء به.
أولاً ، قم بإنشاء ملفين ، remove-consoles.js
و remove-consoles.input.js
:
//remove-consoles.js export default (fileInfo, api) => { };
//remove-consoles.input.js export const sum = (a, b) => { console.log('calling sum with', arguments); return a + b; }; export const multiply = (a, b) => { console.warn('calling multiply with', arguments); return a * b; }; export const divide = (a, b) => { console.error(`calling divide with ${ arguments }`); return a / b; }; export const average = (a, b) => { console.log('calling average with ' + arguments); return divide(sum(a, b), 2); };
هذا هو الأمر الذي سنستخدمه في الجهاز لدفعه خلال jscodeshift:
jscodeshift -t remove-consoles.js remove-consoles.input.js -d -p
إذا تم إعداد كل شيء بشكل صحيح ، عند تشغيله يجب أن ترى شيئًا كهذا.
Processing 1 files... Spawning 1 workers... Running in dry mode, no files will be written! Sending 1 files to free worker... All done. Results: 0 errors 0 unmodified 1 skipped 0 ok Time elapsed: 0.514seconds
حسنًا ، كان ذلك قليلًا من التأثير المناخي لأن تحويلنا لم يفعل شيئًا بعد ، ولكن على الأقل نعلم أنه يعمل بالكامل. إذا لم يتم تشغيله على الإطلاق ، فتأكد من تثبيت jscodeshift على مستوى العالم. إذا كان الأمر لتشغيل التحويل غير صحيح ، فسترى إما رسالة "ERROR Transform file… not found" أو "TypeError: path must be a string or Buffer" إذا تعذر العثور على ملف الإدخال. إذا كنت تعاني من شيء ما ، فيجب أن يكون من السهل اكتشاف أخطاء التحويل الوصفية للغاية.
لكن هدفنا النهائي ، بعد تحول ناجح ، هو رؤية هذا المصدر:
export const sum = (a, b) => { return a + b; }; export const multiply = (a, b) => { return a * b; }; export const divide = (a, b) => { return a / b; }; export const average = (a, b) => { return divide(sum(a, b), 2); };
للوصول إلى هناك ، نحتاج إلى تحويل المصدر إلى AST ، والعثور على وحدات التحكم ، وإزالتها ، ثم تحويل AST المعدل إلى مصدر. الخطوات الأولى والأخيرة سهلة ، إنها فقط:
remove-consoles.js export default (fileInfo, api) => { const j = api.jscodeshift; const root = j(fileInfo.source); return root.toSource(); };
لكن كيف نجد المفاتيح ونزيلها؟ ما لم تكن لديك معرفة استثنائية بواجهة برمجة تطبيقات موزيلا ، فربما تحتاج إلى أداة للمساعدة في فهم شكل AST. لذلك يمكنك استخدام مستكشف AST. الصق محتويات remove-consoles.input.js
فيه وسترى AST. هناك الكثير من البيانات حتى في أبسط رمز ، لذا فهي تساعد على إخفاء بيانات الموقع وطرقه. يمكنك تبديل رؤية الخصائص في AST Explorer باستخدام مربعات الاختيار أعلى الشجرة.
يمكننا أن نرى أن استدعاءات أساليب التحكم يُشار إليها باسم CallExpressions
، فكيف نجدها في التحويل الخاص بنا؟ نستخدم استعلامات jscodeshift ، مع تذكر مناقشتنا السابقة حول الاختلافات بين المجموعات ومسارات العقد والعقد نفسها:
//remove-consoles.js export default (fileInfo, api) => { const j = api.jscodeshift; const root = j(fileInfo.source); return root.toSource(); };
const root = j(fileInfo.source);
تقوم بإرجاع مجموعة من مسار عقدة واحد ، والذي يلتف عقدة AST الجذرية. يمكننا استخدام التابع find
للمجموعة للبحث عن العقد التابعة من نوع معين ، مثل:
const callExpressions = root.find(j.CallExpression);
يؤدي هذا إلى إرجاع مجموعة أخرى من مسارات العقد تحتوي فقط على العقد التي تمثل تعبيرات النداء. للوهلة الأولى ، يبدو هذا كما نريد ، لكنه واسع جدًا. قد ينتهي بنا الأمر إلى تشغيل مئات أو آلاف الملفات من خلال عمليات التحويل الخاصة بنا ، لذلك يجب أن نكون دقيقين للتأكد من أنها ستعمل على النحو المنشود. الاكتشاف البسيط أعلاه لن يعثر فقط على وحدة التحكم CallExpressions ، بل find
كل CallExpression في المصدر ، بما في ذلك
require('foo') bar() setTimeout(() => {}, 0)
لفرض مزيد من التحديد ، نقدم وسيطة ثانية لـ .find
: كائن من معلمات إضافية ، يجب تضمين كل عقدة في النتائج. يمكننا إلقاء نظرة على AST Explorer لنرى أن وحدة التحكم الخاصة بنا. * المكالمات لها شكل:
{ "type": "CallExpression", "callee": { "type": "MemberExpression", "object": { "type": "Identifier", "name": "console" } } }
بهذه المعرفة ، نعلم أنه يجب تنقيح استعلامنا باستخدام محدد سيعيد فقط نوع CallExpressions الذي نهتم به:
const callExpressions = root.find(j.CallExpression, { callee: { type: 'MemberExpression', object: { type: 'Identifier', name: 'console' }, }, });
الآن وقد حصلنا على مجموعة دقيقة من مواقع الاتصال ، فلنقم بإزالتها من AST. بشكل ملائم ، يحتوي نوع كائن المجموعة على طريقة remove
ستقوم بذلك بالضبط. سيبدو ملف remove-consoles.js
بنا الآن على النحو التالي:
//remove-consoles.js export default (fileInfo, api) => { const j = api.jscodeshift; const root = j(fileInfo.source) const callExpressions = root.find(j.CallExpression, { callee: { type: 'MemberExpression', object: { type: 'Identifier', name: 'console' }, }, } ); callExpressions.remove(); return root.toSource(); };
الآن ، إذا قمنا بتشغيل التحويل من سطر الأوامر باستخدام jscodeshift -t remove-consoles.js remove-consoles.input.js -d -p
، فيجب أن نرى:
Processing 1 files... Spawning 1 workers... Running in dry mode, no files will be written! Sending 1 files to free worker... export const sum = (a, b) => { return a + b; }; export const multiply = (a, b) => { return a * b; }; export const divide = (a, b) => { return a / b; }; export const average = (a, b) => { return divide(sum(a, b), 2); }; All done. Results: 0 errors 0 unmodified 0 skipped 1 ok Time elapsed: 0.604seconds
تبدو جيدة. الآن بعد أن قام التحويل الخاص بنا بتغيير AST الأساسي ، فإن استخدام .toSource()
يولد سلسلة مختلفة عن الأصل. يعرض الخيار -p من الأمر الخاص بنا النتيجة ، ويظهر في الجزء السفلي عدد التصرفات الخاصة بكل ملف تمت معالجته. ستؤدي إزالة الخيار -d من الأمر إلى استبدال محتوى remove-Consoles.input.js بإخراج التحويل.
اكتمل تمريننا الأول ... تقريبًا. تبدو الشفرة غريبة المظهر وربما مسيئة جدًا لأي متطرف وظيفي هناك ، وبالتالي لجعل تدفق كود التحويل أفضل ، جعل jscodeshift معظم الأشياء قابلة للتسلسل. هذا يسمح لنا بإعادة كتابة التحويل الخاص بنا على النحو التالي:
// remove-consoles.js export default (fileInfo, api) => { const j = api.jscodeshift; return j(fileInfo.source) .find(j.CallExpression, { callee: { type: 'MemberExpression', object: { type: 'Identifier', name: 'console' }, }, } ) .remove() .toSource(); };
أفضل بكثير. لتلخيص التمرين 1 ، قمنا بلف المصدر ، واستفسرنا عن مجموعة من مسارات العقد ، وقمنا بتغيير AST ، ثم أعدنا إنشاء هذا المصدر. لقد بللنا أقدامنا بمثال بسيط جدًا وتطرقنا إلى أهم الجوانب. الآن ، لنفعل شيئًا أكثر إثارة للاهتمام.
التمرين 2: استبدال استدعاءات الطريقة المستوردة
بالنسبة لهذا السيناريو ، لدينا وحدة نمطية "هندسية" مع طريقة تسمى "CircleArea" والتي تم إيقافها لصالح "getCircleArea". يمكننا بسهولة العثور على هذه واستبدالها بـ /geometry\.circleArea/g
، ولكن ماذا لو قام المستخدم باستيراد الوحدة وتخصيص اسم مختلف لها؟ علي سبيل المثال:
import g from 'geometry'; const area = g.circleArea(radius);
كيف نعرف استبدال g.circleArea
بدلاً من geometry.circleArea
؟ لا يمكننا بالتأكيد افتراض أن جميع مكالمات circleArea
هي تلك التي نبحث عنها ، فنحن بحاجة إلى بعض السياق. هذا هو المكان الذي تبدأ فيه codemods في إظهار قيمتها. لنبدأ بإنشاء ملفين ، deprecated.js
و deprecated.input.js
.
//deprecated.js export default (fileInfo, api) => { const j = api.jscodeshift; const root = j(fileInfo.source); return root.toSource(); };
deprecated.input.js import g from 'geometry'; import otherModule from 'otherModule'; const radius = 20; const area = g.circleArea(radius); console.log(area === Math.pow(g.getPi(), 2) * radius); console.log(area === otherModule.circleArea(radius));
الآن قم بتشغيل هذا الأمر لتشغيل codemod.

jscodeshift -t ./deprecated.js ./deprecated.input.js -d -p
يجب أن ترى الإخراج يشير إلى أن التحويل تم تشغيله ، لكن لم يغير أي شيء بعد.
Processing 1 files... Spawning 1 workers... Running in dry mode, no files will be written! Sending 1 files to free worker... All done. Results: 0 errors 1 unmodified 0 skipped 0 ok Time elapsed: 0.892seconds
نحتاج إلى معرفة ما تم استيراد وحدة geometry
الخاصة بنا به. لنلقِ نظرة على مستكشف AST ومعرفة ما نبحث عنه. استيرادنا يأخذ هذا الشكل.
{ "type": "ImportDeclaration", "specifiers": [ { "type": "ImportDefaultSpecifier", "local": { "type": "Identifier", "name": "g" } } ], "source": { "type": "Literal", "value": "geometry" } }
يمكننا تحديد نوع كائن للعثور على مجموعة من العقد مثل هذا:
const importDeclaration = root.find(j.ImportDeclaration, { source: { type: 'Literal', value: 'geometry', }, });
هذا يحصل لنا على إعلان الاستيراد المستخدم لاستيراد "الهندسة". من هناك ، ابحث لأسفل للعثور على الاسم المحلي المستخدم للاحتفاظ بالوحدة المستوردة. نظرًا لأن هذه هي المرة الأولى التي نقوم فيها بذلك ، دعنا نشير إلى نقطة مهمة ومربكة عند البدء.
ملاحظة: من المهم معرفة أن root.find()
يعرض مجموعة من مسارات العقد. من هناك ، تقوم .get(n)
بإرجاع مسار العقدة في الفهرس n
في تلك المجموعة ، وللحصول على العقدة الفعلية ، نستخدم .node
. العقدة هي في الأساس ما نراه في AST Explorer. تذكر أن مسار العقدة هو في الغالب معلومات حول نطاق العقدة وعلاقاتها ، وليس العقدة نفسها.
// find the Identifiers const identifierCollection = importDeclaration.find(j.Identifier); // get the first NodePath from the Collection const nodePath = identifierCollection.get(0); // get the Node in the NodePath and grab its "name" const localName = nodePath.node.name;
هذا يسمح لنا بمعرفة ديناميكيًا ما تم استيراد وحدة geometry
الخاصة بنا عليه. بعد ذلك ، نجد الأماكن التي يتم استخدامها ونغيرها. بالنظر إلى AST Explorer ، يمكننا أن نرى أننا بحاجة إلى العثور على MemberExpressions التي تبدو كالتالي:
{ "type": "MemberExpression", "object": { "name": "geometry" }, "property": { "name": "circleArea" } }
تذكر ، مع ذلك ، أن الوحدة الخاصة بنا ربما تم استيرادها باسم مختلف ، لذلك يتعين علينا حساب ذلك بجعل استعلامنا يبدو كما يلي:
j.MemberExpression, { object: { name: localName, }, property: { name: "circleArea", }, })
الآن بعد أن أصبح لدينا استعلام ، يمكننا الحصول على مجموعة من جميع مواقع الاتصال لطريقتنا القديمة ثم استخدام طريقة replaceWith()
. يتكرر أسلوب replaceWith()
خلال المجموعة ، ويمرر كل مسار عقدة إلى دالة رد نداء. ثم يتم استبدال عقدة AST بأي عقدة تعود من رد الاتصال.
بمجرد الانتهاء من الاستبدال ، نقوم بإنشاء المصدر كالمعتاد. هذا هو التحويل النهائي:
//deprecated.js export default (fileInfo, api) => { const j = api.jscodeshift; const root = j(fileInfo.source); // find declaration for "geometry" import const importDeclaration = root.find(j.ImportDeclaration, { source: { type: 'Literal', value: 'geometry', }, }); // get the local name for the imported module const localName = // find the Identifiers importDeclaration.find(j.Identifier) // get the first NodePath from the Collection .get(0) // get the Node in the NodePath and grab its "name" .node.name; return root.find(j.MemberExpression, { object: { name: localName, }, property: { name: 'circleArea', }, }) .replaceWith(nodePath => { // get the underlying Node const { node } = nodePath; // change to our new prop node.property.name = 'getCircleArea'; // replaceWith should return a Node, not a NodePath return node; }) .toSource(); };
عندما نقوم بتشغيل المصدر من خلال التحويل ، نرى أنه تم تغيير استدعاء الطريقة الموقوفة في وحدة geometry
، ولكن بقي الباقي دون تغيير ، مثل:
import g from 'geometry'; import otherModule from 'otherModule'; const radius = 20; const area = g.getCircleArea(radius); console.log(area === Math.pow(g.getPi(), 2) * radius); console.log(area === otherModule.circleArea(radius));
التمرين 3: تغيير توقيع الطريقة
في التدريبات السابقة ، قمنا بتغطية مجموعات الاستعلام عن أنواع معينة من العقد ، وإزالة العقد ، وتغيير العقد ، ولكن ماذا عن إنشاء عقد جديدة تمامًا؟ هذا ما سنتناوله في هذا التمرين.
في هذا السيناريو ، لدينا توقيع للطريقة خرج عن نطاق السيطرة باستخدام الحجج الفردية مع نمو البرنامج ، ولذا فقد تقرر أنه من الأفضل قبول كائن يحتوي على هذه الحجج بدلاً من ذلك.
بدلاً من car.factory('white', 'Kia', 'Sorento', 2010, 50000, null, true);
نود أن نرى
const suv = car.factory({ color: 'white', make: 'Kia', model: 'Sorento', year: 2010, miles: 50000, bedliner: null, alarm: true, });
لنبدأ بإجراء التحويل وملف الإدخال للاختبار باستخدام:
//signature-change.js export default (fileInfo, api) => { const j = api.jscodeshift; const root = j(fileInfo.source); return root.toSource(); };
//signature-change.input.js import car from 'car'; const suv = car.factory('white', 'Kia', 'Sorento', 2010, 50000, null, true); const truck = car.factory('silver', 'Toyota', 'Tacoma', 2006, 100000, true, true);
سيكون أمرنا لتشغيل التحويل هو jscodeshift -t signature-change.js signature-change.input.js -d -p
والخطوات التي نحتاجها لإجراء هذا التحويل هي:
- ابحث عن الاسم المحلي للوحدة المستوردة
- البحث عن جميع مواقع الاتصال إلى طريقة المصنع
- اقرأ جميع الحجج التي يتم تمريرها
- استبدل هذا الاستدعاء بمتوسط واحد يحتوي على كائن بالقيم الأصلية
باستخدام مستكشف AST والعملية التي استخدمناها في التدريبات السابقة ، فإن الخطوتين الأوليين سهلة:
//signature-change.js export default (fileInfo, api) => { const j = api.jscodeshift; const root = j(fileInfo.source); // find declaration for "car" import const importDeclaration = root.find(j.ImportDeclaration, { source: { type: 'Literal', value: 'car', }, }); // get the local name for the imported module const localName = importDeclaration.find(j.Identifier) .get(0) .node.name; // find where `.factory` is being called return root.find(j.CallExpression, { callee: { type: 'MemberExpression', object: { name: localName, }, property: { name: 'factory', }, } }) .toSource(); };
لقراءة جميع الحجج التي يتم تمريرها حاليًا ، نستخدم طريقة replaceWith()
في مجموعتنا من CallExpressions لتبديل كل من العقد. ستحل العقد الجديدة محل node.arguments مع وسيط واحد جديد ، وهو كائن.
دعونا نجربها باستخدام كائن بسيط للتأكد من أننا نعرف كيف يعمل هذا قبل أن نستخدم القيم المناسبة:
.replaceWith(nodePath => { const { node } = nodePath; node.arguments = [{ foo: 'bar' }]; return node; })
عندما نقوم بتشغيل هذا ( jscodeshift -t signature-change.js signature-change.input.js -d -p
) ، سينفجر التحويل مع:
ERR signature-change.input.js Transformation error Error: {foo: bar} does not match type Printable
اتضح أنه لا يمكننا فقط تشويش الكائنات العادية في عُقد AST الخاصة بنا. بدلاً من ذلك ، نحتاج إلى استخدام البناة لإنشاء العقد المناسبة.
بناة العقدة
يسمح لنا البناة بإنشاء عقد جديدة بشكل صحيح ؛ يتم توفيرها بواسطة ast-types
وظهرت من خلال jscodeshift. إنهم يتحققون بصرامة من أن الأنواع المختلفة من العقد تم إنشاؤها بشكل صحيح ، الأمر الذي قد يكون محبطًا عندما تقوم بالقرصنة بعيدًا ، ولكن في النهاية ، هذا شيء جيد. لفهم كيفية استخدام البناة ، هناك شيئان يجب أن تضعهما في اعتبارك:
يتم تحديد جميع أنواع عقدة AST المتاحة في المجلد def
الخاص بمشروع github من نوع ast ، ومعظمها في core.js هناك بناة لجميع أنواع عقدة AST ، لكنهم يستخدمون إصدار غلاف الجمل من نوع العقدة ، وليس pascal -قضية. (لم يذكر هذا صراحة ، ولكن يمكنك أن ترى هذا هو الحال في مصادر الأنواع ast
إذا استخدمنا AST Explorer مع مثال لما نريد أن تكون النتيجة ، فيمكننا تجميع هذا معًا بسهولة تامة. في حالتنا ، نريد أن تكون الوسيطة المفردة الجديدة ObjectExpression مع مجموعة من الخصائص. بالنظر إلى تعريفات النوع المذكورة أعلاه ، يمكننا أن نرى ما يستلزمه ذلك:
def("ObjectExpression") .bases("Expression") .build("properties") .field("properties", [def("Property")]); def("Property") .bases("Node") .build("kind", "key", "value") .field("kind", or("init", "get", "set")) .field("key", or(def("Literal"), def("Identifier"))) .field("value", def("Expression"));
لذا ، فإن الكود الخاص بإنشاء عقدة AST لـ {foo: 'bar'} سيبدو كما يلي:
j.objectExpression([ j.property( 'init', j.identifier('foo'), j.literal('bar') ) ]);
خذ هذا الرمز وقم بتوصيله في التحويل الخاص بنا على النحو التالي:
.replaceWith(nodePath => { const { node } = nodePath; const object = j.objectExpression([ j.property( 'init', j.identifier('foo'), j.literal('bar') ) ]); node.arguments = [object]; return node; })
تشغيل هذا يعطينا النتيجة:
import car from 'car'; const suv = car.factory({ foo: "bar" }); const truck = car.factory({ foo: "bar" });
الآن بعد أن عرفنا كيفية إنشاء عقدة AST مناسبة ، من السهل تكرار الحجج القديمة وإنشاء كائن جديد لاستخدامه ، بدلاً من ذلك. هذا ما يبدو عليه ملف signature-change.js
الآن:
//signature-change.js export default (fileInfo, api) => { const j = api.jscodeshift; const root = j(fileInfo.source); // find declaration for "car" import const importDeclaration = root.find(j.ImportDeclaration, { source: { type: 'Literal', value: 'car', }, }); // get the local name for the imported module const localName = importDeclaration.find(j.Identifier) .get(0) .node.name; // current order of arguments const argKeys = [ 'color', 'make', 'model', 'year', 'miles', 'bedliner', 'alarm', ]; // find where `.factory` is being called return root.find(j.CallExpression, { callee: { type: 'MemberExpression', object: { name: localName, }, property: { name: 'factory', }, } }) .replaceWith(nodePath => { const { node } = nodePath; // use a builder to create the ObjectExpression const argumentsAsObject = j.objectExpression( // map the arguments to an Array of Property Nodes node.arguments.map((arg, i) => j.property( 'init', j.identifier(argKeys[i]), j.literal(arg.value) ) ) ); // replace the arguments with our new ObjectExpression node.arguments = [argumentsAsObject]; return node; }) // specify print options for recast .toSource({ quote: 'single', trailingComma: true }); };
قم بتشغيل التحويل ( jscodeshift -t signature-change.js signature-change.input.js -d -p
) وسنرى أن التواقيع قد تم تحديثها كما هو متوقع:
import car from 'car'; const suv = car.factory({ color: 'white', make: 'Kia', model: 'Sorento', year: 2010, miles: 50000, bedliner: null, alarm: true, }); const truck = car.factory({ color: 'silver', make: 'Toyota', model: 'Tacoma', year: 2006, miles: 100000, bedliner: true, alarm: true, });
Codemods مع خلاصة jscodeshift
لقد استغرق الأمر القليل من الوقت والجهد للوصول إلى هذه النقطة ، لكن الفوائد كبيرة عند مواجهة إعادة البناء الشامل. يعد توزيع مجموعات من الملفات على عمليات مختلفة وتشغيلها بالتوازي شيئًا يتفوق فيه jscodeshift ، مما يسمح لك بإجراء تحويلات معقدة عبر قاعدة بيانات ضخمة في ثوانٍ. عندما تصبح أكثر كفاءة مع طرق الشفرة ، ستبدأ في إعادة تعيين البرامج النصية الحالية (مثل مستودع جيثب الخاص بالتفاعل أو كودمود أو كتابة ما يخصك لجميع أنواع المهام ، وهذا سيجعلك أنت وفريقك ومستخدمي الحزم أكثر كفاءة .