@barefootjs/rust 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/rust",
3
- "version": "0.18.5",
3
+ "version": "0.19.0",
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.19.0"
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.19.0",
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
+ })
@@ -56,6 +56,19 @@ runAdapterConformanceTests({
56
56
  // Same `/* @client */` keyed-map elision (data-table).
57
57
  'data-table',
58
58
  ]),
59
+ skipDataPoints: new Set<string>([
60
+ // #2255 — minijinja's length counts chars (codepoints); JS counts
61
+ // UTF-16 code units, so a surrogate-pair character is 2 in JS, 1 here.
62
+ 'string-length-text:astral',
63
+ // #2260 — controlled boolean props: the SSR seed evaluates only the
64
+ // static fallback of `props.X ?? internal()` chains.
65
+ 'toggle:gen:pressed:true',
66
+ 'switch:gen:checked:true',
67
+ 'checkbox:gen:checked:true',
68
+ // #2261 — invalid dynamic CSS value kept (escaped) where the oracle
69
+ // drops the property.
70
+ 'style-object-dynamic:gen:color:markup',
71
+ ]),
59
72
  onRenderError: (err, id) => {
60
73
  if (err instanceof RustNotAvailableError) {
61
74
  console.log(`Skipping [${id}]: ${err.message}`)
@@ -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'
@@ -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 `minijinja-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
  )
@@ -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
+ }