Files
Carlos Gutierrez a63a758cc5 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-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
2026-01-16 09:13:14 -05:00

696 lines
16 KiB
Go

package main
import (
"fmt"
"log"
"os"
"path/filepath"
"github.com/CarGDev/strata-compile/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-compile 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
}