EcmaScript Modules na prática
Fala turma, para reforçar os conhecimentos que tive estudando EcmaScript modules, resolvi fazer um pequeno monorepo para praticar. Vamos conseguir ver na prática como funciona o suporte do NodeJS e dos browsers para o ESM, incluindo utilizar o ESM com o CommonJS, e também como utilizar o ESM com o TypeScript.
Exemplo 1: "type": "module"
Vamos começar com o exemplo mais utilizado. Eu criei uma pasta chamada esm-node-type-module
para esse primeiro exemplo, dentro dela vamos criar um arquivo package.json
com o seguinte conteúdo:
{
"name": "esm-node-type-module",
"version": "1.0.0",
"description": "Exemplo de uso do ESM com o type module",
"main": "index.js",
"scripts": {
"start": "node index.js"
},
"author": "Caio Fuzatto",
"license": "MIT"
}
Depois vamos criar os arquivos index.js
e module1.js
com o seguinte conteúdo:
// index.js
import { test1 } from './module1.js';
test1();
Uma das primeiras coisas que vamos notar é a extensão do arquivo deve ser especificada no import, diferente do "ESM transpilado" que conseguia inferir a extensão do arquivo. Expliquei tudo sobre isso no post Entendendo EcmaScript Modules.
// module1.js
export const test1 = () => {
console.log('Estamos dentro de um módulo ESM');
}
Agora vamos rodar o comando yarn start
e ver o resultado:
Opa, erro ao carregar o módulo. Mas felizmente o NodeJS nos dá duas dicas do que fazer, nesse primeiro exemplo vamos utilizar a primeira dica e adicionar "type": "module"
no nosso package.json
. Agora é só rodar novamente e ver o resultado:
$ node index.js
Estamos dentro de um módulo ESM
Compatibilidade com o CommonJS
O NodeJS também trouxe um suporte aos módulos ESM com o CommonJS, ou seja, podemos utilizar o ESM com o CommonJS. Para isso vamos criar um arquivo module2.cjs
com o seguinte conteúdo:
const test2 = async () => {
await new Promise((resolve) => setTimeout(resolve, 2000));
console.log("Estamos dentro de um módulo CommonJS");
};
module.exports = { test2 };
Observe que a extensão do arquivo é
.cjs
, isso é necessário para o NodeJS entender que esse arquivo é um módulo CommonJS.
E agora vamos importar esse módulo no nosso index.js
:
import { test1 } from './module1.js';
import { test2 } from './module2.cjs';
test1();
await test2();
Fiz uma função
async
para você ver que o ESM suporta o uso de await no top-level de módulos. Incrível né?
Agora vamos rodar o comando yarn start
e ver o resultado, que após 2 segundos será o seguinte:
$ node index.js
Estamos dentro de um módulo ESM
Estamos dentro de um módulo CommonJS
Exemplo 2: Extensão .mjs
Vamos agora para o segundo exemplo, que é o uso da extensão .mjs
. Como já estamos familiarizados, vamos direto agora em?
Para isso vamos criar uma pasta chamada esm-node-mjs
e dentro dela vamos criar um arquivo package.json
com o seguinte conteúdo:
{
"name": "esm-node-mjs",
"version": "1.0.0",
"description": "Exemplo de uso do ESM com a extensão .mjs",
"main": "index.mjs",
"scripts": {
"start": "node index.mjs"
},
"author": "Caio Fuzatto",
"license": "MIT"
}
Observe que nesse exemplo já estamos utilizando a extensão
.mjs
na propriedademain
e no scriptstart
.
Agora vamos copiar os arquivos index.js
e module1.js
do exemplo anterior e renomear para index.mjs
e module1.mjs
respectivamente. Já o module2.cjs
vamos renomear para module2.js
. Além disso é importante ajustar os imports na index né? Confira como ficou:
import { test1 } from './module1.mjs';
import { test2 } from './module2.js';
test1();
await test2();
Percebeu que inverteu o jogo? Agora os módulos ESM precisam ter a extensão .mjs
e os CommonJS utilizam a .js
, enquanto no outro exemplo era o contrário. Agora vamos rodar o comando yarn start
e temos o mesmo resultado:
$ node index.mjs
Estamos dentro de um módulo ESM
Estamos dentro de um módulo CommonJS
Exemplo 3: ESM no frontend
Vamos agora para o terceiro exemplo, que é o uso do ESM no frontend. Para isso vamos criar uma pasta chamada esm-frontend
e dentro dela vamos criar um arquivo app.js
com o seguinte conteúdo:
import { test1 } from './module1.js';
document.getElementById('content').textContent = await test1();
Bem semelhante aos exemplos de NodeJS, a diferença aqui é que estamos utilizando o
document
para alterar o conteúdo de um elemento HTML.
Agora vamos criar nosso módulo, para isso vamos criar um arquivo module1.js
com o seguinte conteúdo:
export const test1 = async () => {
console.log("Estamos dentro de um módulo ESM | ", new Date().toISOString());
await new Promise((resolve) => setTimeout(resolve, 2000));
console.log("Conteúdo carregou | ", new Date().toISOString());
return "Fui gerado por um módulo ESM";
};
E por fim, a página HTML que vai carregar o nosso módulo. Para isso vamos criar um arquivo index.html
com o seguinte conteúdo:
<!DOCTYPE html>
<head>
<title>ESM no Frontend</title>
</head>
<body>
<div id="content">Carregando...</div>
<script type="module" src="app.js"></script>
</body>
Observe que estamos utilizando o atributo
type="module"
para indicar que o arquivoapp.js
é um módulo ESM.
Para testar (e não tomar erro de CORS) vou utilizar a lib HTTP-SERVE. Você pode rodar npx http-serve
dentro da pasta esm-frontend
e acessar a URL http://localhost:8080
para ver o resultado:
Exemplo 4: ESM nativo e Typescript
Vamos agora para o quarto exemplo, que é o uso do ESM nativo com o TypeScript. Para isso vamos criar uma pasta chamada esm-typescript
e dentro dela vamos criar um arquivo package.json
com o seguinte conteúdo:
{
"name": "esm-node-type-module",
"version": "1.0.0",
"description": "Exemplo de uso do ESM com o type module",
"main": "index.js",
"license": "MIT",
"author": "Caio Fuzatto",
"scripts": {
"start": "ts-node-esm index.ts"
},
"type": "module",
"devDependencies": {
"ts-node": "^10.9.1",
"typescript": "^5.2.2"
}
}
Observe que estamos utilizando o
ts-node-esm
para rodar o TypeScript com o ESM nativo. Você pode ver mais sobre no Github do ts-node
Agora vamos criar nosso arquivo module1.ts
com o seguinte conteúdo:
export const test1 = async () => {
await new Promise((resolve) => setTimeout(resolve, 2000));
console.log("Estamos dentro de um módulo ESM nativo no Typescript");
};
Até aqui tudo ok né? Agora vamos criar o index.ts
com o seguinte conteúdo:
import { test1 } from './module1.js';
await test1();
Observe que estamos importando o módulo
module1.js
e nãomodule1.ts
. Isto é porque o TypeScript é transpilado para JavaScript, e o Node.js vai executar o código JavaScript transpilado, não o código TypeScript original.
Agora vamos rodar o comando yarn start
e ver o resultado:
$ ts-node-esm index.ts
Estamos dentro de um módulo ESM nativo no Typescript
Conclusão
Bom pessoal, espero que tenham gostado do conteúdo. Conseguimos ver várias formas de utilizar o ESM nativo do NodeJS e dos browsers, e também como utilizar o ESM com o CommonJS. Vale sempre a pena avaliar a situação do projeto para escolher qual desses caminhos seguir, se precisar de ajuda da uma olhada no meu post Entendendo o EcmaScript Modules. Você pode ver o código completo no Github. Até a próxima!