Napisz kod, aby przepisać swój kod: jscodeshift

Opublikowany: 2022-03-11

Codemods z jscodeshift

Ile razy używałeś funkcji „znajdź i zamień” w katalogu, aby dokonać zmian w plikach źródłowych JavaScript? Jeśli jesteś dobry, przyzwyczaiłeś się i użyłeś wyrażeń regularnych z grupami przechwytującymi, ponieważ jest to warte wysiłku, jeśli baza kodu jest pokaźna. Regex ma jednak ograniczenia. W przypadku nietrywialnych zmian potrzebujesz programisty, który rozumie kod w kontekście i jest również chętny do podjęcia długiego, żmudnego i podatnego na błędy procesu.

Tutaj wkraczają „modyfikacje kodowe”.

Codemods to skrypty używane do przepisywania innych skryptów. Pomyśl o nich jako o funkcji znajdowania i zastępowania, która może czytać i pisać kod. Możesz ich używać do aktualizowania kodu źródłowego, aby pasował do konwencji kodowania zespołu, wprowadzania szeroko zakrojonych zmian, gdy interfejs API jest modyfikowany, a nawet automatycznego poprawiania istniejącego kodu, gdy Twój pakiet publiczny wprowadza przełomową zmianę.

Zestaw narzędzi jscodeshift doskonale nadaje się do pracy z modyfikacjami kodu.

Pomyśl o codemods jako o skryptowanej funkcji znajdowania i zastępowania, która może czytać i pisać kod.
Ćwierkać

W tym artykule przyjrzymy się zestawowi narzędzi do kodowania o nazwie „jscodeshift”, tworząc jednocześnie trzy kodody o coraz większej złożoności. Pod koniec będziesz miał szeroką ekspozycję na ważne aspekty jscodeshift i będziesz gotowy do rozpoczęcia pisania własnych modów kodowych. Przejdziemy przez trzy ćwiczenia, które obejmują kilka podstawowych, ale niesamowitych zastosowań codemodów, a kod źródłowy tych ćwiczeń możesz wyświetlić w moim projekcie na githubie.

Co to jest jscodeshift?

Zestaw narzędzi jscodeshift umożliwia pompowanie wielu plików źródłowych przez transformację i zastępowanie ich tym, co pochodzi z drugiego końca. Wewnątrz transformacji analizujesz źródło w abstrakcyjne drzewo składni (AST), przeglądasz, aby wprowadzić zmiany, a następnie regenerujesz źródło ze zmienionego AST.

Interfejs udostępniany przez jscodeshift jest opakowaniem otaczającym pakiety recast i ast-types . recast obsługuje konwersję ze źródła do AST iz powrotem, podczas gdy ast-types obsługują interakcję niskiego poziomu z węzłami AST.

Organizować coś

Aby rozpocząć, zainstaluj jscodeshift globalnie z npm.

 npm i -g jscodeshift

Istnieją opcje uruchamiania, z których możesz skorzystać, oraz uparty zestaw testów, który sprawia, że ​​uruchamianie zestawu testów za pośrednictwem Jest (szkielet testowy JavaScript o otwartym kodzie źródłowym) jest naprawdę łatwe, ale na razie ominiemy to na rzecz prostoty:

jscodeshift -t some-transform.js input-file.js -d -p

Spowoduje to uruchomienie input-file.js przez transformację some-transform.js i wydrukowanie wyników bez zmiany pliku.

Zanim jednak zaczniesz, ważne jest, aby zrozumieć trzy główne typy obiektów, którymi zajmuje się interfejs API jscodeshift: węzły, ścieżki do węzłów i kolekcje.

Węzły

Węzły są podstawowymi elementami składowymi AST, często określanymi jako „węzły AST”. Oto, co widzisz podczas eksploracji kodu za pomocą AST Explorer. Są to proste obiekty i nie dostarczają żadnych metod.

Ścieżki węzłów

Ścieżki węzłów to opakowania wokół węzła AST dostarczane przez ast-types jako sposób na przechodzenie przez abstrakcyjne drzewo składni (AST, pamiętasz?). W odosobnieniu węzły nie mają żadnych informacji o swoim rodzicu ani zasięgu, więc troszczą się o to ścieżki node-path. Dostęp do opakowanego węzła można uzyskać za pomocą właściwości node i dostępnych jest kilka metod zmiany węzła bazowego. ścieżki węzłów są często nazywane po prostu „ścieżkami”.

Kolekcje

