diff --git a/.changeset/tender-candles-laugh.md b/.changeset/tender-candles-laugh.md
new file mode 100644
index 000000000..7a2dc7967
--- /dev/null
+++ b/.changeset/tender-candles-laugh.md
@@ -0,0 +1,5 @@
+---
+'@hono/swagger-editor': major
+---
+
+Create swagger editor middleware for hono
diff --git a/.github/workflows/ci-swagger-editor.yml b/.github/workflows/ci-swagger-editor.yml
new file mode 100644
index 000000000..0c9eec4e1
--- /dev/null
+++ b/.github/workflows/ci-swagger-editor.yml
@@ -0,0 +1,25 @@
+name: ci-swagger-editor
+on:
+ push:
+ branches: [main]
+ paths:
+ - 'packages/swagger-editor/**'
+ pull_request:
+ branches: ['*']
+ paths:
+ - 'packages/swagger-editor/**'
+
+jobs:
+ ci:
+ runs-on: ubuntu-latest
+ defaults:
+ run:
+ working-directory: ./packages/swagger-editor
+ 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
diff --git a/package.json b/package.json
index 459b39b3b..e85797dbe 100644
--- a/package.json
+++ b/package.json
@@ -23,6 +23,7 @@
"build:zod-openapi": "yarn workspace @hono/zod-openapi install && yarn workspace @hono/zod-openapi build",
"build:typia-validator": "yarn workspace @hono/typia-validator build",
"build:swagger-ui": "yarn workspace @hono/swagger-ui build",
+ "build:swagger-editor": "yarn workspace @hono/swagger-editor build",
"build:esbuild-transpiler": "yarn workspace @hono/esbuild-transpiler build",
"build:event-emitter": "yarn workspace @hono/event-emitter build",
"build:oauth-providers": "yarn workspace @hono/oauth-providers build",
diff --git a/packages/swagger-editor/README.md b/packages/swagger-editor/README.md
new file mode 100644
index 000000000..b92df570b
--- /dev/null
+++ b/packages/swagger-editor/README.md
@@ -0,0 +1,40 @@
+# Swagger Editor Middleware for Hono
+
+This library, `@hono/swagger-editor` is the middleware for integrating Swagger Editor with Hono applications. The Swagger Editor is an open source editor to design, define and document RESTful APIs in the Swagger Specification.
+
+## Installation
+
+```bash
+npm install @hono/swagger-editor
+# or
+yarn add @hono/swagger-editor
+```
+
+## Usage
+
+You can use the `swaggerEditor` middleware to serve Swagger Editor 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 Editor at /swagger-editor
+app.get('/swagger-editor', swaggerEditor({ url: '/doc' }))
+
+export default app
+```
+
+## Options
+
+Middleware supports almost all swagger-editor options. See full documentation:
+
+## Authors
+
+- Ogabek Yuldoshev
+
+## License
+
+MIT
\ No newline at end of file
diff --git a/packages/swagger-editor/package.json b/packages/swagger-editor/package.json
new file mode 100644
index 000000000..74a95d6b5
--- /dev/null
+++ b/packages/swagger-editor/package.json
@@ -0,0 +1,48 @@
+{
+ "name": "@hono/swagger-editor",
+ "version": "0.0.0",
+ "description": "A middleware for using Swagger Editor 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",
+ "prerelease": "yarn build && yarn test",
+ "release": "yarn publish"
+ },
+ "license": "MIT",
+ "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": {
+ "hono": "^3.11.7",
+ "tsup": "^7.2.0",
+ "vitest": "^0.34.5"
+ }
+}
\ No newline at end of file
diff --git a/packages/swagger-editor/src/index.ts b/packages/swagger-editor/src/index.ts
new file mode 100644
index 000000000..326fc7f74
--- /dev/null
+++ b/packages/swagger-editor/src/index.ts
@@ -0,0 +1,195 @@
+import type { Context } from 'hono'
+import type { CustomSwaggerUIOptions } from './types'
+
+const DEFAULT_VERSION = '4.13.1'
+
+const CDN_LINK = 'https://cdn.jsdelivr.net/npm/swagger-editor-dist'
+
+export const MODERN_NORMALIZE_CSS = `
+*,
+::before,
+::after {
+ box-sizing: border-box;
+}
+
+html {
+ font-family:
+ system-ui,
+ 'Segoe UI',
+ Roboto,
+ Helvetica,
+ Arial,
+ sans-serif,
+ 'Apple Color Emoji',
+ 'Segoe UI Emoji';
+ line-height: 1.15; /* 1. Correct the line height in all browsers. */
+ -webkit-text-size-adjust: 100%; /* 2. Prevent adjustments of font size after orientation changes in iOS. */
+ tab-size: 4; /* 3. Use a more readable tab size (opinionated). */
+}
+
+body {
+ margin: 0;
+}
+
+b,
+strong {
+ font-weight: bolder;
+}
+
+code,
+kbd,
+samp,
+pre {
+ font-family:
+ ui-monospace,
+ SFMono-Regular,
+ Consolas,
+ 'Liberation Mono',
+ Menlo,
+ monospace; /* 1 */
+ font-size: 1em; /* 2 */
+}
+
+small {
+ font-size: 80%;
+}
+
+sub,
+sup {
+ font-size: 75%;
+ line-height: 0;
+ position: relative;
+ vertical-align: baseline;
+}
+
+sub {
+ bottom: -0.25em;
+}
+
+sup {
+ top: -0.5em;
+}
+
+table {
+ border-color: currentcolor;
+}
+
+button,
+input,
+optgroup,
+select,
+textarea {
+ font-family: inherit; /* 1 */
+ font-size: 100%; /* 1 */
+ line-height: 1.15; /* 1 */
+ margin: 0; /* 2 */
+}
+
+button,
+[type='button'],
+[type='reset'],
+[type='submit'] {
+ -webkit-appearance: button;
+}
+
+legend {
+ padding: 0;
+}
+
+progress {
+ vertical-align: baseline;
+}
+
+::-webkit-inner-spin-button,
+::-webkit-outer-spin-button {
+ height: auto;
+}
+
+[type='search'] {
+ -webkit-appearance: textfield; /* 1 */
+ outline-offset: -2px; /* 2 */
+}
+
+::-webkit-search-decoration {
+ -webkit-appearance: none;
+}
+
+::-webkit-file-upload-button {
+ -webkit-appearance: button; /* 1 */
+ font: inherit; /* 2 */
+}
+
+summary {
+ display: list-item;
+}
+
+.Pane2 {
+ overflow-y: scroll;
+}
+`
+
+function getUrl(version?: string) {
+ return `${CDN_LINK}@${version ? version : DEFAULT_VERSION}`
+}
+
+export interface SwaggerEditorOptions extends CustomSwaggerUIOptions {
+ version?: string
+}
+
+export function swaggerEditor(options: SwaggerEditorOptions = {}) {
+ const url = getUrl()
+
+ options.layout = 'StandaloneLayout'
+
+ const optionString = Object.entries(options)
+ .map(([key, value]) => {
+ if (typeof value === 'string') {
+ return `${key}:'${value}'`
+ }
+ if (Array.isArray(value)) {
+ return `${key}:${value.map((v) => `${v}`).join(', ')}`
+ }
+ if (typeof value === 'object') {
+ return `${key}:${JSON.stringify(value)}`
+ }
+
+ return `${key}: ${value}`
+ })
+ .join(',')
+
+ return async (c: Context) =>
+ c.html(`
+
+
+
+
+ Swagger Editor
+
+
+
+
+
+
+
+
+
+
+
+
+
+`)
+}
diff --git a/packages/swagger-editor/src/types.ts b/packages/swagger-editor/src/types.ts
new file mode 100644
index 000000000..7097df54d
--- /dev/null
+++ b/packages/swagger-editor/src/types.ts
@@ -0,0 +1,297 @@
+export interface CustomSwaggerUIOptions {
+ /**
+ * URL to fetch external configuration document from.
+ */
+ configUrl?: string | undefined
+ /**
+ * A JavaScript object describing the OpenAPI definition. When used, the url parameter will not be parsed. This is useful for testing manually-generated definitions without hosting them
+ */
+ spec?: { [propName: string]: any } | undefined
+ /**
+ * The URL pointing to API definition (normally swagger.json or swagger.yaml). Will be ignored if urls or spec is used.
+ */
+ url?: string | undefined
+
+ // Plugin system
+
+ /**
+ * The name of a component available via the plugin system to use as the top-level layout
+ * for Swagger UI.
+ */
+ layout?: string | undefined
+ /**
+ * A Javascript object to configure plugin integration and behaviors
+ */
+ pluginsOptions?: PluginsOptions
+ /**
+ * An array of plugin functions to use in Swagger UI.
+ */
+ plugins?: SwaggerUIPlugin[] | undefined
+ /**
+ * An array of presets to use in Swagger UI.
+ * Usually, you'll want to include ApisPreset if you use this option.
+ */
+ presets?: SwaggerUIPlugin[] | undefined
+
+ // Display
+
+ /**
+ * If set to true, enables deep linking for tags and operations.
+ * See the Deep Linking documentation for more information.
+ */
+ deepLinking?: boolean | undefined
+ /**
+ * Controls the display of operationId in operations list. The default is false.
+ */
+ displayOperationId?: boolean | undefined
+ /**
+ * The default expansion depth for models (set to -1 completely hide the models).
+ */
+ defaultModelsExpandDepth?: number | undefined
+ /**
+ * The default expansion depth for the model on the model-example section.
+ */
+ defaultModelExpandDepth?: number | undefined
+ /**
+ * Controls how the model is shown when the API is first rendered.
+ * (The user can always switch the rendering for a given model by clicking the
+ * 'Model' and 'Example Value' links.)
+ */
+ defaultModelRendering?: 'example' | 'model' | undefined
+ /**
+ * Controls the display of the request duration (in milliseconds) for "Try it out" requests.
+ */
+ displayRequestDuration?: boolean | undefined
+ /**
+ * Controls the default expansion setting for the operations and tags.
+ * It can be 'list' (expands only the tags), 'full' (expands the tags and operations)
+ * or 'none' (expands nothing).
+ */
+ docExpansion?: 'list' | 'full' | 'none' | undefined
+ /**
+ * If set, enables filtering.
+ * The top bar will show an edit box that you can use to filter the tagged operations that are shown.
+ * Can be Boolean to enable or disable, or a string, in which case filtering will be enabled
+ * using that string as the filter expression.
+ * Filtering is case sensitive matching the filter expression anywhere inside the tag.
+ */
+ filter?: boolean | string | undefined
+ /**
+ * If set, limits the number of tagged operations displayed to at most this many.
+ * The default is to show all operations.
+ */
+ maxDisplayedTags?: number | undefined
+ /**
+ * Apply a sort to the operation list of each API.
+ * It can be 'alpha' (sort by paths alphanumerically),
+ * 'method' (sort by HTTP method) or a function (see Array.prototype.sort() to know how sort function works).
+ * Default is the order returned by the server unchanged.
+ */
+ operationsSorter?: SorterLike | undefined
+ /**
+ * Controls the display of vendor extension (x-) fields and values for Operations,
+ * Parameters, Responses, and Schema.
+ */
+ showExtensions?: boolean | undefined
+ /**
+ * Controls the display of extensions (pattern, maxLength, minLength, maximum, minimum) fields
+ * and values for Parameters.
+ */
+ showCommonExtensions?: boolean | undefined
+ /**
+ * Apply a sort to the tag list of each API.
+ * It can be 'alpha' (sort by paths alphanumerically)
+ * or a function (see Array.prototype.sort() to learn how to write a sort function).
+ * Two tag name strings are passed to the sorter for each pass.
+ * Default is the order determined by Swagger UI.
+ */
+ tagsSorter?: SorterLike | undefined
+ /**
+ * When enabled, sanitizer will leave style, class and data-* attributes untouched
+ * on all HTML Elements declared inside markdown strings.
+ * This parameter is Deprecated and will be removed in 4.0.0.
+ * @deprecated
+ */
+ useUnsafeMarkdown?: boolean | undefined
+ /**
+ * Provides a mechanism to be notified when Swagger UI has finished rendering a newly provided definition.
+ */
+ onComplete?: (() => any) | undefined
+ /**
+ * Set to false to deactivate syntax highlighting of payloads and cURL command,
+ * can be otherwise an object with the activate and theme properties.
+ */
+ syntaxHighlight?:
+ | false
+ | {
+ /**
+ * Whether syntax highlighting should be activated or not.
+ */
+ activate?: boolean | undefined
+ /**
+ * Highlight.js syntax coloring theme to use. (Only these 6 styles are available.)
+ */
+ theme?:
+ | 'agate'
+ | 'arta'
+ | 'idea'
+ | 'monokai'
+ | 'nord'
+ | 'obsidian'
+ | 'tomorrow-night'
+ | undefined
+ }
+ | undefined
+ /**
+ * Controls whether the "Try it out" section should be enabled by default.
+ */
+ tryItOutEnabled?: boolean | undefined
+ /**
+ * This is the default configuration section for the the requestSnippets plugin.
+ */
+ requestSnippets?:
+ | {
+ generators?:
+ | {
+ [genName: string]: {
+ title: string
+ syntax: string
+ }
+ }
+ | undefined
+ defaultExpanded?: boolean | undefined
+ /**
+ * e.g. only show curl bash = ["curl_bash"]
+ */
+ languagesMask?: string[] | undefined
+ }
+ | undefined
+
+ // Network
+
+ /**
+ * OAuth redirect URL.
+ */
+ oauth2RedirectUrl?: string | undefined
+ /**
+ * MUST be a function. Function to intercept remote definition,
+ * "Try it out", and OAuth 2.0 requests.
+ * Accepts one argument requestInterceptor(request) and must return the modified request,
+ * or a Promise that resolves to the modified request.
+ */
+ requestInterceptor?: ((a: Request) => Request | Promise) | undefined
+ /**
+ * MUST be a function. Function to intercept remote definition,
+ * "Try it out", and OAuth 2.0 responses.
+ * Accepts one argument responseInterceptor(response) and must return the modified response,
+ * or a Promise that resolves to the modified response.
+ */
+ responseInterceptor?: ((a: Response) => Response | Promise) | undefined
+ /**
+ * If set to true, uses the mutated request returned from a requestInterceptor
+ * to produce the curl command in the UI, otherwise the request
+ * beforethe requestInterceptor was applied is used.
+ */
+ showMutatedRequest?: boolean | undefined
+ /**
+ * List of HTTP methods that have the "Try it out" feature enabled.
+ * An empty array disables "Try it out" for all operations.
+ * This does not filter the operations from the display.
+ */
+ supportedSubmitMethods?: SupportedHTTPMethods[] | undefined
+ /**
+ * By default, Swagger UI attempts to validate specs against swagger.io's online validator.
+ * You can use this parameter to set a different validator URL,
+ * for example for locally deployed validators (Validator Badge).
+ * Setting it to either none, 127.0.0.1 or localhost will disable validation.
+ */
+ validatorUrl?: string | undefined
+ /**
+ * If set to true, enables passing credentials, as defined in the Fetch standard,
+ * in CORS requests that are sent by the browser.
+ * Note that Swagger UI cannot currently set cookies cross-domain (see swagger-js#1163)
+ * - as a result, you will have to rely on browser-supplied
+ * cookies (which this setting enables sending) that Swagger UI cannot control.
+ */
+ withCredentials?: boolean | undefined
+
+ // Macros
+
+ /**
+ * Function to set default values to each property in model.
+ * Accepts one argument modelPropertyMacro(property), property is immutable
+ */
+ modelPropertyMacro?: ((propName: Readonly) => any) | undefined
+ /**
+ * Function to set default value to parameters.
+ * Accepts two arguments parameterMacro(operation, parameter).
+ * Operation and parameter are objects passed for context, both remain immutable
+ */
+ parameterMacro?: ((operation: Readonly, parameter: Readonly) => any) | undefined
+
+ // Authorization
+
+ /**
+ * If set to true, it persists authorization data and it would not be lost on browser close/refresh
+ */
+ persistAuthorization?: boolean | undefined
+}
+
+interface PluginsOptions {
+ /**
+ * Control behavior of plugins when targeting the same component with wrapComponent.
+ * - `legacy` (default) : last plugin takes precedence over the others
+ * - `chain` : chain wrapComponents when targeting the same core component,
+ * allowing multiple plugins to wrap the same component
+ * @default 'legacy'
+ */
+ pluginLoadType?: PluginLoadType
+}
+
+type PluginLoadType = 'legacy' | 'chain'
+
+type SupportedHTTPMethods =
+ | 'get'
+ | 'put'
+ | 'post'
+ | 'delete'
+ | 'options'
+ | 'head'
+ | 'patch'
+ | 'trace'
+
+type SorterLike = 'alpha' | 'method' | ((name1: string, name2: string) => number)
+
+interface Request {
+ [prop: string]: any
+}
+
+interface Response {
+ [prop: string]: any
+}
+
+/**
+ * See https://swagger.io/docs/open-source-tools/swagger-ui/customization/plugin-api/
+ */
+type SwaggerUIPlugin = (system: any) => {
+ statePlugins?:
+ | {
+ [stateKey: string]: {
+ actions?: Indexable | undefined
+ reducers?: Indexable | undefined
+ selectors?: Indexable | undefined
+ wrapActions?: Indexable | undefined
+ wrapSelectors?: Indexable | undefined
+ }
+ }
+ | undefined
+ components?: Indexable | undefined
+ wrapComponents?: Indexable | undefined
+ rootInjects?: Indexable | undefined
+ afterLoad?: ((system: any) => any) | undefined
+ fn?: Indexable | undefined
+}
+
+interface Indexable {
+ [index: string]: any
+}
diff --git a/packages/swagger-editor/test/index.test.ts b/packages/swagger-editor/test/index.test.ts
new file mode 100644
index 000000000..c9f799103
--- /dev/null
+++ b/packages/swagger-editor/test/index.test.ts
@@ -0,0 +1,32 @@
+import { Hono } from 'hono'
+import { swaggerEditor } from '../src'
+
+describe('Swagger Editor Middleware', () => {
+ let app: Hono
+
+ beforeEach(() => {
+ app = new Hono()
+ })
+
+ it('responds with status 200', async () => {
+ app.get('/swagger-editor', swaggerEditor())
+
+ const res = await app.request('/swagger-editor')
+ expect(res.status).toBe(200)
+ })
+
+ it('should contents shown', async () => {
+ app.get(
+ '/swagger-editor',
+ swaggerEditor({
+ url: 'https://petstore3.swagger.io/api/v3/openapi.json',
+ })
+ )
+
+ const res = await app.request('/swagger-editor')
+ const html = await res.text()
+
+ expect(html).toContain('https://petstore3.swagger.io/api/v3/openapi.json')
+ expect(html).toContain('https://cdn.jsdelivr.net/npm/swagger-editor-dist')
+ })
+})
diff --git a/packages/swagger-editor/tsconfig.json b/packages/swagger-editor/tsconfig.json
new file mode 100644
index 000000000..af751bf5e
--- /dev/null
+++ b/packages/swagger-editor/tsconfig.json
@@ -0,0 +1,7 @@
+{
+ "extends": "../../tsconfig.json",
+ "compilerOptions": {
+ "types": ["vitest/globals"]
+ },
+ "include": ["src/**/*.ts", "src/**/*.tsx"]
+}
diff --git a/packages/swagger-editor/vitest.config.ts b/packages/swagger-editor/vitest.config.ts
new file mode 100644
index 000000000..17b54e485
--- /dev/null
+++ b/packages/swagger-editor/vitest.config.ts
@@ -0,0 +1,8 @@
+///
+import { defineConfig } from 'vitest/config'
+
+export default defineConfig({
+ test: {
+ globals: true,
+ },
+})
diff --git a/yarn.lock b/yarn.lock
index dec6276d2..4a6cd2ae0 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2622,6 +2622,18 @@ __metadata:
languageName: unknown
linkType: soft
+"@hono/swagger-editor@workspace:packages/swagger-editor":
+ version: 0.0.0-use.local
+ resolution: "@hono/swagger-editor@workspace:packages/swagger-editor"
+ dependencies:
+ hono: "npm:^3.11.7"
+ tsup: "npm:^7.2.0"
+ vitest: "npm:^0.34.5"
+ peerDependencies:
+ hono: "*"
+ languageName: unknown
+ linkType: soft
+
"@hono/swagger-ui@workspace:packages/swagger-ui":
version: 0.0.0-use.local
resolution: "@hono/swagger-ui@workspace:packages/swagger-ui"