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
5 changes: 5 additions & 0 deletions .bumpy/sveltekit-cf-dev-skip-loader.md
Original file line number Diff line number Diff line change
@@ -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
5 changes: 5 additions & 0 deletions .bumpy/sveltekit-cf-legacy-plugin.md
Original file line number Diff line number Diff line change
@@ -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
2 changes: 1 addition & 1 deletion framework-tests/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/`)

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"extends": "$app/tsconfig",
"include": ["src", "*"]
}
Original file line number Diff line number Diff line change
@@ -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() }),
],
});
Original file line number Diff line number Diff line change
@@ -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() }),
],
});
173 changes: 173 additions & 0 deletions framework-tests/frameworks/sveltekit/sveltekit-v3-cloudflare.test.ts
Original file line number Diff line number Diff line change
@@ -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'],
},
],
});
});
28 changes: 17 additions & 11 deletions packages/integrations/cloudflare/src/sveltekit.ts
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -26,12 +27,17 @@ 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> {
// `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 })];
}
8 changes: 7 additions & 1 deletion packages/integrations/vite/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<any> }) {
async function detectSvelteKitCloudflareTarget(config: { command?: string, plugins?: ReadonlyArray<any> }) {
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`,
Expand Down
14 changes: 12 additions & 2 deletions packages/integrations/vite/test/ssr-init-code.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -402,13 +402,17 @@ describe('SvelteKit Cloudflare adapter detection', () => {
api: { options },
});

async function initModuleCode(options: Record<string, any>, otherPlugins: Array<any> = []) {
async function initModuleCode(
options: Record<string, any>,
otherPlugins: Array<any> = [],
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<any>, 'inject-varlock-config');
return plugin.load.call({ environment: { name: 'ssr' } }, '\0varlock-ssr-init') as string;
}
Expand All @@ -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');
Expand Down
Loading