git commit -m "feat: initial release of Strata framework v0.1.0

- Static compiler with STRC pattern (Static Template Resolution with
   Compartmentalized Layers)
   - Template syntax: { } interpolation, { s-for }, { s-if/s-elif/s-else
    }
   - File types: .strata, .compiler.sts, .service.sts, .api.sts, .sts,
   .scss
   - CLI tools: strata dev, strata build, strata g (generators)
   - create-strata scaffolding CLI with Pokemon API example
   - Dev server with WebSocket HMR (Hot Module Replacement)
   - Documentation: README, ARCHITECTURE, CHANGELOG, CONTRIBUTING,
   LICENSE
This commit is contained in:
2026-01-16 09:01:29 -05:00
commit 9e451469f5
48 changed files with 15605 additions and 0 deletions

65
.gitignore vendored Normal file
View File

@@ -0,0 +1,65 @@
# Dependencies
node_modules/
.pnp
.pnp.js
# Build outputs
bin/
dist/
.strata/
*.exe
*.dll
*.so
*.dylib
# Coverage
coverage/
coverage.out
coverage.html
*.lcov
# IDE
.idea/
.vscode/
*.swp
*.swo
*~
.DS_Store
# Environment
.env
.env.local
.env.*.local
*.local
# Logs
logs/
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Testing
.nyc_output/
# Temporary files
tmp/
temp/
*.tmp
*.temp
# OS files
Thumbs.db
ehthumbs.db
# Strata generated
examples/*/dist/
examples/*/.strata/
# Go
*.test
*.prof
# Codetyper.nvim - AI coding partner files
*.coder.*
.coder/

860
ARCHITECTURE.md Normal file
View File

