@getsoda/core 0.2.1 → 0.3.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 (137) hide show
  1. package/README.md +103 -0
  2. package/dist/app/app.d.ts +15 -5
  3. package/dist/app/app.d.ts.map +1 -1
  4. package/dist/app/app.js +31 -8
  5. package/dist/app/app.js.map +1 -1
  6. package/dist/app/index.d.ts +1 -1
  7. package/dist/app/index.d.ts.map +1 -1
  8. package/dist/config/config.d.ts +57 -0
  9. package/dist/config/config.d.ts.map +1 -0
  10. package/dist/config/config.js +74 -0
  11. package/dist/config/config.js.map +1 -0
  12. package/dist/config/index.d.ts +9 -0
  13. package/dist/config/index.d.ts.map +1 -0
  14. package/dist/config/index.js +7 -0
  15. package/dist/config/index.js.map +1 -0
  16. package/dist/config/module.d.ts +29 -0
  17. package/dist/config/module.d.ts.map +1 -0
  18. package/dist/config/module.js +28 -0
  19. package/dist/config/module.js.map +1 -0
  20. package/dist/di/errors.d.ts.map +1 -1
  21. package/dist/di/errors.js +2 -0
  22. package/dist/di/errors.js.map +1 -1
  23. package/dist/http/arguments.d.ts +19 -6
  24. package/dist/http/arguments.d.ts.map +1 -1
  25. package/dist/http/arguments.js +7 -0
  26. package/dist/http/arguments.js.map +1 -1
  27. package/dist/http/cookies.d.ts.map +1 -1
  28. package/dist/http/cookies.js +2 -0
  29. package/dist/http/cookies.js.map +1 -1
  30. package/dist/http/decorators.d.ts +18 -6
  31. package/dist/http/decorators.d.ts.map +1 -1
  32. package/dist/http/decorators.js +7 -2
  33. package/dist/http/decorators.js.map +1 -1
  34. package/dist/http/errors.d.ts +3 -1
  35. package/dist/http/errors.d.ts.map +1 -1
  36. package/dist/http/errors.js +2 -0
  37. package/dist/http/errors.js.map +1 -1
  38. package/dist/http/index.d.ts +7 -5
  39. package/dist/http/index.d.ts.map +1 -1
  40. package/dist/http/index.js +3 -2
  41. package/dist/http/index.js.map +1 -1
  42. package/dist/http/parsers.d.ts +4 -0
  43. package/dist/http/parsers.d.ts.map +1 -1
  44. package/dist/http/parsers.js +9 -5
  45. package/dist/http/parsers.js.map +1 -1
  46. package/dist/http/pipeline.d.ts +4 -5
  47. package/dist/http/pipeline.d.ts.map +1 -1
  48. package/dist/http/pipeline.js +2 -2
  49. package/dist/http/pipeline.js.map +1 -1
  50. package/dist/http/routes.d.ts +27 -2
  51. package/dist/http/routes.d.ts.map +1 -1
  52. package/dist/http/routes.js +34 -20
  53. package/dist/http/routes.js.map +1 -1
  54. package/dist/http/schema.d.ts +2 -0
  55. package/dist/http/schema.d.ts.map +1 -1
  56. package/dist/http/schema.js +2 -1
  57. package/dist/http/schema.js.map +1 -1
  58. package/dist/http/sse.d.ts +39 -0
  59. package/dist/http/sse.d.ts.map +1 -0
  60. package/dist/http/sse.js +89 -0
  61. package/dist/http/sse.js.map +1 -0
  62. package/dist/index.d.ts +2 -0
  63. package/dist/index.d.ts.map +1 -1
  64. package/dist/index.js +2 -0
  65. package/dist/index.js.map +1 -1
  66. package/dist/openapi/decorators.d.ts +41 -0
  67. package/dist/openapi/decorators.d.ts.map +1 -0
  68. package/dist/openapi/decorators.js +29 -0
  69. package/dist/openapi/decorators.js.map +1 -0
  70. package/dist/openapi/document.d.ts +62 -0
  71. package/dist/openapi/document.d.ts.map +1 -0
  72. package/dist/openapi/document.js +135 -0
  73. package/dist/openapi/document.js.map +1 -0
  74. package/dist/openapi/index.d.ts +11 -0
  75. package/dist/openapi/index.d.ts.map +1 -0
  76. package/dist/openapi/index.js +8 -0
  77. package/dist/openapi/index.js.map +1 -0
  78. package/dist/openapi/module.d.ts +26 -0
  79. package/dist/openapi/module.d.ts.map +1 -0
  80. package/dist/openapi/module.js +73 -0
  81. package/dist/openapi/module.js.map +1 -0
  82. package/dist/routing/errors.d.ts.map +1 -1
  83. package/dist/routing/errors.js +2 -0
  84. package/dist/routing/errors.js.map +1 -1
  85. package/dist/testing/index.d.ts +46 -0
  86. package/dist/testing/index.d.ts.map +1 -0
  87. package/dist/testing/index.js +49 -0
  88. package/dist/testing/index.js.map +1 -0
  89. package/dist/utils/errors.d.ts +11 -0
  90. package/dist/utils/errors.d.ts.map +1 -0
  91. package/dist/utils/errors.js +22 -0
  92. package/dist/utils/errors.js.map +1 -0
  93. package/dist/utils/object.d.ts +4 -0
  94. package/dist/utils/object.d.ts.map +1 -1
  95. package/dist/utils/object.js +16 -0
  96. package/dist/utils/object.js.map +1 -1
  97. package/dist/websocket/decorators.d.ts +31 -0
  98. package/dist/websocket/decorators.d.ts.map +1 -0
  99. package/dist/websocket/decorators.js +43 -0
  100. package/dist/websocket/decorators.js.map +1 -0
  101. package/dist/websocket/gateway.d.ts +61 -0
  102. package/dist/websocket/gateway.d.ts.map +1 -0
  103. package/dist/websocket/gateway.js +142 -0
  104. package/dist/websocket/gateway.js.map +1 -0
  105. package/dist/websocket/index.d.ts +7 -0
  106. package/dist/websocket/index.d.ts.map +1 -0
  107. package/dist/websocket/index.js +6 -0
  108. package/dist/websocket/index.js.map +1 -0
  109. package/package.json +12 -1
  110. package/src/app/app.ts +69 -18
  111. package/src/app/index.ts +1 -1
  112. package/src/config/config.ts +90 -0
  113. package/src/config/index.ts +9 -0
  114. package/src/config/module.ts +40 -0
  115. package/src/di/errors.ts +3 -0
  116. package/src/http/arguments.ts +31 -6
  117. package/src/http/cookies.ts +2 -0
  118. package/src/http/decorators.ts +25 -10
  119. package/src/http/errors.ts +7 -0
  120. package/src/http/index.ts +7 -4
  121. package/src/http/parsers.ts +18 -7
  122. package/src/http/pipeline.ts +6 -6
  123. package/src/http/routes.ts +68 -29
  124. package/src/http/schema.ts +2 -1
  125. package/src/http/sse.ts +120 -0
  126. package/src/index.ts +2 -0
  127. package/src/openapi/decorators.ts +50 -0
  128. package/src/openapi/document.ts +221 -0
  129. package/src/openapi/index.ts +11 -0
  130. package/src/openapi/module.ts +95 -0
  131. package/src/routing/errors.ts +3 -0
  132. package/src/testing/index.ts +73 -0
  133. package/src/utils/errors.ts +25 -0
  134. package/src/utils/object.ts +23 -0
  135. package/src/websocket/decorators.ts +55 -0
  136. package/src/websocket/gateway.ts +213 -0
  137. package/src/websocket/index.ts +7 -0
