===================== 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: .. mermaid:: 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
(code + hash)"] style B fill:#fef3c7,stroke:#d97706 style H fill:#d1fae5,stroke:#059669 esbuild configuration ===================== The bundler invokes esbuild with these flags: .. list-table:: :widths: 30 70 :header-rows: 1 * - 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__``): .. mermaid:: 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): .. code-block:: tsx // 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_modules** — ``src/wilcojs/react/node_modules/.bin/esbuild`` (development, when the monorepo is available) 2. **Global PATH** — ``esbuild`` on the system PATH 3. **Common npm paths** — ``/opt/homebrew/bin/esbuild``, ``/usr/local/bin/esbuild``, etc. 4. **npx fallback** — ``npx --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: .. code-block:: text 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: .. code-block:: javascript //# 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: .. mermaid:: graph TD A[ComponentRegistry] -->|"discover all"| B[Component list] B --> C{For each component} C --> D[bundle_component] D --> E["sanitize name
(: → --)"] 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: .. code-block:: text 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: .. code-block:: json { "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 ---------------------- .. code-block:: text 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``: .. mermaid:: 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 ======== - :doc:`architecture` — high-level system overview - :doc:`request-lifecycle` — how bundles are loaded and transformed at runtime - :doc:`/reference/cli` — ``wilco build`` command reference - :doc:`/reference/components` — component structure and naming