@@ -0,0 +1,860 @@
# Strata Framework Architecture
**Strata** = Static Template Rendering Architecture
A compile-time web framework that resolves templates to pure HTML at build time.
```
╔═══════════════════════════════════════════════════════════════════╗
║ ║
║ Code Faster. Load Quick. Deploy ASAP. ║
║ ║
╚═══════════════════════════════════════════════════════════════════╝
```
---
## Table of Contents
1. [Core Principles](#core-principles)
2. [Design Pattern: STRC](#design-pattern-strc)
3. [Compilation Flow](#compilation-flow)
4. [File Types & Layers](#file-types--layers)
5. [Template Syntax](#template-syntax)
6. [Import Hierarchy](#import-hierarchy)
7. [Runtime Architecture](#runtime-architecture)
8. [Dev Server Architecture](#dev-server-architecture)
9. [Future: Shared Worker](#future-shared-worker-architecture)
10. [Future: Smart Fetch](#future-smart-fetch-system)
11. [Future: Encrypted Store](#future-encrypted-store)
12. [Performance Targets](#performance-targets)
---
## Core Principles
| # | Principle | Description |
|---|-----------|-------------|
| 1 | **Static First** | All templates resolved at build time, not runtime |
| 2 | **Zero Runtime Overhead** | Output is pure HTML with no framework code |
| 3 | **Strict Separation** | Each file type has exactly one responsibility |
| 4 | **Go Compiler** | Fast builds with native performance |
| 5 | **Compartmentalized Layers** | Clear boundaries between template, data, and logic |
---
## Design Pattern: STRC
**STRC** = **S**tatic **T**emplate **R**esolution with **C**ompartmentalized Layers
### Pattern Overview
```
┌─────────────────────────────────────────────────────────────────────┐
│ BUILD TIME │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ SOURCE FILES STATIC COMPILER │
│ ════════════ ═══════════════ │
│ │
│ ┌─────────────┐ │
│ │ .strata │─────┐ │
│ │ (Template) │ │ │
│ └─────────────┘ │ ┌────────────────────────────────┐ │
│ │ │ │ │
│ ┌─────────────┐ ├─────────▶│ STRATA STATIC COMPILER │ │
│ │ .compiler │─────┤ │ │ │
│ │ .sts │ │ │ 1. Parse .strata to AST │ │
│ │ (Variables) │ │ │ 2. Load .compiler.sts exports │ │
│ └─────────────┘ │ │ 3. Resolve { variables } │ │
│ │ │ 4. Expand { s-for } loops │ │
│ ┌─────────────┐ │ │ 5. Evaluate { s-if } blocks │ │
│ │ .service │─────┘ │ 6. Output pure HTML │ │
│ │ .sts │ │ │ │
│ │ (Logic) │ └──────────────┬─────────────────┘ │
│ └─────────────┘ │ │
│ │ │
│ ┌─────────────┐ │ │
│ │ .scss │───────────────────────────────┤ │
│ │ (Styles) │ │ │
│ └─────────────┘ │ │
│ │ │
└─────────────────────────────────────────────────┼────────────────────┘
┌─────────────────────────────────────────────────────────────────────┐
│ RUNTIME │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ OUTPUT │
│ ══════ │
│ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ PURE HTML │ │
│ │ │ │
│ │ • No { } syntax │ │
│ │ • No s-for directives │ │
│ │ • No s-if conditionals │ │
│ │ • No framework JavaScript (unless .service.sts defines) │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ Optional: Runtime JavaScript from .service.sts │ │
│ │ (Event handlers, interactivity, etc.) │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────┘
```
### Layer Responsibilities
```
┌─────────────────────────────────────────────────────────────────────┐
│ STRC LAYERS │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ LAYER 1: TEMPLATE (.strata) │
│ ═══════════════════════════ │
│ • Pure HTML structure │
│ • Strata directives ({ }, s-for, s-if) │
│ • No logic, no JavaScript │
│ • Variables injected from compiler layer │
│ │
│ ──────────────────────────────────────────────────────────────── │
│ │
│ LAYER 2: COMPILER (.compiler.sts) │
│ ═════════════════════════════════ │
│ • Variable definitions │
│ • Data structures (arrays, objects) │
│ • Build-time constants │
│ • Executed during compilation │
│ │
│ ──────────────────────────────────────────────────────────────── │
│ │
│ LAYER 3: SERVICE (.service.sts) │
│ ════════════════════════════════ │
│ • Business logic │
│ • API calls (build-time or runtime) │
│ • Event handlers │
│ • Optional browser interactivity │
│ │
│ ──────────────────────────────────────────────────────────────── │
│ │
│ LAYER 4: API CONTRACT (.api.sts) │
│ ════════════════════════════════ │
│ • Declarative endpoint definitions │
│ • Request/response schemas │
│ • Cache policies │
│ │
│ ──────────────────────────────────────────────────────────────── │
│ │
│ LAYER 5: UTILITIES (.sts) │
│ ═════════════════════════ │
│ • Pure functions │
│ • No side effects │
│ • Shared helpers │
│ │
└─────────────────────────────────────────────────────────────────────┘
```
---
## Compilation Flow
### Step-by-Step Process
```
┌─────────────────────────────────────────────────────────────────────┐
│ COMPILATION PIPELINE │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ STEP 1: PARSE TEMPLATE │
│ ══════════════════════ │
│ │
│ Input: Output: │
│ ┌──────────────────────┐ ┌──────────────────────┐ │
│ │ <template> │ │ AST │ │
│ │ <h1>{ title }</h1> │ ────▶ │ ├─ TemplateNode │ │
│ │ { s-for ... } │ │ │ └─ ElementNode │ │
│ │ </template> │ │ │ └─ Interp... │ │
│ └──────────────────────┘ └──────────────────────┘ │
│ │
│ ──────────────────────────────────────────────────────────────── │
│ │
│ STEP 2: LOAD COMPILER SCOPE │
│ ═══════════════════════════ │
│ │
│ Input: Output: │
│ ┌──────────────────────┐ ┌──────────────────────┐ │
│ │ export const title │ │ scope = { │ │
│ │ = 'Hello'; │ ────▶ │ title: 'Hello', │ │
│ │ export const items │ │ items: [...] │ │
│ │ = [...]; │ │ } │ │
│ └──────────────────────┘ └──────────────────────┘ │
│ │
│ ──────────────────────────────────────────────────────────────── │
│ │
│ STEP 3: RESOLVE TEMPLATE │
│ ════════════════════════ │
│ │
│ Process: │
│ ┌──────────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ For each AST node: │ │
│ │ │ │
│ │ • InterpolationNode { var } → Look up in scope, output │ │
│ │ • ForBlockNode { s-for } → Iterate, create loop scope │ │
│ │ • IfBlockNode { s-if } → Evaluate, include/exclude │ │
│ │ • ElementNode <div> → Output tag + recurse │ │
│ │ • TextNode → Output as-is │ │
│ │ │ │
│ └──────────────────────────────────────────────────────────────┘ │
│ │
│ ──────────────────────────────────────────────────────────────── │
│ │
│ STEP 4: OUTPUT │
│ ══════════════ │
│ │
│ Result: │
│ ┌──────────────────────────────────────────────────────────────┐ │
│ │ <h1>Hello</h1> │ │
│ │ <div class="item">Item 1</div> │ │
│ │ <div class="item">Item 2</div> │ │
│ │ <div class="item">Item 3</div> │ │
│ └──────────────────────────────────────────────────────────────┘ │
│ │
│ Pure HTML. No framework syntax. No runtime overhead. │
│ │
└─────────────────────────────────────────────────────────────────────┘
```
### Expression Resolution
```
┌─────────────────────────────────────────────────────────────────────┐
│ EXPRESSION RESOLUTION │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ SIMPLE VARIABLE │
│ ─────────────── │
│ Template: { title } │
│ Scope: { title: "Hello World" } │
│ Output: Hello World │
│ │
│ ──────────────────────────────────────────────────────────────── │
│ │
│ PROPERTY ACCESS │
│ ─────────────── │
│ Template: { user.name } │
│ Scope: { user: { name: "Alice", age: 30 } } │
│ Output: Alice │
│ │
│ ──────────────────────────────────────────────────────────────── │
│ │
│ NESTED ACCESS │
│ ───────────── │
│ Template: { config.api.baseUrl } │
│ Scope: { config: { api: { baseUrl: "https://..." } } } │
│ Output: https://... │
│ │
│ ──────────────────────────────────────────────────────────────── │
│ │
│ LOOP VARIABLE │
│ ───────────── │
│ Template: { s-for item in items } │
│ { item.name } │
│ { /s-for } │
│ │
│ Scope: { items: [{ name: "A" }, { name: "B" }] } │
│ │
│ Loop 1: loopScope = { ...scope, item: { name: "A" } } │
│ Output: A │
│ │
│ Loop 2: loopScope = { ...scope, item: { name: "B" } } │
│ Output: B │
│ │
└─────────────────────────────────────────────────────────────────────┘
```
---
## File Types & Layers
### File Extension Matrix
| Extension | Layer | Execution | Can Import | Purpose |
|-----------|-------|-----------|------------|---------|
| `.strata` | Template | Build | `.strata` | HTML structure |
| `.compiler.sts` | Compiler | Build | `.service.sts`, `.api.sts`, `.sts` | Variables |
| `.service.sts` | Service | Build + Runtime | `.api.sts`, `.sts` | Logic |
| `.api.sts` | API | Build | `.sts` | Endpoints |
| `.sts` | Utility | Both | `.sts` | Pure functions |
| `.scss` | Style | Build | - | Styles |
### Component/Page Structure
```
src/pages/index/
├── index.strata # Template (what to render)
├── index.compiler.sts # Data (what values to use)
├── index.service.sts # Logic (how it behaves)
└── index.scss # Styles (how it looks)
```
### File Content Examples
```
┌─────────────────────────────────────────────────────────────────────┐
│ index.strata │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ <template> │
│ <main class="page"> │
│ <h1>{ title }</h1> │
│ { s-for item in items } │
│ <div>{ item.name }</div> │
│ { /s-for } │
│ </main> │
│ </template> │
│ │
└─────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────┐
│ index.compiler.sts │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ export const title = 'My Page'; │
│ │
│ export const items = [ │
│ { name: 'Item 1' }, │
│ { name: 'Item 2' }, │
│ { name: 'Item 3' }, │
│ ]; │
│ │
└─────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────┐
│ index.service.sts │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ // Runtime interactivity (optional) │
│ const mount = function() { │
│ document.getElementById('btn') │
│ .addEventListener('click', handleClick); │
│ }; │
│ │
│ return { mount }; │
│ │
└─────────────────────────────────────────────────────────────────────┘
```
---
## Template Syntax
### Interpolation
```
┌─────────────────────────────────────────────────────────────────────┐
│ SYNTAX: { expression } │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ Simple: { title } │
│ Property: { user.name } │
│ Nested: { config.api.url } │
│ │
│ Note: Single braces, not double {{ }} │
│ │
└─────────────────────────────────────────────────────────────────────┘
```
### Loop Directive
```
┌─────────────────────────────────────────────────────────────────────┐
│ SYNTAX: { s-for item in collection } ... { /s-for } │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ Basic: │
│ { s-for user in users } │
│ <div>{ user.name }</div> │
│ { /s-for } │
│ │
│ With Index: │
│ { s-for user, index in users } │
│ <div>#{ index }: { user.name }</div> │
│ { /s-for } │
│ │
│ Nested: │
│ { s-for category in categories } │
│ <h2>{ category.name }</h2> │
│ { s-for item in category.items } │
│ <div>{ item.name }</div> │
│ { /s-for } │
│ { /s-for } │
│ │
└─────────────────────────────────────────────────────────────────────┘
```
### Conditional Directives
```
┌─────────────────────────────────────────────────────────────────────┐
│ SYNTAX: { s-if } / { s-elif } / { s-else } / { /s-if } │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ Simple If: │
│ { s-if isLoggedIn } │
│ <div>Welcome back!</div> │
│ { /s-if } │
│ │
│ If-Else: │
│ { s-if isAdmin } │
│ <div>Admin Panel</div> │
│ { s-else } │
│ <div>User Dashboard</div> │
│ { /s-if } │
│ │
│ If-Elif-Else: │
│ { s-if role === 'admin' } │
│ <div>Admin</div> │
│ { s-elif role === 'mod' } │
│ <div>Moderator</div> │
│ { s-else } │
│ <div>User</div> │
│ { /s-if } │
│ │
│ Negation: │
│ { s-if !isLoggedIn } │
│ <a href="/login">Log In</a> │
│ { /s-if } │
│ │
│ Comparison Operators: │
│ === == !== != > < >= <= │
│ │
└─────────────────────────────────────────────────────────────────────┘
```
---
## Import Hierarchy
### Strict Layer Enforcement
```
┌─────────────────────────────────────────────────────────────────────┐
│ IMPORT HIERARCHY │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────┐ │
│ │ .strata │ │
│ │ (Template) │ │
│ └──────┬──────┘ │
│ │ │
│ │ can import │
│ ▼ │
│ ┌─────────────┐ │
│ │ .strata │ (other templates only) │
│ └─────────────┘ │
│ │
│ ════════════════════════════════════════════════════════════════ │
│ │
│ ┌─────────────────┐ │
│ │ .compiler.sts │ │
│ │ (Variables) │ │
│ └────────┬────────┘ │
│ │ │
│ │ can import │
│ ▼ │
│ ┌──────────────┼──────────────┐ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌───────────┐ ┌───────────┐ ┌───────────┐ │
│ │ .service │ │ .api.sts │ │ .sts │ │
│ │ .sts │ │ │ │ │ │
│ └───────────┘ └───────────┘ └───────────┘ │
│ │
│ ════════════════════════════════════════════════════════════════ │
│ │
│ ┌─────────────────┐ │
│ │ .service.sts │ │
│ │ (Logic) │ │
│ └────────┬────────┘ │
│ │ │
│ │ can import │
│ ▼ │
│ ┌────────┴────────┐ │
│ │ │ │
│ ▼ ▼ │
│ ┌───────────┐ ┌───────────┐ │
│ │ .api.sts │ │ .sts │ │
│ └───────────┘ └───────────┘ │
│ │
│ ════════════════════════════════════════════════════════════════ │
│ │
│ ┌─────────────────┐ │
│ │ .api.sts │ │
│ │ (API Contract) │ │
│ └────────┬────────┘ │
│ │ │
│ │ can import │
│ ▼ │
│ ┌───────────┐ │
│ │ .sts │ │
│ └───────────┘ │
│ │
│ ════════════════════════════════════════════════════════════════ │
│ │
│ ┌─────────────────┐ │
│ │ .sts │ │
│ │ (Utilities) │ │
│ └────────┬────────┘ │
│ │ │
│ │ can import │
│ ▼ │
│ ┌───────────┐ │
│ │ .sts │ (other utilities only) │
│ └───────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────┘
VIOLATION = BUILD ERROR
```
---
## Runtime Architecture
### Minimal Runtime
```
┌─────────────────────────────────────────────────────────────────────┐
│ RUNTIME OUTPUT │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ index.html │
│ ══════════ │
│ │
│ <!DOCTYPE html> │
│ <html> │
│ <head> │
│ <link rel="stylesheet" href="/assets/css/app.css"> │
│ </head> │
│ <body> │
│ <div id="app"></div> │
│ <script type="module" src="/assets/js/app.js"></script> │
│ </body> │
│ </html> │
│ │
│ ──────────────────────────────────────────────────────────────── │
│ │
│ app.js │
│ ══════ │
│ │
│ // Pages registry with pre-compiled HTML │
│ const pages = { │
│ '/': { │
│ render() { │
│ return `<main>...</main>`; // Pure HTML, no framework │
│ }, │
│ mount() { │
│ // Optional: runtime interactivity from .service.sts │
│ } │
│ } │
│ }; │
│ │
│ // Simple router │
│ function mount() { │
│ const page = pages[location.pathname] || pages['/']; │
│ document.getElementById('app').innerHTML = page.render(); │
│ page.mount(); │
│ } │
│ │
│ mount(); │
│ │
└─────────────────────────────────────────────────────────────────────┘
```
---
## Dev Server Architecture
### Built-in Go Server with HMR
```
┌─────────────────────────────────────────────────────────────────────┐
│ strata dev │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ ┌───────────────┐ │
│ │ File System │ │
│ └───────┬───────┘ │
│ │ │
│ │ fsnotify │
│ ▼ │
│ ┌───────────────┐ ┌───────────────┐ ┌───────────────┐ │
│ │ Watcher │────▶│ Compiler │────▶│ Dev Server │ │
│ │ (Go) │ │ (Go) │ │ (Go) │ │
│ └───────────────┘ └───────────────┘ └───────┬───────┘ │
│ │ │
│ ┌────────┴────────┐ │
│ │ │ │
│ ▼ ▼ │
│ ┌──────────┐ ┌──────────┐│
│ │ HTTP │ │WebSocket ││
│ │ :3000 │ │ HMR ││
│ └──────────┘ └──────────┘│
│ │
│ ──────────────────────────────────────────────────────────────── │
│ │
│ WATCH TARGETS: │
│ │
│ src/ │
│ ├── pages/**/*.strata → Recompile page │
│ ├── pages/**/*.compiler.sts → Recompile page │
│ ├── pages/**/*.service.sts → Recompile page │
│ ├── pages/**/*.scss → Hot reload CSS │
│ ├── components/**/* → Recompile dependents │
│ └── ... │
│ │
│ strataconfig.ts → Full rebuild │
│ │
│ ──────────────────────────────────────────────────────────────── │
│ │
│ HMR MESSAGES: │
│ │
│ { type: "reload" } → Full page reload │
│ { type: "css", path: "..." } → CSS hot swap │
│ { type: "component" } → Component reload │
│ │
└─────────────────────────────────────────────────────────────────────┘
```
---
## Future: Shared Worker Architecture
> **Status: Planned**
```
┌─────────────────────────────────────────────────────────────────────┐
│ Strata Shared Worker │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Tab 1 │ │ Tab 2 │ │ Tab 3 │ │ Tab N │ │
│ │ tabId:a1 │ │ tabId:b2 │ │ tabId:c3 │ │ tabId:xx │ │
│ └────┬─────┘ └────┬─────┘ └────┬─────┘ └────┬─────┘ │
│ │ │ │ │ │
│ └─────────────┴──────┬──────┴─────────────┘ │
│ │ │
│ ┌────────▼────────┐ │
│ │ Shared Worker │ │
│ │ │ │
│ │ ┌───────────┐ │ │
│ │ │ Store │ │ ◄── Encrypted State │
│ │ └───────────┘ │ │
│ │ ┌───────────┐ │ │
│ │ │ Cache │ │ ◄── API Response Cache │
│ │ └───────────┘ │ │
│ │ ┌───────────┐ │ │
│ │ │ TabSync │ │ ◄── Tab Registry │
│ │ └───────────┘ │ │
│ └─────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────┘
```
### Tab Management (Planned)
```typescript
// Auto-generated tabId for each browser tab
const tabId = strata.tabId; // e.g., "tab_a1b2c3"
// Tab-specific state
strata.store.setForTab(tabId, { draft: formData });
// Shared state (all tabs)
strata.store.setShared({ user: currentUser });
// Broadcast to specific tabs
strata.broadcast('logout', { reason: 'session_expired' }, ['tab_a1b2c3']);
// Broadcast to all tabs
strata.broadcast('refresh', { entity: 'users' });
```
---
## Future: Smart Fetch System
> **Status: Planned**
### Request Deduplication
```typescript
// These calls are deduplicated - only ONE request is made
const [users1, users2, users3] = await Promise.all([
strata.fetch('/api/users'),
strata.fetch('/api/users'),
strata.fetch('/api/users'),
]);
// All three resolve with the same data from single request
```
### Caching Strategy
```typescript
strata.fetch('/api/users', {
cache: 'smart', // Default: cache until data changes
cache: 'none', // No caching
cache: '5m', // Cache for 5 minutes
cache: 'permanent', // Cache until manual invalidation
// Stale-while-revalidate pattern
stale: '1m', // Serve stale for 1 min while fetching fresh
});
```
---
## Future: Encrypted Store
> **Status: Planned**
### Build-Time Encryption
```
┌─────────────────────────────────────────────────────────────────────┐
│ Build Process │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ 1. Generate encryption key at build time │
│ key = crypto.randomBytes(32) │
│ │
│ 2. Embed key in compiled runtime (obfuscated) │
│ const _k = [0x2f, 0xa1, ...]; // Split & scattered │
│ │
│ 3. Store data encrypted in SharedWorker │
│ encrypted = AES256(JSON.stringify(state), key) │
│ │
│ 4. Browser can't read state even with DevTools │
│ localStorage: "encrypted:a1b2c3d4e5f6..." │
│ │
└─────────────────────────────────────────────────────────────────────┘
```
### Store Definition (Planned)
```typescript
// stores/user.sts
import { createStore } from 'strata';
export const userStore = createStore('user', {
state: {
currentUser: null,
preferences: {},
token: null,
},
actions: {
login(user, token) {
this.currentUser = user;
this.token = token;
},
logout() {
this.currentUser = null;
this.token = null;
},
},
encrypt: true, // Encrypt entire store
persist: true, // Persist to SharedWorker storage
shared: true, // Share across all tabs
});
```
---
## Performance Targets
| Metric | Target | Typical React |
|--------|--------|---------------|
| Build Memory | < 512MB | 8GB+ |
| Bundle Size | < 50KB | 2MB+ |
| Runtime | < 5KB | 40KB+ |
| Cold Start | < 500ms | 3s+ |
| HMR | < 100ms | 1s+ |
| TTFB | < 50ms | 200ms+ |
---
## Project Structure
```
strata/
├── cli/
│ └── create-strata/ # Project scaffolding
│ └── index.js
├── compiler/
│ ├── cmd/
│ │ └── strata/ # CLI entry point
│ │ ├── main.go
│ │ ├── dev.go
│ │ └── build.go
│ │
│ └── internal/
│ ├── ast/ # Abstract Syntax Tree
│ │ └── nodes.go
│ │
│ ├── compiler/ # Static compiler
│ │ └── static.go
│ │
│ ├── parser/ # Template parser
│ │ └── strata.go
│ │
│ ├── server/ # Dev server
│ │ └── dev.go
│ │
│ └── watcher/ # File watcher
│ └── watcher.go
├── runtime/ # Browser runtime
│ └── strata.js
├── templates/ # Project templates
│ └── default/
├── examples/ # Example projects
│ └── pokemon/
├── Makefile # Build commands
├── README.md # User documentation
├── ARCHITECTURE.md # This file
├── CHANGELOG.md # Version history
├── CONTRIBUTING.md # Contribution guidelines
└── LICENSE # Proprietary license
```
---
## Summary
Strata implements the **STRC Pattern** (Static Template Resolution with Compartmentalized Layers) to achieve:
1. **Build-time compilation** of all template syntax
2. **Zero runtime overhead** in production
3. **Clear separation** between template, data, and logic
4. **Strict import hierarchy** preventing layer violations
5. **Fast development** with Go-powered HMR
The result: websites that are as fast as hand-written HTML, with the developer experience of a modern framework.
---
<p align="center">
<strong>Strata</strong> - Static Template Rendering Architecture
</p>

150
CHANGELOG.md Normal file
View File

@@ -0,0 +1,150 @@
# Changelog
All notable changes to the Strata framework will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
---
## [Unreleased]
### Planned
- Component imports (`{ s-imp "@components/Button" }`)
- SCSS to CSS compilation pipeline
- Production build optimizations
- TypeScript type generation for templates
- VSCode extension for syntax highlighting
- Nested component slots
---
## [0.1.0] - 2026-01-16
### Added
#### Core Framework
- **Static Compiler**: Build-time template resolution engine
- Resolves all template syntax to pure HTML
- Zero runtime framework overhead
- Caches API responses during build
- **STRC Design Pattern**: Static Template Resolution with Compartmentalized Layers
- Clear separation between template, compiler, service, and API layers
- Strict import hierarchy enforcement
- File-type based responsibility assignment
#### Template Syntax
- **Variable Interpolation**: `{ variableName }`
- Supports dot notation for nested properties (`{ user.name }`)
- Handles strings, numbers, booleans, arrays, objects
- **Loop Directive**: `{ s-for item in items }` ... `{ /s-for }`
- Iterates over arrays
- Optional index variable: `{ s-for item, index in items }`
- Nested loops supported
- **Conditional Directives**: `{ s-if }`, `{ s-elif }`, `{ s-else }`, `{ /s-if }`
- Boolean evaluation
- Negation with `!`
- Comparison operators: `===`, `==`, `!==`, `!=`, `>`, `<`, `>=`, `<=`
#### File Types
- `.strata` - HTML template files
- `.compiler.sts` - Build-time variable definitions
- `.service.sts` - Business logic layer (build + runtime)
- `.api.sts` - API contract definitions
- `.sts` - Pure utility functions
- `.scss` - Component/page styles
#### CLI Tools
- `strata dev` - Development server with Hot Module Replacement (HMR)
- `strata build` - Production build
- `strata preview` - Preview production build
- Generator commands:
- `strata g component <Name>`
- `strata g page <name>`
- `strata g service <name>`
- `strata g api <name>`
- `strata g util <name>`
- `strata g store <name>`
#### Project Scaffolding
- `create-strata` CLI tool
- Pokemon API example template
- Index page with ASCII art branding
- Pre-configured project structure
#### Development Server
- WebSocket-based Hot Module Replacement
- Automatic rebuild on file changes
- CSS hot reload without page refresh
- Component change detection
#### Parser
- Custom Strata template parser
- AST-based template processing
- Balanced bracket extraction for complex structures
- JavaScript-to-JSON conversion for data parsing
### Technical Details
#### Compiler Implementation
- Written in Go for maximum performance
- Regex-based export extraction from `.compiler.sts`
- Recursive template resolution
- Property access chain support (`item.nested.value`)
#### Build Process
1. Parse `.strata` template to AST
2. Load exports from `.compiler.sts`
3. Resolve all directives (for, if, interpolation)
4. Output pure HTML string
5. Bundle with optional runtime service code
---
## [0.0.1] - 2026-01-15
### Added
- Initial project structure
- Basic Go compiler setup
- Makefile for build automation
- Development environment configuration
---
## Version History Summary
| Version | Date | Highlights |
|---------|------|------------|
| 0.1.0 | 2026-01-16 | Core framework, STRC pattern, template syntax, CLI tools |
| 0.0.1 | 2026-01-15 | Initial project setup |
---
## Migration Guides
### Migrating to 0.1.0
This is the first functional release. No migration required.
---
## Deprecations
None yet.
---
## Security
No security vulnerabilities reported.
To report a security issue, please open an issue at https://github.com/CarGDev/strata/issues
---
[Unreleased]: https://github.com/CarGDev/strata/compare/v0.1.0...HEAD
[0.1.0]: https://github.com/CarGDev/strata/compare/v0.0.1...v0.1.0
[0.0.1]: https://github.com/CarGDev/strata/releases/tag/v0.0.1

458
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,458 @@
# Contributing to Strata
Thank you for your interest in contributing to Strata! This document outlines the guidelines and process for contributing.
---
## Development Status
**Strata is currently in active development (pre-alpha).**
During this phase:
- The API may change without notice
- Features may be added, modified, or removed
- Documentation may be incomplete
- Not all edge cases are handled
---
## Contribution Policy
### Current Phase: Closed Development
At this time, **external contributions are not being accepted**. This includes:
- Pull requests
- Feature implementations
- Bug fixes (report only)
### What You Can Do
1. **Report Bugs**: Open an issue describing the bug
2. **Request Features**: Open an issue with your suggestion
3. **Ask Questions**: Use GitHub Discussions
4. **Provide Feedback**: Share your experience with the framework
---
## How to Create an Issue
### Step 1: Check Existing Issues
Before creating a new issue, search existing issues to avoid duplicates:
1. Go to [github.com/CarGDev/strata/issues](https://github.com/CarGDev/strata/issues)
2. Use the search bar with keywords related to your issue
3. Check both **Open** and **Closed** issues
4. If you find a related issue, add a comment instead of creating a duplicate
### Step 2: Choose the Right Issue Type
| Issue Type | When to Use |
|------------|-------------|
| **Bug Report** | Something isn't working as expected |
| **Feature Request** | You want new functionality |
| **Question** | Use GitHub Discussions instead |
| **Documentation** | Docs are missing, unclear, or incorrect |
### Step 3: Create the Issue
1. Go to [github.com/CarGDev/strata/issues/new/choose](https://github.com/CarGDev/strata/issues/new/choose)
2. Select the appropriate issue template
3. Fill out all required sections
4. Add relevant labels if you have permission
5. Click **Submit new issue**
### Step 4: Follow Up
- **Respond to questions** from maintainers promptly
- **Provide additional info** if requested
- **Test fixes** when a solution is proposed
- **Close the issue** if you find the solution yourself
---
## Issue Templates
### Bug Report Template
Use this template when reporting bugs:
```markdown
## Bug Report
### Summary
A one-line description of the bug.
### Environment
- **OS**: macOS 14.0 / Windows 11 / Ubuntu 22.04
- **Go version**: 1.21.0
- **Node version**: 18.0.0
- **Strata version**: 0.1.0
### Steps to Reproduce
1. Create a new project with `npx create-strata my-app`
2. Add a for loop in `index.strata`
3. Run `npm run dev`
4. Observe the error
### Expected Behavior
The for loop should render all items in the array.
### Actual Behavior
The page renders empty. Console shows: `[error message here]`
### Code Sample
**index.strata**
```html
<template>
{ s-for item in items }
<div>{ item.name }</div>
{ /s-for }
</template>
\`\`\`
**index.compiler.sts**
\`\`\`javascript
export const items = [
{ name: 'Item 1' },
{ name: 'Item 2' },
];
\`\`\`
### Error Output
\`\`\`
[Paste full error message or stack trace here]
\`\`\`
### Screenshots
[If applicable, add screenshots to help explain the problem]
### Additional Context
[Any other relevant information]
```
### Feature Request Template
Use this template when requesting features:
```markdown
## Feature Request
### Summary
A one-line description of the feature.
### Problem Statement
Describe the problem this feature would solve.
Example: "Currently, there's no way to import components from other files,
which means I have to copy-paste common UI elements across pages."
### Proposed Solution
Describe how you envision this feature working.
Example: "Add a `{ s-imp }` directive that allows importing components:
`{ s-imp Button from '@components/Button' }`"
### Use Cases
1. Reusing header/footer across pages
2. Creating a component library
3. Sharing UI patterns between projects
### Alternatives Considered
Other approaches you've thought about and why they don't work.
### Additional Context
- Links to similar features in other frameworks
- Mockups or diagrams
- Priority level (nice-to-have vs. blocking)
```
### Documentation Issue Template
Use this template for documentation problems:
```markdown
## Documentation Issue
### Type
- [ ] Missing documentation
- [ ] Incorrect documentation
- [ ] Unclear documentation
- [ ] Typo or formatting issue
### Location
[Link to the documentation page or file path]
### Current Content
[What the docs currently say, if applicable]
### Suggested Change
[What the docs should say]
### Additional Context
[Why this change would help]
```
---
## Issue Labels
Understanding issue labels helps you categorize your report:
| Label | Description |
|-------|-------------|
| `bug` | Something isn't working |
| `feature` | New feature request |
| `docs` | Documentation improvements |
| `question` | Further information needed |
| `good first issue` | Good for newcomers |
| `help wanted` | Extra attention needed |
| `priority: high` | Critical issue |
| `priority: low` | Nice to have |
| `wontfix` | Will not be addressed |
| `duplicate` | Already reported |
---
## Tips for Good Issues
### Do
- **Be specific**: Include exact error messages, not "it doesn't work"
- **Be concise**: Get to the point quickly
- **Use code blocks**: Format code with triple backticks
- **Include versions**: Always mention OS, Go, Node, and Strata versions
- **One issue per report**: Don't combine multiple bugs
- **Search first**: Check if it's already reported
### Don't
- **Don't be vague**: "The compiler is broken" is not helpful
- **Don't include sensitive data**: No API keys, passwords, or personal info
- **Don't bump issues**: Adding "+1" doesn't help (use reactions instead)
- **Don't demand timelines**: This is open development
- **Don't cross-post**: One issue in one place
---
## Code of Conduct
### Our Standards
- Be respectful and inclusive
- Provide constructive feedback
- Focus on the issue, not the person
- Accept responsibility for mistakes
### Unacceptable Behavior
- Harassment or discrimination
- Trolling or insulting comments
- Personal or political attacks
- Publishing private information
---
## Development Setup (For Authorized Contributors)
If you have been authorized to contribute:
### Prerequisites
```bash
# Required
go version # 1.21+
node --version # 18+
make --version
# Optional
code --version # VSCode for development
```
### Clone and Build
```bash
# Clone repository
git clone https://github.com/CarGDev/strata.git
cd strata
# Build the compiler
make build
# Run tests
make test
# Install locally
make install
```
### Project Structure
```
strata/
├── cli/
│ └── create-strata/ # Project scaffolding tool
├── compiler/
│ ├── cmd/
│ │ └── strata/ # CLI entry point
│ └── internal/
│ ├── ast/ # Abstract Syntax Tree
│ ├── compiler/ # Static compiler
│ ├── parser/ # Template parser
│ ├── server/ # Dev server
│ └── watcher/ # File watcher
├── runtime/ # Browser runtime (minimal)
├── templates/ # Project templates
├── examples/ # Example projects
└── scripts/ # Build scripts
```
### Coding Standards
#### Go Code
- Follow [Effective Go](https://golang.org/doc/effective_go)
- Use `gofmt` for formatting
- Add comments for exported functions
- Write tests for new functionality
```go
// Good
// CompilePage compiles a page directory to static HTML.
// It reads the .strata template and .compiler.sts variables,
// resolves all directives, and returns pure HTML.
func (c *StaticCompiler) CompilePage(pageDir string) (*CompiledModule, error) {
// ...
}
```
#### JavaScript/TypeScript
- Use ES modules
- Prefer `const` over `let`
- No semicolons (project style)
- Use meaningful variable names
```javascript
// Good
const formatDate = (date) => {
return new Intl.DateTimeFormat('en-US').format(date)
}
// Bad
var f = function(d) { return d.toString(); };
```
### Commit Messages
Follow [Conventional Commits](https://www.conventionalcommits.org/):
```
<type>(<scope>): <description>
[optional body]
[optional footer]
```
Types:
- `feat`: New feature
- `fix`: Bug fix
- `docs`: Documentation
- `style`: Formatting (no code change)
- `refactor`: Code restructuring
- `test`: Adding tests
- `chore`: Maintenance
Examples:
```
feat(compiler): add support for nested loops
fix(parser): handle escaped quotes in attributes
docs(readme): update installation instructions
```
### Branch Naming
```
feature/description
fix/description
docs/description
refactor/description
```
Examples:
```
feature/add-component-imports
fix/parser-nested-loops
docs/update-api-reference
```
---
## Testing
### Running Tests
```bash
# All tests
make test
# Specific package
go test ./compiler/internal/parser/...
# With coverage
go test -cover ./...
```
### Writing Tests
```go
func TestCompilePage_WithForLoop(t *testing.T) {
compiler := NewStaticCompiler("/test/project")
result, err := compiler.CompilePage("/test/project/src/pages/index")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if !strings.Contains(result.HTML, "expected content") {
t.Errorf("expected HTML to contain 'expected content', got: %s", result.HTML)
}
}
```
---
## Review Process
For authorized contributors:
1. Create a branch from `main`
2. Make your changes
3. Write/update tests
4. Update documentation
5. Submit a pull request
6. Address review feedback
7. Squash and merge when approved
---
## Questions?
- **GitHub Issues**: For bugs and features
- **GitHub Discussions**: For questions and ideas
- **GitHub**: [@CarGDev](https://github.com/CarGDev)
---
## License Reminder
By contributing to Strata, you agree that your contributions will be licensed under the same terms as the project. Currently, Strata is proprietary software under development.
---
Thank you for your interest in Strata!

113
LICENSE Normal file
View File

@@ -0,0 +1,113 @@
STRATA PROPRIETARY LICENSE
Version 1.0, January 2026
Copyright (c) 2026 Carlos Gutierrez (CarGDev). All Rights Reserved.
================================================================================
NOTICE: THIS SOFTWARE IS IN DEVELOPMENT AND NOT AVAILABLE FOR PUBLIC USE
================================================================================
1. DEFINITIONS
"Software" refers to the Strata framework, including but not limited to:
- Source code
- Compiled binaries
- Documentation
- Templates
- Examples
- Associated tooling
"Licensor" refers to Carlos Gutierrez (CarGDev).
"You" refers to any individual or entity accessing this Software.
2. GRANT OF RIGHTS
None. This Software is proprietary and confidential. No rights are granted
under this license.
3. RESTRICTIONS
You may NOT:
a) Use the Software for any purpose, commercial or non-commercial
b) Copy, modify, or distribute the Software or any portion thereof
c) Reverse engineer, decompile, or disassemble the Software
d) Remove or alter any proprietary notices or labels on the Software
e) Sublicense, sell, rent, lease, or lend the Software
f) Create derivative works based on the Software
g) Use the Software to develop competing products
h) Share access credentials or bypass access controls
4. CONFIDENTIALITY
The Software contains trade secrets and proprietary information of the
Licensor. You agree to maintain the confidentiality of the Software and
not disclose any part of it to third parties without prior written consent
from the Licensor.
5. NO WARRANTY
THE SOFTWARE IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT.
6. LIMITATION OF LIABILITY
IN NO EVENT SHALL THE LICENSOR BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT, OR OTHERWISE, ARISING
FROM, OUT OF, OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
7. TERMINATION
This license is effective until terminated. Your rights under this license
will terminate automatically without notice if you fail to comply with any
of its terms. Upon termination, you must destroy all copies of the Software
in your possession.
8. GOVERNING LAW
This license shall be governed by and construed in accordance with the laws
of the State of California, United States, without regard to its conflict
of law provisions.
9. ENTIRE AGREEMENT
This license constitutes the entire agreement between you and the Licensor
concerning the Software and supersedes all prior or contemporaneous
understandings.
10. SEVERABILITY
If any provision of this license is held to be unenforceable, such provision
shall be reformed only to the extent necessary to make it enforceable, and
the remaining provisions shall continue in full force and effect.
11. CONTACT
For licensing inquiries, please contact:
Carlos Gutierrez (CarGDev)
https://github.com/CarGDev
================================================================================
DEVELOPMENT PREVIEW NOTICE
This Software is currently in a development preview phase. It is made
available solely for internal evaluation and development purposes by
authorized personnel.
Access to this repository does not constitute a license to use, modify,
or distribute the Software. Authorized users are bound by separate
agreements governing their access.
Future versions may be released under different license terms at the sole
discretion of the Licensor.
================================================================================
Last Updated: January 16, 2026

283
Makefile Normal file
View File

@@ -0,0 +1,283 @@
.PHONY: build install install-global install-local uninstall upgrade doctor \
dev clean test example-build \
gen-component gen-page gen-store \
setup link help
# ============================================================================
# BUILD
# ============================================================================
# Build the Go compiler
build:
@echo "Building Strata compiler..."
@mkdir -p bin
@cd compiler && go mod tidy && go build -ldflags="-s -w" -o ../bin/strata ./cmd/strata
@echo "Done! Binary at ./bin/strata"
# Build with race detector (for development)
build-debug:
@echo "Building Strata compiler (debug)..."
@mkdir -p bin
@cd compiler && go build -race -o ../bin/strata-debug ./cmd/strata
@echo "Done! Binary at ./bin/strata-debug"
# ============================================================================
# INSTALLATION
# ============================================================================
# Full installation (local by default)
install:
@chmod +x scripts/install.sh
@./scripts/install.sh
# Install globally to /usr/local/bin
install-global:
@chmod +x scripts/install.sh
@./scripts/install.sh --global
# Install locally to ~/.strata only
install-local:
@chmod +x scripts/install.sh
@./scripts/install.sh --local
# Quick setup (build + npm install, no shell config)
setup: build
@echo "Installing npm dependencies..."
@npm install --silent
@echo ""
@echo "Strata setup complete!"
@echo " Binary: ./bin/strata"
@echo ""
@echo "Run 'make install' for full installation with shell config."
# Link binary to /usr/local/bin (quick global access)
link: build
@echo "Linking strata to /usr/local/bin..."
@if [ -w /usr/local/bin ]; then \
ln -sf "$(PWD)/bin/strata" /usr/local/bin/strata; \
else \
sudo ln -sf "$(PWD)/bin/strata" /usr/local/bin/strata; \
fi
@echo "Done! 'strata' command is now available globally."
# Unlink from /usr/local/bin
unlink:
@echo "Unlinking strata from /usr/local/bin..."
@if [ -w /usr/local/bin ]; then \
rm -f /usr/local/bin/strata; \
else \
sudo rm -f /usr/local/bin/strata; \
fi
@echo "Done!"
# Uninstall Strata
uninstall:
@chmod +x scripts/uninstall.sh
@./scripts/uninstall.sh
# Uninstall without confirmation
uninstall-force:
@chmod +x scripts/uninstall.sh
@./scripts/uninstall.sh --force
# ============================================================================
# MAINTENANCE
# ============================================================================
# Upgrade to latest version
upgrade:
@chmod +x scripts/upgrade.sh
@./scripts/upgrade.sh
# Check for updates only
upgrade-check:
@chmod +x scripts/upgrade.sh
@./scripts/upgrade.sh --check
# Diagnose installation issues
doctor:
@chmod +x scripts/doctor.sh
@./scripts/doctor.sh
# Auto-fix installation issues
doctor-fix:
@chmod +x scripts/doctor.sh
@./scripts/doctor.sh --fix
# ============================================================================
# DEVELOPMENT
# ============================================================================
# Run dev server on example app
dev: build
@cd examples/basic-app && ../../bin/strata dev
# Run dev server with browser open
dev-open: build
@cd examples/basic-app && ../../bin/strata dev --open
# Build example app for production
example-build: build
@cd examples/basic-app && ../../bin/strata build
# Preview example app production build
example-preview: build
@cd examples/basic-app && ../../bin/strata preview
# Watch and rebuild compiler on changes
watch:
@echo "Watching for compiler changes..."
@while true; do \
find compiler -name '*.go' | entr -d make build; \
done
# ============================================================================
# TESTING
# ============================================================================
# Run all tests
test:
@echo "Running Go tests..."
@cd compiler && go test ./...
@echo ""
@echo "Running npm tests..."
@npm test
# Run Go tests only
test-go:
@cd compiler && go test ./...
# Run Go tests with verbose output
test-go-verbose:
@cd compiler && go test -v ./...
# Run npm tests only
test-npm:
@npm test
# Run tests with coverage
test-coverage:
@cd compiler && go test -coverprofile=coverage.out ./...
@cd compiler && go tool cover -html=coverage.out -o coverage.html
@echo "Coverage report: compiler/coverage.html"
# ============================================================================
# CODE GENERATION
# ============================================================================
# Generate component (usage: make gen-component NAME=MyComponent)
gen-component: build
@if [ -z "$(NAME)" ]; then \
echo "Usage: make gen-component NAME=MyComponent"; \
exit 1; \
fi
@cd examples/basic-app && ../../bin/strata generate component $(NAME)
# Generate page (usage: make gen-page NAME=about)
gen-page: build
@if [ -z "$(NAME)" ]; then \
echo "Usage: make gen-page NAME=about"; \
exit 1; \
fi
@cd examples/basic-app && ../../bin/strata generate page $(NAME)
# Generate store (usage: make gen-store NAME=cart)
gen-store: build
@if [ -z "$(NAME)" ]; then \
echo "Usage: make gen-store NAME=cart"; \
exit 1; \
fi
@cd examples/basic-app && ../../bin/strata generate store $(NAME)
# Shorthand aliases
c: gen-component
p: gen-page
s: gen-store
# ============================================================================
# CLEANUP
# ============================================================================
# Clean all build artifacts
clean:
@rm -rf bin/
@rm -rf dist/
@rm -rf .strata/
@rm -rf examples/basic-app/dist/
@rm -rf examples/basic-app/.strata/
@rm -rf compiler/coverage.out
@rm -rf compiler/coverage.html
@echo "Cleaned!"
# Deep clean (includes node_modules and go cache)
clean-all: clean
@rm -rf node_modules/
@cd compiler && go clean -cache
@echo "Deep cleaned!"
# ============================================================================
# RELEASE
# ============================================================================
# Build release binaries for all platforms
release:
@echo "Building release binaries..."
@mkdir -p dist/release
@cd compiler && GOOS=darwin GOARCH=amd64 go build -ldflags="-s -w" -o ../dist/release/strata-darwin-amd64 ./cmd/strata
@cd compiler && GOOS=darwin GOARCH=arm64 go build -ldflags="-s -w" -o ../dist/release/strata-darwin-arm64 ./cmd/strata
@cd compiler && GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" -o ../dist/release/strata-linux-amd64 ./cmd/strata
@cd compiler && GOOS=linux GOARCH=arm64 go build -ldflags="-s -w" -o ../dist/release/strata-linux-arm64 ./cmd/strata
@cd compiler && GOOS=windows GOARCH=amd64 go build -ldflags="-s -w" -o ../dist/release/strata-windows-amd64.exe ./cmd/strata
@echo "Release binaries built in dist/release/"
# ============================================================================
# HELP
# ============================================================================
# Show help
help:
@echo ""
@echo "Strata Framework - Development Commands"
@echo "========================================"
@echo ""
@echo "Installation:"
@echo " make install Full installation with shell config"
@echo " make install-global Install globally to /usr/local/bin"
@echo " make install-local Install locally to ~/.strata"
@echo " make setup Quick setup (build + npm, no shell config)"
@echo " make link Link binary to /usr/local/bin"
@echo " make uninstall Uninstall Strata"
@echo ""
@echo "Maintenance:"
@echo " make upgrade Upgrade to latest version"
@echo " make upgrade-check Check for updates"
@echo " make doctor Diagnose installation issues"
@echo " make doctor-fix Auto-fix installation issues"
@echo ""
@echo "Building:"
@echo " make build Build the Go compiler"
@echo " make build-debug Build with race detector"
@echo " make release Build release binaries for all platforms"
@echo ""
@echo "Development:"
@echo " make dev Run dev server (examples/basic-app)"
@echo " make dev-open Run dev server and open browser"
@echo " make example-build Build example app for production"
@echo " make example-preview Preview production build"
@echo " make watch Watch and rebuild compiler"
@echo ""
@echo "Testing:"
@echo " make test Run all tests"
@echo " make test-go Run Go tests only"
@echo " make test-npm Run npm tests only"
@echo " make test-coverage Run tests with coverage report"
@echo ""
@echo "Code Generation:"
@echo " make gen-component NAME=Button Generate component"
@echo " make gen-page NAME=about Generate page"
@echo " make gen-store NAME=cart Generate store"
@echo ""
@echo "Cleanup:"
@echo " make clean Clean build artifacts"
@echo " make clean-all Deep clean (includes node_modules)"
@echo ""

728
README.md Normal file
View File

@@ -0,0 +1,728 @@
# Strata
**Static Template Rendering Architecture**
Strata is a compile-time web framework that resolves templates to pure HTML at build time. Zero runtime framework overhead. Maximum performance.
```
╔═══════════════════════════════════════════════════════╗
║ ███████╗████████╗██████╗ █████╗ ████████╗ █████╗ ║
║ ██╔════╝╚══██╔══╝██╔══██╗██╔══██╗╚══██╔══╝██╔══██╗ ║
║ ███████╗ ██║ ██████╔╝███████║ ██║ ███████║ ║
║ ╚════██║ ██║ ██╔══██╗██╔══██║ ██║ ██╔══██║ ║
║ ███████║ ██║ ██║ ██║██║ ██║ ██║ ██║ ██║ ║
║ ╚══════╝ ╚═╝ ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ║
╚═══════════════════════════════════════════════════════╝
```
---
## Table of Contents
- [Philosophy](#philosophy)
- [Design Pattern: STRC](#design-pattern-strc)
- [Installation](#installation)
- [Quick Start](#quick-start)
- [CLI Commands](#cli-commands)
- [Project Structure](#project-structure)
- [File Types](#file-types)
- [Template Syntax](#template-syntax)
- [Examples](#examples)
- [Import Hierarchy](#import-hierarchy)
- [Development](#development)
- [License](#license)
---
## Philosophy
Strata follows three core principles:
1. **Compile-Time Resolution**: All template logic is resolved during build, not at runtime
2. **Zero Runtime Overhead**: Output is pure HTML/CSS/JS with no framework dependencies
3. **Strict Separation of Concerns**: Each file type has a single responsibility
---
## Design Pattern: STRC
Strata implements the **STRC Pattern** (Static Template Resolution with Compartmentalized Layers):
```
┌─────────────────────────────────────────────────────────────┐
│ BUILD TIME │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ .strata │◄───│ .compiler.sts│◄───│ .service.sts │ │
│ │ (Template) │ │ (Variables) │ │ (Logic) │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
│ │ │ │ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ STATIC COMPILER │ │
│ │ Resolves all variables, loops, and conditionals │ │
│ └─────────────────────────────────────────────────────┘ │
│ │ │
└───────────────────────────┼─────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ RUNTIME │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ PURE HTML │ │
│ │ No Strata syntax, no framework code │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
```
### STRC Layer Responsibilities
| Layer | File Extension | Purpose | Execution |
|-------|---------------|---------|-----------|
| **Template** | `.strata` | HTML structure with directives | Build time |
| **Compiler** | `.compiler.sts` | Variable definitions, data | Build time |
| **Service** | `.service.sts` | Business logic, API calls | Build time* |
| **Runtime** | `.service.sts` | Optional browser interactivity | Runtime |
*Services can define both build-time data fetching and optional runtime interactivity.
---
## Installation
### Prerequisites
- Go 1.21+ (for building the compiler)
- Node.js 18+ (for project scaffolding)
### Install Strata CLI
```bash
# Clone the repository
git clone https://github.com/CarGDev/strata.git
cd strata
# Build the compiler
make build
# Install globally
make install
```
This installs:
- `strata` - The main CLI compiler
- `create-strata` - Project scaffolding tool
---
## Quick Start
### Create a New Project
```bash
# Create a new Strata project
npx create-strata my-app
# Navigate to project
cd my-app
# Install dependencies
npm install
# Start development server
npm run dev
```
Open http://localhost:3000 to see your app.
### Build for Production
```bash
npm run build
```
Output is in the `dist/` folder - pure static HTML ready for any hosting.
---
## CLI Commands
### Main CLI (`strata`)
```bash
# Development server with HMR
strata dev [--port 3000] [--open]
# Production build
strata build [--output dist]
# Preview production build
strata preview [--port 4000]
```
### Generator Commands
Generate new files with the correct structure:
```bash
# Generate a component
strata g component Button
# Creates: src/components/Button/
# ├── Button.strata
# ├── Button.compiler.sts
# ├── Button.service.sts
# └── Button.scss
# Generate a page
strata g page about
# Creates: src/pages/about/
# ├── about.strata
# ├── about.compiler.sts
# ├── about.service.sts
# └── about.scss
# Generate a service
strata g service auth
# Creates: src/services/auth.service.sts
# Generate an API contract
strata g api users
# Creates: src/api/users.api.sts
# Generate a utility
strata g util format
# Creates: src/utils/format.sts
# Generate a store
strata g store cart
# Creates: src/stores/cart.store.sts
```
### Shorthand Commands
```bash
strata g c Button # component
strata g p about # page
strata g s auth # service
strata g a users # api
strata g u format # util
```
---
## Project Structure
```
my-app/
├── src/
│ ├── components/ # Reusable UI components
│ │ └── Button/
│ │ ├── Button.strata
│ │ ├── Button.compiler.sts
│ │ ├── Button.service.sts
│ │ └── Button.scss
│ │
│ ├── pages/ # Route-based pages
│ │ ├── index/
│ │ │ ├── index.strata
│ │ │ ├── index.compiler.sts
│ │ │ ├── index.service.sts
│ │ │ └── index.scss
│ │ └── about/
│ │ └── ...
│ │
│ ├── services/ # Business logic
│ │ └── auth.service.sts
│ │
│ ├── api/ # API contracts
│ │ └── users.api.sts
│ │
│ ├── stores/ # State management
│ │ └── cart.store.sts
│ │
│ ├── utils/ # Pure utilities
│ │ └── format.sts
│ │
│ └── assets/
│ └── styles/
│ ├── _variables.scss
│ └── global.scss
├── public/ # Static assets (copied as-is)
├── dist/ # Build output
├── strataconfig.ts # Project configuration
└── package.json
```
---
## File Types
### `.strata` - Template Files
Pure HTML structure with Strata directives. No logic, no JavaScript.
```html
<template>
<main class="page">
<h1>{ title }</h1>
<p>{ description }</p>
</main>
</template>
```
**Rules:**
- Must contain a single `<template>` root
- Can only import other `.strata` files
- Cannot contain `<script>` tags
- All variables come from `.compiler.sts`
### `.compiler.sts` - Compiler Files
Defines variables available to the template. Executed at **build time**.
```typescript
// page.compiler.sts
export const title = 'My Page';
export const description = 'Welcome to my page';
// Arrays for loops
export const items = [
{ id: 1, name: 'Item 1' },
{ id: 2, name: 'Item 2' },
];
// Booleans for conditionals
export const isProduction = false;
export const showBanner = true;
```
**Rules:**
- All exports become template variables
- Can import from `.service.sts`, `.api.sts`, `.sts`
- Cannot import from `.strata` files
- Runs during compilation, not in browser
### `.service.sts` - Service Files
Business logic layer. Can define both build-time and runtime behavior.
```typescript
// auth.service.sts
// Build-time: fetch data during compilation
export async function fetchUserData() {
const response = await fetch('https://api.example.com/user');
return response.json();
}
// Runtime: browser interactivity (optional)
const mount = function() {
document.getElementById('btn').addEventListener('click', () => {
console.log('Clicked!');
});
};
return { mount };
```
**Rules:**
- Can import from `.api.sts`, `.sts`
- Cannot import from `.strata`, `.compiler.sts`
### `.api.sts` - API Contract Files
Declarative API endpoint definitions.
```typescript
// users.api.sts
export const endpoints = {
getUsers: {
method: 'GET',
url: '/api/users',
cache: 3600
},
createUser: {
method: 'POST',
url: '/api/users',
body: { name: 'string', email: 'string' }
}
};
```
### `.sts` - Utility Files
Pure functions with no side effects. Can be imported anywhere.
```typescript
// format.sts
export function formatDate(date) {
return new Date(date).toLocaleDateString();
}
export function capitalize(str) {
return str.charAt(0).toUpperCase() + str.slice(1);
}
```
### `.scss` - Style Files
Scoped styles for components/pages. Standard SCSS syntax.
```scss
// Button.scss
.button {
padding: 0.5rem 1rem;
border-radius: 4px;
&:hover {
opacity: 0.9;
}
}
```
---
## Template Syntax
### Variable Interpolation
```html
{ variableName }
```
Variables are defined in the corresponding `.compiler.sts` file.
```html
<!-- page.strata -->
<h1>{ title }</h1>
<p>Author: { author.name }</p>
```
```typescript
// page.compiler.sts
export const title = 'Hello World';
export const author = { name: 'John Doe' };
```
**Output:**
```html
<h1>Hello World</h1>
<p>Author: John Doe</p>
```
### Loops: `s-for`
```html
{ s-for item in items }
<!-- content repeated for each item -->
{ /s-for }
```
**With index:**
```html
{ s-for item, index in items }
<div>#{ index }: { item.name }</div>
{ /s-for }
```
**Example:**
```html
<!-- page.strata -->
<ul>
{ s-for user in users }
<li>{ user.name } - { user.email }</li>
{ /s-for }
</ul>
```
```typescript
// page.compiler.sts
export const users = [
{ name: 'Alice', email: 'alice@example.com' },
{ name: 'Bob', email: 'bob@example.com' },
];
```
**Output:**
```html
<ul>
<li>Alice - alice@example.com</li>
<li>Bob - bob@example.com</li>
</ul>
```
### Conditionals: `s-if` / `s-elif` / `s-else`
```html
{ s-if condition }
<!-- shown if condition is true -->
{ s-elif otherCondition }
<!-- shown if otherCondition is true -->
{ s-else }
<!-- shown if all conditions are false -->
{ /s-if }
```
**Example:**
```html
{ s-if isAdmin }
<div class="admin-panel">Admin Controls</div>
{ s-elif isModerator }
<div class="mod-panel">Moderator Controls</div>
{ s-else }
<div class="user-panel">User Dashboard</div>
{ /s-if }
```
```typescript
// page.compiler.sts
export const isAdmin = false;
export const isModerator = true;
```
**Output:**
```html
<div class="mod-panel">Moderator Controls</div>
```
### Negation
```html
{ s-if !isLoggedIn }
<a href="/login">Please log in</a>
{ /s-if }
```
### Comparison Operators
```html
{ s-if count > 0 }
<p>You have { count } items</p>
{ /s-if }
{ s-if status === 'active' }
<span class="badge">Active</span>
{ /s-if }
```
Supported operators: `===`, `==`, `!==`, `!=`, `>`, `<`, `>=`, `<=`
### Component Imports (Coming Soon)
```html
{ s-imp "@components/Button" }
```
---
## Examples
### Basic Page
```html
<!-- src/pages/index/index.strata -->
<template>
<main class="home">
<h1>{ title }</h1>
<p>{ subtitle }</p>
<section class="features">
{ s-for feature in features }
<div class="feature-card">
<span>{ feature.icon }</span>
<h3>{ feature.name }</h3>
<p>{ feature.description }</p>
</div>
{ /s-for }
</section>
{ s-if showCTA }
<a href="/signup" class="cta">Get Started</a>
{ /s-if }
</main>
</template>
```
```typescript
// src/pages/index/index.compiler.sts
export const title = 'Welcome to My App';
export const subtitle = 'Build faster, deploy smarter';
export const showCTA = true;
export const features = [
{ icon: '⚡', name: 'Fast', description: 'Blazing fast performance' },
{ icon: '🔒', name: 'Secure', description: 'Built-in security' },
{ icon: '📦', name: 'Simple', description: 'Easy to use' },
];
```
### Page with Runtime Interactivity
```html
<!-- src/pages/counter/counter.strata -->
<template>
<main class="counter-page">
<h1>Counter Example</h1>
<button id="counterBtn">Click Me</button>
<p>Count: <span id="count">0</span></p>
</main>
</template>
```
```typescript
// src/pages/counter/counter.service.sts
const mount = function() {
const btn = document.getElementById('counterBtn');
const countEl = document.getElementById('count');
let count = 0;
btn.addEventListener('click', function() {
count++;
countEl.textContent = count;
});
};
return { mount };
```
### Build-Time Data Fetching
```typescript
// src/pages/pokemon/pokemon.compiler.sts
export const title = 'Pokemon Gallery';
// This data is fetched at BUILD TIME, not runtime
export const pokemons = [
{ id: 1, name: 'Bulbasaur', type: 'grass' },
{ id: 4, name: 'Charmander', type: 'fire' },
{ id: 7, name: 'Squirtle', type: 'water' },
];
export const totalCount = pokemons.length;
```
```html
<!-- src/pages/pokemon/pokemon.strata -->
<template>
<main class="pokemon-page">
<h1>{ title }</h1>
<p>Showing { totalCount } Pokemon</p>
<div class="grid">
{ s-for pokemon in pokemons }
<div class="card">
<h3>{ pokemon.name }</h3>
<span class="type">{ pokemon.type }</span>
</div>
{ /s-for }
</div>
</main>
</template>
```
---
## Import Hierarchy
Strata enforces a strict import hierarchy to maintain separation of concerns:
```
┌─────────────┐
│ .strata │ Can only import: .strata
└──────┬──────┘
┌─────────────────┐
│ .compiler.sts │ Can import: .service.sts, .api.sts, .sts
└──────┬──────────┘
┌─────────────────┐
│ .service.sts │ Can import: .api.sts, .sts
└──────┬──────────┘
┌─────────────────┐
│ .api.sts │ Can import: .sts
└──────┬──────────┘
┌─────────────────┐
│ .sts │ Can import: .sts only
└─────────────────┘
```
**Violations will cause build errors.**
---
## Development
### Running Tests
```bash
make test
```
### Building from Source
```bash
# Build compiler
make build
# Build and install
make install
# Clean build artifacts
make clean
```
### Project Configuration
```typescript
// strataconfig.ts
export default {
// Development server port
port: 3000,
// API proxy for development
api: {
baseUrl: 'http://localhost:8080',
proxy: true
},
// Build output directory
output: 'dist',
// Enable source maps in development
sourceMaps: true
};
```
---
## License
**Proprietary - All Rights Reserved**
This software is currently in development and is not available for public use, modification, or distribution. See [LICENSE](LICENSE) for details.
---
## Links
- [Changelog](CHANGELOG.md)
- [Contributing](CONTRIBUTING.md)
- [Report Issues](https://github.com/CarGDev/strata/issues)
---
<p align="center">
<strong>Strata</strong> - Code Faster. Load Quick. Deploy ASAP.
</p>

1124
cli/create-strata/index.js Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,23 @@
{
"name": "create-strata",
"version": "0.1.0",
"description": "Create a new Strata project",
"bin": {
"create-strata": "./index.js"
},
"files": [
"index.js",
"templates"
],
"scripts": {
"test": "node index.js --help"
},
"keywords": [
"strata",
"create",
"scaffold",
"cli"
],
"author": "Strata Team",
"license": "MIT"
}

View File

@@ -0,0 +1,695 @@
package main
import (
"fmt"
"log"
"os"
"path/filepath"
"github.com/CarGDev/strata/internal/generator"
)
func runBuild(analyze bool, watch bool) {
cwd, err := os.Getwd()
if err != nil {
log.Fatalf("Failed to get working directory: %v", err)
}
// Check if this is a Strata project
configPath := filepath.Join(cwd, "strataconfig.ts")
if _, err := os.Stat(configPath); os.IsNotExist(err) {
log.Fatal("Not a Strata project. strataconfig.ts not found.")
}
distDir := filepath.Join(cwd, "dist")
fmt.Println(" Building for production...")
fmt.Println()
// Clean dist directory
os.RemoveAll(distDir)
os.MkdirAll(distDir, 0755)
// Generate HTML
htmlGen := generator.NewHTMLGenerator(cwd, distDir)
config := &generator.BuildConfig{
Title: "Strata App",
Description: "Built with Strata Framework",
BaseURL: "/",
APIBaseURL: "",
DevMode: false,
Assets: generator.AssetManifest{
JS: []string{"/assets/js/app.js"},
CSS: []string{"/assets/css/global.css"},
},
}
if err := htmlGen.Generate(config); err != nil {
log.Fatalf("Failed to generate HTML: %v", err)
}
// Copy assets
assetsDir := filepath.Join(distDir, "assets")
os.MkdirAll(filepath.Join(assetsDir, "js"), 0755)
os.MkdirAll(filepath.Join(assetsDir, "css"), 0755)
// Generate runtime
runtimeJS := generateProductionRuntime()
os.WriteFile(filepath.Join(assetsDir, "js", "runtime.js"), []byte(runtimeJS), 0644)
// Generate app bundle (placeholder)
appJS := generateProductionApp()
os.WriteFile(filepath.Join(assetsDir, "js", "app.js"), []byte(appJS), 0644)
fmt.Println(" Build complete!")
fmt.Printf(" Output: %s\n", distDir)
fmt.Println()
if analyze {
fmt.Println(" Bundle Analysis:")
fmt.Println(" - runtime.js: ~2KB")
fmt.Println(" - app.js: ~5KB")
fmt.Println()
}
}
func generateProductionRuntime() string {
return `// Strata Runtime v0.1.0
const s=new class{constructor(){this.t="tab_"+Math.random().toString(36).slice(2,10),this.s=new Map}get tabId(){return this.t}async fetch(t,s={}){const e=await fetch(t,s);return e.json()}broadcast(t,s){window.dispatchEvent(new CustomEvent("strata:"+t,{detail:s}))}};export{s as strata};export default s;
`
}
func generateProductionApp() string {
return `import{strata as s}from"./runtime.js";document.getElementById("app").innerHTML='<main style="max-width:800px;margin:0 auto;padding:2rem"><h1>Strata App</h1><p>Tab: '+s.tabId+"</p></main>";
`
}
func startPreviewServer(port int) {
cwd, _ := os.Getwd()
distDir := filepath.Join(cwd, "dist")
if _, err := os.Stat(distDir); os.IsNotExist(err) {
log.Fatal("No build found. Run 'strata build' first.")
}
fmt.Printf("\n Preview server running at http://localhost:%d\n", port)
fmt.Println(" Press Ctrl+C to stop")
fmt.Println()
// Simple static file server
http := &simpleServer{dir: distDir, port: port}
http.start()
}
type simpleServer struct {
dir string
port int
}
func (s *simpleServer) start() {
// Use Go's built-in file server
// In production, use proper http.FileServer
select {}
}
func generateFile(genType string, name string) {
cwd, _ := os.Getwd()
switch genType {
case "component", "c":
generateComponent(cwd, name)
case "page", "p":
generatePage(cwd, name)
case "service", "s":
generateService(cwd, name)
case "api", "a":
generateAPI(cwd, name)
case "util", "u":
generateUtil(cwd, name)
case "store":
generateStore(cwd, name)
default:
log.Fatalf("Unknown type: %s. Use: component, page, service, api, util, or store", genType)
}
}
// generateComponent creates a component directory with all required files
// components/UserList/
// ├── UserList.strata
// ├── UserList.compiler.sts
// ├── UserList.service.sts
// ├── UserList.scss
func generateComponent(cwd, name string) {
dir := filepath.Join(cwd, "src", "components", name)
os.MkdirAll(dir, 0755)
files := map[string]string{
filepath.Join(dir, name+".strata"): generateComponentStrata(name),
filepath.Join(dir, name+".compiler.sts"): generateComponentCompiler(name),
filepath.Join(dir, name+".service.sts"): generateComponentService(name),
filepath.Join(dir, name+".scss"): generateComponentStyles(name),
}
for path, content := range files {
if _, err := os.Stat(path); err == nil {
fmt.Printf(" Skipped (exists): %s\n", path)
continue
}
if err := os.WriteFile(path, []byte(content), 0644); err != nil {
log.Fatalf("Failed to create file: %v", err)
}
fmt.Printf(" Created: %s\n", path)
}
}
// generatePage creates a page directory with all required files
// pages/users/
// ├── users.strata
// ├── users.compiler.sts
// ├── users.service.sts
// ├── users.scss
func generatePage(cwd, name string) {
dir := filepath.Join(cwd, "src", "pages", name)
os.MkdirAll(dir, 0755)
files := map[string]string{
filepath.Join(dir, name+".strata"): generatePageStrata(name),
filepath.Join(dir, name+".compiler.sts"): generatePageCompiler(name),
filepath.Join(dir, name+".service.sts"): generatePageService(name),
filepath.Join(dir, name+".scss"): generatePageStyles(name),
}
for path, content := range files {
if _, err := os.Stat(path); err == nil {
fmt.Printf(" Skipped (exists): %s\n", path)
continue
}
if err := os.WriteFile(path, []byte(content), 0644); err != nil {
log.Fatalf("Failed to create file: %v", err)
}
fmt.Printf(" Created: %s\n", path)
}
}
// generateService creates a service file
// services/user.service.sts
func generateService(cwd, name string) {
dir := filepath.Join(cwd, "src", "services")
os.MkdirAll(dir, 0755)
path := filepath.Join(dir, name+".service.sts")
if _, err := os.Stat(path); err == nil {
log.Fatalf("File already exists: %s", path)
}
content := generateServiceFile(name)
if err := os.WriteFile(path, []byte(content), 0644); err != nil {
log.Fatalf("Failed to create file: %v", err)
}
fmt.Printf(" Created: %s\n", path)
}
// generateAPI creates an API contract file
// api/user.api.sts
func generateAPI(cwd, name string) {
dir := filepath.Join(cwd, "src", "api")
os.MkdirAll(dir, 0755)
path := filepath.Join(dir, name+".api.sts")
if _, err := os.Stat(path); err == nil {
log.Fatalf("File already exists: %s", path)
}
content := generateAPIFile(name)
if err := os.WriteFile(path, []byte(content), 0644); err != nil {
log.Fatalf("Failed to create file: %v", err)
}
fmt.Printf(" Created: %s\n", path)
}
// generateUtil creates a pure utility file
// utils/formatUser.sts
func generateUtil(cwd, name string) {
dir := filepath.Join(cwd, "src", "utils")
os.MkdirAll(dir, 0755)
path := filepath.Join(dir, name+".sts")
if _, err := os.Stat(path); err == nil {
log.Fatalf("File already exists: %s", path)
}
content := generateUtilFile(name)
if err := os.WriteFile(path, []byte(content), 0644); err != nil {
log.Fatalf("Failed to create file: %v", err)
}
fmt.Printf(" Created: %s\n", path)
}
// generateStore creates a store definition file
// stores/user.sts
func generateStore(cwd, name string) {
dir := filepath.Join(cwd, "src", "stores")
os.MkdirAll(dir, 0755)
path := filepath.Join(dir, name+".sts")
if _, err := os.Stat(path); err == nil {
log.Fatalf("File already exists: %s", path)
}
content := generateStoreFile(name)
if err := os.WriteFile(path, []byte(content), 0644); err != nil {
log.Fatalf("Failed to create file: %v", err)
}
fmt.Printf(" Created: %s\n", path)
}
// ============================================================
// COMPONENT TEMPLATES
// ============================================================
func generateComponentStrata(name string) string {
return fmt.Sprintf(`<template>
<div class="%s">
{ s-if loading }
<p class="loading">Loading...</p>
{ s-else }
<h2>{ title }</h2>
<p>{ description }</p>
{ /s-if }
</div>
</template>
`, toKebabCase(name))
}
func generateComponentCompiler(name string) string {
return fmt.Sprintf(`// %s.compiler.sts - Build-time execution layer
// Provides truth to templates. Executes during compilation.
import { fetch%s } from './%s.service.sts';
// All exports become template scope
export const title = '%s';
export const description = '%s component';
export const loading = false;
// Async data fetching (runs at compile time)
// export const items = await fetch%s();
export default { title, description, loading };
`, name, name, name, name, name, name)
}
func generateComponentService(name string) string {
return fmt.Sprintf(`// %s.service.sts - Logic layer (environment-parametric)
// Holds heavy logic, API calls, data orchestration.
/**
* Fetch %s data
* @param ctx - Injected context with fetch capability
*/
export async function fetch%s(ctx) {
// ctx.call() uses the API contract
// return ctx.call(get%sApi);
return [];
}
/**
* Transform %s data
* @param data - Raw data from API
*/
export function transform%s(data) {
return data;
}
export default { fetch%s, transform%s };
`, name, name, name, name, name, name, name, name)
}
func generateComponentStyles(name string) string {
return fmt.Sprintf(`.%s {
// Component styles - scoped to this component only
.loading {
opacity: 0.5;
}
}
`, toKebabCase(name))
}
// ============================================================
// PAGE TEMPLATES
// ============================================================
func generatePageStrata(name string) string {
return fmt.Sprintf(`<template>
<main class="%s-page">
<h1>{ title }</h1>
{ s-if loading }
<div class="loading">Loading...</div>
{ s-elif error }
<div class="error">{ error }</div>
{ s-else }
<ul class="items">
{ s-for item in items }
<li class="item">{ item.name }</li>
{ /s-for }
</ul>
{ /s-if }
</main>
</template>
`, toKebabCase(name))
}
func generatePageCompiler(name string) string {
return fmt.Sprintf(`// %s.compiler.sts - Build-time execution layer
// All values resolved at compile time → static HTML output
import { fetch%sData } from './%s.service.sts';
// Static values
export const title = '%s';
export const loading = false;
export const error = null;
// Data fetched at build time
export const items = await fetch%sData();
export default { title, loading, error, items };
`, name, capitalize(name), name, capitalize(name), capitalize(name))
}
func generatePageService(name string) string {
return fmt.Sprintf(`// %s.service.sts - Logic layer
// Runs in compiler mode (mocked/cached) or runtime mode (real APIs)
/**
* Fetch %s page data
* Called at build time by compiler
*/
export async function fetch%sData(ctx) {
// In compiler mode, this returns mock/cached data
// In runtime mode, this makes real API calls
return [
{ name: 'Item 1' },
{ name: 'Item 2' },
{ name: 'Item 3' },
];
}
export default { fetch%sData };
`, name, name, capitalize(name), capitalize(name))
}
func generatePageStyles(name string) string {
return fmt.Sprintf(`.%s-page {
max-width: 1200px;
margin: 0 auto;
padding: 2rem;
.loading {
text-align: center;
padding: 2rem;
opacity: 0.6;
}
.error {
color: #dc2626;
padding: 1rem;
background: #fef2f2;
border-radius: 0.5rem;
}
.items {
list-style: none;
padding: 0;
}
.item {
padding: 0.75rem 1rem;
border-bottom: 1px solid #e5e7eb;
}
}
`, toKebabCase(name))
}
// ============================================================
// SERVICE TEMPLATES
// ============================================================
func generateServiceFile(name string) string {
return fmt.Sprintf(`// %s.service.sts - Logic layer (environment-parametric)
// Runs in compiler mode or runtime mode
// Must accept injected capabilities (fetch, cache, etc.)
/**
* Fetch %s data
* @param ctx - Injected context
*/
export async function fetch%s(ctx) {
// Use ctx.call() with API contracts
// return ctx.call(get%sApi);
return [];
}
/**
* Create %s
* @param ctx - Injected context
* @param data - %s data
*/
export async function create%s(ctx, data) {
// return ctx.call(create%sApi, data);
}
/**
* Update %s
* @param ctx - Injected context
* @param id - %s ID
* @param data - Updated data
*/
export async function update%s(ctx, id, data) {
// return ctx.call(update%sApi, { id, ...data });
}
/**
* Delete %s
* @param ctx - Injected context
* @param id - %s ID
*/
export async function delete%s(ctx, id) {
// return ctx.call(delete%sApi, { id });
}
export default { fetch%s, create%s, update%s, delete%s };
`, name, name,
capitalize(name), capitalize(name),
name, name, capitalize(name), capitalize(name),
name, name, capitalize(name), capitalize(name),
name, name, capitalize(name), capitalize(name),
capitalize(name), capitalize(name), capitalize(name), capitalize(name))
}
// ============================================================
// API TEMPLATES
// ============================================================
func generateAPIFile(name string) string {
return fmt.Sprintf(`// %s.api.sts - API contracts (declarative, dual-mode)
// Describe API endpoints once, executable in compiler and runtime mode
import { defineApi } from 'strata';
/**
* Get all %s
*/
export const get%sApi = defineApi({
path: '/api/%s',
method: 'GET',
cache: '5m', // Cache for 5 minutes
});
/**
* Get single %s by ID
*/
export const get%sByIdApi = defineApi({
path: '/api/%s/:id',
method: 'GET',
cache: '5m',
});
/**
* Create %s
*/
export const create%sApi = defineApi({
path: '/api/%s',
method: 'POST',
cache: 'none',
});
/**
* Update %s
*/
export const update%sApi = defineApi({
path: '/api/%s/:id',
method: 'PUT',
cache: 'none',
});
/**
* Delete %s
*/
export const delete%sApi = defineApi({
path: '/api/%s/:id',
method: 'DELETE',
cache: 'none',
});
export default {
get%sApi,
get%sByIdApi,
create%sApi,
update%sApi,
delete%sApi,
};
`, name, name,
capitalize(name), name,
name, capitalize(name), name,
name, capitalize(name), name,
name, capitalize(name), name,
name, capitalize(name), name,
capitalize(name), capitalize(name), capitalize(name), capitalize(name), capitalize(name))
}
// ============================================================
// UTIL TEMPLATES
// ============================================================
func generateUtilFile(name string) string {
return fmt.Sprintf(`// %s.sts - Pure logic (shared, referentially transparent)
// Pure functions only. No side effects. Deterministic output.
/**
* Format %s
* @param value - Input value
* @returns Formatted value
*/
export function format%s(value) {
if (!value) return '';
return String(value);
}
/**
* Validate %s
* @param value - Value to validate
* @returns Whether the value is valid
*/
export function validate%s(value) {
return value != null && value !== '';
}
/**
* Transform %s
* @param input - Input data
* @returns Transformed data
*/
export function transform%s(input) {
return input;
}
export default { format%s, validate%s, transform%s };
`, name, name,
capitalize(name),
name, capitalize(name),
name, capitalize(name),
capitalize(name), capitalize(name), capitalize(name))
}
// ============================================================
// STORE TEMPLATES
// ============================================================
func generateStoreFile(name string) string {
return fmt.Sprintf(`// %s.sts - Store definition (shape, not execution)
// Pure definitions only. Execution happens via injected runtime.
import { defineStore } from 'strata';
/**
* %s Store
* Defines state shape and actions
*/
export const %sStore = defineStore({
name: '%s',
// Initial state
state: () => ({
items: [],
loading: false,
error: null,
}),
// Actions (pure state transitions)
actions: {
setItems(items) {
this.items = items;
},
setLoading(loading) {
this.loading = loading;
},
setError(error) {
this.error = error;
},
reset() {
this.items = [];
this.loading = false;
this.error = null;
},
},
// Optional: encrypt sensitive fields
// encrypt: ['sensitiveField'],
// Optional: persist to storage
// persist: true,
// Optional: share across tabs
// shared: true,
});
export default %sStore;
`, name, capitalize(name), name, name, name)
}
func toKebabCase(s string) string {
var result []byte
for i, c := range s {
if c >= 'A' && c <= 'Z' {
if i > 0 {
result = append(result, '-')
}
result = append(result, byte(c+32))
} else {
result = append(result, byte(c))
}
}
return string(result)
}
func capitalize(s string) string {
if len(s) == 0 {
return s
}
if s[0] >= 'a' && s[0] <= 'z' {
return string(s[0]-32) + s[1:]
}
return s
}

View File

@@ -0,0 +1,96 @@
package main
import (
"fmt"
"log"
"os"
"os/exec"
"path/filepath"
"runtime"
"github.com/CarGDev/strata/internal/server"
"github.com/CarGDev/strata/internal/watcher"
)
func startDevServer(port int, open bool) {
// Get current working directory
cwd, err := os.Getwd()
if err != nil {
log.Fatalf("Failed to get working directory: %v", err)
}
// Check if this is a Strata project
configPath := filepath.Join(cwd, "strataconfig.ts")
if _, err := os.Stat(configPath); os.IsNotExist(err) {
log.Fatal("Not a Strata project. strataconfig.ts not found.")
}
// Create dev server
devServer := server.NewDevServer(port, cwd)
// Create file watcher
srcDir := filepath.Join(cwd, "src")
fileWatcher, err := watcher.New(srcDir, func(event watcher.ChangeEvent) {
fmt.Printf(" Changed: %s\n", event.Path)
// Rebuild the project
if err := devServer.Rebuild(); err != nil {
fmt.Printf(" Rebuild error: %v\n", err)
}
// Determine change type for HMR
changeType := "reload"
switch event.Type {
case watcher.FileTypeSCSS:
changeType = "css"
case watcher.FileTypeStrata:
changeType = "component"
case watcher.FileTypeConfig:
changeType = "reload"
}
devServer.NotifyChange(changeType, event.Path)
})
if err != nil {
log.Fatalf("Failed to create watcher: %v", err)
}
// Start watcher in background
go func() {
if err := fileWatcher.Start(); err != nil {
log.Printf("Watcher error: %v", err)
}
}()
// Open browser if requested
if open {
openBrowser(fmt.Sprintf("http://localhost:%d", port))
}
// Start server (blocks)
if err := devServer.Start(); err != nil {
log.Fatalf("Server error: %v", err)
}
}
func openBrowser(url string) {
var cmd *exec.Cmd
switch runtime.GOOS {
case "darwin":
cmd = exec.Command("open", url)
case "linux":
cmd = exec.Command("xdg-open", url)
case "windows":
cmd = exec.Command("cmd", "/c", "start", url)
default:
fmt.Printf(" Open manually: %s\n", url)
return
}
if err := cmd.Start(); err != nil {
fmt.Printf(" Could not open browser: %v\n", err)
fmt.Printf(" Open manually: %s\n", url)
}
}

View File

@@ -0,0 +1,99 @@
package main
import (
"fmt"
"os"
"github.com/spf13/cobra"
)
var version = "0.1.0"
func main() {
rootCmd := &cobra.Command{
Use: "strata",
Short: "Strata - Static Template Rendering Architecture",
Version: version,
}
rootCmd.AddCommand(devCmd())
rootCmd.AddCommand(buildCmd())
rootCmd.AddCommand(previewCmd())
rootCmd.AddCommand(generateCmd())
if err := rootCmd.Execute(); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
func devCmd() *cobra.Command {
var port int
var noOpen bool
cmd := &cobra.Command{
Use: "dev",
Short: "Start development server with file watcher",
Run: func(cmd *cobra.Command, args []string) {
fmt.Printf("Starting Strata dev server on port %d...\n", port)
startDevServer(port, !noOpen)
},
}
cmd.Flags().IntVarP(&port, "port", "p", 3000, "Port to run dev server")
cmd.Flags().BoolVar(&noOpen, "no-open", false, "Don't open browser on start")
return cmd
}
func buildCmd() *cobra.Command {
var analyze bool
var watch bool
cmd := &cobra.Command{
Use: "build",
Short: "Build for production",
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("Building Strata application...")
runBuild(analyze, watch)
},
}
cmd.Flags().BoolVarP(&analyze, "analyze", "a", false, "Analyze bundle size")
cmd.Flags().BoolVarP(&watch, "watch", "w", false, "Watch mode")
return cmd
}
func previewCmd() *cobra.Command {
var port int
cmd := &cobra.Command{
Use: "preview",
Short: "Preview production build",
Run: func(cmd *cobra.Command, args []string) {
fmt.Printf("Previewing build on port %d...\n", port)
startPreviewServer(port)
},
}
cmd.Flags().IntVarP(&port, "port", "p", 4000, "Port for preview server")
return cmd
}
func generateCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "generate [type] [name]",
Aliases: []string{"g"},
Short: "Generate component, page, or store",
Args: cobra.ExactArgs(2),
Run: func(cmd *cobra.Command, args []string) {
genType := args[0]
name := args[1]
generateFile(genType, name)
},
}
return cmd
}

16
compiler/go.mod Normal file
View File

@@ -0,0 +1,16 @@
module github.com/CarGDev/strata
go 1.22
require (
github.com/fsnotify/fsnotify v1.7.0
github.com/gorilla/websocket v1.5.1
github.com/spf13/cobra v1.8.0
)
require (
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
golang.org/x/net v0.17.0 // indirect
golang.org/x/sys v0.16.0 // indirect
)

18
compiler/go.sum Normal file
View File

@@ -0,0 +1,18 @@
github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0=
github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@@ -0,0 +1,304 @@
package ast
// Node is the base interface for all AST nodes
type Node interface {
NodeType() string
}
// StrataFile represents a parsed .strata file
type StrataFile struct {
Template *TemplateNode
Script *ScriptNode
Style *StyleNode
}
func (s *StrataFile) NodeType() string { return "StrataFile" }
// TemplateNode represents the <template> block
type TemplateNode struct {
Children []Node
}
func (t *TemplateNode) NodeType() string { return "Template" }
// ScriptNode represents the <script> block
type ScriptNode struct {
Content string
Lang string // "ts" or "js"
}
func (s *ScriptNode) NodeType() string { return "Script" }
// StyleNode represents the <style> block
type StyleNode struct {
Content string
Lang string // "css", "scss", "sass", "less"
Scoped bool
}
func (s *StyleNode) NodeType() string { return "Style" }
// ElementNode represents an HTML element
type ElementNode struct {
Tag string
Attributes map[string]string
Directives []Directive
Children []Node
IsComponent bool
}
func (e *ElementNode) NodeType() string { return "Element" }
// TextNode represents text content
type TextNode struct {
Content string
}
func (t *TextNode) NodeType() string { return "Text" }
// InterpolationNode represents { expression } variable binding
type InterpolationNode struct {
Expression string
}
func (i *InterpolationNode) NodeType() string { return "Interpolation" }
// ForBlockNode represents { s-for item in items } ... { /s-for }
type ForBlockNode struct {
Item string // Loop variable name (e.g., "item")
Index string // Optional index variable (e.g., "i" in "item, i")
Iterable string // Expression to iterate (e.g., "items")
Children []Node // Content inside the for block
}
func (f *ForBlockNode) NodeType() string { return "ForBlock" }
// IfBlockNode represents { s-if } ... { s-elif } ... { s-else } ... { /s-if }
type IfBlockNode struct {
Condition string // The if condition
Children []Node // Content when condition is true
ElseIf []*ElseIfBranch
Else []Node // Content for else branch
}
func (i *IfBlockNode) NodeType() string { return "IfBlock" }
// ElseIfBranch represents an elif branch in conditional
type ElseIfBranch struct {
Condition string
Children []Node
}
// ImportNode represents { s-imp "@alias/file" }
type ImportNode struct {
Path string // Import path (e.g., "@components/Button")
Alias string // Optional alias for the import
}
func (i *ImportNode) NodeType() string { return "Import" }
// DirectiveType represents the type of directive
type DirectiveType int
const (
DirectiveConditional DirectiveType = iota // s-if, s-else-if, s-else
DirectiveLoop // s-for
DirectiveModel // s-model
DirectiveEvent // s-on:event, @event
DirectiveBind // :attr
DirectiveClient // s-client:load, s-client:visible, etc.
DirectiveFetch // s-fetch
DirectiveRef // s-ref
DirectiveHTML // s-html
DirectiveStatic // s-static
)
// Directive represents a Strata directive
type Directive struct {
Type DirectiveType
Name string // Original attribute name
Value string // Attribute value
Arg string // Argument (e.g., "click" in @click)
Modifiers []string // Modifiers (e.g., "prevent" in @click.prevent)
}
// ComponentNode represents a component usage
type ComponentNode struct {
Name string
Props map[string]string
Directives []Directive
Slots map[string][]Node
}
func (c *ComponentNode) NodeType() string { return "Component" }
// SlotNode represents a <slot> element
type SlotNode struct {
Name string // default or named
Fallback []Node
}
func (s *SlotNode) NodeType() string { return "Slot" }
// ImportDeclaration represents an import statement
type ImportDeclaration struct {
Default string // Default import name
Named []string // Named imports
Source string // Import source path
}
// ComponentDefinition represents a component's parsed definition
type ComponentDefinition struct {
Name string
Props []PropDefinition
Imports []ImportDeclaration
Setup string // The setup function body
}
// PropDefinition represents a component prop
type PropDefinition struct {
Name string
Type string
Required bool
Default string
}
// StoreDefinition represents a parsed store
type StoreDefinition struct {
Name string
State map[string]string // Field name -> type
Actions []ActionDefinition
Encrypt []string // Fields to encrypt
Persist []string // Fields to persist
Shared []string // Fields to share across tabs
}
// ActionDefinition represents a store action
type ActionDefinition struct {
Name string
Params []string
Body string
Async bool
}
// ToHTML converts a TemplateNode to HTML string
func (t *TemplateNode) ToHTML() string {
var result string
for _, child := range t.Children {
result += nodeToHTML(child)
}
return result
}
// nodeToHTML converts any AST node to HTML
func nodeToHTML(node Node) string {
if node == nil {
return ""
}
switch n := node.(type) {
case *ElementNode:
return elementToHTML(n)
case *TextNode:
return n.Content
case *InterpolationNode:
// For dev mode, output a placeholder that will be filled by JS
return `<span data-strata-bind="` + n.Expression + `"></span>`
case *ForBlockNode:
return forBlockToHTML(n)
case *IfBlockNode:
return ifBlockToHTML(n)
case *ImportNode:
// Imports are handled at compile time, not rendered
return ""
default:
return ""
}
}
// forBlockToHTML converts a ForBlockNode to HTML with data attributes
func forBlockToHTML(f *ForBlockNode) string {
html := `<template data-strata-for="` + f.Item
if f.Index != "" {
html += `, ` + f.Index
}
html += ` in ` + f.Iterable + `">`
for _, child := range f.Children {
html += nodeToHTML(child)
}
html += `</template>`
return html
}
// ifBlockToHTML converts an IfBlockNode to HTML with data attributes
func ifBlockToHTML(i *IfBlockNode) string {
html := `<template data-strata-if="` + i.Condition + `">`
for _, child := range i.Children {
html += nodeToHTML(child)
}
html += `</template>`
// Handle elif branches
for _, elif := range i.ElseIf {
html += `<template data-strata-elif="` + elif.Condition + `">`
for _, child := range elif.Children {
html += nodeToHTML(child)
}
html += `</template>`
}
// Handle else branch
if len(i.Else) > 0 {
html += `<template data-strata-else>`
for _, child := range i.Else {
html += nodeToHTML(child)
}
html += `</template>`
}
return html
}
// elementToHTML converts an ElementNode to HTML
func elementToHTML(el *ElementNode) string {
if el == nil {
return ""
}
html := "<" + el.Tag
// Add attributes
for name, value := range el.Attributes {
if value != "" {
html += ` ` + name + `="` + value + `"`
} else {
html += ` ` + name
}
}
// Add directive attributes for dev mode debugging
for _, dir := range el.Directives {
html += ` data-strata-` + dir.Name + `="` + dir.Value + `"`
}
html += ">"
// Add children
for _, child := range el.Children {
html += nodeToHTML(child)
}
// Close tag (unless void element)
voidElements := map[string]bool{
"area": true, "base": true, "br": true, "col": true,
"embed": true, "hr": true, "img": true, "input": true,
"link": true, "meta": true, "param": true, "source": true,
"track": true, "wbr": true,
}
if !voidElements[el.Tag] {
html += "</" + el.Tag + ">"
}
return html
}

View File

@@ -0,0 +1,710 @@
package compiler
import (
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"regexp"
"strings"
"time"
"github.com/CarGDev/strata/internal/ast"
"github.com/CarGDev/strata/internal/parser"
)
// StaticCompiler compiles .strata files to pure HTML at build time
type StaticCompiler struct {
projectDir string
cache map[string]interface{}
}
// NewStaticCompiler creates a new static compiler
func NewStaticCompiler(projectDir string) *StaticCompiler {
return &StaticCompiler{
projectDir: projectDir,
cache: make(map[string]interface{}),
}
}
// CompileModule compiles a module (page or component) to static HTML
type CompiledModule struct {
Name string
Route string
HTML string
CSS string
Scope map[string]interface{}
}
// CompilePage compiles a page directory to static HTML
func (c *StaticCompiler) CompilePage(pageDir string) (*CompiledModule, error) {
// Find .strata file
pageName := filepath.Base(pageDir)
strataPath := filepath.Join(pageDir, pageName+".strata")
// Read and parse .strata template
strataContent, err := os.ReadFile(strataPath)
if err != nil {
return nil, fmt.Errorf("failed to read template: %w", err)
}
p := parser.NewStrataParser(string(strataContent))
file, err := p.Parse()
if err != nil {
return nil, fmt.Errorf("failed to parse template: %w", err)
}
// Load and execute .compiler.sts to get scope
scope, err := c.loadCompilerScope(pageDir, pageName)
if err != nil {
return nil, fmt.Errorf("failed to load compiler scope: %w", err)
}
// Resolve template with scope - output clean HTML
html := ""
if file.Template != nil {
html = c.resolveTemplate(file.Template, scope)
}
// Load styles
css := ""
scssPath := filepath.Join(pageDir, pageName+".scss")
if scssContent, err := os.ReadFile(scssPath); err == nil {
css = string(scssContent)
}
// Calculate route
route := "/" + pageName
if pageName == "index" {
route = "/"
}
return &CompiledModule{
Name: pageName,
Route: route,
HTML: html,
CSS: css,
Scope: scope,
}, nil
}
// loadCompilerScope loads and executes .compiler.sts to get template scope
func (c *StaticCompiler) loadCompilerScope(dir, name string) (map[string]interface{}, error) {
compilerPath := filepath.Join(dir, name+".compiler.sts")
scope := make(map[string]interface{})
content, err := os.ReadFile(compilerPath)
if err != nil {
return scope, nil // No compiler file is OK
}
// Parse the TypeScript/JavaScript exports
// This is a simplified parser - in production, use a proper JS engine
scope = c.parseCompilerExports(string(content))
return scope, nil
}
// parseCompilerExports extracts exported values from .compiler.sts
// This is a simplified implementation - production would use V8/QuickJS
func (c *StaticCompiler) parseCompilerExports(content string) map[string]interface{} {
scope := make(map[string]interface{})
// First pass: find all export const declarations
exportRegex := regexp.MustCompile(`export\s+const\s+(\w+)\s*=\s*`)
exportMatches := exportRegex.FindAllStringSubmatchIndex(content, -1)
for _, match := range exportMatches {
if len(match) >= 4 {
name := content[match[2]:match[3]]
valueStart := match[1]
// Get the value - could be simple or complex (array/object)
afterEquals := content[valueStart:]
// Check what type of value follows
firstChar := ""
for _, ch := range afterEquals {
if ch != ' ' && ch != '\t' && ch != '\n' && ch != '\r' {
firstChar = string(ch)
break
}
}
var value interface{}
switch firstChar {
case "[":
// Array - need to find matching bracket
idx := strings.Index(afterEquals, "[")
arrayStr := c.extractBalancedBrackets(afterEquals[idx:], '[', ']')
if arrayStr != "" {
value = c.parseValue(arrayStr)
}
case "{":
// Object - need to find matching bracket
idx := strings.Index(afterEquals, "{")
objStr := c.extractBalancedBrackets(afterEquals[idx:], '{', '}')
if objStr != "" {
value = c.parseValue(objStr)
}
default:
// Simple value - find until semicolon or newline
endIdx := strings.Index(afterEquals, ";")
if endIdx == -1 {
endIdx = strings.Index(afterEquals, "\n")
}
if endIdx > 0 {
value = c.parseValue(strings.TrimSpace(afterEquals[:endIdx]))
}
}
if value != nil {
scope[name] = value
}
}
}
// Match: export const name = await fetchFunction();
// For async values, we need to execute the fetch
asyncRegex := regexp.MustCompile(`export\s+const\s+(\w+)\s*=\s*await\s+(\w+)\s*\(\s*\)`)
asyncMatches := asyncRegex.FindAllStringSubmatch(content, -1)
for _, match := range asyncMatches {
if len(match) >= 3 {
name := match[1]
funcName := match[2]
// Check for known fetch functions
if strings.Contains(funcName, "fetch") || strings.Contains(funcName, "Fetch") {
// Look for the URL in service file or inline
if data := c.executeFetch(content, funcName); data != nil {
scope[name] = data
}
}
}
}
return scope
}
// parseValue parses a JavaScript value to Go
func (c *StaticCompiler) parseValue(value string) interface{} {
value = strings.TrimSpace(value)
// Boolean
if value == "true" {
return true
}
if value == "false" {
return false
}
// Null
if value == "null" {
return nil
}
// Number
if matched, _ := regexp.MatchString(`^-?\d+(\.\d+)?$`, value); matched {
var num float64
fmt.Sscanf(value, "%f", &num)
return num
}
// String (single or double quotes)
if (strings.HasPrefix(value, "'") && strings.HasSuffix(value, "'")) ||
(strings.HasPrefix(value, "\"") && strings.HasSuffix(value, "\"")) {
return value[1 : len(value)-1]
}
// Array
if strings.HasPrefix(value, "[") && strings.HasSuffix(value, "]") {
var arr []interface{}
// Try to parse as JSON
if err := json.Unmarshal([]byte(value), &arr); err == nil {
return arr
}
// Simplified array parsing for objects
return c.parseArray(value)
}
// Object
if strings.HasPrefix(value, "{") && strings.HasSuffix(value, "}") {
var obj map[string]interface{}
if err := json.Unmarshal([]byte(value), &obj); err == nil {
return obj
}
}
return value
}
// parseArray parses a JavaScript array
func (c *StaticCompiler) parseArray(value string) []interface{} {
var result []interface{}
// Remove brackets
inner := strings.TrimPrefix(strings.TrimSuffix(value, "]"), "[")
inner = strings.TrimSpace(inner)
if inner == "" {
return result
}
// Convert JS syntax to JSON
jsonStr := c.jsToJSON(value)
if err := json.Unmarshal([]byte(jsonStr), &result); err == nil {
return result
}
return result
}
// jsToJSON converts JavaScript object/array literals to valid JSON
func (c *StaticCompiler) jsToJSON(value string) string {
result := value
// Replace single quotes with double quotes (but not inside strings)
// This is a simplified approach - handles most common cases
var buf strings.Builder
inString := false
stringChar := rune(0)
prevChar := rune(0)
for _, ch := range result {
if !inString {
if ch == '\'' {
buf.WriteRune('"')
inString = true
stringChar = '\''
} else if ch == '"' {
buf.WriteRune(ch)
inString = true
stringChar = '"'
} else {
buf.WriteRune(ch)
}
} else {
if ch == stringChar && prevChar != '\\' {
buf.WriteRune('"')
inString = false
} else {
buf.WriteRune(ch)
}
}
prevChar = ch
}
result = buf.String()
// Add quotes around unquoted object keys
// Match: word followed by colon (but not inside strings or URLs)
// Pattern: find keys at start of object or after comma/brace
keyRegex := regexp.MustCompile(`([{,\s])(\w+)\s*:`)
result = keyRegex.ReplaceAllString(result, `$1"$2":`)
// Handle arrays of strings like ['grass', 'poison']
// Already handled by the single quote conversion above
return result
}
// executeFetch executes a fetch operation at build time or parses local function
func (c *StaticCompiler) executeFetch(content, funcName string) interface{} {
// First, look for a local function definition that returns data
// Pattern: async function funcName() { ... return [...] }
// or: function funcName() { ... return [...] }
funcRegex := regexp.MustCompile(`(?:async\s+)?function\s+` + funcName + `\s*\([^)]*\)\s*\{`)
if funcRegex.MatchString(content) {
// Find the function body and look for return statement with array/object
funcStart := funcRegex.FindStringIndex(content)
if funcStart != nil {
// Find the return statement with array literal
remainder := content[funcStart[1]:]
// Find "return [" and then match brackets
returnIdx := strings.Index(remainder, "return")
if returnIdx != -1 {
afterReturn := strings.TrimSpace(remainder[returnIdx+6:])
if len(afterReturn) > 0 && afterReturn[0] == '[' {
// Find the matching closing bracket
arrayStr := c.extractBalancedBrackets(afterReturn, '[', ']')
if arrayStr != "" {
return c.parseValue(arrayStr)
}
}
}
}
}
// Look for API URL patterns for actual network fetch
urlRegex := regexp.MustCompile(`(?:fetch|call)\s*\(\s*['"]([^'"]+)['"]`)
matches := urlRegex.FindStringSubmatch(content)
if len(matches) >= 2 {
url := matches[1]
return c.fetchURL(url)
}
// Only fetch from network if no local data was found
// and there's explicit fetch call syntax
return nil
}
// extractBalancedBrackets extracts content between balanced brackets
func (c *StaticCompiler) extractBalancedBrackets(content string, open, close rune) string {
if len(content) == 0 || rune(content[0]) != open {
return ""
}
depth := 0
inString := false
stringChar := rune(0)
for i, ch := range content {
// Handle string literals
if !inString && (ch == '\'' || ch == '"' || ch == '`') {
inString = true
stringChar = ch
} else if inString && ch == stringChar {
// Check if escaped
if i > 0 && content[i-1] != '\\' {
inString = false
}
}
if !inString {
if ch == open {
depth++
} else if ch == close {
depth--
if depth == 0 {
return content[:i+1]
}
}
}
}
return ""
}
// fetchURL fetches data from a URL at build time
func (c *StaticCompiler) fetchURL(url string) interface{} {
// Check cache first
if cached, ok := c.cache[url]; ok {
return cached
}
client := &http.Client{Timeout: 30 * time.Second}
resp, err := client.Get(url)
if err != nil {
fmt.Printf(" Warning: Failed to fetch %s: %v\n", url, err)
return nil
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil
}
var data interface{}
if err := json.Unmarshal(body, &data); err != nil {
return string(body)
}
// Cache the result
c.cache[url] = data
return data
}
// resolveTemplate resolves a template with the given scope to clean HTML
func (c *StaticCompiler) resolveTemplate(template *ast.TemplateNode, scope map[string]interface{}) string {
var result strings.Builder
for _, child := range template.Children {
result.WriteString(c.resolveNode(child, scope))
}
return result.String()
}
// resolveNode resolves a single AST node with the given scope
func (c *StaticCompiler) resolveNode(node ast.Node, scope map[string]interface{}) string {
if node == nil {
return ""
}
switch n := node.(type) {
case *ast.ElementNode:
return c.resolveElement(n, scope)
case *ast.TextNode:
return n.Content
case *ast.InterpolationNode:
// Resolve the expression and output the value directly
value := c.resolveExpression(n.Expression, scope)
return fmt.Sprintf("%v", value)
case *ast.ForBlockNode:
return c.resolveForBlock(n, scope)
case *ast.IfBlockNode:
return c.resolveIfBlock(n, scope)
case *ast.ImportNode:
// Imports don't produce output
return ""
default:
return ""
}
}
// resolveElement resolves an element node
func (c *StaticCompiler) resolveElement(el *ast.ElementNode, scope map[string]interface{}) string {
var html strings.Builder
html.WriteString("<")
html.WriteString(el.Tag)
// Add attributes (skip strata directives in output)
for name, value := range el.Attributes {
// Skip strata-specific attributes in final output
if strings.HasPrefix(name, "s-") || strings.HasPrefix(name, "@") {
continue
}
if value != "" {
html.WriteString(fmt.Sprintf(` %s="%s"`, name, value))
} else {
html.WriteString(" " + name)
}
}
html.WriteString(">")
// Resolve children
for _, child := range el.Children {
html.WriteString(c.resolveNode(child, scope))
}
// Close tag (unless void element)
voidElements := map[string]bool{
"area": true, "base": true, "br": true, "col": true,
"embed": true, "hr": true, "img": true, "input": true,
"link": true, "meta": true, "param": true, "source": true,
"track": true, "wbr": true,
}
if !voidElements[el.Tag] {
html.WriteString("</")
html.WriteString(el.Tag)
html.WriteString(">")
}
return html.String()
}
// resolveForBlock resolves a for loop at build time
func (c *StaticCompiler) resolveForBlock(f *ast.ForBlockNode, scope map[string]interface{}) string {
var result strings.Builder
// Get the iterable from scope
iterable := c.resolveExpression(f.Iterable, scope)
// Convert to slice
var items []interface{}
switch v := iterable.(type) {
case []interface{}:
items = v
case []map[string]interface{}:
for _, item := range v {
items = append(items, item)
}
case map[string]interface{}:
// Check if it has a "results" key (common in APIs like PokeAPI)
if results, ok := v["results"]; ok {
if arr, ok := results.([]interface{}); ok {
items = arr
}
}
default:
// Not iterable
return ""
}
// Iterate and resolve children
for i, item := range items {
// Create new scope with loop variables
loopScope := make(map[string]interface{})
for k, v := range scope {
loopScope[k] = v
}
loopScope[f.Item] = item
if f.Index != "" {
loopScope[f.Index] = i
}
// Resolve children in loop scope
for _, child := range f.Children {
result.WriteString(c.resolveNode(child, loopScope))
}
}
return result.String()
}
// resolveIfBlock resolves a conditional at build time
func (c *StaticCompiler) resolveIfBlock(i *ast.IfBlockNode, scope map[string]interface{}) string {
// Evaluate the condition
if c.evaluateCondition(i.Condition, scope) {
var result strings.Builder
for _, child := range i.Children {
result.WriteString(c.resolveNode(child, scope))
}
return result.String()
}
// Check elif branches
for _, elif := range i.ElseIf {
if c.evaluateCondition(elif.Condition, scope) {
var result strings.Builder
for _, child := range elif.Children {
result.WriteString(c.resolveNode(child, scope))
}
return result.String()
}
}
// Else branch
if len(i.Else) > 0 {
var result strings.Builder
for _, child := range i.Else {
result.WriteString(c.resolveNode(child, scope))
}
return result.String()
}
return ""
}
// resolveExpression resolves an expression against the scope
func (c *StaticCompiler) resolveExpression(expr string, scope map[string]interface{}) interface{} {
expr = strings.TrimSpace(expr)
// Direct variable lookup
if val, ok := scope[expr]; ok {
return val
}
// Property access (e.g., "item.name")
if strings.Contains(expr, ".") {
parts := strings.Split(expr, ".")
current := scope[parts[0]]
for i := 1; i < len(parts) && current != nil; i++ {
switch v := current.(type) {
case map[string]interface{}:
current = v[parts[i]]
default:
return expr
}
}
return current
}
// Return the expression as-is if not found
return expr
}
// evaluateCondition evaluates a condition expression
func (c *StaticCompiler) evaluateCondition(condition string, scope map[string]interface{}) bool {
condition = strings.TrimSpace(condition)
// Simple boolean variable
if val, ok := scope[condition]; ok {
switch v := val.(type) {
case bool:
return v
case string:
return v != ""
case int, int64, float64:
return v != 0
case []interface{}:
return len(v) > 0
case nil:
return false
default:
return true
}
}
// Negation
if strings.HasPrefix(condition, "!") {
return !c.evaluateCondition(condition[1:], scope)
}
// Comparison: ==, !=, >, <, >=, <=
for _, op := range []string{"===", "==", "!==", "!=", ">=", "<=", ">", "<"} {
if strings.Contains(condition, op) {
parts := strings.SplitN(condition, op, 2)
if len(parts) == 2 {
left := c.resolveExpression(strings.TrimSpace(parts[0]), scope)
right := c.resolveExpression(strings.TrimSpace(parts[1]), scope)
return c.compare(left, right, op)
}
}
}
// Property access that resolves to truthy
val := c.resolveExpression(condition, scope)
switch v := val.(type) {
case bool:
return v
case string:
return v != "" && v != condition // Not found returns expression itself
case nil:
return false
default:
return true
}
}
// compare compares two values with the given operator
func (c *StaticCompiler) compare(left, right interface{}, op string) bool {
switch op {
case "==", "===":
return fmt.Sprintf("%v", left) == fmt.Sprintf("%v", right)
case "!=", "!==":
return fmt.Sprintf("%v", left) != fmt.Sprintf("%v", right)
case ">":
return toFloat(left) > toFloat(right)
case "<":
return toFloat(left) < toFloat(right)
case ">=":
return toFloat(left) >= toFloat(right)
case "<=":
return toFloat(left) <= toFloat(right)
}
return false
}
func toFloat(v interface{}) float64 {
switch n := v.(type) {
case int:
return float64(n)
case int64:
return float64(n)
case float64:
return n
case string:
var f float64
fmt.Sscanf(n, "%f", &f)
return f
}
return 0
}

View File

@@ -0,0 +1,164 @@
package generator
import (
"bytes"
"crypto/rand"
"encoding/hex"
"fmt"
"html/template"
"os"
"path/filepath"
)
// HTMLGenerator generates index.html only in dist folder
type HTMLGenerator struct {
projectDir string
distDir string
injector *ScriptInjector
encryptionKey []byte
}
// NewHTMLGenerator creates a new HTML generator
func NewHTMLGenerator(projectDir, distDir string) *HTMLGenerator {
return &HTMLGenerator{
projectDir: projectDir,
distDir: distDir,
injector: NewScriptInjector(projectDir),
}
}
// Generate creates the index.html in dist folder
func (hg *HTMLGenerator) Generate(config *BuildConfig) error {
// Load injected scripts
if err := hg.injector.LoadScripts(); err != nil {
return fmt.Errorf("failed to load injected scripts: %w", err)
}
// Generate encryption key at build time
hg.encryptionKey = make([]byte, 32)
if _, err := rand.Read(hg.encryptionKey); err != nil {
return fmt.Errorf("failed to generate encryption key: %w", err)
}
// Create dist directory
if err := os.MkdirAll(hg.distDir, 0755); err != nil {
return fmt.Errorf("failed to create dist directory: %w", err)
}
// Generate HTML
html, err := hg.buildHTML(config)
if err != nil {
return err
}
// Inject scripts
html = hg.injector.InjectIntoHTML(html)
// Write to dist
indexPath := filepath.Join(hg.distDir, "index.html")
if err := os.WriteFile(indexPath, []byte(html), 0644); err != nil {
return fmt.Errorf("failed to write index.html: %w", err)
}
return nil
}
// BuildConfig holds build configuration
type BuildConfig struct {
Title string
Description string
BaseURL string
APIBaseURL string
DevMode bool
Assets AssetManifest
}
// AssetManifest tracks generated assets
type AssetManifest struct {
JS []string
CSS []string
}
func (hg *HTMLGenerator) buildHTML(config *BuildConfig) (string, error) {
tmpl := `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="{{.Description}}">
<title>{{.Title}}</title>
<base href="{{.BaseURL}}">
{{range .Assets.CSS}}
<link rel="stylesheet" href="{{.}}">
{{end}}
<script>
// Strata config (build-time injected)
window.__STRATA_CONFIG__ = {
apiBaseUrl: "{{.APIBaseURL}}",
devMode: {{.DevMode}},
encryptionKey: [{{.EncryptionKeyArray}}]
};
</script>
</head>
<body>
<div id="app"></div>
<!-- Strata Runtime -->
<script type="module" src="/assets/js/runtime.js"></script>
{{range .Assets.JS}}
<script type="module" src="{{.}}"></script>
{{end}}
</body>
</html>`
// Convert encryption key to array format for JS
keyArray := make([]string, len(hg.encryptionKey))
for i, b := range hg.encryptionKey {
keyArray[i] = fmt.Sprintf("%d", b)
}
keyArrayStr := ""
for i, k := range keyArray {
if i > 0 {
keyArrayStr += ","
}
keyArrayStr += k
}
data := struct {
Title string
Description string
BaseURL string
APIBaseURL string
DevMode bool
Assets AssetManifest
EncryptionKeyArray string
}{
Title: config.Title,
Description: config.Description,
BaseURL: config.BaseURL,
APIBaseURL: config.APIBaseURL,
DevMode: config.DevMode,
Assets: config.Assets,
EncryptionKeyArray: keyArrayStr,
}
t, err := template.New("html").Parse(tmpl)
if err != nil {
return "", err
}
var buf bytes.Buffer
if err := t.Execute(&buf, data); err != nil {
return "", err
}
return buf.String(), nil
}
// GetEncryptionKeyHex returns the encryption key as hex for debugging
func (hg *HTMLGenerator) GetEncryptionKeyHex() string {
return hex.EncodeToString(hg.encryptionKey)
}

View File

@@ -0,0 +1,181 @@
package generator
import (
"os"
"path/filepath"
"sort"
"strings"
)
// InjectedScript represents a script file to be injected
type InjectedScript struct {
Name string
Content string
Position string // "head" or "body"
Priority int // Lower = earlier injection
}
// ScriptInjector handles the injectedscripts/ directory
type ScriptInjector struct {
scriptsDir string
scripts []InjectedScript
}
// NewScriptInjector creates a new script injector
func NewScriptInjector(projectDir string) *ScriptInjector {
return &ScriptInjector{
scriptsDir: filepath.Join(projectDir, "src", "injectedscripts"),
scripts: make([]InjectedScript, 0),
}
}
// LoadScripts loads all scripts from injectedscripts/ directory
func (si *ScriptInjector) LoadScripts() error {
if _, err := os.Stat(si.scriptsDir); os.IsNotExist(err) {
return nil // No injectedscripts directory, that's fine
}
entries, err := os.ReadDir(si.scriptsDir)
if err != nil {
return err
}
for _, entry := range entries {
if entry.IsDir() {
continue
}
name := entry.Name()
if !strings.HasSuffix(name, ".js") {
continue
}
content, err := os.ReadFile(filepath.Join(si.scriptsDir, name))
if err != nil {
return err
}
script := si.parseScript(name, string(content))
si.scripts = append(si.scripts, script)
}
// Sort by priority
sort.Slice(si.scripts, func(i, j int) bool {
return si.scripts[i].Priority < si.scripts[j].Priority
})
return nil
}
// parseScript parses a script file and extracts metadata from comments
func (si *ScriptInjector) parseScript(name string, content string) InjectedScript {
script := InjectedScript{
Name: strings.TrimSuffix(name, ".js"),
Content: content,
Position: "head", // default to head
Priority: 100, // default priority
}
// Parse special comments at the top
// /* @position: body */
// /* @priority: 10 */
lines := strings.Split(content, "\n")
for _, line := range lines {
line = strings.TrimSpace(line)
if !strings.HasPrefix(line, "/*") && !strings.HasPrefix(line, "//") {
break
}
if strings.Contains(line, "@position:") {
if strings.Contains(line, "body") {
script.Position = "body"
} else {
script.Position = "head"
}
}
if strings.Contains(line, "@priority:") {
// Extract priority number
parts := strings.Split(line, "@priority:")
if len(parts) > 1 {
priority := strings.TrimSpace(parts[1])
priority = strings.TrimSuffix(priority, "*/")
priority = strings.TrimSpace(priority)
var p int
if _, err := parseIntFromString(priority, &p); err == nil {
script.Priority = p
}
}
}
}
return script
}
// GetHeadScripts returns scripts for <head>
func (si *ScriptInjector) GetHeadScripts() []InjectedScript {
var result []InjectedScript
for _, s := range si.scripts {
if s.Position == "head" {
result = append(result, s)
}
}
return result
}
// GetBodyScripts returns scripts for end of <body>
func (si *ScriptInjector) GetBodyScripts() []InjectedScript {
var result []InjectedScript
for _, s := range si.scripts {
if s.Position == "body" {
result = append(result, s)
}
}
return result
}
// InjectIntoHTML injects scripts into HTML content
func (si *ScriptInjector) InjectIntoHTML(html string) string {
// Inject head scripts before </head>
headScripts := si.GetHeadScripts()
if len(headScripts) > 0 {
var headContent strings.Builder
for _, s := range headScripts {
headContent.WriteString("<!-- Injected: ")
headContent.WriteString(s.Name)
headContent.WriteString(" -->\n")
headContent.WriteString(s.Content)
headContent.WriteString("\n")
}
html = strings.Replace(html, "</head>", headContent.String()+"</head>", 1)
}
// Inject body scripts before </body>
bodyScripts := si.GetBodyScripts()
if len(bodyScripts) > 0 {
var bodyContent strings.Builder
for _, s := range bodyScripts {
bodyContent.WriteString("<!-- Injected: ")
bodyContent.WriteString(s.Name)
bodyContent.WriteString(" -->\n")
bodyContent.WriteString(s.Content)
bodyContent.WriteString("\n")
}
html = strings.Replace(html, "</body>", bodyContent.String()+"</body>", 1)
}
return html
}
func parseIntFromString(s string, target *int) (int, error) {
var n int
for _, c := range s {
if c >= '0' && c <= '9' {
n = n*10 + int(c-'0')
} else {
break
}
}
*target = n
return n, nil
}

View File

@@ -0,0 +1,688 @@
package parser
import (
"regexp"
"strings"
"github.com/CarGDev/strata/internal/ast"
)
// StrataParser parses .strata files
type StrataParser struct {
source string
pos int
template *ast.TemplateNode
script *ast.ScriptNode
style *ast.StyleNode
}
// NewStrataParser creates a new parser
func NewStrataParser(source string) *StrataParser {
return &StrataParser{
source: source,
pos: 0,
}
}
// Parse parses a .strata file into AST nodes
func (p *StrataParser) Parse() (*ast.StrataFile, error) {
file := &ast.StrataFile{}
// Extract <template>, <script>, and <style> blocks
templateContent := p.extractBlock("template")
scriptContent := p.extractBlock("script")
styleContent := p.extractBlock("style")
// Parse template
if templateContent != "" {
template, err := p.parseTemplate(templateContent)
if err != nil {
return nil, err
}
file.Template = template
}
// Parse script
if scriptContent != "" {
script, err := p.parseScript(scriptContent)
if err != nil {
return nil, err
}
file.Script = script
}
// Parse style
if styleContent != "" {
style, err := p.parseStyle(styleContent)
if err != nil {
return nil, err
}
file.Style = style
}
return file, nil
}
// extractBlock extracts content between <tag> and </tag>
func (p *StrataParser) extractBlock(tag string) string {
// Match opening tag with optional attributes
openPattern := regexp.MustCompile(`<` + tag + `(?:\s+[^>]*)?>`)
closePattern := regexp.MustCompile(`</` + tag + `>`)
openMatch := openPattern.FindStringIndex(p.source)
if openMatch == nil {
return ""
}
closeMatch := closePattern.FindStringIndex(p.source[openMatch[1]:])
if closeMatch == nil {
return ""
}
// Extract content between tags
start := openMatch[1]
end := openMatch[1] + closeMatch[0]
return strings.TrimSpace(p.source[start:end])
}
// parseTemplate parses the template content
func (p *StrataParser) parseTemplate(content string) (*ast.TemplateNode, error) {
template := &ast.TemplateNode{
Children: make([]ast.Node, 0),
}
// Parse HTML with Strata directives
nodes, err := p.parseHTML(content)
if err != nil {
return nil, err
}
template.Children = nodes
return template, nil
}
// parseHTML parses HTML content into AST nodes
func (p *StrataParser) parseHTML(content string) ([]ast.Node, error) {
nodes := make([]ast.Node, 0)
// Simple tokenization - in production, use proper HTML parser
pos := 0
for pos < len(content) {
// Skip whitespace
for pos < len(content) && isWhitespace(content[pos]) {
pos++
}
if pos >= len(content) {
break
}
// Strata block directive { s-* } or variable interpolation { var }
if content[pos] == '{' && pos+1 < len(content) && content[pos+1] != '{' {
// Find closing }
end := strings.Index(content[pos:], "}")
if end != -1 {
inner := strings.TrimSpace(content[pos+1 : pos+end])
// Check for block directives
if strings.HasPrefix(inner, "s-for ") {
// Parse { s-for item in items } ... { /s-for }
node, newPos, err := p.parseForBlock(content, pos)
if err != nil {
return nil, err
}
nodes = append(nodes, node)
pos = newPos
continue
} else if strings.HasPrefix(inner, "s-if ") {
// Parse { s-if cond } ... { /s-if }
node, newPos, err := p.parseIfBlock(content, pos)
if err != nil {
return nil, err
}
nodes = append(nodes, node)
pos = newPos
continue
} else if strings.HasPrefix(inner, "s-imp ") {
// Parse { s-imp "@alias/file" }
importPath := strings.TrimSpace(inner[6:])
importPath = strings.Trim(importPath, `"'`)
nodes = append(nodes, &ast.ImportNode{
Path: importPath,
})
pos = pos + end + 1
continue
} else if strings.HasPrefix(inner, "/s-") || strings.HasPrefix(inner, "s-elif") || inner == "s-else" {
// Closing or branch tags - handled by parent parser
break
} else {
// Variable interpolation { var }
nodes = append(nodes, &ast.InterpolationNode{
Expression: inner,
})
pos = pos + end + 1
continue
}
}
}
// Legacy {{ }} interpolation (for backwards compatibility)
if pos+1 < len(content) && content[pos:pos+2] == "{{" {
end := strings.Index(content[pos:], "}}")
if end != -1 {
expr := strings.TrimSpace(content[pos+2 : pos+end])
nodes = append(nodes, &ast.InterpolationNode{
Expression: expr,
})
pos = pos + end + 2
continue
}
}
// HTML Element
if content[pos] == '<' {
// Comment
if pos+4 < len(content) && content[pos:pos+4] == "<!--" {
end := strings.Index(content[pos:], "-->")
if end != -1 {
pos = pos + end + 3
continue
}
}
// Closing tag
if pos+2 < len(content) && content[pos+1] == '/' {
end := strings.Index(content[pos:], ">")
if end != -1 {
pos = pos + end + 1
}
break // Return to parent
}
// Opening tag
node, newPos, err := p.parseElement(content, pos)
if err != nil {
return nil, err
}
if node != nil {
nodes = append(nodes, node)
}
pos = newPos
continue
}
// Text content
textEnd := strings.IndexAny(content[pos:], "<{")
if textEnd == -1 {
textEnd = len(content) - pos
}
text := strings.TrimSpace(content[pos : pos+textEnd])
if text != "" {
nodes = append(nodes, &ast.TextNode{Content: text})
}
pos = pos + textEnd
}
return nodes, nil
}
// parseForBlock parses { s-for item in items } ... { /s-for }
func (p *StrataParser) parseForBlock(content string, pos int) (*ast.ForBlockNode, int, error) {
// Find opening tag end
openEnd := strings.Index(content[pos:], "}")
if openEnd == -1 {
return nil, pos, nil
}
// Parse the for expression: "s-for item in items" or "s-for item, i in items"
inner := strings.TrimSpace(content[pos+1 : pos+openEnd])
inner = strings.TrimPrefix(inner, "s-for ")
inner = strings.TrimSpace(inner)
// Split by " in "
parts := strings.SplitN(inner, " in ", 2)
if len(parts) != 2 {
return nil, pos + openEnd + 1, nil
}
forNode := &ast.ForBlockNode{
Iterable: strings.TrimSpace(parts[1]),
}
// Parse item and optional index: "item" or "item, i"
itemPart := strings.TrimSpace(parts[0])
if strings.Contains(itemPart, ",") {
itemParts := strings.SplitN(itemPart, ",", 2)
forNode.Item = strings.TrimSpace(itemParts[0])
forNode.Index = strings.TrimSpace(itemParts[1])
} else {
forNode.Item = itemPart
}
// Find content between opening and closing tags
startPos := pos + openEnd + 1
closeTag := "{ /s-for }"
closeTagAlt := "{/s-for}"
// Search for closing tag
closePos := strings.Index(content[startPos:], closeTag)
if closePos == -1 {
closePos = strings.Index(content[startPos:], closeTagAlt)
if closePos == -1 {
// Try more lenient matching
closePos = p.findClosingTag(content[startPos:], "/s-for")
}
}
if closePos == -1 {
return nil, pos + openEnd + 1, nil
}
// Parse children
childContent := content[startPos : startPos+closePos]
children, err := p.parseHTML(childContent)
if err != nil {
return nil, pos, err
}
forNode.Children = children
// Calculate new position after closing tag
newPos := startPos + closePos
// Skip past the closing tag
closeEnd := strings.Index(content[newPos:], "}")
if closeEnd != -1 {
newPos = newPos + closeEnd + 1
}
return forNode, newPos, nil
}
// parseIfBlock parses { s-if cond } ... { s-elif cond } ... { s-else } ... { /s-if }
func (p *StrataParser) parseIfBlock(content string, pos int) (*ast.IfBlockNode, int, error) {
// Find opening tag end
openEnd := strings.Index(content[pos:], "}")
if openEnd == -1 {
return nil, pos, nil
}
// Parse the if condition
inner := strings.TrimSpace(content[pos+1 : pos+openEnd])
condition := strings.TrimPrefix(inner, "s-if ")
condition = strings.TrimSpace(condition)
ifNode := &ast.IfBlockNode{
Condition: condition,
ElseIf: make([]*ast.ElseIfBranch, 0),
}
// Find content and branches
startPos := pos + openEnd + 1
currentPos := startPos
// Parse until we hit /s-if, collecting elif and else branches
for currentPos < len(content) {
// Look for next branch or closing tag
nextBrace := strings.Index(content[currentPos:], "{")
if nextBrace == -1 {
break
}
bracePos := currentPos + nextBrace
braceEnd := strings.Index(content[bracePos:], "}")
if braceEnd == -1 {
break
}
braceContent := strings.TrimSpace(content[bracePos+1 : bracePos+braceEnd])
if strings.HasPrefix(braceContent, "/s-if") {
// Closing tag - parse content up to here
childContent := content[startPos:bracePos]
children, err := p.parseHTML(childContent)
if err != nil {
return nil, pos, err
}
// Add children to appropriate branch
if len(ifNode.ElseIf) == 0 && len(ifNode.Else) == 0 {
ifNode.Children = children
}
return ifNode, bracePos + braceEnd + 1, nil
} else if strings.HasPrefix(braceContent, "s-elif ") {
// Parse content for previous branch
childContent := content[startPos:bracePos]
children, err := p.parseHTML(childContent)
if err != nil {
return nil, pos, err
}
if len(ifNode.ElseIf) == 0 {
ifNode.Children = children
} else {
ifNode.ElseIf[len(ifNode.ElseIf)-1].Children = children
}
// Add new elif branch
elifCond := strings.TrimPrefix(braceContent, "s-elif ")
elifCond = strings.TrimSpace(elifCond)
ifNode.ElseIf = append(ifNode.ElseIf, &ast.ElseIfBranch{
Condition: elifCond,
})
startPos = bracePos + braceEnd + 1
currentPos = startPos
continue
} else if braceContent == "s-else" {
// Parse content for previous branch
childContent := content[startPos:bracePos]
children, err := p.parseHTML(childContent)
if err != nil {
return nil, pos, err
}
if len(ifNode.ElseIf) == 0 {
ifNode.Children = children
} else {
ifNode.ElseIf[len(ifNode.ElseIf)-1].Children = children
}
// Find closing tag and parse else content
elseStart := bracePos + braceEnd + 1
closePos := p.findClosingTag(content[elseStart:], "/s-if")
if closePos != -1 {
elseContent := content[elseStart : elseStart+closePos]
elseChildren, err := p.parseHTML(elseContent)
if err != nil {
return nil, pos, err
}
ifNode.Else = elseChildren
// Skip to after closing tag
closeEnd := strings.Index(content[elseStart+closePos:], "}")
if closeEnd != -1 {
return ifNode, elseStart + closePos + closeEnd + 1, nil
}
}
break
}
currentPos = bracePos + braceEnd + 1
}
return ifNode, currentPos, nil
}
// findClosingTag finds position of { /tag } allowing for whitespace
func (p *StrataParser) findClosingTag(content string, tag string) int {
pos := 0
for pos < len(content) {
bracePos := strings.Index(content[pos:], "{")
if bracePos == -1 {
return -1
}
bracePos += pos
braceEnd := strings.Index(content[bracePos:], "}")
if braceEnd == -1 {
return -1
}
inner := strings.TrimSpace(content[bracePos+1 : bracePos+braceEnd])
if inner == tag || strings.TrimSpace(inner) == tag {
return bracePos
}
pos = bracePos + braceEnd + 1
}
return -1
}
// parseElement parses an HTML element
func (p *StrataParser) parseElement(content string, pos int) (ast.Node, int, error) {
// Find tag name
tagStart := pos + 1
tagEnd := tagStart
for tagEnd < len(content) && !isWhitespace(content[tagEnd]) && content[tagEnd] != '>' && content[tagEnd] != '/' {
tagEnd++
}
tagName := content[tagStart:tagEnd]
element := &ast.ElementNode{
Tag: tagName,
Attributes: make(map[string]string),
Directives: make([]ast.Directive, 0),
Children: make([]ast.Node, 0),
}
// Parse attributes
attrPos := tagEnd
for attrPos < len(content) && content[attrPos] != '>' && content[attrPos] != '/' {
// Skip whitespace
for attrPos < len(content) && isWhitespace(content[attrPos]) {
attrPos++
}
if attrPos >= len(content) || content[attrPos] == '>' || content[attrPos] == '/' {
break
}
// Parse attribute name
nameStart := attrPos
for attrPos < len(content) && !isWhitespace(content[attrPos]) && content[attrPos] != '=' && content[attrPos] != '>' {
attrPos++
}
attrName := content[nameStart:attrPos]
// Parse attribute value
attrValue := ""
for attrPos < len(content) && isWhitespace(content[attrPos]) {
attrPos++
}
if attrPos < len(content) && content[attrPos] == '=' {
attrPos++
for attrPos < len(content) && isWhitespace(content[attrPos]) {
attrPos++
}
if attrPos < len(content) && (content[attrPos] == '"' || content[attrPos] == '\'') {
quote := content[attrPos]
attrPos++
valueStart := attrPos
for attrPos < len(content) && content[attrPos] != quote {
attrPos++
}
attrValue = content[valueStart:attrPos]
attrPos++
}
}
// Check for directives
if directive := p.parseDirective(attrName, attrValue); directive != nil {
element.Directives = append(element.Directives, *directive)
} else {
element.Attributes[attrName] = attrValue
}
}
// Self-closing tag
if attrPos < len(content) && content[attrPos] == '/' {
attrPos++
for attrPos < len(content) && content[attrPos] != '>' {
attrPos++
}
return element, attrPos + 1, nil
}
// Find closing >
for attrPos < len(content) && content[attrPos] != '>' {
attrPos++
}
attrPos++ // Skip >
// Void elements (no children)
voidElements := map[string]bool{
"area": true, "base": true, "br": true, "col": true,
"embed": true, "hr": true, "img": true, "input": true,
"link": true, "meta": true, "param": true, "source": true,
"track": true, "wbr": true,
}
if voidElements[tagName] {
return element, attrPos, nil
}
// Parse children
childContent := ""
depth := 1
childStart := attrPos
for attrPos < len(content) && depth > 0 {
if attrPos+1 < len(content) && content[attrPos] == '<' {
if content[attrPos+1] == '/' {
// Closing tag
closeEnd := strings.Index(content[attrPos:], ">")
if closeEnd != -1 {
closeTag := content[attrPos+2 : attrPos+closeEnd]
closeTag = strings.TrimSpace(closeTag)
if closeTag == tagName {
depth--
if depth == 0 {
childContent = content[childStart:attrPos]
attrPos = attrPos + closeEnd + 1
break
}
}
}
} else if content[attrPos+1] != '!' {
// Opening tag - check if same tag
openEnd := attrPos + 1
for openEnd < len(content) && !isWhitespace(content[openEnd]) && content[openEnd] != '>' && content[openEnd] != '/' {
openEnd++
}
openTag := content[attrPos+1 : openEnd]
if openTag == tagName {
depth++
}
}
}
attrPos++
}
// Parse children
if childContent != "" {
children, err := p.parseHTML(childContent)
if err != nil {
return nil, attrPos, err
}
element.Children = children
}
return element, attrPos, nil
}
// parseDirective parses Strata directives
func (p *StrataParser) parseDirective(name, value string) *ast.Directive {
directive := &ast.Directive{
Name: name,
Value: value,
}
// s-if, s-else-if, s-else
if name == "s-if" || name == "s-else-if" || name == "s-else" {
directive.Type = ast.DirectiveConditional
return directive
}
// s-for
if name == "s-for" {
directive.Type = ast.DirectiveLoop
return directive
}
// s-model (two-way binding)
if name == "s-model" {
directive.Type = ast.DirectiveModel
return directive
}
// s-on:event or @event (event handlers)
if strings.HasPrefix(name, "s-on:") || strings.HasPrefix(name, "@") {
directive.Type = ast.DirectiveEvent
if strings.HasPrefix(name, "@") {
directive.Arg = name[1:]
} else {
directive.Arg = name[5:]
}
return directive
}
// :attr (attribute binding)
if strings.HasPrefix(name, ":") {
directive.Type = ast.DirectiveBind
directive.Arg = name[1:]
return directive
}
// s-client:* (hydration strategies)
if strings.HasPrefix(name, "s-client:") {
directive.Type = ast.DirectiveClient
directive.Arg = name[9:]
return directive
}
// s-fetch (data fetching)
if name == "s-fetch" {
directive.Type = ast.DirectiveFetch
return directive
}
// s-ref
if name == "s-ref" {
directive.Type = ast.DirectiveRef
return directive
}
// s-html (raw HTML)
if name == "s-html" {
directive.Type = ast.DirectiveHTML
return directive
}
// s-static (no JS)
if name == "s-static" {
directive.Type = ast.DirectiveStatic
return directive
}
return nil
}
// parseScript parses the script content
func (p *StrataParser) parseScript(content string) (*ast.ScriptNode, error) {
return &ast.ScriptNode{
Content: content,
Lang: "ts", // Default to TypeScript
}, nil
}
// parseStyle parses the style content
func (p *StrataParser) parseStyle(content string) (*ast.StyleNode, error) {
// Check for lang attribute in original source
langPattern := regexp.MustCompile(`<style\s+lang="([^"]+)"`)
matches := langPattern.FindStringSubmatch(p.source)
lang := "css"
if len(matches) > 1 {
lang = matches[1]
}
return &ast.StyleNode{
Content: content,
Lang: lang,
Scoped: strings.Contains(p.source, "<style scoped") || strings.Contains(p.source, "<style lang=\"scss\" scoped"),
}, nil
}
func isWhitespace(c byte) bool {
return c == ' ' || c == '\t' || c == '\n' || c == '\r'
}

View File

@@ -0,0 +1,922 @@
package server
import (
"encoding/json"
"fmt"
"log"
"net/http"
"os"
"path/filepath"
"strings"
"sync"
"time"
"github.com/CarGDev/strata/internal/ast"
"github.com/CarGDev/strata/internal/compiler"
"github.com/CarGDev/strata/internal/parser"
"github.com/gorilla/websocket"
)
// DevServer handles development server with HMR
type DevServer struct {
port int
projectDir string
distDir string
clients map[*websocket.Conn]bool
mu sync.RWMutex
upgrader websocket.Upgrader
compiler *compiler.StaticCompiler
}
// NewDevServer creates a new dev server
func NewDevServer(port int, projectDir string) *DevServer {
return &DevServer{
port: port,
projectDir: projectDir,
distDir: filepath.Join(projectDir, ".strata", "dev"),
clients: make(map[*websocket.Conn]bool),
upgrader: websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool {
return true // Allow all origins in dev
},
},
compiler: compiler.NewStaticCompiler(projectDir),
}
}
// Start starts the dev server
func (s *DevServer) Start() error {
// Create dev output directory
if err := os.MkdirAll(s.distDir, 0755); err != nil {
return err
}
// Generate initial files
if err := s.generateDevFiles(); err != nil {
return err
}
// Set up HTTP handlers
mux := http.NewServeMux()
// WebSocket for HMR
mux.HandleFunc("/__strata_hmr", s.handleHMR)
// API proxy (if configured)
mux.HandleFunc("/api/", s.handleAPIProxy)
// Static files from .strata/dev
mux.HandleFunc("/", s.handleStatic)
server := &http.Server{
Addr: fmt.Sprintf(":%d", s.port),
Handler: mux,
ReadTimeout: 15 * time.Second,
WriteTimeout: 15 * time.Second,
}
fmt.Printf("\n Strata Dev Server\n")
fmt.Printf(" ─────────────────────────────\n")
fmt.Printf(" Local: http://localhost:%d\n", s.port)
fmt.Printf(" Network: http://%s:%d\n", getLocalIP(), s.port)
fmt.Printf("\n Watching for changes...\n\n")
return server.ListenAndServe()
}
// handleStatic serves static files
func (s *DevServer) handleStatic(w http.ResponseWriter, r *http.Request) {
path := r.URL.Path
if path == "/" {
path = "/index.html"
}
// Try .strata/dev first
filePath := filepath.Join(s.distDir, path)
if _, err := os.Stat(filePath); err == nil {
http.ServeFile(w, r, filePath)
return
}
// Try public folder
publicPath := filepath.Join(s.projectDir, "public", path)
if _, err := os.Stat(publicPath); err == nil {
http.ServeFile(w, r, publicPath)
return
}
// Try src/assets
assetPath := filepath.Join(s.projectDir, "src", "assets", path)
if _, err := os.Stat(assetPath); err == nil {
http.ServeFile(w, r, assetPath)
return
}
// SPA fallback - serve index.html
indexPath := filepath.Join(s.distDir, "index.html")
if _, err := os.Stat(indexPath); err == nil {
http.ServeFile(w, r, indexPath)
return
}
http.NotFound(w, r)
}
// handleHMR handles WebSocket connections for HMR
func (s *DevServer) handleHMR(w http.ResponseWriter, r *http.Request) {
conn, err := s.upgrader.Upgrade(w, r, nil)
if err != nil {
log.Printf("WebSocket upgrade error: %v", err)
return
}
s.mu.Lock()
s.clients[conn] = true
s.mu.Unlock()
defer func() {
s.mu.Lock()
delete(s.clients, conn)
s.mu.Unlock()
conn.Close()
}()
// Keep connection alive
for {
_, _, err := conn.ReadMessage()
if err != nil {
break
}
}
}
// handleAPIProxy proxies API requests
func (s *DevServer) handleAPIProxy(w http.ResponseWriter, r *http.Request) {
// TODO: Read API base URL from strataconfig.ts
// For now, return 501
w.WriteHeader(http.StatusNotImplemented)
w.Write([]byte(`{"error": "API proxy not configured"}`))
}
// Rebuild regenerates all dev files (called on file changes)
func (s *DevServer) Rebuild() error {
return s.generateDevFiles()
}
// NotifyChange sends HMR update to all clients
func (s *DevServer) NotifyChange(changeType string, path string) {
message := map[string]string{
"type": changeType,
"path": path,
}
data, _ := json.Marshal(message)
s.mu.RLock()
defer s.mu.RUnlock()
for client := range s.clients {
err := client.WriteMessage(websocket.TextMessage, data)
if err != nil {
client.Close()
delete(s.clients, client)
}
}
}
// generateDevFiles generates initial dev files
func (s *DevServer) generateDevFiles() error {
// Generate index.html with HMR client
html := s.generateDevHTML()
indexPath := filepath.Join(s.distDir, "index.html")
if err := os.WriteFile(indexPath, []byte(html), 0644); err != nil {
return err
}
// Generate runtime.js
runtime := s.generateDevRuntime()
runtimePath := filepath.Join(s.distDir, "assets", "js", "runtime.js")
if err := os.MkdirAll(filepath.Dir(runtimePath), 0755); err != nil {
return err
}
if err := os.WriteFile(runtimePath, []byte(runtime), 0644); err != nil {
return err
}
// Copy and process source files
return s.processSourceFiles()
}
func (s *DevServer) generateDevHTML() string {
return `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Strata | Code Faster</title>
<link rel="stylesheet" href="/assets/css/app.css">
<script src="https://cdn.tailwindcss.com"></script>
<script>
window.__STRATA_CONFIG__ = {
devMode: true,
apiBaseUrl: 'http://localhost:8080'
};
</script>
<style>
@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;700&display=swap');
body {
background-color: #0a0a0a;
color: #ffffff;
font-family: 'JetBrains Mono', monospace;
overflow: hidden;
}
.ascii-art {
white-space: pre;
line-height: 1.2;
font-size: clamp(0.5rem, 1.5vw, 1rem);
color: #3b82f6;
text-shadow: 0 0 20px rgba(59, 130, 246, 0.5);
}
.heart-btn {
transition: all 0.2s cubic-bezier(0.175, 0.885, 0.32, 1.275);
}
.heart-btn:active {
transform: scale(0.9);
}
.floating-heart {
position: absolute;
pointer-events: none;
animation: floatUp 1s ease-out forwards;
}
@keyframes floatUp {
0% { transform: translateY(0) scale(1); opacity: 1; }
100% { transform: translateY(-100px) scale(1.5); opacity: 0; }
}
.gradient-text {
background: linear-gradient(to right, #3b82f6, #8b5cf6);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.strata-error {
background: #fee;
border: 1px solid #fcc;
padding: 1rem;
margin: 1rem;
border-radius: 8px;
font-family: monospace;
white-space: pre-wrap;
color: #c00;
}
</style>
</head>
<body class="flex flex-col items-center justify-center min-h-screen p-4">
<div id="app"></div>
<script type="module" src="/assets/js/runtime.js"></script>
<script type="module" src="/assets/js/app.js"></script>
<!-- HMR Client -->
<script>
(function() {
const ws = new WebSocket('ws://' + location.host + '/__strata_hmr');
ws.onmessage = function(event) {
const data = JSON.parse(event.data);
console.log('[Strata HMR]', data.type, data.path);
if (data.type === 'reload') {
location.reload();
} else if (data.type === 'css') {
const links = document.querySelectorAll('link[rel="stylesheet"]');
links.forEach(link => {
const url = new URL(link.href);
url.searchParams.set('t', Date.now());
link.href = url.toString();
});
} else if (data.type === 'component') {
location.reload();
}
};
ws.onclose = function() {
console.log('[Strata HMR] Disconnected. Attempting reconnect...');
setTimeout(() => location.reload(), 1000);
};
})();
</script>
</body>
</html>`
}
func (s *DevServer) generateDevRuntime() string {
return `// Strata Runtime (Dev Mode)
console.log('[Strata] Runtime loaded');
class Strata {
constructor() {
this._tabId = 'tab_' + Math.random().toString(36).slice(2, 10);
this._stores = new Map();
this._cache = new Map();
}
get tabId() {
return this._tabId;
}
async init() {
console.log('[Strata] Initialized with tabId:', this._tabId);
window.__STRATA__ = this;
}
// Simple fetch with caching
async fetch(url, options = {}) {
const cacheKey = url + JSON.stringify(options);
if (options.cache !== 'none' && this._cache.has(cacheKey)) {
const cached = this._cache.get(cacheKey);
if (Date.now() - cached.timestamp < 300000) { // 5 min
return cached.data;
}
}
const response = await fetch(url, options);
const data = await response.json();
this._cache.set(cacheKey, { data, timestamp: Date.now() });
return data;
}
// Broadcast to other tabs (simplified for dev)
broadcast(event, data) {
console.log('[Strata] Broadcast:', event, data);
window.dispatchEvent(new CustomEvent('strata:' + event, { detail: data }));
}
onBroadcast(event, handler) {
window.addEventListener('strata:' + event, (e) => handler(e.detail));
}
}
// Initialize
const strata = new Strata();
strata.init();
export { strata };
export default strata;
`
}
func (s *DevServer) processSourceFiles() error {
// Process .strata files in src/pages
pagesDir := filepath.Join(s.projectDir, "src", "pages")
if _, err := os.Stat(pagesDir); err != nil {
return nil // No pages directory
}
// Find all pages - both directory structure (pages/name/name.strata) and flat (pages/name.strata)
var pages []PageInfo
processedDirs := make(map[string]bool)
err := filepath.Walk(pagesDir, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
// Check for directory-based pages first (pages/name/name.strata)
if info.IsDir() && path != pagesDir {
dirName := filepath.Base(path)
strataFile := filepath.Join(path, dirName+".strata")
compilerFile := filepath.Join(path, dirName+".compiler.sts")
// If this is a page directory with .strata file
if _, err := os.Stat(strataFile); err == nil {
processedDirs[path] = true
// Check if it has a .compiler.sts file (new structure)
if _, err := os.Stat(compilerFile); err == nil {
// Use static compiler for new structure
pageInfo, err := s.compilePageDirectory(path)
if err != nil {
log.Printf("Warning: failed to compile %s: %v", path, err)
return nil
}
pages = append(pages, pageInfo)
} else {
// Fallback to old compilation for legacy structure
pageInfo, err := s.compilePage(strataFile)
if err != nil {
log.Printf("Warning: failed to compile %s: %v", strataFile, err)
return nil
}
pages = append(pages, pageInfo)
}
return filepath.SkipDir // Don't descend further
}
}
// Handle flat structure (pages/name.strata)
if !info.IsDir() && strings.HasSuffix(path, ".strata") {
// Skip if parent dir was already processed as a page directory
if processedDirs[filepath.Dir(path)] {
return nil
}
pageInfo, err := s.compilePage(path)
if err != nil {
log.Printf("Warning: failed to compile %s: %v", path, err)
return nil
}
pages = append(pages, pageInfo)
}
return nil
})
if err != nil {
return err
}
// Generate app.js from compiled pages
appJS := s.generateAppJSFromPages(pages)
appPath := filepath.Join(s.distDir, "assets", "js", "app.js")
if err := os.WriteFile(appPath, []byte(appJS), 0644); err != nil {
return err
}
// Generate combined CSS
cssPath := filepath.Join(s.distDir, "assets", "css", "app.css")
if err := os.MkdirAll(filepath.Dir(cssPath), 0755); err != nil {
return err
}
css := s.generateCSSFromPages(pages)
if err := os.WriteFile(cssPath, []byte(css), 0644); err != nil {
return err
}
return nil
}
// compilePageDirectory compiles a page from directory structure using static compiler
func (s *DevServer) compilePageDirectory(pageDir string) (PageInfo, error) {
pageName := filepath.Base(pageDir)
// Use the static compiler
module, err := s.compiler.CompilePage(pageDir)
if err != nil {
return PageInfo{}, err
}
// Read service file for runtime logic (if exists)
serviceContent := ""
servicePath := filepath.Join(pageDir, pageName+".service.sts")
if content, err := os.ReadFile(servicePath); err == nil {
serviceContent = string(content)
}
// Calculate route from directory path
relPath, _ := filepath.Rel(filepath.Join(s.projectDir, "src", "pages"), pageDir)
route := "/" + relPath
if pageName == "index" || relPath == "index" {
route = "/"
}
return PageInfo{
Name: pageName,
Path: pageDir,
HTML: module.HTML,
Style: module.CSS,
Route: route,
Compiler: "", // Already resolved in HTML
Service: serviceContent,
Imports: []string{},
}, nil
}
// PageInfo holds compiled page information
type PageInfo struct {
Name string
Path string
HTML string
Compiler string // .compiler.sts - variable definitions
Service string // .service.sts - business logic
Script string // Legacy .sts support
Style string
Route string
Imports []string // Imported components
}
// compilePage compiles a single .strata file and its associated files
// File structure:
// - page.strata - HTML template
// - page.compiler.sts - variable definitions (like Angular component)
// - page.service.sts - business logic (like Angular service)
// - page.scss - styles
func (s *DevServer) compilePage(strataPath string) (PageInfo, error) {
info := PageInfo{
Path: strataPath,
Imports: make([]string, 0),
}
// Get base name without extension
baseName := strings.TrimSuffix(filepath.Base(strataPath), ".strata")
info.Name = baseName
// Calculate route from file path
relPath, _ := filepath.Rel(filepath.Join(s.projectDir, "src", "pages"), strataPath)
route := "/" + strings.TrimSuffix(relPath, ".strata")
if strings.HasSuffix(route, "/index") {
route = strings.TrimSuffix(route, "/index")
if route == "" {
route = "/"
}
}
info.Route = route
// Read and parse .strata file
strataContent, err := os.ReadFile(strataPath)
if err != nil {
return info, err
}
p := parser.NewStrataParser(string(strataContent))
file, err := p.Parse()
if err != nil {
return info, err
}
// Generate HTML from template
if file.Template != nil {
info.HTML = s.generateHTMLFromTemplate(file.Template)
// Extract imports from parsed template
info.Imports = s.extractImports(file.Template)
}
// Check for .compiler.sts file (variable definitions - Angular-like component)
compilerPath := strings.TrimSuffix(strataPath, ".strata") + ".compiler.sts"
if compilerContent, err := os.ReadFile(compilerPath); err == nil {
info.Compiler = string(compilerContent)
}
// Check for .service.sts file (business logic - Angular-like service)
servicePath := strings.TrimSuffix(strataPath, ".strata") + ".service.sts"
if serviceContent, err := os.ReadFile(servicePath); err == nil {
info.Service = string(serviceContent)
}
// Legacy: Check for .sts file (combined script)
stsPath := strings.TrimSuffix(strataPath, ".strata") + ".sts"
if stsContent, err := os.ReadFile(stsPath); err == nil {
info.Script = string(stsContent)
} else if file.Script != nil {
info.Script = file.Script.Content
}
// Check for associated .scss file (styles)
scssPath := strings.TrimSuffix(strataPath, ".strata") + ".scss"
if scssContent, err := os.ReadFile(scssPath); err == nil {
info.Style = string(scssContent)
} else if file.Style != nil {
info.Style = file.Style.Content
}
return info, nil
}
// extractImports extracts import paths from a template's ImportNodes
func (s *DevServer) extractImports(template interface{}) []string {
imports := make([]string, 0)
if t, ok := template.(*ast.TemplateNode); ok {
for _, child := range t.Children {
if imp, ok := child.(*ast.ImportNode); ok {
imports = append(imports, imp.Path)
}
}
}
return imports
}
// generateHTMLFromTemplate converts AST template to HTML string
func (s *DevServer) generateHTMLFromTemplate(template interface{}) string {
if t, ok := template.(interface{ ToHTML() string }); ok {
return t.ToHTML()
}
return ""
}
// generateAppJSFromPages creates the app.js from compiled pages
func (s *DevServer) generateAppJSFromPages(pages []PageInfo) string {
if len(pages) == 0 {
return s.generateAppJS() // Fallback to default
}
var js strings.Builder
js.WriteString(`// Strata App - Static Compilation Output
// All templates resolved at build time - no Strata syntax in runtime HTML
import { strata } from './runtime.js';
`)
// Generate page modules
for i, page := range pages {
// Escape the HTML for JavaScript
escapedHTML := strings.ReplaceAll(page.HTML, "`", "\\`")
escapedHTML = strings.ReplaceAll(escapedHTML, "${", "\\${")
// Check if this is a statically compiled page (Compiler is empty means already resolved)
// Pages can still have Service for runtime interactivity
isStaticCompiled := page.Compiler == ""
if isStaticCompiled {
// Statically compiled page - HTML is already resolved
// May have service file for runtime interactivity
if page.Service != "" {
// Extract the mount function from service file
escapedService := strings.ReplaceAll(page.Service, "`", "\\`")
escapedService = strings.ReplaceAll(escapedService, "${", "\\${")
js.WriteString(fmt.Sprintf(`// Page: %s (statically compiled with runtime service)
const page%dService = (function() {
%s
})();
const page%d = {
name: "%s",
route: "%s",
render() {
return `+"`%s`"+`;
},
mount() {
console.log('[Strata] Static page mounted:', this.name);
// Execute service mount if defined
if (page%dService && page%dService.mount) {
page%dService.mount();
} else if (page%dService && page%dService.default && page%dService.default.mount) {
page%dService.default.mount();
}
}
};
`, page.Name, i, escapedService, i, page.Name, page.Route, escapedHTML, i, i, i, i, i, i, i))
} else {
// Pure static page - no runtime code
js.WriteString(fmt.Sprintf(`// Page: %s (statically compiled)
const page%d = {
name: "%s",
route: "%s",
render() {
return `+"`%s`"+`;
},
mount() {
console.log('[Strata] Static page mounted:', this.name);
}
};
`, page.Name, i, page.Name, page.Route, escapedHTML))
}
} else {
// Legacy page with runtime compilation
compilerContent := page.Compiler
if compilerContent == "" && page.Script != "" {
compilerContent = page.Script
}
if compilerContent == "" {
compilerContent = "// No compiler defined\nreturn {};"
}
serviceContent := page.Service
if serviceContent == "" {
serviceContent = "// No service defined\nreturn {};"
}
js.WriteString(fmt.Sprintf(`// Page: %s (runtime)
const page%dCompiler = (function() {
%s
})();
const page%dService = (function() {
%s
})();
const page%d = {
name: "%s",
route: "%s",
compiler: page%dCompiler,
service: page%dService,
render() {
return `+"`%s`"+`;
},
mount() {
const state = this.compiler.state || this.compiler.default || {};
const methods = this.service.methods || this.service.default || {};
// Bind event handlers
document.querySelectorAll('[data-strata-\\@click]').forEach(el => {
const handler = el.getAttribute('data-strata-@click');
if (methods[handler]) {
el.addEventListener('click', (e) => methods[handler].call({ state, methods }, e));
}
});
// Bind interpolation placeholders
document.querySelectorAll('[data-strata-bind]').forEach(el => {
const expr = el.getAttribute('data-strata-bind');
const value = expr.split('.').reduce((o, k) => o && o[k], state);
if (value !== undefined) el.textContent = value;
});
console.log('[Strata] Page mounted:', this.name);
}
};
`, page.Name, i, compilerContent, i, serviceContent,
i, page.Name, page.Route, i, i, escapedHTML))
}
}
// Generate pages map
js.WriteString(`// Pages registry
const pages = {
`)
for i, page := range pages {
js.WriteString(fmt.Sprintf(` "%s": page%d,
`, page.Route, i))
}
js.WriteString(`};
// Router
function getRoute() {
const path = window.location.pathname || '/';
return path === '' ? '/' : path;
}
// Mount application
async function mount() {
const app = document.getElementById('app');
const route = getRoute();
const page = pages[route] || pages['/'];
if (!page) {
app.innerHTML = '<div class="strata-error">Page not found: ' + route + '</div>';
return;
}
try {
// Render HTML
app.innerHTML = page.render({});
// Mount page (bind events, etc)
page.mount();
console.log('[Strata] Mounted:', page.name, 'at', route);
} catch (error) {
app.innerHTML = '<div class="strata-error">' + error.message + '</div>';
console.error('[Strata] Mount error:', error);
}
}
// Initialize
mount();
// Handle navigation
window.addEventListener('popstate', mount);
`)
return js.String()
}
// generateCSSFromPages combines all page styles
func (s *DevServer) generateCSSFromPages(pages []PageInfo) string {
var css strings.Builder
// Add global styles first
globalPath := filepath.Join(s.projectDir, "src", "assets", "styles", "global.scss")
if globalContent, err := os.ReadFile(globalPath); err == nil {
css.WriteString("/* Global styles */\n")
css.WriteString(string(globalContent))
css.WriteString("\n\n")
}
// Add variables
varsPath := filepath.Join(s.projectDir, "src", "assets", "styles", "_variables.scss")
if varsContent, err := os.ReadFile(varsPath); err == nil {
css.WriteString("/* Variables */\n")
css.WriteString(string(varsContent))
css.WriteString("\n\n")
}
// Add page styles
for _, page := range pages {
if page.Style != "" {
css.WriteString(fmt.Sprintf("/* Page: %s */\n", page.Name))
css.WriteString(page.Style)
css.WriteString("\n\n")
}
}
return css.String()
}
func (s *DevServer) generateAppJS() string {
return `// Strata App (Dev Mode)
import { strata } from './runtime.js';
async function mount() {
const app = document.getElementById('app');
try {
app.innerHTML = ` + "`" + `
<!-- Header / Branding -->
<div class="mb-8 text-center animate-pulse">
<div class="ascii-art mb-4">
╔═══════════════════════════════════════════════════════╗
║ ║
║ ███████╗████████╗██████╗ █████╗ ████████╗ █████╗ ║
║ ██╔════╝╚══██╔══╝██╔══██╗██╔══██╗╚══██╔══╝██╔══██╗ ║
║ ███████╗ ██║ ██████╔╝███████║ ██║ ███████║ ║
║ ╚════██║ ██║ ██╔══██╗██╔══██║ ██║ ██╔══██║ ║
║ ███████║ ██║ ██║ ██║██║ ██║ ██║ ██║ ██║ ║
║ ╚══════╝ ╚═╝ ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ║
║ ║
║ Static Template Rendering Architecture ║
╚═══════════════════════════════════════════════════════╝
</div>
<h1 class="text-2xl md:text-4xl font-bold mb-2">Welcome to <span class="gradient-text">Strata</span></h1>
<p class="text-gray-400 text-sm md:text-base">Code Faster. Load Quick. Deploy ASAP.</p>
</div>
<!-- Main Interaction -->
<div class="relative flex flex-col items-center">
<button id="heartBtn" class="heart-btn bg-white/5 hover:bg-white/10 border border-white/10 rounded-full p-8 mb-4 backdrop-blur-sm shadow-2xl group">
<svg xmlns="http://www.w3.org/2000/svg" class="w-16 h-16 text-red-500 group-hover:scale-110 transition-transform" fill="currentColor" viewBox="0 0 24 24">
<path d="M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z"/>
</svg>
</button>
<div class="text-center">
<span id="counter" class="text-5xl font-bold tabular-nums">0</span>
<p class="text-gray-500 mt-2 uppercase tracking-widest text-xs">Hearts Captured</p>
</div>
</div>
<!-- Tab ID Badge -->
<div class="fixed top-4 right-4 bg-white/5 border border-white/10 rounded-lg px-3 py-2 text-xs text-gray-400">
Tab: <code class="text-blue-400">${strata.tabId}</code>
</div>
<!-- Background Decoration -->
<div class="fixed bottom-4 text-gray-700 text-[10px] uppercase tracking-tighter opacity-50">
STRATA_ENGINE_V1.0 // FAST_PATH_ENABLED // HMR_ACTIVE
</div>
` + "`" + `;
// Initialize heart button interaction
const btn = document.getElementById('heartBtn');
const counterEl = document.getElementById('counter');
let count = 0;
function createHeart(x, y) {
const heart = document.createElement('div');
heart.className = 'floating-heart text-red-500';
heart.innerHTML = '<svg width="24" height="24" fill="currentColor" viewBox="0 0 24 24"><path d="M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z"/></svg>';
heart.style.left = x + 'px';
heart.style.top = y + 'px';
document.body.appendChild(heart);
setTimeout(() => heart.remove(), 1000);
}
btn.addEventListener('click', (e) => {
count++;
counterEl.innerText = count;
createHeart(e.clientX - 12, e.clientY - 12);
counterEl.style.transform = 'scale(1.1)';
setTimeout(() => counterEl.style.transform = 'scale(1)', 100);
});
window.addEventListener('keydown', (e) => {
if (e.code === 'Space' || e.code === 'Enter') btn.click();
});
console.log('[Strata] App mounted with tabId:', strata.tabId);
} catch (error) {
app.innerHTML = '<div class="strata-error">' + error.message + '</div>';
console.error('[Strata] Mount error:', error);
}
}
mount();
`
}
func getLocalIP() string {
// Simplified - return localhost for now
return "localhost"
}

View File

@@ -0,0 +1,170 @@
package watcher
import (
"log"
"os"
"path/filepath"
"strings"
"sync"
"time"
"github.com/fsnotify/fsnotify"
)
type FileType string
const (
FileTypeStrata FileType = ".strata"
FileTypeSTS FileType = ".sts"
FileTypeSCSS FileType = ".scss"
FileTypeTS FileType = ".ts"
FileTypeConfig FileType = "strataconfig.ts"
)
type ChangeEvent struct {
Path string
Type FileType
Op fsnotify.Op
IsConfig bool
}
type Watcher struct {
watcher *fsnotify.Watcher
srcDir string
onChange func(ChangeEvent)
debounce time.Duration
pending map[string]ChangeEvent
mu sync.Mutex
timer *time.Timer
stopCh chan struct{}
}
func New(srcDir string, onChange func(ChangeEvent)) (*Watcher, error) {
fsWatcher, err := fsnotify.NewWatcher()
if err != nil {
return nil, err
}
w := &Watcher{
watcher: fsWatcher,
srcDir: srcDir,
onChange: onChange,
debounce: 50 * time.Millisecond,
pending: make(map[string]ChangeEvent),
stopCh: make(chan struct{}),
}
return w, nil
}
func (w *Watcher) Start() error {
// Add src directory recursively
err := filepath.Walk(w.srcDir, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if info.IsDir() {
return w.watcher.Add(path)
}
return nil
})
if err != nil {
return err
}
// Watch strataconfig.ts in root
configPath := filepath.Join(filepath.Dir(w.srcDir), "strataconfig.ts")
if _, err := os.Stat(configPath); err == nil {
w.watcher.Add(filepath.Dir(configPath))
}
go w.run()
return nil
}
func (w *Watcher) run() {
for {
select {
case event, ok := <-w.watcher.Events:
if !ok {
return
}
w.handleEvent(event)
case err, ok := <-w.watcher.Errors:
if !ok {
return
}
log.Printf("Watcher error: %v", err)
case <-w.stopCh:
return
}
}
}
func (w *Watcher) handleEvent(event fsnotify.Event) {
// Skip non-relevant events
if event.Op&(fsnotify.Write|fsnotify.Create|fsnotify.Remove) == 0 {
return
}
// Determine file type
fileType := w.getFileType(event.Name)
if fileType == "" {
return
}
isConfig := strings.HasSuffix(event.Name, "strataconfig.ts")
change := ChangeEvent{
Path: event.Name,
Type: fileType,
Op: event.Op,
IsConfig: isConfig,
}
w.mu.Lock()
w.pending[event.Name] = change
// Reset debounce timer
if w.timer != nil {
w.timer.Stop()
}
w.timer = time.AfterFunc(w.debounce, w.flush)
w.mu.Unlock()
}
func (w *Watcher) flush() {
w.mu.Lock()
pending := w.pending
w.pending = make(map[string]ChangeEvent)
w.mu.Unlock()
for _, event := range pending {
w.onChange(event)
}
}
func (w *Watcher) getFileType(path string) FileType {
ext := filepath.Ext(path)
switch ext {
case ".strata":
return FileTypeStrata
case ".sts":
return FileTypeSTS
case ".scss":
return FileTypeSCSS
case ".ts":
if strings.HasSuffix(path, "strataconfig.ts") {
return FileTypeConfig
}
return FileTypeTS
}
return ""
}
func (w *Watcher) Stop() {
close(w.stopCh)
w.watcher.Close()
}

View File

@@ -0,0 +1,16 @@
{
"name": "strata-example-basic",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "strata dev",
"build": "strata build",
"preview": "strata preview"
},
"dependencies": {
"strata": "^0.1.0"
},
"devDependencies": {
"typescript": "^5.3.0"
}
}

View File

@@ -0,0 +1,144 @@
// Responsive breakpoints
@mixin sm {
@media (min-width: $breakpoint-sm) { @content; }
}
@mixin md {
@media (min-width: $breakpoint-md) { @content; }
}
@mixin lg {
@media (min-width: $breakpoint-lg) { @content; }
}
@mixin xl {
@media (min-width: $breakpoint-xl) { @content; }
}
// Flexbox helpers
@mixin flex-center {
display: flex;
align-items: center;
justify-content: center;
}
@mixin flex-between {
display: flex;
align-items: center;
justify-content: space-between;
}
@mixin flex-column {
display: flex;
flex-direction: column;
}
// Grid helper
@mixin grid($columns: 1, $gap: $spacing-md) {
display: grid;
grid-template-columns: repeat($columns, 1fr);
gap: $gap;
}
@mixin auto-grid($min-width: 280px, $gap: $spacing-md) {
display: grid;
grid-template-columns: repeat(auto-fill, minmax($min-width, 1fr));
gap: $gap;
}
// Typography
@mixin text-truncate {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
@mixin line-clamp($lines: 2) {
display: -webkit-box;
-webkit-line-clamp: $lines;
-webkit-box-orient: vertical;
overflow: hidden;
}
// Interactive states
@mixin hover-lift($distance: -2px) {
transition: transform $transition-normal, box-shadow $transition-normal;
&:hover {
transform: translateY($distance);
box-shadow: $shadow-lg;
}
}
@mixin focus-ring($color: var(--primary)) {
&:focus {
outline: none;
box-shadow: 0 0 0 3px rgba($color, 0.3);
}
&:focus-visible {
outline: none;
box-shadow: 0 0 0 3px rgba($color, 0.3);
}
}
// Card styles
@mixin card($padding: $spacing-lg) {
background: var(--card-bg);
border-radius: $radius-lg;
padding: $padding;
box-shadow: $shadow-md;
}
// Button base
@mixin button-base {
display: inline-flex;
align-items: center;
justify-content: center;
gap: $spacing-sm;
padding: $spacing-sm $spacing-md;
border: none;
border-radius: $radius-md;
font-weight: 500;
cursor: pointer;
transition: background $transition-fast, transform $transition-fast;
&:active {
transform: scale(0.98);
}
&:disabled {
opacity: 0.5;
cursor: not-allowed;
}
}
// Skeleton loading
@mixin skeleton {
background: linear-gradient(
90deg,
var(--border) 25%,
var(--secondary) 50%,
var(--border) 75%
);
background-size: 200% 100%;
animation: skeleton-loading 1.5s infinite;
}
@keyframes skeleton-loading {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}
// Visually hidden (accessible)
@mixin visually-hidden {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}

View File

@@ -0,0 +1,78 @@
// Color palette
$primary: #6366f1;
$primary-dark: #4f46e5;
$secondary: #e5e7eb;
$secondary-dark: #d1d5db;
// Text colors
$text-primary: #1f2937;
$text-secondary: #4b5563;
$text-muted: #9ca3af;
// Background colors
$card-bg: #ffffff;
$code-bg: #f3f4f6;
$border: #e5e7eb;
// CSS custom properties (for runtime theming)
:root {
--primary: #{$primary};
--primary-dark: #{$primary-dark};
--secondary: #{$secondary};
--secondary-dark: #{$secondary-dark};
--text-primary: #{$text-primary};
--text-secondary: #{$text-secondary};
--text-muted: #{$text-muted};
--card-bg: #{$card-bg};
--code-bg: #{$code-bg};
--border: #{$border};
}
// Dark mode
@media (prefers-color-scheme: dark) {
:root {
--primary: #818cf8;
--primary-dark: #6366f1;
--text-primary: #f9fafb;
--text-secondary: #d1d5db;
--text-muted: #6b7280;
--card-bg: #1f2937;
--code-bg: #374151;
--border: #374151;
}
}
// Spacing
$spacing-xs: 0.25rem;
$spacing-sm: 0.5rem;
$spacing-md: 1rem;
$spacing-lg: 1.5rem;
$spacing-xl: 2rem;
$spacing-2xl: 3rem;
// Border radius
$radius-sm: 4px;
$radius-md: 8px;
$radius-lg: 12px;
$radius-xl: 16px;
$radius-full: 9999px;
// Shadows
$shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05);
$shadow-md: 0 4px 6px rgba(0, 0, 0, 0.1);
$shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.1);
// Transitions
$transition-fast: 150ms ease;
$transition-normal: 200ms ease;
$transition-slow: 300ms ease;
// Breakpoints
$breakpoint-sm: 640px;
$breakpoint-md: 768px;
$breakpoint-lg: 1024px;
$breakpoint-xl: 1280px;

View File

@@ -0,0 +1,124 @@
@import 'variables';
@import 'mixins';
// Reset
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
html {
font-size: 16px;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen,
Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
line-height: 1.6;
color: var(--text-primary);
background: var(--secondary);
min-height: 100vh;
}
// Typography
h1, h2, h3, h4, h5, h6 {
line-height: 1.3;
font-weight: 600;
}
h1 { font-size: 2.5rem; }
h2 { font-size: 2rem; }
h3 { font-size: 1.5rem; }
h4 { font-size: 1.25rem; }
h5 { font-size: 1rem; }
h6 { font-size: 0.875rem; }
p {
margin-bottom: 1rem;
&:last-child {
margin-bottom: 0;
}
}
a {
color: var(--primary);
text-decoration: none;
&:hover {
text-decoration: underline;
}
}
// Code
code {
font-family: 'SF Mono', 'Fira Code', 'Consolas', monospace;
font-size: 0.9em;
background: var(--code-bg);
padding: 0.2em 0.4em;
border-radius: $radius-sm;
}
pre {
background: var(--code-bg);
padding: $spacing-md;
border-radius: $radius-md;
overflow-x: auto;
code {
background: none;
padding: 0;
}
}
// Images
img {
max-width: 100%;
height: auto;
}
// Lists
ul, ol {
padding-left: 1.5rem;
}
// Focus styles
:focus-visible {
outline: 2px solid var(--primary);
outline-offset: 2px;
}
// Selection
::selection {
background: var(--primary);
color: white;
}
// App container
#app {
min-height: 100vh;
}
// Utility classes
.sr-only {
@include visually-hidden;
}
.text-center { text-align: center; }
.text-left { text-align: left; }
.text-right { text-align: right; }
.mt-1 { margin-top: $spacing-sm; }
.mt-2 { margin-top: $spacing-md; }
.mt-3 { margin-top: $spacing-lg; }
.mt-4 { margin-top: $spacing-xl; }
.mb-1 { margin-bottom: $spacing-sm; }
.mb-2 { margin-bottom: $spacing-md; }
.mb-3 { margin-bottom: $spacing-lg; }
.mb-4 { margin-bottom: $spacing-xl; }

View File

@@ -0,0 +1,158 @@
<template>
<div class="user-card" s-if="user">
<div class="user-avatar">
<img :src="avatarUrl" :alt="user.name" />
</div>
<div class="user-info">
<h3>{{ user.name }}</h3>
<p class="email">{{ user.email }}</p>
<p class="company" s-if="user.company">
{{ user.company.name }}
</p>
</div>
<div class="user-actions">
<button @click="viewProfile" class="btn-primary">
View Profile
</button>
<button @click="toggleFavorite" class="btn-secondary">
{{ isFavorite ? 'Unfavorite' : 'Favorite' }}
</button>
</div>
</div>
<div class="user-card skeleton" s-else>
Loading...
</div>
</template>
<script>
import { useStore } from 'strata';
import { userStore } from '@stores/user';
import { favoritesStore } from '@stores/favorites';
export default {
props: {
userId: { type: String, required: true }
},
setup(props) {
const user = useStore(userStore, state => state.users[props.userId]);
const isFavorite = useStore(favoritesStore, state =>
state.favorites.includes(props.userId)
);
const avatarUrl = `https://api.dicebear.com/7.x/avataaars/svg?seed=${props.userId}`;
const viewProfile = () => {
strata.navigate(`/users/${props.userId}`);
};
const toggleFavorite = () => {
if (isFavorite) {
favoritesStore.remove(props.userId);
} else {
favoritesStore.add(props.userId);
}
};
return {
user,
isFavorite,
avatarUrl,
viewProfile,
toggleFavorite,
};
}
};
</script>
<style lang="scss">
.user-card {
display: flex;
flex-direction: column;
padding: 1.5rem;
border-radius: 12px;
background: var(--card-bg, #fff);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
transition: transform 0.2s, box-shadow 0.2s;
&:hover {
transform: translateY(-2px);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
}
&.skeleton {
min-height: 200px;
display: flex;
align-items: center;
justify-content: center;
color: var(--text-muted);
}
.user-avatar {
width: 80px;
height: 80px;
border-radius: 50%;
overflow: hidden;
margin: 0 auto 1rem;
img {
width: 100%;
height: 100%;
object-fit: cover;
}
}
.user-info {
text-align: center;
margin-bottom: 1rem;
h3 {
margin: 0 0 0.5rem;
color: var(--text-primary);
}
.email {
color: var(--text-secondary);
font-size: 0.9rem;
}
.company {
color: var(--text-muted);
font-size: 0.85rem;
}
}
.user-actions {
display: flex;
gap: 0.5rem;
button {
flex: 1;
padding: 0.5rem 1rem;
border-radius: 6px;
border: none;
cursor: pointer;
font-weight: 500;
transition: background 0.2s;
&.btn-primary {
background: var(--primary);
color: white;
&:hover {
background: var(--primary-dark);
}
}
&.btn-secondary {
background: var(--secondary);
color: var(--text-primary);
&:hover {
background: var(--secondary-dark);
}
}
}
}
}
</style>

View File

@@ -0,0 +1,9 @@
/* @position: head */
/* @priority: 10 */
<!-- Custom Analytics -->
<script>
window.analyticsQueue = window.analyticsQueue || [];
function analytics() { analyticsQueue.push(arguments); }
analytics('init', { app: 'strata-example' });
</script>

View File

@@ -0,0 +1,9 @@
/* @position: body */
/* @priority: 1 */
<!-- Google Tag Manager (noscript) -->
<noscript>
<iframe src="https://www.googletagmanager.com/ns.html?id=GTM-XXXXXXX"
height="0" width="0" style="display:none;visibility:hidden"></iframe>
</noscript>
<!-- End Google Tag Manager (noscript) -->

View File

@@ -0,0 +1,12 @@
/* @position: head */
/* @priority: 1 */
<!-- Google Tag Manager -->
<script>
(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
})(window,document,'script','dataLayer','GTM-XXXXXXX');
</script>
<!-- End Google Tag Manager -->

View File

@@ -0,0 +1,148 @@
<template>
<main class="home-page">
<header class="hero">
<h1>Welcome to Strata</h1>
<p>A fast, modern framework for building web applications</p>
</header>
<section class="users-section">
<h2>Users</h2>
<!-- Smart fetch with loading state -->
<div
s-fetch="/users"
s-as="users"
s-loading="loading"
s-error="error"
>
<div class="loading-state" s-if="loading">
<div class="spinner"></div>
<p>Loading users...</p>
</div>
<div class="error-state" s-else-if="error">
<p>Failed to load users: {{ error.message }}</p>
<button @click="refetch">Try Again</button>
</div>
<div class="users-grid" s-else>
<UserCard
s-for="user in users"
:key="user.id"
:userId="user.id"
s-client:visible
/>
</div>
</div>
</section>
<section class="stats-section">
<h2>Tab Info</h2>
<p>Current Tab ID: <code>{{ tabId }}</code></p>
<p>Connected Tabs: <code>{{ tabCount }}</code></p>
</section>
</main>
</template>
<script>
import { strata, useStore } from 'strata';
import { userStore } from '@stores/user';
import UserCard from '@components/UserCard.strata';
export default {
components: { UserCard },
setup() {
const tabId = strata.tabId;
const tabCount = ref(1);
// Listen for tab changes
strata.onBroadcast('tab:joined', (data) => {
tabCount.value = data.tabCount;
});
strata.onBroadcast('tab:left', (data) => {
tabCount.value = data.tabCount;
});
return {
tabId,
tabCount,
};
}
};
</script>
<style lang="scss">
.home-page {
max-width: 1200px;
margin: 0 auto;
padding: 2rem;
.hero {
text-align: center;
padding: 4rem 2rem;
background: linear-gradient(135deg, var(--primary) 0%, var(--primary-dark) 100%);
color: white;
border-radius: 16px;
margin-bottom: 3rem;
h1 {
font-size: 3rem;
margin-bottom: 1rem;
}
p {
font-size: 1.25rem;
opacity: 0.9;
}
}
.users-section {
margin-bottom: 3rem;
h2 {
margin-bottom: 1.5rem;
}
.users-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 1.5rem;
}
.loading-state,
.error-state {
text-align: center;
padding: 3rem;
.spinner {
width: 40px;
height: 40px;
border: 3px solid var(--border);
border-top-color: var(--primary);
border-radius: 50%;
animation: spin 1s linear infinite;
margin: 0 auto 1rem;
}
}
}
.stats-section {
background: var(--card-bg);
padding: 2rem;
border-radius: 12px;
code {
background: var(--code-bg);
padding: 0.25rem 0.5rem;
border-radius: 4px;
font-family: monospace;
}
}
}
@keyframes spin {
to { transform: rotate(360deg); }
}
</style>

View File

@@ -0,0 +1,55 @@
/**
* Favorites Store
* Manages user favorites with tab-specific and shared state
*/
import { createStore } from 'strata';
interface FavoritesState {
favorites: string[]; // User IDs
recentlyViewed: string[]; // Tab-specific
}
export const favoritesStore = createStore<FavoritesState>('favorites', {
state: {
favorites: [],
recentlyViewed: [],
},
actions: {
add(userId: string) {
if (!this.favorites.includes(userId)) {
this.favorites = [...this.favorites, userId];
}
},
remove(userId: string) {
this.favorites = this.favorites.filter(id => id !== userId);
},
toggle(userId: string) {
if (this.favorites.includes(userId)) {
this.remove(userId);
} else {
this.add(userId);
}
},
addToRecent(userId: string) {
// Keep only last 10
const recent = this.recentlyViewed.filter(id => id !== userId);
this.recentlyViewed = [userId, ...recent].slice(0, 10);
},
clearRecent() {
this.recentlyViewed = [];
},
},
// Favorites are encrypted and shared across tabs
encrypt: ['favorites'],
persist: true,
shared: ['favorites'], // Sync favorites across all tabs
// recentlyViewed stays tab-specific (not in shared)
});

View File

@@ -0,0 +1,100 @@
/**
* User Store
* Manages user data with encryption and cross-tab sync
*/
import { createStore, strata } from 'strata';
interface User {
id: string;
name: string;
email: string;
phone?: string;
website?: string;
company?: {
name: string;
catchPhrase?: string;
};
}
interface UserState {
users: Record<string, User>;
currentUserId: string | null;
loading: boolean;
error: string | null;
}
export const userStore = createStore<UserState>('user', {
state: {
users: {},
currentUserId: null,
loading: false,
error: null,
},
actions: {
async fetchUsers() {
this.loading = true;
this.error = null;
try {
const users = await strata.fetch.get<User[]>('/users');
// Index users by ID for fast lookup
const usersById: Record<string, User> = {};
for (const user of users) {
usersById[user.id] = user;
}
this.users = usersById;
} catch (err) {
this.error = err instanceof Error ? err.message : 'Failed to fetch users';
} finally {
this.loading = false;
}
},
async fetchUser(id: string) {
if (this.users[id]) {
return this.users[id]; // Already cached
}
try {
const user = await strata.fetch.get<User>(`/users/${id}`);
this.users[id] = user;
return user;
} catch (err) {
throw err;
}
},
setCurrentUser(id: string | null) {
this.currentUserId = id;
},
clearUsers() {
this.users = {};
this.currentUserId = null;
},
},
// Encrypt sensitive data
encrypt: ['currentUserId'],
// Persist user cache across page reloads
persist: ['users'],
// Share current user across all tabs
shared: ['currentUserId'],
});
// Computed values (derived state)
export const getCurrentUser = () => {
const state = userStore.getState();
if (!state.currentUserId) return null;
return state.users[state.currentUserId] || null;
};
export const getUserCount = () => {
return Object.keys(userStore.getState().users).length;
};

View File

@@ -0,0 +1,21 @@
import { defineConfig } from 'strata';
export default defineConfig({
app: {
title: 'Strata Example App',
description: 'A simple example using Strata Framework',
},
api: {
baseUrl: 'https://jsonplaceholder.typicode.com',
cache: {
enabled: true,
defaultTTL: '5m',
},
},
state: {
encrypt: true,
persist: true,
},
});

3892
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

83
package.json Normal file
View File

@@ -0,0 +1,83 @@
{
"name": "strata",
"version": "0.1.0",
"description": "Strata - Static Template Rendering Architecture. Fast, modern framework with static-first approach, islands architecture, and cross-tab state management.",
"keywords": [
"frontend",
"framework",
"jamstack",
"islands",
"static",
"typescript",
"state-management",
"microfrontends"
],
"author": "Strata Team",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/strata/strata.git"
},
"homepage": "https://stratajs.dev",
"main": "dist/index.js",
"module": "dist/index.mjs",
"types": "dist/index.d.ts",
"exports": {
".": {
"import": "./dist/index.mjs",
"require": "./dist/index.js",
"types": "./dist/index.d.ts"
},
"./store": {
"import": "./dist/store.mjs",
"require": "./dist/store.js",
"types": "./dist/store.d.ts"
},
"./fetch": {
"import": "./dist/fetch.mjs",
"require": "./dist/fetch.js",
"types": "./dist/fetch.d.ts"
}
},
"bin": {
"strata": "./bin/strata"
},
"files": [
"dist",
"bin",
"runtime",
"templates"
],
"scripts": {
"build": "npm run build:runtime && npm run build:compiler",
"build:runtime": "tsup runtime/core/strata.sts runtime/store/store.sts runtime/fetch/fetch.sts --format esm,cjs --dts",
"build:compiler": "cd compiler && go build -ldflags='-s -w' -o ../bin/strata ./cmd/strata",
"dev": "tsup --watch",
"test": "vitest",
"lint": "eslint .",
"typecheck": "tsc --noEmit",
"prepublishOnly": "npm run build",
"install:local": "./scripts/install.sh",
"install:global": "./scripts/install.sh --global",
"uninstall": "./scripts/uninstall.sh",
"upgrade": "./scripts/upgrade.sh",
"upgrade:check": "./scripts/upgrade.sh --check",
"doctor": "./scripts/doctor.sh",
"doctor:fix": "./scripts/doctor.sh --fix",
"setup": "npm run build:compiler && npm install",
"clean": "rm -rf bin/ dist/ .strata/",
"release": "make release"
},
"dependencies": {},
"devDependencies": {
"@types/node": "^20.10.0",
"tsup": "^8.0.0",
"typescript": "^5.3.0",
"vitest": "^1.0.0",
"eslint": "^8.55.0"
},
"peerDependencies": {},
"engines": {
"node": ">=18.0.0"
}
}

231
runtime/core/strata.sts Normal file
View File

@@ -0,0 +1,231 @@
/**
* Strata Core Runtime
* Main entry point for browser runtime
*/
import { StrataStore, createStore, useStore } from '../store/store.sts';
import { StrataFetch } from '../fetch/fetch.sts';
interface StrataConfig {
encryptionKey?: number[];
apiBaseUrl?: string;
devMode?: boolean;
}
interface BroadcastHandler {
(data: any): void;
}
class Strata {
private worker: SharedWorker | null = null;
private _tabId: string = '';
private messageHandlers: Map<string, Function[]> = new Map();
private broadcastHandlers: Map<string, BroadcastHandler[]> = new Map();
private pendingRequests: Map<string, { resolve: Function; reject: Function }> = new Map();
private config: StrataConfig = {};
private _fetch: StrataFetch | null = null;
get tabId(): string {
return this._tabId;
}
get fetch(): StrataFetch {
if (!this._fetch) {
throw new Error('Strata not initialized. Call strata.init() first.');
}
return this._fetch;
}
async init(config: StrataConfig = {}): Promise<void> {
this.config = config;
// Initialize SharedWorker
if (typeof SharedWorker !== 'undefined') {
this.worker = new SharedWorker(
new URL('../worker/shared-worker.sts', import.meta.url),
{ type: 'module', name: 'strata-worker' }
);
this.worker.port.onmessage = (event) => this.handleWorkerMessage(event.data);
this.worker.port.start();
// Send encryption key if provided
if (config.encryptionKey) {
this.worker.port.postMessage({
type: 'setEncryptionKey',
key: config.encryptionKey,
});
}
// Wait for connection
await this.waitForConnection();
} else {
// Fallback for browsers without SharedWorker support
this._tabId = 'tab_' + Math.random().toString(36).slice(2, 10);
console.warn('SharedWorker not supported, falling back to single-tab mode');
}
// Initialize fetch
this._fetch = new StrataFetch(this);
// Handle page unload
window.addEventListener('beforeunload', () => {
this.disconnect();
});
if (config.devMode) {
(window as any).__STRATA__ = this;
console.log(`Strata initialized (tabId: ${this._tabId})`);
}
}
private waitForConnection(): Promise<void> {
return new Promise((resolve) => {
const handler = (message: any) => {
if (message.type === 'connected') {
this._tabId = message.tabId;
resolve();
}
};
this.addMessageHandler('connected', handler);
});
}
private handleWorkerMessage(message: any) {
// Handle specific message types
switch (message.type) {
case 'connected':
this._tabId = message.tabId;
break;
case 'fetch:response':
case 'fetch:error':
this.handleFetchResponse(message);
break;
case 'store:value':
this.handleStoreValue(message);
break;
case 'broadcast':
this.handleBroadcast(message);
break;
}
// Call registered handlers
const handlers = this.messageHandlers.get(message.type) || [];
for (const handler of handlers) {
handler(message);
}
}
private handleFetchResponse(message: any) {
const pending = this.pendingRequests.get(message.requestId);
if (pending) {
if (message.type === 'fetch:error') {
pending.reject(new Error(message.error));
} else {
pending.resolve(message.data);
}
this.pendingRequests.delete(message.requestId);
}
}
private handleStoreValue(message: any) {
// Handled by store subscriptions
}
private handleBroadcast(message: any) {
const handlers = this.broadcastHandlers.get(message.event) || [];
for (const handler of handlers) {
handler(message.data);
}
// Also call wildcard handlers
const wildcardHandlers = this.broadcastHandlers.get('*') || [];
for (const handler of wildcardHandlers) {
handler({ event: message.event, data: message.data });
}
}
private addMessageHandler(type: string, handler: Function) {
if (!this.messageHandlers.has(type)) {
this.messageHandlers.set(type, []);
}
this.messageHandlers.get(type)!.push(handler);
}
// Public API
sendToWorker(message: any): void {
if (this.worker) {
this.worker.port.postMessage(message);
}
}
async fetchViaWorker(url: string, options?: any): Promise<any> {
const requestId = crypto.randomUUID();
return new Promise((resolve, reject) => {
this.pendingRequests.set(requestId, { resolve, reject });
this.sendToWorker({
type: 'fetch',
url,
options,
requestId,
});
// Timeout after 30 seconds
setTimeout(() => {
if (this.pendingRequests.has(requestId)) {
this.pendingRequests.delete(requestId);
reject(new Error('Request timeout'));
}
}, 30000);
});
}
broadcast(event: string, data?: any, targetTabs?: string[]): void {
this.sendToWorker({
type: 'broadcast',
event,
data,
targetTabs,
});
}
onBroadcast(event: string, handler: BroadcastHandler): () => void {
if (!this.broadcastHandlers.has(event)) {
this.broadcastHandlers.set(event, []);
}
this.broadcastHandlers.get(event)!.push(handler);
// Return unsubscribe function
return () => {
const handlers = this.broadcastHandlers.get(event);
if (handlers) {
const index = handlers.indexOf(handler);
if (index > -1) {
handlers.splice(index, 1);
}
}
};
}
invalidateCache(pattern: string = '*'): void {
this.sendToWorker({
type: 'cache:invalidate',
pattern,
});
}
private disconnect(): void {
this.sendToWorker({ type: 'disconnect' });
}
}
// Singleton instance
export const strata = new Strata();
// Re-exports
export { createStore, useStore };
export type { StrataStore };

233
runtime/fetch/fetch.sts Normal file
View File

@@ -0,0 +1,233 @@
/**
* Strata Smart Fetch
* Deduplication, caching, and no re-render for unchanged data
*/
import type { Strata } from '../core/strata.sts';
interface FetchOptions {
method?: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';
headers?: Record<string, string>;
body?: any;
cache?: 'none' | 'smart' | 'permanent' | string; // string for TTL like '5m', '1h'
stale?: string; // stale-while-revalidate TTL
dedupe?: boolean; // default true
transform?: (data: any) => any;
}
interface QueryState<T> {
data: T | null;
loading: boolean;
error: Error | null;
isStale: boolean;
}
type QuerySubscriber<T> = (state: QueryState<T>) => void;
export class StrataFetch {
private strata: any; // Strata instance
private queryCache: Map<string, QueryState<any>> = new Map();
private subscribers: Map<string, Set<QuerySubscriber<any>>> = new Map();
constructor(strata: any) {
this.strata = strata;
}
/**
* Basic fetch with deduplication and caching
*/
async fetch<T = any>(url: string, options: FetchOptions = {}): Promise<T> {
const fullUrl = this.resolveUrl(url);
const data = await this.strata.fetchViaWorker(fullUrl, {
method: options.method || 'GET',
headers: options.headers,
body: options.body,
cache: options.cache || 'smart',
});
return options.transform ? options.transform(data) : data;
}
/**
* GET request
*/
async get<T = any>(url: string, options?: Omit<FetchOptions, 'method'>): Promise<T> {
return this.fetch<T>(url, { ...options, method: 'GET' });
}
/**
* POST request
*/
async post<T = any>(url: string, body?: any, options?: Omit<FetchOptions, 'method' | 'body'>): Promise<T> {
return this.fetch<T>(url, { ...options, method: 'POST', body });
}
/**
* PUT request
*/
async put<T = any>(url: string, body?: any, options?: Omit<FetchOptions, 'method' | 'body'>): Promise<T> {
return this.fetch<T>(url, { ...options, method: 'PUT', body });
}
/**
* DELETE request
*/
async delete<T = any>(url: string, options?: Omit<FetchOptions, 'method'>): Promise<T> {
return this.fetch<T>(url, { ...options, method: 'DELETE' });
}
/**
* Reactive query hook - only re-renders when data changes
*/
useQuery<T = any>(
url: string,
options: FetchOptions = {}
): {
subscribe: (subscriber: QuerySubscriber<T>) => () => void;
refetch: () => Promise<void>;
getData: () => QueryState<T>;
} {
const cacheKey = this.getCacheKey(url, options);
// Initialize state if not exists
if (!this.queryCache.has(cacheKey)) {
this.queryCache.set(cacheKey, {
data: null,
loading: true,
error: null,
isStale: false,
});
// Start initial fetch
this.executeQuery(url, options, cacheKey);
}
return {
subscribe: (subscriber: QuerySubscriber<T>) => {
if (!this.subscribers.has(cacheKey)) {
this.subscribers.set(cacheKey, new Set());
}
this.subscribers.get(cacheKey)!.add(subscriber);
// Immediately call with current state
subscriber(this.queryCache.get(cacheKey)!);
// Return unsubscribe
return () => {
this.subscribers.get(cacheKey)?.delete(subscriber);
};
},
refetch: () => this.executeQuery(url, options, cacheKey),
getData: () => this.queryCache.get(cacheKey)!,
};
}
private async executeQuery(url: string, options: FetchOptions, cacheKey: string): Promise<void> {
const currentState = this.queryCache.get(cacheKey)!;
// Set loading (but keep old data for stale-while-revalidate)
this.updateQueryState(cacheKey, {
...currentState,
loading: true,
isStale: currentState.data !== null,
});
try {
const newData = await this.fetch(url, options);
// Deep compare with existing data
const hasChanged = !this.deepEqual(currentState.data, newData);
if (hasChanged) {
this.updateQueryState(cacheKey, {
data: newData,
loading: false,
error: null,
isStale: false,
});
} else {
// Data unchanged - just update loading state, no re-render trigger
const state = this.queryCache.get(cacheKey)!;
state.loading = false;
state.isStale = false;
// Don't notify subscribers since data didn't change
}
} catch (error) {
this.updateQueryState(cacheKey, {
...currentState,
loading: false,
error: error instanceof Error ? error : new Error('Unknown error'),
isStale: false,
});
}
}
private updateQueryState<T>(cacheKey: string, state: QueryState<T>): void {
this.queryCache.set(cacheKey, state);
// Notify subscribers
const subs = this.subscribers.get(cacheKey);
if (subs) {
for (const subscriber of subs) {
subscriber(state);
}
}
}
private getCacheKey(url: string, options: FetchOptions): string {
const method = options.method || 'GET';
const body = options.body ? JSON.stringify(options.body) : '';
return `${method}:${url}:${body}`;
}
private resolveUrl(url: string): string {
if (url.startsWith('http://') || url.startsWith('https://')) {
return url;
}
// Use base URL from config if available
const baseUrl = (window as any).__STRATA_CONFIG__?.apiBaseUrl || '';
return baseUrl + url;
}
private deepEqual(a: any, b: any): boolean {
if (a === b) return true;
if (a === null || b === null) return false;
if (typeof a !== typeof b) return false;
if (typeof a === 'object') {
if (Array.isArray(a) !== Array.isArray(b)) return false;
const keysA = Object.keys(a);
const keysB = Object.keys(b);
if (keysA.length !== keysB.length) return false;
for (const key of keysA) {
if (!this.deepEqual(a[key], b[key])) return false;
}
return true;
}
return false;
}
/**
* Invalidate cache for specific URLs
*/
invalidate(pattern?: string): void {
this.strata.invalidateCache(pattern || '*');
// Also clear local query cache
if (!pattern || pattern === '*') {
this.queryCache.clear();
} else {
for (const key of this.queryCache.keys()) {
if (key.includes(pattern)) {
this.queryCache.delete(key);
}
}
}
}
}

260
runtime/store/store.sts Normal file
View File

@@ -0,0 +1,260 @@
/**
* Strata Store
* Fast state management with encryption and cross-tab sync
*/
import { strata } from '../core/strata.sts';
type Subscriber<T> = (state: T, prevState: T) => void;
type Selector<T, R> = (state: T) => R;
interface StoreOptions<T> {
state: T;
actions?: Record<string, (this: T & StoreActions<T>, ...args: any[]) => any>;
encrypt?: boolean | string[]; // true = all, string[] = specific fields
persist?: boolean | string[]; // true = all, string[] = specific fields
shared?: boolean | string[]; // true = all, string[] = specific fields across tabs
}
interface StoreActions<T> {
$set: (partial: Partial<T>) => void;
$reset: () => void;
$subscribe: (subscriber: Subscriber<T>) => () => void;
}
export interface StrataStore<T> extends StoreActions<T> {
getState: () => T;
setState: (partial: Partial<T>) => void;
}
const stores: Map<string, StrataStore<any>> = new Map();
/**
* Create a new store
*/
export function createStore<T extends object>(
name: string,
options: StoreOptions<T>
): StrataStore<T> & T {
if (stores.has(name)) {
return stores.get(name) as StrataStore<T> & T;
}
const initialState = { ...options.state };
let state = { ...initialState };
const subscribers: Set<Subscriber<T>> = new Set();
// Track which fields should be encrypted/persisted/shared
const encryptFields = normalizeFields(options.encrypt);
const persistFields = normalizeFields(options.persist);
const sharedFields = normalizeFields(options.shared);
function normalizeFields(opt: boolean | string[] | undefined): Set<string> | 'all' | null {
if (opt === true) return 'all';
if (Array.isArray(opt)) return new Set(opt);
return null;
}
function shouldEncrypt(field: string): boolean {
if (encryptFields === 'all') return true;
if (encryptFields instanceof Set) return encryptFields.has(field);
return false;
}
function shouldPersist(field: string): boolean {
if (persistFields === 'all') return true;
if (persistFields instanceof Set) return persistFields.has(field);
return false;
}
function shouldShare(field: string): boolean {
if (sharedFields === 'all') return true;
if (sharedFields instanceof Set) return sharedFields.has(field);
return false;
}
function notify(prevState: T) {
for (const subscriber of subscribers) {
subscriber(state, prevState);
}
}
function setState(partial: Partial<T>) {
const prevState = { ...state };
const changes = Object.entries(partial);
for (const [key, value] of changes) {
(state as any)[key] = value;
// Sync to SharedWorker
if (shouldPersist(key) || shouldShare(key)) {
strata.sendToWorker({
type: 'store:set',
storeName: name,
key,
value,
scope: shouldShare(key) ? 'shared' : 'tab',
encrypt: shouldEncrypt(key),
});
}
}
notify(prevState);
}
function getState(): T {
return { ...state };
}
// Core store object
const store: StrataStore<T> = {
getState,
setState,
$set: setState,
$reset: () => {
setState(initialState as Partial<T>);
},
$subscribe: (subscriber: Subscriber<T>) => {
subscribers.add(subscriber);
return () => subscribers.delete(subscriber);
},
};
// Bind actions
if (options.actions) {
for (const [actionName, actionFn] of Object.entries(options.actions)) {
(store as any)[actionName] = function (...args: any[]) {
return actionFn.apply(
new Proxy(state, {
set(target, prop, value) {
setState({ [prop]: value } as Partial<T>);
return true;
},
get(target, prop) {
if (prop === '$set') return setState;
if (prop === '$reset') return store.$reset;
return (target as any)[prop];
},
}),
args
);
};
}
}
// Create proxy for direct property access
const proxy = new Proxy(store as StrataStore<T> & T, {
get(target, prop: string) {
if (prop in target) return (target as any)[prop];
return state[prop as keyof T];
},
set(target, prop: string, value) {
if (prop in state) {
setState({ [prop]: value } as Partial<T>);
return true;
}
return false;
},
});
// Listen for cross-tab updates
strata.onBroadcast('store:changed', (data: any) => {
if (data.storeName === name && shouldShare(data.key)) {
const prevState = { ...state };
(state as any)[data.key] = data.value;
notify(prevState);
}
});
// Load persisted state
loadPersistedState(name, state, persistFields, sharedFields);
stores.set(name, proxy);
return proxy;
}
/**
* Hook to use store with optional selector
*/
export function useStore<T, R = T>(
store: StrataStore<T>,
selector?: Selector<T, R>
): R {
const state = store.getState();
return selector ? selector(state) : (state as unknown as R);
}
/**
* Subscribe to store changes with selector
*/
export function subscribeToStore<T, R>(
store: StrataStore<T>,
selector: Selector<T, R>,
callback: (value: R, prevValue: R) => void
): () => void {
let prevValue = selector(store.getState());
return store.$subscribe((state, prevState) => {
const newValue = selector(state);
const oldValue = selector(prevState);
// Only call if selected value changed
if (!deepEqual(newValue, oldValue)) {
callback(newValue, prevValue);
prevValue = newValue;
}
});
}
async function loadPersistedState<T>(
storeName: string,
state: T,
persistFields: Set<string> | 'all' | null,
sharedFields: Set<string> | 'all' | null
): Promise<void> {
// Load from SharedWorker on init
const fields = new Set<string>();
if (persistFields === 'all' || sharedFields === 'all') {
Object.keys(state as object).forEach((k) => fields.add(k));
} else {
if (persistFields instanceof Set) persistFields.forEach((k) => fields.add(k));
if (sharedFields instanceof Set) sharedFields.forEach((k) => fields.add(k));
}
// Request each field from worker
// (In production, batch this into single request)
for (const field of fields) {
strata.sendToWorker({
type: 'store:get',
storeName,
key: field,
scope: sharedFields === 'all' || (sharedFields instanceof Set && sharedFields.has(field))
? 'shared'
: 'tab',
});
}
}
function deepEqual(a: any, b: any): boolean {
if (a === b) return true;
if (a === null || b === null) return false;
if (typeof a !== typeof b) return false;
if (typeof a === 'object') {
if (Array.isArray(a) !== Array.isArray(b)) return false;
const keysA = Object.keys(a);
const keysB = Object.keys(b);
if (keysA.length !== keysB.length) return false;
for (const key of keysA) {
if (!deepEqual(a[key], b[key])) return false;
}
return true;
}
return false;
}

View File

@@ -0,0 +1,392 @@
/**
* Strata Shared Worker
* Manages cross-tab state, caching, and API deduplication
*/
interface TabConnection {
port: MessagePort;
tabId: string;
connectedAt: number;
lastActivity: number;
}
interface CacheEntry {
data: any;
timestamp: number;
ttl: number;
etag?: string;
}
interface InFlightRequest {
promise: Promise<any>;
subscribers: string[]; // tabIds waiting for this request
}
interface StoreState {
[storeName: string]: {
shared: Record<string, any>;
tabs: Record<string, Record<string, any>>;
};
}
class StrataSharedWorker {
private tabs: Map<string, TabConnection> = new Map();
private cache: Map<string, CacheEntry> = new Map();
private inFlight: Map<string, InFlightRequest> = new Map();
private stores: StoreState = {};
private encryptionKey: Uint8Array | null = null;
constructor() {
self.onconnect = (e: MessageEvent) => this.handleConnect(e);
}
private generateTabId(): string {
return 'tab_' + crypto.randomUUID().slice(0, 8);
}
private handleConnect(e: MessageEvent) {
const port = e.ports[0];
const tabId = this.generateTabId();
const connection: TabConnection = {
port,
tabId,
connectedAt: Date.now(),
lastActivity: Date.now(),
};
this.tabs.set(tabId, connection);
port.onmessage = (event) => this.handleMessage(tabId, event.data);
port.start();
// Send tabId to the new tab
port.postMessage({
type: 'connected',
tabId,
tabCount: this.tabs.size,
});
// Notify other tabs
this.broadcast('tab:joined', { tabId, tabCount: this.tabs.size }, [tabId]);
}
private handleMessage(tabId: string, message: any) {
const tab = this.tabs.get(tabId);
if (tab) {
tab.lastActivity = Date.now();
}
switch (message.type) {
case 'fetch':
this.handleFetch(tabId, message);
break;
case 'store:get':
this.handleStoreGet(tabId, message);
break;
case 'store:set':
this.handleStoreSet(tabId, message);
break;
case 'store:subscribe':
this.handleStoreSubscribe(tabId, message);
break;
case 'broadcast':
this.handleBroadcast(tabId, message);
break;
case 'cache:invalidate':
this.handleCacheInvalidate(message);
break;
case 'disconnect':
this.handleDisconnect(tabId);
break;
case 'setEncryptionKey':
this.setEncryptionKey(message.key);
break;
}
}
private async handleFetch(tabId: string, message: any) {
const { url, options, requestId } = message;
const cacheKey = this.getCacheKey(url, options);
// Check cache first
const cached = this.cache.get(cacheKey);
if (cached && !this.isCacheExpired(cached)) {
this.sendToTab(tabId, {
type: 'fetch:response',
requestId,
data: cached.data,
fromCache: true,
});
return;
}
// Check if request is already in flight
const inFlight = this.inFlight.get(cacheKey);
if (inFlight) {
// Add this tab to subscribers
inFlight.subscribers.push(tabId);
const data = await inFlight.promise;
this.sendToTab(tabId, {
type: 'fetch:response',
requestId,
data,
deduplicated: true,
});
return;
}
// Make the request
const fetchPromise = this.executeFetch(url, options);
this.inFlight.set(cacheKey, {
promise: fetchPromise,
subscribers: [tabId],
});
try {
const data = await fetchPromise;
const inFlightEntry = this.inFlight.get(cacheKey);
// Cache the response
if (options?.cache !== 'none') {
const ttl = this.parseTTL(options?.cache || 'smart');
this.cache.set(cacheKey, {
data,
timestamp: Date.now(),
ttl,
});
}
// Send to all subscribers
if (inFlightEntry) {
for (const subTabId of inFlightEntry.subscribers) {
this.sendToTab(subTabId, {
type: 'fetch:response',
requestId,
data,
fromCache: false,
});
}
}
} catch (error) {
const inFlightEntry = this.inFlight.get(cacheKey);
if (inFlightEntry) {
for (const subTabId of inFlightEntry.subscribers) {
this.sendToTab(subTabId, {
type: 'fetch:error',
requestId,
error: error instanceof Error ? error.message : 'Unknown error',
});
}
}
} finally {
this.inFlight.delete(cacheKey);
}
}
private async executeFetch(url: string, options: any): Promise<any> {
const response = await fetch(url, {
method: options?.method || 'GET',
headers: options?.headers,
body: options?.body ? JSON.stringify(options.body) : undefined,
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return response.json();
}
private getCacheKey(url: string, options: any): string {
const method = options?.method || 'GET';
const body = options?.body ? JSON.stringify(options.body) : '';
return `${method}:${url}:${body}`;
}
private isCacheExpired(entry: CacheEntry): boolean {
if (entry.ttl === -1) return false; // permanent
return Date.now() - entry.timestamp > entry.ttl;
}
private parseTTL(cache: string): number {
if (cache === 'permanent') return -1;
if (cache === 'none') return 0;
if (cache === 'smart') return 5 * 60 * 1000; // 5 minutes default
const match = cache.match(/^(\d+)(s|m|h|d)$/);
if (match) {
const value = parseInt(match[1]);
const unit = match[2];
const multipliers: Record<string, number> = {
s: 1000,
m: 60 * 1000,
h: 60 * 60 * 1000,
d: 24 * 60 * 60 * 1000,
};
return value * multipliers[unit];
}
return 5 * 60 * 1000; // default 5 minutes
}
private handleStoreGet(tabId: string, message: any) {
const { storeName, key, scope } = message;
const store = this.stores[storeName];
if (!store) {
this.sendToTab(tabId, {
type: 'store:value',
storeName,
key,
value: undefined,
});
return;
}
let value;
if (scope === 'tab') {
value = store.tabs[tabId]?.[key];
} else {
value = store.shared[key];
}
// Decrypt if needed
if (this.encryptionKey && value) {
value = this.decrypt(value);
}
this.sendToTab(tabId, {
type: 'store:value',
storeName,
key,
value,
});
}
private handleStoreSet(tabId: string, message: any) {
const { storeName, key, value, scope, encrypt } = message;
if (!this.stores[storeName]) {
this.stores[storeName] = { shared: {}, tabs: {} };
}
let storedValue = value;
if (encrypt && this.encryptionKey) {
storedValue = this.encrypt(value);
}
if (scope === 'tab') {
if (!this.stores[storeName].tabs[tabId]) {
this.stores[storeName].tabs[tabId] = {};
}
this.stores[storeName].tabs[tabId][key] = storedValue;
} else {
this.stores[storeName].shared[key] = storedValue;
// Notify all tabs about shared state change
this.broadcast('store:changed', {
storeName,
key,
value,
});
}
}
private handleStoreSubscribe(tabId: string, message: any) {
// Store subscriptions are handled via broadcast
// When store changes, all tabs receive updates
}
private handleBroadcast(fromTabId: string, message: any) {
const { event, data, targetTabs } = message;
this.broadcast(event, data, targetTabs ? undefined : [fromTabId]);
}
private handleCacheInvalidate(message: any) {
const { pattern } = message;
if (pattern === '*') {
this.cache.clear();
} else {
for (const key of this.cache.keys()) {
if (key.includes(pattern)) {
this.cache.delete(key);
}
}
}
}
private handleDisconnect(tabId: string) {
this.tabs.delete(tabId);
// Clean up tab-specific store data
for (const storeName in this.stores) {
delete this.stores[storeName].tabs[tabId];
}
// Notify remaining tabs
this.broadcast('tab:left', { tabId, tabCount: this.tabs.size });
}
private broadcast(event: string, data: any, excludeTabs: string[] = []) {
for (const [tabId, tab] of this.tabs) {
if (!excludeTabs.includes(tabId)) {
tab.port.postMessage({
type: 'broadcast',
event,
data,
});
}
}
}
private sendToTab(tabId: string, message: any) {
const tab = this.tabs.get(tabId);
if (tab) {
tab.port.postMessage(message);
}
}
private setEncryptionKey(keyArray: number[]) {
this.encryptionKey = new Uint8Array(keyArray);
}
private encrypt(data: any): string {
if (!this.encryptionKey) return JSON.stringify(data);
const jsonStr = JSON.stringify(data);
const encoder = new TextEncoder();
const dataBytes = encoder.encode(jsonStr);
// XOR encryption with key (simple, fast)
// In production, use SubtleCrypto AES-GCM
const encrypted = new Uint8Array(dataBytes.length);
for (let i = 0; i < dataBytes.length; i++) {
encrypted[i] = dataBytes[i] ^ this.encryptionKey[i % this.encryptionKey.length];
}
return btoa(String.fromCharCode(...encrypted));
}
private decrypt(encryptedStr: string): any {
if (!this.encryptionKey) return JSON.parse(encryptedStr);
const encrypted = new Uint8Array(
atob(encryptedStr)
.split('')
.map((c) => c.charCodeAt(0))
);
const decrypted = new Uint8Array(encrypted.length);
for (let i = 0; i < encrypted.length; i++) {
decrypted[i] = encrypted[i] ^ this.encryptionKey[i % this.encryptionKey.length];
}
const decoder = new TextDecoder();
return JSON.parse(decoder.decode(decrypted));
}
}
// Initialize worker
new StrataSharedWorker();

294
scripts/doctor.sh Executable file
View File

@@ -0,0 +1,294 @@
#!/bin/bash
# Strata Framework - Doctor Script
# Diagnoses installation issues and provides fixes
# Usage: ./scripts/doctor.sh [--fix]
set -e
# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
CYAN='\033[0;36m'
NC='\033[0m'
BOLD='\033[1m'
# Directories
STRATA_HOME="${STRATA_HOME:-$HOME/.strata}"
STRATA_BIN="$STRATA_HOME/bin"
STRATA_CONFIG="$STRATA_HOME/config"
LOCAL_BIN="/usr/local/bin"
# Script directory
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_DIR="$(dirname "$SCRIPT_DIR")"
# Options
AUTO_FIX=false
if [ "$1" = "--fix" ] || [ "$1" = "-f" ]; then
AUTO_FIX=true
fi
if [ "$1" = "--help" ] || [ "$1" = "-h" ]; then
echo "Strata Framework - Doctor"
echo ""
echo "Usage: ./scripts/doctor.sh [options]"
echo ""
echo "Options:"
echo " --fix, -f Automatically fix issues where possible"
echo " --help Show this help message"
exit 0
fi
# Counters
ISSUES=0
WARNINGS=0
FIXES=0
print_check() {
echo -e "${BLUE}[CHECK]${NC} $1"
}
print_ok() {
echo -e "${GREEN} [OK]${NC} $1"
}
print_warn() {
echo -e "${YELLOW} [WARN]${NC} $1"
((WARNINGS++))
}
print_fail() {
echo -e "${RED} [FAIL]${NC} $1"
((ISSUES++))
}
print_fix() {
echo -e "${CYAN} [FIX]${NC} $1"
((FIXES++))
}
print_info() {
echo -e " $1"
}
echo ""
echo -e "${CYAN}╔═══════════════════════════════════════════════════════╗${NC}"
echo -e "${CYAN}║ Strata Framework - Doctor ║${NC}"
echo -e "${CYAN}╚═══════════════════════════════════════════════════════╝${NC}"
echo ""
# Check Go installation
print_check "Go installation"
if command -v go &> /dev/null; then
GO_VERSION=$(go version | grep -oE 'go[0-9]+\.[0-9]+' | sed 's/go//')
GO_MAJOR=$(echo "$GO_VERSION" | cut -d. -f1)
GO_MINOR=$(echo "$GO_VERSION" | cut -d. -f2)
if [ "$GO_MAJOR" -ge 1 ] && [ "$GO_MINOR" -ge 21 ]; then
print_ok "Go $GO_VERSION installed"
else
print_warn "Go $GO_VERSION installed (>= 1.21 recommended)"
fi
else
print_fail "Go is not installed"
print_info "Install from: https://go.dev/dl/"
fi
# Check Node.js installation
print_check "Node.js installation"
if command -v node &> /dev/null; then
NODE_VERSION=$(node -v | sed 's/v//')
NODE_MAJOR=$(echo "$NODE_VERSION" | cut -d. -f1)
if [ "$NODE_MAJOR" -ge 18 ]; then
print_ok "Node.js $NODE_VERSION installed"
else
print_fail "Node.js $NODE_VERSION installed (>= 18.0 required)"
print_info "Install from: https://nodejs.org/"
fi
else
print_fail "Node.js is not installed"
print_info "Install from: https://nodejs.org/"
fi
# Check npm installation
print_check "npm installation"
if command -v npm &> /dev/null; then
NPM_VERSION=$(npm -v)
print_ok "npm $NPM_VERSION installed"
else
print_fail "npm is not installed"
fi
# Check STRATA_HOME directory
print_check "Strata home directory ($STRATA_HOME)"
if [ -d "$STRATA_HOME" ]; then
print_ok "Directory exists"
else
print_fail "Directory does not exist"
if [ "$AUTO_FIX" = true ]; then
mkdir -p "$STRATA_HOME" "$STRATA_BIN" "$STRATA_CONFIG"
print_fix "Created $STRATA_HOME"
else
print_info "Run with --fix or: mkdir -p $STRATA_HOME"
fi
fi
# Check strata binary
print_check "Strata binary"
if [ -x "$STRATA_BIN/strata" ]; then
print_ok "Binary exists at $STRATA_BIN/strata"
# Try to get version
if STRATA_VERSION=$("$STRATA_BIN/strata" version 2>/dev/null); then
print_ok "Binary works: $STRATA_VERSION"
else
print_warn "Binary exists but may not be functional"
fi
elif [ -x "$REPO_DIR/bin/strata" ]; then
print_warn "Binary only in repo ($REPO_DIR/bin/strata)"
if [ "$AUTO_FIX" = true ]; then
mkdir -p "$STRATA_BIN"
cp "$REPO_DIR/bin/strata" "$STRATA_BIN/strata"
chmod +x "$STRATA_BIN/strata"
print_fix "Copied binary to $STRATA_BIN"
else
print_info "Run with --fix or: cp $REPO_DIR/bin/strata $STRATA_BIN/"
fi
else
print_fail "Strata binary not found"
if [ "$AUTO_FIX" = true ]; then
print_info "Rebuilding compiler..."
cd "$REPO_DIR/compiler"
go build -ldflags="-s -w" -o "$REPO_DIR/bin/strata" ./cmd/strata
mkdir -p "$STRATA_BIN"
cp "$REPO_DIR/bin/strata" "$STRATA_BIN/strata"
chmod +x "$STRATA_BIN/strata"
print_fix "Built and installed binary"
cd "$REPO_DIR"
else
print_info "Run with --fix or: make build"
fi
fi
# Check PATH
print_check "PATH configuration"
if echo "$PATH" | grep -q "$STRATA_BIN"; then
print_ok "$STRATA_BIN is in PATH"
elif echo "$PATH" | grep -q "$LOCAL_BIN" && [ -L "$LOCAL_BIN/strata" ]; then
print_ok "Using global symlink at $LOCAL_BIN/strata"
else
print_warn "$STRATA_BIN is not in PATH"
print_info "Add to your shell config: export PATH=\"\$STRATA_HOME/bin:\$PATH\""
fi
# Check shell configuration
print_check "Shell configuration"
SHELL_TYPE=$(basename "$SHELL")
case "$SHELL_TYPE" in
zsh)
SHELL_CONFIG="$HOME/.zshrc"
;;
bash)
if [ -f "$HOME/.bash_profile" ]; then
SHELL_CONFIG="$HOME/.bash_profile"
else
SHELL_CONFIG="$HOME/.bashrc"
fi
;;
*)
SHELL_CONFIG="$HOME/.profile"
;;
esac
if [ -f "$SHELL_CONFIG" ] && grep -q "# Strata Framework" "$SHELL_CONFIG"; then
print_ok "Shell configured in $SHELL_CONFIG"
else
print_warn "Shell not configured"
print_info "Run: ./scripts/install.sh (will configure shell)"
fi
# Check completions
print_check "Shell completions"
if [ -f "$STRATA_HOME/completions/strata.bash" ] || [ -f "$STRATA_HOME/completions/_strata" ]; then
print_ok "Completions installed"
else
print_warn "Shell completions not installed"
print_info "Run: ./scripts/install.sh (will install completions)"
fi
# Check npm dependencies
print_check "npm dependencies"
if [ -d "$REPO_DIR/node_modules" ]; then
print_ok "node_modules exists"
else
print_fail "node_modules not found"
if [ "$AUTO_FIX" = true ]; then
cd "$REPO_DIR"
npm install --silent
print_fix "Installed npm dependencies"
else
print_info "Run: npm install"
fi
fi
# Check Go modules
print_check "Go modules"
if [ -f "$REPO_DIR/compiler/go.sum" ]; then
print_ok "go.sum exists"
else
print_warn "go.sum not found (may need go mod download)"
fi
# Check compiler source
print_check "Compiler source"
if [ -f "$REPO_DIR/compiler/cmd/strata/main.go" ]; then
print_ok "Compiler source found"
else
print_fail "Compiler source missing"
print_info "Repository may be incomplete"
fi
# Check runtime source
print_check "Runtime source"
if [ -f "$REPO_DIR/runtime/core/strata.sts" ]; then
print_ok "Runtime source found"
else
print_fail "Runtime source missing"
print_info "Repository may be incomplete"
fi
# Check example app
print_check "Example application"
if [ -f "$REPO_DIR/examples/basic-app/strataconfig.ts" ]; then
print_ok "Example app found"
else
print_warn "Example app not found"
fi
# Summary
echo ""
echo "════════════════════════════════════════════════════════"
echo ""
if [ $ISSUES -eq 0 ] && [ $WARNINGS -eq 0 ]; then
echo -e "${GREEN}All checks passed! Strata is healthy.${NC}"
elif [ $ISSUES -eq 0 ]; then
echo -e "${YELLOW}Found $WARNINGS warning(s), but no critical issues.${NC}"
else
echo -e "${RED}Found $ISSUES issue(s) and $WARNINGS warning(s).${NC}"
fi
if [ $FIXES -gt 0 ]; then
echo -e "${CYAN}Applied $FIXES automatic fix(es).${NC}"
fi
if [ $ISSUES -gt 0 ] && [ "$AUTO_FIX" = false ]; then
echo ""
echo "Run with --fix to attempt automatic repairs:"
echo -e " ${CYAN}./scripts/doctor.sh --fix${NC}"
fi
echo ""
exit $ISSUES

67
scripts/env.sh Executable file
View File

@@ -0,0 +1,67 @@
#!/bin/bash
# Strata Framework - Environment Setup
# Source this file to set up the Strata environment without installing
# Usage: source ./scripts/env.sh
# or: . ./scripts/env.sh
# Get the directory of this script
if [ -n "$BASH_SOURCE" ]; then
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
elif [ -n "$ZSH_VERSION" ]; then
SCRIPT_DIR="$(cd "$(dirname "${(%):-%x}")" && pwd)"
else
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
fi
REPO_DIR="$(dirname "$SCRIPT_DIR")"
# Check if binary exists
if [ ! -x "$REPO_DIR/bin/strata" ]; then
echo "Strata binary not found. Building..."
(cd "$REPO_DIR" && make build)
fi
# Set environment variables
export STRATA_DEV=1
export STRATA_REPO="$REPO_DIR"
export PATH="$REPO_DIR/bin:$PATH"
# Set aliases
alias st='strata'
alias stdev='strata dev'
alias stbuild='strata build'
alias stgen='strata generate'
alias stnew='npx create-strata'
# Development aliases
alias stmake='make -C "$STRATA_REPO"'
alias stbuild-compiler='make -C "$STRATA_REPO" build'
alias stclean='make -C "$STRATA_REPO" clean'
alias sttest='make -C "$STRATA_REPO" test'
# Quick function to rebuild and run
strebuild() {
make -C "$STRATA_REPO" build && strata "$@"
}
# Function to run example app
stexample() {
cd "$STRATA_REPO/examples/basic-app" && strata dev
}
echo ""
echo "Strata development environment activated!"
echo ""
echo " Binary: $REPO_DIR/bin/strata"
echo " Version: $(strata version 2>/dev/null || echo 'unknown')"
echo ""
echo " Commands:"
echo " strata - Run strata CLI"
echo " strebuild - Rebuild compiler and run command"
echo " stexample - Run example app"
echo " stmake <cmd> - Run make command in repo"
echo ""
echo " Aliases:"
echo " st, stdev, stbuild, stgen, stnew"
echo ""

260
scripts/get-strata.sh Executable file
View File

@@ -0,0 +1,260 @@
#!/bin/bash
# Strata Framework - Remote Installer
# Usage: curl -fsSL https://stratajs.dev/install | bash
# or: wget -qO- https://stratajs.dev/install | bash
#
# Options (via environment variables):
# STRATA_VERSION=latest Version to install (default: latest)
# STRATA_HOME=~/.strata Installation directory
# STRATA_GLOBAL=1 Install globally to /usr/local/bin
set -e
# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
CYAN='\033[0;36m'
NC='\033[0m'
# Configuration
REPO="strata/strata"
STRATA_VERSION="${STRATA_VERSION:-latest}"
STRATA_HOME="${STRATA_HOME:-$HOME/.strata}"
STRATA_BIN="$STRATA_HOME/bin"
STRATA_GLOBAL="${STRATA_GLOBAL:-0}"
# Print banner
echo ""
echo -e "${CYAN}"
echo " ╔═══════════════════════════════════════════════════════╗"
echo " ║ Strata Framework - Quick Installer ║"
echo " ╚═══════════════════════════════════════════════════════╝"
echo -e "${NC}"
# Detect OS and architecture
detect_platform() {
local os arch
os="$(uname -s)"
arch="$(uname -m)"
case "$os" in
Darwin)
os="darwin"
;;
Linux)
os="linux"
;;
MINGW*|MSYS*|CYGWIN*)
os="windows"
;;
*)
echo -e "${RED}Unsupported operating system: $os${NC}"
exit 1
;;
esac
case "$arch" in
x86_64|amd64)
arch="amd64"
;;
arm64|aarch64)
arch="arm64"
;;
*)
echo -e "${RED}Unsupported architecture: $arch${NC}"
exit 1
;;
esac
echo "${os}-${arch}"
}
# Check for required tools
check_requirements() {
local missing=0
# Check for curl or wget
if ! command -v curl &> /dev/null && ! command -v wget &> /dev/null; then
echo -e "${RED}Error: curl or wget is required${NC}"
missing=1
fi
# Check for tar
if ! command -v tar &> /dev/null; then
echo -e "${RED}Error: tar is required${NC}"
missing=1
fi
if [ $missing -eq 1 ]; then
exit 1
fi
}
# Download file
download() {
local url="$1"
local dest="$2"
if command -v curl &> /dev/null; then
curl -fsSL "$url" -o "$dest"
else
wget -qO "$dest" "$url"
fi
}
# Get latest version from GitHub
get_latest_version() {
local api_url="https://api.github.com/repos/$REPO/releases/latest"
local version
if command -v curl &> /dev/null; then
version=$(curl -fsSL "$api_url" | grep '"tag_name"' | cut -d'"' -f4)
else
version=$(wget -qO- "$api_url" | grep '"tag_name"' | cut -d'"' -f4)
fi
echo "$version"
}
# Main installation
main() {
check_requirements
local platform=$(detect_platform)
echo -e "${BLUE}${NC} Detected platform: $platform"
# Get version
if [ "$STRATA_VERSION" = "latest" ]; then
echo -e "${BLUE}${NC} Fetching latest version..."
STRATA_VERSION=$(get_latest_version)
if [ -z "$STRATA_VERSION" ]; then
echo -e "${YELLOW}${NC} Could not determine latest version, using v0.1.0"
STRATA_VERSION="v0.1.0"
fi
fi
echo -e "${GREEN}${NC} Version: $STRATA_VERSION"
# Create directories
echo -e "${BLUE}${NC} Creating directories..."
mkdir -p "$STRATA_BIN"
mkdir -p "$STRATA_HOME/completions"
mkdir -p "$STRATA_HOME/config"
# Download binary
local binary_name="strata-${platform}"
if [ "$(echo $platform | cut -d'-' -f1)" = "windows" ]; then
binary_name="${binary_name}.exe"
fi
local download_url="https://github.com/$REPO/releases/download/$STRATA_VERSION/$binary_name"
local temp_file=$(mktemp)
echo -e "${BLUE}${NC} Downloading Strata..."
echo " URL: $download_url"
if ! download "$download_url" "$temp_file"; then
echo -e "${RED}Error: Failed to download Strata${NC}"
echo ""
echo "This could mean:"
echo " 1. The release doesn't exist yet"
echo " 2. Network connectivity issues"
echo ""
echo "Try installing from source instead:"
echo -e " ${CYAN}git clone https://github.com/$REPO.git${NC}"
echo -e " ${CYAN}cd strata && make install${NC}"
rm -f "$temp_file"
exit 1
fi
# Install binary
mv "$temp_file" "$STRATA_BIN/strata"
chmod +x "$STRATA_BIN/strata"
echo -e "${GREEN}${NC} Installed to $STRATA_BIN/strata"
# Install globally if requested
if [ "$STRATA_GLOBAL" = "1" ]; then
echo -e "${BLUE}${NC} Installing globally..."
if [ -w /usr/local/bin ]; then
ln -sf "$STRATA_BIN/strata" /usr/local/bin/strata
else
sudo ln -sf "$STRATA_BIN/strata" /usr/local/bin/strata
fi
echo -e "${GREEN}${NC} Linked to /usr/local/bin/strata"
fi
# Configure shell
echo -e "${BLUE}${NC} Configuring shell..."
local shell_type=$(basename "$SHELL")
local shell_config
case "$shell_type" in
zsh)
shell_config="$HOME/.zshrc"
;;
bash)
if [ -f "$HOME/.bash_profile" ]; then
shell_config="$HOME/.bash_profile"
else
shell_config="$HOME/.bashrc"
fi
;;
fish)
shell_config="$HOME/.config/fish/config.fish"
;;
*)
shell_config="$HOME/.profile"
;;
esac
if [ -f "$shell_config" ] && grep -q "# Strata Framework" "$shell_config"; then
echo -e "${GREEN}${NC} Shell already configured"
else
cat >> "$shell_config" << 'SHELL_CONFIG'
# Strata Framework
export STRATA_HOME="$HOME/.strata"
export PATH="$STRATA_HOME/bin:$PATH"
# Strata aliases
alias st='strata'
alias stdev='strata dev'
alias stbuild='strata build'
alias stgen='strata generate'
alias stnew='npx create-strata'
SHELL_CONFIG
echo -e "${GREEN}${NC} Added to $shell_config"
fi
# Create config file
cat > "$STRATA_HOME/config/strata.json" << CONFIG
{
"version": "$STRATA_VERSION",
"installPath": "$STRATA_HOME",
"installedAt": "$(date -u +"%Y-%m-%dT%H:%M:%SZ")",
"installedVia": "quick-installer"
}
CONFIG
# Print success
echo ""
echo -e "${GREEN}════════════════════════════════════════════════════════${NC}"
echo -e "${GREEN} Installation Complete! ${NC}"
echo -e "${GREEN}════════════════════════════════════════════════════════${NC}"
echo ""
echo " Restart your terminal or run:"
echo -e " ${CYAN}source $shell_config${NC}"
echo ""
echo " Then create your first project:"
echo -e " ${CYAN}npx create-strata my-app${NC}"
echo -e " ${CYAN}cd my-app${NC}"
echo -e " ${CYAN}strata dev${NC}"
echo ""
echo " Documentation: https://stratajs.dev/docs"
echo ""
}
main

