@opsydyn/elysia-spectral 0.2.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +47 -0
- package/README.md +637 -0
- package/dist/core/index.d.mts +2 -0
- package/dist/core/index.mjs +2 -0
- package/dist/core-Czin3kvK.mjs +1099 -0
- package/dist/index-BrFQCFDI.d.mts +172 -0
- package/dist/index.d.mts +36 -0
- package/dist/index.mjs +85 -0
- package/package.json +86 -0
- package/spectral.yaml +25 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [0.2.4](https://github.com/opsydyn/elysia-spectral/compare/v0.2.3...v0.2.4) (2026-04-14)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
* clarify bun and npm install commands in tutorial ([535ca1e](https://github.com/opsydyn/elysia-spectral/commit/535ca1e77568801dbdc85a7b4e733ed9eb4e0486))
|
|
9
|
+
|
|
10
|
+
## [0.2.3](https://github.com/opsydyn/elysia-spectral/compare/v0.2.2...v0.2.3) (2026-04-14)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Bug Fixes
|
|
14
|
+
|
|
15
|
+
* :wrench: remove stale biome suppressions and unused import ([e81e392](https://github.com/opsydyn/elysia-spectral/commit/e81e3925c66687678be5c9834bdb76880399753c))
|
|
16
|
+
|
|
17
|
+
## [0.2.2](https://github.com/opsydyn/elysia-spectral/compare/v0.2.1...v0.2.2) (2026-04-14)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
### Bug Fixes
|
|
21
|
+
|
|
22
|
+
* :wrench: migrate biome config to 2.4.11 and pin version ([327d089](https://github.com/opsydyn/elysia-spectral/commit/327d08969af3e394ef6ea992cdba9e5189129f39))
|
|
23
|
+
|
|
24
|
+
## [0.2.1](https://github.com/opsydyn/elysia-spectral/compare/v0.2.0...v0.2.1) (2026-04-14)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
### Bug Fixes
|
|
28
|
+
|
|
29
|
+
* :wrench: resolve all biome lint and format errors ([cb3ff8d](https://github.com/opsydyn/elysia-spectral/commit/cb3ff8dd5464b5a7036861c5550390dbf79f2dd5))
|
|
30
|
+
|
|
31
|
+
## [0.2.0](https://github.com/opsydyn/elysia-spectral/compare/v0.1.0...v0.2.0) (2026-04-14)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
### Features
|
|
35
|
+
|
|
36
|
+
* :memo: update readme ([721a832](https://github.com/opsydyn/elysia-spectral/commit/721a832aa5a2596cb7fd26b02a575be495cc316a))
|
|
37
|
+
* :sparkles: cli enhance ([aa97001](https://github.com/opsydyn/elysia-spectral/commit/aa970015e18af9b7a5aff8d5242f2fad787ef17d))
|
|
38
|
+
* :sparkles: enhance logging ([bfbc5bb](https://github.com/opsydyn/elysia-spectral/commit/bfbc5bbfd828b07d26c6b69fcf27676b58754b31))
|
|
39
|
+
* :sparkles: healthcheck pkg opt in ([41df9dd](https://github.com/opsydyn/elysia-spectral/commit/41df9ddd1ba8f1b2dbcd7b90bec6fdeef3ed3cd8))
|
|
40
|
+
* :sparkles: logging refactor ([4a0f98c](https://github.com/opsydyn/elysia-spectral/commit/4a0f98c09f1f3e6a71b5ba20ad11fdb35f5c42e4))
|
|
41
|
+
* :sparkles: plugin bootstrap ([a1bcdd4](https://github.com/opsydyn/elysia-spectral/commit/a1bcdd48f5a105735d97bbfacec8159ff58816a3))
|
|
42
|
+
* :sparkles: rename to @opsydyn/elysia-spectral and prep for npm publish ([87ffdda](https://github.com/opsydyn/elysia-spectral/commit/87ffdda2d1c844753ee50d8e3c1800d0cafdda99))
|
|
43
|
+
* :sparkles: rule set update ([efb85c6](https://github.com/opsydyn/elysia-spectral/commit/efb85c62edabb17a9852b43009d9fbc044345a9c))
|
|
44
|
+
|
|
45
|
+
## Changelog
|
|
46
|
+
|
|
47
|
+
All notable changes to this package will be documented in this file.
|
package/README.md
ADDED
|
@@ -0,0 +1,637 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="https://raw.githubusercontent.com/opsydyn/elysia-spectral/main/api-lint-repo-logo.png" alt="@opsydyn/elysia-spectral" width="480" />
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
# @opsydyn/elysia-spectral
|
|
6
|
+
|
|
7
|
+
Thin Elysia plugin that lints the OpenAPI document generated by `@elysiajs/openapi` with Spectral.
|
|
8
|
+
|
|
9
|
+
## What Is Spectral?
|
|
10
|
+
|
|
11
|
+
Spectral is an open-source linter and style guide engine for API descriptions. It was built with OpenAPI, AsyncAPI, and JSON Schema in mind, and is commonly used to enforce API style guides, catch weak or inconsistent contract definitions, and improve the usefulness of generated API descriptions.
|
|
12
|
+
|
|
13
|
+
For API teams, that matters because a spec can be technically valid while still being poor for:
|
|
14
|
+
|
|
15
|
+
- generated documentation
|
|
16
|
+
- client generation
|
|
17
|
+
- contract testing
|
|
18
|
+
- consistency across teams and services
|
|
19
|
+
- long-term API governance
|
|
20
|
+
|
|
21
|
+
`@opsydyn/elysia-spectral` uses Spectral to turn the OpenAPI document generated by `@elysiajs/openapi` into an automated quality gate for Elysia apps.
|
|
22
|
+
|
|
23
|
+
Official Spectral references:
|
|
24
|
+
|
|
25
|
+
- Stoplight overview: <https://stoplight.io/open-source/spectral>
|
|
26
|
+
- GitHub repository: <https://github.com/stoplightio/spectral>
|
|
27
|
+
|
|
28
|
+
This README is organized using the Diataxis documentation model:
|
|
29
|
+
|
|
30
|
+
- Tutorial: learn by building a working setup
|
|
31
|
+
- How-to guides: solve specific tasks
|
|
32
|
+
- Reference: look up exact behavior and API shape
|
|
33
|
+
- Explanation: understand the design choices
|
|
34
|
+
|
|
35
|
+
Current package scope:
|
|
36
|
+
|
|
37
|
+
- startup linting
|
|
38
|
+
- threshold-based failure
|
|
39
|
+
- repo-level and local rulesets
|
|
40
|
+
- YAML, JS, TS, and in-memory rulesets
|
|
41
|
+
- resolver pipeline for advanced ruleset loading
|
|
42
|
+
- console output
|
|
43
|
+
- JSON report output
|
|
44
|
+
- JUnit report output
|
|
45
|
+
- SARIF report output
|
|
46
|
+
- OpenAPI snapshot output
|
|
47
|
+
- reusable runtime for CI and tests
|
|
48
|
+
- opt-in healthcheck endpoint for cached and fresh runs
|
|
49
|
+
|
|
50
|
+
## Tutorial
|
|
51
|
+
|
|
52
|
+
### Add OpenAPI linting to an Elysia app
|
|
53
|
+
|
|
54
|
+
This tutorial takes a minimal Elysia app and adds startup OpenAPI linting with a repo-level ruleset and JSON artifacts.
|
|
55
|
+
|
|
56
|
+
1. Install the dependencies.
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
# bun
|
|
60
|
+
bun add elysia @elysiajs/openapi @opsydyn/elysia-spectral
|
|
61
|
+
|
|
62
|
+
# npm
|
|
63
|
+
npm install elysia @elysiajs/openapi @opsydyn/elysia-spectral
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
2. Create a repo-level `spectral.yaml`.
|
|
67
|
+
|
|
68
|
+
```yaml
|
|
69
|
+
extends:
|
|
70
|
+
- spectral:oas
|
|
71
|
+
|
|
72
|
+
rules:
|
|
73
|
+
operation-description:
|
|
74
|
+
severity: error
|
|
75
|
+
|
|
76
|
+
operation-tags:
|
|
77
|
+
severity: warn
|
|
78
|
+
|
|
79
|
+
elysia-operation-summary:
|
|
80
|
+
severity: error
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
3. Add `@elysiajs/openapi` and `spectralPlugin` to your app.
|
|
84
|
+
|
|
85
|
+
```ts
|
|
86
|
+
import { Elysia, t } from 'elysia'
|
|
87
|
+
import { openapi } from '@elysiajs/openapi'
|
|
88
|
+
import { spectralPlugin } from '@opsydyn/elysia-spectral'
|
|
89
|
+
|
|
90
|
+
new Elysia()
|
|
91
|
+
.use(
|
|
92
|
+
openapi({
|
|
93
|
+
documentation: {
|
|
94
|
+
info: {
|
|
95
|
+
title: 'Example API',
|
|
96
|
+
version: '1.0.0'
|
|
97
|
+
},
|
|
98
|
+
tags: [{ name: 'Users', description: 'User operations' }]
|
|
99
|
+
}
|
|
100
|
+
})
|
|
101
|
+
)
|
|
102
|
+
.use(
|
|
103
|
+
spectralPlugin({
|
|
104
|
+
failOn: 'error',
|
|
105
|
+
startup: {
|
|
106
|
+
mode: 'enforce'
|
|
107
|
+
},
|
|
108
|
+
output: {
|
|
109
|
+
console: true,
|
|
110
|
+
jsonReportPath: './artifacts/openapi-lint.json',
|
|
111
|
+
specSnapshotPath: true
|
|
112
|
+
}
|
|
113
|
+
})
|
|
114
|
+
)
|
|
115
|
+
.get('/users', () => [{ id: '1', name: 'Ada Lovelace' }], {
|
|
116
|
+
response: {
|
|
117
|
+
200: t.Array(
|
|
118
|
+
t.Object({
|
|
119
|
+
id: t.String(),
|
|
120
|
+
name: t.String()
|
|
121
|
+
})
|
|
122
|
+
)
|
|
123
|
+
},
|
|
124
|
+
detail: {
|
|
125
|
+
summary: 'List users',
|
|
126
|
+
description: 'Return all users.',
|
|
127
|
+
operationId: 'listUsers',
|
|
128
|
+
tags: ['Users']
|
|
129
|
+
}
|
|
130
|
+
})
|
|
131
|
+
.listen(3000)
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
4. Start the app.
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
bun run src/index.ts
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
5. Confirm the outcome.
|
|
141
|
+
|
|
142
|
+
- the app serves OpenAPI JSON at `/openapi/json`
|
|
143
|
+
- the plugin lints that generated document during startup
|
|
144
|
+
- a clean run prints Signale-style success and hype lines in the terminal
|
|
145
|
+
- `./artifacts/openapi-lint.json` contains the lint result
|
|
146
|
+
- `./<package-name>.open-api.json` contains the generated OpenAPI snapshot
|
|
147
|
+
|
|
148
|
+
If startup fails, the terminal output includes:
|
|
149
|
+
|
|
150
|
+
- the failing rule code
|
|
151
|
+
- the affected operation
|
|
152
|
+
- a fix hint when one is known
|
|
153
|
+
- a spec reference in `open-api.json#/json/pointer` form
|
|
154
|
+
|
|
155
|
+
## How-to Guides
|
|
156
|
+
|
|
157
|
+
### Use a repo-level ruleset
|
|
158
|
+
|
|
159
|
+
Use a ruleset file at the consuming app root:
|
|
160
|
+
|
|
161
|
+
```ts
|
|
162
|
+
spectralPlugin({
|
|
163
|
+
ruleset: './spectral.yaml'
|
|
164
|
+
})
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
Or let the plugin autodiscover a standard ruleset filename:
|
|
168
|
+
|
|
169
|
+
```ts
|
|
170
|
+
spectralPlugin()
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
Autodiscovery looks for these files in order:
|
|
174
|
+
|
|
175
|
+
- `spectral.yaml`
|
|
176
|
+
- `spectral.yml`
|
|
177
|
+
- `spectral.ts`
|
|
178
|
+
- `spectral.mts`
|
|
179
|
+
- `spectral.cts`
|
|
180
|
+
- `spectral.js`
|
|
181
|
+
- `spectral.mjs`
|
|
182
|
+
- `spectral.cjs`
|
|
183
|
+
- `spectral.config.yaml`
|
|
184
|
+
- `spectral.config.yml`
|
|
185
|
+
- `spectral.config.ts`
|
|
186
|
+
- `spectral.config.mts`
|
|
187
|
+
- `spectral.config.cts`
|
|
188
|
+
- `spectral.config.js`
|
|
189
|
+
- `spectral.config.mjs`
|
|
190
|
+
- `spectral.config.cjs`
|
|
191
|
+
|
|
192
|
+
Autodiscovered repo-level rulesets are merged with the package default ruleset.
|
|
193
|
+
|
|
194
|
+
### Use a JS or TS ruleset module
|
|
195
|
+
|
|
196
|
+
Module rulesets can export the ruleset as the default export or as a named `ruleset` export.
|
|
197
|
+
|
|
198
|
+
```ts
|
|
199
|
+
export default {
|
|
200
|
+
extends: ['spectral:oas'],
|
|
201
|
+
rules: {
|
|
202
|
+
operation-description: {
|
|
203
|
+
severity: 'error'
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
Module rulesets can also export custom Spectral functions:
|
|
210
|
+
|
|
211
|
+
```ts
|
|
212
|
+
const startsWithPrefix = (input, options) => {
|
|
213
|
+
if (typeof input !== 'string' || !input.startsWith(options.prefix)) {
|
|
214
|
+
return [{ message: `OperationId must start with "${options.prefix}".` }]
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export const functions = {
|
|
219
|
+
startsWithPrefix
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export default {
|
|
223
|
+
extends: ['spectral:oas'],
|
|
224
|
+
rules: {
|
|
225
|
+
'operation-id-prefix': {
|
|
226
|
+
given: '$.paths[*][get,put,post,delete,options,head,patch,trace]',
|
|
227
|
+
then: {
|
|
228
|
+
field: 'operationId',
|
|
229
|
+
function: 'startsWithPrefix',
|
|
230
|
+
functionOptions: { prefix: 'fetch' }
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
### Keep the app running when lint fails at startup
|
|
238
|
+
|
|
239
|
+
Use `startup.mode: 'report'` when you want the full package-owned terminal report but do not want lint findings to block boot.
|
|
240
|
+
|
|
241
|
+
```ts
|
|
242
|
+
spectralPlugin({
|
|
243
|
+
failOn: 'warn',
|
|
244
|
+
startup: {
|
|
245
|
+
mode: 'report'
|
|
246
|
+
}
|
|
247
|
+
})
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
This is useful for local development and intentionally broken fixtures.
|
|
251
|
+
|
|
252
|
+
### Disable startup lint entirely
|
|
253
|
+
|
|
254
|
+
Use `startup.mode: 'off'` when you only want manual or healthcheck-triggered runs.
|
|
255
|
+
|
|
256
|
+
```ts
|
|
257
|
+
spectralPlugin({
|
|
258
|
+
startup: {
|
|
259
|
+
mode: 'off'
|
|
260
|
+
}
|
|
261
|
+
})
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
`enabled: false` is still supported as a backwards-compatible way to disable startup lint.
|
|
265
|
+
|
|
266
|
+
### Add a lint healthcheck endpoint
|
|
267
|
+
|
|
268
|
+
The healthcheck route is opt-in.
|
|
269
|
+
|
|
270
|
+
```ts
|
|
271
|
+
spectralPlugin({
|
|
272
|
+
healthcheck: {
|
|
273
|
+
path: '/health/openapi-lint'
|
|
274
|
+
}
|
|
275
|
+
})
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
Behavior:
|
|
279
|
+
|
|
280
|
+
- `GET /health/openapi-lint` returns the cached startup result when available
|
|
281
|
+
- `GET /health/openapi-lint?fresh=1` forces a fresh lint run
|
|
282
|
+
- the route returns `200` when findings stay below `failOn`
|
|
283
|
+
- the route returns `503` when findings meet or exceed `failOn`
|
|
284
|
+
- the route is hidden from generated OpenAPI docs
|
|
285
|
+
|
|
286
|
+
### Persist JSON reports and OpenAPI snapshots
|
|
287
|
+
|
|
288
|
+
```ts
|
|
289
|
+
spectralPlugin({
|
|
290
|
+
output: {
|
|
291
|
+
jsonReportPath: './artifacts/openapi-lint.json',
|
|
292
|
+
specSnapshotPath: true
|
|
293
|
+
}
|
|
294
|
+
})
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
Snapshot behavior:
|
|
298
|
+
|
|
299
|
+
- `specSnapshotPath: true` derives `./<package-name>.open-api.json` from the consuming app's `package.json` name
|
|
300
|
+
- `specSnapshotPath: './contracts/openapi.json'` writes to the exact relative path you provide
|
|
301
|
+
- scoped package names are sanitized, so `@acme/orders-api` becomes `./acme-orders-api.open-api.json`
|
|
302
|
+
|
|
303
|
+
All report and snapshot paths resolve from the consuming app's `process.cwd()`.
|
|
304
|
+
|
|
305
|
+
### Emit SARIF for code scanning tools
|
|
306
|
+
|
|
307
|
+
```ts
|
|
308
|
+
spectralPlugin({
|
|
309
|
+
output: {
|
|
310
|
+
sarifReportPath: './artifacts/openapi-lint.sarif'
|
|
311
|
+
}
|
|
312
|
+
})
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
SARIF is useful when you want lint results in a standard machine-readable format for platforms such as GitHub code scanning and other static analysis tooling.
|
|
316
|
+
|
|
317
|
+
### Emit JUnit XML for CI test reporting
|
|
318
|
+
|
|
319
|
+
```ts
|
|
320
|
+
spectralPlugin({
|
|
321
|
+
output: {
|
|
322
|
+
junitReportPath: './artifacts/openapi-lint.junit.xml'
|
|
323
|
+
}
|
|
324
|
+
})
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
JUnit is useful when your CI system expects test-style XML output and you want lint findings to appear in standard test reports.
|
|
328
|
+
|
|
329
|
+
### Add a custom sink
|
|
330
|
+
|
|
331
|
+
The existing `output` convenience flags are built on top of sink abstractions. You can add your own sink for custom reporting or automation:
|
|
332
|
+
|
|
333
|
+
```ts
|
|
334
|
+
spectralPlugin({
|
|
335
|
+
output: {
|
|
336
|
+
sinks: [
|
|
337
|
+
{
|
|
338
|
+
name: 'capture',
|
|
339
|
+
async write(result, context) {
|
|
340
|
+
console.log(result.summary.total)
|
|
341
|
+
console.log(context.spec.openapi)
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
]
|
|
345
|
+
}
|
|
346
|
+
})
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
Custom sinks run after linting and can read:
|
|
350
|
+
|
|
351
|
+
- the normalized lint result
|
|
352
|
+
- the resolved OpenAPI document
|
|
353
|
+
- artifact paths already produced by earlier built-in sinks
|
|
354
|
+
|
|
355
|
+
### Use a custom ruleset resolver pipeline
|
|
356
|
+
|
|
357
|
+
If you use the runtime programmatically, you can provide your own resolver pipeline to `loadRuleset` or `loadResolvedRuleset`.
|
|
358
|
+
|
|
359
|
+
```ts
|
|
360
|
+
import { loadRuleset } from '@opsydyn/elysia-spectral/core'
|
|
361
|
+
|
|
362
|
+
const ruleset = await loadRuleset('virtual://team-ruleset', {
|
|
363
|
+
resolvers: [
|
|
364
|
+
async (input) =>
|
|
365
|
+
input === 'virtual://team-ruleset'
|
|
366
|
+
? {
|
|
367
|
+
ruleset: {
|
|
368
|
+
extends: ['spectral:oas'],
|
|
369
|
+
rules: {
|
|
370
|
+
'team-summary': {
|
|
371
|
+
given: '$.paths[*][get,put,post,delete,options,head,patch,trace]',
|
|
372
|
+
then: {
|
|
373
|
+
field: 'summary',
|
|
374
|
+
function: 'truthy'
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
: undefined
|
|
381
|
+
]
|
|
382
|
+
})
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
This is an advanced extension point. Most apps should continue using repo-level `spectral.*` files.
|
|
386
|
+
|
|
387
|
+
### Make artifact write failures fatal in CI
|
|
388
|
+
|
|
389
|
+
Artifact writes are non-fatal by default. In CI, set them to fatal:
|
|
390
|
+
|
|
391
|
+
```ts
|
|
392
|
+
spectralPlugin({
|
|
393
|
+
output: {
|
|
394
|
+
jsonReportPath: './artifacts/openapi-lint.json',
|
|
395
|
+
specSnapshotPath: true,
|
|
396
|
+
artifactWriteFailures: 'error'
|
|
397
|
+
}
|
|
398
|
+
})
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
Use `'warn'` for local development and `'error'` when artifact generation is required.
|
|
402
|
+
|
|
403
|
+
### Run the runtime in CI or tests
|
|
404
|
+
|
|
405
|
+
```ts
|
|
406
|
+
import { Elysia } from 'elysia'
|
|
407
|
+
import { openapi } from '@elysiajs/openapi'
|
|
408
|
+
import { createOpenApiLintRuntime } from '@opsydyn/elysia-spectral'
|
|
409
|
+
|
|
410
|
+
const app = new Elysia().use(openapi())
|
|
411
|
+
|
|
412
|
+
const runtime = createOpenApiLintRuntime({
|
|
413
|
+
ruleset: './spectral.yaml',
|
|
414
|
+
failOn: 'error',
|
|
415
|
+
output: {
|
|
416
|
+
console: true,
|
|
417
|
+
jsonReportPath: './artifacts/openapi-lint.json',
|
|
418
|
+
specSnapshotPath: true,
|
|
419
|
+
artifactWriteFailures: 'error'
|
|
420
|
+
}
|
|
421
|
+
})
|
|
422
|
+
|
|
423
|
+
await runtime.run(app)
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
### Work on this repository locally
|
|
427
|
+
|
|
428
|
+
From the monorepo root:
|
|
429
|
+
|
|
430
|
+
```bash
|
|
431
|
+
bun install
|
|
432
|
+
bun run dev
|
|
433
|
+
```
|
|
434
|
+
|
|
435
|
+
That starts `apps/dev-app` with:
|
|
436
|
+
|
|
437
|
+
- OpenAPI UI at `/openapi`
|
|
438
|
+
- raw OpenAPI JSON at `/openapi/json`
|
|
439
|
+
- opt-in lint healthcheck at `/health/openapi-lint`
|
|
440
|
+
- JSON lint report output at `./artifacts/openapi-lint.json`
|
|
441
|
+
- OpenAPI snapshot output at `./elysia-spectral-dev-app.open-api.json`
|
|
442
|
+
|
|
443
|
+
To run an intentionally failing fixture:
|
|
444
|
+
|
|
445
|
+
```bash
|
|
446
|
+
bun run dev:unhappy
|
|
447
|
+
```
|
|
448
|
+
|
|
449
|
+
That example uses `startup.mode: 'report'`, so the app still boots while the package prints the full lint report during startup.
|
|
450
|
+
|
|
451
|
+
## Reference
|
|
452
|
+
|
|
453
|
+
### Package API
|
|
454
|
+
|
|
455
|
+
```ts
|
|
456
|
+
type SeverityThreshold = 'error' | 'warn' | 'info' | 'hint' | 'never'
|
|
457
|
+
|
|
458
|
+
type StartupLintMode = 'enforce' | 'report' | 'off'
|
|
459
|
+
|
|
460
|
+
type ArtifactWriteFailureMode = 'warn' | 'error'
|
|
461
|
+
|
|
462
|
+
type OpenApiLintArtifacts = {
|
|
463
|
+
jsonReportPath?: string
|
|
464
|
+
junitReportPath?: string
|
|
465
|
+
sarifReportPath?: string
|
|
466
|
+
specSnapshotPath?: string
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
type OpenApiLintSink = {
|
|
470
|
+
name: string
|
|
471
|
+
write: (
|
|
472
|
+
result: LintRunResult,
|
|
473
|
+
context: {
|
|
474
|
+
spec: Record<string, unknown>
|
|
475
|
+
logger: SpectralLogger
|
|
476
|
+
}
|
|
477
|
+
) => void | Partial<OpenApiLintArtifacts> | Promise<void | Partial<OpenApiLintArtifacts>>
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
type RulesetResolver = (
|
|
481
|
+
input: string | RulesetDefinition | Record<string, unknown> | undefined,
|
|
482
|
+
context: {
|
|
483
|
+
baseDir: string
|
|
484
|
+
defaultRuleset: RulesetDefinition
|
|
485
|
+
mergeAutodiscoveredWithDefault: boolean
|
|
486
|
+
}
|
|
487
|
+
) => Promise<LoadedRuleset | undefined>
|
|
488
|
+
|
|
489
|
+
type LoadResolvedRulesetOptions = {
|
|
490
|
+
baseDir?: string
|
|
491
|
+
resolvers?: RulesetResolver[]
|
|
492
|
+
mergeAutodiscoveredWithDefault?: boolean
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
type SpectralPluginOptions = {
|
|
496
|
+
ruleset?: string | RulesetDefinition | Record<string, unknown>
|
|
497
|
+
failOn?: SeverityThreshold
|
|
498
|
+
healthcheck?: false | { path?: string }
|
|
499
|
+
output?: {
|
|
500
|
+
console?: boolean
|
|
501
|
+
jsonReportPath?: string
|
|
502
|
+
junitReportPath?: string
|
|
503
|
+
sarifReportPath?: string
|
|
504
|
+
specSnapshotPath?: string | true
|
|
505
|
+
pretty?: boolean
|
|
506
|
+
artifactWriteFailures?: ArtifactWriteFailureMode
|
|
507
|
+
sinks?: OpenApiLintSink[]
|
|
508
|
+
}
|
|
509
|
+
source?: {
|
|
510
|
+
specPath?: string
|
|
511
|
+
baseUrl?: string
|
|
512
|
+
}
|
|
513
|
+
enabled?: boolean | ((env: Record<string, string | undefined>) => boolean)
|
|
514
|
+
startup?: {
|
|
515
|
+
mode?: StartupLintMode
|
|
516
|
+
}
|
|
517
|
+
logger?: {
|
|
518
|
+
info: (message: string) => void
|
|
519
|
+
warn: (message: string) => void
|
|
520
|
+
error: (message: string) => void
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
```
|
|
524
|
+
|
|
525
|
+
### Runtime state
|
|
526
|
+
|
|
527
|
+
The runtime object exposes:
|
|
528
|
+
|
|
529
|
+
- `status`: `idle | running | passed | failed`
|
|
530
|
+
- `startedAt`: ISO timestamp for the most recent run start
|
|
531
|
+
- `completedAt`: ISO timestamp for the most recent run completion
|
|
532
|
+
- `durationMs`: duration of the most recent run
|
|
533
|
+
- `latest`: last completed lint result
|
|
534
|
+
- `lastSuccess`: last successful lint result
|
|
535
|
+
- `lastFailure`: last thrown runtime error summary
|
|
536
|
+
- `running`: boolean convenience flag
|
|
537
|
+
|
|
538
|
+
When used as a plugin, the runtime is also available on `app.store.openApiLint`.
|
|
539
|
+
|
|
540
|
+
### Healthcheck response shape
|
|
541
|
+
|
|
542
|
+
Example successful response:
|
|
543
|
+
|
|
544
|
+
```json
|
|
545
|
+
{
|
|
546
|
+
"ok": true,
|
|
547
|
+
"cached": true,
|
|
548
|
+
"threshold": "error",
|
|
549
|
+
"result": {
|
|
550
|
+
"ok": true,
|
|
551
|
+
"generatedAt": "2026-04-06T12:00:00.000Z",
|
|
552
|
+
"summary": {
|
|
553
|
+
"error": 0,
|
|
554
|
+
"warn": 0,
|
|
555
|
+
"info": 0,
|
|
556
|
+
"hint": 0,
|
|
557
|
+
"total": 0
|
|
558
|
+
},
|
|
559
|
+
"findings": []
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
```
|
|
563
|
+
|
|
564
|
+
### Ruleset resolution
|
|
565
|
+
|
|
566
|
+
- ruleset paths resolve from the consuming app's `process.cwd()`
|
|
567
|
+
- in a typical service repo, `./spectral.yaml` means the repo root
|
|
568
|
+
- in this monorepo, `apps/dev-app` resolves `./spectral.yaml` from `apps/dev-app`
|
|
569
|
+
- supported local ruleset files are `.yaml`, `.yml`, `.js`, `.mjs`, `.cjs`, `.ts`, `.mts`, and `.cts`
|
|
570
|
+
- module rulesets may export the ruleset as the default export or a named `ruleset` export
|
|
571
|
+
- module rulesets may export `functions` to register custom Spectral functions
|
|
572
|
+
- in-memory ruleset objects are supported
|
|
573
|
+
- `loadRuleset` and `loadResolvedRuleset` also accept an options object with a custom `resolvers` pipeline
|
|
574
|
+
- autodiscovered rulesets merge with the package default ruleset by default and can opt out with `mergeAutodiscoveredWithDefault: false`
|
|
575
|
+
|
|
576
|
+
### Error behavior
|
|
577
|
+
|
|
578
|
+
- startup mode `enforce` throws on threshold failures
|
|
579
|
+
- startup mode `report` prints the same lint report but allows boot to continue on threshold failures
|
|
580
|
+
- startup mode `off` skips startup lint
|
|
581
|
+
- bad `source.specPath` or invalid spec JSON produces an actionable provider error
|
|
582
|
+
- artifact writes warn by default and can be made fatal with `output.artifactWriteFailures: 'error'`
|
|
583
|
+
|
|
584
|
+
### Output model
|
|
585
|
+
|
|
586
|
+
The current output model has two layers:
|
|
587
|
+
|
|
588
|
+
- convenience options such as `jsonReportPath`, `junitReportPath`, `specSnapshotPath`, and `sarifReportPath`
|
|
589
|
+
- sink abstractions under `output.sinks`
|
|
590
|
+
|
|
591
|
+
The convenience options compile down to built-in sinks so the current API stays simple while the internal output model becomes extensible.
|
|
592
|
+
|
|
593
|
+
## Explanation
|
|
594
|
+
|
|
595
|
+
### Why this package exists
|
|
596
|
+
|
|
597
|
+
`@opsydyn/elysia-spectral` is meant to add contract-quality feedback to Elysia APIs without inventing a second schema system. The source of truth remains the route metadata and response schemas you already define for `@elysiajs/openapi`.
|
|
598
|
+
|
|
599
|
+
### How it works
|
|
600
|
+
|
|
601
|
+
The plugin does not inspect private `@elysiajs/openapi` internals. It resolves the generated OpenAPI JSON document through Elysia's public `app.handle(new Request(...))` API, using `source.specPath` or the default `/openapi/json`.
|
|
602
|
+
|
|
603
|
+
If `source.baseUrl` is configured, the provider can also fall back to loopback HTTP fetch. This keeps spec resolution on public surfaces rather than framework internals.
|
|
604
|
+
|
|
605
|
+
### Why startup and healthcheck are separate
|
|
606
|
+
|
|
607
|
+
Startup linting and route exposure solve different problems:
|
|
608
|
+
|
|
609
|
+
- startup linting protects boot and local feedback loops
|
|
610
|
+
- healthchecks expose operational state to external callers
|
|
611
|
+
|
|
612
|
+
Separating them avoids a production surprise where enabling linting also adds a route you did not intend to expose.
|
|
613
|
+
|
|
614
|
+
### Why repo-level rulesets are the default customization path
|
|
615
|
+
|
|
616
|
+
Teams usually want policy to live with the service, not inside library code. A repo-level ruleset keeps API governance close to the app, easy to review in pull requests, and easy to override without forking this package.
|
|
617
|
+
|
|
618
|
+
That is why `spectralPlugin()` can autodiscover a repo-level ruleset and merge it with the package defaults.
|
|
619
|
+
|
|
620
|
+
### Why the runtime is exported separately
|
|
621
|
+
|
|
622
|
+
The runtime exists so the same linting behavior can run in:
|
|
623
|
+
|
|
624
|
+
- app startup
|
|
625
|
+
- healthcheck-triggered manual runs
|
|
626
|
+
- CI pipelines
|
|
627
|
+
- tests
|
|
628
|
+
|
|
629
|
+
That keeps the plugin thin while still giving teams a stable programmatic surface for automation.
|
|
630
|
+
|
|
631
|
+
### Why runtime state is tracked explicitly
|
|
632
|
+
|
|
633
|
+
Production-grade linting needs more than a pass/fail boolean. The runtime tracks status, timestamps, last success, and last failure so operators and automated systems can understand what happened without re-running lint blindly.
|
|
634
|
+
|
|
635
|
+
### Project status
|
|
636
|
+
|
|
637
|
+
This package currently implements the narrowed v0.1 scope from [project.md](../../project.md) and the ongoing hardening work tracked in [roadmap.md](../../roadmap.md).
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import { _ as defaultRulesetResolvers, a as OpenApiLintArtifactWriteError, b as lintOpenApi, c as resolveStartupMode, d as LoadedRuleset, f as ResolvedRulesetCandidate, g as RulesetResolverInput, h as RulesetResolverContext, i as shouldFail, l as normalizeFindings, m as RulesetResolver, n as enforceThreshold, o as createOpenApiLintRuntime, p as RulesetLoadError, r as exceedsThreshold, s as isEnabled, t as OpenApiLintThresholdError, u as LoadResolvedRulesetOptions, v as loadResolvedRuleset, y as loadRuleset } from "../index-BrFQCFDI.mjs";
|
|
2
|
+
export { LoadResolvedRulesetOptions, LoadedRuleset, OpenApiLintArtifactWriteError, OpenApiLintThresholdError, ResolvedRulesetCandidate, RulesetLoadError, RulesetResolver, RulesetResolverContext, RulesetResolverInput, createOpenApiLintRuntime, defaultRulesetResolvers, enforceThreshold, exceedsThreshold, isEnabled, lintOpenApi, loadResolvedRuleset, loadRuleset, normalizeFindings, resolveStartupMode, shouldFail };
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import { a as OpenApiLintThresholdError, c as shouldFail, d as defaultRulesetResolvers, f as loadResolvedRuleset, h as normalizeFindings, i as resolveStartupMode, m as lintOpenApi, n as createOpenApiLintRuntime, o as enforceThreshold, p as loadRuleset, r as isEnabled, s as exceedsThreshold, t as OpenApiLintArtifactWriteError, u as RulesetLoadError } from "../core-Czin3kvK.mjs";
|
|
2
|
+
export { OpenApiLintArtifactWriteError, OpenApiLintThresholdError, RulesetLoadError, createOpenApiLintRuntime, defaultRulesetResolvers, enforceThreshold, exceedsThreshold, isEnabled, lintOpenApi, loadResolvedRuleset, loadRuleset, normalizeFindings, resolveStartupMode, shouldFail };
|