blumenjs 0.1.7 → 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 +270 -15
- package/dist/cli/commands/build.js +25 -6
- package/dist/cli/commands/create.js +1 -1
- package/dist/cli/commands/dev.js +1 -1
- package/dist/cli/commands/fonts.js +232 -0
- package/dist/cli/commands/start.js +1 -1
- package/dist/templates/app/client/entry.tsx +3 -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 +81 -18
- package/dist/templates/app/shared/router.ts +2 -1
- package/dist/templates/go-server/cache.go +147 -0
- package/dist/templates/go-server/image.go +200 -0
- package/dist/templates/go-server/main.go +394 -39
- 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 +364 -8
- package/dist/templates/scripts/generate-routes.ts +355 -7
- package/package.json +12 -6
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
package main
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"bytes"
|
|
5
|
+
"crypto/sha256"
|
|
6
|
+
"encoding/hex"
|
|
7
|
+
"fmt"
|
|
8
|
+
"image"
|
|
9
|
+
"image/jpeg"
|
|
10
|
+
"image/png"
|
|
11
|
+
"log"
|
|
12
|
+
"net/http"
|
|
13
|
+
"os"
|
|
14
|
+
"path/filepath"
|
|
15
|
+
"strconv"
|
|
16
|
+
"strings"
|
|
17
|
+
|
|
18
|
+
"golang.org/x/image/draw"
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
const (
|
|
22
|
+
imageCacheDir = ".blumen/image-cache"
|
|
23
|
+
maxImageWidth = 4096
|
|
24
|
+
defaultQuality = 80
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
func init() {
|
|
28
|
+
// Ensure cache directory exists
|
|
29
|
+
os.MkdirAll(imageCacheDir, 0755)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// ImageHandler serves optimized images with on-the-fly resizing and caching.
|
|
33
|
+
// URL: /_blumen/image?src=/static/hero.jpg&w=800&q=80&blur=1
|
|
34
|
+
func ImageHandler() http.HandlerFunc {
|
|
35
|
+
return func(w http.ResponseWriter, r *http.Request) {
|
|
36
|
+
if r.Method != http.MethodGet {
|
|
37
|
+
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
|
38
|
+
return
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
q := r.URL.Query()
|
|
42
|
+
src := q.Get("src")
|
|
43
|
+
widthStr := q.Get("w")
|
|
44
|
+
qualityStr := q.Get("q")
|
|
45
|
+
isBlur := q.Get("blur") == "1"
|
|
46
|
+
|
|
47
|
+
if src == "" {
|
|
48
|
+
http.Error(w, "Missing 'src' parameter", http.StatusBadRequest)
|
|
49
|
+
return
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Parse width
|
|
53
|
+
targetWidth := 0
|
|
54
|
+
if widthStr != "" {
|
|
55
|
+
var err error
|
|
56
|
+
targetWidth, err = strconv.Atoi(widthStr)
|
|
57
|
+
if err != nil || targetWidth <= 0 || targetWidth > maxImageWidth {
|
|
58
|
+
http.Error(w, "Invalid 'w' parameter", http.StatusBadRequest)
|
|
59
|
+
return
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Parse quality
|
|
64
|
+
quality := defaultQuality
|
|
65
|
+
if qualityStr != "" {
|
|
66
|
+
var err error
|
|
67
|
+
quality, err = strconv.Atoi(qualityStr)
|
|
68
|
+
if err != nil || quality < 1 || quality > 100 {
|
|
69
|
+
quality = defaultQuality
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Security: resolve the source path and ensure it's within the project
|
|
74
|
+
cleanSrc := strings.TrimPrefix(src, "/")
|
|
75
|
+
absPath, err := filepath.Abs(cleanSrc)
|
|
76
|
+
if err != nil {
|
|
77
|
+
http.Error(w, "Invalid path", http.StatusBadRequest)
|
|
78
|
+
return
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Ensure path is within the project directory
|
|
82
|
+
cwd, _ := os.Getwd()
|
|
83
|
+
if !strings.HasPrefix(absPath, cwd) {
|
|
84
|
+
http.Error(w, "Access denied", http.StatusForbidden)
|
|
85
|
+
return
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Check if file exists
|
|
89
|
+
if _, err := os.Stat(absPath); os.IsNotExist(err) {
|
|
90
|
+
http.Error(w, "Image not found", http.StatusNotFound)
|
|
91
|
+
return
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Generate cache key
|
|
95
|
+
cacheKey := generateCacheKey(src, targetWidth, quality, isBlur)
|
|
96
|
+
cachePath := filepath.Join(imageCacheDir, cacheKey+".jpg")
|
|
97
|
+
|
|
98
|
+
// Check disk cache
|
|
99
|
+
if cachedData, err := os.ReadFile(cachePath); err == nil {
|
|
100
|
+
serveCachedImage(w, cachedData)
|
|
101
|
+
return
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Process the image
|
|
105
|
+
processedData, err := processImage(absPath, targetWidth, quality, isBlur)
|
|
106
|
+
if err != nil {
|
|
107
|
+
log.Printf("Image processing error: %v", err)
|
|
108
|
+
http.ServeFile(w, r, absPath)
|
|
109
|
+
return
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Write to disk cache
|
|
113
|
+
os.WriteFile(cachePath, processedData, 0644)
|
|
114
|
+
|
|
115
|
+
// Serve the processed image
|
|
116
|
+
serveCachedImage(w, processedData)
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
func generateCacheKey(src string, width, quality int, blur bool) string {
|
|
121
|
+
data := fmt.Sprintf("%s-%d-%d-%v", src, width, quality, blur)
|
|
122
|
+
hash := sha256.Sum256([]byte(data))
|
|
123
|
+
return hex.EncodeToString(hash[:16])
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
func serveCachedImage(w http.ResponseWriter, data []byte) {
|
|
127
|
+
w.Header().Set("Content-Type", "image/jpeg")
|
|
128
|
+
w.Header().Set("Cache-Control", "public, max-age=31536000, immutable")
|
|
129
|
+
w.Header().Set("X-Blumen-Image", "optimized")
|
|
130
|
+
w.Header().Set("Content-Length", strconv.Itoa(len(data)))
|
|
131
|
+
w.WriteHeader(http.StatusOK)
|
|
132
|
+
w.Write(data)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
func processImage(filePath string, targetWidth, quality int, blur bool) ([]byte, error) {
|
|
136
|
+
file, err := os.Open(filePath)
|
|
137
|
+
if err != nil {
|
|
138
|
+
return nil, fmt.Errorf("open image: %w", err)
|
|
139
|
+
}
|
|
140
|
+
defer file.Close()
|
|
141
|
+
|
|
142
|
+
img, format, err := image.Decode(file)
|
|
143
|
+
if err != nil {
|
|
144
|
+
return nil, fmt.Errorf("decode image (%s): %w", format, err)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
bounds := img.Bounds()
|
|
148
|
+
origWidth := bounds.Dx()
|
|
149
|
+
origHeight := bounds.Dy()
|
|
150
|
+
|
|
151
|
+
// Calculate target dimensions maintaining aspect ratio
|
|
152
|
+
newWidth := origWidth
|
|
153
|
+
newHeight := origHeight
|
|
154
|
+
|
|
155
|
+
if targetWidth > 0 && targetWidth < origWidth {
|
|
156
|
+
ratio := float64(targetWidth) / float64(origWidth)
|
|
157
|
+
newWidth = targetWidth
|
|
158
|
+
newHeight = int(float64(origHeight) * ratio)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Resize using high-quality bilinear interpolation
|
|
162
|
+
resized := image.NewRGBA(image.Rect(0, 0, newWidth, newHeight))
|
|
163
|
+
draw.BiLinear.Scale(resized, resized.Bounds(), img, bounds, draw.Over, nil)
|
|
164
|
+
|
|
165
|
+
// For blur placeholders, downscale aggressively
|
|
166
|
+
var finalImg image.Image = resized
|
|
167
|
+
if blur {
|
|
168
|
+
blurWidth := 20
|
|
169
|
+
if newWidth < blurWidth {
|
|
170
|
+
blurWidth = newWidth
|
|
171
|
+
}
|
|
172
|
+
blurHeight := int(float64(newHeight) * (float64(blurWidth) / float64(newWidth)))
|
|
173
|
+
if blurHeight < 1 {
|
|
174
|
+
blurHeight = 1
|
|
175
|
+
}
|
|
176
|
+
blurred := image.NewRGBA(image.Rect(0, 0, blurWidth, blurHeight))
|
|
177
|
+
draw.BiLinear.Scale(blurred, blurred.Bounds(), resized, resized.Bounds(), draw.Over, nil)
|
|
178
|
+
finalImg = blurred
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Encode to JPEG
|
|
182
|
+
var buf bytes.Buffer
|
|
183
|
+
|
|
184
|
+
jpegQuality := quality
|
|
185
|
+
if blur {
|
|
186
|
+
jpegQuality = 30
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
err = jpeg.Encode(&buf, finalImg, &jpeg.Options{Quality: jpegQuality})
|
|
190
|
+
if err != nil {
|
|
191
|
+
return nil, fmt.Errorf("encode jpeg: %w", err)
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return buf.Bytes(), nil
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Ensure PNG decoder is registered
|
|
198
|
+
func init() {
|
|
199
|
+
_ = png.Decode
|
|
200
|
+
}
|