diff --git a/src/index.ts b/src/index.ts index 3ff0bd5..2408a58 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,118 +1,118 @@ /* eslint-disable @typescript-eslint/ban-ts-comment */ -import { Elysia, type InternalRoute } from "elysia"; +import { Elysia, type InternalRoute } from 'elysia' -import { SwaggerUIRender } from "./swagger"; -import { ScalarRender } from "./scalar"; +import { SwaggerUIRender } from './swagger' +import { ScalarRender } from './scalar' -import { filterPaths, registerSchemaPath } from "./utils"; +import { filterPaths, registerSchemaPath } from './utils' -import type { OpenAPIV3 } from "openapi-types"; -import type { ReferenceConfiguration } from "./scalar/types"; -import type { ElysiaSwaggerConfig } from "./types"; +import type { OpenAPIV3 } from 'openapi-types' +import type { ReferenceConfiguration } from './scalar/types' +import type { ElysiaSwaggerConfig } from './types' /** * Plugin for [elysia](https://github.com/elysiajs/elysia) that auto-generate Swagger page. * * @see https://github.com/elysiajs/elysia-swagger */ -export const swagger = async ( +export const swagger = async ( { - provider = "scalar", - scalarVersion = "latest", - scalarCDN = "", + provider = 'scalar', + scalarVersion = 'latest', + scalarCDN = '', scalarConfig = {}, documentation = {}, - version = "5.9.0", + version = '5.9.0', excludeStaticFile = true, - path = "/swagger" as Path, + path = '/swagger' as Path, exclude = [], swaggerOptions = {}, theme = `https://unpkg.com/swagger-ui-dist@${version}/swagger-ui.css`, autoDarkMode = true, - excludeMethods = ["OPTIONS"], - excludeTags = [], + excludeMethods = ['OPTIONS'], + excludeTags = [] }: ElysiaSwaggerConfig = { - provider: "scalar", - scalarVersion: "latest", - scalarCDN: "", + provider: 'scalar', + scalarVersion: 'latest', + scalarCDN: '', scalarConfig: {}, documentation: {}, - version: "5.9.0", + version: '5.9.0', excludeStaticFile: true, - path: "/swagger" as Path, + path: '/swagger' as Path, exclude: [], swaggerOptions: {}, autoDarkMode: true, - excludeMethods: ["OPTIONS"], - excludeTags: [], - }, + excludeMethods: ['OPTIONS'], + excludeTags: [] + } ) => { - const schema = {}; - let totalRoutes = 0; + const schema = {} + let totalRoutes = 0 if (!version) - version = `https://unpkg.com/swagger-ui-dist@${version}/swagger-ui.css`; + version = `https://unpkg.com/swagger-ui-dist@${version}/swagger-ui.css` const info = { - title: "Elysia Documentation", - description: "Development documentation", - version: "0.0.0", - ...documentation.info, - }; + title: 'Elysia Documentation', + description: 'Development documentation', + version: '0.0.0', + ...documentation.info + } - const relativePath = path.startsWith("/") ? path.slice(1) : path; + const relativePath = path.startsWith('/') ? path.slice(1) : path - const app = new Elysia({ name: "@elysiajs/swagger" }); + const app = new Elysia({ name: '@elysiajs/swagger' }) app.get(path, function documentation() { const combinedSwaggerOptions = { url: `${relativePath}/json`, - dom_id: "#swagger-ui", - ...swaggerOptions, - }; + dom_id: '#swagger-ui', + ...swaggerOptions + } const stringifiedSwaggerOptions = JSON.stringify( combinedSwaggerOptions, (key, value) => { - if (typeof value == "function") return undefined; + if (typeof value == 'function') return undefined - return value; - }, - ); + return value + } + ) const scalarConfiguration: ReferenceConfiguration = { spec: { ...scalarConfig.spec, - url: `${relativePath}/json`, + url: `${relativePath}/json` }, - ...scalarConfig, - }; + ...scalarConfig + } return new Response( - provider === "swagger-ui" + provider === 'swagger-ui' ? SwaggerUIRender( info, version, theme, stringifiedSwaggerOptions, - autoDarkMode, + autoDarkMode ) : ScalarRender(scalarVersion, scalarConfiguration, scalarCDN), { headers: { - "content-type": "text/html; charset=utf8", - }, - }, - ); - }).get(path === "/" ? "/json" : `${path}/json`, function openAPISchema() { + 'content-type': 'text/html; charset=utf8' + } + } + ) + }).get(path === '/' ? '/json' : `${path}/json`, function openAPISchema() { // @ts-expect-error Private property - const routes = app.getGlobalRoutes() as InternalRoute[]; + const routes = app.getGlobalRoutes() as InternalRoute[] if (routes.length !== totalRoutes) { - totalRoutes = routes.length; + totalRoutes = routes.length routes.forEach((route: InternalRoute) => { - if (excludeMethods.includes(route.method)) return; + if (excludeMethods.includes(route.method)) return registerSchemaPath({ schema, @@ -121,45 +121,45 @@ export const swagger = async ( path: route.path, // @ts-ignore models: app.definitions?.type, - contentType: route.hooks.type, - }); - }); + contentType: route.hooks.type + }) + }) } return { - openapi: "3.0.3", + openapi: '3.0.3', ...{ ...documentation, tags: documentation.tags?.filter( - (tag) => !excludeTags?.includes(tag?.name), + (tag) => !excludeTags?.includes(tag?.name) ), info: { - title: "Elysia Documentation", - description: "Development documentation", - version: "0.0.0", - ...documentation.info, - }, + title: 'Elysia Documentation', + description: 'Development documentation', + version: '0.0.0', + ...documentation.info + } }, paths: { - ...filterPaths(schema, { + ...filterPaths(schema, relativePath, { excludeStaticFile, - exclude: Array.isArray(exclude) ? exclude : [exclude], + exclude: Array.isArray(exclude) ? exclude : [exclude] }), - ...documentation.paths, + ...documentation.paths }, components: { ...documentation.components, schemas: { // @ts-ignore ...app.definitions?.type, - ...documentation.components?.schemas, - }, - }, - } satisfies OpenAPIV3.Document; - }); + ...documentation.components?.schemas + } + } + } satisfies OpenAPIV3.Document + }) - // This is intentional to prevent deeply nested type - return app; -}; + return app +} -export default swagger; +export type { ElysiaSwaggerConfig } +export default swagger diff --git a/src/utils.ts b/src/utils.ts index a52ea84..814bf20 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,5 +1,6 @@ /* eslint-disable @typescript-eslint/ban-ts-comment */ /* eslint-disable @typescript-eslint/no-unused-vars */ +import path from 'path' import type { HTTPMethod, LocalHook } from 'elysia' import { Kind, type TSchema } from '@sinclair/typebox' @@ -295,30 +296,36 @@ export const registerSchemaPath = ({ } export const filterPaths = ( - paths: Record, - { - excludeStaticFile = true, - exclude = [] - }: { - excludeStaticFile: boolean - exclude: (string | RegExp)[] - } + paths: Record, + docsPath: string, + { + excludeStaticFile = true, + exclude = [] + }: { + excludeStaticFile: boolean + exclude: (string | RegExp)[] + } ) => { const newPaths: Record = {} - for (const [key, value] of Object.entries(paths)) - if ( - !exclude.some((x) => { - if (typeof x === 'string') return key === x - - return x.test(key) - }) && - !key.includes('/swagger') && - !key.includes('*') && - (excludeStaticFile ? !key.includes('.') : true) - ) { - Object.keys(value).forEach((method) => { - const schema = value[method] + // exclude docs path and OpenAPI json path + const excludePaths = [`/${docsPath}`, `/${docsPath}/json`].map((p) => + path.normalize(p) + ) + + for (const [key, value] of Object.entries(paths)) + if ( + !exclude.some((x) => { + if (typeof x === 'string') return key === x + + return x.test(key) + }) && + !excludePaths.includes(key) && + !key.includes('*') && + (excludeStaticFile ? !key.includes('.') : true) + ) { + Object.keys(value).forEach((method) => { + const schema = value[method] if (key.includes('{')) { if (!schema.parameters) schema.parameters = []