@opsydyn/elysia-spectral 1.5.0 → 1.5.2
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 +14 -0
- package/README.md +51 -3
- package/dist/core/index.d.mts +1 -1
- package/dist/core/index.mjs +4 -1
- package/dist/{index-CyJXdIRT.d.mts → index-DzJWrqPA.d.mts} +7 -5
- package/dist/index.d.mts +8 -3
- package/dist/index.mjs +16 -2
- package/dist/lint-openapi-D76sC7S5.mjs +122 -0
- package/dist/load-ruleset-CiikrzWx.mjs +301 -0
- package/dist/presets-CCfU_diN.mjs +132 -0
- package/dist/recommended-DgrTqq-3.mjs +40 -0
- package/dist/rolldown-runtime-wcPFST8Q.mjs +13 -0
- package/dist/ruleset-load-error-CogUOC7W.mjs +10 -0
- package/dist/{core-BLJeXQ15.mjs → runtime-PGHAFx-E.mjs} +60 -584
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.5.2](https://github.com/opsydyn/elysia-spectral/compare/v1.5.1...v1.5.2) (2026-05-12)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
* complete self-describing lint reports ([b0917ce](https://github.com/opsydyn/elysia-spectral/commit/b0917ce4c977a1378ba851efc643058a70fea70b))
|
|
9
|
+
|
|
10
|
+
## [1.5.1](https://github.com/opsydyn/elysia-spectral/compare/v1.5.0...v1.5.1) (2026-05-12)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Bug Fixes
|
|
14
|
+
|
|
15
|
+
* harden spectral import startup path ([024fedf](https://github.com/opsydyn/elysia-spectral/commit/024fedf7fe294fc7919f9d17d7727029e2a04fae))
|
|
16
|
+
|
|
3
17
|
## [1.5.0](https://github.com/opsydyn/elysia-spectral/compare/v1.4.0...v1.5.0) (2026-04-28)
|
|
4
18
|
|
|
5
19
|
|
package/README.md
CHANGED
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
|
|
7
7
|
Thin Elysia plugin that lints the OpenAPI document generated by `@elysiajs/openapi` with Spectral.
|
|
8
8
|
|
|
9
|
+
`@opsydyn/elysia-spectral` does not generate OpenAPI documents by itself. Your app must expose an OpenAPI JSON document — most commonly by mounting `@elysiajs/openapi`, or by configuring `source.specPath` / `source.baseUrl` to point at an equivalent route.
|
|
10
|
+
|
|
9
11
|
## What Is Elysia?
|
|
10
12
|
|
|
11
13
|
Elysia is a fast, ergonomic TypeScript web framework for Bun. It uses a plugin model for composing functionality and integrates with `@elysiajs/openapi` to generate OpenAPI documentation directly from route schemas — no separate spec file or annotation layer required.
|
|
@@ -56,6 +58,7 @@ Current package scope:
|
|
|
56
58
|
- resolver pipeline for advanced ruleset loading
|
|
57
59
|
- console output
|
|
58
60
|
- JSON report output
|
|
61
|
+
- self-describing JSON report metadata (`failOn`, `durationMs`, relative artifact paths)
|
|
59
62
|
- JUnit report output
|
|
60
63
|
- SARIF report output
|
|
61
64
|
- OpenAPI snapshot output
|
|
@@ -109,6 +112,8 @@ bun add elysia @elysiajs/openapi @opsydyn/elysia-spectral
|
|
|
109
112
|
npm install elysia @elysiajs/openapi @opsydyn/elysia-spectral
|
|
110
113
|
```
|
|
111
114
|
|
|
115
|
+
`@elysiajs/openapi` is the recommended generator because it exposes the default `/openapi/json` route this package expects. If your app uses a different OpenAPI generator or serves the JSON at a different path, configure `source.specPath` accordingly.
|
|
116
|
+
|
|
112
117
|
2. Add `@elysiajs/openapi` and `spectralPlugin` to your app with the `strict` preset.
|
|
113
118
|
|
|
114
119
|
```ts
|
|
@@ -217,6 +222,34 @@ spectralPlugin({ ruleset: strict })
|
|
|
217
222
|
|
|
218
223
|
When `preset` is set, autodiscovered `spectral.yaml` overrides merge on top of the preset rather than the package default. This lets you tighten or loosen individual rules without losing the preset baseline.
|
|
219
224
|
|
|
225
|
+
### Provide an OpenAPI JSON route
|
|
226
|
+
|
|
227
|
+
`@opsydyn/elysia-spectral` needs a public OpenAPI JSON document to lint. By default it looks for `/openapi/json` via `app.handle(new Request(...))`.
|
|
228
|
+
|
|
229
|
+
The recommended setup is to mount `@elysiajs/openapi`:
|
|
230
|
+
|
|
231
|
+
```ts
|
|
232
|
+
import { Elysia } from 'elysia'
|
|
233
|
+
import { openapi } from '@elysiajs/openapi'
|
|
234
|
+
import { spectralPlugin } from '@opsydyn/elysia-spectral'
|
|
235
|
+
|
|
236
|
+
new Elysia()
|
|
237
|
+
.use(openapi())
|
|
238
|
+
.use(spectralPlugin())
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
If your app exposes the OpenAPI JSON document somewhere else, point the plugin at that route:
|
|
242
|
+
|
|
243
|
+
```ts
|
|
244
|
+
spectralPlugin({
|
|
245
|
+
source: {
|
|
246
|
+
specPath: '/docs/openapi.json'
|
|
247
|
+
}
|
|
248
|
+
})
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
If the route is missing, the runtime throws an actionable provider error explaining that an OpenAPI generator (for example `@elysiajs/openapi`) must be installed and mounted, or that `source.specPath` should be updated.
|
|
252
|
+
|
|
220
253
|
## How-to Guides
|
|
221
254
|
|
|
222
255
|
### Use a repo-level ruleset
|
|
@@ -386,9 +419,10 @@ What it surfaces:
|
|
|
386
419
|
|
|
387
420
|
Keyboard shortcuts: `r` re-runs, `/` focuses the filter, `Enter`/`Space` toggles the focused severity chip.
|
|
388
421
|
|
|
389
|
-
|
|
422
|
+
Four dark themes ship with the dashboard, selectable from the header dropdown and persisted per-browser via `localStorage`:
|
|
390
423
|
|
|
391
424
|
- **Astro Houston** (default) — purple / blue gradient
|
|
425
|
+
- **Elysia** — pink on deep purple, sourced from the Scalar palette
|
|
392
426
|
- **Tron Legacy** — cyan neon on near-black
|
|
393
427
|
- **Detroit 808** — TR-808 amber, red, and yellow
|
|
394
428
|
|
|
@@ -523,6 +557,8 @@ Use `'warn'` for local development and `'error'` when artifact generation is req
|
|
|
523
557
|
|
|
524
558
|
Use `createOpenApiLintRuntime` to run a standalone lint check in CI without starting an HTTP server. Import your Elysia app instance directly — the runtime uses `app.handle()` in-process to retrieve the generated OpenAPI document. No port binding required.
|
|
525
559
|
|
|
560
|
+
Before using the runtime, make sure the app instance mounts an OpenAPI generator (typically `@elysiajs/openapi`) or otherwise serves a reachable OpenAPI JSON route.
|
|
561
|
+
|
|
526
562
|
Create a script at `scripts/lint-openapi.ts`:
|
|
527
563
|
|
|
528
564
|
```ts
|
|
@@ -760,7 +796,9 @@ type SpectralPluginOptions = {
|
|
|
760
796
|
sinks?: OpenApiLintSink[]
|
|
761
797
|
}
|
|
762
798
|
source?: {
|
|
799
|
+
/** Public OpenAPI JSON route. Defaults to '/openapi/json'. Mount @elysiajs/openapi or serve an equivalent route yourself. */
|
|
763
800
|
specPath?: string
|
|
801
|
+
/** Optional base URL fallback when the OpenAPI document is only reachable over HTTP. */
|
|
764
802
|
baseUrl?: string
|
|
765
803
|
}
|
|
766
804
|
/**
|
|
@@ -809,6 +847,10 @@ type LintRunResult = {
|
|
|
809
847
|
generatedAt: string
|
|
810
848
|
/** Where the lint run was triggered from. */
|
|
811
849
|
source: LintRunSource
|
|
850
|
+
/** The configured threshold that produced this result. */
|
|
851
|
+
failOn: SeverityThreshold
|
|
852
|
+
/** Duration of the completed lint run in milliseconds. */
|
|
853
|
+
durationMs: number | null
|
|
812
854
|
summary: {
|
|
813
855
|
error: number
|
|
814
856
|
warn: number
|
|
@@ -971,6 +1013,8 @@ Example successful response:
|
|
|
971
1013
|
"ok": true,
|
|
972
1014
|
"generatedAt": "2026-04-06T12:00:00.000Z",
|
|
973
1015
|
"source": "startup",
|
|
1016
|
+
"failOn": "error",
|
|
1017
|
+
"durationMs": 42,
|
|
974
1018
|
"summary": {
|
|
975
1019
|
"error": 0,
|
|
976
1020
|
"warn": 0,
|
|
@@ -1000,7 +1044,7 @@ Example successful response:
|
|
|
1000
1044
|
- startup mode `enforce` throws on threshold failures
|
|
1001
1045
|
- startup mode `report` prints the same lint report but allows boot to continue on threshold failures
|
|
1002
1046
|
- startup mode `off` skips startup lint
|
|
1003
|
-
- bad `source.specPath
|
|
1047
|
+
- missing OpenAPI generator / missing OpenAPI JSON route, bad `source.specPath`, or invalid spec JSON produces an actionable provider error
|
|
1004
1048
|
- artifact writes warn by default and can be made fatal with `output.artifactWriteFailures: 'error'`
|
|
1005
1049
|
|
|
1006
1050
|
### Output model
|
|
@@ -1012,6 +1056,8 @@ The current output model has two layers:
|
|
|
1012
1056
|
|
|
1013
1057
|
The convenience options compile down to built-in sinks so the current API stays simple while the internal output model becomes extensible.
|
|
1014
1058
|
|
|
1059
|
+
Persisted JSON reports are self-describing: they embed the configured `failOn` threshold, the completed `durationMs`, and relative artifact paths so the same report shape is portable across CI runners and developer machines.
|
|
1060
|
+
|
|
1015
1061
|
## Explanation
|
|
1016
1062
|
|
|
1017
1063
|
### Why this package exists
|
|
@@ -1022,6 +1068,8 @@ The convenience options compile down to built-in sinks so the current API stays
|
|
|
1022
1068
|
|
|
1023
1069
|
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`.
|
|
1024
1070
|
|
|
1071
|
+
`@elysiajs/openapi` is the default and recommended source for that document, but it is not the only supported source. Any equivalent OpenAPI JSON route works as long as `source.specPath` and, when needed, `source.baseUrl` point to it.
|
|
1072
|
+
|
|
1025
1073
|
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.
|
|
1026
1074
|
|
|
1027
1075
|
### Why startup and healthcheck are separate
|
|
@@ -1057,4 +1105,4 @@ Production-grade linting needs more than a pass/fail boolean. The runtime tracks
|
|
|
1057
1105
|
|
|
1058
1106
|
### Project status
|
|
1059
1107
|
|
|
1060
|
-
The package is
|
|
1108
|
+
The package is published to npm and used in production. The current pre-`1.0` feature roadmap is functionally complete: startup/runtime flows, presets, output sinks, CI workflows, dashboard support, and self-describing result artifacts are all shipped. Remaining work is primarily `1.0` stabilization — public API/package boundaries, backwards-compatibility audit, and migration notes — tracked in [roadmap.md](../../roadmap.md).
|
package/dist/core/index.d.mts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as createOpenApiLintRuntime, c as
|
|
1
|
+
import { a as createOpenApiLintRuntime, c as LoadedRuleset, d as RulesetResolverContext, f as RulesetResolverInput, g as lintOpenApi, h as loadRuleset, i as OpenApiLintArtifactWriteError, l as ResolvedRulesetCandidate, m as loadResolvedRuleset, n as enforceThreshold, o as RulesetLoadError, p as defaultRulesetResolvers, r as shouldFail, s as LoadResolvedRulesetOptions, t as OpenApiLintThresholdError, u as RulesetResolver } from "../index-DzJWrqPA.mjs";
|
|
2
2
|
export { LoadResolvedRulesetOptions, LoadedRuleset, OpenApiLintArtifactWriteError, OpenApiLintThresholdError, ResolvedRulesetCandidate, RulesetLoadError, RulesetResolver, RulesetResolverContext, RulesetResolverInput, createOpenApiLintRuntime, defaultRulesetResolvers, enforceThreshold, lintOpenApi, loadResolvedRuleset, loadRuleset, shouldFail };
|
package/dist/core/index.mjs
CHANGED
|
@@ -1,2 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { t as RulesetLoadError } from "../ruleset-load-error-CogUOC7W.mjs";
|
|
2
|
+
import { a as enforceThreshold, i as OpenApiLintThresholdError, n as createOpenApiLintRuntime, o as shouldFail, t as OpenApiLintArtifactWriteError } from "../runtime-PGHAFx-E.mjs";
|
|
3
|
+
import { n as loadResolvedRuleset, r as loadRuleset, t as defaultRulesetResolvers } from "../load-ruleset-CiikrzWx.mjs";
|
|
4
|
+
import { t as lintOpenApi } from "../lint-openapi-D76sC7S5.mjs";
|
|
2
5
|
export { OpenApiLintArtifactWriteError, OpenApiLintThresholdError, RulesetLoadError, createOpenApiLintRuntime, defaultRulesetResolvers, enforceThreshold, lintOpenApi, loadResolvedRuleset, loadRuleset, shouldFail };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { RulesetDefinition } from "@stoplight/spectral-core";
|
|
2
1
|
import { AnyElysia } from "elysia";
|
|
2
|
+
import { RulesetDefinition } from "@stoplight/spectral-core";
|
|
3
3
|
|
|
4
4
|
//#region src/types.d.ts
|
|
5
5
|
type PresetName = 'recommended' | 'server' | 'strict';
|
|
@@ -147,14 +147,16 @@ type LoadResolvedRulesetOptions = {
|
|
|
147
147
|
mergeAutodiscoveredWithDefault?: boolean; /** Override the ruleset used as the merge base for autodiscovery and the fallback when no ruleset is configured. Defaults to the package default (recommended preset). */
|
|
148
148
|
defaultRuleset?: RulesetDefinition;
|
|
149
149
|
};
|
|
150
|
+
declare const loadRuleset: (input?: RulesetResolverInput, baseDirOrOptions?: string | LoadResolvedRulesetOptions) => Promise<RulesetDefinition>;
|
|
151
|
+
declare const loadResolvedRuleset: (input?: RulesetResolverInput, baseDirOrOptions?: string | LoadResolvedRulesetOptions) => Promise<LoadedRuleset>;
|
|
152
|
+
declare const defaultRulesetResolvers: RulesetResolver[];
|
|
153
|
+
//#endregion
|
|
154
|
+
//#region src/core/ruleset-load-error.d.ts
|
|
150
155
|
declare class RulesetLoadError extends Error {
|
|
151
156
|
constructor(message: string, options?: {
|
|
152
157
|
cause?: unknown;
|
|
153
158
|
});
|
|
154
159
|
}
|
|
155
|
-
declare const loadRuleset: (input?: RulesetResolverInput, baseDirOrOptions?: string | LoadResolvedRulesetOptions) => Promise<RulesetDefinition>;
|
|
156
|
-
declare const loadResolvedRuleset: (input?: RulesetResolverInput, baseDirOrOptions?: string | LoadResolvedRulesetOptions) => Promise<LoadedRuleset>;
|
|
157
|
-
declare const defaultRulesetResolvers: RulesetResolver[];
|
|
158
160
|
//#endregion
|
|
159
161
|
//#region src/core/runtime.d.ts
|
|
160
162
|
declare const createOpenApiLintRuntime: (options?: SpectralPluginOptions) => OpenApiLintRuntime;
|
|
@@ -173,4 +175,4 @@ declare class OpenApiLintThresholdError extends Error {
|
|
|
173
175
|
declare const shouldFail: (result: LintRunResult, threshold: SeverityThreshold) => boolean;
|
|
174
176
|
declare const enforceThreshold: (result: LintRunResult, threshold: SeverityThreshold) => void;
|
|
175
177
|
//#endregion
|
|
176
|
-
export { SpectralLogger as A, OpenApiLintRuntime as C, OpenApiLintSinkContext as D, OpenApiLintSink as E, StartupLintMode as M, PresetName as O, OpenApiLintArtifacts as S, OpenApiLintRuntimeStatus as T, ArtifactWriteFailureMode as _, createOpenApiLintRuntime as a, LintRunSource as b,
|
|
178
|
+
export { SpectralLogger as A, OpenApiLintRuntime as C, OpenApiLintSinkContext as D, OpenApiLintSink as E, StartupLintMode as M, PresetName as O, OpenApiLintArtifacts as S, OpenApiLintRuntimeStatus as T, ArtifactWriteFailureMode as _, createOpenApiLintRuntime as a, LintRunSource as b, LoadedRuleset as c, RulesetResolverContext as d, RulesetResolverInput as f, lintOpenApi as g, loadRuleset as h, OpenApiLintArtifactWriteError as i, SpectralPluginOptions as j, SeverityThreshold as k, ResolvedRulesetCandidate as l, loadResolvedRuleset as m, enforceThreshold as n, RulesetLoadError as o, defaultRulesetResolvers as p, shouldFail as r, LoadResolvedRulesetOptions as s, OpenApiLintThresholdError as t, RulesetResolver as u, LintFinding as v, OpenApiLintRuntimeFailure as w, LintSeverity as x, LintRunResult as y };
|
package/dist/index.d.mts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { A as SpectralLogger, C as OpenApiLintRuntime, D as OpenApiLintSinkContext, E as OpenApiLintSink, M as StartupLintMode, O as PresetName, S as OpenApiLintArtifacts, T as OpenApiLintRuntimeStatus, _ as ArtifactWriteFailureMode, a as createOpenApiLintRuntime, b as LintRunSource, c as
|
|
2
|
-
import { RulesetDefinition } from "@stoplight/spectral-core";
|
|
1
|
+
import { A as SpectralLogger, C as OpenApiLintRuntime, D as OpenApiLintSinkContext, E as OpenApiLintSink, M as StartupLintMode, O as PresetName, S as OpenApiLintArtifacts, T as OpenApiLintRuntimeStatus, _ as ArtifactWriteFailureMode, a as createOpenApiLintRuntime, b as LintRunSource, c as LoadedRuleset, d as RulesetResolverContext, f as RulesetResolverInput, i as OpenApiLintArtifactWriteError, j as SpectralPluginOptions, k as SeverityThreshold, l as ResolvedRulesetCandidate, n as enforceThreshold, o as RulesetLoadError, r as shouldFail, s as LoadResolvedRulesetOptions, t as OpenApiLintThresholdError, u as RulesetResolver, v as LintFinding, w as OpenApiLintRuntimeFailure, x as LintSeverity, y as LintRunResult } from "./index-DzJWrqPA.mjs";
|
|
3
2
|
import { Elysia } from "elysia";
|
|
3
|
+
import { RulesetDefinition } from "@stoplight/spectral-core";
|
|
4
4
|
|
|
5
5
|
//#region src/plugin.d.ts
|
|
6
6
|
declare const spectralPlugin: (options?: SpectralPluginOptions) => Elysia<"", {
|
|
@@ -72,4 +72,9 @@ declare const strict: RulesetDefinition;
|
|
|
72
72
|
//#region src/presets/index.d.ts
|
|
73
73
|
declare const presets: Record<PresetName, RulesetDefinition>;
|
|
74
74
|
//#endregion
|
|
75
|
-
|
|
75
|
+
//#region src/index.d.ts
|
|
76
|
+
declare const loadRuleset: (input?: RulesetResolverInput, baseDirOrOptions?: string | LoadResolvedRulesetOptions) => Promise<RulesetDefinition>;
|
|
77
|
+
declare const loadResolvedRuleset: (input?: RulesetResolverInput, baseDirOrOptions?: string | LoadResolvedRulesetOptions) => Promise<LoadedRuleset>;
|
|
78
|
+
declare const lintOpenApi: (spec: Record<string, unknown>, ruleset: RulesetDefinition) => Promise<LintRunResult>;
|
|
79
|
+
//#endregion
|
|
80
|
+
export { ArtifactWriteFailureMode, LintFinding, LintRunResult, LintRunSource, LintSeverity, type LoadResolvedRulesetOptions, type LoadedRuleset, OpenApiLintArtifactWriteError, OpenApiLintArtifacts, OpenApiLintRuntime, OpenApiLintRuntimeFailure, OpenApiLintRuntimeStatus, OpenApiLintSink, OpenApiLintSinkContext, OpenApiLintThresholdError, PresetName, type ResolvedRulesetCandidate, RulesetLoadError, type RulesetResolver, type RulesetResolverContext, type RulesetResolverInput, SeverityThreshold, SpectralLogger, SpectralPluginOptions, StartupLintMode, createOpenApiLintRuntime, enforceThreshold, lintOpenApi, loadResolvedRuleset, loadRuleset, presets, recommended, server, shouldFail, spectralPlugin, strict };
|
package/dist/index.mjs
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { t as RulesetLoadError } from "./ruleset-load-error-CogUOC7W.mjs";
|
|
2
|
+
import { a as enforceThreshold, i as OpenApiLintThresholdError, n as createOpenApiLintRuntime, o as shouldFail, r as resolveStartupMode, s as resolveReporter, t as OpenApiLintArtifactWriteError } from "./runtime-PGHAFx-E.mjs";
|
|
3
|
+
import { t as recommended } from "./recommended-DgrTqq-3.mjs";
|
|
4
|
+
import { i as server, r as strict, t as presets } from "./presets-CCfU_diN.mjs";
|
|
2
5
|
import { Elysia } from "elysia";
|
|
3
6
|
//#region \0inline-text:3.mjs
|
|
4
7
|
var _inline_text_3_default = "(() => {\n const THEME_KEY = 'elysia-spectral-theme';\n const THEMES = ['astro', 'elysia', 'tron', '808'];\n let stored = null;\n try {\n stored = localStorage.getItem(THEME_KEY);\n } catch {}\n const initial = THEMES.includes(stored) ? stored : 'astro';\n document.documentElement.dataset.theme = initial;\n\n const themeSel = document.querySelector('[data-theme-switcher]');\n if (themeSel) {\n themeSel.value = initial;\n themeSel.addEventListener('change', () => {\n const next = THEMES.includes(themeSel.value) ? themeSel.value : 'astro';\n document.documentElement.dataset.theme = next;\n try {\n localStorage.setItem(THEME_KEY, next);\n } catch {}\n });\n }\n\n const rel = (iso) => {\n const t = Date.parse(iso);\n if (Number.isNaN(t)) return '';\n const s = Math.round((Date.now() - t) / 1000);\n if (s < 60) return s + 's ago';\n if (s < 3600) return Math.round(s / 60) + 'm ago';\n if (s < 86400) return Math.round(s / 3600) + 'h ago';\n return Math.round(s / 86400) + 'd ago';\n };\n for (const el of document.querySelectorAll('[data-relative-time]')) {\n el.textContent = rel(el.getAttribute('data-relative-time'));\n }\n\n const rows = Array.from(document.querySelectorAll('[data-findings] tr'));\n const search = document.querySelector('[data-search]');\n const empty = document.querySelector('[data-empty-findings]');\n const chips = Array.from(document.querySelectorAll('[data-filter]'));\n let activeSeverity = 'all';\n let query = '';\n\n const apply = () => {\n let visible = 0;\n for (const tr of rows) {\n const sev = tr.getAttribute('data-severity');\n const hay = tr.getAttribute('data-haystack') || '';\n const sevOk = activeSeverity === 'all' || sev === activeSeverity;\n const qOk = !query || hay.includes(query);\n const show = sevOk && qOk;\n tr.classList.toggle('is-hidden', !show);\n if (show) visible += 1;\n }\n if (empty)\n empty.classList.toggle('hidden', visible !== 0 || rows.length === 0);\n };\n\n for (const chip of chips) {\n const select = () => {\n activeSeverity = chip.getAttribute('data-filter') || 'all';\n for (const c of chips) c.classList.toggle('is-active', c === chip);\n apply();\n };\n chip.addEventListener('click', select);\n chip.addEventListener('keydown', (e) => {\n if (e.key === 'Enter' || e.key === ' ') {\n e.preventDefault();\n select();\n }\n });\n }\n\n if (search) {\n search.addEventListener('input', () => {\n query = search.value.trim().toLowerCase();\n apply();\n });\n }\n\n for (const btn of document.querySelectorAll('[data-copy]')) {\n btn.addEventListener('click', async () => {\n const value = btn.getAttribute('data-copy') || '';\n try {\n await navigator.clipboard.writeText(value);\n btn.classList.add('copied');\n btn.textContent = 'copied';\n setTimeout(() => {\n btn.classList.remove('copied');\n btn.textContent = 'copy';\n }, 1200);\n } catch {}\n });\n }\n\n document.addEventListener('keydown', (e) => {\n if (\n e.target &&\n (e.target.tagName === 'INPUT' ||\n e.target.tagName === 'TEXTAREA' ||\n e.target.tagName === 'SELECT')\n )\n return;\n if (e.key === 'r') {\n const link = document.querySelector('[data-refresh]');\n if (link) link.click();\n }\n if (e.key === '/' && search) {\n e.preventDefault();\n search.focus();\n }\n });\n})();\n";
|
|
@@ -239,4 +242,15 @@ const spectralPlugin = (options = {}) => {
|
|
|
239
242
|
return plugin;
|
|
240
243
|
};
|
|
241
244
|
//#endregion
|
|
242
|
-
|
|
245
|
+
//#region src/index.ts
|
|
246
|
+
const loadRuleset = async (input, baseDirOrOptions = process.cwd()) => {
|
|
247
|
+
return await (await import("./load-ruleset-CiikrzWx.mjs").then((n) => n.i)).loadRuleset(input, baseDirOrOptions);
|
|
248
|
+
};
|
|
249
|
+
const loadResolvedRuleset = async (input, baseDirOrOptions = process.cwd()) => {
|
|
250
|
+
return await (await import("./load-ruleset-CiikrzWx.mjs").then((n) => n.i)).loadResolvedRuleset(input, baseDirOrOptions);
|
|
251
|
+
};
|
|
252
|
+
const lintOpenApi = async (spec, ruleset) => {
|
|
253
|
+
return await (await import("./lint-openapi-D76sC7S5.mjs").then((n) => n.n)).lintOpenApi(spec, ruleset);
|
|
254
|
+
};
|
|
255
|
+
//#endregion
|
|
256
|
+
export { OpenApiLintArtifactWriteError, OpenApiLintThresholdError, RulesetLoadError, createOpenApiLintRuntime, enforceThreshold, lintOpenApi, loadResolvedRuleset, loadRuleset, presets, recommended, server, shouldFail, spectralPlugin, strict };
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { t as __exportAll } from "./rolldown-runtime-wcPFST8Q.mjs";
|
|
2
|
+
import { a as getSpectralConstructor, r as loadRuleset } from "./load-ruleset-CiikrzWx.mjs";
|
|
3
|
+
//#region src/core/finding-guidance.ts
|
|
4
|
+
const guidanceByCode = {
|
|
5
|
+
"elysia-operation-summary": "Add detail.summary to the Elysia route options so generated docs and clients have a short operation label.",
|
|
6
|
+
"elysia-operation-tags": "Add detail.tags with at least one stable tag, for example ['Users'] or ['Dev'].",
|
|
7
|
+
"operation-description": "Add detail.description with a short user-facing explanation of what the route does.",
|
|
8
|
+
"operation-tags": "Add a non-empty detail.tags array on the route so the OpenAPI operation is grouped consistently.",
|
|
9
|
+
"operation-operationId": "Add detail.operationId with a unique camelCase identifier so generated clients and SDKs have stable method names.",
|
|
10
|
+
"operation-success-response": "Add at least one 2xx response schema to the route, for example response: { 200: t.Object(...) }.",
|
|
11
|
+
"oas3-api-servers": "Add a servers array to the OpenAPI documentation config with at least one base URL.",
|
|
12
|
+
"info-contact": "Add an info.contact object to the OpenAPI documentation config with a name and url or email.",
|
|
13
|
+
"rfc9457-problem-details": "Add an \"application/problem+json\" content entry to the error response. See RFC 9457 for the Problem Details schema."
|
|
14
|
+
};
|
|
15
|
+
const getFindingRecommendation = (code, message) => {
|
|
16
|
+
const direct = guidanceByCode[code];
|
|
17
|
+
if (direct) return direct;
|
|
18
|
+
if (code === "oas3-schema" && message.includes("required property \"responses\"")) return "Add a response schema to the route, for example response: { 200: t.Object(...) } or response: { 200: t.Array(...) }.";
|
|
19
|
+
if (code.startsWith("operation-")) return "Add the missing operation metadata under detail on the Elysia route options.";
|
|
20
|
+
};
|
|
21
|
+
//#endregion
|
|
22
|
+
//#region src/core/normalize-findings.ts
|
|
23
|
+
const httpMethods = new Set([
|
|
24
|
+
"get",
|
|
25
|
+
"put",
|
|
26
|
+
"post",
|
|
27
|
+
"delete",
|
|
28
|
+
"options",
|
|
29
|
+
"head",
|
|
30
|
+
"patch",
|
|
31
|
+
"trace"
|
|
32
|
+
]);
|
|
33
|
+
const normalizeFindings = (diagnostics, spec) => {
|
|
34
|
+
const findings = diagnostics.map((diagnostic) => normalizeFinding(diagnostic, spec));
|
|
35
|
+
const summary = findings.reduce((current, finding) => {
|
|
36
|
+
current[finding.severity] += 1;
|
|
37
|
+
current.total += 1;
|
|
38
|
+
return current;
|
|
39
|
+
}, {
|
|
40
|
+
error: 0,
|
|
41
|
+
warn: 0,
|
|
42
|
+
info: 0,
|
|
43
|
+
hint: 0,
|
|
44
|
+
total: 0
|
|
45
|
+
});
|
|
46
|
+
return {
|
|
47
|
+
ok: summary.error === 0,
|
|
48
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
49
|
+
source: "manual",
|
|
50
|
+
failOn: "error",
|
|
51
|
+
durationMs: null,
|
|
52
|
+
summary,
|
|
53
|
+
findings
|
|
54
|
+
};
|
|
55
|
+
};
|
|
56
|
+
const normalizeFinding = (diagnostic, spec) => {
|
|
57
|
+
const path = [...diagnostic.path];
|
|
58
|
+
const finding = {
|
|
59
|
+
code: String(diagnostic.code),
|
|
60
|
+
message: diagnostic.message,
|
|
61
|
+
severity: toLintSeverity(diagnostic.severity),
|
|
62
|
+
path,
|
|
63
|
+
documentPointer: toDocumentPointer(path)
|
|
64
|
+
};
|
|
65
|
+
const recommendation = getFindingRecommendation(String(diagnostic.code), diagnostic.message);
|
|
66
|
+
if (recommendation) finding.recommendation = recommendation;
|
|
67
|
+
if (diagnostic.source !== void 0) finding.source = diagnostic.source;
|
|
68
|
+
if (diagnostic.range) finding.range = {
|
|
69
|
+
start: diagnostic.range.start,
|
|
70
|
+
end: diagnostic.range.end
|
|
71
|
+
};
|
|
72
|
+
const operation = inferOperation(path, spec);
|
|
73
|
+
if (operation) finding.operation = operation;
|
|
74
|
+
return finding;
|
|
75
|
+
};
|
|
76
|
+
const toLintSeverity = (severity) => {
|
|
77
|
+
switch (severity) {
|
|
78
|
+
case 0: return "error";
|
|
79
|
+
case 1: return "warn";
|
|
80
|
+
case 2: return "info";
|
|
81
|
+
default: return "hint";
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
const toDocumentPointer = (path) => {
|
|
85
|
+
if (path.length === 0) return "";
|
|
86
|
+
return `/${path.map((segment) => String(segment).replace(/~/g, "~0").replace(/\//g, "~1")).join("/")}`;
|
|
87
|
+
};
|
|
88
|
+
const inferOperation = (path, spec) => {
|
|
89
|
+
if (path[0] !== "paths") return;
|
|
90
|
+
const routePath = typeof path[1] === "string" ? path[1] : void 0;
|
|
91
|
+
const method = typeof path[2] === "string" && httpMethods.has(path[2]) ? path[2] : void 0;
|
|
92
|
+
if (!routePath && !method) return;
|
|
93
|
+
const operationRecord = routePath && method ? getNestedValue(spec, [
|
|
94
|
+
"paths",
|
|
95
|
+
routePath,
|
|
96
|
+
method
|
|
97
|
+
]) : void 0;
|
|
98
|
+
const operation = {};
|
|
99
|
+
if (routePath !== void 0) operation.path = routePath;
|
|
100
|
+
if (method !== void 0) operation.method = method;
|
|
101
|
+
if (operationRecord && typeof operationRecord === "object" && "operationId" in operationRecord) operation.operationId = String(operationRecord.operationId);
|
|
102
|
+
return operation;
|
|
103
|
+
};
|
|
104
|
+
const getNestedValue = (value, path) => {
|
|
105
|
+
let current = value;
|
|
106
|
+
for (const segment of path) {
|
|
107
|
+
if (current === null || typeof current !== "object") return;
|
|
108
|
+
current = current[segment];
|
|
109
|
+
}
|
|
110
|
+
return current;
|
|
111
|
+
};
|
|
112
|
+
//#endregion
|
|
113
|
+
//#region src/core/lint-openapi.ts
|
|
114
|
+
var lint_openapi_exports = /* @__PURE__ */ __exportAll({ lintOpenApi: () => lintOpenApi });
|
|
115
|
+
const lintOpenApi = async (spec, ruleset) => {
|
|
116
|
+
const [Spectral, resolvedRuleset] = await Promise.all([getSpectralConstructor(), loadRuleset(ruleset, { baseDir: process.cwd() })]);
|
|
117
|
+
const spectral = new Spectral();
|
|
118
|
+
spectral.setRuleset(resolvedRuleset);
|
|
119
|
+
return normalizeFindings(await spectral.run(spec, { ignoreUnknownFormat: false }), spec);
|
|
120
|
+
};
|
|
121
|
+
//#endregion
|
|
122
|
+
export { lint_openapi_exports as n, lintOpenApi as t };
|