~repos /gromer
git clone https://pyrossh.dev/repos/gromer.git
gromer is a framework and cli to build multipage web apps in golang using htmx and alpinejs.
fix styles caching
- _example/main.go +14 -10
- api_explorer.go +28 -22
- cmd/gromer/main.go +13 -9
- handlebars/template.go +14 -0
- http.go +56 -48
_example/main.go
CHANGED
|
@@ -29,19 +29,23 @@ func init() {
|
|
|
29
29
|
|
|
30
30
|
func main() {
|
|
31
31
|
port := os.Getenv("PORT")
|
|
32
|
-
|
|
32
|
+
baseRouter := mux.NewRouter()
|
|
33
|
-
|
|
33
|
+
baseRouter.Use(gromer.LogMiddleware)
|
|
34
34
|
|
|
35
|
-
|
|
35
|
+
baseRouter.NotFoundHandler = gromer.StatusHandler(not_found_404.GET)
|
|
36
36
|
|
|
37
|
+
staticRouter := baseRouter.NewRoute().Subrouter()
|
|
38
|
+
staticRouter.Use(gromer.CacheMiddleware)
|
|
37
|
-
gromer.
|
|
39
|
+
gromer.StaticRoute(staticRouter, "/assets/", assets.FS)
|
|
38
|
-
gromer.
|
|
40
|
+
gromer.StylesRoute(staticRouter, "/styles.css")
|
|
41
|
+
|
|
42
|
+
pageRouter := baseRouter.NewRoute().Subrouter()
|
|
39
|
-
gromer.
|
|
43
|
+
gromer.ApiExplorerRoute(pageRouter, "/explorer")
|
|
40
|
-
gromer.Handle(
|
|
44
|
+
gromer.Handle(pageRouter, "GET", "/", pages.GET)
|
|
41
|
-
gromer.Handle(
|
|
45
|
+
gromer.Handle(pageRouter, "GET", "/about", about.GET)
|
|
42
46
|
|
|
43
47
|
|
|
44
|
-
apiRouter :=
|
|
48
|
+
apiRouter := baseRouter.NewRoute().Subrouter()
|
|
45
49
|
apiRouter.Use(gromer.CorsMiddleware)
|
|
46
50
|
gromer.Handle(apiRouter, "GET", "/api/recover", recover.GET)
|
|
47
51
|
gromer.Handle(apiRouter, "GET", "/api/todos", todos.GET)
|
|
@@ -53,7 +57,7 @@ func main() {
|
|
|
53
57
|
|
|
54
58
|
|
|
55
59
|
log.Info().Msg("http server listening on http://localhost:"+port)
|
|
56
|
-
srv := server.New(
|
|
60
|
+
srv := server.New(baseRouter, nil)
|
|
57
61
|
if err := srv.ListenAndServe(":"+port); err != nil {
|
|
58
62
|
log.Fatal().Stack().Err(err).Msg("failed to listen")
|
|
59
63
|
}
|
api_explorer.go
CHANGED
|
@@ -1,32 +1,34 @@
|
|
|
1
1
|
package gromer
|
|
2
2
|
|
|
3
3
|
import (
|
|
4
|
-
"context"
|
|
5
4
|
"encoding/json"
|
|
6
5
|
"html/template"
|
|
6
|
+
"net/http"
|
|
7
7
|
"strings"
|
|
8
8
|
|
|
9
9
|
"github.com/carlmjohnson/versioninfo"
|
|
10
|
+
"github.com/gorilla/mux"
|
|
10
11
|
"github.com/pyros2097/gromer/assets"
|
|
11
12
|
. "github.com/pyros2097/gromer/handlebars"
|
|
12
13
|
)
|
|
13
14
|
|
|
14
|
-
func
|
|
15
|
+
func ApiExplorerRoute(router *mux.Router, path string) {
|
|
16
|
+
router.Path(path).Methods("GET").HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
15
|
-
|
|
17
|
+
cmcss, _ := assets.FS.ReadFile("css/codemirror@5.63.1.css")
|
|
16
|
-
|
|
18
|
+
stylescss, _ := assets.FS.ReadFile("css/styles.css")
|
|
17
|
-
|
|
19
|
+
cmjs, _ := assets.FS.ReadFile("js/codemirror@5.63.1.min.js")
|
|
18
|
-
|
|
20
|
+
cmjsjs, _ := assets.FS.ReadFile("js/codemirror-javascript@5.63.1.js")
|
|
19
|
-
|
|
21
|
+
apiRoutes := []RouteDefinition{}
|
|
20
|
-
|
|
22
|
+
for _, v := range RouteDefs {
|
|
21
|
-
|
|
23
|
+
if strings.Contains(v.Path, "/api/") {
|
|
22
|
-
|
|
24
|
+
apiRoutes = append(apiRoutes, v)
|
|
25
|
+
}
|
|
23
26
|
}
|
|
27
|
+
apiData, err := json.Marshal(apiRoutes)
|
|
28
|
+
if err != nil {
|
|
29
|
+
RespondError(w, 500, err)
|
|
24
|
-
|
|
30
|
+
}
|
|
25
|
-
apiData, err := json.Marshal(apiRoutes)
|
|
26
|
-
if err != nil {
|
|
27
|
-
return HtmlErr(400, err)
|
|
28
|
-
}
|
|
29
|
-
|
|
31
|
+
status, err := Html(`
|
|
30
32
|
<!DOCTYPE html>
|
|
31
33
|
<html lang="en">
|
|
32
34
|
<head>
|
|
@@ -260,10 +262,14 @@ func ApiExplorer(ctx context.Context) (HtmlContent, int, error) {
|
|
|
260
262
|
</body>
|
|
261
263
|
</html>
|
|
262
264
|
`).Props(
|
|
263
|
-
|
|
265
|
+
"commit", versioninfo.Revision[0:7],
|
|
264
|
-
|
|
266
|
+
"routes", apiRoutes,
|
|
265
|
-
|
|
267
|
+
"apiData", template.HTML(string(apiData)),
|
|
266
|
-
|
|
268
|
+
"css", template.HTML(string(cmcss)+"\n\n"+string(stylescss)),
|
|
267
|
-
|
|
269
|
+
"js", template.HTML(string(cmjs)+"\n\n"+string(cmjsjs)),
|
|
268
|
-
|
|
270
|
+
).RenderWriter(w)
|
|
271
|
+
if err != nil {
|
|
272
|
+
RespondError(w, status, err)
|
|
273
|
+
}
|
|
274
|
+
})
|
|
269
275
|
}
|
cmd/gromer/main.go
CHANGED
|
@@ -195,25 +195,29 @@ func init() {
|
|
|
195
195
|
|
|
196
196
|
func main() {
|
|
197
197
|
port := os.Getenv("PORT")
|
|
198
|
-
|
|
198
|
+
baseRouter := mux.NewRouter()
|
|
199
|
-
|
|
199
|
+
baseRouter.Use(gromer.LogMiddleware)
|
|
200
200
|
{{#if notFoundPkg}}
|
|
201
|
-
|
|
201
|
+
baseRouter.NotFoundHandler = gromer.StatusHandler({{ notFoundPkg }}.GET)
|
|
202
202
|
{{/if}}
|
|
203
|
+
staticRouter := baseRouter.NewRoute().Subrouter()
|
|
204
|
+
staticRouter.Use(gromer.CacheMiddleware)
|
|
203
|
-
gromer.
|
|
205
|
+
gromer.StaticRoute(staticRouter, "/assets/", assets.FS)
|
|
204
|
-
gromer.
|
|
206
|
+
gromer.StylesRoute(staticRouter, "/styles.css")
|
|
207
|
+
|
|
208
|
+
pageRouter := baseRouter.NewRoute().Subrouter()
|
|
205
|
-
gromer.
|
|
209
|
+
gromer.ApiExplorerRoute(pageRouter, "/explorer")
|
|
206
|
-
{{#each pageRoutes as |route| }}gromer.Handle(
|
|
210
|
+
{{#each pageRoutes as |route| }}gromer.Handle(pageRouter, "{{ route.Method }}", "{{ route.Path }}", {{ route.Pkg }}.{{ route.Method }})
|
|
207
211
|
{{/each}}
|
|
208
212
|
|
|
209
|
-
apiRouter :=
|
|
213
|
+
apiRouter := baseRouter.NewRoute().Subrouter()
|
|
210
214
|
apiRouter.Use(gromer.CorsMiddleware)
|
|
211
215
|
{{#each apiRoutes as |route| }}gromer.Handle(apiRouter, "{{ route.Method }}", "{{ route.Path }}", {{ route.Pkg }}.{{ route.Method }})
|
|
212
216
|
{{/each}}
|
|
213
217
|
|
|
214
218
|
|
|
215
219
|
log.Info().Msg("http server listening on http://localhost:"+port)
|
|
216
|
-
srv := server.New(
|
|
220
|
+
srv := server.New(baseRouter, nil)
|
|
217
221
|
if err := srv.ListenAndServe(":"+port); err != nil {
|
|
218
222
|
log.Fatal().Stack().Err(err).Msg("failed to listen")
|
|
219
223
|
}
|
handlebars/template.go
CHANGED
|
@@ -2,6 +2,8 @@ package handlebars
|
|
|
2
2
|
|
|
3
3
|
import (
|
|
4
4
|
"fmt"
|
|
5
|
+
"io"
|
|
6
|
+
"net/http"
|
|
5
7
|
|
|
6
8
|
"github.com/aymerick/raymond/ast"
|
|
7
9
|
"github.com/aymerick/raymond/parser"
|
|
@@ -73,6 +75,18 @@ func (t *Template) Render() (HtmlContent, int, error) {
|
|
|
73
75
|
return t.RenderWithStatus(200)
|
|
74
76
|
}
|
|
75
77
|
|
|
78
|
+
func (t *Template) RenderWriter(w io.Writer) (int, error) {
|
|
79
|
+
s, status, err := t.Render()
|
|
80
|
+
if err != nil {
|
|
81
|
+
return status, err
|
|
82
|
+
}
|
|
83
|
+
if v, ok := w.(http.ResponseWriter); ok {
|
|
84
|
+
v.WriteHeader(status)
|
|
85
|
+
}
|
|
86
|
+
w.Write([]byte(s))
|
|
87
|
+
return 200, nil
|
|
88
|
+
}
|
|
89
|
+
|
|
76
90
|
func (t *Template) Prop(key string, v any) *Template {
|
|
77
91
|
t.Context.Set(key, v)
|
|
78
92
|
return t
|
http.go
CHANGED
|
@@ -269,6 +269,7 @@ func headerSize(h http.Header) int64 {
|
|
|
269
269
|
|
|
270
270
|
type LogResponseWriter struct {
|
|
271
271
|
http.ResponseWriter
|
|
272
|
+
startTime time.Time
|
|
272
273
|
responseStatusCode int
|
|
273
274
|
responseContentLength int
|
|
274
275
|
responseHeaderSize int
|
|
@@ -276,7 +277,7 @@ type LogResponseWriter struct {
|
|
|
276
277
|
}
|
|
277
278
|
|
|
278
279
|
func NewLogResponseWriter(w http.ResponseWriter) *LogResponseWriter {
|
|
279
|
-
return &LogResponseWriter{ResponseWriter: w}
|
|
280
|
+
return &LogResponseWriter{ResponseWriter: w, startTime: time.Now()}
|
|
280
281
|
}
|
|
281
282
|
|
|
282
283
|
func (w *LogResponseWriter) WriteHeader(code int) {
|
|
@@ -294,51 +295,54 @@ func (w *LogResponseWriter) SetError(err error) {
|
|
|
294
295
|
w.err = err
|
|
295
296
|
}
|
|
296
297
|
|
|
298
|
+
func (w *LogResponseWriter) LogRequest(r *http.Request) {
|
|
299
|
+
ip, _, _ := net.SplitHostPort(r.RemoteAddr)
|
|
300
|
+
if len(ip) > 0 && ip[0] == '[' {
|
|
301
|
+
ip = ip[1 : len(ip)-1]
|
|
302
|
+
}
|
|
303
|
+
logger := log.WithLevel(zerolog.InfoLevel)
|
|
304
|
+
if w.err != nil {
|
|
305
|
+
stack := string(debug.Stack())
|
|
306
|
+
logger = log.WithLevel(zerolog.ErrorLevel).Err(w.err).Str("stack", stack).Stack()
|
|
307
|
+
}
|
|
308
|
+
ua := useragent.Parse(r.UserAgent())
|
|
309
|
+
logger.Msgf("%s %d %.2f KB %s %s %s", r.Method,
|
|
310
|
+
w.responseStatusCode,
|
|
311
|
+
float32(w.responseContentLength)/1024.0,
|
|
312
|
+
time.Since(w.startTime).Round(time.Millisecond).String(), ua.Name, r.URL.Path)
|
|
313
|
+
// logger.
|
|
314
|
+
// Str("method", r.Method).
|
|
315
|
+
// Str("url", r.URL.String()).
|
|
316
|
+
// Int("header_size", int(headerSize(r.Header))).
|
|
317
|
+
// Int64("body_size", r.ContentLength).
|
|
318
|
+
// Str("host", r.Host).
|
|
319
|
+
// // Str("agent", r.UserAgent()).
|
|
320
|
+
// Str("referer", r.Referer()).
|
|
321
|
+
// Str("proto", r.Proto).
|
|
322
|
+
// Str("remote_ip", ip).
|
|
323
|
+
// Int("status", logRespWriter.responseStatusCode).
|
|
324
|
+
// Int("resp_header_size", logRespWriter.responseHeaderSize).
|
|
325
|
+
// Int("resp_body_size", logRespWriter.responseContentLength).
|
|
326
|
+
// Str("latency", time.Since(startTime).String()).
|
|
327
|
+
// Msgf("")
|
|
328
|
+
}
|
|
329
|
+
|
|
297
|
-
|
|
330
|
+
func LogMiddleware(next http.Handler) http.Handler {
|
|
298
331
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
332
|
+
logRespWriter := NewLogResponseWriter(w)
|
|
299
333
|
defer func() {
|
|
300
334
|
if err := recover(); err != nil {
|
|
301
|
-
stack := string(debug.Stack())
|
|
302
|
-
RespondError(
|
|
335
|
+
RespondError(logRespWriter, 599, fmt.Errorf("%+v", err))
|
|
336
|
+
logRespWriter.LogRequest(r)
|
|
303
337
|
}
|
|
304
338
|
}()
|
|
305
|
-
startTime := time.Now()
|
|
306
|
-
logRespWriter := NewLogResponseWriter(w)
|
|
307
339
|
next.ServeHTTP(logRespWriter, r)
|
|
308
340
|
if IsCloundRun {
|
|
309
341
|
return
|
|
310
342
|
}
|
|
311
|
-
ip, _, _ := net.SplitHostPort(r.RemoteAddr)
|
|
312
|
-
if len(ip) > 0 && ip[0] == '[' {
|
|
313
|
-
ip = ip[1 : len(ip)-1]
|
|
314
|
-
}
|
|
315
|
-
logger := log.WithLevel(zerolog.InfoLevel)
|
|
316
|
-
if logRespWriter.err != nil {
|
|
317
|
-
logger = log.WithLevel(zerolog.ErrorLevel).Err(logRespWriter.err).Stack()
|
|
318
|
-
}
|
|
319
|
-
ua := useragent.Parse(r.UserAgent())
|
|
320
|
-
logger.Msgf("%s %d %.2f KB %s %s %s", r.Method,
|
|
321
|
-
|
|
343
|
+
logRespWriter.LogRequest(r)
|
|
322
|
-
float32(logRespWriter.responseContentLength)/1024.0,
|
|
323
|
-
time.Since(startTime).Round(time.Millisecond).String(), ua.Name, r.URL.Path)
|
|
324
|
-
|
|
325
|
-
// logger.
|
|
326
|
-
// Str("method", r.Method).
|
|
327
|
-
// Str("url", r.URL.String()).
|
|
328
|
-
// Int("header_size", int(headerSize(r.Header))).
|
|
329
|
-
// Int64("body_size", r.ContentLength).
|
|
330
|
-
// Str("host", r.Host).
|
|
331
|
-
// // Str("agent", r.UserAgent()).
|
|
332
|
-
// Str("referer", r.Referer()).
|
|
333
|
-
// Str("proto", r.Proto).
|
|
334
|
-
// Str("remote_ip", ip).
|
|
335
|
-
// Int("status", logRespWriter.responseStatusCode).
|
|
336
|
-
// Int("resp_header_size", logRespWriter.responseHeaderSize).
|
|
337
|
-
// Int("resp_body_size", logRespWriter.responseContentLength).
|
|
338
|
-
// Str("latency", time.Since(startTime).String()).
|
|
339
|
-
// Msgf("")
|
|
340
344
|
})
|
|
341
|
-
}
|
|
345
|
+
}
|
|
342
346
|
|
|
343
347
|
func CorsMiddleware(next http.Handler) http.Handler {
|
|
344
348
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
@@ -353,6 +357,13 @@ func CorsMiddleware(next http.Handler) http.Handler {
|
|
|
353
357
|
})
|
|
354
358
|
}
|
|
355
359
|
|
|
360
|
+
func CacheMiddleware(next http.Handler) http.Handler {
|
|
361
|
+
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
362
|
+
w.Header().Set("Cache-Control", "public, max-age=2592000") // perma cache for 1 month
|
|
363
|
+
next.ServeHTTP(w, r)
|
|
364
|
+
})
|
|
365
|
+
}
|
|
366
|
+
|
|
356
367
|
func StatusHandler(h interface{}) http.Handler {
|
|
357
368
|
return LogMiddleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
358
369
|
ctx := context.WithValue(context.WithValue(r.Context(), "url", r.URL), "header", r.Header)
|
|
@@ -366,21 +377,22 @@ func StatusHandler(h interface{}) http.Handler {
|
|
|
366
377
|
}
|
|
367
378
|
w.Header().Set("Content-Type", "text/html")
|
|
368
379
|
|
|
369
|
-
// This has to be at end always
|
|
380
|
+
// This has to be at end always after headers are set
|
|
370
381
|
w.WriteHeader(responseStatus)
|
|
371
382
|
w.Write([]byte(response.(handlebars.HtmlContent)))
|
|
372
383
|
})).(http.Handler)
|
|
373
384
|
}
|
|
374
385
|
|
|
375
|
-
func WrapCache(h http.Handler) http.Handler {
|
|
376
|
-
|
|
386
|
+
func StaticRoute(router *mux.Router, path string, fs embed.FS) {
|
|
377
|
-
|
|
387
|
+
router.Path(path).Methods("GET").Handler(http.StripPrefix(path, http.FileServer(http.FS(fs))))
|
|
378
|
-
h.ServeHTTP(w, r)
|
|
379
|
-
})
|
|
380
388
|
}
|
|
381
389
|
|
|
382
|
-
func
|
|
390
|
+
func StylesRoute(router *mux.Router, path string) {
|
|
383
|
-
router.
|
|
391
|
+
router.Path(path).Methods("GET").HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
392
|
+
w.Header().Set("Content-Type", "text/css")
|
|
393
|
+
w.WriteHeader(200)
|
|
394
|
+
w.Write([]byte(handlebars.GetStyles()))
|
|
395
|
+
})
|
|
384
396
|
}
|
|
385
397
|
|
|
386
398
|
func Handle(router *mux.Router, method, route string, h interface{}) {
|
|
@@ -391,10 +403,6 @@ func Handle(router *mux.Router, method, route string, h interface{}) {
|
|
|
391
403
|
}).Methods(method, "OPTIONS")
|
|
392
404
|
}
|
|
393
405
|
|
|
394
|
-
func Styles(c context.Context) (handlebars.CssContent, int, error) {
|
|
395
|
-
return handlebars.GetStyles(), 200, nil
|
|
396
|
-
}
|
|
397
|
-
|
|
398
406
|
func GetUrl(ctx context.Context) *url.URL {
|
|
399
407
|
return ctx.Value("url").(*url.URL)
|
|
400
408
|
}
|