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');