tina4-nodejs 3.12.9 → 3.12.10
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/package.json +8 -13
- package/packages/core/src/router.ts +16 -75
- package/packages/core/src/server.ts +0 -60
package/CLAUDE.md
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
# CLAUDE.md — AI Developer Guide for tina4-nodejs (v3.12.
|
|
1
|
+
# CLAUDE.md — AI Developer Guide for tina4-nodejs (v3.12.10)
|
|
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.12.
|
|
7
|
+
Tina4 for Node.js/TypeScript v3.12.10 — The Intelligent Native Application 4ramework. A convention-over-configuration structural paradigm. 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/package.json
CHANGED
|
@@ -1,18 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tina4-nodejs",
|
|
3
|
-
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
"version": "3.12.10",
|
|
7
|
+
|
|
4
8
|
"type": "module",
|
|
5
|
-
"description": "Tina4 for Node.js/TypeScript
|
|
6
|
-
"keywords": [
|
|
7
|
-
"tina4",
|
|
8
|
-
"framework",
|
|
9
|
-
"web",
|
|
10
|
-
"api",
|
|
11
|
-
"orm",
|
|
12
|
-
"graphql",
|
|
13
|
-
"websocket",
|
|
14
|
-
"typescript"
|
|
15
|
-
],
|
|
9
|
+
"description": "Tina4 for Node.js/TypeScript — 54 built-in features, zero dependencies",
|
|
10
|
+
"keywords": ["tina4", "framework", "web", "api", "orm", "graphql", "websocket", "typescript"],
|
|
16
11
|
"homepage": "https://tina4.com/nodejs",
|
|
17
12
|
"repository": {
|
|
18
13
|
"type": "git",
|
|
@@ -64,4 +59,4 @@
|
|
|
64
59
|
"tsx": "^4.19.0",
|
|
65
60
|
"esbuild": "^0.24.0"
|
|
66
61
|
}
|
|
67
|
-
}
|
|
62
|
+
}
|
|
@@ -190,28 +190,6 @@ export class Router {
|
|
|
190
190
|
return this.addRoute({ method: "DELETE", pattern: path, handler, middlewares, meta });
|
|
191
191
|
}
|
|
192
192
|
|
|
193
|
-
/**
|
|
194
|
-
* Register an explicit HEAD route. By default the framework auto-handles
|
|
195
|
-
* HEAD by falling back to the GET route and stripping the body
|
|
196
|
-
* (RFC 9110 §9.3.2). Use this only when you need a HEAD handler that
|
|
197
|
-
* does something different from GET — e.g. cheaper existence-check
|
|
198
|
-
* logic, custom validator headers without the cost of building the body.
|
|
199
|
-
* The framework still strips the response body for you on the way out.
|
|
200
|
-
*/
|
|
201
|
-
head(path: string, handler: RouteHandler, middlewares?: Middleware[], meta?: RouteMeta): RouteRef {
|
|
202
|
-
return this.addRoute({ method: "HEAD", pattern: path, handler, middlewares, meta });
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
/**
|
|
206
|
-
* Register an explicit OPTIONS route. By default the framework auto-
|
|
207
|
-
* handles OPTIONS by building an Allow header from every method
|
|
208
|
-
* registered for the path and returning 204 (RFC 9110 §9.3.7). Use
|
|
209
|
-
* this to take over that behaviour.
|
|
210
|
-
*/
|
|
211
|
-
options(path: string, handler: RouteHandler, middlewares?: Middleware[], meta?: RouteMeta): RouteRef {
|
|
212
|
-
return this.addRoute({ method: "OPTIONS", pattern: path, handler, middlewares, meta });
|
|
213
|
-
}
|
|
214
|
-
|
|
215
193
|
/**
|
|
216
194
|
* Register a route that matches ANY HTTP method.
|
|
217
195
|
*/
|
|
@@ -244,65 +222,28 @@ export class Router {
|
|
|
244
222
|
|
|
245
223
|
// Try exact method first, then ANY routes are already registered under each method
|
|
246
224
|
const routes = this.routes.get(upperMethod);
|
|
247
|
-
if (routes)
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
)
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
// RFC 9110 §9.3.2: HEAD is identical to GET except for the absence
|
|
267
|
-
// of a response body. If no explicit HEAD route matched, fall back
|
|
268
|
-
// to the GET route — the dispatcher strips the body on the way out.
|
|
269
|
-
if (upperMethod === "HEAD") {
|
|
270
|
-
const getRoutes = this.routes.get("GET");
|
|
271
|
-
if (getRoutes) {
|
|
272
|
-
return this.matchRoute(getRoutes, path);
|
|
225
|
+
if (!routes) return null;
|
|
226
|
+
|
|
227
|
+
const direct = this.matchRoute(routes, path);
|
|
228
|
+
if (direct) return direct;
|
|
229
|
+
|
|
230
|
+
// Trailing-slash redirect — strip a single trailing "/" and retry.
|
|
231
|
+
// Root "/" is intentionally excluded (it's its own route).
|
|
232
|
+
if (
|
|
233
|
+
isTrailingSlashRedirectEnabled() &&
|
|
234
|
+
path.length > 1 &&
|
|
235
|
+
path.endsWith("/")
|
|
236
|
+
) {
|
|
237
|
+
const stripped = path.replace(/\/+$/, "");
|
|
238
|
+
if (stripped.length > 0) {
|
|
239
|
+
const retry = this.matchRoute(routes, stripped);
|
|
240
|
+
if (retry) return retry;
|
|
273
241
|
}
|
|
274
242
|
}
|
|
275
243
|
|
|
276
244
|
return null;
|
|
277
245
|
}
|
|
278
246
|
|
|
279
|
-
/**
|
|
280
|
-
* Return the list of HTTP methods registered for ``path``, in canonical
|
|
281
|
-
* order GET / POST / PUT / PATCH / DELETE / HEAD / OPTIONS. Used by the
|
|
282
|
-
* dispatcher to build the ``Allow:`` header on 405 / OPTIONS responses
|
|
283
|
-
* (RFC 9110 §10.2.1, §9.3.7).
|
|
284
|
-
*
|
|
285
|
-
* If GET is registered, HEAD is appended implicitly (HEAD auto-fallback).
|
|
286
|
-
* OPTIONS is appended whenever any method exists for the path (the
|
|
287
|
-
* framework auto-handles OPTIONS).
|
|
288
|
-
*/
|
|
289
|
-
methodsAllowedForPath(path: string): string[] {
|
|
290
|
-
const order = ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"];
|
|
291
|
-
const seen = new Set<string>();
|
|
292
|
-
for (const m of order) {
|
|
293
|
-
const routes = this.routes.get(m);
|
|
294
|
-
if (!routes) continue;
|
|
295
|
-
if (this.matchRoute(routes, path)) {
|
|
296
|
-
seen.add(m);
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
if (seen.size > 0) {
|
|
300
|
-
if (seen.has("GET")) seen.add("HEAD");
|
|
301
|
-
seen.add("OPTIONS");
|
|
302
|
-
}
|
|
303
|
-
return order.filter((m) => seen.has(m));
|
|
304
|
-
}
|
|
305
|
-
|
|
306
247
|
/** Inner match against a list of compiled routes, no trailing-slash logic. */
|
|
307
248
|
private matchRoute(routes: CompiledRoute[], path: string): MatchResult | null {
|
|
308
249
|
for (const route of routes) {
|
|
@@ -913,36 +913,6 @@ ${reset}
|
|
|
913
913
|
const req = createRequest(rawReq);
|
|
914
914
|
const res = createResponse(rawRes);
|
|
915
915
|
|
|
916
|
-
// RFC 9110 §9.3.2: the server MUST NOT send content in a HEAD response.
|
|
917
|
-
// Intercept rawRes.write / rawRes.end so every code path — explicit
|
|
918
|
-
// Router.head() handler, GET auto-fallback, 405 / 404 responses — drops
|
|
919
|
-
// its body. Content-Length is preserved when present, so cache
|
|
920
|
-
// validators / link checkers / monitoring probes still see the size
|
|
921
|
-
// the equivalent GET would have sent.
|
|
922
|
-
if ((rawReq.method ?? "GET").toUpperCase() === "HEAD") {
|
|
923
|
-
const origEnd = rawRes.end.bind(rawRes);
|
|
924
|
-
const origWrite = rawRes.write.bind(rawRes);
|
|
925
|
-
let accumulated = 0;
|
|
926
|
-
rawRes.write = ((chunk?: any, _enc?: any, cb?: any): boolean => {
|
|
927
|
-
if (chunk != null) {
|
|
928
|
-
accumulated += Buffer.isBuffer(chunk) ? chunk.length : Buffer.byteLength(String(chunk));
|
|
929
|
-
}
|
|
930
|
-
if (typeof cb === "function") cb();
|
|
931
|
-
return true;
|
|
932
|
-
}) as typeof rawRes.write;
|
|
933
|
-
rawRes.end = ((chunk?: any, _enc?: any, cb?: any): any => {
|
|
934
|
-
if (chunk != null && typeof chunk !== "function") {
|
|
935
|
-
accumulated += Buffer.isBuffer(chunk) ? chunk.length : Buffer.byteLength(String(chunk));
|
|
936
|
-
}
|
|
937
|
-
if (accumulated > 0 && !rawRes.headersSent && !rawRes.hasHeader("Content-Length")) {
|
|
938
|
-
rawRes.setHeader("Content-Length", String(accumulated));
|
|
939
|
-
}
|
|
940
|
-
const realCb = typeof chunk === "function" ? chunk : cb;
|
|
941
|
-
return origEnd(undefined, undefined, realCb);
|
|
942
|
-
void origWrite; // referenced to keep tsc happy
|
|
943
|
-
}) as typeof rawRes.end;
|
|
944
|
-
}
|
|
945
|
-
|
|
946
916
|
// Auto-start session — read cookie, create session, save + set cookie on response end
|
|
947
917
|
{
|
|
948
918
|
const { Session, buildSessionCookie } = await import("./session.js");
|
|
@@ -1200,36 +1170,6 @@ ${reset}
|
|
|
1200
1170
|
}
|
|
1201
1171
|
}
|
|
1202
1172
|
|
|
1203
|
-
// RFC 9110 conformance — before falling through to 404, check whether
|
|
1204
|
-
// the PATH is registered under any OTHER method.
|
|
1205
|
-
// - OPTIONS request → 204 with Allow header (§9.3.7)
|
|
1206
|
-
// - Any other method (PUT on GET-only, TRACE, CONNECT, etc.)
|
|
1207
|
-
// → 405 with Allow header (§15.5.6 + §10.2.1)
|
|
1208
|
-
const allowedMethods = router.methodsAllowedForPath(pathname);
|
|
1209
|
-
if (allowedMethods.length > 0) {
|
|
1210
|
-
const allowHeader = allowedMethods.join(", ");
|
|
1211
|
-
const requestMethod = (req.method ?? "GET").toUpperCase();
|
|
1212
|
-
if (requestMethod === "OPTIONS") {
|
|
1213
|
-
res.raw.writeHead(204, undefined, { Allow: allowHeader, "Content-Length": "0" });
|
|
1214
|
-
res.raw.end();
|
|
1215
|
-
return;
|
|
1216
|
-
}
|
|
1217
|
-
const body = JSON.stringify({
|
|
1218
|
-
error: "Method Not Allowed",
|
|
1219
|
-
path: pathname,
|
|
1220
|
-
method: requestMethod,
|
|
1221
|
-
allow: allowedMethods,
|
|
1222
|
-
statusCode: 405,
|
|
1223
|
-
});
|
|
1224
|
-
res.raw.writeHead(405, httpReason(405), {
|
|
1225
|
-
Allow: allowHeader,
|
|
1226
|
-
"Content-Type": "application/json",
|
|
1227
|
-
"Content-Length": String(Buffer.byteLength(body)),
|
|
1228
|
-
});
|
|
1229
|
-
res.raw.end(body);
|
|
1230
|
-
return;
|
|
1231
|
-
}
|
|
1232
|
-
|
|
1233
1173
|
// 404 — pass canonical reason phrase so the status line is well-formed
|
|
1234
1174
|
const html404 = await renderErrorPage(404, { path: pathname }, templatesDir);
|
|
1235
1175
|
if (html404) {
|