Compiler Internals
docs/advanced/compiler-internals.md · line 62 of 64
The Pyxle compiler transforms .pyxl files into separate Python and JSX artifacts. This document explains how the compilation process works.
Compilation overview#
When you run pyxle dev or pyxle build, the compiler:
- Scans the
pages/directory for.pyxlfiles - Parses each file to separate Python from JSX
- Writes server-side Python modules to
.pyxle-build/server/ - Writes client-side JSX modules to
.pyxle-build/client/ - Generates layout composition wrappers in
.pyxle-build/client/routes/ - Creates a Vite configuration at
.pyxle-build/vite.config.js
The parser#
The parser (pyxle/compiler/parser.py) is AST-driven — there are no fence markers, string directives, or per-line keyword heuristics. It finds the Python/JSX boundary by walking the source and, at each position, growing the largest region that parses as valid Python via ast.parse; when Python stops parsing, it grows a JSX segment until valid Python resumes.
How the split works#
- The boundary is decided by what parses as Python, not by the leading keyword.
import React from 'react'is not valid Python, so it lands in JSX;from db import usersis, so it stays in Python. - Arbitrary alternation is supported (
python | jsx | python | jsx | ...), including JSX-first files. - Inside a JSX segment the parser tracks JS structural state (string and template literals, block comments, brace/paren/bracket depth), so Python-looking text inside a JSX body or template literal is never misclassified.
What the parser extracts#
| Artifact | Description |
|---|---|
python_code |
All Python lines concatenated |
jsx_code |
All JSX lines concatenated |
loader |
@server function metadata (name, line number, parameters) |
actions |
@action function metadata (name, line number, parameters) |
head_elements |
Static HEAD variable content |
head_is_dynamic |
Whether HEAD is a callable |
head_jsx_blocks |
<Head>...</Head> JSX blocks extracted for server-side use |
script_declarations |
<Script> component props |
image_declarations |
<Image> component props |
Validation#
The parser enforces (raising a compile-time error otherwise):
- At most one
@serverloader per file @serverand@actionfunctions must beasync@server/@actionmust be defined at module scope (not nested)- Their first parameter must be named
request @actionfunction names must be unique within the file
Code generation#
Server module (.pyxle-build/server/pages/*.py)#
The compiled Python module contains:
from pyxle.runtime import server, action
# Original imports from the .pyxl file
from datetime import datetime
# Loader function
@server
async def load_page(request):
return {"now": datetime.now().isoformat()}
# Action functions
@action
async def delete_item(request):
body = await request.json()
return {"deleted": True}Client module (.pyxle-build/client/pages/*.jsx)#
The compiled JSX module contains:
import React from 'react';
import { Head } from 'pyxle/client';
export default function MyPage({ data }) {
return (
<>
<Head>
<title>My Page</title>
</Head>
<h1>{data.now}</h1>
</>
);
}Composed route module (.pyxle-build/routes/*.jsx)#
When layouts exist, the compiler generates a wrapper:
import Page from '../client/pages/index.jsx';
import Layout from '../client/pages/layout.jsx';
const WRAPPERS = [
{ kind: 'layout', component: Layout, reset: false },
];
export default function PyxleWrappedPage(props) {
// Nests: Layout(Page)
let element = <Page {...props} />;
for (const wrapper of WRAPPERS.reverse()) {
const Wrapper = wrapper.component;
element = <Wrapper>{element}</Wrapper>;
}
return element;
}Vite configuration#
The compiler generates .pyxle-build/vite.config.js that:
- Configures
@vitejs/plugin-reactfor JSX transforms and React Refresh - Sets the
rootto the build directory - Maps import aliases for
pyxle/client - Injects
PYXLE_PUBLIC_*environment variables via Vite'sdefineoption
Incremental compilation#
During pyxle dev, the file watcher triggers recompilation only for changed files:
- The watcher detects a file change in
pages/ - Only the changed
.pyxlfile is recompiled - The server module is re-imported (with module cache invalidation)
- Connected browsers are told to reload, and pick up the rebuilt client bundle (a full page reload, not a hot update)
Build artifacts#
After pyxle build, the output structure is:
dist/
server/ # Compiled Python modules
client/ # Vite-bundled JS/CSS assets
page-manifest.json # Route-to-asset mappingThe page-manifest.json maps each route to its client-side assets:
{
"/": {
"client": {
"file": "assets/index-abc123.js",
"css": ["assets/index-def456.css"]
}
}
}