From 72fa17a44182736e84d8769dbeedf0f612bbf2c7 Mon Sep 17 00:00:00 2001 From: Dmytro Oliinyk Date: Mon, 15 Jun 2026 15:21:10 +0200 Subject: [PATCH] Fix service worker failing to load due to 404 on chunk dependencies (#39433) --- config/vite/plugin-sw-chunk-paths.ts | 61 ++++++++++++++++++++++++++++ vite.config.mts | 2 + 2 files changed, 63 insertions(+) create mode 100644 config/vite/plugin-sw-chunk-paths.ts diff --git a/config/vite/plugin-sw-chunk-paths.ts b/config/vite/plugin-sw-chunk-paths.ts new file mode 100644 index 0000000000..bcbf468904 --- /dev/null +++ b/config/vite/plugin-sw-chunk-paths.ts @@ -0,0 +1,61 @@ +/* The service worker is built as a regular Vite entry point and is served from + `/sw.js` (a symlink to `packs/sw.js`), while the chunks it depends on live in + the build output directory (e.g. `/packs/`). + + Browsers resolve a module's static import specifiers relative to the *URL* of + the script, not its location on disk. Because the worker is served from the + root, relative specifiers such as `./chunk-abc123.js` resolve to + `/chunk-abc123.js` and return a 404, which prevents the service worker from + installing. + + This plugin rewrites those relative specifiers in the service worker output + to absolute paths pointing at the build output directory, so they resolve + correctly regardless of the URL the worker is served from. +*/ + +import type { Plugin, ResolvedConfig } from 'vite'; + +const SERVICE_WORKER_FILENAME = 'sw.js'; + +export function MastodonServiceWorkerChunkPaths(): Plugin { + let config: ResolvedConfig; + + return { + name: 'mastodon-sw-chunk-paths', + configResolved(resolvedConfig) { + config = resolvedConfig; + }, + renderChunk(code, chunk) { + if (chunk.fileName !== SERVICE_WORKER_FILENAME) { + return null; + } + + // Resolve the base from the final Vite config rather than receiving it as + // an argument, so forks or other plugins that alter `base` are respected. + // Vite normalises `base` to always include a trailing slash. + const { base } = config; + + // Drive the rewrite from the chunk's actual dependency list rather than a + // generic regex, so we only ever touch real (hashed) chunk specifiers and + // never accidentally match similar-looking text inside string literals. + const dependencies = [...chunk.imports, ...chunk.dynamicImports]; + + let rewritten = code; + for (const dependency of dependencies) { + // The worker sits at the root of the output directory, so each + // dependency is imported with a leading `./`. + rewritten = rewritten + .replaceAll(`"./${dependency}"`, `"${base}${dependency}"`) + .replaceAll(`'./${dependency}'`, `'${base}${dependency}'`); + } + + if (rewritten === code) { + return null; + } + + // Only import specifiers are changed, so the existing source map stays + // accurate enough; returning `null` avoids a spurious sourcemap warning. + return { code: rewritten, map: null }; + }, + }; +} diff --git a/vite.config.mts b/vite.config.mts index 06d6ab1990..2fa892b309 100644 --- a/vite.config.mts +++ b/vite.config.mts @@ -22,6 +22,7 @@ import svgr from 'vite-plugin-svgr'; import { MastodonAssetsManifest } from './config/vite/plugin-assets-manifest'; import { MastodonThemes } from './config/vite/plugin-mastodon-themes'; import { MastodonNameLookup } from './config/vite/plugin-name-lookup'; +import { MastodonServiceWorkerChunkPaths } from './config/vite/plugin-sw-chunk-paths'; import { MastodonServiceWorkerLocales } from './config/vite/plugin-sw-locales'; const jsRoot = path.resolve(__dirname, 'app/javascript'); @@ -185,6 +186,7 @@ export const config: UserConfigFnPromise = async ({ mode, command }) => { MastodonThemes(), MastodonAssetsManifest(), MastodonServiceWorkerLocales(), + MastodonServiceWorkerChunkPaths(), legacy({ renderLegacyChunks: false, modernPolyfills: true,