Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add support for automatic session driver config #13145

Merged
merged 28 commits into from
Feb 12, 2025
Merged
Show file tree
Hide file tree
Changes from 27 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
dd09093
feat: add support for automatic session driver config
ascorbic Feb 6, 2025
aa98169
chore: fix error logic
ascorbic Feb 6, 2025
154eb97
Lint test
ascorbic Feb 6, 2025
8dff1a1
Merge branch 'main' into session-config2
ascorbic Feb 6, 2025
bc4de01
Add node support
ascorbic Feb 6, 2025
4c3ded8
Add node test fixture
ascorbic Feb 6, 2025
d22222a
Merge branch 'main' into session-config2
ascorbic Feb 6, 2025
e1e2328
Lock
ascorbic Feb 6, 2025
f0545d0
Add Netlify support
ascorbic Feb 6, 2025
680f928
Use workspace Astro version
ascorbic Feb 6, 2025
bb76d36
Format
ascorbic Feb 6, 2025
be1e237
Changeset
ascorbic Feb 6, 2025
78cd5eb
Merge branch 'main' into session-config2
ascorbic Feb 7, 2025
e422b63
Add tests
ascorbic Feb 7, 2025
41e0474
Add dep for tests
ascorbic Feb 7, 2025
c9c63a6
Merge branch 'main' into session-config2
ascorbic Feb 7, 2025
304303e
chore: fix repo URL
ascorbic Feb 7, 2025
db142f0
temp log
ascorbic Feb 7, 2025
3bb038e
Fix module resoltuion
ascorbic Feb 7, 2025
f6223c4
Merge branch 'main' into session-config2
ascorbic Feb 7, 2025
3ddeaf0
[skip ci] Update changeset
ascorbic Feb 7, 2025
d0fac27
chore: bump peer dependencies
ascorbic Feb 10, 2025
704fe3e
Changes from review
ascorbic Feb 10, 2025
db89093
Changeset changes from review
ascorbic Feb 10, 2025
7d4f1da
Apply suggestions from code review
ascorbic Feb 12, 2025
1801e55
More changeset detail
ascorbic Feb 12, 2025
1f29a2e
Merge branch 'main' into session-config2
ascorbic Feb 12, 2025
f673dee
Lock
ascorbic Feb 12, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
ascorbic marked this conversation as resolved.
Show resolved Hide resolved
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 {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why don't we validate the configuration inside the zod schema using refine or superRefine?

EDIT: I understand why we need a function, I think it's worth adding a comment that explains that

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
Loading