@@ -0,0 +1,61 @@
1
+ /**
2
+ * @module
3
+ * Serving a gateway: the handshake goes through the route pipeline (guards
4
+ * and exception filters of the gateway), then frames are dispatched to its
5
+ * `@SubscribeMessage()` methods.
6
+ *
7
+ * The protocol is the one of Nest's `WsAdapter`: text frames holding
8
+ * `{ "event": string, "data": any }`. Frames that are not JSON, not such an
9
+ * object, or name no handled event are dropped silently, as anyone can send
10
+ * them; errors of the gateway's own code are reported.
11
+ */
12
+ import type { ControllerEntry } from '../di/index.ts';
13
+ import type { Context } from '../http/context.ts';
14
+ import type { FilterStep, GuardStep } from '../http/pipeline.ts';
15
+ /** What a gateway socket carries. */
16
+ export interface GatewaySocketData {
17
+ /** Context of the handshake request: what its guards set is read with `context.get(key)`. */
18
+ readonly context: Context;
19
+ /** @internal */
20
+ readonly gateway: GatewayRuntime;
21
+ }
22
+ /** A connection of a gateway: Bun's `ServerWebSocket`, with `subscribe()`, `publish()`... */
23
+ export type GatewaySocket = Bun.ServerWebSocket<GatewaySocketData>;
24
+ /** `handleConnection(socket)`: called when a client is connected. */
25
+ export interface OnGatewayConnection {
26
+ handleConnection(socket: GatewaySocket): unknown;
27
+ }
28
+ /** `handleDisconnect(socket, code, reason)`: called when a connection is closed. */
29
+ export interface OnGatewayDisconnect {
30
+ handleDisconnect(socket: GatewaySocket, code: number, reason: string): unknown;
31
+ }
32
+ /** The live part of a gateway: its sockets and handlers. */
33
+ export declare class GatewayRuntime {
34
+ #private;
35
+ constructor(instance: object, onError: (error: unknown, req: Request) => void);
36
+ get events(): readonly string[];
37
+ open(socket: GatewaySocket): void;
38
+ message(socket: GatewaySocket, message: string | Buffer): void;
39
+ close(socket: GatewaySocket, code: number, reason: string): void;
40
+ /** Closes every connection, as the application shuts down. */
41
+ closeAll(): void;
42
+ }
43
+ /** Bun's handlers for every gateway of a server: each socket knows its gateway. */
44
+ export declare const websocketHandler: Bun.WebSocketHandler<GatewaySocketData>;
45
+ export interface GatewayParts {
46
+ readonly entry: ControllerEntry;
47
+ readonly path: string;
48
+ readonly guards: readonly GuardStep[];
49
+ readonly filters: readonly FilterStep[];
50
+ readonly metadata: ReadonlyMap<symbol, unknown>;
51
+ readonly onError: (error: unknown, req: Request) => void;
52
+ }
53
+ /** A gateway ready to serve: its native route and its runtime. */
54
+ export interface BuiltGateway {
55
+ readonly path: string;
56
+ readonly runtime: GatewayRuntime;
57
+ /** The native route of the handshake, for `Bun.serve({ routes })`. */
58
+ readonly handle: (req: Bun.BunRequest, server: Bun.Server<GatewaySocketData>) => Response | undefined | Promise<Response | undefined>;
59
+ }
60
+ export declare const buildGateway: (parts: GatewayParts) => BuiltGateway;
61
+ //# sourceMappingURL=gateway.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gateway.d.ts","sourceRoot":"","sources":["../../src/websocket/gateway.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAA;AAErD,OAAO,KAAK,EAAE,OAAO,EAAa,MAAM,oBAAoB,CAAA;AAE5D,OAAO,KAAK,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAA;AAKhE,qCAAqC;AACrC,MAAM,WAAW,iBAAiB;IAChC,6FAA6F;IAC7F,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAA;IACzB,gBAAgB;IAChB,QAAQ,CAAC,OAAO,EAAE,cAAc,CAAA;CACjC;AAED,6FAA6F;AAC7F,MAAM,MAAM,aAAa,GAAG,GAAG,CAAC,eAAe,CAAC,iBAAiB,CAAC,CAAA;AAElE,qEAAqE;AACrE,MAAM,WAAW,mBAAmB;IAClC,gBAAgB,CAAC,MAAM,EAAE,aAAa,GAAG,OAAO,CAAA;CACjD;AAED,oFAAoF;AACpF,MAAM,WAAW,mBAAmB;IAClC,gBAAgB,CAAC,MAAM,EAAE,aAAa,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAA;CAC/E;AAoBD,4DAA4D;AAC5D,qBAAa,cAAc;;IAMzB,YAAY,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,GAAG,EAAE,OAAO,KAAK,IAAI,EAY5E;IAED,IAAI,MAAM,IAAI,SAAS,MAAM,EAAE,CAE9B;IAED,IAAI,CAAC,MAAM,EAAE,aAAa,GAAG,IAAI,CAGhC;IAED,OAAO,CAAC,MAAM,EAAE,aAAa,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAiB7D;IAED,KAAK,CAAC,MAAM,EAAE,aAAa,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI,CAG/D;IAED,8DAA8D;IAC9D,QAAQ,IAAI,IAAI,CAIf;CAqBF;AAkBD,mFAAmF;AACnF,eAAO,MAAM,gBAAgB,EAAE,GAAG,CAAC,gBAAgB,CAAC,iBAAiB,CAIpE,CAAA;AAKD,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,KAAK,EAAE,eAAe,CAAA;IAC/B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,MAAM,EAAE,SAAS,SAAS,EAAE,CAAA;IACrC,QAAQ,CAAC,OAAO,EAAE,SAAS,UAAU,EAAE,CAAA;IACvC,QAAQ,CAAC,QAAQ,EAAE,WAAW,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IAC/C,QAAQ,CAAC,OAAO,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,GAAG,EAAE,OAAO,KAAK,IAAI,CAAA;CACzD;AAED,kEAAkE;AAClE,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,OAAO,EAAE,cAAc,CAAA;IAChC,sEAAsE;IACtE,QAAQ,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,GAAG,CAAC,UAAU,EAAE,MAAM,EAAE,GAAG,CAAC,MAAM,CAAC,iBAAiB,CAAC,KAAK,QAAQ,GAAG,SAAS,GAAG,OAAO,CAAC,QAAQ,GAAG,SAAS,CAAC,CAAA;CACtI;AAED,eAAO,MAAM,YAAY,UAAW,YAAY,KAAG,YA2BlD,CAAA"}
@@ -0,0 +1,142 @@
1
+ /**
2
+ * @module
3
+ * Serving a gateway: the handshake goes through the route pipeline (guards
4
+ * and exception filters of the gateway), then frames are dispatched to its
5
+ * `@SubscribeMessage()` methods.
6
+ *
7
+ * The protocol is the one of Nest's `WsAdapter`: text frames holding
8
+ * `{ "event": string, "data": any }`. Frames that are not JSON, not such an
9
+ * object, or name no handled event are dropped silently, as anyone can send
10
+ * them; errors of the gateway's own code are reported.
11
+ */
12
+ import { parseJson } from '../http/body.js';
13
+ import { createPipeline } from '../http/pipeline.js';
14
+ import { methodNames } from '../utils/object.js';
15
+ import { isThenable } from '../utils/promise.js';
16
+ import { getSubscribedEvent } from './decorators.js';
17
+ /** Code of the close frame sent when the application shuts down. */
18
+ const GOING_AWAY = 1001;
19
+ /** Runs `fn`, reporting a synchronous or asynchronous failure. */
20
+ const settle = (fn, report, then) => {
21
+ try {
22
+ const result = fn();
23
+ if (isThenable(result)) {
24
+ Promise.resolve(result).then(then, report);
25
+ }
26
+ else {
27
+ then?.(result);
28
+ }
29
+ }
30
+ catch (error) {
31
+ report(error);
32
+ }
33
+ };
34
+ /** The live part of a gateway: its sockets and handlers. */
35
+ export class GatewayRuntime {
36
+ #instance;
37
+ #handlers = new Map();
38
+ #sockets = new Set();
39
+ #onError;
40
+ constructor(instance, onError) {
41
+ this.#instance = instance;
42
+ this.#onError = onError;
43
+ for (const name of methodNames(instance)) {
44
+ const method = this.#instance[name];
45
+ const event = getSubscribedEvent(method);
46
+ if (event !== undefined) {
47
+ this.#handlers.set(event, method);
48
+ }
49
+ }
50
+ }
51
+ get events() {
52
+ return [...this.#handlers.keys()];
53
+ }
54
+ open(socket) {
55
+ this.#sockets.add(socket);
56
+ this.#call('handleConnection', socket, [socket]);
57
+ }
58
+ message(socket, message) {
59
+ const frame = typeof message === 'string' ? parseFrame(message) : undefined;
60
+ const handler = frame === undefined ? undefined : this.#handlers.get(frame.event);
61
+ if (handler === undefined) {
62
+ return;
63
+ }
64
+ settle(() => handler.call(this.#instance, frame.data, socket), (error) => this.#onError(error, socket.data.context.req), (result) => {
65
+ if (result !== undefined) {
66
+ this.#send(socket, result);
67
+ }
68
+ });
69
+ }
70
+ close(socket, code, reason) {
71
+ this.#sockets.delete(socket);
72
+ this.#call('handleDisconnect', socket, [socket, code, reason]);
73
+ }
74
+ /** Closes every connection, as the application shuts down. */
75
+ closeAll() {
76
+ for (const socket of this.#sockets) {
77
+ socket.close(GOING_AWAY, 'Server shutting down');
78
+ }
79
+ }
80
+ #call(hook, socket, args) {
81
+ const fn = this.#instance[hook];
82
+ if (typeof fn === 'function') {
83
+ settle(() => fn.apply(this.#instance, args), (error) => this.#onError(error, socket.data.context.req));
84
+ }
85
+ }
86
+ #send(socket, value) {
87
+ try {
88
+ // JSON.stringify throws on cycles and bigints: an application error, reported.
89
+ socket.send(JSON.stringify(value));
90
+ }
91
+ catch (error) {
92
+ this.#onError(error, socket.data.context.req);
93
+ }
94
+ }
95
+ }
96
+ const parseFrame = (message) => {
97
+ let frame;
98
+ try {
99
+ frame = parseJson(message);
100
+ }
101
+ catch {
102
+ return undefined;
103
+ }
104
+ if (typeof frame !== 'object' || frame === null || typeof frame.event !== 'string') {
105
+ return undefined;
106
+ }
107
+ return frame;
108
+ };
109
+ /** Bun's handlers for every gateway of a server: each socket knows its gateway. */
110
+ export const websocketHandler = {
111
+ open: (socket) => socket.data.gateway.open(socket),
112
+ message: (socket, message) => socket.data.gateway.message(socket, message),
113
+ close: (socket, code, reason) => socket.data.gateway.close(socket, code, reason),
114
+ };
115
+ /** Result of the handshake once the connection is upgraded: Bun wants no response then. */
116
+ const UPGRADED = Symbol('upgraded');
117
+ export const buildGateway = (parts) => {
118
+ const runtime = new GatewayRuntime(parts.entry.instance, parts.onError);
119
+ const route = { controller: parts.entry.controller, handler: 'handshake', method: 'GET', path: parts.path, metadata: parts.metadata };
120
+ // The pipeline is built once; the server of each request is found here.
121
+ const servers = new WeakMap();
122
+ const pipeline = createPipeline({
123
+ route,
124
+ guards: parts.guards,
125
+ interceptors: [],
126
+ filters: parts.filters,
127
+ invoke: (ctx) => servers.get(ctx.req).upgrade(ctx.req, { data: { context: ctx, gateway: runtime } })
128
+ ? UPGRADED
129
+ : new Response('Expected a WebSocket upgrade', { status: 426, headers: { upgrade: 'websocket' } }),
130
+ respond: (value) => (value === UPGRADED ? undefined : value),
131
+ onError: parts.onError,
132
+ });
133
+ return {
134
+ path: parts.path,
135
+ runtime,
136
+ handle: (req, server) => {
137
+ servers.set(req, server);
138
+ return pipeline(req, {});
139
+ },
140
+ };
141
+ };
142
+ //# sourceMappingURL=gateway.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gateway.js","sourceRoot":"","sources":["../../src/websocket/gateway.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAGH,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAA;AAE3C,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAA;AAEpD,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAA;AAChD,OAAO,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAA;AAChD,OAAO,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAA;AAuBpD,oEAAoE;AACpE,MAAM,UAAU,GAAG,IAAI,CAAA;AAEvB,kEAAkE;AAClE,MAAM,MAAM,GAAG,CAAC,EAAiB,EAAE,MAAgC,EAAE,IAA+B,EAAQ,EAAE;IAC5G,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,EAAE,EAAE,CAAA;QAEnB,IAAI,UAAU,CAAC,MAAM,CAAC,EAAE,CAAC;YACvB,OAAO,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,IAAI,EAAE,MAAM,CAAC,CAAA;QAC5C,CAAC;aAAM,CAAC;YACN,IAAI,EAAE,CAAC,MAAM,CAAC,CAAA;QAChB,CAAC;IACH,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,MAAM,CAAC,KAAK,CAAC,CAAA;IACf,CAAC;AACH,CAAC,CAAA;AAED,4DAA4D;AAC5D,MAAM,OAAO,cAAc;IAChB,SAAS,CAAyB;IAClC,SAAS,GAAG,IAAI,GAAG,EAA6D,CAAA;IAChF,QAAQ,GAAG,IAAI,GAAG,EAAiB,CAAA;IACnC,QAAQ,CAAwC;IAEzD,YAAY,QAAgB,EAAE,OAA+C;QAC3E,IAAI,CAAC,SAAS,GAAG,QAAmC,CAAA;QACpD,IAAI,CAAC,QAAQ,GAAG,OAAO,CAAA;QAEvB,KAAK,MAAM,IAAI,IAAI,WAAW,CAAC,QAAQ,CAAC,EAAE,CAAC;YACzC,MAAM,MAAM,GAAG,IAAI,CAAC,SAAS,CAAC,IAAI,CAAsD,CAAA;YACxF,MAAM,KAAK,GAAG,kBAAkB,CAAC,MAAM,CAAC,CAAA;YAExC,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;gBACxB,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,KAAK,EAAE,MAAM,CAAC,CAAA;YACnC,CAAC;QACH,CAAC;IACH,CAAC;IAED,IAAI,MAAM;QACR,OAAO,CAAC,GAAG,IAAI,CAAC,SAAS,CAAC,IAAI,EAAE,CAAC,CAAA;IACnC,CAAC;IAED,IAAI,CAAC,MAAqB;QACxB,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,MAAM,CAAC,CAAA;QACzB,IAAI,CAAC,KAAK,CAAC,kBAAkB,EAAE,MAAM,EAAE,CAAC,MAAM,CAAC,CAAC,CAAA;IAClD,CAAC;IAED,OAAO,CAAC,MAAqB,EAAE,OAAwB;QACrD,MAAM,KAAK,GAAG,OAAO,OAAO,KAAK,QAAQ,CAAC,CAAC,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,SAAS,CAAA;QAC3E,MAAM,OAAO,GAAG,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,KAAK,CAAC,KAAK,CAAC,CAAA;QAEjF,IAAI,OAAO,KAAK,SAAS,EAAE,CAAC;YAC1B,OAAM;QACR,CAAC;QAED,MAAM,CACJ,GAAG,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,KAAM,CAAC,IAAI,EAAE,MAAM,CAAC,EACvD,CAAC,KAAK,EAAE,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,EACxD,CAAC,MAAM,EAAE,EAAE;YACT,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;gBACzB,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;YAC5B,CAAC;QACH,CAAC,CACF,CAAA;IACH,CAAC;IAED,KAAK,CAAC,MAAqB,EAAE,IAAY,EAAE,MAAc;QACvD,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,MAAM,CAAC,CAAA;QAC5B,IAAI,CAAC,KAAK,CAAC,kBAAkB,EAAE,MAAM,EAAE,CAAC,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,CAAC,CAAA;IAChE,CAAC;IAED,8DAA8D;IAC9D,QAAQ;QACN,KAAK,MAAM,MAAM,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;YACnC,MAAM,CAAC,KAAK,CAAC,UAAU,EAAE,sBAAsB,CAAC,CAAA;QAClD,CAAC;IACH,CAAC;IAED,KAAK,CAAC,IAAY,EAAE,MAAqB,EAAE,IAAwB;QACjE,MAAM,EAAE,GAAG,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,CAAA;QAE/B,IAAI,OAAO,EAAE,KAAK,UAAU,EAAE,CAAC;YAC7B,MAAM,CACJ,GAAG,EAAE,CAAC,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,SAAS,EAAE,IAAI,CAAC,EACpC,CAAC,KAAK,EAAE,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CACzD,CAAA;QACH,CAAC;IACH,CAAC;IAED,KAAK,CAAC,MAAqB,EAAE,KAAc;QACzC,IAAI,CAAC;YACH,+EAA+E;YAC/E,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,CAAA;QACpC,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,IAAI,CAAC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAA;QAC/C,CAAC;IACH,CAAC;CACF;AAED,MAAM,UAAU,GAAG,CAAC,OAAe,EAAgD,EAAE;IACnF,IAAI,KAAc,CAAA;IAElB,IAAI,CAAC;QACH,KAAK,GAAG,SAAS,CAAC,OAAO,CAAC,CAAA;IAC5B,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,SAAS,CAAA;IAClB,CAAC;IAED,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,IAAI,OAAQ,KAA6B,CAAC,KAAK,KAAK,QAAQ,EAAE,CAAC;QAC5G,OAAO,SAAS,CAAA;IAClB,CAAC;IAED,OAAO,KAAyC,CAAA;AAClD,CAAC,CAAA;AAED,mFAAmF;AACnF,MAAM,CAAC,MAAM,gBAAgB,GAA4C;IACvE,IAAI,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC;IAClD,OAAO,EAAE,CAAC,MAAM,EAAE,OAAO,EAAE,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,MAAM,EAAE,OAAO,CAAC;IAC1E,KAAK,EAAE,CAAC,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC;CACjF,CAAA;AAED,2FAA2F;AAC3F,MAAM,QAAQ,GAAG,MAAM,CAAC,UAAU,CAAC,CAAA;AAmBnC,MAAM,CAAC,MAAM,YAAY,GAAG,CAAC,KAAmB,EAAgB,EAAE;IAChE,MAAM,OAAO,GAAG,IAAI,cAAc,CAAC,KAAK,CAAC,KAAK,CAAC,QAAkB,EAAE,KAAK,CAAC,OAAO,CAAC,CAAA;IACjF,MAAM,KAAK,GAAc,EAAE,UAAU,EAAE,KAAK,CAAC,KAAK,CAAC,UAAU,EAAE,OAAO,EAAE,WAAW,EAAE,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI,EAAE,QAAQ,EAAE,KAAK,CAAC,QAAQ,EAAE,CAAA;IAChJ,wEAAwE;IACxE,MAAM,OAAO,GAAG,IAAI,OAAO,EAA0C,CAAA;IACrE,MAAM,QAAQ,GAAG,cAAc,CAAC;QAC9B,KAAK;QACL,MAAM,EAAE,KAAK,CAAC,MAAM;QACpB,YAAY,EAAE,EAAE;QAChB,OAAO,EAAE,KAAK,CAAC,OAAO;QACtB,MAAM,EAAE,CAAC,GAAG,EAAE,EAAE,CACd,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAE,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,EAAE,EAAE,IAAI,EAAE,EAAE,OAAO,EAAE,GAAG,EAAE,OAAO,EAAE,OAAO,EAAE,EAAE,CAAC;YAClF,CAAC,CAAC,QAAQ;YACV,CAAC,CAAC,IAAI,QAAQ,CAAC,8BAA8B,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,OAAO,EAAE,EAAE,OAAO,EAAE,WAAW,EAAE,EAAE,CAAC;QACtG,OAAO,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAE,SAAiC,CAAC,CAAC,CAAE,KAAkB,CAAC;QACnG,OAAO,EAAE,KAAK,CAAC,OAAO;KACvB,CAAC,CAAA;IAEF,OAAO;QACL,IAAI,EAAE,KAAK,CAAC,IAAI;QAChB,OAAO;QACP,MAAM,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,EAAE;YACtB,OAAO,CAAC,GAAG,CAAC,GAAG,EAAE,MAAM,CAAC,CAAA;YAExB,OAAO,QAAQ,CAAC,GAAG,EAAE,EAAE,CAAC,CAAA;QAC1B,CAAC;KACF,CAAA;AACH,CAAC,CAAA"}
@@ -0,0 +1,7 @@
1
+ /**
2
+ * @module
3
+ * WebSocket gateways, as in `@nestjs/websockets`.
4
+ */
5
+ export { SubscribeMessage, WebSocketGateway } from './decorators.ts';
6
+ export type { GatewaySocket, GatewaySocketData, OnGatewayConnection, OnGatewayDisconnect } from './gateway.ts';
7
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/websocket/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAA;AACpE,YAAY,EAAE,aAAa,EAAE,iBAAiB,EAAE,mBAAmB,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAA"}
@@ -0,0 +1,6 @@
1
+ /**
2
+ * @module
3
+ * WebSocket gateways, as in `@nestjs/websockets`.
4
+ */
5
+ export { SubscribeMessage, WebSocketGateway } from './decorators.js';
6
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/websocket/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAA"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@getsoda/core",
3
- "version": "0.2.1",
3
+ "version": "0.3.0",
4
4
  "description": "Class-based web framework for Bun: modules, dependency injection and decorators like Nest, on Bun's native router",
5
5
  "keywords": [
6
6
  "bun",
@@ -32,6 +32,14 @@
32
32
  "types": "./dist/jwt/index.d.ts",
33
33
  "default": "./dist/jwt/index.js"
34
34
  },
35
+ "./openapi": {
36
+ "types": "./dist/openapi/index.d.ts",
37
+ "default": "./dist/openapi/index.js"
38
+ },
39
+ "./testing": {
40
+ "types": "./dist/testing/index.d.ts",
41
+ "default": "./dist/testing/index.js"
42
+ },
35
43
  "./package.json": "./package.json"
36
44
  },
