@voyant-travel/hono 0.118.4 → 0.120.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/dist/app.d.ts +22 -0
- package/dist/app.d.ts.map +1 -1
- package/dist/app.js +34 -10
- package/dist/openapi.d.ts +93 -0
- package/dist/openapi.d.ts.map +1 -1
- package/dist/openapi.js +250 -0
- package/package.json +3 -3
package/dist/app.d.ts
CHANGED
|
@@ -17,6 +17,20 @@ export interface LazyMount {
|
|
|
17
17
|
prefix: string;
|
|
18
18
|
load: LazyRoutesLoader;
|
|
19
19
|
}
|
|
20
|
+
/**
|
|
21
|
+
* A single module route mount recorded at mount time so a build-time generator
|
|
22
|
+
* can produce one self-contained OpenAPI document per module (voyant#2733).
|
|
23
|
+
* Mirrors `ModuleMount` in `./openapi.ts` (structurally identical, declared here
|
|
24
|
+
* to keep the build-time-only openapi module out of the runtime path). `load`
|
|
25
|
+
* returns the sub-app without serving it — eager mounts wrap the already-built
|
|
26
|
+
* routes as `() => routes`; lazy mounts pass their loader.
|
|
27
|
+
*/
|
|
28
|
+
export interface ModuleMount {
|
|
29
|
+
moduleName: string;
|
|
30
|
+
/** Absolute surface mount prefix, or `"/"` for absolute `lazyRoutes`. */
|
|
31
|
+
prefix: string;
|
|
32
|
+
load: () => Hono<any, any, any> | Promise<Hono<any, any, any>>;
|
|
33
|
+
}
|
|
20
34
|
/**
|
|
21
35
|
* App handle returned alongside the Hono instance. Carries `ready()` for
|
|
22
36
|
* headless / sibling-process deployments that need to fire the lazy
|
|
@@ -59,6 +73,14 @@ export interface VoyantAppExtensions<TBindings = unknown> {
|
|
|
59
73
|
* `.openapi()` operations via `mergeLazyOpenApiPaths`. Never read at runtime.
|
|
60
74
|
*/
|
|
61
75
|
lazyMounts: LazyMount[];
|
|
76
|
+
/**
|
|
77
|
+
* Every module route mount (admin + public, eager + lazy), tagged with its
|
|
78
|
+
* owning module name (voyant#2733). A build-time generator reads this to
|
|
79
|
+
* produce one self-contained OpenAPI document per module via
|
|
80
|
+
* `generateModuleOpenApiDocuments`, keeping the module boundary authoritative
|
|
81
|
+
* rather than guessed from path prefixes. Never read at runtime.
|
|
82
|
+
*/
|
|
83
|
+
moduleMounts: ModuleMount[];
|
|
62
84
|
}
|
|
63
85
|
/**
|
|
64
86
|
* Low-level app factory: given an already-resolved `modules`/`extensions` set
|
package/dist/app.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"app.d.ts","sourceRoot":"","sources":["../src/app.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,MAAM,CAAA;AAQhC,OAAO,EAAE,KAAK,gBAAgB,EAA0C,MAAM,kBAAkB,CAAA;AA+BhG,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAY,eAAe,EAAE,MAAM,YAAY,CAAA;AAI5F,4EAA4E;AAC5E,KAAK,QAAQ,CAAC,SAAS,SAAS,cAAc,IAAI;IAChD,QAAQ,EAAE,SAAS,CAAA;IACnB,SAAS,EAAE,eAAe,CAAA;CAC3B,CAAA;AAuBD;;;;;GAKG;AACH,MAAM,WAAW,SAAS;IACxB,yEAAyE;IACzE,MAAM,EAAE,MAAM,CAAA;IACd,IAAI,EAAE,gBAAgB,CAAA;CACvB;AAED;;;;;;;;;;GAUG;AACH,MAAM,WAAW,mBAAmB,CAAC,SAAS,GAAG,OAAO;IACtD;;;;;;;;;;;OAWG;IACH,KAAK,CAAC,QAAQ,CAAC,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAC1C;;;;;;;;OAQG;IACH,QAAQ,EAAE,OAAO,qBAAqB,EAAE,QAAQ,CAAA;IAChD;;;;;OAKG;IACH,UAAU,EAAE,SAAS,EAAE,CAAA;
|
|
1
|
+
{"version":3,"file":"app.d.ts","sourceRoot":"","sources":["../src/app.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,MAAM,CAAA;AAQhC,OAAO,EAAE,KAAK,gBAAgB,EAA0C,MAAM,kBAAkB,CAAA;AA+BhG,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAY,eAAe,EAAE,MAAM,YAAY,CAAA;AAI5F,4EAA4E;AAC5E,KAAK,QAAQ,CAAC,SAAS,SAAS,cAAc,IAAI;IAChD,QAAQ,EAAE,SAAS,CAAA;IACnB,SAAS,EAAE,eAAe,CAAA;CAC3B,CAAA;AAuBD;;;;;GAKG;AACH,MAAM,WAAW,SAAS;IACxB,yEAAyE;IACzE,MAAM,EAAE,MAAM,CAAA;IACd,IAAI,EAAE,gBAAgB,CAAA;CACvB;AAED;;;;;;;GAOG;AACH,MAAM,WAAW,WAAW;IAC1B,UAAU,EAAE,MAAM,CAAA;IAClB,yEAAyE;IACzE,MAAM,EAAE,MAAM,CAAA;IAEd,IAAI,EAAE,MAAM,IAAI,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC,CAAC,CAAA;CAC/D;AAED;;;;;;;;;;GAUG;AACH,MAAM,WAAW,mBAAmB,CAAC,SAAS,GAAG,OAAO;IACtD;;;;;;;;;;;OAWG;IACH,KAAK,CAAC,QAAQ,CAAC,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAC1C;;;;;;;;OAQG;IACH,QAAQ,EAAE,OAAO,qBAAqB,EAAE,QAAQ,CAAA;IAChD;;;;;OAKG;IACH,UAAU,EAAE,SAAS,EAAE,CAAA;IACvB;;;;;;OAMG;IACH,YAAY,EAAE,WAAW,EAAE,CAAA;CAC5B;AAED;;;;;;GAMG;AACH,wBAAgB,QAAQ,CAAC,SAAS,SAAS,cAAc,EACvD,MAAM,EAAE,eAAe,CAAC,SAAS,CAAC,GACjC,IAAI,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC,GAAG,mBAAmB,CAAC,SAAS,CAAC,CA+f5D"}
|
package/dist/app.js
CHANGED
|
@@ -410,56 +410,79 @@ export function mountApp(config) {
|
|
|
410
410
|
// them so the prefix logic is single-sourced here. Cheap array pushes — no
|
|
411
411
|
// eager `import()` and no runtime read.
|
|
412
412
|
const lazyMounts = [];
|
|
413
|
+
// Per-module OpenAPI generation manifest (voyant#2733). Recorded alongside the
|
|
414
|
+
// mount so the module name + real prefix are single-sourced here; `publicPath`
|
|
415
|
+
// overrides are captured with their actual mount, not re-derived. Eager routes
|
|
416
|
+
// are wrapped as `() => routes` so the generator's loader-based path is
|
|
417
|
+
// uniform across eager + lazy. Webhook routes are intentionally excluded — the
|
|
418
|
+
// per-module docs cover the admin/storefront surfaces only.
|
|
419
|
+
const moduleMounts = [];
|
|
413
420
|
// Mount module routes
|
|
414
421
|
for (const mod of allModules) {
|
|
415
|
-
const
|
|
416
|
-
const
|
|
422
|
+
const moduleName = mod.module.name;
|
|
423
|
+
const adminPrefix = `/v1/admin/${moduleName}`;
|
|
424
|
+
const publicPrefix = resolveSurfaceMountPath("/v1/public", mod.publicPath, moduleName);
|
|
417
425
|
if (mod.adminRoutes) {
|
|
418
|
-
|
|
426
|
+
const adminRoutes = mod.adminRoutes;
|
|
427
|
+
app.route(adminPrefix, adminRoutes);
|
|
428
|
+
moduleMounts.push({ moduleName, prefix: adminPrefix, load: () => adminRoutes });
|
|
419
429
|
}
|
|
420
430
|
if (mod.publicRoutes) {
|
|
421
|
-
|
|
431
|
+
const publicRoutes = mod.publicRoutes;
|
|
432
|
+
app.route(publicPrefix, publicRoutes);
|
|
433
|
+
moduleMounts.push({ moduleName, prefix: publicPrefix, load: () => publicRoutes });
|
|
422
434
|
}
|
|
423
435
|
if (mod.lazyAdminRoutes) {
|
|
424
436
|
mountLazyRoutesAt(app, adminPrefix, mod.lazyAdminRoutes);
|
|
425
437
|
lazyMounts.push({ prefix: adminPrefix, load: mod.lazyAdminRoutes });
|
|
438
|
+
moduleMounts.push({ moduleName, prefix: adminPrefix, load: mod.lazyAdminRoutes });
|
|
426
439
|
}
|
|
427
440
|
if (mod.lazyPublicRoutes) {
|
|
428
441
|
mountLazyRoutesAt(app, publicPrefix, mod.lazyPublicRoutes);
|
|
429
442
|
lazyMounts.push({ prefix: publicPrefix, load: mod.lazyPublicRoutes });
|
|
443
|
+
moduleMounts.push({ moduleName, prefix: publicPrefix, load: mod.lazyPublicRoutes });
|
|
430
444
|
}
|
|
431
445
|
if (mod.lazyRoutes) {
|
|
432
446
|
mountLazyRoutePaths(app, mod.lazyRoutes.paths, mod.lazyRoutes.load);
|
|
433
447
|
lazyMounts.push({ prefix: "/", load: mod.lazyRoutes.load });
|
|
448
|
+
moduleMounts.push({ moduleName, prefix: "/", load: mod.lazyRoutes.load });
|
|
434
449
|
}
|
|
435
450
|
if (mod.webhookRoutes) {
|
|
436
|
-
app.route(`/v1/${
|
|
451
|
+
app.route(`/v1/${moduleName}`, mod.webhookRoutes);
|
|
437
452
|
}
|
|
438
453
|
}
|
|
439
454
|
// Mount extension routes
|
|
440
455
|
for (const ext of allExtensions) {
|
|
441
|
-
const
|
|
442
|
-
const
|
|
456
|
+
const moduleName = ext.extension.module;
|
|
457
|
+
const adminPrefix = `/v1/admin/${moduleName}`;
|
|
458
|
+
const publicPrefix = resolveSurfaceMountPath("/v1/public", ext.publicPath, moduleName);
|
|
443
459
|
if (ext.adminRoutes) {
|
|
444
|
-
|
|
460
|
+
const adminRoutes = ext.adminRoutes;
|
|
461
|
+
app.route(adminPrefix, adminRoutes);
|
|
462
|
+
moduleMounts.push({ moduleName, prefix: adminPrefix, load: () => adminRoutes });
|
|
445
463
|
}
|
|
446
464
|
if (ext.publicRoutes) {
|
|
447
|
-
|
|
465
|
+
const publicRoutes = ext.publicRoutes;
|
|
466
|
+
app.route(publicPrefix, publicRoutes);
|
|
467
|
+
moduleMounts.push({ moduleName, prefix: publicPrefix, load: () => publicRoutes });
|
|
448
468
|
}
|
|
449
469
|
if (ext.lazyAdminRoutes) {
|
|
450
470
|
mountLazyRoutesAt(app, adminPrefix, ext.lazyAdminRoutes);
|
|
451
471
|
lazyMounts.push({ prefix: adminPrefix, load: ext.lazyAdminRoutes });
|
|
472
|
+
moduleMounts.push({ moduleName, prefix: adminPrefix, load: ext.lazyAdminRoutes });
|
|
452
473
|
}
|
|
453
474
|
if (ext.lazyPublicRoutes) {
|
|
454
475
|
mountLazyRoutesAt(app, publicPrefix, ext.lazyPublicRoutes);
|
|
455
476
|
lazyMounts.push({ prefix: publicPrefix, load: ext.lazyPublicRoutes });
|
|
477
|
+
moduleMounts.push({ moduleName, prefix: publicPrefix, load: ext.lazyPublicRoutes });
|
|
456
478
|
}
|
|
457
479
|
if (ext.lazyRoutes) {
|
|
458
480
|
mountLazyRoutePaths(app, ext.lazyRoutes.paths, ext.lazyRoutes.load);
|
|
459
481
|
lazyMounts.push({ prefix: "/", load: ext.lazyRoutes.load });
|
|
482
|
+
moduleMounts.push({ moduleName, prefix: "/", load: ext.lazyRoutes.load });
|
|
460
483
|
}
|
|
461
484
|
if (ext.webhookRoutes) {
|
|
462
|
-
app.route(`/v1/${
|
|
485
|
+
app.route(`/v1/${moduleName}`, ext.webhookRoutes);
|
|
463
486
|
}
|
|
464
487
|
}
|
|
465
488
|
// Additional routes
|
|
@@ -477,6 +500,7 @@ export function mountApp(config) {
|
|
|
477
500
|
const augmented = app;
|
|
478
501
|
augmented.eventBus = eventBus;
|
|
479
502
|
augmented.lazyMounts = lazyMounts;
|
|
503
|
+
augmented.moduleMounts = moduleMounts;
|
|
480
504
|
augmented.ready = (bindings) => ensureRuntimeBootstrapped(bindings ?? {});
|
|
481
505
|
return augmented;
|
|
482
506
|
}
|
package/dist/openapi.d.ts
CHANGED
|
@@ -66,6 +66,23 @@ export interface LazyMount {
|
|
|
66
66
|
prefix: string;
|
|
67
67
|
load: LazyRoutesLoader;
|
|
68
68
|
}
|
|
69
|
+
/**
|
|
70
|
+
* A single module route mount recorded by `mountApp` for build-time per-module
|
|
71
|
+
* spec generation (voyant#2733). One module contributes several mounts (admin +
|
|
72
|
+
* public, eager + lazy), all tagged with the same `moduleName` — so the module
|
|
73
|
+
* boundary is the authoritative one from the registration, not a path-prefix
|
|
74
|
+
* guess. `prefix` is the real absolute mount (base path included), so the
|
|
75
|
+
* generated per-module doc carries correct absolute paths — including
|
|
76
|
+
* `publicPath` overrides whose prefix isn't the module name (e.g. a module that
|
|
77
|
+
* mounts its storefront routes under `/v1/public/booking-engine`). `load`
|
|
78
|
+
* returns the sub-app without serving it (eager mounts wrap the already-built
|
|
79
|
+
* app as `() => routes`; lazy mounts pass their loader).
|
|
80
|
+
*/
|
|
81
|
+
export interface ModuleMount {
|
|
82
|
+
moduleName: string;
|
|
83
|
+
prefix: string;
|
|
84
|
+
load: () => AnyApp | Promise<AnyApp>;
|
|
85
|
+
}
|
|
69
86
|
/**
|
|
70
87
|
* Eager-merge lazy route families into a generated base document (voyant#2114).
|
|
71
88
|
*
|
|
@@ -86,5 +103,81 @@ export interface LazyMount {
|
|
|
86
103
|
* (`@asteasolutions/zod-to-openapi` stays out of the Worker bundle).
|
|
87
104
|
*/
|
|
88
105
|
export declare function mergeLazyOpenApiPaths(base: OpenApiDocument, mounts: readonly LazyMount[], options: GenerateOpenApiOptions): Promise<OpenApiDocument>;
|
|
106
|
+
/**
|
|
107
|
+
* Generate one self-contained OpenAPI document per module from its recorded
|
|
108
|
+
* mounts (voyant#2733).
|
|
109
|
+
*
|
|
110
|
+
* Instead of building one giant composed document and splitting it by path
|
|
111
|
+
* prefix, this generates each module's spec directly from the routes the module
|
|
112
|
+
* registered — the authoritative module boundary. Each module's admin + public
|
|
113
|
+
* (+ lazy) sub-apps are re-mounted into a throwaway `OpenAPIHono` at their real
|
|
114
|
+
* absolute prefix (reusing the exact prefix-merge `OpenAPIHono.route(...)`
|
|
115
|
+
* applies), then its `getOpenAPI31Document()` is read. The returned docs are
|
|
116
|
+
* self-contained (each carries its own referenced components), so they render
|
|
117
|
+
* and diff cleanly on their own — unlike a 7 MB aggregate.
|
|
118
|
+
*
|
|
119
|
+
* A mount whose loader throws, or that returns a plain `Hono` with no
|
|
120
|
+
* `.openapi()` registry, is skipped without failing the module. Modules that
|
|
121
|
+
* contribute no documented operation are omitted from the result entirely.
|
|
122
|
+
*
|
|
123
|
+
* Build-time only — same constraint as the rest of this module
|
|
124
|
+
* (`@asteasolutions/zod-to-openapi` must stay out of the Worker bundle).
|
|
125
|
+
*/
|
|
126
|
+
export declare function generateModuleOpenApiDocuments(mounts: readonly ModuleMount[], options: GenerateOpenApiOptions): Promise<Map<string, OpenApiDocument>>;
|
|
127
|
+
/**
|
|
128
|
+
* Build the authoritative path → module ownership map from the mount manifest.
|
|
129
|
+
*
|
|
130
|
+
* Generates each module's isolated doc (see `generateModuleOpenApiDocuments`)
|
|
131
|
+
* only to learn which real absolute paths it owns — so `publicPath` overrides
|
|
132
|
+
* (whose prefix isn't the module name, e.g. `/v1/public/booking-engine`) map to
|
|
133
|
+
* the right module. Routes the manifest doesn't record (`additionalRoutes`,
|
|
134
|
+
* directly-mounted routes) are absent here and fall back to their path segment.
|
|
135
|
+
*
|
|
136
|
+
* Build-time only.
|
|
137
|
+
*/
|
|
138
|
+
export declare function buildModulePathOwnership(mounts: readonly ModuleMount[], options: GenerateOpenApiOptions): Promise<Map<string, string>>;
|
|
139
|
+
/**
|
|
140
|
+
* Partition a composed document into one document per module, covering EVERY
|
|
141
|
+
* admin/storefront path (voyant#2733).
|
|
142
|
+
*
|
|
143
|
+
* Uses the ownership map as the authoritative module owner, falling back to the
|
|
144
|
+
* path's own segment for anything the manifest doesn't claim (e.g.
|
|
145
|
+
* `additionalRoutes` mounts like the operator's workflow-runs admin surface).
|
|
146
|
+
* The full surface is therefore partitioned exactly: every `/v1/admin/*` and
|
|
147
|
+
* `/v1/public/*` path lands in exactly one module document. Non-surface routes
|
|
148
|
+
* (`/v1/<name>` webhooks, legacy `/v1/*`) live only in the aggregate, as before.
|
|
149
|
+
*
|
|
150
|
+
* Each per-module document carries the aggregate's shared `components` verbatim
|
|
151
|
+
* (there is ~one), so it stays a valid, self-contained OpenAPI document.
|
|
152
|
+
*/
|
|
153
|
+
export declare function partitionByModule(full: OpenApiDocument, owner: ReadonlyMap<string, string>): Map<string, OpenApiDocument>;
|
|
154
|
+
/**
|
|
155
|
+
* Convenience: build the ownership map and partition in one call. Prefer the
|
|
156
|
+
* two-step `buildModulePathOwnership` + `partitionByModule` when you also want
|
|
157
|
+
* to `stampModuleMetadata` the aggregate from the same map (so it's built once).
|
|
158
|
+
*
|
|
159
|
+
* Build-time only.
|
|
160
|
+
*/
|
|
161
|
+
export declare function splitDocumentByModule(full: OpenApiDocument, mounts: readonly ModuleMount[], options: GenerateOpenApiOptions): Promise<Map<string, OpenApiDocument>>;
|
|
162
|
+
/**
|
|
163
|
+
* Stamp every operation with the metadata standard OpenAPI tooling expects
|
|
164
|
+
* (voyant#2733 / voyant#2729). All fields are non-destructive — a value a route
|
|
165
|
+
* already declares is never overwritten:
|
|
166
|
+
* - `operationId` — stable camelCase id from method + path, for readable
|
|
167
|
+
* generated client method names.
|
|
168
|
+
* - `summary` — the method + path signature, so viewers/linters have a title
|
|
169
|
+
* for every operation.
|
|
170
|
+
* - `tags: [module]` — so Swagger/Scalar group the sidebar by module (they
|
|
171
|
+
* key grouping off `tags` and ignore `x-*`).
|
|
172
|
+
* - `x-voyant-module` / `x-voyant-surface` — machine-readable owner + surface
|
|
173
|
+
* for custom tooling that shouldn't re-derive them from path prefixes.
|
|
174
|
+
*
|
|
175
|
+
* The module is the authoritative owner from the manifest, so `publicPath`
|
|
176
|
+
* overrides are labelled with their real owning module rather than their mount
|
|
177
|
+
* prefix. Applied to the aggregate before it's split, so the per-module and
|
|
178
|
+
* surface documents (all derived from it) inherit the stamps. Keys are appended,
|
|
179
|
+
* keeping order deterministic for the drift gate.
|
|
180
|
+
*/
|
|
181
|
+
export declare function stampModuleMetadata(doc: OpenApiDocument, owner: ReadonlyMap<string, string>): OpenApiDocument;
|
|
89
182
|
export {};
|
|
90
183
|
//# sourceMappingURL=openapi.d.ts.map
|
package/dist/openapi.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"openapi.d.ts","sourceRoot":"","sources":["../src/openapi.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AAC/C,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,MAAM,CAAA;AAEhC,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAA;AAExD;;;;;;;;;;;;GAYG;AAEH,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,MAAM,CAAA;IACb,OAAO,EAAE,MAAM,CAAA;IACf,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,8EAA8E;IAC9E,CAAC,SAAS,EAAE,KAAK,MAAM,EAAE,GAAG,OAAO,CAAA;CACpC;AAED,MAAM,WAAW,aAAa;IAC5B,GAAG,EAAE,MAAM,CAAA;IACX,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,CAAC,SAAS,EAAE,KAAK,MAAM,EAAE,GAAG,OAAO,CAAA;CACpC;AAED,MAAM,WAAW,sBAAsB;IACrC,IAAI,EAAE,WAAW,CAAA;IACjB,OAAO,CAAC,EAAE,aAAa,EAAE,CAAA;CAC1B;AAGD,KAAK,MAAM,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC,CAAA;AAEjC;;;GAGG;AACH,MAAM,MAAM,eAAe,GAAG,UAAU,CAAC,WAAW,CAAC,sBAAsB,CAAC,CAAC,CAAA;AAE7E;;;;GAIG;AACH,wBAAgB,uBAAuB,CACrC,GAAG,EAAE,MAAM,EACX,OAAO,EAAE,sBAAsB,GAC9B,eAAe,CASjB;AAED;;;;GAIG;AACH,MAAM,MAAM,UAAU,GAAG,OAAO,GAAG,YAAY,CAAA;AAO/C;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,eAAe,EAAE,OAAO,EAAE,UAAU,GAAG,eAAe,CAMxF;AAED;;;;;;GAMG;AACH,MAAM,WAAW,SAAS;IACxB,MAAM,EAAE,MAAM,CAAA;IACd,IAAI,EAAE,gBAAgB,CAAA;CACvB;AAYD;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAsB,qBAAqB,CACzC,IAAI,EAAE,eAAe,EACrB,MAAM,EAAE,SAAS,SAAS,EAAE,EAC5B,OAAO,EAAE,sBAAsB,GAC9B,OAAO,CAAC,eAAe,CAAC,CA8D1B"}
|
|
1
|
+
{"version":3,"file":"openapi.d.ts","sourceRoot":"","sources":["../src/openapi.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AAC/C,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,MAAM,CAAA;AAEhC,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAA;AAExD;;;;;;;;;;;;GAYG;AAEH,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,MAAM,CAAA;IACb,OAAO,EAAE,MAAM,CAAA;IACf,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,8EAA8E;IAC9E,CAAC,SAAS,EAAE,KAAK,MAAM,EAAE,GAAG,OAAO,CAAA;CACpC;AAED,MAAM,WAAW,aAAa;IAC5B,GAAG,EAAE,MAAM,CAAA;IACX,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,CAAC,SAAS,EAAE,KAAK,MAAM,EAAE,GAAG,OAAO,CAAA;CACpC;AAED,MAAM,WAAW,sBAAsB;IACrC,IAAI,EAAE,WAAW,CAAA;IACjB,OAAO,CAAC,EAAE,aAAa,EAAE,CAAA;CAC1B;AAGD,KAAK,MAAM,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC,CAAA;AAEjC;;;GAGG;AACH,MAAM,MAAM,eAAe,GAAG,UAAU,CAAC,WAAW,CAAC,sBAAsB,CAAC,CAAC,CAAA;AAE7E;;;;GAIG;AACH,wBAAgB,uBAAuB,CACrC,GAAG,EAAE,MAAM,EACX,OAAO,EAAE,sBAAsB,GAC9B,eAAe,CASjB;AAED;;;;GAIG;AACH,MAAM,MAAM,UAAU,GAAG,OAAO,GAAG,YAAY,CAAA;AAO/C;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,eAAe,EAAE,OAAO,EAAE,UAAU,GAAG,eAAe,CAMxF;AAED;;;;;;GAMG;AACH,MAAM,WAAW,SAAS;IACxB,MAAM,EAAE,MAAM,CAAA;IACd,IAAI,EAAE,gBAAgB,CAAA;CACvB;AAED;;;;;;;;;;;GAWG;AACH,MAAM,WAAW,WAAW;IAC1B,UAAU,EAAE,MAAM,CAAA;IAClB,MAAM,EAAE,MAAM,CAAA;IACd,IAAI,EAAE,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAA;CACrC;AAYD;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAsB,qBAAqB,CACzC,IAAI,EAAE,eAAe,EACrB,MAAM,EAAE,SAAS,SAAS,EAAE,EAC5B,OAAO,EAAE,sBAAsB,GAC9B,OAAO,CAAC,eAAe,CAAC,CA8D1B;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAsB,8BAA8B,CAClD,MAAM,EAAE,SAAS,WAAW,EAAE,EAC9B,OAAO,EAAE,sBAAsB,GAC9B,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,eAAe,CAAC,CAAC,CAsCvC;AAuBD;;;;;;;;;;GAUG;AACH,wBAAsB,wBAAwB,CAC5C,MAAM,EAAE,SAAS,WAAW,EAAE,EAC9B,OAAO,EAAE,sBAAsB,GAC9B,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAO9B;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,iBAAiB,CAC/B,IAAI,EAAE,eAAe,EACrB,KAAK,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,GACjC,GAAG,CAAC,MAAM,EAAE,eAAe,CAAC,CAsB9B;AAED;;;;;;GAMG;AACH,wBAAsB,qBAAqB,CACzC,IAAI,EAAE,eAAe,EACrB,MAAM,EAAE,SAAS,WAAW,EAAE,EAC9B,OAAO,EAAE,sBAAsB,GAC9B,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,eAAe,CAAC,CAAC,CAEvC;AAyCD;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,mBAAmB,CACjC,GAAG,EAAE,eAAe,EACpB,KAAK,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,GACjC,eAAe,CAgDjB"}
|
package/dist/openapi.js
CHANGED
|
@@ -112,3 +112,253 @@ export async function mergeLazyOpenApiPaths(base, mounts, options) {
|
|
|
112
112
|
}
|
|
113
113
|
return merged;
|
|
114
114
|
}
|
|
115
|
+
/**
|
|
116
|
+
* Generate one self-contained OpenAPI document per module from its recorded
|
|
117
|
+
* mounts (voyant#2733).
|
|
118
|
+
*
|
|
119
|
+
* Instead of building one giant composed document and splitting it by path
|
|
120
|
+
* prefix, this generates each module's spec directly from the routes the module
|
|
121
|
+
* registered — the authoritative module boundary. Each module's admin + public
|
|
122
|
+
* (+ lazy) sub-apps are re-mounted into a throwaway `OpenAPIHono` at their real
|
|
123
|
+
* absolute prefix (reusing the exact prefix-merge `OpenAPIHono.route(...)`
|
|
124
|
+
* applies), then its `getOpenAPI31Document()` is read. The returned docs are
|
|
125
|
+
* self-contained (each carries its own referenced components), so they render
|
|
126
|
+
* and diff cleanly on their own — unlike a 7 MB aggregate.
|
|
127
|
+
*
|
|
128
|
+
* A mount whose loader throws, or that returns a plain `Hono` with no
|
|
129
|
+
* `.openapi()` registry, is skipped without failing the module. Modules that
|
|
130
|
+
* contribute no documented operation are omitted from the result entirely.
|
|
131
|
+
*
|
|
132
|
+
* Build-time only — same constraint as the rest of this module
|
|
133
|
+
* (`@asteasolutions/zod-to-openapi` must stay out of the Worker bundle).
|
|
134
|
+
*/
|
|
135
|
+
export async function generateModuleOpenApiDocuments(mounts, options) {
|
|
136
|
+
const byModule = new Map();
|
|
137
|
+
for (const mount of mounts) {
|
|
138
|
+
const list = byModule.get(mount.moduleName);
|
|
139
|
+
if (list)
|
|
140
|
+
list.push(mount);
|
|
141
|
+
else
|
|
142
|
+
byModule.set(mount.moduleName, [mount]);
|
|
143
|
+
}
|
|
144
|
+
const result = new Map();
|
|
145
|
+
for (const [moduleName, moduleMounts] of byModule) {
|
|
146
|
+
const throwaway = new OpenAPIHono();
|
|
147
|
+
let mountedAny = false;
|
|
148
|
+
for (const { prefix, load } of moduleMounts) {
|
|
149
|
+
let subApp;
|
|
150
|
+
try {
|
|
151
|
+
subApp = await load();
|
|
152
|
+
}
|
|
153
|
+
catch (error) {
|
|
154
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
155
|
+
console.warn(`[voyant] openapi: module "${moduleName}" mount at "${prefix}" failed to load, skipping: ${message}`);
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
if (!isOpenApiHono(subApp))
|
|
159
|
+
continue;
|
|
160
|
+
throwaway.route(prefix === "/" ? "/" : prefix, subApp);
|
|
161
|
+
mountedAny = true;
|
|
162
|
+
}
|
|
163
|
+
if (!mountedAny)
|
|
164
|
+
continue;
|
|
165
|
+
const doc = throwaway.getOpenAPI31Document({
|
|
166
|
+
openapi: "3.1.0",
|
|
167
|
+
info: options.info,
|
|
168
|
+
...(options.servers ? { servers: options.servers } : {}),
|
|
169
|
+
});
|
|
170
|
+
if (!doc.paths || Object.keys(doc.paths).length === 0)
|
|
171
|
+
continue;
|
|
172
|
+
result.set(moduleName, doc);
|
|
173
|
+
}
|
|
174
|
+
return result;
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* The module a path belongs to, given the authoritative owner map: the manifest
|
|
178
|
+
* owner if known (which is what keeps `publicPath` overrides correct), else the
|
|
179
|
+
* path's own module segment — `<seg>` in `/v1/admin/<seg>/...`,
|
|
180
|
+
* `/v1/public/<seg>/...`, or `/v1/<seg>/...` (webhooks/legacy).
|
|
181
|
+
*/
|
|
182
|
+
function moduleNameForPath(path, owner) {
|
|
183
|
+
const known = owner.get(path);
|
|
184
|
+
if (known)
|
|
185
|
+
return known;
|
|
186
|
+
const parts = path.split("/").filter(Boolean); // ["v1","admin","bookings",...]
|
|
187
|
+
const seg = parts[1] === "admin" || parts[1] === "public" ? parts[2] : parts[1];
|
|
188
|
+
return seg ?? "misc";
|
|
189
|
+
}
|
|
190
|
+
/** The API surface a path is served on, or `null` for non-surface routes. */
|
|
191
|
+
function surfaceForPath(path) {
|
|
192
|
+
if (path.startsWith("/v1/admin/"))
|
|
193
|
+
return "admin";
|
|
194
|
+
if (path.startsWith("/v1/public/"))
|
|
195
|
+
return "storefront";
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Build the authoritative path → module ownership map from the mount manifest.
|
|
200
|
+
*
|
|
201
|
+
* Generates each module's isolated doc (see `generateModuleOpenApiDocuments`)
|
|
202
|
+
* only to learn which real absolute paths it owns — so `publicPath` overrides
|
|
203
|
+
* (whose prefix isn't the module name, e.g. `/v1/public/booking-engine`) map to
|
|
204
|
+
* the right module. Routes the manifest doesn't record (`additionalRoutes`,
|
|
205
|
+
* directly-mounted routes) are absent here and fall back to their path segment.
|
|
206
|
+
*
|
|
207
|
+
* Build-time only.
|
|
208
|
+
*/
|
|
209
|
+
export async function buildModulePathOwnership(mounts, options) {
|
|
210
|
+
const moduleDocs = await generateModuleOpenApiDocuments(mounts, options);
|
|
211
|
+
const owner = new Map();
|
|
212
|
+
for (const [moduleName, doc] of moduleDocs) {
|
|
213
|
+
for (const path of Object.keys(doc.paths ?? {}))
|
|
214
|
+
owner.set(path, moduleName);
|
|
215
|
+
}
|
|
216
|
+
return owner;
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Partition a composed document into one document per module, covering EVERY
|
|
220
|
+
* admin/storefront path (voyant#2733).
|
|
221
|
+
*
|
|
222
|
+
* Uses the ownership map as the authoritative module owner, falling back to the
|
|
223
|
+
* path's own segment for anything the manifest doesn't claim (e.g.
|
|
224
|
+
* `additionalRoutes` mounts like the operator's workflow-runs admin surface).
|
|
225
|
+
* The full surface is therefore partitioned exactly: every `/v1/admin/*` and
|
|
226
|
+
* `/v1/public/*` path lands in exactly one module document. Non-surface routes
|
|
227
|
+
* (`/v1/<name>` webhooks, legacy `/v1/*`) live only in the aggregate, as before.
|
|
228
|
+
*
|
|
229
|
+
* Each per-module document carries the aggregate's shared `components` verbatim
|
|
230
|
+
* (there is ~one), so it stays a valid, self-contained OpenAPI document.
|
|
231
|
+
*/
|
|
232
|
+
export function partitionByModule(full, owner) {
|
|
233
|
+
const buckets = new Map();
|
|
234
|
+
for (const [path, item] of Object.entries(full.paths ?? {})) {
|
|
235
|
+
if (!path.startsWith("/v1/admin/") && !path.startsWith("/v1/public/"))
|
|
236
|
+
continue;
|
|
237
|
+
const moduleName = moduleNameForPath(path, owner);
|
|
238
|
+
let bucket = buckets.get(moduleName);
|
|
239
|
+
if (!bucket) {
|
|
240
|
+
bucket = {};
|
|
241
|
+
buckets.set(moduleName, bucket);
|
|
242
|
+
}
|
|
243
|
+
bucket[path] = item;
|
|
244
|
+
}
|
|
245
|
+
const result = new Map();
|
|
246
|
+
for (const [moduleName, paths] of buckets) {
|
|
247
|
+
result.set(moduleName, {
|
|
248
|
+
...full,
|
|
249
|
+
paths,
|
|
250
|
+
...(full.components ? { components: full.components } : {}),
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
return result;
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Convenience: build the ownership map and partition in one call. Prefer the
|
|
257
|
+
* two-step `buildModulePathOwnership` + `partitionByModule` when you also want
|
|
258
|
+
* to `stampModuleMetadata` the aggregate from the same map (so it's built once).
|
|
259
|
+
*
|
|
260
|
+
* Build-time only.
|
|
261
|
+
*/
|
|
262
|
+
export async function splitDocumentByModule(full, mounts, options) {
|
|
263
|
+
return partitionByModule(full, await buildModulePathOwnership(mounts, options));
|
|
264
|
+
}
|
|
265
|
+
const HTTP_METHODS = ["get", "put", "post", "delete", "options", "head", "patch", "trace"];
|
|
266
|
+
/** PascalCase a path segment, splitting on non-alphanumerics (kebab, etc.). */
|
|
267
|
+
function pascalCase(segment) {
|
|
268
|
+
return segment
|
|
269
|
+
.split(/[^a-zA-Z0-9]+/)
|
|
270
|
+
.filter(Boolean)
|
|
271
|
+
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
|
272
|
+
.join("");
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* Stable camelCase operationId derived from method + path (voyant#2729) — gives
|
|
276
|
+
* client generators readable, deterministic method names instead of guessing.
|
|
277
|
+
* Path params render as `ByX`, the `v1` prefix is dropped:
|
|
278
|
+
* `GET /v1/admin/bookings/{id}` → `getAdminBookingsById`. Method + path is unique
|
|
279
|
+
* per OpenAPI, so the derived id is too (a numeric suffix guards edge cases).
|
|
280
|
+
*/
|
|
281
|
+
function deriveOperationId(method, path) {
|
|
282
|
+
const parts = path
|
|
283
|
+
.split("/")
|
|
284
|
+
.filter(Boolean)
|
|
285
|
+
.filter((segment) => segment !== "v1")
|
|
286
|
+
.map((segment) => {
|
|
287
|
+
const param = /^\{(.+)\}$/.exec(segment);
|
|
288
|
+
return param?.[1] ? `By${pascalCase(param[1])}` : pascalCase(segment);
|
|
289
|
+
});
|
|
290
|
+
return `${method}${parts.join("")}`;
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* A readable, always-correct operation summary (voyant#2729): the method + path
|
|
294
|
+
* signature. Deliberately mechanical rather than guessed prose — a hand-authored
|
|
295
|
+
* per-route `summary` overrides it (forward-only).
|
|
296
|
+
*/
|
|
297
|
+
function deriveSummary(method, path) {
|
|
298
|
+
return `${method.toUpperCase()} ${path}`;
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* Stamp every operation with the metadata standard OpenAPI tooling expects
|
|
302
|
+
* (voyant#2733 / voyant#2729). All fields are non-destructive — a value a route
|
|
303
|
+
* already declares is never overwritten:
|
|
304
|
+
* - `operationId` — stable camelCase id from method + path, for readable
|
|
305
|
+
* generated client method names.
|
|
306
|
+
* - `summary` — the method + path signature, so viewers/linters have a title
|
|
307
|
+
* for every operation.
|
|
308
|
+
* - `tags: [module]` — so Swagger/Scalar group the sidebar by module (they
|
|
309
|
+
* key grouping off `tags` and ignore `x-*`).
|
|
310
|
+
* - `x-voyant-module` / `x-voyant-surface` — machine-readable owner + surface
|
|
311
|
+
* for custom tooling that shouldn't re-derive them from path prefixes.
|
|
312
|
+
*
|
|
313
|
+
* The module is the authoritative owner from the manifest, so `publicPath`
|
|
314
|
+
* overrides are labelled with their real owning module rather than their mount
|
|
315
|
+
* prefix. Applied to the aggregate before it's split, so the per-module and
|
|
316
|
+
* surface documents (all derived from it) inherit the stamps. Keys are appended,
|
|
317
|
+
* keeping order deterministic for the drift gate.
|
|
318
|
+
*/
|
|
319
|
+
export function stampModuleMetadata(doc, owner) {
|
|
320
|
+
const paths = {};
|
|
321
|
+
// operationId must be unique across the document; track what we've assigned
|
|
322
|
+
// (including route-declared ids) so a derived id never collides.
|
|
323
|
+
const usedOperationIds = new Set();
|
|
324
|
+
for (const [path, item] of Object.entries(doc.paths ?? {})) {
|
|
325
|
+
if (!item || typeof item !== "object") {
|
|
326
|
+
paths[path] = item;
|
|
327
|
+
continue;
|
|
328
|
+
}
|
|
329
|
+
const moduleName = moduleNameForPath(path, owner);
|
|
330
|
+
const surface = surfaceForPath(path);
|
|
331
|
+
const nextItem = { ...item };
|
|
332
|
+
for (const method of HTTP_METHODS) {
|
|
333
|
+
const op = nextItem[method];
|
|
334
|
+
if (!op || typeof op !== "object")
|
|
335
|
+
continue;
|
|
336
|
+
const operation = op;
|
|
337
|
+
const declaredId = typeof operation.operationId === "string" && operation.operationId.length > 0
|
|
338
|
+
? operation.operationId
|
|
339
|
+
: null;
|
|
340
|
+
let operationId = declaredId ?? deriveOperationId(method, path);
|
|
341
|
+
if (!declaredId) {
|
|
342
|
+
let suffix = 2;
|
|
343
|
+
while (usedOperationIds.has(operationId)) {
|
|
344
|
+
operationId = `${deriveOperationId(method, path)}_${suffix++}`;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
usedOperationIds.add(operationId);
|
|
348
|
+
const hasSummary = typeof operation.summary === "string" && operation.summary.length > 0;
|
|
349
|
+
// Swagger/Scalar group by `tags` (and ignore `x-*`) — without one a
|
|
350
|
+
// whole-surface document collapses under a single "default" group (#2733).
|
|
351
|
+
const hasTags = Array.isArray(operation.tags) && operation.tags.length > 0;
|
|
352
|
+
nextItem[method] = {
|
|
353
|
+
...operation,
|
|
354
|
+
operationId,
|
|
355
|
+
...(hasSummary ? {} : { summary: deriveSummary(method, path) }),
|
|
356
|
+
...(hasTags ? {} : { tags: [moduleName] }),
|
|
357
|
+
"x-voyant-module": moduleName,
|
|
358
|
+
...(surface ? { "x-voyant-surface": surface } : {}),
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
paths[path] = nextItem;
|
|
362
|
+
}
|
|
363
|
+
return { ...doc, paths };
|
|
364
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@voyant-travel/hono",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.120.0",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -140,14 +140,14 @@
|
|
|
140
140
|
"@voyant-travel/storage": "^0.106.0",
|
|
141
141
|
"@voyant-travel/types": "^0.106.1",
|
|
142
142
|
"@voyant-travel/utils": "^0.105.5",
|
|
143
|
-
"@voyant-travel/workflows": "^0.111.
|
|
143
|
+
"@voyant-travel/workflows": "^0.111.14"
|
|
144
144
|
},
|
|
145
145
|
"devDependencies": {
|
|
146
146
|
"@cloudflare/workers-types": "^4.20260628.1",
|
|
147
147
|
"typescript": "^6.0.3",
|
|
148
148
|
"vitest": "^4.1.9",
|
|
149
149
|
"@voyant-travel/voyant-typescript-config": "^0.1.0",
|
|
150
|
-
"@voyant-travel/workflows-orchestrator": "^0.111.
|
|
150
|
+
"@voyant-travel/workflows-orchestrator": "^0.111.14"
|
|
151
151
|
},
|
|
152
152
|
"files": [
|
|
153
153
|
"dist"
|