Unverified Commit 542d3bc6 authored by Hong Minhee (洪 民憙)'s avatar Hong Minhee (洪 民憙) Committed by GitHub
Browse files

Merge pull request #395 from 2chanhaeng/next/fix

parents a6cb8d46 1d27e9b8
Loading
Loading
Loading
Loading
+1 −162
Original line number Diff line number Diff line
@@ -27,7 +27,7 @@ This package provides a simple way to integrate [Fedify] with [Next.js].
Usage
-----

~~~~ typescript
~~~~ typescript ignore
// --- middleware.ts ---
import { fedifyWith } from "@fedify/next";
import { federation } from "./federation";
@@ -63,164 +63,3 @@ export const config = {
  ],
};
~~~~

The integration code looks like this:

~~~~ typescript
/**
 * Fedify with Next.js
 * ===================
 *
 * This module provides a [Next.js] middleware to integrate with the Fedify.
 *
 * [Next.js]: https://nextjs.org/
 *
 * @module
 * @since 1.9.0
 */
import type { Federation, FederationFetchOptions } from "@fedify/fedify";
import { notFound } from "next/navigation";
import { NextResponse } from "next/server";
import { getXForwardedRequest } from "x-forwarded-fetch";

interface ContextDataFactory<TContextData> {
  (request: Request):
    | TContextData
    | Promise<TContextData>;
}
type ErrorHandlers = Omit<FederationFetchOptions<unknown>, "contextData">;

/**
 * Wrapper function for Next.js middleware to integrate with the
 * {@link Federation} object.
 *
 * @template TContextData A type of the context data for the
 *                         {@link Federation} object.
 * @param federation A {@link Federation} object to integrate with Next.js.
 * @param contextDataFactory A function to create a context data for the
 *                         {@link Federation} object.
 * @param errorHandlers A set of error handlers to handle errors during
 *                      the federation fetch.
 * @returns A Next.js middleware function to integrate with the
 *          {@link Federation} object.
 *
 * @example
 * ```ts
 * import { fedifyWith } from "@fedify/next";
 * import { federation } from "./federation";
 *
 * export default fedifyWith(federation)(
 *   function (request: Request) {
 *     // You can add custom logic here for other requests
 *     // except federation requests.  If there is no custom logic,
 *     // you can omit this function.
 *   }
 * )
 *
 * // This config makes middleware process only requests with the
 * // "Accept" header matching the federation accept regex.
 * // More details: https://nextjs.org/docs/app/api-reference/file-conventions/middleware#config-object-optional.
 * export const config = {
 *   runtime: "nodejs",
 *   matcher: [
 *     {
 *       source: "/:path*",
 *       has: [
 *         {
 *           type: "header",
 *           key: "Accept",
 *           value: ".*application\\/((jrd|activity|ld)\\+json|xrd\\+xml).*",
 *         },
 *       ],
 *     },
 *     {
 *       source: "/:path*",
 *       has: [
 *         {
 *           type: "header",
 *           key: "content-type",
 *           value: ".*application\\/((jrd|activity|ld)\\+json|xrd\\+xml).*",
 *         },
 *       ],
 *     },
 *     { source: "/.well-known/nodeinfo" },
 *     { source: "/.well-known/x-nodeinfo2" },
 *   ],
 * };
 * ```
 */
export const fedifyWith = <TContextData>(
  federation: Federation<TContextData>,
  contextDataFactory?: ContextDataFactory<TContextData>,
  errorHandlers?: Partial<ErrorHandlers>,
) =>
(
  middleware: (request: Request) => unknown =
    ((_: Request) => NextResponse.next()),
): (request: Request) => unknown =>
async (request: Request) => {
  if (hasFederationAcceptHeader(request)) {
    return await integrateFederation(
      federation,
      contextDataFactory,
      errorHandlers,
    )(request);
  }
  return await middleware(request);
};

/**
 * Check if the request has the "Accept" header matching the federation
 * accept regex.
 *
 * @param {Request} request The request to check.
 * @returns {boolean} `true` if the request has the "Accept" header matching
 *                    the federation accept regex, `false` otherwise.
 */
export const hasFederationAcceptHeader = (request: Request): boolean => {
  const acceptHeader = request.headers.get("Accept");
  // Check if the Accept header matches the federation accept regex.
  // If the header is not present, return false.
  return acceptHeader ? FEDERATION_ACCEPT_REGEX.test(acceptHeader) : false;
};
const FEDERATION_ACCEPT_REGEX =
  /.*application\/((jrd|activity|ld)\+json|xrd\+xml).*/;

/**
 * Create a Next.js handler to integrate with the {@link Federation} object.
 *
 * @template TContextData A type of the context data for the
 *                        {@link Federation} object.
 * @param federation A {@link Federation} object to integrate with Next.js.
 * @param contextDataFactory A function to create a context data for the
 *                           {@link Federation} object.
 * @param errorHandlers A set of error handlers to handle errors during
 *                      the federation fetch.
 * @returns A Next.js handler.
 */
export function integrateFederation<TContextData>(
  federation: Federation<TContextData>,
  contextDataFactory: ContextDataFactory<TContextData> = () =>
    undefined as TContextData,
  errorHandlers?: Partial<ErrorHandlers>,
) {
  return async (request: Request) => {
    const forwardedRequest = await getXForwardedRequest(request);
    const contextData = await contextDataFactory(forwardedRequest);
    return await federation.fetch(
      forwardedRequest,
      {
        contextData,
        onNotFound: notFound,
        onNotAcceptable,
        ...errorHandlers,
      },
    );
  };
}
const onNotAcceptable = () =>
  new Response("Not acceptable", {
    status: 406,
    headers: { "Content-Type": "text/plain", Vary: "Accept" },
  });
~~~~