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:
65
.gitignore
vendored
Normal file
65
.gitignore
vendored
Normal 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
860
ARCHITECTURE.md
Normal 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
150
CHANGELOG.md
Normal 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
458
CONTRIBUTING.md
Normal 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
113
LICENSE
Normal 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
283
Makefile
Normal 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
728
README.md
Normal 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
1124
cli/create-strata/index.js
Normal file
File diff suppressed because it is too large
Load Diff
23
cli/create-strata/package.json
Normal file
23
cli/create-strata/package.json
Normal 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"
|
||||
}
|
||||
695
compiler/cmd/strata/build.go
Normal file
695
compiler/cmd/strata/build.go
Normal 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
|
||||
}
|
||||
96
compiler/cmd/strata/dev.go
Normal file
96
compiler/cmd/strata/dev.go
Normal 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)
|
||||
}
|
||||
}
|
||||
99
compiler/cmd/strata/main.go
Normal file
99
compiler/cmd/strata/main.go
Normal 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
16
compiler/go.mod
Normal 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
18
compiler/go.sum
Normal 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=
|
||||
304
compiler/internal/ast/nodes.go
Normal file
304
compiler/internal/ast/nodes.go
Normal 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
|
||||
}
|
||||
710
compiler/internal/compiler/static.go
Normal file
710
compiler/internal/compiler/static.go
Normal 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
|
||||
}
|
||||
164
compiler/internal/generator/html.go
Normal file
164
compiler/internal/generator/html.go
Normal 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)
|
||||
}
|
||||
181
compiler/internal/generator/injector.go
Normal file
181
compiler/internal/generator/injector.go
Normal 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
|
||||
}
|
||||
688
compiler/internal/parser/strata.go
Normal file
688
compiler/internal/parser/strata.go
Normal 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'
|
||||
}
|
||||
922
compiler/internal/server/dev.go
Normal file
922
compiler/internal/server/dev.go
Normal 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"
|
||||
}
|
||||
170
compiler/internal/watcher/watcher.go
Normal file
170
compiler/internal/watcher/watcher.go
Normal 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()
|
||||
}
|
||||
16
examples/basic-app/package.json
Normal file
16
examples/basic-app/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
144
examples/basic-app/src/assets/styles/_mixins.scss
Normal file
144
examples/basic-app/src/assets/styles/_mixins.scss
Normal 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;
|
||||
}
|
||||
78
examples/basic-app/src/assets/styles/_variables.scss
Normal file
78
examples/basic-app/src/assets/styles/_variables.scss
Normal 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;
|
||||
124
examples/basic-app/src/assets/styles/global.scss
Normal file
124
examples/basic-app/src/assets/styles/global.scss
Normal 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; }
|
||||
158
examples/basic-app/src/components/UserCard.strata
Normal file
158
examples/basic-app/src/components/UserCard.strata
Normal 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>
|
||||
9
examples/basic-app/src/injectedscripts/analytics.js
Normal file
9
examples/basic-app/src/injectedscripts/analytics.js
Normal 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>
|
||||
9
examples/basic-app/src/injectedscripts/gtm-noscript.js
Normal file
9
examples/basic-app/src/injectedscripts/gtm-noscript.js
Normal 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) -->
|
||||
12
examples/basic-app/src/injectedscripts/gtm.js
Normal file
12
examples/basic-app/src/injectedscripts/gtm.js
Normal 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 -->
|
||||
148
examples/basic-app/src/pages/index.strata
Normal file
148
examples/basic-app/src/pages/index.strata
Normal 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>
|
||||
55
examples/basic-app/src/stores/favorites.sts
Normal file
55
examples/basic-app/src/stores/favorites.sts
Normal 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)
|
||||
});
|
||||
100
examples/basic-app/src/stores/user.sts
Normal file
100
examples/basic-app/src/stores/user.sts
Normal 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;
|
||||
};
|
||||
21
examples/basic-app/strataconfig.ts
Normal file
21
examples/basic-app/strataconfig.ts
Normal 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
3892
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
83
package.json
Normal file
83
package.json
Normal 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
231
runtime/core/strata.sts
Normal 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
233
runtime/fetch/fetch.sts
Normal 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
260
runtime/store/store.sts
Normal 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;
|
||||
}
|
||||
392
runtime/worker/shared-worker.sts
Normal file
392
runtime/worker/shared-worker.sts
Normal 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
294
scripts/doctor.sh
Executable 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
67
scripts/env.sh
Executable 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
260
scripts/get-strata.sh
Executable 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
624
scripts/install.sh
Executable 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
64
scripts/setup.sh
Executable 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
177
scripts/uninstall.sh
Executable 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
189
scripts/upgrade.sh
Executable 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
18
templates/.nvim.lua
Normal 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
80
templates/strataconfig.ts
Normal 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 },
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user