-
Notifications
You must be signed in to change notification settings - Fork 6
/
golte.go
146 lines (128 loc) · 4.57 KB
/
golte.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
package golte
import (
"context"
"io/fs"
"net/http"
"strings"
"github.com/nichady/golte/render"
)
// Props is an alias for map[string]any. It exists for documentation purposes.
// Props must be JSON-serializable when passing to fuctions defined in this package.
type Props = map[string]any
// New constructs a golte middleware from the given filesystem.
// The root of the filesystem should be the golte build directory.
//
// The returned middleware is used to add a render context to incoming requests.
// It will allow you to use [Layout], [AddLayout], [Page], and [RenderPage].
// It should be mounted on the root of your router.
// The middleware should not be mounted on routes other than the root.
func New(fsys fs.FS) func(http.Handler) http.Handler {
serverDir, err := fs.Sub(fsys, "server")
if err != nil {
panic(err)
}
clientDir, err := fs.Sub(fsys, "client")
if err != nil {
panic(err)
}
renderer := render.New(serverDir)
assets := fileServer(clientDir)
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if strings.HasPrefix(r.URL.Path, "/"+renderer.Assets()+"/") {
assets.ServeHTTP(w, r)
return
}
scheme := "http"
if r.TLS != nil {
scheme += "s"
}
ctx := context.WithValue(r.Context(), contextKey{}, &RenderContext{
Renderer: renderer,
ErrPage: "$$$GOLTE_DEFAULT_ERROR$$$",
csr: r.Header["Golte"] != nil,
scdata: render.SvelteContextData{
URL: scheme + "://" + r.Host + r.URL.String(),
},
})
next.ServeHTTP(w, r.WithContext(ctx))
})
}
}
// Layout returns a middleware that calls [AddLayout].
// Use this when there are no props needed to render the component.
// If you need to pass props, use [AddLayout] instead.
func Layout(component string) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
AddLayout(r, component, nil)
next.ServeHTTP(w, r)
})
}
}
// Error returns a middleware that calls [SetError].
// Use this when there are no props needed to render the component.
// If you need to pass props, use [SetError] instead.
func Error(component string) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
SetError(r, component)
next.ServeHTTP(w, r)
})
}
}
// Page returns a handler that calls [RenderPage].
// Use this when there are no props needed to render the component.
// If you need to pass props, use [RenderPage] instead.
func Page(component string) http.HandlerFunc {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
RenderPage(w, r, component, nil)
})
}
// AddLayout appends the component to the request.
// Layouts consist of any components with a <slot>.
// Calling this multiple times on the same request will nest layouts.
func AddLayout(r *http.Request, component string, props Props) {
rctx := MustGetRenderContext(r)
rctx.Components = append(rctx.Components, render.Entry{
Comp: component,
Props: props,
})
}
// SetError sets the error page for the request.
// Errors consist of any components that take the "message" and "status" props.
// Calling this multiple times on the same request will overrite the previous error page.
func SetError(r *http.Request, component string) {
MustGetRenderContext(r).ErrPage = component
}
// RenderPage renders the specified component.
// If any layouts were added previously, then each subsequent layout will
// go in the <slot> of the previous layout. The page will be in the <slot>
// of the last layout.
func RenderPage(w http.ResponseWriter, r *http.Request, component string, props Props) {
rctx := MustGetRenderContext(r)
rctx.Components = append(rctx.Components, render.Entry{
Comp: component,
Props: props,
})
rctx.Render(w)
}
// RenderError renders the current error page along with layouts.
// The error componenet will receive "message" and "status" as props.
// It will also write the status code to the header.
func RenderError(w http.ResponseWriter, r *http.Request, message string, status int) {
rctx := MustGetRenderContext(r)
entry := render.Entry{Comp: rctx.ErrPage, Props: Props{
"message": message,
"status": status,
}}
rctx.Components = append(rctx.Components, entry)
rctx.Render(respWriterWrapper{w})
}
// respWriterWrapper is needed to prevent superfluous WriteHeader calls
type respWriterWrapper struct {
http.ResponseWriter
}
func (w respWriterWrapper) WriteHeader(int) {
w.ResponseWriter.WriteHeader(http.StatusInternalServerError)
}