@websublime/vite-plugin-open-api-server 0.24.0-next.1 → 0.24.0-next.3

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.d.ts CHANGED
@@ -1,6 +1,8 @@
1
1
  import { Plugin, ViteDevServer } from 'vite';
2
- import { Logger, HandlerFn, AnySeedFn } from '@websublime/vite-plugin-open-api-core';
2
+ import { Logger, SpecInfo, OpenApiServer, HandlerFn, AnySeedFn } from '@websublime/vite-plugin-open-api-core';
3
3
  export { HandlerContext, HandlerDefinition, HandlerFn, HandlerReturn, SecurityContext, SeedContext, SeedDefinition, SeedFn, SeedHelper, defineHandlers, defineSeeds } from '@websublime/vite-plugin-open-api-core';
4
+ import { OpenAPIV3_1 } from '@scalar/openapi-types';
5
+ import { Hono } from 'hono';
4
6
  import { App } from 'vue';
5
7
 
6
8
  /**
@@ -42,6 +44,16 @@ declare class ValidationError extends Error {
42
44
  readonly code: ValidationErrorCode;
43
45
  constructor(code: ValidationErrorCode, message: string);
44
46
  }
47
+ /**
48
+ * How a proxy path was determined.
49
+ *
50
+ * - `'explicit'` — set directly in SpecConfig.proxyPath
51
+ * - `'auto'` — auto-derived from the OpenAPI document's servers[0].url
52
+ *
53
+ * Used by both DeriveProxyPathResult and ResolvedSpecConfig to ensure
54
+ * the two sources of this value stay in sync.
55
+ */
56
+ type ProxyPathSource = 'auto' | 'explicit';
45
57
  /**
46
58
  * Configuration for a single OpenAPI spec instance
47
59
  *
@@ -175,18 +187,18 @@ interface OpenApiServerOptions {
175
187
  */
