tina4-nodejs 3.10.86 → 3.10.88
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tina4-nodejs",
|
|
3
|
-
"version": "3.10.
|
|
3
|
+
"version": "3.10.88",
|
|
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"],
|
|
@@ -37,17 +37,34 @@ export async function serveProject(options: ServeOptions): Promise<void> {
|
|
|
37
37
|
staticDir,
|
|
38
38
|
});
|
|
39
39
|
|
|
40
|
-
// Watch for file changes
|
|
40
|
+
// Watch for file changes.
|
|
41
|
+
//
|
|
42
|
+
// Templates and static assets are re-read from disk every request in dev mode,
|
|
43
|
+
// so we only need to touch the router when a .ts/.js route file actually
|
|
44
|
+
// changes. Clearing the router on every edit (including templates) leaves a
|
|
45
|
+
// brief window where the router is empty — any request hitting that window
|
|
46
|
+
// gets a 404 whose response path bypasses the dev toolbar injection, so the
|
|
47
|
+
// toolbar appears to "vanish" after a hot reload. Route-file-only clearing
|
|
48
|
+
// matches the behaviour of Python's DevReload and the fix made in PHP v3.10.87.
|
|
41
49
|
const noReload = ["true", "1", "yes"].includes((process.env.TINA4_NO_RELOAD ?? "").toLowerCase());
|
|
42
50
|
const watchDirs = [routesDir, ormDir, modelsDir, templatesDir].filter((d) => existsSync(d));
|
|
43
51
|
let watcher: { close: () => void } | null = null;
|
|
44
52
|
if (!noReload) {
|
|
45
|
-
watcher = watchForChanges(watchDirs, async () => {
|
|
53
|
+
watcher = watchForChanges(watchDirs, async ({ code }) => {
|
|
54
|
+
if (!code) {
|
|
55
|
+
// Template/CSS/JS asset change — nothing to do in the server. The
|
|
56
|
+
// browser will re-fetch on its own reload cycle and the request will
|
|
57
|
+
// be served against the existing route set with the toolbar intact.
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
46
60
|
try {
|
|
61
|
+
// Re-discover routes. discoverRoutes() cache-busts imports via ?t=<timestamp>,
|
|
62
|
+
// so the new modules are loaded fresh. Build the new list first, then
|
|
63
|
+
// replace the router's state in one back-to-back block to minimise the
|
|
64
|
+
// window where the router is empty.
|
|
47
65
|
const { discoverRoutes } = await import("../../../core/src/index.js");
|
|
48
|
-
// Clear routes BEFORE re-discovery to avoid stale duplicates
|
|
49
|
-
server.router.clear();
|
|
50
66
|
const routes = await discoverRoutes(routesDir);
|
|
67
|
+
server.router.clear();
|
|
51
68
|
for (const route of routes) {
|
|
52
69
|
server.router.addRoute(route);
|
|
53
70
|
}
|
|
@@ -1,25 +1,48 @@
|
|
|
1
1
|
import { watch, existsSync } from "node:fs";
|
|
2
|
-
import { resolve } from "node:path";
|
|
2
|
+
import { resolve, extname } from "node:path";
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
* File types that indicate a code change and require route re-discovery.
|
|
6
|
+
* Template/CSS/JS asset changes don't need the router to be touched.
|
|
7
|
+
*/
|
|
8
|
+
const CODE_EXTENSIONS = new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"]);
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Watch directories for file changes.
|
|
12
|
+
*
|
|
13
|
+
* The callback receives `{ code: boolean }` indicating whether any of the
|
|
14
|
+
* changed files were source code (.ts/.js). If only templates/CSS/JS assets
|
|
15
|
+
* changed, `code` is false and the caller should skip route re-discovery —
|
|
16
|
+
* clearing the router for an asset edit leaves a brief window of 404s that
|
|
17
|
+
* make the dev toolbar vanish.
|
|
18
|
+
*/
|
|
4
19
|
export function watchForChanges(
|
|
5
20
|
dirs: string[],
|
|
6
|
-
onChange: () => void
|
|
21
|
+
onChange: (info: { code: boolean }) => void
|
|
7
22
|
): { close: () => void } {
|
|
8
23
|
const watchers: ReturnType<typeof watch>[] = [];
|
|
9
24
|
let debounceTimer: ReturnType<typeof setTimeout> | null = null;
|
|
25
|
+
let codeChangePending = false;
|
|
10
26
|
|
|
11
27
|
const debouncedOnChange = () => {
|
|
12
28
|
if (debounceTimer) clearTimeout(debounceTimer);
|
|
13
29
|
debounceTimer = setTimeout(() => {
|
|
14
|
-
|
|
15
|
-
|
|
30
|
+
const code = codeChangePending;
|
|
31
|
+
codeChangePending = false;
|
|
32
|
+
console.log(
|
|
33
|
+
`\n \x1b[33mFile change detected${code ? ", reloading routes" : ""}...\x1b[0m\n`,
|
|
34
|
+
);
|
|
35
|
+
onChange({ code });
|
|
16
36
|
}, 200);
|
|
17
37
|
};
|
|
18
38
|
|
|
19
39
|
for (const dir of dirs) {
|
|
20
40
|
if (!existsSync(dir)) continue;
|
|
21
41
|
try {
|
|
22
|
-
const watcher = watch(resolve(dir), { recursive: true }, () => {
|
|
42
|
+
const watcher = watch(resolve(dir), { recursive: true }, (_event, filename) => {
|
|
43
|
+
if (filename && CODE_EXTENSIONS.has(extname(filename))) {
|
|
44
|
+
codeChangePending = true;
|
|
45
|
+
}
|
|
23
46
|
debouncedOnChange();
|
|
24
47
|
});
|
|
25
48
|
watchers.push(watcher);
|
|
@@ -19,6 +19,103 @@ class SafeString {
|
|
|
19
19
|
toString() { return this.value; }
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
/**
|
|
23
|
+
* Produce a human-readable, debugger-friendly inspection of any value.
|
|
24
|
+
*
|
|
25
|
+
* Equivalent to PHP's var_dump, Python's repr, and Ruby's inspect. Unlike
|
|
26
|
+
* JSON.stringify (the previous implementation) this handles:
|
|
27
|
+
* - Circular references (marked as [Circular])
|
|
28
|
+
* - BigInt (shown as `123n`)
|
|
29
|
+
* - undefined, Symbol, and function values (shown inline, not dropped)
|
|
30
|
+
* - Date, Map, Set, Error, RegExp (shown with their type and contents)
|
|
31
|
+
* - Class instances (shown with the class name prefix)
|
|
32
|
+
*
|
|
33
|
+
* Intended for the `|dump` filter in templates. Output is a plain string;
|
|
34
|
+
* the filter wraps it in <pre> and marks it safe so the template engine
|
|
35
|
+
* doesn't double-escape.
|
|
36
|
+
*/
|
|
37
|
+
function inspectValue(value: unknown, seen: WeakSet<object> = new WeakSet(), depth = 0): string {
|
|
38
|
+
// Primitives
|
|
39
|
+
if (value === null) return "null";
|
|
40
|
+
if (value === undefined) return "undefined";
|
|
41
|
+
if (typeof value === "string") return JSON.stringify(value);
|
|
42
|
+
if (typeof value === "number" || typeof value === "boolean") return String(value);
|
|
43
|
+
if (typeof value === "bigint") return `${value.toString()}n`;
|
|
44
|
+
if (typeof value === "symbol") return value.toString();
|
|
45
|
+
if (typeof value === "function") {
|
|
46
|
+
const name = value.name || "(anonymous)";
|
|
47
|
+
return `[Function: ${name}]`;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// value is now object (including arrays, Date, Map, Set, etc.)
|
|
51
|
+
const obj = value as object;
|
|
52
|
+
|
|
53
|
+
// Cycle detection
|
|
54
|
+
if (seen.has(obj)) return "[Circular]";
|
|
55
|
+
seen.add(obj);
|
|
56
|
+
|
|
57
|
+
// Depth cap — prevents runaway recursion on enormous graphs
|
|
58
|
+
if (depth > 8) return "[...]";
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
// Date
|
|
62
|
+
if (obj instanceof Date) {
|
|
63
|
+
return `Date(${obj.toISOString()})`;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// RegExp
|
|
67
|
+
if (obj instanceof RegExp) {
|
|
68
|
+
return obj.toString();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Error
|
|
72
|
+
if (obj instanceof Error) {
|
|
73
|
+
return `${obj.constructor.name}(${JSON.stringify(obj.message)})`;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Map
|
|
77
|
+
if (obj instanceof Map) {
|
|
78
|
+
if (obj.size === 0) return "Map(0) {}";
|
|
79
|
+
const entries: string[] = [];
|
|
80
|
+
for (const [k, v] of obj) {
|
|
81
|
+
entries.push(`${inspectValue(k, seen, depth + 1)} => ${inspectValue(v, seen, depth + 1)}`);
|
|
82
|
+
}
|
|
83
|
+
return `Map(${obj.size}) { ${entries.join(", ")} }`;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Set
|
|
87
|
+
if (obj instanceof Set) {
|
|
88
|
+
if (obj.size === 0) return "Set(0) {}";
|
|
89
|
+
const items: string[] = [];
|
|
90
|
+
for (const v of obj) {
|
|
91
|
+
items.push(inspectValue(v, seen, depth + 1));
|
|
92
|
+
}
|
|
93
|
+
return `Set(${obj.size}) { ${items.join(", ")} }`;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Array
|
|
97
|
+
if (Array.isArray(obj)) {
|
|
98
|
+
if (obj.length === 0) return "[]";
|
|
99
|
+
const items = obj.map((v) => inspectValue(v, seen, depth + 1));
|
|
100
|
+
return `[${items.join(", ")}]`;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Plain object or class instance
|
|
104
|
+
const keys = Object.keys(obj);
|
|
105
|
+
const className = obj.constructor && obj.constructor.name !== "Object"
|
|
106
|
+
? `${obj.constructor.name} `
|
|
107
|
+
: "";
|
|
108
|
+
if (keys.length === 0) return `${className}{}`;
|
|
109
|
+
const props = keys.map((k) => {
|
|
110
|
+
const v = (obj as Record<string, unknown>)[k];
|
|
111
|
+
return `${k}: ${inspectValue(v, seen, depth + 1)}`;
|
|
112
|
+
});
|
|
113
|
+
return `${className}{ ${props.join(", ")} }`;
|
|
114
|
+
} finally {
|
|
115
|
+
seen.delete(obj);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
22
119
|
type TokenType = "TEXT" | "VAR" | "BLOCK" | "COMMENT";
|
|
23
120
|
type Token = [TokenType, string];
|
|
24
121
|
|
|
@@ -1590,7 +1687,18 @@ export class Frond {
|
|
|
1590
1687
|
case "keys": value = (typeof value === "object" && value !== null && !Array.isArray(value)) ? Object.keys(value) : []; continue;
|
|
1591
1688
|
case "values": value = (typeof value === "object" && value !== null && !Array.isArray(value)) ? Object.values(value) : []; continue;
|
|
1592
1689
|
case "json_encode": value = JSON.stringify(value); continue;
|
|
1593
|
-
case "dump":
|
|
1690
|
+
case "dump": {
|
|
1691
|
+
// Use a safe inspector rather than JSON.stringify — handles
|
|
1692
|
+
// circular refs, BigInt, Map/Set, Error, Date, class instances.
|
|
1693
|
+
const dumped = inspectValue(value);
|
|
1694
|
+
const escaped = dumped
|
|
1695
|
+
.replace(/&/g, "&")
|
|
1696
|
+
.replace(/</g, "<")
|
|
1697
|
+
.replace(/>/g, ">")
|
|
1698
|
+
.replace(/"/g, """);
|
|
1699
|
+
value = new SafeString(`<pre>${escaped}</pre>`);
|
|
1700
|
+
continue;
|
|
1701
|
+
}
|
|
1594
1702
|
case "nl2br": value = String(value).replace(/\n/g, "<br>\n"); continue;
|
|
1595
1703
|
case "unique": value = Array.isArray(value) ? [...new Set(value)] : value; continue;
|
|
1596
1704
|
case "sort": value = Array.isArray(value) ? [...value].sort() : value; continue;
|