@barefootjs/jsx 0.31.1 → 0.31.3

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 (80) hide show
  1. package/dist/adapters/interface.d.ts +11 -0
  2. package/dist/adapters/interface.d.ts.map +1 -1
  3. package/dist/adapters/jsx-adapter.d.ts +92 -1
  4. package/dist/adapters/jsx-adapter.d.ts.map +1 -1
  5. package/dist/adapters/test-adapter.d.ts.map +1 -1
  6. package/dist/analyzer.d.ts.map +1 -1
  7. package/dist/compiler.d.ts.map +1 -1
  8. package/dist/css-layer-prefixer.d.ts +16 -0
  9. package/dist/css-layer-prefixer.d.ts.map +1 -1
  10. package/dist/errors.d.ts +1 -0
  11. package/dist/errors.d.ts.map +1 -1
  12. package/dist/html-types.d.ts +19 -0
  13. package/dist/html-types.d.ts.map +1 -1
  14. package/dist/index.d.ts +5 -2
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +1531 -1155
  17. package/dist/ir-to-client-js/collect-elements.d.ts.map +1 -1
  18. package/dist/ir-to-client-js/emit-reactive.d.ts.map +1 -1
  19. package/dist/ir-to-client-js/phases/props-event-handlers.d.ts.map +1 -1
  20. package/dist/ir-to-client-js/phases/props-extraction.d.ts.map +1 -1
  21. package/dist/ir-to-client-js/utils.d.ts +6 -4
  22. package/dist/ir-to-client-js/utils.d.ts.map +1 -1
  23. package/dist/jsx-runtime/index.d.ts +2 -8
  24. package/dist/jsx-runtime/index.d.ts.map +1 -1
  25. package/dist/jsx-to-ir.d.ts.map +1 -1
  26. package/dist/module-exports.d.ts +9 -1
  27. package/dist/module-exports.d.ts.map +1 -1
  28. package/dist/prop-rewrite.d.ts +8 -3
  29. package/dist/prop-rewrite.d.ts.map +1 -1
  30. package/dist/props-binding.d.ts +40 -0
  31. package/dist/props-binding.d.ts.map +1 -0
  32. package/dist/relocate.d.ts +9 -0
  33. package/dist/relocate.d.ts.map +1 -1
  34. package/dist/scope/binding-scope.d.ts +179 -0
  35. package/dist/scope/binding-scope.d.ts.map +1 -0
  36. package/dist/ssr-defaults.d.ts +43 -0
  37. package/dist/ssr-defaults.d.ts.map +1 -1
  38. package/dist/template-parts.d.ts +53 -0
  39. package/dist/template-parts.d.ts.map +1 -0
  40. package/dist/types.d.ts +18 -0
  41. package/dist/types.d.ts.map +1 -1
  42. package/package.json +2 -2
  43. package/src/__tests__/adapter-output.test.ts +8 -4
  44. package/src/__tests__/aliased-destructured-prop-csr.test.ts +112 -0
  45. package/src/__tests__/binding-scope-preamble-shadowing.test.ts +115 -0
  46. package/src/__tests__/binding-scope-ratchet.test.ts +194 -0
  47. package/src/__tests__/binding-scope.test.ts +200 -0
  48. package/src/__tests__/css-layer-prefixer.test.ts +72 -0
  49. package/src/__tests__/form-control-value-ssr.test.ts +48 -3
  50. package/src/__tests__/let-type-annotation.test.ts +208 -0
  51. package/src/__tests__/memo-deps-comments.test.ts +99 -0
  52. package/src/__tests__/multi-return-sibling-diagnostic.test.ts +241 -0
  53. package/src/__tests__/ssr-defaults.test.ts +124 -1
  54. package/src/__tests__/staged-ir/08-relocate-unit.test.ts +1 -0
  55. package/src/__tests__/staged-ir/11-template-primitive-registry.test.ts +1 -0
  56. package/src/adapters/interface.ts +11 -0
  57. package/src/adapters/jsx-adapter.ts +295 -5
  58. package/src/adapters/test-adapter.ts +13 -10
  59. package/src/analyzer.ts +70 -11
  60. package/src/compiler.ts +119 -18
  61. package/src/css-layer-prefixer.ts +80 -24
  62. package/src/errors.ts +18 -0
  63. package/src/html-types.ts +24 -0
  64. package/src/index.ts +11 -1
  65. package/src/ir-to-client-js/collect-elements.ts +4 -1
  66. package/src/ir-to-client-js/emit-reactive.ts +4 -2
  67. package/src/ir-to-client-js/phases/props-event-handlers.ts +4 -3
  68. package/src/ir-to-client-js/phases/props-extraction.ts +7 -4
  69. package/src/ir-to-client-js/plan/build-declaration-emit.ts +6 -3
  70. package/src/ir-to-client-js/utils.ts +5 -24
  71. package/src/jsx-runtime/index.ts +2 -7
  72. package/src/jsx-to-ir.ts +245 -107
  73. package/src/module-exports.ts +11 -2
  74. package/src/prop-rewrite.ts +26 -6
  75. package/src/props-binding.ts +70 -0
  76. package/src/relocate.ts +19 -2
  77. package/src/scope/binding-scope.ts +238 -0
  78. package/src/ssr-defaults.ts +70 -0
  79. package/src/template-parts.ts +81 -0
  80. package/src/types.ts +18 -0
