agentsclimarketplace

Litestar build

Skill litestar-org/litestar-skills/skills/litestar-build

Opinionated first-party agent skills, plugins, subagents, slash commands, and MCP servers for the Litestar framework ecosystem — publishable to Claude Code, Gemini CLI, Codex CLI, Cursor, OpenCode, and VS Code/Copilot from a single repo.

Install
npx -y skills add litestar-org/litestar-skills --skill litestar-build

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

One thing to look at

  • 13 stars13 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

What its author says it does

Copied from the file, not written here

Auto-activate for uv build, hatch build, PyApp, PYAPP_*, wheel assets, GitHub release matrices, cargo-zigbuild, or python-build-standalone. Not for runtime deployment.

SKILL.md

14.5 KB, as published. Nobody here has run it

litestar-build

Build-side packaging patterns for Litestar applications: how to produce a self-contained wheel that embeds the Vite/Bun frontend, how to wrap that wheel in a PyApp onefile binary, and how to wire the whole pipeline into GitHub Actions CI and releases.

This skill is the counterpart to litestar-deployment — build is about producing artifacts, deployment is about running them.

The Core Idea: One Wheel, Self-Contained

A Litestar wheel is the single source of truth for a release. It contains:

  • Python code (src/py/app/ or app/)
  • SQL migrations, Jinja templates, INI configs
  • The built Vite/Bun frontend bundle (JS, CSS, HTML, images)
  • Email templates rendered from React/MJX to static HTML

Once produced, that wheel can be:

  1. pip installed into a container (litestar-deployment).
  2. Wrapped in a PyApp binary (dist/<app>, dist/app-x86_64-linux-gnu) for zero-dep distribution.
  3. Uploaded to PyPI or a private index.

All three paths assume the wheel is already complete — no bun run build happens at deploy/install time.

Why bundle assets into the wheel (and not serve from a CDN)

PropertyBundled wheelExternal CDN
Deploy artifacts1 (.whl or binary)2+ (wheel + CDN upload)
Version alignmentAtomic — API and UI lock-stepEasy to skew; rollback is painful
PyApp onefileRequired — the binary embeds the wheelNot possible — binary can't fetch CDN URLs at install time
Offline/air-gappedWorksDoesn't
Dev server startupInstant (files on disk next to package)Fine
Frontend-only deploysRebuild + redeploy wheelPush to CDN only

For most Litestar apps that ship as a product (CLIs, internal tools, enterprise installers), bundled-in-wheel is correct. Projects like litestar-fullstack-inertia and litestar-fullstack all bundle.

Why litestar-vite configs look the way they do in reference apps

This is the piece most developers miss. The Vite/litestar-vite configs in the reference apps are deliberately set up so the Vite output lands inside the Python package directory — because that's what makes the wheel pick them up automatically.

litestar-fullstack (src/js/web/vite.config.ts):

export default defineConfig({
  build: {
    outDir: path.resolve(__dirname, "../../py/app/server/static/web"),  // ← inside src/py/app/ (the Python package)
    emptyOutDir: true,
  },
  plugins: [
    litestar({
      bundleDir: path.resolve(__dirname, "../../py/app/server/static/web"),
      hotFile: path.resolve(__dirname, "../../py/app/server/static/web/hot"),
    }),
  ],
})

litestar-fullstack-inertia — the litestar-vite plugin resolves bundle_dir relative to the project root, and Python settings point it at a package-internal path:

# app/lib/settings.py
return ViteConfig(
    paths=PathConfig(
        root=BASE_DIR.parent,
        bundle_dir=Path("app/domain/web/public"),   # ← inside app/ (the Python package)
        resource_dir=Path("resources"),
    ),
)

Advanced reference pattern — same approach: Vite and the offline-report build write to src/py/<app>/server/public/ and src/py/<app>/domain/web/static/reports/offline/, both under the package root.

Contrast with a naïve vite build that writes to ./dist/ at the repo root: those files are outside the package directory listed in [tool.hatch.build.targets.wheel] packages = [...], so Hatchling silently drops them. The wheel ships without a frontend.

Rule: Vite's outDir and litestar-vite's bundle_dir must point inside one of the Python packages that Hatchling is told to include. Everything else flows from that.

Quick Reference

