@scalar/fastify-api-reference
Version:
a fastify plugin to render an API reference from an OpenAPI file
178 lines (177 loc) • 6.81 kB
JavaScript
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