From 7445bf931ec00b841178b9560a4712beda1027ec Mon Sep 17 00:00:00 2001 From: Thomas Foster Date: Thu, 24 Apr 2025 23:19:08 +1000 Subject: [PATCH 1/3] Implement additional handlers. --- .../60-adapter-cloudflare.md | 23 +++ packages/adapter-cloudflare/index.d.ts | 5 + packages/adapter-cloudflare/index.js | 13 +- packages/adapter-cloudflare/internal.d.ts | 9 + packages/adapter-cloudflare/src/worker.js | 158 ++++++++++-------- 5 files changed, 136 insertions(+), 72 deletions(-) diff --git a/documentation/docs/25-build-and-deploy/60-adapter-cloudflare.md b/documentation/docs/25-build-and-deploy/60-adapter-cloudflare.md index b078b25ee4fa..cdbf6384dbcc 100644 --- a/documentation/docs/25-build-and-deploy/60-adapter-cloudflare.md +++ b/documentation/docs/25-build-and-deploy/60-adapter-cloudflare.md @@ -77,6 +77,29 @@ Only for Cloudflare Pages. Allows you to customise the [`_routes.json`](https:// You can have up to 100 `include` and `exclude` rules combined. Generally you can omit the `routes` options, but if (for example) your `` paths exceed that limit, you may find it helpful to manually create an `exclude` list that includes `'/articles/*'` instead of the auto-generated `['/articles/foo', '/articles/bar', '/articles/baz', ...]`. +### worker + +Path to a file with additional [handlers](https://developers.cloudflare.com/workers/runtime-apis/handlers/) export alongside the SvelteKit-generated `fetch()` handler. Enables integration of, for example, `scheduled()` or `queue()` handlers with your SvelteKit app. + +Default: `undefined`- no additional handlers are exported. + +The worker file should export a default object with any additional handlers. Example below: + +```js +// @errors: 2307 2377 7006 +/// file: src/worker.js +export default { + async scheduled(event, env, ctx) { + console.log("Scheduled trigger!"); + }, + // additional handlers go here +} +``` + +> [!NOTE] The adapter expects the `handlers` file to have a default export. + +> [!NOTE] The adapter will overwrite any [fetch handler](https://developers.cloudflare.com/workers/runtime-apis/handlers/fetch/) exported from the `worker` file in the generated worker. Most uses for a fetch handler are covered by endpoints or server hooks, so you should use those instead. + ## Cloudflare Workers ### Basic configuration diff --git a/packages/adapter-cloudflare/index.d.ts b/packages/adapter-cloudflare/index.d.ts index 0c02fb786cee..3540d11ee8ae 100644 --- a/packages/adapter-cloudflare/index.d.ts +++ b/packages/adapter-cloudflare/index.d.ts @@ -65,6 +65,11 @@ export interface AdapterOptions { * during development and preview. */ platformProxy?: GetPlatformProxyOptions; + + /** + * + */ + worker?: string; } export interface RoutesJSONSpec { diff --git a/packages/adapter-cloudflare/index.js b/packages/adapter-cloudflare/index.js index af99452ef8fd..614b249c56d8 100644 --- a/packages/adapter-cloudflare/index.js +++ b/packages/adapter-cloudflare/index.js @@ -91,6 +91,16 @@ export default function (options = {}) { `export const prerendered = new Set(${JSON.stringify(builder.prerendered.paths)});\n\n` + `export const base_path = ${JSON.stringify(builder.config.kit.paths.base)};\n` ); + + let exports_path; + + if (!building_for_cloudflare_pages && options.worker) { + exports_path = `${posixify(path.relative(worker_dest_dir, path.resolve(process.cwd(), options.worker)))}`; + } else { + writeFileSync(`${tmp}/exports.js`, 'export default {};\n\n'); + exports_path = `${posixify(path.relative(worker_dest_dir, tmp))}/manifest.js`; + } + builder.copy(`${files}/worker.js`, worker_dest, { replace: { // the paths returned by the Wrangler config might be Windows paths, @@ -98,7 +108,8 @@ export default function (options = {}) { // will be interpreted as escape characters and create an incorrect import path SERVER: `${posixify(path.relative(worker_dest_dir, builder.getServerDirectory()))}/index.js`, MANIFEST: `${posixify(path.relative(worker_dest_dir, tmp))}/manifest.js`, - ASSETS: assets_binding + ASSETS: assets_binding, + WORKER: exports_path } }); diff --git a/packages/adapter-cloudflare/internal.d.ts b/packages/adapter-cloudflare/internal.d.ts index 6c79569f7f7f..b0437c714de2 100644 --- a/packages/adapter-cloudflare/internal.d.ts +++ b/packages/adapter-cloudflare/internal.d.ts @@ -10,3 +10,12 @@ declare module 'MANIFEST' { export const app_path: string; export const base_path: string; } + +declare module 'WORKER' { + import { ExportedHandler } from '@cloudflare/workers-types'; + import { WorkerEntrypoint } from 'cloudflare:workers'; + + const handlers: Omit | WorkerEntrypoint; + + export default handlers; +} diff --git a/packages/adapter-cloudflare/src/worker.js b/packages/adapter-cloudflare/src/worker.js index a2ea423c1653..d6e98151f276 100644 --- a/packages/adapter-cloudflare/src/worker.js +++ b/packages/adapter-cloudflare/src/worker.js @@ -1,6 +1,8 @@ import { Server } from 'SERVER'; import { manifest, prerendered, base_path } from 'MANIFEST'; +import handlers from 'WORKER'; import * as Cache from 'worktop/cfw.cache'; +import { WorkerEntrypoint } from 'cloudflare:workers'; const server = new Server(manifest); @@ -9,82 +11,96 @@ const app_path = `/${manifest.appPath}`; const immutable = `${app_path}/immutable/`; const version_file = `${app_path}/version.json`; -export default { - /** - * @param {Request} req - * @param {{ ASSETS: { fetch: typeof fetch } }} env - * @param {ExecutionContext} context - * @returns {Promise} - */ - async fetch(req, env, context) { - await server.init({ - // @ts-expect-error env contains environment variables and bindings - env - }); +export * from 'WORKER'; - // skip cache if "cache-control: no-cache" in request - let pragma = req.headers.get('cache-control') || ''; - let res = !pragma.includes('no-cache') && (await Cache.lookup(req)); - if (res) return res; +/** + * @param {Request} req + * @param {{ ASSETS: { fetch: typeof globalThis.fetch } }} env + * @param {ExecutionContext} context + * @returns {Promise} + */ +async function fetch(req, env, context) { + await server.init({ + // @ts-expect-error env contains environment variables and bindings + env + }); - let { pathname, search } = new URL(req.url); - try { - pathname = decodeURIComponent(pathname); - } catch { - // ignore invalid URI - } + // skip cache if "cache-control: no-cache" in request + let pragma = req.headers.get('cache-control') || ''; + let res = !pragma.includes('no-cache') && (await Cache.lookup(req)); + if (res) return res; - const stripped_pathname = pathname.replace(/\/$/, ''); + let { pathname, search } = new URL(req.url); + try { + pathname = decodeURIComponent(pathname); + } catch { + // ignore invalid URI + } - // files in /static, the service worker, and Vite imported server assets - let is_static_asset = false; - const filename = stripped_pathname.slice(base_path.length + 1); - if (filename) { - is_static_asset = - manifest.assets.has(filename) || - manifest.assets.has(filename + '/index.html') || - filename in manifest._.server_assets || - filename + '/index.html' in manifest._.server_assets; - } + const stripped_pathname = pathname.replace(/\/$/, ''); - let location = pathname.at(-1) === '/' ? stripped_pathname : pathname + '/'; + // files in /static, the service worker, and Vite imported server assets + let is_static_asset = false; + const filename = stripped_pathname.slice(base_path.length + 1); + if (filename) { + is_static_asset = + manifest.assets.has(filename) || + manifest.assets.has(filename + '/index.html') || + filename in manifest._.server_assets || + filename + '/index.html' in manifest._.server_assets; + } - if ( - is_static_asset || - prerendered.has(pathname) || - pathname === version_file || - pathname.startsWith(immutable) - ) { - res = await env.ASSETS.fetch(req); - } else if (location && prerendered.has(location)) { - // trailing slash redirect for prerendered pages - if (search) location += search; - res = new Response('', { - status: 308, - headers: { - location - } - }); - } else { - // dynamically-generated pages - res = await server.respond(req, { - platform: { - env, - context, - // @ts-expect-error webworker types from worktop are not compatible with Cloudflare Workers types - caches, - // @ts-expect-error the type is correct but ts is confused because platform.cf uses the type from index.ts while req.cf uses the type from index.d.ts - cf: req.cf - }, - getClientAddress() { - return req.headers.get('cf-connecting-ip'); - } - }); - } + let location = pathname.at(-1) === '/' ? stripped_pathname : pathname + '/'; - // write to `Cache` only if response is not an error, - // let `Cache.save` handle the Cache-Control and Vary headers - pragma = res.headers.get('cache-control') || ''; - return pragma && res.status < 400 ? Cache.save(req, res, context) : res; + if ( + is_static_asset || + prerendered.has(pathname) || + pathname === version_file || + pathname.startsWith(immutable) + ) { + res = await env.ASSETS.fetch(req); + } else if (location && prerendered.has(location)) { + // trailing slash redirect for prerendered pages + if (search) location += search; + res = new Response('', { + status: 308, + headers: { + location + } + }); + } else { + // dynamically-generated pages + res = await server.respond(req, { + platform: { + env, + context, + // @ts-expect-error webworker types from worktop are not compatible with Cloudflare Workers types + caches, + // @ts-expect-error the type is correct but ts is confused because platform.cf uses the type from index.ts while req.cf uses the type from index.d.ts + cf: req.cf + }, + getClientAddress() { + return req.headers.get('cf-connecting-ip'); + } + }); } -}; + + // write to `Cache` only if response is not an error, + // let `Cache.save` handle the Cache-Control and Vary headers + pragma = res.headers.get('cache-control') || ''; + return pragma && res.status < 400 ? Cache.save(req, res, context) : res; +} + +export default 'prototype' in handlers && handlers.prototype instanceof WorkerEntrypoint + ? Object.defineProperty(handlers.prototype, 'fetch', { + value: fetch, + writable: true, + enumerable: false, + configurable: true + }) + : Object.defineProperty(handlers, 'fetch', { + value: fetch, + writable: true, + enumerable: true, + configurable: true + }); From b984c528ff24ea9b282a701b6d6a7a38c56e5afe Mon Sep 17 00:00:00 2001 From: Thomas Foster Date: Sat, 10 May 2025 03:04:35 +0000 Subject: [PATCH 2/3] Add changeset, fix check issues. --- .changeset/empty-feet-approve.md | 5 +++++ packages/adapter-cloudflare/package.json | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 .changeset/empty-feet-approve.md diff --git a/.changeset/empty-feet-approve.md b/.changeset/empty-feet-approve.md new file mode 100644 index 000000000000..1fb8ef133b5f --- /dev/null +++ b/.changeset/empty-feet-approve.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/adapter-cloudflare': minor +--- + +Enable the Cloudflare Workers adapter to export additional handlers. diff --git a/packages/adapter-cloudflare/package.json b/packages/adapter-cloudflare/package.json index 411d947b62b6..488ca899b60f 100644 --- a/packages/adapter-cloudflare/package.json +++ b/packages/adapter-cloudflare/package.json @@ -33,7 +33,7 @@ "ambient.d.ts" ], "scripts": { - "build": "esbuild src/worker.js --bundle --outfile=files/worker.js --external:SERVER --external:MANIFEST --format=esm", + "build": "esbuild src/worker.js --bundle --outfile=files/worker.js --external:SERVER --external:MANIFEST --external:WORKER --external:\"cloudflare:workers\" --format=esm", "lint": "prettier --check .", "format": "pnpm lint --write", "check": "tsc --skipLibCheck", From ee87b35fd489cbb9e70ac76646a0ce1846f72280 Mon Sep 17 00:00:00 2001 From: Thomas Foster Date: Sat, 10 May 2025 03:47:17 +0000 Subject: [PATCH 3/3] Fix incorrect import in generated fallback worker. --- packages/adapter-cloudflare/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/adapter-cloudflare/index.js b/packages/adapter-cloudflare/index.js index 614b249c56d8..8b1a360fb20e 100644 --- a/packages/adapter-cloudflare/index.js +++ b/packages/adapter-cloudflare/index.js @@ -98,7 +98,7 @@ export default function (options = {}) { exports_path = `${posixify(path.relative(worker_dest_dir, path.resolve(process.cwd(), options.worker)))}`; } else { writeFileSync(`${tmp}/exports.js`, 'export default {};\n\n'); - exports_path = `${posixify(path.relative(worker_dest_dir, tmp))}/manifest.js`; + exports_path = `${posixify(path.relative(worker_dest_dir, tmp))}/exports.js`; } builder.copy(`${files}/worker.js`, worker_dest, {