diff --git a/backend/package.json b/backend/package.json index 9e546ac..4cd1d83 100644 --- a/backend/package.json +++ b/backend/package.json @@ -1,6 +1,6 @@ { "name": "backend", - "version": "1.12.2", + "version": "1.12.3", "private": true, "type": "commonjs", "main": "src/index.js", diff --git a/backend/src/app.js b/backend/src/app.js index 87aa6eb..d3662e6 100644 --- a/backend/src/app.js +++ b/backend/src/app.js @@ -11,7 +11,10 @@ const notesRoutes = require('./routes/notes.routes'); function createApp() { const app = express(); - app.use(cors({ origin: process.env.CORS_ORIGIN || 'http://localhost:5173' })); + // The site and the API are on different hosts, so a browser asks leave + // before each kind of request. maxAge lets it remember the answer (browsers + // cap it at two hours) and not ask again every time. + app.use(cors({ origin: process.env.CORS_ORIGIN || 'http://localhost:5173', maxAge: 7200 })); // Nothing in front of the API compresses for it (frp only tunnels), so the // JSON is gzipped here. app.use(compression()); diff --git a/frontend/package.json b/frontend/package.json index 10e6f5a..45d6537 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,7 +1,7 @@ { "name": "frontend", "private": true, - "version": "1.12.2", + "version": "1.12.3", "type": "module", "scripts": { "dev": "vite", diff --git a/frontend/src/hooks/useCampaignSocket.ts b/frontend/src/hooks/useCampaignSocket.ts index 2a5913c..bbd497b 100644 --- a/frontend/src/hooks/useCampaignSocket.ts +++ b/frontend/src/hooks/useCampaignSocket.ts @@ -8,7 +8,10 @@ let sharedSocket: Socket | null = null; function getSocket(token: string): Socket { if (!sharedSocket) { - sharedSocket = io(API_URL, { auth: { token }, transports: ['websocket', 'polling'] }); + // Starts on plain requests and moves up to a websocket when one gets + // through. Asking for the websocket first leaves no way back: behind a + // proxy that does not pass the upgrade, nothing live arrives at all. + sharedSocket = io(API_URL, { auth: { token } }); } return sharedSocket; } diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index 1ffa626..3d74c88 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -1,20 +1,33 @@ +import { existsSync } from 'node:fs' +import { join } from 'node:path' import react from '@vitejs/plugin-react' import compression from 'compression' import { defineConfig, type Connect, type Plugin } from 'vite' -// `vite preview` is what serves the built site, and nothing in front of it -// compresses (frp only tunnels), so the files are gzipped here. -const gzipPreview: Plugin = { - name: 'gzip-preview', +// `vite preview` is what serves the built site, so the two things a web +// server would be set up to do are done here. +const servePreview: Plugin = { + name: 'serve-preview', configurePreviewServer(server) { + const assets = join(server.config.root, server.config.build.outDir) // Typed for Express, and at home in any Node server. server.middlewares.use(compression() as Connect.NextHandleFunction) + // A built file carries a hash of its contents in its name, so a browser + // can keep it for good and never ask again. Only for files that are + // there: a name from an older build must not be remembered as missing. + server.middlewares.use((req, res, next) => { + const path = (req.url ?? '').split('?')[0] + if (path.startsWith('/assets/') && !path.includes('..') && existsSync(join(assets, path))) { + res.setHeader('Cache-Control', 'public, max-age=31536000, immutable') + } + next() + }) }, } // https://vite.dev/config/ export default defineConfig({ - plugins: [react(), gzipPreview], + plugins: [react(), servePreview], preview: { host: true, diff --git a/package-lock.json b/package-lock.json index 6adf345..1bdde88 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,19 +1,19 @@ { "name": "dnd-campaign-manager", - "version": "1.12.2", + "version": "1.12.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "dnd-campaign-manager", - "version": "1.12.2", + "version": "1.12.3", "workspaces": [ "backend", "frontend" ] }, "backend": { - "version": "1.12.2", + "version": "1.12.3", "dependencies": { "@prisma/adapter-pg": "^7.10.0", "@prisma/client": "^7.10.0", @@ -33,7 +33,7 @@ } }, "frontend": { - "version": "1.12.2", + "version": "1.12.3", "dependencies": { "@tanstack/react-query": "^5.103.2", "axios": "^1.20.0", diff --git a/package.json b/package.json index 74eb3d4..5bb2100 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "dnd-campaign-manager", - "version": "1.12.2", + "version": "1.12.3", "private": true, "description": "D&D 5e campaign manager — monorepo root", "workspaces": [