@barefootjs/jsx 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 (109) hide show
  1. package/dist/adapters/dangerous-inner-html.d.ts +77 -0
  2. package/dist/adapters/dangerous-inner-html.d.ts.map +1 -0
  3. package/dist/adapters/loop-bound-names.d.ts +22 -0
  4. package/dist/adapters/loop-bound-names.d.ts.map +1 -0
  5. package/dist/adapters/parsed-expr-emitter.d.ts +2 -2
  6. package/dist/adapters/parsed-expr-emitter.d.ts.map +1 -1
  7. package/dist/analyzer.d.ts.map +1 -1
  8. package/dist/expression-parser.d.ts +2 -1
  9. package/dist/expression-parser.d.ts.map +1 -1
  10. package/dist/index.d.ts +14 -4
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/index.js +1108 -307
  13. package/dist/ir-to-client-js/collect-elements.d.ts.map +1 -1
  14. package/dist/ir-to-client-js/control-flow/plan/build-event-delegation.d.ts.map +1 -1
  15. package/dist/ir-to-client-js/control-flow/plan/build-inner-loop.d.ts.map +1 -1
  16. package/dist/ir-to-client-js/control-flow/plan/build-loop-child-arm.d.ts.map +1 -1
  17. package/dist/ir-to-client-js/control-flow/plan/build-loop.d.ts.map +1 -1
  18. package/dist/ir-to-client-js/control-flow/plan/event-delegation.d.ts +10 -0
  19. package/dist/ir-to-client-js/control-flow/plan/event-delegation.d.ts.map +1 -1
  20. package/dist/ir-to-client-js/control-flow/plan/loop-child-arm.d.ts +9 -0
  21. package/dist/ir-to-client-js/control-flow/plan/loop-child-arm.d.ts.map +1 -1
  22. package/dist/ir-to-client-js/control-flow/plan/loop.d.ts +9 -0
  23. package/dist/ir-to-client-js/control-flow/plan/loop.d.ts.map +1 -1
  24. package/dist/ir-to-client-js/control-flow/shared.d.ts +45 -3
  25. package/dist/ir-to-client-js/control-flow/shared.d.ts.map +1 -1
  26. package/dist/ir-to-client-js/control-flow/stringify/event-delegation.d.ts.map +1 -1
  27. package/dist/ir-to-client-js/control-flow/stringify/loop-child-arm.d.ts.map +1 -1
  28. package/dist/ir-to-client-js/control-flow/stringify/loop.d.ts +1 -0
  29. package/dist/ir-to-client-js/control-flow/stringify/loop.d.ts.map +1 -1
  30. package/dist/ir-to-client-js/control-flow/stringify/reactive-effects.d.ts +10 -0
  31. package/dist/ir-to-client-js/control-flow/stringify/reactive-effects.d.ts.map +1 -1
  32. package/dist/ir-to-client-js/control-flow/stringify/skeleton-paths.d.ts +38 -0
  33. package/dist/ir-to-client-js/control-flow/stringify/skeleton-paths.d.ts.map +1 -0
  34. package/dist/ir-to-client-js/control-flow/stringify/template-parse.d.ts +19 -0
  35. package/dist/ir-to-client-js/control-flow/stringify/template-parse.d.ts.map +1 -1
  36. package/dist/ir-to-client-js/csr-substitute.d.ts +24 -0
  37. package/dist/ir-to-client-js/csr-substitute.d.ts.map +1 -1
  38. package/dist/ir-to-client-js/html-template.d.ts +42 -0
  39. package/dist/ir-to-client-js/html-template.d.ts.map +1 -1
  40. package/dist/ir-to-client-js/imports.d.ts +2 -2
  41. package/dist/ir-to-client-js/imports.d.ts.map +1 -1
  42. package/dist/ir-to-client-js/plan/static-array-child-init.d.ts +20 -1
  43. package/dist/ir-to-client-js/plan/static-array-child-init.d.ts.map +1 -1
  44. package/dist/ir-to-client-js/reactivity.d.ts.map +1 -1
  45. package/dist/ir-to-client-js/stringify/static-array-child-init.d.ts +1 -1
  46. package/dist/ir-to-client-js/types.d.ts +30 -0
  47. package/dist/ir-to-client-js/types.d.ts.map +1 -1
  48. package/dist/ir-to-client-js/utils.d.ts +25 -0
  49. package/dist/ir-to-client-js/utils.d.ts.map +1 -1
  50. package/dist/jsx-to-ir.d.ts.map +1 -1
  51. package/dist/signal-init-eval.d.ts +82 -0
  52. package/dist/signal-init-eval.d.ts.map +1 -0
  53. package/dist/static-literal.d.ts +45 -0
  54. package/dist/static-literal.d.ts.map +1 -0
  55. package/dist/types.d.ts +66 -0
  56. package/dist/types.d.ts.map +1 -1
  57. package/package.json +2 -2
  58. package/src/__tests__/__snapshots__/doc-examples.test.ts.snap +19 -13
  59. package/src/__tests__/create-selector.test.ts +110 -0
  60. package/src/__tests__/csr-template-loop-shadowing.test.ts +214 -0
  61. package/src/__tests__/dangerous-inner-html-resolver.test.ts +197 -0
  62. package/src/__tests__/event-delegation-index-param.test.ts +130 -0
  63. package/src/__tests__/expression-parser.test.ts +38 -0
  64. package/src/__tests__/inner-loop-svg-namespace.test.ts +245 -0
  65. package/src/__tests__/ir-walker.test.ts +1 -0
  66. package/src/__tests__/loop-bound-names.test.ts +102 -0
  67. package/src/__tests__/loop-plan-classification.test.ts +2 -0
  68. package/src/__tests__/map-function-reference.test.ts +303 -0
  69. package/src/__tests__/materialize-getter-calls.test.ts +1 -0
  70. package/src/__tests__/nested-loop-index-param.test.ts +285 -0
  71. package/src/__tests__/signal-init-eval.test.ts +138 -0
  72. package/src/__tests__/static-array-inner-loop-index-param.test.ts +180 -0
  73. package/src/__tests__/static-literal.test.ts +126 -0
  74. package/src/__tests__/string-concat-identifier.test.ts +55 -0
  75. package/src/adapters/dangerous-inner-html.ts +186 -0
  76. package/src/adapters/loop-bound-names.ts +80 -0
  77. package/src/adapters/parsed-expr-emitter.ts +19 -1
  78. package/src/analyzer.ts +6 -0
  79. package/src/expression-parser.ts +59 -25
  80. package/src/index.ts +19 -3
  81. package/src/ir-to-client-js/collect-elements.ts +18 -3
  82. package/src/ir-to-client-js/control-flow/plan/build-event-delegation.ts +6 -0
  83. package/src/ir-to-client-js/control-flow/plan/build-inner-loop.ts +21 -4
  84. package/src/ir-to-client-js/control-flow/plan/build-loop-child-arm.ts +13 -1
  85. package/src/ir-to-client-js/control-flow/plan/build-loop.ts +1 -0
  86. package/src/ir-to-client-js/control-flow/plan/event-delegation.ts +10 -0
  87. package/src/ir-to-client-js/control-flow/plan/loop-child-arm.ts +9 -0
  88. package/src/ir-to-client-js/control-flow/plan/loop.ts +9 -0
  89. package/src/ir-to-client-js/control-flow/shared.ts +103 -6
  90. package/src/ir-to-client-js/control-flow/stringify/event-delegation.ts +31 -7
  91. package/src/ir-to-client-js/control-flow/stringify/inner-loop.ts +11 -2
  92. package/src/ir-to-client-js/control-flow/stringify/loop-child-arm.ts +5 -0
  93. package/src/ir-to-client-js/control-flow/stringify/loop.ts +53 -10
  94. package/src/ir-to-client-js/control-flow/stringify/reactive-effects.ts +25 -4
  95. package/src/ir-to-client-js/control-flow/stringify/skeleton-paths.ts +70 -0
  96. package/src/ir-to-client-js/control-flow/stringify/template-parse.ts +25 -1
  97. package/src/ir-to-client-js/csr-substitute.ts +54 -1
  98. package/src/ir-to-client-js/html-template.ts +292 -13
  99. package/src/ir-to-client-js/imports.ts +1 -0
  100. package/src/ir-to-client-js/plan/build-static-array-child-init.ts +12 -1
  101. package/src/ir-to-client-js/plan/static-array-child-init.ts +20 -1
  102. package/src/ir-to-client-js/reactivity.ts +6 -0
  103. package/src/ir-to-client-js/stringify/static-array-child-init.ts +11 -6
  104. package/src/ir-to-client-js/types.ts +31 -0
  105. package/src/ir-to-client-js/utils.ts +34 -1
  106. package/src/jsx-to-ir.ts +322 -48
  107. package/src/signal-init-eval.ts +165 -0
  108. package/src/static-literal.ts +128 -0
  109. package/src/types.ts +68 -0
