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
|
.PHONY: build clean test lint lintfix run-server run-cli help build-ui migrate generate-models install-relspecgo seed
|
||||||
|
|
||||||
# Variables
|
# Variables
|
||||||
FRONTEND_DIR=frontend
|
FRONTEND_DIR=web
|
||||||
SQL_DIR=sql
|
SQL_DIR=sql
|
||||||
MODELS_DIR=pkg/models
|
MODELS_DIR=pkg/models
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io/fs"
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -12,6 +13,7 @@ import (
|
|||||||
"git.warky.dev/wdevs/whatshooked/pkg/config"
|
"git.warky.dev/wdevs/whatshooked/pkg/config"
|
||||||
"git.warky.dev/wdevs/whatshooked/pkg/handlers"
|
"git.warky.dev/wdevs/whatshooked/pkg/handlers"
|
||||||
"git.warky.dev/wdevs/whatshooked/pkg/models"
|
"git.warky.dev/wdevs/whatshooked/pkg/models"
|
||||||
|
"git.warky.dev/wdevs/whatshooked/pkg/serverembed"
|
||||||
"git.warky.dev/wdevs/whatshooked/pkg/storage"
|
"git.warky.dev/wdevs/whatshooked/pkg/storage"
|
||||||
"github.com/bitechdev/ResolveSpec/pkg/common"
|
"github.com/bitechdev/ResolveSpec/pkg/common"
|
||||||
"github.com/bitechdev/ResolveSpec/pkg/common/adapters/database"
|
"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
|
// Add custom routes (login, logout, etc.) on main router
|
||||||
SetupCustomRoutes(router, secProvider, db)
|
SetupCustomRoutes(router, secProvider, db)
|
||||||
|
|
||||||
// Add static file serving for React app at /ui/ route
|
// Serve React SPA from the embedded filesystem at /ui/
|
||||||
// Serve React app from configurable filesystem path
|
distFS, err := fs.Sub(serverembed.RootEmbedFS, "dist")
|
||||||
spa := spaHandler{staticPath: cfg.Server.UIPath, indexPath: "index.html"}
|
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))
|
||||||
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 ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
// spaHandler implements the http.Handler interface for serving a SPA
|
// embeddedSPAHandler serves a React SPA from an embedded fs.FS.
|
||||||
type spaHandler struct {
|
// Static assets are served directly; all other paths fall back to index.html
|
||||||
staticPath string
|
// to support client-side routing.
|
||||||
indexPath string
|
type embeddedSPAHandler struct {
|
||||||
|
fs fs.FS
|
||||||
|
indexPath string
|
||||||
}
|
}
|
||||||
|
|
||||||
// ServeHTTP inspects the URL path to locate a file within the static dir
|
func (h embeddedSPAHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
// 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
|
|
||||||
path := r.URL.Path
|
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
|
if path == "" {
|
||||||
info, err := http.Dir(h.staticPath).Open(path)
|
path = h.indexPath
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to open the requested path in the embedded FS.
|
||||||
|
f, err := h.fs.Open(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// File does not exist, serve index.html for client-side routing
|
// Not found — serve index.html for client-side routing.
|
||||||
http.ServeFile(w, r, h.staticPath+"/"+h.indexPath)
|
r2 := r.Clone(r.Context())
|
||||||
|
r2.URL.Path = "/" + h.indexPath
|
||||||
|
http.FileServer(http.FS(h.fs)).ServeHTTP(w, r2)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer info.Close()
|
defer f.Close()
|
||||||
|
|
||||||
// Check if path is a directory
|
stat, err := f.Stat()
|
||||||
stat, err := info.Stat()
|
if err != nil || stat.IsDir() {
|
||||||
if err != nil {
|
r2 := r.Clone(r.Context())
|
||||||
http.ServeFile(w, r, h.staticPath+"/"+h.indexPath)
|
r2.URL.Path = "/" + h.indexPath
|
||||||
|
http.FileServer(http.FS(h.fs)).ServeHTTP(w, r2)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if stat.IsDir() {
|
// Serve the real file.
|
||||||
// Serve index.html for directories
|
http.FileServer(http.FS(h.fs)).ServeHTTP(w, r)
|
||||||
http.ServeFile(w, r, h.staticPath+"/"+h.indexPath)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Otherwise, serve the file
|
|
||||||
http.FileServer(http.Dir(h.staticPath)).ServeHTTP(w, r)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// corsMiddleware adds CORS headers to allow frontend requests
|
// 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"
|
"embed"
|
||||||
)
|
)
|
||||||
|
|
||||||
//go:embed dist/** readme
|
//go:embed all:dist readme
|
||||||
var RootEmbedFS embed.FS
|
var RootEmbedFS embed.FS
|
||||||
|
|||||||
@@ -1,21 +1,25 @@
|
|||||||
import { defineConfig } from 'vite'
|
import { defineConfig } from "vite";
|
||||||
import react from '@vitejs/plugin-react'
|
import react from "@vitejs/plugin-react";
|
||||||
|
|
||||||
// https://vite.dev/config/
|
// https://vite.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [react()],
|
plugins: [react()],
|
||||||
base: '/ui/',
|
base: "/ui/",
|
||||||
|
build: {
|
||||||
|
outDir: "../pkg/serverembed/dist",
|
||||||
|
emptyOutDir: true,
|
||||||
|
},
|
||||||
server: {
|
server: {
|
||||||
port: 3000,
|
port: 3000,
|
||||||
proxy: {
|
proxy: {
|
||||||
'/api': {
|
"/api": {
|
||||||
target: 'http://localhost:8080',
|
target: "http://localhost:8080",
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
},
|
},
|
||||||
'/health': {
|
"/health": {
|
||||||
target: 'http://localhost:8080',
|
target: "http://localhost:8080",
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
})
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user