API

Utils

alliance_platform.frontend.util.transform_attribute_names(attrs)[source]

Transform HTML attributes to use proper cased names for passing to JSX

Example:

>>> transform_attribute_names({ "class": "item" })

{ "className": "item" }
Parameters:

attrs (dict[str, Any])

Return type:

dict[str, Any]

alliance_platform.frontend.util.guess_node_path(nvmrc_path)[source]

Attempt to guess the node path based on the .nvmrc file

Parameters:

nvmrc_path (Path) – Path to the .nvmrc file

Return type:

str | None

alliance_platform.frontend.util.get_node_ver(node_path)[source]

Get the node version from a given path

if raise_errors is not True then exceptions will be swallowed and None will be returned on error

Parameters:

node_path (str) – The path to the node executable

Return type:

Version | None

Bundler

class alliance_platform.frontend.bundler.frontend_resource.FrontendResource(path)

Represents a resource to be bundled by the bundler

Previously, we just tracked files that a bundler needed to care about but this limits what you can do. For example, you have to bundle everything at that path, even if you use one export in the case with ES Modules. By storing the specifics about the resource, the bundler can then optimise this and - in the case of ES Modules - only bundle the resources actually used (e.g. perform tree shaking). It also means we can more easily detect what resource we are dealing with without relying on only inferring it from the filename.

Parameters:

path (Path)

property content_type: str | None

Get the content type string for this resource

classmethod from_path(path)

Given a path, return the most appropriate resource for it

Parameters:

path (Path | str)

is_substitutable_for(other)

Check if this resource can be used in place of another resource.

Parameters:

other (FrontendResource)

Return type:

bool

path: Path

The path to the resource

serialize()

Serialize the resource for use by the bundler.

Returns a dict with, at minimum, the following:

  • type - a string identifying the resource type. Currently, this will be one of “javascript”, “css”, “esmodule”, “image”.

  • path - path to the file for the resource

Then any additional specific to the resource (e.g. the import details for esmodule usage)

This is used by extract_frontend_resources to export the resource for use in the bundler.

class alliance_platform.frontend.bundler.frontend_resource.JavascriptResource(path)

A plain javascript resource.

See also ESModuleResource

Parameters:

path (Path)

class alliance_platform.frontend.bundler.frontend_resource.CssResource(path)

A CSS resource

Parameters:

path (Path)

class alliance_platform.frontend.bundler.frontend_resource.ESModuleResource(path, import_name, is_default_import)

An ESModule resource

This allows the bundler to perform tree shaking rather than bundling the whole file and its dependencies.

Parameters:
  • path (Path)

  • import_name (str)

  • is_default_import (bool)

class alliance_platform.frontend.bundler.frontend_resource.VanillaExtractResource(path)

A Vanilla Extract CSS resource

This will be a file with extension .css.ts

Parameters:

path (Path)

class alliance_platform.frontend.bundler.frontend_resource.ImageResource(path)

An image resource

Parameters:

path (Path)

alliance_platform.frontend.bundler.get_bundler()[source]

Get the current bundler instance

This comes from the BUNDLER setting

Return type:

BaseBundler

class alliance_platform.frontend.bundler.base.BaseBundler(root_dir, path_resolvers)
Parameters:
check_dev_server()

Check if the dev server is running.

Should return DevServerCheck which should indicate whether the server is running and if so, what the project dir for that server is. This allows detection of when a server is running, but it’s for a different project.

Return type:

DevServerCheck

does_asset_exist(filename)

Check if asset exists. By default, looks to filesystem.

Implementations may not rely on the filesystem, e.g. once a project is built and deployed the source may not exist and instead the built files are stored. In Vite, this would be checked by seeing if the asset exists in the manifest.

Parameters:

filename (Path)

format_code(code)

Form code for output. Typically, this would only occur in development for performance reasons.

This exists on the bundler so that we can leverage existing dev server to avoid having to shell out to a prettier process each time.

Parameters:

code (str)

get_embed_items(resources, resource_type=None)

Generate the necessary AssetFileEmbed instances for the specified resource(s).

For example, a javascript file for a component could return an AssetFileEmbed instance for its javascript content plus one for it’s CSS content.

Parameters:
  • resources (FrontendResource | Iterable[FrontendResource]) – The resource(s) to embed. If you need to embed multiple assets it’s best to do them all together so that any necessary de-duplication can occur.

  • resource_type (str | type[FrontendResource] | None) – If set only assets of that type will be embedded, otherwise all asset will be. The two common content types are JavascriptResource. but others like image/png are also possible.

Returns:

The list of AssetFileEmbed instances that will be embedded.

Return type:

list[AssetFileEmbed]

get_preamble_html()

Return preamble that is included in the HTML head

Defaults to nothing. This can be used for things like setting up hot module reloading.

Is used by the bundler_preamble() tag.

Return type:

str

get_ssr_cancel_url()

Return URL for canceling an in-flight SSR request.

Implementations may return None if cancellation is not supported.

Return type:

str | None

get_ssr_headers()

Return any headers to add to the SSR request

Return type:

dict[str, str]

get_ssr_url()

Return the URL to use to perform server side rendering

BundlerAssetContextMiddleware will POST to this URL in development to generate the HTML for any queued SSR items.

Note that production SSR generation works with built files and so the bundler itself isn’t responsible for handling requests, but it may provide a URL that we use (e.g. in PREVIEW mode with Vite).

See Server Side Rendering (SSR) for details on how SSR works.

get_url(path)

Return the URL to load the specified asset at path

Parameters:

path (Path | str)

