@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 +7 -0
- package/README.md +42 -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-4LlfDIZv.mjs} +13 -570
- package/package.json +1 -1
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
|
-
|
|
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
|
|
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
|
|
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).
|
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-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,
|
|
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-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
|
-
|
|
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 };
|