Files
dynamicbrain.neuroinf.jp/vite.config.ts
T
orrisroot d880bcaa3a fix: serve data files with reserved characters in dev and preview
sirv decodes request paths with decodeURI, which leaves %26 (&) and
%23 (#) encoded, so data files with such characters in their names
were not found by the dev and preview servers. Hand sirv the fully
decoded path, the way Apache resolves it in production.
2026-09-14 18:33:37 +09:00

90 lines
3.8 KiB
TypeScript

import fs from 'node:fs';
import path from 'node:path';
import react from '@vitejs/plugin-react';
import sirv from 'sirv';
import type { Connect, Plugin, ResolvedConfig } from 'vite';
import { defineConfig } from 'vite';
/**
* `static-contents/` holds the dumped site data (`modules/`, `rss.xml`, and the static `conferences/` and `hetero/` sites).
* It sits next to the repository and is served from the site root next to
* `public/`, but it is deliberately kept out of the bundle: it runs to
* gigabytes of frozen data that the web server maps in directly. The dev and
* preview servers mount it so that both behave like production.
*/
const staticContents = (): Plugin => {
let dir: string;
const mount = (middlewares: { use: (fn: Connect.NextHandleFunction) => void }, dev: boolean) => {
if (fs.existsSync(dir)) {
// maxAge 0 makes sirv send `Cache-Control: public, max-age=0`, so the
// browser revalidates against the ETag instead of falling back to
// heuristic caching. Without it a re-dump, or another site served
// from the same localhost port, is answered from a stale cache.
const serve = sirv(dir, { dev, etag: true, maxAge: 0 });
middlewares.use((req, res, next) => {
// sirv decodes the path with decodeURI, which leaves reserved
// characters such as %26 (&) and %23 (#) encoded, so a file like
// `CFP&Reg.png` is not found. sirv reuses `req._parsedUrl` when
// it was parsed from the same `req.url`, so hand it the path
// decoded the way Apache resolves it.
const raw = req.url ?? '/';
const idx = raw.search(/[?#]/);
const pathname = idx === -1 ? raw : raw.slice(0, idx);
if (pathname.includes('%')) {
try {
const decoded = pathname.split('/').map(decodeURIComponent).join('/');
if (!decoded.includes('%')) {
(req as typeof req & { _parsedUrl?: object })._parsedUrl = {
pathname: decoded,
search: '',
query: undefined,
hash: undefined,
raw,
};
}
} catch {
// leave malformed escapes to sirv
}
}
serve(req, res, next);
});
}
};
return {
name: 'dynamicbrain:static-contents',
configResolved(config: ResolvedConfig) {
dir = path.resolve(config.root, '../static-contents');
},
configureServer(server) {
mount(server.middlewares, true);
},
configurePreviewServer(server) {
mount(server.middlewares, false);
},
};
};
// https://vite.dev/config/
export default defineConfig({
plugins: [react(), staticContents()],
optimizeDeps: { exclude: ['fs'] },
build: {
sourcemap: true,
rolldownOptions: {
external: ['fs'],
output: {
// react and react-dom are over 200 kB together, which would
// push a single dependency chunk past the 500 kB warning
// threshold. They also change far less often than the rest,
// so split them off and let the two cache independently.
codeSplitting: {
groups: [
{ name: 'vendor-react', test: /node_modules\/(react|react-dom|scheduler)\// },
{ name: 'vendor-misc', test: /node_modules\// },
],
},
},
},
},
});