Guide / 17 β€” Internals

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:

1
Lexer (stewlang/lexer.go)

Tokenizes the source file. It identifies <goscript> tags, {{ }} expressions, {{ if }} / {{ each }} blocks, bind: / on: attributes, components (PascalCase), and raw HTML.

2
Parser (stewlang/parser.go)

Transforms the token list into an Abstract Syntax Tree (AST) composed of nodes: NodeGoScript, NodeHTML, NodeExpr, NodeIf, NodeEach, NodeComponent, and NodeBind.

3
Compiler (stewlang/compiler.go)

Translates the AST into Go code. This phase includes several sub-steps:

  • Import Extraction: Scans NodeGoScript for 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 used errors.
  • AST Analysis & DX: Scans client scripts to detect variable/constant declarations and automatically generates _ = name boilerplate to prevent unused variable errors.
  • Formatting: Calls go/format to 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/parser and go/ast to traverse your <goscript client> blocks.
  • Detection: It identifies all :=, var, and const declarations at the script's root.
  • Injection: It automatically generates _ = myVariable lines at the end of the main() 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
NodeHTMLRaw HTML block emitted as-is
NodeGoScript<goscript> block with its context (server/client) and Go content
NodeExprInline expression {{ expr }}
NodeRawA call to {{ raw(...) }} for unescaped HTML
NodeIfConditional block with Then/Else branches
NodeEachLoop with Iterable, ItemName, and IndexName
NodeComponentComponent call with Props and Slot
NodeBindA 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:

  1. Detection: air (or an internal watcher) detects a file modification.
  2. Rebuild: stew compile and stew generate commands are triggered.
  3. Broadcast: Once rebuilt, the SSE server sends a reload event to the client.
  4. Fetch: The injected script (live.InjectScript()) intercepts the event and fetches the new HTML for the current URL.
  5. 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"}