Skip to content
Draft
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
119 changes: 119 additions & 0 deletions packages/adapter-cloudflare/fallback-worker.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import { Server } from '__sveltekit/server';
import { manifest, prerendered, base_path } from '__sveltekit/ssr-manifest';
import { env } from 'cloudflare:workers';
import * as Cache from 'worktop/cfw.cache';

const server = new Server(manifest);

const app_path = `/${manifest.appPath}`;

const immutable = `${app_path}/immutable/`;
const version_file = `${app_path}/version.json`;

/**
* We don't know the origin until we receive a request, but
* that's guaranteed to happen before we call `read`
* @type {string}
*/
let origin;

const initialized = server.init({
// @ts-expect-error env contains environment variables and bindings
env,
read: async (file) => {
const url = `${origin}/${file}`;
const response = await /** @type {{ ASSETS: { fetch: typeof fetch } }} */ (env).ASSETS.fetch(
url
);

if (!response.ok) {
throw new Error(
`read(...) failed: could not fetch ${url} (${response.status} ${response.statusText})`
);
}

return response.body;
}
});

export default {
/**
* @param {Request} req
* @param {{ ASSETS: { fetch: typeof fetch } }} env
* @param {ExecutionContext} ctx
* @returns {Promise<Response>}
*/
async fetch(req, env, ctx) {
if (!origin) {
origin = new URL(req.url).origin;
}

// always await initialization to prevent race condition with concurrent requests
await initialized;

// 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;

let { pathname, search } = new URL(req.url);
try {
pathname = decodeURIComponent(pathname);
} catch {
// ignore invalid URI
}

const stripped_pathname = pathname.replace(/\/$/, '');

// 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;
}

let location = pathname.at(-1) === '/' ? stripped_pathname : pathname + '/';

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,
ctx,
// @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 /** @type {string} */ (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, ctx) : res;
}
};
64 changes: 12 additions & 52 deletions packages/adapter-cloudflare/index.d.ts
Original file line number Diff line number Diff line change
@@ -1,79 +1,39 @@
import { PluginConfig } from '@cloudflare/vite-plugin';
import { Adapter } from '@sveltejs/kit';
import './ambient.js';
import { GetPlatformProxyOptions } from 'wrangler';
import './ambient.js';

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

export interface AdapterOptions {
/**
* Path to your [Wrangler configuration file](https://developers.cloudflare.com/workers/wrangler/configuration/).
* Options to pass to the Cloudflare Vite plugin.
* @see https://developers.cloudflare.com/workers/vite-plugin/reference/api/#interface-pluginconfig
*/
config?: string;
vitePluginOptions?: PluginConfig;
/**
* Whether to render a plaintext 404.html page or a rendered SPA fallback page
* for non-matching asset requests.
*
* For Cloudflare Workers, the default behaviour is to return a null-body
* The default behaviour is to return a null-body
* 404-status response for non-matching assets requests. However, if the
* [`assets.not_found_handling`](https://developers.cloudflare.com/workers/static-assets/routing/#2-not_found_handling)
* Wrangler configuration setting is set to `"404-page"`, this page will be
* served if a request fails to match an asset. If `assets.not_found_handling`
* is set to `"single-page-application"`, the adapter will render a SPA fallback
* `index.html` page regardless of the `fallback` option specified.
*
* For Cloudflare Pages, this page will only be served when a request that
* matches an entry in `routes.exclude` fails to match an asset.
*
* Most of the time `plaintext` is sufficient, but if you are using `routes.exclude` to manually
* exclude a set of prerendered pages without exceeding the 100 route limit, you may wish to
* use `spa` instead to avoid showing an unstyled 404 page to users.
*
* See [Cloudflare Pages' Not Found behavior](https://developers.cloudflare.com/pages/configuration/serving-pages/#not-found-behavior) for more info.
*
* @default 'plaintext'
*/
fallback?: 'plaintext' | 'spa';

/**
* Only for Cloudflare Pages. Customize the automatically-generated [`_routes.json`](https://developers.cloudflare.com/pages/platform/functions/routing/#create-a-_routesjson-file) 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[];
};

/**
* Config object passed to [`getPlatformProxy`](https://developers.cloudflare.com/workers/wrangler/api/#getplatformproxy)
* during development and preview.
* @deprecated removed in 8.0.0. Use `vitePluginOptions` instead
*/
platformProxy?: GetPlatformProxyOptions;
}

/**
* The JSON format of the {@link https://developers.cloudflare.com/pages/functions/routing/#create-a-_routesjson-file | `_routes.json`}
* file that controls when the Cloudflare Pages Function is invoked.
*/
export interface RoutesJSONSpec {
version: 1;
description: string;
include: string[];
exclude: string[];
/**
* Path to your [Wrangler configuration file](https://developers.cloudflare.com/workers/wrangler/configuration/).
* @deprecated removed in 8.0.0. Use `vitePluginOptions.configPath` instead
*/
config?: string;
}
Loading
Loading