Skip to content

Commit

Permalink
feat: add support for automatic session driver config (#13145)
Browse files Browse the repository at this point in the history
* feat: add support for automatic session driver config

* chore: fix error logic

* Lint test

* Add node support

* Add node test fixture

* Lock

* Add Netlify support

* Use workspace Astro version

* Format

* Changeset

* Add tests

* Add dep for tests

* chore: fix repo URL

* temp log

* Fix module resoltuion

* [skip ci] Update changeset

* chore: bump peer dependencies

* Changes from review

* Changeset changes from review

* Apply suggestions from code review

Co-authored-by: Sarah Rainsberger <[email protected]>

* More changeset detail

* Lock

---------

Co-authored-by: Sarah Rainsberger <[email protected]>
  • Loading branch information
ascorbic and sarah11918 authored Feb 12, 2025
1 parent 3b66955 commit 8d4e566
Show file tree
Hide file tree
Showing 44 changed files with 1,184 additions and 137 deletions.
9 changes: 9 additions & 0 deletions .changeset/cool-deers-join.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
'@astrojs/node': minor
---

Automatically configures filesystem storage when experimental session enabled

If the `experimental.session` flag is enabled when using the Node adapter, Astro will automatically configure session storage using the filesystem driver. You can still manually configure session storage if you need to use a different driver or want to customize the session storage configuration.

See [the experimental session docs](https://docs.astro.build/en/reference/experimental-flags/sessions/) for more information on configuring session storage.
7 changes: 7 additions & 0 deletions .changeset/tame-games-enjoy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'astro': minor
---

Adds support for adapters auto-configuring experimental session storage drivers.

Adapters can now configure a default session storage driver when the `experimental.session` flag is enabled. If a hosting platform has a storage primitive that can be used for session storage, the adapter can automatically configure the session storage using that driver. This allows Astro to provide a more seamless experience for users who want to use sessions without needing to manually configure the session storage.
9 changes: 9 additions & 0 deletions .changeset/thin-cobras-glow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
'@astrojs/netlify': minor
---

Automatically configures Netlify Blobs storage when experimental session enabled

If the `experimental.session` flag is enabled when using the Netlify adapter, Astro will automatically configure the session storage using the Netlify Blobs driver. You can still manually configure the session storage if you need to use a different driver or want to customize the session storage configuration.

See [the experimental session docs](https://docs.astro.build/en/reference/experimental-flags/sessions/) for more information on configuring session storage.
44 changes: 44 additions & 0 deletions .changeset/tricky-insects-argue.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
---
'astro': patch
---

:warning: **BREAKING CHANGE FOR EXPERIMENTAL SESSIONS ONLY** :warning:

Changes the `experimental.session` option to a boolean flag and moves session config to a top-level value. This change is to allow the new automatic session driver support. You now need to separately enable the `experimental.session` flag, and then configure the session driver using the top-level `session` key if providing manual configuration.

```diff
defineConfig({
// ...
experimental: {
- session: {
- driver: 'upstash',
- },
+ session: true,
},
+ session: {
+ driver: 'upstash',
+ },
});
```

You no longer need to configure a session driver if you are using an adapter that supports automatic session driver configuration and wish to use its default settings.

```diff
defineConfig({
adapter: node({
mode: "standalone",
}),
experimental: {
- session: {
- driver: 'fs',
- cookie: 'astro-cookie',
- },
+ session: true,
},
+ session: {
+ cookie: 'astro-cookie',
+ },
});
```

However, you can still manually configure additional driver options or choose a non-default driver to use with your adapter with the new top-level `session` config option. For more information, see the [experimental session docs](https://docs.astro.build/en/reference/experimental-flags/sessions/).
6 changes: 2 additions & 4 deletions packages/astro/src/core/build/plugins/plugin-manifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,7 @@ function vitePluginManifest(options: StaticBuildOptions, internals: BuildInterna
`import { _privateSetManifestDontUseThis } from 'astro:ssr-manifest'`,
];

const resolvedDriver = await resolveSessionDriver(
options.settings.config.experimental?.session?.driver,
);
const resolvedDriver = await resolveSessionDriver(options.settings.config.session?.driver);

const contents = [
`const manifest = _deserializeManifest('${manifestReplace}');`,
Expand Down Expand Up @@ -304,6 +302,6 @@ function buildManifest(
(settings.config.security?.checkOrigin && settings.buildOutput === 'server') ?? false,
serverIslandNameMap: Array.from(settings.serverIslandNameMap),
key: encodedKey,
sessionConfig: settings.config.experimental.session,
sessionConfig: settings.config.session,
};
}
53 changes: 27 additions & 26 deletions packages/astro/src/core/config/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,12 +92,14 @@ export const ASTRO_CONFIG_DEFAULTS = {
schema: {},
validateSecrets: false,
},
session: undefined,
experimental: {
clientPrerender: false,
contentIntellisense: false,
responsiveImages: false,
svg: false,
serializeConfig: false,
session: false,
},
} satisfies AstroUserConfig & { server: { open: boolean } };

Expand Down Expand Up @@ -522,6 +524,30 @@ export const AstroConfigSchema = z.object({
.strict()
.optional()
.default(ASTRO_CONFIG_DEFAULTS.env),
session: z
.object({
driver: z.string(),
options: z.record(z.any()).optional(),
cookie: z
.object({
name: z.string().optional(),
domain: z.string().optional(),
path: z.string().optional(),
maxAge: z.number().optional(),
sameSite: z.union([z.enum(['strict', 'lax', 'none']), z.boolean()]).optional(),
secure: z.boolean().optional(),
})
.or(z.string())
.transform((val) => {
if (typeof val === 'string') {
return { name: val };
}
return val;
})
.optional(),
ttl: z.number().optional(),
})
.optional(),
experimental: z
.object({
clientPrerender: z
Expand All @@ -536,32 +562,7 @@ export const AstroConfigSchema = z.object({
.boolean()
.optional()
.default(ASTRO_CONFIG_DEFAULTS.experimental.responsiveImages),
session: z
.object({
driver: z.string(),
options: z.record(z.any()).optional(),
cookie: z
.union([
z.object({
name: z.string().optional(),
domain: z.string().optional(),
path: z.string().optional(),
maxAge: z.number().optional(),
sameSite: z.union([z.enum(['strict', 'lax', 'none']), z.boolean()]).optional(),
secure: z.boolean().optional(),
}),
z.string(),
])
.transform((val) => {
if (typeof val === 'string') {
return { name: val };
}
return val;
})
.optional(),
ttl: z.number().optional(),
})
.optional(),
session: z.boolean().optional(),
svg: z
.union([
z.boolean(),
Expand Down
116 changes: 84 additions & 32 deletions packages/astro/src/core/errors/errors-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -881,38 +881,6 @@ export const AstroResponseHeadersReassigned = {
hint: 'Consider using `Astro.response.headers.add()`, and `Astro.response.headers.delete()`.',
} satisfies ErrorData;

/**
* @docs
* @message Error when initializing session storage with driver `DRIVER`. `ERROR`
* @see
* - [experimental.session](https://docs.astro.build/en/reference/experimental-flags/sessions/)
* @description
* Thrown when the session storage could not be initialized.
*/
export const SessionStorageInitError = {
name: 'SessionStorageInitError',
title: 'Session storage could not be initialized.',
message: (error: string, driver?: string) =>
`Error when initializing session storage${driver ? ` with driver \`${driver}\`` : ''}. \`${error ?? ''}\``,
hint: 'For more information, see https://docs.astro.build/en/reference/experimental-flags/sessions/',
} satisfies ErrorData;

/**
* @docs
* @message Error when saving session data with driver `DRIVER`. `ERROR`
* @see
* - [experimental.session](https://docs.astro.build/en/reference/experimental-flags/sessions/)
* @description
* Thrown when the session data could not be saved.
*/
export const SessionStorageSaveError = {
name: 'SessionStorageSaveError',
title: 'Session data could not be saved.',
message: (error: string, driver?: string) =>
`Error when saving session data${driver ? ` with driver \`${driver}\`` : ''}. \`${error ?? ''}\``,
hint: 'For more information, see https://docs.astro.build/en/reference/experimental-flags/sessions/',
} satisfies ErrorData;

/**
* @docs
* @description
Expand Down Expand Up @@ -1838,6 +1806,90 @@ export const ActionCalledFromServerError = {
// Generic catch-all - Only use this in extreme cases, like if there was a cosmic ray bit flip.
export const UnknownError = { name: 'UnknownError', title: 'Unknown Error.' } satisfies ErrorData;

/**
* @docs
* @kind heading
* @name Session Errors
*/
// Session Errors
/**
* @docs
* @see
* - [On-demand rendering](https://docs.astro.build/en/guides/on-demand-rendering/)
* @description
* Your project must have a server output to use sessions.
*/
export const SessionWithoutServerOutputError = {
name: 'SessionWithoutServerOutputError',
title: 'Sessions must be used with server output.',
message:
'A server is required to use sessions. To deploy routes to a server, add an adapter to your Astro config and configure your route for on-demand rendering',
hint: 'Add an adapter and enable on-demand rendering: https://docs.astro.build/en/guides/on-demand-rendering/',
} satisfies ErrorData;

/**
* @docs
* @message Error when initializing session storage with driver `DRIVER`. `ERROR`
* @see
* - [experimental.session](https://docs.astro.build/en/reference/experimental-flags/sessions/)
* @description
* Thrown when the session storage could not be initialized.
*/
export const SessionStorageInitError = {
name: 'SessionStorageInitError',
title: 'Session storage could not be initialized.',
message: (error: string, driver?: string) =>
`Error when initializing session storage${driver ? ` with driver \`${driver}\`` : ''}. \`${error ?? ''}\``,
hint: 'For more information, see https://docs.astro.build/en/reference/experimental-flags/sessions/',
} satisfies ErrorData;

/**
* @docs
* @message Error when saving session data with driver `DRIVER`. `ERROR`
* @see
* - [experimental.session](https://docs.astro.build/en/reference/experimental-flags/sessions/)
* @description
* Thrown when the session data could not be saved.
*/
export const SessionStorageSaveError = {
name: 'SessionStorageSaveError',
title: 'Session data could not be saved.',
message: (error: string, driver?: string) =>
`Error when saving session data${driver ? ` with driver \`${driver}\`` : ''}. \`${error ?? ''}\``,
hint: 'For more information, see https://docs.astro.build/en/reference/experimental-flags/sessions/',
} satisfies ErrorData;

/**
* @docs
* @message The `experimental.session` flag was set to `true`, but no storage was configured. Either configure the storage manually or use an adapter that provides session storage
* @see
* - [experimental.session](https://docs.astro.build/en/reference/experimental-flags/sessions/)
* @description
* Thrown when session storage is enabled but not configured.
*/
export const SessionConfigMissingError = {
name: 'SessionConfigMissingError',
title: 'Session storage was enabled but not configured.',
message:
'The `experimental.session` flag was set to `true`, but no storage was configured. Either configure the storage manually or use an adapter that provides session storage',
hint: 'See https://docs.astro.build/en/reference/experimental-flags/sessions/',
} satisfies ErrorData;

/**
* @docs
* @message Session config was provided without enabling the `experimental.session` flag
* @see
* - [experimental.session](https://docs.astro.build/en/reference/experimental-flags/sessions/)
* @description
* Thrown when session storage is configured but the `experimental.session` flag is not enabled.
*/
export const SessionConfigWithoutFlagError = {
name: 'SessionConfigWithoutFlagError',
title: 'Session flag not set',
message: 'Session config was provided without enabling the `experimental.session` flag',
hint: 'See https://docs.astro.build/en/reference/experimental-flags/sessions/',
} satisfies ErrorData;

/*
* Adding an error? Follow these steps:
* 1. Determine in which category it belongs (Astro, Vite, CSS, Content Collections etc.)
Expand Down
45 changes: 38 additions & 7 deletions packages/astro/src/core/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,21 @@ import {
builtinDrivers,
createStorage,
} from 'unstorage';
import type { AstroSettings } from '../types/astro.js';
import type {
ResolvedSessionConfig,
SessionConfig,
SessionDriverName,
} from '../types/public/config.js';
import type { AstroCookies } from './cookies/cookies.js';
import type { AstroCookieSetOptions } from './cookies/cookies.js';
import { SessionStorageInitError, SessionStorageSaveError } from './errors/errors-data.js';
import {
SessionConfigMissingError,
SessionConfigWithoutFlagError,
SessionStorageInitError,
SessionStorageSaveError,
SessionWithoutServerOutputError,
} from './errors/errors-data.js';
import { AstroError } from './errors/index.js';

export const PERSIST_SYMBOL = Symbol();
Expand Down Expand Up @@ -462,15 +469,39 @@ export class AstroSession<TDriver extends SessionDriverName = any> {
}
}
// TODO: make this sync when we drop support for Node < 18.19.0
export function resolveSessionDriver(driver: string | undefined): Promise<string> | string | null {
export async function resolveSessionDriver(driver: string | undefined): Promise<string | null> {
if (!driver) {
return null;
}
if (driver === 'fs') {
return import.meta.resolve(builtinDrivers.fsLite);
}
if (driver in builtinDrivers) {
return import.meta.resolve(builtinDrivers[driver as keyof typeof builtinDrivers]);
try {
if (driver === 'fs') {
return await import.meta.resolve(builtinDrivers.fsLite);
}
if (driver in builtinDrivers) {
return await import.meta.resolve(builtinDrivers[driver as keyof typeof builtinDrivers]);
}
} catch {
return null;
}

return driver;
}

export function validateSessionConfig(settings: AstroSettings): void {
const { experimental, session } = settings.config;
const { buildOutput } = settings;
let error: AstroError | undefined;
if (experimental.session) {
if (!session?.driver) {
error = new AstroError(SessionConfigMissingError);
} else if (buildOutput === 'static') {
error = new AstroError(SessionWithoutServerOutputError);
}
} else if (session?.driver) {
error = new AstroError(SessionConfigWithoutFlagError);
}
if (error) {
error.stack = undefined;
throw error;
}
}
6 changes: 6 additions & 0 deletions packages/astro/src/integrations/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { buildClientDirectiveEntrypoint } from '../core/client-directive/index.j
import { mergeConfig } from '../core/config/index.js';
import { validateSetAdapter } from '../core/dev/adapter-validation.js';
import type { AstroIntegrationLogger, Logger } from '../core/logger/core.js';
import { validateSessionConfig } from '../core/session.js';
import type { AstroSettings } from '../types/astro.js';
import type { AstroConfig } from '../types/public/config.js';
import type {
Expand Down Expand Up @@ -369,6 +370,11 @@ export async function runHookConfigDone({
});
}
}
// Session config is validated after all integrations have had a chance to
// register a default session driver, and we know the output type.
// This can't happen in the Zod schema because it that happens before adapters run
// and also doesn't know whether it's a server build or static build.
validateSessionConfig(settings);
}

export async function runHookServerSetup({
Expand Down
Loading

0 comments on commit 8d4e566

Please sign in to comment.