diff --git a/packages/plugin-vite/deno.json b/packages/plugin-vite/deno.json index fd9ed2302f8..e9e2e8b9671 100644 --- a/packages/plugin-vite/deno.json +++ b/packages/plugin-vite/deno.json @@ -42,6 +42,7 @@ "rollup-plugin-visualizer": "npm:rollup-plugin-visualizer@^6.0.3", "stripe": "npm:stripe@^19.1.0", "vite": "npm:vite@^7.1.4", - "vite-plugin-inspect": "npm:vite-plugin-inspect@^11.3.2" + "vite-plugin-inspect": "npm:vite-plugin-inspect@^11.3.2", + "vite-plugin-pwa": "npm:vite-plugin-pwa@^1.0.3" } } diff --git a/packages/plugin-vite/src/plugins/server_snapshot.ts b/packages/plugin-vite/src/plugins/server_snapshot.ts index 15f5c499d10..63878527088 100644 --- a/packages/plugin-vite/src/plugins/server_snapshot.ts +++ b/packages/plugin-vite/src/plugins/server_snapshot.ts @@ -318,6 +318,47 @@ export function serverSnapshot(options: ResolvedFreshViteConfig): Plugin[] { } } } + + // Scan client output directory for any additional files generated + // by Vite plugins (e.g., vite-plugin-pwa generates sw.js, + // manifest.webmanifest) that aren't in the Vite manifest or + // public directory. + if (await fsAdapter.isDirectory(clientOutDir)) { + // Normalize registered pathnames (strip leading /) for comparison + const registeredPaths = new Set( + staticFiles.map((f) => + f.pathname.startsWith("/") ? f.pathname.slice(1) : f.pathname + ), + ); + + const clientFiles = await fsAdapter.walk( + clientOutDir, + { + followSymlinks: false, + includeDirs: false, + includeFiles: true, + }, + ); + + for await (const entry of clientFiles) { + const relative = path.relative(clientOutDir, entry.path); + + // Skip .vite directory and already-registered files + if ( + relative.startsWith(".vite/") || + relative === ".vite" || + registeredPaths.has(relative) + ) { + continue; + } + + staticFiles.push({ + filePath: entry.path, + hash: null, + pathname: relative, + }); + } + } } const code = await generateSnapshotServer({ diff --git a/packages/plugin-vite/tests/build_test.ts b/packages/plugin-vite/tests/build_test.ts index 46e7948667c..0416c9f87f7 100644 --- a/packages/plugin-vite/tests/build_test.ts +++ b/packages/plugin-vite/tests/build_test.ts @@ -664,3 +664,65 @@ Deno.test({ sanitizeOps: false, sanitizeResources: false, }); + +Deno.test({ + name: "vite build - vite-plugin-pwa generates service worker", + fn: async () => { + const fixture = path.join(FIXTURE_DIR, "vite_plugin_pwa"); + await using res = await buildVite(fixture); + + // Verify that vite-plugin-pwa generated the expected files in _fresh/client + const swPath = path.join(res.tmp, "_fresh", "client", "sw.js"); + const manifestPath = path.join( + res.tmp, + "_fresh", + "client", + "manifest.webmanifest", + ); + + // Check that files were generated + const swStat = await Deno.stat(swPath); + expect(swStat.isFile).toEqual(true); + + const manifestStat = await Deno.stat(manifestPath); + expect(manifestStat.isFile).toEqual(true); + }, + sanitizeOps: false, + sanitizeResources: false, +}); + +Deno.test({ + name: "vite build - vite-plugin-pwa files are accessible via HTTP", + fn: async () => { + const fixture = path.join(FIXTURE_DIR, "vite_plugin_pwa"); + await using res = await buildVite(fixture); + + await launchProd( + { cwd: res.tmp }, + async (address) => { + // Test that service worker is accessible + const swRes = await fetch(`${address}/sw.js`); + expect(swRes.status).toEqual(200); + expect(swRes.headers.get("content-type")).toMatch(/javascript/); + + const swContent = await swRes.text(); + expect(swContent.length).toBeGreaterThan(0); + + // Test that manifest is accessible + const manifestRes = await fetch(`${address}/manifest.webmanifest`); + expect(manifestRes.status).toEqual(200); + expect(manifestRes.headers.get("content-type")).toMatch(/json/); + + const manifestContent = await manifestRes.json(); + expect(manifestContent.name).toEqual("Fresh PWA Test"); + + // Test that registerSW.js is accessible + const registerRes = await fetch(`${address}/registerSW.js`); + expect(registerRes.status).toEqual(200); + expect(registerRes.headers.get("content-type")).toMatch(/javascript/); + }, + ); + }, + sanitizeOps: false, + sanitizeResources: false, +}); diff --git a/packages/plugin-vite/tests/fixtures/vite_plugin_pwa/main.ts b/packages/plugin-vite/tests/fixtures/vite_plugin_pwa/main.ts new file mode 100644 index 00000000000..e5cf428a2cd --- /dev/null +++ b/packages/plugin-vite/tests/fixtures/vite_plugin_pwa/main.ts @@ -0,0 +1,5 @@ +import { App, staticFiles } from "@fresh/core"; + +export const app = new App() + .use(staticFiles()) + .fsRoutes(); diff --git a/packages/plugin-vite/tests/fixtures/vite_plugin_pwa/routes/index.tsx b/packages/plugin-vite/tests/fixtures/vite_plugin_pwa/routes/index.tsx new file mode 100644 index 00000000000..4767575875b --- /dev/null +++ b/packages/plugin-vite/tests/fixtures/vite_plugin_pwa/routes/index.tsx @@ -0,0 +1,8 @@ +export default function Home() { + return ( +
+

PWA Test

+

Testing vite-plugin-pwa integration

+
+ ); +} diff --git a/packages/plugin-vite/tests/fixtures/vite_plugin_pwa/vite.config.ts b/packages/plugin-vite/tests/fixtures/vite_plugin_pwa/vite.config.ts new file mode 100644 index 00000000000..46fc78cd7e9 --- /dev/null +++ b/packages/plugin-vite/tests/fixtures/vite_plugin_pwa/vite.config.ts @@ -0,0 +1,27 @@ +import { defineConfig } from "vite"; +import { fresh } from "@fresh/plugin-vite"; +import { VitePWA } from "vite-plugin-pwa"; + +export default defineConfig({ + plugins: [ + fresh(), + VitePWA({ + // Minimal configuration to generate PWA files + registerType: "autoUpdate", + includeAssets: [], + manifest: { + name: "Fresh PWA Test", + short_name: "PWA Test", + description: "Testing vite-plugin-pwa with Fresh", + theme_color: "#ffffff", + }, + workbox: { + globPatterns: ["**/*.{js,css,html}"], + }, + devOptions: { + enabled: true, + type: "module", + }, + }), + ], +});