@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/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,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 === "")
|
|
@@ -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"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
package Mojolicious::Plugin::BarefootJS::DevReload;
|
|
2
|
-
our $VERSION = "0.
|
|
2
|
+
our $VERSION = "0.8.0";
|
|
3
3
|
use Mojo::Base 'Mojolicious::Plugin', -signatures;
|
|
4
4
|
|
|
5
5
|
=head1 NAME
|
|
@@ -30,21 +30,12 @@ C<< enabled => 1 >> to force-enable.
|
|
|
30
30
|
use Mojo::ByteStream qw(b);
|
|
31
31
|
use Mojo::IOLoop;
|
|
32
32
|
use File::Spec;
|
|
33
|
+
use BarefootJS::DevReload ();
|
|
33
34
|
|
|
34
|
-
#
|
|
35
|
-
#
|
|
36
|
-
|
|
37
|
-
my $
|
|
38
|
-
my $BUILD_ID_FILE = 'build-id';
|
|
39
|
-
my $SCROLL_STORAGE_KEY = '__bf_devreload_scroll';
|
|
40
|
-
|
|
41
|
-
# Heartbeat < any reasonable proxy/IOLoop idle timeout so a quiet connection
|
|
42
|
-
# doesn't get reaped between rebuilds.
|
|
43
|
-
my $HEARTBEAT_S = 5;
|
|
44
|
-
|
|
45
|
-
# Polling instead of Linux::Inotify2 / Mac::FSEvents keeps the runtime
|
|
46
|
-
# dependency-free. Sub-second latency is imperceptible next to browser reload.
|
|
47
|
-
my $POLL_S = 0.5;
|
|
35
|
+
# Engine-agnostic snippet, build-id reading, and timing constants are shared
|
|
36
|
+
# with the PSGI/Plack path in BarefootJS::DevReload — one source of truth.
|
|
37
|
+
my $HEARTBEAT_S = $BarefootJS::DevReload::HEARTBEAT_S;
|
|
38
|
+
my $POLL_S = $BarefootJS::DevReload::POLL_S;
|
|
48
39
|
|
|
49
40
|
sub register ($self, $app, $config = {}) {
|
|
50
41
|
my $dist_dir = $config->{dist_dir} // 'dist';
|
|
@@ -57,7 +48,7 @@ sub register ($self, $app, $config = {}) {
|
|
|
57
48
|
# on mode — it simply returns an empty ByteStream when disabled.
|
|
58
49
|
$app->helper(bf_dev_snippet => sub ($c) {
|
|
59
50
|
return b('') unless $enabled;
|
|
60
|
-
return b(
|
|
51
|
+
return b(BarefootJS::DevReload->snippet($endpoint));
|
|
61
52
|
});
|
|
62
53
|
|
|
63
54
|
return unless $enabled;
|
|
@@ -68,9 +59,8 @@ sub register ($self, $app, $config = {}) {
|
|
|
68
59
|
my $dist_abs = File::Spec->file_name_is_absolute($dist_dir)
|
|
69
60
|
? $dist_dir
|
|
70
61
|
: $app->home->child($dist_dir)->to_string;
|
|
71
|
-
|
|
72
|
-
my $build_id_path =
|
|
73
|
-
mkdir $dev_dir unless -d $dev_dir;
|
|
62
|
+
BarefootJS::DevReload->ensure_dev_dir($dist_abs);
|
|
63
|
+
my $build_id_path = BarefootJS::DevReload->build_id_path($dist_abs);
|
|
74
64
|
|
|
75
65
|
$app->routes->get($endpoint => sub ($c) {
|
|
76
66
|
my $last_event_id = $c->req->headers->header('Last-Event-ID') // '';
|
|
@@ -83,7 +73,7 @@ sub register ($self, $app, $config = {}) {
|
|
|
83
73
|
|
|
84
74
|
$c->write("retry: 1000\n\n");
|
|
85
75
|
|
|
86
|
-
my $initial_id =
|
|
76
|
+
my $initial_id = BarefootJS::DevReload->read_build_id($build_id_path);
|
|
87
77
|
my $last_sent = '';
|
|
88
78
|
if (length $initial_id) {
|
|
89
79
|
$last_sent = $initial_id;
|
|
@@ -106,7 +96,7 @@ sub register ($self, $app, $config = {}) {
|
|
|
106
96
|
$c->write(": hb\n\n");
|
|
107
97
|
});
|
|
108
98
|
$poll_id = Mojo::IOLoop->recurring($POLL_S => sub {
|
|
109
|
-
my $id =
|
|
99
|
+
my $id = BarefootJS::DevReload->read_build_id($build_id_path);
|
|
110
100
|
return unless length $id;
|
|
111
101
|
return if $id eq $last_sent;
|
|
112
102
|
$last_sent = $id;
|
|
@@ -117,35 +107,4 @@ sub register ($self, $app, $config = {}) {
|
|
|
117
107
|
return;
|
|
118
108
|
}
|
|
119
109
|
|
|
120
|
-
sub _read_build_id ($path) {
|
|
121
|
-
return '' unless -f $path;
|
|
122
|
-
open my $fh, '<', $path or return '';
|
|
123
|
-
local $/;
|
|
124
|
-
my $content = <$fh>;
|
|
125
|
-
close $fh;
|
|
126
|
-
$content //= '';
|
|
127
|
-
$content =~ s/^\s+|\s+$//g;
|
|
128
|
-
return $content;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
sub _snippet ($endpoint) {
|
|
132
|
-
my $ep = _js_str($endpoint);
|
|
133
|
-
my $sk = _js_str($SCROLL_STORAGE_KEY);
|
|
134
|
-
# Small IIFE: EventSource subscriber + scrollY preservation. Idempotent
|
|
135
|
-
# across duplicate mounts (window.__bfDevReload guard).
|
|
136
|
-
return qq{<script>(function(){if(window.__bfDevReload)return;window.__bfDevReload=1;try{var s=sessionStorage.getItem($sk);if(s){sessionStorage.removeItem($sk);var y=parseInt(s,10);if(!isNaN(y)){var restore=function(){window.scrollTo(0,y)};if(document.readyState==='loading'){addEventListener('DOMContentLoaded',restore,{once:true})}else{restore()}}}}catch(e){}var es=new EventSource($ep);es.addEventListener('reload',function(){try{sessionStorage.setItem($sk,String(window.scrollY))}catch(e){}location.reload()});es.addEventListener('error',function(){})})();</script>};
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
sub _js_str ($s) {
|
|
140
|
-
# Minimal JS string escape for the handful of characters that can appear
|
|
141
|
-
# in a URL path or storage key. Good enough for package-internal + trusted
|
|
142
|
-
# operator-supplied strings; never interpolate untrusted input here.
|
|
143
|
-
my $t = $s;
|
|
144
|
-
$t =~ s/\\/\\\\/g;
|
|
145
|
-
$t =~ s/"/\\"/g;
|
|
146
|
-
$t =~ s/\n/\\n/g;
|
|
147
|
-
$t =~ s/\r/\\r/g;
|
|
148
|
-
return qq{"$t"};
|
|
149
|
-
}
|
|
150
|
-
|
|
151
110
|
1;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@barefootjs/mojolicious",
|
|
3
|
-
"version": "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.
|
|
56
|
-
"@barefootjs/shared": "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.
|
|
64
|
+
"@barefootjs/jsx": "0.9.0"
|
|
65
65
|
}
|
|
66
66
|
}
|
|
@@ -17,69 +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` / `textarea` are
|
|
77
|
-
// pass-through native controls.)
|
|
78
|
-
'toggle',
|
|
79
|
-
'switch',
|
|
80
|
-
'checkbox',
|
|
81
|
-
'textarea',
|
|
82
|
-
'kbd',
|
|
83
33
|
],
|
|
84
34
|
// Per-fixture build-time contracts for shapes the Mojo adapter
|
|
85
35
|
// intentionally refuses to lower. Owned by this adapter test file
|
|
@@ -123,6 +73,11 @@ runAdapterConformanceTests({
|
|
|
123
73
|
// surfaces BF101 with a wrap-in-`/* @client */` suggestion, matching
|
|
124
74
|
// the Go adapter's behaviour.
|
|
125
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' }],
|
|
126
81
|
// #1244 stress catalog #12 (#1323): tagged-template-literal call
|
|
127
82
|
// (`cn\`base \${tone()}\``) — same family as #1322 above and refused
|
|
128
83
|
// via the same gate.
|
|
@@ -240,6 +195,190 @@ function compileAndGenerate(source: string, adapter?: MojoAdapter) {
|
|
|
240
195
|
// Mojo-Specific Tests
|
|
241
196
|
// =============================================================================
|
|
242
197
|
|
|
198
|
+
describe('MojoAdapter - conditional inline-object spread (textarea aria-describedby)', () => {
|
|
199
|
+
// `{...(cond ? { 'aria-describedby': cond } : {})}` lowers to a Perl
|
|
200
|
+
// inline ternary of hashrefs so the falsy `{}` branch OMITS the key
|
|
201
|
+
// (bf->spread_attrs does not filter empty strings). The shared
|
|
202
|
+
// fixture only exercises the falsy branch; this pins the truthy one.
|
|
203
|
+
test('emits a Perl inline ternary of hashrefs through bf->spread_attrs', () => {
|
|
204
|
+
const { template } = compileAndGenerate(`
|
|
205
|
+
function Box({ describedBy }: { describedBy?: string }) {
|
|
206
|
+
return <div {...(describedBy ? { 'aria-describedby': describedBy } : {})} />
|
|
207
|
+
}
|
|
208
|
+
`)
|
|
209
|
+
expect(template).toContain(
|
|
210
|
+
"bf->spread_attrs($describedBy ? { 'aria-describedby' => $describedBy } : {})",
|
|
211
|
+
)
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
test('resolves the value reference and preserves the static key for a second prop', () => {
|
|
215
|
+
const { template } = compileAndGenerate(`
|
|
216
|
+
function Box({ label }: { label: string }) {
|
|
217
|
+
return <div {...(label ? { 'data-label': label } : {})} />
|
|
218
|
+
}
|
|
219
|
+
`)
|
|
220
|
+
expect(template).toContain(
|
|
221
|
+
"bf->spread_attrs($label ? { 'data-label' => $label } : {})",
|
|
222
|
+
)
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
test('falls back to BF101 for a computed (non-static) object key', () => {
|
|
226
|
+
const adapter = new MojoAdapter()
|
|
227
|
+
const ir = compileToIR(`
|
|
228
|
+
function Box({ k, v }: { k?: string; v?: string }) {
|
|
229
|
+
return <div {...(v ? { [k]: v } : {})} />
|
|
230
|
+
}
|
|
231
|
+
`, adapter)
|
|
232
|
+
adapter.generate(ir)
|
|
233
|
+
const errs = (adapter as unknown as { errors: { code: string }[] }).errors
|
|
234
|
+
expect(errs.some(e => e.code === 'BF101')).toBe(true)
|
|
235
|
+
})
|
|
236
|
+
})
|
|
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
|
+
|
|
352
|
+
describe('MojoAdapter - nullish optional-attribute omission (textarea rows)', () => {
|
|
353
|
+
// A no-destructure-default, nillable-typed prop is `undef` when the
|
|
354
|
+
// caller omits it; guard its bare-reference attribute with Perl
|
|
355
|
+
// `defined` so it DROPS instead of rendering `attr=""` — matching
|
|
356
|
+
// Hono's nullish-attribute omission. Concrete/defaulted props are
|
|
357
|
+
// never `undef` and stay unconditional.
|
|
358
|
+
test('guards a no-default nillable attr with a Perl defined check', () => {
|
|
359
|
+
const { template } = compileAndGenerate(`
|
|
360
|
+
function C({ rows }: { rows?: number }) {
|
|
361
|
+
return <textarea rows={rows} />
|
|
362
|
+
}
|
|
363
|
+
`)
|
|
364
|
+
expect(template).toContain('<% if (defined $rows) { %>rows="<%= $rows %>"<% } %>')
|
|
365
|
+
// Must NOT emit the bare unconditional form.
|
|
366
|
+
expect(template).not.toMatch(/(?<!\{ %>)rows="<%= \$rows %>"/)
|
|
367
|
+
})
|
|
368
|
+
|
|
369
|
+
test('leaves a defaulted attr unconditional (scope did not widen)', () => {
|
|
370
|
+
const { template } = compileAndGenerate(`
|
|
371
|
+
function C({ value = '' }: { value?: string }) {
|
|
372
|
+
return <textarea value={value} />
|
|
373
|
+
}
|
|
374
|
+
`)
|
|
375
|
+
// `value` has a destructure default → never undef → unconditional,
|
|
376
|
+
// exactly like Hono's value="".
|
|
377
|
+
expect(template).toContain('value="<%= $value %>"')
|
|
378
|
+
expect(template).not.toContain('defined $value')
|
|
379
|
+
})
|
|
380
|
+
})
|
|
381
|
+
|
|
243
382
|
describe('MojoAdapter - Template Generation', () => {
|
|
244
383
|
test('generates basic element with scope marker', () => {
|
|
245
384
|
const result = compileAndGenerate(`
|