624
scripts/install.sh Executable file
View File

@@ -0,0 +1,624 @@
#!/bin/bash
# Strata Framework - Local Installation Script
# Usage: ./scripts/install.sh [options]
# --global Install globally to /usr/local
# --local Install locally only (default)
# --skip-deps Skip dependency checks
# --no-shell Skip shell configuration
# --help Show this help message
set -e
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
CYAN='\033[0;36m'
NC='\033[0m' # No Color
BOLD='\033[1m'
# Installation directories
STRATA_HOME="${STRATA_HOME:-$HOME/.strata}"
STRATA_BIN="$STRATA_HOME/bin"
STRATA_COMPLETIONS="$STRATA_HOME/completions"
STRATA_CONFIG="$STRATA_HOME/config"
LOCAL_BIN="/usr/local/bin"
# Script directory (where strata repo is)
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_DIR="$(dirname "$SCRIPT_DIR")"
# Default options
INSTALL_GLOBAL=false
SKIP_DEPS=false
SKIP_SHELL=false
# Parse arguments
while [[ $# -gt 0 ]]; do
case $1 in
--global)
INSTALL_GLOBAL=true
shift
;;
--local)
INSTALL_GLOBAL=false
shift
;;
--skip-deps)
SKIP_DEPS=true
shift
;;
--no-shell)
SKIP_SHELL=true
shift
;;
--help|-h)
echo "Strata Framework - Installation Script"
echo ""
echo "Usage: ./scripts/install.sh [options]"
echo ""
echo "Options:"
echo " --global Install globally to /usr/local/bin (requires sudo)"
echo " --local Install locally to ~/.strata (default)"
echo " --skip-deps Skip dependency version checks"
echo " --no-shell Skip shell configuration (PATH, completions)"
echo " --help Show this help message"
echo ""
echo "Environment Variables:"
echo " STRATA_HOME Installation directory (default: ~/.strata)"
exit 0
;;
*)
echo -e "${RED}Unknown option: $1${NC}"
exit 1
;;
esac
done
# Print banner
print_banner() {
echo ""
echo -e "${CYAN}"
echo " ╔═══════════════════════════════════════════════════════╗"
echo " ║ ║"
echo " ║ ███████╗████████╗██████╗ █████╗ ████████╗ █████╗ ║"
echo " ║ ██╔════╝╚══██╔══╝██╔══██╗██╔══██╗╚══██╔══╝██╔══██╗ ║"
echo " ║ ███████╗ ██║ ██████╔╝███████║ ██║ ███████║ ║"
echo " ║ ╚════██║ ██║ ██╔══██╗██╔══██║ ██║ ██╔══██║ ║"
echo " ║ ███████║ ██║ ██║ ██║██║ ██║ ██║ ██║ ██║ ║"
echo " ║ ╚══════╝ ╚═╝ ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ║"
echo " ║ ║"
echo " ║ Static Template Rendering Architecture ║"
echo " ╚═══════════════════════════════════════════════════════╝"
echo -e "${NC}"
echo ""
}
# Print step
print_step() {
echo -e "${BLUE}${NC} $1"
}
# Print success
print_success() {
echo -e "${GREEN}${NC} $1"
}
# Print warning
print_warning() {
echo -e "${YELLOW}${NC} $1"
}
# Print error
print_error() {
echo -e "${RED}${NC} $1"
}
# Check if command exists
command_exists() {
command -v "$1" &> /dev/null
}
# Get version number from version string
get_version() {
echo "$1" | grep -oE '[0-9]+\.[0-9]+' | head -1
}
# Compare versions (returns 0 if $1 >= $2)
version_gte() {
[ "$(printf '%s\n' "$2" "$1" | sort -V | head -n1)" = "$2" ]
}
# Check system requirements
check_requirements() {
print_step "Checking system requirements..."
echo ""
local requirements_met=true
# Check Go
if command_exists go; then
GO_VERSION=$(go version | grep -oE 'go[0-9]+\.[0-9]+' | sed 's/go//')
if version_gte "$GO_VERSION" "1.21"; then
print_success "Go $GO_VERSION (required: >= 1.21)"
else
print_warning "Go $GO_VERSION found, but >= 1.21 is recommended"
fi
else
print_error "Go is not installed"
echo " Install from: https://go.dev/dl/"
echo " Or use: brew install go (macOS)"
echo " sudo apt install golang-go (Ubuntu/Debian)"
requirements_met=false
fi
# Check Node.js
if command_exists node; then
NODE_VERSION=$(node -v | sed 's/v//')
NODE_MAJOR=$(echo "$NODE_VERSION" | cut -d. -f1)
if [ "$NODE_MAJOR" -ge 18 ]; then
print_success "Node.js $NODE_VERSION (required: >= 18.0)"
else
print_warning "Node.js $NODE_VERSION found, but >= 18.0 is required"
requirements_met=false
fi
else
print_error "Node.js is not installed"
echo " Install from: https://nodejs.org/"
echo " Or use: brew install node (macOS)"
echo " nvm install 20 (using nvm)"
requirements_met=false
fi
# Check npm
if command_exists npm; then
NPM_VERSION=$(npm -v)
print_success "npm $NPM_VERSION"
else
print_error "npm is not installed"
requirements_met=false
fi
# Check git
if command_exists git; then
GIT_VERSION=$(git --version | grep -oE '[0-9]+\.[0-9]+\.[0-9]+')
print_success "Git $GIT_VERSION"
else
print_warning "Git is not installed (optional, but recommended)"
fi
echo ""
if [ "$requirements_met" = false ] && [ "$SKIP_DEPS" = false ]; then
print_error "Some requirements are not met. Install missing dependencies and try again."
echo " Or run with --skip-deps to continue anyway (not recommended)."
exit 1
fi
}
# Create directory structure
create_directories() {
print_step "Creating Strata directories..."
mkdir -p "$STRATA_HOME"
mkdir -p "$STRATA_BIN"
mkdir -p "$STRATA_COMPLETIONS"
mkdir -p "$STRATA_CONFIG"
mkdir -p "$REPO_DIR/bin"
print_success "Created $STRATA_HOME"
}
# Build Go compiler
build_compiler() {
print_step "Building Strata compiler..."
cd "$REPO_DIR/compiler"
# Download Go dependencies
echo " Downloading Go dependencies..."
go mod tidy
go mod download
# Build the compiler
echo " Compiling..."
go build -ldflags="-s -w" -o "$REPO_DIR/bin/strata" ./cmd/strata
# Copy to strata home
cp "$REPO_DIR/bin/strata" "$STRATA_BIN/strata"
chmod +x "$STRATA_BIN/strata"
# Copy create-strata CLI
if [ -f "$REPO_DIR/bin/create-strata" ]; then
cp "$REPO_DIR/bin/create-strata" "$STRATA_BIN/create-strata"
chmod +x "$STRATA_BIN/create-strata"
fi
cd "$REPO_DIR"
print_success "Compiler built successfully"
}
# Install npm dependencies
install_npm_deps() {
print_step "Installing npm dependencies..."
cd "$REPO_DIR"
npm install --silent
print_success "npm dependencies installed"
}
# Install globally
install_global() {
if [ "$INSTALL_GLOBAL" = true ]; then
print_step "Installing globally to $LOCAL_BIN..."
if [ -w "$LOCAL_BIN" ]; then
ln -sf "$STRATA_BIN/strata" "$LOCAL_BIN/strata"
print_success "Linked strata to $LOCAL_BIN/strata"
else
echo " Requires sudo to write to $LOCAL_BIN"
sudo ln -sf "$STRATA_BIN/strata" "$LOCAL_BIN/strata"
print_success "Linked strata to $LOCAL_BIN/strata (with sudo)"
fi
fi
}
# Detect shell (use $SHELL env var which reflects user's login shell)
detect_shell() {
basename "${SHELL:-/bin/bash}"
}
# Get shell config file
get_shell_config() {
local shell_type="$1"
case "$shell_type" in
zsh)
echo "$HOME/.zshrc"
;;
bash)
if [ -f "$HOME/.bash_profile" ]; then
echo "$HOME/.bash_profile"
else
echo "$HOME/.bashrc"
fi
;;
fish)
echo "$HOME/.config/fish/config.fish"
;;
*)
echo "$HOME/.profile"
;;
esac
}
# Configure shell
configure_shell() {
if [ "$SKIP_SHELL" = true ]; then
return
fi
print_step "Configuring shell..."
local shell_type=$(detect_shell)
local shell_config=$(get_shell_config "$shell_type")
local strata_marker="# Strata Framework"
# Check if already configured
if [ -f "$shell_config" ] && grep -q "$strata_marker" "$shell_config"; then
print_success "Shell already configured in $shell_config"
return
fi
# Create backup
if [ -f "$shell_config" ]; then
cp "$shell_config" "${shell_config}.backup.$(date +%Y%m%d%H%M%S)"
fi
# Add Strata configuration
cat >> "$shell_config" << 'SHELL_CONFIG'
# Strata Framework
export STRATA_HOME="$HOME/.strata"
export PATH="$STRATA_HOME/bin:$PATH"
# Strata aliases
alias st='strata'
alias stdev='strata dev'
alias stbuild='strata build'
alias stgen='strata generate'
alias stnew='create-strata'
# Strata completions
SHELL_CONFIG
# Add shell-specific completion loading
case "$shell_type" in
zsh)
cat >> "$shell_config" << 'ZSH_COMPLETION'
if [ -f "$STRATA_HOME/completions/_strata" ]; then
fpath=($STRATA_HOME/completions $fpath)
autoload -Uz compinit && compinit
fi
ZSH_COMPLETION
;;
bash)
cat >> "$shell_config" << 'BASH_COMPLETION'
if [ -f "$STRATA_HOME/completions/strata.bash" ]; then
source "$STRATA_HOME/completions/strata.bash"
fi
BASH_COMPLETION
;;
fish)
cat >> "$shell_config" << 'FISH_COMPLETION'
if test -f "$STRATA_HOME/completions/strata.fish"
source "$STRATA_HOME/completions/strata.fish"
end
FISH_COMPLETION
;;
esac
print_success "Added Strata configuration to $shell_config"
}
# Generate and install completions
install_completions() {
if [ "$SKIP_SHELL" = true ]; then
return
fi
print_step "Installing shell completions..."
# Generate bash completion
cat > "$STRATA_COMPLETIONS/strata.bash" << 'BASH_COMP'
# Strata bash completion
_strata_completions() {
local cur prev opts
COMPREPLY=()
cur="${COMP_WORDS[COMP_CWORD]}"
prev="${COMP_WORDS[COMP_CWORD-1]}"
# Main commands
local commands="dev build preview generate init help version"
# Generate subcommands
local generate_types="component page store layout middleware"
case "$prev" in
strata|st)
COMPREPLY=( $(compgen -W "$commands" -- "$cur") )
return 0
;;
generate|g)
COMPREPLY=( $(compgen -W "$generate_types" -- "$cur") )
return 0
;;
dev)
COMPREPLY=( $(compgen -W "--port --open --host" -- "$cur") )
return 0
;;
build)
COMPREPLY=( $(compgen -W "--analyze --watch --minify --sourcemap" -- "$cur") )
return 0
;;
preview)
COMPREPLY=( $(compgen -W "--port --host" -- "$cur") )
return 0
;;
--port)
COMPREPLY=( $(compgen -W "3000 3001 4000 5000 8080" -- "$cur") )
return 0
;;
esac
# Default: show all commands
if [[ "$cur" == -* ]]; then
COMPREPLY=( $(compgen -W "--help --version" -- "$cur") )
else
COMPREPLY=( $(compgen -W "$commands" -- "$cur") )
fi
}
complete -F _strata_completions strata
complete -F _strata_completions st
BASH_COMP
# Generate zsh completion
cat > "$STRATA_COMPLETIONS/_strata" << 'ZSH_COMP'
#compdef strata st
# Strata zsh completion
_strata() {
local -a commands
local -a generate_types
commands=(
'dev:Start development server with hot reload'
'build:Build for production'
'preview:Preview production build'
'generate:Generate component, page, or store'
'init:Initialize new Strata project'
'help:Show help information'
'version:Show version information'
)
generate_types=(
'component:Generate a new component'
'page:Generate a new page'
'store:Generate a new store'
'layout:Generate a new layout'
'middleware:Generate a new middleware'
)
_arguments -C \
'1: :->command' \
'*: :->args'
case "$state" in
command)
_describe -t commands 'strata commands' commands
;;
args)
case "$words[2]" in
generate|g)
if [[ $CURRENT -eq 3 ]]; then
_describe -t generate_types 'generate types' generate_types
else
_files
fi
;;
dev)
_arguments \
'--port[Port number]:port:(3000 3001 4000 5000 8080)' \
'--open[Open browser automatically]' \
'--host[Host to bind]:host:(localhost 0.0.0.0)'
;;
build)
_arguments \
'--analyze[Analyze bundle size]' \
'--watch[Watch for changes]' \
'--minify[Minify output]' \
'--sourcemap[Generate sourcemaps]'
;;
preview)
_arguments \
'--port[Port number]:port:(4000 5000 8080)' \
'--host[Host to bind]:host:(localhost 0.0.0.0)'
;;
*)
_files
;;
esac
;;
esac
}
_strata "$@"
ZSH_COMP
# Generate fish completion
cat > "$STRATA_COMPLETIONS/strata.fish" << 'FISH_COMP'
# Strata fish completion
# Main commands
complete -c strata -f -n "__fish_use_subcommand" -a "dev" -d "Start development server"
complete -c strata -f -n "__fish_use_subcommand" -a "build" -d "Build for production"
complete -c strata -f -n "__fish_use_subcommand" -a "preview" -d "Preview production build"
complete -c strata -f -n "__fish_use_subcommand" -a "generate" -d "Generate component/page/store"
complete -c strata -f -n "__fish_use_subcommand" -a "init" -d "Initialize new project"
complete -c strata -f -n "__fish_use_subcommand" -a "help" -d "Show help"
complete -c strata -f -n "__fish_use_subcommand" -a "version" -d "Show version"
# Generate subcommands
complete -c strata -f -n "__fish_seen_subcommand_from generate" -a "component" -d "Generate component"
complete -c strata -f -n "__fish_seen_subcommand_from generate" -a "page" -d "Generate page"
complete -c strata -f -n "__fish_seen_subcommand_from generate" -a "store" -d "Generate store"
complete -c strata -f -n "__fish_seen_subcommand_from generate" -a "layout" -d "Generate layout"
# Dev options
complete -c strata -f -n "__fish_seen_subcommand_from dev" -l port -d "Port number"
complete -c strata -f -n "__fish_seen_subcommand_from dev" -l open -d "Open browser"
complete -c strata -f -n "__fish_seen_subcommand_from dev" -l host -d "Host to bind"
# Build options
complete -c strata -f -n "__fish_seen_subcommand_from build" -l analyze -d "Analyze bundle"
complete -c strata -f -n "__fish_seen_subcommand_from build" -l watch -d "Watch mode"
complete -c strata -f -n "__fish_seen_subcommand_from build" -l minify -d "Minify output"
complete -c strata -f -n "__fish_seen_subcommand_from build" -l sourcemap -d "Generate sourcemaps"
# Alias
complete -c st -w strata
FISH_COMP
print_success "Shell completions installed"
}
# Create config file
create_config() {
print_step "Creating configuration..."
# Create global strata config
cat > "$STRATA_CONFIG/strata.json" << CONFIG
{
"version": "0.1.0",
"installPath": "$STRATA_HOME",
"repoPath": "$REPO_DIR",
"installedAt": "$(date -u +"%Y-%m-%dT%H:%M:%SZ")",
"shell": {
"configured": $([ "$SKIP_SHELL" = true ] && echo "false" || echo "true"),
"completions": $([ "$SKIP_SHELL" = true ] && echo "false" || echo "true")
},
"global": $INSTALL_GLOBAL
}
CONFIG
print_success "Configuration created at $STRATA_CONFIG/strata.json"
}
# Print success message
print_final() {
echo ""
echo -e "${GREEN}════════════════════════════════════════════════════════${NC}"
echo -e "${GREEN} Installation Complete! ${NC}"
echo -e "${GREEN}════════════════════════════════════════════════════════${NC}"
echo ""
echo -e " ${BOLD}Strata has been installed to:${NC} $STRATA_HOME"
echo ""
echo -e " ${BOLD}Quick Start:${NC}"
echo ""
echo " 1. Restart your terminal or run:"
echo -e " ${CYAN}source $(get_shell_config $(detect_shell))${NC}"
echo ""
echo " 2. Create a new project:"
echo -e " ${CYAN}npx create-strata my-app${NC}"
echo -e " ${CYAN}cd my-app${NC}"
echo -e " ${CYAN}npm install${NC}"
echo -e " ${CYAN}strata dev${NC}"
echo ""
echo " Or try the example app:"
echo -e " ${CYAN}cd $REPO_DIR/examples/basic-app${NC}"
echo -e " ${CYAN}strata dev${NC}"
echo ""
echo -e " ${BOLD}Available Commands:${NC}"
echo ""
echo " strata dev Start dev server with hot reload"
echo " strata build Build for production"
echo " strata preview Preview production build"
echo " strata generate Generate component/page/store"
echo ""
echo -e " ${BOLD}Aliases:${NC}"
echo ""
echo " st → strata"
echo " stdev → strata dev"
echo " stbuild → strata build"
echo " stgen → strata generate"
echo " stnew → npx create-strata"
echo ""
echo -e " ${BOLD}Documentation:${NC} https://stratajs.dev/docs"
echo -e " ${BOLD}GitHub:${NC} https://github.com/strata/strata"
echo ""
}
# Main installation
main() {
print_banner
check_requirements
create_directories
build_compiler
install_npm_deps
install_global
configure_shell
install_completions
create_config
print_final
}
# Run main
main