@@ -0,0 +1,241 @@
1
+ /**
2
+ * Regression tests for #2556: a `'use client'` file where a component
3
+ * references a same-file sibling whose body is a multi-return JSX dispatch
4
+ * (`switch` / `if`-`else` chain) previously compiled clean with ZERO
5
+ * diagnostics, but the sibling produced no template — so the emitted
6
+ * `renderChild`/`initChild`/`createComponent` call referenced a component
7
+ * name with nothing registered under it, throwing
8
+ * `ReferenceError: <Name> is not defined` at SSR/hydrate time.
9
+ *
10
+ * Root cause: `listComponentFunctions`'s #932 "preserve verbatim helper"
11
+ * bypass is gated on `!hasUseClient` — in a `'use client'` file, a
12
+ * multi-return sibling like `NavIcon` below IS added to `componentNames`
13
+ * and asked to compile as a standalone component. But `visitComponentBody`
14
+ * only folds `if`/`else`-chain multi-return bodies into `conditionalReturns`
15
+ * (#1401); a top-level `switch` statement is preserved as a verbatim init
16
+ * statement instead, so `ctx.jsxReturn` stays null and
17
+ * `compileMultipleComponents`'s Pass-1 loop silently `continue`s past it —
18
+ * no entry, no template, no error. Meanwhile the referencing sibling's IR
19
+ * still holds a `component` reference to the dropped name.
20
+ *
21
+ * Fix: BF048 detects this structurally — after Pass 1, any name in
22
+ * `componentNames` that did not make it into `entries` is cross-referenced
23
+ * against every compiled sibling's IR component-reference graph (the same
24
+ * walk `@bf-child` import markers use, `collectComponentNamesFromIR`). A
25
+ * hit fails the compile instead of shipping a silent `ReferenceError`.
26
+ */
27
+
28
+ import { describe, test, expect } from 'bun:test'
29
+ import { compileJSX } from '../compiler'
30
+ import { TestAdapter } from '../adapters/test-adapter'
31
+
32
+ const adapter = new TestAdapter()
33
+
34
+ describe('Sibling multi-return JSX dispatch produces no template in a client file (#2556)', () => {
35
+ // Higher timeout: `.map()` in the source trips `needsTypeBasedDetection`,
36
+ // and building the one-time `ts.Program` for BF023/BF024's nullable-key
37
+ // check is slow the first time the TS API is touched in a fresh process
38
+ // (e.g. this file run in isolation) — well past bun's 5s default.
39
+ test('BF048: .map() loop child referencing a switch-dispatch sibling fails the compile', () => {
40
+ const source = `
41
+ "use client"
42
+ import { createSignal } from '@barefootjs/client'
43
+ function NavIcon({ name }: { name: string }) {
44
+ switch (name) {
45
+ case 'home': return <svg><path d="M1"/></svg>
46
+ case 'bell': return <svg><path d="M2"/></svg>
47
+ default: return null
48
+ }
49
+ }
50
+ export function Shell() {
51
+ const [n, setN] = createSignal(0)
52
+ return (
53
+ <nav onClick={() => setN(n() + 1)}>
54
+ {['home', 'bell'].map(item => <NavIcon key={item} name={item} />)}
55
+ </nav>
56
+ )
57
+ }
58
+ `
59
+
60
+ const result = compileJSX(source, 'Shell.tsx', { adapter })
61
+ const errs = result.errors.filter(e => e.severity === 'error')
62
+ expect(errs.length).toBeGreaterThan(0)
63
+ const bf048 = errs.find(e => e.code === 'BF048')
64
+ expect(bf048).toBeDefined()
65
+ expect(bf048!.message).toContain('NavIcon')
66
+ expect(bf048!.message).toContain('Shell')
67
+ expect(bf048!.message).toMatch(/did not compile to a template/)
68
+ }, 20000)
69
+
70
+ test('BF048: direct JSX tag (non-loop) referencing a switch-dispatch sibling fails the compile', () => {
71
+ const source = `
72
+ "use client"
73
+ import { createSignal } from '@barefootjs/client'
74
+ function StatusIcon({ status }: { status: string }) {
75
+ switch (status) {
76
+ case 'ok': return <span class="ok">OK</span>
77
+ case 'err': return <span class="err">ERR</span>
78
+ default: return null
79
+ }
80
+ }
81
+ export function Panel() {
82
+ const [n, setN] = createSignal(0)
83
+ return <div onClick={() => setN(n() + 1)}><StatusIcon status="ok" /></div>
84
+ }
85
+ `
86
+
87
+ const result = compileJSX(source, 'Panel.tsx', { adapter })
88
+ const errs = result.errors.filter(e => e.severity === 'error')
89
+ const bf048 = errs.find(e => e.code === 'BF048')
90
+ expect(bf048).toBeDefined()
91
+ expect(bf048!.message).toContain('StatusIcon')
92
+ })
93
+
94
+ test('legal neighbor: the identical switch-dispatch shape in a non-"use client" file compiles clean (#932)', () => {
95
+ // Same shape as the failing case above, but without the 'use client'
96
+ // directive: `listComponentFunctions`'s #932 bypass keeps NavIcon OFF
97
+ // `componentNames` entirely, so it is preserved verbatim in the marked
98
+ // template rather than compiled (and dropped) as a component. BF048
99
+ // must not fire here.
100
+ const source = `
101
+ function NavIcon({ name }: { name: string }) {
102
+ switch (name) {
103
+ case 'home': return <svg><path d="M1"/></svg>
104
+ case 'bell': return <svg><path d="M2"/></svg>
105
+ default: return null
106
+ }
107
+ }
108
+ export function Shell() {
109
+ return (
110
+ <nav>
111
+ {['home', 'bell'].map(item => <NavIcon key={item} name={item} />)}
112
+ </nav>
113
+ )
114
+ }
115
+ `
116
+
117
+ const result = compileJSX(source, 'Shell.tsx', { adapter })
118
+ const errs = result.errors.filter(e => e.severity === 'error')
119
+ expect(errs.filter(e => e.code === 'BF048')).toHaveLength(0)
120
+ expect(errs).toHaveLength(0)
121
+
122
+ const markedTemplate = result.files.find(f => f.type === 'markedTemplate')
123
+ expect(markedTemplate).toBeDefined()
124
+ expect(markedTemplate!.content).toContain('function NavIcon')
125
+ })
126
+
127
+ test('legal neighbor: a non-"use client" file never trips BF048, even when a sibling lands in componentNames', () => {
128
+ // The #932 bypass only skips NON-exported multi-return functions, so an
129
+ // exported switch-dispatch sibling in a non-client file IS in
130
+ // `componentNames` and produces no entry — the same set-difference shape
131
+ // BF048 keys on. But the diagnostic's premise (the 'use client' branch
132
+ // of #932, `ReferenceError` at hydrate time) doesn't apply outside
133
+ // client files, so BF048 is gated on a compiled client entry and must
134
+ // stay silent here.
135
+ const source = `
136
+ export function NavIcon({ name }: { name: string }) {
137
+ switch (name) {
138
+ case 'home': return <svg><path d="M1"/></svg>
139
+ case 'bell': return <svg><path d="M2"/></svg>
140
+ default: return null
141
+ }
142
+ }
143
+ export function Shell() {
144
+ return (
145
+ <nav>
146
+ {['home', 'bell'].map(item => <NavIcon key={item} name={item} />)}
147
+ </nav>
148
+ )
149
+ }
150
+ `
151
+
152
+ const result = compileJSX(source, 'Shell.tsx', { adapter })
153
+ const errs = result.errors.filter(e => e.severity === 'error')
154
+ expect(errs.filter(e => e.code === 'BF048')).toHaveLength(0)
155
+ })
156
+
157
+ test('legal neighbor: a "use client" sibling whose multi-return body DOES compile (if/else chain, #1401) stays clean', () => {
158
+ // if/else-if chains fold into `conditionalReturns` (#1401) and produce
159
+ // a real ternary template, so the sibling ends up in `entries` and the
160
+ // reference resolves. BF048 must not fire.
161
+ const source = `
162
+ "use client"
163
+ import { createSignal } from '@barefootjs/client'
164
+ function Badge({ kind }: { kind: string }) {
165
+ if (kind === 'ok') return <span class="ok">ok</span>
166
+ if (kind === 'warn') return <span class="warn">warn</span>
167
+ return <span class="err">err</span>
168
+ }
169
+ export function Panel() {
170
+ const [n, setN] = createSignal(0)
171
+ return (
172
+ <div onClick={() => setN(n() + 1)}>
173
+ <Badge kind="ok" />
174
+ </div>
175
+ )
176
+ }
177
+ `
178
+
179
+ const result = compileJSX(source, 'Panel.tsx', { adapter })
180
+ const errs = result.errors.filter(e => e.severity === 'error')
181
+ expect(errs.filter(e => e.code === 'BF048')).toHaveLength(0)
182
+
183
+ const clientJs = result.files.find(f => f.type === 'clientJs')
184
+ expect(clientJs).toBeDefined()
185
+ // Both the Panel and Badge components' hydrate() registrations exist
186
+ // with real templates — the sibling reference resolves. Badge is a
187
+ // non-exported sibling so its runtime key is file-scoped
188
+ // (`Badge__<hash>`); match on the `name: 'Badge'` metadata instead of
189
+ // the registry key.
190
+ expect(clientJs!.content).toMatch(/hydrate\('Badge[^']*',\s*\{[^}]*template:/s)
191
+ expect(clientJs!.content).toContain("name: 'Badge'")
192
+ })
193
+
194
+ test('legal neighbor: single-component "use client" multi-return root (#1401) is unaffected', () => {
195
+ // Only one component in the file, so `compileJSX` never enters the
196
+ // multi-component path where BF048 is computed at all.
197
+ const source = `
198
+ "use client"
199
+ import { createSignal } from '@barefootjs/client'
200
+ export function Toggle(props: { asChild?: boolean }) {
201
+ const [open, setOpen] = createSignal(false)
202
+ if (props.asChild) {
203
+ return <span onClick={() => setOpen(!open())}>child</span>
204
+ }
205
+ return <button onClick={() => setOpen(!open())}>toggle</button>
206
+ }
207
+ `
208
+
209
+ const result = compileJSX(source, 'Toggle.tsx', { adapter })
210
+ expect(result.errors.filter(e => e.severity === 'error')).toHaveLength(0)
211
+ const clientJs = result.files.find(f => f.type === 'clientJs')
212
+ expect(clientJs).toBeDefined()
213
+ expect(clientJs!.content).toContain('template:')
214
+ })
215
+
216
+ test('legal neighbor: a component-scope local JSX factory does not trip BF048', () => {
217
+ // `listComponentFunctions` recurses into function bodies, so a local
218
+ // factory like `Inner` below lands in `componentNames` and produces no
219
+ // standalone template — but its call sites are handled by the
220
+ // JSX-function-inlining pass, not `createComponent`, so it is NOT the
221
+ // dropped-sibling shape. BF048's uncompiled-sibling set is restricted
222
+ // to module top-level declarations precisely so this stays legal (the
223
+ // first BF048 cut flagged it and broke the ir-dynamic-tag corpus).
224
+ // Two components so the multi-component path (where BF048 runs) engages.
225
+ const source = `
226
+ "use client"
227
+ import { createSignal } from '@barefootjs/client'
228
+ export function Demo() {
229
+ const Inner = () => <span>x</span>
230
+ const [n, setN] = createSignal(0)
231
+ return <div onClick={() => setN(n() + 1)}><Inner /></div>
232
+ }
233
+ export function Other() {
234
+ return <p>other</p>
235
+ }
236
+ `
237
+
238
+ const result = compileJSX(source, 'demo.tsx', { adapter })
239
+ expect(result.errors.filter(e => e.severity === 'error')).toHaveLength(0)
240
+ })
241
+ })
@@ -13,7 +13,8 @@
13
13
  // computations that derive from those signals (`count() * 2`).
14
14
 
15
15
  import { describe, test, expect } from 'bun:test'
16
- import { extractSsrDefaults } from '../ssr-defaults'
16
+ import { extractSsrDefaults, deriveStashFromDefaults } from '../ssr-defaults'
17
+ import type { SsrDefault } from '../ssr-defaults'
17
18
  import { analyzeComponent } from '../analyzer'
18
19
  import { buildMetadata } from '../compiler'
19
20
 
@@ -253,3 +254,125 @@ describe('extractSsrDefaults', () => {
253
254
  expect(defaults?.classes).toEqual({ value: 'a b c d tail' })
254
255
  })
255
256
  })
