Skip to main content

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

interface Props {
  title: str = "Dashboard"
  users: list = []
}

derived {
  has_users = len(users) > 0
}
---

<BaseLayout title={title}>
  <h1>{title}</h1>

  <div ?hidden={!has_users}>
    <For each={users} as="user">
      <UserCard name={user.name} />
    </For>
  </div>

  <Show when={!has_users}>
    <p>No users yet.</p>
  </Show>
</BaseLayout>

What You Get

  • JSX-like components in Jinja2 templates
  • <For>, <Show>, <ElseIf>, <Switch>, and <Fragment>
  • interface Props {}, state {}, derived {}, and slots {} in frontmatter
  • Named slots with <slot:name> and {slot.name}
  • Expression sugar: &&, ||, !, ternary ?:, len(), template literals
  • {expr} interpolation in both text and attributes
  • Conditional attributes like ?hidden={expr}
  • HTMX and SSE aliases via pjx[htmx]
  • Stimulus aliases via pjx[stimulus]
  • cn() Tailwind class merging via pjx[tailwind]
  • 78 UI components via pjx-ui (buttons, forms, modals, tables, and more)
  • 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-htmx injects HTMX when the rendered HTML contains hx-* or sse-*
  • pjx-stimulus injects Stimulus when Stimulus data-* attributes are present
  • pjx-tailwind injects the Tailwind browser build when it detects utility classes or text/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

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

pjx-0.3.1.tar.gz (204.1 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

pjx-0.3.1-py3-none-any.whl (215.5 kB view details)

Uploaded Python 3

File details

Details for the file pjx-0.3.1.tar.gz.

File metadata

  • Download URL: pjx-0.3.1.tar.gz
  • Upload date:
  • Size: 204.1 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for pjx-0.3.1.tar.gz
Algorithm Hash digest
SHA256 a1505c2564b075e4af014d4793be658a81ae21dbcb5c6cebe1b20d664f01c105
MD5 feb54032e698d364bd54883c735b9e73
BLAKE2b-256 68df6e515ce58cca2aa62fbb76662a2134c59eb69b4049e73b8cbe79944b2e74

See more details on using hashes here.

Provenance

The following attestation bundles were made for pjx-0.3.1.tar.gz:

Publisher: release.yml on oornnery/pjx

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file pjx-0.3.1-py3-none-any.whl.

File metadata

  • Download URL: pjx-0.3.1-py3-none-any.whl
  • Upload date:
  • Size: 215.5 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for pjx-0.3.1-py3-none-any.whl
Algorithm Hash digest
SHA256 119857583536c1c8fc5153aa2a6528651d53eff2d977ef7d909188a5d2c7883f
MD5 3f81c1d23a41d92ae15fb920b289920c
BLAKE2b-256 b34b6c2f11fd92b24b2ccfb131c8178171fa4e60919f2b35b8b4c5683fd19dcd

See more details on using hashes here.

Provenance

The following attestation bundles were made for pjx-0.3.1-py3-none-any.whl:

Publisher: release.yml on oornnery/pjx

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page