tina4-nodejs 3.10.41 → 3.10.42

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/CLAUDE.md CHANGED
@@ -1,10 +1,10 @@
1
- # CLAUDE.md — AI Developer Guide for tina4-nodejs (v3.10.41)
1
+ # CLAUDE.md — AI Developer Guide for tina4-nodejs (v3.10.42)
2
2
 
3
3
  > This file helps AI assistants (Claude, Copilot, Cursor, etc.) understand and work on this codebase effectively.
4
4
 
5
5
  ## What This Project Is
6
6
 
7
- Tina4 for Node.js/TypeScript v3.10.41 — a convention-over-configuration structural paradigm. **Not a framework.** The developer writes TypeScript; Tina4 is invisible infrastructure.
7
+ Tina4 for Node.js/TypeScript v3.10.42 — a convention-over-configuration structural paradigm. **Not a framework.** The developer writes TypeScript; Tina4 is invisible infrastructure.
8
8
 
9
9
  The philosophy: zero ceremony, batteries included, file system as source of truth.
10
10
 
package/README.md CHANGED
@@ -6,7 +6,7 @@
6
6
  <p align="center">54 built-in features. Zero dependencies. One import, everything works.</p>
7
7
  <p align="center">
8
8
  <a href="https://www.npmjs.com/package/tina4-nodejs"><img src="https://img.shields.io/npm/v/tina4-nodejs?color=7b1fa2&label=npm" alt="npm"></a>
9
- <img src="https://img.shields.io/badge/tests-1%2C950%20passing-brightgreen" alt="Tests">
9
+ <img src="https://img.shields.io/badge/tests-1%2C812%20passing-brightgreen" alt="Tests">
10
10
  <img src="https://img.shields.io/badge/features-54-blue" alt="Features">
11
11
  <img src="https://img.shields.io/badge/dependencies-0-brightgreen" alt="Zero Deps">
12
12
  <a href="https://tina4.com"><img src="https://img.shields.io/badge/docs-tina4.com-7b1fa2" alt="Docs"></a>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tina4-nodejs",
3
- "version": "3.10.41",
3
+ "version": "3.10.42",
4
4
  "type": "module",
5
5
  "description": "Tina4 for Node.js/TypeScript — 54 built-in features, zero dependencies",
6
6
  "keywords": ["tina4", "framework", "web", "api", "orm", "graphql", "websocket", "typescript"],
@@ -2185,6 +2185,15 @@ function drillDownFile(path){
2185
2185
  });
2186
2186
  html+='</div>';
2187
2187
  }
2188
+ if(d.warnings&&d.warnings.length){
2189
+ html+='<h3 style="margin:0.75rem 0 0.25rem;color:#eab308;font-size:0.85rem">&#9888; Warnings</h3>';
2190
+ html+='<div style="display:flex;flex-direction:column;gap:4px">';
2191
+ d.warnings.forEach(function(w){
2192
+ html+='<div style="padding:4px 8px;background:rgba(234,179,8,0.08);border-left:3px solid #eab308;border-radius:0 4px 4px 0;font-size:0.75rem;font-family:var(--mono);color:var(--text)">';
2193
+ html+='<span style="color:#eab308;margin-right:6px">L'+w.line+'</span>'+w.message+'</div>';
2194
+ });
2195
+ html+='</div>';
2196
+ }
2188
2197
  dd.querySelector('.p-md').innerHTML=html;
