How it Works
This section details the internal mechanics of the Stew-Lang compiler, the router generator, and the file tracker.
The .stew Compilation Pipeline
The stew compile command processes each .stew file through 3 distinct passes:
stewlang/lexer.go)
Tokenizes the source file. It identifies <goscript> tags, {{ }} expressions, {{ if }} / {{ each }} blocks, bind: / on: attributes, components (PascalCase), and raw HTML.
stewlang/parser.go)
Transforms the token list into an Abstract Syntax Tree (AST) composed of nodes: NodeGoScript, NodeHTML, NodeExpr, NodeIf, NodeEach, NodeComponent, and NodeBind.
stewlang/compiler.go)
Translates the AST into Go code. This phase includes several sub-steps:
- Import Extraction: Scans
NodeGoScriptfor server, client, and Stew-specific imports. - Type Extraction: Lifts structs (
type Xxx struct) to the package level. - Binding ID Assignment: For each
NodeBind, generates a unique ID and injects it into the preceding HTML node. - Wasm Build: If client scripts exist, generates a temporary Go file and compiles it with TinyGo.
- Server-side Go Emission: Generates the rendering function with
w.Write([]byte(...))and formatted expressions. - Dynamic Import Management: Includes only the imports actually used in the generated code to avoid
imported and not usederrors. - AST Analysis & DX: Scans client scripts to detect variable/constant declarations and automatically generates
_ = nameboilerplate to prevent unused variable errors. - Formatting: Calls
go/formatto pretty-print the generated code.
Wasm Generation Pipeline
pages/@page.stew
β
βΌ (stew compile)
stewlang/compiler.go
β
βββ Client goscript extraction
βββ Generates /tmp/stew_main_wasm_pages_page.go
β
βΌ
tinygo build -target wasm -no-debug -o ./static/wasm/pages_page.wasm /tmp/...
β
βΌ
static/wasm/pages_page.wasm β Loaded by HTML via wasm_exec.js
Reactivity Model: Signals & Auto-Tracking
Stew-Lang uses a "Push-Based" reactivity system inspired by SolidJS. Unlike polling (Dirty Checking), the CPU usage remains at 0% until a state is modified.
1. Registration (Get)
When an effect (state.Effect or BindBlock) execution starts, it sets its function as the "current context". Any call to signal.Get() during this phase adds the effect to the signal's subscriber list.
2. Notification (Set)
Upon a signal.Set(val), the signal iterates through its subscriber list and immediately triggers their re-execution. This allows the DOM to be updated surgically.
Compiler Heuristics: Stew scans your {{ ... }} expressions. If it finds a .Get() call, it automatically wraps the HTML output in a reactive wasm.BindBlock.
HTML Context (inTag) & Idiomorph: The compiler intelligently tracks HTML tag states. If you place a Signal inside an attribute (e.g., class=""), Stew avoids creating an invisible wrapper tag to maintain valid HTML. Furthermore, our Wasm SDK's Idiomorph engine is configured to morph via innerHTML only, ensuring that Stew's reactive DOM targets are preserved indefinitely.
DX Optimization: Unused Variables
"Go is strict, Stew is flexible."
Since client scripts are injected into a single main() function, any variable declared but not used within the script would cause a Go compilation error.
The Stew compiler solves this automatically:
- AST Analysis: The compiler uses
go/parserandgo/astto traverse your<goscript client>blocks. - Detection: It identifies all
:=,var, andconstdeclarations at the script's root. - Injection: It automatically generates
_ = myVariablelines at the end of themain()function.
Note: This automation only applies to the script's "root" level. Variables declared inside a block (e.g., a for loop, if condition, or nested function) follow standard Go rules and must be used within their respective scopes.
Router Generator
stew generate traverses pages/ using internal/generator/scanner.go and builds a RouteNode tree. internal/generator/writer.go luego translates this tree into Go using text/template:
RouteNode{
URLPath: "/users/{id}"
HasPage: true
HasLayout: true (parent)
HasMiddleware: true (parent)
ImportPath: "github.com/.../pages/users/__id__"
PackageAlias: "stew_pages_users_id"
Children: [...]
}
The middleware and layout chains are constructed by walking up the tree toward the root.
File Tracker (.stew/compiled.json)
Each stew compile call initializes a Tracker (internal/tracker/tracker.go) and records every file it generates:
{
"tracked_files": [
"pages/stew.layout.go",
"pages/stew.page.go",
"pages/blog/stew.page.go",
"static/wasm/pages_page.wasm",
"static/wasm/pages_blog_page.wasm"
]
}
The stew clean command reads this file and deletes exactly these files, followed by the .stew/ directory itself. This ensures precise cleanup even if pages were renamed between builds.
Generated Go Package Naming
To avoid package name conflicts, the compiler uses the folder's relative path as an alias:
pages/ β package pages
pages/users/ β package users (alias: stew_pages_users)
pages/users/__id__/ β package stew_pages_users_id
pages/blog/__slug__/ β package stew_pages_blog_slug
AST β Available Nodes
| Node | Represents |
|---|---|
| NodeHTML | Raw HTML block emitted as-is |
| NodeGoScript | <goscript> block with its context (server/client) and Go content |
| NodeExpr | Inline expression {{ expr }} |
| NodeRaw | A call to {{ raw(...) }} for unescaped HTML |
| NodeIf | Conditional block with Then/Else branches |
| NodeEach | Loop with Iterable, ItemName, and IndexName |
| NodeComponent | Component call with Props and Slot |
| NodeBind | A bind: or on: directive with BindType and BindVar |
Hot Morphing Protocol (SSE)
Hot Morphing relies on a Server-Sent Events connection established between the browser and the stew run dev server. Here is the process flow:
- Detection:
air(or an internal watcher) detects a file modification. - Rebuild:
stew compileandstew generatecommands are triggered. - Broadcast: Once rebuilt, the SSE server sends a
reloadevent to the client. - Fetch: The injected script (
live.InjectScript()) intercepts the event and fetches the new HTML for the current URL. - Morph: The new HTML is compared against the current one via Idiomorph. Only modified nodes are updated, preserving state (focus, scroll, inputs).
// Example SSE payload sent by the server:
event: message
data: {"type": "reload", "path": "/guide/internals"}