Skip to content

Commit

Permalink
feat: use the new Workers Static Assets feature from Cloudflare
Browse files Browse the repository at this point in the history
This changes the adapter to stop using the old Workers Sites (kv-asset-handler) approach.
Instead, making use of the new Workers Static Assets feature, which is embedded into Cloudflare natively.

Also this change removes the extra esbuild step that was being run inside the adapter, relying upon Wrangler to do the bundling.
The extra esbuild step required a hardcoded list of Node.js compatible modules.
This is no longer needed since Wrangler now manages all of that.

- This version of the adapter requires Wrangler version 3.87.0 or later.

  Run `npm add -D wrangler@latest` (or similar) in your project to update Wrangler.
- The user's Wrangler configuration (`wrangler.toml`) must be migrated from using Workers Sites to using Workers Assets.

  Previously a user's `wrangler.toml` might look like:

  ```toml
  name = "<your-site-name>"
  account_id = "<your-account-id>"
  compatibility_date = "2021-11-12"
  main = "./.cloudflare/worker.js"

  # Workers Sites configuration
  site.bucket = "./.cloudflare/public"
  ```

  Change it to to look like:

  ```toml
  name = "<your-site-name>"
  account_id = "<your-account-id>"
  compatibility_date = "2021-11-12"`
  main = ".svelte-kit/cloudflare/server/index.js"

  # Workers Assets configuration
  assets = { directory = ".svelte-kit/cloudflare/client" }
  ```

- Workers Assets defaults to serving assets directly for a matching request, rather than routing it through the Worker code.

  The previous adapter would add custom headers to assets responses (such as `cache-control`, `content-type`, and `x-robots-tag`. Such direct asset responses no longer contain these headers - but the will include eTag headers that have proven (in Pages) to be an effective caching strategy for assets.

  If you wish to always run the Worker before every request then add `serve_directly = false` to the assets configuration section. For example:

  ```toml
  assets = { directory = ".svelte-kit/cloudflare/client", serve_directly = false }
  ```
  • Loading branch information
petebacondarwin committed Nov 28, 2024
1 parent 7246a54 commit 30d1f46
Show file tree
Hide file tree
Showing 8 changed files with 260 additions and 182 deletions.
53 changes: 53 additions & 0 deletions .changeset/smart-owls-trade.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
---
'@sveltejs/adapter-cloudflare-workers': major
---

feat: use the new Workers Static Assets feature from Cloudflare

This changes the adapter to stop using the old Workers Sites (kv-asset-handler) approach.
Instead, making use of the new Workers Static Assets feature, which is embedded into Cloudflare natively.

Also this change removes the extra esbuild step that was being run inside the adapter, relying upon Wrangler to do the bundling.
The extra esbuild step required a hardcoded list of Node.js compatible modules.
This is no longer needed since Wrangler now manages all of that.

## Breaking changes and migration

- This version of the adapter requires Wrangler version 3.87.0 or later.

Run `npm add -D wrangler@latest` (or similar) in your project to update Wrangler.
- The user's Wrangler configuration (`wrangler.toml`) must be migrated from using Workers Sites to using Workers Assets.

Previously a user's `wrangler.toml` might look like:

```toml
name = "<your-site-name>"
account_id = "<your-account-id>"
compatibility_date = "2021-11-12"
main = "./.cloudflare/worker.js"

# Workers Sites configuration
site.bucket = "./.cloudflare/public"
```

Change it to to look like:

```toml
name = "<your-site-name>"
account_id = "<your-account-id>"
compatibility_date = "2021-11-12"`
main = ".svelte-kit/cloudflare/server/index.js"

# Workers Assets configuration
assets = { directory = ".svelte-kit/cloudflare/client" }
```

- Workers Assets defaults to serving assets directly for a matching request, rather than routing it through the Worker code.

