tina4-nodejs 3.13.3 → 3.13.5
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.13.
|
|
1
|
+
# CLAUDE.md — AI Developer Guide for tina4-nodejs (v3.13.5)
|
|
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.13.
|
|
7
|
+
Tina4 for Node.js/TypeScript v3.13.5 — 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
|
|
|
@@ -571,7 +571,7 @@ db.delete(table, filter?, params?): DatabaseWriteResult
|
|
|
571
571
|
db.getLastId(): string | number | null
|
|
572
572
|
db.getError(): string | null
|
|
573
573
|
|
|
574
|
-
// Transactions — autoCommit defaults to
|
|
574
|
+
// Transactions — autoCommit defaults to OFF; set TINA4_AUTOCOMMIT=true to enable
|
|
575
575
|
db.startTransaction(): void
|
|
576
576
|
db.commit(): void
|
|
577
577
|
db.rollback(): void
|
package/package.json
CHANGED
|
@@ -20,7 +20,7 @@ export type { RouteInfo } from "./router.js";
|
|
|
20
20
|
export { discoverRoutes } from "./routeDiscovery.js";
|
|
21
21
|
export { MiddlewareChain, MiddlewareRunner, cors, requestLogger, CorsMiddleware, RateLimiterMiddleware, RequestLogger, SecurityHeadersMiddleware, CsrfMiddleware } from "./middleware.js";
|
|
22
22
|
export type { CorsConfig } from "./middleware.js";
|
|
23
|
-
export { createRequest } from "./request.js";
|
|
23
|
+
export { createRequest, makeCaseInsensitiveHeaders } from "./request.js";
|
|
24
24
|
export { createResponse, errorResponse, setDefaultTemplatesDir, getFrond, setFrond, getFrameworkFrond } from "./response.js";
|
|
25
25
|
export { tryServeStatic } from "./static.js";
|
|
26
26
|
export { loadEnv, getEnv, requireEnv, hasEnv, allEnv, resetEnv, isTruthy } from "./dotenv.js";
|
|
@@ -1,8 +1,42 @@
|
|
|
1
|
-
import type { IncomingMessage } from "node:http";
|
|
1
|
+
import type { IncomingMessage, IncomingHttpHeaders } from "node:http";
|
|
2
2
|
import type { Tina4Request, UploadedFile } from "./types.js";
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
* Wrap Node's `IncomingHttpHeaders` in a Proxy so mixed-case lookups
|
|
6
|
+
* (`req.headers["Content-Type"]`) work alongside the canonical lowercase
|
|
7
|
+
* form Node already provides. Parity with PY-10-03 (Python ships a
|
|
8
|
+
* `CaseInsensitiveDict` for the same reason).
|
|
9
|
+
*
|
|
10
|
+
* The raw object is returned as-is by `Object.keys` / iteration — only
|
|
11
|
+
* string property reads/`in` checks are normalised.
|
|
12
|
+
*/
|
|
13
|
+
export function makeCaseInsensitiveHeaders(
|
|
14
|
+
raw: IncomingHttpHeaders,
|
|
15
|
+
): IncomingHttpHeaders {
|
|
16
|
+
return new Proxy(raw, {
|
|
17
|
+
get(target, prop, receiver) {
|
|
18
|
+
if (typeof prop === "string") {
|
|
19
|
+
const lower = prop.toLowerCase();
|
|
20
|
+
if (lower in target) return (target as Record<string, unknown>)[lower];
|
|
21
|
+
return Reflect.get(target, prop, receiver);
|
|
22
|
+
}
|
|
23
|
+
return Reflect.get(target, prop, receiver);
|
|
24
|
+
},
|
|
25
|
+
has(target, prop) {
|
|
26
|
+
if (typeof prop === "string") {
|
|
27
|
+
return prop.toLowerCase() in target || Reflect.has(target, prop);
|
|
28
|
+
}
|
|
29
|
+
return Reflect.has(target, prop);
|
|
30
|
+
},
|
|
31
|
+
}) as IncomingHttpHeaders;
|
|
32
|
+
}
|
|
33
|
+
|
|
4
34
|
export function createRequest(req: IncomingMessage): Tina4Request {
|
|
5
35
|
const tReq = req as Tina4Request;
|
|
36
|
+
// Wrap `req.headers` so mixed-case lookups work — Node's underlying object
|
|
37
|
+
// is already lower-cased, this just lets readers use any casing they like.
|
|
38
|
+
(tReq as unknown as { headers: IncomingHttpHeaders }).headers =
|
|
39
|
+
makeCaseInsensitiveHeaders(req.headers);
|
|
6
40
|
|
|
7
41
|
// Resolve scheme + host honouring proxy headers — parity with PHP/Python/Ruby.
|
|
8
42
|
const xfProto = req.headers["x-forwarded-proto"];
|
|
@@ -131,12 +131,13 @@ export class Router {
|
|
|
131
131
|
routes.splice(existingIndex, 1);
|
|
132
132
|
}
|
|
133
133
|
|
|
134
|
-
// Write methods (POST/PUT/PATCH/DELETE) are secure by default
|
|
135
|
-
//
|
|
134
|
+
// Write methods (POST/PUT/PATCH/DELETE) are secure by default. Middleware is
|
|
135
|
+
// purely additive — registering custom middleware must NOT silently disable the
|
|
136
|
+
// built-in Bearer-token gate (parity with PY-10-02). Use `noAuth()` to open a
|
|
137
|
+
// write route explicitly.
|
|
136
138
|
const WRITE_METHODS = new Set(["POST", "PUT", "PATCH", "DELETE"]);
|
|
137
139
|
const isWrite = WRITE_METHODS.has(method);
|
|
138
|
-
const
|
|
139
|
-
const secureDefault = isWrite && !hasMiddleware ? (definition.secure ?? true) : definition.secure;
|
|
140
|
+
const secureDefault = isWrite ? (definition.secure ?? true) : definition.secure;
|
|
140
141
|
|
|
141
142
|
const compiled: CompiledRoute = {
|
|
142
143
|
pattern: definition.pattern,
|
|
@@ -1317,6 +1317,56 @@ function _generateFormTokenValue(descriptor: string = ""): SafeString {
|
|
|
1317
1317
|
// ── Frond Engine ───────────────────────────────────────────────
|
|
1318
1318
|
|
|
1319
1319
|
export class Frond {
|
|
1320
|
+
// ── Class-level registries ──────────────────────────────────
|
|
1321
|
+
// Persist globals, filters, and tests across hot-reloads and at
|
|
1322
|
+
// app-startup before any instance exists. When app.ts calls
|
|
1323
|
+
// ``Frond.addFilter("money", fn)`` once, the class remembers it
|
|
1324
|
+
// and every future ``new Frond()`` inherits it automatically.
|
|
1325
|
+
// Mirrors Python's _class_globals / _class_filters / _class_tests.
|
|
1326
|
+
private static classFilters: Map<string, FilterFn> = new Map();
|
|
1327
|
+
private static classGlobals: Map<string, unknown> = new Map();
|
|
1328
|
+
private static classTests: Map<string, TestFn> = new Map();
|
|
1329
|
+
|
|
1330
|
+
/**
|
|
1331
|
+
* Register a custom filter at the class level — available to every
|
|
1332
|
+
* future ``new Frond()`` instance. Callable as ``Frond.addFilter()``
|
|
1333
|
+
* (static) or ``frond.addFilter()`` (instance). See instance method
|
|
1334
|
+
* below for the dual-call semantics.
|
|
1335
|
+
*/
|
|
1336
|
+
static addFilter(name: string, fn: FilterFn): void {
|
|
1337
|
+
Frond.classFilters.set(name, fn);
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
/**
|
|
1341
|
+
* Register a global variable available in all templates of every
|
|
1342
|
+
* future instance. Callable as ``Frond.addGlobal()`` (static) or
|
|
1343
|
+
* ``frond.addGlobal()`` (instance).
|
|
1344
|
+
*/
|
|
1345
|
+
static addGlobal(name: string, value: unknown): void {
|
|
1346
|
+
Frond.classGlobals.set(name, value);
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
/**
|
|
1350
|
+
* Register a custom test (``{% if x is positive %}``) at the class
|
|
1351
|
+
* level. Callable as ``Frond.addTest()`` (static) or
|
|
1352
|
+
* ``frond.addTest()`` (instance).
|
|
1353
|
+
*/
|
|
1354
|
+
static addTest(name: string, fn: TestFn): void {
|
|
1355
|
+
Frond.classTests.set(name, fn);
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1358
|
+
/**
|
|
1359
|
+
* Clear the class-level globals/filters/tests registries.
|
|
1360
|
+
* Useful in test fixtures to prevent leaking state between tests.
|
|
1361
|
+
* Does NOT affect built-in filters or globals — only user-registered
|
|
1362
|
+
* ones via Frond.addFilter / addGlobal / addTest.
|
|
1363
|
+
*/
|
|
1364
|
+
static clearRegistry(): void {
|
|
1365
|
+
Frond.classFilters.clear();
|
|
1366
|
+
Frond.classGlobals.clear();
|
|
1367
|
+
Frond.classTests.clear();
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1320
1370
|
private templateDir: string;
|
|
1321
1371
|
private filters: Record<string, FilterFn>;
|
|
1322
1372
|
private globals: Record<string, unknown>;
|
|
@@ -1362,6 +1412,14 @@ export class Frond {
|
|
|
1362
1412
|
// Available alongside the |dump filter so both styles work:
|
|
1363
1413
|
// {{ user|dump }} and {{ dump(user) }}
|
|
1364
1414
|
this.globals.dump = (value: unknown) => renderDump(value);
|
|
1415
|
+
|
|
1416
|
+
// Drain the class-level registry. This is the key to surviving
|
|
1417
|
+
// hot-reloads AND the static-facade pattern: app.ts calls
|
|
1418
|
+
// Frond.addFilter("money", fn) once, the class remembers it,
|
|
1419
|
+
// and every future Frond() instance picks it up automatically.
|
|
1420
|
+
for (const [k, v] of Frond.classFilters) this.filters[k] = v;
|
|
1421
|
+
for (const [k, v] of Frond.classGlobals) this.globals[k] = v;
|
|
1422
|
+
for (const [k, v] of Frond.classTests) this.tests[k] = v;
|
|
1365
1423
|
}
|
|
1366
1424
|
|
|
1367
1425
|
sandbox(filters?: string[], tags?: string[], vars?: string[]): Frond {
|
|
@@ -1380,15 +1438,32 @@ export class Frond {
|
|
|
1380
1438
|
return this;
|
|
1381
1439
|
}
|
|
1382
1440
|
|
|
1441
|
+
/**
|
|
1442
|
+
* Register a custom filter. The filter is persisted at class level
|
|
1443
|
+
* so new instances created by hot-reload inherit it automatically;
|
|
1444
|
+
* the live instance's local filter map also receives the addition
|
|
1445
|
+
* immediately. Mirrors Python's _ClassOrInstanceMethod dual-call.
|
|
1446
|
+
*/
|
|
1383
1447
|
addFilter(name: string, fn: FilterFn): void {
|
|
1448
|
+
Frond.classFilters.set(name, fn);
|
|
1384
1449
|
this.filters[name] = fn;
|
|
1385
1450
|
}
|
|
1386
1451
|
|
|
1452
|
+
/**
|
|
1453
|
+
* Register a global variable available in all templates. Persisted
|
|
1454
|
+
* at class level — see ``addFilter`` for the dual-call semantics.
|
|
1455
|
+
*/
|
|
1387
1456
|
addGlobal(name: string, value: unknown): void {
|
|
1457
|
+
Frond.classGlobals.set(name, value);
|
|
1388
1458
|
this.globals[name] = value;
|
|
1389
1459
|
}
|
|
1390
1460
|
|
|
1461
|
+
/**
|
|
1462
|
+
* Register a custom test. Persisted at class level — see
|
|
1463
|
+
* ``addFilter`` for the dual-call semantics.
|
|
1464
|
+
*/
|
|
1391
1465
|
addTest(name: string, fn: TestFn): void {
|
|
1466
|
+
Frond.classTests.set(name, fn);
|
|
1392
1467
|
this.tests[name] = fn;
|
|
1393
1468
|
}
|
|
1394
1469
|
|