From 1a9f36ce9efbea3993d328bf704b5bc7d658cb42 Mon Sep 17 00:00:00 2001 From: jeffvli Date: Thu, 12 Feb 2026 11:18:24 -0800 Subject: [PATCH] fix vite web build to work with subpath --- Dockerfile | 15 ++- README.md | 3 +- ng.conf.template | 3 +- web.vite.config.ts | 238 +++++++++++++++++++++++---------------------- 4 files changed, 139 insertions(+), 120 deletions(-) diff --git a/Dockerfile b/Dockerfile index bd1b86940..41281dcd7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,6 +2,11 @@ FROM node:23-alpine as builder WORKDIR /app +# PUBLIC_PATH at build time: passed as VITE_BASE_PATH so assets are built for that subpath. +# Use "/" for root or "/feishin/" for a subpath (trailing slash required for subpaths). +ARG PUBLIC_PATH=/ +ENV PUBLIC_PATH=${PUBLIC_PATH} + # Copy package.json first to cache node_modules COPY package.json pnpm-lock.yaml . @@ -10,18 +15,24 @@ RUN npm install -g pnpm RUN pnpm install # Copy code and build with cached modules +# Normalize PUBLIC_PATH to trailing slash for subpaths (Vite base expects "/" or "/foo/") COPY . . -RUN pnpm run build:web +RUN VITE_BASE_PATH=$(echo "$PUBLIC_PATH" | sed 's|/*$|/|') && \ + export VITE_BASE_PATH && \ + pnpm run build:web # --- Production stage FROM nginx:alpine-slim +ARG PUBLIC_PATH=/ +ENV PUBLIC_PATH=${PUBLIC_PATH} + COPY --chown=nginx:nginx --from=builder /app/out/web /usr/share/nginx/html COPY ./settings.js.template /etc/nginx/templates/settings.js.template COPY ng.conf.template /etc/nginx/templates/default.conf.template ENV SERVER_LOCK=false SERVER_NAME="" SERVER_TYPE="" SERVER_URL="" -ENV LEGACY_AUTHENTICATION="" ANALYTICS_DISABLED="" PUBLIC_PATH="/" +ENV LEGACY_AUTHENTICATION="" ANALYTICS_DISABLED="" EXPOSE 9180 CMD ["nginx", "-g", "daemon off;"] diff --git a/README.md b/README.md index 37637f35d..5a411ef69 100644 --- a/README.md +++ b/README.md @@ -116,6 +116,7 @@ services: - SERVER_URL= # http://address:port or https://address:port - LEGACY_AUTHENTICATION=false # When SERVER_LOCK is true, sets the legacy (plaintext) authentication flag for Subsonic/OpenSubsonic servers - ANALYTICS_DISABLED=true # Set to true to disable Umami analytics tracking + - PUBLIC_PATH=/feishin/ # Optional: if you want to host Feishin on a subpath (not `/`) ports: - 9180:9180 # Alternatively, to restrict to only localhost, - 127.0.0.1:9180:8190 @@ -130,7 +131,7 @@ services: - **Navidrome** - For the best experience, select "Save password" when creating the server and configure the `SessionTimeout` setting in your Navidrome config to a larger value (e.g. 72h). - **Linux users** - The default password store uses `libsecret`. `kwallet4/5/6` are also supported, but must be explicitly set in Settings > Window > Passwords/secret store. -3. _Optional_ - If you want to host Feishin on a subpath (not `/`), then pass in the following environment variable: `PUBLIC_PATH=PATH`. For example, to host on `/feishin`, pass in `PUBLIC_PATH=/feishin`. +3. _Optional_ - If you want to host Feishin on a subpath (not `/`), pass `PUBLIC_PATH` at **build time** (and the same value at run time for nginx). Use a trailing slash for subpaths, e.g. `PUBLIC_PATH=/feishin/`. Example: `docker build --build-arg PUBLIC_PATH=/feishin/ .` then run with `-e PUBLIC_PATH=/feishin/`. 4. _Optional_ - To hard code the server url, pass the following environment variables: `SERVER_NAME`, `SERVER_TYPE` (one of `jellyfin` or `navidrome` or `subsonic`), `SERVER_URL`. To prevent users from changing these settings, pass `SERVER_LOCK=true`. This can only be set if all three of the previous values are set. When `SERVER_LOCK=true`, you can also set `LEGACY_AUTHENTICATION=true` or `LEGACY_AUTHENTICATION=false` to configure the legacy authentication flag for the server (only applicable for Subsonic/OpenSubsonic servers). diff --git a/ng.conf.template b/ng.conf.template index 1e6e0e635..65a13ca99 100644 --- a/ng.conf.template +++ b/ng.conf.template @@ -12,9 +12,10 @@ server { gzip_types text/plain text/css application/json application/javascript application/x-javascript text/xml application/xml application/xml+rss text/javascript; gzip_comp_level 9; + # PUBLIC_PATH must be "/" or a subpath with trailing slash (e.g. "/feishin/") location ${PUBLIC_PATH} { alias /usr/share/nginx/html/; - try_files $uri $uri/ /index.html =404; + try_files $uri $uri/ /index.html; } location ${PUBLIC_PATH}settings.js { diff --git a/web.vite.config.ts b/web.vite.config.ts index 5a11d0a5c..d0a6d7f19 100644 --- a/web.vite.config.ts +++ b/web.vite.config.ts @@ -4,126 +4,132 @@ import { defineConfig, normalizePath } from 'vite'; import { ViteEjsPlugin } from 'vite-plugin-ejs'; import { VitePWA } from 'vite-plugin-pwa'; -export default defineConfig({ - base: '/', - build: { - emptyOutDir: true, - outDir: path.resolve(__dirname, './out/web'), - rollupOptions: { - input: { - '32x32': normalizePath(path.resolve(__dirname, './assets/icons/32x32.png')), - '64x64': normalizePath(path.resolve(__dirname, './assets/icons/64x64.png')), - '128x128': normalizePath(path.resolve(__dirname, './assets/icons/128x128.png')), - '256x256': normalizePath(path.resolve(__dirname, './assets/icons/256x256.png')), - '512x512': normalizePath(path.resolve(__dirname, './assets/icons/512x512.png')), - '1024x1024': normalizePath(path.resolve(__dirname, './assets/icons/1024x1024.png')), - favicon: normalizePath(path.resolve(__dirname, './assets/icons/favicon.ico')), - index: normalizePath(path.resolve(__dirname, './src/renderer/index.html')), - preview_full_screen_player: normalizePath( - path.resolve(__dirname, './media/preview_full_screen_player.webp'), - ), +// eslint-disable-next-line @typescript-eslint/no-unused-vars +export default defineConfig(({ mode }) => { + const base = process.env.VITE_BASE_PATH || '/'; + + return { + base, + build: { + emptyOutDir: true, + outDir: path.resolve(__dirname, './out/web'), + rollupOptions: { + input: { + '32x32': normalizePath(path.resolve(__dirname, './assets/icons/32x32.png')), + '64x64': normalizePath(path.resolve(__dirname, './assets/icons/64x64.png')), + '128x128': normalizePath(path.resolve(__dirname, './assets/icons/128x128.png')), + '256x256': normalizePath(path.resolve(__dirname, './assets/icons/256x256.png')), + '512x512': normalizePath(path.resolve(__dirname, './assets/icons/512x512.png')), + '1024x1024': normalizePath( + path.resolve(__dirname, './assets/icons/1024x1024.png'), + ), + favicon: normalizePath(path.resolve(__dirname, './assets/icons/favicon.ico')), + index: normalizePath(path.resolve(__dirname, './src/renderer/index.html')), + preview_full_screen_player: normalizePath( + path.resolve(__dirname, './media/preview_full_screen_player.webp'), + ), + }, + }, + sourcemap: true, + }, + css: { + modules: { + generateScopedName: 'fs-[name]-[local]', + localsConvention: 'camelCase', }, }, - sourcemap: true, - }, - css: { - modules: { - generateScopedName: 'fs-[name]-[local]', - localsConvention: 'camelCase', + optimizeDeps: { + exclude: [ + '@atlaskit/pragmatic-drag-and-drop', + '@atlaskit/pragmatic-drag-and-drop-auto-scroll', + '@atlaskit/pragmatic-drag-and-drop-hitbox', + '@tanstack/react-query-persist-client', + 'idb-keyval', + ], }, - }, - optimizeDeps: { - exclude: [ - '@atlaskit/pragmatic-drag-and-drop', - '@atlaskit/pragmatic-drag-and-drop-auto-scroll', - '@atlaskit/pragmatic-drag-and-drop-hitbox', - '@tanstack/react-query-persist-client', - 'idb-keyval', + plugins: [ + react(), + ViteEjsPlugin({ + root: normalizePath(path.resolve(__dirname, './src/renderer')), + web: true, + }), + VitePWA({ + devOptions: { + // The PWA will not be shown during development + enabled: false, + }, + filename: 'sw.js', + injectRegister: 'inline', + manifest: { + background_color: '#FFDCB5', + display: 'standalone', + icons: [ + { + sizes: '32x32', + src: '32x32.png', + type: 'image/png', + }, + { + sizes: '64x64', + src: '64x64.png', + type: 'image/png', + }, + { + sizes: '128x128', + src: '128x128.png', + type: 'image/png', + }, + { + sizes: '256x256', + src: '256x256.png', + type: 'image/png', + }, + { + purpose: 'any', + sizes: '512x512', + src: '512x512.png', + type: 'image/png', + }, + { + sizes: '1024x1024', + src: '1024x1024.png', + type: 'image/png', + }, + ], + name: 'Feishin', + orientation: 'portrait', + screenshots: [ + { + form_factor: 'wide', + label: 'Full screen player showing music player and lyrics', + sizes: '720x450', + src: 'preview_full_screen_player.webp', + type: 'image/webp', + }, + ], + short_name: 'Feishin', + start_url: '.', + theme_color: '#1E003D', + }, + manifestFilename: 'assets/manifest.webmanifest', + outDir: path.resolve(__dirname, './out/web/'), + registerType: 'autoUpdate', + workbox: { + cleanupOutdatedCaches: true, + clientsClaim: true, + maximumFileSizeToCacheInBytes: 1000000 * 5, // 5 MB + skipWaiting: true, + }, + }), ], - }, - plugins: [ - react(), - ViteEjsPlugin({ - root: normalizePath(path.resolve(__dirname, './src/renderer')), - web: true, - }), - VitePWA({ - devOptions: { - // The PWA will not be shown during development - enabled: false, + resolve: { + alias: { + '/@/i18n': path.resolve(__dirname, './src/i18n'), + '/@/remote': path.resolve(__dirname, './src/remote'), + '/@/renderer': path.resolve(__dirname, './src/renderer'), + '/@/shared': path.resolve(__dirname, './src/shared'), }, - filename: 'sw.js', - injectRegister: 'inline', - manifest: { - background_color: '#FFDCB5', - display: 'standalone', - icons: [ - { - sizes: '32x32', - src: '32x32.png', - type: 'image/png', - }, - { - sizes: '64x64', - src: '64x64.png', - type: 'image/png', - }, - { - sizes: '128x128', - src: '128x128.png', - type: 'image/png', - }, - { - sizes: '256x256', - src: '256x256.png', - type: 'image/png', - }, - { - purpose: 'any', - sizes: '512x512', - src: '512x512.png', - type: 'image/png', - }, - { - sizes: '1024x1024', - src: '1024x1024.png', - type: 'image/png', - }, - ], - name: 'Feishin', - orientation: 'portrait', - screenshots: [ - { - form_factor: 'wide', - label: 'Full screen player showing music player and lyrics', - sizes: '720x450', - src: 'preview_full_screen_player.webp', - type: 'image/webp', - }, - ], - short_name: 'Feishin', - start_url: '/', - theme_color: '#1E003D', - }, - manifestFilename: 'assets/manifest.webmanifest', - outDir: path.resolve(__dirname, './out/web/'), - registerType: 'autoUpdate', - scope: '/', - workbox: { - cleanupOutdatedCaches: true, - clientsClaim: true, - maximumFileSizeToCacheInBytes: 1000000 * 5, // 5 MB - skipWaiting: true, - }, - }), - ], - resolve: { - alias: { - '/@/i18n': path.resolve(__dirname, './src/i18n'), - '/@/remote': path.resolve(__dirname, './src/remote'), - '/@/renderer': path.resolve(__dirname, './src/renderer'), - '/@/shared': path.resolve(__dirname, './src/shared'), }, - }, - root: path.resolve(__dirname, './src/renderer'), + root: path.resolve(__dirname, './src/renderer'), + }; });