@barefootjs/mojolicious 0.15.2 → 0.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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 +75 -0
- package/dist/adapter/expr/array-method.d.ts.map +1 -0
- package/dist/adapter/expr/emitters.d.ts +77 -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 +1490 -1334
- 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 +40 -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 +43 -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 +1490 -1334
- package/dist/index.js +1490 -1334
- 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 +163 -62
- 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 +408 -0
- package/src/adapter/expr/emitters.ts +607 -0
- package/src/adapter/expr/operand.ts +55 -0
- package/src/adapter/lib/constants.ts +39 -0
- package/src/adapter/lib/ir-scope.ts +70 -0
- package/src/adapter/lib/perl-naming.ts +36 -0
- package/src/adapter/lib/types.ts +31 -0
- package/src/adapter/memo/seed.ts +126 -0
- package/src/adapter/mojo-adapter.ts +230 -1476
- 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.0",
|
|
4
4
|
"description": "Mojolicious EP template adapter for BarefootJS - generates .html.ep files from IR",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
"directory": "packages/adapter-mojolicious"
|
|
53
53
|
},
|
|
54
54
|
"dependencies": {
|
|
55
|
-
"@barefootjs/shared": "0.
|
|
55
|
+
"@barefootjs/shared": "0.17.0"
|
|
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.0"
|
|
64
64
|
}
|
|
65
65
|
}
|
|
@@ -78,8 +78,12 @@ runAdapterConformanceTests({
|
|
|
78
78
|
// #1467 Phase 2e: `data-table` is no longer pinned here either — it
|
|
79
79
|
// compiles clean (`selected()[index]` → `index-access`,
|
|
80
80
|
// `.toFixed(2)` → `bf->to_fixed`, `/* @client */` memo SSR-folded)
|
|
81
|
-
// and
|
|
82
|
-
// divergence
|
|
81
|
+
// and renders to Hono parity on real Mojolicious. The keyed-loop
|
|
82
|
+
// scope-ID divergence (#1896) was fixed by the body-children
|
|
83
|
+
// `inLoop` reset (loop-item children get `_bf_slot`); data-table is
|
|
84
|
+
// off `skipJsx` entirely and only kept in `skipMarkerConformance`
|
|
85
|
+
// below for the shared `/* @client */` keyed-map slot-id elision
|
|
86
|
+
// contract (same as `todo-app`), not a render or BF101 gap.
|
|
83
87
|
// #1443: `[a, b].filter(Boolean).join(' ')` (the registry Slot's
|
|
84
88
|
// shape) now lowers to `join(' ', @{[grep { $_ } @{[$a, $b]}]})`.
|
|
85
89
|
// No BF101 expected — pinned positively via the
|
|
@@ -118,8 +122,8 @@ runAdapterConformanceTests({
|
|
|
118
122
|
// (Go) and new `bf->trim` helper (Mojo) handle the strip
|
|
119
123
|
// (#1448 Tier A ninth PR, closing out Tier A).
|
|
120
124
|
// `.find` / `.findIndex` / `.findLast` / `.findLastIndex` are no longer
|
|
121
|
-
// pinned — the Mojo `
|
|
122
|
-
// `bf->find` / `find_index` / `find_last` / `find_last_index` helpers
|
|
125
|
+
// pinned — the Mojo `callbackMethod` predicate arm now lowers them to the
|
|
126
|
+
// runtime `bf->find` / `find_index` / `find_last` / `find_last_index` helpers
|
|
123
127
|
// (per-element coderef predicate), matching Xslate. `.join` was never
|
|
124
128
|
// pinned (handled by `renderArrayMethod`'s `case 'join'`).
|
|
125
129
|
},
|
|
@@ -235,8 +239,9 @@ describe('MojoAdapter - searchParams() env-signal lowering (#1922)', () => {
|
|
|
235
239
|
// generic hash deref `$searchParams->{get}` (which drops the arg).
|
|
236
240
|
test('lowers searchParams().get(k) to a method call on $searchParams', () => {
|
|
237
241
|
const { template } = compileAndGenerate(`
|
|
238
|
-
import {
|
|
242
|
+
import { createSearchParams } from '@barefootjs/client'
|
|
239
243
|
function SortLabel() {
|
|
244
|
+
const [searchParams] = createSearchParams()
|
|
240
245
|
return <p>{searchParams().get('sort') ?? 'none'}</p>
|
|
241
246
|
}
|
|
242
247
|
`)
|
|
@@ -244,13 +249,14 @@ function SortLabel() {
|
|
|
244
249
|
expect(template).not.toContain('$searchParams->{get}')
|
|
245
250
|
})
|
|
246
251
|
|
|
247
|
-
// An aliased
|
|
248
|
-
// expression reads `sp()`, but it still lowers to the canonical
|
|
252
|
+
// An aliased destructured getter binds the env signal to a different local
|
|
253
|
+
// name; the expression reads `sp()`, but it still lowers to the canonical
|
|
249
254
|
// `$searchParams` reader (the harness/plugin seed that fixed var).
|
|
250
|
-
test('matches an aliased
|
|
255
|
+
test('matches an aliased env-signal getter (`const [sp] = createSearchParams()`) and emits canonical $searchParams', () => {
|
|
251
256
|
const { template } = compileAndGenerate(`
|
|
252
|
-
import {
|
|
257
|
+
import { createSearchParams } from '@barefootjs/client'
|
|
253
258
|
function SortLabel() {
|
|
259
|
+
const [sp] = createSearchParams()
|
|
254
260
|
return <p>{sp().get('sort') ?? 'none'}</p>
|
|
255
261
|
}
|
|
256
262
|
`)
|
|
@@ -716,10 +722,12 @@ export function C() {
|
|
|
716
722
|
// empty array there, which a template can't mirror.
|
|
717
723
|
{ name: 'reduce (no init)', body: `<div>{items().reduce((s, x) => s + x)}</div>`, needle: '.reduce(' },
|
|
718
724
|
{ name: 'forEach', body: `<ul>{items().forEach(x => x)}</ul>`, needle: '.forEach(' },
|
|
719
|
-
//
|
|
720
|
-
//
|
|
721
|
-
//
|
|
722
|
-
|
|
725
|
+
// #2018 P5: an array-literal projection with a literal element
|
|
726
|
+
// (`x => [x.tag, "x"]`) now lowers through the runtime evaluator
|
|
727
|
+
// (`bf->flat_map_eval`) — the structured-tuple restriction that
|
|
728
|
+
// refused a non-`self`/`field` leaf is gone. See the positive
|
|
729
|
+
// flat_map_eval pins below. The remaining BF101 flatMap surface
|
|
730
|
+
// (a projection the evaluator can't serialize) has no example here.
|
|
723
731
|
]
|
|
724
732
|
|
|
725
733
|
for (const { name, body, needle } of cases) {
|
|
@@ -756,7 +764,11 @@ export function C() {
|
|
|
756
764
|
}`, 'C.tsx', { adapter })
|
|
757
765
|
expect(result.errors?.filter(e => e.code === 'BF101') ?? []).toEqual([])
|
|
758
766
|
const template = result.files.find(f => f.path.endsWith('.html.ep'))?.content ?? ''
|
|
759
|
-
|
|
767
|
+
// #2018 P2: the `.filter().map()` loop hoists over the filtered array,
|
|
768
|
+
// now produced by the evaluator (`bf->filter_eval`). (Hoisting the call
|
|
769
|
+
// into a single `my` var is a P3 optimization.)
|
|
770
|
+
expect(template).toContain('bf->filter_eval($items,')
|
|
771
|
+
expect(template).toContain('"property":"done"')
|
|
760
772
|
})
|
|
761
773
|
|
|
762
774
|
test('lowers nested .filter(...).length > 0 in outer filter predicate (#1443 PR4)', () => {
|
|
@@ -794,7 +806,11 @@ export function C() {
|
|
|
794
806
|
}`, 'C.tsx', { adapter })
|
|
795
807
|
expect(result.errors?.filter(e => e.code === 'BF101') ?? []).toEqual([])
|
|
796
808
|
const template = result.files.find(f => f.path.endsWith('.html.ep'))?.content ?? ''
|
|
797
|
-
|
|
809
|
+
// #2018 P2: the `.filter().map()` loop hoists over the filtered array,
|
|
810
|
+
// now produced by the evaluator (`bf->filter_eval`). (Hoisting the call
|
|
811
|
+
// into a single `my` var is a P3 optimization.)
|
|
812
|
+
expect(template).toContain('bf->filter_eval($items,')
|
|
813
|
+
expect(template).toContain('"property":"done"')
|
|
798
814
|
})
|
|
799
815
|
|
|
800
816
|
test('lowers the registry Slot\'s [a, b].filter(Boolean).join(\' \') chain (#1443)', () => {
|
|
@@ -973,17 +989,18 @@ export { A }`, 'A.tsx', { adapter })
|
|
|
973
989
|
expect(template).not.toContain('$bf->trim')
|
|
974
990
|
})
|
|
975
991
|
|
|
976
|
-
test('lowers .find / .findIndex / .findLast / .findLastIndex via
|
|
977
|
-
//
|
|
978
|
-
//
|
|
979
|
-
//
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
['
|
|
983
|
-
['
|
|
984
|
-
['
|
|
992
|
+
test('lowers .find / .findIndex / .findLast / .findLastIndex via the evaluator', () => {
|
|
993
|
+
// #2018 P2: the pure predicate `x => x === 'b'` serializes and lowers
|
|
994
|
+
// through the evaluator. find / findIndex share `bf->find_eval` /
|
|
995
|
+
// `bf->find_index_eval` with findLast / findLastIndex, distinguished by
|
|
996
|
+
// the `forward` flag (1 = forward, 0 = backward).
|
|
997
|
+
const cases: Array<[string, string, number]> = [
|
|
998
|
+
['find', 'find_eval', 1],
|
|
999
|
+
['findIndex', 'find_index_eval', 1],
|
|
1000
|
+
['findLast', 'find_eval', 0],
|
|
1001
|
+
['findLastIndex', 'find_index_eval', 0],
|
|
985
1002
|
]
|
|
986
|
-
for (const [js, helper] of cases) {
|
|
1003
|
+
for (const [js, helper, fwd] of cases) {
|
|
987
1004
|
const adapter = new MojoAdapter()
|
|
988
1005
|
const result = compileJSX(`function A({ items }: { items: string[] }) {
|
|
989
1006
|
return <div>{items.${js}(x => x === 'b')}</div>
|
|
@@ -991,11 +1008,45 @@ export { A }`, 'A.tsx', { adapter })
|
|
|
991
1008
|
export { A }`, 'A.tsx', { adapter })
|
|
992
1009
|
expect(result.errors?.filter(e => e.code === 'BF101') ?? []).toEqual([])
|
|
993
1010
|
const template = result.files.find(f => f.path.endsWith('.html.ep'))?.content ?? ''
|
|
994
|
-
expect(template).toContain(`bf->${helper}($items
|
|
1011
|
+
expect(template).toContain(`bf->${helper}($items,`)
|
|
1012
|
+
expect(template).toContain(`'x', ${fwd}, {})`)
|
|
995
1013
|
expect(template).not.toContain(`$bf->${helper}`)
|
|
996
1014
|
}
|
|
997
1015
|
})
|
|
998
1016
|
|
|
1017
|
+
test('lowers .every / .some via the evaluator, with grep fallback for a method-call predicate', () => {
|
|
1018
|
+
// #2018 P2: a pure predicate routes `.every` / `.some` through
|
|
1019
|
+
// `bf->every_eval` / `bf->some_eval`; a method-call predicate the
|
|
1020
|
+
// evaluator can't model (`serializeParsedExpr` → null) falls back to the
|
|
1021
|
+
// inline `grep` form.
|
|
1022
|
+
const evalCases: Array<[string, string]> = [
|
|
1023
|
+
['every', 'every_eval'],
|
|
1024
|
+
['some', 'some_eval'],
|
|
1025
|
+
]
|
|
1026
|
+
for (const [js, helper] of evalCases) {
|
|
1027
|
+
const adapter = new MojoAdapter()
|
|
1028
|
+
const result = compileJSX(`function A({ items }: { items: { done: boolean }[] }) {
|
|
1029
|
+
return <div>{items.${js}(x => x.done) ? 'y' : 'n'}</div>
|
|
1030
|
+
}
|
|
1031
|
+
export { A }`, 'A.tsx', { adapter })
|
|
1032
|
+
expect(result.errors?.filter(e => e.code === 'BF101') ?? []).toEqual([])
|
|
1033
|
+
const template = result.files.find(f => f.path.endsWith('.html.ep'))?.content ?? ''
|
|
1034
|
+
expect(template).toContain(`bf->${helper}($items,`)
|
|
1035
|
+
expect(template).toContain('"property":"done"')
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
// Fallback: a method-call predicate (`x => x.name.includes('a')`) is
|
|
1039
|
+
// outside the evaluator surface, so `.every` keeps the inline grep form.
|
|
1040
|
+
const adapter = new MojoAdapter()
|
|
1041
|
+
const fb = compileJSX(`function A({ items }: { items: { name: string }[] }) {
|
|
1042
|
+
return <div>{items.every(x => x.name.includes('a')) ? 'y' : 'n'}</div>
|
|
1043
|
+
}
|
|
1044
|
+
export { A }`, 'A.tsx', { adapter })
|
|
1045
|
+
const fbTemplate = fb.files.find(f => f.path.endsWith('.html.ep'))?.content ?? ''
|
|
1046
|
+
expect(fbTemplate).toContain('grep {')
|
|
1047
|
+
expect(fbTemplate).not.toContain('every_eval')
|
|
1048
|
+
})
|
|
1049
|
+
|
|
999
1050
|
test('lowers .reverse().join(\' \') via bf->reverse + join (#1448 Tier A)', () => {
|
|
1000
1051
|
// SSR templates render a snapshot, so `.reverse` and
|
|
1001
1052
|
// `.toReversed` share a Mojo lowering — both return a new
|
|
@@ -1351,19 +1402,26 @@ describe('MojoAdapter - #1448 Tier A/B fixture-driven lowering pins', () => {
|
|
|
1351
1402
|
// #1448 Tier B — string → string, padded to a target width.
|
|
1352
1403
|
{ fixture: stringPadStartFixture, expect: `bf->pad_start($value, 5, '0')` },
|
|
1353
1404
|
{ fixture: stringPadEndFixture, expect: `bf->pad_end($value, 5, '.')` },
|
|
1354
|
-
// #1448 Tier B — sort / toSorted.
|
|
1355
|
-
//
|
|
1356
|
-
//
|
|
1357
|
-
//
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1405
|
+
// #1448 Tier B — sort / toSorted. EXPR2 migration (#2018): both a
|
|
1406
|
+
// STANDALONE `.sort(cmp)` value call AND the `.sort().map()` loop-hoist
|
|
1407
|
+
// (#2018 P3) serialize the comparator body and emit `bf->sort_eval(...)`
|
|
1408
|
+
// (JSON body + param names + captured env). `localeCompare` comparators
|
|
1409
|
+
// fall back to the structured `bf->sort` — `serializeParsedExpr` refuses
|
|
1410
|
+
// them. (Loop-hoist field cases pin only the helper + receiver; their
|
|
1411
|
+
// comparator JSON is verified by the render conformance.)
|
|
1412
|
+
{ fixture: arraySortFieldAscFixture, expect: `bf->sort_eval($items,` },
|
|
1413
|
+
{ fixture: arraySortFieldDescFixture, expect: `bf->sort_eval($items,` },
|
|
1414
|
+
{ fixture: arraySortPrimitiveFixture, expect: `bf->sort_eval($nums, '{"kind":"binary","op":"-","left":{"kind":"identifier","name":"a"},"right":{"kind":"identifier","name":"b"}}', 'a', 'b', {})` },
|
|
1415
|
+
// localeCompare → outside the evaluator surface → legacy `bf->sort`.
|
|
1361
1416
|
{ fixture: arraySortLocaleFixture, expect: `bf->sort($names, { keys => [{ key_kind => 'self', compare_type => 'string', direction => 'asc' }] })` },
|
|
1362
|
-
// Multi-key (`||`-chain):
|
|
1417
|
+
// Multi-key (`||`-chain): the second key is a `localeCompare`, so the
|
|
1418
|
+
// whole comparator falls back to the structured `bf->sort` (one hash
|
|
1419
|
+
// per comparison key, in order).
|
|
1363
1420
|
{ 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' }] })` },
|
|
1364
|
-
// Relational-ternary comparator
|
|
1365
|
-
|
|
1366
|
-
{ fixture:
|
|
1421
|
+
// Relational-ternary comparator — a pure body, so the loop-hoist now
|
|
1422
|
+
// serializes it through `bf->sort_eval` like the other field sorts.
|
|
1423
|
+
{ fixture: arraySortTernaryFixture, expect: `bf->sort_eval($items,` },
|
|
1424
|
+
{ fixture: arrayToSortedFixture, expect: `bf->sort_eval($nums, '{"kind":"binary","op":"-","left":{"kind":"identifier","name":"a"},"right":{"kind":"identifier","name":"b"}}', 'a', 'b', {})` },
|
|
1367
1425
|
// #1448 Tier B — iteration shapes. These are loop-level patterns.
|
|
1368
1426
|
// .entries() → for loop with both $i index var and $v value var
|
|
1369
1427
|
{ fixture: arrayEntriesFixture, expect: '% my $v = $items->[$i];' },
|
|
@@ -1371,17 +1429,19 @@ describe('MojoAdapter - #1448 Tier A/B fixture-driven lowering pins', () => {
|
|
|
1371
1429
|
{ fixture: arrayKeysFixture, expect: '% for my $k (0..$#{$items})' },
|
|
1372
1430
|
// .values() → standard for loop (same as plain .map())
|
|
1373
1431
|
{ fixture: arrayValuesFixture, expect: '% my $v = $items->[$_i];' },
|
|
1374
|
-
// #1448 Tier C — .reduce(fn, init) arithmetic fold.
|
|
1375
|
-
//
|
|
1376
|
-
//
|
|
1377
|
-
//
|
|
1378
|
-
//
|
|
1379
|
-
//
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
{ fixture:
|
|
1383
|
-
{ fixture:
|
|
1384
|
-
{ fixture:
|
|
1432
|
+
// #1448 Tier C — .reduce(fn, init) arithmetic fold. EXPR2 migration
|
|
1433
|
+
// (#2018): the reducer body is serialized to ParsedExpr JSON and
|
|
1434
|
+
// folded by `bf->reduce_eval(json, accName, itemName, init, direction,
|
|
1435
|
+
// env)` — the runtime evaluator subsumes the old `+`/`*` catalogue.
|
|
1436
|
+
// A numeric seed passes through bare (`0` / `1`); a concat seed as a
|
|
1437
|
+
// single-quoted string (`''`). Each shape exercises one arm: field-
|
|
1438
|
+
// numeric sum, self-numeric sum, string-concat fold, the product
|
|
1439
|
+
// (`*`) operator, and the right-to-left `direction` of reduceRight.
|
|
1440
|
+
{ 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', {})` },
|
|
1441
|
+
{ fixture: reduceSumSelfFixture, expect: `bf->reduce_eval($nums, '{"kind":"binary","op":"+","left":{"kind":"identifier","name":"a"},"right":{"kind":"identifier","name":"b"}}', 'a', 'b', 0, 'left', {})` },
|
|
1442
|
+
{ 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', {})` },
|
|
1443
|
+
{ 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', {})` },
|
|
1444
|
+
{ 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', {})` },
|
|
1385
1445
|
]
|
|
1386
1446
|
|
|
1387
1447
|
for (const { fixture, expect: expectedHelper } of cases) {
|
|
@@ -1440,20 +1500,26 @@ export { C }
|
|
|
1440
1500
|
return a.generate(ir).template ?? ''
|
|
1441
1501
|
}
|
|
1442
1502
|
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
test('.flatMap(i => i) emits the
|
|
1448
|
-
|
|
1503
|
+
// #2018 P3: `.flatMap(proj)` lowers through the evaluator — the projection
|
|
1504
|
+
// body serializes to ParsedExpr JSON and `bf->flat_map_eval` flattens the
|
|
1505
|
+
// results one level. (In Mojo the JSON rides in a single-quoted Perl string,
|
|
1506
|
+
// so its double quotes are literal and assertable.)
|
|
1507
|
+
test('.flatMap(i => i.field) emits bf->flat_map_eval with the field projection', () => {
|
|
1508
|
+
const t = emitFlatMap('rows.flatMap(i => i.tags).join(" ")')
|
|
1509
|
+
expect(t).toContain(`bf->flat_map_eval($rows,`)
|
|
1510
|
+
expect(t).toContain(`"property":"tags"`)
|
|
1449
1511
|
})
|
|
1450
1512
|
|
|
1451
|
-
test('.flatMap(i =>
|
|
1452
|
-
|
|
1513
|
+
test('.flatMap(i => i) emits bf->flat_map_eval (self/identifier projection)', () => {
|
|
1514
|
+
const t = emitFlatMap('rows.flatMap(i => i).join(" ")')
|
|
1515
|
+
expect(t).toContain(`bf->flat_map_eval($rows,`)
|
|
1516
|
+
expect(t).toContain(`"kind":"identifier","name":"i"`)
|
|
1453
1517
|
})
|
|
1454
1518
|
|
|
1455
|
-
test('
|
|
1456
|
-
|
|
1519
|
+
test('.flatMap(i => [i.a, i.b]) emits bf->flat_map_eval over an array-literal projection', () => {
|
|
1520
|
+
const t = emitFlatMap('rows.flatMap(i => [i.a, i.b]).join(" ")')
|
|
1521
|
+
expect(t).toContain(`bf->flat_map_eval($rows,`)
|
|
1522
|
+
expect(t).toContain(`"kind":"array-literal"`)
|
|
1457
1523
|
})
|
|
1458
1524
|
|
|
1459
1525
|
test('field-projection flatMap as a loop base lowers (no BF101)', () => {
|
|
@@ -1466,7 +1532,7 @@ export function C() {
|
|
|
1466
1532
|
}`, a)
|
|
1467
1533
|
const template = a.generate(ir).template ?? ''
|
|
1468
1534
|
expect((a.errors ?? []).filter(e => e.code === 'BF101')).toEqual([])
|
|
1469
|
-
expect(template).toContain(`bf->
|
|
1535
|
+
expect(template).toContain(`bf->flat_map_eval($items,`)
|
|
1470
1536
|
})
|
|
1471
1537
|
})
|
|
1472
1538
|
|
|
@@ -1533,9 +1599,11 @@ export function C() {
|
|
|
1533
1599
|
// the no-initial-value form stays refused — JS throws on an empty
|
|
1534
1600
|
// array there, which a template can't mirror.
|
|
1535
1601
|
{ name: 'reduce (no init)', expr: `items().reduce((a, b) => a + b.n)`, badEmit: '->{reduce}' },
|
|
1536
|
-
//
|
|
1537
|
-
//
|
|
1538
|
-
|
|
1602
|
+
// #2018 P5: an array-literal projection with a literal element
|
|
1603
|
+
// (`i => [i.name, "x"]`) now lowers through the runtime evaluator
|
|
1604
|
+
// (`bf->flat_map_eval`) rather than refusing — the structured-tuple
|
|
1605
|
+
// leaf restriction is gone. (Cross-adapter isomorphic with the Go
|
|
1606
|
+
// adapter's `bf_flat_map_eval`.) Pinned positively below.
|
|
1539
1607
|
// Lowered methods whose MEANINGFUL extra argument isn't lowered yet
|
|
1540
1608
|
// (#1448): the `fromIndex` of `.includes`/`.indexOf`/`.lastIndexOf`
|
|
1541
1609
|
// and the variadic `.concat`. The parser refuses these (silently
|
|
@@ -1748,3 +1816,36 @@ export function C() {
|
|
|
1748
1816
|
expect(template).not.toContain('$JSON->{stringify}')
|
|
1749
1817
|
})
|
|
1750
1818
|
})
|
|
1819
|
+
|
|
1820
|
+
// =============================================================================
|
|
1821
|
+
// #1966 — `/* @client */` defers ATTRIBUTE bindings (not just child/text)
|
|
1822
|
+
// =============================================================================
|
|
1823
|
+
//
|
|
1824
|
+
// `renderAttributes` skips SSR emission for `attr.clientOnly`, so a
|
|
1825
|
+
// deferred attribute predicate is omitted from the Mojo template (and the
|
|
1826
|
+
// unsupported-expression lowering is never reached → no BF101/BF102). The
|
|
1827
|
+
// client runtime sets the attribute on hydrate. Mirrors the Go pins.
|
|
1828
|
+
describe('MojoAdapter - #1966 @client defers attribute bindings', () => {
|
|
1829
|
+
function compileAttr(attrExpr: string) {
|
|
1830
|
+
const adapter = new MojoAdapter()
|
|
1831
|
+
const ir = compileToIR(`
|
|
1832
|
+
"use client"
|
|
1833
|
+
import { createSignal } from "@barefootjs/client"
|
|
1834
|
+
export function C() {
|
|
1835
|
+
const [sel] = createSignal(0)
|
|
1836
|
+
const pred = (n: number) => sel() === n
|
|
1837
|
+
return <div data-x={${attrExpr}}>hi</div>
|
|
1838
|
+
}
|
|
1839
|
+
`, adapter)
|
|
1840
|
+
const template = adapter.generate(ir).template ?? ''
|
|
1841
|
+
const errors = (adapter as unknown as { errors: { code: string }[] }).errors ?? []
|
|
1842
|
+
return { errors, template }
|
|
1843
|
+
}
|
|
1844
|
+
|
|
1845
|
+
test('bare emits data-x; @client omits it from SSR', () => {
|
|
1846
|
+
expect(compileAttr('pred(1)').template).toContain('data-x')
|
|
1847
|
+
const deferred = compileAttr('/* @client */ pred(1)')
|
|
1848
|
+
expect(deferred.errors).toEqual([])
|
|
1849
|
+
expect(deferred.template).not.toContain('data-x')
|
|
1850
|
+
})
|
|
1851
|
+
})
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `queryHref(base, { … })` → `bf->query(...)` lowering for the Mojolicious
|
|
3
|
+
* adapter (#2042). Parity with the go-template `bf_query` lowering: the call +
|
|
4
|
+
* object literal are structured IR, so it lowers directly. The `bf->query`
|
|
5
|
+
* runtime helper (BarefootJS.pm) includes a pair iff its guard is truthy AND its
|
|
6
|
+
* value is a non-empty string, so a plain `key: v` passes guard `1` and a
|
|
7
|
+
* conditional `key: cond ? v : undefined` passes the lowered condition.
|
|
8
|
+
*/
|
|
9
|
+
import { describe, test, expect } from 'bun:test'
|
|
10
|
+
import { compileJSX, type ComponentIR } from '@barefootjs/jsx'
|
|
11
|
+
import { MojoAdapter } from '../adapter/mojo-adapter'
|
|
12
|
+
|
|
13
|
+
function template(src: string): string {
|
|
14
|
+
const a = new MojoAdapter()
|
|
15
|
+
const r = compileJSX(src.trimStart(), 'T.tsx', { adapter: a, outputIR: true })
|
|
16
|
+
const ir = JSON.parse(r.files.find(f => f.type === 'ir')!.content) as ComponentIR
|
|
17
|
+
return a.generate(ir).template
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
describe('queryHref → bf->query (Mojo, #2042)', () => {
|
|
21
|
+
test('a plain value passes guard 1', () => {
|
|
22
|
+
const t = template(`
|
|
23
|
+
'use client'
|
|
24
|
+
import { queryHref } from '@barefootjs/client'
|
|
25
|
+
export function P(props: { base: string; tag: string }) {
|
|
26
|
+
return <a href={queryHref(props.base, { tag: props.tag })}>x</a>
|
|
27
|
+
}
|
|
28
|
+
`)
|
|
29
|
+
expect(t).toContain("bf->query($base, 1, 'tag', $tag)")
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
test('a conditional include passes the lowered condition as the guard', () => {
|
|
33
|
+
const t = template(`
|
|
34
|
+
'use client'
|
|
35
|
+
import { queryHref } from '@barefootjs/client'
|
|
36
|
+
export function P(props: { base: string; sort: string; tag: string }) {
|
|
37
|
+
return <a href={queryHref(props.base, { sort: props.sort !== 'date' ? props.sort : undefined, tag: props.tag })}>x</a>
|
|
38
|
+
}
|
|
39
|
+
`)
|
|
40
|
+
expect(t).toContain("bf->query($base, ($sort ne 'date'), 'sort', $sort, 1, 'tag', $tag)")
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
// A bare-value guard (`flag ? v : undefined`) is JS *string* truthiness — `'0'`
|
|
44
|
+
// is a truthy string in JS but false under Perl's `unless`. The lowering must
|
|
45
|
+
// normalise it to a non-empty-string test so SSR matches the client / go (where
|
|
46
|
+
// `lowerUrlGuard` emits `ne <value> ""`), not pass the bare value as the guard.
|
|
47
|
+
test('a bare-value guard is normalised to a non-empty-string test', () => {
|
|
48
|
+
const t = template(`
|
|
49
|
+
'use client'
|
|
50
|
+
import { queryHref } from '@barefootjs/client'
|
|
51
|
+
export function P(props: { base: string; flag: string; val: string }) {
|
|
52
|
+
return <a href={queryHref(props.base, { q: props.flag ? props.val : undefined })}>x</a>
|
|
53
|
+
}
|
|
54
|
+
`)
|
|
55
|
+
expect(t).toContain("bf->query($base, ($flag ne ''), 'q', $val)")
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
// An array value (`{ tag: props.tags }`) lowers to the bare slice expression;
|
|
59
|
+
// the shared Perl `query` helper detects the arrayref at runtime and appends
|
|
60
|
+
// one pair per non-empty member (#2048). No adapter-side change beyond passing
|
|
61
|
+
// the value through.
|
|
62
|
+
test('an array value passes the slice expression for the helper to append', () => {
|
|
63
|
+
const t = template(`
|
|
64
|
+
'use client'
|
|
65
|
+
import { queryHref } from '@barefootjs/client'
|
|
66
|
+
export function P(props: { base: string; tags: string[] }) {
|
|
67
|
+
return <a href={queryHref(props.base, { tag: props.tags })}>x</a>
|
|
68
|
+
}
|
|
69
|
+
`)
|
|
70
|
+
expect(t).toContain("bf->query($base, 1, 'tag', $tags)")
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
test('an aliased import is recognised', () => {
|
|
74
|
+
const t = template(`
|
|
75
|
+
'use client'
|
|
76
|
+
import { queryHref as qh } from '@barefootjs/client'
|
|
77
|
+
export function P(props: { base: string; tag: string }) {
|
|
78
|
+
return <a href={qh(props.base, { tag: props.tag })}>x</a>
|
|
79
|
+
}
|
|
80
|
+
`)
|
|
81
|
+
expect(t).toContain("bf->query($base, 1, 'tag', $tag)")
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
test('a dynamic (non-literal) params object falls back (no bf->query)', () => {
|
|
85
|
+
const t = template(`
|
|
86
|
+
'use client'
|
|
87
|
+
import { queryHref } from '@barefootjs/client'
|
|
88
|
+
export function P(props: { base: string; q: Record<string, string> }) {
|
|
89
|
+
return <a href={queryHref(props.base, props.q)}>x</a>
|
|
90
|
+
}
|
|
91
|
+
`)
|
|
92
|
+
expect(t).not.toContain('bf->query')
|
|
93
|
+
})
|
|
94
|
+
})
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Component-tree analysis for the Mojolicious EP template adapter.
|
|
3
|
+
*
|
|
4
|
+
* Extracted from `mojo-adapter.ts` (domain-module refactor, issue #2018
|
|
5
|
+
* track D). Pure functions over the IR — they read no adapter instance
|
|
6
|
+
* state. `collectImportedLoopChildComponentErrors` returns its diagnostics
|
|
7
|
+
* instead of pushing onto the adapter's error list, so the adapter stays the
|
|
8
|
+
* sole owner of `errors`.
|
|
9
|
+
*
|
|
10
|
+
* SHARED CANDIDATE: `hasClientInteractivity` is byte-identical to the Xslate
|
|
11
|
+
* adapter's copy and adapter-agnostic; the BF103 loop-child check is the same
|
|
12
|
+
* structural walk in both, differing only in the Perl/Kolon diagnostic text —
|
|
13
|
+
* both are groundwork for a shared Perl-family codegen module (issue #2018
|
|
14
|
+
* track D).
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import type {
|
|
18
|
+
ComponentIR,
|
|
19
|
+
IRNode,
|
|
20
|
+
IRComponent,
|
|
21
|
+
IRElement,
|
|
22
|
+
IRFragment,
|
|
23
|
+
IRConditional,
|
|
24
|
+
IRLoop,
|
|
25
|
+
IRIfStatement,
|
|
26
|
+
IRProvider,
|
|
27
|
+
IRAsync,
|
|
28
|
+
CompilerError,
|
|
29
|
+
} from '@barefootjs/jsx'
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Whether the component needs the client runtime — it owns reactive state
|
|
33
|
+
* (signals / effects / onMount) or the analyzer flagged it as needing init.
|
|
34
|
+
*/
|
|
35
|
+
export function hasClientInteractivity(ir: ComponentIR): boolean {
|
|
36
|
+
return (
|
|
37
|
+
ir.metadata.signals.length > 0 ||
|
|
38
|
+
ir.metadata.effects.length > 0 ||
|
|
39
|
+
ir.metadata.onMounts.length > 0 ||
|
|
40
|
+
(ir.metadata.clientAnalysis?.needsInit ?? false)
|
|
41
|
+
)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Build a `BF103` diagnostic for every component reference inside a loop body
|
|
46
|
+
* whose name is imported from a relative-path module. Mirror of the Go
|
|
47
|
+
* adapter's check — the Mojo adapter has the same cross-template-registration
|
|
48
|
+
* constraint at request time. Returns the diagnostics so the caller pushes
|
|
49
|
+
* them onto its own error list.
|
|
50
|
+
*/
|
|
51
|
+
export function collectImportedLoopChildComponentErrors(
|
|
52
|
+
ir: ComponentIR,
|
|
53
|
+
componentName: string,
|
|
54
|
+
): CompilerError[] {
|
|
55
|
+
const errors: CompilerError[] = []
|
|
56
|
+
// Collect every name imported from a relative-path module (no
|
|
57
|
+
// case filter — `IRComponent` nodes only exist for PascalCase JSX
|
|
58
|
+
// usages, so a lowercase utility import in the set can't match
|
|
59
|
+
// anyway, and any heuristic on the import name itself would be
|
|
60
|
+
// strictly less robust than the structural IR check below).
|
|
61
|
+
const relativeImports = new Set<string>()
|
|
62
|
+
for (const imp of ir.metadata.templateImports ?? ir.metadata.imports ?? []) {
|
|
63
|
+
if (!imp.source.startsWith('./') && !imp.source.startsWith('../')) continue
|
|
64
|
+
if (imp.isTypeOnly) continue
|
|
65
|
+
for (const spec of imp.specifiers) {
|
|
66
|
+
relativeImports.add(spec.alias ?? spec.name)
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
if (relativeImports.size === 0) return errors
|
|
70
|
+
|
|
71
|
+
const loc = { file: componentName + '.tsx', start: { line: 1, column: 0 }, end: { line: 1, column: 0 } }
|
|
72
|
+
const visit = (node: IRNode, inLoop: boolean): void => {
|
|
73
|
+
switch (node.type) {
|
|
74
|
+
case 'component': {
|
|
75
|
+
const comp = node as IRComponent
|
|
76
|
+
if (inLoop && relativeImports.has(comp.name)) {
|
|
77
|
+
errors.push({
|
|
78
|
+
code: 'BF103',
|
|
79
|
+
severity: 'error',
|
|
80
|
+
message: `Component <${comp.name}> is imported from a sibling module and used inside a loop. The Mojo adapter emits a cross-template call; the child template must be registered alongside the parent at render time.`,
|
|
81
|
+
loc: comp.loc ?? loc,
|
|
82
|
+
suggestion: {
|
|
83
|
+
message:
|
|
84
|
+
`Options:\n` +
|
|
85
|
+
` 1. Compile '${comp.name}' (its source file) with the same adapter and register the resulting Mojo template alongside the parent at render time.\n` +
|
|
86
|
+
` 2. Inline <${comp.name}> directly inside the loop body so no cross-file template lookup is needed.\n` +
|
|
87
|
+
` 3. Mark the loop position as @client-only so the template is materialised on the client instead of at SSR time.`,
|
|
88
|
+
},
|
|
89
|
+
})
|
|
90
|
+
}
|
|
91
|
+
for (const child of comp.children) visit(child, inLoop)
|
|
92
|
+
break
|
|
93
|
+
}
|
|
94
|
+
case 'element':
|
|
95
|
+
for (const child of (node as IRElement).children) visit(child, inLoop)
|
|
96
|
+
break
|
|
97
|
+
case 'fragment':
|
|
98
|
+
for (const child of (node as IRFragment).children) visit(child, inLoop)
|
|
99
|
+
break
|
|
100
|
+
case 'conditional': {
|
|
101
|
+
const cond = node as IRConditional
|
|
102
|
+
visit(cond.whenTrue, inLoop)
|
|
103
|
+
if (cond.whenFalse) visit(cond.whenFalse, inLoop)
|
|
104
|
+
break
|
|
105
|
+
}
|
|
106
|
+
case 'loop':
|
|
107
|
+
for (const child of (node as IRLoop).children) visit(child, true)
|
|
108
|
+
break
|
|
109
|
+
case 'if-statement': {
|
|
110
|
+
const stmt = node as IRIfStatement
|
|
111
|
+
visit(stmt.consequent, inLoop)
|
|
112
|
+
if (stmt.alternate) visit(stmt.alternate, inLoop)
|
|
113
|
+
break
|
|
114
|
+
}
|
|
115
|
+
case 'provider':
|
|
116
|
+
for (const child of (node as IRProvider).children) visit(child, inLoop)
|
|
117
|
+
break
|
|
118
|
+
case 'async': {
|
|
119
|
+
const a = node as IRAsync
|
|
120
|
+
visit(a.fallback, inLoop)
|
|
121
|
+
for (const child of a.children) visit(child, inLoop)
|
|
122
|
+
break
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
visit(ir.root, false)
|
|
127
|
+
return errors
|
|
128
|
+
}
|