Skip to content

Commit 8b0d42b

Browse files
fry69bartlomiejuclaude
authored
fix: register files generated by Vite plugins in static file cache (#3534)
Fixes #3487 --------- Co-authored-by: Bartek Iwańczuk <biwanczuk@gmail.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 400e217 commit 8b0d42b

6 files changed

Lines changed: 145 additions & 1 deletion

File tree

packages/plugin-vite/deno.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
"rollup-plugin-visualizer": "npm:rollup-plugin-visualizer@^6.0.3",
4343
"stripe": "npm:stripe@^19.1.0",
4444
"vite": "npm:vite@^7.1.4",
45-
"vite-plugin-inspect": "npm:vite-plugin-inspect@^11.3.2"
45+
"vite-plugin-inspect": "npm:vite-plugin-inspect@^11.3.2",
46+
"vite-plugin-pwa": "npm:vite-plugin-pwa@^1.0.3"
4647
}
4748
}

packages/plugin-vite/src/plugins/server_snapshot.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -318,6 +318,47 @@ export function serverSnapshot(options: ResolvedFreshViteConfig): Plugin[] {
318318
}
319319
}
320320
}
321+
322+
// Scan client output directory for any additional files generated
323+
// by Vite plugins (e.g., vite-plugin-pwa generates sw.js,
324+
// manifest.webmanifest) that aren't in the Vite manifest or
325+
// public directory.
326+
if (await fsAdapter.isDirectory(clientOutDir)) {
327+
// Normalize registered pathnames (strip leading /) for comparison
328+
const registeredPaths = new Set(
329+
staticFiles.map((f) =>
330+
f.pathname.startsWith("/") ? f.pathname.slice(1) : f.pathname
331+
),
332+
);
333+
334+
const clientFiles = await fsAdapter.walk(
335+
clientOutDir,
336+
{
337+
followSymlinks: false,
338+
includeDirs: false,
339+
includeFiles: true,
340+
},
341+
);
342+
343+
for await (const entry of clientFiles) {
344+
const relative = path.relative(clientOutDir, entry.path);
345+
346+
// Skip .vite directory and already-registered files
347+
if (
348+
relative.startsWith(".vite/") ||
349+
relative === ".vite" ||
350+
registeredPaths.has(relative)
351+
) {
352+
continue;
353+
}
354+
355+
staticFiles.push({
356+
filePath: entry.path,
357+
hash: null,
358+
pathname: relative,
359+
});
360+
}
361+
}
321362
}
322363

323364
const code = await generateSnapshotServer({

packages/plugin-vite/tests/build_test.ts

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -664,3 +664,65 @@ Deno.test({
664664
sanitizeOps: false,
665665
sanitizeResources: false,
666666
});
667+
668+
Deno.test({
669+
name: "vite build - vite-plugin-pwa generates service worker",
670+
fn: async () => {
671+
const fixture = path.join(FIXTURE_DIR, "vite_plugin_pwa");
672+
await using res = await buildVite(fixture);
673+
674+
// Verify that vite-plugin-pwa generated the expected files in _fresh/client
675+
const swPath = path.join(res.tmp, "_fresh", "client", "sw.js");
676+
const manifestPath = path.join(
677+
res.tmp,
678+
"_fresh",
679+
"client",
680+
"manifest.webmanifest",
681+
);
682+
683+
// Check that files were generated
684+
const swStat = await Deno.stat(swPath);
685+
expect(swStat.isFile).toEqual(true);
686+
687+
const manifestStat = await Deno.stat(manifestPath);
688+
expect(manifestStat.isFile).toEqual(true);
689+
},
690+
sanitizeOps: false,
691+
sanitizeResources: false,
692+
});
693+
694+
Deno.test({
695+
name: "vite build - vite-plugin-pwa files are accessible via HTTP",
696+
fn: async () => {
697+
const fixture = path.join(FIXTURE_DIR, "vite_plugin_pwa");
698+
await using res = await buildVite(fixture);
699+
700+
await launchProd(
701+
{ cwd: res.tmp },
702+
async (address) => {
703+
// Test that service worker is accessible
704+
const swRes = await fetch(`${address}/sw.js`);
705+
expect(swRes.status).toEqual(200);
706+
expect(swRes.headers.get("content-type")).toMatch(/javascript/);
707+
708+
const swContent = await swRes.text();
709+
expect(swContent.length).toBeGreaterThan(0);
710+
711+
// Test that manifest is accessible
712+
const manifestRes = await fetch(`${address}/manifest.webmanifest`);
713+
expect(manifestRes.status).toEqual(200);
714+
expect(manifestRes.headers.get("content-type")).toMatch(/json/);
715+
716+
const manifestContent = await manifestRes.json();
717+
expect(manifestContent.name).toEqual("Fresh PWA Test");
718+
719+
// Test that registerSW.js is accessible
720+
const registerRes = await fetch(`${address}/registerSW.js`);
721+
expect(registerRes.status).toEqual(200);
722+
expect(registerRes.headers.get("content-type")).toMatch(/javascript/);
723+
},
724+
);
725+
},
726+
sanitizeOps: false,
727+
sanitizeResources: false,
728+
});
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { App, staticFiles } from "@fresh/core";
2+
3+
export const app = new App()
4+
.use(staticFiles())
5+
.fsRoutes();
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
export default function Home() {
2+
return (
3+
<div>
4+
<h1>PWA Test</h1>
5+
<p>Testing vite-plugin-pwa integration</p>
6+
</div>
7+
);
8+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { defineConfig } from "vite";
2+
import { fresh } from "@fresh/plugin-vite";
3+
import { VitePWA } from "vite-plugin-pwa";
4+
5+
export default defineConfig({
6+
plugins: [
7+
fresh(),
8+
VitePWA({
9+
// Minimal configuration to generate PWA files
10+
registerType: "autoUpdate",
11+
includeAssets: [],
12+
manifest: {
13+
name: "Fresh PWA Test",
14+
short_name: "PWA Test",
15+
description: "Testing vite-plugin-pwa with Fresh",
16+
theme_color: "#ffffff",
17+
},
18+
workbox: {
19+
globPatterns: ["**/*.{js,css,html}"],
20+
},
21+
devOptions: {
22+
enabled: true,
23+
type: "module",
24+
},
25+
}),
26+
],
27+
});

0 commit comments

Comments
 (0)