Módulos ES
O Nuxt usa módulos ES nativos.
Este guia ajuda a explicar o que são Módulos ES e como tornar um aplicativo Nuxt (ou biblioteca upstream) compatível com ESM.
Contexto
Módulos CommonJS
CommonJS (CJS) é um formato introduzido pelo Node.js que permite compartilhar funcionalidades entre módulos JavaScript isolados (leia mais). Você já pode estar familiarizado com esta sintaxe:
const a = require('./a')
module.exports.a = a
Empacotadores como webpack e Rollup suportam essa sintaxe e permitem que você use módulos escritos em CommonJS no navegador.
Sintaxe ESM
Na maioria das vezes, quando as pessoas falam sobre ESM vs. CJS, estão falando sobre uma sintaxe diferente para escrever módulos.
import a from './a'
export { a }
Antes que os Módulos ECMAScript (ESM) se tornassem um padrão (levou mais de 10 anos!), ferramentas como webpack e até mesmo linguagens como TypeScript começaram a suportar a chamada sintaxe ESM. No entanto, existem algumas diferenças chave com a especificação real; aqui está uma explicação útil.
O que é ESM 'Nativo'?
Você pode ter escrito seu aplicativo usando a sintaxe ESM por um longo tempo. Afinal, é suportado nativamente pelo navegador, e no Nuxt 2 compilamos todo o código que você escreveu para o formato apropriado (CJS para servidor, ESM para navegador).
Ao adicionar módulos ao seu pacote, as coisas eram um pouco diferentes. Uma biblioteca de exemplo pode expor versões CJS e ESM, e nos deixar escolher qual queremos:
{
"name": "sample-library",
"main": "dist/sample-library.cjs.js",
"module": "dist/sample-library.esm.js"
}
Assim, no Nuxt 2, o empacotador (webpack) puxaria o arquivo CJS ('main') para a construção do servidor e usaria o arquivo ESM ('module') para a construção do cliente.
No entanto, nas versões recentes do Node.js LTS, agora é possível usar módulo ESM nativo dentro do Node.js. Isso significa que o próprio Node.js pode processar JavaScript usando a sintaxe ESM, embora não o faça por padrão. As duas maneiras mais comuns de habilitar a sintaxe ESM são:
- definir
"type": "module"
dentro do seupackage.json
e continuar usando a extensão.js
- usar as extensões de arquivo
.mjs
(recomendado)
Isso é o que fazemos para o Nuxt Nitro; nós geramos um arquivo .output/server/index.mjs
. Isso diz ao Node.js para tratar este arquivo como um módulo ES nativo.
Quais são as Importações Válidas em um Contexto Node.js?
Quando você importa
um módulo em vez de requerer
ele, o Node.js o resolve de maneira diferente. Por exemplo, quando você importa sample-library
, o Node.js não procurará pelo main
, mas pelo exports
ou module
na package.json
dessa biblioteca.
Isso também é verdade para importações dinâmicas, como const b = await import('sample-library')
.
O Node suporta os seguintes tipos de importações (veja documentação):
- arquivos terminando em
.mjs
- espera-se que usem a sintaxe ESM - arquivos terminando em
.cjs
- espera-se que usem a sintaxe CJS - arquivos terminando em
.js
- espera-se que usem a sintaxe CJS, a menos que seupackage.json
tenha"type": "module"
Que Tipos de Problemas Podem Ocorrer?
Por muito tempo, autores de módulos têm produzido builds com sintaxe ESM, mas usando convenções como .esm.js
ou .es.js
, que eles adicionaram ao campo module
em seu package.json
. Isso não foi um problema até agora porque eles só foram usados por empacotadores como webpack, que não se importam especialmente com a extensão do arquivo.
No entanto, se você tentar importar um pacote com um arquivo .esm.js
em um contexto ESM do Node.js, não funcionará, e você receberá um erro como:
(node:22145) Warning: To load an ES module, set "type": "module" in the package.json or use the .mjs extension.
/path/to/index.js:1
export default {}
^^^^^^
SyntaxError: Unexpected token 'export'
at wrapSafe (internal/modules/cjs/loader.js:1001:16)
at Module._compile (internal/modules/cjs/loader.js:1049:27)
at Object.Module._extensions..js (internal/modules/cjs/loader.js:1114:10)
....
at async Object.loadESM (internal/process/esm_loader.js:68:5)
Você também pode receber esse erro se tiver uma importação nomeada de um build com sintaxe ESM que o Node.js pensa ser CJS:
file:///path/to/index.mjs:5
import { named } from 'sample-library'
^^^^^
SyntaxError: Named export 'named' not found. The requested module 'sample-library' is a CommonJS module, which may not support all module.exports as named exports.
CommonJS modules can always be imported via the default export, for example using:
import pkg from 'sample-library';
const { named } = pkg;
at ModuleJob._instantiate (internal/modules/esm/module_job.js:120:21)
at async ModuleJob.run (internal/modules/esm/module_job.js:165:5)
at async Loader.import (internal/modules/esm/loader.js:177:24)
at async Object.loadESM (internal/process/esm_loader.js:68:5)
Solução de Problemas com ESM
Se você encontrar esses erros, o problema é quase certamente com a biblioteca upstream. Eles precisam corrigir sua biblioteca para suportar ser importada pelo Node.
Transpilando Bibliotecas
Enquanto isso, você pode dizer ao Nuxt para não tentar importar essas bibliotecas adicionando-as ao build.transpile
:
export default defineNuxtConfig({
build: {
transpile: ['sample-library']
}
})
Você pode descobrir que também precisa adicionar outros pacotes que estão sendo importados por essas bibliotecas.
Fazendo Alias de Bibliotecas
Em alguns casos, você também pode precisar fazer manualmente um alias da biblioteca para a versão CJS, por exemplo:
export default defineNuxtConfig({
alias: {
'sample-library': 'sample-library/dist/sample-library.cjs.js'
}
})
Exportações Padrão
Uma dependência com formato CommonJS pode usar module.exports
ou exports
para fornecer uma exportação padrão:
module.exports = { test: 123 }
// ou
exports.test = 123
Isso normalmente funciona bem se requerermos
tal dependência:
const pkg = require('cjs-pkg')
console.log(pkg) // { test: 123 }
Node.js em modo ESM nativo, typescript com esModuleInterop
habilitado e empacotadores como webpack, fornecem um mecanismo de compatibilidade para que possamos importar por padrão tal biblioteca.
Esse mecanismo é frequentemente referido como "interop require default":
import pkg from 'cjs-pkg'
console.log(pkg) // { test: 123 }
No entanto, devido às complexidades da detecção de sintaxe e diferentes formatos de pacote, sempre há a chance de que o interop default falhe e acabemos com algo assim:
import pkg from 'cjs-pkg'
console.log(pkg) // { default: { test: 123 } }
Além disso, ao usar a sintaxe de importação dinâmica (em arquivos CJS e ESM), sempre temos essa situação:
import('cjs-pkg').then(console.log) // [Module: null prototype] { default: { test: '123' } }
Nesse caso, precisamos fazer manualmente a interoperação da exportação padrão:
// Importação estática
import { default as pkg } from 'cjs-pkg'
// Importação dinâmica
import('cjs-pkg').then(m => m.default || m).then(console.log)
Para lidar com situações mais complexas e com mais segurança, recomendamos e usamos internamente mlly no Nuxt, que pode preservar exportações nomeadas.
import { interopDefault } from 'mlly'
// Supondo que a forma seja { default: { foo: 'bar' }, baz: 'qux' }
import myModule from 'my-module'
console.log(interopDefault(myModule)) // { foo: 'bar', baz: 'qux' }
Guia para Autores de Bibliotecas
A boa notícia é que é relativamente simples corrigir problemas de compatibilidade com ESM. Existem duas opções principais:
-
Você pode renomear seus arquivos ESM para terminar com
.mjs
.Esta é a abordagem recomendada e mais simples. Você pode ter que resolver problemas com as dependências da sua biblioteca e possivelmente com seu sistema de build, mas na maioria dos casos, isso deve resolver o problema para você. Também é recomendado renomear seus arquivos CJS para terminar com
.cjs
, para maior clareza. -
Você pode optar por tornar toda a sua biblioteca apenas ESM.
Isso significaria definir
"type": "module"
no seupackage.json
e garantir que sua biblioteca construída use a sintaxe ESM. No entanto, você pode enfrentar problemas com suas dependências - e essa abordagem significa que sua biblioteca só pode ser consumida em um contexto ESM.
Migração
O passo inicial de CJS para ESM é atualizar qualquer uso de require
para usar import
em vez disso:
module.exports = ...
exports.hello = ...
const myLib = require('my-lib')
Nos Módulos ESM, ao contrário do CJS, require
, require.resolve
, __filename
e __dirname
globais não estão disponíveis
e devem ser substituídos por import()
e import.meta.filename
.
import { join } from 'path'
const newDir = join(__dirname, 'new-dir')
const someFile = require.resolve('./lib/foo.js')
Melhores Práticas
-
Prefira exportações nomeadas em vez de exportação padrão. Isso ajuda a reduzir conflitos CJS. (veja a seção Exportações padrão)
-
Evite depender de recursos internos do Node.js e de dependências apenas do CommonJS ou Node.js tanto quanto possível para tornar sua biblioteca utilizável em navegadores e Edge Workers sem precisar de polyfills do Nitro.
-
Use o novo campo
exports
com exportações condicionais. (leia mais).
{
"exports": {
".": {
"import": "./dist/mymodule.mjs"
}
}
}
※Esta página é uma tradução não oficial da documentação oficial do Nuxt.js.
A página correspondente na documentação oficial está aqui:
https://nuxt.com/docs/3.x/guide/concepts/esm