@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.
@@ -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 children = this.renderChildren(loop.children);
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 keyEntry = c.key.kind === "self" ? `key_kind => 'self'` : `key_kind => 'field', key => '${c.key.field}'`;
923
- return `bf->sort(${recv}, { ${keyEntry}, compare_type => '${c.type}', direction => '${c.direction}' })`;
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":"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"}
1
+ {"version":3,"file":"mojo-adapter.d.ts","sourceRoot":"","sources":["../../src/adapter/mojo-adapter.ts"],"names":[],"mappings":"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"}
package/dist/build.js CHANGED
@@ -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 children = this.renderChildren(loop.children);
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 keyEntry = c.key.kind === "self" ? `key_kind => 'self'` : `key_kind => 'field', key => '${c.key.field}'`;
923
- return `bf->sort(${recv}, { ${keyEntry}, compare_type => '${c.type}', direction => '${c.direction}' })`;
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 children = this.renderChildren(loop.children);
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 keyEntry = c.key.kind === "self" ? `key_kind => 'self'` : `key_kind => 'field', key => '${c.key.field}'`;
923
- return `bf->sort(${recv}, { ${keyEntry}, compare_type => '${c.type}', direction => '${c.direction}' })`;
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 (compiler emits exactly these four keys):
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
- # A future `nulls => 'first' | 'last'` knob can land alongside
554
- # without churnthe opts hash is the right place to grow.
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
- my $key_kind = $opts->{key_kind} // 'self';
559
- my $key = $opts->{key} // '';
560
- my $compare_type = $opts->{compare_type} // 'numeric';
561
- my $direction = $opts->{direction} // 'asc';
562
-
563
- # Schwartzian transform: project each item to its sort key once,
564
- # then sort by key, then drop the keys. Cheaper than re-resolving
565
- # the field accessor inside every comparison for non-trivial arrays.
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 $k = $key_kind eq 'field' && ref($item) eq 'HASH' ? $item->{$key} : $item;
569
- [$k, $item]
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
- if ($compare_type eq 'string') {
574
- $cmp = $direction eq 'desc'
575
- ? sub { ($b->[0] // '') cmp ($a->[0] // '') }
576
- : sub { ($a->[0] // '') cmp ($b->[0] // '') };
577
- } else {
578
- # Numeric: undef projects to 0 so the sort is total without
579
- # warnings on missing fields. Documented divergence from JS
580
- # (which would coerce undef → NaN and produce indeterminate
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.4.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.4.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.4.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
- // #1633: child component forwards a destructured rest bag
73
- // (`function NativeSelect({ children, ...props })`) onto its
74
- // element via `{...props}`. The Mojo child template references
75
- // `$props` (`<%== $bf->spread_attrs($props) %>`), but
76
- // `render_child` doesn't plumb the rest-spread bag into the child
77
- // template's scope, so rendering dies with `Global symbol "$props"
78
- // requires explicit package name`. The Go adapter plumbs the same
79
- // bag via the `Spread_0`/`Extras map[string]any` Input field, so it
80
- // renders fine; Mojo needs the equivalent render_child plumbing.
81
- // The fixture exists to pin the CSR layer of #1633 (children
82
- // materialization), which Hono / Go / CSR all verify Mojo's
83
- // spread-bag-in-child gap is orthogonal. Same class of spread-bag
84
- // limitation that keeps `jsx-spread-rest-prop` /
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
- { fixture: arraySortFieldAscFixture, expect: `bf->sort($items, { key_kind => 'field', key => 'price', compare_type => 'numeric', direction => 'asc' })` },
938
- { fixture: arraySortFieldDescFixture, expect: `bf->sort($items, { key_kind => 'field', key => 'price', compare_type => 'numeric', direction => 'desc' })` },
939
- { fixture: arraySortPrimitiveFixture, expect: `bf->sort($nums, { key_kind => 'self', compare_type => 'numeric', direction => 'asc' })` },
940
- { fixture: arraySortLocaleFixture, expect: `bf->sort($names, { key_kind => 'self', compare_type => 'string', direction => 'asc' })` },
941
- { fixture: arrayToSortedFixture, expect: `bf->sort($nums, { key_kind => 'self', compare_type => 'numeric', direction => 'asc' })` },
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 children = this.renderChildren(loop.children)
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 (room for a future
1607
- * `nulls` knob without arity churn), and returns a fresh ARRAY ref
1608
- * so downstream composition (`@{bf->sort(...)}` in `join(...)`, etc.)
1609
- * stays straightforward.
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
- const keyEntry =
1630
- c.key.kind === 'self'
1631
- ? `key_kind => 'self'`
1632
- : `key_kind => 'field', key => '${c.key.field}'`
1633
- return `bf->sort(${recv}, { ${keyEntry}, compare_type => '${c.type}', direction => '${c.direction}' })`
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
  /**
@@ -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");`)