Skip to content

feat: Cloudflare Vite plugin integration#15627

Draft
teemingc wants to merge 78 commits intofetchable-dev-environmentfrom
cloudflare-vite-plugin
Draft

feat: Cloudflare Vite plugin integration#15627
teemingc wants to merge 78 commits intofetchable-dev-environmentfrom
cloudflare-vite-plugin

Conversation

@teemingc
Copy link
Copy Markdown
Member

@teemingc teemingc commented Mar 31, 2026

closes #10496
closes #13692
closes #1712
closes #2963
closes #13300
closes #1519

wip

This PR also removes support for Cloudflare Pages.

  • export a fetch handler for adapter-cloudflare
  • analysis in workerd
  • prerender in workerd
  • think about what tests are needed

Please don't delete this checklist! Before submitting the PR, please make sure you do the following:

  • It's really useful if your PR references an issue where it is discussed ahead of time. In many cases, features are absent for a reason. For large changes, please create an RFC: https://github.com/sveltejs/rfcs
  • This message body should clearly illustrate what problems it solves.
  • Ideally, include a test that fails without this PR but passes with it.

Tests

  • Run the tests with pnpm test and lint the project with pnpm lint and pnpm check

Changesets

  • If your PR makes a change that should be noted in one or more packages' changelogs, generate a changeset by running pnpm changeset and following the prompts. Changesets that add features should be minor and those that fix bugs should be patch. Please prefix changeset messages with feat:, fix:, or chore:.

Edits

  • Please ensure that 'Allow edits from maintainers' is checked. PRs without this option may be closed.

@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Mar 31, 2026

⚠️ No Changeset found

Latest commit: 3fed050

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@svelte-docs-bot
Copy link
Copy Markdown

@raymonddaikon
Copy link
Copy Markdown

Got the same issue as @zhihengGet as well as @ivo22dev. Had to patch adapter cloudflare like so

handler(id, importer, options) {
 	if (!importer) return;
    const importer_path = path.normalize(importer.split('?')[0]);
 	if (importer_path !== path.normalize(default_worker)) return;
 	if (!building) { const source = `sveltekit:${id === 'SERVER' ? 'server' : 'server-manifest' }`;

and my vite config now looks like

// During `vite dev`, the custom worker imports `@sveltejs/adapter-cloudflare/worker`,
// which references the SvelteKit virtual modules `SERVER` and `MANIFEST`. If that
// worker module is pre-bundled into `node_modules/.vite/deps_ssr/`, the virtual
// imports inside it bypass SvelteKit's resolver and Vite errors with
// `Cannot find module 'SERVER'`. We exclude it from optimizeDeps and alias the
// virtual ids so SvelteKit's runtime resolver handles them.
const cloudflareWorkerSsrAliases: Plugin = {
  name: "cloudflare-worker-ssr-aliases",
  config(_config, env) {
    if (env.command !== "serve") return;
    return {
      environments: {
        ssr: {
          optimizeDeps: {
            exclude: ["@sveltejs/adapter-cloudflare/worker"],
            // `cookie` (used by SvelteKit runtime/server/cookie.js) is CJS-only.
            // Without forcing it through esbuild's CJS->ESM pre-bundle, Vite serves
            // the raw CJS file via /@fs/, the SSR runner has no `exports` global,
            // and evaluation throws `ReferenceError: exports is not defined`.
            include: ["cookie"],
          },
        },
      },
    };
  },
};

const removeSsrExternal: Plugin = {
  name: "remove-ssr-external",
  configResolved(config) {
    if (config.environments.ssr) config.environments.ssr.resolve.external = [];
  },
};

export default defineConfig({
  server: {
    fs: {
      allow: [searchForWorkspaceRoot(process.cwd())],
    },
  },
  plugins: [
    tailwindcss(),
    removeSsrExternal,
    cloudflareWorkerSsrAliases,
    sveltekit({
      adapter: adapter({ vitePluginOptions: { viteEnvironment: { name: "ssr" } }, worker: true }),
    }),
    paraglideVitePlugin({
      project: fileURLToPath(new URL("./project.inlang", import.meta.url)),
      outdir: fileURLToPath(new URL("./src/lib/paraglide", import.meta.url)),
    }),
  ],
});

@teemingc
Copy link
Copy Markdown
Member Author

teemingc commented May 1, 2026

In the latest version, I've changed it to use path.join so that the separators fit the OS's preference https://github.com/sveltejs/kit/pull/15627/changes#diff-78806028d48ad47894445421562b7796c53178948c017e13d4550def4193903eR10 However, I don't currently have access to a Windows machine to test this because I'm away from home. Can you confirm if this resolves the issue?

@zhihengGet
Copy link
Copy Markdown

zhihengGet commented May 1, 2026

i tried @raymonddaikon approach still cant run on dev, tried latest pr.new but getting 'SERVER' module cannot be found .vite/deps_ssr/@sveltejs_adapter-cloudflare_worke

[vite] program reload
7:17:10 PM [vite] (ssr) ✨ new dependencies optimized: svelte-sonner
7:17:10 PM [vite] (ssr) ✨ optimized dependencies changed. reloading
ReferenceError: $state is not defined
    at runInRunnerObject (workers/runner-worker/index.js:106:3)
    at get_hooks /.svelte-kit/generated/server/internal.js:50:71)
    at /node_modules/@sveltejs/kit/src/runtime/server/index.js?v=ce38e579:88:20 {
  remote: true
}

for build i got this issue on windows

[UNRESOLVED_IMPORT] Error: Could not resolve '/node_modules\@sveltejs\kit\src\runtime/shared-server.js' in \0virtual:env/dynamic/private
   ╭─[ \0virtual:env/dynamic/private:1:36 ]
   │
 1 │ export { private_env as env } from '/node_modules\@sveltejs\kit\src\runtime/shared-server.js';
   │                                    ─────────────────────────────┬────────────────────────────  
   │                                                                 ╰────────────────────────────── Module not found.
   │ 
   │ Help: '\0virtual:env/dynamic/private' is imported by the following path:
   │         - \0virtual:env/dynamic/private
   │         - src/hooks.server.ts
───╯

@teemingc
Copy link
Copy Markdown
Member Author

teemingc commented May 1, 2026

i tried @raymonddaikon approach still cant run on dev, tried latest pr.new but getting 'SERVER' module cannot be found .vite/deps_ssr/@sveltejs_adapter-cloudflare_worke

[vite] program reload
7:17:10 PM [vite] (ssr) ✨ new dependencies optimized: svelte-sonner
7:17:10 PM [vite] (ssr) ✨ optimized dependencies changed. reloading
ReferenceError: $state is not defined
    at runInRunnerObject (workers/runner-worker/index.js:106:3)
    at get_hooks (C:/Users/Hello World/Documents/git_proj/fantastic-dollop/apps/web/.svelte-kit/generated/server/internal.js:50:71)
    at C:/Users/Hello World/Documents/git_proj/fantastic-dollop/apps/web/node_modules/@sveltejs/kit/src/runtime/server/index.js?v=ce38e579:88:20 {
  remote: true
}

for build i got this issue on windows

[UNRESOLVED_IMPORT] Error: Could not resolve '/node_modules\@sveltejs\kit\src\runtime/shared-server.js' in \0virtual:env/dynamic/private
   ╭─[ \0virtual:env/dynamic/private:1:36 ]
   │
 1 │ export { private_env as env } from '/node_modules\@sveltejs\kit\src\runtime/shared-server.js';
   │                                    ─────────────────────────────┬────────────────────────────  
   │                                                                 ╰────────────────────────────── Module not found.
   │ 
   │ Help: '\0virtual:env/dynamic/private' is imported by the following path:
   │         - \0virtual:env/dynamic/private
   │         - src/hooks.server.ts
───╯

Thanks this is useful info. I’ll need to check the Kit code too to properly posixify or not where necessary. Meanwhile the server code optimisation will be fixed once we merge the vite-plugin-svelte PR. However, that’s also blocked on a Windows bug I’m failing to reproduce currently. I’m away from home until May 10th but I’ll have Windows access after that

@raymonddaikon
Copy link
Copy Markdown

i tried @raymonddaikon approach still cant run on dev, tried latest pr.new but getting 'SERVER' module cannot be found .vite/deps_ssr/@sveltejs_adapter-cloudflare_worke

[vite] program reload
7:17:10 PM [vite] (ssr) ✨ new dependencies optimized: svelte-sonner
7:17:10 PM [vite] (ssr) ✨ optimized dependencies changed. reloading
ReferenceError: $state is not defined
    at runInRunnerObject (workers/runner-worker/index.js:106:3)
    at get_hooks /.svelte-kit/generated/server/internal.js:50:71)
    at /node_modules/@sveltejs/kit/src/runtime/server/index.js?v=ce38e579:88:20 {
  remote: true
}

