Skip to content

Die Modulauflösung in JavaScript ist ein komplexes Thema.

Das Ökosystem befindet sich derzeit mitten in einem jahrelangen Übergang von CommonJS-Modulen zu nativen ES-Modulen. TypeScript erzwingt eigene Regeln für Importerweiterungen, die nicht mit ESM kompatibel sind. Unterschiedliche Build-Tools unterstützen die Pfadneuzuordnung über verschiedene nicht kompatible Mechanismen.

Bun zielt darauf ab, ein konsistentes und vorhersagbares Modulauflösungssystem bereitzustellen, das einfach funktioniert. Leider ist es immer noch ziemlich komplex.

Syntax

Betrachten Sie die folgenden Dateien.

ts
import { hello } from "./hello";

hello();
ts
export function hello() {
  console.log("Hello world!");
}

Wenn wir index.ts ausführen, wird "Hello world!" ausgegeben.

bash
bun index.ts
Hello world!

In diesem Fall importieren wir aus ./hello, einem relativen Pfad ohne Erweiterung. Importe mit Erweiterung sind optional, aber unterstützt. Um diesen Import aufzulösen, prüft Bun der Reihe nach die folgenden Dateien:

  • ./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

Importpfade können optional Erweiterungen enthalten. Wenn eine Erweiterung vorhanden ist, prüft Bun nur nach einer Datei mit genau dieser Erweiterung.

ts
import { hello } from "./hello";
import { hello } from "./hello.ts"; // das funktioniert

Wenn Sie from "*.js{x}" importieren, prüft Bun zusätzlich nach einer passenden *.ts{x}-Datei, um mit der ES-Modul-Unterstützung von TypeScript kompatibel zu sein.

ts
import { hello } from "./hello";
import { hello } from "./hello.ts"; // das funktioniert
import { hello } from "./hello.js"; // das funktioniert auch

Bun unterstützt sowohl ES-Module (import/export-Syntax) als auch CommonJS-Module (require()/module.exports). Die folgende CommonJS-Version würde auch in Bun funktionieren.

js
const { hello } = require("./hello");

hello();
js
function hello() {
  console.log("Hello world!");
}

exports.hello = hello;

Dennoch wird die Verwendung von CommonJS in neuen Projekten nicht empfohlen.


Modulsysteme

Bun bietet native Unterstützung für CommonJS und ES-Module. ES-Module sind das empfohlene Modulformat für neue Projekte, aber CommonJS-Module werden im Node.js-Ökosystem immer noch häufig verwendet.

In Buns JavaScript-Laufzeit kann require sowohl von ES-Modulen als auch von CommonJS-Modulen verwendet werden. Wenn das Zielmodul ein ES-Modul ist, gibt require das Modul-Namensraumobjekt zurück (äquivalent zu import * as). Wenn das Zielmodul ein CommonJS-Modul ist, gibt require das module.exports-Objekt zurück (wie in Node.js).

Modultyprequire()import * as
ES-ModulModul-NamensraumModul-Namensraum
CommonJSmodule.exportsdefault ist module.exports, Schlüssel von module.exports sind benannte Exporte

Verwendung von require()

Sie können jede Datei oder jedes Paket mit require() importieren, sogar .ts- oder .mjs-Dateien.

index.ts
ts
const { foo } = require("./foo"); // Erweiterungen sind optional
const { bar } = require("./bar.mjs");
const { baz } = require("./baz.tsx");

Was ist ein CommonJS-Modul?">

Im Jahr 2016 fügte ECMAScript Unterstützung für ES-Module hinzu. ES-Module sind der Standard für JavaScript-Module. Millionen von npm-Paketen verwenden jedoch weiterhin CommonJS-Module.

CommonJS-Module sind Module, die module.exports zum Exportieren von Werten verwenden. Typischerweise wird require verwendet, um CommonJS-Module zu importieren.

ts
const stuff = require("./stuff");
module.exports = { stuff };

