@barefootjs/mojolicious 0.7.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 +21 -0
- package/dist/adapter/index.js +107 -8
- package/dist/adapter/mojo-adapter.d.ts +66 -0
- package/dist/adapter/mojo-adapter.d.ts.map +1 -1
- package/dist/build.js +107 -8
- package/dist/index.js +107 -8
- package/dist/test-render.d.ts.map +1 -1
- package/lib/BarefootJS/Backend/Mojo.pm +1 -1
- package/lib/Mojolicious/Plugin/BarefootJS/DevReload.pm +11 -52
- package/lib/Mojolicious/Plugin/BarefootJS.pm +1 -1
- package/package.json +4 -4
- package/src/__tests__/mojo-adapter.test.ts +198 -59
- package/src/adapter/mojo-adapter.ts +256 -7
- 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,7 +118,9 @@ class MojoAdapter extends BaseAdapter {
|
|
|
112
118
|
propsParams = [];
|
|
113
119
|
stringValueNames = new Set;
|
|
114
120
|
moduleStringConsts = new Map;
|
|
121
|
+
localConstants = [];
|
|
115
122
|
loopBoundNames = new Map;
|
|
123
|
+
nullableOptionalProps = new Set;
|
|
116
124
|
constructor(options = {}) {
|
|
117
125
|
super();
|
|
118
126
|
this.options = {
|
|
@@ -123,7 +131,9 @@ class MojoAdapter extends BaseAdapter {
|
|
|
123
131
|
generate(ir, options) {
|
|
124
132
|
this.componentName = ir.metadata.componentName;
|
|
125
133
|
this.propsObjectName = ir.metadata.propsObjectName ?? null;
|
|
134
|
+
augmentInheritedPropAccesses(ir);
|
|
126
135
|
this.propsParams = ir.metadata.propsParams.map((p) => ({ name: p.name }));
|
|
136
|
+
this.nullableOptionalProps = new Set(ir.metadata.propsParams.filter((p) => p.defaultValue === undefined && !p.isRest && p.type?.kind !== "primitive").map((p) => p.name));
|
|
127
137
|
this.stringValueNames = new Set;
|
|
128
138
|
for (const s of ir.metadata.signals) {
|
|
129
139
|
if (isStringTypeInfo(s.type) || isBareStringLiteral(s.initialValue)) {
|
|
@@ -135,6 +145,7 @@ class MojoAdapter extends BaseAdapter {
|
|
|
135
145
|
this.stringValueNames.add(p.name);
|
|
136
146
|
}
|
|
137
147
|
this.moduleStringConsts = this.collectModuleStringConsts(ir.metadata.localConstants);
|
|
148
|
+
this.localConstants = ir.metadata.localConstants ?? [];
|
|
138
149
|
this.loopBoundNames.clear();
|
|
139
150
|
this.errors = [];
|
|
140
151
|
this.childrenCaptureCounter = 0;
|
|
@@ -497,20 +508,20 @@ ${renderedChildren}` : renderedChildren;
|
|
|
497
508
|
`);
|
|
498
509
|
}
|
|
499
510
|
componentPropEmitter = {
|
|
500
|
-
emitLiteral: (value, name) => `${name} => '${value.value}'`,
|
|
511
|
+
emitLiteral: (value, name) => `${perlHashKey(name)} => '${value.value}'`,
|
|
501
512
|
emitExpression: (value, name) => {
|
|
502
513
|
if (value.parts) {
|
|
503
|
-
return `${name} => ${this.convertTemplateLiteralPartsToPerl(value.parts)}`;
|
|
514
|
+
return `${perlHashKey(name)} => ${this.convertTemplateLiteralPartsToPerl(value.parts)}`;
|
|
504
515
|
}
|
|
505
|
-
return `${name} => ${this.convertExpressionToPerl(value.expr)}`;
|
|
516
|
+
return `${perlHashKey(name)} => ${this.convertExpressionToPerl(value.expr)}`;
|
|
506
517
|
},
|
|
507
518
|
emitSpread: (value) => {
|
|
508
519
|
const perlExpr = this.convertExpressionToPerl(value.expr);
|
|
509
520
|
return perlExpr.startsWith("%") ? perlExpr : `%{${perlExpr}}`;
|
|
510
521
|
},
|
|
511
|
-
emitTemplate: (value, name) => `${name} => ${this.convertTemplateLiteralPartsToPerl(value.parts)}`,
|
|
512
|
-
emitBooleanAttr: (_value, name) => `${name} => 1`,
|
|
513
|
-
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`,
|
|
514
525
|
emitJsxChildren: () => ""
|
|
515
526
|
};
|
|
516
527
|
renderComponent(comp) {
|
|
@@ -582,6 +593,13 @@ ${children}`;
|
|
|
582
593
|
if (this.refuseUnsupportedAttrExpression(value.expr, name)) {
|
|
583
594
|
return "";
|
|
584
595
|
}
|
|
596
|
+
const bareId = value.expr.trim();
|
|
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)) {
|
|
599
|
+
const perl2 = this.convertExpressionToPerl(value.expr);
|
|
600
|
+
const body = isBooleanResultExpr(value.expr) || isAriaBooleanAttr(name) ? `${name}="<%= bf->bool_str(${perl2}) %>"` : `${name}="<%= ${perl2} %>"`;
|
|
601
|
+
return `<% if (defined ${perl2}) { %>${body}<% } %>`;
|
|
602
|
+
}
|
|
585
603
|
if (isBooleanAttr(name) || value.presenceOrUndefined) {
|
|
586
604
|
return `<%= ${this.convertExpressionToPerl(value.expr)} ? '${name}' : '' %>`;
|
|
587
605
|
}
|
|
@@ -602,6 +620,22 @@ ${children}`;
|
|
|
602
620
|
const entries = this.propsParams.map((p) => `${JSON.stringify(p.name)} => $${p.name}`);
|
|
603
621
|
return `<%== bf->spread_attrs({${entries.join(", ")}}) %>`;
|
|
604
622
|
}
|
|
623
|
+
const ternaryHashref = this.conditionalSpreadToPerl(trimmed);
|
|
624
|
+
if (ternaryHashref !== null) {
|
|
625
|
+
return `<%== bf->spread_attrs(${ternaryHashref}) %>`;
|
|
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
|
+
}
|
|
605
639
|
const perlExpr = this.convertExpressionToPerl(value.expr);
|
|
606
640
|
return `<%== bf->spread_attrs(${perlExpr}) %>`;
|
|
607
641
|
},
|
|
@@ -766,6 +800,71 @@ ${reason}` : "";
|
|
|
766
800
|
});
|
|
767
801
|
return true;
|
|
768
802
|
}
|
|
803
|
+
conditionalSpreadToPerl(expr) {
|
|
804
|
+
const sf = ts.createSourceFile("__spread.ts", `(${expr})`, ts.ScriptTarget.Latest, true);
|
|
805
|
+
if (sf.statements.length !== 1)
|
|
806
|
+
return null;
|
|
807
|
+
const stmt = sf.statements[0];
|
|
808
|
+
if (!ts.isExpressionStatement(stmt))
|
|
809
|
+
return null;
|
|
810
|
+
let node = stmt.expression;
|
|
811
|
+
while (ts.isParenthesizedExpression(node))
|
|
812
|
+
node = node.expression;
|
|
813
|
+
if (!ts.isConditionalExpression(node))
|
|
814
|
+
return null;
|
|
815
|
+
const unwrap = (e) => {
|
|
816
|
+
let n = e;
|
|
817
|
+
while (ts.isParenthesizedExpression(n))
|
|
818
|
+
n = n.expression;
|
|
819
|
+
return n;
|
|
820
|
+
};
|
|
821
|
+
const whenTrue = unwrap(node.whenTrue);
|
|
822
|
+
const whenFalse = unwrap(node.whenFalse);
|
|
823
|
+
if (!ts.isObjectLiteralExpression(whenTrue) || !ts.isObjectLiteralExpression(whenFalse)) {
|
|
824
|
+
return null;
|
|
825
|
+
}
|
|
826
|
+
const condPerl = this.convertExpressionToPerl(node.condition.getText(sf));
|
|
827
|
+
const truePerl = this.objectLiteralToPerlHashref(whenTrue, sf);
|
|
828
|
+
const falsePerl = this.objectLiteralToPerlHashref(whenFalse, sf);
|
|
829
|
+
if (truePerl === null || falsePerl === null)
|
|
830
|
+
return null;
|
|
831
|
+
return `${condPerl} ? ${truePerl} : ${falsePerl}`;
|
|
832
|
+
}
|
|
833
|
+
objectLiteralToPerlHashref(obj, sf) {
|
|
834
|
+
const entries = [];
|
|
835
|
+
for (const prop of obj.properties) {
|
|
836
|
+
if (!ts.isPropertyAssignment(prop))
|
|
837
|
+
return null;
|
|
838
|
+
let key;
|
|
839
|
+
if (ts.isIdentifier(prop.name)) {
|
|
840
|
+
key = prop.name.text;
|
|
841
|
+
} else if (ts.isStringLiteral(prop.name) || ts.isNoSubstitutionTemplateLiteral(prop.name)) {
|
|
842
|
+
key = prop.name.text;
|
|
843
|
+
} else {
|
|
844
|
+
return null;
|
|
845
|
+
}
|
|
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));
|
|
854
|
+
entries.push(`'${key.replace(/'/g, "\\'")}' => ${valPerl}`);
|
|
855
|
+
}
|
|
856
|
+
return entries.length === 0 ? "{}" : `{ ${entries.join(", ")} }`;
|
|
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
|
+
}
|
|
769
868
|
convertExpressionToPerl(expr) {
|
|
770
869
|
const trimmed = expr.trim();
|
|
771
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
|
|
@@ -78,6 +86,23 @@ export declare class MojoAdapter extends BaseAdapter implements IRNodeEmitter<Mo
|
|
|
78
86
|
* loop-var shadowing guards). (#1749 review)
|
|
79
87
|
*/
|
|
80
88
|
private loopBoundNames;
|
|
89
|
+
/**
|
|
90
|
+
* Prop names whose value is `undef` in the template body when the caller
|
|
91
|
+
* omits them — so a bare-reference attribute should be dropped rather
|
|
92
|
+
* than rendered as `attr=""`. The actual population criterion (see
|
|
93
|
+
* `generate()`) is: NO destructure default (`defaultValue === undefined`)
|
|
94
|
+
* AND non-rest (`!isRest`) AND non-primitive type (`type.kind !==
|
|
95
|
+
* 'primitive'`). It deliberately does NOT consult `p.optional`: the
|
|
96
|
+
* analyzer derives `optional` from the presence of a default initializer,
|
|
97
|
+
* not the `?` token, so it's not the right witness here. Excluding
|
|
98
|
+
* concrete primitives (`string`/`number`/`boolean`) mirrors the Go
|
|
99
|
+
* adapter's scope, which guards only `interface{}` (nillable) fields.
|
|
100
|
+
* Used by `elementAttrEmitter.emitExpression` to guard such an attribute
|
|
101
|
+
* with a Perl `defined $x` check (`<textarea>` omits `rows`), matching
|
|
102
|
+
* Hono's nullish-attribute omission. Concrete/defaulted props are
|
|
103
|
+
* excluded and always emit unconditionally.
|
|
104
|
+
*/
|
|
105
|
+
private nullableOptionalProps;
|
|
81
106
|
constructor(options?: MojoAdapterOptions);
|
|
82
107
|
generate(ir: ComponentIR, options?: AdapterGenerateOptions): AdapterOutput;
|
|
83
108
|
/**
|
|
@@ -202,6 +227,47 @@ export declare class MojoAdapter extends BaseAdapter implements IRNodeEmitter<Mo
|
|
|
202
227
|
* should drop the attribute / skip the emit).
|
|
203
228
|
*/
|
|
204
229
|
private refuseUnsupportedAttrExpression;
|
|
230
|
+
/**
|
|
231
|
+
* Lower a conditional inline-object spread expression
|
|
232
|
+
* `(COND ? { 'aria-describedby': describedBy } : {})`
|
|
233
|
+
* (either branch possibly `{}`) into a Perl inline ternary of
|
|
234
|
+
* hashrefs for `bf->spread_attrs`:
|
|
235
|
+
* `$describedBy ? { 'aria-describedby' => $describedBy } : {}`
|
|
236
|
+
*
|
|
237
|
+
* The condition is translated via `convertExpressionToPerl` (a bare
|
|
238
|
+
* prop ident becomes `$describedBy`; Perl truthiness handles the
|
|
239
|
+
* test). Object literals become Perl hashrefs with `=>`; string-
|
|
240
|
+
* literal keys are quoted, values resolve via `convertExpressionToPerl`.
|
|
241
|
+
*
|
|
242
|
+
* Returns null when the expression is NOT this shape, or when a part
|
|
243
|
+
* can't be faithfully lowered (non-static key, etc.) so the caller
|
|
244
|
+
* falls back to the standard `convertExpressionToPerl` path (which
|
|
245
|
+
* records BF101). Scoped strictly to ternary-of-object-literals so no
|
|
246
|
+
* other spread shape regresses.
|
|
247
|
+
*/
|
|
248
|
+
private conditionalSpreadToPerl;
|
|
249
|
+
/**
|
|
250
|
+
* Convert a static object literal into a Perl hashref string for a
|
|
251
|
+
* conditional spread. Only static string/identifier keys are allowed;
|
|
252
|
+
* values resolve via `convertExpressionToPerl`. Returns null for any
|
|
253
|
+
* computed/spread/dynamic key. Empty object → `{}`.
|
|
254
|
+
*/
|
|
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;
|
|
205
271
|
private convertExpressionToPerl;
|
|
206
272
|
/**
|
|
207
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,7 +118,9 @@ class MojoAdapter extends BaseAdapter {
|
|
|
112
118
|
propsParams = [];
|
|
113
119
|
stringValueNames = new Set;
|
|
114
120
|
moduleStringConsts = new Map;
|
|
121
|
+
localConstants = [];
|
|
115
122
|
loopBoundNames = new Map;
|
|
123
|
+
nullableOptionalProps = new Set;
|
|
116
124
|
constructor(options = {}) {
|
|
117
125
|
super();
|
|
118
126
|
this.options = {
|
|
@@ -123,7 +131,9 @@ class MojoAdapter extends BaseAdapter {
|
|
|
123
131
|
generate(ir, options) {
|
|
124
132
|
this.componentName = ir.metadata.componentName;
|
|
125
133
|
this.propsObjectName = ir.metadata.propsObjectName ?? null;
|
|
134
|
+
augmentInheritedPropAccesses(ir);
|
|
126
135
|
this.propsParams = ir.metadata.propsParams.map((p) => ({ name: p.name }));
|
|
136
|
+
this.nullableOptionalProps = new Set(ir.metadata.propsParams.filter((p) => p.defaultValue === undefined && !p.isRest && p.type?.kind !== "primitive").map((p) => p.name));
|
|
127
137
|
this.stringValueNames = new Set;
|
|
128
138
|
for (const s of ir.metadata.signals) {
|
|
129
139
|
if (isStringTypeInfo(s.type) || isBareStringLiteral(s.initialValue)) {
|
|
@@ -135,6 +145,7 @@ class MojoAdapter extends BaseAdapter {
|
|
|
135
145
|
this.stringValueNames.add(p.name);
|
|
136
146
|
}
|
|
137
147
|
this.moduleStringConsts = this.collectModuleStringConsts(ir.metadata.localConstants);
|
|
148
|
+
this.localConstants = ir.metadata.localConstants ?? [];
|
|
138
149
|
this.loopBoundNames.clear();
|
|
139
150
|
this.errors = [];
|
|
140
151
|
this.childrenCaptureCounter = 0;
|
|
@@ -497,20 +508,20 @@ ${renderedChildren}` : renderedChildren;
|
|
|
497
508
|
`);
|
|
498
509
|
}
|
|
499
510
|
componentPropEmitter = {
|
|
500
|
-
emitLiteral: (value, name) => `${name} => '${value.value}'`,
|
|
511
|
+
emitLiteral: (value, name) => `${perlHashKey(name)} => '${value.value}'`,
|
|
501
512
|
emitExpression: (value, name) => {
|
|
502
513
|
if (value.parts) {
|
|
503
|
-
return `${name} => ${this.convertTemplateLiteralPartsToPerl(value.parts)}`;
|
|
514
|
+
return `${perlHashKey(name)} => ${this.convertTemplateLiteralPartsToPerl(value.parts)}`;
|
|
504
515
|
}
|
|
505
|
-
return `${name} => ${this.convertExpressionToPerl(value.expr)}`;
|
|
516
|
+
return `${perlHashKey(name)} => ${this.convertExpressionToPerl(value.expr)}`;
|
|
506
517
|
},
|
|
507
518
|
emitSpread: (value) => {
|
|
508
519
|
const perlExpr = this.convertExpressionToPerl(value.expr);
|
|
509
520
|
return perlExpr.startsWith("%") ? perlExpr : `%{${perlExpr}}`;
|
|
510
521
|
},
|
|
511
|
-
emitTemplate: (value, name) => `${name} => ${this.convertTemplateLiteralPartsToPerl(value.parts)}`,
|
|
512
|
-
emitBooleanAttr: (_value, name) => `${name} => 1`,
|
|
513
|
-
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`,
|
|
514
525
|
emitJsxChildren: () => ""
|
|
515
526
|
};
|
|
516
527
|
renderComponent(comp) {
|
|
@@ -582,6 +593,13 @@ ${children}`;
|
|
|
582
593
|
if (this.refuseUnsupportedAttrExpression(value.expr, name)) {
|
|
583
594
|
return "";
|
|
584
595
|
}
|
|
596
|
+
const bareId = value.expr.trim();
|
|
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)) {
|
|
599
|
+
const perl2 = this.convertExpressionToPerl(value.expr);
|
|
600
|
+
const body = isBooleanResultExpr(value.expr) || isAriaBooleanAttr(name) ? `${name}="<%= bf->bool_str(${perl2}) %>"` : `${name}="<%= ${perl2} %>"`;
|
|
601
|
+
return `<% if (defined ${perl2}) { %>${body}<% } %>`;
|
|
602
|
+
}
|
|
585
603
|
if (isBooleanAttr(name) || value.presenceOrUndefined) {
|
|
586
604
|
return `<%= ${this.convertExpressionToPerl(value.expr)} ? '${name}' : '' %>`;
|
|
587
605
|
}
|
|
@@ -602,6 +620,22 @@ ${children}`;
|
|
|
602
620
|
const entries = this.propsParams.map((p) => `${JSON.stringify(p.name)} => $${p.name}`);
|
|
603
621
|
return `<%== bf->spread_attrs({${entries.join(", ")}}) %>`;
|
|
604
622
|
}
|
|
623
|
+
const ternaryHashref = this.conditionalSpreadToPerl(trimmed);
|
|
624
|
+
if (ternaryHashref !== null) {
|
|
625
|
+
return `<%== bf->spread_attrs(${ternaryHashref}) %>`;
|
|
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
|
+
}
|
|
605
639
|
const perlExpr = this.convertExpressionToPerl(value.expr);
|
|
606
640
|
return `<%== bf->spread_attrs(${perlExpr}) %>`;
|
|
607
641
|
},
|
|
@@ -766,6 +800,71 @@ ${reason}` : "";
|
|
|
766
800
|
});
|
|
767
801
|
return true;
|
|
768
802
|
}
|
|
803
|
+
conditionalSpreadToPerl(expr) {
|
|
804
|
+
const sf = ts.createSourceFile("__spread.ts", `(${expr})`, ts.ScriptTarget.Latest, true);
|
|
805
|
+
if (sf.statements.length !== 1)
|
|
806
|
+
return null;
|
|
807
|
+
const stmt = sf.statements[0];
|
|
808
|
+
if (!ts.isExpressionStatement(stmt))
|
|
809
|
+
return null;
|
|
810
|
+
let node = stmt.expression;
|
|
811
|
+
while (ts.isParenthesizedExpression(node))
|
|
812
|
+
node = node.expression;
|
|
813
|
+
if (!ts.isConditionalExpression(node))
|
|
814
|
+
return null;
|
|
815
|
+
const unwrap = (e) => {
|
|
816
|
+
let n = e;
|
|
817
|
+
while (ts.isParenthesizedExpression(n))
|
|
818
|
+
n = n.expression;
|
|
819
|
+
return n;
|
|
820
|
+
};
|
|
821
|
+
const whenTrue = unwrap(node.whenTrue);
|
|
822
|
+
const whenFalse = unwrap(node.whenFalse);
|
|
823
|
+
if (!ts.isObjectLiteralExpression(whenTrue) || !ts.isObjectLiteralExpression(whenFalse)) {
|
|
824
|
+
return null;
|
|
825
|
+
}
|
|
826
|
+
const condPerl = this.convertExpressionToPerl(node.condition.getText(sf));
|
|
827
|
+
const truePerl = this.objectLiteralToPerlHashref(whenTrue, sf);
|
|
828
|
+
const falsePerl = this.objectLiteralToPerlHashref(whenFalse, sf);
|
|
829
|
+
if (truePerl === null || falsePerl === null)
|
|
830
|
+
return null;
|
|
831
|
+
return `${condPerl} ? ${truePerl} : ${falsePerl}`;
|
|
832
|
+
}
|
|
833
|
+
objectLiteralToPerlHashref(obj, sf) {
|
|
834
|
+
const entries = [];
|
|
835
|
+
for (const prop of obj.properties) {
|
|
836
|
+
if (!ts.isPropertyAssignment(prop))
|
|
837
|
+
return null;
|
|
838
|
+
let key;
|
|
839
|
+
if (ts.isIdentifier(prop.name)) {
|
|
840
|
+
key = prop.name.text;
|
|
841
|
+
} else if (ts.isStringLiteral(prop.name) || ts.isNoSubstitutionTemplateLiteral(prop.name)) {
|
|
842
|
+
key = prop.name.text;
|
|
843
|
+
} else {
|
|
844
|
+
return null;
|
|
845
|
+
}
|
|
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));
|
|
854
|
+
entries.push(`'${key.replace(/'/g, "\\'")}' => ${valPerl}`);
|
|
855
|
+
}
|
|
856
|
+
return entries.length === 0 ? "{}" : `{ ${entries.join(", ")} }`;
|
|
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
|
+
}
|
|
769
868
|
convertExpressionToPerl(expr) {
|
|
770
869
|
const trimmed = expr.trim();
|
|
771
870
|
if (trimmed === "")
|