|
1 | 1 | package dashboard |
2 | 2 |
|
3 | 3 | import ( |
| 4 | + "bytes" |
4 | 5 | "context" |
| 6 | + "encoding/json" |
5 | 7 | "errors" |
6 | 8 | "fmt" |
7 | | - "io" |
8 | 9 | "io/fs" |
9 | 10 | "net/http" |
10 | 11 | "net/url" |
@@ -1730,17 +1731,40 @@ func (e *Extension) makeShellStaticHandler(shellFS fs.FS, stripPrefix string) ht |
1730 | 1731 |
|
1731 | 1732 | // makeShellSPAHandler returns the SPA index.html for any path under |
1732 | 1733 | // /{base}/contract/app/*. React Router handles the client-side routing. |
| 1734 | +// |
| 1735 | +// The handler injects a small bootstrap script just before </head> that |
| 1736 | +// surfaces the configured BasePath to the shell. This lets the React shell |
| 1737 | +// derive its API endpoint and Router basename at runtime instead of baking |
| 1738 | +// /dashboard into the bundle — required when the dashboard is mounted at a |
| 1739 | +// non-default base (e.g. /admin) or rebased behind a reverse proxy. |
1733 | 1740 | func (e *Extension) makeShellSPAHandler(shellFS fs.FS) http.HandlerFunc { |
1734 | 1741 | return func(w http.ResponseWriter, r *http.Request) { |
1735 | | - f, err := shellFS.Open("index.html") |
| 1742 | + raw, err := fs.ReadFile(shellFS, "index.html") |
1736 | 1743 | if err != nil { |
1737 | 1744 | http.Error(w, "shell index missing — has `pnpm build` been run inside extensions/dashboard/contract/shell?", http.StatusInternalServerError) |
1738 | 1745 | return |
1739 | 1746 | } |
1740 | | - defer f.Close() |
| 1747 | + // Build the inline bootstrap. Marshal through JSON so the basePath is |
| 1748 | + // safely string-escaped even if it ever contains odd characters. |
| 1749 | + cfg := map[string]string{ |
| 1750 | + "basePath": e.config.BasePath, |
| 1751 | + "contractBase": e.config.BasePath + "/api/dashboard/v1", |
| 1752 | + "shellBase": e.config.BasePath + "/contract/app", |
| 1753 | + } |
| 1754 | + cfgJSON, _ := json.Marshal(cfg) |
| 1755 | + bootstrap := []byte("<script>window.__FORGE_DASHBOARD__=" + string(cfgJSON) + ";</script>") |
| 1756 | + |
| 1757 | + out := raw |
| 1758 | + if idx := bytes.Index(out, []byte("</head>")); idx >= 0 { |
| 1759 | + out = append(out[:idx:idx], append(bootstrap, out[idx:]...)...) |
| 1760 | + } else { |
| 1761 | + // No </head> (unlikely with Vite output) — prepend the bootstrap so |
| 1762 | + // it still runs before any module script. |
| 1763 | + out = append(bootstrap, out...) |
| 1764 | + } |
1741 | 1765 | w.Header().Set("Content-Type", "text/html; charset=utf-8") |
1742 | 1766 | w.Header().Set("Cache-Control", "no-cache") |
1743 | | - _, _ = io.Copy(w, f) |
| 1767 | + _, _ = w.Write(out) |
1744 | 1768 | } |
1745 | 1769 | } |
1746 | 1770 |
|
|
0 commit comments