From 3e741a9516ccc8b2edb9d002ca054b2be96287fd Mon Sep 17 00:00:00 2001 From: Tee Ming Date: Sun, 29 Mar 2026 19:03:26 +0800 Subject: [PATCH 1/9] add env --- packages/kit/src/exports/vite/dev/index.js | 3 +- packages/kit/src/exports/vite/index.js | 273 ++++++++++-------- .../test/apps/basics/src/service-worker.js | 7 + .../test/apps/options-2/src/service-worker.js | 12 +- packages/kit/test/apps/options-2/test/test.js | 13 - 5 files changed, 164 insertions(+), 144 deletions(-) diff --git a/packages/kit/src/exports/vite/dev/index.js b/packages/kit/src/exports/vite/dev/index.js index 52923f56e880..0722a4c56c63 100644 --- a/packages/kit/src/exports/vite/dev/index.js +++ b/packages/kit/src/exports/vite/dev/index.js @@ -475,10 +475,11 @@ export async function dev(vite, vite_config, svelte_config, get_remotes, root) { const resolved = resolve_entry(svelte_config.kit.files.serviceWorker); if (resolved) { + const transformed = await vite.environments.serviceWorker.transformRequest(resolved); res.writeHead(200, { 'content-type': 'application/javascript' }); - res.end(`import '${svelte_config.kit.paths.base}${to_fs(resolved)}';`); + res.end(transformed?.code); } else { res.writeHead(404); res.end('not found'); diff --git a/packages/kit/src/exports/vite/index.js b/packages/kit/src/exports/vite/index.js index 17cc04de9110..f3b93c143bee 100644 --- a/packages/kit/src/exports/vite/index.js +++ b/packages/kit/src/exports/vite/index.js @@ -4,6 +4,7 @@ import process from 'node:process'; import { styleText } from 'node:util'; import { exactRegex, prefixRegex } from 'rolldown/filter'; +import { createFetchableDevEnvironment, normalizePath } from 'vite'; import { copy, mkdirp, posixify, read, resolve_entry, rimraf } from '../../utils/filesystem.js'; import { create_static_module, create_dynamic_module } from '../../core/env.js'; @@ -260,11 +261,6 @@ function kit({ svelte_config }) { /** @type {import('vite').UserConfig} */ let initial_config; - /** @type {string | null} */ - let service_worker_entry_file; - /** @type {import('node:path').ParsedPath} */ - let parsed_service_worker; - /** @type {string} */ let normalized_cwd; /** @type {string} */ @@ -285,10 +281,6 @@ function kit({ svelte_config }) { const plugin_setup = { name: 'vite-plugin-sveltekit-setup', - applyToEnvironment(environment) { - return environment.name !== 'serviceWorker'; - }, - // TODO: add `order: pre` to avoid false-positive warnings of overridden config options set by Vitest /** * Build the SvelteKit-provided Vite config to be merged with the user's vite.config.js file. @@ -306,9 +298,6 @@ function kit({ svelte_config }) { env = get_env(kit.env, vite_config_env.mode); - service_worker_entry_file = resolve_entry(kit.files.serviceWorker); - parsed_service_worker = path.parse(kit.files.serviceWorker); - vite = await import_peer('vite', root); normalized_cwd = vite.normalizePath(root); @@ -473,44 +462,25 @@ function kit({ svelte_config }) { return environment.name !== 'serviceWorker'; }, - resolveId(id, importer) { - if (id === '__sveltekit/manifest') { - return `${kit.outDir}/generated/client-optimized/app.js`; - } - - // If importing from a service-worker, only allow $service-worker & $env/static/public, but none of the other virtual modules. - // This check won't catch transitive imports, but it will warn when the import comes from a service-worker directly. - // Transitive imports will be caught during the build. - // TODO move this logic to plugin_guard. add a filter to this resolveId when doing so - if (importer) { - const parsed_importer = path.parse(importer); - - const importer_is_service_worker = - parsed_importer.dir === parsed_service_worker.dir && - parsed_importer.name === parsed_service_worker.name; - - if (importer_is_service_worker && id !== '$service-worker' && id !== '$env/static/public') { - throw new Error( - `Cannot import ${normalize_id( - id, - normalized_lib, - normalized_cwd - )} into service-worker code. Only the modules $service-worker and $env/static/public are available in service workers.` - ); + resolveId: { + filter: { + id: [prefixRegex('$env/'), exactRegex('$service-worker'), prefixRegex('__sveltekit/')] + }, + handler(id) { + // treat $env/static/[public|private] as virtual + if (id.startsWith('$env/') || id === '$service-worker') { + // ids with :$ don't work with reverse proxies like nginx + return `\0virtual:${id.substring(1)}`; } - } - // treat $env/static/[public|private] as virtual - if (id.startsWith('$env/') || id === '$service-worker') { - // ids with :$ don't work with reverse proxies like nginx - return `\0virtual:${id.substring(1)}`; - } + if (id === '__sveltekit/manifest') { + return `${kit.outDir}/generated/client-optimized/app.js`; + } - if (id === '__sveltekit/remote') { - return `${runtime_directory}/client/remote-functions/index.js`; - } + if (id === '__sveltekit/remote') { + return `${runtime_directory}/client/remote-functions/index.js`; + } - if (id.startsWith('__sveltekit/')) { return `\0virtual:${id}`; } }, @@ -907,11 +877,8 @@ function kit({ svelte_config }) { let client_manifest; /** @type {import('types').Prerendered} */ let prerendered; - - /** @type {Set} */ - let build; - /** @type {string} */ - let service_worker_code; + /** @type {string | null} */ + let service_worker_entry_file; /** * Creates the service worker virtual modules @@ -920,72 +887,152 @@ function kit({ svelte_config }) { const plugin_service_worker = { name: 'vite-plugin-sveltekit-service-worker', + config(config) { + service_worker_entry_file = resolve_entry(kit.files.serviceWorker); + + if (!service_worker_entry_file) return; + + if (kit.paths.assets) { + throw new Error('Cannot use service worker alongside config.kit.paths.assets'); + } + + // TODO: in the future we can use unbundled service workers as the default + // it became baseline in January 2026 https://caniuse.com/wf-js-modules-service-workers + + const conditions = ['worker', 'browser', 'production|development']; + + /** @type {import('vite').UserConfig} */ + const new_config = { + environments: { + serviceWorker: { + consumer: 'client', + resolve: { + conditions + // TODO: bundle the worker in development once noExternal is supported for client environments + // noExternal: kit.serviceWorker.type === 'module' ? undefined : true + }, + optimizeDeps: { + // Note: ssr pre-bundling is opt-in and we need to enable it by setting `noDiscovery` to false + // noDiscovery: false, + // Workaround for https://github.com/vitejs/vite/issues/20867 + // Longer term solution is to use full-bundle mode rather than `optimizeDeps` + // ignoreOutdatedRequests: true, + entries: normalizePath(kit.files.serviceWorker), + rolldownOptions: { + platform: 'neutral', + resolve: { + conditionNames: conditions + } + } + }, + dev: { + createEnvironment(name, config) { + return createFetchableDevEnvironment(name, config, { + hot: false, + handleRequest() { + throw new Error( + 'This should never happen. The service worker environment does not handle requests directly' + ); + } + }); + } + }, + build: { + modulePreload: false, + rolldownOptions: { + input: { + 'service-worker': service_worker_entry_file + }, + output: { + entryFileNames: 'service-worker.js', + assetFileNames: `${kit.appDir}/immutable/assets/[name].[hash][extname]`, + codeSplitting: false + } + }, + outDir: `${out}/client`, + minify: initial_config.build?.minify + } + } + } + }; + + warn_overridden_config(config, new_config); + + return new_config; + }, + applyToEnvironment(environment) { return environment.name === 'serviceWorker'; }, - resolveId(id) { - if (id.startsWith('$env/') || id.startsWith('$app/') || id === '$service-worker') { + resolveId: { + order: 'pre', + filter: { + id: [prefixRegex('$env/'), prefixRegex('$app/'), exactRegex('$service-worker')] + }, + handler(id) { // ids with :$ don't work with reverse proxies like nginx return `\0virtual:${id.substring(1)}`; } }, - load(id) { - if (!build) { - build = new Set(); - for (const key in client_manifest) { - const { file, css = [], assets = [] } = client_manifest[key]; - build.add(file); - css.forEach((file) => build.add(file)); - assets.forEach((file) => build.add(file)); - } + load: { + filter: { + id: [/^(?!\\0virtual:).*/, exactRegex(env_static_public), exactRegex(service_worker)] + }, + handler(id) { + if (id === service_worker_entry_file) return; + + if (id === service_worker) { + const build = new Set(); + for (const key in client_manifest) { + const { file, css = [], assets = [] } = client_manifest[key]; + build.add(file); + css.forEach((file) => build.add(file)); + assets.forEach((file) => build.add(file)); + } - // in a service worker, `location` is the location of the service worker itself, - // which is guaranteed to be `/service-worker.js` - const base = "location.pathname.split('/').slice(0, -1).join('/')"; + // in a service worker, `location` is the location of the service worker itself, + // which is guaranteed to be `/service-worker.js` + const base = "location.pathname.split('/').slice(0, -1).join('/')"; - service_worker_code = dedent` - export const base = /*@__PURE__*/ ${base}; + // in dev, this doesn't exist, so we need to create it + manifest_data ??= sync.all(svelte_config, vite_config_env.mode, root).manifest_data; - export const build = [ - ${Array.from(build) - .map((file) => `base + ${s(`/${file}`)}`) - .join(',\n')} - ]; + return dedent` + export const base = /*@__PURE__*/ ${base}; - export const files = [ - ${manifest_data.assets - .filter((asset) => kit.serviceWorker.files(asset.file)) - .map((asset) => `base + ${s(`/${asset.file}`)}`) - .join(',\n')} - ]; + export const build = [ + ${Array.from(build) + .map((file) => `base + ${s(`/${file}`)}`) + .join(',\n')} + ]; - export const prerendered = [ - ${prerendered.paths.map((path) => `base + ${s(path.replace(kit.paths.base, ''))}`).join(',\n')} - ]; + export const files = [ + ${manifest_data.assets + .filter((asset) => kit.serviceWorker.files(asset.file)) + .map((asset) => `base + ${s(`/${asset.file}`)}`) + .join(',\n')} + ]; - export const version = ${s(kit.version.name)}; - `; - } + export const prerendered = [ + ${prerendered?.paths.map((path) => `base + ${s(path.replace(kit.paths.base, ''))}`).join(',\n')} + ]; - if (!id.startsWith('\0virtual:')) return; + export const version = ${s(kit.version.name)}; + `; + } - if (id === service_worker) { - return service_worker_code; - } + if (id === env_static_public) { + return create_static_module('$env/static/public', env.public); + } - if (id === env_static_public) { - return create_static_module('$env/static/public', env.public); + const relative = normalize_id(id, normalized_lib, normalized_cwd); + const stripped = strip_virtual_prefix(relative); + throw new Error( + `Cannot import ${stripped} into service-worker code. Only the modules $service-worker and $env/static/public are available in service workers.` + ); } - - const normalized_cwd = vite.normalizePath(vite_config.root); - const normalized_lib = vite.normalizePath(kit.files.lib); - const relative = normalize_id(id, normalized_lib, normalized_cwd); - const stripped = strip_virtual_prefix(relative); - throw new Error( - `Cannot import ${stripped} into service-worker code. Only the modules $service-worker and $env/static/public are available in service workers.` - ); } }; @@ -1224,29 +1271,6 @@ function kit({ svelte_config }) { }, publicDir: kit.files.assets }; - - if (service_worker_entry_file) { - /** @type {Record} */ ( - new_config.environments - ).serviceWorker = { - build: { - modulePreload: false, - rolldownOptions: { - input: { - 'service-worker': service_worker_entry_file - }, - output: { - entryFileNames: 'service-worker.js', - assetFileNames: `${kit.appDir}/immutable/assets/[name].[hash][extname]`, - codeSplitting: false - } - }, - outDir: `${out}/client`, - minify: initial_config.build?.minify - }, - consumer: 'client' - }; - } } else { new_config = { appType: 'custom', @@ -1577,12 +1601,7 @@ function kit({ svelte_config }) { ); if (service_worker_entry_file) { - if (kit.paths.assets) { - throw new Error('Cannot use service worker alongside config.kit.paths.assets'); - } - - log.info('Building service worker'); - + // mirror client settings that we can't set in the environment config earlier builder.environments.serviceWorker.config.define = builder.environments.client.config.define; builder.environments.serviceWorker.config.resolve.alias = [ diff --git a/packages/kit/test/apps/basics/src/service-worker.js b/packages/kit/test/apps/basics/src/service-worker.js index fc3dbc27600f..52dda00c86f8 100644 --- a/packages/kit/test/apps/basics/src/service-worker.js +++ b/packages/kit/test/apps/basics/src/service-worker.js @@ -1,3 +1,10 @@ +/// +/// +/// + +/// +/// + import { build, version } from '$service-worker'; import { PUBLIC_STATIC } from '$env/static/public'; diff --git a/packages/kit/test/apps/options-2/src/service-worker.js b/packages/kit/test/apps/options-2/src/service-worker.js index 5d2346ffc333..84bce78f173f 100644 --- a/packages/kit/test/apps/options-2/src/service-worker.js +++ b/packages/kit/test/apps/options-2/src/service-worker.js @@ -1,6 +1,15 @@ +/// +/// +/// + +/// + import { base, build, version } from '$service-worker'; import src from './image.jpg?url'; +// This gives `self` the correct types +const self = /** @type {ServiceWorkerGlobalScope} */ (/** @type {unknown} */ (globalThis.self)); + //@ts-ignore self.base = base; //@ts-ignore @@ -11,12 +20,10 @@ self.image_src = src; const name = `cache-${version}`; self.addEventListener('install', (event) => { - // @ts-expect-error event.waitUntil(caches.open(name).then((cache) => cache.addAll(build))); }); self.addEventListener('activate', (event) => { - // @ts-expect-error event.waitUntil( caches.keys().then(async (keys) => { for (const key of keys) { @@ -27,7 +34,6 @@ self.addEventListener('activate', (event) => { }); self.addEventListener('fetch', (event) => { - // @ts-expect-error const { request } = event; if (request.method !== 'GET' || request.headers.has('range')) return; diff --git a/packages/kit/test/apps/options-2/test/test.js b/packages/kit/test/apps/options-2/test/test.js index 02d78b60e359..c89dd18f4adf 100644 --- a/packages/kit/test/apps/options-2/test/test.js +++ b/packages/kit/test/apps/options-2/test/test.js @@ -1,4 +1,3 @@ -import path from 'node:path'; import process from 'node:process'; import { expect } from '@playwright/test'; import { test } from '../../../utils.js'; @@ -123,18 +122,6 @@ test.describe('trailing slash', () => { }); test.describe('Service worker', () => { - if (process.env.DEV) { - test('import proxy /basepath/service-worker.js', async ({ request }) => { - const response = await request.get('/basepath/service-worker.js'); - const content = await response.text(); - expect(content).toEqual( - `import '${path.join('/basepath', '/@fs', import.meta.dirname, '../src/service-worker.js')}';` - ); - }); - - return; - } - test('build /basepath/service-worker.js', async ({ baseURL, request }) => { const response = await request.get('/basepath/service-worker.js'); const content = await response.text(); From cff828a94fe3629f13d09b288f9933e0c271876b Mon Sep 17 00:00:00 2001 From: Tee Ming Date: Sun, 29 Mar 2026 19:03:30 +0800 Subject: [PATCH 2/9] fix types --- packages/kit/src/exports/public.d.ts | 4 ++-- packages/kit/types/index.d.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/kit/src/exports/public.d.ts b/packages/kit/src/exports/public.d.ts index 8124d09ebd55..7dd0b07e593d 100644 --- a/packages/kit/src/exports/public.d.ts +++ b/packages/kit/src/exports/public.d.ts @@ -834,9 +834,9 @@ export interface KitConfig { * Whether to automatically register the service worker, if it exists. * @default true */ - register: true; + register?: true; /** - * Options for serviceWorker.register("...", options); + * Options passed to the automatic service worker registration `serviceWorker.register("...", options);` */ options?: RegistrationOptions; } diff --git a/packages/kit/types/index.d.ts b/packages/kit/types/index.d.ts index 3ddb59f94727..c2e8b180a19e 100644 --- a/packages/kit/types/index.d.ts +++ b/packages/kit/types/index.d.ts @@ -808,9 +808,9 @@ declare module '@sveltejs/kit' { * Whether to automatically register the service worker, if it exists. * @default true */ - register: true; + register?: true; /** - * Options for serviceWorker.register("...", options); + * Options passed to the automatic service worker registration `serviceWorker.register("...", options);` */ options?: RegistrationOptions; } From 48877f2dad23429f22445217cc5b1fe856854948 Mon Sep 17 00:00:00 2001 From: Tee Ming Date: Sun, 29 Mar 2026 19:12:56 +0800 Subject: [PATCH 3/9] comment --- packages/kit/src/exports/vite/index.js | 3 --- packages/kit/src/runtime/server/page/render.js | 2 ++ 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/kit/src/exports/vite/index.js b/packages/kit/src/exports/vite/index.js index f3b93c143bee..544f3009bfc7 100644 --- a/packages/kit/src/exports/vite/index.js +++ b/packages/kit/src/exports/vite/index.js @@ -896,9 +896,6 @@ function kit({ svelte_config }) { throw new Error('Cannot use service worker alongside config.kit.paths.assets'); } - // TODO: in the future we can use unbundled service workers as the default - // it became baseline in January 2026 https://caniuse.com/wf-js-modules-service-workers - const conditions = ['worker', 'browser', 'production|development']; /** @type {import('vite').UserConfig} */ diff --git a/packages/kit/src/runtime/server/page/render.js b/packages/kit/src/runtime/server/page/render.js index 104cc1de5b1b..3ae1607be64e 100644 --- a/packages/kit/src/runtime/server/page/render.js +++ b/packages/kit/src/runtime/server/page/render.js @@ -596,6 +596,8 @@ export async function render_response({ } if (options.service_worker) { + // TODO: in the future we could register service workers as a module by default + // it became baseline in January 2026 https://caniuse.com/wf-js-modules-service-workers let opts = DEV ? ", { type: 'module' }" : ''; if (options.service_worker_options != null) { const service_worker_options = { ...options.service_worker_options }; From 93e7a618baf5c1a99cb5488112ca9834480069b6 Mon Sep 17 00:00:00 2001 From: Tee Ming Date: Sun, 29 Mar 2026 19:17:41 +0800 Subject: [PATCH 4/9] don't error on handling service worker entry file --- packages/kit/src/exports/vite/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/kit/src/exports/vite/index.js b/packages/kit/src/exports/vite/index.js index 544f3009bfc7..e841b9a5f76b 100644 --- a/packages/kit/src/exports/vite/index.js +++ b/packages/kit/src/exports/vite/index.js @@ -978,7 +978,7 @@ function kit({ svelte_config }) { id: [/^(?!\\0virtual:).*/, exactRegex(env_static_public), exactRegex(service_worker)] }, handler(id) { - if (id === service_worker_entry_file) return; + if (id === kit.files.serviceWorker || id === service_worker_entry_file) return; if (id === service_worker) { const build = new Set(); From 24f7bf6d0b48ff1ad63313b3bf6570c417ae9870 Mon Sep 17 00:00:00 2001 From: Tee Ming Date: Sun, 29 Mar 2026 19:41:24 +0800 Subject: [PATCH 5/9] Apply suggestion from @teemingc --- packages/kit/src/exports/vite/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/kit/src/exports/vite/index.js b/packages/kit/src/exports/vite/index.js index e841b9a5f76b..f70a8a1741fd 100644 --- a/packages/kit/src/exports/vite/index.js +++ b/packages/kit/src/exports/vite/index.js @@ -975,7 +975,7 @@ function kit({ svelte_config }) { load: { filter: { - id: [/^(?!\\0virtual:).*/, exactRegex(env_static_public), exactRegex(service_worker)] + id: [/^\\0virtual:/, exactRegex(env_static_public), exactRegex(service_worker)] }, handler(id) { if (id === kit.files.serviceWorker || id === service_worker_entry_file) return; From 169f840b0e03fd57c92b42ac6abf12c2742a7dad Mon Sep 17 00:00:00 2001 From: Tee Ming Date: Sun, 29 Mar 2026 21:30:54 +0800 Subject: [PATCH 6/9] run server guard for service worker too --- packages/kit/src/exports/vite/index.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/kit/src/exports/vite/index.js b/packages/kit/src/exports/vite/index.js index f70a8a1741fd..c87f646ac210 100644 --- a/packages/kit/src/exports/vite/index.js +++ b/packages/kit/src/exports/vite/index.js @@ -584,10 +584,6 @@ function kit({ svelte_config }) { // are added to the module graph enforce: 'pre', - applyToEnvironment(environment) { - return environment.name !== 'serviceWorker'; - }, - resolveId: { // TODO: use composable filter API here when supported: // https://github.com/vitejs/rolldown-vite/issues/605 From 1a5a35ab1fd1634c2cc06abcda240decb9436db9 Mon Sep 17 00:00:00 2001 From: Tee Ming Date: Sun, 29 Mar 2026 21:45:35 +0800 Subject: [PATCH 7/9] fix build --- packages/kit/src/exports/vite/index.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/kit/src/exports/vite/index.js b/packages/kit/src/exports/vite/index.js index c87f646ac210..8a67bdd6deaf 100644 --- a/packages/kit/src/exports/vite/index.js +++ b/packages/kit/src/exports/vite/index.js @@ -943,7 +943,9 @@ function kit({ svelte_config }) { } }, outDir: `${out}/client`, - minify: initial_config.build?.minify + minify: initial_config.build?.minify, + // avoid overwriting the client build Vite manifest + manifest: '.vite/service-worker-manifest.json' } } } @@ -959,7 +961,6 @@ function kit({ svelte_config }) { }, resolveId: { - order: 'pre', filter: { id: [prefixRegex('$env/'), prefixRegex('$app/'), exactRegex('$service-worker')] }, From 56d928f3a9d76151e4dfab52597b07cf74fc6175 Mon Sep 17 00:00:00 2001 From: Tee Ming Date: Sun, 29 Mar 2026 22:13:12 +0800 Subject: [PATCH 8/9] real fix --- packages/kit/src/exports/vite/index.js | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/packages/kit/src/exports/vite/index.js b/packages/kit/src/exports/vite/index.js index 8a67bdd6deaf..5ef35632a6c3 100644 --- a/packages/kit/src/exports/vite/index.js +++ b/packages/kit/src/exports/vite/index.js @@ -464,11 +464,11 @@ function kit({ svelte_config }) { resolveId: { filter: { - id: [prefixRegex('$env/'), exactRegex('$service-worker'), prefixRegex('__sveltekit/')] + id: [prefixRegex('$env/'), prefixRegex('__sveltekit/')] }, handler(id) { // treat $env/static/[public|private] as virtual - if (id.startsWith('$env/') || id === '$service-worker') { + if (id.startsWith('$env/')) { // ids with :$ don't work with reverse proxies like nginx return `\0virtual:${id.substring(1)}`; } @@ -622,7 +622,12 @@ function kit({ svelte_config }) { ] }, handler(id) { - if (this.environment.config.consumer !== 'client') return; + if ( + this.environment.config.consumer !== 'client' || + (this.environment.name === 'serviceWorker' && id === env_dynamic_private) + ) { + return; + } // skip .server.js files outside the cwd or in node_modules, as the filename might not mean 'server-only module' in this context const is_internal = @@ -972,7 +977,7 @@ function kit({ svelte_config }) { load: { filter: { - id: [/^\\0virtual:/, exactRegex(env_static_public), exactRegex(service_worker)] + id: [/^\0virtual:/, exactRegex(env_static_public), exactRegex(service_worker)] }, handler(id) { if (id === kit.files.serviceWorker || id === service_worker_entry_file) return; From 97baddafd9966f1e3325cba5d69bb62f6634c589 Mon Sep 17 00:00:00 2001 From: Tee Ming Date: Tue, 31 Mar 2026 10:33:49 +0800 Subject: [PATCH 9/9] wip --- .../src/exports/vite/build/service_worker.js | 32 +++++ packages/kit/src/exports/vite/dev/index.js | 17 ++- packages/kit/src/exports/vite/index.js | 109 +++++------------- packages/kit/test/apps/options-2/.gitignore | 1 + .../test/apps/options-2/src/service-worker.js | 6 +- packages/kit/test/apps/options-2/test/test.js | 46 +++++--- 6 files changed, 109 insertions(+), 102 deletions(-) create mode 100644 packages/kit/src/exports/vite/build/service_worker.js create mode 100644 packages/kit/test/apps/options-2/.gitignore diff --git a/packages/kit/src/exports/vite/build/service_worker.js b/packages/kit/src/exports/vite/build/service_worker.js new file mode 100644 index 000000000000..3d0691b87fe4 --- /dev/null +++ b/packages/kit/src/exports/vite/build/service_worker.js @@ -0,0 +1,32 @@ +/** + * @param {import("vite").ViteBuilder} builder + * @param {import("vite").Alias[]} config_aliases + * @returns {Promise} + */ +export async function build_service_worker(builder, config_aliases) { + // mirror client settings that we couldn't set per environment in the config hook + builder.environments.serviceWorker.config.define = builder.environments.client.config.define; + builder.environments.serviceWorker.config.resolve.alias = [...config_aliases]; + + // we have to overwrite this because it can't be configured per environment in the config hook + builder.environments.serviceWorker.config.experimental.renderBuiltUrl = (filename) => { + return { + runtime: `new URL(${JSON.stringify(filename)}, location.href).pathname` + }; + }; + + // TODO: use Vite's dev full-bundle mode when it's out + const build = /** @type {import('vite').Rolldown.RolldownOutput} */ ( + await builder.build(builder.environments.serviceWorker) + ); + + const chunk = build.output.find( + (chunk) => chunk.type === 'chunk' && chunk.fileName === 'service-worker.js' + ); + + if (chunk?.type !== 'chunk') { + throw new Error('Failed to find the service-worker chunk'); + } + + return chunk.code; +} diff --git a/packages/kit/src/exports/vite/dev/index.js b/packages/kit/src/exports/vite/dev/index.js index 0722a4c56c63..043ad72d2eb1 100644 --- a/packages/kit/src/exports/vite/dev/index.js +++ b/packages/kit/src/exports/vite/dev/index.js @@ -4,7 +4,7 @@ import { URL } from 'node:url'; import { AsyncLocalStorage } from 'node:async_hooks'; import { styleText } from 'node:util'; import sirv from 'sirv'; -import { isCSSRequest, loadEnv, buildErrorMessage } from 'vite'; +import { isCSSRequest, loadEnv, buildErrorMessage, createBuilder } from 'vite'; import { createReadableStream, getRequest, setResponse } from '../../../exports/node/index.js'; import { coalesce_to_error } from '../../../utils/error.js'; import { from_fs, posixify, resolve_entry, to_fs } from '../../../utils/filesystem.js'; @@ -17,6 +17,7 @@ import { not_found } from '../utils.js'; import { SCHEME } from '../../../utils/url.js'; import { check_feature } from '../../../utils/features.js'; import { escape_html } from '../../../utils/escape.js'; +import { build_service_worker } from '../build/service_worker.js'; // vite-specifc queries that we should skip handling for css urls const vite_css_query_regex = /(?:\?|&)(?:raw|url|inline)(?:&|$)/; @@ -27,9 +28,10 @@ const vite_css_query_regex = /(?:\?|&)(?:raw|url|inline)(?:&|$)/; * @param {import('types').ValidatedConfig} svelte_config * @param {() => Array<{ hash: string, file: string }>} get_remotes * @param {string} root The project root directory + * @param {import('vite').Alias[]} config_aliases * @return {Promise void>>} */ -export async function dev(vite, vite_config, svelte_config, get_remotes, root) { +export async function dev(vite, vite_config, svelte_config, get_remotes, root, config_aliases) { const async_local_storage = new AsyncLocalStorage(); globalThis.__SVELTEKIT_TRACK__ = (label) => { @@ -432,6 +434,13 @@ export async function dev(vite, vite_config, svelte_config, get_remotes, root) { const env = loadEnv(vite_config.mode, svelte_config.kit.env.dir, ''); const emulator = await svelte_config.kit.adapter?.emulate?.(); + const builder = await createBuilder({ + build: { + write: false, + watch: {} + } + }); + return () => { const serve_static_middleware = vite.middlewares.stack.find( (middleware) => @@ -475,11 +484,11 @@ export async function dev(vite, vite_config, svelte_config, get_remotes, root) { const resolved = resolve_entry(svelte_config.kit.files.serviceWorker); if (resolved) { - const transformed = await vite.environments.serviceWorker.transformRequest(resolved); + const service_worker_code = await build_service_worker(builder, config_aliases); res.writeHead(200, { 'content-type': 'application/javascript' }); - res.end(transformed?.code); + res.end(service_worker_code); } else { res.writeHead(404); res.end('not found'); diff --git a/packages/kit/src/exports/vite/index.js b/packages/kit/src/exports/vite/index.js index 5ef35632a6c3..e82e6ef971dc 100644 --- a/packages/kit/src/exports/vite/index.js +++ b/packages/kit/src/exports/vite/index.js @@ -4,12 +4,10 @@ import process from 'node:process'; import { styleText } from 'node:util'; import { exactRegex, prefixRegex } from 'rolldown/filter'; -import { createFetchableDevEnvironment, normalizePath } from 'vite'; import { copy, mkdirp, posixify, read, resolve_entry, rimraf } from '../../utils/filesystem.js'; import { create_static_module, create_dynamic_module } from '../../core/env.js'; import * as sync from '../../core/sync/sync.js'; -import { create_assets } from '../../core/sync/create_manifest_data/index.js'; import { runtime_directory, logger } from '../../core/utils.js'; import { generate_manifest } from '../../core/generate_manifest/index.js'; import { build_server_nodes } from './build/build_server.js'; @@ -44,6 +42,7 @@ import { import_peer } from '../../utils/import.js'; import { compact } from '../../utils/array.js'; import { should_ignore, has_children } from './static_analysis/utils.js'; import { load_config } from '../../core/config/index.js'; +import { build_service_worker } from './build/service_worker.js'; const cwd = process.cwd(); @@ -277,6 +276,9 @@ function kit({ svelte_config }) { const sourcemapIgnoreList = /** @param {string} relative_path */ (relative_path) => relative_path.includes('node_modules') || relative_path.includes(kit.outDir); + /** @type {import('vite').Alias[]} The user's aliases set in svelte.config.js */ + let config_aliases; + /** @type {import('vite').Plugin} */ const plugin_setup = { name: 'vite-plugin-sveltekit-setup', @@ -320,6 +322,8 @@ function kit({ svelte_config }) { const generated = path.posix.join(kit.outDir, 'generated'); + config_aliases = get_config_aliases(kit, root); + // dev and preview config can be shared /** @type {import('vite').UserConfig} */ const new_config = { @@ -327,7 +331,7 @@ function kit({ svelte_config }) { alias: [ { find: '__SERVER__', replacement: `${generated}/server` }, { find: '$app', replacement: `${runtime_directory}/app` }, - ...get_config_aliases(kit, root) + ...config_aliases ] }, server: { @@ -464,11 +468,11 @@ function kit({ svelte_config }) { resolveId: { filter: { - id: [prefixRegex('$env/'), prefixRegex('__sveltekit/')] + id: [prefixRegex('$env/'), prefixRegex('__sveltekit/'), exactRegex(service_worker)] }, handler(id) { // treat $env/static/[public|private] as virtual - if (id.startsWith('$env/')) { + if (id.startsWith('$env/') || id === service_worker) { // ids with :$ don't work with reverse proxies like nginx return `\0virtual:${id.substring(1)}`; } @@ -528,7 +532,7 @@ function kit({ svelte_config }) { } case service_worker: - return create_service_worker_module(svelte_config); + throw new Error('$service-worker can only be imported inside a service worker'); case sveltekit_environment: { const { version } = svelte_config.kit; @@ -874,8 +878,8 @@ function kit({ svelte_config }) { } }; - /** @type {import('vite').Manifest} */ - let client_manifest; + /** @type {import('vite').Manifest} Only available when the client build begins */ + let vite_client_manifest; /** @type {import('types').Prerendered} */ let prerendered; /** @type {string | null} */ @@ -904,40 +908,13 @@ function kit({ svelte_config }) { environments: { serviceWorker: { consumer: 'client', - resolve: { - conditions - // TODO: bundle the worker in development once noExternal is supported for client environments - // noExternal: kit.serviceWorker.type === 'module' ? undefined : true - }, - optimizeDeps: { - // Note: ssr pre-bundling is opt-in and we need to enable it by setting `noDiscovery` to false - // noDiscovery: false, - // Workaround for https://github.com/vitejs/vite/issues/20867 - // Longer term solution is to use full-bundle mode rather than `optimizeDeps` - // ignoreOutdatedRequests: true, - entries: normalizePath(kit.files.serviceWorker), - rolldownOptions: { - platform: 'neutral', - resolve: { - conditionNames: conditions - } - } - }, - dev: { - createEnvironment(name, config) { - return createFetchableDevEnvironment(name, config, { - hot: false, - handleRequest() { - throw new Error( - 'This should never happen. The service worker environment does not handle requests directly' - ); - } - }); - } - }, build: { modulePreload: false, rolldownOptions: { + platform: 'browser', + resolve: { + conditionNames: conditions + }, input: { 'service-worker': service_worker_entry_file }, @@ -977,15 +954,15 @@ function kit({ svelte_config }) { load: { filter: { - id: [/^\0virtual:/, exactRegex(env_static_public), exactRegex(service_worker)] + id: [/^\0virtual:/, exactRegex(env_static_public)] }, handler(id) { if (id === kit.files.serviceWorker || id === service_worker_entry_file) return; if (id === service_worker) { const build = new Set(); - for (const key in client_manifest) { - const { file, css = [], assets = [] } = client_manifest[key]; + for (const key in vite_client_manifest) { + const { file, css = [], assets = [] } = vite_client_manifest[key]; build.add(file); css.forEach((file) => build.add(file)); assets.forEach((file) => build.add(file)); @@ -1297,7 +1274,7 @@ function kit({ svelte_config }) { * @see https://vitejs.dev/guide/api-plugin.html#configureserver */ async configureServer(vite) { - return await dev(vite, vite_config, svelte_config, () => remotes, root); + return await dev(vite, vite_config, svelte_config, () => remotes, root, config_aliases); }, /** @@ -1457,14 +1434,19 @@ function kit({ svelte_config }) { } /** @type {import('vite').Manifest} */ - client_manifest = JSON.parse(read(`${out}/client/.vite/manifest.json`)); + vite_client_manifest = JSON.parse(read(`${out}/client/.vite/manifest.json`)); /** * @param {string} entry * @param {boolean} [add_dynamic_css] */ const deps_of = (entry, add_dynamic_css = false) => - find_deps(client_manifest, posixify(path.relative(root, entry)), add_dynamic_css, root); + find_deps( + vite_client_manifest, + posixify(path.relative(root, entry)), + add_dynamic_css, + root + ); if (svelte_config.kit.output.bundleStrategy === 'split') { const start = deps_of(`${runtime_directory}/client/entry.js`); @@ -1490,7 +1472,7 @@ function kit({ svelte_config }) { const entry = `${kit.outDir}/generated/client-optimized/nodes/${i}.js`; const deps = deps_of(entry, true); const file = resolve_symlinks( - client_manifest, + vite_client_manifest, `${kit.outDir}/generated/client-optimized/nodes/${i}.js`, root ).chunk.file; @@ -1565,7 +1547,7 @@ function kit({ svelte_config }) { kit, manifest_data, server_manifest, - client_manifest, + vite_client_manifest, assets_path, client_chunks, svelte_config.kit.output, @@ -1600,19 +1582,7 @@ function kit({ svelte_config }) { ); if (service_worker_entry_file) { - // mirror client settings that we can't set in the environment config earlier - builder.environments.serviceWorker.config.define = - builder.environments.client.config.define; - builder.environments.serviceWorker.config.resolve.alias = [ - ...get_config_aliases(kit, vite_config.root) - ]; - builder.environments.serviceWorker.config.experimental.renderBuiltUrl = (filename) => { - return { - runtime: `new URL(${JSON.stringify(filename)}, location.href).pathname` - }; - }; - - await builder.build(builder.environments.serviceWorker); + await build_service_worker(builder, config_aliases); } console.log( @@ -1696,22 +1666,3 @@ function find_overridden_config(config, resolved_config, enforced_config, path, } return out; } - -/** - * @param {import('types').ValidatedConfig} config - */ -const create_service_worker_module = (config) => dedent` - if (typeof self === 'undefined' || self instanceof ServiceWorkerGlobalScope === false) { - throw new Error('This module can only be imported inside a service worker'); - } - - export const build = []; - export const files = [ - ${create_assets(config) - .filter((asset) => config.kit.serviceWorker.files(asset.file)) - .map((asset) => `${s(`${config.kit.paths.base}/${asset.file}`)}`) - .join(',\n')} - ]; - export const prerendered = []; - export const version = ${s(config.kit.version.name)}; -`; diff --git a/packages/kit/test/apps/options-2/.gitignore b/packages/kit/test/apps/options-2/.gitignore new file mode 100644 index 000000000000..78a3ff3dcc6e --- /dev/null +++ b/packages/kit/test/apps/options-2/.gitignore @@ -0,0 +1 @@ +test/temp-service-worker.js \ No newline at end of file diff --git a/packages/kit/test/apps/options-2/src/service-worker.js b/packages/kit/test/apps/options-2/src/service-worker.js index 84bce78f173f..63eb293a5535 100644 --- a/packages/kit/test/apps/options-2/src/service-worker.js +++ b/packages/kit/test/apps/options-2/src/service-worker.js @@ -7,9 +7,6 @@ import { base, build, version } from '$service-worker'; import src from './image.jpg?url'; -// This gives `self` the correct types -const self = /** @type {ServiceWorkerGlobalScope} */ (/** @type {unknown} */ (globalThis.self)); - //@ts-ignore self.base = base; //@ts-ignore @@ -20,10 +17,12 @@ self.image_src = src; const name = `cache-${version}`; self.addEventListener('install', (event) => { + // @ts-expect-error event.waitUntil(caches.open(name).then((cache) => cache.addAll(build))); }); self.addEventListener('activate', (event) => { + // @ts-expect-error event.waitUntil( caches.keys().then(async (keys) => { for (const key of keys) { @@ -34,6 +33,7 @@ self.addEventListener('activate', (event) => { }); self.addEventListener('fetch', (event) => { + // @ts-expect-error const { request } = event; if (request.method !== 'GET' || request.headers.has('range')) return; diff --git a/packages/kit/test/apps/options-2/test/test.js b/packages/kit/test/apps/options-2/test/test.js index c89dd18f4adf..61cd794290ba 100644 --- a/packages/kit/test/apps/options-2/test/test.js +++ b/packages/kit/test/apps/options-2/test/test.js @@ -1,4 +1,6 @@ import process from 'node:process'; +import fs from 'node:fs'; +import path from 'node:path'; import { expect } from '@playwright/test'; import { test } from '../../../utils.js'; @@ -126,24 +128,36 @@ test.describe('Service worker', () => { const response = await request.get('/basepath/service-worker.js'); const content = await response.text(); - const fn = new Function('self', 'location', content); - - const self = { - addEventListener: () => {}, - base: null, - build: null - }; - const pathname = '/basepath/service-worker.js'; - fn(self, { - href: baseURL + pathname, - pathname - }); - - expect(self.base).toBe('/basepath'); - expect(self.build?.[0]).toMatch(/\/basepath\/_app\/immutable\/bundle\.[\w-]+\.js/); - expect(self.image_src).toMatch(/\/basepath\/_app\/immutable\/assets\/image\.[\w-]+\.jpg/); + const temp = path.join(import.meta.dirname, 'temp-service-worker.js'); + fs.writeFileSync( + temp, + ` +const location = { + href: ${JSON.stringify(baseURL + pathname)}, + pathname: ${JSON.stringify(pathname)} +}; + +export const self = { + addEventListener: () => {}, + base: null, + build: null +}; + +${content} +` + ); + + const service_worker = await import(temp); + + expect(service_worker.self.base).toBe('/basepath'); + expect(service_worker.self.build?.[0]).toMatch( + /\/basepath\/_app\/immutable\/bundle\.[\w-]+\.js/ + ); + expect(service_worker.self.image_src).toMatch( + /\/basepath\/_app\/immutable\/assets\/image\.[\w-]+\.jpg/ + ); }); test('does not register /basepath/service-worker.js', async ({ page }) => {