@barefootjs/xslate 0.17.1 → 0.18.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/index.js CHANGED
@@ -187302,12 +187302,13 @@ import {
187302
187302
  emitAttrValue,
187303
187303
  augmentInheritedPropAccesses,
187304
187304
  collectModuleStringConsts,
187305
- isLowerableObjectRestDestructure,
187306
187305
  lookupStaticRecordLiteral,
187307
187306
  searchParamsLocalNames,
187308
187307
  prepareLoweringMatchers,
187309
187308
  queryHrefArgs,
187310
- sortComparatorFromArrow as sortComparatorFromArrow2
187309
+ isValidHelperId,
187310
+ sortComparatorFromArrow as sortComparatorFromArrow2,
187311
+ isLowerableLoopDestructure
187311
187312
  } from "@barefootjs/jsx";
187312
187313
 
187313
187314
  // src/adapter/boolean-result.ts
@@ -187600,7 +187601,10 @@ function renderSortMethod(recv, c) {
187600
187601
  });
187601
187602
  return `$bf.sort(${recv}, { keys => [${keyHashes.join(", ")}] })`;
187602
187603
  }
187603
- function renderFlatMethod(recv, depth) {
187604
+ function renderFlatMethod(recv, depth, emit) {
187605
+ if (typeof depth === "object") {
187606
+ return `$bf.flat_dynamic(${recv}, ${emit(depth.expr)})`;
187607
+ }
187604
187608
  const d = depth === "infinity" ? -1 : depth;
187605
187609
  return `$bf.flat(${recv}, ${d})`;
187606
187610
  }
@@ -187711,7 +187715,7 @@ class XslateFilterEmitter {
187711
187715
  return renderArrayMethod(method, object, args, emit);
187712
187716
  }
187713
187717
  flatMethod(object, depth, emit) {
187714
- return renderFlatMethod(emit(object), depth);
187718
+ return renderFlatMethod(emit(object), depth, emit);
187715
187719
  }
187716
187720
  conditional(_test, _consequent, _alternate) {
187717
187721
  return "1";
@@ -187911,7 +187915,7 @@ class XslateTopLevelEmitter {
187911
187915
  return renderArrayMethod(method, object, args, emit);
187912
187916
  }
187913
187917
  flatMethod(object, depth, emit) {
187914
- return renderFlatMethod(emit(object), depth);
187918
+ return renderFlatMethod(emit(object), depth, emit);
187915
187919
  }
187916
187920
  conditional(test, consequent, alternate, emit) {
187917
187921
  return `(${emit(test)} ? ${emit(consequent)} : ${emit(alternate)})`;
@@ -187942,8 +187946,8 @@ class XslateTopLevelEmitter {
187942
187946
  unsupported(_raw, _reason) {
187943
187947
  return "''";
187944
187948
  }
187945
- objectLiteral(_properties, _raw, _emit) {
187946
- return "''";
187949
+ objectLiteral(properties, _raw, _emit) {
187950
+ return properties.length === 0 ? "{}" : "''";
187947
187951
  }
187948
187952
  }
187949
187953
 
@@ -188174,6 +188178,17 @@ function collectStringValueNames(ir) {
188174
188178
  }
188175
188179
 
188176
188180
  // src/adapter/xslate-adapter.ts
188181
+ function kolonSegmentAccessor(base, segments) {
188182
+ let expr = base;
188183
+ for (const seg of segments) {
188184
+ expr += seg.kind === "field" ? seg.isIdent ? `.${seg.key}` : `[${kolonHashKey(seg.key)}]` : `[${seg.index}]`;
188185
+ }
188186
+ return expr;
188187
+ }
188188
+ function kolonStringLiteral(s) {
188189
+ return `'${escapeKolonSingleQuoted(s)}'`;
188190
+ }
188191
+
188177
188192
  class XslateAdapter extends BaseAdapter {
188178
188193
  name = "xslate";
188179
188194
  extension = ".tx";
@@ -188442,21 +188457,37 @@ ${whenTrue}
188442
188457
  return `<: $bf.comment("loop:${loop.markerId}") | mark_raw :><: $bf.comment("/loop:${loop.markerId}") | mark_raw :>`;
188443
188458
  }
188444
188459
  const destructure = !!(loop.paramBindings && loop.paramBindings.length > 0);
188445
- const supportableDestructure = destructure && isLowerableObjectRestDestructure(loop);
188460
+ const supportableDestructure = destructure && isLowerableLoopDestructure(loop);
188446
188461
  if (destructure && !supportableDestructure) {
188447
188462
  this.errors.push({
188448
188463
  code: "BF104",
188449
188464
  severity: "error",
188450
- message: `Loop callback uses an array/object destructure pattern (\`${loop.param}\`) that the Xslate adapter cannot lower — Kolon \`for LIST -> $item\` binds a single scalar and can't unpack a tuple.`,
188465
+ message: `Loop callback uses a destructure pattern (\`${loop.param}\`) that the Xslate adapter cannot lower — see the diagnostic detail for the specific shape.`,
188451
188466
  loc: loop.loc ?? { file: this.componentName + ".tsx", start: { line: 1, column: 0 }, end: { line: 1, column: 0 } },
188452
188467
  suggestion: {
188453
188468
  message: `Options:
188454
- ` + ` 1. Rename the parameter to a single name and access tuple elements with index syntax in the body (e.g. \`entry => entry[0]\` instead of \`([k, v]) => ...\`).
188455
- ` + ` 2. Mark the loop position as @client-only so the destructure runs in JS on the client.
188456
- ` + ` 3. Move the loop into a primitive that the adapter registers explicitly.`
188469
+ ` + ` 1. If this is an object-rest binding (\`{ ...rest }\`), only reading \`rest.field\` or spreading \`{...rest}\` onto an intrinsic element lowers — other uses (passing \`rest\` to a function, rendering it as text) need the client runtime.
188470
+ ` + ` 2. If this is chained \`.filter().map(({ ... }) => ...)\`, hoist the destructure into a variable inside the callback body instead.
188471
+ ` + ` 3. Mark the loop position as @client-only so the destructure runs in JS on the client.
188472
+ ` + ` 4. Move the loop into a primitive that the adapter registers explicitly.`
188457
188473
  }
188458
188474
  });
188459
188475
  }
188476
+ const arrayName = loop.array.trim();
188477
+ if (/^[A-Za-z_$][\w$]*$/.test(arrayName)) {
188478
+ const arrayConst = (this.localConstants ?? []).find((c) => c.name === arrayName);
188479
+ if (arrayConst && !arrayConst.isModule && this._resolveLiteralConst(arrayName) === null) {
188480
+ this.errors.push({
188481
+ code: "BF101",
188482
+ severity: "error",
188483
+ message: `Loop array \`${arrayName}\` is a local computed value (\`${arrayConst.value}\`) that the Xslate adapter cannot bind as a template variable — only numeric/string-literal locals inline at their use site.`,
188484
+ loc: loop.loc ?? { file: this.componentName + ".tsx", start: { line: 1, column: 0 }, end: { line: 1, column: 0 } },
188485
+ suggestion: {
188486
+ message: "Pre-compute the array server-side and pass it as a prop, or mark the loop position as @client-only so it runs in JS on the client."
188487
+ }
188488
+ });
188489
+ }
188490
+ }
188460
188491
  const rawArray = this.convertExpressionToKolon(loop.array);
188461
188492
  let array = rawArray;
188462
188493
  if (loop.sortComparator) {
@@ -188478,7 +188509,15 @@ ${whenTrue}
188478
188509
  }
188479
188510
  if (supportableDestructure) {
188480
188511
  for (const b of loop.paramBindings ?? []) {
188481
- indexLocalLines.push(b.rest ? `: my $${b.name} = $${loopVar};` : `: my $${b.name} = $${loopVar}${b.path};`);
188512
+ const parent = kolonSegmentAccessor(`$${loopVar}`, b.segments ?? []);
188513
+ if (b.rest?.kind === "object") {
188514
+ const exclude = b.rest.exclude.map((k) => kolonStringLiteral(k.key)).join(", ");
188515
+ indexLocalLines.push(`: my $${b.name} = $bf.omit(${parent}, [${exclude}]);`);
188516
+ } else if (b.rest?.kind === "array") {
188517
+ indexLocalLines.push(`: my $${b.name} = $bf.slice(${parent}, ${b.rest.from}, nil);`);
188518
+ } else {
188519
+ indexLocalLines.push(`: my $${b.name} = ${kolonSegmentAccessor(`$${loopVar}`, b.segments ?? [])};`);
188520
+ }
188482
188521
  }
188483
188522
  }
188484
188523
  const prevInLoop = this.inLoop;
@@ -188534,8 +188573,32 @@ ${childrenUnderLoop}` : childrenUnderLoop;
188534
188573
  emitBooleanShorthand: (_value, name) => `${kolonHashKey(name)} => 1`,
188535
188574
  emitJsxChildren: () => ""
188536
188575
  };
188576
+ componentPropSegmentEntries(segments) {
188577
+ const last = segments[segments.length - 1];
188578
+ if (last && last.kind === "entries")
188579
+ return last.parts;
188580
+ const seg = { kind: "entries", parts: [] };
188581
+ segments.push(seg);
188582
+ return seg.parts;
188583
+ }
188584
+ combineComponentPropSegments(segments) {
188585
+ let acc = null;
188586
+ for (const seg of segments) {
188587
+ if (seg.kind === "entries") {
188588
+ if (seg.parts.length === 0)
188589
+ continue;
188590
+ const text = `{ ${seg.parts.join(", ")} }`;
188591
+ acc = acc === null ? text : `${acc}.merge(${text})`;
188592
+ } else {
188593
+ const text = `(${seg.expr} // {})`;
188594
+ acc = acc === null ? text : `${acc}.merge(${text})`;
188595
+ }
188596
+ }
188597
+ return acc ?? "{}";
188598
+ }
188537
188599
  renderComponent(comp) {
188538
- const propParts = [];
188600
+ const segments = [{ kind: "entries", parts: [] }];
188601
+ const currentEntries = () => this.componentPropSegmentEntries(segments);
188539
188602
  for (const p of comp.props) {
188540
188603
  if ((p.name.match(/^on[A-Z]/) || p.name === "ref") && p.value.kind === "expression")
188541
188604
  continue;
@@ -188543,27 +188606,19 @@ ${childrenUnderLoop}` : childrenUnderLoop;
188543
188606
  const trimmed = p.value.expr.trim();
188544
188607
  if (this.propsObjectName && this.propsObjectName === trimmed) {
188545
188608
  for (const pp of this.propsParams) {
188546
- propParts.push(`${pp.name} => $${pp.name}`);
188609
+ currentEntries().push(`${pp.name} => $${pp.name}`);
188547
188610
  }
188548
188611
  continue;
188549
188612
  }
188550
- this.errors.push({
188551
- code: "BF101",
188552
- severity: "error",
188553
- message: `Spread props (\`{...${trimmed}}\`) on a child component cannot be lowered to Kolon — Kolon hashref method args can't splat a runtime hash into named entries.`,
188554
- loc: comp.loc ?? { file: this.componentName + ".tsx", start: { line: 1, column: 0 }, end: { line: 1, column: 0 } },
188555
- suggestion: {
188556
- message: "Pass the child component its props explicitly rather than spreading a runtime object."
188557
- }
188558
- });
188613
+ segments.push({ kind: "spread", expr: this.convertExpressionToKolon(p.value.expr) });
188559
188614
  continue;
188560
188615
  }
188561
188616
  const lowered = emitAttrValue(p.value, this.componentPropEmitter, p.name);
188562
188617
  if (lowered)
188563
- propParts.push(lowered);
188618
+ currentEntries().push(lowered);
188564
188619
  }
188565
188620
  if (comp.slotId && !this.inLoop) {
188566
- propParts.push(`_bf_slot => '${comp.slotId}'`);
188621
+ currentEntries().push(`_bf_slot => '${comp.slotId}'`);
188567
188622
  }
188568
188623
  const tplName = this.toTemplateName(comp.name);
188569
188624
  const effectiveChildren = comp.children.length > 0 ? comp.children : resolveJsxChildrenProp(comp.props);
@@ -188573,11 +188628,12 @@ ${childrenUnderLoop}` : childrenUnderLoop;
188573
188628
  const childrenBody = this.renderChildren(effectiveChildren);
188574
188629
  this.inLoop = prevInLoop;
188575
188630
  const macroName = `bf_children_${comp.slotId ?? "c" + this.childrenCaptureCounter++}`;
188576
- const childrenEntry = `children => ${macroName}()`;
188577
- const allParts = [...propParts, childrenEntry];
188578
- return `<: macro ${macroName} -> () { :>${childrenBody}<: } :><: $bf.render_child('${tplName}', { ${allParts.join(", ")} }) | mark_raw :>`;
188631
+ currentEntries().push(`children => ${macroName}()`);
188632
+ const dict = this.combineComponentPropSegments(segments);
188633
+ return `<: macro ${macroName} -> () { :>${childrenBody}<: } :><: $bf.render_child('${tplName}', ${dict}) | mark_raw :>`;
188579
188634
  }
188580
- const hashEntries = propParts.length > 0 ? `, { ${propParts.join(", ")} }` : "";
188635
+ const isEmpty = segments.every((s) => s.kind === "entries" && s.parts.length === 0);
188636
+ const hashEntries = isEmpty ? "" : `, ${this.combineComponentPropSegments(segments)}`;
188581
188637
  return `<: $bf.render_child('${tplName}'${hashEntries}) | mark_raw :>`;
188582
188638
  }
188583
188639
  childrenCaptureCounter = 0;
@@ -188852,6 +188908,10 @@ ${reason}` : "";
188852
188908
  const qArgs = queryHrefArgs(node, (n) => this.renderParsedExprToKolon(n));
188853
188909
  return `$bf.query(${qArgs.join(", ")})`;
188854
188910
  }
188911
+ if (node?.kind === "helper-call" && isValidHelperId(node.helper)) {
188912
+ const argsX = node.args.map((a) => this.renderParsedExprToKolon(a));
188913
+ return `$bf.${node.helper}(${argsX.join(", ")})`;
188914
+ }
188855
188915
  }
