Como eu sempre faço por aqui, vamos falar de mais um lançamento sensacional do Node.js, a versão 18 foi anunciada em Abril de 2022! E você deve estar se perguntando: E dai?

Para você que é dev JavaScript ou não, essa versão do Node.js trouxe uma série de mudanças muito interessantes para o runtime em si, e algumas dessas mudanças são tão importantes que podem inspirar outros runtimes a fazerem o mesmo, então bora dar uma olhada em tudo que a gente tem por ai!

Mas antes, como eu sempre faço em artigos desse tipo, bora explicar um pouco mais sobre esse processo de lançamento do Node.js.

O processo de releases do Node.js

Assim como muitos outros grandes projetos que tem uma dependência muito forte da comunidade, o Node.js possui um calendário e organização de novas versões e releases extremamente organizado.

Todas as versões pares são consideradas versões "prontas para produção", enquanto as versões ímpares são as versões de teste e desenvolvimento. Em outras palavras, versões ímpares são como o ambiente de staging, ou seja, testes mais estruturados, para dar lugar a uma versão de produção. Geralmente novas funcionalidades são testadas com a comunidade nessas versões e, depois de um tempo, elas são promovidas a uma versão estável.

Calendário de releases para o Node.js em 2022

Versões pares são lançadas em Abril e ficam designadas como Current até Outubro, quando se tornam a versão ativa, depreciando a versão par anterior para o estado de manutenção.

A diferença entre uma versão Active e Current é que as versões ativas são consideradas LTS ou Long Term Support, que recebem updates de segurança e manutenções por 3 anos, sempre existem 3 versões em estado de manutenção e uma versão LTS, todas as versões mais antigas do que 3 anos são depreciadas, que é o que aconteceu com a versão 10 agora que a versão 18 foi lançada.

Você pode ver todas as datas e todos planos para as versões anteriores e as próximas versões no site oficial das releases.

Atualmente este é o estado do ambiente:

  • Node v12: alcançou o fim de sua vida em Abril de 2022
  • Node v14: Continua em manutenção até Abril de 2023, depois será abandonado
  • Node v16: Atualmente é a versão LTS até Outubro de 2022, depois entra em manutenção até Abril de 2024, quando será abandonado.
  • Node v18: É a versão Current até Outubro de 2022 quando se torna a próxima LTS até Abril de 2025.

Global Fetch disponível por padrão

Na versão 17 do Node, foi anunciado que a API fetch, já presente na maioria dos browsers para JavaScript, chegaria também ao Node. De forma que a gente não iria mais precisar de pacotes externos como o famoso axios e got para poder fazer requisições HTTP de uma forma mais fácil, sem precisar o client HTTP nativo do Node – que é, digamos... Um pouco complexo.

Esse client é implementado usando uma das bibliotecas mais interessantes já feitas para Node, o undici, um client HTTP/1.1 escrito do zero, completamente em JavaScript para Node.js.

Essa implementação foi originalmente adicionada através de uma flag experimental no Node que ativava a funcionalidade, mas agora temos o fetch habilitado por padrão.

Veja como podemos usar esse novo cliente:

const res = await fetch('https://nodejs.org/api/documentation.json');
if (res.ok) {
  const data = await res.json();
  console.log(data);
}

Além do fetch, outras variáveis globais foram adicionadas: Headers, Request, Response e FormData

Cuidado para não confundir os tipos nativos Request e Response com os mesmos tipos do Express quando estamos usando TypeScript

Outras APIs globais

  • Uma versão experimental da API de WebStreams foi adicionada, permitindo o uso de streams nativamente em ambientes Web sem o uso de integrações locais
  • Um novo tipo de Buffer experimental, o Blob, foi também colocado nas APIs globais
  • Para uma adição nas worker_threads, o BroadcastChannel agora também é uma API global exposta

Test runner nativo

Uma das APIs mais legais que eu, pessoalmente, estava esperando há anos, é o suporte a rodar testes nativamente. Isso mesmo, sem mais mocha, jest, ava e outros.

Agora você pode rodar nativamente todos os testes de software que você já possui através do módulo test, que só pode ser carregado se denotado com o prefixo node::

import test from 'node:test'
import assert from 'node:assert'

test('top level test', async (t) => {
  await t.test('subtest 1', (t) => {
    assert.strictEqual(1, 1);
  });

  await t.test('subtest 2', (t) => {
    assert.strictEqual(2, 2);
  });
});

A API está totalmente documentada, claro que vai demorar um tempo até que ela alcance o nível de outra bibliotecas como o jest, se é que um dia ela vai alcançar.

Eu digo isso porque a ideia principal dessa biblioteca não é que ela substitua as principais libs que já utilizamos, como as que eu falei anteriormente, mas sim diminua a barreira de entrada para a criação de testes automatizados usando Node.js. Dessa forma mais sistemas poderão contar com testes automatizados e ficarão muito mais seguros.

