Skip to content

Commit d028a9d

Browse files
committed
🎉 feat: operationId per possible path
1 parent 598fea4 commit d028a9d

File tree

14 files changed

+397
-249
lines changed

14 files changed

+397
-249
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
Feature:
33
- add `withHeader` for adding custom headers to response schema
44
- spread all possible path for optional params
5+
- provider can be `null` to disable provider
6+
- export `toOpenAPI` to generate spec programmatically
57

68
Breaking change:
79
- rename `@elysiajs/swagger` to `@elysiajs/openapi`

build.ts

Lines changed: 21 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -4,30 +4,30 @@ import { build, type Options } from 'tsup'
44
await $`rm -rf dist`
55

66
const tsupConfig: Options = {
7-
entry: ['src/**/*.ts'],
8-
splitting: false,
9-
sourcemap: false,
10-
clean: true,
11-
bundle: true
7+
entry: ['src/**/*.ts'],
8+
splitting: false,
9+
sourcemap: false,
10+
clean: true,
11+
bundle: true
1212
} satisfies Options
1313

1414
await Promise.all([
15-
// ? tsup esm
16-
build({
17-
outDir: 'dist',
18-
format: 'esm',
19-
target: 'node20',
20-
cjsInterop: false,
21-
...tsupConfig
22-
}),
23-
// ? tsup cjs
24-
build({
25-
outDir: 'dist/cjs',
26-
format: 'cjs',
27-
target: 'node20',
28-
// dts: true,
29-
...tsupConfig
30-
})
15+
// ? tsup esm
16+
build({
17+
outDir: 'dist',
18+
format: 'esm',
19+
target: 'node20',
20+
cjsInterop: false,
21+
...tsupConfig
22+
}),
23+
// ? tsup cjs
24+
build({
25+
outDir: 'dist/cjs',
26+
format: 'cjs',
27+
target: 'node20',
28+
// dts: true,
29+
...tsupConfig
30+
})
3131
])
3232

3333
await $`tsc --project tsconfig.dts.json`

bun.lock

Lines changed: 56 additions & 33 deletions
Large diffs are not rendered by default.

example/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ const app = new Elysia()
6161
description: 'Void response'
6262
}),
6363
{
64-
'X-Custom-Header': t.String()
64+
'X-Custom-Header': t.Literal('Elysia')
6565
}
6666
)
6767
}

package.json

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -72,16 +72,15 @@
7272
"elysia": ">= 1.3.0"
7373
},
7474
"devDependencies": {
75-
"@apidevtools/swagger-parser": "^10.1.0",
76-
"@types/bun": "1.1.14",
77-
"@scalar/types": "^0.0.12",
78-
"elysia": "1.3.0-exp.71",
75+
"@apidevtools/swagger-parser": "^12.0.0",
76+
"@types/bun": "1.2.20",
77+
"@scalar/types": "^0.2.13",
78+
"elysia": "1.3.21",
7979
"eslint": "9.6.0",
80-
"tsup": "^8.1.0",
81-
"typescript": "^5.5.3"
80+
"tsup": "^8.5.0",
81+
"typescript": "^5.9.2"
8282
},
8383
"dependencies": {
84-
"@scalar/themes": "^0.9.52",
8584
"openapi-types": "^12.1.3"
8685
}
8786
}

src/index.ts

Lines changed: 23 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -6,38 +6,29 @@ import { ScalarRender } from './scalar'
66
import { toOpenAPISchema } from './openapi'
77

88
import type { OpenAPIV3 } from 'openapi-types'
9-
import type { ReferenceConfiguration } from '@scalar/types'
9+
import type { ApiReferenceConfiguration } from '@scalar/types'
1010
import type { ElysiaOpenAPIConfig, OpenAPIProvider } from './types'
1111

