- 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
696 lines
16 KiB
Go
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
|
|
}
|