for build i got this issue on windows

[UNRESOLVED_IMPORT] Error: Could not resolve '/node_modules\@sveltejs\kit\src\runtime/shared-server.js' in \0virtual:env/dynamic/private
   ╭─[ \0virtual:env/dynamic/private:1:36 ]
   │
 1 │ export { private_env as env } from '/node_modules\@sveltejs\kit\src\runtime/shared-server.js';
   │                                    ─────────────────────────────┬────────────────────────────  
   │                                                                 ╰────────────────────────────── Module not found.
   │ 
   │ Help: '\0virtual:env/dynamic/private' is imported by the following path:
   │         - \0virtual:env/dynamic/private
   │         - src/hooks.server.ts
───╯

What node version are you running? Also you may need to add an override to your package.json since the adapter-cloudflare package doesn't resolve to the pr.new bulid of sveltekit.

  "overrides": {
    "@sveltejs/kit": "https://pkg.pr.new/@sveltejs/kit@d69e335",
  },

I managed to get the dev server running on the latest pr.pkg but still applying my patch and workaround vite plugins.

@zhihengGet
Copy link
Copy Markdown

zhihengGet commented May 2, 2026

btw @teemingc i think it might be a good idea to remove below, it threw me off because if i use the same URL as cache key, it will skip calling my api. i wanted to return cache under certain checks then return it. due to below, i need to use a different cache key

worker cache said caching happens after worker. user can start caching in their code and we can remove worktop/cfw.cache.

		let pragma = req.headers.get('cache-control') || '';
		let res = !pragma.includes('no-cache') && (await Cache.lookup(req));

@zhihengGet
Copy link
Copy Markdown

zhihengGet commented May 2, 2026

so i tried on wsl ubuntu with node 22, got the same error when doing build with and without extra patches ray mentioned

9:45:35 PM [vite] (ssr) [optimizer] bundling dependencies...
Using secrets defined in .env
▲ [WARNING] AI bindings always access remote resources, and so may incur usage charges even in local dev. To suppress this warning, set `remote: true` for the binding definition in your configuration file.



node:internal/event_target:1118
  process.nextTick(() => { throw err; });
                           ^
Error: Cannot find module 'SERVER' imported from '/mnt/c/Users/.../node_modules/.vite-analyse/deps_ssr/@sveltejs_adapter-cloudflare_worker.js'
    at runInRunnerObject (workers/runner-worker/index.js:106:3)
    at getWorkerEntryExportTypes (workers/runner-worker/index.js:245:24)
    at null.<anonymous> (workers/runner-worker/index.js:349:37)
    at maybeCaptureError (workers/runner-worker/index.js:50:10) {
  [cause]: undefined
}

Node.js v22.22.2

idk what is wrong, maybe i have circular deps or something weird which caued this, but it works without this new adapter on windows.

@teemingc
Copy link
Copy Markdown
Member Author

teemingc commented May 2, 2026

btw @teemingc i think it might be a good idea to remove below, it threw me off because if i use the same URL as cache key, it will skip calling my api. i wanted to return cache under certain checks then return it. due to below, i need to use a different cache key

worker cache said caching happens after worker. user can start caching in their code and we can remove worktop/cfw.cache.

		let pragma = req.headers.get('cache-control') || '';
		let res = !pragma.includes('no-cache') && (await Cache.lookup(req));

I'd recommend creating a new issue to discuss this as this behaviour has existed before this PR.

@Giggiux
Copy link
Copy Markdown

Giggiux commented May 2, 2026