Der größte Unterschied zwischen CommonJS und ES-Modulen besteht darin, dass CommonJS-Module synchron sind, während ES-Module asynchron sind. Es gibt noch andere Unterschiede.

  • ES-Module unterstützen top-level await und CommonJS-Module nicht.
  • ES-Module sind immer im Strict Mode, während CommonJS-Module dies nicht sind.
  • Browser haben keine native Unterstützung für CommonJS-Module, aber sie haben native Unterstützung für ES-Module über <script type="module">.
  • CommonJS-Module sind nicht statisch analysierbar, während ES-Module nur statische Importe und Exporte erlauben.

CommonJS-Module: Dies sind eine Art von Modulsystem, das in JavaScript verwendet wird. Ein Hauptmerkmal von CommonJS-Modulen ist, dass sie synchron geladen und ausgeführt werden. Das bedeutet, dass beim Importieren eines CommonJS-Moduls der Code in diesem Modul sofort ausgeführt wird und Ihr Programm wartet, bis es fertig ist, bevor es mit der nächsten Aufgabe fortfährt. Es ist ähnlich wie das Lesen eines Buches von Anfang bis Ende, ohne Seiten zu überspringen.

ES-Module (ESM): Dies sind eine andere Art von Modulsystem, das in JavaScript eingeführt wurde. Sie haben ein etwas anderes Verhalten im Vergleich zu CommonJS. In ESM sind statische Importe (Importe, die mit import-Anweisungen gemacht werden) synchron, genau wie CommonJS. Das bedeutet, dass beim Importieren eines ESM mit einer regulären import-Anweisung der Code in diesem Modul sofort ausgeführt wird und Ihr Programm schrittweise fortfährt. Stellen Sie es sich wie das Lesen eines Buches Seite für Seite vor.

Dynamische Importe: Nun kommt der Teil, der verwirrend sein könnte. ES-Module unterstützen auch das Importieren von Modulen im laufenden Betrieb über die import()-Funktion. Dies wird als "dynamischer Import" bezeichnet und ist asynchron, blockiert also nicht die Hauptprogrammausführung. Stattdessen lädt es das Modul im Hintergrund, während Ihr Programm weiterläuft. Sobald das Modul bereit ist, können Sie es verwenden. Das ist wie das Nachschlagen zusätzlicher Informationen in einem Buch, während Sie noch lesen, ohne das Lesen unterbrechen zu müssen.

Zusammenfassend:

  • CommonJS-Module und statische ES-Module (import-Anweisungen) funktionieren auf ähnliche synchrone Weise, wie das Lesen eines Buches von Anfang bis Ende.
  • ES-Module bieten auch die Möglichkeit, Module asynchron mit der import()-Funktion zu importieren. Das ist wie das Nachschlagen zusätzlicher Informationen mitten im Lesen des Buches, ohne anzuhalten.

Verwendung von import

Sie können jede Datei oder jedes Paket importieren, sogar .cjs-Dateien.

index.ts
ts
import { foo } from "./foo"; // Erweiterungen sind optional
import bar from "./bar.ts";
import { stuff } from "./my-commonjs.cjs";

Verwendung von import und require() zusammen

In Bun können Sie import oder require in derselben Datei verwenden – beide funktionieren jederzeit.

index.ts
ts
import { stuff } from "./my-commonjs.cjs";
import Stuff from "./my-commonjs.cjs";

const myStuff = require("./my-commonjs.cjs");

Top-level-await

Die einzige Ausnahme von dieser Regel ist top-level-await. Sie können eine Datei, die top-level-await verwendet, nicht mit require() importieren, da die require()-Funktion inhärent synchron ist.

Glücklicherweise verwenden sehr wenige Bibliotheken top-level-await, sodass dies selten ein Problem darstellt. Aber wenn Sie top-level-await in Ihrem Anwendungscode verwenden, stellen Sie sicher, dass diese Datei nicht von anderer Stelle in Ihrer Anwendung mit require() importiert wird. Stattdessen sollten Sie import oder dynamisches import() verwenden.


Pakete importieren

Bun implementiert den Node.js-Modulauflösungsalgorithmus, sodass Sie Pakete aus node_modules mit einem bloßen Bezeichner importieren können.

index.ts
ts
import { stuff } from "foo";

