A resolução de módulos em JavaScript é um tópico complexo.
O ecossistema está atualmente em meio a uma transição de anos dos módulos CommonJS para módulos ES nativos. O TypeScript impõe seu próprio conjunto de regras sobre extensões de import que não são compatíveis com ESM. Diferentes ferramentas de build suportam remapeamento de caminhos através de mecanismos díspares não compatíveis.
O Bun visa fornecer um sistema de resolução de módulos consistente e previsível que simplesmente funciona. Infelizmente, ainda é bastante complexo.
Sintaxe
Considere os seguintes arquivos.
import { hello } from "./hello";
hello();export function hello() {
console.log("Hello world!");
}Quando executamos index.ts, ele imprime "Hello world!".
bun index.ts
Hello world!Neste caso, estamos importando de ./hello, um caminho relativo sem extensão. Imports com extensões são opcionais mas suportados. Para resolver este import, o Bun verificará os seguintes arquivos em ordem:
./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
Caminhos de import podem opcionalmente incluir extensões. Se uma extensão estiver presente, o Bun verificará apenas um arquivo com essa extensão exata.
import { hello } from "./hello";
import { hello } from "./hello.ts"; // isso funcionaSe você importar from "*.js{x}", o Bun verificará adicionalmente um arquivo *.ts{x} correspondente, para ser compatível com o suporte a módulos ES do TypeScript.
import { hello } from "./hello";
import { hello } from "./hello.ts"; // isso funciona
import { hello } from "./hello.js"; // isso também funcionaO Bun suporta tanto módulos ES (sintaxe import/export) quanto módulos CommonJS (sintaxe require()/module.exports). A seguinte versão CommonJS também funcionaria no Bun.
const { hello } = require("./hello");
hello();function hello() {
console.log("Hello world!");
}
exports.hello = hello;Dito isso, usar CommonJS é desencorajado em novos projetos.
Sistemas de módulos
O Bun tem suporte nativo para CommonJS e módulos ES. Módulos ES são o formato de módulo recomendado para novos projetos, mas módulos CommonJS ainda são amplamente usados no ecossistema Node.js.
No runtime JavaScript do Bun, require pode ser usado tanto por módulos ES quanto módulos CommonJS. Se o módulo alvo for um módulo ES, require retorna o objeto de namespace do módulo (equivalente a import * as). Se o módulo alvo for um módulo CommonJS, require retorna o objeto module.exports (como no Node.js).
| Tipo de Módulo | require() | import * as |
|---|---|---|
| ES Module | Module Namespace | Module Namespace |
| CommonJS | module.exports | default é module.exports, chaves de module.exports são exports nomeados |
Usando require()
Você pode require() qualquer arquivo ou pacote, mesmo arquivos .ts ou .mjs.
const { foo } = require("./foo"); // extensões são opcionais
const { bar } = require("./bar.mjs");
const { baz } = require("./baz.tsx");O que é um módulo CommonJS?
Em 2016, o ECMAScript adicionou suporte para Módulos ES. Módulos ES são o padrão para módulos JavaScript. No entanto, milhões de pacotes npm ainda usam módulos CommonJS.
Módulos CommonJS são módulos que usam module.exports para exportar valores. Tipicamente, require é usado para importar módulos CommonJS.
const stuff = require("./stuff");
module.exports = { stuff };A maior diferença entre CommonJS e Módulos ES é que módulos CommonJS são síncronos, enquanto Módulos ES são assíncronos. Existem outras diferenças também.
- Módulos ES suportam
awaitde nível superior e módulos CommonJS não. - Módulos ES estão sempre em strict mode, enquanto módulos CommonJS não.
- Navegadores não têm suporte nativo para módulos CommonJS, mas têm suporte nativo para Módulos ES via
<script type="module">. - Módulos CommonJS não são estaticamente analisáveis, enquanto Módulos ES permitem apenas imports e exports estáticos.
Módulos CommonJS: Estes são um tipo de sistema de módulos usado em JavaScript. Uma característica chave dos módulos CommonJS é que eles carregam e executam sincronamente. Isso significa que quando você importa um módulo CommonJS, o código naquele módulo executa imediatamente, e seu programa espera que ele termine antes de prosseguir para a próxima tarefa. É similar a ler um livro do início ao fim sem pular páginas.
Módulos ES (ESM): Estes são outro tipo de sistema de módulos introduzido em JavaScript. Eles têm um comportamento ligeiramente diferente comparado ao CommonJS. Em ESM, imports estáticos (imports feitos usando declarações import) são síncronos, assim como CommonJS. Isso significa que quando você importa um ESM usando uma declaração import regular, o código naquele módulo executa imediatamente, e seu programa prossegue passo a passo. Pense nisso como ler um livro página por página.
Imports dinâmicos: Agora, aqui vem a parte que pode ser confusa. Módulos ES também suportam importar módulos em tempo de execução via função import(). Isso é chamado de "import dinâmico" e é assíncrono, então não bloqueia a execução do programa principal. Em vez disso, ele busca e carrega o módulo em segundo plano enquanto seu programa continua executando. Uma vez que o módulo esteja pronto, você pode usá-lo. Isso é como obter informações adicionais de um livro enquanto você ainda está lendo, sem precisar pausar sua leitura.
Em resumo:
- Módulos CommonJS e Módulos ES estáticos (declarações
import) funcionam de maneira síncrona similar, como ler um livro do início ao fim. - Módulos ES também oferecem a opção de importar módulos assincronamente usando a função
import(). Isso é como procurar informações adicionais no meio da leitura do livro sem parar.
Usando import
Você pode import qualquer arquivo ou pacote, mesmo arquivos .cjs.
import { foo } from "./foo"; // extensões são opcionais
import bar from "./bar.ts";
import { stuff } from "./my-commonjs.cjs";Usando import e require() juntos
No Bun, você pode usar import ou require no mesmo arquivo—ambos funcionam, o tempo todo.
import { stuff } from "./my-commonjs.cjs";
import Stuff from "./my-commonjs.cjs";
const myStuff = require("./my-commonjs.cjs");Await de nível superior
A única exceção a esta regra é await de nível superior. Você não pode require() um arquivo que usa await de nível superior, já que a função require() é inerentemente síncrona.
Felizmente, muito poucas bibliotecas usam await de nível superior, então isso raramente é um problema. Mas se você estiver usando await de nível superior no seu código de aplicação, certifique-se de que esse arquivo não está sendo require() de outro lugar na sua aplicação. Em vez disso, você deve usar import ou import() dinâmico.
Importando pacotes
O Bun implementa o algoritmo de resolução de módulos do Node.js, então você pode importar pacotes de node_modules com um especificador simples.
import { stuff } from "foo";A especificação completa deste algoritmo está oficialmente documentada na documentação do Node.js; não vamos repetir aqui. Resumidamente: se você importar from "foo", o Bun escaneia o sistema de arquivos para cima procurando um diretório node_modules contendo o pacote foo.
NODE_PATH
O Bun suporta NODE_PATH para diretórios de resolução de módulos adicionais:
NODE_PATH=./packages bun run src/index.js// packages/foo/index.js
export const hello = "world";
// src/index.js
import { hello } from "foo";Múltiplos caminhos usam o delimitador da plataforma (: no Unix, ; no Windows):
NODE_PATH=./packages:./lib bun run src/index.js # Unix/macOS
NODE_PATH=./packages;./lib bun run src/index.js # WindowsUma vez que encontra o pacote foo, o Bun lê o package.json para determinar como o pacote deve ser importado. Para determinar o ponto de entrada do pacote, o Bun primeiro lê o campo exports e verifica as seguintes condições.
{
"name": "foo",
"exports": {
"bun": "./index.js",
"node": "./index.js",
"require": "./index.js", // se o importador for CommonJS
"import": "./index.mjs", // se o importador for módulo ES
"default": "./index.js"
}
}Qualquer uma destas condições que ocorrer primeiro no package.json é usada para determinar o ponto de entrada do pacote.
O Bun respeita subpath "exports" e "imports".
{
"name": "foo",
"exports": {
".": "./index.js"
}
}Imports de subpath e imports condicionais funcionam em conjunto uns com os outros.
{
"name": "foo",
"exports": {
".": {
"import": "./index.mjs",
"require": "./index.js"
}
}
}Como no Node.js, especificar qualquer subpath no mapa "exports" impedirá que outros subpaths sejam importáveis; você só pode importar arquivos que estão explicitamente exportados. Dado o package.json acima:
import stuff from "foo"; // isso funciona
import stuff from "foo/index.mjs"; // isso não funcionaNOTE
**Distribuindo TypeScript** — Note que o Bun suporta a condição de export especial `"bun"`. Se sua biblioteca for escrita em TypeScript, você pode publicar seus arquivos TypeScript (não transpilados!) diretamente no `npm`. Se você especificar o ponto de entrada `*.ts` do seu pacote na condição `"bun"`, o Bun importará e executará diretamente seus arquivos fonte TypeScript.Se exports não estiver definido, o Bun faz fallback para "module" (apenas imports ESM) então "main".
{
"name": "foo",
"module": "./index.js",
"main": "./index.js"
}Condições customizadas
A flag --conditions permite que você especifique uma lista de condições para usar ao resolver pacotes do package.json "exports".
Esta flag é suportada tanto no bun build quanto no runtime do Bun.
# Use com bun build:
bun build --conditions="react-server" --target=bun ./app/foo/route.js
# Use com runtime do bun:
bun --conditions="react-server" ./app/foo/route.jsVocê também pode usar conditions programaticamente com Bun.build:
await Bun.build({
conditions: ["react-server"],
target: "bun",
entryPoints: ["./app/foo/route.js"],
});Remapeamento de caminho
O Bun suporta remapeamento de caminho de import através de compilerOptions.paths do TypeScript em tsconfig.json, que funciona bem com editores. Se você não é um usuário TypeScript, pode alcançar o mesmo comportamento usando um jsconfig.json na raiz do seu projeto.
{
"compilerOptions": {
"paths": {
"config": ["./config.ts"], // mapeia especificador para arquivo
"components/*": ["components/*"] // correspondência curinga
}
}
}O Bun também suporta imports de subpath no estilo do Node.js em package.json, onde caminhos mapeados devem começar com #. Esta abordagem não funciona tão bem com editores, mas ambas as opções podem ser usadas juntas.
{
"imports": {
"#config": "./config.ts", // mapeia especificador para arquivo
"#components/*": "./components/*" // correspondência curinga
}
}Detalhes de baixo nível da interoperabilidade CommonJS no Bun
O runtime JavaScript do Bun tem suporte nativo para CommonJS. Quando o transpilador JavaScript do Bun detecta usos de module.exports, ele trata o arquivo como CommonJS. O carregador de módulos então envolverá o módulo transpilado em uma função com o formato:
(function (module, exports, require) {
// módulo transpilado
})(module, exports, require);module, exports, e require são muito similares ao module, exports, e require no Node.js. Estes são atribuídos via um with scope em C++. Um Map interno armazena o objeto exports para lidar com chamadas require cíclicas antes que o módulo seja completamente carregado.
Uma vez que o módulo CommonJS é avaliado com sucesso, um Synthetic Module Record é criado com o export default do Módulo ES definido como module.exports e chaves do objeto module.exports são re-exportadas como exports nomeados (se o objeto module.exports for um objeto).
Ao usar o bundler do Bun, isso funciona de maneira diferente. O bundler envolverá o módulo CommonJS em uma função require_${moduleName} que retorna o objeto module.exports.
import.meta
O objeto import.meta é uma maneira de um módulo acessar informações sobre si mesmo. Faz parte da linguagem JavaScript, mas seu conteúdo não é padronizado. Cada "host" (navegador, runtime, etc) é livre para implementar quaisquer propriedades que desejar no objeto import.meta.
O Bun implementa as seguintes propriedades.
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` se este arquivo for executado diretamente por `bun run`
// `false` caso contrário
import.meta.resolve("zod"); // => "file:///path/to/project/node_modules/zod/index.js"| Propriedade | Descrição |
|---|---|
import.meta.dir | Caminho absoluto para o diretório contendo o arquivo atual, e.g. /path/to/project. Equivalente a __dirname em módulos CommonJS (e Node.js) |
import.meta.dirname | Um alias para import.meta.dir, para compatibilidade com Node.js |
import.meta.env | Um alias para process.env. |
import.meta.file | O nome do arquivo atual, e.g. index.tsx |
import.meta.path | Caminho absoluto para o arquivo atual, e.g. /path/to/project/index.ts. Equivalente a __filename em módulos CommonJS (e Node.js) |
import.meta.filename | Um alias para import.meta.path, para compatibilidade com Node.js |
import.meta.main | Indica se o arquivo atual é o ponto de entrada para o processo bun atual. O arquivo está sendo executado diretamente por bun run ou está sendo importado? |
import.meta.resolve | Resolve um especificador de módulo (e.g. "zod" ou "./file.tsx") para uma url. Equivalente a import.meta.resolve em navegadores. Exemplo: import.meta.resolve("zod") retorna "file:///path/to/project/node_modules/zod/index.ts" |
import.meta.url | Uma url string para o arquivo atual, e.g. file:///path/to/project/index.ts. Equivalente a import.meta.url em navegadores |