is_development()

Should return true if running in development mode

Return type:

bool

is_ssr_enabled()

Should return true if server side rendering is enabled and supported by this bundler

Return type:

bool

path_resolvers: list[PathResolver]

A list of PathResolver instances used to resolve paths

resolve_path(path, context, suffix_whitelist=None, suffix_hint=None, resolve_extensions=None)

Resolve a string to a Path based on specified path_resolvers

This allows things like relative paths or resolving relative to a custom directory.

Each resolver in path_resolvers is called in turn until one returns a Path at which point it stops.

Parameters:
  • path (str | Path) – The path to resolve

  • context (ResolveContext) – The context

  • suffix_whitelist (list[str] | None) – (optional) Whitelist of suffixes to accept. Each suffix should include the leading ‘.’. If not specified then all suffixes are supported.

  • suffix_hint (str | None) – (optional) Hint to display as part of error message if suffix not valid

  • resolve_extensions (list[str] | None) – (optional) List of extensions to try if the path as specified doesn’t exist.

Returns:

The resolved absolute Path

Return type:

Path

resolve_ssr_import_path(path)

Resolve a path for use in an ES import when server side rendering

This is necessary as in dev importing from the file directly will work (e.g. import { Button } from 'components/Button') but in production it needs to be the built file (e.g. import { Button } from '/assets/Button-abc123.js').

Note that this only applies for code that will be embedded directly in script tags - code that is bundled by the bundler will resolve this imports itself. :param path: The path to transform into a value that can be used in an ES import

Returns:

A string that can be used in an ES import

Parameters:

path (Path | str)

Return type:

Path | str

root_dir: Path

The root path everything sits under; all other paths are resolved relative to this

should_check_case_sensitive()

Should return true if the bundler should check for case-sensitive filesystems

Reads from the BUNDLER_CHECK_CASE_SENSITIVE setting. Disabled by default as it is slow. Recommended to enable in CI.

Return type:

bool

validate_path(filename, suffix_whitelist=None, suffix_hint=None, resolve_extensions=None)

Validate a path exists, and optional has a suffix in the whitelist

If resolve_extensions are specified the path will be checked with each extension in turn until one matches. If filename doesn’t exist an error is raised.

If suffix_whitelist is specified and the filename doesn’t have a suffix in the whitelist an error is raised.

Parameters:
  • suffix_whitelist (list[str] | None) – (optional) Whitelist of suffixes to accept. Each suffix should include the leading ‘.’. If not specified then all suffixes are supported.

  • suffix_hint (str | None) – (optional) Hint to display as part of error message if suffix not valid

  • resolve_extensions (list[str] | None) – (optional) List of extensions to try if the path as specified doesn’t exist.

  • filename (str | Path)

Returns:

The resolved absolute Path with any missing extensions added.

Return type:

Path

class alliance_platform.frontend.bundler.base.AssetFileEmbed(resource, html_attrs=None)

Represents a file that should be embedded in the HTML

Each AssetFileEmbed is responsible for generating the HTML to embed the file. This can contain logic for doing things differently in dev vs production, or for different targets (e.g. browser vs PDF).

These are created by the bundler when get_embed_items() is called.

If extra HTML attributes are required for the tag, they can be set on the html_attrs dict.

Parameters:
can_embed_head()

Should return True if this file can be embedded in HTML head rather than inline where used

This is useful to load CSS before HTML renders to avoid un-styled content, and for javascript to start loading before it’s used for performance

Things that display inline cannot do this (e.g. images)

generate_code(html_target)

Generate the HTML code necessary to embed this file

Parameters:

html_target (HtmlGenerationTarget)

get_content_type()

Return the content type of the file to embed

The most common types are text/css and text/javascript. Images will be image/<type>, e.g. image/png

Return type:

str | None

get_dependencies()

Return any additional dependencies for this file. For example a JS file may have some CSS associated with it.

Return type:

list[AssetFileEmbed]

html_attrs: dict[str, str]

Any HTML attributes to include in the tag

matches_content_type(content_type)

Return True if this file matches the given content type

If content_type is None then it matches anything.

Parameters:

content_type (type[FrontendResource] | str | Pattern | None)

class alliance_platform.frontend.bundler.base.ResolveContext(root_dir, source_path=None)

Passed to resolve() to project context for resolving paths

Parameters:
  • root_dir (Path)

  • source_path (str | Path | None)

root_dir: Path

The root dir for the project. Paths should sit under this.

source_path: str | Path | None = None

The source path for the ‘thing’ path is being resolved for (e.g. a template file). May not be available.

class alliance_platform.frontend.bundler.base.PathResolver

A class that resolves a path string to a Path

This can be used to do things like resolve paths relative to a root directory, use path aliases etc.

resolve(path, context)

Resolve a string path to a Path.

Should return None if this class was unable to resolve the path, in which case resolving will move onto the next resolver in order.

Parameters:
Return type:

Path | None

class alliance_platform.frontend.bundler.base.RegExAliasResolver(find, replace)

Resolves a path by replacing a substring using a regex

Parameters:
  • find (str)

  • replace (str)

find: str

The regular expression to match

replace: str

The string to replace the match with

resolve(path, context)

Resolve a string path to a Path.

Should return None if this class was unable to resolve the path, in which case resolving will move onto the next resolver in order.

Parameters:
Return type:

Path | None

class alliance_platform.frontend.bundler.base.SourceDirResolver(base_dir)

Resolves any non-absolute path relative to base_dir

Parameters:

base_dir (Path)

base_dir: Path

The directory to resolve paths relative to

