This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
stx is a fast, modern UI/templating framework that combines Laravel Blade-like syntax with Bun's performance. It's a monorepo containing multiple packages that work together to provide server-side rendering, component-based architecture, and a rich development experience.
This is a Bun workspace monorepo with packages in packages/:
packages/stx- Core framework with template processing enginepackages/bun-plugin- Bun plugin for.stxfile processingpackages/desktop- Native desktop application framework (NEW)packages/markdown- Markdown parsing with frontmatter supportpackages/sanitizer- HTML/XSS sanitizationpackages/iconify-core- Iconify integration corepackages/iconify-generator- Icon package generation CLIpackages/vscode- VS Code extension for.stxsyntaxpackages/devtools- Development toolingpackages/benchmarks- Performance benchmarks
External dependency: Craft (~/Code/craft) - Zig-based native webview framework for desktop/mobile apps
The core template processing is orchestrated by packages/stx/src/process.ts (~920 lines), which acts as a pipeline orchestrator delegating to extracted modules:
- Pre-processing: Comments removal, escaped directives
- Directive Processing: Sequential processing of directives in specific order:
- Stack directives (
@push,@prepend) - JavaScript/TypeScript execution (
@js,@ts) - Includes and layouts (
@include,@layout,@extends,@section) - Custom directives
- Components (via
component-renderer.tsandcomponent-registry.ts) - Async components (
@async) - Conditionals (
@if,@switch,@auth,@env) - Loops (
@foreach,@for) - Error boundaries (
@errorBoundary,@fallback,@enderrorBoundary) - Memoization (
@memo,v-memo) - Expressions (
{{ }},{!! !!}) — includes placeholder system for compile mode - i18n (
@translate) - Forms (
@csrf,@method,@error) - SEO directives (
@meta,@seo)
- Stack directives (
- Post-processing: Middleware, stack replacements, web component injection
| Module | Responsibility |
|---|---|
signal-processing.ts |
Signal detection, setup function wrapping |
runtime-injection.ts |
Signals/router/browser runtime injection |
component-processing.ts |
Component tag parsing (findComponentTags, parseMultilineAttributes) |
script-validation.ts |
Client script validation rules |
inline-assets.ts |
stx-inline asset resolution |
misc-directives.ts |
@json, @once, ref attrs, x-cloak |
signals.ts(~3075 lines) — runtime generation (template literal for client-side signals runtime)signals-api.ts(~550 lines) — TypeScript API (state, derived, effect, batch, lifecycle, type guards)
The Bun plugin (packages/bun-plugin/src/index.ts) registers loaders for:
.stxfiles - Processed as templates and exported as JavaScript modules.mdfiles - Parsed with frontmatter and exported withcontentanddataexports
# Build all packages
bun run build
# Build individual packages
cd packages/bun-plugin && bun run build
cd packages/stx && bun run buildThe build process:
- Builds CSS assets (
packages/stx/scripts/build-css.ts) - Compiles TypeScript using custom build scripts (
build.tsin each package) - Creates compiled CLI binaries for multiple platforms
# Run all tests
bun test
# Run tests for a specific package
cd packages/stx && bun test
# Run specific test file
bun test packages/stx/test/directives/conditionals.test.ts
# Run tests with coverage
cd packages/stx && bun test --coverage
# Run tests in watch mode
cd packages/stx && bun test --watchTests use Bun's built-in test runner with Happy DOM preloaded (configured in bunfig.toml). Test files follow the pattern *.test.ts and are located in each package's test/ directory.
# Lint all code
bun run lint
# Auto-fix linting issues
bun run lint:fixUses @stacksjs/eslint-config for consistent code style.
# Serve .stx files for development
bun packages/bun-plugin/dist/serve.js pages/ --port 8888
# Or using the CLI
stx-serve pages/ --port 3000Only <script server> runs on the server. All other script types run on the client:
| Tag | Execution | Purpose |
|---|---|---|
<script server> |
Server-side | Data fetching, variable extraction for templates |
<script> |
Client-side | Browser code, signals, composables |
<script client> |
Client-side | Same as bare <script> (explicit alias) |
<script type="module"> |
Client-side | ES module scripts |
This rule is enforced across ALL code paths: process.ts, includes.ts, render.ts, serve.ts, plugin.ts, streaming.ts, and build-views.ts. Previously, some paths used heuristics (checking for document/window/localStorage references) to guess whether a bare <script> was client or server — this caused crashes when browser-only code like localStorage.getItem() was executed server-side.
Templates execute in an isolated context. Variables are extracted from:
<script server>tags - Variables declared here are available to template expressions- Parent component/layout context
- Props passed to components
Important: The export keyword is optional in <script server> tags. All variable declarations (const, let, var) and function declarations are automatically made available to the template, whether exported or not.
<script server>
// Both styles work identically:
const title = 'Hello' // ✅ Works (auto-exported)
export const subtitle = 'World' // ✅ Works (explicitly exported)
function greet(name) { // ✅ Works (auto-exported)
return `Hello, ${name}!`
}
</script>
<h1>{{ title }}</h1>
<h2>{{ subtitle }}</h2>
<p>{{ greet('Alice') }}</p>See packages/stx/src/variable-extractor.ts extractVariables() and convertToCommonJS() for implementation details.
Custom directives are registered in packages/stx/src/config.ts as part of defaultConfig.customDirectives. Each directive needs:
name- without the@prefixhandler- function that processes the directivehasEndTag- whether it uses an end tag (e.g.,@directive...@enddirective)description- for documentation
Template caching is managed in packages/stx/src/caching.ts:
- Cache location:
.stx/cache(configurable viacachePath) - Cache invalidation based on file modification times and dependencies
- Disabled in development mode by default
Components use a centralized registry and unified renderer:
component-registry.ts—ComponentRegistrywith builtin registration and file resolutioncomponent-renderer.ts— unifiedprocessComponents()replacing the previous five separate functions- Builtins (in
packages/stx/src/builtins/):stx-link.ts—<StxLink>produces<a data-stx-link>directly (no custom element)stx-image.ts—<StxImage>produces<img>directlystx-loading-indicator.ts— loading indicator builtinindex.ts— barrel file +registerBuiltins()
- User components are
.stxfiles incomponentsDir, resolved recursively to prevent circular dependencies - Components can receive props and slots, and support scoped context
Props are categorized into three types:
static— plain string values (title="Hello")serverDynamic—:prop="expr"evaluated server-sideclientReactive—:prop="expr"preserved for signals runtime
<a href>= native full page reload (always)<StxLink to>= SPA navigation via router- Router intercepts clicks on
[data-stx-link]elements only - Fragment extraction includes body-level styles from
@push
The router detects layout changes and handles them without page reloads:
- Detection:
X-STX-Layoutresponse header and<meta name="stx-layout">tag - Same layout group: Fragment swap (fast, only
<main>content replaced) - Different layout group: Full body swap (entire
<body>replaced) - Layout groups:
'auth'(contains auth/guest layouts),'app'(everything else) - No page reloads — true SPA transitions across layout changes (like Vue/React)
- Prefetch cache stores layout info for instant layout change detection on hover
Components can emit custom events to parent templates:
defineEmits()returns anemit(event, payload)function inside<script>- Uses
CustomEventwithbubbles: truefor DOM propagation @eventattributes on component tags are forwarded to the component root element- Parent handles emitted events via
@event="handler"on component usage
@async(component: 'Name', timeout: 5000)directive loads components asynchronously/_stx/component/:nameendpoint serves individual components as HTML fragments- Supports loading/error/resolved states with configurable timeout and delay
- Scripts are re-executed and
stx:loadevent fired after component loads
stx uses three distinct prefixes:
| Prefix | Purpose | Examples |
|---|---|---|
: |
Structural directives (control flow) | :if, :show, :for, :key |
x- |
Attribute bindings & content | x-class, x-style, x-href, x-src, x-text, x-html, x-model, x-cloak |
@ |
Event listeners | @click, @submit, @keydown.enter |
— all client-side state lives in x-data is deprecated<script client> blocks using signals:
<script client>
const open = state(false)
const items = state(null)
onMount(async () => {
items.set(await fetch('/api').then(r => r.json()))
})
</script>
<button @click="open.set(!open())">Toggle</button>
<div :show="open">Content</div>
<div :for="item in items()" :key="item.id" class="card">
<p x-text="item.name"></p>
</div>- No
<template>wrappers — put:for/:ifdirectly on the element. stx strips<template>tags during server processing (SFC extraction), so<template :for>breaks. Use<div :for>instead. - All directives work:
:for,:if,:show,x-text,x-html,x-model,x-class,x-style,@click :forsupports parenthesized syntax:(item, index) in array- Avoid
>operator in attribute expressions (:if="count > 0") — use:if="count"or:if="count >= 1"instead. The>can be parsed as the HTML tag closer by regex-based processors.
Components support named slots using the Web Component-style slot="name" attribute:
slot="name"attribute on direct children (no<template>wrapper needed)- Self-closing elements supported
- Multiple children can target the same slot
<template #name>backward compatibility preserved
Web components are built from .stx templates (see packages/stx/src/web-components.ts):
- Configured in
stx.config.tsunderwebComponents - Generate custom element classes
- Support Shadow DOM, observed attributes, and lifecycle callbacks
Configuration is loaded from stx.config.ts or .config/stx.config.ts using bunfig.
Default config is in packages/stx/src/config.ts.
All directories default relative to where stx.config.ts lives. Only override what differs:
my-app/
pages/ ← default pagesDir
layouts/ ← default layoutsDir
components/ ← default componentsDir
partials/ ← default partialsDir
functions/ ← shared composables
stores/ ← state management
public/ ← default publicDir (static assets)
stx.config.ts
crosswind.config.ts ← auto-discovered next to stx.config.ts
Minimal config (all defaults):
export default {
app: { head: { title: 'My App' } },
}Stacks embedded (only override what differs):
export default {
root: 'resources', // shifts base directory
pagesDir: 'views', // pages/ → views/
}root- Base directory for all relative paths (default:.)pagesDir- Pages directory name (default:pages)componentsDir- Components directory (default:components)layoutsDir- Layouts directory (default:layouts)partialsDir- Partials directory (default:partials)storesDir- Stores directory (default:stores)publicDir- Static assets directory (default:public)css- Path to Crosswind config or inline CSS config (default: auto-discoverscrosswind.config.ts)envPrefix- Env var prefix for template exposure (default:STX_PUBLIC_)envFile- Path to .env file (Bun loads.envautomatically)plugins- Plugin/module array (npm packages, local paths, or[path, options]tuples)cache- Enable/disable template cachingdebug- Enable detailed error loggingcustomDirectives- Register custom directivesmiddleware- Pre/post-processing middlewarei18n- Internationalization settingswebComponents- Web component generation config
Plugins register components, functions, stores, pages, and middleware into an stx app:
// stx.config.ts
export default {
plugins: [
'@stacksjs/stx-auth', // npm package
'./plugins/analytics', // local plugin
['bun-queue/devtools', { port: 4400 }], // with options
],
}Plugin definition:
import { definePlugin } from 'stx'
export default definePlugin({
name: 'my-plugin',
components: './components', // auto-registered in host app
functions: './functions', // importable via @/functions/
stores: './stores', // auto-registered
pages: './pages', // merged into file-based routing
setup(options, stx) {
stx.addDirective({ name: 'auth', handler: authHandler, hasEndTag: true })
stx.addRoute('/api/auth/login', loginHandler)
},
})- Bun automatically loads
.envfiles — no stx config needed - Variables with
STX_PUBLIC_prefix (configurable viaenvPrefix) are exposed to templates via$env:
<p>API: {{ $env.STX_PUBLIC_API_URL }}</p>
@if($env.STX_PUBLIC_FEATURE_FLAG === 'true')
<div>New feature enabled</div>
@endifTypeScript path mappings are configured in tsconfig.json:
"paths": {
"stx": ["./packages/stx/src/index.ts"],
"@stacksjs/stx": ["./packages/stx/src/index.ts"],
"bun-plugin-stx": ["./packages/bun-plugin/src/index.ts"],
// ... other packages
}Use these imports consistently across the codebase.
# Generate changelog
bun run changelog
# Bump version and release (prompts for version)
bun run releaseThe release script:
- Generates
CHANGELOG.mdusinglogsmith - Prompts for version bump using
bumpx - Updates versions recursively across workspace packages
- Compiles binaries for all platforms
- Creates zip archives of binaries
Icons use Iconify with 200K+ icons:
# List available icon collections
bun stx iconify list
# Generate icon package
bun stx iconify generate <collection-name>Icon components are generated in packages/collections/ and used as <IconName size="24" /> in templates.
The @stacksjs/desktop package provides native desktop application support:
@stacksjs/desktop (TypeScript API)
↓
Craft (~/Code/craft - Zig webview implementation)
↓
Native APIs (WebKit/GTK/WebView2)
Note: The desktop package uses Craft for native webview rendering. Craft source lives at ~/Code/craft.
# Open native window with dev server
stx dev examples/homepage.stx --nativeThis internally calls openDevWindow() from the desktop package, which uses Craft to create a lightweight native window.
- Window Management: Create and control native windows
- System Tray: Build menubar applications
- Modals & Alerts: Native dialogs and notifications
- 35 UI Components: Documented component library
- Hot Reload: Development mode support
- 100% Test Coverage: 132 tests, 96.77% line coverage
packages/desktop/src/window.ts- Window management (fully implemented)packages/desktop/src/system-tray.ts- System tray with Craft bridge + web simulation (fully implemented)packages/desktop/src/modals.ts- Modal dialogs with native + web fallback (fully implemented)packages/desktop/src/alerts.ts- Toast notifications with native + web fallback (fully implemented)packages/desktop/src/components.ts- 35+ UI components with HTML rendering (fully implemented)packages/desktop/src/types.ts- Complete type definitionspackages/desktop/test/- Comprehensive test suitepackages/desktop/examples/- Working examples
The --native flag in stx dev is implemented via the dev-server module. dev-server.ts is now a 7-line re-export hub delegating to:
dev-server/serve-markdown.ts— markdown file servingdev-server/serve-file.ts— single.stxfile servingdev-server/serve-multi.ts— multi-file routingdev-server/serve-app.ts— full app serving with file-based routing
Native window integration example:
import { openDevWindow } from '@stacksjs/desktop'
async function openNativeWindow(port: number) {
return await openDevWindow(port, {
title: 'stx Development',
width: 1400,
height: 900,
darkMode: true,
hotReload: true,
})
}Run desktop package tests:
cd packages/desktop
bun test # Run all tests
bun test --coverage # With coverage reportAll desktop functionality is fully tested. The package uses Craft (~/Code/craft) for native rendering.
The framework has robust error handling (packages/stx/src/error-handling.ts):
StxRuntimeError- Enhanced errors with file path, line/column infoerrorLogger- Structured error loggingerrorRecovery- Fallback content generation in productiondevHelpers- Development-friendly error messages
When debugging, enable debug: true in config for detailed stack traces.
Performance tracking is available via packages/stx/src/performance-utils.ts:
performanceMonitor.timeAsync()- Measure async operations- Metrics tracked: template processing, directive execution, file I/O
- Enable with performance config options
The stx CLI (packages/stx/bin/cli.ts) provides:
# Initialize new project
stx init
# Generate documentation
stx docs [--format html|markdown|json] [--output dir]
# Icon management
stx iconify list
stx iconify generate <collection>
# Serve templates
stx serve <directory> [--port 3000]-
Directive Processing Order Matters: Directives are processed sequentially. The order in
process.tsensures that includes/layouts are resolved before conditionals, which are resolved before expressions. -
Context Isolation: Each template execution gets an isolated VM context to prevent variable leakage and security issues. See
packages/stx/src/safe-evaluator.ts. -
Dependency Tracking: The build plugin tracks all template dependencies (includes, components, layouts) for proper cache invalidation.
-
Async Processing: Most directive handlers support async operations, allowing for file I/O, API calls, etc.
-
Middleware Timing: Middleware can run
beforeorafterdirective processing. Set thetimingfield appropriately. -
Component Resolution: Components are resolved via
ComponentRegistry— builtins first, thencomponentsDir, then current directory. Paths without extensions automatically append.stx. -
Bun-First APIs: The codebase uses Bun-native APIs where possible.
Bun.file().text()replacesfs.readFileSync,Bun.file().exists()replacesfs.existsSync, andBun.write()replacesfs.writeFileSyncin async contexts.node:pathis retained (no Bun alternative) andnode:fsis kept for directory operations.import process from 'node:process'has been removed across the codebase (global in Bun). -
Production Build: The placeholder system is wired into the expression processor for compile mode. The production server returns a handle with
stop()and proper 404 handling. Tested with real projects: bun-queue (12 pages, 169ms), 11ly (17 pages, 510ms). -
State Management (defineStore/useStore): Signals-based store system in the runtime.
defineStore('id', () => setup)(setup style, primary) anddefineStore('id', { state, getters, actions })(options style, backward compat).useStore('id')retrieves a store. Supports persistence via{ persist: true }or{ persist: { pick, storage, key } }. SSR hydration viawindow.__STX_STORE_STATE__. Stores survive SPA navigation (not cleaned up bycleanupContainer). -
Client Script Bundler:
client-script-bundler.tsprovideshasUserImports()andbundleClientScript(). UsesBun.buildto bundle client scripts that import from@/functions/...,./relativepaths, or npm packages. Features tree-shaking and content-hash caching. stx/stores/composables are marked as external. Opt-in: only triggers when user imports are detected. -
Route Manifest Generation:
.stx/routes.tsis generated on startup (like Nuxt's.nuxt/routes.mjs)..stx/route-types.d.tsprovides a TypeScript route map. Auto-filters components/layouts/partials. Generated by theRouterconstructor, so it works for all serve paths. -
Script Execution Rule: Only
<script server>runs on the server. Bare<script>and<script client>are both client-side. This is enforced in ALL code paths:process.ts,includes.ts,render.ts,serve.ts,plugin.ts,streaming.ts, andbuild-views.ts. Never use heuristics to guess — check for theserverattribute only. -
Error Boundaries:
@errorBoundary/@fallback/@enderrorBoundarydirectives provide template-level error catching.onErrorCaptured()composition API hook available for programmatic error handling. Client-side error catching supports retry. -
v-memo / @memo:
@memo="[dep1, dep2]"memoizes template subtrees. Runtime skips re-processing if dependency values are unchanged. Vue compatibility:v-memoalso works. -
Lazy Routes: Pages are server-rendered by architecture, with scripts loading per-page. SPA fragments only include the target page's scripts. Router prefetches on hover. Production build generates separate fragments per route.
-
Bun-style Dev Server Output: Dev server displays a pretty startup banner matching Bun's HTML dev server style. Interactive shortcuts:
o= open browser,q= quit. Route count and timing displayed on startup. -
Runtime Pre-Initialization Shim: Before the signals IIFE, a shim captures
onMount/onDestroycalls into temporary arrays (window.__stx_early_mounts,window.__stx_early_destroys). Once the real runtime initializes, it drains these queues. This prevents errors when partial scripts execute before the full runtime is ready. -
HTML Entity Decoding in Expressions:
evalAttrExprin the signals runtime decodes common HTML entities (&,<,>,",',') before evaluating expressions. This handles cases where browsers encode attribute values like:text="a > b"asa > b. -
Runtime Cache Behavior: The signals runtime is cached in memory via
getCachedSignalsRuntime()incaching.ts. In debug mode, the runtime is always regenerated (no caching) to prevent stale output during development. In production mode, it's cached for the lifetime of the process. -
Active Class Handling: Both
updateNav()andupdateActiveLinks()in the router handle space-separated class strings.activeClass="bg-indigo-500/10 text-indigo-400"is split and each class added/removed individually viaclassList. -
Alpine-style x-data Reactive Bridge:
reactive.tsprovides a bridge between Alpine-stylex-datasyntax and the signals runtime. The bridge parsesx-data, wraps state properties in signals viastx.state(), handlesinit()(including async), and registers scope intowindow.stx._scopes. The signals runtime then processes ALL directives (x-for,x-text,x-show,:bind,@click, etc.) within those scopes — the reactive bridge does NOT evaluate bindings itself.x-datatriggers signals runtime injection viahasSignalsSyntax. -
x- Directive Support in Signals Runtime*: The signals runtime handles Alpine-style directives alongside the native
@/:syntax:x-for,x-if,x-show,x-text,x-html,x-model,x-bind:attr,x-ref. Thex-forregex supports bothitem in listand Alpine's parenthesized(item, index) in listsyntax. All bind functions (bindShow,bindFor,bindIf,bindClass,bindStyle) usecreateAutoUnwrapProxyto auto-unwrap signals during evaluation. -
Signal Auto-Unwrap in All Evaluation Contexts: Every expression evaluation function uses
createAutoUnwrapProxyso that signal objects from x-data scopes are automatically unwrapped to their values. This ensuresx-show="mobileOpen"evaluates tofalse(the signal value) not truthy (the signal function). Event handlers (@click="mobileOpen = !mobileOpen") detect signals in scope and write back throughsignal.set(). -
CRITICAL: Never use
.replace('</body>', ...)for injection: Always uselastIndexOf('</body>')+ slice. The first</body>in the document may be inside a<script>tag's string content (e.g. the x-element or router runtime). Using.replace()matches the first occurrence and injects code into the middle of a script, breaking the page. This applies to ALL modules that inject before</body>: reactive bridge, x-element, events, hot-reload, animation, production-build, css-scoping, PWA, analytics, heatmap. Also never include</body>in comments inside generated<script>blocks. -
Runtime Minification ASI Fix: After
Bun.Transpilerminifies the signals runtime withminifyWhitespace: true, a post-processing step inserts semicolons at}var,}let,}const,}functionboundaries (→};var,};function). Browsers in strict mode reject these without semicolons when newlines are stripped, even though Bun's parser accepts them. -
SPA Fragment Script Extraction: When serving page fragments for SPA navigation,
serve.tsextracts ALLdata-stx-scopedscripts from the full page response (partial scope IIFEs, setup functions, reactive bridge initScope calls) and appends them to the fragment. The router re-executes these scripts after swapping content. A_latestSetup=nullclear script is prepended to prevent stale scope from the previous page. Previously, fragments only included__stx_setup_scripts, leaving partial scopes uninitialized on SPA navigation. -
Partial Signal Scripts Use Real APIs:
transformSignalScriptinincludes.tsdestructures directly fromwindow.stxinstead of using polyfill fallbacks. The old polyfills created signals without._isSignal, which broke auto-unwrap and effect tracking. The signals runtime is always available (injected in<head>before anydata-stx-scopedscript). -
Global mountCallbacks Flush After Scope Processing: The DOMContentLoaded handler flushes the global
mountCallbacksarray after processing all[data-stx-scope]elements. Previously,mountCallbackswas only flushed inside the[data-stx]loop (for setup function pages). Partial<script client>blocks that callonMount()push to the global array, so it must be flushed regardless of which code path registered the callbacks. -
Document Shell Comment Stripping:
hasDocumentShellstrips HTML comments (like<!-- stx-layout: ... -->) before checking if the output starts with<!DOCTYPE>or<html>. Without this, layout comments caused double<body>wrapping — the document shell wrapped the already-complete layout output because it didn't detect the existing document structure. -
Known Limitation: Server-Side Component Props in Loops: Component props (
:prop="expr") inside@foreachloops do not receive loop variables in their evaluation context. The component renderer creates an isolated context where loop variables likefeatureare not available. Workaround: inline the HTML directly in the@foreachloop instead of using a component. This affects server-side rendering only — client-side:forwith components works correctly. -
<template>Tag Stripping:bun-plugin/src/serve.tsstrips<template>wrapper tags from output (browsers don't render template content). Tags with reactive directives (x-for,x-if,@for,@if,:for,:if) are preserved for the client-side runtime. Prefer puttingx-for/x-ifdirectly on elements (<div x-for="item in items">) instead of using<template>wrappers to avoid stripping issues. -
Stacks App Conventions: Stacks apps use
resources/views/for stx pages (Mode 1: server-side). Config:root: 'resources',pagesDir: 'views',componentsDir: 'views/components',layoutsDir: 'views/layouts',partialsDir: 'views/partials'. Standalone SPA apps (training, bench-review, 11ly) usepages/at root (Mode 2: client-side with API). Both modes use the same stx engine — the mode is implicit from which directives and script types are used, not a config flag. -
CRITICAL: @click Signal Writeback Only for Direct Assignments: The
@clickhandler has a signal writeback mechanism for inline assignment expressions like@click="count = count + 1"or@click="open = !open". This MUST NOT run for function call expressions like@click="openModal()"— the function internally callssignal.set(), and the writeback would reset the signal to its pre-handler value (undoing the function's changes). The writeback is guarded byisDirectAssignmentcheck: only fires when the expression matches/^[a-zA-Z_$]\w*\s*=/(direct variable assignment). -
Directive Double-Bind Guards: All directive binding functions (
bindIf,bindShow,bindFor,bindModel, event handlers) have guards (el.__stx_if_bound,el.__stx_show_bound,el.__stx_for_bound,el.__stx_model_bound,el.__stx_evt_*) to prevent duplicate binding whenprocessElementis called multiple times on the same element (e.g., from:ifsubtree re-processing). -
bindIf Subtree Processing Deferred: When
:ifinserts an element and needs to process its children (bind:text,@click,:show, etc.), the processing is deferred viasetTimeout(0). This prevents child effects from accidentally subscribing to the parentbindIfeffect's tracked signals (which would cause cascading re-runs). ThechildrenProcessedflag ensures processing happens only once per element. -
Client-side useHead / useSeoMeta:
useHead({ title, meta, link, script, bodyAttrs, htmlAttrs })anduseSeoMeta({ title, description, ogTitle, ogImage, ... })are available in<script client>blocks. They updatedocument.title,<meta>tags, and<link>tags at runtime — works on both full page load and SPA navigation (scripts re-execute after fragment swap). -
Dev Server No-Cache: The dev server (
bun-plugin/src/serve.ts) does NOT cache processed templates or partials. Every request re-reads files from disk and re-processes. This ensures file changes are reflected immediately on browser refresh without restarting the server. Production caching is handled separately. -
Lazy Hydration (
stx-hydrate): DeferprocessElementfor a subtree until a trigger fires. Supported triggers:visible(IntersectionObserver, 50px rootMargin),idle(requestIdleCallback, 2000ms timeout),interaction(mouseenter/click/focusin/touchstart, once),media:<query>(matchMedia). Firesstx:hydratedCustomEvent onwindowwhen the subtree activates. Implementation insignals.tsdeferHydration()— runs before the mainprocessElementbody, short-circuits processing until the trigger. Elements withstx-hydratestill ship their HTML immediately (no fetch, unlike@async) — only the wire-up is deferred. Seedocs/features/lazy-hydration.md. -
Route Guard Middleware (SSG): SSG builds run route middleware before rendering each page. Auto-loads from
middleware/directory. Pages declare middleware viadefinePageMeta({ middleware: ['auth'] })in<script server>. Redirect → generates a static redirect page (<meta http-equiv="refresh">). Abort → skips the page. Global middleware viastx.config.tsrouteMiddleware.global. Seepackages/stx/src/ssg.tsmiddleware integration.
- Bug Fixes — Comprehensive tracking of significant bugs found and fixed, with root causes and commit references
- Lazy Hydration —
stx-hydrateattribute documentation with trigger types and usage examples - Deployment — Deployment guide for static sites and SSR apps
- Use pickier for linting — never use eslint directly
- Run
bunx --bun pickier .to lint,bunx --bun pickier . --fixto auto-fix - When fixing unused variable warnings, prefer
// eslint-disable-next-linecomments over prefixing with_
- Use stx for templating — use signals/composables in
<script>or<script client>tags - Use
<script server>for server-side data fetching — this is the ONLY script type that runs on the server - Bare
<script>tags with browser APIs (localStorage,document,window) are fine — they run client-side - Use crosswind as the default CSS framework which enables standard Tailwind-like utility classes
- If you see an abundance of custom styling or utility classes in
<style>blocks, that's wrong — use Crosswind utility classes in the HTML instead. Custom CSS should be rare (only for things Tailwind can't express).
- buddy-bot handles dependency updates — not renovatebot
- better-dx provides shared dev tooling as peer dependencies — do not install its peers (e.g.,
typescript,pickier,bun-plugin-dtsx) separately ifbetter-dxis already inpackage.json - If
better-dxis inpackage.json, ensurebunfig.tomlincludeslinker = "hoisted"