hono-openapi 1.3.2 → 1.3.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.
package/dist/index.cjs CHANGED
@@ -55,10 +55,13 @@ function mergeParameters(...params) {
55
55
  }, /* @__PURE__ */ new Map());
56
56
  return Array.from(merged.values());
57
57
  }
58
+ const specComponents = /* @__PURE__ */ new WeakMap();
59
+ const setSpecComponents = (spec, components) => specComponents.set(spec, components);
60
+ const getSpecComponents = (spec) => spec != null && typeof spec === "object" && specComponents.get(spec) || [];
58
61
  const specsByPathContext = /* @__PURE__ */ new Map();
59
- function getPathContext(path) {
62
+ function getPathContext(path, pathContext) {
60
63
  const context = [];
61
- for (const [key, data] of specsByPathContext) {
64
+ for (const [key, data] of pathContext) {
62
65
  if (!data) continue;
63
66
  const prefix = key.endsWith("/*") ? key.slice(0, -2) : key;
64
67
  if (path === prefix || path.startsWith(`${prefix}/`)) {
@@ -70,8 +73,21 @@ function getPathContext(path) {
70
73
  function clearSpecsContext() {
71
74
  specsByPathContext.clear();
72
75
  }
76
+ function mergeRequestBodies(previous, current) {
77
+ if (!previous || !current || "$ref" in previous || "$ref" in current) {
78
+ return current;
79
+ }
80
+ return {
81
+ ...previous,
82
+ ...current,
83
+ content: {
84
+ ...previous.content,
85
+ ...current.content
86
+ }
87
+ };
88
+ }
73
89
  function mergeSpecs(route, ...specs) {
74
- return specs.reduce(
90
+ const merged = specs.reduce(
75
91
  (prev, spec) => {
76
92
  if (!spec || !prev) return prev;
77
93
  for (const [key, value] of Object.entries(spec)) {
@@ -91,6 +107,11 @@ function mergeSpecs(route, ...specs) {
91
107
  } else {
92
108
  if (key === "parameters") {
93
109
  prev[key] = mergeParameters(prev[key], value);
110
+ } else if (key === "requestBody") {
111
+ prev.requestBody = mergeRequestBodies(
112
+ prev.requestBody,
113
+ value
114
+ );
94
115
  } else {
95
116
  prev[key] = {
96
117
  ...prev[key],
@@ -108,31 +129,29 @@ function mergeSpecs(route, ...specs) {
108
129
  operationId: generateOperationId(route)
109
130
  }
110
131
  );
132
+ setSpecComponents(merged, specs.flatMap(getSpecComponents));
133
+ return merged;
111
134
  }
112
- function registerSchemaPath({
113
- route,
114
- specs,
115
- paths
116
- }) {
135
+ function registerSchemaPath({ route, specs, paths }, pathContext = specsByPathContext) {
117
136
  const path = toOpenAPIPath(route.path);
118
137
  const method = route.method.toLowerCase();
119
138
  if (method === "all") {
120
139
  if (!specs) return;
121
- if (specsByPathContext.has(path)) {
122
- const prev = specsByPathContext.get(path) ?? {};
123
- specsByPathContext.set(path, mergeSpecs(route, prev, specs));
140
+ if (pathContext.has(path)) {
141
+ const prev = pathContext.get(path) ?? {};
142
+ pathContext.set(path, mergeSpecs(route, prev, specs));
124
143
  } else {
125
- specsByPathContext.set(path, specs);
144
+ pathContext.set(path, specs);
126
145
  }
127
146
  } else {
128
- const pathContext = getPathContext(path);
147
+ const context = getPathContext(path, pathContext);
129
148
  if (!(path in paths)) {
130
149
  paths[path] = {};
131
150
  }
132
151
  if (paths[path]) {
133
152
  paths[path][method] = mergeSpecs(
134
153
  route,
135
- ...pathContext,
154
+ ...context,
136
155
  paths[path]?.[method],
137
156
  specs
138
157
  );
@@ -226,6 +245,52 @@ function removeExcludedPaths(paths, ctx) {
226
245
  }
227
246
  return newPaths;
228
247
  }
248
+ const COMPONENTS_PREFIX = "#/components/";
249
+ const componentKey = (type, name) => `${type}/${name.replaceAll("~", "~0").replaceAll("/", "~1")}`;
250
+ const componentRefs = (value) => {
251
+ if (Array.isArray(value)) {
252
+ return value.flatMap(componentRefs);
253
+ }
254
+ if (value == null || typeof value !== "object") {
255
+ return [];
256
+ }
257
+ return Object.entries(value).flatMap(([key, item]) => {
258
+ if (key !== "$ref" || typeof item !== "string") {
259
+ return componentRefs(item);
260
+ }
261
+ return item.startsWith(COMPONENTS_PREFIX) ? [item.slice(COMPONENTS_PREFIX.length)] : [];
262
+ });
263
+ };
264
+ const reachableRefs = (refs, components, reached) => refs.reduce((seen, ref) => {
265
+ if (seen.has(ref)) {
266
+ return seen;
267
+ }
268
+ const [type, ...name] = ref.split("/");
269
+ const component = components[type]?.[name.join("/").replaceAll("~1", "/").replaceAll("~0", "~")];
270
+ return reachableRefs(componentRefs(component), components, seen.add(ref));
271
+ }, reached);
272
+ function documentedComponents(components, paths, documentation) {
273
+ const contributed = new Set(
274
+ Object.values(paths).flatMap((item) => item == null ? [] : Object.values(item)).flatMap(getSpecComponents).flatMap(
275
+ (operationComponents) => Object.entries(operationComponents).flatMap(
276
+ ([type, entries]) => Object.keys(entries ?? {}).map((name) => componentKey(type, name))
277
+ )
278
+ )
279
+ );
280
+ const kept = reachableRefs(
281
+ componentRefs(documentation),
282
+ components,
283
+ contributed
284
+ );
285
+ return Object.fromEntries(
286
+ Object.entries(components).flatMap(([type, entries]) => {
287
+ const remaining = Object.entries(entries ?? {}).filter(
288
+ ([name]) => kept.has(componentKey(type, name))
289
+ );
290
+ return remaining.length > 0 ? [[type, Object.fromEntries(remaining)]] : [];
291
+ })
292
+ );
293
+ }
229
294
 
230
295
  const DEFAULT_OPTIONS = {
231
296
  documentation: {},
@@ -277,7 +342,6 @@ async function generateSpecs(hono, options = DEFAULT_OPTIONS, c) {
277
342
  ..._documentation,
278
343
  components: _documentation.components && { ..._documentation.components }
279
344
  };
280
- clearSpecsContext();
281
345
  const paths = await generatePaths(hono, ctx);
282
346
  for (const path in paths) {
283
347
  for (const method in paths[path]) {
@@ -296,10 +360,14 @@ async function generateSpecs(hono, options = DEFAULT_OPTIONS, c) {
296
360
  if (resolvedDocumentation && documentation.components) {
297
361
  documentation.components.responses = resolvedDocumentation.responses;
298
362
  }
363
+ const documentedPaths = removeExcludedPaths(paths, ctx);
299
364
  const components = mergeComponentsObjects(
300
365
  documentation.components,
301
366
  resolvedDocumentation?.components,
302
- ctx.components
367
+ documentedComponents(ctx.components, documentedPaths, [
368
+ documentation,
369
+ resolvedDocumentation?.components
370
+ ])
303
371
  );
304
372
  return {
305
373
  openapi: "3.1.0",
@@ -314,7 +382,7 @@ async function generateSpecs(hono, options = DEFAULT_OPTIONS, c) {
314
382
  ...documentation.info
315
383
  },
316
384
  paths: {
317
- ...removeExcludedPaths(paths, ctx),
385
+ ...documentedPaths,
318
386
  ...documentation.paths
319
387
  },
320
388
  components
@@ -322,14 +390,18 @@ async function generateSpecs(hono, options = DEFAULT_OPTIONS, c) {
322
390
  }
323
391
  async function generatePaths(hono, ctx) {
324
392
  const paths = {};
393
+ const pathContext = /* @__PURE__ */ new Map();
325
394
  for (const route of hono.routes) {
326
395
  const middlewareHandler = handler.findTargetHandler(route.handler)[uniqueSymbol];
327
396
  if (!middlewareHandler) {
328
397
  if (ctx.options.includeEmptyPaths) {
329
- registerSchemaPath({
330
- route,
331
- paths
332
- });
398
+ registerSchemaPath(
399
+ {
400
+ route,
401
+ paths
402
+ },
403
+ pathContext
404
+ );
333
405
  }
334
406
  continue;
335
407
  }
@@ -345,14 +417,19 @@ async function generatePaths(hono, ctx) {
345
417
  const defaultOptionsForThisMethod = ctx.options.defaultOptions?.[routeMethod] && { ...ctx.options.defaultOptions[routeMethod] };
346
418
  const { schema: routeSpecs, components = {} } = await getSpec(
347
419
  middlewareHandler,
348
- defaultOptionsForThisMethod
420
+ defaultOptionsForThisMethod,
421
+ ctx.components.parameters
349
422
  );
350
423
  ctx.components = mergeComponentsObjects(ctx.components, components);
351
- registerSchemaPath({
352
- route,
353
- specs: routeSpecs,
354
- paths
355
- });
424
+ setSpecComponents(routeSpecs, [components]);
425
+ registerSchemaPath(
426
+ {
427
+ route,
428
+ specs: routeSpecs,
429
+ paths
430
+ },
431
+ pathContext
432
+ );
356
433
  }
357
434
  return paths;
358
435
  }
@@ -368,7 +445,7 @@ function getHiddenValue(options) {
368
445
  }
369
446
  return false;
370
447
  }
371
- async function getSpec(middlewareHandler, defaultOptions) {
448
+ async function getSpec(middlewareHandler, defaultOptions, parameterComponents) {
372
449
  if ("spec" in middlewareHandler) {
373
450
  const tmp = {
374
451
  ...defaultOptions,
@@ -403,7 +480,7 @@ async function getSpec(middlewareHandler, defaultOptions) {
403
480
  [VALIDATION_MARKER]: true
404
481
  };
405
482
  if (middlewareHandler.target === "form" || middlewareHandler.target === "json") {
406
- const media = middlewareHandler.options?.media ?? middlewareHandler.target === "json" ? "application/json" : "multipart/form-data";
483
+ const media = middlewareHandler.options?.media ?? (middlewareHandler.target === "json" ? "application/json" : "multipart/form-data");
407
484
  if (!docs.requestBody || !("content" in docs.requestBody) || !docs.requestBody.content) {
408
485
  docs.requestBody = {
409
486
  required: true,
@@ -425,18 +502,21 @@ async function getSpec(middlewareHandler, defaultOptions) {
425
502
  const pos = ref.split("/").pop();
426
503
  if (pos && result.components?.schemas?.[pos]) {
427
504
  const schema = result.components.schemas[pos];
428
- const newParameters = generateParameters(
505
+ const generatedParameters = generateParameters(
429
506
  middlewareHandler.target,
430
- schema
431
- )[0];
432
- if (!result.components.parameters) {
433
- result.components.parameters = {};
507
+ schema,
508
+ result.components.schemas
509
+ );
510
+ const singleParameter = generatedParameters.length === 1 ? generatedParameters[0] : void 0;
511
+ const existingParameter = parameterComponents?.[pos];
512
+ if (singleParameter && (!existingParameter || "in" in existingParameter && existingParameter.in === singleParameter.in && existingParameter.name === singleParameter.name)) {
513
+ result.components.parameters ??= {};
514
+ result.components.parameters[pos] = singleParameter;
515
+ parameters.push({ $ref: `#/components/parameters/${pos}` });
516
+ delete result.components.schemas[pos];
517
+ } else {
518
+ parameters = generatedParameters;
434
519
  }
435
- result.components.parameters[pos] = newParameters;
436
- delete result.components.schemas[pos];
437
- parameters.push({
438
- $ref: `#/components/parameters/${pos}`
439
- });
440
520
  }
441
521
  } else {
442
522
  parameters = generateParameters(
@@ -695,11 +775,14 @@ exports.VALIDATION_MARKER = VALIDATION_MARKER;
695
775
  exports.clearSpecsContext = clearSpecsContext;
696
776
  exports.describeResponse = describeResponse;
697
777
  exports.describeRoute = describeRoute;
778
+ exports.documentedComponents = documentedComponents;
698
779
  exports.generateSpecs = generateSpecs;
780
+ exports.getSpecComponents = getSpecComponents;
699
781
  exports.loadVendor = loadVendor;
700
782
  exports.openAPIRouteHandler = openAPIRouteHandler;
701
783
  exports.registerSchemaPath = registerSchemaPath;
702
784
  exports.removeExcludedPaths = removeExcludedPaths;
703
785
  exports.resolver = resolver;
786
+ exports.setSpecComponents = setSpecComponents;
704
787
  exports.uniqueSymbol = uniqueSymbol;
705
788
  exports.validator = validator;
package/dist/index.d.cts CHANGED
@@ -1,5 +1,6 @@
1
1
  import * as openapi_types from 'openapi-types';
2
2
  import { OpenAPIV3_1 } from 'openapi-types';
3
+ import * as hono from 'hono';
3
4
  import { Env, Input, Context, Next, MiddlewareHandler, ValidationTargets, Hono } from 'hono';
4
5
  import { TypedResponse, RouterRoute, ValidationTargets as ValidationTargets$1, BlankEnv, Input as Input$1, BlankInput, Schema, BlankSchema, MiddlewareHandler as MiddlewareHandler$1 } from 'hono/types';
5
6
  import { loadVendor as loadVendor$2, ToOpenAPISchemaContext } from '@standard-community/standard-openapi';
@@ -83,9 +84,34 @@ declare const uniqueSymbol: unique symbol;
83
84
  declare const VALIDATION_MARKER = "__HonoOpenAPIValidator__";
84
85
  declare const ALLOWED_METHODS: readonly ["GET", "PUT", "POST", "DELETE", "OPTIONS", "HEAD", "PATCH", "TRACE"];
85
86
  type AllowedMethods = (typeof ALLOWED_METHODS)[number];
87
+ declare const setSpecComponents: (spec: object, components: OpenAPIV3_1.ComponentsObject[]) => WeakMap<object, OpenAPIV3_1.ComponentsObject[]>;
88
+ declare const getSpecComponents: (spec: unknown) => OpenAPIV3_1.ComponentsObject[];
86
89
  declare function clearSpecsContext(): void;
87
- declare function registerSchemaPath({ route, specs, paths, }: RegisterSchemaPathOptions): void;
90
+ declare function registerSchemaPath({ route, specs, paths }: RegisterSchemaPathOptions, pathContext?: Map<string, Omit<OpenAPIV3_1.OperationObject<{}>, "responses" | "operationId" | "requestBody"> & {
91
+ operationId?: string | ((route: RouterRoute) => string);
92
+ requestBody?: OpenAPIV3_1.ReferenceObject | (Omit<OpenAPIV3_1.RequestBodyObject, "content"> & {
93
+ content: ContentWithResolver;
94
+ });
95
+ hide?: boolean | ((props: {
96
+ c?: hono.Context;
97
+ method: string;
98
+ path: string;
99
+ }) => boolean);
100
+ responses?: ResponsesWithResolver;
101
+ } & {
102
+ operationId?: string | ((route: RouterRoute) => string);
103
+ }>): void;
88
104
  declare function removeExcludedPaths(paths: OpenAPIV3_1.PathsObject, ctx: SpecContext): OpenAPIV3_1.PathsObject<{}, {}>;
105
+ /**
106
+ * Keeps the generated components that the documented operations brought in,
107
+ * and the ones `documentation` refers to, directly or through other
108
+ * components.
109
+ *
110
+ * Components are collected from every route while the paths are generated,
111
+ * including routes that are hidden or excluded afterwards. Without this, those
112
+ * routes would still publish the schemas only they use.
113
+ */
114
+ declare function documentedComponents(components: OpenAPIV3_1.ComponentsObject, paths: OpenAPIV3_1.PathsObject, documentation: unknown): OpenAPIV3_1.ComponentsObject;
89
115
 
90
116
  type PromiseOr<T> = T | Promise<T>;
91
117
  type ResolverReturnType = ReturnType<typeof resolver> & {
@@ -265,4 +291,4 @@ declare function generateSpecs<E extends Env = BlankEnv, P extends string = stri
265
291
  jsonSchemaDialect?: string;
266
292
  }>;
267
293
 
268
- export { ALLOWED_METHODS, type AllowedMethods, type ContentWithResolver, type DescribeRouteOptions, type GenerateSpecOptions, type HandlerUniqueProperty, type PromiseOr, type RegisterSchemaPathOptions, type ResolverReturnType, type ResponsesWithResolver, type SpecContext, VALIDATION_MARKER, clearSpecsContext, describeResponse, describeRoute, generateSpecs, loadVendor, openAPIRouteHandler, registerSchemaPath, removeExcludedPaths, resolver, uniqueSymbol, validator };
294
+ export { ALLOWED_METHODS, type AllowedMethods, type ContentWithResolver, type DescribeRouteOptions, type GenerateSpecOptions, type HandlerUniqueProperty, type PromiseOr, type RegisterSchemaPathOptions, type ResolverReturnType, type ResponsesWithResolver, type SpecContext, VALIDATION_MARKER, clearSpecsContext, describeResponse, describeRoute, documentedComponents, generateSpecs, getSpecComponents, loadVendor, openAPIRouteHandler, registerSchemaPath, removeExcludedPaths, resolver, setSpecComponents, uniqueSymbol, validator };
package/dist/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import * as openapi_types from 'openapi-types';
2
2
  import { OpenAPIV3_1 } from 'openapi-types';
3
+ import * as hono from 'hono';
3
4
  import { Env, Input, Context, Next, MiddlewareHandler, ValidationTargets, Hono } from 'hono';
4
5
  import { TypedResponse, RouterRoute, ValidationTargets as ValidationTargets$1, BlankEnv, Input as Input$1, BlankInput, Schema, BlankSchema, MiddlewareHandler as MiddlewareHandler$1 } from 'hono/types';
5
6
  import { loadVendor as loadVendor$2, ToOpenAPISchemaContext } from '@standard-community/standard-openapi';
@@ -83,9 +84,34 @@ declare const uniqueSymbol: unique symbol;
83
84
  declare const VALIDATION_MARKER = "__HonoOpenAPIValidator__";
84
85
  declare const ALLOWED_METHODS: readonly ["GET", "PUT", "POST", "DELETE", "OPTIONS", "HEAD", "PATCH", "TRACE"];
85
86
  type AllowedMethods = (typeof ALLOWED_METHODS)[number];
87
+ declare const setSpecComponents: (spec: object, components: OpenAPIV3_1.ComponentsObject[]) => WeakMap<object, OpenAPIV3_1.ComponentsObject[]>;
88
+ declare const getSpecComponents: (spec: unknown) => OpenAPIV3_1.ComponentsObject[];
86
89
  declare function clearSpecsContext(): void;
87
- declare function registerSchemaPath({ route, specs, paths, }: RegisterSchemaPathOptions): void;
90
+ declare function registerSchemaPath({ route, specs, paths }: RegisterSchemaPathOptions, pathContext?: Map<string, Omit<OpenAPIV3_1.OperationObject<{}>, "responses" | "operationId" | "requestBody"> & {
91
+ operationId?: string | ((route: RouterRoute) => string);
92
+ requestBody?: OpenAPIV3_1.ReferenceObject | (Omit<OpenAPIV3_1.RequestBodyObject, "content"> & {
93
+ content: ContentWithResolver;
94
+ });
95
+ hide?: boolean | ((props: {
96
+ c?: hono.Context;
97
+ method: string;
98
+ path: string;
99
+ }) => boolean);
100
+ responses?: ResponsesWithResolver;
101
+ } & {
102
+ operationId?: string | ((route: RouterRoute) => string);
103
+ }>): void;
88
104
  declare function removeExcludedPaths(paths: OpenAPIV3_1.PathsObject, ctx: SpecContext): OpenAPIV3_1.PathsObject<{}, {}>;
105
+ /**
106
+ * Keeps the generated components that the documented operations brought in,
107
+ * and the ones `documentation` refers to, directly or through other
108
+ * components.
109
+ *
110
+ * Components are collected from every route while the paths are generated,
111
+ * including routes that are hidden or excluded afterwards. Without this, those
112
+ * routes would still publish the schemas only they use.
113
+ */
114
+ declare function documentedComponents(components: OpenAPIV3_1.ComponentsObject, paths: OpenAPIV3_1.PathsObject, documentation: unknown): OpenAPIV3_1.ComponentsObject;
89
115
 
90
116
  type PromiseOr<T> = T | Promise<T>;
91
117
  type ResolverReturnType = ReturnType<typeof resolver> & {
@@ -265,4 +291,4 @@ declare function generateSpecs<E extends Env = BlankEnv, P extends string = stri
265
291
  jsonSchemaDialect?: string;
266
292
  }>;
267
293
 
268
- export { ALLOWED_METHODS, type AllowedMethods, type ContentWithResolver, type DescribeRouteOptions, type GenerateSpecOptions, type HandlerUniqueProperty, type PromiseOr, type RegisterSchemaPathOptions, type ResolverReturnType, type ResponsesWithResolver, type SpecContext, VALIDATION_MARKER, clearSpecsContext, describeResponse, describeRoute, generateSpecs, loadVendor, openAPIRouteHandler, registerSchemaPath, removeExcludedPaths, resolver, uniqueSymbol, validator };
294
+ export { ALLOWED_METHODS, type AllowedMethods, type ContentWithResolver, type DescribeRouteOptions, type GenerateSpecOptions, type HandlerUniqueProperty, type PromiseOr, type RegisterSchemaPathOptions, type ResolverReturnType, type ResponsesWithResolver, type SpecContext, VALIDATION_MARKER, clearSpecsContext, describeResponse, describeRoute, documentedComponents, generateSpecs, getSpecComponents, loadVendor, openAPIRouteHandler, registerSchemaPath, removeExcludedPaths, resolver, setSpecComponents, uniqueSymbol, validator };
package/dist/index.js CHANGED
@@ -53,10 +53,13 @@ function mergeParameters(...params) {
53
53
  }, /* @__PURE__ */ new Map());
54
54
  return Array.from(merged.values());
55
55
  }
56
+ const specComponents = /* @__PURE__ */ new WeakMap();
57
+ const setSpecComponents = (spec, components) => specComponents.set(spec, components);
58
+ const getSpecComponents = (spec) => spec != null && typeof spec === "object" && specComponents.get(spec) || [];
56
59
  const specsByPathContext = /* @__PURE__ */ new Map();
57
- function getPathContext(path) {
60
+ function getPathContext(path, pathContext) {
58
61
  const context = [];
59
- for (const [key, data] of specsByPathContext) {
62
+ for (const [key, data] of pathContext) {
60
63
  if (!data) continue;
61
64
  const prefix = key.endsWith("/*") ? key.slice(0, -2) : key;
62
65
  if (path === prefix || path.startsWith(`${prefix}/`)) {
@@ -68,8 +71,21 @@ function getPathContext(path) {
68
71
  function clearSpecsContext() {
69
72
  specsByPathContext.clear();
70
73
  }
74
+ function mergeRequestBodies(previous, current) {
75
+ if (!previous || !current || "$ref" in previous || "$ref" in current) {
76
+ return current;
77
+ }
78
+ return {
79
+ ...previous,
80
+ ...current,
81
+ content: {
82
+ ...previous.content,
83
+ ...current.content
84
+ }
85
+ };
86
+ }
71
87
  function mergeSpecs(route, ...specs) {
72
- return specs.reduce(
88
+ const merged = specs.reduce(
73
89
  (prev, spec) => {
74
90
  if (!spec || !prev) return prev;
75
91
  for (const [key, value] of Object.entries(spec)) {
@@ -89,6 +105,11 @@ function mergeSpecs(route, ...specs) {
89
105
  } else {
90
106
  if (key === "parameters") {
91
107
  prev[key] = mergeParameters(prev[key], value);
108
+ } else if (key === "requestBody") {
109
+ prev.requestBody = mergeRequestBodies(
110
+ prev.requestBody,
111
+ value
112
+ );
92
113
  } else {
93
114
  prev[key] = {
94
115
  ...prev[key],
@@ -106,31 +127,29 @@ function mergeSpecs(route, ...specs) {
106
127
  operationId: generateOperationId(route)
107
128
  }
108
129
  );
130
+ setSpecComponents(merged, specs.flatMap(getSpecComponents));
131
+ return merged;
109
132
  }
110
- function registerSchemaPath({
111
- route,
112
- specs,
113
- paths
114
- }) {
133
+ function registerSchemaPath({ route, specs, paths }, pathContext = specsByPathContext) {
115
134
  const path = toOpenAPIPath(route.path);
116
135
  const method = route.method.toLowerCase();
117
136
  if (method === "all") {
118
137
  if (!specs) return;
119
- if (specsByPathContext.has(path)) {
120
- const prev = specsByPathContext.get(path) ?? {};
121
- specsByPathContext.set(path, mergeSpecs(route, prev, specs));
138
+ if (pathContext.has(path)) {
139
+ const prev = pathContext.get(path) ?? {};
140
+ pathContext.set(path, mergeSpecs(route, prev, specs));
122
141
  } else {
123
- specsByPathContext.set(path, specs);
142
+ pathContext.set(path, specs);
124
143
  }
125
144
  } else {
126
- const pathContext = getPathContext(path);
145
+ const context = getPathContext(path, pathContext);
127
146
  if (!(path in paths)) {
128
147
  paths[path] = {};
129
148
  }
130
149
  if (paths[path]) {
131
150
  paths[path][method] = mergeSpecs(
132
151
  route,
133
- ...pathContext,
152
+ ...context,
134
153
  paths[path]?.[method],
135
154
  specs
136
155
  );
@@ -224,6 +243,52 @@ function removeExcludedPaths(paths, ctx) {
224
243
  }
225
244
  return newPaths;
226
245
  }
246
+ const COMPONENTS_PREFIX = "#/components/";
247
+ const componentKey = (type, name) => `${type}/${name.replaceAll("~", "~0").replaceAll("/", "~1")}`;
248
+ const componentRefs = (value) => {
249
+ if (Array.isArray(value)) {
250
+ return value.flatMap(componentRefs);
251
+ }
252
+ if (value == null || typeof value !== "object") {
253
+ return [];
254
+ }
255
+ return Object.entries(value).flatMap(([key, item]) => {
256
+ if (key !== "$ref" || typeof item !== "string") {
257
+ return componentRefs(item);
258
+ }
259
+ return item.startsWith(COMPONENTS_PREFIX) ? [item.slice(COMPONENTS_PREFIX.length)] : [];
260
+ });
261
+ };
262
+ const reachableRefs = (refs, components, reached) => refs.reduce((seen, ref) => {
263
+ if (seen.has(ref)) {
264
+ return seen;
265
+ }
266
+ const [type, ...name] = ref.split("/");
267
+ const component = components[type]?.[name.join("/").replaceAll("~1", "/").replaceAll("~0", "~")];
268
+ return reachableRefs(componentRefs(component), components, seen.add(ref));
269
+ }, reached);
270
+ function documentedComponents(components, paths, documentation) {
271
+ const contributed = new Set(
272
+ Object.values(paths).flatMap((item) => item == null ? [] : Object.values(item)).flatMap(getSpecComponents).flatMap(
273
+ (operationComponents) => Object.entries(operationComponents).flatMap(
274
+ ([type, entries]) => Object.keys(entries ?? {}).map((name) => componentKey(type, name))
275
+ )
276
+ )
277
+ );
278
+ const kept = reachableRefs(
279
+ componentRefs(documentation),
280
+ components,
281
+ contributed
282
+ );
283
+ return Object.fromEntries(
284
+ Object.entries(components).flatMap(([type, entries]) => {
285
+ const remaining = Object.entries(entries ?? {}).filter(
286
+ ([name]) => kept.has(componentKey(type, name))
287
+ );
288
+ return remaining.length > 0 ? [[type, Object.fromEntries(remaining)]] : [];
289
+ })
290
+ );
291
+ }
227
292
 
228
293
  const DEFAULT_OPTIONS = {
229
294
  documentation: {},
@@ -275,7 +340,6 @@ async function generateSpecs(hono, options = DEFAULT_OPTIONS, c) {
275
340
  ..._documentation,
276
341
  components: _documentation.components && { ..._documentation.components }
277
342
  };
278
- clearSpecsContext();
279
343
  const paths = await generatePaths(hono, ctx);
280
344
  for (const path in paths) {
281
345
  for (const method in paths[path]) {
@@ -294,10 +358,14 @@ async function generateSpecs(hono, options = DEFAULT_OPTIONS, c) {
294
358
  if (resolvedDocumentation && documentation.components) {
295
359
  documentation.components.responses = resolvedDocumentation.responses;
296
360
  }
361
+ const documentedPaths = removeExcludedPaths(paths, ctx);
297
362
  const components = mergeComponentsObjects(
298
363
  documentation.components,
299
364
  resolvedDocumentation?.components,
300
- ctx.components
365
+ documentedComponents(ctx.components, documentedPaths, [
366
+ documentation,
367
+ resolvedDocumentation?.components
368
+ ])
301
369
  );
302
370
  return {
303
371
  openapi: "3.1.0",
@@ -312,7 +380,7 @@ async function generateSpecs(hono, options = DEFAULT_OPTIONS, c) {
312
380
  ...documentation.info
313
381
  },
314
382
  paths: {
315
- ...removeExcludedPaths(paths, ctx),
383
+ ...documentedPaths,
316
384
  ...documentation.paths
317
385
  },
318
386
  components
@@ -320,14 +388,18 @@ async function generateSpecs(hono, options = DEFAULT_OPTIONS, c) {
320
388
  }
321
389
  async function generatePaths(hono, ctx) {
322
390
  const paths = {};
391
+ const pathContext = /* @__PURE__ */ new Map();
323
392
  for (const route of hono.routes) {
324
393
  const middlewareHandler = findTargetHandler(route.handler)[uniqueSymbol];
325
394
  if (!middlewareHandler) {
326
395
  if (ctx.options.includeEmptyPaths) {
327
- registerSchemaPath({
328
- route,
329
- paths
330
- });
396
+ registerSchemaPath(
397
+ {
398
+ route,
399
+ paths
400
+ },
401
+ pathContext
402
+ );
331
403
  }
332
404
  continue;
333
405
  }
@@ -343,14 +415,19 @@ async function generatePaths(hono, ctx) {
343
415
  const defaultOptionsForThisMethod = ctx.options.defaultOptions?.[routeMethod] && { ...ctx.options.defaultOptions[routeMethod] };
344
416
  const { schema: routeSpecs, components = {} } = await getSpec(
345
417
  middlewareHandler,
346
- defaultOptionsForThisMethod
418
+ defaultOptionsForThisMethod,
419
+ ctx.components.parameters
347
420
  );
348
421
  ctx.components = mergeComponentsObjects(ctx.components, components);
349
- registerSchemaPath({
350
- route,
351
- specs: routeSpecs,
352
- paths
353
- });
422
+ setSpecComponents(routeSpecs, [components]);
423
+ registerSchemaPath(
424
+ {
425
+ route,
426
+ specs: routeSpecs,
427
+ paths
428
+ },
429
+ pathContext
430
+ );
354
431
  }
355
432
  return paths;
356
433
  }
@@ -366,7 +443,7 @@ function getHiddenValue(options) {
366
443
  }
367
444
  return false;
368
445
  }
369
- async function getSpec(middlewareHandler, defaultOptions) {
446
+ async function getSpec(middlewareHandler, defaultOptions, parameterComponents) {
370
447
  if ("spec" in middlewareHandler) {
371
448
  const tmp = {
372
449
  ...defaultOptions,
@@ -401,7 +478,7 @@ async function getSpec(middlewareHandler, defaultOptions) {
401
478
  [VALIDATION_MARKER]: true
402
479
  };
403
480
  if (middlewareHandler.target === "form" || middlewareHandler.target === "json") {
404
- const media = middlewareHandler.options?.media ?? middlewareHandler.target === "json" ? "application/json" : "multipart/form-data";
481
+ const media = middlewareHandler.options?.media ?? (middlewareHandler.target === "json" ? "application/json" : "multipart/form-data");
405
482
  if (!docs.requestBody || !("content" in docs.requestBody) || !docs.requestBody.content) {
406
483
  docs.requestBody = {
407
484
  required: true,
@@ -423,18 +500,21 @@ async function getSpec(middlewareHandler, defaultOptions) {
423
500
  const pos = ref.split("/").pop();
424
501
  if (pos && result.components?.schemas?.[pos]) {
425
502
  const schema = result.components.schemas[pos];
426
- const newParameters = generateParameters(
503
+ const generatedParameters = generateParameters(
427
504
  middlewareHandler.target,
428
- schema
429
- )[0];
430
- if (!result.components.parameters) {
431
- result.components.parameters = {};
505
+ schema,
506
+ result.components.schemas
507
+ );
508
+ const singleParameter = generatedParameters.length === 1 ? generatedParameters[0] : void 0;
509
+ const existingParameter = parameterComponents?.[pos];
510
+ if (singleParameter && (!existingParameter || "in" in existingParameter && existingParameter.in === singleParameter.in && existingParameter.name === singleParameter.name)) {
511
+ result.components.parameters ??= {};
512
+ result.components.parameters[pos] = singleParameter;
513
+ parameters.push({ $ref: `#/components/parameters/${pos}` });
514
+ delete result.components.schemas[pos];
515
+ } else {
516
+ parameters = generatedParameters;
432
517
  }
433
- result.components.parameters[pos] = newParameters;
434
- delete result.components.schemas[pos];
435
- parameters.push({
436
- $ref: `#/components/parameters/${pos}`
437
- });
438
518
  }
439
519
  } else {
440
520
  parameters = generateParameters(
@@ -688,4 +768,4 @@ function describeResponse(handler, responses, options) {
688
768
  });
689
769
  }
690
770
 
691
- export { ALLOWED_METHODS, VALIDATION_MARKER, clearSpecsContext, describeResponse, describeRoute, generateSpecs, loadVendor, openAPIRouteHandler, registerSchemaPath, removeExcludedPaths, resolver, uniqueSymbol, validator };
771
+ export { ALLOWED_METHODS, VALIDATION_MARKER, clearSpecsContext, describeResponse, describeRoute, documentedComponents, generateSpecs, getSpecComponents, loadVendor, openAPIRouteHandler, registerSchemaPath, removeExcludedPaths, resolver, setSpecComponents, uniqueSymbol, validator };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "hono-openapi",
3
3
  "description": "OpenAPI schema generator for Hono",
4
- "version": "1.3.2",
4
+ "version": "1.3.4",
5
5
  "type": "module",
6
6
  "main": "dist/index.cjs",
7
7
  "module": "dist/index.js",