@barefootjs/rust 0.18.5 → 0.18.7

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.
@@ -187308,7 +187308,13 @@ import {
187308
187308
  prepareLoweringMatchers,
187309
187309
  queryHrefArgs,
187310
187310
  isValidHelperId,
187311
- sortComparatorFromArrow as sortComparatorFromArrow2
187311
+ sortComparatorFromArrow as sortComparatorFromArrow2,
187312
+ isDangerousInnerHtmlAttr,
187313
+ resolveDangerousInnerHtml,
187314
+ dangerousInnerHtmlMetacharViolation,
187315
+ dangerousInnerHtmlDiagnostic,
187316
+ resolveStaticLoopSource,
187317
+ collectLoopBoundNames
187312
187318
  } from "@barefootjs/jsx";
187313
187319
 
187314
187320
  // src/adapter/boolean-result.ts
@@ -187686,6 +187692,39 @@ function renderFlatMethod(recv, depth, emit) {
187686
187692
  return `bf.flat(${recv}, ${d})`;
187687
187693
  }
187688
187694
 
187695
+ // src/adapter/lib/static-value.ts
187696
+ function staticValueToMinijinja(value) {
187697
+ if (value === null || value === undefined)
187698
+ return "none";
187699
+ if (typeof value === "boolean")
187700
+ return value ? "true" : "false";
187701
+ if (typeof value === "number")
187702
+ return String(value);
187703
+ if (typeof value === "string")
187704
+ return `'${escapeMinijinjaSingleQuoted(value)}'`;
187705
+ if (Array.isArray(value)) {
187706
+ const items = [];
187707
+ for (const el of value) {
187708
+ const serialized = staticValueToMinijinja(el);
187709
+ if (serialized === null)
187710
+ return null;
187711
+ items.push(serialized);
187712
+ }
187713
+ return `[${items.join(", ")}]`;
187714
+ }
187715
+ if (typeof value === "object") {
187716
+ const entries = [];
187717
+ for (const [key, val] of Object.entries(value)) {
187718
+ const serialized = staticValueToMinijinja(val);
187719
+ if (serialized === null)
187720
+ return null;
187721
+ entries.push(`${minijinjaHashKey(key)}: ${serialized}`);
187722
+ }
187723
+ return `{${entries.join(", ")}}`;
187724
+ }
187725
+ return null;
187726
+ }
187727
+
187689
187728
  // src/adapter/expr/emitters.ts
187690
187729
  import {
187691
187730
  groupBinaryOperand,
@@ -188276,6 +188315,7 @@ class MinijinjaAdapter extends BaseAdapter {
188276
188315
  _searchParamsLocals = new Set;
188277
188316
  _loweringMatchers = [];
188278
188317
  localConstants = [];
188318
+ staticLoopSourceBoundNames = new Set;
188279
188319
  nullableOptionalProps = new Set;
188280
188320
  constructor(options = {}) {
188281
188321
  super();
@@ -188291,6 +188331,7 @@ class MinijinjaAdapter extends BaseAdapter {
188291
188331
  this.propsParams = ir.metadata.propsParams.map((p) => ({ name: p.name }));
188292
188332
  this.booleanTypedProps = collectBooleanTypedProps(ir);
188293
188333
  this.localConstants = ir.metadata.localConstants ?? [];
188334
+ this.staticLoopSourceBoundNames = collectLoopBoundNames(ir);
188294
188335
  this.nullableOptionalProps = collectNullableOptionalProps(ir);
188295
188336
  this.stringValueNames = collectStringValueNames(ir);
188296
188337
  this.moduleStringConsts = collectModuleStringConsts(ir.metadata.localConstants);
@@ -188412,7 +188453,8 @@ class MinijinjaAdapter extends BaseAdapter {
188412
188453
  renderElement(element) {
188413
188454
  const tag = element.tag;
188414
188455
  const attrs = this.renderAttributes(element);
188415
- const children = this.renderChildren(element.children);
188456
+ const dangerousHtml = this.renderDangerousInnerHtml(element);
188457
+ const children = dangerousHtml !== null ? dangerousHtml : this.renderChildren(element.children);
188416
188458
  let hydrationAttrs = "";
188417
188459
  if (element.needsScope) {
188418
188460
  hydrationAttrs += ` ${this.renderScopeMarker("")}`;
@@ -188447,6 +188489,22 @@ class MinijinjaAdapter extends BaseAdapter {
188447
188489
  }
188448
188490
  return `<${tag}${attrs}${hydrationAttrs}>${children}</${tag}>`;
188449
188491
  }
188492
+ renderDangerousInnerHtml(element) {
188493
+ const resolution = resolveDangerousInnerHtml(element);
188494
+ if (!resolution)
188495
+ return null;
188496
+ if (resolution.kind === "dynamic") {
188497
+ this.errors.push(dangerousInnerHtmlDiagnostic(resolution.expr, resolution.loc));
188498
+ return "";
188499
+ }
188500
+ const violation = dangerousInnerHtmlMetacharViolation(resolution.html, this.name);
188501
+ if (violation) {
188502
+ const attr = element.attrs.find(isDangerousInnerHtmlAttr);
188503
+ this.errors.push(dangerousInnerHtmlDiagnostic(`{ __html: ${JSON.stringify(resolution.html)} }`, attr.loc, violation));
188504
+ return "";
188505
+ }
188506
+ return resolution.html;
188507
+ }
188450
188508
  renderExpression(expr) {
188451
188509
  if (expr.clientOnly) {
188452
188510
  if (expr.slotId) {
@@ -188454,7 +188512,7 @@ class MinijinjaAdapter extends BaseAdapter {
188454
188512
  }
188455
188513
  return "";
188456
188514
  }
188457
- const jinjaExpr = `bf.string(${this.convertExpressionToJinja(expr.expr)})`;
188515
+ const jinjaExpr = `bf.string(${this.convertExpressionToJinja(expr.expr, expr.parsed)})`;
188458
188516
  if (expr.slotId) {
188459
188517
  return `{{ bf.text_start("${expr.slotId}") | safe }}{{ ${jinjaExpr} }}{{ bf.text_end() | safe }}`;
188460
188518
  }
@@ -188545,8 +188603,12 @@ ${whenTrue}
188545
188603
  }
188546
188604
  });
188547
188605
  }
188606
+ const staticItems = resolveStaticLoopSource(loop.arrayParsed, this.localConstants, {
188607
+ isNameShadowed: (name) => this.staticLoopSourceBoundNames.has(name)
188608
+ });
188609
+ const staticArray = staticItems !== null ? staticValueToMinijinja(staticItems) : null;
188548
188610
  const arrayName = loop.array.trim();
188549
- if (/^[A-Za-z_$][\w$]*$/.test(arrayName)) {
188611
+ if (staticArray === null && /^[A-Za-z_$][\w$]*$/.test(arrayName)) {
188550
188612
  const arrayConst = (this.localConstants ?? []).find((c) => c.name === arrayName);
188551
188613
  if (arrayConst && !arrayConst.isModule && this._resolveLiteralConst(arrayName) === null) {
188552
188614
  this.errors.push({
@@ -188560,7 +188622,7 @@ ${whenTrue}
188560
188622
  });
188561
188623
  }
188562
188624
  }
188563
- const rawArray = this.convertExpressionToJinja(loop.array);
188625
+ const rawArray = staticArray ?? this.convertExpressionToJinja(loop.array);
188564
188626
  let array = rawArray;
188565
188627
  if (loop.sortComparator) {
188566
188628
  const sort = loop.sortComparator;
@@ -188872,6 +188934,8 @@ ${name}="{{ bf.string(${val}) }}"
188872
188934
  for (const attr of element.attrs) {
188873
188935
  if (attr.clientOnly)
188874
188936
  continue;
188937
+ if (isDangerousInnerHtmlAttr(attr))
188938
+ continue;
188875
188939
  let attrName;
188876
188940
  if (attr.name === "className")
188877
188941
  attrName = "class";
@@ -189071,6 +189135,8 @@ Options:
189071
189135
  return isBooleanResultExpr(expr) || isAriaBooleanAttr(name) || this.isBooleanTypedPropRef(expr);
189072
189136
  }
189073
189137
  _resolveLiteralConst(name) {
189138
+ if (this.staticLoopSourceBoundNames.has(name))
189139
+ return null;
189074
189140
  const c = (this.localConstants ?? []).find((lc) => lc.name === name);
189075
189141
  if (c?.value === undefined)
189076
189142
  return null;
@@ -189083,6 +189149,8 @@ Options:
189083
189149
  return null;
189084
189150
  }
189085
189151
  _resolveStaticRecordLiteral(objectName, key) {
189152
+ if (this.staticLoopSourceBoundNames.has(objectName))
189153
+ return null;
189086
189154
  const hit = lookupStaticRecordLiteral(objectName, key, this.localConstants);
189087
189155
  if (!hit)
189088
189156
  return null;
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Serialize a compile-time-evaluated JS value (`@barefootjs/jsx`'s
3
+ * `evaluateStaticLiteral`/`resolveStaticLoopSource`, #2208) into a native
4
+ * MiniJinja literal. Used to inline a fully-static loop source (an inline
5
+ * array literal, or a function-scope local const with a static
6
+ * initializer) directly in a `{% for %}` header, rather than requiring a
7
+ * bound template variable.
8
+ *
9
+ * Returns `null` for a value this adapter can't represent as a literal —
10
+ * the caller falls back to its existing BF101 refusal instead of guessing.
11
+ */
12
+ export declare function staticValueToMinijinja(value: unknown): string | null;
13
+ //# sourceMappingURL=static-value.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"static-value.d.ts","sourceRoot":"","sources":["../../../src/adapter/lib/static-value.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAIH,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAwBpE"}
@@ -193,6 +193,16 @@ export declare class MinijinjaAdapter extends BaseAdapter implements IRNodeEmitt
193
193
  * const (`const sizeAttrs = size ? {…} : {}`) to its initializer text.
194
194
  */
195
195
  private localConstants;
196
+ /**
197
+ * Every name a `.map()`/`.filter()` loop callback binds as its item/index
198
+ * parameter anywhere in the component (#2208 fable review). A static
199
+ * loop-SOURCE name (e.g. a function-scope `const items = [...]`) must
200
+ * never resolve through `resolveStaticLoopSource` at a use site where a
201
+ * DIFFERENT, enclosing loop's own callback param shadows it — same
202
+ * shadowing hazard, and same coarse-but-safe mitigation, as #2212's
203
+ * `collectLoopBoundNames` use in `collectStringValueNames`.
204
+ */
205
+ private staticLoopSourceBoundNames;
196
206
  /**
197
207
  * Optional, no-default props that are `None` when the caller omits them.
198
208
  * Their bare-reference attribute emission is guarded with a Jinja
@@ -244,6 +254,13 @@ export declare class MinijinjaAdapter extends BaseAdapter implements IRNodeEmitt
244
254
  private providerObjectLiteralJinja;
245
255
  emitAsync(node: IRAsync, _ctx: JinjaRenderCtx, _emit: EmitIRNode<JinjaRenderCtx>): string;
246
256
  renderElement(element: IRElement): string;
257
+ /**
258
+ * `dangerouslySetInnerHTML={{ __html: '...' }}` (#2207) — see the Blade
259
+ * adapter's identical helper for the full rationale. `null` means the
260
+ * attribute is absent (caller falls through to normal `renderChildren`);
261
+ * a non-`null` string (possibly `''`) replaces the children outright.
262
+ */
263
+ private renderDangerousInnerHtml;
247
264
  renderExpression(expr: IRExpression): string;
248
265
  renderConditional(cond: IRConditional): string;
249
266
  private renderNodeOrNull;
@@ -417,8 +434,32 @@ export declare class MinijinjaAdapter extends BaseAdapter implements IRNodeEmitt
417
434
  * single-quoted string literal (`const totalPages = 5`, #1897
418
435
  * pagination) — function-scope consts never reach the per-render
419
436
  * context, so a bare reference would resolve to Undefined.
437
+ *
438
+ * The lookup is a flat name match with no notion of AST scope, so a
439
+ * name that any loop callback binds as its item/index param never
440
+ * inlines (#2221) — the occurrence may be the loop's own (shadowing)
441
+ * binding, and substituting the outer const's value there renders every
442
+ * iteration with the same hard-coded literal. Coarse (a genuinely
443
+ * non-shadowed same-named const elsewhere in the component also stops
444
+ * inlining, falling back to the bare identifier) but safe — the same
445
+ * trade-off as #2212's `collectLoopBoundNames` use in
446
+ * `collectStringValueNames`.
420
447
  */
421
448
  private _resolveLiteralConst;
449
+ /**
450
+ * Resolve `IDENT.key` where `IDENT` is a module-scope object-literal const
451
+ * (`variantClasses.ghost`, #1896/#1897) to the looked-up scalar.
452
+ *
453
+ * The lookup is a flat name match on `objectName` with no notion of AST
454
+ * scope, so an enclosing loop callback's own param of the same name
455
+ * (`.map((cfg) => <li>{cfg.x}</li>)` shadowing a module `const cfg = {…}`)
456
+ * still resolved to the OUTER const's member value at every iteration
457
+ * (#2237) — the sibling hazard to #2221's `_resolveLiteralConst`. Same
458
+ * coarse-but-safe `staticLoopSourceBoundNames` guard: any name a loop
459
+ * binds anywhere in the component never inlines, falling back to the bare
460
+ * `cfg.x` member expression (which a minijinja `for` loop binds correctly
461
+ * at the shadowed occurrences).
462
+ */
422
463
  private _resolveStaticRecordLiteral;
423
464
  private _resolveModuleStringConst;
424
465
  private _recordExprBF101;
@@ -1 +1 @@
1
- {"version":3,"file":"minijinja-adapter.d.ts","sourceRoot":"","sources":["../../src/adapter/minijinja-adapter.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"minijinja-adapter.d.ts","sourceRoot":"","sources":["../../src/adapter/minijinja-adapter.ts"],"names":[],"mappings":"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"}
package/dist/build.js CHANGED
@@ -187308,7 +187308,13 @@ import {
187308
187308
  prepareLoweringMatchers,
187309
187309
  queryHrefArgs,
187310
187310
  isValidHelperId,
187311
- sortComparatorFromArrow as sortComparatorFromArrow2
187311
+ sortComparatorFromArrow as sortComparatorFromArrow2,
187312
+ isDangerousInnerHtmlAttr,
187313
+ resolveDangerousInnerHtml,
187314
+ dangerousInnerHtmlMetacharViolation,
187315
+ dangerousInnerHtmlDiagnostic,
187316
+ resolveStaticLoopSource,
187317
+ collectLoopBoundNames
187312
187318
  } from "@barefootjs/jsx";
187313
187319
 
187314
187320
  // src/adapter/boolean-result.ts
@@ -187686,6 +187692,39 @@ function renderFlatMethod(recv, depth, emit) {
187686
187692
  return `bf.flat(${recv}, ${d})`;
187687
187693
  }
187688
187694
 
187695
+ // src/adapter/lib/static-value.ts
187696
+ function staticValueToMinijinja(value) {
187697
+ if (value === null || value === undefined)
187698
+ return "none";
187699
+ if (typeof value === "boolean")
187700
+ return value ? "true" : "false";
187701
+ if (typeof value === "number")
187702
+ return String(value);
187703
+ if (typeof value === "string")
187704
+ return `'${escapeMinijinjaSingleQuoted(value)}'`;
187705
+ if (Array.isArray(value)) {
187706
+ const items = [];
187707
+ for (const el of value) {
187708
+ const serialized = staticValueToMinijinja(el);
187709
+ if (serialized === null)
187710
+ return null;
187711
+ items.push(serialized);
187712
+ }
187713
+ return `[${items.join(", ")}]`;
187714
+ }
187715
+ if (typeof value === "object") {
187716
+ const entries = [];
187717
+ for (const [key, val] of Object.entries(value)) {
187718
+ const serialized = staticValueToMinijinja(val);
187719
+ if (serialized === null)
187720
+ return null;
187721
+ entries.push(`${minijinjaHashKey(key)}: ${serialized}`);
187722
+ }
187723
+ return `{${entries.join(", ")}}`;
187724
+ }
187725
+ return null;
187726
+ }
187727
+
187689
187728
  // src/adapter/expr/emitters.ts
187690
187729
  import {
187691
187730
  groupBinaryOperand,
@@ -188276,6 +188315,7 @@ class MinijinjaAdapter extends BaseAdapter {
188276
188315
  _searchParamsLocals = new Set;
188277
188316
  _loweringMatchers = [];
188278
188317
  localConstants = [];
188318
+ staticLoopSourceBoundNames = new Set;
188279
188319
  nullableOptionalProps = new Set;
188280
188320
  constructor(options = {}) {
188281
188321
  super();
@@ -188291,6 +188331,7 @@ class MinijinjaAdapter extends BaseAdapter {
188291
188331
  this.propsParams = ir.metadata.propsParams.map((p) => ({ name: p.name }));
188292
188332
  this.booleanTypedProps = collectBooleanTypedProps(ir);
188293
188333
  this.localConstants = ir.metadata.localConstants ?? [];
188334
+ this.staticLoopSourceBoundNames = collectLoopBoundNames(ir);
188294
188335
  this.nullableOptionalProps = collectNullableOptionalProps(ir);
188295
188336
  this.stringValueNames = collectStringValueNames(ir);
188296
188337
  this.moduleStringConsts = collectModuleStringConsts(ir.metadata.localConstants);
@@ -188412,7 +188453,8 @@ class MinijinjaAdapter extends BaseAdapter {
188412
188453
  renderElement(element) {
188413
188454
  const tag = element.tag;
188414
188455
  const attrs = this.renderAttributes(element);
188415
- const children = this.renderChildren(element.children);
188456
+ const dangerousHtml = this.renderDangerousInnerHtml(element);
188457
+ const children = dangerousHtml !== null ? dangerousHtml : this.renderChildren(element.children);
188416
188458
  let hydrationAttrs = "";
188417
188459
  if (element.needsScope) {
188418
188460
  hydrationAttrs += ` ${this.renderScopeMarker("")}`;
@@ -188447,6 +188489,22 @@ class MinijinjaAdapter extends BaseAdapter {
188447
188489
  }
188448
188490
  return `<${tag}${attrs}${hydrationAttrs}>${children}</${tag}>`;
188449
188491
  }
188492
+ renderDangerousInnerHtml(element) {
188493
+ const resolution = resolveDangerousInnerHtml(element);
188494
+ if (!resolution)
188495
+ return null;
188496
+ if (resolution.kind === "dynamic") {
188497
+ this.errors.push(dangerousInnerHtmlDiagnostic(resolution.expr, resolution.loc));
188498
+ return "";
188499
+ }
188500
+ const violation = dangerousInnerHtmlMetacharViolation(resolution.html, this.name);
188501
+ if (violation) {
188502
+ const attr = element.attrs.find(isDangerousInnerHtmlAttr);
188503
+ this.errors.push(dangerousInnerHtmlDiagnostic(`{ __html: ${JSON.stringify(resolution.html)} }`, attr.loc, violation));
188504
+ return "";
188505
+ }
188506
+ return resolution.html;
188507
+ }
188450
188508
  renderExpression(expr) {
188451
188509
  if (expr.clientOnly) {
188452
188510
  if (expr.slotId) {
@@ -188454,7 +188512,7 @@ class MinijinjaAdapter extends BaseAdapter {
188454
188512
  }
188455
188513
  return "";
188456
188514
  }
188457
- const jinjaExpr = `bf.string(${this.convertExpressionToJinja(expr.expr)})`;
188515
+ const jinjaExpr = `bf.string(${this.convertExpressionToJinja(expr.expr, expr.parsed)})`;
188458
188516
  if (expr.slotId) {
188459
188517
  return `{{ bf.text_start("${expr.slotId}") | safe }}{{ ${jinjaExpr} }}{{ bf.text_end() | safe }}`;
188460
188518
  }
@@ -188545,8 +188603,12 @@ ${whenTrue}
188545
188603
  }
188546
188604
  });
188547
188605
  }
188606
+ const staticItems = resolveStaticLoopSource(loop.arrayParsed, this.localConstants, {
188607
+ isNameShadowed: (name) => this.staticLoopSourceBoundNames.has(name)
188608
+ });
188609
+ const staticArray = staticItems !== null ? staticValueToMinijinja(staticItems) : null;
188548
188610
  const arrayName = loop.array.trim();
188549
- if (/^[A-Za-z_$][\w$]*$/.test(arrayName)) {
188611
+ if (staticArray === null && /^[A-Za-z_$][\w$]*$/.test(arrayName)) {
188550
188612
  const arrayConst = (this.localConstants ?? []).find((c) => c.name === arrayName);
188551
188613
  if (arrayConst && !arrayConst.isModule && this._resolveLiteralConst(arrayName) === null) {
188552
188614
  this.errors.push({
@@ -188560,7 +188622,7 @@ ${whenTrue}
188560
188622
  });
188561
188623
  }
188562
188624
  }
188563
- const rawArray = this.convertExpressionToJinja(loop.array);
188625
+ const rawArray = staticArray ?? this.convertExpressionToJinja(loop.array);
188564
188626
  let array = rawArray;
188565
188627
  if (loop.sortComparator) {
188566
188628
  const sort = loop.sortComparator;
@@ -188872,6 +188934,8 @@ ${name}="{{ bf.string(${val}) }}"
188872
188934
  for (const attr of element.attrs) {
188873
188935
  if (attr.clientOnly)
188874
188936
  continue;
188937
+ if (isDangerousInnerHtmlAttr(attr))
188938
+ continue;
188875
188939
  let attrName;
188876
188940
  if (attr.name === "className")
188877
188941
  attrName = "class";
@@ -189071,6 +189135,8 @@ Options:
189071
189135
  return isBooleanResultExpr(expr) || isAriaBooleanAttr(name) || this.isBooleanTypedPropRef(expr);
189072
189136
  }
189073
189137
  _resolveLiteralConst(name) {
189138
+ if (this.staticLoopSourceBoundNames.has(name))
189139
+ return null;
189074
189140
  const c = (this.localConstants ?? []).find((lc) => lc.name === name);
189075
189141
  if (c?.value === undefined)
189076
189142
  return null;
@@ -189083,6 +189149,8 @@ Options:
189083
189149
  return null;
189084
189150
  }
189085
189151
  _resolveStaticRecordLiteral(objectName, key) {
189152
+ if (this.staticLoopSourceBoundNames.has(objectName))
189153
+ return null;
189086
189154
  const hit = lookupStaticRecordLiteral(objectName, key, this.localConstants);
189087
189155
  if (!hit)
189088
189156
  return null;
@@ -1 +1 @@
1
- {"version":3,"file":"conformance-pins.d.ts","sourceRoot":"","sources":["../src/conformance-pins.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAA;AAEtD,eAAO,MAAM,eAAe,EAAE,eA6F7B,CAAA"}
1
+ {"version":3,"file":"conformance-pins.d.ts","sourceRoot":"","sources":["../src/conformance-pins.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAA;AAEtD,eAAO,MAAM,eAAe,EAAE,eA4F7B,CAAA"}
package/dist/index.js CHANGED
@@ -187308,7 +187308,13 @@ import {
187308
187308
  prepareLoweringMatchers,
187309
187309
  queryHrefArgs,
187310
187310
  isValidHelperId,
187311
- sortComparatorFromArrow as sortComparatorFromArrow2
187311
+ sortComparatorFromArrow as sortComparatorFromArrow2,
187312
+ isDangerousInnerHtmlAttr,
187313
+ resolveDangerousInnerHtml,
187314
+ dangerousInnerHtmlMetacharViolation,
187315
+ dangerousInnerHtmlDiagnostic,
187316
+ resolveStaticLoopSource,
187317
+ collectLoopBoundNames
187312
187318
  } from "@barefootjs/jsx";
187313
187319
 
187314
187320
  // src/adapter/boolean-result.ts
@@ -187686,6 +187692,39 @@ function renderFlatMethod(recv, depth, emit) {
187686
187692
  return `bf.flat(${recv}, ${d})`;
187687
187693
  }
187688
187694
 
187695
+ // src/adapter/lib/static-value.ts
187696
+ function staticValueToMinijinja(value) {
187697
+ if (value === null || value === undefined)
187698
+ return "none";
187699
+ if (typeof value === "boolean")
187700
+ return value ? "true" : "false";
187701
+ if (typeof value === "number")
187702
+ return String(value);
187703
+ if (typeof value === "string")
187704
+ return `'${escapeMinijinjaSingleQuoted(value)}'`;
187705
+ if (Array.isArray(value)) {
187706
+ const items = [];
187707
+ for (const el of value) {
187708
+ const serialized = staticValueToMinijinja(el);
187709
+ if (serialized === null)
187710
+ return null;
187711
+ items.push(serialized);
187712
+ }
187713
+ return `[${items.join(", ")}]`;
187714
+ }
187715
+ if (typeof value === "object") {
187716
+ const entries = [];
187717
+ for (const [key, val] of Object.entries(value)) {
187718
+ const serialized = staticValueToMinijinja(val);
187719
+ if (serialized === null)
187720
+ return null;
187721
+ entries.push(`${minijinjaHashKey(key)}: ${serialized}`);
187722
+ }
187723
+ return `{${entries.join(", ")}}`;
187724
+ }
187725
+ return null;
187726
+ }
187727
+
187689
187728
  // src/adapter/expr/emitters.ts
187690
187729
  import {
187691
187730
  groupBinaryOperand,
@@ -188276,6 +188315,7 @@ class MinijinjaAdapter extends BaseAdapter {
188276
188315
  _searchParamsLocals = new Set;
188277
188316
  _loweringMatchers = [];
188278
188317
  localConstants = [];
188318
+ staticLoopSourceBoundNames = new Set;
188279
188319
  nullableOptionalProps = new Set;
188280
188320
  constructor(options = {}) {
188281
188321
  super();
@@ -188291,6 +188331,7 @@ class MinijinjaAdapter extends BaseAdapter {
188291
188331
  this.propsParams = ir.metadata.propsParams.map((p) => ({ name: p.name }));
188292
188332
  this.booleanTypedProps = collectBooleanTypedProps(ir);
188293
188333
  this.localConstants = ir.metadata.localConstants ?? [];
188334
+ this.staticLoopSourceBoundNames = collectLoopBoundNames(ir);
188294
188335
  this.nullableOptionalProps = collectNullableOptionalProps(ir);
188295
188336
  this.stringValueNames = collectStringValueNames(ir);
188296
188337
  this.moduleStringConsts = collectModuleStringConsts(ir.metadata.localConstants);
@@ -188412,7 +188453,8 @@ class MinijinjaAdapter extends BaseAdapter {
188412
188453
  renderElement(element) {
188413
188454
  const tag = element.tag;
188414
188455
  const attrs = this.renderAttributes(element);
188415
- const children = this.renderChildren(element.children);
188456
+ const dangerousHtml = this.renderDangerousInnerHtml(element);
188457
+ const children = dangerousHtml !== null ? dangerousHtml : this.renderChildren(element.children);
188416
188458
  let hydrationAttrs = "";
188417
188459
  if (element.needsScope) {
188418
188460
  hydrationAttrs += ` ${this.renderScopeMarker("")}`;
@@ -188447,6 +188489,22 @@ class MinijinjaAdapter extends BaseAdapter {
188447
188489
  }
188448
188490
  return `<${tag}${attrs}${hydrationAttrs}>${children}</${tag}>`;
188449
188491
  }
188492
+ renderDangerousInnerHtml(element) {
188493
+ const resolution = resolveDangerousInnerHtml(element);
188494
+ if (!resolution)
188495
+ return null;
188496
+ if (resolution.kind === "dynamic") {
188497
+ this.errors.push(dangerousInnerHtmlDiagnostic(resolution.expr, resolution.loc));
188498
+ return "";
188499
+ }
188500
+ const violation = dangerousInnerHtmlMetacharViolation(resolution.html, this.name);
188501
+ if (violation) {
188502
+ const attr = element.attrs.find(isDangerousInnerHtmlAttr);
188503
+ this.errors.push(dangerousInnerHtmlDiagnostic(`{ __html: ${JSON.stringify(resolution.html)} }`, attr.loc, violation));
188504
+ return "";
188505
+ }
188506
+ return resolution.html;
188507
+ }
188450
188508
  renderExpression(expr) {
188451
188509
  if (expr.clientOnly) {
188452
188510
  if (expr.slotId) {
@@ -188454,7 +188512,7 @@ class MinijinjaAdapter extends BaseAdapter {
188454
188512
  }
188455
188513
  return "";
188456
188514
  }
188457
- const jinjaExpr = `bf.string(${this.convertExpressionToJinja(expr.expr)})`;
188515
+ const jinjaExpr = `bf.string(${this.convertExpressionToJinja(expr.expr, expr.parsed)})`;
188458
188516
  if (expr.slotId) {
188459
188517
  return `{{ bf.text_start("${expr.slotId}") | safe }}{{ ${jinjaExpr} }}{{ bf.text_end() | safe }}`;
188460
188518
  }
@@ -188545,8 +188603,12 @@ ${whenTrue}
188545
188603
  }
188546
188604
  });
188547
188605
  }
188606
+ const staticItems = resolveStaticLoopSource(loop.arrayParsed, this.localConstants, {
188607
+ isNameShadowed: (name) => this.staticLoopSourceBoundNames.has(name)
188608
+ });
188609
+ const staticArray = staticItems !== null ? staticValueToMinijinja(staticItems) : null;
188548
188610
  const arrayName = loop.array.trim();
188549
- if (/^[A-Za-z_$][\w$]*$/.test(arrayName)) {
188611
+ if (staticArray === null && /^[A-Za-z_$][\w$]*$/.test(arrayName)) {
188550
188612
  const arrayConst = (this.localConstants ?? []).find((c) => c.name === arrayName);
188551
188613
  if (arrayConst && !arrayConst.isModule && this._resolveLiteralConst(arrayName) === null) {
188552
188614
  this.errors.push({
@@ -188560,7 +188622,7 @@ ${whenTrue}
188560
188622
  });
188561
188623
  }
188562
188624
  }
188563
- const rawArray = this.convertExpressionToJinja(loop.array);
188625
+ const rawArray = staticArray ?? this.convertExpressionToJinja(loop.array);
188564
188626
  let array = rawArray;
188565
188627
  if (loop.sortComparator) {
188566
188628
  const sort = loop.sortComparator;
@@ -188872,6 +188934,8 @@ ${name}="{{ bf.string(${val}) }}"
188872
188934
  for (const attr of element.attrs) {
188873
188935
  if (attr.clientOnly)
188874
188936
  continue;
188937
+ if (isDangerousInnerHtmlAttr(attr))
188938
+ continue;
188875
188939
  let attrName;
188876
188940
  if (attr.name === "className")
188877
188941
  attrName = "class";
@@ -189071,6 +189135,8 @@ Options:
189071
189135
  return isBooleanResultExpr(expr) || isAriaBooleanAttr(name) || this.isBooleanTypedPropRef(expr);
189072
189136
  }
189073
189137
  _resolveLiteralConst(name) {
189138
+ if (this.staticLoopSourceBoundNames.has(name))
189139
+ return null;
189074
189140
  const c = (this.localConstants ?? []).find((lc) => lc.name === name);
189075
189141
  if (c?.value === undefined)
189076
189142
  return null;
@@ -189083,6 +189149,8 @@ Options:
189083
189149
  return null;
189084
189150
  }
189085
189151
  _resolveStaticRecordLiteral(objectName, key) {
189152
+ if (this.staticLoopSourceBoundNames.has(objectName))
189153
+ return null;
189086
189154
  const hit = lookupStaticRecordLiteral(objectName, key, this.localConstants);
189087
189155
  if (!hit)
189088
189156
  return null;
@@ -189120,14 +189188,10 @@ Options:
189120
189188
  var minijinjaAdapter = new MinijinjaAdapter;
189121
189189
  // src/conformance-pins.ts
189122
189190
  var conformancePins = {
189123
- "static-array-children": [{ code: "BF103", severity: "error" }],
189124
- "todo-app": [{ code: "BF103", severity: "error" }],
189125
- "todo-app-ssr": [{ code: "BF103", severity: "error" }],
189126
189191
  "static-array-from-props": [
189127
189192
  { code: "BF101", severity: "error", issue: "https://github.com/piconic-ai/barefootjs/issues/2087" }
189128
189193
  ],
189129
189194
  "static-array-from-props-with-component": [
189130
- { code: "BF103", severity: "error" },
189131
189195
  { code: "BF101", severity: "error", issue: "https://github.com/piconic-ai/barefootjs/issues/2087" }
189132
189196
  ],
189133
189197
  "filter-nested-callback-predicate": [
@@ -189136,8 +189200,7 @@ var conformancePins = {
189136
189200
  "filter-nested-find-predicate": [
189137
189201
  { code: "BF101", severity: "error", issue: "https://github.com/piconic-ai/barefootjs/issues/2038" }
189138
189202
  ],
189139
- "array-map-function-reference": [{ code: "BF101", severity: "error" }],
189140
- "dangerous-inner-html": [{ code: "BF101", severity: "error" }]
189203
+ "dangerous-inner-html-dynamic": [{ code: "BF101", severity: "error", issue: "https://github.com/piconic-ai/barefootjs/issues/2215" }]
189141
189204
  };
189142
189205
  // src/render-divergences.ts
189143
189206
  var renderDivergences = {};
@@ -1 +1 @@
1
- {"version":3,"file":"render-divergences.d.ts","sourceRoot":"","sources":["../src/render-divergences.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAA;AAExD,eAAO,MAAM,iBAAiB,EAAE,iBAAsB,CAAA"}
1
+ {"version":3,"file":"render-divergences.d.ts","sourceRoot":"","sources":["../src/render-divergences.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAA;AAExD,eAAO,MAAM,iBAAiB,EAAE,iBAK/B,CAAA"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@barefootjs/rust",
3
- "version": "0.18.5",
3
+ "version": "0.18.7",
4
4
  "description": "minijinja (Rust) adapter for BarefootJS — compiles IR to .j2 templates and ships a Rust rendering runtime (packages/adapter-rust/runtime/); runs under any Rust web framework (axum, etc.)",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -54,14 +54,14 @@
54
54
  "directory": "packages/adapter-rust"
55
55
  },
56
56
  "dependencies": {
57
- "@barefootjs/shared": "0.18.5"
57
+ "@barefootjs/shared": "0.18.7"
58
58
  },
59
59
  "peerDependencies": {
60
60
  "@barefootjs/jsx": ">=0.2.0"
61
61
  },
62
62
  "devDependencies": {
63
63
  "@barefootjs/adapter-tests": "0.1.0",
64
- "@barefootjs/jsx": "0.18.5",
64
+ "@barefootjs/jsx": "0.18.7",
65
65
  "typescript": "^5.0.0"
66
66
  }
67
67
  }
@@ -411,3 +411,122 @@ export function Parent() {
411
411
  // `filter-nested-find-predicate` (BF101 via `expectedDiagnostics`) and
412
412
  // `filter-nested-callback-predicate-client` (the `/* @client */` suppression
413
413
  // twin, which must render clean).
414
+
415
+ // #2221: `_resolveLiteralConst` is a flat name lookup against
416
+ // `ir.metadata.localConstants` with no notion of AST scope — it used to
417
+ // substitute an outer const's literal value even at an occurrence that is
418
+ // actually an enclosing loop callback's own (shadowing) parameter, so every
419
+ // iteration rendered the same hard-coded literal. Guarded with the same
420
+ // coarse `collectLoopBoundNames` exclusion as #2212: any name a loop binds
421
+ // anywhere in the component never inlines, falling back to the bare
422
+ // identifier.
423
+ describe('MinijinjaAdapter - const inlining vs loop-param shadowing (#2221)', () => {
424
+ test('a loop param shadowing an outer literal const emits the identifier, not the const value', () => {
425
+ const { template } = compileAndGenerate(`
426
+ function Widget() {
427
+ const label: string = 'x'
428
+ return <ul>{[2, 5].map((label) => <li key={label}>{1 + label}</li>)}</ul>
429
+ }
430
+ `)
431
+ // The loop body must reference the per-iteration loop var...
432
+ expect(template).toContain('1 + label')
433
+ // ...never the outer const's hard-coded value.
434
+ expect(template).not.toContain("1 + 'x'")
435
+ })
436
+
437
+ test('a numeric const shadowed by a loop param emits the identifier too', () => {
438
+ const { template } = compileAndGenerate(`
439
+ function Widget() {
440
+ const count = 7
441
+ return <ul>{[2, 5].map((count) => <li key={count}>{1 + count}</li>)}</ul>
442
+ }
443
+ `)
444
+ expect(template).toContain('1 + count')
445
+ expect(template).not.toContain('1 + 7')
446
+ })
447
+
448
+ test('a literal const NOT shadowed by any loop still inlines (#1897 pin)', () => {
449
+ const { template } = compileAndGenerate(`
450
+ function Widget({ values }: { values: number[] }) {
451
+ const totalPages = 5
452
+ return <div>
453
+ <p>Page 1 of {1 + totalPages}</p>
454
+ <ul>{values.map((v) => <li key={v}>{v}</li>)}</ul>
455
+ </div>
456
+ }
457
+ `)
458
+ expect(template).toContain('1 + 5')
459
+ })
460
+
461
+ // The accepted coarse-exclusion trade-off (same as #2212): a name that is
462
+ // loop-bound ANYWHERE in the component never inlines, even at a genuinely
463
+ // non-shadowed occurrence outside the loop — the bare identifier is
464
+ // emitted instead of the value.
465
+ test('a const referenced outside the loop whose name is loop-bound elsewhere falls back to the identifier (accepted trade-off)', () => {
466
+ const { template } = compileAndGenerate(`
467
+ function Widget({ values }: { values: number[] }) {
468
+ const label: string = 'x'
469
+ return <div>
470
+ <p>{1 + label}</p>
471
+ <ul>{values.map((label) => <li key={label}>{2 + label}</li>)}</ul>
472
+ </div>
473
+ }
474
+ `)
475
+ expect(template).not.toContain("1 + 'x'")
476
+ expect(template).toContain('2 + label')
477
+ })
478
+ })
479
+
480
+ // #2237: `_resolveStaticRecordLiteral` (`IDENT.key` on a module-scope
481
+ // object-literal const, e.g. `variantClasses.ghost` — #1896/#1897) is a
482
+ // flat name lookup on `objectName` with no notion of AST scope, the
483
+ // record-literal sibling of #2221's `_resolveLiteralConst` bug. It used to
484
+ // substitute the outer const's member value even at an occurrence that is
485
+ // actually an enclosing loop callback's own (shadowing) parameter, so every
486
+ // iteration rendered the same hard-coded literal instead of the per-item
487
+ // value. Guarded with the same coarse `staticLoopSourceBoundNames`
488
+ // exclusion as #2221: any name a loop binds anywhere in the component
489
+ // never inlines, falling back to the bare `cfg.x` member expression.
490
+ describe('MinijinjaAdapter - record-literal member lookup vs loop-param shadowing (#2237)', () => {
491
+ test('a loop param shadowing an outer module object const emits the member access, not the outer literal', () => {
492
+ const { template } = compileAndGenerate(`
493
+ const cfg = { x: 'outer-lit' }
494
+ function Widget({ rows }: { rows: { x: string }[] }) {
495
+ return <ul>{rows.map((cfg) => <li key={cfg.x}>{cfg.x}</li>)}</ul>
496
+ }
497
+ `)
498
+ // The loop body must reference the per-iteration member access...
499
+ expect(template).toContain('bf.string(cfg.x)')
500
+ // ...never the outer const's hard-coded value.
501
+ expect(template).not.toContain("bf.string('outer-lit')")
502
+ })
503
+
504
+ test('a module object const NOT shadowed by any loop still inlines (variantClasses.ghost shape, #1896/#1897 pin)', () => {
505
+ const { template } = compileAndGenerate(`
506
+ const variantClasses = { solid: 'bg-solid', ghost: 'bg-ghost' }
507
+ function Widget({ variant }: { variant: 'solid' | 'ghost' }) {
508
+ return <div>{variantClasses.ghost}</div>
509
+ }
510
+ `)
511
+ expect(template).toContain("bf.string('bg-ghost')")
512
+ })
513
+
514
+ // The accepted coarse-exclusion trade-off (same as #2221/#2212): an
515
+ // object name that is loop-bound ANYWHERE in the component never
516
+ // inlines its member lookups, even at a genuinely non-shadowed
517
+ // occurrence outside the loop — the bare member expression is emitted
518
+ // instead of the value.
519
+ test('a record member referenced outside the loop whose object name is loop-bound elsewhere falls back to the member expression (accepted trade-off)', () => {
520
+ const { template } = compileAndGenerate(`
521
+ const cfg = { x: 'outer-lit' }
522
+ function Widget({ rows }: { rows: { x: string }[] }) {
523
+ return <div>
524
+ <p>{cfg.x}</p>
525
+ <ul>{rows.map((cfg) => <li key={cfg.x}>{cfg.x}</li>)}</ul>
526
+ </div>
527
+ }
528
+ `)
529
+ expect(template).not.toContain("bf.string('outer-lit')")
530
+ expect(template).toContain('bf.string(cfg.x)')
531
+ })
532
+ })
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Serialize a compile-time-evaluated JS value (`@barefootjs/jsx`'s
3
+ * `evaluateStaticLiteral`/`resolveStaticLoopSource`, #2208) into a native
4
+ * MiniJinja literal. Used to inline a fully-static loop source (an inline
5
+ * array literal, or a function-scope local const with a static
6
+ * initializer) directly in a `{% for %}` header, rather than requiring a
7
+ * bound template variable.
8
+ *
9
+ * Returns `null` for a value this adapter can't represent as a literal —
10
+ * the caller falls back to its existing BF101 refusal instead of guessing.
11
+ */
12
+
13
+ import { escapeMinijinjaSingleQuoted, minijinjaHashKey } from './minijinja-naming.ts'
14
+
15
+ export function staticValueToMinijinja(value: unknown): string | null {
16
+ if (value === null || value === undefined) return 'none'
17
+ if (typeof value === 'boolean') return value ? 'true' : 'false'
18
+ if (typeof value === 'number') return String(value)
19
+ if (typeof value === 'string') return `'${escapeMinijinjaSingleQuoted(value)}'`
20
+ if (Array.isArray(value)) {
21
+ const items: string[] = []
22
+ for (const el of value) {
23
+ const serialized = staticValueToMinijinja(el)
24
+ if (serialized === null) return null
25
+ items.push(serialized)
26
+ }
27
+ return `[${items.join(', ')}]`
28
+ }
29
+ if (typeof value === 'object') {
30
+ const entries: string[] = []
31
+ for (const [key, val] of Object.entries(value as Record<string, unknown>)) {
32
+ const serialized = staticValueToMinijinja(val)
33
+ if (serialized === null) return null
34
+ entries.push(`${minijinjaHashKey(key)}: ${serialized}`)
35
+ }
36
+ return `{${entries.join(', ')}}`
37
+ }
38
+ return null
39
+ }
@@ -165,6 +165,12 @@ import {
165
165
  queryHrefArgs,
166
166
  isValidHelperId,
167
167
  sortComparatorFromArrow,
168
+ isDangerousInnerHtmlAttr,
169
+ resolveDangerousInnerHtml,
170
+ dangerousInnerHtmlMetacharViolation,
171
+ dangerousInnerHtmlDiagnostic,
172
+ resolveStaticLoopSource,
173
+ collectLoopBoundNames,
168
174
  } from '@barefootjs/jsx'
169
175
  import { isAriaBooleanAttr, isBooleanResultExpr, isExplicitStringCall } from './boolean-result.ts'
170
176
  import type { ParsedExpr, LoweringMatcher, LoopBindingPathSegment } from '@barefootjs/jsx'
@@ -178,6 +184,7 @@ import {
178
184
  collectRootScopeNodes,
179
185
  } from './lib/ir-scope.ts'
180
186
  import { renderSortMethod, renderSortEval } from './expr/array-method.ts'
187
+ import { staticValueToMinijinja } from './lib/static-value.ts'
181
188
  import { JinjaFilterEmitter, JinjaTopLevelEmitter, truthyTest } from './expr/emitters.ts'
182
189
  import type { JinjaEmitContext, JinjaSpreadContext, JinjaMemoContext } from './emit-context.ts'
183
190
  import {
@@ -314,6 +321,17 @@ export class MinijinjaAdapter extends BaseAdapter implements IRNodeEmitter<Jinja
314
321
  */
315
322
  private localConstants: IRMetadata['localConstants'] = []
316
323
 
324
+ /**
325
+ * Every name a `.map()`/`.filter()` loop callback binds as its item/index
326
+ * parameter anywhere in the component (#2208 fable review). A static
327
+ * loop-SOURCE name (e.g. a function-scope `const items = [...]`) must
328
+ * never resolve through `resolveStaticLoopSource` at a use site where a
329
+ * DIFFERENT, enclosing loop's own callback param shadows it — same
330
+ * shadowing hazard, and same coarse-but-safe mitigation, as #2212's
331
+ * `collectLoopBoundNames` use in `collectStringValueNames`.
332
+ */
333
+ private staticLoopSourceBoundNames: Set<string> = new Set()
334
+
317
335
  /**
318
336
  * Optional, no-default props that are `None` when the caller omits them.
319
337
  * Their bare-reference attribute emission is guarded with a Jinja
@@ -346,6 +364,7 @@ export class MinijinjaAdapter extends BaseAdapter implements IRNodeEmitter<Jinja
346
364
  // ("True"/"False") (#1897, pagination's data-active).
347
365
  this.booleanTypedProps = collectBooleanTypedProps(ir)
348
366
  this.localConstants = ir.metadata.localConstants ?? []
367
+ this.staticLoopSourceBoundNames = collectLoopBoundNames(ir)
349
368
  this.nullableOptionalProps = collectNullableOptionalProps(ir)
350
369
  this.stringValueNames = collectStringValueNames(ir)
351
370
  this.moduleStringConsts = collectModuleStringConsts(ir.metadata.localConstants)
@@ -568,7 +587,8 @@ export class MinijinjaAdapter extends BaseAdapter implements IRNodeEmitter<Jinja
568
587
  renderElement(element: IRElement): string {
569
588
  const tag = element.tag
570
589
  const attrs = this.renderAttributes(element)
571
- const children = this.renderChildren(element.children)
590
+ const dangerousHtml = this.renderDangerousInnerHtml(element)
591
+ const children = dangerousHtml !== null ? dangerousHtml : this.renderChildren(element.children)
572
592
 
573
593
  let hydrationAttrs = ''
574
594
  if (element.needsScope) {
@@ -603,6 +623,28 @@ export class MinijinjaAdapter extends BaseAdapter implements IRNodeEmitter<Jinja
603
623
  return `<${tag}${attrs}${hydrationAttrs}>${children}</${tag}>`
604
624
  }
605
625
 
626
+ /**
627
+ * `dangerouslySetInnerHTML={{ __html: '...' }}` (#2207) — see the Blade
628
+ * adapter's identical helper for the full rationale. `null` means the
629
+ * attribute is absent (caller falls through to normal `renderChildren`);
630
+ * a non-`null` string (possibly `''`) replaces the children outright.
631
+ */
632
+ private renderDangerousInnerHtml(element: IRElement): string | null {
633
+ const resolution = resolveDangerousInnerHtml(element)
634
+ if (!resolution) return null
635
+ if (resolution.kind === 'dynamic') {
636
+ this.errors.push(dangerousInnerHtmlDiagnostic(resolution.expr, resolution.loc))
637
+ return ''
638
+ }
639
+ const violation = dangerousInnerHtmlMetacharViolation(resolution.html, this.name)
640
+ if (violation) {
641
+ const attr = element.attrs.find(isDangerousInnerHtmlAttr)!
642
+ this.errors.push(dangerousInnerHtmlDiagnostic(`{ __html: ${JSON.stringify(resolution.html)} }`, attr.loc, violation))
643
+ return ''
644
+ }
645
+ return resolution.html
646
+ }
647
+
606
648
  // ===========================================================================
607
649
  // Expression Rendering
608
650
  // ===========================================================================
@@ -616,8 +658,12 @@ export class MinijinjaAdapter extends BaseAdapter implements IRNodeEmitter<Jinja
616
658
  }
617
659
 
618
660
  // Text-position interpolation of a possibly-non-string value — see the
619
- // file header, divergence 2.
620
- const jinjaExpr = `bf.string(${this.convertExpressionToJinja(expr.expr)})`
661
+ // file header, divergence 2. Thread the IR-carried `.parsed` tree
662
+ // through (mirrors go-template's `convertExpressionToGo(expr.expr,
663
+ // classify, expr.parsed)`) so a resolved bare-identifier
664
+ // `.map`/`.filter`/… callback (`resolveCallbackMethodFunctionReferences`,
665
+ // #2206) isn't lost to a fresh, unresolved re-parse of the raw string.
666
+ const jinjaExpr = `bf.string(${this.convertExpressionToJinja(expr.expr, expr.parsed)})`
621
667
 
622
668
  if (expr.slotId) {
623
669
  return `{{ bf.text_start("${expr.slotId}") | safe }}{{ ${jinjaExpr} }}{{ bf.text_end() | safe }}`
@@ -762,8 +808,27 @@ export class MinijinjaAdapter extends BaseAdapter implements IRNodeEmitter<Jinja
762
808
  // corpus only because the widened destructure gate (#2087 Phase A/B)
763
809
  // no longer refuses this fixture's `([emoji, users]) => ...` param
764
810
  // first. Mirrors adapter-jinja's identical check.
811
+ // #2208: a loop source that is a fully-static array literal — either
812
+ // inline (`[{ label: 'Alpha' }, ...].map(...)`) or a bare identifier
813
+ // bound to a FUNCTION-scope local const whose initializer has no
814
+ // prop/signal/function-call dependency — inlines as a native MiniJinja
815
+ // list/dict literal below, the same way a module-scope const's value
816
+ // is already seeded. A runtime-computed local (#2069, e.g.
817
+ // `Object.entries(props.tags).filter(...)`) still refuses below.
818
+ // `isNameShadowed` guards a DIFFERENT, enclosing loop's own callback
819
+ // param shadowing this identifier (fable review) — never resolve the
820
+ // static const in that case. `rawArray` then falls through to the
821
+ // bare identifier expression below, same as before #2208 — which
822
+ // still trips the pre-existing BF101 gate for an unresolvable local
823
+ // const reference (a loud, conservative refusal, not a silent wrong
824
+ // value).
825
+ const staticItems = resolveStaticLoopSource(loop.arrayParsed, this.localConstants, {
826
+ isNameShadowed: name => this.staticLoopSourceBoundNames.has(name),
827
+ })
828
+ const staticArray = staticItems !== null ? staticValueToMinijinja(staticItems) : null
829
+
765
830
  const arrayName = loop.array.trim()
766
- if (/^[A-Za-z_$][\w$]*$/.test(arrayName)) {
831
+ if (staticArray === null && /^[A-Za-z_$][\w$]*$/.test(arrayName)) {
767
832
  const arrayConst = (this.localConstants ?? []).find(c => c.name === arrayName)
768
833
  if (arrayConst && !arrayConst.isModule && this._resolveLiteralConst(arrayName) === null) {
769
834
  this.errors.push({
@@ -779,7 +844,7 @@ export class MinijinjaAdapter extends BaseAdapter implements IRNodeEmitter<Jinja
779
844
  }
780
845
  }
781
846
 
782
- const rawArray = this.convertExpressionToJinja(loop.array)
847
+ const rawArray = staticArray ?? this.convertExpressionToJinja(loop.array)
783
848
  // Apply sort if present: wrap the loop array in the shared `bf.sort`
784
849
  // helper, binding the sorted result to a per-iteration local so the
785
850
  // helper runs once.
@@ -1434,6 +1499,12 @@ export class MinijinjaAdapter extends BaseAdapter implements IRNodeEmitter<Jinja
1434
1499
  // the unsupported-expression lowering is never reached for a deferred
1435
1500
  // predicate (no BF101 / BF102). #1966
1436
1501
  if (attr.clientOnly) continue
1502
+ // `dangerouslySetInnerHTML` never renders as an HTML attribute — it's
1503
+ // handled by `renderDangerousInnerHtml` instead, which replaces the
1504
+ // element's children. Skip it here so its `{ __html: ... }` object
1505
+ // literal never reaches the generic object-literal BF101 refusal
1506
+ // (which would double-report alongside the purpose-built one).
1507
+ if (isDangerousInnerHtmlAttr(attr)) continue
1437
1508
  // Rewrite JSX special-prop names to their HTML-attribute counterparts.
1438
1509
  let attrName: string
1439
1510
  if (attr.name === 'className') attrName = 'class'
@@ -1804,8 +1875,19 @@ export class MinijinjaAdapter extends BaseAdapter implements IRNodeEmitter<Jinja
1804
1875
  * single-quoted string literal (`const totalPages = 5`, #1897
1805
1876
  * pagination) — function-scope consts never reach the per-render
1806
1877
  * context, so a bare reference would resolve to Undefined.
1878
+ *
1879
+ * The lookup is a flat name match with no notion of AST scope, so a
1880
+ * name that any loop callback binds as its item/index param never
1881
+ * inlines (#2221) — the occurrence may be the loop's own (shadowing)
1882
+ * binding, and substituting the outer const's value there renders every
1883
+ * iteration with the same hard-coded literal. Coarse (a genuinely
1884
+ * non-shadowed same-named const elsewhere in the component also stops
1885
+ * inlining, falling back to the bare identifier) but safe — the same
1886
+ * trade-off as #2212's `collectLoopBoundNames` use in
1887
+ * `collectStringValueNames`.
1807
1888
  */
1808
1889
  private _resolveLiteralConst(name: string): string | null {
1890
+ if (this.staticLoopSourceBoundNames.has(name)) return null
1809
1891
  const c = (this.localConstants ?? []).find(lc => lc.name === name)
1810
1892
  if (c?.value === undefined) return null
1811
1893
  const v = c.value.trim()
@@ -1815,7 +1897,22 @@ export class MinijinjaAdapter extends BaseAdapter implements IRNodeEmitter<Jinja
1815
1897
  return null
1816
1898
  }
1817
1899
 
1900
+ /**
1901
+ * Resolve `IDENT.key` where `IDENT` is a module-scope object-literal const
1902
+ * (`variantClasses.ghost`, #1896/#1897) to the looked-up scalar.
1903
+ *
1904
+ * The lookup is a flat name match on `objectName` with no notion of AST
1905
+ * scope, so an enclosing loop callback's own param of the same name
1906
+ * (`.map((cfg) => <li>{cfg.x}</li>)` shadowing a module `const cfg = {…}`)
1907
+ * still resolved to the OUTER const's member value at every iteration
1908
+ * (#2237) — the sibling hazard to #2221's `_resolveLiteralConst`. Same
1909
+ * coarse-but-safe `staticLoopSourceBoundNames` guard: any name a loop
1910
+ * binds anywhere in the component never inlines, falling back to the bare
1911
+ * `cfg.x` member expression (which a minijinja `for` loop binds correctly
1912
+ * at the shadowed occurrences).
1913
+ */
1818
1914
  private _resolveStaticRecordLiteral(objectName: string, key: string): string | null {
1915
+ if (this.staticLoopSourceBoundNames.has(objectName)) return null
1819
1916
  const hit = lookupStaticRecordLiteral(objectName, key, this.localConstants)
1820
1917
  if (!hit) return null
1821
1918
  return hit.kind === 'number'
@@ -12,15 +12,17 @@
12
12
  import type { ConformancePins } from '@barefootjs/jsx'
13
13
 
14
14
  export const conformancePins: ConformancePins = {
15
- // Sibling-imported child component in a loop body: emits a
16
- // cross-template call needing separate registration. BF103 makes
17
- // the requirement loud (same as xslate).
18
- 'static-array-children': [{ code: 'BF103', severity: 'error' }],
19
- // TodoApp / TodoAppSSR import `TodoItem` from a sibling file and
20
- // call it inside a keyed `.map`. Same BF103 (imported child in
21
- // `.map`) as xslate.
22
- 'todo-app': [{ code: 'BF103', severity: 'error' }],
23
- 'todo-app-ssr': [{ code: 'BF103', severity: 'error' }],
15
+ // `todo-app` / `todo-app-ssr` no longer pinned (#2205) the conformance
16
+ // harness now passes `siblingTemplatesRegistered: true` for fixtures with
17
+ // sibling `components`, matching `bf build`'s real semantics, so the
18
+ // BF103 loop-body cross-template check no longer fires spuriously. (Both
19
+ // fixtures are still skipped on this adapter via `render-divergences.ts`
20
+ // #2209 for an unrelated signal-seeding gap.)
21
+ // `static-array-children` no longer pinned (#2208) `items`'s
22
+ // array-literal initializer is now recognized as fully-static
23
+ // (`resolveStaticLoopSource`) and inlined as a native MiniJinja
24
+ // list/dict literal in the `{% for %}` header, the same way a
25
+ // module-scope const's value is already seeded.
24
26
  // The `([emoji, users]) => ...` / `([id, t]) => ...` params in these two
25
27
  // fixtures no longer trip BF104 — the destructure itself now lowers
26
28
  // cleanly to a native `{% set %}` accessor (#2087 Phase B). But both
@@ -44,10 +46,10 @@ export const conformancePins: ConformancePins = {
44
46
  'static-array-from-props': [
45
47
  { code: 'BF101', severity: 'error', issue: 'https://github.com/piconic-ai/barefootjs/issues/2087' },
46
48
  ],
47
- // Both BF103 (imported child) and BF101 (unresolvable computed loop
48
- // array, see above) fire.
49
+ // BF101 (unresolvable computed loop array, see above) fires; BF103
50
+ // (imported child in the loop body) no longer does now that the
51
+ // conformance harness passes `siblingTemplatesRegistered: true` (#2205).
49
52
  'static-array-from-props-with-component': [
50
- { code: 'BF103', severity: 'error' },
51
53
  { code: 'BF101', severity: 'error', issue: 'https://github.com/piconic-ai/barefootjs/issues/2087' },
52
54
  ],
53
55
  // Rest-destructure `.map()` callbacks (#2087 Phase B): every shape now
@@ -91,17 +93,14 @@ export const conformancePins: ConformancePins = {
91
93
  // / etc. via the same evaluator-JSON mechanism as `.filter` / `.every` /
92
94
  // `.some`, so they render. Only the NESTED-in-a-predicate form above is
93
95
  // refused (#2038).
94
- // #2073 follow-up (same as xslate): a function-reference `.map(format)`
95
- // callback has no arrow body to serialize — not a CALLBACK_METHODS shape
96
- // (`asCallbackMethodCall` requires an arrow argument) so the shared
97
- // `isSupported`'s `UNSUPPORTED_METHODS` gate refuses it with the generic
98
- // "Expression not supported" BF101 rather than emitting a broken
99
- // template.
100
- 'array-map-function-reference': [{ code: 'BF101', severity: 'error' }],
101
- // Edge-case sweep (Priority 12): `dangerouslySetInnerHTML` requires a
102
- // deliberate raw-HTML (unescaped) output affordance in the target
103
- // template language. No lowering exists yet, so the compiler refuses
104
- // the shape loudly instead of emitting entity-escaped markup that
105
- // silently renders tags as text.
106
- 'dangerous-inner-html': [{ code: 'BF101', severity: 'error' }],
96
+ // `array-map-function-reference` no longer pinned a bare-identifier
97
+ // `.map(format)` callback now resolves one hop to its declaration
98
+ // (`resolveCallbackMethodFunctionReferences`, #2206), the same mechanism
99
+ // #2090 established for `.sort(fnref)`.
100
+ // `dangerous-inner-html` no longer pinned a compile-time string-literal
101
+ // `dangerouslySetInnerHTML={{ __html: '...' }}` is spliced directly into
102
+ // the template as trusted raw text (`resolveDangerousInnerHtml`, #2207).
103
+ // A dynamic/signal-derived value still refuses with BF101 — see the
104
+ // `dangerous-inner-html-dynamic` fixture/pin below (tracked: #2215).
105
+ 'dangerous-inner-html-dynamic': [{ code: 'BF101', severity: 'error', issue: 'https://github.com/piconic-ai/barefootjs/issues/2215' }],
107
106
  }
@@ -16,4 +16,9 @@
16
16
 
17
17
  import type { RenderDivergences } from '@barefootjs/jsx'
18
18
 
19
- export const renderDivergences: RenderDivergences = {}
19
+ export const renderDivergences: RenderDivergences = {
20
+ // `todo-app` / `todo-app-ssr` no longer diverge (#2209) — the shared
21
+ // `evaluateSignalInit` (`@barefootjs/jsx`, sandboxed real-JS evaluation
22
+ // instead of a fixed regex-shape catalogue) now correctly seeds `todos`
23
+ // from `(props.initialTodos ?? []).map(t => ({ ...t, editing: false }))`.
24
+ }
@@ -28,7 +28,7 @@
28
28
  * that closes that gap.
29
29
  */
30
30
 
31
- import { compileJSX, extractSsrDefaults, importsSearchParams } from '@barefootjs/jsx'
31
+ import { compileJSX, extractSsrDefaults, importsSearchParams, evaluateSignalInit } from '@barefootjs/jsx'
32
32
  import type { ComponentIR } from '@barefootjs/jsx'
33
33
  import { mkdir, rm } from 'node:fs/promises'
34
34
  import { resolve } from 'node:path'
@@ -170,8 +170,15 @@ export async function renderMinijinjaComponent(options: RenderOptions): Promise<
170
170
  }
171
171
  }
172
172
 
173
- // Compile parent source.
174
- const result = compileJSX(source, 'component.tsx', { adapter, outputIR: true })
173
+ // Compile parent source. `siblingTemplatesRegistered: Boolean(components)`
174
+ // matches this harness's real behavior every sibling child template is registered
175
+ // alongside the parent before rendering, so a loop-body cross-template
176
+ // call resolves at render time (#2205).
177
+ const result = compileJSX(source, 'component.tsx', {
178
+ adapter,
179
+ outputIR: true,
180
+ siblingTemplatesRegistered: Boolean(components),
181
+ })
175
182
 
176
183
  const errors = result.errors.filter(e => e.severity === 'error')
177
184
  if (errors.length > 0) {
@@ -441,7 +448,7 @@ function buildVars(
441
448
  for (const param of ir.metadata.propsParams) {
442
449
  if (props && param.name in props) continue
443
450
  if (param.defaultValue) {
444
- const value = jsDefaultToVarValue(param.defaultValue)
451
+ const value = evaluateSignalInit(param.defaultValue.trim(), props)
445
452
  if (value !== null) {
446
453
  vars[param.name] = value
447
454
  continue
@@ -510,146 +517,6 @@ function buildVars(
510
517
  return vars
511
518
  }
512
519
 
513
- /**
514
- * Convert a destructure-default's JS source text (`{ size = 'md' }`'s
515
- * `'md'`) to a real JS value. Near-verbatim port of `buildPythonProps`'s
516
- * `jsToPyValue` helper — which returned Python SOURCE text (safe to reuse
517
- * verbatim for a string/numeric literal, since JS and Python share that
518
- * literal grammar) — ported to resolve directly to the JS runtime value
519
- * instead, via the shared `parseLiteral` for the literal shapes both
520
- * versions handle identically. The match ORDER is preserved from
521
- * `jsToPyValue`: numeric/string/bool/`[]` are checked BEFORE the `??`
522
- * regex, so a string literal containing a literal `??` substring (e.g.
523
- * `'a??b'`) is caught by the string-literal branch first, not
524
- * mis-parsed as a nullish-coalescing default.
525
- */
526
- function jsDefaultToVarValue(jsValue: string): unknown {
527
- const v = jsValue.trim()
528
- if (/^-?\d+(\.\d+)?$/.test(v)) return Number(v)
529
- const strMatch = v.match(/^(['"])(.*)\1$/s)
530
- if (strMatch) return unescapeJsString(strMatch[2])
531
- if (v === 'true') return true
532
- if (v === 'false') return false
533
- if (v === '[]') return []
534
- const nullishMatch = v.match(/\?\?\s*(.+)$/)
535
- if (nullishMatch) return jsDefaultToVarValue(nullishMatch[1])
536
- if (v.startsWith('props.')) return null
537
- return null
538
- }
539
-
540
- /**
541
- * Evaluate a signal initializer expression using provided props.
542
- * Handles: props.initial ?? 0, props.value, literal values.
543
- */
544
- export function evaluateSignalInit(
545
- expr: string,
546
- props?: Record<string, unknown>,
547
- ): unknown {
548
- const nullishMatch = expr.match(/^props\.(\w+)\s*\?\?\s*(.+)$/)
549
- if (nullishMatch) {
550
- const propName = nullishMatch[1]
551
- const defaultExpr = nullishMatch[2].trim()
552
- if (props && propName in props) return props[propName]
553
- return parseLiteral(defaultExpr)
554
- }
555
-
556
- const propsMatch = expr.match(/^props\.(\w+)$/)
557
- if (propsMatch) {
558
- if (props && propsMatch[1] in props) return props[propsMatch[1]]
559
- return null
560
- }
561
-
562
- return parseLiteral(expr)
563
- }
564
-
565
- function parseLiteral(expr: string): unknown {
566
- if (/^-?\d+(\.\d+)?$/.test(expr)) return Number(expr)
567
- if (expr === 'true') return true
568
- if (expr === 'false') return false
569
- if (expr === '[]') return []
570
-
571
- {
572
- const t = expr.trim()
573
- if (t.startsWith('[') && t.endsWith(']')) {
574
- const inner = t.slice(1, -1).trim()
575
- if (!inner) return []
576
- const out: unknown[] = []
577
- for (const seg of splitTopLevelCommas(inner)) {
578
- if (!seg.trim()) continue
579
- const parsed = parseLiteral(seg.trim())
580
- if (parsed === null && seg.trim() !== 'null') return null
581
- out.push(parsed)
582
- }
583
- return out
584
- }
585
- }
586
-
587
- const stringMatch = expr.match(/^(['"])(.*)\1$/s)
588
- if (stringMatch) return unescapeJsString(stringMatch[2])
589
-
590
- const trimmed = expr.trim()
591
- if (trimmed.startsWith('{') && trimmed.endsWith('}')) {
592
- const inner = trimmed.slice(1, -1).trim()
593
- if (!inner) return {}
594
- const obj: Record<string, unknown> = {}
595
- for (const pair of splitTopLevelCommas(inner)) {
596
- if (!pair.trim()) continue
597
- const colonIdx = pair.indexOf(':')
598
- if (colonIdx < 0) return null
599
- let key = pair.slice(0, colonIdx).trim()
600
- const val = pair.slice(colonIdx + 1).trim()
601
- const keyMatch = key.match(/^(['"])(.*)\1$/s)
602
- if (keyMatch) key = unescapeJsString(keyMatch[2])
603
- const parsedVal = parseLiteral(val)
604
- if (parsedVal === null && val !== 'null') return null
605
- obj[key] = parsedVal
606
- }
607
- return obj
608
- }
609
- return null
610
- }
611
-
612
- function splitTopLevelCommas(inner: string): string[] {
613
- const segments: string[] = []
614
- let depth = 0
615
- let start = 0
616
- let quote: string | null = null
617
- for (let i = 0; i < inner.length; i++) {
618
- const c = inner[i]
619
- if (quote) {
620
- if (c === quote) {
621
- let backslashes = 0
622
- for (let j = i - 1; j >= 0 && inner[j] === '\\'; j--) backslashes++
623
- if (backslashes % 2 === 0) quote = null
624
- }
625
- continue
626
- }
627
- if (c === '"' || c === "'") {
628
- quote = c
629
- continue
630
- }
631
- if (c === '{' || c === '[') depth++
632
- else if (c === '}' || c === ']') depth--
633
- else if (c === ',' && depth === 0) {
634
- segments.push(inner.slice(start, i))
635
- start = i + 1
636
- }
637
- }
638
- segments.push(inner.slice(start))
639
- return segments
640
- }
641
-
642
- function unescapeJsString(s: string): string {
643
- return s.replace(/\\(.)/g, (_, c) => {
644
- switch (c) {
645
- case 'n': return '\n'
646
- case 'r': return '\r'
647
- case 't': return '\t'
648
- case '0': return '\0'
649
- default: return c
650
- }
651
- })
652
- }
653
520
 
654
521
  /**
655
522
  * Recursively replace JS's non-finite numbers (`NaN`, `Infinity`,