Front-End tools: Transpiladores Javascript (parte 1)
Introdução
É com muito orgulho que dou início a essa jornada de mergulho nas ferramentas mais importantes do universo Javascript! Acredito que entender o funcionamento dessas ferramentas vai te tornar um programador melhor. Eu mesmo precisei estudar bastante, ler documentação, abrir código-fonte, fazer experimentos e, mesmo assim, ainda há muito que preciso aprender.
Então, combinado. Pegue um café, pois hoje falaremos sobre compiladores, ou melhor, transpiladores Javascript!
O Javascript vem evoluindo a cada ano e acredito que quem é Front-End deve estar atento a isso. O ES6 (ECMAScript 2015), por exemplo, trouxe diversas funcionalidades que facilitam demais o nosso dia a dia como desenvolvedores. Coisas como Promises, Arrow functions, Classes, entre várias outras funcionalidades que foram lançadas e que foram essenciais para a linguagem. E não para por aí: novas especificações são lançadas a cada ano e o Javascript se torna uma linguagem cada vez mais poderosa e versátil.
Parece tudo perfeito: podemos entregar mais escrevendo menos. Porém, existe a seguinte questão: o que acontece se quisermos dar suporte a browsers e ambientes mais antigos? Por exemplo, você pode usar o caniuse.com para verificar quais browsers e versões têm suporte a uma determinada funcionalidade. Veja, por exemplo, o suporte ao método de arrays toSorted, que faz parte do ES2023: https://caniuse.com/?search=toSorted. Se você olhar o Google Chrome, por exemplo, apenas as versões 110 em diante têm suporte a esse método. Ou seja, o usuário que estiver usando uma versão mais antiga provavelmente terá problemas. E no console vai aparecer algo como:
Uncaught TypeError: array.toSorted is not a function
at <anonymous>:1:8
É para esse tipo de problema que ferramentas como o Babel foram criadas. Pegando a definição direta da documentação: "O Babel é um conjunto de ferramentas usado principalmente para converter código ECMAScript 2015+ em uma versão JavaScript compatível com versões anteriores em navegadores ou ambientes atuais e mais antigos." Neste artigo, usaremos o Babel como um "estudo de caso". Porém, o conhecimento que você adquirir aqui será útil para o entendimento de várias outras ferramentas que abordaremos em artigos futuros. Vale lembrar que o Babel não é o único transpilador do mercado. O próprio tsc do TypeScript pode desempenhar esse papel em alguns projetos, assim como o swc, que talvez seja um concorrente direto do Babel e, possivelmente, o mais recomendado para projetos modernos. A ideia deste artigo não é comparar ferramentas, mas sim mostrar como elas funcionam e quais problemas elas resolvem.
Entendendo o Babel
Como citei anteriormente, o Babel é um transpilador de JavaScript. Um transpilador, também conhecido como source-to-source compiler, é um tipo de compilador que converte código de uma linguagem para a mesma linguagem, mas em uma versão diferente.
Recomendo assistir ao vídeo do Fabio Akita: Linguagem Compilada vs Interpretada | Qual é melhor?, para entender melhor como um compilador funciona. Compiladores são um tema extenso e exigiriam diversos artigos para cobrir todo o assunto. Além disso, também não sou especialista nesse tema.
Utilizando o Babel
Para falar a verdade, dificilmente em um projeto você usará apenas o Babel. Provavelmente, irá utilizá-lo junto de um bundler como o Webpack. Porém, o objetivo deste artigo é mostrar como o Babel funciona, então vamos usá-lo de forma isolada. Para isso, você pode instalar o pacote @babel/core para uso programático, ou o @babel/cli para transpilar arquivos pela linha de comando. Para facilitar, usaremos a segunda opção.
Os próximos passos serão em formato de tutorial, pois é interessante que você teste e entenda como tudo funciona.
Supondo que você já tenha o Node e o npm instalados na sua máquina, crie uma pasta no seu computador e siga estes 3 passos dentro dela:
- Inicie o projeto:
npm init -y
- Instale o @babel/cli como dependência de desenvolvimento:
npm i @babel/cli -D
- Adicione alguns scripts ao
package.json
para facilitar o desenvolvimento:
"scripts": {
"transpile": "babel src --out-dir lib",
"execute": "node lib/index.js"
}
Agora que temos todo o setup pronto, crie uma pasta chamada src
e, dentro dela, um arquivo index.js
. O conteúdo desse arquivo será uma função simples que retorna a role do usuário:
const getUserRole = (user) => {
return user.role ?? 'guest';
}
console.log(getUserRole({ name: 'Pedro', role: 'admin' })); // admin
console.log(getUserRole({ name: 'Ana' })); // guest
Por fim, basta executar o comando: npm run transpile
. Como você deve ter percebido, nada aconteceu. O código dentro da pasta de saída lib é o mesmo:
lib/index.js
const getUserRole = user => {
return user.role ?? 'guest';
};
console.log(getUserRole({
name: 'Pedro',
role: 'admin'
})); // admin
console.log(getUserRole({
name: 'Ana'
})); // guest
Por padrão, o Babel não faz nada sozinho. Você precisa informar quais transformações de código deseja realizar. É aí que entra o nosso primeiro conceito: plugins.
Plugins
Um plugin do Babel é responsável por transformar sintaxe moderna em sintaxe antiga. Existe uma lista de plugins que podemos utilizar. Neste exemplo, usarei dois:
- babel-plugin-transform-arrow-functions: Responsável por transformar todas as arrow functions encontradas no código-fonte em functions tradicionais.
- babel-plugin-transform-nullish-coalescing-operator: Responsável por transformar todas as ocorrências do operador Nullish Coalescing em código compatível com browsers antigos.
Para isso, adicionaremos mais dois passos:
- Instalar esses dois plugins via npm:
npm i @babel/plugin-transform-nullish-coalescing-operator @babel/plugin-transform-arrow-functions -D
- Criar um arquivo de configuração chamado
babel.config.json
e adicionar os plugins nele:
{
"plugins": [
"@babel/plugin-transform-arrow-functions",
"@babel/plugin-transform-nullish-coalescing-operator"
]
}
Se você executar o comando npm run transpile
novamente, notará que as transformações foram aplicadas no arquivo de saída:
lib/index.js
const getUserRole = function (user) {
var _user$role;
return (_user$role = user.role) !== null && _user$role !== void 0 ? _user$role : 'guest';
};
console.log(getUserRole({
name: 'Pedro',
role: 'admin'
})); // admin
console.log(getUserRole({
name: 'Ana'
})); // guest
Legal, mas você deve estar se fazendo as seguintes perguntas:
- Preciso adicionar todos os plugins manualmente?
- Como vou saber quais plugins preciso adicionar?
Bem, agora vamos para o nosso segundo conceito: presets
Presets
Um preset no Babel é um conjunto de plugins de transformação. O @babel/preset-env agrupa todos os plugins necessários para transformar recursos modernos do JavaScript com base nos ambientes que você deseja suportar.
Vamos instalar o babel-preset-env (npm i @babel/preset-env
) e atualizar o arquivo de configuração:
{
"presets": [
[
"@babel/preset-env",
{
"targets": {
"browsers": [
"last 2 versions",
"not dead"
]
}
}
]
]
}
Os targets adicionados informam ao @babel/preset-env
duas coisas:
- "last 2 versions": As duas versões mais recentes de cada navegador principal (Chrome, Firefox, Safari, Edge, etc).
- "not dead": Exclui navegadores que não recebem mais atualizações ou têm pouquíssimos usuários.
Essa configuração tem um coverage global de 65.4% no momento em que este artigo foi postado, segundo o site browsersl.ist. A partir disso, o preset identifica quais browsers e versões precisam de suporte e quais plugins devem ser adicionados. Particularmente, acho isso fantástico.
Para visualizar exatamente quais plugins são adicionados, basta incluir a flag debug nas configurações:
{
"presets": [
[
"@babel/preset-env",
{
"targets": {
"browsers": [
"last 2 versions",
"not dead"
]
},
"debug": true
}
]
]
}
Rode o comando transpile
novamente e veja o resultado. O código provavelmente não sofrerá alterações porque estamos dando suporte a versões bem recentes, e no terminal você deve ter visto as seguintes mensagens devido ao debug ativado:
Using targets:
{
"android": "136",
"chrome": "136",
"edge": "136",
"firefox": "138",
"ios": "18.4",
"opera": "116",
"opera_mobile": "80",
"safari": "18.4",
"samsung": "27"
}
Using plugins:
transform-duplicate-named-capturing-groups-regex { opera_mobile, samsung }
transform-regexp-modifiers { ios, opera_mobile, safari }
syntax-class-static-block
syntax-private-property-in-object
syntax-class-properties
syntax-numeric-separator
syntax-nullish-coalescing-operator
syntax-optional-chaining
syntax-json-strings
syntax-optional-catch-binding
syntax-async-generators
syntax-object-rest-spread
proposal-export-namespace-from { }
transform-modules-commonjs
proposal-dynamic-import
syntax-top-level-await
syntax-import-meta
Ou seja, você consegue visualizar quais browsers/versões e plugins foram adicionados. Apenas um adendo:
- Com transform-* e { targets }: Esses recursos precisam de transformação nos browsers citados.
- Com syntax-* e sem transform-*: Esses plugins foram ativados apenas para que o Babel aceite o código com essa sintaxe, mas não o transforme para versões mais antigas. Por isso, você deve ter percebido que o nullish-coalescing-operator não foi transformado.
Polyfill
Polyfill é um código JavaScript que implementa uma funcionalidade nativa que não existe em um ambiente específico. É uma "simulação" de uma funcionalidade nova para que ela funcione em navegadores antigos.
Aqui vai um exemplo simples de um polyfill para o método includes do array:
if (!Array.prototype.includes) {
Array.prototype.includes = function (element) {
return this.indexOf(element) !== -1;
};
}
Nem tudo pode ser feito através de plugins (transformações), por isso o @babel/preset-env
trabalha em conjunto com a biblioteca core-js para identificar quais polyfills devem ser adicionados com base nos targets. Para isso, podemos instalar o core-js e atualizar nosso arquivo de configuração para ativar a adição de polyfills.
{
"presets": [
[
"@babel/preset-env",
{
"targets": {
"browsers": [
"last 2 versions",
"not dead"
]
},
"debug": true,
"useBuiltIns": "usage",
"corejs": 3
}
]
]
}
Para simular a adição de polyfills, vamos simular o suporte ao IE 11, que exigirá muitas transformações e atualizar o nosso código exemplo:
src/index.js
const getUserRole = async (user) => {
return user.role ?? 'guest';
}
async function run() {
const user1 = { name: 'Pedro', role: 'admin' };
const user2 = { name: 'Ana' };
console.log(await getUserRole(user1)); // admin
console.log(await getUserRole(user2)); // guest
}
run()
babel.config.json
{
"presets": [
[
"@babel/preset-env",
{
"targets": {
"browsers": [
"last 2 versions",
"not dead",
"ie >= 11"
]
},
"debug": true,
"useBuiltIns": "usage",
"corejs": 3
}
]
]
}
Você vai perceber que ao dar suporte ao Internet Explorer, a transpilação gerou um código bem maior e com alguns polyfills:
require("core-js/modules/es.symbol.js");
require("core-js/modules/es.symbol.description.js");
require("core-js/modules/es.object.get-prototype-of.js");
require("core-js/modules/es.object.to-string.js");
require("core-js/modules/es.promise.js");
// Continua...
Considerações finais
- Avalie bem quais browsers você deseja suportar. Quanto mais antigos, maior será o JavaScript gerado e pior será a performance.
- Você pode utilizar o site browsersl.ist para ajudar a identificar a audiência ou usar dados de analytics do seu site para entender quais browsers seus usuários utilizam.
- Crie um arquivo chamado
.browserslistrc
e adicione os targets lá. Assim, outras ferramentas como Autoprefixer e Eslint também podem usar as mesmas configurações. - Existem outros presets famosos no mercado, como o babel-preset-react e o babel-preset-typescript para transpilação de JSX e TypeScript.
Próximo artigo
No próximo artigo, continuaremos no assunto de transpiladores, mas agora entendendo mais sobre como o Babel faz esse processo de transpilação. Aprenderemos o que é uma Abstract Syntax Tree e criaremos nosso próprio plugin do Babel. Você também aprenderá que os conceitos que iremos ver também são aplicados em várias outras bibliotecas.