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