JavaScript Architecture

Overview

Wilco’s JavaScript layer enables Python backends to serve React components without requiring a full frontend build pipeline. The architecture consists of three main parts:

  1. Standalone Loader (loader.js) - Self-contained runtime that renders components on any HTML page

  2. Live Loader (live-loader.js) - Extension for Django admin live preview

  3. Component Bundles - ESM bundles created by esbuild at runtime

All JavaScript assets are pre-built and included in the Python wheel, so users don’t need Node.js installed to use wilco. However, esbuild is required for runtime component bundling.

File structure

src/
├── wilco/bridges/django/static/wilco/
│   ├── loader.js        # Pre-built standalone loader (~195KB)
│   └── live-loader.js   # Live preview extension (~9KB)
└── wilcojs/react/
    └── src/loader/
        └── standalone.ts # TypeScript source for loader.js

The loader.js file is compiled from standalone.ts using esbuild. This happens during package release (via make wheel), not at install time.

Standalone loader

The standalone loader (loader.js) is a self-contained IIFE that bundles:

  • React 19 and ReactDOM - Full React runtime

  • goober - Lightweight CSS-in-JS library (~1KB)

  • Error Boundary - Catches and displays component errors

  • Source Map Support - Maps errors to original TypeScript

Size: ~195KB minified (includes React)

How it works

  1. Waits for DOMContentLoaded

  2. Finds elements with data-wilco-component attribute

  3. Fetches component bundle from /api/bundles/{name}.js

  4. Transforms ESM imports to use the module registry

  5. Executes the bundle and renders the component

Example usage:

<div
    data-wilco-component="store:product"
    data-wilco-props='{"name": "Widget", "price": 9.99}'
    data-wilco-api="/api"
    data-wilco-hash="abc123">
    Loading...
</div>
<script src="/static/wilco/loader.js" defer></script>

Module registry

Components are bundled with external dependencies (React, goober) that are provided at runtime via window.__MODULES__:

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

This allows components to use standard imports:

import React, { useState } from "react";
import { styled } from "goober";

useComponent hook

The loader provides a useComponent hook for dynamic component loading with React Suspense:

import { useComponent } from "@wilcojs/react";

function ProductList({ products }) {
    const ProductCard = useComponent("store:product");
    return products.map(p => <ProductCard {...p} />);
}

The hook integrates with React Suspense, so components automatically show a loading fallback while child components load.

Static mode

When pre-built bundles are available, the loader can serve bundles from static file URLs instead of the API. This is controlled via two global variables set on the window object:

window.staticManifest = {
    "store:product": {
        "file": "bundles/store--product.a1b2c3d4.js",
        "hash": "a1b2c3d4"
    }
};
window.staticManifestBaseUrl = "/static/wilco/";

When staticManifest is set, the loader:

  1. Looks up the component in the manifest

  2. Loads the bundle from {staticManifestBaseUrl}{file} instead of the API

  3. Falls back to API loading if the component is not in the manifest

The Django template tag {% wilco_loader_script %} automatically sets these variables when WILCO_BUILD_DIR is configured and contains a valid manifest.

These variables are stored on window (not as module-level variables) to survive duplicate <script> includes of loader.js.

Global API

The loader exposes window.wilco for programmatic control:

// Load a component
const Component = await window.wilco.loadComponent("store:product");

// Render into a container
await window.wilco.renderComponent(container, "store:product", props);

// Update props on a mounted component
window.wilco.updateComponentProps(container, newProps);

Live loader

The live loader (live-loader.js) extends the standalone loader for Django admin integration. It provides live preview that updates as you edit forms.

Features:

  • Debounced Updates - Waits 300ms after field changes

  • Server Validation - POSTs form data to validate endpoint

  • Error Display - Shows validation errors above preview

  • Blob URL Preview - Shows uploaded images before save

Usage:

<div
    data-wilco-component="store:product_preview"
    data-wilco-props='{"name": "Widget"}'
    data-wilco-live="true"
    data-wilco-validate-url="/admin/store/product/123/validate_preview/">
</div>
<script src="/static/wilco/loader.js" defer></script>
<script src="/static/wilco/live-loader.js" defer></script>

Image preview

When users select an image file, the live loader creates a blob URL for instant preview without uploading:

// Field "image" gets mapped to prop "imageUrl"
const blobUrl = URL.createObjectURL(file);
props.imageUrl = blobUrl;

This works transparently with components that accept imageUrl props.

Building the loader

The loader is pre-built in the wheel, but for development:

# Build loader only
make build-loader

# Build wheel (includes loader build)
make wheel

Or directly:

cd src/wilcojs/react
pnpm build:loader

The build command uses esbuild:

esbuild src/loader/standalone.ts \
    --bundle \
    --minify \
    --format=iife \
    --outfile=../../wilco/bridges/django/static/wilco/loader.js

Runtime requirements

To use wilco components (render on pages):

  • No Node.js required

  • JavaScript files included in wheel

  • Modern browser with ES2020 support

To create components (bundle TypeScript at runtime):

  • esbuild must be available in PATH

  • Or: Node.js with npx (downloads esbuild on demand)

Install esbuild globally:

npm install -g esbuild

Or let wilco use npx to download it automatically.

Component bundles

When a component is requested, wilco’s Python bundler:

  1. Locates the component’s index.tsx file

  2. Runs esbuild with external dependencies

  3. Returns ESM bundle with inline source maps

Bundler configuration:

bundle_component(
    ts_path=Path("components/product/index.tsx"),
    component_name="store:product",
    external_deps=["react", "react-dom", "react/jsx-runtime",
                   "@wilcojs/react", "goober"]
)

External dependencies are resolved via the module registry at runtime.

See also