From f75adfe6a3d71f9629d6ad73f844e128506aa62c Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sun, 12 Apr 2026 19:03:07 +0300 Subject: [PATCH] fix(standalone): PDFjs not working in dev mode --- apps/client-standalone/src/sw.ts | 18 ++++-- apps/client-standalone/vite.config.mts | 83 ++++++++++++++++++++++---- 2 files changed, 87 insertions(+), 14 deletions(-) diff --git a/apps/client-standalone/src/sw.ts b/apps/client-standalone/src/sw.ts index 630d09acd9..4e5b5831f7 100644 --- a/apps/client-standalone/src/sw.ts +++ b/apps/client-standalone/src/sw.ts @@ -85,12 +85,22 @@ async function networkFirst(request) { } } -async function forwardToClientLocalServer(request, clientId) { - // Find a client to handle the request (prefer the initiating client if available) - let client = clientId ? await self.clients.get(clientId) : null; +async function forwardToClientLocalServer(request, _clientId) { + // Find the main app window to handle the request + // We must route to the main app (which has the local bridge), not iframes like PDF.js viewer + // @ts-expect-error - self.clients is valid in service worker context + const all = await self.clients.matchAll({ type: "window", includeUncontrolled: true }); + // Find the main app window - it's the one NOT serving pdfjs or other embedded content + // The main app has the local bridge handler for LOCAL_FETCH messages + let client = all.find((c: { url: string }) => { + const url = new URL(c.url); + // Main app is at root or index.html, not in /pdfjs/ or other iframe paths + return !url.pathname.startsWith("/pdfjs/"); + }) || null; + + // If no main app window found, fall back to any available client if (!client) { - const all = await self.clients.matchAll({ type: "window", includeUncontrolled: true }); client = all[0] || null; } diff --git a/apps/client-standalone/vite.config.mts b/apps/client-standalone/vite.config.mts index ea92dd7855..0d5a7a9c9d 100644 --- a/apps/client-standalone/vite.config.mts +++ b/apps/client-standalone/vite.config.mts @@ -1,7 +1,9 @@ -import prefresh from '@prefresh/vite'; -import { join } from 'path'; -import { defineConfig } from 'vite'; -import { viteStaticCopy } from 'vite-plugin-static-copy'; +import fs from "fs"; +import { join } from "path"; + +import prefresh from "@prefresh/vite"; +import { defineConfig, type Plugin } from "vite"; +import { viteStaticCopy } from "vite-plugin-static-copy"; const clientAssets = ["assets", "stylesheets", "fonts", "translations"]; @@ -9,15 +11,15 @@ const isDev = process.env.NODE_ENV === "development"; // Watch client files and trigger reload in development const clientWatchPlugin = () => ({ - name: 'client-watch', + name: "client-watch", configureServer(server: any) { if (isDev) { // Watch client source files (adjusted for new root) - server.watcher.add('../../client/src/**/*'); - server.watcher.on('change', (file: string) => { - if (file.includes('../../client/src/')) { + server.watcher.add("../../client/src/**/*"); + server.watcher.on("change", (file: string) => { + if (file.includes("../../client/src/")) { server.ws.send({ - type: 'full-reload' + type: "full-reload" }); } }); @@ -25,6 +27,53 @@ const clientWatchPlugin = () => ({ } }); +// Serve PDF.js files directly in dev mode to bypass SPA fallback +const pdfjsServePlugin = (): Plugin => ({ + name: "pdfjs-serve", + configureServer(server) { + const pdfjsRoot = join(__dirname, "../../packages/pdfjs-viewer/dist"); + + server.middlewares.use((req, res, next) => { + if (!req.url?.startsWith("/pdfjs/")) { + return next(); + } + + // Map /pdfjs/web/... to dist/web/... + // Map /pdfjs/build/... to dist/build/... + // Strip query string (e.g., ?v=0.102.2) before resolving path + const urlWithoutQuery = req.url.split("?")[0]; + const relativePath = urlWithoutQuery.replace(/^\/pdfjs\//, ""); + const filePath = join(pdfjsRoot, relativePath); + + // Security: ensure we're still within pdfjsRoot + if (!filePath.startsWith(pdfjsRoot)) { + return next(); + } + + if (fs.existsSync(filePath) && fs.statSync(filePath).isFile()) { + const ext = filePath.split(".").pop() || ""; + const mimeTypes: Record = { + html: "text/html", + css: "text/css", + js: "application/javascript", + mjs: "application/javascript", + wasm: "application/wasm", + png: "image/png", + svg: "image/svg+xml", + json: "application/json" + }; + res.setHeader("Content-Type", mimeTypes[ext] || "application/octet-stream"); + // Match isolation headers from main page for iframe compatibility + res.setHeader("Cross-Origin-Opener-Policy", "same-origin"); + res.setHeader("Cross-Origin-Embedder-Policy", "require-corp"); + fs.createReadStream(filePath).pipe(res); + } else { + next(); + } + }); + } +}); + // Always copy SQLite WASM files so they're available to the module const sqliteWasmPlugin = viteStaticCopy({ targets: [ @@ -65,10 +114,24 @@ let plugins: any = [ } ] }), + // PDF.js viewer for PDF preview support + viteStaticCopy({ + targets: [ + { + src: join(__dirname, "../../packages/pdfjs-viewer/dist/web/**/*"), + dest: "pdfjs/web", + }, + { + src: join(__dirname, "../../packages/pdfjs-viewer/dist/build/**/*"), + dest: "pdfjs/build", + } + ] + }), // Watch client files for changes in development ...(isDev ? [ prefresh(), - clientWatchPlugin() + clientWatchPlugin(), + pdfjsServePlugin() ] : []) ];