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
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
|
@@ -1675,18 +1675,21 @@ var init_postgres = __esm({
|
|
|
1675
1675
|
}
|
|
1676
1676
|
async startTransactionAsync() {
|
|
1677
1677
|
await this.executeAsync("BEGIN");
|
|
1678
|
+
this._inTransaction = true;
|
|
1678
1679
|
}
|
|
1679
1680
|
commit() {
|
|
1680
1681
|
throw new Error("Use commitAsync() for PostgreSQL.");
|
|
1681
1682
|
}
|
|
1682
1683
|
async commitAsync() {
|
|
1683
1684
|
await this.executeAsync("COMMIT");
|
|
1685
|
+
this._inTransaction = false;
|
|
1684
1686
|
}
|
|
1685
1687
|
rollback() {
|
|
1686
1688
|
throw new Error("Use rollbackAsync() for PostgreSQL.");
|
|
1687
1689
|
}
|
|
1688
1690
|
async rollbackAsync() {
|
|
1689
1691
|
await this.executeAsync("ROLLBACK");
|
|
1692
|
+
this._inTransaction = false;
|
|
1690
1693
|
}
|
|
1691
1694
|
tables() {
|
|
1692
1695
|
throw new Error("Use tablesAsync() for PostgreSQL.");
|
|
@@ -2705,10 +2708,35 @@ var init_firebird = __esm({
|
|
|
2705
2708
|
translated = SQLTranslator.ilikeToLike(translated);
|
|
2706
2709
|
return translated;
|
|
2707
2710
|
}
|
|
2711
|
+
/**
|
|
2712
|
+
* The handle every statement runs on. While an explicit transaction is open
|
|
2713
|
+
* (startTransactionAsync set `this.transaction`), statements MUST run on that
|
|
2714
|
+
* transaction object so they are undone by rollbackAsync() / persisted by
|
|
2715
|
+
* commitAsync() — node-firebird's transaction exposes the same
|
|
2716
|
+
* query()/execute() as the connection. With no transaction open we run on
|
|
2717
|
+
* `this.db`, whose per-statement work auto-commits on the connection.
|
|
2718
|
+
*
|
|
2719
|
+
* This matches the Python master's contract (tina4_python/database/firebird.py):
|
|
2720
|
+
* there, ALL statements run on the single connection and start_transaction()
|
|
2721
|
+
* merely suppresses the per-statement autocommit in execute() so the batch
|
|
2722
|
+
* stays open until commit()/rollback(). node-firebird has no such suppression
|
|
2723
|
+
* hook — its `db.query/execute` always auto-commit — so the equivalent is to
|
|
2724
|
+
* route statements through the transaction object instead. Same observable
|
|
2725
|
+
* behaviour: an open transaction is atomic and rolls back cleanly.
|
|
2726
|
+
*
|
|
2727
|
+
* Previously every statement ran on `this.db` unconditionally, so the
|
|
2728
|
+
* transaction created by startTransactionAsync() never saw a single statement
|
|
2729
|
+
* — rollbackAsync() rolled back an EMPTY transaction and the already
|
|
2730
|
+
* auto-committed write survived (silent no-op). Twin of the PHP pdo_firebird
|
|
2731
|
+
* bug fixed in 3.13.86.
|
|
2732
|
+
*/
|
|
2733
|
+
statementHandle() {
|
|
2734
|
+
return this.transaction ?? this.db;
|
|
2735
|
+
}
|
|
2708
2736
|
queryPromise(sql, params) {
|
|
2709
2737
|
return new Promise((resolve31, reject) => {
|
|
2710
2738
|
const translated = this.translateSql(sql);
|
|
2711
|
-
this.
|
|
2739
|
+
this.statementHandle().query(translated, params ?? [], (err, result) => {
|
|
2712
2740
|
if (err) reject(err);
|
|
2713
2741
|
else resolve31(result ?? []);
|
|
2714
2742
|
});
|
|
@@ -2717,7 +2745,7 @@ var init_firebird = __esm({
|
|
|
2717
2745
|
executePromise(sql, params) {
|
|
2718
2746
|
return new Promise((resolve31, reject) => {
|
|
2719
2747
|
const translated = this.translateSql(sql);
|
|
2720
|
-
this.
|
|
2748
|
+
this.statementHandle().execute(translated, params ?? [], (err) => {
|
|
2721
2749
|
if (err) reject(err);
|
|
2722
2750
|
else resolve31();
|
|
2723
2751
|
});
|
|
@@ -4691,14 +4719,15 @@ var init_database = __esm({
|
|
|
4691
4719
|
async executeMany(sql, paramSets = []) {
|
|
4692
4720
|
const adapter = this.getNextAdapter();
|
|
4693
4721
|
const results = [];
|
|
4694
|
-
|
|
4722
|
+
const owns = !this.inExplicitTransaction();
|
|
4723
|
+
if (owns) await adapterStartTransaction(adapter);
|
|
4695
4724
|
try {
|
|
4696
4725
|
for (const params of paramSets) {
|
|
4697
4726
|
results.push(await adapterExecute(adapter, sql, params));
|
|
4698
4727
|
}
|
|
4699
|
-
await adapterCommit(adapter);
|
|
4728
|
+
if (owns) await adapterCommit(adapter);
|
|
4700
4729
|
} catch (e) {
|
|
4701
|
-
await adapterRollback(adapter);
|
|
4730
|
+
if (owns) await adapterRollback(adapter);
|
|
4702
4731
|
throw e;
|
|
4703
4732
|
}
|
|
4704
4733
|
return results;
|
|
@@ -12751,11 +12780,32 @@ var init_request = __esm({
|
|
|
12751
12780
|
var engine_exports = {};
|
|
12752
12781
|
__export(engine_exports, {
|
|
12753
12782
|
Frond: () => Frond,
|
|
12783
|
+
TEMPLATE_CACHE_MAX: () => TEMPLATE_CACHE_MAX,
|
|
12754
12784
|
setFormTokenSessionId: () => setFormTokenSessionId
|
|
12755
12785
|
});
|
|
12756
12786
|
import { createHash as createHash2, createHmac as createHmac3, randomBytes as randomBytes4 } from "node:crypto";
|
|
12757
12787
|
import { readFileSync as readFileSync6, existsSync as existsSync9, statSync as statSync6 } from "node:fs";
|
|
12758
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
|
+
}
|
|
12759
12809
|
function inspectValue(value, seen = /* @__PURE__ */ new WeakSet(), depth = 0) {
|
|
12760
12810
|
if (value === null) return "null";
|
|
12761
12811
|
if (value === void 0) return "undefined";
|
|
@@ -12826,6 +12876,14 @@ function renderDump(value) {
|
|
|
12826
12876
|
function liveAttr(value) {
|
|
12827
12877
|
return String(value).replace(/&/g, "&").replace(/"/g, """).replace(/</g, "<").replace(/>/g, ">");
|
|
12828
12878
|
}
|
|
12879
|
+
function capCache(cache, maxEntries) {
|
|
12880
|
+
if (cache.size < maxEntries) return;
|
|
12881
|
+
let drop = Math.floor(maxEntries / 2);
|
|
12882
|
+
for (const key of cache.keys()) {
|
|
12883
|
+
cache.delete(key);
|
|
12884
|
+
if (--drop <= 0) break;
|
|
12885
|
+
}
|
|
12886
|
+
}
|
|
12829
12887
|
function tokenize(source) {
|
|
12830
12888
|
const rawBlocks = [];
|
|
12831
12889
|
source = source.replace(RAW_BLOCK_RE, (_match, content) => {
|
|
@@ -13177,6 +13235,9 @@ function evalExpr(expr, context) {
|
|
|
13177
13235
|
}).join("");
|
|
13178
13236
|
}
|
|
13179
13237
|
}
|
|
13238
|
+
if (expr.startsWith("not ")) {
|
|
13239
|
+
return evalComparison(expr, context);
|
|
13240
|
+
}
|
|
13180
13241
|
for (const op of [" not in ", " in ", " is not ", " is ", "!=", "==", ">=", "<=", ">", "<", " and ", " or ", " not "]) {
|
|
13181
13242
|
if (findOutsideQuotes(expr, op) >= 0) {
|
|
13182
13243
|
return evalComparison(expr, context);
|
|
@@ -13732,7 +13793,7 @@ function _generateFormToken(descriptor = "") {
|
|
|
13732
13793
|
function _generateFormTokenValue(descriptor = "") {
|
|
13733
13794
|
return new SafeString(_buildFormTokenJwt(descriptor));
|
|
13734
13795
|
}
|
|
13735
|
-
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;
|
|
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;
|
|
13736
13797
|
var init_engine = __esm({
|
|
13737
13798
|
"../frond/src/engine.ts"() {
|
|
13738
13799
|
"use strict";
|
|
@@ -13744,6 +13805,15 @@ var init_engine = __esm({
|
|
|
13744
13805
|
return this.value;
|
|
13745
13806
|
}
|
|
13746
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
|
+
};
|
|
13747
13817
|
NUMERIC_RE = /^-?\d+(\.\d+)?$/;
|
|
13748
13818
|
METHOD_CALL_RE = /^(\w+)\s*\(([\s\S]*)?\)$/;
|
|
13749
13819
|
FN_CALL_RE = /^([\w.]+)\s*\(([\s\S]*)?\)$/;
|
|
@@ -13765,6 +13835,7 @@ var init_engine = __esm({
|
|
|
13765
13835
|
LIVE_SRC_RE = /src\s+["']([^"']+)["']/;
|
|
13766
13836
|
filterChainCache = /* @__PURE__ */ new Map();
|
|
13767
13837
|
pathParseCache = /* @__PURE__ */ new Map();
|
|
13838
|
+
TEMPLATE_CACHE_MAX = 256;
|
|
13768
13839
|
TOKEN_RE = /(\{%-?\s*[\s\S]*?\s*-?%\})|(\{\{-?\s*[\s\S]*?\s*-?\}\})|(\{#[\s\S]*?#\})/g;
|
|
13769
13840
|
RAW_BLOCK_RE = /\{%-?\s*raw\s*-?%\}([\s\S]*?)\{%-?\s*endraw\s*-?%\}/g;
|
|
13770
13841
|
VarRef = class {
|
|
@@ -13833,7 +13904,7 @@ var init_engine = __esm({
|
|
|
13833
13904
|
int: (v) => v ? parseInt(String(v), 10) || 0 : 0,
|
|
13834
13905
|
float: (v) => v ? parseFloat(String(v)) || 0 : 0,
|
|
13835
13906
|
string: (v) => String(v),
|
|
13836
|
-
json_encode: (v) =>
|
|
13907
|
+
json_encode: (v) => jsonSafe(v),
|
|
13837
13908
|
json_decode: (v) => typeof v === "string" ? JSON.parse(v) : v,
|
|
13838
13909
|
keys: (v) => typeof v === "object" && v !== null && !Array.isArray(v) ? Object.keys(v) : [],
|
|
13839
13910
|
values: (v) => typeof v === "object" && v !== null && !Array.isArray(v) ? Object.values(v) : [],
|
|
@@ -13978,8 +14049,12 @@ var init_engine = __esm({
|
|
|
13978
14049
|
form_token: (v) => _generateFormToken(v != null ? String(v) : ""),
|
|
13979
14050
|
formTokenValue: (v) => _generateFormTokenValue(v != null ? String(v) : ""),
|
|
13980
14051
|
form_token_value: (v) => _generateFormTokenValue(v != null ? String(v) : ""),
|
|
13981
|
-
|
|
13982
|
-
|
|
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),
|
|
13983
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"))
|
|
13984
14059
|
};
|
|
13985
14060
|
_formTokenSessionId = "";
|
|
@@ -14170,6 +14245,7 @@ var init_engine = __esm({
|
|
|
14170
14245
|
const source = readFileSync6(filePath, "utf-8");
|
|
14171
14246
|
const mtime = statSync6(filePath).mtimeMs;
|
|
14172
14247
|
const tokens = tokenize(source);
|
|
14248
|
+
capCache(this.compiled, TEMPLATE_CACHE_MAX);
|
|
14173
14249
|
this.compiled.set(template, { tokens, mtime, cachedAt: Date.now() });
|
|
14174
14250
|
return this.executeWithSource(source, tokens, context);
|
|
14175
14251
|
}
|
|
@@ -14184,6 +14260,7 @@ var init_engine = __esm({
|
|
|
14184
14260
|
}
|
|
14185
14261
|
}
|
|
14186
14262
|
const tokens = tokenize(source);
|
|
14263
|
+
capCache(this.compiledStrings, TEMPLATE_CACHE_MAX);
|
|
14187
14264
|
this.compiledStrings.set(key, { tokens, cachedAt: Date.now() });
|
|
14188
14265
|
return this.executeCached(tokens, context);
|
|
14189
14266
|
}
|
|
@@ -14399,6 +14476,9 @@ var init_engine = __esm({
|
|
|
14399
14476
|
} else if (tag === "macro") {
|
|
14400
14477
|
const skip = this.handleMacro(tokens, i, context);
|
|
14401
14478
|
i = skip;
|
|
14479
|
+
} else if (tag === "import") {
|
|
14480
|
+
this.handleImportAs(content, context);
|
|
14481
|
+
i++;
|
|
14402
14482
|
} else if (tag === "from") {
|
|
14403
14483
|
this.handleFromImport(content, context);
|
|
14404
14484
|
i++;
|
|
@@ -14714,7 +14794,7 @@ var init_engine = __esm({
|
|
|
14714
14794
|
value = typeof value === "object" && value !== null && !Array.isArray(value) ? Object.values(value) : [];
|
|
14715
14795
|
continue;
|
|
14716
14796
|
case "json_encode":
|
|
14717
|
-
value =
|
|
14797
|
+
value = jsonSafe(value);
|
|
14718
14798
|
continue;
|
|
14719
14799
|
case "dump":
|
|
14720
14800
|
value = renderDump(value);
|
|
@@ -14965,7 +15045,7 @@ var init_engine = __esm({
|
|
|
14965
15045
|
return i2;
|
|
14966
15046
|
}
|
|
14967
15047
|
const macroName = m[1];
|
|
14968
|
-
const
|
|
15048
|
+
const params = _Frond.parseMacroParams(m[2]);
|
|
14969
15049
|
const bodyTokens = [];
|
|
14970
15050
|
let i = start2 + 1;
|
|
14971
15051
|
while (i < tokens.length) {
|
|
@@ -14980,13 +15060,94 @@ var init_engine = __esm({
|
|
|
14980
15060
|
const capturedContext = { ...context };
|
|
14981
15061
|
context[macroName] = (...args) => {
|
|
14982
15062
|
const macroCtx = { ...capturedContext };
|
|
14983
|
-
for (let pi = 0; pi <
|
|
14984
|
-
|
|
15063
|
+
for (let pi = 0; pi < params.length; pi++) {
|
|
15064
|
+
const [pname, pdefault] = params[pi];
|
|
15065
|
+
macroCtx[pname] = pi < args.length ? args[pi] : pdefault;
|
|
14985
15066
|
}
|
|
14986
15067
|
return new SafeString(engine.renderTokens([...bodyTokens], macroCtx));
|
|
14987
15068
|
};
|
|
14988
15069
|
return i;
|
|
14989
15070
|
}
|
|
15071
|
+
/**
|
|
15072
|
+
* Parse a macro parameter list into [name, default] pairs.
|
|
15073
|
+
*
|
|
15074
|
+
* Handles: name, name="default", name='default'. Splitting on "," alone left a
|
|
15075
|
+
* defaulted parameter literally NAMED `greeting='Hello'`, so the body's
|
|
15076
|
+
* {{ greeting }} matched nothing (rendered empty) AND the caller's positional
|
|
15077
|
+
* argument was stored under that junk key and lost. Mirrors the Python master's
|
|
15078
|
+
* _parse_macro_params. The default is null when none is declared.
|
|
15079
|
+
*/
|
|
15080
|
+
static parseMacroParams(rawParams) {
|
|
15081
|
+
return rawParams.split(",").map((p) => p.trim()).filter(Boolean).map((p) => {
|
|
15082
|
+
const eq = p.indexOf("=");
|
|
15083
|
+
if (eq === -1) return [p, null];
|
|
15084
|
+
const name = p.slice(0, eq).trim();
|
|
15085
|
+
let dflt = p.slice(eq + 1).trim();
|
|
15086
|
+
if (dflt.length >= 2 && (dflt.startsWith('"') && dflt.endsWith('"') || dflt.startsWith("'") && dflt.endsWith("'"))) {
|
|
15087
|
+
dflt = dflt.slice(1, -1);
|
|
15088
|
+
}
|
|
15089
|
+
return [name, dflt];
|
|
15090
|
+
});
|
|
15091
|
+
}
|
|
15092
|
+
/**
|
|
15093
|
+
* {% import "file" as alias %} -- load EVERY macro in a file under one namespace.
|
|
15094
|
+
*
|
|
15095
|
+
* The alias is bound as a plain object of macro functions, so {{ alias.greet(x) }}
|
|
15096
|
+
* resolves through the engine's existing dotted-call path and each macro keeps the
|
|
15097
|
+
* same argument binding, default handling and SafeString output as any other macro.
|
|
15098
|
+
* A namespace object (not a class) is deliberate: a function stored as a class
|
|
15099
|
+
* attribute binds as a method and would inject the namespace as the first argument,
|
|
15100
|
+
* which is exactly the argument-shift bug the Python master carried (fixed there
|
|
15101
|
+
* with types.SimpleNamespace). Both import forms must render identically.
|
|
15102
|
+
*/
|
|
15103
|
+
handleImportAs(content, context) {
|
|
15104
|
+
const m = content.match(/^import\s+["'](.+?)["']\s+as\s+(\w+)/);
|
|
15105
|
+
if (!m) return;
|
|
15106
|
+
const filename = m[1];
|
|
15107
|
+
const alias = m[2];
|
|
15108
|
+
const namespace = {};
|
|
15109
|
+
const source = this.load(filename);
|
|
15110
|
+
const tokens = tokenize(source);
|
|
15111
|
+
let i = 0;
|
|
15112
|
+
while (i < tokens.length) {
|
|
15113
|
+
const [ttype, raw] = tokens[i];
|
|
15114
|
+
if (ttype === "BLOCK") {
|
|
15115
|
+
const [tagContent] = stripTag(raw);
|
|
15116
|
+
if ((tagContent.split(/\s+/)[0] || "") === "macro") {
|
|
15117
|
+
const macroM = tagContent.match(/^macro\s+(\w+)\s*\(([^)]*)\)/);
|
|
15118
|
+
if (macroM) {
|
|
15119
|
+
const macroName = macroM[1];
|
|
15120
|
+
const params = _Frond.parseMacroParams(macroM[2]);
|
|
15121
|
+
const bodyTokens = [];
|
|
15122
|
+
i++;
|
|
15123
|
+
while (i < tokens.length) {
|
|
15124
|
+
if (tokens[i][0] === "BLOCK" && tokens[i][1].includes("endmacro")) {
|
|
15125
|
+
i++;
|
|
15126
|
+
break;
|
|
15127
|
+
}
|
|
15128
|
+
bodyTokens.push(tokens[i]);
|
|
15129
|
+
i++;
|
|
15130
|
+
}
|
|
15131
|
+
const capturedBody = [...bodyTokens];
|
|
15132
|
+
const capturedParams = [...params];
|
|
15133
|
+
const capturedCtx = { ...context };
|
|
15134
|
+
const engine = this;
|
|
15135
|
+
namespace[macroName] = (...args) => {
|
|
15136
|
+
const macroCtx = { ...capturedCtx };
|
|
15137
|
+
for (let pi = 0; pi < capturedParams.length; pi++) {
|
|
15138
|
+
const [pname, pdefault] = capturedParams[pi];
|
|
15139
|
+
macroCtx[pname] = pi < args.length ? args[pi] : pdefault;
|
|
15140
|
+
}
|
|
15141
|
+
return new SafeString(engine.renderTokens([...capturedBody], macroCtx));
|
|
15142
|
+
};
|
|
15143
|
+
continue;
|
|
15144
|
+
}
|
|
15145
|
+
}
|
|
15146
|
+
}
|
|
15147
|
+
i++;
|
|
15148
|
+
}
|
|
15149
|
+
context[alias] = namespace;
|
|
15150
|
+
}
|
|
14990
15151
|
handleFromImport(content, context) {
|
|
14991
15152
|
const m = content.match(/^from\s+["'](.+?)["']\s+import\s+(.+)/);
|
|
14992
15153
|
if (!m) return;
|
|
@@ -15004,7 +15165,7 @@ var init_engine = __esm({
|
|
|
15004
15165
|
const macroM = tagContent.match(/^macro\s+(\w+)\s*\(([^)]*)\)/);
|
|
15005
15166
|
if (macroM && names.includes(macroM[1])) {
|
|
15006
15167
|
const macroName = macroM[1];
|
|
15007
|
-
const paramNames = macroM[2]
|
|
15168
|
+
const paramNames = _Frond.parseMacroParams(macroM[2]);
|
|
15008
15169
|
const bodyTokens = [];
|
|
15009
15170
|
i++;
|
|
15010
15171
|
while (i < tokens.length) {
|
|
@@ -15022,7 +15183,8 @@ var init_engine = __esm({
|
|
|
15022
15183
|
context[macroName] = (...args) => {
|
|
15023
15184
|
const macroCtx = { ...capturedCtx };
|
|
15024
15185
|
for (let pi = 0; pi < capturedParams.length; pi++) {
|
|
15025
|
-
|
|
15186
|
+
const [pname, pdefault] = capturedParams[pi];
|
|
15187
|
+
macroCtx[pname] = pi < args.length ? args[pi] : pdefault;
|
|
15026
15188
|
}
|
|
15027
15189
|
return new SafeString(engine.renderTokens([...capturedBody], macroCtx));
|
|
15028
15190
|
};
|
|
@@ -18129,7 +18291,14 @@ function fullAnalysis(root = "src") {
|
|
|
18129
18291
|
total_functions: allFunctions.length,
|
|
18130
18292
|
avg_complexity: Math.round(avgCC * 100) / 100,
|
|
18131
18293
|
avg_maintainability: Math.round(avgMI * 10) / 10,
|
|
18294
|
+
// Display-only: the top-15 for the "most complex functions" report.
|
|
18295
|
+
// Do NOT source offenders / --fail-on from this — capping here silently
|
|
18296
|
+
// hides the 16th+ over-threshold function from the gate. offenders()
|
|
18297
|
+
// reads "all_functions" (below) instead.
|
|
18132
18298
|
most_complex_functions: allFunctions.slice(0, 15),
|
|
18299
|
+
// Full, uncapped, complexity-sorted list — offenders()/--fail-on use this
|
|
18300
|
+
// so no function over the complexity threshold ever escapes the gate.
|
|
18301
|
+
all_functions: allFunctions,
|
|
18133
18302
|
file_metrics: fileMetrics,
|
|
18134
18303
|
violations,
|
|
18135
18304
|
dependency_graph: importGraph,
|
|
@@ -18145,7 +18314,7 @@ function offenders(root = "src", top = 20) {
|
|
|
18145
18314
|
return { offenders: [], summary: { error: analysis.error } };
|
|
18146
18315
|
}
|
|
18147
18316
|
const items = [];
|
|
18148
|
-
for (const fn of analysis.most_complex_functions || []) {
|
|
18317
|
+
for (const fn of analysis.all_functions || analysis.most_complex_functions || []) {
|
|
18149
18318
|
const cc = fn.complexity;
|
|
18150
18319
|
if (cc > 10) {
|
|
18151
18320
|
items.push({
|