Skip to content

Commit c25d8e8

Browse files
committed
feat: Cloudflare Pages _routes.json specification (#6441)
When a SvelteKit project is deployed to Cloudflare Pages, the server-side code is supported through Pages Functions (currently in beta). Unfortunately, by using Functions, each request must go through the server-side Functions code even if it doesn't need to. For instance, an immutable asset request to https://kit.svelte.dev/_app/immutable/assets/_layout-ab34ca4f.css would first have to route through Functions. This is problematic since the request would "count" as a request to Functions even though only a static asset was served. Further, there is a slight amount of added latency. This change exposes a set of includes and excludes based on static files generated.
1 parent 4f5e266 commit c25d8e8

File tree

5 files changed

+76
-7
lines changed

5 files changed

+76
-7
lines changed

.changeset/afraid-gifts-act.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@sveltejs/adapter-cloudflare': patch
3+
'@sveltejs/kit': patch
4+
---
5+
6+
Support Cloudflare Pages \_routes.json specification

packages/adapter-cloudflare/index.d.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,10 @@ import { Adapter } from '@sveltejs/kit';
22
import './ambient.js';
33

44
export default function plugin(): Adapter;
5+
6+
export interface RoutesJSONSpec {
7+
version: 1;
8+
description: string;
9+
include: string[];
10+
exclude: string[];
11+
}

packages/adapter-cloudflare/index.js

Lines changed: 57 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { writeFileSync } from 'fs';
1+
import { writeFile } from 'fs/promises';
22
import { posix } from 'path';
33
import { fileURLToPath } from 'url';
44
import * as esbuild from 'esbuild';
@@ -15,6 +15,7 @@ export default function () {
1515
return {
1616
name: '@sveltejs/adapter-cloudflare',
1717
async adapt(builder) {
18+
const build_data = builder.getBuildData();
1819
const files = fileURLToPath(new URL('./files', import.meta.url).href);
1920
const dest = builder.getBuildDirectory('cloudflare');
2021
const tmp = builder.getBuildDirectory('cloudflare-tmp');
@@ -28,13 +29,23 @@ export default function () {
2829

2930
const relativePath = posix.relative(tmp, builder.getServerDirectory());
3031

31-
writeFileSync(
32-
`${tmp}/manifest.js`,
33-
`export const manifest = ${builder.generateManifest({
34-
relativePath
35-
})};\n\nexport const prerendered = new Set(${JSON.stringify(builder.prerendered.paths)});\n`
32+
builder.log.info(
33+
`adapter-cloudfare is writing its own _headers file. If you have your own, you should duplicate the headers contained in: ${dest}/_headers`
3634
);
3735

36+
await Promise.all([
37+
writeFile(
38+
`${tmp}/manifest.js`,
39+
`export const manifest = ${builder.generateManifest({
40+
relativePath
41+
})};\n\nexport const prerendered = new Set(${JSON.stringify(
42+
builder.prerendered.paths
43+
)});\n`
44+
),
45+
writeFile(`${dest}/_routes.json`, JSON.stringify(getRoutesJSONFromBuildData(build_data))),
46+
writeFile(`${dest}/_headers`, getHeadersFromBuildData(build_data))
47+
]);
48+
3849
builder.copy(`${files}/worker.js`, `${tmp}/_worker.js`, {
3950
replace: {
4051
SERVER: `${relativePath}/index.js`,
@@ -55,3 +66,43 @@ export default function () {
5566
}
5667
};
5768
}
69+
70+
/**
71+
* @param {import('../kit/types/internal').BuildData} build_data
72+
* @returns {import('.').RoutesJSONSpec}
73+
*/
74+
function getRoutesJSONFromBuildData(build_data) {
75+
return {
76+
version: 1,
77+
description: 'Generated by @sveltejs/adapter-cloudflare',
78+
include: ['/*'],
79+
exclude: [
80+
`/${build_data.app_dir}/immutable/*`,
81+
...build_data.manifest_data.assets
82+
// We're being conservative by not excluding all assets in
83+
// /static just yet. If there are any upstream auth rules to
84+
// protect certain things (e.g. a PDF that requires auth),
85+
// then we wouldn't want to prevent those requests from going
86+
// to the user functions worker.
87+
// We do want to show an example of a _routes.json that
88+
// excludes more than just /_app/immutable/*, and favicons
89+
// are a reasonable choice
90+
.filter(({ file }) => file.includes('favicon'))
91+
.map((asset) => asset.file)
92+
]
93+
};
94+
}
95+
96+
/**
97+
* @param {import('../kit/types/internal').BuildData} build_data
98+
* @returns {string}
99+
*/
100+
function getHeadersFromBuildData(build_data) {
101+
return `
102+
# === START AUTOGENERATED SVELTE IMMUTABLE HEADERS ===
103+
/${build_data.app_dir}/immutable/*
104+
Cache-Control: public, immutable, max-age=31536000
105+
X-Robots-Tag: noindex
106+
# === END AUTOGENERATED SVELTE IMMUTABLE HEADERS ===
107+
`.trim();
108+
}

packages/kit/src/core/adapt/builder.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,10 @@ export function create_builder({ config, build_data, routes, prerendered, log })
113113
});
114114
},
115115

116+
getBuildData() {
117+
return build_data;
118+
},
119+
116120
getBuildDirectory(name) {
117121
return `${config.kit.outDir}/${name}`;
118122
},

packages/kit/types/index.d.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import {
1616
RouteDefinition,
1717
TrailingSlash
1818
} from './private.js';
19-
import { SSRNodeLoader, SSRRoute, ValidatedConfig } from './internal.js';
19+
import { BuildData, SSRNodeLoader, SSRRoute, ValidatedConfig } from './internal.js';
2020
import { HttpError, Redirect } from '../src/runtime/control.js';
2121

2222
export { PrerenderOption } from './private.js';
@@ -58,6 +58,7 @@ export interface Builder {
5858

5959
generateManifest: (opts: { relativePath: string; format?: 'esm' | 'cjs' }) => string;
6060

61+
getBuildData(): BuildData;
6162
getBuildDirectory(name: string): string;
6263
getClientDirectory(): string;
6364
getServerDirectory(): string;

0 commit comments

Comments
 (0)