@velajs/vela 1.2.0 → 1.4.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/factory.js CHANGED
@@ -1,39 +1,8 @@
1
1
  import { VelaApplication } from "./application.js";
2
- import { Container } from "./container/container.js";
3
- import { ModuleRef } from "./container/module-ref.js";
4
- import { bindAppProviders } from "./pipeline/app-providers.js";
5
- import { RouteManager } from "./http/route.manager.js";
6
- import { ModuleLoader } from "./module/module-loader.js";
7
- import { ComponentManager } from "./pipeline/component.manager.js";
2
+ import { bootstrap } from "./factory/bootstrap.js";
8
3
  export const VelaFactory = {
9
4
  async create (rootModule, options = {}) {
10
- const container = new Container();
11
- container.register({
12
- provide: Container,
13
- useValue: container
14
- });
15
- container.register({
16
- provide: ModuleRef,
17
- useFactory: (c)=>new ModuleRef(c),
18
- inject: [
19
- Container
20
- ]
21
- });
22
- const routeManager = new RouteManager(container, options);
23
- ComponentManager.init(container);
24
- const loader = new ModuleLoader(container, routeManager);
25
- loader.load(rootModule);
26
- bindAppProviders(routeManager, container, loader);
27
- routeManager.registerConsumerMiddleware(loader.getConsumerMiddlewareDefinitions());
28
- if (options.globalPrefix) {
29
- routeManager.setGlobalPrefix(options.globalPrefix);
30
- }
31
- for (const handler of options.middleware ?? []){
32
- const mw = {
33
- use: handler
34
- };
35
- routeManager.useGlobalMiddleware(mw);
36
- }
5
+ const { container, routeManager, loader } = await bootstrap(rootModule, options);
37
6
  const app = new VelaApplication(container, routeManager);
38
7
  const instances = await loader.resolveAllInstances();
39
8
  app.setInstances(instances);
@@ -0,0 +1,13 @@
1
+ import type { Context } from 'hono';
2
+ import { InjectionToken } from '../container/types';
3
+ export interface RequestContext {
4
+ readonly id: string;
5
+ readonly receivedAt: Date;
6
+ readonly request: Request;
7
+ readonly hono: Context;
8
+ set<T>(key: string | symbol, value: T): void;
9
+ get<T>(key: string | symbol): T | undefined;
10
+ has(key: string | symbol): boolean;
11
+ }
12
+ export declare const REQUEST_CONTEXT: InjectionToken<RequestContext>;
13
+ export declare function createRequestContext(c: Context): RequestContext;
@@ -0,0 +1,22 @@
1
+ import { InjectionToken } from "../container/types.js";
2
+ export const REQUEST_CONTEXT = new InjectionToken('vela.RequestContext');
3
+ export function createRequestContext(c) {
4
+ const bag = new Map();
5
+ const inboundId = c.req.raw.headers.get('x-request-id');
6
+ const id = inboundId && inboundId.length > 0 ? inboundId : crypto.randomUUID();
7
+ return {
8
+ id,
9
+ receivedAt: new Date(),
10
+ request: c.req.raw,
11
+ hono: c,
12
+ set (key, value) {
13
+ bag.set(key, value);
14
+ },
15
+ get (key) {
16
+ return bag.get(key);
17
+ },
18
+ has (key) {
19
+ return bag.has(key);
20
+ }
21
+ };
22
+ }
@@ -4,6 +4,7 @@ import { HttpMethod, ParamType } from "../constants.js";
4
4
  import { getMetadata } from "../metadata.js";
5
5
  import { joinPaths } from "../registry/paths.js";
6
6
  import { buildExecutionContext } from "./execution-context.js";
7
+ import { REQUEST_CONTEXT, createRequestContext } from "./request-context.js";
7
8
  import { ForbiddenException, HttpException } from "../errors/http-exception.js";
8
9
  import { ComponentManager } from "../pipeline/component.manager.js";
9
10
  import { shouldFilterCatch } from "../pipeline/decorators.js";
@@ -206,6 +207,7 @@ export class RouteManager {
206
207
  return existing;
207
208
  }
208
209
  const child = this.container.createChild();
210
+ child.setRequestInstance(REQUEST_CONTEXT, createRequestContext(c));
209
211
  c.set('container', child);
210
212
  return child;
211
213
  }
package/dist/index.d.ts CHANGED
@@ -2,12 +2,16 @@ import './metadata';
2
2
  export { defineMetadata, getMetadata } from './metadata';
3
3
  export { VelaFactory } from './factory';
4
4
  export { VelaApplication } from './application';
5
+ export { bootstrap } from './factory/bootstrap';
6
+ export type { BootstrapOptions, BootstrapResult } from './factory/bootstrap';
5
7
  export { createOpenApiDocument, ApiDoc, ApiTags, ApiResponse, zodToJsonSchema, } from './openapi/index';
6
8
  export type { OpenApiDocument, OpenApiInfo, OpenApiOperation, OpenApiParameter, OpenApiPathItem, OpenApiRequestBody, OpenApiResponse, ApiDocMetadata, ApiResponseEntry, ApiResponseOptions, CreateOpenApiDocumentOptions, HttpVerb, JsonSchema, } from './openapi/index';
7
- export { Container, Injectable, Inject, Optional, InjectionToken, ForwardRef, forwardRef, ModuleRef, mixin } from './container/index';
8
- export type { Type, Token, InjectableOptions, ProviderOptions, } from './container/index';
9
+ export { Container, Injectable, Inject, Optional, InjectionToken, ForwardRef, forwardRef, ModuleRef, ModuleVisibilityError, mixin, } from './container/index';
10
+ export type { Type, Token, InjectableOptions, ProviderOptions, ModuleScope, ContainerOptions, Diagnostics, } from './container/index';
9
11
  export { METADATA_KEYS, HttpMethod, ParamType, Scope } from './constants';
10
12
  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, applyDecorators, } from './http/index';
