From 3500d1e0de3eb5291adade51d516c19e09d6c254 Mon Sep 17 00:00:00 2001 From: Theo Ephraim Date: Tue, 3 Nov 2026 11:20:28 -0700 Subject: [PATCH] Skip the Cloudflare env loader in SvelteKit vite dev adapter-cloudflare 8 emulates cloudflare:workers in Node during vite dev with wrangler's platform proxy env, which has no __VARLOCK_ENV binding, so the injected loader threw on every SSR request. SvelteKit dev SSR runs in Node and uses varlock's normal load flow, so the loader is now only injected into builds. The deprecated varlockSvelteKitCloudflarePlugin goes through the same detection and logs a deprecation warning. Adds a SvelteKit 3 + adapter-cloudflare 8 framework test suite. Fixes #1267 --- .bumpy/sveltekit-cf-dev-skip-loader.md | 5 + .bumpy/sveltekit-cf-legacy-plugin.md | 5 + framework-tests/README.md | 2 +- .../sveltekit/files/configs/tsconfig.v3.json | 4 + .../vite.config.v3-cloudflare-legacy.ts | 12 ++ .../configs/vite.config.v3-cloudflare.ts | 12 ++ .../sveltekit/sveltekit-v3-cloudflare.test.ts | 173 ++++++++++++++++++ .../integrations/cloudflare/src/sveltekit.ts | 28 +-- packages/integrations/vite/src/index.ts | 8 +- .../vite/test/ssr-init-code.test.ts | 14 +- 10 files changed, 248 insertions(+), 15 deletions(-) create mode 100644 .bumpy/sveltekit-cf-dev-skip-loader.md create mode 100644 .bumpy/sveltekit-cf-legacy-plugin.md create mode 100644 framework-tests/frameworks/sveltekit/files/configs/tsconfig.v3.json create mode 100644 framework-tests/frameworks/sveltekit/files/configs/vite.config.v3-cloudflare-legacy.ts create mode 100644 framework-tests/frameworks/sveltekit/files/configs/vite.config.v3-cloudflare.ts create mode 100644 framework-tests/frameworks/sveltekit/sveltekit-v3-cloudflare.test.ts diff --git a/.bumpy/sveltekit-cf-dev-skip-loader.md b/.bumpy/sveltekit-cf-dev-skip-loader.md new file mode 100644 index 000000000..8851cacb0 --- /dev/null +++ b/.bumpy/sveltekit-cf-dev-skip-loader.md @@ -0,0 +1,5 @@ +--- +"@varlock/vite-integration": patch +--- + +Fix `vite dev` 500s on SvelteKit with adapter-cloudflare 8 ("no __VARLOCK_ENV binding found"): the Workers env loader is now only injected into builds diff --git a/.bumpy/sveltekit-cf-legacy-plugin.md b/.bumpy/sveltekit-cf-legacy-plugin.md new file mode 100644 index 000000000..2264f6328 --- /dev/null +++ b/.bumpy/sveltekit-cf-legacy-plugin.md @@ -0,0 +1,5 @@ +--- +"@varlock/cloudflare-integration": patch +--- + +The deprecated `varlockSvelteKitCloudflarePlugin` now uses the same adapter detection as `varlockVitePlugin()` (fixing `vite dev` on adapter-cloudflare 8) and logs a deprecation warning diff --git a/framework-tests/README.md b/framework-tests/README.md index dc9a3eb46..9b6df7d9f 100644 --- a/framework-tests/README.md +++ b/framework-tests/README.md @@ -75,7 +75,7 @@ Each scenario uses `describeScenario()` which: - **Next.js** — tests multiple versions (14, 15, 16) and bundlers (webpack, turbopack), verifying env injection, leak detection, log redaction, and sourcemap scrubbing - **OpenNext**: Next.js built for Cloudflare Workers with `@opennextjs/cloudflare` and served from local workerd, verifying the frozen (encrypted) env, middleware, request-time leak detection, and log redaction on that target - **Expo** — tests the babel plugin transform pipeline, verifying static replacement of public vars, protection of sensitive vars, and correct handling of server (+api) routes -- **SvelteKit** — tests static replacement behavior for static vs dynamic public vars and runtime access to dynamic public vars via server routes +- **SvelteKit**: tests static replacement behavior for static vs dynamic public vars and runtime access to dynamic public vars via server routes, plus Cloudflare adapter auto-detection on SvelteKit 2 (adapter 7) and SvelteKit 3 (adapter 8, including `vite dev` and prerendered builds) ## Real deployment tests (`deploy/`) diff --git a/framework-tests/frameworks/sveltekit/files/configs/tsconfig.v3.json b/framework-tests/frameworks/sveltekit/files/configs/tsconfig.v3.json new file mode 100644 index 000000000..ca1512b1b --- /dev/null +++ b/framework-tests/frameworks/sveltekit/files/configs/tsconfig.v3.json @@ -0,0 +1,4 @@ +{ + "extends": "$app/tsconfig", + "include": ["src", "*"] +} diff --git a/framework-tests/frameworks/sveltekit/files/configs/vite.config.v3-cloudflare-legacy.ts b/framework-tests/frameworks/sveltekit/files/configs/vite.config.v3-cloudflare-legacy.ts new file mode 100644 index 000000000..3cb40e590 --- /dev/null +++ b/framework-tests/frameworks/sveltekit/files/configs/vite.config.v3-cloudflare-legacy.ts @@ -0,0 +1,12 @@ +import adapter from '@sveltejs/adapter-cloudflare'; +import { sveltekit } from '@sveltejs/kit/vite'; +import { varlockSvelteKitCloudflarePlugin } from '@varlock/cloudflare-integration/sveltekit'; +import { defineConfig } from 'vite'; + +// the deprecated Cloudflare-specific wrapper, kept working for back-compat +export default defineConfig({ + plugins: [ + varlockSvelteKitCloudflarePlugin(), + sveltekit({ adapter: adapter() }), + ], +}); diff --git a/framework-tests/frameworks/sveltekit/files/configs/vite.config.v3-cloudflare.ts b/framework-tests/frameworks/sveltekit/files/configs/vite.config.v3-cloudflare.ts new file mode 100644 index 000000000..0a4857c37 --- /dev/null +++ b/framework-tests/frameworks/sveltekit/files/configs/vite.config.v3-cloudflare.ts @@ -0,0 +1,12 @@ +import adapter from '@sveltejs/adapter-cloudflare'; +import { sveltekit } from '@sveltejs/kit/vite'; +import { varlockVitePlugin } from '@varlock/vite-integration'; +import { defineConfig } from 'vite'; + +// SvelteKit 3 dropped svelte.config.js: the adapter is configured inline +export default defineConfig({ + plugins: [ + varlockVitePlugin(), + sveltekit({ adapter: adapter() }), + ], +}); diff --git a/framework-tests/frameworks/sveltekit/sveltekit-v3-cloudflare.test.ts b/framework-tests/frameworks/sveltekit/sveltekit-v3-cloudflare.test.ts new file mode 100644 index 000000000..ccc5c5c84 --- /dev/null +++ b/framework-tests/frameworks/sveltekit/sveltekit-v3-cloudflare.test.ts @@ -0,0 +1,173 @@ +/* +SvelteKit 3 + adapter-cloudflare 8. + +Adapter 8 resolves `cloudflare:workers` itself: in `vite dev` it maps to +wrangler's platform proxy env (running in Node), and during the build it maps to +a stub that throws on any property access outside a request. Varlock's injected +Workers env loader must not mistake either one for a real Worker. +*/ +import { + describe, beforeAll, afterAll, +} from 'vitest'; +import { FrameworkTestEnv } from '../../harness/index'; + +describe('SvelteKit 3 + Cloudflare adapter 8', () => { + const env = new FrameworkTestEnv({ + testDir: import.meta.dirname, + framework: 'sveltekit-v3-cloudflare', + packageManager: 'pnpm', + dependencies: { + '@sveltejs/adapter-cloudflare': '^8', + '@sveltejs/kit': '^3', + '@sveltejs/vite-plugin-svelte': '^7', + svelte: '^5', + vite: '^8', + wrangler: '^4', + varlock: 'will-be-replaced', + '@varlock/vite-integration': 'will-be-replaced', + '@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', + 'vite.config.ts': 'configs/vite.config.v3-cloudflare.ts', + 'tsconfig.json': 'configs/tsconfig.v3.json', + 'wrangler.jsonc': 'configs/wrangler.jsonc', + 'src/routes/+page.svelte': 'pages/basic-page.svelte', + }, + }); + + beforeAll(() => env.setup(), 180_000); + afterAll(() => env.teardown()); + + // SvelteKit 3 no longer supports svelte.config.js + const deleteFiles = ['svelte.config.js']; + + // Regression for #1267: the loader threw "no __VARLOCK_ENV binding found" on + // every SSR request, because the platform proxy env has no varlock binding + env.describeDevScenario('dev: ENV available at runtime, secret redacted', { + command: 'vite dev --port 14732', + readyPattern: /localhost:14732/, + readyTimeout: 60_000, + deleteFiles, + 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'], + }, + }, + { + path: '/', + bodyAssertions: { + shouldContain: ['public-test-value'], + shouldNotContain: ['__VARLOCK_ENV binding'], + }, + }, + ], + }); + + env.describeDevScenario('dev: deprecated varlockSvelteKitCloudflarePlugin still works', { + command: 'vite dev --port 14733', + readyPattern: /localhost:14733/, + readyTimeout: 60_000, + deleteFiles, + templateFiles: { + 'vite.config.ts': 'configs/vite.config.v3-cloudflare-legacy.ts', + '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'], + }, + }, + ], + outputAssertions: [ + { + description: 'logs the deprecation warning', + shouldContain: ['varlockSvelteKitCloudflarePlugin is deprecated'], + }, + ], + }); + + env.describeScenario('build: deprecated varlockSvelteKitCloudflarePlugin injects the edge loader', { + command: 'vite build', + expectSuccess: true, + timeout: 180_000, + deleteFiles, + templateFiles: { + 'vite.config.ts': 'configs/vite.config.v3-cloudflare-legacy.ts', + 'src/routes/api/env/+server.ts': 'routes/env-endpoint.ts', + }, + fileAssertions: [ + { + description: 'SSR bundle contains the injected Cloudflare runtime env-loader', + fileGlob: '.svelte-kit/output/server/**/*.js', + shouldContain: ['__VARLOCK_ENV_CHUNKS'], + }, + ], + }); + + env.describeScenario('build: adapter detected and the edge loader injected', { + command: 'vite build', + expectSuccess: true, + timeout: 180_000, + deleteFiles, + templateFiles: { + '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', + shouldContain: ['__VARLOCK_ENV_CHUNKS'], + }, + { + description: 'sensitive value is not inlined into any built output', + fileGlob: '.svelte-kit/**/*.js', + shouldNotContain: ['super-secret-value'], + }, + ], + }); + + // SvelteKit evaluates the SSR entry in Node while prerendering, where the + // adapter's `cloudflare:workers` stub throws on property access + env.describeScenario('build: fully prerendered app does not break', { + command: 'vite build', + expectSuccess: true, + timeout: 180_000, + deleteFiles, + templateFiles: { + '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/packages/integrations/cloudflare/src/sveltekit.ts b/packages/integrations/cloudflare/src/sveltekit.ts index 1a497b06a..825eb765d 100644 --- a/packages/integrations/cloudflare/src/sveltekit.ts +++ b/packages/integrations/cloudflare/src/sveltekit.ts @@ -1,14 +1,15 @@ import { varlockVitePlugin } from '@varlock/vite-integration'; -import { CLOUDFLARE_SSR_ENTRY_CODE } from './shared-ssr-entry-code'; + +let deprecationNoticeLogged = false; /** * Varlock SvelteKit + Cloudflare Vite plugin. * * @deprecated Use `varlockVitePlugin()` from `@varlock/vite-integration` - * instead — it now auto-detects SvelteKit projects using + * instead. It 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. + * back-compat and goes through the same auto-detection. * * @example * ```ts @@ -26,12 +27,17 @@ 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 { - // `varlockVitePlugin` already marks `cloudflare:workers` external; here we - // just force the Cloudflare env loader explicitly (bypassing auto-detection). - return [ - varlockVitePlugin({ - ssrEntryCode: [CLOUDFLARE_SSR_ENTRY_CODE], - isCloudflareTarget: true, - }), - ]; + if (!deprecationNoticeLogged) { + deprecationNoticeLogged = true; + // eslint-disable-next-line no-console + console.warn( + '[varlock] varlockSvelteKitCloudflarePlugin is deprecated. ' + + 'Use varlockVitePlugin() from @varlock/vite-integration, which detects adapter-cloudflare on its own.', + ); + } + // No explicit `ssrEntryCode`: the vite plugin's SvelteKit detection injects + // the Workers env loader into builds and skips it in `vite dev`, where SSR + // runs in Node (adapter-cloudflare 8 emulates `cloudflare:workers` there with + // an env that has no varlock binding). + return [varlockVitePlugin({ isCloudflareTarget: true })]; } diff --git a/packages/integrations/vite/src/index.ts b/packages/integrations/vite/src/index.ts index a3d677eea..24f3114d3 100644 --- a/packages/integrations/vite/src/index.ts +++ b/packages/integrations/vite/src/index.ts @@ -568,8 +568,14 @@ export function varlockVitePlugin( // consumer already supplied // `ssrEntryCode` (e.g. the astro integration or the legacy // `varlockSvelteKitCloudflarePlugin`) so we never double-inject. - async function detectSvelteKitCloudflareTarget(config: { plugins?: ReadonlyArray }) { + async function detectSvelteKitCloudflareTarget(config: { command?: string, plugins?: ReadonlyArray }) { if (resolvedSsrEntryCode) return; + // `vite dev` runs SvelteKit's SSR in Node, where varlock's normal flow loads + // the env. adapter-cloudflare 8 emulates `cloudflare:workers` there with + // wrangler's platform proxy, whose env has no `__VARLOCK_ENV` binding, so the + // loader would throw. `vite preview` also reports 'serve' but runs the built + // output, so the loader only matters for builds. + if (config.command === 'serve') return; // SvelteKit's setup plugin (`vite-plugin-sveltekit-setup`) exposes the // resolved config via `api.options`. SvelteKit 2 nests it under `kit`, diff --git a/packages/integrations/vite/test/ssr-init-code.test.ts b/packages/integrations/vite/test/ssr-init-code.test.ts index c114cac67..d65b41be1 100644 --- a/packages/integrations/vite/test/ssr-init-code.test.ts +++ b/packages/integrations/vite/test/ssr-init-code.test.ts @@ -402,13 +402,17 @@ describe('SvelteKit Cloudflare adapter detection', () => { api: { options }, }); - async function initModuleCode(options: Record, otherPlugins: Array = []) { + async function initModuleCode( + options: Record, + otherPlugins: Array = [], + command: 'build' | 'serve' = 'build', + ) { const { varlockVitePlugin } = await importPlugin(); const resolved = await resolveConfig({ configFile: false, logLevel: 'silent', plugins: [...varlockVitePlugin(), ...otherPlugins, sveltekitSetup(options)], - }, 'build'); + }, command); const plugin = findPlugin(resolved.plugins as Array, 'inject-varlock-config'); return plugin.load.call({ environment: { name: 'ssr' } }, '\0varlock-ssr-init') as string; } @@ -423,6 +427,12 @@ describe('SvelteKit Cloudflare adapter detection', () => { expect(code).toContain('/* cloudflare edge env loader */'); }); + it('skips the edge env loader in `vite dev`, where SSR runs in Node', async () => { + const adapter = { name: '@sveltejs/adapter-cloudflare' }; + expect(await initModuleCode({ adapter }, [], 'serve')).not.toContain('cloudflare edge env loader'); + expect(await initModuleCode({ kit: { adapter } }, [], 'serve')).not.toContain('cloudflare edge env loader'); + }); + it('leaves other adapters alone, in both config shapes', async () => { const nodeAdapter = { name: '@sveltejs/adapter-node' }; expect(await initModuleCode({ kit: { adapter: nodeAdapter } })).not.toContain('cloudflare edge env loader');