188856
188916
  }
188857
188917
  const support = isSupported(parsed);
@@ -188951,7 +189011,26 @@ Options:
188951
189011
  }
188952
189012
  }
188953
189013
  var xslateAdapter = new XslateAdapter;
189014
+ // src/conformance-pins.ts
189015
+ var conformancePins = {
189016
+ "static-array-children": [{ code: "BF103", severity: "error" }],
189017
+ "todo-app": [{ code: "BF103", severity: "error" }],
189018
+ "todo-app-ssr": [{ code: "BF103", severity: "error" }],
189019
+ "static-array-from-props": [{ code: "BF101", severity: "error" }],
189020
+ "static-array-from-props-with-component": [
189021
+ { code: "BF103", severity: "error" },
189022
+ { code: "BF101", severity: "error" }
189023
+ ],
189024
+ "filter-nested-callback-predicate": [
189025
+ { code: "BF101", severity: "error", issue: "https://github.com/piconic-ai/barefootjs/issues/2038" }
189026
+ ],
189027
+ "filter-nested-find-predicate": [
189028
+ { code: "BF101", severity: "error", issue: "https://github.com/piconic-ai/barefootjs/issues/2038" }
189029
+ ],
189030
+ "array-map-function-reference": [{ code: "BF101", severity: "error" }]
189031
+ };
188954
189032
  export {
188955
189033
  xslateAdapter,
189034
+ conformancePins,
188956
189035
  XslateAdapter
188957
189036
  };
