Skip to content
Draft
Show file tree
Hide file tree
Changes from 5 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
4 changes: 2 additions & 2 deletions packages/kit/src/exports/public.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -834,9 +834,9 @@ export interface KitConfig {
* Whether to automatically register the service worker, if it exists.
* @default true
*/
register: true;
register?: true;
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This fixes a type issue where you can't specify options unless you specifically set register: true although that's the default

/**
* Options for serviceWorker.register("...", options);
* Options passed to the automatic service worker registration `serviceWorker.register("...", options);`
*/
options?: RegistrationOptions;
}
Expand Down
3 changes: 2 additions & 1 deletion packages/kit/src/exports/vite/dev/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down
270 changes: 143 additions & 127 deletions packages/kit/src/exports/vite/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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} */
Expand All @@ -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.
Expand All @@ -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);
Expand Down Expand Up @@ -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}`;
}
},
Expand Down Expand Up @@ -907,11 +877,8 @@ function kit({ svelte_config }) {
let client_manifest;
/** @type {import('types').Prerendered} */
let prerendered;

/** @type {Set<string>} */
let build;
/** @type {string} */
let service_worker_code;
/** @type {string | null} */
let service_worker_entry_file;

/**
* Creates the service worker virtual modules
Expand All @@ -920,72 +887,149 @@ 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');
}

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 === 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];
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 `<base>/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 `<base>/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.`
);
}
};

Expand Down Expand Up @@ -1224,29 +1268,6 @@ function kit({ svelte_config }) {
},
publicDir: kit.files.assets
};

if (service_worker_entry_file) {
/** @type {Record<string, import('vite').EnvironmentOptions>} */ (
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',
Expand Down Expand Up @@ -1577,12 +1598,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 = [
Expand Down
2 changes: 2 additions & 0 deletions packages/kit/src/runtime/server/page/render.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
Expand Down
Loading
Loading