=================
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:
.. code-block:: html
Loading...
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:
.. code-block:: javascript
window.__MODULES__ = {
"react": React,
"react/jsx-runtime": ReactJsxRuntime,
};
When esbuild bundles a component, it marks React as external:
.. code-block:: javascript
// 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:
.. code-block:: javascript
// Transformed for execution
const { useState } = window.__MODULES__["react"];
return MyComponent;
ESM transformation
==================
The ``transformEsmToRuntime`` function converts ESM syntax:
**Import statements:**
.. code-block:: javascript
// 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:**
.. code-block:: javascript
// 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:
.. code-block:: javascript
//# sourceURL=components://bundles/store:product.js
//# sourceMappingURL=data:application/json;base64,...
Global API
==========
The loader exposes a ``window.wilco`` API for programmatic control:
.. code-block:: javascript
window.wilco = {
renderComponent,
loadComponent,
updateComponentProps,
};
``loadComponent(name, apiBase, hash)``
--------------------------------------
Load a component bundle by name.
.. code-block:: javascript
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.
.. code-block:: javascript
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.
.. code-block:: javascript
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:
.. code-block:: javascript
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 ``
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:
.. code-block:: bash
# 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``:
.. code-block:: bash
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:
.. code-block:: bash
make wheel # Runs build-loader, then uv build
Error handling
==============
Component load errors
---------------------
If a component fails to load, the container displays an error:
.. code-block:: html
Failed to load component: store:product
Errors are also logged to the console with full details.
Compilation errors
------------------
If the transformed code fails to compile, the error is logged:
.. code-block:: javascript
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:
.. code-block:: text
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