@barefootjs/mojolicious 0.17.0 → 0.18.0

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/dist/index.js CHANGED
@@ -2,23 +2,24 @@
2
2
  import {
3
3
  BaseAdapter,
4
4
  isBooleanAttr,
5
- parseExpression as parseExpression3,
5
+ parseExpression as parseExpression2,
6
6
  stringifyParsedExpr as stringifyParsedExpr2,
7
7
  parseStyleObjectEntries,
8
- isSupported as isSupported2,
8
+ isSupported,
9
9
  exprToString,
10
10
  parseProviderObjectLiteral,
11
11
  emitParsedExpr as emitParsedExpr2,
12
12
  emitIRNode,
13
13
  emitAttrValue,
14
14
  augmentInheritedPropAccesses,
15
- isLowerableObjectRestDestructure,
16
15
  collectModuleStringConsts,
17
16
  lookupStaticRecordLiteral,
18
17
  searchParamsLocalNames,
19
18
  prepareLoweringMatchers,
20
19
  queryHrefArgs,
21
- sortComparatorFromArrow as sortComparatorFromArrow2
20
+ isValidHelperId,
21
+ sortComparatorFromArrow as sortComparatorFromArrow2,
22
+ isLowerableLoopDestructure
22
23
  } from "@barefootjs/jsx";
23
24
 
24
25
  // src/adapter/boolean-result.ts
@@ -128,13 +129,6 @@ function collectRootScopeNodes(node) {
128
129
  visit(node);
129
130
  return out;
130
131
  }
131
- function referencedVarsAreAvailable(expr, available) {
132
- for (const m of expr.matchAll(/\$([A-Za-z_]\w*)/g)) {
133
- if (!available.has(m[1]))
134
- return false;
135
- }
136
- return true;
137
- }
138
132
 
139
133
  // src/adapter/expr/array-method.ts
140
134
  import {
@@ -307,6 +301,13 @@ function renderFlatMapEval(recv, body, param, emit) {
307
301
  const env = emitEvalEnvArg(body, [param], emit);
308
302
  return `bf->flat_map_eval(${recv}, '${escapePerlSingleQuote(json)}', '${param}', ${env})`;
309
303
  }
304
+ function renderMapEval(recv, body, param, emit) {
305
+ const json = serializeParsedExpr(body);
306
+ if (json === null)
307
+ return null;
308
+ const env = emitEvalEnvArg(body, [param], emit);
309
+ return `bf->map_eval(${recv}, '${escapePerlSingleQuote(json)}', '${param}', ${env})`;
310
+ }
310
311
  function renderSortMethod(recv, c) {
311
312
  const keyHashes = c.keys.map((k) => {
312
313
  const keyEntry = k.key.kind === "self" ? `key_kind => 'self'` : `key_kind => 'field', key => '${k.key.field}'`;
@@ -314,7 +315,10 @@ function renderSortMethod(recv, c) {
314
315
  });
315
316
  return `bf->sort(${recv}, { keys => [${keyHashes.join(", ")}] })`;
316
317
  }
317
- function renderFlatMethod(recv, depth) {
318
+ function renderFlatMethod(recv, depth, emit) {
319
+ if (typeof depth === "object") {
320
+ return `bf->flat_dynamic(${recv}, ${emit(depth.expr)})`;
321
+ }
318
322
  const d = depth === "infinity" ? -1 : depth;
319
323
  return `bf->flat(${recv}, ${d})`;
320
324
  }
@@ -360,10 +364,12 @@ class MojoFilterEmitter {
360
364
  param;
361
365
  localVarMap;
362
366
  isStringName;
363
- constructor(param, localVarMap, isStringName = () => false) {
367
+ onUnsupported;
368
+ constructor(param, localVarMap, isStringName = () => false, onUnsupported) {
364
369
  this.param = param;
365
370
  this.localVarMap = localVarMap;
366
371
  this.isStringName = isStringName;
372
+ this.onUnsupported = onUnsupported;
367
373
  }
368
374
  identifier(name) {
369
375
  if (name === this.param)
@@ -442,12 +448,14 @@ class MojoFilterEmitter {
442
448
  return `(${l} // ${r})`;
443
449
  }
444
450
  callbackMethod(method, object, arrow, _restArgs, emit) {
445
- if (!PREDICATE_METHODS.has(method))
451
+ if (!PREDICATE_METHODS.has(method)) {
452
+ this.onUnsupported?.(`Filter predicate contains a nested '.${method}(...)' callback, which has no Perl scalar form`, `Rewrite the predicate without a nested callback method, or add /* @client */ for client-only evaluation (no SSR).`);
446
453
  return "1";
454
+ }
447
455
  const param = arrow.params[0];
448
456
  const predicate = arrow.body;
449
457
  const arrayExpr = emit(object);
450
- const predBody = emitParsedExpr(predicate, new MojoFilterEmitter(param, this.localVarMap, this.isStringName));
458
+ const predBody = emitParsedExpr(predicate, new MojoFilterEmitter(param, this.localVarMap, this.isStringName, this.onUnsupported));
451
459
  const grepBody = predBody.replace(new RegExp(`\\$${param}\\b`, "g"), "$_");
452
460
  if (method === "filter")
453
461
  return `[grep { ${grepBody} } @{${arrayExpr}}]`;
@@ -455,6 +463,7 @@ class MojoFilterEmitter {
455
463
  return `!(grep { !(${grepBody}) } @{${arrayExpr}})`;
456
464
  if (method === "some")
457
465
  return `!!(grep { ${grepBody} } @{${arrayExpr}})`;
466
+ this.onUnsupported?.(`Filter predicate contains a nested '.${method}(...)' callback, which has no Perl scalar form`, `Rewrite the predicate without a nested callback method, or add /* @client */ for client-only evaluation (no SSR).`);
458
467
  return arrayExpr;
459
468
  }
460
469
  arrayLiteral(elements, emit) {
@@ -464,7 +473,7 @@ class MojoFilterEmitter {
464
473
  return renderArrayMethod(method, object, args, emit);
465
474
  }
466
475
  flatMethod(object, depth, emit) {
467
- return renderFlatMethod(emit(object), depth);
476
+ return renderFlatMethod(emit(object), depth, emit);
468
477
  }
469
478
  conditional(_test, _consequent, _alternate) {
470
479
  return "1";
@@ -620,6 +629,13 @@ class MojoTopLevelEmitter {
620
629
  this.ctx._recordExprBF101(`.flatMap(...) projection is not lowerable to a template flat-map`, `Pre-compute the projection in the route handler, or mark the loop @client-only.`);
621
630
  return "''";
622
631
  }
632
+ if (method === "map") {
633
+ const evalForm = renderMapEval(recv, body, params[0], emit);
634
+ if (evalForm !== null)
635
+ return evalForm;
636
+ this.ctx._recordExprBF101(`.map(...) projection is not lowerable to a template map`, `Pre-compute the projection in the route handler, or mark the position @client-only.`);
637
+ return "''";
638
+ }
623
639
  const cb = {
624
640
  method,
625
641
  object,
@@ -672,7 +688,7 @@ class MojoTopLevelEmitter {
672
688
  return renderArrayMethod(method, object, args, emit);
673
689
  }
674
690
  flatMethod(object, depth, emit) {
675
- return renderFlatMethod(emit(object), depth);
691
+ return renderFlatMethod(emit(object), depth, emit);
676
692
  }
677
693
  conditional(test, consequent, alternate, emit) {
678
694
  return `(${emit(test)} ? ${emit(consequent)} : ${emit(alternate)})`;
@@ -703,8 +719,8 @@ class MojoTopLevelEmitter {
703
719
  unsupported(_raw, _reason) {
704
720
  return "''";
705
721
  }
706
- objectLiteral(_properties, _raw, _emit) {
707
- return "''";
722
+ objectLiteral(properties, _raw, _emit) {
723
+ return properties.length === 0 ? "{}" : "''";
708
724
  }
709
725
  }
710
726
 
@@ -863,9 +879,7 @@ function recordIndexAccessToPerl(ctx, val) {
863
879
  // src/adapter/memo/seed.ts
864
880
  import {
865
881
  collectContextConsumers,
866
- extractArrowBodyExpression,
867
- isSupported,
868
- parseExpression as parseExpression2
882
+ computeSsrSeedPlan
869
883
  } from "@barefootjs/jsx";
870
884
  function contextDefaultPerl(c) {
871
885
  const d = c.defaultValue;
@@ -886,42 +900,20 @@ function generateContextConsumerSeed(ir) {
886
900
  `;
887
901
  }
888
902
  function generateDerivedMemoSeed(ctx, ir) {
889
- const memos = ir.metadata.memos ?? [];
890
- const signals = ir.metadata.signals ?? [];
891
- if (memos.length === 0 && signals.length === 0)
892
- return "";
893
- const available = new Set(ir.metadata.propsParams.map((p) => p.name));
903
+ const plan = ir.metadata.ssrSeedPlan ?? computeSsrSeedPlan(ir.metadata);
894
904
  const lines = [];
895
- for (const signal of signals) {
896
- const perl = tryLowerToPerl(ctx, signal.initialValue, available);
897
- if (perl !== null)
898
- lines.push(`% my $${signal.getter} = ${perl};`);
899
- available.add(signal.getter);
900
- }
901
- for (const memo of memos) {
902
- const body = extractArrowBodyExpression(memo.computation);
903
- if (body !== null) {
904
- const perl = tryLowerToPerl(ctx, body, available);
905
- if (perl !== null)
906
- lines.push(`% my $${memo.name} = ${perl};`);
907
- }
908
- available.add(memo.name);
905
+ for (const step of plan.steps) {
906
+ if (step.kind !== "derived")
907
+ continue;
908
+ const perl = ctx.convertExpressionToPerl(step.expr, step.parsed);
909
+ if (perl === "" || !/\$[A-Za-z_]\w*/.test(perl))
910
+ continue;
911
+ lines.push(`% my $${step.name} = ${perl};`);
909
912
  }
910
913
  return lines.length > 0 ? lines.join(`
911
914
  `) + `
912
915
  ` : "";
913
916
  }
914
- function tryLowerToPerl(ctx, expr, available) {
915
- const trimmed = expr.trim();
916
- if (!trimmed)
917
- return null;
918
- if (!isSupported(parseExpression2(trimmed)).supported)
919
- return null;
920
- const perl = ctx.convertExpressionToPerl(trimmed);
921
- if (perl === "" || !/\$[A-Za-z_]\w*/.test(perl))
922
- return null;
923
- return referencedVarsAreAvailable(perl, available) ? perl : null;
924
- }
925
917
 
926
918
  // src/adapter/value/parsed-literal.ts
927
919
  function isStringTypeInfo(type) {
@@ -963,6 +955,17 @@ function collectStringValueNames(ir) {
963
955
  }
964
956
 
965
957
  // src/adapter/mojo-adapter.ts
958
+ function perlSegmentAccessor(base, segments) {
959
+ let expr = base;
960
+ for (const seg of segments) {
961
+ expr += seg.kind === "field" ? `->{${seg.isIdent ? seg.key : perlHashKey(seg.key)}}` : `->[${seg.index}]`;
962
+ }
963
+ return expr;
964
+ }
965
+ function perlStringLiteral(s) {
966
+ return `'${s.replace(/\\/g, "\\\\").replace(/'/g, "\\'")}'`;
967
+ }
968
+
966
969
  class MojoAdapter extends BaseAdapter {
967
970
  name = "mojolicious";
968
971
  extension = ".html.ep";
@@ -1043,7 +1046,7 @@ class MojoAdapter extends BaseAdapter {
1043
1046
  return this.booleanTypedProps.has(bare);
1044
1047
  }
1045
1048
  parseUndefinedAlternateTernary(expr) {
1046
- const parsed = parseExpression3(expr.trim());
1049
+ const parsed = parseExpression2(expr.trim());
1047
1050
  if (parsed?.kind !== "conditional")
1048
1051
  return null;
1049
1052
  const alt = parsed.alternate;
@@ -1291,24 +1294,41 @@ ${whenTrue}
1291
1294
  return `<%== bf->comment("cond-start:${condId}") %>${content}<%== bf->comment("cond-end:${condId}") %>`;
1292
1295
  }
1293
1296
  renderLoop(loop) {
1294
- if (loop.clientOnly)
1295
- return "";
1297
+ if (loop.clientOnly) {
1298
+ return `<%== bf->comment("loop:${loop.markerId}") %><%== bf->comment("/loop:${loop.markerId}") %>`;
1299
+ }
1296
1300
  const destructure = !!(loop.paramBindings && loop.paramBindings.length > 0);
1297
- const supportableDestructure = destructure && isLowerableObjectRestDestructure(loop);
1301
+ const supportableDestructure = destructure && isLowerableLoopDestructure(loop);
1298
1302
  if (destructure && !supportableDestructure) {
1299
1303
  this.errors.push({
1300
1304
  code: "BF104",
1301
1305
  severity: "error",
1302
- message: `Loop callback uses an array/object destructure pattern (\`${loop.param}\`) that the Mojo adapter cannot lower — Perl scalar bindings can't unpack a tuple in a single \`my\` declaration.`,
1306
+ message: `Loop callback uses a destructure pattern (\`${loop.param}\`) that the Mojo adapter cannot lower — see the diagnostic detail for the specific shape.`,
1303
1307
  loc: loop.loc ?? { file: this.componentName + ".tsx", start: { line: 1, column: 0 }, end: { line: 1, column: 0 } },
1304
1308
  suggestion: {
1305
1309
  message: `Options:
1306
- ` + ` 1. Rename the parameter to a single name and access tuple elements with index syntax in the body (e.g. \`entry => entry->[0]\` instead of \`([k, v]) => ...\`).
1307
- ` + ` 2. Mark the loop position as @client-only so the destructure runs in JS on the client.
1308
- ` + ` 3. Move the loop into a primitive that the adapter registers explicitly.`
1310
+ ` + ` 1. If this is an object-rest binding (\`{ ...rest }\`), only reading \`rest.field\` or spreading \`{...rest}\` onto an intrinsic element lowers — other uses (passing \`rest\` to a function, rendering it as text) need the client runtime.
1311
+ ` + ` 2. If this is chained \`.filter().map(({ ... }) => ...)\`, hoist the destructure into a variable inside the callback body instead.
1312
+ ` + ` 3. Mark the loop position as @client-only so the destructure runs in JS on the client.
1313
+ ` + ` 4. Move the loop into a primitive that the adapter registers explicitly.`
1309
1314
  }
1310
1315
  });
1311
1316
  }
1317
+ const arrayName = loop.array.trim();
1318
+ if (/^[A-Za-z_$][\w$]*$/.test(arrayName)) {
1319
+ const arrayConst = (this.localConstants ?? []).find((c) => c.name === arrayName);
1320
+ if (arrayConst && !arrayConst.isModule && this.resolveLiteralConst(arrayName) === null) {
1321
+ this.errors.push({
1322
+ code: "BF101",
1323
+ severity: "error",
1324
+ message: `Loop array \`${arrayName}\` is a local computed value (\`${arrayConst.value}\`) that the Mojo adapter cannot bind as a template variable — only numeric/string-literal locals inline at their use site.`,
1325
+ loc: loop.loc ?? { file: this.componentName + ".tsx", start: { line: 1, column: 0 }, end: { line: 1, column: 0 } },
1326
+ suggestion: {
1327
+ message: "Pre-compute the array server-side and pass it as a prop, or mark the loop position as @client-only so it runs in JS on the client."
1328
+ }
1329
+ });
1330
+ }
1331
+ }
1312
1332
  const rawArray = this.convertExpressionToPerl(loop.array);
1313
1333
  let sortedHoist = null;
1314
1334
  let array = rawArray;
@@ -1363,7 +1383,15 @@ ${renderedChildren}` : renderedChildren;
1363
1383
  if (supportableDestructure) {
1364
1384
  lines.push(`% my $__bf_item = ${array}->[${indexVar}];`);
1365
1385
  for (const b of loop.paramBindings ?? []) {
1366
- lines.push(b.rest ? `% my $${b.name} = $__bf_item;` : `% my $${b.name} = $__bf_item->{${b.path.slice(1)}};`);
1386
+ const parent = perlSegmentAccessor("$__bf_item", b.segments ?? []);
1387
+ if (b.rest?.kind === "object") {
1388
+ const exclude = b.rest.exclude.map((k) => perlStringLiteral(k.key)).join(", ");
1389
+ lines.push(`% my $${b.name} = bf->omit(${parent}, [${exclude}]);`);
1390
+ } else if (b.rest?.kind === "array") {
1391
+ lines.push(`% my $${b.name} = bf->slice(${parent}, ${b.rest.from}, undef);`);
1392
+ } else {
1393
+ lines.push(`% my $${b.name} = ${perlSegmentAccessor("$__bf_item", b.segments ?? [])};`);
1394
+ }
1367
1395
  }
1368
1396
  } else {
1369
1397
  lines.push(`% my $${param} = ${array}->[${indexVar}];`);
@@ -1547,7 +1575,7 @@ ${children}`;
1547
1575
  if (localConst?.value !== undefined) {
1548
1576
  const initTrimmed = localConst.value.trim();
1549
1577
  if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(initTrimmed)) {
1550
- const resolved = conditionalSpreadToPerl(this.spreadCtx, parseExpression3(initTrimmed));
1578
+ const resolved = conditionalSpreadToPerl(this.spreadCtx, parseExpression2(initTrimmed));
1551
1579
  if (resolved !== null) {
1552
1580
  return `<%== bf->spread_attrs(${resolved}) %>`;
1553
1581
  }
@@ -1565,7 +1593,7 @@ ${children}`;
1565
1593
  if (!entries)
1566
1594
  return null;
1567
1595
  for (const e of entries) {
1568
- if (e.kind === "expr" && !isSupported2(parseExpression3(e.expr)).supported)
1596
+ if (e.kind === "expr" && !isSupported(parseExpression2(e.expr)).supported)
1569
1597
  return null;
1570
1598
  }
1571
1599
  return entries.map((e) => e.kind === "literal" ? `${this.escapeAttrText(e.cssKey)}:${this.escapeAttrText(e.value)}` : `${this.escapeAttrText(e.cssKey)}:<%= ${this.convertExpressionToPerl(e.expr)} %>`).join(";");
@@ -1601,7 +1629,7 @@ ${children}`;
1601
1629
  return `${BF_COND}="${condId}"`;
1602
1630
  }
1603
1631
  renderPerlFilterExpr(expr, param, localVarMap = new Map) {
1604
- return emitParsedExpr2(expr, new MojoFilterEmitter(param, localVarMap, (n) => this._isStringValueName(n)));
1632
+ return emitParsedExpr2(expr, new MojoFilterEmitter(param, localVarMap, (n) => this._isStringValueName(n), (message, reason) => this._recordExprBF101(message, reason)));
1605
1633
  }
1606
1634
  convertTemplateLiteralPartsToPerl(literalParts) {
1607
1635
  const parts = [];
@@ -1646,8 +1674,8 @@ ${children}`;
1646
1674
  const hasTaggedTemplate = /[A-Za-z_$][\w$]*\s*`/.test(probe);
1647
1675
  if (!startsAsObjectLiteral && !hasTaggedTemplate)
1648
1676
  return false;
1649
- const parsed = parseExpression3(expr.trim());
1650
- const support = isSupported2(parsed);
1677
+ const parsed = parseExpression2(expr.trim());
1678
+ const support = isSupported(parsed);
1651
1679
  if (parsed.kind !== "unsupported" && support.supported)
1652
1680
  return false;
1653
1681
  const reason = support.reason ?? (parsed.kind === "unsupported" ? parsed.reason : undefined);
@@ -1695,7 +1723,7 @@ ${reason}` : "";
1695
1723
  const trimmed = expr.trim();
1696
1724
  if (trimmed === "")
1697
1725
  return "''";
1698
- parsed = parseExpression3(trimmed);
1726
+ parsed = parseExpression2(trimmed);
1699
1727
  }
1700
1728
  if (parsed.kind === "call") {
1701
1729
  for (const matcher of this._loweringMatchers) {
@@ -1704,9 +1732,13 @@ ${reason}` : "";
1704
1732
  const argsGo = queryHrefArgs(node, (n) => this.renderParsedExprToPerl(n));
1705
1733
  return `bf->query(${argsGo.join(", ")})`;
1706
1734
  }
1735
+ if (node?.kind === "helper-call" && isValidHelperId(node.helper)) {
1736
+ const argsX = node.args.map((a) => this.renderParsedExprToPerl(a));
1737
+ return `bf->${node.helper}(${argsX.join(", ")})`;
1738
+ }
1707
1739
  }
1708
1740
  }
1709
- const support = isSupported2(parsed);
1741
+ const support = isSupported(parsed);
1710
1742
  if (!support.supported) {
1711
1743
  this.errors.push({
1712
1744
  code: "BF101",
@@ -1755,7 +1787,23 @@ Options:
1755
1787
  }
1756
1788
  }
1757
1789
  var mojoAdapter = new MojoAdapter;
1790
+ // src/conformance-pins.ts
1791
+ var conformancePins = {
1792
+ "static-array-children": [{ code: "BF103", severity: "error" }],
1793
+ "todo-app": [{ code: "BF103", severity: "error" }],
1794
+ "todo-app-ssr": [{ code: "BF103", severity: "error" }],
1795
+ "static-array-from-props": [{ code: "BF101", severity: "error" }],
1796
+ "static-array-from-props-with-component": [
1797
+ { code: "BF103", severity: "error" },
1798
+ { code: "BF101", severity: "error" }
1799
+ ],
1800
+ "filter-nested-find-predicate": [
1801
+ { code: "BF101", severity: "error", issue: "https://github.com/piconic-ai/barefootjs/issues/2038" }
1802
+ ],
1803
+ "array-map-function-reference": [{ code: "BF101", severity: "error" }]
1804
+ };
1758
1805
  export {
1759
1806
  mojoAdapter,
1807
+ conformancePins,
1760
1808
  MojoAdapter
1761
1809
  };
@@ -1 +1 @@
1
- {"version":3,"file":"test-render.d.ts","sourceRoot":"","sources":["../src/test-render.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAeH,qBAAa,qBAAsB,SAAQ,KAAK;IAC9C,YAAY,OAAO,EAAE,MAAM,EAG1B;CACF;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAKxE;AAkCD,MAAM,WAAW,aAAa;IAC5B,sBAAsB;IACtB,MAAM,EAAE,MAAM,CAAA;IACd,8BAA8B;IAC9B,OAAO,EAAE,OAAO,iBAAiB,EAAE,eAAe,CAAA;IAClD,iCAAiC;IACjC,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IAC/B,qDAAqD;IACrD,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IACnC;;;;;OAKG;IACH,aAAa,CAAC,EAAE,MAAM,CAAA;CACvB;AAED,wBAAsB,mBAAmB,CAAC,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,MAAM,CAAC,CAgNjF;AAqWD;;;GAGG;AACH,wBAAgB,kBAAkB,CAChC,IAAI,EAAE,MAAM,EACZ,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC9B,OAAO,CAuBT"}
1
+ {"version":3,"file":"test-render.d.ts","sourceRoot":"","sources":["../src/test-render.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAeH,qBAAa,qBAAsB,SAAQ,KAAK;IAC9C,YAAY,OAAO,EAAE,MAAM,EAG1B;CACF;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAKxE;AAkCD,MAAM,WAAW,aAAa;IAC5B,sBAAsB;IACtB,MAAM,EAAE,MAAM,CAAA;IACd,8BAA8B;IAC9B,OAAO,EAAE,OAAO,iBAAiB,EAAE,eAAe,CAAA;IAClD,iCAAiC;IACjC,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IAC/B,qDAAqD;IACrD,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IACnC;;;;;OAKG;IACH,aAAa,CAAC,EAAE,MAAM,CAAA;CACvB;AAED,wBAAsB,mBAAmB,CAAC,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,MAAM,CAAC,CAwNjF;AAqWD;;;GAGG;AACH,wBAAgB,kBAAkB,CAChC,IAAI,EAAE,MAAM,EACZ,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC9B,OAAO,CAuBT"}
@@ -1,5 +1,5 @@
1
1
  package BarefootJS::Backend::Mojo;
2
- our $VERSION = "0.16.0";
2
+ our $VERSION = "0.17.1";
3
3
  use Mojo::Base -base, -signatures;
4
4
 
5
5
  use Mojo::ByteStream qw(b);
@@ -78,13 +78,22 @@ sub materialize ($self, $value) {
78
78
  # instance for that render. The Mojo `bf` helper resolves the current
79
79
  # instance off `$c->stash->{'bf.instance'}`; swap it for the duration of
80
80
  # the nested render and restore it afterwards so sibling renders are
81
- # unaffected.
81
+ # unaffected. `local` (rather than a manual swap) keeps the restore
82
+ # exception-safe: a die inside the nested render — e.g. a grandchild with
83
+ # no registered renderer — propagates without leaving the child instance
84
+ # bound as the request's active one.
82
85
  sub render_named ($self, $template_name, $child_bf, $vars) {
83
86
  my $c = $self->c;
84
- my $prev = $c->stash->{'bf.instance'};
85
- $c->stash->{'bf.instance'} = $child_bf;
87
+ local $c->stash->{'bf.instance'} = $child_bf;
86
88
  my $html = $c->render_to_string(template => $template_name, %$vars);
87
- $c->stash->{'bf.instance'} = $prev;
89
+ # `render_to_string` returns undef — it does NOT die — when the named
90
+ # template can't be rendered (typically: the template file is missing
91
+ # from the renderer paths). The calling template's `<%==` would emit
92
+ # that as an empty string, silently dropping the whole child subtree
93
+ # from the page (#2132). Fail the render loudly instead.
94
+ die qq{BarefootJS: child template "$template_name" rendered no output }
95
+ . qq{(is the template missing from the renderer paths?)\n}
96
+ unless defined $html;
88
97
  return $html;
89
98
  }
90
99
 
@@ -1,5 +1,5 @@
1
1
  package Mojolicious::Plugin::BarefootJS::DevReload;
2
- our $VERSION = "0.16.0";
2
+ our $VERSION = "0.17.1";
3
3
  use Mojo::Base 'Mojolicious::Plugin', -signatures;
4
4
 
5
5
  =head1 NAME
@@ -1,5 +1,5 @@
1
1
  package Mojolicious::Plugin::BarefootJS;
2
- our $VERSION = "0.16.0";
2
+ our $VERSION = "0.17.1";
3
3
  use Mojo::Base 'Mojolicious::Plugin', -signatures;
4
4
 
5
5
  use Mojo::File qw(path);
@@ -31,28 +31,64 @@ sub register ($self, $app, $config = {}) {
31
31
  $c->stash->{'bf.instance'} //= BarefootJS->new($c, $config);
32
32
  });
33
33
 
34
- my $manifest = _load_manifest($app, $config);
35
- return unless $manifest;
36
-
37
- # Cache the set of UI-registry slot keys so we can answer
38
- # "is this template name a child or a top-level page?" with a
39
- # single hash lookup at render time. Top-level entries are
40
- # everything that isn't `__barefoot__` and doesn't match
41
- # `ui/<name>/index` the same partition `register_components_from_manifest`
42
- # applies internally.
43
- my %is_child_entry;
44
- for my $entry_name (keys %$manifest) {
45
- next if $entry_name eq '__barefoot__';
46
- next unless $entry_name =~ m{^ui/[^/]+/index$};
47
- $is_child_entry{$entry_name} = 1;
48
- }
34
+ return if exists $config->{manifest_path} && !defined $config->{manifest_path};
35
+ my $manifest_path = path(
36
+ $config->{manifest_path} // $app->home->child('dist/templates/manifest.json'));
37
+
38
+ # Manifest resolution is lazy, re-checked per render (#2126). The
39
+ # scaffold's dev script starts `bf build --watch` and the web server
40
+ # concurrently, so the app routinely boots before the first build has
41
+ # written `manifest.json`. Loading once at register time turned that
42
+ # startup race into a permanent failure: auto-init stayed disabled for
43
+ # the server's lifetime and every top-level render died under strict
44
+ # (`Global symbol "$initial" requires explicit package name`). The
45
+ # cache is keyed on the file's (mtime, size), so the steady-state cost
46
+ # is one stat() per render — and `bf build --watch` rebuilds (new
47
+ # ssrDefaults, `bf add`ed components) are picked up without a restart.
48
+ my ($cached_sig, $manifest, $is_child_entry);
49
+ my $load_manifest = sub {
50
+ my ($size, $mtime) = (stat "$manifest_path")[7, 9];
51
+ unless (defined $mtime) {
52
+ # Missing (or unreadable) — drop the cache so the manifest is
53
+ # re-read as soon as the first build writes it.
54
+ ($cached_sig, $manifest, $is_child_entry) = (undef, undef, undef);
55
+ return undef;
56
+ }
57
+ my $sig = "$mtime/$size";
58
+ return $manifest if defined $cached_sig && $cached_sig eq $sig;
59
+ # Cache parse failures under the same signature so a broken file
60
+ # warns once, not on every render, and retries when it changes.
61
+ $cached_sig = $sig;
62
+ ($manifest, $is_child_entry) = (undef, undef);
63
+ my $m = eval { decode_json($manifest_path->slurp) };
64
+ if ($@ || ref($m) ne 'HASH') {
65
+ $app->log->warn("BarefootJS: cannot parse manifest at $manifest_path: $@") if $@;
66
+ return undef;
67
+ }
68
+ $manifest = $m;
69
+
70
+ # Cache the set of UI-registry slot keys so we can answer
71
+ # "is this template name a child or a top-level page?" with a
72
+ # single hash lookup at render time. Top-level entries are
73
+ # everything that isn't `__barefoot__` and doesn't match
74
+ # `ui/<name>/index` — the same partition `register_components_from_manifest`
75
+ # applies internally.
76
+ $is_child_entry = {};
77
+ for my $entry_name (keys %$manifest) {
78
+ next if $entry_name eq '__barefoot__';
79
+ next unless $entry_name =~ m{^ui/[^/]+/index$};
80
+ $is_child_entry->{$entry_name} = 1;
81
+ }
82
+ return $manifest;
83
+ };
49
84
 
50
85
  $app->hook(before_render => sub ($c, $args) {
51
86
  my $template = $args->{template};
52
87
  return unless defined $template && length $template;
53
- my $entry = $manifest->{$template};
88
+ my $m = $load_manifest->() or return;
89
+ my $entry = $m->{$template};
54
90
  return unless $entry;
55
- return if $is_child_entry{$template};
91
+ return if $is_child_entry->{$template};
56
92
  # Idempotency guard for nested renders. A controller might
57
93
  # call `render_to_string` inside an action and then `render`
58
94
  # — without this we'd re-init `bf` on the second pass and
@@ -72,7 +108,7 @@ sub register ($self, $app, $config = {}) {
72
108
  $c->stash->{'bf.auto_init_done'} = 1;
73
109
 
74
110
  $bf->_scope_id($template . '_' . substr(rand() =~ s/^0\.//r, 0, 6));
75
- $bf->register_components_from_manifest($manifest);
111
+ $bf->register_components_from_manifest($m);
76
112
 
77
113
  # Seed each ssrDefault into the stash unless the caller has
78
114
  # already supplied a value for that key — callers always win.
@@ -98,20 +134,6 @@ sub register ($self, $app, $config = {}) {
98
134
  });
99
135
  }
100
136
 
101
- sub _load_manifest ($app, $config) {
102
- return undef if exists $config->{manifest_path} && !defined $config->{manifest_path};
103
- my $manifest_path = $config->{manifest_path}
104
- // $app->home->child('dist/templates/manifest.json');
105
- my $file = path($manifest_path);
106
- return undef unless -r $file;
107
- my $manifest = eval { decode_json($file->slurp) };
108
- if ($@ || ref($manifest) ne 'HASH') {
109
- $app->log->warn("BarefootJS: cannot parse manifest at $file: $@") if $@;
110
- return undef;
111
- }
112
- return $manifest;
113
- }
114
-
115
137
  1;
116
138
  __END__
117
139
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@barefootjs/mojolicious",
3
- "version": "0.17.0",
3
+ "version": "0.18.0",
4
4
  "description": "Mojolicious EP template adapter for BarefootJS - generates .html.ep files from IR",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -52,7 +52,7 @@
52
52
  "directory": "packages/adapter-mojolicious"
53
53
  },
54
54
  "dependencies": {
55
- "@barefootjs/shared": "0.17.0"
55
+ "@barefootjs/shared": "0.18.0"
56
56
  },
57
57
  "peerDependencies": {
58
58
  "@barefootjs/jsx": ">=0.2.0",
@@ -60,6 +60,6 @@
60
60
  },
61
61
  "devDependencies": {
62
62
  "@barefootjs/adapter-tests": "0.1.0",
63
- "@barefootjs/jsx": "0.17.0"
63
+ "@barefootjs/jsx": "0.18.0"
64
64
  }
65
65
  }