@velajs/vela 1.9.0 → 1.12.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.
Files changed (161) hide show
  1. package/CHANGELOG.md +122 -0
  2. package/dist/__tests__/workers-als/entry-als.d.ts +9 -0
  3. package/dist/application.d.ts +18 -0
  4. package/dist/application.js +45 -0
  5. package/dist/cache/cache.module.d.ts +5 -7
  6. package/dist/cache/cache.module.js +44 -66
  7. package/dist/cache/cache.types.d.ts +26 -0
  8. package/dist/cache/cache.types.js +1 -1
  9. package/dist/cache/index.d.ts +2 -1
  10. package/dist/cache/index.js +1 -0
  11. package/dist/cache/tiered-cache.store.d.ts +19 -0
  12. package/dist/cache/tiered-cache.store.js +40 -0
  13. package/dist/config/config.module.d.ts +6 -5
  14. package/dist/config/config.module.js +46 -49
  15. package/dist/container/container.d.ts +58 -0
  16. package/dist/container/container.js +223 -18
  17. package/dist/container/disposable.d.ts +5 -0
  18. package/dist/container/disposable.js +33 -0
  19. package/dist/container/types.d.ts +8 -0
  20. package/dist/cors/cors.module.d.ts +5 -5
  21. package/dist/cors/cors.module.js +26 -25
  22. package/dist/discovery/discoverable.decorator.d.ts +38 -0
  23. package/dist/discovery/discoverable.decorator.js +42 -0
  24. package/dist/discovery/discovery.service.d.ts +94 -0
  25. package/dist/discovery/discovery.service.js +166 -0
  26. package/dist/discovery/index.d.ts +2 -0
  27. package/dist/discovery/index.js +2 -0
  28. package/dist/entrypoint/entrypoint.registry.d.ts +40 -0
  29. package/dist/entrypoint/entrypoint.registry.js +106 -0
  30. package/dist/entrypoint/entrypoint.types.d.ts +41 -0
  31. package/dist/entrypoint/entrypoint.types.js +3 -0
  32. package/dist/entrypoint/execution-context.d.ts +15 -0
  33. package/dist/entrypoint/execution-context.js +21 -0
  34. package/dist/entrypoint/execution-scope.d.ts +21 -0
  35. package/dist/entrypoint/execution-scope.js +28 -0
  36. package/dist/entrypoint/index.d.ts +4 -0
  37. package/dist/entrypoint/index.js +4 -0
  38. package/dist/event-emitter/event-emitter.subscriber.d.ts +3 -3
  39. package/dist/event-emitter/event-emitter.subscriber.js +12 -29
  40. package/dist/factory/adapter.d.ts +36 -0
  41. package/dist/factory/adapter.js +18 -0
  42. package/dist/factory/bootstrap.js +20 -2
  43. package/dist/factory.d.ts +11 -1
  44. package/dist/factory.js +23 -2
  45. package/dist/fetch/fetch.module.d.ts +5 -6
  46. package/dist/fetch/fetch.module.js +9 -42
  47. package/dist/http/ambient.d.ts +17 -0
  48. package/dist/http/ambient.js +44 -0
  49. package/dist/http/execution-context.js +4 -1
  50. package/dist/http/handler-executor.js +22 -19
  51. package/dist/http/index.d.ts +1 -0
  52. package/dist/http/index.js +1 -0
  53. package/dist/http/route-contributor.d.ts +68 -0
  54. package/dist/http/route-contributor.js +22 -0
  55. package/dist/http/route.manager.d.ts +19 -0
  56. package/dist/http/route.manager.js +112 -19
  57. package/dist/i18n/deep-merge.d.ts +2 -0
  58. package/dist/i18n/deep-merge.js +15 -0
  59. package/dist/i18n/i18n.middleware.d.ts +15 -0
  60. package/dist/i18n/i18n.middleware.js +94 -0
  61. package/dist/i18n/i18n.module.d.ts +13 -0
  62. package/dist/i18n/i18n.module.js +64 -0
  63. package/dist/i18n/i18n.options.d.ts +30 -0
  64. package/dist/i18n/i18n.options.js +16 -0
  65. package/dist/i18n/i18n.service.d.ts +20 -0
  66. package/dist/i18n/i18n.service.js +49 -0
  67. package/dist/i18n/i18n.tokens.d.ts +6 -0
  68. package/dist/i18n/i18n.tokens.js +3 -0
  69. package/dist/i18n/i18n.types.d.ts +6 -0
  70. package/dist/i18n/i18n.types.js +1 -0
  71. package/dist/i18n/index.d.ts +10 -0
  72. package/dist/i18n/index.js +8 -0
  73. package/dist/i18n/message-loader.service.d.ts +21 -0
  74. package/dist/i18n/message-loader.service.js +90 -0
  75. package/dist/i18n/message-registry.d.ts +16 -0
  76. package/dist/i18n/message-registry.js +49 -0
  77. package/dist/index.d.ts +17 -5
  78. package/dist/index.js +14 -3
  79. package/dist/internal.d.ts +0 -2
  80. package/dist/internal.js +0 -7
  81. package/dist/module/configurable-module.builder.d.ts +56 -0
  82. package/dist/module/configurable-module.builder.js +84 -0
  83. package/dist/module/configurable-module.types.d.ts +93 -0
  84. package/dist/module/configurable-module.types.js +5 -0
  85. package/dist/module/define-module.d.ts +105 -0
  86. package/dist/module/define-module.js +227 -0
  87. package/dist/module/index.d.ts +4 -0
  88. package/dist/module/index.js +3 -0
  89. package/dist/module/lazy-provider.d.ts +65 -0
  90. package/dist/module/lazy-provider.js +111 -0
  91. package/dist/openapi/document.js +16 -15
  92. package/dist/pipeline/component.manager.d.ts +15 -9
  93. package/dist/pipeline/component.manager.js +32 -46
  94. package/dist/pipeline/index.d.ts +2 -0
  95. package/dist/pipeline/index.js +1 -0
  96. package/dist/pipeline/pipeline-runner.d.ts +31 -0
  97. package/dist/pipeline/pipeline-runner.js +40 -0
  98. package/dist/pipeline/types.d.ts +21 -4
  99. package/dist/registry/metadata.registry.d.ts +24 -18
  100. package/dist/registry/metadata.registry.js +137 -49
  101. package/dist/schedule/schedule.module.d.ts +5 -0
  102. package/dist/schedule/schedule.module.js +18 -8
  103. package/dist/schedule/schedule.registry.d.ts +3 -3
  104. package/dist/schedule/schedule.registry.js +23 -44
  105. package/dist/schedule-node/schedule-node.module.d.ts +5 -0
  106. package/dist/schedule-node/schedule-node.module.js +20 -10
  107. package/dist/seeder/index.d.ts +5 -0
  108. package/dist/seeder/index.js +4 -0
  109. package/dist/seeder/seeder.decorator.d.ts +7 -0
  110. package/dist/seeder/seeder.decorator.js +15 -0
  111. package/dist/seeder/seeder.module.d.ts +16 -0
  112. package/dist/seeder/seeder.module.js +41 -0
  113. package/dist/seeder/seeder.registry.d.ts +23 -0
  114. package/dist/seeder/seeder.registry.js +83 -0
  115. package/dist/seeder/seeder.tokens.d.ts +2 -0
  116. package/dist/seeder/seeder.tokens.js +1 -0
  117. package/dist/seeder/seeder.types.d.ts +20 -0
  118. package/dist/seeder/seeder.types.js +1 -0
  119. package/dist/storage/index.d.ts +4 -0
  120. package/dist/storage/index.js +2 -0
  121. package/dist/storage/path-template.d.ts +9 -0
  122. package/dist/storage/path-template.js +20 -0
  123. package/dist/storage/signed-url.d.ts +12 -0
  124. package/dist/storage/signed-url.js +55 -0
  125. package/dist/storage/storage.types.d.ts +41 -0
  126. package/dist/storage/storage.types.js +5 -0
  127. package/dist/throttler/throttler.module.d.ts +5 -8
  128. package/dist/throttler/throttler.module.js +27 -55
  129. package/dist/websocket/index.d.ts +12 -0
  130. package/dist/websocket/index.js +16 -0
  131. package/dist/websocket/websocket.decorators.d.ts +33 -0
  132. package/dist/websocket/websocket.decorators.js +58 -0
  133. package/dist/websocket/websocket.module.d.ts +35 -0
  134. package/dist/websocket/websocket.module.js +63 -0
  135. package/dist/websocket/websocket.tokens.d.ts +23 -0
  136. package/dist/websocket/websocket.tokens.js +21 -0
  137. package/dist/websocket/websocket.types.d.ts +86 -0
  138. package/dist/websocket/websocket.types.js +4 -0
  139. package/dist/websocket/ws-argument-resolver.d.ts +13 -0
  140. package/dist/websocket/ws-argument-resolver.js +40 -0
  141. package/dist/websocket/ws-dispatcher.d.ts +45 -0
  142. package/dist/websocket/ws-dispatcher.js +259 -0
  143. package/dist/websocket/ws-exception.d.ts +16 -0
  144. package/dist/websocket/ws-exception.js +32 -0
  145. package/dist/websocket/ws-execution-context.d.ts +9 -0
  146. package/dist/websocket/ws-execution-context.js +24 -0
  147. package/dist/websocket/ws-server.d.ts +31 -0
  148. package/dist/websocket/ws-server.js +74 -0
  149. package/dist/websocket/ws-sync.d.ts +47 -0
  150. package/dist/websocket/ws-sync.js +74 -0
  151. package/dist/websocket-node/index.d.ts +4 -0
  152. package/dist/websocket-node/index.js +5 -0
  153. package/dist/websocket-node/node-ws-client.d.ts +24 -0
  154. package/dist/websocket-node/node-ws-client.js +49 -0
  155. package/dist/websocket-node/redis-sync.d.ts +33 -0
  156. package/dist/websocket-node/redis-sync.js +56 -0
  157. package/dist/websocket-node/register-gateways.d.ts +20 -0
  158. package/dist/websocket-node/register-gateways.js +61 -0
  159. package/package.json +31 -1
  160. package/dist/http/crud-bridge.d.ts +0 -90
  161. package/dist/http/crud-bridge.js +0 -25