257
+
258
+ // TS twin of the Ruby/Python/PHP/Perl/Rust `derive*FromDefaults` runtime
259
+ // ports (#2524 SSR half). Matches `derive_vars_from_defaults`'s edge cases
260
+ // exactly — see that Ruby method's docstring
261
+ // (packages/adapter-erb/lib/barefoot_js.rb:337-360) for the reference
262
+ // semantics this mirrors.
263
+ describe('deriveStashFromDefaults', () => {
264
+ test('aliased prop: resolves the CALLER-facing propName, not the local key', () => {
265
+ // `{ n: count }` — extractSsrDefaults keys the entry by the LOCAL
266
+ // binding (`count`) with `propName: 'n'`. The caller supplies `n`.
267
+ const defaults: Record<string, SsrDefault> = {
268
+ count: { propName: 'n', value: null },
269
+ }
270
+ expect(deriveStashFromDefaults(defaults, { n: 7 })).toEqual({ count: 7 })
271
+ })
272
+
273
+ test('un-aliased prop: propName equals the local key, resolves the same way', () => {
274
+ const defaults: Record<string, SsrDefault> = {
275
+ n: { propName: 'n', value: null },
276
+ }
277
+ expect(deriveStashFromDefaults(defaults, { n: 7 })).toEqual({ n: 7 })
278
+ })
279
+
280
+ test('caller omits the prop: falls back to the static default value', () => {
281
+ const defaults: Record<string, SsrDefault> = {
282
+ count: { propName: 'n', value: 3 },
283
+ }
284
+ expect(deriveStashFromDefaults(defaults, {})).toEqual({ count: 3 })
285
+ })
286
+
287
+ test('caller supplies null / undefined for the propName: falls back to the static value', () => {
288
+ // Mirrors every runtime port's "present AND defined" check — an
289
+ // explicit null/undefined caller value does not count as an override.
290
+ const defaults: Record<string, SsrDefault> = {
291
+ count: { propName: 'n', value: 3 },
292
+ }
293
+ expect(deriveStashFromDefaults(defaults, { n: null })).toEqual({ count: 3 })
294
+ expect(deriveStashFromDefaults(defaults, { n: undefined })).toEqual({ count: 3 })
295
+ })
296
+
297
+ test('caller supplies a falsy-but-defined propName value: the caller value wins', () => {
298
+ // `0` / `''` / `false` are legitimate override values, not "absent".
299
+ const defaults: Record<string, SsrDefault> = {
300
+ count: { propName: 'n', value: 99 },
301
+ }
302
+ expect(deriveStashFromDefaults(defaults, { n: 0 })).toEqual({ count: 0 })
303
+ })
304
+
305
+ test('propName-less entry (signal / memo local): always uses the static value', () => {
306
+ // A caller cannot override an internal signal/memo by construction —
307
+ // even a same-named caller prop must not leak in.
308
+ const defaults: Record<string, SsrDefault> = {
309
+ doubled: { value: 10 },
310
+ }
311
+ expect(deriveStashFromDefaults(defaults, { doubled: 999 })).toEqual({ doubled: 10 })
312
+ })
313
+
314
+ test('isRestProps entry: prefers the caller-assembled rest bag under its OWN key', () => {
315
+ const defaults: Record<string, SsrDefault> = {
316
+ rest: { isRestProps: true, value: {} },
317
+ }
318
+ expect(deriveStashFromDefaults(defaults, { rest: { href: '/x' } })).toEqual({
319
+ rest: { href: '/x' },
320
+ })
321
+ })
322
+
323
+ test('isRestProps entry: falls back to the static value (normally {}) when the caller supplied none', () => {
324
+ const defaults: Record<string, SsrDefault> = {
325
+ rest: { isRestProps: true, value: {} },
326
+ }
327
+ expect(deriveStashFromDefaults(defaults, {})).toEqual({ rest: {} })
328
+ })
329
+
330
+ test('isRestProps entry: an explicit `undefined` caller value still counts as supplied', () => {
331
+ // Unlike the ordinary propName branch (nullish check), the isRestProps
332
+ // branch tests presence via `in` — the rest bag is a caller-assembled
333
+ // aggregate, not a single scalar with a meaningful "absent" state, so
334
+ // an explicit `undefined` still wins over the static fallback instead
335
+ // of falling through to it.
336
+ const defaults: Record<string, SsrDefault> = {
337
+ rest: { isRestProps: true, value: { fallback: true } },
338
+ }
339
+ const result = deriveStashFromDefaults(defaults, { rest: undefined })
340
+ expect('rest' in result).toBe(true)
341
+ expect(result.rest).toBeUndefined()
342
+ })
343
+
344
+ test('a bare (non-object) entry passes through as-is', () => {
345
+ // Defensive parity with every runtime port's `ref($d) eq 'HASH'` /
346
+ // `isinstance(d, dict)` guard — never emitted by `extractSsrDefaults`
347
+ // itself, but a caller may hand this a manifest round-tripped through a
348
+ // generic JSON domain.
349
+ const defaults = { flag: true } as unknown as Record<string, SsrDefault>
350
+ expect(deriveStashFromDefaults(defaults, {})).toEqual({ flag: true })
351
+ })
352
+
353
+ test('a literal null entry hits the same non-object passthrough guard', () => {
354
+ // `d === null` is checked ALONGSIDE `typeof d !== 'object'` (not just
355
+ // the latter) — `typeof null === 'object'` in JS, so without the
356
+ // explicit `d === null` check a null entry would wrongly fall into the
357
+ // object-shaped branches below and throw reading `d.isRestProps`.
358
+ const defaults = { flag: null } as unknown as Record<string, SsrDefault>
359
+ expect(deriveStashFromDefaults(defaults, {})).toEqual({ flag: null })
360
+ })
361
+
362
+ test('mixed defaults map resolves every entry kind independently', () => {
363
+ const defaults: Record<string, SsrDefault> = {
364
+ count: { propName: 'n', value: null },
365
+ text: { propName: 'text', value: null },
366
+ doubled: { value: 0 },
367
+ rest: { isRestProps: true, value: {} },
368
+ }
369
+ expect(
370
+ deriveStashFromDefaults(defaults, { n: 7, rest: { extra: 'x' } }),
371
+ ).toEqual({
372
+ count: 7,
373
+ text: null,
374
+ doubled: 0,
375
+ rest: { extra: 'x' },
376
+ })
377
+ })
378
+ })
@@ -20,6 +20,7 @@ function envWith(
20
20
  propsForLift: new Set(
21
21
  bindings.filter(([, k]) => k === 'prop').map(([n]) => n),
22
22
  ),
23
+ propSourceNames: options?.propSourceNames ?? new Map(),
23
24
  propsObjectName: options?.propsObjectName ?? 'props',
24
25
  allowFallback: options?.allowFallback ?? true,
25
26
  }
