tina4-nodejs 3.13.99 → 3.13.100
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 +2 -2
- package/package.json +1 -1
- package/packages/cli/dist/bin.js +130 -40
- package/packages/core/dist/index.js +129 -40
- package/packages/core/src/ai.ts +15 -6
- package/packages/frond/dist/index.js +118 -36
- package/packages/frond/src/engine.ts +195 -45
- package/packages/orm/dist/index.js +129 -40
- package/types/core/src/ai.d.ts +29 -0
- package/types/frond/src/engine.d.ts +50 -8
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.100)
|
|
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.100 - 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
|
|
package/package.json
CHANGED
package/packages/cli/dist/bin.js
CHANGED
|
@@ -1461,7 +1461,10 @@ var init_trustedProxy = __esm({
|
|
|
1461
1461
|
var engine_exports = {};
|
|
1462
1462
|
__export(engine_exports, {
|
|
1463
1463
|
Frond: () => Frond,
|
|
1464
|
+
MEMO_CACHE_MAX: () => MEMO_CACHE_MAX,
|
|
1464
1465
|
TEMPLATE_CACHE_MAX: () => TEMPLATE_CACHE_MAX,
|
|
1466
|
+
filterChainCache: () => filterChainCache,
|
|
1467
|
+
pathParseCache: () => pathParseCache,
|
|
1465
1468
|
setFormTokenSessionId: () => setFormTokenSessionId
|
|
1466
1469
|
});
|
|
1467
1470
|
import { createHash as createHash2, createHmac as createHmac2, randomBytes as randomBytes2 } from "node:crypto";
|
|
@@ -1565,6 +1568,12 @@ function capCache(cache, maxEntries) {
|
|
|
1565
1568
|
if (--drop <= 0) break;
|
|
1566
1569
|
}
|
|
1567
1570
|
}
|
|
1571
|
+
function sweepExpiredCache(cache) {
|
|
1572
|
+
const now = Date.now();
|
|
1573
|
+
for (const [key, [, expiresAt]] of cache) {
|
|
1574
|
+
if (expiresAt <= now) cache.delete(key);
|
|
1575
|
+
}
|
|
1576
|
+
}
|
|
1568
1577
|
function tokenize(source) {
|
|
1569
1578
|
const rawBlocks = [];
|
|
1570
1579
|
source = source.replace(RAW_BLOCK_RE, (_match, content) => {
|
|
@@ -1628,6 +1637,16 @@ function stripTag(raw) {
|
|
|
1628
1637
|
}
|
|
1629
1638
|
return [inner.trim(), stripBefore, stripAfter];
|
|
1630
1639
|
}
|
|
1640
|
+
function extendsTarget(source) {
|
|
1641
|
+
const matches = source.match(EXTENDS_RE_GLOBAL);
|
|
1642
|
+
if (matches && matches.length > 1) {
|
|
1643
|
+
throw new Error(
|
|
1644
|
+
`Frond: template has ${matches.length} "{% extends %}" tags -- a template can extend only one parent`
|
|
1645
|
+
);
|
|
1646
|
+
}
|
|
1647
|
+
const match = source.match(EXTENDS_RE);
|
|
1648
|
+
return match ? match[1] : "";
|
|
1649
|
+
}
|
|
1631
1650
|
function resolveVar(expr, context) {
|
|
1632
1651
|
expr = expr.trim();
|
|
1633
1652
|
if (expr.startsWith('"') && expr.endsWith('"') || expr.startsWith("'") && expr.endsWith("'")) {
|
|
@@ -1708,6 +1727,7 @@ function resolveVar(expr, context) {
|
|
|
1708
1727
|
fromBracket.push(false);
|
|
1709
1728
|
}
|
|
1710
1729
|
}
|
|
1730
|
+
capCache(pathParseCache, MEMO_CACHE_MAX);
|
|
1711
1731
|
pathParseCache.set(expr, [parts, fromBracket]);
|
|
1712
1732
|
}
|
|
1713
1733
|
let value = context;
|
|
@@ -2295,6 +2315,7 @@ function parseFilterChain(expr) {
|
|
|
2295
2315
|
}
|
|
2296
2316
|
}
|
|
2297
2317
|
const result = [variable, filters];
|
|
2318
|
+
capCache(filterChainCache, MEMO_CACHE_MAX);
|
|
2298
2319
|
filterChainCache.set(expr, result);
|
|
2299
2320
|
return result;
|
|
2300
2321
|
}
|
|
@@ -2474,7 +2495,7 @@ function _generateFormToken(descriptor = "") {
|
|
|
2474
2495
|
function _generateFormTokenValue(descriptor = "") {
|
|
2475
2496
|
return new SafeString(_buildFormTokenJwt(descriptor));
|
|
2476
2497
|
}
|
|
2477
|
-
var SafeString, KNOWN_TAGS, TERMINATOR_TAGS, GATEABLE_TAGS, BLOCK_TAG_ENDS, 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;
|
|
2498
|
+
var SafeString, KNOWN_TAGS, TERMINATOR_TAGS, GATEABLE_TAGS, BLOCK_TAG_ENDS, 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, EXTENDS_RE, EXTENDS_RE_GLOBAL, filterChainCache, pathParseCache, TEMPLATE_CACHE_MAX, MEMO_CACHE_MAX, TOKEN_RE, RAW_BLOCK_RE, VarRef, BUILTIN_FILTERS, _formTokenSessionId, Frond;
|
|
2478
2499
|
var init_engine = __esm({
|
|
2479
2500
|
"../frond/src/engine.ts"() {
|
|
2480
2501
|
"use strict";
|
|
@@ -2568,9 +2589,12 @@ var init_engine = __esm({
|
|
|
2568
2589
|
LIVE_RE = /^live\s+["']([^"']+)["']([\s\S]*)$/;
|
|
2569
2590
|
LIVE_WS_RE = /ws\s+["']([^"']+)["']/;
|
|
2570
2591
|
LIVE_SRC_RE = /src\s+["']([^"']+)["']/;
|
|
2592
|
+
EXTENDS_RE = /\{%[-\s]*extends\s+["'](.+?)["']\s*[-]?%\}/;
|
|
2593
|
+
EXTENDS_RE_GLOBAL = /\{%[-\s]*extends\s+["'](.+?)["']\s*[-]?%\}/g;
|
|
2571
2594
|
filterChainCache = /* @__PURE__ */ new Map();
|
|
2572
2595
|
pathParseCache = /* @__PURE__ */ new Map();
|
|
2573
2596
|
TEMPLATE_CACHE_MAX = 256;
|
|
2597
|
+
MEMO_CACHE_MAX = 1024;
|
|
2574
2598
|
TOKEN_RE = /(\{%-?\s*[\s\S]*?\s*-?%\})|(\{\{-?\s*[\s\S]*?\s*-?\}\})|(\{#[\s\S]*?#\})/g;
|
|
2575
2599
|
RAW_BLOCK_RE = /\{%-?\s*raw\s*-?%\}([\s\S]*?)\{%-?\s*endraw\s*-?%\}/g;
|
|
2576
2600
|
VarRef = class {
|
|
@@ -2923,29 +2947,22 @@ var init_engine = __esm({
|
|
|
2923
2947
|
return this;
|
|
2924
2948
|
}
|
|
2925
2949
|
/**
|
|
2926
|
-
* Register a custom filter
|
|
2927
|
-
*
|
|
2928
|
-
* the live instance's local filter map also receives the addition
|
|
2929
|
-
* immediately. Mirrors Python's _ClassOrInstanceMethod dual-call.
|
|
2950
|
+
* Register a custom filter on this instance only. Use the static method
|
|
2951
|
+
* for process-global registration. tina4: ADR-0052.
|
|
2930
2952
|
*/
|
|
2931
2953
|
addFilter(name, fn) {
|
|
2932
|
-
_Frond.classFilters.set(name, fn);
|
|
2933
2954
|
this.filters[name] = fn;
|
|
2934
2955
|
}
|
|
2935
2956
|
/**
|
|
2936
|
-
* Register a global variable
|
|
2937
|
-
* at class level — see ``addFilter`` for the dual-call semantics.
|
|
2957
|
+
* Register a global variable on this instance only.
|
|
2938
2958
|
*/
|
|
2939
2959
|
addGlobal(name, value) {
|
|
2940
|
-
_Frond.classGlobals.set(name, value);
|
|
2941
2960
|
this.globals[name] = value;
|
|
2942
2961
|
}
|
|
2943
2962
|
/**
|
|
2944
|
-
* Register a custom test
|
|
2945
|
-
* ``addFilter`` for the dual-call semantics.
|
|
2963
|
+
* Register a custom test on this instance only.
|
|
2946
2964
|
*/
|
|
2947
2965
|
addTest(name, fn) {
|
|
2948
|
-
_Frond.classTests.set(name, fn);
|
|
2949
2966
|
this.tests[name] = fn;
|
|
2950
2967
|
}
|
|
2951
2968
|
/**
|
|
@@ -3060,9 +3077,8 @@ var init_engine = __esm({
|
|
|
3060
3077
|
if (Object.keys(this.tests).length > 0) {
|
|
3061
3078
|
context.__frond_tests__ = this.tests;
|
|
3062
3079
|
}
|
|
3063
|
-
const
|
|
3064
|
-
if (
|
|
3065
|
-
const parentName = extendsMatch[1];
|
|
3080
|
+
const parentName = extendsTarget(source);
|
|
3081
|
+
if (parentName) {
|
|
3066
3082
|
const parentSource = this.load(parentName);
|
|
3067
3083
|
const childBlocks = this.extractBlocks(source);
|
|
3068
3084
|
return this.renderWithBlocks(parentSource, context, childBlocks);
|
|
@@ -3073,9 +3089,8 @@ var init_engine = __esm({
|
|
|
3073
3089
|
if (Object.keys(this.tests).length > 0) {
|
|
3074
3090
|
context.__frond_tests__ = this.tests;
|
|
3075
3091
|
}
|
|
3076
|
-
const
|
|
3077
|
-
if (
|
|
3078
|
-
const parentName = extendsMatch[1];
|
|
3092
|
+
const parentName = extendsTarget(source);
|
|
3093
|
+
if (parentName) {
|
|
3079
3094
|
const parentSource = this.load(parentName);
|
|
3080
3095
|
const childBlocks = this.extractBlocks(source);
|
|
3081
3096
|
return this.renderWithBlocks(parentSource, context, childBlocks);
|
|
@@ -3120,10 +3135,93 @@ var init_engine = __esm({
|
|
|
3120
3135
|
}
|
|
3121
3136
|
return blocks;
|
|
3122
3137
|
}
|
|
3138
|
+
/**
|
|
3139
|
+
* Depth-aware block substitution against `source` (typically the
|
|
3140
|
+
* fully-resolved root template).
|
|
3141
|
+
*
|
|
3142
|
+
* A single regex `.replace()` pass (the flat `pattern` this replaces in
|
|
3143
|
+
* renderWithBlocks) pairs an OUTER block's open tag with the FIRST
|
|
3144
|
+
* `{% endblock %}` found -- which, when the outer block wraps a NESTED
|
|
3145
|
+
* `{% block %}`, is the nested block's own close tag, not the outer's.
|
|
3146
|
+
* That silently truncates the outer block's captured content and drops
|
|
3147
|
+
* everything after the inner endblock (the root-nested-block
|
|
3148
|
+
* content-loss bug). This scans with an open/close depth counter
|
|
3149
|
+
* instead (mirroring extractBlocks), so an outer block always captures
|
|
3150
|
+
* its FULL body, nested child blocks included.
|
|
3151
|
+
*
|
|
3152
|
+
* The content chosen for each block -- the child override in `blocks`
|
|
3153
|
+
* if present, else the block's own default body -- is then recursively
|
|
3154
|
+
* substituted against the SAME `blocks` map before being tokenized and
|
|
3155
|
+
* rendered, so a block nested inside another block resolves correctly
|
|
3156
|
+
* regardless of which template in the inheritance chain declared the
|
|
3157
|
+
* nesting (the root, an intermediate, however many levels deep).
|
|
3158
|
+
*
|
|
3159
|
+
* `{{ parent() }}` / `{{ super() }}` inside a block still render that
|
|
3160
|
+
* block's OWN default content at this level (lazy, on first call).
|
|
3161
|
+
*/
|
|
3162
|
+
substituteBlocks(source, blocks, context) {
|
|
3163
|
+
const blockOpen = /\{%[-\s]*block\s+(\w+)\s*[-]?%\}/g;
|
|
3164
|
+
const blockClose = /\{%[-\s]*endblock\s*[-]?%\}/g;
|
|
3165
|
+
const engine = this;
|
|
3166
|
+
const pieces = [];
|
|
3167
|
+
let pos = 0;
|
|
3168
|
+
while (pos < source.length) {
|
|
3169
|
+
blockOpen.lastIndex = pos;
|
|
3170
|
+
const mOpen = blockOpen.exec(source);
|
|
3171
|
+
if (!mOpen) {
|
|
3172
|
+
pieces.push(source.slice(pos));
|
|
3173
|
+
break;
|
|
3174
|
+
}
|
|
3175
|
+
pieces.push(source.slice(pos, mOpen.index));
|
|
3176
|
+
const name = mOpen[1];
|
|
3177
|
+
const contentStart = mOpen.index + mOpen[0].length;
|
|
3178
|
+
let depth = 1;
|
|
3179
|
+
let scan = contentStart;
|
|
3180
|
+
let closeMatch = null;
|
|
3181
|
+
while (depth > 0 && scan < source.length) {
|
|
3182
|
+
blockOpen.lastIndex = scan;
|
|
3183
|
+
blockClose.lastIndex = scan;
|
|
3184
|
+
const nextOpen = blockOpen.exec(source);
|
|
3185
|
+
const nextClose = blockClose.exec(source);
|
|
3186
|
+
if (!nextClose) break;
|
|
3187
|
+
if (nextOpen && nextOpen.index < nextClose.index) {
|
|
3188
|
+
depth++;
|
|
3189
|
+
scan = nextOpen.index + nextOpen[0].length;
|
|
3190
|
+
} else {
|
|
3191
|
+
depth--;
|
|
3192
|
+
if (depth === 0) {
|
|
3193
|
+
closeMatch = nextClose;
|
|
3194
|
+
} else {
|
|
3195
|
+
scan = nextClose.index + nextClose[0].length;
|
|
3196
|
+
}
|
|
3197
|
+
}
|
|
3198
|
+
}
|
|
3199
|
+
if (!closeMatch) {
|
|
3200
|
+
pieces.push(source.slice(mOpen.index));
|
|
3201
|
+
pos = source.length;
|
|
3202
|
+
break;
|
|
3203
|
+
}
|
|
3204
|
+
const parentContent = source.slice(contentStart, closeMatch.index);
|
|
3205
|
+
const blockSource = blocks[name] ?? parentContent;
|
|
3206
|
+
const resolvedSource = engine.substituteBlocks(blockSource, blocks, context);
|
|
3207
|
+
let renderedParent = null;
|
|
3208
|
+
const getParent = () => {
|
|
3209
|
+
if (renderedParent === null) {
|
|
3210
|
+
renderedParent = new SafeString(
|
|
3211
|
+
engine.renderTokens(tokenize(parentContent), context)
|
|
3212
|
+
);
|
|
3213
|
+
}
|
|
3214
|
+
return renderedParent;
|
|
3215
|
+
};
|
|
3216
|
+
const blockCtx = { ...context, parent: getParent, super: getParent };
|
|
3217
|
+
pieces.push(engine.renderTokens(tokenize(resolvedSource), blockCtx));
|
|
3218
|
+
pos = closeMatch.index + closeMatch[0].length;
|
|
3219
|
+
}
|
|
3220
|
+
return pieces.join("");
|
|
3221
|
+
}
|
|
3123
3222
|
renderWithBlocks(parentSource, context, childBlocks) {
|
|
3124
|
-
const
|
|
3125
|
-
if (
|
|
3126
|
-
const grandparentName = extendsMatch[1];
|
|
3223
|
+
const grandparentName = extendsTarget(parentSource);
|
|
3224
|
+
if (grandparentName) {
|
|
3127
3225
|
const grandparentSource = this.load(grandparentName);
|
|
3128
3226
|
const parentBlocks = this.extractBlocks(parentSource);
|
|
3129
3227
|
const mergedBlocks = { ...parentBlocks, ...childBlocks };
|
|
@@ -3143,22 +3241,7 @@ var init_engine = __esm({
|
|
|
3143
3241
|
}
|
|
3144
3242
|
return this.renderWithBlocks(grandparentSource, context, mergedBlocks);
|
|
3145
3243
|
}
|
|
3146
|
-
const
|
|
3147
|
-
const engine = this;
|
|
3148
|
-
const result = parentSource.replace(pattern, (_match, name, parentContent) => {
|
|
3149
|
-
const blockSource = childBlocks[name] ?? parentContent;
|
|
3150
|
-
let renderedParent = null;
|
|
3151
|
-
const getParent = () => {
|
|
3152
|
-
if (renderedParent === null) {
|
|
3153
|
-
renderedParent = new SafeString(
|
|
3154
|
-
engine.renderTokens(tokenize(parentContent), context)
|
|
3155
|
-
);
|
|
3156
|
-
}
|
|
3157
|
-
return renderedParent;
|
|
3158
|
-
};
|
|
3159
|
-
const blockCtx = { ...context, parent: getParent, super: getParent };
|
|
3160
|
-
return this.renderTokens(tokenize(blockSource), blockCtx);
|
|
3161
|
-
});
|
|
3244
|
+
const result = this.substituteBlocks(parentSource, childBlocks, context);
|
|
3162
3245
|
return this.renderTokens(tokenize(result), context);
|
|
3163
3246
|
}
|
|
3164
3247
|
renderTokens(tokens, context) {
|
|
@@ -3981,6 +4064,7 @@ var init_engine = __esm({
|
|
|
3981
4064
|
const m = content.match(/^cache\s+["'](.+?)["']\s*(\d+)?/);
|
|
3982
4065
|
const cacheKey = m ? m[1] : "default";
|
|
3983
4066
|
const ttl = m && m[2] ? parseInt(m[2], 10) : 60;
|
|
4067
|
+
sweepExpiredCache(this.fragmentCache);
|
|
3984
4068
|
const cached = this.fragmentCache.get(cacheKey);
|
|
3985
4069
|
if (cached) {
|
|
3986
4070
|
const [htmlContent, expiresAt] = cached;
|
|
@@ -4028,6 +4112,7 @@ var init_engine = __esm({
|
|
|
4028
4112
|
i++;
|
|
4029
4113
|
}
|
|
4030
4114
|
const rendered = this.renderTokens([...bodyTokens], context);
|
|
4115
|
+
capCache(this.fragmentCache, TEMPLATE_CACHE_MAX);
|
|
4031
4116
|
this.fragmentCache.set(cacheKey, [rendered, Date.now() + ttl * 1e3]);
|
|
4032
4117
|
return [rendered, i];
|
|
4033
4118
|
}
|
|
@@ -39672,6 +39757,7 @@ var ai_exports = {};
|
|
|
39672
39757
|
__export(ai_exports, {
|
|
39673
39758
|
AI_TOOLS: () => AI_TOOLS,
|
|
39674
39759
|
DEV_SKILL: () => DEV_SKILL,
|
|
39760
|
+
downloadSkillsSync: () => downloadSkillsSync,
|
|
39675
39761
|
generateContext: () => generateContext,
|
|
39676
39762
|
hasMarkers: () => hasMarkers,
|
|
39677
39763
|
installAll: () => installAll,
|
|
@@ -39713,14 +39799,16 @@ function downloadSkillsSync(jobs) {
|
|
|
39713
39799
|
const jobs = JSON.parse(process.argv[1]);
|
|
39714
39800
|
const fs = require("node:fs");
|
|
39715
39801
|
const path = require("node:path");
|
|
39802
|
+
const transientStatuses = new Set([429, 500, 502, 503, 504]);
|
|
39716
39803
|
async function fetchOne(job) {
|
|
39717
39804
|
const resp = await fetch(job.url, { signal: AbortSignal.timeout(15000) });
|
|
39718
|
-
if (!resp.ok)
|
|
39805
|
+
if (!resp.ok) return { ok: false, retry: transientStatuses.has(resp.status) };
|
|
39719
39806
|
const buf = Buffer.from(await resp.arrayBuffer());
|
|
39720
39807
|
for (const dest of job.dests) {
|
|
39721
39808
|
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
39722
39809
|
fs.writeFileSync(dest, buf);
|
|
39723
39810
|
}
|
|
39811
|
+
return { ok: true, retry: false };
|
|
39724
39812
|
}
|
|
39725
39813
|
(async () => {
|
|
39726
39814
|
const ok = [];
|
|
@@ -39729,9 +39817,11 @@ function downloadSkillsSync(jobs) {
|
|
|
39729
39817
|
const failed = [];
|
|
39730
39818
|
await Promise.all(pending.map(async (job) => {
|
|
39731
39819
|
try {
|
|
39732
|
-
await fetchOne(job);
|
|
39733
|
-
ok.push(job.url);
|
|
39820
|
+
const result = await fetchOne(job);
|
|
39821
|
+
if (result.ok) ok.push(job.url);
|
|
39822
|
+
else if (result.retry) failed.push(job);
|
|
39734
39823
|
} catch {
|
|
39824
|
+
// DNS, TLS, timeout and connection failures are transient.
|
|
39735
39825
|
failed.push(job);
|
|
39736
39826
|
}
|
|
39737
39827
|
}));
|
|
@@ -1460,7 +1460,10 @@ var init_trustedProxy = __esm({
|
|
|
1460
1460
|
var engine_exports = {};
|
|
1461
1461
|
__export(engine_exports, {
|
|
1462
1462
|
Frond: () => Frond,
|
|
1463
|
+
MEMO_CACHE_MAX: () => MEMO_CACHE_MAX,
|
|
1463
1464
|
TEMPLATE_CACHE_MAX: () => TEMPLATE_CACHE_MAX,
|
|
1465
|
+
filterChainCache: () => filterChainCache,
|
|
1466
|
+
pathParseCache: () => pathParseCache,
|
|
1464
1467
|
setFormTokenSessionId: () => setFormTokenSessionId
|
|
1465
1468
|
});
|
|
1466
1469
|
import { createHash as createHash2, createHmac as createHmac2, randomBytes as randomBytes2 } from "node:crypto";
|
|
@@ -1564,6 +1567,12 @@ function capCache(cache, maxEntries) {
|
|
|
1564
1567
|
if (--drop <= 0) break;
|
|
1565
1568
|
}
|
|
1566
1569
|
}
|
|
1570
|
+
function sweepExpiredCache(cache) {
|
|
1571
|
+
const now = Date.now();
|
|
1572
|
+
for (const [key, [, expiresAt]] of cache) {
|
|
1573
|
+
if (expiresAt <= now) cache.delete(key);
|
|
1574
|
+
}
|
|
1575
|
+
}
|
|
1567
1576
|
function tokenize(source) {
|
|
1568
1577
|
const rawBlocks = [];
|
|
1569
1578
|
source = source.replace(RAW_BLOCK_RE, (_match, content) => {
|
|
@@ -1627,6 +1636,16 @@ function stripTag(raw) {
|
|
|
1627
1636
|
}
|
|
1628
1637
|
return [inner.trim(), stripBefore, stripAfter];
|
|
1629
1638
|
}
|
|
1639
|
+
function extendsTarget(source) {
|
|
1640
|
+
const matches = source.match(EXTENDS_RE_GLOBAL);
|
|
1641
|
+
if (matches && matches.length > 1) {
|
|
1642
|
+
throw new Error(
|
|
1643
|
+
`Frond: template has ${matches.length} "{% extends %}" tags -- a template can extend only one parent`
|
|
1644
|
+
);
|
|
1645
|
+
}
|
|
1646
|
+
const match = source.match(EXTENDS_RE);
|
|
1647
|
+
return match ? match[1] : "";
|
|
1648
|
+
}
|
|
1630
1649
|
function resolveVar(expr, context) {
|
|
1631
1650
|
expr = expr.trim();
|
|
1632
1651
|
if (expr.startsWith('"') && expr.endsWith('"') || expr.startsWith("'") && expr.endsWith("'")) {
|
|
@@ -1707,6 +1726,7 @@ function resolveVar(expr, context) {
|
|
|
1707
1726
|
fromBracket.push(false);
|
|
1708
1727
|
}
|
|
1709
1728
|
}
|
|
1729
|
+
capCache(pathParseCache, MEMO_CACHE_MAX);
|
|
1710
1730
|
pathParseCache.set(expr, [parts, fromBracket]);
|
|
1711
1731
|
}
|
|
1712
1732
|
let value = context;
|
|
@@ -2294,6 +2314,7 @@ function parseFilterChain(expr) {
|
|
|
2294
2314
|
}
|
|
2295
2315
|
}
|
|
2296
2316
|
const result = [variable, filters];
|
|
2317
|
+
capCache(filterChainCache, MEMO_CACHE_MAX);
|
|
2297
2318
|
filterChainCache.set(expr, result);
|
|
2298
2319
|
return result;
|
|
2299
2320
|
}
|
|
@@ -2473,7 +2494,7 @@ function _generateFormToken(descriptor = "") {
|
|
|
2473
2494
|
function _generateFormTokenValue(descriptor = "") {
|
|
2474
2495
|
return new SafeString(_buildFormTokenJwt(descriptor));
|
|
2475
2496
|
}
|
|
2476
|
-
var SafeString, KNOWN_TAGS, TERMINATOR_TAGS, GATEABLE_TAGS, BLOCK_TAG_ENDS, 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;
|
|
2497
|
+
var SafeString, KNOWN_TAGS, TERMINATOR_TAGS, GATEABLE_TAGS, BLOCK_TAG_ENDS, 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, EXTENDS_RE, EXTENDS_RE_GLOBAL, filterChainCache, pathParseCache, TEMPLATE_CACHE_MAX, MEMO_CACHE_MAX, TOKEN_RE, RAW_BLOCK_RE, VarRef, BUILTIN_FILTERS, _formTokenSessionId, Frond;
|
|
2477
2498
|
var init_engine = __esm({
|
|
2478
2499
|
"../frond/src/engine.ts"() {
|
|
2479
2500
|
"use strict";
|
|
@@ -2567,9 +2588,12 @@ var init_engine = __esm({
|
|
|
2567
2588
|
LIVE_RE = /^live\s+["']([^"']+)["']([\s\S]*)$/;
|
|
2568
2589
|
LIVE_WS_RE = /ws\s+["']([^"']+)["']/;
|
|
2569
2590
|
LIVE_SRC_RE = /src\s+["']([^"']+)["']/;
|
|
2591
|
+
EXTENDS_RE = /\{%[-\s]*extends\s+["'](.+?)["']\s*[-]?%\}/;
|
|
2592
|
+
EXTENDS_RE_GLOBAL = /\{%[-\s]*extends\s+["'](.+?)["']\s*[-]?%\}/g;
|
|
2570
2593
|
filterChainCache = /* @__PURE__ */ new Map();
|
|
2571
2594
|
pathParseCache = /* @__PURE__ */ new Map();
|
|
2572
2595
|
TEMPLATE_CACHE_MAX = 256;
|
|
2596
|
+
MEMO_CACHE_MAX = 1024;
|
|
2573
2597
|
TOKEN_RE = /(\{%-?\s*[\s\S]*?\s*-?%\})|(\{\{-?\s*[\s\S]*?\s*-?\}\})|(\{#[\s\S]*?#\})/g;
|
|
2574
2598
|
RAW_BLOCK_RE = /\{%-?\s*raw\s*-?%\}([\s\S]*?)\{%-?\s*endraw\s*-?%\}/g;
|
|
2575
2599
|
VarRef = class {
|
|
@@ -2922,29 +2946,22 @@ var init_engine = __esm({
|
|
|
2922
2946
|
return this;
|
|
2923
2947
|
}
|
|
2924
2948
|
/**
|
|
2925
|
-
* Register a custom filter
|
|
2926
|
-
*
|
|
2927
|
-
* the live instance's local filter map also receives the addition
|
|
2928
|
-
* immediately. Mirrors Python's _ClassOrInstanceMethod dual-call.
|
|
2949
|
+
* Register a custom filter on this instance only. Use the static method
|
|
2950
|
+
* for process-global registration. tina4: ADR-0052.
|
|
2929
2951
|
*/
|
|
2930
2952
|
addFilter(name, fn) {
|
|
2931
|
-
_Frond.classFilters.set(name, fn);
|
|
2932
2953
|
this.filters[name] = fn;
|
|
2933
2954
|
}
|
|
2934
2955
|
/**
|
|
2935
|
-
* Register a global variable
|
|
2936
|
-
* at class level — see ``addFilter`` for the dual-call semantics.
|
|
2956
|
+
* Register a global variable on this instance only.
|
|
2937
2957
|
*/
|
|
2938
2958
|
addGlobal(name, value) {
|
|
2939
|
-
_Frond.classGlobals.set(name, value);
|
|
2940
2959
|
this.globals[name] = value;
|
|
2941
2960
|
}
|
|
2942
2961
|
/**
|
|
2943
|
-
* Register a custom test
|
|
2944
|
-
* ``addFilter`` for the dual-call semantics.
|
|
2962
|
+
* Register a custom test on this instance only.
|
|
2945
2963
|
*/
|
|
2946
2964
|
addTest(name, fn) {
|
|
2947
|
-
_Frond.classTests.set(name, fn);
|
|
2948
2965
|
this.tests[name] = fn;
|
|
2949
2966
|
}
|
|
2950
2967
|
/**
|
|
@@ -3059,9 +3076,8 @@ var init_engine = __esm({
|
|
|
3059
3076
|
if (Object.keys(this.tests).length > 0) {
|
|
3060
3077
|
context.__frond_tests__ = this.tests;
|
|
3061
3078
|
}
|
|
3062
|
-
const
|
|
3063
|
-
if (
|
|
3064
|
-
const parentName = extendsMatch[1];
|
|
3079
|
+
const parentName = extendsTarget(source);
|
|
3080
|
+
if (parentName) {
|
|
3065
3081
|
const parentSource = this.load(parentName);
|
|
3066
3082
|
const childBlocks = this.extractBlocks(source);
|
|
3067
3083
|
return this.renderWithBlocks(parentSource, context, childBlocks);
|
|
@@ -3072,9 +3088,8 @@ var init_engine = __esm({
|
|
|
3072
3088
|
if (Object.keys(this.tests).length > 0) {
|
|
3073
3089
|
context.__frond_tests__ = this.tests;
|
|
3074
3090
|
}
|
|
3075
|
-
const
|
|
3076
|
-
if (
|
|
3077
|
-
const parentName = extendsMatch[1];
|
|
3091
|
+
const parentName = extendsTarget(source);
|
|
3092
|
+
if (parentName) {
|
|
3078
3093
|
const parentSource = this.load(parentName);
|
|
3079
3094
|
const childBlocks = this.extractBlocks(source);
|
|
3080
3095
|
return this.renderWithBlocks(parentSource, context, childBlocks);
|
|
@@ -3119,10 +3134,93 @@ var init_engine = __esm({
|
|
|
3119
3134
|
}
|
|
3120
3135
|
return blocks;
|
|
3121
3136
|
}
|
|
3137
|
+
/**
|
|
3138
|
+
* Depth-aware block substitution against `source` (typically the
|
|
3139
|
+
* fully-resolved root template).
|
|
3140
|
+
*
|
|
3141
|
+
* A single regex `.replace()` pass (the flat `pattern` this replaces in
|
|
3142
|
+
* renderWithBlocks) pairs an OUTER block's open tag with the FIRST
|
|
3143
|
+
* `{% endblock %}` found -- which, when the outer block wraps a NESTED
|
|
3144
|
+
* `{% block %}`, is the nested block's own close tag, not the outer's.
|
|
3145
|
+
* That silently truncates the outer block's captured content and drops
|
|
3146
|
+
* everything after the inner endblock (the root-nested-block
|
|
3147
|
+
* content-loss bug). This scans with an open/close depth counter
|
|
3148
|
+
* instead (mirroring extractBlocks), so an outer block always captures
|
|
3149
|
+
* its FULL body, nested child blocks included.
|
|
3150
|
+
*
|
|
3151
|
+
* The content chosen for each block -- the child override in `blocks`
|
|
3152
|
+
* if present, else the block's own default body -- is then recursively
|
|
3153
|
+
* substituted against the SAME `blocks` map before being tokenized and
|
|
3154
|
+
* rendered, so a block nested inside another block resolves correctly
|
|
3155
|
+
* regardless of which template in the inheritance chain declared the
|
|
3156
|
+
* nesting (the root, an intermediate, however many levels deep).
|
|
3157
|
+
*
|
|
3158
|
+
* `{{ parent() }}` / `{{ super() }}` inside a block still render that
|
|
3159
|
+
* block's OWN default content at this level (lazy, on first call).
|
|
3160
|
+
*/
|
|
3161
|
+
substituteBlocks(source, blocks, context) {
|
|
3162
|
+
const blockOpen = /\{%[-\s]*block\s+(\w+)\s*[-]?%\}/g;
|
|
3163
|
+
const blockClose = /\{%[-\s]*endblock\s*[-]?%\}/g;
|
|
3164
|
+
const engine = this;
|
|
3165
|
+
const pieces = [];
|
|
3166
|
+
let pos = 0;
|
|
3167
|
+
while (pos < source.length) {
|
|
3168
|
+
blockOpen.lastIndex = pos;
|
|
3169
|
+
const mOpen = blockOpen.exec(source);
|
|
3170
|
+
if (!mOpen) {
|
|
3171
|
+
pieces.push(source.slice(pos));
|
|
3172
|
+
break;
|
|
3173
|
+
}
|
|
3174
|
+
pieces.push(source.slice(pos, mOpen.index));
|
|
3175
|
+
const name = mOpen[1];
|
|
3176
|
+
const contentStart = mOpen.index + mOpen[0].length;
|
|
3177
|
+
let depth = 1;
|
|
3178
|
+
let scan = contentStart;
|
|
3179
|
+
let closeMatch = null;
|
|
3180
|
+
while (depth > 0 && scan < source.length) {
|
|
3181
|
+
blockOpen.lastIndex = scan;
|
|
3182
|
+
blockClose.lastIndex = scan;
|
|
3183
|
+
const nextOpen = blockOpen.exec(source);
|
|
3184
|
+
const nextClose = blockClose.exec(source);
|
|
3185
|
+
if (!nextClose) break;
|
|
3186
|
+
if (nextOpen && nextOpen.index < nextClose.index) {
|
|
3187
|
+
depth++;
|
|
3188
|
+
scan = nextOpen.index + nextOpen[0].length;
|
|
3189
|
+
} else {
|
|
3190
|
+
depth--;
|
|
3191
|
+
if (depth === 0) {
|
|
3192
|
+
closeMatch = nextClose;
|
|
3193
|
+
} else {
|
|
3194
|
+
scan = nextClose.index + nextClose[0].length;
|
|
3195
|
+
}
|
|
3196
|
+
}
|
|
3197
|
+
}
|
|
3198
|
+
if (!closeMatch) {
|
|
3199
|
+
pieces.push(source.slice(mOpen.index));
|
|
3200
|
+
pos = source.length;
|
|
3201
|
+
break;
|
|
3202
|
+
}
|
|
3203
|
+
const parentContent = source.slice(contentStart, closeMatch.index);
|
|
3204
|
+
const blockSource = blocks[name] ?? parentContent;
|
|
3205
|
+
const resolvedSource = engine.substituteBlocks(blockSource, blocks, context);
|
|
3206
|
+
let renderedParent = null;
|
|
3207
|
+
const getParent = () => {
|
|
3208
|
+
if (renderedParent === null) {
|
|
3209
|
+
renderedParent = new SafeString(
|
|
3210
|
+
engine.renderTokens(tokenize(parentContent), context)
|
|
3211
|
+
);
|
|
3212
|
+
}
|
|
3213
|
+
return renderedParent;
|
|
3214
|
+
};
|
|
3215
|
+
const blockCtx = { ...context, parent: getParent, super: getParent };
|
|
3216
|
+
pieces.push(engine.renderTokens(tokenize(resolvedSource), blockCtx));
|
|
3217
|
+
pos = closeMatch.index + closeMatch[0].length;
|
|
3218
|
+
}
|
|
3219
|
+
return pieces.join("");
|
|
3220
|
+
}
|
|
3122
3221
|
renderWithBlocks(parentSource, context, childBlocks) {
|
|
3123
|
-
const
|
|
3124
|
-
if (
|
|
3125
|
-
const grandparentName = extendsMatch[1];
|
|
3222
|
+
const grandparentName = extendsTarget(parentSource);
|
|
3223
|
+
if (grandparentName) {
|
|
3126
3224
|
const grandparentSource = this.load(grandparentName);
|
|
3127
3225
|
const parentBlocks = this.extractBlocks(parentSource);
|
|
3128
3226
|
const mergedBlocks = { ...parentBlocks, ...childBlocks };
|
|
@@ -3142,22 +3240,7 @@ var init_engine = __esm({
|
|
|
3142
3240
|
}
|
|
3143
3241
|
return this.renderWithBlocks(grandparentSource, context, mergedBlocks);
|
|
3144
3242
|
}
|
|
3145
|
-
const
|
|
3146
|
-
const engine = this;
|
|
3147
|
-
const result = parentSource.replace(pattern, (_match, name, parentContent) => {
|
|
3148
|
-
const blockSource = childBlocks[name] ?? parentContent;
|
|
3149
|
-
let renderedParent = null;
|
|
3150
|
-
const getParent = () => {
|
|
3151
|
-
if (renderedParent === null) {
|
|
3152
|
-
renderedParent = new SafeString(
|
|
3153
|
-
engine.renderTokens(tokenize(parentContent), context)
|
|
3154
|
-
);
|
|
3155
|
-
}
|
|
3156
|
-
return renderedParent;
|
|
3157
|
-
};
|
|
3158
|
-
const blockCtx = { ...context, parent: getParent, super: getParent };
|
|
3159
|
-
return this.renderTokens(tokenize(blockSource), blockCtx);
|
|
3160
|
-
});
|
|
3243
|
+
const result = this.substituteBlocks(parentSource, childBlocks, context);
|
|
3161
3244
|
return this.renderTokens(tokenize(result), context);
|
|
3162
3245
|
}
|
|
3163
3246
|
renderTokens(tokens, context) {
|
|
@@ -3980,6 +4063,7 @@ var init_engine = __esm({
|
|
|
3980
4063
|
const m = content.match(/^cache\s+["'](.+?)["']\s*(\d+)?/);
|
|
3981
4064
|
const cacheKey = m ? m[1] : "default";
|
|
3982
4065
|
const ttl = m && m[2] ? parseInt(m[2], 10) : 60;
|
|
4066
|
+
sweepExpiredCache(this.fragmentCache);
|
|
3983
4067
|
const cached = this.fragmentCache.get(cacheKey);
|
|
3984
4068
|
if (cached) {
|
|
3985
4069
|
const [htmlContent, expiresAt] = cached;
|
|
@@ -4027,6 +4111,7 @@ var init_engine = __esm({
|
|
|
4027
4111
|
i++;
|
|
4028
4112
|
}
|
|
4029
4113
|
const rendered = this.renderTokens([...bodyTokens], context);
|
|
4114
|
+
capCache(this.fragmentCache, TEMPLATE_CACHE_MAX);
|
|
4030
4115
|
this.fragmentCache.set(cacheKey, [rendered, Date.now() + ttl * 1e3]);
|
|
4031
4116
|
return [rendered, i];
|
|
4032
4117
|
}
|
|
@@ -39664,14 +39749,16 @@ function downloadSkillsSync(jobs) {
|
|
|
39664
39749
|
const jobs = JSON.parse(process.argv[1]);
|
|
39665
39750
|
const fs = require("node:fs");
|
|
39666
39751
|
const path = require("node:path");
|
|
39752
|
+
const transientStatuses = new Set([429, 500, 502, 503, 504]);
|
|
39667
39753
|
async function fetchOne(job) {
|
|
39668
39754
|
const resp = await fetch(job.url, { signal: AbortSignal.timeout(15000) });
|
|
39669
|
-
if (!resp.ok)
|
|
39755
|
+
if (!resp.ok) return { ok: false, retry: transientStatuses.has(resp.status) };
|
|
39670
39756
|
const buf = Buffer.from(await resp.arrayBuffer());
|
|
39671
39757
|
for (const dest of job.dests) {
|
|
39672
39758
|
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
39673
39759
|
fs.writeFileSync(dest, buf);
|
|
39674
39760
|
}
|
|
39761
|
+
return { ok: true, retry: false };
|
|
39675
39762
|
}
|
|
39676
39763
|
(async () => {
|
|
39677
39764
|
const ok = [];
|
|
@@ -39680,9 +39767,11 @@ function downloadSkillsSync(jobs) {
|
|
|
39680
39767
|
const failed = [];
|
|
39681
39768
|
await Promise.all(pending.map(async (job) => {
|
|
39682
39769
|
try {
|
|
39683
|
-
await fetchOne(job);
|
|
39684
|
-
ok.push(job.url);
|
|
39770
|
+
const result = await fetchOne(job);
|
|
39771
|
+
if (result.ok) ok.push(job.url);
|
|
39772
|
+
else if (result.retry) failed.push(job);
|
|
39685
39773
|
} catch {
|
|
39774
|
+
// DNS, TLS, timeout and connection failures are transient.
|
|
39686
39775
|
failed.push(job);
|
|
39687
39776
|
}
|
|
39688
39777
|
}));
|