@velajs/vela 1.14.0 → 1.15.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/CHANGELOG.md CHANGED
@@ -1,5 +1,30 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.15.0 (2026-07-04)
4
+
5
+ Introspection seams for `@velajs/cli` (roadmap phase 3, CLI introspection):
6
+ serializable, zero-instantiation views of the app the CLI renders instead of
7
+ re-deriving framework internals.
8
+
9
+ ### Added
10
+
11
+ - **`app.describeRoutes(): RouteDescription[]`** — every explicit controller
12
+ route exactly as `build()` registered it: method AS DECLARED (`@Head()`
13
+ reports HEAD even though Hono serves it under GET), fully composed path
14
+ (global prefix + version segment + controller prefix + route path),
15
+ controller name, handler name, version. Contributed routes
16
+ (`RouteContributor`/CRUD) mount directly on Hono and are observable via
17
+ `getHonoApp().routes`.
18
+ - **`app.getGlobalPrefix()`** — the prefix in effect ('' when none); also
19
+ what `openapi` tooling threads into `createOpenApiDocument`.
20
+ - **`Container.getModuleDescriptions(): ModuleDescription[]`** — the loaded
21
+ module graph (moduleId, imports, isGlobal, lazy, provider/export token
22
+ labels), load order plus the `__root__` bucket; reads registration state
23
+ only — safe pre/post bootstrap, never constructs, never claims lazy
24
+ modules.
25
+ - **`describeToken(token)`** — the token-label helper vela's own errors use,
26
+ exported for tooling.
27
+
3
28
  ## 1.14.0 (2026-07-04)
4
29
 
5
30
  First-party `QueueModule` (roadmap phase 3, "the openness proof"): a whole
@@ -2,7 +2,7 @@ import type { Hono } from 'hono';
2
2
  import type { Container } from './container/container';
3
3
  import type { Token } from './container/types';
4
4
  import { EntrypointRegistry } from './entrypoint/entrypoint.registry';
5
- import type { RouteManager } from './http/route.manager';
5
+ import type { RouteDescription, RouteManager } from './http/route.manager';
6
6
  import type { MountOpenApiOptions } from './openapi/types';
7
7
  import type { FilterType, GuardType, InterceptorType, PipeType } from './registry/types';
