@barefootjs/jinja 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
@@ -187699,6 +187705,39 @@ function renderFlatMethod(recv, depth, emit) {
187699
187705
  return `bf.flat(${recv}, ${d})`;
187700
187706
  }
187701
187707
 
187708
+ // src/adapter/lib/static-value.ts
187709
+ function staticValueToJinja(value) {
187710
+ if (value === null || value === undefined)
187711
+ return "none";
187712
+ if (typeof value === "boolean")
187713
+ return value ? "true" : "false";
187714
+ if (typeof value === "number")
187715
+ return String(value);
187716
+ if (typeof value === "string")
187717
+ return `'${escapeJinjaSingleQuoted(value)}'`;
187718
+ if (Array.isArray(value)) {
187719
+ const items = [];
187720
+ for (const el of value) {
187721
+ const serialized = staticValueToJinja(el);
187722
+ if (serialized === null)
187723
+ return null;
187724
+ items.push(serialized);
187725
+ }
187726
+ return `[${items.join(", ")}]`;
187727
+ }
187728
+ if (typeof value === "object") {
187729
+ const entries = [];
187730
+ for (const [key, val] of Object.entries(value)) {
187731
+ const serialized = staticValueToJinja(val);
187732
+ if (serialized === null)
187733
+ return null;
187734
+ entries.push(`${jinjaHashKey(key)}: ${serialized}`);
187735
+ }
187736
+ return `{${entries.join(", ")}}`;
187737
+ }
187738
+ return null;
187739
+ }
187740
+
187702
187741
  // src/adapter/expr/emitters.ts
