From fcc2464158af6ef4f96602010d0aadbbb1f70462 Mon Sep 17 00:00:00 2001 From: Ottomated Date: Sat, 8 Aug 2026 17:26:03 -0700 Subject: [PATCH 1/4] initial --- packages/adapter-cloudflare/index.js | 41 ++++++- packages/adapter-cloudflare/package.json | 8 +- .../test/apps/workers/.gitignore | 1 + .../test/apps/workers/config/wrangler.jsonc | 25 +++- .../test/apps/workers/package.json | 3 +- .../test/apps/workers/src/app.d.ts | 10 ++ .../test/apps/workers/src/routes/+page.svelte | 7 +- .../apps/workers/src/routes/ws/+server.ts | 4 + .../test/apps/workers/src/worker.ts | 24 ++++ .../test/apps/workers/tsconfig.json | 13 +- .../test/apps/workers/vite.config.js | 12 +- packages/adapter-cloudflare/tsconfig.json | 2 +- packages/adapter-cloudflare/worker.d.ts | 1 + packages/adapter-cloudflare/worker.js | 9 ++ packages/kit/src/exports/vite/dev/index.js | 3 +- pnpm-lock.yaml | 113 +++++++++++------- pnpm-workspace.yaml | 2 +- 17 files changed, 214 insertions(+), 64 deletions(-) create mode 100644 packages/adapter-cloudflare/test/apps/workers/src/app.d.ts create mode 100644 packages/adapter-cloudflare/test/apps/workers/src/routes/ws/+server.ts create mode 100644 packages/adapter-cloudflare/test/apps/workers/src/worker.ts create mode 100644 packages/adapter-cloudflare/worker.d.ts create mode 100644 packages/adapter-cloudflare/worker.js diff --git a/packages/adapter-cloudflare/index.js b/packages/adapter-cloudflare/index.js index 185c964ceafc..ac51166765b5 100644 --- a/packages/adapter-cloudflare/index.js +++ b/packages/adapter-cloudflare/index.js @@ -182,7 +182,10 @@ export default function (options = {}) { // 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 proxy = await getPlatformProxy({ + configPath: options.config, + ...options.platformProxy, + }); const platform = { env: proxy.env, ctx: proxy.ctx, @@ -215,6 +218,42 @@ export default function (options = {}) { supports: { read: () => true, instrumentation: () => true + }, + vite: { + plugins: [ + // ...cloudflare({ + // configPath: 'config/wrangler.jsonc', + // }), + { + name: 'vite-plugin-sveltekit-adapter-cloudflare', + enforce: 'post', + configureServer(server) { + return () => { + const sveltekit_dev_middleware = server.middlewares.stack.find( + (middleware) => + /** @type {Function} */ (middleware.handle).name === 'sveltekitDevMiddleware' + ); + if (!sveltekit_dev_middleware) { + throw new Error( + '@sveltekit/adapter-cloudflare could not find sveltekitDevMiddleware' + ); + } + const handler = /** @type {import('vite').Connect.NextHandleFunction} */ ( + sveltekit_dev_middleware.handle + ); + /** @type {import('vite').Connect.NextHandleFunction} */ + sveltekit_dev_middleware.handle = (req, res, next) => { + if (req.headers['x-sveltekit-cloudflare-handle']) { + delete req.headers['x-sveltekit-cloudflare-handle']; + handler(req, res, next); + return; + } + next(); + }; + }; + } + } + ] } }; } diff --git a/packages/adapter-cloudflare/package.json b/packages/adapter-cloudflare/package.json index 50f117f3fb8f..4d7f98762797 100644 --- a/packages/adapter-cloudflare/package.json +++ b/packages/adapter-cloudflare/package.json @@ -23,6 +23,10 @@ "types": "./index.d.ts", "import": "./index.js" }, + "./worker": { + "types": "./worker.d.ts", + "import": "./worker.js" + }, "./package.json": "./package.json" }, "types": "index.d.ts", @@ -49,10 +53,12 @@ "@sveltejs/kit": "workspace:^", "@types/node": "catalog:", "typescript": "catalog:", + "vite": "catalog:", "vitest": "catalog:" }, "peerDependencies": { "@sveltejs/kit": "^3.0.0-next.0", - "wrangler": "^4.118.0" + "@cloudflare/vite-plugin": "^1.51.0", + "wrangler": "^4.119.0" } } 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..42a5547086ef 100644 --- a/packages/adapter-cloudflare/test/apps/workers/config/wrangler.jsonc +++ b/packages/adapter-cloudflare/test/apps/workers/config/wrangler.jsonc @@ -2,10 +2,31 @@ // to test that the adapter still resolves the paths correctly { "$schema": "../node_modules/wrangler/config-schema.json", - "main": "../dist/index.js", + "name": "adapter-cloudflare-test", + "main": "../src/worker.ts", "compatibility_date": "2026-06-24", "assets": { "directory": "../dist/public", "binding": "ASSETS" - } + }, + "durable_objects": { + "bindings": [ + { + "name": "DO", + "class_name": "DO" + } + ] + }, + "kv_namespaces": [ + { + "binding": "KV", + "remote": false + } + ] + // "migrations": [ + // { + // "tag": "v1", + // "new_classes": ["DO"] + // } + // ] } diff --git a/packages/adapter-cloudflare/test/apps/workers/package.json b/packages/adapter-cloudflare/test/apps/workers/package.json index 2414fec7cb50..5da5a2bb3e8b 100644 --- a/packages/adapter-cloudflare/test/apps/workers/package.json +++ b/packages/adapter-cloudflare/test/apps/workers/package.json @@ -6,12 +6,13 @@ "dev": "vite dev", "build": "vite build", "preview": "wrangler dev dist/index.js --config config/wrangler.jsonc", - "prepare": "svelte-kit sync || echo ''", + "prepare": "svelte-kit sync || echo ''; wrangler types --config config/wrangler.jsonc || echo ''", "test:dev": "DEV=true playwright test", "test:build": "playwright test", "test": "pnpm test:dev && pnpm test:build" }, "devDependencies": { + "@cloudflare/vite-plugin": "^1.51.0", "@sveltejs/kit": "workspace:^", "@sveltejs/vite-plugin-svelte": "catalog:", "server-side-dep": "file:server-side-dep", diff --git a/packages/adapter-cloudflare/test/apps/workers/src/app.d.ts b/packages/adapter-cloudflare/test/apps/workers/src/app.d.ts new file mode 100644 index 000000000000..4448bb67727c --- /dev/null +++ b/packages/adapter-cloudflare/test/apps/workers/src/app.d.ts @@ -0,0 +1,10 @@ +declare global { + namespace App { + interface Platform { + env: Cloudflare.Env; + ctx: Cloudflare.ExecutionContext; + } + } +} + +export {}; diff --git a/packages/adapter-cloudflare/test/apps/workers/src/routes/+page.svelte b/packages/adapter-cloudflare/test/apps/workers/src/routes/+page.svelte index d5e339683387..582fb148b8f2 100644 --- a/packages/adapter-cloudflare/test/apps/workers/src/routes/+page.svelte +++ b/packages/adapter-cloudflare/test/apps/workers/src/routes/+page.svelte @@ -1,5 +1,8 @@ -

Sum: {data.sum}

diff --git a/packages/adapter-cloudflare/test/apps/workers/src/routes/ws/+server.ts b/packages/adapter-cloudflare/test/apps/workers/src/routes/ws/+server.ts new file mode 100644 index 000000000000..57f6db75fc54 --- /dev/null +++ b/packages/adapter-cloudflare/test/apps/workers/src/routes/ws/+server.ts @@ -0,0 +1,4 @@ +export const GET = async ({ platform, request }) => { + const stub = platform!.env.DO.getByName('stub'); + return stub.fetch(request.url, request); +} diff --git a/packages/adapter-cloudflare/test/apps/workers/src/worker.ts b/packages/adapter-cloudflare/test/apps/workers/src/worker.ts new file mode 100644 index 000000000000..1509208c1975 --- /dev/null +++ b/packages/adapter-cloudflare/test/apps/workers/src/worker.ts @@ -0,0 +1,24 @@ +import { DurableObject } from 'cloudflare:workers'; +import { handler } from '../../../../worker.js'; + +export class DO extends DurableObject { + async fetch(_req: Request): Promise { + const { 0: client, 1: server } = new WebSocketPair(); + + this.ctx.acceptWebSocket(server); + setInterval(() => { + server.send('hello'); + }, 1000); + + return new Response(null, { + status: 101, + webSocket: client, + }); + } +} + +export default { + async fetch(request) { + return handler(request); + }, +} satisfies ExportedHandler; diff --git a/packages/adapter-cloudflare/test/apps/workers/tsconfig.json b/packages/adapter-cloudflare/test/apps/workers/tsconfig.json index 0c2092500732..bfb714a809aa 100644 --- a/packages/adapter-cloudflare/test/apps/workers/tsconfig.json +++ b/packages/adapter-cloudflare/test/apps/workers/tsconfig.json @@ -1,13 +1,4 @@ { - "compilerOptions": { - "allowJs": true, - "checkJs": true, - "esModuleInterop": true, - "forceConsistentCasingInFileNames": true, - "resolveJsonModule": true, - "skipLibCheck": true, - "sourceMap": true, - "moduleResolution": "bundler" - }, - "extends": "$app/tsconfig" + "extends": "$app/tsconfig", + "include": ["src", "worker-configuration.d.ts"] } diff --git a/packages/adapter-cloudflare/test/apps/workers/vite.config.js b/packages/adapter-cloudflare/test/apps/workers/vite.config.js index 195f6c80776e..7db96ca9b974 100644 --- a/packages/adapter-cloudflare/test/apps/workers/vite.config.js +++ b/packages/adapter-cloudflare/test/apps/workers/vite.config.js @@ -1,5 +1,6 @@ import { sveltekit } from '@sveltejs/kit/vite'; import adapter from '../../../index.js'; +import { cloudflare } from '@cloudflare/vite-plugin' /** @type {import('vite').UserConfig} */ const config = { @@ -11,7 +12,16 @@ const config = { adapter: adapter({ config: 'config/wrangler.jsonc' }) - }) + }), + cloudflare({ + configPath: 'config/wrangler.jsonc', + config: user_config => { + // Assets are handled by SvelteKit + delete user_config.assets; + + return user_config; + } + }), ] }; diff --git a/packages/adapter-cloudflare/tsconfig.json b/packages/adapter-cloudflare/tsconfig.json index ce628354cbad..afcbf9c5ce74 100644 --- a/packages/adapter-cloudflare/tsconfig.json +++ b/packages/adapter-cloudflare/tsconfig.json @@ -15,5 +15,5 @@ "lib": ["es2024"], "types": ["node"] }, - "include": ["index.js", "utils.js", "utils.spec.js", "vitest.config.js", "test/utils.js"] + "include": ["index.js", "worker.js", "utils.js", "utils.spec.js", "vitest.config.js", "test/utils.js"] } diff --git a/packages/adapter-cloudflare/worker.d.ts b/packages/adapter-cloudflare/worker.d.ts new file mode 100644 index 000000000000..9cd11c778278 --- /dev/null +++ b/packages/adapter-cloudflare/worker.d.ts @@ -0,0 +1 @@ +export function handler(request: Request): Response; diff --git a/packages/adapter-cloudflare/worker.js b/packages/adapter-cloudflare/worker.js new file mode 100644 index 000000000000..5b2d09db92cd --- /dev/null +++ b/packages/adapter-cloudflare/worker.js @@ -0,0 +1,9 @@ +/** + * @param {Request} request + * @returns Response + */ +export function handler(request) { + const headers = new Headers(request.headers); + headers.set('x-sveltekit-cloudflare-handle', 'true'); + return fetch(request, { headers }); +} diff --git a/packages/kit/src/exports/vite/dev/index.js b/packages/kit/src/exports/vite/dev/index.js index 2dec6d74d782..1f6bab5ffb0f 100644 --- a/packages/kit/src/exports/vite/dev/index.js +++ b/packages/kit/src/exports/vite/dev/index.js @@ -502,7 +502,8 @@ export async function dev( // serving routes with those names. See https://github.com/vitejs/vite/issues/7363 remove_static_middlewares(vite_dev_server.middlewares); - vite_dev_server.middlewares.use(async (req, res) => { + // eslint-disable-next-line prefer-arrow-callback + vite_dev_server.middlewares.use(async function sveltekitDevMiddleware(req, res) { // Vite throws a Cannot read properties of undefined (reading 'wrapDynamicImport') // if you try to run ssr.runner.import before the server has started so // we do it inside here to avoid that diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a2a675ac1bdc..9c335159d27c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -94,8 +94,8 @@ catalogs: specifier: ^4.1.10 version: 4.1.10 wrangler: - specifier: ^4.118.0 - version: 4.118.0 + specifier: ^4.119.0 + version: 4.119.0 importers: @@ -143,12 +143,15 @@ importers: packages/adapter-cloudflare: dependencies: + '@cloudflare/vite-plugin': + specifier: ^1.51.0 + version: 1.51.0(vite@8.1.5(@types/node@22.19.19)(esbuild@0.28.1)(jiti@2.4.2)(yaml@2.9.0))(wrangler@4.119.0(@cloudflare/workers-types@5.20260804.1)) '@cloudflare/workers-types': specifier: ^5.20260730.1 version: 5.20260804.1 wrangler: - specifier: ^4.118.0 - version: 4.118.0(@cloudflare/workers-types@5.20260804.1) + specifier: ^4.119.0 + version: 4.119.0(@cloudflare/workers-types@5.20260804.1) devDependencies: '@playwright/test': specifier: 'catalog:' @@ -162,6 +165,9 @@ importers: typescript: specifier: 'catalog:' version: 6.0.3 + vite: + specifier: 'catalog:' + version: 8.1.5(@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.0)(@types/node@22.19.19)(@vitest/browser-playwright@4.1.10)(jsdom@30.0.1)(vite@8.1.5(@types/node@22.19.19)(esbuild@0.28.1)(jiti@2.4.2)(yaml@2.9.0)) @@ -185,10 +191,13 @@ importers: version: 8.1.5(@types/node@22.19.19)(esbuild@0.28.1)(jiti@2.4.2)(yaml@2.9.0) wrangler: specifier: 'catalog:' - version: 4.118.0(@cloudflare/workers-types@5.20260804.1) + version: 4.119.0(@cloudflare/workers-types@5.20260804.1) packages/adapter-cloudflare/test/apps/workers: devDependencies: + '@cloudflare/vite-plugin': + specifier: ^1.51.0 + version: 1.51.0(vite@8.1.5(@types/node@22.19.19)(esbuild@0.28.1)(jiti@2.4.2)(yaml@2.9.0))(wrangler@4.119.0(@cloudflare/workers-types@5.20260804.1)) '@sveltejs/kit': specifier: workspace:^ version: link:../../../../kit @@ -206,7 +215,7 @@ importers: version: 8.1.5(@types/node@22.19.19)(esbuild@0.28.1)(jiti@2.4.2)(yaml@2.9.0) wrangler: specifier: 'catalog:' - version: 4.118.0(@cloudflare/workers-types@5.20260804.1) + version: 4.119.0(@cloudflare/workers-types@5.20260804.1) packages/adapter-netlify: dependencies: @@ -1662,32 +1671,39 @@ packages: workerd: optional: true - '@cloudflare/workerd-darwin-64@1.20260730.1': - resolution: {integrity: sha512-+MBHmPaiTe2KajryW0T24rZvWFxb41hD3d8anNzQqHzft6vSEb18+sp0znSwxgij7ApPhSM1+vhkNg4f3YMguA==} + '@cloudflare/vite-plugin@1.51.0': + resolution: {integrity: sha512-8ltb2RA6U1i7KGDzVsRBDa7+GvWRBNGBx0mjTxdsHh/6zgCcUPLT3I8Kwp/xUkIbInojnwh8GAtnbuLJPfcK6w==} + hasBin: true + peerDependencies: + vite: ^6.1.0 || ^7.0.0 || ^8.0.0 + wrangler: ^4.119.0 + + '@cloudflare/workerd-darwin-64@1.20260801.1': + resolution: {integrity: sha512-wuJWbXpKvncJi1P0GKS+iYpN5tHdb7JPJJ/+6ZQe8zzovHHVMkLJPNBsgWpqeUhpM3g9qTwEKd2rglNKejuh5A==} engines: {node: '>=16'} cpu: [x64] os: [darwin] - '@cloudflare/workerd-darwin-arm64@1.20260730.1': - resolution: {integrity: sha512-SBHKntPkKvNPgaCrTe99xC1CAl8ygJDzlYfK0LbuJ1muKadIw35WnhO0wu894fKBtllsVQdNzDLee+cm0ppLSQ==} + '@cloudflare/workerd-darwin-arm64@1.20260801.1': + resolution: {integrity: sha512-kwoZiTpnhNrF3+APx84Q/oAqvJ3sU9yefGagwm/ASaH/2W19x0vghkW/r4qCoHCK0WW7EPugZ+aXjgPMRtlq1Q==} engines: {node: '>=16'} cpu: [arm64] os: [darwin] - '@cloudflare/workerd-linux-64@1.20260730.1': - resolution: {integrity: sha512-ouyPOSMbiKPeSwUJUvxtMcxGAXs2J4aPE4T5ABIYX5ClcQx5j5bbHTmnqOQEY8sAuLTPjH7dY+iB6UI5ISlwwA==} + '@cloudflare/workerd-linux-64@1.20260801.1': + resolution: {integrity: sha512-r0vAxCZH+Jih9Unm1yoyiByPNWNgawcKciOHDm5Q37ZVGOkKLsT9AtLe3yLSaul76WrKqtf+JP2n0WW32VBLJg==} engines: {node: '>=16'} cpu: [x64] os: [linux] - '@cloudflare/workerd-linux-arm64@1.20260730.1': - resolution: {integrity: sha512-YQ+Mi78U3TPdgBPtwq+Sm6rJU+Ihl2y0pjYtuuKkdmUbYzL7oLR6Xqq9wljhasnuCFICssDJaqhMep5WizYoEQ==} + '@cloudflare/workerd-linux-arm64@1.20260801.1': + resolution: {integrity: sha512-zWgpdZtSozvIgzQNmQiDSF8yEOQJUkRAWNsDXXzAAoy+fCn8YUoSibj3mpFSbZRvbUldeBEaW6SCdC2VEMkhNQ==} engines: {node: '>=16'} cpu: [arm64] os: [linux] - '@cloudflare/workerd-windows-64@1.20260730.1': - resolution: {integrity: sha512-27fAN+vUECW1oYVc1KOcHYpkL8COM2Uxtxql7TL595kxbjoqS5yckw7NLz7bTf2pALFCZWjqXDjZGJ/xbG4ZKQ==} + '@cloudflare/workerd-windows-64@1.20260801.1': + resolution: {integrity: sha512-2oQz+Ksu4ji6e/+ZoYX+tWQEcxAii2p7l+iR8kx48W1llMalaufAsmVxTlk+3/vrM7D3/2c0iK448e0UQTcIMg==} engines: {node: '>=16'} cpu: [x64] os: [win32] @@ -3655,8 +3671,8 @@ packages: mdn-data@2.27.1: resolution: {integrity: sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==} - miniflare@5.20260730.0-alpha: - resolution: {integrity: sha512-8/dspSXDshP6nSkCpjKO7BYc2qZoYSXm7iM+QxY7qJyJpAB3onnQSaiu0cvKJlfuMGwULl55hG69FJCcCMXU1Q==} + miniflare@5.20260801.0-alpha: + resolution: {integrity: sha512-AfMrnQbJg81ESsGkGhOkHBtwTqCG+mosR+3GE+qDrhF1I/ieIvgg3ICxNyBvgHBZ3iapy6cysBQHNPykkzrfgw==} engines: {node: '>=22.0.0'} minimatch@10.2.5: @@ -4296,17 +4312,17 @@ packages: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} - workerd@1.20260730.1: - resolution: {integrity: sha512-zmfNIjwYSWFY5chGBOjWtH3xAE7p97FTC6vR4Ep98290ho6AeAR/NVcBD274YCLEUYzqm8yxdtZlxMybU8a3jA==} + workerd@1.20260801.1: + resolution: {integrity: sha512-/g9JGTyqnHtoIscpBHqKD8swE2V4StBs2i69PmLiOhH45OP95jCFICl4F1hKlgN57rqfni5LiCitttoX5OOkVA==} engines: {node: '>=16'} hasBin: true - wrangler@4.118.0: - resolution: {integrity: sha512-9pkBw/b8zWqGx2S+oLhgHMR1M/4VOE8SynUFABnGWiSFGlcOQ4xiI/B71Xf66RYP2xzngU37IQFPtUruij3lYw==} + wrangler@4.119.0: + resolution: {integrity: sha512-ookClf+zly4DTc8pBMNrwGQzZKH8IpIYTXkjDw3XS7ZvBQ5mLYH6eOvfD5BEpk3U63zTbv91WRlo1UeRSKXa0g==} engines: {node: '>=22.0.0'} hasBin: true peerDependencies: - '@cloudflare/workers-types': ^5.20260730.1 + '@cloudflare/workers-types': ^5.20260801.1 peerDependenciesMeta: '@cloudflare/workers-types': optional: true @@ -4522,25 +4538,38 @@ snapshots: '@cloudflare/kv-asset-handler@0.5.0': {} - '@cloudflare/unenv-preset@2.16.1(unenv@2.0.0-rc.24)(workerd@1.20260730.1)': + '@cloudflare/unenv-preset@2.16.1(unenv@2.0.0-rc.24)(workerd@1.20260801.1)': dependencies: unenv: 2.0.0-rc.24 optionalDependencies: - workerd: 1.20260730.1 + workerd: 1.20260801.1 + + '@cloudflare/vite-plugin@1.51.0(vite@8.1.5(@types/node@22.19.19)(esbuild@0.28.1)(jiti@2.4.2)(yaml@2.9.0))(wrangler@4.119.0(@cloudflare/workers-types@5.20260804.1))': + dependencies: + '@cloudflare/unenv-preset': 2.16.1(unenv@2.0.0-rc.24)(workerd@1.20260801.1) + miniflare: 5.20260801.0-alpha + unenv: 2.0.0-rc.24 + vite: 8.1.5(@types/node@22.19.19)(esbuild@0.28.1)(jiti@2.4.2)(yaml@2.9.0) + workerd: 1.20260801.1 + wrangler: 4.119.0(@cloudflare/workers-types@5.20260804.1) + ws: 8.21.0 + transitivePeerDependencies: + - bufferutil + - utf-8-validate - '@cloudflare/workerd-darwin-64@1.20260730.1': + '@cloudflare/workerd-darwin-64@1.20260801.1': optional: true - '@cloudflare/workerd-darwin-arm64@1.20260730.1': + '@cloudflare/workerd-darwin-arm64@1.20260801.1': optional: true - '@cloudflare/workerd-linux-64@1.20260730.1': + '@cloudflare/workerd-linux-64@1.20260801.1': optional: true - '@cloudflare/workerd-linux-arm64@1.20260730.1': + '@cloudflare/workerd-linux-arm64@1.20260801.1': optional: true - '@cloudflare/workerd-windows-64@1.20260730.1': + '@cloudflare/workerd-windows-64@1.20260801.1': optional: true '@cloudflare/workers-types@5.20260804.1': {} @@ -6302,12 +6331,12 @@ snapshots: mdn-data@2.27.1: {} - miniflare@5.20260730.0-alpha: + miniflare@5.20260801.0-alpha: dependencies: '@cspotcode/source-map-support': 0.8.1 sharp: 0.35.2 undici: 7.28.0 - workerd: 1.20260730.1 + workerd: 1.20260801.1 ws: 8.21.0 youch: 4.1.0-beta.10 transitivePeerDependencies: @@ -6921,24 +6950,24 @@ snapshots: word-wrap@1.2.5: {} - workerd@1.20260730.1: + workerd@1.20260801.1: optionalDependencies: - '@cloudflare/workerd-darwin-64': 1.20260730.1 - '@cloudflare/workerd-darwin-arm64': 1.20260730.1 - '@cloudflare/workerd-linux-64': 1.20260730.1 - '@cloudflare/workerd-linux-arm64': 1.20260730.1 - '@cloudflare/workerd-windows-64': 1.20260730.1 + '@cloudflare/workerd-darwin-64': 1.20260801.1 + '@cloudflare/workerd-darwin-arm64': 1.20260801.1 + '@cloudflare/workerd-linux-64': 1.20260801.1 + '@cloudflare/workerd-linux-arm64': 1.20260801.1 + '@cloudflare/workerd-windows-64': 1.20260801.1 - wrangler@4.118.0(@cloudflare/workers-types@5.20260804.1): + wrangler@4.119.0(@cloudflare/workers-types@5.20260804.1): dependencies: '@cloudflare/kv-asset-handler': 0.5.0 - '@cloudflare/unenv-preset': 2.16.1(unenv@2.0.0-rc.24)(workerd@1.20260730.1) + '@cloudflare/unenv-preset': 2.16.1(unenv@2.0.0-rc.24)(workerd@1.20260801.1) blake3-wasm: 2.1.5 esbuild: 0.28.1 - miniflare: 5.20260730.0-alpha + miniflare: 5.20260801.0-alpha path-to-regexp: 6.3.0 unenv: 2.0.0-rc.24 - workerd: 1.20260730.1 + workerd: 1.20260801.1 optionalDependencies: '@cloudflare/workers-types': 5.20260804.1 fsevents: 2.3.3 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 3bfac531a678..f0a9f5eef2ba 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -78,7 +78,7 @@ catalog: typescript: ^6.0.3 valibot: ^1.4.2 vite: ^8.1.5 - wrangler: ^4.118.0 + wrangler: ^4.119.0 # unit test deps that should be upgraded together '@vitest/browser-playwright': ^4.1.10 From b252d6c530da7d168c796333ba1434d4329ad1e3 Mon Sep 17 00:00:00 2001 From: Ottomated Date: Sat, 8 Aug 2026 23:08:39 -0700 Subject: [PATCH 2/4] actually working somehow?? --- packages/adapter-cloudflare/index.js | 58 ++++++++++++++++++- packages/adapter-cloudflare/package.json | 7 ++- .../test/apps/workers/config/wrangler.jsonc | 5 +- .../test/apps/workers/src/routes/+page.svelte | 16 ++++- .../test/apps/workers/src/worker.ts | 7 ++- .../test/apps/workers/vite.config.js | 1 + packages/kit/src/exports/public.d.ts | 9 +++ packages/kit/src/exports/vite/dev/index.js | 8 ++- packages/kit/src/runtime/server/endpoint.js | 11 ++-- packages/kit/types/index.d.ts | 9 +++ pnpm-lock.yaml | 32 +++++++++- 11 files changed, 146 insertions(+), 17 deletions(-) diff --git a/packages/adapter-cloudflare/index.js b/packages/adapter-cloudflare/index.js index ac51166765b5..54a0321ff226 100644 --- a/packages/adapter-cloudflare/index.js +++ b/packages/adapter-cloudflare/index.js @@ -10,11 +10,15 @@ import { parse_redirects, append_headers } from './utils.js'; +import { ServerResponse } from 'node:http'; +import { coupleWebSocket } from 'miniflare'; +import { WebSocketServer } from 'ws'; const name = '@sveltejs/adapter-cloudflare'; /** @type {import('./index.js').default} */ export default function (options = {}) { + const node_ws_server = new WebSocketServer({ noServer: true }); return { name, async adapt(builder) { @@ -219,6 +223,23 @@ export default function (options = {}) { read: () => true, instrumentation: () => true }, + setResponse(res, response) { + if (!response.webSocket || !res.socket || !(WEBSOCKET_HEAD in res)) return false; + + const socket = res.socket; + res.detachSocket(socket); + node_ws_server.handleUpgrade( + res.req, + socket, + res[WEBSOCKET_HEAD], + (client) => { + void coupleWebSocket(client, response.webSocket); + node_ws_server.emit('connection', client, res.req); + } + ); + + return true; + }, vite: { plugins: [ // ...cloudflare({ @@ -229,6 +250,37 @@ export default function (options = {}) { enforce: 'post', configureServer(server) { return () => { + if (server.httpServer) { + const upgrade_listeners = server.httpServer.listeners('upgrade').filter(listener => listener.name !== 'hmrServerWsListener'); + + for (const listener of upgrade_listeners) { + server.httpServer.removeListener('upgrade', /** @type {() => void} */ (listener)); + } + + /** + * @param {import('http').IncomingMessage} req + * @param {import('net').Socket} socket + * @param {Buffer} head + */ + const upgrade_handler = (req, socket, head) => { + if (req.headers['x-sveltekit-cloudflare-handle']) { + delete req.headers['x-sveltekit-cloudflare-handle']; + req.originalUrl = req.url; + + const res = new ServerResponse(req); + res.assignSocket(socket); + res[WEBSOCKET_HEAD] = head; + handler(req, res); + return; + } + for (const listener of upgrade_listeners) { + listener(req, socket, head); + } + } + + server.httpServer.on('upgrade', upgrade_handler); + + } const sveltekit_dev_middleware = server.middlewares.stack.find( (middleware) => /** @type {Function} */ (middleware.handle).name === 'sveltekitDevMiddleware' @@ -238,14 +290,14 @@ export default function (options = {}) { '@sveltekit/adapter-cloudflare could not find sveltekitDevMiddleware' ); } - const handler = /** @type {import('vite').Connect.NextHandleFunction} */ ( + const handler = /** @type {import('vite').Connect.SimpleHandleFunction} */ ( sveltekit_dev_middleware.handle ); /** @type {import('vite').Connect.NextHandleFunction} */ sveltekit_dev_middleware.handle = (req, res, next) => { if (req.headers['x-sveltekit-cloudflare-handle']) { delete req.headers['x-sveltekit-cloudflare-handle']; - handler(req, res, next); + handler(req, res); return; } next(); @@ -258,6 +310,8 @@ export default function (options = {}) { }; } +const WEBSOCKET_HEAD = Symbol('websocketHead'); + /** * @param {string} app_dir * @param {string | undefined} content existing `_headers` file content diff --git a/packages/adapter-cloudflare/package.json b/packages/adapter-cloudflare/package.json index 4d7f98762797..d98fa4f64407 100644 --- a/packages/adapter-cloudflare/package.json +++ b/packages/adapter-cloudflare/package.json @@ -52,13 +52,16 @@ "@playwright/test": "catalog:", "@sveltejs/kit": "workspace:^", "@types/node": "catalog:", + "@types/ws": "^8.18.1", + "miniflare": "5.20260801.0-alpha", "typescript": "catalog:", "vite": "catalog:", - "vitest": "catalog:" + "vitest": "catalog:", + "ws": "^8.21.2" }, "peerDependencies": { - "@sveltejs/kit": "^3.0.0-next.0", "@cloudflare/vite-plugin": "^1.51.0", + "@sveltejs/kit": "^3.0.0-next.0", "wrangler": "^4.119.0" } } diff --git a/packages/adapter-cloudflare/test/apps/workers/config/wrangler.jsonc b/packages/adapter-cloudflare/test/apps/workers/config/wrangler.jsonc index 42a5547086ef..621caff14ab7 100644 --- a/packages/adapter-cloudflare/test/apps/workers/config/wrangler.jsonc +++ b/packages/adapter-cloudflare/test/apps/workers/config/wrangler.jsonc @@ -2,7 +2,7 @@ // to test that the adapter still resolves the paths correctly { "$schema": "../node_modules/wrangler/config-schema.json", - "name": "adapter-cloudflare-test", + "name": "adapter-cloudflare-test-sveltekit", "main": "../src/worker.ts", "compatibility_date": "2026-06-24", "assets": { @@ -13,7 +13,8 @@ "bindings": [ { "name": "DO", - "class_name": "DO" + "class_name": "DO", + "script_name": "adapter-cloudflare-test" } ] }, diff --git a/packages/adapter-cloudflare/test/apps/workers/src/routes/+page.svelte b/packages/adapter-cloudflare/test/apps/workers/src/routes/+page.svelte index 582fb148b8f2..7cd008c06705 100644 --- a/packages/adapter-cloudflare/test/apps/workers/src/routes/+page.svelte +++ b/packages/adapter-cloudflare/test/apps/workers/src/routes/+page.svelte @@ -1,8 +1,20 @@

Sum: {data.sum}

+ +

WebSocket message: {ws_message}

diff --git a/packages/adapter-cloudflare/test/apps/workers/src/worker.ts b/packages/adapter-cloudflare/test/apps/workers/src/worker.ts index 1509208c1975..bc9120ae1a48 100644 --- a/packages/adapter-cloudflare/test/apps/workers/src/worker.ts +++ b/packages/adapter-cloudflare/test/apps/workers/src/worker.ts @@ -3,11 +3,12 @@ import { handler } from '../../../../worker.js'; export class DO extends DurableObject { async fetch(_req: Request): Promise { + // return new Response('from DO'); const { 0: client, 1: server } = new WebSocketPair(); this.ctx.acceptWebSocket(server); setInterval(() => { - server.send('hello'); + server.send(new Date().toISOString()); }, 1000); return new Response(null, { @@ -18,7 +19,9 @@ export class DO extends DurableObject { } export default { - async fetch(request) { + async fetch(request, env) { return handler(request); + const stub = env.DO.getByName('stub'); + return stub.fetch(request.url, request); }, } satisfies ExportedHandler; diff --git a/packages/adapter-cloudflare/test/apps/workers/vite.config.js b/packages/adapter-cloudflare/test/apps/workers/vite.config.js index 7db96ca9b974..8bb3e14167f6 100644 --- a/packages/adapter-cloudflare/test/apps/workers/vite.config.js +++ b/packages/adapter-cloudflare/test/apps/workers/vite.config.js @@ -18,6 +18,7 @@ const config = { config: user_config => { // Assets are handled by SvelteKit delete user_config.assets; + user_config.name = 'adapter-cloudflare-test'; return user_config; } diff --git a/packages/kit/src/exports/public.d.ts b/packages/kit/src/exports/public.d.ts index 64c852afbf78..0ce6dc06b65c 100644 --- a/packages/kit/src/exports/public.d.ts +++ b/packages/kit/src/exports/public.d.ts @@ -35,6 +35,9 @@ export { PrerenderOption } from '../types/private.js'; // @ts-ignore this is an optional peer dependency so could be missing. Written like this so dts-buddy preserves the ts-ignore type Span = import('@opentelemetry/api').Span; +// @ts-ignore see above +type ServerResponse = import('http').ServerResponse; + type AppErrorWithOptionalStatus = Omit & { status?: App.Error['status'] }; /** @@ -50,6 +53,12 @@ export interface Adapter { * @param builder An object provided by SvelteKit that contains methods for adapting the app */ adapt: (builder: Builder) => MaybePromise; + /** + * This function allows adapters to handle the low-level process of transforming a `Response` into + * the format that vite understands. + * @returns Whether the response was handled + */ + setResponse?: (res: ServerResponse, response: Response) => boolean; /** * Checks called during dev and build to determine whether specific features will work in production with this adapter. */ diff --git a/packages/kit/src/exports/vite/dev/index.js b/packages/kit/src/exports/vite/dev/index.js index 1f6bab5ffb0f..b2783bdf9ad7 100644 --- a/packages/kit/src/exports/vite/dev/index.js +++ b/packages/kit/src/exports/vite/dev/index.js @@ -652,7 +652,13 @@ export async function dev( }); } else { log_response(rendered.status, request); - setResponse(res, rendered); + let adapter_set_response = false; + if (svelte_config.kit.adapter?.setResponse) { + adapter_set_response = svelte_config.kit.adapter.setResponse(res, rendered); + } + if (!adapter_set_response) { + setResponse(res, rendered); + } } } catch (e) { const error = coalesce_to_error(e); diff --git a/packages/kit/src/runtime/server/endpoint.js b/packages/kit/src/runtime/server/endpoint.js index 4d640769dcc4..fc6d244939fa 100644 --- a/packages/kit/src/runtime/server/endpoint.js +++ b/packages/kit/src/runtime/server/endpoint.js @@ -45,11 +45,12 @@ export async function render_endpoint(event, state, mod) { handler(/** @type {import('@sveltejs/kit').RequestEvent>} */ (event)) ); - if (!(response instanceof Response)) { - throw new Error( - `Invalid response from route ${event.url.pathname}: handler should return a Response object` - ); - } + // TODO: need to accept Response-like objects here, such as from a different undici version + // if (!(response instanceof Response)) { + // throw new Error( + // `Invalid response from route ${event.url.pathname}: handler should return a Response object` + // ); + // } if (state.prerendering && (!state.prerendering.inside_reroute || prerender)) { // The returned Response might have immutable Headers diff --git a/packages/kit/types/index.d.ts b/packages/kit/types/index.d.ts index 6122f53e10aa..dee05ec519da 100644 --- a/packages/kit/types/index.d.ts +++ b/packages/kit/types/index.d.ts @@ -9,6 +9,9 @@ declare module '@sveltejs/kit' { // @ts-ignore this is an optional peer dependency so could be missing. Written like this so dts-buddy preserves the ts-ignore type Span = import('@opentelemetry/api').Span; + // @ts-ignore see above + type ServerResponse = import('http').ServerResponse; + type AppErrorWithOptionalStatus = Omit & { status?: App.Error['status'] }; /** @@ -24,6 +27,12 @@ declare module '@sveltejs/kit' { * @param builder An object provided by SvelteKit that contains methods for adapting the app */ adapt: (builder: Builder) => MaybePromise; + /** + * This function allows adapters to handle the low-level process of transforming a `Response` into + * the format that vite understands. + * @returns Whether the response was handled + */ + setResponse?: (res: ServerResponse, response: Response) => boolean; /** * Checks called during dev and build to determine whether specific features will work in production with this adapter. */ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9c335159d27c..f23451fa77b6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -162,6 +162,12 @@ importers: '@types/node': specifier: 'catalog:' version: 22.19.19 + '@types/ws': + specifier: ^8.18.1 + version: 8.18.1 + miniflare: + specifier: 5.20260801.0-alpha + version: 5.20260801.0-alpha typescript: specifier: 'catalog:' version: 6.0.3 @@ -171,6 +177,9 @@ importers: vitest: specifier: 'catalog:' version: 4.1.10(@opentelemetry/api@1.9.0)(@types/node@22.19.19)(@vitest/browser-playwright@4.1.10)(jsdom@30.0.1)(vite@8.1.5(@types/node@22.19.19)(esbuild@0.28.1)(jiti@2.4.2)(yaml@2.9.0)) + ws: + specifier: ^8.21.2 + version: 8.21.2 packages/adapter-cloudflare/test/apps/pages: devDependencies: @@ -2937,6 +2946,9 @@ packages: '@types/trusted-types@2.0.7': resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} + '@types/ws@8.18.1': + resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==} + '@typescript-eslint/eslint-plugin@8.58.0': resolution: {integrity: sha512-RLkVSiNuUP1C2ROIWfqX+YcUfLaSnxGE/8M+Y57lopVwg9VTYYfhuz15Yf1IzCKgZj6/rIbYTmJCUSqr76r0Wg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -4343,6 +4355,18 @@ packages: utf-8-validate: optional: true + ws@8.21.2: + resolution: {integrity: sha512-54dMVAo4WIe6SKy3vBgN+9bJZqqQ8IMRevAkOLQALhi49qkkQDQfWdAZ8KQlXiEabw88ARXXdUrlvtbKQX+aKw==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + xml-name-validator@5.0.0: resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==} engines: {node: '>=18'} @@ -5559,6 +5583,10 @@ snapshots: '@types/trusted-types@2.0.7': {} + '@types/ws@8.18.1': + dependencies: + '@types/node': 22.19.19 + '@typescript-eslint/eslint-plugin@8.58.0(@typescript-eslint/parser@8.58.0(eslint@10.8.0(jiti@2.4.2)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3))(eslint@10.8.0(jiti@2.4.2)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3)': dependencies: '@eslint-community/regexpp': 4.12.2 @@ -5694,7 +5722,7 @@ snapshots: sirv: 3.0.2 tinyrainbow: 3.1.0 vitest: 4.1.10(@opentelemetry/api@1.9.0)(@types/node@22.19.19)(@vitest/browser-playwright@4.1.10)(jsdom@30.0.1)(vite@8.1.5(@types/node@22.19.19)(esbuild@0.28.1)(jiti@2.4.2)(yaml@2.9.0)) - ws: 8.21.0 + ws: 8.21.2 transitivePeerDependencies: - bufferutil - msw @@ -6983,6 +7011,8 @@ snapshots: ws@8.21.0: {} + ws@8.21.2: {} + xml-name-validator@5.0.0: {} xmlchars@2.2.0: {} From 4a2f51f03f67156dcb15bc2b22b60832dd3b0cab Mon Sep 17 00:00:00 2001 From: Ottomated Date: Sun, 9 Aug 2026 10:16:39 -0700 Subject: [PATCH 3/4] clean up types --- packages/adapter-cloudflare/index.js | 19 +++++++++++++------ .../test/apps/workers/src/worker.ts | 5 +---- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/packages/adapter-cloudflare/index.js b/packages/adapter-cloudflare/index.js index 54a0321ff226..86fd5d15f1cd 100644 --- a/packages/adapter-cloudflare/index.js +++ b/packages/adapter-cloudflare/index.js @@ -223,18 +223,24 @@ export default function (options = {}) { read: () => true, instrumentation: () => true }, + /** + * + * @param {WebSocketServerResponse} res + * @param {Response & { webSocket?: import('miniflare').WebSocket }} response + */ setResponse(res, response) { - if (!response.webSocket || !res.socket || !(WEBSOCKET_HEAD in res)) return false; + if (!response.webSocket || !res.socket || !res[WEBSOCKET_HEAD]) return false; const socket = res.socket; + const worker_socket = response.webSocket; res.detachSocket(socket); node_ws_server.handleUpgrade( res.req, socket, res[WEBSOCKET_HEAD], - (client) => { - void coupleWebSocket(client, response.webSocket); - node_ws_server.emit('connection', client, res.req); + (client_socket) => { + void coupleWebSocket(client_socket, worker_socket); + node_ws_server.emit('connection', client_socket, res.req); } ); @@ -258,7 +264,7 @@ export default function (options = {}) { } /** - * @param {import('http').IncomingMessage} req + * @param {import('vite').Connect.IncomingMessage} req * @param {import('net').Socket} socket * @param {Buffer} head */ @@ -269,7 +275,7 @@ export default function (options = {}) { const res = new ServerResponse(req); res.assignSocket(socket); - res[WEBSOCKET_HEAD] = head; + (/** @type {WebSocketServerResponse} */ (res))[WEBSOCKET_HEAD] = head; handler(req, res); return; } @@ -311,6 +317,7 @@ export default function (options = {}) { } const WEBSOCKET_HEAD = Symbol('websocketHead'); +/** @typedef {import('http').ServerResponse & { [WEBSOCKET_HEAD]?: Buffer }} WebSocketServerResponse */ /** * @param {string} app_dir diff --git a/packages/adapter-cloudflare/test/apps/workers/src/worker.ts b/packages/adapter-cloudflare/test/apps/workers/src/worker.ts index bc9120ae1a48..200cd77650b1 100644 --- a/packages/adapter-cloudflare/test/apps/workers/src/worker.ts +++ b/packages/adapter-cloudflare/test/apps/workers/src/worker.ts @@ -3,7 +3,6 @@ import { handler } from '../../../../worker.js'; export class DO extends DurableObject { async fetch(_req: Request): Promise { - // return new Response('from DO'); const { 0: client, 1: server } = new WebSocketPair(); this.ctx.acceptWebSocket(server); @@ -19,9 +18,7 @@ export class DO extends DurableObject { } export default { - async fetch(request, env) { + async fetch(request) { return handler(request); - const stub = env.DO.getByName('stub'); - return stub.fetch(request.url, request); }, } satisfies ExportedHandler; From f356c53f8ad0cde513cbd0c4757b823b95c3420d Mon Sep 17 00:00:00 2001 From: Ottomated Date: Sun, 9 Aug 2026 22:33:58 -0700 Subject: [PATCH 4/4] transform user config, virtual cloudflare:workers module --- .../99-writing-adapters.md | 7 +- packages/adapter-cloudflare/index.js | 238 ++++++++++++------ packages/adapter-cloudflare/package.json | 1 + .../test/apps/workers/config/wrangler.jsonc | 17 +- .../test/apps/workers/package.json | 1 - .../apps/workers/src/routes/ws/+server.js | 6 + .../apps/workers/src/routes/ws/+server.ts | 4 - .../test/apps/workers/vite.config.js | 11 - .../kit/src/exports/internal/server/index.js | 2 + packages/kit/src/exports/public.d.ts | 9 +- packages/kit/src/exports/vite/index.js | 5 +- packages/kit/types/index.d.ts | 9 +- pnpm-lock.yaml | 6 +- 13 files changed, 199 insertions(+), 117 deletions(-) create mode 100644 packages/adapter-cloudflare/test/apps/workers/src/routes/ws/+server.js delete mode 100644 packages/adapter-cloudflare/test/apps/workers/src/routes/ws/+server.ts diff --git a/documentation/docs/25-build-and-deploy/99-writing-adapters.md b/documentation/docs/25-build-and-deploy/99-writing-adapters.md index 204445e5e889..e3b76df224aa 100644 --- a/documentation/docs/25-build-and-deploy/99-writing-adapters.md +++ b/documentation/docs/25-build-and-deploy/99-writing-adapters.md @@ -41,8 +41,11 @@ export default function (options) { } }, vite: { - plugins: [ - // add plugins here to integrate with Vite + pre_plugins: [ + // add plugins here... + ], + post_plugins: [ + // ...or here to integrate with Vite ] } }; diff --git a/packages/adapter-cloudflare/index.js b/packages/adapter-cloudflare/index.js index 86fd5d15f1cd..e10020400569 100644 --- a/packages/adapter-cloudflare/index.js +++ b/packages/adapter-cloudflare/index.js @@ -13,14 +13,16 @@ import { import { ServerResponse } from 'node:http'; import { coupleWebSocket } from 'miniflare'; import { WebSocketServer } from 'ws'; - -const name = '@sveltejs/adapter-cloudflare'; +import { cloudflare } from '@cloudflare/vite-plugin'; +import { exactRegex } from '@rolldown/pluginutils'; +// @ts-expect-error types are private +import { dedent } from '@sveltejs/kit/internal'; /** @type {import('./index.js').default} */ export default function (options = {}) { const node_ws_server = new WebSocketServer({ noServer: true }); return { - name, + name: '@sveltejs/adapter-cloudflare', async adapt(builder) { if ( existsSync('_routes.json') || @@ -186,9 +188,12 @@ export default function (options = {}) { // 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 () => { + // TODO - need access to the vite dev server here + // const runner = get_runner(vite, server); + // runner.import('cloudflare:workers'); const proxy = await getPlatformProxy({ - configPath: options.config, - ...options.platformProxy, + configPath: '.svelte-kit/cloudflare-tmp/wrangler.json', + ...options.platformProxy }); const platform = { env: proxy.env, @@ -224,9 +229,9 @@ export default function (options = {}) { instrumentation: () => true }, /** - * - * @param {WebSocketServerResponse} res - * @param {Response & { webSocket?: import('miniflare').WebSocket }} response + * + * @param {WebSocketServerResponse} res + * @param {Response & { webSocket?: import('miniflare').WebSocket }} response */ setResponse(res, response) { if (!response.webSocket || !res.socket || !res[WEBSOCKET_HEAD]) return false; @@ -234,90 +239,161 @@ export default function (options = {}) { const socket = res.socket; const worker_socket = response.webSocket; res.detachSocket(socket); - node_ws_server.handleUpgrade( - res.req, - socket, - res[WEBSOCKET_HEAD], - (client_socket) => { - void coupleWebSocket(client_socket, worker_socket); - node_ws_server.emit('connection', client_socket, res.req); - } - ); + node_ws_server.handleUpgrade(res.req, socket, res[WEBSOCKET_HEAD], (client_socket) => { + void coupleWebSocket(client_socket, worker_socket); + node_ws_server.emit('connection', client_socket, res.req); + }); return true; }, vite: { - plugins: [ - // ...cloudflare({ - // configPath: 'config/wrangler.jsonc', - // }), - { - name: 'vite-plugin-sveltekit-adapter-cloudflare', - enforce: 'post', - configureServer(server) { - return () => { - if (server.httpServer) { - const upgrade_listeners = server.httpServer.listeners('upgrade').filter(listener => listener.name !== 'hmrServerWsListener'); - - for (const listener of upgrade_listeners) { - server.httpServer.removeListener('upgrade', /** @type {() => void} */ (listener)); - } - - /** - * @param {import('vite').Connect.IncomingMessage} req - * @param {import('net').Socket} socket - * @param {Buffer} head - */ - const upgrade_handler = (req, socket, head) => { - if (req.headers['x-sveltekit-cloudflare-handle']) { - delete req.headers['x-sveltekit-cloudflare-handle']; - req.originalUrl = req.url; - - const res = new ServerResponse(req); - res.assignSocket(socket); - (/** @type {WebSocketServerResponse} */ (res))[WEBSOCKET_HEAD] = head; - handler(req, res); - return; - } - for (const listener of upgrade_listeners) { - listener(req, socket, head); - } - } - - server.httpServer.on('upgrade', upgrade_handler); - - } - const sveltekit_dev_middleware = server.middlewares.stack.find( - (middleware) => - /** @type {Function} */ (middleware.handle).name === 'sveltekitDevMiddleware' - ); - if (!sveltekit_dev_middleware) { - throw new Error( - '@sveltekit/adapter-cloudflare could not find sveltekitDevMiddleware' - ); - } - const handler = /** @type {import('vite').Connect.SimpleHandleFunction} */ ( - sveltekit_dev_middleware.handle - ); - /** @type {import('vite').Connect.NextHandleFunction} */ - sveltekit_dev_middleware.handle = (req, res, next) => { - if (req.headers['x-sveltekit-cloudflare-handle']) { - delete req.headers['x-sveltekit-cloudflare-handle']; - handler(req, res); - return; - } - next(); - }; - }; + pre_plugins: [listener_plugin, virtual_modules_plugin(options)], + post_plugins: [ + // Cloudflare's plugin needs to be after SvelteKit so sveltekit's middleware + // can read the requests first and ignore them if necessary. + ...cloudflare({ + configPath: options.config, + config: (user_config) => { + // Assets are handled by SvelteKit + delete user_config.assets; + return user_config; } - } + }) ] } }; } +/** + * @param {import('./index.js').AdapterOptions} options + * @returns {import('vite').Plugin} + * */ +function virtual_modules_plugin(options) { + const modules = ['cloudflare:workers', 'virtual:todo-name-cloudflare-handler']; + /** @type {string} */ + let out_dir; + const { wrangler_config } = validate_wrangler_config(options.config); + const worker_name = wrangler_config.name ?? 'worker'; + // Rename the worker because two workers are running at once + wrangler_config.name = worker_name + '-sveltekit'; + // Point all durable objects at the worker ran by @cloudflare/vite-plugin + for (const binding of wrangler_config.durable_objects?.bindings ?? []) { + if (binding.script_name === undefined) { + binding.script_name = worker_name; + } + } + // squash warnings + if (Object.keys(wrangler_config.unsafe).length === 0) { + delete /** @type {{ unsafe?: {} }} */ (wrangler_config).unsafe; + } + + return { + name: 'vite-plugin-sveltekit-adapter-cloudflare-virtual-modules', + configResolved(config) { + const plugin = config.plugins.find((plugin) => plugin.name === 'vite-plugin-sveltekit-setup'); + const options = plugin?.api?.options; + if (!options) throw new Error('vite-plugin-sveltekit-setup not found'); + out_dir = options.kit.outDir; + }, + resolveId: { + filter: { + id: modules.map((m) => exactRegex(m)) + }, + handler(id) { + return '\0' + id; + } + }, + load: { + filter: { + id: modules.map((m) => exactRegex('\0' + m)) + }, + handler(id) { + if (id === '\0cloudflare:workers') { + return dedent` + import { getPlatformProxy } from 'wrangler'; + import { writeFileSync, mkdirSync } from 'node:fs'; + + const tmp_dir = ${JSON.stringify(out_dir + '/cloudflare-tmp')}; + mkdirSync(tmp_dir, { recursive: true }); + const tmp_config_file = tmp_dir + '/wrangler.json'; + writeFileSync(tmp_config_file, ${JSON.stringify(JSON.stringify(wrangler_config))}); + const proxy = await getPlatformProxy({ + ...${JSON.stringify(options.platformProxy ?? {})}, + configPath: tmp_config_file + }); + export const env = proxy.env; + export const waitUntil = () => {}; + // TODO - stub other exports + `; + } + } + } + }; +} + + const WEBSOCKET_HEAD = Symbol('websocketHead'); /** @typedef {import('http').ServerResponse & { [WEBSOCKET_HEAD]?: Buffer }} WebSocketServerResponse */ +/** @type {import('vite').Plugin} */ +const listener_plugin = { + name: 'vite-plugin-sveltekit-adapter-cloudflare-listeners', + enforce: 'post', + configureServer(server) { + return () => { + if (server.httpServer) { + const upgrade_listeners = server.httpServer + .listeners('upgrade') + .filter((listener) => listener.name !== 'hmrServerWsListener'); + + for (const listener of upgrade_listeners) { + server.httpServer.removeListener('upgrade', /** @type {() => void} */ (listener)); + } + + /** + * @param {import('vite').Connect.IncomingMessage} req + * @param {import('net').Socket} socket + * @param {Buffer} head + */ + const upgrade_handler = (req, socket, head) => { + if (req.headers['x-sveltekit-cloudflare-handle']) { + delete req.headers['x-sveltekit-cloudflare-handle']; + req.originalUrl = req.url; + + const res = new ServerResponse(req); + res.assignSocket(socket); + /** @type {WebSocketServerResponse} */ (res)[WEBSOCKET_HEAD] = head; + handler(req, res); + return; + } + for (const listener of upgrade_listeners) { + listener(req, socket, head); + } + }; + + server.httpServer.on('upgrade', upgrade_handler); + } + const sveltekit_dev_middleware = server.middlewares.stack.find( + (middleware) => + /** @type {Function} */ (middleware.handle).name === 'sveltekitDevMiddleware' + ); + if (!sveltekit_dev_middleware) { + throw new Error('@sveltekit/adapter-cloudflare could not find sveltekitDevMiddleware'); + } + const handler = /** @type {import('vite').Connect.SimpleHandleFunction} */ ( + sveltekit_dev_middleware.handle + ); + /** @type {import('vite').Connect.NextHandleFunction} */ + sveltekit_dev_middleware.handle = (req, res, next) => { + if (req.headers['x-sveltekit-cloudflare-handle']) { + delete req.headers['x-sveltekit-cloudflare-handle']; + handler(req, res); + return; + } + next(); + }; + }; + } +}; /** * @param {string} app_dir @@ -377,7 +453,7 @@ _redirects * building_for_cloudflare_pages: boolean * }} */ -function validate_wrangler_config(config_file = undefined) { +function validate_wrangler_config(config_file) { const wrangler_config = unstable_readConfig({ config: config_file }); const building_for_cloudflare_pages = is_building_for_cloudflare_pages(wrangler_config); diff --git a/packages/adapter-cloudflare/package.json b/packages/adapter-cloudflare/package.json index d98fa4f64407..0ee3589bf7bd 100644 --- a/packages/adapter-cloudflare/package.json +++ b/packages/adapter-cloudflare/package.json @@ -50,6 +50,7 @@ }, "devDependencies": { "@playwright/test": "catalog:", + "@rolldown/pluginutils": "^1.0.1", "@sveltejs/kit": "workspace:^", "@types/node": "catalog:", "@types/ws": "^8.18.1", diff --git a/packages/adapter-cloudflare/test/apps/workers/config/wrangler.jsonc b/packages/adapter-cloudflare/test/apps/workers/config/wrangler.jsonc index 621caff14ab7..d0ad76b1637f 100644 --- a/packages/adapter-cloudflare/test/apps/workers/config/wrangler.jsonc +++ b/packages/adapter-cloudflare/test/apps/workers/config/wrangler.jsonc @@ -2,7 +2,7 @@ // to test that the adapter still resolves the paths correctly { "$schema": "../node_modules/wrangler/config-schema.json", - "name": "adapter-cloudflare-test-sveltekit", + "name": "adapter-cloudflare-test", "main": "../src/worker.ts", "compatibility_date": "2026-06-24", "assets": { @@ -13,21 +13,20 @@ "bindings": [ { "name": "DO", - "class_name": "DO", - "script_name": "adapter-cloudflare-test" + "class_name": "DO" } ] }, + "exports": { + "DO": { + "type": "durable-object", + "storage": "sqlite" + } + }, "kv_namespaces": [ { "binding": "KV", "remote": false } ] - // "migrations": [ - // { - // "tag": "v1", - // "new_classes": ["DO"] - // } - // ] } diff --git a/packages/adapter-cloudflare/test/apps/workers/package.json b/packages/adapter-cloudflare/test/apps/workers/package.json index 5da5a2bb3e8b..00e7521c5ba5 100644 --- a/packages/adapter-cloudflare/test/apps/workers/package.json +++ b/packages/adapter-cloudflare/test/apps/workers/package.json @@ -12,7 +12,6 @@ "test": "pnpm test:dev && pnpm test:build" }, "devDependencies": { - "@cloudflare/vite-plugin": "^1.51.0", "@sveltejs/kit": "workspace:^", "@sveltejs/vite-plugin-svelte": "catalog:", "server-side-dep": "file:server-side-dep", diff --git a/packages/adapter-cloudflare/test/apps/workers/src/routes/ws/+server.js b/packages/adapter-cloudflare/test/apps/workers/src/routes/ws/+server.js new file mode 100644 index 000000000000..b62a401ecd83 --- /dev/null +++ b/packages/adapter-cloudflare/test/apps/workers/src/routes/ws/+server.js @@ -0,0 +1,6 @@ +import { env } from 'cloudflare:workers'; + +export const GET = async ({ request }) => { + const stub = env.DO.getByName('stub'); + return stub.fetch(request.url, request); +} diff --git a/packages/adapter-cloudflare/test/apps/workers/src/routes/ws/+server.ts b/packages/adapter-cloudflare/test/apps/workers/src/routes/ws/+server.ts deleted file mode 100644 index 57f6db75fc54..000000000000 --- a/packages/adapter-cloudflare/test/apps/workers/src/routes/ws/+server.ts +++ /dev/null @@ -1,4 +0,0 @@ -export const GET = async ({ platform, request }) => { - const stub = platform!.env.DO.getByName('stub'); - return stub.fetch(request.url, request); -} diff --git a/packages/adapter-cloudflare/test/apps/workers/vite.config.js b/packages/adapter-cloudflare/test/apps/workers/vite.config.js index 8bb3e14167f6..2e7d6f775655 100644 --- a/packages/adapter-cloudflare/test/apps/workers/vite.config.js +++ b/packages/adapter-cloudflare/test/apps/workers/vite.config.js @@ -1,6 +1,5 @@ import { sveltekit } from '@sveltejs/kit/vite'; import adapter from '../../../index.js'; -import { cloudflare } from '@cloudflare/vite-plugin' /** @type {import('vite').UserConfig} */ const config = { @@ -13,16 +12,6 @@ const config = { config: 'config/wrangler.jsonc' }) }), - cloudflare({ - configPath: 'config/wrangler.jsonc', - config: user_config => { - // Assets are handled by SvelteKit - delete user_config.assets; - user_config.name = 'adapter-cloudflare-test'; - - return user_config; - } - }), ] }; diff --git a/packages/kit/src/exports/internal/server/index.js b/packages/kit/src/exports/internal/server/index.js index a48db594f0db..1b81e1393748 100644 --- a/packages/kit/src/exports/internal/server/index.js +++ b/packages/kit/src/exports/internal/server/index.js @@ -34,4 +34,6 @@ export { init_remote_functions } from './remote-functions.js'; export { init_tracing, otel, record_span } from './telemetry.js'; +export { dedent } from '../../../core/sync/utils.js'; + export * from '../shared.js'; diff --git a/packages/kit/src/exports/public.d.ts b/packages/kit/src/exports/public.d.ts index 0ce6dc06b65c..c615d89b6d57 100644 --- a/packages/kit/src/exports/public.d.ts +++ b/packages/kit/src/exports/public.d.ts @@ -82,10 +82,15 @@ export interface Adapter { emulate?: () => MaybePromise; vite?: { /** - * Plugins provided by the adapter are placed before any of SvelteKit's own plugins. + * Vite plugins placed before any of SvelteKit's own plugins. * @since 3.0.0 */ - plugins?: Plugin[]; + pre_plugins?: Plugin[]; + /** + * Vite plugins placed after any of SvelteKit's own plugins. + * @since 3.0.0 + */ + post_plugins?: Plugin[]; }; } diff --git a/packages/kit/src/exports/vite/index.js b/packages/kit/src/exports/vite/index.js index a37f5e64fa6c..8b3785807abc 100644 --- a/packages/kit/src/exports/vite/index.js +++ b/packages/kit/src/exports/vite/index.js @@ -2056,7 +2056,7 @@ function kit({ svelte_config }) { return /** @type {Plugin[]} */ ( [ - svelte_config.kit.adapter?.vite?.plugins, + svelte_config.kit.adapter?.vite?.pre_plugins, plugin_resolve_root, plugin_setup, plugin_remote_guard, @@ -2066,7 +2066,8 @@ function kit({ svelte_config }) { plugin_service_worker, plugin_service_worker_env, plugin_compile, - plugin_adapter + plugin_adapter, + svelte_config.kit.adapter?.vite?.post_plugins, ].filter(Boolean) ); } diff --git a/packages/kit/types/index.d.ts b/packages/kit/types/index.d.ts index dee05ec519da..7472d78c9b46 100644 --- a/packages/kit/types/index.d.ts +++ b/packages/kit/types/index.d.ts @@ -56,10 +56,15 @@ declare module '@sveltejs/kit' { emulate?: () => MaybePromise; vite?: { /** - * Plugins provided by the adapter are placed before any of SvelteKit's own plugins. + * Vite plugins placed before any of SvelteKit's own plugins. * @since 3.0.0 */ - plugins?: Plugin[]; + pre_plugins?: Plugin[]; + /** + * Vite plugins placed after any of SvelteKit's own plugins. + * @since 3.0.0 + */ + post_plugins?: Plugin[]; }; } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f23451fa77b6..995458026e7f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -156,6 +156,9 @@ importers: '@playwright/test': specifier: 'catalog:' version: 1.62.1 + '@rolldown/pluginutils': + specifier: ^1.0.1 + version: 1.0.1 '@sveltejs/kit': specifier: workspace:^ version: link:../kit @@ -204,9 +207,6 @@ importers: packages/adapter-cloudflare/test/apps/workers: devDependencies: - '@cloudflare/vite-plugin': - specifier: ^1.51.0 - version: 1.51.0(vite@8.1.5(@types/node@22.19.19)(esbuild@0.28.1)(jiti@2.4.2)(yaml@2.9.0))(wrangler@4.119.0(@cloudflare/workers-types@5.20260804.1)) '@sveltejs/kit': specifier: workspace:^ version: link:../../../../kit