@opsydyn/elysia-spectral 0.2.4

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.
@@ -0,0 +1,172 @@
1
+ import { ISpectralDiagnostic, RulesetDefinition } from "@stoplight/spectral-core";
2
+ import { AnyElysia } from "elysia";
3
+
4
+ //#region src/types.d.ts
5
+ type SeverityThreshold = 'error' | 'warn' | 'info' | 'hint' | 'never';
6
+ type LintSeverity = 'error' | 'warn' | 'info' | 'hint';
7
+ type StartupLintMode = 'enforce' | 'report' | 'off';
8
+ type OpenApiLintRuntimeStatus = 'idle' | 'running' | 'passed' | 'failed';
9
+ type ArtifactWriteFailureMode = 'warn' | 'error';
10
+ type SpectralLogger = {
11
+ info: (message: string) => void;
12
+ warn: (message: string) => void;
13
+ error: (message: string) => void;
14
+ };
15
+ type OpenApiLintArtifacts = {
16
+ jsonReportPath?: string;
17
+ junitReportPath?: string;
18
+ sarifReportPath?: string;
19
+ specSnapshotPath?: string;
20
+ };
21
+ type OpenApiLintSinkContext = {
22
+ spec: Record<string, unknown>;
23
+ logger: SpectralLogger;
24
+ };
25
+ type OpenApiLintSink = {
26
+ name: string;
27
+ write: (result: LintRunResult, context: OpenApiLintSinkContext) => undefined | Partial<OpenApiLintArtifacts> | Promise<undefined | Partial<OpenApiLintArtifacts>>;
28
+ };
29
+ type SpectralPluginOptions = {
30
+ ruleset?: string | RulesetDefinition | Record<string, unknown>;
31
+ failOn?: SeverityThreshold;
32
+ healthcheck?: false | {
33
+ path?: string;
34
+ };
35
+ output?: {
36
+ console?: boolean;
37
+ jsonReportPath?: string;
38
+ junitReportPath?: string;
39
+ sarifReportPath?: string;
40
+ specSnapshotPath?: string | true;
41
+ pretty?: boolean;
42
+ artifactWriteFailures?: ArtifactWriteFailureMode;
43
+ sinks?: OpenApiLintSink[];
44
+ };
45
+ source?: {
46
+ specPath?: string;
47
+ baseUrl?: string;
48
+ };
49
+ enabled?: boolean | ((env: Record<string, string | undefined>) => boolean);
50
+ startup?: {
51
+ mode?: StartupLintMode;
52
+ };
53
+ logger?: SpectralLogger;
54
+ };
55
+ type LintFinding = {
56
+ code: string;
57
+ message: string;
58
+ severity: LintSeverity;
59
+ path: Array<string | number>;
60
+ documentPointer?: string;
61
+ recommendation?: string;
62
+ source?: string;
63
+ range?: {
64
+ start?: {
65
+ line: number;
66
+ character: number;
67
+ };
68
+ end?: {
69
+ line: number;
70
+ character: number;
71
+ };
72
+ };
73
+ operation?: {
74
+ method?: string;
75
+ path?: string;
76
+ operationId?: string;
77
+ };
78
+ };
79
+ type LintRunResult = {
80
+ ok: boolean;
81
+ generatedAt: string;
82
+ summary: {
83
+ error: number;
84
+ warn: number;
85
+ info: number;
86
+ hint: number;
87
+ total: number;
88
+ };
89
+ artifacts?: OpenApiLintArtifacts;
90
+ findings: LintFinding[];
91
+ };
92
+ interface SpecProvider {
93
+ getSpec(): Promise<unknown>;
94
+ }
95
+ type OpenApiLintRuntimeFailure = {
96
+ name: string;
97
+ message: string;
98
+ generatedAt: string;
99
+ };
100
+ type OpenApiLintRuntime = {
101
+ status: OpenApiLintRuntimeStatus;
102
+ startedAt: string | null;
103
+ completedAt: string | null;
104
+ durationMs: number | null;
105
+ latest: LintRunResult | null;
106
+ lastSuccess: LintRunResult | null;
107
+ lastFailure: OpenApiLintRuntimeFailure | null;
108
+ running: boolean;
109
+ run: (app: AnyElysia) => Promise<LintRunResult>;
110
+ };
111
+ //#endregion
112
+ //#region src/core/lint-openapi.d.ts
113
+ declare const lintOpenApi: (spec: Record<string, unknown>, ruleset: RulesetDefinition) => Promise<LintRunResult>;
114
+ //#endregion
115
+ //#region src/core/load-ruleset.d.ts
116
+ type LoadedRuleset = {
117
+ ruleset: RulesetDefinition;
118
+ source?: {
119
+ path: string;
120
+ autodiscovered: boolean;
121
+ mergedWithDefault: boolean;
122
+ };
123
+ };
124
+ type ResolvedRulesetCandidate = {
125
+ ruleset: unknown;
126
+ source?: LoadedRuleset['source'];
127
+ };
128
+ type RulesetResolverInput = string | RulesetDefinition | Record<string, unknown> | undefined;
129
+ type RulesetResolverContext = {
130
+ baseDir: string;
131
+ defaultRuleset: RulesetDefinition;
132
+ mergeAutodiscoveredWithDefault: boolean;
133
+ };
134
+ type RulesetResolver = (input: RulesetResolverInput, context: RulesetResolverContext) => Promise<ResolvedRulesetCandidate | undefined>;
135
+ type LoadResolvedRulesetOptions = {
136
+ baseDir?: string;
137
+ resolvers?: RulesetResolver[];
138
+ mergeAutodiscoveredWithDefault?: boolean;
139
+ };
140
+ declare class RulesetLoadError extends Error {
141
+ constructor(message: string, options?: {
142
+ cause?: unknown;
143
+ });
144
+ }
145
+ declare const loadRuleset: (input?: RulesetResolverInput, baseDirOrOptions?: string | LoadResolvedRulesetOptions) => Promise<RulesetDefinition>;
146
+ declare const loadResolvedRuleset: (input?: RulesetResolverInput, baseDirOrOptions?: string | LoadResolvedRulesetOptions) => Promise<LoadedRuleset>;
147
+ declare const defaultRulesetResolvers: RulesetResolver[];
148
+ //#endregion
149
+ //#region src/core/normalize-findings.d.ts
150
+ declare const normalizeFindings: (diagnostics: ISpectralDiagnostic[], spec: unknown) => LintRunResult;
151
+ //#endregion
152
+ //#region src/core/runtime.d.ts
153
+ declare const createOpenApiLintRuntime: (options?: SpectralPluginOptions) => OpenApiLintRuntime;
154
+ declare class OpenApiLintArtifactWriteError extends Error {
155
+ readonly artifact: string;
156
+ readonly cause: unknown;
157
+ constructor(artifact: string, cause: unknown);
158
+ }
159
+ declare const isEnabled: (options?: SpectralPluginOptions) => boolean;
160
+ declare const resolveStartupMode: (options?: SpectralPluginOptions) => StartupLintMode;
161
+ //#endregion
162
+ //#region src/core/thresholds.d.ts
163
+ declare class OpenApiLintThresholdError extends Error {
164
+ readonly threshold: SeverityThreshold;
165
+ readonly result: LintRunResult;
166
+ constructor(threshold: SeverityThreshold, result: LintRunResult);
167
+ }
168
+ declare const exceedsThreshold: (severity: LintSeverity, threshold: SeverityThreshold) => boolean;
169
+ declare const shouldFail: (result: LintRunResult, threshold: SeverityThreshold) => boolean;
170
+ declare const enforceThreshold: (result: LintRunResult, threshold: SeverityThreshold) => void;
171
+ //#endregion
172
+ export { OpenApiLintSinkContext as A, LintRunResult as C, OpenApiLintRuntimeFailure as D, OpenApiLintRuntime as E, StartupLintMode as F, SpecProvider as M, SpectralLogger as N, OpenApiLintRuntimeStatus as O, SpectralPluginOptions as P, LintFinding as S, OpenApiLintArtifacts as T, defaultRulesetResolvers as _, OpenApiLintArtifactWriteError as a, lintOpenApi as b, resolveStartupMode as c, LoadedRuleset as d, ResolvedRulesetCandidate as f, RulesetResolverInput as g, RulesetResolverContext as h, shouldFail as i, SeverityThreshold as j, OpenApiLintSink as k, normalizeFindings as l, RulesetResolver as m, enforceThreshold as n, createOpenApiLintRuntime as o, RulesetLoadError as p, exceedsThreshold as r, isEnabled as s, OpenApiLintThresholdError as t, LoadResolvedRulesetOptions as u, loadResolvedRuleset as v, LintSeverity as w, ArtifactWriteFailureMode as x, loadRuleset as y };
@@ -0,0 +1,36 @@
1
+ import { A as OpenApiLintSinkContext, C as LintRunResult, D as OpenApiLintRuntimeFailure, E as OpenApiLintRuntime, F as StartupLintMode, M as SpecProvider, N as SpectralLogger, O as OpenApiLintRuntimeStatus, P as SpectralPluginOptions, S as LintFinding, T as OpenApiLintArtifacts, _ as defaultRulesetResolvers, a as OpenApiLintArtifactWriteError, b as lintOpenApi, c as resolveStartupMode, d as LoadedRuleset, f as ResolvedRulesetCandidate, g as RulesetResolverInput, h as RulesetResolverContext, i as shouldFail, j as SeverityThreshold, k as OpenApiLintSink, l as normalizeFindings, m as RulesetResolver, n as enforceThreshold, o as createOpenApiLintRuntime, p as RulesetLoadError, r as exceedsThreshold, s as isEnabled, t as OpenApiLintThresholdError, u as LoadResolvedRulesetOptions, v as loadResolvedRuleset, w as LintSeverity, x as ArtifactWriteFailureMode, y as loadRuleset } from "./index-BrFQCFDI.mjs";
2
+ import { Elysia } from "elysia";
3
+
4
+ //#region src/plugin.d.ts
5
+ declare const spectralPlugin: (options?: SpectralPluginOptions) => Elysia<"", {
6
+ decorator: {};
7
+ store: {
8
+ openApiLint: OpenApiLintRuntime;
9
+ };
10
+ derive: {};
11
+ resolve: {};
12
+ }, {
13
+ typebox: {};
14
+ error: {};
15
+ }, {
16
+ schema: {};
17
+ standaloneSchema: {};
18
+ macro: {};
19
+ macroFn: {};
20
+ parser: {};
21
+ response: {};
22
+ }, {}, {
23
+ derive: {};
24
+ resolve: {};
25
+ schema: {};
26
+ standaloneSchema: {};
27
+ response: {};
28
+ }, {
29
+ derive: {};
30
+ resolve: {};
31
+ schema: {};
32
+ standaloneSchema: {};
33
+ response: {};
34
+ }>;
35
+ //#endregion
36
+ export { ArtifactWriteFailureMode, LintFinding, LintRunResult, LintSeverity, LoadResolvedRulesetOptions, LoadedRuleset, OpenApiLintArtifactWriteError, OpenApiLintArtifacts, OpenApiLintRuntime, OpenApiLintRuntimeFailure, OpenApiLintRuntimeStatus, OpenApiLintSink, OpenApiLintSinkContext, OpenApiLintThresholdError, ResolvedRulesetCandidate, RulesetLoadError, RulesetResolver, RulesetResolverContext, RulesetResolverInput, SeverityThreshold, SpecProvider, SpectralLogger, SpectralPluginOptions, StartupLintMode, createOpenApiLintRuntime, defaultRulesetResolvers, enforceThreshold, exceedsThreshold, isEnabled, lintOpenApi, loadResolvedRuleset, loadRuleset, normalizeFindings, resolveStartupMode, shouldFail, spectralPlugin };
package/dist/index.mjs ADDED
@@ -0,0 +1,85 @@
1
+ import { a as OpenApiLintThresholdError, c as shouldFail, d as defaultRulesetResolvers, f as loadResolvedRuleset, h as normalizeFindings, i as resolveStartupMode, l as resolveReporter, m as lintOpenApi, n as createOpenApiLintRuntime, o as enforceThreshold, p as loadRuleset, r as isEnabled, s as exceedsThreshold, t as OpenApiLintArtifactWriteError, u as RulesetLoadError } from "./core-Czin3kvK.mjs";
2
+ import { Elysia } from "elysia";
3
+ //#region src/plugin.ts
4
+ const spectralPlugin = (options = {}) => {
5
+ const runtime = createOpenApiLintRuntime(options);
6
+ const hostAppRef = { current: null };
7
+ const reporter = resolveReporter(options.logger);
8
+ let plugin = new Elysia({ name: "@opsydyn/elysia-spectral" }).state("openApiLint", runtime).onStart(async (context) => {
9
+ const app = context.app ?? context;
10
+ hostAppRef.current = app;
11
+ const startupMode = resolveStartupMode(options);
12
+ if (startupMode === "off") return;
13
+ try {
14
+ await runtime.run(app);
15
+ } catch (error) {
16
+ if (startupMode === "report" && error instanceof OpenApiLintThresholdError) {
17
+ reporter.report(`OpenAPI lint exceeded the "${options.failOn ?? "error"}" threshold, but startup is continuing because startup.mode is "report".`);
18
+ return;
19
+ }
20
+ throw error;
21
+ }
22
+ });
23
+ if (options.healthcheck) {
24
+ const healthcheckPath = options.healthcheck.path ?? "/__openapi/health";
25
+ plugin = plugin.get(healthcheckPath, async ({ request, set }) => {
26
+ const fresh = new URL(request.url).searchParams.get("fresh") === "1";
27
+ const threshold = options.failOn ?? "error";
28
+ const currentApp = hostAppRef.current;
29
+ if (!currentApp) {
30
+ set.status = 503;
31
+ return {
32
+ ok: false,
33
+ cached: false,
34
+ threshold,
35
+ error: "OpenAPI lint runtime is not initialized yet."
36
+ };
37
+ }
38
+ try {
39
+ const usedCache = !fresh && (runtime.latest !== null || runtime.running);
40
+ const result = usedCache ? runtime.latest ?? await runtime.run(currentApp) : await runtime.run(currentApp);
41
+ if (result === null) {
42
+ set.status = 500;
43
+ return {
44
+ ok: false,
45
+ cached: false,
46
+ threshold,
47
+ error: "OpenAPI lint returned no result."
48
+ };
49
+ }
50
+ const healthy = !shouldFail(result, threshold);
51
+ set.status = healthy ? 200 : 503;
52
+ return {
53
+ ok: healthy,
54
+ cached: usedCache,
55
+ threshold,
56
+ result
57
+ };
58
+ } catch (error) {
59
+ if (error instanceof OpenApiLintThresholdError) {
60
+ set.status = 503;
61
+ return {
62
+ ok: false,
63
+ cached: false,
64
+ threshold,
65
+ result: error.result,
66
+ error: error.message
67
+ };
68
+ }
69
+ set.status = 500;
70
+ return {
71
+ ok: false,
72
+ cached: false,
73
+ threshold,
74
+ error: error instanceof Error ? error.message : String(error)
75
+ };
76
+ }
77
+ }, { detail: {
78
+ hide: true,
79
+ summary: "OpenAPI lint healthcheck"
80
+ } });
81
+ }
82
+ return plugin;
83
+ };
84
+ //#endregion
85
+ export { OpenApiLintArtifactWriteError, OpenApiLintThresholdError, RulesetLoadError, createOpenApiLintRuntime, defaultRulesetResolvers, enforceThreshold, exceedsThreshold, isEnabled, lintOpenApi, loadResolvedRuleset, loadRuleset, normalizeFindings, resolveStartupMode, shouldFail, spectralPlugin };
package/package.json ADDED
@@ -0,0 +1,86 @@
1
+ {
2
+ "name": "@opsydyn/elysia-spectral",
3
+ "version": "0.2.4",
4
+ "description": "Thin Elysia plugin that lints generated OpenAPI documents with Spectral.",
5
+ "packageManager": "bun@1.2.9",
6
+ "publishConfig": {
7
+ "access": "public"
8
+ },
9
+ "sideEffects": false,
10
+ "type": "module",
11
+ "main": "./dist/index.mjs",
12
+ "module": "./dist/index.mjs",
13
+ "types": "./dist/index.d.mts",
14
+ "exports": {
15
+ ".": {
16
+ "types": "./dist/index.d.mts",
17
+ "import": "./dist/index.mjs"
18
+ },
19
+ "./core": {
20
+ "types": "./dist/core/index.d.mts",
21
+ "import": "./dist/core/index.mjs"
22
+ }
23
+ },
24
+ "typesVersions": {
25
+ "*": {
26
+ "core": [
27
+ "./dist/core/index.d.mts"
28
+ ]
29
+ }
30
+ },
31
+ "files": [
32
+ "CHANGELOG.md",
33
+ "dist",
34
+ "README.md",
35
+ "spectral.yaml"
36
+ ],
37
+ "scripts": {
38
+ "build": "tsdown",
39
+ "typecheck": "tsc --noEmit",
40
+ "test": "bun test",
41
+ "test:watch": "bun test --watch"
42
+ },
43
+ "keywords": [
44
+ "elysia",
45
+ "openapi",
46
+ "spectral",
47
+ "lint",
48
+ "bun"
49
+ ],
50
+ "author": "Alan P Currie",
51
+ "license": "MIT",
52
+ "repository": {
53
+ "type": "git",
54
+ "url": "git+https://github.com/opsydyn/elysia-spectral.git",
55
+ "directory": "packages/elysia-spectral"
56
+ },
57
+ "homepage": "https://github.com/opsydyn/elysia-spectral#readme",
58
+ "bugs": {
59
+ "url": "https://github.com/opsydyn/elysia-spectral/issues"
60
+ },
61
+ "engines": {
62
+ "node": ">=22.0.0",
63
+ "bun": ">=1.3.0"
64
+ },
65
+ "peerDependencies": {
66
+ "elysia": "^1.4.0"
67
+ },
68
+ "dependencies": {
69
+ "@stoplight/spectral-core": "^1.22.0",
70
+ "@stoplight/spectral-formats": "^1.8.0",
71
+ "@stoplight/spectral-functions": "^1.10.2",
72
+ "@stoplight/spectral-parsers": "^1.0.4",
73
+ "@stoplight/spectral-rulesets": "^1.22.1",
74
+ "signale": "^1.4.0",
75
+ "yaml": "^2.8.1"
76
+ },
77
+ "devDependencies": {
78
+ "@elysiajs/openapi": "^1.4.0",
79
+ "@types/bun": "^1.3.12",
80
+ "@types/node": "^25.6.0",
81
+ "@types/signale": "^1.4.7",
82
+ "elysia": "^1.4.0",
83
+ "tsdown": "^0.21.8",
84
+ "typescript": "^5.8.3"
85
+ }
86
+ }
package/spectral.yaml ADDED
@@ -0,0 +1,25 @@
1
+ extends:
2
+ - spectral:oas
3
+ rules:
4
+ oas3-api-servers: off
5
+ info-contact: off
6
+
7
+ elysia-operation-summary:
8
+ description: Operations should define a summary for generated docs and clients.
9
+ severity: warn
10
+ given: $.paths[*][get,put,post,delete,options,head,patch,trace]
11
+ then:
12
+ field: summary
13
+ function: truthy
14
+
15
+ elysia-operation-tags:
16
+ description: Operations should declare at least one tag for grouping and downstream tooling.
17
+ severity: warn
18
+ given: $.paths[*][get,put,post,delete,options,head,patch,trace]
19
+ then:
20
+ field: tags
21
+ function: schema
22
+ functionOptions:
23
+ schema:
24
+ type: array
25
+ minItems: 1