Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
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
5 changes: 5 additions & 0 deletions packages/astro/dev-only.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,8 @@ declare module 'virtual:astro:middleware' {
const middleware: AstroMiddlewareInstance;
export default middleware;
}

declare module 'virtual:astro:session-driver' {
import type { Driver } from 'unstorage';
export const driver: Driver;
}
1 change: 1 addition & 0 deletions packages/astro/src/core/app/entrypoint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ const manifest: SSRManifest = Object.assign(serializedManifest, {
renderers,
actions: () => import('virtual:astro:actions/entrypoint'),
middleware: () => import('virtual:astro:middleware'),
sessionDriver: () => import('virtual:astro:session-driver'),
routes,
});

Expand Down
4 changes: 4 additions & 0 deletions packages/astro/src/core/app/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import type {
} from '../../types/public/internal.js';
import type { SinglePageBuiltModule } from '../build/types.js';
import type { CspDirective } from '../csp/config.js';
import type { SessionDriver } from '../session.js';
import type { RoutingStrategies } from './common.js';

type ComponentPath = string;
Expand Down Expand Up @@ -84,6 +85,7 @@ export type SSRManifest = {
i18n: SSRManifestI18n | undefined;
middleware?: () => Promise<AstroMiddlewareInstance> | AstroMiddlewareInstance;
actions?: () => Promise<SSRActions> | SSRActions;
sessionDriver?: () => Promise<{ default: SessionDriver | null }>;
checkOrigin: boolean;
allowedDomains?: Partial<RemotePattern>[];
sessionConfig?: ResolvedSessionConfig<any>;
Expand Down Expand Up @@ -111,6 +113,8 @@ export type SSRManifest = {
};
};

export type SSRSessionDriver = SessionDriver | null;

export type SSRActions = {
server: Record<string, ActionClient<unknown, ActionAccept, ZodType>>;
};
Expand Down
22 changes: 22 additions & 0 deletions packages/astro/src/core/base-pipeline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import type {
SSRResult,
} from '../types/public/internal.js';
import { createOriginCheckMiddleware } from './app/middlewares.js';
import type { SSRSessionDriver } from './app/types.js';
import type { SinglePageBuiltModule } from './build/types.js';
import { ActionNotFoundError } from './errors/errors-data.js';
import { AstroError } from './errors/index.js';
Expand All @@ -22,6 +23,7 @@ import { sequence } from './middleware/sequence.js';
import { RedirectSinglePageBuiltModule } from './redirects/index.js';
import { RouteCache } from './render/route-cache.js';
import { createDefaultRoutes } from './routing/default.js';
import type { SessionDriver } from './session.js';

