La resolución de módulos en JavaScript es un tema complejo.
El ecosistema está actualmente en medio de una transición de varios años desde los módulos CommonJS a los módulos ES nativos. TypeScript impone su propio conjunto de reglas sobre las extensiones de importación que no son compatibles con ESM. Diferentes herramientas de construcción soportan el re-mapeo de rutas mediante mecanismos dispares no compatibles.
Bun tiene como objetivo proporcionar un sistema de resolución de módulos consistente y predecible que simplemente funcione. Desafortunadamente, todavía es bastante complejo.
Sintaxis
Considera los siguientes archivos.
import { hello } from "./hello";
hello();export function hello() {
console.log("Hello world!");
}Cuando ejecutamos index.ts, imprime "Hello world!".
bun index.ts
Hello world!En este caso, estamos importando desde ./hello, una ruta relativa sin extensión. Las importaciones con extensión son opcionales pero soportadas. Para resolver esta importación, Bun verificará los siguientes archivos en orden:
./hello.tsx./hello.jsx./hello.ts./hello.mjs./hello.js./hello.cjs./hello.json./hello/index.tsx./hello/index.jsx./hello/index.ts./hello/index.mjs./hello/index.js./hello/index.cjs./hello/index.json
Las rutas de importación pueden opcionalmente incluir extensiones. Si hay una extensión presente, Bun solo verificará un archivo con esa extensión exacta.
import { hello } from "./hello";
import { hello } from "./hello.ts"; // esto funcionaSi importas from "*.js{x}", Bun además verificará un archivo *.ts{x} coincidente, para ser compatible con el soporte de módulos ES de TypeScript.
import { hello } from "./hello";
import { hello } from "./hello.ts"; // esto funciona
import { hello } from "./hello.js"; // esto también funcionaBun soporta tanto módulos ES (sintaxis import/export) como módulos CommonJS (require()/module.exports). La siguiente versión CommonJS también funcionaría en Bun.
const { hello } = require("./hello");
hello();function hello() {
console.log("Hello world!");
}
exports.hello = hello;Dicho esto, el uso de CommonJS se desalienta en nuevos proyectos.
Sistemas de módulos
Bun tiene soporte nativo para CommonJS y módulos ES. Los módulos ES son el formato de módulo recomendado para nuevos proyectos, pero los módulos CommonJS todavía son ampliamente utilizados en el ecosistema de Node.js.
En el runtime JavaScript de Bun, require puede ser usado tanto por módulos ES como por módulos CommonJS. Si el módulo objetivo es un módulo ES, require devuelve el objeto de espacio de nombres del módulo (equivalente a import * as). Si el módulo objetivo es un módulo CommonJS, require devuelve el objeto module.exports (como en Node.js).
| Tipo de Módulo | require() | import * as |
|---|---|---|
| ES Module | Espacio de Nombres del Módulo | Espacio de Nombres del Módulo |
| CommonJS | module.exports | default es module.exports, las claves de module.exports son exportaciones nombradas |
Usando require()
Puedes require() cualquier archivo o paquete, incluso archivos .ts o .mjs.
const { foo } = require("./foo"); // las extensiones son opcionales
const { bar } = require("./bar.mjs");
const { baz } = require("./baz.tsx");¿Qué es un módulo CommonJS?
En 2016, ECMAScript añadió soporte para módulos ES. Los módulos ES son el estándar para módulos JavaScript. Sin embargo, millones de paquetes npm todavía usan módulos CommonJS.
Los módulos CommonJS son módulos que usan module.exports para exportar valores. Típicamente, require se usa para importar módulos CommonJS.
const stuff = require("./stuff");
module.exports = { stuff };La mayor diferencia entre CommonJS y los módulos ES es que los módulos CommonJS son sincrónicos, mientras que los módulos ES son asincrónicos. También hay otras diferencias.
- Los módulos ES soportan
awaita nivel superior y los módulos CommonJS no. - Los módulos ES siempre están en modo estricto, mientras que los módulos CommonJS no.
- Los navegadores no tienen soporte nativo para módulos CommonJS, pero tienen soporte nativo para módulos ES vía
<script type="module">. - Los módulos CommonJS no son estáticamente analizables, mientras que los módulos ES solo permiten importaciones y exportaciones estáticas.
Módulos CommonJS: Estos son un tipo de sistema de módulos usado en JavaScript. Una característica clave de los módulos CommonJS es que se cargan y ejecutan sincrónicamente. Esto significa que cuando importas un módulo CommonJS, el código en ese módulo se ejecuta inmediatamente, y tu programa espera a que termine antes de pasar a la siguiente tarea. Es similar a leer un libro de principio a fin sin saltar páginas.
Módulos ES (ESM): Estos son otro tipo de sistema de módulos introducido en JavaScript. Tienen un comportamiento ligeramente diferente comparado con CommonJS. En ESM, las importaciones estáticas (importaciones hechas usando declaraciones import) son sincrónicas, igual que CommonJS. Esto significa que cuando importas un ESM usando una declaración import regular, el código en ese módulo se ejecuta inmediatamente, y tu programa procede paso a paso. Piénsalo como leer un libro página por página.
Importaciones dinámicas: Ahora, aquí viene la parte que puede ser confusa. Los módulos ES también soportan importar módulos sobre la marcha vía la función import(). Esto se llama "importación dinámica" y es asincrónica, por lo que no bloquea la ejecución del programa principal. En cambio, busca y carga el módulo en segundo plano mientras tu programa continúa ejecutándose. Una vez que el módulo está listo, puedes usarlo. Esto es como obtener información adicional de un libro mientras todavía lo estás leyendo, sin tener que pausar tu lectura.
En resumen:
- Los módulos CommonJS y los módulos ES estáticos (declaraciones
import) funcionan de manera sincrónica similar, como leer un libro de principio a fin. - Los módulos ES también ofrecen la opción de importar módulos asincrónicamente usando la función
import(). Esto es como buscar información adicional en medio de leer el libro sin detenerse.
Usando import
Puedes import cualquier archivo o paquete, incluso archivos .cjs.
import { foo } from "./foo"; // las extensiones son opcionales
import bar from "./bar.ts";
import { stuff } from "./my-commonjs.cjs";Usando import y require() juntos
En Bun, puedes usar import o require en el mismo archivo: ambos funcionan todo el tiempo.
import { stuff } from "./my-commonjs.cjs";
import Stuff from "./my-commonjs.cjs";
const myStuff = require("./my-commonjs.cjs");Await a nivel superior
La única excepción a esta regla es el await a nivel superior. No puedes require() un archivo que usa await a nivel superior, ya que la función require() es inherentemente sincrónica.
Afortunadamente, muy pocas librerías usan await a nivel superior, por lo que esto rara vez es un problema. Pero si estás usando await a nivel superior en tu código de aplicación, asegúrate de que ese archivo no esté siendo require() desde otra parte de tu aplicación. En su lugar, deberías usar import o import() dinámico.
Importando paquetes
Bun implementa el algoritmo de resolución de módulos de Node.js, por lo que puedes importar paquetes desde node_modules con un especificador desnudo.
import { stuff } from "foo";La especificación completa de este algoritmo está oficialmente documentada en la documentación de Node.js; no lo repetiremos aquí. Brevemente: si importas from "foo", Bun escanea hacia arriba en el sistema de archivos buscando un directorio node_modules que contenga el paquete foo.
NODE_PATH
Bun soporta NODE_PATH para directorios de resolución de módulos adicionales:
NODE_PATH=./packages bun run src/index.js// packages/foo/index.js
export const hello = "world";
// src/index.js
import { hello } from "foo";Múltiples rutas usan el delimitador de la plataforma (: en Unix, ; en Windows):
NODE_PATH=./packages:./lib bun run src/index.js # Unix/macOS
NODE_PATH=./packages;./lib bun run src/index.js # WindowsUna vez que encuentra el paquete foo, Bun lee el package.json para determinar cómo se debe importar el paquete. Para determinar el punto de entrada del paquete, Bun primero lee el campo exports y verifica las siguientes condiciones.
{
"name": "foo",
"exports": {
"bun": "./index.js",
"node": "./index.js",
"require": "./index.js", // si el importador es CommonJS
"import": "./index.mjs", // si el importador es módulo ES
"default": "./index.js"
}
}Cualquiera de estas condiciones que ocurra primero en el package.json se usa para determinar el punto de entrada del paquete.
Bun respeta las "exports" de subruta y las "imports".
{
"name": "foo",
"exports": {
".": "./index.js"
}
}Las importaciones de subruta y las importaciones condicionales funcionan en conjunto entre sí.
{
"name": "foo",
"exports": {
".": {
"import": "./index.mjs",
"require": "./index.js"
}
}
}Como en Node.js, especificar cualquier subruta en el mapa "exports" evitará que otras subrutas sean importables; solo puedes importar archivos que estén explícitamente exportados. Dado el package.json anterior:
import stuff from "foo"; // esto funciona
import stuff from "foo/index.mjs"; // esto no funcionaNOTE
**Distribuyendo TypeScript** — Ten en cuenta que Bun soporta la condición de exportación especial `"bun"`. Si tu librería está escrita en TypeScript, puedes publicar tus archivos TypeScript (¡sin transpilar!) directamente en `npm`. Si especificas el punto de entrada `*.ts` de tu paquete en la condición `"bun"`, Bun importará y ejecutará directamente tus archivos fuente de TypeScript.Si exports no está definido, Bun vuelve a "module" (solo importaciones ESM) y luego a "main".
{
"name": "foo",
"module": "./index.js",
"main": "./index.js"
}Condiciones personalizadas
La bandera --conditions te permite especificar una lista de condiciones a usar al resolver paquetes desde package.json "exports".
Esta bandera está soportada tanto en bun build como en el runtime de Bun.
# Úsala con bun build:
bun build --conditions="react-server" --target=bun ./app/foo/route.js
# Úsala con el runtime de bun:
bun --conditions="react-server" ./app/foo/route.jsTambién puedes usar conditions programáticamente con Bun.build:
await Bun.build({
conditions: ["react-server"],
target: "bun",
entryPoints: ["./app/foo/route.js"],
});Re-mapeo de rutas
Bun soporta el re-mapeo de rutas de importación a través de compilerOptions.paths de TypeScript en tsconfig.json, que funciona bien con editores. Si no eres usuario de TypeScript, puedes lograr el mismo comportamiento usando un jsconfig.json en la raíz de tu proyecto.
{
"compilerOptions": {
"paths": {
"config": ["./config.ts"], // mapea especificador a archivo
"components/*": ["components/*"] // coincidencia con comodín
}
}
}Bun también soporta importaciones de subruta al estilo de Node.js en package.json, donde las rutas mapeadas deben comenzar con #. Este enfoque no funciona tan bien con editores, pero ambas opciones se pueden usar juntas.
{
"imports": {
"#config": "./config.ts", // mapea especificador a archivo
"#components/*": "./components/*" // coincidencia con comodín
}
}Detalles de bajo nivel de la interoperabilidad CommonJS en Bun
El runtime JavaScript de Bun tiene soporte nativo para CommonJS. Cuando el transpilador JavaScript de Bun detecta usos de module.exports, trata el archivo como CommonJS. El cargador de módulos luego envolverá el módulo transpilado en una función con esta forma:
(function (module, exports, require) {
// módulo transpilado
})(module, exports, require);module, exports, y require son muy similares al module, exports, y require en Node.js. Estos se asignan vía un with scope en C++. Un Map interno almacena el objeto exports para manejar llamadas require cíclicas antes de que el módulo esté completamente cargado.
Una vez que el módulo CommonJS se evalúa exitosamente, se crea un Synthetic Module Record con la exportación de módulo ES default establecida en module.exports y las claves del objeto module.exports se re-exportan como exportaciones nombradas (si el objeto module.exports es un objeto).
Al usar el empaquetador de Bun, esto funciona de manera diferente. El empaquetador envolverá el módulo CommonJS en una función require_${moduleName} que devuelve el objeto module.exports.
import.meta
El objeto import.meta es una forma para que un módulo acceda a información sobre sí mismo. Es parte del lenguaje JavaScript, pero su contenido no está estandarizado. Cada "host" (navegador, runtime, etc) es libre de implementar cualquier propiedad que desee en el objeto import.meta.
Bun implementa las siguientes propiedades.
import.meta.dir; // => "/path/to/project"
import.meta.file; // => "file.ts"
import.meta.path; // => "/path/to/project/file.ts"
import.meta.url; // => "file:///path/to/project/file.ts"
import.meta.main; // `true` si este archivo se ejecuta directamente con `bun run`
// `false` en caso contrario
import.meta.resolve("zod"); // => "file:///path/to/project/node_modules/zod/index.js"| Propiedad | Descripción |
|---|---|
import.meta.dir | Ruta absoluta al directorio que contiene el archivo actual, ej. /path/to/project. Equivalente a __dirname en módulos CommonJS (y Node.js) |
import.meta.dirname | Un alias para import.meta.dir, para compatibilidad con Node.js |
import.meta.env | Un alias para process.env. |
import.meta.file | El nombre del archivo actual, ej. index.tsx |
import.meta.path | Ruta absoluta al archivo actual, ej. /path/to/project/index.ts. Equivalente a __filename en módulos CommonJS (y Node.js) |
import.meta.filename | Un alias para import.meta.path, para compatibilidad con Node.js |
import.meta.main | Indica si el archivo actual es el punto de entrada al proceso bun actual. ¿El archivo está siendo ejecutado directamente por bun run o está siendo importado? |
import.meta.resolve | Resuelve un especificador de módulo (ej. "zod" o "./file.tsx") a una url. Equivalente a import.meta.resolve en navegadores. Ejemplo: import.meta.resolve("zod") devuelve "file:///path/to/project/node_modules/zod/index.ts" |
import.meta.url | Una url string al archivo actual, ej. file:///path/to/project/index.ts. Equivalente a import.meta.url en navegadores |