@opsydyn/elysia-spectral 0.4.0 → 0.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +21 -0
- package/README.md +244 -44
- package/dist/core/index.d.mts +2 -2
- package/dist/core/index.mjs +2 -2
- package/dist/{core-D_ro1XEW.mjs → core-DTKNy6TU.mjs} +25 -5
- package/dist/{index-CMyl_MsI.d.mts → index-11HnbLDN.d.mts} +4 -11
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +2 -2
- package/package.json +4 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.5.2](https://github.com/opsydyn/elysia-spectral/compare/v0.5.1...v0.5.2) (2026-04-15)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
* replace export * with named exports in core/index.ts for stable public API ([d0ef1c9](https://github.com/opsydyn/elysia-spectral/commit/d0ef1c95618ecd5a83e8224f2ba831b226a7ab38))
|
|
9
|
+
|
|
10
|
+
## [0.5.1](https://github.com/opsydyn/elysia-spectral/compare/v0.5.0...v0.5.1) (2026-04-15)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Bug Fixes
|
|
14
|
+
|
|
15
|
+
* tighten public API surface and align LintRunResult.ok with threshold ([8ca3331](https://github.com/opsydyn/elysia-spectral/commit/8ca3331db59e5753d454e759730e3ecd931d2087))
|
|
16
|
+
|
|
17
|
+
## [0.5.0](https://github.com/opsydyn/elysia-spectral/compare/v0.4.0...v0.5.0) (2026-04-15)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
### Features
|
|
21
|
+
|
|
22
|
+
* add Bruno collection output via output.brunoCollectionPath ([45abcd6](https://github.com/opsydyn/elysia-spectral/commit/45abcd6225bd94098d5974a27ef2c7642614c77f))
|
|
23
|
+
|
|
3
24
|
## [0.4.0](https://github.com/opsydyn/elysia-spectral/compare/v0.3.0...v0.4.0) (2026-04-15)
|
|
4
25
|
|
|
5
26
|
|
package/README.md
CHANGED
|
@@ -49,6 +49,8 @@ Current package scope:
|
|
|
49
49
|
|
|
50
50
|
- startup linting
|
|
51
51
|
- threshold-based failure
|
|
52
|
+
- first-party governance presets: `recommended`, `server`, `strict`
|
|
53
|
+
- RFC 9457 Problem Details enforcement (strict preset)
|
|
52
54
|
- repo-level and local rulesets
|
|
53
55
|
- YAML, JS, TS, and in-memory rulesets
|
|
54
56
|
- resolver pipeline for advanced ruleset loading
|
|
@@ -57,6 +59,7 @@ Current package scope:
|
|
|
57
59
|
- JUnit report output
|
|
58
60
|
- SARIF report output
|
|
59
61
|
- OpenAPI snapshot output
|
|
62
|
+
- Bruno collection output (OpenCollection YAML or JSON)
|
|
60
63
|
- reusable runtime for CI and tests
|
|
61
64
|
- opt-in healthcheck endpoint for cached and fresh runs
|
|
62
65
|
|
|
@@ -497,6 +500,47 @@ JUnit output lets CI systems that consume test XML (Buildkite, CircleCI, GitLab,
|
|
|
497
500
|
if: always()
|
|
498
501
|
```
|
|
499
502
|
|
|
503
|
+
### Generate a Bruno collection
|
|
504
|
+
|
|
505
|
+
Export the generated OpenAPI spec as a Bruno collection. Bruno is an open-source API client — generated collections let your team test API endpoints without manual import steps.
|
|
506
|
+
|
|
507
|
+
The output format is determined by the file extension:
|
|
508
|
+
|
|
509
|
+
- `.yml` / `.yaml` — OpenCollection YAML (recommended, Bruno v3.0.0+)
|
|
510
|
+
- `.json` — Bruno collection JSON (compatible with all Bruno versions)
|
|
511
|
+
|
|
512
|
+
```ts
|
|
513
|
+
// OpenCollection YAML — recommended
|
|
514
|
+
spectralPlugin({
|
|
515
|
+
output: {
|
|
516
|
+
brunoCollectionPath: './bruno/collection.yml'
|
|
517
|
+
}
|
|
518
|
+
})
|
|
519
|
+
|
|
520
|
+
// Bruno JSON — for older Bruno versions
|
|
521
|
+
spectralPlugin({
|
|
522
|
+
output: {
|
|
523
|
+
brunoCollectionPath: './bruno/collection.json'
|
|
524
|
+
}
|
|
525
|
+
})
|
|
526
|
+
```
|
|
527
|
+
|
|
528
|
+
The collection is written after each lint run — at startup, on healthcheck, or in CI. Commit it alongside the spec snapshot so it stays in sync with the API surface.
|
|
529
|
+
|
|
530
|
+
To regenerate in CI as part of your lint script:
|
|
531
|
+
|
|
532
|
+
```ts
|
|
533
|
+
const runtime = createOpenApiLintRuntime({
|
|
534
|
+
preset: 'strict',
|
|
535
|
+
output: {
|
|
536
|
+
specSnapshotPath: './reports/openapi-snapshot.json',
|
|
537
|
+
brunoCollectionPath: './bruno/collection.yml',
|
|
538
|
+
},
|
|
539
|
+
})
|
|
540
|
+
|
|
541
|
+
await runtime.run(app)
|
|
542
|
+
```
|
|
543
|
+
|
|
500
544
|
### Track OpenAPI snapshot drift
|
|
501
545
|
|
|
502
546
|
Commit the generated OpenAPI snapshot and use `git diff --exit-code` to detect when the API surface changes unexpectedly in a PR.
|
|
@@ -521,6 +565,43 @@ git commit -m "chore: add openapi snapshot"
|
|
|
521
565
|
|
|
522
566
|
If the snapshot has changed, the CI step fails and the diff is visible in the logs. Deliberate API changes are acknowledged by updating the committed snapshot — accidental ones are caught before they ship.
|
|
523
567
|
|
|
568
|
+
### Generate a typed client
|
|
569
|
+
|
|
570
|
+
**If your consumer is a TypeScript project that can import from your Elysia app, use [Eden Treaty](https://elysiajs.com/eden/treaty/overview) instead.** It derives types directly from the app instance with no codegen, no snapshot, and no drift.
|
|
571
|
+
|
|
572
|
+
```ts
|
|
573
|
+
import { treaty } from '@elysiajs/eden'
|
|
574
|
+
import type { App } from '../server'
|
|
575
|
+
|
|
576
|
+
const client = treaty<App>('localhost:3000')
|
|
577
|
+
// fully typed — zero codegen
|
|
578
|
+
```
|
|
579
|
+
|
|
580
|
+
For vendor-agnostic consumers — cross-repo TypeScript, non-TypeScript clients, or a published SDK — use the committed OpenAPI snapshot as input to [`openapi-ts`](https://openapi-ts.dev):
|
|
581
|
+
|
|
582
|
+
```json
|
|
583
|
+
{
|
|
584
|
+
"scripts": {
|
|
585
|
+
"generate:client": "openapi-ts --input ./reports/openapi-snapshot.json --output ./src/generated/client --client @hey-api/client-fetch"
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
```
|
|
589
|
+
|
|
590
|
+
Chain it after the lint step in CI and guard against drift:
|
|
591
|
+
|
|
592
|
+
```yaml
|
|
593
|
+
- name: Lint OpenAPI spec
|
|
594
|
+
run: bun scripts/lint-openapi.ts
|
|
595
|
+
|
|
596
|
+
- name: Generate typed client
|
|
597
|
+
run: bun run generate:client
|
|
598
|
+
|
|
599
|
+
- name: Check for client drift
|
|
600
|
+
run: git diff --exit-code src/generated/client/
|
|
601
|
+
```
|
|
602
|
+
|
|
603
|
+
The lint gate runs first — if the spec is invalid the codegen step never runs.
|
|
604
|
+
|
|
524
605
|
### Work on this repository locally
|
|
525
606
|
|
|
526
607
|
From the monorepo root:
|
|
@@ -551,63 +632,38 @@ That example uses `startup.mode: 'report'`, so the app still boots while the pac
|
|
|
551
632
|
### Package API
|
|
552
633
|
|
|
553
634
|
```ts
|
|
554
|
-
|
|
635
|
+
// ── Vocabulary types ──────────────────────────────────────────────────────────
|
|
555
636
|
|
|
637
|
+
type PresetName = 'recommended' | 'server' | 'strict'
|
|
638
|
+
type LintSeverity = 'error' | 'warn' | 'info' | 'hint'
|
|
556
639
|
type SeverityThreshold = 'error' | 'warn' | 'info' | 'hint' | 'never'
|
|
557
|
-
|
|
558
640
|
type StartupLintMode = 'enforce' | 'report' | 'off'
|
|
559
|
-
|
|
641
|
+
type LintRunSource = 'startup' | 'healthcheck' | 'manual'
|
|
560
642
|
type ArtifactWriteFailureMode = 'warn' | 'error'
|
|
643
|
+
type OpenApiLintRuntimeStatus = 'idle' | 'running' | 'passed' | 'failed'
|
|
561
644
|
|
|
562
|
-
|
|
563
|
-
jsonReportPath?: string
|
|
564
|
-
junitReportPath?: string
|
|
565
|
-
sarifReportPath?: string
|
|
566
|
-
specSnapshotPath?: string
|
|
567
|
-
}
|
|
568
|
-
|
|
569
|
-
type OpenApiLintSink = {
|
|
570
|
-
name: string
|
|
571
|
-
write: (
|
|
572
|
-
result: LintRunResult,
|
|
573
|
-
context: {
|
|
574
|
-
spec: Record<string, unknown>
|
|
575
|
-
logger: SpectralLogger
|
|
576
|
-
}
|
|
577
|
-
) => undefined | Partial<OpenApiLintArtifacts> | Promise<undefined | Partial<OpenApiLintArtifacts>>
|
|
578
|
-
}
|
|
579
|
-
|
|
580
|
-
type RulesetResolver = (
|
|
581
|
-
input: string | RulesetDefinition | Record<string, unknown> | undefined,
|
|
582
|
-
context: {
|
|
583
|
-
baseDir: string
|
|
584
|
-
defaultRuleset: RulesetDefinition
|
|
585
|
-
mergeAutodiscoveredWithDefault: boolean
|
|
586
|
-
}
|
|
587
|
-
) => Promise<LoadedRuleset | undefined>
|
|
588
|
-
|
|
589
|
-
type LoadResolvedRulesetOptions = {
|
|
590
|
-
baseDir?: string
|
|
591
|
-
resolvers?: RulesetResolver[]
|
|
592
|
-
mergeAutodiscoveredWithDefault?: boolean
|
|
593
|
-
/** Override the base ruleset used for autodiscovery merging and the fallback when no ruleset is configured. */
|
|
594
|
-
defaultRuleset?: RulesetDefinition
|
|
595
|
-
}
|
|
645
|
+
// ── Plugin options ────────────────────────────────────────────────────────────
|
|
596
646
|
|
|
597
647
|
type SpectralPluginOptions = {
|
|
598
648
|
/** First-party governance preset. Sets the base ruleset and autodiscovery merge target. */
|
|
599
649
|
preset?: PresetName
|
|
600
650
|
/** Custom ruleset path, object, or inline definition. Merged on top of preset when both are set. */
|
|
601
651
|
ruleset?: string | RulesetDefinition | Record<string, unknown>
|
|
652
|
+
/** Severity level at which the lint run is considered failed. Defaults to 'error'. */
|
|
602
653
|
failOn?: SeverityThreshold
|
|
603
654
|
healthcheck?: false | { path?: string }
|
|
604
655
|
output?: {
|
|
656
|
+
/** Print findings to the console. Default: true. */
|
|
605
657
|
console?: boolean
|
|
606
658
|
jsonReportPath?: string
|
|
607
659
|
junitReportPath?: string
|
|
608
660
|
sarifReportPath?: string
|
|
661
|
+
/** true derives the path from the consuming app's package name. */
|
|
609
662
|
specSnapshotPath?: string | true
|
|
663
|
+
/** .yml/.yaml → OpenCollection YAML (Bruno v3+), .json → Bruno collection JSON */
|
|
664
|
+
brunoCollectionPath?: string
|
|
610
665
|
pretty?: boolean
|
|
666
|
+
/** Whether artifact write failures throw or warn. Default: 'warn'. */
|
|
611
667
|
artifactWriteFailures?: ArtifactWriteFailureMode
|
|
612
668
|
sinks?: OpenApiLintSink[]
|
|
613
669
|
}
|
|
@@ -615,16 +671,160 @@ type SpectralPluginOptions = {
|
|
|
615
671
|
specPath?: string
|
|
616
672
|
baseUrl?: string
|
|
617
673
|
}
|
|
618
|
-
|
|
674
|
+
/**
|
|
675
|
+
* Controls startup lint behaviour.
|
|
676
|
+
* startup.mode takes precedence over the legacy enabled option.
|
|
677
|
+
* 'enforce' — lint runs at startup and throws on threshold failure (default)
|
|
678
|
+
* 'report' — lint runs at startup, prints findings, but never blocks boot
|
|
679
|
+
* 'off' — startup lint is skipped entirely
|
|
680
|
+
*/
|
|
619
681
|
startup?: {
|
|
620
682
|
mode?: StartupLintMode
|
|
621
683
|
}
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
684
|
+
/**
|
|
685
|
+
* Legacy enable flag. Prefer startup.mode for new code.
|
|
686
|
+
* false or () => false is equivalent to startup.mode: 'off'.
|
|
687
|
+
* The function form receives process.env for environment-based toggling.
|
|
688
|
+
*/
|
|
689
|
+
enabled?: boolean | ((env: Record<string, string | undefined>) => boolean)
|
|
690
|
+
logger?: SpectralLogger
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
// ── Result types ──────────────────────────────────────────────────────────────
|
|
694
|
+
|
|
695
|
+
type LintFinding = {
|
|
696
|
+
code: string
|
|
697
|
+
message: string
|
|
698
|
+
severity: LintSeverity
|
|
699
|
+
path: Array<string | number>
|
|
700
|
+
documentPointer?: string
|
|
701
|
+
recommendation?: string
|
|
702
|
+
source?: string
|
|
703
|
+
range?: {
|
|
704
|
+
start?: { line: number; character: number }
|
|
705
|
+
end?: { line: number; character: number }
|
|
706
|
+
}
|
|
707
|
+
operation?: {
|
|
708
|
+
method?: string
|
|
709
|
+
path?: string
|
|
710
|
+
operationId?: string
|
|
626
711
|
}
|
|
627
712
|
}
|
|
713
|
+
|
|
714
|
+
type LintRunResult = {
|
|
715
|
+
/** True when no findings meet or exceed the configured failOn threshold. */
|
|
716
|
+
ok: boolean
|
|
717
|
+
generatedAt: string
|
|
718
|
+
/** Where the lint run was triggered from. */
|
|
719
|
+
source: LintRunSource
|
|
720
|
+
summary: {
|
|
721
|
+
error: number
|
|
722
|
+
warn: number
|
|
723
|
+
info: number
|
|
724
|
+
hint: number
|
|
725
|
+
total: number
|
|
726
|
+
}
|
|
727
|
+
artifacts?: OpenApiLintArtifacts
|
|
728
|
+
findings: LintFinding[]
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
type OpenApiLintArtifacts = {
|
|
732
|
+
jsonReportPath?: string
|
|
733
|
+
junitReportPath?: string
|
|
734
|
+
sarifReportPath?: string
|
|
735
|
+
specSnapshotPath?: string
|
|
736
|
+
brunoCollectionPath?: string
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
type OpenApiLintRuntimeFailure = {
|
|
740
|
+
name: string
|
|
741
|
+
message: string
|
|
742
|
+
generatedAt: string
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
// ── Runtime ───────────────────────────────────────────────────────────────────
|
|
746
|
+
|
|
747
|
+
type OpenApiLintRuntime = {
|
|
748
|
+
status: OpenApiLintRuntimeStatus
|
|
749
|
+
startedAt: string | null
|
|
750
|
+
completedAt: string | null
|
|
751
|
+
durationMs: number | null
|
|
752
|
+
latest: LintRunResult | null
|
|
753
|
+
lastSuccess: LintRunResult | null
|
|
754
|
+
lastFailure: OpenApiLintRuntimeFailure | null
|
|
755
|
+
running: boolean
|
|
756
|
+
run: (app: AnyElysia, source?: LintRunSource) => Promise<LintRunResult>
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
function createOpenApiLintRuntime(options?: SpectralPluginOptions): OpenApiLintRuntime
|
|
760
|
+
|
|
761
|
+
// ── Extension points (advanced) ───────────────────────────────────────────────
|
|
762
|
+
|
|
763
|
+
type SpectralLogger = {
|
|
764
|
+
info: (message: string) => void
|
|
765
|
+
warn: (message: string) => void
|
|
766
|
+
error: (message: string) => void
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
type OpenApiLintSinkContext = {
|
|
770
|
+
spec: Record<string, unknown>
|
|
771
|
+
logger: SpectralLogger
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
type OpenApiLintSink = {
|
|
775
|
+
name: string
|
|
776
|
+
write: (
|
|
777
|
+
result: LintRunResult,
|
|
778
|
+
context: OpenApiLintSinkContext,
|
|
779
|
+
) => undefined | Partial<OpenApiLintArtifacts> | Promise<undefined | Partial<OpenApiLintArtifacts>>
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
type RulesetResolver = (
|
|
783
|
+
input: string | RulesetDefinition | Record<string, unknown> | undefined,
|
|
784
|
+
context: RulesetResolverContext,
|
|
785
|
+
) => Promise<ResolvedRulesetCandidate | undefined>
|
|
786
|
+
|
|
787
|
+
type RulesetResolverContext = {
|
|
788
|
+
baseDir: string
|
|
789
|
+
defaultRuleset: RulesetDefinition
|
|
790
|
+
mergeAutodiscoveredWithDefault: boolean
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
type ResolvedRulesetCandidate = {
|
|
794
|
+
ruleset: unknown
|
|
795
|
+
source?: LoadedRuleset['source']
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
type LoadedRuleset = {
|
|
799
|
+
ruleset: RulesetDefinition
|
|
800
|
+
source?: {
|
|
801
|
+
path: string
|
|
802
|
+
autodiscovered: boolean
|
|
803
|
+
mergedWithDefault: boolean
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
type LoadResolvedRulesetOptions = {
|
|
808
|
+
baseDir?: string
|
|
809
|
+
resolvers?: RulesetResolver[]
|
|
810
|
+
mergeAutodiscoveredWithDefault?: boolean
|
|
811
|
+
/** Override the base ruleset used for autodiscovery merging and the fallback when no ruleset is configured. */
|
|
812
|
+
defaultRuleset?: RulesetDefinition
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
// ── Error classes ─────────────────────────────────────────────────────────────
|
|
816
|
+
|
|
817
|
+
class OpenApiLintThresholdError extends Error {
|
|
818
|
+
readonly threshold: SeverityThreshold
|
|
819
|
+
readonly result: LintRunResult
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
class OpenApiLintArtifactWriteError extends Error {
|
|
823
|
+
readonly artifact: string
|
|
824
|
+
readonly cause: unknown
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
class RulesetLoadError extends Error {}
|
|
628
828
|
```
|
|
629
829
|
|
|
630
830
|
### Presets
|
|
@@ -715,7 +915,7 @@ Example successful response:
|
|
|
715
915
|
|
|
716
916
|
The current output model has two layers:
|
|
717
917
|
|
|
718
|
-
- convenience options such as `jsonReportPath`, `junitReportPath`, `specSnapshotPath`, and `
|
|
918
|
+
- convenience options such as `jsonReportPath`, `junitReportPath`, `specSnapshotPath`, `sarifReportPath`, and `brunoCollectionPath`
|
|
719
919
|
- sink abstractions under `output.sinks`
|
|
720
920
|
|
|
721
921
|
The convenience options compile down to built-in sinks so the current API stays simple while the internal output model becomes extensible.
|
|
@@ -764,4 +964,4 @@ Production-grade linting needs more than a pass/fail boolean. The runtime tracks
|
|
|
764
964
|
|
|
765
965
|
### Project status
|
|
766
966
|
|
|
767
|
-
|
|
967
|
+
The package is actively developed toward a stable `v1`. Milestones 0.2 through 0.6 are complete. Ongoing work is tracked in [roadmap.md](../../roadmap.md).
|
package/dist/core/index.d.mts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import {
|
|
2
|
-
export { LoadResolvedRulesetOptions, LoadedRuleset, OpenApiLintArtifactWriteError, OpenApiLintThresholdError, ResolvedRulesetCandidate, RulesetLoadError, RulesetResolver, RulesetResolverContext, RulesetResolverInput, createOpenApiLintRuntime, defaultRulesetResolvers, enforceThreshold,
|
|
1
|
+
import { a as createOpenApiLintRuntime, c as ResolvedRulesetCandidate, d as RulesetResolverContext, f as RulesetResolverInput, g as lintOpenApi, h as loadRuleset, i as OpenApiLintArtifactWriteError, l as RulesetLoadError, m as loadResolvedRuleset, n as enforceThreshold, o as LoadResolvedRulesetOptions, p as defaultRulesetResolvers, r as shouldFail, s as LoadedRuleset, t as OpenApiLintThresholdError, u as RulesetResolver } from "../index-11HnbLDN.mjs";
|
|
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,2 @@
|
|
|
1
|
-
import { a as
|
|
2
|
-
export { OpenApiLintArtifactWriteError, OpenApiLintThresholdError, RulesetLoadError, createOpenApiLintRuntime, defaultRulesetResolvers, enforceThreshold,
|
|
1
|
+
import { a as enforceThreshold, d as RulesetLoadError, f as defaultRulesetResolvers, g as lintOpenApi, i as OpenApiLintThresholdError, m as loadRuleset, n as createOpenApiLintRuntime, o as shouldFail, p as loadResolvedRuleset, t as OpenApiLintArtifactWriteError } from "../core-DTKNy6TU.mjs";
|
|
2
|
+
export { OpenApiLintArtifactWriteError, OpenApiLintThresholdError, RulesetLoadError, createOpenApiLintRuntime, defaultRulesetResolvers, enforceThreshold, lintOpenApi, loadResolvedRuleset, loadRuleset, shouldFail };
|
|
@@ -7,6 +7,7 @@ import spectralRulesets from "@stoplight/spectral-rulesets";
|
|
|
7
7
|
import YAML from "yaml";
|
|
8
8
|
import signale from "signale";
|
|
9
9
|
import { styleText } from "node:util";
|
|
10
|
+
import { brunoToOpenCollection, openApiToBruno } from "@usebruno/converters";
|
|
10
11
|
//#region src/core/finding-guidance.ts
|
|
11
12
|
const guidanceByCode = {
|
|
12
13
|
"elysia-operation-summary": "Add detail.summary to the Elysia route options so generated docs and clients have a short operation label.",
|
|
@@ -408,7 +409,7 @@ const normalizeExtends = (value) => {
|
|
|
408
409
|
};
|
|
409
410
|
const resolveExtendsEntry = (value) => {
|
|
410
411
|
const resolved = extendsMap[value];
|
|
411
|
-
if (!resolved) throw new RulesetLoadError(`Unsupported ruleset extend target: ${value}.
|
|
412
|
+
if (!resolved) throw new RulesetLoadError(`Unsupported ruleset extend target: "${value}". Supported extend targets: spectral:oas.`);
|
|
412
413
|
return resolved;
|
|
413
414
|
};
|
|
414
415
|
const normalizeRules = (value, availableFunctions) => {
|
|
@@ -669,6 +670,17 @@ const buildSpecReference = (finding, specSnapshotPath) => {
|
|
|
669
670
|
return `${specSnapshotPath}#${finding.documentPointer}`;
|
|
670
671
|
};
|
|
671
672
|
//#endregion
|
|
673
|
+
//#region src/output/bruno-reporter.ts
|
|
674
|
+
const isYamlPath = (filePath) => filePath.endsWith(".yml") || filePath.endsWith(".yaml");
|
|
675
|
+
const writeBrunoCollection = async (outputPath, spec) => {
|
|
676
|
+
const resolvedPath = path.resolve(process.cwd(), outputPath);
|
|
677
|
+
const collection = openApiToBruno(spec);
|
|
678
|
+
const content = isYamlPath(outputPath) ? YAML.stringify(brunoToOpenCollection(collection)) : JSON.stringify(collection, null, 2);
|
|
679
|
+
await mkdir(path.dirname(resolvedPath), { recursive: true });
|
|
680
|
+
await writeFile(resolvedPath, content, "utf8");
|
|
681
|
+
return resolvedPath;
|
|
682
|
+
};
|
|
683
|
+
//#endregion
|
|
672
684
|
//#region src/output/json-reporter.ts
|
|
673
685
|
const DEFAULT_SPEC_SNAPSHOT_FILENAME = "open-api.json";
|
|
674
686
|
const writeJsonArtifact = async (artifactPath, payload, pretty = true) => {
|
|
@@ -867,6 +879,16 @@ const createOutputSinks = (options) => {
|
|
|
867
879
|
return { junitReportPath: writtenJunitReportPath };
|
|
868
880
|
}
|
|
869
881
|
});
|
|
882
|
+
const configuredBrunoCollectionPath = options.output?.brunoCollectionPath;
|
|
883
|
+
if (configuredBrunoCollectionPath) sinks.push({
|
|
884
|
+
name: "Bruno collection",
|
|
885
|
+
kind: "artifact",
|
|
886
|
+
async write(_result, context) {
|
|
887
|
+
const writtenPath = await writeBrunoCollection(configuredBrunoCollectionPath, context.spec);
|
|
888
|
+
reporter.artifact(`OpenAPI lint wrote Bruno collection to ${writtenPath}.`);
|
|
889
|
+
return { brunoCollectionPath: writtenPath };
|
|
890
|
+
}
|
|
891
|
+
});
|
|
870
892
|
if (configuredSarifReportPath) sinks.push({
|
|
871
893
|
name: "SARIF report",
|
|
872
894
|
kind: "artifact",
|
|
@@ -1176,6 +1198,7 @@ const createOpenApiLintRuntime = (options = {}) => {
|
|
|
1176
1198
|
else if (loadedRuleset.source?.path) reporter.ruleset(`OpenAPI lint loaded ruleset ${loadedRuleset.source.path}.`);
|
|
1177
1199
|
const result = await lintOpenApi(spec, loadedRuleset.ruleset);
|
|
1178
1200
|
result.source = source;
|
|
1201
|
+
result.ok = !shouldFail(result, options.failOn ?? "error");
|
|
1179
1202
|
await writeOutputSinks(result, spec, options, artifactWriteFailureMode);
|
|
1180
1203
|
runtime.latest = result;
|
|
1181
1204
|
reporter.complete("OpenAPI lint completed.");
|
|
@@ -1245,13 +1268,10 @@ const mergeArtifacts = (current, next) => ({
|
|
|
1245
1268
|
...current,
|
|
1246
1269
|
...next
|
|
1247
1270
|
});
|
|
1248
|
-
const isEnabled = (options = {}) => {
|
|
1249
|
-
return resolveStartupMode(options) !== "off";
|
|
1250
|
-
};
|
|
1251
1271
|
const resolveStartupMode = (options = {}) => {
|
|
1252
1272
|
if (options.startup?.mode) return options.startup.mode;
|
|
1253
1273
|
if (typeof options.enabled === "function") return options.enabled(process.env) ? "enforce" : "off";
|
|
1254
1274
|
return options.enabled === false ? "off" : "enforce";
|
|
1255
1275
|
};
|
|
1256
1276
|
//#endregion
|
|
1257
|
-
export {
|
|
1277
|
+
export { enforceThreshold as a, strict as c, RulesetLoadError as d, defaultRulesetResolvers as f, lintOpenApi as g, recommended as h, OpenApiLintThresholdError as i, server as l, loadRuleset as m, createOpenApiLintRuntime as n, shouldFail as o, loadResolvedRuleset as p, resolveStartupMode as r, presets as s, OpenApiLintArtifactWriteError as t, resolveReporter as u };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { RulesetDefinition } from "@stoplight/spectral-core";
|
|
2
2
|
import { AnyElysia } from "elysia";
|
|
3
3
|
|
|
4
4
|
//#region src/types.d.ts
|
|
@@ -19,6 +19,7 @@ type OpenApiLintArtifacts = {
|
|
|
19
19
|
junitReportPath?: string;
|
|
20
20
|
sarifReportPath?: string;
|
|
21
21
|
specSnapshotPath?: string;
|
|
22
|
+
brunoCollectionPath?: string;
|
|
22
23
|
};
|
|
23
24
|
type OpenApiLintSinkContext = {
|
|
24
25
|
spec: Record<string, unknown>;
|
|
@@ -41,6 +42,7 @@ type SpectralPluginOptions = {
|
|
|
41
42
|
junitReportPath?: string;
|
|
42
43
|
sarifReportPath?: string;
|
|
43
44
|
specSnapshotPath?: string | true;
|
|
45
|
+
brunoCollectionPath?: string;
|
|
44
46
|
pretty?: boolean;
|
|
45
47
|
artifactWriteFailures?: ArtifactWriteFailureMode;
|
|
46
48
|
sinks?: OpenApiLintSink[];
|
|
@@ -93,9 +95,6 @@ type LintRunResult = {
|
|
|
93
95
|
artifacts?: OpenApiLintArtifacts;
|
|
94
96
|
findings: LintFinding[];
|
|
95
97
|
};
|
|
96
|
-
interface SpecProvider {
|
|
97
|
-
getSpec(): Promise<unknown>;
|
|
98
|
-
}
|
|
99
98
|
type OpenApiLintRuntimeFailure = {
|
|
100
99
|
name: string;
|
|
101
100
|
message: string;
|
|
@@ -151,9 +150,6 @@ declare const loadRuleset: (input?: RulesetResolverInput, baseDirOrOptions?: str
|
|
|
151
150
|
declare const loadResolvedRuleset: (input?: RulesetResolverInput, baseDirOrOptions?: string | LoadResolvedRulesetOptions) => Promise<LoadedRuleset>;
|
|
152
151
|
declare const defaultRulesetResolvers: RulesetResolver[];
|
|
153
152
|
//#endregion
|
|
154
|
-
//#region src/core/normalize-findings.d.ts
|
|
155
|
-
declare const normalizeFindings: (diagnostics: ISpectralDiagnostic[], spec: unknown) => LintRunResult;
|
|
156
|
-
//#endregion
|
|
157
153
|
//#region src/core/runtime.d.ts
|
|
158
154
|
declare const createOpenApiLintRuntime: (options?: SpectralPluginOptions) => OpenApiLintRuntime;
|
|
159
155
|
declare class OpenApiLintArtifactWriteError extends Error {
|
|
@@ -161,8 +157,6 @@ declare class OpenApiLintArtifactWriteError extends Error {
|
|
|
161
157
|
readonly cause: unknown;
|
|
162
158
|
constructor(artifact: string, cause: unknown);
|
|
163
159
|
}
|
|
164
|
-
declare const isEnabled: (options?: SpectralPluginOptions) => boolean;
|
|
165
|
-
declare const resolveStartupMode: (options?: SpectralPluginOptions) => StartupLintMode;
|
|
166
160
|
//#endregion
|
|
167
161
|
//#region src/core/thresholds.d.ts
|
|
168
162
|
declare class OpenApiLintThresholdError extends Error {
|
|
@@ -170,8 +164,7 @@ declare class OpenApiLintThresholdError extends Error {
|
|
|
170
164
|
readonly result: LintRunResult;
|
|
171
165
|
constructor(threshold: SeverityThreshold, result: LintRunResult);
|
|
172
166
|
}
|
|
173
|
-
declare const exceedsThreshold: (severity: LintSeverity, threshold: SeverityThreshold) => boolean;
|
|
174
167
|
declare const shouldFail: (result: LintRunResult, threshold: SeverityThreshold) => boolean;
|
|
175
168
|
declare const enforceThreshold: (result: LintRunResult, threshold: SeverityThreshold) => void;
|
|
176
169
|
//#endregion
|
|
177
|
-
export {
|
|
170
|
+
export { SpectralLogger as A, OpenApiLintRuntime as C, OpenApiLintSinkContext as D, OpenApiLintSink as E, StartupLintMode as M, PresetName as O, OpenApiLintArtifacts as S, OpenApiLintRuntimeStatus as T, ArtifactWriteFailureMode as _, createOpenApiLintRuntime as a, LintRunSource as b, ResolvedRulesetCandidate as c, RulesetResolverContext as d, RulesetResolverInput as f, lintOpenApi as g, loadRuleset as h, OpenApiLintArtifactWriteError as i, SpectralPluginOptions as j, SeverityThreshold as k, RulesetLoadError as l, loadResolvedRuleset as m, enforceThreshold as n, LoadResolvedRulesetOptions as o, defaultRulesetResolvers as p, shouldFail as r, LoadedRuleset as s, OpenApiLintThresholdError as t, RulesetResolver as u, LintFinding as v, OpenApiLintRuntimeFailure as w, LintSeverity as x, LintRunResult as y };
|
package/dist/index.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { A as
|
|
1
|
+
import { A as SpectralLogger, C as OpenApiLintRuntime, D as OpenApiLintSinkContext, E as OpenApiLintSink, M as StartupLintMode, O as PresetName, S as OpenApiLintArtifacts, T as OpenApiLintRuntimeStatus, _ as ArtifactWriteFailureMode, a as createOpenApiLintRuntime, b as LintRunSource, c as ResolvedRulesetCandidate, d as RulesetResolverContext, f as RulesetResolverInput, g as lintOpenApi, h as loadRuleset, i as OpenApiLintArtifactWriteError, j as SpectralPluginOptions, k as SeverityThreshold, l as RulesetLoadError, m as loadResolvedRuleset, n as enforceThreshold, o as LoadResolvedRulesetOptions, p as defaultRulesetResolvers, r as shouldFail, s as LoadedRuleset, t as OpenApiLintThresholdError, u as RulesetResolver, v as LintFinding, w as OpenApiLintRuntimeFailure, x as LintSeverity, y as LintRunResult } from "./index-11HnbLDN.mjs";
|
|
2
2
|
import { RulesetDefinition } from "@stoplight/spectral-core";
|
|
3
3
|
import { Elysia } from "elysia";
|
|
4
4
|
|
|
@@ -72,4 +72,4 @@ declare const strict: RulesetDefinition;
|
|
|
72
72
|
//#region src/presets/index.d.ts
|
|
73
73
|
declare const presets: Record<PresetName, RulesetDefinition>;
|
|
74
74
|
//#endregion
|
|
75
|
-
export { ArtifactWriteFailureMode, LintFinding, LintRunResult, LintRunSource, LintSeverity, LoadResolvedRulesetOptions, LoadedRuleset, OpenApiLintArtifactWriteError, OpenApiLintArtifacts, OpenApiLintRuntime, OpenApiLintRuntimeFailure, OpenApiLintRuntimeStatus, OpenApiLintSink, OpenApiLintSinkContext, OpenApiLintThresholdError, PresetName, ResolvedRulesetCandidate, RulesetLoadError, RulesetResolver, RulesetResolverContext, RulesetResolverInput, SeverityThreshold,
|
|
75
|
+
export { ArtifactWriteFailureMode, LintFinding, LintRunResult, LintRunSource, LintSeverity, LoadResolvedRulesetOptions, LoadedRuleset, OpenApiLintArtifactWriteError, OpenApiLintArtifacts, OpenApiLintRuntime, OpenApiLintRuntimeFailure, OpenApiLintRuntimeStatus, OpenApiLintSink, OpenApiLintSinkContext, OpenApiLintThresholdError, PresetName, ResolvedRulesetCandidate, RulesetLoadError, RulesetResolver, RulesetResolverContext, RulesetResolverInput, SeverityThreshold, SpectralLogger, SpectralPluginOptions, StartupLintMode, createOpenApiLintRuntime, defaultRulesetResolvers, enforceThreshold, lintOpenApi, loadResolvedRuleset, loadRuleset, presets, recommended, server, shouldFail, spectralPlugin, strict };
|
package/dist/index.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { a as enforceThreshold, c as strict, d as RulesetLoadError, f as defaultRulesetResolvers, g as lintOpenApi, h as recommended, i as OpenApiLintThresholdError, l as server, m as loadRuleset, n as createOpenApiLintRuntime, o as shouldFail, p as loadResolvedRuleset, r as resolveStartupMode, s as presets, t as OpenApiLintArtifactWriteError, u as resolveReporter } from "./core-DTKNy6TU.mjs";
|
|
2
2
|
import { Elysia } from "elysia";
|
|
3
3
|
//#region src/plugin.ts
|
|
4
4
|
const spectralPlugin = (options = {}) => {
|
|
@@ -82,4 +82,4 @@ const spectralPlugin = (options = {}) => {
|
|
|
82
82
|
return plugin;
|
|
83
83
|
};
|
|
84
84
|
//#endregion
|
|
85
|
-
export { OpenApiLintArtifactWriteError, OpenApiLintThresholdError, RulesetLoadError, createOpenApiLintRuntime, defaultRulesetResolvers, enforceThreshold,
|
|
85
|
+
export { OpenApiLintArtifactWriteError, OpenApiLintThresholdError, RulesetLoadError, createOpenApiLintRuntime, defaultRulesetResolvers, enforceThreshold, lintOpenApi, loadResolvedRuleset, loadRuleset, presets, recommended, server, shouldFail, spectralPlugin, strict };
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opsydyn/elysia-spectral",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.2",
|
|
4
4
|
"description": "Thin Elysia plugin that lints generated OpenAPI documents with Spectral.",
|
|
5
|
-
"packageManager": "bun@1.
|
|
5
|
+
"packageManager": "bun@1.3.11",
|
|
6
6
|
"publishConfig": {
|
|
7
7
|
"access": "public"
|
|
8
8
|
},
|
|
@@ -71,6 +71,8 @@
|
|
|
71
71
|
"@stoplight/spectral-functions": "^1.10.2",
|
|
72
72
|
"@stoplight/spectral-parsers": "^1.0.4",
|
|
73
73
|
"@stoplight/spectral-rulesets": "^1.22.1",
|
|
74
|
+
"@usebruno/converters": "^0.18.1",
|
|
75
|
+
"@usebruno/lang": "^0.35.0",
|
|
74
76
|
"signale": "^1.4.0",
|
|
75
77
|
"yaml": "^2.8.1"
|
|
76
78
|
},
|