@barefootjs/mojolicious 0.8.0 → 0.9.1
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 +21 -0
- package/dist/adapter/index.js +49 -10
- package/dist/adapter/mojo-adapter.d.ts +23 -0
- package/dist/adapter/mojo-adapter.d.ts.map +1 -1
- package/dist/build.js +49 -10
- package/dist/index.js +49 -10
- package/dist/test-render.d.ts.map +1 -1
- package/lib/BarefootJS/Backend/Mojo.pm +1 -1
- package/lib/Mojolicious/Plugin/BarefootJS/DevReload.pm +1 -1
- package/lib/Mojolicious/Plugin/BarefootJS.pm +1 -1
- package/package.json +3 -4
- package/src/__tests__/mojo-adapter.test.ts +128 -66
- package/src/adapter/mojo-adapter.ts +108 -9
- package/src/test-render.ts +58 -1
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.
|
package/dist/adapter/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
|
|
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
|
-
|
|
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
|
|
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":"AAAA;;;;GAIG;AAGH,OAAO,KAAK,EACV,WAAW,EAEX,MAAM,EACN,SAAS,EACT,MAAM,EACN,YAAY,EACZ,aAAa,EACb,MAAM,EACN,WAAW,EACX,UAAU,EACV,MAAM,EACN,aAAa,EACb,UAAU,EACV,OAAO,EAMP,yBAAyB,EAC1B,MAAM,iBAAiB,CAAA;AACxB,OAAO,EACL,WAAW,EACX,KAAK,aAAa,EAClB,KAAK,sBAAsB,EAM3B,KAAK,aAAa,EAClB,KAAK,UAAU,
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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;
|
|
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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@barefootjs/mojolicious",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.1",
|
|
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,7 @@
|
|
|
52
52
|
"directory": "packages/adapter-mojolicious"
|
|
53
53
|
},
|
|
54
54
|
"dependencies": {
|
|
55
|
-
"@barefootjs/
|
|
56
|
-
"@barefootjs/shared": "0.8.0"
|
|
55
|
+
"@barefootjs/shared": "0.9.1"
|
|
57
56
|
},
|
|
58
57
|
"peerDependencies": {
|
|
59
58
|
"@barefootjs/jsx": ">=0.2.0",
|
|
@@ -61,6 +60,6 @@
|
|
|
61
60
|
},
|
|
62
61
|
"devDependencies": {
|
|
63
62
|
"@barefootjs/adapter-tests": "0.1.0",
|
|
64
|
-
"@barefootjs/jsx": "0.
|
|
63
|
+
"@barefootjs/jsx": "0.9.1"
|
|
65
64
|
}
|
|
66
65
|
}
|
|
@@ -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
|
-
|
|
49
|
-
'
|
|
50
|
-
|
|
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
|
|
64
|
-
//
|
|
65
|
-
//
|
|
66
|
-
//
|
|
67
|
-
//
|
|
68
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
package/src/test-render.ts
CHANGED
|
@@ -349,7 +349,12 @@ function buildChildRenderers(
|
|
|
349
349
|
}
|
|
350
350
|
lines.push(` ${slotIdsPerl}`)
|
|
351
351
|
lines.push(` my $child_bf = BarefootJS->new($c, {});`)
|
|
352
|
-
|
|
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.
|