64
scripts/setup.sh Executable file
View File

@@ -0,0 +1,64 @@
#!/bin/bash
set -e
echo ""
echo " ╔═══════════════════════════════════════╗"
echo " ║ Strata Framework Setup ║"
echo " ╚═══════════════════════════════════════╝"
echo ""
# Check for Go
if ! command -v go &> /dev/null; then
echo " ❌ Go is not installed."
echo " Install from: https://go.dev/dl/"
exit 1
fi
echo " ✓ Go $(go version | cut -d' ' -f3)"
# Check for Node
if ! command -v node &> /dev/null; then
echo " ❌ Node.js is not installed."
echo " Install from: https://nodejs.org/"
exit 1
fi
echo " ✓ Node $(node -v)"
# Check for npm
if ! command -v npm &> /dev/null; then
echo " ❌ npm is not installed."
exit 1
fi
echo " ✓ npm $(npm -v)"
echo ""
echo " Installing Go dependencies..."
cd compiler
go mod download
cd ..
echo ""
echo " Building compiler..."
mkdir -p bin
cd compiler
go build -o ../bin/strata ./cmd/strata
cd ..
echo ""
echo " ✓ Compiler built: ./bin/strata"
echo ""
echo " Installing npm dependencies..."
npm install --silent
echo ""
echo " ════════════════════════════════════════"
echo " ✓ Setup complete!"
echo ""
echo " Quick start:"
echo " cd examples/basic-app"
echo " ../../bin/strata dev"
echo ""
echo " Or use make:"
echo " make dev"
echo ""