resolve(path, context)

Resolve a string path to a Path.

Should return None if this class was unable to resolve the path, in which case resolving will move onto the next resolver in order.

Parameters:
class alliance_platform.frontend.bundler.base.RelativePathResolver

Resolves strings that begin with ./ or ../ as relative to context.source_path

resolve(path, context)

Resolve a string path to a Path.

Should return None if this class was unable to resolve the path, in which case resolving will move onto the next resolver in order.

Parameters:
class alliance_platform.frontend.bundler.resource_registry.FrontendResourceRegistry

Stores any extra resources in addition to those found in templates that should be included in the frontend build

This should be populated on startup such that the resources are available when extract_frontend_resources runs.

The registry to use is specified by the FRONTEND_RESOURCE_REGISTRY setting.

These resources are only used when building the frontend in dev. In production, the underlying files do not need to exist - for example if your deployment process excludes the source files.

Warning

In most cases you do not need this.

This is necessary only for resources that can’t be auto-discovered - for example in a dynamic template. If you try and use a resource that cannot be auto-discovered an error will be raised.

Usage:

frontend_resource_registry = FrontendResourceRegistry()

frontend_resource_registry.add_resource(
    // Adding a path will be converted to a resource for you based on the extension
    settings.PROJECT_DIR / "frontend/src/file1.tsx",
    // Pass resource directly to have full control
    ESModuleResource(settings.PROJECT_DIR / "frontend/src/file2.tsx", "MyComponent", True),
)
add_resource(*resources)

Add resource to be included in frontend build

Parameters:

resources (FrontendResource | Path)

get_resources_for_bundling()

Get all resources to include in build

Return type:

set[FrontendResource]

get_unknown(*resources)

From the specified resource(s) return any that aren’t in the registry

Parameters:

resources (FrontendResource)

Return type:

list[FrontendResource]

alliance_platform.frontend.bundler.asset_registry.FrontendAssetRegistry

alias of FrontendResourceRegistry

Warning

Deprecated. Use FrontendResourceRegistry directly.

class alliance_platform.frontend.bundler.base.DevServerCheck(is_running, read_timeout=False, project_dir=None)

Describes status of dev server

Parameters:
  • is_running (bool)

  • read_timeout (bool)

  • project_dir (Path | None)

is_ok()

Returns True if server is good to use for this project

Return type:

bool

is_running: bool

True if dev server is running at the expected location (depends on the bundler)

is_wrong_server()

Returns True if server is running, but it’s for a different project

Return type:

bool

project_dir: Path | None = None

The project dir the frontend dev server is running at. This is used to determine if it’s for the same project as Django.

read_timeout: bool = False

Read timeout

class alliance_platform.frontend.bundler.context.BundlerAssetContext(*, frontend_resource_registry=None, frontend_asset_registry=None, html_target=HtmlGenerationTarget(label='browser', include_scripts=True, inline_css=False), skip_checks=False, request=None)

Class that acts as container for global context data for React apps and a context manager

When called in a django request and BundlerAssetContextMiddleware is used a context always available from get_current().

Usage:

with BundlerAssetContext() as context:
    # Any templates that contain nodes that extend BundlerAsset will end up in context
    contents = render_template()
    # Post process contents to handle SSR & embedding the asset tags
    contents = context.post_process(contents)

    # Retrieve the resources for all assets that were added to the context
    context.get_resources_for_bundling()

Usage in a django view when BundlerAssetContextMiddleware is active:

context = BundlerAssetContext.get_current()
context.get_resources_for_bundling()
Parameters:
abort_post_process()

Called when post_process can’t be called (e.g. on a request where content-type isn’t text/html)

add_asset(asset)

Add an asset to the current context

Parameters:

asset (BundlerAsset)

assets: list[BundlerAsset]

All the assets that have been added

embed_item_queue: AssetEmbedFileQueue

All assets that have been queued for embedding in the final HTML

frontend_resource_registry: FrontendResourceRegistry

The registry to use for resources that can’t be discovered automatically.

generate_id()

Generate an ID that is guaranteed to be unique for this context

This can be used for generating ID’s to use in HTML, e.g. for container rendering React components into

Return type:

str

get_assets(of_type=None)

Get assets that have been added to context, optionally filtered by of_type

Parameters:

of_type (Type[BundlerAsset] | None) – If provided only assets of this type will be returned

Return type:

list[BundlerAsset]

classmethod get_current()

Get the current BundlerAssetContext or raise NoActiveBundlerAssetContext if none

When called in a django request and BundlerAssetContextMiddleware is used one will always be available.

Return type:

BundlerAssetContext

get_resources_for_bundling(of_type=None)

Return the paths for assets used by assets added to this context

Parameters:

of_type (Type[BundlerAsset] | None)

Return type:

list[FrontendResource]

html_target: HtmlGenerationTarget

The target HTML is being generated for. This determines things like whether scripts are required at runtime, and whether CSS should be inlined in style tags or linked from an external file. Defaults to html_target_browser.

post_process(content)

Given the HTML content, post process it to embed assets and render server side rendered components

This is typically called by BundlerAssetContextMiddleware, but can be called directly if rendering templates of other purposes such as emails of PDFs. content should be the full HTML content of the page, including the <head> and <body> tags. This function will handle embedding any necessary assets based on calls to queue_embed_file() as well as any server side rendered components queued with queue_ssr().

This should be the last step before the HTML is outputted to the client.

Parameters:

content (str)

queue_embed_file(item)

Queues an asset to be embedded in the final HTML

