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>
111 lines
3.8 KiB
TypeScript
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,
|
|
},
|
|
}),
|
|
],
|
|
});
|