Skip to content

Commit 128cff3

Browse files
committed
🔧 fix(type generator): handle non-intersect routes
1 parent 0d9fa40 commit 128cff3

File tree

6 files changed

+139
-60
lines changed

6 files changed

+139
-60
lines changed

CHANGELOG.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,15 @@
1+
# 1.4.4 - 20 Sep 2025
2+
Improvement:
3+
- cast `exclude.methods` to lowercase when checking for method exclusion
4+
- type generator: handle non-intersect routes eg. group/guard
5+
6+
Change:
7+
- type generator: enable type error log
8+
- type generator: do not remove temp files when debug is enabled
9+
10+
Bug fix:
11+
- exclude `options` method by default
12+
113
# 1.4.3 - 18 Sep 2025
214
Improvement:
315
- unwrap model reference into parameter schema

example/gen.ts

Lines changed: 44 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,44 @@
1-
import { Elysia, t } from 'elysia'
2-
import { openapi, withHeaders } from '../src/index'
3-
import { fromTypes } from '../src/gen'
4-
5-
export const app = new Elysia()
6-
.use(
7-
openapi({
8-
references: fromTypes('example/gen.ts', {
9-
debug: true
10-
})
11-
})
12-
)
13-
.get(
14-
'/',
15-
() =>
16-
({ test: 'hello' as const }) as any as
17-
| { test: 'hello' }
18-
| undefined,
19-
{
20-
response: {
21-
204: withHeaders(
22-
t.Void({
23-
title: 'Thing',
24-
description: 'Void response'
25-
}),
26-
{
27-
'X-Custom-Header': t.Literal('Elysia')
28-
}
29-
)
30-
}
31-
}
32-
)
33-
.post(
34-
'/json',
35-
({ body, status }) => (Math.random() > 0.5 ? status(418) : body),
36-
{
37-
body: t.Object({
38-
hello: t.String()
39-
})
40-
}
41-
)
42-
.get('/id/:id/name/:name', ({ params }) => params)
43-
.listen(3000)
1+
import { Elysia, t } from 'elysia'
2+
import { openapi, withHeaders } from '../src/index'
3+
import { fromTypes } from '../src/gen'
4+
5+
export const app = new Elysia()
6+
.use(
7+
openapi({
8+
references: fromTypes('example/gen.ts', {
9+
debug: true
10+
})
11+
})
12+
)
13+
.get(
14+
'/',
15+
() =>
16+
({ test: 'hello' as const }) as any as
17+
| { test: 'hello' }
18+
| undefined,
19+
{
20+
response: {
21+
204: withHeaders(
22+
t.Void({
23+
title: 'Thing',
24+
description: 'Void response'
25+
}),
26+
{
27+
'X-Custom-Header': t.Literal('Elysia')
28+
}
29+
)
30+
}
31+
}
32+
)
33+
.post(
34+
'/json',
35+
({ body, status }) => (Math.random() > 0.5 ? status(418) : body),
36+
{
37+
body: t.Object({
38+
hello: t.String()
39+
})
40+
}
41+
)
42+
.get('/id/:id/name/:name', ({ params }) => params)
43+
.get('/a', () => 'hello')
44+
.listen(3000)

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@elysiajs/openapi",
3-
"version": "1.4.3",
3+
"version": "1.4.4",
44
"description": "Plugin for Elysia to auto-generate API documentation",
55
"author": {
66
"name": "saltyAom",

src/gen/index.ts

Lines changed: 59 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,53 @@ interface OpenAPIGeneratorOptions {
7373
* ! be careful that the folder will be removed after the process ends
7474
*/
7575
tmpRoot?: string
76+
77+
/**
78+
* disable log
79+
* @default false
80+
*/
81+
silent?: boolean
82+
}
83+
84+
function extractRootObjects(code: string) {
85+
const results = []
86+
let i = 0
87+
88+
while (i < code.length) {
89+
// find the next colon
90+
const colonIdx = code.indexOf(':', i)
91+
if (colonIdx === -1) break
92+
93+
// backtrack to find the key (simple word characters)
94+
let keyEnd = colonIdx - 1
95+
while (keyEnd >= 0 && /\s/.test(code[keyEnd])) keyEnd--
96+
let keyStart = keyEnd
97+
while (keyStart >= 0 && /\w/.test(code[keyStart])) keyStart--
98+
99+
// find the opening brace after colon
100+
const braceIdx = code.indexOf('{', colonIdx)
101+
if (braceIdx === -1) break
102+
103+
// scan braces
104+
let depth = 0
105+
let end = braceIdx
106+
for (; end < code.length; end++) {
107+
if (code[end] === '{') depth++
108+
else if (code[end] === '}') {
109+
depth--
110+
if (depth === 0) {
111+
end++ // move past closing brace
112+
break
113+
}
114+
}
115+
}
116+
117+
results.push(`{${code.slice(keyStart + 1, end)};}`)
118+
119+
i = end
120+
}
121+
122+
return results
76123
}
77124

78125
/**
@@ -97,7 +144,8 @@ export const fromTypes =
97144
overrideOutputPath,
98145
debug = false,
99146
compilerOptions,
100-
tmpRoot = join(tmpdir(), '.ElysiaAutoOpenAPI')
147+
tmpRoot = join(tmpdir(), '.ElysiaAutoOpenAPI'),
148+
silent = false
101149
}: OpenAPIGeneratorOptions = {}
102150
) =>
103151
() => {
@@ -177,7 +225,7 @@ export const fromTypes =
177225
spawnSync(`tsc`, {
178226
shell: true,
179227
cwd: tmpRoot,
180-
stdio: debug ? 'inherit' : undefined
228+
stdio: silent ? undefined : 'inherit'
181229
})
182230

183231
const fileName = targetFilePath
@@ -235,7 +283,7 @@ export const fromTypes =
235283
console.warn(tempFiles)
236284
}
237285
} else {
238-
console.log(
286+
console.warn(
239287
"reason: root folder doesn't exists",
240288
join(tmpRoot, 'dist')
241289
)
@@ -247,8 +295,10 @@ export const fromTypes =
247295

248296
const declaration = readFileSync(targetFile, 'utf8')
249297

298+
// console.log(declaration, targetFile)
299+
250300
// Check just in case of race-condition
251-
if (existsSync(tmpRoot))
301+
if (!debug && existsSync(tmpRoot))
252302
rmSync(tmpRoot, { recursive: true, force: true })
253303

254304
let instance = declaration.match(
@@ -282,11 +332,8 @@ export const fromTypes =
282332
const routes: AdditionalReference = {}
283333

284334
// Treaty is a collection of { ... } & { ... } & { ... }
285-
// Each route will be intersected with each other
286-
// instead of being nested in a route object
287-
for (const route of routesString.slice(1).split('} & {')) {
288-
// as '} & {' is removed, we need to add it back
289-
let schema = TypeBox(`{${route}}`)
335+
for (const route of extractRootObjects(routesString)) {
336+
let schema = TypeBox(route)
290337
if (schema.type !== 'object') continue
291338

292339
const paths = []
@@ -302,6 +349,9 @@ export const fromTypes =
302349
}
303350

304351
const method = paths.pop()!
352+
// For whatever reason, if failed to infer route correctly
353+
if (!method) continue
354+
305355
const path = '/' + paths.join('/')
306356
schema = schema.properties
307357

src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { toOpenAPISchema } from './openapi'
77

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

1212
/**
1313
* Plugin for [elysia](https://github.com/elysiajs/elysia) that auto-generate OpenAPI documentation page.

src/openapi.ts

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,8 @@ openapi({
8787
zod: zodToJsonSchema
8888
}
8989
})`,
90-
valibot: `import { toJsonSchema } from '@valibot/to-json-schema'
90+
valibot: `import openapi from '@elysiajs/openapi'
91+
import { toJsonSchema } from '@valibot/to-json-schema'
9192
9293
openapi({
9394
mapJsonSchema: {
@@ -204,12 +205,14 @@ export function toOpenAPISchema(
204205
references?: AdditionalReferences,
205206
vendors?: MapJsonSchema
206207
) {
207-
const {
208-
methods: excludeMethods = ['OPTIONS'],
208+
let {
209+
methods: excludeMethods = ['options'],
209210
staticFile: excludeStaticFile = true,
210211
tags: excludeTags
211212
} = exclude ?? {}
212213

214+
excludeMethods = excludeMethods.map((method) => method.toLowerCase())
215+
213216
const excludePaths = Array.isArray(exclude?.paths)
214217
? exclude.paths
215218
: typeof exclude?.paths !== 'undefined'
@@ -249,6 +252,10 @@ export function toOpenAPISchema(
249252
detail: Partial<OpenAPIV3.OperationObject>
250253
} = route.hooks ?? {}
251254

255+
if (route.path === '/a') {
256+
console.log('H')
257+
}
258+
252259
if (references?.length)
253260
for (const reference of references as AdditionalReference[]) {
254261
if (!reference) continue
@@ -327,7 +334,10 @@ export function toOpenAPISchema(
327334

328335
// Handle query parameters
329336
if (hooks.query) {
330-
const query = unwrapReference(unwrapSchema(hooks.query, vendors), definitions)
337+
const query = unwrapReference(
338+
unwrapSchema(hooks.query, vendors),
339+
definitions
340+
)
331341

332342
if (query && query.type === 'object' && query.properties) {
333343
const required = query.required || []
@@ -345,7 +355,10 @@ export function toOpenAPISchema(
345355

346356
// Handle header parameters
347357
if (hooks.headers) {
348-
const headers = unwrapReference(unwrapSchema(hooks.query, vendors), definitions)
358+
const headers = unwrapReference(
359+
unwrapSchema(hooks.query, vendors),
360+
definitions
361+
)
349362

350363
if (headers && headers.type === 'object' && headers.properties) {
351364
const required = headers.required || []
@@ -363,7 +376,10 @@ export function toOpenAPISchema(
363376

364377
// Handle cookie parameters
365378
if (hooks.cookie) {
366-
const cookie = unwrapReference(unwrapSchema(hooks.cookie, vendors), definitions)
379+
const cookie = unwrapReference(
380+
unwrapSchema(hooks.cookie, vendors),
381+
definitions
382+
)
367383

368384
if (cookie && cookie.type === 'object' && cookie.properties) {
369385
const required = cookie.required || []

0 commit comments

Comments
 (0)