The actual embedding and insertion into the HTML happens in post_process(), which is called by BundlerAssetContextMiddleware which relies on bundler_embed_collected_assets() being used in the template. Typically, this tag would exist in any base templates that are used for all pages.

item is responsible for generating the actual HTML to embed which typically depends on the bundler.

Parameters:

item (AssetFileEmbed)

queue_ssr(item)

Queue an item for server side rendering

Note that if the current bundler does not support SSR, this will have no effect. You can safely call it still and output the placeholder HTML comment, it just will not be replaced with anything.

Parameters:

item (SSRItem) – The item to render

Returns:

A string which is the placeholder that should be outputted in the HTML. It will be replaced by BundlerAssetContextMiddleware.

Return type:

str

register_embed_collected_assets_tag()

For use by bundler_embed_collected_assets only

This is used by the template tag to register that it has been used, and to get the placeholder string to write in the initial HTML that will then be replaced in post_process.

Return type:

str

request: HttpRequest | None

Will be set if used within BundlerAssetContextMiddleware. In other contexts may be unavailable

requires_post_processing()

Check if post_process needs to be called

skip_checks: bool

If specified, no checks will be done on context exit to ensure all assets have been embedded or post processed. Useful for tests.

ssr_queue: dict[str, SSRItem]

Contains all SSRItem’s that have been queued for rendering

class alliance_platform.frontend.bundler.middleware.BundlerAssetContextMiddleware(get_response)

Middleware that wraps requests in a BundlerAssetContext manager

Within a Django request you can call get_current() and it will return context instance for the current request. BundlerAssetContext does this on init.

This is currently only used to facilitate server side rendering by collecting all the items that need to be rendered. This works as follows:

  • SSRItem can be queued for server side rendering using queue_ssr(). The caller (e.g. a template node) must render a placeholder element in the page, e.g. <!-- ___SSR_PLACEHOLDER_0___ --> - this placeholder string is returned by queue_ssr

  • The middleware processes the request like normal so all the HTML is generated, including all the necessary placeholders.

  • It then does the SSR for all requested items.

  • For each item, it will replace the placeholder in the HTML with the rendered version.

Vite Bundler

Example setup:

import json
import logging
from pathlib import Path
from textwrap import dedent
import time

from django.conf import settings

from alliance_platform.frontend.bundler.base import PathResolver
from alliance_platform.frontend.bundler.base import RegExAliasResolver
from alliance_platform.frontend.bundler.base import RelativePathResolver
from alliance_platform.frontend.bundler.base import ResolveContext
from alliance_platform.frontend.bundler.base import SourceDirResolver
from alliance_platform.frontend.bundler.vite import ViteBundler
from django_site.settings.base import get_env_setting

root_dir = settings.PROJECT_DIR
bundler_mode = settings.VITE_BUNDLER_MODE

# This assumes the Vite server writes these files to the cache directory for you
server_details_path = ap_core_settings.CACHE_DIR / "vite-server-address.json"
server_state_path = ap_core_settings.CACHE_DIR / "vite-server-state.json"
server_details = {}

if server_details_path.exists():
    server_details = json.loads(server_details_path.read_text())

if bundler_mode == "development" and server_details:
    # Allow switching to 'preview' mode in dev
    bundler_mode = server_details["serverType"]

# An example of a custom resolver that will resolve paths starting with @alliancesoftware/ui or @alliancesoftware/icons to
# the node modules directory
class AlliancePlatformPackageResolver(PathResolver):
    """Resolves strings that begin with ``./`` or ``../`` as relative to ``context.source_path``"""

    def resolve(self, path: str, context: ResolveContext):
        if path.startswith("@alliancesoftware/ui") or path.startswith("@alliancesoftware/icons"):
            return ap_frontend_settings.NODE_MODULES_DIR / path
        return None

def wait_for_server():
    if server_state_path.exists():
        server_details = json.loads(server_state_path.read_text())
        if server_details.get("status") == "starting":
            logger.warning(
                "Vite server is starting... web requests will wait until this resolves before loading"
            )
            warning_logged = False
            start = time.time()
            while server_details.get("status") == "starting":
                time.sleep(0.1)
                server_details = json.loads(server_state_path.read_text())
                if not warning_logged and (time.time() - start) > 10:
                    logger.warning(
                        "Vite appears to be taking a while to build. Check your `yarn dev` or `yarn preview` command is running and has not crashed"
                    )
                    warning_logged = True


vite_bundler = ViteBundler(
    root_dir=root_dir,
    path_resolvers=[
        AlliancePlatformPackageResolver(),
        RelativePathResolver(),
        RegExAliasResolver("^/", str(settings.PROJECT_DIR) + "/"),
        # Resolve relative paths from this directory
        SourceDirResolver(root_dir / "frontend/src"),
    ],
    mode=bundler_mode,
    server_build_dir=settings.PROJECT_DIR / "frontend/server-build",
    build_dir=settings.PROJECT_DIR / "frontend/build",
    server_host=server_details.get("host", "localhost"),
    server_port=server_details.get("port", "5173"),
    server_protocol=server_details.get("protocol", "http"),
    production_ssr_url=settings.FRONTEND_PRODUCTION_SSR_URL,
    wait_for_server=wait_for_server,
)
class alliance_platform.frontend.bundler.vite.ViteBundler(*, root_dir, path_resolvers, build_dir, server_host, server_port, server_protocol, server_resolve_package_url, mode, wait_for_server=None, disable_ssr=False, server_build_dir=None, production_ssr_url=None, static_url_prefix=None)

Bundler implementation for Vite.

