- 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-compile dev, strata-compile build, strata-compile
g (generators)
- create-strata-compile scaffolding CLI with Pokemon API example
- Dev server with WebSocket HMR (Hot Module Replacement)
- Documentation: README, ARCHITECTURE, CHANGELOG, CONTRIBUTING,
LICENSE
923 lines
27 KiB
Go
923 lines
27 KiB
Go
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 `<!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"
|
|
}
|