@barefootjs/mojolicious 0.16.0 → 0.17.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.
Files changed (53) hide show
  1. package/dist/adapter/analysis/component-tree.d.ts +30 -0
  2. package/dist/adapter/analysis/component-tree.d.ts.map +1 -0
  3. package/dist/adapter/emit-context.d.ts +96 -0
  4. package/dist/adapter/emit-context.d.ts.map +1 -0
  5. package/dist/adapter/expr/array-method.d.ts +85 -0
  6. package/dist/adapter/expr/array-method.d.ts.map +1 -0
  7. package/dist/adapter/expr/emitters.d.ts +78 -0
  8. package/dist/adapter/expr/emitters.d.ts.map +1 -0
  9. package/dist/adapter/expr/operand.d.ts +34 -0
  10. package/dist/adapter/expr/operand.d.ts.map +1 -0
  11. package/dist/adapter/index.js +1482 -1339
  12. package/dist/adapter/lib/constants.d.ts +27 -0
  13. package/dist/adapter/lib/constants.d.ts.map +1 -0
  14. package/dist/adapter/lib/ir-scope.d.ts +33 -0
  15. package/dist/adapter/lib/ir-scope.d.ts.map +1 -0
  16. package/dist/adapter/lib/perl-naming.d.ts +29 -0
  17. package/dist/adapter/lib/perl-naming.d.ts.map +1 -0
  18. package/dist/adapter/lib/types.d.ts +28 -0
  19. package/dist/adapter/lib/types.d.ts.map +1 -0
  20. package/dist/adapter/memo/seed.d.ts +34 -0
  21. package/dist/adapter/memo/seed.d.ts.map +1 -0
  22. package/dist/adapter/mojo-adapter.d.ts +46 -104
  23. package/dist/adapter/mojo-adapter.d.ts.map +1 -1
  24. package/dist/adapter/props/prop-classes.d.ts +48 -0
  25. package/dist/adapter/props/prop-classes.d.ts.map +1 -0
  26. package/dist/adapter/spread/spread-codegen.d.ts +64 -0
  27. package/dist/adapter/spread/spread-codegen.d.ts.map +1 -0
  28. package/dist/adapter/value/parsed-literal.d.ts +26 -0
  29. package/dist/adapter/value/parsed-literal.d.ts.map +1 -0
  30. package/dist/build.js +1482 -1339
  31. package/dist/index.js +1482 -1339
  32. package/dist/test-render.d.ts.map +1 -1
  33. package/lib/BarefootJS/Backend/Mojo.pm +1 -1
  34. package/lib/Mojolicious/Plugin/BarefootJS/DevReload.pm +1 -1
  35. package/lib/Mojolicious/Plugin/BarefootJS.pm +1 -1
  36. package/package.json +3 -3
  37. package/src/__tests__/mojo-adapter.test.ts +286 -69
  38. package/src/__tests__/query-href.test.ts +94 -0
  39. package/src/adapter/analysis/component-tree.ts +128 -0
  40. package/src/adapter/emit-context.ts +107 -0
  41. package/src/adapter/expr/array-method.ts +429 -0
  42. package/src/adapter/expr/emitters.ts +639 -0
  43. package/src/adapter/expr/operand.ts +55 -0
  44. package/src/adapter/lib/constants.ts +39 -0
  45. package/src/adapter/lib/ir-scope.ts +57 -0
  46. package/src/adapter/lib/perl-naming.ts +36 -0
  47. package/src/adapter/lib/types.ts +31 -0
  48. package/src/adapter/memo/seed.ts +73 -0
  49. package/src/adapter/mojo-adapter.ts +248 -1485
  50. package/src/adapter/props/prop-classes.ts +87 -0
  51. package/src/adapter/spread/spread-codegen.ts +181 -0
  52. package/src/adapter/value/parsed-literal.ts +34 -0
  53. package/src/test-render.ts +2 -0
@@ -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;IACnC;;;;;OAKG;IACH,aAAa,CAAC,EAAE,MAAM,CAAA;CACvB;AAED,wBAAsB,mBAAmB,CAAC,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,MAAM,CAAC,CAgNjF;AAmWD;;;GAGG;AACH,wBAAgB,kBAAkB,CAChC,IAAI,EAAE,MAAM,EACZ,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC9B,OAAO,CAuBT"}
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;IACnC;;;;;OAKG;IACH,aAAa,CAAC,EAAE,MAAM,CAAA;CACvB;AAED,wBAAsB,mBAAmB,CAAC,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,MAAM,CAAC,CAgNjF;AAqWD;;;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 BarefootJS::Backend::Mojo;
2
- our $VERSION = "0.15.2";
2
+ our $VERSION = "0.17.0";
3
3
  use Mojo::Base -base, -signatures;
4
4
 
5
5
  use Mojo::ByteStream qw(b);
@@ -1,5 +1,5 @@
1
1
  package Mojolicious::Plugin::BarefootJS::DevReload;
2
- our $VERSION = "0.15.2";
2
+ our $VERSION = "0.17.0";
3
3
  use Mojo::Base 'Mojolicious::Plugin', -signatures;
4
4
 
5
5
  =head1 NAME
@@ -1,5 +1,5 @@
1
1
  package Mojolicious::Plugin::BarefootJS;
