~repos /gromer

#golang#htmx#ssr

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.


a135e427 Peter John

tag: v0.18.1

v0.18.1

3 years ago
fix styles caching
Files changed (5) hide show
  1. _example/main.go +14 -10
  2. api_explorer.go +28 -22
  3. cmd/gromer/main.go +13 -9
  4. handlebars/template.go +14 -0
  5. 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
- r := mux.NewRouter()
32
+ baseRouter := mux.NewRouter()
33
- r.Use(gromer.LogMiddleware)
33
+ baseRouter.Use(gromer.LogMiddleware)
34
34
 
35
- r.NotFoundHandler = gromer.StatusHandler(not_found_404.GET)
35
+ baseRouter.NotFoundHandler = gromer.StatusHandler(not_found_404.GET)
36
36
 
37
+ staticRouter := baseRouter.NewRoute().Subrouter()
38
+ staticRouter.Use(gromer.CacheMiddleware)
37
- gromer.Static(r, "/assets/", assets.FS)
39
+ gromer.StaticRoute(staticRouter, "/assets/", assets.FS)
38
- gromer.Handle(r, "GET", "/styles.css", gromer.Styles)
40
+ gromer.StylesRoute(staticRouter, "/styles.css")
41
+
42
+ pageRouter := baseRouter.NewRoute().Subrouter()
39
- gromer.Handle(r, "GET", "/api", gromer.ApiExplorer)
43
+ gromer.ApiExplorerRoute(pageRouter, "/explorer")
40
- gromer.Handle(r, "GET", "/", pages.GET)
44
+ gromer.Handle(pageRouter, "GET", "/", pages.GET)
41
- gromer.Handle(r, "GET", "/about", about.GET)
45
+ gromer.Handle(pageRouter, "GET", "/about", about.GET)
42
46
 
43
47
 
44
- apiRouter := r.NewRoute().Subrouter()
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(r, nil)
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 ApiExplorer(ctx context.Context) (HtmlContent, int, error) {
15
+ func ApiExplorerRoute(router *mux.Router, path string) {
16
+ router.Path(path).Methods("GET").HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
15
- cmcss, _ := assets.FS.ReadFile("css/codemirror@5.63.1.css")
17
+ cmcss, _ := assets.FS.ReadFile("css/codemirror@5.63.1.css")
16
- stylescss, _ := assets.FS.ReadFile("css/styles.css")
18
+ stylescss, _ := assets.FS.ReadFile("css/styles.css")
17
- cmjs, _ := assets.FS.ReadFile("js/codemirror@5.63.1.min.js")
19
+ cmjs, _ := assets.FS.ReadFile("js/codemirror@5.63.1.min.js")
18
- cmjsjs, _ := assets.FS.ReadFile("js/codemirror-javascript@5.63.1.js")
20
+ cmjsjs, _ := assets.FS.ReadFile("js/codemirror-javascript@5.63.1.js")
19
- apiRoutes := []RouteDefinition{}
21
+ apiRoutes := []RouteDefinition{}
20
- for _, v := range RouteDefs {
22
+ for _, v := range RouteDefs {
21
- if strings.Contains(v.Path, "/api/") {
23
+ if strings.Contains(v.Path, "/api/") {
22
- apiRoutes = append(apiRoutes, v)
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
- return Html(`
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
- "commit", versioninfo.Revision[0:7],
265
+ "commit", versioninfo.Revision[0:7],
264
- "routes", apiRoutes,
266
+ "routes", apiRoutes,
265
- "apiData", template.HTML(string(apiData)),
267
+ "apiData", template.HTML(string(apiData)),
266
- "css", template.HTML(string(cmcss)+"\n\n"+string(stylescss)),
268
+ "css", template.HTML(string(cmcss)+"\n\n"+string(stylescss)),
267
- "js", template.HTML(string(cmjs)+"\n\n"+string(cmjsjs)),
269
+ "js", template.HTML(string(cmjs)+"\n\n"+string(cmjsjs)),
268
- ).Render()
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
- r := mux.NewRouter()
198
+ baseRouter := mux.NewRouter()
199
- r.Use(gromer.LogMiddleware)
199
+ baseRouter.Use(gromer.LogMiddleware)
200
200
  {{#if notFoundPkg}}
201
- r.NotFoundHandler = gromer.StatusHandler({{ notFoundPkg }}.GET)
201
+ baseRouter.NotFoundHandler = gromer.StatusHandler({{ notFoundPkg }}.GET)
202
202
  {{/if}}
203
+ staticRouter := baseRouter.NewRoute().Subrouter()
204
+ staticRouter.Use(gromer.CacheMiddleware)
203
- gromer.Static(r, "/assets/", assets.FS)
205
+ gromer.StaticRoute(staticRouter, "/assets/", assets.FS)
204
- gromer.Handle(r, "GET", "/styles.css", gromer.Styles)
206
+ gromer.StylesRoute(staticRouter, "/styles.css")
207
+
208
+ pageRouter := baseRouter.NewRoute().Subrouter()
205
- gromer.Handle(r, "GET", "/api", gromer.ApiExplorer)
209
+ gromer.ApiExplorerRoute(pageRouter, "/explorer")
206
- {{#each pageRoutes as |route| }}gromer.Handle(r, "{{ route.Method }}", "{{ route.Path }}", {{ route.Pkg }}.{{ route.Method }})
210
+ {{#each pageRoutes as |route| }}gromer.Handle(pageRouter, "{{ route.Method }}", "{{ route.Path }}", {{ route.Pkg }}.{{ route.Method }})
207
211
  {{/each}}
208
212
 
209
- apiRouter := r.NewRoute().Subrouter()
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(r, nil)
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
- var LogMiddleware = mux.MiddlewareFunc(func(next http.Handler) http.Handler {
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(w, 599, fmt.Errorf("panic: %+v\n %s", err, stack))
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
- logRespWriter.responseStatusCode,
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
- return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
386
+ func StaticRoute(router *mux.Router, path string, fs embed.FS) {
377
- w.Header().Set("Cache-Control", "public, max-age=2592000") // perma cache for 1 month
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 Static(router *mux.Router, path string, fs embed.FS) {
390
+ func StylesRoute(router *mux.Router, path string) {
383
- router.PathPrefix(path).Handler(http.StripPrefix(path, WrapCache(http.FileServer(http.FS(fs)))))
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
  }