37
45
  "types": "./dist/index.d.ts",
@@ -47,5 +55,8 @@
47
55
  "scripts": {
48
56
  "build": "rm -rf dist && tsc -p tsconfig.build.json",
49
57
  "prepublishOnly": "cd ../.. && bun run lint && bun run typecheck && bun run test && cd packages/core && bun run build"
58
+ },
59
+ "devDependencies": {
60
+ "zod": "4.6.7"
50
61
  }
51
62
  }
package/src/app/app.ts CHANGED
@@ -22,13 +22,15 @@
22
22
  import { createContainer, provide } from '../di/index.ts'
23
23
  import type { Container, ContainerOptions, ModuleImport } from '../di/index.ts'
24
24
  import { destroyAll, initializeAll } from '../di/lifecycle.ts'
25
- import { buildRoutes } from '../http/index.ts'
25
+ import { buildRoutes, ControllerError, ROUTES } from '../http/index.ts'
26
26
  import type { ControllerRoutesOptions, MappedController, Middleware } from '../http/index.ts'
27
27
  import { errorResponse } from '../http/exceptions.ts'
28
28
  import { applyMiddleware, resolveMiddleware } from '../http/middleware.ts'
29
29
  import { ConsoleLogger, Logger, SilentLogger } from '../logger/index.ts'
