@opsydyn/elysia-spectral 1.5.0 → 1.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.
@@ -1,446 +1,10 @@
1
- import spectralCore from "@stoplight/spectral-core";
2
- import { access, mkdir, readFile, writeFile } from "node:fs/promises";
3
1
  import path from "node:path";
4
- import { pathToFileURL } from "node:url";
5
- import spectralFunctions from "@stoplight/spectral-functions";
6
- import spectralRulesets from "@stoplight/spectral-rulesets";
7
- import YAML from "yaml";
8
2
  import signale from "signale";
9
3
  import { styleText } from "node:util";
4
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
10
5
  import { brunoToOpenCollection, openApiToBruno } from "@usebruno/converters";
11
- //#region src/core/finding-guidance.ts
12
- const guidanceByCode = {
13
- "elysia-operation-summary": "Add detail.summary to the Elysia route options so generated docs and clients have a short operation label.",
14
- "elysia-operation-tags": "Add detail.tags with at least one stable tag, for example ['Users'] or ['Dev'].",
15
- "operation-description": "Add detail.description with a short user-facing explanation of what the route does.",
16
- "operation-tags": "Add a non-empty detail.tags array on the route so the OpenAPI operation is grouped consistently.",
17
- "operation-operationId": "Add detail.operationId with a unique camelCase identifier so generated clients and SDKs have stable method names.",
18
- "operation-success-response": "Add at least one 2xx response schema to the route, for example response: { 200: t.Object(...) }.",
19
- "oas3-api-servers": "Add a servers array to the OpenAPI documentation config with at least one base URL.",
20
- "info-contact": "Add an info.contact object to the OpenAPI documentation config with a name and url or email.",
21
- "rfc9457-problem-details": "Add an \"application/problem+json\" content entry to the error response. See RFC 9457 for the Problem Details schema."
22
- };
23
- const getFindingRecommendation = (code, message) => {
24
- const direct = guidanceByCode[code];
25
- if (direct) return direct;
26
- if (code === "oas3-schema" && message.includes("required property \"responses\"")) return "Add a response schema to the route, for example response: { 200: t.Object(...) } or response: { 200: t.Array(...) }.";
27
- if (code.startsWith("operation-")) return "Add the missing operation metadata under detail on the Elysia route options.";
28
- };
29
- //#endregion
30
- //#region src/core/normalize-findings.ts
31
- const httpMethods = new Set([
32
- "get",
33
- "put",
34
- "post",
35
- "delete",
36
- "options",
37
- "head",
38
- "patch",
39
- "trace"
40
- ]);
41
- const normalizeFindings = (diagnostics, spec) => {
42
- const findings = diagnostics.map((diagnostic) => normalizeFinding(diagnostic, spec));
43
- const summary = findings.reduce((current, finding) => {
44
- current[finding.severity] += 1;
45
- current.total += 1;
46
- return current;
47
- }, {
48
- error: 0,
49
- warn: 0,
50
- info: 0,
51
- hint: 0,
52
- total: 0
53
- });
54
- return {
55
- ok: summary.error === 0,
56
- generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
57
- source: "manual",
58
- failOn: "error",
59
- durationMs: null,
60
- summary,
61
- findings
62
- };
63
- };
64
- const normalizeFinding = (diagnostic, spec) => {
65
- const path = [...diagnostic.path];
66
- const finding = {
67
- code: String(diagnostic.code),
68
- message: diagnostic.message,
69
- severity: toLintSeverity(diagnostic.severity),
70
- path,
71
- documentPointer: toDocumentPointer(path)
72
- };
73
- const recommendation = getFindingRecommendation(String(diagnostic.code), diagnostic.message);
74
- if (recommendation) finding.recommendation = recommendation;
75
- if (diagnostic.source !== void 0) finding.source = diagnostic.source;
76
- if (diagnostic.range) finding.range = {
77
- start: diagnostic.range.start,
78
- end: diagnostic.range.end
79
- };
80
- const operation = inferOperation(path, spec);
81
- if (operation) finding.operation = operation;
82
- return finding;
83
- };
84
- const toLintSeverity = (severity) => {
85
- switch (severity) {
86
- case 0: return "error";
87
- case 1: return "warn";
88
- case 2: return "info";
89
- default: return "hint";
90
- }
91
- };
92
- const toDocumentPointer = (path) => {
93
- if (path.length === 0) return "";
94
- return `/${path.map((segment) => String(segment).replace(/~/g, "~0").replace(/\//g, "~1")).join("/")}`;
95
- };
96
- const inferOperation = (path, spec) => {
97
- if (path[0] !== "paths") return;
98
- const routePath = typeof path[1] === "string" ? path[1] : void 0;
99
- const method = typeof path[2] === "string" && httpMethods.has(path[2]) ? path[2] : void 0;
100
- if (!routePath && !method) return;
101
- const operationRecord = routePath && method ? getNestedValue(spec, [
102
- "paths",
103
- routePath,
104
- method
105
- ]) : void 0;
106
- const operation = {};
107
- if (routePath !== void 0) operation.path = routePath;
108
- if (method !== void 0) operation.method = method;
109
- if (operationRecord && typeof operationRecord === "object" && "operationId" in operationRecord) operation.operationId = String(operationRecord.operationId);
110
- return operation;
111
- };
112
- const getNestedValue = (value, path) => {
113
- let current = value;
114
- for (const segment of path) {
115
- if (current === null || typeof current !== "object") return;
116
- current = current[segment];
117
- }
118
- return current;
119
- };
120
- //#endregion
121
- //#region src/core/lint-openapi.ts
122
- const { Spectral } = spectralCore;
123
- const lintOpenApi = async (spec, ruleset) => {
124
- const spectral = new Spectral();
125
- spectral.setRuleset(ruleset);
126
- return normalizeFindings(await spectral.run(spec, { ignoreUnknownFormat: false }), spec);
127
- };
128
- //#endregion
129
- //#region src/presets/recommended.ts
130
- const { schema: schema$3, truthy: truthy$3 } = spectralFunctions;
131
- const { oas: oas$3 } = spectralRulesets;
132
- const operationSelector$2 = "$.paths[*][get,put,post,delete,options,head,patch,trace]";
133
- /**
134
- * Baseline quality preset. Equivalent to the package default ruleset.
135
- *
136
- * - Extends spectral:oas/recommended
137
- * - elysia-operation-summary and elysia-operation-tags at warn
138
- * - oas3-api-servers and info-contact disabled (local-dev friendly)
139
- */
140
- const recommended = {
141
- extends: [[oas$3, "recommended"]],
142
- rules: {
143
- "oas3-api-servers": "off",
144
- "info-contact": "off",
145
- "elysia-operation-summary": {
146
- description: "Operations should define a summary for generated docs and clients.",
147
- severity: "warn",
148
- given: operationSelector$2,
149
- then: {
150
- field: "summary",
151
- function: truthy$3
152
- }
153
- },
154
- "elysia-operation-tags": {
155
- description: "Operations should declare at least one tag for grouping and downstream tooling.",
156
- severity: "warn",
157
- given: operationSelector$2,
158
- then: {
159
- field: "tags",
160
- function: schema$3,
161
- functionOptions: { schema: {
162
- type: "array",
163
- minItems: 1
164
- } }
165
- }
166
- }
167
- }
168
- };
169
- //#endregion
170
- //#region src/rulesets/default-ruleset.ts
171
- const defaultRuleset = recommended;
172
- //#endregion
173
- //#region src/core/load-ruleset.ts
174
- const { alphabetical, casing, defined, enumeration, falsy, length, or, pattern, schema: schema$2, truthy: truthy$2, undefined: undefinedFunction, unreferencedReusableObject, xor } = spectralFunctions;
175
- const { oas: oas$2 } = spectralRulesets;
176
- const functionMap = {
177
- alphabetical,
178
- casing,
179
- defined,
180
- enumeration,
181
- falsy,
182
- length,
183
- or,
184
- pattern,
185
- schema: schema$2,
186
- truthy: truthy$2,
187
- undefined: undefinedFunction,
188
- unreferencedReusableObject,
189
- xor
190
- };
191
- const extendsMap = { "spectral:oas": oas$2 };
192
- const autodiscoverRulesetFilenames = [
193
- "spectral.yaml",
194
- "spectral.yml",
195
- "spectral.ts",
196
- "spectral.mts",
197
- "spectral.cts",
198
- "spectral.js",
199
- "spectral.mjs",
200
- "spectral.cjs",
201
- "spectral.config.yaml",
202
- "spectral.config.yml",
203
- "spectral.config.ts",
204
- "spectral.config.mts",
205
- "spectral.config.cts",
206
- "spectral.config.js",
207
- "spectral.config.mjs",
208
- "spectral.config.cjs"
209
- ];
210
- var RulesetLoadError = class extends Error {
211
- constructor(message, options) {
212
- super(message);
213
- this.name = "RulesetLoadError";
214
- if (options?.cause !== void 0) this.cause = options.cause;
215
- }
216
- };
217
- const loadRuleset = async (input, baseDirOrOptions = process.cwd()) => {
218
- return (await loadResolvedRuleset(input, baseDirOrOptions)).ruleset;
219
- };
220
- const loadResolvedRuleset = async (input, baseDirOrOptions = process.cwd()) => {
221
- const options = normalizeLoadResolvedRulesetOptions(baseDirOrOptions);
222
- const context = {
223
- baseDir: options.baseDir,
224
- defaultRuleset: options.defaultRuleset,
225
- mergeAutodiscoveredWithDefault: options.mergeAutodiscoveredWithDefault
226
- };
227
- for (const resolver of options.resolvers) {
228
- const loaded = await resolver(input, context);
229
- if (loaded) {
230
- const normalized = { ruleset: normalizeRulesetDefinition(loaded.ruleset) };
231
- if (loaded.source) normalized.source = loaded.source;
232
- return normalized;
233
- }
234
- }
235
- if (input === void 0) return { ruleset: options.defaultRuleset };
236
- throw new RulesetLoadError("Ruleset input could not be resolved.");
237
- };
238
- const normalizeLoadResolvedRulesetOptions = (value) => {
239
- if (typeof value === "string") return {
240
- baseDir: value,
241
- resolvers: defaultRulesetResolvers,
242
- mergeAutodiscoveredWithDefault: true,
243
- defaultRuleset
244
- };
245
- return {
246
- baseDir: value.baseDir ?? process.cwd(),
247
- resolvers: value.resolvers ?? defaultRulesetResolvers,
248
- mergeAutodiscoveredWithDefault: value.mergeAutodiscoveredWithDefault ?? true,
249
- defaultRuleset: value.defaultRuleset ?? defaultRuleset
250
- };
251
- };
252
- const resolveAutodiscoveredRuleset = async (input, context) => {
253
- if (input !== void 0) return;
254
- const autodiscoveredPath = await findAutodiscoveredRulesetPath(context.baseDir);
255
- if (!autodiscoveredPath) return;
256
- const loaded = await loadResolvedPathRuleset(autodiscoveredPath, context);
257
- if (!context.mergeAutodiscoveredWithDefault) return {
258
- ...loaded,
259
- source: {
260
- path: autodiscoveredPath,
261
- autodiscovered: true,
262
- mergedWithDefault: false
263
- }
264
- };
265
- return {
266
- ruleset: mergeRulesets(context.defaultRuleset, loaded.ruleset),
267
- source: {
268
- path: autodiscoveredPath,
269
- autodiscovered: true,
270
- mergedWithDefault: true
271
- }
272
- };
273
- };
274
- const resolvePathRuleset = async (input, context) => {
275
- if (typeof input !== "string") return;
276
- return await loadResolvedPathRuleset(input, context);
277
- };
278
- const resolveInlineRuleset = async (input) => {
279
- if (input === void 0 || typeof input === "string") return;
280
- return { ruleset: normalizeRulesetDefinition(input) };
281
- };
282
- const defaultRulesetResolvers = [
283
- resolveAutodiscoveredRuleset,
284
- resolvePathRuleset,
285
- resolveInlineRuleset
286
- ];
287
- const loadResolvedPathRuleset = async (inputPath, context) => {
288
- const resolvedPath = path.resolve(context.baseDir, inputPath);
289
- if (isYamlRulesetPath(resolvedPath)) return {
290
- ruleset: await loadYamlRuleset(resolvedPath),
291
- source: {
292
- path: inputPath,
293
- autodiscovered: false,
294
- mergedWithDefault: false
295
- }
296
- };
297
- if (!isModuleRulesetPath(resolvedPath)) throw new RulesetLoadError(`Unsupported ruleset path: ${inputPath}. Supported local rulesets are .yaml, .yml, .js, .mjs, .cjs, .ts, .mts, and .cts.`);
298
- return {
299
- ruleset: await loadModuleRuleset(resolvedPath),
300
- source: {
301
- path: inputPath,
302
- autodiscovered: false,
303
- mergedWithDefault: false
304
- }
305
- };
306
- };
307
- const findAutodiscoveredRulesetPath = async (baseDir) => {
308
- for (const filename of autodiscoverRulesetFilenames) {
309
- const candidatePath = path.resolve(baseDir, filename);
310
- try {
311
- await access(candidatePath);
312
- return `./${filename}`;
313
- } catch (error) {
314
- if (error.code !== "ENOENT") throw error;
315
- }
316
- }
317
- };
318
- const loadYamlRuleset = async (resolvedPath) => {
319
- let fileContents;
320
- try {
321
- fileContents = await readFile(resolvedPath, "utf8");
322
- } catch (error) {
323
- throw new RulesetLoadError(`Unable to read ruleset at ${resolvedPath}.`, { cause: error });
324
- }
325
- let parsed;
326
- try {
327
- parsed = YAML.parse(fileContents);
328
- } catch (error) {
329
- throw new RulesetLoadError(`Unable to parse YAML ruleset at ${resolvedPath}.`, { cause: error });
330
- }
331
- return normalizeRulesetDefinition(parsed);
332
- };
333
- const loadModuleRuleset = async (resolvedPath) => {
334
- let imported;
335
- try {
336
- imported = await import(pathToFileURL(resolvedPath).href);
337
- } catch (error) {
338
- throw new RulesetLoadError(`Unable to import module ruleset at ${resolvedPath}.`, { cause: error });
339
- }
340
- const resolvedRuleset = resolveModuleRulesetValue(imported);
341
- if (resolvedRuleset === void 0) throw new RulesetLoadError(`Module ruleset at ${resolvedPath} must export a ruleset as the default export or a named "ruleset" export.`);
342
- return normalizeRulesetDefinition(resolvedRuleset, {
343
- ...functionMap,
344
- ...resolveModuleFunctions(imported)
345
- });
346
- };
347
- const resolveModuleRulesetValue = (imported) => {
348
- if (!isRecord$1(imported)) return;
349
- if ("default" in imported) return imported.default;
350
- if ("ruleset" in imported) return imported.ruleset;
351
- };
352
- const resolveModuleFunctions = (imported) => {
353
- if (!isRecord$1(imported) || !("functions" in imported)) return {};
354
- const { functions } = imported;
355
- if (!isRecord$1(functions)) throw new RulesetLoadError("Module ruleset \"functions\" export must be an object map of function names to Spectral functions.");
356
- const entries = Object.entries(functions).filter(([, value]) => typeof value === "function");
357
- return Object.fromEntries(entries);
358
- };
359
- const isYamlRulesetPath = (value) => value.endsWith(".yaml") || value.endsWith(".yml");
360
- const isModuleRulesetPath = (value) => value.endsWith(".js") || value.endsWith(".mjs") || value.endsWith(".cjs") || value.endsWith(".ts") || value.endsWith(".mts") || value.endsWith(".cts");
361
- const normalizeRulesetDefinition = (input, availableFunctions = functionMap) => {
362
- if (!isRecord$1(input)) throw new RulesetLoadError("Ruleset must be an object.");
363
- const normalized = { ...input };
364
- if ("extends" in normalized) normalized.extends = normalizeExtends(normalized.extends);
365
- if ("rules" in normalized) normalized.rules = normalizeRules(normalized.rules, availableFunctions);
366
- return normalized;
367
- };
368
- const mergeRuleEntry = (base, override) => {
369
- if (!isRecord$1(override)) return override;
370
- if ("given" in override || "then" in override) return override;
371
- if (isRecord$1(base) && ("given" in base || "then" in base)) return {
372
- ...base,
373
- ...override
374
- };
375
- const keys = Object.keys(override);
376
- if (keys.length === 1 && keys[0] === "severity") return override.severity;
377
- return override;
378
- };
379
- const mergeRulesets = (baseRuleset, overrideRuleset) => {
380
- const mergedBase = baseRuleset;
381
- const mergedOverride = overrideRuleset;
382
- const baseRules = isRecord$1(mergedBase.rules) ? mergedBase.rules : {};
383
- const overrideRules = isRecord$1(mergedOverride.rules) ? mergedOverride.rules : {};
384
- const mergedRules = { ...baseRules };
385
- for (const [name, overrideRule] of Object.entries(overrideRules)) mergedRules[name] = mergeRuleEntry(baseRules[name], overrideRule);
386
- const baseExtends = toExtendsArray(mergedBase.extends);
387
- const overrideExtends = toExtendsArray(mergedOverride.extends);
388
- const mergedExtends = [...baseExtends, ...overrideExtends];
389
- const merged = {
390
- ...mergedBase,
391
- ...mergedOverride
392
- };
393
- delete merged.extends;
394
- delete merged.rules;
395
- if (mergedExtends.length > 0) merged.extends = mergedExtends;
396
- if (Object.keys(mergedRules).length > 0) merged.rules = mergedRules;
397
- return merged;
398
- };
399
- const toExtendsArray = (value) => {
400
- if (value === void 0) return [];
401
- return Array.isArray(value) ? [...value] : [value];
402
- };
403
- const normalizeExtends = (value) => {
404
- if (typeof value === "string") return resolveExtendsEntry(value);
405
- if (!Array.isArray(value)) return value;
406
- return value.map((entry) => {
407
- if (typeof entry === "string") return resolveExtendsEntry(entry);
408
- if (Array.isArray(entry) && entry.length >= 1 && typeof entry[0] === "string") return [resolveExtendsEntry(entry[0]), entry[1]];
409
- return entry;
410
- });
411
- };
412
- const resolveExtendsEntry = (value) => {
413
- const resolved = extendsMap[value];
414
- if (!resolved) throw new RulesetLoadError(`Unsupported ruleset extend target: "${value}". Supported extend targets: spectral:oas.`);
415
- return resolved;
416
- };
417
- const normalizeRules = (value, availableFunctions) => {
418
- if (!isRecord$1(value)) return value;
419
- const entries = Object.entries(value).map(([ruleName, ruleValue]) => [ruleName, normalizeRule(ruleValue, availableFunctions)]);
420
- return Object.fromEntries(entries);
421
- };
422
- const normalizeRule = (value, availableFunctions) => {
423
- if (!isRecord$1(value)) return value;
424
- const normalized = { ...value };
425
- if ("then" in normalized) normalized.then = normalizeThen(normalized.then, availableFunctions);
426
- return normalized;
427
- };
428
- const normalizeThen = (value, availableFunctions) => {
429
- if (Array.isArray(value)) return value.map((entry) => normalizeThenEntry(entry, availableFunctions));
430
- return normalizeThenEntry(value, availableFunctions);
431
- };
432
- const normalizeThenEntry = (value, availableFunctions) => {
433
- if (!isRecord$1(value)) return value;
434
- const normalized = { ...value };
435
- if (typeof normalized.function === "string") {
436
- const resolved = availableFunctions[normalized.function];
437
- if (!resolved) throw new RulesetLoadError(`Unsupported Spectral function: ${String(normalized.function)}.`);
438
- normalized.function = resolved;
439
- }
440
- return normalized;
441
- };
442
- const isRecord$1 = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
443
- //#endregion
6
+ import YAML from "yaml";
7
+ import { pathToFileURL } from "node:url";
444
8
  //#region src/output/terminal-format.ts