@@ -1,10 +1,11 @@
1
1
  import { Hono } from "hono";
2
+ import { contextStorage } from "hono/context-storage";
2
3
  import { HttpMethod } from "../constants.js";
3
4
  import { HttpException } from "../errors/http-exception.js";
4
5
  import { getMetadata } from "../metadata.js";
5
6
  import { joinPaths } from "../registry/paths.js";
6
7
  import { ArgumentResolver } from "./argument-resolver.js";
7
- import { getCrudBridge } from "./crud-bridge.js";
8
+ import { getRouteContributors } from "./route-contributor.js";
8
9
  import { buildMiddlewareExecutionContext } from "./execution-context.js";
9
10
  import { HandlerExecutor } from "./handler-executor.js";
10
11
  import { instantiate, instantiateMany } from "./instantiate.js";
@@ -14,6 +15,39 @@ import { ComponentManager } from "../pipeline/component.manager.js";
14
15
  import { shouldFilterCatch } from "../pipeline/decorators.js";
15
16
  import { MetadataRegistry } from "../registry/metadata.registry.js";
16
17
  const defaultGetClientIp = (c)=>c.req.raw.headers.get('x-forwarded-for')?.split(',')[0]?.trim() ?? c.req.raw.headers.get('x-real-ip') ?? null;
