@tetsujs/openapi 0.4.0 → 0.4.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/dist/src/index.js CHANGED
@@ -17,6 +17,7 @@ function docsOf(hook) {
17
17
  }
18
18
  function contributionsOf(chains, warn) {
19
19
  const security = new Map;
20
+ const conditions = new Map;
20
21
  const responses = new Map;
21
22
  for (const slot of Object.values(chains)) {
22
23
  for (const hook of slot) {
@@ -25,11 +26,20 @@ function contributionsOf(chains, warn) {
25
26
  continue;
26
27
  }
27
28
  if (docs.security) {
28
- const claimed = security.get(docs.security.name);
29
- if (!claimed) {
30
- security.set(docs.security.name, docs.security);
31
- } else if (JSON.stringify(claimed.scheme) !== JSON.stringify(docs.security.scheme)) {
32
- warn?.(`two hooks claim the security scheme "${docs.security.name}" with different definitions; one of them is not described`);
29
+ const alternatives = alternativesOf(docs.security);
30
+ conditions.set(conditionKey(alternatives), alternatives);
31
+ for (const requirement of alternatives) {
32
+ const claimed = security.get(requirement.name);
33
+ if (!claimed) {
34
+ security.set(requirement.name, requirement);
35
+ } else if (JSON.stringify(claimed.scheme) === JSON.stringify(requirement.scheme)) {
36
+ security.set(requirement.name, {
37
+ ...claimed,
38
+ scopes: joinScopes(claimed.scopes, requirement.scopes)
39
+ });
40
+ } else {
41
+ warn?.(`two hooks claim the security scheme "${requirement.name}" with different definitions; one of them is not described`);
42
+ }
33
43
  }
34
44
  }
35
45
  for (const response of docs.responses ?? []) {
@@ -42,9 +52,28 @@ function contributionsOf(chains, warn) {
42
52
  }
43
53
  return {
44
54
  security: [...security.values()],
55
+ conditions: [...conditions.values()],
45
56
  responses: [...responses.values()]
46
57
  };
47
58
  }
59
+ function alternativesOf(security) {
60
+ return "anyOf" in security ? security.anyOf : [security];
61
+ }
62
+ function conditionKey(alternatives) {
63
+ return JSON.stringify(alternatives.map((requirement) => [
64
+ requirement.name,
65
+ [...requirement.scopes ?? []].sort()
66
+ ]));
67
+ }
68
+ function joinScopes(first, second) {
69
+ const joined = [...first ?? []];
70
+ for (const scope of second ?? []) {
71
+ if (!joined.includes(scope)) {
72
+ joined.push(scope);
73
+ }
74
+ }
75
+ return joined;
76
+ }
48
77
 
49
78
  // packages/openapi/src/components.ts
50
79
  function schemaComponents() {
@@ -73,6 +102,104 @@ function failureName(status, error) {
73
102
  return name || `Failure${status}`;
74
103
  }
75
104
 
105
+ // packages/openapi/src/envelopes.ts
106
+ function envelopes(components) {
107
+ const declared = new Map;
108
+ const defined = new Map;
109
+ const refs = new Map;
110
+ const codes = new Map;
111
+ const names = new Map;
112
+ const reported = new Set;
113
+ return {
114
+ declare(status, code, schema) {
115
+ const key = keyOf(status, code);
116
+ if (!declared.has(key)) {
117
+ declared.set(key, schema);
118
+ }
119
+ },
120
+ ref(status, code, schema, warn) {
121
+ const key = keyOf(status, code);
122
+ const definition = declared.get(key) ?? defined.get(key) ?? schema;
123
+ defined.set(key, definition);
124
+ const difference = differenceOf(definition, schema);
125
+ if (difference && !reported.has(`${key} ${difference}`)) {
126
+ reported.add(`${key} ${difference}`);
127
+ warn(`the ${status} "${code}" is described by one definition in the document, and a definition that differs from it in ${difference} is not described`);
128
+ }
129
+ const existing = refs.get(key);
130
+ if (existing) {
131
+ return existing;
132
+ }
133
+ const created = components.ref(nameOf(status, code), definition);
134
+ refs.set(key, created);
135
+ codes.set(created.$ref, code);
136
+ return created;
137
+ },
138
+ codeOf(ref) {
139
+ return codes.get(ref);
140
+ }
141
+ };
142
+ function nameOf(status, code) {
143
+ const name = failureName(status, code);
144
+ const holder = names.get(name);
145
+ if (holder === undefined || holder === keyOf(status, code)) {
146
+ names.set(name, keyOf(status, code));
147
+ return name;
148
+ }
149
+ return `${name}${status}`;
150
+ }
151
+ }
152
+ function envelopeCode(schema) {
153
+ const required = Array.isArray(schema.required) ? schema.required : [];
154
+ if (!required.includes("error")) {
155
+ return;
156
+ }
157
+ const error = schema.properties?.error;
158
+ if (typeof error?.const === "string") {
159
+ return error.const;
160
+ }
161
+ if (Array.isArray(error?.enum) && error.enum.length === 1 && typeof error.enum[0] === "string") {
162
+ return error.enum[0];
163
+ }
164
+ return;
165
+ }
166
+ function branchesOf(schema) {
167
+ const keys = Object.keys(schema);
168
+ if (keys.length !== 1) {
169
+ return [schema];
170
+ }
171
+ if (Array.isArray(schema.anyOf)) {
172
+ return schema.anyOf.flatMap(branchesOf);
173
+ }
174
+ if (Array.isArray(schema.oneOf)) {
175
+ const branches = schema.oneOf.flatMap(branchesOf);
176
+ const codes = branches.map(envelopeCode);
177
+ if (codes.every((code) => code !== undefined) && new Set(codes).size === codes.length) {
178
+ return branches;
179
+ }
180
+ }
181
+ return [schema];
182
+ }
183
+ function keyOf(status, code) {
184
+ return `${status} ${code}`;
185
+ }
186
+ function differenceOf(kept, other) {
187
+ const fields = (schema) => Object.keys(schema.properties ?? {});
188
+ const keptFields = fields(kept);
189
+ const otherFields = fields(other);
190
+ const missing = otherFields.filter((name) => !keptFields.includes(name));
191
+ const extra = keptFields.filter((name) => !otherFields.includes(name));
192
+ const changed = [...missing, ...extra];
193
+ if (changed.length > 0) {
194
+ return `fields: ${changed.join(", ")}`;
195
+ }
196
+ const required = (schema) => JSON.stringify([...Array.isArray(schema.required) ? schema.required : []].sort());
197
+ return required(kept) === required(other) ? undefined : "which fields are required";
198
+ }
199
+
200
+ // packages/openapi/src/operation.ts
201
+ import { errorBody } from "@tetsujs/core";
202
+
76
203
  // packages/openapi/src/emit.ts
77
204
  import { toJsonSchema } from "@tetsujs/core";
78
205
  var target = { target: "draft-2020-12" };
@@ -163,7 +290,7 @@ function objectProperties(schema, where, warn) {
163
290
  }
164
291
 
165
292
  // packages/openapi/src/operation.ts
166
- function operationOf(entry, options, components, ids, warn) {
293
+ function operationOf(entry, options, components, envelopes, ids, warn) {
167
294
  const schema = entry.def.schema;
168
295
  const parameters = [
169
296
  ...pathParameters(entry.path, schema?.params, warn),
@@ -181,14 +308,64 @@ function operationOf(entry, options, components, ids, warn) {
181
308
  ...entry.def.docs?.deprecated === undefined ? {} : { deprecated: entry.def.docs.deprecated },
182
309
  ...parameters.length > 0 ? { parameters } : {},
183
310
  ...body ? { requestBody: body } : {},
184
- ...contributed.security.length > 0 ? {
185
- security: contributed.security.map((requirement) => ({
186
- [requirement.name]: requirement.scopes ?? []
187
- }))
188
- } : {},
189
- responses: responses(entry, options, contributed, components, warn)
311
+ ...contributed.security.length > 0 ? { security: securityOf(contributed.conditions) } : {},
312
+ responses: responses(entry, options, contributed, components, envelopes, warn)
190
313
  };
191
314
  }
315
+ function declareEnvelopes(entry, envelopes) {
316
+ for (const [status, schema] of declaredResponses(entry)) {
317
+ const described = schema === null ? undefined : emitted(schema, "", () => {});
318
+ for (const branch of described ? branchesOf(described) : []) {
319
+ const code = envelopeCode(branch);
320
+ if (code !== undefined) {
321
+ envelopes.declare(Number(status), code, branch);
322
+ }
323
+ }
324
+ }
325
+ }
326
+ function declaredResponses(entry) {
327
+ const response = entry.def.schema?.response;
328
+ if (!response) {
329
+ return [];
330
+ }
331
+ if ("~standard" in response) {
332
+ return [["200", response]];
333
+ }
334
+ return Object.entries(response);
335
+ }
336
+ function branchesFor(schema, status, envelopes, warn) {
337
+ return branchesOf(schema).map((branch) => {
338
+ const code = envelopeCode(branch);
339
+ return code === undefined ? branch : envelopes.ref(status, code, branch, warn);
340
+ });
341
+ }
342
+ function securityOf(conditions) {
343
+ let combinations = [{}];
344
+ for (const alternatives of conditions) {
345
+ combinations = combinations.flatMap((combination) => alternatives.map((requirement) => ({
346
+ ...combination,
347
+ [requirement.name]: joined(combination[requirement.name], requirement.scopes)
348
+ })));
349
+ }
350
+ const seen = new Set;
351
+ return combinations.filter((combination) => {
352
+ const key = JSON.stringify(Object.entries(combination).map(([name, scopes]) => [name, [...scopes].sort()]).sort());
353
+ if (seen.has(key)) {
354
+ return false;
355
+ }
356
+ seen.add(key);
357
+ return true;
358
+ });
359
+ }
360
+ function joined(present, added) {
361
+ const scopes = [...present ?? []];
362
+ for (const scope of added ?? []) {
363
+ if (!scopes.includes(scope)) {
364
+ scopes.push(scope);
365
+ }
366
+ }
367
+ return scopes;
368
+ }
192
369
  function operationIds() {
193
370
  const taken = new Map;
194
371
  return {
@@ -265,94 +442,147 @@ function carriesBinary(root) {
265
442
  }
266
443
  return false;
267
444
  }
268
- function responses(entry, options, contributed, components, warn) {
269
- const declared = {};
270
- const response = entry.def.schema?.response;
271
- if (response && "~standard" in response) {
272
- declared["200"] = jsonResponse(response, "Successful response", warn);
273
- } else if (response) {
274
- for (const [status, schema] of Object.entries(response)) {
275
- declared[status] = schema === null ? { description: describeStatus(status) } : jsonResponse(schema, describeStatus(status), warn);
276
- }
445
+ function responses(entry, options, contributed, components, envelopes, warn) {
446
+ const answers = new Map;
447
+ const add = (status, answer) => {
448
+ answers.set(status, [...answers.get(status) ?? [], answer]);
449
+ };
450
+ for (const [status, schema] of declaredResponses(entry)) {
451
+ const described = schema === null ? undefined : emitted(schema, "a response", warn);
452
+ add(status, {
453
+ description: describeStatus(status),
454
+ placeholder: true,
455
+ schemas: described ? branchesFor(described, Number(status), envelopes, warn) : []
456
+ });
457
+ }
458
+ if (![...answers.keys()].some((status) => status.startsWith("2"))) {
459
+ add("200", {
460
+ description: "Successful response",
461
+ placeholder: true,
462
+ schemas: []
463
+ });
277
464
  }
278
- if (!declared["200"] && !hasSuccess(declared)) {
279
- declared["200"] = { description: "Successful response" };
465
+ for (const [status, answer] of failures(entry, options, contributed, components, envelopes, warn)) {
466
+ add(String(status), answer);
280
467
  }
281
- return {
282
- ...declared,
283
- ...frameworkFailures(entry, options, contributed, declared, components, warn)
284
- };
468
+ const described = {};
469
+ for (const [status, list] of answers) {
470
+ described[status] = merge(list, envelopes);
471
+ }
472
+ return described;
285
473
  }
286
- function frameworkFailures(entry, options, contributed, declared, components, warn) {
474
+ function failures(entry, options, contributed, components, envelopes, warn) {
287
475
  const schema = entry.def.schema;
288
- const failures = new Map;
289
- const add = (status, failure) => {
290
- const key = String(status);
291
- failures.set(key, [...failures.get(key) ?? [], failure]);
476
+ const found = [];
477
+ const envelopeRef = (status, error, message, fields) => {
478
+ const described = envelope(status, error, message, fields);
479
+ const ref = error === undefined ? components.ref(failureName(status), described) : envelopes.ref(status, error, described, warn);
480
+ return ref;
292
481
  };
293
- const envelopeRef = (status, error, message) => components.ref(failureName(status, error), envelope(status, error, message));
294
482
  if (schema?.params || schema?.query || schema?.headers || schema?.body) {
295
- add(options.validationStatus, {
296
- description: "Request failed schema validation",
297
- schema: components.ref(failureName(options.validationStatus, "VALIDATION_FAILED"), validationFailed(options.validationStatus))
298
- });
483
+ found.push([
484
+ options.validationStatus,
485
+ {
486
+ description: "Request failed schema validation",
487
+ schemas: [
488
+ envelopes.ref(options.validationStatus, "VALIDATION_FAILED", validationFailed(options.validationStatus), warn)
489
+ ]
490
+ }
491
+ ]);
299
492
  }
300
493
  for (const requirement of contributed.security) {
301
494
  const status = requirement.status ?? 401;
302
- add(status, {
303
- description: requirement.description ?? `Request did not satisfy ${requirement.name}`,
304
- schema: envelopeRef(status, requirement.error, requirement.message)
305
- });
495
+ found.push([
496
+ status,
497
+ {
498
+ description: requirement.description ?? `Request did not satisfy ${requirement.name}`,
499
+ schemas: [envelopeRef(status, requirement.error, requirement.message)]
500
+ }
501
+ ]);
306
502
  }
307
503
  for (const response of contributed.responses) {
308
504
  const described = response.schema ? emitted(response.schema, "a hook's response", warn) : undefined;
309
- add(response.status, {
310
- description: response.description,
311
- schema: described ?? envelopeRef(response.status, response.error, response.message)
312
- });
505
+ found.push([
506
+ response.status,
507
+ {
508
+ description: response.description,
509
+ schemas: described ? branchesFor(described, response.status, envelopes, warn) : [
510
+ envelopeRef(response.status, response.error, response.message, response.fields)
511
+ ],
512
+ ...response.headers ? { headers: response.headers } : {}
513
+ }
514
+ ]);
313
515
  }
314
516
  if (schema?.body || entry.def.bodyType) {
315
517
  const unparsable = parseFailure(entry.def.bodyType);
316
518
  if (unparsable) {
317
- add(400, {
318
- description: "Body could not be parsed in the declared shape",
319
- schema: envelopeRef(400, unparsable.error, unparsable.message)
320
- });
519
+ found.push([
520
+ 400,
521
+ {
522
+ description: "Body could not be parsed in the declared shape",
523
+ schemas: [envelopeRef(400, unparsable.error, unparsable.message)]
524
+ }
525
+ ]);
526
+ }
527
+ found.push([
528
+ 413,
529
+ {
530
+ description: "Body exceeded the configured size limit",
531
+ schemas: [
532
+ envelopeRef(413, "BODY_TOO_LARGE", "Body exceeds the configured limit")
533
+ ]
534
+ }
535
+ ]);
536
+ }
537
+ found.push([
538
+ 500,
539
+ {
540
+ description: "The request failed and nothing mapped the failure",
541
+ schemas: [
542
+ envelopeRef(500, "INTERNAL_SERVER_ERROR", "Internal Server Error")
543
+ ]
544
+ }
545
+ ]);
546
+ return found;
547
+ }
548
+ function merge(list, envelopes) {
549
+ const schemas = distinct(list.flatMap((answer) => answer.schemas));
550
+ const worded = list.filter((answer) => !answer.placeholder);
551
+ const headers = {};
552
+ for (const answer of list) {
553
+ for (const [name, header] of Object.entries(answer.headers ?? {})) {
554
+ headers[name] ??= header;
321
555
  }
322
- add(413, {
323
- description: "Body exceeded the configured size limit",
324
- schema: envelopeRef(413, "BODY_TOO_LARGE", "Body exceeds the configured limit")
325
- });
326
- }
327
- add(500, {
328
- description: "The request failed and nothing mapped the failure",
329
- schema: envelopeRef(500, "INTERNAL_SERVER_ERROR", "Internal Server Error")
330
- });
331
- const responses = {};
332
- for (const [status, list] of failures) {
333
- responses[status] = merge(declared[status], list);
334
556
  }
335
- return responses;
557
+ return {
558
+ description: (worded.length > 0 ? worded : list).map((answer) => answer.description).join("; "),
559
+ ...Object.keys(headers).length > 0 ? { headers } : {},
560
+ ...schemas.length > 0 ? {
561
+ content: {
562
+ "application/json": { schema: unionOf(schemas, envelopes) }
563
+ }
564
+ } : {}
565
+ };
336
566
  }
337
- function merge(declared, list) {
338
- const descriptions = [
339
- ...declared ? [declared.description] : [],
340
- ...list.map((failure) => failure.description)
341
- ];
342
- const schemas = distinct([
343
- ...declaredSchemas(declared),
344
- ...list.map((failure) => failure.schema)
345
- ]);
567
+ function unionOf(schemas, envelopes) {
346
568
  const [only] = schemas;
569
+ if (schemas.length === 1 && only) {
570
+ return only;
571
+ }
572
+ const mapping = {};
573
+ for (const schema of schemas) {
574
+ const ref = schema.$ref;
575
+ const code = typeof ref === "string" ? envelopes.codeOf(ref) : undefined;
576
+ if (typeof ref !== "string" || code === undefined) {
577
+ return { anyOf: schemas };
578
+ }
579
+ mapping[code] = ref;
580
+ }
347
581
  return {
348
- description: descriptions.join("; "),
349
- ...schemas.length === 1 && only ? { content: { "application/json": { schema: only } } } : { content: { "application/json": { schema: { anyOf: schemas } } } }
582
+ anyOf: schemas,
583
+ discriminator: { propertyName: "error", mapping }
350
584
  };
351
585
  }
352
- function declaredSchemas(declared) {
353
- const schema = declared?.content?.["application/json"]?.schema;
354
- return schema ? [schema] : [];
355
- }
356
586
  function distinct(schemas) {
357
587
  const seen = new Set;
358
588
  return schemas.filter((schema) => {
@@ -364,34 +594,23 @@ function distinct(schemas) {
364
594
  return true;
365
595
  });
366
596
  }
367
- function jsonResponse(schema, description, warn) {
368
- const described = emitted(schema, "a response", warn);
369
- if (!described) {
370
- return { description };
371
- }
372
- return {
373
- description,
374
- content: { "application/json": { schema: described } }
375
- };
376
- }
377
- function hasSuccess(declared) {
378
- return Object.keys(declared).some((status) => status.startsWith("2"));
379
- }
380
597
  function describeStatus(status) {
381
598
  if (status === "204") {
382
599
  return "No content";
383
600
  }
384
- return status.startsWith("2") ? "Successful response" : `Response ${status}`;
601
+ return status.startsWith("2") ? "Successful response" : errorBody(Number(status)).message;
385
602
  }
386
- function envelope(status, error, message) {
603
+ function envelope(status, error, message, fields = {}) {
604
+ const own = {
605
+ status: { type: "integer", const: status },
606
+ message: message ? { type: "string", examples: [message] } : { type: "string" },
607
+ error: error ? { type: "string", const: error } : { type: "string" }
608
+ };
609
+ const added = Object.keys(fields).filter((name) => !Object.hasOwn(own, name));
387
610
  return {
388
611
  type: "object",
389
- required: ["status", "message", "error"],
390
- properties: {
391
- status: { type: "integer", const: status },
392
- message: message ? { type: "string", examples: [message] } : { type: "string" },
393
- error: error ? { type: "string", const: error } : { type: "string" }
394
- }
612
+ required: ["status", "message", "error", ...added],
613
+ properties: { ...fields, ...own }
395
614
  };
396
615
  }
397
616
  function validationFailed(status) {
@@ -433,14 +652,24 @@ import { onMount, route } from "@tetsujs/core";
433
652
  // packages/openapi/src/page.ts
434
653
  var defaults = {
435
654
  scalar: {
436
- script: "https://cdn.jsdelivr.net/npm/@scalar/api-reference"
655
+ script: "https://cdn.jsdelivr.net/npm/@scalar/api-reference@1.72.1/dist/browser/standalone.js",
656
+ integrity: {
657
+ script: "sha384-U11tb2XnKvmwt8RlTvnwUnYgrN+ur4Xyh9htLhjajWNR/Oyl5AX5DEz00qRmlrmK"
658
+ }
437
659
  },
438
660
  "swagger-ui": {
439
- script: "https://cdn.jsdelivr.net/npm/swagger-ui-dist@5/swagger-ui-bundle.js",
440
- style: "https://cdn.jsdelivr.net/npm/swagger-ui-dist@5/swagger-ui.css"
661
+ script: "https://cdn.jsdelivr.net/npm/swagger-ui-dist@5.33.0/swagger-ui-bundle.js",
662
+ style: "https://cdn.jsdelivr.net/npm/swagger-ui-dist@5.33.0/swagger-ui.css",
663
+ integrity: {
664
+ script: "sha384-YDALVcy8kj8yltLBVi1vBiBAUqdxvus673gM8XKwiy6aDUJFXivF/KCufekjYbVf",
665
+ style: "sha384-Ov4/wv3j2bmct8cDc5X4ngJZohVPzEmc6uDPH8WeljUxO5vtoykvMEfbu9Vh6RaW"
666
+ }
441
667
  },
442
668
  redoc: {
443
- script: "https://cdn.jsdelivr.net/npm/redoc@2/bundles/redoc.standalone.js"
669
+ script: "https://cdn.jsdelivr.net/npm/redoc@2.5.4/bundles/redoc.standalone.js",
670
+ integrity: {
671
+ script: "sha384-w447zOpYfw/1Tv/5AK9NfHTlQIqE3RVR6KY62jCyy9zNDgO64cMwGGP1Fj0zJVf5"
672
+ }
444
673
  }
445
674
  };
446
675
  function docsPage(options) {
@@ -454,11 +683,11 @@ function docsPage(options) {
454
683
  '<meta charset="utf-8" />',
455
684
  '<meta name="viewport" content="width=device-width, initial-scale=1" />',
456
685
  `<title>${title}</title>`,
457
- assets.style ? `<link rel="stylesheet" href="${escapeHtml(assets.style)}" />` : "",
686
+ assets.style ? `<link rel="stylesheet" href="${escapeHtml(assets.style)}"${verified(assets.integrity?.style)} />` : "",
458
687
  "</head>",
459
688
  "<body>",
460
689
  body(options.ui, url),
461
- `<script src="${escapeHtml(assets.script)}"></script>`,
690
+ `<script src="${escapeHtml(assets.script)}"${verified(assets.integrity?.script)}></script>`,
462
691
  bootstrap(options.ui, url),
463
692
  "</body>",
464
693
  "</html>"
@@ -485,6 +714,9 @@ function bootstrap(ui, url) {
485
714
  ].join(`
486
715
  `);
487
716
  }
717
+ function verified(integrity) {
718
+ return integrity ? ` integrity="${escapeHtml(integrity)}" crossorigin="anonymous"` : "";
719
+ }
488
720
  function escapeHtml(value) {
489
721
  return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
490
722
  }
@@ -495,7 +727,8 @@ class DocsController {
495
727
  json;
496
728
  ui;
497
729
  document;
498
- constructor(options, documentPath, uiPath, page) {
730
+ page;
731
+ constructor(options, documentPath, uiPath) {
499
732
  this.options = options;
500
733
  const hidden = !options.documentSelf;
501
734
  const json = route({
@@ -508,12 +741,12 @@ class DocsController {
508
741
  method: "GET",
509
742
  path: "/docs",
510
743
  docs: { summary: "Rendered documentation", tags: ["docs"], hidden },
511
- handler: () => new Response(page, {
744
+ handler: () => new Response(this.page, {
512
745
  headers: { "content-type": "text/html; charset=utf-8" }
513
746
  })
514
747
  });
515
748
  this.json = { ...json, path: documentPath };
516
- this.ui = { ...ui, path: uiPath };
749
+ this.ui = options.ui === false ? undefined : { ...ui, path: uiPath };
517
750
  }
518
751
  [onMount](app) {
519
752
  const generated = openapi(app, {
@@ -521,6 +754,17 @@ class DocsController {
521
754
  ...this.options.servers ? { servers: this.options.servers } : {}
522
755
  });
523
756
  this.document = generated.document;
757
+ const ui = this.options.ui ?? "scalar";
758
+ const json = this.json;
759
+ const served = app.entries.find((entry) => entry.def === json);
760
+ if (ui !== false && served) {
761
+ this.page = docsPage({
762
+ ui,
763
+ documentUrl: served.path,
764
+ title: this.options.title ?? this.options.info.title,
765
+ ...this.options.assets ? { assets: this.options.assets } : {}
766
+ });
767
+ }
524
768
  const report = this.options.onWarning ?? warn;
525
769
  for (const warning of generated.warnings) {
526
770
  report(warning);
@@ -528,15 +772,7 @@ class DocsController {
528
772
  }
529
773
  }
530
774
  function docs(options) {
531
- const documentPath = options.path ?? "/openapi.json";
532
- const uiPath = options.uiPath ?? "/docs";
533
- const page = docsPage({
534
- ui: options.ui ?? "scalar",
535
- documentUrl: documentPath,
536
- title: options.title ?? options.info.title,
537
- ...options.assets ? { assets: options.assets } : {}
538
- });
539
- return new DocsController(options, documentPath, uiPath, page);
775
+ return new DocsController(options, options.path ?? "/openapi.json", options.uiPath ?? "/docs");
540
776
  }
541
777
  function warn(warning) {
542
778
  console.warn(`[openapi] ${warning.route}: ${warning.message}`);
@@ -548,15 +784,17 @@ function openapi(app, options) {
548
784
  const warnings = [];
549
785
  const schemes = {};
550
786
  const components = schemaComponents();
787
+ const envelopes2 = envelopes(components);
551
788
  const ids = operationIds();
789
+ const described = app.entries.filter((entry) => !entry.ws && !entry.def.docs?.hidden);
790
+ for (const entry of described) {
791
+ declareEnvelopes(entry, envelopes2);
792
+ }
552
793
  const occupied = new Map;
553
- for (const entry of app.entries) {
554
- if (entry.ws || entry.def.docs?.hidden) {
555
- continue;
556
- }
794
+ for (const entry of described) {
557
795
  const template = toTemplate(entry.path);
558
796
  const route = `${entry.method} ${entry.path}`;
559
- const operation = operationOf(entry, app.options, components, ids, (message) => {
797
+ const operation = operationOf(entry, app.options, components, envelopes2, ids, (message) => {
560
798
  warnings.push({ route, message });
561
799
  });
562
800
  for (const requirement of contributionsOf(entry.hooks).security) {
@@ -613,5 +851,5 @@ export {
613
851
  secured
614
852
  };
615
853
 
616
- //# debugId=7F7340DB513FEEB464756E2164756E21
854
+ //# debugId=8D8E4C09AE6F825C64756E2164756E21
617
855
  //# sourceMappingURL=index.js.map