Kolekcje to grupy zawierające zero lub więcej ścieżek węzłów, które interfejs API jscodeshift zwraca podczas wykonywania zapytania do AST. Mają wiele przydatnych metod, z których niektóre omówimy.

Kolekcje zawierają ścieżki węzłów, ścieżki węzłów zawierają węzły, a węzły są tym, z czego składa się AST. Pamiętaj o tym, a zrozumienie interfejsu API zapytań jscodeshift będzie łatwe.

Śledzenie różnic między tymi obiektami i ich odpowiednimi możliwościami API może być trudne, więc istnieje sprytne narzędzie o nazwie jscodeshift-helper, które rejestruje typ obiektu i dostarcza inne kluczowe informacje.

Ważna jest znajomość różnicy między węzłami, ścieżkami węzłów i kolekcjami.

Ważna jest znajomość różnicy między węzłami, ścieżkami węzłów i kolekcjami.

Ćwiczenie 1: Usuń wywołania do konsoli

Aby zmoknąć nogi, zacznijmy od usunięcia wywołań wszystkich metod konsoli w naszej bazie kodu. Chociaż możesz to zrobić za pomocą funkcji znajdowania i zastępowania oraz odrobiny wyrażeń regularnych, zaczyna być to trudne z instrukcjami wielowierszowymi, literałami szablonów i bardziej złożonymi wywołaniami, więc jest to idealny przykład na początek.

Najpierw utwórz dwa pliki, remove-consoles.js i 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); };

Oto polecenie, którego użyjemy w terminalu, aby przepchnąć go przez jscodeshift:

jscodeshift -t remove-consoles.js remove-consoles.input.js -d -p

Jeśli wszystko jest poprawnie skonfigurowane, po uruchomieniu powinieneś zobaczyć coś takiego.

 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, to było trochę nieprzyjemne, ponieważ nasza transformata jeszcze nic nie robi, ale przynajmniej wiemy, że wszystko działa. Jeśli w ogóle nie działa, upewnij się, że zainstalowałeś jscodeshift globalnie. Jeśli polecenie uruchomienia transformacji jest nieprawidłowe, zobaczysz komunikat „BŁĄD Przekształć plik … nie istnieje” lub „Błąd typu: ścieżka musi być ciągiem lub buforem”, jeśli nie można znaleźć pliku wejściowego. Jeśli masz coś tłustego, powinno to być łatwe do zauważenia dzięki bardzo opisowym błędom transformacji.

Powiązane: Szybka i praktyczna ściągawka JavaScript Toptal: ES6 i więcej

Jednak naszym celem końcowym, po udanej transformacji, jest zobaczenie tego źródła:

 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); };

Aby się tam dostać, musimy przekonwertować źródło do AST, znaleźć konsole, usunąć je, a następnie przekonwertować zmienione AST z powrotem na źródło. Pierwsze i ostatnie kroki są proste, wystarczy:

 remove-consoles.js export default (fileInfo, api) => { const j = api.jscodeshift; const root = j(fileInfo.source); return root.toSource(); };

Ale jak znaleźć konsole i je usunąć? Jeśli nie masz wyjątkowej wiedzy na temat Mozilla Parser API, prawdopodobnie będziesz potrzebować narzędzia, które pomoże zrozumieć, jak wygląda AST. Do tego możesz użyć Eksploratora AST. Wklej do niego zawartość remove-consoles.input.js , a zobaczysz AST. Nawet w najprostszym kodzie jest dużo danych, więc pomaga ukryć dane i metody lokalizacji. Możesz przełączać widoczność właściwości w AST Explorer za pomocą pól wyboru nad drzewem.

Widzimy, że wywołania metod konsoli są określane jako CallExpressions , więc jak je znaleźć w naszej transformacji? Korzystamy z zapytań jscodeshift, pamiętając naszą wcześniejszą dyskusję na temat różnic między kolekcjami, ścieżkami do węzłów i samymi węzłami:

 //remove-consoles.js export default (fileInfo, api) => { const j = api.jscodeshift; const root = j(fileInfo.source); return root.toSource(); };

Wiersz const root = j(fileInfo.source); zwraca kolekcję jednej ścieżki węzła, która otacza główny węzeł AST. Możemy użyć metody find z kolekcji, aby wyszukać potomne węzły określonego typu, na przykład:

 const callExpressions = root.find(j.CallExpression);

