@barefootjs/go-template 0.18.4 → 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.
Files changed (37) hide show
  1. package/dist/adapter/analysis/static-child-loop-bake.d.ts +61 -0
  2. package/dist/adapter/analysis/static-child-loop-bake.d.ts.map +1 -0
  3. package/dist/adapter/analysis/static-element-loop-bake.d.ts +83 -0
  4. package/dist/adapter/analysis/static-element-loop-bake.d.ts.map +1 -0
  5. package/dist/adapter/go-template-adapter.d.ts +151 -3
  6. package/dist/adapter/go-template-adapter.d.ts.map +1 -1
  7. package/dist/adapter/index.js +500 -52
  8. package/dist/adapter/lib/compile-state.d.ts +15 -0
  9. package/dist/adapter/lib/compile-state.d.ts.map +1 -1
  10. package/dist/adapter/lib/constants.d.ts.map +1 -1
  11. package/dist/adapter/memo/memo-compute.d.ts.map +1 -1
  12. package/dist/adapter/props/prop-classes.d.ts +40 -0
  13. package/dist/adapter/props/prop-classes.d.ts.map +1 -0
  14. package/dist/adapter/props/prop-types.d.ts.map +1 -1
  15. package/dist/adapter/type/type-codegen.d.ts +5 -1
  16. package/dist/adapter/type/type-codegen.d.ts.map +1 -1
  17. package/dist/adapter/value/value-lowering.d.ts.map +1 -1
  18. package/dist/build.js +500 -52
  19. package/dist/conformance-pins.d.ts.map +1 -1
  20. package/dist/index.js +503 -79
  21. package/dist/render-divergences.d.ts.map +1 -1
  22. package/dist/test-render.d.ts.map +1 -1
  23. package/package.json +3 -3
  24. package/src/__tests__/go-template-adapter.test.ts +708 -4
  25. package/src/adapter/analysis/static-child-loop-bake.ts +119 -0
  26. package/src/adapter/analysis/static-element-loop-bake.ts +211 -0
  27. package/src/adapter/go-template-adapter.ts +661 -34
  28. package/src/adapter/lib/compile-state.ts +17 -0
  29. package/src/adapter/lib/constants.ts +1 -0
  30. package/src/adapter/memo/memo-compute.ts +37 -9
  31. package/src/adapter/props/prop-classes.ts +70 -0
  32. package/src/adapter/props/prop-types.ts +69 -1
  33. package/src/adapter/type/type-codegen.ts +19 -2
  34. package/src/adapter/value/value-lowering.ts +27 -2
  35. package/src/conformance-pins.ts +30 -36
  36. package/src/render-divergences.ts +12 -30
  37. package/src/test-render.ts +131 -13
