Makros sind ein Mechanismus zum Ausführen von JavaScript-Funktionen zur Bundle-Zeit. Die von diesen Funktionen zurückgegebenen Werte werden direkt in Ihr Bundle inline eingefügt.
Als einfaches Beispiel betrachten wir diese einfache Funktion, die eine Zufallszahl zurückgibt.
export function random() {
return Math.random();
}Dies ist nur eine normale Funktion in einer normalen Datei, aber wir können sie wie folgt als Makro verwenden:
import { random } from "./random.ts" with { type: "macro" };
console.log(`Ihre Zufallszahl ist ${random()}`);NOTE
Makros werden mit der Importattribut-Syntax angegeben. Falls Sie diese Syntax noch nicht kennen, es handelt sich um einen Stage-3-TC39-Vorschlag, der es ermöglicht, Importanweisungen zusätzliche Metadaten hinzuzufügen.Jetzt bündeln wir diese Datei mit bun build. Die gebündelte Datei wird nach stdout ausgegeben.
bun build ./cli.tsxconsole.log(`Ihre Zufallszahl ist ${0.6805550949689833}`);Wie Sie sehen können, kommt der Quellcode der random-Funktion im Bundle nirgendwo vor. Stattdessen wird sie während des Bündelns ausgeführt und der Funktionsaufruf (random()) wird durch das Ergebnis der Funktion ersetzt. Da der Quellcode niemals im Bundle enthalten sein wird, können Makros sicher privilegierte Operationen wie das Lesen aus einer Datenbank durchführen.
Wann Makros verwendet werden sollten
Wenn Sie mehrere Build-Skripte für kleine Dinge haben, für die Sie ansonsten ein einmaliges Build-Skript hätten, kann die Code-Ausführung zur Bundle-Zeit einfacher zu warten sein. Sie lebt mit dem Rest Ihres Codes, läuft mit dem Rest des Builds, wird automatisch parallelisiert, und wenn sie fehlschlägt, schlägt auch der Build fehl.
Wenn Sie jedoch feststellen, dass Sie viel Code zur Bundle-Zeit ausführen, sollten Sie stattdessen die Ausführung eines Servers in Betracht ziehen.
Importattribute
Bun-Makros sind Importanweisungen, die mit einem der folgenden annotiert sind:
with { type: 'macro' }— ein Importattribut, ein Stage-3-ECMAScript-Vorschlagassert { type: 'macro' }— eine Importbehauptung, eine frühere Version von Importattributen, die mittlerweile aufgegeben wurde (aber bereits von einer Reihe von Browsern und Runtimes unterstützt wird)
Sicherheitsüberlegungen
Makros müssen explizit mit { type: "macro" } importiert werden, um zur Bundle-Zeit ausgeführt zu werden. Diese Importe haben keine Wirkung, wenn sie nicht aufgerufen werden, im Gegensatz zu regulären JavaScript-Importen, die Nebenwirkungen haben können.
Sie können Makros vollständig deaktivieren, indem Sie die --no-macros-Flag an Bun übergeben. Dies erzeugt einen Build-Fehler wie diesen:
error: Macros are disabled
foo();
^
./hello.js:3:1 53Um die potenzielle Angriffsfläche für bösartige Pakete zu reduzieren, können Makros nicht von innerhalb node_modules/**/* aufgerufen werden. Wenn ein Paket versucht, ein Makro aufzurufen, sehen Sie einen Fehler wie diesen:
error: For security reasons, macros cannot be run from node_modules.
beEvil();
^
node_modules/evil/index.js:3:1 50Ihr Anwendungscode kann weiterhin Makros aus node_modules importieren und aufrufen.
import { macro } from "some-package" with { type: "macro" };
macro();Exportbedingung "macro"
Wenn Sie eine Bibliothek, die ein Makro enthält, an npm oder ein anderes Paketregister ausliefern, verwenden Sie die "macro"-Exportbedingung, um eine spezielle Version Ihres Pakets ausschließlich für die Makro-Umgebung bereitzustellen.
{
"name": "my-package",
"exports": {
"import": "./index.js",
"require": "./index.js",
"default": "./index.js",
"macro": "./index.macro.js"
}
}Mit dieser Konfiguration können Benutzer Ihr Paket zur Runtime oder zur Bundle-Zeit mit demselben Import-Specifier verwenden:
import pkg from "my-package"; // Runtime-Import
import { macro } from "my-package" with { type: "macro" }; // Makro-ImportDer erste Import wird zu ./node_modules/my-package/index.js aufgelöst, während der zweite von Bun's Bundler zu ./node_modules/my-package/index.macro.js aufgelöst wird.
Ausführung
Wenn Bun's Transpiler einen Makro-Import sieht, ruft er die Funktion innerhalb des Transpilers mit Bun's JavaScript-Runtime auf und konvertiert den Rückgabewert von JavaScript in einen AST-Knoten. Diese JavaScript-Funktionen werden zur Bundle-Zeit aufgerufen, nicht zur Runtime.
Makros werden synchron im Transpiler während der Besuchsphase ausgeführt – vor Plugins und bevor der Transpiler den AST generiert. Sie werden in der Reihenfolge ausgeführt, in der sie importiert werden. Der Transpiler wartet, bis das Makro fertig ausgeführt ist, bevor er fortfährt. Der Transpiler wartet auch auf jedes Promise, das von einem Makro zurückgegeben wird.
Bun's Bundler ist multithreaded. Daher werden Makros parallel innerhalb mehrerer gespawnter JavaScript-"Worker" ausgeführt.
Dead-Code-Eliminierung
Der Bundler führt eine Dead-Code-Eliminierung nach dem Ausführen und Inline-Einfügen von Makros durch. Bei gegebenem folgendem Makro:
export function returnFalse() {
return false;
}...erzeugt das Bündeln der folgenden Datei ein leeres Bundle, vorausgesetzt, die Option zur Syntax-Minifizierung ist aktiviert.
import { returnFalse } from "./returnFalse.ts" with { type: "macro" };
if (returnFalse()) {
console.log("Dieser Code wird eliminiert");
}Serialisierbarkeit
Bun's Transpiler muss in der Lage sein, das Ergebnis des Makros zu serialisieren, damit es in den AST inline eingefügt werden kann. Alle JSON-kompatiblen Datenstrukturen werden unterstützt:
export function getObject() {
return {
foo: "bar",
baz: 123,
array: [1, 2, { nested: "value" }],
};
}Makros können async sein oder Promise-Instanzen zurückgeben. Bun's Transpiler wartet automatisch auf das Promise und fügt das Ergebnis inline ein.
export async function getText() {
return "async value";
}Der Transpiler implementiert spezielle Logik zum Serialisieren gängiger Datenformate wie Response, Blob, TypedArray.
- TypedArray: Wird zu einem base64-kodierten String aufgelöst.
- Response: Bun liest den
Content-Typeund serialisiert entsprechend; zum Beispiel wird eine Response mit dem Typapplication/jsonautomatisch in ein Objekt geparst undtext/plainwird als String inline eingefügt. Responses mit einem nicht erkannten oder undefinierten Typ werden base64-kodiert. - Blob: Wie bei Response hängt die Serialisierung von der
type-Eigenschaft ab.
Das Ergebnis von fetch ist Promise<Response>, sodass es direkt zurückgegeben werden kann.
export function getObject() {
return fetch("https://bun.com");
}Funktionen und Instanzen der meisten Klassen (außer den oben genannten) sind nicht serialisierbar.
export function getText(url: string) {
// das funktioniert nicht!
return () => {};
}Argumente
Makros können Eingaben akzeptieren, aber nur in begrenzten Fällen. Der Wert muss statisch bekannt sein. Zum Beispiel ist das Folgende nicht erlaubt:
import { getText } from "./getText.ts" with { type: "macro" };
export function howLong() {
// der Wert von `foo` kann nicht statisch bekannt sein
const foo = Math.random() ? "foo" : "bar";
const text = getText(`https://example.com/${foo}`);
console.log("Die Seite ist ", text.length, " Zeichen lang");
}Wenn der Wert von foo jedoch zur Bundle-Zeit bekannt ist (wenn es sich beispielsweise um eine Konstante oder das Ergebnis eines anderen Makros handelt), dann ist es erlaubt:
import { getText } from "./getText.ts" with { type: "macro" };
import { getFoo } from "./getFoo.ts" with { type: "macro" };
export function howLong() {
// das funktioniert, weil getFoo() statisch bekannt ist
const foo = getFoo();
const text = getText(`https://example.com/${foo}`);
console.log("Die Seite ist", text.length, "Zeichen lang");
}Dies gibt Folgendes aus:
function howLong() {
console.log("Die Seite ist", 1322, "Zeichen lang");
}
export { howLong };Beispiele
Neuesten Git-Commit-Hash einbetten
export function getGitCommitHash() {
const { stdout } = Bun.spawnSync({
cmd: ["git", "rev-parse", "HEAD"],
stdout: "pipe",
});
return stdout.toString();
}Wenn wir es bauen, wird getGitCommitHash durch das Ergebnis des Funktionsaufrufs ersetzt:
import { getGitCommitHash } from "./getGitCommitHash.ts" with { type: "macro" };
console.log(`Der aktuelle Git-Commit-Hash ist ${getGitCommitHash()}`);console.log(`Der aktuelle Git-Commit-Hash ist 3ee3259104e4507cf62c160f0ff5357ec4c7a7f8`);fetch()-Anfragen zur Bundle-Zeit stellen
In diesem Beispiel stellen wir eine ausgehende HTTP-Anfrage mit fetch(), parsen die HTML-Antwort mit HTMLRewriter und geben ein Objekt zurück, das den Titel und die Meta-Tags enthält – alles zur Bundle-Zeit.
export async function extractMetaTags(url: string) {
const response = await fetch(url);
const meta = {
title: "",
};
new HTMLRewriter()
.on("title", {
text(element) {
meta.title += element.text;
},
})
.on("meta", {
element(element) {
const name =
element.getAttribute("name") || element.getAttribute("property") || element.getAttribute("itemprop");
if (name) meta[name] = element.getAttribute("content");
},
})
.transform(response);
return meta;
}Die extractMetaTags-Funktion wird zur Bundle-Zeit gelöscht und durch das Ergebnis des Funktionsaufrufs ersetzt. Dies bedeutet, dass die fetch-Anfrage zur Bundle-Zeit stattfindet und das Ergebnis im Bundle eingebettet ist. Außerdem wird der Zweig, der den Fehler wirft, eliminiert, da er nicht erreichbar ist.
import { extractMetaTags } from "./meta.ts" with { type: "macro" };
export const Head = () => {
const headTags = extractMetaTags("https://example.com");
if (headTags.title !== "Example Domain") {
throw new Error("Erwarteter Titel ist 'Example Domain'");
}
return (
<head>
<title>{headTags.title}</title>
<meta name="viewport" content={headTags.viewport} />
</head>
);
};export const Head = () => {
const headTags = {
title: "Example Domain",
viewport: "width=device-width, initial-scale=1",
};
return (
<head>
<title>{headTags.title}</title>
<meta name="viewport" content={headTags.viewport} />
</head>
);
};