tina4-nodejs 3.13.97 → 3.13.99
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 +60 -25
- package/package.json +1 -2
- package/packages/cli/dist/bin.js +20620 -18995
- package/packages/cli/src/bin.ts +28 -71
- package/packages/cli/src/commands/migrate.ts +36 -75
- package/packages/cli/src/commands/migrateRollback.ts +10 -1
- package/packages/cli/src/commands/test.ts +92 -21
- package/packages/core/dist/index.js +20459 -18815
- package/packages/core/public/js/tina4-dev-admin.min.js +23 -19
- package/packages/core/src/ai.ts +28 -12
- package/packages/core/src/api.ts +13 -5
- package/packages/core/src/background.ts +9 -3
- package/packages/core/src/devAdmin.ts +135 -20
- package/packages/core/src/dispatchPipeline.ts +185 -1
- package/packages/core/src/docs.ts +33 -5
- package/packages/core/src/env.ts +1 -1
- package/packages/core/src/errorOverlay.ts +39 -48
- package/packages/core/src/fakeData.ts +15 -0
- package/packages/core/src/index.ts +17 -6
- package/packages/core/src/logger.ts +892 -572
- package/packages/core/src/mcp.ts +9 -1
- package/packages/core/src/messenger.ts +31 -4
- package/packages/core/src/middleware.ts +169 -43
- package/packages/core/src/portTakeover.ts +232 -0
- package/packages/core/src/request.ts +57 -8
- package/packages/core/src/response.ts +67 -0
- package/packages/core/src/router.ts +35 -7
- package/packages/core/src/server.ts +450 -190
- package/packages/core/src/static.ts +81 -12
- package/packages/core/src/testClient.ts +126 -137
- package/packages/core/src/testing.ts +16 -12
- package/packages/core/src/types.ts +21 -9
- package/packages/core/src/version.ts +66 -0
- package/packages/core/src/websocket.ts +2 -2
- package/packages/core/src/websocketBackplane.ts +2 -2
- package/packages/frond/dist/index.js +31 -13
- package/packages/frond/src/engine.ts +39 -7
- package/packages/orm/dist/index.js +10879 -9258
- package/packages/orm/src/adapters/firebird.ts +200 -27
- package/packages/orm/src/adapters/mongodb.ts +160 -10
- package/packages/orm/src/adapters/mssql.ts +38 -11
- package/packages/orm/src/adapters/mysql.ts +24 -1
- package/packages/orm/src/adapters/odbc.ts +127 -29
- package/packages/orm/src/adapters/postgres.ts +18 -0
- package/packages/orm/src/adapters/sqlite.ts +93 -14
- package/packages/orm/src/autoCrud.ts +72 -8
- package/packages/orm/src/baseModel.ts +323 -71
- package/packages/orm/src/cachedDatabase.ts +48 -1
- package/packages/orm/src/database.ts +162 -59
- package/packages/orm/src/fakeData.ts +6 -2
- package/packages/orm/src/index.ts +4 -1
- package/packages/orm/src/migration.ts +95 -52
- package/packages/orm/src/query.ts +16 -4
- package/packages/orm/src/seeder.ts +43 -25
- package/packages/orm/src/sqlTranslator.ts +104 -19
- package/packages/orm/src/types.ts +97 -21
- package/packages/orm/src/validation.ts +5 -1
- package/packages/swagger/dist/index.js +3 -2
- package/packages/swagger/src/generator.ts +19 -4
- package/packages/swagger/src/ui.ts +6 -4
- package/types/cli/src/bin.d.ts +0 -22
- package/types/core/src/api.d.ts +11 -4
- package/types/core/src/background.d.ts +5 -2
- package/types/core/src/devAdmin.d.ts +35 -0
- package/types/core/src/dispatchPipeline.d.ts +41 -1
- package/types/core/src/errorOverlay.d.ts +13 -13
- package/types/core/src/index.d.ts +9 -6
- package/types/core/src/logger.d.ts +111 -185
- package/types/core/src/middleware.d.ts +40 -5
- package/types/core/src/portTakeover.d.ts +50 -0
- package/types/core/src/request.d.ts +15 -0
- package/types/core/src/response.d.ts +29 -0
- package/types/core/src/server.d.ts +92 -0
- package/types/core/src/testClient.d.ts +29 -3
- package/types/core/src/testing.d.ts +16 -12
- package/types/core/src/types.d.ts +21 -9
- package/types/core/src/version.d.ts +11 -0
- package/types/core/src/websocketBackplane.d.ts +1 -1
- package/types/frond/src/engine.d.ts +10 -0
- package/types/orm/src/adapters/firebird.d.ts +61 -2
- package/types/orm/src/adapters/mongodb.d.ts +20 -0
- package/types/orm/src/adapters/mssql.d.ts +11 -0
- package/types/orm/src/adapters/mysql.d.ts +11 -0
- package/types/orm/src/adapters/odbc.d.ts +35 -4
- package/types/orm/src/adapters/postgres.d.ts +11 -0
- package/types/orm/src/adapters/sqlite.d.ts +23 -4
- package/types/orm/src/baseModel.d.ts +45 -25
- package/types/orm/src/cachedDatabase.d.ts +27 -1
- package/types/orm/src/database.d.ts +56 -6
- package/types/orm/src/index.d.ts +3 -2
- package/types/orm/src/migration.d.ts +23 -5
- package/types/orm/src/query.d.ts +3 -0
- package/types/orm/src/seeder.d.ts +15 -2
- package/types/orm/src/sqlTranslator.d.ts +17 -4
- package/types/orm/src/types.d.ts +75 -16
- package/packages/core/src/errorOverlay.test.ts +0 -122
package/packages/cli/src/bin.ts
CHANGED
|
@@ -11,7 +11,7 @@ import { runSeeds } from "./commands/seed.js";
|
|
|
11
11
|
import { runMetrics } from "./commands/metrics.js";
|
|
12
12
|
import { queueCommand, QUEUE_SUBCOMMAND_NAMES } from "./commands/queue.js";
|
|
13
13
|
import { buildImage } from "./commands/build.js";
|
|
14
|
-
import {
|
|
14
|
+
import { spawnSync } from "node:child_process";
|
|
15
15
|
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
16
16
|
import { delimiter, dirname, join } from "node:path";
|
|
17
17
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
@@ -43,81 +43,34 @@ function readCliVersion(): string {
|
|
|
43
43
|
return "0.0.0";
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
// ── Port-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
* stale sibling to reclaim from -- and trying is dangerous (see below).
|
|
54
|
-
*/
|
|
55
|
-
function inContainer(): boolean {
|
|
56
|
-
if (existsSync("/.dockerenv") || existsSync("/run/.containerenv")) return true;
|
|
57
|
-
try {
|
|
58
|
-
const blob = readFileSync("/proc/1/cgroup", "utf-8");
|
|
59
|
-
return blob.includes("docker") || blob.includes("containerd") || blob.includes("kubepods");
|
|
60
|
-
} catch {
|
|
61
|
-
return false;
|
|
62
|
-
}
|
|
63
|
-
}
|
|
46
|
+
// ── Port-takeover helper ────────────────────────────────────────────
|
|
47
|
+
//
|
|
48
|
+
// The identity check, PID safety filter, container guard, dev gate and opt-out
|
|
49
|
+
// all live in ONE shared module (@tina4/core portTakeover) so this CLI path and
|
|
50
|
+
// the runtime bind-failure fallback in core/server.ts cannot diverge
|
|
51
|
+
// (TAKEOVER-DEC-02). Loaded lazily (mirroring how serve.ts pulls in core) so a
|
|
52
|
+
// quick `tina4nodejs --help` never pays to import it.
|
|
64
53
|
|
|
65
54
|
/**
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
* Every PID is validated first. `parseInt` on a non-numeric lsof field yields
|
|
69
|
-
* 1, and SIGTERM to PID 1 is the container's own init -- which is exactly how a
|
|
70
|
-
* production container logged "Killed existing process on port 7148 (PID: 1
|
|
71
|
-
* ...)" and then exited 143, killing itself on startup.
|
|
72
|
-
*/
|
|
73
|
-
/**
|
|
74
|
-
* The PIDs from `lsof -ti` output that are safe to signal.
|
|
55
|
+
* Reclaim `port` from a stale Tina4 dev server, only when it is safe.
|
|
75
56
|
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
* is what produced "Killed existing process on port 7148 (PID: 1 ...)" in a
|
|
81
|
-
* real image, where the container then exited 143.
|
|
57
|
+
* Signals a holder ONLY when a Tina4 dev server recorded its PID in the per-port
|
|
58
|
+
* PID file (TAKEOVER-DEC-01). A foreign holder is left running and a clear
|
|
59
|
+
* message is printed; takeover is also skipped in a container, outside dev mode,
|
|
60
|
+
* and when opted out (`TINA4_NO_TAKEOVER` / `tina4 serve --no-kill`).
|
|
82
61
|
*
|
|
83
|
-
*
|
|
84
|
-
* ourselves, or our own process group.
|
|
62
|
+
* Returns true only when a Tina4 holder was actually signalled.
|
|
85
63
|
*/
|
|
86
|
-
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
if (myGroup !== undefined && pid === myGroup) continue;
|
|
93
|
-
if (!pids.includes(pid)) pids.push(pid);
|
|
94
|
-
}
|
|
95
|
-
return pids;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
function killProcessOnPort(port: number): boolean {
|
|
99
|
-
if (inContainer()) return false;
|
|
100
|
-
try {
|
|
101
|
-
const result = execSync(`lsof -ti :${port}`, { encoding: "utf-8", timeout: 5000 }).trim();
|
|
102
|
-
if (!result) return false;
|
|
103
|
-
|
|
104
|
-
const killed: string[] = [];
|
|
105
|
-
// Node core exposes no getpgrp(), so myGroup stays unset here. The pid <= 1
|
|
106
|
-
// guard already covers the dangerous case (a junk field coercing to 0);
|
|
107
|
-
// Python and Ruby pass their real process group in addition.
|
|
108
|
-
for (const pid of selectablePids(result, process.pid)) {
|
|
109
|
-
try {
|
|
110
|
-
process.kill(pid, "SIGTERM");
|
|
111
|
-
killed.push(String(pid));
|
|
112
|
-
} catch {
|
|
113
|
-
// ignore ProcessLookupError / PermissionError
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
if (killed.length === 0) return false;
|
|
117
|
-
console.log(` Killed existing process on port ${port} (PID: ${killed.join(", ")})`);
|
|
64
|
+
async function killProcessOnPort(port: number): Promise<boolean> {
|
|
65
|
+
const { takeOverPort, isDev, noTakeoverOptedOut, TAKEOVER_KILLED, TAKEOVER_REFUSALS } =
|
|
66
|
+
await import("../../core/src/portTakeover.js");
|
|
67
|
+
const result = takeOverPort(port, isDev(), noTakeoverOptedOut());
|
|
68
|
+
if (result.status === TAKEOVER_KILLED) {
|
|
69
|
+
console.log(` ${result.message}`);
|
|
118
70
|
return true;
|
|
119
|
-
}
|
|
120
|
-
|
|
71
|
+
}
|
|
72
|
+
if (result.message && TAKEOVER_REFUSALS.includes(result.status)) {
|
|
73
|
+
console.log(` ${result.message}`);
|
|
121
74
|
}
|
|
122
75
|
return false;
|
|
123
76
|
}
|
|
@@ -352,7 +305,11 @@ export const COMMANDS: Record<string, CommandSpec> = {
|
|
|
352
305
|
const port = portIndex !== -1 ? parseInt(a[portIndex + 1], 10) : 7148;
|
|
353
306
|
const noBrowser = a.includes("--no-browser");
|
|
354
307
|
const noReload = a.includes("--no-reload");
|
|
355
|
-
|
|
308
|
+
// --no-kill opts out of port takeover for the whole process, so the CLI
|
|
309
|
+
// path here AND the runtime bind-failure fallback both honour it
|
|
310
|
+
// (TAKEOVER-DEC-03).
|
|
311
|
+
if (a.includes("--no-kill")) process.env.TINA4_NO_TAKEOVER = "true";
|
|
312
|
+
await killProcessOnPort(port);
|
|
356
313
|
await serveProject({ port, noBrowser, noReload });
|
|
357
314
|
},
|
|
358
315
|
usage: "[--port P] [--no-browser] [--no-reload]",
|
|
@@ -1,15 +1,25 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* CLI command: migrate — Run pending SQL migration files.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* MIG-NODE-CLI-DIVERGENT (feature 15, MIG-DEC-01): this used to be a SECOND,
|
|
5
|
+
* weaker migration implementation -- a naive `sql.split(";")` (breaks on a
|
|
6
|
+
* `;` inside a string/comment/proc block), no per-file transaction (a
|
|
7
|
+
* mid-file failure left earlier statements applied on every engine
|
|
8
|
+
* including PostgreSQL, with no rollback), no Firebird/MSSQL idempotency
|
|
9
|
+
* skips, and the ledger row recorded OUTSIDE any transaction. All untested.
|
|
10
|
+
*
|
|
11
|
+
* It now delegates to the SAME `migrate()` the ORM's programmatic API uses
|
|
12
|
+
* (`packages/orm/src/migration.ts`) -- ONE code path, so the CLI gets the
|
|
13
|
+
* transactional, robust-split, idempotent behaviour for free. The weaker
|
|
14
|
+
* re-implementation is deleted, not kept alongside (maintainability = less
|
|
15
|
+
* code).
|
|
6
16
|
*
|
|
7
17
|
* Supports both naming patterns:
|
|
8
18
|
* - Sequential: 000001_name.sql
|
|
9
19
|
* - Timestamp: YYYYMMDDHHMMSS_name.sql
|
|
10
20
|
*/
|
|
11
|
-
import { existsSync
|
|
12
|
-
import {
|
|
21
|
+
import { existsSync } from "node:fs";
|
|
22
|
+
import { resolve } from "node:path";
|
|
13
23
|
import { loadEnv } from "../../../core/src/dotenv.js";
|
|
14
24
|
|
|
15
25
|
export async function runMigrations(migrationDir?: string): Promise<void> {
|
|
@@ -25,33 +35,22 @@ export async function runMigrations(migrationDir?: string): Promise<void> {
|
|
|
25
35
|
return;
|
|
26
36
|
}
|
|
27
37
|
|
|
28
|
-
// Initialise the database so the adapter is available
|
|
29
38
|
let initDatabase: typeof import("../../../orm/src/index.js").initDatabase;
|
|
30
|
-
let
|
|
31
|
-
let isMigrationApplied: typeof import("../../../orm/src/index.js").isMigrationApplied;
|
|
32
|
-
let recordMigration: typeof import("../../../orm/src/index.js").recordMigration;
|
|
33
|
-
let getNextBatch: typeof import("../../../orm/src/index.js").getNextBatch;
|
|
34
|
-
let getAdapter: typeof import("../../../orm/src/index.js").getAdapter;
|
|
35
|
-
let adapterExecute: typeof import("../../../orm/src/index.js").adapterExecute;
|
|
39
|
+
let migrate: typeof import("../../../orm/src/index.js").migrate;
|
|
36
40
|
|
|
37
41
|
try {
|
|
38
42
|
const orm = await import("../../../orm/src/index.js");
|
|
39
43
|
initDatabase = orm.initDatabase;
|
|
40
|
-
|
|
41
|
-
isMigrationApplied = orm.isMigrationApplied;
|
|
42
|
-
recordMigration = orm.recordMigration;
|
|
43
|
-
getNextBatch = orm.getNextBatch;
|
|
44
|
-
getAdapter = orm.getAdapter;
|
|
45
|
-
adapterExecute = orm.adapterExecute;
|
|
44
|
+
migrate = orm.migrate;
|
|
46
45
|
} catch {
|
|
47
46
|
console.error(" Error: @tina4/orm is required to run migrations.");
|
|
48
47
|
process.exit(1);
|
|
49
48
|
}
|
|
50
49
|
|
|
51
|
-
//
|
|
52
|
-
//
|
|
53
|
-
//
|
|
54
|
-
//
|
|
50
|
+
// Initialise the database so the adapter is available. initDatabase() is
|
|
51
|
+
// async — MUST be awaited, otherwise setAdapter() has not run by the time
|
|
52
|
+
// migrate() asks for the adapter and the whole CLI crashes with
|
|
53
|
+
// "No database adapter configured."
|
|
55
54
|
try {
|
|
56
55
|
await initDatabase();
|
|
57
56
|
} catch (err) {
|
|
@@ -59,64 +58,26 @@ export async function runMigrations(migrationDir?: string): Promise<void> {
|
|
|
59
58
|
process.exit(1);
|
|
60
59
|
}
|
|
61
60
|
|
|
62
|
-
await
|
|
63
|
-
|
|
64
|
-
// Collect .sql files, excluding .down.sql, sorted by numeric prefix
|
|
65
|
-
const files = readdirSync(dir)
|
|
66
|
-
.filter((f) => f.endsWith(".sql") && !f.endsWith(".down.sql"))
|
|
67
|
-
.sort((a, b) => {
|
|
68
|
-
const aMatch = a.match(/^(\d+)/);
|
|
69
|
-
const bMatch = b.match(/^(\d+)/);
|
|
70
|
-
if (aMatch && bMatch) {
|
|
71
|
-
const aNum = BigInt(aMatch[1]);
|
|
72
|
-
const bNum = BigInt(bMatch[1]);
|
|
73
|
-
if (aNum < bNum) return -1;
|
|
74
|
-
if (aNum > bNum) return 1;
|
|
75
|
-
}
|
|
76
|
-
return a.localeCompare(b);
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
if (files.length === 0) {
|
|
80
|
-
console.log(" No .sql migration files found.");
|
|
81
|
-
return;
|
|
82
|
-
}
|
|
61
|
+
const result = await migrate(undefined, { migrationsDir: dir });
|
|
83
62
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
if (await isMigrationApplied(name)) {
|
|
91
|
-
continue;
|
|
63
|
+
if (result.applied.length === 0 && result.failed.length === 0) {
|
|
64
|
+
console.log(" Nothing to migrate — all migrations already applied.");
|
|
65
|
+
} else {
|
|
66
|
+
for (const file of result.applied) {
|
|
67
|
+
console.log(` Migrated: ${file}`);
|
|
92
68
|
}
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
if (!sql) continue;
|
|
96
|
-
|
|
97
|
-
console.log(` Migrating: ${file}`);
|
|
98
|
-
|
|
99
|
-
const adapter = getAdapter();
|
|
100
|
-
// Split on semicolons and execute each statement
|
|
101
|
-
const statements = sql.split(";").map((s) => s.trim()).filter(Boolean);
|
|
102
|
-
|
|
103
|
-
for (const stmt of statements) {
|
|
104
|
-
try {
|
|
105
|
-
await adapterExecute(adapter, stmt);
|
|
106
|
-
} catch (err) {
|
|
107
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
108
|
-
console.error(` Error in ${file}: ${msg}`);
|
|
109
|
-
process.exit(1);
|
|
110
|
-
}
|
|
69
|
+
if (result.applied.length > 0) {
|
|
70
|
+
console.log(` Applied ${result.applied.length} migration(s).`);
|
|
111
71
|
}
|
|
112
|
-
|
|
113
|
-
await recordMigration(name, batch);
|
|
114
|
-
applied++;
|
|
115
72
|
}
|
|
116
73
|
|
|
117
|
-
if (
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
74
|
+
if (result.failed.length > 0) {
|
|
75
|
+
// migrate() has already logged the specific statement error for each
|
|
76
|
+
// failed file (console.error + Log.error) -- fail-fast so CI/CD actually
|
|
77
|
+
// fails when a migration breaks, matching the explicit `tina4 migrate`
|
|
78
|
+
// CLI contract in every other framework (the startup auto-migrate hook
|
|
79
|
+
// is the one that swallows; this command must not).
|
|
80
|
+
console.error(` ${result.failed.length} migration(s) failed: ${result.failed.join(", ")}`);
|
|
81
|
+
process.exit(1);
|
|
121
82
|
}
|
|
122
83
|
}
|
|
@@ -54,7 +54,16 @@ export async function migrateRollback(migrationDir?: string): Promise<void> {
|
|
|
54
54
|
|
|
55
55
|
console.log(` Rolling back batch ${lastBatch[0].batch} (${lastBatch.length} migration(s))...`);
|
|
56
56
|
|
|
57
|
-
|
|
57
|
+
// rollback() is fail-safe (MIG-DEC-02): a missing/failed down artifact now
|
|
58
|
+
// THROWS instead of silently dropping the tracking record, so the CLI must
|
|
59
|
+
// catch it and exit non-zero rather than let it crash with a raw stack.
|
|
60
|
+
let rolledBack: string[];
|
|
61
|
+
try {
|
|
62
|
+
rolledBack = await rollbackFn(dir);
|
|
63
|
+
} catch (err) {
|
|
64
|
+
console.error(` Rollback failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
65
|
+
process.exit(1);
|
|
66
|
+
}
|
|
58
67
|
|
|
59
68
|
if (rolledBack.length === 0) {
|
|
60
69
|
console.log(" Nothing was rolled back.");
|
|
@@ -1,17 +1,92 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* CLI command: test — Run project tests.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* Two stages, and the process exits non-zero if EITHER fails:
|
|
5
|
+
* 1. Inline @tests stage (INLINE-DEC-01) — discover functions decorated with the
|
|
6
|
+
* inline tests() builder under src/ and run them with a real exit code. Only
|
|
7
|
+
* files that call tests() are imported, so discovery never runs an arbitrary
|
|
8
|
+
* scanned source file's side effect (INLINE-DEC-02).
|
|
9
|
+
* 2. File-runner stage — execute test files with tsx (test/*.ts, tests/*.ts, an
|
|
10
|
+
* explicit file arg), propagating their exit codes (python#96 parity).
|
|
6
11
|
*/
|
|
7
|
-
import { existsSync, readdirSync } from "node:fs";
|
|
12
|
+
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
8
13
|
import { resolve, join } from "node:path";
|
|
14
|
+
import { pathToFileURL } from "node:url";
|
|
9
15
|
import { execSync } from "node:child_process";
|
|
10
16
|
|
|
17
|
+
/** Collect every .ts/.js file under a directory, recursively. */
|
|
18
|
+
function walkSource(dir: string): string[] {
|
|
19
|
+
const out: string[] = [];
|
|
20
|
+
let entries: string[];
|
|
21
|
+
try {
|
|
22
|
+
entries = readdirSync(dir);
|
|
23
|
+
} catch {
|
|
24
|
+
return out;
|
|
25
|
+
}
|
|
26
|
+
for (const name of entries) {
|
|
27
|
+
const full = join(dir, name);
|
|
28
|
+
let st;
|
|
29
|
+
try {
|
|
30
|
+
st = statSync(full);
|
|
31
|
+
} catch {
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
if (st.isDirectory()) {
|
|
35
|
+
out.push(...walkSource(full));
|
|
36
|
+
} else if (name.endsWith(".ts") || name.endsWith(".js")) {
|
|
37
|
+
out.push(full);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return out;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Discover inline tests() under src/ and run them. Returns true if any inline
|
|
45
|
+
* test FAILED or ERRORED. Only files that call tests() are imported, so a source
|
|
46
|
+
* file without an inline test is never executed during discovery.
|
|
47
|
+
*/
|
|
48
|
+
async function runInlineTests(cwd: string): Promise<boolean> {
|
|
49
|
+
const srcDir = resolve(cwd, "src");
|
|
50
|
+
if (!existsSync(srcDir)) return false;
|
|
51
|
+
|
|
52
|
+
const { runAll, reset } = await import("@tina4/core");
|
|
53
|
+
reset();
|
|
54
|
+
|
|
55
|
+
let discovered = 0;
|
|
56
|
+
for (const file of walkSource(srcDir)) {
|
|
57
|
+
let text: string;
|
|
58
|
+
try {
|
|
59
|
+
text = readFileSync(file, "utf-8");
|
|
60
|
+
} catch {
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
if (!/\btests\s*\(/.test(text)) continue; // only files using the inline decorator
|
|
64
|
+
discovered++;
|
|
65
|
+
try {
|
|
66
|
+
await import(pathToFileURL(file).href);
|
|
67
|
+
} catch (err) {
|
|
68
|
+
console.log(
|
|
69
|
+
` ! could not import ${file}: ${err instanceof Error ? err.message : String(err)}`,
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (discovered === 0) return false;
|
|
75
|
+
|
|
76
|
+
const results = runAll();
|
|
77
|
+
return results.failed + results.errors > 0;
|
|
78
|
+
}
|
|
79
|
+
|
|
11
80
|
export async function runTests(testPath?: string): Promise<void> {
|
|
12
81
|
const cwd = process.cwd();
|
|
13
82
|
|
|
14
|
-
//
|
|
83
|
+
// Stage 1 — inline @tests discovered under src/, run with a real exit code.
|
|
84
|
+
const inlineFailed = await runInlineTests(cwd);
|
|
85
|
+
|
|
86
|
+
// Stage 2 — file runner (existing behaviour), capturing failure into fileFailed.
|
|
87
|
+
let fileFailed = false;
|
|
88
|
+
|
|
89
|
+
// If a specific test file is provided, run it directly.
|
|
15
90
|
if (testPath) {
|
|
16
91
|
const file = resolve(testPath);
|
|
17
92
|
if (!existsSync(file)) {
|
|
@@ -22,17 +97,13 @@ export async function runTests(testPath?: string): Promise<void> {
|
|
|
22
97
|
try {
|
|
23
98
|
execSync(`npx tsx "${file}"`, { cwd, stdio: "inherit" });
|
|
24
99
|
} catch {
|
|
25
|
-
|
|
100
|
+
fileFailed = true;
|
|
26
101
|
}
|
|
27
|
-
|
|
102
|
+
process.exit(inlineFailed || fileFailed ? 1 : 0);
|
|
28
103
|
}
|
|
29
104
|
|
|
30
|
-
// Auto-discover test files
|
|
31
|
-
const candidates = [
|
|
32
|
-
"test/integration.ts",
|
|
33
|
-
"test",
|
|
34
|
-
"tests",
|
|
35
|
-
];
|
|
105
|
+
// Auto-discover test files.
|
|
106
|
+
const candidates = ["test/integration.ts", "test", "tests"];
|
|
36
107
|
|
|
37
108
|
let testFiles: string[] = [];
|
|
38
109
|
|
|
@@ -40,13 +111,13 @@ export async function runTests(testPath?: string): Promise<void> {
|
|
|
40
111
|
const fullPath = resolve(cwd, candidate);
|
|
41
112
|
if (!existsSync(fullPath)) continue;
|
|
42
113
|
|
|
43
|
-
// If it's a file, run it
|
|
114
|
+
// If it's a file, run it.
|
|
44
115
|
if (candidate.endsWith(".ts")) {
|
|
45
116
|
testFiles.push(fullPath);
|
|
46
117
|
break;
|
|
47
118
|
}
|
|
48
119
|
|
|
49
|
-
// If it's a directory, collect all .ts and .test.ts files
|
|
120
|
+
// If it's a directory, collect all .ts and .test.ts files.
|
|
50
121
|
try {
|
|
51
122
|
const files = readdirSync(fullPath)
|
|
52
123
|
.filter((f) => f.endsWith(".ts"))
|
|
@@ -59,25 +130,25 @@ export async function runTests(testPath?: string): Promise<void> {
|
|
|
59
130
|
}
|
|
60
131
|
|
|
61
132
|
if (testFiles.length === 0) {
|
|
133
|
+
if (inlineFailed) {
|
|
134
|
+
process.exit(1);
|
|
135
|
+
}
|
|
62
136
|
console.log(" No test files found.");
|
|
63
|
-
console.log(" Looked in: test/integration.ts, test/*.ts, tests/*.ts");
|
|
64
|
-
|
|
137
|
+
console.log(" Looked in: src/**/*.ts (@tests), test/integration.ts, test/*.ts, tests/*.ts");
|
|
138
|
+
process.exit(0);
|
|
65
139
|
}
|
|
66
140
|
|
|
67
141
|
console.log(` Found ${testFiles.length} test file(s)\n`);
|
|
68
142
|
|
|
69
|
-
let failed = false;
|
|
70
143
|
for (const file of testFiles) {
|
|
71
144
|
const relative = file.replace(cwd + "/", "");
|
|
72
145
|
console.log(` Running: ${relative}`);
|
|
73
146
|
try {
|
|
74
147
|
execSync(`npx tsx "${file}"`, { cwd, stdio: "inherit" });
|
|
75
148
|
} catch {
|
|
76
|
-
|
|
149
|
+
fileFailed = true;
|
|
77
150
|
}
|
|
78
151
|
}
|
|
79
152
|
|
|
80
|
-
|
|
81
|
-
process.exit(1);
|
|
82
|
-
}
|
|
153
|
+
process.exit(inlineFailed || fileFailed ? 1 : 0);
|
|
83
154
|
}
|