30
30
  import { compileRoutes } from '../routing/index.ts'
31
- import type { CompiledRoutes, CompileOptions } from '../routing/index.ts'
31
+ import type { CompiledRoutes, CompileOptions, NativeHandler } from '../routing/index.ts'
32
+ import { websocketHandler } from '../websocket/gateway.ts'
33
+ import type { BuiltGateway, GatewaySocketData } from '../websocket/gateway.ts'
32
34
  import { release, takeOver } from './hot.ts'
33
35
 
34
36
  /**
@@ -68,16 +70,16 @@ const DEFAULT_SIGNALS: readonly NodeJS.Signals[] = ['SIGTERM', 'SIGINT']
68
70
  */
69
71
  export interface App {
70
72
  readonly container: Container
71
- /** Routes for `Bun.serve()`, when serving them yourself instead of calling `listen()`. */
72
- readonly routes: CompiledRoutes
73
+ /** Routes (and WebSocket handlers) for `Bun.serve()`, when serving them yourself instead of calling `listen()`. */
74
+ readonly routes: ServeRoutes
73
75
  /** The server once `listen()` resolved. */
74
- readonly server: Bun.Server<undefined> | undefined
76
+ readonly server: Bun.Server<GatewaySocketData> | undefined
75
77
  /**
76
78
  * Starts serving. Under `bun --hot`, a new version of the application
77
79
  * listening on the same port takes the server over from the previous one,
78
80
  * which is then destroyed (its server is not stopped: it is shared).
79
81
  */
80
- listen(options?: number | ListenOptions): Promise<Bun.Server<undefined>>
82
+ listen(options?: number | ListenOptions): Promise<Bun.Server<GatewaySocketData>>
81
83
  /**
82
84
  * Graceful shutdown: refuses new requests, waits for those in flight, then
83
85
  * runs `onDestroy` hooks. Safe to call more than once, even from a hook.
@@ -92,27 +94,35 @@ export interface App {
92
94
 
93
95
  class Application implements App {
94
96
  readonly container: Container
95
- readonly routes: CompiledRoutes
96
- #server: Bun.Server<undefined> | undefined
97
+ readonly routes: ServeRoutes
98
+ #server: Bun.Server<GatewaySocketData> | undefined
97
99
  /** Set when a new version took the server over: it must not be stopped. */
98
100
  #handedOver = false
99
101
  #closing: Promise<void> | undefined
100
102
  readonly #enhancers: readonly unknown[]
101
103
  readonly #signals = new Map<NodeJS.Signals, () => void>()
102
104
  readonly #logger: Logger
103
-
104
- constructor(container: Container, routes: CompiledRoutes, enhancers: readonly unknown[], logger: Logger) {
105
+ readonly #gateways: readonly BuiltGateway[]
106
+
107
+ constructor(
108
+ container: Container,
109
+ routes: ServeRoutes,
110
+ gateways: readonly BuiltGateway[],
111
+ enhancers: readonly unknown[],
112
+ logger: Logger
113
+ ) {
105
114
  this.container = container
106
115
  this.routes = routes
116
+ this.#gateways = gateways
107
117
  this.#enhancers = enhancers
108
118
  this.#logger = logger
109
119
  }
110
120
 
111
- get server(): Bun.Server<undefined> | undefined {
121
+ get server(): Bun.Server<GatewaySocketData> | undefined {
112
122
  return this.#server
113
123
  }
114
124
 
115
- async listen(options: number | ListenOptions = {}): Promise<Bun.Server<undefined>> {
125
+ async listen(options: number | ListenOptions = {}): Promise<Bun.Server<GatewaySocketData>> {
116
126
  if (this.#closing !== undefined) {
117
127
  throw new Error('Cannot listen: the application is closed')
118
128
  }
@@ -191,6 +201,11 @@ class Application implements App {
191
201
  const server = this.#server
192
202
 
193
203
  try {
204
+ // Open sockets would keep stop() waiting; under --hot, clients reconnect to the new version.
205
+ for (const gateway of this.#gateways) {
206
+ gateway.runtime.closeAll()
207
+ }
208
+
194
209
  if (server !== undefined && !this.#handedOver) {
195
210
  await server.stop()
196
211
  }
@@ -245,8 +260,37 @@ const destroyEverything = async (enhancers: readonly unknown[], container: Conta
245
260
  }
246
261
  }
247
262
 
263
+ /**
264
+ * Routes and WebSocket handlers for `Bun.serve()`: the handshakes of gateways
265
+ * are native routes of their own, outside middleware, as an upgrade returns no response.
266
+ */
267
+ export interface ServeRoutes {
268
+ readonly routes: Record<string, NativeHandler | BuiltGateway['handle']>
269
+ readonly fetch: CompiledRoutes['fetch']
270
+ readonly websocket: Bun.WebSocketHandler<GatewaySocketData>
271
+ }
272
+
273
+ const withGateways = (compiled: CompiledRoutes, gateways: readonly BuiltGateway[]): ServeRoutes => {
274
+ const routes: ServeRoutes['routes'] = { ...compiled.routes }
275
+
276
+ for (const gateway of gateways) {
277
+ if (Object.hasOwn(routes, gateway.path)) {
278
+ throw new ControllerError('DUPLICATE_ROUTE', `The WebSocket gateway on ${gateway.path} has the path of another gateway or route`)
279
+ }
280
+
281
+ routes[gateway.path] = gateway.handle
282
+ }
283
+
284
+ return { routes, fetch: compiled.fetch, websocket: websocketHandler }
285
+ }
286
+
248
287
  /** What Nest logs at startup: the modules, then each controller and its routes. */
249
- const logStartup = (logger: Logger, container: Container, controllers: readonly MappedController[]): void => {
288
+ const logStartup = (
289
+ logger: Logger,
290
+ container: Container,
291
+ controllers: readonly MappedController[],
292
+ gateways: readonly BuiltGateway[]
293
+ ): void => {
250
294
  const modules = logger.child('Container')
251
295
  const routes = logger.child('Routes')
252
296
 
@@ -257,10 +301,14 @@ const logStartup = (logger: Logger, container: Container, controllers: readonly
257
301
  for (const { controller, path, routes: mapped } of controllers) {
258
302
  routes.log(`${controller.name} {${path}}:`)
259
303
 
260
- for (const route of mapped) {
261
- routes.log(`Mapped {${route.path}, ${route.method}} route`)
304
+ for (const { info } of mapped) {
305
+ routes.log(`Mapped {${info.path}, ${info.method}} route`)
262
306
  }
263
307
  }
308
+
309
+ for (const gateway of gateways) {
310
+ routes.log(`WebSocket gateway {${gateway.path}}, events: ${gateway.runtime.events.join(', ') || 'none'}`)
311
+ }
264
312
  }
265
313
 
266
314
  /**
@@ -280,9 +328,11 @@ export const createApp = async (root: ModuleImport, options: AppOptions = {}): P
280
328
 
281
329
  own.log('Starting application')
282
330
 
331
+ // Filled once the routes are built; read by what describes them (OpenAPI).
332
+ const registry: { controllers: readonly MappedController[] } = { controllers: [] }
283
333
  const container = await createContainer(root, {
284
334
  ...options,
285
- providers: [...(options.providers ?? []), provide(Logger, { useValue: logger })],
335
+ providers: [...(options.providers ?? []), provide(Logger, { useValue: logger }), provide(ROUTES, { useValue: registry })],
286
336
  })
287
337
  const enhancers: unknown[] = []
288
338
 
@@ -293,7 +343,8 @@ export const createApp = async (root: ModuleImport, options: AppOptions = {}): P
293
343
  })
294
344
  const built = buildRoutes(container, { ...options, onError })
295
345
 
296
- logStartup(logger, container, built.controllers)
346
+ registry.controllers = built.controllers
347
+ logStartup(logger, container, built.controllers, built.gateways)
297
348
  enhancers.push(...built.enhancers)
298
349
  await initializeAll(enhancers)
299
350
  own.log('Application started')
@@ -303,7 +354,7 @@ export const createApp = async (root: ModuleImport, options: AppOptions = {}): P
303
354
  errorResponse(error, (unknown) => onError(unknown, req))
304
355
  )
305
356
 
306
- return new Application(container, routes, enhancers, own)
357
+ return new Application(container, withGateways(routes, built.gateways), built.gateways, enhancers, own)
307
358
  } catch (error) {
308
359
  try {
309
360
  await destroyEverything(enhancers, container)
package/src/app/index.ts CHANGED
@@ -4,4 +4,4 @@
4
4
  */
5
5
 
6
6
  export { createApp } from './app.ts'
7
- export type { App, AppOptions, ListenOptions } from './app.ts'
7
+ export type { App, AppOptions, ListenOptions, ServeRoutes } from './app.ts'
@@ -0,0 +1,90 @@
1
+ /**
2
+ * @module
3
+ * Configuration typed by a schema and validated at startup.
4
+ *
5
+ * What `@nestjs/config` does with `registerAs()` and a `validationSchema`,
6
+ * typed by the schema instead of read with string keys. Bun already loads
7
+ * `.env`, `.env.local` and `.env.<NODE_ENV>` into `process.env`.
8
+ *
9
+ * ```ts
10
+ * export const AppConfig = defineConfig('app', z.object({
11
+ * PORT: z.coerce.number().default(3000),
12
+ * JWT_SECRET: z.string().min(32),
13
+ * }))
14
+ *
15
+ * @Module({ imports: [ConfigModule.forRoot({ load: [AppConfig], global: true })] })
16
+ * class AppModule {}
17
+ *
18
+ * class AuthService {
19
+ * readonly #config = inject(AppConfig) // { PORT: number; JWT_SECRET: string }
20
+ * }
21
+ * ```
22
+ */
23
+
24
+ import { InjectionToken } from '../di/token.ts'
25
+ import { formatIssues } from '../http/schema.ts'
26
+ import type { InferOutput, StandardSchemaV1 } from '../http/schema.ts'
27
+ import { hideFrameworkFrames } from '../utils/errors.ts'
28
+
29
+ /** Environment variables, as in `process.env`. */
30
+ export type Environment = Readonly<Record<string, string | undefined>>
31
+
32
+ /** A configuration that does not match its schema; `issues` lists every problem. */
33
+ export class ConfigError extends Error {
34
+ override readonly name = 'ConfigError'
35
+
36
+ constructor(
37
+ readonly config: string,
38
+ readonly issues: readonly string[]
39
+ ) {
40
+ super(`Invalid configuration "${config}":\n ${issues.join('\n ')}`)
41
+ hideFrameworkFrames(this)
42
+ }
43
+ }
44
+
45
+ /**
46
+ * A configuration, and the DI token of its value: `inject(AppConfig)`.
47
+ * `ConfigModule.forRoot()` loads it at startup; `load()` works without DI.
48
+ */
49
+ export class Config<T> extends InjectionToken<T> {
50
+ readonly #schema: StandardSchemaV1<unknown, T>
51
+ readonly #source: (env: Environment) => unknown
52
+
53
+ constructor(name: string, schema: StandardSchemaV1<unknown, T>, source: (env: Environment) => unknown) {
54
+ super(name)
55
+ this.#schema = schema
56
+ this.#source = source
57
+ }
58
+
59
+ /** The value read from `env` and validated; throws a {@link ConfigError} listing every issue. */
60
+ async load(env: Environment = process.env): Promise<T> {
61
+ const result = await this.#schema['~standard'].validate(this.#source(env))
62
+
63
+ if (result.issues !== undefined) {
64
+ throw new ConfigError(this.description, formatIssues(result.issues, ''))
65
+ }
66
+
67
+ return result.value
68
+ }
69
+
70
+ override toString(): string {
71
+ return `Config(${this.description})`
72
+ }
73
+ }
74
+
75
+ /**
76
+ * Defines a configuration validated by `schema`. Its input is the environment,
77
+ * or what `source` builds from it, for a nested configuration:
78
+ *
79
+ * ```ts
80
+ * const DatabaseConfig = defineConfig('database', DatabaseSchema, (env) => ({
81
+ * url: env.DATABASE_URL,
82
+ * pool: { max: env.DATABASE_POOL_MAX },
83
+ * }))
84
+ * ```
85
+ */
86
+ export const defineConfig = <S extends StandardSchemaV1>(
87
+ name: string,
88
+ schema: S,
89
+ source: (env: Environment) => unknown = (env) => env
90
+ ): Config<InferOutput<S>> => new Config(name, schema as StandardSchemaV1<unknown, InferOutput<S>>, source)
@@ -0,0 +1,9 @@
1
+ /**
2
+ * @module
3
+ * Configuration typed by a schema and validated at startup.
4
+ */
5
+
6
+ export { Config, ConfigError, defineConfig } from './config.ts'
7
+ export type { Environment } from './config.ts'
8
+ export { ConfigModule } from './module.ts'
9
+ export type { ConfigModuleOptions } from './module.ts'
@@ -0,0 +1,40 @@
1
+ /**
2
+ * @module
3
+ * `ConfigModule`: loads configurations at startup, like `ConfigModule.forRoot()` of `@nestjs/config`.
4
+ */
5
+
6
+ import { Module, provide } from '../di/index.ts'
7
+ import type { DynamicModule } from '../di/index.ts'
8
+ import type { Config, Environment } from './config.ts'
9
+
10
+ export interface ConfigModuleOptions {
11
+ /** Configurations to load and provide. */
12
+ readonly load: readonly Config<unknown>[]
13
+ /** Makes them injectable in every module. Defaults to `false`, as in Nest. */
14
+ readonly global?: boolean
15
+ /** Where variables come from, read when the container starts. Defaults to `process.env`. */
16
+ readonly env?: Environment
17
+ }
18
+
19
+ /**
20
+ * ```ts
21
+ * @Module({ imports: [ConfigModule.forRoot({ load: [AppConfig, DatabaseConfig], global: true })] })
22
+ * class AppModule {}
23
+ * ```
24
+ *
25
+ * Each configuration is loaded when the container starts; an invalid one
26
+ * fails the startup with a `ConfigError` listing all its issues. Tests
27
+ * replace a value with `overrides: [provide(AppConfig, { useValue })]`, and
28
+ * the replaced configuration is not loaded.
29
+ */
30
+ @Module({})
31
+ export class ConfigModule {
32
+ static forRoot(options: ConfigModuleOptions): DynamicModule {
33
+ return {
34
+ module: ConfigModule,
35
+ global: options.global ?? false,
36
+ providers: options.load.map((config) => provide(config, { useFactory: () => config.load(options.env) })),
37
+ exports: options.load,
38
+ }
39
+ }
40
+ }
package/src/di/errors.ts CHANGED
@@ -3,6 +3,8 @@
3
3
  * Errors raised by the dependency injection container.
4
4
  */
5
5
 
6
+ import { hideFrameworkFrames } from '../utils/errors.ts'
7
+
6
8
  /**
7
9
  * Reason of a `DiError`.
8
10
  *
@@ -42,5 +44,6 @@ export class DiError extends Error {
42
44
  message: string
43
45
  ) {
44
46
  super(message)
47
+ hideFrameworkFrames(this)
45
48
  }
46
49
  }