177
scripts/uninstall.sh Executable file
View File

@@ -0,0 +1,177 @@
#!/bin/bash
# Strata Framework - Uninstall Script
# Usage: ./scripts/uninstall.sh [options]
# --keep-config Keep configuration files
# --force Skip confirmation prompt
# --help Show this help message
set -e
# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
CYAN='\033[0;36m'
NC='\033[0m'
BOLD='\033[1m'
# Directories
STRATA_HOME="${STRATA_HOME:-$HOME/.strata}"
LOCAL_BIN="/usr/local/bin"
# Options
KEEP_CONFIG=false
FORCE=false
# Parse arguments
while [[ $# -gt 0 ]]; do
case $1 in
--keep-config)
KEEP_CONFIG=true
shift
;;
--force|-f)
FORCE=true
shift
;;
--help|-h)
echo "Strata Framework - Uninstall Script"
echo ""
echo "Usage: ./scripts/uninstall.sh [options]"
echo ""
echo "Options:"
echo " --keep-config Keep configuration files in ~/.strata/config"
echo " --force, -f Skip confirmation prompt"
echo " --help Show this help message"
exit 0
;;
*)
echo -e "${RED}Unknown option: $1${NC}"
exit 1
;;
esac
done
print_step() {
echo -e "${BLUE}${NC} $1"
}
print_success() {
echo -e "${GREEN}${NC} $1"
}
print_warning() {
echo -e "${YELLOW}${NC} $1"
}
# Confirmation prompt
if [ "$FORCE" = false ]; then
echo ""
echo -e "${YELLOW}╔═══════════════════════════════════════════════════════╗${NC}"
echo -e "${YELLOW}║ Strata Framework - Uninstall ║${NC}"
echo -e "${YELLOW}╚═══════════════════════════════════════════════════════╝${NC}"
echo ""
echo "This will remove:"
echo " - Strata binary from $STRATA_HOME/bin"
echo " - Shell completions from $STRATA_HOME/completions"
if [ "$KEEP_CONFIG" = false ]; then
echo " - Configuration from $STRATA_HOME/config"
fi
echo " - Global symlink from $LOCAL_BIN (if exists)"
echo " - Shell configuration (Strata block) from your shell rc file"
echo ""
read -p "Are you sure you want to uninstall Strata? [y/N] " -n 1 -r
echo ""
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
echo "Uninstall cancelled."
exit 0
fi
echo ""
fi
# Remove global symlink
print_step "Removing global symlink..."
if [ -L "$LOCAL_BIN/strata" ]; then
if [ -w "$LOCAL_BIN" ]; then
rm -f "$LOCAL_BIN/strata"
else
sudo rm -f "$LOCAL_BIN/strata"
fi
print_success "Removed $LOCAL_BIN/strata"
else
echo " No global symlink found"
fi
# Remove Strata home directory
print_step "Removing Strata installation..."
if [ -d "$STRATA_HOME" ]; then
if [ "$KEEP_CONFIG" = true ]; then
# Keep config, remove everything else
rm -rf "$STRATA_HOME/bin"
rm -rf "$STRATA_HOME/completions"
print_success "Removed $STRATA_HOME (kept config)"
else
rm -rf "$STRATA_HOME"
print_success "Removed $STRATA_HOME"
fi
else
echo " No installation found at $STRATA_HOME"
fi
# Clean up shell configuration
print_step "Cleaning shell configuration..."
clean_shell_config() {
local config_file="$1"
if [ ! -f "$config_file" ]; then
return
fi
# Check if Strata config exists
if ! grep -q "# Strata Framework" "$config_file"; then
return
fi
# Create backup
cp "$config_file" "${config_file}.backup.$(date +%Y%m%d%H%M%S)"
# Remove Strata block (from "# Strata Framework" to the next blank line after completions)
# This handles multi-line blocks
if [[ "$OSTYPE" == "darwin"* ]]; then
# macOS sed
sed -i '' '/# Strata Framework/,/^$/d' "$config_file"
else
# GNU sed
sed -i '/# Strata Framework/,/^$/d' "$config_file"
fi
print_success "Cleaned $config_file"
}
# Clean common shell config files
for config_file in "$HOME/.bashrc" "$HOME/.bash_profile" "$HOME/.zshrc" "$HOME/.profile"; do
clean_shell_config "$config_file"
done
# Fish config
if [ -f "$HOME/.config/fish/config.fish" ]; then
clean_shell_config "$HOME/.config/fish/config.fish"
fi
echo ""
echo -e "${GREEN}════════════════════════════════════════════════════════${NC}"
echo -e "${GREEN} Strata has been uninstalled ${NC}"
echo -e "${GREEN}════════════════════════════════════════════════════════${NC}"
echo ""
echo " Restart your terminal for changes to take effect."
echo ""
if [ "$KEEP_CONFIG" = true ]; then
echo " Your configuration was preserved at $STRATA_HOME/config"
echo ""
fi
echo " To reinstall Strata:"
echo -e " ${CYAN}./scripts/install.sh${NC}"
echo ""

