UNPKG

@scalar/fastify-api-reference

Version:

a fastify plugin to render an API reference from an OpenAPI file

178 lines (177 loc) 6.81 kB
import { getHtmlDocument } from "@scalar/core/libs/html-rendering"; import { normalize, toJson, toYaml } from "@scalar/openapi-parser"; import fp from "fastify-plugin"; import { slug } from "github-slugger"; import { getJavaScriptFile } from "./utils/getJavaScriptFile.js"; const RELATIVE_JAVASCRIPT_PATH = "js/scalar.js"; const schemaToHideRoute = { hide: true }; const getRoutePrefix = (routePrefix) => { const prefix = routePrefix ?? "/reference"; return prefix.endsWith("/") ? prefix.slice(0, -1) : prefix; }; const getOpenApiDocumentEndpoints = (openApiDocumentEndpoints) => { const { json = "/openapi.json", yaml = "/openapi.yaml" } = openApiDocumentEndpoints ?? {}; return { json, yaml }; }; const getJavaScriptUrl = (routePrefix) => `${getRoutePrefix(routePrefix)}/${RELATIVE_JAVASCRIPT_PATH}`.replace(/\/\//g, "/"); const customTheme = ""; const DEFAULT_CONFIGURATION = { _integration: "fastify" }; const fastifyApiReference = fp( async (fastify, options) => { const { configuration: givenConfiguration } = options; let configuration = { ...DEFAULT_CONFIGURATION, ...givenConfiguration }; const specSource = (() => { const { content, url } = configuration ?? {}; if (content) { return { type: "content", get: () => { if (typeof content === "function") { return content(); } return content; } }; } if (url) { return { type: "url", get: () => url }; } if (fastify.hasPlugin("@fastify/swagger") && typeof fastify.swagger === "function") { return { type: "swagger", get: () => fastify.swagger() }; } return void 0; })(); if (!specSource) { fastify.log.warn( "[@scalar/fastify-api-reference] You didn't provide a `content` or `url`, and @fastify/swagger could not be found. Please provide one of these options." ); return; } const fileContent = getJavaScriptFile(); const hooks = {}; if (options.hooks) { const additionalHooks = ["onRequest", "preHandler"]; for (const hook of additionalHooks) { if (options.hooks[hook]) { hooks[hook] = options.hooks[hook]; } } } const getSpecFilenameSlug = async (spec) => { return slug(spec?.specification?.info?.title ?? "spec"); }; const openApiSpecUrlJson = `${getRoutePrefix(options.routePrefix)}${getOpenApiDocumentEndpoints(options.openApiDocumentEndpoints).json}`; fastify.route({ method: "GET", url: openApiSpecUrlJson, // @ts-ignore We don't know whether @fastify/swagger is loaded. schema: schemaToHideRoute, ...hooks, ...options.logLevel && { logLevel: options.logLevel }, async handler(_, reply) { const spec = normalize(specSource.get()); const filename = await getSpecFilenameSlug(spec); const json = JSON.parse(toJson(spec)); return reply.header("Content-Type", "application/json").header("Content-Disposition", `filename=${filename}.json`).header("Access-Control-Allow-Origin", "*").header("Access-Control-Allow-Methods", "*").send(json); } }); const openApiSpecUrlYaml = `${getRoutePrefix(options.routePrefix)}${getOpenApiDocumentEndpoints(options.openApiDocumentEndpoints).yaml}`; fastify.route({ method: "GET", url: openApiSpecUrlYaml, // @ts-ignore We don't know whether @fastify/swagger is loaded. schema: schemaToHideRoute, ...hooks, ...options.logLevel && { logLevel: options.logLevel }, async handler(_, reply) { const spec = normalize(specSource.get()); const filename = await getSpecFilenameSlug(spec); const yaml = toYaml(spec); return reply.header("Content-Type", "application/yaml").header("Content-Disposition", `filename=${filename}.yaml`).header("Access-Control-Allow-Origin", "*").header("Access-Control-Allow-Methods", "*").send(yaml); } }); const ignoreTrailingSlash = ( // @ts-expect-error We're still on Fastify 4, this is introduced in Fastify 5 fastify.initialConfig?.routerOptions?.ignoreTrailingSlash === true || fastify.initialConfig?.ignoreTrailingSlash === true ); if (!ignoreTrailingSlash && getRoutePrefix(options.routePrefix)) { fastify.route({ method: "GET", url: getRoutePrefix(options.routePrefix), // @ts-ignore We don't know whether @fastify/swagger is loaded. schema: schemaToHideRoute, ...hooks, ...options.logLevel && { logLevel: options.logLevel }, handler(_, reply) { return reply.redirect(getRoutePrefix(options.routePrefix) + "/", 302); } }); } fastify.route({ method: "GET", url: `${getRoutePrefix(options.routePrefix)}/`, // We don't know whether @fastify/swagger is registered, but it doesn't hurt to add a schema anyway. // @ts-ignore We don't know whether @fastify/swagger is loaded. schema: schemaToHideRoute, ...hooks, ...options.logLevel && { logLevel: options.logLevel }, handler(_, reply) { const currentUrl = new URL(_.url, `${_.protocol}://${_.hostname}`); if (!currentUrl.pathname.endsWith("/")) { return reply.redirect(`${currentUrl.pathname}/`, 301); } if (specSource.type !== "url") { configuration = { ...configuration, // Use a relative URL in case we're proxied url: `.${getOpenApiDocumentEndpoints(options.openApiDocumentEndpoints).json}` }; } return reply.header("Content-Type", "text/html; charset=utf-8").send( getHtmlDocument( { // We're using the bundled JS here by default, but the user can pass a CDN URL. cdn: RELATIVE_JAVASCRIPT_PATH, ...configuration }, customTheme ) ); } }); fastify.route({ method: "GET", url: getJavaScriptUrl(options.routePrefix), // We don't know whether @fastify/swagger is registered, but it doesn't hurt to add a schema anyway. // @ts-ignore We don't know whether @fastify/swagger is loaded. schema: schemaToHideRoute, ...hooks, ...options.logLevel && { logLevel: options.logLevel }, handler(_, reply) { return reply.header("Content-Type", "application/javascript; charset=utf-8").send(fileContent); } }); }, { name: "@scalar/fastify-api-reference" } ); var fastifyApiReference_default = fastifyApiReference; export { customTheme, fastifyApiReference_default as default }; //# sourceMappingURL=fastifyApiReference.js.map