From f9dcd3f201cc12b6b61b4ace761febf1fc4ffe7f Mon Sep 17 00:00:00 2001 From: Theo Ephraim Date: Tue, 23 Jun 2026 15:00:44 -0700 Subject: [PATCH] feat(vite): auto-detect SvelteKit Cloudflare adapter The standard varlockVitePlugin() now detects @sveltejs/adapter-cloudflare (configured in svelte.config.js or inline in vite.config) and injects the Cloudflare Workers runtime env-loader automatically, so the same import works across every deploy target. The Cloudflare-specific loader stays in @varlock/cloudflare-integration (an optional peer dep) and is pulled in lazily. - deprecate varlockSvelteKitCloudflarePlugin to a thin alias - single setup path in the SvelteKit/Cloudflare docs - add SvelteKit framework tests (build, dev, cloudflare auto-detection) --- .bumpy/sveltekit-cf-autodetect.md | 6 + framework-tests/bun.lock | 5 + .../sveltekit/files/_base/package.json | 10 ++ .../sveltekit/files/_base/src/app.html | 11 ++ .../sveltekit/files/_base/svelte.config.js | 12 ++ .../sveltekit/files/_base/tsconfig.json | 14 ++ .../sveltekit/files/_base/vite.config.ts | 13 ++ .../files/configs/svelte.config.cloudflare.js | 12 ++ .../sveltekit/files/configs/wrangler.jsonc | 10 ++ .../sveltekit/files/pages/basic-page.svelte | 10 ++ .../sveltekit/files/pages/prerender.ts | 4 + .../sveltekit/files/routes/env-endpoint.ts | 15 ++ .../sveltekit/files/schemas/.env.dev | 1 + .../sveltekit/files/schemas/.env.prod | 1 + .../sveltekit/files/schemas/.env.schema | 12 ++ .../frameworks/sveltekit/sveltekit.test.ts | 142 ++++++++++++++++++ framework-tests/package.json | 1 + packages/integrations/cloudflare/src/index.ts | 9 +- .../integrations/cloudflare/src/sveltekit.ts | 38 ++--- packages/integrations/vite/src/index.ts | 82 +++++++++- .../content/docs/integrations/cloudflare.mdx | 8 +- .../content/docs/integrations/sveltekit.mdx | 23 +-- scripts/detect-changed-integrations.ts | 3 + 23 files changed, 389 insertions(+), 53 deletions(-) create mode 100644 .bumpy/sveltekit-cf-autodetect.md create mode 100644 framework-tests/frameworks/sveltekit/files/_base/package.json create mode 100644 framework-tests/frameworks/sveltekit/files/_base/src/app.html create mode 100644 framework-tests/frameworks/sveltekit/files/_base/svelte.config.js create mode 100644 framework-tests/frameworks/sveltekit/files/_base/tsconfig.json create mode 100644 framework-tests/frameworks/sveltekit/files/_base/vite.config.ts create mode 100644 framework-tests/frameworks/sveltekit/files/configs/svelte.config.cloudflare.js create mode 100644 framework-tests/frameworks/sveltekit/files/configs/wrangler.jsonc create mode 100644 framework-tests/frameworks/sveltekit/files/pages/basic-page.svelte create mode 100644 framework-tests/frameworks/sveltekit/files/pages/prerender.ts create mode 100644 framework-tests/frameworks/sveltekit/files/routes/env-endpoint.ts create mode 100644 framework-tests/frameworks/sveltekit/files/schemas/.env.dev create mode 100644 framework-tests/frameworks/sveltekit/files/schemas/.env.prod create mode 100644 framework-tests/frameworks/sveltekit/files/schemas/.env.schema create mode 100644 framework-tests/frameworks/sveltekit/sveltekit.test.ts 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.