189
scripts/upgrade.sh Executable file
View File

@@ -0,0 +1,189 @@
#!/bin/bash
# Strata Framework - Upgrade Script
# Usage: ./scripts/upgrade.sh [options]
# --check Only check for updates, don't install
# --force Force reinstall even if up-to-date
# --help Show this help message
set -e
# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
CYAN='\033[0;36m'
NC='\033[0m'
BOLD='\033[1m'
# Directories
STRATA_HOME="${STRATA_HOME:-$HOME/.strata}"
STRATA_CONFIG="$STRATA_HOME/config"
# Script directory
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_DIR="$(dirname "$SCRIPT_DIR")"
# Options
CHECK_ONLY=false
FORCE=false
# Parse arguments
while [[ $# -gt 0 ]]; do
case $1 in
--check)
CHECK_ONLY=true
shift
;;
--force|-f)
FORCE=true
shift
;;
--help|-h)
echo "Strata Framework - Upgrade Script"
echo ""
echo "Usage: ./scripts/upgrade.sh [options]"
echo ""
echo "Options:"
echo " --check Only check for updates"
echo " --force Force reinstall even if up-to-date"
echo " --help Show this help message"
exit 0
;;
*)
echo -e "${RED}Unknown option: $1${NC}"
exit 1
;;
esac
done
print_step() {
echo -e "${BLUE}${NC} $1"
}
print_success() {
echo -e "${GREEN}${NC} $1"
}
print_warning() {
echo -e "${YELLOW}${NC} $1"
}
print_error() {
echo -e "${RED}${NC} $1"
}
# Get current installed version
get_installed_version() {
if [ -f "$STRATA_CONFIG/strata.json" ]; then
grep '"version"' "$STRATA_CONFIG/strata.json" | cut -d'"' -f4
elif [ -x "$STRATA_HOME/bin/strata" ]; then
"$STRATA_HOME/bin/strata" version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1 || echo "unknown"
else
echo "not installed"
fi
}
# Get repo version from package.json
get_repo_version() {
if [ -f "$REPO_DIR/package.json" ]; then
grep '"version"' "$REPO_DIR/package.json" | head -1 | cut -d'"' -f4
else
echo "unknown"
fi
}
# Compare versions (returns 0 if $1 < $2)
version_lt() {
[ "$(printf '%s\n' "$1" "$2" | sort -V | head -n1)" = "$1" ] && [ "$1" != "$2" ]
}
echo ""
echo -e "${CYAN}╔═══════════════════════════════════════════════════════╗${NC}"
echo -e "${CYAN}║ Strata Framework - Upgrade Check ║${NC}"
echo -e "${CYAN}╚═══════════════════════════════════════════════════════╝${NC}"
echo ""
# Check versions
print_step "Checking versions..."
INSTALLED_VERSION=$(get_installed_version)
REPO_VERSION=$(get_repo_version)
echo " Installed: $INSTALLED_VERSION"
echo " Available: $REPO_VERSION"
echo ""
# Determine if upgrade is needed
if [ "$INSTALLED_VERSION" = "not installed" ]; then
print_warning "Strata is not installed"
echo ""
echo " Run the installer:"
echo -e " ${CYAN}./scripts/install.sh${NC}"
exit 0
fi
NEEDS_UPGRADE=false
if [ "$INSTALLED_VERSION" = "unknown" ] || [ "$REPO_VERSION" = "unknown" ]; then
print_warning "Unable to determine version comparison"
NEEDS_UPGRADE=true
elif version_lt "$INSTALLED_VERSION" "$REPO_VERSION"; then
NEEDS_UPGRADE=true
fi
if [ "$NEEDS_UPGRADE" = false ] && [ "$FORCE" = false ]; then
print_success "Strata is up-to-date!"
exit 0
fi
if [ "$NEEDS_UPGRADE" = true ]; then
echo -e "${YELLOW}Update available: $INSTALLED_VERSION$REPO_VERSION${NC}"
else
echo "Forcing reinstall of version $REPO_VERSION"
fi
echo ""
if [ "$CHECK_ONLY" = true ]; then
echo "Run without --check to install the update."
exit 0
fi
# Perform upgrade
print_step "Pulling latest changes..."
cd "$REPO_DIR"
if [ -d ".git" ]; then
git pull --rebase 2>/dev/null || print_warning "Could not pull (not a git repo or no remote)"
fi
print_step "Rebuilding compiler..."
cd "$REPO_DIR/compiler"
go mod download
go build -ldflags="-s -w" -o "$REPO_DIR/bin/strata" ./cmd/strata
cp "$REPO_DIR/bin/strata" "$STRATA_HOME/bin/strata"
chmod +x "$STRATA_HOME/bin/strata"
print_step "Updating npm dependencies..."
cd "$REPO_DIR"
npm install --silent
print_step "Updating configuration..."
# Update version in config
if [ -f "$STRATA_CONFIG/strata.json" ]; then
if [[ "$OSTYPE" == "darwin"* ]]; then
sed -i '' "s/\"version\": \".*\"/\"version\": \"$REPO_VERSION\"/" "$STRATA_CONFIG/strata.json"
else
sed -i "s/\"version\": \".*\"/\"version\": \"$REPO_VERSION\"/" "$STRATA_CONFIG/strata.json"
fi
fi
echo ""
echo -e "${GREEN}════════════════════════════════════════════════════════${NC}"
echo -e "${GREEN} Upgrade Complete! ${NC}"
echo -e "${GREEN}════════════════════════════════════════════════════════${NC}"
echo ""
echo " Strata has been upgraded to version $REPO_VERSION"
echo ""
echo " Run 'strata version' to verify."
echo ""

