@barefootjs/mojolicious 0.14.0 → 0.15.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
@@ -6,6 +6,8 @@ import {
6
6
  parseExpression as parseExpression2,
7
7
  parseStyleObjectEntries,
8
8
  isSupported,
9
+ exprToString,
10
+ parseProviderObjectLiteral,
9
11
  identifierPath,
10
12
  emitParsedExpr,
11
13
  emitIRNode,
@@ -16,7 +18,10 @@ import {
16
18
  extractArrowBodyExpression,
17
19
  collectContextConsumers,
18
20
  isLowerableObjectRestDestructure,
19
- extractSsrDefaults
21
+ collectModuleStringConsts,
22
+ lookupStaticRecordLiteral,
23
+ searchParamsLocalNames,
24
+ matchSearchParamsMethodCall
20
25
  } from "@barefootjs/jsx";
21
26
 
22
27
  // src/adapter/boolean-result.ts
@@ -63,6 +68,8 @@ var ARIA_BOOLEAN_ATTRS = new Set([
63
68
  "aria-multiselectable",
64
69
  "aria-readonly",
65
70
  "aria-required",
71
+ "aria-selected",
72
+ "aria-expanded",
66
73
  "aria-checked",
67
74
  "aria-pressed"
68
75
  ]);
@@ -71,7 +78,7 @@ function isAriaBooleanAttr(name) {
71
78
  }
72
79
 
73
80
  // src/adapter/mojo-adapter.ts
74
- import { BF_SLOT, BF_COND } from "@barefootjs/shared";
81
+ import { BF_SLOT, BF_COND, BF_REGION } from "@barefootjs/shared";
75
82
  var MOJO_TEMPLATE_PRIMITIVES = {
76
83
  "JSON.stringify": { arity: 1, emit: (args) => `bf->json(${args[0]})` },
77
84
  String: { arity: 1, emit: (args) => `bf->string(${args[0]})` },
@@ -89,22 +96,6 @@ function resolveJsxChildrenProp(props) {
89
96
  return [];
90
97
  return prop.value.children;
91
98
  }
92
- function parsePureStringLiteral(source) {
93
- const sf = ts.createSourceFile("__const.ts", `const __x = (${source});`, ts.ScriptTarget.Latest, false);
94
- const stmt = sf.statements[0];
95
- if (!stmt || !ts.isVariableStatement(stmt))
96
- return null;
97
- const decl = stmt.declarationList.declarations[0];
98
- let init = decl?.initializer;
99
- while (init && ts.isParenthesizedExpression(init))
100
- init = init.expression;
101
- if (!init)
102
- return null;
103
- if (ts.isStringLiteral(init) || ts.isNoSubstitutionTemplateLiteral(init)) {
104
- return init.text;
105
- }
106
- return evalStringArrayJoin(source);
107
- }
108
99
  function perlHashKey(name) {
109
100
  return /^[A-Za-z_][A-Za-z0-9_]*$/.test(name) ? name : `'${name.replace(/'/g, "\\'")}'`;
110
101
  }
@@ -152,7 +143,9 @@ class MojoAdapter extends BaseAdapter {
152
143
  inLoop = false;
153
144
  propsObjectName = null;
154
145
  propsParams = [];
146
+ booleanTypedProps = new Set;
155
147
  stringValueNames = new Set;
148
+ _searchParamsLocals = new Set;
156
149
  moduleStringConsts = new Map;
157
150
  localConstants = [];
158
151
  loopBoundNames = new Map;
@@ -169,6 +162,7 @@ class MojoAdapter extends BaseAdapter {
169
162
  this.propsObjectName = ir.metadata.propsObjectName ?? null;
170
163
  augmentInheritedPropAccesses(ir);
171
164
  this.propsParams = ir.metadata.propsParams.map((p) => ({ name: p.name }));
165
+ this.booleanTypedProps = new Set(ir.metadata.propsParams.filter((prop) => prop.type?.primitive === "boolean" || prop.type?.raw === "boolean").map((prop) => prop.name));
172
166
  this.nullableOptionalProps = new Set(ir.metadata.propsParams.filter((p) => p.defaultValue === undefined && !p.isRest && p.type?.kind !== "primitive").map((p) => p.name));
173
167
  this.stringValueNames = new Set;
174
168
  for (const s of ir.metadata.signals) {
@@ -180,7 +174,8 @@ class MojoAdapter extends BaseAdapter {
180
174
  if (isStringTypeInfo(p.type))
181
175
  this.stringValueNames.add(p.name);
182
176
  }
183
- this.moduleStringConsts = this.collectModuleStringConsts(ir.metadata.localConstants);
177
+ this.moduleStringConsts = collectModuleStringConsts(ir.metadata.localConstants);
178
+ this._searchParamsLocals = searchParamsLocalNames(ir.metadata);
184
179
  this.localConstants = ir.metadata.localConstants ?? [];
185
180
  this.loopBoundNames.clear();
186
181
  this.errors = [];
@@ -210,18 +205,49 @@ class MojoAdapter extends BaseAdapter {
210
205
  extension: this.extension
211
206
  };
212
207
  }
213
- collectModuleStringConsts(constants) {
214
- const map = new Map;
215
- for (const c of constants ?? []) {
216
- if (!c.isModule)
217
- continue;
218
- if (c.value === undefined)
219
- continue;
220
- const literal = parsePureStringLiteral(c.value);
221
- if (literal !== null)
222
- map.set(c.name, literal);
208
+ isBooleanTypedPropRef(expr) {
209
+ let bare = expr.trim();
210
+ if (this.propsObjectName && bare.startsWith(`${this.propsObjectName}.`)) {
211
+ bare = bare.slice(this.propsObjectName.length + 1);
223
212
  }
224
- return map;
213
+ if (!/^[A-Za-z_$][\w$]*$/.test(bare))
214
+ return false;
215
+ return this.booleanTypedProps.has(bare);
216
+ }
217
+ parseUndefinedAlternateTernary(expr) {
218
+ const parsed = parseExpression2(expr.trim());
219
+ if (parsed?.kind !== "conditional")
220
+ return null;
221
+ const alt = parsed.alternate;
222
+ const isUndef = alt.kind === "identifier" && (alt.name === "undefined" || alt.name === "null") || alt.kind === "literal" && (alt.value === null || alt.value === undefined);
223
+ if (!isUndef)
224
+ return null;
225
+ return {
226
+ condition: exprToString(parsed.test),
227
+ consequent: exprToString(parsed.consequent)
228
+ };
229
+ }
230
+ resolveLiteralConst(name) {
231
+ if (this.loopBoundNames?.has?.(name))
232
+ return null;
233
+ const c = (this.localConstants ?? []).find((lc) => lc.name === name);
234
+ if (c?.value === undefined)
235
+ return null;
236
+ const v = c.value.trim();
237
+ if (/^-?\d+(\.\d+)?$/.test(v))
238
+ return v;
239
+ const strLit = /^'([^'\\]*)'$/.exec(v) ?? /^"([^"\\]*)"$/.exec(v);
240
+ if (strLit)
241
+ return `'${strLit[1].replace(/[\\']/g, (m) => `\\${m}`)}'`;
242
+ return null;
243
+ }
244
+ resolveStaticRecordLiteral(objectName, key) {
245
+ if (this.loopBoundNames?.has?.(objectName))
246
+ return null;
247
+ const hit = lookupStaticRecordLiteral(objectName, key, this.localConstants);
248
+ if (!hit)
249
+ return null;
250
+ return hit.kind === "number" ? hit.text : `'${hit.text.replace(/[\\']/g, (m) => `\\${m}`)}'`;
225
251
  }
226
252
  resolveModuleStringConst(name) {
227
253
  if (this.loopBoundNames.has(name))
@@ -289,12 +315,29 @@ class MojoAdapter extends BaseAdapter {
289
315
  if (v.kind === "literal") {
290
316
  return typeof v.value === "string" ? `'${v.value.replace(/[\\']/g, (m) => `\\${m}`)}'` : String(v.value);
291
317
  }
292
- if (v.kind === "expression")
318
+ if (v.kind === "expression") {
319
+ const hashref = this.providerObjectLiteralPerl(v.expr);
320
+ if (hashref !== null)
321
+ return hashref;
293
322
  return this.convertExpressionToPerl(v.expr);
323
+ }
294
324
  if (v.kind === "template")
295
325
  return this.convertTemplateLiteralPartsToPerl(v.parts);
296
326
  return "undef";
297
327
  }
328
+ providerObjectLiteralPerl(expr) {
329
+ const members = parseProviderObjectLiteral(expr.trim());
330
+ if (members === null)
331
+ return null;
332
+ const entries = members.map((m) => {
333
+ const key = `'${m.name.replace(/[\\']/g, (c) => `\\${c}`)}'`;
334
+ if (m.kind === "function" || /^on[A-Z]/.test(m.name))
335
+ return `${key} => undef`;
336
+ const src = m.kind === "getter" ? m.body : m.expr;
337
+ return `${key} => ${this.convertExpressionToPerl(src)}`;
338
+ });
339
+ return `{ ${entries.join(", ")} }`;
340
+ }
298
341
  contextDefaultPerl(c) {
299
342
  const d = c.defaultValue;
300
343
  if (d === null || d === undefined)
@@ -318,7 +361,6 @@ class MojoAdapter extends BaseAdapter {
318
361
  const signals = ir.metadata.signals ?? [];
319
362
  if (memos.length === 0 && signals.length === 0)
320
363
  return "";
321
- const ssrDefaults = extractSsrDefaults(ir.metadata) ?? {};
322
364
  const available = new Set(ir.metadata.propsParams.map((p) => p.name));
323
365
  const lines = [];
324
366
  for (const signal of signals) {
@@ -328,18 +370,12 @@ class MojoAdapter extends BaseAdapter {
328
370
  available.add(signal.getter);
329
371
  }
330
372
  for (const memo of memos) {
331
- const def = ssrDefaults[memo.name];
332
- const isNull = !def || typeof def === "object" && "value" in def && def.value === null;
333
- if (!isNull) {
334
- available.add(memo.name);
335
- continue;
336
- }
337
373
  const body = extractArrowBodyExpression(memo.computation);
338
- if (body === null)
339
- continue;
340
- const perl = this.tryLowerToPerl(body, available);
341
- if (perl !== null)
342
- lines.push(`% my $${memo.name} = ${perl};`);
374
+ if (body !== null) {
375
+ const perl = this.tryLowerToPerl(body, available);
376
+ if (perl !== null)
377
+ lines.push(`% my $${memo.name} = ${perl};`);
378
+ }
343
379
  available.add(memo.name);
344
380
  }
345
381
  return lines.length > 0 ? lines.join(`
@@ -374,6 +410,9 @@ class MojoAdapter extends BaseAdapter {
374
410
  if (element.slotId) {
375
411
  hydrationAttrs += ` ${this.renderSlotMarker(element.slotId)}`;
376
412
  }
413
+ if (element.regionId) {
414
+ hydrationAttrs += ` ${BF_REGION}="${element.regionId}"`;
415
+ }
377
416
  const voidElements = [
378
417
  "area",
379
418
  "base",
@@ -654,7 +693,7 @@ ${renderedChildren}` : renderedChildren;
654
693
  renderComponent(comp) {
655
694
  const propParts = [];
656
695
  for (const p of comp.props) {
657
- if (p.name.match(/^on[A-Z]/) && p.value.kind === "expression")
696
+ if ((p.name.match(/^on[A-Z]/) || p.name === "ref") && p.value.kind === "expression")
658
697
  continue;
659
698
  const lowered = emitAttrValue(p.value, this.componentPropEmitter, p.name);
660
699
  if (lowered)
@@ -667,13 +706,17 @@ ${renderedChildren}` : renderedChildren;
667
706
  const tplName = this.toTemplateName(comp.name);
668
707
  const effectiveChildren = comp.children.length > 0 ? comp.children : resolveJsxChildrenProp(comp.props);
669
708
  if (effectiveChildren.length > 0) {
709
+ const prevInLoop = this.inLoop;
710
+ this.inLoop = false;
670
711
  const childrenBody = this.renderChildren(effectiveChildren);
712
+ this.inLoop = prevInLoop;
671
713
  const varName = `$bf_children_${comp.slotId ?? "c" + this.childrenCaptureCounter++}`;
672
714
  return `<% my ${varName} = begin %>${childrenBody}<% end %><%== bf->render_child('${tplName}'${propsStr}, children => ${varName}) %>`;
673
715
  }
674
716
  return `<%== bf->render_child('${tplName}'${propsStr}) %>`;
675
717
  }
676
718
  childrenCaptureCounter = 0;
719
+ presenceVarCounter = 0;
677
720
  toTemplateName(componentName) {
678
721
  return componentName.replace(/([A-Z])/g, "_$1").toLowerCase().replace(/^_/, "");
679
722
  }
@@ -729,14 +772,28 @@ ${children}`;
729
772
  const normalizedBareId = this.propsObjectName && bareId.startsWith(`${this.propsObjectName}.`) ? bareId.slice(this.propsObjectName.length + 1) : bareId;
730
773
  if (!isBooleanAttr(name) && !value.presenceOrUndefined && /^[A-Za-z_$][\w$]*$/.test(normalizedBareId) && this.nullableOptionalProps.has(normalizedBareId)) {
731
774
  const perl2 = this.convertExpressionToPerl(value.expr);
732
- const body = isBooleanResultExpr(value.expr) || isAriaBooleanAttr(name) ? `${name}="<%= bf->bool_str(${perl2}) %>"` : `${name}="<%= ${perl2} %>"`;
775
+ const body = isBooleanResultExpr(value.expr) || isAriaBooleanAttr(name) || this.isBooleanTypedPropRef(value.expr) ? `${name}="<%= bf->bool_str(${perl2}) %>"` : `${name}="<%= ${perl2} %>"`;
733
776
  return `<% if (defined ${perl2}) { %>${body}<% } %>`;
734
777
  }
735
- if (isBooleanAttr(name) || value.presenceOrUndefined) {
778
+ if (isBooleanAttr(name)) {
736
779
  return `<%= ${this.convertExpressionToPerl(value.expr)} ? '${name}' : '' %>`;
737
780
  }
781
+ if (value.presenceOrUndefined) {
782
+ const perl2 = this.convertExpressionToPerl(value.expr);
783
+ const tmp = `$bf_pu${this.presenceVarCounter++}`;
784
+ const body = isBooleanResultExpr(value.expr) || isAriaBooleanAttr(name) || this.isBooleanTypedPropRef(value.expr) ? `${name}="<%= bf->bool_str(${tmp}) %>"` : `${name}="<%= ${tmp} %>"`;
785
+ return `<% my ${tmp} = ${perl2}; if (${tmp}) { %>${body}<% } %>`;
786
+ }
787
+ {
788
+ const m = this.parseUndefinedAlternateTernary(value.expr);
789
+ if (m) {
790
+ const cond = this.convertExpressionToPerl(m.condition);
791
+ const val = this.convertExpressionToPerl(m.consequent);
792
+ return `<% if (${cond}) { %>${name}="<%= ${val} %>"<% } %>`;
793
+ }
794
+ }
738
795
  const perl = this.convertExpressionToPerl(value.expr);
739
- if (isBooleanResultExpr(value.expr) || isAriaBooleanAttr(name)) {
796
+ if (isBooleanResultExpr(value.expr) || isAriaBooleanAttr(name) || this.isBooleanTypedPropRef(value.expr)) {
740
797
  return `${name}="<%= bf->bool_str(${perl}) %>"`;
741
798
  }
742
799
  return `${name}="<%= ${perl} %>"`;
@@ -995,6 +1052,18 @@ ${reason}` : "";
995
1052
  return n;
996
1053
  })();
997
1054
  const indexed = this.recordIndexAccessToPerl(initNode);
1055
+ if (indexed === null && ts.isElementAccessExpression(initNode) && initNode.argumentExpression && !ts.isNumericLiteral(initNode.argumentExpression) && !ts.isStringLiteral(initNode.argumentExpression)) {
1056
+ this.errors.push({
1057
+ code: "BF101",
1058
+ severity: "error",
1059
+ message: `Spread object value '${initNode.getText(sf)}' indexes a record map whose values aren't scalar literals — it can't lower to an inline Perl hashref.`,
1060
+ loc: { file: this.componentName + ".tsx", start: { line: 1, column: 0 }, end: { line: 1, column: 0 } },
1061
+ suggestion: {
1062
+ message: "Index a record whose values are number/string literals, or move the spread into a `'use client'` component so hydration computes it."
1063
+ }
1064
+ });
1065
+ return null;
1066
+ }
998
1067
  const valPerl = indexed !== null ? indexed : this.convertExpressionToPerl(prop.initializer.getText(sf));
999
1068
  entries.push(`'${key.replace(/'/g, "\\'")}' => ${valPerl}`);
1000
1069
  }
@@ -1118,6 +1187,11 @@ function renderArrayMethod(method, object, args, emit) {
1118
1187
  const recv = emit(object);
1119
1188
  return `bf->trim(${recv})`;
1120
1189
  }
1190
+ case "toFixed": {
1191
+ const recv = emit(object);
1192
+ const digits = args.length >= 1 ? emit(args[0]) : "0";
1193
+ return `bf->to_fixed(${recv}, ${digits})`;
1194
+ }
1121
1195
  case "split": {
1122
1196
  const recv = emit(object);
1123
1197
  if (args.length === 0) {
@@ -1220,6 +1294,10 @@ function isStringTypedOperand(expr, isStringName) {
1220
1294
  }
1221
1295
  return false;
1222
1296
  }
1297
+ function emitIndexAccessPerl(object, index, emit, isStringName) {
1298
+ const i = emit(index);
1299
+ return isStringTypedOperand(index, isStringName) ? `${emit(object)}->{${i}}` : `${emit(object)}->[${i}]`;
1300
+ }
1223
1301
 
1224
1302
  class MojoFilterEmitter {
1225
1303
  param;
@@ -1253,6 +1331,9 @@ class MojoFilterEmitter {
1253
1331
  }
1254
1332
  return `${emit(object)}->{${property}}`;
1255
1333
  }
1334
+ indexAccess(object, index, emit) {
1335
+ return emitIndexAccessPerl(object, index, emit, this.isStringName);
1336
+ }
1256
1337
  call(callee, args, emit) {
1257
1338
  if (callee.kind === "identifier" && args.length === 0) {
1258
1339
  return `$${callee.name}`;
@@ -1353,9 +1434,14 @@ class MojoTopLevelEmitter {
1353
1434
  this.adapter = adapter;
1354
1435
  }
1355
1436
  identifier(name) {
1437
+ if (name === "undefined" || name === "null")
1438
+ return "undef";
1356
1439
  const inlined = this.adapter.resolveModuleStringConst(name);
1357
1440
  if (inlined !== null)
1358
1441
  return inlined;
1442
+ const literalConst = this.adapter.resolveLiteralConst(name);
1443
+ if (literalConst !== null)
1444
+ return literalConst;
1359
1445
  return `$${name}`;
1360
1446
  }
1361
1447
  literal(value, literalType) {
@@ -1371,15 +1457,29 @@ class MojoTopLevelEmitter {
1371
1457
  if (object.kind === "identifier" && object.name === "props") {
1372
1458
  return `$${property}`;
1373
1459
  }
1460
+ if (object.kind === "identifier") {
1461
+ const staticValue = this.adapter.resolveStaticRecordLiteral(object.name, property);
1462
+ if (staticValue !== null)
1463
+ return staticValue;
1464
+ }
1374
1465
  const obj = emit(object);
1375
1466
  if (property === "length")
1376
1467
  return `scalar(@{${obj}})`;
1377
1468
  return `${obj}->{${property}}`;
1378
1469
  }
1470
+ indexAccess(object, index, emit) {
1471
+ return emitIndexAccessPerl(object, index, emit, (n) => this.adapter._isStringValueName(n));
1472
+ }
1379
1473
  call(callee, args, emit) {
1380
1474
  if (callee.kind === "identifier" && args.length === 0) {
1381
1475
  return `$${callee.name}`;
1382
1476
  }
1477
+ if (this.adapter._searchParamsLocals.size > 0) {
1478
+ const sp = matchSearchParamsMethodCall(callee, args, this.adapter._searchParamsLocals);
1479
+ if (sp) {
1480
+ return `$searchParams->${sp.method}(${sp.args.map(emit).join(", ")})`;
1481
+ }
1482
+ }
1383
1483
  const path = identifierPath(callee);
1384
1484
  const spec = path ? MOJO_TEMPLATE_PRIMITIVES[path] : undefined;
1385
1485
  if (path && spec) {
@@ -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;AAyQD;;;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,CAgNjF;AAmWD;;;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.13.0";
2
+ our $VERSION = "0.14.0";
3
3
  use Mojo::Base -base, -signatures;
4
4
 
5
5
  use Mojo::ByteStream qw(b);
@@ -1,5 +1,5 @@
1
1
  package Mojolicious::Plugin::BarefootJS::DevReload;
2
- our $VERSION = "0.13.0";
2
+ our $VERSION = "0.14.0";
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.13.0";
2
+ our $VERSION = "0.14.0";
3
3
  use Mojo::Base 'Mojolicious::Plugin', -signatures;
4
4
 
5
5
  use Mojo::File qw(path);
@@ -85,6 +85,16 @@ sub register ($self, $app, $config = {}) {
85
85
  $c->stash->{$name} = $value;
86
86
  }
87
87
  }
88
+
89
+ # (#1922) Seed the request-scoped `searchParams()` reader as the
90
+ # `$searchParams` template var, built from the live request query —
91
+ # so `searchParams().get(k)` resolves the current query during SSR
92
+ # (the client re-reads window.location on hydration). A caller that
93
+ # set it by hand wins (`//=`). Harmless for components that never read
94
+ # it; the var simply goes unused. `$bf->search_params` lazy-loads the
95
+ # reader class, so the plugin needn't `use` it directly.
96
+ $c->stash->{searchParams} //=
97
+ $bf->search_params($c->req->query_params->to_string);
88
98
  });
89
99
  }
90
100
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@barefootjs/mojolicious",
3
- "version": "0.14.0",
3
+ "version": "0.15.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.14.0"
55
+ "@barefootjs/shared": "0.15.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.14.0"
63
+ "@barefootjs/jsx": "0.15.0"
64
64
  }
65
65
  }
@@ -17,10 +17,13 @@ runAdapterConformanceTests({
17
17
  name: 'mojo',
18
18
  factory: () => new MojoAdapter(),
19
19
  render: renderMojoComponent,
20
- // No JSX-render skips: every shared conformance fixture renders to Hono
21
- // parity on real Mojolicious. Shapes the adapter intentionally refuses at
22
- // build time are pinned in `expectedDiagnostics` below.
23
- skipJsx: [],
20
+ // No JSX-render skips: every shared conformance fixture including
21
+ // the composed `site/ui` demo corpus (#1467 / #1897) renders to
22
+ // Hono parity on real Mojolicious. `data-table` came off via the
23
+ // body-children `inLoop` reset (#1896): the loop-item component
24
+ // (TableRow) still gets `ComponentName_<random>` scope IDs, but its
25
+ // body children (TableCell) now receive `_bf_slot` for deterministic
26
+ // parent-scope-derived IDs matching Hono.
24
27
  // Per-fixture build-time contracts for shapes the Mojo adapter
25
28
  // intentionally refuses to lower. Owned by this adapter test file
26
29
  // (not by the shared fixtures) so adding a new adapter doesn't
@@ -62,6 +65,21 @@ runAdapterConformanceTests({
62
65
  // (`cn\`base \${tone()}\``) — same family as #1322 above and refused
63
66
  // via the same gate.
64
67
  'tagged-template-classname': [{ code: 'BF101', severity: 'error' }],
68
+ // #1467 demo-corpus context providers (`radio-group`, `accordion`,
69
+ // `dialog`, `popover`, `select`, `dropdown-menu`, `combobox`,
70
+ // `command`) are no longer pinned — an object-literal provider value
71
+ // (`{ open: () => props.open ?? false, onOpenChange: (v) => {…} }`)
72
+ // lowers to a Perl hashref via `parseProviderObjectLiteral` (#1897):
73
+ // getter members snapshot their body's SSR value, handler /
74
+ // function-shaped members lower to `undef`. The command demo's
75
+ // `ref={(el) => {…}}` function prop on an imported component is
76
+ // skipped at SSR like `on*` handlers.
77
+ //
78
+ // #1467 Phase 2e: `data-table` is no longer pinned here either — it
79
+ // compiles clean (`selected()[index]` → `index-access`,
80
+ // `.toFixed(2)` → `bf->to_fixed`, `/* @client */` memo SSR-folded)
81
+ // and is pinned in `skipJsx` above on the keyed-loop scope-ID
82
+ // divergence alone (#1896), not a BF101.
65
83
  // #1443: `[a, b].filter(Boolean).join(' ')` (the registry Slot's
66
84
  // shape) now lowers to `join(' ', @{[grep { $_ } @{[$a, $b]}]})`.
67
85
  // No BF101 expected — pinned positively via the
@@ -135,6 +153,8 @@ runAdapterConformanceTests({
135
153
  // intentionally elide a slot id from the SSR template that the IR
136
154
  // still declares (s6). See hono-adapter.test for the contract.
137
155
  'todo-app',
156
+ // #1467 Phase 2e: same `/* @client */` keyed-map elision (data-table).
157
+ 'data-table',
138
158
  ]),
139
159
  onRenderError: (err, id) => {
140
160
  if (err instanceof PerlNotAvailableError) {
@@ -209,6 +229,35 @@ function Box({ k, v }: { k?: string; v?: string }) {
209
229
  })
210
230
  })
211
231
 
232
+ describe('MojoAdapter - searchParams() env-signal lowering (#1922)', () => {
233
+ // `searchParams().get(k)` is an env-signal method call: it must lower to a
234
+ // real method call on the per-request `$searchParams` reader, not the
235
+ // generic hash deref `$searchParams->{get}` (which drops the arg).
236
+ test('lowers searchParams().get(k) to a method call on $searchParams', () => {
237
+ const { template } = compileAndGenerate(`
238
+ import { searchParams } from '@barefootjs/client'
239
+ function SortLabel() {
240
+ return <p>{searchParams().get('sort') ?? 'none'}</p>
241
+ }
242
+ `)
243
+ expect(template).toContain("($searchParams->get('sort') // 'none')")
244
+ expect(template).not.toContain('$searchParams->{get}')
245
+ })
246
+
247
+ // An aliased import binds the env signal to a different local name; the
248
+ // expression reads `sp()`, but it still lowers to the canonical
249
+ // `$searchParams` reader (the harness/plugin seed that fixed var).
250
+ test('matches an aliased import (`searchParams as sp`) and emits canonical $searchParams', () => {
251
+ const { template } = compileAndGenerate(`
252
+ import { searchParams as sp } from '@barefootjs/client'
253
+ function SortLabel() {
254
+ return <p>{sp().get('sort') ?? 'none'}</p>
255
+ }
256
+ `)
257
+ expect(template).toContain("($searchParams->get('sort') // 'none')")
258
+ })
259
+ })
260
+
212
261
  describe('MojoAdapter - local-const conditional-spread resolution (#checkbox icon)', () => {
213
262
  // A FUNCTION-scope const holding a `cond ? {…} : {}` ternary, spread as
214
263
  // a bare identifier (`{...attrs}`), resolves through the same Perl
@@ -112,6 +112,11 @@ const ARIA_BOOLEAN_ATTRS = new Set([
112
112
  'aria-multiselectable',
113
113
  'aria-readonly',
114
114
  'aria-required',
115
+ // true | false | undefined (absent) — selection / disclosure state
116
+ // (#1897: tabs' `aria-selected={props.selected ?? false}` rendered the
117
+ // Perl-native `1`/`0` without this).
118
+ 'aria-selected',
119
+ 'aria-expanded',
115
120
  // Tri-state (true | false | mixed). The `bool_str` helper only
116
121
  // maps Perl truthy / falsy to true / false — a fixture that wants
117
122
  // the literal `"mixed"` would bind a string-valued JSX attr