Usando ES Modules (ESM) em Node.js: Um guia Prático - Parte 1

oieduardorabelo

Eduardo Rabelo

Posted on February 15, 2021

Usando ES Modules (ESM) em Node.js: Um guia Prático - Parte 1

Créditos

(Ei, se você quiser vir trabalhar comigo na Roundforest e experimentar ESM em Node.js, sinta-se à vontade para entrar em contato no LinkedIn ou no Twitter)

Módulos ES são o futuro dos módulos em JavaScript. Eles já são a regra no frontend, mas até agora não eram usados ​​no Node.js. Agora nós podemos! Além disso, a comunidade Node.js está trabalhando rapidamente para adicionar suporte para ESM no Node.js. Isso inclui ferramentas como Mocha, Ava e até Jest (embora no Jest o suporte seja incremental). Além disso, ESlint e TypeScript funcionam bem com ESM, apesar de precisarmos de alguns truques.

Este guia mostra como usar o ESM no Node.js, detalhando os fundamentos e também as pegadinhas com as quais você precisa ter cuidado. Você pode encontrar todo o código no repositório do GitHub. É um monorepo onde cada pacote exibe uma certa estrutura do suporte ESM do Node.js. Este post passa por cada um dos pacotes, explicando o que foi feito lá e quais são as pegadinhas.

Este guia acabou sendo bem longo, então eu o dividi em três partes:

  1. Parte 1 - O Básico (este artigo que você está lendo)
  2. Parte 2 - "exports" e seus usos (incluindo bibliotecas de duplo-módulos)
  3. Parte 3 - Ferraments e TypeScript

Importante: Este guia abrange Node.js ESM e não cobre ESM em navegadores.

O que quero dizer com ESM em Node.js? Já não temos isso?

ESM é o sistema de módulo JavaScript padrão (ESM é um abreviamento para Módulos JavaScript que também é chamado de ESM, ou Módulos EcmaScript, em que “EcmaScript” é o nome oficial da especificação da linguagem JavaScript). ESM é o sistema de módulo “mais novo” e deve ser um substituto para o sistema de módulo Node.js atual, que é CommonJS (CJS para abreviar), embora o CommonJS provavelmente ainda estará conosco por muito, muito tempo. A sintaxe do módulo é esta:

// add.js
export function add(a, b) {
  return a + b
}

// main.js
import { add } from "./add.js"
Enter fullscreen mode Exit fullscreen mode

(Uma introdução ao ESM está fora do escopo deste guia, mas você pode encontrá-la hoje em qualquer lugar na Internet)

O ESM foi padronizado em 2015, mas demorou um pouco para os navegadores suportarem isso, e demorou ainda mais para o Node.js suportá-lo (a versão estável final no Node.js foi finalizada apenas em 2020!). Se você quiser mais informações, pode ver minha palestra no Node.TLV. Na palestra, no final, discuto se o ESM está pronto para funcionar, e digo que ainda não está lá e as pessoas devem começar a migrar para ele em um ou dois anos. Bem, esse ano é AGORA e está PRONTO, e este guia irá prepará-lo para isso.

Alguns de vocês podem estar balançando a cabeça e se perguntando: já não estamos usando isso? Bem, se estiver, então você está transpilando seu código usando Babel ou TypeScript, que oferecem suporte a ESM pronto para uso e o transpilando para CJS. O ESM sobre o qual esta postagem está falando é o ESM nativo compatível com Node.js sem transpilar. Embora sintaticamente seja o mesmo, existem pequenas diferenças entre ele e o Babel / TypeScript ESM, diferenças que são discutidas em minha palestra no Node.TLV acima. Mais importante ainda, o ESM nativo em Node.js não precisa de transpilação e, portanto, não vem com a bagagem de problemas que a transpilação traz.

Sem enrolação, posso começar a usar o ESM no Node.js?