18
+ // Mirror a response body stream, invoking `onDone` exactly once when it is fully
19
+ // read, errors, or is cancelled. Lets request-scoped resources be disposed only
20
+ // after the body has drained — never mid-stream.
21
+ function disposeStreamWhenDone(body, onDone) {
22
+ const reader = body.getReader();
23
+ let finished = false;
24
+ const finish = ()=>{
25
+ if (!finished) {
26
+ finished = true;
27
+ onDone();
28
+ }
29
+ };
30
+ return new ReadableStream({
31
+ async pull (controller) {
32
+ try {
33
+ const { done, value } = await reader.read();
34
+ if (done) {
35
+ controller.close();
36
+ finish();
37
+ return;
38
+ }
39
+ controller.enqueue(value);
40
+ } catch (error) {
41
+ controller.error(error);
42
+ finish();
43
+ }
44
+ },
45
+ cancel (reason) {
46
+ finish();
47
+ return reader.cancel(reason);
48
+ }
49
+ });
50
+ }
17
51
  export class RouteManager {
18
52
  container;
19
53
  static METHOD_REGISTRAR = new Map([
@@ -59,8 +93,10 @@ export class RouteManager {
59
93
  globalPrefix = '';
60
94
  consumerMiddlewareDefinitions = [];
61
95
  handlerExecutor;
96
+ ambientContainer;
62
97
  constructor(container, options = {}){
63
98
  this.container = container;
99
+ this.ambientContainer = options.ambientContainer ?? false;
64
100
  const argumentResolver = new ArgumentResolver(options.getClientIp ?? defaultGetClientIp);
65
101
  this.handlerExecutor = new HandlerExecutor(argumentResolver, ()=>({
66
102
  guards: this.globalGuards,
@@ -69,6 +105,19 @@ export class RouteManager {
69
105
  filters: this.globalFilters
70
106
  }), (c)=>this.getRequestContainer(c));
71
107
  }
108
+ /**
109
+ * The global-tier components (`APP_*` provider tokens + imperative
110
+ * `app.useGlobalX()` instances). Exposed so non-HTTP transports (the
111
+ * WebSocket dispatcher) can apply the same app-wide guards/pipes/interceptors/
112
+ * filters as HTTP routes, read live so late registrations propagate.
113
+ */ getGlobalComponents() {
114
+ return {
115
+ guards: this.globalGuards,
116
+ pipes: this.globalPipes,
117
+ interceptors: this.globalInterceptors,
118
+ filters: this.globalFilters
119
+ };
120
+ }
72
121
  registerConsumerMiddleware(definitions) {
73
122
  this.consumerMiddlewareDefinitions.push(...definitions);
74
123
  return this;
@@ -221,6 +270,41 @@ export class RouteManager {
221
270
  }
222
271
  async build() {
223
272
  const app = new Hono();
273
+ // Outermost: dispose the per-request child container once the request is
274
+ // fully done. Fast-paths out when the child has no request-scoped
275
+ // disposables (the common case → zero overhead / no behavior change).
276
+ // Streaming-safe: when a response body is present, disposal is deferred
277
+ // until the body is fully read (or errors/cancels), never mid-stream.
278
+ app.use('*', async (c, next)=>{
279
+ let threw = false;
280
+ try {
281
+ await next();
282
+ } catch (error) {
283
+ threw = true;
284
+ throw error;
285
+ } finally{
286
+ const child = c.get('container');
287
+ if (child?.hasDisposables()) {
288
+ const body = threw ? null : c.res?.body ?? null;
289
+ if (body) {
290
+ // Defer disposal to when the runtime finishes reading the body.
291
+ c.res = new Response(disposeStreamWhenDone(body, ()=>void child.dispose()), {
292
+ status: c.res.status,
293
+ statusText: c.res.statusText,
294
+ headers: c.res.headers
295
+ });
296
+ } else {
297
+ // No body (or error path) — nothing streaming, dispose now.
298
+ await child.dispose();
299
+ }
300
+ }
301
+ }
302
+ });
303
+ // Opt-in ambient container: contextStorage() must be an early middleware so
304
+ // getContext() is available to everything downstream.
305
+ if (this.ambientContainer) {
306
+ app.use('*', contextStorage());
307
+ }
224
308
  // Sort global middleware by priority (lower runs first) with insertion
225
309
  // index as tiebreaker so equal priorities preserve registration order.
226
310
  const sortedGlobal = this.globalMiddleware.map((entry, index)=>({
@@ -264,7 +348,9 @@ export class RouteManager {
264
348
  for (const route of routes){
265
349
  const effectiveVersion = route.version ?? metadata.version;
266
350
  const versionedPaths = this.buildVersionedPaths(this.globalPrefix, metadata.prefix, route.path, effectiveVersion);
267
- const middlewareItems = ComponentManager.getComponents('middleware', controller, route.handlerName);
351
+ // Scoped (controller/handler) middleware only — global middleware is
352
+ // applied once by this manager's own global pass, never re-read here.
353
+ const middlewareItems = ComponentManager.getScopedComponents('middleware', controller, route.handlerName);
268
354
  const handler = this.handlerExecutor.create(route, controller, allParamMetadata);
269
355
  for (const fullPath of versionedPaths){
270
356
  for (const middlewareItem of middlewareItems){
@@ -279,26 +365,33 @@ export class RouteManager {
279
365
  }
280
366
  }
281
367
  }
282
- // Second pass: mount CRUD sub-apps (/:id routes registered last).
283
- //
284
- // Routes are produced by a registered CrudBridge — usually contributed
285
- // by `@velajs/crud` as an import side effect. Inversion was deliberate:
286
- // a previous implementation called `await import('@velajs/crud')` via
287
- // an indirect variable to dodge esbuild's static analysis, which made
288
- // Cloudflare Workers unable to resolve the module at runtime. The
289
- // bridge contract removes that hazard — no runtime resolver involved.
368
+ // Second pass: route contributors (generated routes registered last, so a
369
+ // contributor's `/:id` catch-alls never shadow explicit routes). Each
370
+ // contributor claims controllers by class-level metadata; `@velajs/crud`
371
+ // registers one as an import side effect. The registry contract (instead
372
+ // of dynamic `import()`) exists because esbuild leaves
373
+ // `await import(variable)` as a runtime import Cloudflare Workers cannot
374
+ // resolve.
375
+ const contributors = getRouteContributors();
290
376
  for (const { controller, metadata } of this.controllers){
291
- const crudConfig = getMetadata('vela:crud', controller);
292
- if (!crudConfig) continue;
293
- const bridge = getCrudBridge();
294
- if (!bridge) {
377
+ for (const contributor of contributors){
378
+ const meta = getMetadata(contributor.claimsMetaKey, controller);
379
+ if (meta === undefined) continue;
380
+ await contributor.buildRoutes(app, {
381
+ controller,
382
+ controllerPrefix: metadata.prefix,
383
+ meta,
384
+ globalPrefix: this.globalPrefix,
385
+ globalGuards: instantiateMany(this.globalGuards, this.container),
386
+ container: this.container,
387
+ joinPaths
388
+ });
389
+ }
390
+ // DX guard for the common mistake: @Crud() metadata present but the
391
+ // contributor package never imported.
392
+ if (getMetadata('vela:crud', controller) !== undefined && !contributors.some((c)=>c.claimsMetaKey === 'vela:crud')) {
295
393
  throw new Error("@Crud() requires '@velajs/crud'. Install it: pnpm add @velajs/crud");
296
394
  }
297
- await bridge.buildRoutes(app, controller, metadata.prefix, crudConfig, {
298
- globalPrefix: this.globalPrefix,
299
- globalGuards: instantiateMany(this.globalGuards, this.container),
300
- joinPaths
301
- });
302
395
  }
303
396
  return app;
304
397
  }
@@ -0,0 +1,2 @@
1
+ /** Deep-merge two message trees; source overrides target at the leaf level. */
2
+ export declare function deepMerge(target: Record<string, unknown>, source: Record<string, unknown>): Record<string, unknown>;
@@ -0,0 +1,15 @@
1
+ /** Deep-merge two message trees; source overrides target at the leaf level. */ export function deepMerge(target, source) {
2
+ const result = {
3
+ ...target
4
+ };
5
+ for (const key of Object.keys(source)){
6
+ const targetValue = target[key];
7
+ const sourceValue = source[key];
8
+ if (typeof targetValue === 'object' && targetValue !== null && !Array.isArray(targetValue) && typeof sourceValue === 'object' && sourceValue !== null && !Array.isArray(sourceValue)) {
9
+ result[key] = deepMerge(targetValue, sourceValue);
10
+ } else {
11
+ result[key] = sourceValue;
12
+ }
13
+ }
14
+ return result;
15
+ }
@@ -0,0 +1,15 @@
1
+ import type { Context, Next } from 'hono';
2
+ import type { NestMiddleware } from '../pipeline/types';
3
+ import { type I18nModuleOptions } from './i18n.options';
4
+ /**
5
+ * Detects the request locale and stashes it on the per-request RequestContext
6
+ * bag under {@link I18N_LOCALE_KEY}, where {@link I18nService} reads it.
7
+ * Registered globally via APP_MIDDLEWARE by {@link I18nModule}. Holds no
8
+ * per-request state (reads the RequestContext off the request container each
9
+ * call), so a singleton instance is safe.
10
+ */
11
+ export declare class I18nLocaleMiddleware implements NestMiddleware {
12
+ private readonly resolved;
13
+ constructor(options: I18nModuleOptions);
14
+ use(c: Context, next: Next): Promise<Response | void>;
15
+ }
@@ -0,0 +1,94 @@
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
+ function _ts_metadata(k, v) {
8
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
9
+ }
10
+ function _ts_param(paramIndex, decorator) {
11
+ return function(target, key) {
12
+ decorator(target, key, paramIndex);
13
+ };
14
+ }
15
+ import { Inject, Injectable } from "../container/decorators.js";
16
+ import { REQUEST_CONTEXT } from "../http/request-context.js";
17
+ import { resolveI18nOptions } from "./i18n.options.js";
18
+ import { I18N_LOCALE_KEY, I18N_OPTIONS } from "./i18n.tokens.js";
19
+ export class I18nLocaleMiddleware {
20
+ resolved;
21
+ constructor(options){
22
+ this.resolved = resolveI18nOptions(options);
23
+ }
24
+ async use(c, next) {
25
+ const container = c.get('container');
26
+ if (container) {
27
+ const ctx = container.resolve(REQUEST_CONTEXT);
28
+ ctx.set(I18N_LOCALE_KEY, detectLocale(c, this.resolved));
29
+ }
30
+ return next();
31
+ }
32
+ }
33
+ I18nLocaleMiddleware = _ts_decorate([
34
+ Injectable(),
35
+ _ts_param(0, Inject(I18N_OPTIONS)),
36
+ _ts_metadata("design:type", Function),
37
+ _ts_metadata("design:paramtypes", [
38
+ typeof I18nModuleOptions === "undefined" ? Object : I18nModuleOptions
39
+ ])
40
+ ], I18nLocaleMiddleware);
41
+ function detectLocale(c, resolved) {
42
+ if (!resolved.detection.enabled) return resolved.defaultLocale;
43
+ const { strategy, lookupKey } = resolved.detection;
44
+ // Candidate tags in preference order.
45
+ const candidates = strategy === 'query' ? [
46
+ new URL(c.req.url).searchParams.get(lookupKey) ?? ''
47
+ ] : strategy === 'cookie' ? [
48
+ readCookie(c.req.raw.headers.get('cookie'), lookupKey) ?? ''
49
+ ] : parseAcceptLanguage(c.req.raw.headers.get('accept-language'));
50
+ for (const candidate of candidates){
51
+ const match = matchLocale(candidate, resolved.locales);
52
+ if (match) return match;
53
+ }
54
+ return resolved.defaultLocale;
55
+ }
56
+ /** Case-insensitive locale match with region fallback (en-US -> en); returns the configured casing. */ function matchLocale(candidate, locales) {
57
+ if (!candidate) return undefined;
58
+ const lc = candidate.toLowerCase();
59
+ const exact = locales.find((l)=>l.toLowerCase() === lc);
60
+ if (exact) return exact;
61
+ const base = lc.split('-')[0];
62
+ return locales.find((l)=>l.toLowerCase().split('-')[0] === base);
63
+ }
64
+ /** Parse Accept-Language into tags ordered by descending q-value (RFC 7231). */ function parseAcceptLanguage(header) {
65
+ if (!header) return [];
66
+ return header.split(',').map((part)=>{
67
+ const [tag, ...params] = part.trim().split(';');
68
+ let q = 1;
69
+ for (const p of params){
70
+ const m = p.trim().match(/^q=([\d.]+)$/);
71
+ if (m) q = Number.parseFloat(m[1]);
72
+ }
73
+ return {
74
+ tag: (tag ?? '').trim(),
75
+ q: Number.isNaN(q) ? 0 : q
76
+ };
77
+ }).filter((e)=>e.tag && e.tag !== '*').sort((a, b)=>b.q - a.q).map((e)=>e.tag);
78
+ }
79
+ function readCookie(header, name) {
80
+ if (!header) return undefined;
81
+ for (const part of header.split(';')){
82
+ const [rawKey, ...rest] = part.split('=');
83
+ if (rawKey?.trim() === name) {
84
+ const raw = rest.join('=').trim();
85
+ try {
86
+ return decodeURIComponent(raw);
87
+ } catch {
88
+ // Malformed percent-encoding must not crash locale detection.
89
+ return raw;
90
+ }
91
+ }
92
+ }
93
+ return undefined;
94
+ }
@@ -0,0 +1,13 @@
1
+ import type { DynamicModule } from '../module/types';
2
+ import type { I18nModuleOptions } from './i18n.options';
3
+ declare const ConfigurableModuleClass: import("..").ConfigurableModuleClassType<I18nModuleOptions, "forRoot", "create", {
4
+ isGlobal?: boolean;
5
+ }>;
6
+ export declare class I18nModule extends ConfigurableModuleClass {
7
+ /**
8
+ * forFeature-style: contribute a message tree (`{ [locale]: { ...messages } }`).
9
+ * Contributions are collected statically at module-load and deep-merged.
10
+ */
11
+ static registerMessages(messages: Record<string, Record<string, unknown>>): DynamicModule;
12
+ }
13
+ export {};
@@ -0,0 +1,64 @@
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 { Module } from "../module/decorators.js";
8
+ import { ConfigurableModuleBuilder } from "../module/configurable-module.builder.js";
9
+ import { stableHash } from "../module/stable-hash.js";
10
+ import { APP_MIDDLEWARE } from "../pipeline/tokens.js";
11
+ import { I18nLocaleMiddleware } from "./i18n.middleware.js";
12
+ import { I18nService } from "./i18n.service.js";
13
+ import { I18N_OPTIONS } from "./i18n.tokens.js";
14
+ import { MessageLoaderService } from "./message-loader.service.js";
15
+ import { MessageRegistry } from "./message-registry.js";
16
+ const { ConfigurableModuleClass } = new ConfigurableModuleBuilder({
17
+ moduleName: 'I18n',
18
+ optionsInjectionToken: I18N_OPTIONS
19
+ }).build();
20
+ // Empty marker module for `registerMessages`. The contribution is a side-effect
21
+ // on the global MessageRegistry; this carries NO providers so it never
22
+ // duplicates I18nModule's providers (which would trip vela's multi-instance
23
+ // encapsulation → MultipleProvidersFoundError). New modules whose contribution
24
+ // is providers (not an external registry) should use `sideEffectModule` from
25
+ // the authoring kit; this predates it and keeps a shared-class identity so
26
+ // identical message trees dedup as one module instance.
27
+ let I18nMessagesModule = class I18nMessagesModule {
28
+ };
29
+ I18nMessagesModule = _ts_decorate([
30
+ Module({})
31
+ ], I18nMessagesModule);
32
+ export class I18nModule extends ConfigurableModuleClass {
33
+ /**
34
+ * forFeature-style: contribute a message tree (`{ [locale]: { ...messages } }`).
35
+ * Contributions are collected statically at module-load and deep-merged.
36
+ */ static registerMessages(messages) {
37
+ MessageRegistry.addMessages(messages);
38
+ return {
39
+ module: I18nMessagesModule,
40
+ key: `messages:${stableHash(messages)}`,
41
+ providers: []
42
+ };
43
+ }
44
+ }
45
+ I18nModule = _ts_decorate([
46
+ Module({
47
+ providers: [
48
+ MessageRegistry,
49
+ MessageLoaderService,
50
+ I18nService,
51
+ I18nLocaleMiddleware,
52
+ // Register the locale middleware globally (NestJS APP_MIDDLEWARE convention).
53
+ {
54
+ provide: APP_MIDDLEWARE,
55
+ useExisting: I18nLocaleMiddleware
56
+ }
57
+ ],
58
+ exports: [
59
+ I18nService,
60
+ MessageLoaderService,
61
+ I18N_OPTIONS
62
+ ]
63
+ })
64
+ ], I18nModule);
@@ -0,0 +1,30 @@
1
+ /** How the request locale is detected. */
2
+ export type DetectionStrategy = 'header' | 'query' | 'cookie';
3
+ export interface I18nModuleOptions {
4
+ /** Default locale. @default 'en' */
5
+ defaultLocale?: string;
6
+ /** Fallback when a translation is missing. @default defaultLocale */
7
+ fallbackLocale?: string;
8
+ /** Supported locales; request locales outside this list fall back to default. @default ['en'] */
9
+ locales?: string[];
10
+ /** Locale detection config. */
11
+ detection?: {
12
+ /** Set false to disable detection (always use defaultLocale). @default true */
13
+ enabled?: boolean;
14
+ /** @default 'header' (Accept-Language) */
15
+ strategy?: DetectionStrategy;
16
+ /** Cookie/query parameter name. @default 'locale' */
17
+ lookupKey?: string;
18
+ };
19
+ }
20
+ export interface ResolvedI18nOptions {
21
+ defaultLocale: string;
22
+ fallbackLocale: string;
23
+ locales: string[];
24
+ detection: {
25
+ enabled: boolean;
26
+ strategy: DetectionStrategy;
27
+ lookupKey: string;
28
+ };
29
+ }
30
+ export declare function resolveI18nOptions(options?: I18nModuleOptions): ResolvedI18nOptions;
@@ -0,0 +1,16 @@
1
+ /** How the request locale is detected. */ export function resolveI18nOptions(options) {
2
+ const detection = options?.detection;
3
+ const defaultLocale = options?.defaultLocale ?? 'en';
4
+ return {
5
+ defaultLocale,
6
+ fallbackLocale: options?.fallbackLocale ?? defaultLocale,
7
+ locales: options?.locales ?? [
8
+ 'en'
9
+ ],
10
+ detection: {
11
+ enabled: detection?.enabled !== false,
12
+ strategy: detection?.strategy ?? 'header',
13
+ lookupKey: detection?.lookupKey ?? 'locale'
14
+ }
15
+ };
16
+ }
@@ -0,0 +1,20 @@
1
+ import { type RequestContext } from '../http/request-context';
2
+ import type { I18nModuleOptions } from './i18n.options';
3
+ import type { II18nService } from './i18n.types';
4
+ import { MessageLoaderService } from './message-loader.service';
5
+ /**
6
+ * Request-scoped translation facade. Reads the locale resolved by
7
+ * {@link I18nLocaleMiddleware} from the per-request RequestContext.
8
+ *
9
+ * Injecting it into a controller/service is safe: vela's request-scope bubbling
10
+ * promotes the consumer to request scope automatically, so no captive-dependency
11
+ * hazard and no `ambientContainer` flag required.
12
+ */
13
+ export declare class I18nService implements II18nService {
14
+ private readonly loader;
15
+ private readonly ctx;
16
+ private readonly options;
17
+ constructor(loader: MessageLoaderService, ctx: RequestContext, options: I18nModuleOptions);
18
+ t(key: string, params?: Record<string, unknown>): string;
19
+ getLocale(): string;
20
+ }
@@ -0,0 +1,49 @@
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
+ function _ts_metadata(k, v) {
8
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
9
+ }
10
+ function _ts_param(paramIndex, decorator) {
11
+ return function(target, key) {
12
+ decorator(target, key, paramIndex);
13
+ };
14
+ }
15
+ import { Scope } from "../constants.js";
16
+ import { Inject, Injectable } from "../container/decorators.js";
17
+ import { REQUEST_CONTEXT } from "../http/request-context.js";
18
+ import { I18N_LOCALE_KEY, I18N_OPTIONS } from "./i18n.tokens.js";
19
+ import { MessageLoaderService } from "./message-loader.service.js";
20
+ export class I18nService {
21
+ loader;
22
+ ctx;
23
+ options;
24
+ constructor(loader, ctx, options){
25
+ this.loader = loader;
26
+ this.ctx = ctx;
27
+ this.options = options;
28
+ }
29
+ t(key, params) {
30
+ return this.loader.translate(this.getLocale(), key, params);
31
+ }
32
+ getLocale() {
33
+ return this.ctx.get(I18N_LOCALE_KEY) ?? this.options.defaultLocale ?? 'en';
34
+ }
35
+ }
36
+ I18nService = _ts_decorate([
37
+ Injectable({
38
+ scope: Scope.REQUEST
39
+ }),
40
+ _ts_param(0, Inject(MessageLoaderService)),
41
+ _ts_param(1, Inject(REQUEST_CONTEXT)),
42
+ _ts_param(2, Inject(I18N_OPTIONS)),
43
+ _ts_metadata("design:type", Function),
44
+ _ts_metadata("design:paramtypes", [
45
+ typeof MessageLoaderService === "undefined" ? Object : MessageLoaderService,
46
+ typeof RequestContext === "undefined" ? Object : RequestContext,
47
+ typeof I18nModuleOptions === "undefined" ? Object : I18nModuleOptions
48
+ ])
49
+ ], I18nService);
@@ -0,0 +1,6 @@
1
+ import { InjectionToken } from '../container/types';
2
+ import type { I18nModuleOptions } from './i18n.options';
3
+ /** Options bag from `I18nModule.forRoot()` (the ConfigurableModuleBuilder options token). */
4
+ export declare const I18N_OPTIONS: InjectionToken<I18nModuleOptions>;
5
+ /** Key under which the resolved locale is stashed on the per-request RequestContext bag. */
6
+ export declare const I18N_LOCALE_KEY: unique symbol;
@@ -0,0 +1,3 @@
1
+ import { InjectionToken } from "../container/types.js";
2
+ /** Options bag from `I18nModule.forRoot()` (the ConfigurableModuleBuilder options token). */ export const I18N_OPTIONS = new InjectionToken('I18N_OPTIONS');
3
+ /** Key under which the resolved locale is stashed on the per-request RequestContext bag. */ export const I18N_LOCALE_KEY = Symbol.for('vela:i18n:locale');
@@ -0,0 +1,6 @@
1
+ export interface II18nService {
2
+ /** Translate `key` (dot-path into the message tree) with optional ICU params. */
3
+ t(key: string, params?: Record<string, unknown>): string;
4
+ /** The resolved locale for the current request. */
5
+ getLocale(): string;
6
+ }
@@ -0,0 +1 @@
1
+ export { };
@@ -0,0 +1,10 @@
1
+ export { I18nModule } from './i18n.module';
2
+ export { I18nService } from './i18n.service';
3
+ export { MessageLoaderService } from './message-loader.service';
4
+ export { MessageRegistry } from './message-registry';
5
+ export { I18nLocaleMiddleware } from './i18n.middleware';
6
+ export { I18N_OPTIONS, I18N_LOCALE_KEY } from './i18n.tokens';
7
+ export { resolveI18nOptions } from './i18n.options';
8
+ export { deepMerge } from './deep-merge';
9
+ export type { I18nModuleOptions, ResolvedI18nOptions, DetectionStrategy } from './i18n.options';
10
+ export type { II18nService } from './i18n.types';
@@ -0,0 +1,8 @@
1
+ export { I18nModule } from "./i18n.module.js";
2
+ export { I18nService } from "./i18n.service.js";
3
+ export { MessageLoaderService } from "./message-loader.service.js";
4
+ export { MessageRegistry } from "./message-registry.js";
5
+ export { I18nLocaleMiddleware } from "./i18n.middleware.js";
6
+ export { I18N_OPTIONS, I18N_LOCALE_KEY } from "./i18n.tokens.js";
7
+ export { resolveI18nOptions } from "./i18n.options.js";
8
+ export { deepMerge } from "./deep-merge.js";
@@ -0,0 +1,21 @@
1
+ import type { I18nModuleOptions } from './i18n.options';
2
+ import { MessageRegistry } from './message-registry';
3
+ /**
4
+ * Compiles and caches ICU messages (via `intl-messageformat`). Singleton: reads
5
+ * the merged registry once at construction (all `registerMessages` calls run at
6
+ * module-load, before providers are instantiated), flattens each locale to
7
+ * dot-paths, and compiles lazily per locale.
8
+ */
9
+ export declare class MessageLoaderService {
10
+ private readonly cache;
11
+ private readonly compiledCache;
12
+ private readonly defaultLocale;
13
+ private readonly fallbackLocale;
14
+ constructor(registry: MessageRegistry, options: I18nModuleOptions);
15
+ translate(locale: string, key: string, params?: Record<string, unknown>): string;
16
+ getAvailableLocales(): string[];
17
+ isLocaleSupported(locale: string): boolean;
18
+ getDefaultLocale(): string;
19
+ private getCompiledMessages;
20
+ private flattenMessages;
21
+ }