This message was written with the help of Cursor, so I would like to apologize on my behalf for not catching obvious mistakes and or idiocy said. It's the first time I'm trying to use pkg.pr.new, and I'm not familiar with it, so I'm not so sure everything I've done makes sense. Plus, I'm definitely not expert in implementation of the adapter system of SvelteKit, but I really enjoy deploying my apps on Cloudflare, and these fixes would help A LOT all my projects as I (think I) extensively use the Vite Environment API via wrangler/vite dev to bind multiple local cloudflare services via Cloudflare Env (or in this case SvelteKit's platform.env).

That said, I think I've found the same bug as above, but on macOS, and I do not understand if I'm making obvious mistakes in using new tech I'm not too aware of, or if there are bugs.

Start of the AI aided message

I am testing the Cloudflare Vite plugin integration branch: @sveltejs/kit and @sveltejs/adapter-cloudflare both come from the same PR via pkg.pr.new.

Relevant package.json excerpts

{
  "pnpm": {
    "overrides": {
      "@sveltejs/kit": "https://pkg.pr.new/@sveltejs/kit@15627"
    }
  },
  "devDependencies": {
    "@cloudflare/vite-plugin": "^1.35.0",
    "@sveltejs/adapter-cloudflare": "https://pkg.pr.new/@sveltejs/adapter-cloudflare@15627",
    "@sveltejs/kit": "^2.59.0",
    "@sveltejs/vite-plugin-svelte": "^7.0.0",
    "svelte": "^5.55.2",
    "vite": "^8.0.7",
    "vitest": "^4.0.14",
    "wrangler": "^4.87.0"
  }
}

I also tested installing directly the latest pkg.pr.new of SvelteKit instead of using the overrides, and the result doesn't change.

How I use SvelteKit: worker: true on the adapter, @cloudflare/vite-plugin in the stack (via the adapter), sveltekit() from @sveltejs/kit/vite with defineConfig from vitest/config (Vitest merged into the same Vite config).


Error I see (vite build)

During build, resolution fails when the module runner loads a pre-bundled adapter worker chunk under SvelteKit’s analyse cache (not the main .vite cache):

node:internal/event_target:1122
  process.nextTick(() => { throw err; });
                           ^
Error: Cannot find module 'SERVER' imported from '…/node_modules/.vite-analyse/deps_ssr/@sveltejs_adapter-cloudflare_worker.js'
    at runInRunnerObject (workers/runner-worker/index.js:106:3)
    at getWorkerEntryExportTypes (workers/runner-worker/index.js:245:24)
    at null.<anonymous> (workers/runner-worker/index.js:349:37)
    at maybeCaptureError (workers/runner-worker/index.js:50:10) {
  [cause]: undefined
}

The on-disk prebundle still contains bare imports such as from "SERVER" / from "MANIFEST", which are virtual and must be resolved by the adapter — but that breaks when the importer is no longer the real adapter-cloudflare/src/worker.js path (e.g. after SSR optimizeDeps writes into .vite-analyse/deps_ssr/…).


What I added for the main Vite app (dev / primary SSR graph)

In root vite.config.ts I created a small pre plugins to:

  1. Exclude @sveltejs/adapter-cloudflare/worker from environments.ssr.optimizeDeps so SERVER / MANIFEST are not pulled through a prebundle that drops adapter resolution.
  2. Resolve SERVER / MANIFEST to sveltekit:server / sveltekit:server-manifest when the importer is the adapter’s src/worker.js (mitigates strict importer === default_worker / realpath quirks under pnpm).
  3. Clear ssr.resolve.external for the Cloudflare path (separate issue we hit with worker externals).

Illustrative structure (abbreviated; full file is longer):

// plugins run before sveltekit()
clearCloudflareWorkerExternals(),
excludeAdapterCloudflareWorkerFromSsrOptimizeDeps(), // environments.ssr.optimizeDeps.exclude includes '@sveltejs/adapter-cloudflare/worker'
resolveSvelteKitWorkerVirtualModules(),              // resolveId for SERVER / MANIFEST when importer ends with …/src/worker.js
sveltekit({
  adapter: adapter({
    vitePluginOptions: { configPath: './wrangler.jsonc' },
    worker: true
  })
})

Important limitation: SvelteKit’s route analyse step spins up an internal Vite server with cacheDir: node_modules/.vite-analyse and configFile: false, so my root vite.config.ts plugins and environments.ssr.optimizeDeps excludes do not apply there. That is why I can still get the SERVER error on vite build even if vite dev looks fine.


Configuration assumptions — am I wrong?

I do not think I am misusing the public API: same PR for Kit + adapter, worker: true, Wrangler config path passed as documented for the integration.

Possible pitfalls on my side (worth confirming):

  • defineConfig from vitest/config: merges test config into Vite; unlikely to be the root cause of SERVER in .vite-analyse, but worth noting for repro minimalism.
  • Custom resolveId for SERVER / MANIFEST: could mask adapter behavior in the main graph; still should not affect the isolated analyse server.
  • ssr.resolve.external = []: broad; I added it for Cloudflare worker resolution — might be unnecessary or risky long-term depending on upstream.

I assume no user vite.config is loaded for the analyse mini-server by design — if that assumption is wrong or there is an official extension point, I'd like to know.


Using a simplified vite.config (isolate Vitest / custom plugins)

To check whether defineConfig from vitest/config or my custom plugins were involved in the original failure, I also copied the a minimal Vite config from this PR (custom-worker test app: defineConfig from vite only, tailwindcss() + sveltekit({ adapter: adapter({ vitePluginOptions: { configPath: './wrangler.jsonc' }, worker: true }) })), with the previous full config commented out in the same file.

With that setup, pnpm dev / vite dev does not reach the earlier SERVER / .vite-analyse problem; it fails earlier with @cloudflare/vite-plugin rejecting non-empty SSR externals:

Error: The following environment options are incompatible with the Cloudflare Vite plugin:
  - "ssr" environment: `resolve.external`: ["@dagrejs/dagre","@floating-ui/core", ... "tailwind-merge","ts-deepmerge", ...]
To resolve this issue, avoid setting `resolve.external` in your Cloudflare Worker environments.

    at validateWorkerEnvironmentOptions (.../@cloudflare/vite-plugin/dist/index.mjs:...)

So for my app, SvelteKit / Vite still populate environments.ssr.resolve.external with many dependencies unless I clear them (I previously used a small configResolved plugin that sets config.environments.ssr.resolve.external = []).

Takeaway: (1) The Vitest-based defineConfig merge does not appear necessary to reproduce the Cloudflare resolve.external validation failure. (2) Clearing SSR resolve.external for the Cloudflare worker environment is a separate concern from the SERVER / optimizeDeps / analyse-server issue discussed above; both may need upstream or documented handling for worker: true + @cloudflare/vite-plugin.

How this could be fixed upstream

(pure guesses by Cursor's auto model - very much apologize if all this is wrong)

  1. Adapter (@sveltejs/adapter-cloudflare): Relax resolveId for SERVER / MANIFEST so it still runs when the importer is a Vite optimizeDeps output (e.g. path under …/.vite-analyse/deps_ssr/… or …/.vite/deps_ssr/…) that originated from the worker entry — or stop requiring importer === default_worker exactly.

  2. Adapter or Kit: Ensure @sveltejs/adapter-cloudflare/worker is in optimizeDeps.exclude for every environment that can load that graph during build/analyse, not only the user’s main ssr environment.

  3. Kit: Optionally merge critical optimizeDeps / resolution behavior into create_build_server for the analyse name, or document a supported hook — today configFile: false means user workarounds cannot reach .vite-analyse.


Extra evidence from the generated output

One detail that might help narrow this down: the main production Worker output appears to be rewritten correctly, but the analysis prebundle does not.

After vite build gets through the main SSR build, .svelte-kit/output/server/index.js contains the expected relative imports:

import { Server } from "./server.js";
import { manifest } from "./manifest.js";

But the analysis cache still contains:

import { Server } from "SERVER";
import { manifest } from "MANIFEST";

in:

node_modules/.vite-analyse/deps_ssr/@sveltejs_adapter-cloudflare_worker.js

So from my side it looks like the final production bundle has already passed through the adapter’s build-time rewrite path, while the Cloudflare/Vite analysis runner is evaluating a separately optimized copy of @sveltejs/adapter-cloudflare/worker where those virtual imports were preserved.

My custom Worker entry is intentionally simple:

import { fetch as svelteKitFetch } from '@sveltejs/adapter-cloudflare/worker';

export default {
  fetch: svelteKitFetch,
  async queue(batch, env) {
    // queue handling
  }
};

export class IndexCoordinator extends DurableObject {
  // durable object RPC methods
}

So the repro condition may specifically be: custom wrangler.main imports @sveltejs/adapter-cloudflare/worker, and the build analysis optimizer prebundles that import before the adapter can rewrite SERVER / MANIFEST.

I am not trying to suggest my local Vite plugins are correct fixes. They were just attempts to keep dev moving. The important bit is that the failure still happens in the isolated .vite-analyse graph, which my root Vite config does not seem able to influence.

I'm happy to trim a repro to a minimal repo if that helps land a fix on the PR.

End of the AI aided message

Anything I can do to test further this into my experimental repo with this, I am more than available to do it.

Hopefully this is something useful and not a total waste of time.

@Giggiux
Copy link
Copy Markdown

Giggiux commented May 2, 2026

In response to my previous message, by cloning this PR branch, a fix was the following (again, not entirely sure is the correct long term fix, but it fixes the symptoms):

Adapter: packages/adapter-cloudflare/index.js

What: Added is_sveltekit_worker_virtual_importer(importer) and used it in the existing resolveId handler for SERVER / MANIFEST instead of importer !== default_worker.

  • Helper + docstring: lines 23–46
/**
 * `SERVER` / `MANIFEST` are virtual imports resolved only from the adapter's
 * `src/worker.js`. Vite's dependency optimizer can pre-bundle
 * `@sveltejs/adapter-cloudflare/worker` into `.vite/deps_ssr` or
 * `.vite-analyse/deps_ssr`, which changes the importer path and must still
 * be accepted (see sveltejs/kit#15627).
 *
 * @param {string | undefined} importer
 * @returns {boolean}
 */
function is_sveltekit_worker_virtual_importer(importer) {
	if (!importer) return false;
	const clean = importer.split('?')[0];
	if (path.normalize(clean) === path.normalize(default_worker)) return true;
	const posix = clean.replace(/\\/g, '/');
	if (
		(posix.includes('/.vite-analyse/deps_ssr/') || posix.includes('/.vite/deps_ssr/')) &&
		posix.includes('adapter-cloudflare') &&
		posix.includes('worker')
	) {
		return true;
	}
	return false;
}

Where (in our tree, same paths as this repo):

  • Handler guard: lines 191–192 (was a strict equality check on default_worker before)
- if (importer !== default_worker) return;
+ if (!is_sveltekit_worker_virtual_importer(importer)) return;

Equivalent upstream path:
packages/adapter-cloudflare/index.js

Why:

  • path.normalize on both sides — same intent as @ivo22dev’s Windows note: Vite can pass a path shape that does not match default_worker string-for-string.
  • .vite-analyse/deps_ssr and .vite/deps_ssr@cloudflare/vite-plugin runs dependency optimization for the worker graph; @sveltejs/adapter-cloudflare/worker can end up in a file like node_modules/.vite-analyse/deps_ssr/@sveltejs_adapter-cloudflare_worker.js. The importer is then not …/adapter-cloudflare/src/worker.js, so the strict guard never runs and literal "SERVER" / "MANIFEST" leak into the bundle that workerd tries to load.

Internal changes in the app after this fix

After the adapter fix, the analyse / getWorkerEntryExportTypes path actually executes src/worker.js. That environment does not resolve SvelteKit’s $lib alias, so the next failure was Cannot find module '$lib/server/db' (from my project). Relative imports match how Node/Vite resolve that graph without SvelteKit’s alias config.

So I had to replace the $lib/server/db with relative calls instead, in my custom worker entry.


With these two changes, vite build completes and @sveltejs/adapter-cloudflare’s adapt step finishes.

@teemingc
Copy link
Copy Markdown
Member Author

teemingc commented May 3, 2026

Thanks @Giggiux let me try to add a test for this and also tighten up that importer check

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment