@tetsujs/openapi 0.5.2 → 0.6.0

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/README.md CHANGED
@@ -91,12 +91,12 @@ wrote, by a rule:
91
91
  | parameters | `schema.params`, `query`, `headers`, `cookies` | required as the schema says |
92
92
  | the request body | `schema.body` and `bodyType` | media types from `bodyType`; required unless `text` or `stream` |
93
93
  | `security` | `secured()` hooks, of every level the route runs under | every hook required together; `anyOf` inside one hook is the alternatives |
94
- | the statuses | the route's `schema.response`, the hooks' `documented()` and `secured()`, the framework | one schema is `200`, a map its statuses, `null` a status without a body; with no `2xx` declared, `200`; the framework's `422`, `400`, `413`, `500` where they can happen — see [Responses the framework adds](#responses-the-framework-adds) |
94
+ | the statuses | the route's `schema.response`, the hooks' `documented()` and `secured()`, the framework | one schema is `200`, a map its statuses, `null` or an entry without `body` a status without a body; with no `schema.response` at all, `200`; the framework's `422`, `400`, `413`, `500` where they can happen — see [Responses the framework adds](#responses-the-framework-adds) |
95
95
  | a status's body | everything answering with that status | one flat `anyOf`, the route's own first, the same body once |
96
96
  | an error envelope | any of them | one definition per status and code in `components`, named after the code; the route's own kept |
97
97
  | `discriminator` | the envelopes of a status | on `error`, or the format's field, when every alternative is an envelope |
98
98
  | a status's description | the schemas' `description`, the hooks', the framework's | one is the description, several a list led by code, none the reason phrase |
99
- | a status's headers | the hooks' `documented()` headers | possible, not required |
99
+ | a status's headers | the route's entry — `{ body, headers, cookies }` — and the hooks' `documented()` headers | the route's required as its schema says, a property's `description` the header's; its cookies one `set-cookie` header listing them; a hook's possible, not required; the route's wins a name both give |
100
100
  | the document's `tags` | the `tags` option | in its order, then the tags routes use and it leaves out |
101
101
 
102
102
  `docs: { hidden: true }` leaves a route out of the document; it is served
@@ -124,9 +124,24 @@ the routes use and `tags` leaves out is listed after them, in the order
124
124
  the routes first use it, and reported — it is usually one tag spelled two
125
125
  ways, `sign_in` next to `sign-in`. So is a tag described and used by no
126
126
  operation. Without `tags` the document lists none, and nothing is
127
- reported. An application of several surfaces, each with its own `docs()`,
128
- gives each one only the tags of its own routes: given all of them, every
129
- `docs()` reports the tags of the others as unused.
127
+ reported.
128
+
129
+ A `docs()` documents the application it is mounted in, all of it. Several
130
+ surfaces with a document each — a public API and an admin one — are
131
+ several applications, each with its own `docs()` and only the tags of its
132
+ own routes:
133
+
134
+ ```ts
135
+ const api = Bun.serve({
136
+ ...createApp({ routes: [orders, docs({ info, tags: { orders: "Orders" } })] }),
137
+ port: 3000,
138
+ });
139
+
140
+ const admin = Bun.serve({
141
+ ...createApp({ routes: [users, docs({ info: adminInfo, tags: { users: "Users" } })] }),
142
+ port: 3001,
143
+ });
144
+ ```
130
145
 
131
146
  ## Operation ids
132
147
 
@@ -210,18 +225,37 @@ A status nothing describes keeps its reason phrase — `Not Found`,
210
225
 
211
226
  Every failure reaches the application's `onError` hooks — a thrown
212
227
  `HttpError`, a validation or body failure, a `404`, a `405`, a rate limit's
213
- refusal — so one hook answers all of them in your format:
228
+ refusal, and an error nothing expected — so one hook answers all of them in
229
+ your format:
214
230
 
215
231
  ```ts
216
- const inOurFormat = hook.onError(({ error }) => {
217
- if (!(error instanceof HttpError)) return undefined;
232
+ import type { ErrorBody } from "@tetsujs/core";
233
+ import { hook, HttpError, reportFailure } from "@tetsujs/core";
218
234
 
219
- const { status, error: code, ...rest } = error.body as ErrorBody;
235
+ const inOurFormat = hook.onError((ctx) => {
236
+ const { error } = ctx;
220
237
 
221
- return Response.json({ code, ...rest }, { status });
238
+ if (error instanceof HttpError) {
239
+ const { status, error: code, ...rest } = error.body as ErrorBody;
240
+
241
+ return Response.json({ code, ...rest }, { status });
242
+ }
243
+
244
+ reportFailure(ctx, "unhandled", error);
245
+
246
+ return Response.json(
247
+ { code: "INTERNAL_SERVER_ERROR", message: "Internal Server Error" },
248
+ { status: 500 },
249
+ );
222
250
  });
223
251
  ```
224
252
 
253
+ An error the hook answers is one nothing else reports: `reportError` hears
254
+ of a failure only when no `onError` hook answered it. So the hook that
255
+ answers the unexpected ones — a database gone, a `TypeError` — reports them
256
+ itself; left out, they answer in your format and never reach the error
257
+ tracker.
258
+
225
259
  The document is generated from the routes, not from that hook, so it is
226
260
  told the same format with `errors`:
227
261
 