187703
187742
  import {
187704
187743
  groupBinaryOperand,
@@ -188281,6 +188320,7 @@ class JinjaAdapter extends BaseAdapter {
188281
188320
  _searchParamsLocals = new Set;
188282
188321
  _loweringMatchers = [];
188283
188322
  localConstants = [];
188323
+ staticLoopSourceBoundNames = new Set;
188284
188324
  nullableOptionalProps = new Set;
188285
188325
  constructor(options = {}) {
188286
188326
  super();
@@ -188296,6 +188336,7 @@ class JinjaAdapter extends BaseAdapter {
188296
188336
  this.propsParams = ir.metadata.propsParams.map((p) => ({ name: p.name }));
188297
188337
  this.booleanTypedProps = collectBooleanTypedProps(ir);
188298
188338
  this.localConstants = ir.metadata.localConstants ?? [];
188339
+ this.staticLoopSourceBoundNames = collectLoopBoundNames(ir);
188299
188340
  this.nullableOptionalProps = collectNullableOptionalProps(ir);
188300
188341
  this.stringValueNames = collectStringValueNames(ir);
188301
188342
  this.moduleStringConsts = collectModuleStringConsts(ir.metadata.localConstants);
@@ -188417,7 +188458,8 @@ class JinjaAdapter extends BaseAdapter {
188417
188458
  renderElement(element) {
188418
188459
  const tag = element.tag;
188419
188460
  const attrs = this.renderAttributes(element);
188420
- const children = this.renderChildren(element.children);
188461
+ const dangerousHtml = this.renderDangerousInnerHtml(element);
188462
+ const children = dangerousHtml !== null ? dangerousHtml : this.renderChildren(element.children);
188421
188463
  let hydrationAttrs = "";
188422
188464
  if (element.needsScope) {
188423
188465
  hydrationAttrs += ` ${this.renderScopeMarker("")}`;
@@ -188452,6 +188494,22 @@ class JinjaAdapter extends BaseAdapter {
188452
188494
  }
188453
188495
  return `<${tag}${attrs}${hydrationAttrs}>${children}</${tag}>`;
188454
188496
  }
188497
+ renderDangerousInnerHtml(element) {
188498
+ const resolution = resolveDangerousInnerHtml(element);
188499
+ if (!resolution)
188500
+ return null;
188501
+ if (resolution.kind === "dynamic") {
188502
+ this.errors.push(dangerousInnerHtmlDiagnostic(resolution.expr, resolution.loc));
188503
+ return "";
188504
+ }
188505
+ const violation = dangerousInnerHtmlMetacharViolation(resolution.html, this.name);
188506
+ if (violation) {
188507
+ const attr = element.attrs.find(isDangerousInnerHtmlAttr);
188508
+ this.errors.push(dangerousInnerHtmlDiagnostic(`{ __html: ${JSON.stringify(resolution.html)} }`, attr.loc, violation));
188509
+ return "";
188510
+ }
188511
+ return resolution.html;
188512
+ }
188455
188513
  renderExpression(expr) {
188456
188514
  if (expr.clientOnly) {
188457
188515
  if (expr.slotId) {
@@ -188459,7 +188517,7 @@ class JinjaAdapter extends BaseAdapter {
188459
188517
  }
188460
188518
  return "";
188461
188519
  }
188462
- const jinjaExpr = `bf.string(${this.convertExpressionToJinja(expr.expr)})`;
188520
+ const jinjaExpr = `bf.string(${this.convertExpressionToJinja(expr.expr, expr.parsed)})`;
188463
188521
  if (expr.slotId) {
188464
188522
  return `{{ bf.text_start("${expr.slotId}") | safe }}{{ ${jinjaExpr} }}{{ bf.text_end() | safe }}`;
188465
188523
  }
@@ -188551,8 +188609,12 @@ ${whenTrue}
188551
188609
  }
188552
188610
  });
188553
188611
  }
188612
+ const staticItems = resolveStaticLoopSource(loop.arrayParsed, this.localConstants, {
188613
+ isNameShadowed: (name) => this.staticLoopSourceBoundNames.has(name)
188614
+ });
188615
+ const staticArray = staticItems !== null ? staticValueToJinja(staticItems) : null;
188554
188616
  const arrayName = loop.array.trim();
188555
- if (/^[A-Za-z_$][\w$]*$/.test(arrayName)) {
188617
+ if (staticArray === null && /^[A-Za-z_$][\w$]*$/.test(arrayName)) {
188556
188618
  const arrayConst = (this.localConstants ?? []).find((c) => c.name === arrayName);
188557
188619
  if (arrayConst && !arrayConst.isModule && this._resolveLiteralConst(arrayName) === null) {
188558
188620
  this.errors.push({
@@ -188566,7 +188628,7 @@ ${whenTrue}
188566
188628
  });
188567
188629
  }
188568
188630
  }
188569
- const rawArray = this.convertExpressionToJinja(loop.array);
188631
+ const rawArray = staticArray ?? this.convertExpressionToJinja(loop.array);
188570
188632
  let array = rawArray;
188571
188633
  if (loop.sortComparator) {
188572
188634
  const sort = loop.sortComparator;
@@ -188878,6 +188940,8 @@ ${name}="{{ bf.string(${val}) }}"
188878
188940
  for (const attr of element.attrs) {
188879
188941
  if (attr.clientOnly)
188880
188942
  continue;
188943
+ if (isDangerousInnerHtmlAttr(attr))
188944
+ continue;
188881
188945
  let attrName;
188882
188946
  if (attr.name === "className")
188883
188947
  attrName = "class";
@@ -189077,6 +189141,8 @@ Options:
189077
189141
  return isBooleanResultExpr(expr) || isAriaBooleanAttr(name) || this.isBooleanTypedPropRef(expr);
189078
189142
  }
189079
189143
  _resolveLiteralConst(name) {
189144
+ if (this.staticLoopSourceBoundNames.has(name))
189145
+ return null;
189080
189146
  const c = (this.localConstants ?? []).find((lc) => lc.name === name);
189081
189147
  if (c?.value === undefined)
189082
189148
  return null;
@@ -189089,6 +189155,8 @@ Options:
189089
189155
  return null;
189090
189156
  }
189091
189157
  _resolveStaticRecordLiteral(objectName, key) {
189158
+ if (this.staticLoopSourceBoundNames.has(objectName))
189159
+ return null;
189092
189160
  const hit = lookupStaticRecordLiteral(objectName, key, this.localConstants);
189093
189161
  if (!hit)
189094
189162
  return null;
@@ -168,6 +168,16 @@ export declare class JinjaAdapter extends BaseAdapter implements IRNodeEmitter<J
168
168
  * const (`const sizeAttrs = size ? {…} : {}`) to its initializer text.
169
169
  */
170
170
  private localConstants;
171
+ /**
172
+ * Every name a `.map()`/`.filter()` loop callback binds as its item/index
173
+ * parameter anywhere in the component (#2208 fable review). A static
174
+ * loop-SOURCE name (e.g. a function-scope `const items = [...]`) must
175
+ * never resolve through `resolveStaticLoopSource` at a use site where a
176
+ * DIFFERENT, enclosing loop's own callback param shadows it — same
177
+ * shadowing hazard, and same coarse-but-safe mitigation, as #2212's
178
+ * `collectLoopBoundNames` use in `collectStringValueNames`.
179
+ */
180
+ private staticLoopSourceBoundNames;
171
181
  /**
172
182
  * Optional, no-default props that are `None` when the caller omits them.
173
183
  * Their bare-reference attribute emission is guarded with a Jinja
@@ -219,6 +229,13 @@ export declare class JinjaAdapter extends BaseAdapter implements IRNodeEmitter<J
219
229
  private providerObjectLiteralJinja;
220
230
  emitAsync(node: IRAsync, _ctx: JinjaRenderCtx, _emit: EmitIRNode<JinjaRenderCtx>): string;
221
231
  renderElement(element: IRElement): string;
232
+ /**
233
+ * `dangerouslySetInnerHTML={{ __html: '...' }}` (#2207) — see the Blade
234
+ * adapter's identical helper for the full rationale. `null` means the
235
+ * attribute is absent (caller falls through to normal `renderChildren`);
236
+ * a non-`null` string (possibly `''`) replaces the children outright.
237
+ */
238
+ private renderDangerousInnerHtml;
222
239
  renderExpression(expr: IRExpression): string;
223
240
  renderConditional(cond: IRConditional): string;
224
241
  private renderNodeOrNull;
@@ -390,8 +407,32 @@ export declare class JinjaAdapter extends BaseAdapter implements IRNodeEmitter<J
390
407
  * single-quoted string literal (`const totalPages = 5`, #1897
391
408
  * pagination) — function-scope consts never reach the per-render
392
409
  * context, so a bare reference would resolve to Undefined.
410
+ *
411
+ * The lookup is a flat name match with no notion of AST scope, so a
412
+ * name that any loop callback binds as its item/index param never
413
+ * inlines (#2221) — the occurrence may be the loop's own (shadowing)
414
+ * binding, and substituting the outer const's value there renders every
415
+ * iteration with the same hard-coded literal. Coarse (a genuinely
416
+ * non-shadowed same-named const elsewhere in the component also stops
417
+ * inlining, falling back to the bare identifier) but safe — the same
418
+ * trade-off as #2212's `collectLoopBoundNames` use in
419
+ * `collectStringValueNames`.
393
420
  */
394
421
  private _resolveLiteralConst;
422
+ /**
423
+ * Resolve `IDENT.key` where `IDENT` is a module-scope object-literal const
424
+ * (`variantClasses.ghost`, #1896/#1897) to the looked-up scalar.
425
+ *
426
+ * The lookup is a flat name match on `objectName` with no notion of AST
427
+ * scope, so an enclosing loop callback's own param of the same name
428
+ * (`.map((cfg) => <li>{cfg.x}</li>)` shadowing a module `const cfg = {…}`)
429
+ * still resolved to the OUTER const's member value at every iteration
430
+ * (#2237) — the sibling hazard to #2221's `_resolveLiteralConst`. Same
431
+ * coarse-but-safe `staticLoopSourceBoundNames` guard: any name a loop
432
+ * binds anywhere in the component never inlines, falling back to the bare
433
+ * `cfg['x']` member expression (which a Jinja for-loop binds correctly
434
+ * at the shadowed occurrences).
435
+ */
395
436
  private _resolveStaticRecordLiteral;
396
437
  private _resolveModuleStringConst;
397
438
  private _recordExprBF101;
@@ -1 +1 @@
1
- {"version":3,"file":"jinja-adapter.d.ts","sourceRoot":"","sources":["../../src/adapter/jinja-adapter.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"jinja-adapter.d.ts","sourceRoot":"","sources":["../../src/adapter/jinja-adapter.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Serialize a compile-time-evaluated JS value (`@barefootjs/jsx`'s
3
+ * `evaluateStaticLiteral`/`resolveStaticLoopSource`, #2208) into a native
4
+ * Jinja2 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
+ * (e.g. `undefined` reads for a missing object key are still representable
11
+ * as `none`, but anything else falls back to the caller's existing BF101
12
+ * refusal instead of guessing).
13
+ */
14
+ export declare function staticValueToJinja(value: unknown): string | null;
15
+ //# 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;;;;;;;;;;;;GAYG;AAIH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAwBhE"}
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
@@ -187699,6 +187705,39 @@ function renderFlatMethod(recv, depth, emit) {
187699
187705
  return `bf.flat(${recv}, ${d})`;
187700
187706
  }
187701
187707
 
187708
+ // src/adapter/lib/static-value.ts
187709
+ function staticValueToJinja(value) {
187710
+ if (value === null || value === undefined)
187711
+ return "none";
187712
+ if (typeof value === "boolean")
187713
+ return value ? "true" : "false";
187714
+ if (typeof value === "number")
187715
+ return String(value);
187716
+ if (typeof value === "string")
187717
+ return `'${escapeJinjaSingleQuoted(value)}'`;
187718
+ if (Array.isArray(value)) {
187719
+ const items = [];
187720
+ for (const el of value) {
187721
+ const serialized = staticValueToJinja(el);
187722
+ if (serialized === null)
187723
+ return null;
187724
+ items.push(serialized);
187725
+ }
187726
+ return `[${items.join(", ")}]`;
187727
+ }
187728
+ if (typeof value === "object") {
187729
+ const entries = [];
187730
+ for (const [key, val] of Object.entries(value)) {
187731
+ const serialized = staticValueToJinja(val);
187732
+ if (serialized === null)
187733
+ return null;
187734
+ entries.push(`${jinjaHashKey(key)}: ${serialized}`);
187735
+ }
187736
+ return `{${entries.join(", ")}}`;
187737
+ }
187738
+ return null;
187739
+ }
187740
+
187702
187741
  // src/adapter/expr/emitters.ts
187703
187742
  import {
187704
187743
  groupBinaryOperand,
@@ -188281,6 +188320,7 @@ class JinjaAdapter extends BaseAdapter {
188281
188320
  _searchParamsLocals = new Set;
188282
188321
  _loweringMatchers = [];
188283
188322
  localConstants = [];
188323
+ staticLoopSourceBoundNames = new Set;
188284
188324
  nullableOptionalProps = new Set;
188285
188325
  constructor(options = {}) {
188286
188326
  super();
@@ -188296,6 +188336,7 @@ class JinjaAdapter extends BaseAdapter {
188296
188336
  this.propsParams = ir.metadata.propsParams.map((p) => ({ name: p.name }));
188297
188337
  this.booleanTypedProps = collectBooleanTypedProps(ir);
188298
188338
  this.localConstants = ir.metadata.localConstants ?? [];
188339
+ this.staticLoopSourceBoundNames = collectLoopBoundNames(ir);
188299
188340
  this.nullableOptionalProps = collectNullableOptionalProps(ir);
188300
188341
  this.stringValueNames = collectStringValueNames(ir);
188301
188342
  this.moduleStringConsts = collectModuleStringConsts(ir.metadata.localConstants);
@@ -188417,7 +188458,8 @@ class JinjaAdapter extends BaseAdapter {
188417
188458
  renderElement(element) {
188418
188459
  const tag = element.tag;
188419
188460
  const attrs = this.renderAttributes(element);
188420
- const children = this.renderChildren(element.children);
188461
+ const dangerousHtml = this.renderDangerousInnerHtml(element);
188462
+ const children = dangerousHtml !== null ? dangerousHtml : this.renderChildren(element.children);
188421
188463
  let hydrationAttrs = "";
188422
188464
  if (element.needsScope) {
188423
188465
  hydrationAttrs += ` ${this.renderScopeMarker("")}`;
@@ -188452,6 +188494,22 @@ class JinjaAdapter extends BaseAdapter {
188452
188494
  }
188453
188495
  return `<${tag}${attrs}${hydrationAttrs}>${children}</${tag}>`;
188454
188496
  }
188497
+ renderDangerousInnerHtml(element) {
188498
+ const resolution = resolveDangerousInnerHtml(element);
188499
+ if (!resolution)
188500
+ return null;
188501
+ if (resolution.kind === "dynamic") {
188502
+ this.errors.push(dangerousInnerHtmlDiagnostic(resolution.expr, resolution.loc));
188503
+ return "";
188504
+ }
188505
+ const violation = dangerousInnerHtmlMetacharViolation(resolution.html, this.name);
188506
+ if (violation) {
188507
+ const attr = element.attrs.find(isDangerousInnerHtmlAttr);
188508
+ this.errors.push(dangerousInnerHtmlDiagnostic(`{ __html: ${JSON.stringify(resolution.html)} }`, attr.loc, violation));
188509
+ return "";
188510
+ }
188511
+ return resolution.html;
188512
+ }
188455
188513
  renderExpression(expr) {
188456
188514
  if (expr.clientOnly) {
188457
188515
  if (expr.slotId) {
@@ -188459,7 +188517,7 @@ class JinjaAdapter extends BaseAdapter {
188459
188517
  }
188460
188518
  return "";
188461
188519
  }
188462
- const jinjaExpr = `bf.string(${this.convertExpressionToJinja(expr.expr)})`;
188520
+ const jinjaExpr = `bf.string(${this.convertExpressionToJinja(expr.expr, expr.parsed)})`;
188463
188521
  if (expr.slotId) {
188464
188522
  return `{{ bf.text_start("${expr.slotId}") | safe }}{{ ${jinjaExpr} }}{{ bf.text_end() | safe }}`;
188465
188523
  }
@@ -188551,8 +188609,12 @@ ${whenTrue}
188551
188609
  }
188552
188610
  });
188553
188611
  }
188612
+ const staticItems = resolveStaticLoopSource(loop.arrayParsed, this.localConstants, {
188613
+ isNameShadowed: (name) => this.staticLoopSourceBoundNames.has(name)
188614
+ });
188615
+ const staticArray = staticItems !== null ? staticValueToJinja(staticItems) : null;
188554
188616
  const arrayName = loop.array.trim();
188555
- if (/^[A-Za-z_$][\w$]*$/.test(arrayName)) {
188617
+ if (staticArray === null && /^[A-Za-z_$][\w$]*$/.test(arrayName)) {
188556
188618
  const arrayConst = (this.localConstants ?? []).find((c) => c.name === arrayName);
188557
188619
  if (arrayConst && !arrayConst.isModule && this._resolveLiteralConst(arrayName) === null) {
188558
188620
  this.errors.push({
@@ -188566,7 +188628,7 @@ ${whenTrue}
188566
188628
  });
188567
188629
  }
188568
188630
  }
188569
- const rawArray = this.convertExpressionToJinja(loop.array);
188631
+ const rawArray = staticArray ?? this.convertExpressionToJinja(loop.array);
188570
188632
  let array = rawArray;
188571
188633
  if (loop.sortComparator) {
188572
188634
  const sort = loop.sortComparator;
@@ -188878,6 +188940,8 @@ ${name}="{{ bf.string(${val}) }}"
188878
188940
  for (const attr of element.attrs) {
188879
188941
  if (attr.clientOnly)
188880
188942
  continue;
188943
+ if (isDangerousInnerHtmlAttr(attr))
188944
+ continue;
188881
188945
  let attrName;
188882
188946
  if (attr.name === "className")
188883
188947
  attrName = "class";
@@ -189077,6 +189141,8 @@ Options:
189077
189141
  return isBooleanResultExpr(expr) || isAriaBooleanAttr(name) || this.isBooleanTypedPropRef(expr);
189078
189142
  }
189079
189143
  _resolveLiteralConst(name) {
189144
+ if (this.staticLoopSourceBoundNames.has(name))
189145
+ return null;
189080
189146
  const c = (this.localConstants ?? []).find((lc) => lc.name === name);
189081
189147
  if (c?.value === undefined)
189082
189148
  return null;
@@ -189089,6 +189155,8 @@ Options:
189089
189155
  return null;
189090
189156
  }
189091
189157
  _resolveStaticRecordLiteral(objectName, key) {
189158
+ if (this.staticLoopSourceBoundNames.has(objectName))
189159
+ return null;
189092
189160
  const hit = lookupStaticRecordLiteral(objectName, key, this.localConstants);
189093
189161
  if (!hit)
189094
189162
  return null;
@@ -1 +1 @@
1
- {"version":3,"file":"conformance-pins.d.ts","sourceRoot":"","sources":["../src/conformance-pins.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAA;AAEtD,eAAO,MAAM,eAAe,EAAE,eA8F7B,CAAA"}
1
+ {"version":3,"file":"conformance-pins.d.ts","sourceRoot":"","sources":["../src/conformance-pins.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAA;AAEtD,eAAO,MAAM,eAAe,EAAE,eA6F7B,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
@@ -187699,6 +187705,39 @@ function renderFlatMethod(recv, depth, emit) {
187699
187705
  return `bf.flat(${recv}, ${d})`;
187700
187706
  }
187701
187707
 
187708
+ // src/adapter/lib/static-value.ts
187709
+ function staticValueToJinja(value) {
187710
+ if (value === null || value === undefined)
187711
+ return "none";
187712
+ if (typeof value === "boolean")
187713
+ return value ? "true" : "false";
187714
+ if (typeof value === "number")
187715
+ return String(value);
187716
+ if (typeof value === "string")
187717
+ return `'${escapeJinjaSingleQuoted(value)}'`;
187718
+ if (Array.isArray(value)) {
187719
+ const items = [];
187720
+ for (const el of value) {
187721
+ const serialized = staticValueToJinja(el);
187722
+ if (serialized === null)
187723
+ return null;
187724
+ items.push(serialized);
187725
+ }
187726
+ return `[${items.join(", ")}]`;
187727
+ }
187728
+ if (typeof value === "object") {
187729
+ const entries = [];
187730
+ for (const [key, val] of Object.entries(value)) {
187731
+ const serialized = staticValueToJinja(val);
187732
+ if (serialized === null)
187733
+ return null;
187734
+ entries.push(`${jinjaHashKey(key)}: ${serialized}`);
187735
+ }
187736
+ return `{${entries.join(", ")}}`;
187737
+ }
187738
+ return null;
187739
+ }
187740
+
187702
187741
  // src/adapter/expr/emitters.ts
187703
187742
  import {
187704
187743
  groupBinaryOperand,
@@ -188281,6 +188320,7 @@ class JinjaAdapter extends BaseAdapter {
188281
188320
  _searchParamsLocals = new Set;
188282
188321
  _loweringMatchers = [];
188283
188322
  localConstants = [];
188323
+ staticLoopSourceBoundNames = new Set;
188284
188324
  nullableOptionalProps = new Set;
188285
188325
  constructor(options = {}) {
188286
188326
  super();
@@ -188296,6 +188336,7 @@ class JinjaAdapter extends BaseAdapter {
188296
188336
  this.propsParams = ir.metadata.propsParams.map((p) => ({ name: p.name }));
188297
188337
  this.booleanTypedProps = collectBooleanTypedProps(ir);
188298
188338
  this.localConstants = ir.metadata.localConstants ?? [];
188339
+ this.staticLoopSourceBoundNames = collectLoopBoundNames(ir);
188299
188340
  this.nullableOptionalProps = collectNullableOptionalProps(ir);
188300
188341
  this.stringValueNames = collectStringValueNames(ir);
188301
188342
  this.moduleStringConsts = collectModuleStringConsts(ir.metadata.localConstants);
@@ -188417,7 +188458,8 @@ class JinjaAdapter extends BaseAdapter {
188417
188458
  renderElement(element) {
188418
188459
  const tag = element.tag;
188419
188460
  const attrs = this.renderAttributes(element);
188420
- const children = this.renderChildren(element.children);
188461
+ const dangerousHtml = this.renderDangerousInnerHtml(element);
188462
+ const children = dangerousHtml !== null ? dangerousHtml : this.renderChildren(element.children);
188421
188463
  let hydrationAttrs = "";
188422
188464
  if (element.needsScope) {
188423
188465
  hydrationAttrs += ` ${this.renderScopeMarker("")}`;
@@ -188452,6 +188494,22 @@ class JinjaAdapter extends BaseAdapter {
188452
188494
  }
188453
188495
  return `<${tag}${attrs}${hydrationAttrs}>${children}</${tag}>`;
188454
188496
  }
188497
+ renderDangerousInnerHtml(element) {
188498
+ const resolution = resolveDangerousInnerHtml(element);
188499
+ if (!resolution)
188500
+ return null;
188501
+ if (resolution.kind === "dynamic") {
188502
+ this.errors.push(dangerousInnerHtmlDiagnostic(resolution.expr, resolution.loc));
188503
+ return "";
188504
+ }
188505
+ const violation = dangerousInnerHtmlMetacharViolation(resolution.html, this.name);
188506
+ if (violation) {
188507
+ const attr = element.attrs.find(isDangerousInnerHtmlAttr);
188508
+ this.errors.push(dangerousInnerHtmlDiagnostic(`{ __html: ${JSON.stringify(resolution.html)} }`, attr.loc, violation));
188509
+ return "";
188510
+ }
188511
+ return resolution.html;
188512
+ }
188455
188513
  renderExpression(expr) {
188456
188514
  if (expr.clientOnly) {
188457
188515
  if (expr.slotId) {
@@ -188459,7 +188517,7 @@ class JinjaAdapter extends BaseAdapter {
188459
188517
  }
188460
188518
  return "";
188461
188519
  }
188462
- const jinjaExpr = `bf.string(${this.convertExpressionToJinja(expr.expr)})`;
188520
+ const jinjaExpr = `bf.string(${this.convertExpressionToJinja(expr.expr, expr.parsed)})`;
188463
188521
  if (expr.slotId) {
188464
188522
  return `{{ bf.text_start("${expr.slotId}") | safe }}{{ ${jinjaExpr} }}{{ bf.text_end() | safe }}`;
188465
188523
  }
@@ -188551,8 +188609,12 @@ ${whenTrue}
188551
188609
  }
188552
188610
  });
188553
188611
  }
188612
+ const staticItems = resolveStaticLoopSource(loop.arrayParsed, this.localConstants, {
188613
+ isNameShadowed: (name) => this.staticLoopSourceBoundNames.has(name)
188614
+ });
188615
+ const staticArray = staticItems !== null ? staticValueToJinja(staticItems) : null;
188554
188616
  const arrayName = loop.array.trim();
188555
- if (/^[A-Za-z_$][\w$]*$/.test(arrayName)) {
188617
+ if (staticArray === null && /^[A-Za-z_$][\w$]*$/.test(arrayName)) {
188556
188618
  const arrayConst = (this.localConstants ?? []).find((c) => c.name === arrayName);
188557
188619
  if (arrayConst && !arrayConst.isModule && this._resolveLiteralConst(arrayName) === null) {
188558
188620
  this.errors.push({
@@ -188566,7 +188628,7 @@ ${whenTrue}
188566
188628
  });
188567
188629
  }
188568
188630
  }
188569
- const rawArray = this.convertExpressionToJinja(loop.array);
188631
+ const rawArray = staticArray ?? this.convertExpressionToJinja(loop.array);
188570
188632
  let array = rawArray;
188571
188633
  if (loop.sortComparator) {
188572
188634
  const sort = loop.sortComparator;
@@ -188878,6 +188940,8 @@ ${name}="{{ bf.string(${val}) }}"
188878
188940
  for (const attr of element.attrs) {
188879
188941
  if (attr.clientOnly)
188880
188942
  continue;
188943
+ if (isDangerousInnerHtmlAttr(attr))
188944
+ continue;
188881
188945
  let attrName;
188882
188946
  if (attr.name === "className")
188883
188947
  attrName = "class";
@@ -189077,6 +189141,8 @@ Options:
189077
189141
  return isBooleanResultExpr(expr) || isAriaBooleanAttr(name) || this.isBooleanTypedPropRef(expr);
189078
189142
  }
189079
189143
  _resolveLiteralConst(name) {
189144
+ if (this.staticLoopSourceBoundNames.has(name))
189145
+ return null;
189080
189146
  const c = (this.localConstants ?? []).find((lc) => lc.name === name);
189081
189147
  if (c?.value === undefined)
189082
189148
  return null;
@@ -189089,6 +189155,8 @@ Options:
189089
189155
  return null;
189090
189156
  }
189091
189157
  _resolveStaticRecordLiteral(objectName, key) {
189158
+ if (this.staticLoopSourceBoundNames.has(objectName))
189159
+ return null;
189092
189160
  const hit = lookupStaticRecordLiteral(objectName, key, this.localConstants);
189093
189161
  if (!hit)
189094
189162
  return null;
@@ -189126,14 +189194,10 @@ Options:
189126
189194
  var jinjaAdapter = new JinjaAdapter;
189127
189195
  // src/conformance-pins.ts
189128
189196
  var conformancePins = {
189129
- "static-array-children": [{ code: "BF103", severity: "error" }],
189130
- "todo-app": [{ code: "BF103", severity: "error" }],
189131
- "todo-app-ssr": [{ code: "BF103", severity: "error" }],
189132
189197
  "static-array-from-props": [
189133
189198
  { code: "BF101", severity: "error", issue: "https://github.com/piconic-ai/barefootjs/issues/2087" }
189134
189199
  ],
189135
189200
  "static-array-from-props-with-component": [
189136
- { code: "BF103", severity: "error" },
189137
189201
  { code: "BF101", severity: "error", issue: "https://github.com/piconic-ai/barefootjs/issues/2087" }
189138
189202
  ],
189139
189203
  "filter-nested-callback-predicate": [
@@ -189142,8 +189206,7 @@ var conformancePins = {
189142
189206
  "filter-nested-find-predicate": [
189143
189207
  { code: "BF101", severity: "error", issue: "https://github.com/piconic-ai/barefootjs/issues/2038" }
189144
189208
  ],
189145
- "array-map-function-reference": [{ code: "BF101", severity: "error" }],
189146
- "dangerous-inner-html": [{ code: "BF101", severity: "error" }]
189209
+ "dangerous-inner-html-dynamic": [{ code: "BF101", severity: "error", issue: "https://github.com/piconic-ai/barefootjs/issues/2215" }]
189147
189210
  };
189148
189211
  // src/render-divergences.ts
189149
189212
  var renderDivergences = {};
@@ -1 +1 @@
1
- {"version":3,"file":"render-divergences.d.ts","sourceRoot":"","sources":["../src/render-divergences.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;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;;;;;;;;;;;;GAYG;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/jinja",
3
- "version": "0.18.5",
3
+ "version": "0.18.7",
4
4
  "description": "Jinja2 adapter for BarefootJS — compiles IR to .jinja templates and ships the Python BarefootJS rendering runtime; runs under any Python web framework (Flask, etc.)",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -53,14 +53,14 @@
53
53
  "directory": "packages/adapter-jinja"
54
54
  },
55
55
  "dependencies": {
56
- "@barefootjs/shared": "0.18.5"
56
+ "@barefootjs/shared": "0.18.7"
57
57
  },
58
58
  "peerDependencies": {
59
59
  "@barefootjs/jsx": ">=0.2.0"
60
60
  },
61
61
  "devDependencies": {
62
62
  "@barefootjs/adapter-tests": "0.1.0",
63
- "@barefootjs/jsx": "0.18.5",
63
+ "@barefootjs/jsx": "0.18.7",
64
64
  "typescript": "^5.0.0"
65
65
  }
66
66
  }
@@ -409,3 +409,123 @@ export function Parent() {
409
409
  // `filter-nested-find-predicate` (BF101 via `expectedDiagnostics`) and
410
410
  // `filter-nested-callback-predicate-client` (the `/* @client */` suppression
411
411
  // twin, which must render clean).
412
+
413
+ // #2221: `_resolveLiteralConst` is a flat name lookup against
414
+ // `ir.metadata.localConstants` with no notion of AST scope — it used to
415
+ // substitute an outer const's literal value even at an occurrence that is
416
+ // actually an enclosing loop callback's own (shadowing) parameter, so every
417
+ // iteration rendered the same hard-coded literal. Guarded with the same
418
+ // coarse `collectLoopBoundNames` exclusion as #2212: any name a loop binds
419
+ // anywhere in the component never inlines, falling back to the bare
420
+ // identifier. SSR-only tests for the same #2222 reason as the #2212
421
+ // describe below.
422
+ describe('JinjaAdapter - const inlining vs loop-param shadowing (#2221)', () => {
423
+ test('a loop param shadowing an outer literal const emits the identifier, not the const value', () => {
424
+ const { template } = compileAndGenerate(`
425
+ function Widget() {
426
+ const label: string = 'x'
427
+ return <ul>{[2, 5].map((label) => <li key={label}>{1 + label}</li>)}</ul>
428
+ }
429
+ `)
430
+ // The loop body must reference the per-iteration loop var...
431
+ expect(template).toContain('1 + label')
432
+ // ...never the outer const's hard-coded value.
433
+ expect(template).not.toContain("1 + 'x'")
434
+ })
435
+
436
+ test('a numeric const shadowed by a loop param emits the identifier too', () => {
437
+ const { template } = compileAndGenerate(`
438
+ function Widget() {
439
+ const count = 7
440
+ return <ul>{[2, 5].map((count) => <li key={count}>{1 + count}</li>)}</ul>
441
+ }
442
+ `)
443
+ expect(template).toContain('1 + count')
444
+ expect(template).not.toContain('1 + 7')
445
+ })
446
+
447
+ test('a literal const NOT shadowed by any loop still inlines (#1897 pin)', () => {
448
+ const { template } = compileAndGenerate(`
449
+ function Widget({ values }: { values: number[] }) {
450
+ const totalPages = 5
451
+ return <div>
452
+ <p>Page 1 of {1 + totalPages}</p>
453
+ <ul>{values.map((v) => <li key={v}>{v}</li>)}</ul>
454
+ </div>
455
+ }
456
+ `)
457
+ expect(template).toContain('1 + 5')
458
+ })
459
+
460
+ // The accepted coarse-exclusion trade-off (same as #2212): a name that is
461
+ // loop-bound ANYWHERE in the component never inlines, even at a genuinely
462
+ // non-shadowed occurrence outside the loop — the bare identifier is
463
+ // emitted instead of the value.
464
+ test('a const referenced outside the loop whose name is loop-bound elsewhere falls back to the identifier (accepted trade-off)', () => {
465
+ const { template } = compileAndGenerate(`
466
+ function Widget({ values }: { values: number[] }) {
467
+ const label: string = 'x'
468
+ return <div>
469
+ <p>{1 + label}</p>
470
+ <ul>{values.map((label) => <li key={label}>{2 + label}</li>)}</ul>
471
+ </div>
472
+ }
473
+ `)
474
+ expect(template).not.toContain("1 + 'x'")
475
+ expect(template).toContain('2 + label')
476
+ })
477
+ })
478
+
479
+ // #2237: `_resolveStaticRecordLiteral` (`IDENT.key` on a module-scope
480
+ // object-literal const, e.g. `variantClasses.ghost` — #1896/#1897) is a
481
+ // flat name lookup on `objectName` with no notion of AST scope, the
482
+ // record-literal sibling of #2221's `_resolveLiteralConst` bug. It used to
483
+ // substitute the outer const's member value even at an occurrence that is
484
+ // actually an enclosing loop callback's own (shadowing) parameter, so every
485
+ // iteration rendered the same hard-coded literal instead of the per-item
486
+ // value. Guarded with the same coarse `staticLoopSourceBoundNames`
487
+ // exclusion as #2221: any name a loop binds anywhere in the component
488
+ // never inlines, falling back to the bare `cfg['x']` member expression.
489
+ describe('JinjaAdapter - record-literal member lookup vs loop-param shadowing (#2237)', () => {
490
+ test('a loop param shadowing an outer module object const emits the member access, not the outer literal', () => {
491
+ const { template } = compileAndGenerate(`
492
+ const cfg = { x: 'outer-lit' }
493
+ function Widget({ rows }: { rows: { x: string }[] }) {
494
+ return <ul>{rows.map((cfg) => <li key={cfg.x}>{cfg.x}</li>)}</ul>
495
+ }
496
+ `)
497
+ // The loop body must reference the per-iteration member access...
498
+ expect(template).toContain("bf.string(cfg['x'])")
499
+ // ...never the outer const's hard-coded value.
500
+ expect(template).not.toContain("bf.string('outer-lit')")
501
+ })
502
+
503
+ test('a module object const NOT shadowed by any loop still inlines (variantClasses.ghost shape, #1896/#1897 pin)', () => {
504
+ const { template } = compileAndGenerate(`
505
+ const variantClasses = { solid: 'bg-solid', ghost: 'bg-ghost' }
506
+ function Widget({ variant }: { variant: 'solid' | 'ghost' }) {
507
+ return <div>{variantClasses.ghost}</div>
508
+ }
509
+ `)
510
+ expect(template).toContain("bf.string('bg-ghost')")
511
+ })
512
+
513
+ // The accepted coarse-exclusion trade-off (same as #2221/#2212): an
514
+ // object name that is loop-bound ANYWHERE in the component never
515
+ // inlines its member lookups, even at a genuinely non-shadowed
516
+ // occurrence outside the loop — the bare member expression is emitted
517
+ // instead of the value.
518
+ test('a record member referenced outside the loop whose object name is loop-bound elsewhere falls back to the member expression (accepted trade-off)', () => {
519
+ const { template } = compileAndGenerate(`
520
+ const cfg = { x: 'outer-lit' }
521
+ function Widget({ rows }: { rows: { x: string }[] }) {
522
+ return <div>
523
+ <p>{cfg.x}</p>
524
+ <ul>{rows.map((cfg) => <li key={cfg.x}>{cfg.x}</li>)}</ul>
525
+ </div>
526
+ }
527
+ `)
528
+ expect(template).not.toContain("bf.string('outer-lit')")
529
+ expect(template).toContain("bf.string(cfg['x'])")
530
+ })
531
+ })
@@ -140,6 +140,12 @@ import {
140
140
  queryHrefArgs,
141
141
  isValidHelperId,
142
142
  sortComparatorFromArrow,
143
+ isDangerousInnerHtmlAttr,
144
+ resolveDangerousInnerHtml,
145
+ dangerousInnerHtmlMetacharViolation,
146
+ dangerousInnerHtmlDiagnostic,
147
+ resolveStaticLoopSource,
148
+ collectLoopBoundNames,
143
149
  } from '@barefootjs/jsx'
144
150
  import { isAriaBooleanAttr, isBooleanResultExpr, isExplicitStringCall } from './boolean-result.ts'
145
151
  import type { ParsedExpr, LoweringMatcher } from '@barefootjs/jsx'
@@ -158,6 +164,7 @@ import {
158
164
  collectRootScopeNodes,
159
165
  } from './lib/ir-scope.ts'
160
166
  import { renderSortMethod, renderSortEval } from './expr/array-method.ts'
167
+ import { staticValueToJinja } from './lib/static-value.ts'
161
168
  import { JinjaFilterEmitter, JinjaTopLevelEmitter, truthyTest } from './expr/emitters.ts'
162
169
  import type { JinjaEmitContext, JinjaSpreadContext, JinjaMemoContext } from './emit-context.ts'
163
170
  import {
@@ -266,6 +273,17 @@ export class JinjaAdapter extends BaseAdapter implements IRNodeEmitter<JinjaRend
266
273
  */
267
274
  private localConstants: IRMetadata['localConstants'] = []
268
275
 
276
+ /**
277
+ * Every name a `.map()`/`.filter()` loop callback binds as its item/index
278
+ * parameter anywhere in the component (#2208 fable review). A static
279
+ * loop-SOURCE name (e.g. a function-scope `const items = [...]`) must
280
+ * never resolve through `resolveStaticLoopSource` at a use site where a
281
+ * DIFFERENT, enclosing loop's own callback param shadows it — same
282
+ * shadowing hazard, and same coarse-but-safe mitigation, as #2212's
283
+ * `collectLoopBoundNames` use in `collectStringValueNames`.
284
+ */
285
+ private staticLoopSourceBoundNames: Set<string> = new Set()
286
+
269
287
  /**
270
288
  * Optional, no-default props that are `None` when the caller omits them.
271
289
  * Their bare-reference attribute emission is guarded with a Jinja
@@ -298,6 +316,7 @@ export class JinjaAdapter extends BaseAdapter implements IRNodeEmitter<JinjaRend
298
316
  // ("True"/"False") (#1897, pagination's data-active).
299
317
  this.booleanTypedProps = collectBooleanTypedProps(ir)
300
318
  this.localConstants = ir.metadata.localConstants ?? []
319
+ this.staticLoopSourceBoundNames = collectLoopBoundNames(ir)
301
320
  this.nullableOptionalProps = collectNullableOptionalProps(ir)
302
321
  this.stringValueNames = collectStringValueNames(ir)
303
322
  this.moduleStringConsts = collectModuleStringConsts(ir.metadata.localConstants)
@@ -520,7 +539,8 @@ export class JinjaAdapter extends BaseAdapter implements IRNodeEmitter<JinjaRend
520
539
  renderElement(element: IRElement): string {
521
540
  const tag = element.tag
522
541
  const attrs = this.renderAttributes(element)
523
- const children = this.renderChildren(element.children)
542
+ const dangerousHtml = this.renderDangerousInnerHtml(element)
543
+ const children = dangerousHtml !== null ? dangerousHtml : this.renderChildren(element.children)
524
544
 
525
545
  let hydrationAttrs = ''
526
546
  if (element.needsScope) {
@@ -555,6 +575,28 @@ export class JinjaAdapter extends BaseAdapter implements IRNodeEmitter<JinjaRend
555
575
  return `<${tag}${attrs}${hydrationAttrs}>${children}</${tag}>`
556
576
  }
557
577
 
578
+ /**
579
+ * `dangerouslySetInnerHTML={{ __html: '...' }}` (#2207) — see the Blade
580
+ * adapter's identical helper for the full rationale. `null` means the
581
+ * attribute is absent (caller falls through to normal `renderChildren`);
582
+ * a non-`null` string (possibly `''`) replaces the children outright.
583
+ */
584
+ private renderDangerousInnerHtml(element: IRElement): string | null {
585
+ const resolution = resolveDangerousInnerHtml(element)
586
+ if (!resolution) return null
587
+ if (resolution.kind === 'dynamic') {
588
+ this.errors.push(dangerousInnerHtmlDiagnostic(resolution.expr, resolution.loc))
589
+ return ''
590
+ }
591
+ const violation = dangerousInnerHtmlMetacharViolation(resolution.html, this.name)
592
+ if (violation) {
593
+ const attr = element.attrs.find(isDangerousInnerHtmlAttr)!
594
+ this.errors.push(dangerousInnerHtmlDiagnostic(`{ __html: ${JSON.stringify(resolution.html)} }`, attr.loc, violation))
595
+ return ''
596
+ }
597
+ return resolution.html
598
+ }
599
+
558
600
  // ===========================================================================
559
601
  // Expression Rendering
560
602
  // ===========================================================================
@@ -568,8 +610,12 @@ export class JinjaAdapter extends BaseAdapter implements IRNodeEmitter<JinjaRend
568
610
  }
569
611
 
570
612
  // Text-position interpolation of a possibly-non-string value — see the
571
- // file header, divergence 2.
572
- const jinjaExpr = `bf.string(${this.convertExpressionToJinja(expr.expr)})`
613
+ // file header, divergence 2. Thread the IR-carried `.parsed` tree
614
+ // through (mirrors go-template's `convertExpressionToGo(expr.expr,
615
+ // classify, expr.parsed)`) so a resolved bare-identifier
616
+ // `.map`/`.filter`/… callback (`resolveCallbackMethodFunctionReferences`,
617
+ // #2206) isn't lost to a fresh, unresolved re-parse of the raw string.
618
+ const jinjaExpr = `bf.string(${this.convertExpressionToJinja(expr.expr, expr.parsed)})`
573
619
 
574
620
  if (expr.slotId) {
575
621
  return `{{ bf.text_start("${expr.slotId}") | safe }}{{ ${jinjaExpr} }}{{ bf.text_end() | safe }}`
@@ -716,8 +762,27 @@ export class JinjaAdapter extends BaseAdapter implements IRNodeEmitter<JinjaRend
716
762
  // test corpus only because the widened destructure gate (#2087 Phase
717
763
  // A/B) no longer refuses this fixture's `([emoji, users]) => ...`
718
764
  // param first.
765
+ // #2208: a loop source that is a fully-static array literal — either
766
+ // inline (`[{ label: 'Alpha' }, ...].map(...)`) or a bare identifier
767
+ // bound to a FUNCTION-scope local const whose initializer has no
768
+ // prop/signal/function-call dependency — inlines as a native Jinja
769
+ // list/dict literal below, the same way a module-scope const's value
770
+ // is already seeded. A runtime-computed local (#2069, e.g.
771
+ // `Object.entries(props.tags).filter(...)`) still refuses below.
772
+ // `isNameShadowed` guards a DIFFERENT, enclosing loop's own callback
773
+ // param shadowing this identifier (fable review) — never resolve the
774
+ // static const in that case. `rawArray` then falls through to the
775
+ // bare identifier expression below, same as before #2208 — which
776
+ // still trips the pre-existing BF101 gate for an unresolvable local
777
+ // const reference (a loud, conservative refusal, not a silent wrong
778
+ // value).
779
+ const staticItems = resolveStaticLoopSource(loop.arrayParsed, this.localConstants, {
780
+ isNameShadowed: name => this.staticLoopSourceBoundNames.has(name),
781
+ })
782
+ const staticArray = staticItems !== null ? staticValueToJinja(staticItems) : null
783
+
719
784
  const arrayName = loop.array.trim()
720
- if (/^[A-Za-z_$][\w$]*$/.test(arrayName)) {
785
+ if (staticArray === null && /^[A-Za-z_$][\w$]*$/.test(arrayName)) {
721
786
  const arrayConst = (this.localConstants ?? []).find(c => c.name === arrayName)
722
787
  if (arrayConst && !arrayConst.isModule && this._resolveLiteralConst(arrayName) === null) {
723
788
  this.errors.push({
@@ -733,7 +798,7 @@ export class JinjaAdapter extends BaseAdapter implements IRNodeEmitter<JinjaRend
733
798
  }
734
799
  }
735
800
 
736
- const rawArray = this.convertExpressionToJinja(loop.array)
801
+ const rawArray = staticArray ?? this.convertExpressionToJinja(loop.array)
737
802
  // Apply sort if present: wrap the loop array in the shared `bf.sort`
738
803
  // helper, binding the sorted result to a per-iteration local so the
739
804
  // helper runs once.
@@ -1385,6 +1450,12 @@ export class JinjaAdapter extends BaseAdapter implements IRNodeEmitter<JinjaRend
1385
1450
  // the unsupported-expression lowering is never reached for a deferred
1386
1451
  // predicate (no BF101 / BF102). #1966
1387
1452
  if (attr.clientOnly) continue
1453
+ // `dangerouslySetInnerHTML` never renders as an HTML attribute — it's
1454
+ // handled by `renderDangerousInnerHtml` instead, which replaces the
1455
+ // element's children. Skip it here so its `{ __html: ... }` object
1456
+ // literal never reaches the generic object-literal BF101 refusal
1457
+ // (which would double-report alongside the purpose-built one).
1458
+ if (isDangerousInnerHtmlAttr(attr)) continue
1388
1459
  // Rewrite JSX special-prop names to their HTML-attribute counterparts.
1389
1460
  let attrName: string
1390
1461
  if (attr.name === 'className') attrName = 'class'
@@ -1749,8 +1820,19 @@ export class JinjaAdapter extends BaseAdapter implements IRNodeEmitter<JinjaRend
1749
1820
  * single-quoted string literal (`const totalPages = 5`, #1897
1750
1821
  * pagination) — function-scope consts never reach the per-render
1751
1822
  * context, so a bare reference would resolve to Undefined.
1823
+ *
1824
+ * The lookup is a flat name match with no notion of AST scope, so a
1825
+ * name that any loop callback binds as its item/index param never
1826
+ * inlines (#2221) — the occurrence may be the loop's own (shadowing)
1827
+ * binding, and substituting the outer const's value there renders every
1828
+ * iteration with the same hard-coded literal. Coarse (a genuinely
1829
+ * non-shadowed same-named const elsewhere in the component also stops
1830
+ * inlining, falling back to the bare identifier) but safe — the same
1831
+ * trade-off as #2212's `collectLoopBoundNames` use in
1832
+ * `collectStringValueNames`.
1752
1833
  */
1753
1834
  private _resolveLiteralConst(name: string): string | null {
1835
+ if (this.staticLoopSourceBoundNames.has(name)) return null
1754
1836
  const c = (this.localConstants ?? []).find(lc => lc.name === name)
1755
1837
  if (c?.value === undefined) return null
1756
1838
  const v = c.value.trim()
@@ -1760,7 +1842,22 @@ export class JinjaAdapter extends BaseAdapter implements IRNodeEmitter<JinjaRend
1760
1842
  return null
1761
1843
  }
1762
1844
 
1845
+ /**
1846
+ * Resolve `IDENT.key` where `IDENT` is a module-scope object-literal const
1847
+ * (`variantClasses.ghost`, #1896/#1897) to the looked-up scalar.
1848
+ *
1849
+ * The lookup is a flat name match on `objectName` with no notion of AST
1850
+ * scope, so an enclosing loop callback's own param of the same name
1851
+ * (`.map((cfg) => <li>{cfg.x}</li>)` shadowing a module `const cfg = {…}`)
1852
+ * still resolved to the OUTER const's member value at every iteration
1853
+ * (#2237) — the sibling hazard to #2221's `_resolveLiteralConst`. Same
1854
+ * coarse-but-safe `staticLoopSourceBoundNames` guard: any name a loop
1855
+ * binds anywhere in the component never inlines, falling back to the bare
1856
+ * `cfg['x']` member expression (which a Jinja for-loop binds correctly
1857
+ * at the shadowed occurrences).
1858
+ */
1763
1859
  private _resolveStaticRecordLiteral(objectName: string, key: string): string | null {
1860
+ if (this.staticLoopSourceBoundNames.has(objectName)) return null
1764
1861
  const hit = lookupStaticRecordLiteral(objectName, key, this.localConstants)
1765
1862
  if (!hit) return null
1766
1863
  return hit.kind === 'number'
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Serialize a compile-time-evaluated JS value (`@barefootjs/jsx`'s
3
+ * `evaluateStaticLiteral`/`resolveStaticLoopSource`, #2208) into a native
4
+ * Jinja2 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
+ * (e.g. `undefined` reads for a missing object key are still representable
11
+ * as `none`, but anything else falls back to the caller's existing BF101
12
+ * refusal instead of guessing).
13
+ */
14
+
15
+ import { escapeJinjaSingleQuoted, jinjaHashKey } from './jinja-naming.ts'
16
+
17
+ export function staticValueToJinja(value: unknown): string | null {
18
+ if (value === null || value === undefined) return 'none'
19
+ if (typeof value === 'boolean') return value ? 'true' : 'false'
20
+ if (typeof value === 'number') return String(value)
21
+ if (typeof value === 'string') return `'${escapeJinjaSingleQuoted(value)}'`
22
+ if (Array.isArray(value)) {
23
+ const items: string[] = []
24
+ for (const el of value) {
25
+ const serialized = staticValueToJinja(el)
26
+ if (serialized === null) return null
27
+ items.push(serialized)
28
+ }
29
+ return `[${items.join(', ')}]`
30
+ }
31
+ if (typeof value === 'object') {
32
+ const entries: string[] = []
33
+ for (const [key, val] of Object.entries(value as Record<string, unknown>)) {
34
+ const serialized = staticValueToJinja(val)
35
+ if (serialized === null) return null
36
+ entries.push(`${jinjaHashKey(key)}: ${serialized}`)
37
+ }
38
+ return `{${entries.join(', ')}}`
39
+ }
40
+ return null
41
+ }
@@ -11,15 +11,17 @@
11
11
  import type { ConformancePins } from '@barefootjs/jsx'
12
12
 
13
13
  export const conformancePins: ConformancePins = {
14
- // Sibling-imported child component in a loop body: emits a
15
- // cross-template call needing separate registration. BF103 makes
16
- // the requirement loud (same as xslate).
17
- 'static-array-children': [{ code: 'BF103', severity: 'error' }],
18
- // TodoApp / TodoAppSSR import `TodoItem` from a sibling file and
19
- // call it inside a keyed `.map`. Same BF103 (imported child in
20
- // `.map`) as xslate.
21
- 'todo-app': [{ code: 'BF103', severity: 'error' }],
22
- 'todo-app-ssr': [{ code: 'BF103', severity: 'error' }],
14
+ // `todo-app` / `todo-app-ssr` no longer pinned (#2205) the conformance
15
+ // harness now passes `siblingTemplatesRegistered: true` for fixtures with
16
+ // sibling `components`, matching `bf build`'s real semantics, so the
17
+ // BF103 loop-body cross-template check no longer fires spuriously. (Both
18
+ // fixtures are still skipped on this adapter via `render-divergences.ts`
19
+ // #2209 for an unrelated signal-seeding gap.)
20
+ // `static-array-children` no longer pinned (#2208) `items`'s
21
+ // array-literal initializer is now recognized as fully-static
22
+ // (`resolveStaticLoopSource`) and inlined as a native Jinja list/dict
23
+ // literal in the `{% for %}` header, the same way a module-scope const's
24
+ // value is already seeded.
23
25
  // #2087 Phase A/B widened the destructure gate (`isLowerableLoopDestructure`)
24
26
  // to admit array-index / nested-path fixed bindings, so the
25
27
  // `([emoji, users]) => ...` / `([id, t]) => ...` params in these two
@@ -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 / structured-path `.map()` callbacks (#2087 Phase B):
@@ -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
  }
@@ -14,4 +14,9 @@
14
14
 
15
15
  import type { RenderDivergences } from '@barefootjs/jsx'
16
16
 
17
- export const renderDivergences: RenderDivergences = {}
17
+ export const renderDivergences: RenderDivergences = {
18
+ // `todo-app` / `todo-app-ssr` no longer diverge (#2209) — the shared
19
+ // `evaluateSignalInit` (`@barefootjs/jsx`, sandboxed real-JS evaluation
20
+ // instead of a fixed regex-shape catalogue) now correctly seeds `todos`
21
+ // from `(props.initialTodos ?? []).map(t => ({ ...t, editing: false }))`.
22
+ }
@@ -16,7 +16,7 @@
16
16
  * generated render script (Python, not Perl) and its literal syntax differ.
17
17
  */
18
18
 
19
- import { compileJSX, extractSsrDefaults, importsSearchParams } from '@barefootjs/jsx'
19
+ import { compileJSX, extractSsrDefaults, importsSearchParams, evaluateSignalInit, tryEvaluateSignalInit } from '@barefootjs/jsx'
20
20
  import type { ComponentIR } from '@barefootjs/jsx'
21
21
  import { mkdir, rm } from 'node:fs/promises'
22
22
  import { resolve } from 'node:path'
@@ -125,8 +125,15 @@ export async function renderJinjaComponent(options: RenderOptions): Promise<stri
125
125
  }
126
126
  }
127
127
 
128
- // Compile parent source.
129
- const result = compileJSX(source, 'component.tsx', { adapter, outputIR: true })
128
+ // Compile parent source. `siblingTemplatesRegistered: Boolean(components)`
129
+ // matches this harness's real behavior every sibling child template is registered
130
+ // alongside the parent before rendering, so a loop-body cross-template
131
+ // call resolves at render time (#2205).
132
+ const result = compileJSX(source, 'component.tsx', {
133
+ adapter,
134
+ outputIR: true,
135
+ siblingTemplatesRegistered: Boolean(components),
136
+ })
130
137
 
131
138
  const errors = result.errors.filter(e => e.severity === 'error')
132
139
  if (errors.length > 0) {
@@ -461,9 +468,9 @@ function buildPythonProps(
461
468
  for (const param of ir.metadata.propsParams) {
462
469
  if (props && param.name in props) continue
463
470
  if (param.defaultValue) {
464
- const pyValue = jsToPyValue(param.defaultValue)
465
- if (pyValue !== null) {
466
- entries.push(`${pyStr(param.name)}: ${pyValue}`)
471
+ const result = tryEvaluateSignalInit(param.defaultValue.trim(), props)
472
+ if (result.ok) {
473
+ entries.push(`${pyStr(param.name)}: ${toPyLiteral(result.value)}`)
467
474
  continue
468
475
  }
469
476
  }
@@ -539,119 +546,6 @@ function buildPythonProps(
539
546
  return `{${entries.join(', ')}}`
540
547
  }
541
548
 
542
- /**
543
- * Evaluate a signal initializer expression using provided props.
544
- * Handles: props.initial ?? 0, props.value, literal values.
545
- */
546
- export function evaluateSignalInit(
547
- expr: string,
548
- props?: Record<string, unknown>,
549
- ): unknown {
550
- const nullishMatch = expr.match(/^props\.(\w+)\s*\?\?\s*(.+)$/)
551
- if (nullishMatch) {
552
- const propName = nullishMatch[1]
553
- const defaultExpr = nullishMatch[2].trim()
554
- if (props && propName in props) return props[propName]
555
- return parseLiteral(defaultExpr)
556
- }
557
-
558
- const propsMatch = expr.match(/^props\.(\w+)$/)
559
- if (propsMatch) {
560
- if (props && propsMatch[1] in props) return props[propsMatch[1]]
561
- return null
562
- }
563
-
564
- return parseLiteral(expr)
565
- }
566
-
567
- function parseLiteral(expr: string): unknown {
568
- if (/^-?\d+(\.\d+)?$/.test(expr)) return Number(expr)
569
- if (expr === 'true') return true
570
- if (expr === 'false') return false
571
- if (expr === '[]') return []
572
-
573
- {
574
- const t = expr.trim()
575
- if (t.startsWith('[') && t.endsWith(']')) {
576
- const inner = t.slice(1, -1).trim()
577
- if (!inner) return []
578
- const out: unknown[] = []
579
- for (const seg of splitTopLevelCommas(inner)) {
580
- if (!seg.trim()) continue
581
- const parsed = parseLiteral(seg.trim())
582
- if (parsed === null && seg.trim() !== 'null') return null
583
- out.push(parsed)
584
- }
585
- return out
586
- }
587
- }
588
-
589
- const stringMatch = expr.match(/^(['"])(.*)\1$/s)
590
- if (stringMatch) return unescapeJsString(stringMatch[2])
591
-
592
- const trimmed = expr.trim()
593
- if (trimmed.startsWith('{') && trimmed.endsWith('}')) {
594
- const inner = trimmed.slice(1, -1).trim()
595
- if (!inner) return {}
596
- const obj: Record<string, unknown> = {}
597
- for (const pair of splitTopLevelCommas(inner)) {
598
- if (!pair.trim()) continue
599
- const colonIdx = pair.indexOf(':')
600
- if (colonIdx < 0) return null
601
- let key = pair.slice(0, colonIdx).trim()
602
- const val = pair.slice(colonIdx + 1).trim()
603
- const keyMatch = key.match(/^(['"])(.*)\1$/s)
604
- if (keyMatch) key = unescapeJsString(keyMatch[2])
605
- const parsedVal = parseLiteral(val)
606
- if (parsedVal === null && val !== 'null') return null
607
- obj[key] = parsedVal
608
- }
609
- return obj
610
- }
611
- return null
612
- }
613
-
614
- function splitTopLevelCommas(inner: string): string[] {
615
- const segments: string[] = []
616
- let depth = 0
617
- let start = 0
618
- let quote: string | null = null
619
- for (let i = 0; i < inner.length; i++) {
620
- const c = inner[i]
621
- if (quote) {
622
- if (c === quote) {
623
- let backslashes = 0
624
- for (let j = i - 1; j >= 0 && inner[j] === '\\'; j--) backslashes++
625
- if (backslashes % 2 === 0) quote = null
626
- }
627
- continue
628
- }
629
- if (c === '"' || c === "'") {
630
- quote = c
631
- continue
632
- }
633
- if (c === '{' || c === '[') depth++
634
- else if (c === '}' || c === ']') depth--
635
- else if (c === ',' && depth === 0) {
636
- segments.push(inner.slice(start, i))
637
- start = i + 1
638
- }
639
- }
640
- segments.push(inner.slice(start))
641
- return segments
642
- }
643
-
644
- function unescapeJsString(s: string): string {
645
- return s.replace(/\\(.)/g, (_, c) => {
646
- switch (c) {
647
- case 'n': return '\n'
648
- case 'r': return '\r'
649
- case 't': return '\t'
650
- case '0': return '\0'
651
- default: return c
652
- }
653
- })
654
- }
655
549
 
656
550
  /**
657
551
  * Python string literal for arbitrary text, via `JSON.stringify`. JSON's
@@ -689,26 +583,3 @@ function toPyLiteral(value: unknown): string {
689
583
  return 'None'
690
584
  }
691
585
 
692
- /**
693
- * Convert a JS literal value to a Python literal.
694
- * Handles: numbers, strings, booleans, empty arrays, props.xxx ?? default.
695
- */
696
- function jsToPyValue(jsValue: string): string | null {
697
- const v = jsValue.trim()
698
-
699
- if (/^-?\d+(\.\d+)?$/.test(v)) return v
700
- // A JS string literal (single- or double-quoted) is, character-for-character,
701
- // ALSO a valid Python string literal for the common escape sequences both
702
- // languages share — pass it through verbatim rather than re-quoting.
703
- if (/^['"].*['"]$/.test(v)) return v
704
- if (v === 'true') return 'True'
705
- if (v === 'false') return 'False'
706
- if (v === '[]') return '[]'
707
-
708
- const nullishMatch = v.match(/\?\?\s*(.+)$/)
709
- if (nullishMatch) return jsToPyValue(nullishMatch[1])
710
-
711
- if (v.startsWith('props.')) return 'None'
712
-
713
- return null
714
- }