13
+ export { REQUEST_CONTEXT } from './http/request-context';
14
+ export type { RequestContext } from './http/request-context';
11
15
  export { Logger, LogLevel } from './services/index';
12
16
  export type { LoggerService, ContextProvider, Writer, LoggerLevelName } from './services/index';
13
17
  export { ConfigModule, ConfigService, CONFIG_OPTIONS } from './config/index';
@@ -29,6 +33,8 @@ export type { ThrottlerModuleOptions, ThrottleConfig, ThrottlerStore, ThrottlerS
29
33
  export { Global, Module, createModuleRef } from './module/index';
30
34
  export type { ModuleOptions, DynamicModule, AsyncModuleOptions, ModuleImport } from './module/index';
31
35
  export type { MiddlewareConsumer, NestModule, RouteInfo } from './http/index';
36
+ export { definePlugin, composePlugins, PluginRegistry, PluginRootModule, PLUGIN_REGISTRY_TOKEN, } from './plugin/plugin';
37
+ export type { Plugin } from './plugin/plugin';
32
38
  export { UseMiddleware, UseGuards, UsePipes, UseInterceptors, UseFilters, Catch, SetMetadata, Reflector, APP_GUARD, APP_PIPE, APP_INTERCEPTOR, APP_FILTER, APP_MIDDLEWARE, } from './pipeline/index';
33
39
  export type { HttpArgumentsHost, ExecutionContext, CanActivate, CallHandler, NestInterceptor, NestMiddleware, PipeTransform, ExceptionFilter, ArgumentMetadata, ReflectableDecorator, CreateDecoratorOptions, } from './pipeline/index';
34
40
  export type { Constructor, MiddlewareType, GuardType, PipeType, InterceptorType, FilterType, } from './registry/index';
package/dist/index.js CHANGED
@@ -3,14 +3,17 @@ export { defineMetadata, getMetadata } from "./metadata.js";
3
3
  // Factory & Application
4
4
  export { VelaFactory } from "./factory.js";
5
5
  export { VelaApplication } from "./application.js";
6
+ export { bootstrap } from "./factory/bootstrap.js";
6
7
  // OpenAPI
7
8
  export { createOpenApiDocument, ApiDoc, ApiTags, ApiResponse, zodToJsonSchema } from "./openapi/index.js";
8
9
  // DI Container
9
- export { Container, Injectable, Inject, Optional, InjectionToken, ForwardRef, forwardRef, ModuleRef, mixin } from "./container/index.js";
10
+ export { Container, Injectable, Inject, Optional, InjectionToken, ForwardRef, forwardRef, ModuleRef, ModuleVisibilityError, mixin } from "./container/index.js";
10
11
  // Constants
11
12
  export { METADATA_KEYS, HttpMethod, ParamType, Scope } from "./constants.js";
12
13
  // HTTP Decorators
13
14
  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, applyDecorators } from "./http/index.js";
