fusee-framework 2.7.0 → 2.7.2
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/README.md +98 -26
- package/bin/create-fusee-linux +0 -0
- package/bin/create-fusee-mac-arm64 +0 -0
- package/bin/create-fusee-mac-intel +0 -0
- package/bin/create-fusee-win.exe +0 -0
- package/framework/auto-import-preset.js +20 -12
- package/framework/auto-imports.d.ts +68 -39
- package/framework/core/compiler.js +310 -310
- package/framework/core/component-core.js +445 -0
- package/framework/core/component.js +11 -440
- package/framework/core/composable.js +29 -29
- package/framework/core/h.js +119 -44
- package/framework/core/jsx-helpers.js +11 -2
- package/framework/core/jsx-runtime.js +73 -73
- package/framework/core/rust-compiler/pkg/fusee_compiler_bg.wasm +0 -0
- package/framework/core/store.js +333 -333
- package/framework/globals.js +42 -0
- package/framework/index.js +22 -55
- package/framework/router/file-router.js +184 -184
- package/framework/server/actions.js +183 -183
- package/framework/server/generate-manifest.js +3 -3
- package/framework/types/component.d.ts +97 -86
- package/framework/types/index.d.ts +118 -117
- package/package.json +83 -80
- package/framework/core/rust-compiler/Cargo.toml +0 -31
- package/framework/core/rust-compiler/README.md +0 -69
- package/framework/core/rust-compiler/pkg/README.md +0 -69
- package/framework/core/rust-compiler/pkg/fusee_compiler.d.ts +0 -6
- package/framework/core/rust-compiler/pkg/fusee_compiler.js +0 -9
- package/framework/core/rust-compiler/pkg/fusee_compiler_bg.js +0 -137
- package/framework/core/rust-compiler/pkg/fusee_compiler_bg.wasm.d.ts +0 -11
- package/framework/core/rust-compiler/pkg/package.json +0 -18
- package/framework/core/rust-compiler/src/ast.rs +0 -274
- package/framework/core/rust-compiler/src/errors.rs +0 -204
- package/framework/core/rust-compiler/src/generator.rs +0 -583
- package/framework/core/rust-compiler/src/lexer.rs +0 -414
- package/framework/core/rust-compiler/src/lib.rs +0 -87
- package/framework/core/rust-compiler/src/main_compiler.rs +0 -165
- package/framework/core/rust-compiler/src/parser.rs +0 -516
- package/framework/core/rust-compiler/src/ssr_generator.rs +0 -688
- package/framework/core/rust-compiler/src/transformer.rs +0 -562
- package/framework/core/rust-compiler/src/utils.rs +0 -10
- package/framework/engine-go/actions.go +0 -112
- package/framework/engine-go/cmd/server/main.go +0 -233
- package/framework/engine-go/comet/comet.go +0 -41
- package/framework/engine-go/comet/fragment.go +0 -51
- package/framework/engine-go/dispatcher.go +0 -120
- package/framework/engine-go/elements.go +0 -248
- package/framework/engine-go/evalexpr/evalexpr.go +0 -733
- package/framework/engine-go/go.mod +0 -3
- package/framework/engine-go/hydration.go +0 -43
- package/framework/engine-go/load-helpers.go +0 -29
- package/framework/engine-go/manifest.go +0 -41
- package/framework/engine-go/reactive.go +0 -123
- package/framework/engine-go/render.go +0 -635
- package/framework/engine-go/reqcontext.go +0 -48
- package/framework/engine-go/router.go +0 -85
- package/framework/engine-go/rustast/rustast.go +0 -317
- package/framework/engine-go/ssr.go +0 -103
- package/framework/engine-go/static-assets.go +0 -139
- package/framework/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +0 -1
|
@@ -1,233 +0,0 @@
|
|
|
1
|
-
package main
|
|
2
|
-
|
|
3
|
-
import (
|
|
4
|
-
"flag"
|
|
5
|
-
"fmt"
|
|
6
|
-
"log"
|
|
7
|
-
"net/http"
|
|
8
|
-
"os"
|
|
9
|
-
"path/filepath"
|
|
10
|
-
|
|
11
|
-
engine "fusee"
|
|
12
|
-
"fusee/evalexpr"
|
|
13
|
-
"fusee/rustast"
|
|
14
|
-
)
|
|
15
|
-
|
|
16
|
-
type PageHandler struct {
|
|
17
|
-
engine.Module
|
|
18
|
-
AST *rustast.Node
|
|
19
|
-
Title string
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
func main() {
|
|
23
|
-
port := flag.Int("port", 3000, "Port to listen on")
|
|
24
|
-
distDir := flag.String("dist", "./dist", "Path to dist directory for static assets")
|
|
25
|
-
flag.Parse()
|
|
26
|
-
|
|
27
|
-
// 1. Locate project root (containing package.json)
|
|
28
|
-
rootDir := "."
|
|
29
|
-
for _, candidate := range []string{".", "..", "../..", "../../.."} {
|
|
30
|
-
if _, err := os.Stat(filepath.Join(candidate, "package.json")); err == nil {
|
|
31
|
-
rootDir = candidate
|
|
32
|
-
break
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
absRoot, _ := filepath.Abs(rootDir)
|
|
36
|
-
|
|
37
|
-
// 2. Initialize Server Actions Registry
|
|
38
|
-
actionReg := engine.NewActionRegistry()
|
|
39
|
-
actionReg.DefineAction("submitContact", func(args []any) (any, error) {
|
|
40
|
-
var name, email string
|
|
41
|
-
if len(args) > 0 {
|
|
42
|
-
if m, ok := args[0].(map[string]any); ok {
|
|
43
|
-
name, _ = m["name"].(string)
|
|
44
|
-
email, _ = m["email"].(string)
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
log.Printf("[Action] submitContact received from %s <%s>", name, email)
|
|
48
|
-
return map[string]any{
|
|
49
|
-
"success": true,
|
|
50
|
-
"message": fmt.Sprintf("Thank you, %s! We received your message.", name),
|
|
51
|
-
}, nil
|
|
52
|
-
})
|
|
53
|
-
|
|
54
|
-
actionReg.DefineAction("submitFeedback", func(args []any) (any, error) {
|
|
55
|
-
var message string
|
|
56
|
-
if len(args) > 0 {
|
|
57
|
-
if m, ok := args[0].(map[string]any); ok {
|
|
58
|
-
message, _ = m["message"].(string)
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
log.Printf("[Action] submitFeedback received: %q", message)
|
|
62
|
-
return map[string]any{
|
|
63
|
-
"ok": true,
|
|
64
|
-
"received": message,
|
|
65
|
-
}, nil
|
|
66
|
-
})
|
|
67
|
-
|
|
68
|
-
// 3. Dynamically Load Routes from .fusee/manifest.json
|
|
69
|
-
var routes []*engine.Route
|
|
70
|
-
var elements engine.ElementRegistry
|
|
71
|
-
manifestPath := filepath.Join(absRoot, ".fusee", "manifest.json")
|
|
72
|
-
|
|
73
|
-
if manifest, err := engine.LoadManifest(manifestPath); err == nil && len(manifest.Routes) > 0 {
|
|
74
|
-
elements = manifest.ElementRegistry()
|
|
75
|
-
if len(elements) > 0 {
|
|
76
|
-
log.Printf("✨ [Fusee SSR] Loaded %d custom elements for declarative shadow DOM", len(elements))
|
|
77
|
-
}
|
|
78
|
-
log.Printf("✨ [Fusee SSR] Loaded %d dynamic routes from %s", len(manifest.Routes), manifestPath)
|
|
79
|
-
for _, mr := range manifest.Routes {
|
|
80
|
-
if mr.AST == nil {
|
|
81
|
-
continue
|
|
82
|
-
}
|
|
83
|
-
routeAST := mr.AST
|
|
84
|
-
routeTitle := mr.Title
|
|
85
|
-
handler := &PageHandler{
|
|
86
|
-
Module: engine.Module{
|
|
87
|
-
Load: func(params map[string]string, query map[string][]string, r *http.Request) (any, error) {
|
|
88
|
-
return map[string]any{
|
|
89
|
-
"title": routeTitle,
|
|
90
|
-
"status": "Pre-rendered dynamically by Go SSR Engine",
|
|
91
|
-
"count": 42,
|
|
92
|
-
"items": []any{"Dynamic Manifest", "Zero Go Code", "Rust AST Parser", "Selective Hydration"},
|
|
93
|
-
}, nil
|
|
94
|
-
},
|
|
95
|
-
},
|
|
96
|
-
AST: routeAST,
|
|
97
|
-
Title: routeTitle,
|
|
98
|
-
}
|
|
99
|
-
routes = append(routes, engine.Compile(mr.Pattern, handler))
|
|
100
|
-
}
|
|
101
|
-
} else {
|
|
102
|
-
log.Printf("ℹ️ [Fusee SSR] No manifest found at %s. Using default fallback routes.", manifestPath)
|
|
103
|
-
routes = append(routes, engine.Compile("/", &engine.Module{
|
|
104
|
-
Load: func(params map[string]string, query map[string][]string, r *http.Request) (any, error) {
|
|
105
|
-
return map[string]any{
|
|
106
|
-
"title": "Welcome to Fusee SSR",
|
|
107
|
-
"status": "SSR Active",
|
|
108
|
-
"count": 10,
|
|
109
|
-
"items": []any{"High Performance Signals", "Go SSR Engine", "Rust AST Parser", "Selective Hydration"},
|
|
110
|
-
"version": "2.0.0",
|
|
111
|
-
}, nil
|
|
112
|
-
},
|
|
113
|
-
}))
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
// 4. Configure Dynamic Render Function
|
|
117
|
-
renderFunc := func(w http.ResponseWriter, r *http.Request, route *engine.Route, params map[string]string, data any) {
|
|
118
|
-
scope := evalexpr.Scope{}
|
|
119
|
-
if m, ok := data.(map[string]any); ok {
|
|
120
|
-
for k, v := range m {
|
|
121
|
-
scope[k] = v
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
for k, v := range params {
|
|
125
|
-
scope[k] = v
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
var bodyHTML string
|
|
129
|
-
var err error
|
|
130
|
-
|
|
131
|
-
if pageH, ok := route.Handler.(*PageHandler); ok && pageH.AST != nil {
|
|
132
|
-
bodyHTML, err = engine.RenderWithElements(pageH.AST, scope, nil, elements)
|
|
133
|
-
} else {
|
|
134
|
-
bodyHTML = fmt.Sprintf(`<div class="page"><h1>%s</h1><p>%s</p></div>`, scope["title"], scope["status"])
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
if err != nil {
|
|
138
|
-
http.Error(w, fmt.Sprintf("Render Error: %v", err), http.StatusInternalServerError)
|
|
139
|
-
return
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
signals := map[string][]any{}
|
|
143
|
-
if c, ok := scope["count"]; ok {
|
|
144
|
-
signals["count"] = []any{c}
|
|
145
|
-
}
|
|
146
|
-
if s, ok := scope["status"]; ok {
|
|
147
|
-
signals["status"] = []any{s}
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
hydrationScript, _ := engine.RenderDehydrationScript(nil, signals)
|
|
151
|
-
|
|
152
|
-
pageTitle := "Fusee SSR App"
|
|
153
|
-
if t, ok := scope["title"].(string); ok {
|
|
154
|
-
pageTitle = t
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
fullHTML := fmt.Sprintf(`<!DOCTYPE html>
|
|
158
|
-
<html lang="en">
|
|
159
|
-
<head>
|
|
160
|
-
<meta charset="UTF-8" />
|
|
161
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
162
|
-
<title>%s</title>
|
|
163
|
-
<link rel="icon" href="/fusee-logo.svg" type="image/svg+xml" />
|
|
164
|
-
<style>
|
|
165
|
-
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
166
|
-
body { font-family: system-ui, sans-serif; background: #0f0f13; color: #e8e8f0; min-height: 100vh; }
|
|
167
|
-
nav { display: flex; flex-wrap: wrap; gap: 1rem; padding: 1rem 2rem; background: #1a1a24; border-bottom: 1px solid #2a2a3a; }
|
|
168
|
-
nav a { color: #ff3333; text-decoration: none; font-weight: 500; }
|
|
169
|
-
.content-wrap { padding: 2rem; max-width: 800px; margin: 0 auto; width: 100%%; }
|
|
170
|
-
.demo-card { background: #13131d; border: 1px solid #2a2a3a; border-radius: 12px; padding: 1.5rem; margin-bottom: 1.5rem; }
|
|
171
|
-
.primary-btn { padding: 0.6rem 1.25rem; border: none; background: #3b82f6; color: #fff; font-weight: 600; border-radius: 6px; cursor: pointer; }
|
|
172
|
-
.cmp-btn { padding: 0.45rem 1.1rem; border-radius: 6px; border: 1px solid #2a2a3a; background: #0f0f1a; color: #e8e8f0; cursor: pointer; }
|
|
173
|
-
.cmp-input { width: 100%%; padding: 0.5rem 0.75rem; background: #0f0f1a; border: 1px solid #2a2a3a; border-radius: 6px; color: #e8e8f0; }
|
|
174
|
-
.badge { display: inline-block; padding: 0.2rem 0.65rem; border-radius: 999px; font-size: 0.75rem; font-weight: 600; }
|
|
175
|
-
.badge-blue { background: rgba(96, 165, 250, 0.15); color: #60a5fa; }
|
|
176
|
-
.badge-red { background: rgba(255, 51, 51, 0.12); color: #ff3333; }
|
|
177
|
-
.info-section { background: #0f0f1a; border: 1px solid #2a2a3a; border-radius: 12px; padding: 1.5rem; margin-top: 2rem; }
|
|
178
|
-
.info-section h2 { color: #34d399; margin-bottom: 1rem; font-size: 1.1rem; }
|
|
179
|
-
.info-section ul { margin-left: 1.5rem; }
|
|
180
|
-
.info-section li { color: #aaa; margin-bottom: 0.5rem; }
|
|
181
|
-
</style>
|
|
182
|
-
</head>
|
|
183
|
-
<body>
|
|
184
|
-
<div id="app">
|
|
185
|
-
<div class="root-layout">
|
|
186
|
-
<nav>
|
|
187
|
-
<a href="/" f-link>Home</a>
|
|
188
|
-
<a href="/about" f-link>About</a>
|
|
189
|
-
<a href="/ssr-demo" f-link style="color: #60a5fa;">🚀 SSR Demo</a>
|
|
190
|
-
<a href="/users" f-link style="color: #f59e0b;">👥 Users</a>
|
|
191
|
-
</nav>
|
|
192
|
-
<main class="content-wrap">
|
|
193
|
-
<div data-router-view>%s</div>
|
|
194
|
-
</main>
|
|
195
|
-
</div>
|
|
196
|
-
</div>
|
|
197
|
-
%s
|
|
198
|
-
<script type="module" src="/app/main.js"></script>
|
|
199
|
-
</body>
|
|
200
|
-
</html>`, pageTitle, bodyHTML, hydrationScript)
|
|
201
|
-
|
|
202
|
-
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
|
203
|
-
w.WriteHeader(http.StatusOK)
|
|
204
|
-
w.Write([]byte(fullHTML))
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
// 5. Configure Static Assets Directory
|
|
208
|
-
absDist := *distDir
|
|
209
|
-
if absDist == "./dist" || absDist == "dist" {
|
|
210
|
-
absDist = filepath.Join(absRoot, "dist")
|
|
211
|
-
if _, err := os.Stat(absDist); os.IsNotExist(err) {
|
|
212
|
-
absDist = absRoot
|
|
213
|
-
}
|
|
214
|
-
} else {
|
|
215
|
-
absDist, _ = filepath.Abs(absDist)
|
|
216
|
-
}
|
|
217
|
-
log.Printf("[Static Assets] Serving files from: %s", absDist)
|
|
218
|
-
|
|
219
|
-
// 6. Create Dispatcher & Listen
|
|
220
|
-
dispatcher := engine.NewDispatcher(engine.Config{
|
|
221
|
-
DistDir: absDist,
|
|
222
|
-
Routes: routes,
|
|
223
|
-
Actions: actionReg,
|
|
224
|
-
Render: renderFunc,
|
|
225
|
-
ActionsBasePath: "/__fusee/actions",
|
|
226
|
-
})
|
|
227
|
-
|
|
228
|
-
addr := fmt.Sprintf(":%d", *port)
|
|
229
|
-
log.Printf("Fusee Go Server listening on http://localhost:%d", *port)
|
|
230
|
-
if err := http.ListenAndServe(addr, dispatcher); err != nil {
|
|
231
|
-
log.Fatalf("Server error: %v", err)
|
|
232
|
-
}
|
|
233
|
-
}
|
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
package comet
|
|
2
|
-
|
|
3
|
-
import "net/http"
|
|
4
|
-
|
|
5
|
-
const (
|
|
6
|
-
headerRequest = "Comet-Request"
|
|
7
|
-
headerTrigger = "Comet-Trigger"
|
|
8
|
-
headerTarget = "Comet-Target"
|
|
9
|
-
headerRetarget = "Comet-Retarget"
|
|
10
|
-
headerReswap = "Comet-Reswap"
|
|
11
|
-
headerPushURL = "Comet-Push-Url"
|
|
12
|
-
headerRedirect = "Comet-Redirect"
|
|
13
|
-
)
|
|
14
|
-
|
|
15
|
-
func IsComet(r *http.Request) bool {
|
|
16
|
-
return r.Header.Get(headerRequest) == "true"
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
func GetCometTarget(r *http.Request) string {
|
|
20
|
-
return r.Header.Get(headerTarget)
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
func GetCometTrigger(r *http.Request) string {
|
|
24
|
-
return r.Header.Get(headerTrigger)
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
func Retarget(w http.ResponseWriter, selector string) {
|
|
28
|
-
w.Header().Set(headerRetarget, selector)
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
func Reswap(w http.ResponseWriter, mode string) {
|
|
32
|
-
w.Header().Set(headerReswap, mode)
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
func PushURL(w http.ResponseWriter, url string) {
|
|
36
|
-
w.Header().Set(headerPushURL, url)
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
func Redirect(w http.ResponseWriter, url string) {
|
|
40
|
-
w.Header().Set(headerRedirect, url)
|
|
41
|
-
}
|
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
package comet
|
|
2
|
-
|
|
3
|
-
import (
|
|
4
|
-
"context"
|
|
5
|
-
"net/http"
|
|
6
|
-
|
|
7
|
-
engine "fusee"
|
|
8
|
-
)
|
|
9
|
-
|
|
10
|
-
// FragmentAware wraps a fragment renderer and a full page renderer, dispatching to
|
|
11
|
-
// the fragment renderer when Comet-Request header is detected, or fullPage otherwise.
|
|
12
|
-
func FragmentAware(fragment, fullPage engine.RenderFunc) engine.RenderFunc {
|
|
13
|
-
return func(w http.ResponseWriter, r *http.Request, route *engine.Route, params map[string]string, data any) {
|
|
14
|
-
if IsComet(r) {
|
|
15
|
-
fragment(w, r, route, params, data)
|
|
16
|
-
return
|
|
17
|
-
}
|
|
18
|
-
fullPage(w, r, route, params, data)
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
// RenderFragment writes a standalone HTML fragment response with text/html content type.
|
|
23
|
-
func RenderFragment(w http.ResponseWriter, html string) {
|
|
24
|
-
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
|
25
|
-
w.WriteHeader(http.StatusOK)
|
|
26
|
-
_, _ = w.Write([]byte(html))
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
// RenderFragmentWithState writes an HTML fragment accompanied by an embedded Fusée hydration state script.
|
|
30
|
-
func RenderFragmentWithState(w http.ResponseWriter, html string, resources map[string]map[string]any, signals map[string][]any) error {
|
|
31
|
-
script, err := engine.RenderDehydrationScript(resources, signals)
|
|
32
|
-
if err != nil {
|
|
33
|
-
return err
|
|
34
|
-
}
|
|
35
|
-
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
|
36
|
-
w.WriteHeader(http.StatusOK)
|
|
37
|
-
_, err = w.Write([]byte(html + "\n" + script))
|
|
38
|
-
return err
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
// RenderFragmentWithContext writes an HTML fragment with hydration state extracted from the request Context.
|
|
42
|
-
func RenderFragmentWithContext(w http.ResponseWriter, ctx context.Context, html string) error {
|
|
43
|
-
script, err := engine.BuildFromContext(ctx)
|
|
44
|
-
if err != nil {
|
|
45
|
-
return err
|
|
46
|
-
}
|
|
47
|
-
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
|
48
|
-
w.WriteHeader(http.StatusOK)
|
|
49
|
-
_, err = w.Write([]byte(html + "\n" + script))
|
|
50
|
-
return err
|
|
51
|
-
}
|
|
@@ -1,120 +0,0 @@
|
|
|
1
|
-
package engine
|
|
2
|
-
|
|
3
|
-
import (
|
|
4
|
-
"encoding/json"
|
|
5
|
-
"errors"
|
|
6
|
-
"log"
|
|
7
|
-
"net/http"
|
|
8
|
-
"strings"
|
|
9
|
-
)
|
|
10
|
-
|
|
11
|
-
const defaultActionsBasePath = "/__fusee/actions"
|
|
12
|
-
|
|
13
|
-
type LoadFunc func(params map[string]string, query map[string][]string, r *http.Request) (any, error)
|
|
14
|
-
|
|
15
|
-
type RenderFunc func(w http.ResponseWriter, r *http.Request, route *Route, params map[string]string, data any)
|
|
16
|
-
|
|
17
|
-
type Module struct {
|
|
18
|
-
Load LoadFunc
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
// AsModule returns the module itself. Route handlers that embed Module (for example a
|
|
22
|
-
// page handler that also carries its AST) inherit it, so the dispatcher still finds Load.
|
|
23
|
-
func (m *Module) AsModule() *Module { return m }
|
|
24
|
-
|
|
25
|
-
type moduleHandler interface {
|
|
26
|
-
AsModule() *Module
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
type Config struct {
|
|
30
|
-
DistDir string
|
|
31
|
-
Routes []*Route
|
|
32
|
-
Actions *ActionRegistry
|
|
33
|
-
Render RenderFunc
|
|
34
|
-
ActionsBasePath string
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
type Dispatcher struct {
|
|
38
|
-
cfg Config
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
func NewDispatcher(cfg Config) *Dispatcher {
|
|
42
|
-
if cfg.ActionsBasePath == "" {
|
|
43
|
-
cfg.ActionsBasePath = defaultActionsBasePath
|
|
44
|
-
}
|
|
45
|
-
return &Dispatcher{cfg: cfg}
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
func (d *Dispatcher) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|
49
|
-
// 1. Static asset bypass
|
|
50
|
-
if d.cfg.DistDir != "" && (r.Method == http.MethodGet || r.Method == http.MethodHead) {
|
|
51
|
-
if path, ok := Resolve(d.cfg.DistDir, r.URL.Path); ok {
|
|
52
|
-
log.Printf("[HTTP 200] %s %s -> static: %s", r.Method, r.URL.Path, path)
|
|
53
|
-
if err := Serve(w, r, path); err != nil {
|
|
54
|
-
log.Printf("[HTTP 500] %s %s error serving static: %v", r.Method, r.URL.Path, err)
|
|
55
|
-
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
|
56
|
-
}
|
|
57
|
-
return
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
// 2. Server Actions interceptor
|
|
62
|
-
if r.Method == http.MethodPost && strings.HasPrefix(r.URL.Path, d.cfg.ActionsBasePath) {
|
|
63
|
-
if d.cfg.Actions == nil {
|
|
64
|
-
log.Printf("[HTTP 404] %s %s (No ActionRegistry)", r.Method, r.URL.Path)
|
|
65
|
-
http.Error(w, "Not Found", http.StatusNotFound)
|
|
66
|
-
return
|
|
67
|
-
}
|
|
68
|
-
name := strings.TrimPrefix(strings.TrimPrefix(r.URL.Path, d.cfg.ActionsBasePath), "/")
|
|
69
|
-
log.Printf("[HTTP Action] %s %s -> action: %s", r.Method, r.URL.Path, name)
|
|
70
|
-
d.cfg.Actions.HandleRequest(w, r, name)
|
|
71
|
-
return
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
match := MatchAll(d.cfg.Routes, r.URL.Path)
|
|
75
|
-
if match == nil {
|
|
76
|
-
log.Printf("[HTTP 404] %s %s (No route or static file matched)", r.Method, r.URL.Path)
|
|
77
|
-
http.Error(w, "Not Found", http.StatusNotFound)
|
|
78
|
-
return
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
log.Printf("[HTTP SSR] %s %s -> route: %s params: %+v", r.Method, r.URL.Path, match.Route.Pattern, match.Params)
|
|
82
|
-
|
|
83
|
-
var data any
|
|
84
|
-
if h, ok := match.Route.Handler.(moduleHandler); ok && h.AsModule() != nil && h.AsModule().Load != nil {
|
|
85
|
-
module := h.AsModule()
|
|
86
|
-
var err error
|
|
87
|
-
data, err = module.Load(match.Params, r.URL.Query(), r)
|
|
88
|
-
if err != nil {
|
|
89
|
-
log.Printf("[HTTP LoadError] %s %s: %v", r.Method, r.URL.Path, err)
|
|
90
|
-
d.handleLoadError(w, r, match.Route, err)
|
|
91
|
-
return
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
if d.cfg.Render == nil {
|
|
96
|
-
w.Header().Set("Content-Type", "application/json")
|
|
97
|
-
w.WriteHeader(http.StatusOK)
|
|
98
|
-
json.NewEncoder(w).Encode(map[string]any{"params": match.Params, "data": data})
|
|
99
|
-
return
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
d.cfg.Render(w, r, match.Route, match.Params, data)
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
func (d *Dispatcher) handleLoadError(w http.ResponseWriter, r *http.Request, route *Route, err error) {
|
|
106
|
-
var redirect *RouteRedirect
|
|
107
|
-
if errors.As(err, &redirect) {
|
|
108
|
-
w.Header().Set("Location", redirect.Location)
|
|
109
|
-
w.WriteHeader(redirect.Status)
|
|
110
|
-
return
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
var httpErr *RouteHTTPError
|
|
114
|
-
if errors.As(err, &httpErr) {
|
|
115
|
-
http.Error(w, httpErr.Message, httpErr.Status)
|
|
116
|
-
return
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
|
120
|
-
}
|
|
@@ -1,248 +0,0 @@
|
|
|
1
|
-
package engine
|
|
2
|
-
|
|
3
|
-
import (
|
|
4
|
-
"encoding/json"
|
|
5
|
-
"fmt"
|
|
6
|
-
"strconv"
|
|
7
|
-
"strings"
|
|
8
|
-
|
|
9
|
-
"fusee/evalexpr"
|
|
10
|
-
"fusee/rustast"
|
|
11
|
-
)
|
|
12
|
-
|
|
13
|
-
// maxElementDepth bounds element-in-element nesting so a self-rendering element fails
|
|
14
|
-
// instead of recursing forever.
|
|
15
|
-
const maxElementDepth = 50
|
|
16
|
-
|
|
17
|
-
// ElementProp mirrors one prop declared with defineElement().
|
|
18
|
-
type ElementProp struct {
|
|
19
|
-
Name string `json:"name"`
|
|
20
|
-
Attribute string `json:"attribute"` // "" for property-only props (attribute: false)
|
|
21
|
-
Type string `json:"type"` // "String", "Number", "Boolean", "Array", "Object" or ""
|
|
22
|
-
Default any `json:"default"`
|
|
23
|
-
Reflect bool `json:"reflect"`
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
// ElementShadow mirrors the shadow root options of an element.
|
|
27
|
-
type ElementShadow struct {
|
|
28
|
-
Mode string `json:"mode"`
|
|
29
|
-
DelegatesFocus bool `json:"delegatesFocus"`
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
// Element is a custom element registered with defineElement(), rendered server-side as
|
|
33
|
-
// declarative shadow DOM. Setup is optional and plays the role of the JS setup():
|
|
34
|
-
// it receives the resolved props and returns the template scope. Without it, the
|
|
35
|
-
// template sees each prop by name and the whole set as `props`.
|
|
36
|
-
type Element struct {
|
|
37
|
-
Tag string `json:"tag"`
|
|
38
|
-
Props []ElementProp `json:"props"`
|
|
39
|
-
Styles string `json:"styles"`
|
|
40
|
-
Shadow ElementShadow `json:"shadow"`
|
|
41
|
-
AST *rustast.Node `json:"ast"`
|
|
42
|
-
Setup func(props evalexpr.Scope) (evalexpr.Scope, error) `json:"-"`
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
// ElementRegistry maps lowercase tag names to element definitions.
|
|
46
|
-
type ElementRegistry map[string]*Element
|
|
47
|
-
|
|
48
|
-
// NewElementRegistry indexes elements by lowercase tag name, skipping entries without an AST.
|
|
49
|
-
func NewElementRegistry(elements []*Element) ElementRegistry {
|
|
50
|
-
reg := ElementRegistry{}
|
|
51
|
-
for _, el := range elements {
|
|
52
|
-
if el == nil || el.AST == nil || el.Tag == "" {
|
|
53
|
-
continue
|
|
54
|
-
}
|
|
55
|
-
reg[strings.ToLower(el.Tag)] = el
|
|
56
|
-
}
|
|
57
|
-
return reg
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
func (reg ElementRegistry) lookup(tag string) *Element {
|
|
61
|
-
if reg == nil || !strings.Contains(tag, "-") {
|
|
62
|
-
return nil
|
|
63
|
-
}
|
|
64
|
-
return reg[strings.ToLower(tag)]
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
func (p ElementProp) matches(name string) bool {
|
|
68
|
-
return strings.EqualFold(name, p.Name) || (p.Attribute != "" && strings.EqualFold(name, p.Attribute))
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
// propFromAttribute converts a static attribute value (nil when present without a value)
|
|
72
|
-
// to the prop's type, mirroring the client converters. ok is false when the value cannot
|
|
73
|
-
// be converted, in which case the default applies.
|
|
74
|
-
func (p ElementProp) propFromAttribute(value *string) (any, bool) {
|
|
75
|
-
if p.Type == "Boolean" {
|
|
76
|
-
return true, true
|
|
77
|
-
}
|
|
78
|
-
if value == nil {
|
|
79
|
-
return nil, false
|
|
80
|
-
}
|
|
81
|
-
switch p.Type {
|
|
82
|
-
case "Number":
|
|
83
|
-
if *value == "" {
|
|
84
|
-
return nil, false
|
|
85
|
-
}
|
|
86
|
-
f, err := strconv.ParseFloat(*value, 64)
|
|
87
|
-
if err != nil {
|
|
88
|
-
return nil, false
|
|
89
|
-
}
|
|
90
|
-
return f, true
|
|
91
|
-
case "Array", "Object":
|
|
92
|
-
var v any
|
|
93
|
-
if err := json.Unmarshal([]byte(*value), &v); err != nil {
|
|
94
|
-
return nil, false
|
|
95
|
-
}
|
|
96
|
-
return v, true
|
|
97
|
-
default:
|
|
98
|
-
return *value, true
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
// propToAttribute converts a prop value to its attribute form; ok is false when the
|
|
103
|
-
// attribute should be absent.
|
|
104
|
-
func (p ElementProp) propToAttribute(value any) (string, bool) {
|
|
105
|
-
if p.Type == "Boolean" {
|
|
106
|
-
return "", truthyForClass(value)
|
|
107
|
-
}
|
|
108
|
-
if value == nil {
|
|
109
|
-
return "", false
|
|
110
|
-
}
|
|
111
|
-
if p.Type == "Array" || p.Type == "Object" {
|
|
112
|
-
raw, err := json.Marshal(value)
|
|
113
|
-
if err != nil {
|
|
114
|
-
return "", false
|
|
115
|
-
}
|
|
116
|
-
return string(raw), true
|
|
117
|
-
}
|
|
118
|
-
return displayString(value), true
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
// resolveElementProps builds the props scope from the element's static attributes and
|
|
122
|
-
// bindings. It also returns the reflected attributes that must be added to the tag
|
|
123
|
-
// because nothing in the template set them.
|
|
124
|
-
func resolveElementProps(el *Element, props []*rustast.Prop, scope evalexpr.Scope) (evalexpr.Scope, [][2]string, error) {
|
|
125
|
-
resolved := evalexpr.Scope{}
|
|
126
|
-
var reflected [][2]string
|
|
127
|
-
|
|
128
|
-
for _, def := range el.Props {
|
|
129
|
-
value, found := def.Default, false
|
|
130
|
-
|
|
131
|
-
for _, p := range props {
|
|
132
|
-
switch p.Type {
|
|
133
|
-
case rustast.PropAttribute:
|
|
134
|
-
if def.Attribute == "" || !strings.EqualFold(p.Attribute.Name, def.Attribute) {
|
|
135
|
-
continue
|
|
136
|
-
}
|
|
137
|
-
found = true
|
|
138
|
-
if v, ok := def.propFromAttribute(p.Attribute.Value); ok {
|
|
139
|
-
value = v
|
|
140
|
-
}
|
|
141
|
-
case rustast.PropBinding:
|
|
142
|
-
if !def.matches(p.Binding.Name) {
|
|
143
|
-
continue
|
|
144
|
-
}
|
|
145
|
-
found = true
|
|
146
|
-
v, err := evalexpr.Eval(p.Binding.Expression.Content, scope)
|
|
147
|
-
if err != nil {
|
|
148
|
-
return nil, nil, err
|
|
149
|
-
}
|
|
150
|
-
if v != nil {
|
|
151
|
-
value = v
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
resolved[def.Name] = value
|
|
157
|
-
if def.Reflect && !found {
|
|
158
|
-
if attr, ok := def.propToAttribute(value); ok {
|
|
159
|
-
reflected = append(reflected, [2]string{def.Attribute, attr})
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
return resolved, reflected, nil
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
// elementScope runs the element's Go Setup, or exposes the props directly to the template.
|
|
168
|
-
func elementScope(el *Element, props evalexpr.Scope) (evalexpr.Scope, error) {
|
|
169
|
-
if el.Setup != nil {
|
|
170
|
-
return el.Setup(props)
|
|
171
|
-
}
|
|
172
|
-
scope := cloneScope(props)
|
|
173
|
-
scope["props"] = map[string]any(props)
|
|
174
|
-
return scope, nil
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
// slotOutlets makes every slot outlet in a shadow template render as a native <slot>.
|
|
178
|
-
func slotOutlets(ast *rustast.Node) map[string][]*rustast.Node {
|
|
179
|
-
outlets := map[string][]*rustast.Node{}
|
|
180
|
-
var walk func(nodes []*rustast.Node)
|
|
181
|
-
walk = func(nodes []*rustast.Node) {
|
|
182
|
-
for _, n := range nodes {
|
|
183
|
-
switch n.Type {
|
|
184
|
-
case rustast.NodeSlotOutlet:
|
|
185
|
-
name := n.SlotOutlet.SlotName
|
|
186
|
-
tag := "<slot></slot>"
|
|
187
|
-
if name != "default" {
|
|
188
|
-
tag = fmt.Sprintf(`<slot name="%s"></slot>`, escapeAttr(name))
|
|
189
|
-
}
|
|
190
|
-
outlets[name] = []*rustast.Node{{Type: rustast.NodeText, Text: &rustast.TextNode{Content: tag}}}
|
|
191
|
-
walk(n.SlotOutlet.Fallback)
|
|
192
|
-
case rustast.NodeElement:
|
|
193
|
-
walk(n.Element.Children)
|
|
194
|
-
case rustast.NodeComponent:
|
|
195
|
-
for _, s := range n.Component.Slots {
|
|
196
|
-
walk(s.Children)
|
|
197
|
-
}
|
|
198
|
-
case rustast.NodeIf:
|
|
199
|
-
for _, b := range n.If.Branches {
|
|
200
|
-
walk([]*rustast.Node{b.Node})
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
walk(ast.Root.Children)
|
|
206
|
-
return outlets
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
// renderShadowRoot writes `<template shadowrootmode>` with the element's styles and its
|
|
210
|
-
// template rendered against the resolved props.
|
|
211
|
-
func (r *Renderer) renderShadowRoot(el *Element, props evalexpr.Scope, sb *strings.Builder) error {
|
|
212
|
-
if r.depth >= maxElementDepth {
|
|
213
|
-
return fmt.Errorf("render: elements nested deeper than %d levels at <%s> (recursive element?)", maxElementDepth, el.Tag)
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
scope, err := elementScope(el, props)
|
|
217
|
-
if err != nil {
|
|
218
|
-
return fmt.Errorf("render: setup of <%s> failed: %w", el.Tag, err)
|
|
219
|
-
}
|
|
220
|
-
scope = cloneScope(scope)
|
|
221
|
-
scope["_slots"] = slotOutlets(el.AST)
|
|
222
|
-
|
|
223
|
-
child := NewRenderer(r.Components)
|
|
224
|
-
child.Elements = r.Elements
|
|
225
|
-
child.depth = r.depth + 1
|
|
226
|
-
html, err := child.renderRoot(el.AST, scope)
|
|
227
|
-
if err != nil {
|
|
228
|
-
return err
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
mode := "open"
|
|
232
|
-
if el.Shadow.Mode == "closed" {
|
|
233
|
-
mode = "closed"
|
|
234
|
-
}
|
|
235
|
-
fmt.Fprintf(sb, `<template shadowrootmode="%s"`, mode)
|
|
236
|
-
if el.Shadow.DelegatesFocus {
|
|
237
|
-
sb.WriteString(" shadowrootdelegatesfocus")
|
|
238
|
-
}
|
|
239
|
-
sb.WriteByte('>')
|
|
240
|
-
if el.Styles != "" {
|
|
241
|
-
sb.WriteString("<style>")
|
|
242
|
-
sb.WriteString(strings.ReplaceAll(el.Styles, "</style", `<\/style`))
|
|
243
|
-
sb.WriteString("</style>")
|
|
244
|
-
}
|
|
245
|
-
sb.WriteString(html)
|
|
246
|
-
sb.WriteString("</template>")
|
|
247
|
-
return nil
|
|
248
|
-
}
|