TopicReferenceKey Commands
Wheel build + asset bundlingreferences/wheel-assets.mduv build --wheel, [tool.hatch.build.targets.wheel.force-include], ignore-vcs = true
PyApp — simple (hatch-binary)references/pyapp-simple.mduv run hatch build --target binary
PyApp — advanced (offline + custom install dir)references/pyapp-advanced.mdtools/bundler.py build, cargo zigbuild
GitHub Actions CI (test matrix)references/github-ci.mdastral-sh/setup-uv@v7, oven-sh/setup-bun@v2, composite actions
GitHub Actions releasereferences/github-release.mdmatrix onefiles, cargo-zigbuild, gh release create
Upgrading Python / PyAppreferences/upgrading.mdFiles to edit in sync

Canonical Makefile Build Graph

Every Litestar app with bundled assets has some variant of this:

.PHONY: install build-assets build-wheel build-onefile

install:                          ## Install Python + JS deps
	@uv sync --all-groups
	@cd src/js/web && bun install --frozen-lockfile

build-assets:                     ## Build frontend into the Python package
	@uv run app assets install
	@uv run app assets build

build-wheel: build-assets         ## Self-contained Python wheel
	@uv build --wheel

build-onefile: build-wheel        ## Single-file PyApp binary
	@./tools/scripts/build-onefile-package.sh

The dependency chain is load-bearing: build-onefile depends on build-wheel, which depends on build-assets. Running them out of order produces an empty or broken artifact.

The two-variant story

Real projects have multiple JS build outputs that all need to land in the wheel:

js-build-all: js-build-web js-build-offline-report
build-wheel: generate-licenses build-templates js-build-all
	@uv build --wheel

Each js-build-* target emits into a distinct subdirectory of the Python package (src/py/<app>/server/public, src/py/<app>/domain/web/static/reports/offline, etc.). Because they're all inside the package, a single uv build --wheel captures everything.

<workflow>

Workflow

Step 1: Point Vite output inside the Python package

Open vite.config.ts. Set build.outDir to an absolute path inside your Python package (src/py/<pkg>/... or <pkg>/...). Set litestar({ bundleDir, hotFile }) to the same path. Do not let Vite default to ./dist/.

Step 2: Choose a Hatchling bundling strategy

  • force-include (inertia): List the built-asset directory explicitly under [tool.hatch.build.targets.wheel.force-include]. Built assets stay .gitignored. Explicit, auditable.
  • ignore-vcs = true (SPA): Tell Hatchling to ignore .gitignore. All package files ship. Simpler; requires discipline to keep dev junk out of package dirs.

See references/wheel-assets.md for full config.

Step 3: Wire Makefile targets

Create install, build-assets, build-wheel. Make the wheel target depend on the asset target. Add any secondary generators (build-templates, generate-licenses) as additional wheel prerequisites.

Step 4: Add PyApp (if shipping a binary)

Decide which flavor:

  • Simple: add [tool.hatch.build.targets.binary] to pyproject.toml and run uv run hatch build --target binary. Good when end-users have PyPI access. See pyapp-simple.md.
  • Advanced: write a tools/bundler.py that pre-installs deps into a python-build-standalone archive, patches PyApp's src/app.rs for a custom install dir, then runs cargo zigbuild. Good for air-gapped distribution or bespoke install locations. See pyapp-advanced.md.

Step 5: Add GitHub Actions CI

Start with a reusable test.yml that accepts python-version + coverage inputs. Call it from ci.yml across a matrix. Use astral-sh/setup-uv@v7 and oven-sh/setup-bun@v2. See github-ci.md.

For larger projects, factor setup-python and setup-node into .github/actions/ composite actions.

Step 6: Add release workflow

Trigger on v* tags. Run the test matrix first. Then build the wheel once. Then build PyApp onefiles in a per-target matrix (x86_64-unknown-linux-gnu, aarch64-unknown-linux-gnu, Apple, Windows). Upload to gh release create. See github-release.md.

</workflow> <guardrails>