445
9
  const severityStyles = {
446
10
  error: [
@@ -847,6 +411,11 @@ const toSarifArtifactUri = (value) => {
847
411
  };
848
412
  //#endregion
849
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
+ };
850
419
  const createOutputSinks = (options) => {
851
420
  const reporter = resolveReporter(options.logger);
852
421
  const sinks = [];
@@ -857,24 +426,17 @@ const createOutputSinks = (options) => {
857
426
  if (configuredSpecSnapshotPath) sinks.push({
858
427
  name: "spec snapshot",
859
428
  kind: "artifact",
429
+ phase: "pre-finalize",
860
430
  async write(_result, context) {
861
431
  const writtenSpecSnapshotPath = await writeSpecSnapshot(configuredSpecSnapshotPath === true ? await resolveDefaultSpecSnapshotPath() : configuredSpecSnapshotPath, context.spec, options.output?.pretty !== false);
862
432
  reporter.artifact(`OpenAPI lint wrote spec snapshot to ${writtenSpecSnapshotPath}.`);
863
433
  return { specSnapshotPath: writtenSpecSnapshotPath };
864
434
  }
865
435
  });
866
- if (configuredJsonReportPath) sinks.push({
867
- name: "JSON report",
868
- kind: "artifact",
869
- async write(result) {
870
- const writtenJsonReportPath = await writeJsonReport(configuredJsonReportPath, result, options.output?.pretty !== false);
871
- reporter.artifact(`OpenAPI lint wrote JSON report to ${writtenJsonReportPath}.`);
872
- return { jsonReportPath: writtenJsonReportPath };
873
- }
874
- });
875
436
  if (configuredJunitReportPath) sinks.push({
876
437
  name: "JUnit report",
877
438
  kind: "artifact",
439
+ phase: "pre-finalize",
878
440
  async write(result) {
879
441
  const writtenJunitReportPath = await writeJunitReport(configuredJunitReportPath, result);
880
442
  reporter.artifact(`OpenAPI lint wrote JUnit report to ${writtenJunitReportPath}.`);
@@ -885,6 +447,7 @@ const createOutputSinks = (options) => {
885
447
  if (configuredBrunoCollectionPath) sinks.push({
886
448
  name: "Bruno collection",
887
449
  kind: "artifact",
450
+ phase: "pre-finalize",
888
451
  async write(_result, context) {
889
452
  const writtenPath = await writeBrunoCollection(configuredBrunoCollectionPath, context.spec);
890
453
  reporter.artifact(`OpenAPI lint wrote Bruno collection to ${writtenPath}.`);
@@ -894,6 +457,7 @@ const createOutputSinks = (options) => {
894
457
  if (configuredSarifReportPath) sinks.push({
895
458
  name: "SARIF report",
896
459
  kind: "artifact",
460
+ phase: "pre-finalize",
897
461
  async write(result) {
898
462
  const writtenSarifReportPath = await writeSarifReport(configuredSarifReportPath, result, options.output?.pretty !== false);
899
463
  reporter.artifact(`OpenAPI lint wrote SARIF report to ${writtenSarifReportPath}.`);
@@ -903,11 +467,29 @@ const createOutputSinks = (options) => {
903
467
  for (const sink of options.output?.sinks ?? []) sinks.push({
904
468
  name: sink.name,
905
469
  kind: "custom",
470
+ phase: "post-finalize",
906
471
  write: async (result, context) => await Promise.resolve(sink.write(result, context))
907
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
+ });
908
489
  if (options.output?.console !== false) sinks.push({
909
490
  name: "console",
910
491
  kind: "report",
492
+ phase: "post-finalize",
911
493
  async write(result) {
912
494
  reportToConsole(result, reporter);
913
495
  }
@@ -915,133 +497,6 @@ const createOutputSinks = (options) => {
915
497
  return sinks;
916
498
  };
917
499
  //#endregion
918
- //#region src/presets/server.ts
919
- const { schema: schema$1, truthy: truthy$1 } = spectralFunctions;
920
- const { oas: oas$1 } = spectralRulesets;
921
- const operationSelector$1 = "$.paths[*][get,put,post,delete,options,head,patch,trace]";
922
- /**
923
- * Production API quality preset. Suitable as a CI gate for teams shipping
924
- * public or internal APIs where contract quality matters.
925
- *
926
- * Tightens recommended:
927
- * - elysia-operation-summary and elysia-operation-tags escalated to error
928
- * - operation-description, operation-operationId, operation-success-response at warn
929
- * - oas3-api-servers at warn (servers should be declared in production specs)
930
- */
931
- const server = {
932
- extends: [[oas$1, "recommended"]],
933
- rules: {
934
- "oas3-api-servers": "warn",
935
- "info-contact": "off",
936
- "elysia-operation-summary": {
937
- description: "Operations should define a summary for generated docs and clients.",
938
- severity: "error",
939
- given: operationSelector$1,
940
- then: {
941
- field: "summary",
942
- function: truthy$1
943
- }
944
- },
945
- "elysia-operation-tags": {
946
- description: "Operations should declare at least one tag for grouping and downstream tooling.",
947
- severity: "error",
948
- given: operationSelector$1,
949
- then: {
950
- field: "tags",
951
- function: schema$1,
952
- functionOptions: { schema: {
953
- type: "array",
954
- minItems: 1
955
- } }
956
- }
957
- },
958
- "operation-description": "warn",
959
- "operation-operationId": "warn",
960
- "operation-success-response": "warn"
961
- }
962
- };
963
- //#endregion
964
- //#region src/presets/strict.ts
965
- const { schema, truthy } = spectralFunctions;
966
- const { oas } = spectralRulesets;
967
- const operationSelector = "$.paths[*][get,put,post,delete,options,head,patch,trace]";
968
- const isRecord = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
969
- /**
970
- * Checks that all 4xx/5xx responses declare application/problem+json as
971
- * their content type, conforming to RFC 9457 Problem Details for HTTP APIs.
972
- */
973
- const checkProblemDetails = (operation) => {
974
- if (!isRecord(operation) || !isRecord(operation.responses)) return;
975
- const results = [];
976
- for (const [statusCode, response] of Object.entries(operation.responses)) {
977
- const code = Number(statusCode);
978
- if (!Number.isFinite(code) || code < 400) continue;
979
- if (!isRecord(response)) continue;
980
- const content = response.content;
981
- if (!isRecord(content) || !("application/problem+json" in content)) results.push({
982
- message: `${statusCode} error response should use "application/problem+json" content type (RFC 9457 Problem Details).`,
983
- path: ["responses", statusCode]
984
- });
985
- }
986
- return results.length > 0 ? results : void 0;
987
- };
988
- /**
989
- * Full API governance preset. Suitable for teams with formal API governance
990
- * requirements, public API programs, or downstream client generation pipelines.
991
- *
992
- * Tightens server:
993
- * - All elysia rules and operation metadata rules escalated to error
994
- * - info-contact at warn (API ownership should be declared)
995
- * - oas3-api-servers at error (server declaration is required)
996
- * - rfc9457-problem-details at warn (error responses should use Problem Details)
997
- */
998
- const strict = {
999
- extends: [[oas, "recommended"]],
1000
- rules: {
1001
- "oas3-api-servers": "error",
1002
- "info-contact": "warn",
1003
- "elysia-operation-summary": {
1004
- description: "Operations should define a summary for generated docs and clients.",
1005
- severity: "error",
1006
- given: operationSelector,
1007
- then: {
1008
- field: "summary",
1009
- function: truthy
1010
- }
1011
- },
1012
- "elysia-operation-tags": {
1013
- description: "Operations should declare at least one tag for grouping and downstream tooling.",
1014
- severity: "error",
1015
- given: operationSelector,
1016
- then: {
1017
- field: "tags",
1018
- function: schema,
1019
- functionOptions: { schema: {
1020
- type: "array",
1021
- minItems: 1
1022
- } }
1023
- }
1024
- },
1025
- "operation-description": "error",
1026
- "operation-operationId": "error",
1027
- "operation-success-response": "error",
1028
- "rfc9457-problem-details": {
1029
- description: "Error responses (4xx, 5xx) should use RFC 9457 Problem Details (application/problem+json).",
1030
- message: "{{error}}",
1031
- severity: "warn",
1032
- given: operationSelector,
1033
- then: { function: checkProblemDetails }
1034
- }
1035
- }
1036
- };
1037
- //#endregion
1038
- //#region src/presets/index.ts
1039
- const presets = {
1040
- recommended,
1041
- server,
1042
- strict
1043
- };
1044
- //#endregion
1045
500
  //#region src/providers/spec-provider.ts
1046
501
  var BaseSpecProvider = class {
1047
502
  constructor(app, options = {}) {
@@ -1078,11 +533,11 @@ var PublicSpecProvider = class extends BaseSpecProvider {
1078
533
  if (response.ok) return await parseSpecResponse(response, sourceLabel, this.specPath);
1079
534
  if (this.options?.baseUrl) return await this.fetchViaLoopback();
1080
535
  const body = await safeReadBody(response);
1081
- throw new PublicSpecProviderError([`Unable to load OpenAPI JSON from ${this.specPath} via ${sourceLabel}: received ${describeResponse(response)}${body ? ` with body ${body}.` : "."}`, `Fix: ensure @elysiajs/openapi is mounted and exposing "${this.specPath}", or update source.specPath to the correct OpenAPI JSON route.`].join(" "));
536
+ throw new PublicSpecProviderError([`Unable to load OpenAPI JSON from ${this.specPath} via ${sourceLabel}: received ${describeResponse(response)}${body ? ` with body ${body}.` : "."}`, `Fix: ensure an OpenAPI generator (for example @elysiajs/openapi) is installed and mounted so it exposes "${this.specPath}", or update source.specPath to the correct OpenAPI JSON route.`].join(" "));
1082
537
  } catch (error) {
1083
538
  if (this.options?.baseUrl) return await this.fetchViaLoopback(error);
1084
539
  if (error instanceof PublicSpecProviderError) throw error;
1085
- throw new PublicSpecProviderError([`Unable to resolve OpenAPI JSON from ${this.specPath} via ${sourceLabel}.`, "Fix: ensure the app can serve the configured OpenAPI JSON route, or set source.baseUrl if the document is only reachable over HTTP."].join(" "), { cause: error });
540
+ throw new PublicSpecProviderError([`Unable to resolve OpenAPI JSON from ${this.specPath} via ${sourceLabel}.`, "Fix: ensure the app mounts an OpenAPI generator (for example @elysiajs/openapi) or otherwise serves the configured OpenAPI JSON route, or set source.baseUrl if the document is only reachable over HTTP."].join(" "), { cause: error });
1086
541
  }
1087
542
  }
1088
543
  async fetchViaLoopback(cause) {
@@ -1192,7 +647,13 @@ const createOpenApiLintRuntime = (options = {}) => {
1192
647
  reporter.start("OpenAPI lint started.");
1193
648
  try {
1194
649
  const spec = await new PublicSpecProvider(app, options.source).getSpec();
1195
- const loadedRuleset = await loadResolvedRuleset(options.ruleset, { ...options.preset ? { defaultRuleset: presets[options.preset] } : {} });
650
+ const [{ lintOpenApi }, { loadResolvedRuleset }, presetsModule] = await Promise.all([
651
+ import("./lint-openapi-D76sC7S5.mjs").then((n) => n.n),
652
+ import("./load-ruleset-CiikrzWx.mjs").then((n) => n.i),
653
+ options.preset ? import("./presets-CCfU_diN.mjs").then((n) => n.n) : Promise.resolve(null)
654
+ ]);
655
+ const defaultRuleset = options.preset ? presetsModule?.presets[options.preset] : void 0;
656
+ const loadedRuleset = await loadResolvedRuleset(options.ruleset, { ...defaultRuleset ? { defaultRuleset } : {} });
1196
657
  if (loadedRuleset.source?.autodiscovered) {
1197
658
  const base = options.preset ? `"${options.preset}" preset` : "package default ruleset";
1198
659
  reporter.ruleset(`OpenAPI lint autodiscovered ruleset ${loadedRuleset.source.path} and merged it with the ${base}.`);
@@ -1202,10 +663,12 @@ const createOpenApiLintRuntime = (options = {}) => {
1202
663
  result.source = source;
1203
664
  result.failOn = options.failOn ?? "error";
1204
665
  result.ok = !shouldFail(result, result.failOn);
1205
- await writeOutputSinks(result, spec, options, artifactWriteFailureMode);
1206
- runtime.latest = result;
666
+ const { preFinalizeSinks, postFinalizeSinks } = partitionOutputSinks(createOutputSinks(options));
667
+ await writeOutputSinks(preFinalizeSinks, result, spec, reporter, artifactWriteFailureMode);
1207
668
  finalizeRuntimeRun(runtime, startedAt);
1208
669
  result.durationMs = runtime.durationMs;
670
+ await writeOutputSinks(postFinalizeSinks, result, spec, reporter, artifactWriteFailureMode);
671
+ runtime.latest = result;
1209
672
  reporter.complete("OpenAPI lint completed.");
1210
673
  enforceThreshold(result, options.failOn ?? "error");
1211
674
  runtime.status = "passed";
@@ -1251,9 +714,7 @@ const handleArtifactWriteFailure = (artifact, error, mode, reporter) => {
1251
714
  if (mode === "error") throw wrappedError;
1252
715
  reporter.warn(wrappedError.message);
1253
716
  };
1254
- const writeOutputSinks = async (result, spec, options, artifactWriteFailureMode) => {
1255
- const reporter = resolveReporter(options.logger);
1256
- const sinks = createOutputSinks(options);
717
+ const writeOutputSinks = async (sinks, result, spec, reporter, artifactWriteFailureMode) => {
1257
718
  for (const sink of sinks) try {
1258
719
  const artifacts = await sink.write(result, {
1259
720
  spec,
@@ -1268,6 +729,21 @@ const writeOutputSinks = async (result, spec, options, artifactWriteFailureMode)
1268
729
  throw error;
1269
730
  }
1270
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
+ };
1271
747
  const relativiseArtifacts = (artifacts) => {
1272
748
  const cwd = process.cwd();
1273
749
  const result = {};
@@ -1287,4 +763,4 @@ const resolveStartupMode = (options = {}) => {
1287
763
  return options.enabled === false ? "off" : "enforce";
1288
764
  };
1289
765
  //#endregion
1290
- 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 };
766
+ export { enforceThreshold as a, OpenApiLintThresholdError as i, createOpenApiLintRuntime as n, shouldFail as o, resolveStartupMode as r, resolveReporter as s, OpenApiLintArtifactWriteError as t };