summaryrefslogtreecommitdiff
path: root/packages
diff options
context:
space:
mode:
Diffstat (limited to 'packages')
-rw-r--r--packages/astro/client-base.d.ts6
-rw-r--r--packages/astro/package.json4
-rw-r--r--packages/astro/src/@types/app.d.ts9
-rw-r--r--packages/astro/src/@types/astro.ts69
-rw-r--r--packages/astro/src/core/app/index.ts50
-rw-r--r--packages/astro/src/core/app/types.ts2
-rw-r--r--packages/astro/src/core/build/generate.ts51
-rw-r--r--packages/astro/src/core/build/plugins/plugin-pages.ts21
-rw-r--r--packages/astro/src/core/build/plugins/plugin-ssr.ts19
-rw-r--r--packages/astro/src/core/build/types.ts2
-rw-r--r--packages/astro/src/core/config/config.ts5
-rw-r--r--packages/astro/src/core/config/schema.ts2
-rw-r--r--packages/astro/src/core/constants.ts3
-rw-r--r--packages/astro/src/core/endpoint/dev/index.ts7
-rw-r--r--packages/astro/src/core/endpoint/index.ts87
-rw-r--r--packages/astro/src/core/errors/errors-data.ts89
-rw-r--r--packages/astro/src/core/middleware/callMiddleware.ts99
-rw-r--r--packages/astro/src/core/middleware/index.ts9
-rw-r--r--packages/astro/src/core/middleware/loadMiddleware.ts22
-rw-r--r--packages/astro/src/core/middleware/sequence.ts36
-rw-r--r--packages/astro/src/core/render/context.ts29
-rw-r--r--packages/astro/src/core/render/core.ts149
-rw-r--r--packages/astro/src/core/render/dev/index.ts31
-rw-r--r--packages/astro/src/core/render/index.ts7
-rw-r--r--packages/astro/src/core/render/result.ts4
-rw-r--r--packages/astro/src/core/request.ts3
-rw-r--r--packages/astro/src/integrations/index.ts3
-rw-r--r--packages/astro/src/runtime/server/endpoint.ts2
-rw-r--r--packages/astro/src/vite-plugin-astro-server/route.ts19
-rw-r--r--packages/astro/test/fixtures/middleware-dev/astro.config.mjs7
-rw-r--r--packages/astro/test/fixtures/middleware-dev/package.json8
-rw-r--r--packages/astro/test/fixtures/middleware-dev/src/middleware.js40
-rw-r--r--packages/astro/test/fixtures/middleware-dev/src/pages/broken-500.astro0
-rw-r--r--packages/astro/test/fixtures/middleware-dev/src/pages/broken-locals.astro0
-rw-r--r--packages/astro/test/fixtures/middleware-dev/src/pages/does-nothing.astro9
-rw-r--r--packages/astro/test/fixtures/middleware-dev/src/pages/index.astro14
-rw-r--r--packages/astro/test/fixtures/middleware-dev/src/pages/lorem.astro13
-rw-r--r--packages/astro/test/fixtures/middleware-dev/src/pages/not-interested.astro9
-rw-r--r--packages/astro/test/fixtures/middleware-dev/src/pages/redirect.astro0
-rw-r--r--packages/astro/test/fixtures/middleware-dev/src/pages/rewrite.astro9
-rw-r--r--packages/astro/test/fixtures/middleware-dev/src/pages/second.astro13
-rw-r--r--packages/astro/test/fixtures/middleware-ssg/astro.config.mjs8
-rw-r--r--packages/astro/test/fixtures/middleware-ssg/package.json8
-rw-r--r--packages/astro/test/fixtures/middleware-ssg/src/middleware.js12
-rw-r--r--packages/astro/test/fixtures/middleware-ssg/src/pages/index.astro14
-rw-r--r--packages/astro/test/fixtures/middleware-ssg/src/pages/second.astro13
-rw-r--r--packages/astro/test/middleware.test.js202
-rw-r--r--packages/astro/test/test-adapter.js2
-rw-r--r--packages/astro/test/test-utils.js2
-rw-r--r--packages/astro/test/units/render/head.test.js44
-rw-r--r--packages/astro/test/units/render/jsx.test.js42
-rw-r--r--packages/integrations/node/src/nodeMiddleware.ts (renamed from packages/integrations/node/src/middleware.ts)0
-rw-r--r--packages/integrations/node/src/server.ts2
-rw-r--r--packages/integrations/node/src/standalone.ts2
54 files changed, 1183 insertions, 129 deletions
diff --git a/packages/astro/client-base.d.ts b/packages/astro/client-base.d.ts
index 15c1fb905..6e37b60c7 100644
--- a/packages/astro/client-base.d.ts
+++ b/packages/astro/client-base.d.ts
@@ -387,3 +387,9 @@ declare module '*?inline' {
const src: string;
export default src;
}
+
+// eslint-disable-next-line @typescript-eslint/no-namespace
+export namespace App {
+ // eslint-disable-next-line @typescript-eslint/no-empty-interface
+ export interface Locals {}
+}
diff --git a/packages/astro/package.json b/packages/astro/package.json
index 1dabc7d78..08ee274a6 100644
--- a/packages/astro/package.json
+++ b/packages/astro/package.json
@@ -64,6 +64,10 @@
"./zod": {
"types": "./zod.d.ts",
"default": "./zod.mjs"
+ },
+ "./middleware": {
+ "types": "./dist/core/middleware/index.d.ts",
+ "default": "./dist/core/middleware/index.js"
}
},
"imports": {
diff --git a/packages/astro/src/@types/app.d.ts b/packages/astro/src/@types/app.d.ts
new file mode 100644
index 000000000..1c0908bb8
--- /dev/null
+++ b/packages/astro/src/@types/app.d.ts
@@ -0,0 +1,9 @@
+/**
+ * Shared interfaces throughout the application that can be overridden by the user.
+ */
+declare namespace App {
+ /**
+ * Used by middlewares to store information, that can be read by the user via the global `Astro.locals`
+ */
+ interface Locals {}
+}
diff --git a/packages/astro/src/@types/astro.ts b/packages/astro/src/@types/astro.ts
index d68f6c75f..b8d7338f6 100644
--- a/packages/astro/src/@types/astro.ts
+++ b/packages/astro/src/@types/astro.ts
@@ -103,6 +103,7 @@ export interface CLIFlags {
drafts?: boolean;
open?: boolean;
experimentalAssets?: boolean;
+ experimentalMiddleware?: boolean;
}
export interface BuildConfig {
@@ -1034,6 +1035,26 @@ export interface AstroUserConfig {
* }
*/
assets?: boolean;
+
+ /**
+ * @docs
+ * @name experimental.middleware
+ * @type {boolean}
+ * @default `false`
+ * @version 2.4.0
+ * @description
+ * Enable experimental support for Astro middleware.
+ *
+ * To enable this feature, set `experimental.middleware` to `true` in your Astro config:
+ *
+ * ```js
+ * {
+ * experimental: {
+ * middleware: true,
+ * },
+ * }
+ */
+ middleware?: boolean;
};
// Legacy options to be removed
@@ -1431,6 +1452,11 @@ interface AstroSharedContext<Props extends Record<string, any> = Record<string,
* Redirect to another page (**SSR Only**).
*/
redirect(path: string, status?: 301 | 302 | 303 | 307 | 308): Response;
+
+ /**
+ * Object accessed via Astro middleware
+ */
+ locals: App.Locals;
}
export interface APIContext<Props extends Record<string, any> = Record<string, any>>
@@ -1464,7 +1490,7 @@ export interface APIContext<Props extends Record<string, any> = Record<string, a
* }
* ```
*
- * [context reference](https://docs.astro.build/en/guides/api-reference/#contextparams)
+ * [context reference](https://docs.astro.build/en/reference/api-reference/#contextparams)
*/
params: AstroSharedContext['params'];
/**
@@ -1504,6 +1530,31 @@ export interface APIContext<Props extends Record<string, any> = Record<string, a
* [context reference](https://docs.astro.build/en/guides/api-reference/#contextredirect)
*/
redirect: AstroSharedContext['redirect'];
+
+ /**
+ * Object accessed via Astro middleware.
+ *
+ * Example usage:
+ *
+ * ```ts
+ * // src/middleware.ts
+ * import {defineMiddleware} from "astro/middleware";
+ *
+ * export const onRequest = defineMiddleware((context, next) => {
+ * context.locals.greeting = "Hello!";
+ * next();
+ * });
+ * ```
+ * Inside a `.astro` file:
+ * ```astro
+ * ---
+ * // src/pages/index.astro
+ * const greeting = Astro.locals.greeting;
+ * ---
+ * <h1>{greeting}</h1>
+ * ```
+ */
+ locals: App.Locals;
}
export type Props = Record<string, unknown>;
@@ -1592,6 +1643,22 @@ export interface AstroIntegration {
};
}
+export type MiddlewareNext<R> = () => Promise<R>;
+export type MiddlewareHandler<R> = (
+ context: APIContext,
+ next: MiddlewareNext<R>
+) => Promise<R> | Promise<void> | void;
+
+export type MiddlewareResponseHandler = MiddlewareHandler<Response>;
+export type MiddlewareEndpointHandler = MiddlewareHandler<Response | EndpointOutput>;
+export type MiddlewareNextResponse = MiddlewareNext<Response>;
+
+// NOTE: when updating this file with other functions,
+// remember to update `plugin-page.ts` too, to add that function as a no-op function.
+export type AstroMiddlewareInstance<R> = {
+ onRequest?: MiddlewareHandler<R>;
+};
+
export interface AstroPluginOptions {
settings: AstroSettings;
logging: LogOptions;
diff --git a/packages/astro/src/core/app/index.ts b/packages/astro/src/core/app/index.ts
index a1d19ee92..b499a875b 100644
--- a/packages/astro/src/core/app/index.ts
+++ b/packages/astro/src/core/app/index.ts
@@ -2,6 +2,7 @@ import type {
ComponentInstance,
EndpointHandler,
ManifestData,
+ MiddlewareResponseHandler,
RouteData,
SSRElement,
} from '../../@types/astro';
@@ -9,9 +10,10 @@ import type { RouteInfo, SSRManifest as Manifest } from './types';
import mime from 'mime';
import { attachToResponse, getSetCookiesFromResponse } from '../cookies/index.js';
-import { call as callEndpoint } from '../endpoint/index.js';
+import { call as callEndpoint, createAPIContext } from '../endpoint/index.js';
import { consoleLogDestination } from '../logger/console.js';
import { error, type LogOptions } from '../logger/core.js';
+import { callMiddleware } from '../middleware/callMiddleware.js';
import { removeTrailingForwardSlash } from '../path.js';
import {
createEnvironment,
@@ -28,6 +30,8 @@ import {
import { matchRoute } from '../routing/match.js';
export { deserializeManifest } from './common.js';
+const clientLocalsSymbol = Symbol.for('astro.locals');
+
export const pagesVirtualModuleId = '@astrojs-pages-virtual-entry';
export const resolvedPagesVirtualModuleId = '\0' + pagesVirtualModuleId;
const responseSentSymbol = Symbol.for('astro.responseSent');
@@ -127,6 +131,8 @@ export class App {
}
}
+ Reflect.set(request, clientLocalsSymbol, {});
+
// Use the 404 status code for 404.astro components
if (routeData.route === '/404') {
defaultStatus = 404;
@@ -191,7 +197,7 @@ export class App {
}
try {
- const ctx = createRenderContext({
+ const renderContext = await createRenderContext({
request,
origin: url.origin,
pathname,
@@ -200,9 +206,35 @@ export class App {
links,
route: routeData,
status,
+ mod: mod as any,
+ env: this.#env,
});
- const response = await renderPage(mod, ctx, this.#env);
+ const apiContext = createAPIContext({
+ request: renderContext.request,
+ params: renderContext.params,
+ props: renderContext.props,
+ site: this.#env.site,
+ adapterName: this.#env.adapterName,
+ });
+ const onRequest = this.#manifest.middleware?.onRequest;
+ let response;
+ if (onRequest) {
+ response = await callMiddleware<Response>(
+ onRequest as MiddlewareResponseHandler,
+ apiContext,
+ () => {
+ return renderPage({ mod, renderContext, env: this.#env, apiContext });
+ }
+ );
+ } else {
+ response = await renderPage({
+ mod,
+ renderContext,
+ env: this.#env,
+ apiContext,
+ });
+ }
Reflect.set(request, responseSentSymbol, true);
return response;
} catch (err: any) {
@@ -224,15 +256,23 @@ export class App {
const pathname = '/' + this.removeBase(url.pathname);
const handler = mod as unknown as EndpointHandler;
- const ctx = createRenderContext({
+ const ctx = await createRenderContext({
request,
origin: url.origin,
pathname,
route: routeData,
status,
+ env: this.#env,
+ mod: handler as any,
});
- const result = await callEndpoint(handler, this.#env, ctx, this.#logging);
+ const result = await callEndpoint(
+ handler,
+ this.#env,
+ ctx,
+ this.#logging,
+ this.#manifest.middleware
+ );
if (result.type === 'response') {
if (result.response.headers.get('X-Astro-Response') === 'Not-Found') {
diff --git a/packages/astro/src/core/app/types.ts b/packages/astro/src/core/app/types.ts
index ab91c13ca..79503161d 100644
--- a/packages/astro/src/core/app/types.ts
+++ b/packages/astro/src/core/app/types.ts
@@ -1,5 +1,6 @@
import type { MarkdownRenderingOptions } from '@astrojs/markdown-remark';
import type {
+ AstroMiddlewareInstance,
ComponentInstance,
RouteData,
SerializedRouteData,
@@ -38,6 +39,7 @@ export interface SSRManifest {
entryModules: Record<string, string>;
assets: Set<string>;
componentMetadata: SSRResult['componentMetadata'];
+ middleware?: AstroMiddlewareInstance<unknown>;
}
export type SerializedSSRManifest = Omit<SSRManifest, 'routes' | 'assets' | 'componentMetadata'> & {
diff --git a/packages/astro/src/core/build/generate.ts b/packages/astro/src/core/build/generate.ts
index 6e50d687f..d89575bd4 100644
--- a/packages/astro/src/core/build/generate.ts
+++ b/packages/astro/src/core/build/generate.ts
@@ -5,10 +5,13 @@ import type { OutputAsset, OutputChunk } from 'rollup';
import { fileURLToPath } from 'url';
import type {
AstroConfig,
+ AstroMiddlewareInstance,
AstroSettings,
ComponentInstance,
EndpointHandler,
+ EndpointOutput,
ImageTransform,
+ MiddlewareResponseHandler,
RouteType,
SSRError,
SSRLoadedRenderer,
@@ -25,9 +28,14 @@ import {
} from '../../core/path.js';
import { runHookBuildGenerated } from '../../integrations/index.js';
import { BEFORE_HYDRATION_SCRIPT_ID, PAGE_SCRIPT_ID } from '../../vite-plugin-scripts/index.js';
-import { call as callEndpoint, throwIfRedirectNotAllowed } from '../endpoint/index.js';
+import {
+ call as callEndpoint,
+ createAPIContext,
+ throwIfRedirectNotAllowed,
+} from '../endpoint/index.js';
import { AstroError } from '../errors/index.js';
import { debug, info } from '../logger/core.js';
+import { callMiddleware } from '../middleware/callMiddleware.js';
import { createEnvironment, createRenderContext, renderPage } from '../render/index.js';
import { callGetStaticPaths } from '../render/route-cache.js';
import {
@@ -157,6 +165,7 @@ async function generatePage(
const scripts = pageInfo?.hoistedScript ?? null;
const pageModule = ssrEntry.pageMap?.get(pageData.component);
+ const middleware = ssrEntry.middleware;
if (!pageModule) {
throw new Error(
@@ -186,7 +195,7 @@ async function generatePage(
for (let i = 0; i < paths.length; i++) {
const path = paths[i];
- await generatePath(path, opts, generationOptions);
+ await generatePath(path, opts, generationOptions, middleware);
const timeEnd = performance.now();
const timeChange = getTimeStat(timeStart, timeEnd);
const timeIncrease = `(+${timeChange})`;
@@ -328,7 +337,8 @@ function getUrlForPath(
async function generatePath(
pathname: string,
opts: StaticBuildOptions,
- gopts: GeneratePathOptions
+ gopts: GeneratePathOptions,
+ middleware?: AstroMiddlewareInstance<unknown>
) {
const { settings, logging, origin, routeCache } = opts;
const { mod, internals, linkIds, scripts: hoistedScripts, pageData, renderers } = gopts;
@@ -414,7 +424,8 @@ async function generatePath(
ssr,
streaming: true,
});
- const ctx = createRenderContext({
+
+ const renderContext = await createRenderContext({
origin,
pathname,
request: createRequest({ url, headers: new Headers(), logging, ssr }),
@@ -422,13 +433,22 @@ async function generatePath(
scripts,
links,
route: pageData.route,
+ env,
+ mod,
});
let body: string | Uint8Array;
let encoding: BufferEncoding | undefined;
if (pageData.route.type === 'endpoint') {
const endpointHandler = mod as unknown as EndpointHandler;
- const result = await callEndpoint(endpointHandler, env, ctx, logging);
+
+ const result = await callEndpoint(
+ endpointHandler,
+ env,
+ renderContext,
+ logging,
+ middleware as AstroMiddlewareInstance<Response | EndpointOutput>
+ );
if (result.type === 'response') {
throwIfRedirectNotAllowed(result.response, opts.settings.config);
@@ -443,7 +463,26 @@ async function generatePath(
} else {
let response: Response;
try {
- response = await renderPage(mod, ctx, env);
+ const apiContext = createAPIContext({
+ request: renderContext.request,
+ params: renderContext.params,
+ props: renderContext.props,
+ site: env.site,
+ adapterName: env.adapterName,
+ });
+
+ const onRequest = middleware?.onRequest;
+ if (onRequest) {
+ response = await callMiddleware<Response>(
+ onRequest as MiddlewareResponseHandler,
+ apiContext,
+ () => {
+ return renderPage({ mod, renderContext, env, apiContext });
+ }
+ );
+ } else {
+ response = await renderPage({ mod, renderContext, env, apiContext });
+ }
} catch (err) {
if (!AstroError.is(err) && !(err as SSRError).id && typeof err === 'object') {
(err as SSRError).id = pageData.component;
diff --git a/packages/astro/src/core/build/plugins/plugin-pages.ts b/packages/astro/src/core/build/plugins/plugin-pages.ts
index 5bb070978..132d03cf8 100644
--- a/packages/astro/src/core/build/plugins/plugin-pages.ts
+++ b/packages/astro/src/core/build/plugins/plugin-pages.ts
@@ -1,10 +1,10 @@
import type { Plugin as VitePlugin } from 'vite';
-import type { AstroBuildPlugin } from '../plugin';
-import type { StaticBuildOptions } from '../types';
-
import { pagesVirtualModuleId, resolvedPagesVirtualModuleId } from '../../app/index.js';
+import { MIDDLEWARE_PATH_SEGMENT_NAME } from '../../constants.js';
import { addRollupInput } from '../add-rollup-input.js';
import { eachPageData, hasPrerenderedPages, type BuildInternals } from '../internal.js';
+import type { AstroBuildPlugin } from '../plugin';
+import type { StaticBuildOptions } from '../types';
export function vitePluginPages(opts: StaticBuildOptions, internals: BuildInternals): VitePlugin {
return {
@@ -22,8 +22,15 @@ export function vitePluginPages(opts: StaticBuildOptions, internals: BuildIntern
}
},
- load(id) {
+ async load(id) {
if (id === resolvedPagesVirtualModuleId) {
+ let middlewareId = null;
+ if (opts.settings.config.experimental.middleware) {
+ middlewareId = await this.resolve(
+ `${opts.settings.config.srcDir.pathname}/${MIDDLEWARE_PATH_SEGMENT_NAME}`
+ );
+ }
+
let importMap = '';
let imports = [];
let i = 0;
@@ -47,8 +54,12 @@ export function vitePluginPages(opts: StaticBuildOptions, internals: BuildIntern
const def = `${imports.join('\n')}
+${middlewareId ? `import * as _middleware from "${middlewareId.id}";` : ''}
+
export const pageMap = new Map([${importMap}]);
-export const renderers = [${rendererItems}];`;
+export const renderers = [${rendererItems}];
+${middlewareId ? `export const middleware = _middleware;` : ''}
+`;
return def;
}
diff --git a/packages/astro/src/core/build/plugins/plugin-ssr.ts b/packages/astro/src/core/build/plugins/plugin-ssr.ts
index e5bca2ad0..65e104425 100644
--- a/packages/astro/src/core/build/plugins/plugin-ssr.ts
+++ b/packages/astro/src/core/build/plugins/plugin-ssr.ts
@@ -1,5 +1,5 @@
import type { Plugin as VitePlugin } from 'vite';
-import type { AstroAdapter } from '../../../@types/astro';
+import type { AstroAdapter, AstroConfig } from '../../../@types/astro';
import type { SerializedRouteInfo, SerializedSSRManifest } from '../../app/types';
import type { BuildInternals } from '../internal.js';
import type { StaticBuildOptions } from '../types';
@@ -21,7 +21,11 @@ const resolvedVirtualModuleId = '\0' + virtualModuleId;
const manifestReplace = '@@ASTRO_MANIFEST_REPLACE@@';
const replaceExp = new RegExp(`['"](${manifestReplace})['"]`, 'g');
-export function vitePluginSSR(internals: BuildInternals, adapter: AstroAdapter): VitePlugin {
+export function vitePluginSSR(
+ internals: BuildInternals,
+ adapter: AstroAdapter,
+ config: AstroConfig
+): VitePlugin {
return {
name: '@astrojs/vite-plugin-astro-ssr',
enforce: 'post',
@@ -35,13 +39,18 @@ export function vitePluginSSR(internals: BuildInternals, adapter: AstroAdapter):
},
load(id) {
if (id === resolvedVirtualModuleId) {
+ let middleware = '';
+ if (config.experimental?.middleware === true) {
+ middleware = 'middleware: _main.middleware';
+ }
return `import * as adapter from '${adapter.serverEntrypoint}';
import * as _main from '${pagesVirtualModuleId}';
import { deserializeManifest as _deserializeManifest } from 'astro/app';
import { _privateSetManifestDontUseThis } from 'astro:ssr-manifest';
const _manifest = Object.assign(_deserializeManifest('${manifestReplace}'), {
pageMap: _main.pageMap,
- renderers: _main.renderers
+ renderers: _main.renderers,
+ ${middleware}
});
_privateSetManifestDontUseThis(_manifest);
const _args = ${adapter.args ? JSON.stringify(adapter.args) : 'undefined'};
@@ -235,7 +244,9 @@ export function pluginSSR(
build: 'ssr',
hooks: {
'build:before': () => {
- let vitePlugin = ssr ? vitePluginSSR(internals, options.settings.adapter!) : undefined;
+ let vitePlugin = ssr
+ ? vitePluginSSR(internals, options.settings.adapter!, options.settings.config)
+ : undefined;
return {
enforce: 'after-user-plugins',
diff --git a/packages/astro/src/core/build/types.ts b/packages/astro/src/core/build/types.ts
index 02f5618d8..fc7839390 100644
--- a/packages/astro/src/core/build/types.ts
+++ b/packages/astro/src/core/build/types.ts
@@ -1,6 +1,7 @@
import type { default as vite, InlineConfig } from 'vite';
import type {
AstroConfig,
+ AstroMiddlewareInstance,
AstroSettings,
BuildConfig,
ComponentInstance,
@@ -44,6 +45,7 @@ export interface StaticBuildOptions {
export interface SingleFileBuiltModule {
pageMap: Map<ComponentPath, ComponentInstance>;
+ middleware: AstroMiddlewareInstance<unknown>;
renderers: SSRLoadedRenderer[];
}
diff --git a/packages/astro/src/core/config/config.ts b/packages/astro/src/core/config/config.ts
index 9c09a934f..9915ed162 100644
--- a/packages/astro/src/core/config/config.ts
+++ b/packages/astro/src/core/config/config.ts
@@ -103,6 +103,8 @@ export function resolveFlags(flags: Partial<Flags>): CLIFlags {
drafts: typeof flags.drafts === 'boolean' ? flags.drafts : undefined,
experimentalAssets:
typeof flags.experimentalAssets === 'boolean' ? flags.experimentalAssets : undefined,
+ experimentalMiddleware:
+ typeof flags.experimentalMiddleware === 'boolean' ? flags.experimentalMiddleware : undefined,
};
}
@@ -136,6 +138,9 @@ function mergeCLIFlags(astroConfig: AstroUserConfig, flags: CLIFlags) {
// TODO: Come back here and refactor to remove this expected error.
astroConfig.server.open = flags.open;
}
+ if (typeof flags.experimentalMiddleware === 'boolean') {
+ astroConfig.experimental.middleware = true;
+ }
return astroConfig;
}
diff --git a/packages/astro/src/core/config/schema.ts b/packages/astro/src/core/config/schema.ts
index 424972cba..6d081d126 100644
--- a/packages/astro/src/core/config/schema.ts
+++ b/packages/astro/src/core/config/schema.ts
@@ -37,6 +37,7 @@ const ASTRO_CONFIG_DEFAULTS: AstroUserConfig & any = {
legacy: {},
experimental: {
assets: false,
+ middleware: false,
},
};
@@ -187,6 +188,7 @@ export const AstroConfigSchema = z.object({
experimental: z
.object({
assets: z.boolean().optional().default(ASTRO_CONFIG_DEFAULTS.experimental.assets),
+ middleware: z.oboolean().optional().default(ASTRO_CONFIG_DEFAULTS.experimental.middleware),
})
.optional()
.default({}),
diff --git a/packages/astro/src/core/constants.ts b/packages/astro/src/core/constants.ts
index 16dde7550..471614ce3 100644
--- a/packages/astro/src/core/constants.ts
+++ b/packages/astro/src/core/constants.ts
@@ -10,3 +10,6 @@ export const SUPPORTED_MARKDOWN_FILE_EXTENSIONS = [
'.mdwn',
'.md',
] as const;
+
+// The folder name where to find the middleware
+export const MIDDLEWARE_PATH_SEGMENT_NAME = 'middleware';
diff --git a/packages/astro/src/core/endpoint/dev/index.ts b/packages/astro/src/core/endpoint/dev/index.ts
index 515b7aa41..24d2aa3bd 100644
--- a/packages/astro/src/core/endpoint/dev/index.ts
+++ b/packages/astro/src/core/endpoint/dev/index.ts
@@ -8,15 +8,18 @@ export async function call(options: SSROptions, logging: LogOptions) {
const {
env,
preload: [, mod],
+ middleware,
} = options;
const endpointHandler = mod as unknown as EndpointHandler;
- const ctx = createRenderContext({
+ const ctx = await createRenderContext({
request: options.request,
origin: options.origin,
pathname: options.pathname,
route: options.route,
+ env,
+ mod: endpointHandler as any,
});
- return await callEndpoint(endpointHandler, env, ctx, logging);
+ return await callEndpoint(endpointHandler, env, ctx, logging, middleware);
}
diff --git a/packages/astro/src/core/endpoint/index.ts b/packages/astro/src/core/endpoint/index.ts
index 876c21aa5..e49ce8a43 100644
--- a/packages/astro/src/core/endpoint/index.ts
+++ b/packages/astro/src/core/endpoint/index.ts
@@ -1,4 +1,12 @@
-import type { APIContext, AstroConfig, EndpointHandler, Params } from '../../@types/astro';
+import type {
+ APIContext,
+ AstroConfig,
+ AstroMiddlewareInstance,
+ EndpointHandler,
+ EndpointOutput,
+ MiddlewareEndpointHandler,
+ Params,
+} from '../../@types/astro';
import type { Environment, RenderContext } from '../render/index';
import { renderEndpoint } from '../../runtime/server/index.js';
@@ -6,9 +14,11 @@ import { ASTRO_VERSION } from '../constants.js';
import { AstroCookies, attachToResponse } from '../cookies/index.js';
import { AstroError, AstroErrorData } from '../errors/index.js';
import { warn, type LogOptions } from '../logger/core.js';
-import { getParamsAndProps, GetParamsAndPropsError } from '../render/core.js';
+import { callMiddleware } from '../middleware/callMiddleware.js';
+import { isValueSerializable } from '../render/core.js';
const clientAddressSymbol = Symbol.for('astro.clientAddress');
+const clientLocalsSymbol = Symbol.for('astro.locals');
type EndpointCallResult =
| {
@@ -22,7 +32,7 @@ type EndpointCallResult =
response: Response;
};
-function createAPIContext({
+export function createAPIContext({
request,
params,
site,
@@ -35,7 +45,7 @@ function createAPIContext({
props: Record<string, any>;
adapterName?: string;
}): APIContext {
- return {
+ const context = {
cookies: new AstroCookies(request),
request,
params,
@@ -51,7 +61,6 @@ function createAPIContext({
});
},
url: new URL(request.url),
- // @ts-expect-error
get clientAddress() {
if (!(clientAddressSymbol in request)) {
if (adapterName) {
@@ -66,44 +75,60 @@ function createAPIContext({
return Reflect.get(request, clientAddressSymbol);
},
- };
+ } as APIContext;
+
+ // We define a custom property, so we can check the value passed to locals
+ Object.defineProperty(context, 'locals', {
+ get() {
+ return Reflect.get(request, clientLocalsSymbol);
+ },
+ set(val) {
+ if (typeof val !== 'object') {
+ throw new AstroError(AstroErrorData.LocalsNotAnObject);
+ } else {
+ Reflect.set(request, clientLocalsSymbol, val);
+ }
+ },
+ });
+ return context;
}
-export async function call(
+export async function call<MiddlewareResult = Response | EndpointOutput>(
mod: EndpointHandler,
env: Environment,
ctx: RenderContext,
- logging: LogOptions
+ logging: LogOptions,
+ middleware?: AstroMiddlewareInstance<MiddlewareResult> | undefined
): Promise<EndpointCallResult> {
- const paramsAndPropsResp = await getParamsAndProps({
- mod: mod as any,
- route: ctx.route,
- routeCache: env.routeCache,
- pathname: ctx.pathname,
- logging: env.logging,
- ssr: env.ssr,
- });
-
- if (paramsAndPropsResp === GetParamsAndPropsError.NoMatchingStaticPath) {
- throw new AstroError({
- ...AstroErrorData.NoMatchingStaticPathFound,
- message: AstroErrorData.NoMatchingStaticPathFound.message(ctx.pathname),
- hint: ctx.route?.component
- ? AstroErrorData.NoMatchingStaticPathFound.hint([ctx.route?.component])
- : '',
- });
- }
- const [params, props] = paramsAndPropsResp;
-
const context = createAPIContext({
request: ctx.request,
- params,
- props,
+ params: ctx.params,
+ props: ctx.props,
site: env.site,
adapterName: env.adapterName,
});
- const response = await renderEndpoint(mod, context, env.ssr);
+ let response = await renderEndpoint(mod, context, env.ssr);
+ if (middleware && middleware.onRequest) {
+ if (response.body === null) {
+ const onRequest = middleware.onRequest as MiddlewareEndpointHandler;
+ response = await callMiddleware<Response | EndpointOutput>(onRequest, context, async () => {
+ if (env.mode === 'development' && !isValueSerializable(context.locals)) {
+ throw new AstroError({
+ ...AstroErrorData.LocalsNotSerializable,
+ message: AstroErrorData.LocalsNotSerializable.message(ctx.pathname),
+ });
+ }
+ return response;
+ });
+ } else {
+ warn(
+ env.logging,
+ 'middleware',
+ "Middleware doesn't work for endpoints that return a simple body. The middleware will be disabled for this page."
+ );
+ }
+ }
if (response instanceof Response) {
attachToResponse(response, context.cookies);
diff --git a/packages/astro/src/core/errors/errors-data.ts b/packages/astro/src/core/errors/errors-data.ts
index bb9a86506..27425aee5 100644
--- a/packages/astro/src/core/errors/errors-data.ts
+++ b/packages/astro/src/core/errors/errors-data.ts
@@ -628,6 +628,95 @@ See https://docs.astro.build/en/guides/server-side-rendering/ for more informati
code: 3030,
message: 'The response has already been sent to the browser and cannot be altered.',
},
+
+ /**
+ * @docs
+ * @description
+ * Thrown when the middleware does not return any data or call the `next` function.
+ *
+ * For example:
+ * ```ts
+ * import {defineMiddleware} from "astro/middleware";
+ * export const onRequest = defineMiddleware((context, _) => {
+ * // doesn't return anything or call `next`
+ * context.locals.someData = false;
+ * });
+ * ```
+ */
+ MiddlewareNoDataOrNextCalled: {
+ title: "The middleware didn't return a response or call `next`",
+ code: 3031,
+ message:
+ 'The middleware needs to either return a `Response` object or call the `next` function.',
+ },
+
+ /**
+ * @docs
+ * @description
+ * Thrown in development mode when middleware returns something that is not a `Response` object.
+ *
+ * For example:
+ * ```ts
+ * import {defineMiddleware} from "astro/middleware";
+ * export const onRequest = defineMiddleware(() => {
+ * return "string"
+ * });
+ * ```
+ */
+ MiddlewareNotAResponse: {
+ title: 'The middleware returned something that is not a `Response` object',
+ code: 3032,
+ message: 'Any data returned from middleware must be a valid `Response` object.',
+ },
+
+ /**
+ * @docs
+ * @description
+ *
+ * Thrown in development mode when `locals` is overwritten with something that is not an object
+ *
+ * For example:
+ * ```ts
+ * import {defineMiddleware} from "astro/middleware";
+ * export const onRequest = defineMiddleware((context, next) => {
+ * context.locals = 1541;
+ * return next();
+ * });
+ * ```
+ */
+ LocalsNotAnObject: {
+ title: 'Value assigned to `locals` is not accepted',
+ code: 3033,
+ message:
+ '`locals` can only be assigned to an object. Other values like numbers, strings, etc. are not accepted.',
+ hint: 'If you tried to remove some information from the `locals` object, try to use `delete` or set the property to `undefined`.',
+ },
+
+ /**
+ * @docs
+ * @description
+ * Thrown in development mode when a user attempts to store something that is not serializable in `locals`.
+ *
+ * For example:
+ * ```ts
+ * import {defineMiddleware} from "astro/middleware";
+ * export const onRequest = defineMiddleware((context, next) => {
+ * context.locals = {
+ * foo() {
+ * alert("Hello world!")
+ * }
+ * };
+ * return next();
+ * });
+ * ```
+ */
+ LocalsNotSerializable: {
+ title: '`Astro.locals` is not serializable',
+ code: 3034,
+ message: (href: string) => {
+ return `The information stored in \`Astro.locals\` for the path "${href}" is not serializable.\nMake sure you store only serializable data.`;
+ },
+ },
// No headings here, that way Vite errors are merged with Astro ones in the docs, which makes more sense to users.
// Vite Errors - 4xxx
/**
diff --git a/packages/astro/src/core/middleware/callMiddleware.ts b/packages/astro/src/core/middleware/callMiddleware.ts
new file mode 100644
index 000000000..5836786e6
--- /dev/null
+++ b/packages/astro/src/core/middleware/callMiddleware.ts
@@ -0,0 +1,99 @@
+import type { APIContext, MiddlewareHandler, MiddlewareNext } from '../../@types/astro';
+import { AstroError, AstroErrorData } from '../errors/index.js';
+
+/**
+ * Utility function that is in charge of calling the middleware.
+ *
+ * It accepts a `R` generic, which usually is the `Response` returned.
+ * It is a generic because endpoints can return a different payload.
+ *
+ * When calling a middleware, we provide a `next` function, this function might or
+ * might not be called.
+ *
+ * A middleware, to behave correctly, can:
+ * - return a `Response`;
+ * - call `next`;
+ *
+ * Failing doing so will result an error. A middleware can call `next` and do not return a
+ * response. A middleware can not call `next` and return a new `Response` from scratch (maybe with a redirect).
+ *
+ * ```js
+ * const onRequest = async (context, next) => {
+ * const response = await next(context);
+ * return response;
+ * }
+ * ```
+ *
+ * ```js
+ * const onRequest = async (context, next) => {
+ * context.locals = "foo";
+ * next();
+ * }
+ * ```
+ *
+ * @param onRequest The function called which accepts a `context` and a `resolve` function
+ * @param apiContext The API context
+ * @param responseFunction A callback function that should return a promise with the response
+ */
+export async function callMiddleware<R>(
+ onRequest: MiddlewareHandler<R>,
+ apiContext: APIContext,
+ responseFunction: () => Promise<R>
+): Promise<Response | R> {
+ let resolveResolve: any;
+ new Promise((resolve) => {
+ resolveResolve = resolve;
+ });
+
+ let nextCalled = false;
+ const next: MiddlewareNext<R> = async () => {
+ nextCalled = true;
+ return await responseFunction();
+ };
+
+ let middlewarePromise = onRequest(apiContext, next);
+
+ return await Promise.resolve(middlewarePromise).then(async (value) => {
+ // first we check if `next` was called
+ if (nextCalled) {
+ /**
+ * Then we check if a value is returned. If so, we need to return the value returned by the
+ * middleware.
+ * e.g.
+ * ```js
+ * const response = await next();
+ * const new Response(null, { status: 500, headers: response.headers });
+ * ```
+ */
+ if (typeof value !== 'undefined') {
+ if (value instanceof Response === false) {
+ throw new AstroError(AstroErrorData.MiddlewareNotAResponse);
+ }
+ return value as R;
+ } else {
+ /**
+ * Here we handle the case where `next` was called and returned nothing.
+ */
+ const responseResult = await responseFunction();
+ return responseResult;
+ }
+ } else if (typeof value === 'undefined') {
+ /**
+ * There might be cases where `next` isn't called and the middleware **must** return
+ * something.
+ *
+ * If not thing is returned, then we raise an Astro error.
+ */
+ throw new AstroError(AstroErrorData.MiddlewareNoDataOrNextCalled);
+ } else if (value instanceof Response === false) {
+ throw new AstroError(AstroErrorData.MiddlewareNotAResponse);
+ } else {
+ // Middleware did not call resolve and returned a value
+ return value as R;
+ }
+ });
+}
+
+function isEndpointResult(response: any): boolean {
+ return response && typeof response.body !== 'undefined';
+}
diff --git a/packages/astro/src/core/middleware/index.ts b/packages/astro/src/core/middleware/index.ts
new file mode 100644
index 000000000..f9fb07bd4
--- /dev/null
+++ b/packages/astro/src/core/middleware/index.ts
@@ -0,0 +1,9 @@
+import type { MiddlewareResponseHandler } from '../../@types/astro';
+import { sequence } from './sequence.js';
+
+function defineMiddleware(fn: MiddlewareResponseHandler) {
+ return fn;
+}
+
+// NOTE: this export must export only the functions that will be exposed to user-land as officials APIs
+export { sequence, defineMiddleware };
diff --git a/packages/astro/src/core/middleware/loadMiddleware.ts b/packages/astro/src/core/middleware/loadMiddleware.ts
new file mode 100644
index 000000000..5c64565af
--- /dev/null
+++ b/packages/astro/src/core/middleware/loadMiddleware.ts
@@ -0,0 +1,22 @@
+import type { AstroSettings } from '../../@types/astro';
+import { MIDDLEWARE_PATH_SEGMENT_NAME } from '../constants.js';
+import type { ModuleLoader } from '../module-loader';
+
+/**
+ * It accepts a module loader and the astro settings, and it attempts to load the middlewares defined in the configuration.
+ *
+ * If not middlewares were not set, the function returns an empty array.
+ */
+export async function loadMiddleware(
+ moduleLoader: ModuleLoader,
+ srcDir: AstroSettings['config']['srcDir']
+) {
+ // can't use node Node.js builtins
+ let middlewarePath = srcDir.pathname + '/' + MIDDLEWARE_PATH_SEGMENT_NAME;
+ try {
+ const module = await moduleLoader.import(middlewarePath);
+ return module;
+ } catch {
+ return void 0;
+ }
+}
diff --git a/packages/astro/src/core/middleware/sequence.ts b/packages/astro/src/core/middleware/sequence.ts
new file mode 100644
index 000000000..0358f3719
--- /dev/null
+++ b/packages/astro/src/core/middleware/sequence.ts
@@ -0,0 +1,36 @@
+import type { APIContext, MiddlewareResponseHandler } from '../../@types/astro';
+import { defineMiddleware } from './index.js';
+
+// From SvelteKit: https://github.com/sveltejs/kit/blob/master/packages/kit/src/exports/hooks/sequence.js
+/**
+ *
+ * It accepts one or more middleware handlers and makes sure that they are run in sequence.
+ */
+export function sequence(...handlers: MiddlewareResponseHandler[]): MiddlewareResponseHandler {
+ const length = handlers.length;
+ if (!length) {
+ const handler: MiddlewareResponseHandler = defineMiddleware((context, next) => {
+ return next();
+ });
+ return handler;
+ }
+
+ return defineMiddleware((context, next) => {
+ return applyHandle(0, context);
+
+ function applyHandle(i: number, handleContext: APIContext) {
+ const handle = handlers[i];
+ // @ts-expect-error
+ // SAFETY: Usually `next` always returns something in user land, but in `sequence` we are actually
+ // doing a loop over all the `next` functions, and eventually we call the last `next` that returns the `Response`.
+ const result = handle(handleContext, async () => {
+ if (i < length - 1) {
+ return applyHandle(i + 1, handleContext);
+ } else {
+ return next();
+ }
+ });
+ return result;
+ }
+ });
+}
diff --git a/packages/astro/src/core/render/context.ts b/packages/astro/src/core/render/context.ts
index f6a82e9ca..d4efe35df 100644
--- a/packages/astro/src/core/render/context.ts
+++ b/packages/astro/src/core/render/context.ts
@@ -1,4 +1,13 @@
-import type { RouteData, SSRElement, SSRResult } from '../../@types/astro';
+import type {
+ ComponentInstance,
+ Params,
+ Props,
+ RouteData,
+ SSRElement,
+ SSRResult,
+} from '../../@types/astro';
+import { getParamsAndPropsOrThrow } from './core.js';
+import type { Environment } from './environment';
/**
* The RenderContext represents the parts of rendering that are specific to one request.
@@ -14,22 +23,38 @@ export interface RenderContext {
componentMetadata?: SSRResult['componentMetadata'];
route?: RouteData;
status?: number;
+ params: Params;
+ props: Props;
}
export type CreateRenderContextArgs = Partial<RenderContext> & {
origin?: string;
request: RenderContext['request'];
+ mod: ComponentInstance;
+ env: Environment;
};
-export function createRenderContext(options: CreateRenderContextArgs): RenderContext {
+export async function createRenderContext(
+ options: CreateRenderContextArgs
+): Promise<RenderContext> {
const request = options.request;
const url = new URL(request.url);
const origin = options.origin ?? url.origin;
const pathname = options.pathname ?? url.pathname;
+ const [params, props] = await getParamsAndPropsOrThrow({
+ mod: options.mod as any,
+ route: options.route,
+ routeCache: options.env.routeCache,
+ pathname: pathname,
+ logging: options.env.logging,
+ ssr: options.env.ssr,
+ });
return {
...options,
origin,
pathname,
url,
+ params,
+ props,
};
}
diff --git a/packages/astro/src/core/render/core.ts b/packages/astro/src/core/render/core.ts
index 8687e9006..fd57ad8bc 100644
--- a/packages/astro/src/core/render/core.ts
+++ b/packages/astro/src/core/render/core.ts
@@ -1,12 +1,11 @@
-import type { ComponentInstance, Params, Props, RouteData } from '../../@types/astro';
-import type { LogOptions } from '../logger/core.js';
-import type { RenderContext } from './context.js';
-import type { Environment } from './environment.js';
-
+import type { APIContext, ComponentInstance, Params, Props, RouteData } from '../../@types/astro';
import { renderPage as runtimeRenderPage } from '../../runtime/server/index.js';
import { attachToResponse } from '../cookies/index.js';
import { AstroError, AstroErrorData } from '../errors/index.js';
+import type { LogOptions } from '../logger/core.js';
import { getParams } from '../routing/params.js';
+import type { RenderContext } from './context.js';
+import type { Environment } from './environment.js';
import { createResult } from './result.js';
import { callGetStaticPaths, findPathItemByKey, RouteCache } from './route-cache.js';
@@ -23,6 +22,26 @@ export const enum GetParamsAndPropsError {
NoMatchingStaticPath,
}
+/**
+ * It retrieves `Params` and `Props`, or throws an error
+ * if they are not correctly retrieved.
+ */
+export async function getParamsAndPropsOrThrow(
+ options: GetParamsAndPropsOptions
+): Promise<[Params, Props]> {
+ let paramsAndPropsResp = await getParamsAndProps(options);
+ if (paramsAndPropsResp === GetParamsAndPropsError.NoMatchingStaticPath) {
+ throw new AstroError({
+ ...AstroErrorData.NoMatchingStaticPathFound,
+ message: AstroErrorData.NoMatchingStaticPathFound.message(options.pathname),
+ hint: options.route?.component
+ ? AstroErrorData.NoMatchingStaticPathFound.hint([options.route?.component])
+ : '',
+ });
+ }
+ return paramsAndPropsResp;
+}
+
export async function getParamsAndProps(
opts: GetParamsAndPropsOptions
): Promise<[Params, Props] | GetParamsAndPropsError> {
@@ -84,65 +103,63 @@ export async function getParamsAndProps(
return [params, pageProps];
}
-export async function renderPage(mod: ComponentInstance, ctx: RenderContext, env: Environment) {
- const paramsAndPropsRes = await getParamsAndProps({
- logging: env.logging,
- mod,
- route: ctx.route,
- routeCache: env.routeCache,
- pathname: ctx.pathname,
- ssr: env.ssr,
- });
-
- if (paramsAndPropsRes === GetParamsAndPropsError.NoMatchingStaticPath) {
- throw new AstroError({
- ...AstroErrorData.NoMatchingStaticPathFound,
- message: AstroErrorData.NoMatchingStaticPathFound.message(ctx.pathname),
- hint: ctx.route?.component
- ? AstroErrorData.NoMatchingStaticPathFound.hint([ctx.route?.component])
- : '',
- });
- }
- const [params, pageProps] = paramsAndPropsRes;
+export type RenderPage = {
+ mod: ComponentInstance;
+ renderContext: RenderContext;
+ env: Environment;
+ apiContext?: APIContext;
+};
+export async function renderPage({ mod, renderContext, env, apiContext }: RenderPage) {
// Validate the page component before rendering the page
const Component = mod.default;
if (!Component)
throw new Error(`Expected an exported Astro component but received typeof ${typeof Component}`);
+ let locals = {};
+ if (apiContext) {
+ if (env.mode === 'development' && !isValueSerializable(apiContext.locals)) {
+ throw new AstroError({
+ ...AstroErrorData.LocalsNotSerializable,
+ message: AstroErrorData.LocalsNotSerializable.message(renderContext.pathname),
+ });
+ }
+ locals = apiContext.locals;
+ }
const result = createResult({
adapterName: env.adapterName,
- links: ctx.links,
- styles: ctx.styles,
+ links: renderContext.links,
+ styles: renderContext.styles,
logging: env.logging,
markdown: env.markdown,
mode: env.mode,
- origin: ctx.origin,
- params,
- props: pageProps,
- pathname: ctx.pathname,
- componentMetadata: ctx.componentMetadata,
+ origin: renderContext.origin,
+ params: renderContext.params,
+ props: renderContext.props,
+ pathname: renderContext.pathname,
+ componentMetadata: renderContext.componentMetadata,
resolve: env.resolve,
renderers: env.renderers,
- request: ctx.request,
+ request: renderContext.request,
site: env.site,
- scripts: ctx.scripts,
+ scripts: renderContext.scripts,
ssr: env.ssr,
- status: ctx.status ?? 200,
+ status: renderContext.status ?? 200,
+ locals,
});
// Support `export const components` for `MDX` pages
if (typeof (mod as any).components === 'object') {
- Object.assign(pageProps, { components: (mod as any).components });
+ Object.assign(renderContext.props, { components: (mod as any).components });
}
- const response = await runtimeRenderPage(
+ let response = await runtimeRenderPage(
result,
Component,
- pageProps,
+ renderContext.props,
null,
env.streaming,
- ctx.route
+ renderContext.route
);
// If there is an Astro.cookies instance, attach it to the response so that
@@ -153,3 +170,57 @@ export async function renderPage(mod: ComponentInstance, ctx: RenderContext, env
return response;
}
+
+/**
+ * Checks whether any value can is serializable.
+ *
+ * A serializable value contains plain values. For example, `Proxy`, `Set`, `Map`, functions, etc.
+ * are not serializable objects.
+ *
+ * @param object
+ */
+export function isValueSerializable(value: unknown): boolean {
+ let type = typeof value;
+ let plainObject = true;
+ if (type === 'object' && isPlainObject(value)) {
+ for (const [, nestedValue] of Object.entries(value)) {
+ if (!isValueSerializable(nestedValue)) {
+ plainObject = false;
+ break;
+ }
+ }
+ } else {
+ plainObject = false;
+ }
+ let result =
+ value === null ||
+ type === 'string' ||
+ type === 'number' ||
+ type === 'boolean' ||
+ Array.isArray(value) ||
+ plainObject;
+
+ return result;
+}
+
+/**
+ *
+ * From [redux-toolkit](https://github.com/reduxjs/redux-toolkit/blob/master/packages/toolkit/src/isPlainObject.ts)
+ *
+ * Returns true if the passed value is "plain" object, i.e. an object whose
+ * prototype is the root `Object.prototype`. This includes objects created
+ * using object literals, but not for instance for class instances.
+ */
+function isPlainObject(value: unknown): value is object {
+ if (typeof value !== 'object' || value === null) return false;
+
+ let proto = Object.getPrototypeOf(value);
+ if (proto === null) return true;
+
+ let baseProto = proto;
+ while (Object.getPrototypeOf(baseProto) !== null) {
+ baseProto = Object.getPrototypeOf(baseProto);
+ }
+
+ return proto === baseProto;
+}
diff --git a/packages/astro/src/core/render/dev/index.ts b/packages/astro/src/core/render/dev/index.ts
index 51920e800..fbbe0d48d 100644
--- a/packages/astro/src/core/render/dev/index.ts
+++ b/packages/astro/src/core/render/dev/index.ts
@@ -1,14 +1,18 @@
import { fileURLToPath } from 'url';
import type {
+ AstroMiddlewareInstance,
AstroSettings,
ComponentInstance,
+ MiddlewareResponseHandler,
RouteData,
SSRElement,
SSRLoadedRenderer,
} from '../../../@types/astro';
import { PAGE_SCRIPT_ID } from '../../../vite-plugin-scripts/index.js';
+import { createAPIContext } from '../../endpoint/index.js';
import { enhanceViteSSRError } from '../../errors/dev/index.js';
import { AggregateError, CSSError, MarkdownError } from '../../errors/index.js';
+import { callMiddleware } from '../../middleware/callMiddleware.js';
import type { ModuleLoader } from '../../module-loader/index';
import { isPage, resolveIdToUrl, viteID } from '../../util.js';
import { createRenderContext, renderPage as coreRenderPage } from '../index.js';
@@ -35,6 +39,10 @@ export interface SSROptions {
request: Request;
/** optional, in case we need to render something outside of a dev server */
route?: RouteData;
+ /**
+ * Optional middlewares
+ */
+ middleware?: AstroMiddlewareInstance<unknown>;
}
export type ComponentPreload = [SSRLoadedRenderer[], ComponentInstance];
@@ -158,8 +166,9 @@ export async function renderPage(options: SSROptions): Promise<Response> {
env: options.env,
filePath: options.filePath,
});
+ const { env } = options;
- const ctx = createRenderContext({
+ const renderContext = await createRenderContext({
request: options.request,
origin: options.origin,
pathname: options.pathname,
@@ -168,7 +177,25 @@ export async function renderPage(options: SSROptions): Promise<Response> {
styles,
componentMetadata: metadata,
route: options.route,
+ mod,
+ env,
});
+ if (options.middleware) {
+ if (options.middleware && options.middleware.onRequest) {
+ const apiContext = createAPIContext({
+ request: options.request,
+ params: renderContext.params,
+ props: renderContext.props,
+ adapterName: options.env.adapterName,
+ });
+
+ const onRequest = options.middleware.onRequest as MiddlewareResponseHandler;
+ const response = await callMiddleware<Response>(onRequest, apiContext, () => {
+ return coreRenderPage({ mod, renderContext, env: options.env, apiContext });
+ });
- return await coreRenderPage(mod, ctx, options.env); // NOTE: without "await", errors won’t get caught below
+ return response;
+ }
+ }
+ return await coreRenderPage({ mod, renderContext, env: options.env }); // NOTE: without "await", errors won’t get caught below
}
diff --git a/packages/astro/src/core/render/index.ts b/packages/astro/src/core/render/index.ts
index 99d680549..4e4df8239 100644
--- a/packages/astro/src/core/render/index.ts
+++ b/packages/astro/src/core/render/index.ts
@@ -1,6 +1,11 @@
export { createRenderContext } from './context.js';
export type { RenderContext } from './context.js';
-export { getParamsAndProps, GetParamsAndPropsError, renderPage } from './core.js';
+export {
+ getParamsAndProps,
+ GetParamsAndPropsError,
+ getParamsAndPropsOrThrow,
+ renderPage,
+} from './core.js';
export type { Environment } from './environment';
export { createBasicEnvironment, createEnvironment } from './environment.js';
export { loadRenderer } from './renderer.js';
diff --git a/packages/astro/src/core/render/result.ts b/packages/astro/src/core/render/result.ts
index 26ea22eee..598ec116f 100644
--- a/packages/astro/src/core/render/result.ts
+++ b/packages/astro/src/core/render/result.ts
@@ -50,6 +50,7 @@ export interface CreateResultArgs {
componentMetadata?: SSRResult['componentMetadata'];
request: Request;
status: number;
+ locals: App.Locals;
}
function getFunctionExpression(slot: any) {
@@ -131,7 +132,7 @@ class Slots {
let renderMarkdown: any = null;
export function createResult(args: CreateResultArgs): SSRResult {
- const { markdown, params, pathname, renderers, request, resolve } = args;
+ const { markdown, params, pathname, renderers, request, resolve, locals } = args;
const url = new URL(request.url);
const headers = new Headers();
@@ -200,6 +201,7 @@ export function createResult(args: CreateResultArgs): SSRResult {
},
params,
props,
+ locals,
request,
url,
redirect: args.ssr
diff --git a/packages/astro/src/core/request.ts b/packages/astro/src/core/request.ts
index 24356983f..d8ac9033d 100644
--- a/packages/astro/src/core/request.ts
+++ b/packages/astro/src/core/request.ts
@@ -16,6 +16,7 @@ export interface CreateRequestOptions {
}
const clientAddressSymbol = Symbol.for('astro.clientAddress');
+const clientLocalsSymbol = Symbol.for('astro.locals');
export function createRequest({
url,
@@ -65,5 +66,7 @@ export function createRequest({
Reflect.set(request, clientAddressSymbol, clientAddress);
}
+ Reflect.set(request, clientLocalsSymbol, {});
+
return request;
}
diff --git a/packages/astro/src/integrations/index.ts b/packages/astro/src/integrations/index.ts
index 9b86259ae..d306e7be3 100644
--- a/packages/astro/src/integrations/index.ts
+++ b/packages/astro/src/integrations/index.ts
@@ -55,6 +55,7 @@ export async function runHookConfigSetup({
let updatedConfig: AstroConfig = { ...settings.config };
let updatedSettings: AstroSettings = { ...settings, config: updatedConfig };
+
for (const integration of settings.config.integrations) {
/**
* By making integration hooks optional, Astro can now ignore null or undefined Integrations
@@ -68,7 +69,7 @@ export async function runHookConfigSetup({
* ]
* ```
*/
- if (integration?.hooks?.['astro:config:setup']) {
+ if (integration.hooks?.['astro:config:setup']) {
const hooks: HookParameters<'astro:config:setup'> = {
config: updatedConfig,
command,
diff --git a/packages/astro/src/runtime/server/endpoint.ts b/packages/astro/src/runtime/server/endpoint.ts
index 33ce8f6f9..9780d6599 100644
--- a/packages/astro/src/runtime/server/endpoint.ts
+++ b/packages/astro/src/runtime/server/endpoint.ts
@@ -19,7 +19,7 @@ function getHandlerFromModule(mod: EndpointHandler, method: string) {
/** Renders an endpoint request to completion, returning the body. */
export async function renderEndpoint(mod: EndpointHandler, context: APIContext, ssr: boolean) {
- const { request, params } = context;
+ const { request, params, locals } = context;
const chosenMethod = request.method?.toLowerCase();
const handler = getHandlerFromModule(mod, chosenMethod);
if (!ssr && ssr === false && chosenMethod && chosenMethod !== 'get') {
diff --git a/packages/astro/src/vite-plugin-astro-server/route.ts b/packages/astro/src/vite-plugin-astro-server/route.ts
index da280f7e1..cb2e76178 100644
--- a/packages/astro/src/vite-plugin-astro-server/route.ts
+++ b/packages/astro/src/vite-plugin-astro-server/route.ts
@@ -1,17 +1,17 @@
import type http from 'http';
import mime from 'mime';
import type { ComponentInstance, ManifestData, RouteData } from '../@types/astro';
-import type {
- ComponentPreload,
- DevelopmentEnvironment,
- SSROptions,
-} from '../core/render/dev/index';
-
import { attachToResponse } from '../core/cookies/index.js';
import { call as callEndpoint } from '../core/endpoint/dev/index.js';
import { throwIfRedirectNotAllowed } from '../core/endpoint/index.js';
import { AstroErrorData } from '../core/errors/index.js';
import { warn } from '../core/logger/core.js';
+import { loadMiddleware } from '../core/middleware/loadMiddleware.js';
+import type {
+ ComponentPreload,
+ DevelopmentEnvironment,
+ SSROptions,
+} from '../core/render/dev/index';
import { preload, renderPage } from '../core/render/dev/index.js';
import { getParamsAndProps, GetParamsAndPropsError } from '../core/render/index.js';
import { createRequest } from '../core/request.js';
@@ -169,7 +169,12 @@ export async function handleRoute(
request,
route,
};
-
+ if (env.settings.config.experimental.middleware) {
+ const middleware = await loadMiddleware(env.loader, env.settings.config.srcDir);
+ if (middleware) {
+ options.middleware = middleware;
+ }
+ }
// Route successfully matched! Render it.
if (route.type === 'endpoint') {
const result = await callEndpoint(options, logging);
diff --git a/packages/astro/test/fixtures/middleware-dev/astro.config.mjs b/packages/astro/test/fixtures/middleware-dev/astro.config.mjs
new file mode 100644
index 000000000..4379be246
--- /dev/null
+++ b/packages/astro/test/fixtures/middleware-dev/astro.config.mjs
@@ -0,0 +1,7 @@
+import { defineConfig } from 'astro/config';
+
+export default defineConfig({
+ experimental: {
+ middleware: true
+ }
+});
diff --git a/packages/astro/test/fixtures/middleware-dev/package.json b/packages/astro/test/fixtures/middleware-dev/package.json
new file mode 100644
index 000000000..bc889aa63
--- /dev/null
+++ b/packages/astro/test/fixtures/middleware-dev/package.json
@@ -0,0 +1,8 @@
+{
+ "name": "@test/middleware-dev",
+ "version": "0.0.0",
+ "private": true,
+ "dependencies": {
+ "astro": "workspace:*"
+ }
+}
diff --git a/packages/astro/test/fixtures/middleware-dev/src/middleware.js b/packages/astro/test/fixtures/middleware-dev/src/middleware.js
new file mode 100644
index 000000000..2a09552e7
--- /dev/null
+++ b/packages/astro/test/fixtures/middleware-dev/src/middleware.js
@@ -0,0 +1,40 @@
+import { sequence, defineMiddleware } from 'astro/middleware';
+
+const first = defineMiddleware(async (context, next) => {
+ if (context.request.url.includes('/lorem')) {
+ context.locals.name = 'ipsum';
+ } else if (context.request.url.includes('/rewrite')) {
+ return new Response('<span>New content!!</span>', {
+ status: 200,
+ });
+ } else if (context.request.url.includes('/broken-500')) {
+ return new Response(null, {
+ status: 500,
+ });
+ } else {
+ context.locals.name = 'bar';
+ }
+ return await next();
+});
+
+const second = defineMiddleware(async (context, next) => {
+ if (context.request.url.includes('/second')) {
+ context.locals.name = 'second';
+ } else if (context.request.url.includes('/redirect')) {
+ return context.redirect('/', 302);
+ }
+ return await next();
+});
+
+const third = defineMiddleware(async (context, next) => {
+ if (context.request.url.includes('/broken-locals')) {
+ context.locals = {
+ fn() {},
+ };
+ } else if (context.request.url.includes('/does-nothing')) {
+ return undefined;
+ }
+ next();
+});
+
+export const onRequest = sequence(first, second, third);
diff --git a/packages/astro/test/fixtures/middleware-dev/src/pages/broken-500.astro b/packages/astro/test/fixtures/middleware-dev/src/pages/broken-500.astro
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/packages/astro/test/fixtures/middleware-dev/src/pages/broken-500.astro
diff --git a/packages/astro/test/fixtures/middleware-dev/src/pages/broken-locals.astro b/packages/astro/test/fixtures/middleware-dev/src/pages/broken-locals.astro
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/packages/astro/test/fixtures/middleware-dev/src/pages/broken-locals.astro
diff --git a/packages/astro/test/fixtures/middleware-dev/src/pages/does-nothing.astro b/packages/astro/test/fixtures/middleware-dev/src/pages/does-nothing.astro
new file mode 100644
index 000000000..344b3797b
--- /dev/null
+++ b/packages/astro/test/fixtures/middleware-dev/src/pages/does-nothing.astro
@@ -0,0 +1,9 @@
+<html>
+<head>
+ <title>Testing</title>
+</head>
+<body>
+
+<p>Not interested</p>
+</body>
+</html>
diff --git a/packages/astro/test/fixtures/middleware-dev/src/pages/index.astro b/packages/astro/test/fixtures/middleware-dev/src/pages/index.astro
new file mode 100644
index 000000000..395a4d695
--- /dev/null
+++ b/packages/astro/test/fixtures/middleware-dev/src/pages/index.astro
@@ -0,0 +1,14 @@
+---
+const data = Astro.locals;
+---
+
+<html>
+<head>
+ <title>Testing</title>
+</head>
+<body>
+
+ <span>Index</span>
+ <p>{data?.name}</p>
+</body>
+</html>
diff --git a/packages/astro/test/fixtures/middleware-dev/src/pages/lorem.astro b/packages/astro/test/fixtures/middleware-dev/src/pages/lorem.astro
new file mode 100644
index 000000000..c6edf9cd7
--- /dev/null
+++ b/packages/astro/test/fixtures/middleware-dev/src/pages/lorem.astro
@@ -0,0 +1,13 @@
+---
+const data = Astro.locals;
+---
+
+<html>
+<head>
+ <title>Testing</title>
+</head>
+<body>
+
+<p>{data?.name}</p>
+</body>
+</html>
diff --git a/packages/astro/test/fixtures/middleware-dev/src/pages/not-interested.astro b/packages/astro/test/fixtures/middleware-dev/src/pages/not-interested.astro
new file mode 100644
index 000000000..344b3797b
--- /dev/null
+++ b/packages/astro/test/fixtures/middleware-dev/src/pages/not-interested.astro
@@ -0,0 +1,9 @@
+<html>
+<head>
+ <title>Testing</title>
+</head>
+<body>
+
+<p>Not interested</p>
+</body>
+</html>
diff --git a/packages/astro/test/fixtures/middleware-dev/src/pages/redirect.astro b/packages/astro/test/fixtures/middleware-dev/src/pages/redirect.astro
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/packages/astro/test/fixtures/middleware-dev/src/pages/redirect.astro
diff --git a/packages/astro/test/fixtures/middleware-dev/src/pages/rewrite.astro b/packages/astro/test/fixtures/middleware-dev/src/pages/rewrite.astro
new file mode 100644
index 000000000..f7f70dc88
--- /dev/null
+++ b/packages/astro/test/fixtures/middleware-dev/src/pages/rewrite.astro
@@ -0,0 +1,9 @@
+<html>
+<head>
+ <title>Testing</title>
+</head>
+<body>
+
+ <p>Rewrite</p>
+</body>
+</html>
diff --git a/packages/astro/test/fixtures/middleware-dev/src/pages/second.astro b/packages/astro/test/fixtures/middleware-dev/src/pages/second.astro
new file mode 100644
index 000000000..c6edf9cd7
--- /dev/null
+++ b/packages/astro/test/fixtures/middleware-dev/src/pages/second.astro
@@ -0,0 +1,13 @@
+---
+const data = Astro.locals;
+---
+
+<html>
+<head>
+ <title>Testing</title>
+</head>
+<body>
+
+<p>{data?.name}</p>
+</body>
+</html>
diff --git a/packages/astro/test/fixtures/middleware-ssg/astro.config.mjs b/packages/astro/test/fixtures/middleware-ssg/astro.config.mjs
new file mode 100644
index 000000000..2f2e911a8
--- /dev/null
+++ b/packages/astro/test/fixtures/middleware-ssg/astro.config.mjs
@@ -0,0 +1,8 @@
+import { defineConfig } from 'astro/config';
+
+export default defineConfig({
+ output: "static",
+ experimental: {
+ middleware: true
+ }
+});
diff --git a/packages/astro/test/fixtures/middleware-ssg/package.json b/packages/astro/test/fixtures/middleware-ssg/package.json
new file mode 100644
index 000000000..2ac442454
--- /dev/null
+++ b/packages/astro/test/fixtures/middleware-ssg/package.json
@@ -0,0 +1,8 @@
+{
+ "name": "@test/middleware-ssg",
+ "version": "0.0.0",
+ "private": true,
+ "dependencies": {
+ "astro": "workspace:*"
+ }
+}
diff --git a/packages/astro/test/fixtures/middleware-ssg/src/middleware.js b/packages/astro/test/fixtures/middleware-ssg/src/middleware.js
new file mode 100644
index 000000000..f28d89f67
--- /dev/null
+++ b/packages/astro/test/fixtures/middleware-ssg/src/middleware.js
@@ -0,0 +1,12 @@
+import { sequence, defineMiddleware } from 'astro/middleware';
+
+const first = defineMiddleware(async (context, next) => {
+ if (context.request.url.includes('/second')) {
+ context.locals.name = 'second';
+ } else {
+ context.locals.name = 'bar';
+ }
+ return await next();
+});
+
+export const onRequest = sequence(first);
diff --git a/packages/astro/test/fixtures/middleware-ssg/src/pages/index.astro b/packages/astro/test/fixtures/middleware-ssg/src/pages/index.astro
new file mode 100644
index 000000000..395a4d695
--- /dev/null
+++ b/packages/astro/test/fixtures/middleware-ssg/src/pages/index.astro
@@ -0,0 +1,14 @@
+---
+const data = Astro.locals;
+---
+
+<html>
+<head>
+ <title>Testing</title>
+</head>
+<body>
+
+ <span>Index</span>
+ <p>{data?.name}</p>
+</body>
+</html>
diff --git a/packages/astro/test/fixtures/middleware-ssg/src/pages/second.astro b/packages/astro/test/fixtures/middleware-ssg/src/pages/second.astro
new file mode 100644
index 000000000..c6edf9cd7
--- /dev/null
+++ b/packages/astro/test/fixtures/middleware-ssg/src/pages/second.astro
@@ -0,0 +1,13 @@
+---
+const data = Astro.locals;
+---
+
+<html>
+<head>
+ <title>Testing</title>
+</head>
+<body>
+
+<p>{data?.name}</p>
+</body>
+</html>
diff --git a/packages/astro/test/middleware.test.js b/packages/astro/test/middleware.test.js
new file mode 100644
index 000000000..784a32c30
--- /dev/null
+++ b/packages/astro/test/middleware.test.js
@@ -0,0 +1,202 @@
+import { loadFixture } from './test-utils.js';
+import { expect } from 'chai';
+import * as cheerio from 'cheerio';
+import testAdapter from './test-adapter.js';
+
+describe('Middleware in DEV mode', () => {
+ /** @type {import('./test-utils').Fixture} */
+ let fixture;
+ let devServer;
+
+ before(async () => {
+ fixture = await loadFixture({
+ root: './fixtures/middleware-dev/',
+ });
+ devServer = await fixture.startDevServer();
+ });
+
+ after(async () => {
+ await devServer.stop();
+ });
+
+ it('should render locals data', async () => {
+ const html = await fixture.fetch('/').then((res) => res.text());
+ const $ = cheerio.load(html);
+ expect($('p').html()).to.equal('bar');
+ });
+
+ it('should change locals data based on URL', async () => {
+ let html = await fixture.fetch('/').then((res) => res.text());
+ let $ = cheerio.load(html);
+ expect($('p').html()).to.equal('bar');
+
+ html = await fixture.fetch('/lorem').then((res) => res.text());
+ $ = cheerio.load(html);
+ expect($('p').html()).to.equal('ipsum');
+ });
+
+ it('should call a second middleware', async () => {
+ let html = await fixture.fetch('/second').then((res) => res.text());
+ let $ = cheerio.load(html);
+ expect($('p').html()).to.equal('second');
+ });
+
+ it('should successfully create a new response', async () => {
+ let html = await fixture.fetch('/rewrite').then((res) => res.text());
+ let $ = cheerio.load(html);
+ expect($('p').html()).to.be.null;
+ expect($('span').html()).to.equal('New content!!');
+ });
+
+ it('should return a new response that is a 500', async () => {
+ await fixture.fetch('/broken-500').then((res) => {
+ expect(res.status).to.equal(500);
+ return res.text();
+ });
+ });
+
+ it('should successfully render a page if the middleware calls only next() and returns nothing', async () => {
+ let html = await fixture.fetch('/not-interested').then((res) => res.text());
+ let $ = cheerio.load(html);
+ expect($('p').html()).to.equal('Not interested');
+ });
+
+ it('should throw an error when locals are not serializable', async () => {
+ let html = await fixture.fetch('/broken-locals').then((res) => res.text());
+ let $ = cheerio.load(html);
+ expect($('title').html()).to.equal('LocalsNotSerializable');
+ });
+
+ it("should throw an error when the middleware doesn't call next or doesn't return a response", async () => {
+ let html = await fixture.fetch('/does-nothing').then((res) => res.text());
+ let $ = cheerio.load(html);
+ expect($('title').html()).to.equal('MiddlewareNoDataOrNextCalled');
+ });
+});
+
+describe('Middleware in PROD mode, SSG', () => {
+ /** @type {import('./test-utils').Fixture} */
+ let fixture;
+ /** @type {import('./test-utils').PreviewServer} */
+ let previewServer;
+
+ before(async () => {
+ fixture = await loadFixture({
+ root: './fixtures/middleware-ssg/',
+ });
+ await fixture.build();
+ });
+
+ it('should render locals data', async () => {
+ const html = await fixture.readFile('/index.html');
+ const $ = cheerio.load(html);
+ expect($('p').html()).to.equal('bar');
+ });
+
+ it('should change locals data based on URL', async () => {
+ let html = await fixture.readFile('/index.html');
+ let $ = cheerio.load(html);
+ expect($('p').html()).to.equal('bar');
+
+ html = await fixture.readFile('/second/index.html');
+ $ = cheerio.load(html);
+ expect($('p').html()).to.equal('second');
+ });
+});
+
+describe('Middleware API in PROD mode, SSR', () => {
+ /** @type {import('./test-utils').Fixture} */
+ let fixture;
+
+ before(async () => {
+ fixture = await loadFixture({
+ root: './fixtures/middleware-dev/',
+ output: 'server',
+ adapter: testAdapter({
+ // exports: ['manifest', 'createApp', 'middleware'],
+ }),
+ });
+ await fixture.build();
+ });
+
+ it('should render locals data', async () => {
+ const app = await fixture.loadTestAdapterApp();
+ const request = new Request('http://example.com/');
+ const response = await app.render(request);
+ const html = await response.text();
+ const $ = cheerio.load(html);
+ expect($('p').html()).to.equal('bar');
+ });
+
+ it('should change locals data based on URL', async () => {
+ const app = await fixture.loadTestAdapterApp();
+ let response = await app.render(new Request('http://example.com/'));
+ let html = await response.text();
+ let $ = cheerio.load(html);
+ expect($('p').html()).to.equal('bar');
+
+ response = await app.render(new Request('http://example.com/lorem'));
+ html = await response.text();
+ $ = cheerio.load(html);
+ expect($('p').html()).to.equal('ipsum');
+ });
+
+ it('should successfully redirect to another page', async () => {
+ const app = await fixture.loadTestAdapterApp();
+ const request = new Request('http://example.com/redirect');
+ const response = await app.render(request);
+ expect(response.status).to.equal(302);
+ });
+
+ it('should call a second middleware', async () => {
+ const app = await fixture.loadTestAdapterApp();
+ const response = await app.render(new Request('http://example.com/second'));
+ const html = await response.text();
+ const $ = cheerio.load(html);
+ expect($('p').html()).to.equal('second');
+ });
+
+ it('should successfully create a new response', async () => {
+ const app = await fixture.loadTestAdapterApp();
+ const request = new Request('http://example.com/rewrite');
+ const response = await app.render(request);
+ const html = await response.text();
+ const $ = cheerio.load(html);
+ expect($('p').html()).to.be.null;
+ expect($('span').html()).to.equal('New content!!');
+ });
+
+ it('should return a new response that is a 500', async () => {
+ const app = await fixture.loadTestAdapterApp();
+ const request = new Request('http://example.com/broken-500');
+ const response = await app.render(request);
+ expect(response.status).to.equal(500);
+ });
+
+ it('should successfully render a page if the middleware calls only next() and returns nothing', async () => {
+ const app = await fixture.loadTestAdapterApp();
+ const request = new Request('http://example.com/not-interested');
+ const response = await app.render(request);
+ const html = await response.text();
+ const $ = cheerio.load(html);
+ expect($('p').html()).to.equal('Not interested');
+ });
+
+ it('should NOT throw an error when locals are not serializable', async () => {
+ const app = await fixture.loadTestAdapterApp();
+ const request = new Request('http://example.com/broken-locals');
+ const response = await app.render(request);
+ const html = await response.text();
+ const $ = cheerio.load(html);
+ expect($('title').html()).to.not.equal('LocalsNotSerializable');
+ });
+
+ it("should throws an error when the middleware doesn't call next or doesn't return a response", async () => {
+ const app = await fixture.loadTestAdapterApp();
+ const request = new Request('http://example.com/does-nothing');
+ const response = await app.render(request);
+ const html = await response.text();
+ const $ = cheerio.load(html);
+ expect($('title').html()).to.not.equal('MiddlewareNoDataReturned');
+ });
+});
diff --git a/packages/astro/test/test-adapter.js b/packages/astro/test/test-adapter.js
index 4faa9a7c6..cc34e3c33 100644
--- a/packages/astro/test/test-adapter.js
+++ b/packages/astro/test/test-adapter.js
@@ -42,6 +42,7 @@ export default function ({ provideAddress = true, extendAdapter } = { provideAdd
return new Response(data);
}
+ Reflect.set(request, Symbol.for('astro.locals'), {});
${provideAddress ? `request[Symbol.for('astro.clientAddress')] = '0.0.0.0';` : ''}
return super.render(request, routeData);
}
@@ -51,6 +52,7 @@ export default function ({ provideAddress = true, extendAdapter } = { provideAdd
return {
manifest,
createApp: (streaming) => new MyApp(manifest, streaming)
+
};
}
`;
diff --git a/packages/astro/test/test-utils.js b/packages/astro/test/test-utils.js
index 6e3113978..f933a13ad 100644
--- a/packages/astro/test/test-utils.js
+++ b/packages/astro/test/test-utils.js
@@ -231,7 +231,7 @@ export async function loadFixture(inlineConfig) {
},
loadTestAdapterApp: async (streaming) => {
const url = new URL(`./server/entry.mjs?id=${fixtureId}`, config.outDir);
- const { createApp, manifest } = await import(url);
+ const { createApp, manifest, middleware } = await import(url);
const app = createApp(streaming);
app.manifest = manifest;
return app;
diff --git a/packages/astro/test/units/render/head.test.js b/packages/astro/test/units/render/head.test.js
index 103c84fda..83fbc8b11 100644
--- a/packages/astro/test/units/render/head.test.js
+++ b/packages/astro/test/units/render/head.test.js
@@ -95,13 +95,21 @@ describe('core/render', () => {
)}`;
});
- const ctx = createRenderContext({
+ const PageModule = createAstroModule(Page);
+ const ctx = await createRenderContext({
request: new Request('http://example.com/'),
links: [{ name: 'link', props: { rel: 'stylesheet', href: '/main.css' }, children: '' }],
+ mod: PageModule,
+ env,
});
- const PageModule = createAstroModule(Page);
- const response = await renderPage(PageModule, ctx, env);
+ const response = await renderPage({
+ mod: PageModule,
+ renderContext: ctx,
+ env,
+ params: ctx.params,
+ props: ctx.props,
+ });
const html = await response.text();
const $ = cheerio.load(html);
@@ -173,14 +181,21 @@ describe('core/render', () => {
)}`;
});
- const ctx = createRenderContext({
+ const PageModule = createAstroModule(Page);
+ const ctx = await createRenderContext({
request: new Request('http://example.com/'),
links: [{ name: 'link', props: { rel: 'stylesheet', href: '/main.css' }, children: '' }],
+ env,
+ mod: PageModule,
});
- const PageModule = createAstroModule(Page);
-
- const response = await renderPage(PageModule, ctx, env);
+ const response = await renderPage({
+ mod: PageModule,
+ renderContext: ctx,
+ env,
+ params: ctx.params,
+ props: ctx.props,
+ });
const html = await response.text();
const $ = cheerio.load(html);
@@ -218,14 +233,21 @@ describe('core/render', () => {
)}`;
});
- const ctx = createRenderContext({
+ const PageModule = createAstroModule(Page);
+ const ctx = await createRenderContext({
request: new Request('http://example.com/'),
links: [{ name: 'link', props: { rel: 'stylesheet', href: '/main.css' }, children: '' }],
+ env,
+ mod: PageModule,
});
- const PageModule = createAstroModule(Page);
-
- const response = await renderPage(PageModule, ctx, env);
+ const response = await renderPage({
+ mod: PageModule,
+ renderContext: ctx,
+ env,
+ params: ctx.params,
+ props: ctx.props,
+ });
const html = await response.text();
const $ = cheerio.load(html);
diff --git a/packages/astro/test/units/render/jsx.test.js b/packages/astro/test/units/render/jsx.test.js
index a34b6b53b..c249bcbd5 100644
--- a/packages/astro/test/units/render/jsx.test.js
+++ b/packages/astro/test/units/render/jsx.test.js
@@ -1,5 +1,4 @@
import { expect } from 'chai';
-
import {
createComponent,
render,
@@ -46,8 +45,18 @@ describe('core/render', () => {
});
});
- const ctx = createRenderContext({ request: new Request('http://example.com/') });
- const response = await renderPage(createAstroModule(Page), ctx, env);
+ const mod = createAstroModule(Page);
+ const ctx = await createRenderContext({
+ request: new Request('http://example.com/'),
+ env,
+ mod,
+ });
+
+ const response = await renderPage({
+ mod,
+ renderContext: ctx,
+ env,
+ });
expect(response.status).to.equal(200);
@@ -85,8 +94,17 @@ describe('core/render', () => {
});
});
- const ctx = createRenderContext({ request: new Request('http://example.com/') });
- const response = await renderPage(createAstroModule(Page), ctx, env);
+ const mod = createAstroModule(Page);
+ const ctx = await createRenderContext({
+ request: new Request('http://example.com/'),
+ env,
+ mod,
+ });
+ const response = await renderPage({
+ mod,
+ renderContext: ctx,
+ env,
+ });
expect(response.status).to.equal(200);
@@ -105,8 +123,18 @@ describe('core/render', () => {
return render`<div>${renderComponent(result, 'Component', Component, {})}</div>`;
});
- const ctx = createRenderContext({ request: new Request('http://example.com/') });
- const response = await renderPage(createAstroModule(Page), ctx, env);
+ const mod = createAstroModule(Page);
+ const ctx = await createRenderContext({
+ request: new Request('http://example.com/'),
+ env,
+ mod,
+ });
+
+ const response = await renderPage({
+ mod,
+ renderContext: ctx,
+ env,
+ });
try {
await response.text();
diff --git a/packages/integrations/node/src/middleware.ts b/packages/integrations/node/src/nodeMiddleware.ts
index c23cdb89c..c23cdb89c 100644
--- a/packages/integrations/node/src/middleware.ts
+++ b/packages/integrations/node/src/nodeMiddleware.ts
diff --git a/packages/integrations/node/src/server.ts b/packages/integrations/node/src/server.ts
index ed03b68a6..98f5cd14b 100644
--- a/packages/integrations/node/src/server.ts
+++ b/packages/integrations/node/src/server.ts
@@ -1,7 +1,7 @@
import { polyfill } from '@astrojs/webapi';
import type { SSRManifest } from 'astro';
import { NodeApp } from 'astro/app/node';
-import middleware from './middleware.js';
+import middleware from './nodeMiddleware.js';
import startServer from './standalone.js';
import type { Options } from './types';
diff --git a/packages/integrations/node/src/standalone.ts b/packages/integrations/node/src/standalone.ts
index 813174252..85eb3822a 100644
--- a/packages/integrations/node/src/standalone.ts
+++ b/packages/integrations/node/src/standalone.ts
@@ -3,7 +3,7 @@ import https from 'https';
import path from 'path';
import { fileURLToPath } from 'url';
import { createServer } from './http-server.js';
-import middleware from './middleware.js';
+import middleware from './nodeMiddleware.js';
import type { Options } from './types';
function resolvePaths(options: Options) {