@@ -269,16 +303,19 @@ code: (schema) => {
269
303
 
270
304
  The hook and `errors` describe one format in two places — the core knows
271
305
  nothing about documents, and a function cannot be read for the shape it
272
- returns — so keep a test that holds them together: provoke a failure and
273
- check its body against the document, with the JSON Schema validator of
274
- your choice.
306
+ returns — so keep a test that holds them together: provoke the failures,
307
+ the unexpected one included, and check each against the document with
308
+ [`assertDescribed`](#testing-against-the-document):
275
309
 
276
310
  ```ts
277
- test("a validation failure is what the document says", async () => {
278
- const res = await request("/items", { method: "POST", body: "{}" });
279
- const schema = document.components?.schemas?.ValidationFailed;
280
-
281
- expect(validator.validate(schema, await res.json())).toBe(true);
311
+ const boom = route({ method: "GET", path: "/boom", handler: () => { throw new Error("boom"); } });
312
+ const app = createApp({ hooks: { onError: [inOurFormat] }, routes: [api, boom] });
313
+ const request = serve(app);
314
+ const { document } = openapi(app, { info, errors });
315
+
316
+ test("failures are what the document says", async () => {
317
+ await assertDescribed(document, "POST /items", await request("/items", { method: "POST", body: "{}" }));
318
+ await assertDescribed(document, "GET /boom", await request("/boom"));
282
319
  });
283
320
  ```
284
321
 
@@ -418,3 +455,11 @@ gap is reported instead of failing the startup:
418
455
  This happens with a validator that does not emit JSON Schema, a schema
419
456
  whose conversion throws, and two routes that map to the same OpenAPI path
420
457
  (`/files/*` and `/files/:wildcard` are both `/files/{wildcard}`).
458
+
459
+ It also happens with a recursive or named schema. A schema is embedded as
460
+ the validator emits it, and the references inside it — `#` for its own
461
+ root, `#/$defs/…` for a definition next to it — resolve against the
462
+ document's root once embedded, where they lead nowhere. Zod's recursive
463
+ getters and `.meta({ id })`, ArkType's scopes and Valibot's `lazy` emit
464
+ them; TypeBox's `Type.Cyclic` names itself with `$id` and is described as
465
+ it is.
@@ -42,6 +42,7 @@ export type ContentMap = Record<string, {
42
42
  /** A header a response carries. */
43
43
  export interface HeaderObject {
44
44
  readonly description?: string;
45
+ readonly required?: boolean;
45
46
  readonly schema: JsonSchema;
46
47
  }
47
48
  /** One response of an operation. */
@@ -1 +1 @@
1
- {"version":3,"file":"document.d.ts","sourceRoot":"","sources":["../../src/document.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"document.d.ts","sourceRoot":"","sources":["../../src/document.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,eAAe,CAAC;AAGzC,OAAO,KAAK,EAAE,eAAe,EAA6B,MAAM,eAAe,CAAC;AAEhF,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAK/C,YAAY,EACV,gBAAgB,EAChB,kBAAkB,EAClB,iBAAiB,EACjB,QAAQ,EACR,oBAAoB,EACpB,mBAAmB,EACnB,cAAc,GACf,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAE,UAAU,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAC;AACvD,YAAY,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AACnD,OAAO,EAAE,cAAc,EAAE,IAAI,EAAE,MAAM,iBAAiB,CAAC;AACvD,YAAY,EACV,UAAU,EACV,YAAY,EACZ,cAAc,EACd,YAAY,EACZ,eAAe,EACf,eAAe,EACf,eAAe,EACf,cAAc,EACd,cAAc,EACd,SAAS,GACV,MAAM,eAAe,CAAC;AACvB,YAAY,EAAE,iBAAiB,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAClE,YAAY,EACV,UAAU,EACV,kBAAkB,EAClB,cAAc,GACf,MAAM,kBAAkB,CAAC;AAC1B,YAAY,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC;AACrE,OAAO,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AAErC;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,kEAAkE;IAClE,QAAQ,CAAC,IAAI,EAAE,eAAe,CAAC,MAAM,CAAC,CAAC;IAEvC,uCAAuC;IACvC,QAAQ,CAAC,OAAO,CAAC,EAAE,eAAe,CAAC,SAAS,CAAC,CAAC;IAE9C;;;;;;;;;;;;;;;;;;;;;;;;OAwBG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,WAAW,CAAC;IAE9B;;;;;;;;;;;;;;;;;;;;OAoBG;IACH,QAAQ,CAAC,IAAI,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;CAClD;AAED;;;;;;;GAOG;AACH,MAAM,WAAW,gBAAgB;IAC/B;;;OAGG;IACH,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B;AAED,sEAAsE;AACtE,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,QAAQ,EAAE,eAAe,CAAC;IACnC,QAAQ,CAAC,QAAQ,EAAE,SAAS,gBAAgB,EAAE,CAAC;CAChD;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,OAAO,CAAC,GAAG,EAAE,GAAG,EAAE,OAAO,EAAE,cAAc,GAAG,eAAe,CA2G1E"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,eAAe,CAAC;AAGzC,OAAO,KAAK,EAAE,eAAe,EAA6B,MAAM,eAAe,CAAC;AAEhF,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAM/C,YAAY,EACV,gBAAgB,EAChB,kBAAkB,EAClB,iBAAiB,EACjB,QAAQ,EACR,oBAAoB,EACpB,mBAAmB,EACnB,cAAc,GACf,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAE,UAAU,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAC;AACvD,YAAY,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AACnD,OAAO,EAAE,cAAc,EAAE,IAAI,EAAE,MAAM,iBAAiB,CAAC;AACvD,YAAY,EACV,UAAU,EACV,YAAY,EACZ,cAAc,EACd,YAAY,EACZ,eAAe,EACf,eAAe,EACf,eAAe,EACf,cAAc,EACd,cAAc,EACd,SAAS,GACV,MAAM,eAAe,CAAC;AACvB,YAAY,EAAE,iBAAiB,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAClE,YAAY,EACV,UAAU,EACV,kBAAkB,EAClB,cAAc,GACf,MAAM,kBAAkB,CAAC;AAC1B,YAAY,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC;AACrE,OAAO,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AAErC;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,kEAAkE;IAClE,QAAQ,CAAC,IAAI,EAAE,eAAe,CAAC,MAAM,CAAC,CAAC;IAEvC,uCAAuC;IACvC,QAAQ,CAAC,OAAO,CAAC,EAAE,eAAe,CAAC,SAAS,CAAC,CAAC;IAE9C;;;;;;;;;;;;;;;;;;;;;;;;OAwBG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,WAAW,CAAC;IAE9B;;;;;;;;;;;;;;;;;;;;OAoBG;IACH,QAAQ,CAAC,IAAI,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;CAClD;AAED;;;;;;;GAOG;AACH,MAAM,WAAW,gBAAgB;IAC/B;;;OAGG;IACH,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B;AAED,sEAAsE;AACtE,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,QAAQ,EAAE,eAAe,CAAC;IACnC,QAAQ,CAAC,QAAQ,EAAE,SAAS,gBAAgB,EAAE,CAAC;CAChD;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,OAAO,CAAC,GAAG,EAAE,GAAG,EAAE,OAAO,EAAE,cAAc,GAAG,eAAe,CA2G1E"}
package/dist/src/index.js CHANGED
@@ -373,8 +373,8 @@ function operationOf(entry, options, components, envelopes, ids, warn) {
373
373
  };
374
374
  }
375
375
  function declareEnvelopes(entry, envelopes) {
376
- for (const [status, schema] of declaredResponses(entry)) {
377
- const described = schema === null ? undefined : emitted(schema, "", () => {});
376
+ for (const [status, { body }] of declaredResponses(entry)) {
377
+ const described = body === null ? undefined : emitted(body, "", () => {});
378
378
  const code = envelopes.format.code;
379
379
  for (const branch of described ? branchesOf(described, code) : []) {
380
380
  const found = code(branch);
@@ -390,9 +390,62 @@ function declaredResponses(entry) {
390
390
  return [];
391
391
  }
392
392
  if ("~standard" in response) {
393
- return [["200", response]];
393
+ return [["200", { body: response }]];
394
394
  }
395
- return Object.entries(response);
395
+ return Object.entries(response).map(([status, value]) => [
396
+ status,
397
+ asDeclared(value)
398
+ ]);
399
+ }
400
+ function asDeclared(value) {
401
+ if (value === null || "~standard" in value) {
402
+ return { body: value };
403
+ }
404
+ return {
405
+ body: value.body ?? null,
406
+ ...value.headers === undefined ? {} : { headers: value.headers },
407
+ ...value.cookies === undefined ? {} : { cookies: value.cookies }
408
+ };
409
+ }
410
+ function declaredHeaders(declared, warn) {
411
+ const headers = {};
412
+ const [properties, required] = shapeOf2(declared.headers, "a response's headers", warn);
413
+ for (const [name, property] of Object.entries(properties)) {
414
+ const { description, ...schema } = property;
415
+ headers[name] = {
416
+ ...typeof description === "string" ? { description } : {},
417
+ ...required.has(name) ? { required: true } : {},
418
+ schema
419
+ };
420
+ }
421
+ const cookies = setCookie(declared.cookies, warn);
422
+ if (cookies) {
423
+ headers["set-cookie"] = cookies;
424
+ }
425
+ return Object.keys(headers).length > 0 ? headers : undefined;
426
+ }
427
+ function setCookie(schema, warn) {
428
+ const [properties, required] = shapeOf2(schema, "a response's cookies", warn);
429
+ const names = Object.keys(properties);
430
+ if (names.length === 0) {
431
+ return;
432
+ }
433
+ const lines = names.map((name) => {
434
+ const description = properties[name]?.["description"];
435
+ return typeof description === "string" ? `- \`${name}\`: ${description}` : `- \`${name}\``;
436
+ });
437
+ return {
438
+ description: ["Sets these cookies:", "", ...lines].join(`
439
+ `),
440
+ ...required.size > 0 ? { required: true } : {},
441
+ schema: { type: "string" }
442
+ };
443
+ }
444
+ function shapeOf2(schema, subject, warn) {
445
+ const described = schema ? emitted(schema, subject, warn, "input") : undefined;
446
+ const properties = described?.properties ?? {};
447
+ const required = new Set(Array.isArray(described?.required) ? described.required : []);
448
+ return [properties, required];
396
449
  }
397
450
  function branchesFor(schema, status, envelopes, warn) {
398
451
  return branchesOf(schema, envelopes.format.code).map((branch) => {
@@ -463,7 +516,7 @@ function derivedId(entry) {
463
516
  }
464
517
  function requestBody(entry, warn) {
465
518
  const declared = entry.def.schema?.body;
466
- const bodyType = entry.def.bodyType ?? (declared ? "json" : undefined);
519
+ const bodyType = entry.def.bodyType ?? (declared || entry.def.rawBody ? "json" : undefined);
467
520
  if (!bodyType) {
468
521
  return;
469
522
  }
@@ -508,15 +561,18 @@ function responses(entry, options, contributed, components, envelopes, warn) {
508
561
  const add = (status, answer) => {
509
562
  answers.set(status, [...answers.get(status) ?? [], answer]);
510
563
  };
511
- for (const [status, schema] of declaredResponses(entry)) {
512
- const described = schema === null ? undefined : emitted(schema, "a response", warn);
564
+ const declared = declaredResponses(entry);
565
+ for (const [status, { body, ...rest }] of declared) {
566
+ const described = body === null ? undefined : emitted(body, "a response", warn);
567
+ const headers = declaredHeaders({ body, ...rest }, warn);
513
568
  add(status, {
514
569
  placeholder: describeStatus(status),
515
570
  said: described ? wordsOf(described, envelopes) : [],
516
- schemas: described ? branchesFor(unionOnly(described), Number(status), envelopes, warn) : []
571
+ schemas: described ? branchesFor(unionOnly(described), Number(status), envelopes, warn) : [],
572
+ ...headers === undefined ? {} : { headers }
517
573
  });
518
574
  }
519
- if (![...answers.keys()].some((status) => status.startsWith("2"))) {
575
+ if (declared.length === 0) {
520
576
  add("200", { placeholder: "Successful response", said: [], schemas: [] });
521
577
  }
522
578
  for (const [status, answer] of failures(entry, options, contributed, components, envelopes, warn)) {
@@ -541,7 +597,7 @@ function failures(entry, options, contributed, components, envelopes, warn) {
541
597
  const ref = error === undefined ? components.ref(failureName(status), described) : envelopes.ref(status, error, described, warn);
542
598
  return ref;
543
599
  };
544
- if (schema?.params || schema?.query || schema?.headers || schema?.body) {
600
+ if (schema?.params || schema?.query || schema?.headers || schema?.cookies || schema?.body) {
545
601
  found.push([
546
602
  options.validationStatus,
547
603
  {
@@ -587,7 +643,7 @@ function failures(entry, options, contributed, components, envelopes, warn) {
587
643
  }
588
644
  ]);
589
645
  }
590
- if (schema?.body || entry.def.bodyType) {
646
+ if (schema?.body || entry.def.bodyType || entry.def.rawBody) {
591
647
  const unparsable = parseFailure(entry.def.bodyType);
592
648
  if (unparsable) {
593
649
  found.push([
@@ -745,6 +801,63 @@ function capitalize(value) {
745
801
  function lowerFirst(value) {
746
802
  return value.charAt(0).toLowerCase() + value.slice(1);
747
803
  }
804
+
805
+ // packages/openapi/src/refs.ts
806
+ var data = new Set(["const", "default", "enum", "example", "examples"]);
807
+ function unresolvedRefs(node, document) {
808
+ const unresolved = new Set;
809
+ const visited = new Map;
810
+ const pending = [[node, document]];
811
+ for (let next = pending.pop();next; next = pending.pop()) {
812
+ const [value, base] = next;
813
+ if (typeof value !== "object" || value === null) {
814
+ continue;
815
+ }
816
+ const bases = visited.get(value) ?? new Set;
817
+ if (bases.has(base)) {
818
+ continue;
819
+ }
820
+ visited.set(value, bases.add(base));
821
+ const schema = value;
822
+ const resource = !Array.isArray(value) && typeof schema["$id"] === "string" ? schema : base;
823
+ const ref = Array.isArray(value) ? undefined : schema["$ref"];
824
+ if (typeof ref === "string" && ref.startsWith("#") && !resolves(ref.slice(1), resource, document)) {
825
+ unresolved.add(ref);
826
+ }
827
+ const children = Object.entries(schema).filter(([keyword]) => Array.isArray(value) || !data.has(keyword));
828
+ for (let index = children.length - 1;index >= 0; index -= 1) {
829
+ pending.push([children[index]?.[1], resource]);
830
+ }
831
+ }
832
+ return [...unresolved];
833
+ }
834
+ function resolves(fragment, base, document) {
835
+ if (fragment === "") {
836
+ return base !== document;
837
+ }
838
+ if (!fragment.startsWith("/")) {
839
+ return true;
840
+ }
841
+ let target = base;
842
+ for (const token of fragment.slice(1).split("/")) {
843
+ if (typeof target !== "object" || target === null) {
844
+ return false;
845
+ }
846
+ const key = decoded(token);
847
+ if (key === undefined || !Object.hasOwn(target, key)) {
848
+ return false;
849
+ }
850
+ target = target[key];
851
+ }
852
+ return typeof target === "object" && target !== null || typeof target === "boolean";
853
+ }
854
+ function decoded(token) {
855
+ try {
856
+ return decodeURIComponent(token).replaceAll("~1", "/").replaceAll("~0", "~");
857
+ } catch {
858
+ return;
859
+ }
860
+ }
748
861
  // packages/openapi/src/controller.ts
749
862
  import { onMount, route } from "@tetsujs/core";
750
863
 
@@ -787,7 +900,7 @@ function docsPage(options) {
787
900
  "<body>",
788
901
  body(options.ui, url),
789
902
  `<script src="${escapeHtml(assets.script)}"${verified(assets.integrity?.script)}></script>`,
790
- bootstrap(options.ui, url),
903
+ bootstrap(options.ui),
791
904
  "</body>",
792
905
  "</html>"
793
906
  ].filter(Boolean).join(`
@@ -800,15 +913,16 @@ function body(ui, url) {
800
913
  if (ui === "redoc") {
801
914
  return `<redoc spec-url="${url}"></redoc>`;
802
915
  }
803
- return '<div id="swagger-ui"></div>';
916
+ return `<div id="swagger-ui" data-url="${url}"></div>`;
804
917
  }
805
- function bootstrap(ui, url) {
918
+ function bootstrap(ui) {
806
919
  if (ui !== "swagger-ui") {
807
920
  return "";
808
921
  }
809
922
  return [
810
923
  "<script>",
811
- `SwaggerUIBundle({ url: "${url}", dom_id: "#swagger-ui" });`,
924
+ 'const root = document.getElementById("swagger-ui");',
925
+ 'SwaggerUIBundle({ url: root.dataset.url, dom_id: "#swagger-ui" });',
812
926
  "</script>"
813
927
  ].join(`
814
928
  `);
@@ -938,17 +1052,36 @@ function openapi(app, options) {
938
1052
  };
939
1053
  }
940
1054
  const tags = options.tags ? tagsOf(options.tags, used, warnings) : undefined;
941
- return {
942
- document: {
943
- openapi: "3.1.0",
944
- info: options.info,
945
- ...options.servers ? { servers: options.servers } : {},
946
- ...tags ? { tags } : {},
947
- paths,
948
- ...componentsOf(schemes, components.schemas)
949
- },
950
- warnings
1055
+ const document = {
1056
+ openapi: "3.1.0",
1057
+ info: options.info,
1058
+ ...options.servers ? { servers: options.servers } : {},
1059
+ ...tags ? { tags } : {},
1060
+ paths,
1061
+ ...componentsOf(schemes, components.schemas)
951
1062
  };
1063
+ refsOf(document, occupied, warnings);
1064
+ return { document, warnings };
1065
+ }
1066
+ function refsOf(document, routes, warnings) {
1067
+ const unresolved = (ref) => `refers to "${ref}", which is no schema in the document: references are embedded as they are, and resolve against the document's root`;
1068
+ for (const [template, item] of Object.entries(document.paths)) {
1069
+ for (const [method, operation] of Object.entries(item)) {
1070
+ const route = routes.get(`${method} ${template}`) ?? "";
1071
+ for (const ref of unresolvedRefs(operation, document)) {
1072
+ warnings.push({ route, message: `a schema ${unresolved(ref)}` });
1073
+ }
1074
+ }
1075
+ }
1076
+ const schemas = document.components?.schemas ?? {};
1077
+ for (const [name, schema] of Object.entries(schemas)) {
1078
+ for (const ref of unresolvedRefs(schema, document)) {
1079
+ warnings.push({
1080
+ route: "",
1081
+ message: `components/schemas/${name} ${unresolved(ref)}`
1082
+ });
1083
+ }
1084
+ }
952
1085
  }
953
1086
  function tagsOf(described, used, warnings) {
954
1087
  const tags = Object.entries(described).map(([name, description]) => ({ name, description }));
@@ -988,5 +1121,5 @@ export {
988
1121
  secured
989
1122
  };
990
1123
 
991
- //# debugId=3A3B60517CA111D464756E2164756E21
1124
+ //# debugId=A9BC9A29B6C6C1DA64756E2164756E21
992
1125
  //# sourceMappingURL=index.js.map