@opsydyn/elysia-spectral 1.5.0 → 1.5.1

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 CHANGED
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.5.1](https://github.com/opsydyn/elysia-spectral/compare/v1.5.0...v1.5.1) (2026-05-12)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * harden spectral import startup path ([024fedf](https://github.com/opsydyn/elysia-spectral/commit/024fedf7fe294fc7919f9d17d7727029e2a04fae))
9
+
3
10
  ## [1.5.0](https://github.com/opsydyn/elysia-spectral/compare/v1.4.0...v1.5.0) (2026-04-28)
4
11
 
5
12
 
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.
@@ -109,6 +111,8 @@ bun add elysia @elysiajs/openapi @opsydyn/elysia-spectral
109
111
  npm install elysia @elysiajs/openapi @opsydyn/elysia-spectral
110
112
  ```
111
113
 
114
+ `@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.
115
+
112
116
  2. Add `@elysiajs/openapi` and `spectralPlugin` to your app with the `strict` preset.
113
117
 
114
118
  ```ts
@@ -217,6 +221,34 @@ spectralPlugin({ ruleset: strict })
217
221
 
218
222
  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
223
 
224
+ ### Provide an OpenAPI JSON route
225
+
226
+ `@opsydyn/elysia-spectral` needs a public OpenAPI JSON document to lint. By default it looks for `/openapi/json` via `app.handle(new Request(...))`.
227
+
228
+ The recommended setup is to mount `@elysiajs/openapi`:
229
+
230
+ ```ts
231
+ import { Elysia } from 'elysia'
232
+ import { openapi } from '@elysiajs/openapi'
233
+ import { spectralPlugin } from '@opsydyn/elysia-spectral'
234
+
235
+ new Elysia()
236
+ .use(openapi())
237
+ .use(spectralPlugin())
238
+ ```
239
+
240
+ If your app exposes the OpenAPI JSON document somewhere else, point the plugin at that route:
241
+
242
+ ```ts
243
+ spectralPlugin({
244
+ source: {
245
+ specPath: '/docs/openapi.json'
246
+ }
247
+ })
248
+ ```
249
+
250
+ 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.
251
+
220
252
  ## How-to Guides
221
253
 
222
254
  ### Use a repo-level ruleset
@@ -386,9 +418,10 @@ What it surfaces:
386
418
 
387
419
  Keyboard shortcuts: `r` re-runs, `/` focuses the filter, `Enter`/`Space` toggles the focused severity chip.
388
420
 
389
- Three dark themes ship with the dashboard, selectable from the header dropdown and persisted per-browser via `localStorage`:
421
+ Four dark themes ship with the dashboard, selectable from the header dropdown and persisted per-browser via `localStorage`:
390
422
 
391
423
  - **Astro Houston** (default) — purple / blue gradient
424
+ - **Elysia** — pink on deep purple, sourced from the Scalar palette
392
425
  - **Tron Legacy** — cyan neon on near-black
393
426
  - **Detroit 808** — TR-808 amber, red, and yellow
394
427
 
@@ -523,6 +556,8 @@ Use `'warn'` for local development and `'error'` when artifact generation is req
523
556
 
524
557
  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
558
 
559
+ Before using the runtime, make sure the app instance mounts an OpenAPI generator (typically `@elysiajs/openapi`) or otherwise serves a reachable OpenAPI JSON route.
560
+
526
561
  Create a script at `scripts/lint-openapi.ts`:
527
562
 
528
563
  ```ts
@@ -760,7 +795,9 @@ type SpectralPluginOptions = {
760
795
  sinks?: OpenApiLintSink[]
761
796
  }
762
797
  source?: {
798
+ /** Public OpenAPI JSON route. Defaults to '/openapi/json'. Mount @elysiajs/openapi or serve an equivalent route yourself. */
763
799
  specPath?: string
800
+ /** Optional base URL fallback when the OpenAPI document is only reachable over HTTP. */
764
801
  baseUrl?: string
765
802
  }
766
803
  /**
@@ -1000,7 +1037,7 @@ Example successful response:
1000
1037
  - startup mode `enforce` throws on threshold failures
1001
1038
  - startup mode `report` prints the same lint report but allows boot to continue on threshold failures
1002
1039
  - startup mode `off` skips startup lint
1003
- - bad `source.specPath` or invalid spec JSON produces an actionable provider error
1040
+ - missing OpenAPI generator / missing OpenAPI JSON route, bad `source.specPath`, or invalid spec JSON produces an actionable provider error
1004
1041
  - artifact writes warn by default and can be made fatal with `output.artifactWriteFailures: 'error'`
1005
1042
 
1006
1043
  ### Output model
@@ -1022,6 +1059,8 @@ The convenience options compile down to built-in sinks so the current API stays
1022
1059
 
1023
1060
  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
1061
 
1062
+ `@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.
1063
+
1025
1064
  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
1065
 
1027
1066
  ### Why startup and healthcheck are separate
@@ -1057,4 +1096,4 @@ Production-grade linting needs more than a pass/fail boolean. The runtime tracks
1057
1096
 
1058
1097
  ### Project status
1059
1098
 
1060
- The package is actively developed toward a stable `v1`. Milestones 0.2 through 0.6 are complete. Ongoing work is tracked in [roadmap.md](../../roadmap.md).
1099
+ The package is published to npm and used in production. Ongoing work additional rules, deeper Elysia integrations, and dashboard polish — is tracked in [roadmap.md](../../roadmap.md).
@@ -1,2 +1,2 @@
1
- import { a as createOpenApiLintRuntime, c as ResolvedRulesetCandidate, d as RulesetResolverContext, f as RulesetResolverInput, g as lintOpenApi, h as loadRuleset, i as OpenApiLintArtifactWriteError, l as RulesetLoadError, m as loadResolvedRuleset, n as enforceThreshold, o as LoadResolvedRulesetOptions, p as defaultRulesetResolvers, r as shouldFail, s as LoadedRuleset, t as OpenApiLintThresholdError, u as RulesetResolver } from "../index-CyJXdIRT.mjs";
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 };
@@ -1,2 +1,5 @@
1
- import { a as enforceThreshold, d as RulesetLoadError, f as defaultRulesetResolvers, g as lintOpenApi, i as OpenApiLintThresholdError, m as loadRuleset, n as createOpenApiLintRuntime, o as shouldFail, p as loadResolvedRuleset, t as OpenApiLintArtifactWriteError } from "../core-BLJeXQ15.mjs";
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-4LlfDIZv.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, ResolvedRulesetCandidate as c, RulesetResolverContext as d, RulesetResolverInput as f, lintOpenApi as g, loadRuleset as h, OpenApiLintArtifactWriteError as i, SpectralPluginOptions as j, SeverityThreshold as k, RulesetLoadError as l, loadResolvedRuleset as m, enforceThreshold as n, LoadResolvedRulesetOptions as o, defaultRulesetResolvers as p, shouldFail as r, LoadedRuleset as s, OpenApiLintThresholdError as t, RulesetResolver as u, LintFinding as v, OpenApiLintRuntimeFailure as w, LintSeverity as x, LintRunResult as y };
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 ResolvedRulesetCandidate, d as RulesetResolverContext, f as RulesetResolverInput, g as lintOpenApi, h as loadRuleset, i as OpenApiLintArtifactWriteError, j as SpectralPluginOptions, k as SeverityThreshold, l as RulesetLoadError, m as loadResolvedRuleset, n as enforceThreshold, o as LoadResolvedRulesetOptions, p as defaultRulesetResolvers, r as shouldFail, s as LoadedRuleset, t as OpenApiLintThresholdError, u as RulesetResolver, v as LintFinding, w as OpenApiLintRuntimeFailure, x as LintSeverity, y as LintRunResult } from "./index-CyJXdIRT.mjs";
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
- export { ArtifactWriteFailureMode, LintFinding, LintRunResult, LintRunSource, LintSeverity, LoadResolvedRulesetOptions, LoadedRuleset, OpenApiLintArtifactWriteError, OpenApiLintArtifacts, OpenApiLintRuntime, OpenApiLintRuntimeFailure, OpenApiLintRuntimeStatus, OpenApiLintSink, OpenApiLintSinkContext, OpenApiLintThresholdError, PresetName, ResolvedRulesetCandidate, RulesetLoadError, RulesetResolver, RulesetResolverContext, RulesetResolverInput, SeverityThreshold, SpectralLogger, SpectralPluginOptions, StartupLintMode, createOpenApiLintRuntime, defaultRulesetResolvers, enforceThreshold, lintOpenApi, loadResolvedRuleset, loadRuleset, presets, recommended, server, shouldFail, spectralPlugin, strict };
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 { a as enforceThreshold, c as strict, d as RulesetLoadError, f as defaultRulesetResolvers, g as lintOpenApi, h as recommended, i as OpenApiLintThresholdError, l as server, m as loadRuleset, n as createOpenApiLintRuntime, o as shouldFail, p as loadResolvedRuleset, r as resolveStartupMode, s as presets, t as OpenApiLintArtifactWriteError, u as resolveReporter } from "./core-BLJeXQ15.mjs";
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-4LlfDIZv.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
- export { OpenApiLintArtifactWriteError, OpenApiLintThresholdError, RulesetLoadError, createOpenApiLintRuntime, defaultRulesetResolvers, enforceThreshold, lintOpenApi, loadResolvedRuleset, loadRuleset, presets, recommended, server, shouldFail, spectralPlugin, strict };
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 };
@@ -0,0 +1,301 @@
1
+ import { t as __exportAll } from "./rolldown-runtime-wcPFST8Q.mjs";
2
+ import { t as RulesetLoadError } from "./ruleset-load-error-CogUOC7W.mjs";
3
+ import { t as recommended } from "./recommended-DgrTqq-3.mjs";
4
+ import path from "node:path";
5
+ import { access, readFile } from "node:fs/promises";
6
+ import YAML from "yaml";
7
+ import { pathToFileURL } from "node:url";
8
+ //#region src/rulesets/default-ruleset.ts
9
+ const defaultRuleset = recommended;
10
+ //#endregion
11
+ //#region src/core/stoplight-runtime.ts
12
+ let runtimeBindingsPromise = null;
13
+ const loadStoplightRuntimeBindings = async () => {
14
+ const [spectralCore, { default: spectralFunctions }, { default: spectralRulesets }] = await Promise.all([
15
+ import("@stoplight/spectral-core"),
16
+ import("@stoplight/spectral-functions"),
17
+ import("@stoplight/spectral-rulesets")
18
+ ]);
19
+ const { alphabetical, casing, defined, enumeration, falsy, length, or, pattern, schema, truthy, undefined: undefinedFunction, unreferencedReusableObject, xor } = spectralFunctions;
20
+ const { oas } = spectralRulesets;
21
+ return {
22
+ Spectral: spectralCore.Spectral,
23
+ extendsMap: { "spectral:oas": oas },
24
+ functionMap: {
25
+ alphabetical,
26
+ casing,
27
+ defined,
28
+ enumeration,
29
+ falsy,
30
+ length,
31
+ or,
32
+ pattern,
33
+ schema,
34
+ truthy,
35
+ undefined: undefinedFunction,
36
+ unreferencedReusableObject,
37
+ xor
38
+ }
39
+ };
40
+ };
41
+ const getStoplightRuntimeBindings = async () => {
42
+ if (runtimeBindingsPromise === null) runtimeBindingsPromise = loadStoplightRuntimeBindings();
43
+ return await runtimeBindingsPromise;
44
+ };
45
+ const getBuiltInFunctionMap = async () => (await getStoplightRuntimeBindings()).functionMap;
46
+ const getExtendsMap = async () => (await getStoplightRuntimeBindings()).extendsMap;
47
+ const getSpectralConstructor = async () => (await getStoplightRuntimeBindings()).Spectral;
48
+ //#endregion
49
+ //#region src/core/load-ruleset.ts
50
+ var load_ruleset_exports = /* @__PURE__ */ __exportAll({
51
+ defaultRulesetResolvers: () => defaultRulesetResolvers,
52
+ loadResolvedRuleset: () => loadResolvedRuleset,
53
+ loadRuleset: () => loadRuleset
54
+ });
55
+ const autodiscoverRulesetFilenames = [
56
+ "spectral.yaml",
57
+ "spectral.yml",
58
+ "spectral.ts",
59
+ "spectral.mts",
60
+ "spectral.cts",
61
+ "spectral.js",
62
+ "spectral.mjs",
63
+ "spectral.cjs",
64
+ "spectral.config.yaml",
65
+ "spectral.config.yml",
66
+ "spectral.config.ts",
67
+ "spectral.config.mts",
68
+ "spectral.config.cts",
69
+ "spectral.config.js",
70
+ "spectral.config.mjs",
71
+ "spectral.config.cjs"
72
+ ];
73
+ const loadRuleset = async (input, baseDirOrOptions = process.cwd()) => {
74
+ return (await loadResolvedRuleset(input, baseDirOrOptions)).ruleset;
75
+ };
76
+ const loadResolvedRuleset = async (input, baseDirOrOptions = process.cwd()) => {
77
+ const options = normalizeLoadResolvedRulesetOptions(baseDirOrOptions);
78
+ const context = {
79
+ baseDir: options.baseDir,
80
+ defaultRuleset: options.defaultRuleset,
81
+ mergeAutodiscoveredWithDefault: options.mergeAutodiscoveredWithDefault
82
+ };
83
+ for (const resolver of options.resolvers) {
84
+ const loaded = await resolver(input, context);
85
+ if (loaded) {
86
+ const normalized = { ruleset: await normalizeRulesetDefinition(loaded.ruleset) };
87
+ if (loaded.source) normalized.source = loaded.source;
88
+ return normalized;
89
+ }
90
+ }
91
+ if (input === void 0) return { ruleset: await normalizeRulesetDefinition(options.defaultRuleset) };
92
+ throw new RulesetLoadError("Ruleset input could not be resolved.");
93
+ };
94
+ const normalizeLoadResolvedRulesetOptions = (value) => {
95
+ if (typeof value === "string") return {
96
+ baseDir: value,
97
+ resolvers: defaultRulesetResolvers,
98
+ mergeAutodiscoveredWithDefault: true,
99
+ defaultRuleset
100
+ };
101
+ return {
102
+ baseDir: value.baseDir ?? process.cwd(),
103
+ resolvers: value.resolvers ?? defaultRulesetResolvers,
104
+ mergeAutodiscoveredWithDefault: value.mergeAutodiscoveredWithDefault ?? true,
105
+ defaultRuleset: value.defaultRuleset ?? defaultRuleset
106
+ };
107
+ };
108
+ const resolveAutodiscoveredRuleset = async (input, context) => {
109
+ if (input !== void 0) return;
110
+ const autodiscoveredPath = await findAutodiscoveredRulesetPath(context.baseDir);
111
+ if (!autodiscoveredPath) return;
112
+ const loaded = await loadResolvedPathRuleset(autodiscoveredPath, context);
113
+ if (!context.mergeAutodiscoveredWithDefault) return {
114
+ ...loaded,
115
+ source: {
116
+ path: autodiscoveredPath,
117
+ autodiscovered: true,
118
+ mergedWithDefault: false
119
+ }
120
+ };
121
+ return {
122
+ ruleset: mergeRulesets(context.defaultRuleset, loaded.ruleset),
123
+ source: {
124
+ path: autodiscoveredPath,
125
+ autodiscovered: true,
126
+ mergedWithDefault: true
127
+ }
128
+ };
129
+ };
130
+ const resolvePathRuleset = async (input, context) => {
131
+ if (typeof input !== "string") return;
132
+ return await loadResolvedPathRuleset(input, context);
133
+ };
134
+ const resolveInlineRuleset = async (input) => {
135
+ if (input === void 0 || typeof input === "string") return;
136
+ return { ruleset: input };
137
+ };
138
+ const defaultRulesetResolvers = [
139
+ resolveAutodiscoveredRuleset,
140
+ resolvePathRuleset,
141
+ resolveInlineRuleset
142
+ ];
143
+ const loadResolvedPathRuleset = async (inputPath, context) => {
144
+ const resolvedPath = path.resolve(context.baseDir, inputPath);
145
+ if (isYamlRulesetPath(resolvedPath)) return {
146
+ ruleset: await loadYamlRuleset(resolvedPath),
147
+ source: {
148
+ path: inputPath,
149
+ autodiscovered: false,
150
+ mergedWithDefault: false
151
+ }
152
+ };
153
+ if (!isModuleRulesetPath(resolvedPath)) throw new RulesetLoadError(`Unsupported ruleset path: ${inputPath}. Supported local rulesets are .yaml, .yml, .js, .mjs, .cjs, .ts, .mts, and .cts.`);
154
+ return {
155
+ ruleset: await loadModuleRuleset(resolvedPath),
156
+ source: {
157
+ path: inputPath,
158
+ autodiscovered: false,
159
+ mergedWithDefault: false
160
+ }
161
+ };
162
+ };
163
+ const findAutodiscoveredRulesetPath = async (baseDir) => {
164
+ for (const filename of autodiscoverRulesetFilenames) {
165
+ const candidatePath = path.resolve(baseDir, filename);
166
+ try {
167
+ await access(candidatePath);
168
+ return `./${filename}`;
169
+ } catch (error) {
170
+ if (error.code !== "ENOENT") throw error;
171
+ }
172
+ }
173
+ };
174
+ const loadYamlRuleset = async (resolvedPath) => {
175
+ let fileContents;
176
+ try {
177
+ fileContents = await readFile(resolvedPath, "utf8");
178
+ } catch (error) {
179
+ throw new RulesetLoadError(`Unable to read ruleset at ${resolvedPath}.`, { cause: error });
180
+ }
181
+ let parsed;
182
+ try {
183
+ parsed = YAML.parse(fileContents);
184
+ } catch (error) {
185
+ throw new RulesetLoadError(`Unable to parse YAML ruleset at ${resolvedPath}.`, { cause: error });
186
+ }
187
+ return parsed;
188
+ };
189
+ const loadModuleRuleset = async (resolvedPath) => {
190
+ let imported;
191
+ try {
192
+ imported = await import(pathToFileURL(resolvedPath).href);
193
+ } catch (error) {
194
+ throw new RulesetLoadError(`Unable to import module ruleset at ${resolvedPath}.`, { cause: error });
195
+ }
196
+ const resolvedRuleset = resolveModuleRulesetValue(imported);
197
+ if (resolvedRuleset === void 0) throw new RulesetLoadError(`Module ruleset at ${resolvedPath} must export a ruleset as the default export or a named "ruleset" export.`);
198
+ return await normalizeRulesetDefinition(resolvedRuleset, {
199
+ ...await getBuiltInFunctionMap(),
200
+ ...resolveModuleFunctions(imported)
201
+ });
202
+ };
203
+ const resolveModuleRulesetValue = (imported) => {
204
+ if (!isRecord(imported)) return;
205
+ if ("default" in imported) return imported.default;
206
+ if ("ruleset" in imported) return imported.ruleset;
207
+ };
208
+ const resolveModuleFunctions = (imported) => {
209
+ if (!isRecord(imported) || !("functions" in imported)) return {};
210
+ const { functions } = imported;
211
+ if (!isRecord(functions)) throw new RulesetLoadError("Module ruleset \"functions\" export must be an object map of function names to Spectral functions.");
212
+ const entries = Object.entries(functions).filter(([, value]) => typeof value === "function");
213
+ return Object.fromEntries(entries);
214
+ };
215
+ const isYamlRulesetPath = (value) => value.endsWith(".yaml") || value.endsWith(".yml");
216
+ const isModuleRulesetPath = (value) => value.endsWith(".js") || value.endsWith(".mjs") || value.endsWith(".cjs") || value.endsWith(".ts") || value.endsWith(".mts") || value.endsWith(".cts");
217
+ const normalizeRulesetDefinition = async (input, availableFunctions) => {
218
+ if (!isRecord(input)) throw new RulesetLoadError("Ruleset must be an object.");
219
+ const resolvedAvailableFunctions = availableFunctions ?? await getBuiltInFunctionMap();
220
+ const normalized = { ...input };
221
+ if ("extends" in normalized) normalized.extends = await normalizeExtends(normalized.extends);
222
+ if ("rules" in normalized) normalized.rules = await normalizeRules(normalized.rules, resolvedAvailableFunctions);
223
+ return normalized;
224
+ };
225
+ const mergeRuleEntry = (base, override) => {
226
+ if (!isRecord(override)) return override;
227
+ if ("given" in override || "then" in override) return override;
228
+ if (isRecord(base) && ("given" in base || "then" in base)) return {
229
+ ...base,
230
+ ...override
231
+ };
232
+ const keys = Object.keys(override);
233
+ if (keys.length === 1 && keys[0] === "severity") return override.severity;
234
+ return override;
235
+ };
236
+ const mergeRulesets = (baseRuleset, overrideRuleset) => {
237
+ const mergedBase = baseRuleset;
238
+ const mergedOverride = overrideRuleset;
239
+ const baseRules = isRecord(mergedBase.rules) ? mergedBase.rules : {};
240
+ const overrideRules = isRecord(mergedOverride.rules) ? mergedOverride.rules : {};
241
+ const mergedRules = { ...baseRules };
242
+ for (const [name, overrideRule] of Object.entries(overrideRules)) mergedRules[name] = mergeRuleEntry(baseRules[name], overrideRule);
243
+ const baseExtends = toExtendsArray(mergedBase.extends);
244
+ const overrideExtends = toExtendsArray(mergedOverride.extends);
245
+ const mergedExtends = [...baseExtends, ...overrideExtends];
246
+ const merged = {
247
+ ...mergedBase,
248
+ ...mergedOverride
249
+ };
250
+ delete merged.extends;
251
+ delete merged.rules;
252
+ if (mergedExtends.length > 0) merged.extends = mergedExtends;
253
+ if (Object.keys(mergedRules).length > 0) merged.rules = mergedRules;
254
+ return merged;
255
+ };
256
+ const toExtendsArray = (value) => {
257
+ if (value === void 0) return [];
258
+ return Array.isArray(value) ? [...value] : [value];
259
+ };
260
+ const normalizeExtends = async (value) => {
261
+ if (typeof value === "string") return await resolveExtendsEntry(value);
262
+ if (!Array.isArray(value)) return value;
263
+ return await Promise.all(value.map(async (entry) => {
264
+ if (typeof entry === "string") return await resolveExtendsEntry(entry);
265
+ if (Array.isArray(entry) && entry.length >= 1 && typeof entry[0] === "string") return [await resolveExtendsEntry(entry[0]), entry[1]];
266
+ return entry;
267
+ }));
268
+ };
269
+ const resolveExtendsEntry = async (value) => {
270
+ const resolved = (await getExtendsMap())[value];
271
+ if (!resolved) throw new RulesetLoadError(`Unsupported ruleset extend target: "${value}". Supported extend targets: spectral:oas.`);
272
+ return resolved;
273
+ };
274
+ const normalizeRules = async (value, availableFunctions) => {
275
+ if (!isRecord(value)) return value;
276
+ const entries = await Promise.all(Object.entries(value).map(async ([ruleName, ruleValue]) => [ruleName, await normalizeRule(ruleValue, availableFunctions)]));
277
+ return Object.fromEntries(entries);
278
+ };
279
+ const normalizeRule = async (value, availableFunctions) => {
280
+ if (!isRecord(value)) return value;
281
+ const normalized = { ...value };
282
+ if ("then" in normalized) normalized.then = await normalizeThen(normalized.then, availableFunctions);
283
+ return normalized;
284
+ };
285
+ const normalizeThen = async (value, availableFunctions) => {
286
+ if (Array.isArray(value)) return await Promise.all(value.map((entry) => normalizeThenEntry(entry, availableFunctions)));
287
+ return await normalizeThenEntry(value, availableFunctions);
288
+ };
289
+ const normalizeThenEntry = async (value, availableFunctions) => {
290
+ if (!isRecord(value)) return value;
291
+ const normalized = { ...value };
292
+ if (typeof normalized.function === "string") {
293
+ const resolved = availableFunctions[normalized.function];
294
+ if (!resolved) throw new RulesetLoadError(`Unsupported Spectral function: ${String(normalized.function)}.`);
295
+ normalized.function = resolved;
296
+ }
297
+ return normalized;
298
+ };
299
+ const isRecord = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
300
+ //#endregion
301
+ export { getSpectralConstructor as a, load_ruleset_exports as i, loadResolvedRuleset as n, loadRuleset as r, defaultRulesetResolvers as t };