@opsydyn/elysia-spectral 1.4.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 +14 -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 +19 -4
- 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,19 @@
|
|
|
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
|
+
|
|
10
|
+
## [1.5.0](https://github.com/opsydyn/elysia-spectral/compare/v1.4.0...v1.5.0) (2026-04-28)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Features
|
|
14
|
+
|
|
15
|
+
* **dashboard:** add Elysia theme using Scalar palette ([58eb58f](https://github.com/opsydyn/elysia-spectral/commit/58eb58fcc04fe48bfce49380a128fd0507aecdcf))
|
|
16
|
+
|
|
3
17
|
## [1.4.0](https://github.com/opsydyn/elysia-spectral/compare/v1.3.1...v1.4.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.
|
|
@@ -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,10 +1,13 @@
|
|
|
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
|
-
var _inline_text_3_default = "(() => {\n const THEME_KEY = 'elysia-spectral-theme';\n const THEMES = ['astro', '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";
|
|
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";
|
|
5
8
|
//#endregion
|
|
6
9
|
//#region \0inline-text:2.mjs
|
|
7
|
-
var _inline_text_2_default = ":root {\n color-scheme: dark;\n --bg: #0e1018;\n --fg: #eef0f9;\n --muted: #8a93a0;\n --line: #262a33;\n --surface: #161922;\n --surface-2: #1d2027;\n --accent: #ad5dca;\n --accent-soft: #2b7eca;\n --accent-2: #2dd4bf;\n --pass: #23d18b;\n --fail: #dc3657;\n --warn: #ffc368;\n --info: #54b9ff;\n --hint: #545864;\n --grid-line: rgba(255, 255, 255, 0.04);\n --grid-size: 2rem;\n --mono:\n \"IBM Plex Mono\", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,\n \"Liberation Mono\", \"Courier New\", monospace;\n --display: \"Bangers\", var(--mono);\n}\n\n[data-theme=\"tron\"] {\n --bg: #06080d;\n --fg: #e6f7ff;\n --muted: #5d8aa3;\n --line: #0e2c3d;\n --surface: #0a1320;\n --surface-2: #0d1a2b;\n --accent: #38f3ff;\n --accent-soft: #1ea7ff;\n --accent-2: #38f3ff;\n --pass: #7de8ff;\n --fail: #ff3c5f;\n --warn: #a8f5ff;\n --info: #73d8ff;\n --hint: #273746;\n --grid-line: rgba(56, 243, 255, 0.06);\n}\n\n[data-theme=\"808\"] {\n --bg: #1a1a1a;\n --fg: #ffffff;\n --muted: #9a958a;\n --line: #2a2a2a;\n --surface: #222222;\n --surface-2: #2a2a2a;\n --accent: #f8a125;\n --accent-soft: #e72e2e;\n --accent-2: #f1f827;\n --pass: #f1f827;\n --fail: #e72e2e;\n --warn: #f8a125;\n --info: #f1f827;\n --hint: #404040;\n --grid-line: rgba(248, 161, 37, 0.06);\n}\n\n* {\n box-sizing: border-box;\n}\n\nhtml {\n background:\n radial-gradient(\n circle at top left,\n color-mix(in srgb, var(--accent) 14%, transparent),\n transparent 32%\n ),\n radial-gradient(\n circle at top right,\n color-mix(in srgb, var(--accent-2) 10%, transparent),\n transparent 30%\n ),\n var(--bg);\n min-height: 100%;\n}\n\nbody {\n margin: 0;\n font-family: var(--mono);\n background:\n linear-gradient(\n 180deg,\n color-mix(in srgb, var(--bg) 92%, transparent),\n transparent 16rem\n ),\n linear-gradient(90deg, var(--grid-line) 1px, transparent 1px),\n linear-gradient(var(--grid-line) 1px, transparent 1px), transparent;\n background-size:\n auto,\n var(--grid-size) var(--grid-size),\n var(--grid-size) var(--grid-size),\n auto;\n background-position:\n 0 0,\n center top,\n center top,\n 0 0;\n color: var(--fg);\n font-size: 13px;\n line-height: 1.55;\n}\n\nheader {\n position: sticky;\n top: 0;\n z-index: 10;\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 18px 24px;\n background: color-mix(in srgb, var(--bg) 82%, transparent);\n backdrop-filter: blur(16px);\n box-shadow: 0 1px 0 color-mix(in srgb, var(--fg) 8%, transparent);\n overflow: hidden;\n}\n\nheader::before {\n content: \"\";\n position: absolute;\n inset: 0;\n z-index: 0;\n background:\n radial-gradient(900px 220px at 12% -30%, var(--accent), transparent 60%),\n radial-gradient(\n 700px 200px at 88% -20%,\n var(--accent-soft),\n transparent 65%\n );\n opacity: 0.35;\n pointer-events: none;\n}\n\nheader::after {\n content: \"\";\n position: absolute;\n left: 0;\n right: 0;\n bottom: 0;\n height: 1px;\n background: linear-gradient(\n 90deg,\n transparent,\n color-mix(in srgb, var(--accent) 55%, transparent),\n transparent\n );\n}\n\nheader > * {\n position: relative;\n z-index: 1;\n}\n\n.brand {\n display: flex;\n align-items: center;\n gap: 12px;\n}\n\n.dot {\n width: 10px;\n height: 10px;\n background: linear-gradient(135deg, var(--accent), var(--accent-2));\n box-shadow:\n 0 0 0 3px color-mix(in srgb, var(--accent) 18%, transparent),\n 0 0 18px color-mix(in srgb, var(--accent) 60%, transparent);\n}\n\nh1 {\n margin: 0;\n font-family: var(--display);\n font-size: 22px;\n font-weight: 400;\n letter-spacing: 0.06em;\n text-transform: uppercase;\n text-shadow: 0.04em 0.04em 0\n color-mix(in srgb, var(--accent) 22%, transparent);\n}\n\n.actions {\n display: flex;\n align-items: center;\n gap: 10px;\n}\n\n.visually-hidden {\n position: absolute;\n width: 1px;\n height: 1px;\n overflow: hidden;\n clip: rect(0 0 0 0);\n}\n\n.theme-switch {\n display: inline-flex;\n align-items: center;\n gap: 8px;\n padding: 4px 10px;\n border: 1px solid var(--line);\n background: color-mix(in srgb, var(--surface) 85%, transparent);\n}\n\n.theme-label {\n font-size: 10px;\n text-transform: uppercase;\n letter-spacing: 0.12em;\n color: var(--muted);\n font-weight: 600;\n}\n\n.theme-switch select {\n font-family: var(--mono);\n font-size: 11px;\n padding: 4px 22px 4px 8px;\n border: 1px solid var(--line);\n border-radius: 0;\n background: var(--surface-2)\n url(\"data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6'><path fill='none' stroke='%238a93a0' stroke-width='1.4' d='M1 1l4 4 4-4'/></svg>\")\n no-repeat right 8px center;\n color: var(--fg);\n -webkit-appearance: none;\n appearance: none;\n cursor: pointer;\n}\n.theme-switch select:hover {\n border-color: var(--accent);\n}\n.theme-switch select:focus {\n outline: none;\n border-color: var(--accent);\n}\n\nkbd {\n font-family: var(--mono);\n font-size: 11px;\n padding: 3px 7px;\n border: 1px solid var(--line);\n border-bottom-width: 2px;\n border-radius: 0;\n color: var(--muted);\n background: var(--surface);\n}\n\n.refresh {\n color: var(--fg);\n text-decoration: none;\n padding: 7px 14px;\n border: 1px solid var(--line);\n border-radius: 0;\n font-size: 12px;\n font-family: var(--mono);\n font-weight: 600;\n letter-spacing: 0.04em;\n text-transform: uppercase;\n background: var(--surface);\n box-shadow: 0 6px 18px rgba(0, 0, 0, 0.25);\n transition:\n border-color 0.15s,\n transform 0.05s,\n color 0.15s;\n}\n.refresh:hover {\n border-color: var(--accent);\n color: var(--accent);\n}\n.refresh:active {\n transform: translateY(1px);\n}\n\nmain {\n padding: 28px 24px 64px;\n max-width: 1100px;\n margin: 0 auto;\n}\n\n.banner {\n position: relative;\n padding: 16px 18px;\n border-radius: 0;\n margin-bottom: 20px;\n font-size: 13px;\n border: 1px solid var(--line);\n background: linear-gradient(\n 180deg,\n color-mix(in srgb, var(--surface) 80%, transparent),\n color-mix(in srgb, var(--bg) 92%, transparent)\n );\n box-shadow: 0 18px 44px rgba(0, 0, 0, 0.28);\n}\n.banner::before {\n content: \"\";\n position: absolute;\n left: 0;\n top: 0;\n bottom: 0;\n width: 3px;\n background: var(--accent);\n}\n.banner-pass::before {\n background: var(--pass);\n}\n.banner-fail::before {\n background: var(--fail);\n}\n.banner-pass {\n border-color: color-mix(in srgb, var(--pass) 45%, var(--line));\n}\n.banner-fail {\n border-color: color-mix(in srgb, var(--fail) 45%, var(--line));\n}\n.banner strong {\n font-family: var(--display);\n font-weight: 400;\n letter-spacing: 0.08em;\n text-transform: uppercase;\n font-size: 16px;\n margin-right: 6px;\n}\n.tagline {\n display: block;\n margin-top: 8px;\n font-weight: 700;\n letter-spacing: 0.16em;\n color: var(--pass);\n text-transform: uppercase;\n font-size: 11px;\n}\n\n.meta {\n display: grid;\n grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));\n gap: 10px;\n margin: 0 0 20px;\n padding: 0;\n}\n.meta div {\n background: linear-gradient(\n 180deg,\n color-mix(in srgb, var(--surface) 90%, var(--accent) 10%),\n color-mix(in srgb, var(--bg) 96%, transparent)\n );\n border: 1px solid var(--line);\n border-radius: 0;\n padding: 10px 14px;\n box-shadow: 0 6px 18px rgba(0, 0, 0, 0.22);\n}\n.meta dt {\n color: var(--muted);\n font-size: 10px;\n text-transform: uppercase;\n letter-spacing: 0.1em;\n margin-bottom: 4px;\n font-weight: 600;\n}\n.meta dd {\n margin: 0;\n font-size: 12px;\n}\n.muted-line {\n display: block;\n color: var(--muted);\n font-size: 11px;\n margin-top: 2px;\n}\n\n.summary {\n list-style: none;\n padding: 0;\n margin: 0 0 28px;\n display: flex;\n gap: 8px;\n flex-wrap: wrap;\n}\n.summary li {\n padding: 7px 14px;\n border: 1px solid var(--line);\n border-radius: 0;\n font-size: 12px;\n color: var(--muted);\n cursor: pointer;\n user-select: none;\n background: color-mix(in srgb, var(--surface) 80%, transparent);\n transition:\n border-color 0.12s,\n background 0.12s,\n color 0.12s;\n letter-spacing: 0.04em;\n text-transform: uppercase;\n font-weight: 600;\n}\n.summary li:hover {\n border-color: var(--muted);\n color: var(--fg);\n}\n.summary li:focus {\n outline: 2px solid color-mix(in srgb, var(--accent) 60%, transparent);\n outline-offset: 2px;\n}\n.summary li.is-active {\n background: color-mix(in srgb, var(--accent) 16%, transparent);\n border-color: var(--accent);\n color: var(--fg);\n}\n.summary li span {\n color: var(--fg);\n font-weight: 700;\n margin-right: 8px;\n font-family: var(--display);\n letter-spacing: 0.05em;\n}\n.summary .sev-error span {\n color: var(--fail);\n}\n.summary .sev-warn span {\n color: var(--warn);\n}\n.summary .sev-info span {\n color: var(--info);\n}\n\nsection {\n margin-top: 28px;\n}\nh2 {\n font-family: var(--display);\n font-size: 14px;\n text-transform: uppercase;\n letter-spacing: 0.08em;\n color: var(--fg);\n margin: 0 0 14px;\n font-weight: 400;\n}\n\n.findings-head {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 12px;\n margin-bottom: 12px;\n}\n.findings-head h2 {\n margin: 0;\n}\n\n[data-search] {\n font-family: var(--mono);\n font-size: 12px;\n padding: 8px 12px;\n min-width: 260px;\n background: var(--surface);\n color: var(--fg);\n border: 1px solid var(--line);\n border-radius: 0;\n}\n[data-search]:focus {\n outline: none;\n border-color: var(--accent);\n}\n\n.artifacts {\n list-style: none;\n padding: 0;\n margin: 0;\n border: 1px solid var(--line);\n background: color-mix(in srgb, var(--surface) 70%, transparent);\n}\n.artifacts li {\n display: flex;\n align-items: center;\n gap: 12px;\n padding: 10px 14px;\n border-bottom: 1px solid var(--line);\n font-size: 12px;\n}\n.artifacts li:last-child {\n border-bottom: 0;\n}\n.artifacts code {\n color: var(--muted);\n min-width: 160px;\n text-transform: uppercase;\n letter-spacing: 0.06em;\n font-weight: 600;\n font-size: 11px;\n}\n.artifacts .path {\n flex: 1;\n word-break: break-all;\n}\n\n.copy {\n font-family: var(--mono);\n font-size: 10px;\n padding: 4px 10px;\n border: 1px solid var(--line);\n border-radius: 0;\n background: var(--surface);\n color: var(--muted);\n cursor: pointer;\n text-transform: uppercase;\n letter-spacing: 0.1em;\n font-weight: 600;\n}\n.copy:hover {\n color: var(--fg);\n border-color: var(--accent);\n}\n.copy.copied {\n color: var(--pass);\n border-color: var(--pass);\n}\n\ntable {\n width: 100%;\n border-collapse: collapse;\n font-size: 12px;\n border: 1px solid var(--line);\n background: color-mix(in srgb, var(--surface) 70%, transparent);\n}\nth,\ntd {\n text-align: left;\n padding: 10px 14px;\n border-bottom: 1px solid var(--line);\n vertical-align: top;\n}\ntr:last-child td {\n border-bottom: 0;\n}\nth {\n color: var(--muted);\n font-weight: 600;\n font-size: 10px;\n text-transform: uppercase;\n letter-spacing: 0.1em;\n background: color-mix(in srgb, var(--surface-2) 70%, transparent);\n}\ntbody tr:hover {\n background: color-mix(in srgb, var(--accent) 5%, transparent);\n}\ntr.is-hidden {\n display: none;\n}\n\n.badge {\n display: inline-block;\n padding: 3px 9px;\n border-radius: 0;\n font-size: 10px;\n text-transform: uppercase;\n letter-spacing: 0.1em;\n font-family: var(--mono);\n font-weight: 700;\n border: 1px solid transparent;\n}\n.sev-error .badge {\n background: color-mix(in srgb, var(--fail) 18%, transparent);\n color: var(--fail);\n border-color: color-mix(in srgb, var(--fail) 35%, transparent);\n}\n.sev-warn .badge {\n background: color-mix(in srgb, var(--warn) 18%, transparent);\n color: var(--warn);\n border-color: color-mix(in srgb, var(--warn) 35%, transparent);\n}\n.sev-info .badge {\n background: color-mix(in srgb, var(--info) 18%, transparent);\n color: var(--info);\n border-color: color-mix(in srgb, var(--info) 35%, transparent);\n}\n.sev-hint .badge {\n background: color-mix(in srgb, var(--hint) 22%, transparent);\n color: var(--hint);\n border-color: color-mix(in srgb, var(--hint) 35%, transparent);\n}\n\n.recommendation {\n margin: 6px 0 0;\n color: var(--muted);\n font-size: 11px;\n}\n.pointer {\n display: block;\n margin-top: 6px;\n color: var(--muted);\n font-size: 11px;\n}\n.empty {\n color: var(--muted);\n font-size: 12px;\n}\n.hidden {\n display: none;\n}\n\n:focus-visible {\n outline: 2px solid var(--accent);\n outline-offset: 2px;\n}\n";
|
|
10
|
+
var _inline_text_2_default = ":root {\n color-scheme: dark;\n --bg: #0e1018;\n --fg: #eef0f9;\n --muted: #8a93a0;\n --line: #262a33;\n --surface: #161922;\n --surface-2: #1d2027;\n --accent: #ad5dca;\n --accent-soft: #2b7eca;\n --accent-2: #2dd4bf;\n --pass: #23d18b;\n --fail: #dc3657;\n --warn: #ffc368;\n --info: #54b9ff;\n --hint: #545864;\n --grid-line: rgba(255, 255, 255, 0.04);\n --grid-size: 2rem;\n --mono:\n \"IBM Plex Mono\", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,\n \"Liberation Mono\", \"Courier New\", monospace;\n --display: \"Bangers\", var(--mono);\n}\n\n[data-theme=\"elysia\"] {\n --bg: oklch(21.2% 0.019 322.12);\n --fg: oklch(98.5% 0 0);\n --muted: oklch(71.1% 0.019 323.02);\n --line: oklch(36.4% 0.029 323.89);\n --surface: oklch(26.3% 0.024 320.12);\n --surface-2: oklch(36.4% 0.029 323.89);\n --accent: oklch(89.2% 0.058 10.001);\n --accent-soft: oklch(43.5% 0.029 321.78);\n --accent-2: #d2a8ff;\n --pass: #a3ffa9;\n --fail: #ffa3a3;\n --warn: #fffca3;\n --info: #a5d6ff;\n --hint: oklch(54.2% 0.034 322.5);\n --grid-line: rgba(242, 184, 235, 0.05);\n}\n\n[data-theme=\"tron\"] {\n --bg: #06080d;\n --fg: #e6f7ff;\n --muted: #5d8aa3;\n --line: #0e2c3d;\n --surface: #0a1320;\n --surface-2: #0d1a2b;\n --accent: #38f3ff;\n --accent-soft: #1ea7ff;\n --accent-2: #38f3ff;\n --pass: #7de8ff;\n --fail: #ff3c5f;\n --warn: #a8f5ff;\n --info: #73d8ff;\n --hint: #273746;\n --grid-line: rgba(56, 243, 255, 0.06);\n}\n\n[data-theme=\"808\"] {\n --bg: #1a1a1a;\n --fg: #ffffff;\n --muted: #9a958a;\n --line: #2a2a2a;\n --surface: #222222;\n --surface-2: #2a2a2a;\n --accent: #f8a125;\n --accent-soft: #e72e2e;\n --accent-2: #f1f827;\n --pass: #f1f827;\n --fail: #e72e2e;\n --warn: #f8a125;\n --info: #f1f827;\n --hint: #404040;\n --grid-line: rgba(248, 161, 37, 0.06);\n}\n\n* {\n box-sizing: border-box;\n}\n\nhtml {\n background:\n radial-gradient(\n circle at top left,\n color-mix(in srgb, var(--accent) 14%, transparent),\n transparent 32%\n ),\n radial-gradient(\n circle at top right,\n color-mix(in srgb, var(--accent-2) 10%, transparent),\n transparent 30%\n ),\n var(--bg);\n min-height: 100%;\n}\n\nbody {\n margin: 0;\n font-family: var(--mono);\n background:\n linear-gradient(\n 180deg,\n color-mix(in srgb, var(--bg) 92%, transparent),\n transparent 16rem\n ),\n linear-gradient(90deg, var(--grid-line) 1px, transparent 1px),\n linear-gradient(var(--grid-line) 1px, transparent 1px), transparent;\n background-size:\n auto,\n var(--grid-size) var(--grid-size),\n var(--grid-size) var(--grid-size),\n auto;\n background-position:\n 0 0,\n center top,\n center top,\n 0 0;\n color: var(--fg);\n font-size: 13px;\n line-height: 1.55;\n}\n\nheader {\n position: sticky;\n top: 0;\n z-index: 10;\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 18px 24px;\n background: color-mix(in srgb, var(--bg) 82%, transparent);\n backdrop-filter: blur(16px);\n box-shadow: 0 1px 0 color-mix(in srgb, var(--fg) 8%, transparent);\n overflow: hidden;\n}\n\nheader::before {\n content: \"\";\n position: absolute;\n inset: 0;\n z-index: 0;\n background:\n radial-gradient(900px 220px at 12% -30%, var(--accent), transparent 60%),\n radial-gradient(\n 700px 200px at 88% -20%,\n var(--accent-soft),\n transparent 65%\n );\n opacity: 0.35;\n pointer-events: none;\n}\n\nheader::after {\n content: \"\";\n position: absolute;\n left: 0;\n right: 0;\n bottom: 0;\n height: 1px;\n background: linear-gradient(\n 90deg,\n transparent,\n color-mix(in srgb, var(--accent) 55%, transparent),\n transparent\n );\n}\n\nheader > * {\n position: relative;\n z-index: 1;\n}\n\n.brand {\n display: flex;\n align-items: center;\n gap: 12px;\n}\n\n.dot {\n width: 10px;\n height: 10px;\n background: linear-gradient(135deg, var(--accent), var(--accent-2));\n box-shadow:\n 0 0 0 3px color-mix(in srgb, var(--accent) 18%, transparent),\n 0 0 18px color-mix(in srgb, var(--accent) 60%, transparent);\n}\n\nh1 {\n margin: 0;\n font-family: var(--display);\n font-size: 22px;\n font-weight: 400;\n letter-spacing: 0.06em;\n text-transform: uppercase;\n text-shadow: 0.04em 0.04em 0\n color-mix(in srgb, var(--accent) 22%, transparent);\n}\n\n.actions {\n display: flex;\n align-items: center;\n gap: 10px;\n}\n\n.visually-hidden {\n position: absolute;\n width: 1px;\n height: 1px;\n overflow: hidden;\n clip: rect(0 0 0 0);\n}\n\n.theme-switch {\n display: inline-flex;\n align-items: center;\n gap: 8px;\n padding: 4px 10px;\n border: 1px solid var(--line);\n background: color-mix(in srgb, var(--surface) 85%, transparent);\n}\n\n.theme-label {\n font-size: 10px;\n text-transform: uppercase;\n letter-spacing: 0.12em;\n color: var(--muted);\n font-weight: 600;\n}\n\n.theme-switch select {\n font-family: var(--mono);\n font-size: 11px;\n padding: 4px 22px 4px 8px;\n border: 1px solid var(--line);\n border-radius: 0;\n background: var(--surface-2)\n url(\"data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6'><path fill='none' stroke='%238a93a0' stroke-width='1.4' d='M1 1l4 4 4-4'/></svg>\")\n no-repeat right 8px center;\n color: var(--fg);\n -webkit-appearance: none;\n appearance: none;\n cursor: pointer;\n}\n.theme-switch select:hover {\n border-color: var(--accent);\n}\n.theme-switch select:focus {\n outline: none;\n border-color: var(--accent);\n}\n\nkbd {\n font-family: var(--mono);\n font-size: 11px;\n padding: 3px 7px;\n border: 1px solid var(--line);\n border-bottom-width: 2px;\n border-radius: 0;\n color: var(--muted);\n background: var(--surface);\n}\n\n.refresh {\n color: var(--fg);\n text-decoration: none;\n padding: 7px 14px;\n border: 1px solid var(--line);\n border-radius: 0;\n font-size: 12px;\n font-family: var(--mono);\n font-weight: 600;\n letter-spacing: 0.04em;\n text-transform: uppercase;\n background: var(--surface);\n box-shadow: 0 6px 18px rgba(0, 0, 0, 0.25);\n transition:\n border-color 0.15s,\n transform 0.05s,\n color 0.15s;\n}\n.refresh:hover {\n border-color: var(--accent);\n color: var(--accent);\n}\n.refresh:active {\n transform: translateY(1px);\n}\n\nmain {\n padding: 28px 24px 64px;\n max-width: 1100px;\n margin: 0 auto;\n}\n\n.banner {\n position: relative;\n padding: 16px 18px;\n border-radius: 0;\n margin-bottom: 20px;\n font-size: 13px;\n border: 1px solid var(--line);\n background: linear-gradient(\n 180deg,\n color-mix(in srgb, var(--surface) 80%, transparent),\n color-mix(in srgb, var(--bg) 92%, transparent)\n );\n box-shadow: 0 18px 44px rgba(0, 0, 0, 0.28);\n}\n.banner::before {\n content: \"\";\n position: absolute;\n left: 0;\n top: 0;\n bottom: 0;\n width: 3px;\n background: var(--accent);\n}\n.banner-pass::before {\n background: var(--pass);\n}\n.banner-fail::before {\n background: var(--fail);\n}\n.banner-pass {\n border-color: color-mix(in srgb, var(--pass) 45%, var(--line));\n}\n.banner-fail {\n border-color: color-mix(in srgb, var(--fail) 45%, var(--line));\n}\n.banner strong {\n font-family: var(--display);\n font-weight: 400;\n letter-spacing: 0.08em;\n text-transform: uppercase;\n font-size: 16px;\n margin-right: 6px;\n}\n.tagline {\n display: block;\n margin-top: 8px;\n font-weight: 700;\n letter-spacing: 0.16em;\n color: var(--pass);\n text-transform: uppercase;\n font-size: 11px;\n}\n\n.meta {\n display: grid;\n grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));\n gap: 10px;\n margin: 0 0 20px;\n padding: 0;\n}\n.meta div {\n background: linear-gradient(\n 180deg,\n color-mix(in srgb, var(--surface) 90%, var(--accent) 10%),\n color-mix(in srgb, var(--bg) 96%, transparent)\n );\n border: 1px solid var(--line);\n border-radius: 0;\n padding: 10px 14px;\n box-shadow: 0 6px 18px rgba(0, 0, 0, 0.22);\n}\n.meta dt {\n color: var(--muted);\n font-size: 10px;\n text-transform: uppercase;\n letter-spacing: 0.1em;\n margin-bottom: 4px;\n font-weight: 600;\n}\n.meta dd {\n margin: 0;\n font-size: 12px;\n}\n.muted-line {\n display: block;\n color: var(--muted);\n font-size: 11px;\n margin-top: 2px;\n}\n\n.summary {\n list-style: none;\n padding: 0;\n margin: 0 0 28px;\n display: flex;\n gap: 8px;\n flex-wrap: wrap;\n}\n.summary li {\n padding: 7px 14px;\n border: 1px solid var(--line);\n border-radius: 0;\n font-size: 12px;\n color: var(--muted);\n cursor: pointer;\n user-select: none;\n background: color-mix(in srgb, var(--surface) 80%, transparent);\n transition:\n border-color 0.12s,\n background 0.12s,\n color 0.12s;\n letter-spacing: 0.04em;\n text-transform: uppercase;\n font-weight: 600;\n}\n.summary li:hover {\n border-color: var(--muted);\n color: var(--fg);\n}\n.summary li:focus {\n outline: 2px solid color-mix(in srgb, var(--accent) 60%, transparent);\n outline-offset: 2px;\n}\n.summary li.is-active {\n background: color-mix(in srgb, var(--accent) 16%, transparent);\n border-color: var(--accent);\n color: var(--fg);\n}\n.summary li span {\n color: var(--fg);\n font-weight: 700;\n margin-right: 8px;\n font-family: var(--display);\n letter-spacing: 0.05em;\n}\n.summary .sev-error span {\n color: var(--fail);\n}\n.summary .sev-warn span {\n color: var(--warn);\n}\n.summary .sev-info span {\n color: var(--info);\n}\n\nsection {\n margin-top: 28px;\n}\nh2 {\n font-family: var(--display);\n font-size: 14px;\n text-transform: uppercase;\n letter-spacing: 0.08em;\n color: var(--fg);\n margin: 0 0 14px;\n font-weight: 400;\n}\n\n.findings-head {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 12px;\n margin-bottom: 12px;\n}\n.findings-head h2 {\n margin: 0;\n}\n\n[data-search] {\n font-family: var(--mono);\n font-size: 12px;\n padding: 8px 12px;\n min-width: 260px;\n background: var(--surface);\n color: var(--fg);\n border: 1px solid var(--line);\n border-radius: 0;\n}\n[data-search]:focus {\n outline: none;\n border-color: var(--accent);\n}\n\n.artifacts {\n list-style: none;\n padding: 0;\n margin: 0;\n border: 1px solid var(--line);\n background: color-mix(in srgb, var(--surface) 70%, transparent);\n}\n.artifacts li {\n display: flex;\n align-items: center;\n gap: 12px;\n padding: 10px 14px;\n border-bottom: 1px solid var(--line);\n font-size: 12px;\n}\n.artifacts li:last-child {\n border-bottom: 0;\n}\n.artifacts code {\n color: var(--muted);\n min-width: 160px;\n text-transform: uppercase;\n letter-spacing: 0.06em;\n font-weight: 600;\n font-size: 11px;\n}\n.artifacts .path {\n flex: 1;\n word-break: break-all;\n}\n\n.copy {\n font-family: var(--mono);\n font-size: 10px;\n padding: 4px 10px;\n border: 1px solid var(--line);\n border-radius: 0;\n background: var(--surface);\n color: var(--muted);\n cursor: pointer;\n text-transform: uppercase;\n letter-spacing: 0.1em;\n font-weight: 600;\n}\n.copy:hover {\n color: var(--fg);\n border-color: var(--accent);\n}\n.copy.copied {\n color: var(--pass);\n border-color: var(--pass);\n}\n\ntable {\n width: 100%;\n border-collapse: collapse;\n font-size: 12px;\n border: 1px solid var(--line);\n background: color-mix(in srgb, var(--surface) 70%, transparent);\n}\nth,\ntd {\n text-align: left;\n padding: 10px 14px;\n border-bottom: 1px solid var(--line);\n vertical-align: top;\n}\ntr:last-child td {\n border-bottom: 0;\n}\nth {\n color: var(--muted);\n font-weight: 600;\n font-size: 10px;\n text-transform: uppercase;\n letter-spacing: 0.1em;\n background: color-mix(in srgb, var(--surface-2) 70%, transparent);\n}\ntbody tr:hover {\n background: color-mix(in srgb, var(--accent) 5%, transparent);\n}\ntr.is-hidden {\n display: none;\n}\n\n.badge {\n display: inline-block;\n padding: 3px 9px;\n border-radius: 0;\n font-size: 10px;\n text-transform: uppercase;\n letter-spacing: 0.1em;\n font-family: var(--mono);\n font-weight: 700;\n border: 1px solid transparent;\n}\n.sev-error .badge {\n background: color-mix(in srgb, var(--fail) 18%, transparent);\n color: var(--fail);\n border-color: color-mix(in srgb, var(--fail) 35%, transparent);\n}\n.sev-warn .badge {\n background: color-mix(in srgb, var(--warn) 18%, transparent);\n color: var(--warn);\n border-color: color-mix(in srgb, var(--warn) 35%, transparent);\n}\n.sev-info .badge {\n background: color-mix(in srgb, var(--info) 18%, transparent);\n color: var(--info);\n border-color: color-mix(in srgb, var(--info) 35%, transparent);\n}\n.sev-hint .badge {\n background: color-mix(in srgb, var(--hint) 22%, transparent);\n color: var(--hint);\n border-color: color-mix(in srgb, var(--hint) 35%, transparent);\n}\n\n.recommendation {\n margin: 6px 0 0;\n color: var(--muted);\n font-size: 11px;\n}\n.pointer {\n display: block;\n margin-top: 6px;\n color: var(--muted);\n font-size: 11px;\n}\n.empty {\n color: var(--muted);\n font-size: 12px;\n}\n.hidden {\n display: none;\n}\n\n:focus-visible {\n outline: 2px solid var(--accent);\n outline-offset: 2px;\n}\n";
|
|
8
11
|
//#endregion
|
|
9
12
|
//#region src/output/dashboard.ts
|
|
10
13
|
const renderDashboard = (input) => {
|
|
@@ -32,6 +35,7 @@ const renderDashboard = (input) => {
|
|
|
32
35
|
<span class="theme-label">Theme</span>
|
|
33
36
|
<select data-theme-switcher aria-label="Dashboard theme">
|
|
34
37
|
<option value="astro">Astro Houston</option>
|
|
38
|
+
<option value="elysia">Elysia</option>
|
|
35
39
|
<option value="tron">Tron Legacy</option>
|
|
36
40
|
<option value="808">Detroit 808</option>
|
|
37
41
|
</select>
|
|
@@ -238,4 +242,15 @@ const spectralPlugin = (options = {}) => {
|
|
|
238
242
|
return plugin;
|
|
239
243
|
};
|
|
240
244
|
//#endregion
|
|
241
|
-
|
|
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 };
|