tina4-nodejs 3.10.55 → 3.10.61
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 +1 -1
- package/README.md +1 -1
- package/package.json +1 -1
- package/packages/cli/src/bin.ts +37 -1
- package/packages/cli/src/commands/init.ts +2 -2
- package/packages/core/src/devAdmin.ts +15 -1
- package/packages/core/src/metrics.ts +2 -0
- package/packages/core/src/server.ts +85 -37
package/CLAUDE.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
## What This Project Is
|
|
6
6
|
|
|
7
|
-
Tina4 for Node.js/TypeScript v3.10.42 —
|
|
7
|
+
Tina4 for Node.js/TypeScript v3.10.42 — 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/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
<img src="https://tina4.com/logo.svg" alt="Tina4" width="200">
|
|
3
3
|
</p>
|
|
4
4
|
<h1 align="center">Tina4 Node.js</h1>
|
|
5
|
-
<h3 align="center">
|
|
5
|
+
<h3 align="center">The Intelligent Native Application 4ramework</h3>
|
|
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>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tina4-nodejs",
|
|
3
|
-
"version": "3.10.
|
|
3
|
+
"version": "3.10.61",
|
|
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"],
|
package/packages/cli/src/bin.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
#!/usr/bin/env npx tsx
|
|
1
2
|
import { initProject } from "./commands/init.js";
|
|
2
3
|
import { serveProject } from "./commands/serve.js";
|
|
3
4
|
import { runMigrations } from "./commands/migrate.js";
|
|
@@ -14,7 +15,7 @@ const args = process.argv.slice(2);
|
|
|
14
15
|
const command = args[0];
|
|
15
16
|
|
|
16
17
|
const HELP = `
|
|
17
|
-
tina4nodejs —
|
|
18
|
+
tina4nodejs — The Intelligent Native Application 4ramework
|
|
18
19
|
|
|
19
20
|
Usage:
|
|
20
21
|
tina4nodejs init [dir] Create a new Tina4 project (default: current directory)
|
|
@@ -26,6 +27,7 @@ const HELP = `
|
|
|
26
27
|
tina4nodejs routes List all registered routes
|
|
27
28
|
tina4nodejs test [file] Run project tests
|
|
28
29
|
tina4nodejs seed [file] Run database seed files from src/seeds/
|
|
30
|
+
tina4nodejs console Open an interactive REPL with the framework loaded
|
|
29
31
|
tina4nodejs ai Install AI coding assistant context files
|
|
30
32
|
tina4nodejs help Show this help message
|
|
31
33
|
|
|
@@ -130,6 +132,40 @@ async function main(): Promise<void> {
|
|
|
130
132
|
await runSeeds(args[1]);
|
|
131
133
|
break;
|
|
132
134
|
}
|
|
135
|
+
case "console": {
|
|
136
|
+
const repl = await import("node:repl");
|
|
137
|
+
const { loadEnv } = await import("@tina4/core");
|
|
138
|
+
const { Router } = await import("@tina4/core");
|
|
139
|
+
const { initDatabase, Database } = await import("@tina4/orm");
|
|
140
|
+
const { Log } = await import("@tina4/core");
|
|
141
|
+
|
|
142
|
+
loadEnv();
|
|
143
|
+
|
|
144
|
+
const dbUrl = process.env.DATABASE_URL;
|
|
145
|
+
let db: unknown = null;
|
|
146
|
+
if (dbUrl) {
|
|
147
|
+
try {
|
|
148
|
+
db = await initDatabase({ url: dbUrl });
|
|
149
|
+
} catch {
|
|
150
|
+
console.warn(" Warning: could not connect to database — db will be null");
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
console.log("\n Tina4 Node.js Console");
|
|
155
|
+
console.log(" Type JavaScript. Framework is loaded.");
|
|
156
|
+
console.log(" Available: db, Router, Database, Log");
|
|
157
|
+
console.log(" Exit: Ctrl+D or .exit\n");
|
|
158
|
+
|
|
159
|
+
const r = repl.start({ prompt: "tina4> " });
|
|
160
|
+
|
|
161
|
+
r.context.Router = Router;
|
|
162
|
+
r.context.Database = Database;
|
|
163
|
+
r.context.Log = Log;
|
|
164
|
+
r.context.db = db;
|
|
165
|
+
|
|
166
|
+
await new Promise<void>((resolve) => r.on("exit", resolve));
|
|
167
|
+
break;
|
|
168
|
+
}
|
|
133
169
|
case "ai": {
|
|
134
170
|
const { showMenu, installSelected, installAll } = await import("../../core/src/ai.js");
|
|
135
171
|
const root = args[1] || ".";
|
|
@@ -196,7 +196,7 @@ export default async function (req: Tina4Request, res: Tina4Response): Promise<v
|
|
|
196
196
|
</head>
|
|
197
197
|
<body>
|
|
198
198
|
<h1>Welcome to {{ name }}</h1>
|
|
199
|
-
<p>
|
|
199
|
+
<p>The Intelligent Native Application 4ramework</p>
|
|
200
200
|
</body>
|
|
201
201
|
</html>
|
|
202
202
|
`
|
|
@@ -219,7 +219,7 @@ export default async function (req: Tina4Request, res: Tina4Response): Promise<v
|
|
|
219
219
|
</head>
|
|
220
220
|
<body>
|
|
221
221
|
<h1>tina4</h1>
|
|
222
|
-
<p><em>
|
|
222
|
+
<p><em>The Intelligent Native Application 4ramework</em></p>
|
|
223
223
|
<p>Your API is running. Try:</p>
|
|
224
224
|
<ul>
|
|
225
225
|
<li><a href="/api/hello">GET /api/hello</a></li>
|
|
@@ -21,7 +21,21 @@ import { quickMetrics, fullAnalysis, fileDetail } from "./metrics.js";
|
|
|
21
21
|
|
|
22
22
|
const cpuCount = osCpus().length;
|
|
23
23
|
|
|
24
|
-
|
|
24
|
+
// Read version from root package.json dynamically
|
|
25
|
+
const TINA4_VERSION = (() => {
|
|
26
|
+
try {
|
|
27
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
28
|
+
// Try root package.json first, then core package.json
|
|
29
|
+
for (const rel of ["../../../package.json", "../../package.json"]) {
|
|
30
|
+
const p = resolve(__dirname, rel);
|
|
31
|
+
if (existsSync(p)) {
|
|
32
|
+
const pkg = JSON.parse(readFileSync(p, "utf-8"));
|
|
33
|
+
if (pkg.version) return pkg.version;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
} catch {}
|
|
37
|
+
return "0.0.0";
|
|
38
|
+
})();
|
|
25
39
|
|
|
26
40
|
// ---------------------------------------------------------------------------
|
|
27
41
|
// Types
|
|
@@ -60,8 +60,10 @@ function hasMatchingTest(relPath: string): boolean {
|
|
|
60
60
|
const name = relPath.split('/').pop()?.replace('.ts', '').replace('.js', '') || '';
|
|
61
61
|
const patterns = [
|
|
62
62
|
`test/${name}.test.ts`,
|
|
63
|
+
`test/${name}.test.js`,
|
|
63
64
|
`${relPath.replace('.ts', '.test.ts').replace('.js', '.test.js')}`,
|
|
64
65
|
`tests/${name}.test.ts`,
|
|
66
|
+
`spec/${name}.spec.ts`,
|
|
65
67
|
];
|
|
66
68
|
return patterns.some(p => fs.existsSync(p));
|
|
67
69
|
}
|
|
@@ -46,23 +46,62 @@ const TINA4_VERSION = readPackageVersion();
|
|
|
46
46
|
const frondCache = new Map<string, InstanceType<any>>();
|
|
47
47
|
|
|
48
48
|
/**
|
|
49
|
-
*
|
|
50
|
-
*
|
|
49
|
+
* Kill whatever process is listening on *port*.
|
|
50
|
+
* Uses lsof on macOS/Linux and netstat + taskkill on Windows.
|
|
51
|
+
* Throws if the port cannot be freed.
|
|
51
52
|
*/
|
|
52
|
-
function
|
|
53
|
-
// execFileSync imported at top of file
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
execFileSync(
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
53
|
+
function killPort(port: number): void {
|
|
54
|
+
// execFileSync is imported at the top of the file
|
|
55
|
+
console.log(` Port ${port} in use — killing existing process...`);
|
|
56
|
+
try {
|
|
57
|
+
if (process.platform === "win32") {
|
|
58
|
+
const out = execFileSync("netstat", ["-ano"], { encoding: "utf-8", timeout: 5000 });
|
|
59
|
+
for (const line of out.split("\n")) {
|
|
60
|
+
if (line.includes(`:${port}`) && (line.includes("LISTENING") || line.includes("ESTABLISHED"))) {
|
|
61
|
+
const parts = line.trim().split(/\s+/);
|
|
62
|
+
const pid = parts[parts.length - 1];
|
|
63
|
+
if (/^\d+$/.test(pid)) {
|
|
64
|
+
execFileSync("taskkill", ["/PID", pid, "/F"], { timeout: 5000 });
|
|
65
|
+
break;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
} else {
|
|
70
|
+
const pids = execFileSync("lsof", ["-ti", `:${port}`], { encoding: "utf-8", timeout: 5000 })
|
|
71
|
+
.trim()
|
|
72
|
+
.split("\n")
|
|
73
|
+
.filter(Boolean);
|
|
74
|
+
for (const pid of pids) {
|
|
75
|
+
if (/^\d+$/.test(pid.trim())) {
|
|
76
|
+
process.kill(parseInt(pid.trim(), 10), "SIGTERM");
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
// Brief pause for the OS to reclaim the port
|
|
81
|
+
execFileSync(process.execPath, ["-e", "setTimeout(() => {}, 500)"], { timeout: 2000 });
|
|
82
|
+
console.log(` Port ${port} freed`);
|
|
83
|
+
} catch (err) {
|
|
84
|
+
throw new Error(`Could not free port ${port}: ${err}`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Check if *port* is available; if not, kill the process on it and return *port*.
|
|
90
|
+
* The auto-increment behaviour is intentionally removed — the server always
|
|
91
|
+
* claims the requested port.
|
|
92
|
+
*/
|
|
93
|
+
function findAvailablePort(start: number): number {
|
|
94
|
+
try {
|
|
95
|
+
execFileSync(process.execPath, ["-e", `
|
|
96
|
+
const s = require("net").createServer();
|
|
97
|
+
s.listen(${start}, "0.0.0.0", () => { s.close(); process.exit(0); });
|
|
98
|
+
s.on("error", () => process.exit(1));
|
|
99
|
+
`], { timeout: 1000 });
|
|
100
|
+
return start;
|
|
101
|
+
} catch {
|
|
102
|
+
killPort(start);
|
|
103
|
+
return start;
|
|
64
104
|
}
|
|
65
|
-
return start;
|
|
66
105
|
}
|
|
67
106
|
|
|
68
107
|
/**
|
|
@@ -322,7 +361,7 @@ h1{font-size:3rem;font-weight:700;margin-bottom:0.25rem;letter-spacing:-1px}
|
|
|
322
361
|
<div class="hero">
|
|
323
362
|
<img src="/images/tina4-logo-icon.webp" class="logo" alt="Tina4">
|
|
324
363
|
<h1>Tina4NodeJs</h1>
|
|
325
|
-
<p class="tagline">
|
|
364
|
+
<p class="tagline">The Intelligent Native Application 4ramework</p>
|
|
326
365
|
<div class="actions">
|
|
327
366
|
<a href="https://tina4.com/nodejs" class="btn" target="_blank">Website</a>
|
|
328
367
|
<a href="/__dev" class="btn">Dev Admin</a>
|
|
@@ -422,12 +461,8 @@ export async function startServer(config?: Tina4Config): Promise<{
|
|
|
422
461
|
const host = resolved.host;
|
|
423
462
|
let port = resolved.port;
|
|
424
463
|
|
|
425
|
-
//
|
|
426
|
-
|
|
427
|
-
if (availablePort !== port) {
|
|
428
|
-
console.log(` Port ${port} in use, using ${availablePort} instead`);
|
|
429
|
-
port = availablePort;
|
|
430
|
-
}
|
|
464
|
+
// Claim the requested port — kill whatever is on it if needed
|
|
465
|
+
port = findAvailablePort(port);
|
|
431
466
|
|
|
432
467
|
// Cluster mode for production: fork workers based on CPU count
|
|
433
468
|
// Only when --production is explicitly set (via TINA4_PRODUCTION env var)
|
|
@@ -448,7 +483,7 @@ export async function startServer(config?: Tina4Config): Promise<{
|
|
|
448
483
|
/ / / / / / / /_/ /__ __/
|
|
449
484
|
/_/ /_/_/ /_/\\__,_/ /_/
|
|
450
485
|
${reset}
|
|
451
|
-
Tina4 Node.js v${TINA4_VERSION} —
|
|
486
|
+
Tina4 Node.js v${TINA4_VERSION} — The Intelligent Native Application 4ramework
|
|
452
487
|
|
|
453
488
|
Server: http://${displayHost}:${port} (cluster, ${numCPUs} workers)
|
|
454
489
|
Swagger: http://localhost:${port}/swagger
|
|
@@ -867,7 +902,17 @@ ${reset}
|
|
|
867
902
|
// Assign to module-level so handle() can dispatch without a server reference
|
|
868
903
|
_dispatchFn = dispatch;
|
|
869
904
|
|
|
870
|
-
|
|
905
|
+
// When dual-port is active (debug mode + no TINA4_NO_AI_PORT), tag main port requests
|
|
906
|
+
// as AI port to suppress reload/toolbar injection. Test port (port+1000) gets full reload.
|
|
907
|
+
const _dualPortActive = isTruthy(process.env.TINA4_DEBUG ?? "") &&
|
|
908
|
+
!isTruthy(process.env.TINA4_NO_AI_PORT ?? "");
|
|
909
|
+
const mainPortDispatch = _dualPortActive
|
|
910
|
+
? async (req: IncomingMessage, res: ServerResponse) => {
|
|
911
|
+
(req as any)._tina4AiPort = true;
|
|
912
|
+
await dispatch(req, res);
|
|
913
|
+
}
|
|
914
|
+
: dispatch;
|
|
915
|
+
const server = createServer(mainPortDispatch);
|
|
871
916
|
|
|
872
917
|
return new Promise((resolvePromise) => {
|
|
873
918
|
server.listen(port, host, () => {
|
|
@@ -883,33 +928,32 @@ ${reset}
|
|
|
883
928
|
// Determine server mode label
|
|
884
929
|
const serverMode = isDebug ? "single" : (cluster.isWorker ? "cluster-worker" : "single");
|
|
885
930
|
|
|
886
|
-
// AI dual-port:
|
|
887
|
-
// and TINA4_NO_AI_PORT is not set
|
|
888
|
-
//
|
|
931
|
+
// AI dual-port: main port = AI dev port (no reload), port+1000 = user testing port (hot-reload)
|
|
932
|
+
// When TINA4_DEBUG=true and TINA4_NO_AI_PORT is not set, main server suppresses reload/toolbar
|
|
933
|
+
// and a second server on port+1000 provides the normal hot-reload experience.
|
|
889
934
|
const noAiPort = isTruthy(process.env.TINA4_NO_AI_PORT ?? "");
|
|
890
935
|
let aiServer: ReturnType<typeof createServer> | null = null;
|
|
891
|
-
let
|
|
936
|
+
let testPort = port + 1000;
|
|
892
937
|
|
|
893
938
|
if (isDebug && !noAiPort) {
|
|
939
|
+
// Test port (port+1000): normal dispatch with full hot-reload
|
|
894
940
|
aiServer = createServer(async (req, res) => {
|
|
895
|
-
// Tag the request so dispatch knows it came from the AI port
|
|
896
|
-
(req as any)._tina4AiPort = true;
|
|
897
941
|
await dispatch(req, res);
|
|
898
942
|
});
|
|
899
943
|
|
|
900
944
|
aiServer.on("error", (err: any) => {
|
|
901
945
|
if (err.code === "EADDRINUSE") {
|
|
902
|
-
Log.warn(`
|
|
946
|
+
Log.warn(`Test port ${testPort} in use — skipping`);
|
|
903
947
|
aiServer = null;
|
|
904
948
|
}
|
|
905
949
|
});
|
|
906
950
|
|
|
907
|
-
aiServer.listen(
|
|
951
|
+
aiServer.listen(testPort, host);
|
|
908
952
|
}
|
|
909
953
|
|
|
910
954
|
// Banner goes to stdout via console.log — NOT through the framework logger
|
|
911
|
-
const
|
|
912
|
-
? `\n
|
|
955
|
+
const dualPortLines = (isDebug && !noAiPort)
|
|
956
|
+
? `\n Test Port: http://localhost:${testPort} (stable — no hot-reload)`
|
|
913
957
|
: "";
|
|
914
958
|
|
|
915
959
|
console.log(`${color}
|
|
@@ -919,16 +963,20 @@ ${reset}
|
|
|
919
963
|
/ / / / / / / /_/ /__ __/
|
|
920
964
|
/_/ /_/_/ /_/\\__,_/ /_/
|
|
921
965
|
${reset}
|
|
922
|
-
Tina4 Node.js v${TINA4_VERSION} —
|
|
966
|
+
Tina4 Node.js v${TINA4_VERSION} — The Intelligent Native Application 4ramework
|
|
923
967
|
|
|
924
968
|
Server: http://${displayHost}:${port} (${serverMode})
|
|
925
969
|
Swagger: http://localhost:${port}/swagger
|
|
926
970
|
Dashboard: http://localhost:${port}/__dev
|
|
927
|
-
Debug: ${isDebug ? "ON" : "OFF"} (Log level: ${logLevel})${
|
|
971
|
+
Debug: ${isDebug ? "ON" : "OFF"} (Log level: ${logLevel})${dualPortLines}
|
|
928
972
|
`);
|
|
929
973
|
const noBrowser = isTruthy(process.env.TINA4_NO_BROWSER);
|
|
930
974
|
if (!noBrowser) {
|
|
931
|
-
|
|
975
|
+
// Open browser on test port (hot-reload) if available, otherwise main port
|
|
976
|
+
const browserTarget = (isDebug && !noAiPort && aiServer)
|
|
977
|
+
? `http://${displayHost}:${testPort}`
|
|
978
|
+
: `http://${displayHost}:${port}`;
|
|
979
|
+
openBrowser(browserTarget);
|
|
932
980
|
}
|
|
933
981
|
resolvePromise({
|
|
934
982
|
close: () => {
|