Files
5chan/vite.config.js
T

384 lines
11 KiB
JavaScript
Raw Permalink Normal View History

import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { resolve } from 'path';
import { readFileSync } from 'fs';
import { createHash } from 'crypto';
2025-03-08 22:32:45 +01:00
import { VitePWA } from 'vite-plugin-pwa';
2025-03-04 18:11:21 +01:00
const { version: packageVersion } = JSON.parse(readFileSync(new URL('./package.json', import.meta.url), 'utf8'));
const appVersion = `${process.env.VITE_APP_VERSION || packageVersion}`.trim() || packageVersion;
process.env.VITE_APP_VERSION = appVersion;
2026-04-20 22:51:31 +07:00
const publicBase = process.env.PUBLIC_URL || '/';
const buildOutDir = 'build';
const basePathPrefix = (() => {
const pathname = new URL(publicBase, 'https://example.invalid/').pathname;
return pathname === '/' ? '' : pathname.replace(/^\/+|\/+$/g, '');
})();
const neverPrecacheUrls = new Set(['index.html', 'version.json']);
const vitePwaManagedAssetUrls = new Set([
'manifest.webmanifest',
'favicon.ico',
'favicon2.ico',
'robots.txt',
'apple-touch-icon.png',
'manifest-icon-192x192.png',
'manifest-icon-512x512.png',
]);
const baselineAppShellUrls = new Set([
'registerSW.js',
'manifest.json',
'manifest.webmanifest',
'favicon.ico',
'favicon2.ico',
'robots.txt',
'apple-touch-icon.png',
'manifest-icon-192x192.png',
'manifest-icon-512x512.png',
]);
function normalizePrecacheUrl(url) {
const normalizedUrl = url.split('?')[0].replace(/^[./]+/, '');
if (basePathPrefix && normalizedUrl.startsWith(`${basePathPrefix}/`)) {
return normalizedUrl.slice(basePathPrefix.length + 1);
}
return normalizedUrl;
}
function collectIndexAssetUrls() {
const indexHtml = readFileSync(new URL(`./${buildOutDir}/index.html`, import.meta.url), 'utf8');
const urls = new Set(baselineAppShellUrls);
const assetAttributePattern = /\b(?:href|src)=["'](?:\.\/|\/)?([^"']+\.(?:css|ico|js|json|png|webmanifest))(?:\?[^"']*)?["']/g;
for (const match of indexHtml.matchAll(assetAttributePattern)) {
urls.add(normalizePrecacheUrl(match[1]));
}
return urls;
}
function keepAppShellPrecacheOnly(manifestEntries) {
const appShellUrls = collectIndexAssetUrls();
const manifest = manifestEntries.filter((entry) => {
const normalizedUrl = normalizePrecacheUrl(entry.url);
if (neverPrecacheUrls.has(normalizedUrl)) {
return false;
}
if (vitePwaManagedAssetUrls.has(normalizedUrl)) {
return false;
}
return appShellUrls.has(normalizedUrl);
});
return { manifest };
}
function appVersionMetadataPlugin() {
const payload = `${JSON.stringify({ version: appVersion })}\n`;
return {
name: 'fivechan-version-metadata',
configureServer(server) {
server.middlewares.use('/version.json', (_req, res) => {
res.setHeader('Content-Type', 'application/json; charset=utf-8');
res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
res.end(payload);
});
},
generateBundle() {
this.emitFile({
type: 'asset',
fileName: 'version.json',
source: payload,
});
},
};
}
function getVercelContentSecurityPolicy() {
const vercelConfig = JSON.parse(readFileSync(new URL('./vercel.json', import.meta.url), 'utf8'));
const cspHeader = vercelConfig.headers
?.flatMap((entry) => entry.headers || [])
.find((header) => typeof header.key === 'string' && header.key.toLowerCase() === 'content-security-policy');
if (typeof cspHeader?.value !== 'string') {
throw new Error('vercel.json is missing a Content-Security-Policy header.');
}
return cspHeader.value;
}
function getInlineScriptHashes(indexHtml) {
const scriptPattern = /<script\b([^>]*)>([\s\S]*?)<\/script>/gi;
const hashes = [];
for (const [, attributes, source] of indexHtml.matchAll(scriptPattern)) {
if (/\bsrc\s*=/i.test(attributes) || source.trim().length === 0) {
continue;
}
hashes.push(`sha256-${createHash('sha256').update(source).digest('base64')}`);
}
return [...new Set(hashes)];
}
function verifyVercelCspHashesPlugin() {
return {
name: 'fivechan-verify-vercel-csp-hashes',
apply: 'build',
enforce: 'post',
closeBundle() {
const indexHtml = readFileSync(new URL(`./${buildOutDir}/index.html`, import.meta.url), 'utf8');
const contentSecurityPolicy = getVercelContentSecurityPolicy();
const missingHashes = getInlineScriptHashes(indexHtml).filter(
(hash) => !contentSecurityPolicy.includes(`'${hash}'`) && !contentSecurityPolicy.includes(hash),
);
if (missingHashes.length > 0) {
const plural = missingHashes.length === 1 ? '' : 'es';
throw new Error(`vercel.json Content-Security-Policy is missing inline script hash${plural}: ${missingHashes.join(', ')}`);
}
},
};
}
function adaptReactPluginForRolldown(plugin) {
if (!plugin?.config || plugin.name !== 'vite:react-babel') {
return plugin;
}
return {
...plugin,
async config(userConfig, configEnv) {
const config = await plugin.config.call(this, userConfig, configEnv);
const optimizeDeps = config?.optimizeDeps;
if (optimizeDeps?.esbuildOptions?.jsx !== 'automatic') {
return config;
}
const { esbuildOptions, ...remainingOptimizeDeps } = optimizeDeps;
return {
...config,
optimizeDeps: {
...remainingOptimizeDeps,
rolldownOptions: {
...optimizeDeps.rolldownOptions,
transform: {
...optimizeDeps.rolldownOptions?.transform,
jsx: optimizeDeps.rolldownOptions?.transform?.jsx ?? {
runtime: 'automatic',
},
},
},
},
};
},
};
}
export default defineConfig({
plugins: [
appVersionMetadataPlugin(),
...react({
babel: {
plugins: [
[
'babel-plugin-react-compiler',
{
verbose: true,
},
],
],
},
}).map(adaptReactPluginForRolldown),
2025-03-08 22:32:45 +01:00
VitePWA({
registerType: 'autoUpdate',
2025-05-22 17:42:04 +02:00
strategies: 'injectManifest',
injectManifest: {
2025-07-29 18:16:44 +02:00
maximumFileSizeToCacheInBytes: 20000000,
2026-04-20 22:51:31 +07:00
globPatterns: ['**/*.{css,html,ico,js,json,png,webmanifest}'],
manifestTransforms: [keepAppShellPrecacheOnly],
2025-05-22 17:42:04 +02:00
},
srcDir: 'src',
filename: 'sw.ts',
devOptions: {
enabled: true,
type: 'module',
},
includeAssets: ['favicon.ico', 'favicon2.ico', 'robots.txt', 'apple-touch-icon.png'],
2025-03-08 22:32:45 +01:00
manifest: {
2025-10-17 23:30:35 +02:00
name: '5chan',
short_name: '5chan',
2026-02-27 13:56:29 +08:00
description: 'A serverless, adminless, decentralized imageboard',
2025-03-08 22:32:45 +01:00
theme_color: '#ffffff',
background_color: '#ffffee',
display: 'standalone',
icons: [
{
2026-04-20 22:51:31 +07:00
src: 'manifest-icon-192x192.png',
2025-03-08 22:32:45 +01:00
sizes: '192x192',
type: 'image/png',
2025-03-08 22:32:45 +01:00
},
{
2026-04-20 22:51:31 +07:00
src: 'manifest-icon-512x512.png',
2025-03-08 22:32:45 +01:00
sizes: '512x512',
type: 'image/png',
},
{
2026-04-20 22:51:31 +07:00
src: 'manifest-icon-512x512.png',
sizes: '512x512',
type: 'image/png',
purpose: 'any maskable',
},
],
2025-03-08 22:32:45 +01:00
},
workbox: {
clientsClaim: true,
skipWaiting: true,
cleanupOutdatedCaches: true,
navigateFallback: 'index.html',
2025-05-13 14:19:28 +02:00
navigateFallbackDenylist: [/^\/api/, /^\/_\(.*\)/],
maximumFileSizeToCacheInBytes: 6000000,
2025-03-08 22:32:45 +01:00
runtimeCaching: [
// PNG caching
{
urlPattern: ({ url }) => url.pathname.endsWith('.png'),
handler: 'StaleWhileRevalidate',
options: {
cacheName: 'images',
expiration: {
maxEntries: 50,
},
},
2025-03-08 22:32:45 +01:00
},
// Add additional asset caching
{
urlPattern: /\.(?:js|css|woff2?|svg|gif|jpg|jpeg)$/,
handler: 'StaleWhileRevalidate',
options: {
cacheName: 'assets-cache',
expiration: {
maxEntries: 100,
maxAgeSeconds: 60 * 60 * 24 * 30, // 30 days
},
},
2025-03-08 22:32:45 +01:00
},
// Google Fonts caching
{
urlPattern: /^https:\/\/fonts\.googleapis\.com\/.*/i,
handler: 'CacheFirst',
options: {
cacheName: 'google-fonts-cache',
expiration: {
maxEntries: 10,
maxAgeSeconds: 60 * 60 * 24 * 365, // 365 days
2025-03-08 22:32:45 +01:00
},
cacheableResponse: {
statuses: [0, 200],
},
},
2025-03-08 22:32:45 +01:00
},
{
urlPattern: /^https:\/\/fonts\.gstatic\.com\/.*/i,
handler: 'CacheFirst',
options: {
cacheName: 'google-fonts-webfonts',
expiration: {
maxEntries: 30,
maxAgeSeconds: 60 * 60 * 24 * 365, // 365 days
2025-03-08 22:32:45 +01:00
},
cacheableResponse: {
statuses: [0, 200],
},
},
},
],
},
2025-03-08 22:32:45 +01:00
}),
verifyVercelCspHashesPlugin(),
],
resolve: {
alias: {
'@': resolve(__dirname, 'src'),
'node-fetch': 'isomorphic-fetch',
assert: 'assert',
stream: 'stream-browserify',
crypto: 'crypto-browserify',
buffer: 'buffer',
events: 'events',
process: 'process',
'node:buffer': 'buffer',
'node:crypto': 'crypto-browserify',
'node:events': 'events',
'node:process': 'process',
'node:stream': 'stream-browserify',
2026-04-11 16:57:00 +07:00
'node:util': 'util/',
'util/': 'util/',
util: 'util/',
},
},
server: {
port: 3000,
open: process.env.PORTLESS_URL ? false : true,
watch: {
usePolling: true,
},
hmr: {
overlay: false,
},
},
build: {
// Use 'build' to match what electron/main.js expects (../build/index.html)
2026-04-20 22:51:31 +07:00
outDir: buildOutDir,
emptyOutDir: true,
sourcemap: process.env.GENERATE_SOURCEMAP === 'true',
target: process.env.ELECTRON ? 'electron-renderer' : 'esnext',
rollupOptions: {
output: {
manualChunks(id) {
if (/[\\/]node_modules[\\/](@pkcprotocol[\\/]pkc-js)[\\/]/.test(id)) {
return 'pkc-js';
}
if (/[\\/]node_modules[\\/](@bitsocialnet[\\/]bitsocial-react-hooks)[\\/]/.test(id)) {
return 'bitsocial-react-hooks';
}
if (/[\\/]node_modules[\\/](@react-spring|@use-gesture)[\\/]/.test(id)) {
return 'spring-gesture';
}
if (/[\\/]node_modules[\\/](react|react-dom|react-router-dom|react-i18next|i18next|i18next-browser-languagedetector|i18next-http-backend)[\\/]/.test(id)) {
return 'vendor';
}
if (/[\\/]node_modules[\\/](react-markdown|remark-|rehype-|unified|micromark|mdast|hast|unist)[\\/]/.test(id)) {
return 'markdown';
}
if (/[\\/]node_modules[\\/](react-virtuoso)[\\/]/.test(id)) {
return 'virtuoso';
}
if (/[\\/]node_modules[\\/](@floating-ui)[\\/]/.test(id)) {
return 'floating-ui';
}
},
},
},
},
2026-04-20 22:51:31 +07:00
base: publicBase,
optimizeDeps: {
include: ['ethers', 'assert', 'buffer', 'process', 'util', 'stream-browserify', 'isomorphic-fetch', 'workbox-core', 'workbox-precaching'],
},
define: {
'import.meta.env.VITE_APP_VERSION': JSON.stringify(appVersion),
2025-03-04 18:39:34 +01:00
'process.env.VITE_COMMIT_REF': JSON.stringify(process.env.COMMIT_REF),
'process.version': JSON.stringify(''),
global: 'globalThis',
__dirname: '""',
},
});