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.
Files changed (97) hide show
  1. package/CLAUDE.md +24 -25
  2. package/package.json +1 -2
  3. package/packages/cli/dist/bin.js +20698 -18983
  4. package/packages/cli/src/bin.ts +28 -71
  5. package/packages/cli/src/commands/migrate.ts +36 -75
  6. package/packages/cli/src/commands/migrateRollback.ts +10 -1
  7. package/packages/cli/src/commands/test.ts +92 -21
  8. package/packages/core/dist/index.js +20561 -18828
  9. package/packages/core/public/js/tina4-dev-admin.min.js +23 -19
  10. package/packages/core/src/ai.ts +38 -13
  11. package/packages/core/src/api.ts +13 -5
  12. package/packages/core/src/background.ts +9 -3
  13. package/packages/core/src/devAdmin.ts +135 -20
  14. package/packages/core/src/dispatchPipeline.ts +185 -1
  15. package/packages/core/src/docs.ts +33 -5
  16. package/packages/core/src/env.ts +1 -1
  17. package/packages/core/src/errorOverlay.ts +39 -48
  18. package/packages/core/src/fakeData.ts +15 -0
  19. package/packages/core/src/index.ts +17 -6
  20. package/packages/core/src/logger.ts +892 -572
  21. package/packages/core/src/mcp.ts +9 -1
  22. package/packages/core/src/messenger.ts +31 -4
  23. package/packages/core/src/middleware.ts +169 -43
  24. package/packages/core/src/portTakeover.ts +232 -0
  25. package/packages/core/src/request.ts +57 -8
  26. package/packages/core/src/response.ts +67 -0
  27. package/packages/core/src/router.ts +35 -7
  28. package/packages/core/src/server.ts +450 -190
  29. package/packages/core/src/static.ts +81 -12
  30. package/packages/core/src/testClient.ts +126 -137
  31. package/packages/core/src/testing.ts +16 -12
  32. package/packages/core/src/types.ts +21 -9
  33. package/packages/core/src/version.ts +66 -0
  34. package/packages/core/src/websocket.ts +2 -2
  35. package/packages/core/src/websocketBackplane.ts +2 -2
  36. package/packages/frond/dist/index.js +149 -49
  37. package/packages/frond/src/engine.ts +234 -52
  38. package/packages/orm/dist/index.js +10941 -9231
  39. package/packages/orm/src/adapters/firebird.ts +200 -27
  40. package/packages/orm/src/adapters/mongodb.ts +160 -10
  41. package/packages/orm/src/adapters/mssql.ts +38 -11
  42. package/packages/orm/src/adapters/mysql.ts +24 -1
  43. package/packages/orm/src/adapters/odbc.ts +127 -29
  44. package/packages/orm/src/adapters/postgres.ts +18 -0
  45. package/packages/orm/src/adapters/sqlite.ts +93 -14
  46. package/packages/orm/src/autoCrud.ts +72 -8
  47. package/packages/orm/src/baseModel.ts +323 -71
  48. package/packages/orm/src/cachedDatabase.ts +48 -1
  49. package/packages/orm/src/database.ts +162 -59
  50. package/packages/orm/src/fakeData.ts +6 -2
  51. package/packages/orm/src/index.ts +4 -1
  52. package/packages/orm/src/migration.ts +95 -52
  53. package/packages/orm/src/query.ts +16 -4
  54. package/packages/orm/src/seeder.ts +43 -25
  55. package/packages/orm/src/sqlTranslator.ts +104 -19
  56. package/packages/orm/src/types.ts +97 -21
  57. package/packages/orm/src/validation.ts +5 -1
  58. package/packages/swagger/dist/index.js +3 -2
  59. package/packages/swagger/src/generator.ts +19 -4
  60. package/packages/swagger/src/ui.ts +6 -4
  61. package/types/cli/src/bin.d.ts +0 -22
  62. package/types/core/src/ai.d.ts +29 -0
  63. package/types/core/src/api.d.ts +11 -4
  64. package/types/core/src/background.d.ts +5 -2
  65. package/types/core/src/devAdmin.d.ts +35 -0
  66. package/types/core/src/dispatchPipeline.d.ts +41 -1
  67. package/types/core/src/errorOverlay.d.ts +13 -13
  68. package/types/core/src/index.d.ts +9 -6
  69. package/types/core/src/logger.d.ts +111 -185
  70. package/types/core/src/middleware.d.ts +40 -5
  71. package/types/core/src/portTakeover.d.ts +50 -0
  72. package/types/core/src/request.d.ts +15 -0
  73. package/types/core/src/response.d.ts +29 -0
  74. package/types/core/src/server.d.ts +92 -0
  75. package/types/core/src/testClient.d.ts +29 -3
  76. package/types/core/src/testing.d.ts +16 -12
  77. package/types/core/src/types.d.ts +21 -9
  78. package/types/core/src/version.d.ts +11 -0
  79. package/types/core/src/websocketBackplane.d.ts +1 -1
  80. package/types/frond/src/engine.d.ts +60 -8
  81. package/types/orm/src/adapters/firebird.d.ts +61 -2
  82. package/types/orm/src/adapters/mongodb.d.ts +20 -0
  83. package/types/orm/src/adapters/mssql.d.ts +11 -0
  84. package/types/orm/src/adapters/mysql.d.ts +11 -0
  85. package/types/orm/src/adapters/odbc.d.ts +35 -4
  86. package/types/orm/src/adapters/postgres.d.ts +11 -0
  87. package/types/orm/src/adapters/sqlite.d.ts +23 -4
  88. package/types/orm/src/baseModel.d.ts +45 -25
  89. package/types/orm/src/cachedDatabase.d.ts +27 -1
  90. package/types/orm/src/database.d.ts +56 -6
  91. package/types/orm/src/index.d.ts +3 -2
  92. package/types/orm/src/migration.d.ts +23 -5
  93. package/types/orm/src/query.d.ts +3 -0
  94. package/types/orm/src/seeder.d.ts +15 -2
  95. package/types/orm/src/sqlTranslator.d.ts +17 -4
  96. package/types/orm/src/types.d.ts +75 -16
  97. 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.warn(`WebSocket client ${client.id} backlog ${backlog}B exceeds limit, dropping slow client`);
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.warn(`WebSocket send to ${client.id} failed, pruning: ${(err as Error).message}`);
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?.warn(`WebSocket backplane publish failed: ${(err as Error).message}`),
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
- warn(message: string): void;
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, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
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, sep) {
445
- if (!expr.includes(sep)) return [expr];
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 = sep.length;
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(sep, i)) {
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, sep) => Array.isArray(v) ? v.map(String).join(sep !== void 0 ? String(sep) : ", ") : String(v),
1105
- split: (v, sep) => String(v).split(sep !== void 0 ? String(sep) : " "),
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 || "tina4-default-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. The filter is persisted at class level
1455
- * so new instances created by hot-reload inherit it automatically;
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 available in all templates. Persisted
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. Persisted at class level — see
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
- return readFileSync(filePath, "utf-8");
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 extendsMatch = source.match(/\{%[-\s]*extends\s+["'](.+?)["']\s*[-]?%\}/);
1574
- if (extendsMatch) {
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 extendsMatch = source.match(/\{%[-\s]*extends\s+["'](.+?)["']\s*[-]?%\}/);
1587
- if (extendsMatch) {
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 extendsMatch = parentSource.trimStart().match(/\{%[-\s]*extends\s+["'](.+?)["']\s*[-]?%\}/);
1635
- if (extendsMatch) {
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 pattern = /\{%[-\s]*block\s+(\w+)\s*[-]?%\}([\s\S]*?)\{%[-\s]*endblock\s*[-]?%\}/g;
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 new Error(`Template not found: ${join(this.templateDir, filename)}`);
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
  }