Asset Collection¶
PyJinHx automatically handles JavaScript and CSS file collection for components.
Automatic Asset Discovery¶
Place asset files next to your component with a matching kebab-case name:
components/ui/
├── my_widget.py # MyWidget class
├── my_widget.html # Template
├── my-widget.js # Auto-collected JavaScript
└── my-widget.css # Auto-collected CSS
Assets are automatically injected when the component renders. The default mode inlines them as <style> and <script> tags.
Naming Convention¶
| Class Name | JS File | CSS File |
|---|---|---|
Button |
button.js |
button.css |
ActionButton |
action-button.js |
action-button.css |
MyWidget |
my-widget.js |
my-widget.css |
Deduplication¶
Assets are collected once per render session. If the same component type is rendered multiple times, each asset is only included once.
Injection Order¶
Rendered output follows this structure:
<style>/* component CSS — INLINE mode */</style>
<link rel="stylesheet" href="..."> <!-- REFERENCE mode -->
<div id="root-component">...</div>
<script>/* component JS — INLINE mode */</script>
<script src="..."></script> <!-- REFERENCE mode -->
- CSS is injected before the HTML (styles apply immediately)
- JS is injected after the HTML (DOM elements exist when scripts run)
- Nested component assets are aggregated and injected at the root level
Asset Delivery Modes¶
Configure how assets are delivered with AssetMode:
| Mode | CSS | JS | Use case |
|---|---|---|---|
INLINE (default) |
<style> |
inline <script> |
Zero-config demos |
REFERENCE |
<link href> |
<script src> |
Production monoliths with static file serving |
NONE |
silence | silence | Manual static wiring |
from pyjinhx import AssetMode, Renderer
Renderer.set_default_js_mode(AssetMode.REFERENCE)
Renderer.set_default_css_mode(AssetMode.REFERENCE)
Renderer.set_asset_url_resolver(lambda path: f"/static/{path}")
When REFERENCE mode is active, co-located assets and js/css fields are collected into a per-render manifest and emitted as URL tags. Override the default URL builder with Renderer.set_asset_url_resolver().
Reactive partial suppression¶
Full-page renders emit assets once at the layout root. Reactive partial responses and OOB swaps never re-ship assets — matching production expectations where the layout shell loads static files once.
Client asset dedup (REFERENCE mode, opt-in)¶
On HTMX requests, pjx.js reports asset URLs already in the DOM via the X-PJX-Assets header. When dedup is enabled, root renders emit only missing <link> / <script src> tags — useful for hx-boost full-page swaps.
Renderer.set_default_asset_dedup(True)
@app.get("/page-b")
def page_b():
return str(PageB(id="app").render()) # X-PJX-Assets from ClientBackend in middleware
With ClientBackend wired in middleware, root renders pick up X-PJX-Assets automatically. Without it, pass client=request explicitly.
Partial renders ignore the asset header (no assets emitted). Dedup defaults to off for backward compatibility.
Client runtime (pjx.js)¶
In REFERENCE mode, root full-page renders emit <script src="/static/pyjinhx/pjx.js"> when the request lacks X-PJX-Mounted. Mount the packaged file from pyjinhx/runtime/pjx.js or override with Renderer.set_default_runtime_url().
For raw Jinja shells, use client_script(mode=AssetMode.REFERENCE) (from pyjinhx.client import client_script). Pass client_script(src=...) to override the runtime URL (otherwise it falls back to Renderer's configured URL, then DEFAULT_RUNTIME_URL).
CSP¶
REFERENCE mode avoids inline scripts entirely — compatible with strict script-src policies without nonce plumbing.
Extra Asset Files¶
Add additional files using the js and css fields:
Warning
Missing files emit a warning via the pyjinhx logger. Check your logs if assets aren't appearing.
Per-Render Manifest¶
Inspect which assets a render used:
from pyjinhx.assets import asset_manifest, make_default_asset_url_resolver
resolver = make_default_asset_url_resolver("./components")
manifest = asset_manifest(session, resolver=resolver)
# manifest.stylesheets, manifest.scripts
Layout Preload (All Components)¶
Ship every component asset from the layout shell instead of per-page discovery:
from pyjinhx.assets import make_default_asset_url_resolver
from pyjinhx.finder import Finder
finder = Finder(root="./components")
resolver = make_default_asset_url_resolver("./components")
head_tags = finder.layout_asset_tags(resolver=resolver)
Combine with AssetMode.REFERENCE and reactive partial suppression so HTMX swaps never re-ship assets.
Cache-Busting¶
Embed content hashes in filenames:
from pyjinhx.assets import hashed_filename, resolver_with_hash
hashed_filename("components/ui/button.js") # button.a1b2c3d4.js
resolver = resolver_with_hash("/static/components", root="./components")
Disabling Assets (NONE mode)¶
from pyjinhx import AssetMode, Renderer
Renderer.set_default_js_mode(AssetMode.NONE)
Renderer.set_default_css_mode(AssetMode.NONE)
When disabled, no asset tags are emitted. Use Finder.collect_javascript_files() and Finder.collect_css_files() to discover files for fully manual static serving.
Static File Serving¶
Use Finder to get lists of asset files for static serving:
from pyjinhx.finder import Finder
finder = Finder(root="./components")
js_files = finder.collect_javascript_files(relative_to_root=True)
# ['ui/button.js', 'ui/dropdown.js', ...]
css_files = finder.collect_css_files(relative_to_root=True)
# ['ui/button.css', 'ui/dropdown.css', ...]
Example: FastAPI with REFERENCE mode¶
from fastapi import FastAPI
from fastapi.staticfiles import StaticFiles
from pyjinhx import AssetMode, Renderer
from pyjinhx.assets import make_default_asset_url_resolver
from pyjinhx.finder import Finder
app = FastAPI()
app.mount("/static/components", StaticFiles(directory="components"), name="components")
app.mount(
"/static/pyjinhx",
StaticFiles(directory="path/to/pyjinhx/runtime"),
name="pyjinhx",
)
Renderer.set_default_js_mode(AssetMode.REFERENCE)
Renderer.set_default_css_mode(AssetMode.REFERENCE)
Renderer.set_asset_url_resolver(make_default_asset_url_resolver("components"))
@app.get("/")
def index():
return str(MyApp(id="app").render()) # emits link/script refs for used components
Example: Django¶
from django.templatetags.static import static
from pyjinhx import Renderer
Renderer.set_asset_url_resolver(lambda path: static(path_relative_to_static_root(path)))
Map each absolute asset path to your {% static %} URL in the resolver callback.
Asset helpers reference¶
| Symbol | Purpose |
|---|---|
DEFAULT_RUNTIME_URL |
Default /static/pyjinhx/pjx.js URL constant |
runtime_asset_path() |
Filesystem path to bundled pjx.js |
default_asset_url() |
Map absolute path → default public URL |
make_default_asset_url_resolver() |
Build a resolver from a component root |
hashed_filename() |
Content-hash a filename for cache-busting |
resolver_with_hash() |
Resolver that embeds hashes in URLs |
asset_manifest() |
Build AssetManifest from a RenderSession |
Finder.layout_asset_tags() |
Preload all component assets in a layout shell (instance method) |
Finder.all_assets() |
Every component asset as (css_paths, js_paths) — bundle input (instance method) |
See Assets API for signatures and examples.
One-bundle deployment¶
For apps that prefer a single stylesheet/script over per-component references, enumerate every component asset and serve two concatenated bundles with a content-hash ETag:
import hashlib
import os
from pathlib import Path
from fastapi import FastAPI, Request, Response
from pyjinhx.finder import Finder
app = FastAPI()
def _build(paths: list[str], marker: str) -> tuple[bytes, str]:
parts = []
for path in paths:
parts.append(marker.format(path=path).encode())
parts.append(Path(path).read_bytes() + b"\n")
payload = b"".join(parts)
return payload, '"' + hashlib.md5(payload).hexdigest() + '"'
CSS_PATHS, JS_PATHS = Finder("app/components").all_assets()
CSS_BUNDLE, CSS_ETAG = _build(CSS_PATHS, "/* === {path} === */\n")
JS_BUNDLE, JS_ETAG = _build(JS_PATHS, "// === {path} ===\n")
def _bundle(request: Request, body: bytes, etag: str, media_type: str) -> Response:
if request.headers.get("if-none-match") == etag:
return Response(status_code=304, headers={"ETag": etag})
return Response(body, media_type=media_type,
headers={"ETag": etag, "Cache-Control": "public, max-age=300"})
@app.get("/assets/bundle.css", include_in_schema=False)
def bundle_css(request: Request) -> Response:
return _bundle(request, CSS_BUNDLE, CSS_ETAG, "text/css")
@app.get("/assets/bundle.js", include_in_schema=False)
def bundle_js(request: Request) -> Response:
return _bundle(request, JS_BUNDLE, JS_ETAG, "application/javascript")
Reference the bundles from your layout <head> and render with
Renderer.set_default_js_mode(AssetMode.NONE) / set_default_css_mode(AssetMode.NONE) so
components stop inlining what the bundle already ships. Concatenation order is alphabetical;
if your app's cascade needs a specific sheet first, prepend it to the list before building.
To include the pyjinhx builtins, add import pyjinhx.builtins and build a second Finder:
Concatenate both lists before building the bundles.