tina4-nodejs 3.13.98 → 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 +24 -25
- package/package.json +1 -2
- package/packages/cli/dist/bin.js +20698 -18983
- package/packages/cli/src/bin.ts +28 -71
- package/packages/cli/src/commands/migrate.ts +36 -75
- package/packages/cli/src/commands/migrateRollback.ts +10 -1
- package/packages/cli/src/commands/test.ts +92 -21
- package/packages/core/dist/index.js +20561 -18828
- package/packages/core/public/js/tina4-dev-admin.min.js +23 -19
- package/packages/core/src/ai.ts +38 -13
- package/packages/core/src/api.ts +13 -5
- package/packages/core/src/background.ts +9 -3
- package/packages/core/src/devAdmin.ts +135 -20
- package/packages/core/src/dispatchPipeline.ts +185 -1
- package/packages/core/src/docs.ts +33 -5
- package/packages/core/src/env.ts +1 -1
- package/packages/core/src/errorOverlay.ts +39 -48
- package/packages/core/src/fakeData.ts +15 -0
- package/packages/core/src/index.ts +17 -6
- package/packages/core/src/logger.ts +892 -572
- package/packages/core/src/mcp.ts +9 -1
- package/packages/core/src/messenger.ts +31 -4
- package/packages/core/src/middleware.ts +169 -43
- package/packages/core/src/portTakeover.ts +232 -0
- package/packages/core/src/request.ts +57 -8
- package/packages/core/src/response.ts +67 -0
- package/packages/core/src/router.ts +35 -7
- package/packages/core/src/server.ts +450 -190
- package/packages/core/src/static.ts +81 -12
- package/packages/core/src/testClient.ts +126 -137
- package/packages/core/src/testing.ts +16 -12
- package/packages/core/src/types.ts +21 -9
- package/packages/core/src/version.ts +66 -0
- package/packages/core/src/websocket.ts +2 -2
- package/packages/core/src/websocketBackplane.ts +2 -2
- package/packages/frond/dist/index.js +149 -49
- package/packages/frond/src/engine.ts +234 -52
- package/packages/orm/dist/index.js +10941 -9231
- package/packages/orm/src/adapters/firebird.ts +200 -27
- package/packages/orm/src/adapters/mongodb.ts +160 -10
- package/packages/orm/src/adapters/mssql.ts +38 -11
- package/packages/orm/src/adapters/mysql.ts +24 -1
- package/packages/orm/src/adapters/odbc.ts +127 -29
- package/packages/orm/src/adapters/postgres.ts +18 -0
- package/packages/orm/src/adapters/sqlite.ts +93 -14
- package/packages/orm/src/autoCrud.ts +72 -8
- package/packages/orm/src/baseModel.ts +323 -71
- package/packages/orm/src/cachedDatabase.ts +48 -1
- package/packages/orm/src/database.ts +162 -59
- package/packages/orm/src/fakeData.ts +6 -2
- package/packages/orm/src/index.ts +4 -1
- package/packages/orm/src/migration.ts +95 -52
- package/packages/orm/src/query.ts +16 -4
- package/packages/orm/src/seeder.ts +43 -25
- package/packages/orm/src/sqlTranslator.ts +104 -19
- package/packages/orm/src/types.ts +97 -21
- package/packages/orm/src/validation.ts +5 -1
- package/packages/swagger/dist/index.js +3 -2
- package/packages/swagger/src/generator.ts +19 -4
- package/packages/swagger/src/ui.ts +6 -4
- package/types/cli/src/bin.d.ts +0 -22
- package/types/core/src/ai.d.ts +29 -0
- package/types/core/src/api.d.ts +11 -4
- package/types/core/src/background.d.ts +5 -2
- package/types/core/src/devAdmin.d.ts +35 -0
- package/types/core/src/dispatchPipeline.d.ts +41 -1
- package/types/core/src/errorOverlay.d.ts +13 -13
- package/types/core/src/index.d.ts +9 -6
- package/types/core/src/logger.d.ts +111 -185
- package/types/core/src/middleware.d.ts +40 -5
- package/types/core/src/portTakeover.d.ts +50 -0
- package/types/core/src/request.d.ts +15 -0
- package/types/core/src/response.d.ts +29 -0
- package/types/core/src/server.d.ts +92 -0
- package/types/core/src/testClient.d.ts +29 -3
- package/types/core/src/testing.d.ts +16 -12
- package/types/core/src/types.d.ts +21 -9
- package/types/core/src/version.d.ts +11 -0
- package/types/core/src/websocketBackplane.d.ts +1 -1
- package/types/frond/src/engine.d.ts +60 -8
- package/types/orm/src/adapters/firebird.d.ts +61 -2
- package/types/orm/src/adapters/mongodb.d.ts +20 -0
- package/types/orm/src/adapters/mssql.d.ts +11 -0
- package/types/orm/src/adapters/mysql.d.ts +11 -0
- package/types/orm/src/adapters/odbc.d.ts +35 -4
- package/types/orm/src/adapters/postgres.d.ts +11 -0
- package/types/orm/src/adapters/sqlite.d.ts +23 -4
- package/types/orm/src/baseModel.d.ts +45 -25
- package/types/orm/src/cachedDatabase.d.ts +27 -1
- package/types/orm/src/database.d.ts +56 -6
- package/types/orm/src/index.d.ts +3 -2
- package/types/orm/src/migration.d.ts +23 -5
- package/types/orm/src/query.d.ts +3 -0
- package/types/orm/src/seeder.d.ts +15 -2
- package/types/orm/src/sqlTranslator.d.ts +17 -4
- package/types/orm/src/types.d.ts +75 -16
- package/packages/core/src/errorOverlay.test.ts +0 -122
|
@@ -676,13 +676,13 @@ export class WebSocketServer {
|
|
|
676
676
|
const backlog = client.socket.writableLength ?? 0;
|
|
677
677
|
const maxBacklog = parseInt(process.env.TINA4_WS_MAX_BACKLOG ?? "1048576", 10);
|
|
678
678
|
if (maxBacklog > 0 && backlog > maxBacklog) {
|
|
679
|
-
Log.
|
|
679
|
+
Log.warning(`WebSocket client ${client.id} backlog ${backlog}B exceeds limit, dropping slow client`);
|
|
680
680
|
this.pruneClient(client);
|
|
681
681
|
return false;
|
|
682
682
|
}
|
|
683
683
|
return true;
|
|
684
684
|
} catch (err) {
|
|
685
|
-
Log.
|
|
685
|
+
Log.warning(`WebSocket send to ${client.id} failed, pruning: ${(err as Error).message}`);
|
|
686
686
|
this.pruneClient(client);
|
|
687
687
|
return false;
|
|
688
688
|
}
|
|
@@ -348,7 +348,7 @@ export class WsBackplaneManager {
|
|
|
348
348
|
// publish() is async; we fire-and-forget but still catch a rejection so a
|
|
349
349
|
// dead bus can't produce an unhandled rejection that crashes the worker.
|
|
350
350
|
Promise.resolve(this.backplane.publish(this.channel, JSON.stringify(envelope))).catch(
|
|
351
|
-
(err) => log?.
|
|
351
|
+
(err) => log?.warning(`WebSocket backplane publish failed: ${(err as Error).message}`),
|
|
352
352
|
);
|
|
353
353
|
}
|
|
354
354
|
|
|
@@ -366,7 +366,7 @@ export class WsBackplaneManager {
|
|
|
366
366
|
/** Minimal logger shape so the manager doesn't import the logger module. */
|
|
367
367
|
export interface WsBackplaneLogger {
|
|
368
368
|
info(message: string): void;
|
|
369
|
-
|
|
369
|
+
warning(message: string): void;
|
|
370
370
|
error(message: string): void;
|
|
371
371
|
}
|
|
372
372
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// src/engine.ts
|
|
2
2
|
import { createHash, createHmac, randomBytes } from "node:crypto";
|
|
3
|
-
import { readFileSync, existsSync, statSync } from "node:fs";
|
|
4
|
-
import { join, resolve } from "node:path";
|
|
3
|
+
import { readFileSync, existsSync, statSync, realpathSync } from "node:fs";
|
|
4
|
+
import { join, resolve, isAbsolute, sep } from "node:path";
|
|
5
5
|
var SafeString = class {
|
|
6
6
|
constructor(value) {
|
|
7
7
|
this.value = value;
|
|
@@ -179,12 +179,15 @@ var THOUSANDS_RE = /\B(?=(\d{3})+(?!\d))/g;
|
|
|
179
179
|
var LIVE_RE = /^live\s+["']([^"']+)["']([\s\S]*)$/;
|
|
180
180
|
var LIVE_WS_RE = /ws\s+["']([^"']+)["']/;
|
|
181
181
|
var LIVE_SRC_RE = /src\s+["']([^"']+)["']/;
|
|
182
|
+
var EXTENDS_RE = /\{%[-\s]*extends\s+["'](.+?)["']\s*[-]?%\}/;
|
|
183
|
+
var EXTENDS_RE_GLOBAL = /\{%[-\s]*extends\s+["'](.+?)["']\s*[-]?%\}/g;
|
|
182
184
|
function liveAttr(value) {
|
|
183
185
|
return String(value).replace(/&/g, "&").replace(/"/g, """).replace(/</g, "<").replace(/>/g, ">");
|
|
184
186
|
}
|
|
185
187
|
var filterChainCache = /* @__PURE__ */ new Map();
|
|
186
188
|
var pathParseCache = /* @__PURE__ */ new Map();
|
|
187
189
|
var TEMPLATE_CACHE_MAX = 256;
|
|
190
|
+
var MEMO_CACHE_MAX = 1024;
|
|
188
191
|
function capCache(cache, maxEntries) {
|
|
189
192
|
if (cache.size < maxEntries) return;
|
|
190
193
|
let drop = Math.floor(maxEntries / 2);
|
|
@@ -193,6 +196,12 @@ function capCache(cache, maxEntries) {
|
|
|
193
196
|
if (--drop <= 0) break;
|
|
194
197
|
}
|
|
195
198
|
}
|
|
199
|
+
function sweepExpiredCache(cache) {
|
|
200
|
+
const now = Date.now();
|
|
201
|
+
for (const [key, [, expiresAt]] of cache) {
|
|
202
|
+
if (expiresAt <= now) cache.delete(key);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
196
205
|
var TOKEN_RE = /(\{%-?\s*[\s\S]*?\s*-?%\})|(\{\{-?\s*[\s\S]*?\s*-?\}\})|(\{#[\s\S]*?#\})/g;
|
|
197
206
|
var RAW_BLOCK_RE = /\{%-?\s*raw\s*-?%\}([\s\S]*?)\{%-?\s*endraw\s*-?%\}/g;
|
|
198
207
|
function tokenize(source) {
|
|
@@ -258,6 +267,16 @@ function stripTag(raw) {
|
|
|
258
267
|
}
|
|
259
268
|
return [inner.trim(), stripBefore, stripAfter];
|
|
260
269
|
}
|
|
270
|
+
function extendsTarget(source) {
|
|
271
|
+
const matches = source.match(EXTENDS_RE_GLOBAL);
|
|
272
|
+
if (matches && matches.length > 1) {
|
|
273
|
+
throw new Error(
|
|
274
|
+
`Frond: template has ${matches.length} "{% extends %}" tags -- a template can extend only one parent`
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
const match = source.match(EXTENDS_RE);
|
|
278
|
+
return match ? match[1] : "";
|
|
279
|
+
}
|
|
261
280
|
function resolveVar(expr, context) {
|
|
262
281
|
expr = expr.trim();
|
|
263
282
|
if (expr.startsWith('"') && expr.endsWith('"') || expr.startsWith("'") && expr.endsWith("'")) {
|
|
@@ -338,6 +357,7 @@ function resolveVar(expr, context) {
|
|
|
338
357
|
fromBracket.push(false);
|
|
339
358
|
}
|
|
340
359
|
}
|
|
360
|
+
capCache(pathParseCache, MEMO_CACHE_MAX);
|
|
341
361
|
pathParseCache.set(expr, [parts, fromBracket]);
|
|
342
362
|
}
|
|
343
363
|
let value = context;
|
|
@@ -441,15 +461,15 @@ function findOutsideQuotes(expr, needle) {
|
|
|
441
461
|
}
|
|
442
462
|
return -1;
|
|
443
463
|
}
|
|
444
|
-
function splitOutsideQuotes(expr,
|
|
445
|
-
if (!expr.includes(
|
|
464
|
+
function splitOutsideQuotes(expr, sep2) {
|
|
465
|
+
if (!expr.includes(sep2)) return [expr];
|
|
446
466
|
const parts = [];
|
|
447
467
|
let currentStart = 0;
|
|
448
468
|
let inQuote = null;
|
|
449
469
|
let depth = 0;
|
|
450
470
|
let bracketDepth = 0;
|
|
451
471
|
let i = 0;
|
|
452
|
-
const sepLen =
|
|
472
|
+
const sepLen = sep2.length;
|
|
453
473
|
const lastStart = expr.length - sepLen;
|
|
454
474
|
while (i <= lastStart) {
|
|
455
475
|
const ch = expr[i];
|
|
@@ -470,7 +490,7 @@ function splitOutsideQuotes(expr, sep) {
|
|
|
470
490
|
else if (ch === ")") depth--;
|
|
471
491
|
else if (ch === "[") bracketDepth++;
|
|
472
492
|
else if (ch === "]") bracketDepth--;
|
|
473
|
-
if (depth === 0 && bracketDepth === 0 && expr.startsWith(
|
|
493
|
+
if (depth === 0 && bracketDepth === 0 && expr.startsWith(sep2, i)) {
|
|
474
494
|
parts.push(expr.slice(currentStart, i));
|
|
475
495
|
i += sepLen;
|
|
476
496
|
currentStart = i;
|
|
@@ -925,6 +945,7 @@ function parseFilterChain(expr) {
|
|
|
925
945
|
}
|
|
926
946
|
}
|
|
927
947
|
const result = [variable, filters];
|
|
948
|
+
capCache(filterChainCache, MEMO_CACHE_MAX);
|
|
928
949
|
filterChainCache.set(expr, result);
|
|
929
950
|
return result;
|
|
930
951
|
}
|
|
@@ -1101,8 +1122,8 @@ var BUILTIN_FILTERS = {
|
|
|
1101
1122
|
},
|
|
1102
1123
|
first: (v) => Array.isArray(v) ? v[0] ?? null : null,
|
|
1103
1124
|
last: (v) => Array.isArray(v) ? v[v.length - 1] ?? null : null,
|
|
1104
|
-
join: (v,
|
|
1105
|
-
split: (v,
|
|
1125
|
+
join: (v, sep2) => Array.isArray(v) ? v.map(String).join(sep2 !== void 0 ? String(sep2) : ", ") : String(v),
|
|
1126
|
+
split: (v, sep2) => String(v).split(sep2 !== void 0 ? String(sep2) : " "),
|
|
1106
1127
|
replace: (v, from, to) => {
|
|
1107
1128
|
const s = String(v);
|
|
1108
1129
|
if (from !== void 0 && typeof from === "object" && from !== null && !Array.isArray(from)) {
|
|
@@ -1290,7 +1311,7 @@ function _b64url(data) {
|
|
|
1290
1311
|
}
|
|
1291
1312
|
var _formTokenSessionId = "";
|
|
1292
1313
|
function _buildFormTokenJwt(descriptor = "") {
|
|
1293
|
-
const secret = process.env.TINA4_SECRET
|
|
1314
|
+
const secret = process.env.TINA4_SECRET ?? "";
|
|
1294
1315
|
const ttlMinutes = parseInt(process.env.TINA4_TOKEN_LIMIT || "60", 10);
|
|
1295
1316
|
const header = { alg: "HS256", typ: "JWT" };
|
|
1296
1317
|
const now = Math.floor(Date.now() / 1e3);
|
|
@@ -1451,29 +1472,22 @@ var Frond = class _Frond {
|
|
|
1451
1472
|
return this;
|
|
1452
1473
|
}
|
|
1453
1474
|
/**
|
|
1454
|
-
* Register a custom filter
|
|
1455
|
-
*
|
|
1456
|
-
* the live instance's local filter map also receives the addition
|
|
1457
|
-
* immediately. Mirrors Python's _ClassOrInstanceMethod dual-call.
|
|
1475
|
+
* Register a custom filter on this instance only. Use the static method
|
|
1476
|
+
* for process-global registration. tina4: ADR-0052.
|
|
1458
1477
|
*/
|
|
1459
1478
|
addFilter(name, fn) {
|
|
1460
|
-
_Frond.classFilters.set(name, fn);
|
|
1461
1479
|
this.filters[name] = fn;
|
|
1462
1480
|
}
|
|
1463
1481
|
/**
|
|
1464
|
-
* Register a global variable
|
|
1465
|
-
* at class level — see ``addFilter`` for the dual-call semantics.
|
|
1482
|
+
* Register a global variable on this instance only.
|
|
1466
1483
|
*/
|
|
1467
1484
|
addGlobal(name, value) {
|
|
1468
|
-
_Frond.classGlobals.set(name, value);
|
|
1469
1485
|
this.globals[name] = value;
|
|
1470
1486
|
}
|
|
1471
1487
|
/**
|
|
1472
|
-
* Register a custom test
|
|
1473
|
-
* ``addFilter`` for the dual-call semantics.
|
|
1488
|
+
* Register a custom test on this instance only.
|
|
1474
1489
|
*/
|
|
1475
1490
|
addTest(name, fn) {
|
|
1476
|
-
_Frond.classTests.set(name, fn);
|
|
1477
1491
|
this.tests[name] = fn;
|
|
1478
1492
|
}
|
|
1479
1493
|
/**
|
|
@@ -1537,12 +1551,30 @@ var Frond = class _Frond {
|
|
|
1537
1551
|
renderDump(value) {
|
|
1538
1552
|
return renderDump(value).toString();
|
|
1539
1553
|
}
|
|
1554
|
+
/**
|
|
1555
|
+
* Load a template's source, CONFINED under the templates directory.
|
|
1556
|
+
*
|
|
1557
|
+
* Every path-taking tag ({% include %}, {% extends %}, {% import %},
|
|
1558
|
+
* {% from ... import %}) funnels through this one loader, so this single guard
|
|
1559
|
+
* confines them all (TAG-DEC-01): a name that is absolute, climbs out with a
|
|
1560
|
+
* `..` up-level segment, or resolves through a symlink to a location OUTSIDE
|
|
1561
|
+
* the templates root is REFUSED -- the outside file is never read. Template
|
|
1562
|
+
* -side analogue of the static-asset confinement (feature 41 / ADR-0050).
|
|
1563
|
+
*/
|
|
1540
1564
|
load(name) {
|
|
1565
|
+
if (isAbsolute(name) || name.split(/[\\/]/).includes("..")) {
|
|
1566
|
+
throw new Error(`Template path escapes the templates directory: ${name}`);
|
|
1567
|
+
}
|
|
1541
1568
|
const filePath = join(this.templateDir, name);
|
|
1542
|
-
if (!existsSync(filePath)) {
|
|
1569
|
+
if (!existsSync(filePath) || !statSync(filePath).isFile()) {
|
|
1543
1570
|
throw new Error(`Template not found: ${filePath}`);
|
|
1544
1571
|
}
|
|
1545
|
-
|
|
1572
|
+
const root = realpathSync(this.templateDir);
|
|
1573
|
+
const real = realpathSync(filePath);
|
|
1574
|
+
if (real !== root && !real.startsWith(root + sep)) {
|
|
1575
|
+
throw new Error(`Template path escapes the templates directory: ${name}`);
|
|
1576
|
+
}
|
|
1577
|
+
return readFileSync(real, "utf-8");
|
|
1546
1578
|
}
|
|
1547
1579
|
/** Execute pre-tokenized template against context. */
|
|
1548
1580
|
executeCached(tokens, context) {
|
|
@@ -1570,9 +1602,8 @@ var Frond = class _Frond {
|
|
|
1570
1602
|
if (Object.keys(this.tests).length > 0) {
|
|
1571
1603
|
context.__frond_tests__ = this.tests;
|
|
1572
1604
|
}
|
|
1573
|
-
const
|
|
1574
|
-
if (
|
|
1575
|
-
const parentName = extendsMatch[1];
|
|
1605
|
+
const parentName = extendsTarget(source);
|
|
1606
|
+
if (parentName) {
|
|
1576
1607
|
const parentSource = this.load(parentName);
|
|
1577
1608
|
const childBlocks = this.extractBlocks(source);
|
|
1578
1609
|
return this.renderWithBlocks(parentSource, context, childBlocks);
|
|
@@ -1583,9 +1614,8 @@ var Frond = class _Frond {
|
|
|
1583
1614
|
if (Object.keys(this.tests).length > 0) {
|
|
1584
1615
|
context.__frond_tests__ = this.tests;
|
|
1585
1616
|
}
|
|
1586
|
-
const
|
|
1587
|
-
if (
|
|
1588
|
-
const parentName = extendsMatch[1];
|
|
1617
|
+
const parentName = extendsTarget(source);
|
|
1618
|
+
if (parentName) {
|
|
1589
1619
|
const parentSource = this.load(parentName);
|
|
1590
1620
|
const childBlocks = this.extractBlocks(source);
|
|
1591
1621
|
return this.renderWithBlocks(parentSource, context, childBlocks);
|
|
@@ -1630,10 +1660,93 @@ var Frond = class _Frond {
|
|
|
1630
1660
|
}
|
|
1631
1661
|
return blocks;
|
|
1632
1662
|
}
|
|
1663
|
+
/**
|
|
1664
|
+
* Depth-aware block substitution against `source` (typically the
|
|
1665
|
+
* fully-resolved root template).
|
|
1666
|
+
*
|
|
1667
|
+
* A single regex `.replace()` pass (the flat `pattern` this replaces in
|
|
1668
|
+
* renderWithBlocks) pairs an OUTER block's open tag with the FIRST
|
|
1669
|
+
* `{% endblock %}` found -- which, when the outer block wraps a NESTED
|
|
1670
|
+
* `{% block %}`, is the nested block's own close tag, not the outer's.
|
|
1671
|
+
* That silently truncates the outer block's captured content and drops
|
|
1672
|
+
* everything after the inner endblock (the root-nested-block
|
|
1673
|
+
* content-loss bug). This scans with an open/close depth counter
|
|
1674
|
+
* instead (mirroring extractBlocks), so an outer block always captures
|
|
1675
|
+
* its FULL body, nested child blocks included.
|
|
1676
|
+
*
|
|
1677
|
+
* The content chosen for each block -- the child override in `blocks`
|
|
1678
|
+
* if present, else the block's own default body -- is then recursively
|
|
1679
|
+
* substituted against the SAME `blocks` map before being tokenized and
|
|
1680
|
+
* rendered, so a block nested inside another block resolves correctly
|
|
1681
|
+
* regardless of which template in the inheritance chain declared the
|
|
1682
|
+
* nesting (the root, an intermediate, however many levels deep).
|
|
1683
|
+
*
|
|
1684
|
+
* `{{ parent() }}` / `{{ super() }}` inside a block still render that
|
|
1685
|
+
* block's OWN default content at this level (lazy, on first call).
|
|
1686
|
+
*/
|
|
1687
|
+
substituteBlocks(source, blocks, context) {
|
|
1688
|
+
const blockOpen = /\{%[-\s]*block\s+(\w+)\s*[-]?%\}/g;
|
|
1689
|
+
const blockClose = /\{%[-\s]*endblock\s*[-]?%\}/g;
|
|
1690
|
+
const engine = this;
|
|
1691
|
+
const pieces = [];
|
|
1692
|
+
let pos = 0;
|
|
1693
|
+
while (pos < source.length) {
|
|
1694
|
+
blockOpen.lastIndex = pos;
|
|
1695
|
+
const mOpen = blockOpen.exec(source);
|
|
1696
|
+
if (!mOpen) {
|
|
1697
|
+
pieces.push(source.slice(pos));
|
|
1698
|
+
break;
|
|
1699
|
+
}
|
|
1700
|
+
pieces.push(source.slice(pos, mOpen.index));
|
|
1701
|
+
const name = mOpen[1];
|
|
1702
|
+
const contentStart = mOpen.index + mOpen[0].length;
|
|
1703
|
+
let depth = 1;
|
|
1704
|
+
let scan = contentStart;
|
|
1705
|
+
let closeMatch = null;
|
|
1706
|
+
while (depth > 0 && scan < source.length) {
|
|
1707
|
+
blockOpen.lastIndex = scan;
|
|
1708
|
+
blockClose.lastIndex = scan;
|
|
1709
|
+
const nextOpen = blockOpen.exec(source);
|
|
1710
|
+
const nextClose = blockClose.exec(source);
|
|
1711
|
+
if (!nextClose) break;
|
|
1712
|
+
if (nextOpen && nextOpen.index < nextClose.index) {
|
|
1713
|
+
depth++;
|
|
1714
|
+
scan = nextOpen.index + nextOpen[0].length;
|
|
1715
|
+
} else {
|
|
1716
|
+
depth--;
|
|
1717
|
+
if (depth === 0) {
|
|
1718
|
+
closeMatch = nextClose;
|
|
1719
|
+
} else {
|
|
1720
|
+
scan = nextClose.index + nextClose[0].length;
|
|
1721
|
+
}
|
|
1722
|
+
}
|
|
1723
|
+
}
|
|
1724
|
+
if (!closeMatch) {
|
|
1725
|
+
pieces.push(source.slice(mOpen.index));
|
|
1726
|
+
pos = source.length;
|
|
1727
|
+
break;
|
|
1728
|
+
}
|
|
1729
|
+
const parentContent = source.slice(contentStart, closeMatch.index);
|
|
1730
|
+
const blockSource = blocks[name] ?? parentContent;
|
|
1731
|
+
const resolvedSource = engine.substituteBlocks(blockSource, blocks, context);
|
|
1732
|
+
let renderedParent = null;
|
|
1733
|
+
const getParent = () => {
|
|
1734
|
+
if (renderedParent === null) {
|
|
1735
|
+
renderedParent = new SafeString(
|
|
1736
|
+
engine.renderTokens(tokenize(parentContent), context)
|
|
1737
|
+
);
|
|
1738
|
+
}
|
|
1739
|
+
return renderedParent;
|
|
1740
|
+
};
|
|
1741
|
+
const blockCtx = { ...context, parent: getParent, super: getParent };
|
|
1742
|
+
pieces.push(engine.renderTokens(tokenize(resolvedSource), blockCtx));
|
|
1743
|
+
pos = closeMatch.index + closeMatch[0].length;
|
|
1744
|
+
}
|
|
1745
|
+
return pieces.join("");
|
|
1746
|
+
}
|
|
1633
1747
|
renderWithBlocks(parentSource, context, childBlocks) {
|
|
1634
|
-
const
|
|
1635
|
-
if (
|
|
1636
|
-
const grandparentName = extendsMatch[1];
|
|
1748
|
+
const grandparentName = extendsTarget(parentSource);
|
|
1749
|
+
if (grandparentName) {
|
|
1637
1750
|
const grandparentSource = this.load(grandparentName);
|
|
1638
1751
|
const parentBlocks = this.extractBlocks(parentSource);
|
|
1639
1752
|
const mergedBlocks = { ...parentBlocks, ...childBlocks };
|
|
@@ -1653,22 +1766,7 @@ var Frond = class _Frond {
|
|
|
1653
1766
|
}
|
|
1654
1767
|
return this.renderWithBlocks(grandparentSource, context, mergedBlocks);
|
|
1655
1768
|
}
|
|
1656
|
-
const
|
|
1657
|
-
const engine = this;
|
|
1658
|
-
const result = parentSource.replace(pattern, (_match, name, parentContent) => {
|
|
1659
|
-
const blockSource = childBlocks[name] ?? parentContent;
|
|
1660
|
-
let renderedParent = null;
|
|
1661
|
-
const getParent = () => {
|
|
1662
|
-
if (renderedParent === null) {
|
|
1663
|
-
renderedParent = new SafeString(
|
|
1664
|
-
engine.renderTokens(tokenize(parentContent), context)
|
|
1665
|
-
);
|
|
1666
|
-
}
|
|
1667
|
-
return renderedParent;
|
|
1668
|
-
};
|
|
1669
|
-
const blockCtx = { ...context, parent: getParent, super: getParent };
|
|
1670
|
-
return this.renderTokens(tokenize(blockSource), blockCtx);
|
|
1671
|
-
});
|
|
1769
|
+
const result = this.substituteBlocks(parentSource, childBlocks, context);
|
|
1672
1770
|
return this.renderTokens(tokenize(result), context);
|
|
1673
1771
|
}
|
|
1674
1772
|
renderTokens(tokens, context) {
|
|
@@ -2309,9 +2407,9 @@ var Frond = class _Frond {
|
|
|
2309
2407
|
let source;
|
|
2310
2408
|
try {
|
|
2311
2409
|
source = this.load(filename);
|
|
2312
|
-
} catch {
|
|
2410
|
+
} catch (err) {
|
|
2313
2411
|
if (ignoreMissing) return "";
|
|
2314
|
-
throw
|
|
2412
|
+
throw err;
|
|
2315
2413
|
}
|
|
2316
2414
|
const incContext = { ...context };
|
|
2317
2415
|
if (withExpr) {
|
|
@@ -2491,6 +2589,7 @@ var Frond = class _Frond {
|
|
|
2491
2589
|
const m = content.match(/^cache\s+["'](.+?)["']\s*(\d+)?/);
|
|
2492
2590
|
const cacheKey = m ? m[1] : "default";
|
|
2493
2591
|
const ttl = m && m[2] ? parseInt(m[2], 10) : 60;
|
|
2592
|
+
sweepExpiredCache(this.fragmentCache);
|
|
2494
2593
|
const cached = this.fragmentCache.get(cacheKey);
|
|
2495
2594
|
if (cached) {
|
|
2496
2595
|
const [htmlContent, expiresAt] = cached;
|
|
@@ -2538,6 +2637,7 @@ var Frond = class _Frond {
|
|
|
2538
2637
|
i++;
|
|
2539
2638
|
}
|
|
2540
2639
|
const rendered = this.renderTokens([...bodyTokens], context);
|
|
2640
|
+
capCache(this.fragmentCache, TEMPLATE_CACHE_MAX);
|
|
2541
2641
|
this.fragmentCache.set(cacheKey, [rendered, Date.now() + ttl * 1e3]);
|
|
2542
2642
|
return [rendered, i];
|
|
2543
2643
|
}
|