diff --git a/.vscode/settings.json b/.vscode/settings.json index d06b2aa..ca7d377 100755 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -36,6 +36,7 @@ "activitypub", "aitertools", "apidoc", + "astro", "bccs", "btos", "callouts", diff --git a/CHANGES.md b/CHANGES.md index 5f0f851..7d7f241 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -96,17 +96,26 @@ To be released. loader that throws an error when the given URL is not an HTTP or HTTPS URL or refers to a private network address. + - Added `@fedify/fedify/x/astro` module for integrating with [Astro] + middleware. [[#50]] + + - Added `createMiddleware()` function. + - Added `createFetchOptions()` function. + - Added `ContextDataFactory` type. + - Added more log messages using the [LogTape] library. Currently the below logger categories are used: - `["fedify", "federation", "queue"]` +[#50]: https://github.com/dahlia/fedify/issues/50 [#53]: https://github.com/dahlia/fedify/issues/53 [#66]: https://github.com/dahlia/fedify/issues/66 [#70]: https://github.com/dahlia/fedify/issues/70 [#81]: https://github.com/dahlia/fedify/issues/81 [#85]: https://github.com/dahlia/fedify/issues/85 [#92]: https://github.com/dahlia/fedify/pull/92 +[Astro]: https://astro.build/ Version 0.11.2 diff --git a/deno.json b/deno.json index a73816f..ccf7764 100644 --- a/deno.json +++ b/deno.json @@ -9,6 +9,7 @@ "./sig": "./sig/mod.ts", "./vocab": "./vocab/mod.ts", "./webfinger": "./webfinger/mod.ts", + "./x/astro": "./x/astro.ts", "./x/denokv": "./x/denokv.ts", "./x/fresh": "./x/fresh.ts", "./x/hono": "./x/hono.ts" diff --git a/x/astro.ts b/x/astro.ts new file mode 100644 index 0000000..761830e --- /dev/null +++ b/x/astro.ts @@ -0,0 +1,140 @@ +/** + * Fedify with Astro + * ================= + * + * This module contains some utilities for integrating Fedify with + * the [Astro] framework. + * + * [Astro]: https://astro.build/ + * + * @module + * @since 0.12.0 + */ +import type { + Federation, + FederationFetchOptions, +} from "../federation/middleware.ts"; + +interface AstroContext { + request: Request; +} +type RewritePayload = string | URL | Request; +type MiddlewareNext = ( + rewritePayload?: RewritePayload, +) => Promise; +type MiddlewareHandler = ( + context: TAstroContext, + next: MiddlewareNext, +) => Promise | Response | Promise | void; + +/** + * Create options for the {@link Federation.fetch} method to integrate with + * Astro. + * + * @example src/middleware.ts + * ``` typescript + * import { defineMiddleware } from "astro:middleware"; + * import { federation } from "./federation"; // Import the `Federation` object + * + * export const onRequest = defineMiddleware((context, next) => { + * return federation.fetch(context.request, { + * contextData: undefined, + * ...createFetchOptions(context, next), + * }); + * }); + * ``` + * + * @typeParam TAstroContext A type of the Astro context. + * @param context An Astro context. + * @param next A function to call the next middleware. + * @returns Options for the {@link Federation.fetch} method. + * @since 0.12.0 + */ +export function createFetchOptions( + _context: TAstroContext, + next: MiddlewareNext, +): Omit, "contextData"> { + return { + // If the `federation` object finds a request not responsible for it + // (i.e., not a federation-related request), it will call the `next` + // provided by the Astro framework to continue the request handling + // by Astro: + onNotFound: next, + + // Similar to `onNotFound`, but slightly more tricky one. + // When the `federation` object finds a request not acceptable type-wise + // (i.e., a user-agent doesn't want JSON-LD), it will call the `next` + // provided by the Astro framework so that it renders HTML if there's some + // page. Otherwise, it will simply return a 406 Not Acceptable response. + // This kind of trick enables the Fedify and Astro to share the same routes + // and they do content negotiation depending on `Accept` header: + async onNotAcceptable(_request: Request) { + const response = await next(); + if (response.status !== 404) return response; + return new Response("Not acceptable", { + status: 406, + headers: { + "Content-Type": "text/plain", + Vary: "Accept", + }, + }); + }, + }; +} + +/** + * The factory function to create a context data for + * the {@link Federation.fetch}. + * + * @typeParam TContextData A type of the context data. + * @typeParam TAstroContext A type of the Astro context. + * @param context An Astro context. + * @returns The context data for the {@link Federation.fetch}. + * @since 0.12.0 + */ +export type ContextDataFactory< + TContextData, + TAstroContext extends AstroContext, +> = ( + context: TAstroContext, +) => TContextData | Promise; + +/** + * Create an Astro middleware handler to integrate with the {@link Federation} + * object. + * + * @example src/middleware.ts + * ``` typescript + * import type { MiddlewareHandler } from "astro"; + * import { federation } from "./federation"; // Import the `Federation` object + * + * export const onRequest: MiddlewareHandler = createMiddleware( + * federation, + * (astroContext) => "context data", + * ); + * ``` + * + * @typeParam TContextData A type of the context data for the {@link Federation} + * object. + * @typeParam TAstroContext A type of the Astro context. + * @param federation A {@link Federation} object to integrate with Astro. + * @param contextDataFactory A factory function to create a context data for the + * {@link Federation} object. + * @returns An Astro middleware handler. + * @since 0.12.0 + */ +export function createMiddleware< + TContextData, + TAstroContext extends AstroContext, +>( + federation: Federation, + contextDataFactory: ContextDataFactory, +): MiddlewareHandler { + return async (context, next) => { + const contextData = await contextDataFactory(context); + return await federation.fetch(context.request, { + contextData, + ...createFetchOptions(context, next), + }); + }; +} diff --git a/x/fresh.ts b/x/fresh.ts index ef163dd..e99be0c 100644 --- a/x/fresh.ts +++ b/x/fresh.ts @@ -20,7 +20,8 @@ interface FreshContext { } /** - * Create options for the `federation` object to integrate with Fresh. + * Create options for the {@link Federation.fetch} method to integrate with + * Fresh. * * @example _middleware.ts * ``` typescript @@ -88,6 +89,7 @@ export function integrateFetchOptions( * @param createContextData A function to create a context data for the * {@link Federation} object. * @returns A Fresh middleware handler. + * @since 0.4.0 */ export function integrateHandler< TContextData, @@ -103,8 +105,7 @@ export function integrateHandler< request: Request, context: TFreshContext, ): Promise => { - let contextData = createContextData(request, context); - if (contextData instanceof Promise) contextData = await contextData; + const contextData = await createContextData(request, context); return await federation.fetch(request, { contextData, ...integrateFetchOptions(context),