Die vollständige Spezifikation dieses Algorithmus ist offiziell in der Node.js-Dokumentation dokumentiert; wir werden sie hier nicht wiederholen. Kurz gesagt: Wenn Sie from "foo" importieren, durchsucht Bun das Dateisystem nach einem node_modules-Verzeichnis, das das Paket foo enthält.

NODE_PATH

Bun unterstützt NODE_PATH für zusätzliche Modulauflösungsverzeichnisse:

bash
NODE_PATH=./packages bun run src/index.js
ts
// packages/foo/index.js
export const hello = "world";

// src/index.js
import { hello } from "foo";

Mehrere Pfade verwenden das plattformspezifische Trennzeichen (: unter Unix, ; unter Windows):

bash
NODE_PATH=./packages:./lib bun run src/index.js  # Unix/macOS
NODE_PATH=./packages;./lib bun run src/index.js  # Windows

Sobald es das foo-Paket findet, liest Bun die package.json, um zu bestimmen, wie das Paket importiert werden soll. Um den Einstiegspunkt des Pakets zu bestimmen, liest Bun zuerst das exports-Feld und prüft die folgenden Bedingungen.

json
{
  "name": "foo",
  "exports": {
    "bun": "./index.js",
    "node": "./index.js",
    "require": "./index.js", // wenn der Importeur CommonJS ist
    "import": "./index.mjs", // wenn der Importeur ein ES-Modul ist
    "default": "./index.js"
  }
}

Welche dieser Bedingungen in der package.json zuerst auftritt, wird verwendet, um den Einstiegspunkt des Pakets zu bestimmen.

Bun berücksichtigt Unterpfad-"exports" und "imports".

json
{
  "name": "foo",
  "exports": {
    ".": "./index.js"
  }
}

Unterpfad-Importe und bedingte Importe funktionieren zusammen.

json
{
  "name": "foo",
  "exports": {
    ".": {
      "import": "./index.mjs",
      "require": "./index.js"
    }
  }
}

Wie in Node.js verhindert die Angabe eines Unterpfads in der "exports"-Map, dass andere Unterpfade importierbar sind; Sie können nur Dateien importieren, die explizit exportiert werden. Gegeben die obige package.json:

ts
import stuff from "foo"; // das funktioniert
import stuff from "foo/index.mjs"; // das funktioniert nicht

NOTE

**TypeScript bereitstellen** — Beachten Sie, dass Bun die spezielle `"bun"`-Exportbedingung unterstützt. Wenn Ihre Bibliothek in TypeScript geschrieben ist, können Sie Ihre (nicht transpilierten!) TypeScript-Dateien direkt auf `npm` veröffentlichen. Wenn Sie den `*.ts`-Einstiegspunkt Ihres Pakets in der `"bun"`-Bedingung angeben, importiert und führt Bun Ihre TypeScript-Quelldateien direkt aus.

Wenn exports nicht definiert ist, fällt Bun auf "module" (nur ESM-Importe) und dann auf "main" zurück.

json
{
  "name": "foo",
  "module": "./index.js",
  "main": "./index.js"
}

Benutzerdefinierte Bedingungen

Die --conditions-Flag ermöglicht es Ihnen, eine Liste von Bedingungen anzugeben, die bei der Auflösung von Paketen aus der package.json-"exports" verwendet werden sollen.

Diese Flag wird sowohl in bun build als auch in Buns Laufzeit unterstützt.

sh
# Verwendung mit bun build:
bun build --conditions="react-server" --target=bun ./app/foo/route.js

# Verwendung mit Buns Laufzeit:
bun --conditions="react-server" ./app/foo/route.js

Sie können conditions auch programmgesteuert mit Bun.build verwenden:

ts
await Bun.build({
  conditions: ["react-server"],
  target: "bun",
  entryPoints: ["./app/foo/route.js"],
});

Pfadneuzuordnung

Bun unterstützt die Neuzuordnung von Importpfaden durch TypeScript's compilerOptions.paths in tsconfig.json, was gut mit Editoren funktioniert. Wenn Sie kein TypeScript-Benutzer sind, können Sie das gleiche Verhalten erreichen, indem Sie eine jsconfig.json in Ihrem Projektstammverzeichnis verwenden.

