@barefootjs/jinja 0.18.5 → 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@barefootjs/jinja",
3
- "version": "0.18.5",
3
+ "version": "0.19.0",
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.19.0"
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.19.0",
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
+ })
@@ -51,6 +51,19 @@ runAdapterConformanceTests({
51
51
  // Same `/* @client */` keyed-map elision (data-table).
52
52
  'data-table',
53
53
  ]),
54
+ skipDataPoints: new Set<string>([
55
+ // #2255 — Python len() counts codepoints; JS counts UTF-16 code
56
+ // units, so a surrogate-pair character is 2 in JS, 1 here.
57
+ 'string-length-text:astral',
58
+ // #2260 — controlled boolean props: the SSR seed evaluates only the
59
+ // static fallback of `props.X ?? internal()` chains.
60
+ 'toggle:gen:pressed:true',
61
+ 'switch:gen:checked:true',
62
+ 'checkbox:gen:checked:true',
63
+ // #2261 — invalid dynamic CSS value kept (escaped) where the oracle
64
+ // drops the property.
65
+ 'style-object-dynamic:gen:color:markup',
66
+ ]),
54
67
  onRenderError: (err, id) => {
55
68
  if (err instanceof PythonNotAvailableError) {
56
69
  console.log(`Skipping [${id}]: ${err.message}`)
@@ -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
+ }
@@ -24,8 +24,9 @@ export function collectBooleanTypedProps(ir: ComponentIR): Set<string> {
24
24
  }
25
25
 
26
26
  /**
27
- * Bare references to optional, no-default, non-primitive props (e.g.
28
- * textarea's `rows`) are `None` when omitted → guarded with
27
+ * Bare references to presence-uncertain no-default props (non-primitive
28
+ * typed OR declared optional, #2259 — e.g. textarea's `rows`) are
29
+ * `None` when omitted → guarded with
29
30
  * `is defined and is not none` in `emitExpression`. See the
30
31
  * `nullableOptionalProps` field docstring in `jinja-adapter.ts`.
31
32
  */
@@ -36,7 +37,7 @@ export function collectNullableOptionalProps(ir: ComponentIR): Set<string> {
36
37
  p =>
37
38
  p.defaultValue === undefined &&
38
39
  !p.isRest &&
39
- p.type?.kind !== 'primitive',
40
+ (p.type?.kind !== 'primitive' || p.optional),
40
41
  )
41
42
  .map(p => p.name),
42
43
  )
@@ -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
+ }