System Architecture

This document explains the high-level architecture of wilco: how Python backends serve React components, how framework bridges work, and how the system operates in development and production modes.

Overview

Wilco bridges the gap between Python backends and React frontends. Instead of building a separate SPA, you define components alongside your Python code, and wilco handles bundling, serving, and rendering them.

        graph LR
    subgraph Python Backend
        A[Component Registry] --> B[Bridge Handlers]
        B --> C[esbuild Bundler]
        B --> D[Manifest Reader]
    end

    subgraph Browser
        E[loader.js] --> F[ESM Transform]
        F --> G[React Render]
    end

    B -- "JS bundle" --> E
    D -- "Static files" --> E

    style A fill:#e0e7ff,stroke:#4f46e5
    style E fill:#fef3c7,stroke:#d97706
    

The system has three layers:

  1. Python layer — discovers components, bundles them with esbuild, serves them via framework-specific bridges

  2. Transport layer — HTTP API endpoints (development) or static files (production)

  3. Browser layerloader.js fetches bundles, transforms ESM imports, renders React components into the DOM

Component system

Components are directories containing a TypeScript entry point:

components/
├── counter/
│   ├── index.tsx        ← Required: default export
│   └── schema.json      ← Optional: metadata + props schema
├── ui/
│   └── button/
│       ├── index.tsx
│       ├── Button.tsx    ← Internal files bundled together
│       └── styles.ts
└── store/
    └── product/
        ├── index.tsx
        └── schema.json

Discovery and naming

The ComponentRegistry scans directories for index.tsx (or index.ts) files. Component names are derived from the filesystem path:

        graph TD
    A["components/ui/button/index.tsx"] --> B["ui.button"]
    C["components/counter/index.tsx"] --> D["counter"]
    E["myapp/components/product/index.tsx"] --> F["myapp:product"]

    style B fill:#d1fae5,stroke:#059669
    style D fill:#d1fae5,stroke:#059669
    style F fill:#d1fae5,stroke:#059669
    
  • Directory separators become dots: ui/buttonui.button

  • Source prefixes become colons: prefix myapp + productmyapp:product

  • Django auto-discovery uses the app label as prefix

Multiple sources can be registered with different prefixes:

registry = ComponentRegistry()
registry.add_source(Path("./shared"), prefix="")        # → "button"
registry.add_source(Path("./store/components"), prefix="store")  # → "store:product"

Bridge pattern

All framework integrations share a common BridgeHandlers class that provides the core logic. Framework-specific bridges are thin wrappers:

        graph TD
    A[BridgeHandlers] --> B[FastAPI Router]
    A --> C[Django Views]
    A --> D[Flask Blueprint]
    A --> E[Starlette Routes]
    A --> F[BundleCache]
    A --> G[Manifest Reader]

    style A fill:#e0e7ff,stroke:#4f46e5
    style F fill:#fef3c7,stroke:#d97706
    style G fill:#fef3c7,stroke:#d97706
    

BridgeHandlers provides three operations:

  • list_bundles() — returns available component names

  • get_bundle(name) — returns bundled JavaScript (from manifest or live bundling)

  • get_metadata(name) — returns component schema and content hash

These map to three HTTP endpoints exposed by every bridge:

Endpoint

Handler method

GET /api/bundles

list_bundles()

GET /api/bundles/{name}.js

get_bundle(name)

GET /api/bundles/{name}/metadata

get_metadata(name)

Deployment modes

Wilco operates in two modes, depending on whether pre-built bundles are available.

        graph TB
    subgraph "Development Mode (API)"
        A1[Browser] -->|"fetch /api/bundles/name.js"| B1[Python Bridge]
        B1 -->|"bundle on-the-fly"| C1[esbuild]
        C1 -->|"ESM + source maps"| B1
        B1 -->|"JS response"| A1
    end

    subgraph "Production Mode (Static)"
        A2[Browser] -->|"fetch /static/wilco/bundles/name.hash.js"| B2[Static Server]
        B2 -->|"pre-built JS"| A2
    end

    style C1 fill:#fef3c7,stroke:#d97706
    style B2 fill:#d1fae5,stroke:#059669
    

Development mode

Components are bundled on-the-fly when requested:

  1. Browser requests /api/bundles/counter.js

  2. Bridge looks up counter in the registry

  3. Checks the BundleCache (mtime-based invalidation)

  4. If cache miss: runs esbuild to bundle the component

  5. Returns JavaScript with Cache-Control: immutable headers

  6. Browser uses the hash query parameter for cache busting

The mtime-based cache means editing a source file instantly invalidates the cache on the next request, without restarting the server.

Production mode

Components are pre-compiled with wilco build:

  1. All components are bundled and written to bundles/{name}.{hash}.js

  2. A manifest.json maps names to files and hashes

  3. Static file server (WhiteNoise, nginx, CDN) serves the bundles

  4. The loader reads the manifest and fetches bundles from static URLs

  5. The API endpoint returns 404 (static_mode = True)

The content hash in filenames makes immutable caching safe: when a component changes, the build produces a new filename.

Module registry

Components are bundled with React, ReactDOM, goober, and @wilcojs/react marked as external dependencies. These are provided at runtime via a global module registry:

window.__MODULES__ = {
    "react": React,
    "react/jsx-runtime": ReactJsxRuntime,
    "react-dom/client": ReactDOMClient,
    "@wilcojs/react": { useComponent },
    "goober": goober,
};

This means:

  • All components share a single React instance — no version conflicts, smaller bundles, consistent behavior

  • Components can import from the registry using standard import syntax

  • Arbitrary npm packages are not available — only the modules in the registry can be imported. If a component needs a library, it must be bundled with the component (not marked as external)

The ESM transformation (transformEsmToRuntime) rewrites imports at load time:

// Original (from esbuild)
import { useState } from "react";

// Transformed (by loader.js)
const { useState } = window.__MODULES__["react"];

See Request Lifecycle for the full transformation details.

Caching strategy

Wilco uses a multi-layer caching strategy:

Layer

Mechanism

Behavior

Python (dev)

BundleCache with mtime

Invalidates when source file is modified. No restart needed.

Python (prod)

Manifest with in-memory cache

Reads bundle file once, caches forever (files are immutable).

HTTP

Cache-Control: immutable

Browser caches for 1 year. Hash-based URLs bust the cache.

Browser

Promise deduplication

Multiple containers requesting the same component share one fetch.

The promise-based deduplication is important for pages with multiple instances of the same component: the loader caches the Promise itself, so concurrent requests don’t trigger duplicate network calls.

See also