2189
2198
  }).catch(function(e){
2190
2199
  dd.querySelector('.p-md').innerHTML='<p style="color:var(--danger)">Error: '+e.message+'</p>';
@@ -12,7 +12,7 @@ export type {
12
12
  WebSocketRouteDefinition,
13
13
  } from "./types.js";
14
14
 
15
- export { startServer, resolvePortAndHost } from "./server.js";
15
+ export { startServer, resolvePortAndHost, handle } from "./server.js";
16
16
  export { Router, RouteGroup, RouteRef, defaultRouter, runRouteMiddlewares } from "./router.js";
17
17
  export { get, post, put, patch, del, any, websocket, del as delete } from "./router.js";
18
18
  export type { RouteInfo } from "./router.js";
@@ -94,6 +94,7 @@ export type { ValkeySessionConfig } from "./sessionHandlers/valkeyHandler.js";
94
94
  export { RedisNpmSessionHandler } from "./sessionHandlers/redisHandler.js";
95
95
  export type { RedisNpmSessionConfig } from "./sessionHandlers/redisHandler.js";
96
96
  export { tests, assertEqual, assertThrows, assertTrue, assertFalse, runAllTests, resetTests } from "./testing.js";
97
+ export { TestClient, TestResponse } from "./testClient.js";
97
98
  export { Container, container } from "./container.js";
98
99
  export { Validator } from "./validator.js";
99
100
  export type { ValidationError } from "./validator.js";
@@ -789,6 +789,18 @@ export function fileDetail(filePath: string): Record<string, any> {
789
789
  // Remove file field from function info for single-file detail
790
790
  const cleanFunctions = functions.map(({ file, ...rest }) => rest);
791
791
 
792
+ // Detect empty methods/functions (loc <= 1 means only a brace or pass-through)
793
+ const warnings: { type: string; message: string; line: number }[] = [];
794
+ for (const fn of cleanFunctions) {
795
+ if (fn.loc <= 1) {
796
+ warnings.push({
797
+ type: "empty_method",
798
+ message: `Method '${fn.name}' appears to be empty`,
799
+ line: fn.line,
800
+ });
801
+ }
802
+ }
803
+
792
804
  return {
793
805
  path: filePath,
794
806
  loc,
@@ -796,5 +808,6 @@ export function fileDetail(filePath: string): Record<string, any> {
796
808
  classes,
797
809
  functions: cleanFunctions,
798
810
  imports,
811
+ warnings,
799
812
  };
800
813
  }
@@ -1,4 +1,4 @@
1
- import { createServer } from "node:http";
1
+ import { createServer, IncomingMessage, ServerResponse } from "node:http";
2
2
  import { resolve, dirname, join, relative } from "node:path";
3
3
  import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
4
4
  import { isatty } from "node:tty";
@@ -369,6 +369,20 @@ function deployGallery(name) {
369
369
  </html>`;
370
370
  }
371
371
 
372
+ // Module-level dispatch function — assigned when startServer() is called.
373
+ // Allows handle() to route requests without requiring a reference to the server.
374
+ let _dispatchFn: ((rawReq: IncomingMessage, rawRes: ServerResponse) => Promise<void>) | null = null;
375
+
376
+ /**
377
+ * Dispatch a raw Node.js request through the Tina4 router and write the response.
378
+ * Requires startServer() to have been called first.
379
+ * Useful for testing and embedding.
380
+ */
381
+ export async function handle(rawReq: IncomingMessage, rawRes: ServerResponse): Promise<void> {
382
+ if (!_dispatchFn) throw new Error("Tina4 server not started — call startServer() first");
383
+ return _dispatchFn(rawReq, rawRes);
384
+ }
385
+
372
386
  export async function startServer(config?: Tina4Config): Promise<{
373
387
  close: () => void;
374
388
  router: Router;
@@ -578,7 +592,7 @@ ${reset}
578
592
  console.log(` Dev dashboard at \x1b[36mhttp://localhost:${port}/__dev\x1b[0m`);
579
593
  }
580
594
 
581
- const server = createServer(async (rawReq, rawRes) => {
595
+ async function dispatch(rawReq: IncomingMessage, rawRes: ServerResponse): Promise<void> {
582
596
  const req = createRequest(rawReq);
583
597
  const res = createResponse(rawRes);
584
598
 
@@ -808,7 +822,12 @@ ${reset}
808
822
  }
809
823
  }
810
824
  }
811
- });
825
+ }
826
+
827
+ // Assign to module-level so handle() can dispatch without a server reference
828
+ _dispatchFn = dispatch;
829
+
830
+ const server = createServer(dispatch);
812
831
 
813
832
  return new Promise((resolvePromise) => {
814
833
  server.listen(port, host, () => {
@@ -0,0 +1,187 @@
1
+ /**
2
+ * Tina4 Test Client — Test routes without starting a server.
3
+ *
4
+ * Usage:
5
+ *
6
+ * import { TestClient } from "@tina4/core";
7
+ *
8
+ * const client = new TestClient(router);
9
+ *
10
+ * const response = await client.get("/api/users");
11
+ * assert(response.status === 200);
12
+ * assert(response.json().users);
13
+ *
14
+ * const response = await client.post("/api/users", { json: { name: "Alice" } });
15
+ * assert(response.status === 201);
16
+ */
17
+ import { IncomingMessage, ServerResponse } from "node:http";
18
+ import { Socket } from "node:net";
19
+ import { createRequest, parseBody } from "./request.js";
20
+ import { createResponse } from "./response.js";
21
+ import { defaultRouter, type Router } from "./router.js";
22
+
23
+ export class TestResponse {
24
+ public readonly status: number;
25
+ public readonly body: string;
26
+ public readonly headers: Record<string, string>;
27
+ public readonly contentType: string;
28
+
29
+ constructor(statusCode: number, headers: Record<string, string>, body: string) {
30
+ this.status = statusCode;
31
+ this.body = body;
32
+ this.headers = headers;
33
+ this.contentType = headers["content-type"] ?? "";
34
+ }
35
+
36
+ /** Parse body as JSON. */
37
+ json(): unknown {
38
+ if (!this.body) return null;
39
+ try {
40
+ return JSON.parse(this.body);
41
+ } catch {
42
+ return null;
43
+ }
44
+ }
45
+
46
+ /** Return body as a string. */
47
+ text(): string {
48
+ return this.body;
49
+ }
50
+
51
+ toString(): string {
52
+ return `<TestResponse status=${this.status} contentType="${this.contentType}">`;
53
+ }
54
+ }
55
+
56
+ interface RequestOptions {
57
+ json?: Record<string, unknown> | unknown[];
58
+ body?: string;
59
+ headers?: Record<string, string>;
60
+ }
61
+
62
+ export class TestClient {
63
+ private router: Router;
64
+
65
+ constructor(router?: Router) {
66
+ this.router = router ?? defaultRouter;
67
+ }
68
+
69
+ /** Send a GET request. */
70
+ async get(path: string, options?: RequestOptions): Promise<TestResponse> {
71
+ return this._request("GET", path, options);
72
+ }
73
+
74
+ /** Send a POST request. */
75
+ async post(path: string, options?: RequestOptions): Promise<TestResponse> {
76
+ return this._request("POST", path, options);
77
+ }
78
+
79
+ /** Send a PUT request. */
80
+ async put(path: string, options?: RequestOptions): Promise<TestResponse> {
81
+ return this._request("PUT", path, options);
82
+ }
83
+
84
+ /** Send a PATCH request. */
85
+ async patch(path: string, options?: RequestOptions): Promise<TestResponse> {
86
+ return this._request("PATCH", path, options);
87
+ }
88
+
89
+ /** Send a DELETE request. */
90
+ async delete(path: string, options?: RequestOptions): Promise<TestResponse> {
91
+ return this._request("DELETE", path, options);
92
+ }
93
+
94
+ /** Build a mock request, match the route, execute the handler. */
95
+ private async _request(method: string, path: string, options?: RequestOptions): Promise<TestResponse> {
96
+ const { json, body, headers } = options ?? {};
97
+
98
+ // Build raw body
99
+ let rawBody = "";
100
+ let contentType = "";
101
+ if (json !== undefined) {
102
+ rawBody = JSON.stringify(json);
103
+ contentType = "application/json";
104
+ } else if (body !== undefined) {
105
+ rawBody = body;
106
+ }
107
+
108
+ // Build headers
109
+ const reqHeaders: Record<string, string> = {};
110
+ if (headers) {
111
+ for (const [k, v] of Object.entries(headers)) {
112
+ reqHeaders[k.toLowerCase()] = v;
113
+ }
114
+ }
115
+ if (contentType && !reqHeaders["content-type"]) {
116
+ reqHeaders["content-type"] = contentType;
117
+ }
118
+ if (rawBody && !reqHeaders["content-length"]) {
119
+ reqHeaders["content-length"] = String(Buffer.byteLength(rawBody));
120
+ }
121
+
122
+ // Create a mock IncomingMessage
123
+ const socket = new Socket();
124
+ const rawReq = new IncomingMessage(socket);
125
+ rawReq.method = method.toUpperCase();
126
+ rawReq.url = path;
127
+ rawReq.headers = { ...reqHeaders, host: "localhost:7145" };
128
+
129
+ // Push body data into the readable stream
130
+ if (rawBody) {
131
+ rawReq.push(Buffer.from(rawBody));
132
+ }
133
+ rawReq.push(null); // signal end of stream
134
+
135
+ // Create a mock ServerResponse that captures output
136
+ const rawRes = new ServerResponse(rawReq);
137
+ const chunks: Buffer[] = [];
138
+ const originalWrite = rawRes.write.bind(rawRes);
139
+ const originalEnd = rawRes.end.bind(rawRes);
140
+
141
+ rawRes.write = function (chunk: any, ...args: any[]): boolean {
142
+ if (chunk) chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk)));
143
+ return true;
144
+ } as typeof rawRes.write;
145
+
146
+ rawRes.end = function (chunk?: any, ...args: any[]): ServerResponse {
147
+ if (chunk) chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk)));
148
+ return rawRes;
149
+ } as typeof rawRes.end;
150
+
151
+ // Create Tina4 request/response wrappers
152
+ const req = createRequest(rawReq);
153
+ const res = createResponse(rawRes);
154
+
155
+ // Parse body (populates req.body)
156
+ await parseBody(req);
157
+
158
+ // Split path for route matching
159
+ const cleanPath = path.includes("?") ? path.split("?")[0] : path;
160
+
161
+ // Match route
162
+ const match = this.router.match(method.toUpperCase(), cleanPath);
163
+ if (!match) {
164
+ return new TestResponse(404, { "content-type": "application/json" }, '{"error":"Not found"}');
165
+ }
166
+
167
+ // Inject route params
168
+ req.params = match.params;
169
+
170
+ // Execute handler
171
+ await match.handler(req, res);
172
+
173
+ // Collect response
174
+ const responseBody = Buffer.concat(chunks).toString();
175
+ const responseHeaders: Record<string, string> = {};
176
+ for (const [name, value] of Object.entries(rawRes.getHeaders())) {
177
+ if (value !== undefined) {
178
+ responseHeaders[name] = Array.isArray(value) ? value.join(", ") : String(value);
179
+ }
180
+ }
181
+
182
+ // Clean up the socket
183
+ socket.destroy();
184
+
185
+ return new TestResponse(rawRes.statusCode, responseHeaders, responseBody);
186
+ }
187
+ }