Zwraca to inną kolekcję ścieżek węzłów zawierających tylko węzły, które są CallExpressions. Na pierwszy rzut oka wydaje się, że to jest to, czego chcemy, ale jest zbyt szeroki. Możemy w końcu uruchomić setki lub tysiące plików przez nasze przekształcenia, więc musimy być precyzyjni, aby mieć pewność, że będzie działać zgodnie z przeznaczeniem. Powyższe naiwne find nie tylko znajdzie konsolę CallExpressions, ale także znajdzie wszystkie CallExpression w źródle, w tym

 require('foo') bar() setTimeout(() => {}, 0)

Aby wymusić większą szczegółowość, podajemy drugi argument do .find : Obiekt z dodatkowymi parametrami, każdy węzeł musi być uwzględniony w wynikach. Możemy spojrzeć na AST Explorer, aby zobaczyć, że nasza konsola.* wywołania mają postać:

 { "type": "CallExpression", "callee": { "type": "MemberExpression", "object": { "type": "Identifier", "name": "console" } } }

Dzięki tej wiedzy wiemy, że możemy udoskonalić nasze zapytanie za pomocą specyfikatora, który zwróci tylko typ CallExpressions, który nas interesuje:

 const callExpressions = root.find(j.CallExpression, { callee: { type: 'MemberExpression', object: { type: 'Identifier', name: 'console' }, }, });

Teraz, gdy mamy dokładny zbiór witryn z połączeniami, usuńmy je z AST. Dogodnie typ obiektu kolekcji ma metodę remove , która właśnie to zrobi. Nasz plik remove-consoles.js będzie teraz wyglądał tak:

 //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(); };

Teraz, jeśli uruchomimy naszą transformację z wiersza poleceń za pomocą jscodeshift -t remove-consoles.js remove-consoles.input.js -d -p , powinniśmy zobaczyć:

 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

Wygląda dobrze. Teraz, gdy nasza transformacja zmienia bazowy AST, użycie .toSource() generuje inny ciąg niż oryginał. Opcja -p z naszego polecenia wyświetla wynik, a na dole wyświetlana jest suma dyspozycji dla każdego przetworzonego pliku. Usunięcie opcji -d z naszego polecenia zastąpiłoby zawartość remove-consoles.input.js danymi wyjściowymi z transformacji.

Nasze pierwsze ćwiczenie jest zakończone… prawie. Kod wygląda dziwacznie i prawdopodobnie jest bardzo obraźliwy dla wszelkich funkcjonalnych purystów, więc aby usprawnić przepływ kodu transformacji, jscodeshift umożliwił tworzenie łańcuchów większości rzeczy. To pozwala nam przepisać naszą transformację w następujący sposób:

 // 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(); };

Dużo lepiej. Podsumowując ćwiczenie 1, opakowaliśmy źródło, zapytaliśmy o kolekcję ścieżek węzłów, zmieniliśmy AST, a następnie ponownie wygenerowaliśmy to źródło. Zmoczyliśmy sobie stopy dość prostym przykładem i poruszyliśmy najważniejsze aspekty. A teraz zróbmy coś ciekawszego.

Ćwiczenie 2: Zastępowanie wywołań importowanych metod

W tym scenariuszu mamy moduł „geometria” z metodą o nazwie „circleArea”, którą zastąpiliśmy „getCircleArea”. Moglibyśmy łatwo je znaleźć i zastąpić /geometry\.circleArea/g , ale co, jeśli użytkownik zaimportował moduł i przypisał mu inną nazwę? Na przykład:

 import g from 'geometry'; const area = g.circleArea(radius);

Skąd mielibyśmy wiedzieć, że należy zastąpić g.circleArea zamiast geometry.circleArea ? Z pewnością nie możemy zakładać, że wszystkie wywołania circleArea są tymi, których szukamy, potrzebujemy kontekstu. W tym miejscu kodmody zaczynają pokazywać swoją wartość. Zacznijmy od stworzenia dwóch plików, deprecated.js i 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));

Teraz uruchom to polecenie, aby uruchomić codemod.

jscodeshift -t ./deprecated.js ./deprecated.input.js -d -p

Powinieneś zobaczyć dane wyjściowe wskazujące, że transformacja została uruchomiona, ale jeszcze niczego nie zmieniła.

 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

