Skip to content

Commit

Permalink
feat: Cloudflare Pages _routes.json specification (#6441)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
jrf0110 committed Sep 2, 2022
1 parent 4f5e266 commit c25d8e8
Show file tree
Hide file tree
Showing 5 changed files with 76 additions and 7 deletions.
6 changes: 6 additions & 0 deletions .changeset/afraid-gifts-act.md
@@ -0,0 +1,6 @@
---
'@sveltejs/adapter-cloudflare': patch
'@sveltejs/kit': patch
---

Support Cloudflare Pages \_routes.json specification
7 changes: 7 additions & 0 deletions packages/adapter-cloudflare/index.d.ts
Expand Up @@ -2,3 +2,10 @@ import { Adapter } from '@sveltejs/kit';
import './ambient.js';

export default function plugin(): Adapter;

export interface RoutesJSONSpec {
version: 1;
description: string;
include: string[];
exclude: string[];
}
63 changes: 57 additions & 6 deletions packages/adapter-cloudflare/index.js
@@ -1,4 +1,4 @@
import { writeFileSync } from 'fs';
import { writeFile } from 'fs/promises';
import { posix } from 'path';
import { fileURLToPath } from 'url';
import * as esbuild from 'esbuild';
Expand All @@ -15,6 +15,7 @@ export default function () {
return {
name: '@sveltejs/adapter-cloudflare',
async adapt(builder) {
const build_data = builder.getBuildData();
const files = fileURLToPath(new URL('./files', import.meta.url).href);
const dest = builder.getBuildDirectory('cloudflare');
const tmp = builder.getBuildDirectory('cloudflare-tmp');
Expand All @@ -28,13 +29,23 @@ export default function () {

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

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

await Promise.all([
writeFile(
`${tmp}/manifest.js`,
`export const manifest = ${builder.generateManifest({
relativePath
})};\n\nexport const prerendered = new Set(${JSON.stringify(
builder.prerendered.paths
)});\n`
),
writeFile(`${dest}/_routes.json`, JSON.stringify(getRoutesJSONFromBuildData(build_data))),
writeFile(`${dest}/_headers`, getHeadersFromBuildData(build_data))
]);

builder.copy(`${files}/worker.js`, `${tmp}/_worker.js`, {
replace: {
SERVER: `${relativePath}/index.js`,
Expand All @@ -55,3 +66,43 @@ export default function () {
}
};
}

/**
* @param {import('../kit/types/internal').BuildData} build_data
* @returns {import('.').RoutesJSONSpec}
*/
function getRoutesJSONFromBuildData(build_data) {
return {
version: 1,
description: 'Generated by @sveltejs/adapter-cloudflare',
include: ['/*'],
exclude: [
`/${build_data.app_dir}/immutable/*`,
...build_data.manifest_data.assets
// We're being conservative by not excluding all assets in
// /static just yet. If there are any upstream auth rules to
// protect certain things (e.g. a PDF that requires auth),
// then we wouldn't want to prevent those requests from going
// to the user functions worker.
// We do want to show an example of a _routes.json that
// excludes more than just /_app/immutable/*, and favicons
// are a reasonable choice
.filter(({ file }) => file.includes('favicon'))
.map((asset) => asset.file)
]
};
}

/**
* @param {import('../kit/types/internal').BuildData} build_data
* @returns {string}
*/
function getHeadersFromBuildData(build_data) {
return `
# === START AUTOGENERATED SVELTE IMMUTABLE HEADERS ===
/${build_data.app_dir}/immutable/*
Cache-Control: public, immutable, max-age=31536000
X-Robots-Tag: noindex
# === END AUTOGENERATED SVELTE IMMUTABLE HEADERS ===
`.trim();
}
4 changes: 4 additions & 0 deletions packages/kit/src/core/adapt/builder.js
Expand Up @@ -113,6 +113,10 @@ export function create_builder({ config, build_data, routes, prerendered, log })
});
},

getBuildData() {
return build_data;
},

getBuildDirectory(name) {
return `${config.kit.outDir}/${name}`;
},
Expand Down
3 changes: 2 additions & 1 deletion packages/kit/types/index.d.ts
Expand Up @@ -16,7 +16,7 @@ import {
RouteDefinition,
TrailingSlash
} from './private.js';
import { SSRNodeLoader, SSRRoute, ValidatedConfig } from './internal.js';
import { BuildData, SSRNodeLoader, SSRRoute, ValidatedConfig } from './internal.js';
import { HttpError, Redirect } from '../src/runtime/control.js';

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

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

getBuildData(): BuildData;
getBuildDirectory(name: string): string;
getClientDirectory(): string;
getServerDirectory(): string;
Expand Down

0 comments on commit c25d8e8

Please sign in to comment.