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

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

.. 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 ``<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:

.. code-block:: html

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

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

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

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