Musimy wiedzieć, w jakim formacie został zaimportowany nasz moduł geometry . Spójrzmy na AST Explorer i dowiedzmy się, czego szukamy. Nasz import przybiera taką formę.

 { "type": "ImportDeclaration", "specifiers": [ { "type": "ImportDefaultSpecifier", "local": { "type": "Identifier", "name": "g" } } ], "source": { "type": "Literal", "value": "geometry" } }

Możemy określić typ obiektu, aby znaleźć kolekcję węzłów w następujący sposób:

 const importDeclaration = root.find(j.ImportDeclaration, { source: { type: 'Literal', value: 'geometry', }, });

W ten sposób otrzymujemy ImportDeclaration używaną do importowania „geometrii”. Stamtąd wyszukaj lokalną nazwę używaną do przechowywania importowanego modułu. Ponieważ robimy to po raz pierwszy, zwróćmy uwagę na ważny i mylący punkt przy pierwszym uruchomieniu.

Uwaga: Ważne jest, aby wiedzieć, że root.find() zwraca kolekcję ścieżek do węzłów. Stamtąd metoda .get(n) zwraca ścieżkę węzła pod indeksem n w tej kolekcji, a aby uzyskać rzeczywisty węzeł, używamy .node . Węzeł jest w zasadzie tym, co widzimy w AST Explorer. Pamiętaj, że ścieżka węzła to głównie informacje o zasięgu i relacjach węzła, a nie o samym węźle.

 // 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;

To pozwala nam dynamicznie zorientować się, jaki nasz moduł geometry został zaimportowany. Następnie znajdujemy miejsca, w których jest używany i zmieniamy je. Patrząc na AST Explorer, widzimy, że musimy znaleźć MemberExpressions, które wyglądają tak:

 { "type": "MemberExpression", "object": { "name": "geometry" }, "property": { "name": "circleArea" } }

Pamiętaj jednak, że nasz moduł mógł zostać zaimportowany pod inną nazwą, więc musimy to uwzględnić, sprawiając, że nasze zapytanie będzie wyglądało tak:

 j.MemberExpression, { object: { name: localName, }, property: { name: "circleArea", }, })

Teraz, gdy mamy zapytanie, możemy pobrać kolekcję wszystkich witryn wywołujących naszą starą metodę, a następnie użyć metody replaceWith() kolekcji, aby je zamienić. Metoda replaceWith() iteruje po kolekcji, przekazując każdą ścieżkę węzła do funkcji zwrotnej. Węzeł AST jest następnie zastępowany dowolnym węzłem zwróconym z wywołania zwrotnego.

Mody kodowe pozwalają na tworzenie skryptów „inteligentnych” rozważań dotyczących refaktoryzacji.

Ponownie zrozumienie różnicy między kolekcjami, ścieżkami węzłów i węzłami jest konieczne, aby miało to sens.

Po zakończeniu wymiany generujemy źródło jak zwykle. Oto nasza gotowa transformacja:

 //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(); };

Kiedy przeprowadzamy źródło przez transformację, widzimy, że wywołanie przestarzałej metody w module geometry zostało zmienione, ale reszta pozostała niezmieniona, na przykład:

 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));

Ćwiczenie 3: Zmiana sygnatury metody

W poprzednich ćwiczeniach omówiliśmy odpytywanie kolekcji dla określonych typów węzłów, usuwanie węzłów i modyfikowanie węzłów, ale co z tworzeniem zupełnie nowych węzłów? Tym zajmiemy się w tym ćwiczeniu.

W tym scenariuszu mamy sygnaturę metody, która wymknęła się spod kontroli z poszczególnymi argumentami w miarę rozwoju oprogramowania, więc zdecydowano, że lepiej byłoby zamiast tego zaakceptować obiekt zawierający te argumenty.

Zamiast car.factory('white', 'Kia', 'Sorento', 2010, 50000, null, true);

chcielibyśmy zobaczyć

 const suv = car.factory({ color: 'white', make: 'Kia', model: 'Sorento', year: 2010, miles: 50000, bedliner: null, alarm: true, });

Zacznijmy od przekształcenia i pliku wejściowego do przetestowania:

 //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);

Naszym poleceniem do uruchomienia transformacji będzie jscodeshift -t signature-change.js signature-change.input.js -d -p , a kroki potrzebne do wykonania tej transformacji to:

  • Znajdź lokalną nazwę importowanego modułu
  • Znajdź wszystkie strony połączeń do metody .factory
  • Przeczytaj wszystkie przekazywane argumenty
  • Zastąp to wywołanie pojedynczym argumentem, który zawiera obiekt z oryginalnymi wartościami

