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.0.tar.gz (204.0 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.0-py3-none-any.whl (215.4 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: pjx-0.3.0.tar.gz
  • Upload date:
  • Size: 204.0 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.0.tar.gz
Algorithm Hash digest
SHA256 31cd98100f6da9012bffc36294bc951a724a5cd56b47d8bfde6d05245d066a6e
MD5 0a48aef9fc591f882ac867dae910969e
BLAKE2b-256 554d0d4e765c1960694b006889f5f220f35bf12f83535558472e4b3ad7fd4277

See more details on using hashes here.

Provenance

The following attestation bundles were made for pjx-0.3.0.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.0-py3-none-any.whl.

File metadata

  • Download URL: pjx-0.3.0-py3-none-any.whl
  • Upload date:
  • Size: 215.4 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.0-py3-none-any.whl
Algorithm Hash digest
SHA256 77ca75bfb3721ccb50ec32252dae60cf0ab33ab8a117ca366dd9688a6e60afbe
MD5 74473ac2a9c10d351a2e927aafa626f1
BLAKE2b-256 1d5536cb33b1d9e2e1fd929bd41569d2ebb743d9393135b7e9b82030ddca65fc

See more details on using hashes here.

Provenance

The following attestation bundles were made for pjx-0.3.0-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