@di-framework/di-framework-http 0.0.0-prerelease.100

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/README.md ADDED
@@ -0,0 +1,209 @@
1
+ # @di-framework/di-framework-http
2
+
3
+ Lightweight TypeScript decorators and a type-safe router for [itty-router](https://github.com/kwhitley/itty-router). Includes a build-time OpenAPI 3.1 generator. Ported from `itty-decorators`.
4
+
5
+ ## Features
6
+
7
+ - **Type-Safe Routing**: `TypedRouter` provides full TypeScript support for request bodies, response types, and context.
8
+ - **Auto JSON Enforcement**: Automatically validates `Content-Type: application/json` for mutation methods (POST, PUT, PATCH).
9
+ - **Multipart Support**: Opt into `multipart/form-data` handling with `Multipart<T>` and `{ multipart: true }`.
10
+ - **Declarative Metadata**: Use `@Controller` and `@Endpoint` decorators to document your API logic directly in code.
11
+ - **DI Integration**: `@Controller` composes the core DI `@Container` decorator, so controllers are auto-registered and can use `@Component` injection and `useContainer().resolve(...)`.
12
+ - **OpenAPI 3.1 Support**: Generate a complete OpenAPI specification from your code at build time.
13
+ - **Minimal Footprint**: Built on top of the ultra-light `itty-router`.
14
+
15
+ ## Installation
16
+
17
+ ```bash
18
+ # Install the HTTP package and the core DI framework (peer dependency)
19
+ bun add @di-framework/di-framework-http @di-framework/di-framework
20
+ # or
21
+ npm install @di-framework/di-framework-http @di-framework/di-framework
22
+ ```
23
+
24
+ ## Quick Start
25
+
26
+ ### 1. Create a Controller (DI-aware)
27
+
28
+ Annotate your API logic using decorators and the `TypedRouter`. Controllers are automatically registered with the DI container, so you can inject services and resolve the controller instance.
29
+
30
+ ```typescript
31
+ import {
32
+ TypedRouter,
33
+ json,
34
+ type RequestSpec,
35
+ type ResponseSpec,
36
+ type Json,
37
+ Controller,
38
+ Endpoint,
39
+ } from "@di-framework/di-framework-http";
40
+ import { Component, Container } from "@di-framework/di-framework/decorators";
41
+ import { useContainer } from "@di-framework/di-framework/container";
42
+
43
+ const router = TypedRouter();
44
+
45
+ type EchoPayload = { message: string };
46
+ type EchoResponse = { echoed: string; timestamp: string };
47
+
48
+ // Example DI-managed service
49
+ @Container()
50
+ export class LoggerService {
51
+ log(msg: string) {
52
+ console.log(msg);
53
+ }
54
+ }
55
+
56
+ @Controller()
57
+ export class EchoController {
58
+ // Because @Controller composes the core @Container decorator, this class is
59
+ // automatically registered with the DI container. We can inject services.
60
+ @Component(LoggerService)
61
+ private logger!: LoggerService;
62
+
63
+ echoMessage(message: string): EchoResponse {
64
+ this.logger.log(`Echoing: ${message}`);
65
+ return { echoed: message, timestamp: new Date().toISOString() };
66
+ }
67
+
68
+ @Endpoint({
69
+ summary: "Echo a message",
70
+ description: "Returns the provided message with a server timestamp.",
71
+ responses: {
72
+ "200": { description: "Successful echo" },
73
+ },
74
+ })
75
+ static post = router.post<
76
+ RequestSpec<Json<EchoPayload>>,
77
+ ResponseSpec<EchoResponse>
78
+ >("/echo", (req) => {
79
+ // Demonstrate auto DI registration: resolve the controller instance from
80
+ // the global container without any manual registration.
81
+ const controller = useContainer().resolve(EchoController);
82
+ return json(controller.echoMessage(req.content.message));
83
+ });
84
+ }
85
+
86
+ // Add a simple GET route
87
+ router.get("/", () => json({ message: "API is healthy" }));
88
+
89
+ export default {
90
+ fetch: (request: Request, env: any, ctx: any) =>
91
+ router.fetch(request, env, ctx),
92
+ };
93
+ ```
94
+
95
+ ### 2. Multipart File Uploads
96
+
97
+ Use `Multipart<T>` and `{ multipart: true }` to accept `multipart/form-data` instead of JSON. The handler receives `req.content` typed as `FormData`.
98
+
99
+ ```typescript
100
+ import {
101
+ TypedRouter,
102
+ json,
103
+ type RequestSpec,
104
+ type ResponseSpec,
105
+ type Multipart,
106
+ } from "@di-framework/di-framework-http";
107
+
108
+ const router = TypedRouter();
109
+
110
+ type UploadPayload = { files: File[] };
111
+ type UploadResult = { filenames: string[] };
112
+
113
+ router.post<RequestSpec<Multipart<UploadPayload>>, ResponseSpec<UploadResult>>(
114
+ "/upload",
115
+ (req) => {
116
+ // req.content is typed as FormData
117
+ const files = req.content.getAll("files") as File[];
118
+ return json({ filenames: files.map((f) => f.name) });
119
+ },
120
+ { multipart: true },
121
+ );
122
+ ```
123
+
124
+ ### OpenAPI Generation
125
+
126
+ `@di-framework/di-framework-http` provides a built-in CLI and a registry to generate OpenAPI specs from your controllers.
127
+
128
+ #### Using the CLI
129
+
130
+ The easiest way to generate a spec is using the provided CLI tool.
131
+
132
+ ```bash
133
+ # Generate openapi.json from your controllers
134
+ bun x di-framework-http generate --controllers ./src/index.ts
135
+ ```
136
+
137
+ **Options:**
138
+
139
+ - `--controllers <path>`: (Required) Path to the file that imports all your decorated controllers.
140
+ - `--output <path>`: (Optional) Path to save the generated JSON (default: `openapi.json`).
141
+
142
+ #### Manual Generation
143
+
144
+ You can also generate the spec programmatically using the `generateOpenAPI` function and the default `registry`:
145
+
146
+ ```typescript
147
+ import registry, { generateOpenAPI } from "@di-framework/di-framework-http";
148
+ import "./controllers/MyController"; // Import to trigger registration
149
+
150
+ const spec = generateOpenAPI(
151
+ {
152
+ title: "My API",
153
+ version: "1.0.0",
154
+ },
155
+ registry,
156
+ );
157
+
158
+ console.log(JSON.stringify(spec, null, 2));
159
+ ```
160
+
161
+ If you need full control, you can iterate the `registry` manually:
162
+
163
+ ```typescript
164
+ import registry from "@di-framework/di-framework-http";
165
+
166
+ for (const target of registry.getTargets()) {
167
+ // target is the decorated class
168
+ // target[methodName].isEndpoint will be true
169
+ // target[methodName].metadata contains your @Endpoint info
170
+ }
171
+ ```
172
+
173
+ ## API Reference
174
+
175
+ ### `TypedRouter<Args[]>()`
176
+
177
+ A proxy for `itty-router` that enables type-safe method definitions.
178
+
179
+ - `Args`: An array of types representing additional arguments passed to `fetch` (e.g., `[Env, ExecutionContext]`).
180
+
181
+ ### `json<T>(data: T, init?: ResponseInit)`
182
+
183
+ A typed wrapper around itty-router's `json` helper.
184
+
185
+ ### `Json<T>` / `Multipart<T>`
186
+
187
+ Body spec markers used with `RequestSpec<>` to declare the expected content type. `Json<T>` types `req.content` as `T`; `Multipart<T>` types it as `FormData`. Multipart routes require passing `{ multipart: true }` as the third argument to the route method.
188
+
189
+ ### `@Controller(options?)`
190
+
191
+ Composed decorator that:
192
+
193
+ - Marks a class for inclusion in the OpenAPI registry; and
194
+ - Registers the class with the core DI container (same instance as `@di-framework/di-framework`).
195
+
196
+ **Options:** `{ singleton?: boolean; container?: DIContainer }`
197
+
198
+ ### `@Endpoint(metadata)`
199
+
200
+ Method or property decorator that attaches OpenAPI metadata.
201
+
202
+ - `summary`: Short summary of the operation.
203
+ - `description`: Verbose explanation.
204
+ - `requestBody`: OpenAPI Request Body object.
205
+ - `responses`: OpenAPI Responses object.
206
+
207
+ ## License
208
+
209
+ MIT
@@ -0,0 +1,5 @@
1
+ export * from "./src/typed-router.ts";
2
+ export * from "./src/decorators.ts";
3
+ export * from "./src/openapi.ts";
4
+ export * from "./src/registry.ts";
5
+ export { default as registry } from "./src/registry.ts";
package/dist/index.js ADDED
@@ -0,0 +1,585 @@
1
+ import { createRequire } from "node:module";
2
+ var __create = Object.create;
3
+ var __getProtoOf = Object.getPrototypeOf;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
7
+ var __toESM = (mod, isNodeMode, target) => {
8
+ target = mod != null ? __create(__getProtoOf(mod)) : {};
9
+ const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
10
+ for (let key of __getOwnPropNames(mod))
11
+ if (!__hasOwnProp.call(to, key))
12
+ __defProp(to, key, {
13
+ get: () => mod[key],
14
+ enumerable: true
15
+ });
16
+ return to;
17
+ };
18
+ var __export = (target, all) => {
19
+ for (var name in all)
20
+ __defProp(target, name, {
21
+ get: all[name],
22
+ enumerable: true,
23
+ configurable: true,
24
+ set: (newValue) => all[name] = () => newValue
25
+ });
26
+ };
27
+ var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
28
+ var __require = /* @__PURE__ */ createRequire(import.meta.url);
29
+
30
+ // src/registry.ts
31
+ var exports_registry = {};
32
+ __export(exports_registry, {
33
+ default: () => registry_default,
34
+ Registry: () => Registry
35
+ });
36
+
37
+ class Registry {
38
+ targets = new Set;
39
+ addTarget(target) {
40
+ this.targets.add(target);
41
+ }
42
+ getTargets() {
43
+ return this.targets;
44
+ }
45
+ }
46
+ var registry, registry_default;
47
+ var init_registry = __esm(() => {
48
+ registry = new Registry;
49
+ registry_default = registry;
50
+ });
51
+
52
+ // src/typed-router.ts
53
+ import {
54
+ Router,
55
+ withContent,
56
+ json as ittyJson
57
+ } from "itty-router";
58
+ function json(data, init) {
59
+ return ittyJson(data, init);
60
+ }
61
+ function TypedRouter(opts) {
62
+ const r = Router(opts);
63
+ function enforceJson(req) {
64
+ const ct = (req.headers.get("content-type") ?? "").toLowerCase();
65
+ if (!ct.includes("application/json") && !ct.includes("+json")) {
66
+ return ittyJson({ error: "Content-Type must be application/json" }, { status: 415 });
67
+ }
68
+ return null;
69
+ }
70
+ async function withFormData(req) {
71
+ try {
72
+ req.content = await req.formData();
73
+ } catch {}
74
+ }
75
+ const methodsToProxy = [
76
+ "get",
77
+ "post",
78
+ "put",
79
+ "delete",
80
+ "patch",
81
+ "head",
82
+ "options"
83
+ ];
84
+ const wrapper = new Proxy(r, {
85
+ get(target, prop, receiver) {
86
+ if (typeof prop === "string" && methodsToProxy.includes(prop)) {
87
+ return (path, controller, options) => {
88
+ const handler = (...args) => {
89
+ const req = args[0];
90
+ const extraArgs = args.slice(1);
91
+ if (prop === "post" || prop === "put" || prop === "patch") {
92
+ if (!options?.multipart) {
93
+ const ctErr = enforceJson(req);
94
+ if (ctErr)
95
+ return ctErr;
96
+ }
97
+ }
98
+ return controller(req, ...extraArgs);
99
+ };
100
+ const middleware = options?.multipart ? withFormData : withContent;
101
+ target[prop](path, middleware, handler);
102
+ const routeInfo = {
103
+ path,
104
+ method: prop,
105
+ handler
106
+ };
107
+ Object.assign(handler, routeInfo);
108
+ return handler;
109
+ };
110
+ }
111
+ const value = Reflect.get(target, prop, receiver);
112
+ if (typeof value === "function") {
113
+ return (...args) => {
114
+ const result = value.apply(target, args);
115
+ return result === target ? wrapper : result;
116
+ };
117
+ }
118
+ return value;
119
+ }
120
+ });
121
+ return wrapper;
122
+ }
123
+ // src/decorators.ts
124
+ init_registry();
125
+
126
+ // ../di-framework/dist/container.js
127
+ var INJECT_METADATA_KEY = "di:inject";
128
+ var DESIGN_PARAM_TYPES_KEY = "design:paramtypes";
129
+ var TELEMETRY_METADATA_KEY = "di:telemetry";
130
+ var TELEMETRY_LISTENER_METADATA_KEY = "di:telemetry-listener";
131
+ var PUBLISHER_METADATA_KEY = "di:publisher";
132
+ var SUBSCRIBER_METADATA_KEY = "di:subscriber";
133
+ var metadataStore = new Map;
134
+ function defineMetadata(key, value, target) {
135
+ if (!metadataStore.has(target)) {
136
+ metadataStore.set(target, new Map);
137
+ }
138
+ metadataStore.get(target).set(key, value);
139
+ }
140
+ function getMetadata(key, target) {
141
+ return metadataStore.get(target)?.get(key);
142
+ }
143
+ function getOwnMetadata(key, target) {
144
+ return getMetadata(key, target);
145
+ }
146
+
147
+ class Container {
148
+ services = new Map;
149
+ resolutionStack = new Set;
150
+ listeners = new Map;
151
+ register(serviceClass, options = { singleton: true }) {
152
+ const name = serviceClass.name;
153
+ this.services.set(name, {
154
+ type: serviceClass,
155
+ singleton: options.singleton ?? true
156
+ });
157
+ this.services.set(serviceClass, {
158
+ type: serviceClass,
159
+ singleton: options.singleton ?? true
160
+ });
161
+ this.emit("registered", {
162
+ key: serviceClass,
163
+ singleton: options.singleton ?? true,
164
+ kind: "class"
165
+ });
166
+ return this;
167
+ }
168
+ registerFactory(name, factory, options = { singleton: true }) {
169
+ this.services.set(name, {
170
+ type: factory,
171
+ singleton: options.singleton ?? true
172
+ });
173
+ this.emit("registered", {
174
+ key: name,
175
+ singleton: options.singleton ?? true,
176
+ kind: "factory"
177
+ });
178
+ return this;
179
+ }
180
+ resolve(serviceClass) {
181
+ const key = typeof serviceClass === "string" ? serviceClass : serviceClass;
182
+ const keyStr = typeof serviceClass === "string" ? serviceClass : serviceClass.name;
183
+ if (this.resolutionStack.has(key)) {
184
+ throw new Error(`Circular dependency detected while resolving ${keyStr}. Stack: ${Array.from(this.resolutionStack).join(" -> ")} -> ${keyStr}`);
185
+ }
186
+ const definition = this.services.get(key);
187
+ if (!definition) {
188
+ throw new Error(`Service '${keyStr}' is not registered in the DI container`);
189
+ }
190
+ const wasCached = definition.singleton && !!definition.instance;
191
+ if (definition.singleton && definition.instance) {
192
+ this.emit("resolved", {
193
+ key,
194
+ instance: definition.instance,
195
+ singleton: true,
196
+ fromCache: true
197
+ });
198
+ return definition.instance;
199
+ }
200
+ this.resolutionStack.add(key);
201
+ try {
202
+ const instance = this.instantiate(definition.type);
203
+ if (definition.singleton) {
204
+ definition.instance = instance;
205
+ }
206
+ this.emit("resolved", {
207
+ key,
208
+ instance,
209
+ singleton: definition.singleton,
210
+ fromCache: wasCached
211
+ });
212
+ return instance;
213
+ } finally {
214
+ this.resolutionStack.delete(key);
215
+ }
216
+ }
217
+ construct(serviceClass, overrides = {}) {
218
+ const keyStr = serviceClass.name;
219
+ if (this.resolutionStack.has(serviceClass)) {
220
+ throw new Error(`Circular dependency detected while constructing ${keyStr}. Stack: ${Array.from(this.resolutionStack).join(" -> ")} -> ${keyStr}`);
221
+ }
222
+ this.resolutionStack.add(serviceClass);
223
+ try {
224
+ const instance = this.instantiate(serviceClass, overrides);
225
+ this.emit("constructed", { key: serviceClass, instance, overrides });
226
+ return instance;
227
+ } finally {
228
+ this.resolutionStack.delete(serviceClass);
229
+ }
230
+ }
231
+ has(serviceClass) {
232
+ return this.services.has(serviceClass);
233
+ }
234
+ clear() {
235
+ const count = this.services.size;
236
+ this.services.clear();
237
+ this.emit("cleared", { count });
238
+ }
239
+ getServiceNames() {
240
+ const names = new Set;
241
+ this.services.forEach((_, key) => {
242
+ if (typeof key === "string") {
243
+ names.add(key);
244
+ }
245
+ });
246
+ return Array.from(names);
247
+ }
248
+ fork(options = {}) {
249
+ const clone = new Container;
250
+ this.services.forEach((def, key) => {
251
+ clone.services.set(key, {
252
+ ...def,
253
+ instance: options.carrySingletons ? def.instance : undefined
254
+ });
255
+ });
256
+ return clone;
257
+ }
258
+ on(event, listener) {
259
+ if (!this.listeners.has(event)) {
260
+ this.listeners.set(event, new Set);
261
+ }
262
+ this.listeners.get(event).add(listener);
263
+ return () => this.off(event, listener);
264
+ }
265
+ off(event, listener) {
266
+ this.listeners.get(event)?.delete(listener);
267
+ }
268
+ emit(event, payload) {
269
+ const listeners = this.listeners.get(event);
270
+ if (!listeners || listeners.size === 0)
271
+ return;
272
+ listeners.forEach((listener) => {
273
+ try {
274
+ listener(payload);
275
+ } catch (err) {
276
+ console.error(`[Container] listener for '${String(event)}' threw`, err);
277
+ }
278
+ });
279
+ }
280
+ applyEvents(instance, constructor) {
281
+ const className = constructor.name;
282
+ const subscriberMap = getMetadata(SUBSCRIBER_METADATA_KEY, constructor.prototype) || {};
283
+ Object.entries(subscriberMap).forEach(([event, methods]) => {
284
+ methods.forEach((methodName) => {
285
+ const method = instance[methodName];
286
+ if (typeof method === "function") {
287
+ this.on(event, (payload) => {
288
+ try {
289
+ method.call(instance, payload);
290
+ } catch (err) {
291
+ console.error(`[Container] Subscriber '${className}.${methodName}' for event '${event}' threw`, err);
292
+ }
293
+ });
294
+ }
295
+ });
296
+ });
297
+ const publisherMethods = getMetadata(PUBLISHER_METADATA_KEY, constructor.prototype) || {};
298
+ Object.entries(publisherMethods).forEach(([methodName, options]) => {
299
+ const originalMethod = instance[methodName];
300
+ if (typeof originalMethod === "function") {
301
+ const self = this;
302
+ const phase = options.phase ?? "after";
303
+ instance[methodName] = function(...args) {
304
+ const startTime = Date.now();
305
+ const emit = (result, error) => {
306
+ const payload = {
307
+ className,
308
+ methodName,
309
+ args,
310
+ startTime,
311
+ endTime: Date.now(),
312
+ result,
313
+ error
314
+ };
315
+ if (options.logging) {
316
+ const duration = payload.endTime - payload.startTime;
317
+ const status = error ? `ERROR: ${error && error.message ? error.message : String(error)}` : "SUCCESS";
318
+ console.log(`[Publisher] ${className}.${methodName} -> '${options.event}' - ${status} (${duration}ms)`);
319
+ }
320
+ self.emit(options.event, payload);
321
+ };
322
+ try {
323
+ if (phase === "before" || phase === "both") {
324
+ emit(undefined, undefined);
325
+ }
326
+ const result = originalMethod.apply(this, args);
327
+ if (result instanceof Promise) {
328
+ return result.then((val) => {
329
+ if (phase === "after" || phase === "both") {
330
+ emit(val, undefined);
331
+ }
332
+ return val;
333
+ }).catch((err) => {
334
+ emit(undefined, err);
335
+ throw err;
336
+ });
337
+ }
338
+ if (phase === "after" || phase === "both") {
339
+ emit(result, undefined);
340
+ }
341
+ return result;
342
+ } catch (err) {
343
+ emit(undefined, err);
344
+ throw err;
345
+ }
346
+ };
347
+ }
348
+ });
349
+ }
350
+ instantiate(type, overrides = {}) {
351
+ if (typeof type !== "function") {
352
+ throw new Error("Service type must be a constructor or factory function");
353
+ }
354
+ if (!this.isClass(type)) {
355
+ return type();
356
+ }
357
+ const paramTypes = getMetadata(DESIGN_PARAM_TYPES_KEY, type) || [];
358
+ const paramNames = this.getConstructorParamNames(type);
359
+ const dependencies = [];
360
+ const injectMetadata = getOwnMetadata(INJECT_METADATA_KEY, type) || {};
361
+ const paramCount = Math.max(paramTypes.length, paramNames.length);
362
+ for (let i = 0;i < paramCount; i++) {
363
+ if (Object.prototype.hasOwnProperty.call(overrides, i)) {
364
+ dependencies.push(overrides[i]);
365
+ continue;
366
+ }
367
+ const paramType = paramTypes[i];
368
+ const paramName = paramNames[i];
369
+ const paramInjectTarget = injectMetadata[`param_${i}`];
370
+ if (paramInjectTarget) {
371
+ dependencies.push(this.resolve(paramInjectTarget));
372
+ } else if (paramType && paramType !== Object) {
373
+ if (this.has(paramType)) {
374
+ dependencies.push(this.resolve(paramType));
375
+ } else if (this.has(paramType.name)) {
376
+ dependencies.push(this.resolve(paramType.name));
377
+ } else {
378
+ throw new Error(`Cannot resolve dependency of type ${paramType.name} for parameter '${paramName}' in ${type.name}`);
379
+ }
380
+ } else {}
381
+ }
382
+ const instance = new type(...dependencies);
383
+ this.applyTelemetry(instance, type);
384
+ this.applyEvents(instance, type);
385
+ const injectProperties = getMetadata(INJECT_METADATA_KEY, type) || {};
386
+ const protoInjectProperties = getMetadata(INJECT_METADATA_KEY, type.prototype) || {};
387
+ const allInjectProperties = {
388
+ ...injectProperties,
389
+ ...protoInjectProperties
390
+ };
391
+ Object.entries(allInjectProperties).forEach(([propName, targetType]) => {
392
+ if (!propName.startsWith("param_") && targetType) {
393
+ try {
394
+ instance[propName] = this.resolve(targetType);
395
+ } catch (error) {
396
+ console.warn(`Failed to inject property '${propName}' on ${type.name}:`, error);
397
+ }
398
+ }
399
+ });
400
+ return instance;
401
+ }
402
+ applyTelemetry(instance, constructor) {
403
+ const className = constructor.name;
404
+ const listenerMethods = getMetadata(TELEMETRY_LISTENER_METADATA_KEY, constructor.prototype) || [];
405
+ listenerMethods.forEach((methodName) => {
406
+ const method = instance[methodName];
407
+ if (typeof method === "function") {
408
+ this.on("telemetry", (payload) => {
409
+ try {
410
+ method.call(instance, payload);
411
+ } catch (err) {
412
+ console.error(`[Container] TelemetryListener '${className}.${methodName}' threw`, err);
413
+ }
414
+ });
415
+ }
416
+ });
417
+ const telemetryMethods = getMetadata(TELEMETRY_METADATA_KEY, constructor.prototype) || {};
418
+ Object.entries(telemetryMethods).forEach(([methodName, options]) => {
419
+ const originalMethod = instance[methodName];
420
+ if (typeof originalMethod === "function") {
421
+ const self = this;
422
+ instance[methodName] = function(...args) {
423
+ const startTime = Date.now();
424
+ const emit = (result, error) => {
425
+ const payload = {
426
+ className,
427
+ methodName,
428
+ args,
429
+ startTime,
430
+ endTime: Date.now(),
431
+ result,
432
+ error
433
+ };
434
+ if (options.logging) {
435
+ const duration = payload.endTime - payload.startTime;
436
+ const status = error ? `ERROR: ${error.message || error}` : "SUCCESS";
437
+ console.log(`[Telemetry] ${className}.${methodName} - ${status} (${duration}ms)`);
438
+ }
439
+ self.emit("telemetry", payload);
440
+ };
441
+ try {
442
+ const result = originalMethod.apply(this, args);
443
+ if (result instanceof Promise) {
444
+ return result.then((val) => {
445
+ emit(val);
446
+ return val;
447
+ }).catch((err) => {
448
+ emit(undefined, err);
449
+ throw err;
450
+ });
451
+ }
452
+ emit(result);
453
+ return result;
454
+ } catch (err) {
455
+ emit(undefined, err);
456
+ throw err;
457
+ }
458
+ };
459
+ }
460
+ });
461
+ }
462
+ isClass(func) {
463
+ return typeof func === "function" && func.prototype && func.prototype.constructor === func;
464
+ }
465
+ getConstructorParamNames(target) {
466
+ const funcStr = target.toString();
467
+ const match = funcStr.match(/constructor\s*\(([^)]*)\)/);
468
+ if (!match || !match[1])
469
+ return [];
470
+ const paramsStr = match[1];
471
+ return paramsStr.split(",").map((param) => {
472
+ const trimmed = param.trim();
473
+ const withoutDefault = trimmed.split("=")[0] || "";
474
+ const withoutType = withoutDefault.split(":")[0] || "";
475
+ return withoutType.trim();
476
+ }).filter((param) => param);
477
+ }
478
+ extractParamTypesFromSource(target) {
479
+ const funcStr = target.toString();
480
+ const decoratorMatch = funcStr.match(/__decorate\(\[\s*(?:\w+\s*\([^)]*\),?\s*)*__param\((\d+),\s*(\w+)\([^)]*\)\)/g);
481
+ if (decoratorMatch) {
482
+ return [];
483
+ }
484
+ return [];
485
+ }
486
+ }
487
+ var container = new Container;
488
+ function useContainer() {
489
+ return container;
490
+ }
491
+
492
+ // ../di-framework/dist/decorators.js
493
+ var INJECTABLE_METADATA_KEY = "di:injectable";
494
+ function Container2(options = {}) {
495
+ return function(constructor) {
496
+ const container2 = options.container ?? useContainer();
497
+ const singleton = options.singleton ?? true;
498
+ defineMetadata(INJECTABLE_METADATA_KEY, true, constructor);
499
+ container2.register(constructor, { singleton });
500
+ return constructor;
501
+ };
502
+ }
503
+
504
+ // src/decorators.ts
505
+ function Controller(options = {}) {
506
+ const container2 = Container2(options);
507
+ return function(target) {
508
+ target.isController = true;
509
+ registry_default.addTarget(target);
510
+ container2(target);
511
+ };
512
+ }
513
+ function Endpoint(metadata) {
514
+ return function(target, propertyKey) {
515
+ if (propertyKey) {
516
+ const property = target[propertyKey];
517
+ const constructor = typeof target === "function" ? target : target.constructor;
518
+ registry_default.addTarget(constructor);
519
+ property.isEndpoint = true;
520
+ if (metadata) {
521
+ property.metadata = metadata;
522
+ }
523
+ } else {
524
+ target.isEndpoint = true;
525
+ if (metadata) {
526
+ target.metadata = metadata;
527
+ }
528
+ registry_default.addTarget(target);
529
+ }
530
+ };
531
+ }
532
+ // src/openapi.ts
533
+ init_registry();
534
+ function generateOpenAPI(options = {}, registryToUse = registry_default) {
535
+ const spec = {
536
+ openapi: "3.1.0",
537
+ info: {
538
+ title: options.title || "Generated API",
539
+ version: options.version || "1.0.0",
540
+ description: options.description || "API documentation generated by @di-framework/di-framework-http."
541
+ },
542
+ paths: {},
543
+ components: {
544
+ schemas: {}
545
+ }
546
+ };
547
+ const targets = registryToUse.getTargets();
548
+ for (const target of targets) {
549
+ for (const key of Object.getOwnPropertyNames(target)) {
550
+ const property = target[key];
551
+ if (property && property.isEndpoint) {
552
+ const path = property.path || "/unknown";
553
+ const method = property.method || "get";
554
+ if (!spec.paths[path]) {
555
+ spec.paths[path] = {};
556
+ }
557
+ spec.paths[path][method] = {
558
+ summary: property.metadata?.summary || key,
559
+ description: property.metadata?.description,
560
+ operationId: `${target.name}.${key}`,
561
+ requestBody: property.metadata?.requestBody,
562
+ responses: property.metadata?.responses || {
563
+ "200": {
564
+ description: "OK"
565
+ }
566
+ }
567
+ };
568
+ }
569
+ }
570
+ }
571
+ return spec;
572
+ }
573
+
574
+ // index.ts
575
+ init_registry();
576
+ init_registry();
577
+ export {
578
+ registry_default as registry,
579
+ json,
580
+ generateOpenAPI,
581
+ TypedRouter,
582
+ Registry,
583
+ Endpoint,
584
+ Controller
585
+ };
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env bun
2
+ export {};
@@ -0,0 +1,144 @@
1
+ #!/usr/bin/env bun
2
+ import { createRequire } from "node:module";
3
+ var __create = Object.create;
4
+ var __getProtoOf = Object.getPrototypeOf;
5
+ var __defProp = Object.defineProperty;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __toESM = (mod, isNodeMode, target) => {
9
+ target = mod != null ? __create(__getProtoOf(mod)) : {};
10
+ const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
11
+ for (let key of __getOwnPropNames(mod))
12
+ if (!__hasOwnProp.call(to, key))
13
+ __defProp(to, key, {
14
+ get: () => mod[key],
15
+ enumerable: true
16
+ });
17
+ return to;
18
+ };
19
+ var __export = (target, all) => {
20
+ for (var name in all)
21
+ __defProp(target, name, {
22
+ get: all[name],
23
+ enumerable: true,
24
+ configurable: true,
25
+ set: (newValue) => all[name] = () => newValue
26
+ });
27
+ };
28
+ var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
29
+ var __require = /* @__PURE__ */ createRequire(import.meta.url);
30
+
31
+ // src/registry.ts
32
+ var exports_registry = {};
33
+ __export(exports_registry, {
34
+ default: () => registry_default,
35
+ Registry: () => Registry
36
+ });
37
+
38
+ class Registry {
39
+ targets = new Set;
40
+ addTarget(target) {
41
+ this.targets.add(target);
42
+ }
43
+ getTargets() {
44
+ return this.targets;
45
+ }
46
+ }
47
+ var registry, registry_default;
48
+ var init_registry = __esm(() => {
49
+ registry = new Registry;
50
+ registry_default = registry;
51
+ });
52
+
53
+ // src/cli.ts
54
+ import fs from "fs";
55
+ import path from "path";
56
+
57
+ // src/openapi.ts
58
+ init_registry();
59
+ function generateOpenAPI(options = {}, registryToUse = registry_default) {
60
+ const spec = {
61
+ openapi: "3.1.0",
62
+ info: {
63
+ title: options.title || "Generated API",
64
+ version: options.version || "1.0.0",
65
+ description: options.description || "API documentation generated by @di-framework/di-framework-http."
66
+ },
67
+ paths: {},
68
+ components: {
69
+ schemas: {}
70
+ }
71
+ };
72
+ const targets = registryToUse.getTargets();
73
+ for (const target of targets) {
74
+ for (const key of Object.getOwnPropertyNames(target)) {
75
+ const property = target[key];
76
+ if (property && property.isEndpoint) {
77
+ const path = property.path || "/unknown";
78
+ const method = property.method || "get";
79
+ if (!spec.paths[path]) {
80
+ spec.paths[path] = {};
81
+ }
82
+ spec.paths[path][method] = {
83
+ summary: property.metadata?.summary || key,
84
+ description: property.metadata?.description,
85
+ operationId: `${target.name}.${key}`,
86
+ requestBody: property.metadata?.requestBody,
87
+ responses: property.metadata?.responses || {
88
+ "200": {
89
+ description: "OK"
90
+ }
91
+ }
92
+ };
93
+ }
94
+ }
95
+ }
96
+ return spec;
97
+ }
98
+
99
+ // src/cli.ts
100
+ var args = process.argv.slice(2);
101
+ var command = args[0];
102
+ if (command === "generate") {
103
+ const outputArg = args.indexOf("--output");
104
+ const outputPath = outputArg !== -1 ? args[outputArg + 1] : "openapi.json";
105
+ const controllersArg = args.indexOf("--controllers");
106
+ if (controllersArg === -1) {
107
+ console.error("Error: --controllers path is required");
108
+ process.exit(1);
109
+ }
110
+ const controllersPath = path.resolve(process.cwd(), args[controllersArg + 1]);
111
+ async function run() {
112
+ try {
113
+ const controllersPathResolved = path.resolve(process.cwd(), args[controllersArg + 1]);
114
+ const imported = await import(controllersPathResolved);
115
+ let registryToUse = imported.default || imported.registry;
116
+ if (!registryToUse || typeof registryToUse.getTargets !== "function") {
117
+ try {
118
+ const dfHttp = await import("@di-framework/di-framework-http");
119
+ registryToUse = dfHttp.default || dfHttp.registry;
120
+ } catch {
121
+ const regModule = await Promise.resolve().then(() => (init_registry(), exports_registry));
122
+ registryToUse = regModule.default;
123
+ }
124
+ }
125
+ const spec = generateOpenAPI({
126
+ title: "API Documentation"
127
+ }, registryToUse);
128
+ fs.writeFileSync(outputPath, JSON.stringify(spec, null, 2));
129
+ console.log(`Successfully generated OpenAPI spec at ${outputPath}`);
130
+ } catch (error) {
131
+ console.error(`Error generating OpenAPI spec: ${error.message}`);
132
+ process.exit(1);
133
+ }
134
+ }
135
+ run();
136
+ } else {
137
+ console.log(`
138
+ Usage: di-framework-http generate --controllers <path-to-controllers> [options]
139
+
140
+ Options:
141
+ --controllers <path> Path to a file that imports all your decorated controllers
142
+ --output <path> Path to save the generated JSON (default: openapi.json)
143
+ `);
144
+ }
@@ -0,0 +1,10 @@
1
+ export declare function Controller(options?: {
2
+ singleton?: boolean;
3
+ container?: any;
4
+ }): (target: any) => void;
5
+ export declare function Endpoint(metadata?: {
6
+ summary?: string;
7
+ description?: string;
8
+ requestBody?: any;
9
+ responses?: Record<string, any>;
10
+ }): (target: any, propertyKey?: string) => void;
@@ -0,0 +1,7 @@
1
+ export type OpenAPIOptions = {
2
+ title?: string;
3
+ version?: string;
4
+ description?: string;
5
+ outputPath?: string;
6
+ };
7
+ export declare function generateOpenAPI(options?: OpenAPIOptions, registryToUse?: import("./registry.ts").Registry): any;
@@ -0,0 +1,21 @@
1
+ export interface EndpointMetadata {
2
+ summary?: string;
3
+ description?: string;
4
+ requestBody?: any;
5
+ responses?: Record<string, any>;
6
+ [key: string]: any;
7
+ }
8
+ export interface RegisteredEndpoint {
9
+ target: any;
10
+ propertyKey: string;
11
+ path: string;
12
+ method: string;
13
+ metadata: EndpointMetadata;
14
+ }
15
+ export declare class Registry {
16
+ private targets;
17
+ addTarget(target: any): void;
18
+ getTargets(): Set<any>;
19
+ }
20
+ declare const registry: Registry;
21
+ export default registry;
@@ -0,0 +1,53 @@
1
+ import { Router, type IRequest } from "itty-router";
2
+ /** Marker for body "shape + content-type" */
3
+ export type Json<T> = {
4
+ readonly __kind: "json";
5
+ readonly __type?: T;
6
+ };
7
+ export type Multipart<T> = {
8
+ readonly __kind: "multipart";
9
+ readonly __type?: T;
10
+ };
11
+ /** Spec types you’ll use in generics */
12
+ export type RequestSpec<BodySpec = unknown> = {
13
+ readonly __req?: BodySpec;
14
+ };
15
+ export type ResponseSpec<Body = unknown> = {
16
+ readonly __res?: Body;
17
+ };
18
+ /** Map a BodySpec to the actual req.content type */
19
+ type ContentOf<BodySpec> = BodySpec extends Json<infer T> ? T : BodySpec extends Multipart<infer _T> ? FormData : unknown;
20
+ /** The actual request type your handlers receive */
21
+ export type TypedRequest<ReqSpec> = IRequest & {
22
+ content: ContentOf<ReqSpec extends RequestSpec<infer B> ? B : never>;
23
+ };
24
+ /** Typed response helper (phantom only) */
25
+ export type TypedResponse<ResSpec> = globalThis.Response & {
26
+ readonly __typedRes?: ResSpec;
27
+ };
28
+ /** HandlerController type derived from the Request/Response specs */
29
+ export type HandlerController<ReqSpec, ResSpec, Args extends any[] = any[]> = (req: TypedRequest<ReqSpec>, ...args: Args) => TypedResponse<ResSpec> | Promise<TypedResponse<ResSpec>>;
30
+ /** A typed json() that returns a Response annotated with Response<T> */
31
+ export declare function json<T>(data: T, init?: ResponseInit): TypedResponse<ResponseSpec<T>>;
32
+ export type RouteOptions = {
33
+ multipart?: boolean;
34
+ };
35
+ export type TypedRoute<Args extends any[] = any[]> = <ReqSpec = RequestSpec<unknown>, ResSpec = ResponseSpec<unknown>>(path: string, controller: HandlerController<ReqSpec, ResSpec, Args>, options?: RouteOptions) => TypedRouterType<Args> & {
36
+ path: string;
37
+ method: string;
38
+ reqSpec: ReqSpec;
39
+ resSpec: ResSpec;
40
+ };
41
+ export type TypedRouterType<Args extends any[] = any[]> = {
42
+ get: TypedRoute<Args>;
43
+ post: TypedRoute<Args>;
44
+ put: TypedRoute<Args>;
45
+ delete: TypedRoute<Args>;
46
+ patch: TypedRoute<Args>;
47
+ head: TypedRoute<Args>;
48
+ options: TypedRoute<Args>;
49
+ fetch: (request: Request, ...args: Args) => Promise<Response>;
50
+ [key: string]: any;
51
+ };
52
+ export declare function TypedRouter<Args extends any[] = any[]>(opts?: Parameters<typeof Router>[0]): TypedRouterType<Args>;
53
+ export {};
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "@di-framework/di-framework-http",
3
+ "version": "0.0.0-prerelease.100",
4
+ "description": "Extends di-framework with HTTP features",
5
+ "main": "./dist/index.js",
6
+ "module": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "private": false,
9
+ "exports": {
10
+ ".": {
11
+ "bun": "./index.ts",
12
+ "import": "./dist/index.js",
13
+ "default": "./dist/index.js"
14
+ }
15
+ },
16
+ "author": "github.com/geoffsee",
17
+ "license": "MIT",
18
+ "repository": "https://github.com/geoffsee/di-framework",
19
+ "bin": {
20
+ "di-framework-http": "./dist/src/cli.js"
21
+ },
22
+ "type": "module",
23
+ "files": [
24
+ "dist",
25
+ "README.md"
26
+ ],
27
+ "scripts": {
28
+ "build": "rm -rf dist && bun build ./index.ts ./src/cli.ts --outdir ./dist --target node --external itty-router && tsc --emitDeclarationOnly --declaration --outDir dist",
29
+ "prepublishOnly": "npm run build",
30
+ "dev": "bun ../examples/http-router/index.ts",
31
+ "test": "bun test"
32
+ },
33
+ "dependencies": {
34
+ "itty-router": "^5.0.22"
35
+ },
36
+ "devDependencies": {
37
+ "@types/bun": "latest",
38
+ "typescript": "^5"
39
+ },
40
+ "peerDependencies": {
41
+ "@di-framework/di-framework": "0.0.0-prerelease-0",
42
+ "typescript": "^5"
43
+ }
44
+ }