diff --git a/.bumpy/sveltekit-cf-autodetect.md b/.bumpy/sveltekit-cf-autodetect.md
new file mode 100644
index 000000000..9fe28e042
--- /dev/null
+++ b/.bumpy/sveltekit-cf-autodetect.md
@@ -0,0 +1,6 @@
+---
+"@varlock/vite-integration": minor
+"@varlock/cloudflare-integration": patch
+---
+
+SvelteKit on Cloudflare now works with the standard varlockVitePlugin() — it auto-detects the @sveltejs/adapter-cloudflare adapter (configured in svelte.config.js or inline in vite.config) and injects the Workers env loader automatically. The same import now works across all deploy targets. varlockSvelteKitCloudflarePlugin is deprecated; install @varlock/cloudflare-integration alongside the vite plugin for Cloudflare deploys.
diff --git a/framework-tests/bun.lock b/framework-tests/bun.lock
index 972ec5029..9d3d548de 100644
--- a/framework-tests/bun.lock
+++ b/framework-tests/bun.lock
@@ -6,6 +6,7 @@
"name": "varlock-framework-tests",
"devDependencies": {
"@types/node": "^24.0.0",
+ "@types/react": "19.2.15",
"typescript": "^5.9.3",
"vitest": "^3.2.4",
},
@@ -124,6 +125,8 @@
"@types/node": ["@types/node@24.12.2", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-A1sre26ke7HDIuY/M23nd9gfB+nrmhtYyMINbjI1zHJxYteKR6qSMX56FsmjMcDb3SMcjJg5BiRRgOCC/yBD0g=="],
+ "@types/react": ["@types/react@19.2.15", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-eRwcGNHve+E8qtEQSSRl6urh+rFop4v8gm6O8rGv25CodbvFdLjA1vVQ1KkiFE0w0UPOnb8tDiFKL5lp0rtY5Q=="],
+
"@vitest/expect": ["@vitest/expect@3.2.4", "", { "dependencies": { "@types/chai": "^5.2.2", "@vitest/spy": "3.2.4", "@vitest/utils": "3.2.4", "chai": "^5.2.0", "tinyrainbow": "^2.0.0" } }, "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig=="],
"@vitest/mocker": ["@vitest/mocker@3.2.4", "", { "dependencies": { "@vitest/spy": "3.2.4", "estree-walker": "^3.0.3", "magic-string": "^0.30.17" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" }, "optionalPeers": ["msw", "vite"] }, "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ=="],
@@ -146,6 +149,8 @@
"check-error": ["check-error@2.1.3", "", {}, "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA=="],
+ "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="],
+
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
"deep-eql": ["deep-eql@5.0.2", "", {}, "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q=="],
diff --git a/framework-tests/frameworks/sveltekit/files/_base/package.json b/framework-tests/frameworks/sveltekit/files/_base/package.json
new file mode 100644
index 000000000..27b2f583b
--- /dev/null
+++ b/framework-tests/frameworks/sveltekit/files/_base/package.json
@@ -0,0 +1,10 @@
+{
+ "name": "sveltekit-cloudflare-framework-test",
+ "version": "0.0.0",
+ "private": true,
+ "type": "module",
+ "scripts": {
+ "build": "vite build",
+ "dev": "vite dev"
+ }
+}
diff --git a/framework-tests/frameworks/sveltekit/files/_base/src/app.html b/framework-tests/frameworks/sveltekit/files/_base/src/app.html
new file mode 100644
index 000000000..adf8bd873
--- /dev/null
+++ b/framework-tests/frameworks/sveltekit/files/_base/src/app.html
@@ -0,0 +1,11 @@
+
+
+
+
+
+ %sveltekit.head%
+
+
+ %sveltekit.body%
+
+
diff --git a/framework-tests/frameworks/sveltekit/files/_base/svelte.config.js b/framework-tests/frameworks/sveltekit/files/_base/svelte.config.js
new file mode 100644
index 000000000..9e49a2d64
--- /dev/null
+++ b/framework-tests/frameworks/sveltekit/files/_base/svelte.config.js
@@ -0,0 +1,12 @@
+import adapter from '@sveltejs/adapter-node';
+import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
+
+/** @type {import('@sveltejs/kit').Config} */
+const config = {
+ preprocess: vitePreprocess(),
+ kit: {
+ adapter: adapter(),
+ },
+};
+
+export default config;
diff --git a/framework-tests/frameworks/sveltekit/files/_base/tsconfig.json b/framework-tests/frameworks/sveltekit/files/_base/tsconfig.json
new file mode 100644
index 000000000..43447105a
--- /dev/null
+++ b/framework-tests/frameworks/sveltekit/files/_base/tsconfig.json
@@ -0,0 +1,14 @@
+{
+ "extends": "./.svelte-kit/tsconfig.json",
+ "compilerOptions": {
+ "allowJs": true,
+ "checkJs": true,
+ "esModuleInterop": true,
+ "forceConsistentCasingInFileNames": true,
+ "resolveJsonModule": true,
+ "skipLibCheck": true,
+ "sourceMap": true,
+ "strict": true,
+ "moduleResolution": "bundler"
+ }
+}
diff --git a/framework-tests/frameworks/sveltekit/files/_base/vite.config.ts b/framework-tests/frameworks/sveltekit/files/_base/vite.config.ts
new file mode 100644
index 000000000..8fc5f27ba
--- /dev/null
+++ b/framework-tests/frameworks/sveltekit/files/_base/vite.config.ts
@@ -0,0 +1,13 @@
+import { sveltekit } from '@sveltejs/kit/vite';
+import { varlockVitePlugin } from '@varlock/vite-integration';
+import { defineConfig } from 'vite';
+
+// Note: the SAME plugin works for every deploy target. With the Cloudflare
+// adapter configured in svelte.config.js, varlockVitePlugin() auto-detects it
+// and injects the Workers runtime env-loader — no separate import needed.
+export default defineConfig({
+ plugins: [
+ varlockVitePlugin(),
+ sveltekit(),
+ ],
+});
diff --git a/framework-tests/frameworks/sveltekit/files/configs/svelte.config.cloudflare.js b/framework-tests/frameworks/sveltekit/files/configs/svelte.config.cloudflare.js
new file mode 100644
index 000000000..6aa8a2c27
--- /dev/null
+++ b/framework-tests/frameworks/sveltekit/files/configs/svelte.config.cloudflare.js
@@ -0,0 +1,12 @@
+import adapter from '@sveltejs/adapter-cloudflare';
+import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
+
+/** @type {import('@sveltejs/kit').Config} */
+const config = {
+ preprocess: vitePreprocess(),
+ kit: {
+ adapter: adapter(),
+ },
+};
+
+export default config;
diff --git a/framework-tests/frameworks/sveltekit/files/configs/wrangler.jsonc b/framework-tests/frameworks/sveltekit/files/configs/wrangler.jsonc
new file mode 100644
index 000000000..e522d4ffc
--- /dev/null
+++ b/framework-tests/frameworks/sveltekit/files/configs/wrangler.jsonc
@@ -0,0 +1,10 @@
+{
+ "name": "sveltekit-cloudflare-framework-test",
+ "main": ".svelte-kit/cloudflare/_worker.js",
+ "compatibility_flags": ["nodejs_compat"],
+ "compatibility_date": "2026-03-17",
+ "assets": {
+ "binding": "ASSETS",
+ "directory": ".svelte-kit/cloudflare"
+ }
+}
diff --git a/framework-tests/frameworks/sveltekit/files/pages/basic-page.svelte b/framework-tests/frameworks/sveltekit/files/pages/basic-page.svelte
new file mode 100644
index 000000000..8ed37656b
--- /dev/null
+++ b/framework-tests/frameworks/sveltekit/files/pages/basic-page.svelte
@@ -0,0 +1,10 @@
+
+
+SvelteKit + Cloudflare Varlock Test
+{publicVar}
+{apiUrl}
diff --git a/framework-tests/frameworks/sveltekit/files/pages/prerender.ts b/framework-tests/frameworks/sveltekit/files/pages/prerender.ts
new file mode 100644
index 000000000..f1454c12c
--- /dev/null
+++ b/framework-tests/frameworks/sveltekit/files/pages/prerender.ts
@@ -0,0 +1,4 @@
+// Make the route fully static — exercises the Cloudflare adapter + varlock's
+// injected edge loader in a prerender-only ("totally static") build, where the
+// loader must be a no-op in Node at build time.
+export const prerender = true;
diff --git a/framework-tests/frameworks/sveltekit/files/routes/env-endpoint.ts b/framework-tests/frameworks/sveltekit/files/routes/env-endpoint.ts
new file mode 100644
index 000000000..dff98b755
--- /dev/null
+++ b/framework-tests/frameworks/sveltekit/files/routes/env-endpoint.ts
@@ -0,0 +1,15 @@
+import { ENV } from 'varlock/env';
+
+// Server endpoint that reads env at request time. In the built Cloudflare
+// worker, ENV is hydrated by the injected runtime loader from the
+// `__VARLOCK_ENV` binding.
+export const GET = async () => {
+ return new Response(
+ JSON.stringify({
+ PUBLIC_VAR: ENV.PUBLIC_VAR,
+ API_URL: ENV.API_URL,
+ HAS_SECRET: ENV.SECRET_KEY ? 'yes' : 'no',
+ }),
+ { headers: { 'content-type': 'application/json; charset=utf-8' } },
+ );
+};
diff --git a/framework-tests/frameworks/sveltekit/files/schemas/.env.dev b/framework-tests/frameworks/sveltekit/files/schemas/.env.dev
new file mode 100644
index 000000000..cdb0a7f31
--- /dev/null
+++ b/framework-tests/frameworks/sveltekit/files/schemas/.env.dev
@@ -0,0 +1 @@
+APP_ENV=dev
diff --git a/framework-tests/frameworks/sveltekit/files/schemas/.env.prod b/framework-tests/frameworks/sveltekit/files/schemas/.env.prod
new file mode 100644
index 000000000..74b8b8318
--- /dev/null
+++ b/framework-tests/frameworks/sveltekit/files/schemas/.env.prod
@@ -0,0 +1 @@
+APP_ENV=prod
diff --git a/framework-tests/frameworks/sveltekit/files/schemas/.env.schema b/framework-tests/frameworks/sveltekit/files/schemas/.env.schema
new file mode 100644
index 000000000..f5a1f8b29
--- /dev/null
+++ b/framework-tests/frameworks/sveltekit/files/schemas/.env.schema
@@ -0,0 +1,12 @@
+# @defaultSensitive=false @defaultRequired=infer
+# @currentEnv=$APP_ENV
+# ---
+
+# @type=enum(dev, prod)
+APP_ENV=dev
+
+PUBLIC_VAR=public-test-value
+API_URL=https://api.example.com
+
+# @sensitive
+SECRET_KEY=super-secret-value
diff --git a/framework-tests/frameworks/sveltekit/sveltekit.test.ts b/framework-tests/frameworks/sveltekit/sveltekit.test.ts
new file mode 100644
index 000000000..102dff136
--- /dev/null
+++ b/framework-tests/frameworks/sveltekit/sveltekit.test.ts
@@ -0,0 +1,142 @@
+/*
+SvelteKit framework tests.
+
+SvelteKit is built on Vite, so it uses `@varlock/vite-integration` directly.
+These cover the common Node deploy path (build + dev) and verify that the same
+`varlockVitePlugin()` auto-detects `@sveltejs/adapter-cloudflare` and injects
+the Workers runtime env-loader — so no Cloudflare-specific import is needed.
+*/
+import {
+ describe, beforeAll, afterAll,
+} from 'vitest';
+import { FrameworkTestEnv } from '../../harness/index';
+
+describe('SvelteKit', () => {
+ const env = new FrameworkTestEnv({
+ testDir: import.meta.dirname,
+ framework: 'sveltekit',
+ packageManager: 'pnpm',
+ dependencies: {
+ '@sveltejs/adapter-node': '^5',
+ '@sveltejs/adapter-cloudflare': '^7',
+ '@sveltejs/kit': '^2',
+ '@sveltejs/vite-plugin-svelte': '^6',
+ svelte: '^5',
+ vite: '^7',
+ wrangler: '^4',
+ varlock: 'will-be-replaced',
+ '@varlock/vite-integration': 'will-be-replaced',
+ // CF deployers always have this (it ships `varlock-wrangler`); the vite
+ // plugin pulls the edge loader from it once it detects the CF adapter.
+ '@varlock/cloudflare-integration': 'will-be-replaced',
+ },
+ packageJsonMerge: {
+ packageManager: 'pnpm@10.17.0',
+ },
+ templateFiles: {
+ '.env.schema': 'schemas/.env.schema',
+ '.env.dev': 'schemas/.env.dev',
+ '.env.prod': 'schemas/.env.prod',
+ 'src/routes/+page.svelte': 'pages/basic-page.svelte',
+ },
+ });
+
+ beforeAll(() => env.setup(), 180_000);
+ afterAll(() => env.teardown());
+
+ env.describeScenario('build: non-sensitive inlined, sensitive not leaked', {
+ command: 'vite build',
+ expectSuccess: true,
+ timeout: 180_000,
+ fileAssertions: [
+ {
+ description: 'client bundle inlines the non-sensitive value',
+ fileGlob: '.svelte-kit/output/client/**/*.js',
+ shouldContain: ['public-test-value'],
+ },
+ {
+ description: 'sensitive value is absent from all output',
+ fileGlob: '.svelte-kit/output/**/*.js',
+ shouldNotContain: ['super-secret-value'],
+ },
+ ],
+ });
+
+ env.describeDevScenario('dev: ENV available at runtime, secret redacted', {
+ command: 'vite dev --port 14730',
+ readyPattern: /localhost:14730/,
+ readyTimeout: 30_000,
+ templateFiles: {
+ 'src/routes/api/env/+server.ts': 'routes/env-endpoint.ts',
+ },
+ requests: [
+ {
+ path: '/api/env',
+ bodyAssertions: {
+ shouldContain: ['"PUBLIC_VAR":"public-test-value"', '"HAS_SECRET":"yes"'],
+ shouldNotContain: ['super-secret-value'],
+ },
+ },
+ ],
+ });
+
+ env.describeScenario('cloudflare adapter is auto-detected and the edge loader injected', {
+ command: 'vite build',
+ expectSuccess: true,
+ timeout: 180_000,
+ templateFiles: {
+ // swap in the Cloudflare adapter + wrangler config — varlockVitePlugin()
+ // in vite.config.ts is unchanged from the Node build above.
+ 'svelte.config.js': 'configs/svelte.config.cloudflare.js',
+ 'wrangler.jsonc': 'configs/wrangler.jsonc',
+ 'src/routes/api/env/+server.ts': 'routes/env-endpoint.ts',
+ },
+ outputAssertions: [
+ {
+ description: 'logs the auto-detection notice',
+ shouldContain: ['detected @sveltejs/adapter-cloudflare'],
+ },
+ ],
+ fileAssertions: [
+ {
+ description: 'SSR bundle contains the injected Cloudflare runtime env-loader',
+ fileGlob: '.svelte-kit/output/server/**/*.js',
+ // markers from CLOUDFLARE_SSR_ENTRY_CODE — present only if detection fired
+ shouldContain: ['Cloudflare-Workers', '__VARLOCK_ENV', 'cloudflare:workers'],
+ },
+ {
+ description: 'sensitive value is not inlined into any built output',
+ fileGlob: '.svelte-kit/**/*.js',
+ shouldNotContain: ['super-secret-value'],
+ },
+ ],
+ });
+
+ // Regression guard: a fully prerendered ("totally static") app on the
+ // Cloudflare adapter must still build. The injected loader is guarded by a
+ // `navigator.userAgent === 'Cloudflare-Workers'` check, so it must be inert
+ // when SvelteKit evaluates the SSR entry in Node during prerendering.
+ env.describeScenario('cloudflare adapter + fully prerendered build does not break', {
+ command: 'vite build',
+ expectSuccess: true,
+ timeout: 180_000,
+ templateFiles: {
+ 'svelte.config.js': 'configs/svelte.config.cloudflare.js',
+ 'wrangler.jsonc': 'configs/wrangler.jsonc',
+ // no dynamic routes — `prerender = true` makes the whole app static
+ 'src/routes/+page.ts': 'pages/prerender.ts',
+ },
+ fileAssertions: [
+ {
+ description: 'prerendered HTML inlines the non-sensitive value',
+ fileGlob: '.svelte-kit/output/prerendered/**/*.html',
+ shouldContain: ['public-test-value'],
+ },
+ {
+ description: 'sensitive value is not present in any output',
+ fileGlob: '.svelte-kit/**/*.{js,html}',
+ shouldNotContain: ['super-secret-value'],
+ },
+ ],
+ });
+});
diff --git a/framework-tests/package.json b/framework-tests/package.json
index 93dcd694c..3b23df44c 100644
--- a/framework-tests/package.json
+++ b/framework-tests/package.json
@@ -13,6 +13,7 @@
"test:cloudflare": "vitest run frameworks/cloudflare",
"test:expo": "vitest run frameworks/expo",
"test:nextjs": "vitest run frameworks/nextjs",
+ "test:sveltekit": "vitest run frameworks/sveltekit",
"test:vanilla-node": "vitest run frameworks/vanilla-node",
"test:tanstack-start": "vitest run frameworks/tanstack-start",
"test:vite": "vitest run frameworks/vite"
diff --git a/packages/integrations/cloudflare/src/index.ts b/packages/integrations/cloudflare/src/index.ts
index ca7d7ed7b..f4f4c7d88 100644
--- a/packages/integrations/cloudflare/src/index.ts
+++ b/packages/integrations/cloudflare/src/index.ts
@@ -165,11 +165,10 @@ function serveFifoOrFile(filePath: string, content: string) {
* Varlock Cloudflare Vite plugin — wraps `@cloudflare/vite-plugin` with
* automatic env var injection.
*
- * For SvelteKit projects deploying via `@sveltejs/adapter-cloudflare`, use
- * `varlockSvelteKitCloudflarePlugin` from
- * `@varlock/cloudflare-integration/sveltekit` instead — it skips
- * `@cloudflare/vite-plugin` (which doesn't support SvelteKit) and uses an
- * SSR-entry-based env loader.
+ * For SvelteKit projects deploying via `@sveltejs/adapter-cloudflare`, use the
+ * standard `varlockVitePlugin` from `@varlock/vite-integration` instead — it
+ * auto-detects the Cloudflare adapter and uses an SSR-entry-based env loader,
+ * skipping `@cloudflare/vite-plugin` (which doesn't support SvelteKit).
*
* **Important:** Do not use a `.dev.vars` file alongside this plugin — varlock
* handles env injection automatically. The plugin will throw an error if a
diff --git a/packages/integrations/cloudflare/src/sveltekit.ts b/packages/integrations/cloudflare/src/sveltekit.ts
index 015997377..813a08196 100644
--- a/packages/integrations/cloudflare/src/sveltekit.ts
+++ b/packages/integrations/cloudflare/src/sveltekit.ts
@@ -4,24 +4,20 @@ import { CLOUDFLARE_SSR_ENTRY_CODE } from './shared-ssr-entry-code';
/**
* Varlock SvelteKit + Cloudflare Vite plugin.
*
- * For SvelteKit projects deploying to Cloudflare Workers via
- * `@sveltejs/adapter-cloudflare`. Unlike `varlockCloudflareVitePlugin`, this
- * does NOT include `@cloudflare/vite-plugin` (which doesn't currently support
- * SvelteKit — see https://github.com/cloudflare/workers-sdk/issues/8922).
- *
- * Injects the `cloudflare:workers` runtime env-loader into SvelteKit's SSR
- * entry, which is picked up by adapter-cloudflare's generated `_worker.js`.
- * Non-sensitive vars and the `__VARLOCK_ENV` secret should still be uploaded
- * via `varlock-wrangler deploy`.
+ * @deprecated Use `varlockVitePlugin()` from `@varlock/vite-integration`
+ * instead — it now auto-detects SvelteKit projects using
+ * `@sveltejs/adapter-cloudflare` and wires this up automatically, so the same
+ * import works whether you deploy to Node or Cloudflare. This alias remains for
+ * back-compat and simply forces the Cloudflare edge loader explicitly.
*
* @example
* ```ts
* import { sveltekit } from '@sveltejs/kit/vite';
- * import { varlockSvelteKitCloudflarePlugin } from '@varlock/cloudflare-integration/sveltekit';
+ * import { varlockVitePlugin } from '@varlock/vite-integration';
*
* export default defineConfig({
* plugins: [
- * varlockSvelteKitCloudflarePlugin(),
+ * varlockVitePlugin(),
* sveltekit(),
* ],
* });
@@ -30,25 +26,9 @@ import { CLOUDFLARE_SSR_ENTRY_CODE } from './shared-ssr-entry-code';
// Return type is `Array` to avoid symlink-induced Vite Plugin type
// conflicts (see note in ./index.ts).
export function varlockSvelteKitCloudflarePlugin(): Array {
- // Mark `cloudflare:workers` as external so Rollup keeps the runtime import
- // our `ssrEntryCode` injects into the SSR bundle. Normally
- // `@cloudflare/vite-plugin` handles this, but we're not using it here.
- const externalizeCloudflareWorkers: import('vite').Plugin = {
- name: 'varlock-sveltekit-cloudflare-external',
- enforce: 'pre',
- config() {
- return {
- build: {
- rollupOptions: {
- external: ['cloudflare:workers'],
- },
- },
- };
- },
- };
-
+ // `varlockVitePlugin` already marks `cloudflare:workers` external; here we
+ // just force the edge runtime + loader explicitly (bypassing auto-detection).
return [
- externalizeCloudflareWorkers,
varlockVitePlugin({
ssrEdgeRuntime: true,
ssrEntryCode: [CLOUDFLARE_SSR_ENTRY_CODE],
diff --git a/packages/integrations/vite/src/index.ts b/packages/integrations/vite/src/index.ts
index eb19d069e..302aaf3e1 100644
--- a/packages/integrations/vite/src/index.ts
+++ b/packages/integrations/vite/src/index.ts
@@ -51,6 +51,8 @@ export let varlockLoadedEnv: SerializedEnvGraph;
export let varlockLastError: string | undefined;
let lastErrorAt = 0;
let configHookCalled = false;
+// one-time guard for the SvelteKit+Cloudflare auto-detection notice
+let cfDetectNoticeLogged = false;
let staticReplacements: Record = {};
let replacerFn: ReturnType;
@@ -171,12 +173,19 @@ export function varlockVitePlugin(
}
}
+ // Resolved at build time. These start from the passed options but may be
+ // overridden by SvelteKit+Cloudflare auto-detection in `configResolved`
+ // (see below). They're read lazily by `buildInitModuleCode()`, which runs
+ // when the virtual init module is loaded — always after `configResolved`.
+ let resolvedSsrEdgeRuntime = vitePluginOptions?.ssrEdgeRuntime ?? false;
+ let resolvedSsrEntryCode = vitePluginOptions?.ssrEntryCode;
+
// Build the virtual init module content once. This module is imported
// by SSR entry points and evaluates before any user code because it
// has no transitive dependencies on user modules.
function buildInitModuleCode() {
const ssrInjectMode = vitePluginOptions?.ssrInjectMode ?? 'init-only';
- const isEdgeRuntime = vitePluginOptions?.ssrEdgeRuntime ?? false;
+ const isEdgeRuntime = resolvedSsrEdgeRuntime;
const lines: Array = [
'// Virtual module generated by @varlock/vite-integration',
'// Runs before any user code to ensure ENV is available at module top-level',
@@ -213,8 +222,8 @@ export function varlockVitePlugin(
}
// inject custom entry code from integrations (e.g., CF bindings loader)
- if (vitePluginOptions?.ssrEntryCode?.length) {
- lines.push(...vitePluginOptions.ssrEntryCode);
+ if (resolvedSsrEntryCode?.length) {
+ lines.push(...resolvedSsrEntryCode);
}
// decrypt the encrypted env blob before initVarlockEnv runs
@@ -250,6 +259,57 @@ export function varlockVitePlugin(
return lines.join('\n');
}
+ // Auto-detect SvelteKit deploying to Cloudflare Workers and wire up the edge
+ // env-loader so users don't need a separate import. SvelteKit resolves its
+ // config (from `svelte.config.js` OR inline `sveltekit({ adapter })`) and
+ // exposes it on the `vite-plugin-sveltekit-setup` plugin's `api.options`, so
+ // this one check covers both config layouts. The Cloudflare-specific injected
+ // code lives in `@varlock/cloudflare-integration` and is pulled in lazily via
+ // a runtime dynamic import. CF deployers already have it installed (it ships
+ // `varlock-wrangler`); it is intentionally NOT declared as a (peer)dependency
+ // here because cloudflare-integration depends on vite-integration, and the
+ // back-edge would create a build/typecheck cycle. We skip this when the
+ // consumer already supplied
+ // `ssrEntryCode` (e.g. the astro integration or the legacy
+ // `varlockSvelteKitCloudflarePlugin`) so we never double-inject.
+ async function detectSvelteKitCloudflareTarget(config: { plugins?: ReadonlyArray }) {
+ if (resolvedSsrEntryCode) return;
+
+ // SvelteKit's setup plugin (`vite-plugin-sveltekit-setup`) exposes the
+ // resolved config via `api.options`. Match structurally on the
+ // `kit.adapter` shape rather than the plugin name so this is resilient to
+ // version changes — only SvelteKit exposes a resolved adapter this way.
+ const sveltekitSetup = config.plugins?.find((p) => p?.api?.options?.kit?.adapter);
+ if (!sveltekitSetup) return;
+
+ const adapterName: string | undefined = sveltekitSetup.api.options.kit.adapter?.name;
+ // `adapter-auto` keeps its own name even when it resolves to Cloudflare in
+ // CF's CI, so fall back to the platform env vars CF sets at build time.
+ const isCloudflare = adapterName === '@sveltejs/adapter-cloudflare'
+ || (adapterName === '@sveltejs/adapter-auto' && !!(process.env.CF_PAGES || process.env.WORKERS_CI));
+ if (!isCloudflare) return;
+
+ try {
+ // Variable specifier + cast: keeps this a runtime-only dynamic import so
+ // typecheck/bundling don't require the (optional) package to be present.
+ const cfEntryCodeModule = '@varlock/cloudflare-integration/ssr-entry-code';
+ const { CLOUDFLARE_SSR_ENTRY_CODE } = await import(cfEntryCodeModule) as { CLOUDFLARE_SSR_ENTRY_CODE: string };
+ resolvedSsrEntryCode = [CLOUDFLARE_SSR_ENTRY_CODE];
+ resolvedSsrEdgeRuntime = true;
+ debug('detected SvelteKit + Cloudflare adapter — injecting edge env loader');
+ // Surface the auto-detection so it isn't silent magic in the build output.
+ if (!cfDetectNoticeLogged) {
+ cfDetectNoticeLogged = true;
+ console.log('\x1b[36m🔒 [varlock] detected @sveltejs/adapter-cloudflare — injecting the Cloudflare Workers env loader into the SSR entry\x1b[0m');
+ }
+ } catch {
+ throw new Error(
+ '[varlock] SvelteKit deploying to Cloudflare requires @varlock/cloudflare-integration.\n'
+ + 'Install it alongside @varlock/vite-integration: npm install @varlock/cloudflare-integration',
+ );
+ }
+ }
+
return {
name: 'inject-varlock-config',
enforce: 'post',
@@ -332,11 +392,25 @@ See https://varlock.dev/integrations/vite/ for more details.
process.exit(1);
}
}
+
+ if (!hasCfPlugin) {
+ // Keep the `cloudflare:workers` runtime import that the SvelteKit+Cloudflare
+ // env loader injects into the SSR entry (see configResolved). The adapter
+ // isn't resolvable this early, so we add it whenever the CF Vite plugin is
+ // absent — it's inert unless something actually imports the specifier
+ // (Rollup only externalizes specifiers that appear in the module graph).
+ // When the CF Vite plugin IS present (non-SvelteKit CF setups) it manages
+ // `cloudflare:*` externals itself, so we stay out of its way. Returned as a
+ // partial config so Vite's mergeConfig folds it into any existing externals.
+ return { build: { rollupOptions: { external: ['cloudflare:workers'] } } };
+ }
},
// hook to observe/modify config after it is resolved
- configResolved(config) {
+ async configResolved(config) {
debug('vite plugin - configResolved fn called');
+ await detectSvelteKitCloudflareTarget(config);
+
if (!varlockLoadedEnv) return;
// inject all .env files that varlock loaded into `configFileDependencies`
// so that vite will watch them and reload if they change
diff --git a/packages/varlock-website/src/content/docs/integrations/cloudflare.mdx b/packages/varlock-website/src/content/docs/integrations/cloudflare.mdx
index 54485eec5..88b02c2f6 100644
--- a/packages/varlock-website/src/content/docs/integrations/cloudflare.mdx
+++ b/packages/varlock-website/src/content/docs/integrations/cloudflare.mdx
@@ -34,10 +34,10 @@ All paths use the same `.env.schema` and `varlock-wrangler deploy` for productio
| **[`varlock-wrangler`](#approach-1-using-varlock-wrangler)** | Plain Workers, minimal tooling | `varlock-wrangler dev` injects env via FIFO and watches `.env` files; `varlock-wrangler deploy` uploads vars + secrets |
| **[Vite plugin](#approach-2-with-the-vite-plugin)** | Already using [`@cloudflare/vite-plugin`](https://developers.cloudflare.com/workers/vite-plugin/) | `vite dev` injects into miniflare automatically; `vite build` + `varlock-wrangler deploy` |
| **[Astro adapter](/integrations/astro/#deploying-to-cloudflare-workers)** | Astro with [`@astrojs/cloudflare`](https://docs.astro.build/en/guides/integrations-guide/cloudflare/) | `@varlock/astro-integration` auto-detects the adapter; `astro dev` works as-is; `astro build` + `varlock-wrangler deploy` |
-| **[SvelteKit adapter plugin](/integrations/sveltekit/#sveltekit-on-cloudflare-workers)** | SvelteKit with [`@sveltejs/adapter-cloudflare`](https://svelte.dev/docs/kit/adapter-cloudflare) | `vite dev` via an SSR entry loader; `vite build` + `varlock-wrangler deploy` |
+| **[SvelteKit](/integrations/sveltekit/#setup-for-cloudflare-workers)** | SvelteKit with [`@sveltejs/adapter-cloudflare`](https://svelte.dev/docs/kit/adapter-cloudflare) | `@varlock/vite-integration` auto-detects the adapter; `vite dev` via an SSR entry loader; `vite build` + `varlock-wrangler deploy` |
-:::caution[SvelteKit uses a separate entry point]
-`@cloudflare/vite-plugin` does not support SvelteKit ([workers-sdk#8922](https://github.com/cloudflare/workers-sdk/issues/8922)). SvelteKit projects must use [`varlockSvelteKitCloudflarePlugin`](/integrations/sveltekit/#sveltekit-on-cloudflare-workers) from `@varlock/cloudflare-integration/sveltekit` — **not** the standard Vite plugin.
+:::caution[Using SvelteKit? Follow the SvelteKit guide instead]
+This page covers plain Workers and the `@cloudflare/vite-plugin` flow, which **does not support SvelteKit** ([workers-sdk#8922](https://github.com/cloudflare/workers-sdk/issues/8922)). For SvelteKit with `@sveltejs/adapter-cloudflare`, set up varlock using the **[SvelteKit → Cloudflare Workers guide](/integrations/sveltekit/#setup-for-cloudflare-workers)** — you use the standard `varlockVitePlugin` (which auto-detects the adapter), **not** `varlockCloudflareVitePlugin`. You'll still install `@varlock/cloudflare-integration` (for `varlock-wrangler`) and deploy the same way described below.
:::
## Approach 1: Using `varlock-wrangler`
@@ -104,7 +104,7 @@ Replace your `wrangler` commands with `varlock-wrangler` and initialize varlock
If you're building with Vite, `varlockCloudflareVitePlugin` wraps the [Cloudflare Workers Vite plugin](https://developers.cloudflare.com/workers/vite-plugin/) and adds env var + varlock init injection — no extra init import needed, and both `ENV.MY_VAR` and native `env.MY_VAR` work in dev and production.
-For SvelteKit on Cloudflare, use the dedicated [`varlockSvelteKitCloudflarePlugin`](/integrations/sveltekit/#sveltekit-on-cloudflare-workers) from `@varlock/cloudflare-integration/sveltekit` instead.
+For SvelteKit on Cloudflare, use the standard [`varlockVitePlugin`](/integrations/sveltekit/#setup-for-cloudflare-workers) instead — it auto-detects the Cloudflare adapter and wires up the SSR entry loader.
### Setup
diff --git a/packages/varlock-website/src/content/docs/integrations/sveltekit.mdx b/packages/varlock-website/src/content/docs/integrations/sveltekit.mdx
index 3e1af932c..a052964a5 100644
--- a/packages/varlock-website/src/content/docs/integrations/sveltekit.mdx
+++ b/packages/varlock-website/src/content/docs/integrations/sveltekit.mdx
@@ -7,22 +7,21 @@ import Badge from '@/components/Badge.astro';
import ExecCommandWidget from '@/components/ExecCommandWidget.astro';
import InstallJsDepsWidget from '@/components/InstallJsDepsWidget.astro';
-[SvelteKit](https://svelte.dev/docs/kit) is built on [Vite](https://vite.dev), so there's no dedicated SvelteKit package — which integration you use depends on where you deploy.
+[SvelteKit](https://svelte.dev/docs/kit) is built on [Vite](https://vite.dev), so there's no dedicated SvelteKit package — you use the [Vite integration](/integrations/vite/) the same way regardless of where you deploy.
-- **Node / Vercel / Netlify / self-hosted / most adapters** — use the [Vite integration](/integrations/vite/). See [setup below](#setup).
-- **Cloudflare Workers (via `@sveltejs/adapter-cloudflare`)** — use `varlockSvelteKitCloudflarePlugin` from `@varlock/cloudflare-integration/sveltekit`. See [setup below](#sveltekit-on-cloudflare-workers).
+Add `varlockVitePlugin()` to your Vite config ([setup below](#setup)) and it works across deploy targets. If you deploy to **Cloudflare Workers** (via [`@sveltejs/adapter-cloudflare`](https://svelte.dev/docs/kit/adapter-cloudflare)), the plugin detects the adapter and automatically wires up the runtime env-loader — you just need `@varlock/cloudflare-integration` installed as well (it ships `varlock-wrangler`). See [Cloudflare Workers setup](#setup-for-cloudflare-workers).
Check out the [SvelteKit example project](https://github.com/dmno-dev/varlock-examples/tree/main/examples/sveltekit) for a working reference.
---
-## Setup (Vite integration)
+## Setup
-For any SvelteKit deployment target _other than_ Cloudflare Workers, use `varlockVitePlugin` exactly as you would in any Vite project.
+Use `varlockVitePlugin` exactly as you would in any Vite project. This is the setup for every deploy target — see [Cloudflare Workers](#setup-for-cloudflare-workers) below for the one extra package CF deployments need.
@@ -62,10 +61,12 @@ See the [**Vite integration page**](/integrations/vite/) for configuration optio
-For SvelteKit projects deploying to Cloudflare Workers via [`@sveltejs/adapter-cloudflare`](https://svelte.dev/docs/kit/adapter-cloudflare), use `varlockSvelteKitCloudflarePlugin` from `@varlock/cloudflare-integration/sveltekit`. This is a separate entry point from the standard `varlockCloudflareVitePlugin` because `@cloudflare/vite-plugin` doesn't currently support SvelteKit (see [cloudflare/workers-sdk#8922](https://github.com/cloudflare/workers-sdk/issues/8922)). The SvelteKit plugin skips that dependency entirely and injects the runtime env-loader into SvelteKit's SSR entry instead, so the resolved env is available inside the worker.
+For SvelteKit projects deploying to Cloudflare Workers via [`@sveltejs/adapter-cloudflare`](https://svelte.dev/docs/kit/adapter-cloudflare), use the same `varlockVitePlugin()` from the [setup above](#setup). When it detects the Cloudflare adapter, it automatically injects the runtime env-loader into SvelteKit's SSR entry so the resolved env is available inside the worker. You just need to additionally install `@varlock/cloudflare-integration` — it provides `varlock-wrangler` and the Cloudflare-specific loader the plugin pulls in. (The standard `varlockCloudflareVitePlugin` isn't used here because `@cloudflare/vite-plugin` doesn't currently support SvelteKit — see [cloudflare/workers-sdk#8922](https://github.com/cloudflare/workers-sdk/issues/8922).)
+
+The adapter is detected whether you configure it in `svelte.config.js` or inline in your Vite config (SvelteKit ≥ 2.62), so no extra setup is needed either way.
:::note[Static-only SvelteKit sites]
-If your SvelteKit app is fully static (no SSR — typically using `@sveltejs/adapter-static` and deployed to Cloudflare Pages or any CDN), you don't need this section at all. Use the generic [Vite integration](#setup) above — there's no server bundle for us to inject into.
+If your SvelteKit app is fully static (no SSR — typically using `@sveltejs/adapter-static` and deployed to Cloudflare Pages or any CDN), you don't need `@varlock/cloudflare-integration` at all. The [base setup](#setup) is enough — there's no server bundle for us to inject into.
:::
### Setup
@@ -73,22 +74,22 @@ If your SvelteKit app is fully static (no SSR — typically using `@sveltejs/ada
1. **Install packages**
-
+
1. **Run `varlock init` to set up your `.env.schema`**
-1. **Add the plugin to your Vite config**
+1. **Add the plugin to your Vite config** — the same as the base setup; it auto-detects the Cloudflare adapter:
```diff lang="ts" title="vite.config.ts"
import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vite';
- +import { varlockSvelteKitCloudflarePlugin } from '@varlock/cloudflare-integration/sveltekit';
+ +import { varlockVitePlugin } from '@varlock/vite-integration';
export default defineConfig({
plugins: [
- + varlockSvelteKitCloudflarePlugin(),
+ + varlockVitePlugin(),
sveltekit(),
],
});
diff --git a/scripts/detect-changed-integrations.ts b/scripts/detect-changed-integrations.ts
index fa993995b..142f2900a 100644
--- a/scripts/detect-changed-integrations.ts
+++ b/scripts/detect-changed-integrations.ts
@@ -30,6 +30,9 @@ const INTEGRATION_PACKAGES: Record> = {
'vanilla-node': [], // no integration package — triggered only by core varlock changes
astro: ['@varlock/astro-integration', '@varlock/vite-integration'],
'tanstack-start': ['@varlock/cloudflare-integration', '@varlock/vite-integration'],
+ // SvelteKit uses the vite plugin directly; the CF scenario also pulls the
+ // cloudflare integration's edge loader via auto-detection.
+ sveltekit: ['@varlock/vite-integration', '@varlock/cloudflare-integration'],
};
// Quick test paths for integrations where full suite is expensive.