diff --git a/api/server/utils/staticCache.js b/api/server/utils/staticCache.js index 5925a56be5..e885273223 100644 --- a/api/server/utils/staticCache.js +++ b/api/server/utils/staticCache.js @@ -1,3 +1,4 @@ +const path = require('path'); const expressStaticGzip = require('express-static-gzip'); const oneDayInSeconds = 24 * 60 * 60; @@ -5,16 +6,45 @@ const oneDayInSeconds = 24 * 60 * 60; const sMaxAge = process.env.STATIC_CACHE_S_MAX_AGE || oneDayInSeconds; const maxAge = process.env.STATIC_CACHE_MAX_AGE || oneDayInSeconds * 2; -const staticCache = (staticPath) => - expressStaticGzip(staticPath, { - enableBrotli: false, // disable Brotli, only using gzip +/** + * Creates an Express static middleware with gzip compression and configurable caching + * + * @param {string} staticPath - The file system path to serve static files from + * @param {Object} [options={}] - Configuration options + * @param {boolean} [options.noCache=false] - If true, disables caching entirely for all files + * @returns {ReturnType} Express middleware function for serving static files + */ +function staticCache(staticPath, options = {}) { + const { noCache = false } = options; + return expressStaticGzip(staticPath, { + enableBrotli: false, orderPreference: ['gz'], - setHeaders: (res, _path) => { - if (process.env.NODE_ENV?.toLowerCase() === 'production') { + setHeaders: (res, filePath) => { + if (process.env.NODE_ENV?.toLowerCase() !== 'production') { + return; + } + if (noCache) { + res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate'); + return; + } + if (filePath.includes('/dist/images/')) { + return; + } + const fileName = path.basename(filePath); + + if ( + fileName === 'index.html' || + fileName.endsWith('.webmanifest') || + fileName === 'manifest.json' || + fileName === 'sw.js' + ) { + res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate'); + } else { res.setHeader('Cache-Control', `public, max-age=${maxAge}, s-maxage=${sMaxAge}`); } }, index: false, }); +} module.exports = staticCache; diff --git a/client/package.json b/client/package.json index 04069a8807..8e4be78764 100644 --- a/client/package.json +++ b/client/package.json @@ -6,7 +6,7 @@ "scripts": { "data-provider": "cd .. && npm run build:data-provider", "build:file": "cross-env NODE_ENV=production vite build --debug > vite-output.log 2>&1", - "build": "cross-env NODE_ENV=production vite build", + "build": "cross-env NODE_ENV=production vite build && node ./scripts/post-build.cjs", "build:ci": "cross-env NODE_ENV=development vite build --mode ci", "dev": "cross-env NODE_ENV=development vite", "preview-prod": "cross-env NODE_ENV=development vite preview", diff --git a/client/scripts/post-build.cjs b/client/scripts/post-build.cjs new file mode 100644 index 0000000000..0c0f00dc14 --- /dev/null +++ b/client/scripts/post-build.cjs @@ -0,0 +1,14 @@ +const fs = require('fs-extra'); + +async function postBuild() { + try { + await fs.copy('public/assets', 'dist/assets'); + await fs.copy('public/robots.txt', 'dist/robots.txt'); + console.log('✅ PWA icons and robots.txt copied successfully. Glob pattern warnings resolved.'); + } catch (err) { + console.error('❌ Error copying files:', err); + process.exit(1); + } +} + +postBuild(); diff --git a/client/vite.config.ts b/client/vite.config.ts index e0d7b1bebe..facf3ec9b4 100644 --- a/client/vite.config.ts +++ b/client/vite.config.ts @@ -8,7 +8,7 @@ import { nodePolyfills } from 'vite-plugin-node-polyfills'; import type { Plugin } from 'vite'; // https://vitejs.dev/config/ -export default defineConfig({ +export default defineConfig(({ command }) => ({ server: { host: 'localhost', port: 3090, @@ -37,13 +37,21 @@ export default defineConfig({ enabled: false, // disable service worker registration in development mode }, useCredentials: true, + includeManifestIcons: false, workbox: { - globPatterns: ['**/*'], + globPatterns: [ + '**/*.{js,css,html}', + 'assets/favicon*.png', + 'assets/icon-*.png', + 'assets/apple-touch-icon*.png', + 'assets/maskable-icon.png', + 'manifest.webmanifest', + ], globIgnores: ['images/**/*', '**/*.map'], maximumFileSizeToCacheInBytes: 4 * 1024 * 1024, navigateFallbackDenylist: [/^\/oauth/], }, - includeAssets: ['**/*'], + includeAssets: [], manifest: { name: 'LibreChat', short_name: 'LibreChat', @@ -94,14 +102,13 @@ export default defineConfig({ template: 'treemap', // 'treemap' | 'sunburst' | 'network' }), ].filter(Boolean), - publicDir: './public', + publicDir: command === 'serve' ? './public' : false, build: { sourcemap: process.env.NODE_ENV === 'development', outDir: './dist', minify: 'terser', rollupOptions: { preserveEntrySignatures: 'strict', - // external: ['uuid'], output: { manualChunks(id: string) { if (id.includes('node_modules')) { @@ -230,10 +237,10 @@ export default defineConfig({ resolve: { alias: { '~': path.join(__dirname, 'src/'), - $fonts: '/fonts', + $fonts: path.resolve(__dirname, 'public/fonts'), }, }, -}); +})); interface SourcemapExclude { excludeNodeModules?: boolean;