blumenjs 0.2.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/blumen.js +15 -5
- package/dist/cli/commands/build.js +13 -3
- package/dist/cli/commands/dev.js +1 -1
- package/dist/cli/commands/start.js +1 -1
- package/dist/templates/app/pages/BlumenStarter.tsx +1 -1
- package/dist/templates/app/shared/DefaultDocument.tsx +49 -4
- package/dist/templates/app/shared/Link.tsx +60 -6
- package/dist/templates/app/shared/RouterContext.tsx +80 -20
- package/dist/templates/app/shared/router.ts +2 -1
- package/dist/templates/go-server/main.go +188 -1
- package/dist/templates/go-server/middleware.go +546 -0
- package/dist/templates/go-server/websocket.go +430 -0
- package/dist/templates/node-ssr/server.ts +246 -2
- package/dist/templates/scripts/generate-routes.ts +318 -10
- package/package.json +6 -4
|
@@ -13,12 +13,20 @@ import (
|
|
|
13
13
|
const (
|
|
14
14
|
nodeSSRURL = "http://localhost:4000/render"
|
|
15
15
|
nodeDataURL = "http://localhost:4000/data"
|
|
16
|
+
nodeAPIURL = "http://localhost:4000/api"
|
|
16
17
|
)
|
|
17
18
|
|
|
18
19
|
// Global page cache — stores rendered HTML in Go memory for near-instant responses.
|
|
19
20
|
// Max 500 entries with LRU eviction. A single Go server can cache the entire site.
|
|
20
21
|
var pageCache = NewPageCache(500)
|
|
21
22
|
|
|
23
|
+
// Global WebSocket hub — manages all real-time connections.
|
|
24
|
+
// Each connection runs in its own goroutine for maximum concurrency.
|
|
25
|
+
var wsHub = NewWSHub()
|
|
26
|
+
|
|
27
|
+
// Global SSG server — serves pre-rendered HTML directly from disk.
|
|
28
|
+
var ssgServer = NewSSGServer("dist/static-pages")
|
|
29
|
+
|
|
22
30
|
// SSRResponse from Node service
|
|
23
31
|
type SSRResponse struct {
|
|
24
32
|
HTML string `json:"html"`
|
|
@@ -55,6 +63,37 @@ var httpClient = &http.Client{
|
|
|
55
63
|
func main() {
|
|
56
64
|
mux := http.NewServeMux()
|
|
57
65
|
|
|
66
|
+
// ── Middleware chain ─────────────────────────────────────
|
|
67
|
+
chain := NewMiddlewareChain()
|
|
68
|
+
|
|
69
|
+
// Redirects & rewrites (processed first, before logging)
|
|
70
|
+
SetupRedirects(chain, "blumen.routes.json")
|
|
71
|
+
|
|
72
|
+
// Global middleware (runs on every request)
|
|
73
|
+
chain.Use(LoggerMiddleware())
|
|
74
|
+
|
|
75
|
+
// CORS middleware for API routes
|
|
76
|
+
chain.UseOn("/api/*", CORSMiddleware(DefaultCORSConfig()))
|
|
77
|
+
|
|
78
|
+
// Security headers (CSP, HSTS, X-Frame-Options, etc.)
|
|
79
|
+
chain.Use(SecurityHeadersMiddleware(DefaultSecurityConfig()))
|
|
80
|
+
|
|
81
|
+
// SSR DoS protection: limit page rendering requests per IP
|
|
82
|
+
// Prevents abuse of the CPU-intensive SSR endpoint
|
|
83
|
+
chain.UseOn("/*", RateLimitMiddleware(200, time.Minute))
|
|
84
|
+
|
|
85
|
+
// Authentication middleware for protected routes
|
|
86
|
+
// Uncomment and configure to enable:
|
|
87
|
+
// chain.UseOn("/dashboard/*", AuthMiddleware(AuthConfig{
|
|
88
|
+
// CookieName: "session",
|
|
89
|
+
// HeaderName: "Authorization",
|
|
90
|
+
// RedirectURL: "/login",
|
|
91
|
+
// ValidateFunc: func(token string) bool {
|
|
92
|
+
// // Add your token validation logic here
|
|
93
|
+
// return token != ""
|
|
94
|
+
// },
|
|
95
|
+
// }))
|
|
96
|
+
|
|
58
97
|
// Static files with immutable cache headers
|
|
59
98
|
staticHandler := http.StripPrefix("/static/", http.FileServer(http.Dir("static")))
|
|
60
99
|
mux.HandleFunc("/static/", func(w http.ResponseWriter, r *http.Request) {
|
|
@@ -78,6 +117,49 @@ func main() {
|
|
|
78
117
|
}, nil
|
|
79
118
|
}))
|
|
80
119
|
|
|
120
|
+
// API routes: forward all /api/* requests to Node for TypeScript handling
|
|
121
|
+
mux.HandleFunc("/api/", APIProxyHandler())
|
|
122
|
+
|
|
123
|
+
// ── WebSocket endpoint ────────────────────────────────────
|
|
124
|
+
// Configure WebSocket event handlers
|
|
125
|
+
wsHub.OnConnect = func(client *WSClient, r *http.Request) bool {
|
|
126
|
+
// Accept all connections by default.
|
|
127
|
+
// Add authentication logic here (e.g. check JWT in query params).
|
|
128
|
+
return true
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
wsHub.OnMessage = func(client *WSClient, msg WSMessage) *WSMessage {
|
|
132
|
+
// Handle custom message types here.
|
|
133
|
+
// Return a *WSMessage to reply directly to the sender, or nil.
|
|
134
|
+
switch msg.Type {
|
|
135
|
+
case "echo":
|
|
136
|
+
// Example: echo the payload back
|
|
137
|
+
return &WSMessage{Type: "echo", Payload: msg.Payload}
|
|
138
|
+
default:
|
|
139
|
+
return nil
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Start the WebSocket hub event loop
|
|
144
|
+
go wsHub.Run()
|
|
145
|
+
|
|
146
|
+
// WebSocket upgrade endpoint
|
|
147
|
+
mux.HandleFunc("/_blumen/ws", WSHandler(wsHub))
|
|
148
|
+
|
|
149
|
+
// WebSocket status endpoint (for monitoring)
|
|
150
|
+
mux.HandleFunc("/_blumen/ws/status", func(w http.ResponseWriter, r *http.Request) {
|
|
151
|
+
w.Header().Set("Content-Type", "application/json")
|
|
152
|
+
status := map[string]interface{}{
|
|
153
|
+
"connections": wsHub.ClientCount(),
|
|
154
|
+
}
|
|
155
|
+
json.NewEncoder(w).Encode(status)
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
// ── Server Actions endpoint ──────────────────────────────
|
|
159
|
+
// Handles POST /_blumen/action with CSRF validation.
|
|
160
|
+
// Forwards action execution to Node SSR server.
|
|
161
|
+
mux.HandleFunc("/_blumen/action", ActionHandler())
|
|
162
|
+
|
|
81
163
|
// Catch-all: forward every other request to the Node SSR server.
|
|
82
164
|
// If the page exports getServerProps, Node will fetch the data first.
|
|
83
165
|
// If a Go DataLoader is registered above, it takes priority.
|
|
@@ -99,7 +181,8 @@ func main() {
|
|
|
99
181
|
|
|
100
182
|
log.Printf("Go server starting on http://localhost:%d", startPort)
|
|
101
183
|
log.Printf("Page cache initialized (max %d entries)", 500)
|
|
102
|
-
|
|
184
|
+
log.Printf("Middleware chain: %d middleware registered", len(chain.entries))
|
|
185
|
+
if err := http.Serve(listener, chain.Then(mux)); err != nil {
|
|
103
186
|
log.Fatalf("Server error: %v", err)
|
|
104
187
|
}
|
|
105
188
|
}
|
|
@@ -113,6 +196,13 @@ func PageHandler(loader DataLoader) http.HandlerFunc {
|
|
|
113
196
|
return
|
|
114
197
|
}
|
|
115
198
|
|
|
199
|
+
// SSG: serve pre-rendered static pages directly from disk (< 0.5ms)
|
|
200
|
+
// Only for full page requests, not SPA data requests
|
|
201
|
+
if r.Header.Get("X-Blumen-Data") != "1" && ssgServer.HasPage(r.URL.Path) {
|
|
202
|
+
ssgServer.ServePage(w, r)
|
|
203
|
+
return
|
|
204
|
+
}
|
|
205
|
+
|
|
116
206
|
// Generate cache key from the full URL (path + query string)
|
|
117
207
|
cacheKey := r.URL.RequestURI()
|
|
118
208
|
|
|
@@ -341,3 +431,100 @@ func cacheStatusHandler(w http.ResponseWriter, r *http.Request) {
|
|
|
341
431
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
|
342
432
|
}
|
|
343
433
|
}
|
|
434
|
+
|
|
435
|
+
// APINodeRequest is the envelope sent to Node for API route handling.
|
|
436
|
+
type APINodeRequest struct {
|
|
437
|
+
Method string `json:"method"`
|
|
438
|
+
Path string `json:"path"`
|
|
439
|
+
Query map[string][]string `json:"query"`
|
|
440
|
+
Headers map[string]string `json:"headers"`
|
|
441
|
+
Body interface{} `json:"body"`
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// APINodeResponse is the envelope received from Node after API route handling.
|
|
445
|
+
type APINodeResponse struct {
|
|
446
|
+
Status int `json:"status"`
|
|
447
|
+
Headers map[string]string `json:"headers"`
|
|
448
|
+
Body interface{} `json:"body"`
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// APIProxyHandler forwards /api/* requests to the Node SSR server.
|
|
452
|
+
// Accepts all HTTP methods (GET, POST, PUT, DELETE, PATCH).
|
|
453
|
+
// Reads the request body, packages it into a JSON envelope, and sends it
|
|
454
|
+
// to Node's /api endpoint. Node matches the route, runs the handler, and
|
|
455
|
+
// returns { status, headers, body } which Go writes back to the client.
|
|
456
|
+
func APIProxyHandler() http.HandlerFunc {
|
|
457
|
+
return func(w http.ResponseWriter, r *http.Request) {
|
|
458
|
+
// Read request body (for POST, PUT, PATCH)
|
|
459
|
+
var requestBody interface{}
|
|
460
|
+
if r.Method == http.MethodPost || r.Method == http.MethodPut || r.Method == http.MethodPatch {
|
|
461
|
+
var bodyBuf bytes.Buffer
|
|
462
|
+
bodyBuf.ReadFrom(r.Body)
|
|
463
|
+
defer r.Body.Close()
|
|
464
|
+
|
|
465
|
+
// Try to parse as JSON; if it fails, send as raw string
|
|
466
|
+
var jsonBody interface{}
|
|
467
|
+
if err := json.Unmarshal(bodyBuf.Bytes(), &jsonBody); err == nil {
|
|
468
|
+
requestBody = jsonBody
|
|
469
|
+
} else if bodyBuf.Len() > 0 {
|
|
470
|
+
requestBody = bodyBuf.String()
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// Forward a subset of headers
|
|
475
|
+
headers := make(map[string]string)
|
|
476
|
+
for _, key := range []string{
|
|
477
|
+
"Authorization", "Cookie", "Content-Type", "Accept",
|
|
478
|
+
"Accept-Language", "User-Agent", "X-Request-ID",
|
|
479
|
+
} {
|
|
480
|
+
if val := r.Header.Get(key); val != "" {
|
|
481
|
+
headers[key] = val
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
apiReq := APINodeRequest{
|
|
486
|
+
Method: r.Method,
|
|
487
|
+
Path: r.URL.Path,
|
|
488
|
+
Query: r.URL.Query(),
|
|
489
|
+
Headers: headers,
|
|
490
|
+
Body: requestBody,
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
reqBody, err := json.Marshal(apiReq)
|
|
494
|
+
if err != nil {
|
|
495
|
+
log.Printf("API proxy: marshal error: %v", err)
|
|
496
|
+
http.Error(w, `{"error":"Internal Server Error"}`, http.StatusInternalServerError)
|
|
497
|
+
return
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
resp, err := httpClient.Post(nodeAPIURL, "application/json", bytes.NewReader(reqBody))
|
|
501
|
+
if err != nil {
|
|
502
|
+
log.Printf("API proxy: node request error: %v", err)
|
|
503
|
+
http.Error(w, `{"error":"API service unavailable"}`, http.StatusBadGateway)
|
|
504
|
+
return
|
|
505
|
+
}
|
|
506
|
+
defer resp.Body.Close()
|
|
507
|
+
|
|
508
|
+
var apiResp APINodeResponse
|
|
509
|
+
if err := json.NewDecoder(resp.Body).Decode(&apiResp); err != nil {
|
|
510
|
+
log.Printf("API proxy: decode error: %v", err)
|
|
511
|
+
http.Error(w, `{"error":"Internal Server Error"}`, http.StatusInternalServerError)
|
|
512
|
+
return
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// Write response headers from Node handler
|
|
516
|
+
for key, val := range apiResp.Headers {
|
|
517
|
+
w.Header().Set(key, val)
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// Security headers
|
|
521
|
+
w.Header().Set("X-Content-Type-Options", "nosniff")
|
|
522
|
+
w.Header().Set("X-Blumen-Route", "api")
|
|
523
|
+
|
|
524
|
+
// Write status and body
|
|
525
|
+
w.WriteHeader(apiResp.Status)
|
|
526
|
+
if apiResp.Body != nil {
|
|
527
|
+
json.NewEncoder(w).Encode(apiResp.Body)
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
}
|