La risoluzione dei moduli in JavaScript è un argomento complesso.
L'ecosistema è attualmente nel mezzo di una transizione pluriennale dai moduli CommonJS ai moduli ES nativi. TypeScript impone il proprio insieme di regole intorno alle estensioni di import che non sono compatibili con ESM. Diversi strumenti di build supportano la rimappatura dei percorsi tramite meccanismi disparati e non compatibili.
Bun mira a fornire un sistema di risoluzione dei moduli coerente e prevedibile che funzioni semplicemente. Purtroppo è ancora piuttosto complesso.
Sintassi
Considera i seguenti file.
import { hello } from "./hello";
hello();export function hello() {
console.log("Hello world!");
}Quando eseguiamo index.ts, stampa "Hello world!".
bun index.ts
Hello world!In questo caso, stiamo importando da ./hello, un percorso relativo senza estensione. Gli import con estensione sono opzionali ma supportati. Per risolvere questo import, Bun controllerà i seguenti file in ordine:
./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
I percorsi di import possono opzionalmente includere estensioni. Se è presente un'estensione, Bun controllerà solo un file con quella esatta estensione.
import { hello } from "./hello";
import { hello } from "./hello.ts"; // funzionaSe importi from "*.js{x}", Bun controllerà inoltre un file *.ts{x} corrispondente, per essere compatibile con il supporto ai moduli ES di TypeScript.
import { hello } from "./hello";
import { hello } from "./hello.ts"; // funziona
import { hello } from "./hello.js"; // funziona anche questoBun supporta sia i moduli ES (sintassi import/export) che i moduli CommonJS (require()/module.exports). La seguente versione CommonJS funzionerebbe anche in Bun.
const { hello } = require("./hello");
hello();function hello() {
console.log("Hello world!");
}
exports.hello = hello;Detto questo, l'uso di CommonJS è sconsigliato nei nuovi progetti.
Sistemi di moduli
Bun ha supporto nativo per CommonJS e moduli ES. I moduli ES sono il formato di modulo consigliato per i nuovi progetti, ma i moduli CommonJS sono ancora ampiamente utilizzati nell'ecosistema Node.js.
Nel runtime JavaScript di Bun, require può essere usato sia dai moduli ES che dai moduli CommonJS. Se il modulo di destinazione è un modulo ES, require restituisce l'oggetto namespace del modulo (equivalente a import * as). Se il modulo di destinazione è un modulo CommonJS, require restituisce l'oggetto module.exports (come in Node.js).
| Tipo di Modulo | require() | import * as |
|---|---|---|
| Modulo ES | Namespace Modulo | Namespace Modulo |
| CommonJS | module.exports | default è module.exports, le chiavi di module.exports sono export nominati |
Usare require()
Puoi usare require() con qualsiasi file o pacchetto, anche file .ts o .mjs.
const { foo } = require("./foo"); // le estensioni sono opzionali
const { bar } = require("./bar.mjs");
const { baz } = require("./baz.tsx");Cos'è un modulo CommonJS?">
Nel 2016, ECMAScript ha aggiunto il supporto per i moduli ES. I moduli ES sono lo standard per i moduli JavaScript. Tuttavia, milioni di pacchetti npm usano ancora i moduli CommonJS.
I moduli CommonJS sono moduli che usano module.exports per esportare valori. Tipicamente, require è usato per importare moduli CommonJS.
const stuff = require("./stuff");
module.exports = { stuff };La differenza più grande tra CommonJS e i moduli ES è che i moduli CommonJS sono sincroni, mentre i moduli ES sono asincroni. Ci sono anche altre differenze.
- I moduli ES supportano
awaita livello superiore e i moduli CommonJS no. - I moduli ES sono sempre in strict mode, mentre i moduli CommonJS no.
- I browser non hanno supporto nativo per i moduli CommonJS, ma hanno supporto nativo per i moduli ES tramite
<script type="module">. - I moduli CommonJS non sono analizzabili staticamente, mentre i moduli ES consentono solo import ed export statici.
Moduli CommonJS: Questi sono un tipo di sistema di moduli usato in JavaScript. Una caratteristica chiave dei moduli CommonJS è che vengono caricati ed eseguiti in modo sincrono. Questo significa che quando importi un modulo CommonJS, il codice in quel modulo viene eseguito immediatamente e il tuo programma aspetta che finisca prima di passare al compito successivo. È simile alla lettura di un libro dall'inizio alla fine senza saltare pagine.
Moduli ES (ESM): Questi sono un altro tipo di sistema di moduli introdotto in JavaScript. Hanno un comportamento leggermente diverso rispetto a CommonJS. In ESM, gli import statici (import fatti usando istruzioni import) sono sincroni, proprio come CommonJS. Questo significa che quando importi un ESM usando una normale istruzione import, il codice in quel modulo viene eseguito immediatamente e il tuo programma procede passo dopo passo. Pensalo come leggere un libro pagina per pagina.
Import dinamici: Ora, ecco la parte che potrebbe creare confusione. I moduli ES supportano anche l'import di moduli al volo tramite la funzione import(). Questo è chiamato "import dinamico" ed è asincrono, quindi non blocca l'esecuzione del programma principale. Invece, recupera e carica il modulo in background mentre il tuo programma continua a funzionare. Una volta che il modulo è pronto, puoi usarlo. È come ottenere informazioni aggiuntive da un libro mentre lo stai ancora leggendo, senza dover interrompere la lettura.
In sintesi:
- I moduli CommonJS e i moduli ES statici (istruzioni
import) funzionano in modo sincrono simile, come leggere un libro dall'inizio alla fine. - I moduli ES offrono anche l'opzione di importare moduli in modo asincrono usando la funzione
import(). È come cercare informazioni aggiuntive nel mezzo della lettura del libro senza fermarsi.
Usare import
Puoi import qualsiasi file o pacchetto, anche file .cjs.
import { foo } from "./foo"; // le estensioni sono opzionali
import bar from "./bar.ts";
import { stuff } from "./my-commonjs.cjs";Usare import e require() insieme
In Bun, puoi usare import o require nello stesso file—funzionano entrambi, sempre.
import { stuff } from "./my-commonjs.cjs";
import Stuff from "./my-commonjs.cjs";
const myStuff = require("./my-commonjs.cjs");Top level await
L'unica eccezione a questa regola è il top-level await. Non puoi require() un file che usa top-level await, poiché la funzione require() è intrinsecamente sincrona.
Fortunatamente, pochissime librerie usano top-level await, quindi questo è raramente un problema. Ma se stai usando top-level await nel tuo codice applicativo, assicurati che quel file non venga require() da altre parti della tua applicazione. Invece, dovresti usare import o dynamic import().
Importare pacchetti
Bun implementa l'algoritmo di risoluzione dei moduli di Node.js, quindi puoi importare pacchetti da node_modules con uno specificatore semplice.
import { stuff } from "foo";Le specifiche complete di questo algoritmo sono documentate ufficialmente nella documentazione di Node.js; non le ripeteremo qui. In breve: se importi from "foo", Bun scansiona il file system alla ricerca di una directory node_modules contenente il pacchetto foo.
NODE_PATH
Bun supporta NODE_PATH per directory di risoluzione dei moduli aggiuntive:
NODE_PATH=./packages bun run src/index.js// packages/foo/index.js
export const hello = "world";
// src/index.js
import { hello } from "foo";Percorsi multipli usano il delimitatore della piattaforma (: su Unix, ; su Windows):
NODE_PATH=./packages:./lib bun run src/index.js # Unix/macOS
NODE_PATH=./packages;./lib bun run src/index.js # WindowsUna volta trovato il pacchetto foo, Bun legge il package.json per determinare come il pacchetto dovrebbe essere importato. Per determinare il punto di ingresso del pacchetto, Bun legge prima il campo exports e controlla le seguenti condizioni.
{
"name": "foo",
"exports": {
"bun": "./index.js",
"node": "./index.js",
"require": "./index.js", // se l'importatore è CommonJS
"import": "./index.mjs", // se l'importatore è modulo ES
"default": "./index.js"
}
}Quale di queste condizioni si verifica per prima nel package.json viene usata per determinare il punto di ingresso del pacchetto.
Bun rispetta i subpath "exports" e "imports".
{
"name": "foo",
"exports": {
".": "./index.js"
}
}Gli import di subpath e gli import condizionali funzionano in congiunzione tra loro.
{
"name": "foo",
"exports": {
".": {
"import": "./index.mjs",
"require": "./index.js"
}
}
}Come in Node.js, specificare qualsiasi subpath nella mappa "exports" impedirà ad altri subpath di essere importabili; puoi importare solo file che sono esplicitamente esportati. Dato il package.json sopra:
import stuff from "foo"; // funziona
import stuff from "foo/index.mjs"; // non funzionaNOTE
**Distribuire TypeScript** — Nota che Bun supporta la condizione di export speciale `"bun"`. Se la tua libreria è scritta in TypeScript, puoi pubblicare i tuoi file TypeScript (non transpilati!) direttamente su `npm`. Se specifichi il punto di ingresso `*.ts` del tuo pacchetto nella condizione `"bun"`, Bun importerà ed eseguirà direttamente i tuoi file sorgente TypeScript.Se exports non è definito, Bun ripiega su "module" (solo import ESM) poi su "main".
{
"name": "foo",
"module": "./index.js",
"main": "./index.js"
}Condizioni personalizzate
Il flag --conditions ti permette di specificare un elenco di condizioni da usare quando si risolvono i pacchetti dal "exports" del package.json.
Questo flag è supportato sia in bun build che nel runtime di Bun.
# Usalo con bun build:
bun build --conditions="react-server" --target=bun ./app/foo/route.js
# Usalo con il runtime di bun:
bun --conditions="react-server" ./app/foo/route.jsPuoi anche usare conditions programmaticamente con Bun.build:
await Bun.build({
conditions: ["react-server"],
target: "bun",
entryPoints: ["./app/foo/route.js"],
});Rimappatura dei percorsi
Bun supporta la rimappatura dei percorsi di import tramite compilerOptions.paths di TypeScript in tsconfig.json, che funziona bene con gli editor. Se non sei un utente TypeScript, puoi ottenere lo stesso comportamento usando un jsconfig.json nella root del tuo progetto.
{
"compilerOptions": {
"paths": {
"config": ["./config.ts"], // mappa lo specificatore al file
"components/*": ["components/*"] // corrispondenza wildcard
}
}
}Bun supporta anche gli import di subpath in stile Node.js in package.json, dove i percorsi mappati devono iniziare con #. Questo approccio non funziona altrettanto bene con gli editor, ma entrambe le opzioni possono essere usate insieme.
{
"imports": {
"#config": "./config.ts", // mappa lo specificatore al file
"#components/*": "./components/*" // corrispondenza wildcard
}
}Dettagli di basso livello dell'interop CommonJS in Bun">
Il runtime JavaScript di Bun ha supporto nativo per CommonJS. Quando il transpiler JavaScript di Bun rileva usi di module.exports, tratta il file come CommonJS. Il caricatore di moduli avvolgerà quindi il modulo transpilato in una funzione strutturata così:
(function (module, exports, require) {
// modulo transpilato
})(module, exports, require);module, exports, e require sono molto simili al module, exports, e require in Node.js. Questi sono assegnati tramite uno with scope in C++. Una Map interna memorizza l'oggetto exports per gestire le chiamate require cicliche prima che il modulo sia completamente caricato.
Una volta che il modulo CommonJS è stato valutato con successo, viene creato un Synthetic Module Record con l'export del modulo ES default impostato su module.exports e le chiavi dell'oggetto module.exports sono riesportate come export nominati (se l'oggetto module.exports è un oggetto).
Quando si usa il bundler di Bun, questo funziona in modo diverso. Il bundler avvolgerà il modulo CommonJS in una funzione require_${moduleName} che restituisce l'oggetto module.exports.
import.meta
L'oggetto import.meta è un modo per un modulo di accedere alle informazioni su se stesso. Fa parte del linguaggio JavaScript, ma i suoi contenuti non sono standardizzati. Ogni "host" (browser, runtime, ecc) è libero di implementare qualsiasi proprietà desideri sull'oggetto import.meta.
Bun implementa le seguenti proprietà.
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 questo file è eseguito direttamente da `bun run`
// `false` altrimenti
import.meta.resolve("zod"); // => "file:///path/to/project/node_modules/zod/index.js"| Proprietà | Descrizione |
|---|---|
import.meta.dir | Percorso assoluto alla directory contenente il file corrente, ad es. /path/to/project. Equivalente a __dirname nei moduli CommonJS (e Node.js) |
import.meta.dirname | Un alias per import.meta.dir, per compatibilità con Node.js |
import.meta.env | Un alias per process.env. |
import.meta.file | Il nome del file corrente, ad es. index.tsx |
import.meta.path | Percorso assoluto al file corrente, ad es. /path/to/project/index.ts. Equivalente a __filename nei moduli CommonJS (e Node.js) |
import.meta.filename | Un alias per import.meta.path, per compatibilità con Node.js |
import.meta.main | Indica se il file corrente è il punto di ingresso del processo bun corrente. Il file viene eseguito direttamente da bun run o viene importato? |
import.meta.resolve | Risolve uno specificatore di modulo (ad es. "zod" o "./file.tsx") in un url. Equivalente a import.meta.resolve nei browser. Esempio: import.meta.resolve("zod") restituisce "file:///path/to/project/node_modules/zod/index.ts" |
import.meta.url | Un url string al file corrente, ad es. file:///path/to/project/index.ts. Equivalente a import.meta.url nei browser |