@@ -35,6 +35,7 @@ function envWith(
35
35
  propsForLift: new Set(
36
36
  bindings.filter(([, k]) => k === 'prop').map(([n]) => n),
37
37
  ),
38
+ propSourceNames: options?.propSourceNames ?? new Map(),
38
39
  propsObjectName: options?.propsObjectName ?? 'props',
39
40
  allowFallback: options?.allowFallback ?? true,
40
41
  templatePrimitives: options?.templatePrimitives,
@@ -28,6 +28,17 @@ export interface TemplateSections {
28
28
  * component in a source file.
29
29
  */
30
30
  moduleConstants?: string
31
+ /**
32
+ * When true, the `moduleConstants` section already carries the module's
33
+ * EXPORTED value declarations (consts/functions, `export` keyword
34
+ * included) interleaved in source order — so the compiler must NOT
35
+ * re-emit them via `generateModuleExports` (only `export { … }`
36
+ * specifier blocks remain its job). JSX adapters set this; keeping the
37
+ * exported and non-exported declarations in one source-ordered section
38
+ * is what prevents a non-exported const that reads an exported one from
39
+ * being emitted above it (a module-load TDZ crash — #2570's input-otp).
40
+ */
41
+ moduleConstantsIncludeExports?: boolean
31
42
  }
32
43
 
33
44
  export interface AdapterOutput {