Bundling and esbuild

This document explains how wilco bundles TypeScript components into JavaScript that can run in the browser. It covers esbuild configuration, the external dependency model, source map handling, and the pre-compilation pipeline.

How bundling works

When a component is requested, wilco’s Python bundler runs esbuild to transform the TypeScript source into a single JavaScript file:

        graph LR
    A["index.tsx"] --> B[esbuild]
    C["Button.tsx"] --> B
    D["styles.ts"] --> B

    B --> E["ESM bundle"]
    E --> F["Source map rewrite"]
    F --> G["SHA-256 hash"]
    G --> H["BundleResult<br/>(code + hash)"]

    style B fill:#fef3c7,stroke:#d97706
    style H fill:#d1fae5,stroke:#059669
    

esbuild configuration

The bundler invokes esbuild with these flags:

Flag

Purpose

--bundle

Inline all local imports (files within the component directory)

--format=esm

Output ES modules (transformed to runtime registry by loader.js)

--target=es2020

Browser compatibility target

--jsx=automatic

React 17+ JSX transform (no manual import React needed)

--external:react

Don’t bundle React (provided by loader.js at runtime)

--external:react-dom

Don’t bundle ReactDOM

--external:react/jsx-runtime

Don’t bundle JSX runtime

--external:@wilcojs/react

Don’t bundle wilco’s React hooks

--external:goober

Don’t bundle CSS-in-JS library

--sourcemap=inline

Include source maps in the bundle (development)

--sources-content=true

Embed original source in source maps

For production builds (wilco build), additional flags:

  • --minify — reduce bundle size (can be disabled with --no-minify)

  • --sourcemap is off by default (enable with --sourcemap flag)

External dependencies

External dependencies are not bundled with the component. Instead, they’re provided at runtime via the module registry (window.__MODULES__):

        graph TD
    subgraph "Bundled by esbuild"
        A[index.tsx]
        B[Button.tsx]
        C[styles.ts]
    end

    subgraph "Provided at runtime"
        D[react]
        E[react-dom]
        F["@wilcojs/react"]
        G[goober]
    end

    A --> B
    A --> C
    A -.->|"import"| D
    A -.->|"import"| F

    style D fill:#e0e7ff,stroke:#4f46e5
    style E fill:#e0e7ff,stroke:#4f46e5
    style F fill:#e0e7ff,stroke:#4f46e5
    style G fill:#e0e7ff,stroke:#4f46e5
    

Why externals?

  • Single React instance — all components share one React, avoiding hooks and context issues that arise from multiple React copies

  • Smaller bundles — React alone is ~130KB minified. Without externals, every component would include its own copy

  • Consistent versions — the loader controls which React version all components use

Limitation: components can only import modules registered in window.__MODULES__. Arbitrary npm packages cannot be used unless they are local files bundled with the component.

To use a third-party library in a component, install it locally and import it from a relative path (esbuild will bundle it):

// This works: local file, bundled by esbuild
import { formatDate } from "./utils";

// This works: registered external
import { useState } from "react";

// This does NOT work: not in module registry
import dayjs from "dayjs";

esbuild resolution order

The bundler searches for esbuild in this order:

  1. Frontend node_modulessrc/wilcojs/react/node_modules/.bin/esbuild (development, when the monorepo is available)

  2. Global PATHesbuild on the system PATH

  3. Common npm paths/opt/homebrew/bin/esbuild, /usr/local/bin/esbuild, etc.

  4. npx fallbacknpx --yes esbuild (downloads esbuild on demand)

If none are found, a BundlerNotFoundError is raised with installation instructions.

Source map handling

Source maps enable debugging original TypeScript in browser DevTools.

Rewriting source URLs

esbuild generates source maps with filesystem paths as sources. The bundler rewrites these to a custom URL scheme:

Before: "../components/store/product/Button.tsx"
After:  "component://store:product/Button.tsx"

This provides:

  • Clean source names in DevTools (no filesystem paths leaked)

  • Component identification in stack traces

  • Consistent naming regardless of the server’s file layout

The transformation:

  1. Extract the inline source map (base64-encoded JSON after //# sourceMappingURL=)

  2. Decode and parse the JSON

  3. Rewrite each sources entry using the component name

  4. Re-encode and replace the original source map comment

A //# sourceURL comment is also added for DevTools identification:

//# sourceURL=components://bundles/store:product.js

Content hashing

After bundling and source map rewriting, the bundler computes a SHA-256 hash of the final JavaScript code (first 12 hex characters). This hash is used for:

  • Cache busting — appended as a query parameter in API mode

  • Immutable filenames — part of the filename in static mode (e.g., counter.a1b2c3d4e5f6.js)

Pre-compilation pipeline

The wilco build command pre-compiles all components for production:

        graph TD
    A[ComponentRegistry] -->|"discover all"| B[Component list]
    B --> C{For each component}
    C --> D[bundle_component]
    D --> E["sanitize name<br/>(: → --)"]
    E --> F["Write bundles/{name}.{hash}.js"]
    C --> G[manifest.json]

    style A fill:#e0e7ff,stroke:#4f46e5
    style G fill:#d1fae5,stroke:#059669
    

Filename sanitization

Component names can contain colons (e.g., store:product), which are not valid in filenames on all platforms. The build process replaces colons with double dashes:

store:product  →  store--product.a1b2c3d4.js
ui.button      →  ui.button.e5f6g7h8.js

Manifest format

The manifest.json file maps component names to their output files:

{
  "store:product": {
    "file": "bundles/store--product.a1b2c3d4.js",
    "hash": "a1b2c3d4"
  },
  "counter": {
    "file": "bundles/counter.e5f6g7h8.js",
    "hash": "e5f6g7h8"
  }
}

The file path is relative to the manifest’s directory. The hash is the same SHA-256 prefix used for cache busting.

Build output structure

dist/wilco/
├── manifest.json
└── bundles/
    ├── counter.a1b2c3d4.js
    ├── store--product.e5f6g7h8.js
    └── ui.button.i9j0k1l2.js

Django integration

For Django, the WilcoBundleFinder integrates with collectstatic:

        graph LR
    A["wilco_build"] -->|"generates"| B["dist/wilco/"]
    B -->|"WilcoBundleFinder"| C["collectstatic"]
    C -->|"copies to"| D["STATIC_ROOT/wilco/"]
    D -->|"served by"| E["WhiteNoise / nginx"]

    style A fill:#e0e7ff,stroke:#4f46e5
    style E fill:#d1fae5,stroke:#059669
    

See also