@barefootjs/xslate 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 +6 -7
- package/dist/adapter/expr/emitters.d.ts.map +1 -1
- package/dist/adapter/index.js +42 -55
- package/dist/adapter/lib/ir-scope.d.ts +0 -5
- package/dist/adapter/lib/ir-scope.d.ts.map +1 -1
- package/dist/adapter/memo/seed.d.ts +14 -14
- package/dist/adapter/memo/seed.d.ts.map +1 -1
- package/dist/adapter/xslate-adapter.d.ts.map +1 -1
- package/dist/build.js +42 -55
- package/dist/index.js +42 -55
- package/lib/BarefootJS/Backend/Xslate.pm +1 -1
- package/package.json +3 -3
- package/src/__tests__/xslate-adapter.test.ts +148 -11
- package/src/adapter/expr/array-method.ts +21 -0
- package/src/adapter/expr/emitters.ts +30 -12
- package/src/adapter/lib/ir-scope.ts +0 -11
- package/src/adapter/memo/seed.ts +25 -72
- package/src/adapter/xslate-adapter.ts +21 -3
package/dist/index.js
CHANGED
|
@@ -187291,12 +187291,12 @@ Additional information: BADCLIENT: Bad error code, ${badCode} not found in range
|
|
|
187291
187291
|
import {
|
|
187292
187292
|
BaseAdapter,
|
|
187293
187293
|
isBooleanAttr,
|
|
187294
|
-
parseExpression as
|
|
187294
|
+
parseExpression as parseExpression2,
|
|
187295
187295
|
stringifyParsedExpr as stringifyParsedExpr2,
|
|
187296
187296
|
exprToString,
|
|
187297
187297
|
parseProviderObjectLiteral,
|
|
187298
187298
|
parseStyleObjectEntries,
|
|
187299
|
-
isSupported
|
|
187299
|
+
isSupported,
|
|
187300
187300
|
emitParsedExpr,
|
|
187301
187301
|
emitIRNode,
|
|
187302
187302
|
emitAttrValue,
|
|
@@ -187417,13 +187417,6 @@ function collectRootScopeNodes(node) {
|
|
|
187417
187417
|
visit(node);
|
|
187418
187418
|
return out;
|
|
187419
187419
|
}
|
|
187420
|
-
function referencedVarsAreAvailable(expr, available) {
|
|
187421
|
-
for (const m of expr.matchAll(/\$([A-Za-z_]\w*)/g)) {
|
|
187422
|
-
if (!available.has(m[1]))
|
|
187423
|
-
return false;
|
|
187424
|
-
}
|
|
187425
|
-
return true;
|
|
187426
|
-
}
|
|
187427
187420
|
|
|
187428
187421
|
// src/adapter/expr/array-method.ts
|
|
187429
187422
|
import {
|
|
@@ -187593,6 +187586,13 @@ function renderFlatMapEval(recv, body, param, emit) {
|
|
|
187593
187586
|
const env = emitEvalEnvArg(body, [param], emit);
|
|
187594
187587
|
return `$bf.flat_map_eval(${recv}, '${escapePerlSingleQuote(json)}', '${param}', ${env})`;
|
|
187595
187588
|
}
|
|
187589
|
+
function renderMapEval(recv, body, param, emit) {
|
|
187590
|
+
const json = serializeParsedExpr(body);
|
|
187591
|
+
if (json === null)
|
|
187592
|
+
return null;
|
|
187593
|
+
const env = emitEvalEnvArg(body, [param], emit);
|
|
187594
|
+
return `$bf.map_eval(${recv}, '${escapePerlSingleQuote(json)}', '${param}', ${env})`;
|
|
187595
|
+
}
|
|
187596
187596
|
function renderSortMethod(recv, c) {
|
|
187597
187597
|
const keyHashes = c.keys.map((k) => {
|
|
187598
187598
|
const keyEntry = k.key.kind === "self" ? `key_kind => 'self'` : `key_kind => 'field', key => '${k.key.field}'`;
|
|
@@ -187625,10 +187625,12 @@ class XslateFilterEmitter {
|
|
|
187625
187625
|
param;
|
|
187626
187626
|
localVarMap;
|
|
187627
187627
|
isStringName;
|
|
187628
|
-
|
|
187628
|
+
onUnsupported;
|
|
187629
|
+
constructor(param, localVarMap, isStringName = () => false, onUnsupported) {
|
|
187629
187630
|
this.param = param;
|
|
187630
187631
|
this.localVarMap = localVarMap;
|
|
187631
187632
|
this.isStringName = isStringName;
|
|
187633
|
+
this.onUnsupported = onUnsupported;
|
|
187632
187634
|
}
|
|
187633
187635
|
identifier(name) {
|
|
187634
187636
|
if (name === this.param)
|
|
@@ -187699,6 +187701,7 @@ class XslateFilterEmitter {
|
|
|
187699
187701
|
return `(${l} // ${r})`;
|
|
187700
187702
|
}
|
|
187701
187703
|
callbackMethod(method, object, _arrow, _restArgs, emit) {
|
|
187704
|
+
this.onUnsupported?.(`Filter predicate contains a nested '.${method}(...)' callback, which has no Kolon scalar form`, `Rewrite the predicate without a nested callback method, or add /* @client */ for client-only evaluation (no SSR).`);
|
|
187702
187705
|
return emit(object);
|
|
187703
187706
|
}
|
|
187704
187707
|
arrayLiteral(elements, emit) {
|
|
@@ -187859,6 +187862,13 @@ class XslateTopLevelEmitter {
|
|
|
187859
187862
|
this.ctx._recordExprBF101(`'.flatMap(...)' projection is outside the Xslate adapter's evaluable surface`, `Pre-compute the projected array, or move this position to a '/* @client */' boundary.`);
|
|
187860
187863
|
return "''";
|
|
187861
187864
|
}
|
|
187865
|
+
if (method === "map") {
|
|
187866
|
+
const evalForm = renderMapEval(recv, body, params[0], emit);
|
|
187867
|
+
if (evalForm !== null)
|
|
187868
|
+
return evalForm;
|
|
187869
|
+
this.ctx._recordExprBF101(`'.map(...)' projection is outside the Xslate adapter's evaluable surface`, `Pre-compute the projected array, or move this position to a '/* @client */' boundary.`);
|
|
187870
|
+
return "''";
|
|
187871
|
+
}
|
|
187862
187872
|
return recv;
|
|
187863
187873
|
}
|
|
187864
187874
|
_emitPredicateCallback(call, arrayExpr, emit) {
|
|
@@ -188092,9 +188102,7 @@ function recordIndexAccessToKolon(ctx, val) {
|
|
|
188092
188102
|
// src/adapter/memo/seed.ts
|
|
188093
188103
|
import {
|
|
188094
188104
|
collectContextConsumers,
|
|
188095
|
-
|
|
188096
|
-
isSupported,
|
|
188097
|
-
parseExpression as parseExpression2
|
|
188105
|
+
computeSsrSeedPlan
|
|
188098
188106
|
} from "@barefootjs/jsx";
|
|
188099
188107
|
function contextDefaultKolon(c) {
|
|
188100
188108
|
const d = c.defaultValue;
|
|
@@ -188115,44 +188123,22 @@ function generateContextConsumerSeed(ir) {
|
|
|
188115
188123
|
`;
|
|
188116
188124
|
}
|
|
188117
188125
|
function generateDerivedMemoSeed(ctx, ir) {
|
|
188118
|
-
const
|
|
188119
|
-
const signals = ir.metadata.signals ?? [];
|
|
188120
|
-
if (memos.length === 0 && signals.length === 0)
|
|
188121
|
-
return "";
|
|
188122
|
-
const available = new Set(ir.metadata.propsParams.map((p) => p.name));
|
|
188126
|
+
const plan = ir.metadata.ssrSeedPlan ?? computeSsrSeedPlan(ir.metadata);
|
|
188123
188127
|
const lines = [];
|
|
188124
|
-
for (const
|
|
188125
|
-
|
|
188126
|
-
|
|
188127
|
-
|
|
188128
|
-
|
|
188129
|
-
|
|
188130
|
-
|
|
188131
|
-
|
|
188132
|
-
|
|
188133
|
-
if (body !== null) {
|
|
188134
|
-
const kolon = tryLowerToKolon(ctx, body, available);
|
|
188135
|
-
const refsSelf = kolon !== null && new RegExp(`\\$${memo.name}\\b`).test(kolon);
|
|
188136
|
-
if (kolon !== null && !refsSelf)
|
|
188137
|
-
lines.push(`: my $${memo.name} = ${kolon};`);
|
|
188138
|
-
}
|
|
188139
|
-
available.add(memo.name);
|
|
188128
|
+
for (const step of plan.steps) {
|
|
188129
|
+
if (step.kind !== "derived")
|
|
188130
|
+
continue;
|
|
188131
|
+
const kolon = ctx.convertExpressionToKolon(step.expr, step.parsed);
|
|
188132
|
+
if (kolon === "" || !/\$[A-Za-z_]\w*/.test(kolon))
|
|
188133
|
+
continue;
|
|
188134
|
+
if (new RegExp(`\\$${step.name}\\b`).test(kolon))
|
|
188135
|
+
continue;
|
|
188136
|
+
lines.push(`: my $${step.name} = ${kolon};`);
|
|
188140
188137
|
}
|
|
188141
188138
|
return lines.length > 0 ? lines.join(`
|
|
188142
188139
|
`) + `
|
|
188143
188140
|
` : "";
|
|
188144
188141
|
}
|
|
188145
|
-
function tryLowerToKolon(ctx, expr, available) {
|
|
188146
|
-
const trimmed = expr.trim();
|
|
188147
|
-
if (!trimmed)
|
|
188148
|
-
return null;
|
|
188149
|
-
if (!isSupported(parseExpression2(trimmed)).supported)
|
|
188150
|
-
return null;
|
|
188151
|
-
const kolon = ctx.convertExpressionToKolon(trimmed);
|
|
188152
|
-
if (kolon === "" || !/\$[A-Za-z_]\w*/.test(kolon))
|
|
188153
|
-
return null;
|
|
188154
|
-
return referencedVarsAreAvailable(kolon, available) ? kolon : null;
|
|
188155
|
-
}
|
|
188156
188142
|
|
|
188157
188143
|
// src/adapter/value/parsed-literal.ts
|
|
188158
188144
|
import { evalStringArrayJoin } from "@barefootjs/jsx";
|
|
@@ -188452,8 +188438,9 @@ ${whenTrue}
|
|
|
188452
188438
|
return `<: $bf.comment("cond-start:${condId}") | mark_raw :>${content}<: $bf.comment("cond-end:${condId}") | mark_raw :>`;
|
|
188453
188439
|
}
|
|
188454
188440
|
renderLoop(loop) {
|
|
188455
|
-
if (loop.clientOnly)
|
|
188456
|
-
return ""
|
|
188441
|
+
if (loop.clientOnly) {
|
|
188442
|
+
return `<: $bf.comment("loop:${loop.markerId}") | mark_raw :><: $bf.comment("/loop:${loop.markerId}") | mark_raw :>`;
|
|
188443
|
+
}
|
|
188457
188444
|
const destructure = !!(loop.paramBindings && loop.paramBindings.length > 0);
|
|
188458
188445
|
const supportableDestructure = destructure && isLowerableObjectRestDestructure(loop);
|
|
188459
188446
|
if (destructure && !supportableDestructure) {
|
|
@@ -188709,7 +188696,7 @@ ${name}="<: ${val} :>"
|
|
|
188709
188696
|
if (localConst?.value !== undefined) {
|
|
188710
188697
|
const initTrimmed = localConst.value.trim();
|
|
188711
188698
|
if (!/^[A-Za-z_$][\w$]*$/.test(initTrimmed)) {
|
|
188712
|
-
const resolved = conditionalSpreadToKolon(this.spreadCtx,
|
|
188699
|
+
const resolved = conditionalSpreadToKolon(this.spreadCtx, parseExpression2(initTrimmed));
|
|
188713
188700
|
if (resolved !== null) {
|
|
188714
188701
|
return `<: $bf.spread_attrs(${resolved}) | mark_raw :>`;
|
|
188715
188702
|
}
|
|
@@ -188727,7 +188714,7 @@ ${name}="<: ${val} :>"
|
|
|
188727
188714
|
if (!entries)
|
|
188728
188715
|
return null;
|
|
188729
188716
|
for (const e of entries) {
|
|
188730
|
-
if (e.kind === "expr" && !
|
|
188717
|
+
if (e.kind === "expr" && !isSupported(parseExpression2(e.expr)).supported)
|
|
188731
188718
|
return null;
|
|
188732
188719
|
}
|
|
188733
188720
|
return entries.map((e) => e.kind === "literal" ? `${this.escapeAttrText(e.cssKey)}:${this.escapeAttrText(e.value)}` : `${this.escapeAttrText(e.cssKey)}:<: ${this.convertExpressionToKolon(e.expr)} :>`).join(";");
|
|
@@ -188763,7 +188750,7 @@ ${name}="<: ${val} :>"
|
|
|
188763
188750
|
return `${BF_COND}="${condId}"`;
|
|
188764
188751
|
}
|
|
188765
188752
|
renderKolonFilterExpr(expr, param, localVarMap = new Map) {
|
|
188766
|
-
return emitParsedExpr(expr, new XslateFilterEmitter(param, localVarMap, (n) => this._isStringValueName(n)));
|
|
188753
|
+
return emitParsedExpr(expr, new XslateFilterEmitter(param, localVarMap, (n) => this._isStringValueName(n), (message, reason) => this._recordExprBF101(message, reason)));
|
|
188767
188754
|
}
|
|
188768
188755
|
convertTemplateLiteralPartsToKolon(literalParts) {
|
|
188769
188756
|
const parts = [];
|
|
@@ -188808,8 +188795,8 @@ ${name}="<: ${val} :>"
|
|
|
188808
188795
|
const hasTaggedTemplate = /[A-Za-z_$][\w$]*\s*`/.test(probe);
|
|
188809
188796
|
if (!startsAsObjectLiteral && !hasTaggedTemplate)
|
|
188810
188797
|
return false;
|
|
188811
|
-
const parsed =
|
|
188812
|
-
const support =
|
|
188798
|
+
const parsed = parseExpression2(expr.trim());
|
|
188799
|
+
const support = isSupported(parsed);
|
|
188813
188800
|
if (parsed.kind !== "unsupported" && support.supported)
|
|
188814
188801
|
return false;
|
|
188815
188802
|
const reason = support.reason ?? (parsed.kind === "unsupported" ? parsed.reason : undefined);
|
|
@@ -188856,7 +188843,7 @@ ${reason}` : "";
|
|
|
188856
188843
|
const trimmed = expr.trim();
|
|
188857
188844
|
if (trimmed === "")
|
|
188858
188845
|
return "''";
|
|
188859
|
-
parsed =
|
|
188846
|
+
parsed = parseExpression2(trimmed);
|
|
188860
188847
|
}
|
|
188861
188848
|
if (parsed.kind === "call") {
|
|
188862
188849
|
for (const matcher of this._loweringMatchers) {
|
|
@@ -188867,7 +188854,7 @@ ${reason}` : "";
|
|
|
188867
188854
|
}
|
|
188868
188855
|
}
|
|
188869
188856
|
}
|
|
188870
|
-
const support =
|
|
188857
|
+
const support = isSupported(parsed);
|
|
188871
188858
|
if (!support.supported) {
|
|
188872
188859
|
this.errors.push({
|
|
188873
188860
|
code: "BF101",
|
|
@@ -188895,7 +188882,7 @@ Options:
|
|
|
188895
188882
|
return this.stringValueNames.has(name);
|
|
188896
188883
|
}
|
|
188897
188884
|
parseUndefinedAlternateTernary(expr) {
|
|
188898
|
-
const parsed =
|
|
188885
|
+
const parsed = parseExpression2(expr.trim());
|
|
188899
188886
|
if (parsed?.kind !== "conditional")
|
|
188900
188887
|
return null;
|
|
188901
188888
|
const alt = parsed.alternate;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@barefootjs/xslate",
|
|
3
|
-
"version": "0.17.
|
|
3
|
+
"version": "0.17.1",
|
|
4
4
|
"description": "Text::Xslate (Kolon) adapter for BarefootJS — compiles IR to .tx templates and ships the Xslate rendering backend; runs under any PSGI/Plack app",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -55,14 +55,14 @@
|
|
|
55
55
|
"directory": "packages/adapter-xslate"
|
|
56
56
|
},
|
|
57
57
|
"dependencies": {
|
|
58
|
-
"@barefootjs/shared": "0.17.
|
|
58
|
+
"@barefootjs/shared": "0.17.1"
|
|
59
59
|
},
|
|
60
60
|
"peerDependencies": {
|
|
61
61
|
"@barefootjs/jsx": ">=0.2.0"
|
|
62
62
|
},
|
|
63
63
|
"devDependencies": {
|
|
64
64
|
"@barefootjs/adapter-tests": "0.1.0",
|
|
65
|
-
"@barefootjs/jsx": "0.17.
|
|
65
|
+
"@barefootjs/jsx": "0.17.1",
|
|
66
66
|
"typescript": "^5.0.0"
|
|
67
67
|
}
|
|
68
68
|
}
|
|
@@ -101,10 +101,29 @@ runAdapterConformanceTests({
|
|
|
101
101
|
// Tagged-template-literal call in a className — same family, same
|
|
102
102
|
// refusal (BF101).
|
|
103
103
|
'tagged-template-classname': [{ code: 'BF101', severity: 'error' }],
|
|
104
|
-
//
|
|
105
|
-
//
|
|
106
|
-
//
|
|
107
|
-
//
|
|
104
|
+
// #2038: a filter predicate whose body contains a NESTED callback call
|
|
105
|
+
// (`t => !picked().some(p => …)` / `t => picked().find(p => …)`). Kolon
|
|
106
|
+
// has no inline `grep` form, so `XslateFilterEmitter.callbackMethod` used
|
|
107
|
+
// to degrade the inner call to its receiver, silently changing predicate
|
|
108
|
+
// semantics — the compiler is loud instead of lossy. (Mojo is pinned only
|
|
109
|
+
// for the `.find` variant: it lowers a nested `.some` to a real inline
|
|
110
|
+
// Perl `grep`.) The `/* @client */` twin
|
|
111
|
+
// (`filter-nested-callback-predicate-client`) has no pin here: it must
|
|
112
|
+
// render clean on every adapter, which asserts the suppression contract.
|
|
113
|
+
// https://github.com/piconic-ai/barefootjs/issues/2038
|
|
114
|
+
'filter-nested-callback-predicate': [{ code: 'BF101', severity: 'error' }],
|
|
115
|
+
'filter-nested-find-predicate': [{ code: 'BF101', severity: 'error' }],
|
|
116
|
+
// NB: TOP-LEVEL `.find` / `.findIndex` / `.findLast` / `.findLastIndex`
|
|
117
|
+
// (text position) are NOT pinned here — unlike mojo (which refuses them),
|
|
118
|
+
// Xslate lowers them to `$bf.find` / `find_index` / `find_last` /
|
|
119
|
+
// `find_last_index` via the same Kolon-lambda mechanism as `.filter` /
|
|
120
|
+
// `.every` / `.some`, so they render. Only the NESTED-in-a-predicate form
|
|
121
|
+
// above is refused (#2038).
|
|
122
|
+
// #2073 follow-up: a function-reference `.map(format)` callback has no
|
|
123
|
+
// arrow body to serialize — not a CALLBACK_METHODS shape — so the
|
|
124
|
+
// UNSUPPORTED_METHODS gate refuses it with BF101 rather than emitting
|
|
125
|
+
// a broken template.
|
|
126
|
+
'array-map-function-reference': [{ code: 'BF101', severity: 'error' }],
|
|
108
127
|
},
|
|
109
128
|
// Template-primitive registry parity: same V1 surface as mojo, so the
|
|
110
129
|
// same two cases stay skipped (bespoke user import + customSerialize
|
|
@@ -113,11 +132,15 @@ runAdapterConformanceTests({
|
|
|
113
132
|
TemplatePrimitiveCaseId.USER_IMPORT_VIA_CONST,
|
|
114
133
|
TemplatePrimitiveCaseId.NO_DOUBLE_REWRITE_OF_PROPS_OBJECT,
|
|
115
134
|
]),
|
|
116
|
-
//
|
|
117
|
-
//
|
|
135
|
+
// `client-only` / `client-only-loop-with-sibling-cond` /
|
|
136
|
+
// `filter-nested-callback-predicate-client` are no longer skipped —
|
|
137
|
+
// `renderLoop` now emits the `$bf.comment("loop:<id>")` boundary pair
|
|
138
|
+
// for clientOnly loops (Hono / Go parity), so mapArray() can locate
|
|
139
|
+
// its insertion anchor at hydration time (#872 / #1087).
|
|
118
140
|
skipMarkerConformance: new Set([
|
|
119
|
-
|
|
120
|
-
|
|
141
|
+
// Same as Hono / Mojo: `/* @client */` markers on TodoApp's keyed
|
|
142
|
+
// `.map` intentionally elide a slot id from the SSR template that
|
|
143
|
+
// the IR still declares (s6). See hono-adapter.test for the contract.
|
|
121
144
|
'todo-app',
|
|
122
145
|
// #1467 Phase 2e: same `/* @client */` keyed-map elision (data-table).
|
|
123
146
|
'data-table',
|
|
@@ -296,6 +319,98 @@ export function C() {
|
|
|
296
319
|
// (`$bf.*_eval`), isomorphic with the Go / Mojo `*_eval` helpers. A predicate
|
|
297
320
|
// the evaluator can't model (a method-call predicate) falls back to the Kolon
|
|
298
321
|
// lambda runtime call. Template-text pins guard against silent divergence.
|
|
322
|
+
describe('XslateAdapter - #2075 searchParams()-derived memo seeding', () => {
|
|
323
|
+
test('seeds an aliased scalar derived memo from the canonical reader', () => {
|
|
324
|
+
const { template } = compileAndGenerate(`
|
|
325
|
+
'use client'
|
|
326
|
+
import { createMemo, createSearchParams } from '@barefootjs/client'
|
|
327
|
+
export function SortStatus() {
|
|
328
|
+
const [sp] = createSearchParams()
|
|
329
|
+
const sort = createMemo(() => sp().get('sort') ?? 'date')
|
|
330
|
+
return <p>sort: {sort()}</p>
|
|
331
|
+
}
|
|
332
|
+
`)
|
|
333
|
+
expect(template).toContain(": my $sort = ($searchParams.get('sort') // 'date');")
|
|
334
|
+
})
|
|
335
|
+
|
|
336
|
+
// The Kolon lambda param and the `$bf` runtime object are
|
|
337
|
+
// lowering-internal, not out-of-scope template vars (#2075).
|
|
338
|
+
test('seeds a filter memo chained off the derived memo', () => {
|
|
339
|
+
const { template } = compileAndGenerate(`
|
|
340
|
+
'use client'
|
|
341
|
+
import { createMemo, createSearchParams } from '@barefootjs/client'
|
|
342
|
+
export function TaggedList(props: { items: { title: string; tags: string[] }[] }) {
|
|
343
|
+
const [searchParams] = createSearchParams()
|
|
344
|
+
const tag = createMemo(() => searchParams().get('tag') ?? '')
|
|
345
|
+
const visible = createMemo(() => props.items.filter((p) => !tag() || p.tags.includes(tag())))
|
|
346
|
+
return <ul>{visible().map((p) => <li key={p.title}>{p.title}</li>)}</ul>
|
|
347
|
+
}
|
|
348
|
+
`)
|
|
349
|
+
expect(template).toContain(": my $tag = ($searchParams.get('tag') // '');")
|
|
350
|
+
expect(template).toContain(': my $visible = $bf.filter($items,')
|
|
351
|
+
})
|
|
352
|
+
|
|
353
|
+
// The seed-scope guard used to scan the LOWERED
|
|
354
|
+
// Kolon string, allowing every arrow-callback param tree-wide. That let an
|
|
355
|
+
// outer, unbound `p` (shadowed only inside the callback) slip past the
|
|
356
|
+
// guard as if it were the callback's own bound `$p` — emitting a bogus
|
|
357
|
+
// seed line. The guard now walks the parsed SOURCE tree with proper
|
|
358
|
+
// lexical scoping (`freeIdentifiers`), so this shape seeds nothing and
|
|
359
|
+
// falls back to the null/ssr-defaults path.
|
|
360
|
+
test('an outer unbound `p` shadowed only inside the callback does not seed', () => {
|
|
361
|
+
const { template } = compileAndGenerate(`
|
|
362
|
+
'use client'
|
|
363
|
+
import { createMemo } from '@barefootjs/client'
|
|
364
|
+
export function C(props: { items: { ok: boolean }[] }) {
|
|
365
|
+
const visible = createMemo(() => props.items.filter((p) => p.ok) && p)
|
|
366
|
+
return <div>{String(visible())}</div>
|
|
367
|
+
}
|
|
368
|
+
`)
|
|
369
|
+
expect(template).not.toContain('my $visible')
|
|
370
|
+
})
|
|
371
|
+
|
|
372
|
+
// An out-of-scope bare `_` reference must not seed either — the old
|
|
373
|
+
// unconditional `allowed.add('_')` / `allowed.add('bf')` masked this.
|
|
374
|
+
test('an out-of-scope bare `_` reference does not seed', () => {
|
|
375
|
+
const { template } = compileAndGenerate(`
|
|
376
|
+
'use client'
|
|
377
|
+
import { createMemo } from '@barefootjs/client'
|
|
378
|
+
export function C(props: { count: number }) {
|
|
379
|
+
const doubled = createMemo(() => props.count * 2 + _)
|
|
380
|
+
return <div>{doubled()}</div>
|
|
381
|
+
}
|
|
382
|
+
`)
|
|
383
|
+
expect(template).not.toContain('my $doubled')
|
|
384
|
+
})
|
|
385
|
+
})
|
|
386
|
+
|
|
387
|
+
describe('XslateAdapter - #2073 value-producing .map(cb)', () => {
|
|
388
|
+
// The blog-showcase shape (#1938/#1939): a value-returning `.map` (string
|
|
389
|
+
// projection, not JSX) lowers through the evaluator — `$bf.map_eval`
|
|
390
|
+
// projects each element (no flatten) and composes through `$bf.join`.
|
|
391
|
+
test('.map(t => `#${t}`).join(" ") emits $bf.map_eval composed into $bf.join', () => {
|
|
392
|
+
const { template } = compileAndGenerate(`
|
|
393
|
+
function TagLine({ tags }: { tags: string[] }) {
|
|
394
|
+
return <p>{tags.map((t) => \`#\${t}\`).join(' ')}</p>
|
|
395
|
+
}
|
|
396
|
+
export { TagLine }
|
|
397
|
+
`)
|
|
398
|
+
expect(template).toContain("$bf.join($bf.map_eval($tags,")
|
|
399
|
+
expect(template).toContain('"kind":"template-literal"')
|
|
400
|
+
})
|
|
401
|
+
|
|
402
|
+
test('.map(u => u.name) emits $bf.map_eval with the field projection', () => {
|
|
403
|
+
const { template } = compileAndGenerate(`
|
|
404
|
+
function NameList({ users }: { users: { name: string }[] }) {
|
|
405
|
+
return <div>{users.map((u) => u.name).join(', ')}</div>
|
|
406
|
+
}
|
|
407
|
+
export { NameList }
|
|
408
|
+
`)
|
|
409
|
+
expect(template).toContain('$bf.map_eval($users,')
|
|
410
|
+
expect(template).toContain('"property":"name"')
|
|
411
|
+
})
|
|
412
|
+
})
|
|
413
|
+
|
|
299
414
|
describe('XslateAdapter - higher-order predicate lowering (#2018 P2)', () => {
|
|
300
415
|
test('a serializable predicate lowers to $bf.filter_eval with the JSON body + env', () => {
|
|
301
416
|
// A standalone `.filter().length` exercises the higher-order emitter (the
|
|
@@ -332,17 +447,39 @@ export { A }
|
|
|
332
447
|
expect(findLast).toContain(', 0, {})')
|
|
333
448
|
})
|
|
334
449
|
|
|
335
|
-
test('a
|
|
450
|
+
test('.includes() in a predicate now lowers via the evaluator, not the Kolon-lambda fallback', () => {
|
|
451
|
+
// #2075: `.includes(x)` joined the evaluator's `array-method` surface
|
|
452
|
+
// (shared with the Perl `Evaluator.pm` runtime), so a predicate built
|
|
453
|
+
// from it routes through `$bf.every_eval` like any other pure predicate.
|
|
336
454
|
const { template } = compileAndGenerate(`
|
|
337
455
|
function A({ items }: { items: { name: string }[] }) {
|
|
338
456
|
return <div>{items.every(x => x.name.includes('a')) ? 'y' : 'n'}</div>
|
|
339
457
|
}
|
|
340
458
|
export { A }
|
|
341
459
|
`)
|
|
342
|
-
|
|
343
|
-
|
|
460
|
+
expect(template).toContain('$bf.every_eval(')
|
|
461
|
+
expect(template).toContain('"method":"includes"')
|
|
462
|
+
expect(template).not.toContain('-> $x {')
|
|
463
|
+
})
|
|
464
|
+
|
|
465
|
+
test('a method-call predicate outside the evaluator surface falls back to the Kolon-lambda runtime call', () => {
|
|
466
|
+
const { template } = compileAndGenerate(`
|
|
467
|
+
function A({ items }: { items: { name: string }[] }) {
|
|
468
|
+
return <div>{items.every(x => x.name.toUpperCase() === 'A') ? 'y' : 'n'}</div>
|
|
469
|
+
}
|
|
470
|
+
export { A }
|
|
471
|
+
`)
|
|
472
|
+
// `.toUpperCase()` is outside the evaluator's `array-method` gate (only
|
|
473
|
+
// `includes` is recognized there), so the predicate keeps the
|
|
474
|
+
// `-> $x { … }` lambda form passed to the runtime `$bf.every`.
|
|
344
475
|
expect(template).not.toContain('every_eval')
|
|
345
476
|
expect(template).toContain('$bf.every(')
|
|
346
477
|
expect(template).toContain('-> $x {')
|
|
347
478
|
})
|
|
348
479
|
})
|
|
480
|
+
|
|
481
|
+
// #2038 nested-callback-predicate loudness is pinned at the shared
|
|
482
|
+
// conformance layer: `filter-nested-callback-predicate` /
|
|
483
|
+
// `filter-nested-find-predicate` (BF101 via `expectedDiagnostics` above) and
|
|
484
|
+
// `filter-nested-callback-predicate-client` (the `/* @client */` suppression
|
|
485
|
+
// twin, which must render clean).
|
|
@@ -287,6 +287,27 @@ export function renderFlatMapEval(
|
|
|
287
287
|
return `$bf.flat_map_eval(${recv}, '${escapePerlSingleQuote(json)}', '${param}', ${env})`
|
|
288
288
|
}
|
|
289
289
|
|
|
290
|
+
/**
|
|
291
|
+
* Emit a value-producing `.map(cb)` via the runtime evaluator (#2073): the
|
|
292
|
+
* projection body serializes to JSON and `$bf.map_eval` projects each element,
|
|
293
|
+
* one result per element (no flatten — the JS `.map` contract). Composes
|
|
294
|
+
* through the array-method chain (`.map(cb).join(' ')`). Returns null when
|
|
295
|
+
* the projection is outside the evaluator surface (→ caller refuses with
|
|
296
|
+
* BF101). The JSX-returning `.map` is an IRLoop upstream and never reaches
|
|
297
|
+
* this emit.
|
|
298
|
+
*/
|
|
299
|
+
export function renderMapEval(
|
|
300
|
+
recv: string,
|
|
301
|
+
body: ParsedExpr,
|
|
302
|
+
param: string,
|
|
303
|
+
emit: (e: ParsedExpr) => string,
|
|
304
|
+
): string | null {
|
|
305
|
+
const json = serializeParsedExpr(body)
|
|
306
|
+
if (json === null) return null
|
|
307
|
+
const env = emitEvalEnvArg(body, [param], emit)
|
|
308
|
+
return `$bf.map_eval(${recv}, '${escapePerlSingleQuote(json)}', '${param}', ${env})`
|
|
309
|
+
}
|
|
310
|
+
|
|
290
311
|
/**
|
|
291
312
|
* Shared Kolon emit for `.sort(cmp)` / `.toSorted(cmp)`. Used by both the
|
|
292
313
|
* filter-context emitter and the top-level emitter, plus the loop-array
|
|
@@ -35,6 +35,7 @@ import {
|
|
|
35
35
|
renderPredicateEval,
|
|
36
36
|
renderFlatMethod,
|
|
37
37
|
renderFlatMapEval,
|
|
38
|
+
renderMapEval,
|
|
38
39
|
} from './array-method.ts'
|
|
39
40
|
|
|
40
41
|
/**
|
|
@@ -62,18 +63,20 @@ const PREDICATE_METHODS = new Set<HigherOrderMethod>([
|
|
|
62
63
|
* filters. Higher-order predicates are emitted using Kolon's own scalar
|
|
63
64
|
* comparison operators (which delegate to Perl semantics).
|
|
64
65
|
*
|
|
65
|
-
* NOTE: Kolon has no `grep { } @{...}` form, so nested higher-order
|
|
66
|
-
* (`x.tags.filter(...).
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
* use; richer nested shapes fall back to the helper or surface as BF101 via
|
|
70
|
-
* the top-level emitter.
|
|
66
|
+
* NOTE: Kolon has no `grep { } @{...}` form, so a nested higher-order call
|
|
67
|
+
* (`x.tags.filter(...)`, `other.some(...)`) inside a predicate has no faithful
|
|
68
|
+
* scalar lowering here. Such predicates surface BF101 via `onUnsupported`
|
|
69
|
+
* (#2038) instead of silently degrading to the callback's receiver.
|
|
71
70
|
*/
|
|
72
71
|
export class XslateFilterEmitter implements ParsedExprEmitter {
|
|
73
72
|
constructor(
|
|
74
73
|
private readonly param: string,
|
|
75
74
|
private readonly localVarMap: Map<string, string>,
|
|
76
75
|
private readonly isStringName: (n: string) => boolean = () => false,
|
|
76
|
+
// Records a BF101 for predicate shapes this emitter can only degrade
|
|
77
|
+
// (#2038). Optional so emitter construction stays possible without an
|
|
78
|
+
// adapter; a missing hook keeps the old silent-degrade emit.
|
|
79
|
+
private readonly onUnsupported?: (message: string, reason?: string) => void,
|
|
77
80
|
) {}
|
|
78
81
|
|
|
79
82
|
identifier(name: string): string {
|
|
@@ -155,12 +158,15 @@ export class XslateFilterEmitter implements ParsedExprEmitter {
|
|
|
155
158
|
_restArgs: ParsedExpr[],
|
|
156
159
|
emit: (e: ParsedExpr) => string,
|
|
157
160
|
): string {
|
|
158
|
-
//
|
|
159
|
-
// form
|
|
160
|
-
//
|
|
161
|
-
//
|
|
162
|
-
//
|
|
163
|
-
|
|
161
|
+
// A nested callback method inside a filter predicate has no Kolon scalar
|
|
162
|
+
// form. The pre-#2038 behavior degraded it to its receiver, which silently
|
|
163
|
+
// changes predicate semantics (`!other.some(r => …)` collapses to
|
|
164
|
+
// `!other`), so surface BF101 instead. The receiver emit is kept only so
|
|
165
|
+
// the template stays syntactically valid while the build fails.
|
|
166
|
+
this.onUnsupported?.(
|
|
167
|
+
`Filter predicate contains a nested '.${method}(...)' callback, which has no Kolon scalar form`,
|
|
168
|
+
`Rewrite the predicate without a nested callback method, or add /* @client */ for client-only evaluation (no SSR).`,
|
|
169
|
+
)
|
|
164
170
|
return emit(object)
|
|
165
171
|
}
|
|
166
172
|
|
|
@@ -398,6 +404,18 @@ export class XslateTopLevelEmitter implements ParsedExprEmitter {
|
|
|
398
404
|
return "''"
|
|
399
405
|
}
|
|
400
406
|
|
|
407
|
+
// Value-producing `.map(cb)` (#2073): serialize the projection body +
|
|
408
|
+
// emit `$bf.map_eval`. (The JSX-returning `.map` is an IRLoop upstream.)
|
|
409
|
+
if (method === 'map') {
|
|
410
|
+
const evalForm = renderMapEval(recv, body, params[0], emit)
|
|
411
|
+
if (evalForm !== null) return evalForm
|
|
412
|
+
this.ctx._recordExprBF101(
|
|
413
|
+
`'.map(...)' projection is outside the Xslate adapter's evaluable surface`,
|
|
414
|
+
`Pre-compute the projected array, or move this position to a '/* @client */' boundary.`,
|
|
415
|
+
)
|
|
416
|
+
return "''"
|
|
417
|
+
}
|
|
418
|
+
|
|
401
419
|
// Unknown callback method (should not arrive — CALLBACK_METHODS is closed).
|
|
402
420
|
void object
|
|
403
421
|
return recv
|
|
@@ -51,14 +51,3 @@ export function collectRootScopeNodes(node: IRNode): Set<IRNode> {
|
|
|
51
51
|
visit(node)
|
|
52
52
|
return out
|
|
53
53
|
}
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* True when every `$var` the lowered Kolon expression references is already in
|
|
57
|
-
* scope — guards in-template memo seeding against an out-of-scope binding. (#1297)
|
|
58
|
-
*/
|
|
59
|
-
export function referencedVarsAreAvailable(expr: string, available: ReadonlySet<string>): boolean {
|
|
60
|
-
for (const m of expr.matchAll(/\$([A-Za-z_]\w*)/g)) {
|
|
61
|
-
if (!available.has(m[1])) return false
|
|
62
|
-
}
|
|
63
|
-
return true
|
|
64
|
-
}
|