Files
api/web/vite.config.ts
Eli Bosley 0d165a6087 fix: add cache busting to web component extractor (#1731)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- Bug Fixes
- Ensures UI assets use content-hashed filenames so browsers load the
latest scripts and styles after updates, reducing stale-cache issues.
- Keeps scripts and their related styles in sync for consistent
rendering and fewer cache-related glitches.
- Ignores non-asset manifest entries to prevent accidental inclusion of
invalid items and ensure correct asset loading.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-09-30 12:27:48 -04:00

200 lines
5.6 KiB
TypeScript

import path from 'node:path';
import { fileURLToPath, URL } from 'node:url';
import ui from '@nuxt/ui/vite';
import tailwindcss from '@tailwindcss/vite';
import vue from '@vitejs/plugin-vue';
import { defineConfig } from 'vite';
import removeConsole from 'vite-plugin-remove-console';
import scopeTailwindToUnapi from './postcss/scopeTailwindToUnapi';
import { serveStaticHtml } from './vite-plugin-serve-static';
const dropConsole = process.env.VITE_ALLOW_CONSOLE_LOGS !== 'true';
console.log(dropConsole ? 'WARN: Console logs are disabled' : 'INFO: Console logs are enabled');
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const assetsDir = path.join(__dirname, '../api/dev/webGui/');
/**
* Used to avoid redeclaring variables in the webgui codebase.
*/
function terserReservations(inputStr: string) {
const combinations = ['ace'];
// Add 1-character combinations
for (let i = 0; i < inputStr.length; i++) {
combinations.push(inputStr[i]);
}
// Add 2-character combinations
for (let i = 0; i < inputStr.length; i++) {
for (let j = 0; j < inputStr.length; j++) {
combinations.push(inputStr[i] + inputStr[j]);
}
}
return combinations;
}
const charsToReserve = '_$abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
/**
* Shared terser options for consistent minification
*/
const sharedTerserOptions = {
mangle: {
reserved: terserReservations(charsToReserve),
toplevel: true,
},
};
/**
* Shared define configuration
*/
const sharedDefine = {
'globalThis.__DEV__': JSON.stringify(process.env.NODE_ENV === 'development'),
__VUE_PROD_DEVTOOLS__: JSON.stringify(false),
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'production'),
'process.env': JSON.stringify({}),
};
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
tailwindcss(),
vue({
template: {
compilerOptions: {
// Treat all unraid-* components as custom elements
isCustomElement: (tag) => tag.startsWith('unraid-'),
},
},
}),
ui(),
serveStaticHtml(), // Serve static test pages
// Remove console logs in production
...(dropConsole
? [
removeConsole({
includes: ['log', 'info', 'debug'],
}),
]
: []),
],
css: {
postcss: {
plugins: [scopeTailwindToUnapi()],
},
},
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url)),
'~': fileURLToPath(new URL('./src', import.meta.url)),
'~~': fileURLToPath(new URL('./', import.meta.url)),
'~/': fileURLToPath(new URL('./src/', import.meta.url)),
},
},
optimizeDeps: {
include: ['ajv', 'ajv-errors'],
esbuildOptions: {
define: {
global: 'globalThis',
},
},
},
define: {
...sharedDefine,
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'production'),
},
publicDir: false, // Don't copy public files to dist
build: {
outDir: 'dist',
emptyOutDir: true,
manifest: false, // Disable Vite's manifest since we generate our own
lib: {
entry: fileURLToPath(new URL('./src/components/Wrapper/auto-mount.ts', import.meta.url)),
name: 'UnraidStandaloneApps',
fileName: 'standalone-apps',
formats: ['es'],
},
rollupOptions: {
output: {
format: 'es',
entryFileNames: 'standalone-apps-[hash].js',
chunkFileNames: '[name]-[hash].js',
assetFileNames: (assetInfo) => {
if (assetInfo.name?.endsWith('.css')) {
return 'standalone-apps-[hash][extname]';
}
return '[name]-[hash][extname]';
},
inlineDynamicImports: false,
},
},
cssCodeSplit: false, // Bundle all CSS together
minify: 'terser',
terserOptions: sharedTerserOptions,
},
server: {
port: 3000,
proxy: {
'/graphql': {
target: 'http://localhost:3001',
changeOrigin: true,
ws: true,
secure: false,
// Important: preserve the host header
configure: (proxy) => {
proxy.on('proxyReq', (proxyReq) => {
proxyReq.setHeader('X-Forwarded-Host', 'localhost:3000');
proxyReq.setHeader('X-Forwarded-Proto', 'http');
proxyReq.setHeader('X-Forwarded-For', '127.0.0.1');
});
// Handle connection errors gracefully
proxy.on('error', (err: Error, _req: unknown, res: unknown) => {
console.warn('[Vite] GraphQL proxy error (API server may not be running):', err.message);
// Check if res has writeHead method (it's an HTTP response, not a socket)
const httpRes = res as {
writeHead?: (statusCode: number, headers: Record<string, string>) => void;
end?: (data: string) => void;
};
if (
httpRes &&
typeof httpRes.writeHead === 'function' &&
typeof httpRes.end === 'function'
) {
httpRes.writeHead(503, {
'Content-Type': 'application/json',
});
httpRes.end(
JSON.stringify({
error: 'GraphQL API server not available',
message: 'Please start the API server on port 3001',
})
);
}
});
},
},
'/webGui': {
target: `file://${assetsDir}`,
changeOrigin: true,
},
},
// Configure static file serving
fs: {
strict: false,
allow: ['..'], // Allow serving files outside of root
},
},
});