Schreiben Sie Code, um Ihren Code neu zu schreiben: jscodeshift
Veröffentlicht: 2022-03-11Codemods mit jscodeshift
Wie oft haben Sie die Suchen-und-Ersetzen-Funktion in einem Verzeichnis verwendet, um Änderungen an JavaScript-Quelldateien vorzunehmen? Wenn Sie gut sind, haben Sie Lust bekommen und reguläre Ausdrücke mit einfangenden Gruppen verwendet, denn es lohnt sich, wenn Ihre Codebasis ansehnlich ist. Regex hat jedoch Grenzen. Für nicht triviale Änderungen benötigen Sie einen Entwickler, der den Code im Kontext versteht und auch bereit ist, den langen, langwierigen und fehleranfälligen Prozess auf sich zu nehmen.
Hier kommen „Codemods“ ins Spiel.
Codemods sind Skripte, die zum Umschreiben anderer Skripte verwendet werden. Betrachten Sie sie als eine Funktion zum Suchen und Ersetzen, die Code lesen und schreiben kann. Sie können sie verwenden, um den Quellcode an die Programmierkonventionen eines Teams anzupassen, umfassende Änderungen vorzunehmen, wenn eine API geändert wird, oder sogar vorhandenen Code automatisch zu reparieren, wenn Ihr öffentliches Paket eine bahnbrechende Änderung vornimmt.
In diesem Artikel untersuchen wir ein Toolkit für Codemods namens „jscodeshift“, während wir drei Codemods mit zunehmender Komplexität erstellen. Am Ende haben Sie einen umfassenden Einblick in die wichtigen Aspekte von jscodeshift und sind bereit, mit dem Schreiben Ihrer eigenen Codemods zu beginnen. Wir werden drei Übungen durchgehen, die einige grundlegende, aber großartige Anwendungen von Codemods abdecken, und Sie können den Quellcode für diese Übungen in meinem Github-Projekt anzeigen.
Was ist jscodeshift?
Mit dem jscodeshift-Toolkit können Sie eine Reihe von Quelldateien durch eine Transformation pumpen und sie durch das ersetzen, was am anderen Ende herauskommt. Innerhalb der Transformation parsen Sie die Quelle in einen abstrakten Syntaxbaum (AST), stochern herum, um Ihre Änderungen vorzunehmen, und generieren dann die Quelle aus der geänderten AST neu.
Die von jscodeshift bereitgestellte Schnittstelle ist ein Wrapper um recast
und ast-types
type-Pakete. recast
übernimmt die Konvertierung von der Quelle zu AST und zurück, während ast-types
die Low-Level-Interaktion mit den AST-Knoten übernimmt.
Aufstellen
Installieren Sie zunächst jscodeshift global von npm.
npm i -g jscodeshift
Es gibt Runner-Optionen, die Sie verwenden können, und ein eigenwilliges Test-Setup, das das Ausführen einer Reihe von Tests über Jest (ein Open-Source-JavaScript-Test-Framework) wirklich einfach macht, aber wir werden das vorerst zugunsten der Einfachheit umgehen:
jscodeshift -t some-transform.js input-file.js -d -p
Dadurch wird input-file.js
durch die Transformation some-transform.js
und die Ergebnisse gedruckt, ohne die Datei zu ändern.
Bevor Sie loslegen, ist es jedoch wichtig, drei Hauptobjekttypen zu verstehen, mit denen die jscodeshift-API zu tun hat: Knoten, Knotenpfade und Sammlungen.
Knoten
Knoten sind die grundlegenden Bausteine des AST, die oft als „AST-Knoten“ bezeichnet werden. Dies sehen Sie, wenn Sie Ihren Code mit AST Explorer untersuchen. Sie sind einfache Objekte und stellen keine Methoden bereit.
Knotenpfade
Knotenpfade sind Wrapper um einen AST-Knoten, der von ast-types
bereitgestellt wird, um den abstrakten Syntaxbaum zu durchlaufen (AST, erinnerst du dich?). Knoten haben isoliert keine Informationen über ihre Eltern oder ihren Geltungsbereich, also kümmern sich Knotenpfade darum. Sie können über die node
auf den umschlossenen Knoten zugreifen, und es stehen mehrere Methoden zur Verfügung, um den zugrunde liegenden Knoten zu ändern. Knotenpfade werden oft nur als „Pfade“ bezeichnet.
Sammlungen
Sammlungen sind Gruppen von null oder mehr Knotenpfaden, die die jscodeshift-API zurückgibt, wenn Sie den AST abfragen. Sie haben alle möglichen nützlichen Methoden, von denen wir einige untersuchen werden.
Sammlungen enthalten Knotenpfade, Knotenpfade enthalten Knoten, und Knoten sind das, woraus der AST besteht. Denken Sie daran und es wird einfach sein, die jscodeshift-Abfrage-API zu verstehen.
Es kann schwierig sein, die Unterschiede zwischen diesen Objekten und ihren jeweiligen API-Fähigkeiten zu verfolgen, daher gibt es ein raffiniertes Tool namens jscodeshift-helper, das den Objekttyp protokolliert und andere wichtige Informationen bereitstellt.
Übung 1: Aufrufe an die Konsole entfernen
Um unsere Füße nass zu machen, beginnen wir damit, Aufrufe an alle Konsolenmethoden in unserer Codebasis zu entfernen. Während Sie dies mit Suchen und Ersetzen und ein wenig Regex tun können, wird es mit mehrzeiligen Anweisungen, Vorlagenliteralen und komplexeren Aufrufen schwierig, sodass dies ein ideales Beispiel für den Anfang ist.
Erstellen Sie zunächst zwei Dateien, remove-consoles.js
und 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); };
Hier ist der Befehl, den wir im Terminal verwenden, um ihn durch jscodeshift zu schieben:
jscodeshift -t remove-consoles.js remove-consoles.input.js -d -p
Wenn alles richtig eingerichtet ist, sollten Sie beim Ausführen so etwas sehen.
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
OK, das war ein bisschen antiklimaktisch, da unsere Transformation noch nichts bewirkt, aber zumindest wissen wir, dass alles funktioniert. Wenn es überhaupt nicht läuft, stellen Sie sicher, dass Sie jscodeshift global installiert haben. Wenn der Befehl zum Ausführen der Transformation falsch ist, sehen Sie entweder die Meldung „ERROR Transform file … does not exist“ oder „TypeError: path must be a string or Buffer“, wenn die Eingabedatei nicht gefunden werden kann. Wenn Sie etwas falsch gemacht haben, sollte es an den sehr anschaulichen Transformationsfehlern leicht zu erkennen sein.
Unser Endziel ist es jedoch, nach einer erfolgreichen Transformation diese Quelle zu sehen:
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); };
Um dorthin zu gelangen, müssen wir die Quelle in einen AST konvertieren, die Konsolen finden, sie entfernen und dann den geänderten AST wieder in die Quelle konvertieren. Die ersten und letzten Schritte sind einfach, es ist nur:
remove-consoles.js export default (fileInfo, api) => { const j = api.jscodeshift; const root = j(fileInfo.source); return root.toSource(); };
Aber wie finden wir die Konsolen und entfernen sie? Wenn Sie keine außergewöhnlichen Kenntnisse der Mozilla Parser-API haben, benötigen Sie wahrscheinlich ein Tool, das Ihnen hilft zu verstehen, wie der AST aussieht. Dazu können Sie den AST Explorer verwenden. Fügen Sie den Inhalt von remove-consoles.input.js
ein und Sie sehen den AST. Selbst der einfachste Code enthält viele Daten, sodass es hilfreich ist, Standortdaten und -methoden zu verbergen. Sie können die Sichtbarkeit von Eigenschaften in AST Explorer mit den Kontrollkästchen über der Struktur umschalten.
Wir können sehen, dass Aufrufe von Konsolenmethoden als CallExpressions
bezeichnet werden. Wie finden wir sie also in unserer Transformation? Wir verwenden die Abfragen von jscodeshift und erinnern uns an unsere frühere Diskussion über die Unterschiede zwischen Sammlungen, Knotenpfaden und Knoten selbst:
//remove-consoles.js export default (fileInfo, api) => { const j = api.jscodeshift; const root = j(fileInfo.source); return root.toSource(); };
Die Zeile const root = j(fileInfo.source);
gibt eine Sammlung eines Knotenpfads zurück, der den Stamm-AST-Knoten umschließt. Wir können die find
-Methode der Sammlung verwenden, um nach untergeordneten Knoten eines bestimmten Typs zu suchen, etwa so:
const callExpressions = root.find(j.CallExpression);
Dies gibt eine weitere Sammlung von Knotenpfaden zurück, die nur die Knoten enthält, die CallExpressions sind. Auf den ersten Blick scheint dies das zu sein, was wir wollen, aber es ist zu weit gefasst. Wir könnten am Ende Hunderte oder Tausende von Dateien durch unsere Transformationen laufen lassen, also müssen wir genau sein, um darauf vertrauen zu können, dass es wie beabsichtigt funktioniert. Die obige naive find
würde nicht nur die CallExpressions der Konsole finden, sondern jeden CallExpression in der Quelle, einschließlich
require('foo') bar() setTimeout(() => {}, 0)
Um eine größere Spezifität zu erzwingen, stellen wir ein zweites Argument für .find
: Als Objekt mit zusätzlichen Parametern muss jeder Knoten in die Ergebnisse aufgenommen werden. Wir können uns den AST Explorer ansehen, um zu sehen, dass unsere console.*-Aufrufe die folgende Form haben:
{ "type": "CallExpression", "callee": { "type": "MemberExpression", "object": { "type": "Identifier", "name": "console" } } }
Mit diesem Wissen wissen wir, dass wir unsere Abfrage mit einem Spezifizierer verfeinern müssen, der nur den Typ von CallExpressions zurückgibt, an dem wir interessiert sind:
const callExpressions = root.find(j.CallExpression, { callee: { type: 'MemberExpression', object: { type: 'Identifier', name: 'console' }, }, });
Nachdem wir nun eine genaue Sammlung der Anrufseiten haben, entfernen wir sie aus dem AST. Praktischerweise hat der Collection-Objekttyp eine remove
-Methode, die genau das tut. Unsere Datei remove-consoles.js
sieht nun so aus:
//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(); };
Wenn wir nun unsere Transformation über die Befehlszeile mit jscodeshift -t remove-consoles.js remove-consoles.input.js -d -p
, sollten wir Folgendes sehen:
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
Es sieht gut aus. Nun, da unsere Transformation den zugrunde liegenden AST ändert, generiert die Verwendung von .toSource()
eine andere Zeichenfolge als das Original. Die Option -p unseres Befehls zeigt das Ergebnis an, und unten wird eine Liste der Dispositionen für jede verarbeitete Datei angezeigt. Das Entfernen der Option -d aus unserem Befehl würde den Inhalt von remove-consoles.input.js durch die Ausgabe der Transformation ersetzen.
Unsere erste Übung ist abgeschlossen … fast. Der Code sieht bizarr aus und ist für funktionale Puristen wahrscheinlich sehr anstößig. Um den Transformationscode besser fließen zu lassen, hat jscodeshift die meisten Dinge verkettbar gemacht. Dies ermöglicht es uns, unsere Transformation wie folgt umzuschreiben:
// 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(); };
Viel besser. Um Übung 1 zusammenzufassen, haben wir die Quelle umschlossen, nach einer Sammlung von Knotenpfaden abgefragt, den AST geändert und dann diese Quelle neu generiert. Wir haben unsere Füße mit einem ziemlich einfachen Beispiel nass gemacht und die wichtigsten Aspekte berührt. Lassen Sie uns jetzt etwas Interessanteres tun.
Übung 2: Importierte Methodenaufrufe ersetzen
Für dieses Szenario haben wir ein „geometry“-Modul mit einer Methode namens „circleArea“, die wir zugunsten von „getCircleArea“ verworfen haben. Wir könnten diese leicht finden und durch /geometry\.circleArea/g
ersetzen, aber was ist, wenn der Benutzer das Modul importiert und ihm einen anderen Namen zugewiesen hat? Zum Beispiel:
import g from 'geometry'; const area = g.circleArea(radius);
Woher wissen wir, dass wir g.circleArea
anstelle von geometry.circleArea
ersetzen müssen? Wir können sicherlich nicht davon ausgehen, dass alle Aufrufe von circleArea
die sind, nach denen wir suchen, wir brauchen etwas Kontext. Hier zeigen Codemods ihren Wert. Beginnen wir damit, zwei Dateien zu erstellen, deprecated.js
und 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));
Führen Sie nun diesen Befehl aus, um den Codemod auszuführen.

