diff --git a/.changeset/tidy-paws-rest.md b/.changeset/tidy-paws-rest.md new file mode 100644 index 000000000000..fcfb6ae725df --- /dev/null +++ b/.changeset/tidy-paws-rest.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/adapter-cloudflare-workers': patch +--- + +Update adapter to use modules API and match the standard adapter-cloudflare implementation more closely diff --git a/packages/adapter-cloudflare-workers/.gitignore b/packages/adapter-cloudflare-workers/.gitignore index e87415077413..ada72314ce34 100644 --- a/packages/adapter-cloudflare-workers/.gitignore +++ b/packages/adapter-cloudflare-workers/.gitignore @@ -1,3 +1,4 @@ .DS_Store node_modules -target \ No newline at end of file +target +/files diff --git a/packages/adapter-cloudflare-workers/README.md b/packages/adapter-cloudflare-workers/README.md index c02c44c0e08a..68c79787a9ce 100644 --- a/packages/adapter-cloudflare-workers/README.md +++ b/packages/adapter-cloudflare-workers/README.md @@ -33,9 +33,19 @@ export default { **You will need [Wrangler](https://developers.cloudflare.com/workers/cli-wrangler/install-update) installed on your system** -This adapter expects to find a [wrangler.toml](https://developers.cloudflare.com/workers/platform/sites/configuration) file in the project root. It will determine where to write static assets and the worker based on the `site.bucket` and `site.entry-point` settings. +This adapter expects to find a [wrangler.toml](https://developers.cloudflare.com/workers/platform/sites/configuration) file in the project root. It will determine where to write static assets and the worker based on the `site.bucket` and `build.upload` settings. These values must be set to the following: -Generate this file using `wrangler` from your project directory +```toml +[build.upload] +format = "modules" +dir = "./.svelte-kit/cloudflare" +main = "./_worker.mjs" + +[site] +bucket = "./.svelte-kit/cloudflare-bucket" +``` + +To get started, generate this file using `wrangler` from your project directory ```sh wrangler init --site my-site-name @@ -48,24 +58,31 @@ Now you should get some details from Cloudflare. You should get your: Get them by visiting your [Cloudflare dashboard](https://dash.cloudflare.com) and click on any domain. There, you can scroll down and on the left, you can see your details under **API**. -Then configure your sites build directory and your account-details in the config file: +Then configure your account-details in the config file: ```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"} - +name = "" type = "javascript" +account_id = "" +workers_dev = true +route = "" +zone_id = "" + +compatibility_date = "2022-02-09" [build] # Assume it's already been built. You can make this "npm run build" to ensure a build before publishing command = "" +# All values below here are required by adapter-cloudflare-workers and should not change [build.upload] -format = "service-worker" -``` +format = "modules" +dir = "./.svelte-kit/cloudflare" +main = "./_worker.mjs" -It's recommended that you add the `build` and `workers-site` folders (or whichever other folders you specify) to your `.gitignore`. +[site] +bucket = "./.svelte-kit/cloudflare-bucket" +``` Now, log in with wrangler: diff --git a/packages/adapter-cloudflare-workers/ambient.d.ts b/packages/adapter-cloudflare-workers/ambient.d.ts index 9df439aab626..104dcde68371 100644 --- a/packages/adapter-cloudflare-workers/ambient.d.ts +++ b/packages/adapter-cloudflare-workers/ambient.d.ts @@ -6,12 +6,10 @@ declare module 'MANIFEST' { import { SSRManifest } from '@sveltejs/kit'; export const manifest: SSRManifest; - export const prerendered: Set; + export const prerendered: Map; } -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 value: string; + export = value; } diff --git a/packages/adapter-cloudflare-workers/files/entry.js b/packages/adapter-cloudflare-workers/files/entry.js deleted file mode 100644 index 9c778e8c109a..000000000000 --- a/packages/adapter-cloudflare-workers/files/entry.js +++ /dev/null @@ -1,61 +0,0 @@ -import { Server } from 'SERVER'; -import { manifest, prerendered } from 'MANIFEST'; -import { getAssetFromKV } from '@cloudflare/kv-asset-handler'; - -const server = new Server(manifest); - -const prefix = `/${manifest.appDir}/`; - -addEventListener('fetch', (/** @type {FetchEvent} */ event) => { - event.respondWith(handle(event)); -}); - -/** - * @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') - } - }); - } - - // prerendered pages and index.html files - const pathname = url.pathname.replace(/\/$/, ''); - let file = pathname.substring(1); - - try { - file = decodeURIComponent(file); - } catch (err) { - // ignore - } - - 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(request, { - getClientAddress() { - return request.headers.get('cf-connecting-ip'); - } - }); - } catch (e) { - return new Response('Error rendering route:' + (e.message || e.toString()), { status: 500 }); - } -} diff --git a/packages/adapter-cloudflare-workers/index.js b/packages/adapter-cloudflare-workers/index.js index f161a109cb37..b9f71d41128b 100644 --- a/packages/adapter-cloudflare-workers/index.js +++ b/packages/adapter-cloudflare-workers/index.js @@ -1,29 +1,34 @@ import { existsSync, readFileSync, writeFileSync } from 'fs'; import { posix } from 'path'; import { execSync } from 'child_process'; -import esbuild from 'esbuild'; +import * as esbuild from 'esbuild'; 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); - - // @ts-ignore - const { bucket } = site; + validate_config(builder); // @ts-ignore - const entrypoint = site['entry-point'] || 'workers-site'; - const files = fileURLToPath(new URL('./files', import.meta.url).href); - const tmp = builder.getBuildDirectory('cloudflare-workers-tmp'); + const dest = builder.getBuildDirectory('cloudflare'); + const bucket = builder.getBuildDirectory('cloudflare-bucket'); + const tmp = builder.getBuildDirectory('cloudflare-tmp'); + builder.rimraf(dest); builder.rimraf(bucket); - builder.rimraf(entrypoint); + builder.rimraf(tmp); + + builder.mkdirp(tmp); + + builder.writeStatic(bucket); + builder.writeClient(bucket); + builder.writePrerendered(bucket); + + const relativePath = posix.relative(tmp, builder.getServerDirectory()); builder.log.info('Installing worker dependencies...'); builder.copy(`${files}/_package.json`, `${tmp}/package.json`); @@ -33,83 +38,136 @@ export default function () { builder.log.info(stdout.toString()); builder.log.minor('Generating worker...'); - const relativePath = posix.relative(tmp, builder.getServerDirectory()); - builder.copy(`${files}/entry.js`, `${tmp}/entry.js`, { + writeFileSync( + `${tmp}/manifest.js`, + `export const manifest = ${builder.generateManifest({ + relativePath + })};\n\nexport const prerendered = new Map(${JSON.stringify( + Array.from(builder.prerendered.pages.entries()) + )});\n` + ); + + builder.copy(`${files}/worker.js`, `${tmp}/_worker.js`, { replace: { SERVER: `${relativePath}/index.js`, MANIFEST: './manifest.js' } }); - writeFileSync( - `${tmp}/manifest.js`, - `export const manifest = ${builder.generateManifest({ - relativePath - })};\n\nexport const prerendered = new Set(${JSON.stringify(builder.prerendered.paths)});\n` - ); + const external = ['__STATIC_CONTENT_MANIFEST']; + + if (options.external) { + external.push(...options.external); + } await esbuild.build({ - entryPoints: [`${tmp}/entry.js`], - outfile: `${entrypoint}/index.js`, - bundle: true, target: 'es2020', - platform: 'browser' + platform: 'browser', + ...options, + entryPoints: [`${tmp}/_worker.js`], + external, + outfile: `${dest}/_worker.mjs`, + allowOverwrite: true, + format: 'esm', + bundle: true }); - - writeFileSync(`${entrypoint}/package.json`, JSON.stringify({ main: 'index.js' })); - - builder.log.minor('Copying assets...'); - builder.writeClient(bucket); - builder.writeStatic(bucket); - builder.writePrerendered(bucket); } }; } /** @param {import('@sveltejs/kit').Builder} builder */ function validate_config(builder) { - if (existsSync('wrangler.toml')) { - let wrangler_config; - - try { - wrangler_config = toml.parse(readFileSync('wrangler.toml', 'utf-8')); - } catch (err) { - err.message = `Error parsing wrangler.toml: ${err.message}`; - throw err; - } + if (!existsSync('wrangler.toml')) { + builder.log.error( + 'Consult https://developers.cloudflare.com/workers/platform/sites/configuration on how to setup your site' + ); + + builder.log( + ` + Sample wrangler.toml: + + name = "" + type = "javascript" + account_id = "" + workers_dev = true + route = "" + zone_id = "" + + compatibility_date = "2022-02-09" + + [build] + # Assume it's already been built. You can make this "npm run build" to ensure a build before publishing + command = "" + + # All values below here are required by adapter-cloudflare-workers and should not change + [build.upload] + format = "modules" + dir = "./.svelte-kit/cloudflare" + main = "./_worker.mjs" + + [site] + bucket = "./.svelte-kit/cloudflare-bucket"` + .replace(/^\t+/gm, '') + .trim() + ); + + throw new Error('Missing a wrangler.toml file'); + } + + let wrangler_config; + + try { + wrangler_config = toml.parse(readFileSync('wrangler.toml', 'utf-8')); + } catch (err) { + err.message = `Error parsing wrangler.toml: ${err.message}`; + throw err; + } + + // @ts-ignore + if (!wrangler_config.site || wrangler_config.site.bucket !== './.svelte-kit/cloudflare-bucket') { + throw new Error( + 'You must specify site.bucket in wrangler.toml, and it must equal "./.svelte-kit/cloudflare-bucket"' + ); + } + // @ts-ignore + if ( // @ts-ignore - if (!wrangler_config.site || !wrangler_config.site.bucket) { - throw new Error( - 'You must specify site.bucket in wrangler.toml. Consult https://developers.cloudflare.com/workers/platform/sites/configuration' - ); - } + !wrangler_config.build || + // @ts-ignore + !wrangler_config.build.upload || + // @ts-ignore + wrangler_config.build.upload.format !== 'modules' + ) { + throw new Error( + 'You must specify build.upload.format in wrangler.toml, and it must equal "modules"' + ); + } - return wrangler_config; + if ( + // @ts-ignore + !wrangler_config.build || + // @ts-ignore + !wrangler_config.build.upload || + // @ts-ignore + wrangler_config.build.upload.dir !== './.svelte-kit/cloudflare' + ) { + throw new Error( + 'You must specify build.upload.dir in wrangler.toml, and it must equal "./.svelte-kit/cloudflare"' + ); } - builder.log.error( - 'Consult https://developers.cloudflare.com/workers/platform/sites/configuration on how to setup your site' - ); - - builder.log( - ` - Sample wrangler.toml: - - name = "" - type = "javascript" - account_id = "" - workers_dev = true - route = "" - zone_id = "" - - [site] - bucket = "./.cloudflare/assets" - entry-point = "./.cloudflare/worker"` - .replace(/^\t+/gm, '') - .trim() - ); - - throw new Error('Missing a wrangler.toml file'); + if ( + // @ts-ignore + !wrangler_config.build || + // @ts-ignore + !wrangler_config.build.upload || + // @ts-ignore + wrangler_config.build.upload.main !== './_worker.mjs' + ) { + throw new Error( + 'You must specify build.upload.main in wrangler.toml, and it must equal "./_worker.mjs"' + ); + } } diff --git a/packages/adapter-cloudflare-workers/package.json b/packages/adapter-cloudflare-workers/package.json index 6dabce3214df..73b8020d0272 100644 --- a/packages/adapter-cloudflare-workers/package.json +++ b/packages/adapter-cloudflare-workers/package.json @@ -19,19 +19,24 @@ "types": "index.d.ts", "files": [ "files", + "index.js", "index.d.ts" ], "scripts": { + "build": "esbuild src/worker.js --bundle --outfile=files/worker.js --external:SERVER --external:MANIFEST --external:__STATIC_CONTENT_MANIFEST --format=esm && cp src/_package.json files/_package.json", "lint": "eslint --ignore-path .gitignore \"**/*.{ts,js,svelte}\" && npm run check-format", "format": "npm run check-format -- --write", - "check": "tsc", - "check-format": "prettier --check . --config ../../.prettierrc --ignore-path .gitignore" + "check": "tsc --skipLibCheck", + "check-format": "prettier --check . --config ../../.prettierrc --ignore-path .gitignore", + "prepublishOnly": "npm run build" }, "dependencies": { "@iarna/toml": "^2.2.5", - "esbuild": "^0.14.21" + "esbuild": "^0.14.21", + "worktop": "0.8.0-next.12" }, "devDependencies": { + "@types/ws": "^8.5.3", "@cloudflare/kv-asset-handler": "^0.2.0", "typescript": "^4.6.2" } diff --git a/packages/adapter-cloudflare-workers/files/_package.json b/packages/adapter-cloudflare-workers/src/_package.json similarity index 76% rename from packages/adapter-cloudflare-workers/files/_package.json rename to packages/adapter-cloudflare-workers/src/_package.json index bc4c8d4aabac..31ba024f2cbc 100644 --- a/packages/adapter-cloudflare-workers/files/_package.json +++ b/packages/adapter-cloudflare-workers/src/_package.json @@ -4,6 +4,6 @@ "description": "Worker site generated by SvelteKit", "main": "index.js", "dependencies": { - "@cloudflare/kv-asset-handler": "~0.1.3" + "@cloudflare/kv-asset-handler": "^0.2.0" } } diff --git a/packages/adapter-cloudflare-workers/src/worker.js b/packages/adapter-cloudflare-workers/src/worker.js new file mode 100644 index 000000000000..6f9965e74db5 --- /dev/null +++ b/packages/adapter-cloudflare-workers/src/worker.js @@ -0,0 +1,104 @@ +import { Server } from 'SERVER'; +import { manifest, prerendered } from 'MANIFEST'; +import * as Cache from 'worktop/cfw.cache'; +import { getAssetFromKV, mapRequestToAsset } from '@cloudflare/kv-asset-handler'; + +// Note: This does not get bundled +import manifestJSON from '__STATIC_CONTENT_MANIFEST'; +const ASSET_MANIFEST = JSON.parse(manifestJSON); +ASSET_MANIFEST['index.html'] = 'index.special.html'; + +const server = new Server(manifest); + +const prefix = `/${manifest.appDir}/`; + +/** @type {import('worktop/cfw').Module.Worker<{ __STATIC_CONTENT: unknown }>} */ +const worker = { + async fetch(req, env, context) { + try { + let res = await Cache.lookup(req); + if (res) return res; + + const url = new URL(req.url); + let { pathname } = url; + + // static assets + if (pathname.startsWith(prefix)) { + res = await getAssetFromKV( + { + request: req, + waitUntil(promise) { + return context.waitUntil(promise); + } + }, + { + ASSET_NAMESPACE: env.__STATIC_CONTENT, + ASSET_MANIFEST, + mapRequestToAsset(request, options) { + if (prerendered.has(pathname || '/')) { + url.pathname = '/' + prerendered.get(pathname || '/').file; + return new Request(url.toString(), request); + } + return mapRequestToAsset(request, options); + } + } + ); + + res = 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' + } + }); + } else { + // prerendered pages and index.html files + pathname = pathname.replace(/\/$/, '') || '/'; + + let file = pathname.substring(1); + + try { + file = decodeURIComponent(file); + } catch (err) { + // ignore + } + + if ( + manifest.assets.has(file) || + manifest.assets.has(file + '/index.html') || + prerendered.has(pathname) + ) { + res = await getAssetFromKV( + { + request: req, + waitUntil(promise) { + return context.waitUntil(promise); + } + }, + { + ASSET_NAMESPACE: env.__STATIC_CONTENT, + ASSET_MANIFEST + } + ); + } else { + // dynamically-generated pages + res = await server.respond(req, { + platform: { env, context }, + getClientAddress() { + return req.headers.get('cf-connecting-ip'); + } + }); + } + } + + // Writes to Cache only if allowed + return Cache.save(req, res, context); + } catch (e) { + return new Response('Error rendering route: ' + (e.message || e.toString()), { status: 500 }); + } + } +}; + +export default worker; diff --git a/packages/adapter-cloudflare-workers/tsconfig.json b/packages/adapter-cloudflare-workers/tsconfig.json index af2a0c525acf..2b4c281e5fb9 100644 --- a/packages/adapter-cloudflare-workers/tsconfig.json +++ b/packages/adapter-cloudflare-workers/tsconfig.json @@ -12,5 +12,5 @@ "@sveltejs/kit": ["../kit/types/index"] } }, - "include": ["**/*.js", "ambient.d.ts"] + "include": ["index.js", "ambient.d.ts", "src/worker.js"] } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 060628d36ee9..a511a751259d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -84,13 +84,17 @@ importers: specifiers: '@cloudflare/kv-asset-handler': ^0.2.0 '@iarna/toml': ^2.2.5 + '@types/ws': ^8.5.3 esbuild: ^0.14.21 typescript: ^4.6.2 + worktop: 0.8.0-next.12 dependencies: '@iarna/toml': 2.2.5 esbuild: 0.14.21 + worktop: 0.8.0-next.12 devDependencies: '@cloudflare/kv-asset-handler': 0.2.0 + '@types/ws': 8.5.3 typescript: 4.6.2 packages/adapter-netlify: @@ -4129,7 +4133,7 @@ packages: /jsonfile/4.0.0: resolution: {integrity: sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=} optionalDependencies: - graceful-fs: 4.2.8 + graceful-fs: 4.2.10 dev: true /kind-of/6.0.3: