From c2856f4f7120bdc614d8771ee13e3b43bb6e9b3b Mon Sep 17 00:00:00 2001 From: Matthew Phillips Date: Thu, 30 Oct 2025 14:31:46 -0400 Subject: [PATCH 1/6] feat: full sessions support w/ environment api - create virtual module for cloudflare config (sessionKVBindingName) - handler dynamically accesses binding from env via virtual module - remove hardcoded SESSION binding name, now configurable - remove unused __env__ global - add test fixture with sessions form - sessions now fully integrated with astro environment api --- .../astro/src/core/session/vite-plugin.ts | 33 +++++++---- packages/integrations/cloudflare/src/index.ts | 11 ++-- .../cloudflare/src/utils/handler.ts | 19 +++---- .../integrations/cloudflare/src/virtual.d.ts | 4 ++ .../cloudflare/src/vite-plugin-config.ts | 24 ++++++++ .../vite-plugin/src/pages/sessions.astro | 57 ++++++++++++------- .../test/fixtures/vite-plugin/wrangler.jsonc | 8 ++- 7 files changed, 106 insertions(+), 50 deletions(-) create mode 100644 packages/integrations/cloudflare/src/virtual.d.ts create mode 100644 packages/integrations/cloudflare/src/vite-plugin-config.ts diff --git a/packages/astro/src/core/session/vite-plugin.ts b/packages/astro/src/core/session/vite-plugin.ts index bcec01d8b3cc..ece1fa473824 100644 --- a/packages/astro/src/core/session/vite-plugin.ts +++ b/packages/astro/src/core/session/vite-plugin.ts @@ -1,3 +1,4 @@ +import { fileURLToPath } from 'node:url'; import { type BuiltinDriverName, builtinDrivers } from 'unstorage'; import type { Plugin as VitePlugin } from 'vite'; import type { AstroSettings } from '../../types/astro.js'; @@ -12,25 +13,33 @@ export function vitePluginSessionDriver({ settings }: { settings: AstroSettings async resolveId(id) { if (id === VIRTUAL_SESSION_DRIVER_ID) { + return RESOLVED_VIRTUAL_SESSION_DRIVER_ID; + } + }, + + async load(id) { + if (id === RESOLVED_VIRTUAL_SESSION_DRIVER_ID) { if (settings.config.session) { + let sessionDriver: string; if (settings.config.session.driver === 'fs') { - return await this.resolve(builtinDrivers.fsLite); + sessionDriver = builtinDrivers.fsLite; + } else if (settings.config.session.driver && settings.config.session.driver in builtinDrivers) { + sessionDriver = builtinDrivers[settings.config.session.driver as BuiltinDriverName]; + } else { + return { code: 'export default null;' }; } - if (settings.config.session.driver && settings.config.session.driver in builtinDrivers) { - return await this.resolve( - builtinDrivers[settings.config.session.driver as BuiltinDriverName], - ); + const importerPath = fileURLToPath(import.meta.url); + const resolved = await this.resolve(sessionDriver, importerPath); + if (!resolved) { + throw new Error(`Failed to resolve session driver: ${sessionDriver}`); } + return { + code: `import { default as _default } from '${resolved.id}';\nexport * from '${resolved.id}';\nexport default _default;`, + }; } else { - return RESOLVED_VIRTUAL_SESSION_DRIVER_ID; + return { code: 'export default null;' }; } } }, - - async load(id) { - if (id === RESOLVED_VIRTUAL_SESSION_DRIVER_ID) { - return { code: 'export default null;' }; - } - }, }; } diff --git a/packages/integrations/cloudflare/src/index.ts b/packages/integrations/cloudflare/src/index.ts index 9c9bfc6e35fb..53578f5fb5bc 100644 --- a/packages/integrations/cloudflare/src/index.ts +++ b/packages/integrations/cloudflare/src/index.ts @@ -27,6 +27,7 @@ import { import { createGetEnv } from './utils/env.js'; import { createRoutesFile, getParts } from './utils/generate-routes-json.js'; import { type ImageService, setImageConfig } from './utils/image-config.js'; +import { createConfigPlugin } from './vite-plugin-config.js'; export type { Runtime } from './utils/handler.js'; @@ -247,6 +248,9 @@ export default function createIntegration(args?: Options): AstroIntegration { } }, }, + createConfigPlugin({ + sessionKVBindingName: SESSION_KV_BINDING_NAME, + }), ], }, image: setImageConfig(args?.imageService ?? 'compile', config.image, command, logger), @@ -316,9 +320,6 @@ export default function createIntegration(args?: Options): AstroIntegration { setProcessEnv(_config, platformProxy.env); - globalThis.__env__ ??= {}; - globalThis.__env__[SESSION_KV_BINDING_NAME] = platformProxy.env[SESSION_KV_BINDING_NAME]; - const clientLocalsSymbol = Symbol.for('astro.locals'); server.middlewares.use(async function middleware(req, _res, next) { @@ -385,10 +386,6 @@ export default function createIntegration(args?: Options): AstroIntegration { // in a global way, so we shim their access as `process.env.*`. This is not the recommended way for users to access environment variables. But we'll add this for compatibility for chosen variables. Mainly to support `@astrojs/db` vite.define = { 'process.env': 'process.env', - // Allows the request handler to know what the binding name is - 'globalThis.__ASTRO_SESSION_BINDING_NAME': JSON.stringify( - args?.sessionKVBindingName ?? 'SESSION', - ), 'globalThis.__ASTRO_IMAGES_BINDING_NAME': JSON.stringify( args?.imagesBindingName ?? 'IMAGES', ), diff --git a/packages/integrations/cloudflare/src/utils/handler.ts b/packages/integrations/cloudflare/src/utils/handler.ts index d5f4bea6b84d..9c2c9fed5586 100644 --- a/packages/integrations/cloudflare/src/utils/handler.ts +++ b/packages/integrations/cloudflare/src/utils/handler.ts @@ -9,6 +9,7 @@ import type { import { createApp } from 'astro/app/entrypoint'; import { setGetEnv } from 'astro/env/setup'; import { createGetEnv } from '../utils/env.js'; +import { sessionKVBindingName } from 'virtual:astro-cloudflare:config'; export type Env = { [key: string]: unknown; @@ -27,14 +28,8 @@ export interface Runtime { } declare global { - // This is not a real global, but is injected using Vite define to allow us to specify the session binding name in the config. - var __ASTRO_SESSION_BINDING_NAME: string; - // This is not a real global, but is injected using Vite define to allow us to specify the Images binding name in the config. var __ASTRO_IMAGES_BINDING_NAME: string; - - // Just used to pass the KV binding to unstorage. - var __env__: Partial; } export async function handle( @@ -44,11 +39,13 @@ export async function handle( ): Promise { const app = createApp(import.meta.env.DEV); const { pathname } = new URL(request.url); - const bindingName = globalThis.__ASTRO_SESSION_BINDING_NAME; - // Assigning the KV binding to globalThis allows unstorage to access it for session storage. - // unstorage checks in globalThis and globalThis.__env__ for the binding. - globalThis.__env__ ??= {}; - globalThis.__env__[bindingName] = env[bindingName]; + + if(env[sessionKVBindingName]) { + const sessionConfigOptions = app.manifest.sessionConfig?.options ?? {}; + Object.assign(sessionConfigOptions, { + binding: env[sessionKVBindingName] + }); + } // static assets fallback, in case default _routes.json is not used if (app.manifest.assets.has(pathname)) { diff --git a/packages/integrations/cloudflare/src/virtual.d.ts b/packages/integrations/cloudflare/src/virtual.d.ts new file mode 100644 index 000000000000..afc8ce716a0a --- /dev/null +++ b/packages/integrations/cloudflare/src/virtual.d.ts @@ -0,0 +1,4 @@ +declare module 'virtual:astro-cloudflare:config' { + export const sessionKVBindingName: string; + // Additional exports can be added here in the future +} diff --git a/packages/integrations/cloudflare/src/vite-plugin-config.ts b/packages/integrations/cloudflare/src/vite-plugin-config.ts new file mode 100644 index 000000000000..4c30f819959b --- /dev/null +++ b/packages/integrations/cloudflare/src/vite-plugin-config.ts @@ -0,0 +1,24 @@ +import type { PluginOption } from 'vite'; + +const VIRTUAL_CONFIG_ID = 'virtual:astro-cloudflare:config'; +const RESOLVED_VIRTUAL_CONFIG_ID = '\0' + VIRTUAL_CONFIG_ID; + +export interface CloudflareConfig { + sessionKVBindingName: string; +} + +export function createConfigPlugin(config: CloudflareConfig): PluginOption { + return { + name: 'vite:astro-cloudflare-config', + resolveId(id) { + if (id === VIRTUAL_CONFIG_ID) { + return RESOLVED_VIRTUAL_CONFIG_ID; + } + }, + load(id) { + if (id === RESOLVED_VIRTUAL_CONFIG_ID) { + return `export const sessionKVBindingName = ${JSON.stringify(config.sessionKVBindingName)};`; + } + }, + }; +} diff --git a/packages/integrations/cloudflare/test/fixtures/vite-plugin/src/pages/sessions.astro b/packages/integrations/cloudflare/test/fixtures/vite-plugin/src/pages/sessions.astro index bab9ec21456b..4f711ad2148e 100644 --- a/packages/integrations/cloudflare/test/fixtures/vite-plugin/src/pages/sessions.astro +++ b/packages/integrations/cloudflare/test/fixtures/vite-plugin/src/pages/sessions.astro @@ -1,25 +1,44 @@ --- export const prerender = false; // Not needed with 'server' output const cart = await Astro.session?.get('cart'); -const user = await Astro.session?.get('user'); +let user = await Astro.session?.get('user'); + +if(Astro.request.method === 'POST') { + const data = await Astro.request.formData(); + const newId = data.get('id'); + const newName = data.get('name'); + + if(newId && newName) { + user = { + id: newId, + name: newName + }; + Astro.session?.set('user', user); + } +} --- -

- Sessions -

-

- Cart -

-

- 🛒 {cart?.length ?? 0} items -

+ + + + Sessions + + +

Sessions

+

Cart

+

+ 🛒 {cart?.length ?? 0} items +

+ +

User

+

Id: {user?.id}

+

Name: {user?.name}

-

- User -

-

- Id: {user?.id} -

-

- Name: {user?.name} -

+
+

Change

+ + + +
+ + diff --git a/packages/integrations/cloudflare/test/fixtures/vite-plugin/wrangler.jsonc b/packages/integrations/cloudflare/test/fixtures/vite-plugin/wrangler.jsonc index e79911e4b422..11c3f2f3fa3d 100644 --- a/packages/integrations/cloudflare/test/fixtures/vite-plugin/wrangler.jsonc +++ b/packages/integrations/cloudflare/test/fixtures/vite-plugin/wrangler.jsonc @@ -5,7 +5,7 @@ "global_fetch_strictly_public" ], "name": "astro-cloudflare-custom-entryfile", - "main": "dist/_worker.js/index.js", + "main": "@astrojs/cloudflare/entrypoints/server", "assets": { "directory": "./dist", "binding": "ASSETS" @@ -13,6 +13,12 @@ "images": { "binding": "IMAGES" }, + "kv_namespaces": [ + { + "binding": "SESSION", + "id": "SESSION" + } + ], "observability": { "logs": { "enabled": true, From 9af11de75cf31e9facb1797dc93dce3e9c73eb27 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Thu, 30 Oct 2025 15:10:02 -0400 Subject: [PATCH 2/6] fix: organize imports and formatting in cloudflare handler (#14697) * Initial plan * fix: organize imports and formatting in handler.ts Co-authored-by: matthewp <361671+matthewp@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: matthewp <361671+matthewp@users.noreply.github.com> --- packages/integrations/cloudflare/src/utils/handler.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/integrations/cloudflare/src/utils/handler.ts b/packages/integrations/cloudflare/src/utils/handler.ts index 9c2c9fed5586..ed86826fd962 100644 --- a/packages/integrations/cloudflare/src/utils/handler.ts +++ b/packages/integrations/cloudflare/src/utils/handler.ts @@ -1,5 +1,6 @@ // @ts-expect-error - It is safe to expect the error here. import { env as globalEnv } from 'cloudflare:workers'; +import { sessionKVBindingName } from 'virtual:astro-cloudflare:config'; import type { Response as CfResponse, CacheStorage as CloudflareCacheStorage, @@ -9,7 +10,6 @@ import type { import { createApp } from 'astro/app/entrypoint'; import { setGetEnv } from 'astro/env/setup'; import { createGetEnv } from '../utils/env.js'; -import { sessionKVBindingName } from 'virtual:astro-cloudflare:config'; export type Env = { [key: string]: unknown; @@ -40,10 +40,10 @@ export async function handle( const app = createApp(import.meta.env.DEV); const { pathname } = new URL(request.url); - if(env[sessionKVBindingName]) { + if (env[sessionKVBindingName]) { const sessionConfigOptions = app.manifest.sessionConfig?.options ?? {}; Object.assign(sessionConfigOptions, { - binding: env[sessionKVBindingName] + binding: env[sessionKVBindingName], }); } From 0ed30fbfb1ab4c19f86fedfba82cc86e08186008 Mon Sep 17 00:00:00 2001 From: Matthew Phillips Date: Thu, 30 Oct 2025 15:18:34 -0400 Subject: [PATCH 3/6] fix: make CloudflareConfig interface internal --- packages/integrations/cloudflare/src/vite-plugin-config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/integrations/cloudflare/src/vite-plugin-config.ts b/packages/integrations/cloudflare/src/vite-plugin-config.ts index 4c30f819959b..b2d20b99599b 100644 --- a/packages/integrations/cloudflare/src/vite-plugin-config.ts +++ b/packages/integrations/cloudflare/src/vite-plugin-config.ts @@ -3,7 +3,7 @@ import type { PluginOption } from 'vite'; const VIRTUAL_CONFIG_ID = 'virtual:astro-cloudflare:config'; const RESOLVED_VIRTUAL_CONFIG_ID = '\0' + VIRTUAL_CONFIG_ID; -export interface CloudflareConfig { +interface CloudflareConfig { sessionKVBindingName: string; } From cf02c11dadc8f735c11dfb984b37651d958e592f Mon Sep 17 00:00:00 2001 From: Matthew Phillips Date: Fri, 31 Oct 2025 08:34:18 -0400 Subject: [PATCH 4/6] fix: use AstroError for session driver resolution failures --- packages/astro/src/core/session/vite-plugin.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/astro/src/core/session/vite-plugin.ts b/packages/astro/src/core/session/vite-plugin.ts index ece1fa473824..f43b27e7811e 100644 --- a/packages/astro/src/core/session/vite-plugin.ts +++ b/packages/astro/src/core/session/vite-plugin.ts @@ -1,6 +1,8 @@ import { fileURLToPath } from 'node:url'; import { type BuiltinDriverName, builtinDrivers } from 'unstorage'; import type { Plugin as VitePlugin } from 'vite'; +import { AstroError } from '../errors/index.js'; +import { SessionStorageInitError } from '../errors/errors-data.js'; import type { AstroSettings } from '../../types/astro.js'; const VIRTUAL_SESSION_DRIVER_ID = 'virtual:astro:session-driver'; @@ -31,7 +33,13 @@ export function vitePluginSessionDriver({ settings }: { settings: AstroSettings const importerPath = fileURLToPath(import.meta.url); const resolved = await this.resolve(sessionDriver, importerPath); if (!resolved) { - throw new Error(`Failed to resolve session driver: ${sessionDriver}`); + throw new AstroError({ + ...SessionStorageInitError, + message: SessionStorageInitError.message( + `Failed to resolve session driver: ${sessionDriver}`, + settings.config.session.driver, + ), + }); } return { code: `import { default as _default } from '${resolved.id}';\nexport * from '${resolved.id}';\nexport default _default;`, From ddd8e794f8c6485ddf6f759c343d7b9e2af9bc1c Mon Sep 17 00:00:00 2001 From: Matthew Phillips Date: Fri, 31 Oct 2025 09:14:23 -0400 Subject: [PATCH 5/6] fix: organize imports in session vite plugin --- packages/astro/src/core/session/vite-plugin.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/astro/src/core/session/vite-plugin.ts b/packages/astro/src/core/session/vite-plugin.ts index f43b27e7811e..d37499bc79c4 100644 --- a/packages/astro/src/core/session/vite-plugin.ts +++ b/packages/astro/src/core/session/vite-plugin.ts @@ -1,9 +1,10 @@ import { fileURLToPath } from 'node:url'; + import { type BuiltinDriverName, builtinDrivers } from 'unstorage'; import type { Plugin as VitePlugin } from 'vite'; -import { AstroError } from '../errors/index.js'; -import { SessionStorageInitError } from '../errors/errors-data.js'; import type { AstroSettings } from '../../types/astro.js'; +import { SessionStorageInitError } from '../errors/errors-data.js'; +import { AstroError } from '../errors/index.js'; const VIRTUAL_SESSION_DRIVER_ID = 'virtual:astro:session-driver'; const RESOLVED_VIRTUAL_SESSION_DRIVER_ID = '\0' + VIRTUAL_SESSION_DRIVER_ID; @@ -25,7 +26,10 @@ export function vitePluginSessionDriver({ settings }: { settings: AstroSettings let sessionDriver: string; if (settings.config.session.driver === 'fs') { sessionDriver = builtinDrivers.fsLite; - } else if (settings.config.session.driver && settings.config.session.driver in builtinDrivers) { + } else if ( + settings.config.session.driver && + settings.config.session.driver in builtinDrivers + ) { sessionDriver = builtinDrivers[settings.config.session.driver as BuiltinDriverName]; } else { return { code: 'export default null;' }; From d6dfa8147001224de4333945aff1fa2f34767da7 Mon Sep 17 00:00:00 2001 From: Matthew Phillips Date: Fri, 31 Oct 2025 09:48:59 -0400 Subject: [PATCH 6/6] fix: move virtual.d.ts to package root --- packages/integrations/cloudflare/tsconfig.json | 2 +- packages/integrations/cloudflare/{src => }/virtual.d.ts | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename packages/integrations/cloudflare/{src => }/virtual.d.ts (100%) diff --git a/packages/integrations/cloudflare/tsconfig.json b/packages/integrations/cloudflare/tsconfig.json index 1504b4b6dfa4..ea1c17c2952b 100644 --- a/packages/integrations/cloudflare/tsconfig.json +++ b/packages/integrations/cloudflare/tsconfig.json @@ -1,6 +1,6 @@ { "extends": "../../../tsconfig.base.json", - "include": ["src"], + "include": ["src", "virtual.d.ts"], "compilerOptions": { "outDir": "./dist" } diff --git a/packages/integrations/cloudflare/src/virtual.d.ts b/packages/integrations/cloudflare/virtual.d.ts similarity index 100% rename from packages/integrations/cloudflare/src/virtual.d.ts rename to packages/integrations/cloudflare/virtual.d.ts