json
{
  "compilerOptions": {
    "paths": {
      "config": ["./config.ts"], // Bezeichner auf Datei abbilden
      "components/*": ["components/*"] // Wildcard-Matching
    }
  }
}

Bun unterstützt auch Node.js-ähnliche Unterpfad-Importe in package.json, wobei abgebildete Pfade mit # beginnen müssen. Dieser Ansatz funktioniert nicht so gut mit Editoren, aber beide Optionen können zusammen verwendet werden.

json
{
  "imports": {
    "#config": "./config.ts", // Bezeichner auf Datei abbilden
    "#components/*": "./components/*" // Wildcard-Matching
  }
}

Low-Level-Details der CommonJS-Interop in Bun">

Buns JavaScript-Laufzeit bietet native Unterstützung für CommonJS. Wenn Buns JavaScript-Transpiler Verwendungen von module.exports erkennt, behandelt es die Datei als CommonJS. Der Modul-Loader wickelt dann das transpilierte Modul in eine Funktion wie diese:

js
(function (module, exports, require) {
  // transpiliertes Modul
})(module, exports, require);

module, exports und require sind sehr ähnlich wie module, exports und require in Node.js. Diese werden über einen with scope in C++ zugewiesen. Eine interne Map speichert das exports-Objekt, um zyklische require-Aufrufe zu handhaben, bevor das Modul vollständig geladen ist.

Sobald das CommonJS-Modul erfolgreich ausgewertet wurde, wird ein Synthetic Module Record erstellt, wobei der default-ES-Modul-Export auf module.exports gesetzt wird und die Schlüssel des module.exports-Objekts als benannte Exporte erneut exportiert werden (wenn das module.exports-Objekt ein Objekt ist).

Bei Verwendung von Buns Bundler funktioniert dies anders. Der Bundler wickelt das CommonJS-Modul in eine require_${moduleName}-Funktion, die das module.exports-Objekt zurückgibt.


import.meta

Das import.meta-Objekt ist eine Möglichkeit für ein Modul, auf Informationen über sich selbst zuzugreifen. Es ist Teil der JavaScript-Sprache, aber sein Inhalt ist nicht standardisiert. Jeder "Host" (Browser, Laufzeit usw.) kann beliebige Eigenschaften auf dem import.meta-Objekt implementieren.

Bun implementiert die folgenden Eigenschaften.

ts
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`, wenn diese Datei direkt von `bun run` ausgeführt wird
// `false` andernfalls

import.meta.resolve("zod"); // => "file:///path/to/project/node_modules/zod/index.js"
EigenschaftBeschreibung
import.meta.dirAbsoluter Pfad zum Verzeichnis, das die aktuelle Datei enthält, z.B. /path/to/project. Entspricht __dirname in CommonJS-Modulen (und Node.js)
import.meta.dirnameEin Alias für import.meta.dir, für Node.js-Kompatibilität
import.meta.envEin Alias für process.env.
import.meta.fileDer Name der aktuellen Datei, z.B. index.tsx
import.meta.pathAbsoluter Pfad zur aktuellen Datei, z.B. /path/to/project/index.ts. Entspricht __filename in CommonJS-Modulen (und Node.js)
import.meta.filenameEin Alias für import.meta.path, für Node.js-Kompatibilität
import.meta.mainGibt an, ob die aktuelle Datei der Einstiegspunkt für den aktuellen bun-Prozess ist. Wird die Datei direkt von bun run ausgeführt oder wird sie importiert?
import.meta.resolveLöst einen Modulspezifizierer (z.B. "zod" oder "./file.tsx") zu einer URL auf. Entspricht import.meta.resolve in Browsern. Beispiel: import.meta.resolve("zod") gibt "file:///path/to/project/node_modules/zod/index.ts" zurück
import.meta.urlEine string-URL zur aktuellen Datei, z.B. file:///path/to/project/index.ts. Entspricht import.meta.url in Browsern

Bun von www.bunjs.com.cn bearbeitet