Sim. Praticamente, sim. Todas as ferramentas que eu uso suportam isso, mas há duas pegadinhas que são provavelmente difíceis de engolir para algumas pessoas, pegadinhas que são difíceis de contornar:

  • O suporte do Jest para ESM em Node.js é experimental
  • O suporte experimental do Jest ainda não suporta módulos de simulação (mocking modules) mas funções regulares e simulação de objeto são compatíveis.
  • proxyquire e outros mockers de módulo populares ainda não suportam ESM (embora testdouble seja totalmente compatível)

O maior problema é a falta de suporte para mockers de módulo. Temos uma biblioteca de mock que suporta ESM, a testdouble, e usamos ela neste guia.

Então você pode viver com isso? Se você puder, ir all-in com ESM no Node.js agora é totalmente possível. Estou usando há quatro meses, sem problemas. Na verdade, parece que o suporte VSCode para ESM é muito melhor do que para CJS, então de repente recebo importações automáticas de módulos e outras vantagens, que não recebia antes no mundo CJS.

O guia para Node.js ESM

  1. Parte 1 - O Básico (este artigo que você está lendo) 1.1. Um pacote Node.js ESM simples 1.2. Usando a extensão .js no ESM
  2. Parte 2 - "exports" e seus usos (incluindo bibliotecas de duplo-módulos) 2.1. O campo "exports" 2.2. Exportações múltiplas 2.3. Bibliotecas de duplo-módulos
  3. Parte 3 - Ferraments e TypeScript 3.1. Ferramentas 3.2. TypeScript

Este guia vem com um monorepo que possui 7 diretórios, cada diretório sendo um pacote que demonstra as seções acima do suporte Node.js para ESM. Você pode encontrar o monorepo nesse link.

Um pacote Node.js ESM simples

Código complementar: https://github.com/giltayar/jsm-in-nodejs-guide/tree/main/01-simplest-mjs

Esse é o exemplo mais simples e demonstra os fundamentos básico. Vamos começar explorando package.json e o novo campo exports.

main e .mjs

Código: https://github.com/giltayar/jsm-in-nodejs-guide/blob/main/01-simplest-mjs/package.json

{
  "name": "01-simplest-mjs",
  "version": "1.0.0",
  "description": "",
  "main": "src/main.mjs"
}
Enter fullscreen mode Exit fullscreen mode

O principal ponto de entrada é src/main.mjs. Por que o arquivo usa a extensão .mjs? Porque no Node.js ESM, a extensão .js é reservada para CJS e .mjs significa que este é um Módulo JS (na próxima seção, veremos como mudar isso). Falaremos um pouco mais sobre isso na próxima parte.

Vamos continuar explorando main.mjs.

"imports" usando extensões

Código: https://github.com/giltayar/jsm-in-nodejs-guide/blob/main/01-simplest-mjs/src/main.mjs

// src/main.mjs
import {bannerInColor} from "./banner-in-color.mjs"

export function banner() {
  return bannerInColor("white")
}
Enter fullscreen mode Exit fullscreen mode

Observe a instrução de importação que importa banner-in-color: Node.js ESM força você a especificar o caminho relativo completo para o arquivo, incluindo a extensão . A razão pela qual eles fizeram isso é para serem compatíveis com o ESM do navegador (ao usar o ESM em navegadores, você sempre especifica o nome completo do arquivo, incluindo a extensão). Portanto, não se esqueça dessa extensão! (Você pode entender mais sobre isso em minha palestra na Node.TLV).

Infelizmente, o VSCode não gosta da extensão .mjs e, portanto, Ctrl / Cmd + click nele não funcionará, e seu intellisense embutido não funciona nele.

Pegadinha : VSCode não gosta da extensão .mjs e ignora essa extensão. Na próxima seção, veremos como lidar com isso, então não é um problema real.

O main.mjs exporta a função banner, que será testada em test/tryout.mjs. Mas primeiro, vamos explorar banner-in-color.mjs, que contém a maior parte da implementação da função banner().

Importando pacotes ESM e CJS

