From d003ccb43f62baea511fb732cf3f44d3cd59e6d8 Mon Sep 17 00:00:00 2001 From: Theo Ephraim Date: Mon, 4 May 2026 16:24:35 -0700 Subject: [PATCH 01/17] fix: cloudflare + tanstack start + vite 6/7/8 compatibility - Fix duplicate SSR init injection when TanStack Start + Cloudflare detect multiple entry modules in the same environment (dedup via Set keyed by environment name, build-mode only) - Fix Vite 6 dev mode crash: `isEntry` property throws in dev, wrapped in try/catch with fallback to moduleIds[0] - Fix Vite dep optimizer breaking varlock/env in CF worker environments: exclude varlock packages from optimizeDeps via configEnvironment (Vite 7+) and configResolved (Vite 6 fallback) - Add preview env injection for CF builds via FIFO named pipe so secrets never touch disk (Windows falls back to temp file) - Split vite and cloudflare framework tests by version (v5-v8) - Add TanStack Start framework tests (node + cloudflare, vite 6/7/8) - Register tanstack-start in CI integration detection --- .../cloudflare/cloudflare-shared.ts | 188 ++++++ .../cloudflare/cloudflare-vite.test.ts | 149 ----- .../cloudflare/cloudflare-vite6.test.ts | 6 + .../cloudflare/cloudflare-vite7.test.ts | 6 + .../cloudflare/cloudflare-vite8.test.ts | 6 + .../tanstack-start/files/_base/tsconfig.json | 16 + .../files/configs/vite.config.cloudflare.ts | 15 + .../files/configs/vite.config.node.ts | 12 + .../files/configs/wrangler.jsonc | 6 + .../tanstack-start/files/routes/__root.tsx | 5 + .../tanstack-start/files/routes/index.tsx | 32 ++ .../tanstack-start/files/routes/router.tsx | 16 + .../tanstack-start/files/schemas/.env.dev | 1 + .../tanstack-start/files/schemas/.env.schema | 12 + .../tanstack-start/tanstack-shared.ts | 226 ++++++++ .../tanstack-start/tanstack-vite6.test.ts | 5 + .../tanstack-start/tanstack-vite7.test.ts | 5 + .../tanstack-start/tanstack-vite8.test.ts | 6 + .../frameworks/vite/vite-shared.ts | 537 ++++++++++++++++++ .../frameworks/vite/vite-v5.test.ts | 6 + .../frameworks/vite/vite-v6.test.ts | 6 + .../frameworks/vite/vite-v7.test.ts | 6 + .../frameworks/vite/vite-v8.test.ts | 6 + framework-tests/frameworks/vite/vite.test.ts | 525 ----------------- framework-tests/harness/repack.ts | 1 + framework-tests/package.json | 1 + packages/integrations/cloudflare/src/index.ts | 201 ++++++- packages/integrations/vite/src/index.ts | 164 ++++-- scripts/detect-changed-integrations.ts | 1 + 29 files changed, 1457 insertions(+), 709 deletions(-) create mode 100644 framework-tests/frameworks/cloudflare/cloudflare-shared.ts delete mode 100644 framework-tests/frameworks/cloudflare/cloudflare-vite.test.ts create mode 100644 framework-tests/frameworks/cloudflare/cloudflare-vite6.test.ts create mode 100644 framework-tests/frameworks/cloudflare/cloudflare-vite7.test.ts create mode 100644 framework-tests/frameworks/cloudflare/cloudflare-vite8.test.ts create mode 100644 framework-tests/frameworks/tanstack-start/files/_base/tsconfig.json create mode 100644 framework-tests/frameworks/tanstack-start/files/configs/vite.config.cloudflare.ts create mode 100644 framework-tests/frameworks/tanstack-start/files/configs/vite.config.node.ts create mode 100644 framework-tests/frameworks/tanstack-start/files/configs/wrangler.jsonc create mode 100644 framework-tests/frameworks/tanstack-start/files/routes/__root.tsx create mode 100644 framework-tests/frameworks/tanstack-start/files/routes/index.tsx create mode 100644 framework-tests/frameworks/tanstack-start/files/routes/router.tsx create mode 100644 framework-tests/frameworks/tanstack-start/files/schemas/.env.dev create mode 100644 framework-tests/frameworks/tanstack-start/files/schemas/.env.schema create mode 100644 framework-tests/frameworks/tanstack-start/tanstack-shared.ts create mode 100644 framework-tests/frameworks/tanstack-start/tanstack-vite6.test.ts create mode 100644 framework-tests/frameworks/tanstack-start/tanstack-vite7.test.ts create mode 100644 framework-tests/frameworks/tanstack-start/tanstack-vite8.test.ts create mode 100644 framework-tests/frameworks/vite/vite-shared.ts create mode 100644 framework-tests/frameworks/vite/vite-v5.test.ts create mode 100644 framework-tests/frameworks/vite/vite-v6.test.ts create mode 100644 framework-tests/frameworks/vite/vite-v7.test.ts create mode 100644 framework-tests/frameworks/vite/vite-v8.test.ts delete mode 100644 framework-tests/frameworks/vite/vite.test.ts diff --git a/framework-tests/frameworks/cloudflare/cloudflare-shared.ts b/framework-tests/frameworks/cloudflare/cloudflare-shared.ts new file mode 100644 index 00000000..f12f2e65 --- /dev/null +++ b/framework-tests/frameworks/cloudflare/cloudflare-shared.ts @@ -0,0 +1,188 @@ +/* +Shared Cloudflare Workers test definitions, parameterized by Vite version. +Covers basic worker dev, leak detection, build + preview, and large env chunking. +*/ +import { + describe, beforeAll, afterAll, +} from 'vitest'; +import { FrameworkTestEnv } from '../../harness/index'; + +export function defineCloudflareTests( + label: string, + testDir: string, + opts: { + viteVersion: string; + /** Base port — each dev scenario offsets from this */ + basePort: number; + }, +) { + const { viteVersion, basePort } = opts; + + describe(`Cloudflare Workers (${label})`, () => { + const cfEnv = new FrameworkTestEnv({ + testDir, + framework: `cloudflare-vite-${label}`, + packageManager: 'bun', + dependencies: { + varlock: 'will-be-replaced', + '@varlock/cloudflare-integration': 'will-be-replaced', + vite: viteVersion, + wrangler: '^4', + '@cloudflare/vite-plugin': '^1.30.0', + }, + templateFiles: { + '.env.schema': 'schemas/.env.schema', + '.env.dev': 'schemas/.env.dev', + }, + overrides: { + punycode: 'npm:punycode@^2.3.1', + }, + }); + beforeAll(() => cfEnv.setup(), 180_000); + afterAll(() => cfEnv.teardown()); + + cfEnv.describeDevScenario('basic worker', { + command: `vite dev --port ${basePort}`, + readyPattern: /Local:.*http/, + readyTimeout: 30_000, + templateFiles: { + 'src/index.ts': 'workers/basic-worker.ts', + 'vite.config.ts': 'vite-configs/vite.config.ts', + 'wrangler.jsonc': '_base-wrangler/wrangler.jsonc', + 'tsconfig.json': '_base-wrangler/tsconfig.json', + }, + requests: [ + { + path: '/', + bodyAssertions: { + shouldContain: [ + // varlock ENV proxy - non-sensitive + 'public_var::public-test-value', + 'api_url::https://api.example.com', + // varlock ENV proxy - sensitive (accessible but value not leaked) + 'has_sensitive::yes', + // cloudflare native env access + 'native_public_var::public-test-value', + 'native_has_secret::yes', + ], + shouldNotContain: ['super-secret-value'], + }, + }, + ], + outputAssertions: [ + { + description: 'sensitive value is redacted in console output', + shouldContain: ['secret-log-test::'], + shouldNotContain: ['super-secret-value'], + }, + ], + }); + + cfEnv.describeDevScenario('leaky worker', { + command: `vite dev --port ${basePort + 1}`, + readyPattern: /Local:.*http/, + readyTimeout: 30_000, + templateFiles: { + 'src/index.ts': 'workers/leaky-worker.ts', + 'vite.config.ts': 'vite-configs/vite.config.ts', + 'wrangler.jsonc': '_base-wrangler/wrangler.jsonc', + 'tsconfig.json': '_base-wrangler/tsconfig.json', + }, + requests: [ + { + path: '/', + expectedStatus: 500, + bodyAssertions: { + shouldNotContain: ['super-secret-value'], + }, + }, + ], + outputAssertions: [ + { + description: 'leak detection message appears', + shouldContain: ['DETECTED LEAKED SENSITIVE CONFIG'], + }, + ], + }); + + cfEnv.describeDevScenario('leaky worker (Uint8Array body)', { + command: `vite dev --port ${basePort + 2}`, + readyPattern: /Local:.*http/, + readyTimeout: 30_000, + templateFiles: { + 'src/index.ts': 'workers/leaky-uint8array-worker.ts', + 'vite.config.ts': 'vite-configs/vite.config.ts', + 'wrangler.jsonc': '_base-wrangler/wrangler.jsonc', + 'tsconfig.json': '_base-wrangler/tsconfig.json', + }, + requests: [ + { + path: '/', + expectedStatus: 500, + bodyAssertions: { + shouldNotContain: ['super-secret-value'], + }, + }, + ], + outputAssertions: [ + { + description: 'leak detection message appears for Uint8Array body', + shouldContain: ['DETECTED LEAKED SENSITIVE CONFIG'], + }, + ], + }); + + cfEnv.describeDevScenario('build + preview', { + command: `vite build && vite preview --port ${basePort + 3}`, + readyPattern: /Local:.*http/, + readyTimeout: 60_000, + timeout: 120_000, + templateFiles: { + 'src/index.ts': 'workers/basic-worker.ts', + 'vite.config.ts': 'vite-configs/vite.config.ts', + 'wrangler.jsonc': '_base-wrangler/wrangler.jsonc', + 'tsconfig.json': '_base-wrangler/tsconfig.json', + }, + requests: [ + { + path: '/', + bodyAssertions: { + shouldContain: [ + 'public_var::public-test-value', + 'api_url::https://api.example.com', + 'has_sensitive::yes', + ], + shouldNotContain: ['super-secret-value'], + }, + }, + ], + }); + + cfEnv.describeDevScenario('large env (chunking)', { + command: `vite dev --port ${basePort + 4}`, + readyPattern: /Local:.*http/, + readyTimeout: 30_000, + templateFiles: { + 'src/index.ts': 'workers/large-env-worker.ts', + 'vite.config.ts': 'vite-configs/vite.config.ts', + 'wrangler.jsonc': '_base-wrangler/wrangler.jsonc', + 'tsconfig.json': '_base-wrangler/tsconfig.json', + '.env.schema': 'schemas/.env.schema.large', + }, + requests: [ + { + path: '/', + bodyAssertions: { + shouldContain: [ + 'public_var::public-test-value', + // two 3000-char vars — verify they survived __VARLOCK_ENV chunking + 'large_var_a_length::3000', + 'large_var_b_length::3000', + 'has_secret::yes', + ], + }, + }, + ], + }); + }); +} diff --git a/framework-tests/frameworks/cloudflare/cloudflare-vite.test.ts b/framework-tests/frameworks/cloudflare/cloudflare-vite.test.ts deleted file mode 100644 index 56b954a8..00000000 --- a/framework-tests/frameworks/cloudflare/cloudflare-vite.test.ts +++ /dev/null @@ -1,149 +0,0 @@ -/* -Tests varlock + cloudflare integration using the cloudflare vite plugin (and our wrapper) -*/ -import { - describe, beforeAll, afterAll, -} from 'vitest'; -import { FrameworkTestEnv } from '../../harness/index'; - -describe('Cloudflare Workers w/ vite plugin', () => { - const cloudflareViteEnv = new FrameworkTestEnv({ - testDir: import.meta.dirname, - framework: 'cloudflare-vite', - packageManager: 'bun', - dependencies: { - varlock: 'will-be-replaced', - '@varlock/cloudflare-integration': 'will-be-replaced', - vite: '^6', - wrangler: '^4', - '@cloudflare/vite-plugin': '^1', - }, - templateFiles: { - '.env.schema': 'schemas/.env.schema', - '.env.dev': 'schemas/.env.dev', - }, - overrides: { - punycode: 'npm:punycode@^2.3.1', - }, - }); - beforeAll(() => cloudflareViteEnv.setup(), 180_000); - afterAll(() => cloudflareViteEnv.teardown()); - - cloudflareViteEnv.describeDevScenario('basic worker', { - command: 'vite dev --port 15173', - readyPattern: /Local:.*http/, - readyTimeout: 30_000, - templateFiles: { - 'src/index.ts': 'workers/basic-worker.ts', - 'vite.config.ts': 'vite-configs/vite.config.ts', - 'wrangler.jsonc': '_base-wrangler/wrangler.jsonc', - 'tsconfig.json': '_base-wrangler/tsconfig.json', - }, - requests: [ - { - path: '/', - bodyAssertions: { - shouldContain: [ - // varlock ENV proxy - non-sensitive - 'public_var::public-test-value', - 'api_url::https://api.example.com', - // varlock ENV proxy - sensitive (accessible but value not leaked) - 'has_sensitive::yes', - // cloudflare native env access - 'native_public_var::public-test-value', - 'native_has_secret::yes', - ], - shouldNotContain: ['super-secret-value'], - }, - }, - ], - outputAssertions: [ - { - description: 'sensitive value is redacted in console output', - shouldContain: ['secret-log-test::'], - shouldNotContain: ['super-secret-value'], - }, - ], - }); - - cloudflareViteEnv.describeDevScenario('leaky worker', { - command: 'vite dev --port 15174', - readyPattern: /Local:.*http/, - readyTimeout: 30_000, - templateFiles: { - 'src/index.ts': 'workers/leaky-worker.ts', - 'vite.config.ts': 'vite-configs/vite.config.ts', - 'wrangler.jsonc': '_base-wrangler/wrangler.jsonc', - 'tsconfig.json': '_base-wrangler/tsconfig.json', - }, - requests: [ - { - path: '/', - expectedStatus: 500, - bodyAssertions: { - shouldNotContain: ['super-secret-value'], - }, - }, - ], - outputAssertions: [ - { - description: 'leak detection message appears', - shouldContain: ['DETECTED LEAKED SENSITIVE CONFIG'], - }, - ], - }); - - cloudflareViteEnv.describeDevScenario('leaky worker (Uint8Array body)', { - command: 'vite dev --port 15176', - readyPattern: /Local:.*http/, - readyTimeout: 30_000, - templateFiles: { - 'src/index.ts': 'workers/leaky-uint8array-worker.ts', - 'vite.config.ts': 'vite-configs/vite.config.ts', - 'wrangler.jsonc': '_base-wrangler/wrangler.jsonc', - 'tsconfig.json': '_base-wrangler/tsconfig.json', - }, - requests: [ - { - path: '/', - expectedStatus: 500, - bodyAssertions: { - shouldNotContain: ['super-secret-value'], - }, - }, - ], - outputAssertions: [ - { - description: 'leak detection message appears for Uint8Array body', - shouldContain: ['DETECTED LEAKED SENSITIVE CONFIG'], - }, - ], - }); - - cloudflareViteEnv.describeDevScenario('large env (chunking)', { - command: 'vite dev --port 15175', - readyPattern: /Local:.*http/, - readyTimeout: 30_000, - templateFiles: { - 'src/index.ts': 'workers/large-env-worker.ts', - 'vite.config.ts': 'vite-configs/vite.config.ts', - 'wrangler.jsonc': '_base-wrangler/wrangler.jsonc', - 'tsconfig.json': '_base-wrangler/tsconfig.json', - '.env.schema': 'schemas/.env.schema.large', - }, - requests: [ - { - path: '/', - bodyAssertions: { - shouldContain: [ - 'public_var::public-test-value', - // two 3000-char vars — verify they survived __VARLOCK_ENV chunking - 'large_var_a_length::3000', - 'large_var_b_length::3000', - 'has_secret::yes', - ], - }, - }, - ], - }); -}); diff --git a/framework-tests/frameworks/cloudflare/cloudflare-vite6.test.ts b/framework-tests/frameworks/cloudflare/cloudflare-vite6.test.ts new file mode 100644 index 00000000..af718399 --- /dev/null +++ b/framework-tests/frameworks/cloudflare/cloudflare-vite6.test.ts @@ -0,0 +1,6 @@ +import { defineCloudflareTests } from './cloudflare-shared'; + +defineCloudflareTests('vite6', import.meta.dirname, { + viteVersion: '^6', + basePort: 15173, +}); diff --git a/framework-tests/frameworks/cloudflare/cloudflare-vite7.test.ts b/framework-tests/frameworks/cloudflare/cloudflare-vite7.test.ts new file mode 100644 index 00000000..8daf1819 --- /dev/null +++ b/framework-tests/frameworks/cloudflare/cloudflare-vite7.test.ts @@ -0,0 +1,6 @@ +import { defineCloudflareTests } from './cloudflare-shared'; + +defineCloudflareTests('vite7', import.meta.dirname, { + viteVersion: '^7', + basePort: 15183, +}); diff --git a/framework-tests/frameworks/cloudflare/cloudflare-vite8.test.ts b/framework-tests/frameworks/cloudflare/cloudflare-vite8.test.ts new file mode 100644 index 00000000..0c36b092 --- /dev/null +++ b/framework-tests/frameworks/cloudflare/cloudflare-vite8.test.ts @@ -0,0 +1,6 @@ +import { defineCloudflareTests } from './cloudflare-shared'; + +defineCloudflareTests('vite8', import.meta.dirname, { + viteVersion: '^8', + basePort: 15193, +}); diff --git a/framework-tests/frameworks/tanstack-start/files/_base/tsconfig.json b/framework-tests/frameworks/tanstack-start/files/_base/tsconfig.json new file mode 100644 index 00000000..795a4ce3 --- /dev/null +++ b/framework-tests/frameworks/tanstack-start/files/_base/tsconfig.json @@ -0,0 +1,16 @@ +{ + "include": ["**/*.ts", "**/*.tsx"], + "compilerOptions": { + "target": "ES2022", + "jsx": "react-jsx", + "module": "ESNext", + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "types": ["vite/client"], + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "noEmit": true, + "skipLibCheck": true, + "strict": true + } +} diff --git a/framework-tests/frameworks/tanstack-start/files/configs/vite.config.cloudflare.ts b/framework-tests/frameworks/tanstack-start/files/configs/vite.config.cloudflare.ts new file mode 100644 index 00000000..88f9e3f5 --- /dev/null +++ b/framework-tests/frameworks/tanstack-start/files/configs/vite.config.cloudflare.ts @@ -0,0 +1,15 @@ +import { defineConfig } from 'vite'; +import { tanstackStart } from '@tanstack/react-start/plugin/vite'; +import { varlockCloudflareVitePlugin } from '@varlock/cloudflare-integration'; +import viteReact from '@vitejs/plugin-react'; + +export default defineConfig({ + plugins: [ + varlockCloudflareVitePlugin({ + inspectorPort: false, + viteEnvironment: { name: 'ssr' }, + }), + tanstackStart(), + viteReact(), + ], +}); diff --git a/framework-tests/frameworks/tanstack-start/files/configs/vite.config.node.ts b/framework-tests/frameworks/tanstack-start/files/configs/vite.config.node.ts new file mode 100644 index 00000000..09de2591 --- /dev/null +++ b/framework-tests/frameworks/tanstack-start/files/configs/vite.config.node.ts @@ -0,0 +1,12 @@ +import { defineConfig } from 'vite'; +import { tanstackStart } from '@tanstack/react-start/plugin/vite'; +import { varlockVitePlugin } from '@varlock/vite-integration'; +import viteReact from '@vitejs/plugin-react'; + +export default defineConfig({ + plugins: [ + varlockVitePlugin(), + tanstackStart(), + viteReact(), + ], +}); diff --git a/framework-tests/frameworks/tanstack-start/files/configs/wrangler.jsonc b/framework-tests/frameworks/tanstack-start/files/configs/wrangler.jsonc new file mode 100644 index 00000000..839ed7fd --- /dev/null +++ b/framework-tests/frameworks/tanstack-start/files/configs/wrangler.jsonc @@ -0,0 +1,6 @@ +{ + "name": "test-tanstack-cf", + "compatibility_date": "2025-04-01", + "compatibility_flags": ["nodejs_compat"], + "main": "@tanstack/react-start/server-entry" +} diff --git a/framework-tests/frameworks/tanstack-start/files/routes/__root.tsx b/framework-tests/frameworks/tanstack-start/files/routes/__root.tsx new file mode 100644 index 00000000..d4053dbe --- /dev/null +++ b/framework-tests/frameworks/tanstack-start/files/routes/__root.tsx @@ -0,0 +1,5 @@ +import { createRootRoute, Outlet } from '@tanstack/react-router'; + +export const Route = createRootRoute({ + component: () => , +}); diff --git a/framework-tests/frameworks/tanstack-start/files/routes/index.tsx b/framework-tests/frameworks/tanstack-start/files/routes/index.tsx new file mode 100644 index 00000000..72b8129a --- /dev/null +++ b/framework-tests/frameworks/tanstack-start/files/routes/index.tsx @@ -0,0 +1,32 @@ +/* eslint-disable no-use-before-define */ +import { createFileRoute } from '@tanstack/react-router'; +import { createServerFn } from '@tanstack/react-start'; +import { ENV } from 'varlock/env'; + +const getServerEnvData = createServerFn({ method: 'GET' }).handler(() => { + // Log a sensitive value to test console redaction + console.log('secret-log-test::', ENV.SECRET_KEY); + return { + public_var: ENV.PUBLIC_VAR, + api_url: ENV.API_URL, + has_sensitive: ENV.SECRET_KEY ? 'yes' : 'no', + }; +}); + +function IndexPage() { + const data = Route.useLoaderData(); + return ( +
+
+ {`public_var::${data.public_var}`} + {`\napi_url::${data.api_url}`} + {`\nhas_sensitive::${data.has_sensitive}`} +
+
+ ); +} + +export const Route = createFileRoute('/')({ + loader: () => getServerEnvData(), + component: IndexPage, +}); diff --git a/framework-tests/frameworks/tanstack-start/files/routes/router.tsx b/framework-tests/frameworks/tanstack-start/files/routes/router.tsx new file mode 100644 index 00000000..5ba988e9 --- /dev/null +++ b/framework-tests/frameworks/tanstack-start/files/routes/router.tsx @@ -0,0 +1,16 @@ +import { createRouter as createTanStackRouter } from '@tanstack/react-router'; +import { routeTree } from './routeTree.gen'; + +export function getRouter() { + const router = createTanStackRouter({ + routeTree, + scrollRestoration: true, + }); + return router; +} + +declare module '@tanstack/react-router' { + interface Register { + router: ReturnType + } +} diff --git a/framework-tests/frameworks/tanstack-start/files/schemas/.env.dev b/framework-tests/frameworks/tanstack-start/files/schemas/.env.dev new file mode 100644 index 00000000..cdb0a7f3 --- /dev/null +++ b/framework-tests/frameworks/tanstack-start/files/schemas/.env.dev @@ -0,0 +1 @@ +APP_ENV=dev diff --git a/framework-tests/frameworks/tanstack-start/files/schemas/.env.schema b/framework-tests/frameworks/tanstack-start/files/schemas/.env.schema new file mode 100644 index 00000000..f5a1f8b2 --- /dev/null +++ b/framework-tests/frameworks/tanstack-start/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/tanstack-start/tanstack-shared.ts b/framework-tests/frameworks/tanstack-start/tanstack-shared.ts new file mode 100644 index 00000000..4d45fa84 --- /dev/null +++ b/framework-tests/frameworks/tanstack-start/tanstack-shared.ts @@ -0,0 +1,226 @@ +/* +Shared TanStack Start test definitions, parameterized by Vite version. +*/ +import { + describe, beforeAll, afterAll, +} from 'vitest'; +import { FrameworkTestEnv } from '../../harness/index'; + +const TANSTACK_DEPS = { + '@tanstack/react-router': '^1.169.1', + '@tanstack/react-start': '^1.167.62', + '@tanstack/router-plugin': '^1.167.32', + react: '19.2.4', + 'react-dom': '19.2.4', +}; + +export function defineTanstackTests( + label: string, + testDir: string, + opts: { + viteVersion: string; + reactPluginVersion?: string; + }, +) { + const { viteVersion, reactPluginVersion = '^5' } = opts; + + // ---- Node target (plain vite plugin) ------------------------------------ + describe(`TanStack Start (${label}) — node target`, () => { + const nodeEnv = new FrameworkTestEnv({ + testDir, + framework: `tanstack-start-node-${label}`, + packageManager: 'pnpm', + dependencies: { + varlock: 'will-be-replaced', + '@varlock/vite-integration': 'will-be-replaced', + vite: viteVersion, + '@vitejs/plugin-react': reactPluginVersion, + ...TANSTACK_DEPS, + }, + templateFiles: { + '.env.schema': 'schemas/.env.schema', + '.env.dev': 'schemas/.env.dev', + 'tsconfig.json': '_base/tsconfig.json', + 'src/routes/__root.tsx': 'routes/__root.tsx', + 'src/routes/index.tsx': 'routes/index.tsx', + 'src/router.tsx': 'routes/router.tsx', + }, + }); + beforeAll(() => nodeEnv.setup(), 180_000); + afterAll(() => nodeEnv.teardown()); + + nodeEnv.describeDevScenario('dev server', { + command: 'vite dev --port 15190', + readyPattern: /Local:.*http/, + readyTimeout: 45_000, + templateFiles: { + 'vite.config.ts': 'configs/vite.config.node.ts', + }, + requests: [ + { + path: '/', + bodyAssertions: { + shouldContain: [ + 'public_var::public-test-value', + 'api_url::https://api.example.com', + 'has_sensitive::yes', + ], + shouldNotContain: ['super-secret-value'], + }, + }, + ], + outputAssertions: [ + { + description: 'sensitive value is redacted in console output', + shouldContain: ['secret-log-test::'], + shouldNotContain: ['super-secret-value'], + }, + ], + }); + + nodeEnv.describeScenario('static build', { + command: 'vite build', + templateFiles: { + 'vite.config.ts': 'configs/vite.config.node.ts', + }, + fileAssertions: [ + { + description: 'server bundle does not contain sensitive values', + fileGlob: 'dist/server/**/*.js', + shouldNotContain: ['super-secret-value'], + }, + ], + }); + + nodeEnv.describeDevScenario('build + preview', { + command: 'vite build && vite preview --port 15193', + readyPattern: /Local:.*http/, + readyTimeout: 60_000, + timeout: 120_000, + templateFiles: { + 'vite.config.ts': 'configs/vite.config.node.ts', + }, + requests: [ + { + path: '/', + bodyAssertions: { + shouldContain: [ + 'public_var::public-test-value', + 'api_url::https://api.example.com', + 'has_sensitive::yes', + ], + shouldNotContain: ['super-secret-value'], + }, + }, + ], + }); + }); + + // ---- Cloudflare target -------------------------------------------------- + describe(`TanStack Start (${label}) — cloudflare target`, () => { + const cfEnv = new FrameworkTestEnv({ + testDir, + framework: `tanstack-start-cf-${label}`, + packageManager: 'pnpm', + dependencies: { + varlock: 'will-be-replaced', + '@varlock/cloudflare-integration': 'will-be-replaced', + vite: viteVersion, + '@vitejs/plugin-react': reactPluginVersion, + wrangler: '^4', + '@cloudflare/vite-plugin': '^1.30.0', + ...TANSTACK_DEPS, + }, + overrides: { + punycode: 'npm:punycode@^2.3.1', + }, + templateFiles: { + '.env.schema': 'schemas/.env.schema', + '.env.dev': 'schemas/.env.dev', + 'tsconfig.json': '_base/tsconfig.json', + 'src/routes/__root.tsx': 'routes/__root.tsx', + 'src/routes/index.tsx': 'routes/index.tsx', + 'src/router.tsx': 'routes/router.tsx', + }, + }); + beforeAll(() => cfEnv.setup(), 180_000); + afterAll(() => cfEnv.teardown()); + + cfEnv.describeDevScenario('dev server', { + command: 'vite dev --port 15191', + readyPattern: /Local:.*http/, + readyTimeout: 45_000, + templateFiles: { + 'vite.config.ts': 'configs/vite.config.cloudflare.ts', + 'wrangler.jsonc': 'configs/wrangler.jsonc', + }, + requests: [ + { + path: '/', + bodyAssertions: { + shouldContain: [ + 'public_var::public-test-value', + 'api_url::https://api.example.com', + 'has_sensitive::yes', + ], + shouldNotContain: ['super-secret-value'], + }, + }, + ], + outputAssertions: [ + { + description: 'sensitive value is redacted in console output', + shouldContain: ['secret-log-test::'], + shouldNotContain: ['super-secret-value'], + }, + ], + }); + + cfEnv.describeScenario('cloudflare build', { + command: 'vite build', + templateFiles: { + 'vite.config.ts': 'configs/vite.config.cloudflare.ts', + 'wrangler.jsonc': 'configs/wrangler.jsonc', + }, + fileAssertions: [ + { + description: 'server bundle does not contain sensitive values', + fileGlob: 'dist/server/**/*.js', + shouldNotContain: ['super-secret-value'], + }, + { + description: 'init code is injected only once', + fileGlob: 'dist/server/**/*.js', + shouldMatch: [ + // only one initVarlockEnv() call across all server JS files + /^(?![\s\S]*initVarlockEnv\(\)[\s\S]*initVarlockEnv\(\))[\s\S]*initVarlockEnv\(\)/, + ], + }, + ], + }); + + cfEnv.describeDevScenario('build + preview', { + command: 'vite build && vite preview --port 15192', + readyPattern: /Local:.*http/, + readyTimeout: 60_000, + timeout: 120_000, + templateFiles: { + 'vite.config.ts': 'configs/vite.config.cloudflare.ts', + 'wrangler.jsonc': 'configs/wrangler.jsonc', + }, + requests: [ + { + path: '/', + bodyAssertions: { + shouldContain: [ + 'public_var::public-test-value', + 'api_url::https://api.example.com', + 'has_sensitive::yes', + ], + shouldNotContain: ['super-secret-value'], + }, + }, + ], + }); + }); +} diff --git a/framework-tests/frameworks/tanstack-start/tanstack-vite6.test.ts b/framework-tests/frameworks/tanstack-start/tanstack-vite6.test.ts new file mode 100644 index 00000000..d49c9a22 --- /dev/null +++ b/framework-tests/frameworks/tanstack-start/tanstack-vite6.test.ts @@ -0,0 +1,5 @@ +import { defineTanstackTests } from './tanstack-shared'; + +defineTanstackTests('vite6', import.meta.dirname, { + viteVersion: '^6', +}); diff --git a/framework-tests/frameworks/tanstack-start/tanstack-vite7.test.ts b/framework-tests/frameworks/tanstack-start/tanstack-vite7.test.ts new file mode 100644 index 00000000..962d4a22 --- /dev/null +++ b/framework-tests/frameworks/tanstack-start/tanstack-vite7.test.ts @@ -0,0 +1,5 @@ +import { defineTanstackTests } from './tanstack-shared'; + +defineTanstackTests('vite7', import.meta.dirname, { + viteVersion: '^7', +}); diff --git a/framework-tests/frameworks/tanstack-start/tanstack-vite8.test.ts b/framework-tests/frameworks/tanstack-start/tanstack-vite8.test.ts new file mode 100644 index 00000000..99b5873a --- /dev/null +++ b/framework-tests/frameworks/tanstack-start/tanstack-vite8.test.ts @@ -0,0 +1,6 @@ +import { defineTanstackTests } from './tanstack-shared'; + +defineTanstackTests('vite8', import.meta.dirname, { + viteVersion: '^8', + reactPluginVersion: '^6', +}); diff --git a/framework-tests/frameworks/vite/vite-shared.ts b/framework-tests/frameworks/vite/vite-shared.ts new file mode 100644 index 00000000..011bcd24 --- /dev/null +++ b/framework-tests/frameworks/vite/vite-shared.ts @@ -0,0 +1,537 @@ +/* +Shared Vite test definitions, parameterized by Vite version. +Covers static builds, HTML constant replacement, leak detection, +log redaction, sourcemap scrubbing, SSR init injection, and dev server. +*/ +import { readFileSync } from 'node:fs'; +import { join } from 'node:path'; +import { + describe, beforeAll, afterAll, +} from 'vitest'; +import { FrameworkTestEnv } from '../../harness/index'; + +export function defineViteTests( + label: string, + testDir: string, + opts: { + viteVersion: string; + /** Base port — each dev scenario offsets from this */ + basePort: number; + }, +) { + const { viteVersion, basePort } = opts; + + describe(`Vite (${label})`, () => { + const viteEnv = new FrameworkTestEnv({ + testDir, + framework: `vite-${label}`, + packageManager: 'pnpm', + dependencies: { + vite: viteVersion, + varlock: 'will-be-replaced', + '@varlock/vite-integration': 'will-be-replaced', + }, + templateFiles: { + '.env.schema': 'schemas/.env.schema', + '.env.dev': 'schemas/.env.dev', + '.env.prod': 'schemas/.env.prod', + }, + }); + + beforeAll(() => viteEnv.setup(), 180_000); + afterAll(() => viteEnv.teardown()); + + // ---- Static SPA build ---- + + describe('static build', () => { + viteEnv.describeScenario('basic page with public vars', { + command: 'vite build', + templateFiles: { + 'vite.config.ts': 'vite-configs/vite.config.ts', + 'index.html': 'html/basic.html', + 'src/main.ts': 'pages/basic-page.ts', + }, + fileAssertions: [ + { + description: 'public env vars are statically replaced in JS output', + fileGlob: 'dist/assets/*.js', + shouldContain: [ + 'public-test-value', + 'https://api.example.com', + 'env-specific-dev', + ], + shouldNotContain: [ + 'super-secret-value', + 'env-specific-default', + ], + }, + { + description: 'HTML entry is present in output', + filePath: 'dist/index.html', + shouldContain: ['Varlock Vite Test'], + }, + ], + }); + + viteEnv.describeScenario('env-specific vars use correct environment (dev)', { + command: 'vite build', + templateFiles: { + 'vite.config.ts': 'vite-configs/vite.config.ts', + 'index.html': 'html/basic.html', + 'src/main.ts': 'pages/basic-page.ts', + }, + fileAssertions: [ + { + description: 'dev-specific value is present (APP_ENV=dev)', + fileGlob: 'dist/assets/*.js', + shouldContain: ['env-specific-dev'], + shouldNotContain: ['env-specific-prod', 'env-specific-default'], + }, + ], + }); + + viteEnv.describeScenario('env-specific vars use prod environment', { + command: 'vite build', + env: { APP_ENV: 'prod' }, + templateFiles: { + 'vite.config.ts': 'vite-configs/vite.config.ts', + 'index.html': 'html/basic.html', + 'src/main.ts': 'pages/basic-page.ts', + }, + fileAssertions: [ + { + description: 'prod-specific value is present (APP_ENV=prod)', + fileGlob: 'dist/assets/*.js', + shouldContain: ['env-specific-prod'], + shouldNotContain: ['env-specific-dev', 'env-specific-default'], + }, + ], + }); + + viteEnv.describeScenario('sensitive var not inlined in client code', { + command: 'vite build', + templateFiles: { + 'vite.config.ts': 'vite-configs/vite.config.ts', + 'index.html': 'html/basic.html', + 'src/main.ts': 'pages/sensitive-ref-page.ts', + }, + fileAssertions: [ + { + description: 'sensitive value is absent from client JS', + fileGlob: 'dist/assets/*.js', + shouldNotContain: ['super-secret-value'], + }, + { + description: 'public var is still replaced', + fileGlob: 'dist/assets/*.js', + shouldContain: ['public-test-value'], + }, + ], + }); + }); + + // ---- HTML constant replacement ---- + + describe('HTML constant replacement', () => { + viteEnv.describeScenario('public vars replaced in HTML via %ENV.x%', { + command: 'vite build', + templateFiles: { + 'vite.config.ts': 'vite-configs/vite.config.ts', + 'index.html': 'html/html-replacement.html', + 'src/main.ts': 'pages/minimal-page.ts', + }, + fileAssertions: [ + { + description: 'HTML has public var values in place of %ENV.x% placeholders', + filePath: 'dist/index.html', + shouldContain: [ + 'public-test-value', + 'https://api.example.com', + 'env-specific-dev', + ], + shouldNotContain: [ + '%ENV.', + 'super-secret-value', + ], + }, + ], + }); + + viteEnv.describeScenario('sensitive var in HTML causes build failure', { + command: 'vite build', + expectSuccess: false, + templateFiles: { + 'vite.config.ts': 'vite-configs/vite.config.ts', + 'index.html': 'html/leaky-html.html', + 'src/main.ts': 'pages/minimal-page.ts', + }, + outputAssertions: [ + { + description: 'error mentions sensitive config item', + shouldContain: ['SECRET_KEY', 'sensitive'], + }, + ], + }); + }); + + // ---- Sourcemap scrubbing ---- + + describe('sourcemaps', () => { + viteEnv.describeScenario('secrets are not present in sourcemaps', { + command: 'vite build', + templateFiles: { + 'vite.config.ts': 'vite-configs/vite.config.sourcemaps.ts', + 'index.html': 'html/basic.html', + 'src/main.ts': 'pages/basic-page.ts', + }, + fileAssertions: [ + { + description: 'sourcemaps do not contain the sensitive value', + fileGlob: 'dist/assets/*.js.map', + shouldNotContain: ['super-secret-value'], + }, + { + description: 'JS output still has public vars', + fileGlob: 'dist/assets/*.js', + shouldContain: ['public-test-value'], + }, + ], + }); + }); + + // ---- Log redaction during build ---- + + describe('log redaction', () => { + viteEnv.describeScenario('sensitive value redacted from build stdout', { + command: 'vite build', + templateFiles: { + 'vite.config.ts': 'vite-configs/vite.config.log-test.ts', + 'index.html': 'html/basic.html', + 'src/main.ts': 'pages/minimal-page.ts', + }, + outputAssertions: [ + { + description: 'log line is present but secret value is redacted', + shouldContain: ['secret-log-test:'], + shouldNotContain: ['super-secret-value'], + }, + ], + }); + }); + + // ---- SSR build ---- + + describe('SSR build', () => { + viteEnv.describeScenario('SSR entry receives init code injection', { + command: 'vite build --ssr src/ssr-entry.ts', + templateFiles: { + 'vite.config.ts': 'vite-configs/vite.config.ts', + 'index.html': 'html/basic.html', + 'src/ssr-entry.ts': 'pages/ssr-entry.ts', + }, + fileAssertions: [ + { + description: 'SSR output contains varlock init calls', + fileGlob: 'dist/*.js', + shouldContain: [ + 'initVarlockEnv', + 'patchGlobalConsole', + 'patchGlobalResponse', + ], + }, + { + description: 'public vars are replaced in SSR output', + fileGlob: 'dist/*.js', + shouldContain: [ + 'public-test-value', + 'https://api.example.com', + ], + }, + { + description: 'sensitive value is not present in SSR output', + fileGlob: 'dist/*.js', + shouldNotContain: ['super-secret-value'], + }, + ], + }); + }); + + // ---- Dev server ---- + + describe('dev server', () => { + viteEnv.describeDevScenario('serves HTML with env replacements and transformed JS', { + command: `vite dev --port ${basePort}`, + readyPattern: /Local:.*http/, + readyTimeout: 30_000, + templateFiles: { + 'vite.config.ts': 'vite-configs/vite.config.ts', + 'index.html': 'html/html-replacement.html', + 'src/main.ts': 'pages/basic-page.ts', + }, + requests: [ + { + path: '/', + bodyAssertions: { + shouldContain: [ + 'public-test-value', + 'https://api.example.com', + 'env-specific-dev', + ], + shouldNotContain: ['super-secret-value'], + }, + }, + { + path: '/src/main.ts', + bodyAssertions: { + shouldContain: ['public-test-value'], + shouldNotContain: ['super-secret-value'], + }, + }, + ], + }); + + viteEnv.describeDevScenario('env reload on .env file change', { + command: `vite dev --port ${basePort + 1}`, + readyPattern: /Local:.*http/, + readyTimeout: 30_000, + templateFiles: { + 'vite.config.ts': 'vite-configs/vite.config.ts', + 'index.html': 'html/html-replacement.html', + 'src/main.ts': 'pages/basic-page.ts', + }, + requests: [ + { + path: '/', + bodyAssertions: { + shouldContain: ['env-specific-dev'], + }, + }, + { + path: '/', + fileEdits: { + '.env.dev': 'ENV_SPECIFIC_VAR=env-specific-changed\n', + }, + // Vite reloads config in-place without restarting the server, + // so the readyPattern never re-appears — use a fixed delay instead + fileEditDelay: 3_000, + bodyAssertions: { + shouldContain: ['env-specific-changed'], + }, + }, + ], + }); + + viteEnv.describeDevScenario('log redaction in dev mode', { + command: `vite dev --port ${basePort + 2}`, + readyPattern: /Local:.*http/, + readyTimeout: 30_000, + templateFiles: { + 'vite.config.ts': 'vite-configs/vite.config.log-test.ts', + 'index.html': 'html/basic.html', + 'src/main.ts': 'pages/minimal-page.ts', + }, + requests: [ + { + path: '/', + bodyAssertions: { + shouldContain: ['Varlock Vite Test'], + }, + }, + ], + outputAssertions: [ + { + description: 'sensitive value is redacted in dev server output', + shouldContain: ['secret-log-test:'], + shouldNotContain: ['super-secret-value'], + }, + ], + }); + + viteEnv.describeDevScenario('source code hot-reload', { + command: `vite dev --port ${basePort + 3}`, + readyPattern: /Local:.*http/, + readyTimeout: 30_000, + templateFiles: { + 'vite.config.ts': 'vite-configs/vite.config.ts', + 'index.html': 'html/basic.html', + 'src/main.ts': 'pages/basic-page.ts', + }, + requests: [ + { + path: '/src/main.ts', + bodyAssertions: { + shouldContain: ['public-test-value'], + shouldNotContain: ['hot-reload-success'], + }, + }, + { + path: '/src/main.ts', + fileEdits: { + 'src/main.ts': readFileSync(join(testDir, 'files/pages/updated-basic-page.ts'), 'utf-8'), + }, + // HMR doesn't restart the server — use a fixed delay + fileEditDelay: 2_000, + bodyAssertions: { + shouldContain: ['public-test-value', 'hot-reload-success'], + }, + }, + ], + }); + }); + + // ---- Leak detection ---- + + describe('leak detection', () => { + viteEnv.describeDevScenario('safe endpoint serves public values', { + command: `vite dev --port ${basePort + 4}`, + readyPattern: /Local:.*http/, + readyTimeout: 30_000, + templateFiles: { + 'vite.config.ts': 'vite-configs/vite.config.leaky-middleware.ts', + 'index.html': 'html/basic.html', + 'src/main.ts': 'pages/minimal-page.ts', + }, + requests: [ + { + path: '/api/safe', + bodyAssertions: { + shouldContain: ['public: public-test-value'], + shouldNotContain: ['super-secret-value'], + }, + }, + ], + }); + + viteEnv.describeDevScenario('leaky endpoint triggers leak detection', { + command: `vite dev --port ${basePort + 5}`, + readyPattern: /Local:.*http/, + readyTimeout: 30_000, + templateFiles: { + 'vite.config.ts': 'vite-configs/vite.config.leaky-middleware.ts', + 'index.html': 'html/basic.html', + 'src/main.ts': 'pages/minimal-page.ts', + }, + requests: [ + { + path: '/api/leak', + expectedStatus: 500, + bodyAssertions: { + shouldNotContain: ['super-secret-value'], + }, + }, + ], + outputAssertions: [ + { + description: 'leak detection message appears', + shouldContain: ['DETECTED LEAKED SENSITIVE CONFIG'], + }, + ], + }); + }); + + // ---- Non-existent config keys ---- + + describe('non-existent config keys', () => { + viteEnv.describeScenario('non-existent key is not replaced in build output', { + command: 'vite build', + templateFiles: { + 'vite.config.ts': 'vite-configs/vite.config.ts', + 'index.html': 'html/basic.html', + 'src/main.ts': 'pages/nonexistent-key-page.ts', + }, + fileAssertions: [ + { + description: 'public var is still replaced', + fileGlob: 'dist/assets/*.js', + shouldContain: ['public-test-value'], + }, + { + description: 'non-existent key reference is not replaced with a real value', + fileGlob: 'dist/assets/*.js', + shouldNotContain: ['DOES_NOT_EXIST_VALUE'], + }, + ], + }); + + viteEnv.describeDevScenario('non-existent key is not replaced in dev server output', { + command: `vite dev --port ${basePort + 6}`, + readyPattern: /Local:.*http/, + readyTimeout: 30_000, + templateFiles: { + 'vite.config.ts': 'vite-configs/vite.config.ts', + 'index.html': 'html/basic.html', + 'src/main.ts': 'pages/nonexistent-key-page.ts', + }, + requests: [ + { + path: '/src/main.ts', + bodyAssertions: { + shouldContain: ['public-test-value', 'DOES_NOT_EXIST'], + shouldNotContain: ['DOES_NOT_EXIST_VALUE'], + }, + }, + ], + }); + }); + + // ---- Invalid config handling ---- + + describe('invalid config', () => { + viteEnv.describeScenario('invalid schema causes build failure', { + command: 'vite build', + expectSuccess: false, + templateFiles: { + 'vite.config.ts': 'vite-configs/vite.config.ts', + 'index.html': 'html/basic.html', + 'src/main.ts': 'pages/minimal-page.ts', + '.env.schema': 'schemas/.env.schema.invalid', + }, + outputAssertions: [ + { + description: 'build output indicates config validation failure', + shouldContain: ['Varlock config validation failed', 'MISSING_REQUIRED_VAR'], + }, + ], + }); + + viteEnv.describeDevScenario('invalid schema shows error page then recovers on fix', { + command: `vite dev --port ${basePort + 7}`, + readyPattern: /Local:.*http/, + readyTimeout: 30_000, + templateFiles: { + 'vite.config.ts': 'vite-configs/vite.config.ts', + 'index.html': 'html/html-replacement.html', + 'src/main.ts': 'pages/basic-page.ts', + '.env.schema': 'schemas/.env.schema.invalid', + }, + requests: [ + { + path: '/', + bodyAssertions: { + shouldContain: ['invalid'], + shouldNotContain: ['public-test-value'], + }, + }, + { + path: '/', + fileEdits: { + '.env.schema': readFileSync(join(testDir, 'files/schemas/.env.schema'), 'utf-8'), + }, + // Config reload after fixing .env.schema — Vite doesn't restart + fileEditDelay: 3_000, + bodyAssertions: { + shouldContain: ['public-test-value'], + shouldNotContain: ['invalid'], + }, + }, + ], + outputAssertions: [ + { + description: 'validation error details are shown in terminal', + shouldContain: ['MISSING_REQUIRED_VAR'], + }, + ], + }); + }); + }); +} diff --git a/framework-tests/frameworks/vite/vite-v5.test.ts b/framework-tests/frameworks/vite/vite-v5.test.ts new file mode 100644 index 00000000..cbe45116 --- /dev/null +++ b/framework-tests/frameworks/vite/vite-v5.test.ts @@ -0,0 +1,6 @@ +import { defineViteTests } from './vite-shared'; + +defineViteTests('vite5', import.meta.dirname, { + viteVersion: '^5', + basePort: 15200, +}); diff --git a/framework-tests/frameworks/vite/vite-v6.test.ts b/framework-tests/frameworks/vite/vite-v6.test.ts new file mode 100644 index 00000000..0a927c02 --- /dev/null +++ b/framework-tests/frameworks/vite/vite-v6.test.ts @@ -0,0 +1,6 @@ +import { defineViteTests } from './vite-shared'; + +defineViteTests('vite6', import.meta.dirname, { + viteVersion: '^6', + basePort: 15210, +}); diff --git a/framework-tests/frameworks/vite/vite-v7.test.ts b/framework-tests/frameworks/vite/vite-v7.test.ts new file mode 100644 index 00000000..e5196fae --- /dev/null +++ b/framework-tests/frameworks/vite/vite-v7.test.ts @@ -0,0 +1,6 @@ +import { defineViteTests } from './vite-shared'; + +defineViteTests('vite7', import.meta.dirname, { + viteVersion: '^7', + basePort: 15230, +}); diff --git a/framework-tests/frameworks/vite/vite-v8.test.ts b/framework-tests/frameworks/vite/vite-v8.test.ts new file mode 100644 index 00000000..0cfcabff --- /dev/null +++ b/framework-tests/frameworks/vite/vite-v8.test.ts @@ -0,0 +1,6 @@ +import { defineViteTests } from './vite-shared'; + +defineViteTests('vite8', import.meta.dirname, { + viteVersion: '^8', + basePort: 15220, +}); diff --git a/framework-tests/frameworks/vite/vite.test.ts b/framework-tests/frameworks/vite/vite.test.ts deleted file mode 100644 index 99e15576..00000000 --- a/framework-tests/frameworks/vite/vite.test.ts +++ /dev/null @@ -1,525 +0,0 @@ -/* -Tests varlock + vite integration (plain Vite SPA and SSR builds). -Covers static builds, HTML constant replacement, leak detection, -log redaction, sourcemap scrubbing, SSR init injection, and dev server. -*/ -import { readFileSync } from 'node:fs'; -import { join } from 'node:path'; -import { - describe, beforeAll, afterAll, -} from 'vitest'; -import { FrameworkTestEnv } from '../../harness/index'; - -describe('Vite', () => { - const viteEnv = new FrameworkTestEnv({ - testDir: import.meta.dirname, - framework: 'vite', - packageManager: 'pnpm', - dependencies: { - vite: '^6', - varlock: 'will-be-replaced', - '@varlock/vite-integration': 'will-be-replaced', - }, - templateFiles: { - '.env.schema': 'schemas/.env.schema', - '.env.dev': 'schemas/.env.dev', - '.env.prod': 'schemas/.env.prod', - }, - }); - - beforeAll(() => viteEnv.setup(), 180_000); - afterAll(() => viteEnv.teardown()); - - // ---- Static SPA build ---- - - describe('static build', () => { - viteEnv.describeScenario('basic page with public vars', { - command: 'vite build', - templateFiles: { - 'vite.config.ts': 'vite-configs/vite.config.ts', - 'index.html': 'html/basic.html', - 'src/main.ts': 'pages/basic-page.ts', - }, - fileAssertions: [ - { - description: 'public env vars are statically replaced in JS output', - fileGlob: 'dist/assets/*.js', - shouldContain: [ - 'public-test-value', - 'https://api.example.com', - 'env-specific-dev', - ], - shouldNotContain: [ - 'super-secret-value', - 'env-specific-default', - ], - }, - { - description: 'HTML entry is present in output', - filePath: 'dist/index.html', - shouldContain: ['Varlock Vite Test'], - }, - ], - }); - - viteEnv.describeScenario('env-specific vars use correct environment (dev)', { - command: 'vite build', - templateFiles: { - 'vite.config.ts': 'vite-configs/vite.config.ts', - 'index.html': 'html/basic.html', - 'src/main.ts': 'pages/basic-page.ts', - }, - fileAssertions: [ - { - description: 'dev-specific value is present (APP_ENV=dev)', - fileGlob: 'dist/assets/*.js', - shouldContain: ['env-specific-dev'], - shouldNotContain: ['env-specific-prod', 'env-specific-default'], - }, - ], - }); - - viteEnv.describeScenario('env-specific vars use prod environment', { - command: 'vite build', - env: { APP_ENV: 'prod' }, - templateFiles: { - 'vite.config.ts': 'vite-configs/vite.config.ts', - 'index.html': 'html/basic.html', - 'src/main.ts': 'pages/basic-page.ts', - }, - fileAssertions: [ - { - description: 'prod-specific value is present (APP_ENV=prod)', - fileGlob: 'dist/assets/*.js', - shouldContain: ['env-specific-prod'], - shouldNotContain: ['env-specific-dev', 'env-specific-default'], - }, - ], - }); - - viteEnv.describeScenario('sensitive var not inlined in client code', { - command: 'vite build', - templateFiles: { - 'vite.config.ts': 'vite-configs/vite.config.ts', - 'index.html': 'html/basic.html', - 'src/main.ts': 'pages/sensitive-ref-page.ts', - }, - fileAssertions: [ - { - description: 'sensitive value is absent from client JS', - fileGlob: 'dist/assets/*.js', - shouldNotContain: ['super-secret-value'], - }, - { - description: 'public var is still replaced', - fileGlob: 'dist/assets/*.js', - shouldContain: ['public-test-value'], - }, - ], - }); - }); - - // ---- HTML constant replacement ---- - - describe('HTML constant replacement', () => { - viteEnv.describeScenario('public vars replaced in HTML via %ENV.x%', { - command: 'vite build', - templateFiles: { - 'vite.config.ts': 'vite-configs/vite.config.ts', - 'index.html': 'html/html-replacement.html', - 'src/main.ts': 'pages/minimal-page.ts', - }, - fileAssertions: [ - { - description: 'HTML has public var values in place of %ENV.x% placeholders', - filePath: 'dist/index.html', - shouldContain: [ - 'public-test-value', - 'https://api.example.com', - 'env-specific-dev', - ], - shouldNotContain: [ - '%ENV.', - 'super-secret-value', - ], - }, - ], - }); - - viteEnv.describeScenario('sensitive var in HTML causes build failure', { - command: 'vite build', - expectSuccess: false, - templateFiles: { - 'vite.config.ts': 'vite-configs/vite.config.ts', - 'index.html': 'html/leaky-html.html', - 'src/main.ts': 'pages/minimal-page.ts', - }, - outputAssertions: [ - { - description: 'error mentions sensitive config item', - shouldContain: ['SECRET_KEY', 'sensitive'], - }, - ], - }); - }); - - // ---- Sourcemap scrubbing ---- - - describe('sourcemaps', () => { - viteEnv.describeScenario('secrets are not present in sourcemaps', { - command: 'vite build', - templateFiles: { - 'vite.config.ts': 'vite-configs/vite.config.sourcemaps.ts', - 'index.html': 'html/basic.html', - 'src/main.ts': 'pages/basic-page.ts', - }, - fileAssertions: [ - { - description: 'sourcemaps do not contain the sensitive value', - fileGlob: 'dist/assets/*.js.map', - shouldNotContain: ['super-secret-value'], - }, - { - description: 'JS output still has public vars', - fileGlob: 'dist/assets/*.js', - shouldContain: ['public-test-value'], - }, - ], - }); - }); - - // ---- Log redaction during build ---- - - describe('log redaction', () => { - viteEnv.describeScenario('sensitive value redacted from build stdout', { - command: 'vite build', - templateFiles: { - 'vite.config.ts': 'vite-configs/vite.config.log-test.ts', - 'index.html': 'html/basic.html', - 'src/main.ts': 'pages/minimal-page.ts', - }, - outputAssertions: [ - { - description: 'log line is present but secret value is redacted', - shouldContain: ['secret-log-test:'], - shouldNotContain: ['super-secret-value'], - }, - ], - }); - }); - - // ---- SSR build ---- - - describe('SSR build', () => { - viteEnv.describeScenario('SSR entry receives init code injection', { - command: 'vite build --ssr src/ssr-entry.ts', - templateFiles: { - 'vite.config.ts': 'vite-configs/vite.config.ts', - 'index.html': 'html/basic.html', - 'src/ssr-entry.ts': 'pages/ssr-entry.ts', - }, - fileAssertions: [ - { - description: 'SSR output contains varlock init calls', - fileGlob: 'dist/*.js', - shouldContain: [ - 'initVarlockEnv', - 'patchGlobalConsole', - 'patchGlobalResponse', - ], - }, - { - description: 'public vars are replaced in SSR output', - fileGlob: 'dist/*.js', - shouldContain: [ - 'public-test-value', - 'https://api.example.com', - ], - }, - { - description: 'sensitive value is not present in SSR output', - fileGlob: 'dist/*.js', - shouldNotContain: ['super-secret-value'], - }, - ], - }); - }); - - // ---- Dev server ---- - - describe('dev server', () => { - viteEnv.describeDevScenario('serves HTML with env replacements and transformed JS', { - command: 'vite dev --port 15180', - readyPattern: /Local:.*http/, - readyTimeout: 30_000, - templateFiles: { - 'vite.config.ts': 'vite-configs/vite.config.ts', - 'index.html': 'html/html-replacement.html', - 'src/main.ts': 'pages/basic-page.ts', - }, - requests: [ - { - path: '/', - bodyAssertions: { - shouldContain: [ - 'public-test-value', - 'https://api.example.com', - 'env-specific-dev', - ], - shouldNotContain: ['super-secret-value'], - }, - }, - { - path: '/src/main.ts', - bodyAssertions: { - shouldContain: ['public-test-value'], - shouldNotContain: ['super-secret-value'], - }, - }, - ], - }); - - viteEnv.describeDevScenario('env reload on .env file change', { - command: 'vite dev --port 15181', - readyPattern: /Local:.*http/, - readyTimeout: 30_000, - templateFiles: { - 'vite.config.ts': 'vite-configs/vite.config.ts', - 'index.html': 'html/html-replacement.html', - 'src/main.ts': 'pages/basic-page.ts', - }, - requests: [ - { - path: '/', - bodyAssertions: { - shouldContain: ['env-specific-dev'], - }, - }, - { - path: '/', - fileEdits: { - '.env.dev': 'ENV_SPECIFIC_VAR=env-specific-changed\n', - }, - // Vite reloads config in-place without restarting the server, - // so the readyPattern never re-appears — use a fixed delay instead - fileEditDelay: 3_000, - bodyAssertions: { - shouldContain: ['env-specific-changed'], - }, - }, - ], - }); - - viteEnv.describeDevScenario('log redaction in dev mode', { - command: 'vite dev --port 15182', - readyPattern: /Local:.*http/, - readyTimeout: 30_000, - templateFiles: { - 'vite.config.ts': 'vite-configs/vite.config.log-test.ts', - 'index.html': 'html/basic.html', - 'src/main.ts': 'pages/minimal-page.ts', - }, - requests: [ - { - path: '/', - bodyAssertions: { - shouldContain: ['Varlock Vite Test'], - }, - }, - ], - outputAssertions: [ - { - description: 'sensitive value is redacted in dev server output', - shouldContain: ['secret-log-test:'], - shouldNotContain: ['super-secret-value'], - }, - ], - }); - - viteEnv.describeDevScenario('source code hot-reload', { - command: 'vite dev --port 15183', - readyPattern: /Local:.*http/, - readyTimeout: 30_000, - templateFiles: { - 'vite.config.ts': 'vite-configs/vite.config.ts', - 'index.html': 'html/basic.html', - 'src/main.ts': 'pages/basic-page.ts', - }, - requests: [ - { - path: '/src/main.ts', - bodyAssertions: { - shouldContain: ['public-test-value'], - shouldNotContain: ['hot-reload-success'], - }, - }, - { - path: '/src/main.ts', - fileEdits: { - 'src/main.ts': readFileSync(join(import.meta.dirname, 'files/pages/updated-basic-page.ts'), 'utf-8'), - }, - // HMR doesn't restart the server — use a fixed delay - fileEditDelay: 2_000, - bodyAssertions: { - shouldContain: ['public-test-value', 'hot-reload-success'], - }, - }, - ], - }); - }); - - // ---- Leak detection ---- - - describe('leak detection', () => { - viteEnv.describeDevScenario('safe endpoint serves public values', { - command: 'vite dev --port 15184', - readyPattern: /Local:.*http/, - readyTimeout: 30_000, - templateFiles: { - 'vite.config.ts': 'vite-configs/vite.config.leaky-middleware.ts', - 'index.html': 'html/basic.html', - 'src/main.ts': 'pages/minimal-page.ts', - }, - requests: [ - { - path: '/api/safe', - bodyAssertions: { - shouldContain: ['public: public-test-value'], - shouldNotContain: ['super-secret-value'], - }, - }, - ], - }); - - viteEnv.describeDevScenario('leaky endpoint triggers leak detection', { - command: 'vite dev --port 15185', - readyPattern: /Local:.*http/, - readyTimeout: 30_000, - templateFiles: { - 'vite.config.ts': 'vite-configs/vite.config.leaky-middleware.ts', - 'index.html': 'html/basic.html', - 'src/main.ts': 'pages/minimal-page.ts', - }, - requests: [ - { - path: '/api/leak', - expectedStatus: 500, - bodyAssertions: { - shouldNotContain: ['super-secret-value'], - }, - }, - ], - outputAssertions: [ - { - description: 'leak detection message appears', - shouldContain: ['DETECTED LEAKED SENSITIVE CONFIG'], - }, - ], - }); - }); - - // ---- Non-existent config keys ---- - - describe('non-existent config keys', () => { - viteEnv.describeScenario('non-existent key is not replaced in build output', { - command: 'vite build', - templateFiles: { - 'vite.config.ts': 'vite-configs/vite.config.ts', - 'index.html': 'html/basic.html', - 'src/main.ts': 'pages/nonexistent-key-page.ts', - }, - fileAssertions: [ - { - description: 'public var is still replaced', - fileGlob: 'dist/assets/*.js', - shouldContain: ['public-test-value'], - }, - { - description: 'non-existent key reference is not replaced with a real value', - fileGlob: 'dist/assets/*.js', - shouldNotContain: ['DOES_NOT_EXIST_VALUE'], - }, - ], - }); - - viteEnv.describeDevScenario('non-existent key is not replaced in dev server output', { - command: 'vite dev --port 15186', - readyPattern: /Local:.*http/, - readyTimeout: 30_000, - templateFiles: { - 'vite.config.ts': 'vite-configs/vite.config.ts', - 'index.html': 'html/basic.html', - 'src/main.ts': 'pages/nonexistent-key-page.ts', - }, - requests: [ - { - path: '/src/main.ts', - bodyAssertions: { - shouldContain: ['public-test-value', 'DOES_NOT_EXIST'], - shouldNotContain: ['DOES_NOT_EXIST_VALUE'], - }, - }, - ], - }); - }); - - // ---- Invalid config handling ---- - - describe('invalid config', () => { - viteEnv.describeScenario('invalid schema causes build failure', { - command: 'vite build', - expectSuccess: false, - templateFiles: { - 'vite.config.ts': 'vite-configs/vite.config.ts', - 'index.html': 'html/basic.html', - 'src/main.ts': 'pages/minimal-page.ts', - '.env.schema': 'schemas/.env.schema.invalid', - }, - outputAssertions: [ - { - description: 'build output indicates config validation failure', - shouldContain: ['Varlock config validation failed', 'MISSING_REQUIRED_VAR'], - }, - ], - }); - - viteEnv.describeDevScenario('invalid schema shows error page then recovers on fix', { - command: 'vite dev --port 15187', - readyPattern: /Local:.*http/, - readyTimeout: 30_000, - templateFiles: { - 'vite.config.ts': 'vite-configs/vite.config.ts', - 'index.html': 'html/html-replacement.html', - 'src/main.ts': 'pages/basic-page.ts', - '.env.schema': 'schemas/.env.schema.invalid', - }, - requests: [ - { - path: '/', - bodyAssertions: { - shouldContain: ['invalid'], - shouldNotContain: ['public-test-value'], - }, - }, - { - path: '/', - fileEdits: { - '.env.schema': readFileSync(join(import.meta.dirname, 'files/schemas/.env.schema'), 'utf-8'), - }, - // Config reload after fixing .env.schema — Vite doesn't restart - fileEditDelay: 3_000, - bodyAssertions: { - shouldContain: ['public-test-value'], - shouldNotContain: ['invalid'], - }, - }, - ], - outputAssertions: [ - { - description: 'validation error details are shown in terminal', - shouldContain: ['MISSING_REQUIRED_VAR'], - }, - ], - }); - }); -}); diff --git a/framework-tests/harness/repack.ts b/framework-tests/harness/repack.ts index 7ab62b52..277bd42f 100644 --- a/framework-tests/harness/repack.ts +++ b/framework-tests/harness/repack.ts @@ -9,6 +9,7 @@ const PACKAGE_NAMES = [ '@varlock/nextjs-integration', '@varlock/astro-integration', '@varlock/vite-integration', + '@varlock/cloudflare-integration', '@varlock/expo-integration', ]; diff --git a/framework-tests/package.json b/framework-tests/package.json index cb1e5be1..ff1b518e 100644 --- a/framework-tests/package.json +++ b/framework-tests/package.json @@ -14,6 +14,7 @@ "test:expo": "vitest run frameworks/expo", "test:nextjs": "vitest run frameworks/nextjs", "test:vanilla-node": "vitest run frameworks/vanilla-node", + "test:tanstack-start": "vitest run frameworks/tanstack-start", "test:vite": "vitest run frameworks/vite" }, "devDependencies": { diff --git a/packages/integrations/cloudflare/src/index.ts b/packages/integrations/cloudflare/src/index.ts index 485c024c..fd129105 100644 --- a/packages/integrations/cloudflare/src/index.ts +++ b/packages/integrations/cloudflare/src/index.ts @@ -1,11 +1,151 @@ -import { varlockVitePlugin } from '@varlock/vite-integration'; +import path from 'node:path'; +import { + existsSync, readdirSync, unlinkSync, writeFileSync, +} from 'node:fs'; +import { execSync, spawn, type ChildProcess } from 'node:child_process'; +import { varlockVitePlugin, varlockLoadedEnv } from '@varlock/vite-integration'; import { execSyncVarlock } from 'varlock/exec-sync-varlock'; import { cloudflare, type PluginConfig, type WorkerConfig } from '@cloudflare/vite-plugin'; import { CLOUDFLARE_SSR_ENTRY_CODE } from './shared-ssr-entry-code'; +const isWindows = process.platform === 'win32'; + /** Name exposed by `@cloudflare/vite-plugin`'s main plugin object. */ const CLOUDFLARE_PLUGIN_NAME = 'vite-plugin-cloudflare'; + +// --- helpers for preview FIFO env injection -------------------------------- + +const CF_SECRET_MAX_BYTES = 5120; + +/** + * Finds the `.dev.vars` path next to the built `wrangler.json` in the build output. + * The CF plugin names output directories after the worker (e.g. `dist/test_worker/`), + * so we scan `dist/` subdirectories for the one containing `wrangler.json`. + */ +function findDevVarsPath(root: string): string | undefined { + const distDir = path.resolve(root, 'dist'); + if (!existsSync(distDir)) return undefined; + + // Check immediate subdirectories of dist/ for wrangler.json + for (const entry of readdirSync(distDir, { withFileTypes: true })) { + if (!entry.isDirectory()) continue; + const wranglerPath = path.join(distDir, entry.name, 'wrangler.json'); + if (existsSync(wranglerPath)) { + return path.join(distDir, entry.name, '.dev.vars'); + } + } + return undefined; +} + +function cleanupFile(filePath: string) { + try { + unlinkSync(filePath); + } catch { + // may already be deleted + } +} + +function chunkString(str: string, maxBytes: number): Array { + const chunks: Array = []; + let current = ''; + let currentBytes = 0; + for (const char of str) { + const charBytes = Buffer.byteLength(char); + if (currentBytes + charBytes > maxBytes && current) { + chunks.push(current); + current = ''; + currentBytes = 0; + } + current += char; + currentBytes += charBytes; + } + if (current) chunks.push(current); + return chunks; +} + +function formatEnvLine(key: string, value: string): string { + if (!value.includes("'")) { + return `${key}='${value}'`; + } + const escaped = value.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, '\\n'); + return `${key}="${escaped}"`; +} + +function formatDevVarsContent( + graph: { config: Record }, + serializedGraph: string, +) { + const lines: Array = []; + for (const key in graph.config) { + const item = graph.config[key]; + if (item.value === undefined) continue; + const strValue = typeof item.value === 'string' ? item.value : JSON.stringify(item.value); + lines.push(formatEnvLine(key, strValue)); + } + // include __VARLOCK_ENV (compact JSON) for the varlock runtime, + // split into chunks if it exceeds CF's 5KB secret limit + if (Buffer.byteLength(serializedGraph) <= CF_SECRET_MAX_BYTES) { + lines.push(formatEnvLine('__VARLOCK_ENV', serializedGraph)); + } else { + const chunks = chunkString(serializedGraph, CF_SECRET_MAX_BYTES); + lines.push(formatEnvLine('__VARLOCK_ENV_CHUNKS', String(chunks.length))); + for (let i = 0; i < chunks.length; i++) { + lines.push(formatEnvLine(`__VARLOCK_ENV_${i}`, chunks[i])); + } + } + return lines.join('\n'); +} + +/** + * Serves `content` at `filePath` for reading by miniflare. + * On Unix: uses a FIFO (named pipe) so secrets never touch disk. + * On Windows: falls back to a regular temp file. + * + * The FIFO child process writes to the pipe in a loop — each `readFileSync` + * by wrangler/miniflare gets the content, and the child immediately starts + * the next write so subsequent reads also work. + */ +function serveFifoOrFile(filePath: string, content: string) { + let fifoProcess: ChildProcess | undefined; + + if (isWindows) { + writeFileSync(filePath, content); + } else { + execSync(`mkfifo -m 0600 "${filePath}"`); + // Spawn a child process that writes to the FIFO in a loop. + // Content is passed as a base64-encoded argument instead of stdin + // to avoid a deadlock: wrangler's readFileSync blocks the parent's + // event loop, which would prevent stdin data from being flushed. + const encoded = Buffer.from(content).toString('base64'); + const parentPid = process.pid; + fifoProcess = spawn(process.execPath, [ + '-e', ` + const fs = require('fs'); + const content = Buffer.from('${encoded}', 'base64').toString(); + // Exit if the parent process dies (orphan protection). + setInterval(() => { + try { process.kill(${parentPid}, 0); } + catch { process.exit(); } + }, 2000); + (function serve() { + try { fs.writeFileSync(${JSON.stringify(filePath)}, content); setImmediate(serve); } + catch { process.exit(); } + })(); + `, + ], { stdio: ['ignore', 'ignore', 'ignore'] }); + } + + return { + stop() { + fifoProcess?.kill(); + }, + }; +} + + +// --- main plugin ----------------------------------------------------------- + /** * Varlock Cloudflare Vite plugin — wraps `@cloudflare/vite-plugin` with * automatic env var injection. @@ -113,9 +253,68 @@ export function varlockCloudflareVitePlugin( config: mergedConfig, }); + // --- preview env injector ----------------------------------------------- + // During `vite preview`, the CF plugin reads `.dev.vars` from the build + // output directory (next to the built wrangler.json) to populate miniflare + // bindings. We serve env vars via a FIFO (named pipe) so secrets never + // touch disk. This plugin's `configurePreviewServer` runs before the CF + // plugin's because it appears earlier in the plugin array. + let resolvedRoot = ''; + const previewEnvInjector: import('vite').Plugin = { + name: 'varlock-cloudflare-preview-env', + configResolved(config) { + resolvedRoot = config.root; + }, + configurePreviewServer(server) { + if (!resolvedRoot) return; + + // Find the build output directory containing wrangler.json. + // The CF plugin names environments after the worker name (e.g. "test_worker"), + // so we scan the build output rather than guessing the environment name. + const devVarsPath = findDevVarsPath(resolvedRoot); + if (!devVarsPath) return; + + if (existsSync(devVarsPath)) { + // A .dev.vars was already emitted by the CF plugin during build + // (copied from the project root). Skip our injection. + return; + } + + // Build dotenv-format content from the already-loaded env graph. + // The vite plugin's module-level reloadConfig() already populated + // varlockLoadedEnv and process.env.__VARLOCK_ENV. + const serializedGraph = process.env.__VARLOCK_ENV; + if (!serializedGraph || !varlockLoadedEnv) return; + + const content = formatDevVarsContent(varlockLoadedEnv, serializedGraph); + + // Write a temporary .dev.vars file for miniflare to pick up. + // On Unix we use a FIFO (named pipe) so secrets stay in memory. + // On Windows we fall back to a regular file. + const fifo = serveFifoOrFile(devVarsPath, content); + + // Clean up when the preview server shuts down. + const origClose = server.close.bind(server); + server.close = async () => { + fifo.stop(); + cleanupFile(devVarsPath); + return origClose(); + }; + // Also clean up on process exit. + const onExit = () => { + fifo.stop(); + cleanupFile(devVarsPath); + }; + process.on('exit', onExit); + process.on('SIGINT', onExit); + process.on('SIGTERM', onExit); + }, + }; + return [ conflictGuard, modeDetector, + previewEnvInjector, varlockPlugin, // cloudflare() may return a single plugin or an array ...(Array.isArray(cloudflarePlugin) ? cloudflarePlugin : [cloudflarePlugin]), diff --git a/packages/integrations/vite/src/index.ts b/packages/integrations/vite/src/index.ts index d4a73541..e71de8e8 100644 --- a/packages/integrations/vite/src/index.ts +++ b/packages/integrations/vite/src/index.ts @@ -114,10 +114,18 @@ export interface VarlockVitePluginOptions { export function varlockVitePlugin( vitePluginOptions?: VarlockVitePluginOptions, ): any { + // tracks which SSR environments have already had init code injected + // to prevent duplicate injection when multiple modules are detected as entries + const injectedSsrEnvironments = new Set(); + return { name: 'inject-varlock-config', enforce: 'post', + buildStart() { + injectedSsrEnvironments.clear(); + }, + // hook to modify config before it is resolved async config(config, env) { debug('vite plugin - config fn called'); @@ -168,6 +176,27 @@ See https://varlock.dev/integrations/vite/ for more details. // we do not want to inject via config.define - instead we use @rollup/plugin-replace + // Exclude varlock from dep optimization. + // `varlock/env` is a runtime proxy that breaks if pre-bundled by + // esbuild/rolldown — especially in Cloudflare worker environments + // where the optimizer runs separately and can lose the pre-bundled + // file after re-optimization cycles. + const varlockExclude = ['varlock', 'varlock/env', 'varlock/patch-console', 'varlock/patch-response']; + config.optimizeDeps ??= {}; + config.optimizeDeps.exclude = [ + ...config.optimizeDeps.exclude ?? [], + ...varlockExclude, + ]; + config.ssr ??= {}; + config.ssr.optimizeDeps ??= {}; + config.ssr.optimizeDeps.exclude = [ + ...config.ssr.optimizeDeps.exclude ?? [], + ...varlockExclude, + ]; + // For Vite 7+, per-environment excludes are handled by the + // `configEnvironment` hook below. For Vite 6, `configResolved` + // patches all resolved environments. + if (!configIsValid) { if (isDevCommand) { // adjust vite's setting so it doesnt bury the error messages @@ -189,9 +218,52 @@ See https://varlock.dev/integrations/vite/ for more details. } } }, + // Vite 6+ hook: runs for each environment (client, ssr, worker, etc.) + // Ensures varlock is excluded from dep optimization in every environment, + // including Cloudflare worker environments created by @cloudflare/vite-plugin. + configEnvironment(_name: string, envConfig: any) { + const varlockExclude = ['varlock', 'varlock/env', 'varlock/patch-console', 'varlock/patch-response']; + envConfig.dev ??= {}; + envConfig.dev.optimizeDeps ??= {}; + envConfig.dev.optimizeDeps.exclude = [ + ...envConfig.dev.optimizeDeps.exclude ?? [], + ...varlockExclude, + ]; + // Also set at the environment level (Vite 6 uses this path) + envConfig.optimizeDeps ??= {}; + envConfig.optimizeDeps.exclude = [ + ...envConfig.optimizeDeps.exclude ?? [], + ...varlockExclude, + ]; + }, // hook to observe/modify config after it is resolved configResolved(config) { debug('vite plugin - configResolved fn called'); + + // Patch per-environment optimizeDeps for Vite 6 (which lacks + // the `configEnvironment` hook). By this point all plugins have + // registered their environments, so we can patch them all. + // The resolved config is technically frozen, but `optimizeDeps` + // objects within environments are still mutable in practice. + const varlockExclude = ['varlock', 'varlock/env', 'varlock/patch-console', 'varlock/patch-response']; + if ((config as any).environments) { + for (const envName of Object.keys((config as any).environments)) { + const envConf = (config as any).environments[envName]; + if (envConf?.dev?.optimizeDeps) { + envConf.dev.optimizeDeps.exclude = [ + ...envConf.dev.optimizeDeps.exclude ?? [], + ...varlockExclude, + ]; + } + if (envConf?.optimizeDeps) { + envConf.optimizeDeps.exclude = [ + ...envConf.optimizeDeps.exclude ?? [], + ...varlockExclude, + ]; + } + } + } + if (!varlockLoadedEnv) return; // inject all .env files that varlock loaded into `configFileDependencies` // so that vite will watch them and reload if they change @@ -232,8 +304,19 @@ See https://varlock.dev/integrations/vite/ for more details. const fileExt = id.split('?')[0].split('#')[0].split('.').pop() || ''; let isEntry = false; if (SUPPORTED_FILES.includes(fileExt)) { - const moduleIds = Array.from(this.getModuleIds()); - if (moduleIds[0] === id) isEntry = true; + // In build mode, getModuleInfo(id).isEntry is reliable. + // In dev mode it's not supported (vite 6 throws, others return undefined), + // so fall back to checking if this is the first module in the graph. + try { + const moduleInfo = this.getModuleInfo(id); + if (moduleInfo?.isEntry) isEntry = true; + } catch { + // vite 6 throws "isEntry property of ModuleInfo is not supported" in dev + } + if (!isEntry) { + const moduleIds = Array.from(this.getModuleIds()); + if (moduleIds[0] === id) isEntry = true; + } } // allow integrations to register additional virtual module IDs as entry points @@ -264,44 +347,57 @@ See https://varlock.dev/integrations/vite/ for more details. // and code to load our env, or the already resolved env // TODO: keep an eye on environments API, as single ssr flag may be phased out if (options?.ssr) { - const ssrInjectMode = vitePluginOptions?.ssrInjectMode ?? 'init-only'; - const isEdgeRuntime = vitePluginOptions?.ssrEdgeRuntime ?? false; - - debug('ssrInjectMode =', ssrInjectMode, 'isDev =', isDevEnv); - if (ssrInjectMode === 'auto-load') { - injectCode.push( - "import 'varlock/auto-load';", - ); + // In build mode, prevent duplicate SSR init injection when multiple + // modules are detected as entries within the same environment (e.g., + // TanStack Start + Cloudflare where both the framework entry and the + // CF virtual worker entry match). + // In dev mode, skip dedup — Vite may re-transform the entry after + // dependency re-optimization, and the init code must be re-injected. + const envKey = this.environment?.name ?? '__ssr__'; + if (!isDevEnv && injectedSsrEnvironments.has(envKey)) { + debug(`skipping duplicate SSR injection for env "${envKey}"`); } else { - if (ssrInjectMode === 'resolved-env') { - injectCode.push(`globalThis.__varlockLoadedEnv = ${JSON.stringify(varlockLoadedEnv)};`); - } + injectedSsrEnvironments.add(envKey); - // inject custom entry code from integrations - if (vitePluginOptions?.ssrEntryCode?.length) { - injectCode.push(...vitePluginOptions.ssrEntryCode); - } + const ssrInjectMode = vitePluginOptions?.ssrInjectMode ?? 'init-only'; + const isEdgeRuntime = vitePluginOptions?.ssrEdgeRuntime ?? false; - // TODO: we may want to move this to a single module we import - injectCode.push( - "import { initVarlockEnv } from 'varlock/env';", - "import { patchGlobalConsole } from 'varlock/patch-console';", - "import { patchGlobalResponse } from 'varlock/patch-response';", - ); - // edge runtimes don't have node:http ServerResponse - if (!isEdgeRuntime) { + debug('ssrInjectMode =', ssrInjectMode, 'isDev =', isDevEnv); + if (ssrInjectMode === 'auto-load') { injectCode.push( - "import { patchGlobalServerResponse } from 'varlock/patch-server-response';", + "import 'varlock/auto-load';", ); + } else { + if (ssrInjectMode === 'resolved-env') { + injectCode.push(`globalThis.__varlockLoadedEnv = ${JSON.stringify(varlockLoadedEnv)};`); + } + + // inject custom entry code from integrations + if (vitePluginOptions?.ssrEntryCode?.length) { + injectCode.push(...vitePluginOptions.ssrEntryCode); + } + + // TODO: we may want to move this to a single module we import + injectCode.push( + "import { initVarlockEnv } from 'varlock/env';", + "import { patchGlobalConsole } from 'varlock/patch-console';", + "import { patchGlobalResponse } from 'varlock/patch-response';", + ); + // edge runtimes don't have node:http ServerResponse + if (!isEdgeRuntime) { + injectCode.push( + "import { patchGlobalServerResponse } from 'varlock/patch-server-response';", + ); + } + injectCode.push( + 'initVarlockEnv();', + 'patchGlobalConsole();', + ); + if (!isEdgeRuntime) { + injectCode.push('patchGlobalServerResponse();'); + } + injectCode.push('patchGlobalResponse();'); } - injectCode.push( - 'initVarlockEnv();', - 'patchGlobalConsole();', - ); - if (!isEdgeRuntime) { - injectCode.push('patchGlobalServerResponse();'); - } - injectCode.push('patchGlobalResponse();'); } // this build is for the client diff --git a/scripts/detect-changed-integrations.ts b/scripts/detect-changed-integrations.ts index 3dd68c1c..d5ed0a24 100644 --- a/scripts/detect-changed-integrations.ts +++ b/scripts/detect-changed-integrations.ts @@ -29,6 +29,7 @@ const INTEGRATION_PACKAGES: Record> = { expo: ['@varlock/expo-integration'], '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'], }; // Quick test paths for integrations where full suite is expensive. From 42184bfacbdd086d8d704829f530afea1c8053eb Mon Sep 17 00:00:00 2001 From: Theo Ephraim Date: Mon, 4 May 2026 16:27:11 -0700 Subject: [PATCH 02/17] chore: add bump files for vite and cloudflare integrations --- .bumpy/fix-cf-preview-tanstack.md | 5 +++++ .bumpy/fix-cf-tanstack-vite-compat.md | 5 +++++ 2 files changed, 10 insertions(+) create mode 100644 .bumpy/fix-cf-preview-tanstack.md create mode 100644 .bumpy/fix-cf-tanstack-vite-compat.md diff --git a/.bumpy/fix-cf-preview-tanstack.md b/.bumpy/fix-cf-preview-tanstack.md new file mode 100644 index 00000000..c31b1789 --- /dev/null +++ b/.bumpy/fix-cf-preview-tanstack.md @@ -0,0 +1,5 @@ +--- +"@varlock/cloudflare-integration": patch +--- + +fix cloudflare preview env injection and tanstack start compatibility diff --git a/.bumpy/fix-cf-tanstack-vite-compat.md b/.bumpy/fix-cf-tanstack-vite-compat.md new file mode 100644 index 00000000..8b9dec9a --- /dev/null +++ b/.bumpy/fix-cf-tanstack-vite-compat.md @@ -0,0 +1,5 @@ +--- +"@varlock/vite-integration": patch +--- + +fix cloudflare + tanstack start + vite 6/7/8 compatibility From cfd8c17d464259f715c0bbe43f0b48f9bbb4ef08 Mon Sep 17 00:00:00 2001 From: Theo Ephraim Date: Mon, 4 May 2026 21:18:49 -0700 Subject: [PATCH 03/17] test: add top-level ENV access tests for tanstack start Verifies ENV.x works at module top-level (not just inside handlers) for both node and cloudflare targets across vite 6/7/8. --- .../files/routes/index-toplevel.tsx | 36 ++++++++++++ .../files/routes/router-toplevel.tsx | 29 ++++++++++ .../tanstack-start/tanstack-shared.ts | 55 +++++++++++++++++++ 3 files changed, 120 insertions(+) create mode 100644 framework-tests/frameworks/tanstack-start/files/routes/index-toplevel.tsx create mode 100644 framework-tests/frameworks/tanstack-start/files/routes/router-toplevel.tsx diff --git a/framework-tests/frameworks/tanstack-start/files/routes/index-toplevel.tsx b/framework-tests/frameworks/tanstack-start/files/routes/index-toplevel.tsx new file mode 100644 index 00000000..f0268a34 --- /dev/null +++ b/framework-tests/frameworks/tanstack-start/files/routes/index-toplevel.tsx @@ -0,0 +1,36 @@ +/* eslint-disable no-use-before-define */ +import { createFileRoute } from '@tanstack/react-router'; +import { createServerFn } from '@tanstack/react-start'; +import { ENV } from 'varlock/env'; +// Import from router — this module has top-level ENV access +// and is statically imported (not code-split). +import { getEnvCheckResult } from '../router'; + +const getServerEnvData = createServerFn({ method: 'GET' }).handler(() => { + console.log('secret-log-test::', ENV.SECRET_KEY); + // Use the top-level result from router.tsx + const topLevel = getEnvCheckResult(); + return { + public_var: ENV.PUBLIC_VAR, + api_url: topLevel.apiUrl, + has_sensitive: topLevel.hasSecret, + }; +}); + +function IndexPage() { + const data = Route.useLoaderData(); + return ( +
+
+ {`public_var::${data.public_var}`} + {`\napi_url::${data.api_url}`} + {`\nhas_sensitive::${data.has_sensitive}`} +
+
+ ); +} + +export const Route = createFileRoute('/')({ + loader: () => getServerEnvData(), + component: IndexPage, +}); diff --git a/framework-tests/frameworks/tanstack-start/files/routes/router-toplevel.tsx b/framework-tests/frameworks/tanstack-start/files/routes/router-toplevel.tsx new file mode 100644 index 00000000..0029ddf5 --- /dev/null +++ b/framework-tests/frameworks/tanstack-start/files/routes/router-toplevel.tsx @@ -0,0 +1,29 @@ +import { createRouter as createTanStackRouter } from '@tanstack/react-router'; +import { ENV } from 'varlock/env'; +import { routeTree } from './routeTree.gen'; + +// Top-level ENV access in a statically-imported module. +// This runs at module evaluation time — before any request handler. +// If initVarlockEnv hasn't run yet, this will fail or return undefined. +const envCheckResult = { + apiUrl: ENV.API_URL, + hasSecret: ENV.SECRET_KEY ? 'yes' : 'no', +}; + +export function getEnvCheckResult() { + return envCheckResult; +} + +export function getRouter() { + const router = createTanStackRouter({ + routeTree, + scrollRestoration: true, + }); + return router; +} + +declare module '@tanstack/react-router' { + interface Register { + router: ReturnType + } +} diff --git a/framework-tests/frameworks/tanstack-start/tanstack-shared.ts b/framework-tests/frameworks/tanstack-start/tanstack-shared.ts index 4d45fa84..43b155fe 100644 --- a/framework-tests/frameworks/tanstack-start/tanstack-shared.ts +++ b/framework-tests/frameworks/tanstack-start/tanstack-shared.ts @@ -114,6 +114,33 @@ export function defineTanstackTests( }, ], }); + + // Top-level ENV access — verifies initVarlockEnv runs before + // application modules so ENV.x works outside handlers/async fns. + nodeEnv.describeDevScenario('top-level ENV access (build + preview)', { + command: 'vite build && vite preview --port 15194', + readyPattern: /Local:.*http/, + readyTimeout: 60_000, + timeout: 120_000, + templateFiles: { + 'vite.config.ts': 'configs/vite.config.node.ts', + 'src/routes/index.tsx': 'routes/index-toplevel.tsx', + 'src/router.tsx': 'routes/router-toplevel.tsx', + }, + requests: [ + { + path: '/', + bodyAssertions: { + shouldContain: [ + 'public_var::public-test-value', + 'api_url::https://api.example.com', + 'has_sensitive::yes', + ], + shouldNotContain: ['super-secret-value'], + }, + }, + ], + }); }); // ---- Cloudflare target -------------------------------------------------- @@ -222,5 +249,33 @@ export function defineTanstackTests( }, ], }); + + // Top-level ENV access — verifies initVarlockEnv runs before + // application modules so ENV.x works outside handlers/async fns. + cfEnv.describeDevScenario('top-level ENV access (build + preview)', { + command: 'vite build && vite preview --port 15195', + readyPattern: /Local:.*http/, + readyTimeout: 60_000, + timeout: 120_000, + templateFiles: { + 'vite.config.ts': 'configs/vite.config.cloudflare.ts', + 'wrangler.jsonc': 'configs/wrangler.jsonc', + 'src/routes/index.tsx': 'routes/index-toplevel.tsx', + 'src/router.tsx': 'routes/router-toplevel.tsx', + }, + requests: [ + { + path: '/', + bodyAssertions: { + shouldContain: [ + 'public_var::public-test-value', + 'api_url::https://api.example.com', + 'has_sensitive::yes', + ], + shouldNotContain: ['super-secret-value'], + }, + }, + ], + }); }); } From 22653175c2a9042c34d32f04f0e295e16c14c062 Mon Sep 17 00:00:00 2001 From: Theo Ephraim Date: Tue, 5 May 2026 09:38:08 -0700 Subject: [PATCH 04/17] test: add top-level ENV access to cloudflare worker tests Adds ENV.x access at module evaluation time (outside fetch handler) to the basic worker. This currently fails because initVarlockEnv() runs after the worker module body in the bundled output. --- .../frameworks/cloudflare/cloudflare-shared.ts | 5 +++++ .../frameworks/cloudflare/files/workers/basic-worker.ts | 8 ++++++++ 2 files changed, 13 insertions(+) diff --git a/framework-tests/frameworks/cloudflare/cloudflare-shared.ts b/framework-tests/frameworks/cloudflare/cloudflare-shared.ts index f12f2e65..842d38f2 100644 --- a/framework-tests/frameworks/cloudflare/cloudflare-shared.ts +++ b/framework-tests/frameworks/cloudflare/cloudflare-shared.ts @@ -61,6 +61,9 @@ export function defineCloudflareTests( 'api_url::https://api.example.com', // varlock ENV proxy - sensitive (accessible but value not leaked) 'has_sensitive::yes', + // top-level ENV access (module evaluation time, not per-request) + 'toplevel_api_url::https://api.example.com', + 'toplevel_has_secret::yes', // cloudflare native env access 'native_public_var::public-test-value', 'native_has_secret::yes', @@ -151,6 +154,8 @@ export function defineCloudflareTests( 'public_var::public-test-value', 'api_url::https://api.example.com', 'has_sensitive::yes', + 'toplevel_api_url::https://api.example.com', + 'toplevel_has_secret::yes', ], shouldNotContain: ['super-secret-value'], }, diff --git a/framework-tests/frameworks/cloudflare/files/workers/basic-worker.ts b/framework-tests/frameworks/cloudflare/files/workers/basic-worker.ts index 0373a714..2e6bb4bf 100644 --- a/framework-tests/frameworks/cloudflare/files/workers/basic-worker.ts +++ b/framework-tests/frameworks/cloudflare/files/workers/basic-worker.ts @@ -1,5 +1,10 @@ import { ENV } from 'varlock/env'; +// Top-level ENV access — runs at module evaluation time, not per-request. +// Verifies initVarlockEnv runs before worker module body. +const TOP_LEVEL_API_URL = ENV.API_URL; +const TOP_LEVEL_HAS_SECRET = ENV.SECRET_KEY ? 'yes' : 'no'; + export default { async fetch(_request: Request, env: Record): Promise { // this should be redacted in varlock-wrangler output @@ -11,6 +16,9 @@ export default { `api_url::${ENV.API_URL}`, // varlock ENV proxy - sensitive var (check accessible without leaking value) `has_sensitive::${ENV.SECRET_KEY ? 'yes' : 'no'}`, + // top-level ENV access (evaluated at module load, not per-request) + `toplevel_api_url::${TOP_LEVEL_API_URL}`, + `toplevel_has_secret::${TOP_LEVEL_HAS_SECRET}`, // cloudflare native env access (injected by varlock-wrangler --env-file) `native_public_var::${env.PUBLIC_VAR}`, `native_has_secret::${env.SECRET_KEY ? 'yes' : 'no'}`, From f1c507b7de990a163847701c9c8b54e16c6be0e6 Mon Sep 17 00:00:00 2001 From: Theo Ephraim Date: Tue, 5 May 2026 09:49:11 -0700 Subject: [PATCH 05/17] fix: use virtual module for SSR init to ensure correct evaluation order MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaces inline init code injection with a virtual module import (`\0varlock-ssr-init`). The virtual module has no transitive dependencies on user code, so the bundler evaluates it first in the concatenated output — ensuring initVarlockEnv() runs before any user modules that access ENV at the top level. Previously, init code was appended to the entry module's body, which ran last in the bundle after all statically-imported user modules had already been evaluated. --- packages/integrations/vite/src/index.ts | 121 +++++++++++++----------- 1 file changed, 65 insertions(+), 56 deletions(-) diff --git a/packages/integrations/vite/src/index.ts b/packages/integrations/vite/src/index.ts index e71de8e8..3c61a3d0 100644 --- a/packages/integrations/vite/src/index.ts +++ b/packages/integrations/vite/src/index.ts @@ -111,6 +111,8 @@ export interface VarlockVitePluginOptions { // Plugin type from this package's node_modules — a different copy than the // consumer's — causing spurious type errors. Since Vite's `plugins` config // is loosely typed, this is functionally equivalent. +const VARLOCK_INIT_MODULE_ID = '\0varlock-ssr-init'; + export function varlockVitePlugin( vitePluginOptions?: VarlockVitePluginOptions, ): any { @@ -118,6 +120,51 @@ export function varlockVitePlugin( // to prevent duplicate injection when multiple modules are detected as entries const injectedSsrEnvironments = new Set(); + // 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 lines: Array = [ + '// Virtual module generated by @varlock/vite-integration', + '// Runs before any user code to ensure ENV is available at module top-level', + 'globalThis.__varlockThrowOnMissingKeys = true;', + ]; + + if (ssrInjectMode === 'auto-load') { + lines.push("import 'varlock/auto-load';"); + } else { + if (ssrInjectMode === 'resolved-env') { + lines.push(`globalThis.__varlockLoadedEnv = ${JSON.stringify(varlockLoadedEnv)};`); + } + + // inject custom entry code from integrations (e.g., CF bindings loader) + if (vitePluginOptions?.ssrEntryCode?.length) { + lines.push(...vitePluginOptions.ssrEntryCode); + } + + lines.push( + "import { initVarlockEnv } from 'varlock/env';", + "import { patchGlobalConsole } from 'varlock/patch-console';", + "import { patchGlobalResponse } from 'varlock/patch-response';", + ); + if (!isEdgeRuntime) { + lines.push("import { patchGlobalServerResponse } from 'varlock/patch-server-response';"); + } + lines.push( + 'initVarlockEnv();', + 'patchGlobalConsole();', + ); + if (!isEdgeRuntime) { + lines.push('patchGlobalServerResponse();'); + } + lines.push('patchGlobalResponse();'); + } + + return lines.join('\n'); + } + return { name: 'inject-varlock-config', enforce: 'post', @@ -126,6 +173,13 @@ export function varlockVitePlugin( injectedSsrEnvironments.clear(); }, + resolveId(id) { + if (id === VARLOCK_INIT_MODULE_ID) return id; + }, + load(id) { + if (id === VARLOCK_INIT_MODULE_ID) return buildInitModuleCode(); + }, + // hook to modify config before it is resolved async config(config, env) { debug('vite plugin - config fn called'); @@ -338,71 +392,26 @@ See https://varlock.dev/integrations/vite/ for more details. isDevEnv = isDevCommand; } - const injectCode = [ - '// INJECTED BY @varlock/vite-integration ----', - 'globalThis.__varlockThrowOnMissingKeys = true;', - ]; + const injectCode = ['// INJECTED BY @varlock/vite-integration ----']; - // on the code intended for the backend we'll inject init logic - // and code to load our env, or the already resolved env - // TODO: keep an eye on environments API, as single ssr flag may be phased out if (options?.ssr) { - // In build mode, prevent duplicate SSR init injection when multiple - // modules are detected as entries within the same environment (e.g., - // TanStack Start + Cloudflare where both the framework entry and the - // CF virtual worker entry match). - // In dev mode, skip dedup — Vite may re-transform the entry after - // dependency re-optimization, and the init code must be re-injected. + // SSR entry: import the virtual init module. Because this module + // has no transitive deps on user code, the bundler evaluates it + // first — ensuring initVarlockEnv() runs before any user modules. + // In build mode, dedup across environments to prevent double injection + // when multiple modules match as entries (e.g. TanStack + CF). + // In dev mode, skip dedup — Vite may re-transform after dep re-optimization. const envKey = this.environment?.name ?? '__ssr__'; if (!isDevEnv && injectedSsrEnvironments.has(envKey)) { debug(`skipping duplicate SSR injection for env "${envKey}"`); } else { injectedSsrEnvironments.add(envKey); - - const ssrInjectMode = vitePluginOptions?.ssrInjectMode ?? 'init-only'; - const isEdgeRuntime = vitePluginOptions?.ssrEdgeRuntime ?? false; - - debug('ssrInjectMode =', ssrInjectMode, 'isDev =', isDevEnv); - if (ssrInjectMode === 'auto-load') { - injectCode.push( - "import 'varlock/auto-load';", - ); - } else { - if (ssrInjectMode === 'resolved-env') { - injectCode.push(`globalThis.__varlockLoadedEnv = ${JSON.stringify(varlockLoadedEnv)};`); - } - - // inject custom entry code from integrations - if (vitePluginOptions?.ssrEntryCode?.length) { - injectCode.push(...vitePluginOptions.ssrEntryCode); - } - - // TODO: we may want to move this to a single module we import - injectCode.push( - "import { initVarlockEnv } from 'varlock/env';", - "import { patchGlobalConsole } from 'varlock/patch-console';", - "import { patchGlobalResponse } from 'varlock/patch-response';", - ); - // edge runtimes don't have node:http ServerResponse - if (!isEdgeRuntime) { - injectCode.push( - "import { patchGlobalServerResponse } from 'varlock/patch-server-response';", - ); - } - injectCode.push( - 'initVarlockEnv();', - 'patchGlobalConsole();', - ); - if (!isEdgeRuntime) { - injectCode.push('patchGlobalServerResponse();'); - } - injectCode.push('patchGlobalResponse();'); - } + debug('ssrInjectMode =', vitePluginOptions?.ssrInjectMode ?? 'init-only', 'isDev =', isDevEnv); + injectCode.push(`import '${VARLOCK_INIT_MODULE_ID}';`); } - - // this build is for the client } else { - // in dev mode, on the client we'll inject a list of the existing keys, to provide better error messages + // Client entry + injectCode.push('globalThis.__varlockThrowOnMissingKeys = true;'); if (isDevEnv) { injectCode.push( '// NOTE - __varlockValidKeys is only injected during development', From 79207ca13902cf0e88d64858fbe3fe706c8c288d Mon Sep 17 00:00:00 2001 From: Theo Ephraim Date: Tue, 5 May 2026 10:00:06 -0700 Subject: [PATCH 06/17] refactor: remove SSR init dedup logic MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The virtual module approach makes dedup unnecessary — ES modules evaluate only once regardless of how many entries import them. --- packages/integrations/vite/src/index.ts | 23 ++++------------------- 1 file changed, 4 insertions(+), 19 deletions(-) diff --git a/packages/integrations/vite/src/index.ts b/packages/integrations/vite/src/index.ts index 3c61a3d0..300c9118 100644 --- a/packages/integrations/vite/src/index.ts +++ b/packages/integrations/vite/src/index.ts @@ -116,10 +116,6 @@ const VARLOCK_INIT_MODULE_ID = '\0varlock-ssr-init'; export function varlockVitePlugin( vitePluginOptions?: VarlockVitePluginOptions, ): any { - // tracks which SSR environments have already had init code injected - // to prevent duplicate injection when multiple modules are detected as entries - const injectedSsrEnvironments = new Set(); - // 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. @@ -169,10 +165,6 @@ export function varlockVitePlugin( name: 'inject-varlock-config', enforce: 'post', - buildStart() { - injectedSsrEnvironments.clear(); - }, - resolveId(id) { if (id === VARLOCK_INIT_MODULE_ID) return id; }, @@ -398,17 +390,10 @@ See https://varlock.dev/integrations/vite/ for more details. // SSR entry: import the virtual init module. Because this module // has no transitive deps on user code, the bundler evaluates it // first — ensuring initVarlockEnv() runs before any user modules. - // In build mode, dedup across environments to prevent double injection - // when multiple modules match as entries (e.g. TanStack + CF). - // In dev mode, skip dedup — Vite may re-transform after dep re-optimization. - const envKey = this.environment?.name ?? '__ssr__'; - if (!isDevEnv && injectedSsrEnvironments.has(envKey)) { - debug(`skipping duplicate SSR injection for env "${envKey}"`); - } else { - injectedSsrEnvironments.add(envKey); - debug('ssrInjectMode =', vitePluginOptions?.ssrInjectMode ?? 'init-only', 'isDev =', isDevEnv); - injectCode.push(`import '${VARLOCK_INIT_MODULE_ID}';`); - } + // Multiple entries importing the same virtual module is harmless — + // ES modules evaluate only once. + debug('ssrInjectMode =', vitePluginOptions?.ssrInjectMode ?? 'init-only', 'isDev =', isDevEnv); + injectCode.push(`import '${VARLOCK_INIT_MODULE_ID}';`); } else { // Client entry injectCode.push('globalThis.__varlockThrowOnMissingKeys = true;'); From 133151882eb7c58bfa13b725f1c98243711e9bd2 Mon Sep 17 00:00:00 2001 From: Theo Ephraim Date: Tue, 5 May 2026 10:04:52 -0700 Subject: [PATCH 07/17] refactor: clean up entry detection and transform logic --- packages/integrations/vite/src/index.ts | 37 ++++++------------------- 1 file changed, 9 insertions(+), 28 deletions(-) diff --git a/packages/integrations/vite/src/index.ts b/packages/integrations/vite/src/index.ts index 300c9118..702f999b 100644 --- a/packages/integrations/vite/src/index.ts +++ b/packages/integrations/vite/src/index.ts @@ -344,15 +344,12 @@ See https://varlock.dev/integrations/vite/ for more details. // replace build-time ENV.x references let magicString = replacerFn(this, code, id); - // we need to detect if this module is one of our worker entry points - // when running `vite build`, we could use `this.getModuleInfo(id).isEntry` - // but that doesnt work in dev, so we try to detect it another way + // Detect if this module is an entry point so we can inject varlock init. + // For regular files: try isEntry (build mode), fall back to moduleIds[0] (dev). + // For virtual modules: check ssrEntryModuleIds from integrations (e.g. CF plugin). const fileExt = id.split('?')[0].split('#')[0].split('.').pop() || ''; let isEntry = false; if (SUPPORTED_FILES.includes(fileExt)) { - // In build mode, getModuleInfo(id).isEntry is reliable. - // In dev mode it's not supported (vite 6 throws, others return undefined), - // so fall back to checking if this is the first module in the graph. try { const moduleInfo = this.getModuleInfo(id); if (moduleInfo?.isEntry) isEntry = true; @@ -364,42 +361,26 @@ See https://varlock.dev/integrations/vite/ for more details. if (moduleIds[0] === id) isEntry = true; } } - - // allow integrations to register additional virtual module IDs as entry points if (vitePluginOptions?.ssrEntryModuleIds?.includes(id)) isEntry = true; if (isEntry) { debug(`detected entry: ${id}`); - // using env.command (in config hook) is misleading - // because some frameworks (react router) boot dev servers during the build process - // even during the build, there are multiple environments - // but at least this seems to work for our needs - let isDevEnv = false; - - // ! environments are only supported in vite 6+ - // so we try to make sure it still works in vite 5 too - if (this.environment) { - isDevEnv = this.environment.mode === 'dev'; - } else { - isDevEnv = isDevCommand; - } const injectCode = ['// INJECTED BY @varlock/vite-integration ----']; if (options?.ssr) { - // SSR entry: import the virtual init module. Because this module - // has no transitive deps on user code, the bundler evaluates it - // first — ensuring initVarlockEnv() runs before any user modules. - // Multiple entries importing the same virtual module is harmless — - // ES modules evaluate only once. - debug('ssrInjectMode =', vitePluginOptions?.ssrInjectMode ?? 'init-only', 'isDev =', isDevEnv); + // SSR entry: import the virtual init module. It has no transitive deps + // on user code so the bundler evaluates it first — ensuring + // initVarlockEnv() runs before any user modules. injectCode.push(`import '${VARLOCK_INIT_MODULE_ID}';`); } else { // Client entry injectCode.push('globalThis.__varlockThrowOnMissingKeys = true;'); + // Detect dev vs build for client-side dev helpers. + // Use the environment API (vite 6+), falling back to command check (vite 5). + const isDevEnv = this.environment ? this.environment.mode === 'dev' : isDevCommand; if (isDevEnv) { injectCode.push( - '// NOTE - __varlockValidKeys is only injected during development', `globalThis.__varlockValidKeys = ${JSON.stringify(Object.keys(varlockLoadedEnv?.config || {}))};`, ); } From 6556ab52c6b1b3e18827606f02a98ac62545be07 Mon Sep 17 00:00:00 2001 From: Theo Ephraim Date: Tue, 5 May 2026 10:06:08 -0700 Subject: [PATCH 08/17] chore: combine bump files --- .bumpy/fix-cf-preview-tanstack.md | 5 ----- .bumpy/fix-cf-tanstack-vite-compat.md | 1 + 2 files changed, 1 insertion(+), 5 deletions(-) delete mode 100644 .bumpy/fix-cf-preview-tanstack.md diff --git a/.bumpy/fix-cf-preview-tanstack.md b/.bumpy/fix-cf-preview-tanstack.md deleted file mode 100644 index c31b1789..00000000 --- a/.bumpy/fix-cf-preview-tanstack.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@varlock/cloudflare-integration": patch ---- - -fix cloudflare preview env injection and tanstack start compatibility diff --git a/.bumpy/fix-cf-tanstack-vite-compat.md b/.bumpy/fix-cf-tanstack-vite-compat.md index 8b9dec9a..f8cdfab4 100644 --- a/.bumpy/fix-cf-tanstack-vite-compat.md +++ b/.bumpy/fix-cf-tanstack-vite-compat.md @@ -1,5 +1,6 @@ --- "@varlock/vite-integration": patch +"@varlock/cloudflare-integration": patch --- fix cloudflare + tanstack start + vite 6/7/8 compatibility From 65b3e68946d03fae7e7a0c2c1309aac725831866 Mon Sep 17 00:00:00 2001 From: Theo Ephraim Date: Tue, 5 May 2026 10:12:06 -0700 Subject: [PATCH 09/17] test: add top-level ENV access to astro API endpoint test --- framework-tests/frameworks/astro/astro-shared.ts | 1 + framework-tests/frameworks/astro/files/pages/api-endpoint.ts | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/framework-tests/frameworks/astro/astro-shared.ts b/framework-tests/frameworks/astro/astro-shared.ts index a5aff53b..5a03bbbe 100644 --- a/framework-tests/frameworks/astro/astro-shared.ts +++ b/framework-tests/frameworks/astro/astro-shared.ts @@ -250,6 +250,7 @@ export function defineAstroTests(astroVersion: number, testDir: string) { shouldContain: [ 'public_var::public-var-value', 'has_secret::yes', + 'toplevel_has_secret::yes', ], shouldNotContain: ['super-secret-value'], }, diff --git a/framework-tests/frameworks/astro/files/pages/api-endpoint.ts b/framework-tests/frameworks/astro/files/pages/api-endpoint.ts index d0986d2f..c8ea7f3a 100644 --- a/framework-tests/frameworks/astro/files/pages/api-endpoint.ts +++ b/framework-tests/frameworks/astro/files/pages/api-endpoint.ts @@ -1,11 +1,16 @@ import type { APIRoute } from 'astro'; import { ENV } from 'varlock/env'; +// Top-level ENV access — runs at module evaluation time. +// Verifies initVarlockEnv runs before user modules. +const TOP_LEVEL_HAS_SECRET = ENV.SENSITIVE_VAR ? 'yes' : 'no'; + export const GET: APIRoute = () => { const lines = [ `public_var::${ENV.PUBLIC_VAR}`, `env_specific::${ENV.ENV_SPECIFIC_VAR}`, `has_secret::${ENV.SENSITIVE_VAR ? 'yes' : 'no'}`, + `toplevel_has_secret::${TOP_LEVEL_HAS_SECRET}`, ]; return new Response(lines.join('\n')); }; From ed24553a9927c1d459f559fd57bab9274748fbc8 Mon Sep 17 00:00:00 2001 From: Theo Ephraim Date: Tue, 5 May 2026 10:26:26 -0700 Subject: [PATCH 10/17] fix: error on .dev.vars file when using varlock cloudflare plugin Throw a clear error if a .dev.vars file exists at the project root (during dev/build) or in the build output (during preview), since it conflicts with varlock's automatic env injection. --- packages/integrations/cloudflare/src/index.ts | 23 +++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/packages/integrations/cloudflare/src/index.ts b/packages/integrations/cloudflare/src/index.ts index fd129105..902b1ec4 100644 --- a/packages/integrations/cloudflare/src/index.ts +++ b/packages/integrations/cloudflare/src/index.ts @@ -156,6 +156,10 @@ function serveFifoOrFile(filePath: string, content: string) { * `@cloudflare/vite-plugin` (which doesn't support SvelteKit) and uses an * SSR-entry-based env loader. * + * **Important:** Do not use a `.dev.vars` file alongside this plugin — varlock + * handles env injection automatically. The plugin will throw an error if a + * `.dev.vars` file is detected in your project root or build output. + * * @example * ```ts * import { varlockCloudflareVitePlugin } from '@varlock/cloudflare-integration'; @@ -202,8 +206,18 @@ export function varlockCloudflareVitePlugin( const modeDetector: import('vite').Plugin = { name: 'varlock-cloudflare-mode', enforce: 'pre', - config(_config, env) { + config(config, env) { isDevMode = env.command === 'serve'; + + // Error if a .dev.vars file exists — it conflicts with varlock's env management. + const root = config.root ? path.resolve(config.root) : process.cwd(); + const devVarsPath = path.resolve(root, '.dev.vars'); + if (existsSync(devVarsPath)) { + throw new Error( + '[varlock] A .dev.vars file was found in your project root, which conflicts with varlock\'s env management.\n' + + 'Remove the .dev.vars file — varlock handles env injection automatically.', + ); + } }, }; @@ -275,9 +289,10 @@ export function varlockCloudflareVitePlugin( if (!devVarsPath) return; if (existsSync(devVarsPath)) { - // A .dev.vars was already emitted by the CF plugin during build - // (copied from the project root). Skip our injection. - return; + throw new Error( + '[varlock] A .dev.vars file was found in the build output, which conflicts with varlock\'s env management.\n' + + 'Remove your project-root .dev.vars file — varlock handles env injection automatically.', + ); } // Build dotenv-format content from the already-loaded env graph. From e5f658c5d6d28d836238a42fcf2c29a613ca232e Mon Sep 17 00:00:00 2001 From: Theo Ephraim Date: Tue, 5 May 2026 10:27:50 -0700 Subject: [PATCH 11/17] docs: add .dev.vars removal note to cloudflare integration page --- .../src/content/docs/integrations/cloudflare.mdx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/varlock-website/src/content/docs/integrations/cloudflare.mdx b/packages/varlock-website/src/content/docs/integrations/cloudflare.mdx index 160a0cbb..4fd7763c 100644 --- a/packages/varlock-website/src/content/docs/integrations/cloudflare.mdx +++ b/packages/varlock-website/src/content/docs/integrations/cloudflare.mdx @@ -151,6 +151,10 @@ For SvelteKit on Cloudflare, use the dedicated [`varlockSvelteKitCloudflarePlugi +:::caution[Remove `.dev.vars`] +If you have a `.dev.vars` file in your project root, **delete it**. Varlock manages env injection automatically — a `.dev.vars` file will conflict and the plugin will throw an error. This also applies to `.dev.vars.` files. +::: + ### How it works - **In dev**: Resolved vars are automatically injected into miniflare's bindings. From 873f694e9f0ddaa69c1bc4d7b8383097caea9601 Mon Sep 17 00:00:00 2001 From: Theo Ephraim Date: Tue, 5 May 2026 10:35:19 -0700 Subject: [PATCH 12/17] refactor: deduplicate optimizeDeps exclude logic --- packages/integrations/vite/src/index.ts | 76 ++++++++----------------- 1 file changed, 24 insertions(+), 52 deletions(-) diff --git a/packages/integrations/vite/src/index.ts b/packages/integrations/vite/src/index.ts index 702f999b..22d78ce7 100644 --- a/packages/integrations/vite/src/index.ts +++ b/packages/integrations/vite/src/index.ts @@ -113,6 +113,18 @@ export interface VarlockVitePluginOptions { // is loosely typed, this is functionally equivalent. const VARLOCK_INIT_MODULE_ID = '\0varlock-ssr-init'; +// Packages to exclude from Vite's dep optimizer — `varlock/env` is a runtime +// proxy that breaks if pre-bundled by esbuild/rolldown. +const VARLOCK_OPTIMIZE_DEPS_EXCLUDE = ['varlock', 'varlock/env', 'varlock/patch-console', 'varlock/patch-response']; + +function addVarlockOptimizeDepsExclude(config: any) { + config.optimizeDeps ??= {}; + config.optimizeDeps.exclude = [ + ...config.optimizeDeps.exclude ?? [], + ...VARLOCK_OPTIMIZE_DEPS_EXCLUDE, + ]; +} + export function varlockVitePlugin( vitePluginOptions?: VarlockVitePluginOptions, ): any { @@ -222,26 +234,11 @@ See https://varlock.dev/integrations/vite/ for more details. // we do not want to inject via config.define - instead we use @rollup/plugin-replace - // Exclude varlock from dep optimization. - // `varlock/env` is a runtime proxy that breaks if pre-bundled by - // esbuild/rolldown — especially in Cloudflare worker environments - // where the optimizer runs separately and can lose the pre-bundled - // file after re-optimization cycles. - const varlockExclude = ['varlock', 'varlock/env', 'varlock/patch-console', 'varlock/patch-response']; - config.optimizeDeps ??= {}; - config.optimizeDeps.exclude = [ - ...config.optimizeDeps.exclude ?? [], - ...varlockExclude, - ]; + // Exclude varlock from dep optimization (Vite 5 top-level config). + // Vite 6+ environments are handled by configEnvironment / configResolved below. + addVarlockOptimizeDepsExclude(config); config.ssr ??= {}; - config.ssr.optimizeDeps ??= {}; - config.ssr.optimizeDeps.exclude = [ - ...config.ssr.optimizeDeps.exclude ?? [], - ...varlockExclude, - ]; - // For Vite 7+, per-environment excludes are handled by the - // `configEnvironment` hook below. For Vite 6, `configResolved` - // patches all resolved environments. + addVarlockOptimizeDepsExclude(config.ssr); if (!configIsValid) { if (isDevCommand) { @@ -264,49 +261,24 @@ See https://varlock.dev/integrations/vite/ for more details. } } }, - // Vite 6+ hook: runs for each environment (client, ssr, worker, etc.) - // Ensures varlock is excluded from dep optimization in every environment, - // including Cloudflare worker environments created by @cloudflare/vite-plugin. + // Vite 7+ hook: runs for each environment (client, ssr, worker, etc.) configEnvironment(_name: string, envConfig: any) { - const varlockExclude = ['varlock', 'varlock/env', 'varlock/patch-console', 'varlock/patch-response']; + addVarlockOptimizeDepsExclude(envConfig); envConfig.dev ??= {}; - envConfig.dev.optimizeDeps ??= {}; - envConfig.dev.optimizeDeps.exclude = [ - ...envConfig.dev.optimizeDeps.exclude ?? [], - ...varlockExclude, - ]; - // Also set at the environment level (Vite 6 uses this path) - envConfig.optimizeDeps ??= {}; - envConfig.optimizeDeps.exclude = [ - ...envConfig.optimizeDeps.exclude ?? [], - ...varlockExclude, - ]; + addVarlockOptimizeDepsExclude(envConfig.dev); }, // hook to observe/modify config after it is resolved configResolved(config) { debug('vite plugin - configResolved fn called'); - // Patch per-environment optimizeDeps for Vite 6 (which lacks - // the `configEnvironment` hook). By this point all plugins have - // registered their environments, so we can patch them all. - // The resolved config is technically frozen, but `optimizeDeps` - // objects within environments are still mutable in practice. - const varlockExclude = ['varlock', 'varlock/env', 'varlock/patch-console', 'varlock/patch-response']; + // Patch per-environment optimizeDeps for Vite 6 (which lacks the + // `configEnvironment` hook). The resolved config is technically frozen, + // but `optimizeDeps` objects within environments are still mutable. if ((config as any).environments) { for (const envName of Object.keys((config as any).environments)) { const envConf = (config as any).environments[envName]; - if (envConf?.dev?.optimizeDeps) { - envConf.dev.optimizeDeps.exclude = [ - ...envConf.dev.optimizeDeps.exclude ?? [], - ...varlockExclude, - ]; - } - if (envConf?.optimizeDeps) { - envConf.optimizeDeps.exclude = [ - ...envConf.optimizeDeps.exclude ?? [], - ...varlockExclude, - ]; - } + if (envConf?.dev?.optimizeDeps) addVarlockOptimizeDepsExclude(envConf.dev); + if (envConf?.optimizeDeps) addVarlockOptimizeDepsExclude(envConf); } } From cd2dcd1e6cb057aadf68d137aa1e053cd69a4efc Mon Sep 17 00:00:00 2001 From: Theo Ephraim Date: Tue, 5 May 2026 10:39:38 -0700 Subject: [PATCH 13/17] =?UTF-8?q?refactor:=20remove=20optimizeDeps=20exclu?= =?UTF-8?q?de=20=E2=80=94=20virtual=20module=20approach=20makes=20it=20unn?= =?UTF-8?q?ecessary?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/integrations/vite/src/index.ts | 35 ------------------------- 1 file changed, 35 deletions(-) diff --git a/packages/integrations/vite/src/index.ts b/packages/integrations/vite/src/index.ts index 22d78ce7..e1b200ca 100644 --- a/packages/integrations/vite/src/index.ts +++ b/packages/integrations/vite/src/index.ts @@ -113,18 +113,6 @@ export interface VarlockVitePluginOptions { // is loosely typed, this is functionally equivalent. const VARLOCK_INIT_MODULE_ID = '\0varlock-ssr-init'; -// Packages to exclude from Vite's dep optimizer — `varlock/env` is a runtime -// proxy that breaks if pre-bundled by esbuild/rolldown. -const VARLOCK_OPTIMIZE_DEPS_EXCLUDE = ['varlock', 'varlock/env', 'varlock/patch-console', 'varlock/patch-response']; - -function addVarlockOptimizeDepsExclude(config: any) { - config.optimizeDeps ??= {}; - config.optimizeDeps.exclude = [ - ...config.optimizeDeps.exclude ?? [], - ...VARLOCK_OPTIMIZE_DEPS_EXCLUDE, - ]; -} - export function varlockVitePlugin( vitePluginOptions?: VarlockVitePluginOptions, ): any { @@ -234,12 +222,6 @@ See https://varlock.dev/integrations/vite/ for more details. // we do not want to inject via config.define - instead we use @rollup/plugin-replace - // Exclude varlock from dep optimization (Vite 5 top-level config). - // Vite 6+ environments are handled by configEnvironment / configResolved below. - addVarlockOptimizeDepsExclude(config); - config.ssr ??= {}; - addVarlockOptimizeDepsExclude(config.ssr); - if (!configIsValid) { if (isDevCommand) { // adjust vite's setting so it doesnt bury the error messages @@ -261,27 +243,10 @@ See https://varlock.dev/integrations/vite/ for more details. } } }, - // Vite 7+ hook: runs for each environment (client, ssr, worker, etc.) - configEnvironment(_name: string, envConfig: any) { - addVarlockOptimizeDepsExclude(envConfig); - envConfig.dev ??= {}; - addVarlockOptimizeDepsExclude(envConfig.dev); - }, // hook to observe/modify config after it is resolved configResolved(config) { debug('vite plugin - configResolved fn called'); - // Patch per-environment optimizeDeps for Vite 6 (which lacks the - // `configEnvironment` hook). The resolved config is technically frozen, - // but `optimizeDeps` objects within environments are still mutable. - if ((config as any).environments) { - for (const envName of Object.keys((config as any).environments)) { - const envConf = (config as any).environments[envName]; - if (envConf?.dev?.optimizeDeps) addVarlockOptimizeDepsExclude(envConf.dev); - if (envConf?.optimizeDeps) addVarlockOptimizeDepsExclude(envConf); - } - } - if (!varlockLoadedEnv) return; // inject all .env files that varlock loaded into `configFileDependencies` // so that vite will watch them and reload if they change From 27ac8ac7429ea7ee2750a8adf18b9657a9a3f051 Mon Sep 17 00:00:00 2001 From: Theo Ephraim Date: Tue, 5 May 2026 11:07:49 -0700 Subject: [PATCH 14/17] fix: log stdout on install failure for better CI debugging --- framework-tests/harness/test-fixture.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/framework-tests/harness/test-fixture.ts b/framework-tests/harness/test-fixture.ts index b8f3403d..39858d7b 100644 --- a/framework-tests/harness/test-fixture.ts +++ b/framework-tests/harness/test-fixture.ts @@ -193,7 +193,7 @@ export class FrameworkTestEnv { }); if (!installResult.success) { - console.error(`[${this.label}] Install failed:\n${installResult.stderr}`); + console.error(`[${this.label}] Install failed (exit code ${installResult.exitCode}):\n${installResult.stderr || installResult.stdout || '(no output)'}`); throw new Error(`Dependency installation failed for fixture "${this.label}"`); } console.log(`[${this.label}] Dependencies installed successfully`); From 56b906f92e19d269d367959ef17d55ef5fbca5e8 Mon Sep 17 00:00:00 2001 From: Theo Ephraim Date: Tue, 5 May 2026 12:45:07 -0700 Subject: [PATCH 15/17] chore: add astro integration to bump file --- .bumpy/fix-cf-tanstack-vite-compat.md | 1 + 1 file changed, 1 insertion(+) diff --git a/.bumpy/fix-cf-tanstack-vite-compat.md b/.bumpy/fix-cf-tanstack-vite-compat.md index f8cdfab4..d24c0597 100644 --- a/.bumpy/fix-cf-tanstack-vite-compat.md +++ b/.bumpy/fix-cf-tanstack-vite-compat.md @@ -1,6 +1,7 @@ --- "@varlock/vite-integration": patch "@varlock/cloudflare-integration": patch +"@varlock/astro-integration": patch --- fix cloudflare + tanstack start + vite 6/7/8 compatibility From 159ddd24aa190abbcdcca501ca98f14bd890e74c Mon Sep 17 00:00:00 2001 From: Theo Ephraim Date: Tue, 5 May 2026 13:21:56 -0700 Subject: [PATCH 16/17] chore: upgrade bumpy to v1.8, add cascadeFrom for astro and cloudflare --- .bumpy/_config.json | 6 ++++++ .bumpy/fix-cf-tanstack-vite-compat.md | 1 - bun.lock | 6 ++---- package.json | 4 +--- 4 files changed, 9 insertions(+), 8 deletions(-) diff --git a/.bumpy/_config.json b/.bumpy/_config.json index 608546f2..e85efab4 100644 --- a/.bumpy/_config.json +++ b/.bumpy/_config.json @@ -12,6 +12,12 @@ "skipNpmPublish": true, "buildCommand": "bun run package", "publishCommand": "bun run publish:vsce && bun run publish:ovsx" + }, + "@varlock/astro-integration": { + "cascadeFrom": ["@varlock/vite-integration"] + }, + "@varlock/cloudflare-integration": { + "cascadeFrom": ["@varlock/vite-integration"] } } } diff --git a/.bumpy/fix-cf-tanstack-vite-compat.md b/.bumpy/fix-cf-tanstack-vite-compat.md index d24c0597..f8cdfab4 100644 --- a/.bumpy/fix-cf-tanstack-vite-compat.md +++ b/.bumpy/fix-cf-tanstack-vite-compat.md @@ -1,7 +1,6 @@ --- "@varlock/vite-integration": patch "@varlock/cloudflare-integration": patch -"@varlock/astro-integration": patch --- fix cloudflare + tanstack start + vite 6/7/8 compatibility diff --git a/bun.lock b/bun.lock index 35485aff..0948daec 100644 --- a/bun.lock +++ b/bun.lock @@ -11,9 +11,7 @@ "@types/node": "catalog:", "@typescript-eslint/eslint-plugin": "^8.56.1", "@typescript-eslint/parser": "^8.56.1", - "@varlock/bumpy": "^1.7.0", - "@varlock/cloudflare-integration": "workspace:*", - "@varlock/keepass-plugin": "workspace:*", + "@varlock/bumpy": "^1.8.0", "@varlock/tsconfig": "workspace:*", "eslint": "^10.0.2", "eslint-plugin-es-x": "^9.5.0", @@ -1332,7 +1330,7 @@ "@varlock/bitwarden-plugin": ["@varlock/bitwarden-plugin@workspace:packages/plugins/bitwarden"], - "@varlock/bumpy": ["@varlock/bumpy@1.7.0", "", { "bin": { "bumpy": "dist/cli.mjs" } }, "sha512-FYsjPJlnQufwkfstMVh2G2bvaa7bctX97Qgisc3NyC063U3AtPT05sGAEESf45DS+3jT7IXuEjeUsxXNAFtatA=="], + "@varlock/bumpy": ["@varlock/bumpy@1.8.0", "", { "bin": { "bumpy": "dist/cli.mjs" } }, "sha512-fpq3nT+5DWQfP1i0kUmtMiZ/94ev5fGQj98wQiCRKfeihscYaUF0rapCpouS+qGPrXl7AOHeibRCTQW1po0Tdw=="], "@varlock/ci-env-info": ["@varlock/ci-env-info@workspace:packages/ci-env-info"], diff --git a/package.json b/package.json index 4c886f91..380f5a22 100644 --- a/package.json +++ b/package.json @@ -30,15 +30,13 @@ "prepare": "lefthook install" }, "devDependencies": { - "@varlock/bumpy": "^1.7.0", + "@varlock/bumpy": "^1.8.0", "@cloudflare/vite-plugin": "^1.30.1", "@eslint/js": "^10.0.1", "@stylistic/eslint-plugin": "^5.9.0", "@types/node": "catalog:", "@typescript-eslint/eslint-plugin": "^8.56.1", "@typescript-eslint/parser": "^8.56.1", - "@varlock/cloudflare-integration": "workspace:*", - "@varlock/keepass-plugin": "workspace:*", "@varlock/tsconfig": "workspace:*", "eslint": "^10.0.2", "eslint-plugin-es-x": "^9.5.0", From 874e5868e699dd599020109dddeef6a5ae1899f0 Mon Sep 17 00:00:00 2001 From: Theo Ephraim Date: Tue, 5 May 2026 13:40:39 -0700 Subject: [PATCH 17/17] fix: close stdin in test command runner to prevent hanging on prompts --- framework-tests/harness/command-runner.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/framework-tests/harness/command-runner.ts b/framework-tests/harness/command-runner.ts index 3658e2ab..26e79fae 100644 --- a/framework-tests/harness/command-runner.ts +++ b/framework-tests/harness/command-runner.ts @@ -15,6 +15,9 @@ export function runCommand( const child = spawn(command, { cwd, shell: true, + // Ignore stdin so commands that prompt for input get EOF immediately + // instead of hanging (e.g. wrangler telemetry consent). + stdio: ['ignore', 'pipe', 'pipe'], env: { ...process.env, // Disable corepack so it doesn't reject pnpm/npm when the repo root