2
- our $VERSION = "0.15.2";
2
+ our $VERSION = "0.17.0";
3
3
  use Mojo::Base 'Mojolicious::Plugin', -signatures;
4
4
 
5
5
  use Mojo::File qw(path);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@barefootjs/mojolicious",
3
- "version": "0.16.0",
3
+ "version": "0.17.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,7 +52,7 @@
52
52
  "directory": "packages/adapter-mojolicious"
53
53
  },
54
54
  "dependencies": {
55
- "@barefootjs/shared": "0.16.0"
55
+ "@barefootjs/shared": "0.17.1"
56
56
  },
57
57
  "peerDependencies": {
58
58
  "@barefootjs/jsx": ">=0.2.0",
@@ -60,6 +60,6 @@
60
60
  },
61
61
  "devDependencies": {
62
62
  "@barefootjs/adapter-tests": "0.1.0",
63
- "@barefootjs/jsx": "0.16.0"
63
+ "@barefootjs/jsx": "0.17.1"
64
64
  }
65
65
  }
@@ -65,6 +65,14 @@ runAdapterConformanceTests({
65
65
  // (`cn\`base \${tone()}\``) — same family as #1322 above and refused
66
66
  // via the same gate.
67
67
  'tagged-template-classname': [{ code: 'BF101', severity: 'error' }],
68
+ // #2038: a filter predicate containing a nested `.find(...)` callback.
69
+ // `find*` returns an element, not a boolean — there is no inline grep
70
+ // form, and the emitter used to degrade the call to its receiver.
71
+ // The nested `.some` sibling (`filter-nested-callback-predicate`) is
72
+ // NOT pinned: Mojo lowers it to a real inline Perl `grep` and must
73
+ // render to Hono parity instead.
74
+ // https://github.com/piconic-ai/barefootjs/issues/2038
75
+ 'filter-nested-find-predicate': [{ code: 'BF101', severity: 'error' }],
68
76
  // #1467 demo-corpus context providers (`radio-group`, `accordion`,
69
77
  // `dialog`, `popover`, `select`, `dropdown-menu`, `combobox`,
70
78
  // `command`) are no longer pinned — an object-literal provider value
@@ -122,10 +130,15 @@ runAdapterConformanceTests({
122
130
  // (Go) and new `bf->trim` helper (Mojo) handle the strip
123
131
  // (#1448 Tier A ninth PR, closing out Tier A).
124
132
  // `.find` / `.findIndex` / `.findLast` / `.findLastIndex` are no longer
125
- // pinned — the Mojo `higherOrder` emitter now lowers them to the runtime
126
- // `bf->find` / `find_index` / `find_last` / `find_last_index` helpers
133
+ // pinned — the Mojo `callbackMethod` predicate arm now lowers them to the
134
+ // runtime `bf->find` / `find_index` / `find_last` / `find_last_index` helpers
127
135
  // (per-element coderef predicate), matching Xslate. `.join` was never
128
136
  // pinned (handled by `renderArrayMethod`'s `case 'join'`).
137
+ // #2073 follow-up: a function-reference `.map(format)` callback has no
138
+ // arrow body to serialize — not a CALLBACK_METHODS shape — so the
139
+ // UNSUPPORTED_METHODS gate refuses it with BF101 rather than emitting
140
+ // a broken template.
141
+ 'array-map-function-reference': [{ code: 'BF101', severity: 'error' }],
129
142
  },
130
143
  // `JSON_STRINGIFY_VIA_CONST` and `MATH_FLOOR_VIA_CONST` now pass
131
144
  // via `MojoAdapter.templatePrimitives` (#1189). The two remaining
@@ -143,16 +156,12 @@ runAdapterConformanceTests({
143
156
  TemplatePrimitiveCaseId.USER_IMPORT_VIA_CONST,
144
157
  TemplatePrimitiveCaseId.NO_DOUBLE_REWRITE_OF_PROPS_OBJECT,
145
158
  ]),
146
- // Mojo `renderLoop` does not yet emit the `bf->comment("loop:<id>")`
147
- // boundary markers when the loop is `@client` (Hono and Go both do).
148
- // The client runtime relies on these markers to locate the insertion
149
- // anchor when hydrating the array; without them, mapArray() resolves
150
- // anchor = null and appends after sibling markers (#872 parity).
151
- // Tracked as a follow-up; remove from this set when Mojo emits the
152
- // boundary pair for clientOnly loops too.
159
+ // `client-only` / `client-only-loop-with-sibling-cond` /
160
+ // `filter-nested-callback-predicate-client` are no longer skipped
161
+ // `renderLoop` now emits the `bf->comment("loop:<id>")` boundary pair
162
+ // for clientOnly loops (Hono / Go parity), so mapArray() can locate
163
+ // its insertion anchor at hydration time (#872 / #1087).
153
164
  skipMarkerConformance: new Set([
154
- 'client-only',
155
- 'client-only-loop-with-sibling-cond',
156
165
  // Same as Hono: `/* @client */` markers on TodoApp's keyed `.map`
157
166
  // intentionally elide a slot id from the SSR template that the IR
158
167
  // still declares (s6). See hono-adapter.test for the contract.
@@ -239,8 +248,9 @@ describe('MojoAdapter - searchParams() env-signal lowering (#1922)', () => {
239
248
  // generic hash deref `$searchParams->{get}` (which drops the arg).
240
249
  test('lowers searchParams().get(k) to a method call on $searchParams', () => {
241
250
  const { template } = compileAndGenerate(`
242
- import { searchParams } from '@barefootjs/client'
251
+ import { createSearchParams } from '@barefootjs/client'
243
252
  function SortLabel() {
253
+ const [searchParams] = createSearchParams()
244
254
  return <p>{searchParams().get('sort') ?? 'none'}</p>
245
255
  }
246
256
  `)
@@ -248,13 +258,14 @@ function SortLabel() {
248
258
  expect(template).not.toContain('$searchParams->{get}')
249
259
  })
250
260
 
251
- // An aliased import binds the env signal to a different local name; the
252
- // expression reads `sp()`, but it still lowers to the canonical
261
+ // An aliased destructured getter binds the env signal to a different local
262
+ // name; the expression reads `sp()`, but it still lowers to the canonical
253
263
  // `$searchParams` reader (the harness/plugin seed that fixed var).
254
- test('matches an aliased import (`searchParams as sp`) and emits canonical $searchParams', () => {
264
+ test('matches an aliased env-signal getter (`const [sp] = createSearchParams()`) and emits canonical $searchParams', () => {
255
265
  const { template } = compileAndGenerate(`
256
- import { searchParams as sp } from '@barefootjs/client'
266
+ import { createSearchParams } from '@barefootjs/client'
257
267
  function SortLabel() {
268
+ const [sp] = createSearchParams()
258
269
  return <p>{sp().get('sort') ?? 'none'}</p>
259
270
  }
260
271
  `)
@@ -720,10 +731,12 @@ export function C() {
720
731
  // empty array there, which a template can't mirror.
721
732
  { name: 'reduce (no init)', body: `<div>{items().reduce((s, x) => s + x)}</div>`, needle: '.reduce(' },
722
733
  { name: 'forEach', body: `<ul>{items().forEach(x => x)}</ul>`, needle: '.forEach(' },
723
- // Self / field / field-tuple `.flatMap` now lowers (#1448 Tier C) —
724
- // even as a loop base so those moved to positive tests below. A
725
- // tuple with a non-leaf element (a string literal) stays refused.
726
- { name: 'flatMap (literal element)', body: `<div>{items().flatMap(x => [x.tag, "x"])}</div>`, needle: '.flatMap(' },
734
+ // #2018 P5: an array-literal projection with a literal element
735
+ // (`x => [x.tag, "x"]`) now lowers through the runtime evaluator
736
+ // (`bf->flat_map_eval`) the structured-tuple restriction that
737
+ // refused a non-`self`/`field` leaf is gone. See the positive
738
+ // flat_map_eval pins below. The remaining BF101 flatMap surface
739
+ // (a projection the evaluator can't serialize) has no example here.
727
740
  ]
728
741
 
729
742
  for (const { name, body, needle } of cases) {
@@ -760,7 +773,11 @@ export function C() {
760
773
  }`, 'C.tsx', { adapter })
761
774
  expect(result.errors?.filter(e => e.code === 'BF101') ?? []).toEqual([])
762
775
  const template = result.files.find(f => f.path.endsWith('.html.ep'))?.content ?? ''
763
- expect(template).toContain('grep { $_->{done} } @{$items}')
776
+ // #2018 P2: the `.filter().map()` loop hoists over the filtered array,
777
+ // now produced by the evaluator (`bf->filter_eval`). (Hoisting the call
778
+ // into a single `my` var is a P3 optimization.)
779
+ expect(template).toContain('bf->filter_eval($items,')
780
+ expect(template).toContain('"property":"done"')
764
781
  })
765
782
 
766
783
  test('lowers nested .filter(...).length > 0 in outer filter predicate (#1443 PR4)', () => {
@@ -784,6 +801,31 @@ export function C() {
784
801
  expect(template).toContain('scalar(@{[grep { $_->{active} } @{$t->{tags}}]})')
785
802
  })
786
803
 
804
+ test('lowers nested .some(...) in filter predicate to an inline grep — no BF101 (#2038)', () => {
805
+ // The evaluator refuses the nested arrow (`serializeParsedExpr` → null),
806
+ // but the Perl filter emitter has a FAITHFUL form for nested
807
+ // filter / every / some: a real inline `grep` closing over the outer
808
+ // loop var. Pin the emitted EP shape positively so the #2038 loudness
809
+ // fix (which targets the degrade-only arms: nested `find*`,
810
+ // sort / reduce / flatMap — see the `filter-nested-find-predicate`
811
+ // expectedDiagnostics entry) never over-reaches into this supported
812
+ // shape. The rendered-HTML side of this contract lives in the shared
813
+ // `filter-nested-callback-predicate` fixture (Hono-parity render).
814
+ const adapter = new MojoAdapter()
815
+ const result = compileJSX(`'use client'
816
+ import { createSignal } from '@barefootjs/client'
817
+ type Item = { id: number }
818
+ export function Picker() {
819
+ const [items] = createSignal<Item[]>([])
820
+ const [picked] = createSignal<Item[]>([])
821
+ return <ul>{items().filter(t => !picked().some(p => p.id === t.id)).map(t => <li key={t.id}>{t.id}</li>)}</ul>
822
+ }`, 'C.tsx', { adapter })
823
+ expect(result.errors?.filter(e => e.code === 'BF101') ?? []).toEqual([])
824
+ const template = result.files.find(f => f.path.endsWith('.html.ep'))?.content ?? ''
825
+ expect(template).toContain('grep')
826
+ expect(template).toContain('@{$picked}')
827
+ })
828
+
787
829
  test('lowers .filter(function (x) { return x.done }).map(...) — function-keyword filter (#1443)', () => {
788
830
  // Function expressions with a single `return <expr>` body normalise
789
831
  // to the arrow-fn IR shape at parse time, so the higher-order
@@ -798,7 +840,11 @@ export function C() {
798
840
  }`, 'C.tsx', { adapter })
799
841
  expect(result.errors?.filter(e => e.code === 'BF101') ?? []).toEqual([])
800
842
  const template = result.files.find(f => f.path.endsWith('.html.ep'))?.content ?? ''
801
- expect(template).toContain('grep { $_->{done} } @{$items}')
843
+ // #2018 P2: the `.filter().map()` loop hoists over the filtered array,
844
+ // now produced by the evaluator (`bf->filter_eval`). (Hoisting the call
845
+ // into a single `my` var is a P3 optimization.)
846
+ expect(template).toContain('bf->filter_eval($items,')
847
+ expect(template).toContain('"property":"done"')
802
848
  })
803
849
 
804
850
  test('lowers the registry Slot\'s [a, b].filter(Boolean).join(\' \') chain (#1443)', () => {
@@ -977,17 +1023,18 @@ export { A }`, 'A.tsx', { adapter })
977
1023
  expect(template).not.toContain('$bf->trim')
978
1024
  })
979
1025
 
980
- test('lowers .find / .findIndex / .findLast / .findLastIndex via runtime helpers', () => {
981
- // The runtime `bf->find` / `find_index` / `find_last` / `find_last_index`
982
- // call the predicate as a per-element coderef; the camelCase JS names map
983
- // to the snake_case helpers (matching Xslate's Kolon-lambda lowering).
984
- const cases: Array<[string, string]> = [
985
- ['find', 'find'],
986
- ['findIndex', 'find_index'],
987
- ['findLast', 'find_last'],
988
- ['findLastIndex', 'find_last_index'],
1026
+ test('lowers .find / .findIndex / .findLast / .findLastIndex via the evaluator', () => {
1027
+ // #2018 P2: the pure predicate `x => x === 'b'` serializes and lowers
1028
+ // through the evaluator. find / findIndex share `bf->find_eval` /
1029
+ // `bf->find_index_eval` with findLast / findLastIndex, distinguished by
1030
+ // the `forward` flag (1 = forward, 0 = backward).
1031
+ const cases: Array<[string, string, number]> = [
1032
+ ['find', 'find_eval', 1],
1033
+ ['findIndex', 'find_index_eval', 1],
1034
+ ['findLast', 'find_eval', 0],
1035
+ ['findLastIndex', 'find_index_eval', 0],
989
1036
  ]
990
- for (const [js, helper] of cases) {
1037
+ for (const [js, helper, fwd] of cases) {
991
1038
  const adapter = new MojoAdapter()
992
1039
  const result = compileJSX(`function A({ items }: { items: string[] }) {
993
1040
  return <div>{items.${js}(x => x === 'b')}</div>
@@ -995,11 +1042,60 @@ export { A }`, 'A.tsx', { adapter })
995
1042
  export { A }`, 'A.tsx', { adapter })
996
1043
  expect(result.errors?.filter(e => e.code === 'BF101') ?? []).toEqual([])
997
1044
  const template = result.files.find(f => f.path.endsWith('.html.ep'))?.content ?? ''
998
- expect(template).toContain(`bf->${helper}($items, sub { my $x = $_[0]; $x eq 'b' })`)
1045
+ expect(template).toContain(`bf->${helper}($items,`)
1046
+ expect(template).toContain(`'x', ${fwd}, {})`)
999
1047
  expect(template).not.toContain(`$bf->${helper}`)
1000
1048
  }
1001
1049
  })
1002
1050
 
1051
+ test('lowers .every / .some via the evaluator, with grep fallback for a method-call predicate', () => {
1052
+ // #2018 P2: a pure predicate routes `.every` / `.some` through
1053
+ // `bf->every_eval` / `bf->some_eval`; a method-call predicate the
1054
+ // evaluator can't model (`serializeParsedExpr` → null) falls back to the
1055
+ // inline `grep` form.
1056
+ const evalCases: Array<[string, string]> = [
1057
+ ['every', 'every_eval'],
1058
+ ['some', 'some_eval'],
1059
+ ]
1060
+ for (const [js, helper] of evalCases) {
1061
+ const adapter = new MojoAdapter()
1062
+ const result = compileJSX(`function A({ items }: { items: { done: boolean }[] }) {
1063
+ return <div>{items.${js}(x => x.done) ? 'y' : 'n'}</div>
1064
+ }
1065
+ export { A }`, 'A.tsx', { adapter })
1066
+ expect(result.errors?.filter(e => e.code === 'BF101') ?? []).toEqual([])
1067
+ const template = result.files.find(f => f.path.endsWith('.html.ep'))?.content ?? ''
1068
+ expect(template).toContain(`bf->${helper}($items,`)
1069
+ expect(template).toContain('"property":"done"')
1070
+ }
1071
+
1072
+ // #2075: `.includes(x)` is now in the evaluator surface (`array-method`
1073
+ // gate, shared with the Perl `Evaluator.pm` runtime), so a method-call
1074
+ // predicate built from it ALSO routes through `every_eval` rather than
1075
+ // falling back — it's no longer the "unsupported method call" example.
1076
+ const includesAdapter = new MojoAdapter()
1077
+ const includesResult = compileJSX(`function A({ items }: { items: { name: string }[] }) {
1078
+ return <div>{items.every(x => x.name.includes('a')) ? 'y' : 'n'}</div>
1079
+ }
1080
+ export { A }`, 'A.tsx', { adapter: includesAdapter })
1081
+ const includesTemplate = includesResult.files.find(f => f.path.endsWith('.html.ep'))?.content ?? ''
1082
+ expect(includesTemplate).toContain('bf->every_eval($items,')
1083
+ expect(includesTemplate).toContain('"method":"includes"')
1084
+ expect(includesTemplate).not.toContain('grep {')
1085
+
1086
+ // Fallback: a method-call predicate the evaluator still can't model
1087
+ // (`.toUpperCase()` is outside the `array-method` gate — only `includes`
1088
+ // is recognized there) keeps the inline grep form.
1089
+ const adapter = new MojoAdapter()
1090
+ const fb = compileJSX(`function A({ items }: { items: { name: string }[] }) {
1091
+ return <div>{items.every(x => x.name.toUpperCase() === 'A') ? 'y' : 'n'}</div>
1092
+ }
1093
+ export { A }`, 'A.tsx', { adapter })
1094
+ const fbTemplate = fb.files.find(f => f.path.endsWith('.html.ep'))?.content ?? ''
1095
+ expect(fbTemplate).toContain('grep {')
1096
+ expect(fbTemplate).not.toContain('every_eval')
1097
+ })
1098
+
1003
1099
  test('lowers .reverse().join(\' \') via bf->reverse + join (#1448 Tier A)', () => {
1004
1100
  // SSR templates render a snapshot, so `.reverse` and
1005
1101
  // `.toReversed` share a Mojo lowering — both return a new
@@ -1355,19 +1451,26 @@ describe('MojoAdapter - #1448 Tier A/B fixture-driven lowering pins', () => {
1355
1451
  // #1448 Tier B — string → string, padded to a target width.
1356
1452
  { fixture: stringPadStartFixture, expect: `bf->pad_start($value, 5, '0')` },
1357
1453
  { fixture: stringPadEndFixture, expect: `bf->pad_end($value, 5, '.')` },
1358
- // #1448 Tier B — sort / toSorted. The loop-chained field cases
1359
- // hoist into a `my $bf_iter_lN = bf->sort(...)` local; the
1360
- // standalone primitive cases inline the call. Each comparison key
1361
- // is one hash under `keys` (a single-key comparator one element).
1362
- { fixture: arraySortFieldAscFixture, expect: `bf->sort($items, { keys => [{ key_kind => 'field', key => 'price', compare_type => 'numeric', direction => 'asc' }] })` },
1363
- { fixture: arraySortFieldDescFixture, expect: `bf->sort($items, { keys => [{ key_kind => 'field', key => 'price', compare_type => 'numeric', direction => 'desc' }] })` },
1364
- { fixture: arraySortPrimitiveFixture, expect: `bf->sort($nums, { keys => [{ key_kind => 'self', compare_type => 'numeric', direction => 'asc' }] })` },
1454
+ // #1448 Tier B — sort / toSorted. EXPR2 migration (#2018): both a
1455
+ // STANDALONE `.sort(cmp)` value call AND the `.sort().map()` loop-hoist
1456
+ // (#2018 P3) serialize the comparator body and emit `bf->sort_eval(...)`
1457
+ // (JSON body + param names + captured env). `localeCompare` comparators
1458
+ // fall back to the structured `bf->sort` `serializeParsedExpr` refuses
1459
+ // them. (Loop-hoist field cases pin only the helper + receiver; their
1460
+ // comparator JSON is verified by the render conformance.)
1461
+ { fixture: arraySortFieldAscFixture, expect: `bf->sort_eval($items,` },
1462
+ { fixture: arraySortFieldDescFixture, expect: `bf->sort_eval($items,` },
1463
+ { fixture: arraySortPrimitiveFixture, expect: `bf->sort_eval($nums, '{"kind":"binary","op":"-","left":{"kind":"identifier","name":"a"},"right":{"kind":"identifier","name":"b"}}', 'a', 'b', {})` },
1464
+ // localeCompare → outside the evaluator surface → legacy `bf->sort`.
1365
1465
  { fixture: arraySortLocaleFixture, expect: `bf->sort($names, { keys => [{ key_kind => 'self', compare_type => 'string', direction => 'asc' }] })` },
1366
- // Multi-key (`||`-chain): one hash per comparison key, in order.
1466
+ // Multi-key (`||`-chain): the second key is a `localeCompare`, so the
1467
+ // whole comparator falls back to the structured `bf->sort` (one hash
1468
+ // per comparison key, in order).
1367
1469
  { 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' }] })` },
1368
- // Relational-ternary comparator lowers to a single `auto` key.
1369
- { fixture: arraySortTernaryFixture, expect: `bf->sort($items, { keys => [{ key_kind => 'field', key => 'rank', compare_type => 'auto', direction => 'asc' }] })` },
1370
- { fixture: arrayToSortedFixture, expect: `bf->sort($nums, { keys => [{ key_kind => 'self', compare_type => 'numeric', direction => 'asc' }] })` },
1470
+ // Relational-ternary comparator a pure body, so the loop-hoist now
1471
+ // serializes it through `bf->sort_eval` like the other field sorts.
1472
+ { fixture: arraySortTernaryFixture, expect: `bf->sort_eval($items,` },
1473
+ { fixture: arrayToSortedFixture, expect: `bf->sort_eval($nums, '{"kind":"binary","op":"-","left":{"kind":"identifier","name":"a"},"right":{"kind":"identifier","name":"b"}}', 'a', 'b', {})` },
1371
1474
  // #1448 Tier B — iteration shapes. These are loop-level patterns.
1372
1475
  // .entries() → for loop with both $i index var and $v value var
1373
1476
  { fixture: arrayEntriesFixture, expect: '% my $v = $items->[$i];' },
@@ -1375,17 +1478,19 @@ describe('MojoAdapter - #1448 Tier A/B fixture-driven lowering pins', () => {
1375
1478
  { fixture: arrayKeysFixture, expect: '% for my $k (0..$#{$items})' },
1376
1479
  // .values() → standard for loop (same as plain .map())
1377
1480
  { fixture: arrayValuesFixture, expect: '% my $v = $items->[$_i];' },
1378
- // #1448 Tier C — .reduce(fn, init) arithmetic fold. The structured
1379
- // ReduceOp lowers to a single `bf->reduce(...)` call with op /
1380
- // key / type / init / direction in the options hash. Each shape
1381
- // exercises one arm: field-numeric sum, self-numeric sum,
1382
- // string-concat fold, the product (`*`) operator, and the
1383
- // right-to-left `direction` of reduceRight.
1384
- { fixture: reduceSumFieldFixture, expect: `bf->reduce($items, { op => '+', key_kind => 'field', key => 'duration', type => 'numeric', init => 0, direction => 'left' })` },
1385
- { fixture: reduceSumSelfFixture, expect: `bf->reduce($nums, { op => '+', key_kind => 'self', type => 'numeric', init => 0, direction => 'left' })` },
1386
- { fixture: reduceConcatFixture, expect: `bf->reduce($items, { op => '+', key_kind => 'field', key => 'label', type => 'string', init => '', direction => 'left' })` },
1387
- { fixture: reduceProductFixture, expect: `bf->reduce($items, { op => '*', key_kind => 'field', key => 'qty', type => 'numeric', init => 1, direction => 'left' })` },
1388
- { fixture: reduceRightConcatFixture, expect: `bf->reduce($items, { op => '+', key_kind => 'field', key => 'label', type => 'string', init => '', direction => 'right' })` },
1481
+ // #1448 Tier C — .reduce(fn, init) arithmetic fold. EXPR2 migration
1482
+ // (#2018): the reducer body is serialized to ParsedExpr JSON and
1483
+ // folded by `bf->reduce_eval(json, accName, itemName, init, direction,
1484
+ // env)` the runtime evaluator subsumes the old `+`/`*` catalogue.
1485
+ // A numeric seed passes through bare (`0` / `1`); a concat seed as a
1486
+ // single-quoted string (`''`). Each shape exercises one arm: field-
1487
+ // numeric sum, self-numeric sum, string-concat fold, the product
1488
+ // (`*`) operator, and the right-to-left `direction` of reduceRight.
1489
+ { fixture: reduceSumFieldFixture, expect: `bf->reduce_eval($items, '{"kind":"binary","op":"+","left":{"kind":"identifier","name":"sum"},"right":{"kind":"member","object":{"kind":"identifier","name":"t"},"property":"duration"}}', 'sum', 't', 0, 'left', {})` },
1490
+ { fixture: reduceSumSelfFixture, expect: `bf->reduce_eval($nums, '{"kind":"binary","op":"+","left":{"kind":"identifier","name":"a"},"right":{"kind":"identifier","name":"b"}}', 'a', 'b', 0, 'left', {})` },
1491
+ { fixture: reduceConcatFixture, expect: `bf->reduce_eval($items, '{"kind":"binary","op":"+","left":{"kind":"identifier","name":"acc"},"right":{"kind":"member","object":{"kind":"identifier","name":"x"},"property":"label"}}', 'acc', 'x', '', 'left', {})` },
1492
+ { fixture: reduceProductFixture, expect: `bf->reduce_eval($items, '{"kind":"binary","op":"*","left":{"kind":"identifier","name":"acc"},"right":{"kind":"member","object":{"kind":"identifier","name":"x"},"property":"qty"}}', 'acc', 'x', 1, 'left', {})` },
1493
+ { fixture: reduceRightConcatFixture, expect: `bf->reduce_eval($items, '{"kind":"binary","op":"+","left":{"kind":"identifier","name":"acc"},"right":{"kind":"member","object":{"kind":"identifier","name":"x"},"property":"label"}}', 'acc', 'x', '', 'right', {})` },
1389
1494
  ]
1390
1495
 
1391
1496
  for (const { fixture, expect: expectedHelper } of cases) {
@@ -1432,6 +1537,110 @@ export { C }
1432
1537
  })
1433
1538
  })
1434
1539
 
1540
+ describe('MojoAdapter - #2075 searchParams()-derived memo seeding', () => {
1541
+ // A memo derived from the createSearchParams() env signal must seed
1542
+ // in-template from the canonical per-request `$searchParams` reader —
1543
+ // including under a local alias (`const [sp] = …`), which the expression
1544
+ // lowering canonicalises.
1545
+ test('seeds an aliased scalar derived memo from the canonical reader', () => {
1546
+ const { template } = compileAndGenerate(`
1547
+ 'use client'
1548
+ import { createMemo, createSearchParams } from '@barefootjs/client'
1549
+ export function SortStatus() {
1550
+ const [sp] = createSearchParams()
1551
+ const sort = createMemo(() => sp().get('sort') ?? 'date')
1552
+ return <p>sort: {sort()}</p>
1553
+ }
1554
+ `)
1555
+ expect(template).toContain("% my $sort = ($searchParams->get('sort') // 'date');")
1556
+ })
1557
+
1558
+ // A list-filter memo chained off the derived memo seeds too: the inline
1559
+ // grep's `$_` topic and the callback param are lowering-internal bindings,
1560
+ // not out-of-scope template vars (the pre-#2075 availability check
1561
+ // rejected them and the list rendered empty at SSR).
1562
+ test('seeds a filter memo chained off the derived memo', () => {
1563
+ const { template } = compileAndGenerate(`
1564
+ 'use client'
1565
+ import { createMemo, createSearchParams } from '@barefootjs/client'
1566
+ export function TaggedList(props: { items: { title: string; tags: string[] }[] }) {
1567
+ const [searchParams] = createSearchParams()
1568
+ const tag = createMemo(() => searchParams().get('tag') ?? '')
1569
+ const visible = createMemo(() => props.items.filter((p) => !tag() || p.tags.includes(tag())))
1570
+ return <ul>{visible().map((p) => <li key={p.title}>{p.title}</li>)}</ul>
1571
+ }
1572
+ `)
1573
+ expect(template).toContain("% my $tag = ($searchParams->get('tag') // '');")
1574
+ expect(template).toMatch(/% my \$visible = \[grep/)
1575
+ })
1576
+
1577
+ // The seed-scope guard used to scan the LOWERED
1578
+ // Perl string, allowing every arrow-callback param tree-wide. That let an
1579
+ // outer, unbound `p` (shadowed only inside the callback) slip past the
1580
+ // guard as if it were the callback's own bound `$p` — emitting a bogus
1581
+ // seed line that would crash Perl strict mode. The guard now walks the
1582
+ // parsed SOURCE tree with proper lexical scoping (`freeIdentifiers`), so
1583
+ // this shape seeds nothing and falls back to the null/ssr-defaults path.
1584
+ test('an outer unbound `p` shadowed only inside the callback does not seed', () => {
1585
+ const { template } = compileAndGenerate(`
1586
+ 'use client'
1587
+ import { createMemo } from '@barefootjs/client'
1588
+ export function C(props: { items: { ok: boolean }[] }) {
1589
+ const visible = createMemo(() => props.items.filter((p) => p.ok) && p)
1590
+ return <div>{String(visible())}</div>
1591
+ }
1592
+ `)
1593
+ expect(template).not.toContain('my $visible')
1594
+ })
1595
+
1596
+ // An out-of-scope bare `_` reference (not the `grep` topic var of an
1597
+ // in-scope higher-order lowering) must not seed either — the old
1598
+ // unconditional `allowed.add('_')` masked this.
1599
+ test('an out-of-scope bare `_` reference does not seed', () => {
1600
+ const { template } = compileAndGenerate(`
1601
+ 'use client'
1602
+ import { createMemo } from '@barefootjs/client'
1603
+ export function C(props: { count: number }) {
1604
+ const doubled = createMemo(() => props.count * 2 + _)
1605
+ return <div>{doubled()}</div>
1606
+ }
1607
+ `)
1608
+ expect(template).not.toContain('my $doubled')
1609
+ })
1610
+ })
1611
+
1612
+ describe('MojoAdapter - #2073 value-producing .map(cb)', () => {
1613
+ function emitMap(expr: string): string {
1614
+ const a = new MojoAdapter()
1615
+ const ir = compileToIR(`
1616
+ function C({ tags, users }: { tags: string[]; users: { name: string }[] }) {
1617
+ return <div>{${expr}}</div>
1618
+ }
1619
+ export { C }
1620
+ `, a)
1621
+ return a.generate(ir).template ?? ''
1622
+ }
1623
+
1624
+ // The blog-showcase shape (#1938/#1939): a value-returning `.map` (string
1625
+ // projection, not JSX) lowers through the evaluator — `bf->map_eval`
1626
+ // projects each element (no flatten) and composes through `.join`.
1627
+ test('.map(t => `#${t}`).join(" ") emits bf->map_eval composed into join', () => {
1628
+ const t = emitMap("tags.map(t => `#${t}`).join(' ')")
1629
+ expect(t).toContain(`join(' ', @{bf->map_eval($tags,`)
1630
+ expect(t).toContain(`"kind":"template-literal"`)
1631
+ })
1632
+
1633
+ test('.map(u => u.name) emits bf->map_eval with the field projection', () => {
1634
+ const t = emitMap("users.map(u => u.name).join(', ')")
1635
+ expect(t).toContain(`bf->map_eval($users,`)
1636
+ expect(t).toContain(`"property":"name"`)
1637
+ })
1638
+
1639
+ // The function-reference `.map(format)` BF101 refusal is now covered
1640
+ // cross-adapter by the `array-map-function-reference` shared fixture's
1641
+ // `expectedDiagnostics` entry above.
1642
+ })
1643
+
1435
1644
  describe('MojoAdapter - #1448 Tier C .flatMap(field projection)', () => {
1436
1645
  function emitFlatMap(expr: string): string {
1437
1646
  const a = new MojoAdapter()
@@ -1444,20 +1653,26 @@ export { C }
1444
1653
  return a.generate(ir).template ?? ''
1445
1654
  }
1446
1655
 
1447
- test('.flatMap(i => i.field) emits bf->flat_map with the raw field key', () => {
1448
- expect(emitFlatMap('rows.flatMap(i => i.tags).join(" ")')).toContain(`bf->flat_map($rows, 'field', 'tags')`)
1449
- })
1450
-
1451
- test('.flatMap(i => i) emits the self projection', () => {
1452
- expect(emitFlatMap('rows.flatMap(i => i).join(" ")')).toContain(`bf->flat_map($rows, 'self', '')`)
1656
+ // #2018 P3: `.flatMap(proj)` lowers through the evaluator the projection
1657
+ // body serializes to ParsedExpr JSON and `bf->flat_map_eval` flattens the
1658
+ // results one level. (In Mojo the JSON rides in a single-quoted Perl string,
1659
+ // so its double quotes are literal and assertable.)
1660
+ test('.flatMap(i => i.field) emits bf->flat_map_eval with the field projection', () => {
1661
+ const t = emitFlatMap('rows.flatMap(i => i.tags).join(" ")')
1662
+ expect(t).toContain(`bf->flat_map_eval($rows,`)
1663
+ expect(t).toContain(`"property":"tags"`)
1453
1664
  })
1454
1665
 
1455
- test('.flatMap(i => [i.a, i.b]) emits bf->flat_map_tuple with leaf specs', () => {
1456
- expect(emitFlatMap('rows.flatMap(i => [i.a, i.b]).join(" ")')).toContain(`bf->flat_map_tuple($rows, ['field', 'a'], ['field', 'b'])`)
1666
+ test('.flatMap(i => i) emits bf->flat_map_eval (self/identifier projection)', () => {
1667
+ const t = emitFlatMap('rows.flatMap(i => i).join(" ")')
1668
+ expect(t).toContain(`bf->flat_map_eval($rows,`)
1669
+ expect(t).toContain(`"kind":"identifier","name":"i"`)
1457
1670
  })
1458
1671
 
1459
- test('tuple self + field leaves', () => {
1460
- expect(emitFlatMap('rows.flatMap(i => [i, i.a]).join(" ")')).toContain(`bf->flat_map_tuple($rows, ['self', ''], ['field', 'a'])`)
1672
+ test('.flatMap(i => [i.a, i.b]) emits bf->flat_map_eval over an array-literal projection', () => {
1673
+ const t = emitFlatMap('rows.flatMap(i => [i.a, i.b]).join(" ")')
1674
+ expect(t).toContain(`bf->flat_map_eval($rows,`)
1675
+ expect(t).toContain(`"kind":"array-literal"`)
1461
1676
  })
1462
1677
 
1463
1678
  test('field-projection flatMap as a loop base lowers (no BF101)', () => {
@@ -1470,7 +1685,7 @@ export function C() {
1470
1685
  }`, a)
1471
1686
  const template = a.generate(ir).template ?? ''
1472
1687
  expect((a.errors ?? []).filter(e => e.code === 'BF101')).toEqual([])
1473
- expect(template).toContain(`bf->flat_map($items, 'field', 'tags')`)
1688
+ expect(template).toContain(`bf->flat_map_eval($items,`)
1474
1689
  })
1475
1690
  })
1476
1691
 
@@ -1537,9 +1752,11 @@ export function C() {
1537
1752
  // the no-initial-value form stays refused — JS throws on an empty
1538
1753
  // array there, which a template can't mirror.
1539
1754
  { name: 'reduce (no init)', expr: `items().reduce((a, b) => a + b.n)`, badEmit: '->{reduce}' },
1540
- // Self / field / field-tuple `.flatMap` now lowers (#1448 Tier C); a
1541
- // tuple with a non-leaf element (here a string literal) stays refused.
1542
- { name: 'flatMap (literal element)', expr: `items().flatMap(i => [i.name, "x"])`, badEmit: '->{flatMap}' },
1755
+ // #2018 P5: an array-literal projection with a literal element
1756
+ // (`i => [i.name, "x"]`) now lowers through the runtime evaluator
1757
+ // (`bf->flat_map_eval`) rather than refusing the structured-tuple
1758
+ // leaf restriction is gone. (Cross-adapter isomorphic with the Go
1759
+ // adapter's `bf_flat_map_eval`.) Pinned positively below.
1543
1760
  // Lowered methods whose MEANINGFUL extra argument isn't lowered yet
1544
1761
  // (#1448): the `fromIndex` of `.includes`/`.indexOf`/`.lastIndexOf`
1545
1762
  // and the variadic `.concat`. The parser refuses these (silently