esbuild, Teil 2: JavaScript-Bundling praktisch umsetzen

Seite 3: Plug-in 1: esbuild-Sass-Plug-in

Inhaltsverzeichnis

Das erste Plug-in beschäftigt sich mit der Integration von Sass-CSS-Dateien via import-Statements.

So wie andere Bundler lässt sich auch esbuild mit verschiedenen Loadern nutzen. Sie sind in der Dokumentation allerdings unter der missverständlichen Rubrik "Content Types" zu finden. Seit Kurzem kann esbuild auch mit einem CSS-Loader umgehen. Das ist in diesem Beispiel von Vorteil, denn damit ist von Haus aus der Import von CSS-Dateien in JavaScript-Modulen verfügbar: import 'example.css'. Im Falle des Imports von SCSS-Dateien müssen diese nur den Sass-CSS-Transpiler durchlaufen und dessen Ausgabe esbuild für das weitere Bundling bereitgestellt werden.

Zuvor muss das Sass-Plug-in esbuild mitteilen, dass es für SCSS-Dateien zuständig ist. Das lässt sich durch das Registrieren eines onResolve-Callbacks bei esbuild in der setup-Funktion des Plug-ins erreichen:

setup(build) {
  build.onResolve({ filter: /\.scss$/ }, ({ resolveDir, path }) => ({
    path: resolve(resolveDir, path),
    namespace: name,
  }));
...

Der Regex-Wert des filter-Parameters teilt esbuild mit, dass Interesse an allen SCSS-Dateien besteht. Über den path- und namespace-Parameter, den der Callback zurückgibt, wird esbuild angewiesen, diese Dateien exklusiv im Namespace des Plug-ins zu behandeln.

Im nächsten Schritt wird der onLoad-Callback registriert, den esbuild für alle Ressourcen, deren path-Argument die filter- und namespace-Parameter abgleicht, aufrufen wird. Der onLoad-Callback ist seinem Namen gemäß für das Laden des Contents einer Ressource zuständig. Er ruft lediglich den SCSS-Compiler auf und gibt das CSS-Kompilat an esbuild zurück.

build.onLoad({ filter: /.*/, namespace: name }, ({ path }) => {
  const {
        css,
        stats: {
            includedFiles
        }
    } = sass.renderSync({
      file: path,
      outFile: options[OUTPUT_ARG].replace(/\.js$/, ".css"),
      outputStyle: options.debug ? "expanded" : "compressed",
      sourceMap: options.debug,
      sourceComments: options.debug,
      sourceMapContents: options.debug,
      sourceMapEmbed: options.debug,
    });

    return {
      contents: css.toString('utf-8'),
      watchFiles: includedFiles, 
      loader: "css",
    };
  });
},

Dazu erhält der Sass-Compiler die SCSS-Quelldatei des ursprünglichen import-Statements mit vollem Pfad, den der onResolve-Callback bereitgestellt hat. In der Datei sind die Optionen zur Steuerung der sourceMap-Erzeugung enthalten – abhängig von dem --debug-Schalter des Bundlers.

Sass teilt mit, welche weiteren (S)CSS-Dateien via CSS-@import-Anweisung in der gegebenen SCSS-Datei zu inkludieren sind. Diese erhält esbuild neben dem erzeugten CSS-Content zurück. Das ermöglicht auch die watch-Funktionalität von esbuild für inkrementelles Bundlen.

Der Sass-Compiler schreibt den CSS-Output nicht automatisch in die Datei, die als Parameter outFile übergeben wurde – das ist in diesem Falle ohnehin nicht gewünscht. Der Parameter dient nur dazu, sinnvolle SourceMap-Referenzen zu erzeugen. Die CSS-Ausgabe aller während des esbuild-Calls aufgesammelten CSS-Ausgaben soll gebündelt in die Ausgabe-CSS-Datei gelangen. Dazu ist lediglich das generierte CSS dem esbuild-css-Loader zu übergeben, der diesen Job erledigen kann:

return {
  contents: result.css.toString(),
  loader: "css",
};

Die Übergabe an den esbuild-css-Loader hat einen nützlichen Nebeneffekt: Er entfernt das import-Statement der SCSS-Datei von esbuild aus dem geparsten JavaScript.

Übrigens dürfen esbuild-Callbacks auch async deklariert sein, was ihre parallele Ausführung ermöglicht. Zugunsten der Übersichtlichkeit kam das Feature in diesem Artikel nicht zum Einsatz.

Damit ist das erste esbuild-Plug-in fertig. Aus 34 Zeilen JavaScript-Code ist ein konfigurierbares esbuild-Plug-in zur Integration von Sass-CSS-Dateien entstanden, das in weiteren Projekten zum Einsatz kommen kann. Es bietet zudem mehr Funktionen als die beiden derzeit bei esbuild gelisteten Sass-Plug-ins.