Quantas vezes você usou a funcionalidade localizar e substituir em um diretório para fazer alterações em arquivos de origem JavaScript? Se você for bom, você usou expressões regulares sofisticadas para capturar grupos, porque vale a pena o esforço se sua base de código for grande. Regex tem limites, no entanto. Para mudanças não triviais, você precisa de um desenvolvedor que entenda o código no contexto e também esteja disposto a assumir o processo longo, tedioso e sujeito a erros.
É aqui que entram os “codemods”.
Codemods são scripts usados para reescrever outros scripts. Pense neles como uma funcionalidade de localização e substituição que pode ler e escrever código. Você pode usá-los para atualizar o código-fonte para se adequar às convenções de codificação de uma equipe, fazer mudanças generalizadas quando uma API é modificada ou até mesmo corrigir automaticamente o código existente quando seu pacote público faz uma alteração significativa.
Neste artigo, vamos explorar um kit de ferramentas para codemods chamado “jscodeshift” enquanto cria três codemods de complexidade crescente. No final, você terá ampla exposição aos aspectos importantes de jscodeshift e estará pronto para começar a escrever seus próprios codemods. Passaremos por três exercícios que cobrem alguns usos básicos, mas impressionantes, de codemods, e você pode ver o código-fonte desses exercícios no meu projeto github .
O kit de ferramentas jscodeshift permite que você envie vários arquivos de origem por meio de uma transformação e substitua-os pelo que sai do outro lado. Dentro da transformação, você analisa a fonte em uma árvore de sintaxe abstrata (AST), vasculha para fazer suas alterações e, em seguida, regenera a fonte a partir do AST alterado.
por que json é melhor que xml
A interface que jscodeshift fornece é um wrapper em torno de recast
e ast-types
pacotes. recast
lida com a conversão da origem para AST e vice-versa enquanto ast-types
lida com a interação de baixo nível com os nós AST.
Para começar, instale o jscodeshift globalmente a partir do npm.
npm i -g jscodeshift
Existem opções de execução que você pode usar e uma configuração de teste opinativa que torna a execução de um conjunto de testes via Jest (uma estrutura de teste de JavaScript de código aberto) realmente fácil, mas vamos contornar isso por agora em favor da simplicidade:
jscodeshift -t some-transform.js input-file.js -d -p
Isso executará input-file.js
por meio da transformação some-transform.js
e imprima os resultados sem alterar o arquivo.
Antes de começar, entretanto, é importante entender três tipos de objetos principais com os quais a API jscodeshift lida: nós, caminhos de nós e coleções.
Os nós são os blocos de construção básicos do AST, muitas vezes chamados de 'nós AST'. Isso é o que você vê ao explorar seu código com o AST Explorer. Eles são objetos simples e não fornecem métodos.
Os caminhos dos nós são invólucros em torno de um nó AST fornecidos por ast-types
como uma forma de percorrer a árvore sintática abstrata (AST, lembra?). Isoladamente, os nós não têm nenhuma informação sobre seu pai ou escopo, então os caminhos dos nós cuidam disso. Você pode acessar o nó empacotado por meio do node
propriedade e existem vários métodos disponíveis para alterar o nó subjacente. caminhos de nó são frequentemente referidos apenas como 'caminhos'.
Coleções são grupos de zero ou mais caminhos de nó que a API jscodeshift retorna quando você consulta o AST. Eles têm todos os tipos de métodos úteis, alguns dos quais iremos explorar.
o node js funciona em todos os navegadores
As coleções contêm caminhos de nó, caminhos de nó contêm nós, e os nós são o que o AST é feito. Tenha isso em mente e será fácil entender a API de consulta jscodeshift.
Pode ser difícil acompanhar as diferenças entre esses objetos e seus respectivos recursos de API, então há uma ferramenta bacana chamada jscodeshift-helper que registra o tipo de objeto e fornece outras informações importantes.
Para começarmos, vamos começar removendo chamadas para todos os métodos de console em nossa base de código. Embora você possa fazer isso com localizar e substituir e um pouco de regex, começa a ficar complicado com instruções de várias linhas, literais de modelo e chamadas mais complexas, por isso é um exemplo ideal para começar.
Primeiro, crie dois arquivos, remove-consoles.js
e 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); };
Este é o comando que usaremos no terminal para empurrá-lo por meio do jscodeshift:
jscodeshift -t remove-consoles.js remove-consoles.input.js -d -p
Se tudo estiver configurado corretamente, ao executá-lo, você deverá ver algo assim.
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, isso foi um pouco anticlimático, uma vez que nossa transformação ainda não faz nada, mas pelo menos sabemos que está tudo funcionando. Se ele não funcionar, certifique-se de instalar o jscodeshift globalmente. Se o comando para executar a transformação estiver incorreto, você verá uma mensagem “ERROR Transform file… does not exist” ou “TypeError: path deve ser uma string ou Buffer” se o arquivo de entrada não puder ser encontrado. Se você errou em algo, deve ser fácil identificá-lo com os erros de transformação muito descritivos.
Relacionado: Folha de referências do JavaScript rápida e prática do ApeeScape: ES6 e alémNosso objetivo final, porém, após uma transformação bem-sucedida, é ver esta fonte:
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); };
Para chegar lá, precisamos converter a fonte em um AST, encontrar os consoles, removê-los e, em seguida, converter o AST alterado de volta à fonte. A primeira e a última etapa são fáceis, são apenas:
remove-consoles.js export default (fileInfo, api) => { const j = api.jscodeshift; const root = j(fileInfo.source); return root.toSource(); };
Mas como encontramos os consoles e os removemos? A menos que você tenha algum conhecimento excepcional da API do Mozilla Parser, provavelmente precisará de uma ferramenta para ajudar a entender a aparência do AST. Para isso, você pode usar o AST Explorer . Cole o conteúdo de remove-consoles.input.js
nele e você verá o AST. Existem muitos dados, mesmo no código mais simples, portanto, ajuda a ocultar dados e métodos de localização. Você pode alternar a visibilidade das propriedades no AST Explorer com as caixas de seleção acima da árvore.
Podemos ver que as chamadas para métodos de console são chamadas de CallExpressions
, então, como podemos encontrá-los em nossa transformação? Usamos as consultas do jscodeshift, lembrando nossa discussão anterior sobre as diferenças entre coleções, caminhos de nós e os próprios nós:
//remove-consoles.js export default (fileInfo, api) => { const j = api.jscodeshift; const root = j(fileInfo.source); return root.toSource(); };
A linha const root = j(fileInfo.source);
retorna uma coleção de um caminho de nó, que envolve o nó AST raiz. Podemos usar a coleção find
método para pesquisar nós descendentes de um determinado tipo, assim:
const callExpressions = root.find(j.CallExpression);
Isso retorna outra coleção de caminhos de nó contendo apenas os nós que são CallExpressions. À primeira vista, parece o que queremos, mas é muito amplo. Podemos acabar executando centenas ou milhares de arquivos por meio de nossas transformações, portanto, temos que ser precisos para ter a confiança de que funcionará conforme o esperado. O ingênuo find
acima não iria apenas encontrar as ExpressõesChamada do console, iria encontrar todas as ExpressõesChamada na fonte, incluindo
require('foo') bar() setTimeout(() => {}, 0)
Para forçar uma maior especificidade, fornecemos um segundo argumento para .find
: Um objeto de parâmetros adicionais, cada nó precisa ser incluído nos resultados. Podemos olhar o AST Explorer para ver se nossas chamadas do console. * Têm a forma de:
{ 'type': 'CallExpression', 'callee': { 'type': 'MemberExpression', 'object': { 'type': 'Identifier', 'name': 'console' } } }
Com esse conhecimento, sabemos como refinar nossa consulta com um especificador que retornará apenas o tipo de CallExpressions em que estamos interessados:
const callExpressions = root.find(j.CallExpression, { callee: { type: 'MemberExpression', object: { type: 'Identifier', name: 'console' }, }, });
Agora que temos uma coleção precisa dos sites de chamada, vamos removê-los do AST. Convenientemente, o tipo de objeto de coleção tem um remove
método que fará exatamente isso. Nosso remove-consoles.js
arquivo agora ficará assim:
//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(); };
Agora, se executarmos nossa transformação a partir da linha de comando usando jscodeshift -t remove-consoles.js remove-consoles.input.js -d -p
, devemos ver:
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
Isso parece bom. Agora que nossa transformação altera o AST subjacente, usando .toSource()
gera uma string diferente da original. A opção -p do nosso comando exibe o resultado e uma contagem das disposições para cada arquivo processado é mostrada na parte inferior. Remover a opção -d de nosso comando substituiria o conteúdo de remove-consoles.input.js pela saída da transformação.
Nosso primeiro exercício está completo ... quase. O código tem uma aparência bizarra e provavelmente muito ofensivo para qualquer purista funcional por aí, e para fazer o código de transformação fluir melhor, jscodeshift tornou a maioria das coisas encadeadas. Isso nos permite reescrever nossa transformação assim:
// 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(); };
Muito melhor. Para recapitular o exercício 1, envolvemos a fonte, consultamos uma coleção de caminhos de nó, alteramos o AST e, em seguida, regeneramos essa fonte. Começamos com um exemplo bastante simples e tocamos nos aspectos mais importantes. Agora, vamos fazer algo mais interessante.
o que é linguagem c no computador
Para este cenário, temos um módulo de “geometria” com um método chamado “circleArea” que substituímos por “getCircleArea”. Poderíamos facilmente localizar e substituir esses por /geometry.circleArea/g
, mas e se o usuário importou o módulo e atribuiu a ele um nome diferente? Por exemplo:
import g from 'geometry'; const area = g.circleArea(radius);
Como saberíamos substituir g.circleArea
em vez de geometry.circleArea
? Certamente não podemos assumir que todos circleArea
as chamadas são as que estamos procurando, precisamos de algum contexto. É aqui que os codemods começam a mostrar seu valor. Vamos começar criando dois arquivos, deprecated.js
e 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));
Agora execute este comando para executar o codemod.
jscodeshift -t ./deprecated.js ./deprecated.input.js -d -p
Você deve ver uma saída indicando que a transformação foi executada, mas ainda não mudou nada.
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
Precisamos saber o que nosso geometry
módulo foi importado como. Vamos dar uma olhada no AST Explorer e descobrir o que estamos procurando. Nossa importação assume este formato.
{ 'type': 'ImportDeclaration', 'specifiers': [ { 'type': 'ImportDefaultSpecifier', 'local': { 'type': 'Identifier', 'name': 'g' } } ], 'source': { 'type': 'Literal', 'value': 'geometry' } }
Podemos especificar um tipo de objeto para encontrar uma coleção de nós como este:
const importDeclaration = root.find(j.ImportDeclaration, { source: { type: 'Literal', value: 'geometry', }, });
Isso nos dá o ImportDeclaration usado para importar “geometria”. A partir daí, vá para baixo para encontrar o nome local usado para conter o módulo importado. Já que esta é a primeira vez que fazemos isso, vamos apontar um ponto importante e confuso ao começar.
Observação: é importante saber que root.find()
retorna uma coleção de caminhos de nós. A partir daí, o .get(n)
método retorna o caminho do nó no índice n
nessa coleção, e para obter o nó real, usamos .node
. O nó é basicamente o que vemos no AST Explorer. Lembre-se de que o caminho do nó consiste principalmente em informações sobre o escopo e os relacionamentos do nó, não o próprio nó.
// 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;
Isso nos permite descobrir dinamicamente o que nosso geometry
módulo foi importado como. Em seguida, encontramos os locais em que está sendo usado e os alteramos. Olhando para o AST Explorer, podemos ver que precisamos encontrar MemberExpressions parecidos com este:
{ 'type': 'MemberExpression', 'object': { 'name': 'geometry' }, 'property': { 'name': 'circleArea' } }
Lembre-se, porém, de que nosso módulo pode ter sido importado com um nome diferente, então temos que levar isso em consideração fazendo com que nossa consulta tenha a seguinte aparência:
j.MemberExpression, { object: { name: localName, }, property: { name: 'circleArea', }, })
Agora que temos uma consulta, podemos obter uma coleção de todos os sites de chamada para nosso método antigo e, em seguida, usar a coleção replaceWith()
método para trocá-los. O replaceWith()
método itera através da coleção, passando cada caminho de nó para uma função de retorno de chamada. O Nó AST é então substituído por qualquer Nó que você retornar do retorno de chamada.
Assim que terminarmos com a substituição, geramos a fonte normalmente. Aqui está nossa transformação final:
//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(); };
Quando executamos a origem por meio da transformação, vemos que a chamada para o método obsoleto no geometry
módulo foi alterado, mas o resto permaneceu inalterado, assim:
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));
Nos exercícios anteriores, cobrimos a consulta de coleções para tipos específicos de nós, removendo nós e alterando nós, mas que tal criar nós totalmente novos? É isso que abordaremos neste exercício.
dívida conversível vs capital conversível
Neste cenário, temos uma assinatura de método que ficou fora de controle com argumentos individuais conforme o software crescia e, portanto, foi decidido que seria melhor aceitar um objeto contendo esses argumentos.
Em vez de car.factory('white', 'Kia', 'Sorento', 2010, 50000, null, true);
nós gostaríamos de ver
const suv = car.factory({ color: 'white', make: 'Kia', model: 'Sorento', year: 2010, miles: 50000, bedliner: null, alarm: true, });
Vamos começar fazendo a transformação e um arquivo de entrada para testar:
//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);
Nosso comando para executar a transformação será jscodeshift -t signature-change.js signature-change.input.js -d -p
e as etapas que precisamos para realizar essa transformação são:
Usando o AST Explorer e o processo que usamos nos exercícios anteriores, as duas primeiras etapas são fáceis:
//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(); };
Para ler todos os argumentos que estão sendo transmitidos, usamos o replaceWith()
método em nossa coleção de CallExpressions para trocar cada um dos nós. Os novos nós substituirão node.arguments por um novo argumento único, um objeto.
Vamos tentar com um objeto simples para ter certeza de que sabemos como isso funciona antes de usar os valores adequados:
diferença entre s e c corp llc
.replaceWith(nodePath => { const { node } = nodePath; node.arguments = [{ foo: 'bar' }]; return node; })
Quando executamos isso (jscodeshift -t signature-change.js signature-change.input.js -d -p
), a transformação explodirá com:
ERR signature-change.input.js Transformation error Error: {foo: bar} does not match type Printable
Acontece que não podemos simplesmente colocar objetos simples em nossos nós AST. Em vez disso, precisamos usar construtores para criar nós adequados.
Relacionado: Contrate os 3% principais desenvolvedores de Javascript freelance.Os construtores nos permitem criar novos nós adequadamente; eles são fornecidos por ast-types
e surgiu por meio de jscodeshift. Eles verificam rigidamente se os diferentes tipos de nós foram criados corretamente, o que pode ser frustrante quando você está cortando um rolo, mas, no final das contas, isso é bom. Para entender como usar construtores, há duas coisas que você deve ter em mente:
Todos os tipos de nós AST disponíveis são definidos na pasta def
do projeto github ast-types , principalmente em core.js. Existem construtores para todos os tipos de nós AST, mas eles usam envolto em camelo versão do tipo de nó, não caso pascal . (Isso não é declarado explicitamente, mas você pode ver que é o caso no fonte ost-types
Se usarmos o AST Explorer com um exemplo de como queremos o resultado, podemos juntar as peças com bastante facilidade. Em nosso caso, queremos que o novo argumento único seja uma ObjectExpression com várias propriedades. Olhando para as definições de tipo mencionadas acima, podemos ver o que isso implica:
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'));
Portanto, o código para construir um nó AST para {foo: ‘bar’} seria semelhante a:
j.objectExpression([ j.property( 'init', j.identifier('foo'), j.literal('bar') ) ]);
Pegue esse código e conecte-o à nossa transformação assim:
.replaceWith(nodePath => { const { node } = nodePath; const object = j.objectExpression([ j.property( 'init', j.identifier('foo'), j.literal('bar') ) ]); node.arguments = [object]; return node; })
Executando isso nos dá o resultado:
import car from 'car'; const suv = car.factory({ foo: 'bar' }); const truck = car.factory({ foo: 'bar' });
Agora que sabemos como criar um nó AST adequado, é fácil percorrer os argumentos antigos e gerar um novo objeto para usar. Aqui está o nosso signature-change.js
arquivo se parece com agora:
//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 }); };
Execute a transformação (jscodeshift -t signature-change.js signature-change.input.js -d -p
) e veremos que as assinaturas foram atualizadas conforme o esperado:
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, });
Demorou um pouco e esforço para chegar a este ponto, mas os benefícios são enormes quando confrontados com a refatoração em massa. Distribuir grupos de arquivos para diferentes processos e executá-los em paralelo é algo em que jscodeshift se destaca, permitindo que você execute transformações complexas em uma enorme base de código em segundos. À medida que você se torna mais proficiente com codemods, começará a reaproveitar os scripts existentes (como o repositório github react-codemod ou escrever o seu próprio para todos os tipos de tarefas, e isso tornará você, sua equipe e seus usuários de pacotes mais eficientes.