diff --git a/.changeset/few-walls-obey.md b/.changeset/few-walls-obey.md new file mode 100644 index 000000000000..b089af1ccb75 --- /dev/null +++ b/.changeset/few-walls-obey.md @@ -0,0 +1,12 @@ +--- +'@sveltejs/adapter-cloudflare-workers': patch +--- + +[Breaking] refactor implementation from "Service Worker" pattern to "Module Worker" used in adapter-cloudflare + +#### add the following to your wrangler.toml +```toml + [build.upload] + format = "modules" + main = "./worker.mjs" +``` diff --git a/packages/adapter-cloudflare-workers/README.md b/packages/adapter-cloudflare-workers/README.md index eb6420d22882..59d26dc25296 100644 --- a/packages/adapter-cloudflare-workers/README.md +++ b/packages/adapter-cloudflare-workers/README.md @@ -53,7 +53,6 @@ Then configure your sites build directory and your account-details in the config ```toml account_id = 'YOUR ACCOUNT_ID' zone_id = 'YOUR ZONE_ID' # optional, if you don't specify this a workers.dev subdomain will be used. -site = {bucket = "./build", entry-point = "./workers-site"} type = "javascript" @@ -62,7 +61,12 @@ type = "javascript" command = "" [build.upload] -format = "service-worker" +format = "modules" +main = "./worker.mjs" + +[site] +bucket = "./.cloudflare/assets" +entry-point = "./.cloudflare/worker" ``` It's recommended that you add the `build` and `workers-site` folders (or whichever other folders you specify) to your `.gitignore`. diff --git a/packages/adapter-cloudflare-workers/ambient.d.ts b/packages/adapter-cloudflare-workers/ambient.d.ts index 9df439aab626..a1381f25f04d 100644 --- a/packages/adapter-cloudflare-workers/ambient.d.ts +++ b/packages/adapter-cloudflare-workers/ambient.d.ts @@ -9,9 +9,7 @@ declare module 'MANIFEST' { export const prerendered: Set; } -declare abstract class FetchEvent extends Event { - readonly request: Request; - respondWith(promise: Response | Promise): void; - passThroughOnException(): void; - waitUntil(promise: Promise): void; +declare module '__STATIC_CONTENT_MANIFEST' { + const json: string; + export default json; } diff --git a/packages/adapter-cloudflare-workers/files/entry.js b/packages/adapter-cloudflare-workers/files/entry.js index 9c778e8c109a..ab6d9b602d41 100644 --- a/packages/adapter-cloudflare-workers/files/entry.js +++ b/packages/adapter-cloudflare-workers/files/entry.js @@ -1,61 +1,101 @@ import { Server } from 'SERVER'; import { manifest, prerendered } from 'MANIFEST'; import { getAssetFromKV } from '@cloudflare/kv-asset-handler'; +import static_asset_manifest_json from '__STATIC_CONTENT_MANIFEST'; +const static_asset_manifest = JSON.parse(static_asset_manifest_json); const server = new Server(manifest); const prefix = `/${manifest.appDir}/`; -addEventListener('fetch', (/** @type {FetchEvent} */ event) => { - event.respondWith(handle(event)); -}); +export default { + /** + * @param {Request} req + * @param {any} env + * @param {any} context + */ + async fetch(req, env, context) { + const url = new URL(req.url); -/** - * @param {FetchEvent} event - * @returns {Promise} - */ -async function handle(event) { - const { request } = event; - - const url = new URL(request.url); - - // generated assets - if (url.pathname.startsWith(prefix)) { - const res = await getAssetFromKV(event); - return new Response(res.body, { - headers: { - 'cache-control': 'public, immutable, max-age=31536000', - 'content-type': res.headers.get('content-type') + // static assets + if (url.pathname.startsWith(prefix)) { + /** @type {Response} */ + const res = await get_asset_from_kv(req, env, context); + if (is_error(res.status)) { + return res; } - }); - } + return new Response(res.body, { + headers: { + // include original cache headers, minus cache-control which + // is overridden, and etag which is no longer useful + 'cache-control': 'public, immutable, max-age=31536000', + 'content-type': res.headers.get('content-type'), + 'x-robots-tag': 'noindex' + } + }); + } - // prerendered pages and index.html files - const pathname = url.pathname.replace(/\/$/, ''); - let file = pathname.substring(1); + // prerendered pages and index.html files + const pathname = url.pathname.replace(/\/$/, ''); + let file = pathname.substring(1); - try { - file = decodeURIComponent(file); - } catch (err) { - // ignore - } + try { + file = decodeURIComponent(file); + } catch (err) { + // ignore + } + + if ( + manifest.assets.has(file) || + manifest.assets.has(file + '/index.html') || + prerendered.has(pathname || '/') + ) { + return get_asset_from_kv(req, env, context); + } - if ( - manifest.assets.has(file) || - manifest.assets.has(file + '/index.html') || - prerendered.has(pathname || '/') - ) { - return await getAssetFromKV(event); + // dynamically-generated pages + try { + return await server.respond(req, { + platform: { env, context }, + getClientAddress() { + return req.headers.get('cf-connecting-ip'); + } + }); + } catch (e) { + return new Response('Error rendering route: ' + (e.message || e.toString()), { status: 500 }); + } } +}; - // dynamically-generated pages +/** + * @param {Request} req + * @param {any} env + * @param {any} context + */ +async function get_asset_from_kv(req, env, context) { try { - return await server.respond(request, { - getClientAddress() { - return request.headers.get('cf-connecting-ip'); + return await getAssetFromKV( + { + request: req, + waitUntil(promise) { + return context.waitUntil(promise); + } + }, + { + ASSET_NAMESPACE: env.__STATIC_CONTENT, + ASSET_MANIFEST: static_asset_manifest } - }); + ); } catch (e) { - return new Response('Error rendering route:' + (e.message || e.toString()), { status: 500 }); + const status = is_error(e.status) ? e.status : 500; + return new Response(e.message || e.toString(), { status }); } } + +/** + * @param {number} status + * @returns {boolean} + */ +function is_error(status) { + return status > 399; +} diff --git a/packages/adapter-cloudflare-workers/index.js b/packages/adapter-cloudflare-workers/index.js index f161a109cb37..94732b553f06 100644 --- a/packages/adapter-cloudflare-workers/index.js +++ b/packages/adapter-cloudflare-workers/index.js @@ -6,12 +6,12 @@ import toml from '@iarna/toml'; import { fileURLToPath } from 'url'; /** @type {import('.')} */ -export default function () { +export default function (options = {}) { return { name: '@sveltejs/adapter-cloudflare-workers', async adapt(builder) { - const { site } = validate_config(builder); + const { site, build } = validate_config(builder); // @ts-ignore const { bucket } = site; @@ -19,6 +19,9 @@ export default function () { // @ts-ignore const entrypoint = site['entry-point'] || 'workers-site'; + // @ts-ignore + const main_path = build.upload.main; + const files = fileURLToPath(new URL('./files', import.meta.url).href); const tmp = builder.getBuildDirectory('cloudflare-workers-tmp'); @@ -50,14 +53,20 @@ export default function () { ); await esbuild.build({ + target: 'es2020', + platform: 'browser', + ...options, entryPoints: [`${tmp}/entry.js`], - outfile: `${entrypoint}/index.js`, + outfile: `${entrypoint}/${main_path}`, bundle: true, - target: 'es2020', - platform: 'browser' + external: ['__STATIC_CONTENT_MANIFEST', ...(options?.external || [])], + format: 'esm' }); - writeFileSync(`${entrypoint}/package.json`, JSON.stringify({ main: 'index.js' })); + writeFileSync( + `${entrypoint}/package.json`, + JSON.stringify({ main: main_path, type: 'module' }) + ); builder.log.minor('Copying assets...'); builder.writeClient(bucket); @@ -86,6 +95,36 @@ function validate_config(builder) { ); } + // @ts-ignore + if (!wrangler_config.build || !wrangler_config.build.upload) { + throw new Error( + 'You must specify build.upload options in wrangler.toml. Consult https://github.com/sveltejs/kit/tree/master/packages/adapter-cloudflare-workers' + ); + } + + // @ts-ignore + if (wrangler_config.build.upload.format !== 'modules') { + throw new Error('build.upload.format in wrangler.toml must be "modules"'); + } + + // @ts-ignore + const main_file = wrangler_config.build?.upload?.main; + const main_file_ext = main_file?.split('.').slice(-1)[0]; + if (main_file_ext && main_file_ext !== 'mjs') { + // @ts-ignore + const upload_rules = wrangler_config.build?.upload?.rules; + // @ts-ignore + const matching_rule = upload_rules?.find(({ globs }) => + // @ts-ignore + globs.find((glob) => glob.endsWith(`*.${main_file_ext}`)) + ); + if (!matching_rule) { + throw new Error( + 'To support a build.upload.main value not ending in .mjs, an upload rule must be added to build.upload.rules. Consult https://developers.cloudflare.com/workers/cli-wrangler/configuration/#build' + ); + } + } + return wrangler_config; } @@ -104,6 +143,13 @@ function validate_config(builder) { route = "" zone_id = "" + [build] + command = "" + + [build.upload] + format = "modules" + main = "./worker.mjs" + [site] bucket = "./.cloudflare/assets" entry-point = "./.cloudflare/worker"`