Skip to content

Commit 90bfdda

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 7fd675f commit 90bfdda

File tree

5 files changed

+74
-7
lines changed

5 files changed

+74
-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: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,9 @@ 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+
include: string[];
9+
exclude: string[];
10+
}

packages/adapter-cloudflare/index.js

Lines changed: 56 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,42 @@ 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+
include: ['/*'],
78+
exclude: [
79+
`/${build_data.app_dir}/immutable/*`,
80+
...build_data.manifest_data.assets
81+
// We're being conservative by not excluding all assets in
82+
// /static just yet. If there are any upstream auth rules to
83+
// protect certain things (e.g. a PDF that requires auth),
84+
// then we wouldn't want to prevent those requests from going
85+
// to the user functions worker.
86+
// We do want to show an example of a _routes.json that
87+
// excludes more than just /_app/immutable/*, and favicons
88+
// are a reasonable choice
89+
.filter(({ file }) => file.includes('favicon'))
90+
.map((asset) => asset.file)
91+
]
92+
};
93+
}
94+
95+
/**
96+
* @param {import('../kit/types/internal').BuildData} build_data
97+
* @returns {string}
98+
*/
99+
function getHeadersFromBuildData(build_data) {
100+
return `
101+
# === START AUTOGENERATED SVELTE IMMUTABLE HEADERS ===
102+
/${build_data.app_dir}/immutable/*
103+
Cache-Control: public, immutable, max-age=31536000
104+
X-Robots-Tag: noindex
105+
# === END AUTOGENERATED SVELTE IMMUTABLE HEADERS ===
106+
`.trim();
107+
}

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,10 @@ export function create_builder({ config, build_data, prerendered, log }) {
151151
});
152152
},
153153

154+
getBuildData() {
155+
return build_data;
156+
},
157+
154158
getBuildDirectory(name) {
155159
return `${config.kit.outDir}/${name}`;
156160
},

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 interface Adapter {
@@ -56,6 +56,7 @@ export interface Builder {
5656

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

59+
getBuildData(): BuildData;
5960
getBuildDirectory(name: string): string;
6061
getClientDirectory(): string;
6162
getServerDirectory(): string;

0 commit comments

Comments
 (0)