86 lines
2.4 KiB
Go
86 lines
2.4 KiB
Go
package webapp
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io/fs"
|
|
)
|
|
|
|
const bootPayloadGlobal = "window.__KANDEV_BOOT_PAYLOAD__"
|
|
const debugGlobalAssignment = "window.__KANDEV_DEBUG=true;"
|
|
const maxInt = int(^uint(0) >> 1)
|
|
|
|
var headCloseTag = []byte("</head>")
|
|
|
|
// RenderShell reads indexPath from assets and injects the boot payload before
|
|
// </head>. The assets FS can be an embedded Vite dist in production or an
|
|
// in-memory filesystem in tests.
|
|
func RenderShell(assets fs.FS, indexPath string, payload BootPayload) ([]byte, error) {
|
|
indexHTML, err := fs.ReadFile(assets, indexPath)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("read web shell: %w", err)
|
|
}
|
|
|
|
return RenderShellHTML(indexHTML, payload)
|
|
}
|
|
|
|
// RenderShellHTML injects the boot payload into an already-loaded HTML shell.
|
|
// Dev mode uses this with Vite's in-memory index.html.
|
|
func RenderShellHTML(indexHTML []byte, payload BootPayload) ([]byte, error) {
|
|
script, err := BootPayloadScript(payload)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return injectBeforeHeadClose(indexHTML, script), nil
|
|
}
|
|
|
|
// BootPayloadScript serializes payload into an inline script safe to embed in
|
|
// HTML. encoding/json escapes '<', '>', and '&', which prevents a state value
|
|
// from closing the script tag.
|
|
func BootPayloadScript(payload BootPayload) ([]byte, error) {
|
|
data, err := json.Marshal(payload)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("marshal boot payload: %w", err)
|
|
}
|
|
debugPrefix := ""
|
|
if payload.Runtime.Debug {
|
|
debugPrefix = debugGlobalAssignment
|
|
}
|
|
script := make([]byte, 0, bytesCapacity(len(data), len(debugPrefix), len(bootPayloadGlobal), 32))
|
|
script = append(script, "<script>"...)
|
|
script = append(script, debugPrefix...)
|
|
script = append(script, bootPayloadGlobal...)
|
|
script = append(script, "="...)
|
|
script = append(script, data...)
|
|
script = append(script, ";</script>"...)
|
|
return script, nil
|
|
}
|
|
|
|
func injectBeforeHeadClose(indexHTML, script []byte) []byte {
|
|
idx := bytes.Index(indexHTML, headCloseTag)
|
|
if idx < 0 {
|
|
out := make([]byte, 0, bytesCapacity(len(indexHTML), len(script)))
|
|
out = append(out, script...)
|
|
out = append(out, indexHTML...)
|
|
return out
|
|
}
|
|
|
|
out := make([]byte, 0, bytesCapacity(len(indexHTML), len(script)))
|
|
out = append(out, indexHTML[:idx]...)
|
|
out = append(out, script...)
|
|
out = append(out, indexHTML[idx:]...)
|
|
return out
|
|
}
|
|
|
|
func bytesCapacity(parts ...int) int {
|
|
total := 0
|
|
for _, part := range parts {
|
|
if part > maxInt-total {
|
|
return 0
|
|
}
|
|
total += part
|
|
}
|
|
return total
|
|
}
|