The previous adapter would add custom headers to assets responses (such as `cache-control`, `content-type`, and `x-robots-tag`. Such direct asset responses no longer contain these headers - but the will include eTag headers that have proven (in Pages) to be an effective caching strategy for assets.

If you wish to always run the Worker before every request then add `serve_directly = false` to the assets configuration section. For example:

```toml
assets = { directory = ".svelte-kit/cloudflare/client", serve_directly = false }
```
2 changes: 1 addition & 1 deletion packages/adapter-cloudflare-workers/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

SvelteKit adapter that creates a Cloudflare Workers site using a function for dynamic server rendering.

**Requires [Wrangler v2](https://developers.cloudflare.com/workers/wrangler/get-started/).** Wrangler v1 is no longer supported.
**Requires [Wrangler v3 or later](https://developers.cloudflare.com/workers/wrangler/get-started/).**.

## Docs

Expand Down
9 changes: 0 additions & 9 deletions packages/adapter-cloudflare-workers/files/_package.json

This file was deleted.

45 changes: 6 additions & 39 deletions packages/adapter-cloudflare-workers/files/entry.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
import { Server } from 'SERVER';
import { manifest, prerendered, base_path } from 'MANIFEST';
import { getAssetFromKV, mapRequestToAsset } 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);

Expand All @@ -25,7 +22,7 @@ export default {
// static assets
if (url.pathname.startsWith(app_path)) {
/** @type {Response} */
const res = await get_asset_from_kv(req, env, context);
const res = await env.ASSETS.fetch(req);
if (is_error(res.status)) return res;

const cache_control = url.pathname.startsWith(immutable)
Expand Down Expand Up @@ -65,20 +62,11 @@ export default {

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

if (
is_static_asset ||
prerendered.has(pathname) ||
pathname === version_file ||
pathname.startsWith(immutable)
) {
return get_asset_from_kv(req, env, context, (request, options) => {
if (prerendered.has(pathname)) {
url.pathname = '/' + prerendered.get(pathname).file;
return new Request(url.toString(), request);
}

return mapRequestToAsset(request, options);
});
if (prerendered.has(pathname)) {
url.pathname = '/' + prerendered.get(pathname).file;
return env.ASSETS.fetch(new Request(url.toString(), req));
} else if (is_static_asset || pathname === version_file || pathname.startsWith(immutable)) {
return env.ASSETS.fetch(req);
} else if (location && prerendered.has(location)) {
if (search) location += search;
return new Response('', {
Expand Down Expand Up @@ -106,27 +94,6 @@ export default {
}
};

/**
* @param {Request} req
* @param {any} env
* @param {any} context
*/
async function get_asset_from_kv(req, env, context, map = mapRequestToAsset) {
return await getAssetFromKV(
{
request: req,
waitUntil(promise) {
return context.waitUntil(promise);
}
},
{
ASSET_NAMESPACE: env.__STATIC_CONTENT,
ASSET_MANIFEST: static_asset_manifest,
mapRequestToAsset: map
}
);
}

/**
* @param {number} status
* @returns {boolean}
Expand Down
142 changes: 25 additions & 117 deletions packages/adapter-cloudflare-workers/index.js
Original file line number Diff line number Diff line change
@@ -1,152 +1,64 @@
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
import { posix, dirname } from 'node:path';
import { execSync } from 'node:child_process';
import esbuild from 'esbuild';
import toml from '@iarna/toml';
import { fileURLToPath } from 'node:url';
import { getPlatformProxy } from 'wrangler';

/**
* @typedef {{
* main: string;
* site: {
* bucket: string;
* assets: {
* directory: string;
* binding: string;
* }
* compatibility_flags?: string[];
* }} WranglerConfig
*/

// list from https://developers.cloudflare.com/workers/runtime-apis/nodejs/
const compatible_node_modules = [
'assert',
'async_hooks',
'buffer',
'crypto',
'diagnostics_channel',
'events',
'path',
'process',
'stream',
'string_decoder',
'util'
];

/** @type {import('./index.js').default} */
export default function ({ config = 'wrangler.toml', platformProxy = {} } = {}) {
return {
name: '@sveltejs/adapter-cloudflare-workers',

async adapt(builder) {
const { main, site, compatibility_flags } = validate_config(builder, config);

adapt(builder) {
const { main, assets } = validate_config(builder, config);
const files = fileURLToPath(new URL('./files', import.meta.url).href);
const tmp = builder.getBuildDirectory('cloudflare-workers-tmp');

builder.rimraf(site.bucket);
builder.rimraf(dirname(main));

builder.log.info('Installing worker dependencies...');
builder.copy(`${files}/_package.json`, `${tmp}/package.json`);

// TODO would be cool if we could make this step unnecessary somehow
const stdout = execSync('npm install', { cwd: tmp });
builder.log.info(stdout.toString());
const outDir = dirname(main);
const relativePath = posix.relative(outDir, builder.getServerDirectory());

builder.log.minor('Generating worker...');
const relativePath = posix.relative(tmp, builder.getServerDirectory());

builder.copy(`${files}/entry.js`, `${tmp}/entry.js`, {
// Clear out old files
builder.rimraf(assets.directory);
builder.rimraf(outDir);

// Create the entry-point for the Worker
builder.copy(`${files}/entry.js`, main, {
replace: {
SERVER: `${relativePath}/index.js`,
MANIFEST: './manifest.js'
MANIFEST: './manifest.js',
ASSETS: assets.binding || 'ASSETS'
}
});

// Create the manifest for the Worker
let prerendered_entries = Array.from(builder.prerendered.pages.entries());

if (builder.config.kit.paths.base) {
prerendered_entries = prerendered_entries.map(([path, { file }]) => [
path,
{ file: `${builder.config.kit.paths.base}/${file}` }
]);
}

writeFileSync(
`${tmp}/manifest.js`,
`${outDir}/manifest.js`,
`export const manifest = ${builder.generateManifest({ relativePath })};\n\n` +
`export const prerendered = new Map(${JSON.stringify(prerendered_entries)});\n\n` +
`export const base_path = ${JSON.stringify(builder.config.kit.paths.base)};\n`
);

const external = ['__STATIC_CONTENT_MANIFEST', 'cloudflare:*'];
if (compatibility_flags && compatibility_flags.includes('nodejs_compat')) {
external.push(...compatible_node_modules.map((id) => `node:${id}`));
}

try {
const result = await esbuild.build({
platform: 'browser',
// https://github.com/cloudflare/workers-sdk/blob/a12b2786ce745f24475174bcec994ad691e65b0f/packages/wrangler/src/deployment-bundle/bundle.ts#L35-L36
conditions: ['workerd', 'worker', 'browser'],
sourcemap: 'linked',
target: 'es2022',
entryPoints: [`${tmp}/entry.js`],
outfile: main,
bundle: true,
external,
alias: Object.fromEntries(compatible_node_modules.map((id) => [id, `node:${id}`])),
format: 'esm',
loader: {
'.wasm': 'copy',
'.woff': 'copy',
'.woff2': 'copy',
'.ttf': 'copy',
'.eot': 'copy',
'.otf': 'copy'
},
logLevel: 'silent'
});

if (result.warnings.length > 0) {
const formatted = await esbuild.formatMessages(result.warnings, {
kind: 'warning',
color: true
});

console.error(formatted.join('\n'));
}
} catch (error) {
for (const e of error.errors) {
for (const node of e.notes) {
const match =
/The package "(.+)" wasn't found on the file system but is built into node/.exec(
node.text
);

if (match) {
node.text = `Cannot use "${match[1]}" when deploying to Cloudflare.`;
}
}
}

const formatted = await esbuild.formatMessages(error.errors, {
kind: 'error',
color: true
});

console.error(formatted.join('\n'));

throw new Error(
`Bundling with esbuild failed with ${error.errors.length} ${
error.errors.length === 1 ? 'error' : 'errors'
}`
);
}

builder.log.minor('Copying assets...');
const bucket_dir = `${site.bucket}${builder.config.kit.paths.base}`;
builder.writeClient(bucket_dir);
builder.writePrerendered(bucket_dir);
const assets_dir = `${assets.directory}${builder.config.kit.paths.base}`;
builder.writeClient(assets_dir);
builder.writePrerendered(assets_dir);
},

async emulate() {
Expand Down Expand Up @@ -198,9 +110,9 @@ function validate_config(builder, config_file) {
throw err;
}

if (!wrangler_config.site?.bucket) {
if (!wrangler_config.assets?.directory) {
throw new Error(
`You must specify site.bucket in ${config_file}. Consult https://developers.cloudflare.com/workers/platform/sites/configuration`
`You must specify assets.directory in ${config_file}. Consult https://developers.cloudflare.com/workers/platform/sites/configuration`
);
}

Expand All @@ -223,14 +135,10 @@ function validate_config(builder, config_file) {
name = "<your-site-name>"
account_id = "<your-account-id>"
main = "./.cloudflare/worker.js"
site.bucket = "./.cloudflare/public"
main = ".svelte-kit/cloudflare/server/index.js"
assets = { directory = ".svelte-kit/cloudflare/client" }
build.command = "npm run build"
compatibility_date = "2021-11-12"
workers_dev = true`
compatibility_date = "2021-11-12"`
.replace(/^\t+/gm, '')
.trim()
);
Expand Down
3 changes: 1 addition & 2 deletions packages/adapter-cloudflare-workers/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,13 +43,12 @@
"esbuild": "^0.21.5"
},
"devDependencies": {
"@cloudflare/kv-asset-handler": "^0.3.0",
"@sveltejs/kit": "workspace:^",
"@types/node": "^18.19.48",
"typescript": "^5.3.3"
},
"peerDependencies": {
"@sveltejs/kit": "^2.0.0",
"wrangler": "^3.28.4"
"wrangler": "^3.87.0"
}
}
5 changes: 0 additions & 5 deletions packages/adapter-cloudflare-workers/placeholders.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,3 @@ declare module 'MANIFEST' {
export const prerendered: Map<string, { file: string }>;
export const base_path: string;
}

declare module '__STATIC_CONTENT_MANIFEST' {
const json: string;
export default json;
}
Loading

0 comments on commit 30d1f46

Please sign in to comment.