/**
* The `Pipeline` represents the static parts of rendering that do not change between requests.
Expand All @@ -33,6 +35,7 @@ export abstract class Pipeline {
readonly internalMiddleware: MiddlewareHandler[];
resolvedMiddleware: MiddlewareHandler | undefined = undefined;
resolvedActions: SSRActions | undefined = undefined;
resolvedSessionDriver: SessionDriver | null | undefined = undefined;

constructor(
readonly logger: Logger,
Expand Down Expand Up @@ -69,6 +72,7 @@ export abstract class Pipeline {
readonly defaultRoutes = createDefaultRoutes(manifest),

readonly actions = manifest.actions,
readonly sessionDriver = manifest.sessionDriver,
) {
this.internalMiddleware = [];
// We do use our middleware only if the user isn't using the manual setup
Expand Down Expand Up @@ -140,6 +144,24 @@ export abstract class Pipeline {
return NOOP_ACTIONS_MOD;
}

async getSessionDriver(): Promise<SSRSessionDriver> {
// Return cached value if already resolved (including null)
if (this.resolvedSessionDriver !== undefined) {
return this.resolvedSessionDriver;
}

// Try to load the driver from the manifest
if (this.sessionDriver) {
const driverModule = await this.sessionDriver();
this.resolvedSessionDriver = driverModule?.default || null;
return this.resolvedSessionDriver;
}

// No driver configured
this.resolvedSessionDriver = null;
return null;
}

async getAction(path: string): Promise<ActionClient<unknown, ActionAccept, ZodType>> {
const pathKeys = path.split('.').map((key) => decodeURIComponent(key));
let { server } = await this.getActions();
Expand Down
26 changes: 2 additions & 24 deletions packages/astro/src/core/build/plugins/plugin-manifest.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { fileURLToPath } from 'node:url';
import type { OutputChunk } from 'rollup';
import { glob } from 'tinyglobby';
import { type BuiltinDriverName, builtinDrivers } from 'unstorage';
import type { Plugin as VitePlugin } from 'vite';
import { getAssetsPrefix } from '../../../assets/utils/getAssetsPrefix.js';
import { normalizeTheLocale } from '../../../i18n/index.js';
Expand Down Expand Up @@ -43,25 +42,7 @@ const replaceExp = new RegExp(`['"]${manifestReplace}['"]`, 'g');
export const SSR_MANIFEST_VIRTUAL_MODULE_ID = '@astrojs-manifest';
export const RESOLVED_SSR_MANIFEST_VIRTUAL_MODULE_ID = '\0' + SSR_MANIFEST_VIRTUAL_MODULE_ID;

function resolveSessionDriver(driver: string | undefined): string | null {
if (!driver) {
return null;
}
try {
if (driver === 'fs') {
return import.meta.resolve(builtinDrivers.fsLite, import.meta.url);
}
if (driver in builtinDrivers) {
return import.meta.resolve(builtinDrivers[driver as BuiltinDriverName], import.meta.url);
}
} catch {
return null;
}

return driver;
}

function vitePluginManifest(options: StaticBuildOptions, internals: BuildInternals): VitePlugin {
function vitePluginManifest(internals: BuildInternals): VitePlugin {
return {
name: '@astro/plugin-build-manifest',
enforce: 'post',
Expand All @@ -85,11 +66,8 @@ function vitePluginManifest(options: StaticBuildOptions, internals: BuildInterna
`import { _privateSetManifestDontUseThis } from 'astro:ssr-manifest'`,
];

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

const contents = [
`const manifest = _deserializeManifest('${manifestReplace}');`,
`if (manifest.sessionConfig) manifest.sessionConfig.driverModule = ${resolvedDriver ? `() => import(${JSON.stringify(resolvedDriver)})` : 'null'};`,
`_privateSetManifestDontUseThis(manifest);`,
];
const exports = [`export { manifest }`];
Expand Down Expand Up @@ -124,7 +102,7 @@ export function pluginManifest(
hooks: {
'build:before': () => {
return {
vitePlugin: vitePluginManifest(options, internals),
vitePlugin: vitePluginManifest(internals),
};
},

Expand Down
4 changes: 3 additions & 1 deletion packages/astro/src/core/build/plugins/plugin-ssr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { ASTRO_RENDERERS_MODULE_ID } from '../../../vite-plugin-renderers/index.
import { MIDDLEWARE_MODULE_ID } from '../../middleware/vite-plugin.js';
import { routeIsRedirect } from '../../redirects/index.js';
import { VIRTUAL_ISLAND_MAP_ID } from '../../server-islands/vite-plugin-server-islands.js';
import { VIRTUAL_SESSION_DRIVER_ID } from '../../session/vite-plugin.js';
import { addRollupInput } from '../add-rollup-input.js';
import type { BuildInternals } from '../internal.js';
import type { AstroBuildPlugin } from '../plugin.js';
Expand Down Expand Up @@ -180,7 +181,8 @@ function generateSSRCode(adapter: AstroAdapter) {
` serverIslandMap,`,
` renderers,`,
` actions: () => import("${ENTRYPOINT_VIRTUAL_MODULE_ID}"),`,
` middleware: ${edgeMiddleware ? 'undefined' : `() => import("${MIDDLEWARE_MODULE_ID}")`}`,
` middleware: ${edgeMiddleware ? 'undefined' : `() => import("${MIDDLEWARE_MODULE_ID}")`},`,
` sessionDriver: () => import("${VIRTUAL_SESSION_DRIVER_ID}")`,
`});`,
`const _args = ${adapter.args ? JSON.stringify(adapter.args, null, 4) : 'undefined'};`,
adapter.exports
Expand Down
2 changes: 2 additions & 0 deletions packages/astro/src/core/create-vite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import { createViteLogger } from './logger/vite.js';
import { vitePluginMiddleware } from './middleware/vite-plugin.js';
import { joinPaths } from './path.js';
import { vitePluginServerIslands } from './server-islands/vite-plugin-server-islands.js';
import { vitePluginSessionDriver } from './session/vite-plugin.js';
import { isObject } from './util.js';

type CreateViteOptions = {
Expand Down Expand Up @@ -174,6 +175,7 @@ export async function createVite(
astroInternationalization({ settings }),
vitePluginActions({ fs, settings }),
vitePluginServerIslands({ settings, logger }),
vitePluginSessionDriver({ settings }),
astroContainer(),
astroHmrReloadPlugin(),
],
Expand Down
18 changes: 14 additions & 4 deletions packages/astro/src/core/render-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,9 +77,7 @@ export class RenderContext {
public props: Props = {},
public partial: undefined | boolean = undefined,
public shouldInjectCspMetaTags = !!pipeline.manifest.csp,
public session: AstroSession | undefined = pipeline.manifest.sessionConfig
? new AstroSession(cookies, pipeline.manifest.sessionConfig, pipeline.runtimeMode)
: undefined,
public session: AstroSession | undefined = undefined,
) {}

/**
Expand Down Expand Up @@ -109,12 +107,23 @@ export class RenderContext {
}: CreateRenderContext): Promise<RenderContext> {
const pipelineMiddleware = await pipeline.getMiddleware();
const pipelineActions = actions ?? (await pipeline.getActions());
const pipelineSessionDriver = await pipeline.getSessionDriver();
setOriginPathname(
request,
pathname,
pipeline.manifest.trailingSlash,
pipeline.manifest.buildFormat,
);
const cookies = new AstroCookies(request);
const session =
pipeline.manifest.sessionConfig && pipelineSessionDriver
? new AstroSession(
cookies,
pipeline.manifest.sessionConfig,
pipeline.runtimeMode,
pipelineSessionDriver,
)
: undefined;
return new RenderContext(
pipeline,
locals,
Expand All @@ -125,12 +134,13 @@ export class RenderContext {
routeData,
status,
clientAddress,
undefined,
cookies,
undefined,
undefined,
props,
partial,
shouldInjectCspMetaTags ?? !!pipeline.manifest.csp,
session,
);
}
/**
Expand Down
70 changes: 12 additions & 58 deletions packages/astro/src/core/session.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,6 @@
import { stringify as rawStringify, unflatten as rawUnflatten } from 'devalue';

import {
type BuiltinDriverName,
type BuiltinDriverOptions,
builtinDrivers,
createStorage,
type Driver,
type Storage,
} from 'unstorage';
import { type BuiltinDriverOptions, createStorage, type Driver, type Storage } from 'unstorage';
import type {
ResolvedSessionConfig,
RuntimeMode,
Expand All @@ -23,6 +16,8 @@ export const PERSIST_SYMBOL = Symbol();
const DEFAULT_COOKIE_NAME = 'astro-session';
const VALID_COOKIE_REGEX = /^[\w-]+$/;

export type SessionDriver = (config: any) => import('unstorage').Driver;

interface SessionEntry {
data: any;
expires?: number;
Expand Down Expand Up @@ -72,6 +67,8 @@ export class AstroSession<TDriver extends SessionDriverName = any> {
// When we load the data from storage, we need to merge it with the local partial data,
// preserving in-memory changes and deletions.
#partial = true;
// The driver factory function provided by the pipeline
#driverFactory: ((config: SessionConfig<TDriver>['options']) => Driver) | null | undefined;

static #sharedStorage = new Map<string, Storage>();

Expand All @@ -82,6 +79,7 @@ export class AstroSession<TDriver extends SessionDriverName = any> {
...config
}: NonNullable<ResolvedSessionConfig<TDriver>>,
runtimeMode?: RuntimeMode,
driverFactory?: ((config: SessionConfig<TDriver>['options']) => Driver) | null,
) {
const { driver } = config;
if (!driver) {
Expand All @@ -93,6 +91,7 @@ export class AstroSession<TDriver extends SessionDriverName = any> {
});
}
this.#cookies = cookies;
this.#driverFactory = driverFactory;
let cookieConfigObject: AstroCookieSetOptions | undefined;
if (typeof cookieConfig === 'object') {
const { name = DEFAULT_COOKIE_NAME, ...rest } = cookieConfig;
Expand Down Expand Up @@ -441,46 +440,19 @@ export class AstroSession<TDriver extends SessionDriverName = any> {
(this.#config.options as BuiltinDriverOptions['fs-lite']).base ??= '.astro/session';
}

let driver: ((config: SessionConfig<TDriver>['options']) => Driver) | null = null;

try {
if (this.#config.driverModule) {
driver = (await this.#config.driverModule()).default;
} else if (this.#config.driver) {
const driverName = resolveSessionDriverName(this.#config.driver);
if (driverName) {
driver = (await import(/* @vite-ignore */ driverName)).default;
}
}
} catch (err: any) {
// If the driver failed to load, throw an error.
if (err.code === 'ERR_MODULE_NOT_FOUND') {
throw new AstroError(
{
...SessionStorageInitError,
message: SessionStorageInitError.message(
err.message.includes(`Cannot find package`)
? 'The driver module could not be found.'
: err.message,
this.#config.driver,
),
},
{ cause: err },
);
}
throw err;
}

