From e6f821b5b46bca82e070939b3b27cc36e6684d8c Mon Sep 17 00:00:00 2001 From: Emanuele Stoppa Date: Fri, 31 Mar 2023 10:56:35 -0300 Subject: [PATCH 01/53] feat(astro): experimental middleware --- packages/astro/src/@types/astro.ts | 59 ++++++++++- packages/astro/src/core/config/schema.ts | 2 + packages/astro/src/core/constants.ts | 3 + packages/astro/src/core/endpoint/index.ts | 4 +- packages/astro/src/core/errors/errors-data.ts | 31 ++++++ packages/astro/src/core/render/core.ts | 30 +++++- packages/astro/src/core/render/dev/index.ts | 99 ++++++++++++++++++- packages/astro/src/core/render/result.ts | 5 +- packages/astro/src/core/request.ts | 4 + packages/astro/src/core/sequence.ts | 24 +++++ packages/astro/src/core/util.ts | 54 ++++++++++ packages/astro/src/integrations/index.ts | 7 +- .../src/vite-plugin-astro-server/route.ts | 42 +++++++- packages/integrations/markdoc/src/index.ts | 9 +- packages/integrations/mdx/src/index.ts | 4 +- 15 files changed, 361 insertions(+), 16 deletions(-) create mode 100644 packages/astro/src/core/sequence.ts diff --git a/packages/astro/src/@types/astro.ts b/packages/astro/src/@types/astro.ts index 674445cd71cd..71b4df1d60ff 100644 --- a/packages/astro/src/@types/astro.ts +++ b/packages/astro/src/@types/astro.ts @@ -944,6 +944,29 @@ export interface AstroUserConfig { AstroIntegration | (AstroIntegration | false | undefined | null)[] | false | undefined | null >; + /** + * @docs + * @kind heading + * @name Integrations + * @description + * + * Extend Astro with custom integrations. Integrations are your one-stop-shop for adding framework support (like Solid.js), new features (like sitemaps), and new libraries (like Partytown and Turbolinks). + * + * Read our [Integrations Guide](https://docs.astro.build/en/guides/integrations-guide/) for help getting started with Astro Integrations. + * + * ```js + * import react from '@astrojs/react'; + * import tailwind from '@astrojs/tailwind'; + * { + * // Example: Add React + Tailwind support to Astro + * integrations: [react(), tailwind()] + * } + * ``` + */ + middlewares?: Array< + AstroIntegration | (AstroIntegration | false | undefined | null)[] | false | undefined | null + >; + /** * @docs * @kind heading @@ -1075,6 +1098,9 @@ export interface AstroConfig extends z.output { // This is a more detailed type than zod validation gives us. // TypeScript still confirms zod validation matches this type. integrations: AstroIntegration[]; + + // Order of the middlewares + middlewareOrder: string[]; } export type ContentEntryModule = { @@ -1373,6 +1399,7 @@ export interface Page { export type PaginateFunction = (data: any[], args?: PaginateOptions) => GetStaticPathsResult; export type Params = Record; +export type Locals = Record; export interface AstroAdapter { name: string; @@ -1414,6 +1441,16 @@ interface AstroSharedContext = Record void; } export interface APIContext = Record> @@ -1487,6 +1524,11 @@ export interface APIContext = Record; @@ -1527,7 +1569,7 @@ export interface SSRLoadedRenderer extends AstroRenderer { }; } -export type HookParameters< +export type HookIntegrationParameters< Hook extends keyof AstroIntegration['hooks'], Fn = AstroIntegration['hooks'][Hook] > = Fn extends (...args: any) => any ? Parameters[0] : never; @@ -1575,6 +1617,21 @@ export interface AstroIntegration { }; } +export type MiddlewareResolve = (context: APIContext) => Promise; + +export type OnBeforeRequestHook = (context: APIContext, resolve: Resolve) => Promise; + +export type Resolve = (context: APIContext) => Promise; + +export type OnAfterRequestHook = ( + context: Readonly, + response: Response +) => Promise; + +export type AstroMiddlewareInstance = { + onRequest?: OnBeforeRequestHook; +}; + export interface AstroPluginOptions { settings: AstroSettings; logging: LogOptions; diff --git a/packages/astro/src/core/config/schema.ts b/packages/astro/src/core/config/schema.ts index 4c55dc5b63a2..bf4e31e61c60 100644 --- a/packages/astro/src/core/config/schema.ts +++ b/packages/astro/src/core/config/schema.ts @@ -29,6 +29,7 @@ const ASTRO_CONFIG_DEFAULTS: AstroUserConfig & any = { open: false, }, integrations: [], + middlewares: [], markdown: { drafts: false, ...markdownConfigDefaults, @@ -80,6 +81,7 @@ export const AstroConfigSchema = z.object({ .array(z.object({ name: z.string(), hooks: z.object({}).passthrough().default({}) })) .default(ASTRO_CONFIG_DEFAULTS.integrations) ), + middlewareOrder: z.array(z.string()).default([]), build: z .object({ format: z diff --git a/packages/astro/src/core/constants.ts b/packages/astro/src/core/constants.ts index 16dde75509e4..471614ce3359 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/index.ts b/packages/astro/src/core/endpoint/index.ts index 876c21aa5dc5..22d901cbc667 100644 --- a/packages/astro/src/core/endpoint/index.ts +++ b/packages/astro/src/core/endpoint/index.ts @@ -9,6 +9,7 @@ import { warn, type LogOptions } from '../logger/core.js'; import { getParamsAndProps, GetParamsAndPropsError } from '../render/core.js'; const clientAddressSymbol = Symbol.for('astro.clientAddress'); +const clientLocalsSymbol = Symbol.for('astro.locals'); type EndpointCallResult = | { @@ -22,7 +23,7 @@ type EndpointCallResult = response: Response; }; -function createAPIContext({ +export function createAPIContext({ request, params, site, @@ -66,6 +67,7 @@ function createAPIContext({ return Reflect.get(request, clientAddressSymbol); }, + locals: Reflect.get(request, clientLocalsSymbol), }; } diff --git a/packages/astro/src/core/errors/errors-data.ts b/packages/astro/src/core/errors/errors-data.ts index bb9a8650666e..b46eef1b8c9b 100644 --- a/packages/astro/src/core/errors/errors-data.ts +++ b/packages/astro/src/core/errors/errors-data.ts @@ -628,6 +628,37 @@ 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.', }, + + /** + * TODO [PLT-101] documentation + */ + MiddlewareNotFound: { + title: 'Middleware not found.', + code: 3030, + message: (middlewareName: string, middlewarePath: string) => + `Can't find the middleware ${middlewareName} at path ${middlewarePath}. Make sure the file exists and has the correct name.`, + }, + + /** + * TODO [PLT-101] documentation + */ + LocalsNotAvailable: { + title: '`Astro.locals` is not available in current adapter.', + code: 3030, + message: (adapterName: string) => + `\`Astro.locals\` is not available in the "${adapterName}" adapter. File an issue with the adapter to add support.`, + }, + + /** + * TODO [PLT-101] documentation + */ + LocalsNotSerializable: { + title: '`Astro.locals` are not serializable.', + code: 3031, + message: (href: string) => { + return `Information stored in \`Astro.locals\` are not serializable when visiting "${href}" path. Make sure you store only data that are compatible.`; + }, + }, // 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/render/core.ts b/packages/astro/src/core/render/core.ts index 8687e90068bc..6478b0c5957d 100644 --- a/packages/astro/src/core/render/core.ts +++ b/packages/astro/src/core/render/core.ts @@ -1,4 +1,11 @@ -import type { ComponentInstance, Params, Props, RouteData } from '../../@types/astro'; +import type { + APIContext, + ComponentInstance, + OnBeforeRequestHook, + 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'; @@ -9,6 +16,7 @@ import { AstroError, AstroErrorData } from '../errors/index.js'; import { getParams } from '../routing/params.js'; import { createResult } from './result.js'; import { callGetStaticPaths, findPathItemByKey, RouteCache } from './route-cache.js'; +import { isValueSerializable } from '../util.js'; interface GetParamsAndPropsOptions { mod: ComponentInstance; @@ -84,7 +92,12 @@ export async function getParamsAndProps( return [params, pageProps]; } -export async function renderPage(mod: ComponentInstance, ctx: RenderContext, env: Environment) { +export async function renderPage( + mod: ComponentInstance, + ctx: RenderContext, + env: Environment, + apiContext?: APIContext +) { const paramsAndPropsRes = await getParamsAndProps({ logging: env.logging, mod, @@ -110,6 +123,16 @@ export async function renderPage(mod: ComponentInstance, ctx: RenderContext, env if (!Component) throw new Error(`Expected an exported Astro component but received typeof ${typeof Component}`); + let locals = {}; + if (apiContext) { + if (!isValueSerializable(apiContext.locals)) { + throw new AstroError({ + ...AstroErrorData.LocalsNotSerializable, + message: AstroErrorData.LocalsNotSerializable.message(ctx.pathname), + }); + } + locals = apiContext.locals; + } const result = createResult({ adapterName: env.adapterName, links: ctx.links, @@ -129,6 +152,7 @@ export async function renderPage(mod: ComponentInstance, ctx: RenderContext, env scripts: ctx.scripts, ssr: env.ssr, status: ctx.status ?? 200, + locals, }); // Support `export const components` for `MDX` pages @@ -136,7 +160,7 @@ export async function renderPage(mod: ComponentInstance, ctx: RenderContext, env Object.assign(pageProps, { components: (mod as any).components }); } - const response = await runtimeRenderPage( + let response = await runtimeRenderPage( result, Component, pageProps, diff --git a/packages/astro/src/core/render/dev/index.ts b/packages/astro/src/core/render/dev/index.ts index 51920e8003a7..57791f2ba008 100644 --- a/packages/astro/src/core/render/dev/index.ts +++ b/packages/astro/src/core/render/dev/index.ts @@ -1,22 +1,38 @@ import { fileURLToPath } from 'url'; import type { + AstroMiddlewareInstance, AstroSettings, ComponentInstance, + OnBeforeRequestHook, + Resolve, RouteData, SSRElement, SSRLoadedRenderer, } from '../../../@types/astro'; import { PAGE_SCRIPT_ID } from '../../../vite-plugin-scripts/index.js'; import { enhanceViteSSRError } from '../../errors/dev/index.js'; -import { AggregateError, CSSError, MarkdownError } from '../../errors/index.js'; +import { + AggregateError, + AstroError, + AstroErrorData, + CSSError, + MarkdownError, +} from '../../errors/index.js'; import type { ModuleLoader } from '../../module-loader/index'; import { isPage, resolveIdToUrl, viteID } from '../../util.js'; -import { createRenderContext, renderPage as coreRenderPage } from '../index.js'; +import { + createRenderContext, + getParamsAndProps, + GetParamsAndPropsError, + renderPage as coreRenderPage, +} from '../index.js'; import { filterFoundRenderers, loadRenderer } from '../renderer.js'; import { getStylesForURL } from './css.js'; import type { DevelopmentEnvironment } from './environment'; import { getComponentMetadata } from './metadata.js'; import { getScriptsForURL } from './scripts.js'; +import { createAPIContext } from '../../endpoint/index.js'; +import { sequence } from '../../sequence.js'; export { createDevelopmentEnvironment } from './environment.js'; export type { DevelopmentEnvironment }; @@ -35,6 +51,10 @@ export interface SSROptions { request: Request; /** optional, in case we need to render something outside of a dev server */ route?: RouteData; + /** + * Optional middlewares + */ + middlewares?: AstroMiddlewareInstance[]; } export type ComponentPreload = [SSRLoadedRenderer[], ComponentInstance]; @@ -170,5 +190,80 @@ export async function renderPage(options: SSROptions): Promise { route: options.route, }); + if (options.middlewares && options.middlewares.length > 0) { + const { env } = options; + 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; + + const apiContext = createAPIContext({ + request: options.request, + params, + props: pageProps, + adapterName: options.env.adapterName, + }); + + const beforeHooks: OnBeforeRequestHook[] = []; + for (const middleware of options.middlewares) { + const { onRequest } = middleware; + if (onRequest) { + beforeHooks.push(onRequest); + } + } + + let middleware = sequence(...beforeHooks); + + // const response = await coreRenderPage(mod, ctx, options.env, apiContext); + + let resolveResolve: any; + new Promise((resolve) => { + resolveResolve = resolve; + }); + + let resolveCalledResolve: any; + let resolveCalled = new Promise((resolve) => { + resolveCalledResolve = resolve; + }); + const resolve: Resolve = (context) => { + const response = coreRenderPage(mod, ctx, options.env, apiContext); + resolveCalledResolve('resolveCalled'); + return response; + }; + + let middlewarePromise = middleware(apiContext, resolve); + + let response = await Promise.race([middlewarePromise, resolveCalled]).then(async (value) => { + console.log('rance', value); + if (value === 'resolveCalled') { + // Middleware called resolve() + // render the page and then pass back to middleware + // for post-processing + const responseResult = await coreRenderPage(mod, ctx, options.env, apiContext); + await resolveResolve(responseResult); + return middlewarePromise; + } else { + // Middleware did not call resolve() + return await coreRenderPage(mod, ctx, options.env, apiContext); + } + }); + + return response; + } return await coreRenderPage(mod, ctx, options.env); // NOTE: without "await", errors won’t get caught below } diff --git a/packages/astro/src/core/render/result.ts b/packages/astro/src/core/render/result.ts index 26ea22eee343..4f6c53c16dbe 100644 --- a/packages/astro/src/core/render/result.ts +++ b/packages/astro/src/core/render/result.ts @@ -2,6 +2,7 @@ import type { MarkdownRenderingOptions } from '@astrojs/markdown-remark'; import type { AstroGlobal, AstroGlobalPartial, + Locals, Params, Props, RuntimeMode, @@ -50,6 +51,7 @@ export interface CreateResultArgs { componentMetadata?: SSRResult['componentMetadata']; request: Request; status: number; + locals: Locals; } function getFunctionExpression(slot: any) { @@ -131,7 +133,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 +202,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 24356983f768..02e3e86e56fd 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,8 @@ export function createRequest({ Reflect.set(request, clientAddressSymbol, clientAddress); } + // TODO: PLT-104 check if this needs to be set in another adapters? + Reflect.set(request, clientLocalsSymbol, {}); + return request; } diff --git a/packages/astro/src/core/sequence.ts b/packages/astro/src/core/sequence.ts new file mode 100644 index 000000000000..a46581a7140e --- /dev/null +++ b/packages/astro/src/core/sequence.ts @@ -0,0 +1,24 @@ +import type { APIContext, OnBeforeRequestHook } from '../@types/astro'; + +export function sequence(...handlers: OnBeforeRequestHook[]): OnBeforeRequestHook { + const length = handlers.length; + if (!length) return (context, resolve) => resolve(context); + + return (context, resolve) => { + return apply_handle(0, context); + + /** + * @param {number} i + * @param {import('types').RequestEvent} event + * @param {import('types').ResolveOptions | undefined} parent_options + * @returns {import('types').MaybePromise} + */ + function apply_handle(i: number, handleContext: APIContext): Promise { + const handle = handlers[i]; + + return handle(handleContext, (nextContext) => { + return i < length - 1 ? apply_handle(i + 1, nextContext) : resolve(nextContext); + }); + } + }; +} diff --git a/packages/astro/src/core/util.ts b/packages/astro/src/core/util.ts index 5d8868115056..79354928dec5 100644 --- a/packages/astro/src/core/util.ts +++ b/packages/astro/src/core/util.ts @@ -217,3 +217,57 @@ export function resolvePath(specifier: string, importer: string) { return specifier; } } + +/** + * 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/integrations/index.ts b/packages/astro/src/integrations/index.ts index 9b86259ae1a0..c931075b50c3 100644 --- a/packages/astro/src/integrations/index.ts +++ b/packages/astro/src/integrations/index.ts @@ -9,7 +9,7 @@ import type { AstroSettings, BuildConfig, ContentEntryType, - HookParameters, + HookIntegrationParameters, RouteData, } from '../@types/astro.js'; import type { SerializedSSRManifest } from '../core/app/types'; @@ -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,8 +69,8 @@ export async function runHookConfigSetup({ * ] * ``` */ - if (integration?.hooks?.['astro:config:setup']) { - const hooks: HookParameters<'astro:config:setup'> = { + if (integration.hooks?.['astro:config:setup']) { + const hooks: HookIntegrationParameters<'astro:config:setup'> = { config: updatedConfig, command, isRestart, diff --git a/packages/astro/src/vite-plugin-astro-server/route.ts b/packages/astro/src/vite-plugin-astro-server/route.ts index da280f7e1b4c..2d060f192b48 100644 --- a/packages/astro/src/vite-plugin-astro-server/route.ts +++ b/packages/astro/src/vite-plugin-astro-server/route.ts @@ -10,7 +10,7 @@ import type { 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 { AstroError, AstroErrorData } from '../core/errors/index.js'; import { warn } from '../core/logger/core.js'; import { preload, renderPage } from '../core/render/dev/index.js'; import { getParamsAndProps, GetParamsAndPropsError } from '../core/render/index.js'; @@ -18,6 +18,10 @@ import { createRequest } from '../core/request.js'; import { matchAllRoutes } from '../core/routing/index.js'; import { log404 } from './common.js'; import { handle404Response, writeSSRResult, writeWebResponse } from './response.js'; +import type { ModuleLoader } from '../core/module-loader'; +import type { AstroMiddlewareInstance } from '../@types/astro'; +import { MIDDLEWARE_PATH_SEGMENT_NAME } from '../core/constants.js'; +import { fileURLToPath } from 'node:url'; type AsyncReturnType Promise> = T extends ( ...args: any @@ -112,6 +116,38 @@ export async function matchRoute( return undefined; } +/** + * 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 loadMiddlewares( + moduleLoader: ModuleLoader, + basePath: URL, + middlewareOrder: string[] +): Promise { + if (middlewareOrder.length === 0) { + return []; + } + const middlewares = await Promise.all( + middlewareOrder.map(async (middlewareName) => { + let path = fileURLToPath(basePath); + let middlewarePath = path + 'src' + '/' + MIDDLEWARE_PATH_SEGMENT_NAME + '/' + middlewareName; + try { + const module = (await moduleLoader.import(middlewarePath)) as AstroMiddlewareInstance; + return module; + } catch { + throw new AstroError({ + ...AstroErrorData.MiddlewareNotFound, + message: AstroErrorData.MiddlewareNotFound.message(middlewareName, middlewarePath), + }); + } + }) + ); + + return middlewares.filter(Boolean) as AstroMiddlewareInstance[]; +} + export async function handleRoute( matchedRoute: AsyncReturnType, url: URL, @@ -169,7 +205,11 @@ export async function handleRoute( request, route, }; + const middlewares = await loadMiddlewares(env.loader, config.root, config.middlewareOrder); + if (middlewares.length > 0) { + options.middlewares = middlewares; + } // Route successfully matched! Render it. if (route.type === 'endpoint') { const result = await callEndpoint(options, logging); diff --git a/packages/integrations/markdoc/src/index.ts b/packages/integrations/markdoc/src/index.ts index 55d13169b7d0..11024a4528e1 100644 --- a/packages/integrations/markdoc/src/index.ts +++ b/packages/integrations/markdoc/src/index.ts @@ -1,6 +1,11 @@ import type { Node } from '@markdoc/markdoc'; import Markdoc from '@markdoc/markdoc'; -import type { AstroConfig, AstroIntegration, ContentEntryType, HookParameters } from 'astro'; +import type { + AstroConfig, + AstroIntegration, + ContentEntryType, + HookIntegrationParameters, +} from 'astro'; import fs from 'node:fs'; import { fileURLToPath } from 'node:url'; import { isValidUrl, MarkdocError, parseFrontmatter, prependForwardSlash } from './utils.js'; @@ -11,7 +16,7 @@ import type * as rollup from 'rollup'; import { applyDefaultConfig } from './default-config.js'; import { loadMarkdocConfig } from './load-config.js'; -type SetupHookParams = HookParameters<'astro:config:setup'> & { +type SetupHookParams = HookIntegrationParameters<'astro:config:setup'> & { // `contentEntryType` is not a public API // Add type defs here addContentEntryType: (contentEntryType: ContentEntryType) => void; diff --git a/packages/integrations/mdx/src/index.ts b/packages/integrations/mdx/src/index.ts index 2ccf6626654b..42ff50a21f19 100644 --- a/packages/integrations/mdx/src/index.ts +++ b/packages/integrations/mdx/src/index.ts @@ -3,7 +3,7 @@ import { toRemarkInitializeAstroData } from '@astrojs/markdown-remark/dist/inter import { compile as mdxCompile } from '@mdx-js/mdx'; import type { PluggableList } from '@mdx-js/mdx/lib/core.js'; import mdxPlugin, { type Options as MdxRollupPluginOptions } from '@mdx-js/rollup'; -import type { AstroIntegration, ContentEntryType, HookParameters } from 'astro'; +import type { AstroIntegration, ContentEntryType, HookIntegrationParameters } from 'astro'; import { parse as parseESM } from 'es-module-lexer'; import fs from 'node:fs/promises'; import { fileURLToPath } from 'node:url'; @@ -24,7 +24,7 @@ export type MdxOptions = Omit & { +type SetupHookParams = HookIntegrationParameters<'astro:config:setup'> & { // `addPageExtension` and `contentEntryType` are not a public APIs // Add type defs here addPageExtension: (extension: string) => void; From db4e21a64a8bd08d62e195e393b613a135e0ba9d Mon Sep 17 00:00:00 2001 From: Emanuele Stoppa Date: Fri, 31 Mar 2023 10:57:27 -0300 Subject: [PATCH 02/53] changeset --- .changeset/pretty-bears-deliver.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/pretty-bears-deliver.md diff --git a/.changeset/pretty-bears-deliver.md b/.changeset/pretty-bears-deliver.md new file mode 100644 index 000000000000..2b8bfc818a3f --- /dev/null +++ b/.changeset/pretty-bears-deliver.md @@ -0,0 +1,5 @@ +--- +'astro': minor +--- + +New middleware API From 9d54c5db38ce16dc028f4b548fd1957307aef863 Mon Sep 17 00:00:00 2001 From: Emanuele Stoppa Date: Mon, 3 Apr 2023 13:05:44 +0100 Subject: [PATCH 03/53] chore: export sequence, load middleware as file --- packages/astro/middleware.d.ts | 3 ++ packages/astro/middleware.mjs | 3 ++ packages/astro/package.json | 6 +++ packages/astro/src/@types/astro.ts | 37 ++--------------- packages/astro/src/core/config/schema.ts | 1 - packages/astro/src/core/errors/errors-data.ts | 7 ++-- packages/astro/src/core/middleware/index.ts | 1 + .../src/core/{ => middleware}/sequence.ts | 4 +- packages/astro/src/core/render/core.ts | 9 +---- packages/astro/src/core/render/dev/index.ts | 34 +++++++--------- .../src/vite-plugin-astro-server/route.ts | 40 ++++++------------- 11 files changed, 50 insertions(+), 95 deletions(-) create mode 100644 packages/astro/middleware.d.ts create mode 100644 packages/astro/middleware.mjs create mode 100644 packages/astro/src/core/middleware/index.ts rename packages/astro/src/core/{ => middleware}/sequence.ts (80%) diff --git a/packages/astro/middleware.d.ts b/packages/astro/middleware.d.ts new file mode 100644 index 000000000000..2dd1c56ff539 --- /dev/null +++ b/packages/astro/middleware.d.ts @@ -0,0 +1,3 @@ +type MiddlewareHandler = import('./dist/@types/astro.js').MiddlewareHandler; + +type Sequence = (...handlers: MiddlewareHandler[]) => MiddlewareHandler; diff --git a/packages/astro/middleware.mjs b/packages/astro/middleware.mjs new file mode 100644 index 000000000000..c4050083bd17 --- /dev/null +++ b/packages/astro/middleware.mjs @@ -0,0 +1,3 @@ +export { + sequence +} from "./dist/core/middleware/index.js" diff --git a/packages/astro/package.json b/packages/astro/package.json index a2f0a6ed074e..3160a44f41d6 100644 --- a/packages/astro/package.json +++ b/packages/astro/package.json @@ -64,6 +64,10 @@ "./zod": { "types": "./zod.d.ts", "default": "./zod.mjs" + }, + "./middleware": { + "default": "./middleware.mjs", + "types": "" } }, "imports": { @@ -81,6 +85,8 @@ "config.mjs", "zod.d.ts", "zod.mjs", + "sequence.d.ts", + "sequence.mjs", "env.d.ts", "client.d.ts", "client-base.d.ts", diff --git a/packages/astro/src/@types/astro.ts b/packages/astro/src/@types/astro.ts index 71b4df1d60ff..33422c11a300 100644 --- a/packages/astro/src/@types/astro.ts +++ b/packages/astro/src/@types/astro.ts @@ -944,29 +944,6 @@ export interface AstroUserConfig { AstroIntegration | (AstroIntegration | false | undefined | null)[] | false | undefined | null >; - /** - * @docs - * @kind heading - * @name Integrations - * @description - * - * Extend Astro with custom integrations. Integrations are your one-stop-shop for adding framework support (like Solid.js), new features (like sitemaps), and new libraries (like Partytown and Turbolinks). - * - * Read our [Integrations Guide](https://docs.astro.build/en/guides/integrations-guide/) for help getting started with Astro Integrations. - * - * ```js - * import react from '@astrojs/react'; - * import tailwind from '@astrojs/tailwind'; - * { - * // Example: Add React + Tailwind support to Astro - * integrations: [react(), tailwind()] - * } - * ``` - */ - middlewares?: Array< - AstroIntegration | (AstroIntegration | false | undefined | null)[] | false | undefined | null - >; - /** * @docs * @kind heading @@ -1098,9 +1075,6 @@ export interface AstroConfig extends z.output { // This is a more detailed type than zod validation gives us. // TypeScript still confirms zod validation matches this type. integrations: AstroIntegration[]; - - // Order of the middlewares - middlewareOrder: string[]; } export type ContentEntryModule = { @@ -1618,18 +1592,13 @@ export interface AstroIntegration { } export type MiddlewareResolve = (context: APIContext) => Promise; - -export type OnBeforeRequestHook = (context: APIContext, resolve: Resolve) => Promise; - -export type Resolve = (context: APIContext) => Promise; - -export type OnAfterRequestHook = ( +export type MiddlewareHandler = ( context: Readonly, - response: Response + response: MiddlewareResolve ) => Promise; export type AstroMiddlewareInstance = { - onRequest?: OnBeforeRequestHook; + onRequest?: MiddlewareHandler; }; export interface AstroPluginOptions { diff --git a/packages/astro/src/core/config/schema.ts b/packages/astro/src/core/config/schema.ts index bf4e31e61c60..49e9f64105e0 100644 --- a/packages/astro/src/core/config/schema.ts +++ b/packages/astro/src/core/config/schema.ts @@ -81,7 +81,6 @@ export const AstroConfigSchema = z.object({ .array(z.object({ name: z.string(), hooks: z.object({}).passthrough().default({}) })) .default(ASTRO_CONFIG_DEFAULTS.integrations) ), - middlewareOrder: z.array(z.string()).default([]), build: z .object({ format: z diff --git a/packages/astro/src/core/errors/errors-data.ts b/packages/astro/src/core/errors/errors-data.ts index b46eef1b8c9b..4f67e2a6f832 100644 --- a/packages/astro/src/core/errors/errors-data.ts +++ b/packages/astro/src/core/errors/errors-data.ts @@ -632,11 +632,10 @@ See https://docs.astro.build/en/guides/server-side-rendering/ for more informati /** * TODO [PLT-101] documentation */ - MiddlewareNotFound: { - title: 'Middleware not found.', + MiddlewareOnRequestNotFound: { + title: "The middleware doesn't export the function 'onRequest'.", code: 3030, - message: (middlewareName: string, middlewarePath: string) => - `Can't find the middleware ${middlewareName} at path ${middlewarePath}. Make sure the file exists and has the correct name.`, + message: "A middleware must export a function called 'onRequest'", }, /** diff --git a/packages/astro/src/core/middleware/index.ts b/packages/astro/src/core/middleware/index.ts new file mode 100644 index 000000000000..e9e03a18d279 --- /dev/null +++ b/packages/astro/src/core/middleware/index.ts @@ -0,0 +1 @@ +export { sequence } from './sequence'; diff --git a/packages/astro/src/core/sequence.ts b/packages/astro/src/core/middleware/sequence.ts similarity index 80% rename from packages/astro/src/core/sequence.ts rename to packages/astro/src/core/middleware/sequence.ts index a46581a7140e..ca6752e22a31 100644 --- a/packages/astro/src/core/sequence.ts +++ b/packages/astro/src/core/middleware/sequence.ts @@ -1,6 +1,6 @@ -import type { APIContext, OnBeforeRequestHook } from '../@types/astro'; +import type { APIContext, MiddlewareHandler } from '../../@types/astro'; -export function sequence(...handlers: OnBeforeRequestHook[]): OnBeforeRequestHook { +export function sequence(...handlers: MiddlewareHandler[]): MiddlewareHandler { const length = handlers.length; if (!length) return (context, resolve) => resolve(context); diff --git a/packages/astro/src/core/render/core.ts b/packages/astro/src/core/render/core.ts index 6478b0c5957d..d224fb0271c1 100644 --- a/packages/astro/src/core/render/core.ts +++ b/packages/astro/src/core/render/core.ts @@ -1,11 +1,4 @@ -import type { - APIContext, - ComponentInstance, - OnBeforeRequestHook, - Params, - Props, - RouteData, -} from '../../@types/astro'; +import type { APIContext, 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'; diff --git a/packages/astro/src/core/render/dev/index.ts b/packages/astro/src/core/render/dev/index.ts index 57791f2ba008..81efdb3d5b6d 100644 --- a/packages/astro/src/core/render/dev/index.ts +++ b/packages/astro/src/core/render/dev/index.ts @@ -3,8 +3,8 @@ import type { AstroMiddlewareInstance, AstroSettings, ComponentInstance, - OnBeforeRequestHook, - Resolve, + MiddlewareHandler, + MiddlewareResolve, RouteData, SSRElement, SSRLoadedRenderer, @@ -32,7 +32,7 @@ import type { DevelopmentEnvironment } from './environment'; import { getComponentMetadata } from './metadata.js'; import { getScriptsForURL } from './scripts.js'; import { createAPIContext } from '../../endpoint/index.js'; -import { sequence } from '../../sequence.js'; +import { sequence } from '../../middleware/sequence.js'; export { createDevelopmentEnvironment } from './environment.js'; export type { DevelopmentEnvironment }; @@ -54,7 +54,7 @@ export interface SSROptions { /** * Optional middlewares */ - middlewares?: AstroMiddlewareInstance[]; + middleware?: AstroMiddlewareInstance; } export type ComponentPreload = [SSRLoadedRenderer[], ComponentInstance]; @@ -190,7 +190,7 @@ export async function renderPage(options: SSROptions): Promise { route: options.route, }); - if (options.middlewares && options.middlewares.length > 0) { + if (options.middleware) { const { env } = options; const paramsAndPropsRes = await getParamsAndProps({ logging: env.logging, @@ -219,18 +219,15 @@ export async function renderPage(options: SSROptions): Promise { adapterName: options.env.adapterName, }); - const beforeHooks: OnBeforeRequestHook[] = []; - for (const middleware of options.middlewares) { - const { onRequest } = middleware; - if (onRequest) { - beforeHooks.push(onRequest); - } + let onRequestHandler: MiddlewareHandler | undefined = undefined; + const { onRequest } = options.middleware; + if (onRequest) { + onRequestHandler = onRequest; + } else { + throw new AstroError({ + ...AstroErrorData.MiddlewareOnRequestNotFound, + }); } - - let middleware = sequence(...beforeHooks); - - // const response = await coreRenderPage(mod, ctx, options.env, apiContext); - let resolveResolve: any; new Promise((resolve) => { resolveResolve = resolve; @@ -240,16 +237,15 @@ export async function renderPage(options: SSROptions): Promise { let resolveCalled = new Promise((resolve) => { resolveCalledResolve = resolve; }); - const resolve: Resolve = (context) => { + const resolve: MiddlewareResolve = (context) => { const response = coreRenderPage(mod, ctx, options.env, apiContext); resolveCalledResolve('resolveCalled'); return response; }; - let middlewarePromise = middleware(apiContext, resolve); + let middlewarePromise = onRequestHandler(apiContext, resolve); let response = await Promise.race([middlewarePromise, resolveCalled]).then(async (value) => { - console.log('rance', value); if (value === 'resolveCalled') { // Middleware called resolve() // render the page and then pass back to middleware diff --git a/packages/astro/src/vite-plugin-astro-server/route.ts b/packages/astro/src/vite-plugin-astro-server/route.ts index 2d060f192b48..1234f9067124 100644 --- a/packages/astro/src/vite-plugin-astro-server/route.ts +++ b/packages/astro/src/vite-plugin-astro-server/route.ts @@ -121,31 +121,18 @@ export async function matchRoute( * * If not middlewares were not set, the function returns an empty array. */ -export async function loadMiddlewares( +export async function loadMiddleware( moduleLoader: ModuleLoader, - basePath: URL, - middlewareOrder: string[] -): Promise { - if (middlewareOrder.length === 0) { - return []; + basePath: URL +): Promise { + let path = fileURLToPath(basePath); + let middlewarePath = path + 'src' + '/' + MIDDLEWARE_PATH_SEGMENT_NAME; + try { + const module = (await moduleLoader.import(middlewarePath)) as AstroMiddlewareInstance; + return module as AstroMiddlewareInstance; + } catch { + return undefined; } - const middlewares = await Promise.all( - middlewareOrder.map(async (middlewareName) => { - let path = fileURLToPath(basePath); - let middlewarePath = path + 'src' + '/' + MIDDLEWARE_PATH_SEGMENT_NAME + '/' + middlewareName; - try { - const module = (await moduleLoader.import(middlewarePath)) as AstroMiddlewareInstance; - return module; - } catch { - throw new AstroError({ - ...AstroErrorData.MiddlewareNotFound, - message: AstroErrorData.MiddlewareNotFound.message(middlewareName, middlewarePath), - }); - } - }) - ); - - return middlewares.filter(Boolean) as AstroMiddlewareInstance[]; } export async function handleRoute( @@ -205,10 +192,9 @@ export async function handleRoute( request, route, }; - const middlewares = await loadMiddlewares(env.loader, config.root, config.middlewareOrder); - - if (middlewares.length > 0) { - options.middlewares = middlewares; + const middleware = await loadMiddleware(env.loader, config.root); + if (middleware) { + options.middleware = middleware; } // Route successfully matched! Render it. if (route.type === 'endpoint') { From c4591a38c8688b838ae29eb18afd48681f180c6d Mon Sep 17 00:00:00 2001 From: Emanuele Stoppa Date: Tue, 4 Apr 2023 09:58:45 +0100 Subject: [PATCH 04/53] chore: format --- packages/astro/middleware.mjs | 4 +--- packages/astro/src/core/middleware/sequence.ts | 17 ++++++++--------- 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/packages/astro/middleware.mjs b/packages/astro/middleware.mjs index c4050083bd17..0470ba892ff2 100644 --- a/packages/astro/middleware.mjs +++ b/packages/astro/middleware.mjs @@ -1,3 +1 @@ -export { - sequence -} from "./dist/core/middleware/index.js" +export { sequence } from './dist/core/middleware/index.js'; diff --git a/packages/astro/src/core/middleware/sequence.ts b/packages/astro/src/core/middleware/sequence.ts index ca6752e22a31..c4b09e91c8d3 100644 --- a/packages/astro/src/core/middleware/sequence.ts +++ b/packages/astro/src/core/middleware/sequence.ts @@ -1,23 +1,22 @@ import type { APIContext, MiddlewareHandler } from '../../@types/astro'; +/** + * 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: MiddlewareHandler[]): MiddlewareHandler { const length = handlers.length; if (!length) return (context, resolve) => resolve(context); return (context, resolve) => { - return apply_handle(0, context); + return applyHandle(0, context); - /** - * @param {number} i - * @param {import('types').RequestEvent} event - * @param {import('types').ResolveOptions | undefined} parent_options - * @returns {import('types').MaybePromise} - */ - function apply_handle(i: number, handleContext: APIContext): Promise { + function applyHandle(i: number, handleContext: APIContext): Promise { const handle = handlers[i]; return handle(handleContext, (nextContext) => { - return i < length - 1 ? apply_handle(i + 1, nextContext) : resolve(nextContext); + return i < length - 1 ? applyHandle(i + 1, nextContext) : resolve(nextContext); }); } }; From 2467e0439584ae49c9f4bd694b49929a87b00509 Mon Sep 17 00:00:00 2001 From: Emanuele Stoppa Date: Tue, 4 Apr 2023 14:09:18 +0100 Subject: [PATCH 05/53] chore: don't throw an error for missing `onRequest` --- packages/astro/src/core/errors/errors-data.ts | 9 --- packages/astro/src/core/render/dev/index.ts | 79 +++++++++---------- 2 files changed, 37 insertions(+), 51 deletions(-) diff --git a/packages/astro/src/core/errors/errors-data.ts b/packages/astro/src/core/errors/errors-data.ts index 4f67e2a6f832..b69790419ef3 100644 --- a/packages/astro/src/core/errors/errors-data.ts +++ b/packages/astro/src/core/errors/errors-data.ts @@ -629,15 +629,6 @@ See https://docs.astro.build/en/guides/server-side-rendering/ for more informati message: 'The response has already been sent to the browser and cannot be altered.', }, - /** - * TODO [PLT-101] documentation - */ - MiddlewareOnRequestNotFound: { - title: "The middleware doesn't export the function 'onRequest'.", - code: 3030, - message: "A middleware must export a function called 'onRequest'", - }, - /** * TODO [PLT-101] documentation */ diff --git a/packages/astro/src/core/render/dev/index.ts b/packages/astro/src/core/render/dev/index.ts index 81efdb3d5b6d..041983989f52 100644 --- a/packages/astro/src/core/render/dev/index.ts +++ b/packages/astro/src/core/render/dev/index.ts @@ -210,56 +210,51 @@ export async function renderPage(options: SSROptions): Promise { : '', }); } - const [params, pageProps] = paramsAndPropsRes; - const apiContext = createAPIContext({ - request: options.request, - params, - props: pageProps, - adapterName: options.env.adapterName, - }); - - let onRequestHandler: MiddlewareHandler | undefined = undefined; const { onRequest } = options.middleware; if (onRequest) { - onRequestHandler = onRequest; - } else { - throw new AstroError({ - ...AstroErrorData.MiddlewareOnRequestNotFound, + const [params, pageProps] = paramsAndPropsRes; + + const apiContext = createAPIContext({ + request: options.request, + params, + props: pageProps, + adapterName: options.env.adapterName, }); - } - let resolveResolve: any; - new Promise((resolve) => { - resolveResolve = resolve; - }); - let resolveCalledResolve: any; - let resolveCalled = new Promise((resolve) => { - resolveCalledResolve = resolve; - }); - const resolve: MiddlewareResolve = (context) => { - const response = coreRenderPage(mod, ctx, options.env, apiContext); - resolveCalledResolve('resolveCalled'); - return response; - }; + let resolveResolve: any; + new Promise((resolve) => { + resolveResolve = resolve; + }); - let middlewarePromise = onRequestHandler(apiContext, resolve); + let resolveCalledResolve: any; + let resolveCalled = new Promise((resolve) => { + resolveCalledResolve = resolve; + }); + const resolve: MiddlewareResolve = (context) => { + const response = coreRenderPage(mod, ctx, options.env, apiContext); + resolveCalledResolve('resolveCalled'); + return response; + }; - let response = await Promise.race([middlewarePromise, resolveCalled]).then(async (value) => { - if (value === 'resolveCalled') { - // Middleware called resolve() - // render the page and then pass back to middleware - // for post-processing - const responseResult = await coreRenderPage(mod, ctx, options.env, apiContext); - await resolveResolve(responseResult); - return middlewarePromise; - } else { - // Middleware did not call resolve() - return await coreRenderPage(mod, ctx, options.env, apiContext); - } - }); + let middlewarePromise = onRequest(apiContext, resolve); - return response; + let response = await Promise.race([middlewarePromise, resolveCalled]).then(async (value) => { + if (value === 'resolveCalled') { + // Middleware called resolve() + // render the page and then pass back to middleware + // for post-processing + const responseResult = await coreRenderPage(mod, ctx, options.env, apiContext); + await resolveResolve(responseResult); + return middlewarePromise; + } else { + // Middleware did not call resolve() + return await coreRenderPage(mod, ctx, options.env, apiContext); + } + }); + + return response; + } } return await coreRenderPage(mod, ctx, options.env); // NOTE: without "await", errors won’t get caught below } From 1d9e277cca0c09ad1ba7c9383d6419020c1bb622 Mon Sep 17 00:00:00 2001 From: Emanuele Stoppa Date: Wed, 5 Apr 2023 16:04:36 +0100 Subject: [PATCH 06/53] feat: support middlewares in SSG for endpoints and pages --- packages/astro/src/@types/astro.ts | 19 +++-- packages/astro/src/core/app/index.ts | 3 +- packages/astro/src/core/build/generate.ts | 70 ++++++++++++++++-- .../src/core/build/plugins/plugin-pages.ts | 12 +++- packages/astro/src/core/build/static-build.ts | 3 +- packages/astro/src/core/build/types.ts | 2 + packages/astro/src/core/config/schema.ts | 1 - packages/astro/src/core/endpoint/dev/index.ts | 15 +++- packages/astro/src/core/endpoint/index.ts | 24 ++++++- .../src/core/middleware/callMiddleware.ts | 72 +++++++++++++++++++ packages/astro/src/core/middleware/index.ts | 6 +- .../src/core/middleware/loadMiddleware.ts | 17 +++++ .../astro/src/core/middleware/sequence.ts | 11 ++- packages/astro/src/core/render/dev/index.ts | 40 ++--------- .../src/vite-plugin-astro-server/route.ts | 29 +------- 15 files changed, 231 insertions(+), 93 deletions(-) create mode 100644 packages/astro/src/core/middleware/callMiddleware.ts create mode 100644 packages/astro/src/core/middleware/loadMiddleware.ts diff --git a/packages/astro/src/@types/astro.ts b/packages/astro/src/@types/astro.ts index 33422c11a300..573ca5803915 100644 --- a/packages/astro/src/@types/astro.ts +++ b/packages/astro/src/@types/astro.ts @@ -1420,11 +1420,6 @@ interface AstroSharedContext = Record void; } export interface APIContext = Record> @@ -1591,14 +1586,16 @@ export interface AstroIntegration { }; } -export type MiddlewareResolve = (context: APIContext) => Promise; -export type MiddlewareHandler = ( +export type MiddlewareResolve = (context: APIContext) => Promise; +export type MiddlewareHandler = ( context: Readonly, - response: MiddlewareResolve -) => Promise; + response: MiddlewareResolve +) => Promise; -export type AstroMiddlewareInstance = { - onRequest?: MiddlewareHandler; +// 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 = { + onRequest?: MiddlewareHandler; }; export interface AstroPluginOptions { diff --git a/packages/astro/src/core/app/index.ts b/packages/astro/src/core/app/index.ts index a1d19ee92938..494a54d88a8e 100644 --- a/packages/astro/src/core/app/index.ts +++ b/packages/astro/src/core/app/index.ts @@ -232,7 +232,8 @@ export class App { status, }); - const result = await callEndpoint(handler, this.#env, ctx, this.#logging); + // TODO PLT-104 add adapter middleware here + const result = await callEndpoint(handler, this.#env, ctx, this.#logging, undefined); if (result.type === 'response') { if (result.response.headers.get('X-Astro-Response') === 'Not-Found') { diff --git a/packages/astro/src/core/build/generate.ts b/packages/astro/src/core/build/generate.ts index 6e50d687f901..2b0da68687c6 100644 --- a/packages/astro/src/core/build/generate.ts +++ b/packages/astro/src/core/build/generate.ts @@ -5,13 +5,16 @@ import type { OutputAsset, OutputChunk } from 'rollup'; import { fileURLToPath } from 'url'; import type { AstroConfig, + AstroMiddlewareInstance, AstroSettings, ComponentInstance, EndpointHandler, ImageTransform, + MiddlewareHandler, RouteType, SSRError, SSRLoadedRenderer, + EndpointOutput, } from '../../@types/astro'; import { generateImage as generateImageInternal, @@ -25,10 +28,20 @@ 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 { AstroError } from '../errors/index.js'; +import { + call as callEndpoint, + createAPIContext, + throwIfRedirectNotAllowed, +} from '../endpoint/index.js'; +import { AstroError, AstroErrorData } from '../errors/index.js'; import { debug, info } from '../logger/core.js'; -import { createEnvironment, createRenderContext, renderPage } from '../render/index.js'; +import { + createEnvironment, + createRenderContext, + getParamsAndProps, + GetParamsAndPropsError, + renderPage, +} from '../render/index.js'; import { callGetStaticPaths } from '../render/route-cache.js'; import { createAssetLink, @@ -42,6 +55,7 @@ import { getOutDirWithinCwd, getOutFile, getOutFolder } from './common.js'; import { eachPageData, getPageDataByComponent, sortedCSS } from './internal.js'; import type { PageBuildData, SingleFileBuiltModule, StaticBuildOptions } from './types'; import { getTimeStat } from './util.js'; +import { callMiddleware } from '../middleware/index.js'; function shouldSkipDraft(pageModule: ComponentInstance, settings: AstroSettings): boolean { return ( @@ -157,6 +171,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 +201,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 +343,8 @@ function getUrlForPath( async function generatePath( pathname: string, opts: StaticBuildOptions, - gopts: GeneratePathOptions + gopts: GeneratePathOptions, + middleware: AstroMiddlewareInstance ) { const { settings, logging, origin, routeCache } = opts; const { mod, internals, linkIds, scripts: hoistedScripts, pageData, renderers } = gopts; @@ -414,6 +430,7 @@ async function generatePath( ssr, streaming: true, }); + const ctx = createRenderContext({ origin, pathname, @@ -428,7 +445,14 @@ async function generatePath( 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, + ctx, + logging, + middleware as AstroMiddlewareInstance + ); if (result.type === 'response') { throwIfRedirectNotAllowed(result.response, opts.settings.config); @@ -443,7 +467,39 @@ async function generatePath( } else { let response: Response; try { - response = await renderPage(mod, ctx, env); + 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, + site: env.site, + adapterName: env.adapterName, + }); + // If the user doesn't configure a middleware, the rollup plugin emits a no-op function, + // so it's safe to use `callMiddleware` regardless + let onRequest = middleware.onRequest as MiddlewareHandler; + response = await callMiddleware(onRequest, context, () => { + return renderPage(mod, ctx, env, context); + }); } 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 5bb070978729..ece0bbc6e734 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 { addRollupInput } from '../add-rollup-input.js'; import { eachPageData, hasPrerenderedPages, type BuildInternals } from '../internal.js'; +import { MIDDLEWARE_PATH_SEGMENT_NAME } from '../../constants.js'; export function vitePluginPages(opts: StaticBuildOptions, internals: BuildInternals): VitePlugin { return { @@ -22,8 +22,10 @@ export function vitePluginPages(opts: StaticBuildOptions, internals: BuildIntern } }, - load(id) { + async load(id) { if (id === resolvedPagesVirtualModuleId) { + const middlewareId = await this.resolve(`./src/${MIDDLEWARE_PATH_SEGMENT_NAME}`); + let importMap = ''; let imports = []; let i = 0; @@ -47,8 +49,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}]; +export const middleware = ${middlewareId ? '_middleware' : '{ onRequest() {} }'}; +`; return def; } diff --git a/packages/astro/src/core/build/static-build.ts b/packages/astro/src/core/build/static-build.ts index ff71e80b8ee7..8b3b088b3d94 100644 --- a/packages/astro/src/core/build/static-build.ts +++ b/packages/astro/src/core/build/static-build.ts @@ -13,7 +13,7 @@ import { } from '../../core/build/internal.js'; import { emptyDir, removeEmptyDirs } from '../../core/fs/index.js'; import { appendForwardSlash, prependForwardSlash } from '../../core/path.js'; -import { isModeServerWithNoAdapter } from '../../core/util.js'; +import { isModeServerWithNoAdapter, viteID } from '../../core/util.js'; import { runHookBuildSetup } from '../../integrations/index.js'; import { PAGE_SCRIPT_ID } from '../../vite-plugin-scripts/index.js'; import { resolvedPagesVirtualModuleId } from '../app/index.js'; @@ -26,6 +26,7 @@ import { createPluginContainer, type AstroBuildPluginContainer } from './plugin. import { registerAllPlugins } from './plugins/index.js'; import type { PageBuildData, StaticBuildOptions } from './types'; import { getTimeStat } from './util.js'; +import { MIDDLEWARE_PATH_SEGMENT_NAME } from '../constants.js'; export async function viteBuild(opts: StaticBuildOptions) { const { allPages, settings } = opts; diff --git a/packages/astro/src/core/build/types.ts b/packages/astro/src/core/build/types.ts index 02f5618d8776..fc7839390698 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; + middleware: AstroMiddlewareInstance; renderers: SSRLoadedRenderer[]; } diff --git a/packages/astro/src/core/config/schema.ts b/packages/astro/src/core/config/schema.ts index 49e9f64105e0..4c55dc5b63a2 100644 --- a/packages/astro/src/core/config/schema.ts +++ b/packages/astro/src/core/config/schema.ts @@ -29,7 +29,6 @@ const ASTRO_CONFIG_DEFAULTS: AstroUserConfig & any = { open: false, }, integrations: [], - middlewares: [], markdown: { drafts: false, ...markdownConfigDefaults, diff --git a/packages/astro/src/core/endpoint/dev/index.ts b/packages/astro/src/core/endpoint/dev/index.ts index 515b7aa41a8e..14432ca3a0e5 100644 --- a/packages/astro/src/core/endpoint/dev/index.ts +++ b/packages/astro/src/core/endpoint/dev/index.ts @@ -1,4 +1,8 @@ -import type { EndpointHandler } from '../../../@types/astro'; +import type { + AstroMiddlewareInstance, + EndpointHandler, + EndpointOutput, +} from '../../../@types/astro'; import type { LogOptions } from '../../logger/core'; import type { SSROptions } from '../../render/dev'; import { createRenderContext } from '../../render/index.js'; @@ -8,6 +12,7 @@ export async function call(options: SSROptions, logging: LogOptions) { const { env, preload: [, mod], + middleware, } = options; const endpointHandler = mod as unknown as EndpointHandler; @@ -18,5 +23,11 @@ export async function call(options: SSROptions, logging: LogOptions) { route: options.route, }); - return await callEndpoint(endpointHandler, env, ctx, logging); + return await callEndpoint( + endpointHandler, + env, + ctx, + logging, + middleware as AstroMiddlewareInstance + ); } diff --git a/packages/astro/src/core/endpoint/index.ts b/packages/astro/src/core/endpoint/index.ts index 22d901cbc667..e1be43ce36d1 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, + MiddlewareHandler, + Params, +} from '../../@types/astro'; import type { Environment, RenderContext } from '../render/index'; import { renderEndpoint } from '../../runtime/server/index.js'; @@ -7,6 +15,7 @@ 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/index.js'; const clientAddressSymbol = Symbol.for('astro.clientAddress'); const clientLocalsSymbol = Symbol.for('astro.locals'); @@ -75,7 +84,8 @@ export async function call( mod: EndpointHandler, env: Environment, ctx: RenderContext, - logging: LogOptions + logging: LogOptions, + middleware: AstroMiddlewareInstance | undefined ): Promise { const paramsAndPropsResp = await getParamsAndProps({ mod: mod as any, @@ -105,7 +115,15 @@ export async function call( adapterName: env.adapterName, }); - const response = await renderEndpoint(mod, context, env.ssr); + let response; + if (middleware && middleware.onRequest) { + const onRequest = middleware.onRequest as MiddlewareHandler; + response = await callMiddleware(onRequest, context, () => { + return renderEndpoint(mod, context, env.ssr); + }); + } else { + response = await renderEndpoint(mod, context, env.ssr); + } if (response instanceof Response) { attachToResponse(response, context.cookies); diff --git a/packages/astro/src/core/middleware/callMiddleware.ts b/packages/astro/src/core/middleware/callMiddleware.ts new file mode 100644 index 000000000000..aa42da7706d8 --- /dev/null +++ b/packages/astro/src/core/middleware/callMiddleware.ts @@ -0,0 +1,72 @@ +import type { APIContext, MiddlewareHandler, MiddlewareResolve } from '../../@types/astro'; +import { renderPage as coreRenderPage } from '../render'; + +/** + * Utility function that is in charge of calling the middleware + * + * It accepts a `R` generic, which usually the `Response` returned. + * It is a generic because endpoints can return a different response. + * + * When calling a middleware, we provide a `resolve` function, this function might or + * might not be called. Because of that, we use a `Promise.race` to understand which + * promise is resolved first. + * + * If `resolve` is called first, we resolve the `responseFunction` and we pass that response + * as resolved value to `resolve`. Finally, we resolve the middleware. + * This logic covers examples like: + * + * ```js + * const onRequest = async (context, resolve) => { + * const response = await resolve(context); + * return response; + * } + * ``` + * + * If the middleware is called first, we return the response without fancy logic. This covers cases like: + * + * ```js + * const onRequest = async (context, _) => { + * context.locals = "foo"; + * } + * ``` + * + * @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( + onRequest: MiddlewareHandler, + apiContext: APIContext, + responseFunction: () => Promise +): Promise { + let resolveResolve: any; + new Promise((resolve) => { + resolveResolve = resolve; + }); + + let resolveCalledResolve: any; + let resolveCalled = new Promise((resolve) => { + resolveCalledResolve = resolve; + }); + const resolve: MiddlewareResolve = () => { + const response = responseFunction(); + resolveCalledResolve('resolveCalled'); + return response; + }; + + let middlewarePromise = onRequest(apiContext, resolve); + + return await Promise.race([middlewarePromise, resolveCalled]).then(async (value) => { + if (value === 'resolveCalled') { + // Middleware called resolve() + // render the page and then pass back to middleware + // for post-processing + const responseResult = await responseFunction(); + await resolveResolve(responseResult); + return middlewarePromise; + } else { + // Middleware did not call resolve() + return await responseFunction(); + } + }); +} diff --git a/packages/astro/src/core/middleware/index.ts b/packages/astro/src/core/middleware/index.ts index e9e03a18d279..667cf65e0a2e 100644 --- a/packages/astro/src/core/middleware/index.ts +++ b/packages/astro/src/core/middleware/index.ts @@ -1 +1,5 @@ -export { sequence } from './sequence'; +import { sequence } from './sequence.js'; +import { callMiddleware } from './callMiddleware.js'; +import { loadMiddleware } from './loadMiddleware.js'; + +export { sequence, callMiddleware, loadMiddleware }; diff --git a/packages/astro/src/core/middleware/loadMiddleware.ts b/packages/astro/src/core/middleware/loadMiddleware.ts new file mode 100644 index 000000000000..be4009d8db84 --- /dev/null +++ b/packages/astro/src/core/middleware/loadMiddleware.ts @@ -0,0 +1,17 @@ +import type { ModuleLoader } from '../module-loader'; +import { MIDDLEWARE_PATH_SEGMENT_NAME } from '../constants.js'; + +/** + * 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, basePath: string) { + let middlewarePath = basePath + 'src/' + 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 index c4b09e91c8d3..7410cba0a45b 100644 --- a/packages/astro/src/core/middleware/sequence.ts +++ b/packages/astro/src/core/middleware/sequence.ts @@ -5,14 +5,19 @@ import type { APIContext, MiddlewareHandler } from '../../@types/astro'; * * It accepts one or more middleware handlers and makes sure that they are run in sequence. */ -export function sequence(...handlers: MiddlewareHandler[]): MiddlewareHandler { +export function sequence(...handlers: MiddlewareHandler[]): MiddlewareHandler { const length = handlers.length; - if (!length) return (context, resolve) => resolve(context); + if (!length) { + const handler: MiddlewareHandler = (context, resolve) => { + return resolve(context); + }; + return handler; + } return (context, resolve) => { return applyHandle(0, context); - function applyHandle(i: number, handleContext: APIContext): Promise { + function applyHandle(i: number, handleContext: APIContext): Promise { const handle = handlers[i]; return handle(handleContext, (nextContext) => { diff --git a/packages/astro/src/core/render/dev/index.ts b/packages/astro/src/core/render/dev/index.ts index 041983989f52..fcb08bcf9bcd 100644 --- a/packages/astro/src/core/render/dev/index.ts +++ b/packages/astro/src/core/render/dev/index.ts @@ -4,7 +4,6 @@ import type { AstroSettings, ComponentInstance, MiddlewareHandler, - MiddlewareResolve, RouteData, SSRElement, SSRLoadedRenderer, @@ -32,7 +31,7 @@ import type { DevelopmentEnvironment } from './environment'; import { getComponentMetadata } from './metadata.js'; import { getScriptsForURL } from './scripts.js'; import { createAPIContext } from '../../endpoint/index.js'; -import { sequence } from '../../middleware/sequence.js'; +import { sequence, callMiddleware } from '../../middleware/index.js'; export { createDevelopmentEnvironment } from './environment.js'; export type { DevelopmentEnvironment }; @@ -54,7 +53,7 @@ export interface SSROptions { /** * Optional middlewares */ - middleware?: AstroMiddlewareInstance; + middleware?: AstroMiddlewareInstance; } export type ComponentPreload = [SSRLoadedRenderer[], ComponentInstance]; @@ -211,8 +210,7 @@ export async function renderPage(options: SSROptions): Promise { }); } - const { onRequest } = options.middleware; - if (onRequest) { + if (options.middleware && options.middleware.onRequest) { const [params, pageProps] = paramsAndPropsRes; const apiContext = createAPIContext({ @@ -222,35 +220,9 @@ export async function renderPage(options: SSROptions): Promise { adapterName: options.env.adapterName, }); - let resolveResolve: any; - new Promise((resolve) => { - resolveResolve = resolve; - }); - - let resolveCalledResolve: any; - let resolveCalled = new Promise((resolve) => { - resolveCalledResolve = resolve; - }); - const resolve: MiddlewareResolve = (context) => { - const response = coreRenderPage(mod, ctx, options.env, apiContext); - resolveCalledResolve('resolveCalled'); - return response; - }; - - let middlewarePromise = onRequest(apiContext, resolve); - - let response = await Promise.race([middlewarePromise, resolveCalled]).then(async (value) => { - if (value === 'resolveCalled') { - // Middleware called resolve() - // render the page and then pass back to middleware - // for post-processing - const responseResult = await coreRenderPage(mod, ctx, options.env, apiContext); - await resolveResolve(responseResult); - return middlewarePromise; - } else { - // Middleware did not call resolve() - return await coreRenderPage(mod, ctx, options.env, apiContext); - } + const onRequest = options.middleware.onRequest as MiddlewareHandler; + const response = await callMiddleware(onRequest, apiContext, () => { + return coreRenderPage(mod, ctx, options.env, apiContext); }); return response; diff --git a/packages/astro/src/vite-plugin-astro-server/route.ts b/packages/astro/src/vite-plugin-astro-server/route.ts index 1234f9067124..1db4692815c9 100644 --- a/packages/astro/src/vite-plugin-astro-server/route.ts +++ b/packages/astro/src/vite-plugin-astro-server/route.ts @@ -6,11 +6,10 @@ import type { 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 { AstroError, AstroErrorData } from '../core/errors/index.js'; +import { AstroErrorData } from '../core/errors/index.js'; import { warn } from '../core/logger/core.js'; import { preload, renderPage } from '../core/render/dev/index.js'; import { getParamsAndProps, GetParamsAndPropsError } from '../core/render/index.js'; @@ -18,10 +17,7 @@ import { createRequest } from '../core/request.js'; import { matchAllRoutes } from '../core/routing/index.js'; import { log404 } from './common.js'; import { handle404Response, writeSSRResult, writeWebResponse } from './response.js'; -import type { ModuleLoader } from '../core/module-loader'; -import type { AstroMiddlewareInstance } from '../@types/astro'; -import { MIDDLEWARE_PATH_SEGMENT_NAME } from '../core/constants.js'; -import { fileURLToPath } from 'node:url'; +import { loadMiddleware } from '../core/middleware/index.js'; type AsyncReturnType Promise> = T extends ( ...args: any @@ -116,25 +112,6 @@ export async function matchRoute( return undefined; } -/** - * 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, - basePath: URL -): Promise { - let path = fileURLToPath(basePath); - let middlewarePath = path + 'src' + '/' + MIDDLEWARE_PATH_SEGMENT_NAME; - try { - const module = (await moduleLoader.import(middlewarePath)) as AstroMiddlewareInstance; - return module as AstroMiddlewareInstance; - } catch { - return undefined; - } -} - export async function handleRoute( matchedRoute: AsyncReturnType, url: URL, @@ -192,7 +169,7 @@ export async function handleRoute( request, route, }; - const middleware = await loadMiddleware(env.loader, config.root); + const middleware = await loadMiddleware(env.loader, config.root.href); if (middleware) { options.middleware = middleware; } From 04e163b1b9388730636d03952c0510deed56c02b Mon Sep 17 00:00:00 2001 From: Emanuele Stoppa Date: Tue, 11 Apr 2023 11:43:36 +0100 Subject: [PATCH 07/53] feat: support middleware in SSR endpoints --- packages/astro/src/core/app/index.ts | 52 ++++++++++++++-- packages/astro/src/core/app/node.ts | 2 + packages/astro/src/core/app/types.ts | 2 + .../src/core/build/plugins/plugin-ssr.ts | 7 ++- packages/astro/src/core/errors/errors-data.ts | 4 +- .../src/core/middleware/callMiddleware.ts | 1 - .../src/core/middleware/loadMiddleware.ts | 4 +- packages/astro/src/core/render/core.ts | 60 +++++++++++++++++-- packages/astro/src/core/util.ts | 54 ----------------- .../src/vite-plugin-astro-server/route.ts | 2 +- .../fixtures/dev-middeware/astro.config.mjs | 3 + .../test/fixtures/dev-middeware/package.json | 8 +++ .../fixtures/dev-middeware/src/middleware.js | 7 +++ .../dev-middeware/src/pages/index.astro | 13 ++++ packages/astro/test/middleware.test.js | 32 ++++++++++ 15 files changed, 182 insertions(+), 69 deletions(-) create mode 100644 packages/astro/test/fixtures/dev-middeware/astro.config.mjs create mode 100644 packages/astro/test/fixtures/dev-middeware/package.json create mode 100644 packages/astro/test/fixtures/dev-middeware/src/middleware.js create mode 100644 packages/astro/test/fixtures/dev-middeware/src/pages/index.astro create mode 100644 packages/astro/test/middleware.test.js diff --git a/packages/astro/src/core/app/index.ts b/packages/astro/src/core/app/index.ts index 494a54d88a8e..082ecdbd19d7 100644 --- a/packages/astro/src/core/app/index.ts +++ b/packages/astro/src/core/app/index.ts @@ -1,7 +1,10 @@ import type { + AstroMiddlewareInstance, ComponentInstance, EndpointHandler, + EndpointOutput, ManifestData, + MiddlewareHandler, RouteData, SSRElement, } from '../../@types/astro'; @@ -9,7 +12,7 @@ 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 { removeTrailingForwardSlash } from '../path.js'; @@ -18,6 +21,8 @@ import { createRenderContext, renderPage, type Environment, + getParamsAndProps, + GetParamsAndPropsError, } from '../render/index.js'; import { RouteCache } from '../render/route-cache.js'; import { @@ -27,6 +32,8 @@ import { } from '../render/ssr-element.js'; import { matchRoute } from '../routing/match.js'; export { deserializeManifest } from './common.js'; +import { callMiddleware } from '../middleware/index.js'; +import { AstroError, AstroErrorData } from '../errors/index.js'; export const pagesVirtualModuleId = '@astrojs-pages-virtual-entry'; export const resolvedPagesVirtualModuleId = '\0' + pagesVirtualModuleId; @@ -191,7 +198,7 @@ export class App { } try { - const ctx = createRenderContext({ + const renderContext = createRenderContext({ request, origin: url.origin, pathname, @@ -201,8 +208,38 @@ export class App { route: routeData, status, }); + const paramsAndPropsResp = await getParamsAndProps({ + mod: mod as any, + route: renderContext.route, + routeCache: this.#env.routeCache, + pathname: renderContext.pathname, + logging: this.#env.logging, + ssr: this.#env.ssr, + }); + + if (paramsAndPropsResp === GetParamsAndPropsError.NoMatchingStaticPath) { + throw new AstroError({ + ...AstroErrorData.NoMatchingStaticPathFound, + message: AstroErrorData.NoMatchingStaticPathFound.message(renderContext.pathname), + hint: renderContext.route?.component + ? AstroErrorData.NoMatchingStaticPathFound.hint([renderContext.route?.component]) + : '', + }); + } + const [params, props] = paramsAndPropsResp; - const response = await renderPage(mod, ctx, this.#env); + const apiContext = createAPIContext({ + request: renderContext.request, + params, + props, + site: this.#env.site, + adapterName: this.#env.adapterName, + }); + const onRequest = this.#manifest.middleware.onRequest as MiddlewareHandler; + const response = await callMiddleware(onRequest, apiContext, () => { + return renderPage(mod, renderContext, this.#env, apiContext); + }); + // const response = await renderPage(mod, renderContext, this.#env, apiContext); Reflect.set(request, responseSentSymbol, true); return response; } catch (err: any) { @@ -233,7 +270,14 @@ export class App { }); // TODO PLT-104 add adapter middleware here - const result = await callEndpoint(handler, this.#env, ctx, this.#logging, undefined); + + const result = await callEndpoint( + handler, + this.#env, + ctx, + this.#logging, + this.#manifest.middleware as AstroMiddlewareInstance + ); if (result.type === 'response') { if (result.response.headers.get('X-Astro-Response') === 'Not-Found') { diff --git a/packages/astro/src/core/app/node.ts b/packages/astro/src/core/app/node.ts index 97d7b71d18dd..e8ef433526b8 100644 --- a/packages/astro/src/core/app/node.ts +++ b/packages/astro/src/core/app/node.ts @@ -8,6 +8,7 @@ import { deserializeManifest } from './common.js'; import { App, type MatchOptions } from './index.js'; const clientAddressSymbol = Symbol.for('astro.clientAddress'); +const clientLocalsSymbol = Symbol.for('astro.locals'); function createRequestFromNodeRequest(req: NodeIncomingMessage, body?: Uint8Array): Request { const protocol = @@ -26,6 +27,7 @@ function createRequestFromNodeRequest(req: NodeIncomingMessage, body?: Uint8Arra if (req.socket?.remoteAddress) { Reflect.set(request, clientAddressSymbol, req.socket.remoteAddress); } + Reflect.set(request, clientLocalsSymbol, {}); return request; } diff --git a/packages/astro/src/core/app/types.ts b/packages/astro/src/core/app/types.ts index ab91c13ca316..6256bbf544e5 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; assets: Set; componentMetadata: SSRResult['componentMetadata']; + middleware: AstroMiddlewareInstance; } export type SerializedSSRManifest = Omit & { diff --git a/packages/astro/src/core/build/plugins/plugin-ssr.ts b/packages/astro/src/core/build/plugins/plugin-ssr.ts index e5bca2ad0c24..a0fb67cd98ff 100644 --- a/packages/astro/src/core/build/plugins/plugin-ssr.ts +++ b/packages/astro/src/core/build/plugins/plugin-ssr.ts @@ -41,7 +41,8 @@ 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: _main.middleware }); _privateSetManifestDontUseThis(_manifest); const _args = ${adapter.args ? JSON.stringify(adapter.args) : 'undefined'}; @@ -221,6 +222,10 @@ function buildManifest( renderers: [], entryModules, assets: staticFiles.map(prefixAssetPath), + middleware: { + // @ts-expect-error + onRequest() {}, + }, }; return ssrManifest; diff --git a/packages/astro/src/core/errors/errors-data.ts b/packages/astro/src/core/errors/errors-data.ts index b69790419ef3..e0d235c55be1 100644 --- a/packages/astro/src/core/errors/errors-data.ts +++ b/packages/astro/src/core/errors/errors-data.ts @@ -645,8 +645,8 @@ See https://docs.astro.build/en/guides/server-side-rendering/ for more informati LocalsNotSerializable: { title: '`Astro.locals` are not serializable.', code: 3031, - message: (href: string) => { - return `Information stored in \`Astro.locals\` are not serializable when visiting "${href}" path. Make sure you store only data that are compatible.`; + message: (href: string, locals: any) => { + return `Information stored in \`Astro.locals\` are not serializable when visiting "${href}" path.\nReceived: ${locals}\nMake sure you store only data that are compatible.`; }, }, // No headings here, that way Vite errors are merged with Astro ones in the docs, which makes more sense to users. diff --git a/packages/astro/src/core/middleware/callMiddleware.ts b/packages/astro/src/core/middleware/callMiddleware.ts index aa42da7706d8..4f7a9db69236 100644 --- a/packages/astro/src/core/middleware/callMiddleware.ts +++ b/packages/astro/src/core/middleware/callMiddleware.ts @@ -1,5 +1,4 @@ import type { APIContext, MiddlewareHandler, MiddlewareResolve } from '../../@types/astro'; -import { renderPage as coreRenderPage } from '../render'; /** * Utility function that is in charge of calling the middleware diff --git a/packages/astro/src/core/middleware/loadMiddleware.ts b/packages/astro/src/core/middleware/loadMiddleware.ts index be4009d8db84..438c14839cef 100644 --- a/packages/astro/src/core/middleware/loadMiddleware.ts +++ b/packages/astro/src/core/middleware/loadMiddleware.ts @@ -6,8 +6,8 @@ import { MIDDLEWARE_PATH_SEGMENT_NAME } from '../constants.js'; * * If not middlewares were not set, the function returns an empty array. */ -export async function loadMiddleware(moduleLoader: ModuleLoader, basePath: string) { - let middlewarePath = basePath + 'src/' + MIDDLEWARE_PATH_SEGMENT_NAME; +export async function loadMiddleware(moduleLoader: ModuleLoader) { + let middlewarePath = 'src/' + MIDDLEWARE_PATH_SEGMENT_NAME; try { const module = await moduleLoader.import(middlewarePath); return module; diff --git a/packages/astro/src/core/render/core.ts b/packages/astro/src/core/render/core.ts index d224fb0271c1..15fc4e98c13d 100644 --- a/packages/astro/src/core/render/core.ts +++ b/packages/astro/src/core/render/core.ts @@ -2,14 +2,12 @@ import type { APIContext, ComponentInstance, Params, Props, RouteData } from '.. import type { LogOptions } from '../logger/core.js'; import type { RenderContext } from './context.js'; import type { Environment } from './environment.js'; - import { renderPage as runtimeRenderPage } from '../../runtime/server/index.js'; import { attachToResponse } from '../cookies/index.js'; import { AstroError, AstroErrorData } from '../errors/index.js'; import { getParams } from '../routing/params.js'; import { createResult } from './result.js'; import { callGetStaticPaths, findPathItemByKey, RouteCache } from './route-cache.js'; -import { isValueSerializable } from '../util.js'; interface GetParamsAndPropsOptions { mod: ComponentInstance; @@ -118,10 +116,10 @@ export async function renderPage( let locals = {}; if (apiContext) { - if (!isValueSerializable(apiContext.locals)) { + if (typeof apiContext.locals !== 'undefined' && !isValueSerializable(apiContext.locals)) { throw new AstroError({ ...AstroErrorData.LocalsNotSerializable, - message: AstroErrorData.LocalsNotSerializable.message(ctx.pathname), + message: AstroErrorData.LocalsNotSerializable.message(ctx.pathname, apiContext.locals), }); } locals = apiContext.locals; @@ -170,3 +168,57 @@ export async function renderPage( 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/util.ts b/packages/astro/src/core/util.ts index 79354928dec5..5d8868115056 100644 --- a/packages/astro/src/core/util.ts +++ b/packages/astro/src/core/util.ts @@ -217,57 +217,3 @@ export function resolvePath(specifier: string, importer: string) { return specifier; } } - -/** - * 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/vite-plugin-astro-server/route.ts b/packages/astro/src/vite-plugin-astro-server/route.ts index 1db4692815c9..b95aeb1554c8 100644 --- a/packages/astro/src/vite-plugin-astro-server/route.ts +++ b/packages/astro/src/vite-plugin-astro-server/route.ts @@ -169,7 +169,7 @@ export async function handleRoute( request, route, }; - const middleware = await loadMiddleware(env.loader, config.root.href); + const middleware = await loadMiddleware(env.loader); if (middleware) { options.middleware = middleware; } diff --git a/packages/astro/test/fixtures/dev-middeware/astro.config.mjs b/packages/astro/test/fixtures/dev-middeware/astro.config.mjs new file mode 100644 index 000000000000..86dbfb924824 --- /dev/null +++ b/packages/astro/test/fixtures/dev-middeware/astro.config.mjs @@ -0,0 +1,3 @@ +import { defineConfig } from 'astro/config'; + +export default defineConfig({}); diff --git a/packages/astro/test/fixtures/dev-middeware/package.json b/packages/astro/test/fixtures/dev-middeware/package.json new file mode 100644 index 000000000000..780e2eb49868 --- /dev/null +++ b/packages/astro/test/fixtures/dev-middeware/package.json @@ -0,0 +1,8 @@ +{ + "name": "@test/dev-middleware", + "version": "0.0.0", + "private": true, + "dependencies": { + "astro": "workspace:*" + } +} diff --git a/packages/astro/test/fixtures/dev-middeware/src/middleware.js b/packages/astro/test/fixtures/dev-middeware/src/middleware.js new file mode 100644 index 000000000000..0a65ec5cf8f9 --- /dev/null +++ b/packages/astro/test/fixtures/dev-middeware/src/middleware.js @@ -0,0 +1,7 @@ +/** @type import("astro").AstroMiddlewareHandler */ +const onRequest = async (context, resolve) => { + console.log('calling on request'); + context.locals.foo = 'bar'; +}; + +export { onRequest }; diff --git a/packages/astro/test/fixtures/dev-middeware/src/pages/index.astro b/packages/astro/test/fixtures/dev-middeware/src/pages/index.astro new file mode 100644 index 000000000000..adcefdb5ea0f --- /dev/null +++ b/packages/astro/test/fixtures/dev-middeware/src/pages/index.astro @@ -0,0 +1,13 @@ +--- +const data = Astro.locals; +--- + + + + Testing + + + +

{data?.foo}

+ + diff --git a/packages/astro/test/middleware.test.js b/packages/astro/test/middleware.test.js new file mode 100644 index 000000000000..f29e68b3155c --- /dev/null +++ b/packages/astro/test/middleware.test.js @@ -0,0 +1,32 @@ +import { loadFixture } from './test-utils.js'; +import { expect } from 'chai'; +import * as cheerio from 'cheerio'; + +describe.skip('Middleware API', () => { + /** @type {import('./test-utils').Fixture} */ + let fixture; + + before(async () => { + fixture = await loadFixture({ + root: './fixtures/dev-middleware/', + }); + }); + + describe('in DEV mode', () => { + let devServer; + + before(async () => { + 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'); + }); + }); +}); From 24d8d3937595be66d4a096c3b494d2fc2a6793a6 Mon Sep 17 00:00:00 2001 From: Emanuele Stoppa Date: Tue, 11 Apr 2023 15:38:17 +0100 Subject: [PATCH 08/53] chore: add tests --- packages/astro/src/core/app/index.ts | 4 ++++ packages/astro/src/core/app/node.ts | 2 -- packages/astro/src/core/render/core.ts | 2 +- .../fixtures/dev-middeware/src/middleware.js | 7 ------- .../astro.config.mjs | 0 .../package.json | 0 .../fixtures/dev-middleware/src/middleware.js | 20 +++++++++++++++++++ .../src/pages/index.astro | 2 +- .../dev-middleware/src/pages/lorem.astro | 13 ++++++++++++ .../dev-middleware/src/pages/second.astro | 13 ++++++++++++ packages/astro/test/middleware.test.js | 18 ++++++++++++++++- 11 files changed, 69 insertions(+), 12 deletions(-) delete mode 100644 packages/astro/test/fixtures/dev-middeware/src/middleware.js rename packages/astro/test/fixtures/{dev-middeware => dev-middleware}/astro.config.mjs (100%) rename packages/astro/test/fixtures/{dev-middeware => dev-middleware}/package.json (100%) create mode 100644 packages/astro/test/fixtures/dev-middleware/src/middleware.js rename packages/astro/test/fixtures/{dev-middeware => dev-middleware}/src/pages/index.astro (83%) create mode 100644 packages/astro/test/fixtures/dev-middleware/src/pages/lorem.astro create mode 100644 packages/astro/test/fixtures/dev-middleware/src/pages/second.astro diff --git a/packages/astro/src/core/app/index.ts b/packages/astro/src/core/app/index.ts index 082ecdbd19d7..7e4181cf7ae5 100644 --- a/packages/astro/src/core/app/index.ts +++ b/packages/astro/src/core/app/index.ts @@ -35,6 +35,8 @@ export { deserializeManifest } from './common.js'; import { callMiddleware } from '../middleware/index.js'; import { AstroError, AstroErrorData } from '../errors/index.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'); @@ -134,6 +136,8 @@ export class App { } } + Reflect.set(request, clientLocalsSymbol, {}); + // Use the 404 status code for 404.astro components if (routeData.route === '/404') { defaultStatus = 404; diff --git a/packages/astro/src/core/app/node.ts b/packages/astro/src/core/app/node.ts index e8ef433526b8..97d7b71d18dd 100644 --- a/packages/astro/src/core/app/node.ts +++ b/packages/astro/src/core/app/node.ts @@ -8,7 +8,6 @@ import { deserializeManifest } from './common.js'; import { App, type MatchOptions } from './index.js'; const clientAddressSymbol = Symbol.for('astro.clientAddress'); -const clientLocalsSymbol = Symbol.for('astro.locals'); function createRequestFromNodeRequest(req: NodeIncomingMessage, body?: Uint8Array): Request { const protocol = @@ -27,7 +26,6 @@ function createRequestFromNodeRequest(req: NodeIncomingMessage, body?: Uint8Arra if (req.socket?.remoteAddress) { Reflect.set(request, clientAddressSymbol, req.socket.remoteAddress); } - Reflect.set(request, clientLocalsSymbol, {}); return request; } diff --git a/packages/astro/src/core/render/core.ts b/packages/astro/src/core/render/core.ts index 15fc4e98c13d..c0b56153bc9c 100644 --- a/packages/astro/src/core/render/core.ts +++ b/packages/astro/src/core/render/core.ts @@ -116,7 +116,7 @@ export async function renderPage( let locals = {}; if (apiContext) { - if (typeof apiContext.locals !== 'undefined' && !isValueSerializable(apiContext.locals)) { + if (!isValueSerializable(apiContext.locals)) { throw new AstroError({ ...AstroErrorData.LocalsNotSerializable, message: AstroErrorData.LocalsNotSerializable.message(ctx.pathname, apiContext.locals), diff --git a/packages/astro/test/fixtures/dev-middeware/src/middleware.js b/packages/astro/test/fixtures/dev-middeware/src/middleware.js deleted file mode 100644 index 0a65ec5cf8f9..000000000000 --- a/packages/astro/test/fixtures/dev-middeware/src/middleware.js +++ /dev/null @@ -1,7 +0,0 @@ -/** @type import("astro").AstroMiddlewareHandler */ -const onRequest = async (context, resolve) => { - console.log('calling on request'); - context.locals.foo = 'bar'; -}; - -export { onRequest }; diff --git a/packages/astro/test/fixtures/dev-middeware/astro.config.mjs b/packages/astro/test/fixtures/dev-middleware/astro.config.mjs similarity index 100% rename from packages/astro/test/fixtures/dev-middeware/astro.config.mjs rename to packages/astro/test/fixtures/dev-middleware/astro.config.mjs diff --git a/packages/astro/test/fixtures/dev-middeware/package.json b/packages/astro/test/fixtures/dev-middleware/package.json similarity index 100% rename from packages/astro/test/fixtures/dev-middeware/package.json rename to packages/astro/test/fixtures/dev-middleware/package.json diff --git a/packages/astro/test/fixtures/dev-middleware/src/middleware.js b/packages/astro/test/fixtures/dev-middleware/src/middleware.js new file mode 100644 index 000000000000..04d1bc449fba --- /dev/null +++ b/packages/astro/test/fixtures/dev-middleware/src/middleware.js @@ -0,0 +1,20 @@ +import { sequence } from 'astro/middleware'; + +/** @type import("astro").AstroMiddlewareHandler */ +const first = async (context, resolve) => { + if (context.request.url.endsWith('/lorem')) { + context.locals.name = 'ipsum'; + } else { + context.locals.name = 'bar'; + } + return await resolve(context); +}; + +/** @type import("astro").AstroMiddlewareHandler */ +const second = async (context, resolve) => { + if (context.request.url.endsWith('/second')) { + context.locals.name = 'second'; + } +}; + +export const onRequest = sequence(first, second); diff --git a/packages/astro/test/fixtures/dev-middeware/src/pages/index.astro b/packages/astro/test/fixtures/dev-middleware/src/pages/index.astro similarity index 83% rename from packages/astro/test/fixtures/dev-middeware/src/pages/index.astro rename to packages/astro/test/fixtures/dev-middleware/src/pages/index.astro index adcefdb5ea0f..b734f2c1e8f7 100644 --- a/packages/astro/test/fixtures/dev-middeware/src/pages/index.astro +++ b/packages/astro/test/fixtures/dev-middleware/src/pages/index.astro @@ -8,6 +8,6 @@ const data = Astro.locals; -

{data?.foo}

+

{data?.name}

diff --git a/packages/astro/test/fixtures/dev-middleware/src/pages/lorem.astro b/packages/astro/test/fixtures/dev-middleware/src/pages/lorem.astro new file mode 100644 index 000000000000..c6edf9cd75a0 --- /dev/null +++ b/packages/astro/test/fixtures/dev-middleware/src/pages/lorem.astro @@ -0,0 +1,13 @@ +--- +const data = Astro.locals; +--- + + + + Testing + + + +

{data?.name}

+ + diff --git a/packages/astro/test/fixtures/dev-middleware/src/pages/second.astro b/packages/astro/test/fixtures/dev-middleware/src/pages/second.astro new file mode 100644 index 000000000000..c6edf9cd75a0 --- /dev/null +++ b/packages/astro/test/fixtures/dev-middleware/src/pages/second.astro @@ -0,0 +1,13 @@ +--- +const data = Astro.locals; +--- + + + + Testing + + + +

{data?.name}

+ + diff --git a/packages/astro/test/middleware.test.js b/packages/astro/test/middleware.test.js index f29e68b3155c..36671d1cbc88 100644 --- a/packages/astro/test/middleware.test.js +++ b/packages/astro/test/middleware.test.js @@ -2,7 +2,7 @@ import { loadFixture } from './test-utils.js'; import { expect } from 'chai'; import * as cheerio from 'cheerio'; -describe.skip('Middleware API', () => { +describe('Middleware API', () => { /** @type {import('./test-utils').Fixture} */ let fixture; @@ -28,5 +28,21 @@ describe.skip('Middleware API', () => { 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'); + }); }); }); From 1fb83c02f1092885e9d6d82732f82136076e2d2d Mon Sep 17 00:00:00 2001 From: Emanuele Stoppa Date: Tue, 11 Apr 2023 16:16:53 +0100 Subject: [PATCH 09/53] chore: rename node middleware file --- .../integrations/node/src/{middleware.ts => nodeMiddleware.ts} | 0 packages/integrations/node/src/server.ts | 2 +- packages/integrations/node/src/standalone.ts | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) rename packages/integrations/node/src/{middleware.ts => nodeMiddleware.ts} (100%) diff --git a/packages/integrations/node/src/middleware.ts b/packages/integrations/node/src/nodeMiddleware.ts similarity index 100% rename from packages/integrations/node/src/middleware.ts rename to packages/integrations/node/src/nodeMiddleware.ts diff --git a/packages/integrations/node/src/server.ts b/packages/integrations/node/src/server.ts index ed03b68a6b77..98f5cd14bd4f 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 813174252692..85eb3822a773 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) { From 8d315ec8770f216706f6e52dc0ad5f51991daf7d Mon Sep 17 00:00:00 2001 From: Emanuele Stoppa Date: Wed, 12 Apr 2023 13:42:23 +0100 Subject: [PATCH 10/53] chore: fix middleware export --- packages/astro/package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/astro/package.json b/packages/astro/package.json index 3160a44f41d6..cb497331f40a 100644 --- a/packages/astro/package.json +++ b/packages/astro/package.json @@ -85,8 +85,8 @@ "config.mjs", "zod.d.ts", "zod.mjs", - "sequence.d.ts", - "sequence.mjs", + "middleware.d.ts", + "middleware.mjs", "env.d.ts", "client.d.ts", "client-base.d.ts", From ad86e02876a149ed88769377e8521fb02f5a21c9 Mon Sep 17 00:00:00 2001 From: Emanuele Stoppa Date: Thu, 13 Apr 2023 13:20:23 +0100 Subject: [PATCH 11/53] chore: fixes, feedback and more tests --- packages/astro/middleware.d.ts | 4 +-- packages/astro/src/@types/astro.ts | 30 +++++++++++-------- packages/astro/src/core/app/index.ts | 3 +- packages/astro/src/core/build/generate.ts | 3 +- packages/astro/src/core/endpoint/dev/index.ts | 3 ++ packages/astro/src/core/endpoint/index.ts | 16 +++++++--- packages/astro/src/core/errors/errors-data.ts | 4 +-- .../src/core/middleware/callMiddleware.ts | 4 +-- .../astro/src/core/middleware/sequence.ts | 10 +++++-- packages/astro/src/core/render/core.ts | 2 +- packages/astro/src/core/render/dev/index.ts | 4 +-- packages/astro/src/runtime/server/endpoint.ts | 2 +- .../fixtures/dev-middleware/astro.config.mjs | 8 ++++- .../test/fixtures/dev-middleware/package.json | 3 +- .../fixtures/dev-middleware/src/middleware.js | 21 +++++++++---- .../dev-middleware/src/pages/index.astro | 1 + .../dev-middleware/src/pages/redirect.astro | 0 .../dev-middleware/src/pages/rewrite.astro | 9 ++++++ packages/astro/test/middleware.test.js | 14 +++++++++ 19 files changed, 103 insertions(+), 38 deletions(-) create mode 100644 packages/astro/test/fixtures/dev-middleware/src/pages/redirect.astro create mode 100644 packages/astro/test/fixtures/dev-middleware/src/pages/rewrite.astro diff --git a/packages/astro/middleware.d.ts b/packages/astro/middleware.d.ts index 2dd1c56ff539..45072ea670cb 100644 --- a/packages/astro/middleware.d.ts +++ b/packages/astro/middleware.d.ts @@ -1,3 +1,3 @@ -type MiddlewareHandler = import('./dist/@types/astro.js').MiddlewareHandler; +type MiddlewareResponseHandler = import('./dist/@types/astro.js').MiddlewareResponseHandler; -type Sequence = (...handlers: MiddlewareHandler[]) => MiddlewareHandler; +type Sequence = (...handlers: MiddlewareResponseHandler[]) => MiddlewareResponseHandler; diff --git a/packages/astro/src/@types/astro.ts b/packages/astro/src/@types/astro.ts index 573ca5803915..fc69a89e3392 100644 --- a/packages/astro/src/@types/astro.ts +++ b/packages/astro/src/@types/astro.ts @@ -1373,7 +1373,6 @@ export interface Page { export type PaginateFunction = (data: any[], args?: PaginateOptions) => GetStaticPathsResult; export type Params = Record; -export type Locals = Record; export interface AstroAdapter { name: string; @@ -1386,7 +1385,10 @@ export interface AstroAdapter { type Body = string; // Shared types between `Astro` global and API context object -interface AstroSharedContext = Record> { +interface AstroSharedContext< + Props extends Record = Record, + Locals extends Record = Record +> { /** * The address (usually IP address) of the user. Used with SSR only. */ @@ -1422,8 +1424,10 @@ interface AstroSharedContext = Record = Record> - extends AstroSharedContext { +export interface APIContext< + Props extends Record = Record, + Locals extends Record = Record +> extends AstroSharedContext { site: URL | undefined; generator: string; /** @@ -1453,7 +1457,7 @@ export interface APIContext = Record = Record['props']; + props: AstroSharedContext['props']; /** * Redirect to another page. Only available in SSR builds. * @@ -1497,10 +1501,11 @@ export interface APIContext = Record['locals']; } export type Props = Record; +export type Locals = Record; export interface EndpointOutput { body: Body; @@ -1586,11 +1591,12 @@ export interface AstroIntegration { }; } -export type MiddlewareResolve = (context: APIContext) => Promise; -export type MiddlewareHandler = ( - context: Readonly, - response: MiddlewareResolve -) => Promise; +export type MiddlewareNext = () => Promise; +export type MiddlewareHandler = (context: APIContext, next: MiddlewareNext) => Promise; + +export type MiddlewareResponseHandler = MiddlewareHandler; +export type MiddlewareEndpointHandler = MiddlewareHandler; +export type MiddlewareNextResponse = MiddlewareNext; // NOTE: when updating this file with other functions, // remember to update `plugin-page.ts` too, to add that function as a no-op function. diff --git a/packages/astro/src/core/app/index.ts b/packages/astro/src/core/app/index.ts index 7e4181cf7ae5..8f8c8f48e6bc 100644 --- a/packages/astro/src/core/app/index.ts +++ b/packages/astro/src/core/app/index.ts @@ -5,6 +5,7 @@ import type { EndpointOutput, ManifestData, MiddlewareHandler, + MiddlewareResponseHandler, RouteData, SSRElement, } from '../../@types/astro'; @@ -239,7 +240,7 @@ export class App { site: this.#env.site, adapterName: this.#env.adapterName, }); - const onRequest = this.#manifest.middleware.onRequest as MiddlewareHandler; + const onRequest = this.#manifest.middleware.onRequest as MiddlewareResponseHandler; const response = await callMiddleware(onRequest, apiContext, () => { return renderPage(mod, renderContext, this.#env, apiContext); }); diff --git a/packages/astro/src/core/build/generate.ts b/packages/astro/src/core/build/generate.ts index 2b0da68687c6..a027ca64c6f8 100644 --- a/packages/astro/src/core/build/generate.ts +++ b/packages/astro/src/core/build/generate.ts @@ -15,6 +15,7 @@ import type { SSRError, SSRLoadedRenderer, EndpointOutput, + MiddlewareResponseHandler, } from '../../@types/astro'; import { generateImage as generateImageInternal, @@ -496,7 +497,7 @@ async function generatePath( }); // If the user doesn't configure a middleware, the rollup plugin emits a no-op function, // so it's safe to use `callMiddleware` regardless - let onRequest = middleware.onRequest as MiddlewareHandler; + const onRequest = middleware.onRequest as MiddlewareResponseHandler; response = await callMiddleware(onRequest, context, () => { return renderPage(mod, ctx, env, context); }); diff --git a/packages/astro/src/core/endpoint/dev/index.ts b/packages/astro/src/core/endpoint/dev/index.ts index 14432ca3a0e5..cb02b501b0c0 100644 --- a/packages/astro/src/core/endpoint/dev/index.ts +++ b/packages/astro/src/core/endpoint/dev/index.ts @@ -7,6 +7,9 @@ import type { LogOptions } from '../../logger/core'; import type { SSROptions } from '../../render/dev'; import { createRenderContext } from '../../render/index.js'; import { call as callEndpoint } from '../index.js'; +import { renderEndpoint } from '../../../runtime/server'; +import { isValueSerializable } from '../../render/core'; +import { AstroError, AstroErrorData } from '../../errors'; export async function call(options: SSROptions, logging: LogOptions) { const { diff --git a/packages/astro/src/core/endpoint/index.ts b/packages/astro/src/core/endpoint/index.ts index e1be43ce36d1..9fd683524f3e 100644 --- a/packages/astro/src/core/endpoint/index.ts +++ b/packages/astro/src/core/endpoint/index.ts @@ -4,6 +4,7 @@ import type { AstroMiddlewareInstance, EndpointHandler, EndpointOutput, + MiddlewareEndpointHandler, MiddlewareHandler, Params, } from '../../@types/astro'; @@ -14,7 +15,7 @@ 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 { getParamsAndProps, GetParamsAndPropsError, isValueSerializable } from '../render/core.js'; import { callMiddleware } from '../middleware/index.js'; const clientAddressSymbol = Symbol.for('astro.clientAddress'); @@ -117,9 +118,16 @@ export async function call( let response; if (middleware && middleware.onRequest) { - const onRequest = middleware.onRequest as MiddlewareHandler; - response = await callMiddleware(onRequest, context, () => { - return renderEndpoint(mod, context, env.ssr); + const onRequest = middleware.onRequest as MiddlewareEndpointHandler; + response = await callMiddleware(onRequest, context, async () => { + const result = await renderEndpoint(mod, context, env.ssr); + if (!isValueSerializable(context.locals)) { + throw new AstroError({ + ...AstroErrorData.LocalsNotSerializable, + message: AstroErrorData.LocalsNotSerializable.message(ctx.pathname), + }); + } + return result; }); } else { response = await renderEndpoint(mod, context, env.ssr); diff --git a/packages/astro/src/core/errors/errors-data.ts b/packages/astro/src/core/errors/errors-data.ts index e0d235c55be1..37bab8616ef3 100644 --- a/packages/astro/src/core/errors/errors-data.ts +++ b/packages/astro/src/core/errors/errors-data.ts @@ -645,8 +645,8 @@ See https://docs.astro.build/en/guides/server-side-rendering/ for more informati LocalsNotSerializable: { title: '`Astro.locals` are not serializable.', code: 3031, - message: (href: string, locals: any) => { - return `Information stored in \`Astro.locals\` are not serializable when visiting "${href}" path.\nReceived: ${locals}\nMake sure you store only data that are compatible.`; + message: (href: string) => { + return `The information stored in \`Astro.locals\` are not serializable when visiting "${href}" path.\nMake sure you store only data that are serializable.`; }, }, // No headings here, that way Vite errors are merged with Astro ones in the docs, which makes more sense to users. diff --git a/packages/astro/src/core/middleware/callMiddleware.ts b/packages/astro/src/core/middleware/callMiddleware.ts index 4f7a9db69236..69215d7b49b1 100644 --- a/packages/astro/src/core/middleware/callMiddleware.ts +++ b/packages/astro/src/core/middleware/callMiddleware.ts @@ -1,4 +1,4 @@ -import type { APIContext, MiddlewareHandler, MiddlewareResolve } from '../../@types/astro'; +import type { APIContext, MiddlewareHandler, MiddlewareNext } from '../../@types/astro'; /** * Utility function that is in charge of calling the middleware @@ -47,7 +47,7 @@ export async function callMiddleware( let resolveCalled = new Promise((resolve) => { resolveCalledResolve = resolve; }); - const resolve: MiddlewareResolve = () => { + const resolve: MiddlewareNext = () => { const response = responseFunction(); resolveCalledResolve('resolveCalled'); return response; diff --git a/packages/astro/src/core/middleware/sequence.ts b/packages/astro/src/core/middleware/sequence.ts index 7410cba0a45b..f32aa1945a48 100644 --- a/packages/astro/src/core/middleware/sequence.ts +++ b/packages/astro/src/core/middleware/sequence.ts @@ -9,7 +9,7 @@ export function sequence(...handlers: MiddlewareHandler[]): MiddlewareHand const length = handlers.length; if (!length) { const handler: MiddlewareHandler = (context, resolve) => { - return resolve(context); + return resolve(); }; return handler; } @@ -20,8 +20,12 @@ export function sequence(...handlers: MiddlewareHandler[]): MiddlewareHand function applyHandle(i: number, handleContext: APIContext): Promise { const handle = handlers[i]; - return handle(handleContext, (nextContext) => { - return i < length - 1 ? applyHandle(i + 1, nextContext) : resolve(nextContext); + return handle(handleContext, () => { + if (i < length - 1) { + return applyHandle(i + 1, handleContext); + } else { + return resolve(); + } }); } }; diff --git a/packages/astro/src/core/render/core.ts b/packages/astro/src/core/render/core.ts index c0b56153bc9c..3732d553f2ce 100644 --- a/packages/astro/src/core/render/core.ts +++ b/packages/astro/src/core/render/core.ts @@ -119,7 +119,7 @@ export async function renderPage( if (!isValueSerializable(apiContext.locals)) { throw new AstroError({ ...AstroErrorData.LocalsNotSerializable, - message: AstroErrorData.LocalsNotSerializable.message(ctx.pathname, apiContext.locals), + message: AstroErrorData.LocalsNotSerializable.message(ctx.pathname), }); } locals = apiContext.locals; diff --git a/packages/astro/src/core/render/dev/index.ts b/packages/astro/src/core/render/dev/index.ts index fcb08bcf9bcd..4613ba4eec00 100644 --- a/packages/astro/src/core/render/dev/index.ts +++ b/packages/astro/src/core/render/dev/index.ts @@ -3,7 +3,7 @@ import type { AstroMiddlewareInstance, AstroSettings, ComponentInstance, - MiddlewareHandler, + MiddlewareResponseHandler, RouteData, SSRElement, SSRLoadedRenderer, @@ -220,7 +220,7 @@ export async function renderPage(options: SSROptions): Promise { adapterName: options.env.adapterName, }); - const onRequest = options.middleware.onRequest as MiddlewareHandler; + const onRequest = options.middleware.onRequest as MiddlewareResponseHandler; const response = await callMiddleware(onRequest, apiContext, () => { return coreRenderPage(mod, ctx, options.env, apiContext); }); diff --git a/packages/astro/src/runtime/server/endpoint.ts b/packages/astro/src/runtime/server/endpoint.ts index 33ce8f6f93c0..9780d6599cb5 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/test/fixtures/dev-middleware/astro.config.mjs b/packages/astro/test/fixtures/dev-middleware/astro.config.mjs index 86dbfb924824..682dab75277c 100644 --- a/packages/astro/test/fixtures/dev-middleware/astro.config.mjs +++ b/packages/astro/test/fixtures/dev-middleware/astro.config.mjs @@ -1,3 +1,9 @@ import { defineConfig } from 'astro/config'; +import node from '@astrojs/node'; -export default defineConfig({}); +export default defineConfig({ + output: "server", + adapter: node({ + mode: "standalone" + }) +}); diff --git a/packages/astro/test/fixtures/dev-middleware/package.json b/packages/astro/test/fixtures/dev-middleware/package.json index 780e2eb49868..e242868c7a9a 100644 --- a/packages/astro/test/fixtures/dev-middleware/package.json +++ b/packages/astro/test/fixtures/dev-middleware/package.json @@ -3,6 +3,7 @@ "version": "0.0.0", "private": true, "dependencies": { - "astro": "workspace:*" + "astro": "workspace:*", + "@astrojs/node": "workspace:*" } } diff --git a/packages/astro/test/fixtures/dev-middleware/src/middleware.js b/packages/astro/test/fixtures/dev-middleware/src/middleware.js index 04d1bc449fba..1fde2f9f2c49 100644 --- a/packages/astro/test/fixtures/dev-middleware/src/middleware.js +++ b/packages/astro/test/fixtures/dev-middleware/src/middleware.js @@ -1,20 +1,31 @@ import { sequence } from 'astro/middleware'; -/** @type import("astro").AstroMiddlewareHandler */ -const first = async (context, resolve) => { +/** @type import("astro").MiddlewareResponseHandler */ +const first = async (context, next) => { if (context.request.url.endsWith('/lorem')) { context.locals.name = 'ipsum'; + } else if (context.request.url.endsWith('/rewrite')) { + const response = await next(); + return new Response('New content!!', { + status: 200, + headers: response.headers, + }); } else { context.locals.name = 'bar'; } - return await resolve(context); + return await next(); }; -/** @type import("astro").AstroMiddlewareHandler */ -const second = async (context, resolve) => { +/** @type import("astro").MiddlewareResponseHandler */ +const second = async (context, next) => { if (context.request.url.endsWith('/second')) { context.locals.name = 'second'; + } else if (context.request.url.endsWith('/redirect')) { + // before a redirect, we need to resolve the response + await next(); + return context.redirect('/', 302); } + return await next(); }; export const onRequest = sequence(first, second); diff --git a/packages/astro/test/fixtures/dev-middleware/src/pages/index.astro b/packages/astro/test/fixtures/dev-middleware/src/pages/index.astro index b734f2c1e8f7..395a4d695cfa 100644 --- a/packages/astro/test/fixtures/dev-middleware/src/pages/index.astro +++ b/packages/astro/test/fixtures/dev-middleware/src/pages/index.astro @@ -8,6 +8,7 @@ const data = Astro.locals; + Index

{data?.name}

diff --git a/packages/astro/test/fixtures/dev-middleware/src/pages/redirect.astro b/packages/astro/test/fixtures/dev-middleware/src/pages/redirect.astro new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/packages/astro/test/fixtures/dev-middleware/src/pages/rewrite.astro b/packages/astro/test/fixtures/dev-middleware/src/pages/rewrite.astro new file mode 100644 index 000000000000..f7f70dc887f0 --- /dev/null +++ b/packages/astro/test/fixtures/dev-middleware/src/pages/rewrite.astro @@ -0,0 +1,9 @@ + + + Testing + + + +

Rewrite

+ + diff --git a/packages/astro/test/middleware.test.js b/packages/astro/test/middleware.test.js index 36671d1cbc88..06c3896c75aa 100644 --- a/packages/astro/test/middleware.test.js +++ b/packages/astro/test/middleware.test.js @@ -44,5 +44,19 @@ describe('Middleware API', () => { let $ = cheerio.load(html); expect($('p').html()).to.equal('second'); }); + + it('should successfully redirect to another page', async () => { + let html = await fixture.fetch('/redirect').then((res) => res.text()); + let $ = cheerio.load(html); + expect($('p').html()).to.equal('bar'); + expect($('span').html()).to.equal('Index'); + }); + + 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!!'); + }); }); }); From 95235899c1bb047f37554fb4eaae8e5b4beca2e4 Mon Sep 17 00:00:00 2001 From: Emanuele Stoppa Date: Thu, 13 Apr 2023 14:34:14 +0100 Subject: [PATCH 12/53] fix: returning a new `Response` should work --- packages/astro/src/core/endpoint/index.ts | 23 +++++++++++++++---- packages/astro/src/core/errors/errors-data.ts | 8 +++---- .../src/core/middleware/callMiddleware.ts | 9 ++++++-- .../fixtures/dev-middleware/src/middleware.js | 4 ---- 4 files changed, 29 insertions(+), 15 deletions(-) diff --git a/packages/astro/src/core/endpoint/index.ts b/packages/astro/src/core/endpoint/index.ts index 9fd683524f3e..d1465a7828b7 100644 --- a/packages/astro/src/core/endpoint/index.ts +++ b/packages/astro/src/core/endpoint/index.ts @@ -46,7 +46,7 @@ export function createAPIContext({ props: Record; adapterName?: string; }): APIContext { - return { + const context = { cookies: new AstroCookies(request), request, params, @@ -62,7 +62,6 @@ export function createAPIContext({ }); }, url: new URL(request.url), - // @ts-expect-error get clientAddress() { if (!(clientAddressSymbol in request)) { if (adapterName) { @@ -77,8 +76,22 @@ export function createAPIContext({ return Reflect.get(request, clientAddressSymbol); }, - locals: Reflect.get(request, clientLocalsSymbol), - }; + } as APIContext; + + // We define a custom property, so we can correctly 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( @@ -121,7 +134,7 @@ export async function call( const onRequest = middleware.onRequest as MiddlewareEndpointHandler; response = await callMiddleware(onRequest, context, async () => { const result = await renderEndpoint(mod, context, env.ssr); - if (!isValueSerializable(context.locals)) { + if (env.mode === 'development' && !isValueSerializable(context.locals)) { throw new AstroError({ ...AstroErrorData.LocalsNotSerializable, message: AstroErrorData.LocalsNotSerializable.message(ctx.pathname), diff --git a/packages/astro/src/core/errors/errors-data.ts b/packages/astro/src/core/errors/errors-data.ts index 37bab8616ef3..2467f51c3bf0 100644 --- a/packages/astro/src/core/errors/errors-data.ts +++ b/packages/astro/src/core/errors/errors-data.ts @@ -632,11 +632,11 @@ See https://docs.astro.build/en/guides/server-side-rendering/ for more informati /** * TODO [PLT-101] documentation */ - LocalsNotAvailable: { - title: '`Astro.locals` is not available in current adapter.', + LocalsNotAnObject: { + title: 'Value assigned to `locals` is not accepted.', code: 3030, - message: (adapterName: string) => - `\`Astro.locals\` is not available in the "${adapterName}" adapter. File an issue with the adapter to add support.`, + message: `The \`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`.', }, /** diff --git a/packages/astro/src/core/middleware/callMiddleware.ts b/packages/astro/src/core/middleware/callMiddleware.ts index 69215d7b49b1..d364819831b2 100644 --- a/packages/astro/src/core/middleware/callMiddleware.ts +++ b/packages/astro/src/core/middleware/callMiddleware.ts @@ -64,8 +64,13 @@ export async function callMiddleware( await resolveResolve(responseResult); return middlewarePromise; } else { - // Middleware did not call resolve() - return await responseFunction(); + if (typeof value === 'undefined') { + // Middleware didn't return anything, so we call the function that should return the response + return responseFunction(); + } else { + // Middleware did not call resolve and returned a value + return value as R; + } } }); } diff --git a/packages/astro/test/fixtures/dev-middleware/src/middleware.js b/packages/astro/test/fixtures/dev-middleware/src/middleware.js index 1fde2f9f2c49..bb43f3073dee 100644 --- a/packages/astro/test/fixtures/dev-middleware/src/middleware.js +++ b/packages/astro/test/fixtures/dev-middleware/src/middleware.js @@ -5,10 +5,8 @@ const first = async (context, next) => { if (context.request.url.endsWith('/lorem')) { context.locals.name = 'ipsum'; } else if (context.request.url.endsWith('/rewrite')) { - const response = await next(); return new Response('New content!!', { status: 200, - headers: response.headers, }); } else { context.locals.name = 'bar'; @@ -21,8 +19,6 @@ const second = async (context, next) => { if (context.request.url.endsWith('/second')) { context.locals.name = 'second'; } else if (context.request.url.endsWith('/redirect')) { - // before a redirect, we need to resolve the response - await next(); return context.redirect('/', 302); } return await next(); From 97786ed362b9617c4ead3a6e44057974c8efeede Mon Sep 17 00:00:00 2001 From: Emanuele Stoppa Date: Tue, 18 Apr 2023 15:20:16 +0200 Subject: [PATCH 13/53] fix: use correct paths and experimental setting --- packages/astro/middleware.d.ts | 27 ++++++++++++++++++- packages/astro/src/@types/astro.ts | 20 ++++++++++++++ .../src/core/build/plugins/plugin-pages.ts | 7 ++++- packages/astro/src/core/config/schema.ts | 2 ++ packages/astro/src/core/middleware/index.ts | 1 - .../src/core/middleware/loadMiddleware.ts | 10 +++++-- .../src/vite-plugin-astro-server/route.ts | 8 +++--- 7 files changed, 67 insertions(+), 8 deletions(-) diff --git a/packages/astro/middleware.d.ts b/packages/astro/middleware.d.ts index 45072ea670cb..4380aa0d2ac7 100644 --- a/packages/astro/middleware.d.ts +++ b/packages/astro/middleware.d.ts @@ -1,3 +1,28 @@ +import type { APIContext, MiddlewareNext } from './src/@types/astro'; + type MiddlewareResponseHandler = import('./dist/@types/astro.js').MiddlewareResponseHandler; -type Sequence = (...handlers: MiddlewareResponseHandler[]) => MiddlewareResponseHandler; +/** + * Utility function to join multiple middleware functions together + */ +export declare function sequence( + ...handlers: MiddlewareResponseHandler[] +): MiddlewareResponseHandler; + +/** + * Utility function to quickly type a middleware + * + * ## Example + * + * ```ts + * import {defineMiddleware} from "astro/middleware" + * + * const onRequest = defineMiddleware(async (context, next) => { + * + * return await next(); + * }) + * ``` + */ +export declare function defineMiddleware( + fn: (context: APIContext, next: MiddlewareNext) => Promise +): (context: APIContext, next: MiddlewareNext) => Promise; diff --git a/packages/astro/src/@types/astro.ts b/packages/astro/src/@types/astro.ts index fc69a89e3392..540f9fc4e037 100644 --- a/packages/astro/src/@types/astro.ts +++ b/packages/astro/src/@types/astro.ts @@ -1017,6 +1017,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 diff --git a/packages/astro/src/core/build/plugins/plugin-pages.ts b/packages/astro/src/core/build/plugins/plugin-pages.ts index ece0bbc6e734..4175c12629bd 100644 --- a/packages/astro/src/core/build/plugins/plugin-pages.ts +++ b/packages/astro/src/core/build/plugins/plugin-pages.ts @@ -24,7 +24,12 @@ export function vitePluginPages(opts: StaticBuildOptions, internals: BuildIntern async load(id) { if (id === resolvedPagesVirtualModuleId) { - const middlewareId = await this.resolve(`./src/${MIDDLEWARE_PATH_SEGMENT_NAME}`); + let middlewareId = null; + if (opts.settings.config.experimental.middleware) { + middlewareId = await this.resolve( + `./${opts.settings.config.srcDir}/${MIDDLEWARE_PATH_SEGMENT_NAME}` + ); + } let importMap = ''; let imports = []; diff --git a/packages/astro/src/core/config/schema.ts b/packages/astro/src/core/config/schema.ts index 4c55dc5b63a2..f316a799b209 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, }, }; @@ -183,6 +184,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/middleware/index.ts b/packages/astro/src/core/middleware/index.ts index 667cf65e0a2e..d2cbe8b23f60 100644 --- a/packages/astro/src/core/middleware/index.ts +++ b/packages/astro/src/core/middleware/index.ts @@ -1,5 +1,4 @@ import { sequence } from './sequence.js'; import { callMiddleware } from './callMiddleware.js'; import { loadMiddleware } from './loadMiddleware.js'; - export { sequence, callMiddleware, loadMiddleware }; diff --git a/packages/astro/src/core/middleware/loadMiddleware.ts b/packages/astro/src/core/middleware/loadMiddleware.ts index 438c14839cef..451da96dcb59 100644 --- a/packages/astro/src/core/middleware/loadMiddleware.ts +++ b/packages/astro/src/core/middleware/loadMiddleware.ts @@ -1,13 +1,19 @@ import type { ModuleLoader } from '../module-loader'; import { MIDDLEWARE_PATH_SEGMENT_NAME } from '../constants.js'; +import type { AstroSettings } from '../../@types/astro'; +import { fileURLToPath } from 'node:url'; +import { join } from 'node:path'; /** * 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) { - let middlewarePath = 'src/' + MIDDLEWARE_PATH_SEGMENT_NAME; +export async function loadMiddleware( + moduleLoader: ModuleLoader, + srcDir: AstroSettings['config']['srcDir'] +) { + let middlewarePath = join(fileURLToPath(srcDir), MIDDLEWARE_PATH_SEGMENT_NAME); try { const module = await moduleLoader.import(middlewarePath); return module; diff --git a/packages/astro/src/vite-plugin-astro-server/route.ts b/packages/astro/src/vite-plugin-astro-server/route.ts index b95aeb1554c8..94cd6b3268a2 100644 --- a/packages/astro/src/vite-plugin-astro-server/route.ts +++ b/packages/astro/src/vite-plugin-astro-server/route.ts @@ -169,9 +169,11 @@ export async function handleRoute( request, route, }; - const middleware = await loadMiddleware(env.loader); - if (middleware) { - options.middleware = middleware; + 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') { From 6a445f33ec9e3351589ee802d167c23aa168fc84 Mon Sep 17 00:00:00 2001 From: Emanuele Stoppa Date: Tue, 18 Apr 2023 15:52:29 +0200 Subject: [PATCH 14/53] fix: types --- packages/astro/middleware.d.ts | 12 +++--------- packages/astro/middleware.mjs | 2 +- packages/astro/src/core/middleware/index.ts | 8 +++++++- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/packages/astro/middleware.d.ts b/packages/astro/middleware.d.ts index 4380aa0d2ac7..278d02da96c8 100644 --- a/packages/astro/middleware.d.ts +++ b/packages/astro/middleware.d.ts @@ -1,13 +1,9 @@ -import type { APIContext, MiddlewareNext } from './src/@types/astro'; - -type MiddlewareResponseHandler = import('./dist/@types/astro.js').MiddlewareResponseHandler; +import type { APIContext, MiddlewareNext, MiddlewareResponseHandler } from './src/@types/astro'; /** * Utility function to join multiple middleware functions together */ -export declare function sequence( - ...handlers: MiddlewareResponseHandler[] -): MiddlewareResponseHandler; +export function sequence(...handlers: MiddlewareResponseHandler[]): MiddlewareResponseHandler; /** * Utility function to quickly type a middleware @@ -23,6 +19,4 @@ export declare function sequence( * }) * ``` */ -export declare function defineMiddleware( - fn: (context: APIContext, next: MiddlewareNext) => Promise -): (context: APIContext, next: MiddlewareNext) => Promise; +export function defineMiddleware(fn: MiddlewareResponseHandler): MiddlewareResponseHandler; diff --git a/packages/astro/middleware.mjs b/packages/astro/middleware.mjs index 0470ba892ff2..c7c873763c8b 100644 --- a/packages/astro/middleware.mjs +++ b/packages/astro/middleware.mjs @@ -1 +1 @@ -export { sequence } from './dist/core/middleware/index.js'; +export { sequence, defineMiddleware } from './dist/core/middleware/index.js'; diff --git a/packages/astro/src/core/middleware/index.ts b/packages/astro/src/core/middleware/index.ts index d2cbe8b23f60..ed063eb4d7ec 100644 --- a/packages/astro/src/core/middleware/index.ts +++ b/packages/astro/src/core/middleware/index.ts @@ -1,4 +1,10 @@ import { sequence } from './sequence.js'; import { callMiddleware } from './callMiddleware.js'; import { loadMiddleware } from './loadMiddleware.js'; -export { sequence, callMiddleware, loadMiddleware }; +import type { MiddlewareResponseHandler } from '../../@types/astro'; + +function defineMiddleware(fn: MiddlewareResponseHandler) { + return fn; +} + +export { sequence, callMiddleware, loadMiddleware, defineMiddleware }; From dcc013ca90e4ed5ce62cade9f98bf8c82dffe950 Mon Sep 17 00:00:00 2001 From: Emanuele Stoppa Date: Tue, 25 Apr 2023 10:17:05 +0100 Subject: [PATCH 15/53] chore: change middleware specs --- packages/astro/src/core/errors/errors-data.ts | 13 +++- .../src/core/middleware/callMiddleware.ts | 59 +++++++++++-------- 2 files changed, 47 insertions(+), 25 deletions(-) diff --git a/packages/astro/src/core/errors/errors-data.ts b/packages/astro/src/core/errors/errors-data.ts index 2467f51c3bf0..b2c5e5950065 100644 --- a/packages/astro/src/core/errors/errors-data.ts +++ b/packages/astro/src/core/errors/errors-data.ts @@ -629,12 +629,21 @@ See https://docs.astro.build/en/guides/server-side-rendering/ for more informati message: 'The response has already been sent to the browser and cannot be altered.', }, + /** + * TODO [PLT-101] documentation + */ + MiddlewareNoDataReturned: { + title: "The middleware didn't return a response or called `next`", + code: 3031, + message: 'The middleware needs to return a `Response` object or call the `next` function.', + }, + /** * TODO [PLT-101] documentation */ LocalsNotAnObject: { title: 'Value assigned to `locals` is not accepted.', - code: 3030, + code: 3032, message: `The \`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`.', }, @@ -644,7 +653,7 @@ See https://docs.astro.build/en/guides/server-side-rendering/ for more informati */ LocalsNotSerializable: { title: '`Astro.locals` are not serializable.', - code: 3031, + code: 3033, message: (href: string) => { return `The information stored in \`Astro.locals\` are not serializable when visiting "${href}" path.\nMake sure you store only data that are serializable.`; }, diff --git a/packages/astro/src/core/middleware/callMiddleware.ts b/packages/astro/src/core/middleware/callMiddleware.ts index d364819831b2..2135a249ef51 100644 --- a/packages/astro/src/core/middleware/callMiddleware.ts +++ b/packages/astro/src/core/middleware/callMiddleware.ts @@ -1,4 +1,5 @@ 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 @@ -43,34 +44,46 @@ export async function callMiddleware( resolveResolve = resolve; }); - let resolveCalledResolve: any; - let resolveCalled = new Promise((resolve) => { - resolveCalledResolve = resolve; - }); - const resolve: MiddlewareNext = () => { - const response = responseFunction(); - resolveCalledResolve('resolveCalled'); - return response; + let nextCalled = false; + const next: MiddlewareNext = async () => { + nextCalled = true; + return await responseFunction(); }; - let middlewarePromise = onRequest(apiContext, resolve); + let middlewarePromise = onRequest(apiContext, next); - return await Promise.race([middlewarePromise, resolveCalled]).then(async (value) => { - if (value === 'resolveCalled') { - // Middleware called resolve() - // render the page and then pass back to middleware - // for post-processing - const responseResult = await responseFunction(); - await resolveResolve(responseResult); - return middlewarePromise; - } else { - if (typeof value === 'undefined') { - // Middleware didn't return anything, so we call the function that should return the response - return responseFunction(); - } else { - // Middleware did not call resolve and returned a value + 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') { 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.MiddlewareNoDataReturned); + } else { + // Middleware did not call resolve and returned a value + return value as R; } }); } From f8624036b19423aeb5237ab5ad53ee086b46b15a Mon Sep 17 00:00:00 2001 From: Emanuele Stoppa Date: Tue, 25 Apr 2023 10:58:06 +0100 Subject: [PATCH 16/53] test: new cases and added experimental flag --- .../fixtures/dev-middleware/astro.config.mjs | 5 +++- .../fixtures/dev-middleware/src/middleware.js | 29 +++++++++++++----- .../src/pages/broken-locals.astro | 0 .../dev-middleware/src/pages/broken.astro | 0 .../src/pages/does-nothing.astro | 9 ++++++ .../src/pages/not-interested.astro | 9 ++++++ packages/astro/test/middleware.test.js | 30 ++++++++++++++++++- 7 files changed, 72 insertions(+), 10 deletions(-) create mode 100644 packages/astro/test/fixtures/dev-middleware/src/pages/broken-locals.astro create mode 100644 packages/astro/test/fixtures/dev-middleware/src/pages/broken.astro create mode 100644 packages/astro/test/fixtures/dev-middleware/src/pages/does-nothing.astro create mode 100644 packages/astro/test/fixtures/dev-middleware/src/pages/not-interested.astro diff --git a/packages/astro/test/fixtures/dev-middleware/astro.config.mjs b/packages/astro/test/fixtures/dev-middleware/astro.config.mjs index 682dab75277c..1523fb4be31e 100644 --- a/packages/astro/test/fixtures/dev-middleware/astro.config.mjs +++ b/packages/astro/test/fixtures/dev-middleware/astro.config.mjs @@ -5,5 +5,8 @@ export default defineConfig({ output: "server", adapter: node({ mode: "standalone" - }) + }), + experimental: { + middleware: true + } }); diff --git a/packages/astro/test/fixtures/dev-middleware/src/middleware.js b/packages/astro/test/fixtures/dev-middleware/src/middleware.js index bb43f3073dee..5e717c1b4c5d 100644 --- a/packages/astro/test/fixtures/dev-middleware/src/middleware.js +++ b/packages/astro/test/fixtures/dev-middleware/src/middleware.js @@ -1,27 +1,40 @@ -import { sequence } from 'astro/middleware'; +import { sequence, defineMiddleware } from 'astro/middleware'; -/** @type import("astro").MiddlewareResponseHandler */ -const first = async (context, next) => { +const first = defineMiddleware(async (context, next) => { if (context.request.url.endsWith('/lorem')) { context.locals.name = 'ipsum'; } else if (context.request.url.endsWith('/rewrite')) { return new Response('New content!!', { status: 200, }); + } else if (context.request.url.endsWith('/broken')) { + return new Response(null, { + status: 500, + }); } else { context.locals.name = 'bar'; } return await next(); -}; +}); -/** @type import("astro").MiddlewareResponseHandler */ -const second = async (context, next) => { +const second = defineMiddleware(async (context, next) => { if (context.request.url.endsWith('/second')) { context.locals.name = 'second'; } else if (context.request.url.endsWith('/redirect')) { return context.redirect('/', 302); } return await next(); -}; +}); + +const third = defineMiddleware(async (context, next) => { + if (context.request.url.endsWith('/broken-locals')) { + context.locals = { + fn() {}, + }; + } else if (context.request.url.endsWith('/does-nothing')) { + return undefined; + } + next(); +}); -export const onRequest = sequence(first, second); +export const onRequest = sequence(first, second, third); diff --git a/packages/astro/test/fixtures/dev-middleware/src/pages/broken-locals.astro b/packages/astro/test/fixtures/dev-middleware/src/pages/broken-locals.astro new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/packages/astro/test/fixtures/dev-middleware/src/pages/broken.astro b/packages/astro/test/fixtures/dev-middleware/src/pages/broken.astro new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/packages/astro/test/fixtures/dev-middleware/src/pages/does-nothing.astro b/packages/astro/test/fixtures/dev-middleware/src/pages/does-nothing.astro new file mode 100644 index 000000000000..344b3797bfee --- /dev/null +++ b/packages/astro/test/fixtures/dev-middleware/src/pages/does-nothing.astro @@ -0,0 +1,9 @@ + + + Testing + + + +

Not interested

+ + diff --git a/packages/astro/test/fixtures/dev-middleware/src/pages/not-interested.astro b/packages/astro/test/fixtures/dev-middleware/src/pages/not-interested.astro new file mode 100644 index 000000000000..344b3797bfee --- /dev/null +++ b/packages/astro/test/fixtures/dev-middleware/src/pages/not-interested.astro @@ -0,0 +1,9 @@ + + + Testing + + + +

Not interested

+ + diff --git a/packages/astro/test/middleware.test.js b/packages/astro/test/middleware.test.js index 06c3896c75aa..588c6f77e381 100644 --- a/packages/astro/test/middleware.test.js +++ b/packages/astro/test/middleware.test.js @@ -46,7 +46,10 @@ describe('Middleware API', () => { }); it('should successfully redirect to another page', async () => { - let html = await fixture.fetch('/redirect').then((res) => res.text()); + let html = await fixture.fetch('/redirect').then((res) => { + expect(res.status).to.equal(200); + return res.text(); + }); let $ = cheerio.load(html); expect($('p').html()).to.equal('bar'); expect($('span').html()).to.equal('Index'); @@ -58,5 +61,30 @@ describe('Middleware API', () => { 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').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('MiddlewareNoDataReturned'); + }); }); }); From f471f2d8ae79ba756cde35e09a165005fc9a1c75 Mon Sep 17 00:00:00 2001 From: Emanuele Stoppa Date: Tue, 25 Apr 2023 11:08:26 +0100 Subject: [PATCH 17/53] fix: update bundled code --- packages/astro/src/core/build/plugins/plugin-pages.ts | 4 +++- packages/astro/src/core/build/plugins/plugin-ssr.ts | 5 +++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/astro/src/core/build/plugins/plugin-pages.ts b/packages/astro/src/core/build/plugins/plugin-pages.ts index 4175c12629bd..e2aa3e7ddfdd 100644 --- a/packages/astro/src/core/build/plugins/plugin-pages.ts +++ b/packages/astro/src/core/build/plugins/plugin-pages.ts @@ -58,7 +58,9 @@ ${middlewareId ? `import * as _middleware from "${middlewareId.id}";` : ''} export const pageMap = new Map([${importMap}]); export const renderers = [${rendererItems}]; -export const middleware = ${middlewareId ? '_middleware' : '{ onRequest() {} }'}; +export const middleware = ${ + middlewareId ? '_middleware' : '{ onRequest(_, next) {return next()} }' + }; `; 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 a0fb67cd98ff..36e8c816ba30 100644 --- a/packages/astro/src/core/build/plugins/plugin-ssr.ts +++ b/packages/astro/src/core/build/plugins/plugin-ssr.ts @@ -223,8 +223,9 @@ function buildManifest( entryModules, assets: staticFiles.map(prefixAssetPath), middleware: { - // @ts-expect-error - onRequest() {}, + onRequest(_, next) { + return next(); + }, }, }; From 7b69bb1cfca33508c43903fa3c1c9c64da29be4d Mon Sep 17 00:00:00 2001 From: Emanuele Stoppa Date: Tue, 25 Apr 2023 12:35:27 +0100 Subject: [PATCH 18/53] fix: node import --- packages/astro/src/core/middleware/loadMiddleware.ts | 4 ++-- packages/astro/src/vite-plugin-astro-postprocess/index.ts | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/astro/src/core/middleware/loadMiddleware.ts b/packages/astro/src/core/middleware/loadMiddleware.ts index 451da96dcb59..bf39f8a1d06b 100644 --- a/packages/astro/src/core/middleware/loadMiddleware.ts +++ b/packages/astro/src/core/middleware/loadMiddleware.ts @@ -1,8 +1,8 @@ import type { ModuleLoader } from '../module-loader'; import { MIDDLEWARE_PATH_SEGMENT_NAME } from '../constants.js'; import type { AstroSettings } from '../../@types/astro'; -import { fileURLToPath } from 'node:url'; -import { join } from 'node:path'; +import { fileURLToPath } from 'url'; +import { join } from 'path'; /** * It accepts a module loader and the astro settings, and it attempts to load the middlewares defined in the configuration. diff --git a/packages/astro/src/vite-plugin-astro-postprocess/index.ts b/packages/astro/src/vite-plugin-astro-postprocess/index.ts index 05895ab627c7..857169aae07c 100644 --- a/packages/astro/src/vite-plugin-astro-postprocess/index.ts +++ b/packages/astro/src/vite-plugin-astro-postprocess/index.ts @@ -33,6 +33,7 @@ export default function astro(_opts: AstroPluginOptions): Plugin { sourceType: 'module', }); + // @ts-expect-error walk(ast, { enter(node: any) { // Transform `Astro.glob("./pages/*.astro")` to `Astro.glob(import.meta.glob("./pages/*.astro"), () => "./pages/*.astro")` From bf16a3d6a7ad3f1117c022e3cada08e7a0b0dec8 Mon Sep 17 00:00:00 2001 From: Emanuele Stoppa Date: Tue, 25 Apr 2023 14:18:19 +0100 Subject: [PATCH 19/53] chore: more tests --- .../src/core/build/plugins/plugin-pages.ts | 2 +- packages/astro/src/core/render/core.ts | 2 +- .../astro.config.mjs | 0 .../package.json | 0 .../src/middleware.js | 0 .../src/pages/broken-locals.astro | 0 .../src/pages/broken.astro | 0 .../src/pages/does-nothing.astro | 0 .../src/pages/index.astro | 0 .../src/pages/lorem.astro | 0 .../src/pages/not-interested.astro | 0 .../src/pages/redirect.astro | 0 .../src/pages/rewrite.astro | 0 .../src/pages/second.astro | 0 packages/astro/test/middleware.test.js | 98 +++++++++++++++++-- 15 files changed, 91 insertions(+), 11 deletions(-) rename packages/astro/test/fixtures/{dev-middleware => middleware}/astro.config.mjs (100%) rename packages/astro/test/fixtures/{dev-middleware => middleware}/package.json (100%) rename packages/astro/test/fixtures/{dev-middleware => middleware}/src/middleware.js (100%) rename packages/astro/test/fixtures/{dev-middleware => middleware}/src/pages/broken-locals.astro (100%) rename packages/astro/test/fixtures/{dev-middleware => middleware}/src/pages/broken.astro (100%) rename packages/astro/test/fixtures/{dev-middleware => middleware}/src/pages/does-nothing.astro (100%) rename packages/astro/test/fixtures/{dev-middleware => middleware}/src/pages/index.astro (100%) rename packages/astro/test/fixtures/{dev-middleware => middleware}/src/pages/lorem.astro (100%) rename packages/astro/test/fixtures/{dev-middleware => middleware}/src/pages/not-interested.astro (100%) rename packages/astro/test/fixtures/{dev-middleware => middleware}/src/pages/redirect.astro (100%) rename packages/astro/test/fixtures/{dev-middleware => middleware}/src/pages/rewrite.astro (100%) rename packages/astro/test/fixtures/{dev-middleware => middleware}/src/pages/second.astro (100%) diff --git a/packages/astro/src/core/build/plugins/plugin-pages.ts b/packages/astro/src/core/build/plugins/plugin-pages.ts index e2aa3e7ddfdd..99e6d986ab00 100644 --- a/packages/astro/src/core/build/plugins/plugin-pages.ts +++ b/packages/astro/src/core/build/plugins/plugin-pages.ts @@ -27,7 +27,7 @@ export function vitePluginPages(opts: StaticBuildOptions, internals: BuildIntern let middlewareId = null; if (opts.settings.config.experimental.middleware) { middlewareId = await this.resolve( - `./${opts.settings.config.srcDir}/${MIDDLEWARE_PATH_SEGMENT_NAME}` + `${opts.settings.config.srcDir.pathname}/${MIDDLEWARE_PATH_SEGMENT_NAME}` ); } diff --git a/packages/astro/src/core/render/core.ts b/packages/astro/src/core/render/core.ts index 3732d553f2ce..2a1e2d6c3564 100644 --- a/packages/astro/src/core/render/core.ts +++ b/packages/astro/src/core/render/core.ts @@ -116,7 +116,7 @@ export async function renderPage( let locals = {}; if (apiContext) { - if (!isValueSerializable(apiContext.locals)) { + if (env.mode === 'development' && !isValueSerializable(apiContext.locals)) { throw new AstroError({ ...AstroErrorData.LocalsNotSerializable, message: AstroErrorData.LocalsNotSerializable.message(ctx.pathname), diff --git a/packages/astro/test/fixtures/dev-middleware/astro.config.mjs b/packages/astro/test/fixtures/middleware/astro.config.mjs similarity index 100% rename from packages/astro/test/fixtures/dev-middleware/astro.config.mjs rename to packages/astro/test/fixtures/middleware/astro.config.mjs diff --git a/packages/astro/test/fixtures/dev-middleware/package.json b/packages/astro/test/fixtures/middleware/package.json similarity index 100% rename from packages/astro/test/fixtures/dev-middleware/package.json rename to packages/astro/test/fixtures/middleware/package.json diff --git a/packages/astro/test/fixtures/dev-middleware/src/middleware.js b/packages/astro/test/fixtures/middleware/src/middleware.js similarity index 100% rename from packages/astro/test/fixtures/dev-middleware/src/middleware.js rename to packages/astro/test/fixtures/middleware/src/middleware.js diff --git a/packages/astro/test/fixtures/dev-middleware/src/pages/broken-locals.astro b/packages/astro/test/fixtures/middleware/src/pages/broken-locals.astro similarity index 100% rename from packages/astro/test/fixtures/dev-middleware/src/pages/broken-locals.astro rename to packages/astro/test/fixtures/middleware/src/pages/broken-locals.astro diff --git a/packages/astro/test/fixtures/dev-middleware/src/pages/broken.astro b/packages/astro/test/fixtures/middleware/src/pages/broken.astro similarity index 100% rename from packages/astro/test/fixtures/dev-middleware/src/pages/broken.astro rename to packages/astro/test/fixtures/middleware/src/pages/broken.astro diff --git a/packages/astro/test/fixtures/dev-middleware/src/pages/does-nothing.astro b/packages/astro/test/fixtures/middleware/src/pages/does-nothing.astro similarity index 100% rename from packages/astro/test/fixtures/dev-middleware/src/pages/does-nothing.astro rename to packages/astro/test/fixtures/middleware/src/pages/does-nothing.astro diff --git a/packages/astro/test/fixtures/dev-middleware/src/pages/index.astro b/packages/astro/test/fixtures/middleware/src/pages/index.astro similarity index 100% rename from packages/astro/test/fixtures/dev-middleware/src/pages/index.astro rename to packages/astro/test/fixtures/middleware/src/pages/index.astro diff --git a/packages/astro/test/fixtures/dev-middleware/src/pages/lorem.astro b/packages/astro/test/fixtures/middleware/src/pages/lorem.astro similarity index 100% rename from packages/astro/test/fixtures/dev-middleware/src/pages/lorem.astro rename to packages/astro/test/fixtures/middleware/src/pages/lorem.astro diff --git a/packages/astro/test/fixtures/dev-middleware/src/pages/not-interested.astro b/packages/astro/test/fixtures/middleware/src/pages/not-interested.astro similarity index 100% rename from packages/astro/test/fixtures/dev-middleware/src/pages/not-interested.astro rename to packages/astro/test/fixtures/middleware/src/pages/not-interested.astro diff --git a/packages/astro/test/fixtures/dev-middleware/src/pages/redirect.astro b/packages/astro/test/fixtures/middleware/src/pages/redirect.astro similarity index 100% rename from packages/astro/test/fixtures/dev-middleware/src/pages/redirect.astro rename to packages/astro/test/fixtures/middleware/src/pages/redirect.astro diff --git a/packages/astro/test/fixtures/dev-middleware/src/pages/rewrite.astro b/packages/astro/test/fixtures/middleware/src/pages/rewrite.astro similarity index 100% rename from packages/astro/test/fixtures/dev-middleware/src/pages/rewrite.astro rename to packages/astro/test/fixtures/middleware/src/pages/rewrite.astro diff --git a/packages/astro/test/fixtures/dev-middleware/src/pages/second.astro b/packages/astro/test/fixtures/middleware/src/pages/second.astro similarity index 100% rename from packages/astro/test/fixtures/dev-middleware/src/pages/second.astro rename to packages/astro/test/fixtures/middleware/src/pages/second.astro diff --git a/packages/astro/test/middleware.test.js b/packages/astro/test/middleware.test.js index 588c6f77e381..53a32002b702 100644 --- a/packages/astro/test/middleware.test.js +++ b/packages/astro/test/middleware.test.js @@ -1,21 +1,18 @@ import { loadFixture } from './test-utils.js'; import { expect } from 'chai'; import * as cheerio from 'cheerio'; +import testAdapter from './test-adapter.js'; describe('Middleware API', () => { - /** @type {import('./test-utils').Fixture} */ - let fixture; - - before(async () => { - fixture = await loadFixture({ - root: './fixtures/dev-middleware/', - }); - }); - describe('in DEV mode', () => { + /** @type {import('./test-utils').Fixture} */ + let fixture; let devServer; before(async () => { + fixture = await loadFixture({ + root: './fixtures/middleware/', + }); devServer = await fixture.startDevServer(); }); @@ -87,4 +84,87 @@ describe('Middleware API', () => { expect($('title').html()).to.equal('MiddlewareNoDataReturned'); }); }); + + describe('in PROD mode', () => { + /** @type {import('./test-utils').Fixture} */ + let fixture; + let previewServer; + + before(async () => { + fixture = await loadFixture({ + root: './fixtures/middleware/', + }); + await fixture.build(); + previewServer = await fixture.preview(); + }); + + // important: close preview server (free up port and connection) + after(async () => { + await previewServer.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 redirect to another page', async () => { + let html = await fixture.fetch('/redirect').then((res) => { + expect(res.status).to.equal(200); + return res.text(); + }); + let $ = cheerio.load(html); + expect($('p').html()).to.equal('bar'); + expect($('span').html()).to.equal('Index'); + }); + + 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').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 NOT 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.not.equal('LocalsNotSerializable'); + }); + + it("should throws 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.not.equal('MiddlewareNoDataReturned'); + }); + }); }); From 6f1a04ec903c86f264a0bc6ec422ed4518ea735f Mon Sep 17 00:00:00 2001 From: Emanuele Stoppa Date: Tue, 25 Apr 2023 14:42:49 +0100 Subject: [PATCH 20/53] chore: do not use Node.js builtins --- packages/astro/src/core/errors/errors-data.ts | 70 +++++++++++++++++-- .../src/core/middleware/callMiddleware.ts | 7 +- .../src/core/middleware/loadMiddleware.ts | 5 +- 3 files changed, 72 insertions(+), 10 deletions(-) diff --git a/packages/astro/src/core/errors/errors-data.ts b/packages/astro/src/core/errors/errors-data.ts index b2c5e5950065..ca09b7f595b1 100644 --- a/packages/astro/src/core/errors/errors-data.ts +++ b/packages/astro/src/core/errors/errors-data.ts @@ -630,30 +630,88 @@ See https://docs.astro.build/en/guides/server-side-rendering/ for more informati }, /** - * TODO [PLT-101] documentation + * @docs + * @description + * Thrown when a middleware doesn't return anything or doesn't call the `next` function. + * + * For example: + * ```ts + * import {defineMiddleware} from "astro/middleware"; + * export const onRequest = defineMiddleware((context, _) => { + * // doesn't return anything or doesn't call next + * context.locals.someData = false; + * }); + * ``` */ - MiddlewareNoDataReturned: { + MiddlewareNoDataOrNextCalled: { title: "The middleware didn't return a response or called `next`", code: 3031, message: 'The middleware needs to return a `Response` object or call the `next` function.', }, /** - * TODO [PLT-101] documentation + * @docs + * @description + * Thrown in development mode, when a middleware return something that is not a `Response` + * + * 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`', + code: 3032, + message: + 'When returning something from a middleware, you must return a valid `Response`. Failing doing so would cause errors..', + }, + + /** + * @docs + * @description + * + * Thrown in development mode, when `locals` are overridden 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: 3032, + code: 3033, message: `The \`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`.', }, /** - * TODO [PLT-101] documentation + * @docs + * @description + * Thrown in development mode, when a user attempts to store in `local` something that is not serializable. + * + * 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` are not serializable.', - code: 3033, + code: 3034, message: (href: string) => { return `The information stored in \`Astro.locals\` are not serializable when visiting "${href}" path.\nMake sure you store only data that are serializable.`; }, diff --git a/packages/astro/src/core/middleware/callMiddleware.ts b/packages/astro/src/core/middleware/callMiddleware.ts index 2135a249ef51..4c56c26e6707 100644 --- a/packages/astro/src/core/middleware/callMiddleware.ts +++ b/packages/astro/src/core/middleware/callMiddleware.ts @@ -65,6 +65,9 @@ export async function callMiddleware( * ``` */ if (typeof value !== 'undefined') { + if (value instanceof Response == false) { + throw new AstroError(AstroErrorData.MiddlewareNotAResponse); + } return value as R; } else { /** @@ -80,7 +83,9 @@ export async function callMiddleware( * * If not thing is returned, then we raise an Astro error. */ - throw new AstroError(AstroErrorData.MiddlewareNoDataReturned); + 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; diff --git a/packages/astro/src/core/middleware/loadMiddleware.ts b/packages/astro/src/core/middleware/loadMiddleware.ts index bf39f8a1d06b..62dea70cdece 100644 --- a/packages/astro/src/core/middleware/loadMiddleware.ts +++ b/packages/astro/src/core/middleware/loadMiddleware.ts @@ -1,8 +1,6 @@ import type { ModuleLoader } from '../module-loader'; import { MIDDLEWARE_PATH_SEGMENT_NAME } from '../constants.js'; import type { AstroSettings } from '../../@types/astro'; -import { fileURLToPath } from 'url'; -import { join } from 'path'; /** * It accepts a module loader and the astro settings, and it attempts to load the middlewares defined in the configuration. @@ -13,7 +11,8 @@ export async function loadMiddleware( moduleLoader: ModuleLoader, srcDir: AstroSettings['config']['srcDir'] ) { - let middlewarePath = join(fileURLToPath(srcDir), MIDDLEWARE_PATH_SEGMENT_NAME); + // can't use node Node.js builtins + let middlewarePath = srcDir.pathname + '/' + MIDDLEWARE_PATH_SEGMENT_NAME; try { const module = await moduleLoader.import(middlewarePath); return module; From c821f5f8cfb6ade776cf04e62abce32a1833faba Mon Sep 17 00:00:00 2001 From: Emanuele Stoppa Date: Tue, 25 Apr 2023 16:07:49 +0100 Subject: [PATCH 21/53] examples: middleware --- examples/middleware/astro.config.mjs | 13 ++++ examples/middleware/package.json | 23 ++++++ examples/middleware/src/components/Card.astro | 63 +++++++++++++++ examples/middleware/src/env.d.ts | 8 ++ examples/middleware/src/layouts/Layout.astro | 35 +++++++++ examples/middleware/src/middleware.ts | 71 +++++++++++++++++ examples/middleware/src/pages/admin.astro | 59 ++++++++++++++ examples/middleware/src/pages/api/login.ts | 18 +++++ examples/middleware/src/pages/index.astro | 69 +++++++++++++++++ examples/middleware/src/pages/login.astro | 77 +++++++++++++++++++ packages/astro/src/@types/astro.ts | 5 +- 11 files changed, 440 insertions(+), 1 deletion(-) create mode 100644 examples/middleware/astro.config.mjs create mode 100644 examples/middleware/package.json create mode 100644 examples/middleware/src/components/Card.astro create mode 100644 examples/middleware/src/env.d.ts create mode 100644 examples/middleware/src/layouts/Layout.astro create mode 100644 examples/middleware/src/middleware.ts create mode 100644 examples/middleware/src/pages/admin.astro create mode 100644 examples/middleware/src/pages/api/login.ts create mode 100644 examples/middleware/src/pages/index.astro create mode 100644 examples/middleware/src/pages/login.astro diff --git a/examples/middleware/astro.config.mjs b/examples/middleware/astro.config.mjs new file mode 100644 index 000000000000..ca71a89dcc88 --- /dev/null +++ b/examples/middleware/astro.config.mjs @@ -0,0 +1,13 @@ +import { defineConfig } from 'astro/config'; +import node from '@astrojs/node'; + +// https://astro.build/config +export default defineConfig({ + output: 'server', + adapter: node({ + mode: 'standalone', + }), + experimental: { + middleware: true + } +}); diff --git a/examples/middleware/package.json b/examples/middleware/package.json new file mode 100644 index 000000000000..0a62e221ee27 --- /dev/null +++ b/examples/middleware/package.json @@ -0,0 +1,23 @@ +{ + "name": "@example/middleware", + "type": "module", + "version": "0.0.1", + "private": true, + "scripts": { + "dev": "astro dev", + "start": "astro dev", + "build": "astro build", + "preview": "astro preview", + "astro": "astro", + "server": "node dist/server/entry.mjs" + }, + "dependencies": { + "astro": "workspace:*", + "svelte": "^3.48.0", + "@astrojs/node": "workspace:*", + "concurrently": "^7.2.1", + "unocss": "^0.15.6", + "vite-imagetools": "^4.0.4", + "html-minifier": "^4.0.0" + } +} diff --git a/examples/middleware/src/components/Card.astro b/examples/middleware/src/components/Card.astro new file mode 100644 index 000000000000..c68fa2ab3448 --- /dev/null +++ b/examples/middleware/src/components/Card.astro @@ -0,0 +1,63 @@ +--- +export interface Props { + title: string; + body: string; + href: string; +} + +const { href, title, body } = Astro.props; +--- + + + diff --git a/examples/middleware/src/env.d.ts b/examples/middleware/src/env.d.ts new file mode 100644 index 000000000000..3dbd03a24337 --- /dev/null +++ b/examples/middleware/src/env.d.ts @@ -0,0 +1,8 @@ +/// + +export interface Locals { + user: { + name: string; + surname: string; + }; +} diff --git a/examples/middleware/src/layouts/Layout.astro b/examples/middleware/src/layouts/Layout.astro new file mode 100644 index 000000000000..f1a62a537b7d --- /dev/null +++ b/examples/middleware/src/layouts/Layout.astro @@ -0,0 +1,35 @@ +--- +export interface Props { + title: string; +} + +const { title } = Astro.props; +--- + + + + + + + + + {title} + + + + + + diff --git a/examples/middleware/src/middleware.ts b/examples/middleware/src/middleware.ts new file mode 100644 index 000000000000..1c0bd855f636 --- /dev/null +++ b/examples/middleware/src/middleware.ts @@ -0,0 +1,71 @@ +import { defineMiddleware, sequence } from 'astro/middleware'; +import htmlMinifier from 'html-minifier'; + +const limit = 50; + +const loginInfo = { + token: undefined, + currentTime: undefined, +}; + +export const minifier = defineMiddleware(async (context, next) => { + const response = await next(); + // check if the response is returning some HTML + if (response.headers.get('content-type') === 'text/html') { + let headers = response.headers; + let html = await response.text(); + let newHtml = htmlMinifier.minify(html, { + removeAttributeQuotes: true, + collapseWhitespace: true, + }); + return new Response(newHtml, { + status: 200, + headers, + }); + } + return response; +}); + +const validation = defineMiddleware(async (context, next) => { + if (context.request.url.endsWith('/admin')) { + if (loginInfo.currentTime) { + const difference = new Date().getTime() - loginInfo.currentTime; + if (difference > limit) { + console.log('hit threshold'); + loginInfo.token = undefined; + loginInfo.currentTime = undefined; + return context.redirect('/login'); + } + } + // we naively check if we have a token + if (loginInfo.token && loginInfo.token === 'loggedIn') { + // we fill the locals with user-facing information + context.locals.user = { + name: 'AstroUser', + surname: 'AstroSurname', + }; + return await next(); + } else { + loginInfo.token = undefined; + loginInfo.currentTime = undefined; + return context.redirect('/login'); + } + } else if (context.request.url.endsWith('/api/login')) { + const response = await next(); + // the login endpoint will return to us a JSON with username and password + const data = await response.json(); + // we naively check if username and password are equals to some string + if (data.username === 'astro' && data.password === 'astro') { + // we store the token somewhere outside of locals because the `locals` object is attached to the request + // and when doing a redirect, we lose that information + loginInfo.token = 'loggedIn'; + loginInfo.currentTime = new Date().getTime(); + return context.redirect('/admin'); + } + } + // we don't really care about awaiting the response in this case + next(); + return; +}); + +export const onRequest = sequence(validation, minifier); diff --git a/examples/middleware/src/pages/admin.astro b/examples/middleware/src/pages/admin.astro new file mode 100644 index 000000000000..3a7d0169fb48 --- /dev/null +++ b/examples/middleware/src/pages/admin.astro @@ -0,0 +1,59 @@ +--- +import Card from "../components/Card.astro"; +import Layout from "../layouts/Layout.astro"; +const user = Astro.locals.user; +--- + + + + +
+

Welcome back {user.name} {user.surname}

+ +
+
+ + diff --git a/examples/middleware/src/pages/api/login.ts b/examples/middleware/src/pages/api/login.ts new file mode 100644 index 000000000000..fa3f7b59b28c --- /dev/null +++ b/examples/middleware/src/pages/api/login.ts @@ -0,0 +1,18 @@ +import { APIRoute } from 'astro'; + +export const post: APIRoute = async ({ request }) => { + const data = await request.formData(); + const username = data.get('username'); + const password = data.get('password'); + return new Response( + JSON.stringify({ + username, + password, + }), + { + headers: { + 'content-type': 'application/json', + }, + } + ); +}; diff --git a/examples/middleware/src/pages/index.astro b/examples/middleware/src/pages/index.astro new file mode 100644 index 000000000000..a990caf89d4a --- /dev/null +++ b/examples/middleware/src/pages/index.astro @@ -0,0 +1,69 @@ +--- +import Layout from '../layouts/Layout.astro'; +import Card from '../components/Card.astro'; + +--- + + +
+

Welcome to Astro

+

+ To get started, open the directory src/pages in your project.
+ Code Challenge: Tweak the "Welcome to Astro" message above. +

+ {} + +
+
+ + diff --git a/examples/middleware/src/pages/login.astro b/examples/middleware/src/pages/login.astro new file mode 100644 index 000000000000..9f2a08890386 --- /dev/null +++ b/examples/middleware/src/pages/login.astro @@ -0,0 +1,77 @@ +--- +import Layout from '../layouts/Layout.astro'; +import Card from '../components/Card.astro'; + +const status = Astro.response.status; +let redirectMessage; +if (status === 301) { + redirectMessage = "Your session is finished, please login again" +} +--- + + +
+

Welcome to Astro

+

+ To get started, open the directory src/pages in your project.
+ Code Challenge: Tweak the "Welcome to Astro" message above. +

+ {redirectMessage} +
+ + + + +
+
+
+ + diff --git a/packages/astro/src/@types/astro.ts b/packages/astro/src/@types/astro.ts index 540f9fc4e037..9464606d3e0b 100644 --- a/packages/astro/src/@types/astro.ts +++ b/packages/astro/src/@types/astro.ts @@ -1612,7 +1612,10 @@ export interface AstroIntegration { } export type MiddlewareNext = () => Promise; -export type MiddlewareHandler = (context: APIContext, next: MiddlewareNext) => Promise; +export type MiddlewareHandler = ( + context: APIContext, + next: MiddlewareNext +) => Promise | void | R; export type MiddlewareResponseHandler = MiddlewareHandler; export type MiddlewareEndpointHandler = MiddlewareHandler; From f34c57a1611d3fb2e50468f20b5de7525a1d9d4e Mon Sep 17 00:00:00 2001 From: Emanuele Stoppa Date: Tue, 25 Apr 2023 16:15:16 +0100 Subject: [PATCH 22/53] add `--experimental-middleware` --- packages/astro/src/@types/astro.ts | 1 + packages/astro/src/core/app/index.ts | 3 -- packages/astro/src/core/config/config.ts | 5 ++++ packages/astro/src/core/endpoint/index.ts | 2 +- .../src/core/middleware/callMiddleware.ts | 29 ++++++++++--------- 5 files changed, 22 insertions(+), 18 deletions(-) diff --git a/packages/astro/src/@types/astro.ts b/packages/astro/src/@types/astro.ts index 9464606d3e0b..00eee3bdfa65 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 { diff --git a/packages/astro/src/core/app/index.ts b/packages/astro/src/core/app/index.ts index 8f8c8f48e6bc..39b06536f58c 100644 --- a/packages/astro/src/core/app/index.ts +++ b/packages/astro/src/core/app/index.ts @@ -244,7 +244,6 @@ export class App { const response = await callMiddleware(onRequest, apiContext, () => { return renderPage(mod, renderContext, this.#env, apiContext); }); - // const response = await renderPage(mod, renderContext, this.#env, apiContext); Reflect.set(request, responseSentSymbol, true); return response; } catch (err: any) { @@ -274,8 +273,6 @@ export class App { status, }); - // TODO PLT-104 add adapter middleware here - const result = await callEndpoint( handler, this.#env, diff --git a/packages/astro/src/core/config/config.ts b/packages/astro/src/core/config/config.ts index 9c09a934f566..9915ed162b31 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): 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/endpoint/index.ts b/packages/astro/src/core/endpoint/index.ts index d1465a7828b7..b1683eb01197 100644 --- a/packages/astro/src/core/endpoint/index.ts +++ b/packages/astro/src/core/endpoint/index.ts @@ -78,7 +78,7 @@ export function createAPIContext({ }, } as APIContext; - // We define a custom property, so we can correctly the value passed to locals + // We define a custom property, so we can check the value passed to locals Object.defineProperty(context, 'locals', { get() { return Reflect.get(request, clientLocalsSymbol); diff --git a/packages/astro/src/core/middleware/callMiddleware.ts b/packages/astro/src/core/middleware/callMiddleware.ts index 4c56c26e6707..7cff2fa78510 100644 --- a/packages/astro/src/core/middleware/callMiddleware.ts +++ b/packages/astro/src/core/middleware/callMiddleware.ts @@ -2,31 +2,32 @@ import type { APIContext, MiddlewareHandler, MiddlewareNext } from '../../@types import { AstroError, AstroErrorData } from '../errors/index.js'; /** - * Utility function that is in charge of calling the middleware + * Utility function that is in charge of calling the middleware. * - * It accepts a `R` generic, which usually the `Response` returned. - * It is a generic because endpoints can return a different response. + * 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 `resolve` function, this function might or - * might not be called. Because of that, we use a `Promise.race` to understand which - * promise is resolved first. + * When calling a middleware, we provide a `next` function, this function might or + * might not be called. * - * If `resolve` is called first, we resolve the `responseFunction` and we pass that response - * as resolved value to `resolve`. Finally, we resolve the middleware. - * This logic covers examples like: + * 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, resolve) => { - * const response = await resolve(context); + * const onRequest = async (context, next) => { + * const response = await next(context); * return response; * } * ``` * - * If the middleware is called first, we return the response without fancy logic. This covers cases like: - * * ```js - * const onRequest = async (context, _) => { + * const onRequest = async (context, next) => { * context.locals = "foo"; + * next(); * } * ``` * From a333904233b90c66f7810513bdc03faefebe5e95 Mon Sep 17 00:00:00 2001 From: Emanuele Stoppa Date: Wed, 26 Apr 2023 11:34:12 +0100 Subject: [PATCH 23/53] fix type issue and try `Locals` expand --- packages/astro/src/@types/astro.ts | 24 ++++++------- .../astro/src/core/middleware/sequence.ts | 35 ++++++++++++------- packages/astro/src/core/render/result.ts | 4 +-- 3 files changed, 36 insertions(+), 27 deletions(-) diff --git a/packages/astro/src/@types/astro.ts b/packages/astro/src/@types/astro.ts index 00eee3bdfa65..d685852c63ac 100644 --- a/packages/astro/src/@types/astro.ts +++ b/packages/astro/src/@types/astro.ts @@ -1406,10 +1406,7 @@ export interface AstroAdapter { type Body = string; // Shared types between `Astro` global and API context object -interface AstroSharedContext< - Props extends Record = Record, - Locals extends Record = Record -> { +interface AstroSharedContext = Record> { /** * The address (usually IP address) of the user. Used with SSR only. */ @@ -1442,13 +1439,11 @@ interface AstroSharedContext< /** * TODO documentation */ - locals: Locals; + locals: AstroMiddleware.Locals; } -export interface APIContext< - Props extends Record = Record, - Locals extends Record = Record -> extends AstroSharedContext { +export interface APIContext = Record> + extends AstroSharedContext { site: URL | undefined; generator: string; /** @@ -1503,7 +1498,7 @@ export interface APIContext< * * [context reference](https://docs.astro.build/en/guides/api-reference/#contextprops) */ - props: AstroSharedContext['props']; + props: AstroSharedContext['props']; /** * Redirect to another page. Only available in SSR builds. * @@ -1522,11 +1517,10 @@ export interface APIContext< /** * TODO documentation */ - locals: AstroSharedContext['locals']; + locals: AstroMiddleware.Locals; } export type Props = Record; -export type Locals = Record; export interface EndpointOutput { body: Body; @@ -1616,7 +1610,7 @@ export type MiddlewareNext = () => Promise; export type MiddlewareHandler = ( context: APIContext, next: MiddlewareNext -) => Promise | void | R; +) => Promise | Promise | void; export type MiddlewareResponseHandler = MiddlewareHandler; export type MiddlewareEndpointHandler = MiddlewareHandler; @@ -1744,3 +1738,7 @@ export type CreatePreviewServer = ( export interface PreviewModule { default: CreatePreviewServer; } + +export declare namespace AstroMiddleware { + export interface Locals {} +} diff --git a/packages/astro/src/core/middleware/sequence.ts b/packages/astro/src/core/middleware/sequence.ts index f32aa1945a48..167153740bc6 100644 --- a/packages/astro/src/core/middleware/sequence.ts +++ b/packages/astro/src/core/middleware/sequence.ts @@ -1,32 +1,43 @@ -import type { APIContext, MiddlewareHandler } from '../../@types/astro'; +import type { APIContext, MiddlewareHandler, 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: MiddlewareHandler[]): MiddlewareHandler { +export function sequence(...handlers: MiddlewareResponseHandler[]): MiddlewareResponseHandler { const length = handlers.length; if (!length) { - const handler: MiddlewareHandler = (context, resolve) => { - return resolve(); - }; + const handler: MiddlewareResponseHandler = defineMiddleware((context, next) => { + return next(); + }); return handler; } - return (context, resolve) => { + return defineMiddleware((context, next) => { return applyHandle(0, context); - function applyHandle(i: number, handleContext: APIContext): Promise { + function applyHandle(i: number, handleContext: APIContext) { const handle = handlers[i]; - - return handle(handleContext, () => { + const result = handle(handleContext, async () => { if (i < length - 1) { - return applyHandle(i + 1, handleContext); + const applyHandlePromise = applyHandle(i + 1, handleContext); + if (applyHandlePromise) { + const applyHandleResult = await applyHandlePromise; + if (applyHandleResult) { + return applyHandleResult; + } else { + return next(); + } + } else { + return next(); + } } else { - return resolve(); + return next(); } }); + return result; } - }; + }); } diff --git a/packages/astro/src/core/render/result.ts b/packages/astro/src/core/render/result.ts index 4f6c53c16dbe..dc37fee5ae57 100644 --- a/packages/astro/src/core/render/result.ts +++ b/packages/astro/src/core/render/result.ts @@ -2,13 +2,13 @@ import type { MarkdownRenderingOptions } from '@astrojs/markdown-remark'; import type { AstroGlobal, AstroGlobalPartial, - Locals, Params, Props, RuntimeMode, SSRElement, SSRLoadedRenderer, SSRResult, + AstroMiddleware, } from '../../@types/astro'; import { renderSlotToString, @@ -51,7 +51,7 @@ export interface CreateResultArgs { componentMetadata?: SSRResult['componentMetadata']; request: Request; status: number; - locals: Locals; + locals: AstroMiddleware.Locals; } function getFunctionExpression(slot: any) { From 68ce642456bdc506522941d81a7243096904890a Mon Sep 17 00:00:00 2001 From: Emanuele Stoppa Date: Wed, 26 Apr 2023 11:42:38 +0100 Subject: [PATCH 24/53] cleanup --- packages/astro/src/@types/astro.ts | 6 ++++-- packages/astro/src/core/request.ts | 1 - 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/astro/src/@types/astro.ts b/packages/astro/src/@types/astro.ts index d685852c63ac..a90ff80eeaf3 100644 --- a/packages/astro/src/@types/astro.ts +++ b/packages/astro/src/@types/astro.ts @@ -1739,6 +1739,8 @@ export interface PreviewModule { default: CreatePreviewServer; } -export declare namespace AstroMiddleware { - export interface Locals {} +export declare module AstroMiddleware { + export interface Locals { + [key: string]: unknown; + } } diff --git a/packages/astro/src/core/request.ts b/packages/astro/src/core/request.ts index 02e3e86e56fd..d8ac9033db1a 100644 --- a/packages/astro/src/core/request.ts +++ b/packages/astro/src/core/request.ts @@ -66,7 +66,6 @@ export function createRequest({ Reflect.set(request, clientAddressSymbol, clientAddress); } - // TODO: PLT-104 check if this needs to be set in another adapters? Reflect.set(request, clientLocalsSymbol, {}); return request; From 7b6c810e0120b65be98193c5d46ecf4881400984 Mon Sep 17 00:00:00 2001 From: Emanuele Stoppa Date: Wed, 26 Apr 2023 13:36:11 +0100 Subject: [PATCH 25/53] fix: type expansion --- packages/astro/src/@types/astro.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/astro/src/@types/astro.ts b/packages/astro/src/@types/astro.ts index a90ff80eeaf3..e674c5b4ca19 100644 --- a/packages/astro/src/@types/astro.ts +++ b/packages/astro/src/@types/astro.ts @@ -1739,8 +1739,8 @@ export interface PreviewModule { default: CreatePreviewServer; } -export declare module AstroMiddleware { - export interface Locals { - [key: string]: unknown; - } +// eslint-disable-next-line @typescript-eslint/no-namespace +export declare namespace AstroMiddleware { + // eslint-disable-next-line @typescript-eslint/no-empty-interface + export interface Locals {} } From d3a000845e6eb4289059bd1f3dacfa3e9785f199 Mon Sep 17 00:00:00 2001 From: Emanuele Stoppa Date: Wed, 26 Apr 2023 13:55:19 +0100 Subject: [PATCH 26/53] fix: regressions around endpoints --- packages/astro/src/core/app/index.ts | 17 +++++++++++---- packages/astro/src/core/app/types.ts | 2 +- packages/astro/src/core/build/generate.ts | 21 ++++++++++++------- .../src/core/build/plugins/plugin-pages.ts | 4 +--- .../src/core/build/plugins/plugin-ssr.ts | 5 ----- 5 files changed, 29 insertions(+), 20 deletions(-) diff --git a/packages/astro/src/core/app/index.ts b/packages/astro/src/core/app/index.ts index 39b06536f58c..35d688cd9ea8 100644 --- a/packages/astro/src/core/app/index.ts +++ b/packages/astro/src/core/app/index.ts @@ -240,10 +240,19 @@ export class App { site: this.#env.site, adapterName: this.#env.adapterName, }); - const onRequest = this.#manifest.middleware.onRequest as MiddlewareResponseHandler; - const response = await callMiddleware(onRequest, apiContext, () => { - return renderPage(mod, renderContext, this.#env, apiContext); - }); + const onRequest = this.#manifest.middleware?.onRequest; + let response; + if (onRequest) { + response = await callMiddleware( + onRequest as MiddlewareResponseHandler, + apiContext, + () => { + return renderPage(mod, renderContext, this.#env, apiContext); + } + ); + } else { + response = await renderPage(mod, renderContext, this.#env, apiContext); + } Reflect.set(request, responseSentSymbol, true); return response; } catch (err: any) { diff --git a/packages/astro/src/core/app/types.ts b/packages/astro/src/core/app/types.ts index 6256bbf544e5..79503161d0e7 100644 --- a/packages/astro/src/core/app/types.ts +++ b/packages/astro/src/core/app/types.ts @@ -39,7 +39,7 @@ export interface SSRManifest { entryModules: Record; assets: Set; componentMetadata: SSRResult['componentMetadata']; - middleware: AstroMiddlewareInstance; + middleware?: AstroMiddlewareInstance; } export type SerializedSSRManifest = Omit & { diff --git a/packages/astro/src/core/build/generate.ts b/packages/astro/src/core/build/generate.ts index a027ca64c6f8..6639db1e628e 100644 --- a/packages/astro/src/core/build/generate.ts +++ b/packages/astro/src/core/build/generate.ts @@ -345,7 +345,7 @@ async function generatePath( pathname: string, opts: StaticBuildOptions, gopts: GeneratePathOptions, - middleware: AstroMiddlewareInstance + middleware?: AstroMiddlewareInstance ) { const { settings, logging, origin, routeCache } = opts; const { mod, internals, linkIds, scripts: hoistedScripts, pageData, renderers } = gopts; @@ -495,12 +495,19 @@ async function generatePath( site: env.site, adapterName: env.adapterName, }); - // If the user doesn't configure a middleware, the rollup plugin emits a no-op function, - // so it's safe to use `callMiddleware` regardless - const onRequest = middleware.onRequest as MiddlewareResponseHandler; - response = await callMiddleware(onRequest, context, () => { - return renderPage(mod, ctx, env, context); - }); + + const onRequest = middleware?.onRequest; + if (onRequest) { + response = await callMiddleware( + onRequest as MiddlewareResponseHandler, + context, + () => { + return renderPage(mod, ctx, env, context); + } + ); + } else { + response = await renderPage(mod, ctx, env, context); + } } 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 99e6d986ab00..a7730e53a6e1 100644 --- a/packages/astro/src/core/build/plugins/plugin-pages.ts +++ b/packages/astro/src/core/build/plugins/plugin-pages.ts @@ -58,9 +58,7 @@ ${middlewareId ? `import * as _middleware from "${middlewareId.id}";` : ''} export const pageMap = new Map([${importMap}]); export const renderers = [${rendererItems}]; -export const middleware = ${ - middlewareId ? '_middleware' : '{ onRequest(_, next) {return next()} }' - }; +${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 36e8c816ba30..9767276af16e 100644 --- a/packages/astro/src/core/build/plugins/plugin-ssr.ts +++ b/packages/astro/src/core/build/plugins/plugin-ssr.ts @@ -222,11 +222,6 @@ function buildManifest( renderers: [], entryModules, assets: staticFiles.map(prefixAssetPath), - middleware: { - onRequest(_, next) { - return next(); - }, - }, }; return ssrManifest; From 48417ee01ea3c633b9fd63b57abe8fc50d04884b Mon Sep 17 00:00:00 2001 From: Emanuele Stoppa Date: Wed, 26 Apr 2023 14:12:16 +0100 Subject: [PATCH 27/53] fix: example type errors --- examples/middleware/src/env.d.ts | 17 +++++++++++------ examples/middleware/src/pages/admin.astro | 4 ---- examples/middleware/src/pages/login.astro | 5 ++--- examples/middleware/tsconfig.json | 3 +++ 4 files changed, 16 insertions(+), 13 deletions(-) create mode 100644 examples/middleware/tsconfig.json diff --git a/examples/middleware/src/env.d.ts b/examples/middleware/src/env.d.ts index 3dbd03a24337..f2de6d45d341 100644 --- a/examples/middleware/src/env.d.ts +++ b/examples/middleware/src/env.d.ts @@ -1,8 +1,13 @@ /// - -export interface Locals { - user: { - name: string; - surname: string; - }; +declare global { + namespace AstroMiddleware { + interface Locals { + user: { + name: string; + surname: string; + }; + } + } } + +export {}; diff --git a/examples/middleware/src/pages/admin.astro b/examples/middleware/src/pages/admin.astro index 3a7d0169fb48..35f7ca237113 100644 --- a/examples/middleware/src/pages/admin.astro +++ b/examples/middleware/src/pages/admin.astro @@ -1,11 +1,7 @@ --- -import Card from "../components/Card.astro"; import Layout from "../layouts/Layout.astro"; const user = Astro.locals.user; --- - - -

Welcome back {user.name} {user.surname}

diff --git a/examples/middleware/src/pages/login.astro b/examples/middleware/src/pages/login.astro index 9f2a08890386..ba5cd295232f 100644 --- a/examples/middleware/src/pages/login.astro +++ b/examples/middleware/src/pages/login.astro @@ -1,6 +1,5 @@ --- import Layout from '../layouts/Layout.astro'; -import Card from '../components/Card.astro'; const status = Astro.response.status; let redirectMessage; @@ -19,10 +18,10 @@ if (status === 301) { {redirectMessage}
diff --git a/examples/middleware/tsconfig.json b/examples/middleware/tsconfig.json new file mode 100644 index 000000000000..d78f81ec4e8e --- /dev/null +++ b/examples/middleware/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "astro/tsconfigs/base" +} From 596c7b1cd23c47eb2803749179bafb9a5bc38f5d Mon Sep 17 00:00:00 2001 From: Emanuele Stoppa Date: Wed, 26 Apr 2023 14:26:32 +0100 Subject: [PATCH 28/53] fix: regression inside tests --- packages/astro/src/@types/astro.ts | 2 +- packages/astro/src/core/middleware/sequence.ts | 7 ++++--- packages/astro/test/middleware.test.js | 2 +- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/astro/src/@types/astro.ts b/packages/astro/src/@types/astro.ts index e674c5b4ca19..69034841a1bf 100644 --- a/packages/astro/src/@types/astro.ts +++ b/packages/astro/src/@types/astro.ts @@ -1437,7 +1437,7 @@ interface AstroSharedContext = Record { 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('MiddlewareNoDataReturned'); + expect($('title').html()).to.equal('MiddlewareNoDataOrNextCalled'); }); }); From d5c7a994dbd8d8f640fdb026dfe1adb6434ce382 Mon Sep 17 00:00:00 2001 From: Emanuele Stoppa Date: Thu, 27 Apr 2023 11:30:22 +0100 Subject: [PATCH 29/53] fix: global generation of `AstroMiddleware.Locals` --- packages/astro/src/@types/astro.ts | 10 ++++++---- packages/astro/src/core/render/result.ts | 1 - 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/astro/src/@types/astro.ts b/packages/astro/src/@types/astro.ts index 69034841a1bf..fba670abd1c4 100644 --- a/packages/astro/src/@types/astro.ts +++ b/packages/astro/src/@types/astro.ts @@ -1739,8 +1739,10 @@ export interface PreviewModule { default: CreatePreviewServer; } -// eslint-disable-next-line @typescript-eslint/no-namespace -export declare namespace AstroMiddleware { - // eslint-disable-next-line @typescript-eslint/no-empty-interface - export interface Locals {} +declare global { + // eslint-disable-next-line @typescript-eslint/no-namespace + export namespace AstroMiddleware { + // eslint-disable-next-line @typescript-eslint/no-empty-interface + export interface Locals {} + } } diff --git a/packages/astro/src/core/render/result.ts b/packages/astro/src/core/render/result.ts index dc37fee5ae57..2bfa005903a0 100644 --- a/packages/astro/src/core/render/result.ts +++ b/packages/astro/src/core/render/result.ts @@ -8,7 +8,6 @@ import type { SSRElement, SSRLoadedRenderer, SSRResult, - AstroMiddleware, } from '../../@types/astro'; import { renderSlotToString, From ee26bff794117bb1ca01092ec5983f5a6059eaaf Mon Sep 17 00:00:00 2001 From: Emanuele Stoppa Date: Thu, 27 Apr 2023 12:02:33 +0100 Subject: [PATCH 30/53] fix: sequence API --- packages/astro/src/core/middleware/sequence.ts | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/packages/astro/src/core/middleware/sequence.ts b/packages/astro/src/core/middleware/sequence.ts index d6615a5b08ba..0ed08b03b502 100644 --- a/packages/astro/src/core/middleware/sequence.ts +++ b/packages/astro/src/core/middleware/sequence.ts @@ -21,19 +21,12 @@ export function sequence(...handlers: MiddlewareResponseHandler[]): MiddlewareRe 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) { - const applyHandlePromise = applyHandle(i + 1, handleContext); - if (applyHandlePromise) { - const applyHandleResult = await applyHandlePromise; - if (applyHandleResult) { - return applyHandleResult; - } else { - throw new AstroError(AstroErrorData.MiddlewareNoDataOrNextCalled); - } - } else { - throw new AstroError(AstroErrorData.MiddlewareNoDataOrNextCalled); - } + return applyHandle(i + 1, handleContext); } else { return next(); } From 617c9fa31e2ed845f9db428c071d7cc54b26d9bd Mon Sep 17 00:00:00 2001 From: Emanuele Stoppa Date: Thu, 27 Apr 2023 16:15:42 +0100 Subject: [PATCH 31/53] chore: changed the name of the global namespace --- packages/astro/client-base.d.ts | 6 ++++ packages/astro/package.json | 2 +- packages/astro/src/@types/app.d.ts | 9 ++++++ packages/astro/src/@types/astro.ts | 35 ++++++++++++++++-------- packages/astro/src/core/render/result.ts | 2 +- 5 files changed, 41 insertions(+), 13 deletions(-) create mode 100644 packages/astro/src/@types/app.d.ts diff --git a/packages/astro/client-base.d.ts b/packages/astro/client-base.d.ts index 15c1fb905420..6e37b60c773b 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 cb497331f40a..cdc7591382cf 100644 --- a/packages/astro/package.json +++ b/packages/astro/package.json @@ -67,7 +67,7 @@ }, "./middleware": { "default": "./middleware.mjs", - "types": "" + "types": "./middleware.d.ts" } }, "imports": { diff --git a/packages/astro/src/@types/app.d.ts b/packages/astro/src/@types/app.d.ts new file mode 100644 index 000000000000..1c0908bb8072 --- /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 fba670abd1c4..bd071abf82d0 100644 --- a/packages/astro/src/@types/astro.ts +++ b/packages/astro/src/@types/astro.ts @@ -1,3 +1,4 @@ +import './app.js'; import type { MarkdownHeading, MarkdownMetadata, @@ -1439,7 +1440,7 @@ interface AstroSharedContext = Record = Record> @@ -1515,9 +1516,29 @@ export interface APIContext = Record { + * context.locals.greeting = "Hello!"; + * next(); + * }); + * ``` + * Inside the an astro file: + * ```astro + * --- + * // index.astro + * const greeting = Astro.locals.greeting; + * --- + *

{greeting}

+ * ``` */ - locals: AstroMiddleware.Locals; + locals: App.Locals; } export type Props = Record; @@ -1738,11 +1759,3 @@ export type CreatePreviewServer = ( export interface PreviewModule { default: CreatePreviewServer; } - -declare global { - // eslint-disable-next-line @typescript-eslint/no-namespace - export namespace AstroMiddleware { - // eslint-disable-next-line @typescript-eslint/no-empty-interface - export interface Locals {} - } -} diff --git a/packages/astro/src/core/render/result.ts b/packages/astro/src/core/render/result.ts index 2bfa005903a0..598ec116f785 100644 --- a/packages/astro/src/core/render/result.ts +++ b/packages/astro/src/core/render/result.ts @@ -50,7 +50,7 @@ export interface CreateResultArgs { componentMetadata?: SSRResult['componentMetadata']; request: Request; status: number; - locals: AstroMiddleware.Locals; + locals: App.Locals; } function getFunctionExpression(slot: any) { From eba8c6e7dd4934785e2fadfc7cc4bd49c744e212 Mon Sep 17 00:00:00 2001 From: Emanuele Stoppa Date: Fri, 28 Apr 2023 12:02:19 +0100 Subject: [PATCH 32/53] chore: remove the unneeded import --- packages/astro/src/@types/astro.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/astro/src/@types/astro.ts b/packages/astro/src/@types/astro.ts index bd071abf82d0..3362a83d7e46 100644 --- a/packages/astro/src/@types/astro.ts +++ b/packages/astro/src/@types/astro.ts @@ -1,4 +1,3 @@ -import './app.js'; import type { MarkdownHeading, MarkdownMetadata, From dc83670a778c89906c50f2a75d994ba0ff9c840a Mon Sep 17 00:00:00 2001 From: Emanuele Stoppa Date: Fri, 28 Apr 2023 13:15:52 +0100 Subject: [PATCH 33/53] chore: split tests --- packages/astro/test/middleware.test.js | 256 +++++++++++++------------ 1 file changed, 129 insertions(+), 127 deletions(-) diff --git a/packages/astro/test/middleware.test.js b/packages/astro/test/middleware.test.js index 2df9565c26c2..996c8981749b 100644 --- a/packages/astro/test/middleware.test.js +++ b/packages/astro/test/middleware.test.js @@ -3,168 +3,170 @@ import { expect } from 'chai'; import * as cheerio from 'cheerio'; import testAdapter from './test-adapter.js'; -describe('Middleware API', () => { - describe('in DEV mode', () => { - /** @type {import('./test-utils').Fixture} */ - let fixture; - let devServer; +describe('Middleware API in DEV mode', () => { + /** @type {import('./test-utils').Fixture} */ + let fixture; + let devServer; - before(async () => { - fixture = await loadFixture({ - root: './fixtures/middleware/', - }); - devServer = await fixture.startDevServer(); + before(async () => { + fixture = await loadFixture({ + root: './fixtures/middleware/', }); + devServer = await fixture.startDevServer(); + }); - after(async () => { - await devServer.stop(); - }); + 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 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'); + 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'); - }); + 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 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 redirect to another page', async () => { - let html = await fixture.fetch('/redirect').then((res) => { - expect(res.status).to.equal(200); - return res.text(); - }); - let $ = cheerio.load(html); - expect($('p').html()).to.equal('bar'); - expect($('span').html()).to.equal('Index'); + it('should successfully redirect to another page', async () => { + let html = await fixture.fetch('/redirect').then((res) => { + expect(res.status).to.equal(200); + return res.text(); }); + let $ = cheerio.load(html); + expect($('p').html()).to.equal('bar'); + expect($('span').html()).to.equal('Index'); + }); - 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 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').then((res) => { - expect(res.status).to.equal(500); - return res.text(); - }); + it('should return a new response that is a 500', async () => { + await fixture.fetch('/broken').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 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 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'); - }); + 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('in PROD mode', () => { - /** @type {import('./test-utils').Fixture} */ - let fixture; - let previewServer; +describe('Middleware API in PROD mode', () => { + /** @type {import('./test-utils').Fixture} */ + let fixture; + let previewServer; - before(async () => { + before(async () => { + try { fixture = await loadFixture({ root: './fixtures/middleware/', }); await fixture.build(); previewServer = await fixture.preview(); - }); + } catch (e) { + console.log(e); + } + }); - // important: close preview server (free up port and connection) - after(async () => { - await previewServer.stop(); - }); + // important: close preview server (free up port and connection) + after(async () => { + await previewServer.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 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'); + 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'); - }); + 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 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 redirect to another page', async () => { - let html = await fixture.fetch('/redirect').then((res) => { - expect(res.status).to.equal(200); - return res.text(); - }); - let $ = cheerio.load(html); - expect($('p').html()).to.equal('bar'); - expect($('span').html()).to.equal('Index'); + it('should successfully redirect to another page', async () => { + let html = await fixture.fetch('/redirect').then((res) => { + expect(res.status).to.equal(200); + return res.text(); }); + let $ = cheerio.load(html); + expect($('p').html()).to.equal('bar'); + expect($('span').html()).to.equal('Index'); + }); - 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 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').then((res) => { - expect(res.status).to.equal(500); - return res.text(); - }); + it('should return a new response that is a 500', async () => { + await fixture.fetch('/broken').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 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 NOT 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.not.equal('LocalsNotSerializable'); - }); + it('should NOT 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.not.equal('LocalsNotSerializable'); + }); - it("should throws 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.not.equal('MiddlewareNoDataReturned'); - }); + it("should throws 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.not.equal('MiddlewareNoDataReturned'); }); }); From 49bcedd106f5f1751dba03e13dc06f2eb42dfafb Mon Sep 17 00:00:00 2001 From: Emanuele Stoppa Date: Fri, 28 Apr 2023 13:39:34 +0100 Subject: [PATCH 34/53] chore: try manually --- packages/astro/test/middleware.test.js | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/packages/astro/test/middleware.test.js b/packages/astro/test/middleware.test.js index 996c8981749b..a73c7c3e7182 100644 --- a/packages/astro/test/middleware.test.js +++ b/packages/astro/test/middleware.test.js @@ -1,7 +1,7 @@ import { loadFixture } from './test-utils.js'; import { expect } from 'chai'; import * as cheerio from 'cheerio'; -import testAdapter from './test-adapter.js'; +import nodejs from '../../integrations/node/dist/index.js'; describe('Middleware API in DEV mode', () => { /** @type {import('./test-utils').Fixture} */ @@ -87,18 +87,17 @@ describe('Middleware API in DEV mode', () => { describe('Middleware API in PROD mode', () => { /** @type {import('./test-utils').Fixture} */ let fixture; + /** @type {import('./test-utils').PreviewServer} */ let previewServer; before(async () => { - try { - fixture = await loadFixture({ - root: './fixtures/middleware/', - }); - await fixture.build(); - previewServer = await fixture.preview(); - } catch (e) { - console.log(e); - } + fixture = await loadFixture({ + root: './fixtures/middleware/', + output: 'server', + adapter: nodejs({ mode: 'standalone' }), + }); + await fixture.build(); + previewServer = await fixture.preview(); }); // important: close preview server (free up port and connection) From 7770ff672a9c8377ad42c20403a5c51cdb82b42b Mon Sep 17 00:00:00 2001 From: Emanuele Stoppa Date: Fri, 28 Apr 2023 14:47:32 +0100 Subject: [PATCH 35/53] chore: try again? --- .../astro.config.mjs | 4 -- .../test/fixtures/middleware-dev/package.json | 8 ++++ .../src/middleware.js | 16 ++++---- .../src/pages/broken-locals.astro | 0 .../src/pages/broken.astro | 0 .../src/pages/does-nothing.astro | 0 .../src/pages/index.astro | 0 .../src/pages/lorem.astro | 0 .../src/pages/not-interested.astro | 0 .../src/pages/redirect.astro | 0 .../src/pages/rewrite.astro | 0 .../src/pages/second.astro | 0 .../fixtures/middleware-ssg/astro.config.mjs | 8 ++++ .../test/fixtures/middleware-ssg/package.json | 8 ++++ .../fixtures/middleware-ssg/src/middleware.js | 12 ++++++ .../middleware-ssg/src/pages/index.astro | 14 +++++++ .../middleware-ssg/src/pages/second.astro | 13 +++++++ .../test/fixtures/middleware/package.json | 9 ----- packages/astro/test/middleware.test.js | 38 +++++++++++++++++-- 19 files changed, 105 insertions(+), 25 deletions(-) rename packages/astro/test/fixtures/{middleware => middleware-dev}/astro.config.mjs (71%) create mode 100644 packages/astro/test/fixtures/middleware-dev/package.json rename packages/astro/test/fixtures/{middleware => middleware-dev}/src/middleware.js (62%) rename packages/astro/test/fixtures/{middleware => middleware-dev}/src/pages/broken-locals.astro (100%) rename packages/astro/test/fixtures/{middleware => middleware-dev}/src/pages/broken.astro (100%) rename packages/astro/test/fixtures/{middleware => middleware-dev}/src/pages/does-nothing.astro (100%) rename packages/astro/test/fixtures/{middleware => middleware-dev}/src/pages/index.astro (100%) rename packages/astro/test/fixtures/{middleware => middleware-dev}/src/pages/lorem.astro (100%) rename packages/astro/test/fixtures/{middleware => middleware-dev}/src/pages/not-interested.astro (100%) rename packages/astro/test/fixtures/{middleware => middleware-dev}/src/pages/redirect.astro (100%) rename packages/astro/test/fixtures/{middleware => middleware-dev}/src/pages/rewrite.astro (100%) rename packages/astro/test/fixtures/{middleware => middleware-dev}/src/pages/second.astro (100%) create mode 100644 packages/astro/test/fixtures/middleware-ssg/astro.config.mjs create mode 100644 packages/astro/test/fixtures/middleware-ssg/package.json create mode 100644 packages/astro/test/fixtures/middleware-ssg/src/middleware.js create mode 100644 packages/astro/test/fixtures/middleware-ssg/src/pages/index.astro create mode 100644 packages/astro/test/fixtures/middleware-ssg/src/pages/second.astro delete mode 100644 packages/astro/test/fixtures/middleware/package.json diff --git a/packages/astro/test/fixtures/middleware/astro.config.mjs b/packages/astro/test/fixtures/middleware-dev/astro.config.mjs similarity index 71% rename from packages/astro/test/fixtures/middleware/astro.config.mjs rename to packages/astro/test/fixtures/middleware-dev/astro.config.mjs index 1523fb4be31e..0e71ac2f2d11 100644 --- a/packages/astro/test/fixtures/middleware/astro.config.mjs +++ b/packages/astro/test/fixtures/middleware-dev/astro.config.mjs @@ -2,10 +2,6 @@ import { defineConfig } from 'astro/config'; import node from '@astrojs/node'; export default defineConfig({ - output: "server", - adapter: node({ - mode: "standalone" - }), 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 000000000000..bc889aa63096 --- /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/src/middleware.js b/packages/astro/test/fixtures/middleware-dev/src/middleware.js similarity index 62% rename from packages/astro/test/fixtures/middleware/src/middleware.js rename to packages/astro/test/fixtures/middleware-dev/src/middleware.js index 5e717c1b4c5d..9816a8d05414 100644 --- a/packages/astro/test/fixtures/middleware/src/middleware.js +++ b/packages/astro/test/fixtures/middleware-dev/src/middleware.js @@ -1,13 +1,13 @@ import { sequence, defineMiddleware } from 'astro/middleware'; const first = defineMiddleware(async (context, next) => { - if (context.request.url.endsWith('/lorem')) { + if (context.request.url.includes('/lorem')) { context.locals.name = 'ipsum'; - } else if (context.request.url.endsWith('/rewrite')) { + } else if (context.request.url.includes('/rewrite')) { return new Response('New content!!', { status: 200, }); - } else if (context.request.url.endsWith('/broken')) { + } else if (context.request.url.includes('/broken')) { return new Response(null, { status: 500, }); @@ -18,21 +18,21 @@ const first = defineMiddleware(async (context, next) => { }); const second = defineMiddleware(async (context, next) => { - if (context.request.url.endsWith('/second')) { + if (context.request.url.includes('/second')) { context.locals.name = 'second'; - } else if (context.request.url.endsWith('/redirect')) { + } else if (context.request.url.includes('/redirect')) { return context.redirect('/', 302); } return await next(); }); const third = defineMiddleware(async (context, next) => { - if (context.request.url.endsWith('/broken-locals')) { + if (context.request.url.includes('/broken-locals')) { context.locals = { fn() {}, }; - } else if (context.request.url.endsWith('/does-nothing')) { - return undefined; + } else if (context.request.url.includes('/does-nothing')) { + // return undefined; } next(); }); diff --git a/packages/astro/test/fixtures/middleware/src/pages/broken-locals.astro b/packages/astro/test/fixtures/middleware-dev/src/pages/broken-locals.astro similarity index 100% rename from packages/astro/test/fixtures/middleware/src/pages/broken-locals.astro rename to packages/astro/test/fixtures/middleware-dev/src/pages/broken-locals.astro diff --git a/packages/astro/test/fixtures/middleware/src/pages/broken.astro b/packages/astro/test/fixtures/middleware-dev/src/pages/broken.astro similarity index 100% rename from packages/astro/test/fixtures/middleware/src/pages/broken.astro rename to packages/astro/test/fixtures/middleware-dev/src/pages/broken.astro diff --git a/packages/astro/test/fixtures/middleware/src/pages/does-nothing.astro b/packages/astro/test/fixtures/middleware-dev/src/pages/does-nothing.astro similarity index 100% rename from packages/astro/test/fixtures/middleware/src/pages/does-nothing.astro rename to packages/astro/test/fixtures/middleware-dev/src/pages/does-nothing.astro diff --git a/packages/astro/test/fixtures/middleware/src/pages/index.astro b/packages/astro/test/fixtures/middleware-dev/src/pages/index.astro similarity index 100% rename from packages/astro/test/fixtures/middleware/src/pages/index.astro rename to packages/astro/test/fixtures/middleware-dev/src/pages/index.astro diff --git a/packages/astro/test/fixtures/middleware/src/pages/lorem.astro b/packages/astro/test/fixtures/middleware-dev/src/pages/lorem.astro similarity index 100% rename from packages/astro/test/fixtures/middleware/src/pages/lorem.astro rename to packages/astro/test/fixtures/middleware-dev/src/pages/lorem.astro diff --git a/packages/astro/test/fixtures/middleware/src/pages/not-interested.astro b/packages/astro/test/fixtures/middleware-dev/src/pages/not-interested.astro similarity index 100% rename from packages/astro/test/fixtures/middleware/src/pages/not-interested.astro rename to packages/astro/test/fixtures/middleware-dev/src/pages/not-interested.astro diff --git a/packages/astro/test/fixtures/middleware/src/pages/redirect.astro b/packages/astro/test/fixtures/middleware-dev/src/pages/redirect.astro similarity index 100% rename from packages/astro/test/fixtures/middleware/src/pages/redirect.astro rename to packages/astro/test/fixtures/middleware-dev/src/pages/redirect.astro diff --git a/packages/astro/test/fixtures/middleware/src/pages/rewrite.astro b/packages/astro/test/fixtures/middleware-dev/src/pages/rewrite.astro similarity index 100% rename from packages/astro/test/fixtures/middleware/src/pages/rewrite.astro rename to packages/astro/test/fixtures/middleware-dev/src/pages/rewrite.astro diff --git a/packages/astro/test/fixtures/middleware/src/pages/second.astro b/packages/astro/test/fixtures/middleware-dev/src/pages/second.astro similarity index 100% rename from packages/astro/test/fixtures/middleware/src/pages/second.astro rename to packages/astro/test/fixtures/middleware-dev/src/pages/second.astro 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 000000000000..2f2e911a8d6d --- /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 000000000000..2ac44245434c --- /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 000000000000..f28d89f670aa --- /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 000000000000..395a4d695cfa --- /dev/null +++ b/packages/astro/test/fixtures/middleware-ssg/src/pages/index.astro @@ -0,0 +1,14 @@ +--- +const data = Astro.locals; +--- + + + + Testing + + + + Index +

{data?.name}

+ + 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 000000000000..c6edf9cd75a0 --- /dev/null +++ b/packages/astro/test/fixtures/middleware-ssg/src/pages/second.astro @@ -0,0 +1,13 @@ +--- +const data = Astro.locals; +--- + + + + Testing + + + +

{data?.name}

+ + diff --git a/packages/astro/test/fixtures/middleware/package.json b/packages/astro/test/fixtures/middleware/package.json deleted file mode 100644 index e242868c7a9a..000000000000 --- a/packages/astro/test/fixtures/middleware/package.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "name": "@test/dev-middleware", - "version": "0.0.0", - "private": true, - "dependencies": { - "astro": "workspace:*", - "@astrojs/node": "workspace:*" - } -} diff --git a/packages/astro/test/middleware.test.js b/packages/astro/test/middleware.test.js index a73c7c3e7182..1e2a1e799ff3 100644 --- a/packages/astro/test/middleware.test.js +++ b/packages/astro/test/middleware.test.js @@ -3,14 +3,14 @@ import { expect } from 'chai'; import * as cheerio from 'cheerio'; import nodejs from '../../integrations/node/dist/index.js'; -describe('Middleware API in DEV mode', () => { +describe('Middleware in DEV mode', () => { /** @type {import('./test-utils').Fixture} */ let fixture; let devServer; before(async () => { fixture = await loadFixture({ - root: './fixtures/middleware/', + root: './fixtures/middleware-dev/', }); devServer = await fixture.startDevServer(); }); @@ -84,7 +84,7 @@ describe('Middleware API in DEV mode', () => { }); }); -describe('Middleware API in PROD mode', () => { +describe('Middleware in PROD mode, SSG', () => { /** @type {import('./test-utils').Fixture} */ let fixture; /** @type {import('./test-utils').PreviewServer} */ @@ -92,7 +92,37 @@ describe('Middleware API in PROD mode', () => { before(async () => { fixture = await loadFixture({ - root: './fixtures/middleware/', + 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; + /** @type {import('./test-utils').PreviewServer} */ + let previewServer; + + before(async () => { + fixture = await loadFixture({ + root: './fixtures/middleware-dev/', output: 'server', adapter: nodejs({ mode: 'standalone' }), }); From 35a8f6e27ea0f1eae1018ef28ab932a9919a2d37 Mon Sep 17 00:00:00 2001 From: Emanuele Stoppa Date: Fri, 28 Apr 2023 15:31:48 +0100 Subject: [PATCH 36/53] chore: skip middleware for simple endpoints --- packages/astro/src/core/endpoint/index.ts | 34 +++++++++++-------- .../src/core/middleware/callMiddleware.ts | 8 +++-- .../vite-plugin-astro-postprocess/index.ts | 1 - 3 files changed, 25 insertions(+), 18 deletions(-) diff --git a/packages/astro/src/core/endpoint/index.ts b/packages/astro/src/core/endpoint/index.ts index b1683eb01197..2468531c252f 100644 --- a/packages/astro/src/core/endpoint/index.ts +++ b/packages/astro/src/core/endpoint/index.ts @@ -5,7 +5,6 @@ import type { EndpointHandler, EndpointOutput, MiddlewareEndpointHandler, - MiddlewareHandler, Params, } from '../../@types/astro'; import type { Environment, RenderContext } from '../render/index'; @@ -129,21 +128,26 @@ export async function call( adapterName: env.adapterName, }); - let response; + let response = await renderEndpoint(mod, context, env.ssr); if (middleware && middleware.onRequest) { - const onRequest = middleware.onRequest as MiddlewareEndpointHandler; - response = await callMiddleware(onRequest, context, async () => { - const result = await renderEndpoint(mod, context, env.ssr); - if (env.mode === 'development' && !isValueSerializable(context.locals)) { - throw new AstroError({ - ...AstroErrorData.LocalsNotSerializable, - message: AstroErrorData.LocalsNotSerializable.message(ctx.pathname), - }); - } - return result; - }); - } else { - response = await renderEndpoint(mod, context, env.ssr); + if (response.body === null) { + const onRequest = middleware.onRequest as MiddlewareEndpointHandler; + response = await callMiddleware(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) { diff --git a/packages/astro/src/core/middleware/callMiddleware.ts b/packages/astro/src/core/middleware/callMiddleware.ts index 7cff2fa78510..5836786e60eb 100644 --- a/packages/astro/src/core/middleware/callMiddleware.ts +++ b/packages/astro/src/core/middleware/callMiddleware.ts @@ -66,7 +66,7 @@ export async function callMiddleware( * ``` */ if (typeof value !== 'undefined') { - if (value instanceof Response == false) { + if (value instanceof Response === false) { throw new AstroError(AstroErrorData.MiddlewareNotAResponse); } return value as R; @@ -85,7 +85,7 @@ export async function callMiddleware( * If not thing is returned, then we raise an Astro error. */ throw new AstroError(AstroErrorData.MiddlewareNoDataOrNextCalled); - } else if (value instanceof Response == false) { + } else if (value instanceof Response === false) { throw new AstroError(AstroErrorData.MiddlewareNotAResponse); } else { // Middleware did not call resolve and returned a value @@ -93,3 +93,7 @@ export async function callMiddleware( } }); } + +function isEndpointResult(response: any): boolean { + return response && typeof response.body !== 'undefined'; +} diff --git a/packages/astro/src/vite-plugin-astro-postprocess/index.ts b/packages/astro/src/vite-plugin-astro-postprocess/index.ts index 857169aae07c..05895ab627c7 100644 --- a/packages/astro/src/vite-plugin-astro-postprocess/index.ts +++ b/packages/astro/src/vite-plugin-astro-postprocess/index.ts @@ -33,7 +33,6 @@ export default function astro(_opts: AstroPluginOptions): Plugin { sourceType: 'module', }); - // @ts-expect-error walk(ast, { enter(node: any) { // Transform `Astro.glob("./pages/*.astro")` to `Astro.glob(import.meta.glob("./pages/*.astro"), () => "./pages/*.astro")` From fb2d2bf041bd6a0eb34fac99cbd247e70e787ea3 Mon Sep 17 00:00:00 2001 From: Emanuele Stoppa Date: Fri, 28 Apr 2023 16:54:13 +0100 Subject: [PATCH 37/53] chore: skip SSR tests (for now) --- .../fixtures/middleware-dev/src/middleware.js | 4 +-- .../pages/{broken.astro => broken-500.astro} | 0 packages/astro/test/middleware.test.js | 26 +++++++++---------- 3 files changed, 15 insertions(+), 15 deletions(-) rename packages/astro/test/fixtures/middleware-dev/src/pages/{broken.astro => broken-500.astro} (100%) diff --git a/packages/astro/test/fixtures/middleware-dev/src/middleware.js b/packages/astro/test/fixtures/middleware-dev/src/middleware.js index 9816a8d05414..2a09552e7186 100644 --- a/packages/astro/test/fixtures/middleware-dev/src/middleware.js +++ b/packages/astro/test/fixtures/middleware-dev/src/middleware.js @@ -7,7 +7,7 @@ const first = defineMiddleware(async (context, next) => { return new Response('New content!!', { status: 200, }); - } else if (context.request.url.includes('/broken')) { + } else if (context.request.url.includes('/broken-500')) { return new Response(null, { status: 500, }); @@ -32,7 +32,7 @@ const third = defineMiddleware(async (context, next) => { fn() {}, }; } else if (context.request.url.includes('/does-nothing')) { - // return undefined; + return undefined; } next(); }); diff --git a/packages/astro/test/fixtures/middleware-dev/src/pages/broken.astro b/packages/astro/test/fixtures/middleware-dev/src/pages/broken-500.astro similarity index 100% rename from packages/astro/test/fixtures/middleware-dev/src/pages/broken.astro rename to packages/astro/test/fixtures/middleware-dev/src/pages/broken-500.astro diff --git a/packages/astro/test/middleware.test.js b/packages/astro/test/middleware.test.js index 1e2a1e799ff3..11d4d7da1e6b 100644 --- a/packages/astro/test/middleware.test.js +++ b/packages/astro/test/middleware.test.js @@ -41,16 +41,6 @@ describe('Middleware in DEV mode', () => { expect($('p').html()).to.equal('second'); }); - it('should successfully redirect to another page', async () => { - let html = await fixture.fetch('/redirect').then((res) => { - expect(res.status).to.equal(200); - return res.text(); - }); - let $ = cheerio.load(html); - expect($('p').html()).to.equal('bar'); - expect($('span').html()).to.equal('Index'); - }); - it('should successfully create a new response', async () => { let html = await fixture.fetch('/rewrite').then((res) => res.text()); let $ = cheerio.load(html); @@ -59,7 +49,7 @@ describe('Middleware in DEV mode', () => { }); it('should return a new response that is a 500', async () => { - await fixture.fetch('/broken').then((res) => { + await fixture.fetch('/broken-500').then((res) => { expect(res.status).to.equal(500); return res.text(); }); @@ -114,7 +104,7 @@ describe('Middleware in PROD mode, SSG', () => { }); }); -describe('Middleware API in PROD mode, SSR', () => { +describe.skip('Middleware API in PROD mode, SSR', () => { /** @type {import('./test-utils').Fixture} */ let fixture; /** @type {import('./test-utils').PreviewServer} */ @@ -151,6 +141,16 @@ describe('Middleware API in PROD mode, SSR', () => { expect($('p').html()).to.equal('ipsum'); }); + it('should successfully redirect to another page', async () => { + let html = await fixture.fetch('/redirect').then((res) => { + expect(res.status).to.equal(200); + return res.text(); + }); + let $ = cheerio.load(html); + expect($('p').html()).to.equal('bar'); + expect($('span').html()).to.equal('Index'); + }); + it('should call a second middleware', async () => { let html = await fixture.fetch('/second').then((res) => res.text()); let $ = cheerio.load(html); @@ -175,7 +175,7 @@ describe('Middleware API in PROD mode, SSR', () => { }); it('should return a new response that is a 500', async () => { - await fixture.fetch('/broken').then((res) => { + await fixture.fetch('/broken-500').then((res) => { expect(res.status).to.equal(500); return res.text(); }); From 42fb7316e6a45aeddd947eb446685ba3106b5e6d Mon Sep 17 00:00:00 2001 From: Emanuele Stoppa Date: Tue, 2 May 2023 15:05:04 +0100 Subject: [PATCH 38/53] chore: update test adapter --- packages/astro/test/middleware.test.js | 90 +++++++++++++------------- packages/astro/test/test-adapter.js | 6 +- packages/astro/test/test-utils.js | 2 +- 3 files changed, 50 insertions(+), 48 deletions(-) diff --git a/packages/astro/test/middleware.test.js b/packages/astro/test/middleware.test.js index 11d4d7da1e6b..711cf0f68b78 100644 --- a/packages/astro/test/middleware.test.js +++ b/packages/astro/test/middleware.test.js @@ -1,7 +1,7 @@ import { loadFixture } from './test-utils.js'; import { expect } from 'chai'; import * as cheerio from 'cheerio'; -import nodejs from '../../integrations/node/dist/index.js'; +import testAdapter from './test-adapter.js'; describe('Middleware in DEV mode', () => { /** @type {import('./test-utils').Fixture} */ @@ -104,98 +104,98 @@ describe('Middleware in PROD mode, SSG', () => { }); }); -describe.skip('Middleware API in PROD mode, SSR', () => { +describe('Middleware API in PROD mode, SSR', () => { /** @type {import('./test-utils').Fixture} */ let fixture; - /** @type {import('./test-utils').PreviewServer} */ - let previewServer; before(async () => { fixture = await loadFixture({ root: './fixtures/middleware-dev/', output: 'server', - adapter: nodejs({ mode: 'standalone' }), + adapter: testAdapter(), }); await fixture.build(); - previewServer = await fixture.preview(); - }); - - // important: close preview server (free up port and connection) - after(async () => { - await previewServer.stop(); }); it('should render locals data', async () => { - const html = await fixture.fetch('/').then((res) => res.text()); + const app = await fixture.loadTestAdapterApp(); + const request = new Request('http://example.com/'); + const response = await app.render(request); + const html = await response.text(); + console.log(html); 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()); + 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'); - html = await fixture.fetch('/lorem').then((res) => res.text()); + 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 () => { - let html = await fixture.fetch('/redirect').then((res) => { - expect(res.status).to.equal(200); - return res.text(); - }); - let $ = cheerio.load(html); - expect($('p').html()).to.equal('bar'); - expect($('span').html()).to.equal('Index'); + 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 () => { - let html = await fixture.fetch('/second').then((res) => res.text()); - let $ = cheerio.load(html); + 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 redirect to another page', async () => { - let html = await fixture.fetch('/redirect').then((res) => { - expect(res.status).to.equal(200); - return res.text(); - }); - let $ = cheerio.load(html); - expect($('p').html()).to.equal('bar'); - expect($('span').html()).to.equal('Index'); - }); - it('should successfully create a new response', async () => { - let html = await fixture.fetch('/rewrite').then((res) => res.text()); - let $ = cheerio.load(html); + 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 () => { - await fixture.fetch('/broken-500').then((res) => { - expect(res.status).to.equal(500); - return res.text(); - }); + 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 () => { - let html = await fixture.fetch('/not-interested').then((res) => res.text()); - let $ = cheerio.load(html); + 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 () => { - let html = await fixture.fetch('/broken-locals').then((res) => res.text()); - let $ = cheerio.load(html); + 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 () => { - let html = await fixture.fetch('/does-nothing').then((res) => res.text()); - let $ = cheerio.load(html); + 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 4faa9a7c69cb..955f7a3df30e 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); } @@ -50,7 +51,8 @@ export default function ({ provideAddress = true, extendAdapter } = { provideAdd export function createExports(manifest) { return { manifest, - createApp: (streaming) => new MyApp(manifest, streaming) + createApp: (streaming) => new MyApp(manifest, streaming), + middleware: (_, next) => {next()} }; } `; @@ -65,7 +67,7 @@ export default function ({ provideAddress = true, extendAdapter } = { provideAdd setAdapter({ name: 'my-ssr-adapter', serverEntrypoint: '@my-ssr', - exports: ['manifest', 'createApp'], + exports: ['manifest', 'createApp', 'middleware'], ...extendAdapter, }); }, diff --git a/packages/astro/test/test-utils.js b/packages/astro/test/test-utils.js index 6e3113978029..f933a13ada33 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; From 2e65f42f23e93dc21297902a4dc8e18976be9597 Mon Sep 17 00:00:00 2001 From: Emanuele Stoppa Date: Tue, 2 May 2023 15:17:52 +0100 Subject: [PATCH 39/53] fix: tweak plugin SSR to not leak warnings --- .../astro/src/core/build/plugins/plugin-ssr.ts | 18 ++++++++++++++---- packages/astro/test/middleware.test.js | 5 +++-- packages/astro/test/test-adapter.js | 6 +++--- 3 files changed, 20 insertions(+), 9 deletions(-) diff --git a/packages/astro/src/core/build/plugins/plugin-ssr.ts b/packages/astro/src/core/build/plugins/plugin-ssr.ts index 9767276af16e..65e104425dc2 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,6 +39,10 @@ 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'; @@ -42,7 +50,7 @@ import { _privateSetManifestDontUseThis } from 'astro:ssr-manifest'; const _manifest = Object.assign(_deserializeManifest('${manifestReplace}'), { pageMap: _main.pageMap, renderers: _main.renderers, - middleware: _main.middleware + ${middleware} }); _privateSetManifestDontUseThis(_manifest); const _args = ${adapter.args ? JSON.stringify(adapter.args) : 'undefined'}; @@ -236,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/test/middleware.test.js b/packages/astro/test/middleware.test.js index 711cf0f68b78..784a32c30146 100644 --- a/packages/astro/test/middleware.test.js +++ b/packages/astro/test/middleware.test.js @@ -112,7 +112,9 @@ describe('Middleware API in PROD mode, SSR', () => { fixture = await loadFixture({ root: './fixtures/middleware-dev/', output: 'server', - adapter: testAdapter(), + adapter: testAdapter({ + // exports: ['manifest', 'createApp', 'middleware'], + }), }); await fixture.build(); }); @@ -122,7 +124,6 @@ describe('Middleware API in PROD mode, SSR', () => { const request = new Request('http://example.com/'); const response = await app.render(request); const html = await response.text(); - console.log(html); const $ = cheerio.load(html); expect($('p').html()).to.equal('bar'); }); diff --git a/packages/astro/test/test-adapter.js b/packages/astro/test/test-adapter.js index 955f7a3df30e..cc34e3c3373c 100644 --- a/packages/astro/test/test-adapter.js +++ b/packages/astro/test/test-adapter.js @@ -51,8 +51,8 @@ export default function ({ provideAddress = true, extendAdapter } = { provideAdd export function createExports(manifest) { return { manifest, - createApp: (streaming) => new MyApp(manifest, streaming), - middleware: (_, next) => {next()} + createApp: (streaming) => new MyApp(manifest, streaming) + }; } `; @@ -67,7 +67,7 @@ export default function ({ provideAddress = true, extendAdapter } = { provideAdd setAdapter({ name: 'my-ssr-adapter', serverEntrypoint: '@my-ssr', - exports: ['manifest', 'createApp', 'middleware'], + exports: ['manifest', 'createApp'], ...extendAdapter, }); }, From 9993b920d59361f4f533f087279852c9ab2b6e1a Mon Sep 17 00:00:00 2001 From: Emanuele Stoppa Date: Tue, 2 May 2023 15:29:43 +0100 Subject: [PATCH 40/53] Apply suggestions from code review Co-authored-by: Sarah Rainsberger --- packages/astro/src/@types/astro.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/astro/src/@types/astro.ts b/packages/astro/src/@types/astro.ts index 3362a83d7e46..b8d541204ee6 100644 --- a/packages/astro/src/@types/astro.ts +++ b/packages/astro/src/@types/astro.ts @@ -1528,10 +1528,10 @@ export interface APIContext = Record{greeting} From bca1b6a9c29a8ac1efa6248a1fabc0e8fa769564 Mon Sep 17 00:00:00 2001 From: Emanuele Stoppa Date: Tue, 2 May 2023 15:30:16 +0100 Subject: [PATCH 41/53] revert: change name --- packages/astro/src/@types/astro.ts | 2 +- packages/astro/src/integrations/index.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/astro/src/@types/astro.ts b/packages/astro/src/@types/astro.ts index b8d541204ee6..ae9b2b4a00de 100644 --- a/packages/astro/src/@types/astro.ts +++ b/packages/astro/src/@types/astro.ts @@ -1578,7 +1578,7 @@ export interface SSRLoadedRenderer extends AstroRenderer { }; } -export type HookIntegrationParameters< +export type HookParameters< Hook extends keyof AstroIntegration['hooks'], Fn = AstroIntegration['hooks'][Hook] > = Fn extends (...args: any) => any ? Parameters[0] : never; diff --git a/packages/astro/src/integrations/index.ts b/packages/astro/src/integrations/index.ts index c931075b50c3..d306e7be3463 100644 --- a/packages/astro/src/integrations/index.ts +++ b/packages/astro/src/integrations/index.ts @@ -9,7 +9,7 @@ import type { AstroSettings, BuildConfig, ContentEntryType, - HookIntegrationParameters, + HookParameters, RouteData, } from '../@types/astro.js'; import type { SerializedSSRManifest } from '../core/app/types'; @@ -70,7 +70,7 @@ export async function runHookConfigSetup({ * ``` */ if (integration.hooks?.['astro:config:setup']) { - const hooks: HookIntegrationParameters<'astro:config:setup'> = { + const hooks: HookParameters<'astro:config:setup'> = { config: updatedConfig, command, isRestart, From c509da6d12248ff4384f60f1925935f0927c993d Mon Sep 17 00:00:00 2001 From: Emanuele Stoppa Date: Tue, 2 May 2023 17:32:37 +0100 Subject: [PATCH 42/53] refactor: call `renderPage` with props and params --- packages/astro/src/core/app/index.ts | 45 ++++----- packages/astro/src/core/build/generate.ts | 45 ++++----- packages/astro/src/core/endpoint/index.ts | 32 +++--- packages/astro/src/core/render/core.ts | 95 ++++++++++-------- packages/astro/src/core/render/dev/index.ts | 41 +++----- packages/astro/src/core/render/index.ts | 7 +- packages/astro/test/units/render/head.test.js | 60 +++++++++++- packages/astro/test/units/render/jsx.test.js | 61 +++++++++++- pnpm-lock.yaml | 97 ++++++++++++++++++- 9 files changed, 338 insertions(+), 145 deletions(-) diff --git a/packages/astro/src/core/app/index.ts b/packages/astro/src/core/app/index.ts index 35d688cd9ea8..f90da036cecf 100644 --- a/packages/astro/src/core/app/index.ts +++ b/packages/astro/src/core/app/index.ts @@ -4,7 +4,6 @@ import type { EndpointHandler, EndpointOutput, ManifestData, - MiddlewareHandler, MiddlewareResponseHandler, RouteData, SSRElement, @@ -22,8 +21,7 @@ import { createRenderContext, renderPage, type Environment, - getParamsAndProps, - GetParamsAndPropsError, + getParamsAndPropsOrThrow, } from '../render/index.js'; import { RouteCache } from '../render/route-cache.js'; import { @@ -34,7 +32,6 @@ import { import { matchRoute } from '../routing/match.js'; export { deserializeManifest } from './common.js'; import { callMiddleware } from '../middleware/index.js'; -import { AstroError, AstroErrorData } from '../errors/index.js'; const clientLocalsSymbol = Symbol.for('astro.locals'); @@ -213,25 +210,18 @@ export class App { route: routeData, status, }); - const paramsAndPropsResp = await getParamsAndProps({ - mod: mod as any, - route: renderContext.route, - routeCache: this.#env.routeCache, - pathname: renderContext.pathname, - logging: this.#env.logging, - ssr: this.#env.ssr, - }); - if (paramsAndPropsResp === GetParamsAndPropsError.NoMatchingStaticPath) { - throw new AstroError({ - ...AstroErrorData.NoMatchingStaticPathFound, - message: AstroErrorData.NoMatchingStaticPathFound.message(renderContext.pathname), - hint: renderContext.route?.component - ? AstroErrorData.NoMatchingStaticPathFound.hint([renderContext.route?.component]) - : '', - }); - } - const [params, props] = paramsAndPropsResp; + const [params, props] = await getParamsAndPropsOrThrow({ + options: { + mod: mod as any, + route: renderContext.route, + routeCache: this.#env.routeCache, + pathname: renderContext.pathname, + logging: this.#env.logging, + ssr: this.#env.ssr, + }, + context: renderContext, + }); const apiContext = createAPIContext({ request: renderContext.request, @@ -247,11 +237,18 @@ export class App { onRequest as MiddlewareResponseHandler, apiContext, () => { - return renderPage(mod, renderContext, this.#env, apiContext); + return renderPage({ mod, renderContext, env: this.#env, apiContext, props, params }); } ); } else { - response = await renderPage(mod, renderContext, this.#env, apiContext); + response = await renderPage({ + mod, + renderContext, + env: this.#env, + apiContext, + props, + params, + }); } Reflect.set(request, responseSentSymbol, true); return response; diff --git a/packages/astro/src/core/build/generate.ts b/packages/astro/src/core/build/generate.ts index 6639db1e628e..63d810d84a8d 100644 --- a/packages/astro/src/core/build/generate.ts +++ b/packages/astro/src/core/build/generate.ts @@ -38,9 +38,8 @@ import { AstroError, AstroErrorData } from '../errors/index.js'; import { debug, info } from '../logger/core.js'; import { createEnvironment, + getParamsAndPropsOrThrow, createRenderContext, - getParamsAndProps, - GetParamsAndPropsError, renderPage, } from '../render/index.js'; import { callGetStaticPaths } from '../render/route-cache.js'; @@ -432,7 +431,7 @@ async function generatePath( streaming: true, }); - const ctx = createRenderContext({ + const renderContext = createRenderContext({ origin, pathname, request: createRequest({ url, headers: new Headers(), logging, ssr }), @@ -450,7 +449,7 @@ async function generatePath( const result = await callEndpoint( endpointHandler, env, - ctx, + renderContext, logging, middleware as AstroMiddlewareInstance ); @@ -468,28 +467,20 @@ async function generatePath( } else { let response: Response; try { - const paramsAndPropsResp = await getParamsAndProps({ - mod: mod as any, - route: ctx.route, - routeCache: env.routeCache, - pathname: ctx.pathname, - logging: env.logging, - ssr: env.ssr, + const [params, props] = await getParamsAndPropsOrThrow({ + options: { + mod: mod as any, + route: renderContext.route, + routeCache: env.routeCache, + pathname: renderContext.pathname, + logging: env.logging, + ssr: env.ssr, + }, + context: renderContext, }); - 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, + const apiContext = createAPIContext({ + request: renderContext.request, params, props, site: env.site, @@ -500,13 +491,13 @@ async function generatePath( if (onRequest) { response = await callMiddleware( onRequest as MiddlewareResponseHandler, - context, + apiContext, () => { - return renderPage(mod, ctx, env, context); + return renderPage({ mod, renderContext, env, apiContext, params, props }); } ); } else { - response = await renderPage(mod, ctx, env, context); + response = await renderPage({ mod, renderContext, env, apiContext, params, props }); } } catch (err) { if (!AstroError.is(err) && !(err as SSRError).id && typeof err === 'object') { diff --git a/packages/astro/src/core/endpoint/index.ts b/packages/astro/src/core/endpoint/index.ts index 2468531c252f..be4b762dd73e 100644 --- a/packages/astro/src/core/endpoint/index.ts +++ b/packages/astro/src/core/endpoint/index.ts @@ -14,7 +14,7 @@ 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, isValueSerializable } from '../render/core.js'; +import { getParamsAndPropsOrThrow, isValueSerializable } from '../render/core.js'; import { callMiddleware } from '../middleware/index.js'; const clientAddressSymbol = Symbol.for('astro.clientAddress'); @@ -98,28 +98,20 @@ export async function call( env: Environment, ctx: RenderContext, logging: LogOptions, - middleware: AstroMiddlewareInstance | undefined + middleware?: AstroMiddlewareInstance | undefined ): Promise { - const paramsAndPropsResp = await getParamsAndProps({ - mod: mod as any, - route: ctx.route, - routeCache: env.routeCache, - pathname: ctx.pathname, - logging: env.logging, - ssr: env.ssr, + const [params, props] = await getParamsAndPropsOrThrow({ + options: { + mod: mod as any, + route: ctx.route, + routeCache: env.routeCache, + pathname: ctx.pathname, + logging: env.logging, + ssr: env.ssr, + }, + context: ctx, }); - 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, diff --git a/packages/astro/src/core/render/core.ts b/packages/astro/src/core/render/core.ts index 2a1e2d6c3564..59fd71fdfe8b 100644 --- a/packages/astro/src/core/render/core.ts +++ b/packages/astro/src/core/render/core.ts @@ -22,6 +22,32 @@ export const enum GetParamsAndPropsError { NoMatchingStaticPath, } +export type GetParamsAndPropsOrThrow = { + options: GetParamsAndPropsOptions; + context: RenderContext; +}; + +/** + * It retrieves `Params` and `Props`, or throws an error + * if they are not correctly retrieved. + */ +export async function getParamsAndPropsOrThrow({ + options, + context, +}: GetParamsAndPropsOrThrow): Promise<[Params, Props]> { + let paramsAndPropsResp = await getParamsAndProps(options); + if (paramsAndPropsResp === GetParamsAndPropsError.NoMatchingStaticPath) { + throw new AstroError({ + ...AstroErrorData.NoMatchingStaticPathFound, + message: AstroErrorData.NoMatchingStaticPathFound.message(context.pathname), + hint: context.route?.component + ? AstroErrorData.NoMatchingStaticPathFound.hint([context.route?.component]) + : '', + }); + } + return paramsAndPropsResp; +} + export async function getParamsAndProps( opts: GetParamsAndPropsOptions ): Promise<[Params, Props] | GetParamsAndPropsError> { @@ -83,32 +109,25 @@ export async function getParamsAndProps( return [params, pageProps]; } -export async function renderPage( - mod: ComponentInstance, - ctx: RenderContext, - env: Environment, - apiContext?: APIContext -) { - 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 async function renderPageWithParamsAndProps() {} +export type RenderPage = { + mod: ComponentInstance; + renderContext: RenderContext; + env: Environment; + apiContext?: APIContext; + props: Props; + params: Params; +}; + +export async function renderPage({ + mod, + renderContext, + env, + apiContext, + params, + props, +}: RenderPage) { // Validate the page component before rendering the page const Component = mod.default; if (!Component) @@ -119,45 +138,45 @@ export async function renderPage( if (env.mode === 'development' && !isValueSerializable(apiContext.locals)) { throw new AstroError({ ...AstroErrorData.LocalsNotSerializable, - message: AstroErrorData.LocalsNotSerializable.message(ctx.pathname), + 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, + origin: renderContext.origin, params, - props: pageProps, - pathname: ctx.pathname, - componentMetadata: ctx.componentMetadata, + 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(props, { components: (mod as any).components }); } let response = await runtimeRenderPage( result, Component, - pageProps, + props, null, env.streaming, - ctx.route + renderContext.route ); // If there is an Astro.cookies instance, attach it to the response so that diff --git a/packages/astro/src/core/render/dev/index.ts b/packages/astro/src/core/render/dev/index.ts index 4613ba4eec00..35365a3e4539 100644 --- a/packages/astro/src/core/render/dev/index.ts +++ b/packages/astro/src/core/render/dev/index.ts @@ -21,8 +21,7 @@ import type { ModuleLoader } from '../../module-loader/index'; import { isPage, resolveIdToUrl, viteID } from '../../util.js'; import { createRenderContext, - getParamsAndProps, - GetParamsAndPropsError, + getParamsAndPropsOrThrow, renderPage as coreRenderPage, } from '../index.js'; import { filterFoundRenderers, loadRenderer } from '../renderer.js'; @@ -31,7 +30,7 @@ import type { DevelopmentEnvironment } from './environment'; import { getComponentMetadata } from './metadata.js'; import { getScriptsForURL } from './scripts.js'; import { createAPIContext } from '../../endpoint/index.js'; -import { sequence, callMiddleware } from '../../middleware/index.js'; +import { callMiddleware } from '../../middleware/index.js'; export { createDevelopmentEnvironment } from './environment.js'; export type { DevelopmentEnvironment }; @@ -178,7 +177,7 @@ export async function renderPage(options: SSROptions): Promise { filePath: options.filePath, }); - const ctx = createRenderContext({ + const renderContext = createRenderContext({ request: options.request, origin: options.origin, pathname: options.pathname, @@ -189,44 +188,34 @@ export async function renderPage(options: SSROptions): Promise { route: options.route, }); - if (options.middleware) { - const { env } = options; - const paramsAndPropsRes = await getParamsAndProps({ + const { env } = options; + const [params, props] = await getParamsAndPropsOrThrow({ + options: { logging: env.logging, mod, - route: ctx.route, + route: renderContext.route, routeCache: env.routeCache, - pathname: ctx.pathname, + pathname: renderContext.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]) - : '', - }); - } - + }, + context: renderContext, + }); + if (options.middleware) { if (options.middleware && options.middleware.onRequest) { - const [params, pageProps] = paramsAndPropsRes; - const apiContext = createAPIContext({ request: options.request, params, - props: pageProps, + props, adapterName: options.env.adapterName, }); const onRequest = options.middleware.onRequest as MiddlewareResponseHandler; const response = await callMiddleware(onRequest, apiContext, () => { - return coreRenderPage(mod, ctx, options.env, apiContext); + return coreRenderPage({ mod, renderContext, env: options.env, apiContext, params, props }); }); return response; } } - return await coreRenderPage(mod, ctx, options.env); // NOTE: without "await", errors won’t get caught below + return await coreRenderPage({ mod, renderContext, env: options.env, params, props }); // 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 99d68054911b..a25fa0ec3feb 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, + renderPage, + getParamsAndPropsOrThrow, +} from './core.js'; export type { Environment } from './environment'; export { createBasicEnvironment, createEnvironment } from './environment.js'; export { loadRenderer } from './renderer.js'; diff --git a/packages/astro/test/units/render/head.test.js b/packages/astro/test/units/render/head.test.js index 103c84fda353..3976964fc8df 100644 --- a/packages/astro/test/units/render/head.test.js +++ b/packages/astro/test/units/render/head.test.js @@ -12,6 +12,7 @@ import { import { createBasicEnvironment, createRenderContext, + getParamsAndPropsOrThrow, renderPage, } from '../../../dist/core/render/index.js'; import { defaultLogging as logging } from '../../test-utils.js'; @@ -101,7 +102,24 @@ describe('core/render', () => { }); const PageModule = createAstroModule(Page); - const response = await renderPage(PageModule, ctx, env); + const [params, props] = await getParamsAndPropsOrThrow({ + options: { + logging: env.logging, + mod: PageModule, + route: ctx.route, + routeCache: env.routeCache, + pathname: ctx.pathname, + ssr: env.ssr, + }, + context: ctx, + }); + const response = await renderPage({ + mod: PageModule, + renderContext: ctx, + env, + params, + props, + }); const html = await response.text(); const $ = cheerio.load(html); @@ -179,8 +197,24 @@ describe('core/render', () => { }); const PageModule = createAstroModule(Page); - const response = await renderPage(PageModule, ctx, env); - + const [params, props] = await getParamsAndPropsOrThrow({ + options: { + logging: env.logging, + mod: PageModule, + route: ctx.route, + routeCache: env.routeCache, + pathname: ctx.pathname, + ssr: env.ssr, + }, + context: ctx, + }); + const response = await renderPage({ + mod: PageModule, + renderContext: ctx, + env, + params, + props, + }); const html = await response.text(); const $ = cheerio.load(html); @@ -224,8 +258,24 @@ describe('core/render', () => { }); const PageModule = createAstroModule(Page); - const response = await renderPage(PageModule, ctx, env); - + const [params, props] = await getParamsAndPropsOrThrow({ + options: { + logging: env.logging, + mod: PageModule, + route: ctx.route, + routeCache: env.routeCache, + pathname: ctx.pathname, + ssr: env.ssr, + }, + context: ctx, + }); + const response = await renderPage({ + mod: PageModule, + renderContext: ctx, + env, + params, + 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 a34b6b53b525..8211250b86b7 100644 --- a/packages/astro/test/units/render/jsx.test.js +++ b/packages/astro/test/units/render/jsx.test.js @@ -12,6 +12,7 @@ import { createRenderContext, renderPage, loadRenderer, + getParamsAndPropsOrThrow, } from '../../../dist/core/render/index.js'; import { createAstroJSXComponent, renderer as jsxRenderer } from '../../../dist/jsx/index.js'; import { defaultLogging as logging } from '../../test-utils.js'; @@ -47,7 +48,25 @@ 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 [params, props] = await getParamsAndPropsOrThrow({ + options: { + logging: env.logging, + mod, + route: ctx.route, + routeCache: env.routeCache, + pathname: ctx.pathname, + ssr: env.ssr, + }, + context: ctx, + }); + const response = await renderPage({ + mod, + renderContext: ctx, + env, + params, + props, + }); expect(response.status).to.equal(200); @@ -86,7 +105,25 @@ 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 [params, props] = await getParamsAndPropsOrThrow({ + options: { + logging: env.logging, + mod, + route: ctx.route, + routeCache: env.routeCache, + pathname: ctx.pathname, + ssr: env.ssr, + }, + context: ctx, + }); + const response = await renderPage({ + mod, + renderContext: ctx, + env, + params, + props, + }); expect(response.status).to.equal(200); @@ -106,7 +143,25 @@ 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 [params, props] = await getParamsAndPropsOrThrow({ + options: { + logging: env.logging, + mod, + route: ctx.route, + routeCache: env.routeCache, + pathname: ctx.pathname, + ssr: env.ssr, + }, + context: ctx, + }); + const response = await renderPage({ + mod, + renderContext: ctx, + env, + params, + props, + }); try { await response.text(); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bf43c28474cd..64a738ecef03 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -361,6 +361,30 @@ importers: specifier: ^2.3.2 version: link:../../packages/astro + examples/middleware: + dependencies: + '@astrojs/node': + specifier: workspace:* + version: link:../../packages/integrations/node + astro: + specifier: workspace:* + version: link:../../packages/astro + concurrently: + specifier: ^7.2.1 + version: 7.2.1 + html-minifier: + specifier: ^4.0.0 + version: 4.0.0 + svelte: + specifier: ^3.48.0 + version: 3.58.0 + unocss: + specifier: ^0.15.6 + version: 0.15.6 + vite-imagetools: + specifier: ^4.0.4 + version: 4.0.4 + examples/minimal: dependencies: astro: @@ -2738,6 +2762,18 @@ importers: specifier: workspace:* version: link:../../.. + packages/astro/test/fixtures/middleware-dev: + dependencies: + astro: + specifier: workspace:* + version: link:../../.. + + packages/astro/test/fixtures/middleware-ssg: + dependencies: + astro: + specifier: workspace:* + version: link:../../.. + packages/astro/test/fixtures/multiple-renderers: dependencies: '@test/astro-renderer-one': @@ -8932,7 +8968,7 @@ packages: chokidar: 3.5.3 colorette: 2.0.19 consola: 2.15.3 - fast-glob: 3.2.11 + fast-glob: 3.2.12 pathe: 0.2.0 dev: false @@ -9833,6 +9869,13 @@ packages: engines: {node: '>=6'} dev: true + /camel-case@3.0.0: + resolution: {integrity: sha512-+MbKztAYHXPr1jNTSKQF52VpcFjwY5RkR7fxksV8Doo4KAYc5Fl4UJRgthBbTmEx8C54DqahhbLJkDwjI3PI/w==} + dependencies: + no-case: 2.3.2 + upper-case: 1.1.3 + dev: false + /camelcase-css@2.0.1: resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==} engines: {node: '>= 6'} @@ -9997,6 +10040,13 @@ packages: /ci-info@3.3.1: resolution: {integrity: sha512-SXgeMX9VwDe7iFFaEWkA5AstuER9YKqy4EhHqr4DVqkwmD9rpVimkMKWHdjn30Ja45txyjhSn63lVX69eVCckg==} + /clean-css@4.2.4: + resolution: {integrity: sha512-EJUDT7nDVFDvaQgAo2G/PJvxmp1o/c6iXLbswsBbUFXi1Nr+AjA2cKmfbKDMjMvzEe75g3P6JkaDDAKk96A85A==} + engines: {node: '>= 4.0'} + dependencies: + source-map: 0.6.1 + dev: false + /clean-stack@4.2.0: resolution: {integrity: sha512-LYv6XPxoyODi36Dp976riBtSY27VmFo+MKqEU9QCCWyTrdEPDog+RWA7xQWHi6Vbp61j5c4cdzzX1NidnwtUWg==} engines: {node: '>=12'} @@ -12333,6 +12383,20 @@ packages: /html-escaper@3.0.3: resolution: {integrity: sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ==} + /html-minifier@4.0.0: + resolution: {integrity: sha512-aoGxanpFPLg7MkIl/DDFYtb0iWz7jMFGqFhvEDZga6/4QTjneiD8I/NXL1x5aaoCp7FSIT6h/OhykDdPsbtMig==} + engines: {node: '>=6'} + hasBin: true + dependencies: + camel-case: 3.0.0 + clean-css: 4.2.4 + commander: 2.20.3 + he: 1.2.0 + param-case: 2.1.1 + relateurl: 0.2.7 + uglify-js: 3.17.4 + dev: false + /html-rewriter-wasm@0.4.1: resolution: {integrity: sha512-lNovG8CMCCmcVB1Q7xggMSf7tqPCijZXaH4gL6iE8BFghdQCbaY5Met9i1x2Ex8m/cZHDUtXK9H6/znKamRP8Q==} dev: true @@ -13162,6 +13226,10 @@ packages: dependencies: get-func-name: 2.0.0 + /lower-case@1.1.4: + resolution: {integrity: sha512-2Fgx1Ycm599x+WGpIYwJOvsjmXFzTSc34IwDWALRA/8AopUKAVPwfJ+h5+f85BCp0PWmmJcWzEpxOpoXycMpdA==} + dev: false + /lower-case@2.0.2: resolution: {integrity: sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==} dependencies: @@ -14107,6 +14175,12 @@ packages: '@types/nlcst': 1.0.0 dev: false + /no-case@2.3.2: + resolution: {integrity: sha512-rmTZ9kz+f3rCvK2TD1Ue/oZlns7OGoIWP4fc3llxxRXlOkHKoWPPWJOfFYpITabSow43QJbRIoHQXtt10VldyQ==} + dependencies: + lower-case: 1.1.4 + dev: false + /no-case@3.0.4: resolution: {integrity: sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==} dependencies: @@ -14482,6 +14556,12 @@ packages: resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==} dev: false + /param-case@2.1.1: + resolution: {integrity: sha512-eQE845L6ot89sk2N8liD8HAuH4ca6Vvr7VWAWwt7+kvvG5aBcPmmphQ68JsEG2qa9n1TykS2DLeMt363AAH8/w==} + dependencies: + no-case: 2.3.2 + dev: false + /parent-module@1.0.1: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} engines: {node: '>=6'} @@ -15510,6 +15590,11 @@ packages: resolution: {integrity: sha512-QIRet3SYrGp0HUHO88jVskiG6seqUGC5iAG7AwI/BV4ypGcuqk9Du6YQBUOUqm9c8pw1eyLoIaONifRua1lsEQ==} dev: false + /relateurl@0.2.7: + resolution: {integrity: sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog==} + engines: {node: '>= 0.10'} + dev: false + /remark-code-titles@0.1.2: resolution: {integrity: sha512-KsHQbaI4FX8Ozxqk7YErxwmBiveUqloKuVqyPG2YPLHojpgomodWgRfG4B+bOtmn/5bfJ8khw4rR0lvgVFl2Uw==} dependencies: @@ -16895,6 +16980,12 @@ packages: engines: {node: '>=12.20'} hasBin: true + /uglify-js@3.17.4: + resolution: {integrity: sha512-T9q82TJI9e/C1TAxYvfb16xO120tMVFZrGA3f9/P4424DNu6ypK103y0GPFVa17yotwSyZW5iYXgjYHkGrJW/g==} + engines: {node: '>=0.8.0'} + hasBin: true + dev: false + /uhyphen@0.1.0: resolution: {integrity: sha512-o0QVGuFg24FK765Qdd5kk0zU/U4dEsCtN/GSiwNI9i8xsSVtjIAOdTaVhLwZ1nrbWxFVMxNDDl+9fednsOMsBw==} dev: true @@ -17134,6 +17225,10 @@ packages: escalade: 3.1.1 picocolors: 1.0.0 + /upper-case@1.1.3: + resolution: {integrity: sha512-WRbjgmYzgXkCV7zNVpy5YgrHgbBv126rMALQQMrmzOVC4GM2waQ9x7xtm8VU+1yF2kWyPzI9zbZ48n4vSxwfSA==} + dev: false + /uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} dependencies: From f9ae85a5a871f97fdcd1eee2a217c26b1b541c59 Mon Sep 17 00:00:00 2001 From: Emanuele Stoppa Date: Tue, 2 May 2023 17:43:39 +0100 Subject: [PATCH 43/53] fix: middleware casting --- packages/astro/src/core/app/index.ts | 2 +- packages/astro/src/core/endpoint/dev/index.ts | 3 --- packages/astro/src/core/endpoint/index.ts | 4 ++-- 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/packages/astro/src/core/app/index.ts b/packages/astro/src/core/app/index.ts index f90da036cecf..639e3a750a6f 100644 --- a/packages/astro/src/core/app/index.ts +++ b/packages/astro/src/core/app/index.ts @@ -284,7 +284,7 @@ export class App { this.#env, ctx, this.#logging, - this.#manifest.middleware as AstroMiddlewareInstance + this.#manifest.middleware ); if (result.type === 'response') { diff --git a/packages/astro/src/core/endpoint/dev/index.ts b/packages/astro/src/core/endpoint/dev/index.ts index cb02b501b0c0..14432ca3a0e5 100644 --- a/packages/astro/src/core/endpoint/dev/index.ts +++ b/packages/astro/src/core/endpoint/dev/index.ts @@ -7,9 +7,6 @@ import type { LogOptions } from '../../logger/core'; import type { SSROptions } from '../../render/dev'; import { createRenderContext } from '../../render/index.js'; import { call as callEndpoint } from '../index.js'; -import { renderEndpoint } from '../../../runtime/server'; -import { isValueSerializable } from '../../render/core'; -import { AstroError, AstroErrorData } from '../../errors'; export async function call(options: SSROptions, logging: LogOptions) { const { diff --git a/packages/astro/src/core/endpoint/index.ts b/packages/astro/src/core/endpoint/index.ts index be4b762dd73e..ab282718ec02 100644 --- a/packages/astro/src/core/endpoint/index.ts +++ b/packages/astro/src/core/endpoint/index.ts @@ -93,12 +93,12 @@ export function createAPIContext({ return context; } -export async function call( +export async function call( mod: EndpointHandler, env: Environment, ctx: RenderContext, logging: LogOptions, - middleware?: AstroMiddlewareInstance | undefined + middleware?: AstroMiddlewareInstance | undefined ): Promise { const [params, props] = await getParamsAndPropsOrThrow({ options: { From b190c5e8f377d0af11ca7bf49ee46185a08c647e Mon Sep 17 00:00:00 2001 From: Emanuele Stoppa Date: Tue, 2 May 2023 17:50:28 +0100 Subject: [PATCH 44/53] chore: code suggestions --- packages/astro/middleware.d.ts | 22 ------------------- packages/astro/middleware.mjs | 1 - packages/astro/package.json | 4 ++-- packages/astro/src/core/app/index.ts | 2 +- packages/astro/src/core/build/generate.ts | 2 +- packages/astro/src/core/endpoint/index.ts | 2 +- packages/astro/src/core/middleware/index.ts | 5 ++--- packages/astro/src/core/render/dev/index.ts | 2 +- .../src/vite-plugin-astro-server/route.ts | 2 +- 9 files changed, 9 insertions(+), 33 deletions(-) delete mode 100644 packages/astro/middleware.d.ts delete mode 100644 packages/astro/middleware.mjs diff --git a/packages/astro/middleware.d.ts b/packages/astro/middleware.d.ts deleted file mode 100644 index 278d02da96c8..000000000000 --- a/packages/astro/middleware.d.ts +++ /dev/null @@ -1,22 +0,0 @@ -import type { APIContext, MiddlewareNext, MiddlewareResponseHandler } from './src/@types/astro'; - -/** - * Utility function to join multiple middleware functions together - */ -export function sequence(...handlers: MiddlewareResponseHandler[]): MiddlewareResponseHandler; - -/** - * Utility function to quickly type a middleware - * - * ## Example - * - * ```ts - * import {defineMiddleware} from "astro/middleware" - * - * const onRequest = defineMiddleware(async (context, next) => { - * - * return await next(); - * }) - * ``` - */ -export function defineMiddleware(fn: MiddlewareResponseHandler): MiddlewareResponseHandler; diff --git a/packages/astro/middleware.mjs b/packages/astro/middleware.mjs deleted file mode 100644 index c7c873763c8b..000000000000 --- a/packages/astro/middleware.mjs +++ /dev/null @@ -1 +0,0 @@ -export { sequence, defineMiddleware } from './dist/core/middleware/index.js'; diff --git a/packages/astro/package.json b/packages/astro/package.json index cdc7591382cf..96e59912fb1b 100644 --- a/packages/astro/package.json +++ b/packages/astro/package.json @@ -66,8 +66,8 @@ "default": "./zod.mjs" }, "./middleware": { - "default": "./middleware.mjs", - "types": "./middleware.d.ts" + "types": "./dist/core/middleware/index.d.ts", + "default": "./dist/core/middleware/index.js" } }, "imports": { diff --git a/packages/astro/src/core/app/index.ts b/packages/astro/src/core/app/index.ts index 639e3a750a6f..fac2085d9cb8 100644 --- a/packages/astro/src/core/app/index.ts +++ b/packages/astro/src/core/app/index.ts @@ -31,7 +31,7 @@ import { } from '../render/ssr-element.js'; import { matchRoute } from '../routing/match.js'; export { deserializeManifest } from './common.js'; -import { callMiddleware } from '../middleware/index.js'; +import { callMiddleware } from '../middleware/callMiddleware.js'; const clientLocalsSymbol = Symbol.for('astro.locals'); diff --git a/packages/astro/src/core/build/generate.ts b/packages/astro/src/core/build/generate.ts index 63d810d84a8d..8fa6a1317899 100644 --- a/packages/astro/src/core/build/generate.ts +++ b/packages/astro/src/core/build/generate.ts @@ -55,7 +55,7 @@ import { getOutDirWithinCwd, getOutFile, getOutFolder } from './common.js'; import { eachPageData, getPageDataByComponent, sortedCSS } from './internal.js'; import type { PageBuildData, SingleFileBuiltModule, StaticBuildOptions } from './types'; import { getTimeStat } from './util.js'; -import { callMiddleware } from '../middleware/index.js'; +import { callMiddleware } from '../middleware/callMiddleware.js'; function shouldSkipDraft(pageModule: ComponentInstance, settings: AstroSettings): boolean { return ( diff --git a/packages/astro/src/core/endpoint/index.ts b/packages/astro/src/core/endpoint/index.ts index ab282718ec02..4abc80c5eb06 100644 --- a/packages/astro/src/core/endpoint/index.ts +++ b/packages/astro/src/core/endpoint/index.ts @@ -15,7 +15,7 @@ import { AstroCookies, attachToResponse } from '../cookies/index.js'; import { AstroError, AstroErrorData } from '../errors/index.js'; import { warn, type LogOptions } from '../logger/core.js'; import { getParamsAndPropsOrThrow, isValueSerializable } from '../render/core.js'; -import { callMiddleware } from '../middleware/index.js'; +import { callMiddleware } from '../middleware/callMiddleware.js'; const clientAddressSymbol = Symbol.for('astro.clientAddress'); const clientLocalsSymbol = Symbol.for('astro.locals'); diff --git a/packages/astro/src/core/middleware/index.ts b/packages/astro/src/core/middleware/index.ts index ed063eb4d7ec..ad409a5523e9 100644 --- a/packages/astro/src/core/middleware/index.ts +++ b/packages/astro/src/core/middleware/index.ts @@ -1,10 +1,9 @@ import { sequence } from './sequence.js'; -import { callMiddleware } from './callMiddleware.js'; -import { loadMiddleware } from './loadMiddleware.js'; import type { MiddlewareResponseHandler } from '../../@types/astro'; function defineMiddleware(fn: MiddlewareResponseHandler) { return fn; } -export { sequence, callMiddleware, loadMiddleware, defineMiddleware }; +// 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/render/dev/index.ts b/packages/astro/src/core/render/dev/index.ts index 35365a3e4539..edfc14eed8e5 100644 --- a/packages/astro/src/core/render/dev/index.ts +++ b/packages/astro/src/core/render/dev/index.ts @@ -30,7 +30,7 @@ import type { DevelopmentEnvironment } from './environment'; import { getComponentMetadata } from './metadata.js'; import { getScriptsForURL } from './scripts.js'; import { createAPIContext } from '../../endpoint/index.js'; -import { callMiddleware } from '../../middleware/index.js'; +import { callMiddleware } from '../../middleware/callMiddleware.js'; export { createDevelopmentEnvironment } from './environment.js'; export type { DevelopmentEnvironment }; diff --git a/packages/astro/src/vite-plugin-astro-server/route.ts b/packages/astro/src/vite-plugin-astro-server/route.ts index 94cd6b3268a2..8942671eca81 100644 --- a/packages/astro/src/vite-plugin-astro-server/route.ts +++ b/packages/astro/src/vite-plugin-astro-server/route.ts @@ -17,7 +17,7 @@ import { createRequest } from '../core/request.js'; import { matchAllRoutes } from '../core/routing/index.js'; import { log404 } from './common.js'; import { handle404Response, writeSSRResult, writeWebResponse } from './response.js'; -import { loadMiddleware } from '../core/middleware/index.js'; +import { loadMiddleware } from '../core/middleware/loadMiddleware.js'; type AsyncReturnType Promise> = T extends ( ...args: any From 1daa1093cce0f4d44ca9be8b5c4dc9541821adc3 Mon Sep 17 00:00:00 2001 From: Emanuele Stoppa Date: Tue, 2 May 2023 18:05:23 +0100 Subject: [PATCH 45/53] revert: hook parameter rename --- packages/integrations/markdoc/src/index.ts | 9 ++------- packages/integrations/mdx/src/index.ts | 4 ++-- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/packages/integrations/markdoc/src/index.ts b/packages/integrations/markdoc/src/index.ts index 11024a4528e1..55d13169b7d0 100644 --- a/packages/integrations/markdoc/src/index.ts +++ b/packages/integrations/markdoc/src/index.ts @@ -1,11 +1,6 @@ import type { Node } from '@markdoc/markdoc'; import Markdoc from '@markdoc/markdoc'; -import type { - AstroConfig, - AstroIntegration, - ContentEntryType, - HookIntegrationParameters, -} from 'astro'; +import type { AstroConfig, AstroIntegration, ContentEntryType, HookParameters } from 'astro'; import fs from 'node:fs'; import { fileURLToPath } from 'node:url'; import { isValidUrl, MarkdocError, parseFrontmatter, prependForwardSlash } from './utils.js'; @@ -16,7 +11,7 @@ import type * as rollup from 'rollup'; import { applyDefaultConfig } from './default-config.js'; import { loadMarkdocConfig } from './load-config.js'; -type SetupHookParams = HookIntegrationParameters<'astro:config:setup'> & { +type SetupHookParams = HookParameters<'astro:config:setup'> & { // `contentEntryType` is not a public API // Add type defs here addContentEntryType: (contentEntryType: ContentEntryType) => void; diff --git a/packages/integrations/mdx/src/index.ts b/packages/integrations/mdx/src/index.ts index 42ff50a21f19..2ccf6626654b 100644 --- a/packages/integrations/mdx/src/index.ts +++ b/packages/integrations/mdx/src/index.ts @@ -3,7 +3,7 @@ import { toRemarkInitializeAstroData } from '@astrojs/markdown-remark/dist/inter import { compile as mdxCompile } from '@mdx-js/mdx'; import type { PluggableList } from '@mdx-js/mdx/lib/core.js'; import mdxPlugin, { type Options as MdxRollupPluginOptions } from '@mdx-js/rollup'; -import type { AstroIntegration, ContentEntryType, HookIntegrationParameters } from 'astro'; +import type { AstroIntegration, ContentEntryType, HookParameters } from 'astro'; import { parse as parseESM } from 'es-module-lexer'; import fs from 'node:fs/promises'; import { fileURLToPath } from 'node:url'; @@ -24,7 +24,7 @@ export type MdxOptions = Omit & { +type SetupHookParams = HookParameters<'astro:config:setup'> & { // `addPageExtension` and `contentEntryType` are not a public APIs // Add type defs here addPageExtension: (extension: string) => void; From a97d9d90c6cfa3b561d4b48c3f4ae784107d2d6d Mon Sep 17 00:00:00 2001 From: Emanuele Stoppa Date: Wed, 3 May 2023 09:33:19 +0100 Subject: [PATCH 46/53] refactor: move `props` and `params` inside `RenderContext` --- packages/astro/src/core/app/index.ts | 28 +++----- packages/astro/src/core/build/generate.ts | 32 +++------- packages/astro/src/core/endpoint/dev/index.ts | 18 ++---- packages/astro/src/core/endpoint/index.ts | 18 +----- packages/astro/src/core/render/context.ts | 29 ++++++++- packages/astro/src/core/render/core.ts | 39 ++++------- packages/astro/src/core/render/dev/index.ts | 40 +++--------- .../fixtures/middleware-dev/astro.config.mjs | 1 - packages/astro/test/units/render/head.test.js | 64 ++++++------------- packages/astro/test/units/render/jsx.test.js | 55 ++++------------ 10 files changed, 105 insertions(+), 219 deletions(-) diff --git a/packages/astro/src/core/app/index.ts b/packages/astro/src/core/app/index.ts index fac2085d9cb8..01df1ea706b5 100644 --- a/packages/astro/src/core/app/index.ts +++ b/packages/astro/src/core/app/index.ts @@ -200,7 +200,7 @@ export class App { } try { - const renderContext = createRenderContext({ + const renderContext = await createRenderContext({ request, origin: url.origin, pathname, @@ -209,24 +209,14 @@ export class App { links, route: routeData, status, - }); - - const [params, props] = await getParamsAndPropsOrThrow({ - options: { - mod: mod as any, - route: renderContext.route, - routeCache: this.#env.routeCache, - pathname: renderContext.pathname, - logging: this.#env.logging, - ssr: this.#env.ssr, - }, - context: renderContext, + mod: mod as any, + env: this.#env, }); const apiContext = createAPIContext({ request: renderContext.request, - params, - props, + params: renderContext.params, + props: renderContext.props, site: this.#env.site, adapterName: this.#env.adapterName, }); @@ -237,7 +227,7 @@ export class App { onRequest as MiddlewareResponseHandler, apiContext, () => { - return renderPage({ mod, renderContext, env: this.#env, apiContext, props, params }); + return renderPage({ mod, renderContext, env: this.#env, apiContext }); } ); } else { @@ -246,8 +236,6 @@ export class App { renderContext, env: this.#env, apiContext, - props, - params, }); } Reflect.set(request, responseSentSymbol, true); @@ -271,12 +259,14 @@ 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( diff --git a/packages/astro/src/core/build/generate.ts b/packages/astro/src/core/build/generate.ts index 8fa6a1317899..d3afa34a37c6 100644 --- a/packages/astro/src/core/build/generate.ts +++ b/packages/astro/src/core/build/generate.ts @@ -10,7 +10,6 @@ import type { ComponentInstance, EndpointHandler, ImageTransform, - MiddlewareHandler, RouteType, SSRError, SSRLoadedRenderer, @@ -36,12 +35,7 @@ import { } from '../endpoint/index.js'; import { AstroError, AstroErrorData } from '../errors/index.js'; import { debug, info } from '../logger/core.js'; -import { - createEnvironment, - getParamsAndPropsOrThrow, - createRenderContext, - renderPage, -} from '../render/index.js'; +import { createEnvironment, createRenderContext, renderPage } from '../render/index.js'; import { callGetStaticPaths } from '../render/route-cache.js'; import { createAssetLink, @@ -431,7 +425,7 @@ async function generatePath( streaming: true, }); - const renderContext = createRenderContext({ + const renderContext = await createRenderContext({ origin, pathname, request: createRequest({ url, headers: new Headers(), logging, ssr }), @@ -439,6 +433,8 @@ async function generatePath( scripts, links, route: pageData.route, + env, + mod, }); let body: string | Uint8Array; @@ -467,22 +463,10 @@ async function generatePath( } else { let response: Response; try { - const [params, props] = await getParamsAndPropsOrThrow({ - options: { - mod: mod as any, - route: renderContext.route, - routeCache: env.routeCache, - pathname: renderContext.pathname, - logging: env.logging, - ssr: env.ssr, - }, - context: renderContext, - }); - const apiContext = createAPIContext({ request: renderContext.request, - params, - props, + params: renderContext.params, + props: renderContext.props, site: env.site, adapterName: env.adapterName, }); @@ -493,11 +477,11 @@ async function generatePath( onRequest as MiddlewareResponseHandler, apiContext, () => { - return renderPage({ mod, renderContext, env, apiContext, params, props }); + return renderPage({ mod, renderContext, env, apiContext }); } ); } else { - response = await renderPage({ mod, renderContext, env, apiContext, params, props }); + response = await renderPage({ mod, renderContext, env, apiContext }); } } catch (err) { if (!AstroError.is(err) && !(err as SSRError).id && typeof err === 'object') { diff --git a/packages/astro/src/core/endpoint/dev/index.ts b/packages/astro/src/core/endpoint/dev/index.ts index 14432ca3a0e5..24d2aa3bda87 100644 --- a/packages/astro/src/core/endpoint/dev/index.ts +++ b/packages/astro/src/core/endpoint/dev/index.ts @@ -1,8 +1,4 @@ -import type { - AstroMiddlewareInstance, - EndpointHandler, - EndpointOutput, -} from '../../../@types/astro'; +import type { EndpointHandler } from '../../../@types/astro'; import type { LogOptions } from '../../logger/core'; import type { SSROptions } from '../../render/dev'; import { createRenderContext } from '../../render/index.js'; @@ -16,18 +12,14 @@ export async function call(options: SSROptions, logging: LogOptions) { } = 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, - middleware as AstroMiddlewareInstance - ); + 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 4abc80c5eb06..af9ecb01c84a 100644 --- a/packages/astro/src/core/endpoint/index.ts +++ b/packages/astro/src/core/endpoint/index.ts @@ -14,7 +14,7 @@ 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 { getParamsAndPropsOrThrow, isValueSerializable } from '../render/core.js'; +import { isValueSerializable } from '../render/core.js'; import { callMiddleware } from '../middleware/callMiddleware.js'; const clientAddressSymbol = Symbol.for('astro.clientAddress'); @@ -100,22 +100,10 @@ export async function call( logging: LogOptions, middleware?: AstroMiddlewareInstance | undefined ): Promise { - const [params, props] = await getParamsAndPropsOrThrow({ - options: { - mod: mod as any, - route: ctx.route, - routeCache: env.routeCache, - pathname: ctx.pathname, - logging: env.logging, - ssr: env.ssr, - }, - context: ctx, - }); - const context = createAPIContext({ request: ctx.request, - params, - props, + params: ctx.params, + props: ctx.props, site: env.site, adapterName: env.adapterName, }); diff --git a/packages/astro/src/core/render/context.ts b/packages/astro/src/core/render/context.ts index f6a82e9ca439..d4efe35df3b7 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 & { origin?: string; request: RenderContext['request']; + mod: ComponentInstance; + env: Environment; }; -export function createRenderContext(options: CreateRenderContextArgs): RenderContext { +export async function createRenderContext( + options: CreateRenderContextArgs +): Promise { 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 59fd71fdfe8b..b396bed6a811 100644 --- a/packages/astro/src/core/render/core.ts +++ b/packages/astro/src/core/render/core.ts @@ -22,26 +22,20 @@ export const enum GetParamsAndPropsError { NoMatchingStaticPath, } -export type GetParamsAndPropsOrThrow = { - options: GetParamsAndPropsOptions; - context: RenderContext; -}; - /** * It retrieves `Params` and `Props`, or throws an error * if they are not correctly retrieved. */ -export async function getParamsAndPropsOrThrow({ - options, - context, -}: GetParamsAndPropsOrThrow): Promise<[Params, Props]> { +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(context.pathname), - hint: context.route?.component - ? AstroErrorData.NoMatchingStaticPathFound.hint([context.route?.component]) + message: AstroErrorData.NoMatchingStaticPathFound.message(options.pathname), + hint: options.route?.component + ? AstroErrorData.NoMatchingStaticPathFound.hint([options.route?.component]) : '', }); } @@ -109,25 +103,14 @@ export async function getParamsAndProps( return [params, pageProps]; } -export async function renderPageWithParamsAndProps() {} - export type RenderPage = { mod: ComponentInstance; renderContext: RenderContext; env: Environment; apiContext?: APIContext; - props: Props; - params: Params; }; -export async function renderPage({ - mod, - renderContext, - env, - apiContext, - params, - props, -}: RenderPage) { +export async function renderPage({ mod, renderContext, env, apiContext }: RenderPage) { // Validate the page component before rendering the page const Component = mod.default; if (!Component) @@ -151,8 +134,8 @@ export async function renderPage({ markdown: env.markdown, mode: env.mode, origin: renderContext.origin, - params, - props, + params: renderContext.params, + props: renderContext.props, pathname: renderContext.pathname, componentMetadata: renderContext.componentMetadata, resolve: env.resolve, @@ -167,13 +150,13 @@ export async function renderPage({ // Support `export const components` for `MDX` pages if (typeof (mod as any).components === 'object') { - Object.assign(props, { components: (mod as any).components }); + Object.assign(renderContext.props, { components: (mod as any).components }); } let response = await runtimeRenderPage( result, Component, - props, + renderContext.props, null, env.streaming, renderContext.route diff --git a/packages/astro/src/core/render/dev/index.ts b/packages/astro/src/core/render/dev/index.ts index edfc14eed8e5..edbf1595d67a 100644 --- a/packages/astro/src/core/render/dev/index.ts +++ b/packages/astro/src/core/render/dev/index.ts @@ -10,20 +10,10 @@ import type { } from '../../../@types/astro'; import { PAGE_SCRIPT_ID } from '../../../vite-plugin-scripts/index.js'; import { enhanceViteSSRError } from '../../errors/dev/index.js'; -import { - AggregateError, - AstroError, - AstroErrorData, - CSSError, - MarkdownError, -} from '../../errors/index.js'; +import { AggregateError, CSSError, MarkdownError } from '../../errors/index.js'; import type { ModuleLoader } from '../../module-loader/index'; import { isPage, resolveIdToUrl, viteID } from '../../util.js'; -import { - createRenderContext, - getParamsAndPropsOrThrow, - renderPage as coreRenderPage, -} from '../index.js'; +import { createRenderContext, renderPage as coreRenderPage } from '../index.js'; import { filterFoundRenderers, loadRenderer } from '../renderer.js'; import { getStylesForURL } from './css.js'; import type { DevelopmentEnvironment } from './environment'; @@ -176,8 +166,9 @@ export async function renderPage(options: SSROptions): Promise { env: options.env, filePath: options.filePath, }); + const { env } = options; - const renderContext = createRenderContext({ + const renderContext = await createRenderContext({ request: options.request, origin: options.origin, pathname: options.pathname, @@ -186,36 +177,25 @@ export async function renderPage(options: SSROptions): Promise { styles, componentMetadata: metadata, route: options.route, - }); - - const { env } = options; - const [params, props] = await getParamsAndPropsOrThrow({ - options: { - logging: env.logging, - mod, - route: renderContext.route, - routeCache: env.routeCache, - pathname: renderContext.pathname, - ssr: env.ssr, - }, - context: renderContext, + mod, + env, }); if (options.middleware) { if (options.middleware && options.middleware.onRequest) { const apiContext = createAPIContext({ request: options.request, - params, - props, + params: renderContext.params, + props: renderContext.props, adapterName: options.env.adapterName, }); const onRequest = options.middleware.onRequest as MiddlewareResponseHandler; const response = await callMiddleware(onRequest, apiContext, () => { - return coreRenderPage({ mod, renderContext, env: options.env, apiContext, params, props }); + return coreRenderPage({ mod, renderContext, env: options.env, apiContext }); }); return response; } } - return await coreRenderPage({ mod, renderContext, env: options.env, params, props }); // NOTE: without "await", errors won’t get caught below + return await coreRenderPage({ mod, renderContext, env: options.env }); // NOTE: without "await", errors won’t get caught below } diff --git a/packages/astro/test/fixtures/middleware-dev/astro.config.mjs b/packages/astro/test/fixtures/middleware-dev/astro.config.mjs index 0e71ac2f2d11..4379be246f94 100644 --- a/packages/astro/test/fixtures/middleware-dev/astro.config.mjs +++ b/packages/astro/test/fixtures/middleware-dev/astro.config.mjs @@ -1,5 +1,4 @@ import { defineConfig } from 'astro/config'; -import node from '@astrojs/node'; export default defineConfig({ experimental: { diff --git a/packages/astro/test/units/render/head.test.js b/packages/astro/test/units/render/head.test.js index 3976964fc8df..83fbc8b11875 100644 --- a/packages/astro/test/units/render/head.test.js +++ b/packages/astro/test/units/render/head.test.js @@ -12,7 +12,6 @@ import { import { createBasicEnvironment, createRenderContext, - getParamsAndPropsOrThrow, renderPage, } from '../../../dist/core/render/index.js'; import { defaultLogging as logging } from '../../test-utils.js'; @@ -96,29 +95,20 @@ 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 [params, props] = await getParamsAndPropsOrThrow({ - options: { - logging: env.logging, - mod: PageModule, - route: ctx.route, - routeCache: env.routeCache, - pathname: ctx.pathname, - ssr: env.ssr, - }, - context: ctx, - }); const response = await renderPage({ mod: PageModule, renderContext: ctx, env, - params, - props, + params: ctx.params, + props: ctx.props, }); const html = await response.text(); @@ -191,29 +181,20 @@ 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 [params, props] = await getParamsAndPropsOrThrow({ - options: { - logging: env.logging, - mod: PageModule, - route: ctx.route, - routeCache: env.routeCache, - pathname: ctx.pathname, - ssr: env.ssr, - }, - context: ctx, - }); const response = await renderPage({ mod: PageModule, renderContext: ctx, env, - params, - props, + params: ctx.params, + props: ctx.props, }); const html = await response.text(); const $ = cheerio.load(html); @@ -252,29 +233,20 @@ 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 [params, props] = await getParamsAndPropsOrThrow({ - options: { - logging: env.logging, - mod: PageModule, - route: ctx.route, - routeCache: env.routeCache, - pathname: ctx.pathname, - ssr: env.ssr, - }, - context: ctx, - }); const response = await renderPage({ mod: PageModule, renderContext: ctx, env, - params, - props, + 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 8211250b86b7..c249bcbd5a43 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, @@ -12,7 +11,6 @@ import { createRenderContext, renderPage, loadRenderer, - getParamsAndPropsOrThrow, } from '../../../dist/core/render/index.js'; import { createAstroJSXComponent, renderer as jsxRenderer } from '../../../dist/jsx/index.js'; import { defaultLogging as logging } from '../../test-utils.js'; @@ -47,25 +45,17 @@ describe('core/render', () => { }); }); - const ctx = createRenderContext({ request: new Request('http://example.com/') }); const mod = createAstroModule(Page); - const [params, props] = await getParamsAndPropsOrThrow({ - options: { - logging: env.logging, - mod, - route: ctx.route, - routeCache: env.routeCache, - pathname: ctx.pathname, - ssr: env.ssr, - }, - context: ctx, + const ctx = await createRenderContext({ + request: new Request('http://example.com/'), + env, + mod, }); + const response = await renderPage({ mod, renderContext: ctx, env, - params, - props, }); expect(response.status).to.equal(200); @@ -104,25 +94,16 @@ describe('core/render', () => { }); }); - const ctx = createRenderContext({ request: new Request('http://example.com/') }); const mod = createAstroModule(Page); - const [params, props] = await getParamsAndPropsOrThrow({ - options: { - logging: env.logging, - mod, - route: ctx.route, - routeCache: env.routeCache, - pathname: ctx.pathname, - ssr: env.ssr, - }, - context: ctx, + const ctx = await createRenderContext({ + request: new Request('http://example.com/'), + env, + mod, }); const response = await renderPage({ mod, renderContext: ctx, env, - params, - props, }); expect(response.status).to.equal(200); @@ -142,25 +123,17 @@ describe('core/render', () => { return render`
${renderComponent(result, 'Component', Component, {})}
`; }); - const ctx = createRenderContext({ request: new Request('http://example.com/') }); const mod = createAstroModule(Page); - const [params, props] = await getParamsAndPropsOrThrow({ - options: { - logging: env.logging, - mod, - route: ctx.route, - routeCache: env.routeCache, - pathname: ctx.pathname, - ssr: env.ssr, - }, - context: ctx, + const ctx = await createRenderContext({ + request: new Request('http://example.com/'), + env, + mod, }); + const response = await renderPage({ mod, renderContext: ctx, env, - params, - props, }); try { From f0e3f453f52aacc1d901c8fe4aa5933c978259f0 Mon Sep 17 00:00:00 2001 From: Emanuele Stoppa Date: Wed, 3 May 2023 15:03:03 +0100 Subject: [PATCH 47/53] chore: address code suggestions --- examples/middleware/astro.config.mjs | 4 ++-- examples/middleware/src/layouts/Layout.astro | 2 +- examples/middleware/src/pages/admin.astro | 4 ++-- examples/middleware/src/pages/index.astro | 8 +------- examples/middleware/src/pages/login.astro | 3 +-- packages/astro/package.json | 2 -- packages/astro/src/core/app/index.ts | 5 +---- packages/astro/src/core/build/generate.ts | 8 ++++---- .../astro/src/core/build/plugins/plugin-pages.ts | 6 +++--- packages/astro/src/core/build/static-build.ts | 3 +-- packages/astro/src/core/endpoint/index.ts | 2 +- packages/astro/src/core/middleware/index.ts | 2 +- packages/astro/src/core/middleware/loadMiddleware.ts | 4 ++-- packages/astro/src/core/middleware/sequence.ts | 3 +-- packages/astro/src/core/render/core.ts | 6 +++--- packages/astro/src/core/render/dev/index.ts | 4 ++-- packages/astro/src/core/render/index.ts | 2 +- packages/astro/src/vite-plugin-astro-server/route.ts | 12 ++++++------ 18 files changed, 33 insertions(+), 47 deletions(-) diff --git a/examples/middleware/astro.config.mjs b/examples/middleware/astro.config.mjs index ca71a89dcc88..1d4662423eff 100644 --- a/examples/middleware/astro.config.mjs +++ b/examples/middleware/astro.config.mjs @@ -8,6 +8,6 @@ export default defineConfig({ mode: 'standalone', }), experimental: { - middleware: true - } + middleware: true, + }, }); diff --git a/examples/middleware/src/layouts/Layout.astro b/examples/middleware/src/layouts/Layout.astro index f1a62a537b7d..22100824e68c 100644 --- a/examples/middleware/src/layouts/Layout.astro +++ b/examples/middleware/src/layouts/Layout.astro @@ -26,7 +26,7 @@ const { title } = Astro.props; } html { font-family: system-ui, sans-serif; - background-color: #F6F6F6; + background-color: #f6f6f6; } code { font-family: Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono, diff --git a/examples/middleware/src/pages/admin.astro b/examples/middleware/src/pages/admin.astro index 35f7ca237113..028fd6b08068 100644 --- a/examples/middleware/src/pages/admin.astro +++ b/examples/middleware/src/pages/admin.astro @@ -1,11 +1,11 @@ --- -import Layout from "../layouts/Layout.astro"; +import Layout from '../layouts/Layout.astro'; const user = Astro.locals.user; --- +

Welcome back {user.name} {user.surname}

-
diff --git a/examples/middleware/src/pages/index.astro b/examples/middleware/src/pages/index.astro index a990caf89d4a..ff77d4a15272 100644 --- a/examples/middleware/src/pages/index.astro +++ b/examples/middleware/src/pages/index.astro @@ -1,7 +1,6 @@ --- import Layout from '../layouts/Layout.astro'; import Card from '../components/Card.astro'; - --- @@ -13,12 +12,7 @@ import Card from '../components/Card.astro';

{}
diff --git a/examples/middleware/src/pages/login.astro b/examples/middleware/src/pages/login.astro index ba5cd295232f..99cf4cc947fe 100644 --- a/examples/middleware/src/pages/login.astro +++ b/examples/middleware/src/pages/login.astro @@ -4,7 +4,7 @@ import Layout from '../layouts/Layout.astro'; const status = Astro.response.status; let redirectMessage; if (status === 301) { - redirectMessage = "Your session is finished, please login again" + redirectMessage = 'Your session is finished, please login again'; } --- @@ -22,7 +22,6 @@ if (status === 301) { diff --git a/packages/astro/package.json b/packages/astro/package.json index 2d921763f2b3..6509a8f5c7f3 100644 --- a/packages/astro/package.json +++ b/packages/astro/package.json @@ -85,8 +85,6 @@ "config.mjs", "zod.d.ts", "zod.mjs", - "middleware.d.ts", - "middleware.mjs", "env.d.ts", "client.d.ts", "client-base.d.ts", diff --git a/packages/astro/src/core/app/index.ts b/packages/astro/src/core/app/index.ts index 01df1ea706b5..b499a875b0e4 100644 --- a/packages/astro/src/core/app/index.ts +++ b/packages/astro/src/core/app/index.ts @@ -1,8 +1,6 @@ import type { - AstroMiddlewareInstance, ComponentInstance, EndpointHandler, - EndpointOutput, ManifestData, MiddlewareResponseHandler, RouteData, @@ -15,13 +13,13 @@ import { attachToResponse, getSetCookiesFromResponse } from '../cookies/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, createRenderContext, renderPage, type Environment, - getParamsAndPropsOrThrow, } from '../render/index.js'; import { RouteCache } from '../render/route-cache.js'; import { @@ -31,7 +29,6 @@ import { } from '../render/ssr-element.js'; import { matchRoute } from '../routing/match.js'; export { deserializeManifest } from './common.js'; -import { callMiddleware } from '../middleware/callMiddleware.js'; const clientLocalsSymbol = Symbol.for('astro.locals'); diff --git a/packages/astro/src/core/build/generate.ts b/packages/astro/src/core/build/generate.ts index d3afa34a37c6..d89575bd414a 100644 --- a/packages/astro/src/core/build/generate.ts +++ b/packages/astro/src/core/build/generate.ts @@ -9,12 +9,12 @@ import type { AstroSettings, ComponentInstance, EndpointHandler, + EndpointOutput, ImageTransform, + MiddlewareResponseHandler, RouteType, SSRError, SSRLoadedRenderer, - EndpointOutput, - MiddlewareResponseHandler, } from '../../@types/astro'; import { generateImage as generateImageInternal, @@ -33,8 +33,9 @@ import { createAPIContext, throwIfRedirectNotAllowed, } from '../endpoint/index.js'; -import { AstroError, AstroErrorData } from '../errors/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 { @@ -49,7 +50,6 @@ import { getOutDirWithinCwd, getOutFile, getOutFolder } from './common.js'; import { eachPageData, getPageDataByComponent, sortedCSS } from './internal.js'; import type { PageBuildData, SingleFileBuiltModule, StaticBuildOptions } from './types'; import { getTimeStat } from './util.js'; -import { callMiddleware } from '../middleware/callMiddleware.js'; function shouldSkipDraft(pageModule: ComponentInstance, settings: AstroSettings): boolean { return ( diff --git a/packages/astro/src/core/build/plugins/plugin-pages.ts b/packages/astro/src/core/build/plugins/plugin-pages.ts index a7730e53a6e1..132d03cf80fd 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 { MIDDLEWARE_PATH_SEGMENT_NAME } from '../../constants.js'; +import type { AstroBuildPlugin } from '../plugin'; +import type { StaticBuildOptions } from '../types'; export function vitePluginPages(opts: StaticBuildOptions, internals: BuildInternals): VitePlugin { return { diff --git a/packages/astro/src/core/build/static-build.ts b/packages/astro/src/core/build/static-build.ts index 8b3b088b3d94..ff71e80b8ee7 100644 --- a/packages/astro/src/core/build/static-build.ts +++ b/packages/astro/src/core/build/static-build.ts @@ -13,7 +13,7 @@ import { } from '../../core/build/internal.js'; import { emptyDir, removeEmptyDirs } from '../../core/fs/index.js'; import { appendForwardSlash, prependForwardSlash } from '../../core/path.js'; -import { isModeServerWithNoAdapter, viteID } from '../../core/util.js'; +import { isModeServerWithNoAdapter } from '../../core/util.js'; import { runHookBuildSetup } from '../../integrations/index.js'; import { PAGE_SCRIPT_ID } from '../../vite-plugin-scripts/index.js'; import { resolvedPagesVirtualModuleId } from '../app/index.js'; @@ -26,7 +26,6 @@ import { createPluginContainer, type AstroBuildPluginContainer } from './plugin. import { registerAllPlugins } from './plugins/index.js'; import type { PageBuildData, StaticBuildOptions } from './types'; import { getTimeStat } from './util.js'; -import { MIDDLEWARE_PATH_SEGMENT_NAME } from '../constants.js'; export async function viteBuild(opts: StaticBuildOptions) { const { allPages, settings } = opts; diff --git a/packages/astro/src/core/endpoint/index.ts b/packages/astro/src/core/endpoint/index.ts index af9ecb01c84a..e49ce8a430b5 100644 --- a/packages/astro/src/core/endpoint/index.ts +++ b/packages/astro/src/core/endpoint/index.ts @@ -14,8 +14,8 @@ 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 { isValueSerializable } 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'); diff --git a/packages/astro/src/core/middleware/index.ts b/packages/astro/src/core/middleware/index.ts index ad409a5523e9..f9fb07bd4d98 100644 --- a/packages/astro/src/core/middleware/index.ts +++ b/packages/astro/src/core/middleware/index.ts @@ -1,5 +1,5 @@ -import { sequence } from './sequence.js'; import type { MiddlewareResponseHandler } from '../../@types/astro'; +import { sequence } from './sequence.js'; function defineMiddleware(fn: MiddlewareResponseHandler) { return fn; diff --git a/packages/astro/src/core/middleware/loadMiddleware.ts b/packages/astro/src/core/middleware/loadMiddleware.ts index 62dea70cdece..5c64565afd5e 100644 --- a/packages/astro/src/core/middleware/loadMiddleware.ts +++ b/packages/astro/src/core/middleware/loadMiddleware.ts @@ -1,6 +1,6 @@ -import type { ModuleLoader } from '../module-loader'; -import { MIDDLEWARE_PATH_SEGMENT_NAME } from '../constants.js'; 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. diff --git a/packages/astro/src/core/middleware/sequence.ts b/packages/astro/src/core/middleware/sequence.ts index 0ed08b03b502..0358f3719133 100644 --- a/packages/astro/src/core/middleware/sequence.ts +++ b/packages/astro/src/core/middleware/sequence.ts @@ -1,9 +1,8 @@ import type { APIContext, MiddlewareResponseHandler } from '../../@types/astro'; import { defineMiddleware } from './index.js'; -import { AstroError, AstroErrorData } from '../errors/index.js'; +// From SvelteKit: https://github.com/sveltejs/kit/blob/master/packages/kit/src/exports/hooks/sequence.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. */ diff --git a/packages/astro/src/core/render/core.ts b/packages/astro/src/core/render/core.ts index b396bed6a811..fd57ad8bc673 100644 --- a/packages/astro/src/core/render/core.ts +++ b/packages/astro/src/core/render/core.ts @@ -1,11 +1,11 @@ import type { APIContext, 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 { 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'; diff --git a/packages/astro/src/core/render/dev/index.ts b/packages/astro/src/core/render/dev/index.ts index edbf1595d67a..fbbe0d48d37e 100644 --- a/packages/astro/src/core/render/dev/index.ts +++ b/packages/astro/src/core/render/dev/index.ts @@ -9,8 +9,10 @@ import type { 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'; @@ -19,8 +21,6 @@ import { getStylesForURL } from './css.js'; import type { DevelopmentEnvironment } from './environment'; import { getComponentMetadata } from './metadata.js'; import { getScriptsForURL } from './scripts.js'; -import { createAPIContext } from '../../endpoint/index.js'; -import { callMiddleware } from '../../middleware/callMiddleware.js'; export { createDevelopmentEnvironment } from './environment.js'; export type { DevelopmentEnvironment }; diff --git a/packages/astro/src/core/render/index.ts b/packages/astro/src/core/render/index.ts index a25fa0ec3feb..4e4df8239f6f 100644 --- a/packages/astro/src/core/render/index.ts +++ b/packages/astro/src/core/render/index.ts @@ -3,8 +3,8 @@ export type { RenderContext } from './context.js'; export { getParamsAndProps, GetParamsAndPropsError, - renderPage, getParamsAndPropsOrThrow, + renderPage, } from './core.js'; export type { Environment } from './environment'; export { createBasicEnvironment, createEnvironment } from './environment.js'; diff --git a/packages/astro/src/vite-plugin-astro-server/route.ts b/packages/astro/src/vite-plugin-astro-server/route.ts index 8942671eca81..cb2e76178d31 100644 --- a/packages/astro/src/vite-plugin-astro-server/route.ts +++ b/packages/astro/src/vite-plugin-astro-server/route.ts @@ -1,23 +1,23 @@ 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'; import { matchAllRoutes } from '../core/routing/index.js'; import { log404 } from './common.js'; import { handle404Response, writeSSRResult, writeWebResponse } from './response.js'; -import { loadMiddleware } from '../core/middleware/loadMiddleware.js'; type AsyncReturnType Promise> = T extends ( ...args: any From bbe4c223d1a6eaa2d4404df15e3557ba2e031340 Mon Sep 17 00:00:00 2001 From: Emanuele Stoppa Date: Wed, 3 May 2023 15:51:38 +0100 Subject: [PATCH 48/53] Apply suggestions from code review Co-authored-by: Sarah Rainsberger --- packages/astro/src/core/errors/errors-data.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/astro/src/core/errors/errors-data.ts b/packages/astro/src/core/errors/errors-data.ts index ca09b7f595b1..94e07d005f57 100644 --- a/packages/astro/src/core/errors/errors-data.ts +++ b/packages/astro/src/core/errors/errors-data.ts @@ -632,27 +632,27 @@ See https://docs.astro.build/en/guides/server-side-rendering/ for more informati /** * @docs * @description - * Thrown when a middleware doesn't return anything or doesn't call the `next` function. + * 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 doesn't call next + * // doesn't return anything or call `next` * context.locals.someData = false; * }); * ``` */ MiddlewareNoDataOrNextCalled: { - title: "The middleware didn't return a response or called `next`", + title: "The middleware didn't return a response or call `next`", code: 3031, - message: 'The middleware needs to return a `Response` object or call the `next` function.', + message: 'The middleware needs to either return a `Response` object or call the `next` function.', }, /** * @docs * @description - * Thrown in development mode, when a middleware return something that is not a `Response` + * Thrown in development mode when middleware returns something that is not a `Response` object. * * For example: * ```ts @@ -663,10 +663,10 @@ See https://docs.astro.build/en/guides/server-side-rendering/ for more informati * ``` */ MiddlewareNotAResponse: { - title: 'The middleware returned something that is not a `Response`', + title: 'The middleware returned something that is not a `Response` object', code: 3032, message: - 'When returning something from a middleware, you must return a valid `Response`. Failing doing so would cause errors..', + 'Any data returned from middleware must be a valid `Response` object.', }, /** From 23f8aa1bea929ab3ce8e64f83b0f1563d36b06ba Mon Sep 17 00:00:00 2001 From: Emanuele Stoppa Date: Wed, 3 May 2023 16:03:02 +0100 Subject: [PATCH 49/53] Update packages/astro/src/core/errors/errors-data.ts Co-authored-by: Sarah Rainsberger --- packages/astro/src/core/errors/errors-data.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/astro/src/core/errors/errors-data.ts b/packages/astro/src/core/errors/errors-data.ts index 94e07d005f57..59441e2ce7a4 100644 --- a/packages/astro/src/core/errors/errors-data.ts +++ b/packages/astro/src/core/errors/errors-data.ts @@ -673,7 +673,7 @@ See https://docs.astro.build/en/guides/server-side-rendering/ for more informati * @docs * @description * - * Thrown in development mode, when `locals` are overridden with something that is not an object + * Thrown in development mode when `locals` is overwritten with something that is not an object * * For example: * ```ts @@ -685,9 +685,9 @@ See https://docs.astro.build/en/guides/server-side-rendering/ for more informati * ``` */ LocalsNotAnObject: { - title: 'Value assigned to `locals` is not accepted.', + title: 'Value assigned to `locals` is not accepted', code: 3033, - message: `The \`locals\` can only be assigned to an object. Other values like numbers, strings, etc. are not accepted.`, + 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`.', }, From a2111262d37ef33278e0f1295beb1f3a91457de8 Mon Sep 17 00:00:00 2001 From: Emanuele Stoppa Date: Wed, 3 May 2023 16:03:33 +0100 Subject: [PATCH 50/53] chore: fix message --- packages/astro/src/core/errors/errors-data.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/astro/src/core/errors/errors-data.ts b/packages/astro/src/core/errors/errors-data.ts index 59441e2ce7a4..bcec0137acaa 100644 --- a/packages/astro/src/core/errors/errors-data.ts +++ b/packages/astro/src/core/errors/errors-data.ts @@ -646,7 +646,8 @@ See https://docs.astro.build/en/guides/server-side-rendering/ for more informati 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.', + message: + 'The middleware needs to either return a `Response` object or call the `next` function.', }, /** @@ -665,8 +666,7 @@ See https://docs.astro.build/en/guides/server-side-rendering/ for more informati 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.', + message: 'Any data returned from middleware must be a valid `Response` object.', }, /** @@ -687,7 +687,8 @@ See https://docs.astro.build/en/guides/server-side-rendering/ for more informati 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.', + 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`.', }, From 95f22486b5d41934cc2772f4cc76b09a983f1d65 Mon Sep 17 00:00:00 2001 From: Emanuele Stoppa Date: Wed, 3 May 2023 16:34:19 +0100 Subject: [PATCH 51/53] Update packages/astro/src/core/errors/errors-data.ts Co-authored-by: Sarah Rainsberger --- packages/astro/src/core/errors/errors-data.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/astro/src/core/errors/errors-data.ts b/packages/astro/src/core/errors/errors-data.ts index bcec0137acaa..9f252576eef6 100644 --- a/packages/astro/src/core/errors/errors-data.ts +++ b/packages/astro/src/core/errors/errors-data.ts @@ -695,7 +695,7 @@ See https://docs.astro.build/en/guides/server-side-rendering/ for more informati /** * @docs * @description - * Thrown in development mode, when a user attempts to store in `local` something that is not serializable. + * Thrown in development mode when a user attempts to store something that is not serializable in `locals`. * * For example: * ```ts @@ -711,7 +711,7 @@ See https://docs.astro.build/en/guides/server-side-rendering/ for more informati * ``` */ LocalsNotSerializable: { - title: '`Astro.locals` are not serializable.', + title: '`Astro.locals` is not serializable', code: 3034, message: (href: string) => { return `The information stored in \`Astro.locals\` are not serializable when visiting "${href}" path.\nMake sure you store only data that are serializable.`; From d45d8cb519e2e5438e27141b15b0abd5f0440a94 Mon Sep 17 00:00:00 2001 From: Emanuele Stoppa Date: Wed, 3 May 2023 16:35:59 +0100 Subject: [PATCH 52/53] chore: code suggestion --- packages/astro/src/core/errors/errors-data.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/astro/src/core/errors/errors-data.ts b/packages/astro/src/core/errors/errors-data.ts index 9f252576eef6..27425aee539b 100644 --- a/packages/astro/src/core/errors/errors-data.ts +++ b/packages/astro/src/core/errors/errors-data.ts @@ -714,7 +714,7 @@ See https://docs.astro.build/en/guides/server-side-rendering/ for more informati title: '`Astro.locals` is not serializable', code: 3034, message: (href: string) => { - return `The information stored in \`Astro.locals\` are not serializable when visiting "${href}" path.\nMake sure you store only data that are serializable.`; + 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. From e8e5007d8308c901e37e107e2b9789c0eda6fce5 Mon Sep 17 00:00:00 2001 From: Emanuele Stoppa Date: Wed, 3 May 2023 16:37:21 +0100 Subject: [PATCH 53/53] chore: update lock file --- pnpm-lock.yaml | 98 +++++++++++++++++++++++++++++--------------------- 1 file changed, 57 insertions(+), 41 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 64a738ecef03..1d72be10a762 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -553,8 +553,8 @@ importers: packages/astro: dependencies: '@astrojs/compiler': - specifier: ^1.3.2 - version: 1.3.2 + specifier: ^1.4.0 + version: 1.4.0 '@astrojs/language-server': specifier: ^1.0.0 version: 1.0.0 @@ -592,8 +592,8 @@ importers: specifier: ^21.0.0 version: 21.0.0 acorn: - specifier: ^8.8.1 - version: 8.8.1 + specifier: ^8.8.2 + version: 8.8.2 boxen: specifier: ^6.2.1 version: 6.2.1 @@ -625,8 +625,8 @@ importers: specifier: ^1.1.0 version: 1.1.1 estree-walker: - specifier: ^3.0.1 - version: 3.0.1 + specifier: 3.0.0 + version: 3.0.0 execa: specifier: ^6.1.0 version: 6.1.0 @@ -673,8 +673,8 @@ importers: specifier: ^1.0.1 version: 1.0.1 shiki: - specifier: ^0.11.1 - version: 0.11.1 + specifier: ^0.14.1 + version: 0.14.1 slash: specifier: ^4.0.0 version: 4.0.0 @@ -2984,6 +2984,12 @@ importers: specifier: workspace:* version: link:../../.. + packages/astro/test/fixtures/scoped-style-strategy: + dependencies: + astro: + specifier: workspace:* + version: link:../../.. + packages/astro/test/fixtures/set-html: dependencies: astro: @@ -3955,8 +3961,8 @@ importers: version: link:../../../../../astro devDependencies: shiki: - specifier: ^0.11.1 - version: 0.11.1 + specifier: ^0.14.1 + version: 0.14.1 packages/integrations/markdoc/test/fixtures/render-with-config: dependencies: @@ -4012,8 +4018,8 @@ importers: specifier: ^2.0.0 version: 2.0.0 shiki: - specifier: ^0.11.1 - version: 0.11.1 + specifier: ^0.14.1 + version: 0.14.1 source-map: specifier: ^0.7.4 version: 0.7.4 @@ -4074,7 +4080,7 @@ importers: version: 4.0.2 rehype-pretty-code: specifier: ^0.4.0 - version: 0.4.0(shiki@0.11.1) + version: 0.4.0(shiki@0.14.1) remark-math: specifier: ^5.1.1 version: 5.1.1 @@ -4935,8 +4941,8 @@ importers: specifier: ^2.0.0 version: 2.0.0 shiki: - specifier: ^0.11.1 - version: 0.11.1 + specifier: ^0.14.1 + version: 0.14.1 unified: specifier: ^10.1.2 version: 10.1.2 @@ -5371,14 +5377,14 @@ packages: sisteransi: 1.0.5 dev: false - /@astrojs/compiler@1.3.2: - resolution: {integrity: sha512-W/2Mdsq75ruK31dPVlXLdvAoknYDcm6+zXiFToSzQWI7wZqqR+51XTFgx90ojYbefk7z4VOJSVtZBz2pA82F5A==} + /@astrojs/compiler@1.4.0: + resolution: {integrity: sha512-Vav3a32Ct+omowV9X9kDM2ghWAvFdjZkv5BdvBjZCKYbFVT6//IZApDIVbHI1UPuLuD2sKyLWx2T+E7clqUJdg==} /@astrojs/language-server@1.0.0: resolution: {integrity: sha512-oEw7AwJmzjgy6HC9f5IdrphZ1GVgfV/+7xQuyf52cpTiRWd/tJISK3MsKP0cDkVlfodmNABNFnAaAWuLZEiiiA==} hasBin: true dependencies: - '@astrojs/compiler': 1.3.2 + '@astrojs/compiler': 1.4.0 '@jridgewell/trace-mapping': 0.3.18 '@vscode/emmet-helper': 2.8.6 events: 3.3.0 @@ -7776,7 +7782,7 @@ packages: estree-util-build-jsx: 2.2.2 estree-util-is-identifier-name: 2.1.0 estree-util-to-js: 1.2.0 - estree-walker: 3.0.1 + estree-walker: 3.0.0 hast-util-to-estree: 2.3.2 markdown-extensions: 1.1.1 periscopic: 3.1.0 @@ -9060,7 +9066,7 @@ packages: hasBin: true dependencies: '@mapbox/node-pre-gyp': 1.0.10 - acorn: 8.8.1 + acorn: 8.8.2 async-sema: 3.1.1 bindings: 1.5.0 estree-walker: 2.0.2 @@ -9315,12 +9321,12 @@ packages: acorn-walk: 7.2.0 dev: true - /acorn-jsx@5.3.2(acorn@8.8.1): + /acorn-jsx@5.3.2(acorn@8.8.2): resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 dependencies: - acorn: 8.8.1 + acorn: 8.8.2 /acorn-walk@7.2.0: resolution: {integrity: sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==} @@ -9342,6 +9348,12 @@ packages: resolution: {integrity: sha512-7zFpHzhnqYKrkYdUjF1HI1bzd0VygEGX8lFk4k5zVMqHEoES+P+7TKI+EvLO9WVMJ8eekdO0aDEK044xTXwPPA==} engines: {node: '>=0.4.0'} hasBin: true + dev: false + + /acorn@8.8.2: + resolution: {integrity: sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==} + engines: {node: '>=0.4.0'} + hasBin: true /agent-base@6.0.2: resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} @@ -9431,6 +9443,9 @@ packages: resolution: {integrity: sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==} engines: {node: '>=12'} + /ansi-sequence-parser@1.1.0: + resolution: {integrity: sha512-lEm8mt52to2fT8GhciPCGeCXACSz2UwIN4X2e2LJSnZ5uAbn2/dsYdOmUXq0AtWS5cpAupysIneExOgH0Vd2TQ==} + /ansi-styles@3.2.1: resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==} engines: {node: '>=4'} @@ -11460,8 +11475,8 @@ packages: resolution: {integrity: sha512-5yxtHSZXRSW5pvv3hAlXM5+/Oswi1AUFqBmbibKb5s6bp3rGIDkyXU6xCoyuuLhijr4SFwPrXRoZjz0AZDN9tg==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dependencies: - acorn: 8.8.1 - acorn-jsx: 5.3.2(acorn@8.8.1) + acorn: 8.8.2 + acorn-jsx: 5.3.2(acorn@8.8.2) eslint-visitor-keys: 3.4.0 dev: true @@ -11505,7 +11520,7 @@ packages: dependencies: '@types/estree-jsx': 1.0.0 estree-util-is-identifier-name: 2.1.0 - estree-walker: 3.0.1 + estree-walker: 3.0.0 dev: false /estree-util-is-identifier-name@2.1.0: @@ -11536,8 +11551,8 @@ packages: /estree-walker@2.0.2: resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} - /estree-walker@3.0.1: - resolution: {integrity: sha512-woY0RUD87WzMBUiZLx8NsYr23N5BKsOMZHhu2hoNRVh6NXGfoiT1KOL8G3UHlJAnEDGmfa5ubNA/AacfG+Kb0g==} + /estree-walker@3.0.0: + resolution: {integrity: sha512-s6ceX0NFiU/vKPiKvFdR83U1Zffu7upwZsGwpoqfg5rbbq1l50WQ5hCeIvM6E6oD4shUHCYMsiFPns4Jk0YfMQ==} dev: false /esutils@2.0.3: @@ -12924,7 +12939,7 @@ packages: optional: true dependencies: abab: 2.0.6 - acorn: 8.8.1 + acorn: 8.8.2 acorn-globals: 6.0.0 cssom: 0.5.0 cssstyle: 2.3.0 @@ -13745,8 +13760,8 @@ packages: /micromark-extension-mdxjs@1.0.0: resolution: {integrity: sha512-TZZRZgeHvtgm+IhtgC2+uDMR7h8eTKF0QUX9YsgoL9+bADBpBY6SiLvWqnBlLbCEevITmTqmEuY3FoxMKVs1rQ==} dependencies: - acorn: 8.8.1 - acorn-jsx: 5.3.2(acorn@8.8.1) + acorn: 8.8.2 + acorn-jsx: 5.3.2(acorn@8.8.2) micromark-extension-mdx-expression: 1.0.4 micromark-extension-mdx-jsx: 1.0.3 micromark-extension-mdx-md: 1.0.0 @@ -14704,7 +14719,7 @@ packages: resolution: {integrity: sha512-vKiQ8RRtkl9P+r/+oefh25C3fhybptkHKCZSPlcXiJux2tJF55GnEj3BVn4A5gKfq9NWWXXrxkHBwVPUfH0opw==} dependencies: '@types/estree': 1.0.0 - estree-walker: 3.0.1 + estree-walker: 3.0.0 is-reference: 3.0.1 dev: false @@ -15224,7 +15239,7 @@ packages: resolution: {integrity: sha512-kt9wk33J7HvFGwFaHb8piwy4zbUmabC8Nu+qCw493jhe96YkpjscqGBPy4nJ9TPy9pd7+kEx1zM81rp+MIdrXg==} engines: {node: ^14.15.0 || >=16.0.0, pnpm: '>=7.14.0'} dependencies: - '@astrojs/compiler': 1.3.2 + '@astrojs/compiler': 1.4.0 prettier: 2.8.8 sass-formatter: 0.7.6 synckit: 0.8.5 @@ -15534,14 +15549,14 @@ packages: unified: 10.1.2 dev: false - /rehype-pretty-code@0.4.0(shiki@0.11.1): + /rehype-pretty-code@0.4.0(shiki@0.14.1): resolution: {integrity: sha512-Bp91nfo4blpgCXlvGP1hsG+kRFfjqBVU09o1RFcnNA62u+iIzJiJRGzpfBj4FaItq7CEQL5ASGB7vLxN5xCvyA==} engines: {node: ^12.16.0 || >=13.2.0} peerDependencies: shiki: '*' dependencies: parse-numeric-range: 1.3.0 - shiki: 0.11.1 + shiki: 0.14.1 dev: true /rehype-raw@6.1.1: @@ -16079,12 +16094,13 @@ packages: vscode-textmate: 5.2.0 dev: true - /shiki@0.11.1: - resolution: {integrity: sha512-EugY9VASFuDqOexOgXR18ZV+TbFrQHeCpEYaXamO+SZlsnT/2LxuLBX25GGtIrwaEVFXUAbUQ601SWE2rMwWHA==} + /shiki@0.14.1: + resolution: {integrity: sha512-+Jz4nBkCBe0mEDqo1eKRcCdjRtrCjozmcbTUjbPTX7OOJfEbTZzlUWlZtGe3Gb5oV1/jnojhG//YZc3rs9zSEw==} dependencies: + ansi-sequence-parser: 1.1.0 jsonc-parser: 3.2.0 vscode-oniguruma: 1.7.0 - vscode-textmate: 6.0.0 + vscode-textmate: 8.0.0 /side-channel@1.0.4: resolution: {integrity: sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==} @@ -16656,7 +16672,7 @@ packages: hasBin: true dependencies: '@jridgewell/source-map': 0.3.3 - acorn: 8.8.1 + acorn: 8.8.2 commander: 2.20.3 source-map-support: 0.5.21 dev: false @@ -17461,7 +17477,7 @@ packages: engines: {node: '>=6.0'} hasBin: true dependencies: - acorn: 8.8.1 + acorn: 8.8.2 acorn-walk: 8.2.0 dev: true @@ -17517,8 +17533,8 @@ packages: resolution: {integrity: sha512-Uw5ooOQxRASHgu6C7GVvUxisKXfSgW4oFlO+aa+PAkgmH89O3CXxEEzNRNtHSqtXFTl0nAC1uYj0GMSH27uwtQ==} dev: true - /vscode-textmate@6.0.0: - resolution: {integrity: sha512-gu73tuZfJgu+mvCSy4UZwd2JXykjK9zAZsfmDeut5dx/1a7FeTk0XwJsSuqQn+cuMCGVbIBfl+s53X4T19DnzQ==} + /vscode-textmate@8.0.0: + resolution: {integrity: sha512-AFbieoL7a5LMqcnOF04ji+rpXadgOXnZsxQr//r83kLPr7biP7am3g9zbaZIaBGwBRWeSvoMD4mgPdX3e4NWBg==} /vscode-uri@2.1.2: resolution: {integrity: sha512-8TEXQxlldWAuIODdukIb+TR5s+9Ds40eSJrw+1iDDA9IFORPjMELarNQE3myz5XIkWWpdprmJjm1/SxMlWOC8A==}