@barefootjs/mojolicious 0.4.0 → 0.5.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/dist/adapter/index.js +9 -3
- package/dist/adapter/mojo-adapter.d.ts +1 -0
- package/dist/adapter/mojo-adapter.d.ts.map +1 -1
- package/dist/build.js +9 -3
- package/dist/index.js +9 -3
- package/lib/BarefootJS.pm +56 -29
- package/package.json +3 -3
- package/src/__tests__/mojo-adapter.test.ts +188 -21
- package/src/adapter/mojo-adapter.ts +31 -10
- package/src/test-render.ts +17 -1
package/dist/adapter/index.js
CHANGED
|
@@ -87,6 +87,7 @@ class MojoAdapter extends BaseAdapter {
|
|
|
87
87
|
name = "mojolicious";
|
|
88
88
|
extension = ".html.ep";
|
|
89
89
|
templatesPerComponent = true;
|
|
90
|
+
importMapInjection = "html-snippet";
|
|
90
91
|
templatePrimitives = MOJO_PRIMITIVE_EMIT_MAP;
|
|
91
92
|
componentName = "";
|
|
92
93
|
options;
|
|
@@ -399,8 +400,10 @@ ${whenTrue}
|
|
|
399
400
|
const indexVar = loop.iterationShape === "keys" ? `$${param}` : loop.index ? `$${loop.index}` : "$_i";
|
|
400
401
|
const prevInLoop = this.inLoop;
|
|
401
402
|
this.inLoop = true;
|
|
402
|
-
const
|
|
403
|
+
const renderedChildren = this.renderChildren(loop.children);
|
|
403
404
|
this.inLoop = prevInLoop;
|
|
405
|
+
const children = loop.bodyIsItemConditional && loop.key ? `<%== bf->comment("loop-i:" . ${this.convertExpressionToPerl(loop.key)}) %>
|
|
406
|
+
${renderedChildren}` : renderedChildren;
|
|
404
407
|
const lines = [];
|
|
405
408
|
lines.push(`<%== bf->comment("loop:${loop.markerId}") %>`);
|
|
406
409
|
if (sortedHoist && loop.sortComparator) {
|
|
@@ -919,8 +922,11 @@ function perlIdentifierFromMarkerId(markerId) {
|
|
|
919
922
|
return markerId.replace(/[^a-zA-Z0-9]/g, (ch) => ch === "_" ? "__" : `_x${ch.charCodeAt(0).toString(16)}`);
|
|
920
923
|
}
|
|
921
924
|
function renderSortMethod(recv, c) {
|
|
922
|
-
const
|
|
923
|
-
|
|
925
|
+
const keyHashes = c.keys.map((k) => {
|
|
926
|
+
const keyEntry = k.key.kind === "self" ? `key_kind => 'self'` : `key_kind => 'field', key => '${k.key.field}'`;
|
|
927
|
+
return `{ ${keyEntry}, compare_type => '${k.type}', direction => '${k.direction}' }`;
|
|
928
|
+
});
|
|
929
|
+
return `bf->sort(${recv}, { keys => [${keyHashes.join(", ")}] })`;
|
|
924
930
|
}
|
|
925
931
|
|
|
926
932
|
class MojoFilterEmitter {
|
|
@@ -24,6 +24,7 @@ export declare class MojoAdapter extends BaseAdapter implements IRNodeEmitter<Mo
|
|
|
24
24
|
name: string;
|
|
25
25
|
extension: string;
|
|
26
26
|
templatesPerComponent: boolean;
|
|
27
|
+
importMapInjection: 'html-snippet';
|
|
27
28
|
/**
|
|
28
29
|
* Identifier-path callees the Mojo runtime can render in template
|
|
29
30
|
* scope. The relocate pass consults this map to mark matching
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mojo-adapter.d.ts","sourceRoot":"","sources":["../../src/adapter/mojo-adapter.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EACV,WAAW,EACX,MAAM,EACN,SAAS,EACT,MAAM,EACN,YAAY,EACZ,aAAa,EACb,MAAM,EACN,WAAW,EACX,UAAU,EACV,MAAM,EACN,aAAa,EACb,UAAU,EACV,OAAO,EAKP,yBAAyB,EAC1B,MAAM,iBAAiB,CAAA;AACxB,OAAO,EACL,WAAW,EACX,KAAK,aAAa,EAClB,KAAK,sBAAsB,EAM3B,KAAK,aAAa,EAClB,KAAK,UAAU,EAYhB,MAAM,iBAAiB,CAAA;AAGxB;;;;;;GAMG;AACH,KAAK,aAAa,GAAG,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,CAAA;AAC1C,OAAO,KAAK,EAAE,UAAU,EAAiD,MAAM,iBAAiB,CAAA;AAkEhG,MAAM,WAAW,kBAAkB;IACjC,qEAAqE;IACrE,gBAAgB,CAAC,EAAE,MAAM,CAAA;IAEzB,8EAA8E;IAC9E,cAAc,CAAC,EAAE,MAAM,CAAA;CACxB;AAED,qBAAa,WAAY,SAAQ,WAAY,YAAW,aAAa,CAAC,aAAa,CAAC;IAClF,IAAI,SAAgB;IACpB,SAAS,SAAa;IACtB,qBAAqB,UAAO;
|
|
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
|
@@ -87,6 +87,7 @@ class MojoAdapter extends BaseAdapter {
|
|
|
87
87
|
name = "mojolicious";
|
|
88
88
|
extension = ".html.ep";
|
|
89
89
|
templatesPerComponent = true;
|
|
90
|
+
importMapInjection = "html-snippet";
|
|
90
91
|
templatePrimitives = MOJO_PRIMITIVE_EMIT_MAP;
|
|
91
92
|
componentName = "";
|
|
92
93
|
options;
|
|
@@ -399,8 +400,10 @@ ${whenTrue}
|
|
|
399
400
|
const indexVar = loop.iterationShape === "keys" ? `$${param}` : loop.index ? `$${loop.index}` : "$_i";
|
|
400
401
|
const prevInLoop = this.inLoop;
|
|
401
402
|
this.inLoop = true;
|
|
402
|
-
const
|
|
403
|
+
const renderedChildren = this.renderChildren(loop.children);
|
|
403
404
|
this.inLoop = prevInLoop;
|
|
405
|
+
const children = loop.bodyIsItemConditional && loop.key ? `<%== bf->comment("loop-i:" . ${this.convertExpressionToPerl(loop.key)}) %>
|
|
406
|
+
${renderedChildren}` : renderedChildren;
|
|
404
407
|
const lines = [];
|
|
405
408
|
lines.push(`<%== bf->comment("loop:${loop.markerId}") %>`);
|
|
406
409
|
if (sortedHoist && loop.sortComparator) {
|
|
@@ -919,8 +922,11 @@ function perlIdentifierFromMarkerId(markerId) {
|
|
|
919
922
|
return markerId.replace(/[^a-zA-Z0-9]/g, (ch) => ch === "_" ? "__" : `_x${ch.charCodeAt(0).toString(16)}`);
|
|
920
923
|
}
|
|
921
924
|
function renderSortMethod(recv, c) {
|
|
922
|
-
const
|
|
923
|
-
|
|
925
|
+
const keyHashes = c.keys.map((k) => {
|
|
926
|
+
const keyEntry = k.key.kind === "self" ? `key_kind => 'self'` : `key_kind => 'field', key => '${k.key.field}'`;
|
|
927
|
+
return `{ ${keyEntry}, compare_type => '${k.type}', direction => '${k.direction}' }`;
|
|
928
|
+
});
|
|
929
|
+
return `bf->sort(${recv}, { keys => [${keyHashes.join(", ")}] })`;
|
|
924
930
|
}
|
|
925
931
|
|
|
926
932
|
class MojoFilterEmitter {
|
package/dist/index.js
CHANGED
|
@@ -87,6 +87,7 @@ class MojoAdapter extends BaseAdapter {
|
|
|
87
87
|
name = "mojolicious";
|
|
88
88
|
extension = ".html.ep";
|
|
89
89
|
templatesPerComponent = true;
|
|
90
|
+
importMapInjection = "html-snippet";
|
|
90
91
|
templatePrimitives = MOJO_PRIMITIVE_EMIT_MAP;
|
|
91
92
|
componentName = "";
|
|
92
93
|
options;
|
|
@@ -399,8 +400,10 @@ ${whenTrue}
|
|
|
399
400
|
const indexVar = loop.iterationShape === "keys" ? `$${param}` : loop.index ? `$${loop.index}` : "$_i";
|
|
400
401
|
const prevInLoop = this.inLoop;
|
|
401
402
|
this.inLoop = true;
|
|
402
|
-
const
|
|
403
|
+
const renderedChildren = this.renderChildren(loop.children);
|
|
403
404
|
this.inLoop = prevInLoop;
|
|
405
|
+
const children = loop.bodyIsItemConditional && loop.key ? `<%== bf->comment("loop-i:" . ${this.convertExpressionToPerl(loop.key)}) %>
|
|
406
|
+
${renderedChildren}` : renderedChildren;
|
|
404
407
|
const lines = [];
|
|
405
408
|
lines.push(`<%== bf->comment("loop:${loop.markerId}") %>`);
|
|
406
409
|
if (sortedHoist && loop.sortComparator) {
|
|
@@ -919,8 +922,11 @@ function perlIdentifierFromMarkerId(markerId) {
|
|
|
919
922
|
return markerId.replace(/[^a-zA-Z0-9]/g, (ch) => ch === "_" ? "__" : `_x${ch.charCodeAt(0).toString(16)}`);
|
|
920
923
|
}
|
|
921
924
|
function renderSortMethod(recv, c) {
|
|
922
|
-
const
|
|
923
|
-
|
|
925
|
+
const keyHashes = c.keys.map((k) => {
|
|
926
|
+
const keyEntry = k.key.kind === "self" ? `key_kind => 'self'` : `key_kind => 'field', key => '${k.key.field}'`;
|
|
927
|
+
return `{ ${keyEntry}, compare_type => '${k.type}', direction => '${k.direction}' }`;
|
|
928
|
+
});
|
|
929
|
+
return `bf->sort(${recv}, { keys => [${keyHashes.join(", ")}] })`;
|
|
924
930
|
}
|
|
925
931
|
|
|
926
932
|
class MojoFilterEmitter {
|
package/lib/BarefootJS.pm
CHANGED
|
@@ -530,7 +530,8 @@ sub trim ($self, $recv) {
|
|
|
530
530
|
# lowering (#1448 Tier B). Non-mutating — JS's mutate-vs-new
|
|
531
531
|
# distinction is moot in SSR template context.
|
|
532
532
|
#
|
|
533
|
-
# Opts hash-ref
|
|
533
|
+
# Opts hash-ref. The compiler emits a `keys` list of per-key hashes
|
|
534
|
+
# in priority order; each hash carries:
|
|
534
535
|
#
|
|
535
536
|
# key_kind => 'self' | 'field'
|
|
536
537
|
# key => '' when key_kind eq 'self'; field name verbatim
|
|
@@ -539,7 +540,7 @@ sub trim ($self, $recv) {
|
|
|
539
540
|
# applied. Perl hash lookups are case-sensitive so
|
|
540
541
|
# the key here must match the actual hash key the
|
|
541
542
|
# user populated.
|
|
542
|
-
# compare_type => 'numeric' | 'string'
|
|
543
|
+
# compare_type => 'numeric' | 'string' | 'auto'
|
|
543
544
|
# direction => 'asc' | 'desc'
|
|
544
545
|
#
|
|
545
546
|
# Accepted comparator catalogue (gated upstream at parse time —
|
|
@@ -548,47 +549,73 @@ sub trim ($self, $recv) {
|
|
|
548
549
|
# (a,b) => a.f - b.f → field, numeric
|
|
549
550
|
# (a,b) => a - b → self, numeric
|
|
550
551
|
# (a,b) => a[.f].localeCompare(b[.f]) → field|self, string
|
|
552
|
+
# (a,b) => a.f > b.f ? 1 : -1 → field|self, auto
|
|
553
|
+
# any of the above ||-chained → multi-key tie-breaks
|
|
551
554
|
# (and reversed-operand variants for `desc`).
|
|
552
555
|
#
|
|
553
|
-
#
|
|
554
|
-
#
|
|
556
|
+
# `auto` (relational-ternary lowering) compares numerically when both
|
|
557
|
+
# keys `looks_like_number`, else lexically — Go's `bf_sort` applies the
|
|
558
|
+
# same rule so the two template adapters stay byte-equal.
|
|
559
|
+
#
|
|
560
|
+
# A future `nulls => 'first' | 'last'` knob can land per key without
|
|
561
|
+
# churn — the opts hash is the right place to grow.
|
|
555
562
|
|
|
556
563
|
sub sort ($self, $recv, $opts = {}) {
|
|
557
564
|
return [] unless ref($recv) eq 'ARRAY';
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
my
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
565
|
+
|
|
566
|
+
# Normalise the per-key specs (priority order, length >= 1).
|
|
567
|
+
my @spec = map {
|
|
568
|
+
{
|
|
569
|
+
key_kind => $_->{key_kind} // 'self',
|
|
570
|
+
key => $_->{key} // '',
|
|
571
|
+
compare_type => $_->{compare_type} // 'numeric',
|
|
572
|
+
direction => $_->{direction} // 'asc',
|
|
573
|
+
}
|
|
574
|
+
} @{ $opts->{keys} // [] };
|
|
575
|
+
return [ @$recv ] unless @spec;
|
|
576
|
+
|
|
577
|
+
# Schwartzian transform: project each item to all its sort keys
|
|
578
|
+
# once, then compare projected keys. Cheaper than re-resolving the
|
|
579
|
+
# field accessors inside every comparison for non-trivial arrays.
|
|
566
580
|
my @keyed = map {
|
|
567
581
|
my $item = $_;
|
|
568
|
-
my
|
|
569
|
-
|
|
582
|
+
my @ks = map {
|
|
583
|
+
$_->{key_kind} eq 'field' && ref($item) eq 'HASH' ? $item->{ $_->{key} } : $item;
|
|
584
|
+
} @spec;
|
|
585
|
+
[ \@ks, $item ];
|
|
570
586
|
} @$recv;
|
|
571
587
|
|
|
572
|
-
my $cmp
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
# ordering); matching Go's `toFloat64(nil) == 0` keeps the
|
|
582
|
-
# adapter outputs symmetric.
|
|
583
|
-
$cmp = $direction eq 'desc'
|
|
584
|
-
? sub { ($b->[0] // 0) <=> ($a->[0] // 0) }
|
|
585
|
-
: sub { ($a->[0] // 0) <=> ($b->[0] // 0) };
|
|
586
|
-
}
|
|
588
|
+
my $cmp = sub {
|
|
589
|
+
for my $i (0 .. $#spec) {
|
|
590
|
+
my $sp = $spec[$i];
|
|
591
|
+
my $c = _compare_sort_key($a->[0][$i], $b->[0][$i], $sp->{compare_type});
|
|
592
|
+
next if $c == 0; # tie on this key — try the next
|
|
593
|
+
return $sp->{direction} eq 'desc' ? -$c : $c;
|
|
594
|
+
}
|
|
595
|
+
return 0;
|
|
596
|
+
};
|
|
587
597
|
|
|
588
598
|
my @sorted = sort $cmp @keyed;
|
|
589
599
|
return [ map { $_->[1] } @sorted ];
|
|
590
600
|
}
|
|
591
601
|
|
|
602
|
+
# Compare two projected keys, ascending orientation (-1 / 0 / 1); the
|
|
603
|
+
# caller negates for 'desc'. 'auto' compares numerically when both
|
|
604
|
+
# keys look like numbers, else lexically (matches Go's `bf_sort`).
|
|
605
|
+
# undef coalesces to '' / 0 so the order stays total without warnings.
|
|
606
|
+
sub _compare_sort_key ($av, $bv, $compare_type) {
|
|
607
|
+
if ($compare_type eq 'string') {
|
|
608
|
+
return ($av // '') cmp ($bv // '');
|
|
609
|
+
}
|
|
610
|
+
if ($compare_type eq 'auto') {
|
|
611
|
+
if (looks_like_number($av // '') && looks_like_number($bv // '')) {
|
|
612
|
+
return ($av // 0) <=> ($bv // 0);
|
|
613
|
+
}
|
|
614
|
+
return ($av // '') cmp ($bv // '');
|
|
615
|
+
}
|
|
616
|
+
return ($av // 0) <=> ($bv // 0); # numeric
|
|
617
|
+
}
|
|
618
|
+
|
|
592
619
|
# ---------------------------------------------------------------------------
|
|
593
620
|
# JSX intrinsic-element spread (#1407)
|
|
594
621
|
# ---------------------------------------------------------------------------
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@barefootjs/mojolicious",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.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,14 +52,14 @@
|
|
|
52
52
|
"directory": "packages/adapter-mojolicious"
|
|
53
53
|
},
|
|
54
54
|
"dependencies": {
|
|
55
|
-
"@barefootjs/shared": "0.
|
|
55
|
+
"@barefootjs/shared": "0.5.1"
|
|
56
56
|
},
|
|
57
57
|
"peerDependencies": {
|
|
58
58
|
"@barefootjs/jsx": ">=0.2.0"
|
|
59
59
|
},
|
|
60
60
|
"devDependencies": {
|
|
61
61
|
"@barefootjs/adapter-tests": "0.1.0",
|
|
62
|
-
"@barefootjs/jsx": "0.
|
|
62
|
+
"@barefootjs/jsx": "0.5.1",
|
|
63
63
|
"typescript": "^5.0.0"
|
|
64
64
|
}
|
|
65
65
|
}
|
|
@@ -69,21 +69,19 @@ runAdapterConformanceTests({
|
|
|
69
69
|
'toggle-shared',
|
|
70
70
|
'reactive-props',
|
|
71
71
|
'props-reactivity-comparison',
|
|
72
|
-
// #
|
|
73
|
-
//
|
|
74
|
-
//
|
|
75
|
-
//
|
|
76
|
-
// `
|
|
77
|
-
//
|
|
78
|
-
//
|
|
79
|
-
//
|
|
80
|
-
//
|
|
81
|
-
//
|
|
82
|
-
//
|
|
83
|
-
//
|
|
84
|
-
|
|
85
|
-
// `jsx-spread-props-object` off the CSR conformance path.
|
|
86
|
-
'native-select-spread-children',
|
|
72
|
+
// #1665 whole-item loop conditional. The Mojo adapter correctly emits the
|
|
73
|
+
// per-item `<!--bf-loop-i:KEY-->` anchor, `data-key`, and the conditional
|
|
74
|
+
// markers (verified by template-structure tests), but the fixture's
|
|
75
|
+
// `sel() === t.id` string comparison lowers to Perl numeric `==`
|
|
76
|
+
// (`"b" == "a"` → `0 == 0` → true), so the perl-executed render renders
|
|
77
|
+
// every item's true branch instead of only the matching one. Selecting
|
|
78
|
+
// `eq` vs `==` from operand types is a separate pre-existing Mojo
|
|
79
|
+
// limitation (same family as the skipped `logical-or-jsx` /
|
|
80
|
+
// `nullish-coalescing-jsx` map shapes); the anchored SSR shape itself is
|
|
81
|
+
// covered cross-adapter by Hono + CSR conformance and the runtime
|
|
82
|
+
// hydration tests. Remove this skip once the Mojo limitation is fixed:
|
|
83
|
+
// https://github.com/piconic-ai/barefootjs/issues/1672
|
|
84
|
+
'loop-item-conditional',
|
|
87
85
|
],
|
|
88
86
|
// Per-fixture build-time contracts for shapes the Mojo adapter
|
|
89
87
|
// intentionally refuses to lower. Owned by this adapter test file
|
|
@@ -909,6 +907,8 @@ import { fixture as arraySortFieldAscFixture } from '../../../adapter-tests/fixt
|
|
|
909
907
|
import { fixture as arraySortFieldDescFixture } from '../../../adapter-tests/fixtures/methods/array-sort-field-desc'
|
|
910
908
|
import { fixture as arraySortPrimitiveFixture } from '../../../adapter-tests/fixtures/methods/array-sort-primitive'
|
|
911
909
|
import { fixture as arraySortLocaleFixture } from '../../../adapter-tests/fixtures/methods/array-sort-locale'
|
|
910
|
+
import { fixture as arraySortMultiKeyFixture } from '../../../adapter-tests/fixtures/methods/array-sort-multikey'
|
|
911
|
+
import { fixture as arraySortTernaryFixture } from '../../../adapter-tests/fixtures/methods/array-sort-ternary'
|
|
912
912
|
import { fixture as arrayToSortedFixture } from '../../../adapter-tests/fixtures/methods/array-toSorted'
|
|
913
913
|
// #1448 Tier B — .entries / .keys / .values iteration shapes.
|
|
914
914
|
import { fixture as arrayEntriesFixture } from '../../../adapter-tests/fixtures/methods/array-entries'
|
|
@@ -933,12 +933,17 @@ describe('MojoAdapter - #1448 Tier A/B fixture-driven lowering pins', () => {
|
|
|
933
933
|
{ fixture: stringTrimFixture, expect: 'bf->trim($value)' },
|
|
934
934
|
// #1448 Tier B — sort / toSorted. The loop-chained field cases
|
|
935
935
|
// hoist into a `my $bf_iter_lN = bf->sort(...)` local; the
|
|
936
|
-
// standalone primitive cases inline the call.
|
|
937
|
-
|
|
938
|
-
{ fixture:
|
|
939
|
-
{ fixture:
|
|
940
|
-
{ fixture:
|
|
941
|
-
{ fixture:
|
|
936
|
+
// standalone primitive cases inline the call. Each comparison key
|
|
937
|
+
// is one hash under `keys` (a single-key comparator → one element).
|
|
938
|
+
{ fixture: arraySortFieldAscFixture, expect: `bf->sort($items, { keys => [{ key_kind => 'field', key => 'price', compare_type => 'numeric', direction => 'asc' }] })` },
|
|
939
|
+
{ fixture: arraySortFieldDescFixture, expect: `bf->sort($items, { keys => [{ key_kind => 'field', key => 'price', compare_type => 'numeric', direction => 'desc' }] })` },
|
|
940
|
+
{ fixture: arraySortPrimitiveFixture, expect: `bf->sort($nums, { keys => [{ key_kind => 'self', compare_type => 'numeric', direction => 'asc' }] })` },
|
|
941
|
+
{ fixture: arraySortLocaleFixture, expect: `bf->sort($names, { keys => [{ key_kind => 'self', compare_type => 'string', direction => 'asc' }] })` },
|
|
942
|
+
// Multi-key (`||`-chain): one hash per comparison key, in order.
|
|
943
|
+
{ fixture: arraySortMultiKeyFixture, expect: `bf->sort($items, { keys => [{ key_kind => 'field', key => 'price', compare_type => 'numeric', direction => 'asc' }, { key_kind => 'field', key => 'name', compare_type => 'string', direction => 'asc' }] })` },
|
|
944
|
+
// Relational-ternary comparator lowers to a single `auto` key.
|
|
945
|
+
{ fixture: arraySortTernaryFixture, expect: `bf->sort($items, { keys => [{ key_kind => 'field', key => 'rank', compare_type => 'auto', direction => 'asc' }] })` },
|
|
946
|
+
{ fixture: arrayToSortedFixture, expect: `bf->sort($nums, { keys => [{ key_kind => 'self', compare_type => 'numeric', direction => 'asc' }] })` },
|
|
942
947
|
// #1448 Tier B — iteration shapes. These are loop-level patterns.
|
|
943
948
|
// .entries() → for loop with both $i index var and $v value var
|
|
944
949
|
{ fixture: arrayEntriesFixture, expect: '% my $v = $items->[$i];' },
|
|
@@ -966,3 +971,165 @@ describe('MojoAdapter - #1448 Tier A/B fixture-driven lowering pins', () => {
|
|
|
966
971
|
})
|
|
967
972
|
}
|
|
968
973
|
})
|
|
974
|
+
|
|
975
|
+
// =============================================================================
|
|
976
|
+
// #1448 — `/* @client */` escape hatch for STILL-UNSUPPORTED methods
|
|
977
|
+
// =============================================================================
|
|
978
|
+
//
|
|
979
|
+
// Mojo sibling of the Go block: #1448 documents `/* @client */` as the
|
|
980
|
+
// universal workaround for any Array/String method the template
|
|
981
|
+
// adapters can't lower. This pins that contract for the Mojo adapter —
|
|
982
|
+
// wrapping the unsupported expression in the directive must clear the
|
|
983
|
+
// BF021/BF101 build error the bare form raises and emit a client-only
|
|
984
|
+
// placeholder so the Mojo SSR pass renders valid `.html.ep` that the
|
|
985
|
+
// client runtime fills at hydration.
|
|
986
|
+
//
|
|
987
|
+
// Same silent-footgun caveat as Go: the unsupported *string* methods
|
|
988
|
+
// raise NO build diagnostic — bare `.startsWith` / `.repeat` / … lower
|
|
989
|
+
// to a Perl hash-deref-and-call (`$name->{startsWith}('a')`) that
|
|
990
|
+
// passes the adapter gate, then dies at render with
|
|
991
|
+
// `Can't use string (...) as a HASH ref while "strict refs"`.
|
|
992
|
+
// `/* @client */` is the only escape hatch, so these tests pin it.
|
|
993
|
+
describe('MojoAdapter - #1448 @client escape hatch (unsupported methods)', () => {
|
|
994
|
+
function emit(expr: string, client: boolean) {
|
|
995
|
+
const marker = client ? '/* @client */ ' : ''
|
|
996
|
+
const adapter = new MojoAdapter()
|
|
997
|
+
const ir = compileToIR(`
|
|
998
|
+
"use client"
|
|
999
|
+
import { createSignal } from "@barefootjs/client"
|
|
1000
|
+
export function C() {
|
|
1001
|
+
const [items, setItems] = createSignal<{ name: string; n: number; tags: string[] }[]>([])
|
|
1002
|
+
const [name, setName] = createSignal("x")
|
|
1003
|
+
return <div>{${marker}${expr}}</div>
|
|
1004
|
+
}
|
|
1005
|
+
`, adapter)
|
|
1006
|
+
const template = adapter.generate(ir).template ?? ''
|
|
1007
|
+
return { errors: adapter.errors ?? [], template }
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
function emitLoop(chain: string, client: boolean) {
|
|
1011
|
+
const marker = client ? '/* @client */ ' : ''
|
|
1012
|
+
const adapter = new MojoAdapter()
|
|
1013
|
+
const ir = compileToIR(`
|
|
1014
|
+
"use client"
|
|
1015
|
+
import { createSignal } from "@barefootjs/client"
|
|
1016
|
+
export function C() {
|
|
1017
|
+
const [items, setItems] = createSignal<{ name: string; n: number }[]>([])
|
|
1018
|
+
const myCmp = (a: { n: number }, b: { n: number }) => a.n - b.n
|
|
1019
|
+
return <ul>{${marker}${chain}}</ul>
|
|
1020
|
+
}
|
|
1021
|
+
`, adapter)
|
|
1022
|
+
const template = adapter.generate(ir).template ?? ''
|
|
1023
|
+
return { errors: adapter.errors ?? [], template }
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
// Tier C array methods — bare form raises BF101 at build time.
|
|
1027
|
+
const unsupportedArray: Array<[string, string]> = [
|
|
1028
|
+
['reduce', `items().reduce((a, b) => a + b.n, 0)`],
|
|
1029
|
+
['flatMap', `items().flatMap(i => i.tags)`],
|
|
1030
|
+
['flat', `items().flat()`],
|
|
1031
|
+
]
|
|
1032
|
+
for (const [name, expr] of unsupportedArray) {
|
|
1033
|
+
test(`array .${name}: bare raises BF101, @client clears it + emits client placeholder`, () => {
|
|
1034
|
+
const bare = emit(expr, false)
|
|
1035
|
+
expect(bare.errors.some(e => e.code === 'BF101')).toBe(true)
|
|
1036
|
+
|
|
1037
|
+
const guarded = emit(expr, true)
|
|
1038
|
+
expect(guarded.errors).toEqual([])
|
|
1039
|
+
// Client-only text slot → `<%== bf->comment("client:sN") %>`.
|
|
1040
|
+
expect(guarded.template).toMatch(/bf->comment\("client:s\d+"\)/)
|
|
1041
|
+
})
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
// Tier B/C string methods — bare form emits an INVALID Perl
|
|
1045
|
+
// hash-deref-and-call with NO build error (the silent footgun).
|
|
1046
|
+
const unsupportedString: Array<[string, string, string]> = [
|
|
1047
|
+
['split', `name().split(",")`, '->{split}'],
|
|
1048
|
+
['startsWith', `name().startsWith("a")`, '->{startsWith}'],
|
|
1049
|
+
['endsWith', `name().endsWith("z")`, '->{endsWith}'],
|
|
1050
|
+
['replace', `name().replace("a", "b")`, '->{replace}'],
|
|
1051
|
+
['repeat', `name().repeat(3)`, '->{repeat}'],
|
|
1052
|
+
['padStart', `name().padStart(5, "0")`, '->{padStart}'],
|
|
1053
|
+
['padEnd', `name().padEnd(5, "0")`, '->{padEnd}'],
|
|
1054
|
+
['charAt', `name().charAt(0)`, '->{charAt}'],
|
|
1055
|
+
]
|
|
1056
|
+
for (const [name, expr, badEmit] of unsupportedString) {
|
|
1057
|
+
test(`string .${name}: bare emits invalid Perl deref, @client emits client placeholder`, () => {
|
|
1058
|
+
const bare = emit(expr, false)
|
|
1059
|
+
// Documents the footgun: no BF101 guard, invalid template emitted.
|
|
1060
|
+
expect(bare.errors.filter(e => e.code === 'BF101')).toEqual([])
|
|
1061
|
+
expect(bare.template).toContain(badEmit)
|
|
1062
|
+
|
|
1063
|
+
const guarded = emit(expr, true)
|
|
1064
|
+
expect(guarded.errors).toEqual([])
|
|
1065
|
+
expect(guarded.template).toMatch(/bf->comment\("client:s\d+"\)/)
|
|
1066
|
+
expect(guarded.template).not.toContain(badEmit)
|
|
1067
|
+
})
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
// Tier B `.sort` / `.toSorted` follow-ups still refused with BF021.
|
|
1071
|
+
// The Mojo client-only loop placeholder is an empty element (the
|
|
1072
|
+
// client runtime repopulates it via the `bf-s` scope marker), so the
|
|
1073
|
+
// contract here is: no errors + the comparator never lowers + no
|
|
1074
|
+
// rendered `<li>` survives.
|
|
1075
|
+
const unsupportedSort: Array<[string, string]> = [
|
|
1076
|
+
['function-reference comparator', `items().toSorted(myCmp).map(x => <li key={x.name}>{x.name}</li>)`],
|
|
1077
|
+
['localeCompare locale/options arg', `items().toSorted((a, b) => a.name.localeCompare(b.name, "ja", { numeric: true })).map(x => <li key={x.name}>{x.name}</li>)`],
|
|
1078
|
+
]
|
|
1079
|
+
for (const [label, chain] of unsupportedSort) {
|
|
1080
|
+
test(`sort follow-up (${label}): bare raises BF021, @client clears it`, () => {
|
|
1081
|
+
const bare = compileJSX(`
|
|
1082
|
+
"use client"
|
|
1083
|
+
import { createSignal } from "@barefootjs/client"
|
|
1084
|
+
export function C() {
|
|
1085
|
+
const [items, setItems] = createSignal<{ name: string; n: number }[]>([])
|
|
1086
|
+
const myCmp = (a: { n: number }, b: { n: number }) => a.n - b.n
|
|
1087
|
+
return <ul>{${chain}}</ul>
|
|
1088
|
+
}
|
|
1089
|
+
`.trimStart(), 'test.tsx', { adapter: new MojoAdapter() })
|
|
1090
|
+
expect(bare.errors?.some(e => e.code === 'BF021')).toBe(true)
|
|
1091
|
+
|
|
1092
|
+
const guarded = emitLoop(chain, true)
|
|
1093
|
+
expect(guarded.errors).toEqual([])
|
|
1094
|
+
// Empty client-only loop placeholder — no item rows emitted SSR.
|
|
1095
|
+
expect(guarded.template).not.toContain('<li')
|
|
1096
|
+
expect(guarded.template).not.toContain('localeCompare')
|
|
1097
|
+
})
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
// End-to-end proof via perl + Mojolicious: the bare unsupported form
|
|
1101
|
+
// crashes Mojo template execution, while the `@client` form renders a
|
|
1102
|
+
// `<!--bf-client:sN-->` placeholder. Skipped on hosts without
|
|
1103
|
+
// Mojolicious installed.
|
|
1104
|
+
test('e2e: bare string method crashes perl render, @client renders placeholder', async () => {
|
|
1105
|
+
const guarded = `
|
|
1106
|
+
"use client"
|
|
1107
|
+
import { createSignal } from "@barefootjs/client"
|
|
1108
|
+
export function C() {
|
|
1109
|
+
const [name, setName] = createSignal("hello")
|
|
1110
|
+
return <div>{/* @client */ name().repeat(3)}</div>
|
|
1111
|
+
}
|
|
1112
|
+
`
|
|
1113
|
+
const bareSrc = guarded.replace('/* @client */ ', '')
|
|
1114
|
+
try {
|
|
1115
|
+
const html = await renderMojoComponent({
|
|
1116
|
+
source: guarded.trimStart(),
|
|
1117
|
+
adapter: new MojoAdapter(),
|
|
1118
|
+
})
|
|
1119
|
+
expect(html).toContain('<!--bf-client:s0-->')
|
|
1120
|
+
} catch (err) {
|
|
1121
|
+
if (err instanceof PerlNotAvailableError) {
|
|
1122
|
+
console.log('Skipping #1448 @client e2e: perl/Mojolicious not found')
|
|
1123
|
+
return
|
|
1124
|
+
}
|
|
1125
|
+
throw err
|
|
1126
|
+
}
|
|
1127
|
+
// Bare form must fail Mojo template execution (no @client guard).
|
|
1128
|
+
await expect(
|
|
1129
|
+
renderMojoComponent({
|
|
1130
|
+
source: bareSrc.trimStart(),
|
|
1131
|
+
adapter: new MojoAdapter(),
|
|
1132
|
+
}),
|
|
1133
|
+
).rejects.toThrow(/HASH ref/)
|
|
1134
|
+
})
|
|
1135
|
+
})
|
|
@@ -135,6 +135,9 @@ export class MojoAdapter extends BaseAdapter implements IRNodeEmitter<MojoRender
|
|
|
135
135
|
name = 'mojolicious'
|
|
136
136
|
extension = '.html.ep'
|
|
137
137
|
templatesPerComponent = true
|
|
138
|
+
// Template-string target with no component layer: `bf build` emits a static
|
|
139
|
+
// `barefoot-importmap.html` to `%= include` into the page <head> (#1644).
|
|
140
|
+
importMapInjection = 'html-snippet' as const
|
|
138
141
|
|
|
139
142
|
/**
|
|
140
143
|
* Identifier-path callees the Mojo runtime can render in template
|
|
@@ -595,9 +598,19 @@ export class MojoAdapter extends BaseAdapter implements IRNodeEmitter<MojoRender
|
|
|
595
598
|
: loop.index ? `$${loop.index}` : '$_i'
|
|
596
599
|
const prevInLoop = this.inLoop
|
|
597
600
|
this.inLoop = true
|
|
598
|
-
const
|
|
601
|
+
const renderedChildren = this.renderChildren(loop.children)
|
|
599
602
|
this.inLoop = prevInLoop
|
|
600
603
|
|
|
604
|
+
// Whole-item conditional (#1665): prepend an always-present
|
|
605
|
+
// `<!--bf-loop-i:KEY-->` anchor before each item's (possibly empty)
|
|
606
|
+
// conditional content so the client's `mapArrayAnchored` can hydrate
|
|
607
|
+
// every SSR-rendered item by its anchor. `bf->comment` prepends `bf-`,
|
|
608
|
+
// so `"loop-i:" . KEY` yields `<!--bf-loop-i:KEY-->`.
|
|
609
|
+
const children =
|
|
610
|
+
loop.bodyIsItemConditional && loop.key
|
|
611
|
+
? `<%== bf->comment("loop-i:" . ${this.convertExpressionToPerl(loop.key)}) %>\n${renderedChildren}`
|
|
612
|
+
: renderedChildren
|
|
613
|
+
|
|
601
614
|
const lines: string[] = []
|
|
602
615
|
// Scoped per-call-site marker so sibling `.map()`s under the same parent
|
|
603
616
|
// each get their own reconciliation range (#1087).
|
|
@@ -1603,10 +1616,11 @@ function renderArrayMethod(
|
|
|
1603
1616
|
* plus the loop-hoist path in `renderLoop` — same emit shape across
|
|
1604
1617
|
* all three so a regression in any one path surfaces consistently.
|
|
1605
1618
|
*
|
|
1606
|
-
* The Perl helper accepts a hash-ref opts bag
|
|
1607
|
-
*
|
|
1608
|
-
*
|
|
1609
|
-
*
|
|
1619
|
+
* The Perl helper accepts a hash-ref opts bag whose `keys` entry is
|
|
1620
|
+
* an ordered list of per-key hashes (room for a future `nulls` knob
|
|
1621
|
+
* without arity churn), and returns a fresh ARRAY ref so downstream
|
|
1622
|
+
* composition (`@{bf->sort(...)}` in `join(...)`, etc.) stays
|
|
1623
|
+
* straightforward.
|
|
1610
1624
|
*/
|
|
1611
1625
|
/**
|
|
1612
1626
|
* Encode an `IRLoop.markerId` into a Perl-identifier-safe suffix
|
|
@@ -1626,11 +1640,18 @@ function perlIdentifierFromMarkerId(markerId: string): string {
|
|
|
1626
1640
|
}
|
|
1627
1641
|
|
|
1628
1642
|
function renderSortMethod(recv: string, c: SortComparator): string {
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1643
|
+
// One hash per comparison key, in priority order, under `keys`. A
|
|
1644
|
+
// simple comparator yields a one-element list; a `||`-chained
|
|
1645
|
+
// multi-key comparator yields one per operand. `bf->sort` walks them
|
|
1646
|
+
// in order, falling through to the next on a tie.
|
|
1647
|
+
const keyHashes = c.keys.map((k) => {
|
|
1648
|
+
const keyEntry =
|
|
1649
|
+
k.key.kind === 'self'
|
|
1650
|
+
? `key_kind => 'self'`
|
|
1651
|
+
: `key_kind => 'field', key => '${k.key.field}'`
|
|
1652
|
+
return `{ ${keyEntry}, compare_type => '${k.type}', direction => '${k.direction}' }`
|
|
1653
|
+
})
|
|
1654
|
+
return `bf->sort(${recv}, { keys => [${keyHashes.join(', ')}] })`
|
|
1634
1655
|
}
|
|
1635
1656
|
|
|
1636
1657
|
/**
|
package/src/test-render.ts
CHANGED
|
@@ -305,7 +305,7 @@ function buildChildRenderers(
|
|
|
305
305
|
const lines: string[] = []
|
|
306
306
|
lines.push(`# Register child component renderers`)
|
|
307
307
|
|
|
308
|
-
for (const [componentName] of childTemplates) {
|
|
308
|
+
for (const [componentName, { ir: childIR }] of childTemplates) {
|
|
309
309
|
const snakeName = toSnakeCase(componentName)
|
|
310
310
|
const childTemplatePath = resolve(tempDir, `${snakeName}.html.ep`)
|
|
311
311
|
|
|
@@ -326,6 +326,22 @@ function buildChildRenderers(
|
|
|
326
326
|
|
|
327
327
|
lines.push(` $bf->register_child_renderer('${snakeName}', sub {`)
|
|
328
328
|
lines.push(` my ($child_props) = @_;`)
|
|
329
|
+
// (#1652) A child that destructures a rest-spread bag
|
|
330
|
+
// (`function NativeSelect({ children, ...props })`) emits a
|
|
331
|
+
// template referencing `$<restPropsName>`
|
|
332
|
+
// (`<%== bf->spread_attrs($props) %>`). The parent's render_child
|
|
333
|
+
// call only forwards the props it explicitly passed (here just
|
|
334
|
+
// `children`), so the rest bag never reaches the child stash and
|
|
335
|
+
// Perl strict mode aborts with `Global symbol "$props" requires
|
|
336
|
+
// explicit package name`. Seed it with an empty hashref when the
|
|
337
|
+
// caller didn't supply one — mirroring the top-level harness path
|
|
338
|
+
// (`buildPerlProps`) and the production runtime's
|
|
339
|
+
// `_derive_stash_from_defaults` `isRestProps` branch, which plumbs
|
|
340
|
+
// the equivalent of Go's `Spread_0`/`Extras` Input field.
|
|
341
|
+
if (childIR.metadata.restPropsName) {
|
|
342
|
+
const rest = childIR.metadata.restPropsName
|
|
343
|
+
lines.push(` $child_props->{${rest}} = {} unless defined $child_props->{${rest}};`)
|
|
344
|
+
}
|
|
329
345
|
lines.push(` ${slotIdsPerl}`)
|
|
330
346
|
lines.push(` my $child_bf = BarefootJS->new($c, {});`)
|
|
331
347
|
lines.push(` $child_bf->_scope_id("test_$sid");`)
|