feat(serverembed): add initial HTML and SVG assets
* Create index.html for the web application entry point * Add vite.svg as a favicon * Update frontend.go to embed all files in the dist directory * Modify vite.config.ts to set output directory for builds
This commit is contained in:
2
Makefile
2
Makefile
@@ -1,7 +1,7 @@
|
||||
.PHONY: build clean test lint lintfix run-server run-cli help build-ui migrate generate-models install-relspecgo seed
|
||||
|
||||
# Variables
|
||||
FRONTEND_DIR=frontend
|
||||
FRONTEND_DIR=web
|
||||
SQL_DIR=sql
|
||||
MODELS_DIR=pkg/models
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
@@ -12,6 +13,7 @@ import (
|
||||
"git.warky.dev/wdevs/whatshooked/pkg/config"
|
||||
"git.warky.dev/wdevs/whatshooked/pkg/handlers"
|
||||
"git.warky.dev/wdevs/whatshooked/pkg/models"
|
||||
"git.warky.dev/wdevs/whatshooked/pkg/serverembed"
|
||||
"git.warky.dev/wdevs/whatshooked/pkg/storage"
|
||||
"github.com/bitechdev/ResolveSpec/pkg/common"
|
||||
"github.com/bitechdev/ResolveSpec/pkg/common/adapters/database"
|
||||
@@ -80,9 +82,12 @@ func NewServer(cfg *config.Config, db *bun.DB, wh WhatsHookedInterface) (*Server
|
||||
// Add custom routes (login, logout, etc.) on main router
|
||||
SetupCustomRoutes(router, secProvider, db)
|
||||
|
||||
// Add static file serving for React app at /ui/ route
|
||||
// Serve React app from configurable filesystem path
|
||||
spa := spaHandler{staticPath: cfg.Server.UIPath, indexPath: "index.html"}
|
||||
// Serve React SPA from the embedded filesystem at /ui/
|
||||
distFS, err := fs.Sub(serverembed.RootEmbedFS, "dist")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to sub embedded dist FS: %w", err)
|
||||
}
|
||||
spa := embeddedSPAHandler{fs: distFS, indexPath: "index.html"}
|
||||
router.PathPrefix("/ui/").Handler(http.StripPrefix("/ui", spa))
|
||||
router.PathPrefix("/ui").Handler(http.StripPrefix("/ui", spa))
|
||||
|
||||
@@ -603,42 +608,46 @@ func extractToken(r *http.Request) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
// spaHandler implements the http.Handler interface for serving a SPA
|
||||
type spaHandler struct {
|
||||
staticPath string
|
||||
indexPath string
|
||||
// embeddedSPAHandler serves a React SPA from an embedded fs.FS.
|
||||
// Static assets are served directly; all other paths fall back to index.html
|
||||
// to support client-side routing.
|
||||
type embeddedSPAHandler struct {
|
||||
fs fs.FS
|
||||
indexPath string
|
||||
}
|
||||
|
||||
// ServeHTTP inspects the URL path to locate a file within the static dir
|
||||
// If a file is found, it is served. If not, the index.html file is served for client-side routing
|
||||
func (h spaHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
// Get the path
|
||||
func (h embeddedSPAHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
path := r.URL.Path
|
||||
// Strip leading slash so fs.FS Open calls work correctly.
|
||||
if len(path) > 0 && path[0] == '/' {
|
||||
path = path[1:]
|
||||
}
|
||||
|
||||
// Check whether a file exists at the given path
|
||||
info, err := http.Dir(h.staticPath).Open(path)
|
||||
if path == "" {
|
||||
path = h.indexPath
|
||||
}
|
||||
|
||||
// Try to open the requested path in the embedded FS.
|
||||
f, err := h.fs.Open(path)
|
||||
if err != nil {
|
||||
// File does not exist, serve index.html for client-side routing
|
||||
http.ServeFile(w, r, h.staticPath+"/"+h.indexPath)
|
||||
// Not found — serve index.html for client-side routing.
|
||||
r2 := r.Clone(r.Context())
|
||||
r2.URL.Path = "/" + h.indexPath
|
||||
http.FileServer(http.FS(h.fs)).ServeHTTP(w, r2)
|
||||
return
|
||||
}
|
||||
defer info.Close()
|
||||
defer f.Close()
|
||||
|
||||
// Check if path is a directory
|
||||
stat, err := info.Stat()
|
||||
if err != nil {
|
||||
http.ServeFile(w, r, h.staticPath+"/"+h.indexPath)
|
||||
stat, err := f.Stat()
|
||||
if err != nil || stat.IsDir() {
|
||||
r2 := r.Clone(r.Context())
|
||||
r2.URL.Path = "/" + h.indexPath
|
||||
http.FileServer(http.FS(h.fs)).ServeHTTP(w, r2)
|
||||
return
|
||||
}
|
||||
|
||||
if stat.IsDir() {
|
||||
// Serve index.html for directories
|
||||
http.ServeFile(w, r, h.staticPath+"/"+h.indexPath)
|
||||
return
|
||||
}
|
||||
|
||||
// Otherwise, serve the file
|
||||
http.FileServer(http.Dir(h.staticPath)).ServeHTTP(w, r)
|
||||
// Serve the real file.
|
||||
http.FileServer(http.FS(h.fs)).ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
// corsMiddleware adds CORS headers to allow frontend requests
|
||||
|
||||
1
pkg/serverembed/dist/assets/index-Bfia8Lvm.css
vendored
Normal file
1
pkg/serverembed/dist/assets/index-Bfia8Lvm.css
vendored
Normal file
File diff suppressed because one or more lines are too long
72
pkg/serverembed/dist/assets/index-D_NQzvuP.js
vendored
Normal file
72
pkg/serverembed/dist/assets/index-D_NQzvuP.js
vendored
Normal file
File diff suppressed because one or more lines are too long
14
pkg/serverembed/dist/index.html
vendored
Normal file
14
pkg/serverembed/dist/index.html
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/ui/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>web</title>
|
||||
<script type="module" crossorigin src="/ui/assets/index-D_NQzvuP.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/ui/assets/index-Bfia8Lvm.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
</html>
|
||||
1
pkg/serverembed/dist/vite.svg
vendored
Normal file
1
pkg/serverembed/dist/vite.svg
vendored
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
@@ -4,5 +4,5 @@ import (
|
||||
"embed"
|
||||
)
|
||||
|
||||
//go:embed dist/** readme
|
||||
//go:embed all:dist readme
|
||||
var RootEmbedFS embed.FS
|
||||
|
||||
@@ -1,21 +1,25 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
import { defineConfig } from "vite";
|
||||
import react from "@vitejs/plugin-react";
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
base: '/ui/',
|
||||
base: "/ui/",
|
||||
build: {
|
||||
outDir: "../pkg/serverembed/dist",
|
||||
emptyOutDir: true,
|
||||
},
|
||||
server: {
|
||||
port: 3000,
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://localhost:8080',
|
||||
"/api": {
|
||||
target: "http://localhost:8080",
|
||||
changeOrigin: true,
|
||||
},
|
||||
'/health': {
|
||||
target: 'http://localhost:8080',
|
||||
"/health": {
|
||||
target: "http://localhost:8080",
|
||||
changeOrigin: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user