La résolution de modules en JavaScript est un sujet complexe.
L'écosystème est actuellement en pleine transition de plusieurs années des modules CommonJS vers les modules ES natifs. TypeScript impose son propre ensemble de règles concernant les extensions d'import qui ne sont pas compatibles avec ESM. Différents outils de build prennent en charge le remappage de chemins via des mécanismes disparates non compatibles.
Bun vise à fournir un système de résolution de modules cohérent et prévisible qui fonctionne simplement. Malheureusement, c'est encore assez complexe.
Syntaxe
Considérons les fichiers suivants.
import { hello } from "./hello";
hello();export function hello() {
console.log("Bonjour le monde !");
}Lorsque nous exécutons index.ts, il affiche "Bonjour le monde !".
bun index.ts
Bonjour le monde !Dans ce cas, nous importons depuis ./hello, un chemin relatif sans extension. Les imports avec extension sont optionnels mais pris en charge. Pour résoudre cet import, Bun vérifiera les fichiers suivants dans l'ordre :
./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
Les chemins d'import peuvent éventuellement inclure des extensions. Si une extension est présente, Bun vérifiera uniquement un fichier avec cette extension exacte.
import { hello } from "./hello";
import { hello } from "./hello.ts"; // cela fonctionneSi vous importez from "*.js{x}", Bun vérifiera également un fichier *.ts{x} correspondant, pour être compatible avec le support des modules ES de TypeScript.
import { hello } from "./hello";
import { hello } from "./hello.ts"; // cela fonctionne
import { hello } from "./hello.js"; // cela fonctionne aussiBun prend en charge à la fois les modules ES (syntaxe import/export) et les modules CommonJS (require()/module.exports). La version CommonJS suivante fonctionnerait également dans Bun.
const { hello } = require("./hello");
hello();function hello() {
console.log("Bonjour le monde !");
}
exports.hello = hello;Cela dit, l'utilisation de CommonJS est découragée dans les nouveaux projets.
Systèmes de modules
Bun prend en charge nativement CommonJS et les modules ES. Les modules ES sont le format de module recommandé pour les nouveaux projets, mais les modules CommonJS sont encore largement utilisés dans l'écosystème Node.js.
Dans le runtime JavaScript de Bun, require peut être utilisé par les modules ES et les modules CommonJS. Si le module cible est un module ES, require retourne l'objet d'espace de noms du module (équivalent à import * as). Si le module cible est un module CommonJS, require retourne l'objet module.exports (comme dans Node.js).
| Type de module | require() | import * as |
|---|---|---|
| Module ES | Espace de noms du module | Espace de noms du module |
| CommonJS | module.exports | default est module.exports, les clés de module.exports sont des exports nommés |
Utilisation de require()
Vous pouvez require() n'importe quel fichier ou package, même les fichiers .ts ou .mjs.
const { foo } = require("./foo"); // les extensions sont optionnelles
const { bar } = require("./bar.mjs");
const { baz } = require("./baz.tsx");Qu'est-ce qu'un module CommonJS ?">
En 2016, ECMAScript a ajouté le support des modules ES. Les modules ES sont la norme pour les modules JavaScript. Cependant, des millions de packages npm utilisent encore les modules CommonJS.
Les modules CommonJS sont des modules qui utilisent module.exports pour exporter des valeurs. Typiquement, require est utilisé pour importer des modules CommonJS.
const stuff = require("./stuff");
module.exports = { stuff };La plus grande différence entre CommonJS et les modules ES est que les modules CommonJS sont synchrones, tandis que les modules ES sont asynchrones. Il y a d'autres différences aussi.
- Les modules ES prennent en charge
awaitde haut niveau et les modules CommonJS non. - Les modules ES sont toujours en mode strict, tandis que les modules CommonJS ne le sont pas.
- Les navigateurs n'ont pas de support natif pour les modules CommonJS, mais ils ont un support natif pour les modules ES via
<script type="module">. - Les modules CommonJS ne sont pas analysables statiquement, tandis que les modules ES n'autorisent que des imports et exports statiques.
Modules CommonJS : Il s'agit d'un type de système de modules utilisé en JavaScript. Une caractéristique clé des modules CommonJS est qu'ils se chargent et s'exécutent de manière synchrone. Cela signifie que lorsque vous importez un module CommonJS, le code de ce module s'exécute immédiatement, et votre programme attend qu'il se termine avant de passer à la tâche suivante. C'est similaire à la lecture d'un livre du début à la fin sans sauter de pages.
Modules ES (ESM) : Il s'agit d'un autre type de système de modules introduit en JavaScript. Ils ont un comportement légèrement différent par rapport à CommonJS. Dans ESM, les imports statiques (imports effectués à l'aide d'instructions import) sont synchrones, tout comme CommonJS. Cela signifie que lorsque vous importez un ESM à l'aide d'une instruction import régulière, le code de ce module s'exécute immédiatement, et votre programme procède étape par étape. Pensez-y comme lire un livre page par page.
Imports dynamiques : Maintenant, voici la partie qui peut prêter à confusion. Les modules ES prennent également en charge l'import de modules à la volée via la fonction import(). C'est ce qu'on appelle un "import dynamique" et il est asynchrone, donc il ne bloque pas l'exécution du programme principal. Au lieu de cela, il récupère et charge le module en arrière-plan pendant que votre programme continue de s'exécuter. Une fois que le module est prêt, vous pouvez l'utiliser. C'est comme obtenir des informations supplémentaires d'un livre pendant que vous êtes toujours en train de le lire, sans avoir à interrompre votre lecture.
En résumé :
- Les modules CommonJS et les modules ES statiques (instructions
import) fonctionnent de manière synchrone similaire, comme lire un livre du début à la fin. - Les modules ES offrent également la possibilité d'importer des modules de manière asynchrone à l'aide de la fonction
import(). C'est comme chercher des informations supplémentaires au milieu de la lecture du livre sans s'arrêter.
Utilisation de import
Vous pouvez import n'importe quel fichier ou package, même les fichiers .cjs.
import { foo } from "./foo"; // les extensions sont optionnelles
import bar from "./bar.ts";
import { stuff } from "./my-commonjs.cjs";Utilisation de import et require() ensemble
Dans Bun, vous pouvez utiliser import ou require dans le même fichier — ils fonctionnent tous les deux, tout le temps.
import { stuff } from "./my-commonjs.cjs";
import Stuff from "./my-commonjs.cjs";
const myStuff = require("./my-commonjs.cjs");Await de haut niveau
La seule exception à cette règle est l'await de haut niveau. Vous ne pouvez pas require() un fichier qui utilise l'await de haut niveau, car la fonction require() est intrinsèquement synchrone.
Heureusement, très peu de bibliothèques utilisent l'await de haut niveau, donc c'est rarement un problème. Mais si vous utilisez l'await de haut niveau dans votre code d'application, assurez-vous que ce fichier n'est pas require() depuis ailleurs dans votre application. Au lieu de cela, vous devriez utiliser import ou import() dynamique.
Import de packages
Bun implémente l'algorithme de résolution de modules de Node.js, vous pouvez donc importer des packages depuis node_modules avec un spécificateur nu.
import { stuff } from "foo";La spécification complète de cet algorithme est officiellement documentée dans la documentation de Node.js ; nous ne la répéterons pas ici. Brièvement : si vous importez from "foo", Bun parcourt le système de fichiers à la recherche d'un répertoire node_modules contenant le package foo.
NODE_PATH
Bun prend en charge NODE_PATH pour les répertoires de résolution de modules supplémentaires :
NODE_PATH=./packages bun run src/index.js// packages/foo/index.js
export const hello = "monde";
// src/index.js
import { hello } from "foo";Plusieurs chemins utilisent le séparateur de la plateforme (: sur Unix, ; sur Windows) :
NODE_PATH=./packages:./lib bun run src/index.js # Unix/macOS
NODE_PATH=./packages;./lib bun run src/index.js # WindowsUne fois qu'il trouve le package foo, Bun lit le package.json pour déterminer comment le package doit être importé. Pour déterminer le point d'entrée du package, Bun lit d'abord le champ exports et vérifie les conditions suivantes.
{
"name": "foo",
"exports": {
"bun": "./index.js",
"node": "./index.js",
"require": "./index.js", // si l'importateur est CommonJS
"import": "./index.mjs", // si l'importateur est un module ES
"default": "./index.js"
}
}Celle de ces conditions qui se produit en premier dans le package.json est utilisée pour déterminer le point d'entrée du package.
Bun respecte les "exports" et "imports" de sous-chemin de Node.js.
{
"name": "foo",
"exports": {
".": "./index.js"
}
}Les imports de sous-chemin et les imports conditionnels fonctionnent conjointement.
{
"name": "foo",
"exports": {
".": {
"import": "./index.mjs",
"require": "./index.js"
}
}
}Comme dans Node.js, la spécification de n'importe quel sous-chemin dans la carte "exports" empêchera l'import d'autres sous-chemins ; vous ne pouvez importer que des fichiers qui sont explicitement exportés. Étant donné le package.json ci-dessus :
import stuff from "foo"; // cela fonctionne
import stuff from "foo/index.mjs"; // cela ne fonctionne pasNOTE
**Publication de TypeScript** — Notez que Bun prend en charge la condition d'export `"bun"` spéciale. Si votre bibliothèque est écrite en TypeScript, vous pouvez publier vos fichiers TypeScript (non transpilés !) directement sur `npm`. Si vous spécifiez votre point d'entrée `*.ts` de votre package dans la condition `"bun"`, Bun importera et exécutera directement vos fichiers source TypeScript.Si exports n'est pas défini, Bun revient à "module" (imports ESM uniquement) puis "main".
{
"name": "foo",
"module": "./index.js",
"main": "./index.js"
}Conditions personnalisées
L'indicateur --conditions vous permet de spécifier une liste de conditions à utiliser lors de la résolution de packages depuis le package.json "exports".
Cet indicateur est pris en charge à la fois dans bun build et dans le runtime de Bun.
# Utilisez-le avec bun build :
bun build --conditions="react-server" --target=bun ./app/foo/route.js
# Utilisez-le avec le runtime de bun :
bun --conditions="react-server" ./app/foo/route.jsVous pouvez également utiliser conditions programmatiquement avec Bun.build :
await Bun.build({
conditions: ["react-server"],
target: "bun",
entryPoints: ["./app/foo/route.js"],
});Remappage de chemins
Bun prend en charge le remappage de chemins d'import via compilerOptions.paths de TypeScript dans tsconfig.json, qui fonctionne bien avec les éditeurs. Si vous n'êtes pas un utilisateur TypeScript, vous pouvez obtenir le même comportement en utilisant un jsconfig.json dans la racine de votre projet.
{
"compilerOptions": {
"paths": {
"config": ["./config.ts"], // mapper le spécificateur au fichier
"components/*": ["components/*"] // correspondance par joker
}
}
}Bun prend également en charge les imports de sous-chemin de Node.js dans package.json, où les chemins mappés doivent commencer par #. Cette approche ne fonctionne pas aussi bien avec les éditeurs, mais les deux options peuvent être utilisées ensemble.
{
"imports": {
"#config": "./config.ts", // mapper le spécificateur au fichier
"#components/*": "./components/*" // correspondance par joker
}
}Détails de bas niveau de l'interopérabilité CommonJS dans Bun">
Le runtime JavaScript de Bun prend en charge nativement CommonJS. Lorsque le transpileur JavaScript de Bun détecte des utilisations de module.exports, il traite le fichier comme CommonJS. Le chargeur de modules enveloppera ensuite le module transpilé dans une fonction de cette forme :
(function (module, exports, require) {
// module transpilé
})(module, exports, require);module, exports et require ressemblent beaucoup au module, exports et require de Node.js. Ceux-ci sont assignés via un with scope en C++. Une Map interne stocke l'objet exports pour gérer les appels require cycliques avant que le module ne soit entièrement chargé.
Une fois que le module CommonJS est évalué avec succès, un enregistrement de module synthétique est créé avec l'export default du module ES défini sur module.exports et les clés de l'objet module.exports sont réexportées en tant qu'exports nommés (si l'objet module.exports est un objet).
Lors de l'utilisation du bundler de Bun, cela fonctionne différemment. Le bundler enveloppera le module CommonJS dans une fonction require_${moduleName} qui retourne l'objet module.exports.
import.meta
L'objet import.meta est un moyen pour un module d'accéder aux informations le concernant. Il fait partie du langage JavaScript, mais son contenu n'est pas standardisé. Chaque "hôte" (navigateur, runtime, etc.) est libre d'implémenter toutes les propriétés qu'il souhaite sur l'objet import.meta.
Bun implémente les propriétés suivantes.
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 ce fichier est directement exécuté par `bun run`
// `false` sinon
import.meta.resolve("zod"); // => "file:///path/to/project/node_modules/zod/index.js"| Propriété | Description |
|---|---|
import.meta.dir | Chemin absolu vers le répertoire contenant le fichier actuel, par exemple /path/to/project. Équivalent à __dirname dans les modules CommonJS (et Node.js) |
import.meta.dirname | Un alias vers import.meta.dir, pour la compatibilité Node.js |
import.meta.env | Un alias vers process.env. |
import.meta.file | Le nom du fichier actuel, par exemple index.tsx |
import.meta.path | Chemin absolu vers le fichier actuel, par exemple /path/to/project/index.ts. Équivalent à __filename dans les modules CommonJS (et Node.js) |
import.meta.filename | Un alias vers import.meta.path, pour la compatibilité Node.js |
import.meta.main | Indique si le fichier actuel est le point d'entrée du processus bun actuel. Le fichier est-il directement exécuté par bun run ou est-il importé ? |
import.meta.resolve | Résout un spécificateur de module (par exemple "zod" ou "./file.tsx") vers une url. Équivalent à import.meta.resolve dans les navigateurs. Exemple : import.meta.resolve("zod") retourne "file:///path/to/project/node_modules/zod/index.ts" |
import.meta.url | Une url string vers le fichier actuel, par exemple file:///path/to/project/index.ts. Équivalent à import.meta.url dans les navigateurs |