exifcleaner-web/vite.config.web.ts
forgejo_admin 1c642f5b37
Some checks failed
CI / Lint, Typecheck & Unit Tests (push) Successful in 36s
CI / Smoke build (VITE_ENABLE_FFMPEG_FALLBACK=false) (push) Successful in 1m2s
CI / E2E (Standalone single-file) (push) Successful in 1m55s
CI / E2E (Web) (push) Has been cancelled
security: remove style-src 'unsafe-inline' from all CSP policies (#197)
Closes #193

Migrate three inline-style React props to CSS classes / CSSOM:
- ErrorExpansion: cursor:copy and copy-hint color moved to BEM classes
- SegmentedControl: dynamic transform now driven by --ec-segment-offset CSS var, set via useLayoutEffect + ref.style.setProperty

Remove 'unsafe-inline' from style-src in all three enforcement layers:
- vite.config.web.ts (prod only; dev keeps it for HMR)
- nginx.conf (both CSP directives)
- public/_headers (Cloudflare Pages)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-23 00:06:28 +04:00

111 lines
3.8 KiB
TypeScript

import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import { VitePWA } from "vite-plugin-pwa";
import { resolve } from "node:path";
import type { Plugin } from "vite";
// Strip crossorigin attributes from script/link tags. Vite adds them by default
// for ES modules, but Electron's file:// load of `dist/web/index.html` in
// packaged builds enforces CORS on those tags and silently fails to load the
// scripts. Same-origin loads (web HTTPS deploy) don't need crossorigin either,
// so stripping it everywhere is safe. (Originally added in commit ca4fcec on
// the now-removed electron.vite.config.ts renderer block; ported here in #75.)
function removeCrossOriginPlugin(): Plugin {
return {
name: "remove-crossorigin",
enforce: "post",
transformIndexHtml(html) {
return html.replace(/ crossorigin/g, "");
},
};
}
function webCspPlugin(): Plugin {
return {
name: "web-csp",
transformIndexHtml(_html, ctx) {
const isDev = ctx.server !== undefined;
const scriptSrc = isDev
? "'self' 'unsafe-inline' 'wasm-unsafe-eval'"
: "'self' 'wasm-unsafe-eval'";
const styleSrc = isDev ? "'self' 'unsafe-inline'" : "'self'";
const connectSrc = isDev ? "'self' ws://localhost:*" : "'self'";
return [
{
tag: "meta",
attrs: {
"http-equiv": "Content-Security-Policy",
content: `default-src 'none'; script-src ${scriptSrc}; style-src ${styleSrc}; img-src 'self' data: blob:; font-src 'self'; connect-src ${connectSrc}; worker-src 'self' blob:; manifest-src 'self'; base-uri 'none'`,
},
injectTo: "head-prepend",
},
];
},
};
}
export default defineConfig({
root: resolve(__dirname, "src/web"),
publicDir: resolve(__dirname, "public"),
// Relative paths so the bundle works at any deploy root (PWA install,
// subdirectory hosting, the single-file standalone build at file://).
base: "./",
build: {
outDir: resolve(__dirname, "dist/web"),
emptyOutDir: true,
},
// Build-time flag consumed by ffmpeg_wasm_fetch.ts. In the PWA / APK
// build the inlined `<script type="text/plain">` ffmpeg tags don't
// exist, so readInlinedCore() returns null and we MUST reach the bare
// `import("@ffmpeg/core")` branch. Setting this to `false` here keeps
// Rollup from tree-shaking that branch. The standalone config sets it
// to `true` to drop ~43 MB from the single-file HTML.
define: {
__WITH_STANDALONE_INLINE__: "false",
},
plugins: [
react(),
webCspPlugin(),
removeCrossOriginPlugin(),
VitePWA({
registerType: "autoUpdate",
manifest: false,
workbox: {
// App shell only — keep precache small (~1.3 MB) so the SW
// installs quickly and reliably. The 25 MB zeroperl.wasm is
// too big for precache (workbox's parallel install fetch
// times out on the e2e CI runner) and most users don't need
// it (JPEG/PNG/PDF/Office/MP4 don't route through the WASM
// fallback). It's served via runtimeCaching below instead.
globPatterns: [
"**/*.{js,css,html,ico,png,svg,webmanifest}",
],
maximumFileSizeToCacheInBytes: 10 * 1024 * 1024,
// Lazy-cache zeroperl.wasm: first time the user drops a
// WebP/GIF/AVIF the WASM is fetched from the network and
// stored in the cache. Subsequent drops (and offline use
// after the first online drop) serve from cache. The 30-day
// expiration is a defensive ceiling — content-hashed
// filenames mean stale entries are naturally orphaned on
// each deploy.
runtimeCaching: [
{
urlPattern: /\/assets\/zeroperl-[A-Za-z0-9_-]+\.wasm$/,
handler: "CacheFirst",
options: {
cacheName: "exiftool-fallback-wasm",
expiration: {
maxEntries: 2,
maxAgeSeconds: 30 * 24 * 60 * 60,
},
},
},
],
},
includeAssets: ["icon-192.png", "icon-512.png"],
devOptions: {
enabled: true,
},
}),
],
});