nuxt logo

Traducción de Documentación (No Oficial)

Nuxt.js
Version:v3.17

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 tu package.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):

  1. archivos que terminan en .mjs - se espera que usen sintaxis ESM
  2. archivos que terminan en .cjs - se espera que usen sintaxis CJS
  3. archivos que terminan en .js - se espera que usen sintaxis CJS a menos que su package.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:

Terminal
(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:

Terminal
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:

node_modules/cjs-pkg/index.js
module.exports = { test: 123 }
// o
exports.test = 123

Esto normalmente funciona bien si requerimos tal dependencia:

test.cjs
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:

  1. 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.

  2. Puedes optar por hacer que toda tu biblioteca sea solo ESM.

    Esto significaría establecer "type": "module" en tu package.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"
    }
  }
}