jscodeshift -t ./deprecated.js ./deprecated.input.js -d -p
Sie sollten eine Ausgabe sehen, die anzeigt, dass die Transformation ausgeführt wurde, aber noch nichts geändert hat.
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
Wir müssen wissen, als was unser geometry
importiert wurde. Schauen wir uns den AST Explorer an und finden heraus, wonach wir suchen. Unser Import nimmt diese Form an.
{ "type": "ImportDeclaration", "specifiers": [ { "type": "ImportDefaultSpecifier", "local": { "type": "Identifier", "name": "g" } } ], "source": { "type": "Literal", "value": "geometry" } }
Wir können einen Objekttyp angeben, um eine Sammlung von Knoten wie folgt zu finden:
const importDeclaration = root.find(j.ImportDeclaration, { source: { type: 'Literal', value: 'geometry', }, });
Dadurch erhalten wir die ImportDeclaration, die zum Importieren von „Geometrie“ verwendet wird. Suchen Sie von dort aus nach dem lokalen Namen, der zum Speichern des importierten Moduls verwendet wird. Da dies das erste Mal ist, dass wir es getan haben, wollen wir beim ersten Start auf einen wichtigen und verwirrenden Punkt hinweisen.
Hinweis: Es ist wichtig zu wissen, dass root.find()
eine Sammlung von Knotenpfaden zurückgibt. Von dort gibt die Methode .get(n)
den Knotenpfad am Index n
in dieser Sammlung zurück, und um den tatsächlichen Knoten zu erhalten, verwenden wir .node
. Der Knoten ist im Grunde das, was wir im AST Explorer sehen. Denken Sie daran, dass der Knotenpfad hauptsächlich Informationen über den Bereich und die Beziehungen des Knotens enthält, nicht den Knoten selbst.
// 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;
Dadurch können wir dynamisch herausfinden, als was unser geometry
importiert wurde. Als nächstes finden wir die Orte, an denen es verwendet wird, und ändern sie. Wenn wir uns AST Explorer ansehen, können wir sehen, dass wir MemberExpressions finden müssen, die wie folgt aussehen:
{ "type": "MemberExpression", "object": { "name": "geometry" }, "property": { "name": "circleArea" } }
Denken Sie jedoch daran, dass unser Modul möglicherweise mit einem anderen Namen importiert wurde, also müssen wir dies berücksichtigen, indem wir unsere Abfrage stattdessen so aussehen lassen:
j.MemberExpression, { object: { name: localName, }, property: { name: "circleArea", }, })
Da wir nun eine Abfrage haben, können wir eine Sammlung aller Aufrufseiten für unsere alte Methode abrufen und sie dann mit der Methode replaceWith()
der Sammlung austauschen. Die Methode replaceWith()
durchläuft die Sammlung und übergibt jeden Knotenpfad an eine Callback-Funktion. Der AST-Knoten wird dann durch den Knoten ersetzt, den Sie vom Rückruf zurückgeben.
Sobald wir mit der Ersetzung fertig sind, generieren wir die Quelle wie gewohnt. Hier ist unsere fertige Transformation:
//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(); };
Wenn wir die Quelle durch die Transformation laufen lassen, sehen wir, dass der Aufruf der veralteten Methode im geometry
geändert wurde, aber der Rest unverändert gelassen wurde, etwa so:
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));
Übung 3: Ändern einer Methodensignatur
In den vorherigen Übungen haben wir das Abfragen von Sammlungen für bestimmte Arten von Knoten, das Entfernen von Knoten und das Ändern von Knoten behandelt, aber was ist mit dem Erstellen ganz neuer Knoten? Das werden wir in dieser Übung angehen.
In diesem Szenario haben wir eine Methodensignatur, die mit einzelnen Argumenten außer Kontrolle gerät, während die Software gewachsen ist, und daher wurde entschieden, dass es besser wäre, stattdessen ein Objekt zu akzeptieren, das diese Argumente enthält.
Statt car.factory('white', 'Kia', 'Sorento', 2010, 50000, null, true);
wir würden gerne sehen
const suv = car.factory({ color: 'white', make: 'Kia', model: 'Sorento', year: 2010, miles: 50000, bedliner: null, alarm: true, });
Beginnen wir damit, die Transformation und eine Eingabedatei zum Testen zu erstellen:
//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);
Unser Befehl zum Ausführen der Transformation jscodeshift -t signature-change.js signature-change.input.js -d -p
und die Schritte, die wir zum Ausführen dieser Transformation benötigen, sind:
- Suchen Sie den lokalen Namen für das importierte Modul
- Finden Sie alle Aufrufseiten für die .factory-Methode
- Lesen Sie alle übergebenen Argumente
- Ersetzen Sie diesen Aufruf durch ein einzelnes Argument, das ein Objekt mit den ursprünglichen Werten enthält
Mit dem AST Explorer und dem Prozess, den wir in den vorherigen Übungen verwendet haben, sind die ersten beiden Schritte einfach:
//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(); };
Um alle derzeit übergebenen Argumente zu lesen, verwenden wir die Methode replaceWith()
in unserer Sammlung von CallExpressions, um jeden der Knoten auszutauschen. Die neuen Knoten ersetzen node.arguments durch ein neues einzelnes Argument, ein Objekt.
Versuchen wir es mit einem einfachen Objekt, um sicherzustellen, dass wir wissen, wie das funktioniert, bevor wir die richtigen Werte verwenden:
.replaceWith(nodePath => { const { node } = nodePath; node.arguments = [{ foo: 'bar' }]; return node; })
Wenn wir dies ausführen ( jscodeshift -t signature-change.js signature-change.input.js -d -p
), explodiert die Transformation mit:
ERR signature-change.input.js Transformation error Error: {foo: bar} does not match type Printable
Es stellt sich heraus, dass wir nicht einfach einfache Objekte in unsere AST-Knoten stopfen können. Stattdessen müssen wir Builder verwenden, um die richtigen Knoten zu erstellen.
Knotenbauer
Mit Buildern können wir neue Knoten richtig erstellen. Sie werden von ast-types
bereitgestellt und durch jscodeshift aufgetaucht. Sie prüfen streng, ob die verschiedenen Arten von Knoten korrekt erstellt werden, was frustrierend sein kann, wenn Sie auf einer Rolle hacken, aber letztendlich ist dies eine gute Sache. Um zu verstehen, wie Builder verwendet werden, sollten Sie zwei Dinge beachten:
Alle verfügbaren AST-Knotentypen sind im def
-Ordner des Github-Projekts ast-types definiert, hauptsächlich in core.js. Es gibt Builder für alle AST-Knotentypen, aber sie verwenden eine Camel-Case-Version des Knotentyps, nicht Pascal -Fall. (Dies wird nicht explizit angegeben, aber Sie können sehen, dass dies in der ast-types-Quelle der Fall ist
Wenn wir AST Explorer mit einem Beispiel für das gewünschte Ergebnis verwenden, können wir dies ziemlich einfach zusammensetzen. In unserem Fall möchten wir, dass das neue einzelne Argument ein ObjectExpression mit einer Reihe von Eigenschaften ist. Wenn wir uns die oben erwähnten Typdefinitionen ansehen, können wir sehen, was dies bedeutet:
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"));
Der Code zum Erstellen eines AST-Knotens für { foo: 'bar' } würde also folgendermaßen aussehen:
j.objectExpression([ j.property( 'init', j.identifier('foo'), j.literal('bar') ) ]);
Nehmen Sie diesen Code und stecken Sie ihn wie folgt in unsere Transformation:
.replaceWith(nodePath => { const { node } = nodePath; const object = j.objectExpression([ j.property( 'init', j.identifier('foo'), j.literal('bar') ) ]); node.arguments = [object]; return node; })
Wenn Sie dies ausführen, erhalten wir das Ergebnis:
import car from 'car'; const suv = car.factory({ foo: "bar" }); const truck = car.factory({ foo: "bar" });
Jetzt, da wir wissen, wie man einen richtigen AST-Knoten erstellt, ist es einfach, die alten Argumente zu durchlaufen und stattdessen ein neues zu verwendendes Objekt zu generieren. So sieht unsere Datei signature-change.js
jetzt aus:
//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 }); };
Führen Sie die Transformation aus ( jscodeshift -t signature-change.js signature-change.input.js -d -p
) und wir sehen, dass die Signaturen wie erwartet aktualisiert wurden:
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 mit jscodeshift Zusammenfassung
Es hat ein wenig Zeit und Mühe gekostet, bis zu diesem Punkt zu gelangen, aber die Vorteile sind enorm, wenn man mit Massen-Refaktorisierung konfrontiert wird. Das Verteilen von Gruppen von Dateien auf verschiedene Prozesse und deren parallele Ausführung ist etwas, worin sich jscodeshift auszeichnet und es Ihnen ermöglicht, komplexe Transformationen in Sekundenschnelle über eine riesige Codebasis hinweg auszuführen. Wenn Sie sich mit Codemods besser auskennen, werden Sie damit beginnen, vorhandene Skripte (wie das Github-Repository von React-Codemod) umzufunktionieren oder Ihre eigenen für alle möglichen Aufgaben zu schreiben, und das wird Sie, Ihr Team und Ihre Paketbenutzer effizienter machen .