if (!driver) {
// Get the driver factory from the pipeline
if (!this.#driverFactory) {
throw new AstroError({
...SessionStorageInitError,
message: SessionStorageInitError.message(
'The module did not export a driver.',
'Astro could not load the driver correctly. Does it exist?',
this.#config.driver,
),
});
}

const driver = this.#driverFactory;

try {
this.#storage = createStorage({
driver: driver(this.#config.options),
Expand All @@ -498,21 +470,3 @@ export class AstroSession<TDriver extends SessionDriverName = any> {
}
}
}

function resolveSessionDriverName(driver: string | undefined): string | null {
if (!driver) {
return null;
}
try {
if (driver === 'fs') {
return builtinDrivers.fsLite;
}
if (driver in builtinDrivers) {
return builtinDrivers[driver as BuiltinDriverName];
}
} catch {
return null;
}

return driver;
}
36 changes: 36 additions & 0 deletions packages/astro/src/core/session/vite-plugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { type BuiltinDriverName, builtinDrivers } from 'unstorage';
import type { Plugin as VitePlugin } from 'vite';
import type { AstroSettings } from '../../types/astro.js';

export const VIRTUAL_SESSION_DRIVER_ID = 'virtual:astro:session-driver';
const RESOLVED_VIRTUAL_SESSION_DRIVER_ID = '\0' + VIRTUAL_SESSION_DRIVER_ID;

export function vitePluginSessionDriver({ settings }: { settings: AstroSettings }): VitePlugin {
return {
name: VIRTUAL_SESSION_DRIVER_ID,
enforce: 'pre',

async resolveId(id) {
if (id === VIRTUAL_SESSION_DRIVER_ID) {
if (settings.config.session) {
if (settings.config.session.driver === 'fs') {
return await this.resolve(builtinDrivers.fsLite);
}
if (settings.config.session.driver && settings.config.session.driver in builtinDrivers) {
return await this.resolve(
builtinDrivers[settings.config.session.driver as BuiltinDriverName],
);
}
} else {
return RESOLVED_VIRTUAL_SESSION_DRIVER_ID;
}
}
},

async load(id) {
if (id === RESOLVED_VIRTUAL_SESSION_DRIVER_ID) {
return { code: 'export default null;' };
}
},
};
}
Loading
Loading