No entanto, existem algumas considerações de implementações que precisamos considerar:

  • O Node vai executar todos os arquivos de teste quando você inicializar o runtime com a flag --test, cada teste será rodado em seu próprio processo isolado.
  • Os testes podem ser síncronos ou assíncronos, testes síncronos vão ser considerados válidos se não derem nenhuma exceção. Os assíncronos, como era esperado, se não rejeitarem uma Promise
  • Subtestes criados com o contexto t, que estamos passando no exemplo, vão ser executados da mesma forma que o teste pai
  • Se você quiser pular um teste, é só mandar um objeto de opções com a flag { skip: 'mensagem' } para o objeto de teste como nesse exemplo:
test('pulado', { skip: 'Esse teste foi pulado' }, (t) => {
    // nunca executado
})

Atualmente o objeto de opções aceita três tipos de chaves:

  • concurrency: Define quantos testes rodam em paralelo
  • skip: Pode ser um boolean ou uma string, se for um boolean true, o teste será pulado sem nenhuma mensagem, caso contrário, a mensagem será exibida
  • todo: Mesma coisa do anterior, aceita um boolean ou uma string, se ele for convertido para true, o teste será marcado como To-Do.

O test runner ainda está em caráter experimental e rodando atrás de flags, mas isso deve ser alterado nas próximas versões.

O prefixo node:

Vamos fazer um contorno para explicar uma funcionalidade que não é necessariamente algo que veio com o Node 18 propriamente dito, porém foi uma alteração importante que cria um precedente que pode ser seguido no futuro para todos os demais módulos.

No exemplo que dei sobre o test runner acima, você pode notar que estamos importando os módulos assert e test com um prefixo node:. Isso é o início do que é chamado de prefix-only core modules.

Isso já existia antes, porém não era obrigatório, até hoje todos os módulos nativos como o fs, assert e outros funcionavam da mesma forma queiram eles fossem importados com o prefixo node: ou não. Hoje isso não é mais o caso.

O node:test é o primeiro módulo nativo que só pode ser importado se utilizado com o prefixo node:, se você não utilizar o prefixo, o runtime vai tentar carregar um módulo chamado test que é considerado um userland module, ou seja, um módulo feito pela comunidade.

Isso é uma mudança incrível porque com o prefixo node: chegando nos novos módulos (e provavelmente como breaking change em alguma versão futura para módulos mais antigos), vamos ter a capacidade de ter dois módulos com o mesmo nome, um no userland e outro no core do Node.

Dessa forma, como módulos do core tem precedência sobre módulos de usuários, será possível para quem colabora com o Node criar módulos sem se importar se o nome do módulo já existe no NPM por exemplo.

Por outro lado, isso cria dois problemas, o primeiro deles é que temos ai uma inconsistência clara entre os módulos que já existem, como o fs e http, e os módulos novos que só usam o prefixo. A solução para isso teria de ser a obrigação do uso do prefixo para todos os módulos, e não só para os mais novos.

Além disso, um problema de segurança acaba surgindo: o typosquatting, quando alguém cria um módulo no NPM com o mesmo nome ou um nome muito parecido com um pacote original – algo como chamar express de expres no NPM – para que devs desavisados possam baixar o pacote malicioso ao invés do pacote original. Esses problemas não são do time do Node, até porque o NPM já possui algumas travas de segurança contra isso, mas de qualquer forma, é algo que vale a pena ser mencionado.

Userland snapshots

Algo super interessante que surgiu na versão 18 é o uso de snapshots para build time do runtime do Node. Isso é algo bastante interessante para quem tem muitos times de desenvolvimento e precisa sincronizar e até mesmo melhorar a performance em um produto entre times.

A partir dessa nova versão, será possível compilar um binário do Node.js com um snapshot customizado de inicialização usando a flag --node-snapshot-main. Por exemplo:

$ cd /path/to/node/source
$ ./configure --node-snapshot-main=marked.js
# Build do binário
$ make node

Construir o binário do Node passando um entrypoint como o marked.js que é um renderer de Markdown vai inicializar o módulo e carregá-lo no globalThis e você vai poder usar ele nativamente como:

const html = globalThis.marked(process.argv[1]);
console.log(html);

E executar o binário construído com:

$ out/Release/node render.js test.md

Claro, isso é para casos de uso bastante específicos onde você precisa, de fato, recompilar todo o runtime do Node para incluir um ou mais entrypoints de módulos diretamente no binário para melhorar o tempo de compilação.

Como follow-up, o time está trabalhando nas PRs #42617 e #38905, que respectivamente:

  • Permite que o módulo seja carregado sem um script de inicialização, o que vai transformar o binário inteiro na aplicação do usuário, dessa forma seu binário final seria executado como $ out/Release/markedNode test.md, um passo mais próximo de binários completos do Node como o próprio Golang faz
  • Permite a adição de entrypoints sem a necessidade de recompilar todo o runtime com um compilador.

Mudanças no V8 e outros pontos

A versão 10 do v8 traz algumas novidades:

  • Suporte para os novos métodos findLast e findLastIndex nos arrays, que fazem exatamente a mesma coisa do find, porém encontrando o último valor ao invés do primeiro
  • Melhorias na API do Intl.Locale
  • Melhorias de performance para inicialização de propriedades de classe e métodos privados para que eles sejam tão rápidos quanto propriedades normais.
  • O import de módulos JSON foi oficialmente removido da flag experimental