tina4-nodejs 3.13.87 → 3.13.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/CLAUDE.md +3 -3
- package/package.json +1 -1
- package/packages/cli/dist/bin.js +38 -5
- package/packages/core/dist/index.js +38 -5
- package/packages/frond/dist/index.js +37 -4
- package/packages/frond/src/engine.ts +71 -4
- package/packages/orm/dist/index.js +38 -5
package/CLAUDE.md
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
# CLAUDE.md - AI Developer Guide for tina4-nodejs (v3.13.
|
|
1
|
+
# CLAUDE.md - AI Developer Guide for tina4-nodejs (v3.13.88)
|
|
2
2
|
|
|
3
3
|
> This file helps AI assistants (Claude, Copilot, Cursor, etc.) understand and work on this codebase effectively.
|
|
4
4
|
|
|
5
5
|
## What This Project Is
|
|
6
6
|
|
|
7
|
-
Tina4 for Node.js/TypeScript v3.13.
|
|
7
|
+
Tina4 for Node.js/TypeScript v3.13.88 - 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
|
|
|
@@ -1244,7 +1244,7 @@ When adding new features, add a corresponding `test/<feature>.test.ts` file.
|
|
|
1244
1244
|
## v3 Features Summary
|
|
1245
1245
|
|
|
1246
1246
|
- **98 built-in features**, zero third-party dependencies
|
|
1247
|
-
- **5,
|
|
1247
|
+
- **5,899 tests** passing across 189 files (build + typecheck green)
|
|
1248
1248
|
- **Race-safe `getNextId()`** with atomic sequence table (`tina4_sequences`) for SQLite/MySQL/MSSQL; PostgreSQL auto-creates sequences
|
|
1249
1249
|
- **Frond template engine optimizations**: pre-compiled regexes, lazy loop context (copy-on-write), filter chain caching, path split caching, inline common filters (11-15% speedup)
|
|
1250
1250
|
- **Production server auto-detect**: `npx tina4nodejs serve --production` auto-uses cluster mode
|
package/package.json
CHANGED
package/packages/cli/dist/bin.js
CHANGED
|
@@ -12786,6 +12786,26 @@ __export(engine_exports, {
|
|
|
12786
12786
|
import { createHash as createHash2, createHmac as createHmac3, randomBytes as randomBytes4 } from "node:crypto";
|
|
12787
12787
|
import { readFileSync as readFileSync6, existsSync as existsSync9, statSync as statSync6 } from "node:fs";
|
|
12788
12788
|
import { join as join11, resolve as resolve7 } from "node:path";
|
|
12789
|
+
function jsonText(value) {
|
|
12790
|
+
try {
|
|
12791
|
+
const text = JSON.stringify(value);
|
|
12792
|
+
return text === void 0 ? "null" : text;
|
|
12793
|
+
} catch {
|
|
12794
|
+
const seen = /* @__PURE__ */ new WeakSet();
|
|
12795
|
+
const text = JSON.stringify(value, (_key, v) => {
|
|
12796
|
+
if (typeof v === "bigint") return v.toString();
|
|
12797
|
+
if (typeof v === "object" && v !== null) {
|
|
12798
|
+
if (seen.has(v)) return null;
|
|
12799
|
+
seen.add(v);
|
|
12800
|
+
}
|
|
12801
|
+
return v;
|
|
12802
|
+
});
|
|
12803
|
+
return text === void 0 ? "null" : text;
|
|
12804
|
+
}
|
|
12805
|
+
}
|
|
12806
|
+
function jsonSafe(value) {
|
|
12807
|
+
return new SafeString(jsonText(value).replace(JSON_UNSAFE_RE, (c) => JSON_UNSAFE_MAP[c]));
|
|
12808
|
+
}
|
|
12789
12809
|
function inspectValue(value, seen = /* @__PURE__ */ new WeakSet(), depth = 0) {
|
|
12790
12810
|
if (value === null) return "null";
|
|
12791
12811
|
if (value === void 0) return "undefined";
|
|
@@ -13773,7 +13793,7 @@ function _generateFormToken(descriptor = "") {
|
|
|
13773
13793
|
function _generateFormTokenValue(descriptor = "") {
|
|
13774
13794
|
return new SafeString(_buildFormTokenJwt(descriptor));
|
|
13775
13795
|
}
|
|
13776
|
-
var SafeString, NUMERIC_RE, METHOD_CALL_RE, FN_CALL_RE, IS_NOT_RE, IS_RE, NOT_IN_RE, IN_RE, DIVISIBLE_BY_RE, FILTER_WITH_ARGS_RE, FILTER_COMPARISON_RE, TITLE_WORD_RE, STRIP_TAGS_RE, FORMAT_RE, LEADING_WS_RE, TRAILING_WS_RE, THOUSANDS_RE, LIVE_RE, LIVE_WS_RE, LIVE_SRC_RE, filterChainCache, pathParseCache, TEMPLATE_CACHE_MAX, TOKEN_RE, RAW_BLOCK_RE, VarRef, BUILTIN_FILTERS, _formTokenSessionId, Frond;
|
|
13796
|
+
var SafeString, JSON_UNSAFE_RE, JSON_UNSAFE_MAP, NUMERIC_RE, METHOD_CALL_RE, FN_CALL_RE, IS_NOT_RE, IS_RE, NOT_IN_RE, IN_RE, DIVISIBLE_BY_RE, FILTER_WITH_ARGS_RE, FILTER_COMPARISON_RE, TITLE_WORD_RE, STRIP_TAGS_RE, FORMAT_RE, LEADING_WS_RE, TRAILING_WS_RE, THOUSANDS_RE, LIVE_RE, LIVE_WS_RE, LIVE_SRC_RE, filterChainCache, pathParseCache, TEMPLATE_CACHE_MAX, TOKEN_RE, RAW_BLOCK_RE, VarRef, BUILTIN_FILTERS, _formTokenSessionId, Frond;
|
|
13777
13797
|
var init_engine = __esm({
|
|
13778
13798
|
"../frond/src/engine.ts"() {
|
|
13779
13799
|
"use strict";
|
|
@@ -13785,6 +13805,15 @@ var init_engine = __esm({
|
|
|
13785
13805
|
return this.value;
|
|
13786
13806
|
}
|
|
13787
13807
|
};
|
|
13808
|
+
JSON_UNSAFE_RE = /[<>&'\u2028\u2029]/g;
|
|
13809
|
+
JSON_UNSAFE_MAP = {
|
|
13810
|
+
"<": "\\u003c",
|
|
13811
|
+
">": "\\u003e",
|
|
13812
|
+
"&": "\\u0026",
|
|
13813
|
+
"'": "\\u0027",
|
|
13814
|
+
"\u2028": "\\u2028",
|
|
13815
|
+
"\u2029": "\\u2029"
|
|
13816
|
+
};
|
|
13788
13817
|
NUMERIC_RE = /^-?\d+(\.\d+)?$/;
|
|
13789
13818
|
METHOD_CALL_RE = /^(\w+)\s*\(([\s\S]*)?\)$/;
|
|
13790
13819
|
FN_CALL_RE = /^([\w.]+)\s*\(([\s\S]*)?\)$/;
|
|
@@ -13875,7 +13904,7 @@ var init_engine = __esm({
|
|
|
13875
13904
|
int: (v) => v ? parseInt(String(v), 10) || 0 : 0,
|
|
13876
13905
|
float: (v) => v ? parseFloat(String(v)) || 0 : 0,
|
|
13877
13906
|
string: (v) => String(v),
|
|
13878
|
-
json_encode: (v) =>
|
|
13907
|
+
json_encode: (v) => jsonSafe(v),
|
|
13879
13908
|
json_decode: (v) => typeof v === "string" ? JSON.parse(v) : v,
|
|
13880
13909
|
keys: (v) => typeof v === "object" && v !== null && !Array.isArray(v) ? Object.keys(v) : [],
|
|
13881
13910
|
values: (v) => typeof v === "object" && v !== null && !Array.isArray(v) ? Object.values(v) : [],
|
|
@@ -14020,8 +14049,12 @@ var init_engine = __esm({
|
|
|
14020
14049
|
form_token: (v) => _generateFormToken(v != null ? String(v) : ""),
|
|
14021
14050
|
formTokenValue: (v) => _generateFormTokenValue(v != null ? String(v) : ""),
|
|
14022
14051
|
form_token_value: (v) => _generateFormTokenValue(v != null ? String(v) : ""),
|
|
14023
|
-
|
|
14024
|
-
|
|
14052
|
+
// Same serializer as json_encode -- the three names are one behaviour. The
|
|
14053
|
+
// old indent argument is gone: PHP cannot honour an arbitrary indent
|
|
14054
|
+
// (JSON_PRETTY_PRINT is fixed at four spaces), so honouring it here alone
|
|
14055
|
+
// broke byte-parity for the one filter whose whole job is a wire format.
|
|
14056
|
+
tojson: (v) => jsonSafe(v),
|
|
14057
|
+
to_json: (v) => jsonSafe(v),
|
|
14025
14058
|
js_escape: (v) => new SafeString(String(v).replace(/\\/g, "\\\\").replace(/'/g, "\\'").replace(/"/g, '\\"').replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/\t/g, "\\t"))
|
|
14026
14059
|
};
|
|
14027
14060
|
_formTokenSessionId = "";
|
|
@@ -14761,7 +14794,7 @@ var init_engine = __esm({
|
|
|
14761
14794
|
value = typeof value === "object" && value !== null && !Array.isArray(value) ? Object.values(value) : [];
|
|
14762
14795
|
continue;
|
|
14763
14796
|
case "json_encode":
|
|
14764
|
-
value =
|
|
14797
|
+
value = jsonSafe(value);
|
|
14765
14798
|
continue;
|
|
14766
14799
|
case "dump":
|
|
14767
14800
|
value = renderDump(value);
|
|
@@ -12785,6 +12785,26 @@ __export(engine_exports, {
|
|
|
12785
12785
|
import { createHash as createHash2, createHmac as createHmac3, randomBytes as randomBytes4 } from "node:crypto";
|
|
12786
12786
|
import { readFileSync as readFileSync6, existsSync as existsSync8, statSync as statSync6 } from "node:fs";
|
|
12787
12787
|
import { join as join10, resolve as resolve6 } from "node:path";
|
|
12788
|
+
function jsonText(value) {
|
|
12789
|
+
try {
|
|
12790
|
+
const text = JSON.stringify(value);
|
|
12791
|
+
return text === void 0 ? "null" : text;
|
|
12792
|
+
} catch {
|
|
12793
|
+
const seen = /* @__PURE__ */ new WeakSet();
|
|
12794
|
+
const text = JSON.stringify(value, (_key, v) => {
|
|
12795
|
+
if (typeof v === "bigint") return v.toString();
|
|
12796
|
+
if (typeof v === "object" && v !== null) {
|
|
12797
|
+
if (seen.has(v)) return null;
|
|
12798
|
+
seen.add(v);
|
|
12799
|
+
}
|
|
12800
|
+
return v;
|
|
12801
|
+
});
|
|
12802
|
+
return text === void 0 ? "null" : text;
|
|
12803
|
+
}
|
|
12804
|
+
}
|
|
12805
|
+
function jsonSafe(value) {
|
|
12806
|
+
return new SafeString(jsonText(value).replace(JSON_UNSAFE_RE, (c) => JSON_UNSAFE_MAP[c]));
|
|
12807
|
+
}
|
|
12788
12808
|
function inspectValue(value, seen = /* @__PURE__ */ new WeakSet(), depth = 0) {
|
|
12789
12809
|
if (value === null) return "null";
|
|
12790
12810
|
if (value === void 0) return "undefined";
|
|
@@ -13772,7 +13792,7 @@ function _generateFormToken(descriptor = "") {
|
|
|
13772
13792
|
function _generateFormTokenValue(descriptor = "") {
|
|
13773
13793
|
return new SafeString(_buildFormTokenJwt(descriptor));
|
|
13774
13794
|
}
|
|
13775
|
-
var SafeString, NUMERIC_RE, METHOD_CALL_RE, FN_CALL_RE, IS_NOT_RE, IS_RE, NOT_IN_RE, IN_RE, DIVISIBLE_BY_RE, FILTER_WITH_ARGS_RE, FILTER_COMPARISON_RE, TITLE_WORD_RE, STRIP_TAGS_RE, FORMAT_RE, LEADING_WS_RE, TRAILING_WS_RE, THOUSANDS_RE, LIVE_RE, LIVE_WS_RE, LIVE_SRC_RE, filterChainCache, pathParseCache, TEMPLATE_CACHE_MAX, TOKEN_RE, RAW_BLOCK_RE, VarRef, BUILTIN_FILTERS, _formTokenSessionId, Frond;
|
|
13795
|
+
var SafeString, JSON_UNSAFE_RE, JSON_UNSAFE_MAP, NUMERIC_RE, METHOD_CALL_RE, FN_CALL_RE, IS_NOT_RE, IS_RE, NOT_IN_RE, IN_RE, DIVISIBLE_BY_RE, FILTER_WITH_ARGS_RE, FILTER_COMPARISON_RE, TITLE_WORD_RE, STRIP_TAGS_RE, FORMAT_RE, LEADING_WS_RE, TRAILING_WS_RE, THOUSANDS_RE, LIVE_RE, LIVE_WS_RE, LIVE_SRC_RE, filterChainCache, pathParseCache, TEMPLATE_CACHE_MAX, TOKEN_RE, RAW_BLOCK_RE, VarRef, BUILTIN_FILTERS, _formTokenSessionId, Frond;
|
|
13776
13796
|
var init_engine = __esm({
|
|
13777
13797
|
"../frond/src/engine.ts"() {
|
|
13778
13798
|
"use strict";
|
|
@@ -13784,6 +13804,15 @@ var init_engine = __esm({
|
|
|
13784
13804
|
return this.value;
|
|
13785
13805
|
}
|
|
13786
13806
|
};
|
|
13807
|
+
JSON_UNSAFE_RE = /[<>&'\u2028\u2029]/g;
|
|
13808
|
+
JSON_UNSAFE_MAP = {
|
|
13809
|
+
"<": "\\u003c",
|
|
13810
|
+
">": "\\u003e",
|
|
13811
|
+
"&": "\\u0026",
|
|
13812
|
+
"'": "\\u0027",
|
|
13813
|
+
"\u2028": "\\u2028",
|
|
13814
|
+
"\u2029": "\\u2029"
|
|
13815
|
+
};
|
|
13787
13816
|
NUMERIC_RE = /^-?\d+(\.\d+)?$/;
|
|
13788
13817
|
METHOD_CALL_RE = /^(\w+)\s*\(([\s\S]*)?\)$/;
|
|
13789
13818
|
FN_CALL_RE = /^([\w.]+)\s*\(([\s\S]*)?\)$/;
|
|
@@ -13874,7 +13903,7 @@ var init_engine = __esm({
|
|
|
13874
13903
|
int: (v) => v ? parseInt(String(v), 10) || 0 : 0,
|
|
13875
13904
|
float: (v) => v ? parseFloat(String(v)) || 0 : 0,
|
|
13876
13905
|
string: (v) => String(v),
|
|
13877
|
-
json_encode: (v) =>
|
|
13906
|
+
json_encode: (v) => jsonSafe(v),
|
|
13878
13907
|
json_decode: (v) => typeof v === "string" ? JSON.parse(v) : v,
|
|
13879
13908
|
keys: (v) => typeof v === "object" && v !== null && !Array.isArray(v) ? Object.keys(v) : [],
|
|
13880
13909
|
values: (v) => typeof v === "object" && v !== null && !Array.isArray(v) ? Object.values(v) : [],
|
|
@@ -14019,8 +14048,12 @@ var init_engine = __esm({
|
|
|
14019
14048
|
form_token: (v) => _generateFormToken(v != null ? String(v) : ""),
|
|
14020
14049
|
formTokenValue: (v) => _generateFormTokenValue(v != null ? String(v) : ""),
|
|
14021
14050
|
form_token_value: (v) => _generateFormTokenValue(v != null ? String(v) : ""),
|
|
14022
|
-
|
|
14023
|
-
|
|
14051
|
+
// Same serializer as json_encode -- the three names are one behaviour. The
|
|
14052
|
+
// old indent argument is gone: PHP cannot honour an arbitrary indent
|
|
14053
|
+
// (JSON_PRETTY_PRINT is fixed at four spaces), so honouring it here alone
|
|
14054
|
+
// broke byte-parity for the one filter whose whole job is a wire format.
|
|
14055
|
+
tojson: (v) => jsonSafe(v),
|
|
14056
|
+
to_json: (v) => jsonSafe(v),
|
|
14024
14057
|
js_escape: (v) => new SafeString(String(v).replace(/\\/g, "\\\\").replace(/'/g, "\\'").replace(/"/g, '\\"').replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/\t/g, "\\t"))
|
|
14025
14058
|
};
|
|
14026
14059
|
_formTokenSessionId = "";
|
|
@@ -14760,7 +14793,7 @@ var init_engine = __esm({
|
|
|
14760
14793
|
value = typeof value === "object" && value !== null && !Array.isArray(value) ? Object.values(value) : [];
|
|
14761
14794
|
continue;
|
|
14762
14795
|
case "json_encode":
|
|
14763
|
-
value =
|
|
14796
|
+
value = jsonSafe(value);
|
|
14764
14797
|
continue;
|
|
14765
14798
|
case "dump":
|
|
14766
14799
|
value = renderDump(value);
|
|
@@ -10,6 +10,35 @@ var SafeString = class {
|
|
|
10
10
|
return this.value;
|
|
11
11
|
}
|
|
12
12
|
};
|
|
13
|
+
function jsonText(value) {
|
|
14
|
+
try {
|
|
15
|
+
const text = JSON.stringify(value);
|
|
16
|
+
return text === void 0 ? "null" : text;
|
|
17
|
+
} catch {
|
|
18
|
+
const seen = /* @__PURE__ */ new WeakSet();
|
|
19
|
+
const text = JSON.stringify(value, (_key, v) => {
|
|
20
|
+
if (typeof v === "bigint") return v.toString();
|
|
21
|
+
if (typeof v === "object" && v !== null) {
|
|
22
|
+
if (seen.has(v)) return null;
|
|
23
|
+
seen.add(v);
|
|
24
|
+
}
|
|
25
|
+
return v;
|
|
26
|
+
});
|
|
27
|
+
return text === void 0 ? "null" : text;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
var JSON_UNSAFE_RE = /[<>&'\u2028\u2029]/g;
|
|
31
|
+
var JSON_UNSAFE_MAP = {
|
|
32
|
+
"<": "\\u003c",
|
|
33
|
+
">": "\\u003e",
|
|
34
|
+
"&": "\\u0026",
|
|
35
|
+
"'": "\\u0027",
|
|
36
|
+
"\u2028": "\\u2028",
|
|
37
|
+
"\u2029": "\\u2029"
|
|
38
|
+
};
|
|
39
|
+
function jsonSafe(value) {
|
|
40
|
+
return new SafeString(jsonText(value).replace(JSON_UNSAFE_RE, (c) => JSON_UNSAFE_MAP[c]));
|
|
41
|
+
}
|
|
13
42
|
function inspectValue(value, seen = /* @__PURE__ */ new WeakSet(), depth = 0) {
|
|
14
43
|
if (value === null) return "null";
|
|
15
44
|
if (value === void 0) return "undefined";
|
|
@@ -1049,7 +1078,7 @@ var BUILTIN_FILTERS = {
|
|
|
1049
1078
|
int: (v) => v ? parseInt(String(v), 10) || 0 : 0,
|
|
1050
1079
|
float: (v) => v ? parseFloat(String(v)) || 0 : 0,
|
|
1051
1080
|
string: (v) => String(v),
|
|
1052
|
-
json_encode: (v) =>
|
|
1081
|
+
json_encode: (v) => jsonSafe(v),
|
|
1053
1082
|
json_decode: (v) => typeof v === "string" ? JSON.parse(v) : v,
|
|
1054
1083
|
keys: (v) => typeof v === "object" && v !== null && !Array.isArray(v) ? Object.keys(v) : [],
|
|
1055
1084
|
values: (v) => typeof v === "object" && v !== null && !Array.isArray(v) ? Object.values(v) : [],
|
|
@@ -1194,8 +1223,12 @@ var BUILTIN_FILTERS = {
|
|
|
1194
1223
|
form_token: (v) => _generateFormToken(v != null ? String(v) : ""),
|
|
1195
1224
|
formTokenValue: (v) => _generateFormTokenValue(v != null ? String(v) : ""),
|
|
1196
1225
|
form_token_value: (v) => _generateFormTokenValue(v != null ? String(v) : ""),
|
|
1197
|
-
|
|
1198
|
-
|
|
1226
|
+
// Same serializer as json_encode -- the three names are one behaviour. The
|
|
1227
|
+
// old indent argument is gone: PHP cannot honour an arbitrary indent
|
|
1228
|
+
// (JSON_PRETTY_PRINT is fixed at four spaces), so honouring it here alone
|
|
1229
|
+
// broke byte-parity for the one filter whose whole job is a wire format.
|
|
1230
|
+
tojson: (v) => jsonSafe(v),
|
|
1231
|
+
to_json: (v) => jsonSafe(v),
|
|
1199
1232
|
js_escape: (v) => new SafeString(String(v).replace(/\\/g, "\\\\").replace(/'/g, "\\'").replace(/"/g, '\\"').replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/\t/g, "\\t"))
|
|
1200
1233
|
};
|
|
1201
1234
|
function _b64url(data) {
|
|
@@ -1970,7 +2003,7 @@ var Frond = class _Frond {
|
|
|
1970
2003
|
value = typeof value === "object" && value !== null && !Array.isArray(value) ? Object.values(value) : [];
|
|
1971
2004
|
continue;
|
|
1972
2005
|
case "json_encode":
|
|
1973
|
-
value =
|
|
2006
|
+
value = jsonSafe(value);
|
|
1974
2007
|
continue;
|
|
1975
2008
|
case "dump":
|
|
1976
2009
|
value = renderDump(value);
|
|
@@ -34,6 +34,69 @@ class SafeString {
|
|
|
34
34
|
toString() { return this.value; }
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
/**
|
|
38
|
+
* Serialize a value to compact JSON text that is always valid JSON.
|
|
39
|
+
*
|
|
40
|
+
* Never throws and never returns an empty string. JSON.stringify already maps a
|
|
41
|
+
* non-finite number to null, but it returns the VALUE `undefined` for undefined,
|
|
42
|
+
* a function, or a symbol, and it throws on a BigInt or a circular structure --
|
|
43
|
+
* all four of which would otherwise reach the page as nothing or as a crash.
|
|
44
|
+
*/
|
|
45
|
+
function jsonText(value: unknown): string {
|
|
46
|
+
try {
|
|
47
|
+
const text = JSON.stringify(value);
|
|
48
|
+
return text === undefined ? "null" : text;
|
|
49
|
+
} catch {
|
|
50
|
+
// Only reached when the happy path threw, so a well-formed payload never
|
|
51
|
+
// pays for the walk.
|
|
52
|
+
const seen = new WeakSet<object>();
|
|
53
|
+
const text = JSON.stringify(value, (_key, v) => {
|
|
54
|
+
if (typeof v === "bigint") return v.toString();
|
|
55
|
+
if (typeof v === "object" && v !== null) {
|
|
56
|
+
if (seen.has(v)) return null;
|
|
57
|
+
seen.add(v);
|
|
58
|
+
}
|
|
59
|
+
return v;
|
|
60
|
+
});
|
|
61
|
+
return text === undefined ? "null" : text;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const JSON_UNSAFE_RE = /[<>&'\u2028\u2029]/g;
|
|
66
|
+
const JSON_UNSAFE_MAP: Record<string, string> = {
|
|
67
|
+
"<": "\\u003c", ">": "\\u003e", "&": "\\u0026", "'": "\\u0027",
|
|
68
|
+
"\u2028": "\\u2028", "\u2029": "\\u2029",
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Serialize to JSON that is valid JSON, valid JavaScript, and safe in HTML.
|
|
73
|
+
*
|
|
74
|
+
* THE cross-framework contract for json_encode / to_json / tojson. Keep the four
|
|
75
|
+
* implementations byte-identical; frond_expression_corpus.txt locks it.
|
|
76
|
+
*
|
|
77
|
+
* Three things this must never do, each of which was a real bug in one of the
|
|
78
|
+
* four engines:
|
|
79
|
+
*
|
|
80
|
+
* 1. Never emit a non-finite literal. Python wrote a bare `Infinity`, PHP
|
|
81
|
+
* returned false, Ruby fell back to inspect output -- none of them parse.
|
|
82
|
+
* Node was the one that already got this right. Reported as tina4-php#184 by
|
|
83
|
+
* justin-k-bruce.
|
|
84
|
+
* 2. Never emit nothing, and never emit something that still parses and means
|
|
85
|
+
* something else. "var ROWS = ;" is at least a loud SyntaxError.
|
|
86
|
+
* 3. Never HTML-escape it. Entity-encoding JSON produces {"a":1}, a
|
|
87
|
+
* SyntaxError inside <script>, which is the filter's whole point. Escape only
|
|
88
|
+
* the dangerous characters, as JSON \uXXXX escapes: the result stays valid
|
|
89
|
+
* JSON AND valid JavaScript, </script> cannot terminate the block, and it is
|
|
90
|
+
* safe inside a single-quoted attribute. This is what Jinja2's tojson does,
|
|
91
|
+
* and it is why the result is a SafeString.
|
|
92
|
+
*
|
|
93
|
+
* U+2028 and U+2029 join that escape set. Both are legal inside a JSON string and
|
|
94
|
+
* both were illegal inside a JavaScript string literal before ES2019.
|
|
95
|
+
*/
|
|
96
|
+
function jsonSafe(value: unknown): SafeString {
|
|
97
|
+
return new SafeString(jsonText(value).replace(JSON_UNSAFE_RE, (c) => JSON_UNSAFE_MAP[c]));
|
|
98
|
+
}
|
|
99
|
+
|
|
37
100
|
/**
|
|
38
101
|
* Produce a human-readable, debugger-friendly inspection of any value.
|
|
39
102
|
*
|
|
@@ -1307,7 +1370,7 @@ const BUILTIN_FILTERS: Record<string, FilterFn> = {
|
|
|
1307
1370
|
int: (v) => v ? parseInt(String(v), 10) || 0 : 0,
|
|
1308
1371
|
float: (v) => v ? parseFloat(String(v)) || 0.0 : 0.0,
|
|
1309
1372
|
string: (v) => String(v),
|
|
1310
|
-
json_encode: (v) =>
|
|
1373
|
+
json_encode: (v) => jsonSafe(v),
|
|
1311
1374
|
json_decode: (v) => typeof v === "string" ? JSON.parse(v) : v,
|
|
1312
1375
|
keys: (v) => (typeof v === "object" && v !== null && !Array.isArray(v)) ? Object.keys(v) : [],
|
|
1313
1376
|
values: (v) => (typeof v === "object" && v !== null && !Array.isArray(v)) ? Object.values(v) : [],
|
|
@@ -1431,8 +1494,12 @@ const BUILTIN_FILTERS: Record<string, FilterFn> = {
|
|
|
1431
1494
|
form_token: (v?: unknown) => _generateFormToken(v != null ? String(v) : ""),
|
|
1432
1495
|
formTokenValue: (v?: unknown) => _generateFormTokenValue(v != null ? String(v) : ""),
|
|
1433
1496
|
form_token_value: (v?: unknown) => _generateFormTokenValue(v != null ? String(v) : ""),
|
|
1434
|
-
|
|
1435
|
-
|
|
1497
|
+
// Same serializer as json_encode -- the three names are one behaviour. The
|
|
1498
|
+
// old indent argument is gone: PHP cannot honour an arbitrary indent
|
|
1499
|
+
// (JSON_PRETTY_PRINT is fixed at four spaces), so honouring it here alone
|
|
1500
|
+
// broke byte-parity for the one filter whose whole job is a wire format.
|
|
1501
|
+
tojson: (v) => jsonSafe(v),
|
|
1502
|
+
to_json: (v) => jsonSafe(v),
|
|
1436
1503
|
js_escape: (v) => new SafeString(String(v).replace(/\\/g, "\\\\").replace(/'/g, "\\'").replace(/"/g, '\\"').replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/\t/g, "\\t")),
|
|
1437
1504
|
};
|
|
1438
1505
|
|
|
@@ -2346,7 +2413,7 @@ export class Frond {
|
|
|
2346
2413
|
case "last": value = Array.isArray(value) ? value[value.length - 1] ?? null : null; continue;
|
|
2347
2414
|
case "keys": value = (typeof value === "object" && value !== null && !Array.isArray(value)) ? Object.keys(value) : []; continue;
|
|
2348
2415
|
case "values": value = (typeof value === "object" && value !== null && !Array.isArray(value)) ? Object.values(value) : []; continue;
|
|
2349
|
-
case "json_encode": value =
|
|
2416
|
+
case "json_encode": value = jsonSafe(value); continue;
|
|
2350
2417
|
case "dump":
|
|
2351
2418
|
// Delegates to renderDump(), which is gated on TINA4_DEBUG.
|
|
2352
2419
|
// In production this emits an empty SafeString (no leaked state).
|
|
@@ -3421,6 +3421,26 @@ __export(engine_exports, {
|
|
|
3421
3421
|
import { createHash as createHash2, createHmac as createHmac2, randomBytes as randomBytes2 } from "node:crypto";
|
|
3422
3422
|
import { readFileSync as readFileSync4, existsSync as existsSync6, statSync as statSync4 } from "node:fs";
|
|
3423
3423
|
import { join as join5, resolve as resolve2 } from "node:path";
|
|
3424
|
+
function jsonText(value) {
|
|
3425
|
+
try {
|
|
3426
|
+
const text = JSON.stringify(value);
|
|
3427
|
+
return text === void 0 ? "null" : text;
|
|
3428
|
+
} catch {
|
|
3429
|
+
const seen = /* @__PURE__ */ new WeakSet();
|
|
3430
|
+
const text = JSON.stringify(value, (_key, v) => {
|
|
3431
|
+
if (typeof v === "bigint") return v.toString();
|
|
3432
|
+
if (typeof v === "object" && v !== null) {
|
|
3433
|
+
if (seen.has(v)) return null;
|
|
3434
|
+
seen.add(v);
|
|
3435
|
+
}
|
|
3436
|
+
return v;
|
|
3437
|
+
});
|
|
3438
|
+
return text === void 0 ? "null" : text;
|
|
3439
|
+
}
|
|
3440
|
+
}
|
|
3441
|
+
function jsonSafe(value) {
|
|
3442
|
+
return new SafeString(jsonText(value).replace(JSON_UNSAFE_RE, (c) => JSON_UNSAFE_MAP[c]));
|
|
3443
|
+
}
|
|
3424
3444
|
function inspectValue(value, seen = /* @__PURE__ */ new WeakSet(), depth = 0) {
|
|
3425
3445
|
if (value === null) return "null";
|
|
3426
3446
|
if (value === void 0) return "undefined";
|
|
@@ -4408,7 +4428,7 @@ function _generateFormToken(descriptor = "") {
|
|
|
4408
4428
|
function _generateFormTokenValue(descriptor = "") {
|
|
4409
4429
|
return new SafeString(_buildFormTokenJwt(descriptor));
|
|
4410
4430
|
}
|
|
4411
|
-
var SafeString, NUMERIC_RE, METHOD_CALL_RE, FN_CALL_RE, IS_NOT_RE, IS_RE, NOT_IN_RE, IN_RE, DIVISIBLE_BY_RE, FILTER_WITH_ARGS_RE, FILTER_COMPARISON_RE, TITLE_WORD_RE, STRIP_TAGS_RE, FORMAT_RE, LEADING_WS_RE, TRAILING_WS_RE, THOUSANDS_RE, LIVE_RE, LIVE_WS_RE, LIVE_SRC_RE, filterChainCache, pathParseCache, TEMPLATE_CACHE_MAX, TOKEN_RE, RAW_BLOCK_RE, VarRef, BUILTIN_FILTERS, _formTokenSessionId, Frond;
|
|
4431
|
+
var SafeString, JSON_UNSAFE_RE, JSON_UNSAFE_MAP, NUMERIC_RE, METHOD_CALL_RE, FN_CALL_RE, IS_NOT_RE, IS_RE, NOT_IN_RE, IN_RE, DIVISIBLE_BY_RE, FILTER_WITH_ARGS_RE, FILTER_COMPARISON_RE, TITLE_WORD_RE, STRIP_TAGS_RE, FORMAT_RE, LEADING_WS_RE, TRAILING_WS_RE, THOUSANDS_RE, LIVE_RE, LIVE_WS_RE, LIVE_SRC_RE, filterChainCache, pathParseCache, TEMPLATE_CACHE_MAX, TOKEN_RE, RAW_BLOCK_RE, VarRef, BUILTIN_FILTERS, _formTokenSessionId, Frond;
|
|
4412
4432
|
var init_engine = __esm({
|
|
4413
4433
|
"../frond/src/engine.ts"() {
|
|
4414
4434
|
"use strict";
|
|
@@ -4420,6 +4440,15 @@ var init_engine = __esm({
|
|
|
4420
4440
|
return this.value;
|
|
4421
4441
|
}
|
|
4422
4442
|
};
|
|
4443
|
+
JSON_UNSAFE_RE = /[<>&'\u2028\u2029]/g;
|
|
4444
|
+
JSON_UNSAFE_MAP = {
|
|
4445
|
+
"<": "\\u003c",
|
|
4446
|
+
">": "\\u003e",
|
|
4447
|
+
"&": "\\u0026",
|
|
4448
|
+
"'": "\\u0027",
|
|
4449
|
+
"\u2028": "\\u2028",
|
|
4450
|
+
"\u2029": "\\u2029"
|
|
4451
|
+
};
|
|
4423
4452
|
NUMERIC_RE = /^-?\d+(\.\d+)?$/;
|
|
4424
4453
|
METHOD_CALL_RE = /^(\w+)\s*\(([\s\S]*)?\)$/;
|
|
4425
4454
|
FN_CALL_RE = /^([\w.]+)\s*\(([\s\S]*)?\)$/;
|
|
@@ -4510,7 +4539,7 @@ var init_engine = __esm({
|
|
|
4510
4539
|
int: (v) => v ? parseInt(String(v), 10) || 0 : 0,
|
|
4511
4540
|
float: (v) => v ? parseFloat(String(v)) || 0 : 0,
|
|
4512
4541
|
string: (v) => String(v),
|
|
4513
|
-
json_encode: (v) =>
|
|
4542
|
+
json_encode: (v) => jsonSafe(v),
|
|
4514
4543
|
json_decode: (v) => typeof v === "string" ? JSON.parse(v) : v,
|
|
4515
4544
|
keys: (v) => typeof v === "object" && v !== null && !Array.isArray(v) ? Object.keys(v) : [],
|
|
4516
4545
|
values: (v) => typeof v === "object" && v !== null && !Array.isArray(v) ? Object.values(v) : [],
|
|
@@ -4655,8 +4684,12 @@ var init_engine = __esm({
|
|
|
4655
4684
|
form_token: (v) => _generateFormToken(v != null ? String(v) : ""),
|
|
4656
4685
|
formTokenValue: (v) => _generateFormTokenValue(v != null ? String(v) : ""),
|
|
4657
4686
|
form_token_value: (v) => _generateFormTokenValue(v != null ? String(v) : ""),
|
|
4658
|
-
|
|
4659
|
-
|
|
4687
|
+
// Same serializer as json_encode -- the three names are one behaviour. The
|
|
4688
|
+
// old indent argument is gone: PHP cannot honour an arbitrary indent
|
|
4689
|
+
// (JSON_PRETTY_PRINT is fixed at four spaces), so honouring it here alone
|
|
4690
|
+
// broke byte-parity for the one filter whose whole job is a wire format.
|
|
4691
|
+
tojson: (v) => jsonSafe(v),
|
|
4692
|
+
to_json: (v) => jsonSafe(v),
|
|
4660
4693
|
js_escape: (v) => new SafeString(String(v).replace(/\\/g, "\\\\").replace(/'/g, "\\'").replace(/"/g, '\\"').replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/\t/g, "\\t"))
|
|
4661
4694
|
};
|
|
4662
4695
|
_formTokenSessionId = "";
|
|
@@ -5396,7 +5429,7 @@ var init_engine = __esm({
|
|
|
5396
5429
|
value = typeof value === "object" && value !== null && !Array.isArray(value) ? Object.values(value) : [];
|
|
5397
5430
|
continue;
|
|
5398
5431
|
case "json_encode":
|
|
5399
|
-
value =
|
|
5432
|
+
value = jsonSafe(value);
|
|
5400
5433
|
continue;
|
|
5401
5434
|
case "dump":
|
|
5402
5435
|
value = renderDump(value);
|