Compiler Internals
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/routes/ - Creates a Vite configuration at
.pyxle-build/vite.config.js
The parser
The parser (pyxle/compiler/parser.py) is a state-machine that classifies each line as Python or JSX.
Line classification
Lines are classified as Python if they match common Python patterns:
import ...orfrom ... import ...def ...orasync def ...class ...- Decorator lines starting with
@ - Control flow:
if,for,while,try,except,finally,with - Continuation of multi-line Python constructs (strings, brackets)
Everything else is treated as JSX.
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:
- At most one
@serverloader per file @serverfunctions must beasync@actionfunctions must beasync- Loader and action names must be valid Python identifiers
- No circular decorator stacking
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)
- Vite's HMR picks up the client-side changes automatically
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"]
}
}
}