Merge pull request #85 from marclave/marc/scalar-integration

feat: Scalar ElysiaJS integration
This commit is contained in:
SaltyAom
2024-01-07 14:09:23 +07:00
committed by GitHub
8 changed files with 333 additions and 51 deletions
+7
View File
@@ -47,6 +47,13 @@ Then go to `http://localhost:8080/swagger`.
# config
## provider
@default 'scalar'
Choose between [Scalar](https://swagger.io/specification/v2/) & [SwaggerUI](https://github.com/swagger-api/swagger-ui)
## scalar
Customize scalarConfig, refers to [Scalar config](https://github.com/scalar/scalar)
## swagger
Customize Swagger config, refers to [Swagger 2.0 config](https://swagger.io/specification/v2/)
BIN
View File
Binary file not shown.
+2 -1
View File
@@ -47,9 +47,10 @@
"typescript": "^5.0.4"
},
"dependencies": {
"@scalar/api-reference": "^1.12.5",
"@types/lodash.clonedeep": "^4.5.7",
"@types/swagger-ui": "^3.52.2",
"lodash.clonedeep": "^4.5.0",
"openapi-types": "^12.1.3"
}
}
}
+16 -50
View File
@@ -4,7 +4,10 @@ import { type Elysia, type InternalRoute } from 'elysia'
import { filterPaths, registerSchemaPath } from './utils'
import type { OpenAPIV3 } from 'openapi-types'
import type { ReferenceConfiguration } from '@scalar/api-reference'
import type { ElysiaSwaggerConfig } from './types'
import { SwaggerUIRender } from './swagger-ui'
import { ScalarRender } from './scalar'
/**
* Plugin for [elysia](https://github.com/elysiajs/elysia) that auto-generate Swagger page.
@@ -14,6 +17,9 @@ import type { ElysiaSwaggerConfig } from './types'
export const swagger =
<Path extends string = '/swagger'>(
{
provider = 'scalar',
scalarVersion = '1.12.5',
scalarConfig = {},
documentation = {},
version = '5.9.0',
excludeStaticFile = true,
@@ -23,6 +29,9 @@ export const swagger =
theme = `https://unpkg.com/swagger-ui-dist@${version}/swagger-ui.css`,
autoDarkMode = true
}: ElysiaSwaggerConfig<Path> = {
provider: 'scalar',
scalarVersion: '1.12.5',
scalarConfig: {},
documentation: {},
version: '5.9.0',
excludeStaticFile: true,
@@ -65,58 +74,15 @@ export const swagger =
}
)
return new Response(
`<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>${info.title}</title>
<meta
name="description"
content="${info.description}"
/>
<meta
name="og:description"
content="${info.description}"
/>
${
autoDarkMode && typeof theme === 'string'
? `
<style>
@media (prefers-color-scheme: dark) {
body {
background-color: #222;
color: #faf9a;
}
.swagger-ui {
filter: invert(92%) hue-rotate(180deg);
const scalarConfiguration: ReferenceConfiguration = {
spec: {
url: `${relativePath}/json`
},
...scalarConfig
}
.swagger-ui .microlight {
filter: invert(100%) hue-rotate(180deg);
}
}
</style>`
: ''
}
${
typeof theme === 'string'
? `<link rel="stylesheet" href="${theme}" />`
: `<link rel="stylesheet" media="(prefers-color-scheme: light)" href="${theme.light}" />
<link rel="stylesheet" media="(prefers-color-scheme: dark)" href="${theme.dark}" />`
}
</head>
<body>
<div id="swagger-ui"></div>
<script src="https://unpkg.com/swagger-ui-dist@${version}/swagger-ui-bundle.js" crossorigin></script>
<script>
window.onload = () => {
window.ui = SwaggerUIBundle(${stringifiedSwaggerOptions});
};
</script>
</body>
</html>`,
return new Response(provider === 'swagger-ui' ? SwaggerUIRender(info, version, theme, stringifiedSwaggerOptions, autoDarkMode) : ScalarRender(scalarVersion, scalarConfiguration),
{
headers: {
'content-type': 'text/html; charset=utf8'
+196
View File
@@ -0,0 +1,196 @@
export default `
/* basic theme */
.light-mode {
--theme-color-1: #2a2f45;
--theme-color-2: #757575;
--theme-color-3: #8e8e8e;
--theme-color-accent: #f06292;
--theme-background-1: #fff;
--theme-background-2: #f6f6f6;
--theme-background-3: #e7e7e7;
--theme-background-accent: #f062921f;
--theme-border-color: rgba(0, 0, 0, 0.1);
}
.dark-mode {
--theme-color-1: rgba(255, 255, 255, 0.9);
--theme-color-2: rgba(156, 163, 175, 1);
--theme-color-3: rgba(255, 255, 255, 0.44);
--theme-color-accent: #f06292;
--theme-background-1: #111728;
--theme-background-2: #1e293b;
--theme-background-3: #334155;
--theme-background-accent: #f062921f;
--theme-border-color: rgba(255, 255, 255, 0.1);
}
/* Document Sidebar */
.light-mode .sidebar,
.dark-mode .sidebar {
--sidebar-background-1: var(--theme-background-1);
--sidebar-item-hover-color: currentColor;
--sidebar-item-hover-background: var(--theme-background-2);
--sidebar-item-active-background: var(--theme-background-accent);
--sidebar-border-color: transparent;
--sidebar-color-1: var(--theme-color-1);
--sidebar-color-2: var(--theme-color-2);
--sidebar-color-active: var(--theme-color-accent);
--sidebar-search-background: transparent;
--sidebar-search-border-color: var(--theme-border-color);
--sidebar-search--color: var(--theme-color-3);
}
/* Document header only shows on mobile*/
.dark-mode .t-doc__header,
.light-mode .t-doc__header {
--header-background-1: rgba(255, 255, 255, 0.85);
--header-border-color: transparent;
--header-color-1: var(--theme-color-1);
--header-color-2: var(--theme-color-2);
--header-background-toggle: var(--theme-color-3);
--header-call-to-action-color: var(--theme-color-accent);
}
.dark-mode .t-doc__header {
--header-background-1: rgba(17, 23, 40, 0.75);
}
/* advanced */
.light-mode {
--theme-button-1: rgb(49 53 56);
--theme-button-1-color: #fff;
--theme-button-1-hover: rgb(28 31 33);
--theme-color-green: #069061;
--theme-color-red: #ef0006;
--theme-color-yellow: #edbe20;
--theme-color-blue: #0082d0;
--theme-color-orange: #fb892c;
--theme-color-purple: #5203d1;
--theme-scrollbar-color: rgba(0, 0, 0, 0.18);
--theme-scrollbar-color-active: rgba(0, 0, 0, 0.36);
}
.dark-mode {
--theme-button-1: #f6f6f6;
--theme-button-1-color: #000;
--theme-button-1-hover: #e7e7e7;
--theme-color-green: #a3ffa9;
--theme-color-red: #ffa3a3;
--theme-color-yellow: #fffca3;
--theme-color-blue: #a5d6ff;
--theme-color-orange: #e2ae83;
--theme-color-purple: #d2a8ff;
--theme-scrollbar-color: rgba(255, 255, 255, 0.24);
--theme-scrollbar-color-active: rgba(255, 255, 255, 0.48);
}
/* Elysia Specific */
.scalar-api-client__send-request-button,
.show-api-client-button {
background: #3c82f6 !important;
}
.show-api-client-button:before {
display: none;
}
.sidebar-search:hover {
transition: all 0.15s ease-in-out;
--sidebar-search-border-color: var(--theme-color-accent) !important;
color: var(--sidebar-color-1) !important;
}
.scalar-api-client__container .sidebar {
--sidebar-border-color: var(--theme-border-color);
}
@media (min-width: 1150px) {
.section-container:has( ~ .footer):before,
.tag-section-container:before {
content: "";
position: absolute;
top: -5px;
left: 0;
width: 100%;
height: 10px;
background: linear-gradient(90deg, var(--theme-background-1) 3%,transparent 10%);
}
}
.section-flare {
position: absolute;
width: 100vw;
height: 300px;
--stripes: repeating-linear-gradient(
100deg,
#fff 0%,
#fff 7%,
transparent 10%,
transparent 12%,
#fff 16%
);
--stripesDark: repeating-linear-gradient(
100deg,
#000 0%,
#000 7%,
transparent 10%,
transparent 12%,
#000 16%
);
--rainbow: repeating-linear-gradient(
100deg,
#60a5fa 10%,
#e879f9 16%,
#5eead4 22%,
#60a5fa 30%
);
background-image: var(--stripes), var(--rainbow);
background-size: 300%, 200%;
background-position: 50% 50%, 50% 50%;
filter: invert(100%);
-webkit-mask-image: radial-gradient(
ellipse at 100% 0%,
black 40%,
transparent 70%
);
mask-image: radial-gradient(ellipse at 100% 0%, black 40%, transparent 70%);
pointer-events: none;
opacity: 0.15;
}
.dark-mode .section-flare {
background-image: var(--stripesDark), var(--rainbow);
filter: opacity(50%) saturate(200%);
opacity: 0.25;
}
.section-flare:after {
content: "";
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
background-image: var(--stripes), var(--rainbow);
background-size: 200%, 100%;
background-attachment: fixed;
mix-blend-mode: difference;
}
.dark-mode .section-flare:after {
background-image: var(--stripesDark), var(--rainbow);
}
@keyframes headerbackground {
from {
background: transparent;
backdrop-filter: none;
}
to {
background: var(--header-background-1);
backdrop-filter: blur(12px);
}
}
.light-mode .t-doc__header,
.dark-mode .t-doc__header {
animation: headerbackground forwards;
animation-timeline: scroll();
animation-range: 0px 200px;
--header-border-color: transparent;
}
`
+27
View File
@@ -0,0 +1,27 @@
import type { ReferenceConfiguration } from '@scalar/api-reference'
import scalarElysiaTheme from './scalar-elysia-theme'
export const ScalarRender = (version: string, config: ReferenceConfiguration) => `<!doctype html>
<html>
<head>
<title>API Reference</title>
<meta charset="utf-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1" />
<style>
body {
margin: 0;
}
</style>
<style>
${config.customCss ?? scalarElysiaTheme}
</style>
</head>
<body>
<script
id="api-reference"
data-url="${config.spec?.url}"></script>
<script src="https://cdn.jsdelivr.net/npm/@scalar/api-reference@${version}/dist/browser/standalone.min.js"></script>
</body>
</html>`
+56
View File
@@ -0,0 +1,56 @@
import { SwaggerInfo } from './types'
export const SwaggerUIRender = (info: SwaggerInfo, version: string, theme: string | {
light: string
dark: string
}, stringifiedSwaggerOptions: string, autoDarkMode?: boolean) => `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>${info.title}</title>
<meta
name="description"
content="${info.description}"
/>
<meta
name="og:description"
content="${info.description}"
/>
${
autoDarkMode && typeof theme === 'string'
? `
<style>
@media (prefers-color-scheme: dark) {
body {
background-color: #222;
color: #faf9a;
}
.swagger-ui {
filter: invert(92%) hue-rotate(180deg);
}
.swagger-ui .microlight {
filter: invert(100%) hue-rotate(180deg);
}
}
</style>`
: ''
}
${
typeof theme === 'string'
? `<link rel="stylesheet" href="${theme}" />`
: `<link rel="stylesheet" media="(prefers-color-scheme: light)" href="${theme.light}" />
<link rel="stylesheet" media="(prefers-color-scheme: dark)" href="${theme.dark}" />`
}
</head>
<body>
<div id="swagger-ui"></div>
<script src="https://unpkg.com/swagger-ui-dist@${version}/swagger-ui-bundle.js" crossorigin></script>
<script>
window.onload = () => {
window.ui = SwaggerUIBundle(${stringifiedSwaggerOptions});
};
</script>
</body>
</html>`
+29
View File
@@ -1,5 +1,12 @@
import type { OpenAPIV3 } from 'openapi-types'
import type { SwaggerUIOptions } from 'swagger-ui'
import type { ReferenceConfiguration } from '@scalar/api-reference'
export type SwaggerInfo = {
title: string
description: string
version: string
}
export interface ElysiaSwaggerConfig<Path extends string = '/swagger'> {
/**
@@ -12,6 +19,28 @@ export interface ElysiaSwaggerConfig<Path extends string = '/swagger'> {
| 'x-express-openapi-additional-middleware'
| 'x-express-openapi-validation-strict'
>
/**
* Choose your provider, Scalar or Swagger UI
*
* @default 'scalar'
* @see https://github.com/scalar/scalar
* @see https://github.com/swagger-api/swagger-ui
*/
provider?: 'scalar' | 'swagger-ui'
/**
* Version to use for Scalar cdn bundle
*
* @default '1.12.5'
* @see https://github.com/scalar/scalar
*/
scalarVersion?: string
/**
* Scalar configuration to customize scalar
*
* @default '1.12.5'
* @see https://github.com/scalar/scalar
*/
scalarConfig?: ReferenceConfiguration
/**
* Version to use for swagger cdn bundle
*