Korzystając z AST Explorer i procesu, którego używaliśmy w poprzednich ćwiczeniach, pierwsze dwa kroki są łatwe:

 //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(); };

Aby odczytać wszystkie argumenty, które są aktualnie przekazywane, używamy metody replaceWith() w naszej kolekcji CallExpressions, aby zamienić każdy z węzłów. Nowe węzły zastąpią node.arguments nowym pojedynczym argumentem, obiektem.

Łatwo zamień argumenty metod za pomocą jscodeshift!

Zmień sygnatury metod za pomocą „replacewith()” i zamień całe węzły.

Spróbujmy z prostym obiektem, aby upewnić się, że wiemy, jak to działa, zanim użyjemy odpowiednich wartości:

 .replaceWith(nodePath => { const { node } = nodePath; node.arguments = [{ foo: 'bar' }]; return node; })

Kiedy uruchomimy to ( jscodeshift -t signature-change.js signature-change.input.js -d -p ), transformacja wybuchnie z:

 ERR signature-change.input.js Transformation error Error: {foo: bar} does not match type Printable

Okazuje się, że nie możemy po prostu zakleszczyć zwykłych obiektów w naszych węzłach AST. Zamiast tego musimy użyć budowniczych, aby stworzyć odpowiednie węzły.

Powiązane: Zatrudnij najlepszych 3% niezależnych programistów Javascript.

Konstruktorzy węzłów

Konstruktorzy pozwalają nam prawidłowo tworzyć nowe węzły; są one dostarczane przez ast-types i wyświetlane przez jscodeshift. Ściśle sprawdzają, czy różne typy węzłów są tworzone poprawnie, co może być frustrujące, gdy hakujesz na rolce, ale ostatecznie jest to dobra rzecz. Aby zrozumieć, jak korzystać z kreatorów, należy pamiętać o dwóch rzeczach:

Wszystkie dostępne typy węzłów AST są zdefiniowane w folderze def projektu ast-types na github, głównie w core.js Istnieją narzędzia do tworzenia wszystkich typów węzłów AST, ale używają one wersji typu węzła z wielbłądami, a nie pascal -walizka. (Nie jest to wyraźnie określone, ale możesz zobaczyć, że tak jest w źródle ast-types)

Jeśli użyjemy AST Explorer z przykładem tego, jaki ma być wynik, możemy to dość łatwo połączyć. W naszym przypadku chcemy, aby nowy pojedynczy argument był ObjectExpression z kilkoma właściwościami. Patrząc na wspomniane powyżej definicje typów, możemy zobaczyć, co to oznacza:

 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"));

Tak więc kod do zbudowania węzła AST dla { foo: 'bar' } wyglądałby tak:

 j.objectExpression([ j.property( 'init', j.identifier('foo'), j.literal('bar') ) ]);

Weź ten kod i podłącz go do naszej transformacji w ten sposób:

 .replaceWith(nodePath => { const { node } = nodePath; const object = j.objectExpression([ j.property( 'init', j.identifier('foo'), j.literal('bar') ) ]); node.arguments = [object]; return node; })

Uruchomienie tego daje nam wynik:

 import car from 'car'; const suv = car.factory({ foo: "bar" }); const truck = car.factory({ foo: "bar" });

Teraz, gdy wiemy, jak utworzyć właściwy węzeł AST, łatwo jest przejść przez stare argumenty i zamiast tego wygenerować nowy obiekt do użycia. Oto jak teraz wygląda nasz plik 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 }); };

Uruchom transformację ( jscodeshift -t signature-change.js signature-change.input.js -d -p ), a zobaczysz, że podpisy zostały zaktualizowane zgodnie z oczekiwaniami:

 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 z jscodeshift Recap

Dotarcie do tego punktu zajęło trochę czasu i wysiłku, ale korzyści są ogromne w obliczu masowej refaktoryzacji. Dystrybucja grup plików do różnych procesów i równoległe ich uruchamianie to coś, w czym wyróżnia się jscodeshift, umożliwiając uruchamianie złożonych przekształceń w ogromnej bazie kodu w ciągu kilku sekund. Gdy staniesz się bardziej biegły w korzystaniu z codemodów, zaczniesz zmieniać przeznaczenie istniejących skryptów (takich jak repozytorium github React-codemod lub pisanie własnego do różnego rodzaju zadań, co sprawi, że Ty, Twój zespół i użytkownicy Twoich pakietów będą bardziej wydajni .