diff --git a/examples/svelte-kit-demo/.gitignore b/examples/svelte-kit-demo/.gitignore index d7d324e0db1f..9a7b33b97ced 100644 --- a/examples/svelte-kit-demo/.gitignore +++ b/examples/svelte-kit-demo/.gitignore @@ -2,4 +2,5 @@ /node_modules /build /.svelte -/.vercel_build_output \ No newline at end of file +/.vercel_build_output +/workers-site \ No newline at end of file diff --git a/examples/svelte-kit-demo/README.md b/examples/svelte-kit-demo/README.md index 1ec94cf2fca3..1adcebcf25f0 100644 --- a/examples/svelte-kit-demo/README.md +++ b/examples/svelte-kit-demo/README.md @@ -9,3 +9,9 @@ This is a simple app to demonstrate a few different features of SvelteKit, and t - URL: https://kit-zeta.vercel.app/ - Info: https://vercel.com/sveltejs/kit - Build command: `npm run build:vercel` + +### Cloudflare Workers + +- URL: https://svelte-kit-demo.halfnelson.workers.dev +- Build command: `npm run build:cloudflare-workers` +- Deploy Command: `wrangler publish` diff --git a/examples/svelte-kit-demo/package.json b/examples/svelte-kit-demo/package.json index 42e5b8fb45dd..7c4662e029f7 100644 --- a/examples/svelte-kit-demo/package.json +++ b/examples/svelte-kit-demo/package.json @@ -7,12 +7,14 @@ "dev": "svelte-kit dev", "build": "svelte-kit build", "start": "svelte-kit start", - "build:vercel": "ADAPTER=@sveltejs/adapter-vercel OPTIONS={} npm run build" + "build:vercel": "ADAPTER=@sveltejs/adapter-vercel OPTIONS={} npm run build", + "build:cloudflare-workers": "ADAPTER=@sveltejs/adapter-cloudflare-workers OPTIONS={} npm run build" }, "devDependencies": { "@sveltejs/adapter-node": "workspace:*", "@sveltejs/adapter-static": "workspace:*", "@sveltejs/adapter-vercel": "workspace:*", + "@sveltejs/adapter-cloudflare-workers": "workspace:*", "@sveltejs/kit": "workspace:*", "svelte": "^3.35.0" } diff --git a/examples/svelte-kit-demo/wrangler.toml b/examples/svelte-kit-demo/wrangler.toml new file mode 100644 index 000000000000..9d503e52ed07 --- /dev/null +++ b/examples/svelte-kit-demo/wrangler.toml @@ -0,0 +1,10 @@ +name = "svelte-kit-demo" +type = "webpack" +account_id = "f60df21486a4f0e5dbd85493882f1d53" +workers_dev = true +route = "" +zone_id = "" + +[site] +bucket = "./build" +entry-point = "./workers-site" \ No newline at end of file diff --git a/packages/adapter-cloudflare-workers/.gitignore b/packages/adapter-cloudflare-workers/.gitignore new file mode 100644 index 000000000000..e87415077413 --- /dev/null +++ b/packages/adapter-cloudflare-workers/.gitignore @@ -0,0 +1,3 @@ +.DS_Store +node_modules +target \ No newline at end of file diff --git a/packages/adapter-cloudflare-workers/CHANGELOG.md b/packages/adapter-cloudflare-workers/CHANGELOG.md new file mode 100644 index 000000000000..7075e7e072b1 --- /dev/null +++ b/packages/adapter-cloudflare-workers/CHANGELOG.md @@ -0,0 +1 @@ +# @sveltejs/adapter-cloudflare-workers diff --git a/packages/adapter-cloudflare-workers/README.md b/packages/adapter-cloudflare-workers/README.md new file mode 100644 index 000000000000..3b1820f4c521 --- /dev/null +++ b/packages/adapter-cloudflare-workers/README.md @@ -0,0 +1,27 @@ +# adapter-cloudflare-workers + +SvelteKit adapter that creates a Cloudflare Workers site using a function for dynamic server rendering. + +This is very experimental; the adapter API isn't at all fleshed out, and things will definitely change. + +## Configuration + +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. + +Generate this file using `wrangler` from your project directory + +```sh +$ wrangler init --site my-site-name +``` + +Then configure your sites build directory in the config file: + +```toml +[site] +bucket = "./build" +entry-point = "./workers-site" +``` + +It's recommended that you add the `build` and `workers-site` folders (or whichever other folders you specify) to your `.gitignore`. + +More info on configuring a cloudflare worker site can be found [here](https://developers.cloudflare.com/workers/platform/sites/start-from-existing) diff --git a/packages/adapter-cloudflare-workers/files/_package.json b/packages/adapter-cloudflare-workers/files/_package.json new file mode 100644 index 000000000000..537e219cb817 --- /dev/null +++ b/packages/adapter-cloudflare-workers/files/_package.json @@ -0,0 +1,9 @@ +{ + "private": true, + "version": "0.0.1", + "description": "Worker site generated by SvelteKit", + "main": "index.js", + "dependencies": { + "@cloudflare/kv-asset-handler": "~0.0.11" + } +} diff --git a/packages/adapter-cloudflare-workers/files/render.js b/packages/adapter-cloudflare-workers/files/render.js new file mode 100644 index 000000000000..b7186d77fbfe --- /dev/null +++ b/packages/adapter-cloudflare-workers/files/render.js @@ -0,0 +1,70 @@ +import { render } from './app.js'; // eslint-disable-line import/no-unresolved +import { getAssetFromKV, NotFoundError } from '@cloudflare/kv-asset-handler'; // eslint-disable-line import/no-unresolved + +// From https://developers.cloudflare.com/workers/examples/read-post +async function readRequestBody(request) { + const { headers } = request; + const contentType = headers.get('content-type') || ''; + if (contentType.includes('application/json')) { + return JSON.stringify(await request.json()); + } else if (contentType.includes('application/text')) { + return await request.text(); + } else if (contentType.includes('text/html')) { + return await request.text(); + } else if (contentType.includes('form')) { + return await request.formData(); + } else { + const myBlob = await request.blob(); + const objectURL = URL.createObjectURL(myBlob); + return objectURL; + } +} + +addEventListener('fetch', (event) => { + event.respondWith(handleEvent(event)); +}); + +async function handleEvent(event) { + //try static files first + if (event.request.method == 'GET') { + try { + return await getAssetFromKV(event); + } catch (e) { + if (!(e instanceof NotFoundError)) { + return new Response('Error loading static asset:' + (e.message || e.toString()), { + status: 500 + }); + } + } + } + + //fall back to an app route + const request = event.request; + const request_url = new URL(request.url); + + try { + const rendered = await render({ + host: request_url.host, + path: request_url.pathname, + query: request_url.searchParams, + body: request.body ? await readRequestBody(request) : null, + headers: request.headers, + method: request.method + }); + + if (rendered) { + const response = new Response(rendered.body, { + status: rendered.status, + headers: rendered.headers + }); + return response; + } + } catch (e) { + return new Response('Error rendering route:' + (e.message || e.toString()), { status: 500 }); + } + + return new Response({ + status: 404, + statusText: 'Not Found' + }); +} diff --git a/packages/adapter-cloudflare-workers/index.js b/packages/adapter-cloudflare-workers/index.js new file mode 100644 index 000000000000..83ffed9b79cd --- /dev/null +++ b/packages/adapter-cloudflare-workers/index.js @@ -0,0 +1,68 @@ +'use strict'; + +const { exec } = require('child_process'); +const fs = require('fs'); +const path = require('path'); +const toml = require('toml'); + +module.exports = function () { + /** @type {import('@sveltejs/kit').Adapter} */ + const adapter = { + name: '@sveltejs/adapter-cloudflare-workers', + async adapt(builder) { + let wrangler_config; + + if (fs.existsSync('wrangler.toml')) { + try { + wrangler_config = toml.parse(fs.readFileSync('wrangler.toml', 'utf-8')); + } catch (err) { + err.message = `Error parsing wrangler.toml: ${err.message}`; + throw err; + } + } else { + // TODO offer to create one? + throw new Error( + 'Missing a wrangler.toml file. Consult https://developers.cloudflare.com/workers/platform/sites/configuration on how to setup your site' + ); + } + + 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' + ); + } + + const bucket = path.resolve(wrangler_config.site.bucket); + const entrypoint = path.resolve(wrangler_config.site['entry-point'] ?? 'workers-site'); + + builder.copy_static_files(bucket); + builder.copy_client_files(bucket); + builder.copy_server_files(entrypoint); + + // copy the renderer + builder.copy(path.resolve(__dirname, 'files/render.js'), `${entrypoint}/index.js`); + builder.copy(path.resolve(__dirname, 'files/_package.json'), `${entrypoint}/package.json`); + + builder.log.info('Prerendering static pages...'); + await builder.prerender({ + dest: bucket + }); + + builder.log.info('Installing Worker Dependencies...'); + exec( + 'npm install', + { + cwd: entrypoint + }, + (error, stdout, stderr) => { + builder.log.info(stderr); + if (error) { + builder.log.error(error); + } + } + ); + } + }; + + return adapter; +}; diff --git a/packages/adapter-cloudflare-workers/package.json b/packages/adapter-cloudflare-workers/package.json new file mode 100644 index 000000000000..adb1b24ebbeb --- /dev/null +++ b/packages/adapter-cloudflare-workers/package.json @@ -0,0 +1,17 @@ +{ + "name": "@sveltejs/adapter-cloudflare-workers", + "version": "0.0.1", + "main": "index.js", + "files": [ + "files" + ], + "scripts": { + "lint": "eslint --ignore-path .gitignore \"**/*.{ts,js,svelte}\" && npm run check-format", + "format": "prettier --write . --config ../../.prettierrc --ignore-path .gitignore", + "check-format": "prettier --check . --config ../../.prettierrc --ignore-path .gitignore" + }, + "dependencies": { + "toml": "^3.0.0", + "@sveltejs/kit": "workspace:*" + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 93c2119c4138..7573049859ba 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -66,12 +66,14 @@ importers: svelte: ^3.35.0 examples/svelte-kit-demo: devDependencies: + '@sveltejs/adapter-cloudflare-workers': link:../../packages/adapter-cloudflare-workers '@sveltejs/adapter-node': link:../../packages/adapter-node '@sveltejs/adapter-static': link:../../packages/adapter-static '@sveltejs/adapter-vercel': link:../../packages/adapter-vercel '@sveltejs/kit': link:../../packages/kit svelte: 3.35.0 specifiers: + '@sveltejs/adapter-cloudflare-workers': workspace:* '@sveltejs/adapter-node': workspace:* '@sveltejs/adapter-static': workspace:* '@sveltejs/adapter-vercel': workspace:* @@ -91,6 +93,13 @@ importers: rollup: ^2.41.1 sirv: ^1.0.11 typescript: ^4.2.3 + packages/adapter-cloudflare-workers: + dependencies: + '@sveltejs/kit': link:../kit + toml: 3.0.0 + specifiers: + '@sveltejs/kit': workspace:* + toml: ^3.0.0 packages/adapter-netlify: dependencies: toml: 3.0.0