Código: https://github.com/giltayar/jsm-in-nodejs-guide/blob/main/01-simplest-mjs/src/banner-in-color.mjs

Vimos como podemos importar módulos ESM. Vamos ver como importar outros pacotes:

// src/banner-in-color.mjs
import {join} from "path"
import chalk from "chalk"
const {underline} = chalk
Enter fullscreen mode Exit fullscreen mode

Podemos importar pacotes internos do Node.js como path facilmente, porque Node.js os expõe como módulos ES.

E se tivéssemos um pacote ESM no NPM, o mesmo poderia ter sido usado para importar esse pacote ESM. Mas a maioria dos pacotes que o NPM possui ainda são pacotes CJS. Como você pode ver na segunda linha, onde importamos chalk, os pacotes CJS também podem ser importados usando import. Mas, na maior parte, ao importar módulos CJS, você só pode usar a importação “padrão” (default) e não as importações “nomeadas”. Portanto, embora você possa importar importações nomeadas em um arquivo CJS:

// -a-cjs-file.cjs
const {underline} = require("chalk")
Enter fullscreen mode Exit fullscreen mode

Você não pode fazer isso em um arquivo ESM:

// -a-jsm-file.mjs
import {underline} from 'chalk'
Enter fullscreen mode Exit fullscreen mode

Você só pode importar a importação padrão (não noemada) e usar a desestruturação mais tarde:

import chalk from "chalk"
const {underline} = chalk
Enter fullscreen mode Exit fullscreen mode

Por que disso? É complicado, mas o ponto principal é que, ao carregar módulos, o ESM não permite a execução de um módulo para determinar o que são as exportações e, portanto, as exportações precisam ser determinadas estaticamente. Infelizmente, no CJS, executar um módulo é a única maneira confiável de determinar quais são as exportações. Na verdade, o Node.js tenta muito descobrir quais são as exportações nomeadas (parseando e analisando o módulo usando um parser muito rápido), mas minha experiência é que esse método não funciona para a maioria dos pacotes com os quais tentei, e eu precisa voltar à importação padrão.

Pegadinha: Importar um módulo CJS é fácil, mas geralmente, você não pode usar importações nomeadas e precisa adicionar uma segunda linha para desestruturar as importações nomeadas.

Eu acredito que em 2021, mais e mais pacotes terão pontos de entrada ESM que se exportam como ESM com as exportações nomeadas corretas. Mas, por enquanto, você pode usar a desestruturação adicional para usar as importações nomeadas de pacotes CJS.

"await" de nível superior

Código: https://github.com/giltayar/jsm-in-nodejs-guide/blob/main/01-simplest-mjs/src/banner-in-color.mjs

Continuando nossa exploração de banner-in-color.mjs, encontramos esta linha extraordinária que lê um arquivo do disco:

// src/banner-in-color.mjs
const text = await fs.readFile(join(__dirname, "text.txt"), "utf8")
Enter fullscreen mode Exit fullscreen mode

Por que tão extraordinário? Por causa do await. Isso é um await fora de uma função async e está no nível superior do código. Esse awaité chamado de "await de nível superior" (top-level await) e é compatível desde o Node.js v14. É extraordinário porque é o único recurso em Node.js que está disponível apenas em módulos ESM (ou seja, não disponível em CJS). Por que? Como o ESM é um sistema de módulo assíncrono e, portanto, oferece suporte a operações assíncronas ao carregar o módulo, enquanto o CJS é carregado de forma síncrona e, portanto, não tem suporte await.

Excelente recurso, e apenas no ESM! 🎉🎉🎉🎉

Mas observe o uso de __dirname na linha acima. Vamos discutir isso.

__dirname

Código: https://github.com/giltayar/jsm-in-nodejs-guide/blob/main/01-simplest-mjs/src/banner-in-color.mjs

Se você tentar usar __dirname no ESM, verá que ele não está disponível (assim como __filename). Mas se você precisar, pode trazê-lo rapidamente usando estas linhas:

// src/banner-in-color.mjs
import url from "url"

const __dirname = url.fileURLToPath(new URL(".", import.meta.url))
Enter fullscreen mode Exit fullscreen mode

Complexo? Sim. Portanto, vamos desconstruir esse código para entendê-lo.

Em primeiro lugar, a expressão import.meta.url faz parte da especificação do ESM e seu propósito é o mesmo do CJS __filename, exceto que é uma URL e não um caminho de arquivo. Por que URLs? Porque o ESM é definido em termos de URLs e não de caminhos de arquivo (para ser compatível com o navegador). Aliás, o URL que obtemos não é um URL HTTP. É um “ file://...” URL.

Agora que temos a URL do arquivo atual, precisamos da URL pai para chegar ao diretório e usaremos new URL('.', import.meta.url)para chegar até ela (o motivo pelo qual isso funciona está fora do escopo deste guia). Finalmente, para obter o caminho do arquivo e não a URL, precisamos de uma função que converta entre os dois e o módulo url do Node.js nos fornece isso por meio da função url.fileURLToPath.

Finalmente, colocamos o caminho do diretório em uma variável chamada __dirname, assim chamada pelas tradições do Node.js 😀.

Testando este módulo

Código: https://github.com/giltayar/jsm-in-nodejs-guide/blob/main/01-simplest-mjs/test/tryout.mjs

// test/tryout.mjs
import assert from 'assert'
import {banner} from '../src/main.mjs'

assert.strict.match(banner(), /The answer is.*42/)

console.log(banner())
Enter fullscreen mode Exit fullscreen mode

O teste executará test/tryout.mjs, no qual fará o import do módulo src/main.mjs, que utilizará (como vimos acima) várias importações CJS e ESM, para exportar uma função do banner colorido retornando a resposta (para a vida, o universo e tudo) de valor 42. Ele afirmará que a resposta é tal, e com console.log podemos vê-la com toda a sua glória.

Para executar o teste, faça cd para 01-simplest-jse execute:

npm install
npm test
Enter fullscreen mode Exit fullscreen mode

Sim! Escrevemos nosso primeiro pacote ESM! Agora vamos fazer o mesmo, mas com uma extensão .js!

Usando a extensão .js para ESM

Código complementar: https://github.com/giltayar/jsm-in-nodejs-guide/blob/main/02-simplest-js

Como vimos na seção anterior, a extensão .mjs é problemática, porque as ferramentas ainda não a suportam totalmente. Queremos nossa extensão .js de volta, e é isso que faremos nesta seção, com uma mudança muito simples no package.json.

type: module

Código: https://github.com/giltayar/jsm-in-nodejs-guide/blob/main/02-simplest-js/package.json

{
  "name": "02-simplest-js",
  "version": "1.0.0",
  "description": "",
  "type": "module",
  "main": "src/main.js"
}
Enter fullscreen mode Exit fullscreen mode

Existe uma maneira muito simples de fazer com que todos os seus arquivos .js sejam interpretados como ESM e não como CJS: basta adicionar "type": "module" ao seu package.json, como acima. É isso aí. A partir desse ponto, todos os arquivos .js serão interpretados como ESM, portanto, todo o seu código pode agora usar a extensão .js.

Você ainda pode usar .mjs que sempre será ESM. Além disso, se você precisar de um módulo CJS em seu código, pode usar a nova extensão .cjs (veremos como usamos isso na seção “Bibliotecas de duplo-módulos”).

É isso aí. O restante do código neste diretório usa .js, e ao importar, também usaremos a extensão .js:

Código: https://github.com/giltayar/jsm-in-nodejs-guide/blob/main/02-simplest-js/src/main.js

// src/main.js
import {bannerInColor} from "./banner-in-color.js"
Enter fullscreen mode Exit fullscreen mode

É isso para o básico. Para a próxima parte deste guia, onde aprendemos sobre uma característica importante do ESM: exports.

💖 💪 🙅 🚩
oieduardorabelo
Eduardo Rabelo

Posted on February 15, 2021

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related