tina4-nodejs 3.11.11 → 3.11.13

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
@@ -108,7 +108,6 @@ The HTTP foundation. Handles request/response lifecycle, route matching, middlew
108
108
  - `response.ts` — Wraps `ServerResponse`, adds `.json()`, `.html()`, `.status()`, `.send()`, `.redirect()`
109
109
  - `middleware.ts` — Chain runner, built-in CORS and request logger
110
110
  - `static.ts` — Serves files from `public/` with MIME type detection
111
- - `watcher.ts` — `fs.watch` for hot-reload in dev mode
112
111
  - `types.ts` — All shared type definitions (`Tina4Request`, `Tina4Response`, `RouteHandler`, etc.)
113
112
  - `events.ts` — Observer-pattern event system (`Events.on`, `emit`, `once`, `off`, `clear`)
114
113
  - `ai.ts` — AI coding tool detection and context scaffolding (`detectAi`, `installAiContext`, `aiStatusReport`)
@@ -566,7 +565,7 @@ import { Router } from "./router.js"; // .js even though the file is .ts
566
565
  2. **`tsx` for dev** — No build step needed during development. TypeScript runs directly.
567
566
  3. **Convention-based models** — `static fields = {}` over decorators. No special TypeScript config needed.
568
567
  4. **CDN for Swagger UI** — Keeps install under 8MB. Single HTML file loads from unpkg.com.
569
- 5. **Process restart for hot-reload** — Simpler and more reliable than HMR with ESM.
568
+ 5. **Browser reload, not process restart** — The `tina4` Rust CLI watches `src/`, `migrations/`, `.env` and POSTs `/__dev/api/reload` to the running server. The server stays up; only the browser reloads (via WS on `/__dev_reload`, polling fallback on `GET /__dev/api/mtime`). No ESM HMR gymnastics, no server restart, no framework-side watcher.
570
569
  6. **SQLite default** — `node:sqlite` is synchronous and fast. Full adapters for Postgres, MySQL, MSSQL/SQL Server, and Firebird.
571
570
  7. **CLI named `tina4nodejs`** (primary) with `tina4` as alias — So `npx tina4nodejs init` or `npx tina4 init` both work.
572
571
  8. **Event system** — Static `Events` class, synchronous dispatch, priority ordering, zero deps.
package/README.md CHANGED
@@ -3,11 +3,11 @@
3
3
  </p>
4
4
  <h1 align="center">Tina4 Node.js</h1>
5
5
  <h3 align="center">The Intelligent Native Application 4ramework</h3>
6
- <p align="center">54 built-in features. Zero dependencies. One import, everything works.</p>
6
+ <p align="center">55 built-in features. Zero dependencies. One import, everything works.</p>
7
7
  <p align="center">
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%2C812%20passing-brightgreen" alt="Tests">
10
- <img src="https://img.shields.io/badge/features-54-blue" alt="Features">
8
+ <a href="https://www.npmjs.com/package/@tina4/core"><img src="https://img.shields.io/npm/v/@tina4/core?color=7b1fa2&label=npm" alt="npm"></a>
9
+ <img src="https://img.shields.io/badge/tests-2%2C897%20passing-brightgreen" alt="Tests">
10
+ <img src="https://img.shields.io/badge/features-55-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>
13
13
  </p>
@@ -17,6 +17,12 @@
17
17
  ## Quick Start
18
18
 
19
19
  ```bash
20
+ # With the Tina4 CLI (recommended — enables SCSS + live reload)
21
+ cargo install tina4 # or grab a binary from https://github.com/tina4stack/tina4/releases
22
+ tina4 init nodejs ./my-app
23
+ cd my-app && tina4 serve
24
+
25
+ # Without the Tina4 CLI
20
26
  npx tina4nodejs init my-app
21
27
  cd my-app && npx tina4nodejs serve
22
28
  ```