1212
/**
13-
* Plugin for [elysia](https://github.com/elysiajs/elysia) that auto-generate Swagger page.
13+
* Plugin for [elysia](https://github.com/elysiajs/elysia) that auto-generate OpenAPI documentation page.
1414
*
1515
* @see https://github.com/elysiajs/elysia-swagger
1616
*/
1717
export const openapi = <
18+
const Enabled extends boolean = true,
1819
const Path extends string = '/openapi',
1920
const Provider extends OpenAPIProvider = 'scalar'
2021
>({
21-
provider = 'scalar',
22+
enabled = true as Enabled,
2223
path = '/openapi' as Path,
24+
provider = 'scalar' as Provider,
2325
specPath = `${path}/json`,
2426
documentation = {},
2527
exclude,
2628
swagger,
2729
scalar
28-
}: ElysiaOpenAPIConfig<Path, Provider> = {}) => {
29-
const {
30-
version: swaggerVersion = '5.9.0',
31-
theme: swaggerTheme = `https://unpkg.com/swagger-ui-dist@${swaggerVersion}/swagger-ui.css`,
32-
autoDarkMode = true,
33-
...swaggerOptions
34-
} = swagger ?? {}
35-
36-
const {
37-
version: scalarVersion = 'latest',
38-
cdn: scalarCDN = '',
39-
...scalarConfig
40-
} = scalar ?? {}
30+
}: ElysiaOpenAPIConfig<Enabled, Path, Provider> = {}) => {
31+
if (!enabled) return new Elysia({ name: '@elysiajs/openapi' })
4132

4233
const info = {
4334
title: 'Elysia Documentation',
@@ -51,46 +42,28 @@ export const openapi = <
5142
let totalRoutes = 0
5243
let cachedSchema: OpenAPIV3.Document | undefined
5344

54-
const app = new Elysia({ name: '@elysiajs/swagger' })
45+
const app = new Elysia({ name: '@elysiajs/openapi' })
5546
.use((app) => {
5647
if (provider === null) return app
5748

5849
return app.get(
5950
path,
6051
new Response(
6152
provider === 'swagger-ui'
62-
? SwaggerUIRender(
63-
info,
64-
swaggerVersion,
65-
swaggerTheme,
66-
JSON.stringify(
67-
{
68-
url: relativePath,
69-
dom_id: '#swagger-ui',
70-
...swaggerOptions
71-
},
72-
(_, value) =>
73-
typeof value === 'function'
74-
? undefined
75-
: value
76-
),
77-
autoDarkMode
78-
)
79-
: ScalarRender(
80-
info,
81-
scalarVersion,
82-
{
83-
spec: {
84-
url: relativePath,
85-
...scalarConfig.spec
86-
},
87-
...scalarConfig,
88-
// so we can showcase the elysia theme
89-
// @ts-expect-error
90-
_integration: 'elysiajs'
91-
} satisfies ReferenceConfiguration,
92-
scalarCDN
93-
),
53+
? SwaggerUIRender(info, {
54+
url: relativePath,
55+
dom_id: '#swagger-ui',
56+
version: 'latest',
57+
autoDarkMode: true,
58+
...swagger
59+
})
60+
: ScalarRender(info, {
61+
url: relativePath,
62+
version: 'latest',
63+
cdn: `https://cdn.jsdelivr.net/npm/@scalar/api-reference@${scalar?.version ?? 'latest'}/dist/browser/standalone.min.js`,
64+
...(scalar as ApiReferenceConfiguration),
65+
_integration: 'elysiajs'
66+
}),
9467
{
9568
headers: {
9669
'content-type': 'text/html; charset=utf8'
@@ -155,4 +128,5 @@ export const openapi = <
155128

156129
export { toOpenAPISchema, withHeaders } from './openapi'
157130
export type { ElysiaOpenAPIConfig }
131+
158132
export default openapi

src/openapi.ts

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,9 @@ const toOperationId = (method: string, paths: string) => {
1717
if (!paths || paths === '/') return operationId + 'Index'
1818

1919
for (const path of paths.split('/'))
20-
operationId +=
21-
path.charCodeAt(0) === 123
22-
? 'By' + capitalize(path.slice(1, -1))
23-
: capitalize(path)
20+
operationId += path.includes(':')
21+
? 'By' + capitalize(path.replace(':', ''))
22+
: capitalize(path)
2423

2524
operationId = operationId.replace(/\?/g, 'Optional')
2625

@@ -71,7 +70,7 @@ export function toOpenAPISchema(
7170
? [exclude.paths]
7271
: []
7372

74-
const paths: OpenAPIV3.PathsObject = {}
73+
const paths: OpenAPIV3.PathsObject = Object.create(null)
7574

7675
// @ts-ignore private property
7776
const routes = app.getGlobalRoutes()
@@ -260,7 +259,8 @@ export function toOpenAPISchema(
260259
for (let [status, schema] of Object.entries(hooks.response)) {
261260
if (typeof schema === 'string') schema = toRef(schema)
262261

263-
const { type, examples, ...options } = schema
262+
// Must exclude $ref from root options
263+
const { type, examples, $ref, ...options } = schema
264264

265265
operation.responses[status] = {
266266
description: `Response for status ${status}`,
@@ -294,7 +294,7 @@ export function toOpenAPISchema(
294294
}
295295

296296
for (let path of getPossiblePath(route.path)) {
297-
const operationId = toOperationId(route.method, route.path)
297+
const operationId = toOperationId(route.method, path)
298298

299299
path = path.replace(/:([^/]+)/g, '{$1}')
300300

@@ -311,11 +311,20 @@ export function toOpenAPISchema(
311311
}
312312

313313
// Handle 'ALL' method by assigning operation to all standard methods
314-
for(const method of ['get', 'post', 'put', 'delete', 'patch', 'head', 'options', 'trace'])
315-
current[method] = {
316-
...operation,
317-
operationId
318-
}
314+
for (const method of [
315+
'get',
316+
'post',
317+
'put',
318+
'delete',
319+
'patch',
320+
'head',
321+
'options',
322+
'trace'
323+
])
324+
current[method] = {
325+
...operation,
326+
operationId
327+
}
319328
}
320329
}
321330

0 commit comments

Comments
 (0)