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:
164
compiler/internal/generator/html.go
Normal file
164
compiler/internal/generator/html.go
Normal file
@@ -0,0 +1,164 @@
|
||||
package generator
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
// HTMLGenerator generates index.html only in dist folder
|
||||
type HTMLGenerator struct {
|
||||
projectDir string
|
||||
distDir string
|
||||
injector *ScriptInjector
|
||||
encryptionKey []byte
|
||||
}
|
||||
|
||||
// NewHTMLGenerator creates a new HTML generator
|
||||
func NewHTMLGenerator(projectDir, distDir string) *HTMLGenerator {
|
||||
return &HTMLGenerator{
|
||||
projectDir: projectDir,
|
||||
distDir: distDir,
|
||||
injector: NewScriptInjector(projectDir),
|
||||
}
|
||||
}
|
||||
|
||||
// Generate creates the index.html in dist folder
|
||||
func (hg *HTMLGenerator) Generate(config *BuildConfig) error {
|
||||
// Load injected scripts
|
||||
if err := hg.injector.LoadScripts(); err != nil {
|
||||
return fmt.Errorf("failed to load injected scripts: %w", err)
|
||||
}
|
||||
|
||||
// Generate encryption key at build time
|
||||
hg.encryptionKey = make([]byte, 32)
|
||||
if _, err := rand.Read(hg.encryptionKey); err != nil {
|
||||
return fmt.Errorf("failed to generate encryption key: %w", err)
|
||||
}
|
||||
|
||||
// Create dist directory
|
||||
if err := os.MkdirAll(hg.distDir, 0755); err != nil {
|
||||
return fmt.Errorf("failed to create dist directory: %w", err)
|
||||
}
|
||||
|
||||
// Generate HTML
|
||||
html, err := hg.buildHTML(config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Inject scripts
|
||||
html = hg.injector.InjectIntoHTML(html)
|
||||
|
||||
// Write to dist
|
||||
indexPath := filepath.Join(hg.distDir, "index.html")
|
||||
if err := os.WriteFile(indexPath, []byte(html), 0644); err != nil {
|
||||
return fmt.Errorf("failed to write index.html: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// BuildConfig holds build configuration
|
||||
type BuildConfig struct {
|
||||
Title string
|
||||
Description string
|
||||
BaseURL string
|
||||
APIBaseURL string
|
||||
DevMode bool
|
||||
Assets AssetManifest
|
||||
}
|
||||
|
||||
// AssetManifest tracks generated assets
|
||||
type AssetManifest struct {
|
||||
JS []string
|
||||
CSS []string
|
||||
}
|
||||
|
||||
func (hg *HTMLGenerator) buildHTML(config *BuildConfig) (string, error) {
|
||||
tmpl := `<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="description" content="{{.Description}}">
|
||||
<title>{{.Title}}</title>
|
||||
<base href="{{.BaseURL}}">
|
||||
|
||||
{{range .Assets.CSS}}
|
||||
<link rel="stylesheet" href="{{.}}">
|
||||
{{end}}
|
||||
|
||||
<script>
|
||||
// Strata config (build-time injected)
|
||||
window.__STRATA_CONFIG__ = {
|
||||
apiBaseUrl: "{{.APIBaseURL}}",
|
||||
devMode: {{.DevMode}},
|
||||
encryptionKey: [{{.EncryptionKeyArray}}]
|
||||
};
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
||||
<!-- Strata Runtime -->
|
||||
<script type="module" src="/assets/js/runtime.js"></script>
|
||||
|
||||
{{range .Assets.JS}}
|
||||
<script type="module" src="{{.}}"></script>
|
||||
{{end}}
|
||||
</body>
|
||||
</html>`
|
||||
|
||||
// Convert encryption key to array format for JS
|
||||
keyArray := make([]string, len(hg.encryptionKey))
|
||||
for i, b := range hg.encryptionKey {
|
||||
keyArray[i] = fmt.Sprintf("%d", b)
|
||||
}
|
||||
keyArrayStr := ""
|
||||
for i, k := range keyArray {
|
||||
if i > 0 {
|
||||
keyArrayStr += ","
|
||||
}
|
||||
keyArrayStr += k
|
||||
}
|
||||
|
||||
data := struct {
|
||||
Title string
|
||||
Description string
|
||||
BaseURL string
|
||||
APIBaseURL string
|
||||
DevMode bool
|
||||
Assets AssetManifest
|
||||
EncryptionKeyArray string
|
||||
}{
|
||||
Title: config.Title,
|
||||
Description: config.Description,
|
||||
BaseURL: config.BaseURL,
|
||||
APIBaseURL: config.APIBaseURL,
|
||||
DevMode: config.DevMode,
|
||||
Assets: config.Assets,
|
||||
EncryptionKeyArray: keyArrayStr,
|
||||
}
|
||||
|
||||
t, err := template.New("html").Parse(tmpl)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
if err := t.Execute(&buf, data); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return buf.String(), nil
|
||||
}
|
||||
|
||||
// GetEncryptionKeyHex returns the encryption key as hex for debugging
|
||||
func (hg *HTMLGenerator) GetEncryptionKeyHex() string {
|
||||
return hex.EncodeToString(hg.encryptionKey)
|
||||
}
|
||||
181
compiler/internal/generator/injector.go
Normal file
181
compiler/internal/generator/injector.go
Normal file
@@ -0,0 +1,181 @@
|
||||
package generator
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// InjectedScript represents a script file to be injected
|
||||
type InjectedScript struct {
|
||||
Name string
|
||||
Content string
|
||||
Position string // "head" or "body"
|
||||
Priority int // Lower = earlier injection
|
||||
}
|
||||
|
||||
// ScriptInjector handles the injectedscripts/ directory
|
||||
type ScriptInjector struct {
|
||||
scriptsDir string
|
||||
scripts []InjectedScript
|
||||
}
|
||||
|
||||
// NewScriptInjector creates a new script injector
|
||||
func NewScriptInjector(projectDir string) *ScriptInjector {
|
||||
return &ScriptInjector{
|
||||
scriptsDir: filepath.Join(projectDir, "src", "injectedscripts"),
|
||||
scripts: make([]InjectedScript, 0),
|
||||
}
|
||||
}
|
||||
|
||||
// LoadScripts loads all scripts from injectedscripts/ directory
|
||||
func (si *ScriptInjector) LoadScripts() error {
|
||||
if _, err := os.Stat(si.scriptsDir); os.IsNotExist(err) {
|
||||
return nil // No injectedscripts directory, that's fine
|
||||
}
|
||||
|
||||
entries, err := os.ReadDir(si.scriptsDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() {
|
||||
continue
|
||||
}
|
||||
|
||||
name := entry.Name()
|
||||
if !strings.HasSuffix(name, ".js") {
|
||||
continue
|
||||
}
|
||||
|
||||
content, err := os.ReadFile(filepath.Join(si.scriptsDir, name))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
script := si.parseScript(name, string(content))
|
||||
si.scripts = append(si.scripts, script)
|
||||
}
|
||||
|
||||
// Sort by priority
|
||||
sort.Slice(si.scripts, func(i, j int) bool {
|
||||
return si.scripts[i].Priority < si.scripts[j].Priority
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// parseScript parses a script file and extracts metadata from comments
|
||||
func (si *ScriptInjector) parseScript(name string, content string) InjectedScript {
|
||||
script := InjectedScript{
|
||||
Name: strings.TrimSuffix(name, ".js"),
|
||||
Content: content,
|
||||
Position: "head", // default to head
|
||||
Priority: 100, // default priority
|
||||
}
|
||||
|
||||
// Parse special comments at the top
|
||||
// /* @position: body */
|
||||
// /* @priority: 10 */
|
||||
lines := strings.Split(content, "\n")
|
||||
for _, line := range lines {
|
||||
line = strings.TrimSpace(line)
|
||||
if !strings.HasPrefix(line, "/*") && !strings.HasPrefix(line, "//") {
|
||||
break
|
||||
}
|
||||
|
||||
if strings.Contains(line, "@position:") {
|
||||
if strings.Contains(line, "body") {
|
||||
script.Position = "body"
|
||||
} else {
|
||||
script.Position = "head"
|
||||
}
|
||||
}
|
||||
|
||||
if strings.Contains(line, "@priority:") {
|
||||
// Extract priority number
|
||||
parts := strings.Split(line, "@priority:")
|
||||
if len(parts) > 1 {
|
||||
priority := strings.TrimSpace(parts[1])
|
||||
priority = strings.TrimSuffix(priority, "*/")
|
||||
priority = strings.TrimSpace(priority)
|
||||
var p int
|
||||
if _, err := parseIntFromString(priority, &p); err == nil {
|
||||
script.Priority = p
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return script
|
||||
}
|
||||
|
||||
// GetHeadScripts returns scripts for <head>
|
||||
func (si *ScriptInjector) GetHeadScripts() []InjectedScript {
|
||||
var result []InjectedScript
|
||||
for _, s := range si.scripts {
|
||||
if s.Position == "head" {
|
||||
result = append(result, s)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// GetBodyScripts returns scripts for end of <body>
|
||||
func (si *ScriptInjector) GetBodyScripts() []InjectedScript {
|
||||
var result []InjectedScript
|
||||
for _, s := range si.scripts {
|
||||
if s.Position == "body" {
|
||||
result = append(result, s)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// InjectIntoHTML injects scripts into HTML content
|
||||
func (si *ScriptInjector) InjectIntoHTML(html string) string {
|
||||
// Inject head scripts before </head>
|
||||
headScripts := si.GetHeadScripts()
|
||||
if len(headScripts) > 0 {
|
||||
var headContent strings.Builder
|
||||
for _, s := range headScripts {
|
||||
headContent.WriteString("<!-- Injected: ")
|
||||
headContent.WriteString(s.Name)
|
||||
headContent.WriteString(" -->\n")
|
||||
headContent.WriteString(s.Content)
|
||||
headContent.WriteString("\n")
|
||||
}
|
||||
html = strings.Replace(html, "</head>", headContent.String()+"</head>", 1)
|
||||
}
|
||||
|
||||
// Inject body scripts before </body>
|
||||
bodyScripts := si.GetBodyScripts()
|
||||
if len(bodyScripts) > 0 {
|
||||
var bodyContent strings.Builder
|
||||
for _, s := range bodyScripts {
|
||||
bodyContent.WriteString("<!-- Injected: ")
|
||||
bodyContent.WriteString(s.Name)
|
||||
bodyContent.WriteString(" -->\n")
|
||||
bodyContent.WriteString(s.Content)
|
||||
bodyContent.WriteString("\n")
|
||||
}
|
||||
html = strings.Replace(html, "</body>", bodyContent.String()+"</body>", 1)
|
||||
}
|
||||
|
||||
return html
|
||||
}
|
||||
|
||||
func parseIntFromString(s string, target *int) (int, error) {
|
||||
var n int
|
||||
for _, c := range s {
|
||||
if c >= '0' && c <= '9' {
|
||||
n = n*10 + int(c-'0')
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
*target = n
|
||||
return n, nil
|
||||
}
|
||||
Reference in New Issue
Block a user