From 3b9de3271e5dbea910d27c69b06dc67df29aa04b Mon Sep 17 00:00:00 2001 From: Hong Minhee Date: Sun, 17 Mar 2024 21:19:01 +0900 Subject: [PATCH] @fedify/fedify/x/fresh module --- CHANGES.md | 8 +++ deno.json | 3 +- docs/manual/federation.md | 13 ++++ examples/blog/import_map.g.json | 1 + examples/blog/import_map.json | 1 + examples/blog/routes/_middleware.ts | 43 +---------- x/fresh.ts | 108 ++++++++++++++++++++++++++++ 7 files changed, 136 insertions(+), 41 deletions(-) create mode 100644 x/fresh.ts diff --git a/CHANGES.md b/CHANGES.md index 95f84ef4..5b256346 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -13,6 +13,14 @@ Version 0.4.0 To be released. + - Added `@fedify/fedify/x/fresh` module for integrating with [Fresh] + middleware. + + - Added `integrateHandler()` function. + - Added `integrateHandlerOptions()` function. + +[Fresh]: https://fresh.deno.dev/ + Version 0.3.0 ------------- diff --git a/deno.json b/deno.json index e433d4a0..447984a8 100644 --- a/deno.json +++ b/deno.json @@ -8,7 +8,8 @@ "./nodeinfo": "./nodeinfo/mod.ts", "./runtime": "./runtime/mod.ts", "./vocab": "./vocab/mod.ts", - "./webfinger": "./webfinger/mod.ts" + "./webfinger": "./webfinger/mod.ts", + "./x/fresh": "./x/fresh.ts" }, "imports": { "@cfworker/json-schema": "npm:@cfworker/json-schema@^1.12.8", diff --git a/docs/manual/federation.md b/docs/manual/federation.md index e082eb15..d2db51b8 100644 --- a/docs/manual/federation.md +++ b/docs/manual/federation.md @@ -142,6 +142,19 @@ as [`Request`] and [`Response`] objects. In that case, you need to convert the request and response objects to the appropriate types that the `Federation` object can handle. +> [!NOTE] +> The above example artificially shows a verbose way to integrate +> the `Federation` object with Fresh, so that a user of other web frameworks +> can understand the concept. In practice, you can define a middleware +> using `integrateHandler()` function from `@fedify/fedify/x/fresh` module: +> +> ~~~~ typescript +> import { federation } from "../federation.ts"; // Import the `Federation` object +> import { integrateHandler } from "jsr:@fedify/fedify/x/fresh"; +> +> export const handler = integrateHandler(federation, () => undefined); +> ~~~~ + > [!TIP] > In theory, you can directly pass `Federation.handle()` to the [`Deno.serve()`] > function, but you probably wouldn't want to do that because you want to handle diff --git a/examples/blog/import_map.g.json b/examples/blog/import_map.g.json index 1636a2ac..447f9a55 100644 --- a/examples/blog/import_map.g.json +++ b/examples/blog/import_map.g.json @@ -31,6 +31,7 @@ "@fedify/fedify/runtime": "../../runtime/mod.ts", "@fedify/fedify/vocab": "../../vocab/mod.ts", "@fedify/fedify/webfinger": "../../webfinger/mod.ts", + "@fedify/fedify/x/fresh": "../../x/fresh.ts", "markdown-it": "npm:markdown-it@^14.0.0", "preact": "https://esm.sh/preact@10.19.2", "preact/": "https://esm.sh/preact@10.19.2/", diff --git a/examples/blog/import_map.json b/examples/blog/import_map.json index ebe6f1bc..4a01254b 100644 --- a/examples/blog/import_map.json +++ b/examples/blog/import_map.json @@ -13,6 +13,7 @@ "@fedify/fedify/runtime": "../../runtime/mod.ts", "@fedify/fedify/vocab": "../../vocab/mod.ts", "@fedify/fedify/webfinger": "../../webfinger/mod.ts", + "@fedify/fedify/x/fresh": "../../x/fresh.ts", "markdown-it": "npm:markdown-it@^14.0.0", "preact": "https://esm.sh/preact@10.19.2", "preact/": "https://esm.sh/preact@10.19.2/", diff --git a/examples/blog/routes/_middleware.ts b/examples/blog/routes/_middleware.ts index 0f60fdc6..00040b3c 100644 --- a/examples/blog/routes/_middleware.ts +++ b/examples/blog/routes/_middleware.ts @@ -1,43 +1,6 @@ -import { FreshContext } from "$fresh/server.ts"; +import { Handler } from "$fresh/server.ts"; import { federation } from "../federation/mod.ts"; +import { integrateHandler } from "@fedify/fedify/x/fresh"; // This is the entry point to the Fedify middleware from the Fresh framework: -export async function handler(request: Request, context: FreshContext) { - // The `federation` object purposes to handle federation-related requests. - // It is responsible for handling, for example, WebFinger queries, actor - // dispatching, and incoming activities to the inbox. The definition of - // the object is in the federation/mod.ts file. - return await federation.handle(request, { - // The context data is not used in this example, but it can be used to - // store data (e.g., database connections) that is shared between - // the different federation-related callbacks: - contextData: undefined, - - // 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 Fresh framework to continue the request handling - // by the Fresh: - onNotFound: context.next.bind(context), - - // 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 Fresh 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 Fresh to share the same routes - // and they do content negotiation depending on `Accept` header. - // For instance, in this example, `/users/{handle}` can return JSON-LD - // by the Fedify and redirects to the home page by the Fresh: - async onNotAcceptable(_request: Request) { - const response = await context.next(); - if (response.status !== 404) return response; - return new Response("Not acceptable", { - status: 406, - headers: { - "Content-Type": "text/plain", - Vary: "Accept", - }, - }); - }, - }); -} +export const handler: Handler = integrateHandler(federation, () => undefined); diff --git a/x/fresh.ts b/x/fresh.ts new file mode 100644 index 00000000..82783974 --- /dev/null +++ b/x/fresh.ts @@ -0,0 +1,108 @@ +/** + * Fedify with Fresh + * ================= + * + * This module contains some utilities for integrating Fedify with [Fresh], + * a web framework for Deno. + * + * [Fresh]: https://fresh.deno.dev/ + * + * @module + */ +import { + Federation, + FederationHandlerParameters, +} from "../federation/middleware.ts"; + +interface FreshContext { + next(): Promise; +} + +/** + * Create options for the `federation` object to integrate with Fresh. + * + * @example _middleware.ts + * ``` typescript + * import { FreshContext } from "$fresh/server.ts"; + * import { federation } from "federation.ts"; // Import the `Federation` object + * + * export async function handler(request: Request, context: FreshContext) { + * return await federation.handle(request, { + * contextData: undefined, + * ...integrateHandlerOptions(context), + * }) + * } + * ``` + * + * @param context A Fresh context. + * @returns Options for the {@link Federation.handle} method. + */ +export function integrateHandlerOptions( + context: FreshContext, +): 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 Fresh framework to continue the request handling + // by the Fresh: + onNotFound: context.next.bind(context), + + // 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 Fresh 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 Fresh to share the same routes + // and they do content negotiation depending on `Accept` header: + async onNotAcceptable(_request: Request) { + const response = await context.next(); + if (response.status !== 404) return response; + return new Response("Not acceptable", { + status: 406, + headers: { + "Content-Type": "text/plain", + Vary: "Accept", + }, + }); + }, + }; +} + +/** + * Create a Fresh middleware handler to integrate with the {@link Federation} + * object. + * + * @example _middleware.ts + * ``` typescript + * import { federation } from "federation.ts"; // Import the `Federation` object + * + * export const handler = integrateHandler(federation, () => undefined); + * ``` + * + * @param federation A {@link Federation} object to integrate with Fresh. + * @param createContextData A function to create a context data for the + * {@link Federation} object. + * @returns A Fresh middleware handler. + */ +export function integrateHandler< + TContextData, + TFreshContext extends FreshContext, +>( + federation: Federation, + createContextData: ( + req: Request, + ctx: TFreshContext, + ) => TContextData | Promise, +): (req: Request, ctx: TFreshContext) => Promise { + return async ( + request: Request, + context: TFreshContext, + ): Promise => { + let contextData = createContextData(request, context); + if (contextData instanceof Promise) contextData = await contextData; + return await federation.handle(request, { + contextData, + ...integrateHandlerOptions(context), + }); + }; +}