feat: swagger ui middleware (#230)
* feat(zod-openapi): create swagger-ui jsx component * feat(zod-openapi): add swagger ui docs * chore(zod-openapi): add versioning doc * chore(zod-openapi): fix SwaggerUI component doc * refactor(zod-openapi): remove jsx comment * feat(swagger-ui): create new package * feat(swagger-ui): provides swagger ui middleware and swaggerui component * feat(zod-openapi): Changed to use @hono/swagger-ui * chore: add versioning doc * feat: update ci for @hono/swagger-ui * refactor: remove package-lock.json * refactor: reverted the extra changes. * chore: remove old changeset doc * refactor(swagger-ui): remove unused file * refactor(swagger-ui): change input type * feat(swagger-ui): Select only the options you need. * chore(swagger-ui): update README * refactor(swagger-ui): rewrite simple * refactor(zod-openapi): remove swagger-ui * chore(swagger-ui): fix readme content * chore(dep): add @types/swagger-ui-dist for option types support * feat: implement SwaggerConfig Renderer for mapping config object to html * feat: extend some option support for swagger-ui-dist * test: move option rendering test and add some cases * feat: add manually option for making full customizable * docs: update swagger-ui middleware README * fix: do not escape HTML strings * docs: update README of swagger-ui middleware * ci: update workflow environment for swagger-ui middleware --------- Co-authored-by: naporin0624 <naporin0624@gmail.com>pull/232/head
parent
296446be5a
commit
c608fa9532
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
'@hono/swagger-ui': minor
|
||||||
|
---
|
||||||
|
|
||||||
|
Create a package that provides swagger ui in hono
|
|
@ -0,0 +1,25 @@
|
||||||
|
name: ci-swagger-ui
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
paths:
|
||||||
|
- 'packages/swagger-ui/**'
|
||||||
|
pull_request:
|
||||||
|
branches: ['*']
|
||||||
|
paths:
|
||||||
|
- 'packages/swagger-ui/**'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
ci:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
working-directory: ./packages/swagger-ui
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 20.x
|
||||||
|
- run: yarn install --frozen-lockfile
|
||||||
|
- run: yarn build
|
||||||
|
- run: yarn test
|
|
@ -21,6 +21,7 @@
|
||||||
"build:valibot-validator": "yarn workspace @hono/valibot-validator build",
|
"build:valibot-validator": "yarn workspace @hono/valibot-validator build",
|
||||||
"build:zod-openapi": "yarn build:zod-validator && yarn workspace @hono/zod-openapi build",
|
"build:zod-openapi": "yarn build:zod-validator && yarn workspace @hono/zod-openapi build",
|
||||||
"build:typia-validator": "yarn workspace @hono/typia-validator build",
|
"build:typia-validator": "yarn workspace @hono/typia-validator build",
|
||||||
|
"build:swagger-ui": "yarn workspace @hono/swagger-ui build",
|
||||||
"build": "run-p build:*"
|
"build": "run-p build:*"
|
||||||
},
|
},
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|
|
@ -0,0 +1,143 @@
|
||||||
|
# Swagger UI Middleware and Component for Hono
|
||||||
|
|
||||||
|
This library, `@hono/swagger-ui`, provides a middleware and a component for integrating Swagger UI with Hono applications. Swagger UI is an interactive documentation interface for APIs compliant with the OpenAPI Specification, making it easier to understand and test API endpoints.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install @hono/swagger-ui
|
||||||
|
# or
|
||||||
|
yarn add @hono/swagger-ui
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Middleware Usage
|
||||||
|
|
||||||
|
You can use the `swaggerUI` middleware to serve Swagger UI on a specific route in your Hono application. Here's how you can do it:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { Hono } from 'hono'
|
||||||
|
import { swaggerUI } from '@hono/swagger-ui'
|
||||||
|
|
||||||
|
const app = new Hono()
|
||||||
|
|
||||||
|
// Use the middleware to serve Swagger UI at /ui
|
||||||
|
app.get('/ui', swaggerUI({ url: '/doc' }))
|
||||||
|
|
||||||
|
export default app
|
||||||
|
```
|
||||||
|
|
||||||
|
### Component Usage
|
||||||
|
|
||||||
|
If you are using `hono/html`, you can use the `SwaggerUI` component to render Swagger UI within your custom HTML structure. Here's an example:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { Hono } from 'hono'
|
||||||
|
import { html } from 'hono/html'
|
||||||
|
import { SwaggerUI } from '@hono/swagger-ui'
|
||||||
|
|
||||||
|
const app = new Hono()
|
||||||
|
|
||||||
|
app.get('/ui', (c) => {
|
||||||
|
return c.html(html`
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<meta name="description" content="Custom Swagger" />
|
||||||
|
<title>Custom Swagger</title>
|
||||||
|
<script>
|
||||||
|
// custom script
|
||||||
|
</script>
|
||||||
|
<style>
|
||||||
|
/* custom style */
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
${SwaggerUI({ url: '/doc' })}
|
||||||
|
</html>
|
||||||
|
`)
|
||||||
|
export default app
|
||||||
|
```
|
||||||
|
|
||||||
|
In this example, the `SwaggerUI` component is used to render Swagger UI within a custom HTML structure, allowing for additional customization such as adding custom scripts and styles.
|
||||||
|
|
||||||
|
### With `OpenAPIHono` Usage
|
||||||
|
|
||||||
|
Hono's middleware has OpenAPI integration `@hono/zod-openapi`, so you can use it to create an OpenAPI document and serve it easily with Swagger UI.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { OpenAPIHono, createRoute, z } from '@hono/zod-openapi'
|
||||||
|
import { swaggerUI } from '@hono/swagger-ui'
|
||||||
|
|
||||||
|
const app = new OpenAPIHono()
|
||||||
|
|
||||||
|
app.openapi(
|
||||||
|
createRoute({
|
||||||
|
method: 'get',
|
||||||
|
path: '/hello',
|
||||||
|
responses: {
|
||||||
|
200: {
|
||||||
|
description: 'Respond a message',
|
||||||
|
content: {
|
||||||
|
'application/json': {
|
||||||
|
schema: z.object({
|
||||||
|
message: z.string()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
(c) => {
|
||||||
|
return c.jsonT({
|
||||||
|
message: 'hello'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
app.get(
|
||||||
|
'/ui',
|
||||||
|
swaggerUI({
|
||||||
|
url: '/doc'
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
app.doc('/doc', {
|
||||||
|
info: {
|
||||||
|
title: 'An API',
|
||||||
|
version: 'v1'
|
||||||
|
},
|
||||||
|
openapi: '3.1.0'
|
||||||
|
})
|
||||||
|
|
||||||
|
export default app
|
||||||
|
```
|
||||||
|
|
||||||
|
## Options
|
||||||
|
|
||||||
|
Both the middleware and the component accept an options object for customization.
|
||||||
|
|
||||||
|
The following options are available:
|
||||||
|
|
||||||
|
- `version` (string, optional): The version of Swagger UI to use, defaults to `latest`.
|
||||||
|
- `manuallySwaggerUIHtml` (string, optional): If you want to use your own custom HTML, you can specify it here. If this option is specified, the all options except `version` will be ignored.
|
||||||
|
|
||||||
|
and most of options from [Swagger UI](
|
||||||
|
https://swagger.io/docs/open-source-tools/swagger-ui/usage/configuration/
|
||||||
|
) are supported as well.
|
||||||
|
|
||||||
|
such as:
|
||||||
|
- `url` (string, optional): The URL pointing to the OpenAPI definition (v2 or v3) that describes the API.
|
||||||
|
- `urls` (array, optional): An array of OpenAPI definitions (v2 or v3) that describe the APIs. Each definition must have a `name` and `url`.
|
||||||
|
- `presets` (array, optional): An array of presets to use for Swagger UI.
|
||||||
|
- `plugins` (array, optional): An array of plugins to use for Swagger UI.
|
||||||
|
|
||||||
|
## Authors
|
||||||
|
|
||||||
|
- naporitan <https://github.com/naporin0624>
|
||||||
|
- sor4chi <https://github.com/sor4chi>
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT
|
|
@ -0,0 +1,52 @@
|
||||||
|
{
|
||||||
|
"name": "@hono/swagger-ui",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"description": "A middleware for using SwaggerUI in Hono",
|
||||||
|
"type": "module",
|
||||||
|
"main": "dist/index.cjs",
|
||||||
|
"module": "dist/index.js",
|
||||||
|
"types": "dist/index.d.cts",
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"import": {
|
||||||
|
"types": "./dist/index.d.ts",
|
||||||
|
"default": "./dist/index.js"
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"types": "./dist/index.d.cts",
|
||||||
|
"default": "./dist/index.cjs"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"dist"
|
||||||
|
],
|
||||||
|
"scripts": {
|
||||||
|
"test": "vitest run",
|
||||||
|
"build": "tsup ./src/index.ts --format esm,cjs --dts",
|
||||||
|
"publint": "publint",
|
||||||
|
"release": "yarn build && yarn test && yarn publint && yarn publish"
|
||||||
|
},
|
||||||
|
"license": "MIT",
|
||||||
|
"private": false,
|
||||||
|
"publishConfig": {
|
||||||
|
"registry": "https://registry.npmjs.org",
|
||||||
|
"access": "public"
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/honojs/middleware.git"
|
||||||
|
},
|
||||||
|
"homepage": "https://github.com/honojs/middleware",
|
||||||
|
"peerDependencies": {
|
||||||
|
"hono": "*"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/swagger-ui-dist": "^3.30.3",
|
||||||
|
"hono": "^3.7.2",
|
||||||
|
"publint": "^0.2.2",
|
||||||
|
"tsup": "^7.2.0",
|
||||||
|
"vite": "^4.4.9",
|
||||||
|
"vitest": "^0.34.5"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,87 @@
|
||||||
|
import type { Env, MiddlewareHandler } from 'hono'
|
||||||
|
import { html } from 'hono/html'
|
||||||
|
import type { DistSwaggerUIOptions } from './swagger/renderer'
|
||||||
|
import { renderSwaggerUIOptions } from './swagger/renderer'
|
||||||
|
import type { AssetURLs } from './swagger/resource'
|
||||||
|
import { remoteAssets } from './swagger/resource'
|
||||||
|
|
||||||
|
type OriginalSwaggerUIOptions = {
|
||||||
|
version?: string
|
||||||
|
/**
|
||||||
|
* manuallySwaggerUIHtml is a string that is used to render SwaggerUI.
|
||||||
|
* If this is set, all other options will be ignored except version.
|
||||||
|
* The string will be inserted into the body of the HTML.
|
||||||
|
* This is useful when you want to fully customize the UI.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```ts
|
||||||
|
* const swaggerUI = SwaggerUI({
|
||||||
|
* manuallySwaggerUIHtml: (asset) => `
|
||||||
|
* <div>
|
||||||
|
* <div id="swagger-ui"></div>
|
||||||
|
* ${asset.css.map((url) => `<link rel="stylesheet" href="${url}" />`)}
|
||||||
|
* ${asset.js.map((url) => `<script src="${url}" crossorigin="anonymous"></script>`)}
|
||||||
|
* <script>
|
||||||
|
* window.onload = () => {
|
||||||
|
* window.ui = SwaggerUIBundle({
|
||||||
|
* dom_id: '#swagger-ui',
|
||||||
|
* url: 'https://petstore.swagger.io/v2/swagger.json',
|
||||||
|
* })
|
||||||
|
* }
|
||||||
|
* </script>
|
||||||
|
* </div>
|
||||||
|
* `,
|
||||||
|
* })
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
manuallySwaggerUIHtml?: (asset: AssetURLs) => string
|
||||||
|
}
|
||||||
|
|
||||||
|
type SwaggerUIOptions = OriginalSwaggerUIOptions & DistSwaggerUIOptions
|
||||||
|
|
||||||
|
const SwaggerUI = (options: SwaggerUIOptions) => {
|
||||||
|
const asset = remoteAssets({ version: options?.version })
|
||||||
|
delete options.version
|
||||||
|
|
||||||
|
if (options.manuallySwaggerUIHtml) {
|
||||||
|
return options.manuallySwaggerUIHtml(asset)
|
||||||
|
}
|
||||||
|
|
||||||
|
const optionsStrings = renderSwaggerUIOptions(options)
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div>
|
||||||
|
<div id="swagger-ui"></div>
|
||||||
|
${asset.css.map((url) => html`<link rel="stylesheet" href="${url}" />`)}
|
||||||
|
${asset.js.map((url) => html`<script src="${url}" crossorigin="anonymous"></script>`)}
|
||||||
|
<script>
|
||||||
|
window.onload = () => {
|
||||||
|
window.ui = SwaggerUIBundle({
|
||||||
|
dom_id: '#swagger-ui',${optionsStrings},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
const middleware =
|
||||||
|
<E extends Env>(options: SwaggerUIOptions): MiddlewareHandler<E> =>
|
||||||
|
async (c) => {
|
||||||
|
return c.html(/* html */ `
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<meta name="description" content="SwaggerUI" />
|
||||||
|
<title>SwaggerUI</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
${SwaggerUI(options)}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { middleware as swaggerUI, SwaggerUI }
|
||||||
|
export { SwaggerUIOptions }
|
|
@ -0,0 +1,67 @@
|
||||||
|
import type { SwaggerConfigs } from 'swagger-ui-dist'
|
||||||
|
|
||||||
|
export type DistSwaggerUIOptions = {
|
||||||
|
configUrl?: SwaggerConfigs['configUrl']
|
||||||
|
deepLinking?: SwaggerConfigs['deepLinking']
|
||||||
|
presets?: string[]
|
||||||
|
plugins?: string[]
|
||||||
|
spec?: SwaggerConfigs['spec']
|
||||||
|
url?: SwaggerConfigs['url']
|
||||||
|
urls?: SwaggerConfigs['urls']
|
||||||
|
layout?: SwaggerConfigs['layout']
|
||||||
|
docExpansion?: SwaggerConfigs['docExpansion']
|
||||||
|
maxDisplayedTags?: SwaggerConfigs['maxDisplayedTags']
|
||||||
|
operationsSorter?: string
|
||||||
|
requestInterceptor?: string
|
||||||
|
responseInterceptor?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const RENDER_TYPE = {
|
||||||
|
STRING_ARRAY: 'string_array',
|
||||||
|
STRING: 'string',
|
||||||
|
JSON_STRING: 'json_string',
|
||||||
|
RAW: 'raw',
|
||||||
|
} as const
|
||||||
|
|
||||||
|
const RENDER_TYPE_MAP = {
|
||||||
|
configUrl: RENDER_TYPE.STRING,
|
||||||
|
deepLinking: RENDER_TYPE.RAW,
|
||||||
|
presets: RENDER_TYPE.STRING_ARRAY,
|
||||||
|
plugins: RENDER_TYPE.STRING_ARRAY,
|
||||||
|
spec: RENDER_TYPE.JSON_STRING,
|
||||||
|
url: RENDER_TYPE.STRING,
|
||||||
|
urls: RENDER_TYPE.JSON_STRING,
|
||||||
|
layout: RENDER_TYPE.STRING,
|
||||||
|
docExpansion: RENDER_TYPE.STRING,
|
||||||
|
maxDisplayedTags: RENDER_TYPE.RAW,
|
||||||
|
operationsSorter: RENDER_TYPE.RAW,
|
||||||
|
requestInterceptor: RENDER_TYPE.RAW,
|
||||||
|
responseInterceptor: RENDER_TYPE.RAW,
|
||||||
|
} as const satisfies Record<
|
||||||
|
keyof DistSwaggerUIOptions,
|
||||||
|
(typeof RENDER_TYPE)[keyof typeof RENDER_TYPE]
|
||||||
|
>
|
||||||
|
|
||||||
|
export const renderSwaggerUIOptions = (options: DistSwaggerUIOptions) => {
|
||||||
|
const optionsStrings = Object.entries(options)
|
||||||
|
.map(([k, v]) => {
|
||||||
|
const key = k as keyof typeof RENDER_TYPE_MAP
|
||||||
|
if (RENDER_TYPE_MAP[key] === RENDER_TYPE.STRING) {
|
||||||
|
return `${key}: '${v}'`
|
||||||
|
}
|
||||||
|
if (RENDER_TYPE_MAP[key] === RENDER_TYPE.STRING_ARRAY) {
|
||||||
|
if (!Array.isArray(v)) return ''
|
||||||
|
return `${key}: [${v.map((ve) => `${ve}`).join(',')}]`
|
||||||
|
}
|
||||||
|
if (RENDER_TYPE_MAP[key] === RENDER_TYPE.JSON_STRING) {
|
||||||
|
return `${key}: ${JSON.stringify(v)}`
|
||||||
|
}
|
||||||
|
if (RENDER_TYPE_MAP[key] === RENDER_TYPE.RAW) {
|
||||||
|
return `${key}: ${v}`
|
||||||
|
}
|
||||||
|
return ''
|
||||||
|
})
|
||||||
|
.join(',')
|
||||||
|
|
||||||
|
return optionsStrings
|
||||||
|
}
|
|
@ -0,0 +1,17 @@
|
||||||
|
export interface AssetURLs {
|
||||||
|
css: string[]
|
||||||
|
js: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
type ResourceConfig = {
|
||||||
|
version?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const remoteAssets = ({ version }: ResourceConfig): AssetURLs => {
|
||||||
|
const url = `https://unpkg.com/swagger-ui-dist${version !== undefined ? `@${version}` : ''}`
|
||||||
|
|
||||||
|
return {
|
||||||
|
css: [`${url}/swagger-ui.css`],
|
||||||
|
js: [`${url}/swagger-ui-bundle.js`],
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,120 @@
|
||||||
|
import { Hono } from 'hono'
|
||||||
|
import { SwaggerUI, swaggerUI } from '../src'
|
||||||
|
|
||||||
|
describe('SwaggerUI Rendering', () => {
|
||||||
|
const url = 'https://petstore3.swagger.io/api/v3/openapi.json'
|
||||||
|
|
||||||
|
it('renders correctly with default UI version', () => {
|
||||||
|
expect(SwaggerUI({ url }).toString()).toEqual(`
|
||||||
|
<div>
|
||||||
|
<div id="swagger-ui"></div>
|
||||||
|
<link rel="stylesheet" href="https://unpkg.com/swagger-ui-dist/swagger-ui.css" />
|
||||||
|
<script src="https://unpkg.com/swagger-ui-dist/swagger-ui-bundle.js" crossorigin="anonymous"></script>
|
||||||
|
<script>
|
||||||
|
window.onload = () => {
|
||||||
|
window.ui = SwaggerUIBundle({
|
||||||
|
dom_id: '#swagger-ui',url: '${url}',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</div>
|
||||||
|
`)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders correctly with specified UI version', () => {
|
||||||
|
expect(SwaggerUI({ url, version: '5.0.0' }).toString()).toEqual(`
|
||||||
|
<div>
|
||||||
|
<div id="swagger-ui"></div>
|
||||||
|
<link rel="stylesheet" href="https://unpkg.com/swagger-ui-dist@5.0.0/swagger-ui.css" />
|
||||||
|
<script src="https://unpkg.com/swagger-ui-dist@5.0.0/swagger-ui-bundle.js" crossorigin="anonymous"></script>
|
||||||
|
<script>
|
||||||
|
window.onload = () => {
|
||||||
|
window.ui = SwaggerUIBundle({
|
||||||
|
dom_id: '#swagger-ui',url: '${url}',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</div>
|
||||||
|
`)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders correctly with custom UI', () => {
|
||||||
|
expect(
|
||||||
|
SwaggerUI({
|
||||||
|
url,
|
||||||
|
manuallySwaggerUIHtml: (asset) =>
|
||||||
|
`
|
||||||
|
<div>
|
||||||
|
<div id="swagger-ui-manually"></div>
|
||||||
|
${asset.css.map((url) => `<link rel="stylesheet" href="${url}" />`)}
|
||||||
|
${asset.js.map((url) => `<script src="${url}" crossorigin="anonymous"></script>`)}
|
||||||
|
<script>
|
||||||
|
window.onload = () => {
|
||||||
|
window.ui = SwaggerUIBundle({
|
||||||
|
dom_id: '#swagger-ui-manually',
|
||||||
|
url: '${url}',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</div>
|
||||||
|
`.trim(),
|
||||||
|
}).toString()
|
||||||
|
).toEqual(
|
||||||
|
`
|
||||||
|
<div>
|
||||||
|
<div id="swagger-ui-manually"></div>
|
||||||
|
<link rel="stylesheet" href="https://unpkg.com/swagger-ui-dist/swagger-ui.css" />
|
||||||
|
<script src="https://unpkg.com/swagger-ui-dist/swagger-ui-bundle.js" crossorigin="anonymous"></script>
|
||||||
|
<script>
|
||||||
|
window.onload = () => {
|
||||||
|
window.ui = SwaggerUIBundle({
|
||||||
|
dom_id: '#swagger-ui-manually',
|
||||||
|
url: '${url}',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</div>
|
||||||
|
`.trim()
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('SwaggerUI Middleware', () => {
|
||||||
|
let app: Hono
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
app = new Hono()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('responds with status 200', async () => {
|
||||||
|
app.get('/', swaggerUI({ url: 'https://petstore3.swagger.io/api/v3/openapi.json' }))
|
||||||
|
|
||||||
|
const res = await app.request('/')
|
||||||
|
expect(res.status).toBe(200)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('collectly renders SwaggerUI with custom options', async () => {
|
||||||
|
app.get(
|
||||||
|
'/',
|
||||||
|
swaggerUI({
|
||||||
|
url: 'https://petstore3.swagger.io/api/v3/openapi.json',
|
||||||
|
spec: {
|
||||||
|
info: {
|
||||||
|
title: 'Custom UI',
|
||||||
|
version: '1.0.0',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
presets: ['SwaggerUIStandalonePreset', 'SwaggerUIBundle.presets.apis'],
|
||||||
|
operationsSorter: '(a, b) => a.get("path").localeCompare(b.get("path"))',
|
||||||
|
})
|
||||||
|
)
|
||||||
|
const res = await app.request('/')
|
||||||
|
expect(res.status).toBe(200)
|
||||||
|
const html = await res.text()
|
||||||
|
expect(html).toContain('https://petstore3.swagger.io/api/v3/openapi.json') // RENDER_TYPE.STRING
|
||||||
|
expect(html).toContain('[SwaggerUIStandalonePreset,SwaggerUIBundle.presets.apis]') // RENDER_TYPE.STRING_ARRAY
|
||||||
|
expect(html).toContain('(a, b) => a.get("path").localeCompare(b.get("path"))') // RENDER_TYPE.RAW
|
||||||
|
expect(html).toContain('{"info":{"title":"Custom UI","version":"1.0.0"}}') // RENDER_TYPE.JSON_STRING
|
||||||
|
expect(html).toContain('window.ui = SwaggerUIBundle({') // entry point of SwaggerUI
|
||||||
|
})
|
||||||
|
})
|
|
@ -0,0 +1,127 @@
|
||||||
|
import { renderSwaggerUIOptions } from '../src/swagger/renderer'
|
||||||
|
|
||||||
|
describe('SwaggerUIOption Rendering', () => {
|
||||||
|
it('renders correctly with configUrl', () => {
|
||||||
|
expect(
|
||||||
|
renderSwaggerUIOptions({
|
||||||
|
configUrl: 'https://petstore3.swagger.io/api/v3/openapi.json',
|
||||||
|
})
|
||||||
|
).toEqual('configUrl: \'https://petstore3.swagger.io/api/v3/openapi.json\'')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders correctly with presets', () => {
|
||||||
|
expect(
|
||||||
|
renderSwaggerUIOptions({
|
||||||
|
presets: ['SwaggerUIBundle.presets.apis', 'SwaggerUIStandalonePreset'],
|
||||||
|
})
|
||||||
|
).toEqual('presets: [SwaggerUIBundle.presets.apis,SwaggerUIStandalonePreset]')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders correctly with plugins', () => {
|
||||||
|
expect(
|
||||||
|
renderSwaggerUIOptions({
|
||||||
|
plugins: ['SwaggerUIBundle.plugins.DownloadUrl'],
|
||||||
|
})
|
||||||
|
).toEqual('plugins: [SwaggerUIBundle.plugins.DownloadUrl]')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders correctly with deepLinking', () => {
|
||||||
|
expect(
|
||||||
|
renderSwaggerUIOptions({
|
||||||
|
deepLinking: true,
|
||||||
|
})
|
||||||
|
).toEqual('deepLinking: true')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders correctly with spec', () => {
|
||||||
|
expect(
|
||||||
|
renderSwaggerUIOptions({
|
||||||
|
spec: {
|
||||||
|
openapi: '3.0.0',
|
||||||
|
info: {
|
||||||
|
title: 'Swagger Petstore',
|
||||||
|
version: '1.0.0',
|
||||||
|
},
|
||||||
|
servers: [
|
||||||
|
{
|
||||||
|
url: 'https://petstore3.swagger.io/api/v3',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
).toEqual(
|
||||||
|
'spec: {"openapi":"3.0.0","info":{"title":"Swagger Petstore","version":"1.0.0"},"servers":[{"url":"https://petstore3.swagger.io/api/v3"}]}'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders correctly with url', () => {
|
||||||
|
expect(
|
||||||
|
renderSwaggerUIOptions({
|
||||||
|
url: 'https://petstore3.swagger.io/api/v3/openapi.json',
|
||||||
|
})
|
||||||
|
).toEqual('url: \'https://petstore3.swagger.io/api/v3/openapi.json\'')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders correctly with urls', () => {
|
||||||
|
expect(
|
||||||
|
renderSwaggerUIOptions({
|
||||||
|
urls: [
|
||||||
|
{
|
||||||
|
name: 'Petstore',
|
||||||
|
url: 'https://petstore3.swagger.io/api/v3/openapi.json',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
).toEqual(
|
||||||
|
'urls: [{"name":"Petstore","url":"https://petstore3.swagger.io/api/v3/openapi.json"}]'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders correctly with layout', () => {
|
||||||
|
expect(
|
||||||
|
renderSwaggerUIOptions({
|
||||||
|
layout: 'StandaloneLayout',
|
||||||
|
})
|
||||||
|
).toEqual('layout: \'StandaloneLayout\'')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders correctly with docExpansion', () => {
|
||||||
|
expect(
|
||||||
|
renderSwaggerUIOptions({
|
||||||
|
docExpansion: 'list',
|
||||||
|
})
|
||||||
|
).toEqual('docExpansion: \'list\'')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders correctly with maxDisplayedTags', () => {
|
||||||
|
expect(
|
||||||
|
renderSwaggerUIOptions({
|
||||||
|
maxDisplayedTags: 5,
|
||||||
|
})
|
||||||
|
).toEqual('maxDisplayedTags: 5')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders correctly with operationsSorter', () => {
|
||||||
|
expect(
|
||||||
|
renderSwaggerUIOptions({
|
||||||
|
operationsSorter: '(a, b) => a.path.localeCompare(b.path)',
|
||||||
|
})
|
||||||
|
).toEqual('operationsSorter: (a, b) => a.path.localeCompare(b.path)')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders correctly with requestInterceptor', () => {
|
||||||
|
expect(
|
||||||
|
renderSwaggerUIOptions({
|
||||||
|
requestInterceptor: '(req) => req',
|
||||||
|
})
|
||||||
|
).toEqual('requestInterceptor: (req) => req')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders correctly with responseInterceptor', () => {
|
||||||
|
expect(
|
||||||
|
renderSwaggerUIOptions({
|
||||||
|
responseInterceptor: '(res) => res',
|
||||||
|
})
|
||||||
|
).toEqual('responseInterceptor: (res) => res')
|
||||||
|
})
|
||||||
|
})
|
|
@ -0,0 +1,16 @@
|
||||||
|
import { remoteAssets } from '../src/swagger/resource'
|
||||||
|
|
||||||
|
describe('remoteAssets', () => {
|
||||||
|
it('should return default assets when no version is provided', () => {
|
||||||
|
const assets = remoteAssets({})
|
||||||
|
expect(assets.css).toEqual(['https://unpkg.com/swagger-ui-dist/swagger-ui.css'])
|
||||||
|
expect(assets.js).toEqual(['https://unpkg.com/swagger-ui-dist/swagger-ui-bundle.js'])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return assets with version when version is provided', () => {
|
||||||
|
const version = '1.2.3'
|
||||||
|
const assets = remoteAssets({ version })
|
||||||
|
expect(assets.css).toEqual([`https://unpkg.com/swagger-ui-dist@${version}/swagger-ui.css`])
|
||||||
|
expect(assets.js).toEqual([`https://unpkg.com/swagger-ui-dist@${version}/swagger-ui-bundle.js`])
|
||||||
|
})
|
||||||
|
})
|
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"extends": "../../tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"types": ["vitest/globals"]
|
||||||
|
},
|
||||||
|
"include": ["src/**/*.ts", "src/**/*.tsx"]
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
import { defineConfig } from 'vite'
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
test: {
|
||||||
|
globals: true,
|
||||||
|
},
|
||||||
|
})
|
187
yarn.lock
187
yarn.lock
|
@ -561,11 +561,12 @@
|
||||||
prettier "^2.7.1"
|
prettier "^2.7.1"
|
||||||
|
|
||||||
"@clerk/backend@^0.30.1":
|
"@clerk/backend@^0.30.1":
|
||||||
version "0.30.1"
|
version "0.30.3"
|
||||||
resolved "https://registry.yarnpkg.com/@clerk/backend/-/backend-0.30.1.tgz#23179c68f0ce8a94d99bc2e8c5000cf896c8e577"
|
resolved "https://registry.yarnpkg.com/@clerk/backend/-/backend-0.30.3.tgz#c0909abe0ed434540899625f5f12b73847e9873a"
|
||||||
integrity sha512-+769bWE89Ejdy4ddu4wF/yBZypGVA5G/vAerJsXv/sdos3rSFtSghLx5OMdatn/2XqZUnrZoQGLtUz5ZxzDJXg==
|
integrity sha512-fEQqfIUevBeHvPXoPP/hoocdAd0tond/cDgG65U7sO3zirmVBpNVI91074pctXBoQGa7QCzs52ZvIBssfaag9w==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@clerk/types" "^3.53.0"
|
"@clerk/shared" "0.24.3"
|
||||||
|
"@clerk/types" "3.54.0"
|
||||||
"@peculiar/webcrypto" "1.4.1"
|
"@peculiar/webcrypto" "1.4.1"
|
||||||
"@types/node" "16.18.6"
|
"@types/node" "16.18.6"
|
||||||
cookie "0.5.0"
|
cookie "0.5.0"
|
||||||
|
@ -574,10 +575,19 @@
|
||||||
snakecase-keys "5.4.4"
|
snakecase-keys "5.4.4"
|
||||||
tslib "2.4.1"
|
tslib "2.4.1"
|
||||||
|
|
||||||
"@clerk/types@^3.53.0":
|
"@clerk/shared@0.24.3":
|
||||||
version "3.53.0"
|
version "0.24.3"
|
||||||
resolved "https://registry.yarnpkg.com/@clerk/types/-/types-3.53.0.tgz#5a7436e0ff89134401edafef7d50557e626a928f"
|
resolved "https://registry.yarnpkg.com/@clerk/shared/-/shared-0.24.3.tgz#44b0b91d110cae78377338abbde2031b4245ebb3"
|
||||||
integrity sha512-V+9BvKAo9f2ShgP+RpQ/MgJTbx4ayk3sYiFbMtLYlmtLCsn0xwglfP7i++7640d6+MAtCnMTAcrTp61IvUxSFw==
|
integrity sha512-fZ0inqzP7hSDW8BUtIutqAt3YrOw/7ZDXxc3vRDMqY0shR8V6KXM7xdGS1dPV2qGxCBhBVbXRA/oFdKhO7PhJg==
|
||||||
|
dependencies:
|
||||||
|
glob-to-regexp "0.4.1"
|
||||||
|
js-cookie "3.0.1"
|
||||||
|
swr "2.2.0"
|
||||||
|
|
||||||
|
"@clerk/types@3.54.0":
|
||||||
|
version "3.54.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@clerk/types/-/types-3.54.0.tgz#9fee657a9e79e7e53dd4b93fcbdf6a69dabaf610"
|
||||||
|
integrity sha512-ZFS8Vz2elyxzupuTd7VO++TrzMmbhsgszriDSQKGCf69KeegZqGJ8QhzUWMcB7Ro/991bUKbZ56QF3x49j8Lbg==
|
||||||
dependencies:
|
dependencies:
|
||||||
csstype "3.1.1"
|
csstype "3.1.1"
|
||||||
|
|
||||||
|
@ -1643,13 +1653,13 @@
|
||||||
integrity sha512-wU5J8rUoo32oSef/rFpOT1HIjLjAv3qIDHkw1QIhODV3OpAVHi5oVzlouozg9obUmZKtbZ0qUe/m7FP0y0yBzA==
|
integrity sha512-wU5J8rUoo32oSef/rFpOT1HIjLjAv3qIDHkw1QIhODV3OpAVHi5oVzlouozg9obUmZKtbZ0qUe/m7FP0y0yBzA==
|
||||||
|
|
||||||
"@peculiar/asn1-schema@^2.3.0", "@peculiar/asn1-schema@^2.3.6":
|
"@peculiar/asn1-schema@^2.3.0", "@peculiar/asn1-schema@^2.3.6":
|
||||||
version "2.3.6"
|
version "2.3.8"
|
||||||
resolved "https://registry.yarnpkg.com/@peculiar/asn1-schema/-/asn1-schema-2.3.6.tgz#3dd3c2ade7f702a9a94dfb395c192f5fa5d6b922"
|
resolved "https://registry.yarnpkg.com/@peculiar/asn1-schema/-/asn1-schema-2.3.8.tgz#04b38832a814e25731232dd5be883460a156da3b"
|
||||||
integrity sha512-izNRxPoaeJeg/AyH8hER6s+H7p4itk+03QCa4sbxI3lNdseQYCuxzgsuNK8bTXChtLTjpJz6NmXKA73qLa3rCA==
|
integrity sha512-ULB1XqHKx1WBU/tTFIA+uARuRoBVZ4pNdOA878RDrRbBfBGcSzi5HBkdScC6ZbHn8z7L8gmKCgPC1LHRrP46tA==
|
||||||
dependencies:
|
dependencies:
|
||||||
asn1js "^3.0.5"
|
asn1js "^3.0.5"
|
||||||
pvtsutils "^1.3.2"
|
pvtsutils "^1.3.5"
|
||||||
tslib "^2.4.0"
|
tslib "^2.6.2"
|
||||||
|
|
||||||
"@peculiar/json-schema@^1.1.12":
|
"@peculiar/json-schema@^1.1.12":
|
||||||
version "1.1.12"
|
version "1.1.12"
|
||||||
|
@ -1819,9 +1829,9 @@
|
||||||
integrity sha512-XJfwUVUKDHF5ugKwIcxEgc9k8b7HbznCp6eUfWgu710hMPNIO4aw4/zB5RogDQz8nd6gyCDpU9O/m6qYEWY6yQ==
|
integrity sha512-XJfwUVUKDHF5ugKwIcxEgc9k8b7HbznCp6eUfWgu710hMPNIO4aw4/zB5RogDQz8nd6gyCDpU9O/m6qYEWY6yQ==
|
||||||
|
|
||||||
"@sinclair/typebox@^0.31.15":
|
"@sinclair/typebox@^0.31.15":
|
||||||
version "0.31.17"
|
version "0.31.22"
|
||||||
resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.31.17.tgz#f9ceed480957b919b203bb0b3e27bc559d1e8e19"
|
resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.31.22.tgz#f13fa4050a7e883d252365902e38186fa0dc8ab8"
|
||||||
integrity sha512-GVYVHHOGINx+DT2DwjXoCQO0mRpztYKyb3d48tucuqhjhHpQYGp7Xx7dhhQGzILx/beuBrzfITMC7/5X7fw+UA==
|
integrity sha512-CKviMgpcXd8q8IsQQD8cCleswe4/EkQRcOqtVQcP1e+XUyszjJYjgL5Dtf3XunWZc2zEGmQPqJEsq08NiW9xfw==
|
||||||
|
|
||||||
"@sindresorhus/is@^0.14.0":
|
"@sindresorhus/is@^0.14.0":
|
||||||
version "0.14.0"
|
version "0.14.0"
|
||||||
|
@ -2196,6 +2206,11 @@
|
||||||
resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.1.tgz#20f18294f797f2209b5f65c8e3b5c8e8261d127c"
|
resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.1.tgz#20f18294f797f2209b5f65c8e3b5c8e8261d127c"
|
||||||
integrity sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==
|
integrity sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==
|
||||||
|
|
||||||
|
"@types/swagger-ui-dist@^3.30.3":
|
||||||
|
version "3.30.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/swagger-ui-dist/-/swagger-ui-dist-3.30.3.tgz#ae72f08754f5674cc8fe5683785363fab5697d2b"
|
||||||
|
integrity sha512-JKu2q4xKSYq5NJNjc+Wb7ohobueuJD8RvH5NrGFwVHn09p6ASBi/to8gfPNBRJAcuYQL7jXOqjxm287VR9HM6A==
|
||||||
|
|
||||||
"@types/triple-beam@^1.3.2":
|
"@types/triple-beam@^1.3.2":
|
||||||
version "1.3.2"
|
version "1.3.2"
|
||||||
resolved "https://registry.yarnpkg.com/@types/triple-beam/-/triple-beam-1.3.2.tgz#38ecb64f01aa0d02b7c8f4222d7c38af6316fef8"
|
resolved "https://registry.yarnpkg.com/@types/triple-beam/-/triple-beam-1.3.2.tgz#38ecb64f01aa0d02b7c8f4222d7c38af6316fef8"
|
||||||
|
@ -2316,6 +2331,15 @@
|
||||||
"@vitest/utils" "0.34.2"
|
"@vitest/utils" "0.34.2"
|
||||||
chai "^4.3.7"
|
chai "^4.3.7"
|
||||||
|
|
||||||
|
"@vitest/expect@0.34.5":
|
||||||
|
version "0.34.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/@vitest/expect/-/expect-0.34.5.tgz#1f58829e746311162220d6580f72d6329efb9081"
|
||||||
|
integrity sha512-/3RBIV9XEH+nRpRMqDJBufKIOQaYUH2X6bt0rKSCW0MfKhXFLYsR5ivHifeajRSTsln0FwJbitxLKHSQz/Xwkw==
|
||||||
|
dependencies:
|
||||||
|
"@vitest/spy" "0.34.5"
|
||||||
|
"@vitest/utils" "0.34.5"
|
||||||
|
chai "^4.3.7"
|
||||||
|
|
||||||
"@vitest/runner@0.34.2":
|
"@vitest/runner@0.34.2":
|
||||||
version "0.34.2"
|
version "0.34.2"
|
||||||
resolved "https://registry.yarnpkg.com/@vitest/runner/-/runner-0.34.2.tgz#3408682cd68475e733a3f151d27792be75d2f07d"
|
resolved "https://registry.yarnpkg.com/@vitest/runner/-/runner-0.34.2.tgz#3408682cd68475e733a3f151d27792be75d2f07d"
|
||||||
|
@ -2325,6 +2349,15 @@
|
||||||
p-limit "^4.0.0"
|
p-limit "^4.0.0"
|
||||||
pathe "^1.1.1"
|
pathe "^1.1.1"
|
||||||
|
|
||||||
|
"@vitest/runner@0.34.5":
|
||||||
|
version "0.34.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/@vitest/runner/-/runner-0.34.5.tgz#2bc69a21cd1a09c9403a2a9b0cbd7c42df79f1ae"
|
||||||
|
integrity sha512-RDEE3ViVvl7jFSCbnBRyYuu23XxmvRTSZWW6W4M7eC5dOsK75d5LIf6uhE5Fqf809DQ1+9ICZZNxhIolWHU4og==
|
||||||
|
dependencies:
|
||||||
|
"@vitest/utils" "0.34.5"
|
||||||
|
p-limit "^4.0.0"
|
||||||
|
pathe "^1.1.1"
|
||||||
|
|
||||||
"@vitest/snapshot@0.34.2":
|
"@vitest/snapshot@0.34.2":
|
||||||
version "0.34.2"
|
version "0.34.2"
|
||||||
resolved "https://registry.yarnpkg.com/@vitest/snapshot/-/snapshot-0.34.2.tgz#fce1b89aa1e836e3fd0229c03ef4ef2f69cb1409"
|
resolved "https://registry.yarnpkg.com/@vitest/snapshot/-/snapshot-0.34.2.tgz#fce1b89aa1e836e3fd0229c03ef4ef2f69cb1409"
|
||||||
|
@ -2334,6 +2367,15 @@
|
||||||
pathe "^1.1.1"
|
pathe "^1.1.1"
|
||||||
pretty-format "^29.5.0"
|
pretty-format "^29.5.0"
|
||||||
|
|
||||||
|
"@vitest/snapshot@0.34.5":
|
||||||
|
version "0.34.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/@vitest/snapshot/-/snapshot-0.34.5.tgz#1d81fce3cdc9cf6ad57e86eb5e5eecefc71d1e02"
|
||||||
|
integrity sha512-+ikwSbhu6z2yOdtKmk/aeoDZ9QPm2g/ZO5rXT58RR9Vmu/kB2MamyDSx77dctqdZfP3Diqv4mbc/yw2kPT8rmA==
|
||||||
|
dependencies:
|
||||||
|
magic-string "^0.30.1"
|
||||||
|
pathe "^1.1.1"
|
||||||
|
pretty-format "^29.5.0"
|
||||||
|
|
||||||
"@vitest/spy@0.34.2":
|
"@vitest/spy@0.34.2":
|
||||||
version "0.34.2"
|
version "0.34.2"
|
||||||
resolved "https://registry.yarnpkg.com/@vitest/spy/-/spy-0.34.2.tgz#5c483d71e4c2d42f90bef29cdf6e5f5559c52827"
|
resolved "https://registry.yarnpkg.com/@vitest/spy/-/spy-0.34.2.tgz#5c483d71e4c2d42f90bef29cdf6e5f5559c52827"
|
||||||
|
@ -2341,6 +2383,13 @@
|
||||||
dependencies:
|
dependencies:
|
||||||
tinyspy "^2.1.1"
|
tinyspy "^2.1.1"
|
||||||
|
|
||||||
|
"@vitest/spy@0.34.5":
|
||||||
|
version "0.34.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/@vitest/spy/-/spy-0.34.5.tgz#2d32993b18eeb50f682e5dde089e390cbb387cb8"
|
||||||
|
integrity sha512-epsicsfhvBjRjCMOC/3k00mP/TBGQy8/P0DxOFiWyLt55gnZ99dqCfCiAsKO17BWVjn4eZRIjKvcqNmSz8gvmg==
|
||||||
|
dependencies:
|
||||||
|
tinyspy "^2.1.1"
|
||||||
|
|
||||||
"@vitest/utils@0.34.2":
|
"@vitest/utils@0.34.2":
|
||||||
version "0.34.2"
|
version "0.34.2"
|
||||||
resolved "https://registry.yarnpkg.com/@vitest/utils/-/utils-0.34.2.tgz#5d291a1b0f5d01be99fd1801d212b837a610c53b"
|
resolved "https://registry.yarnpkg.com/@vitest/utils/-/utils-0.34.2.tgz#5d291a1b0f5d01be99fd1801d212b837a610c53b"
|
||||||
|
@ -2350,6 +2399,15 @@
|
||||||
loupe "^2.3.6"
|
loupe "^2.3.6"
|
||||||
pretty-format "^29.5.0"
|
pretty-format "^29.5.0"
|
||||||
|
|
||||||
|
"@vitest/utils@0.34.5":
|
||||||
|
version "0.34.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/@vitest/utils/-/utils-0.34.5.tgz#2178fdbc36524d25b8d846b3d408962e1771e83a"
|
||||||
|
integrity sha512-ur6CmmYQoeHMwmGb0v+qwkwN3yopZuZyf4xt1DBBSGBed8Hf9Gmbm/5dEWqgpLPdRx6Av6jcWXrjcKfkTzg/pw==
|
||||||
|
dependencies:
|
||||||
|
diff-sequences "^29.4.3"
|
||||||
|
loupe "^2.3.6"
|
||||||
|
pretty-format "^29.5.0"
|
||||||
|
|
||||||
abbrev@^1.0.0:
|
abbrev@^1.0.0:
|
||||||
version "1.1.1"
|
version "1.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8"
|
resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8"
|
||||||
|
@ -5547,6 +5605,11 @@ glob-slasher@^1.0.1:
|
||||||
lodash.isobject "^2.4.1"
|
lodash.isobject "^2.4.1"
|
||||||
toxic "^1.0.0"
|
toxic "^1.0.0"
|
||||||
|
|
||||||
|
glob-to-regexp@0.4.1:
|
||||||
|
version "0.4.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e"
|
||||||
|
integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==
|
||||||
|
|
||||||
glob@7.1.6:
|
glob@7.1.6:
|
||||||
version "7.1.6"
|
version "7.1.6"
|
||||||
resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6"
|
resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6"
|
||||||
|
@ -5944,12 +6007,12 @@ hono@^3.5.8:
|
||||||
resolved "https://registry.yarnpkg.com/hono/-/hono-3.5.8.tgz#9bbc412f5a54183cf2a81a36a9b9ea56da10f785"
|
resolved "https://registry.yarnpkg.com/hono/-/hono-3.5.8.tgz#9bbc412f5a54183cf2a81a36a9b9ea56da10f785"
|
||||||
integrity sha512-ZipTmGfHm43q5QOEBGog2wyejyNUcicjPt0BLDQ8yz9xij/y9RYXRpR1YPxMpQqeyNM7isvpsIAe9Ems51Wq0Q==
|
integrity sha512-ZipTmGfHm43q5QOEBGog2wyejyNUcicjPt0BLDQ8yz9xij/y9RYXRpR1YPxMpQqeyNM7isvpsIAe9Ems51Wq0Q==
|
||||||
|
|
||||||
hono@^3.7.3:
|
hono@^3.7.2:
|
||||||
version "3.7.3"
|
version "3.7.2"
|
||||||
resolved "https://registry.yarnpkg.com/hono/-/hono-3.7.3.tgz#01fd88360e9a431235110197fbb5a998131e104e"
|
resolved "https://registry.yarnpkg.com/hono/-/hono-3.7.2.tgz#c3839d7ffbb5120850b2b926363d065020f4d18c"
|
||||||
integrity sha512-BQHdLPXb30hQ9k+04byeSi4QMHk20U1GUq0nT5kGUCGZtxeYhAS7mUJ1wgjn4SCvgiw1rcc6oBOAlwJQ7jQymA==
|
integrity sha512-5SWYrAQJlfjHggcDTnmKZd5zlUEXmoUiBjnmL6C1W8MX39/bUw6ZIvfEJZgpo7d7Z/vCJ5FRfkjIQPRH5yV/dQ==
|
||||||
|
|
||||||
hono@^3.9.1:
|
hono@^3.7.3, hono@^3.9.1:
|
||||||
version "3.9.1"
|
version "3.9.1"
|
||||||
resolved "https://registry.yarnpkg.com/hono/-/hono-3.9.1.tgz#7a630aad35b8709e10e6117468116d38404ab87e"
|
resolved "https://registry.yarnpkg.com/hono/-/hono-3.9.1.tgz#7a630aad35b8709e10e6117468116d38404ab87e"
|
||||||
integrity sha512-z3nM9CjNZ8PRAH6NNntk4ESKW2POEbGanhK1QpYdQ1MOYRzZPSEE8B5mqw8bYEPa7qIQxX0vtlv7XOxtwFbosg==
|
integrity sha512-z3nM9CjNZ8PRAH6NNntk4ESKW2POEbGanhK1QpYdQ1MOYRzZPSEE8B5mqw8bYEPa7qIQxX0vtlv7XOxtwFbosg==
|
||||||
|
@ -7517,6 +7580,11 @@ joycon@^3.0.1:
|
||||||
resolved "https://registry.yarnpkg.com/joycon/-/joycon-3.1.1.tgz#bce8596d6ae808f8b68168f5fc69280996894f03"
|
resolved "https://registry.yarnpkg.com/joycon/-/joycon-3.1.1.tgz#bce8596d6ae808f8b68168f5fc69280996894f03"
|
||||||
integrity sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==
|
integrity sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==
|
||||||
|
|
||||||
|
js-cookie@3.0.1:
|
||||||
|
version "3.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/js-cookie/-/js-cookie-3.0.1.tgz#9e39b4c6c2f56563708d7d31f6f5f21873a92414"
|
||||||
|
integrity sha512-+0rgsUXZu4ncpPxRL+lNEptWMOWl9etvPHc/koSRp6MPwpRYAhmk0dUG00J4bxVV3r9uUzfo24wW0knS07SKSw==
|
||||||
|
|
||||||
js-tokens@^4.0.0:
|
js-tokens@^4.0.0:
|
||||||
version "4.0.0"
|
version "4.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
|
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
|
||||||
|
@ -9065,9 +9133,9 @@ node-fetch-native@1.0.1:
|
||||||
integrity sha512-VzW+TAk2wE4X9maiKMlT+GsPU4OMmR1U9CrHSmd3DFLn2IcZ9VJ6M6BBugGfYUnPCLSYxXdZy17M0BEJyhUTwg==
|
integrity sha512-VzW+TAk2wE4X9maiKMlT+GsPU4OMmR1U9CrHSmd3DFLn2IcZ9VJ6M6BBugGfYUnPCLSYxXdZy17M0BEJyhUTwg==
|
||||||
|
|
||||||
node-fetch-native@^1.4.0:
|
node-fetch-native@^1.4.0:
|
||||||
version "1.4.0"
|
version "1.4.1"
|
||||||
resolved "https://registry.yarnpkg.com/node-fetch-native/-/node-fetch-native-1.4.0.tgz#fbe8ac033cb6aa44bd106b5e4fd2b6277ba70fa1"
|
resolved "https://registry.yarnpkg.com/node-fetch-native/-/node-fetch-native-1.4.1.tgz#5a336e55b4e1b1e72b9927da09fecd2b374c9be5"
|
||||||
integrity sha512-F5kfEj95kX8tkDhUCYdV8dg3/8Olx/94zB8+ZNthFs6Bz31UpUi8Xh40TN3thLwXgrwXry1pEg9lJ++tLWTcqA==
|
integrity sha512-NsXBU0UgBxo2rQLOeWNZqS3fvflWePMECr8CoSWoSTqCqGbVVsvl9vZu1HfQicYN0g5piV9Gh8RTEvo/uP752w==
|
||||||
|
|
||||||
node-fetch@^2.5.0, node-fetch@^2.6.1, node-fetch@^2.6.7, node-fetch@^2.6.9:
|
node-fetch@^2.5.0, node-fetch@^2.6.1, node-fetch@^2.6.7, node-fetch@^2.6.9:
|
||||||
version "2.6.11"
|
version "2.6.11"
|
||||||
|
@ -10081,6 +10149,15 @@ publint@^0.2.0:
|
||||||
picocolors "^1.0.0"
|
picocolors "^1.0.0"
|
||||||
sade "^1.8.1"
|
sade "^1.8.1"
|
||||||
|
|
||||||
|
publint@^0.2.2:
|
||||||
|
version "0.2.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/publint/-/publint-0.2.2.tgz#b6ab2073dab6a68a6bce312a7536ccd1d1c4f04d"
|
||||||
|
integrity sha512-2t2IO6Y8Z+QBNLG89bpRhTQH7Ifn/83Kr0dVVdmOybq7GAT6+M4YGZd5AhtfMJbYPmbT7YD469pDKLCK94Q2+Q==
|
||||||
|
dependencies:
|
||||||
|
npm-packlist "^5.1.3"
|
||||||
|
picocolors "^1.0.0"
|
||||||
|
sade "^1.8.1"
|
||||||
|
|
||||||
pump@^3.0.0:
|
pump@^3.0.0:
|
||||||
version "3.0.0"
|
version "3.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64"
|
resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64"
|
||||||
|
@ -10111,7 +10188,7 @@ pure-rand@^6.0.0:
|
||||||
resolved "https://registry.yarnpkg.com/pure-rand/-/pure-rand-6.0.2.tgz#a9c2ddcae9b68d736a8163036f088a2781c8b306"
|
resolved "https://registry.yarnpkg.com/pure-rand/-/pure-rand-6.0.2.tgz#a9c2ddcae9b68d736a8163036f088a2781c8b306"
|
||||||
integrity sha512-6Yg0ekpKICSjPswYOuC5sku/TSWaRYlA0qsXqJgM/d/4pLPHPuTxK7Nbf7jFKzAeedUhR8C7K9Uv63FBsSo8xQ==
|
integrity sha512-6Yg0ekpKICSjPswYOuC5sku/TSWaRYlA0qsXqJgM/d/4pLPHPuTxK7Nbf7jFKzAeedUhR8C7K9Uv63FBsSo8xQ==
|
||||||
|
|
||||||
pvtsutils@^1.3.2:
|
pvtsutils@^1.3.2, pvtsutils@^1.3.5:
|
||||||
version "1.3.5"
|
version "1.3.5"
|
||||||
resolved "https://registry.yarnpkg.com/pvtsutils/-/pvtsutils-1.3.5.tgz#b8705b437b7b134cd7fd858f025a23456f1ce910"
|
resolved "https://registry.yarnpkg.com/pvtsutils/-/pvtsutils-1.3.5.tgz#b8705b437b7b134cd7fd858f025a23456f1ce910"
|
||||||
integrity sha512-ARvb14YB9Nm2Xi6nBq1ZX6dAM0FsJnuk+31aUp4TrcZEdKUlSqOqsxJHUPJDNE3qiIp+iUPEIeR6Je/tgV7zsA==
|
integrity sha512-ARvb14YB9Nm2Xi6nBq1ZX6dAM0FsJnuk+31aUp4TrcZEdKUlSqOqsxJHUPJDNE3qiIp+iUPEIeR6Je/tgV7zsA==
|
||||||
|
@ -11347,6 +11424,13 @@ svgo@^3.0.2:
|
||||||
csso "^5.0.5"
|
csso "^5.0.5"
|
||||||
picocolors "^1.0.0"
|
picocolors "^1.0.0"
|
||||||
|
|
||||||
|
swr@2.2.0:
|
||||||
|
version "2.2.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/swr/-/swr-2.2.0.tgz#575c6ac1bec087847f4c86a39ccbc0043c834d6a"
|
||||||
|
integrity sha512-AjqHOv2lAhkuUdIiBu9xbuettzAzWXmCEcLONNKJRba87WAefz8Ca9d6ds/SzrPc235n1IxWYdhJ2zF3MNUaoQ==
|
||||||
|
dependencies:
|
||||||
|
use-sync-external-store "^1.2.0"
|
||||||
|
|
||||||
symbol-observable@^1.1.0:
|
symbol-observable@^1.1.0:
|
||||||
version "1.2.0"
|
version "1.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.2.0.tgz#c22688aed4eab3cdc2dfeacbb561660560a00804"
|
resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.2.0.tgz#c22688aed4eab3cdc2dfeacbb561660560a00804"
|
||||||
|
@ -11666,9 +11750,9 @@ tslib@^1.8.1, tslib@^1.9.0:
|
||||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
|
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
|
||||||
integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==
|
integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==
|
||||||
|
|
||||||
tslib@^2.0.0, tslib@^2.0.3, tslib@^2.4.0, tslib@^2.4.1, "tslib@^2.4.1 || ^1.9.3", tslib@^2.6.1:
|
tslib@^2.0.0, tslib@^2.0.3, tslib@^2.4.0, tslib@^2.4.1, "tslib@^2.4.1 || ^1.9.3", tslib@^2.6.1, tslib@^2.6.2:
|
||||||
version "2.6.2"
|
version "2.6.2"
|
||||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae"
|
resolved "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz"
|
||||||
integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==
|
integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==
|
||||||
|
|
||||||
tslib@^2.0.1, tslib@^2.1.0, tslib@^2.5.0:
|
tslib@^2.0.1, tslib@^2.1.0, tslib@^2.5.0:
|
||||||
|
@ -12089,6 +12173,11 @@ urlpattern-polyfill@^4.0.3:
|
||||||
resolved "https://registry.yarnpkg.com/urlpattern-polyfill/-/urlpattern-polyfill-4.0.3.tgz#c1fa7a73eb4e6c6a1ffb41b24cf31974f7392d3b"
|
resolved "https://registry.yarnpkg.com/urlpattern-polyfill/-/urlpattern-polyfill-4.0.3.tgz#c1fa7a73eb4e6c6a1ffb41b24cf31974f7392d3b"
|
||||||
integrity sha512-DOE84vZT2fEcl9gqCUTcnAw5ZY5Id55ikUcziSUntuEFL3pRvavg5kwDmTEUJkeCHInTlV/HexFomgYnzO5kdQ==
|
integrity sha512-DOE84vZT2fEcl9gqCUTcnAw5ZY5Id55ikUcziSUntuEFL3pRvavg5kwDmTEUJkeCHInTlV/HexFomgYnzO5kdQ==
|
||||||
|
|
||||||
|
use-sync-external-store@^1.2.0:
|
||||||
|
version "1.2.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz#7dbefd6ef3fe4e767a0cf5d7287aacfb5846928a"
|
||||||
|
integrity sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==
|
||||||
|
|
||||||
util-deprecate@^1.0.1, util-deprecate@~1.0.1:
|
util-deprecate@^1.0.1, util-deprecate@~1.0.1:
|
||||||
version "1.0.2"
|
version "1.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
|
resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
|
||||||
|
@ -12234,7 +12323,19 @@ vite-node@0.34.2:
|
||||||
picocolors "^1.0.0"
|
picocolors "^1.0.0"
|
||||||
vite "^3.0.0 || ^4.0.0"
|
vite "^3.0.0 || ^4.0.0"
|
||||||
|
|
||||||
"vite@^3.0.0 || ^4.0.0":
|
vite-node@0.34.5:
|
||||||
|
version "0.34.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/vite-node/-/vite-node-0.34.5.tgz#21d6bd637cb0c14d0edc1d7bdf832a70dc11c427"
|
||||||
|
integrity sha512-RNZ+DwbCvDoI5CbCSQSyRyzDTfFvFauvMs6Yq4ObJROKlIKuat1KgSX/Ako5rlDMfVCyMcpMRMTkJBxd6z8YRA==
|
||||||
|
dependencies:
|
||||||
|
cac "^6.7.14"
|
||||||
|
debug "^4.3.4"
|
||||||
|
mlly "^1.4.0"
|
||||||
|
pathe "^1.1.1"
|
||||||
|
picocolors "^1.0.0"
|
||||||
|
vite "^3.0.0 || ^4.0.0 || ^5.0.0-0"
|
||||||
|
|
||||||
|
"vite@^3.0.0 || ^4.0.0", "vite@^3.0.0 || ^4.0.0 || ^5.0.0-0", "vite@^3.1.0 || ^4.0.0 || ^5.0.0-0", vite@^4.4.9:
|
||||||
version "4.4.9"
|
version "4.4.9"
|
||||||
resolved "https://registry.yarnpkg.com/vite/-/vite-4.4.9.tgz#1402423f1a2f8d66fd8d15e351127c7236d29d3d"
|
resolved "https://registry.yarnpkg.com/vite/-/vite-4.4.9.tgz#1402423f1a2f8d66fd8d15e351127c7236d29d3d"
|
||||||
integrity sha512-2mbUn2LlUmNASWwSCNSJ/EG2HuSRTnVNaydp6vMCm5VIqJsjMfbIWtbH2kDuwUVW5mMUKKZvGPX/rqeqVvv1XA==
|
integrity sha512-2mbUn2LlUmNASWwSCNSJ/EG2HuSRTnVNaydp6vMCm5VIqJsjMfbIWtbH2kDuwUVW5mMUKKZvGPX/rqeqVvv1XA==
|
||||||
|
@ -12275,6 +12376,36 @@ vitest@^0.34.2:
|
||||||
vite-node "0.34.2"
|
vite-node "0.34.2"
|
||||||
why-is-node-running "^2.2.2"
|
why-is-node-running "^2.2.2"
|
||||||
|
|
||||||
|
vitest@^0.34.5:
|
||||||
|
version "0.34.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/vitest/-/vitest-0.34.5.tgz#c2200566d4b133588d69124bc0fbe8bf179f644f"
|
||||||
|
integrity sha512-CPI68mmnr2DThSB3frSuE5RLm9wo5wU4fbDrDwWQQB1CWgq9jQVoQwnQSzYAjdoBOPoH2UtXpOgHVge/uScfZg==
|
||||||
|
dependencies:
|
||||||
|
"@types/chai" "^4.3.5"
|
||||||
|
"@types/chai-subset" "^1.3.3"
|
||||||
|
"@types/node" "*"
|
||||||
|
"@vitest/expect" "0.34.5"
|
||||||
|
"@vitest/runner" "0.34.5"
|
||||||
|
"@vitest/snapshot" "0.34.5"
|
||||||
|
"@vitest/spy" "0.34.5"
|
||||||
|
"@vitest/utils" "0.34.5"
|
||||||
|
acorn "^8.9.0"
|
||||||
|
acorn-walk "^8.2.0"
|
||||||
|
cac "^6.7.14"
|
||||||
|
chai "^4.3.7"
|
||||||
|
debug "^4.3.4"
|
||||||
|
local-pkg "^0.4.3"
|
||||||
|
magic-string "^0.30.1"
|
||||||
|
pathe "^1.1.1"
|
||||||
|
picocolors "^1.0.0"
|
||||||
|
std-env "^3.3.3"
|
||||||
|
strip-literal "^1.0.1"
|
||||||
|
tinybench "^2.5.0"
|
||||||
|
tinypool "^0.7.0"
|
||||||
|
vite "^3.1.0 || ^4.0.0 || ^5.0.0-0"
|
||||||
|
vite-node "0.34.5"
|
||||||
|
why-is-node-running "^2.2.2"
|
||||||
|
|
||||||
vm2@^3.9.17:
|
vm2@^3.9.17:
|
||||||
version "3.9.19"
|
version "3.9.19"
|
||||||
resolved "https://registry.yarnpkg.com/vm2/-/vm2-3.9.19.tgz#be1e1d7a106122c6c492b4d51c2e8b93d3ed6a4a"
|
resolved "https://registry.yarnpkg.com/vm2/-/vm2-3.9.19.tgz#be1e1d7a106122c6c492b4d51c2e8b93d3ed6a4a"
|
||||||
|
|
Loading…
Reference in New Issue