Standalone Loader

Overview

The standalone loader is a self-contained JavaScript bundle that can render wilco components in any HTML page without requiring a full React application. It’s used by all template-based integrations (Django, Flask, Starlette) for server-side rendered pages.

The loader:

  1. Bundles React and ReactDOM

  2. Provides a module registry for component dependencies

  3. Transforms ESM bundles for runtime execution

  4. Manages component lifecycle (mounting, updating, unmounting)

How it works

Data attributes

Components are defined using HTML data attributes:

<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>

Required attributes:

data-wilco-component

Component name (e.g., store:product, counter)

data-wilco-props

JSON-encoded props object

Optional attributes:

data-wilco-api

Base URL for the component API (default: /api)

data-wilco-hash

Content hash for cache busting. When provided, the loader appends it as a query parameter to the bundle URL.

Initialization flow

  1. DOM Ready: Loader waits for DOMContentLoaded

  2. Discovery: Finds all [data-wilco-component] elements

  3. Fetch: Loads bundle from {api}/bundles/{name}.js

  4. Transform: Converts ESM imports to runtime registry lookups

  5. Compile: Executes transformed code via new Function()

  6. Render: Creates React root and renders component

Warning

Content Security Policy (CSP): The loader uses new Function() to compile component bundles, which is functionally equivalent to eval(). If your application sets a Content Security Policy, you must include 'unsafe-eval' in the script-src directive. Applications using django-csp or similar CSP middleware should add this to their CSP configuration. Always serve bundles over HTTPS to prevent code injection via man-in-the-middle attacks.

Module registry

The loader provides a global module registry that component bundles use:

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

When esbuild bundles a component, it marks React as external:

// Original component code
import { useState } from 'react';

// Bundled (ESM with externals)
import { useState } from 'react';
export { MyComponent as default };

The loader transforms this at runtime:

// Transformed for execution
const { useState } = window.__MODULES__["react"];
return MyComponent;

ESM transformation

The transformEsmToRuntime function converts ESM syntax:

Import statements:

// Before
import { useState, useEffect } from 'react';
import * as React from 'react';
import Component from './Component';

// After
const { useState, useEffect } = window.__MODULES__["react"];
const React = window.__MODULES__["react"];
const Component = window.__MODULES__["./Component"];

Export statements:

// Before
export { MyComponent as default };

// After
return MyComponent;

Source maps:

The transformation preserves source maps by extracting and reattaching the //# sourceMappingURL comment. It also adds a //# sourceURL for debugging:

//# sourceURL=components://bundles/store:product.js
//# sourceMappingURL=data:application/json;base64,...

Global API

The loader exposes a window.wilco API for programmatic control:

window.wilco = {
    renderComponent,
    loadComponent,
    updateComponentProps,
};

loadComponent(name, apiBase, hash)

Load a component bundle by name.

const Component = await window.wilco.loadComponent(
    "store:product",
    "/api",
    "abc123"
);

Returns a Promise that resolves to the React component function.

Components are cached by name or name?hash if a hash is provided. The promise itself is cached to prevent duplicate fetches when multiple containers request the same component simultaneously.

renderComponent(container, name, props, apiBase, hash)

Render a component into a container element.

const container = document.getElementById("my-component");
await window.wilco.renderComponent(
    container,
    "store:product",
    { name: "Widget", price: 9.99 },
    "/api",
    "abc123"
);

The container element is enhanced with internal references:

  • _wilcoRoot: React root instance

  • _wilcoComponent: Loaded component function

  • _wilcoProps: Current props

updateComponentProps(container, newProps)

Update props on an already-rendered component.

window.wilco.updateComponentProps(container, {
    name: "New Name",
    price: 19.99,
});

Returns true if successful, false if component not yet loaded.

Static mode (pre-built bundles)

When components are pre-compiled with wilco build, the loader can fetch bundles from static file URLs instead of the API.

How static mode activates

The loader checks for window.staticManifest at initialization. If present, it contains a mapping of component names to their hashed bundle files:

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

These are typically set by the server-rendered HTML (e.g., Django’s {% wilco_loader_script %} tag).

Bundle resolution in static mode

When loading a component:

  1. Check window.staticManifest for the component name

  2. If found, fetch from {staticManifestBaseUrl}{file}

  3. If not found, fall back to the API endpoint ({apiBase}/bundles/{name}.js)

This fallback enables incremental adoption: pre-build some components while others are still bundled at runtime.

Persistence across duplicate loads

Both staticManifest and staticManifestBaseUrl are stored on the window object rather than as module-level variables. This ensures the manifest state survives when multiple <script> tags load the same loader.js file (e.g., one from a widget, one from a template), since each IIFE execution would otherwise reset module-level variables.

Live preview extension

The live-loader.js script extends the standalone loader with live preview functionality for Django admin forms.

Enabling live preview

Add additional data attributes:

<div
    data-wilco-component="store:product"
    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>

How live preview works

  1. Event Listening: Listens for blur events on form fields

  2. Debouncing: Waits 300ms after last field change

  3. Validation: POSTs form data to validate_url

  4. Response Handling:

    • Success: Updates component with new props

    • Failure: Shows validation errors above preview

Extended API

Live preview adds functions to window.wilco:

validateAndUpdate(container)

Trigger validation and update for a container.

showValidationError(container, errors)

Display validation errors above the preview.

clearValidationError(container)

Remove validation error display.

Building the loader

The standalone loader is built using esbuild:

# Via Makefile
make build-loader

# Or directly
cd src/wilcojs/react
pnpm build:loader

This compiles src/loader/standalone.ts directly to src/wilco/bridges/django/static/wilco/loader.js.

The build command in package.json:

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

The IIFE format ensures the loader is self-contained and doesn’t pollute the global namespace (except for window.__MODULES__ and window.wilco).

When building the Python wheel, the loader is automatically rebuilt:

make wheel  # Runs build-loader, then uv build

Error handling

Component load errors

If a component fails to load, the container displays an error:

<div style="color: red; padding: 1rem;">
    Failed to load component: store:product
</div>

Errors are also logged to the console with full details.

Compilation errors

If the transformed code fails to compile, the error is logged:

console.error("Failed to compile component 'store:product':", error);

Invalid props JSON

If data-wilco-props contains invalid JSON, the error is logged and the component renders with empty props.

Debugging

Source maps

Component bundles include inline source maps. In browser DevTools, you can:

  1. View original TypeScript source in the Sources panel

  2. Set breakpoints in original code

  3. See mapped stack traces in errors

The sourceURL comment helps identify component sources:

components://bundles/store:product.js

Performance considerations

  • Caching: Bundles are cached with long Cache-Control headers

  • Deduplication: Concurrent requests for the same component share one fetch

  • Lazy Loading: Components load on-demand when containers appear

  • React Reuse: Single React instance shared across all components

For optimal performance:

  1. Use data-wilco-hash for cache busting

  2. Pre-load critical components using window.wilco.loadComponent()

  3. Consider server-side rendering for above-the-fold content