@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.
- package/dist/adapter/analysis/component-tree.d.ts +30 -0
- package/dist/adapter/analysis/component-tree.d.ts.map +1 -0
- package/dist/adapter/emit-context.d.ts +96 -0
- package/dist/adapter/emit-context.d.ts.map +1 -0
- package/dist/adapter/expr/array-method.d.ts +85 -0
- package/dist/adapter/expr/array-method.d.ts.map +1 -0
- package/dist/adapter/expr/emitters.d.ts +78 -0
- package/dist/adapter/expr/emitters.d.ts.map +1 -0
- package/dist/adapter/expr/operand.d.ts +34 -0
- package/dist/adapter/expr/operand.d.ts.map +1 -0
- package/dist/adapter/index.js +1482 -1339
- package/dist/adapter/lib/constants.d.ts +27 -0
- package/dist/adapter/lib/constants.d.ts.map +1 -0
- package/dist/adapter/lib/ir-scope.d.ts +33 -0
- package/dist/adapter/lib/ir-scope.d.ts.map +1 -0
- package/dist/adapter/lib/perl-naming.d.ts +29 -0
- package/dist/adapter/lib/perl-naming.d.ts.map +1 -0
- package/dist/adapter/lib/types.d.ts +28 -0
- package/dist/adapter/lib/types.d.ts.map +1 -0
- package/dist/adapter/memo/seed.d.ts +34 -0
- package/dist/adapter/memo/seed.d.ts.map +1 -0
- package/dist/adapter/mojo-adapter.d.ts +46 -104
- package/dist/adapter/mojo-adapter.d.ts.map +1 -1
- package/dist/adapter/props/prop-classes.d.ts +48 -0
- package/dist/adapter/props/prop-classes.d.ts.map +1 -0
- package/dist/adapter/spread/spread-codegen.d.ts +64 -0
- package/dist/adapter/spread/spread-codegen.d.ts.map +1 -0
- package/dist/adapter/value/parsed-literal.d.ts +26 -0
- package/dist/adapter/value/parsed-literal.d.ts.map +1 -0
- package/dist/build.js +1482 -1339
- package/dist/index.js +1482 -1339
- package/dist/test-render.d.ts.map +1 -1
- package/lib/BarefootJS/Backend/Mojo.pm +1 -1
- package/lib/Mojolicious/Plugin/BarefootJS/DevReload.pm +1 -1
- package/lib/Mojolicious/Plugin/BarefootJS.pm +1 -1
- package/package.json +3 -3
- package/src/__tests__/mojo-adapter.test.ts +286 -69
- package/src/__tests__/query-href.test.ts +94 -0
- package/src/adapter/analysis/component-tree.ts +128 -0
- package/src/adapter/emit-context.ts +107 -0
- package/src/adapter/expr/array-method.ts +429 -0
- package/src/adapter/expr/emitters.ts +639 -0
- package/src/adapter/expr/operand.ts +55 -0
- package/src/adapter/lib/constants.ts +39 -0
- package/src/adapter/lib/ir-scope.ts +57 -0
- package/src/adapter/lib/perl-naming.ts +36 -0
- package/src/adapter/lib/types.ts +31 -0
- package/src/adapter/memo/seed.ts +73 -0
- package/src/adapter/mojo-adapter.ts +248 -1485
- package/src/adapter/props/prop-classes.ts +87 -0
- package/src/adapter/spread/spread-codegen.ts +181 -0
- package/src/adapter/value/parsed-literal.ts +34 -0
- 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;
|
|
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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@barefootjs/mojolicious",
|
|
3
|
-
"version": "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.
|
|
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.
|
|
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 `
|
|
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
|
-
//
|
|
147
|
-
//
|
|
148
|
-
//
|
|
149
|
-
//
|
|
150
|
-
//
|
|
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 {
|
|
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
|
|
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
|
|
264
|
+
test('matches an aliased env-signal getter (`const [sp] = createSearchParams()`) and emits canonical $searchParams', () => {
|
|
255
265
|
const { template } = compileAndGenerate(`
|
|
256
|
-
import {
|
|
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
|
-
//
|
|
724
|
-
//
|
|
725
|
-
//
|
|
726
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
981
|
-
//
|
|
982
|
-
//
|
|
983
|
-
//
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
['
|
|
987
|
-
['
|
|
988
|
-
['
|
|
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
|
|
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.
|
|
1359
|
-
//
|
|
1360
|
-
//
|
|
1361
|
-
//
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
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):
|
|
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
|
|
1369
|
-
|
|
1370
|
-
{ fixture:
|
|
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.
|
|
1379
|
-
//
|
|
1380
|
-
//
|
|
1381
|
-
//
|
|
1382
|
-
//
|
|
1383
|
-
//
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
{ fixture:
|
|
1387
|
-
{ fixture:
|
|
1388
|
-
{ fixture:
|
|
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
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
test('.flatMap(i => i) emits the
|
|
1452
|
-
|
|
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 =>
|
|
1456
|
-
|
|
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('
|
|
1460
|
-
|
|
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->
|
|
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
|
-
//
|
|
1541
|
-
//
|
|
1542
|
-
|
|
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
|