Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/khaki-lights-design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sveltejs/kit': major
---

breaking: delete `$service-worker` module
94 changes: 56 additions & 38 deletions documentation/docs/30-advanced/40-service-workers.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,40 +4,40 @@ title: Service workers

Service workers act as proxy servers that handle network requests inside your app. This makes it possible to make your app work offline, but even if you don't need offline support (or can't realistically implement it because of the type of app you're building), it's often worth using service workers to speed up navigation by precaching your built JS and CSS.

In SvelteKit, if you have a `src/service-worker.js` file (or `src/service-worker/index.js`) it will be bundled and automatically registered.
In SvelteKit, if you have a `src/service-worker/index.ts` file it will be bundled and automatically registered.

> [!NOTE] `src/service-worker.ts` or `.js` is also valid, but see the section on [type safety](#type-safety) below

## Inside the service worker

Inside the service worker you have access to the [`$service-worker` module]($service-worker), which provides you with the paths to all static assets, build files and prerendered pages. You're also provided with an app version string, which you can use for creating a unique cache name, and the deployment's `base` path. If your Vite config specifies `define` (used for global variable replacements), this will be applied to service workers as well as your server/client builds.
For the service worker to do anything useful, you will likely need to import some stuff:

- [`$app/service-worker`]($app-service-worker) exports `self` which is just `globalThis` typed as [`ServiceWorkerGlobalScope`](https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerGlobalScope) (provided you follow [these steps](#type-safety)), so that your `fetch` events are typed correctly
- [`$app/env`]($app-env) exports `version`, which is useful for creating deployment-scoped caches
- [`$app/manifest`]($app-manifest) exports `immutable` build files, your `assets`, and any `prerendered` content, allowing you to populate your caches

The following example caches the built app and any files in `static` eagerly, and caches all other requests as they happen. This would make each page work offline once visited.
A typical service worker might look like this:

```js
// @errors: 2688
/// file: src/service-worker.js
// Disables access to DOM typings like `HTMLElement` which are not available
// inside a service worker and instantiates the correct globals
/// <reference no-default-lib="true"/>
/// <reference lib="esnext" />
/// <reference lib="webworker" />
Comment thread
elliott-with-the-longest-name-on-github marked this conversation as resolved.

// Ensures that the `$service-worker` import has proper type definitions
/// <reference types="@sveltejs/kit" />

// Only necessary if you have an import from `$app/env/*`
/// <reference types="../.svelte-kit/env.d.ts" />

import { build, files, version } from '$service-worker';

// This gives `self` the correct types
const self = /** @type {ServiceWorkerGlobalScope} */ (/** @type {unknown} */ (globalThis.self));
// ---cut---
import { self } from '$app/service-worker';
import { version } from '$app/env';
import { immutable, assets } from '$app/manifest';
import { resolve } from '$app/paths';

// Create a unique cache name for this deployment
const CACHE = `cache-${version}`;

// `immutable`/`assets` paths from `$app/manifest` are relative to the
// base path, so resolve them to absolute pathnames that can be matched
// against `url.pathname` in the `fetch` handler
const ASSETS = [
...build, // the app itself
...files // everything in `static`
...immutable.map((asset) => resolve(asset.path)), // the Vite output
...assets.map((asset) => resolve(asset.path)) // everything in `static`
];

self.addEventListener('install', (event) => {
Expand Down Expand Up @@ -69,7 +69,7 @@ self.addEventListener('fetch', (event) => {
const url = new URL(event.request.url);
const cache = await caches.open(CACHE);

// `build`/`files` can always be served from the cache
// `immutable`/`assets` can always be served from the cache
if (ASSETS.includes(url.pathname)) {
const response = await cache.match(url.pathname);

Expand All @@ -78,32 +78,26 @@ self.addEventListener('fetch', (event) => {
}
}

// for everything else, try the network first, but
// fall back to the cache if we're offline
// for everything else, try the network first...
try {
const response = await fetch(event.request);

// if we're offline, fetch can return a value that is not a Response
// instead of throwing - and we can't pass this non-Response to respondWith
if (!(response instanceof Response)) {
throw new Error('invalid response from fetch');
}

if (response.status === 200 && !response.headers.get('cache-control')?.includes('no-store')) {
cache.put(event.request, response.clone());
// ...and cache responses in the background for next time....
void cache.put(event.request, response.clone());
}

return response;
} catch (err) {
} catch (error) {
// ...otherwise fall back to previously cached data if it exists...
const response = await cache.match(event.request);

if (response) {
return response;
}

// if there's no cache, then just error out
// as there is nothing we can do to respond to this request
throw err;
// ...or throw the error
throw error;
}
}

Expand All @@ -113,18 +107,42 @@ self.addEventListener('fetch', (event) => {

> [!NOTE] Be careful when caching! In some cases, stale data might be worse than data that's unavailable while offline. Since browsers will empty caches if they get too full, you should also be careful about caching large assets like video files.

> [!NOTE] `build` and `prerendered` are empty arrays during development
## Type safety

Service workers run in a different context to the rest of your app. As such, they needs different types. You should ensure that your project's root `tsconfig.json` excludes your service worker code...

```json
/// file: tsconfig.json
{
"extends": "$app/tsconfig",
"include": ["src", "test"],
"exclude": ["src/service-worker"]
}
```
Comment thread
Rich-Harris marked this conversation as resolved.

...and that your `src/service-worker/index.ts` file sits alongside a separate `tsconfig.json`, which should set up the correct types by extending [`$app/tsconfig/service-worker`]($app-tsconfig-service-worker):

```json
/// file: src/service-worker/tsconfig.json
{
"extends": "$app/tsconfig/service-worker"
}
```

## Manual registration

You can [disable automatic registration](configuration#serviceWorker) if you need to register the service worker with your own logic. The default registration looks something like this:
You can [disable automatic registration](configuration#serviceWorker) if you need to register the service worker with your own logic. The default registration, which is injected into server-rendered HTML, looks something like this:

```js
if ('serviceWorker' in navigator) {
const script_url = './service-worker.js';
const policy = globalThis?.window?.trustedTypes?.createPolicy(
'sveltekit-trusted-url',
{ createScriptURL(url) { return url; } }
);
const sanitised = policy?.createScriptURL(script_url) ?? script_url;
addEventListener('load', function () {
navigator.serviceWorker.register('./path/to/service-worker.js', {
type: 'module'
});
navigator.serviceWorker.register(sanitised, { type: 'module' });
});
}
```
Expand Down
5 changes: 0 additions & 5 deletions documentation/docs/98-reference/27-$service-worker.md

This file was deleted.

26 changes: 26 additions & 0 deletions documentation/docs/99-legacy-reference/10-$service-worker.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
---
title: $service-worker
---

The `service-worker` module existed in SvelteKit 2, and provided access to the following exports:

## base

A root-relative path representing the application's base path. Use [`resolve(...)`]($app-paths#resolve) from `$app/paths` instead.

## build

A `string[]` array of files generated by Vite. Empty during development. Use [`immutable`]($app-manifest#immutable) from `$app/manifest` instead.

## files

A `string[]` array of files in your `static` directory. Use [`assets`]($app-manifest#assets) from `$app/manifest` instead.


## prerendered

A `string[]` array of prerendered pages. Empty during dev. Use [`prerendered`]($app-manifest#prerendered) from `$app/manifest` instead.

## version

The value of [`config.version.name`](configuration#version), used for populating caches. Use [`version`]($app/env#version) from `$app/env` instead.
3 changes: 3 additions & 0 deletions documentation/docs/99-legacy-reference/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
---
title: Legacy
---
4 changes: 0 additions & 4 deletions packages/kit/src/core/config/index.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -150,8 +150,6 @@ const get_defaults = (prefix = '') => ({
test('fills in defaults', () => {
const validated = validate_config({});

assert.equal(validated.kit.serviceWorker.files(''), true);

remove_keys(validated, ([, v]) => typeof v === 'function');

const defaults = get_defaults();
Expand Down Expand Up @@ -212,8 +210,6 @@ test('fills in partial blanks', () => {
}
});

assert.equal(validated.kit.serviceWorker.files(''), true);

remove_keys(validated, ([, v]) => typeof v === 'function');

const config = get_defaults();
Expand Down
4 changes: 2 additions & 2 deletions packages/kit/src/core/config/options.js
Original file line number Diff line number Diff line change
Expand Up @@ -275,11 +275,11 @@ export const validate_kit_options = object({
}),

serviceWorker: object({
files: removed(),
register: boolean(true),
// options could be undefined but if it is defined we only validate that
// it's an object since the type comes from the browser itself
options: validate(undefined, object({}, true)),
files: fun((filename) => !/\.DS_Store/.test(filename))
options: validate(undefined, object({}, true))
}),

tracing: object({
Expand Down
7 changes: 6 additions & 1 deletion packages/kit/src/core/env.js
Original file line number Diff line number Diff line change
Expand Up @@ -265,7 +265,11 @@ export function create_sveltekit_env_service_worker(
return dedent`
import { env } from '${base}/${app_dir}/env.js';

${global} = { env, version: ${JSON.stringify(version)} };
${global} = {
base: location.pathname.split('/').slice(0, -1).join('/'),
env,
version: ${JSON.stringify(version)}
};
`;
}

Expand Down Expand Up @@ -294,6 +298,7 @@ export function create_sveltekit_env_service_worker_dev(variables, env, version,

return dedent`
${global} = {
base: location.pathname.split('/').slice(0, -1).join('/'),
env: {
${properties.join(',\n\t\t') || '// empty'}
},
Expand Down
11 changes: 2 additions & 9 deletions packages/kit/src/exports/public.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -824,13 +824,7 @@ export interface KitConfig {
*/
resolution?: 'client' | 'server';
};
serviceWorker?: {
/**
* Determine which files in your `static` directory will be available in `$service-worker.files`.
* @default (filename) => !/\.DS_Store/.test(filename)
*/
files?: (file: string) => boolean;
} & (
serviceWorker?:
| {
/**
* Whether to automatically register the service worker, if it exists.
Expand All @@ -848,8 +842,7 @@ export interface KitConfig {
* @default true
*/
register?: false;
}
);
};
/**
* Options for enabling [OpenTelemetry](https://opentelemetry.io/) tracing for SvelteKit operations.
* @default { server: false }
Expand Down
Loading
Loading