@@ -61,7 +67,7 @@ export default class User {
61
67
  | **Developer Tools** (7) | Dev dashboard (11 tabs), dev toolbar, error overlay (Catppuccin Mocha), dev mailbox, hot reload + CSS hot-reload, code metrics (complexity, coupling, maintainability), AI context installer (7 tools) |
62
68
  | **Utilities** (7) | DI container (transient + singleton), HtmlElement builder, inline testing (`@tests` decorator), i18n (6 languages), Swagger/OpenAPI auto-generation, CLI scaffolding (`generate model/route/migration/middleware`), structured logging |
63
69
 
64
- **1,950 tests. Zero dependencies. Full parity across Python, PHP, Ruby, and Node.js.**
70
+ **2,897 tests. Zero dependencies. Full parity across Python, PHP, Ruby, and Node.js.**
65
71
 
66
72
  ---
67
73
 
@@ -85,9 +91,9 @@ Benchmarked with `wrk` — 5,000 requests, 50 concurrent, median of 3 runs:
85
91
  | Framework | JSON req/s | Deps | Features |
86
92
  |-----------|-----------|------|----------|
87
93
  | Raw `node:http` | 91,110 | 0 | 1 |
88
- | **Tina4 Node.js** | **84,771** | 0 | 54 |
94
+ | **Tina4 Node.js** | **84,771** | 0 | 55 |
89
95
 
90
- Tina4 Node.js runs at **93% of raw Node.js speed** while providing 54 built-in features — zero overhead architecture.
96
+ Tina4 Node.js runs at **93% of raw Node.js speed** while providing 55 built-in features — zero overhead architecture.
91
97
 
92
98
  **Across all 4 Tina4 implementations:**
93
99
 
@@ -95,21 +101,21 @@ Tina4 Node.js runs at **93% of raw Node.js speed** while providing 54 built-in f
95
101
  |---|--------|-----|------|---------|
96
102
  | **JSON req/s** | 6,508 | 29,293 | 10,243 | 84,771 |
97
103
  | **Dependencies** | 0 | 0 | 0 | 0 |
98
- | **Features** | 54 | 54 | 54 | 54 |
104
+ | **Features** | 55 | 55 | 55 | 55 |
99
105
 
100
106
  ---
101
107
 
102
108
  ## Cross-Framework Parity
103
109
 
104
- Tina4 ships identical features across four languages — same architecture, same conventions, same 54 features:
110
+ Tina4 ships identical features across four languages — same architecture, same conventions, same 55 features:
105
111
 
106
112
  | | Python | PHP | Ruby | Node.js |
107
113
  |---|--------|-----|------|---------|
108
- | **Package** | `tina4-python` | `tina4stack/tina4php` | `tina4ruby` | `tina4-nodejs` |
109
- | **Tests** | 2,066 | 1,427 | 1,793 | 1,950 |
110
- | **Default port** | 7145 | 7146 | 7147 | 7148 |
114
+ | **Package** | `tina4-python` | `tina4stack/tina4php` | `tina4ruby` | `@tina4/core` |
115
+ | **Tests (v3.11.12)** | 2,281 | 2,073 | 2,508 | 2,897 |
116
+ | **Default port** | 7146 | 7145 | 7147 | 7148 |
111
117
 
112
- **7,236 tests** across all 4 frameworks. See [tina4.com](https://tina4.com).
118
+ **~9,700 tests** across all 4 frameworks. See [tina4.com](https://tina4.com).
113
119
 
114
120
  ---
115
121
 
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
 
4
4
 
5
5
 
6
- "version": "3.11.11",
6
+ "version": "3.11.13",
7
7
 
8
8
  "type": "module",
9
9
  "description": "Tina4 for Node.js/TypeScript — 54 built-in features, zero dependencies",
@@ -384,6 +384,29 @@ export class Router {
384
384
  defaultRouter.group(prefix, callback, middlewares);
385
385
  }
386
386
 
387
+ /**
388
+ * Supported typed-parameter constraints. Mirrored verbatim in
389
+ * tina4-python / tina4-php / tina4-ruby for cross-framework parity.
390
+ *
391
+ * Any type name not in this table throws at route registration time —
392
+ * we never silently fall through to the default matcher, because a
393
+ * typo like `{id:inetger}` would otherwise match anything and create
394
+ * a security footgun (see tina4-book#125).
395
+ */
396
+ private static readonly PARAM_TYPE_PATTERNS: Record<string, string> = {
397
+ string: "[^/]+", // default, any non-slash segment
398
+ int: "\\d+",
399
+ integer: "\\d+",
400
+ float: "[\\d.]+",
401
+ number: "[\\d.]+",
402
+ alpha: "[A-Za-z]+", // letters only
403
+ alnum: "[A-Za-z0-9]+", // letters + digits
404
+ slug: "[a-z0-9-]+", // URL slug
405
+ uuid: "[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}",
406
+ path: ".+", // greedy
407
+ ".*": ".+",
408
+ };
409
+
387
410
  private compilePattern(pattern: string): { regex: RegExp; paramNames: string[] } {
388
411
  const paramNames: string[] = [];
389
412
 
@@ -407,19 +430,14 @@ export class Router {
407
430
  const name = colonIdx >= 0 ? inner.slice(0, colonIdx) : inner;
408
431
  const type = colonIdx >= 0 ? inner.slice(colonIdx + 1) : "string";
409
432
  paramNames.push(name);
410
- switch (type) {
411
- case "int":
412
- case "integer":
413
- return "(\\d+)";
414
- case "float":
415
- case "number":
416
- return "([\\d.]+)";
417
- case "path":
418
- case ".*":
419
- return "(.+)";
420
- default:
421
- return "([^/]+)";
433
+ const table = Router.PARAM_TYPE_PATTERNS;
434
+ if (!Object.prototype.hasOwnProperty.call(table, type)) {
435
+ const valid = Object.keys(table).filter((k) => k !== ".*").sort().join(", ");
436
+ throw new Error(
437
+ `Unknown param type '${type}' in route '${pattern}'. Valid types: ${valid}.`
438
+ );
422
439
  }
440
+ return `(${table[type]})`;
423
441
  }
424
442
  // Dynamic param: [id] (file-based routing internal syntax)
425
443
  if (segment.startsWith("[") && segment.endsWith("]")) {
@@ -1,17 +1,48 @@
1
1
  import { DatabaseSync } from "node:sqlite";
2
2
  import { mkdirSync } from "node:fs";
3
- import { dirname } from "node:path";
3
+ import { dirname, isAbsolute, join, resolve } from "node:path";
4
4
  import type { DatabaseAdapter, DatabaseResult, ColumnInfo, FieldDefinition } from "../types.js";
5
5
 
6
+ /**
7
+ * Resolve a SQLite path argument against the project root (cwd).
8
+ *
9
+ * Matches the tina4-python + tina4-php convention:
10
+ * ":memory:" → passthrough
11
+ * "data/app.db" → {cwd}/data/app.db (auto-mkdir under cwd)
12
+ * "/abs/app.db" → /abs/app.db (NO auto-mkdir; user's responsibility)
13
+ * "C:/Users/app.db" → C:/Users/app.db (NO auto-mkdir)
14
+ *
15
+ * Never mkdir a directory that isn't a descendant of cwd — that was the
16
+ * root cause of the `EROFS: read-only file system, mkdir '/data'` crash
17
+ * reported on macOS.
18
+ */
19
+ function resolveSqlitePath(dbPath: string): string {
20
+ if (dbPath === ":memory:") return dbPath;
21
+
22
+ let path = dbPath;
23
+ if (!isAbsolute(path)) {
24
+ path = join(process.cwd(), path);
25
+ // Auto-mkdir is safe here — we know the parent is under cwd
26
+ mkdirSync(dirname(path), { recursive: true });
27
+ } else {
28
+ // Absolute path. Only auto-mkdir if it's a descendant of cwd.
29
+ const cwd = resolve(process.cwd());
30
+ const abs = resolve(path);
31
+ if (abs.startsWith(cwd + "/") || abs === cwd) {
32
+ mkdirSync(dirname(abs), { recursive: true });
33
+ }
34
+ // Otherwise, trust the user — don't touch the filesystem.
35
+ }
36
+ return path;
37
+ }
38
+
6
39
  export class SQLiteAdapter implements DatabaseAdapter {
7
40
  private db: DatabaseSync;
8
41
  private _lastInsertId: number | bigint | null = null;
9
42
 
10
43
  constructor(dbPath: string) {
11
- if (dbPath !== ":memory:") {
12
- mkdirSync(dirname(dbPath), { recursive: true });
13
- }
14
- this.db = new DatabaseSync(dbPath);
44
+ const resolved = resolveSqlitePath(dbPath);
45
+ this.db = new DatabaseSync(resolved);
15
46
  this.db.exec("PRAGMA journal_mode = WAL");
16
47
  this.db.exec("PRAGMA foreign_keys = ON");
17
48
  }
@@ -92,18 +92,31 @@ export interface ParsedDatabaseUrl {
92
92
  export function parseDatabaseUrl(url: string, username?: string, password?: string): ParsedDatabaseUrl {
93
93
  let result: ParsedDatabaseUrl;
94
94
 
95
- // Handle sqlite:// separately because URL class mangles the path
96
- if (url.startsWith("sqlite:///")) {
97
- // sqlite:///absolute/path three slashes means absolute
98
- let path = url.slice("sqlite://".length);
99
- // Windows: sqlite:///C:/Users/app.db /C:/Users/app.db after slicing.
100
- // The leading / before the drive letter must be removed.
101
- if (/^\/[A-Za-z]:/.test(path)) {
102
- path = path.slice(1);
103
- }
104
- result = { type: "sqlite", path };
95
+ // Handle sqlite:// separately because URL class mangles the path.
96
+ //
97
+ // Convention (matches tina4-python, tina4-php, and the docs):
98
+ // sqlite::memory: → in-memory
99
+ // sqlite:///:memory: in-memory (URL form)
100
+ // sqlite:///app.db → ./app.db (relative to cwd)
101
+ // sqlite:///data/app.db → ./data/app.db (relative)
102
+ // sqlite:////absolute/app.db → /absolute/app.db (absolute)
103
+ // sqlite:///C:/Users/app.db → C:/Users/app.db (Windows absolute)
104
+ if (url === "sqlite::memory:" || url === "sqlite:///:memory:") {
105
+ result = { type: "sqlite", path: ":memory:" };
106
+ } else if (url.startsWith("sqlite:///")) {
107
+ // Strip the "sqlite://" prefix (leaving one "/" + path)
108
+ let rest = url.slice("sqlite://".length); // e.g. "/data/app.db" or "//abs/app.db" or "/C:/Users/..."
109
+ // Drop exactly one leading "/"
110
+ if (rest.startsWith("/")) rest = rest.slice(1);
111
+ // Windows absolute: C:/Users/app.db or C:\...
112
+ const isWindowsAbs = /^[A-Za-z]:[\/\\]/.test(rest);
113
+ // Unix absolute: still starts with "/" after the strip (four-slash URL form)
114
+ const isUnixAbs = rest.startsWith("/");
115
+ result = { type: "sqlite", path: isWindowsAbs || isUnixAbs ? rest : rest };
116
+ // Relative paths are resolved against cwd by the SQLite adapter at connect time;
117
+ // keep the string as-is here so tests can inspect the raw form.
105
118
  } else if (url.startsWith("sqlite://")) {
106
- // sqlite://./relative or sqlite://relative
119
+ // sqlite://./relative or sqlite://relative — legacy two-slash form
107
120
  const path = url.slice("sqlite://".length);
108
121
  result = { type: "sqlite", path };
109
122
  } else if (url.startsWith("mssql://") || url.startsWith("sqlserver://")) {