@opsydyn/elysia-spectral 1.5.1 → 1.5.3
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 +80 -6
- package/dist/core/index.mjs +1 -1
- package/dist/index.d.mts +1 -1
- package/dist/index.mjs +7 -7
- package/dist/{runtime-4LlfDIZv.mjs → runtime-PGHAFx-E.mjs} +47 -14
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.5.3](https://github.com/opsydyn/elysia-spectral/compare/v1.5.2...v1.5.3) (2026-05-12)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
* stabilize public API docs and smoke coverage ([d2373db](https://github.com/opsydyn/elysia-spectral/commit/d2373db7a40dc615b3d60c6c5e421ee848a1ffdf))
|
|
9
|
+
|
|
10
|
+
## [1.5.2](https://github.com/opsydyn/elysia-spectral/compare/v1.5.1...v1.5.2) (2026-05-12)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Bug Fixes
|
|
14
|
+
|
|
15
|
+
* complete self-describing lint reports ([b0917ce](https://github.com/opsydyn/elysia-spectral/commit/b0917ce4c977a1378ba851efc643058a70fea70b))
|
|
16
|
+
|
|
3
17
|
## [1.5.1](https://github.com/opsydyn/elysia-spectral/compare/v1.5.0...v1.5.1) (2026-05-12)
|
|
4
18
|
|
|
5
19
|
|
package/README.md
CHANGED
|
@@ -58,6 +58,7 @@ Current package scope:
|
|
|
58
58
|
- resolver pipeline for advanced ruleset loading
|
|
59
59
|
- console output
|
|
60
60
|
- JSON report output
|
|
61
|
+
- self-describing JSON report metadata (`failOn`, `durationMs`, relative artifact paths)
|
|
61
62
|
- JUnit report output
|
|
62
63
|
- SARIF report output
|
|
63
64
|
- OpenAPI snapshot output
|
|
@@ -346,6 +347,8 @@ spectralPlugin({
|
|
|
346
347
|
|
|
347
348
|
This is useful for local development and intentionally broken fixtures.
|
|
348
349
|
|
|
350
|
+
`startup.mode: 'report'` relaxes threshold failures only. Missing OpenAPI routes, invalid spec JSON, broken rulesets, or artifact writes promoted to `output.artifactWriteFailures: 'error'` still surface as startup errors.
|
|
351
|
+
|
|
349
352
|
### Disable startup lint entirely
|
|
350
353
|
|
|
351
354
|
Use `startup.mode: 'off'` when you only want manual or healthcheck-triggered runs.
|
|
@@ -360,6 +363,8 @@ spectralPlugin({
|
|
|
360
363
|
|
|
361
364
|
`enabled: false` is still supported as a backwards-compatible way to disable startup lint.
|
|
362
365
|
|
|
366
|
+
`startup.mode` takes precedence over the legacy `enabled` flag.
|
|
367
|
+
|
|
363
368
|
### Add a lint healthcheck endpoint
|
|
364
369
|
|
|
365
370
|
The healthcheck route is opt-in.
|
|
@@ -509,7 +514,10 @@ Custom sinks run after linting and can read:
|
|
|
509
514
|
If you use the runtime programmatically, you can provide your own resolver pipeline to `loadRuleset` or `loadResolvedRuleset`.
|
|
510
515
|
|
|
511
516
|
```ts
|
|
512
|
-
import {
|
|
517
|
+
import {
|
|
518
|
+
defaultRulesetResolvers,
|
|
519
|
+
loadRuleset,
|
|
520
|
+
} from '@opsydyn/elysia-spectral/core'
|
|
513
521
|
|
|
514
522
|
const ruleset = await loadRuleset('virtual://team-ruleset', {
|
|
515
523
|
resolvers: [
|
|
@@ -529,11 +537,14 @@ const ruleset = await loadRuleset('virtual://team-ruleset', {
|
|
|
529
537
|
}
|
|
530
538
|
}
|
|
531
539
|
}
|
|
532
|
-
: undefined
|
|
540
|
+
: undefined,
|
|
541
|
+
...defaultRulesetResolvers
|
|
533
542
|
]
|
|
534
543
|
})
|
|
535
544
|
```
|
|
536
545
|
|
|
546
|
+
`defaultRulesetResolvers` is a supported advanced export from `@opsydyn/elysia-spectral/core`. Prefer prepending your resolver and then spreading the defaults so built-in autodiscovery, path loading, and inline ruleset resolution continue to work.
|
|
547
|
+
|
|
537
548
|
This is an advanced extension point. Most apps should continue using repo-level `spectral.*` files.
|
|
538
549
|
|
|
539
550
|
### Make artifact write failures fatal in CI
|
|
@@ -755,6 +766,26 @@ That example uses `startup.mode: 'report'`, so the app still boots while the pac
|
|
|
755
766
|
|
|
756
767
|
## Reference
|
|
757
768
|
|
|
769
|
+
### Supported import surfaces
|
|
770
|
+
|
|
771
|
+
Use `@opsydyn/elysia-spectral` for the primary stable consumer API:
|
|
772
|
+
|
|
773
|
+
- `spectralPlugin`
|
|
774
|
+
- `createOpenApiLintRuntime`
|
|
775
|
+
- `recommended`, `server`, `strict`, and `presets`
|
|
776
|
+
- `loadRuleset`, `loadResolvedRuleset`, and `lintOpenApi`
|
|
777
|
+
- `shouldFail`, `enforceThreshold`, and the exported error classes
|
|
778
|
+
|
|
779
|
+
Use `@opsydyn/elysia-spectral/core` for advanced composition details:
|
|
780
|
+
|
|
781
|
+
- `defaultRulesetResolvers`
|
|
782
|
+
- resolver pipeline types such as `RulesetResolver`, `RulesetResolverInput`, and `LoadResolvedRulesetOptions`
|
|
783
|
+
- the same programmatic runtime helpers when you want an explicitly advanced import surface
|
|
784
|
+
|
|
785
|
+
`defaultRulesetResolvers` is a supported extension hook. Prefer `[myResolver, ...defaultRulesetResolvers]` when extending the resolver pipeline instead of replacing the defaults outright.
|
|
786
|
+
|
|
787
|
+
When used as a plugin, `app.store.openApiLint` is part of the supported plugin contract.
|
|
788
|
+
|
|
758
789
|
### Package API
|
|
759
790
|
|
|
760
791
|
```ts
|
|
@@ -804,7 +835,7 @@ type SpectralPluginOptions = {
|
|
|
804
835
|
* Controls startup lint behaviour.
|
|
805
836
|
* startup.mode takes precedence over the legacy enabled option.
|
|
806
837
|
* 'enforce' — lint runs at startup and throws on threshold failure (default)
|
|
807
|
-
|
|
838
|
+
* 'report' — lint runs at startup, prints findings, but does not block boot on threshold failures
|
|
808
839
|
* 'off' — startup lint is skipped entirely
|
|
809
840
|
*/
|
|
810
841
|
startup?: {
|
|
@@ -846,6 +877,10 @@ type LintRunResult = {
|
|
|
846
877
|
generatedAt: string
|
|
847
878
|
/** Where the lint run was triggered from. */
|
|
848
879
|
source: LintRunSource
|
|
880
|
+
/** The configured threshold that produced this result. */
|
|
881
|
+
failOn: SeverityThreshold
|
|
882
|
+
/** Duration of the completed lint run in milliseconds. */
|
|
883
|
+
durationMs: number | null
|
|
849
884
|
summary: {
|
|
850
885
|
error: number
|
|
851
886
|
warn: number
|
|
@@ -887,6 +922,28 @@ type OpenApiLintRuntime = {
|
|
|
887
922
|
|
|
888
923
|
function createOpenApiLintRuntime(options?: SpectralPluginOptions): OpenApiLintRuntime
|
|
889
924
|
|
|
925
|
+
// ── Root utility exports ──────────────────────────────────────────────────────
|
|
926
|
+
|
|
927
|
+
const presets: Record<PresetName, RulesetDefinition>
|
|
928
|
+
|
|
929
|
+
function loadRuleset(
|
|
930
|
+
input?: string | RulesetDefinition | Record<string, unknown>,
|
|
931
|
+
baseDirOrOptions?: string | LoadResolvedRulesetOptions,
|
|
932
|
+
): Promise<RulesetDefinition>
|
|
933
|
+
|
|
934
|
+
function loadResolvedRuleset(
|
|
935
|
+
input?: string | RulesetDefinition | Record<string, unknown>,
|
|
936
|
+
baseDirOrOptions?: string | LoadResolvedRulesetOptions,
|
|
937
|
+
): Promise<LoadedRuleset>
|
|
938
|
+
|
|
939
|
+
function lintOpenApi(
|
|
940
|
+
spec: Record<string, unknown>,
|
|
941
|
+
ruleset: RulesetDefinition,
|
|
942
|
+
): Promise<LintRunResult>
|
|
943
|
+
|
|
944
|
+
function shouldFail(result: LintRunResult, threshold: SeverityThreshold): boolean
|
|
945
|
+
function enforceThreshold(result: LintRunResult, threshold: SeverityThreshold): void
|
|
946
|
+
|
|
890
947
|
// ── Extension points (advanced) ───────────────────────────────────────────────
|
|
891
948
|
|
|
892
949
|
type SpectralLogger = {
|
|
@@ -908,8 +965,14 @@ type OpenApiLintSink = {
|
|
|
908
965
|
) => undefined | Partial<OpenApiLintArtifacts> | Promise<undefined | Partial<OpenApiLintArtifacts>>
|
|
909
966
|
}
|
|
910
967
|
|
|
968
|
+
type RulesetResolverInput =
|
|
969
|
+
| string
|
|
970
|
+
| RulesetDefinition
|
|
971
|
+
| Record<string, unknown>
|
|
972
|
+
| undefined
|
|
973
|
+
|
|
911
974
|
type RulesetResolver = (
|
|
912
|
-
input:
|
|
975
|
+
input: RulesetResolverInput,
|
|
913
976
|
context: RulesetResolverContext,
|
|
914
977
|
) => Promise<ResolvedRulesetCandidate | undefined>
|
|
915
978
|
|
|
@@ -941,6 +1004,8 @@ type LoadResolvedRulesetOptions = {
|
|
|
941
1004
|
defaultRuleset?: RulesetDefinition
|
|
942
1005
|
}
|
|
943
1006
|
|
|
1007
|
+
const defaultRulesetResolvers: RulesetResolver[]
|
|
1008
|
+
|
|
944
1009
|
// ── Error classes ─────────────────────────────────────────────────────────────
|
|
945
1010
|
|
|
946
1011
|
class OpenApiLintThresholdError extends Error {
|
|
@@ -993,7 +1058,7 @@ The runtime object exposes:
|
|
|
993
1058
|
- `lastFailure`: last thrown runtime error summary
|
|
994
1059
|
- `running`: boolean convenience flag
|
|
995
1060
|
|
|
996
|
-
When used as a plugin, the runtime is also available on `app.store.openApiLint
|
|
1061
|
+
When used as a plugin, the runtime is also available on `app.store.openApiLint` as part of the supported plugin contract.
|
|
997
1062
|
|
|
998
1063
|
### Healthcheck response shape
|
|
999
1064
|
|
|
@@ -1008,6 +1073,8 @@ Example successful response:
|
|
|
1008
1073
|
"ok": true,
|
|
1009
1074
|
"generatedAt": "2026-04-06T12:00:00.000Z",
|
|
1010
1075
|
"source": "startup",
|
|
1076
|
+
"failOn": "error",
|
|
1077
|
+
"durationMs": 42,
|
|
1011
1078
|
"summary": {
|
|
1012
1079
|
"error": 0,
|
|
1013
1080
|
"warn": 0,
|
|
@@ -1036,9 +1103,12 @@ Example successful response:
|
|
|
1036
1103
|
|
|
1037
1104
|
- startup mode `enforce` throws on threshold failures
|
|
1038
1105
|
- startup mode `report` prints the same lint report but allows boot to continue on threshold failures
|
|
1106
|
+
- startup mode defaults to `enforce`
|
|
1039
1107
|
- startup mode `off` skips startup lint
|
|
1108
|
+
- `failOn` defaults to `error`
|
|
1040
1109
|
- missing OpenAPI generator / missing OpenAPI JSON route, bad `source.specPath`, or invalid spec JSON produces an actionable provider error
|
|
1041
1110
|
- artifact writes warn by default and can be made fatal with `output.artifactWriteFailures: 'error'`
|
|
1111
|
+
- `enabled: false` remains a backwards-compatible alias for `startup.mode: 'off'`, and `startup.mode` takes precedence when both are provided
|
|
1042
1112
|
|
|
1043
1113
|
### Output model
|
|
1044
1114
|
|
|
@@ -1049,6 +1119,8 @@ The current output model has two layers:
|
|
|
1049
1119
|
|
|
1050
1120
|
The convenience options compile down to built-in sinks so the current API stays simple while the internal output model becomes extensible.
|
|
1051
1121
|
|
|
1122
|
+
Persisted JSON reports are self-describing: they embed the configured `failOn` threshold, the completed `durationMs`, and relative artifact paths so the same report shape is portable across CI runners and developer machines.
|
|
1123
|
+
|
|
1052
1124
|
## Explanation
|
|
1053
1125
|
|
|
1054
1126
|
### Why this package exists
|
|
@@ -1096,4 +1168,6 @@ Production-grade linting needs more than a pass/fail boolean. The runtime tracks
|
|
|
1096
1168
|
|
|
1097
1169
|
### Project status
|
|
1098
1170
|
|
|
1099
|
-
The package is published to npm and used in production.
|
|
1171
|
+
The package is published to npm and used in production. The current pre-`1.0` feature roadmap is functionally complete: startup/runtime flows, presets, output sinks, CI workflows, dashboard support, and self-describing result artifacts are all shipped. Remaining work is primarily final `1.0` release-readiness and package-boundary polish, tracked in [roadmap.md](../../roadmap.md).
|
|
1172
|
+
|
|
1173
|
+
If you are upgrading from an early `v0.1`-style or other pre-`1.0` setup, see the published migration guide: [1.0 migration notes](../../docs/1.0-migration-notes.md).
|
package/dist/core/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
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-
|
|
2
|
+
import { a as enforceThreshold, i as OpenApiLintThresholdError, n as createOpenApiLintRuntime, o as shouldFail, t as OpenApiLintArtifactWriteError } from "../runtime-PGHAFx-E.mjs";
|
|
3
3
|
import { n as loadResolvedRuleset, r as loadRuleset, t as defaultRulesetResolvers } from "../load-ruleset-CiikrzWx.mjs";
|
|
4
4
|
import { t as lintOpenApi } from "../lint-openapi-D76sC7S5.mjs";
|
|
5
5
|
export { OpenApiLintArtifactWriteError, OpenApiLintThresholdError, RulesetLoadError, createOpenApiLintRuntime, defaultRulesetResolvers, enforceThreshold, lintOpenApi, loadResolvedRuleset, loadRuleset, shouldFail };
|
package/dist/index.d.mts
CHANGED
|
@@ -77,4 +77,4 @@ declare const loadRuleset: (input?: RulesetResolverInput, baseDirOrOptions?: str
|
|
|
77
77
|
declare const loadResolvedRuleset: (input?: RulesetResolverInput, baseDirOrOptions?: string | LoadResolvedRulesetOptions) => Promise<LoadedRuleset>;
|
|
78
78
|
declare const lintOpenApi: (spec: Record<string, unknown>, ruleset: RulesetDefinition) => Promise<LintRunResult>;
|
|
79
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 };
|
|
80
|
+
export { type ArtifactWriteFailureMode, type LintFinding, type LintRunResult, type LintRunSource, type LintSeverity, type LoadResolvedRulesetOptions, type LoadedRuleset, OpenApiLintArtifactWriteError, type OpenApiLintArtifacts, type OpenApiLintRuntime, type OpenApiLintRuntimeFailure, type OpenApiLintRuntimeStatus, type OpenApiLintSink, type OpenApiLintSinkContext, OpenApiLintThresholdError, type PresetName, type ResolvedRulesetCandidate, RulesetLoadError, type RulesetResolver, type RulesetResolverContext, type RulesetResolverInput, type SeverityThreshold, type SpectralLogger, type SpectralPluginOptions, type StartupLintMode, createOpenApiLintRuntime, enforceThreshold, lintOpenApi, loadResolvedRuleset, loadRuleset, presets, recommended, server, shouldFail, spectralPlugin, strict };
|
package/dist/index.mjs
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
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-
|
|
2
|
+
import { a as enforceThreshold, i as OpenApiLintThresholdError, n as createOpenApiLintRuntime, o as shouldFail, r as resolveStartupMode, s as resolveReporter, t as OpenApiLintArtifactWriteError } from "./runtime-PGHAFx-E.mjs";
|
|
3
3
|
import { t as recommended } from "./recommended-DgrTqq-3.mjs";
|
|
4
4
|
import { i as server, r as strict, t as presets } from "./presets-CCfU_diN.mjs";
|
|
5
5
|
import { Elysia } from "elysia";
|
|
6
|
-
//#region \0inline-text:3.mjs
|
|
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";
|
|
8
|
-
//#endregion
|
|
9
6
|
//#region \0inline-text:2.mjs
|
|
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";
|
|
7
|
+
var _inline_text_2_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";
|
|
8
|
+
//#endregion
|
|
9
|
+
//#region \0inline-text:3.mjs
|
|
10
|
+
var _inline_text_3_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";
|
|
11
11
|
//#endregion
|
|
12
12
|
//#region src/output/dashboard.ts
|
|
13
13
|
const renderDashboard = (input) => {
|
|
@@ -22,7 +22,7 @@ const renderDashboard = (input) => {
|
|
|
22
22
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
23
23
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
24
24
|
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Bangers&family=IBM+Plex+Mono:wght@400;500;600;700&display=swap" />
|
|
25
|
-
<style>${
|
|
25
|
+
<style>${_inline_text_3_default}</style>
|
|
26
26
|
</head>
|
|
27
27
|
<body>
|
|
28
28
|
<header>
|
|
@@ -45,7 +45,7 @@ const renderDashboard = (input) => {
|
|
|
45
45
|
</div>
|
|
46
46
|
</header>
|
|
47
47
|
<main>${body}</main>
|
|
48
|
-
<script>${
|
|
48
|
+
<script>${_inline_text_2_default}<\/script>
|
|
49
49
|
</body>
|
|
50
50
|
</html>`;
|
|
51
51
|
};
|
|
@@ -411,6 +411,11 @@ const toSarifArtifactUri = (value) => {
|
|
|
411
411
|
};
|
|
412
412
|
//#endregion
|
|
413
413
|
//#region src/output/sinks.ts
|
|
414
|
+
const relativiseArtifactPath = (artifactPath) => {
|
|
415
|
+
const resolvedPath = path.resolve(process.cwd(), artifactPath);
|
|
416
|
+
const relativePath = path.relative(process.cwd(), resolvedPath);
|
|
417
|
+
return relativePath.startsWith(".") ? relativePath : `./${relativePath}`;
|
|
418
|
+
};
|
|
414
419
|
const createOutputSinks = (options) => {
|
|
415
420
|
const reporter = resolveReporter(options.logger);
|
|
416
421
|
const sinks = [];
|
|
@@ -421,24 +426,17 @@ const createOutputSinks = (options) => {
|
|
|
421
426
|
if (configuredSpecSnapshotPath) sinks.push({
|
|
422
427
|
name: "spec snapshot",
|
|
423
428
|
kind: "artifact",
|
|
429
|
+
phase: "pre-finalize",
|
|
424
430
|
async write(_result, context) {
|
|
425
431
|
const writtenSpecSnapshotPath = await writeSpecSnapshot(configuredSpecSnapshotPath === true ? await resolveDefaultSpecSnapshotPath() : configuredSpecSnapshotPath, context.spec, options.output?.pretty !== false);
|
|
426
432
|
reporter.artifact(`OpenAPI lint wrote spec snapshot to ${writtenSpecSnapshotPath}.`);
|
|
427
433
|
return { specSnapshotPath: writtenSpecSnapshotPath };
|
|
428
434
|
}
|
|
429
435
|
});
|
|
430
|
-
if (configuredJsonReportPath) sinks.push({
|
|
431
|
-
name: "JSON report",
|
|
432
|
-
kind: "artifact",
|
|
433
|
-
async write(result) {
|
|
434
|
-
const writtenJsonReportPath = await writeJsonReport(configuredJsonReportPath, result, options.output?.pretty !== false);
|
|
435
|
-
reporter.artifact(`OpenAPI lint wrote JSON report to ${writtenJsonReportPath}.`);
|
|
436
|
-
return { jsonReportPath: writtenJsonReportPath };
|
|
437
|
-
}
|
|
438
|
-
});
|
|
439
436
|
if (configuredJunitReportPath) sinks.push({
|
|
440
437
|
name: "JUnit report",
|
|
441
438
|
kind: "artifact",
|
|
439
|
+
phase: "pre-finalize",
|
|
442
440
|
async write(result) {
|
|
443
441
|
const writtenJunitReportPath = await writeJunitReport(configuredJunitReportPath, result);
|
|
444
442
|
reporter.artifact(`OpenAPI lint wrote JUnit report to ${writtenJunitReportPath}.`);
|
|
@@ -449,6 +447,7 @@ const createOutputSinks = (options) => {
|
|
|
449
447
|
if (configuredBrunoCollectionPath) sinks.push({
|
|
450
448
|
name: "Bruno collection",
|
|
451
449
|
kind: "artifact",
|
|
450
|
+
phase: "pre-finalize",
|
|
452
451
|
async write(_result, context) {
|
|
453
452
|
const writtenPath = await writeBrunoCollection(configuredBrunoCollectionPath, context.spec);
|
|
454
453
|
reporter.artifact(`OpenAPI lint wrote Bruno collection to ${writtenPath}.`);
|
|
@@ -458,6 +457,7 @@ const createOutputSinks = (options) => {
|
|
|
458
457
|
if (configuredSarifReportPath) sinks.push({
|
|
459
458
|
name: "SARIF report",
|
|
460
459
|
kind: "artifact",
|
|
460
|
+
phase: "pre-finalize",
|
|
461
461
|
async write(result) {
|
|
462
462
|
const writtenSarifReportPath = await writeSarifReport(configuredSarifReportPath, result, options.output?.pretty !== false);
|
|
463
463
|
reporter.artifact(`OpenAPI lint wrote SARIF report to ${writtenSarifReportPath}.`);
|
|
@@ -467,11 +467,29 @@ const createOutputSinks = (options) => {
|
|
|
467
467
|
for (const sink of options.output?.sinks ?? []) sinks.push({
|
|
468
468
|
name: sink.name,
|
|
469
469
|
kind: "custom",
|
|
470
|
+
phase: "post-finalize",
|
|
470
471
|
write: async (result, context) => await Promise.resolve(sink.write(result, context))
|
|
471
472
|
});
|
|
473
|
+
if (configuredJsonReportPath) sinks.push({
|
|
474
|
+
name: "JSON report",
|
|
475
|
+
kind: "artifact",
|
|
476
|
+
phase: "post-finalize",
|
|
477
|
+
async write(result) {
|
|
478
|
+
const writtenJsonReportPath = await writeJsonReport(configuredJsonReportPath, {
|
|
479
|
+
...result,
|
|
480
|
+
artifacts: {
|
|
481
|
+
...result.artifacts ?? {},
|
|
482
|
+
jsonReportPath: relativiseArtifactPath(configuredJsonReportPath)
|
|
483
|
+
}
|
|
484
|
+
}, options.output?.pretty !== false);
|
|
485
|
+
reporter.artifact(`OpenAPI lint wrote JSON report to ${writtenJsonReportPath}.`);
|
|
486
|
+
return { jsonReportPath: writtenJsonReportPath };
|
|
487
|
+
}
|
|
488
|
+
});
|
|
472
489
|
if (options.output?.console !== false) sinks.push({
|
|
473
490
|
name: "console",
|
|
474
491
|
kind: "report",
|
|
492
|
+
phase: "post-finalize",
|
|
475
493
|
async write(result) {
|
|
476
494
|
reportToConsole(result, reporter);
|
|
477
495
|
}
|
|
@@ -645,10 +663,12 @@ const createOpenApiLintRuntime = (options = {}) => {
|
|
|
645
663
|
result.source = source;
|
|
646
664
|
result.failOn = options.failOn ?? "error";
|
|
647
665
|
result.ok = !shouldFail(result, result.failOn);
|
|
648
|
-
|
|
649
|
-
|
|
666
|
+
const { preFinalizeSinks, postFinalizeSinks } = partitionOutputSinks(createOutputSinks(options));
|
|
667
|
+
await writeOutputSinks(preFinalizeSinks, result, spec, reporter, artifactWriteFailureMode);
|
|
650
668
|
finalizeRuntimeRun(runtime, startedAt);
|
|
651
669
|
result.durationMs = runtime.durationMs;
|
|
670
|
+
await writeOutputSinks(postFinalizeSinks, result, spec, reporter, artifactWriteFailureMode);
|
|
671
|
+
runtime.latest = result;
|
|
652
672
|
reporter.complete("OpenAPI lint completed.");
|
|
653
673
|
enforceThreshold(result, options.failOn ?? "error");
|
|
654
674
|
runtime.status = "passed";
|
|
@@ -694,9 +714,7 @@ const handleArtifactWriteFailure = (artifact, error, mode, reporter) => {
|
|
|
694
714
|
if (mode === "error") throw wrappedError;
|
|
695
715
|
reporter.warn(wrappedError.message);
|
|
696
716
|
};
|
|
697
|
-
const writeOutputSinks = async (result, spec,
|
|
698
|
-
const reporter = resolveReporter(options.logger);
|
|
699
|
-
const sinks = createOutputSinks(options);
|
|
717
|
+
const writeOutputSinks = async (sinks, result, spec, reporter, artifactWriteFailureMode) => {
|
|
700
718
|
for (const sink of sinks) try {
|
|
701
719
|
const artifacts = await sink.write(result, {
|
|
702
720
|
spec,
|
|
@@ -711,6 +729,21 @@ const writeOutputSinks = async (result, spec, options, artifactWriteFailureMode)
|
|
|
711
729
|
throw error;
|
|
712
730
|
}
|
|
713
731
|
};
|
|
732
|
+
const partitionOutputSinks = (sinks) => {
|
|
733
|
+
const preFinalizeSinks = [];
|
|
734
|
+
const postFinalizeSinks = [];
|
|
735
|
+
for (const sink of sinks) {
|
|
736
|
+
if (sink.phase === "post-finalize") {
|
|
737
|
+
postFinalizeSinks.push(sink);
|
|
738
|
+
continue;
|
|
739
|
+
}
|
|
740
|
+
preFinalizeSinks.push(sink);
|
|
741
|
+
}
|
|
742
|
+
return {
|
|
743
|
+
preFinalizeSinks,
|
|
744
|
+
postFinalizeSinks
|
|
745
|
+
};
|
|
746
|
+
};
|
|
714
747
|
const relativiseArtifacts = (artifacts) => {
|
|
715
748
|
const cwd = process.cwd();
|
|
716
749
|
const result = {};
|