@@ -1,5 +1,5 @@
1
1
  package BarefootJS::Backend::Xslate;
2
- our $VERSION = "0.17.0";
2
+ our $VERSION = "0.18.0";
3
3
  use strict;
4
4
  use warnings;
5
5
  use utf8;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@barefootjs/xslate",
3
- "version": "0.17.1",
3
+ "version": "0.18.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.1"
58
+ "@barefootjs/shared": "0.18.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.1",
65
+ "@barefootjs/jsx": "0.18.1",
66
66
  "typescript": "^5.0.0"
67
67
  }
68
68
  }
@@ -13,13 +13,11 @@
13
13
  */
14
14
 
15
15
  import { describe, test, expect } from 'bun:test'
16
- import {
17
- runAdapterConformanceTests,
18
- TemplatePrimitiveCaseId,
19
- } from '@barefootjs/adapter-tests'
16
+ import { runAdapterConformanceTests } from '@barefootjs/adapter-tests'
20
17
  import { XslateAdapter } from '../adapter'
21
18
  import { renderXslateComponent, XslateNotAvailableError } from '../test-render'
22
19
  import { compileJSX, type ComponentIR } from '@barefootjs/jsx'
20
+ import { conformancePins } from '../conformance-pins'
23
21
 
24
22
  runAdapterConformanceTests({
25
23
  name: 'xslate',
@@ -33,105 +31,17 @@ runAdapterConformanceTests({
33
31
  // body children (TableCell) now receive `_bf_slot` for deterministic
34
32
  // parent-scope-derived IDs matching Hono.
35
33
  // Per-fixture build-time contracts for shapes the Xslate adapter
36
- // intentionally refuses to lower. Mirrors mojo's set the lowering
37
- // gates are shared code paths in the ported adapter.
38
- expectedDiagnostics: {
39
- // Sibling-imported child component in a loop body: emits a
40
- // cross-template call needing separate registration. BF103 makes
41
- // the requirement loud (same as mojo).
42
- 'static-array-children': [{ code: 'BF103', severity: 'error' }],
43
- // TodoApp / TodoAppSSR import `TodoItem` from a sibling file and
44
- // call it inside a keyed `.map`. With the standalone-filter fix in
45
- // place these reach the SAME BF103 (imported child in `.map`) as
46
- // mojo NOT BF101 — confirming the `.filter(...)` chain itself now
47
- // lowers and the only remaining gate is the imported-child one.
48
- 'todo-app': [{ code: 'BF103', severity: 'error' }],
49
- 'todo-app-ssr': [{ code: 'BF103', severity: 'error' }],
50
- // Array-destructure loop param (`([k, v]) => …`) can't lower to a
51
- // single Kolon loop variable (same BF104 as mojo).
52
- 'static-array-from-props': [{ code: 'BF104', severity: 'error' }],
53
- // Both BF103 (imported child) and BF104 (destructure) fire.
54
- 'static-array-from-props-with-component': [
55
- { code: 'BF103', severity: 'error' },
56
- { code: 'BF104', severity: 'error' },
57
- ],
58
- // Rest-destructure `.map()` callbacks — the object-rest shape read via
59
- // member access (`rest-destructure-object-in-map`) now lowers via Kolon
60
- // `: my` binding locals (`$rest` aliases the item). The other three stay
61
- // refused: rest SPREAD needs a residual object, array-index / nested paths
62
- // can't unpack a tuple (same surface as mojo).
63
- 'rest-destructure-object-spread-in-map': [{ code: 'BF104', severity: 'error' }],
64
- 'rest-destructure-array-in-map': [{ code: 'BF104', severity: 'error' }],
65
- 'rest-destructure-nested-in-map': [{ code: 'BF104', severity: 'error' }],
66
- // XSLATE-SPECIFIC (mojo passes this): the site/ui Button auto-infers a
67
- // `<Slot>` sibling that spreads `{...props}` / `{...children.props}`
68
- // onto its root element. Kolon hashref method args can't splat a
69
- // runtime hash into named entries (no `%$h`-into-call-args form), so
70
- // the adapter refuses the spread with BF101 rather than emit a broken
71
- // render_child call. Mojo's EP `%= include` accepts a flat stash, so it
72
- // lowers the same shape; this is a genuine engine divergence, pinned
73
- // declaratively here.
74
- 'button': [{ code: 'BF101', severity: 'error' }],
75
- // `kbd` auto-infers the same `<Slot>` `{...props}` spread as `button`
76
- // above — refused with BF101 for the identical Kolon engine reason, not a
77
- // render-mismatch (so it's pinned here, not in `skipJsx`).
78
- 'kbd': [{ code: 'BF101', severity: 'error' }],
79
- // #1467 demo-corpus context providers (`radio-group`, `select`,
80
- // `dropdown-menu`, `combobox`, `command`) are no longer pinned — an
81
- // object-literal provider value (`{ value: currentValue,
82
- // onValueChange: (v) => {…} }`) lowers to a Kolon hashref via
83
- // `parseProviderObjectLiteral` (#1897): getter members snapshot
84
- // their body's SSR value, handler / function-shaped members lower
85
- // to `nil`. The command demo's `ref={(el) => {…}}` function prop on
86
- // an imported component is skipped at SSR like `on*` handlers.
87
- //
88
- // #1467 Phase 2e: `data-table` is no longer pinned here — it
89
- // compiles clean now (`selected()[index]` → `index-access`,
90
- // `.toFixed(2)` → `$bf.to_fixed`, `/* @client */` memo SSR-folded)
91
- // and renders to Hono parity on real Text::Xslate. The keyed-loop
92
- // scope-ID divergence (#1896) was fixed by the body-children
93
- // `inLoop` reset (loop-item children get `_bf_slot`); data-table is
94
- // off `skipJsx` entirely and only kept in `skipMarkerConformance`
95
- // below for the shared `/* @client */` keyed-map slot-id elision
96
- // contract (same as `todo-app`), not a render or BF101 gap.
97
- // `style-3-signals` / `style-object-dynamic` no longer pinned — a
98
- // `style={{ … }}` object literal now lowers to a CSS string with dynamic
99
- // values interpolated (`background-color:<: $color :>;padding:8px`) via
100
- // `tryLowerStyleObject` (#1322).
101
- // Tagged-template-literal call in a className — same family, same
102
- // refusal (BF101).
103
- 'tagged-template-classname': [{ code: 'BF101', severity: 'error' }],
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' }],
127
- },
128
- // Template-primitive registry parity: same V1 surface as mojo, so the
129
- // same two cases stay skipped (bespoke user import + customSerialize
130
- // can't render server-side without user-supplied helper mappings).
131
- skipTemplatePrimitives: new Set([
132
- TemplatePrimitiveCaseId.USER_IMPORT_VIA_CONST,
133
- TemplatePrimitiveCaseId.NO_DOUBLE_REWRITE_OF_PROPS_OBJECT,
134
- ]),
34
+ // intentionally refuses to lower. Lives in `../conformance-pins`
35
+ // mirrors mojo's set (the lowering gates are shared code paths in
36
+ // the ported adapter).
37
+ expectedDiagnostics: conformancePins,
38
+ // Template-primitive registry: `USER_IMPORT_VIA_CONST` and
39
+ // `NO_DOUBLE_REWRITE_OF_PROPS_OBJECT` now pass (#2069) a bespoke user
40
+ // import can never be added to the string-keyed registry, but the
41
+ // shared `RelocateEnv.loweringMatchers` acceptance path recognises it
42
+ // via a `LoweringPlugin` the case setup registers around the compile
43
+ // (see `packages/adapter-tests/src/cases/template-primitives.ts`). No
44
+ // skips left, so `skipTemplatePrimitives` is omitted entirely.
135
45
  // `client-only` / `client-only-loop-with-sibling-cond` /
136
46
  // `filter-nested-callback-predicate-client` are no longer skipped —
137
47
  // `renderLoop` now emits the `$bf.comment("loop:<id>")` boundary pair
@@ -326,7 +326,28 @@ export function renderSortMethod(recv: string, c: SortComparator): string {
326
326
  }
327
327
 
328
328
  // `.flat(depth?)` → `$bf.flat($recv, $depth)`.
329
- export function renderFlatMethod(recv: string, depth: FlatDepth): string {
329
+ export function renderFlatMethod(
330
+ recv: string,
331
+ depth: FlatDepth | { expr: ParsedExpr },
332
+ emit: (e: ParsedExpr) => string,
333
+ ): string {
334
+ if (typeof depth === 'object') {
335
+ // Dynamic depth (#2094): routed to a SEPARATE runtime helper
336
+ // (`$bf.flat_dynamic`), not `$bf.flat`. The literal-depth path below
337
+ // already uses `-1` as a compile-time SENTINEL baked into the template
338
+ // source meaning "the source literally said `Infinity`". A genuinely
339
+ // dynamic depth that happens to evaluate to `-1` at render time means
340
+ // the OPPOSITE in real JS (`[1,[2]].flat(-1)` never recurses — same as
341
+ // `.flat(0)`). Since both paths would otherwise hand the same
342
+ // literal-looking argument to one shared helper, that helper couldn't
343
+ // tell which case it's in — so `flat_dynamic` performs its own
344
+ // `ToIntegerOrInfinity`-style coercion (truncate toward zero; negative
345
+ // -> 0; NaN/non-numeric -> 0; +Infinity or a huge finite value ->
346
+ // flatten fully) and then delegates to `flat`. See `sub flat_dynamic`
347
+ // in BarefootJS.pm and the Go reference (`FlatDynamicDepth`/
348
+ // `coerceFlatDepth` in bf.go).
349
+ return `$bf.flat_dynamic(${recv}, ${emit(depth.expr)})`
350
+ }
330
351
  const d = depth === 'infinity' ? -1 : depth
331
352
  return `$bf.flat(${recv}, ${d})`
332
353
  }
@@ -183,8 +183,12 @@ export class XslateFilterEmitter implements ParsedExprEmitter {
183
183
  return renderArrayMethod(method, object, args, emit)
184
184
  }
185
185
 
186
- flatMethod(object: ParsedExpr, depth: FlatDepth, emit: (e: ParsedExpr) => string): string {
187
- return renderFlatMethod(emit(object), depth)
186
+ flatMethod(
187
+ object: ParsedExpr,
188
+ depth: FlatDepth | { expr: ParsedExpr },
189
+ emit: (e: ParsedExpr) => string,
190
+ ): string {
191
+ return renderFlatMethod(emit(object), depth, emit)
188
192
  }
189
193
 
190
194
  conditional(_test: ParsedExpr, _consequent: ParsedExpr, _alternate: ParsedExpr): string {
@@ -486,8 +490,12 @@ export class XslateTopLevelEmitter implements ParsedExprEmitter {
486
490
  return renderArrayMethod(method, object, args, emit)
487
491
  }
488
492
 
489
- flatMethod(object: ParsedExpr, depth: FlatDepth, emit: (e: ParsedExpr) => string): string {
490
- return renderFlatMethod(emit(object), depth)
493
+ flatMethod(
494
+ object: ParsedExpr,
495
+ depth: FlatDepth | { expr: ParsedExpr },
496
+ emit: (e: ParsedExpr) => string,
497
+ ): string {
498
+ return renderFlatMethod(emit(object), depth, emit)
491
499
  }
492
500
 
493
501
  conditional(
@@ -537,12 +545,16 @@ export class XslateTopLevelEmitter implements ParsedExprEmitter {
537
545
  return "''"
538
546
  }
539
547
 
540
- objectLiteral(_properties: ObjectLiteralProperty[], _raw: string, _emit: (e: ParsedExpr) => string): string {
541
- // Mirror `unsupported`: a bare object literal reaching the dispatcher
542
- // lowers to the safe empty-string literal, exactly as before the
543
- // `object-literal` kind existed (byte-identical; Roadmap A-1). Object
544
- // values that round-trip to a Kolon hashref go through the dedicated
545
- // `objectLiteralToKolonHashref` lowering in the conditional/attr paths.
546
- return "''"
548
+ objectLiteral(properties: ObjectLiteralProperty[], _raw: string, _emit: (e: ParsedExpr) => string): string {
549
+ // The shared `isSupported` gate only ever lets this dispatcher see an
550
+ // object literal as the EMPTY (`?? {}`) fallback operand of `??`
551
+ // (expression-parser.ts, `logical` case) any other object literal is
552
+ // refused before reaching here. Emit Kolon's real empty hashref
553
+ // literal, matching the `'{}'` convention `objectLiteralToKolonHashref`
554
+ // already uses for the zero-property case in the spread path. A
555
+ // populated literal is structurally unreachable given the gate, but
556
+ // still degrades safely to the pre-existing empty-string sentinel
557
+ // rather than silently dropping keys.
558
+ return properties.length === 0 ? '{}' : "''"
547
559
  }
548
560
  }