package server import ( "encoding/json" "fmt" "log" "net/http" "os" "path/filepath" "strings" "sync" "time" "github.com/CarGDev/strata-compile/internal/ast" "github.com/CarGDev/strata-compile/internal/compiler" "github.com/CarGDev/strata-compile/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 ` Strata | Code Faster
` } 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 = '
Page not found: ' + route + '
'; 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 = '
' + error.message + '
'; 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 = ` + "`" + `
╔═══════════════════════════════════════════════════════╗ ║ ║ ║ ███████╗████████╗██████╗ █████╗ ████████╗ █████╗ ║ ║ ██╔════╝╚══██╔══╝██╔══██╗██╔══██╗╚══██╔══╝██╔══██╗ ║ ║ ███████╗ ██║ ██████╔╝███████║ ██║ ███████║ ║ ║ ╚════██║ ██║ ██╔══██╗██╔══██║ ██║ ██╔══██║ ║ ║ ███████║ ██║ ██║ ██║██║ ██║ ██║ ██║ ██║ ║ ║ ╚══════╝ ╚═╝ ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ║ ║ ║ ║ Static Template Rendering Architecture ║ ╚═══════════════════════════════════════════════════════╝

Welcome to Strata

Code Faster. Load Quick. Deploy ASAP.

0

Hearts Captured

Tab: ${strata.tabId}
STRATA_ENGINE_V1.0 // FAST_PATH_ENABLED // HMR_ACTIVE
` + "`" + `; // 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 = ''; 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 = '
' + error.message + '
'; console.error('[Strata] Mount error:', error); } } mount(); ` } func getLocalIP() string { // Simplified - return localhost for now return "localhost" }