esbuild Plugin
The esbuild plugin provides static CSS extraction for projects that use esbuild as their bundler. It uses esbuild’s onResolve and onLoad hooks to intercept source files, pass them through the native Rust compiler, and serve the extracted CSS through a virtual module.
Installation
Install the esbuild plugin alongside the core compiler package:
# bunbun add typewritingclassbun add -d typewritingclass-esbuild typewritingclass-compiler
# pnpmpnpm add typewritingclasspnpm add -D typewritingclass-esbuild typewritingclass-compiler
# npmnpm install typewritingclassnpm install -D typewritingclass-esbuild typewritingclass-compilertypewritingclass-esbuild depends on typewritingclass-compiler as a peer dependency. The compiler package contains the native Rust binary that performs the actual AST analysis and CSS extraction.
Configuration
Add the plugin to your esbuild build script:
import * as esbuild from 'esbuild'import twcEsbuildPlugin from 'typewritingclass-esbuild'
await esbuild.build({ entryPoints: ['src/main.tsx'], bundle: true, outdir: 'dist', plugins: [twcEsbuildPlugin()],})Options
The plugin accepts the same strict option as the Vite plugin:
twcEsbuildPlugin({ strict: true, // default: true})strict
- Type:
boolean - Default:
true
When enabled, the compiler reports an error if a cx() argument cannot be statically evaluated at build time, unless wrapped in dynamic(). See Strict Mode for details.
// Default: strict mode ontwcEsbuildPlugin()
// Explicitly enabletwcEsbuildPlugin({ strict: true })
// Disable strict modetwcEsbuildPlugin({ strict: false })Virtual CSS module
Like the Vite plugin, the esbuild plugin uses a virtual module to serve the extracted CSS. Import it in your entry file:
// src/main.ts or src/main.tsximport 'virtual:twc.css'The plugin intercepts this import using onResolve and resolves it to a custom namespace. The onLoad hook then generates the combined CSS from all extracted rules and returns it with loader: 'css', so esbuild treats it as a CSS file and includes it in the output bundle.
How it works
The esbuild plugin operates through two hook types:
onResolve
The plugin registers an onResolve hook that matches the virtual:twc.css import path. When esbuild encounters this import, the hook redirects it to a custom namespace (twc-virtual-css) instead of looking for a file on disk.
onLoad
Two onLoad hooks are registered:
-
Virtual CSS loader: Matches the
twc-virtual-cssnamespace. When triggered, it collects all CSS rules extracted from source files and returns them as a CSS string. -
Source file transformer: Matches
.ts,.tsx,.js, and.jsxfiles. For each file that containstypewritingclassimports:- Reads the file contents.
- Passes the source to the native Rust compiler.
- Stores the extracted CSS rules.
- Returns the transformed JavaScript with the
virtual:twc.cssimport prepended. - Surfaces any diagnostics as esbuild warnings or errors.
File filtering
The source file transformer only processes files that:
- Have a
.ts,.tsx,.js, or.jsxextension. - Are not inside
node_modules. - Contain the string
typewritingclassin their source code.
The correct esbuild loader (ts, tsx, js, or jsx) is automatically selected based on the file extension.
Build script examples
Basic build
import * as esbuild from 'esbuild'import twcEsbuildPlugin from 'typewritingclass-esbuild'
await esbuild.build({ entryPoints: ['src/main.tsx'], bundle: true, outdir: 'dist', format: 'esm', plugins: [twcEsbuildPlugin()],})With React JSX transform
import * as esbuild from 'esbuild'import twcEsbuildPlugin from 'typewritingclass-esbuild'
await esbuild.build({ entryPoints: ['src/main.tsx'], bundle: true, outdir: 'dist', format: 'esm', jsx: 'automatic', plugins: [twcEsbuildPlugin()],})Watch mode
import * as esbuild from 'esbuild'import twcEsbuildPlugin from 'typewritingclass-esbuild'
const ctx = await esbuild.context({ entryPoints: ['src/main.tsx'], bundle: true, outdir: 'dist', plugins: [twcEsbuildPlugin()],})
await ctx.watch()console.log('Watching for changes...')Serve mode
import * as esbuild from 'esbuild'import twcEsbuildPlugin from 'typewritingclass-esbuild'
const ctx = await esbuild.context({ entryPoints: ['src/main.tsx'], bundle: true, outdir: 'dist', plugins: [twcEsbuildPlugin()],})
const { host, port } = await ctx.serve({ servedir: 'dist',})
console.log(`Serving at http://${host}:${port}`)Diagnostics
The plugin surfaces compiler diagnostics through esbuild’s native warning and error system:
- Errors cause the build to fail and are displayed with file path, line, and column information.
- Warnings are displayed in the terminal but do not fail the build.
✘ [ERROR] Cannot statically evaluate argument to cx() — wrap dynamic values in dynamic()
src/App.tsx:12:15: 12 │ const style = cx(p(spacing), bg(color)) ╵ ~~~~~~~Fallback behavior
If the Rust compiler fails to transform a file, the plugin falls back gracefully:
- The original source code is preserved.
- A runtime import (
import 'typewritingclass/inject') is prepended so styles work at runtime. - No CSS is extracted for that file, but the application still functions.
Comparison with the Vite plugin
| Feature | Vite Plugin | esbuild Plugin |
|---|---|---|
| Package | typewritingclass-compiler | typewritingclass-esbuild |
| HMR support | Yes (full) | Via esbuild watch/serve |
| Virtual CSS module | virtual:twc.css | virtual:twc.css |
| Strict mode | Yes | Yes |
| Source maps | Yes (MagicString) | Handled by esbuild |
| Diagnostics | Vite error overlay | esbuild terminal output |
Both plugins use the same native Rust compiler binary from typewritingclass-compiler and produce identical CSS output.