Parameters:
  • root_dir (Path) – The root path everything sits under; all other paths are resolved relative to this

  • path_resolvers (list[PathResolver]) – A list of PathResolver instances used to resolve paths

  • build_dir (Path) – The directory client side files are outputted to (see yarn build:client)

  • server_host (str) – The hostname used for the dev server (e.g. 127.0.0.1)

  • server_port (str) – The port used for the dev server (e.g. 5173)

  • server_protocol (str) – The protocol used for the dev server (http or https)

  • server_resolve_package_url (str) –

    The URL, handled by the dev server, to use to resolve package requests in dev mode. This is only used for packages in /node_modules/ so that we can use the optimized versions of packages that Vite generates. Without this, you encounter errors like “Outdated Optimized Deps”. The dev server should handle requests to this URL and redirect to the final package file. Example implementation for fastify, where server_resolve_package_url="redirect-package-url".

    server.get('/redirect-package-url/*', async (req, res) => {
        const packagePath = (req.params as Record<string, string>)['*'];
        const [, resolvedId] = await vite.moduleGraph.resolveUrl(packagePath);
        // `resolveId` is an absolute path with query string. Resolve it relative to the project root
        const relativePath = normalizePath(path.relative(projectDir, resolvedId));
        // Convert the filesystem path to a web-accessible path
        const url = path.join(vite.config.base, relativePath);
        res.redirect(302, url);
    });
    

  • mode (str) – The mode the bundler is running in; one of development, production or preview

  • wait_for_server (Callable[[], None] | None) – Function that can be passed that will be called before requesting assets for server. This function should handle waiting until the dev server is ready before returning (e.g. by polling the server status)

  • disable_ssr (bool) – Set to True to disable SSR entirely. Defaults to False.

  • server_build_dir (Path | None) – The directory SSR files are outputted to (see yarn build:ssr). Required unless disable_ssr is True.

  • production_ssr_url (str | None) – URL for SSR (only used in production mode). Required unless disable_ssr is True.

  • static_url_prefix (str | None) – Static prefix for assets in production. This should match the prefix specified (if any) in the STATICFILES_DIRS entry for the frontend build. For example, if the frontend build is outputted to /frontend/build and STATICFILES_DIRS is set to (('frontend-dist', '/frontend/build'),), then you should pass static_url_prefix="frontend-dist".

build_dir: Path

Directory client side files are compiled to

build_manifest: ViteManifest

Manifest for client build (only available when in production mode)

check_dev_server()

Check if the dev server is running.

Should return DevServerCheck which should indicate whether the server is running and if so, what the project dir for that server is. This allows detection of when a server is running, but it’s for a different project.

disable_ssr: bool = False

Can be set to disable SSR entirely

does_asset_exist(filename)

In production node_modules might not exist - instead check the manifest file

Parameters:

filename (Path)

format_code(code)

In dev format code using /format-code endpoint defined in dev-server.ts

In production this is a no-op for performance reasons.

Parameters:

code (str)

get_development_ssr_url()

Returns the URL for SSR rendering used in dev

This assumes the Vite dev server has a ‘ssr/’ endpoint to handle requests.

get_embed_items(resources, resource_type=None)

Generate the necessary AssetFileEmbed instances for the specified asset(s) paths.

For example, a javascript file for a component could return an AssetFileEmbed instance for it’s javascript content, and one for it’s CSS content.

Note that in development passing a ‘.css’ file in paths will embed as a javascript file as that’s how Vite works with HMR. See ViteEmbedCss for where this is handled.

Note that if specific items require extra attributes on the tag (e.g. alt="my alt tag") then this can be attached to item.html_attrs.

TODO: Doing preload need to do testing in browser to work out best approach. In particular script modules - does rel=”preload” work or do you have to use rel=”modulepreload” (which has poor support)?

Parameters:
  • resources (FrontendResource | Iterable[FrontendResource]) – The resource(s) to embed. If you need to embed multiple assets it’s best to do them all together so that any necessary de-duplication can occur.

  • resource_type (type[FrontendResource] | str | Pattern | None) – If set only assets of that type will be embedded, otherwise all asset will be. The two common content types are JavascriptResource and CssResource, but others like ImageResource are also possible.

Returns:

The list of AssetFileEmbed instances that will be embedded.

get_preamble_html()

In development returns HMR client setup for Vite

NOTE: Does not include react-refresh, use {% react_refresh_preamble %} for that.

get_ssr_cancel_url()

Return URL for canceling an in-flight SSR request.

Implementations may return None if cancellation is not supported.

get_ssr_headers()

In development add an X-SSR-ROOT-DIR header to the request to the dev server.

This lets the dev server check the request came from the same project to avoid trying to SSR requests from a different project which results in confusing errors.

get_ssr_url()

Returns the URL for SSR rendering

In dev, this uses get_development_ssr_url().

In production production_ssr_url is used.

get_url(path)

Get the URL to load asset as path

In development this uses the dev server, in production is served from static files.

Parameters:

path (Path | str)

is_development()

Should return true if running in development mode

is_ssr_enabled()

Should return true if server side rendering is enabled and supported by this bundler

Return type:

bool

mode: str

The mode bundler is running in; either ‘development’, ‘production’ or ‘preview’

production_ssr_url: str | None

URL for SSR (only used in production mode)

resolve_ssr_import_path(path)

See resolve_ssr_import_path()

Parameters:

path (Path | str)

Return type:

Path | str

resolve_url(path)

Resolve a URL to use to serve the specified path

In development this will be served from the dev server and in production from STATIC_URL

Parameters:

path (Path | str)

server_build_dir: Path | None

Directory server side rendering (SSR) files are compiled to. May be None if disable_ssr is True.

server_build_manifest: ViteManifest

Manifest for SSR (only available when in production mode)

static_url_prefix: str | None

Static prefix for assets in production. This should match the prefix specified (if any) in the STATICFILES_DIRS entry for the frontend build.

vite_metadata_path: Path

Path to the vite metdata JSON file that we use in dev to resolve optimised deps

wait_for_server: Callable[[], None] | None = None

Function that can be passed that will be called before requesting assets for server.

class alliance_platform.frontend.bundler.vite.ViteEmbed(bundler, resource, html_attrs=None)

A embed item for Vite assets

Parameters:
bundler: ViteBundler

The bundler instance to use to resolve asset paths

get_content_type()

Return the content type of the file to embed

The most common types are text/css and text/javascript. Images will be image/<type>, e.g. image/png

Return type:

str | None

resource: FrontendResource

The resource to embed. This will be used to resolve the final URL to embed (e.g. bundled files in production, dev server URLs in dev)

class alliance_platform.frontend.bundler.vite.ViteJavaScriptEmbed(bundler, resource, html_attrs=None)

A JavaScript embed

In development this loads from the dev server, in production it loads from the bundled file.

Parameters:
generate_code(html_target)

Generate the HTML code necessary to embed this file

Parameters:

html_target (HtmlGenerationTarget)

get_dependencies()

Return any additional dependencies for this file. For example a JS file may have some CSS associated with it.

Return type:

list[AssetFileEmbed]

class alliance_platform.frontend.bundler.vite.ViteImageEmbed(bundler, resource, html_attrs=None)

An image embed

