Jinja2 preprocessor with JSX-like syntax for FastAPI + HTMX + Stimulus
Project description
PJX
Jinja2 preprocessor with JSX-like syntax for FastAPI + HTMX + Stimulus.
PJX extends Jinja2 with components, control flow, frontmatter, conditional attributes, and template expressions that compile transparently to standard Jinja2 at template load time. No build step, no runtime overhead.
Installation
pip install pjx
pip install pjx[htmx]
pip install pjx[htmx,stimulus]
pip install pjx[all]
Run the CLI without installing the package globally:
uvx pjx --help
uvx pjx check templates/
uvx pjx check templates/ --fix
uvx pjx skills --claude
uvx pjx assets build static/vendor/pjx
uvx pjx assets add alpinejs@3 --dist alpinejs/dist/cdn.min.js --out js/alpine.min.js
uvx pjx assets list
Quick Setup
from fastapi import FastAPI, Request
from fastapi.templating import Jinja2Templates
from jinja2 import FileSystemLoader
from pydantic import BaseModel
from pjx import PJXEnvironment
from pjx.router import PJXRouter
app = FastAPI()
templates = Jinja2Templates(
env=PJXEnvironment(loader=FileSystemLoader("templates"))
)
ui = PJXRouter(templates)
app.include_router(ui)
class HomeProps(BaseModel):
title: str = "Hello"
items: list[str] = []
@ui.page("/", "pages/home.jinja")
async def home(request: Request) -> HomeProps:
return HomeProps(items=["Alice", "Bob", "Charlie"])
Example Template
---
from ..layouts import BaseLayout
from ..components import UserCard
props:
title: str = "Dashboard"
users: list = []
computed:
has_users: props.users | length > 0
---
<BaseLayout title={props.title}>
<h1>{{ props.title }}</h1>
<div ?hidden={not has_users}>
<For each={props.users} as="user">
<UserCard name={user.name} />
</For>
</div>
<Show when={not has_users}>
<p>No users yet.</p>
</Show>
</BaseLayout>
What You Get
- JSX-like components in Jinja2 templates
<For>,<Show>,<Switch>, and<Fragment>props:,vars:,computed:, and slots in frontmatter- Conditional attributes like
?hidden={expr} - HTMX and SSE aliases via
pjx[htmx] - Stimulus aliases via
pjx[stimulus] cn()Tailwind class merging viapjx[tailwind]- FastAPI helpers via
PJXRouter - CLI commands for template validation, formatting, and sitemap generation
Extension System
PJX uses a unified extension model. Each extension subclasses PJXExtension
and can provide processors, Jinja2 globals, and browser asset providers through
three hooks:
from pjx.extension import PJXExtension
class MyExtension(PJXExtension):
@property
def name(self) -> str:
return "my-ext"
def get_processors(self):
return [(40, MyProcessor())]
def get_jinja_globals(self):
return {"my_func": my_func}
def get_asset_provider(self):
return MyAssetProvider()
Register extensions explicitly or let them be discovered automatically:
from pjx import PJXEnvironment
# Explicit registration
env = PJXEnvironment(
loader=FileSystemLoader("templates"),
extensions=[MyExtension()],
)
# Auto-discovery via pjx.extensions entry point
env = PJXEnvironment(loader=FileSystemLoader("templates"))
Third-party packages register extensions in pyproject.toml:
[project.entry-points."pjx.extensions"]
my-ext = "my_package.extension:MyExtension"
The old pjx.processors, pjx.jinja_globals, and pjx.assets entry point
groups no longer exist. Use pjx.extensions instead.
Browser Assets
Installed extensions can provide browser assets that PJX injects automatically on full HTML documents:
pjx-htmxinjects HTMX when the rendered HTML containshx-*orsse-*pjx-stimulusinjects Stimulus when Stimulusdata-*attributes are presentpjx-tailwindinjects the Tailwind browser build when it detects utility classes ortext/tailwindcss
CDN mode works out of the box:
templates = Jinja2Templates(
env=PJXEnvironment(loader=FileSystemLoader("templates"))
)
Vendor mode switches injected URLs to local static files:
templates = Jinja2Templates(
env=PJXEnvironment(
loader=FileSystemLoader("templates"),
asset_mode="vendor",
asset_base_url="/static/vendor/pjx",
)
)
Build vendor files with the CLI. The build generates a package.json, runs
npm install, and copies dist files into the output directory. The resulting
package.json and package-lock.json are kept for reproducibility:
pjx assets build static/vendor/pjx
pjx assets build static/vendor/pjx --provider htmx --provider stimulus
CLI
pjx check templates/
pjx check templates/ --fix
pjx format templates/
pjx sitemap templates/ --base-url https://example.com
pjx skills --claude
pjx skills --agents
pjx assets build static/vendor/pjx
pjx assets add alpinejs@3 --dist alpinejs/dist/cdn.min.js --out js/alpine.min.js
pjx assets list
pjx assets remove alpinejs
pjx demo
# no global install needed
uvx pjx --help
uvx pjx demo
pjx check --fix is for safe technical autofixes.
pjx format stays focused on formatting and frontmatter layout.
pjx assets build vendors browser assets via npm, merging extension and manifest packages.
pjx assets add adds an npm package to the local pjx-assets.json manifest.
pjx assets list shows all assets from extensions and the manifest.
pjx assets remove removes a package from the manifest.
pjx demo launches the bundled demo application.
Links
- Repository: https://github.com/oornnery/pjx
- Full README: https://github.com/oornnery/pjx/blob/main/README.md
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file pjx-0.2.2.tar.gz.
File metadata
- Download URL: pjx-0.2.2.tar.gz
- Upload date:
- Size: 155.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b6abab5f566bc5cdf630a1f6ba09c798617aa4e62256a3e01cbd014ccf1dc767
|
|
| MD5 |
4f7368c1ba7f889f95a8df7fd385b2ff
|
|
| BLAKE2b-256 |
046ce8373e4c99bb410155defd5e4fabe10ca967bdda0ba0196e343f657f1419
|
Provenance
The following attestation bundles were made for pjx-0.2.2.tar.gz:
Publisher:
release.yml on oornnery/pjx
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pjx-0.2.2.tar.gz -
Subject digest:
b6abab5f566bc5cdf630a1f6ba09c798617aa4e62256a3e01cbd014ccf1dc767 - Sigstore transparency entry: 1239424053
- Sigstore integration time:
-
Permalink:
oornnery/pjx@05309169b1ecf985fce2f90f642a5f4642e27c79 -
Branch / Tag:
refs/tags/v0.2.2 - Owner: https://github.com/oornnery
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@05309169b1ecf985fce2f90f642a5f4642e27c79 -
Trigger Event:
release
-
Statement type:
File details
Details for the file pjx-0.2.2-py3-none-any.whl.
File metadata
- Download URL: pjx-0.2.2-py3-none-any.whl
- Upload date:
- Size: 163.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
3b10d6025737152fd88470cec2de9952399c430eeb397b8eb34abf61bd9d8f83
|
|
| MD5 |
ee2ecbb6ee0225dfdb20dba772650b17
|
|
| BLAKE2b-256 |
4fdfcafd9c68180b3d1eb68809325d18763a160cd83086b572790934a592070c
|
Provenance
The following attestation bundles were made for pjx-0.2.2-py3-none-any.whl:
Publisher:
release.yml on oornnery/pjx
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pjx-0.2.2-py3-none-any.whl -
Subject digest:
3b10d6025737152fd88470cec2de9952399c430eeb397b8eb34abf61bd9d8f83 - Sigstore transparency entry: 1239424062
- Sigstore integration time:
-
Permalink:
oornnery/pjx@05309169b1ecf985fce2f90f642a5f4642e27c79 -
Branch / Tag:
refs/tags/v0.2.2 - Owner: https://github.com/oornnery
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@05309169b1ecf985fce2f90f642a5f4642e27c79 -
Trigger Event:
release
-
Statement type: