feat(http): implement proper BaseUrl support to coexist with legacy mode (#1298)

* refactor: remove baseUrl from api calls and sseBaseUrl

* refactor: set cookie session to '/'.

Since that's where the api endpoint is that way we set it to the root domain, we can't set it to the subfolder since the api is called directly now and not using the baseUrl.

* feat: add the baseUrl route.

When user for example is in `/autobrr` and hit reload it should just return the index.html.

* refactor: now it have to be `/autobrr`

Remove the trailing `/`, now base url is set to /autobrr aligned with other arrs.

* refactor: remove baseUrl stuff.

* refactor: use separate router for the api endpoint and the baseUrl.

I don't think we need separate router, but I didn't test it, so feel free to test it and see if it works without the separate router, the whole point was to make sure that it's not prefixed with baseUrl and I noticed that it was being called in the frontend `APIClients.ts`. So yea just check if it works without it then keep the old one.

Also removed the index since it was zombie code not being used anywhere.

* feat: Dynamic base url.

* fix: auth handler deps

* feat(http): mount web and api on baseurl

* feat(http): web api client routes

* feat(http): baseurl legacy mode

* feat(http): baseurl legacy mode test

* feat(http): add assetBaseUrl

* feat(http): try separate web handlers

* feat(http): improve file serving

* feat(http): ignore .gitkeep

* fix(assets): windows paths

* fix(assets): windows paths trimprefix

* fix(assets): windows paths join

* fix(assets): cleanup

* fix(assets): additional web route check

* feat(http): add comments

---------

Co-authored-by: ze0s <ze0s@riseup.net>
This commit is contained in:
KaiserBh 2024-12-19 22:56:04 +11:00 committed by GitHub
parent c1d8a4a850
commit 4432dfb099
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 598 additions and 147 deletions

View file

@ -20,6 +20,14 @@ port = 7474
#
#baseUrl = "/autobrr/"
# Base url mode legacy
# This is kept for compatibility with older versions doing url rewrite on the proxy.
# If you use baseUrl you can set this to false and skip any url rewrite in your proxy.
#
# Default: true
#
baseUrlModeLegacy = true
# autobrr logs file
# If not defined, logs to stdout
#

View file

@ -46,6 +46,14 @@ port = 7474
#
#baseUrl = "/autobrr/"
# Base url mode legacy
# This is kept for compatibility with older versions doing url rewrite on the proxy.
# If you use baseUrl you can set this to false and skip any url rewrite in your proxy.
#
# Default: true
#
baseUrlModeLegacy = true
# autobrr logs file
# If not defined, logs to stdout
# Make sure to use forward slashes and include the filename with extension. eg: "log/autobrr.log", "C:/autobrr/log/autobrr.log"
@ -224,6 +232,7 @@ func (c *AppConfig) defaults() {
LogMaxBackups: 3,
DatabaseMaxBackups: 5,
BaseURL: "/",
BaseURLModeLegacy: true,
SessionSecret: api.GenerateSecureToken(16),
CustomDefinitions: "",
CheckForUpdates: true,
@ -260,6 +269,10 @@ func (c *AppConfig) loadFromEnv() {
c.Config.BaseURL = v
}
if v := os.Getenv(prefix + "BASE_URL_MODE_LEGACY"); v != "" {
c.Config.BaseURLModeLegacy = strings.EqualFold(strings.ToLower(v), "true")
}
if v := os.Getenv(prefix + "LOG_LEVEL"); v != "" {
c.Config.LogLevel = v
}

View file

@ -13,6 +13,7 @@ type Config struct {
LogMaxSize int `toml:"logMaxSize"`
LogMaxBackups int `toml:"logMaxBackups"`
BaseURL string `toml:"baseUrl"`
BaseURLModeLegacy bool `toml:"baseUrlModeLegacy"`
SessionSecret string `toml:"sessionSecret"`
CustomDefinitions string `toml:"customDefinitions"`
CheckForUpdates bool `toml:"checkForUpdates"`

View file

@ -127,10 +127,11 @@ func (s Server) Handler() http.Handler {
encoder := newEncoder(s.log)
r.Route("/api", func(r chi.Router) {
r.Route("/auth", newAuthHandler(encoder, s.log, s, s.config.Config, s.cookieStore, s.authService).Routes)
r.Route("/healthz", newHealthHandler(encoder, s.db).Routes)
// Create a separate router for API
apiRouter := chi.NewRouter()
apiRouter.Route("/auth", newAuthHandler(encoder, s.log, s, s.config.Config, s.cookieStore, s.authService).Routes)
apiRouter.Route("/healthz", newHealthHandler(encoder, s.db).Routes)
apiRouter.Group(func(r chi.Router) {
r.Group(func(r chi.Router) {
r.Use(s.IsAuthenticated)
@ -163,17 +164,46 @@ func (s Server) Handler() http.Handler {
})
})
// serve the web
web.RegisterHandler(r, s.version, s.config.Config.BaseURL)
routeBaseURL := "/"
webRouter := chi.NewRouter()
// handle backwards compatibility for base url routing
if s.config.Config.BaseURLModeLegacy {
// this is required to keep assets "url rewritable" via a reverse-proxy
routeAssetBaseURL := "./"
// serve the web
webHandlers := newWebLegacyHandler(s.log, web.DistDirFS, s.version, s.config.Config.BaseURL, routeAssetBaseURL)
webHandlers.RegisterRoutes(webRouter)
} else {
routeBaseURL = s.config.Config.BaseURL
// serve the web
webHandlers := newWebHandler(s.log, web.DistDirFS, s.version, routeBaseURL, routeBaseURL)
webHandlers.RegisterRoutes(webRouter)
// add fallback routes when base url is set to inform user to redirect and use /baseurl/
r.Get("/", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNotFound)
if err := webHandlers.RenderFallbackIndex(w); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
})
r.Get("/*", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNotFound)
if err := webHandlers.RenderFallbackIndex(w); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
})
}
// Mount the web router under baseUrl + '/'
r.Mount(routeBaseURL, webRouter)
// Mount the API router under baseUrl + '/api'
r.Mount(routeBaseURL+"api", apiRouter)
return r
}
func (s Server) index(w http.ResponseWriter, r *http.Request) {
p := web.IndexParams{
Title: "Dashboard",
Version: s.version,
BaseUrl: s.config.Config.BaseURL,
}
web.Index(w, p)
}

265
internal/http/web.go Normal file
View file

@ -0,0 +1,265 @@
// Copyright (c) 2021 - 2024, Ludvig Lundgren and the autobrr contributors.
// SPDX-License-Identifier: GPL-2.0-or-later
package http
import (
"bufio"
"bytes"
"html/template"
"io"
"io/fs"
"net/http"
"os"
filePath "path"
"strings"
"github.com/go-chi/chi/v5"
"github.com/rs/zerolog"
)
type webHandler struct {
log zerolog.Logger
embedFS fs.FS
baseUrl string
assetBaseURL string
version string
files map[string]string
}
func newWebHandler(log zerolog.Logger, embedFS fs.FS, version, baseURL, assetBaseURL string) *webHandler {
return &webHandler{
log: log.With().Str("module", "web-assets").Logger(),
embedFS: embedFS,
baseUrl: baseURL,
assetBaseURL: assetBaseURL,
version: version,
files: make(map[string]string),
}
}
// registerAssets walks the FS Dist dir and registers each file as a route
func (h *webHandler) registerAssets(r *chi.Mux) {
err := fs.WalkDir(h.embedFS, ".", func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if d.IsDir() {
//h.log.Trace().Msgf("web assets: skip dir: %s", d.Name())
return nil
}
h.log.Trace().Msgf("web assets: found path: %s", path)
// ignore index.html, so we can render it as a template and inject variables
if path == "index.html" || path == "manifest.webmanifest" || path == ".gitkeep" {
return nil
}
// use old path.Join to not be os specific
FileFS(r, filePath.Join("/", path), path, h.embedFS)
h.files[path] = path
return nil
})
if err != nil {
return
}
}
func (h *webHandler) RegisterRoutes(r *chi.Mux) {
h.registerAssets(r)
// Serve static files without a prefix
assets, err := fs.Sub(h.embedFS, "assets")
if err != nil {
h.log.Error().Err(err).Msg("could not load assets sub dir")
}
StaticFSNew(r, h.baseUrl, "/assets", assets)
p := IndexParams{
Title: "Dashboard",
Version: h.version,
BaseUrl: h.baseUrl,
AssetBaseUrl: h.assetBaseURL,
}
// serve on base route
r.Get("/", func(w http.ResponseWriter, r *http.Request) {
if err := h.RenderIndex(w, p); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
})
// handle manifest
r.Get("/manifest.webmanifest", func(w http.ResponseWriter, r *http.Request) {
if err := h.RenderManifest(w, p); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
})
// handle all other routes
r.Get("/*", func(w http.ResponseWriter, r *http.Request) {
file := strings.TrimPrefix(r.RequestURI, h.baseUrl)
if strings.Contains(file, "favicon.ico") {
fsFile(w, r, "favicon.ico", h.embedFS)
return
}
if strings.Contains(file, "Inter-Variable.woff2") {
fsFile(w, r, "Inter-Variable.woff2", h.embedFS)
return
}
// if valid web route then serve html
if validWebRoute(file) || file == "index.html" {
if err := h.RenderIndex(w, p); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
return
}
// if not valid web route then try and serve files
fsFile(w, r, file, h.embedFS)
})
}
func (h *webHandler) RenderIndex(w io.Writer, p IndexParams) error {
return h.parseIndex().Execute(w, p)
}
func (h *webHandler) parseIndex() *template.Template {
return template.Must(template.New("index.html").ParseFS(h.embedFS, "index.html"))
}
func (h *webHandler) RenderManifest(w io.Writer, p IndexParams) error {
return h.parseManifest().Execute(w, p)
}
func (h *webHandler) parseManifest() *template.Template {
return template.Must(template.New("manifest.webmanifest").ParseFS(h.embedFS, "manifest.webmanifest"))
}
func (h *webHandler) RenderFallbackIndex(w io.Writer) error {
p := IndexParams{
Title: "autobrr Dashboard",
Version: h.version,
BaseUrl: h.baseUrl,
AssetBaseUrl: h.assetBaseURL,
}
return h.parseFallbackIndex().Execute(w, p)
}
func (h *webHandler) parseFallbackIndex() *template.Template {
return template.Must(template.New("fallback-index").Parse(`<!DOCTYPE html>
<html>
<head>
<title>autobrr</title>
<style>
@media (prefers-color-scheme: dark) {
body {
color:#fff;
background:#333333
}
a {
color: #3d70ea
}
}
</style>
</head>
<body>
<span>Must use base url: <a href="{{.BaseUrl}}">{{.BaseUrl}}</a></span>
</body>
</html>
`))
}
type defaultFS struct {
prefix string
fs fs.FS
}
type IndexParams struct {
Title string
Version string
BaseUrl string
AssetBaseUrl string
}
func (fs defaultFS) Open(name string) (fs.File, error) {
if fs.fs == nil {
return os.Open(name)
}
return fs.fs.Open(name)
}
// FileFS registers a new route with path to serve a file from the provided file system.
func FileFS(r *chi.Mux, path, file string, filesystem fs.FS) {
r.Get(path, StaticFileHandler(file, filesystem))
}
// StaticFileHandler creates a handler function to serve a file from the provided file system.
func StaticFileHandler(file string, filesystem fs.FS) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
fsFile(w, r, file, filesystem)
}
}
//// StaticFS registers a new route with path prefix to serve static files from the provided file system.
//func StaticFS(r *chi.Mux, pathPrefix string, filesystem fs.FS) {
// r.Handle(pathPrefix+"*", http.StripPrefix(pathPrefix, http.FileServer(http.FS(filesystem))))
//}
// StaticFSNew registers a new route with path prefix to serve static files from the provided file system.
func StaticFSNew(r *chi.Mux, baseUrl, pathPrefix string, filesystem fs.FS) {
r.Handle(pathPrefix+"*", http.StripPrefix(filePath.Join(baseUrl, pathPrefix), http.FileServer(http.FS(filesystem))))
}
// fsFile is a helper function to serve a file from the provided file system.
func fsFile(w http.ResponseWriter, r *http.Request, file string, filesystem fs.FS) {
f, err := filesystem.Open(file)
if err != nil {
http.Error(w, "File not found", http.StatusNotFound)
return
}
defer f.Close()
stat, err := f.Stat()
if err != nil {
http.Error(w, "File not found", http.StatusNotFound)
return
}
data, err := io.ReadAll(bufio.NewReader(f))
if err != nil {
http.Error(w, "Failed to read the file", http.StatusInternalServerError)
return
}
reader := bytes.NewReader(data)
http.ServeContent(w, r, file, stat.ModTime(), reader)
}
var validWebRoutes = []string{"filters", "releases", "settings", "logs", "onboard", "login", "logout"}
func validWebRoute(route string) bool {
if route == "" || route == "/" {
return true
}
for _, valid := range validWebRoutes {
if strings.HasPrefix(route, valid) || strings.HasPrefix(route, "/"+valid) {
return true
}
}
return false
}

201
internal/http/web_legacy.go Normal file
View file

@ -0,0 +1,201 @@
// Copyright (c) 2021 - 2024, Ludvig Lundgren and the autobrr contributors.
// SPDX-License-Identifier: GPL-2.0-or-later
package http
import (
"html/template"
"io"
"io/fs"
"net/http"
filePath "path"
"strings"
"github.com/go-chi/chi/v5"
"github.com/rs/zerolog"
)
type webLegacyHandler struct {
log zerolog.Logger
embedFS fs.FS
baseUrl string
assetBaseURL string
version string
files map[string]string
}
func newWebLegacyHandler(log zerolog.Logger, embedFS fs.FS, version, baseURL, assetBaseURL string) *webLegacyHandler {
return &webLegacyHandler{
log: log.With().Str("module", "web-assets").Logger(),
embedFS: embedFS,
baseUrl: baseURL,
assetBaseURL: assetBaseURL,
version: version,
files: make(map[string]string),
}
}
// registerAssets walks the FS Dist dir and registers each file as a route
func (h *webLegacyHandler) registerAssets(r *chi.Mux) {
err := fs.WalkDir(h.embedFS, ".", func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if d.IsDir() {
//h.log.Trace().Msgf("web assets: skip dir: %s", d.Name())
return nil
}
h.log.Trace().Msgf("web assets: found path: %s", path)
// ignore index.html, so we can render it as a template and inject variables
if path == "index.html" || path == "manifest.webmanifest" || path == ".gitkeep" {
return nil
}
// use old path.Join to not be os specific
FileFS(r, filePath.Join("/", path), path, h.embedFS)
h.files[path] = path
return nil
})
if err != nil {
return
}
}
func (h *webLegacyHandler) RegisterRoutes(r *chi.Mux) {
h.registerAssets(r)
// Serve static files without a prefix
assets, err := fs.Sub(h.embedFS, "assets")
if err != nil {
h.log.Error().Err(err).Msg("could not load assets sub dir")
}
StaticFS(r, "/assets", assets)
p := IndexParams{
Title: "Dashboard",
Version: h.version,
BaseUrl: h.baseUrl,
AssetBaseUrl: h.assetBaseURL,
}
// serve on base route
r.Get("/", func(w http.ResponseWriter, r *http.Request) {
if err := h.RenderIndex(w, p); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
})
// handle manifest
r.Get("/manifest.webmanifest", func(w http.ResponseWriter, r *http.Request) {
if err := h.RenderManifest(w, p); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
})
// handle all other routes and files
r.Get("/*", func(w http.ResponseWriter, r *http.Request) {
file := strings.TrimPrefix(r.RequestURI, h.baseUrl)
if strings.Contains(file, "/assets") {
if strings.HasPrefix(file, "/assets/") {
fsFile(w, r, file, h.embedFS)
return
}
parts := strings.SplitAfter(file, "/assets/")
if len(parts) > 1 {
fsFile(w, r, "assets/"+parts[1], h.embedFS)
return
}
return
}
if strings.Contains(file, "favicon.ico") {
fsFile(w, r, "favicon.ico", h.embedFS)
return
}
if strings.Contains(file, "Inter-Variable.woff2") {
fsFile(w, r, "Inter-Variable.woff2", h.embedFS)
return
}
// if valid web route then serve html
if validWebRoute(file) || file == "index.html" {
if err := h.RenderIndex(w, p); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
return
}
// if not valid web route then try and serve files
fsFile(w, r, file, h.embedFS)
})
}
func (h *webLegacyHandler) RenderIndex(w io.Writer, p IndexParams) error {
return h.parseIndex().Execute(w, p)
}
func (h *webLegacyHandler) parseIndex() *template.Template {
return template.Must(template.New("index.html").ParseFS(h.embedFS, "index.html"))
}
func (h *webLegacyHandler) RenderManifest(w io.Writer, p IndexParams) error {
return h.parseManifest().Execute(w, p)
}
func (h *webLegacyHandler) parseManifest() *template.Template {
return template.Must(template.New("manifest.webmanifest").ParseFS(h.embedFS, "manifest.webmanifest"))
}
func (h *webLegacyHandler) RenderFallbackIndex(w io.Writer) error {
p := IndexParams{
Title: "autobrr Dashboard",
Version: h.version,
BaseUrl: h.baseUrl,
AssetBaseUrl: h.assetBaseURL,
}
return h.parseFallbackIndex().Execute(w, p)
}
func (h *webLegacyHandler) parseFallbackIndex() *template.Template {
return template.Must(template.New("fallback-index").Parse(`<!DOCTYPE html>
<html>
<head>
<title>autobrr</title>
<style>
@media (prefers-color-scheme: dark) {
body {
color:#fff;
background:#333333
}
a {
color: #3d70ea
}
}
</style>
</head>
<body>
<span>Must use base url: <a href="{{.BaseUrl}}">{{.BaseUrl}}</a></span>
</body>
</html>
`))
}
// StaticFS registers a new route with path prefix to serve static files from the provided file system.
func StaticFS(r *chi.Mux, pathPrefix string, filesystem fs.FS) {
r.Handle(pathPrefix+"*", http.StripPrefix(pathPrefix, http.FileServer(http.FS(filesystem))))
}

View file

@ -5,32 +5,13 @@
package web
import (
"bufio"
"bytes"
"embed"
"fmt"
"html/template"
"io"
"io/fs"
"net/http"
"os"
"path/filepath"
"strings"
"github.com/go-chi/chi/v5"
)
type defaultFS struct {
prefix string
fs fs.FS
}
type IndexParams struct {
Title string
Version string
BaseUrl string
}
var (
//go:embed all:dist
Dist embed.FS
@ -38,6 +19,11 @@ var (
DistDirFS = MustSubFS(Dist, "dist")
)
type defaultFS struct {
prefix string
fs fs.FS
}
func (fs defaultFS) Open(name string) (fs.File, error) {
if fs.fs == nil {
return os.Open(name)
@ -74,113 +60,3 @@ func subFS(currentFs fs.FS, root string) (fs.FS, error) {
}
return fs.Sub(currentFs, root)
}
// FileFS registers a new route with path to serve a file from the provided file system.
func FileFS(r *chi.Mux, path, file string, filesystem fs.FS) {
r.Get(path, StaticFileHandler(file, filesystem))
}
// StaticFileHandler creates a handler function to serve a file from the provided file system.
func StaticFileHandler(file string, filesystem fs.FS) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
fsFile(w, r, file, filesystem)
}
}
// StaticFS registers a new route with path prefix to serve static files from the provided file system.
func StaticFS(r *chi.Mux, pathPrefix string, filesystem fs.FS) {
r.Handle(pathPrefix+"*", http.StripPrefix(pathPrefix, http.FileServer(http.FS(filesystem))))
}
// fsFile is a helper function to serve a file from the provided file system.
func fsFile(w http.ResponseWriter, r *http.Request, file string, filesystem fs.FS) {
//fmt.Printf("file: %s\n", file)
f, err := filesystem.Open(file)
if err != nil {
http.Error(w, "File not found", http.StatusNotFound)
return
}
defer f.Close()
stat, err := f.Stat()
if err != nil {
http.Error(w, "File not found", http.StatusNotFound)
return
}
data, err := io.ReadAll(bufio.NewReader(f))
if err != nil {
http.Error(w, "Failed to read the file", http.StatusInternalServerError)
return
}
reader := bytes.NewReader(data)
http.ServeContent(w, r, file, stat.ModTime(), reader)
}
var validRoutes = []string{"/", "filters", "releases", "settings", "logs", "onboard", "login", "logout"}
func validRoute(route string) bool {
for _, valid := range validRoutes {
if strings.Contains(route, valid) {
return true
}
}
return false
}
// RegisterHandler register web routes and file serving
func RegisterHandler(c *chi.Mux, version, baseUrl string) {
// Serve static files without a prefix
assets, _ := fs.Sub(DistDirFS, "assets")
static, _ := fs.Sub(DistDirFS, "static")
StaticFS(c, "/assets", assets)
StaticFS(c, "/static", static)
p := IndexParams{
Title: "Dashboard",
Version: version,
BaseUrl: baseUrl,
}
// serve on base route
c.Get("/", func(w http.ResponseWriter, r *http.Request) {
Index(w, p)
})
// handle all other routes
c.Get("/*", func(w http.ResponseWriter, r *http.Request) {
file := strings.TrimPrefix(r.RequestURI, "/")
// if valid web route then serve html
if validRoute(file) || file == "index.html" {
Index(w, p)
return
}
if strings.Contains(file, "manifest.webmanifest") {
Manifest(w, p)
return
}
// if not valid web route then try and serve files
fsFile(w, r, file, DistDirFS)
})
}
func Index(w io.Writer, p IndexParams) error {
return parseIndex().Execute(w, p)
}
func parseIndex() *template.Template {
return template.Must(template.New("index.html").ParseFS(Dist, "dist/index.html"))
}
func Manifest(w io.Writer, p IndexParams) error {
return parseManifest().Execute(w, p)
}
func parseManifest() *template.Template {
return template.Must(template.New("manifest.webmanifest").ParseFS(Dist, "dist/manifest.webmanifest"))
}

