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

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

View File

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

View File

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

View File

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

16
compiler/go.mod Normal file
View File

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

18
compiler/go.sum Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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