-
Notifications
You must be signed in to change notification settings - Fork 89
Refactor Swagger to OpenAPI #235
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
Warning Rate limit exceeded@SaltyAom has exceeded the limit for the number of commits or files that can be reviewed per hour. Please wait 10 minutes and 47 seconds before requesting another review. ⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. ⛔ Files ignored due to path filters (1)
📒 Files selected for processing (4)
WalkthroughRenames the package/plugin from swagger to openapi, adds an OpenAPI generator (src/openapi.ts) with helpers (toOpenAPISchema, getPossiblePath, withHeaders), restructures types/config/exports, replaces legacy utils, updates renders and examples, and adjusts tests, package metadata, and build/TS config. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
participant Dev as Developer App
participant App as Elysia App
participant Plugin as openapi() Plugin
participant Gen as toOpenAPISchema
participant UI as Provider UI (Scalar / Swagger-UI)
Dev->>App: new Elysia().use(openapi({ provider, path, documentation, exclude, swagger, scalar }))
activate Plugin
Plugin->>App: register UI routes if provider != null (e.g., `/openapi`, specPath `/openapi/json`)
Note right of Plugin: plugin caches generated schema and tracks route count
deactivate Plugin
Client->>App: GET /openapi/json
App->>Gen: toOpenAPISchema(app, exclude, references)
Gen-->>App: { paths, components }
App-->>Client: JSON spec
sequenceDiagram
autonumber
participant Gen as toOpenAPISchema
participant Routes as App.getGlobalRoutes()
participant Filter as Exclude Filter
participant Expand as getPossiblePath()
participant Mapper as Param/Body/Response Mapper
Gen->>Routes: fetch global routes
Routes-->>Gen: route list with hooks/schemas
Gen->>Filter: apply exclude.{paths,methods,tags,staticFile}
Filter-->>Gen: filtered routes
Gen->>Expand: expand optional segments
Expand-->>Gen: multiple concrete paths
Gen->>Mapper: map params, query, headers, cookie, body, responses (supports withHeaders)
Mapper-->>Gen: operations per path/status
Gen-->>Gen: aggregate components.schemas and return document
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Poem
✨ Finishing Touches
🧪 Generate unit tests
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. CodeRabbit Commands (Invoked using PR/Issue comments)Type Other keywords and placeholders
CodeRabbit Configuration File (
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 10
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
src/swagger/index.ts (2)
21-46
: Date-time transform likely misses nested schema.properties; fix traversal.Iterating Object.entries(schema) won’t reach schema.properties[key]. The transform won’t touch createdAt/updatedAt under properties/items.
Proposed traversal:
function transformDateProperties(schema: SchemaObject): SchemaObject { if (!isSchemaObject(schema) || typeof schema !== 'object' || schema === null) return schema; - const newSchema: OpenAPIV3.SchemaObject = { ...schema }; - Object.entries(newSchema).forEach(([key, value]) => { - if (isSchemaObject(value)) { - if (isDateTimeProperty(key, value)) { + const newSchema: OpenAPIV3.SchemaObject = { ...schema }; + // Dive into object properties + if (newSchema.properties && typeof newSchema.properties === 'object') { + for (const [key, prop] of Object.entries(newSchema.properties)) { + if (isSchemaObject(prop) && isDateTimeProperty(key, prop)) { const dateTimeFormat = value.anyOf?.find((item): item is OpenAPIV3.SchemaObject => isSchemaObject(item) && item.format === 'date-time' ); if (dateTimeFormat) { const dateTimeSchema: DateTimeSchema = { type: 'string', format: 'date-time', default: dateTimeFormat.default }; - (newSchema as Record<string, SchemaObject>)[key] = dateTimeSchema; + (newSchema.properties as Record<string, SchemaObject>)[key] = dateTimeSchema; } - } else { - (newSchema as Record<string, SchemaObject>)[key] = transformDateProperties(value); - } - } - }); + } else if (isSchemaObject(prop)) { + (newSchema.properties as Record<string, SchemaObject>)[key] = transformDateProperties(prop); + } + } + } + // Arrays / tuples + if (newSchema.items) newSchema.items = transformDateProperties(newSchema.items) as any; + // oneOf/allOf/anyOf + for (const k of ['oneOf','allOf','anyOf'] as const) { + if (Array.isArray((newSchema as any)[k])) { + (newSchema as any)[k] = (newSchema as any)[k].map((s: SchemaObject) => transformDateProperties(s)); + } + } return newSchema; }
97-98
: Invalid CSS hex color "#faf9a".Hex must be 3/6/8 digits; this is 5 and will be ignored. Use "#faf9aa" or intended value.
- color: #faf9a; + color: #faf9aa;
🧹 Nitpick comments (22)
tsconfig.json (1)
30-30
: "declaration": true + "noEmit": true = no-op; remove for clarity.Since noEmit prevents any output, keeping declaration here is confusing (d.ts are emitted from tsconfig.dts.json anyway).
Apply:
- "declaration": true, + // Declarations are built via tsconfig.dts.jsonCHANGELOG.md (1)
1-11
: Polish CHANGELOG.md entry for 1.3.2-# 1.3.2 - 22 Aug 2025 -Feature: -- add `withHeader` for adding custom headers to response schema -- spread all possible path for optional params - -Breaking change: +# 1.3.2 - 22 Aug 2025 +Features: +- add `withHeaders` for adding custom headers to response schema +- spread all possible paths for optional params + +Breaking changes: - rename `@elysiajs/swagger` to `@elysiajs/openapi` - map all `swagger`, and `scalar` prefix to respective `swagger` and `scalar` properties - rename `swaggerConfig`, and `scalarConfig` to `swagger` and `scalar` respectively -- map `excludePaths`, `excludeMethods`, `excludeTags`, `excludeStaticFiles` to property of `excludes` +- map `excludePaths`, `excludeMethods`, `excludeTags`, `excludeStaticFiles` to property `excludes`src/types.ts (3)
31-55
: Fix typos and CDN domain in Scalar docs.“Lease” → “Leave”; “jsdeliver.net” → “jsdelivr.com”; remove stray apostrophe.
- *' + * ... - * Lease blank to use default jsdeliver.net CDN + * Leave blank to use default jsdelivr.com CDN
11-15
: JSDoc references Swagger 2.0 but the type is OpenAPI v3.Update wording and link.
- * Customize Swagger config, refers to Swagger 2.0 config - * - * @see https://swagger.io/specification/v2/ + * Customize OpenAPI document (3.x) + * + * @see https://swagger.io/specification/
100-106
: Swagger CDN default version in docs mismatches runtime default.src/index.ts defaults to 5.9.0; fix JSDoc.
- * @default 4.18.2 + * @default 5.9.0If you intend to keep 4.18.2, update src/index.ts accordingly.
test/openapi.test.ts (1)
6-12
: Test expects a duplicate '/user/name'; prefer dedup + order-insensitive assertion.Prevents brittle tests and reflects unique route expansions.
- expect(getPossiblePath('/user/:user?/name/:name?')).toEqual([ - '/user/:user/name/:name', - '/user/name/:name', - '/user/name', - '/user/:user/name', - '/user/name' - ]) + const paths = getPossiblePath('/user/:user?/name/:name?') + expect([...new Set(paths)].sort()).toEqual( + [ + '/user/:user/name/:name', + '/user/name/:name', + '/user/name', + '/user/:user/name' + ].sort() + )Optionally dedupe in getPossiblePath itself to avoid emitting duplicates.
test/validate-schema.test.ts (1)
82-83
: Assert status and content-type before parsing.Improves failure signals and guards against non-JSON responses.
- const res = await app.handle(req('/openapi/json')).then((x) => x.json()) - await SwaggerParser.validate(res).catch((err) => fail(err)) + const response = await app.handle(req('/openapi/json')) + expect(response.status).toBe(200) + expect(response.headers.get('content-type') ?? '').toContain('application/json') + const spec = await response.json() + await SwaggerParser.validate(spec).catch((err) => fail(err))src/openapi.ts (6)
37-51
: Deduplicate expanded optional-path variantsgetPossiblePath can return duplicates when multiple optional params exist. Deduplicate to reduce redundant work.
- return paths + return Array.from(new Set(paths))
115-117
: Avoid mutating route.hooks when converting string schemas to $refYou overwrite hooks.* in-place (params/query/headers/cookie/body/response). This mutates the original route definitions and can cause side-effects in other plugins.
Create local variables (e.g., const paramsSchema = typeof hooks.params === 'string' ? toRef(hooks.params) : hooks.params) and use those for building the OpenAPI objects.
Also applies to: 133-135, 152-154, 171-173, 193-194, 281-283
12-12
: Verify toRef correctness with Elysia/TypeBoxt.Ref usually references a symbol/name, not a full JSON Pointer. Using "#/components/schemas/${name}" may generate an invalid $ref.
If needed, switch to emitting a plain OAS ref:
-const toRef = (name: string) => t.Ref(`#/components/schemas/${name}`) +const toRef = (name: string) => ({ $ref: `#/components/schemas/${name}` })Confirm with the existing validator tests and real clients (Swagger UI/Scalar). I can add a focused test if you want.
62-67
: exclude.staticFile is unusedThe option is read but never applied; either implement filtering for static-file routes or remove the option to avoid confusion.
150-167
: Headers/query/cookie: preserve required semantics for nested/array typesIf any param schemas are arrays or have nested objects, OpenAPI expects style/explode flags and accurate “required” at the parameter level. Current mapping may misrepresent such cases.
Optionally add support for style/explode and handle array formats (e.g., csv/ssv) when detected. I can draft a helper.
251-294
: Void/Null/Undefined responses deviate from OAS 3.0 content shapeStoring the schema object directly under responses[*].content is non-standard (though your tests expect it). This may trip external validators/tools.
Consider emitting either no content or an empty content object for 204/void-like types while keeping description/headers at the ResponseObject level. If you want, I can provide a compatibility toggle.
example/index.ts (1)
55-57
: Reuse declared schema constant to avoid duplicationYou already declared const schema; reuse it here instead of recreating the same object.
- test: t.Literal('hello') -}),test/index.test.ts (3)
265-274
: Add assertions for operationId uniqueness on .all expansionCurrently only counts methods. Also assert that operationIds are unique across the expanded methods.
Example:
const ops = response.paths['/all'] expect(new Set(Object.values(ops).map((o: any) => o.operationId)).size) .toBe(Object.keys(ops).length)
114-128
: Tests for exclude paths (RegExp) and case-insensitive methods would harden behaviorAdd specs that:
- exclude.paths accepts a RegExp (e.g., /^/internal/)
- exclude.methods honors 'OPTIONS' regardless of case
I can draft these tests if you share preferred route fixtures.
148-169
: Non-standard 204 content shape: confirm long-term intentThese tests lock in a non-OAS content representation for void/undefined/null. If intentional, consider documenting this quirk; if not, adjust expectations.
I can add an additional validator step gated behind a flag to ensure downstream tooling compatibility.
Also applies to: 171-196, 198-219
src/index.ts (5)
54-54
: Rename plugin identifier to match new name.Update the plugin name for clarity and tooling consistency.
-const app = new Elysia({ name: '@elysiajs/swagger' }) +const app = new Elysia({ name: '@elysiajs/openapi' })
95-97
: Use the correct charset token in Content-Type.Minor correctness fix.
- 'content-type': 'text/html; charset=utf8' + 'content-type': 'text/html; charset=utf-8'
23-23
: Normalize specPath to avoid double slashes when path ends with '/'.Prevents '/openapi//json'.
-specPath = `${path}/json`, +specPath = `${path.endsWith('/') ? path.slice(0, -1) : path}/json`,
36-41
: Avoid 'latest' for Scalar bundle by default.Pin to a known version for reproducibility, or expose a constant for easy updates.
- version: scalarVersion = 'latest', + version: scalarVersion = '1.25.0',
157-157
: Re-export OpenAPIProvider alongside config type.Improves DX when importing types from the package.
-export type { ElysiaOpenAPIConfig } +export type { ElysiaOpenAPIConfig, OpenAPIProvider }
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
⛔ Files ignored due to path filters (1)
bun.lock
is excluded by!**/*.lock
📒 Files selected for processing (15)
CHANGELOG.md
(1 hunks)example/index.ts
(2 hunks)example/index2.ts
(0 hunks)example/index3.ts
(0 hunks)package.json
(4 hunks)src/index.ts
(2 hunks)src/openapi.ts
(1 hunks)src/swagger/index.ts
(1 hunks)src/types.ts
(4 hunks)src/utils.ts
(0 hunks)test/index.test.ts
(10 hunks)test/openapi.test.ts
(1 hunks)test/validate-schema.test.ts
(1 hunks)tsconfig.dts.json
(1 hunks)tsconfig.json
(1 hunks)
💤 Files with no reviewable changes (3)
- example/index3.ts
- example/index2.ts
- src/utils.ts
🧰 Additional context used
🧬 Code graph analysis (7)
test/openapi.test.ts (1)
src/openapi.ts (1)
getPossiblePath
(37-51)
src/openapi.ts (1)
src/types.ts (1)
ElysiaOpenAPIConfig
(7-135)
test/validate-schema.test.ts (1)
src/index.ts (1)
openapi
(17-154)
test/index.test.ts (1)
src/index.ts (1)
openapi
(17-154)
src/types.ts (1)
src/index.ts (1)
ElysiaOpenAPIConfig
(157-157)
src/index.ts (4)
src/types.ts (2)
OpenAPIProvider
(5-5)ElysiaOpenAPIConfig
(7-135)src/swagger/index.ts (1)
SwaggerUIRender
(51-127)src/scalar/index.ts (1)
ScalarRender
(5-48)src/openapi.ts (1)
toOpenAPISchema
(58-331)
example/index.ts (2)
src/index.ts (2)
openapi
(17-154)withHeaders
(156-156)src/openapi.ts (1)
withHeaders
(333-336)
🪛 LanguageTool
CHANGELOG.md
[grammar] ~1-~1: There might be a mistake here.
Context: # 1.3.2 - 22 Aug 2025 Feature: - add withHeader
for adding c...
(QB_NEW_EN)
[grammar] ~12-~12: There might be a mistake here.
Context: ...rty of excludes
# 1.3.1 - 28 Jun 2025 Bug fix: - Using relative path for specP...
(QB_NEW_EN)
🪛 markdownlint-cli2 (0.17.2)
CHANGELOG.md
10-10: Hard tabs
Column: 76
(MD010, no-hard-tabs)
🔇 Additional comments (7)
tsconfig.dts.json (1)
31-31
: moduleResolution 'bundler' looks safe—verify build config
- No
require()
calls ornode:
imports found in TS/TSX sources, so deep-path resolution shouldn’t break.- Ensure your tsup/bun build is pointed at tsconfig.dts.json (or has its
tsconfig
option set) so.d.ts
outputs honormoduleResolution: "bundler"
.tsconfig.json (1)
30-30
: Verify moduleResolution "bundler" compatibility across tooling. No deep or path-mapped imports detected in src/ (only standard packages like elysia, openapi-types, @scalar/*), but ensure ESLint (parserOptions.project), ts-node/bun test runners, and IDEs all consume this tsconfig.json or have matching overrides.src/swagger/index.ts (1)
31-33
: LGTM: Arrow function tightening preserves narrowing semantics.Predicate still narrows to OpenAPIV3.SchemaObject correctly and behavior is unchanged.
package.json (2)
10-19
: Top-level fields remain consistent with exports — LGTM.main/module/types align with "." export.
35-44
: Verify Swagger export files are published. After running the build, confirm that the following paths exist to match the exports in package.json:
- dist/swagger/index.d.ts
- dist/swagger/index.mjs
- dist/cjs/swagger/index.js
- dist/swagger/types.d.ts
- dist/swagger/types.mjs
- dist/cjs/swagger/types.js
example/index.ts (1)
58-67
: Nice use of withHeaders for 204 exampleClear demonstration of header attachment on a no-content response.
src/index.ts (1)
17-20
: No changes needed – TS @^5.5.3 supports const generics. Verified the repo’s TypeScript version (package.json) is ^5.5.3, which meets the ≥5.0 requirement.
"./openapi": { | ||
"types": "./dist/openapi.d.ts", | ||
"import": "./dist/openapi.mjs", | ||
"require": "./dist/cjs/openapi.js" | ||
}, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Verification agent
🧩 Analysis chain
Ensure subpath export "./openapi" is the documented entry.
Looks good; verify README and examples import from "@elysiajs/openapi/openapi".
🏁 Script executed:
#!/bin/bash
rg -nP "@elysiajs/(swagger|openapi)" -g 'README.md' -g 'docs/**' -S
Length of output: 194
🏁 Script executed:
#!/bin/bash
# Search for openapi references in all markdown files and examples directory
rg -nP "@elysiajs/openapi" -g '*.md' -S
rg -nP "@elysiajs/openapi" -g 'examples/**' -S
Length of output: 302
Update README and examples imports to use the new subpath
Replace all occurrences of @elysiajs/swagger
in README.md (e.g. lines 1, 6, 12) with @elysiajs/openapi/openapi
, and ensure any example files import from @elysiajs/openapi/openapi
rather than the old package.
🤖 Prompt for AI Agents
In package.json around lines 20 to 24, the package subpath was changed to
"./openapi" but README and example files still import from the old package name;
update README.md (e.g. lines 1, 6, 12) and all example source files to replace
any occurrences of "@elysiajs/swagger" with "@elysiajs/openapi/openapi" and
update import statements accordingly so examples import from
"@elysiajs/openapi/openapi" (ensure default/named import forms remain correct
and run a quick project-wide search to catch all occurrences).
"homepage": "https://github.com/elysiajs/elysia-openapi", | ||
"repository": { | ||
"type": "git", | ||
"url": "https://github.com/elysiajs/elysia-swagger" | ||
"url": "https://github.com/elysiajs/elysia-openapi" | ||
}, | ||
"bugs": "https://github.com/elysiajs/elysia-swagger/issues", | ||
"bugs": "https://github.com/elysiajs/elysia-openapi/issues", | ||
"license": "MIT", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Verification agent
🧩 Analysis chain
Repository/homepage/bugs URLs changed — validate correctness before release.
Avoid publishing with stale links.
🏁 Script executed:
#!/bin/bash
for u in \
"https://github.com/elysiajs/elysia-openapi" \
"https://github.com/elysiajs/elysia-openapi/issues"; do
echo "Check: $u"
done
Length of output: 447
🏁 Script executed:
#!/bin/bash
for u in \
"https://github.com/elysiajs/elysia-openapi" \
"https://github.com/elysiajs/elysia-openapi/issues"; do
echo "Checking: $u"
curl -o /dev/null -s -w "HTTP %{http_code} - Final URL: %{url_effective}\n" -L "$u"
done
Length of output: 851
Broken homepage/repository/bugs URLs — update before release.
The URLs in package.json (lines 57–63) currently return HTTP 404. Replace them with the correct GitHub repository and issue-tracker links before publishing.
🤖 Prompt for AI Agents
In package.json around lines 57 to 63, the homepage/repository/bugs URLs are
invalid and must be updated before release; replace "homepage",
"repository.url", and "bugs" with the correct GitHub repository URL and the
repository's issues URL (e.g., the canonical https://github.com/<org>/<repo> and
https://github.com/<org>/<repo>/issues), ensure "repository" remains an object
with type and url, and verify there are no trailing commas or malformed JSON
after the change.
package.json
Outdated
"@apidevtools/swagger-parser": "^10.1.0", | ||
"@types/bun": "1.1.14", | ||
"@scalar/types": "^0.0.12", | ||
"elysia": "1.3.0-exp.71", | ||
"eslint": "9.6.0", | ||
"tsup": "^8.1.0", | ||
"typescript": "^5.5.3" | ||
}, | ||
"dependencies": { | ||
"@scalar/themes": "^0.9.52", | ||
"@scalar/types": "^0.0.12", | ||
"openapi-types": "^12.1.3", | ||
"pathe": "^1.1.2" | ||
"openapi-types": "^12.1.3" | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Verification agent
🧩 Analysis chain
Move @scalar/types to devDependencies only if not used at runtime.
If imported from runtime code, keep under dependencies to avoid prod install failures.
🏁 Script executed:
#!/bin/bash
rg -nP "from\s+['\"]@scalar/types['\"]|require\(['\"]@scalar/types['\"]\)" -g 'src/**' -S
Length of output: 331
Move @scalar/types to devDependencies
Since it’s only ever imported via import type
(all uses are erased at build time), it isn’t needed at runtime and can be safely relocated from dependencies to devDependencies.
🤖 Prompt for AI Agents
package.json around lines 75 to 86: @scalar/types is listed under "dependencies"
but only used via import type so it should be moved to "devDependencies"; edit
package.json to remove the "@scalar/types" entry from the dependencies block and
add it to the devDependencies block (same version "^0.0.12"), then run your
package manager (npm/yarn/pnpm install) to update node_modules and the lockfile,
and verify builds/tests to ensure no runtime dependency breakage.
|
||
const app = new Elysia({ name: '@elysiajs/swagger' }) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fix broken spec URL: UI currently fetches /openapi/openapi/json (relative path).
Don't strip the leading slash. Use an absolute URL for both Swagger UI and Scalar.
-const relativePath = specPath.startsWith('/') ? specPath.slice(1) : specPath
+const specURL = specPath.startsWith('/') ? specPath : `/${specPath}`
...
- {
- url: relativePath,
+ {
+ url: specURL,
dom_id: '#swagger-ui',
...swaggerOptions
},
...
- {
- spec: {
- url: relativePath,
+ {
+ spec: {
+ url: specURL,
...scalarConfig.spec
},
Also applies to: 66-76, 83-91
🤖 Prompt for AI Agents
In src/index.ts around lines 49-50 (and also update similarly at 66-76 and
83-91), the code strips the leading slash from specPath which produces a
relative URL like /openapi/openapi/json => openapi/openapi/json; change logic to
keep the leading slash and construct an absolute URL for Swagger UI and Scalar
(e.g., ensure spec URL begins with '/' or a full origin + path) so both
integrations fetch the correct absolute path; update all three locations to stop
slicing off the leading '/' and build/assign the absolute spec URL consistently.
let totalRoutes = 0 | ||
let cachedSchema: OpenAPIV3.Document | undefined | ||
|
||
const page = new Response( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Stale schema cache: route-count check uses plugin-local routes, not global routes.
Compare against global routes to avoid returning an out-of-date schema.
- if (totalRoutes === app.routes.length) return cachedSchema
-
- totalRoutes = app.routes.length
+ // Prefer global route count; fallback to local if unavailable
+ // @ts-ignore private API
+ const currentRouteCount =
+ // @ts-ignore
+ typeof (app as any).getGlobalRoutes === 'function'
+ // @ts-ignore
+ ? (app as any).getGlobalRoutes().length
+ : app.routes.length
+ if (totalRoutes === currentRouteCount) return cachedSchema
+ totalRoutes = currentRouteCount
Also applies to: 109-114
🤖 Prompt for AI Agents
In src/index.ts around lines 51-53 (and similarly 109-114), the cachedSchema
invalidation uses a plugin-local totalRoutes counter which can be stale; change
the check to compare against the application's global route count (or a single
shared/global routes array/length) when deciding to reuse cachedSchema. Update
the code to read the actual global routes length (or derive it from the
application's router/registry) instead of the plugin-local totalRoutes, and
invalidate/update cachedSchema whenever the global route count differs.
src/openapi.ts
Outdated
const { | ||
methods: excludeMethods = ['OPTIONS'], | ||
staticFile: excludeStaticFile = true, | ||
tags: excludeTags | ||
} = exclude ?? {} | ||
|
||
const excludePaths = Array.isArray(exclude?.paths) | ||
? exclude.paths | ||
: typeof exclude?.paths !== 'undefined' | ||
? [exclude.paths] | ||
: [] | ||
|
||
const paths: OpenAPIV3.PathsObject = {} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Fix exclude.methods case and add RegExp support for exclude.paths
- methods: you compare lowercase route methods to an uppercase default ['OPTIONS'], so exclusions won’t trigger.
- paths: RegExp values in exclude.paths are not honored (using includes).
Apply:
const {
- methods: excludeMethods = ['OPTIONS'],
+ methods: excludeMethods = ['OPTIONS'],
staticFile: excludeStaticFile = true,
tags: excludeTags
} = exclude ?? {}
-const excludePaths = Array.isArray(exclude?.paths)
- ? exclude.paths
- : typeof exclude?.paths !== 'undefined'
- ? [exclude.paths]
- : []
+const excludePaths = Array.isArray(exclude?.paths)
+ ? exclude.paths
+ : typeof exclude?.paths !== 'undefined'
+ ? [exclude.paths]
+ : []
+const excludeMethodsNormalized = excludeMethods.map((m) => m.toLowerCase())
+const shouldExcludePath = (p: string) =>
+ excludePaths.some((x) => (typeof x === 'string' ? x === p : x.test(p)))
...
- if (excludePaths.includes(route.path)) continue
- if (excludeMethods.includes(method)) continue
+ if (shouldExcludePath(route.path)) continue
+ if (excludeMethodsNormalized.includes(method)) continue
Also applies to: 84-86
🤖 Prompt for AI Agents
In src/openapi.ts around lines 62 to 74 (also apply same fix at lines 84-86),
the exclude.methods default is uppercase ['OPTIONS'] and the code compares it to
lowercase route methods so exclusions never match; change to normalize both
sides to the same case (preferably store default as ['options'] or call
.toLowerCase() on exclude methods and route.method before comparing). For
exclude.paths, support RegExp entries by treating exclude.paths as an array of
strings or RegExps and when checking a route path use Array.prototype.some to
return true if any entry is a RegExp that tests the path or a string that equals
the path (instead of using includes), ensuring both single value and array forms
are handled.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 5
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
src/scalar/index.ts (1)
132-140
: Escape injected text and fix Open Graph meta attributeAvoid rendering “undefined” and prevent accidental HTML injection. Also OG tags use
property
, notname
.- <title>${info.title}</title> + <title>${escapeHtml(info.title)}</title> @@ - content="${info.description}" + content="${escapeHtml(info.description ?? '')}" @@ - name="og:description" - content="${info.description}" + property="og:description" + content="${escapeHtml(info.description ?? '')}"Add this helper (outside the shown range, near the top of the file):
const escapeHtml = (s: string) => s.replace(/[&<>"']/g, (c) => ({ '&':'&','<':'<','>':'>','"':'"', "'":''' }[c]!))src/swagger/index.ts (1)
103-111
: Escape dynamic HTML to prevent injection
info.title
andinfo.description
are interpolated into HTML/attributes without escaping.- <title>${info.title}</title> + <title>${escapeHtml(info.title)}</title> @@ - content="${info.description}" + content="${escapeHtml(info.description ?? '')}" @@ - content="${info.description}" + content="${escapeHtml(info.description ?? '')}"Add this helper anywhere above:
function escapeHtml(s?: string) { const str = String(s ?? '') const map: Record<string, string> = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' } return str.replace(/[&<>"']/g, (c) => map[c]) }
♻️ Duplicate comments (6)
package.json (3)
20-24
: New OpenAPI export path added successfully.The new
./openapi
subpath export provides proper access to the OpenAPI functionality with correct TypeScript definitions and build outputs.
57-62
: Repository URLs need verification before release.The URLs point to
elysiajs/elysia-openapi
but need verification that this repository exists and is properly configured.
77-77
: Move @scalar/types to devDependencies for build optimization.Since
@scalar/types
is only used for TypeScript type information (viaimport type
), it should be indevDependencies
rather thandependencies
to reduce production install size.src/index.ts (1)
40-41
: Duplicate comment - broken spec URL issue persists.The relative path construction still creates incorrect URLs by stripping the leading slash, resulting in relative URLs like
openapi/json
instead of absolute paths like/openapi/json
.src/types.ts (2)
7-11
: Duplicate comment - Path generic default mismatch.The
Path
generic still defaults to'/swagger'
instead of'/openapi'
, creating a mismatch with the runtime default in src/index.ts wherepath = '/openapi'
.
34-34
: Duplicate comment - Use Provider generic for type narrowing.The provider field should use the
Provider
generic instead of the concreteOpenAPIProvider
type to enable proper type narrowing when callers specify a provider.
🧹 Nitpick comments (8)
test/node/cjs/index.js (1)
3-6
: Also assertwithHeaders
to catch partial export regressionsExpand the check to ensure both primary exports are present.
Apply this diff:
-const { openapi } = require('@elysiajs/openapi') +const { openapi, withHeaders } = require('@elysiajs/openapi') -if (typeof openapi !== 'function') throw new Error('❌ CommonJS Node.js failed') +if (typeof openapi !== 'function' || typeof withHeaders !== 'function') + throw new Error('❌ CommonJS Node.js failed')src/scalar/index.ts (2)
3-3
: Use a type-only import to avoid emitting runtime code
ElysiaOpenAPIConfig
is types-only; import it withimport type
to prevent an unnecessary JS import.-import { ElysiaOpenAPIConfig } from '../types' +import type { ElysiaOpenAPIConfig } from '../types'
141-145
: Minor: move charset meta first for best practicePlace
<meta charset="utf-8" />
before other tags to ensure correct parsing.example/index.ts (1)
52-69
: Align declared 204 with runtime behaviorThe handler currently returns a 200. Consider showing how to emit 204 with the custom header to match the schema.
Example:
.get('/', ({ set }) => { set.status = 204 set.headers['X-Custom-Header'] = 'Elysia' }, { response: { /* keep 200/204 schemas as-is */ }})src/swagger/index.ts (4)
71-77
: Pin default Swagger UI version; align with docsTypes say default is 4.18.2, but here it’s “latest”. Pinning avoids CDN drift and surprises; you can still override via config.
- const { - version = 'latest', - theme = `https://unpkg.com/swagger-ui-dist@${version ?? 'latest'}/swagger-ui.css`, + const { + version = '4.18.2', + theme = `https://unpkg.com/swagger-ui-dist@${version}/swagger-ui.css`, cdn = `https://unpkg.com/swagger-ui-dist@${version}/swagger-ui-bundle.js`, autoDarkMode = true, ...rest } = configIf the project intends to move to “latest”, update the JSDoc in
src/types.ts
to match. Do you want me to submit that follow-up?
113-130
: Fix invalid hex color in dark-mode CSS
#faf9a
is a 5-digit hex and gets ignored.- color: #faf9a; + color: #faf9fa;
140-140
: Set an explicit crossorigin modeFor broader browser compatibility and clearer intent.
- <script src="${cdn}" crossorigin></script> + <script src="${cdn}" crossorigin="anonymous"></script>Optional: consider adding SRI
integrity
when using the default CDN.
79-87
: Minor: keep replacer only where neededYou already exclude function-valued options; no need to stringify/parse the whole object earlier. The updated diff above scopes sanitization to
spec
and the final UI options payload, avoiding unnecessary parse/serialize work.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
⛔ Files ignored due to path filters (1)
bun.lock
is excluded by!**/*.lock
📒 Files selected for processing (13)
CHANGELOG.md
(1 hunks)build.ts
(1 hunks)example/index.ts
(2 hunks)package.json
(4 hunks)src/index.ts
(2 hunks)src/openapi.ts
(1 hunks)src/scalar/index.ts
(2 hunks)src/swagger/index.ts
(2 hunks)src/types.ts
(4 hunks)test/node/cjs/index.js
(1 hunks)test/node/cjs/package.json
(1 hunks)test/node/esm/index.js
(1 hunks)test/node/esm/package.json
(1 hunks)
✅ Files skipped from review due to trivial changes (1)
- build.ts
🚧 Files skipped from review as they are similar to previous changes (1)
- src/openapi.ts
🧰 Additional context used
🧬 Code graph analysis (2)
src/scalar/index.ts (1)
src/types.ts (1)
ElysiaOpenAPIConfig
(7-149)
src/swagger/index.ts (2)
src/types.ts (1)
ElysiaOpenAPIConfig
(7-149)src/swagger/types.ts (1)
SwaggerUIOptions
(6-266)
🪛 LanguageTool
CHANGELOG.md
[grammar] ~1-~1: There might be a mistake here.
Context: # 1.3.2 - 22 Aug 2025 Feature: - add withHeader
for adding c...
(QB_NEW_EN)
[grammar] ~14-~14: There might be a mistake here.
Context: ...rty of excludes
# 1.3.1 - 28 Jun 2025 Bug fix: - Using relative path for specP...
(QB_NEW_EN)
🪛 markdownlint-cli2 (0.17.2)
CHANGELOG.md
12-12: Hard tabs
Column: 76
(MD010, no-hard-tabs)
🔇 Additional comments (25)
test/node/esm/package.json (2)
4-4
: LGTM! Dependency update aligns with the OpenAPI refactoring.The change from
@elysiajs/swagger
to@elysiajs/openapi
is consistent with the broader package rename across the codebase.
6-6
: File formatting improved with trailing newline.Good addition of the trailing newline for consistent file formatting.
package.json (4)
2-4
: Package identity successfully updated to OpenAPI.The package name change from
@elysiajs/swagger
to@elysiajs/openapi
and updated description properly reflects the transition to OpenAPI documentation focus.
35-49
: Swagger compatibility exports and types restoration look good.The addition of
./swagger
and./swagger/types
exports maintains backward compatibility, while the restoration of./types
export ensures existing type consumers continue to work.
53-56
: Keywords appropriately updated for OpenAPI focus.Adding "openapi" to the keywords while retaining "swagger" and "scalar" ensures discoverability across the ecosystem.
84-84
: Dependency cleanup aligns with OpenAPI focus.The updated
openapi-types
version and removal of unused dependencies like@scalar/themes
andpathe
streamlines the dependency list.test/node/cjs/package.json (2)
4-4
: LGTM! CommonJS test dependency properly updated.The dependency change from
@elysiajs/swagger
to@elysiajs/openapi
ensures the CommonJS test environment uses the new package correctly.
6-6
: File formatting improved with trailing newline.Good addition for consistent file formatting across the codebase.
test/node/esm/index.js (3)
1-1
: Bun guard appropriately condensed to single line.The guard logic remains functionally identical while improving readability.
3-3
: Import successfully updated to OpenAPI.The import change from
swagger
toopenapi
from the new package name correctly reflects the API surface changes.
5-5
: Runtime check properly updated for OpenAPI function.The validation now correctly checks for the
openapi
function instead ofswagger
, ensuring the ESM test validates the right export.test/node/cjs/index.js (1)
3-5
: Rename to OpenAPI looks goodImporting and validating the
openapi
export is correct for the CJS test.example/index.ts (2)
58-66
: Nice use of withHeaders for a typed 204 responseDemonstrates header typing clearly.
76-77
: Double-check parse options spellingVerify that
'formdata'
is the correct token for Elysia’s parser (vs'formData'
or similar), to avoid silent no-parse.Run a quick grep against Elysia’s type defs or docs to confirm accepted values.
src/index.ts (6)
6-6
: Import addition aligns with new architecture.The import of
toOpenAPISchema
from the new openapi module correctly supports the architectural shift from manual route scanning to the centralized OpenAPI generation approach.
17-30
: Generic signature properly structured.The new generic parameters with proper defaults provide good type safety and flexibility. The function signature follows TypeScript best practices.
42-44
: Caching mechanism properly implemented.The route-based cache invalidation using
totalRoutes
andcachedSchema
is a good optimization to avoid unnecessary OpenAPI schema regeneration.
45-79
: Provider-driven route registration is well-designed.The conditional route registration based on the provider parameter is elegant, allowing users to disable UI routes entirely by setting provider to null.
87-90
: Centralized schema generation improves maintainability.The shift to using
toOpenAPISchema
instead of manual path building is a significant improvement that centralizes OpenAPI generation logic.
129-132
: Public API exports are appropriate.The re-exports of
toOpenAPISchema
,withHeaders
, and the type export provide a clean public API surface.src/types.ts (4)
5-5
: OpenAPIProvider type is well-defined.The union type including
null
is appropriate for allowing complete disabling of UI routes.
40-61
: Scalar configuration structure is well-organized.The nested structure with version and CDN overrides provides good flexibility for customizing Scalar deployments.
79-125
: Swagger UI configuration comprehensive and well-structured.The configuration options properly exclude function-based options that can't be serialized, and the additional theme, version, autoDarkMode, and CDN options provide good customization capabilities.
127-148
: Exclusion configuration is comprehensive.The unified exclusion structure covering paths, methods, tags, and static files provides good flexibility for customizing what gets included in the OpenAPI documentation.
src/swagger/index.ts (1)
19-28
: Scope the date-key check to properties only (contextual note)
isDateTimeProperty
relies on the key name; after the recursion fix, it runs only for schema properties, which matches intent. No change needed—just flagging to ensure future edits don’t call it on non-property keys.
# 1.3.2 - 22 Aug 2025 | ||
Feature: | ||
- add `withHeader` for adding custom headers to response schema | ||
- spread all possible path for optional params | ||
- provider can be `null` to disable provider | ||
- export `toOpenAPI` to generate spec programmatically | ||
|
||
Breaking change: | ||
- rename `@elysiajs/swagger` to `@elysiajs/openapi` | ||
- map all `swagger`, and `scalar` prefix to respective `swagger` and `scalar` properties | ||
- rename `swaggerConfig`, and `scalarConfig` to `swagger` and `scalar` respectively | ||
- map `excludePaths`, `excludeMethods`, `excludeTags`, `excludeStaticFiles` to property of `excludes` | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Document the breaking changes clearly but fix formatting issues.
The changelog properly documents the major refactoring from Swagger to OpenAPI, but there are formatting issues that need to be addressed.
Apply this diff to fix the formatting issues:
-# 1.3.2 - 22 Aug 2025
-Feature:
+# 1.3.2 - 22 Aug 2025
+Features:
- add `withHeader` for adding custom headers to response schema
- spread all possible path for optional params
- provider can be `null` to disable provider
- export `toOpenAPI` to generate spec programmatically
-Breaking change:
+Breaking changes:
- rename `@elysiajs/swagger` to `@elysiajs/openapi`
- map all `swagger`, and `scalar` prefix to respective `swagger` and `scalar` properties
- rename `swaggerConfig`, and `scalarConfig` to `swagger` and `scalar` respectively
-- map `excludePaths`, `excludeMethods`, `excludeTags`, `excludeStaticFiles` to property of `excludes`
+- map `excludePaths`, `excludeMethods`, `excludeTags`, `excludeStaticFiles` to property of `excludes`
-# 1.3.1 - 28 Jun 2025
+# 1.3.1 - 28 Jun 2025
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
# 1.3.2 - 22 Aug 2025 | |
Feature: | |
- add `withHeader` for adding custom headers to response schema | |
- spread all possible path for optional params | |
- provider can be `null` to disable provider | |
- export `toOpenAPI` to generate spec programmatically | |
Breaking change: | |
- rename `@elysiajs/swagger` to `@elysiajs/openapi` | |
- map all `swagger`, and `scalar` prefix to respective `swagger` and `scalar` properties | |
- rename `swaggerConfig`, and `scalarConfig` to `swagger` and `scalar` respectively | |
- map `excludePaths`, `excludeMethods`, `excludeTags`, `excludeStaticFiles` to property of `excludes` | |
# 1.3.2 - 22 Aug 2025 | |
Features: | |
- add `withHeader` for adding custom headers to response schema | |
- spread all possible path for optional params | |
- provider can be `null` to disable provider | |
- export `toOpenAPI` to generate spec programmatically | |
Breaking changes: | |
- rename `@elysiajs/swagger` to `@elysiajs/openapi` | |
- map all `swagger`, and `scalar` prefix to respective `swagger` and `scalar` properties | |
- rename `swaggerConfig`, and `scalarConfig` to `swagger` and `scalar` respectively | |
- map `excludePaths`, `excludeMethods`, `excludeTags`, `excludeStaticFiles` to property of `excludes` | |
# 1.3.1 - 28 Jun 2025 |
🧰 Tools
🪛 LanguageTool
[grammar] ~1-~1: There might be a mistake here.
Context: # 1.3.2 - 22 Aug 2025 Feature: - add withHeader
for adding c...
(QB_NEW_EN)
🪛 markdownlint-cli2 (0.17.2)
12-12: Hard tabs
Column: 76
(MD010, no-hard-tabs)
🤖 Prompt for AI Agents
In CHANGELOG.md around lines 1 to 13, tidy the formatting of the release notes:
ensure the top-level heading is "## 1.3.2 - 22 Aug 2025" (use a consistent
markdown header level), add blank lines between the header and sections, change
"Feature:" and "Breaking change:" to bold section headings like "### Features"
and "### Breaking changes", normalize bullet items to start with "- " and keep
code identifiers wrapped in backticks (e.g., `withHeader`, `toOpenAPI`), rename
"Breaking change:" to plural and make each mapping line consistent (e.g., "-
rename `@elysiajs/swagger` to `@elysiajs/openapi`"), fix the tab/spacing issue
on the last mapping line so it reads "- map `excludePaths`, `excludeMethods`,
`excludeTags`, `excludeStaticFiles` to properties of `excludes`", and ensure
consistent capitalization and punctuation across bullets.
id="api-reference" | ||
data-url="${config.spec?.url}" | ||
data-configuration='${JSON.stringify(config)}' | ||
data-url="${config.url}" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Restore compatibility: fall back to spec.url when config.url is absent
ApiReferenceConfiguration
commonly uses spec.url
. Using only config.url
risks an empty data-url and a blank UI.
- data-url="${config.url}"
+ data-url="${(config as any).url ?? (config as any).spec?.url ?? ''}"
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
data-url="${config.url}" | |
data-url="${(config as any).url ?? (config as any).spec?.url ?? ''}" |
🤖 Prompt for AI Agents
In src/scalar/index.ts around line 157, the template sets data-url using only
config.url which can be empty; update it to fall back to spec.url when
config.url is absent (e.g. use config.url || spec.url or equivalent) so data-url
is never blank, and ensure the code accesses spec safely (handle undefined spec)
and prefer config.url when present otherwise use spec.url.
? cdn | ||
: `https://cdn.jsdelivr.net/npm/@scalar/api-reference@${version}/dist/browser/standalone.min.js` | ||
}" crossorigin></script> | ||
<script src="${config.cdn}" crossorigin></script> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Critical: provide a default CDN URL (and honor version) when cdn is not set
scalar.cdn
is optional per types. Without a fallback the UI will not load.
- <script src="${config.cdn}" crossorigin></script>
+ <script
+ src="${
+ config.cdn ||
+ `https://cdn.jsdelivr.net/npm/@scalar/api-reference@${config.version ?? 'latest'}/dist/browser/standalone.js`
+ }"
+ crossorigin="anonymous"
+ defer
+ ></script>
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
<script src="${config.cdn}" crossorigin></script> | |
<script | |
src="${ | |
config.cdn || | |
`https://cdn.jsdelivr.net/npm/@scalar/api-reference@${config.version ?? 'latest'}/dist/browser/standalone.js` | |
}" | |
crossorigin="anonymous" | |
defer | |
></script> |
🤖 Prompt for AI Agents
In src/scalar/index.ts around line 160, the template uses config.cdn directly
which breaks the UI when scalar.cdn is undefined; update the code to compute a
fallback CDN URL that honors the configured scalar version (e.g. use config.cdn
if present, otherwise build a default URL that includes config.version pointing
to the scalar bundle on a public CDN), then use that resolved cdn value in the
script tag and keep the crossorigin attribute unchanged.
function isSchemaObject( | ||
schema: SchemaObject | ||
): schema is OpenAPIV3.SchemaObject { | ||
return 'type' in schema || 'properties' in schema || 'items' in schema | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Date transform never reaches schema.properties; type guard is unreliable
isSchemaObject
checks'type' in schema || 'properties' in schema || 'items' in schema'
. This can incorrectly return true for aproperties
dictionary if it contains a property namedtype
.transformDateProperties
iterates top-level keys of a schema object and doesn’t traverseproperties
,items
,allOf
/oneOf
/anyOf
,additionalProperties
, etc. As a result,createdAt
/updatedAt
fields insideproperties
won’t be transformed.
Apply:
-function isSchemaObject(
- schema: SchemaObject
-): schema is OpenAPIV3.SchemaObject {
- return 'type' in schema || 'properties' in schema || 'items' in schema
-}
+function isSchemaObject(schema: SchemaObject): schema is OpenAPIV3.SchemaObject {
+ // ReferenceObject always has $ref; SchemaObject never does
+ return !('$ref' in (schema as OpenAPIV3.ReferenceObject))
+}
@@
-export function transformDateProperties(schema: SchemaObject): SchemaObject {
- if (
- !isSchemaObject(schema) ||
- typeof schema !== 'object' ||
- schema === null
- )
- return schema
-
- const newSchema: OpenAPIV3.SchemaObject = { ...schema }
-
- Object.entries(newSchema).forEach(([key, value]) => {
- if (isSchemaObject(value)) {
- if (isDateTimeProperty(key, value)) {
- const dateTimeFormat = value.anyOf?.find(
- (item): item is OpenAPIV3.SchemaObject =>
- isSchemaObject(item) && item.format === 'date-time'
- )
-
- if (dateTimeFormat) {
- const dateTimeSchema: DateTimeSchema = {
- type: 'string',
- format: 'date-time',
- default: dateTimeFormat.default
- }
- ;(newSchema as Record<string, SchemaObject>)[key] =
- dateTimeSchema
- }
- } else {
- ;(newSchema as Record<string, SchemaObject>)[key] =
- transformDateProperties(value)
- }
- }
- })
-
- return newSchema
-}
+export function transformDateProperties(schema: SchemaObject): SchemaObject {
+ if (!isSchemaObject(schema)) return schema
+ const out: OpenAPIV3.SchemaObject = { ...schema }
+
+ // properties
+ if (out.properties && typeof out.properties === 'object') {
+ out.properties = Object.fromEntries(
+ Object.entries(out.properties).map(([prop, propSchema]) => {
+ if (isSchemaObject(propSchema) && isDateTimeProperty(prop, propSchema)) {
+ const dateTime = propSchema.anyOf?.find(
+ (i): i is OpenAPIV3.SchemaObject =>
+ isSchemaObject(i) && i.format === 'date-time'
+ )
+ if (dateTime) {
+ const dateTimeSchema: DateTimeSchema = {
+ type: 'string',
+ format: 'date-time',
+ default: dateTime.default
+ }
+ return [prop, dateTimeSchema]
+ }
+ }
+ return [prop, transformDateProperties(propSchema)]
+ })
+ )
+ }
+
+ // arrays and composites
+ if (out.items) out.items = transformDateProperties(out.items as SchemaObject)
+ if (out.additionalProperties && typeof out.additionalProperties === 'object')
+ out.additionalProperties = transformDateProperties(
+ out.additionalProperties as SchemaObject
+ )
+ if (out.allOf) out.allOf = out.allOf.map((s) => transformDateProperties(s))
+ if (out.oneOf) out.oneOf = out.oneOf.map((s) => transformDateProperties(s))
+ if (out.anyOf) out.anyOf = out.anyOf.map((s) => transformDateProperties(s))
+ if (out.not) out.not = transformDateProperties(out.not as SchemaObject)
+
+ return out
+}
Also applies to: 30-65
🤖 Prompt for AI Agents
In src/swagger/index.ts around lines 13-17 (and extend to 30-65), the type guard
and date transformation are both incomplete: replace isSchemaObject with a
reliable discriminator that treats ReferenceObject by presence of $ref (e.g.
return typeof schema === 'object' && schema !== null && !('$ref' in schema));
then rewrite transformDateProperties to recursively traverse schema nodes
(handle schema.properties, schema.items, schema.additionalProperties,
schema.allOf/oneOf/anyOf arrays, schema.not, and nested items) and apply the
date transformation at property-level when schema.format === 'date-time' or
property names match createdAt/updatedAt; ensure you recurse into each child
schema and handle arrays and boolean/Record forms for additionalProperties so
date formats inside nested objects are transformed.
// remove function in rest | ||
const stringifiedOptions = JSON.stringify( | ||
{ | ||
dom_id: '#swagger-ui', | ||
...rest | ||
}, | ||
(_, value) => (typeof value === 'function' ? undefined : value) | ||
) | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Spec transformation is applied to the wrong object and then discarded
You're parsing UI options as an OpenAPI document and transforming options.components.schemas
(which won't exist unless nested in spec
). Also, you never use the transformed object; SwaggerUIBundle
is called with the original stringifiedOptions
. Transform spec
and pass the transformed options to the bundle.
Apply:
- // remove function in rest
- const stringifiedOptions = JSON.stringify(
- {
- dom_id: '#swagger-ui',
- ...rest
- },
- (_, value) => (typeof value === 'function' ? undefined : value)
- )
-
- const options: OpenAPIV3.Document = JSON.parse(stringifiedOptions)
-
- if (options.components && options.components.schemas)
- options.components.schemas = Object.fromEntries(
- Object.entries(options.components.schemas).map(([key, schema]) => [
- key,
- transformDateProperties(schema)
- ])
- )
+ // sanitize and transform spec, then build UI options
+ const { spec, ...uiOptions } = rest
+ const sanitizedSpec =
+ spec && typeof spec === 'object'
+ ? JSON.parse(
+ JSON.stringify(spec, (_, v) =>
+ typeof v === 'function' ? undefined : v
+ )
+ )
+ : undefined
+
+ if (sanitizedSpec?.components?.schemas)
+ sanitizedSpec.components.schemas = Object.fromEntries(
+ Object.entries(sanitizedSpec.components.schemas).map(([key, schema]) => [
+ key,
+ transformDateProperties(schema)
+ ])
+ )
+
+ const stringifiedUIOptions = JSON.stringify(
+ {
+ dom_id: '#swagger-ui',
+ ...uiOptions,
+ ...(sanitizedSpec ? { spec: sanitizedSpec } : {})
+ },
+ (_, value) => (typeof value === 'function' ? undefined : value)
+ )
@@
- window.ui = SwaggerUIBundle(${stringifiedOptions});
+ window.ui = SwaggerUIBundle(${stringifiedUIOptions});
Also applies to: 88-96, 143-144
🤖 Prompt for AI Agents
In src/swagger/index.ts around lines 79-87 (and similarly update 88-96 and
143-144): the code currently parses UI options as an OpenAPI doc, transforms
options.components.schemas (which won't exist when spec is nested) and then
discards the transformed result by still passing the original stringified
options to SwaggerUIBundle. Fix by locating the OpenAPI spec (options.spec if
present, otherwise the root object), apply the schema transformation to
spec.components.schemas (or create components if missing), assign the
transformed spec back into the options object, then JSON.stringify that updated
options and pass the updated stringified options to SwaggerUIBundle; replicate
the same correction in the other affected blocks (lines 88-96 and 143-144).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 2
♻️ Duplicate comments (5)
src/types.ts (1)
26-30
: Align default Path generic with OpenAPI default.The generic still defaults to '/swagger' while docs and runtime default to '/openapi'. This causes type-level inconsistencies for consumers.
Apply:
-export interface ElysiaOpenAPIConfig< - Enabled extends boolean = true, - Path extends string = '/swagger', - Provider extends OpenAPIProvider = 'scalar' -> { +export interface ElysiaOpenAPIConfig< + Enabled extends boolean = true, + Path extends string = '/openapi', + Provider extends OpenAPIProvider = 'scalar' +> {src/openapi.ts (2)
66-77
: Normalize exclude.methods and support RegExp in exclude.paths.Uppercase defaults never match lowercased route methods; exclude.paths should accept RegExp.
Apply:
const { - methods: excludeMethods = ['OPTIONS'], + methods: excludeMethods = ['OPTIONS'], staticFile: excludeStaticFile = true, tags: excludeTags } = exclude ?? {} - const excludePaths = Array.isArray(exclude?.paths) - ? exclude.paths - : typeof exclude?.paths !== 'undefined' - ? [exclude.paths] - : [] + const excludePaths = Array.isArray(exclude?.paths) + ? exclude.paths + : typeof exclude?.paths !== 'undefined' + ? [exclude.paths] + : [] + const excludeMethodsNormalized = excludeMethods.map((m) => m.toLowerCase()) + const shouldExcludePath = (p: string) => + excludePaths.some((x) => (typeof x === 'string' ? x === p : x.test(p))) @@ - if (excludePaths.includes(route.path)) continue - if (excludeMethods.includes(method)) continue + if (shouldExcludePath(route.path)) continue + if (excludeMethodsNormalized.includes(method)) continueAlso applies to: 96-101
328-360
: Ensure unique operationId per emitted HTTP method (including .all).Compute per-method opIds; current code reuses one opId across all variants.
Apply:
- for (let path of getPossiblePath(route.path)) { - const operationId = toOperationId(route.method, path) + for (let path of getPossiblePath(route.path)) { path = path.replace(/:([^/]+)/g, '{$1}') if (!paths[path]) paths[path] = {} const current = paths[path] as any if (method !== 'all') { current[method] = { ...operation, - operationId + operationId: toOperationId(method, path) } continue } // Handle 'ALL' method by assigning operation to all standard methods - for (const method of [ + for (const m of [ 'get', 'post', 'put', 'delete', 'patch', 'head', 'options', 'trace' ]) - current[method] = { + current[m] = { ...operation, - operationId + operationId: toOperationId(m, path) } }src/index.ts (2)
41-41
: Fix spec URL: don’t strip the leading slash (UI fetches wrong path).Using a relative path turns “/openapi/json” into “openapi/json”, which the browser resolves to “/openapi/openapi/json”. Keep it absolute.
-const relativePath = specPath.startsWith('/') ? specPath.slice(1) : specPath +const specURL = specPath.startsWith('/') ? specPath : `/${specPath}` @@ - url: relativePath, + url: specURL, @@ - url: relativePath, + url: specURL,Also applies to: 55-56, 62-63
84-87
: Schema cache can go stale: compare against global route count.app.routes is plugin-local; use global routes when available to avoid stale caches after other plugins add routes.
- if (totalRoutes === app.routes.length) return cachedSchema - - totalRoutes = app.routes.length + // Prefer global route count; fallback to local + // @ts-ignore private API + const currentRouteCount = + typeof (app as any).getGlobalRoutes === 'function' + // @ts-ignore + ? (app as any).getGlobalRoutes().length + : app.routes.length + if (totalRoutes === currentRouteCount) return cachedSchema + totalRoutes = currentRouteCount
🧹 Nitpick comments (9)
src/types.ts (4)
36-41
: Fix documentation JSDoc: not Swagger 2.0.This field is OpenAPI v3 (typed as OpenAPIV3.Document). Link and wording should reflect that.
Apply:
- /** - * Customize Swagger config, refers to Swagger 2.0 config - * - * @see https://swagger.io/specification/v2/ - */ + /** + * Customize OpenAPI document (OpenAPI v3) + * + * @see https://spec.openapis.org/oas/v3.0.3 + */
56-59
: Remove stray apostrophe in JSDoc.Apply:
- *' + *
72-79
: Fix typos and CDN name in Scalar CDN docs.“Lease” → “Leave”, “jsdeliver.net” → “jsDelivr”.
Apply:
- * Lease blank to use default jsdeliver.net CDN + * Leave blank to use the default jsDelivr CDN
125-133
: Update Swagger UI version default docs to match runtime.Runtime sets version 'latest' (see src/index.ts). Doc says 4.18.2.
Apply:
- * @default 4.18.2 + * @default 'latest'If you intend to pin a specific version, also update src/index.ts to keep them consistent.
example/gen.ts (1)
8-9
: Avoid self-referencing in fromTypes to reduce tooling brittleness.Point the generator at a separate app file (e.g., example/index.ts) to avoid circular edits impacting generation.
Apply:
- references: fromTypes('example/gen.ts') + references: fromTypes('example/index.ts')src/openapi.ts (1)
40-54
: Optional: de-duplicate paths emitted by getPossiblePath.Avoid emitting the same path multiple times for multi-optional segments.
Apply:
- return paths + return Array.from(new Set(paths))example/index.ts (1)
55-69
: Prefer reusing registered schema name to avoid duplication.Since you model()‘d 'schema', you can reference it directly.
Apply:
- 200: t.Object({ - test: t.Literal('hello') - }), + 200: 'schema',src/index.ts (1)
69-71
: Content-Type charset casing.Use utf-8 (with hyphen) for correctness.
- 'content-type': 'text/html; charset=utf8' + 'content-type': 'text/html; charset=utf-8'package.json (1)
1-4
: Consider adding a files whitelist to slim the published package.{ "name": "@elysiajs/openapi", "version": "1.3.2", "description": "Plugin for Elysia to auto-generate OpenAPI documentation", + "files": [ + "dist/", + "README.md", + "LICENSE", + "package.json" + ],
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
⛔ Files ignored due to path filters (2)
bun.lock
is excluded by!**/*.lock
src/gen/index.ts
is excluded by!**/gen/**
📒 Files selected for processing (6)
example/gen.ts
(1 hunks)example/index.ts
(2 hunks)package.json
(4 hunks)src/index.ts
(2 hunks)src/openapi.ts
(1 hunks)src/types.ts
(4 hunks)
🧰 Additional context used
🧬 Code graph analysis (5)
example/gen.ts (2)
src/index.ts (1)
openapi
(17-128)src/gen/index.ts (1)
fromTypes
(58-192)
src/openapi.ts (3)
example/gen.ts (1)
app
(5-22)example/index.ts (1)
app
(18-85)src/types.ts (3)
ElysiaOpenAPIConfig
(26-170)AdditionalReferences
(22-24)AdditionalReference
(10-20)
example/index.ts (3)
example/gen.ts (1)
app
(5-22)src/index.ts (2)
openapi
(17-128)withHeaders
(130-130)src/openapi.ts (1)
withHeaders
(374-377)
src/index.ts (4)
src/types.ts (2)
OpenAPIProvider
(6-6)ElysiaOpenAPIConfig
(26-170)src/swagger/index.ts (1)
SwaggerUIRender
(67-148)src/scalar/index.ts (1)
ScalarRender
(126-162)src/openapi.ts (1)
toOpenAPISchema
(61-372)
src/types.ts (1)
src/index.ts (1)
ElysiaOpenAPIConfig
(131-131)
🔇 Additional comments (6)
src/types.ts (1)
53-53
: Good: provider type uses the Provider generic.Enables narrowing when callers specify a provider.
src/openapi.ts (1)
66-71
: Unused excludeStaticFile — confirm intent and implement or remove.The flag is read but never applied. If intended, add a check to skip static-file routes; otherwise, drop the option to avoid confusion.
Would you like me to wire this to whatever marker Elysia exposes for static assets (e.g., a route kind/detail flag) and send a patch?
example/index.ts (2)
20-47
: Docs config looks consistent with OpenAPI switch.Provider, documentation, and securitySchemes updates look good.
76-82
: Nice: parse + named body + per-status responses.Good showcase for generator coverage.
package.json (2)
82-83
: Good: @scalar/types moved to devDependencies; runtime deps minimal.Also applies to: 89-91
62-68
: URLs resolve correctly – both the homepage and bugs links return HTTP 200.
"./gen": { | ||
"types": "./dist/gen/index.d.ts", | ||
"import": "./dist/gen/index.mjs", | ||
"require": "./dist/cjs/gen/index.js" | ||
}, | ||
"./utils": { | ||
"types": "./dist/utils.d.ts", | ||
"import": "./dist/utils.mjs", | ||
"require": "./dist/cjs/utils.js" | ||
"./openapi": { | ||
"types": "./dist/openapi.d.ts", | ||
"import": "./dist/openapi.mjs", | ||
"require": "./dist/cjs/openapi.js" | ||
}, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Verification agent
🧩 Analysis chain
Sanity-check built artifacts for new export subpaths.
Ensure these files exist after build to prevent “module not found” at runtime.
Also applies to: 40-54
🏁 Script executed:
#!/usr/bin/env bash
set -euo pipefail
# Run after building locally (npm run build)
for f in \
dist/openapi.mjs dist/openapi.d.ts dist/cjs/openapi.js \
dist/swagger/index.mjs dist/swagger/index.d.ts dist/cjs/swagger/index.js \
dist/types.mjs dist/types.d.ts dist/cjs/types.js \
dist/gen/index.mjs dist/gen/index.d.ts dist/cjs/gen/index.js; do
test -f "$f" && echo "OK: $f" || echo "MISSING: $f"
done
Length of output: 4165
Missing built artifacts for export subpaths
All expected files under dist/openapi
, dist/swagger
, dist/types
, and dist/gen
are not generated after npm run build
:
- openapi:
dist/openapi.{mjs,d.ts}
,dist/cjs/openapi.js
- swagger:
dist/swagger/index.{mjs,d.ts}
,dist/cjs/swagger/index.js
- types:
dist/types.{mjs,d.ts}
,dist/cjs/types.js
- gen:
dist/gen/index.{mjs,d.ts}
,dist/cjs/gen/index.js
Update the build configuration to emit these artifacts to match the export
map in package.json
.
🤖 Prompt for AI Agents
In package.json around lines 20–29 the export subpaths (./gen, ./openapi,
./swagger, ./types) expect built artifacts that are not produced by the current
build; update the build configuration (tsconfig and/or your bundler like
tsup/rollup/webpack) and npm scripts so the build emits ESM .mjs, CommonJS .js
under dist/cjs, and TypeScript declaration .d.ts files for each subpath (e.g.,
dist/openapi.mjs + dist/openapi.d.ts + dist/cjs/openapi.js;
dist/swagger/index.mjs + dist/swagger/index.d.ts + dist/cjs/swagger/index.js;
dist/types.mjs + dist/types.d.ts + dist/cjs/types.js; dist/gen/index.mjs +
dist/gen/index.d.ts + dist/cjs/gen/index.js). Ensure tsconfig.json has
"declaration": true and correct "outDir", or configure your bundler’s entry
points and outputs to produce both esm and cjs bundles and generate .d.ts (or
add a step to copy/rename outputs) so the files match the package.json export
map.
if ( | ||
typeof hooks.response === 'object' && | ||
!(hooks.response as TSchema).type && | ||
!(hooks.response as TSchema).$ref | ||
) { | ||
for (let [status, schema] of Object.entries(hooks.response)) { | ||
if (typeof schema === 'string') schema = toRef(schema) | ||
|
||
// Must exclude $ref from root options | ||
const { type, examples, $ref, ...options } = schema | ||
|
||
operation.responses[status] = { | ||
description: `Response for status ${status}`, | ||
...options, | ||
content: | ||
type === 'void' || | ||
type === 'null' || | ||
type === 'undefined' | ||
? schema | ||
: { | ||
'application/json': { | ||
schema | ||
} | ||
} | ||
} | ||
} | ||
} else { | ||
if (typeof hooks.response === 'string') | ||
hooks.response = toRef(hooks.response) | ||
|
||
// It's a single schema, default to 200 | ||
operation.responses['200'] = { | ||
description: 'Successful response', | ||
content: { | ||
'application/json': { | ||
schema: hooks.response | ||
} | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fix invalid ResponseObject when schema is void/null/undefined.
Setting content
to a schema object is not valid OAS; omit content
for empty bodies while preserving headers and other options.
Apply:
- for (let [status, schema] of Object.entries(hooks.response)) {
+ for (let [status, schema] of Object.entries(hooks.response)) {
if (typeof schema === 'string') schema = toRef(schema)
// Must exclude $ref from root options
const { type, examples, $ref, ...options } = schema
-
- operation.responses[status] = {
- description: `Response for status ${status}`,
- ...options,
- content:
- type === 'void' ||
- type === 'null' ||
- type === 'undefined'
- ? schema
- : {
- 'application/json': {
- schema
- }
- }
- }
+ const resp: OpenAPIV3.ResponseObject = {
+ description: `Response for status ${status}`,
+ ...options
+ }
+ if (
+ type !== 'void' &&
+ type !== 'null' &&
+ type !== 'undefined'
+ ) {
+ resp.content = {
+ 'application/json': { schema }
+ }
+ }
+ operation.responses[status] = resp
}
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
if ( | |
typeof hooks.response === 'object' && | |
!(hooks.response as TSchema).type && | |
!(hooks.response as TSchema).$ref | |
) { | |
for (let [status, schema] of Object.entries(hooks.response)) { | |
if (typeof schema === 'string') schema = toRef(schema) | |
// Must exclude $ref from root options | |
const { type, examples, $ref, ...options } = schema | |
operation.responses[status] = { | |
description: `Response for status ${status}`, | |
...options, | |
content: | |
type === 'void' || | |
type === 'null' || | |
type === 'undefined' | |
? schema | |
: { | |
'application/json': { | |
schema | |
} | |
} | |
} | |
} | |
} else { | |
if (typeof hooks.response === 'string') | |
hooks.response = toRef(hooks.response) | |
// It's a single schema, default to 200 | |
operation.responses['200'] = { | |
description: 'Successful response', | |
content: { | |
'application/json': { | |
schema: hooks.response | |
} | |
} | |
} | |
} | |
// … inside the `if (typeof hooks.response === 'object' && …)` branch: | |
for (let [status, schema] of Object.entries(hooks.response)) { | |
if (typeof schema === 'string') schema = toRef(schema) | |
// Must exclude $ref from root options | |
const { type, examples, $ref, ...options } = schema | |
// Build the response object, omitting `content` for empty bodies | |
const resp: OpenAPIV3.ResponseObject = { | |
description: `Response for status ${status}`, | |
...options | |
} | |
// Only add a JSON media type when there's a concrete schema | |
if ( | |
type !== 'void' && | |
type !== 'null' && | |
type !== 'undefined' | |
) { | |
resp.content = { | |
'application/json': { schema } | |
} | |
} | |
operation.responses[status] = resp | |
} | |
// … |
🤖 Prompt for AI Agents
In src/openapi.ts around lines 286 to 325, the response generation wrongly sets
content to a schema object when the response schema has type
'void'|'null'|'undefined' (invalid OAS). Change both branches so that for
empty-body types you omit the content property entirely but still include
description and other response options (headers, examples, $ref-excluded
options) for that status; for non-empty types keep the existing application/json
content wrapper. Ensure the single-schema (default 200) path applies the same
rule (omit content for void/null/undefined) while preserving other response
options.
LGTM 👍 |
It looks like this was an AI making this suggestion, but I'm actually running into a case now where I'd like to add Would be nice if there was a setting or hook of some sort to update that. Should I create a feature request? |
Summary by CodeRabbit
New Features
Breaking Changes
Bug Fixes