Módulos ES
Nuxt utiliza módulos ES nativos.
Esta guía ayuda a explicar qué son los Módulos ES y cómo hacer que una aplicación de Nuxt (o una biblioteca upstream) sea compatible con ESM.
Antecedentes
Módulos CommonJS
CommonJS (CJS) es un formato introducido por Node.js que permite compartir funcionalidad entre módulos JavaScript aislados (leer más). Probablemente ya estés familiarizado con esta sintaxis:
const a = require('./a')
module.exports.a = a
Empaquetadores como webpack y Rollup soportan esta sintaxis y te permiten usar módulos escritos en CommonJS en el navegador.
Sintaxis ESM
La mayoría de las veces, cuando la gente habla de ESM vs. CJS, están hablando de una sintaxis diferente para escribir módulos.
import a from './a'
export { a }
Antes de que los Módulos ECMAScript (ESM) se convirtieran en un estándar (¡tomó más de 10 años!), herramientas como webpack e incluso lenguajes como TypeScript comenzaron a soportar la llamada sintaxis ESM. Sin embargo, hay algunas diferencias clave con la especificación real; aquí hay una explicación útil.
¿Qué es el ESM 'Nativo'?
Es posible que hayas estado escribiendo tu aplicación usando sintaxis ESM durante mucho tiempo. Después de todo, es soportado nativamente por el navegador, y en Nuxt 2 compilamos todo el código que escribiste al formato apropiado (CJS para el servidor, ESM para el navegador).
Al agregar módulos a tu paquete, las cosas eran un poco diferentes. Una biblioteca de ejemplo podría exponer versiones tanto CJS como ESM, y dejarnos elegir cuál queríamos:
{
"name": "sample-library",
"main": "dist/sample-library.cjs.js",
"module": "dist/sample-library.esm.js"
}
Así que en Nuxt 2, el empaquetador (webpack) tomaría el archivo CJS ('main') para la construcción del servidor y usaría el archivo ESM ('module') para la construcción del cliente.
Sin embargo, en las versiones recientes de Node.js LTS, ahora es posible usar módulos ESM nativos dentro de Node.js. Eso significa que Node.js en sí puede procesar JavaScript usando sintaxis ESM, aunque no lo hace por defecto. Las dos formas más comunes de habilitar la sintaxis ESM son:
- establecer
"type": "module"
dentro de tupackage.json
y seguir usando la extensión.js
- usar las extensiones de archivo
.mjs
(recomendado)
Esto es lo que hacemos para Nuxt Nitro; generamos un archivo .output/server/index.mjs
. Eso le dice a Node.js que trate este archivo como un módulo ES nativo.
¿Cuáles son las Importaciones Válidas en un Contexto de Node.js?
Cuando importas
un módulo en lugar de requerirlo
, Node.js lo resuelve de manera diferente. Por ejemplo, cuando importas sample-library
, Node.js no buscará el main
sino la entrada exports
o module
en el package.json
de esa biblioteca.
Esto también es cierto para las importaciones dinámicas, como const b = await import('sample-library')
.
Node soporta los siguientes tipos de importaciones (ver documentación):
- archivos que terminan en
.mjs
- se espera que usen sintaxis ESM - archivos que terminan en
.cjs
- se espera que usen sintaxis CJS - archivos que terminan en
.js
- se espera que usen sintaxis CJS a menos que supackage.json
tenga"type": "module"
¿Qué Tipos de Problemas Pueden Existir?
Durante mucho tiempo, los autores de módulos han estado produciendo compilaciones con sintaxis ESM pero usando convenciones como .esm.js
o .es.js
, que han agregado al campo module
en su package.json
. Esto no ha sido un problema hasta ahora porque solo han sido utilizados por empaquetadores como webpack, que no se preocupan especialmente por la extensión del archivo.
Sin embargo, si intentas importar un paquete con un archivo .esm.js
en un contexto ESM de Node.js, no funcionará, y obtendrás un error 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)
También podrías obtener este error si tienes una importación nombrada de una compilación con sintaxis ESM que Node.js piensa que es 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)
Solución de Problemas de ESM
Si encuentras estos errores, el problema casi con certeza está en la biblioteca upstream. Necesitan arreglar su biblioteca para soportar ser importada por Node.
Transpilación de Bibliotecas
Mientras tanto, puedes decirle a Nuxt que no intente importar estas bibliotecas agregándolas a build.transpile
:
export default defineNuxtConfig({
build: {
transpile: ['sample-library']
}
})
Es posible que también necesites agregar otros paquetes que están siendo importados por estas bibliotecas.
Alias de Bibliotecas
En algunos casos, también puedes necesitar aliasar manualmente la biblioteca a la versión CJS, por ejemplo:
export default defineNuxtConfig({
alias: {
'sample-library': 'sample-library/dist/sample-library.cjs.js'
}
})
Exportaciones por Defecto
Una dependencia con formato CommonJS puede usar module.exports
o exports
para proporcionar una exportación por defecto:
module.exports = { test: 123 }
// o
exports.test = 123
Esto normalmente funciona bien si requerimos
tal dependencia:
const pkg = require('cjs-pkg')
console.log(pkg) // { test: 123 }
Node.js en modo ESM nativo, typescript con esModuleInterop
habilitado y empaquetadores como webpack, proporcionan un mecanismo de compatibilidad para que podamos importar por defecto tal biblioteca.
Este mecanismo a menudo se refiere como "interop require default":
import pkg from 'cjs-pkg'
console.log(pkg) // { test: 123 }
Sin embargo, debido a las complejidades de la detección de sintaxis y diferentes formatos de paquete, siempre existe la posibilidad de que el interop por defecto falle y terminemos con algo como esto:
import pkg from 'cjs-pkg'
console.log(pkg) // { default: { test: 123 } }
También al usar la sintaxis de importación dinámica (en archivos CJS y ESM), siempre tenemos esta situación:
import('cjs-pkg').then(console.log) // [Module: null prototype] { default: { test: '123' } }
En este caso, necesitamos interop manualmente la exportación por defecto:
// Importación estática
import { default as pkg } from 'cjs-pkg'
// Importación dinámica
import('cjs-pkg').then(m => m.default || m).then(console.log)
Para manejar situaciones más complejas y con más seguridad, recomendamos y usamos internamente mlly en Nuxt que puede preservar exportaciones nombradas.
import { interopDefault } from 'mlly'
// Asumiendo que la forma es { default: { foo: 'bar' }, baz: 'qux' }
import myModule from 'my-module'
console.log(interopDefault(myModule)) // { foo: 'bar', baz: 'qux' }
Guía para Autores de Bibliotecas
La buena noticia es que es relativamente simple solucionar problemas de compatibilidad con ESM. Hay dos opciones principales:
-
Puedes renombrar tus archivos ESM para que terminen con
.mjs
.Este es el enfoque recomendado y más simple. Es posible que tengas que resolver problemas con las dependencias de tu biblioteca y posiblemente con tu sistema de compilación, pero en la mayoría de los casos, esto debería solucionar el problema para ti. También se recomienda renombrar tus archivos CJS para que terminen con
.cjs
, para mayor claridad. -
Puedes optar por hacer que toda tu biblioteca sea solo ESM.
Esto significaría establecer
"type": "module"
en tupackage.json
y asegurarte de que tu biblioteca compilada use sintaxis ESM. Sin embargo, podrías enfrentar problemas con tus dependencias, y este enfoque significa que tu biblioteca solo puede ser consumida en un contexto ESM.
Migración
El paso inicial de CJS a ESM es actualizar cualquier uso de require
para usar import
en su lugar:
module.exports = ...
exports.hello = ...
const myLib = require('my-lib')
En los Módulos ESM, a diferencia de CJS, require
, require.resolve
, __filename
y __dirname
no están disponibles
y deben ser reemplazados con import()
y import.meta.filename
.
import { join } from 'path'
const newDir = join(__dirname, 'new-dir')
const someFile = require.resolve('./lib/foo.js')
Mejores Prácticas
-
Prefiere exportaciones nombradas en lugar de exportación por defecto. Esto ayuda a reducir conflictos con CJS. (ver sección Exportaciones por Defecto)
-
Evita depender de las integraciones de Node.js y de dependencias solo de CommonJS o Node.js tanto como sea posible para hacer que tu biblioteca sea utilizable en navegadores y Edge Workers sin necesitar polyfills de Nitro.
-
Usa el nuevo campo
exports
con exportaciones condicionales. (leer más).
{
"exports": {
".": {
"import": "./dist/mymodule.mjs"
}
}
}
※Esta página es una traducción no oficial de la documentación oficial de Nuxt.js.
La página correspondiente en la documentación oficial está aquí:
https://nuxt.com/docs/3.x/guide/concepts/esm