diff --git a/.changeset/fuzzy-kings-sit.md b/.changeset/fuzzy-kings-sit.md new file mode 100644 index 000000000000..e08a9fad8bce --- /dev/null +++ b/.changeset/fuzzy-kings-sit.md @@ -0,0 +1,6 @@ +--- +"@sveltejs/adapter-cloudflare": major +--- + +breaking: remove cloudflare `platform`, emulate the `cloudflare:workers` module instead + \ No newline at end of file diff --git a/documentation/docs/25-build-and-deploy/60-adapter-cloudflare.md b/documentation/docs/25-build-and-deploy/60-adapter-cloudflare.md index a7f16b855eae..5271950a043e 100644 --- a/documentation/docs/25-build-and-deploy/60-adapter-cloudflare.md +++ b/documentation/docs/25-build-and-deploy/60-adapter-cloudflare.md @@ -4,7 +4,7 @@ title: Cloudflare To deploy to [Cloudflare Workers](https://workers.cloudflare.com/) or [Cloudflare Pages](https://pages.cloudflare.com/), use [`adapter-cloudflare`](https://github.com/sveltejs/kit/tree/main/packages/adapter-cloudflare). -This adapter will be installed by default when you use [`adapter-auto`](adapter-auto). If you plan on staying with Cloudflare, you can switch from [`adapter-auto`](adapter-auto) to using this adapter directly so that `event.platform` is emulated during local development, type declarations are automatically applied, and the ability to set Cloudflare-specific options is provided. +This adapter will be installed by default when you use [`adapter-auto`](adapter-auto). If you plan on staying with Cloudflare, you can switch from [`adapter-auto`](adapter-auto) to using this adapter directly so that `cloudflare:workers` is emulated during local development, type declarations are automatically applied, and the ability to set Cloudflare-specific options is provided. ## Comparisons @@ -53,7 +53,7 @@ Path to your [Wrangler configuration file](https://developers.cloudflare.com/wor ### platformProxy -Preferences for the emulated `platform.env` local bindings. See the [getPlatformProxy](https://developers.cloudflare.com/workers/wrangler/api/#parameters-1) Wrangler API documentation for a full list of options. +Preferences for the emulated `env` local bindings. See the [getPlatformProxy](https://developers.cloudflare.com/workers/wrangler/api/#parameters-1) Wrangler API documentation for a full list of options. ### fallback @@ -130,62 +130,34 @@ Functions contained in the [`/functions` directory](https://developers.cloudflar ## Runtime APIs -The [`env`](https://developers.cloudflare.com/workers/runtime-apis/fetch-event#parameters) object contains your project's [bindings](https://developers.cloudflare.com/workers/runtime-apis/bindings/), which consist of KV/DO namespaces, etc. It is passed to SvelteKit via the `platform` property, along with [`ctx`](https://developers.cloudflare.com/workers/runtime-apis/context/), [`caches`](https://developers.cloudflare.com/workers/runtime-apis/cache/), and [`cf`](https://developers.cloudflare.com/workers/runtime-apis/request/#incomingrequestcfproperties), meaning that you can access it in hooks and endpoints: +The [`env`](https://developers.cloudflare.com/workers/runtime-apis/fetch-event#parameters) object contains your project's [bindings](https://developers.cloudflare.com/workers/runtime-apis/bindings/), which consist of KV/DO namespaces, etc. It is available on the [`cloudflare:workers`](https://developers.cloudflare.com/workers/runtime-apis/bindings/#importing-env-as-a-global) module. ```js +/// file: +server.js // @filename: ambient.d.ts -import { DurableObjectNamespace } from '@cloudflare/workers-types'; - -declare global { - namespace App { - interface Platform { - env: { - YOUR_DURABLE_OBJECT_NAMESPACE: DurableObjectNamespace; - }; - } - } +declare module 'cloudflare:workers' { + export const env: { + YOUR_DURABLE_OBJECT_NAMESPACE: import('@cloudflare/workers-types').DurableObjectNamespace; + }; } // @filename: +server.js // ---cut--- // @errors: 2355 2322 -/// file: +server.js +import { env } from 'cloudflare:workers'; + /** @type {import('./$types').RequestHandler} */ -export async function POST({ request, platform }) { - const x = platform?.env.YOUR_DURABLE_OBJECT_NAMESPACE.idFromName('x'); +export async function POST() { + const x = env.YOUR_DURABLE_OBJECT_NAMESPACE.idFromName('x'); } ``` > [!NOTE] SvelteKit's built-in [`$app/env/*` modules](environment-variables) should be preferred for environment variables. -To make these types available to your app, install [`wrangler`](https://www.npmjs.com/package/wrangler), run [`wrangler types`](https://developers.cloudflare.com/workers/languages/typescript/), and reference them in your `src/app.d.ts`: - -```ts -// @filename: ambient.d.ts -import { KVNamespace, DurableObjectNamespace } from '@cloudflare/workers-types'; - -namespace Cloudflare { - export interface Env { - YOUR_KV_NAMESPACE: KVNamespace; - YOUR_DURABLE_OBJECT_NAMESPACE: DurableObjectNamespace; - } -} -/// file: src/app.d.ts -// ---cut--- - -declare global { - namespace App { - interface Platform { -+++ env: Cloudflare.Env;+++ - } - } -} - -export {}; -``` +To make these types available to your app, install [`wrangler`](https://www.npmjs.com/package/wrangler) and run [`wrangler types`](https://developers.cloudflare.com/workers/languages/typescript/). ### Testing locally -Cloudflare specific values in the `platform` property are emulated during dev and preview modes. Local [bindings](https://developers.cloudflare.com/workers/wrangler/configuration/#bindings) are created based on your [Wrangler configuration file](https://developers.cloudflare.com/workers/wrangler/) and are used to populate `platform.env` during development and preview. Use the adapter config [`platformProxy` option](#Options-platformProxy) to change your preferences for the bindings. +Cloudflare specific values are emulated during dev and preview modes. Local [bindings](https://developers.cloudflare.com/workers/wrangler/configuration/#bindings) are created based on your [Wrangler configuration file](https://developers.cloudflare.com/workers/wrangler/) and are used to populate `env` during development and preview. Use the adapter config [`platformProxy` option](#Options-platformProxy) to change your preferences for the bindings. > [!NOTE] [Durable Objects](https://developers.cloudflare.com/durable-objects/) and [Workflows](https://developers.cloudflare.com/workflows/), which require custom classes to be exported from your worker, are not currently supported. diff --git a/documentation/docs/60-appendix/35-migrating-to-sveltekit-3.md b/documentation/docs/60-appendix/35-migrating-to-sveltekit-3.md index 8751bc616187..bbb839906cbd 100644 --- a/documentation/docs/60-appendix/35-migrating-to-sveltekit-3.md +++ b/documentation/docs/60-appendix/35-migrating-to-sveltekit-3.md @@ -453,9 +453,45 @@ All first-party adapters now require SvelteKit 3, alongside these adapter-specif ### `adapter-cloudflare` +Cloudflare-specific APIs are no longer available on `platform`. Instead, find them where you would expect on a Cloudflare worker: + +- `env`, `ctx.waitUntil`, and other `ctx` properties should be imported from `cloudflare:workers`: +```js +// @filename: ambient.d.ts +declare module 'cloudflare:workers' { + export const env: { KV: { get(): Promise } }; + export function waitUntil(promise: Promise): void; +} +// ---cut--- +import { env, waitUntil } from 'cloudflare:workers'; + +const value = await env.KV.get('key'); +``` +- `cf` is now a property of the `Request` object: +```js +/// file: src/routes/cf/+server.js +// @filename: ambient.d.ts +interface Request { + cf: import('@cloudflare/workers-types').IncomingRequestCfProperties; +} +// @filename: index.js +// @errors: 7031 +// ---cut--- +export async function GET({ request }) { + const { country } = request.cf; +} +``` +- `caches` is now a global variable: +```js +/// file: src/routes/cache/+server.js +let request = new Request(''); +// ---cut--- +const myCache = await caches.open('foo'); +await myCache.match(request); +``` + - minimum `wrangler` is now `^4.67.0` - `@cloudflare/workers-types` upgraded -- `platform.context` removed in favour of `platform.ctx` ### `adapter-node` diff --git a/packages/adapter-cloudflare/ambient.d.ts b/packages/adapter-cloudflare/ambient.d.ts deleted file mode 100644 index d3c19971446c..000000000000 --- a/packages/adapter-cloudflare/ambient.d.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { - CacheStorage, - IncomingRequestCfProperties, - ExecutionContext -} from '@cloudflare/workers-types'; - -declare global { - namespace App { - export interface Platform { - // we should not type `env` here because it will override any type set by - // the user in `src/app.d.ts` - ctx: ExecutionContext; - caches: CacheStorage; - cf?: IncomingRequestCfProperties; - } - } -} diff --git a/packages/adapter-cloudflare/files/worker.js b/packages/adapter-cloudflare/files/worker.js index 911af0cfb971..2f14adde2038 100644 --- a/packages/adapter-cloudflare/files/worker.js +++ b/packages/adapter-cloudflare/files/worker.js @@ -37,10 +37,9 @@ export default { /** * @param {Request} req * @param {{ ASSETS: { fetch: typeof fetch } }} env - * @param {ExecutionContext} ctx * @returns {Promise} */ - async fetch(req, env, ctx) { + async fetch(req, env) { if (!origin) { origin = new URL(req.url).origin; } @@ -96,12 +95,6 @@ export default { // dynamically-generated pages return await server.respond(req, { - platform: { - env, - ctx, - caches, - cf: req.cf - }, getClientAddress() { return /** @type {string} */ (req.headers.get('cf-connecting-ip')); } diff --git a/packages/adapter-cloudflare/globals.d.ts b/packages/adapter-cloudflare/globals.d.ts new file mode 100644 index 000000000000..a64ccbc1392f --- /dev/null +++ b/packages/adapter-cloudflare/globals.d.ts @@ -0,0 +1,5 @@ +import type { PlatformProxy } from 'wrangler'; + +declare global { + var __platform_proxy: PlatformProxy; +} diff --git a/packages/adapter-cloudflare/index.d.ts b/packages/adapter-cloudflare/index.d.ts index b9062094bd08..9c7266f124e0 100644 --- a/packages/adapter-cloudflare/index.d.ts +++ b/packages/adapter-cloudflare/index.d.ts @@ -1,5 +1,4 @@ import { Adapter } from '@sveltejs/kit'; -import './ambient.js'; import { GetPlatformProxyOptions } from 'wrangler'; export default function plugin(options?: AdapterOptions): Adapter; diff --git a/packages/adapter-cloudflare/index.js b/packages/adapter-cloudflare/index.js index d137538e7194..f57cfd331faa 100644 --- a/packages/adapter-cloudflare/index.js +++ b/packages/adapter-cloudflare/index.js @@ -1,4 +1,7 @@ -import { copyFileSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; +/** @import { Plugin } from 'vite' */ +/** @import { GetPlatformProxyOptions } from 'wrangler' */ + +import fs from 'node:fs'; import path from 'node:path'; import process from 'node:process'; import { fileURLToPath } from 'node:url'; @@ -10,27 +13,35 @@ import { parse_redirects, append_headers } from './utils.js'; +import { exactRegex } from '@rolldown/pluginutils'; +import { getRequest } from '@sveltejs/kit/node'; const name = '@sveltejs/adapter-cloudflare'; /** @type {typeof import('./index.js').default} */ export default function (options = {}) { + // Add a random query so we can reliably string-replace the stub + const stub_import = + import.meta.resolve('./src/virtual-cloudflare-workers.js') + '?' + crypto.randomUUID(); return { name, async adapt(builder) { - if (existsSync('_routes.json') || existsSync(`${builder.config.files.assets}/_routes.json`)) { + if ( + fs.existsSync('_routes.json') || + fs.existsSync(`${builder.config.files.assets}/_routes.json`) + ) { throw new Error( "Cloudflare Pages' _routes.json should be configured from the adapter option of the SvelteKit plugin in your vite.config.js. See https://svelte.dev/docs/kit/adapter-cloudflare#Options-routes" ); } - if (existsSync(`${builder.config.files.assets}/_headers`)) { + if (fs.existsSync(`${builder.config.files.assets}/_headers`)) { throw new Error( `The _headers file should be placed in the project root rather than the ${builder.config.files.assets} directory` ); } - if (existsSync(`${builder.config.files.assets}/_redirects`)) { + if (fs.existsSync(`${builder.config.files.assets}/_redirects`)) { throw new Error( `The _redirects file should be placed in the project root rather than the ${builder.config.files.assets} directory` ); @@ -69,15 +80,17 @@ export default function (options = {}) { const files = fileURLToPath(new URL('./files', import.meta.url).href); const tmp = builder.getBuildDirectory('cloudflare-tmp'); - rmSync(dest, { force: true, recursive: true }); - rmSync(worker_dest, { force: true, recursive: true }); + fs.rmSync(dest, { force: true, recursive: true }); + fs.rmSync(worker_dest, { force: true, recursive: true }); + + fs.mkdirSync(dest, { recursive: true }); + fs.mkdirSync(tmp, { recursive: true }); - mkdirSync(dest, { recursive: true }); - mkdirSync(tmp, { recursive: true }); + replace_stub(builder.getServerDirectory(), stub_import); // client assets and prerendered pages const assets_dest = `${dest}${builder.config.paths.base}`; - mkdirSync(assets_dest, { recursive: true }); + fs.mkdirSync(assets_dest, { recursive: true }); if ( building_for_cloudflare_pages || wrangler_config.assets?.not_found_handling === '404-page' @@ -89,7 +102,7 @@ export default function (options = {}) { if (options.fallback === 'spa') { await builder.generateFallback(fallback); } else { - writeFileSync(fallback, 'Not Found'); + fs.writeFileSync(fallback, 'Not Found'); } } const client_assets = builder.writeClient(assets_dest); @@ -103,7 +116,7 @@ export default function (options = {}) { // worker const worker_dest_dir = path.dirname(worker_dest); - writeFileSync( + fs.writeFileSync( `${tmp}/manifest.js`, `export const manifest = ${builder.generateManifest({ relativePath: path.posix.relative(tmp, builder.getServerDirectory()) })};\n\n` + `export const prerendered = new Set(${JSON.stringify(builder.prerendered.paths)});\n\n` + @@ -133,19 +146,19 @@ export default function (options = {}) { const headers_dest = `${dest}/_headers`; /** @type {string | undefined} */ let headers; - if (existsSync(headers_src)) { - headers = readFileSync(headers_src, 'utf-8'); + if (fs.existsSync(headers_src)) { + headers = fs.readFileSync(headers_src, 'utf-8'); } - writeFileSync(headers_dest, generate_headers(builder.getAppPath(), headers)); + fs.writeFileSync(headers_dest, generate_headers(builder.getAppPath(), headers)); // _redirects const redirects_src = '_redirects'; const redirects_dest = `${dest}/_redirects`; - if (existsSync(redirects_src)) { - copyFileSync(redirects_src, redirects_dest); + if (fs.existsSync(redirects_src)) { + fs.copyFileSync(redirects_src, redirects_dest); } if (builder.prerendered.redirects.size > 0) { - writeFileSync(redirects_dest, generate_redirects(builder.prerendered.redirects), { + fs.writeFileSync(redirects_dest, generate_redirects(builder.prerendered.redirects), { flag: 'a' }); } @@ -158,12 +171,12 @@ export default function (options = {}) { // Worker but instead let the rules in the `_redirects` file take over. /** @type {string[]} */ let redirects = []; - if (existsSync(redirects_dest)) { - const redirect_rules = readFileSync(redirects_dest, 'utf8'); + if (fs.existsSync(redirects_dest)) { + const redirect_rules = fs.readFileSync(redirects_dest, 'utf8'); redirects = parse_redirects(redirect_rules); } - writeFileSync( + fs.writeFileSync( `${dest}/_routes.json`, JSON.stringify( get_routes_json(builder, client_assets, redirects, options.routes ?? {}), @@ -172,46 +185,60 @@ export default function (options = {}) { ) ); } else { - writeFileSync(`${dest}/.assetsignore`, generate_assetsignore(), { flag: 'a' }); + fs.writeFileSync(`${dest}/.assetsignore`, generate_assetsignore(), { flag: 'a' }); } }, - emulate() { - // we want to invoke `getPlatformProxy` only once, but await it only when it is accessed. - // If we would await it here, it would hang indefinitely because the platform proxy only resolves once a request happens - const get_emulated = async () => { - const proxy = await getPlatformProxy(options.platformProxy); - const platform = { - env: proxy.env, - ctx: proxy.ctx, - context: proxy.ctx, // deprecated in favor of ctx - caches: proxy.caches, - cf: proxy.cf - }; - /** @type {Record} */ - const env = {}; - const prerender_platform = /** @type {App.Platform} */ (/** @type {unknown} */ ({ env })); - for (const key in proxy.env) { - Object.defineProperty(env, key, { - get: () => { - throw new Error(`Cannot access platform.env.${key} in a prerenderable route`); - } - }); - } - return { platform, prerender_platform }; - }; - - let emulated; - - return { - platform: async ({ prerender }) => { - emulated ??= await get_emulated(); - return prerender ? emulated.prerender_platform : emulated.platform; - } - }; - }, supports: { read: () => true, instrumentation: () => true + }, + getRequest(options) { + const request = getRequest(options); + /** @type {import('@cloudflare/workers-types').Request} */ ( + /** @type {unknown} */ (request) + ).cf = globalThis.__platform_proxy?.cf; + return request; + }, + vite: { + plugins: { + pre: [ + virtual_workers_module( + { + configPath: options.config, + ...options.platformProxy + }, + stub_import + ) + ] + } + } + }; +} + +/** + * @param {GetPlatformProxyOptions} options + * @param {string} stub_import + * @returns {Plugin} + */ +function virtual_workers_module(options, stub_import) { + const setup = async () => { + if (globalThis.__platform_proxy) return; + const proxy = await getPlatformProxy(options); + globalThis.__platform_proxy = proxy; + /** @type {any} */ (globalThis).caches = proxy.caches; + }; + return { + name: 'vite-plugin-adapter-cloudflare-virtual-workers-module', + configureServer: setup, + configurePreviewServer: setup, + resolveId: { + filter: { id: exactRegex('cloudflare:workers') }, + handler() { + return { + id: stub_import, + external: true + }; + } } }; } @@ -295,3 +322,24 @@ function validate_wrangler_config(config_file = undefined) { function posixify(str) { return str.replace(/\\/g, '/'); } + +/** + * + * @param {string} directory + * @param {string} stub_import + */ +function replace_stub(directory, stub_import) { + // recurse, find stub_import, replace with "cloudflare:workers" + const files = fs.readdirSync(directory); + for (const file of files) { + const file_path = path.join(directory, file); + if (fs.statSync(file_path).isDirectory()) { + replace_stub(file_path, stub_import); + } else { + const contents = fs.readFileSync(file_path, 'utf8'); + if (contents.includes(stub_import)) { + fs.writeFileSync(file_path, contents.replaceAll(stub_import, 'cloudflare:workers')); + } + } + } +} diff --git a/packages/adapter-cloudflare/package.json b/packages/adapter-cloudflare/package.json index 4ad741a03261..3cbbc57b78c0 100644 --- a/packages/adapter-cloudflare/package.json +++ b/packages/adapter-cloudflare/package.json @@ -27,11 +27,11 @@ }, "types": "index.d.ts", "files": [ + "src/virtual-cloudflare-workers.js", "files/worker.js", "index.js", "utils.js", - "index.d.ts", - "ambient.d.ts" + "index.d.ts" ], "scripts": { "lint": "oxfmt --check .", @@ -42,13 +42,15 @@ "test": "pnpm test:unit && pnpm test:e2e" }, "dependencies": { - "@cloudflare/workers-types": "^5.20260809.1" + "@cloudflare/workers-types": "^5.20260813.1", + "@rolldown/pluginutils": "^1.0.1" }, "devDependencies": { "@playwright/test": "catalog:", "@sveltejs/kit": "workspace:^", "@types/node": "catalog:", "typescript": "catalog:typescript-native", + "vite": "catalog:", "vitest": "catalog:" }, "peerDependencies": { diff --git a/packages/adapter-cloudflare/src/virtual-cloudflare-workers.js b/packages/adapter-cloudflare/src/virtual-cloudflare-workers.js new file mode 100644 index 000000000000..35721552a1c7 --- /dev/null +++ b/packages/adapter-cloudflare/src/virtual-cloudflare-workers.js @@ -0,0 +1,154 @@ +import { AsyncLocalStorage } from 'node:async_hooks'; + +const als = new AsyncLocalStorage(); + +const proxy = globalThis.__platform_proxy; + +function get_current_env() { + return als.getStore() ?? proxy.env; +} + +/** @typedef {typeof import('cloudflare:workers')} Module */ + +export const env = new Proxy( + {}, + { + get(_, prop) { + if (!proxy) { + throw new Error(`Cannot access cloudflare:workers in a prerenderable route`); + } + const inner = get_current_env(); + if (inner) { + return Reflect.get(inner, prop); + } + return undefined; + }, + + set(_, prop, newValue) { + if (!proxy) { + throw new Error(`Cannot access cloudflare:workers in a prerenderable route`); + } + const inner = get_current_env(); + if (inner) { + return Reflect.set(inner, prop, newValue); + } + return true; + }, + + has(_, prop) { + if (!proxy) { + throw new Error(`Cannot access cloudflare:workers in a prerenderable route`); + } + const inner = get_current_env(); + if (inner) { + return Reflect.has(inner, prop); + } + return false; + }, + + ownKeys(_) { + if (!proxy) { + throw new Error(`Cannot access cloudflare:workers in a prerenderable route`); + } + const inner = get_current_env(); + if (inner) { + return Reflect.ownKeys(inner); + } + return []; + }, + + deleteProperty(_, prop) { + if (!proxy) { + throw new Error(`Cannot access cloudflare:workers in a prerenderable route`); + } + const inner = get_current_env(); + if (inner) { + return Reflect.deleteProperty(inner, prop); + } + return true; + }, + + defineProperty(_, prop, attr) { + if (!proxy) { + throw new Error(`Cannot access cloudflare:workers in a prerenderable route`); + } + const inner = get_current_env(); + if (inner) { + return Reflect.defineProperty(inner, prop, attr); + } + return true; + }, + + getOwnPropertyDescriptor(_, prop) { + if (!proxy) { + throw new Error(`Cannot access cloudflare:workers in a prerenderable route`); + } + const inner = get_current_env(); + if (inner) { + return Reflect.getOwnPropertyDescriptor(inner, prop); + } + return undefined; + } + } +); + +/** @type {Module['withEnv']} */ +export function withEnv(newEnv, fn) { + if (!proxy) { + throw new Error(`Cannot access cloudflare:workers in a prerenderable route`); + } + return als.run(newEnv, fn); +} +/** @type {Module['withEnvAndExports']} */ +export function withEnvAndExports(newEnv, _, fn) { + if (!proxy) { + throw new Error(`Cannot access cloudflare:workers in a prerenderable route`); + } + return als.run(newEnv, fn); +} + +// no-ops +export const exports = new Proxy( + {}, + { + get() { + throw new Error('exports is not available in dev mode'); + }, + has() { + throw new Error('exports is not available in dev mode'); + } + } +); +/** @type {Module['waitUntil']} */ +export function waitUntil() {} +/** @type {Module['cache']} */ +export const cache = { + purge() { + return Promise.resolve({ success: true, errors: [] }); + } +}; +class Span { + get isTraced() { + return false; + } + setAttribute() { + return this; + } + setAttributes() { + return this; + } + end() {} +} +/** @type {Module['tracing']} */ +export const tracing = { + enterSpan(_, callback, ...args) { + return callback(new Span(), ...args); + }, + startActiveSpan(_, callback, ...args) { + return callback(new Span(), ...args); + }, + startSpan(_) { + return new Span(); + }, + Span +}; diff --git a/packages/adapter-cloudflare/test/apps/workers/.gitignore b/packages/adapter-cloudflare/test/apps/workers/.gitignore index de2e15da78f4..63d9a515f164 100644 --- a/packages/adapter-cloudflare/test/apps/workers/.gitignore +++ b/packages/adapter-cloudflare/test/apps/workers/.gitignore @@ -5,3 +5,4 @@ node_modules # Cloudflare .wrangler /dist +worker-configuration.d.ts diff --git a/packages/adapter-cloudflare/test/apps/workers/config/wrangler.jsonc b/packages/adapter-cloudflare/test/apps/workers/config/wrangler.jsonc index 8c2d83f8c21a..372a381e9b61 100644 --- a/packages/adapter-cloudflare/test/apps/workers/config/wrangler.jsonc +++ b/packages/adapter-cloudflare/test/apps/workers/config/wrangler.jsonc @@ -7,5 +7,8 @@ "assets": { "directory": "../dist/public", "binding": "ASSETS" + }, + "vars": { + "FOO": "from wrangler.jsonc" } } diff --git a/packages/adapter-cloudflare/test/apps/workers/package.json b/packages/adapter-cloudflare/test/apps/workers/package.json index c59a5adc2774..816115baeaf1 100644 --- a/packages/adapter-cloudflare/test/apps/workers/package.json +++ b/packages/adapter-cloudflare/test/apps/workers/package.json @@ -6,6 +6,7 @@ "dev": "vite dev", "build": "vite build", "preview": "wrangler dev dist/index.js --config config/wrangler.jsonc", + "generate:types": "wrangler types --config config/wrangler.jsonc", "test:dev": "DEV=true playwright test", "test:build": "playwright test", "test": "pnpm test:dev && pnpm test:build" diff --git a/packages/adapter-cloudflare/test/apps/workers/src/routes/cf/+server.js b/packages/adapter-cloudflare/test/apps/workers/src/routes/cf/+server.js new file mode 100644 index 000000000000..677e42171f0e --- /dev/null +++ b/packages/adapter-cloudflare/test/apps/workers/src/routes/cf/+server.js @@ -0,0 +1,3 @@ +export function GET({ request }) { + return Response.json(request.cf); +} diff --git a/packages/adapter-cloudflare/test/apps/workers/src/routes/ctx/+server.js b/packages/adapter-cloudflare/test/apps/workers/src/routes/ctx/+server.js deleted file mode 100644 index e26ae2cbbd71..000000000000 --- a/packages/adapter-cloudflare/test/apps/workers/src/routes/ctx/+server.js +++ /dev/null @@ -1,3 +0,0 @@ -export function GET({ platform }) { - return new Response(platform?.ctx.waitUntil ? 'ctx works' : 'ctx does not work'); -} diff --git a/packages/adapter-cloudflare/test/apps/workers/src/routes/env/+server.js b/packages/adapter-cloudflare/test/apps/workers/src/routes/env/+server.js new file mode 100644 index 000000000000..b21afacc72d4 --- /dev/null +++ b/packages/adapter-cloudflare/test/apps/workers/src/routes/env/+server.js @@ -0,0 +1,5 @@ +import { env } from 'cloudflare:workers'; + +export function GET() { + return new Response(env.FOO); +} diff --git a/packages/adapter-cloudflare/test/apps/workers/src/routes/prerender/+server.js b/packages/adapter-cloudflare/test/apps/workers/src/routes/prerender/+server.js new file mode 100644 index 000000000000..0bc773d04e72 --- /dev/null +++ b/packages/adapter-cloudflare/test/apps/workers/src/routes/prerender/+server.js @@ -0,0 +1,12 @@ +import { env } from 'cloudflare:workers'; + +export function GET() { + try { + env.FOO; + return new Response('no error'); + } catch (e) { + return new Response(String(e)); + } +} + +export const prerender = true; diff --git a/packages/adapter-cloudflare/test/apps/workers/test/test.js b/packages/adapter-cloudflare/test/apps/workers/test/test.js index 44832c70890f..8b3ba355fb55 100644 --- a/packages/adapter-cloudflare/test/apps/workers/test/test.js +++ b/packages/adapter-cloudflare/test/apps/workers/test/test.js @@ -7,9 +7,15 @@ test('worker', async ({ page }) => { await expect(page.locator('h1')).toContainText('Sum: 3'); }); -test('ctx', async ({ request }) => { - const res = await request.get('/ctx'); - expect(await res.text()).toBe('ctx works'); +test('cloudflare:workers', async ({ request }) => { + const res = await request.get('/env'); + expect(await res.text()).toBe('from wrangler.jsonc'); +}); + +test('Request.cf', async ({ request }) => { + const res = await request.get('cf'); + const cf = await res.json(); + expect(cf.colo).toBeDefined(); }); test('read from $app/server works', async ({ request }) => { @@ -20,3 +26,9 @@ test('read from $app/server works', async ({ request }) => { const response = await request.get('/read'); expect(await response.text()).toBe(content); }); + +test('prerendering throws', async ({ request }) => { + if (process.env.DEV) return; + const res = await request.get('/prerender'); + expect(await res.text()).toContain('Cannot access cloudflare:workers in a prerenderable route'); +}); diff --git a/packages/adapter-cloudflare/tsconfig.json b/packages/adapter-cloudflare/tsconfig.json index ce628354cbad..700eb7ca2df9 100644 --- a/packages/adapter-cloudflare/tsconfig.json +++ b/packages/adapter-cloudflare/tsconfig.json @@ -13,7 +13,15 @@ // taken from the Cloudflare Workers TypeScript template https://github.com/cloudflare/workers-sdk/blob/main/packages/create-cloudflare/templates/hello-world/ts/tsconfig.json "target": "es2024", "lib": ["es2024"], - "types": ["node"] + "types": ["node", "@cloudflare/workers-types"] }, - "include": ["index.js", "utils.js", "utils.spec.js", "vitest.config.js", "test/utils.js"] + "include": [ + "index.js", + "src", + "utils.js", + "utils.spec.js", + "vitest.config.js", + "globals.d.ts", + "test/utils.js" + ] } diff --git a/packages/kit/src/exports/public.d.ts b/packages/kit/src/exports/public.d.ts index eecdde2b0856..fbf84c2134c1 100644 --- a/packages/kit/src/exports/public.d.ts +++ b/packages/kit/src/exports/public.d.ts @@ -54,14 +54,14 @@ export interface Adapter { instrumentation?: () => boolean; }; /** - * This function overrides the default behavior to convert an `http.IncomingMessage` to a `Request` object. - * To call the original setRequest function, import it from `@sveltejs/kit/node`. + * This function overrides the default behavior in vite dev and preview mode to convert an `http.IncomingMessage` + * to a `Request` object. To call the original setRequest function, import it from `@sveltejs/kit/node`. * @since 3.0.0 */ getRequest?: typeof getRequest; /** - * This function overrides the default behavior to write a `Response` object to an `http.ServerResponse`. - * To call the original setResponse function, import it from `@sveltejs/kit/node`. + * This function overrides the default behavior in vite dev and preview mode to write a `Response` object + * to an `http.ServerResponse`. To call the original setResponse function, import it from `@sveltejs/kit/node`. * @since 3.0.0 */ setResponse?: typeof setResponse; diff --git a/packages/kit/types/index.d.ts b/packages/kit/types/index.d.ts index a1c01f07ef32..f3adb14a4a80 100644 --- a/packages/kit/types/index.d.ts +++ b/packages/kit/types/index.d.ts @@ -40,14 +40,14 @@ declare module '@sveltejs/kit' { instrumentation?: () => boolean; }; /** - * This function overrides the default behavior to convert an `http.IncomingMessage` to a `Request` object. - * To call the original setRequest function, import it from `@sveltejs/kit/node`. + * This function overrides the default behavior in vite dev and preview mode to convert an `http.IncomingMessage` + * to a `Request` object. To call the original setRequest function, import it from `@sveltejs/kit/node`. * @since 3.0.0 */ getRequest?: typeof getRequest; /** - * This function overrides the default behavior to write a `Response` object to an `http.ServerResponse`. - * To call the original setResponse function, import it from `@sveltejs/kit/node`. + * This function overrides the default behavior in vite dev and preview mode to write a `Response` object + * to an `http.ServerResponse`. To call the original setResponse function, import it from `@sveltejs/kit/node`. * @since 3.0.0 */ setResponse?: typeof setResponse; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cb600534f2a7..bbd54ea48e23 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -145,11 +145,14 @@ importers: packages/adapter-cloudflare: dependencies: '@cloudflare/workers-types': - specifier: ^5.20260809.1 - version: 5.20260809.1 + specifier: ^5.20260813.1 + version: 5.20260813.1 + '@rolldown/pluginutils': + specifier: ^1.0.1 + version: 1.0.1 wrangler: specifier: ^4.118.0 - version: 4.120.1(@cloudflare/workers-types@5.20260809.1) + version: 4.120.1(@cloudflare/workers-types@5.20260813.1) devDependencies: '@playwright/test': specifier: 'catalog:' @@ -163,6 +166,9 @@ importers: typescript: specifier: catalog:typescript-native version: 7.0.2 + vite: + specifier: 'catalog:' + version: 8.2.1(@types/node@22.19.19)(esbuild@0.28.1)(jiti@2.4.2)(yaml@2.9.0) vitest: specifier: 'catalog:' version: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@22.19.19)(@vitest/browser-playwright@4.1.10)(jsdom@30.0.1)(vite@8.2.1(@types/node@22.19.19)(esbuild@0.28.1)(jiti@2.4.2)(yaml@2.9.0)) @@ -186,7 +192,7 @@ importers: version: 8.2.1(@types/node@22.19.19)(esbuild@0.28.1)(jiti@2.4.2)(yaml@2.9.0) wrangler: specifier: 'catalog:' - version: 4.120.1(@cloudflare/workers-types@5.20260809.1) + version: 4.120.1(@cloudflare/workers-types@5.20260813.1) packages/adapter-cloudflare/test/apps/workers: devDependencies: @@ -207,7 +213,7 @@ importers: version: 8.2.1(@types/node@22.19.19)(esbuild@0.28.1)(jiti@2.4.2)(yaml@2.9.0) wrangler: specifier: 'catalog:' - version: 4.120.1(@cloudflare/workers-types@5.20260809.1) + version: 4.120.1(@cloudflare/workers-types@5.20260813.1) packages/adapter-netlify: dependencies: @@ -1819,8 +1825,8 @@ packages: cpu: [x64] os: [win32] - '@cloudflare/workers-types@5.20260809.1': - resolution: {integrity: sha512-sBM+0I5lCY9LgTnorn/N2UyrA6KVbUzj9tncxwH8v6sH8tVeAyJj+i0z3xXWY4l4fd1oDcwt8CGf50vGwVISYQ==} + '@cloudflare/workers-types@5.20260813.1': + resolution: {integrity: sha512-RQNfm7xD10hNHEQZFxQPmyGMJ9+aDGPcdFZ0x1LtmjRoLFcgZkGvfaqJAbOMQBAUFSESO3bYJS4p9mOLv28Ihg==} '@cspotcode/source-map-support@0.8.1': resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} @@ -4760,7 +4766,7 @@ snapshots: '@cloudflare/workerd-windows-64@1.20260804.1': optional: true - '@cloudflare/workers-types@5.20260809.1': {} + '@cloudflare/workers-types@5.20260813.1': {} '@cspotcode/source-map-support@0.8.1': dependencies: @@ -7148,7 +7154,7 @@ snapshots: '@cloudflare/workerd-linux-arm64': 1.20260804.1 '@cloudflare/workerd-windows-64': 1.20260804.1 - wrangler@4.120.1(@cloudflare/workers-types@5.20260809.1): + wrangler@4.120.1(@cloudflare/workers-types@5.20260813.1): dependencies: '@cloudflare/kv-asset-handler': 0.5.0 '@cloudflare/unenv-preset': 2.16.1(unenv@2.0.0-rc.24)(workerd@1.20260804.1) @@ -7159,7 +7165,7 @@ snapshots: unenv: 2.0.0-rc.24 workerd: 1.20260804.1 optionalDependencies: - '@cloudflare/workers-types': 5.20260809.1 + '@cloudflare/workers-types': 5.20260813.1 fsevents: 2.3.3 transitivePeerDependencies: - bufferutil