tina4-nodejs 3.10.42 → 3.10.45
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/package.json +1 -1
- package/packages/cli/src/bin.ts +48 -4
- package/packages/cli/src/commands/generate.ts +800 -103
- package/packages/cli/src/commands/serve.ts +1 -0
- package/packages/core/src/ai.ts +488 -108
- package/packages/core/src/devAdmin.ts +634 -98
- package/packages/core/src/index.ts +1 -1
- package/packages/core/src/metrics.ts +39 -0
- package/packages/core/src/server.ts +18 -2
- package/packages/orm/src/adapters/sqlite.ts +7 -3
- package/packages/orm/src/baseModel.ts +17 -5
|
@@ -68,7 +68,7 @@ export type { ResponseCacheConfig } from "./cache.js";
|
|
|
68
68
|
export { Api } from "./api.js";
|
|
69
69
|
export type { ApiResult } from "./api.js";
|
|
70
70
|
export { Events } from "./events.js";
|
|
71
|
-
export { DevAdmin, MessageLog, RequestInspector, ErrorTracker, DevMailboxStore, DevQueue, WsTracker } from "./devAdmin.js";
|
|
71
|
+
export { DevAdmin, MessageLog, RequestInspector, ErrorTracker, DevMailboxStore, DevQueue, WsTracker, renderDashboard } from "./devAdmin.js";
|
|
72
72
|
export { Messenger } from "./messenger.js";
|
|
73
73
|
export type { SendResult, EmailMessage } from "./messenger.js";
|
|
74
74
|
export { DevMailbox, createMessenger } from "./devMailbox.js";
|
|
@@ -54,6 +54,18 @@ function relativePath(filePath: string): string {
|
|
|
54
54
|
return path.relative(".", filePath);
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
+
// ── Test file detection ─────────────────────────────────────
|
|
58
|
+
|
|
59
|
+
function hasMatchingTest(relPath: string): boolean {
|
|
60
|
+
const name = relPath.split('/').pop()?.replace('.ts', '').replace('.js', '') || '';
|
|
61
|
+
const patterns = [
|
|
62
|
+
`test/${name}.test.ts`,
|
|
63
|
+
`${relPath.replace('.ts', '.test.ts').replace('.js', '.test.js')}`,
|
|
64
|
+
`tests/${name}.test.ts`,
|
|
65
|
+
];
|
|
66
|
+
return patterns.some(p => fs.existsSync(p));
|
|
67
|
+
}
|
|
68
|
+
|
|
57
69
|
// ── Line counting ────────────────────────────────────────────
|
|
58
70
|
|
|
59
71
|
interface LineCounts {
|
|
@@ -517,9 +529,27 @@ function detectViolations(
|
|
|
517
529
|
return violations;
|
|
518
530
|
}
|
|
519
531
|
|
|
532
|
+
// ── Root Resolution ──────────────────────────────────────────
|
|
533
|
+
|
|
534
|
+
/**
|
|
535
|
+
* Pick the right directory to scan.
|
|
536
|
+
*
|
|
537
|
+
* If the root dir has .ts files, scan the user's project code.
|
|
538
|
+
* Otherwise, scan the framework itself — so the bubble chart is never empty.
|
|
539
|
+
*/
|
|
540
|
+
function resolveRoot(root: string = "src"): string {
|
|
541
|
+
const rootPath = path.resolve(root);
|
|
542
|
+
if (fs.existsSync(rootPath) && walkFiles(rootPath, [".ts", ".js"]).length > 0) {
|
|
543
|
+
return root;
|
|
544
|
+
}
|
|
545
|
+
// Fallback: scan the framework package itself
|
|
546
|
+
return path.resolve(path.dirname(new URL(import.meta.url).pathname));
|
|
547
|
+
}
|
|
548
|
+
|
|
520
549
|
// ── Quick Metrics ────────────────────────────────────────────
|
|
521
550
|
|
|
522
551
|
export function quickMetrics(root: string = "src"): Record<string, any> {
|
|
552
|
+
root = resolveRoot(root);
|
|
523
553
|
const rootPath = path.resolve(root);
|
|
524
554
|
if (!fs.existsSync(rootPath)) {
|
|
525
555
|
return { error: `Directory not found: ${root}` };
|
|
@@ -644,6 +674,7 @@ function filesHash(root: string = "src"): string {
|
|
|
644
674
|
}
|
|
645
675
|
|
|
646
676
|
export function fullAnalysis(root: string = "src"): Record<string, any> {
|
|
677
|
+
root = resolveRoot(root);
|
|
647
678
|
const currentHash = filesHash(root);
|
|
648
679
|
const now = Date.now() / 1000;
|
|
649
680
|
|
|
@@ -730,6 +761,8 @@ export function fullAnalysis(root: string = "src"): Record<string, any> {
|
|
|
730
761
|
coupling_afferent: ca,
|
|
731
762
|
coupling_efferent: ce,
|
|
732
763
|
instability: Math.round(instability * 1000) / 1000,
|
|
764
|
+
has_tests: hasMatchingTest(relPath),
|
|
765
|
+
dep_count: ce,
|
|
733
766
|
});
|
|
734
767
|
}
|
|
735
768
|
|
|
@@ -746,6 +779,10 @@ export function fullAnalysis(root: string = "src"): Record<string, any> {
|
|
|
746
779
|
const totalMI = fileMetrics.reduce((sum, f) => sum + f.maintainability, 0);
|
|
747
780
|
const avgMI = fileMetrics.length > 0 ? totalMI / fileMetrics.length : 0;
|
|
748
781
|
|
|
782
|
+
// Detect if we're scanning framework or project
|
|
783
|
+
const frameworkDir = path.resolve(path.dirname(new URL(import.meta.url).pathname));
|
|
784
|
+
const scanningFramework = rootPath === frameworkDir || rootPath.startsWith(frameworkDir + path.sep);
|
|
785
|
+
|
|
749
786
|
const result: Record<string, any> = {
|
|
750
787
|
files_analyzed: fileMetrics.length,
|
|
751
788
|
total_functions: allFunctions.length,
|
|
@@ -755,6 +792,8 @@ export function fullAnalysis(root: string = "src"): Record<string, any> {
|
|
|
755
792
|
file_metrics: fileMetrics,
|
|
756
793
|
violations,
|
|
757
794
|
dependency_graph: importGraph,
|
|
795
|
+
scan_mode: scanningFramework ? "framework" : "project",
|
|
796
|
+
scan_root: rootPath,
|
|
758
797
|
};
|
|
759
798
|
|
|
760
799
|
_fullCache = { hash: currentHash, data: result, time: now };
|
|
@@ -302,6 +302,8 @@ h1{font-size:3rem;font-weight:700;margin-bottom:0.25rem;letter-spacing:-1px}
|
|
|
302
302
|
.gbtn-deploy{background:#3b82f6;color:#fff}
|
|
303
303
|
.gbtn-deploy:hover{background:#2563eb}
|
|
304
304
|
.gbtn-deployed{background:transparent;border:1px solid #22c55e;color:#22c55e;cursor:default;font-size:0.7rem}
|
|
305
|
+
@keyframes wiggle{0%{transform:rotate(0deg)}15%{transform:rotate(14deg)}30%{transform:rotate(-10deg)}45%{transform:rotate(8deg)}60%{transform:rotate(-4deg)}75%{transform:rotate(2deg)}100%{transform:rotate(0deg)}}
|
|
306
|
+
.star-wiggle{display:inline-block;transform-origin:center}
|
|
305
307
|
</style>
|
|
306
308
|
</head>
|
|
307
309
|
<body>
|
|
@@ -315,7 +317,7 @@ h1{font-size:3rem;font-weight:700;margin-bottom:0.25rem;letter-spacing:-1px}
|
|
|
315
317
|
<a href="/__dev" class="btn">Dev Admin</a>
|
|
316
318
|
<a href="#gallery" class="btn">Gallery</a>
|
|
317
319
|
<a href="https://github.com/tina4stack/tina4-nodejs" class="btn" target="_blank">GitHub</a>
|
|
318
|
-
<a href="https://github.com/tina4stack/tina4-nodejs/stargazers" class="btn" target="_blank">&#
|
|
320
|
+
<a href="https://github.com/tina4stack/tina4-nodejs/stargazers" class="btn" target="_blank"><span class="star-wiggle">☆</span> Star</a>
|
|
319
321
|
</div>
|
|
320
322
|
<div class="status">
|
|
321
323
|
<span><span class="dot"></span>Server running</span>
|
|
@@ -364,6 +366,20 @@ function deployGallery(name) {
|
|
|
364
366
|
})
|
|
365
367
|
.catch(function(e) { alert('Deploy error: ' + e.message); });
|
|
366
368
|
}
|
|
369
|
+
(function(){
|
|
370
|
+
var star=document.querySelector('.star-wiggle');
|
|
371
|
+
if(!star)return;
|
|
372
|
+
function doWiggle(){
|
|
373
|
+
star.style.animation='wiggle 1.2s ease-in-out';
|
|
374
|
+
star.addEventListener('animationend',function onEnd(){
|
|
375
|
+
star.removeEventListener('animationend',onEnd);
|
|
376
|
+
star.style.animation='none';
|
|
377
|
+
var delay=3000+Math.random()*15000;
|
|
378
|
+
setTimeout(doWiggle,delay);
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
setTimeout(doWiggle,3000);
|
|
382
|
+
})();
|
|
367
383
|
</script>
|
|
368
384
|
</body>
|
|
369
385
|
</html>`;
|
|
@@ -731,7 +747,7 @@ ${reset}
|
|
|
731
747
|
let result: unknown;
|
|
732
748
|
const routeParams = req.params || {};
|
|
733
749
|
const fnStr = match.handler.toString();
|
|
734
|
-
const argMatch = fnStr.match(/^(?:async\s
|
|
750
|
+
const argMatch = fnStr.match(/^(?:async\s*)?(?:function\s*\w*)?\s*\(([^)]*)\)/);
|
|
735
751
|
const argNames = argMatch?.[1]?.split(",").map((s: string) => s.trim().replace(/[:=].*/,"")) ?? [];
|
|
736
752
|
const filteredArgs = argNames.filter((n: string) => n.length > 0);
|
|
737
753
|
|
|
@@ -57,9 +57,13 @@ export class SQLiteAdapter implements DatabaseAdapter {
|
|
|
57
57
|
fetch<T = Record<string, unknown>>(sql: string, params?: unknown[], limit?: number, skip?: number): T[] {
|
|
58
58
|
let effectiveSql = sql;
|
|
59
59
|
if (limit !== undefined) {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
60
|
+
// Skip appending LIMIT when the SQL already contains one (dedup)
|
|
61
|
+
const sqlBeforeComment = sql.toUpperCase().split("--")[0];
|
|
62
|
+
if (!sqlBeforeComment.includes("LIMIT")) {
|
|
63
|
+
effectiveSql += ` LIMIT ${limit}`;
|
|
64
|
+
if (skip !== undefined && skip > 0) {
|
|
65
|
+
effectiveSql += ` OFFSET ${skip}`;
|
|
66
|
+
}
|
|
63
67
|
}
|
|
64
68
|
}
|
|
65
69
|
return this.query<T>(effectiveSql, params);
|
|
@@ -17,6 +17,15 @@ export function camelToSnake(name: string): string {
|
|
|
17
17
|
return name.replace(/[A-Z]/g, (c) => `_${c.toLowerCase()}`);
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
/**
|
|
21
|
+
* Check whether ORM_PLURAL_TABLE_NAMES is enabled in .env.
|
|
22
|
+
* When true, hasMany relationship keys get an "s" suffix (e.g. "posts" instead of "post").
|
|
23
|
+
*/
|
|
24
|
+
function _pluralRelKeys(): boolean {
|
|
25
|
+
const v = process.env.ORM_PLURAL_TABLE_NAMES ?? "";
|
|
26
|
+
return /^(true|1|yes)$/i.test(v);
|
|
27
|
+
}
|
|
28
|
+
|
|
20
29
|
/**
|
|
21
30
|
* BaseModel provides instance methods for ORM models.
|
|
22
31
|
* Models extend this class and define static properties.
|
|
@@ -395,7 +404,8 @@ export class BaseModel {
|
|
|
395
404
|
}
|
|
396
405
|
if (ModelClass.hasMany) {
|
|
397
406
|
for (const rel of ModelClass.hasMany) {
|
|
398
|
-
const
|
|
407
|
+
const base = rel.model.toLowerCase();
|
|
408
|
+
const relKey = _pluralRelKeys() ? base + "s" : base;
|
|
399
409
|
if (this[relKey] !== undefined) {
|
|
400
410
|
result[relKey] = this[relKey];
|
|
401
411
|
}
|
|
@@ -818,8 +828,9 @@ export class BaseModel {
|
|
|
818
828
|
// Check hasMany
|
|
819
829
|
if (ModelClass.hasMany) {
|
|
820
830
|
const rel = ModelClass.hasMany.find((r) => {
|
|
821
|
-
const
|
|
822
|
-
|
|
831
|
+
const base = r.model.toLowerCase();
|
|
832
|
+
const key = _pluralRelKeys() ? base + "s" : base;
|
|
833
|
+
return key === relName || base === relName || r.model === relName;
|
|
823
834
|
});
|
|
824
835
|
if (rel) {
|
|
825
836
|
const relatedClass = BaseModel._modelRegistry[rel.model];
|
|
@@ -877,8 +888,9 @@ export class BaseModel {
|
|
|
877
888
|
}
|
|
878
889
|
if (!relDef && ModelClass.hasMany) {
|
|
879
890
|
relDef = ModelClass.hasMany.find((r) => {
|
|
880
|
-
const
|
|
881
|
-
|
|
891
|
+
const base = r.model.toLowerCase();
|
|
892
|
+
const key = _pluralRelKeys() ? base + "s" : base;
|
|
893
|
+
return key === relName || base === relName || r.model === relName;
|
|
882
894
|
});
|
|
883
895
|
if (relDef) relType = "hasMany";
|
|
884
896
|
}
|