Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions docs/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,13 @@

Release notes for Pyxle. While we're in beta (`0.x`), minor versions may include breaking changes — those are called out explicitly here. To upgrade, run `pip install --upgrade pyxle-framework`.

## 0.7.0 — Unreleased

- **`pyxle init` no longer scaffolds a stale framework pin.** The generated `requirements.txt` used to hard-code `pyxle-framework>=0.4.1,<0.5`, so `pyxle install` in a fresh project silently *downgraded* the framework that scaffolded it. The requirement is now rendered from the running version — running `0.7.0` emits `pyxle-framework>=0.7.0,<0.8` (current release up to the next minor).
- **Fresh scaffolds pass `npm audit` clean.** The scaffold now pins `vite ^6.3` and `@vitejs/plugin-react ^4.4` — the old `vite ^5.2` range resolved a transitive `esbuild <= 0.24.2` that `npm audit` flags (GHSA-67mh-4wv8-2f99) on every new project. Vite 6 keeps Node 18+ support; no config changes are needed in existing projects, but bumping your own pin clears the same warning.
- **`pyxle typecheck` now fails fast with a clear error when TypeScript isn't installed.** It previously fell back to `npx --yes tsc`, which resolves npm's placeholder `tsc` package — relaying its banner and ending with a self-contradictory "Type check failed with 0 error(s)". The command now checks for a real `tsc` (local `node_modules/.bin`, then global) *before* building and exits with `TypeScript is required for 'pyxle typecheck' — run: npm install --save-dev typescript` (or "run: npm install" when it's declared but not installed). A failed `tsc` run that produces no diagnostics now reports the exit code instead of "0 error(s)". See [TypeScript guide](guides/typescript.md).
- **Scaffolded `AGENTS.md` corrected on auto-injected names.** It claimed `LoaderError` had to be imported from `pyxle.runtime` — false since 0.6.1, and contradicted by the rest of the file. It now consistently documents that `server`, `action`, `LoaderError`, `ActionError`, `ValidationActionError`, and `invalidate_routes` are all auto-injected by the compiler.

## 0.6.1 — 2026-07-01

A sharper `pyxle check` — the edit → check → fix loop now catches classes of mistake it used to wave through.
Expand Down
2 changes: 1 addition & 1 deletion docs/guides/typescript.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ Two honest notes on coverage:

## `pyxle typecheck`

`pyxle typecheck` compiles your `.pyxl` files to `.jsx` and runs `tsc --noEmit` against the generated `tsconfig.json`, surfacing any type errors against the framework declarations. It looks for `tsc` in your local `node_modules/.bin`, then globally, then via `npx`, so install `typescript` (locally is recommended) to use it. It's the recommended CI gate for catching framework-API misuse. See the [CLI reference](../reference/cli.md).
`pyxle typecheck` compiles your `.pyxl` files to `.jsx` and runs `tsc --noEmit` against the generated `tsconfig.json`, surfacing any type errors against the framework declarations. It looks for `tsc` in your local `node_modules/.bin`, then globally — TypeScript must actually be installed (locally is recommended: `npm install --save-dev typescript`); if it isn't, the command fails fast with that instruction rather than invoking anything. It's the recommended CI gate for catching framework-API misuse. See the [CLI reference](../reference/cli.md).

## Editor setup

Expand Down
62 changes: 47 additions & 15 deletions pyxle/cli/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -1014,6 +1014,22 @@ def typecheck(
logger.error(f"Failed to create settings: {exc}")
raise typer.Exit(code=1) from exc

# Resolve the TypeScript compiler up front — if it isn't installed there
# is nothing to run, so fail fast with one actionable error instead of
# building the project first and failing afterwards.
tsc_command = _find_tsc(project_root)
if tsc_command is None:
if _typescript_declared(project_root):
logger.error(
"TypeScript is declared in package.json but not installed — run: npm install"
)
else:
logger.error(
"TypeScript is required for 'pyxle typecheck' — "
"run: npm install --save-dev typescript"
)
raise typer.Exit(code=1)

from pyxle.devserver.builder import build_once # noqa: PLC0415

logger.info("Building project before type-checking")
Expand All @@ -1029,14 +1045,6 @@ def typecheck(
logger.error(f"tsconfig.json not found at '{tsconfig_path}'")
raise typer.Exit(code=1)

tsc_command = _find_tsc(project_root)
if tsc_command is None:
logger.error(
"TypeScript compiler (tsc) not found. "
"Install it with 'npm install --save-dev typescript' or 'npm install -g typescript'."
)
raise typer.Exit(code=1)

full_command = [*tsc_command, "--noEmit", "--project", str(tsconfig_path)]
logger.step("TypeScript", " ".join(full_command))

Expand Down Expand Up @@ -1067,7 +1075,12 @@ def typecheck(
logger.success("Type check passed — no errors found")
else:
error_count = sum(1 for line in (result.stdout or "").splitlines() if ": error TS" in line)
logger.error(f"Type check failed with {error_count} error(s)")
if error_count > 0:
logger.error(f"Type check failed with {error_count} error(s)")
else:
# tsc failed without reporting diagnostics (crash, bad invocation,
# unexpected binary) — never claim "0 error(s)" for a failed run.
logger.error(f"Type check failed — tsc exited with code {result.returncode}")
raise typer.Exit(code=1)


Expand All @@ -1090,7 +1103,13 @@ def _emit_tsc_diagnostic(logger: ConsoleLogger, line: str) -> None:


def _find_tsc(project_root: Path) -> list[str] | None:
"""Locate the TypeScript compiler binary."""
"""Locate the TypeScript compiler binary.

Only a real ``tsc`` is returned: the project-local ``node_modules/.bin``
entry or a global install on ``PATH``. There is deliberately no
``npx tsc`` fallback — when TypeScript isn't installed, npm's ``tsc``
package is a placeholder that prints a banner instead of type-checking.
"""

import shutil # noqa: PLC0415

Expand All @@ -1108,14 +1127,27 @@ def _find_tsc(project_root: Path) -> list[str] | None:
if global_tsc is not None:
return [global_tsc]

# npx fallback
npx = shutil.which("npx")
if npx is not None:
return [npx, "--yes", "tsc"]

return None


def _typescript_declared(project_root: Path) -> bool:
"""Whether the project's ``package.json`` declares a ``typescript`` dependency."""

package_json = project_root / "package.json"
if not package_json.is_file():
return False
try:
manifest = json.loads(package_json.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError):
return False
if not isinstance(manifest, dict):
return False
return any(
isinstance(manifest.get(section), dict) and "typescript" in manifest[section]
for section in ("devDependencies", "dependencies")
)


@app.command(help="Display the project's file-based route table.")
def routes(
directory: Path = typer.Argument(
Expand Down
22 changes: 22 additions & 0 deletions pyxle/cli/init.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from __future__ import annotations

import re
import secrets
from pathlib import Path
from typing import Mapping
Expand All @@ -17,6 +18,26 @@

SUPPORTED_TEMPLATES = {"default"}

_MAJOR_MINOR_RE = re.compile(r"^(\d+)\.(\d+)")


def framework_requirement(version: str) -> str:
"""Return the ``pyxle-framework`` requirement line for a scaffolded project.

The specifier is derived from the *running* framework version so a scaffold
never pins an older release than the CLI that generated it: running
``0.7.0`` emits ``pyxle-framework>=0.7.0,<0.8`` (current version up to, but
excluding, the next minor). When the version metadata is unavailable —
e.g. an uninstalled source checkout reports ``"unknown"`` — the requirement
is left unpinned rather than emitting an unsatisfiable specifier.
"""

match = _MAJOR_MINOR_RE.match(version)
if match is None:
return "pyxle-framework"
major, minor = int(match.group(1)), int(match.group(2))
return f"pyxle-framework>={version},<{major}.{minor + 1}"


def build_template_registry() -> TemplateRegistry:
registry = TemplateRegistry()
Expand Down Expand Up @@ -103,6 +124,7 @@ def run_init(
"package_name": project_slug,
"project_name": project_name,
"pyxle_version": __version__,
"pyxle_framework_requirement": framework_requirement(__version__),
}
render_templates(writer, build_template_registry(), context, overwrite=force)
writer.write("public/favicon.ico", default_favicon_bytes(), binary=True, overwrite=force)
Expand Down
9 changes: 6 additions & 3 deletions pyxle/templates/scaffold/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -213,9 +213,12 @@ pyxle install # (re)install Python + Node deps
- **DON'T** call your own actions with `fetch` or write a route for them — use `useAction`/`<Form>`.
- **DON'T** put emoji or other non-BMP characters in **server-rendered** JSX text — SSR can fail
to encode them. Use them only in client-only paths, or stick to plain text.
- **DON'T** import `server`/`action` to use the decorators (they're injected). In a file with an
`@action`, `ActionError`/`ValidationActionError` are injected too — don't import them. **DO**
import `LoaderError` (`from pyxle.runtime import LoaderError`) before raising it — it is *not* injected.
- **DON'T** import `server`/`action` to use the decorators, or `LoaderError`/`ActionError`/
`ValidationActionError`/`invalidate_routes` to raise/call them — the compiler auto-injects each
alongside the decorator that uses it (a `@server` loader injects `LoaderError`; an `@action`
injects `ActionError`/`ValidationActionError`/`invalidate_routes`). An explicit import (or your
own definition) of one of these names is harmless — it takes precedence over the injected one —
just unnecessary.
- **DON'T** expose secrets: env vars reach the browser only with a `PYXLE_PUBLIC_` prefix, and
loader/action return values are sent to the client.
- **DON'T** add a `# --- JSX ---` / `# --- client ---` marker — the split is automatic.
Expand Down
4 changes: 2 additions & 2 deletions pyxle/templates/scaffold/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,10 @@
"react-dom": "^18.3.1"
},
"devDependencies": {
"@vitejs/plugin-react": "^4.3.0",
"@vitejs/plugin-react": "^4.4.1",
"autoprefixer": "^10.4.20",
"postcss": "^8.4.47",
"tailwindcss": "^3.4.10",
"vite": "^5.2.0"
"vite": "^6.3.5"
}
}
2 changes: 1 addition & 1 deletion pyxle/templates/scaffold/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
pyxle-framework>=0.4.1,<0.5
$pyxle_framework_requirement
starlette>=0.37,<0.38
httpx>=0.27,<0.28
uvicorn[standard]>=0.29,<1.0
Loading
Loading