Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .bumpy/sveltekit-cf-autodetect.md
Original file line number Diff line number Diff line change
@@ -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.
5 changes: 5 additions & 0 deletions framework-tests/bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 10 additions & 0 deletions framework-tests/frameworks/sveltekit/files/_base/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"name": "sveltekit-cloudflare-framework-test",
"version": "0.0.0",
"private": true,
"type": "module",
"scripts": {
"build": "vite build",
"dev": "vite dev"
}
}
11 changes: 11 additions & 0 deletions framework-tests/frameworks/sveltekit/files/_base/src/app.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div>
</body>
</html>
12 changes: 12 additions & 0 deletions framework-tests/frameworks/sveltekit/files/_base/svelte.config.js
Original file line number Diff line number Diff line change
@@ -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;
14 changes: 14 additions & 0 deletions framework-tests/frameworks/sveltekit/files/_base/tsconfig.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
13 changes: 13 additions & 0 deletions framework-tests/frameworks/sveltekit/files/_base/vite.config.ts
Original file line number Diff line number Diff line change
@@ -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(),
],
});
Original file line number Diff line number Diff line change
@@ -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;
10 changes: 10 additions & 0 deletions framework-tests/frameworks/sveltekit/files/configs/wrangler.jsonc
Original file line number Diff line number Diff line change
@@ -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"
}
}
10 changes: 10 additions & 0 deletions framework-tests/frameworks/sveltekit/files/pages/basic-page.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<script lang="ts">
import { ENV } from 'varlock/env';

const publicVar = ENV.PUBLIC_VAR;
const apiUrl = ENV.API_URL;
</script>

<h1>SvelteKit + Cloudflare Varlock Test</h1>
<p class="public">{publicVar}</p>
<p class="api">{apiUrl}</p>
4 changes: 4 additions & 0 deletions framework-tests/frameworks/sveltekit/files/pages/prerender.ts
Original file line number Diff line number Diff line change
@@ -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;
15 changes: 15 additions & 0 deletions framework-tests/frameworks/sveltekit/files/routes/env-endpoint.ts
Original file line number Diff line number Diff line change
@@ -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' } },
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
APP_ENV=dev
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
APP_ENV=prod
12 changes: 12 additions & 0 deletions framework-tests/frameworks/sveltekit/files/schemas/.env.schema
Original file line number Diff line number Diff line change
@@ -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
142 changes: 142 additions & 0 deletions framework-tests/frameworks/sveltekit/sveltekit.test.ts
Original file line number Diff line number Diff line change
@@ -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'],
},
],
});
});
1 change: 1 addition & 0 deletions framework-tests/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
9 changes: 4 additions & 5 deletions packages/integrations/cloudflare/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
38 changes: 9 additions & 29 deletions packages/integrations/cloudflare/src/sveltekit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
* ],
* });
Expand All @@ -30,25 +26,9 @@ import { CLOUDFLARE_SSR_ENTRY_CODE } from './shared-ssr-entry-code';
// Return type is `Array<any>` to avoid symlink-induced Vite Plugin type
// conflicts (see note in ./index.ts).
export function varlockSvelteKitCloudflarePlugin(): Array<any> {
// 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],
Expand Down
Loading
Loading