176
188
  interface ResolvedSpecConfig {
177
189
  spec: string;
178
- /** Guaranteed to be set after orchestrator resolution */
190
+ /** Empty string until orchestrator resolution; guaranteed non-empty after `createOrchestrator()` */
179
191
  id: string;
180
- /** Guaranteed to be set after orchestrator resolution */
192
+ /** Empty string until orchestrator resolution; guaranteed non-empty after `createOrchestrator()` */
181
193
  proxyPath: string;
182
194
  /**
183
195
  * How proxyPath was determined — used for banner display.
184
196
  *
185
- * Set during static option resolution. The orchestrator (Task 1.7)
186
- * will pass this to the multi-spec banner so it can show
187
- * "(auto-derived)" vs "(explicit)" next to each proxy path.
197
+ * Written back by the orchestrator after document processing.
198
+ * Used by the startup banner to display `(auto-derived)` vs
199
+ * `(explicit)` next to each proxy path.
188
200
  */
189
- proxyPathSource: 'auto' | 'explicit';
201
+ proxyPathSource: ProxyPathSource;
190
202
  handlersDir: string;
191
203
  seedsDir: string;
192
204
  idFields: Record<string, string>;
@@ -208,6 +220,25 @@ interface ResolvedOptions {
208
220
  silent: boolean;
209
221
  logger?: Logger;
210
222
  }
223
+ /**
224
+ * Validate that specs array is non-empty and each entry has a valid spec field.
225
+ *
226
+ * @param specs - Array of spec configurations to validate
227
+ * @throws {ValidationError} SPECS_EMPTY if specs array is missing or empty
228
+ * @throws {ValidationError} SPEC_NOT_FOUND if a spec entry has empty spec field
229
+ */
230
+ declare function validateSpecs(specs: SpecConfig[]): void;
231
+ /**
232
+ * Resolve options with defaults.
233
+ *
234
+ * Note: spec ID and proxyPath resolution requires processing the OpenAPI document
235
+ * first, so they are resolved later in the orchestrator.
236
+ * This function only resolves static defaults.
237
+ *
238
+ * @param options - User-provided options
239
+ * @returns Resolved options with all defaults applied
240
+ */
241
+ declare function resolveOptions(options: OpenApiServerOptions): ResolvedOptions;
211
242
 
212
243
  /**
213
244
  * Vite Plugin Implementation
@@ -221,6 +252,244 @@ interface ResolvedOptions {
221
252
 
222
253
  declare function openApiServer(options: OpenApiServerOptions): Plugin;
223
254
 
255
+ /**
256
+ * Spec ID Derivation
257
+ *
258
+ * What: Functions to derive and validate spec identifiers
259
+ * How: Slugifies explicit IDs or auto-derives from OpenAPI info.title
260
+ * Why: Each spec instance needs a unique, URL-safe identifier for routing,
261
+ * DevTools grouping, logging, and default directory names
262
+ *
263
+ * @module spec-id
264
+ */
265
+
266
+ /**
267
+ * Slugify a string for use as a spec identifier
268
+ *
269
+ * Rules:
270
+ * - Lowercase
271
+ * - Replace spaces and special chars with hyphens
272
+ * - Remove consecutive hyphens
273
+ * - Trim leading/trailing hyphens
274
+ *
275
+ * @example
276
+ * slugify("Swagger Petstore") → "swagger-petstore"
277
+ * slugify("Billing API v2") → "billing-api-v2"
278
+ * slugify("café") → "cafe"
279
+ */
280
+ declare function slugify(input: string): string;
281
+ /**
282
+ * Derive a spec ID from the processed OpenAPI document
283
+ *
284
+ * Priority:
285
+ * 1. Explicit id from config (if non-empty)
286
+ * 2. Slugified info.title from the processed document
287
+ *
288
+ * @param explicitId - ID from SpecConfig.id (may be empty)
289
+ * @param document - Processed OpenAPI document
290
+ * @returns Stable, URL-safe spec identifier
291
+ * @throws {ValidationError} SPEC_ID_MISSING if no ID can be derived (missing title and no explicit id)
292
+ */
293
+ declare function deriveSpecId(explicitId: string, document: OpenAPIV3_1.Document): string;
294
+ /**
295
+ * Validate spec IDs are unique across all specs
296
+ *
297
+ * Collects all duplicated IDs and reports them in a single error.
298
+ *
299
+ * @param ids - Array of resolved spec IDs
300
+ * @throws {ValidationError} SPEC_ID_DUPLICATE if duplicate IDs found
301
+ */
302
+ declare function validateUniqueIds(ids: string[]): void;
303
+
304
+ /**
305
+ * Proxy Path Auto-Detection
306
+ *
307
+ * What: Functions to derive and validate proxy paths for spec instances
308
+ * How: Extracts path from explicit config or auto-derives from servers[0].url
309
+ * Why: Each spec instance needs a unique, non-overlapping proxy path for
310
+ * Vite proxy configuration and request routing
311
+ *
312
+ * @module proxy-path
313
+ */
314
+
315
+ /**
316
+ * Result of proxy path derivation
317
+ *
318
+ * Includes the normalized path and how it was determined,
319
+ * so the startup banner can display "(auto-derived)" vs "(explicit)".
320
+ */
321
+ interface DeriveProxyPathResult {
322
+ /** Normalized proxy path (e.g., "/api/v3") */
323
+ proxyPath: string;
324
+ /** How the path was determined */
325
+ proxyPathSource: ProxyPathSource;
326
+ }
327
+ /**
328
+ * Derive the proxy path from config or OpenAPI document's servers field
329
+ *
330
+ * Priority:
331
+ * 1. Explicit proxyPath from config (if non-empty after trimming)
332
+ * 2. Path portion of servers[0].url
333
+ *
334
+ * Full URLs (e.g., "https://api.example.com/api/v3") have their path
335
+ * extracted via the URL constructor. Relative paths (e.g., "/api/v3")
336
+ * are used directly.
337
+ *
338
+ * @param explicitPath - proxyPath from SpecConfig (may be empty)
339
+ * @param document - Processed OpenAPI document
340
+ * @param specId - Spec ID for error messages
341
+ * @returns Normalized proxy path with source indication
342
+ * @throws {ValidationError} PROXY_PATH_MISSING if path cannot be derived
343
+ * @throws {ValidationError} PROXY_PATH_TOO_BROAD if path resolves to "/" (e.g., "/", ".", "..")
344
+ *
345
+ * @example
346
+ * // Explicit path
347
+ * deriveProxyPath('/api/v3', document, 'petstore')
348
+ * // → { proxyPath: '/api/v3', proxyPathSource: 'explicit' }
349
+ *
350
+ * @example
351
+ * // Auto-derived from servers[0].url = "https://api.example.com/api/v3"
352
+ * deriveProxyPath('', document, 'petstore')
353
+ * // → { proxyPath: '/api/v3', proxyPathSource: 'auto' }
354
+ */
355
+ declare function deriveProxyPath(explicitPath: string, document: OpenAPIV3_1.Document, specId: string): DeriveProxyPathResult;
356
+ /**
357
+ * Normalize and validate a proxy path
358
+ *
359
+ * Rules:
360
+ * - Strip query strings and fragments
361
+ * - Ensure leading slash
362
+ * - Collapse consecutive slashes
363
+ * - Resolve dot segments ("." and ".." per RFC 3986 §5.2.4)
364
+ * - Remove trailing slash
365
+ * - Reject "/" as too broad (would capture all requests, including dot-segments that resolve to "/")
366
+ *
367
+ * @param path - Raw path string to normalize
368
+ * @param specId - Spec ID for error messages
369
+ * @returns Normalized path (e.g., "/api/v3")
370
+ * @throws {ValidationError} PROXY_PATH_TOO_BROAD if path resolves to "/" (e.g., "/", ".", "..")
371
+ *
372
+ * @example
373
+ * normalizeProxyPath('api/v3', 'petstore') → '/api/v3'
374
+ * normalizeProxyPath('/api/v3/', 'petstore') → '/api/v3'
375
+ */
376
+ declare function normalizeProxyPath(path: string, specId: string): string;
377
+ /**
378
+ * Validate proxy paths are unique and non-overlapping
379
+ *
380
+ * Checks for:
381
+ * 1. Duplicate paths — two specs with the same proxyPath
382
+ * 2. Overlapping paths — one path is a prefix of another (e.g., "/api" and "/api/v1")
383
+ * which would cause routing ambiguity
384
+ *
385
+ * @remarks
386
+ * Entries with an empty or falsy `proxyPath` are silently skipped. These
387
+ * represent specs whose proxy path has not yet been resolved (e.g., during
388
+ * early option resolution before the OpenAPI document is processed). Callers
389
+ * should expect unresolved entries to be excluded from uniqueness checks
390
+ * rather than triggering false-positive PROXY_PATH_DUPLICATE or
391
+ * PROXY_PATH_OVERLAP errors.
392
+ *
393
+ * @param specs - Array of spec entries with id and proxyPath.
394
+ * Entries with empty/falsy proxyPath are skipped (unresolved).
395
+ * @throws {ValidationError} PROXY_PATH_DUPLICATE if duplicate paths found
396
+ * @throws {ValidationError} PROXY_PATH_OVERLAP if overlapping paths found
397
+ *
398
+ * @example
399
+ * // Throws PROXY_PATH_DUPLICATE
400
+ * validateUniqueProxyPaths([
401
+ * { id: 'petstore', proxyPath: '/api/v3' },
402
+ * { id: 'inventory', proxyPath: '/api/v3' },
403
+ * ]);
404
+ *
405
+ * @example
406
+ * // Throws PROXY_PATH_OVERLAP
407
+ * validateUniqueProxyPaths([
408
+ * { id: 'petstore', proxyPath: '/api' },
409
+ * { id: 'inventory', proxyPath: '/api/v1' },
410
+ * ]);
411
+ */
412
+ declare function validateUniqueProxyPaths(specs: Array<{
413
+ id: string;
414
+ proxyPath: string;
415
+ }>): void;
416
+
417
+ /**
418
+ * Multi-Spec Orchestrator
419
+ *
420
+ * What: Central orchestrator that creates N spec instances and mounts them on a single Hono app
421
+ * How: Three phases — process specs, validate uniqueness, build main Hono app with dispatch middleware
422
+ * Why: Enables multiple OpenAPI specs to run on a single server with isolated stores and handlers
423
+ *
424
+ * @module orchestrator
425
+ */
426
+
427
+ /**
428
+ * Deterministic color palette for spec identification in DevTools.
429
+ *
430
+ * Colors are assigned by index: spec 0 gets green, spec 1 gets blue, etc.
431
+ * Wraps around for >8 specs.
432
+ */
433
+ declare const SPEC_COLORS: readonly string[];
434
+ /**
435
+ * Resolved spec instance with all runtime data.
436
+ *
437
+ * Created during Phase 1 of orchestration. Each instance owns
438
+ * an isolated core `OpenApiServer` with its own store, registry,
439
+ * handlers, seeds, and timeline.
440
+ */
441
+ interface SpecInstance {
442
+ /** Unique spec identifier (explicit or auto-derived from info.title) */
443
+ id: string;
444
+ /** Spec metadata for DevTools display and WebSocket protocol */
445
+ info: SpecInfo;
446
+ /** Core server instance (isolated Hono app, store, registry, etc.) */
447
+ server: OpenApiServer;
448
+ /** Resolved configuration for this spec */
449
+ config: ResolvedSpecConfig;
450
+ }
451
+ /**
452
+ * Orchestrator result — returned by `createOrchestrator()`.
453
+ *
454
+ * Provides access to the main Hono app (all specs mounted),
455
+ * individual spec instances, aggregated metadata, and lifecycle methods.
456
+ */
457
+ interface OrchestratorResult {
458
+ /** Main Hono app with all specs mounted via X-Spec-Id dispatch */
459
+ app: Hono;
460
+ /** All spec instances (in config order) */
461
+ instances: SpecInstance[];
462
+ /** Spec metadata array for WebSocket `connected` event */
463
+ specsInfo: SpecInfo[];
464
+ /** Start the shared HTTP server on the configured port */
465
+ start(): Promise<void>;
466
+ /** Stop the HTTP server and clean up resources */
467
+ stop(): Promise<void>;
468
+ /** Actual bound port after start() resolves (0 before start or after stop) */
469
+ readonly port: number;
470
+ }
471
+ /**
472
+ * Create the multi-spec orchestrator.
473
+ *
474
+ * Flow:
475
+ * 1. **Phase 1 — Process specs**: For each spec config, load handlers/seeds,
476
+ * create a core `OpenApiServer` instance, derive ID and proxy path.
477
+ * 2. **Phase 2 — Validate uniqueness**: Ensure all spec IDs and proxy paths
478
+ * are unique and non-overlapping.
479
+ * 3. **Phase 3 — Build main app**: Create a single Hono app with CORS,
480
+ * DevTools, Internal API, and X-Spec-Id dispatch middleware.
481
+ *
482
+ * **Note**: This function mutates `options.specs[i]` to write back resolved
483
+ * values (id, proxyPath, proxyPathSource, handlersDir, seedsDir) so that
484
+ * downstream consumers (banner, file watcher, plugin.ts) see the final values.
485
+ *
486
+ * @param options - Resolved plugin options (from `resolveOptions()`)
487
+ * @param vite - Vite dev server instance (for ssrLoadModule)
488
+ * @param cwd - Project root directory
489
+ * @returns Orchestrator result with app, instances, and lifecycle methods
490
+ */
491
+ declare function createOrchestrator(options: ResolvedOptions, vite: ViteDevServer, cwd: string): Promise<OrchestratorResult>;
492
+
224
493
  /**
225
494
  * Handler Loading
226
495
  *
@@ -517,4 +786,4 @@ declare function registerDevTools(app: App, options?: RegisterDevToolsOptions):
517
786
  */
518
787
  declare function getDevToolsUrl(port?: number, host?: string, protocol?: 'http' | 'https'): string;
519
788
 
520
- export { type FileWatcher, type FileWatcherOptions, type LoadHandlersResult, type LoadSeedsResult, type OpenApiServerOptions, type RegisterDevToolsOptions, type ResolvedOptions, type ResolvedSpecConfig, type SpecConfig, ValidationError, type ValidationErrorCode, createFileWatcher, debounce, getDevToolsUrl, getHandlerFiles, getSeedFiles, loadHandlers, loadSeeds, openApiServer, registerDevTools };
789
+ export { type DeriveProxyPathResult, type FileWatcher, type FileWatcherOptions, type LoadHandlersResult, type LoadSeedsResult, type OpenApiServerOptions, type OrchestratorResult, type ProxyPathSource, type RegisterDevToolsOptions, type ResolvedOptions, type ResolvedSpecConfig, SPEC_COLORS, type SpecConfig, type SpecInstance, ValidationError, type ValidationErrorCode, createFileWatcher, createOrchestrator, debounce, deriveProxyPath, deriveSpecId, getDevToolsUrl, getHandlerFiles, getSeedFiles, loadHandlers, loadSeeds, normalizeProxyPath, openApiServer, registerDevTools, resolveOptions, slugify, validateSpecs, validateUniqueIds, validateUniqueProxyPaths };