tina4-nodejs 3.13.86 → 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 +185 -16
- package/packages/core/dist/index.js +184 -15
- package/packages/core/src/metrics.ts +11 -2
- package/packages/frond/dist/index.js +141 -9
- package/packages/frond/src/engine.ts +230 -10
- package/packages/orm/dist/index.js +184 -15
- package/packages/orm/src/adapters/firebird.ts +28 -2
- package/packages/orm/src/adapters/postgres.ts +11 -0
- package/packages/orm/src/database.ts +15 -3
|
@@ -1674,18 +1674,21 @@ var init_postgres = __esm({
|
|
|
1674
1674
|
}
|
|
1675
1675
|
async startTransactionAsync() {
|
|
1676
1676
|
await this.executeAsync("BEGIN");
|
|
1677
|
+
this._inTransaction = true;
|
|
1677
1678
|
}
|
|
1678
1679
|
commit() {
|
|
1679
1680
|
throw new Error("Use commitAsync() for PostgreSQL.");
|
|
1680
1681
|
}
|
|
1681
1682
|
async commitAsync() {
|
|
1682
1683
|
await this.executeAsync("COMMIT");
|
|
1684
|
+
this._inTransaction = false;
|
|
1683
1685
|
}
|
|
1684
1686
|
rollback() {
|
|
1685
1687
|
throw new Error("Use rollbackAsync() for PostgreSQL.");
|
|
1686
1688
|
}
|
|
1687
1689
|
async rollbackAsync() {
|
|
1688
1690
|
await this.executeAsync("ROLLBACK");
|
|
1691
|
+
this._inTransaction = false;
|
|
1689
1692
|
}
|
|
1690
1693
|
tables() {
|
|
1691
1694
|
throw new Error("Use tablesAsync() for PostgreSQL.");
|
|
@@ -2704,10 +2707,35 @@ var init_firebird = __esm({
|
|
|
2704
2707
|
translated = SQLTranslator.ilikeToLike(translated);
|
|
2705
2708
|
return translated;
|
|
2706
2709
|
}
|
|
2710
|
+
/**
|
|
2711
|
+
* The handle every statement runs on. While an explicit transaction is open
|
|
2712
|
+
* (startTransactionAsync set `this.transaction`), statements MUST run on that
|
|
2713
|
+
* transaction object so they are undone by rollbackAsync() / persisted by
|
|
2714
|
+
* commitAsync() — node-firebird's transaction exposes the same
|
|
2715
|
+
* query()/execute() as the connection. With no transaction open we run on
|
|
2716
|
+
* `this.db`, whose per-statement work auto-commits on the connection.
|
|
2717
|
+
*
|
|
2718
|
+
* This matches the Python master's contract (tina4_python/database/firebird.py):
|
|
2719
|
+
* there, ALL statements run on the single connection and start_transaction()
|
|
2720
|
+
* merely suppresses the per-statement autocommit in execute() so the batch
|
|
2721
|
+
* stays open until commit()/rollback(). node-firebird has no such suppression
|
|
2722
|
+
* hook — its `db.query/execute` always auto-commit — so the equivalent is to
|
|
2723
|
+
* route statements through the transaction object instead. Same observable
|
|
2724
|
+
* behaviour: an open transaction is atomic and rolls back cleanly.
|
|
2725
|
+
*
|
|
2726
|
+
* Previously every statement ran on `this.db` unconditionally, so the
|
|
2727
|
+
* transaction created by startTransactionAsync() never saw a single statement
|
|
2728
|
+
* — rollbackAsync() rolled back an EMPTY transaction and the already
|
|
2729
|
+
* auto-committed write survived (silent no-op). Twin of the PHP pdo_firebird
|
|
2730
|
+
* bug fixed in 3.13.86.
|
|
2731
|
+
*/
|
|
2732
|
+
statementHandle() {
|
|
2733
|
+
return this.transaction ?? this.db;
|
|
2734
|
+
}
|
|
2707
2735
|
queryPromise(sql, params) {
|
|
2708
2736
|
return new Promise((resolve21, reject) => {
|
|
2709
2737
|
const translated = this.translateSql(sql);
|
|
2710
|
-
this.
|
|
2738
|
+
this.statementHandle().query(translated, params ?? [], (err, result) => {
|
|
2711
2739
|
if (err) reject(err);
|
|
2712
2740
|
else resolve21(result ?? []);
|
|
2713
2741
|
});
|
|
@@ -2716,7 +2744,7 @@ var init_firebird = __esm({
|
|
|
2716
2744
|
executePromise(sql, params) {
|
|
2717
2745
|
return new Promise((resolve21, reject) => {
|
|
2718
2746
|
const translated = this.translateSql(sql);
|
|
2719
|
-
this.
|
|
2747
|
+
this.statementHandle().execute(translated, params ?? [], (err) => {
|
|
2720
2748
|
if (err) reject(err);
|
|
2721
2749
|
else resolve21();
|
|
2722
2750
|
});
|
|
@@ -4690,14 +4718,15 @@ var init_database = __esm({
|
|
|
4690
4718
|
async executeMany(sql, paramSets = []) {
|
|
4691
4719
|
const adapter = this.getNextAdapter();
|
|
4692
4720
|
const results = [];
|
|
4693
|
-
|
|
4721
|
+
const owns = !this.inExplicitTransaction();
|
|
4722
|
+
if (owns) await adapterStartTransaction(adapter);
|
|
4694
4723
|
try {
|
|
4695
4724
|
for (const params of paramSets) {
|
|
4696
4725
|
results.push(await adapterExecute(adapter, sql, params));
|
|
4697
4726
|
}
|
|
4698
|
-
await adapterCommit(adapter);
|
|
4727
|
+
if (owns) await adapterCommit(adapter);
|
|
4699
4728
|
} catch (e) {
|
|
4700
|
-
await adapterRollback(adapter);
|
|
4729
|
+
if (owns) await adapterRollback(adapter);
|
|
4701
4730
|
throw e;
|
|
4702
4731
|
}
|
|
4703
4732
|
return results;
|
|
@@ -12750,11 +12779,32 @@ var init_request = __esm({
|
|
|
12750
12779
|
var engine_exports = {};
|
|
12751
12780
|
__export(engine_exports, {
|
|
12752
12781
|
Frond: () => Frond,
|
|
12782
|
+
TEMPLATE_CACHE_MAX: () => TEMPLATE_CACHE_MAX,
|
|
12753
12783
|
setFormTokenSessionId: () => setFormTokenSessionId
|
|
12754
12784
|
});
|
|
12755
12785
|
import { createHash as createHash2, createHmac as createHmac3, randomBytes as randomBytes4 } from "node:crypto";
|
|
12756
12786
|
import { readFileSync as readFileSync6, existsSync as existsSync8, statSync as statSync6 } from "node:fs";
|
|
12757
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
|
+
}
|
|
12758
12808
|
function inspectValue(value, seen = /* @__PURE__ */ new WeakSet(), depth = 0) {
|
|
12759
12809
|
if (value === null) return "null";
|
|
12760
12810
|
if (value === void 0) return "undefined";
|
|
@@ -12825,6 +12875,14 @@ function renderDump(value) {
|
|
|
12825
12875
|
function liveAttr(value) {
|
|
12826
12876
|
return String(value).replace(/&/g, "&").replace(/"/g, """).replace(/</g, "<").replace(/>/g, ">");
|
|
12827
12877
|
}
|
|
12878
|
+
function capCache(cache, maxEntries) {
|
|
12879
|
+
if (cache.size < maxEntries) return;
|
|
12880
|
+
let drop = Math.floor(maxEntries / 2);
|
|
12881
|
+
for (const key of cache.keys()) {
|
|
12882
|
+
cache.delete(key);
|
|
12883
|
+
if (--drop <= 0) break;
|
|
12884
|
+
}
|
|
12885
|
+
}
|
|
12828
12886
|
function tokenize(source) {
|
|
12829
12887
|
const rawBlocks = [];
|
|
12830
12888
|
source = source.replace(RAW_BLOCK_RE, (_match, content) => {
|
|
@@ -13176,6 +13234,9 @@ function evalExpr(expr, context) {
|
|
|
13176
13234
|
}).join("");
|
|
13177
13235
|
}
|
|
13178
13236
|
}
|
|
13237
|
+
if (expr.startsWith("not ")) {
|
|
13238
|
+
return evalComparison(expr, context);
|
|
13239
|
+
}
|
|
13179
13240
|
for (const op of [" not in ", " in ", " is not ", " is ", "!=", "==", ">=", "<=", ">", "<", " and ", " or ", " not "]) {
|
|
13180
13241
|
if (findOutsideQuotes(expr, op) >= 0) {
|
|
13181
13242
|
return evalComparison(expr, context);
|
|
@@ -13731,7 +13792,7 @@ function _generateFormToken(descriptor = "") {
|
|
|
13731
13792
|
function _generateFormTokenValue(descriptor = "") {
|
|
13732
13793
|
return new SafeString(_buildFormTokenJwt(descriptor));
|
|
13733
13794
|
}
|
|
13734
|
-
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, 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;
|
|
13735
13796
|
var init_engine = __esm({
|
|
13736
13797
|
"../frond/src/engine.ts"() {
|
|
13737
13798
|
"use strict";
|
|
@@ -13743,6 +13804,15 @@ var init_engine = __esm({
|
|
|
13743
13804
|
return this.value;
|
|
13744
13805
|
}
|
|
13745
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
|
+
};
|
|
13746
13816
|
NUMERIC_RE = /^-?\d+(\.\d+)?$/;
|
|
13747
13817
|
METHOD_CALL_RE = /^(\w+)\s*\(([\s\S]*)?\)$/;
|
|
13748
13818
|
FN_CALL_RE = /^([\w.]+)\s*\(([\s\S]*)?\)$/;
|
|
@@ -13764,6 +13834,7 @@ var init_engine = __esm({
|
|
|
13764
13834
|
LIVE_SRC_RE = /src\s+["']([^"']+)["']/;
|
|
13765
13835
|
filterChainCache = /* @__PURE__ */ new Map();
|
|
13766
13836
|
pathParseCache = /* @__PURE__ */ new Map();
|
|
13837
|
+
TEMPLATE_CACHE_MAX = 256;
|
|
13767
13838
|
TOKEN_RE = /(\{%-?\s*[\s\S]*?\s*-?%\})|(\{\{-?\s*[\s\S]*?\s*-?\}\})|(\{#[\s\S]*?#\})/g;
|
|
13768
13839
|
RAW_BLOCK_RE = /\{%-?\s*raw\s*-?%\}([\s\S]*?)\{%-?\s*endraw\s*-?%\}/g;
|
|
13769
13840
|
VarRef = class {
|
|
@@ -13832,7 +13903,7 @@ var init_engine = __esm({
|
|
|
13832
13903
|
int: (v) => v ? parseInt(String(v), 10) || 0 : 0,
|
|
13833
13904
|
float: (v) => v ? parseFloat(String(v)) || 0 : 0,
|
|
13834
13905
|
string: (v) => String(v),
|
|
13835
|
-
json_encode: (v) =>
|
|
13906
|
+
json_encode: (v) => jsonSafe(v),
|
|
13836
13907
|
json_decode: (v) => typeof v === "string" ? JSON.parse(v) : v,
|
|
13837
13908
|
keys: (v) => typeof v === "object" && v !== null && !Array.isArray(v) ? Object.keys(v) : [],
|
|
13838
13909
|
values: (v) => typeof v === "object" && v !== null && !Array.isArray(v) ? Object.values(v) : [],
|
|
@@ -13977,8 +14048,12 @@ var init_engine = __esm({
|
|
|
13977
14048
|
form_token: (v) => _generateFormToken(v != null ? String(v) : ""),
|
|
13978
14049
|
formTokenValue: (v) => _generateFormTokenValue(v != null ? String(v) : ""),
|
|
13979
14050
|
form_token_value: (v) => _generateFormTokenValue(v != null ? String(v) : ""),
|
|
13980
|
-
|
|
13981
|
-
|
|
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),
|
|
13982
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"))
|
|
13983
14058
|
};
|
|
13984
14059
|
_formTokenSessionId = "";
|
|
@@ -14169,6 +14244,7 @@ var init_engine = __esm({
|
|
|
14169
14244
|
const source = readFileSync6(filePath, "utf-8");
|
|
14170
14245
|
const mtime = statSync6(filePath).mtimeMs;
|
|
14171
14246
|
const tokens = tokenize(source);
|
|
14247
|
+
capCache(this.compiled, TEMPLATE_CACHE_MAX);
|
|
14172
14248
|
this.compiled.set(template, { tokens, mtime, cachedAt: Date.now() });
|
|
14173
14249
|
return this.executeWithSource(source, tokens, context);
|
|
14174
14250
|
}
|
|
@@ -14183,6 +14259,7 @@ var init_engine = __esm({
|
|
|
14183
14259
|
}
|
|
14184
14260
|
}
|
|
14185
14261
|
const tokens = tokenize(source);
|
|
14262
|
+
capCache(this.compiledStrings, TEMPLATE_CACHE_MAX);
|
|
14186
14263
|
this.compiledStrings.set(key, { tokens, cachedAt: Date.now() });
|
|
14187
14264
|
return this.executeCached(tokens, context);
|
|
14188
14265
|
}
|
|
@@ -14398,6 +14475,9 @@ var init_engine = __esm({
|
|
|
14398
14475
|
} else if (tag === "macro") {
|
|
14399
14476
|
const skip = this.handleMacro(tokens, i, context);
|
|
14400
14477
|
i = skip;
|
|
14478
|
+
} else if (tag === "import") {
|
|
14479
|
+
this.handleImportAs(content, context);
|
|
14480
|
+
i++;
|
|
14401
14481
|
} else if (tag === "from") {
|
|
14402
14482
|
this.handleFromImport(content, context);
|
|
14403
14483
|
i++;
|
|
@@ -14713,7 +14793,7 @@ var init_engine = __esm({
|
|
|
14713
14793
|
value = typeof value === "object" && value !== null && !Array.isArray(value) ? Object.values(value) : [];
|
|
14714
14794
|
continue;
|
|
14715
14795
|
case "json_encode":
|
|
14716
|
-
value =
|
|
14796
|
+
value = jsonSafe(value);
|
|
14717
14797
|
continue;
|
|
14718
14798
|
case "dump":
|
|
14719
14799
|
value = renderDump(value);
|
|
@@ -14964,7 +15044,7 @@ var init_engine = __esm({
|
|
|
14964
15044
|
return i2;
|
|
14965
15045
|
}
|
|
14966
15046
|
const macroName = m[1];
|
|
14967
|
-
const
|
|
15047
|
+
const params = _Frond.parseMacroParams(m[2]);
|
|
14968
15048
|
const bodyTokens = [];
|
|
14969
15049
|
let i = start2 + 1;
|
|
14970
15050
|
while (i < tokens.length) {
|
|
@@ -14979,13 +15059,94 @@ var init_engine = __esm({
|
|
|
14979
15059
|
const capturedContext = { ...context };
|
|
14980
15060
|
context[macroName] = (...args) => {
|
|
14981
15061
|
const macroCtx = { ...capturedContext };
|
|
14982
|
-
for (let pi = 0; pi <
|
|
14983
|
-
|
|
15062
|
+
for (let pi = 0; pi < params.length; pi++) {
|
|
15063
|
+
const [pname, pdefault] = params[pi];
|
|
15064
|
+
macroCtx[pname] = pi < args.length ? args[pi] : pdefault;
|
|
14984
15065
|
}
|
|
14985
15066
|
return new SafeString(engine.renderTokens([...bodyTokens], macroCtx));
|
|
14986
15067
|
};
|
|
14987
15068
|
return i;
|
|
14988
15069
|
}
|
|
15070
|
+
/**
|
|
15071
|
+
* Parse a macro parameter list into [name, default] pairs.
|
|
15072
|
+
*
|
|
15073
|
+
* Handles: name, name="default", name='default'. Splitting on "," alone left a
|
|
15074
|
+
* defaulted parameter literally NAMED `greeting='Hello'`, so the body's
|
|
15075
|
+
* {{ greeting }} matched nothing (rendered empty) AND the caller's positional
|
|
15076
|
+
* argument was stored under that junk key and lost. Mirrors the Python master's
|
|
15077
|
+
* _parse_macro_params. The default is null when none is declared.
|
|
15078
|
+
*/
|
|
15079
|
+
static parseMacroParams(rawParams) {
|
|
15080
|
+
return rawParams.split(",").map((p) => p.trim()).filter(Boolean).map((p) => {
|
|
15081
|
+
const eq = p.indexOf("=");
|
|
15082
|
+
if (eq === -1) return [p, null];
|
|
15083
|
+
const name = p.slice(0, eq).trim();
|
|
15084
|
+
let dflt = p.slice(eq + 1).trim();
|
|
15085
|
+
if (dflt.length >= 2 && (dflt.startsWith('"') && dflt.endsWith('"') || dflt.startsWith("'") && dflt.endsWith("'"))) {
|
|
15086
|
+
dflt = dflt.slice(1, -1);
|
|
15087
|
+
}
|
|
15088
|
+
return [name, dflt];
|
|
15089
|
+
});
|
|
15090
|
+
}
|
|
15091
|
+
/**
|
|
15092
|
+
* {% import "file" as alias %} -- load EVERY macro in a file under one namespace.
|
|
15093
|
+
*
|
|
15094
|
+
* The alias is bound as a plain object of macro functions, so {{ alias.greet(x) }}
|
|
15095
|
+
* resolves through the engine's existing dotted-call path and each macro keeps the
|
|
15096
|
+
* same argument binding, default handling and SafeString output as any other macro.
|
|
15097
|
+
* A namespace object (not a class) is deliberate: a function stored as a class
|
|
15098
|
+
* attribute binds as a method and would inject the namespace as the first argument,
|
|
15099
|
+
* which is exactly the argument-shift bug the Python master carried (fixed there
|
|
15100
|
+
* with types.SimpleNamespace). Both import forms must render identically.
|
|
15101
|
+
*/
|
|
15102
|
+
handleImportAs(content, context) {
|
|
15103
|
+
const m = content.match(/^import\s+["'](.+?)["']\s+as\s+(\w+)/);
|
|
15104
|
+
if (!m) return;
|
|
15105
|
+
const filename = m[1];
|
|
15106
|
+
const alias = m[2];
|
|
15107
|
+
const namespace = {};
|
|
15108
|
+
const source = this.load(filename);
|
|
15109
|
+
const tokens = tokenize(source);
|
|
15110
|
+
let i = 0;
|
|
15111
|
+
while (i < tokens.length) {
|
|
15112
|
+
const [ttype, raw] = tokens[i];
|
|
15113
|
+
if (ttype === "BLOCK") {
|
|
15114
|
+
const [tagContent] = stripTag(raw);
|
|
15115
|
+
if ((tagContent.split(/\s+/)[0] || "") === "macro") {
|
|
15116
|
+
const macroM = tagContent.match(/^macro\s+(\w+)\s*\(([^)]*)\)/);
|
|
15117
|
+
if (macroM) {
|
|
15118
|
+
const macroName = macroM[1];
|
|
15119
|
+
const params = _Frond.parseMacroParams(macroM[2]);
|
|
15120
|
+
const bodyTokens = [];
|
|
15121
|
+
i++;
|
|
15122
|
+
while (i < tokens.length) {
|
|
15123
|
+
if (tokens[i][0] === "BLOCK" && tokens[i][1].includes("endmacro")) {
|
|
15124
|
+
i++;
|
|
15125
|
+
break;
|
|
15126
|
+
}
|
|
15127
|
+
bodyTokens.push(tokens[i]);
|
|
15128
|
+
i++;
|
|
15129
|
+
}
|
|
15130
|
+
const capturedBody = [...bodyTokens];
|
|
15131
|
+
const capturedParams = [...params];
|
|
15132
|
+
const capturedCtx = { ...context };
|
|
15133
|
+
const engine = this;
|
|
15134
|
+
namespace[macroName] = (...args) => {
|
|
15135
|
+
const macroCtx = { ...capturedCtx };
|
|
15136
|
+
for (let pi = 0; pi < capturedParams.length; pi++) {
|
|
15137
|
+
const [pname, pdefault] = capturedParams[pi];
|
|
15138
|
+
macroCtx[pname] = pi < args.length ? args[pi] : pdefault;
|
|
15139
|
+
}
|
|
15140
|
+
return new SafeString(engine.renderTokens([...capturedBody], macroCtx));
|
|
15141
|
+
};
|
|
15142
|
+
continue;
|
|
15143
|
+
}
|
|
15144
|
+
}
|
|
15145
|
+
}
|
|
15146
|
+
i++;
|
|
15147
|
+
}
|
|
15148
|
+
context[alias] = namespace;
|
|
15149
|
+
}
|
|
14989
15150
|
handleFromImport(content, context) {
|
|
14990
15151
|
const m = content.match(/^from\s+["'](.+?)["']\s+import\s+(.+)/);
|
|
14991
15152
|
if (!m) return;
|
|
@@ -15003,7 +15164,7 @@ var init_engine = __esm({
|
|
|
15003
15164
|
const macroM = tagContent.match(/^macro\s+(\w+)\s*\(([^)]*)\)/);
|
|
15004
15165
|
if (macroM && names.includes(macroM[1])) {
|
|
15005
15166
|
const macroName = macroM[1];
|
|
15006
|
-
const paramNames = macroM[2]
|
|
15167
|
+
const paramNames = _Frond.parseMacroParams(macroM[2]);
|
|
15007
15168
|
const bodyTokens = [];
|
|
15008
15169
|
i++;
|
|
15009
15170
|
while (i < tokens.length) {
|
|
@@ -15021,7 +15182,8 @@ var init_engine = __esm({
|
|
|
15021
15182
|
context[macroName] = (...args) => {
|
|
15022
15183
|
const macroCtx = { ...capturedCtx };
|
|
15023
15184
|
for (let pi = 0; pi < capturedParams.length; pi++) {
|
|
15024
|
-
|
|
15185
|
+
const [pname, pdefault] = capturedParams[pi];
|
|
15186
|
+
macroCtx[pname] = pi < args.length ? args[pi] : pdefault;
|
|
15025
15187
|
}
|
|
15026
15188
|
return new SafeString(engine.renderTokens([...capturedBody], macroCtx));
|
|
15027
15189
|
};
|
|
@@ -18128,7 +18290,14 @@ function fullAnalysis(root = "src") {
|
|
|
18128
18290
|
total_functions: allFunctions.length,
|
|
18129
18291
|
avg_complexity: Math.round(avgCC * 100) / 100,
|
|
18130
18292
|
avg_maintainability: Math.round(avgMI * 10) / 10,
|
|
18293
|
+
// Display-only: the top-15 for the "most complex functions" report.
|
|
18294
|
+
// Do NOT source offenders / --fail-on from this — capping here silently
|
|
18295
|
+
// hides the 16th+ over-threshold function from the gate. offenders()
|
|
18296
|
+
// reads "all_functions" (below) instead.
|
|
18131
18297
|
most_complex_functions: allFunctions.slice(0, 15),
|
|
18298
|
+
// Full, uncapped, complexity-sorted list — offenders()/--fail-on use this
|
|
18299
|
+
// so no function over the complexity threshold ever escapes the gate.
|
|
18300
|
+
all_functions: allFunctions,
|
|
18132
18301
|
file_metrics: fileMetrics,
|
|
18133
18302
|
violations,
|
|
18134
18303
|
dependency_graph: importGraph,
|
|
@@ -1183,7 +1183,14 @@ export function fullAnalysis(root: string = "src"): Record<string, any> {
|
|
|
1183
1183
|
total_functions: allFunctions.length,
|
|
1184
1184
|
avg_complexity: Math.round(avgCC * 100) / 100,
|
|
1185
1185
|
avg_maintainability: Math.round(avgMI * 10) / 10,
|
|
1186
|
+
// Display-only: the top-15 for the "most complex functions" report.
|
|
1187
|
+
// Do NOT source offenders / --fail-on from this — capping here silently
|
|
1188
|
+
// hides the 16th+ over-threshold function from the gate. offenders()
|
|
1189
|
+
// reads "all_functions" (below) instead.
|
|
1186
1190
|
most_complex_functions: allFunctions.slice(0, 15),
|
|
1191
|
+
// Full, uncapped, complexity-sorted list — offenders()/--fail-on use this
|
|
1192
|
+
// so no function over the complexity threshold ever escapes the gate.
|
|
1193
|
+
all_functions: allFunctions,
|
|
1187
1194
|
file_metrics: fileMetrics,
|
|
1188
1195
|
violations,
|
|
1189
1196
|
dependency_graph: importGraph,
|
|
@@ -1243,8 +1250,10 @@ export function offenders(root: string = "src", top: number = 20): OffendersResu
|
|
|
1243
1250
|
|
|
1244
1251
|
const items: Offender[] = [];
|
|
1245
1252
|
|
|
1246
|
-
// Function-level: cyclomatic complexity.
|
|
1247
|
-
|
|
1253
|
+
// Function-level: cyclomatic complexity. Use the FULL function list (not the
|
|
1254
|
+
// display-capped most_complex_functions[:15]) so a 16th+ over-threshold
|
|
1255
|
+
// function is never silently dropped from the offenders list or --fail-on.
|
|
1256
|
+
for (const fn of analysis.all_functions || analysis.most_complex_functions || []) {
|
|
1248
1257
|
const cc: number = fn.complexity;
|
|
1249
1258
|
if (cc > 10) {
|
|
1250
1259
|
items.push({
|
|
@@ -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";
|
|
@@ -101,6 +130,15 @@ function liveAttr(value) {
|
|
|
101
130
|
}
|
|
102
131
|
var filterChainCache = /* @__PURE__ */ new Map();
|
|
103
132
|
var pathParseCache = /* @__PURE__ */ new Map();
|
|
133
|
+
var TEMPLATE_CACHE_MAX = 256;
|
|
134
|
+
function capCache(cache, maxEntries) {
|
|
135
|
+
if (cache.size < maxEntries) return;
|
|
136
|
+
let drop = Math.floor(maxEntries / 2);
|
|
137
|
+
for (const key of cache.keys()) {
|
|
138
|
+
cache.delete(key);
|
|
139
|
+
if (--drop <= 0) break;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
104
142
|
var TOKEN_RE = /(\{%-?\s*[\s\S]*?\s*-?%\})|(\{\{-?\s*[\s\S]*?\s*-?\}\})|(\{#[\s\S]*?#\})/g;
|
|
105
143
|
var RAW_BLOCK_RE = /\{%-?\s*raw\s*-?%\}([\s\S]*?)\{%-?\s*endraw\s*-?%\}/g;
|
|
106
144
|
function tokenize(source) {
|
|
@@ -454,6 +492,9 @@ function evalExpr(expr, context) {
|
|
|
454
492
|
}).join("");
|
|
455
493
|
}
|
|
456
494
|
}
|
|
495
|
+
if (expr.startsWith("not ")) {
|
|
496
|
+
return evalComparison(expr, context);
|
|
497
|
+
}
|
|
457
498
|
for (const op of [" not in ", " in ", " is not ", " is ", "!=", "==", ">=", "<=", ">", "<", " and ", " or ", " not "]) {
|
|
458
499
|
if (findOutsideQuotes(expr, op) >= 0) {
|
|
459
500
|
return evalComparison(expr, context);
|
|
@@ -1037,7 +1078,7 @@ var BUILTIN_FILTERS = {
|
|
|
1037
1078
|
int: (v) => v ? parseInt(String(v), 10) || 0 : 0,
|
|
1038
1079
|
float: (v) => v ? parseFloat(String(v)) || 0 : 0,
|
|
1039
1080
|
string: (v) => String(v),
|
|
1040
|
-
json_encode: (v) =>
|
|
1081
|
+
json_encode: (v) => jsonSafe(v),
|
|
1041
1082
|
json_decode: (v) => typeof v === "string" ? JSON.parse(v) : v,
|
|
1042
1083
|
keys: (v) => typeof v === "object" && v !== null && !Array.isArray(v) ? Object.keys(v) : [],
|
|
1043
1084
|
values: (v) => typeof v === "object" && v !== null && !Array.isArray(v) ? Object.values(v) : [],
|
|
@@ -1182,8 +1223,12 @@ var BUILTIN_FILTERS = {
|
|
|
1182
1223
|
form_token: (v) => _generateFormToken(v != null ? String(v) : ""),
|
|
1183
1224
|
formTokenValue: (v) => _generateFormTokenValue(v != null ? String(v) : ""),
|
|
1184
1225
|
form_token_value: (v) => _generateFormTokenValue(v != null ? String(v) : ""),
|
|
1185
|
-
|
|
1186
|
-
|
|
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),
|
|
1187
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"))
|
|
1188
1233
|
};
|
|
1189
1234
|
function _b64url(data) {
|
|
@@ -1409,6 +1454,7 @@ var Frond = class _Frond {
|
|
|
1409
1454
|
const source = readFileSync(filePath, "utf-8");
|
|
1410
1455
|
const mtime = statSync(filePath).mtimeMs;
|
|
1411
1456
|
const tokens = tokenize(source);
|
|
1457
|
+
capCache(this.compiled, TEMPLATE_CACHE_MAX);
|
|
1412
1458
|
this.compiled.set(template, { tokens, mtime, cachedAt: Date.now() });
|
|
1413
1459
|
return this.executeWithSource(source, tokens, context);
|
|
1414
1460
|
}
|
|
@@ -1423,6 +1469,7 @@ var Frond = class _Frond {
|
|
|
1423
1469
|
}
|
|
1424
1470
|
}
|
|
1425
1471
|
const tokens = tokenize(source);
|
|
1472
|
+
capCache(this.compiledStrings, TEMPLATE_CACHE_MAX);
|
|
1426
1473
|
this.compiledStrings.set(key, { tokens, cachedAt: Date.now() });
|
|
1427
1474
|
return this.executeCached(tokens, context);
|
|
1428
1475
|
}
|
|
@@ -1638,6 +1685,9 @@ var Frond = class _Frond {
|
|
|
1638
1685
|
} else if (tag === "macro") {
|
|
1639
1686
|
const skip = this.handleMacro(tokens, i, context);
|
|
1640
1687
|
i = skip;
|
|
1688
|
+
} else if (tag === "import") {
|
|
1689
|
+
this.handleImportAs(content, context);
|
|
1690
|
+
i++;
|
|
1641
1691
|
} else if (tag === "from") {
|
|
1642
1692
|
this.handleFromImport(content, context);
|
|
1643
1693
|
i++;
|
|
@@ -1953,7 +2003,7 @@ var Frond = class _Frond {
|
|
|
1953
2003
|
value = typeof value === "object" && value !== null && !Array.isArray(value) ? Object.values(value) : [];
|
|
1954
2004
|
continue;
|
|
1955
2005
|
case "json_encode":
|
|
1956
|
-
value =
|
|
2006
|
+
value = jsonSafe(value);
|
|
1957
2007
|
continue;
|
|
1958
2008
|
case "dump":
|
|
1959
2009
|
value = renderDump(value);
|
|
@@ -2204,7 +2254,7 @@ var Frond = class _Frond {
|
|
|
2204
2254
|
return i2;
|
|
2205
2255
|
}
|
|
2206
2256
|
const macroName = m[1];
|
|
2207
|
-
const
|
|
2257
|
+
const params = _Frond.parseMacroParams(m[2]);
|
|
2208
2258
|
const bodyTokens = [];
|
|
2209
2259
|
let i = start + 1;
|
|
2210
2260
|
while (i < tokens.length) {
|
|
@@ -2219,13 +2269,94 @@ var Frond = class _Frond {
|
|
|
2219
2269
|
const capturedContext = { ...context };
|
|
2220
2270
|
context[macroName] = (...args) => {
|
|
2221
2271
|
const macroCtx = { ...capturedContext };
|
|
2222
|
-
for (let pi = 0; pi <
|
|
2223
|
-
|
|
2272
|
+
for (let pi = 0; pi < params.length; pi++) {
|
|
2273
|
+
const [pname, pdefault] = params[pi];
|
|
2274
|
+
macroCtx[pname] = pi < args.length ? args[pi] : pdefault;
|
|
2224
2275
|
}
|
|
2225
2276
|
return new SafeString(engine.renderTokens([...bodyTokens], macroCtx));
|
|
2226
2277
|
};
|
|
2227
2278
|
return i;
|
|
2228
2279
|
}
|
|
2280
|
+
/**
|
|
2281
|
+
* Parse a macro parameter list into [name, default] pairs.
|
|
2282
|
+
*
|
|
2283
|
+
* Handles: name, name="default", name='default'. Splitting on "," alone left a
|
|
2284
|
+
* defaulted parameter literally NAMED `greeting='Hello'`, so the body's
|
|
2285
|
+
* {{ greeting }} matched nothing (rendered empty) AND the caller's positional
|
|
2286
|
+
* argument was stored under that junk key and lost. Mirrors the Python master's
|
|
2287
|
+
* _parse_macro_params. The default is null when none is declared.
|
|
2288
|
+
*/
|
|
2289
|
+
static parseMacroParams(rawParams) {
|
|
2290
|
+
return rawParams.split(",").map((p) => p.trim()).filter(Boolean).map((p) => {
|
|
2291
|
+
const eq = p.indexOf("=");
|
|
2292
|
+
if (eq === -1) return [p, null];
|
|
2293
|
+
const name = p.slice(0, eq).trim();
|
|
2294
|
+
let dflt = p.slice(eq + 1).trim();
|
|
2295
|
+
if (dflt.length >= 2 && (dflt.startsWith('"') && dflt.endsWith('"') || dflt.startsWith("'") && dflt.endsWith("'"))) {
|
|
2296
|
+
dflt = dflt.slice(1, -1);
|
|
2297
|
+
}
|
|
2298
|
+
return [name, dflt];
|
|
2299
|
+
});
|
|
2300
|
+
}
|
|
2301
|
+
/**
|
|
2302
|
+
* {% import "file" as alias %} -- load EVERY macro in a file under one namespace.
|
|
2303
|
+
*
|
|
2304
|
+
* The alias is bound as a plain object of macro functions, so {{ alias.greet(x) }}
|
|
2305
|
+
* resolves through the engine's existing dotted-call path and each macro keeps the
|
|
2306
|
+
* same argument binding, default handling and SafeString output as any other macro.
|
|
2307
|
+
* A namespace object (not a class) is deliberate: a function stored as a class
|
|
2308
|
+
* attribute binds as a method and would inject the namespace as the first argument,
|
|
2309
|
+
* which is exactly the argument-shift bug the Python master carried (fixed there
|
|
2310
|
+
* with types.SimpleNamespace). Both import forms must render identically.
|
|
2311
|
+
*/
|
|
2312
|
+
handleImportAs(content, context) {
|
|
2313
|
+
const m = content.match(/^import\s+["'](.+?)["']\s+as\s+(\w+)/);
|
|
2314
|
+
if (!m) return;
|
|
2315
|
+
const filename = m[1];
|
|
2316
|
+
const alias = m[2];
|
|
2317
|
+
const namespace = {};
|
|
2318
|
+
const source = this.load(filename);
|
|
2319
|
+
const tokens = tokenize(source);
|
|
2320
|
+
let i = 0;
|
|
2321
|
+
while (i < tokens.length) {
|
|
2322
|
+
const [ttype, raw] = tokens[i];
|
|
2323
|
+
if (ttype === "BLOCK") {
|
|
2324
|
+
const [tagContent] = stripTag(raw);
|
|
2325
|
+
if ((tagContent.split(/\s+/)[0] || "") === "macro") {
|
|
2326
|
+
const macroM = tagContent.match(/^macro\s+(\w+)\s*\(([^)]*)\)/);
|
|
2327
|
+
if (macroM) {
|
|
2328
|
+
const macroName = macroM[1];
|
|
2329
|
+
const params = _Frond.parseMacroParams(macroM[2]);
|
|
2330
|
+
const bodyTokens = [];
|
|
2331
|
+
i++;
|
|
2332
|
+
while (i < tokens.length) {
|
|
2333
|
+
if (tokens[i][0] === "BLOCK" && tokens[i][1].includes("endmacro")) {
|
|
2334
|
+
i++;
|
|
2335
|
+
break;
|
|
2336
|
+
}
|
|
2337
|
+
bodyTokens.push(tokens[i]);
|
|
2338
|
+
i++;
|
|
2339
|
+
}
|
|
2340
|
+
const capturedBody = [...bodyTokens];
|
|
2341
|
+
const capturedParams = [...params];
|
|
2342
|
+
const capturedCtx = { ...context };
|
|
2343
|
+
const engine = this;
|
|
2344
|
+
namespace[macroName] = (...args) => {
|
|
2345
|
+
const macroCtx = { ...capturedCtx };
|
|
2346
|
+
for (let pi = 0; pi < capturedParams.length; pi++) {
|
|
2347
|
+
const [pname, pdefault] = capturedParams[pi];
|
|
2348
|
+
macroCtx[pname] = pi < args.length ? args[pi] : pdefault;
|
|
2349
|
+
}
|
|
2350
|
+
return new SafeString(engine.renderTokens([...capturedBody], macroCtx));
|
|
2351
|
+
};
|
|
2352
|
+
continue;
|
|
2353
|
+
}
|
|
2354
|
+
}
|
|
2355
|
+
}
|
|
2356
|
+
i++;
|
|
2357
|
+
}
|
|
2358
|
+
context[alias] = namespace;
|
|
2359
|
+
}
|
|
2229
2360
|
handleFromImport(content, context) {
|
|
2230
2361
|
const m = content.match(/^from\s+["'](.+?)["']\s+import\s+(.+)/);
|
|
2231
2362
|
if (!m) return;
|
|
@@ -2243,7 +2374,7 @@ var Frond = class _Frond {
|
|
|
2243
2374
|
const macroM = tagContent.match(/^macro\s+(\w+)\s*\(([^)]*)\)/);
|
|
2244
2375
|
if (macroM && names.includes(macroM[1])) {
|
|
2245
2376
|
const macroName = macroM[1];
|
|
2246
|
-
const paramNames = macroM[2]
|
|
2377
|
+
const paramNames = _Frond.parseMacroParams(macroM[2]);
|
|
2247
2378
|
const bodyTokens = [];
|
|
2248
2379
|
i++;
|
|
2249
2380
|
while (i < tokens.length) {
|
|
@@ -2261,7 +2392,8 @@ var Frond = class _Frond {
|
|
|
2261
2392
|
context[macroName] = (...args) => {
|
|
2262
2393
|
const macroCtx = { ...capturedCtx };
|
|
2263
2394
|
for (let pi = 0; pi < capturedParams.length; pi++) {
|
|
2264
|
-
|
|
2395
|
+
const [pname, pdefault] = capturedParams[pi];
|
|
2396
|
+
macroCtx[pname] = pi < args.length ? args[pi] : pdefault;
|
|
2265
2397
|
}
|
|
2266
2398
|
return new SafeString(engine.renderTokens([...capturedBody], macroCtx));
|
|
2267
2399
|
};
|