Skip to content

Commit 8d4e566

Browse files
ascorbicsarah11918
andauthored
feat: add support for automatic session driver config (#13145)
* 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 <5098874+sarah11918@users.noreply.github.com> * More changeset detail * Lock --------- Co-authored-by: Sarah Rainsberger <5098874+sarah11918@users.noreply.github.com>
1 parent 3b66955 commit 8d4e566

44 files changed

Lines changed: 1184 additions & 137 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.changeset/cool-deers-join.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
'@astrojs/node': minor
3+
---
4+
5+
Automatically configures filesystem storage when experimental session enabled
6+
7+
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.
8+
9+
See [the experimental session docs](https://docs.astro.build/en/reference/experimental-flags/sessions/) for more information on configuring session storage.

.changeset/tame-games-enjoy.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
'astro': minor
3+
---
4+
5+
Adds support for adapters auto-configuring experimental session storage drivers.
6+
7+
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.

.changeset/thin-cobras-glow.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
'@astrojs/netlify': minor
3+
---
4+
5+
Automatically configures Netlify Blobs storage when experimental session enabled
6+
7+
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.
8+
9+
See [the experimental session docs](https://docs.astro.build/en/reference/experimental-flags/sessions/) for more information on configuring session storage.

.changeset/tricky-insects-argue.md

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
---
2+
'astro': patch
3+
---
4+
5+
:warning: **BREAKING CHANGE FOR EXPERIMENTAL SESSIONS ONLY** :warning:
6+
7+
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.
8+
9+
```diff
10+
defineConfig({
11+
// ...
12+
experimental: {
13+
- session: {
14+
- driver: 'upstash',
15+
- },
16+
+ session: true,
17+
},
18+
+ session: {
19+
+ driver: 'upstash',
20+
+ },
21+
});
22+
```
23+
24+
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.
25+
26+
```diff
27+
defineConfig({
28+
adapter: node({
29+
mode: "standalone",
30+
}),
31+
experimental: {
32+
- session: {
33+
- driver: 'fs',
34+
- cookie: 'astro-cookie',
35+
- },
36+
+ session: true,
37+
},
38+
+ session: {
39+
+ cookie: 'astro-cookie',
40+
+ },
41+
});
42+
```
43+
44+
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/).

packages/astro/src/core/build/plugins/plugin-manifest.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -54,9 +54,7 @@ function vitePluginManifest(options: StaticBuildOptions, internals: BuildInterna
5454
`import { _privateSetManifestDontUseThis } from 'astro:ssr-manifest'`,
5555
];
5656

57-
const resolvedDriver = await resolveSessionDriver(
58-
options.settings.config.experimental?.session?.driver,
59-
);
57+
const resolvedDriver = await resolveSessionDriver(options.settings.config.session?.driver);
6058

6159
const contents = [
6260
`const manifest = _deserializeManifest('${manifestReplace}');`,
@@ -304,6 +302,6 @@ function buildManifest(
304302
(settings.config.security?.checkOrigin && settings.buildOutput === 'server') ?? false,
305303
serverIslandNameMap: Array.from(settings.serverIslandNameMap),
306304
key: encodedKey,
307-
sessionConfig: settings.config.experimental.session,
305+
sessionConfig: settings.config.session,
308306
};
309307
}

packages/astro/src/core/config/schema.ts

Lines changed: 27 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -92,12 +92,14 @@ export const ASTRO_CONFIG_DEFAULTS = {
9292
schema: {},
9393
validateSecrets: false,
9494
},
95+
session: undefined,
9596
experimental: {
9697
clientPrerender: false,
9798
contentIntellisense: false,
9899
responsiveImages: false,
99100
svg: false,
100101
serializeConfig: false,
102+
session: false,
101103
},
102104
} satisfies AstroUserConfig & { server: { open: boolean } };
103105

@@ -522,6 +524,30 @@ export const AstroConfigSchema = z.object({
522524
.strict()
523525
.optional()
524526
.default(ASTRO_CONFIG_DEFAULTS.env),
527+
session: z
528+
.object({
529+
driver: z.string(),
530+
options: z.record(z.any()).optional(),
531+
cookie: z
532+
.object({
533+
name: z.string().optional(),
534+
domain: z.string().optional(),
535+
path: z.string().optional(),
536+
maxAge: z.number().optional(),
537+
sameSite: z.union([z.enum(['strict', 'lax', 'none']), z.boolean()]).optional(),
538+
secure: z.boolean().optional(),
539+
})
540+
.or(z.string())
541+
.transform((val) => {
542+
if (typeof val === 'string') {
543+
return { name: val };
544+
}
545+
return val;
546+
})
547+
.optional(),
548+
ttl: z.number().optional(),
549+
})
550+
.optional(),
525551
experimental: z
526552
.object({
527553
clientPrerender: z
@@ -536,32 +562,7 @@ export const AstroConfigSchema = z.object({
536562
.boolean()
537563
.optional()
538564
.default(ASTRO_CONFIG_DEFAULTS.experimental.responsiveImages),
539-
session: z
540-
.object({
541-
driver: z.string(),
542-
options: z.record(z.any()).optional(),
543-
cookie: z
544-
.union([
545-
z.object({
546-
name: z.string().optional(),
547-
domain: z.string().optional(),
548-
path: z.string().optional(),
549-
maxAge: z.number().optional(),
550-
sameSite: z.union([z.enum(['strict', 'lax', 'none']), z.boolean()]).optional(),
551-
secure: z.boolean().optional(),
552-
}),
553-
z.string(),
554-
])
555-
.transform((val) => {
556-
if (typeof val === 'string') {
557-
return { name: val };
558-
}
559-
return val;
560-
})
561-
.optional(),
562-
ttl: z.number().optional(),
563-
})
564-
.optional(),
565+
session: z.boolean().optional(),
565566
svg: z
566567
.union([
567568
z.boolean(),

packages/astro/src/core/errors/errors-data.ts

Lines changed: 84 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -881,38 +881,6 @@ export const AstroResponseHeadersReassigned = {
881881
hint: 'Consider using `Astro.response.headers.add()`, and `Astro.response.headers.delete()`.',
882882
} satisfies ErrorData;
883883

884-
/**
885-
* @docs
886-
* @message Error when initializing session storage with driver `DRIVER`. `ERROR`
887-
* @see
888-
* - [experimental.session](https://docs.astro.build/en/reference/experimental-flags/sessions/)
889-
* @description
890-
* Thrown when the session storage could not be initialized.
891-
*/
892-
export const SessionStorageInitError = {
893-
name: 'SessionStorageInitError',
894-
title: 'Session storage could not be initialized.',
895-
message: (error: string, driver?: string) =>
896-
`Error when initializing session storage${driver ? ` with driver \`${driver}\`` : ''}. \`${error ?? ''}\``,
897-
hint: 'For more information, see https://docs.astro.build/en/reference/experimental-flags/sessions/',
898-
} satisfies ErrorData;
899-
900-
/**
901-
* @docs
902-
* @message Error when saving session data with driver `DRIVER`. `ERROR`
903-
* @see
904-
* - [experimental.session](https://docs.astro.build/en/reference/experimental-flags/sessions/)
905-
* @description
906-
* Thrown when the session data could not be saved.
907-
*/
908-
export const SessionStorageSaveError = {
909-
name: 'SessionStorageSaveError',
910-
title: 'Session data could not be saved.',
911-
message: (error: string, driver?: string) =>
912-
`Error when saving session data${driver ? ` with driver \`${driver}\`` : ''}. \`${error ?? ''}\``,
913-
hint: 'For more information, see https://docs.astro.build/en/reference/experimental-flags/sessions/',
914-
} satisfies ErrorData;
915-
916884
/**
917885
* @docs
918886
* @description
@@ -1838,6 +1806,90 @@ export const ActionCalledFromServerError = {
18381806
// Generic catch-all - Only use this in extreme cases, like if there was a cosmic ray bit flip.
18391807
export const UnknownError = { name: 'UnknownError', title: 'Unknown Error.' } satisfies ErrorData;
18401808

1809+
/**
1810+
* @docs
1811+
* @kind heading
1812+
* @name Session Errors
1813+
*/
1814+
// Session Errors
1815+
/**
1816+
* @docs
1817+
* @see
1818+
* - [On-demand rendering](https://docs.astro.build/en/guides/on-demand-rendering/)
1819+
* @description
1820+
* Your project must have a server output to use sessions.
1821+
*/
1822+
export const SessionWithoutServerOutputError = {
1823+
name: 'SessionWithoutServerOutputError',
1824+
title: 'Sessions must be used with server output.',
1825+
message:
1826+
'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',
1827+
hint: 'Add an adapter and enable on-demand rendering: https://docs.astro.build/en/guides/on-demand-rendering/',
1828+
} satisfies ErrorData;
1829+
1830+
/**
1831+
* @docs
1832+
* @message Error when initializing session storage with driver `DRIVER`. `ERROR`
1833+
* @see
1834+
* - [experimental.session](https://docs.astro.build/en/reference/experimental-flags/sessions/)
1835+
* @description
1836+
* Thrown when the session storage could not be initialized.
1837+
*/
1838+
export const SessionStorageInitError = {
1839+
name: 'SessionStorageInitError',
1840+
title: 'Session storage could not be initialized.',
1841+
message: (error: string, driver?: string) =>
1842+
`Error when initializing session storage${driver ? ` with driver \`${driver}\`` : ''}. \`${error ?? ''}\``,
1843+
hint: 'For more information, see https://docs.astro.build/en/reference/experimental-flags/sessions/',
1844+
} satisfies ErrorData;
1845+
1846+
/**
1847+
* @docs
1848+
* @message Error when saving session data with driver `DRIVER`. `ERROR`
1849+
* @see
1850+
* - [experimental.session](https://docs.astro.build/en/reference/experimental-flags/sessions/)
1851+
* @description
1852+
* Thrown when the session data could not be saved.
1853+
*/
1854+
export const SessionStorageSaveError = {
1855+
name: 'SessionStorageSaveError',
1856+
title: 'Session data could not be saved.',
1857+
message: (error: string, driver?: string) =>
1858+
`Error when saving session data${driver ? ` with driver \`${driver}\`` : ''}. \`${error ?? ''}\``,
1859+
hint: 'For more information, see https://docs.astro.build/en/reference/experimental-flags/sessions/',
1860+
} satisfies ErrorData;
1861+
1862+
/**
1863+
* @docs
1864+
* @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
1865+
* @see
1866+
* - [experimental.session](https://docs.astro.build/en/reference/experimental-flags/sessions/)
1867+
* @description
1868+
* Thrown when session storage is enabled but not configured.
1869+
*/
1870+
export const SessionConfigMissingError = {
1871+
name: 'SessionConfigMissingError',
1872+
title: 'Session storage was enabled but not configured.',
1873+
message:
1874+
'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',
1875+
hint: 'See https://docs.astro.build/en/reference/experimental-flags/sessions/',
1876+
} satisfies ErrorData;
1877+
1878+
/**
1879+
* @docs
1880+
* @message Session config was provided without enabling the `experimental.session` flag
1881+
* @see
1882+
* - [experimental.session](https://docs.astro.build/en/reference/experimental-flags/sessions/)
1883+
* @description
1884+
* Thrown when session storage is configured but the `experimental.session` flag is not enabled.
1885+
*/
1886+
export const SessionConfigWithoutFlagError = {
1887+
name: 'SessionConfigWithoutFlagError',
1888+
title: 'Session flag not set',
1889+
message: 'Session config was provided without enabling the `experimental.session` flag',
1890+
hint: 'See https://docs.astro.build/en/reference/experimental-flags/sessions/',
1891+
} satisfies ErrorData;
1892+
18411893
/*
18421894
* Adding an error? Follow these steps:
18431895
* 1. Determine in which category it belongs (Astro, Vite, CSS, Content Collections etc.)

packages/astro/src/core/session.ts

Lines changed: 38 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,21 @@ import {
66
builtinDrivers,
77
createStorage,
88
} from 'unstorage';
9+
import type { AstroSettings } from '../types/astro.js';
910
import type {
1011
ResolvedSessionConfig,
1112
SessionConfig,
1213
SessionDriverName,
1314
} from '../types/public/config.js';
1415
import type { AstroCookies } from './cookies/cookies.js';
1516
import type { AstroCookieSetOptions } from './cookies/cookies.js';
16-
import { SessionStorageInitError, SessionStorageSaveError } from './errors/errors-data.js';
17+
import {
18+
SessionConfigMissingError,
19+
SessionConfigWithoutFlagError,
20+
SessionStorageInitError,
21+
SessionStorageSaveError,
22+
SessionWithoutServerOutputError,
23+
} from './errors/errors-data.js';
1724
import { AstroError } from './errors/index.js';
1825

1926
export const PERSIST_SYMBOL = Symbol();
@@ -462,15 +469,39 @@ export class AstroSession<TDriver extends SessionDriverName = any> {
462469
}
463470
}
464471
// TODO: make this sync when we drop support for Node < 18.19.0
465-
export function resolveSessionDriver(driver: string | undefined): Promise<string> | string | null {
472+
export async function resolveSessionDriver(driver: string | undefined): Promise<string | null> {
466473
if (!driver) {
467474
return null;
468475
}
469-
if (driver === 'fs') {
470-
return import.meta.resolve(builtinDrivers.fsLite);
471-
}
472-
if (driver in builtinDrivers) {
473-
return import.meta.resolve(builtinDrivers[driver as keyof typeof builtinDrivers]);
476+
try {
477+
if (driver === 'fs') {
478+
return await import.meta.resolve(builtinDrivers.fsLite);
479+
}
480+
if (driver in builtinDrivers) {
481+
return await import.meta.resolve(builtinDrivers[driver as keyof typeof builtinDrivers]);
482+
}
483+
} catch {
484+
return null;
474485
}
486+
475487
return driver;
476488
}
489+
490+
export function validateSessionConfig(settings: AstroSettings): void {
491+
const { experimental, session } = settings.config;
492+
const { buildOutput } = settings;
493+
let error: AstroError | undefined;
494+
if (experimental.session) {
495+
if (!session?.driver) {
496+
error = new AstroError(SessionConfigMissingError);
497+
} else if (buildOutput === 'static') {
498+
error = new AstroError(SessionWithoutServerOutputError);
499+
}
500+
} else if (session?.driver) {
501+
error = new AstroError(SessionConfigWithoutFlagError);
502+
}
503+
if (error) {
504+
error.stack = undefined;
505+
throw error;
506+
}
507+
}

packages/astro/src/integrations/hooks.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { buildClientDirectiveEntrypoint } from '../core/client-directive/index.j
1414
import { mergeConfig } from '../core/config/index.js';
1515
import { validateSetAdapter } from '../core/dev/adapter-validation.js';
1616
import type { AstroIntegrationLogger, Logger } from '../core/logger/core.js';
17+
import { validateSessionConfig } from '../core/session.js';
1718
import type { AstroSettings } from '../types/astro.js';
1819
import type { AstroConfig } from '../types/public/config.js';
1920
import type {
@@ -369,6 +370,11 @@ export async function runHookConfigDone({
369370
});
370371
}
371372
}
373+
// Session config is validated after all integrations have had a chance to
374+
// register a default session driver, and we know the output type.
375+
// This can't happen in the Zod schema because it that happens before adapters run
376+
// and also doesn't know whether it's a server build or static build.
377+
validateSessionConfig(settings);
372378
}
373379

374380
export async function runHookServerSetup({

0 commit comments

Comments
 (0)