chore(deps)!: vite 8 (rolldown) + @vitejs/plugin-react 6 (4h)
Build drops 6.5s -> 0.65s. Three breakages found by the prod-smoke gate, all fixed: - rolldown removed object-form manualChunks -> function form in vite.config.mjs (same vendor split, all @radix/@tanstack grouped) - the 4a 'send' bump rejects bare absolute paths: SPA fallback sendFile and both download routes (admin backup, user-db export) now use the root-option form - rolldown minifies inline HTML scripts, breaking any CSP hash approach: the pre-paint theme script moved to client/public/theme-init.js so script-src 'self' holds with no inline hash to maintain @testing-library/dom added (peer no longer auto-installed). Server 252, client 52, probe 18/18, PROD SMOKE: PASS. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
a1b7011149
commit
d8365d0e22
|
|
@ -0,0 +1,24 @@
|
|||
// Pre-bundle theme apply — avoids a light/dark flash. Mirrors applyTheme in
|
||||
// client/contexts/ThemeContext.tsx (theme 'system' follows the OS).
|
||||
// External file (not inline in index.html) so the CSP stays `script-src 'self'`
|
||||
// with no inline-hash maintenance across build-tool upgrades.
|
||||
(function () {
|
||||
try {
|
||||
var s = localStorage.getItem('bt-theme');
|
||||
if (s === 'dark-purple') s = 'dark';
|
||||
var theme = s === 'light' || s === 'dark' || s === 'system' ? s : 'dark';
|
||||
var resolved =
|
||||
theme === 'system'
|
||||
? window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches
|
||||
? 'dark'
|
||||
: 'light'
|
||||
: theme;
|
||||
var root = document.documentElement;
|
||||
if (resolved === 'dark') root.classList.add('dark');
|
||||
else root.classList.remove('dark');
|
||||
var m = document.querySelector('meta[name="theme-color"]');
|
||||
if (m) m.setAttribute('content', resolved === 'dark' ? '#101417' : '#f8faf9');
|
||||
} catch (e) {
|
||||
/* localStorage/matchMedia unavailable — keep default */
|
||||
}
|
||||
})();
|
||||
21
index.html
21
index.html
|
|
@ -23,24 +23,9 @@
|
|||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
||||
<meta name="apple-mobile-web-app-title" content="Bill Tracker">
|
||||
<title>Bill Tracker</title>
|
||||
<script>
|
||||
// Pre-bundle theme apply — avoids a light/dark flash. Mirrors applyTheme in
|
||||
// client/contexts/ThemeContext.tsx (theme 'system' follows the OS).
|
||||
(function () {
|
||||
try {
|
||||
var s = localStorage.getItem('bt-theme');
|
||||
if (s === 'dark-purple') s = 'dark';
|
||||
var theme = (s === 'light' || s === 'dark' || s === 'system') ? s : 'dark';
|
||||
var resolved = theme === 'system'
|
||||
? ((window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) ? 'dark' : 'light')
|
||||
: theme;
|
||||
var root = document.documentElement;
|
||||
if (resolved === 'dark') root.classList.add('dark'); else root.classList.remove('dark');
|
||||
var m = document.querySelector('meta[name="theme-color"]');
|
||||
if (m) m.setAttribute('content', resolved === 'dark' ? '#101417' : '#f8faf9');
|
||||
} catch (e) { /* localStorage/matchMedia unavailable — keep default */ }
|
||||
})();
|
||||
</script>
|
||||
<!-- Pre-bundle theme apply (external so CSP stays script-src 'self'): a
|
||||
classic script blocks parsing, so the class lands before first paint. -->
|
||||
<script src="/theme-init.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -66,7 +66,7 @@
|
|||
"qrcode": "^1.5.4",
|
||||
"react": "^19.2.7",
|
||||
"react-dom": "^19.2.7",
|
||||
"react-router-dom": "^6.26.2",
|
||||
"react-router-dom": "^7.18.1",
|
||||
"sonner": "^2.0.7",
|
||||
"tailwind-merge": "^2.5.4",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
|
|
@ -76,11 +76,12 @@
|
|||
"@axe-core/playwright": "^4.10.1",
|
||||
"@eslint/js": "^10.0.1",
|
||||
"@playwright/test": "^1.50.1",
|
||||
"@testing-library/dom": "^10.4.1",
|
||||
"@testing-library/react": "^16.3.2",
|
||||
"@types/node": "^26.1.0",
|
||||
"@types/react": "^19.2.17",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@vitejs/plugin-react": "^4.3.3",
|
||||
"@vitejs/plugin-react": "^6.0.3",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"babel-plugin-react-compiler": "^1.0.0",
|
||||
"concurrently": "^10.0.3",
|
||||
|
|
@ -97,7 +98,7 @@
|
|||
"tailwindcss": "^3.4.14",
|
||||
"typescript": "^6.0.3",
|
||||
"typescript-eslint": "^8.62.1",
|
||||
"vite": "^5.4.10",
|
||||
"vite": "^8.1.4",
|
||||
"vite-plugin-pwa": "^1.3.0",
|
||||
"vitest": "^4.1.8"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -149,9 +149,16 @@ router.get('/backups/:id/download', (req: Req, res: Res) => {
|
|||
const backup = getBackupFile(req.params.id);
|
||||
res.setHeader('Content-Type', 'application/octet-stream');
|
||||
res.setHeader('X-Content-Type-Options', 'nosniff');
|
||||
res.download(backup.path, backup.metadata.id, (err) => {
|
||||
if (err && !res.headersSent) sendError(res, err);
|
||||
});
|
||||
// root-option form: send >= 2026-07 rejects bare absolute paths (4a dep sweep)
|
||||
const pathMod = require('path');
|
||||
res.download(
|
||||
pathMod.basename(backup.path),
|
||||
backup.metadata.id,
|
||||
{ root: pathMod.dirname(backup.path) },
|
||||
(err) => {
|
||||
if (err && !res.headersSent) sendError(res, err);
|
||||
},
|
||||
);
|
||||
} catch (err) {
|
||||
sendError(res, err);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -273,7 +273,8 @@ const { getCsrfToken } = require('./middleware/csrf.cts');
|
|||
app.get('/{*splat}', (req, res) => {
|
||||
// Set CSRF cookie if not present (needed for SPA to read token)
|
||||
getCsrfToken(req, res);
|
||||
res.sendFile(path.join(DIST, 'index.html'));
|
||||
// root-option form: send >= 2026-07 rejects bare absolute paths (4a dep sweep)
|
||||
res.sendFile('index.html', { root: DIST });
|
||||
});
|
||||
|
||||
// ── Global error handler ──────────────────────────────────────────────────────
|
||||
|
|
|
|||
|
|
@ -103,18 +103,15 @@ export default defineConfig({
|
|||
output: {
|
||||
// QA-B0-01: split the shared vendor code out of the ~659 kB index chunk
|
||||
// so large libs load/cache independently and the main bundle shrinks.
|
||||
manualChunks: {
|
||||
'vendor-react': ['react', 'react-dom', 'react-router-dom'],
|
||||
'vendor-radix': [
|
||||
'@radix-ui/react-dialog',
|
||||
'@radix-ui/react-select',
|
||||
'@radix-ui/react-dropdown-menu',
|
||||
'@radix-ui/react-tabs',
|
||||
'@radix-ui/react-tooltip',
|
||||
'@radix-ui/react-alert-dialog',
|
||||
],
|
||||
'vendor-motion': ['framer-motion'],
|
||||
'vendor-query': ['@tanstack/react-query', '@tanstack/react-virtual'],
|
||||
// Function form: vite 8 (rolldown core) dropped the object form.
|
||||
manualChunks(id) {
|
||||
if (!id.includes('node_modules')) return undefined;
|
||||
if (/node_modules[\\/](react|react-dom|react-router|react-router-dom)[\\/]/.test(id))
|
||||
return 'vendor-react';
|
||||
if (/node_modules[\\/]@radix-ui[\\/]/.test(id)) return 'vendor-radix';
|
||||
if (/node_modules[\\/]framer-motion[\\/]/.test(id)) return 'vendor-motion';
|
||||
if (/node_modules[\\/]@tanstack[\\/]/.test(id)) return 'vendor-query';
|
||||
return undefined;
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
|||
Loading…
Reference in New Issue