View file

@ -18,7 +18,7 @@
<link rel="apple-touch-icon" sizes="114x114" href="/apple-touch-icon-iphone-retina-120x120.png"/>
<link rel="apple-touch-icon" sizes="144x144" href="/apple-touch-icon-ipad-retina-152x152.png"/>
<title>autobrr</title>
<base href="{{.BaseUrl}}">
<!-- <base href="{{.BaseUrl}}"> -->
<style>
@font-face {
font-family: "Inter Var";
@ -45,6 +45,11 @@
window.APP = {};
window.APP.baseUrl = "{{.BaseUrl}}";
// Update the base tag dynamically
const base = document.createElement('base');
base.href = window.APP.baseUrl;
document.head.appendChild(base);
const browserPrefers = !(window.matchMedia !== undefined && window.matchMedia("(prefers-color-scheme: light)").matches);
const {darkTheme = browserPrefers} = JSON.parse(localStorage.getItem("settings")) || {};
document.documentElement.classList.toggle("dark", darkTheme);

View file

@ -152,6 +152,7 @@ export async function HttpClient<T = unknown>(
}
const response = await window.fetch(`${baseUrl()}${endpoint}`, init);
const isJson = response.headers.get("Content-Type")?.includes("application/json");
if (response.status >= 200 && response.status < 300) {

View file

@ -3,6 +3,8 @@ import { defineConfig, loadEnv, ConfigEnv } from "vite";
import { VitePWA } from "vite-plugin-pwa";
import react from "@vitejs/plugin-react-swc";
import svgr from "vite-plugin-svgr";
import path from "node:path";
import fs from "node:fs";
interface PreRenderedAsset {
name: string | undefined;
@ -17,6 +19,7 @@ export default ({ mode }: ConfigEnv) => {
process.env = { ...process.env, ...loadEnv(mode, process.cwd()) };
return defineConfig({
// __BASE_URL__: "{{.BaseUrl}}",
base: "",
plugins: [react(), svgr(), VitePWA({
injectRegister: null,
@ -72,7 +75,36 @@ export default ({ mode }: ConfigEnv) => {
sourcemap: true,
navigateFallbackDenylist: [/^\/api/]
}
})],
}),
{
name: "html-transformer-plugin",
enforce: "post",
apply: "build",
async closeBundle() {
const outputDir = 'dist'; // Adjust this if your `build.outDir` is different
const htmlPath = path.resolve(outputDir, 'index.html');
// Check if the file exists
if (!fs.existsSync(htmlPath)) {
console.error(`Could not find ${htmlPath}. Make sure the output directory matches.`);
return;
}
// Read the `index.html` content
let html = fs.readFileSync(htmlPath, 'utf-8');
// Perform your transformations here
// the experimental renderBuiltUrl works except for the style font url where it escapes the curly braces
// we look for those and replace with the non escaped curly braces to be able to correctly replace baseurl.
html = html.replace('%7B%7B.AssetBaseUrl%7D%7D/', '{{.AssetBaseUrl}}'); // Example: Replace `{{.BaseUrl}}`
// Write the updated `index.html` back
fs.writeFileSync(htmlPath, html);
console.log('Transformed index.html successfully.');
},
},
],
resolve: {
alias: [
{ find: "@", replacement: fileURLToPath(new URL("./src/", import.meta.url)) },
@ -97,7 +129,7 @@ export default ({ mode }: ConfigEnv) => {
target: "http://127.0.0.1:7474/",
changeOrigin: true,
secure: false
}
},
}
},
build: {
@ -112,6 +144,25 @@ export default ({ mode }: ConfigEnv) => {
}
},
}
},
experimental: {
renderBuiltUrl(filename: string, { hostId, hostType, type }: {
hostId: string,
hostType: 'js' | 'css' | 'html',
type: 'public' | 'asset'
}) {
// console.debug(filename, hostId, hostType, type)
return '{{.AssetBaseUrl}}' + filename
// if (type === 'public') {
// return 'https://www.domain.com/' + filename
// }
// else if (path.extname(hostId) === '.js') {
// return { runtime: `window.__assetsPath(${JSON.stringify(filename)})` }
// }
// else {
// return 'https://cdn.domain.com/assets/' + filename
// }
}
}
});
};