@@ -66,6 +66,15 @@ export class CompileState {
66
66
  */
67
67
  localConstants: IRMetadata['localConstants'] = []
68
68
 
69
+ /**
70
+ * Every name a `.map()`/`.filter()` loop callback binds as its item/index
71
+ * parameter anywhere in the component (#2208 fable review). Consulted by
72
+ * static loop-source resolution (`getBakedStaticChildLoop`) so a const
73
+ * whose name a DIFFERENT, enclosing loop's own callback param shadows is
74
+ * never resolved as that const's static value.
75
+ */
76
+ staticLoopSourceBoundNames: Set<string> = new Set()
77
+
69
78
  /**
70
79
  * Names of component-scope arrow-const helpers (`const sortClass = …`),
71
80
  * eligible for call-site inlining.
@@ -137,6 +146,14 @@ export class CompileState {
137
146
  */
138
147
  nillablePropNames: Set<string> = new Set()
139
148
 
149
+ /**
150
+ * String-typed signal getter / prop names (#2168 string-concat-plus).
151
+ * Feeds `isStringName` for `isStringConcatBinary`, which decides whether a
152
+ * JS `+` operand chain is string concatenation rather than numeric
153
+ * addition — see `collectStringValueNames` (`props/prop-classes.ts`).
154
+ */
155
+ stringValueNames: Set<string> = new Set()
156
+
140
157
  /** Component root scope element(s) — each carries `data-key` for a keyed loop
141
158
  * item. */
142
159
  rootScopeNodes: Set<IRNode> = new Set()
@@ -17,6 +17,7 @@ export const GO_TEMPLATE_PRIMITIVES: Record<string, PrimitiveSpec> = {
17
17
  'Math.floor': { arity: 1, emit: (args) => `bf_floor ${wrapGoArg(args[0])}` },
18
18
  'Math.ceil': { arity: 1, emit: (args) => `bf_ceil ${wrapGoArg(args[0])}` },
19
19
  'Math.round': { arity: 1, emit: (args) => `bf_round ${wrapGoArg(args[0])}` },
20
+ 'Math.abs': { arity: 1, emit: (args) => `bf_abs ${wrapGoArg(args[0])}` },
20
21
  // Two-arg forms only; an N-arg `Math.min(a, b, c)` falls through to the
21
22
  // standard BF101 unsupported-call diagnostic via the arity gate.
22
23
  'Math.min': { arity: 2, emit: (args) => `bf_min ${wrapGoArg(args[0])} ${wrapGoArg(args[1])}` },
@@ -366,13 +366,40 @@ export function memoInitialFromParsedBody(
366
366
  const operator = body.op
367
367
  const operand = String(body.right.value)
368
368
 
369
- // getter() * N — return the signal's Go initial value times N.
369
+ // getter() * N — return the signal's (or, #2168 memo-chain, another
370
+ // memo's) Go initial value times N. `resolveGetterValueAsGo` checks
371
+ // `signals` first (unchanged behavior for a signal-derived memo like
372
+ // `doubled = createMemo(() => count() * 2)`), then falls back to
373
+ // `ctx.state.currentMemos` and recurses — needed for a memo derived from
374
+ // ANOTHER memo (`label = createMemo(() => doubled() + 1)`), which this
375
+ // branch previously couldn't recognize at all (a signals-only lookup),
376
+ // silently folding to the Go zero value instead of "7".
370
377
  const depName = getterCallName(body.left)
371
378
  if (depName) {
372
- const signal = signals.find(s => s.getter === depName)
373
- if (signal) {
374
- const signalInitial = getSignalInitialValueAsGo(ctx, signal.initialValue, propsParams, propFallbackVars)
375
- return `${signalInitial} ${operator} ${operand}`
379
+ const depInitial = resolveGetterValueAsGo(ctx, depName, signals, propsParams, propFallbackVars, resolving)
380
+ // `resolveGetterValueAsGo` can return an IIFE (`func() string { ... }()`
381
+ // / `func() interface{} { ... }()`, e.g. a memo that shadows `props.X
382
+ // ?? <lit>`, #2075) rather than a plain atom — none of those return
383
+ // types support Go's `<op>` arithmetic operators, so splicing one in
384
+ // bare would emit invalid Go (Copilot review, #2200: e.g. `operator
385
+ // is not defined on interface{}`/`string`). Bail (fall through to the
386
+ // caller's zero-value default) rather than emit broken arithmetic.
387
+ const isArithmeticSafe = depInitial !== null && !depInitial.startsWith('func(')
388
+ if (isArithmeticSafe) {
389
+ // A signal's own initial value is always a simple atom (a literal,
390
+ // `in.Field`, a hoisted var) — never needs grouping. A MEMO's initial
391
+ // value can itself be a compound expression from this exact branch
392
+ // one level up (`"3 * 2"`), and splicing that in bare under a
393
+ // DIFFERENT outer operator can silently invert precedence (a memo
394
+ // chain shaped `inner = () => count() + 1` then `outer = () =>
395
+ // inner() * 2` would fold to `3 + 1 * 2` = 5 in Go, vs JS's `(3+1)*2`
396
+ // = 8). Parenthesize whenever `depInitial` is compound (contains
397
+ // whitespace — a simple atom never does) so precedence is
398
+ // preserved regardless of which operator combination a memo chain
399
+ // uses. A simple atom is left bare so existing exact-text
400
+ // expectations (`Doubled: 3 * 2,`) don't gain a no-op paren.
401
+ const wrapped = /\s/.test(depInitial) ? `(${depInitial})` : depInitial
402
+ return `${wrapped} ${operator} ${operand}`
376
403
  }
377
404
  }
378
405
 
@@ -408,12 +435,13 @@ export function memoInitialFromParsedBody(
408
435
  }
409
436
  }
410
437
 
411
- // () => getter() — just return the signal's Go initial value.
438
+ // () => getter() — just return the signal's (or another memo's, #2168
439
+ // memo-chain) Go initial value.
412
440
  const simpleDep = getterCallName(body)
413
441
  if (simpleDep) {
414
- const signal = signals.find(s => s.getter === simpleDep)
415
- if (signal) {
416
- return getSignalInitialValueAsGo(ctx, signal.initialValue, propsParams, propFallbackVars)
442
+ const depInitial = resolveGetterValueAsGo(ctx, simpleDep, signals, propsParams, propFallbackVars, resolving)
443
+ if (depInitial !== null) {
444
+ return depInitial
417
445
  }
418
446
  }
419
447
 
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Prop classification for the Go template adapter.
3
+ *
4
+ * Ported from `packages/adapter-blade/src/adapter/props/prop-classes.ts` (itself
5
+ * ported from Jinja) — ONE function: string-typed signal/prop names, needed to
6
+ * decide `+` string-concat vs numeric addition (#2168 string-concat-plus). Pure
7
+ * function over `ir.metadata`; no adapter instance state.
8
+ */
9
+
10
+ import { collectLoopBoundNames, type ComponentIR, type TypeInfo } from '@barefootjs/jsx'
11
+
12
+ /** True when `type` is the `string` primitive. */
13
+ function isStringTypeInfo(type: TypeInfo): boolean {
14
+ return type.kind === 'primitive' && type.primitive === 'string'
15
+ }
16
+
17
+ /** True when `initialValue` is a bare string-literal expression. */
18
+ function isBareStringLiteral(initialValue: string | undefined): boolean {
19
+ if (!initialValue) return false
20
+ const v = initialValue.trim()
21
+ return (v.startsWith("'") && v.endsWith("'")) || (v.startsWith('"') && v.endsWith('"'))
22
+ }
23
+
24
+ /**
25
+ * String-typed signals, props, and same-file local consts (#2212, ported
26
+ * here for #2236). A signal is string-typed when its inferred type is
27
+ * `string` (or, defensively, when its initial value is a bare string
28
+ * literal); a prop when its annotated type is `string`; a local const the
29
+ * same way. Drives `isStringName` for `isStringConcatBinary` — the shared
30
+ * helper (`@barefootjs/jsx`) that decides whether a JS `+` is string
31
+ * concatenation rather than numeric addition (Go's `html/template` has no
32
+ * native `+` at all; `binary()` always emits a runtime call, `bf_add` for
33
+ * addition or `bf_concat_str` for concatenation — see
34
+ * `go-template-adapter.ts`'s `binary()`). Local consts matter for exactly
35
+ * the shadowing shape this exclusion exists for: with a loop-bound `label`
36
+ * subtracted, an outer `{label + suffix}` (where `suffix = '!'`) must
37
+ * still classify as string concat via its OTHER operand, or it would fall
38
+ * back to `bf_add` and render `0`.
39
+ *
40
+ * Excludes any name bound as a `.map()`/`.filter()` loop callback's item or
41
+ * index parameter ANYWHERE in the component (#2212, ported here for #2236):
42
+ * this lookup is a flat, scope-blind `Set<string>` with no notion of a loop
43
+ * param shadowing an outer string-typed binding of the same name
44
+ * (`values.map((label) => 1 + label)` inside a component that also has a
45
+ * string `label` prop) — left unguarded, that shadowed `label` would be
46
+ * misdetected as string-typed and `1 + label` would silently lower to
47
+ * `bf_concat_str` instead of staying numeric `bf_add`. Subtracting loop-bound
48
+ * names is coarse (it also suppresses a genuinely non-shadowed same-named
49
+ * string elsewhere in the component) but safe: the suppressed case just
50
+ * falls back to today's numeric `bf_add` — the same, already-accepted
51
+ * residual as an unresolvable operand — never silently-wrong output.
52
+ */
53
+ export function collectStringValueNames(ir: ComponentIR): Set<string> {
54
+ const names = new Set<string>()
55
+ for (const s of ir.metadata.signals) {
56
+ if (isStringTypeInfo(s.type) || isBareStringLiteral(s.initialValue)) {
57
+ names.add(s.getter)
58
+ }
59
+ }
60
+ for (const p of ir.metadata.propsParams) {
61
+ if (isStringTypeInfo(p.type)) names.add(p.name)
62
+ }
63
+ for (const c of ir.metadata.localConstants) {
64
+ if ((c.type !== null && isStringTypeInfo(c.type)) || isBareStringLiteral(c.value)) {
65
+ names.add(c.name)
66
+ }
67
+ }
68
+ for (const bound of collectLoopBoundNames(ir)) names.delete(bound)
69
+ return names
70
+ }
@@ -5,7 +5,7 @@
5
5
  * generators and the nillable-field set so they can't drift.
6
6
  */
7
7
 
8
- import type { ComponentIR, IRMetadata } from '@barefootjs/jsx'
8
+ import type { ComponentIR, IRMetadata, IRNode, ParsedExpr } from '@barefootjs/jsx'
9
9
 
10
10
  import type { GoEmitContext } from '../emit-context.ts'
11
11
  import { typeInfoToGo } from '../type/type-codegen.ts'
@@ -35,9 +35,77 @@ export function buildPropTypeOverrides(ctx: GoEmitContext, ir: ComponentIR): Map
35
35
  }
36
36
  }
37
37
  }
38
+
39
+ // A bare `number`-typed prop with no other evidence resolves to Go `int`
40
+ // (`typeInfoToGo`'s blind default) — but `.toFixed()` can only be called on
41
+ // a real JS number, and the runtime value it formats (e.g. a price, 19.5)
42
+ // may be fractional, which a Go `int` struct field can't hold (assigning a
43
+ // fractional untyped constant to it is a compile error: #2168
44
+ // number-tofixed). Unlike a signal's fractional LITERAL initial value
45
+ // (rescued by `typeInfoToGo`'s own `defaultValue` consultation — the
46
+ // math-methods half of the same divergence), a prop with no default has no
47
+ // literal to read the fraction off of; the usage of `.toFixed()` itself is
48
+ // the only available evidence, so it's collected by walking the JSX tree.
49
+ for (const propName of collectToFixedPropNames(ir.root)) {
50
+ const param = ir.metadata.propsParams.find(p => p.name === propName)
51
+ if (!param) continue
52
+ const resolved = overrides.get(propName) ?? typeInfoToGo(ctx, param.type, param.defaultValue)
53
+ if (resolved === 'int') {
54
+ overrides.set(propName, 'float64')
55
+ }
56
+ }
57
+
38
58
  return overrides
39
59
  }
40
60
 
61
+ /**
62
+ * Names of identifiers used as the receiver of `.toFixed(...)` anywhere in
63
+ * the component's JSX tree (text expressions, conditions, and attribute
64
+ * values). Deliberately narrow — `.toFixed()` is the one number-shape usage
65
+ * that needs this rescue (see `buildPropTypeOverrides` above); it isn't a
66
+ * general "infer number-ness from usage" walker.
67
+ *
68
+ * KNOWN LIMITATION: only a DIRECT identifier receiver in the JSX tree is
69
+ * caught (`{price.toFixed(2)}`). A bare `number` prop with no default is
70
+ * still silently typed `int` — and hits the exact same `go run` compile
71
+ * failure (#2168 number-tofixed) on a fractional runtime value — if the
72
+ * fraction only surfaces indirectly (`.toFixed()` inside a signal's
73
+ * initial value or a memo's computation, reached via `ir.metadata` rather
74
+ * than `ir.root`) or via any OTHER fraction-producing operation on the
75
+ * same bare prop (division, `Math.round`/`Math.floor`, etc. — none of
76
+ * which carry the same unambiguous "this must be a real JS number" signal
77
+ * `.toFixed()` does). Widening this walker to those cases is a real,
78
+ * currently-unaddressed gap, not a hypothetical one — flag it rather than
79
+ * treating a future occurrence as a fresh regression.
80
+ */
81
+ function collectToFixedPropNames(root: IRNode): Set<string> {
82
+ const names = new Set<string>()
83
+ const checkExpr = (expr: ParsedExpr | undefined) => {
84
+ if (expr?.kind === 'array-method' && expr.method === 'toFixed' && expr.object.kind === 'identifier') {
85
+ names.add(expr.object.name)
86
+ }
87
+ }
88
+ const walk = (node: IRNode | null | undefined) => {
89
+ if (!node) return
90
+ if (node.type === 'expression') checkExpr(node.parsed)
91
+ if (node.type === 'conditional') {
92
+ checkExpr(node.parsedCondition)
93
+ walk(node.whenTrue)
94
+ walk(node.whenFalse)
95
+ }
96
+ if (node.type === 'element') {
97
+ for (const attr of node.attrs) {
98
+ if (attr.value.kind === 'expression') checkExpr(attr.value.parsed)
99
+ }
100
+ }
101
+ if ('children' in node && Array.isArray(node.children)) {
102
+ node.children.forEach(walk)
103
+ }
104
+ }
105
+ walk(root)
106
+ return names
107
+ }
108
+
41
109
  /**
42
110
  * Resolve a prop param's Go struct-field type using the SAME logic
43
111
  * `generatePropsStruct` / `generateInputStruct` use: a `propTypeOverrides` entry
@@ -18,7 +18,11 @@ import type { GoEmitContext } from '../emit-context.ts'
18
18
  /**
19
19
  * Convert a `TypeInfo` to a Go type string.
20
20
  *
21
- * @param defaultValue used to infer the type when `typeInfo.kind` is `unknown`
21
+ * @param defaultValue used to infer the type when `typeInfo.kind` is
22
+ * `unknown`, and to distinguish `int` vs `float64` when `kind` is
23
+ * `primitive`/`number` (#2168 math-methods/number-tofixed — a bare TS
24
+ * `number` blindly mapped to Go `int`, so a fractional signal initial
25
+ * value like `-7.6` silently truncated to the Go zero value)
22
26
  * @returns the Go type, falling back to `interface{}` when unresolvable
23
27
  */
24
28
  export function typeInfoToGo(
@@ -32,7 +36,7 @@ export function typeInfoToGo(
32
36
  case 'string':
33
37
  return 'string'
34
38
  case 'number':
35
- return 'int'
39
+ return defaultValue !== undefined ? numberPrimitiveGoType(defaultValue) : 'int'
36
40
  case 'boolean':
37
41
  return 'bool'
38
42
  default:
@@ -99,6 +103,19 @@ export function tsTypeStringToGo(ctx: GoEmitContext, tsType: string): string {
99
103
  return 'interface{}'
100
104
  }
101
105
 
106
+ /**
107
+ * Distinguish Go `int` vs `float64` for a `number`-typed field from the
108
+ * literal source text of its default/initial value. Falls back to `int`
109
+ * when `value` isn't recognizably a bare numeric literal (e.g. a
110
+ * destructured default that's itself an expression, `props.initial ?? 0`)
111
+ * — `int` remains the blind fallback for `kind: 'primitive'`; only a
112
+ * literal fractional value (`-7.6`) is positive enough evidence to widen
113
+ * to `float64`.
114
+ */
115
+ function numberPrimitiveGoType(value: string): string {
116
+ return /^-?\d+\.\d+$/.test(value) ? 'float64' : 'int'
117
+ }
118
+
102
119
  /** Infer a Go type from a JS value literal; `interface{}` when unrecognized. */
103
120
  export function inferTypeFromValue(value: string): string {
104
121
  if (value === 'true' || value === 'false') return 'bool'
@@ -42,8 +42,12 @@ export function convertInitialValue(
42
42
  return value === 'true' ? 'true' : 'false'
43
43
  }
44
44
  if (typeInfo.primitive === 'number') {
45
- if (/^\d+$/.test(value)) return value
46
- if (/^\d+\.\d+$/.test(value)) return value
45
+ // Leading `-` (#2168 math-methods: `createSignal(-7.6)`) without it,
46
+ // a negative initial value never matches either literal shape below
47
+ // and silently falls to the `0` zero-value fallback, regardless of the
48
+ // field's Go type.
49
+ if (/^-?\d+$/.test(value)) return value
50
+ if (/^-?\d+\.\d+$/.test(value)) return value
47
51
  return '0'
48
52
  }
49
53
  if (typeInfo.primitive === 'string') {
@@ -70,6 +74,27 @@ export function convertInitialValue(
70
74
  }
71
75
  return '""'
72
76
  }
77
+ // A struct-backed `interface` kind (an explicitly-typed object signal,
78
+ // `createSignal<User>({...})`) — #2168 signal-object-field. Mirrors the
79
+ // `array` branch above: `jsLiteralToGo` → `parsedLiteralToGo`'s
80
+ // object-literal case already bakes an object literal against a named
81
+ // local struct correctly (proven by the existing typed-array-of-objects
82
+ // test); it just wasn't reachable from a SCALAR struct signal, which
83
+ // fell straight through to `nil` — a compile error for a non-pointer
84
+ // struct field (`cannot use nil as User value in struct literal`), not
85
+ // merely a silently-dropped initial value.
86
+ if (ctx.state.localStructFields.has(typeInfo.raw)) {
87
+ const baked = jsLiteralToGo(ctx, typeInfo, preParsed)
88
+ if (baked !== null) return baked
89
+ // Baking failed (a non-literal initial value, or no `preParsed` tree)
90
+ // — `nil` is STILL invalid Go for this non-pointer struct field, so
91
+ // the same compile error would resurface for any such case (Copilot
92
+ // review, #2201). The struct's own zero value (`User{}`) is the
93
+ // correct fallback here — mirrors this function's own docstring
94
+ // ("falls back to the type's zero value") for every other typed
95
+ // branch above.
96
+ return `${typeInfo.raw}{}`
97
+ }
73
98
  }
74
99
 
75
100
  return 'nil'
@@ -15,19 +15,21 @@ export const conformancePins: ConformancePins = {
15
15
  // `style={{ … }}` object literal now lowers to a CSS string with dynamic
16
16
  // values interpolated (`background-color:{{.Color}};padding:8px`) via
17
17
  // `tryLowerStyleObject` (#1322).
18
- // Sibling-imported child component inside a loop body: the adapter
19
- // emits `{{template "X" .}}` which only resolves if the user has
20
- // compiled the sibling file and registered the template on the
21
- // same instance. BF103 makes that requirement loud. (The barefoot
22
- // CLI passes `siblingTemplatesRegistered: true` so CLI builds
23
- // suppress the diagnosticsee compileJSX `siblingTemplatesRegistered`.)
24
- 'static-array-children': [{ code: 'BF103', severity: 'error' }],
25
- // TodoApp / TodoAppSSR import `TodoItem` from a sibling file and
26
- // call it inside a keyed `.map`. Same BF103 surface as
27
- // `static-array-children` above pinned at adapter level so the
28
- // shared-component corpus stays adapter-neutral.
29
- 'todo-app': [{ code: 'BF103', severity: 'error' }],
30
- 'todo-app-ssr': [{ code: 'BF103', severity: 'error' }],
18
+ // `todo-app` / `todo-app-ssr` no longer pinned (#2205) the conformance
19
+ // harness now passes `siblingTemplatesRegistered: true` for fixtures with
20
+ // sibling `components`, matching `bf build`'s real semantics, so the
21
+ // BF103 loop-body cross-template check no longer fires spuriously.
22
+ // (`todo-app-ssr` is still skipped on this adapter via
23
+ // `render-divergences.ts` #2209for an unrelated signal-seeding gap;
24
+ // `todo-app`'s pre-hydration empty render is unaffected.)
25
+ // `static-array-children` no longer pinned (#2208) — `items`'s
26
+ // array-literal initializer is now recognized as fully-static and its
27
+ // per-item ListItem props/data-key are baked directly into
28
+ // `NewStaticListProps`'s constructor (`analyzeBakeableStaticChildLoop`),
29
+ // since the loop body is a single child component with a plain-value
30
+ // prop set. See #2224 for the narrower remaining gap (a plain-ELEMENT
31
+ // loop body over a static array, or an inline/unnamed array literal —
32
+ // still refused).
31
33
  // `([emoji, users]) => ...` is an array-index tuple destructure — #2087
32
34
  // Phase B's widened gate now admits this shape (`destructure-array-index-in-map`
33
35
  // exercises the same `segments`-based lowering). The remaining refusal here
@@ -40,13 +42,11 @@ export const conformancePins: ConformancePins = {
40
42
  // array bound to such a const. See the `renderLoop` comment at the check
41
43
  // site; Jinja / ERB apply the same narrow check for the same reason.
42
44
  'static-array-from-props': [{ code: 'BF101', severity: 'error' }],
43
- // Same computed-const array as above, plus the pre-existing BF103 (a
44
- // sibling-imported child component used inside the loop body) — the
45
- // destructure param itself no longer contributes a diagnostic.
46
- 'static-array-from-props-with-component': [
47
- { code: 'BF103', severity: 'error' },
48
- { code: 'BF101', severity: 'error' },
49
- ],
45
+ // Same computed-const array as above the destructure param itself no
46
+ // longer contributes a diagnostic, and BF103 (sibling-imported child
47
+ // component in the loop body) no longer fires either now that the
48
+ // conformance harness passes `siblingTemplatesRegistered: true` (#2205).
49
+ 'static-array-from-props-with-component': [{ code: 'BF101', severity: 'error' }],
50
50
  // (`style-3-signals` graduated alongside `style-object-dynamic` — see note
51
51
  // above; the `style={{ … }}` object now lowers to a CSS string.)
52
52
  // (`tagged-template-classname` graduated by #2092 — the tag resolves
@@ -127,20 +127,14 @@ export const conformancePins: ConformancePins = {
127
127
  // `string-trim` no longer pinned — pre-existing `bf_trim`
128
128
  // (wraps `strings.TrimSpace`) handles the strip (#1448 Tier A
129
129
  // ninth PR, closing out Tier A).
130
- // #2073 follow-up: a function-reference `.map(format)` callback has no
131
- // arrow body to serialize not a CALLBACK_METHODS shape — so the
132
- // UNSUPPORTED_METHODS gate refuses it with BF101 rather than emitting
133
- // a broken template.
134
- 'array-map-function-reference': [{ code: 'BF101', severity: 'error' }],
135
- // Edge-case sweep (Priority 12): `dangerouslySetInnerHTML` requires a
136
- // deliberate raw-HTML (unescaped) output affordance in the target
137
- // template language. No lowering exists yet, so the compiler refuses
138
- // the shape loudly instead of emitting entity-escaped markup that
139
- // silently renders tags as text.
140
- 'dangerous-inner-html': [{ code: 'BF101', severity: 'error' }],
141
- // Edge-case sweep (Priority 12): `.replaceAll` has no lowering yet —
142
- // only first-occurrence `.replace` is wired to the runtime helpers.
143
- // Refused with BF101 rather than reusing the first-only lowering,
144
- // which would silently change semantics.
145
- 'string-replaceall': [{ code: 'BF101', severity: 'error' }],
130
+ // `array-map-function-reference` no longer pinned — a bare-identifier
131
+ // `.map(format)` callback now resolves one hop to its declaration
132
+ // (`resolveCallbackMethodFunctionReferences`, #2206), the same mechanism
133
+ // #2090 established for `.sort(fnref)`.
134
+ // `dangerous-inner-html` no longer pinned a compile-time string-literal
135
+ // `dangerouslySetInnerHTML={{ __html: '...' }}` is spliced directly into
136
+ // the template as trusted raw text (`resolveDangerousInnerHtml`, #2207).
137
+ // A dynamic/signal-derived value still refuses with BF101 see the
138
+ // `dangerous-inner-html-dynamic` fixture/pin below (tracked: #2215).
139
+ 'dangerous-inner-html-dynamic': [{ code: 'BF101', severity: 'error', issue: 'https://github.com/piconic-ai/barefootjs/issues/2215' }],
146
140
  }
@@ -17,34 +17,16 @@
17
17
  import type { RenderDivergences } from '@barefootjs/jsx'
18
18
 
19
19
  export const renderDivergences: RenderDivergences = {
20
- 'html-entity-text':
21
- '`&copy;` in JSX literal text: Hono decodes to `©`, this adapter re-emits the raw entity — same DOM, different bytes',
22
- 'string-concat-plus':
23
- "`'Hello, ' + name` renders \"0\" JS string-concat `+` lowered through numeric addition",
24
- 'optional-chaining-prop':
25
- '`user?.name ?? …` on an object prop: generated Go fails to run (exit 1) optional chaining into a struct/map prop has no lowering',
26
- 'number-tofixed':
27
- '`.toFixed(2)` on a number PROP: generated Go fails to run (exit 1)',
28
- 'math-methods':
29
- 'Math.min/max/abs over a signal: generated Go fails to run (exit 1) — only Math.floor is registered',
30
- 'static-attr-escape':
31
- 'static attribute values are not HTML-escaped (`title="Fish & Chips"` emitted raw; Hono escapes)',
32
- 'object-entries-map':
33
- '`Object.entries(prop).map(([k, v]) => …)`: generated Go fails to run (exit 1) — no object-iteration loop lowering',
34
- 'nested-loop-outer-binding':
35
- 'nested-loop inner items carry `data-key` where the reference emits the depth-suffixed `data-key-1`',
36
- 'jsx-element-prop':
37
- 'a JSX element passed as a NON-children prop renders an empty slot — the element value is silently dropped',
38
- 'grandchild-composition':
39
- "three-level composition: the grandchild's threaded prop renders EMPTY — prop forwarding through two template-render layers loses the value",
40
- 'child-primitive-props':
41
- 'numeric/boolean LITERAL props on a child (`count={5}` `active={true}`) render as Go zero values (0 / false)',
42
- 'memo-chain':
43
- 'a memo derived from another memo renders EMPTY for the second layer — the constructor folds only one derivation level',
44
- 'signal-object-field':
45
- 'object-valued signal (`user().name`): generated Go fails to run (exit 1) — no struct synthesis outside loops',
46
- 'string-slice':
47
- '`.slice()` on a STRING routes through the array `bf_slice` helper and renders "[]" instead of the substring',
48
- 'string-trim-sided':
49
- '`.trimStart()` / `.trimEnd()`: generated Go fails to run (exit 1) — only both-sides `bf_trim` exists',
20
+ // `todo-app-ssr` no longer diverges (#2209). Two parts: (1) `.Todos`
21
+ // (the loop's DATUM slice) is already seeded straight from the caller's
22
+ // Input — the constructor derives it from `initialTodos`, and `[]Todo`
23
+ // zero-fills `Editing: false`, so the `.map(t => ({ ...t, editing:
24
+ // false }))` transform in the signal initializer was never actually the
25
+ // gap on Go, unlike the 7 template-string adapters. (2) The real gap was
26
+ // `.TodoItems []TodoItemProps` — the loop-body CHILD COMPONENT slice the
27
+ // template actually ranges over which has no server-side population
28
+ // path in this harness (documented as route-handler-populated in
29
+ // production). `buildDynamicChildLoopSeeding` (this package's
30
+ // `test-render.ts`) now replicates that documented contract for a
31
+ // signal-backed dynamic child-component loop.
50
32
  }