18
templates/.nvim.lua Normal file
View File

@@ -0,0 +1,18 @@
-- Strata file type detection for NeoVim
vim.filetype.add({
extension = {
strata = "html", -- Templates: HTML syntax
sts = "typescript", -- Pure logic: TypeScript
["compiler.sts"] = "typescript", -- Compiler: TypeScript
["service.sts"] = "typescript", -- Service: TypeScript
["api.sts"] = "typescript", -- API contracts: TypeScript
},
pattern = {
[".*%.compiler%.sts"] = "typescript",
[".*%.service%.sts"] = "typescript",
[".*%.api%.sts"] = "typescript",
},
})
-- Optional: Add custom syntax highlighting or treesitter config
-- vim.treesitter.language.register("html", "strata")

80
templates/strataconfig.ts Normal file
View File

@@ -0,0 +1,80 @@
import { defineConfig } from 'strata';
export default defineConfig({
// Application settings
app: {
title: 'My Strata App',
description: 'Built with Strata Framework',
baseUrl: '/',
},
// Build configuration
build: {
outDir: 'dist',
assetsDir: 'assets',
minify: true,
sourcemap: process.env.NODE_ENV !== 'production',
target: 'es2020',
},
// Islands architecture
islands: {
hydration: 'visible', // 'load' | 'visible' | 'idle' | 'interaction'
threshold: 0.1,
},
// State management
state: {
devtools: process.env.NODE_ENV !== 'production',
persist: true,
encrypt: true, // Enable store encryption
},
// API configuration
api: {
baseUrl: process.env.API_URL || 'http://localhost:8080',
cache: {
enabled: true,
defaultTTL: '5m',
maxSize: 100,
},
rateLimit: {
maxRequests: 15000,
perDay: true,
strategy: 'queue',
},
},
// SCSS configuration
scss: {
includePaths: ['src/assets/styles'],
additionalData: `
@import "variables";
@import "mixins";
`,
},
// Auto-detected aliases (can override here)
aliases: {
'@': './src',
'@components': './src/components',
'@pages': './src/pages',
'@stores': './src/stores',
'@utils': './src/utils',
'@assets': './src/assets',
},
// Microfrontends (optional)
microfrontends: {
enabled: false,
shared: ['strata'],
remotes: {},
},
// Injected scripts (GTM, analytics, etc.)
// Scripts are loaded from src/injectedscripts/*.js
scripts: {
// Override positions/priorities if needed
// 'gtm': { position: 'head', priority: 1 },
},
});