15
+ // Request-scoped context primitive
16
+ export { REQUEST_CONTEXT } from "./http/request-context.js";
14
17
  // Services
15
18
  export { Logger, LogLevel } from "./services/index.js";
16
19
  // Config
@@ -31,6 +34,8 @@ export { HealthModule, HealthCheckService, HealthIndicatorService, HttpHealthInd
31
34
  export { ThrottlerModule, ThrottlerGuard, ThrottlerStorage, Throttle, SkipThrottle, THROTTLER_OPTIONS, THROTTLER_STORAGE, THROTTLE_METADATA, SKIP_THROTTLE_METADATA } from "./throttler/index.js";
32
35
  // Module
33
36
  export { Global, Module, createModuleRef } from "./module/index.js";
37
+ // Plugin manifest + composer
38
+ export { definePlugin, composePlugins, PluginRegistry, PluginRootModule, PLUGIN_REGISTRY_TOKEN } from "./plugin/plugin.js";
34
39
  // Pipeline Decorators
35
40
  export { UseMiddleware, UseGuards, UsePipes, UseInterceptors, UseFilters, Catch, SetMetadata, Reflector, APP_GUARD, APP_PIPE, APP_INTERCEPTOR, APP_FILTER, APP_MIDDLEWARE } from "./pipeline/index.js";
36
41
  // Built-in Pipes
@@ -1,5 +1,7 @@
1
1
  export { Container } from './container/container';
2
2
  export { ModuleRef } from './container/module-ref';
3
+ export { ModuleVisibilityError } from './container/types';
4
+ export type { ModuleScope, ContainerOptions, Diagnostics, } from './container/types';
3
5
  export { bindAppProviders } from './pipeline/app-providers';
4
6
  export { RouteManager } from './http/route.manager';
5
7
  export type { RouteManagerOptions } from './http/route.manager';
@@ -9,3 +11,7 @@ export { VelaApplication } from './application';
9
11
  export { MetadataRegistry } from './registry/metadata.registry';
10
12
  export { APP_GUARD, APP_PIPE, APP_INTERCEPTOR, APP_FILTER, APP_MIDDLEWARE, } from './pipeline/tokens';
11
13
  export { getModuleMetadata, isModule } from './module/decorators';
14
+ export { bootstrap } from './factory/bootstrap';
15
+ export type { BootstrapOptions, BootstrapResult } from './factory/bootstrap';
16
+ export { definePlugin, composePlugins, PluginRegistry, PluginRootModule, PLUGIN_REGISTRY_TOKEN, } from './plugin/plugin';
17
+ export type { Plugin } from './plugin/plugin';
package/dist/internal.js CHANGED
@@ -3,6 +3,7 @@
3
3
  // to the same degree as the public surface (./index).
4
4
  export { Container } from "./container/container.js";
5
5
  export { ModuleRef } from "./container/module-ref.js";
6
+ export { ModuleVisibilityError } from "./container/types.js";
6
7
  export { bindAppProviders } from "./pipeline/app-providers.js";
7
8
  export { RouteManager } from "./http/route.manager.js";
8
9
  export { ModuleLoader } from "./module/module-loader.js";
@@ -11,3 +12,8 @@ export { VelaApplication } from "./application.js";
11
12
  export { MetadataRegistry } from "./registry/metadata.registry.js";
12
13
  export { APP_GUARD, APP_PIPE, APP_INTERCEPTOR, APP_FILTER, APP_MIDDLEWARE } from "./pipeline/tokens.js";
13
14
  export { getModuleMetadata, isModule } from "./module/decorators.js";
15
+ // Bootstrap primitive — used by VelaFactory.create, @velajs/testing, and any
16
+ // non-HTTP consumer (CLI tools, custom runtimes).
17
+ export { bootstrap } from "./factory/bootstrap.js";
18
+ // Plugin manifest + composer
19
+ export { definePlugin, composePlugins, PluginRegistry, PluginRootModule, PLUGIN_REGISTRY_TOKEN } from "./plugin/plugin.js";
@@ -14,8 +14,11 @@ export declare class ModuleLoader {
14
14
  private consumerMiddlewareDefinitions;
15
15
  private appProviderCounter;
16
16
  private appProviderTokens;
17
+ private moduleIdByClass;
18
+ private seenModuleIds;
17
19
  constructor(container: Container, router: RouteManager);
18
20
  load(rootModule: Type): void;
21
+ private getModuleId;
19
22
  private processModule;
20
23
  private registerProvider;
21
24
  private isAppToken;
@@ -25,4 +28,5 @@ export declare class ModuleLoader {
25
28
  getAppProviderTokens(token: Token): Token[];
26
29
  getConsumerMiddlewareDefinitions(): MiddlewareRouteDefinition[];
27
30
  resolveAllInstances(): Promise<unknown[]>;
31
+ private routeError;
28
32
  }
@@ -1,5 +1,5 @@
1
1
  import { Scope } from "../constants.js";
2
- import { ForwardRef, InjectionToken } from "../container/types.js";
2
+ import { ForwardRef, InjectionToken, ModuleVisibilityError } from "../container/types.js";
3
3
  import { APP_FILTER, APP_GUARD, APP_INTERCEPTOR, APP_MIDDLEWARE, APP_PIPE } from "../pipeline/tokens.js";
4
4
  import { MetadataRegistry } from "../registry/metadata.registry.js";
5
5
  import { getOrCreateArray } from "../registry/util.js";
@@ -18,6 +18,9 @@ function isDynamicModule(value) {
18
18
  function implementsNestModule(cls) {
19
19
  return typeof cls.prototype?.configure === "function";
20
20
  }
21
+ function tokenOfProvider(provider) {
22
+ return typeof provider === "function" ? provider : provider.provide;
23
+ }
21
24
  export class ModuleLoader {
22
25
  container;
23
26
  router;
@@ -51,6 +54,8 @@ export class ModuleLoader {
51
54
  []
52
55
  ]
53
56
  ]);
57
+ moduleIdByClass = new Map();
58
+ seenModuleIds = new Set();
54
59
  constructor(container, router){
55
60
  this.container = container;
56
61
  this.router = router;
@@ -61,6 +66,19 @@ export class ModuleLoader {
61
66
  this.router.registerController(controller);
62
67
  }
63
68
  }
69
+ getModuleId(moduleClass) {
70
+ const cached = this.moduleIdByClass.get(moduleClass);
71
+ if (cached) return cached;
72
+ const baseName = moduleClass.name || "AnonModule";
73
+ let id = baseName;
74
+ let counter = 0;
75
+ while(this.seenModuleIds.has(id)){
76
+ id = `${baseName}#${++counter}`;
77
+ }
78
+ this.seenModuleIds.add(id);
79
+ this.moduleIdByClass.set(moduleClass, id);
80
+ return id;
81
+ }
64
82
  processModule(moduleClassOrDynamic) {
65
83
  // Handle dynamic modules ({ module, imports, controllers, providers })
66
84
  let moduleClass;
@@ -103,8 +121,12 @@ export class ModuleLoader {
103
121
  throw new Error(`Failed to get module metadata for ${moduleClass.name}`);
104
122
  }
105
123
  this.processingStack.add(moduleClass);
124
+ const moduleId = this.getModuleId(moduleClass);
106
125
  try {
107
126
  const importedProviders = new Set(this.globalExports);
127
+ // Determine importedModuleIds eagerly (before recursing) so child
128
+ // modules can be referenced in our scope's importedModules set.
129
+ const importedModuleIds = new Set();
108
130
  for (const entry of [
109
131
  ...metadata.imports,
110
132
  ...extraImports
@@ -113,6 +135,7 @@ export class ModuleLoader {
113
135
  const isForwardRef = entry instanceof ForwardRef;
114
136
  const importedModule = isForwardRef ? entry.factory() : entry;
115
137
  const importedModuleClass = isDynamicModule(importedModule) ? importedModule.module : importedModule;
138
+ importedModuleIds.add(this.getModuleId(importedModuleClass));
116
139
  // If this forwardRef-wrapped import is currently being processed, skip it to
117
140
  // break the circular chain. Non-forwardRef circular imports still throw.
118
141
  if (isForwardRef && this.processingStack.has(importedModuleClass)) {
@@ -123,19 +146,42 @@ export class ModuleLoader {
123
146
  importedProviders.add(token);
124
147
  }
125
148
  }
126
- // Register metadata providers + dynamic module extra providers
127
149
  const allProviders = [
128
150
  ...metadata.providers,
129
151
  ...extraProviders
130
152
  ];
131
- for (const provider of allProviders){
132
- this.registerProvider(provider);
133
- }
134
- // Collect metadata controllers + dynamic module extra controllers
135
153
  const allControllers = [
136
154
  ...metadata.controllers,
137
155
  ...extraControllers
138
156
  ];
157
+ const allExports = [
158
+ ...metadata.exports,
159
+ ...extraExports
160
+ ];
161
+ // Build the ModuleScope BEFORE registering providers so the visibility
162
+ // check sees the local-provider set as we register.
163
+ const localProviders = new Set();
164
+ for (const provider of allProviders){
165
+ const tk = tokenOfProvider(provider);
166
+ if (tk !== undefined) localProviders.add(tk);
167
+ }
168
+ for (const controller of allControllers){
169
+ localProviders.add(controller);
170
+ }
171
+ // The module class itself can be DI-resolved (NestModule.configure path);
172
+ // include it in its own scope.
173
+ localProviders.add(moduleClass);
174
+ const isGlobal = metadata.isGlobal || isDynamicModule(moduleClassOrDynamic) && moduleClassOrDynamic.global === true;
175
+ this.container.registerScope({
176
+ moduleId,
177
+ localProviders,
178
+ importedModules: importedModuleIds,
179
+ exportedTokens: new Set(allExports),
180
+ isGlobal
181
+ });
182
+ for (const provider of allProviders){
183
+ this.registerProvider(provider, moduleId);
184
+ }
139
185
  for (const controller of allControllers){
140
186
  this.collectedControllers.add(controller);
141
187
  }
@@ -144,13 +190,8 @@ export class ModuleLoader {
144
190
  MetadataRegistry.propagateControllerComponents(moduleClass, controller);
145
191
  }
146
192
  this.processedModules.add(moduleClass);
147
- const allExports = [
148
- ...metadata.exports,
149
- ...extraExports
150
- ];
151
193
  const exports = this.buildExportSet(allExports, allProviders, importedProviders);
152
194
  this.moduleExportsCache.set(moduleClass, exports);
153
- const isGlobal = metadata.isGlobal || isDynamicModule(moduleClassOrDynamic) && moduleClassOrDynamic.global === true;
154
195
  if (isGlobal) {
155
196
  for (const token of exports){
156
197
  this.globalExports.add(token);
@@ -160,9 +201,9 @@ export class ModuleLoader {
160
201
  // resolved through the container so it can have its own DI dependencies.
161
202
  if (implementsNestModule(moduleClass)) {
162
203
  if (!this.container.has(moduleClass)) {
163
- this.container.register(moduleClass);
204
+ this.container.register(moduleClass, moduleId);
164
205
  }
165
- const instance = this.container.resolve(moduleClass);
206
+ const instance = this.container.resolve(moduleClass, moduleId);
166
207
  const builder = new MiddlewareBuilder();
167
208
  instance.configure(builder);
168
209
  this.consumerMiddlewareDefinitions.push(...builder.getDefinitions());
@@ -172,16 +213,16 @@ export class ModuleLoader {
172
213
  this.processingStack.delete(moduleClass);
173
214
  }
174
215
  }
175
- registerProvider(provider) {
216
+ registerProvider(provider, moduleId) {
176
217
  if (typeof provider === "function") {
177
218
  if (!this.container.has(provider)) {
178
- this.container.register(provider);
219
+ this.container.register(provider, moduleId);
179
220
  this.registeredProviders.push(provider);
180
221
  }
181
222
  } else {
182
223
  const token = provider.provide;
183
224
  if (!token) {
184
- this.container.register(provider);
225
+ this.container.register(provider, moduleId);
185
226
  return;
186
227
  }
187
228
  if (this.isAppToken(token)) {
@@ -189,13 +230,16 @@ export class ModuleLoader {
189
230
  this.container.register({
190
231
  ...provider,
191
232
  provide: syntheticToken
192
- });
233
+ }, moduleId);
234
+ // Synthetic APP_* tokens are resolved at request time by RouteManager
235
+ // with no requester; mark them global so the visibility check passes.
236
+ this.container.markGlobalToken(syntheticToken);
193
237
  this.registeredProviders.push(syntheticToken);
194
238
  getOrCreateArray(this.appProviderTokens, token).push(syntheticToken);
195
239
  return;
196
240
  }
197
241
  if (!this.container.has(token)) {
198
- this.container.register(provider);
242
+ this.container.register(provider, moduleId);
199
243
  this.registeredProviders.push(token);
200
244
  }
201
245
  }
@@ -207,7 +251,7 @@ export class ModuleLoader {
207
251
  const exportSet = new Set();
208
252
  const providerTokens = new Set();
209
253
  for (const p of providers){
210
- const token = typeof p === "function" ? p : p.provide;
254
+ const token = tokenOfProvider(p);
211
255
  if (token !== undefined) providerTokens.add(token);
212
256
  }
213
257
  for (const exported of exports){
@@ -250,8 +294,11 @@ export class ModuleLoader {
250
294
  }
251
295
  const instance = await this.container.resolveAsync(token);
252
296
  instanceSet.add(instance);
253
- } catch {
254
- // Skip unresolvable tokens
297
+ } catch (err) {
298
+ // Module visibility errors must always propagate — they indicate a
299
+ // genuine wiring bug, not something to swallow as a discovery skip.
300
+ if (err instanceof ModuleVisibilityError) throw err;
301
+ this.routeError(err, `resolve provider`);
255
302
  }
256
303
  }
257
304
  for (const controller of this.collectedControllers){
@@ -261,12 +308,22 @@ export class ModuleLoader {
261
308
  }
262
309
  const instance = await this.container.resolveAsync(controller);
263
310
  instanceSet.add(instance);
264
- } catch {
265
- // Skip unresolvable controllers
311
+ } catch (err) {
312
+ if (err instanceof ModuleVisibilityError) throw err;
313
+ this.routeError(err, `resolve controller ${controller.name}`);
266
314
  }
267
315
  }
268
316
  return [
269
317
  ...instanceSet
270
318
  ];
271
319
  }
320
+ routeError(err, context) {
321
+ const mode = this.container.getDiagnostics();
322
+ if (mode === "silent") return;
323
+ if (mode === "throw") {
324
+ throw err instanceof Error ? err : new Error(String(err));
325
+ }
326
+ // 'log'
327
+ console.warn(`[vela] ${context} failed:`, err);
328
+ }
272
329
  }
@@ -0,0 +1,31 @@
1
+ import { InjectionToken } from '../container/types';
2
+ import type { Type } from '../container/types';
3
+ import type { DynamicModule } from '../registry/types';
4
+ export interface Plugin {
5
+ id: string;
6
+ version: string;
7
+ module: Type | DynamicModule;
8
+ dependsOn?: string[];
9
+ metadata?: Record<string, unknown>;
10
+ }
11
+ /**
12
+ * Freeze and validate a plugin manifest. Returns the same object so it can
13
+ * be used in `composePlugins([definePlugin(...), ...])`.
14
+ */
15
+ export declare function definePlugin(plugin: Plugin): Plugin;
16
+ export declare class PluginRegistry {
17
+ private byId;
18
+ constructor(plugins: readonly Plugin[]);
19
+ list(): Plugin[];
20
+ get(id: string): Plugin | undefined;
21
+ dependents(id: string): Plugin[];
22
+ }
23
+ export declare const PLUGIN_REGISTRY_TOKEN: InjectionToken<PluginRegistry>;
24
+ export declare class PluginRootModule {
25
+ }
26
+ /**
27
+ * Compose a list of plugins into a single dynamic module that imports each
28
+ * plugin's module in dependency order, registers a `PluginRegistry`, and
29
+ * exposes it globally so any plugin can introspect peers.
30
+ */
31
+ export declare function composePlugins(plugins: readonly Plugin[]): DynamicModule;
@@ -0,0 +1,111 @@
1
+ function _ts_decorate(decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for(var i = decorators.length - 1; i >= 0; i--)if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ }
7
+ import { InjectionToken } from "../container/types.js";
8
+ import { Module } from "../module/decorators.js";
9
+ /**
10
+ * Freeze and validate a plugin manifest. Returns the same object so it can
11
+ * be used in `composePlugins([definePlugin(...), ...])`.
12
+ */ export function definePlugin(plugin) {
13
+ if (!plugin.id) throw new Error('Plugin requires an id');
14
+ if (!plugin.version) throw new Error('Plugin requires a version');
15
+ if (!plugin.module) throw new Error('Plugin requires a module');
16
+ return Object.freeze({
17
+ ...plugin
18
+ });
19
+ }
20
+ export class PluginRegistry {
21
+ byId;
22
+ constructor(plugins){
23
+ this.byId = new Map();
24
+ for (const p of plugins){
25
+ if (this.byId.has(p.id)) {
26
+ throw new Error(`Duplicate plugin id: '${p.id}'`);
27
+ }
28
+ this.byId.set(p.id, p);
29
+ }
30
+ }
31
+ list() {
32
+ return [
33
+ ...this.byId.values()
34
+ ];
35
+ }
36
+ get(id) {
37
+ return this.byId.get(id);
38
+ }
39
+ dependents(id) {
40
+ return this.list().filter((p)=>p.dependsOn?.includes(id) ?? false);
41
+ }
42
+ }
43
+ export const PLUGIN_REGISTRY_TOKEN = new InjectionToken('PLUGIN_REGISTRY');
44
+ export class PluginRootModule {
45
+ }
46
+ PluginRootModule = _ts_decorate([
47
+ Module({})
48
+ ], PluginRootModule);
49
+ /**
50
+ * Topologically sort plugins so each plugin's dependencies appear first.
51
+ * Throws on missing dependencies and cycles.
52
+ */ function topologicalSort(plugins) {
53
+ const byId = new Map();
54
+ for (const p of plugins){
55
+ if (byId.has(p.id)) {
56
+ throw new Error(`Duplicate plugin id: '${p.id}'`);
57
+ }
58
+ byId.set(p.id, p);
59
+ }
60
+ const result = [];
61
+ const visiting = new Set();
62
+ const visited = new Set();
63
+ const visit = (p, path)=>{
64
+ if (visited.has(p.id)) return;
65
+ if (visiting.has(p.id)) {
66
+ const cycle = [
67
+ ...path,
68
+ p.id
69
+ ].join(' -> ');
70
+ throw new Error(`Plugin cycle detected: ${cycle}`);
71
+ }
72
+ visiting.add(p.id);
73
+ for (const depId of p.dependsOn ?? []){
74
+ const dep = byId.get(depId);
75
+ if (!dep) {
76
+ throw new Error(`Plugin '${p.id}' depends on missing plugin '${depId}'`);
77
+ }
78
+ visit(dep, [
79
+ ...path,
80
+ p.id
81
+ ]);
82
+ }
83
+ visiting.delete(p.id);
84
+ visited.add(p.id);
85
+ result.push(p);
86
+ };
87
+ for (const p of plugins)visit(p, []);
88
+ return result;
89
+ }
90
+ /**
91
+ * Compose a list of plugins into a single dynamic module that imports each
92
+ * plugin's module in dependency order, registers a `PluginRegistry`, and
93
+ * exposes it globally so any plugin can introspect peers.
94
+ */ export function composePlugins(plugins) {
95
+ const sorted = topologicalSort(plugins);
96
+ const registry = new PluginRegistry(sorted);
97
+ return {
98
+ module: PluginRootModule,
99
+ imports: sorted.map((p)=>p.module),
100
+ providers: [
101
+ {
102
+ provide: PLUGIN_REGISTRY_TOKEN,
103
+ useValue: registry
104
+ }
105
+ ],
106
+ exports: [
107
+ PLUGIN_REGISTRY_TOKEN
108
+ ],
109
+ global: true
110
+ };
111
+ }
@@ -265,6 +265,5 @@ export class MetadataRegistry {
265
265
  this.handlerComponents[type].clear();
266
266
  }
267
267
  this.globalComponents = emptyComponentStore();
268
- // reflectMeta is a WeakMap — entries die with their targets.
269
268
  }
270
269
  }
@@ -33,7 +33,12 @@ export class ScheduleRegistry {
33
33
  let instance;
34
34
  try {
35
35
  instance = this.container.resolve(token);
36
- } catch {
36
+ } catch (err) {
37
+ const mode = this.container.getDiagnostics();
38
+ if (mode === 'throw') throw err;
39
+ if (mode === 'log') {
40
+ console.warn(`[vela] schedule discovery: cannot resolve ${token.name || String(token)}:`, err);
41
+ }
37
42
  continue;
38
43
  }
39
44
  if (cronMeta) {
@@ -1,13 +1,15 @@
1
+ import { Container } from '../container/container';
1
2
  import type { OnApplicationBootstrap, OnModuleDestroy } from '../lifecycle/index';
2
3
  import { ScheduleRegistry } from '../schedule/schedule.registry';
3
4
  export declare class ScheduleExecutor implements OnApplicationBootstrap, OnModuleDestroy {
4
5
  private registry;
6
+ private container;
5
7
  private intervalTimers;
6
8
  private cronTimers;
7
9
  private cronMatcherCache;
8
10
  private lastCronMinute;
9
11
  private running;
10
- constructor(registry: ScheduleRegistry);
12
+ constructor(registry: ScheduleRegistry, container: Container);
11
13
  onApplicationBootstrap(): void;
12
14
  private scheduleInterval;
13
15
  private scheduleCron;