8
8
  export declare class VelaApplication {
@@ -25,6 +25,15 @@ export declare class VelaApplication {
25
25
  setInstances(instances: unknown[]): void;
26
26
  get<T>(token: Token<T>): T;
27
27
  getHonoApp(): Hono;
28
+ /**
29
+ * Every explicit controller route as the framework registered it (fully
30
+ * composed paths — the `vela route list` seam). Requires built routes;
31
+ * contributed routes (`@velajs/crud`) mount directly on Hono and are
32
+ * observable via `getHonoApp().routes` instead.
33
+ */
34
+ describeRoutes(): RouteDescription[];
35
+ /** The global route prefix in effect ('' when none). */
36
+ getGlobalPrefix(): string;
28
37
  useGlobalPipes(...pipes: PipeType[]): this;
29
38
  useGlobalGuards(...guards: GuardType[]): this;
30
39
  useGlobalInterceptors(...interceptors: InterceptorType[]): this;
@@ -63,6 +63,18 @@ export class VelaApplication {
63
63
  getHonoApp() {
64
64
  return this.getApp();
65
65
  }
66
+ /**
67
+ * Every explicit controller route as the framework registered it (fully
68
+ * composed paths — the `vela route list` seam). Requires built routes;
69
+ * contributed routes (`@velajs/crud`) mount directly on Hono and are
70
+ * observable via `getHonoApp().routes` instead.
71
+ */ describeRoutes() {
72
+ this.getApp(); // same routes-not-built guard as every HTTP accessor
73
+ return this.routeManager.getRouteDescriptions();
74
+ }
75
+ /** The global route prefix in effect ('' when none). */ getGlobalPrefix() {
76
+ return this.routeManager.getGlobalPrefix();
77
+ }
66
78
  // Pipeline components — applied at request time, no rebuild needed
67
79
  useGlobalPipes(...pipes) {
68
80
  this.routeManager.useGlobalPipes(...pipes);
@@ -1,5 +1,5 @@
1
1
  import { Scope } from '../constants';
2
- import type { ContainerOptions, Diagnostics, LazyResolutionHook, ModuleScope, ProviderOptions, Token, Type } from './types';
2
+ import type { ContainerOptions, Diagnostics, LazyResolutionHook, ModuleDescription, ModuleScope, ProviderOptions, Token, Type } from './types';
3
3
  /**
4
4
  * Per-module provider buckets. Each module instance owns its providers under
5
5
  * its `moduleId`; the same logical token can have distinct registrations in
@@ -95,6 +95,13 @@ export declare class Container {
95
95
  * binding module — so framework adapters can initialize all of them.
96
96
  */
97
97
  getUseValues(): unknown[];
98
+ /**
99
+ * Serializable description of every loaded module instance (load order),
100
+ * plus the `__root__` bucket (bootstrap primitives) when non-empty —
101
+ * the `vela module graph` seam. Reads registration state only: safe pre-
102
+ * and post-bootstrap, never constructs, never claims lazy modules.
103
+ */
104
+ getModuleDescriptions(): ModuleDescription[];
98
105
  /**
99
106
  * Compute request-scope bubbling for every registration (call once at
100
107
  * bootstrap, after all providers are registered). A provider whose declared
@@ -1,7 +1,7 @@
1
1
  import { Scope } from "../constants.js";
2
2
  import { disposeInstance, isDisposable } from "./disposable.js";
3
3
  import { getConstructorDependencies, getInjectMetadata, getScope, isInjectable } from "./decorators.js";
4
- import { ForwardRef, InjectionToken, ModuleVisibilityError, MultipleProvidersFoundError, ROOT_MODULE_ID } from "./types.js";
4
+ import { describeToken, ForwardRef, InjectionToken, ModuleVisibilityError, MultipleProvidersFoundError, ROOT_MODULE_ID } from "./types.js";
5
5
  const IMPORT_TYPE_HINT = 'Did you use `import type { X }`? TypeScript strips type-only imports at ' + 'runtime and `design:paramtypes` emits `Object`/`undefined` for their ' + 'positions. Use a runtime `import { X }` for DI tokens.';
6
6
  /**
7
7
  * Sentinel comparing a token to the bare `Object` class — `design:paramtypes`
@@ -415,6 +415,44 @@ const IMPORT_TYPE_HINT = 'Did you use `import type { X }`? TypeScript strips typ
415
415
  return out;
416
416
  }
417
417
  /**
418
+ * Serializable description of every loaded module instance (load order),
419
+ * plus the `__root__` bucket (bootstrap primitives) when non-empty —
420
+ * the `vela module graph` seam. Reads registration state only: safe pre-
421
+ * and post-bootstrap, never constructs, never claims lazy modules.
422
+ */ getModuleDescriptions() {
423
+ const out = [];
424
+ for (const scope of this.scopes.values()){
425
+ out.push({
426
+ moduleId: scope.moduleId,
427
+ imports: [
428
+ ...scope.importedModules
429
+ ],
430
+ isGlobal: scope.isGlobal,
431
+ lazy: scope.lazy === true,
432
+ providers: [
433
+ ...this.providers.get(scope.moduleId)?.keys() ?? []
434
+ ].map(describeToken),
435
+ exports: [
436
+ ...scope.exportedTokens
437
+ ].map(describeToken)
438
+ });
439
+ }
440
+ const rootBucket = this.providers.get(ROOT_MODULE_ID);
441
+ if (rootBucket && rootBucket.size > 0) {
442
+ out.push({
443
+ moduleId: ROOT_MODULE_ID,
444
+ imports: [],
445
+ isGlobal: false,
446
+ lazy: false,
447
+ providers: [
448
+ ...rootBucket.keys()
449
+ ].map(describeToken),
450
+ exports: []
451
+ });
452
+ }
453
+ return out;
454
+ }
455
+ /**
418
456
  * Compute request-scope bubbling for every registration (call once at
419
457
  * bootstrap, after all providers are registered). A provider whose declared
420
458
  * scope is not REQUEST but which (transitively) depends on a request-scoped
@@ -1,6 +1,6 @@
1
1
  export { Container } from './container';
2
2
  export { Injectable, Inject, Optional, isInjectable, getScope } from './decorators';
3
- export { InjectionToken, ForwardRef, forwardRef, ModuleVisibilityError, MultipleProvidersFoundError, ROOT_MODULE_ID, } from './types';
3
+ export { InjectionToken, ForwardRef, forwardRef, ModuleVisibilityError, MultipleProvidersFoundError, ROOT_MODULE_ID, describeToken, } from './types';
4
4
  export { ModuleRef } from './module-ref';
5
5
  export { mixin } from './mixin';
6
- export type { Type, Token, InferToken, InferTokens, InjectableOptions, InjectMetadata, ProviderOptions, ProviderRegistration, InjectionTokenOptions, ModuleScope, ContainerOptions, Diagnostics, } from './types';
6
+ export type { Type, Token, InferToken, InferTokens, InjectableOptions, InjectMetadata, ProviderOptions, ProviderRegistration, InjectionTokenOptions, ModuleScope, ModuleDescription, ContainerOptions, Diagnostics, } from './types';
@@ -1,5 +1,5 @@
1
1
  export { Container } from "./container.js";
2
2
  export { Injectable, Inject, Optional, isInjectable, getScope } from "./decorators.js";
3
- export { InjectionToken, ForwardRef, forwardRef, ModuleVisibilityError, MultipleProvidersFoundError, ROOT_MODULE_ID } from "./types.js";
3
+ export { InjectionToken, ForwardRef, forwardRef, ModuleVisibilityError, MultipleProvidersFoundError, ROOT_MODULE_ID, describeToken } from "./types.js";
4
4
  export { ModuleRef } from "./module-ref.js";
5
5
  export { mixin } from "./mixin.js";
@@ -117,6 +117,29 @@ export type Diagnostics = 'silent' | 'log' | 'throw';
117
117
  export interface ContainerOptions {
118
118
  diagnostics?: Diagnostics;
119
119
  }
120
+ /**
121
+ * Human-readable token label — class name, `InjectionToken(desc)`, symbol
122
+ * string, or String() fallback. Public so introspection tooling (module
123
+ * graphs, entrypoint listings) renders tokens the same way vela's own errors
124
+ * do.
125
+ */
126
+ export declare function describeToken(token: Token): string;
127
+ /**
128
+ * One module instance in the loaded graph, serializable (strings only) —
129
+ * what `Container.getModuleDescriptions()` returns for introspection tooling
130
+ * (`vela module graph`). Reads registration state only; never constructs.
131
+ */
132
+ export interface ModuleDescription {
133
+ moduleId: string;
134
+ /** moduleIds this instance imports. */
135
+ imports: string[];
136
+ isGlobal: boolean;
137
+ lazy: boolean;
138
+ /** Token labels registered in this instance's bucket (registration order). */
139
+ providers: string[];
140
+ /** Token labels this instance exports. */
141
+ exports: string[];
142
+ }
120
143
  export declare class ModuleVisibilityError extends Error {
121
144
  readonly moduleId: string;
122
145
  readonly token: Token;
@@ -18,7 +18,12 @@ export class ForwardRef {
18
18
  export function forwardRef(factory) {
19
19
  return new ForwardRef(factory);
20
20
  }
21
- function describeToken(token) {
21
+ /**
22
+ * Human-readable token label — class name, `InjectionToken(desc)`, symbol
23
+ * string, or String() fallback. Public so introspection tooling (module
24
+ * graphs, entrypoint listings) renders tokens the same way vela's own errors
25
+ * do.
26
+ */ export function describeToken(token) {
22
27
  if (token instanceof InjectionToken) return token.toString();
23
28
  if (typeof token === 'function') return token.name;
24
29
  if (typeof token === 'symbol') return token.toString();
@@ -5,6 +5,23 @@ import type { MiddlewareRouteDefinition } from '../module/middleware';
5
5
  import type { CanActivate, ExceptionFilter, NestInterceptor, NestMiddleware, PipeTransform } from '../pipeline/types';
6
6
  import type { FilterType, GuardType, InterceptorType, MiddlewareType, PipeType } from '../registry/types';
7
7
  import type { ControllerRegistration } from './types';
8
+ /**
9
+ * One explicit controller route as the framework registered it — recorded by
10
+ * `build()` with the SAME composed path handed to Hono (global prefix +
11
+ * version segment + controller prefix + route path), so introspection tooling
12
+ * (`vela route list`) never re-derives composition. Contributed routes
13
+ * (`RouteContributor`, e.g. `@velajs/crud`) mount directly on the Hono app
14
+ * and are not described here — diff `app.getHonoApp().routes` for those.
15
+ */
16
+ export interface RouteDescription {
17
+ /** As declared on the handler — `@Head()` reports HEAD (Hono serves it under GET). */
18
+ method: string;
19
+ /** Fully composed path exactly as registered. */
20
+ path: string;
21
+ controller: string;
22
+ handler: string;
23
+ version?: number;
24
+ }
8
25
  export interface RouteManagerOptions {
9
26
  getClientIp?: (c: Context) => string | null;
10
27
  middleware?: MiddlewareHandler[];
@@ -27,6 +44,7 @@ export declare class RouteManager {
27
44
  private globalFilters;
28
45
  private globalPrefix;
29
46
  private consumerMiddlewareDefinitions;
47
+ private routeDescriptions;
30
48
  private readonly handlerExecutor;
31
49
  private readonly ambientContainer;
32
50
  constructor(container: Container, options?: RouteManagerOptions);
@@ -59,6 +77,10 @@ export declare class RouteManager {
59
77
  private wrapMiddlewareWithFilters;
60
78
  private mapMiddlewareError;
61
79
  registerController(controller: Type): this;
80
+ /** The explicit controller routes recorded by the last `build()`. */
81
+ getRouteDescriptions(): RouteDescription[];
82
+ /** The global prefix in effect (set at bootstrap; '' when none). */
83
+ getGlobalPrefix(): string;
62
84
  build(): Promise<Hono>;
63
85
  private registerRoute;
64
86
  private buildVersionedPaths;
@@ -92,6 +92,7 @@ export class RouteManager {
92
92
  globalFilters = [];
93
93
  globalPrefix = '';
94
94
  consumerMiddlewareDefinitions = [];
95
+ routeDescriptions = [];
95
96
  handlerExecutor;
96
97
  ambientContainer;
97
98
  constructor(container, options = {}){
@@ -273,8 +274,17 @@ export class RouteManager {
273
274
  });
274
275
  return this;
275
276
  }
277
+ /** The explicit controller routes recorded by the last `build()`. */ getRouteDescriptions() {
278
+ return [
279
+ ...this.routeDescriptions
280
+ ];
281
+ }
282
+ /** The global prefix in effect (set at bootstrap; '' when none). */ getGlobalPrefix() {
283
+ return this.globalPrefix;
284
+ }
276
285
  async build() {
277
286
  const app = new Hono();
287
+ this.routeDescriptions = [];
278
288
  // Outermost: dispose the per-request child container once the request is
279
289
  // fully done. Fast-paths out when the child has no request-scoped
280
290
  // disposables (the common case → zero overhead / no behavior change).
@@ -353,11 +363,18 @@ export class RouteManager {
353
363
  for (const route of routes){
354
364
  const effectiveVersion = route.version ?? metadata.version;
355
365
  const versionedPaths = this.buildVersionedPaths(this.globalPrefix, metadata.prefix, route.path, effectiveVersion);
366
+ // Parallel to versionedPaths: buildVersionedPaths maps versions to
367
+ // paths 1:1 in order, so index i of both arrays belongs together.
368
+ const versionsForPaths = effectiveVersion === undefined ? [
369
+ undefined
370
+ ] : Array.isArray(effectiveVersion) ? effectiveVersion : [
371
+ effectiveVersion
372
+ ];
356
373
  // Scoped (controller/handler) middleware only — global middleware is
357
374
  // applied once by this manager's own global pass, never re-read here.
358
375
  const middlewareItems = ComponentManager.getScopedComponents('middleware', controller, route.handlerName);
359
376
  const handler = this.handlerExecutor.create(route, controller, allParamMetadata);
360
- for (const fullPath of versionedPaths){
377
+ for (const [pathIndex, fullPath] of versionedPaths.entries()){
361
378
  for (const middlewareItem of middlewareItems){
362
379
  app.use(fullPath, this.wrapMiddlewareWithFilters((c, next)=>{
363
380
  const requestContainer = this.getRequestContainer(c);
@@ -366,6 +383,13 @@ export class RouteManager {
366
383
  }));
367
384
  }
368
385
  this.registerRoute(app, route.method, fullPath, handler);
386
+ this.routeDescriptions.push({
387
+ method: String(route.method),
388
+ path: fullPath || '/',
389
+ controller: controller.name,
390
+ handler: String(route.handlerName),
391
+ version: versionsForPaths[pathIndex]
392
+ });
369
393
  }
370
394
  }
371
395
  }
package/dist/index.d.ts CHANGED
@@ -6,8 +6,9 @@ export { bootstrap } from './factory/bootstrap';
6
6
  export type { BootstrapOptions, BootstrapResult } from './factory/bootstrap';
7
7
  export { createOpenApiDocument, ApiDoc, ApiTags, ApiResponse, zodToJsonSchema, } from './openapi/index';
8
8
  export type { OpenApiDocument, OpenApiInfo, OpenApiOperation, OpenApiParameter, OpenApiPathItem, OpenApiRequestBody, OpenApiResponse, ApiDocMetadata, ApiResponseEntry, ApiResponseOptions, CreateOpenApiDocumentOptions, HttpVerb, JsonSchema, } from './openapi/index';
9
- export { Container, Injectable, Inject, Optional, InjectionToken, ForwardRef, forwardRef, ModuleRef, ModuleVisibilityError, MultipleProvidersFoundError, ROOT_MODULE_ID, mixin, } from './container/index';
10
- export type { Type, Token, InferToken, InferTokens, InjectableOptions, ProviderOptions, ModuleScope, ContainerOptions, Diagnostics, } from './container/index';
9
+ export { Container, Injectable, Inject, Optional, InjectionToken, ForwardRef, forwardRef, ModuleRef, ModuleVisibilityError, MultipleProvidersFoundError, ROOT_MODULE_ID, mixin, describeToken, } from './container/index';
10
+ export type { Type, Token, InferToken, InferTokens, InjectableOptions, ProviderOptions, ModuleScope, ModuleDescription, ContainerOptions, Diagnostics, } from './container/index';
11
+ export type { RouteDescription } from './http/route.manager';
11
12
  export { METADATA_KEYS, HttpMethod, ParamType, Scope } from './constants';
12
13
  export { Controller, Version, Get, Post, Put, Patch, Delete, Options, Head, All, Sse, Param, Query, Body, Headers, Req, Res, Ip, Cookie, Cookies, RawBody, HttpCode, Header, Redirect, createParamDecorator, createLazyParamDecorator, applyDecorators, } from './http/index';
13
14
  export { REQUEST_CONTEXT } from './http/request-context';
package/dist/index.js CHANGED
@@ -7,7 +7,7 @@ export { bootstrap } from "./factory/bootstrap.js";
7
7
  // OpenAPI
8
8
  export { createOpenApiDocument, ApiDoc, ApiTags, ApiResponse, zodToJsonSchema } from "./openapi/index.js";
9
9
  // DI Container
10
- export { Container, Injectable, Inject, Optional, InjectionToken, ForwardRef, forwardRef, ModuleRef, ModuleVisibilityError, MultipleProvidersFoundError, ROOT_MODULE_ID, mixin } from "./container/index.js";
10
+ export { Container, Injectable, Inject, Optional, InjectionToken, ForwardRef, forwardRef, ModuleRef, ModuleVisibilityError, MultipleProvidersFoundError, ROOT_MODULE_ID, mixin, describeToken } from "./container/index.js";
11
11
  // Constants
12
12
  export { METADATA_KEYS, HttpMethod, ParamType, Scope } from "./constants.js";
13
13
  // HTTP Decorators
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@velajs/vela",
3
- "version": "1.14.0",
3
+ "version": "1.15.0",
4
4
  "description": "NestJS-compatible framework for edge runtimes, powered by Hono",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",