This embeds as a img tag. In development this loads the image from the dev server, in production it loads from the bundled file. This is useful for cache busting (the bundled file just gets a hash in its name) or for using an image pipeline (see https://kanban.alliancesoftware.com.au/board/75/card/133647/ for possibility of this)

Parameters:
can_embed_head()

Should return True if this file can be embedded in HTML head rather than inline where used

This is useful to load CSS before HTML renders to avoid un-styled content, and for javascript to start loading before it’s used for performance

Things that display inline cannot do this (e.g. images)

generate_code(html_target)

Generate the HTML code necessary to embed this file

Parameters:

html_target (HtmlGenerationTarget)

class alliance_platform.frontend.bundler.vite.ViteManifest(root_dir, manifest_file)

Contains entries from a Vite manifest file

Vite generates a manifest.json file as part of build based on the build.manifest and build.ssrManifest options. This files contains a mapping from each built file to the corresponding outputs (e.g. the built javascript file and css files).

Parameters:
  • root_dir (Path)

  • manifest_file (Path)

__init__(root_dir, manifest_file)
Parameters:
  • root_dir (Path) – The root that any absolute paths will be resolved relative to

  • manifest_file (Path) – The path to the manifest file. This will be read as JSON and used to populate entries

entries: dict[Path, ViteManifestAsset]

The entries from the file indexed by the source path

get_asset(path)

Given path return the matching ViteManifestAsset or raise ViteManifestAssetMissingError if none found

Parameters:

path (Path | str)

manifest_file: Path

The file path entries were read from

root_dir: Path

The root that any absolute paths will be resolved relative to

class alliance_platform.frontend.bundler.vite.ViteManifestAsset(manifest, file, is_entry, is_dynamic_entry, src, css, assets, imports, dynamic_imports, vanilla_extract_mapping_file)

See https://github.com/vitejs/vite/blob/main/packages/vite/src/node/plugins/manifest.ts for what this looks like

Parameters:
  • manifest (ViteManifest)

  • file (str)

  • is_entry (bool)

  • is_dynamic_entry (bool)

  • src (str | None)

  • css (tuple[str])

  • assets (tuple[str])

  • imports (tuple[str])

  • dynamic_imports (tuple[str])

  • vanilla_extract_mapping_file (str | None)

assets: tuple[str]

Can exist in the manifest file but currently not used by this

collect_dependencies()

Collect all dependencies for this asset, including itself.

This is useful to resolve all the files that are needed for a particular asset.

See AssetDependencies for more details

Return type:

AssetDependencies

css: tuple[str]

Any CSS file dependencies

dynamic_imports: tuple[str]

Any other assets this asset imports dynamically (i.e. using import()

file: str

The built file (e.g. a js or css file)

get_content_type()

Get the content type for this asset

For css & vanilla extract files this will be text/css, for typescript/js it will be text/javascript. For other files it will be guessed from the file extension (e.g. image/svg+xml for svg files or image/png for .png files).

imports: tuple[str]

Any other assets this asset imports

is_dynamic_entry: bool

Whether this is used as a dynamic entry point (i.e. imported using import()

is_entry: bool

Whether this is an entry point. If False then the file hasn’t been imported directly but is a dependency of another entry point.

manifest: ViteManifest

The manifest this asset is referenced from

src: str | None

The source file this was built from. For common chunks this won’t be set.

vanilla_extract_mapping_file: str | None

File that contains class mappings for a vanilla extract file

class alliance_platform.frontend.bundler.vite.AssetDependencies(dependencies, dynamic_dependencies)
Parameters:
dependencies: list[ViteManifestAsset]

Any dependencies not loaded dynamically. This includes any dependencies of dependencies.

dynamic_dependencies: list[ViteManifestAsset]

Any dependencies loaded dynamically (i.e. by a import("asset") call). Includes any non-dynamic dependencies of the dependencies so that all requires assets can be preloaded if desired.

get_css_dependencies()

Returns list of css file names used

Return type:

list[str]

get_dynamic_css_dependencies()

Returns list of css file names used by any dynamic imports

Return type:

list[str]

get_dynamic_js_dependencies()

Returns list of javascript file names used by any dynamic imports

Return type:

list[str]

get_js_dependencies()

Returns list of javascript file names used

Return type:

list[str]

merge(deps)

Merge two AssetDependencies together

Parameters:

deps (AssetDependencies)

SSR

class alliance_platform.frontend.bundler.ssr.SSRSerializable

Mixin for classes which can be serialized for SSR

SSRJsonEncoder can be used to serialize objects that implement this

The frontend handler, currently processSSRRequest in ssr.ts, will use a custom reviver to convert the serialized objects back into their original form. If it encounters an object with the tag @@AP_SSR_V2, it will trigger custom conversions that allow non-standard JSON objects (e.g. a Date or Set, or any arbitrary object you may define). See SSRCustomFormatSerializable for a base class to implement custom serialization.

serialize(context)

Serialize the object for SSR

This method should return a value that can be JSON serialized. Note that it’s fine to return values that contain SSRSerializable objects, as these will be serialized recursively.

Parameters:

context (SSRSerializerContext)

Return type:

dict | str | list

class alliance_platform.frontend.bundler.ssr.SSRCustomFormatSerializable

Mixin for classes which can be serialized for SSR using a custom format

serializer converting the object to the form:

["@@AP_SSR_V2", <tag name>, <serialized representation>]

For example, a date object might look like:

[“@@AP_SSR_V2”, “date”, “2023-01-01”]

The frontend will then have a matching reviver to convert this into a date object.

See SSRJsonEncoder for how this is handled.

get_representation(context)

Get the representation of the object to be serialized

This can be any JSON serializable value. The matching reviver in ssrJsonReviviers.tsx will be passed this value.

Parameters:

context (SSRSerializerContext)

Return type:

dict | str | list

get_tag()

Get the tag to identify the type of serializable. This is matched in nodejs for de-serialization.

class alliance_platform.frontend.bundler.ssr.SSRSerializerContext(bundler)

Context available to SSRSerializable objects during serialization

Parameters:

bundler (BaseBundler)

add_import(definition)

Add the specified import to the list of required imports and return the cache key

This allows us to collect all the imports that are required for a given SSR request and load them all up front, then assign them as part of the JSON parsing.

For example, an import will be serialized as:

[‘@@AP_SSR_V2’, ‘ModuleImport’, ‘abc123’]

abc123 is the key returned by add_import. In processSSRRequest, the required imports will be loaded first, then the JSON will be parsed with a custom reviver. This reviver will replace any ['@@AP_SSR_V2', 'ModuleImport', 'abc123'] with the actual resolved import.

This turns out to be a lot faster than traversing the object after JSON parsing and loading any encountered imports.

Parameters:

definition (ImportDefinition)

Return type:

str

React

See {% component %} for more information on how to use React components in your templates.

class alliance_platform.frontend.templatetags.react.ComponentNode(origin, source, props, target_var=None, ssr_disabled=False, omit_if_empty=False, container_tag='dj-component', container_props=None, html_attribute_template_nodes=None)

A template node used by component()

Parameters:
  • origin (Origin)

  • source (ComponentSourceBase)

  • props (dict[str, Any])

  • ssr_disabled (bool | FilterExpression)

  • omit_if_empty (bool | FilterExpression)

  • container_tag (str | FilterExpression)

  • container_props (dict[str, Any])

  • html_attribute_template_nodes (HtmlAttributeTemplateNodeList | None)

add_dynamic_resource(resource)

Track when a dynamic dependency is used.

A dynamic dependency is JS file that can’t be statically analyzed. For example, if a ComponentProp required an import (e.g. useViewModelCache for ViewModelProp) then that would be a dynamic dependency. This lets us identify during dev imports that need to be explicitly included in the created bundle.

Parameters:

resource (FrontendResource)

dynamic_bundle_resources: list[FrontendResource]

Any extra dependencies for this component. This comes from props used that may require imports, for example DateProp may require the date library be included.

get_dynamic_resources_for_bundling()

Return a list of resources this asset used at runtime.

This is useful for cases where it can’t be determined statically what dependencies are required (e.g. based on the usage of a component).

This is used by BundlerAssetContext during development to determine that any used dependencies will be available in production.

Return type:

list[FrontendResource]

get_resources_for_bundling()

Return a list of resources this asset requires

These will be included in the build process - see extract_frontend_resources

html_attribute_template_nodes: HtmlAttributeTemplateNodeList | None

For tags that are parsed from raw HTML we can have instances where the attributes can’t be resolved until the component is rendered. This is used to store those nodes and resolve them to props later.

print_debug_tree(props, include_template_origin=True)

Print a debug tree of the component and its children.

This renders to look like JSX with comments indicating which templates are used to render the component:

{ /* django/forms/widgets/select.html */ }
<DjangoWidgetWrapper name="is_active">
  <Select >
    {/* django/forms/widgets/select_option.html */ }
    <Item key="unknown">
      --------
    </Item>
  </Select>
</DjangoWidgetWrapper>
Parameters:

props (ComponentProps)

render(context)

Return the node rendered as a string.

Parameters:

context (Context)

resolve_prop(value, context)

Handles resolving values to a type that can be serialized

If you add new ComponentProp there must a case here to convert values to the new type.

Parameters:

context (Context)

resolve_props(context)

Resolve the props for this component to values that can be serialized

To add special handling override the resolve_prop() method.

Parameters:

context (Context)

Return type:

ComponentProps

class alliance_platform.frontend.prop_handlers.ComponentProp(value, node, context)[source]

Represents a prop that requires special conversion to generate the code and perform server side rendering.

Simple props like strings, numbers, lists and dicts can all be converted automatically to the corresponding types in javascript. This works automatically as a) the code generator knows how to convert these types and b) they are JSON serializable so can be sent to the SSR server without any extra processing.

Anything more complex than this needs to be handled by a custom prop handler. There are two parts to this:

  1. Implement generate_code to generate the typescript code that will be embedded in a <script> tag when ComponentNode is rendered.

  2. Implement get_tag and get_representation to return a JSON serializable representation of the prop that can then be revived on the frontend.

A concrete example is DateProp. This will generate code like:

new Date("2020-01-01")

such that the prop passed to the component is a Date instance. The get_representation method will return the string representation “2020-01-01” and get_tag returns "Date". This gets serialized to ["@@AP_SSR_V2", "Date", "2020-01-01"]. The @@AP_SSR_V2 tag is used to indicate that the prop needs special “reviving” on the frontend which is detected by processSSRRequest in ssr.ts, and will call the appropriate “reviver” defined in ssrJsonRevivers.tsx.

Parameters:
get_representation(context)[source]

Get the representation of the object to be serialized

This can be any JSON serializable value. The matching reviver in ssrJsonReviviers.tsx will be passed this value.

Parameters:

context (SSRSerializerContext)

Return type:

dict | str | list

classmethod get_resources_for_bundling()[source]

Return any resources that need to be included in bundling to make use of this prop

Note that if this is specified, the paths will _always_ be included in the bundle even if the prop is not used.

Returns:

A list of Path instances that need to be included in the bundling process.

Return type:

list[FrontendResource]

get_tag()[source]

Get the tag to identify the type of serializable. This is matched in nodejs for de-serialization.

serialize(context)[source]

Serialize the object for SSR

This method should return a value that can be JSON serialized. Note that it’s fine to return values that contain SSRSerializable objects, as these will be serialized recursively.

Parameters:

context (SSRSerializerContext)

Return type:

list

classmethod should_apply(value, node, context)[source]

Return True if this prop handler should be used for the given value.

Parameters:
  • value (Any) – The value in question

  • node (ComponentNode) – The ComponentNode this is being used in

  • context (Context) – The current template context

Returns:

True if this prop handler should be used for the given value.

class alliance_platform.frontend.templatetags.react.ComponentSSRItem(source, props, identifier_prefix)

Represents a component that will be rendered on the server

Provides payload containing necessary import & the props for rendering the component.

See the ComponentSSRItem type in ssr.ts for where this is handled.

NOTE: Keep in sync with ssr.ts

Parameters:
  • source (ComponentSourceBase)

  • props (ComponentProps)

  • identifier_prefix (str)

get_ssr_payload(ssr_context)

Matches ComponentSSRItem type in ssr.ts

For a common component this looks like:

// {% component "div" %}Hello{% component %}
{
  "component": "div",
  "props": {
    "children": ["Hello"]
  },
  "identifierPrefix": "abc123"
}

For an imported component source gets serialized as a custom format handled in ssrJsonRevivers.tsx

// {% component "components/Input" value=5 %}{% endcomponent %}
{
  "component": ["@@AP_SSR_V2", "ComponentImport", {"import": "<generated string>", "propertyName": null}],
  "props": {
    "value": 5
  },
  "identifierPrefix": "6169587712__1_1"
}

After going through the reviver this becomes:

{
    "component": [Function: Input],
    "props: { "value": 5 },
    "identifierPrefix": "6169587712__1_1"
}
Parameters:

ssr_context (SSRSerializerContext)

get_ssr_type()

Returns the type of SSR which is then interpreted by the frontend

See ssr.ts for where this is done

identifier_prefix: str

Used by React to prefix id’s to guarantee uniqueness when multiple roots rendered on one page

props: ComponentProps

Props for the component

source: ComponentSourceBase

Tells the renderer how to resolve the component source - either CommonComponentSource or ESMAssetSource

Default Prop Handlers

These are the default prop handlers that are used by convert props to the correct format for the frontend when using the {% component %} tag.

Warning

Currently DateProp, DateTimeProp, ZonedDateTimeProp and TimeProp use the @internationalized/date library to parse and format dates. Make sure this package is installed.

class alliance_platform.frontend.prop_handlers.SetProp(value, node, context)[source]

Handles passing a python set to a JS Set

Parameters:
class alliance_platform.frontend.prop_handlers.DateProp(value, *args, **kwargs)[source]

Convert a python date or datetime to a js Date

Parameters:

value (date)

class alliance_platform.frontend.prop_handlers.DateTimeProp(value, *args, **kwargs)[source]

Convert a python datetime

Parameters:

value (datetime)

class alliance_platform.frontend.prop_handlers.ZonedDateTimeProp(value, *args, **kwargs)[source]

Convert a python datetime

Parameters:

value (datetime)

class alliance_platform.frontend.prop_handlers.TimeProp(value, *args, **kwargs)[source]

Convert a python time to a @internationalized/date time

Parameters:

value (time)

class alliance_platform.frontend.prop_handlers.SpecialNumeric(value, node, context)[source]

Special prop that indicates a prop should be passed as NaN or Infinity/-Infinity

This is used whenever a math.inf or math.nan / float(“nan”) is passed as a prop.

Parameters: