@barefootjs/mojolicious 0.8.0 → 0.9.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025-present BarefootJS Contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -8,7 +8,10 @@ import {
8
8
  identifierPath,
9
9
  emitParsedExpr,
10
10
  emitIRNode,
11
- emitAttrValue
11
+ emitAttrValue,
12
+ augmentInheritedPropAccesses,
13
+ parseRecordIndexAccess,
14
+ evalStringArrayJoin
12
15
  } from "@barefootjs/jsx";
13
16
 
14
17
  // src/adapter/boolean-result.ts
@@ -95,7 +98,10 @@ function parsePureStringLiteral(source) {
95
98
  if (ts.isStringLiteral(init) || ts.isNoSubstitutionTemplateLiteral(init)) {
96
99
  return init.text;
97
100
  }
98
- return null;
101
+ return evalStringArrayJoin(source);
102
+ }
103
+ function perlHashKey(name) {
104
+ return /^[A-Za-z_][A-Za-z0-9_]*$/.test(name) ? name : `'${name.replace(/'/g, "\\'")}'`;
99
105
  }
100
106
 
101
107
  class MojoAdapter extends BaseAdapter {
@@ -112,6 +118,7 @@ class MojoAdapter extends BaseAdapter {
112
118
  propsParams = [];
113
119
  stringValueNames = new Set;
114
120
  moduleStringConsts = new Map;
121
+ localConstants = [];
115
122
  loopBoundNames = new Map;
116
123
  nullableOptionalProps = new Set;
117
124
  constructor(options = {}) {
@@ -124,6 +131,7 @@ class MojoAdapter extends BaseAdapter {
124
131
  generate(ir, options) {
125
132
  this.componentName = ir.metadata.componentName;
126
133
  this.propsObjectName = ir.metadata.propsObjectName ?? null;
134
+ augmentInheritedPropAccesses(ir);
127
135
  this.propsParams = ir.metadata.propsParams.map((p) => ({ name: p.name }));
128
136
  this.nullableOptionalProps = new Set(ir.metadata.propsParams.filter((p) => p.defaultValue === undefined && !p.isRest && p.type?.kind !== "primitive").map((p) => p.name));
129
137
  this.stringValueNames = new Set;
@@ -137,6 +145,7 @@ class MojoAdapter extends BaseAdapter {
137
145
  this.stringValueNames.add(p.name);
138
146
  }
139
147
  this.moduleStringConsts = this.collectModuleStringConsts(ir.metadata.localConstants);
148
+ this.localConstants = ir.metadata.localConstants ?? [];
140
149
  this.loopBoundNames.clear();
141
150
  this.errors = [];
142
151
  this.childrenCaptureCounter = 0;
@@ -499,20 +508,20 @@ ${renderedChildren}` : renderedChildren;
499
508
  `);
500
509
  }
501
510
  componentPropEmitter = {
502
- emitLiteral: (value, name) => `${name} => '${value.value}'`,
511
+ emitLiteral: (value, name) => `${perlHashKey(name)} => '${value.value}'`,
503
512
  emitExpression: (value, name) => {
504
513
  if (value.parts) {
505
- return `${name} => ${this.convertTemplateLiteralPartsToPerl(value.parts)}`;
514
+ return `${perlHashKey(name)} => ${this.convertTemplateLiteralPartsToPerl(value.parts)}`;
506
515
  }
507
- return `${name} => ${this.convertExpressionToPerl(value.expr)}`;
516
+ return `${perlHashKey(name)} => ${this.convertExpressionToPerl(value.expr)}`;
508
517
  },
509
518
  emitSpread: (value) => {
510
519
  const perlExpr = this.convertExpressionToPerl(value.expr);
511
520
  return perlExpr.startsWith("%") ? perlExpr : `%{${perlExpr}}`;
512
521
  },
513
- emitTemplate: (value, name) => `${name} => ${this.convertTemplateLiteralPartsToPerl(value.parts)}`,
514
- emitBooleanAttr: (_value, name) => `${name} => 1`,
515
- emitBooleanShorthand: (_value, name) => `${name} => 1`,
522
+ emitTemplate: (value, name) => `${perlHashKey(name)} => ${this.convertTemplateLiteralPartsToPerl(value.parts)}`,
523
+ emitBooleanAttr: (_value, name) => `${perlHashKey(name)} => 1`,
524
+ emitBooleanShorthand: (_value, name) => `${perlHashKey(name)} => 1`,
516
525
  emitJsxChildren: () => ""
517
526
  };
518
527
  renderComponent(comp) {
@@ -585,7 +594,8 @@ ${children}`;
585
594
  return "";
586
595
  }
587
596
  const bareId = value.expr.trim();
588
- if (!isBooleanAttr(name) && !value.presenceOrUndefined && this.nullableOptionalProps.has(bareId)) {
597
+ const normalizedBareId = this.propsObjectName && bareId.startsWith(`${this.propsObjectName}.`) ? bareId.slice(this.propsObjectName.length + 1) : bareId;
598
+ if (!isBooleanAttr(name) && !value.presenceOrUndefined && /^[A-Za-z_$][\w$]*$/.test(normalizedBareId) && this.nullableOptionalProps.has(normalizedBareId)) {
589
599
  const perl2 = this.convertExpressionToPerl(value.expr);
590
600
  const body = isBooleanResultExpr(value.expr) || isAriaBooleanAttr(name) ? `${name}="<%= bf->bool_str(${perl2}) %>"` : `${name}="<%= ${perl2} %>"`;
591
601
  return `<% if (defined ${perl2}) { %>${body}<% } %>`;
@@ -614,6 +624,18 @@ ${children}`;
614
624
  if (ternaryHashref !== null) {
615
625
  return `<%== bf->spread_attrs(${ternaryHashref}) %>`;
616
626
  }
627
+ if (/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(trimmed)) {
628
+ const localConst = this.localConstants.find((c) => c.name === trimmed && !c.isModule);
629
+ if (localConst?.value !== undefined) {
630
+ const initTrimmed = localConst.value.trim();
631
+ if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(initTrimmed)) {
632
+ const resolved = this.conditionalSpreadToPerl(initTrimmed);
633
+ if (resolved !== null) {
634
+ return `<%== bf->spread_attrs(${resolved}) %>`;
635
+ }
636
+ }
637
+ }
638
+ }
617
639
  const perlExpr = this.convertExpressionToPerl(value.expr);
618
640
  return `<%== bf->spread_attrs(${perlExpr}) %>`;
619
641
  },
@@ -821,11 +843,28 @@ ${reason}` : "";
821
843
  } else {
822
844
  return null;
823
845
  }
824
- const valPerl = this.convertExpressionToPerl(prop.initializer.getText(sf));
846
+ const initNode = (() => {
847
+ let n = prop.initializer;
848
+ while (ts.isParenthesizedExpression(n))
849
+ n = n.expression;
850
+ return n;
851
+ })();
852
+ const indexed = this.recordIndexAccessToPerl(initNode);
853
+ const valPerl = indexed !== null ? indexed : this.convertExpressionToPerl(prop.initializer.getText(sf));
825
854
  entries.push(`'${key.replace(/'/g, "\\'")}' => ${valPerl}`);
826
855
  }
827
856
  return entries.length === 0 ? "{}" : `{ ${entries.join(", ")} }`;
828
857
  }
858
+ recordIndexAccessToPerl(val) {
859
+ const parsed = parseRecordIndexAccess(val, this.localConstants, this.propsParams);
860
+ if (!parsed)
861
+ return null;
862
+ const entries = parsed.entries.map((e) => {
863
+ const mapVal = e.value.kind === "number" ? e.value.text : `'${e.value.text.replace(/'/g, "\\'")}'`;
864
+ return `'${e.key.replace(/'/g, "\\'")}' => ${mapVal}`;
865
+ });
866
+ return `{ ${entries.join(", ")} }->{$${parsed.indexPropName}}`;
867
+ }
829
868
  convertExpressionToPerl(expr) {
830
869
  const trimmed = expr.trim();
831
870
  if (trimmed === "")
@@ -69,6 +69,14 @@ export declare class MojoAdapter extends BaseAdapter implements IRNodeEmitter<Mo
69
69
  * module-scope pure string literals qualify (see `collectModuleStringConsts`).
70
70
  */
71
71
  private moduleStringConsts;
72
+ /**
73
+ * Full local-constant metadata from the entry IR, kept so spread
74
+ * lowering can resolve a bare-identifier spread (`{...sizeAttrs}`) to
75
+ * its initializer text and a `Record[propKey]` spread value to the
76
+ * module-const object literal it indexes (#checkbox / icon). Populated
77
+ * at `generate()` entry alongside `moduleStringConsts`.
78
+ */
79
+ private localConstants;
72
80
  /**
73
81
  * Names currently bound by an enclosing loop body — the `my $<param>` and
74
82
  * `my $<index>` bindings `renderLoop` introduces — ref-counted so nested
@@ -245,6 +253,21 @@ export declare class MojoAdapter extends BaseAdapter implements IRNodeEmitter<Mo
245
253
  * computed/spread/dynamic key. Empty object → `{}`.
246
254
  */
247
255
  private objectLiteralToPerlHashref;
256
+ /**
257
+ * Lower a spread-object VALUE of the form `IDENT[KEY]` where:
258
+ * - `IDENT` resolves via `localConstants` to a MODULE-scope object
259
+ * literal whose property values are all scalar (number/string)
260
+ * literals under static (string-literal or identifier) keys
261
+ * (a `Record<staticKeys, scalar>` map like `sizeMap`), AND
262
+ * - `KEY` is a bare identifier that is a prop.
263
+ * Emits an inline indexed Perl hashref:
264
+ * `{ 'sm' => 16, 'md' => 20, ... }->{$size}`
265
+ *
266
+ * Returns the Perl string when convertible, else `null` so the caller
267
+ * falls back to its normal value lowering (which records BF101 for an
268
+ * unsupported shape). Mirror of the Go adapter's `recordIndexAccessToGoMap`.
269
+ */
270
+ private recordIndexAccessToPerl;
248
271
  private convertExpressionToPerl;
249
272
  /**
250
273
  * Render a full ParsedExpr tree to Perl for top-level (non-filter)
@@ -1 +1 @@
1
- {"version":3,"file":"mojo-adapter.d.ts","sourceRoot":"","sources":["../../src/adapter/mojo-adapter.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"mojo-adapter.d.ts","sourceRoot":"","sources":["../../src/adapter/mojo-adapter.ts"],"names":[],"mappings":"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"}
package/dist/build.js CHANGED
@@ -8,7 +8,10 @@ import {
8
8
  identifierPath,
9
9
  emitParsedExpr,
10
10
  emitIRNode,
11
- emitAttrValue
11
+ emitAttrValue,
12
+ augmentInheritedPropAccesses,
13
+ parseRecordIndexAccess,
14
+ evalStringArrayJoin
12
15
  } from "@barefootjs/jsx";
13
16
 
14
17
  // src/adapter/boolean-result.ts
@@ -95,7 +98,10 @@ function parsePureStringLiteral(source) {
95
98
  if (ts.isStringLiteral(init) || ts.isNoSubstitutionTemplateLiteral(init)) {
96
99
  return init.text;
97
100
  }
98
- return null;
101
+ return evalStringArrayJoin(source);
102
+ }
103
+ function perlHashKey(name) {
104
+ return /^[A-Za-z_][A-Za-z0-9_]*$/.test(name) ? name : `'${name.replace(/'/g, "\\'")}'`;
99
105
  }
100
106
 
101
107
  class MojoAdapter extends BaseAdapter {
@@ -112,6 +118,7 @@ class MojoAdapter extends BaseAdapter {
112
118
  propsParams = [];
113
119
  stringValueNames = new Set;
114
120
  moduleStringConsts = new Map;
121
+ localConstants = [];
115
122
  loopBoundNames = new Map;
116
123
  nullableOptionalProps = new Set;
117
124
  constructor(options = {}) {
@@ -124,6 +131,7 @@ class MojoAdapter extends BaseAdapter {
124
131
  generate(ir, options) {
125
132
  this.componentName = ir.metadata.componentName;
126
133
  this.propsObjectName = ir.metadata.propsObjectName ?? null;
134
+ augmentInheritedPropAccesses(ir);
127
135
  this.propsParams = ir.metadata.propsParams.map((p) => ({ name: p.name }));
128
136
  this.nullableOptionalProps = new Set(ir.metadata.propsParams.filter((p) => p.defaultValue === undefined && !p.isRest && p.type?.kind !== "primitive").map((p) => p.name));
129
137
  this.stringValueNames = new Set;
@@ -137,6 +145,7 @@ class MojoAdapter extends BaseAdapter {
137
145
  this.stringValueNames.add(p.name);
138
146
  }
139
147
  this.moduleStringConsts = this.collectModuleStringConsts(ir.metadata.localConstants);
148
+ this.localConstants = ir.metadata.localConstants ?? [];
140
149
  this.loopBoundNames.clear();
141
150
  this.errors = [];
142
151
  this.childrenCaptureCounter = 0;
@@ -499,20 +508,20 @@ ${renderedChildren}` : renderedChildren;
499
508
  `);
500
509
  }
501
510
  componentPropEmitter = {
502
- emitLiteral: (value, name) => `${name} => '${value.value}'`,
511
+ emitLiteral: (value, name) => `${perlHashKey(name)} => '${value.value}'`,
503
512
  emitExpression: (value, name) => {
504
513
  if (value.parts) {
505
- return `${name} => ${this.convertTemplateLiteralPartsToPerl(value.parts)}`;
514
+ return `${perlHashKey(name)} => ${this.convertTemplateLiteralPartsToPerl(value.parts)}`;
506
515
  }
507
- return `${name} => ${this.convertExpressionToPerl(value.expr)}`;
516
+ return `${perlHashKey(name)} => ${this.convertExpressionToPerl(value.expr)}`;
508
517
  },
509
518
  emitSpread: (value) => {
510
519
  const perlExpr = this.convertExpressionToPerl(value.expr);
511
520
  return perlExpr.startsWith("%") ? perlExpr : `%{${perlExpr}}`;
512
521
  },
513
- emitTemplate: (value, name) => `${name} => ${this.convertTemplateLiteralPartsToPerl(value.parts)}`,
514
- emitBooleanAttr: (_value, name) => `${name} => 1`,
515
- emitBooleanShorthand: (_value, name) => `${name} => 1`,
522
+ emitTemplate: (value, name) => `${perlHashKey(name)} => ${this.convertTemplateLiteralPartsToPerl(value.parts)}`,
523
+ emitBooleanAttr: (_value, name) => `${perlHashKey(name)} => 1`,
524
+ emitBooleanShorthand: (_value, name) => `${perlHashKey(name)} => 1`,
516
525
  emitJsxChildren: () => ""
517
526
  };
518
527
  renderComponent(comp) {
@@ -585,7 +594,8 @@ ${children}`;
585
594
  return "";
586
595
  }
587
596
  const bareId = value.expr.trim();
588
- if (!isBooleanAttr(name) && !value.presenceOrUndefined && this.nullableOptionalProps.has(bareId)) {
597
+ const normalizedBareId = this.propsObjectName && bareId.startsWith(`${this.propsObjectName}.`) ? bareId.slice(this.propsObjectName.length + 1) : bareId;
598
+ if (!isBooleanAttr(name) && !value.presenceOrUndefined && /^[A-Za-z_$][\w$]*$/.test(normalizedBareId) && this.nullableOptionalProps.has(normalizedBareId)) {
589
599
  const perl2 = this.convertExpressionToPerl(value.expr);
590
600
  const body = isBooleanResultExpr(value.expr) || isAriaBooleanAttr(name) ? `${name}="<%= bf->bool_str(${perl2}) %>"` : `${name}="<%= ${perl2} %>"`;
591
601
  return `<% if (defined ${perl2}) { %>${body}<% } %>`;
@@ -614,6 +624,18 @@ ${children}`;
614
624
  if (ternaryHashref !== null) {
615
625
  return `<%== bf->spread_attrs(${ternaryHashref}) %>`;
616
626
  }
627
+ if (/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(trimmed)) {
628
+ const localConst = this.localConstants.find((c) => c.name === trimmed && !c.isModule);
629
+ if (localConst?.value !== undefined) {
630
+ const initTrimmed = localConst.value.trim();
631
+ if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(initTrimmed)) {
632
+ const resolved = this.conditionalSpreadToPerl(initTrimmed);
633
+ if (resolved !== null) {
634
+ return `<%== bf->spread_attrs(${resolved}) %>`;
635
+ }
636
+ }
637
+ }
638
+ }
617
639
  const perlExpr = this.convertExpressionToPerl(value.expr);
618
640
  return `<%== bf->spread_attrs(${perlExpr}) %>`;
619
641
  },
@@ -821,11 +843,28 @@ ${reason}` : "";
821
843
  } else {
822
844
  return null;
823
845
  }
824
- const valPerl = this.convertExpressionToPerl(prop.initializer.getText(sf));
846
+ const initNode = (() => {
847
+ let n = prop.initializer;
848
+ while (ts.isParenthesizedExpression(n))
849
+ n = n.expression;
850
+ return n;
851
+ })();
852
+ const indexed = this.recordIndexAccessToPerl(initNode);
853
+ const valPerl = indexed !== null ? indexed : this.convertExpressionToPerl(prop.initializer.getText(sf));
825
854
  entries.push(`'${key.replace(/'/g, "\\'")}' => ${valPerl}`);
826
855
  }
827
856
  return entries.length === 0 ? "{}" : `{ ${entries.join(", ")} }`;
828
857
  }
858
+ recordIndexAccessToPerl(val) {
859
+ const parsed = parseRecordIndexAccess(val, this.localConstants, this.propsParams);
860
+ if (!parsed)
861
+ return null;
862
+ const entries = parsed.entries.map((e) => {
863
+ const mapVal = e.value.kind === "number" ? e.value.text : `'${e.value.text.replace(/'/g, "\\'")}'`;
864
+ return `'${e.key.replace(/'/g, "\\'")}' => ${mapVal}`;
865
+ });
866
+ return `{ ${entries.join(", ")} }->{$${parsed.indexPropName}}`;
867
+ }
829
868
  convertExpressionToPerl(expr) {
830
869
  const trimmed = expr.trim();
831
870
  if (trimmed === "")
package/dist/index.js CHANGED
@@ -8,7 +8,10 @@ import {
8
8
  identifierPath,
9
9
  emitParsedExpr,
10
10
  emitIRNode,
11
- emitAttrValue
11
+ emitAttrValue,
12
+ augmentInheritedPropAccesses,
13
+ parseRecordIndexAccess,
14
+ evalStringArrayJoin
12
15
  } from "@barefootjs/jsx";
13
16
 
14
17
  // src/adapter/boolean-result.ts
@@ -95,7 +98,10 @@ function parsePureStringLiteral(source) {
95
98
  if (ts.isStringLiteral(init) || ts.isNoSubstitutionTemplateLiteral(init)) {
96
99
  return init.text;
97
100
  }
98
- return null;
101
+ return evalStringArrayJoin(source);
102
+ }
103
+ function perlHashKey(name) {
104
+ return /^[A-Za-z_][A-Za-z0-9_]*$/.test(name) ? name : `'${name.replace(/'/g, "\\'")}'`;
99
105
  }
100
106
 
101
107
  class MojoAdapter extends BaseAdapter {
@@ -112,6 +118,7 @@ class MojoAdapter extends BaseAdapter {
112
118
  propsParams = [];
113
119
  stringValueNames = new Set;
114
120
  moduleStringConsts = new Map;
121
+ localConstants = [];
115
122
  loopBoundNames = new Map;
116
123
  nullableOptionalProps = new Set;
117
124
  constructor(options = {}) {
@@ -124,6 +131,7 @@ class MojoAdapter extends BaseAdapter {
124
131
  generate(ir, options) {
125
132
  this.componentName = ir.metadata.componentName;
126
133
  this.propsObjectName = ir.metadata.propsObjectName ?? null;
134
+ augmentInheritedPropAccesses(ir);
127
135
  this.propsParams = ir.metadata.propsParams.map((p) => ({ name: p.name }));
128
136
  this.nullableOptionalProps = new Set(ir.metadata.propsParams.filter((p) => p.defaultValue === undefined && !p.isRest && p.type?.kind !== "primitive").map((p) => p.name));
129
137
  this.stringValueNames = new Set;
@@ -137,6 +145,7 @@ class MojoAdapter extends BaseAdapter {
137
145
  this.stringValueNames.add(p.name);
138
146
  }
139
147
  this.moduleStringConsts = this.collectModuleStringConsts(ir.metadata.localConstants);
148
+ this.localConstants = ir.metadata.localConstants ?? [];
140
149
  this.loopBoundNames.clear();
141
150
  this.errors = [];
142
151
  this.childrenCaptureCounter = 0;
@@ -499,20 +508,20 @@ ${renderedChildren}` : renderedChildren;
499
508
  `);
500
509
  }
501
510
  componentPropEmitter = {
502
- emitLiteral: (value, name) => `${name} => '${value.value}'`,
511
+ emitLiteral: (value, name) => `${perlHashKey(name)} => '${value.value}'`,
503
512
  emitExpression: (value, name) => {
504
513
  if (value.parts) {
505
- return `${name} => ${this.convertTemplateLiteralPartsToPerl(value.parts)}`;
514
+ return `${perlHashKey(name)} => ${this.convertTemplateLiteralPartsToPerl(value.parts)}`;
506
515
  }
507
- return `${name} => ${this.convertExpressionToPerl(value.expr)}`;
516
+ return `${perlHashKey(name)} => ${this.convertExpressionToPerl(value.expr)}`;
508
517
  },
509
518
  emitSpread: (value) => {
510
519
  const perlExpr = this.convertExpressionToPerl(value.expr);
511
520
  return perlExpr.startsWith("%") ? perlExpr : `%{${perlExpr}}`;
512
521
  },
513
- emitTemplate: (value, name) => `${name} => ${this.convertTemplateLiteralPartsToPerl(value.parts)}`,
514
- emitBooleanAttr: (_value, name) => `${name} => 1`,
515
- emitBooleanShorthand: (_value, name) => `${name} => 1`,
522
+ emitTemplate: (value, name) => `${perlHashKey(name)} => ${this.convertTemplateLiteralPartsToPerl(value.parts)}`,
523
+ emitBooleanAttr: (_value, name) => `${perlHashKey(name)} => 1`,
524
+ emitBooleanShorthand: (_value, name) => `${perlHashKey(name)} => 1`,
516
525
  emitJsxChildren: () => ""
517
526
  };
518
527
  renderComponent(comp) {
@@ -585,7 +594,8 @@ ${children}`;
585
594
  return "";
586
595
  }
587
596
  const bareId = value.expr.trim();
588
- if (!isBooleanAttr(name) && !value.presenceOrUndefined && this.nullableOptionalProps.has(bareId)) {
597
+ const normalizedBareId = this.propsObjectName && bareId.startsWith(`${this.propsObjectName}.`) ? bareId.slice(this.propsObjectName.length + 1) : bareId;
598
+ if (!isBooleanAttr(name) && !value.presenceOrUndefined && /^[A-Za-z_$][\w$]*$/.test(normalizedBareId) && this.nullableOptionalProps.has(normalizedBareId)) {
589
599
  const perl2 = this.convertExpressionToPerl(value.expr);
590
600
  const body = isBooleanResultExpr(value.expr) || isAriaBooleanAttr(name) ? `${name}="<%= bf->bool_str(${perl2}) %>"` : `${name}="<%= ${perl2} %>"`;
591
601
  return `<% if (defined ${perl2}) { %>${body}<% } %>`;
@@ -614,6 +624,18 @@ ${children}`;
614
624
  if (ternaryHashref !== null) {
615
625
  return `<%== bf->spread_attrs(${ternaryHashref}) %>`;
616
626
  }
627
+ if (/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(trimmed)) {
628
+ const localConst = this.localConstants.find((c) => c.name === trimmed && !c.isModule);
629
+ if (localConst?.value !== undefined) {
630
+ const initTrimmed = localConst.value.trim();
631
+ if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(initTrimmed)) {
632
+ const resolved = this.conditionalSpreadToPerl(initTrimmed);
633
+ if (resolved !== null) {
634
+ return `<%== bf->spread_attrs(${resolved}) %>`;
635
+ }
636
+ }
637
+ }
638
+ }
617
639
  const perlExpr = this.convertExpressionToPerl(value.expr);
618
640
  return `<%== bf->spread_attrs(${perlExpr}) %>`;
619
641
  },
@@ -821,11 +843,28 @@ ${reason}` : "";
821
843
  } else {
822
844
  return null;
823
845
  }
824
- const valPerl = this.convertExpressionToPerl(prop.initializer.getText(sf));
846
+ const initNode = (() => {
847
+ let n = prop.initializer;
848
+ while (ts.isParenthesizedExpression(n))
849
+ n = n.expression;
850
+ return n;
851
+ })();
852
+ const indexed = this.recordIndexAccessToPerl(initNode);
853
+ const valPerl = indexed !== null ? indexed : this.convertExpressionToPerl(prop.initializer.getText(sf));
825
854
  entries.push(`'${key.replace(/'/g, "\\'")}' => ${valPerl}`);
826
855
  }
827
856
  return entries.length === 0 ? "{}" : `{ ${entries.join(", ")} }`;
828
857
  }
858
+ recordIndexAccessToPerl(val) {
859
+ const parsed = parseRecordIndexAccess(val, this.localConstants, this.propsParams);
860
+ if (!parsed)
861
+ return null;
862
+ const entries = parsed.entries.map((e) => {
863
+ const mapVal = e.value.kind === "number" ? e.value.text : `'${e.value.text.replace(/'/g, "\\'")}'`;
864
+ return `'${e.key.replace(/'/g, "\\'")}' => ${mapVal}`;
865
+ });
866
+ return `{ ${entries.join(", ")} }->{$${parsed.indexPropName}}`;
867
+ }
829
868
  convertExpressionToPerl(expr) {
830
869
  const trimmed = expr.trim();
831
870
  if (trimmed === "")
@@ -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;CACpC;AAED,wBAAsB,mBAAmB,CAAC,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,MAAM,CAAC,CA2MjF;AAsOD;;;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;CACpC;AAED,wBAAsB,mBAAmB,CAAC,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,MAAM,CAAC,CA2MjF;AA+RD;;;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.01";
2
+ our $VERSION = "0.8.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.01";
2
+ our $VERSION = "0.8.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.01";
2
+ our $VERSION = "0.8.0";
3
3
  use Mojo::Base 'Mojolicious::Plugin', -signatures;
4
4
 
5
5
  use Mojo::File qw(path);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@barefootjs/mojolicious",
3
- "version": "0.8.0",
3
+ "version": "0.9.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,8 +52,8 @@
52
52
  "directory": "packages/adapter-mojolicious"
53
53
  },
54
54
  "dependencies": {
55
- "@barefootjs/perl": "0.8.0",
56
- "@barefootjs/shared": "0.8.0"
55
+ "@barefootjs/perl": "0.9.0",
56
+ "@barefootjs/shared": "0.9.0"
57
57
  },
58
58
  "peerDependencies": {
59
59
  "@barefootjs/jsx": ">=0.2.0",
@@ -61,6 +61,6 @@
61
61
  },
62
62
  "devDependencies": {
63
63
  "@barefootjs/adapter-tests": "0.1.0",
64
- "@barefootjs/jsx": "0.8.0"
64
+ "@barefootjs/jsx": "0.9.0"
65
65
  }
66
66
  }
@@ -17,76 +17,19 @@ runAdapterConformanceTests({
17
17
  name: 'mojo',
18
18
  factory: () => new MojoAdapter(),
19
19
  render: renderMojoComponent,
20
- // Dynamic style objects (non-static values) require Perl template
21
- // interpolation support for JS object literals, not yet implemented.
22
- // Mojo currently emits invalid Perl silently for this shape — the
23
- // Go adapter records BF101 via `convertExpressionToGo()` for the
24
- // same fixture (now contracted via `expectedDiagnostics`), but the
25
- // Mojo adapter's expression gate doesn't yet lift the same
26
- // failure into a `CompilerError`, so the fixture stays on `skipJsx`
27
- // until that gate is extended (#1266 follow-up).
28
- // `logical-or-jsx`, `nullish-coalescing-jsx`, `branch-map` reference
29
- // a prop directly inside a conditional branch (`$label`, `$banner`,
30
- // `$active`). The Mojo adapter emits these as bare Perl variables
31
- // (`% if ($label) { ... }`) without a corresponding
32
- // `my $label = ...;` declaration, so Perl rejects the template with
33
- // "Global symbol requires explicit package name". Same class of
34
- // Perl-scoping divergence that motivates the existing skips —
35
- // out of scope for the #971 refactor.
36
- // Return-position variants of the same divergence —
37
- // `return-logical-or` / `return-nullish-coalescing` reference
38
- // `$label` / `$banner` directly; `return-map` iterates over `$items`
39
- // without a `my` declaration.
40
- //
41
- // `static-array-children` / `static-array-from-props` /
42
- // `static-array-from-props-with-component` are no longer here —
43
- // they're covered by `expectedDiagnostics` below, asserting that
44
- // the adapter emits `BF103` / `BF104` at build time instead of
45
- // silently emitting invalid Perl / unresolved cross-template
46
- // references (#1266).
47
20
  skipJsx: [
48
- 'style-object-dynamic',
49
- 'logical-or-jsx',
50
- 'nullish-coalescing-jsx',
51
- 'branch-map',
52
- 'return-logical-or',
53
- 'return-nullish-coalescing',
54
- 'return-map',
55
- // #1297 fixed the harness-side IR emission gate. The remaining
56
- // gap is adapter-side: the Mojo adapter has no SSR context-
57
- // propagation mechanism, so `<Ctx.Provider value="dark">` doesn't
58
- // make `useContext(Ctx)` resolve to `"dark"` at template-eval
59
- // time — the template emits `<%= $theme %>` against a hash that
60
- // never receives a `theme` key. Provider SSR coverage on Mojo
61
- // waits on that adapter feature; see #1297 follow-up.
21
+ // SSR context propagation (`<Ctx.Provider value>` → `useContext`): the
22
+ // template reads a stash key that's never seeded. Implemented on Go; the
23
+ // Perl stash-seed path is a follow-up port, so Mojo stays skipped (#1297).
62
24
  'context-provider',
63
- // Multi-component fixtures still diverge because Mojo's child
64
- // template emitter pins the child's `bf-s` to the literal
65
- // `test_<sN>` (`_scope_id("test_$sid")` in `test-render.ts`)
66
- // instead of `<ChildName>_<id>_<sN>` like Hono / CSR. Same family
67
- // of test-harness scope-id plumbing the `componentName` option
68
- // fixed on the Hono side. Separate follow-up.
25
+ // Multi-component shared fixtures with per-item loop state. The child
26
+ // scope-id plumbing is now fixed (children derive `<parentScope>_<sN>`
27
+ // from `$bf->_scope_id` in `test-render.ts`, which unblocked
28
+ // `reactive-props`); these two still diverge on additional per-item
29
+ // loop-state seeding the Mojo harness doesn't yet replicate. (xslate
30
+ // skips the same pair.)
69
31
  'toggle-shared',
70
- 'reactive-props',
71
32
  'props-reactivity-comparison',
72
- // #1467 Phase 2b: basic interactive `site/ui` primitives. Cross-adapter
73
- // parity for the `site/ui` corpus is Phase 3, so these participate only
74
- // in Hono SSR conformance + the fixture-hydrate runtime layer for now.
75
- // (`label` and `kbd` are static helpers; `toggle` / `switch` /
76
- // `checkbox` carry uncontrolled state; `input` is a pass-through
77
- // native control.)
78
- //
79
- // `textarea` now participates: its conditional inline-object spread
80
- // (`{...(describedBy ? {...} : {})}`) lowers via
81
- // `conditionalSpreadToPerl`, and its optional `rows={rows}`
82
- // attribute is omitted when `undef` via the `defined`-guard in
83
- // `elementAttrEmitter.emitExpression`
84
- // (`<% if (defined $rows) { %>rows="<%= $rows %>"<% } %>`), matching
85
- // Hono's nullish-attribute omission.
86
- 'toggle',
87
- 'switch',
88
- 'checkbox',
89
- 'kbd',
90
33
  ],
91
34
  // Per-fixture build-time contracts for shapes the Mojo adapter
92
35
  // intentionally refuses to lower. Owned by this adapter test file
@@ -130,6 +73,11 @@ runAdapterConformanceTests({
130
73
  // surfaces BF101 with a wrap-in-`/* @client */` suggestion, matching
131
74
  // the Go adapter's behaviour.
132
75
  'style-3-signals': [{ code: 'BF101', severity: 'error' }],
76
+ // Dynamic JS object literal in `style={{ … }}` — same no-EP-form
77
+ // refusal as `style-3-signals`; `refuseUnsupportedAttrExpression`
78
+ // surfaces BF101 (mirrors the xslate + Go adapters). Was a stale
79
+ // `skipJsx` entry claiming the gate didn't lift to a CompilerError.
80
+ 'style-object-dynamic': [{ code: 'BF101', severity: 'error' }],
133
81
  // #1244 stress catalog #12 (#1323): tagged-template-literal call
134
82
  // (`cn\`base \${tone()}\``) — same family as #1322 above and refused
135
83
  // via the same gate.
@@ -287,6 +235,120 @@ function Box({ k, v }: { k?: string; v?: string }) {
287
235
  })
288
236
  })
289
237
 
238
+ describe('MojoAdapter - local-const conditional-spread resolution (#checkbox icon)', () => {
239
+ // A FUNCTION-scope const holding a `cond ? {…} : {}` ternary, spread as
240
+ // a bare identifier (`{...attrs}`), resolves through the same Perl
241
+ // ternary-of-hashrefs lowering as the inline form. CheckIcon's
242
+ // `const sizeAttrs = size ? {…} : {}` is exactly this shape.
243
+ test('resolves a bare-identifier spread of a function-scope conditional const', () => {
244
+ const { template } = compileAndGenerate(`
245
+ function Box({ flag }: { flag?: boolean }) {
246
+ const attrs = flag ? { 'data-on': 'yes' } : {}
247
+ return <div {...attrs} />
248
+ }
249
+ `)
250
+ expect(template).toContain(
251
+ "bf->spread_attrs($flag ? { 'data-on' => 'yes' } : {})",
252
+ )
253
+ })
254
+
255
+ // A const that aliases another bare identifier must NOT be forwarded
256
+ // (loop guard): the resolver bails, so the spread falls through to the
257
+ // standard `convertExpressionToPerl` path emitting the bare `$attrs`
258
+ // variable rather than recursively resolving the alias into a hashref.
259
+ test('does not forward a const that aliases another identifier (loop guard)', () => {
260
+ const { template } = compileAndGenerate(`
261
+ function Box({ other }: { other?: object }) {
262
+ const attrs = other
263
+ return <div {...attrs} />
264
+ }
265
+ `)
266
+ expect(template).toContain('bf->spread_attrs($attrs)')
267
+ })
268
+ })
269
+
270
+ describe('MojoAdapter - Record<staticKeys,scalar>[propKey] spread value (#checkbox icon)', () => {
271
+ // `const sizeMap: Record<IconSize, number> = { sm: 16, ... }` indexed by
272
+ // a prop inside a conditional-spread object value lowers to an inline
273
+ // indexed Perl hashref `{ ... }->{$key}`. This is CheckIcon's
274
+ // `{ width: sizeMap[size], height: sizeMap[size] }` shape.
275
+ test('lowers an indexed module-const map to an inline hashref index', () => {
276
+ const { template } = compileAndGenerate(`
277
+ const sizeMap: Record<string, number> = { sm: 16, md: 20, lg: 24, xl: 32 }
278
+ function Box({ size }: { size?: string }) {
279
+ const attrs = size ? { width: sizeMap[size] } : {}
280
+ return <div {...attrs} />
281
+ }
282
+ `)
283
+ expect(template).toContain(
284
+ "{ 'sm' => 16, 'md' => 20, 'lg' => 24, 'xl' => 32 }->{$size}",
285
+ )
286
+ })
287
+
288
+ test('lowers string-valued record maps too', () => {
289
+ const { template } = compileAndGenerate(`
290
+ const labelMap: Record<string, string> = { a: 'Alpha', b: 'Beta' }
291
+ function Box({ k }: { k?: string }) {
292
+ const attrs = k ? { 'data-label': labelMap[k] } : {}
293
+ return <div {...attrs} />
294
+ }
295
+ `)
296
+ expect(template).toContain("{ 'a' => 'Alpha', 'b' => 'Beta' }->{$k}")
297
+ })
298
+
299
+ // A non-scalar record value (object) is out of shape: the spread object
300
+ // value can't lower, so the whole spread falls back to BF101.
301
+ test('refuses a non-scalar record value with BF101 (out-of-shape fallback)', () => {
302
+ const adapter = new MojoAdapter()
303
+ const ir = compileToIR(`
304
+ const sizeMap: Record<string, object> = { sm: { w: 1 } }
305
+ function Box({ size }: { size?: string }) {
306
+ const attrs = size ? { width: sizeMap[size] } : {}
307
+ return <div {...attrs} />
308
+ }
309
+ `, adapter)
310
+ adapter.generate(ir)
311
+ const errs = (adapter as unknown as { errors: { code: string }[] }).errors
312
+ expect(errs.some(e => e.code === 'BF101')).toBe(true)
313
+ })
314
+ })
315
+
316
+ describe('MojoAdapter - props-object inherited-attribute enumeration (#checkbox)', () => {
317
+ // A SolidJS props-object component reads inherited attributes (`props.id`)
318
+ // not enumerated in `propsParams`. The bare optional attribute must be
319
+ // guarded with Perl `defined` so it's omitted when unset (Hono parity),
320
+ // even though `id` isn't a declared param.
321
+ test('guards a props-object bare optional attr (props.id) with defined', () => {
322
+ const { template } = compileAndGenerate(`
323
+ "use client"
324
+ interface P { tone?: string }
325
+ export function Widget(props: P) {
326
+ return <button id={props.id}>x</button>
327
+ }
328
+ `)
329
+ expect(template).toContain('<% if (defined $id) { %>id="<%= $id %>"<% } %>')
330
+ })
331
+ })
332
+
333
+ describe('MojoAdapter - hyphenated child attr hash key (#checkbox)', () => {
334
+ // A child component prop whose JSX name isn't a bare Perl identifier
335
+ // (`<CheckIcon data-slot="..."/>`) must be quoted in the `render_child`
336
+ // named-arg list — an unquoted `data-slot => ...` parses as `data - slot`.
337
+ test('quotes a hyphenated child attribute name in render_child', () => {
338
+ const { template } = compileAndGenerate(`
339
+ "use client"
340
+ import { Leaf } from './leaf'
341
+ export function Host() {
342
+ return <div><Leaf data-slot="indicator" size="sm" /></div>
343
+ }
344
+ `)
345
+ expect(template).toContain("'data-slot' => 'indicator'")
346
+ // A bare-identifier name stays unquoted.
347
+ expect(template).toContain('size => ')
348
+ expect(template).not.toContain('data-slot => ')
349
+ })
350
+ })
351
+
290
352
  describe('MojoAdapter - nullish optional-attribute omission (textarea rows)', () => {
291
353
  // A no-destructure-default, nillable-typed prop is `undef` when the
292
354
  // caller omits it; guard its bare-reference attribute with Perl
@@ -47,6 +47,9 @@ import {
47
47
  emitParsedExpr,
48
48
  emitIRNode,
49
49
  emitAttrValue,
50
+ augmentInheritedPropAccesses,
51
+ parseRecordIndexAccess,
52
+ evalStringArrayJoin,
50
53
  } from '@barefootjs/jsx'
51
54
  import { isAriaBooleanAttr, isBooleanResultExpr } from './boolean-result'
52
55
 
@@ -142,7 +145,20 @@ function parsePureStringLiteral(source: string): string | null {
142
145
  if (ts.isStringLiteral(init) || ts.isNoSubstitutionTemplateLiteral(init)) {
143
146
  return init.text
144
147
  }
145
- return null
148
+ // `[<literals>].join(' ')` module consts (e.g. Switch's `trackStateClasses`)
149
+ // → inline the flattened string byte-for-byte. See `evalStringArrayJoin`.
150
+ return evalStringArrayJoin(source)
151
+ }
152
+
153
+ /**
154
+ * (#checkbox) Quote a `render_child` named-arg / hashref key when it isn't a
155
+ * bare Perl identifier. A JSX attribute name like `data-slot` would otherwise
156
+ * emit `data-slot => '...'`, which Perl parses as the subtraction
157
+ * `data - slot`. Identifier-safe names (`className`, `size`, `_bf_slot`) pass
158
+ * through unquoted to keep the generated template readable.
159
+ */
160
+ function perlHashKey(name: string): string {
161
+ return /^[A-Za-z_][A-Za-z0-9_]*$/.test(name) ? name : `'${name.replace(/'/g, "\\'")}'`
146
162
  }
147
163
 
148
164
  export class MojoAdapter extends BaseAdapter implements IRNodeEmitter<MojoRenderCtx> {
@@ -198,6 +214,14 @@ export class MojoAdapter extends BaseAdapter implements IRNodeEmitter<MojoRender
198
214
  * module-scope pure string literals qualify (see `collectModuleStringConsts`).
199
215
  */
200
216
  private moduleStringConsts: Map<string, string> = new Map()
217
+ /**
218
+ * Full local-constant metadata from the entry IR, kept so spread
219
+ * lowering can resolve a bare-identifier spread (`{...sizeAttrs}`) to
220
+ * its initializer text and a `Record[propKey]` spread value to the
221
+ * module-const object literal it indexes (#checkbox / icon). Populated
222
+ * at `generate()` entry alongside `moduleStringConsts`.
223
+ */
224
+ private localConstants: IRMetadata['localConstants'] = []
201
225
  /**
202
226
  * Names currently bound by an enclosing loop body — the `my $<param>` and
203
227
  * `my $<index>` bindings `renderLoop` introduces — ref-counted so nested
@@ -236,6 +260,14 @@ export class MojoAdapter extends BaseAdapter implements IRNodeEmitter<MojoRender
236
260
  generate(ir: ComponentIR, options?: AdapterGenerateOptions): AdapterOutput {
237
261
  this.componentName = ir.metadata.componentName
238
262
  this.propsObjectName = ir.metadata.propsObjectName ?? null
263
+ // (#checkbox) Enumerate inherited-attribute accesses for the props-object
264
+ // pattern (`function Checkbox(props: CheckboxProps)`) before deriving
265
+ // `nullableOptionalProps`, so a bare optional attribute like
266
+ // `id={props.id}` gets the Perl `defined`-guard (Hono-style omission).
267
+ // Shared with the Go adapter (single source of truth in `@barefootjs/jsx`).
268
+ // The harness separately declares the matching stash vars (Pass-1 IR
269
+ // serialization happens before `generate`, so this mutation doesn't reach it).
270
+ augmentInheritedPropAccesses(ir)
239
271
  this.propsParams = ir.metadata.propsParams.map(p => ({ name: p.name }))
240
272
  // No-destructure-default props → `undef` when the caller omits them
241
273
  // → guard their bare-reference attribute emission with Perl `defined`
@@ -277,6 +309,7 @@ export class MojoAdapter extends BaseAdapter implements IRNodeEmitter<MojoRender
277
309
  if (isStringTypeInfo(p.type)) this.stringValueNames.add(p.name)
278
310
  }
279
311
  this.moduleStringConsts = this.collectModuleStringConsts(ir.metadata.localConstants)
312
+ this.localConstants = ir.metadata.localConstants ?? []
280
313
  this.loopBoundNames.clear()
281
314
  this.errors = []
282
315
  this.childrenCaptureCounter = 0
@@ -808,7 +841,7 @@ export class MojoAdapter extends BaseAdapter implements IRNodeEmitter<MojoRender
808
841
  * `render_child` named-arg list.
809
842
  */
810
843
  private readonly componentPropEmitter: AttrValueEmitter = {
811
- emitLiteral: (value, name) => `${name} => '${value.value}'`,
844
+ emitLiteral: (value, name) => `${perlHashKey(name)} => '${value.value}'`,
812
845
  emitExpression: (value, name) => {
813
846
  // The IR producer collapses component-prop `template` kinds
814
847
  // into `expression` for client-runtime reasons but preserves
@@ -817,9 +850,9 @@ export class MojoAdapter extends BaseAdapter implements IRNodeEmitter<MojoRender
817
850
  // `${MAP[KEY]}` shapes (the JS object literal leaks into the
818
851
  // Perl template).
819
852
  if (value.parts) {
820
- return `${name} => ${this.convertTemplateLiteralPartsToPerl(value.parts)}`
853
+ return `${perlHashKey(name)} => ${this.convertTemplateLiteralPartsToPerl(value.parts)}`
821
854
  }
822
- return `${name} => ${this.convertExpressionToPerl(value.expr)}`
855
+ return `${perlHashKey(name)} => ${this.convertExpressionToPerl(value.expr)}`
823
856
  },
824
857
  emitSpread: (value) => {
825
858
  // Perl has no JS-style spread — emit the source as a hash
@@ -832,9 +865,9 @@ export class MojoAdapter extends BaseAdapter implements IRNodeEmitter<MojoRender
832
865
  return perlExpr.startsWith('%') ? perlExpr : `%{${perlExpr}}`
833
866
  },
834
867
  emitTemplate: (value, name) =>
835
- `${name} => ${this.convertTemplateLiteralPartsToPerl(value.parts)}`,
836
- emitBooleanAttr: (_value, name) => `${name} => 1`,
837
- emitBooleanShorthand: (_value, name) => `${name} => 1`,
868
+ `${perlHashKey(name)} => ${this.convertTemplateLiteralPartsToPerl(value.parts)}`,
869
+ emitBooleanAttr: (_value, name) => `${perlHashKey(name)} => 1`,
870
+ emitBooleanShorthand: (_value, name) => `${perlHashKey(name)} => 1`,
838
871
  // JSX children flow through Mojo's `begin %>…<% end` capture
839
872
  // below; they're not part of the named-arg list.
840
873
  emitJsxChildren: () => '',
@@ -991,10 +1024,19 @@ export class MojoAdapter extends BaseAdapter implements IRNodeEmitter<MojoRender
991
1024
  // exprs, calls, concrete/defaulted props, and boolean attrs are
992
1025
  // unaffected and still emit unconditionally.
993
1026
  const bareId = value.expr.trim()
1027
+ // Normalize a props-object access (`props.id`) to its bare prop name
1028
+ // (`id`) so the nullable-optional set — keyed by bare name — matches the
1029
+ // SolidJS props-object pattern, not just destructured params (#checkbox
1030
+ // `id={props.id}`).
1031
+ const normalizedBareId =
1032
+ this.propsObjectName && bareId.startsWith(`${this.propsObjectName}.`)
1033
+ ? bareId.slice(this.propsObjectName.length + 1)
1034
+ : bareId
994
1035
  if (
995
1036
  !isBooleanAttr(name) &&
996
1037
  !value.presenceOrUndefined &&
997
- this.nullableOptionalProps.has(bareId)
1038
+ /^[A-Za-z_$][\w$]*$/.test(normalizedBareId) &&
1039
+ this.nullableOptionalProps.has(normalizedBareId)
998
1040
  ) {
999
1041
  const perl = this.convertExpressionToPerl(value.expr)
1000
1042
  const body =
@@ -1087,6 +1129,26 @@ export class MojoAdapter extends BaseAdapter implements IRNodeEmitter<MojoRender
1087
1129
  if (ternaryHashref !== null) {
1088
1130
  return `<%== bf->spread_attrs(${ternaryHashref}) %>`
1089
1131
  }
1132
+ // Function-scope local const holding a conditional inline-object
1133
+ // `const sizeAttrs = size ? {…} : {}` then `{...sizeAttrs}`
1134
+ // (#checkbox / icon). Resolve the bare identifier to its
1135
+ // initializer text and route through the same conditional-spread
1136
+ // lowering. Only function-scope (`!isModule`) consts whose value is
1137
+ // NOT itself a bare identifier (loop guard) are considered.
1138
+ if (/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(trimmed)) {
1139
+ const localConst = this.localConstants.find(
1140
+ c => c.name === trimmed && !c.isModule,
1141
+ )
1142
+ if (localConst?.value !== undefined) {
1143
+ const initTrimmed = localConst.value.trim()
1144
+ if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(initTrimmed)) {
1145
+ const resolved = this.conditionalSpreadToPerl(initTrimmed)
1146
+ if (resolved !== null) {
1147
+ return `<%== bf->spread_attrs(${resolved}) %>`
1148
+ }
1149
+ }
1150
+ }
1151
+ }
1090
1152
  const perlExpr = this.convertExpressionToPerl(value.expr)
1091
1153
  return `<%== bf->spread_attrs(${perlExpr}) %>`
1092
1154
  },
@@ -1437,12 +1499,49 @@ export class MojoAdapter extends BaseAdapter implements IRNodeEmitter<MojoRender
1437
1499
  } else {
1438
1500
  return null
1439
1501
  }
1440
- const valPerl = this.convertExpressionToPerl(prop.initializer.getText(sf))
1502
+ const initNode = (() => {
1503
+ let n: ts.Expression = prop.initializer
1504
+ while (ts.isParenthesizedExpression(n)) n = n.expression
1505
+ return n
1506
+ })()
1507
+ const indexed = this.recordIndexAccessToPerl(initNode)
1508
+ const valPerl =
1509
+ indexed !== null
1510
+ ? indexed
1511
+ : this.convertExpressionToPerl(prop.initializer.getText(sf))
1441
1512
  entries.push(`'${key.replace(/'/g, "\\'")}' => ${valPerl}`)
1442
1513
  }
1443
1514
  return entries.length === 0 ? '{}' : `{ ${entries.join(', ')} }`
1444
1515
  }
1445
1516
 
1517
+ /**
1518
+ * Lower a spread-object VALUE of the form `IDENT[KEY]` where:
1519
+ * - `IDENT` resolves via `localConstants` to a MODULE-scope object
1520
+ * literal whose property values are all scalar (number/string)
1521
+ * literals under static (string-literal or identifier) keys
1522
+ * (a `Record<staticKeys, scalar>` map like `sizeMap`), AND
1523
+ * - `KEY` is a bare identifier that is a prop.
1524
+ * Emits an inline indexed Perl hashref:
1525
+ * `{ 'sm' => 16, 'md' => 20, ... }->{$size}`
1526
+ *
1527
+ * Returns the Perl string when convertible, else `null` so the caller
1528
+ * falls back to its normal value lowering (which records BF101 for an
1529
+ * unsupported shape). Mirror of the Go adapter's `recordIndexAccessToGoMap`.
1530
+ */
1531
+ private recordIndexAccessToPerl(val: ts.Expression): string | null {
1532
+ // Shared structural parse (single source of truth in `@barefootjs/jsx`);
1533
+ // this wrapper only does the Perl-specific emit (single-quote escaping)
1534
+ // from the structured result.
1535
+ const parsed = parseRecordIndexAccess(val, this.localConstants, this.propsParams)
1536
+ if (!parsed) return null
1537
+ const entries = parsed.entries.map(e => {
1538
+ const mapVal =
1539
+ e.value.kind === 'number' ? e.value.text : `'${e.value.text.replace(/'/g, "\\'")}'`
1540
+ return `'${e.key.replace(/'/g, "\\'")}' => ${mapVal}`
1541
+ })
1542
+ return `{ ${entries.join(', ')} }->{$${parsed.indexPropName}}`
1543
+ }
1544
+
1446
1545
 
1447
1546
  private convertExpressionToPerl(expr: string): string {
1448
1547
  // Parse-first lowering — parity with the Go adapter's
@@ -349,7 +349,12 @@ function buildChildRenderers(
349
349
  }
350
350
  lines.push(` ${slotIdsPerl}`)
351
351
  lines.push(` my $child_bf = BarefootJS->new($c, {});`)
352
- lines.push(` $child_bf->_scope_id("test_$sid");`)
352
+ // Child scope id derives from the PARENT's live scope id, not a literal
353
+ // `test_` prefix — so `<ReactiveProps_test>_s5` matches Hono / CSR (and
354
+ // survives an explicit `__instanceId`). Mirrors xslate's
355
+ // `rootChildScopePrefix` (`$bf->_scope_id`); `$bf` is the parent instance
356
+ // captured by this renderer closure.
357
+ lines.push(` $child_bf->_scope_id($bf->_scope_id . "_$sid");`)
353
358
  lines.push(` my $rendered = $child_mt->render($child_tmpl, { %$child_props, bf => $child_bf });`)
354
359
  lines.push(` die $rendered->to_string if ref $rendered;`)
355
360
  lines.push(` chomp $rendered;`)
@@ -433,6 +438,26 @@ function buildPerlProps(
433
438
  entries.push(`${param.name} => undef`)
434
439
  }
435
440
 
441
+ // (#checkbox) SolidJS props-object pattern: `function Checkbox(props:
442
+ // CheckboxProps)` only enumerates `CheckboxProps`'s own members in
443
+ // `propsParams`; inherited `ButtonHTMLAttributes` members the component
444
+ // reads as bare template vars (`$id`, `$disabled`, `$className`) are not.
445
+ // The generated template references them, so the stash must declare them or
446
+ // Perl strict mode aborts with `Global symbol "$id" requires explicit
447
+ // package name`. Scan the IR for `props.<name>` accesses and seed any not
448
+ // already declared (and not supplied by the caller below) as `undef`.
449
+ // Mirrors the Go adapter's `augmentInheritedPropAccesses`.
450
+ if (ir.metadata.propsObjectName) {
451
+ const propsObj = ir.metadata.propsObjectName
452
+ const declared = new Set(ir.metadata.propsParams.map(p => p.name))
453
+ for (const name of collectPropsObjectAccesses(ir, propsObj)) {
454
+ if (declared.has(name)) continue
455
+ if (props && name in props) continue // emitted by the user-props loop
456
+ entries.push(`${name} => undef`)
457
+ declared.add(name)
458
+ }
459
+ }
460
+
436
461
  // A `{...props}` rest spread means props that aren't declared named
437
462
  // params flow through the rest bag (`bf->spread_attrs($<restPropsName>)`),
438
463
  // not their own top-level template var. Route them into the bag hashref so
@@ -524,6 +549,38 @@ function buildPerlProps(
524
549
  return `{${entries.join(', ')}}`
525
550
  }
526
551
 
552
+ /**
553
+ * (#checkbox) Collect `<propsObj>.<name>` accesses across a component's memos,
554
+ * signal initializers, init statements, and template attribute expressions —
555
+ * the inherited-attribute reads (`props.className`, `props.id`, `props.disabled`)
556
+ * a SolidJS props-object component makes but that aren't enumerated in
557
+ * `propsParams`. Used to declare matching stash variables.
558
+ */
559
+ function collectPropsObjectAccesses(ir: ComponentIR, propsObj: string): Set<string> {
560
+ const out = new Set<string>()
561
+ const re = new RegExp(`(?:^|[^\\w$.])${propsObj}\\.([A-Za-z_$][\\w$]*)`, 'g')
562
+ const scan = (s: string | undefined): void => {
563
+ if (!s) return
564
+ for (const m of s.matchAll(re)) out.add(m[1])
565
+ }
566
+ for (const memo of ir.metadata.memos) scan(memo.computation)
567
+ for (const sig of ir.metadata.signals) scan(sig.initialValue)
568
+ for (const stmt of ir.metadata.initStatements ?? []) scan(stmt.body)
569
+ const walk = (node: unknown): void => {
570
+ if (!node || typeof node !== 'object') return
571
+ const el = node as { attrs?: Array<{ value?: { kind?: string; expr?: string } }>; children?: unknown[] }
572
+ for (const attr of el.attrs ?? []) {
573
+ if (attr.value?.kind === 'expression') scan(attr.value.expr)
574
+ }
575
+ for (const child of el.children ?? []) {
576
+ const c = child as { element?: unknown }
577
+ walk(c.element ?? child)
578
+ }
579
+ }
580
+ walk(ir.root)
581
+ return out
582
+ }
583
+
527
584
  /**
528
585
  * Evaluate a signal initializer expression using provided props.
529
586
  * Handles patterns like: props.initial ?? 0, props.value, literal values.