Guardrails

  • Vite/bun output must land inside a Python package directory. Otherwise Hatchling drops it. Set build.outDir and litestar({ bundleDir }) to an absolute path under src/py/<pkg>/ or <pkg>/.
  • uv build runs last. Assets, licenses, templates, OpenAPI TypeGen all run before uv build --wheel. Hatchling can't build Vite itself.
  • Pick one bundling strategy. force-include or ignore-vcs = true, not both. Mixing them causes duplicate-file warnings and unpredictable wheel contents.
  • PyApp envs are build-time, not runtime. PYAPP_PROJECT_NAME, PYAPP_PYTHON_VERSION, PYAPP_DISTRIBUTION_EMBED are consumed when cargo build compiles PyApp — not when the resulting binary runs. Setting them at runtime does nothing.
  • PyApp version upgrades touch multiple files. pyproject.toml, build-onefile-package.sh, .github/workflows/release.yml, tools/bundler.py. See upgrading.md.
  • cargo-zigbuild for portable glibc. Plain cargo build on a modern Linux runner produces binaries that fail on older distros (glibc too new). Use cargo zigbuild --target x86_64-unknown-linux-gnu.2.17 to link against glibc 2.17 (CentOS 7-era). Required for broad compatibility.
  • Static-link native deps in PyApp. Set BZIP2_SYS_STATIC=1 and LZMA_API_STATIC=1 before cargo zigbuild, or patch Cargo.toml to add features = ["static"]. Otherwise the onefile fails to load on systems without matching libbz2.so / liblzma.so.
  • Pin uv and bun versions in CI. Use exact pinned versions (e.g., UV_VERSION=0.11.6 and BUN_INSTALL_VERSION=bun-v1.3.12). Drift in either breaks reproducible builds.
  • Create placeholder asset dirs in CI. Hatchling's wheel target fails if app/domain/web/public or src/py/app/server/static/web doesn't exist at wheel-build time. CI jobs that don't build the frontend (lint, mypy, pyright) still need mkdir -p <asset-dir> before uv sync.
  • Never commit built frontend output. Keep bundle_dir paths in .gitignore. CI rebuilds them on every run. Reason: JS builds are non-deterministic across machines and cause noisy diffs.
  • Coverage on one Python version only. Multiple versions uploading the same coverage.xml silently stomp each other. Pin it to one version in your matrix (if: matrix.python-version == '3.12').
  • Disk cleanup on self-hosted runners. GitHub's ubuntu-latest has ~30GB free; building wheels + PyApp + Docker images can blow past that. Aggressive cleanup before the build job is routine.
</guardrails> <validation>

Validation Checkpoint

Before claiming "the wheel builds":

  • make build-wheel succeeds in a clean checkout (after make install)
  • unzip -l dist/*.whl | grep -E '\.(js|css|html)$' shows the built frontend
  • The wheel installs cleanly (uv pip install dist/*.whl in a fresh venv)
  • python -c "import app; app.run()" (or equivalent) serves assets with no extra steps
  • .gitignore excludes the built asset directory
  • Vite's build.outDir is an absolute path inside a Python package dir
  • Hatchling config uses exactly one of force-include OR ignore-vcs = true

Before claiming "the PyApp binary works":

  • dist/<app> --help runs on the build machine
  • The binary is ≥ 50 MB (much smaller means it's not embedding Python)
  • On Linux, ldd dist/<app> shows ≤ libc / libm / libpthread (no libbz2, no liblzma)
  • A network-isolated docker run --rm --network=none ghcr.io/.../distroless -- <app> --help succeeds (proves no runtime PyPI fetches)
  • The install dir (~/.<app>/runtime/ or similar) is created on first run and re-used on second run

Before claiming "CI works":

  • Python matrix covers minimum + stable + latest (e.g., 3.11, 3.12, 3.13)
  • make build-wheel runs in CI and the resulting wheel is uploaded as an artifact
  • Pre-commit / ruff / mypy / pyright / slotscheck run on every PR
  • Release workflow is gated on CI (needs: [lint, test])
  • A tag push produces wheel + onefiles + GitHub release in one run
</validation>

Example Projects

Everything in this skill is distilled from three production projects. Read these for the full picture:

  • litestar-fullstack-inertia — monolithic app/ layout, Inertia.js + React 19, force-include bundling, hatch build --target binary for 4-platform PyApp.
  • litestar-fullstack — nested src/py/app/ + src/js/web/ layout, React + TanStack Router SPA, ignore-vcs = true bundling, React Email templates.

Official References

Cross-References

  • litestar-deployment — runtime deployment (Dockerfiles, K8s, Railway, Cloud Run, systemd) that consumes the artifacts this skill produces
  • litestar-vite — Vite plugin config (asset pipeline details, TypeGen, HMR)
  • litestar-granian — Granian ASGI server (what the wheel's entry-point starts)
  • litestar settings — env-driven @dataclass settings that work both in-wheel and as a PyApp binary

Shared Styleguide Baseline

Keep looking

Skills are one crate of 328,083. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.