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:
Python layer — discovers components, bundles them with esbuild, serves them via framework-specific bridges
Transport layer — HTTP API endpoints (development) or static files (production)
Browser layer —
loader.jsfetches 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/button→ui.buttonSource prefixes become colons: prefix
myapp+product→myapp:productDjango 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 namesget_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 |
|---|---|
|
|
|
|
|
|
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:
Browser requests
/api/bundles/counter.jsBridge looks up
counterin the registryChecks the
BundleCache(mtime-based invalidation)If cache miss: runs esbuild to bundle the component
Returns JavaScript with
Cache-Control: immutableheadersBrowser 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:
All components are bundled and written to
bundles/{name}.{hash}.jsA
manifest.jsonmaps names to files and hashesStatic file server (WhiteNoise, nginx, CDN) serves the bundles
The loader reads the manifest and fetches bundles from static URLs
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
importsyntaxArbitrary 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) |
|
Invalidates when source file is modified. No restart needed. |
Python (prod) |
|
Reads bundle file once, caches forever (files are immutable). |
HTTP |
|
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¶
Request Lifecycle — step-by-step request flow with sequence diagrams
Bundling and esbuild — esbuild configuration and source map handling
Live Preview System — live preview architecture for admin forms
CLI Reference — CLI reference for
wilco serveandwilco buildHTTP Caching — HTTP caching specification