Skip to content

feat: allow additional handlers to be exported by adapter-cloudflare #13739

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
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/empty-feet-approve.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sveltejs/adapter-cloudflare': minor
---

Enable the Cloudflare Workers adapter to export additional handlers.
23 changes: 23 additions & 0 deletions documentation/docs/25-build-and-deploy/60-adapter-cloudflare.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<prerendered>` 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
Expand Down
5 changes: 5 additions & 0 deletions packages/adapter-cloudflare/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,11 @@ export interface AdapterOptions {
* during development and preview.
*/
platformProxy?: GetPlatformProxyOptions;

/**
*
*/
worker?: string;
}

export interface RoutesJSONSpec {
Expand Down
13 changes: 12 additions & 1 deletion packages/adapter-cloudflare/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -91,14 +91,25 @@ 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))}/exports.js`;
}

builder.copy(`${files}/worker.js`, worker_dest, {
replace: {
// the paths returned by the Wrangler config might be Windows paths,
// so we need to convert them to POSIX paths or else the backslashes
// 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
}
});

Expand Down
9 changes: 9 additions & 0 deletions packages/adapter-cloudflare/internal.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<ExportedHandler, 'fetch'> | WorkerEntrypoint;

export default handlers;
}
2 changes: 1 addition & 1 deletion packages/adapter-cloudflare/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
158 changes: 87 additions & 71 deletions packages/adapter-cloudflare/src/worker.js
Original file line number Diff line number Diff line change
@@ -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);

Expand All @@ -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<Response>}
*/
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<Response>}
*/
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
});
Loading