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 +2 -2
- package/README.md +1 -1
- package/package.json +1 -1
- package/packages/core/src/devAdmin.ts +9 -0
- package/packages/core/src/index.ts +2 -1
- package/packages/core/src/metrics.ts +13 -0
- package/packages/core/src/server.ts +22 -3
- package/packages/core/src/testClient.ts +187 -0
package/CLAUDE.md
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
# CLAUDE.md — AI Developer Guide for tina4-nodejs (v3.10.
|
|
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.
|
|
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%
|
|
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.
|
|
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">⚠ 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
|
-
|
|
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
|
+
}
|