@barefootjs/mojolicious 0.17.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/expr/array-method.d.ts +10 -0
- package/dist/adapter/expr/array-method.d.ts.map +1 -1
- package/dist/adapter/expr/emitters.d.ts +2 -1
- package/dist/adapter/expr/emitters.d.ts.map +1 -1
- package/dist/adapter/index.js +44 -55
- package/dist/adapter/lib/ir-scope.d.ts +0 -7
- package/dist/adapter/lib/ir-scope.d.ts.map +1 -1
- package/dist/adapter/memo/seed.d.ts +10 -19
- package/dist/adapter/memo/seed.d.ts.map +1 -1
- package/dist/adapter/mojo-adapter.d.ts.map +1 -1
- package/dist/build.js +44 -55
- package/dist/index.js +44 -55
- 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 +165 -12
- package/src/adapter/expr/array-method.ts +21 -0
- package/src/adapter/expr/emitters.ts +36 -4
- package/src/adapter/lib/ir-scope.ts +0 -13
- package/src/adapter/memo/seed.ts +20 -73
- package/src/adapter/mojo-adapter.ts +25 -9
package/dist/index.js
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
import {
|
|
3
3
|
BaseAdapter,
|
|
4
4
|
isBooleanAttr,
|
|
5
|
-
parseExpression as
|
|
5
|
+
parseExpression as parseExpression2,
|
|
6
6
|
stringifyParsedExpr as stringifyParsedExpr2,
|
|
7
7
|
parseStyleObjectEntries,
|
|
8
|
-
isSupported
|
|
8
|
+
isSupported,
|
|
9
9
|
exprToString,
|
|
10
10
|
parseProviderObjectLiteral,
|
|
11
11
|
emitParsedExpr as emitParsedExpr2,
|
|
@@ -128,13 +128,6 @@ function collectRootScopeNodes(node) {
|
|
|
128
128
|
visit(node);
|
|
129
129
|
return out;
|
|
130
130
|
}
|
|
131
|
-
function referencedVarsAreAvailable(expr, available) {
|
|
132
|
-
for (const m of expr.matchAll(/\$([A-Za-z_]\w*)/g)) {
|
|
133
|
-
if (!available.has(m[1]))
|
|
134
|
-
return false;
|
|
135
|
-
}
|
|
136
|
-
return true;
|
|
137
|
-
}
|
|
138
131
|
|
|
139
132
|
// src/adapter/expr/array-method.ts
|
|
140
133
|
import {
|
|
@@ -307,6 +300,13 @@ function renderFlatMapEval(recv, body, param, emit) {
|
|
|
307
300
|
const env = emitEvalEnvArg(body, [param], emit);
|
|
308
301
|
return `bf->flat_map_eval(${recv}, '${escapePerlSingleQuote(json)}', '${param}', ${env})`;
|
|
309
302
|
}
|
|
303
|
+
function renderMapEval(recv, body, param, emit) {
|
|
304
|
+
const json = serializeParsedExpr(body);
|
|
305
|
+
if (json === null)
|
|
306
|
+
return null;
|
|
307
|
+
const env = emitEvalEnvArg(body, [param], emit);
|
|
308
|
+
return `bf->map_eval(${recv}, '${escapePerlSingleQuote(json)}', '${param}', ${env})`;
|
|
309
|
+
}
|
|
310
310
|
function renderSortMethod(recv, c) {
|
|
311
311
|
const keyHashes = c.keys.map((k) => {
|
|
312
312
|
const keyEntry = k.key.kind === "self" ? `key_kind => 'self'` : `key_kind => 'field', key => '${k.key.field}'`;
|
|
@@ -360,10 +360,12 @@ class MojoFilterEmitter {
|
|
|
360
360
|
param;
|
|
361
361
|
localVarMap;
|
|
362
362
|
isStringName;
|
|
363
|
-
|
|
363
|
+
onUnsupported;
|
|
364
|
+
constructor(param, localVarMap, isStringName = () => false, onUnsupported) {
|
|
364
365
|
this.param = param;
|
|
365
366
|
this.localVarMap = localVarMap;
|
|
366
367
|
this.isStringName = isStringName;
|
|
368
|
+
this.onUnsupported = onUnsupported;
|
|
367
369
|
}
|
|
368
370
|
identifier(name) {
|
|
369
371
|
if (name === this.param)
|
|
@@ -442,12 +444,14 @@ class MojoFilterEmitter {
|
|
|
442
444
|
return `(${l} // ${r})`;
|
|
443
445
|
}
|
|
444
446
|
callbackMethod(method, object, arrow, _restArgs, emit) {
|
|
445
|
-
if (!PREDICATE_METHODS.has(method))
|
|
447
|
+
if (!PREDICATE_METHODS.has(method)) {
|
|
448
|
+
this.onUnsupported?.(`Filter predicate contains a nested '.${method}(...)' callback, which has no Perl scalar form`, `Rewrite the predicate without a nested callback method, or add /* @client */ for client-only evaluation (no SSR).`);
|
|
446
449
|
return "1";
|
|
450
|
+
}
|
|
447
451
|
const param = arrow.params[0];
|
|
448
452
|
const predicate = arrow.body;
|
|
449
453
|
const arrayExpr = emit(object);
|
|
450
|
-
const predBody = emitParsedExpr(predicate, new MojoFilterEmitter(param, this.localVarMap, this.isStringName));
|
|
454
|
+
const predBody = emitParsedExpr(predicate, new MojoFilterEmitter(param, this.localVarMap, this.isStringName, this.onUnsupported));
|
|
451
455
|
const grepBody = predBody.replace(new RegExp(`\\$${param}\\b`, "g"), "$_");
|
|
452
456
|
if (method === "filter")
|
|
453
457
|
return `[grep { ${grepBody} } @{${arrayExpr}}]`;
|
|
@@ -455,6 +459,7 @@ class MojoFilterEmitter {
|
|
|
455
459
|
return `!(grep { !(${grepBody}) } @{${arrayExpr}})`;
|
|
456
460
|
if (method === "some")
|
|
457
461
|
return `!!(grep { ${grepBody} } @{${arrayExpr}})`;
|
|
462
|
+
this.onUnsupported?.(`Filter predicate contains a nested '.${method}(...)' callback, which has no Perl scalar form`, `Rewrite the predicate without a nested callback method, or add /* @client */ for client-only evaluation (no SSR).`);
|
|
458
463
|
return arrayExpr;
|
|
459
464
|
}
|
|
460
465
|
arrayLiteral(elements, emit) {
|
|
@@ -620,6 +625,13 @@ class MojoTopLevelEmitter {
|
|
|
620
625
|
this.ctx._recordExprBF101(`.flatMap(...) projection is not lowerable to a template flat-map`, `Pre-compute the projection in the route handler, or mark the loop @client-only.`);
|
|
621
626
|
return "''";
|
|
622
627
|
}
|
|
628
|
+
if (method === "map") {
|
|
629
|
+
const evalForm = renderMapEval(recv, body, params[0], emit);
|
|
630
|
+
if (evalForm !== null)
|
|
631
|
+
return evalForm;
|
|
632
|
+
this.ctx._recordExprBF101(`.map(...) projection is not lowerable to a template map`, `Pre-compute the projection in the route handler, or mark the position @client-only.`);
|
|
633
|
+
return "''";
|
|
634
|
+
}
|
|
623
635
|
const cb = {
|
|
624
636
|
method,
|
|
625
637
|
object,
|
|
@@ -863,9 +875,7 @@ function recordIndexAccessToPerl(ctx, val) {
|
|
|
863
875
|
// src/adapter/memo/seed.ts
|
|
864
876
|
import {
|
|
865
877
|
collectContextConsumers,
|
|
866
|
-
|
|
867
|
-
isSupported,
|
|
868
|
-
parseExpression as parseExpression2
|
|
878
|
+
computeSsrSeedPlan
|
|
869
879
|
} from "@barefootjs/jsx";
|
|
870
880
|
function contextDefaultPerl(c) {
|
|
871
881
|
const d = c.defaultValue;
|
|
@@ -886,42 +896,20 @@ function generateContextConsumerSeed(ir) {
|
|
|
886
896
|
`;
|
|
887
897
|
}
|
|
888
898
|
function generateDerivedMemoSeed(ctx, ir) {
|
|
889
|
-
const
|
|
890
|
-
const signals = ir.metadata.signals ?? [];
|
|
891
|
-
if (memos.length === 0 && signals.length === 0)
|
|
892
|
-
return "";
|
|
893
|
-
const available = new Set(ir.metadata.propsParams.map((p) => p.name));
|
|
899
|
+
const plan = ir.metadata.ssrSeedPlan ?? computeSsrSeedPlan(ir.metadata);
|
|
894
900
|
const lines = [];
|
|
895
|
-
for (const
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
const body = extractArrowBodyExpression(memo.computation);
|
|
903
|
-
if (body !== null) {
|
|
904
|
-
const perl = tryLowerToPerl(ctx, body, available);
|
|
905
|
-
if (perl !== null)
|
|
906
|
-
lines.push(`% my $${memo.name} = ${perl};`);
|
|
907
|
-
}
|
|
908
|
-
available.add(memo.name);
|
|
901
|
+
for (const step of plan.steps) {
|
|
902
|
+
if (step.kind !== "derived")
|
|
903
|
+
continue;
|
|
904
|
+
const perl = ctx.convertExpressionToPerl(step.expr, step.parsed);
|
|
905
|
+
if (perl === "" || !/\$[A-Za-z_]\w*/.test(perl))
|
|
906
|
+
continue;
|
|
907
|
+
lines.push(`% my $${step.name} = ${perl};`);
|
|
909
908
|
}
|
|
910
909
|
return lines.length > 0 ? lines.join(`
|
|
911
910
|
`) + `
|
|
912
911
|
` : "";
|
|
913
912
|
}
|
|
914
|
-
function tryLowerToPerl(ctx, expr, available) {
|
|
915
|
-
const trimmed = expr.trim();
|
|
916
|
-
if (!trimmed)
|
|
917
|
-
return null;
|
|
918
|
-
if (!isSupported(parseExpression2(trimmed)).supported)
|
|
919
|
-
return null;
|
|
920
|
-
const perl = ctx.convertExpressionToPerl(trimmed);
|
|
921
|
-
if (perl === "" || !/\$[A-Za-z_]\w*/.test(perl))
|
|
922
|
-
return null;
|
|
923
|
-
return referencedVarsAreAvailable(perl, available) ? perl : null;
|
|
924
|
-
}
|
|
925
913
|
|
|
926
914
|
// src/adapter/value/parsed-literal.ts
|
|
927
915
|
function isStringTypeInfo(type) {
|
|
@@ -1043,7 +1031,7 @@ class MojoAdapter extends BaseAdapter {
|
|
|
1043
1031
|
return this.booleanTypedProps.has(bare);
|
|
1044
1032
|
}
|
|
1045
1033
|
parseUndefinedAlternateTernary(expr) {
|
|
1046
|
-
const parsed =
|
|
1034
|
+
const parsed = parseExpression2(expr.trim());
|
|
1047
1035
|
if (parsed?.kind !== "conditional")
|
|
1048
1036
|
return null;
|
|
1049
1037
|
const alt = parsed.alternate;
|
|
@@ -1291,8 +1279,9 @@ ${whenTrue}
|
|
|
1291
1279
|
return `<%== bf->comment("cond-start:${condId}") %>${content}<%== bf->comment("cond-end:${condId}") %>`;
|
|
1292
1280
|
}
|
|
1293
1281
|
renderLoop(loop) {
|
|
1294
|
-
if (loop.clientOnly)
|
|
1295
|
-
return ""
|
|
1282
|
+
if (loop.clientOnly) {
|
|
1283
|
+
return `<%== bf->comment("loop:${loop.markerId}") %><%== bf->comment("/loop:${loop.markerId}") %>`;
|
|
1284
|
+
}
|
|
1296
1285
|
const destructure = !!(loop.paramBindings && loop.paramBindings.length > 0);
|
|
1297
1286
|
const supportableDestructure = destructure && isLowerableObjectRestDestructure(loop);
|
|
1298
1287
|
if (destructure && !supportableDestructure) {
|
|
@@ -1547,7 +1536,7 @@ ${children}`;
|
|
|
1547
1536
|
if (localConst?.value !== undefined) {
|
|
1548
1537
|
const initTrimmed = localConst.value.trim();
|
|
1549
1538
|
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(initTrimmed)) {
|
|
1550
|
-
const resolved = conditionalSpreadToPerl(this.spreadCtx,
|
|
1539
|
+
const resolved = conditionalSpreadToPerl(this.spreadCtx, parseExpression2(initTrimmed));
|
|
1551
1540
|
if (resolved !== null) {
|
|
1552
1541
|
return `<%== bf->spread_attrs(${resolved}) %>`;
|
|
1553
1542
|
}
|
|
@@ -1565,7 +1554,7 @@ ${children}`;
|
|
|
1565
1554
|
if (!entries)
|
|
1566
1555
|
return null;
|
|
1567
1556
|
for (const e of entries) {
|
|
1568
|
-
if (e.kind === "expr" && !
|
|
1557
|
+
if (e.kind === "expr" && !isSupported(parseExpression2(e.expr)).supported)
|
|
1569
1558
|
return null;
|
|
1570
1559
|
}
|
|
1571
1560
|
return entries.map((e) => e.kind === "literal" ? `${this.escapeAttrText(e.cssKey)}:${this.escapeAttrText(e.value)}` : `${this.escapeAttrText(e.cssKey)}:<%= ${this.convertExpressionToPerl(e.expr)} %>`).join(";");
|
|
@@ -1601,7 +1590,7 @@ ${children}`;
|
|
|
1601
1590
|
return `${BF_COND}="${condId}"`;
|
|
1602
1591
|
}
|
|
1603
1592
|
renderPerlFilterExpr(expr, param, localVarMap = new Map) {
|
|
1604
|
-
return emitParsedExpr2(expr, new MojoFilterEmitter(param, localVarMap, (n) => this._isStringValueName(n)));
|
|
1593
|
+
return emitParsedExpr2(expr, new MojoFilterEmitter(param, localVarMap, (n) => this._isStringValueName(n), (message, reason) => this._recordExprBF101(message, reason)));
|
|
1605
1594
|
}
|
|
1606
1595
|
convertTemplateLiteralPartsToPerl(literalParts) {
|
|
1607
1596
|
const parts = [];
|
|
@@ -1646,8 +1635,8 @@ ${children}`;
|
|
|
1646
1635
|
const hasTaggedTemplate = /[A-Za-z_$][\w$]*\s*`/.test(probe);
|
|
1647
1636
|
if (!startsAsObjectLiteral && !hasTaggedTemplate)
|
|
1648
1637
|
return false;
|
|
1649
|
-
const parsed =
|
|
1650
|
-
const support =
|
|
1638
|
+
const parsed = parseExpression2(expr.trim());
|
|
1639
|
+
const support = isSupported(parsed);
|
|
1651
1640
|
if (parsed.kind !== "unsupported" && support.supported)
|
|
1652
1641
|
return false;
|
|
1653
1642
|
const reason = support.reason ?? (parsed.kind === "unsupported" ? parsed.reason : undefined);
|
|
@@ -1695,7 +1684,7 @@ ${reason}` : "";
|
|
|
1695
1684
|
const trimmed = expr.trim();
|
|
1696
1685
|
if (trimmed === "")
|
|
1697
1686
|
return "''";
|
|
1698
|
-
parsed =
|
|
1687
|
+
parsed = parseExpression2(trimmed);
|
|
1699
1688
|
}
|
|
1700
1689
|
if (parsed.kind === "call") {
|
|
1701
1690
|
for (const matcher of this._loweringMatchers) {
|
|
@@ -1706,7 +1695,7 @@ ${reason}` : "";
|
|
|
1706
1695
|
}
|
|
1707
1696
|
}
|
|
1708
1697
|
}
|
|
1709
|
-
const support =
|
|
1698
|
+
const support = isSupported(parsed);
|
|
1710
1699
|
if (!support.supported) {
|
|
1711
1700
|
this.errors.push({
|
|
1712
1701
|
code: "BF101",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@barefootjs/mojolicious",
|
|
3
|
-
"version": "0.17.
|
|
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.17.
|
|
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.17.
|
|
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
|
|
@@ -126,6 +134,11 @@ runAdapterConformanceTests({
|
|
|
126
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.
|
|
@@ -792,6 +801,31 @@ export function C() {
|
|
|
792
801
|
expect(template).toContain('scalar(@{[grep { $_->{active} } @{$t->{tags}}]})')
|
|
793
802
|
})
|
|
794
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
|
+
|
|
795
829
|
test('lowers .filter(function (x) { return x.done }).map(...) — function-keyword filter (#1443)', () => {
|
|
796
830
|
// Function expressions with a single `return <expr>` body normalise
|
|
797
831
|
// to the arrow-fn IR shape at parse time, so the higher-order
|
|
@@ -1035,11 +1069,26 @@ export { A }`, 'A.tsx', { adapter })
|
|
|
1035
1069
|
expect(template).toContain('"property":"done"')
|
|
1036
1070
|
}
|
|
1037
1071
|
|
|
1038
|
-
//
|
|
1039
|
-
//
|
|
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.
|
|
1040
1089
|
const adapter = new MojoAdapter()
|
|
1041
1090
|
const fb = compileJSX(`function A({ items }: { items: { name: string }[] }) {
|
|
1042
|
-
return <div>{items.every(x => x.name.
|
|
1091
|
+
return <div>{items.every(x => x.name.toUpperCase() === 'A') ? 'y' : 'n'}</div>
|
|
1043
1092
|
}
|
|
1044
1093
|
export { A }`, 'A.tsx', { adapter })
|
|
1045
1094
|
const fbTemplate = fb.files.find(f => f.path.endsWith('.html.ep'))?.content ?? ''
|
|
@@ -1488,6 +1537,110 @@ export { C }
|
|
|
1488
1537
|
})
|
|
1489
1538
|
})
|
|
1490
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
|
+
|
|
1491
1644
|
describe('MojoAdapter - #1448 Tier C .flatMap(field projection)', () => {
|
|
1492
1645
|
function emitFlatMap(expr: string): string {
|
|
1493
1646
|
const a = new MojoAdapter()
|
|
@@ -372,6 +372,27 @@ export function renderFlatMapEval(
|
|
|
372
372
|
return `bf->flat_map_eval(${recv}, '${escapePerlSingleQuote(json)}', '${param}', ${env})`
|
|
373
373
|
}
|
|
374
374
|
|
|
375
|
+
/**
|
|
376
|
+
* Emit a value-producing `.map(cb)` via the runtime evaluator (#2073): the
|
|
377
|
+
* projection `body` serializes to JSON and `bf->map_eval` projects each
|
|
378
|
+
* element, one result per element (no flatten — the JS `.map` contract).
|
|
379
|
+
* Composes through the array-method chain (`.map(cb).join(' ')`). Returns
|
|
380
|
+
* null when the projection is outside the evaluator surface, and the caller
|
|
381
|
+
* records BF101. The JSX-returning `.map` is an IRLoop upstream and never
|
|
382
|
+
* reaches this emit.
|
|
383
|
+
*/
|
|
384
|
+
export function renderMapEval(
|
|
385
|
+
recv: string,
|
|
386
|
+
body: ParsedExpr,
|
|
387
|
+
param: string,
|
|
388
|
+
emit: (e: ParsedExpr) => string,
|
|
389
|
+
): string | null {
|
|
390
|
+
const json = serializeParsedExpr(body)
|
|
391
|
+
if (json === null) return null
|
|
392
|
+
const env = emitEvalEnvArg(body, [param], emit)
|
|
393
|
+
return `bf->map_eval(${recv}, '${escapePerlSingleQuote(json)}', '${param}', ${env})`
|
|
394
|
+
}
|
|
395
|
+
|
|
375
396
|
/**
|
|
376
397
|
* Shared Mojo emit for `.sort(cmp)` / `.toSorted(cmp)` (#1448 Tier B).
|
|
377
398
|
* Used by both the filter-context emitter and the top-level emitter,
|
|
@@ -38,6 +38,7 @@ import {
|
|
|
38
38
|
renderPredicateEval,
|
|
39
39
|
renderFlatMethod,
|
|
40
40
|
renderFlatMapEval,
|
|
41
|
+
renderMapEval,
|
|
41
42
|
} from './array-method.ts'
|
|
42
43
|
|
|
43
44
|
/**
|
|
@@ -81,6 +82,11 @@ export class MojoFilterEmitter implements ParsedExprEmitter {
|
|
|
81
82
|
// against it lowers to `eq`/`ne` (#1672). Defaults to "never" for callers
|
|
82
83
|
// that don't thread it through.
|
|
83
84
|
private readonly isStringName: (n: string) => boolean = () => false,
|
|
85
|
+
// Records a BF101 for nested callback shapes this emitter can only
|
|
86
|
+
// degrade — `find*` and the non-predicate methods (#2038). Optional so
|
|
87
|
+
// emitter construction stays possible without an adapter; a missing hook
|
|
88
|
+
// keeps the old silent-degrade emit.
|
|
89
|
+
private readonly onUnsupported?: (message: string, reason?: string) => void,
|
|
84
90
|
) {}
|
|
85
91
|
|
|
86
92
|
identifier(name: string): string {
|
|
@@ -172,20 +178,34 @@ export class MojoFilterEmitter implements ParsedExprEmitter {
|
|
|
172
178
|
): string {
|
|
173
179
|
// Filter context only meaningfully handles the predicate methods
|
|
174
180
|
// (filter / every / some land here through nested `.filter(...)` chains).
|
|
175
|
-
// Sort / reduce / flatMap
|
|
176
|
-
//
|
|
177
|
-
|
|
181
|
+
// Sort / reduce / flatMap have no scalar Perl form here — the old
|
|
182
|
+
// `default` arm degraded them to the truthy sentinel, silently rewriting
|
|
183
|
+
// the predicate, so surface BF101 instead (#2038).
|
|
184
|
+
if (!PREDICATE_METHODS.has(method)) {
|
|
185
|
+
this.onUnsupported?.(
|
|
186
|
+
`Filter predicate contains a nested '.${method}(...)' callback, which has no Perl scalar form`,
|
|
187
|
+
`Rewrite the predicate without a nested callback method, or add /* @client */ for client-only evaluation (no SSR).`,
|
|
188
|
+
)
|
|
189
|
+
return '1'
|
|
190
|
+
}
|
|
178
191
|
// The predicate body is also a filter context, but with this
|
|
179
192
|
// callback's own `param` (potentially shadowing the outer one),
|
|
180
193
|
// so we spin up a nested emitter with the inner param.
|
|
181
194
|
const param = arrow.params[0]
|
|
182
195
|
const predicate = arrow.body
|
|
183
196
|
const arrayExpr = emit(object)
|
|
184
|
-
const predBody = emitParsedExpr(predicate, new MojoFilterEmitter(param, this.localVarMap, this.isStringName))
|
|
197
|
+
const predBody = emitParsedExpr(predicate, new MojoFilterEmitter(param, this.localVarMap, this.isStringName, this.onUnsupported))
|
|
185
198
|
const grepBody = predBody.replace(new RegExp(`\\$${param}\\b`, 'g'), '$_')
|
|
186
199
|
if (method === 'filter') return `[grep { ${grepBody} } @{${arrayExpr}}]`
|
|
187
200
|
if (method === 'every') return `!(grep { !(${grepBody}) } @{${arrayExpr}})`
|
|
188
201
|
if (method === 'some') return `!!(grep { ${grepBody} } @{${arrayExpr}})`
|
|
202
|
+
// `find` / `findIndex` / `findLast` / `findLastIndex` return an element
|
|
203
|
+
// (or index), not a boolean — there is no inline grep form. Degrading to
|
|
204
|
+
// the receiver silently changes predicate semantics, so be loud (#2038).
|
|
205
|
+
this.onUnsupported?.(
|
|
206
|
+
`Filter predicate contains a nested '.${method}(...)' callback, which has no Perl scalar form`,
|
|
207
|
+
`Rewrite the predicate without a nested callback method, or add /* @client */ for client-only evaluation (no SSR).`,
|
|
208
|
+
)
|
|
189
209
|
return arrayExpr
|
|
190
210
|
}
|
|
191
211
|
|
|
@@ -448,6 +468,18 @@ export class MojoTopLevelEmitter implements ParsedExprEmitter {
|
|
|
448
468
|
return "''"
|
|
449
469
|
}
|
|
450
470
|
|
|
471
|
+
// Value-producing map (#2073): eval-only; BF101 when the projection is
|
|
472
|
+
// outside the surface. (The JSX-returning `.map` is an IRLoop upstream.)
|
|
473
|
+
if (method === 'map') {
|
|
474
|
+
const evalForm = renderMapEval(recv, body, params[0], emit)
|
|
475
|
+
if (evalForm !== null) return evalForm
|
|
476
|
+
this.ctx._recordExprBF101(
|
|
477
|
+
`.map(...) projection is not lowerable to a template map`,
|
|
478
|
+
`Pre-compute the projection in the route handler, or mark the position @client-only.`,
|
|
479
|
+
)
|
|
480
|
+
return "''"
|
|
481
|
+
}
|
|
482
|
+
|
|
451
483
|
// Predicate methods: filter / find / every / some / findIndex / findLast /
|
|
452
484
|
// findLastIndex. Eval-first (#2018 P2), then the inline `grep` / `bf->find`
|
|
453
485
|
// fallback for a predicate the evaluator can't model.
|
|
@@ -55,16 +55,3 @@ export function collectRootScopeNodes(node: IRNode): Set<IRNode> {
|
|
|
55
55
|
visit(node)
|
|
56
56
|
return out
|
|
57
57
|
}
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* True when every `$var` the lowered (Perl / Kolon) expression references is
|
|
61
|
-
* in the available set — i.e. the template already has that var in scope.
|
|
62
|
-
* Guards in-template memo seeding from referencing an out-of-scope binding
|
|
63
|
-
* (which would trip Perl strict mode). (#1297)
|
|
64
|
-
*/
|
|
65
|
-
export function referencedVarsAreAvailable(expr: string, available: ReadonlySet<string>): boolean {
|
|
66
|
-
for (const m of expr.matchAll(/\$([A-Za-z_]\w*)/g)) {
|
|
67
|
-
if (!available.has(m[1])) return false
|
|
68
|
-
}
|
|
69
|
-
return true
|
|
70
|
-
}
|