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/tender-scissors-sip.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sveltejs/adapter-cloudflare': minor
---

feat: allow custom `include` and `exclude` rules in `_routes.json`
21 changes: 20 additions & 1 deletion documentation/docs/25-build-and-deploy/60-adapter-cloudflare.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,30 @@ import adapter from '@sveltejs/adapter-cloudflare';

export default {
kit: {
adapter: adapter()
adapter: adapter({
// See below for an explanation of these options
routes: {
include: ['/*'],
exclude: ['<all>']
}
})
}
};
```

## Options

The `routes` option allows you to customise the [`_routes.json`](https://developers.cloudflare.com/pages/platform/functions/routing/#create-a-_routesjson-file) file generated by `adapter-cloudflare`.

- `include` defines routes that will invoke a function, and defaults to `['/*']`
- `exclude` defines routes that will _not_ invoke a function — this is a faster and cheaper way to serve your app's static assets. This array can include the following special values:
- `<build>` contains your app's build artifacts (the files generated by Vite)
- `<files>` contains the contents of your `static` directory
- `<prerendered>` contains a list of prerendered pages
- `<all>` (the default) contains all of the above

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', ...]`.

## Deployment

Please follow the [Get Started Guide](https://developers.cloudflare.com/pages/get-started) for Cloudflare Pages to begin.
Expand Down
31 changes: 30 additions & 1 deletion packages/adapter-cloudflare/index.d.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,36 @@
import { Adapter } from '@sveltejs/kit';
import './ambient.js';

export default function plugin(): Adapter;
export default function plugin(options?: AdapterOptions): Adapter;

export interface AdapterOptions {
/**
* Customize the automatically-generated `_routes.json` file
* https://developers.cloudflare.com/pages/platform/functions/routing/#create-a-_routesjson-file
*/
routes?: {
/**
* Routes that will be invoked by functions. Accepts wildcards.
* @default ["/*"]
*/
include?: string[];

/**
* Routes that will not be invoked by functions. Accepts wildcards.
* `exclude` takes priority over `include`.
*
* To have the adapter automatically exclude certain things, you can use these placeholders:
*
* - `<build>` to exclude build artifacts (files generated by Vite)
* - `<files>` for the contents of your `static` directory
* - `<prerendered>` for prerendered routes
* - `<all>` to exclude all of the above
*
* @default ["<all>"]
*/
exclude?: string[];
};
}

export interface RoutesJSONSpec {
version: 1;
Expand Down
90 changes: 51 additions & 39 deletions packages/adapter-cloudflare/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { fileURLToPath } from 'node:url';
import * as esbuild from 'esbuild';

/** @type {import('.').default} */
export default function () {
export default function (options = {}) {
return {
name: '@sveltejs/adapter-cloudflare',
async adapt(builder) {
Expand All @@ -30,7 +30,7 @@ export default function () {

writeFileSync(
`${dest}/_routes.json`,
JSON.stringify(get_routes_json(builder, written_files))
JSON.stringify(get_routes_json(builder, written_files, options.routes ?? {}), null, '\t')
);

writeFileSync(`${dest}/_headers`, generate_headers(builder.config.kit.appDir), { flag: 'a' });
Expand Down Expand Up @@ -60,56 +60,68 @@ export default function () {
/**
* @param {import('@sveltejs/kit').Builder} builder
* @param {string[]} assets
* @param {import('./index').AdapterOptions['routes']} routes
* @returns {import('.').RoutesJSONSpec}
*/
function get_routes_json(builder, assets) {
/**
* The list of routes that will _not_ invoke functions (which cost money).
* This is done on a best-effort basis, as there is a limit of 100 rules
*/
const exclude = [
`/${builder.config.kit.appDir}/*`,
...assets
.filter(
(file) =>
!(
file.startsWith(`${builder.config.kit.appDir}/`) ||
file === '_headers' ||
file === '_redirects'
)
)
.map((file) => `/${file}`)
];

const MAX_EXCLUSIONS = 99; // 100 minus existing `include` rules
let excess;

if (exclude.length > MAX_EXCLUSIONS) {
excess = 'static assets';
function get_routes_json(builder, assets, { include = ['/*'], exclude = ['<all>'] }) {
if (!Array.isArray(include) || !Array.isArray(exclude)) {
throw new Error(`routes.include and routes.exclude must be arrays`);
}

if (builder.prerendered.paths.length > 0) {
excess += ' or prerendered routes';
}
} else if (exclude.length + builder.prerendered.paths.length > MAX_EXCLUSIONS) {
excess = 'prerendered routes';
if (include.length === 0) {
throw new Error(`routes.include must contain at least one route`);
}

for (const path of builder.prerendered.paths) {
if (!builder.prerendered.redirects.has(path)) {
exclude.push(path);
}
if (include.length > 100) {
throw new Error(`routes.include must contain 100 or fewer routes`);
}

if (excess) {
const message = `Static file count exceeds _routes.json limits (see https://developers.cloudflare.com/pages/platform/functions/routing/#limits). Accessing some ${excess} will cause function invocations.`;
exclude = exclude
.flatMap((rule) => (rule === '<all>' ? ['<build>', '<files>', '<prerendered>'] : rule))
.flatMap((rule) => {
if (rule === '<build>') {
return `/${builder.config.kit.appDir}/*`;
}

if (rule === '<files>') {
return assets
.filter(
(file) =>
!(
file.startsWith(`${builder.config.kit.appDir}/`) ||
file === '_headers' ||
file === '_redirects'
)
)
.map((file) => `/${file}`);
}

if (rule === '<prerendered>') {
const prerendered = [];
for (const path of builder.prerendered.paths) {
if (!builder.prerendered.redirects.has(path)) {
prerendered.push(path);
}
}

return prerendered;
}

return rule;
});

const excess = include.length + exclude.length - 100;
if (excess > 0) {
const message = `Function includes/excludes exceeds _routes.json limits (see https://developers.cloudflare.com/pages/platform/functions/routing/#limits). Dropping ${excess} exclude rules — this will cause unnecessary function invocations.`;
builder.log.warn(message);
exclude.length = 99;

exclude.length -= excess;
}

return {
version: 1,
description: 'Generated by @sveltejs/adapter-cloudflare',
include: ['/*'],
include,
exclude
};
}
Expand Down