Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cloudflare Worker Adapter #717

Merged
Merged
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
3 changes: 2 additions & 1 deletion examples/svelte-kit-demo/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@
/node_modules
/build
/.svelte
/.vercel_build_output
/.vercel_build_output
/workers-site
6 changes: 6 additions & 0 deletions examples/svelte-kit-demo/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
4 changes: 3 additions & 1 deletion examples/svelte-kit-demo/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
Expand Down
10 changes: 10 additions & 0 deletions examples/svelte-kit-demo/wrangler.toml
Original file line number Diff line number Diff line change
@@ -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"
3 changes: 3 additions & 0 deletions packages/adapter-cloudflare-workers/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.DS_Store
node_modules
target
1 change: 1 addition & 0 deletions packages/adapter-cloudflare-workers/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# @sveltejs/adapter-cloudflare-workers
27 changes: 27 additions & 0 deletions packages/adapter-cloudflare-workers/README.md
Original file line number Diff line number Diff line change
@@ -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)
9 changes: 9 additions & 0 deletions packages/adapter-cloudflare-workers/files/_package.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
70 changes: 70 additions & 0 deletions packages/adapter-cloudflare-workers/files/render.js
Original file line number Diff line number Diff line change
@@ -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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

any idea why the eslint-disable-line might be needed?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yep. I don't particularly want to bundle up kv-asset-handler as part of the adapter, and the adapt function itself doesn't have a dependency on it, so lint complains.


// 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'
});
}
68 changes: 68 additions & 0 deletions packages/adapter-cloudflare-workers/index.js
Original file line number Diff line number Diff line change
@@ -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;
};
17 changes: 17 additions & 0 deletions packages/adapter-cloudflare-workers/package.json
Original file line number Diff line number Diff line change
@@ -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:*"
}
}
9 changes: 9 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.