@@ -0,0 +1,165 @@
1
+ /**
2
+ * TEST-HARNESS ONLY. Evaluate a signal initializer / prop-default source
3
+ * expression against a mock `props` object by EXECUTING it (`new
4
+ * Function`, with `undefined` shadowing the globals listed below), the
5
+ * same way the Hono/CSR conformance reference produces its value — by
6
+ * actually running the component. Never call this from a build path (`bf
7
+ * build`, `compileJSX`, any adapter's `generate`); it exists only so each
8
+ * adapter's `test-render.ts` conformance harness can seed a signal/
9
+ * prop-default for a non-JS-runtime SSR render (PHP/Ruby/Python/Perl/Go)
10
+ * that matches what Hono would compute. (#2209)
11
+ *
12
+ * NOT a security sandbox — the global shadowing below is a determinism
13
+ * tripwire (catches the common accidental-nondeterminism case, e.g.
14
+ * `Date.now()`), not containment: a sufficiently creative expression can
15
+ * still reach the real global scope (`new Function` bodies execute
16
+ * unlexically-scoped, so e.g. `[].constructor.constructor('return
17
+ * Date')()` recovers `Date` even though the bare name is shadowed, and
18
+ * `eval` can't be shadowed at all — JS forbids declaring or binding a
19
+ * parameter literally named `eval` in strict-mode code). Do not widen this
20
+ * module's use to anything but first-party fixture source. The actual
21
+ * trust boundary is that the evaluated text is first-party fixture
22
+ * source, already compiled by this same process — the conformance harness
23
+ * already executes fixture-derived code far more invasively (spawning
24
+ * `ruby`/`php`/`perl`/`go run` on generated programs). This module is
25
+ * never reachable from anything a real end user's untrusted input could
26
+ * influence.
27
+ *
28
+ * Why real execution instead of a hand-rolled evaluator: the initializer
29
+ * source is an arbitrary JS-subset expression over `props` (e.g. `(props.x
30
+ * ?? []).map(t => ({ ...t, editing: false }))`, #2209's actual repro) —
31
+ * every previous approach here was a regex/pattern-match over a small
32
+ * catalogue of recognized shapes (`props.x`, `props.x ?? default`, a bare
33
+ * literal), and #2209 is literally "the catalogue missed a shape" (the
34
+ * THIRD such miss on this codebase, per the superseded
35
+ * `evaluate-signal-init.test.ts`'s own #1672 pins). A hand-written
36
+ * evaluator over `ParsedExpr` would face the same drift, and can't
37
+ * represent object spread (`{ ...t, editing: false }`) without extending
38
+ * `ParsedExpr` — a production-compiler change that ripples into every
39
+ * adapter's exhaustive switch, disproportionate for a test-only need.
40
+ * `new Function` delegates parsing to the JS engine itself — CLAUDE.md's
41
+ * regex-parsing ban exists precisely to avoid the false-match/missed-shape
42
+ * failure mode this replaces.
43
+ *
44
+ * Shadowed as `undefined` (best-effort determinism, not containment — see
45
+ * above): `globalThis`, `window`, `document`, `Date`, `Math`, `crypto`,
46
+ * `performance`, `fetch`, `setTimeout`, `setInterval`, `require`,
47
+ * `process`, `Function`. (`eval` is deliberately absent from this list —
48
+ * it cannot be shadowed as a parameter name in strict-mode code; see
49
+ * above.)
50
+ *
51
+ * `props` bare-identifier destructured params (`createSignal(count)` where
52
+ * `count` is a destructured prop, not a `props.x` member) are NOT bound —
53
+ * the evaluator only exposes `props` — so such an initializer throws
54
+ * `ReferenceError` and falls back to "unset", matching every prior
55
+ * evaluator's behavior for that shape. Extending the environment with
56
+ * `ir.metadata.propsParams` bindings is a natural follow-up if a fixture
57
+ * ever needs it.
58
+ */
59
+
60
+ const BLOCKED_GLOBALS = [
61
+ 'globalThis',
62
+ 'window',
63
+ 'document',
64
+ 'Date',
65
+ 'Math',
66
+ 'crypto',
67
+ 'performance',
68
+ 'fetch',
69
+ 'setTimeout',
70
+ 'setInterval',
71
+ 'require',
72
+ 'process',
73
+ 'Function',
74
+ ] as const
75
+
76
+ export type SignalInitEvalResult = { ok: true; value: unknown } | { ok: false }
77
+
78
+ /**
79
+ * A value the harness's downstream language serializers (JSON/Python/PHP/
80
+ * Perl/Ruby literal builders) can actually marshal: `null`, a boolean, a
81
+ * number (including non-finite — several serializers already special-case
82
+ * NaN/Infinity), a string, a dense array with no holes and no `undefined`
83
+ * elements, or a plain object (`Object.prototype` or null prototype only —
84
+ * rejects class instances, functions, Maps/Sets, etc). Rejects genuine
85
+ * cycles; a shared (non-cyclic) reference appearing more than once — e.g.
86
+ * the same object at two array indices — is fine (JSON-equivalent
87
+ * behavior just duplicates it), so `ancestors` tracks only the current
88
+ * recursion path, not every value ever visited.
89
+ */
90
+ function isTransportable(value: unknown, ancestors: Set<unknown> = new Set()): boolean {
91
+ if (value === null) return true
92
+ const t = typeof value
93
+ if (t === 'boolean' || t === 'number' || t === 'string') return true
94
+ if (t !== 'object') return false
95
+ if (ancestors.has(value)) return false // a real cycle (value is its own ancestor)
96
+ ancestors.add(value)
97
+ try {
98
+ if (Array.isArray(value)) {
99
+ // `Array.prototype.every` silently skips holes (`[1, , 3]`), so a
100
+ // sparse array would otherwise pass — compare against the own
101
+ // enumerable key count (holes aren't own keys) to catch that.
102
+ if (Object.keys(value).length !== value.length) return false
103
+ return value.every(el => el !== undefined && isTransportable(el, ancestors))
104
+ }
105
+ const proto = Object.getPrototypeOf(value)
106
+ if (proto !== Object.prototype && proto !== null) return false
107
+ return Object.values(value as Record<string, unknown>).every(v => isTransportable(v, ancestors))
108
+ } finally {
109
+ ancestors.delete(value)
110
+ }
111
+ }
112
+
113
+ /**
114
+ * Evaluate `expr` (a JS-subset source expression) against `props`. Returns
115
+ * `{ ok: false }` when the expression fails to parse, throws at evaluation
116
+ * time (e.g. a `ReferenceError` for an unbound identifier), or evaluates to
117
+ * something the downstream serializers can't marshal (see
118
+ * {@link isTransportable}).
119
+ */
120
+ export function tryEvaluateSignalInit(
121
+ expr: string,
122
+ props?: Record<string, unknown>,
123
+ ): SignalInitEvalResult {
124
+ const src = expr.trim()
125
+ if (src === '') return { ok: false }
126
+ let fn: (props: Record<string, unknown>, ...blocked: undefined[]) => unknown
127
+ try {
128
+ // `props` plus the blocked-globals shadows are the only bindings
129
+ // passed in — best-effort determinism, not containment; see the file
130
+ // docstring for the trust-boundary rationale (test-harness-only,
131
+ // never a build path).
132
+ fn = new Function(
133
+ 'props',
134
+ ...BLOCKED_GLOBALS,
135
+ `'use strict'; return (\n${src}\n);`,
136
+ ) as typeof fn
137
+ } catch {
138
+ return { ok: false }
139
+ }
140
+ try {
141
+ const value = fn(props ?? {})
142
+ // `undefined` at the TOP level is a genuine, distinguishable result
143
+ // (e.g. an explicit `undefined` initializer, or `props.x` with no `x`
144
+ // and no `??` fallback) — not a marshal failure. `evaluateSignalInit`'s
145
+ // wrapper still collapses it to the "skip" outcome; callers that need
146
+ // the distinction use this function directly.
147
+ if (value === undefined) return { ok: true, value: undefined }
148
+ return isTransportable(value) ? { ok: true, value } : { ok: false }
149
+ } catch {
150
+ return { ok: false }
151
+ }
152
+ }
153
+
154
+ /**
155
+ * Drop-in replacement for the harnesses' former per-adapter regex
156
+ * evaluator: `null` means "could not evaluate, or evaluated to
157
+ * `undefined` — leave the signal/default unseeded", matching every prior
158
+ * evaluator's convention (an explicit JS `null` initializer also maps to
159
+ * `null` — the same "skip" outcome, since none of these harnesses
160
+ * distinguish "explicitly null" from "unset" downstream).
161
+ */
162
+ export function evaluateSignalInit(expr: string, props?: Record<string, unknown>): unknown {
163
+ const result = tryEvaluateSignalInit(expr, props)
164
+ return result.ok && result.value !== undefined ? result.value : null
165
+ }
@@ -0,0 +1,128 @@
1
+ /**
2
+ * Fully compile-time-known literal evaluation (#2208). A `ParsedExpr` that
3
+ * is a literal, a no-substitution (or fully-static) template literal, an
4
+ * array-literal of pure elements, or an object-literal of pure values
5
+ * evaluates here to its plain JS value. Consumed by each adapter's
6
+ * `renderLoop` to admit a function-scope local const whose initializer is
7
+ * entirely known at compile time as a loop source — inlining its value the
8
+ * same way a module-scope const already is — while still refusing a
9
+ * runtime-computed expression (`Object.entries(props.tags).filter(...)`,
10
+ * #2069) with BF101, since `call`/unresolvable-`member` shapes fall through
11
+ * to `null` below.
12
+ *
13
+ * Analysis only: this module never emits template syntax. Each adapter
14
+ * serializes the resolved JS value into its own native literal syntax
15
+ * (`packages/adapter-<name>/src/adapter/lib/static-value.ts`).
16
+ */
17
+
18
+ import type { ParsedExpr } from './expression-parser.ts'
19
+ import type { ConstantInfo } from './types.ts'
20
+
21
+ /**
22
+ * Recursively evaluate a `ParsedExpr` that is a fully compile-time-known
23
+ * literal to its JS value. `bindings` lets a caller resolve identifiers
24
+ * (and member reads off them) against already-evaluated values — e.g. the
25
+ * Go adapter binding a loop's item parameter to one already-resolved array
26
+ * element while baking per-item child-component props. Returns `null` when
27
+ * the expression is not statically known (depends on a prop, signal,
28
+ * function call, or an identifier absent from `bindings`).
29
+ */
30
+ export function evaluateStaticLiteral(
31
+ expr: ParsedExpr,
32
+ bindings?: ReadonlyMap<string, unknown>,
33
+ ): { value: unknown } | null {
34
+ switch (expr.kind) {
35
+ case 'literal':
36
+ return { value: expr.value }
37
+ case 'template-literal': {
38
+ let out = ''
39
+ for (const part of expr.parts) {
40
+ if (part.type === 'string') {
41
+ out += part.value
42
+ continue
43
+ }
44
+ const resolved = evaluateStaticLiteral(part.expr, bindings)
45
+ if (!resolved) return null
46
+ out += String(resolved.value)
47
+ }
48
+ return { value: out }
49
+ }
50
+ case 'array-literal': {
51
+ const values: unknown[] = []
52
+ for (const element of expr.elements) {
53
+ const resolved = evaluateStaticLiteral(element, bindings)
54
+ if (!resolved) return null
55
+ values.push(resolved.value)
56
+ }
57
+ return { value: values }
58
+ }
59
+ case 'object-literal': {
60
+ // Shorthand (`{ a }`) and explicit (`{ a: value }`) properties both
61
+ // carry their resolved tree in `value` (shorthand's is an
62
+ // `identifier`) — recursing here handles both uniformly: a shorthand
63
+ // property only resolves when `bindings` supplies it.
64
+ const out: Record<string, unknown> = {}
65
+ for (const prop of expr.properties) {
66
+ const resolved = evaluateStaticLiteral(prop.value, bindings)
67
+ if (!resolved) return null
68
+ out[prop.key] = resolved.value
69
+ }
70
+ return { value: out }
71
+ }
72
+ case 'unary': {
73
+ const resolved = evaluateStaticLiteral(expr.argument, bindings)
74
+ if (!resolved) return null
75
+ if (expr.op === '-') return typeof resolved.value === 'number' ? { value: -resolved.value } : null
76
+ if (expr.op === '+') return typeof resolved.value === 'number' ? { value: +resolved.value } : null
77
+ if (expr.op === '!') return { value: !resolved.value }
78
+ return null
79
+ }
80
+ case 'identifier':
81
+ return bindings?.has(expr.name) ? { value: bindings.get(expr.name) } : null
82
+ case 'member': {
83
+ const base = evaluateStaticLiteral(expr.object, bindings)
84
+ if (!base || base.value === null || typeof base.value !== 'object') return null
85
+ return { value: (base.value as Record<string, unknown>)[expr.property] }
86
+ }
87
+ case 'index-access': {
88
+ const base = evaluateStaticLiteral(expr.object, bindings)
89
+ const index = evaluateStaticLiteral(expr.index, bindings)
90
+ if (!base || !index || !Array.isArray(base.value) || typeof index.value !== 'number') return null
91
+ return { value: base.value[index.value] }
92
+ }
93
+ default:
94
+ return null
95
+ }
96
+ }
97
+
98
+ /** `true` iff {@link evaluateStaticLiteral} resolves `expr` with no bindings. */
99
+ export function isFullyStaticLiteral(expr: ParsedExpr): boolean {
100
+ return evaluateStaticLiteral(expr) !== null
101
+ }
102
+
103
+ /**
104
+ * Shared loop-source resolution for `renderLoop` across adapters: the loop
105
+ * array is either an inline array-literal, or a bare identifier naming a
106
+ * FUNCTION-scope (`!isModule`) local const whose initializer evaluates
107
+ * statically. Module-scope consts are deliberately excluded — an existing,
108
+ * separate seeding path already handles those. Returns the evaluated items,
109
+ * or `null` if the source isn't resolvable this way (including when it
110
+ * resolves to something other than an array).
111
+ */
112
+ export function resolveStaticLoopSource(
113
+ arrayParsed: ParsedExpr | undefined,
114
+ localConstants: ReadonlyArray<ConstantInfo> | undefined,
115
+ opts?: { isNameShadowed?: (name: string) => boolean },
116
+ ): unknown[] | null {
117
+ if (!arrayParsed) return null
118
+ let target = arrayParsed
119
+ if (arrayParsed.kind === 'identifier') {
120
+ if (opts?.isNameShadowed?.(arrayParsed.name)) return null
121
+ const local = localConstants?.find(c => c.name === arrayParsed.name)
122
+ if (!local || local.isModule || !local.parsed) return null
123
+ target = local.parsed
124
+ }
125
+ const resolved = evaluateStaticLiteral(target)
126
+ if (!resolved || !Array.isArray(resolved.value)) return null
127
+ return resolved.value
128
+ }
package/src/types.ts CHANGED
@@ -574,6 +574,74 @@ export interface IRLoop {
574
574
  */
575
575
  iterationShape?: 'entries' | 'keys'
576
576
 
577
+ /**
578
+ * Pre-`.map()` object iteration (#2168 object-entries-map). Distinct
579
+ * from {@link iterationShape}, which is scoped ENTIRELY to an array's
580
+ * own zero-arg `.entries()`/`.keys()`/`.values()` methods — those
581
+ * synthesize a real numeric index off the array's position, and every
582
+ * adapter's consumption of `iterationShape` assumes an actual
583
+ * array/slice underneath.
584
+ *
585
+ * `objectIteration` instead records the STATIC `Object.entries(x)` /
586
+ * `Object.keys(x)` / `Object.values(x)` call form, where `x` is a
587
+ * plain object/Record (not an array): the "index" bound for `'entries'`
588
+ * is a STRING KEY, not a numeric position, and the collection an
589
+ * adapter must iterate is its native map/dict/hash type, not an
590
+ * array/slice. `transformMapCall` strips the `Object.<method>(...)`
591
+ * wrapper the same way it strips `arr.entries()` — `array`/`arrayParsed`
592
+ * end up holding just `x` — and records the shape here so each
593
+ * adapter's loop renderer picks the right native construct:
594
+ *
595
+ * - `'entries'` → both `index` (bound to the KEY) and `param` (bound
596
+ * to the VALUE), synthesized from the 2-element destructure the
597
+ * same way `iterationShape: 'entries'` is (see `jsx-to-ir.ts`'s
598
+ * `transformMapCall`) — e.g. Jinja `for k, v in x.items()`.
599
+ * - `'keys'` → `param` bound to the key only — e.g. Jinja
600
+ * `for k in x.keys()`.
601
+ * - `'values'` → `param` bound to the value only — e.g. Jinja
602
+ * `for v in x.values()`. Unlike the array case, `'values'` is NOT
603
+ * a no-op here: `Object.values(x)` genuinely differs from
604
+ * iterating `x` itself (`x` isn't iterable at all as a plain
605
+ * object), so it must be recorded.
606
+ *
607
+ * Iteration ORDER is native-map-dependent: Python `dict`/PHP
608
+ * array-object/Ruby `Hash` preserve the source object's insertion
609
+ * order (matching JS `Object.entries()` semantics exactly), so Jinja,
610
+ * Twig, Blade, and ERB lower directly to their native map/dict/hash
611
+ * iteration. Go's `map[string]T`, Rust's `BTreeMap` (deliberate design,
612
+ * see `num.rs`), and Perl's hash (Xslate/Mojolicious) have NO
613
+ * order-preserving native map type, so those four instead lower to a
614
+ * DETERMINISTIC SORTED-BY-KEY iteration — Go's `{{range}}` (the
615
+ * stdlib's own `fmtsort`), minijinja's `BTreeMap` (already sorted),
616
+ * Kolon's `.kv()`/`.keys()`/`.values()` (verified empirically sorted),
617
+ * and Mojolicious's explicit `sort keys %$hash` (mirroring the
618
+ * existing `spread_attrs`/`_style_to_css` convention in
619
+ * `BarefootJS.pm`). This is a documented, permanent known limitation
620
+ * relative to JS's insertion-order guarantee (not a follow-up TODO) —
621
+ * true insertion order is physically unrecoverable from those
622
+ * languages' native map types once constructed, so sorted order is the
623
+ * best available deterministic approximation, not an interim refusal.
624
+ *
625
+ * Only the OUTERMOST, unchained `Object.<method>(x).map(cb)` shape is
626
+ * recognized — mirroring `iterationShape`'s own scope, chaining
627
+ * (`Object.entries(x).filter(pred).map(cb)`) is not (yet) recognized
628
+ * either, same as the array case.
629
+ */
630
+ objectIteration?: 'entries' | 'keys' | 'values'
631
+
632
+ /**
633
+ * Count of enclosing `.map()` loops (0 = outermost, 1 = nested one
634
+ * level deep, ...). Adapters use this to derive the loop body's
635
+ * `key`/`data-key` attribute suffix — `depth > 0 ? 'data-key-' +
636
+ * depth : 'data-key'` — matching `keyAttrName()` in
637
+ * `ir-to-client-js/utils.ts`, which the CSR path and the Hono SSR
638
+ * adapter each already derive independently (a recursion counter and
639
+ * a push/pop stack respectively). Before this field, the 8 template
640
+ * (non-JS) adapters had no depth awareness at all and always emitted
641
+ * plain `data-key` on nested-loop items (#2168 nested-loop-outer-binding).
642
+ */
643
+ depth: number
644
+
577
645
  /**
578
646
  * When true, loop should be evaluated on client side only.
579
647
  * SSR adapters should skip rendering and output placeholder markers.