@barefootjs/jsx 0.31.9 → 0.32.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.
@@ -103,6 +103,211 @@ describe('extractSsrDefaults', () => {
103
103
  expect(defaults?.n).toEqual({ value: 0 })
104
104
  })
105
105
 
106
+ test('self-derived signal collision (#2669, shape A): entry stays a PROP entry, not the evaluated signal value', () => {
107
+ // `props.label` and the signal getter `label` share a name — the
108
+ // bare-props-arg form is the only shape where this collides (a
109
+ // destructured-arg `function C({ label })` + `const [label] = ...`
110
+ // is a JS redeclaration error, so it can't happen there). The
111
+ // emitted template reads the stash var `label` as the RAW caller
112
+ // prop and OVERWRITES it with the derived signal value under that
113
+ // SAME name (`{% set label = (label if label is defined else
114
+ // 'Default') %}`). Seeding the stash with the pre-fix `{ value:
115
+ // 'Default' }` (the EVALUATED signal value, discarding `propName`)
116
+ // means a caller-passed `label` can never win — production seeds
117
+ // 'Default', the template sees a non-none value, and keeps it. The
118
+ // fix: this entry must stay a PROP entry (`propName` set, `value:
119
+ // null`) so the template's own `?? 'Default'` guard supplies the
120
+ // real fallback, and a caller-supplied prop still wins via
121
+ // `propName`.
122
+ const metadata = metadataFor(`
123
+ 'use client'
124
+ import { createSignal } from '@barefootjs/client'
125
+ function C(props: { label?: string }) {
126
+ const [label, setLabel] = createSignal(props.label ?? 'Default')
127
+ return <span>{label()}</span>
128
+ }
129
+ `)
130
+
131
+ const defaults = extractSsrDefaults(metadata)
132
+ expect(defaults?.label).toEqual({ propName: 'label', value: null })
133
+ })
134
+
135
+ test('self-derived signal collision (#2669, shape B): non-idempotent derivation must not seed the pre-fix double-applied value', () => {
136
+ // `(props.count ?? 1) * 2` is NOT idempotent: seeding the stash with
137
+ // the pre-fix EVALUATED value (2, for no caller prop) makes the
138
+ // template's own recompute apply the `* 2` a second time (`2 * 2 =
139
+ // 4`) — wrong even with no caller props at all. Pin that the entry
140
+ // is the RAW-prop shape (`value: null`), not `{ value: 2 }`.
141
+ const metadata = metadataFor(`
142
+ 'use client'
143
+ import { createSignal } from '@barefootjs/client'
144
+ function C(props: { count?: number }) {
145
+ const [count, setCount] = createSignal((props.count ?? 1) * 2)
146
+ return <span>{count()}</span>
147
+ }
148
+ `)
149
+
150
+ const defaults = extractSsrDefaults(metadata)
151
+ expect(defaults?.count).toEqual({ propName: 'count', value: null })
152
+ })
153
+
154
+ test('self-derived memo collision (#2669): the same rule applies to a memo whose computation derives from a same-named prop', () => {
155
+ // Same collision, memo flavor: `createMemo(() => (props.label ??
156
+ // 'Default') + n())` — the memo's OWN name (`label`) is the
157
+ // template variable the emitted template both reads (raw prop) and
158
+ // overwrites (derived memo value). `n` is an unrelated ordinary
159
+ // signal and must be unaffected (plain `{ value }` entry, no
160
+ // `propName`).
161
+ const metadata = metadataFor(`
162
+ 'use client'
163
+ import { createSignal, createMemo } from '@barefootjs/client'
164
+ function C(props: { label?: string }) {
165
+ const [n, setN] = createSignal(0)
166
+ const label = createMemo(() => (props.label ?? 'Default') + n())
167
+ return <span>{label()}</span>
168
+ }
169
+ `)
170
+
171
+ const defaults = extractSsrDefaults(metadata)
172
+ expect(defaults?.label).toEqual({ propName: 'label', value: null })
173
+ expect(defaults?.n).toEqual({ value: 0 })
174
+ })
175
+
176
+ test('NON-self-derived collision (#2669, shape C — out of scope, must be unchanged): signal value still wins, no `propName`', () => {
177
+ // Same-named `label` getter and `label` prop, but the signal's OWN
178
+ // initializer does NOT derive from `props.label` (it's a plain
179
+ // string literal) — the JSX body separately reads `label()` AND
180
+ // `props.label`. That's a template-variable-ALIASING defect (both
181
+ // expressions lower to the same template variable, so no seeding
182
+ // choice can be right for both readers) — a different bug, tracked
183
+ // separately, and this fix must leave it byte-identical: the entry
184
+ // stays the plain evaluated-signal-value shape with no `propName`.
185
+ const metadata = metadataFor(`
186
+ 'use client'
187
+ import { createSignal } from '@barefootjs/client'
188
+ function C(props: { label?: string }) {
189
+ const [label, setLabel] = createSignal('sig')
190
+ return <span>{label()}{props.label}</span>
191
+ }
192
+ `)
193
+
194
+ const defaults = extractSsrDefaults(metadata)
195
+ expect(defaults?.label).toEqual({ value: 'sig' })
196
+ })
197
+
198
+ test('self-derived signal collision THROUGH a component-scope const (#2685 review): entry stays a PROP entry', () => {
199
+ // One hop of pure indirection past #2669's shape A: `referencesOwnProp`
200
+ // (built on `collectPropRefs`) only saw a DIRECT `props.<name>` access
201
+ // in the initializer — a `const mid = props.label` sitting between the
202
+ // prop read and `createSignal(mid ?? 'Default')` defeated the
203
+ // detection, so this entry regressed to the pre-#2669 `{ value:
204
+ // 'Default' }` shape (no `propName`) even after the #2669 fix landed.
205
+ // Same fix, widened: `collectPropRefsTransitive` now looks through
206
+ // component-scope const locals.
207
+ const metadata = metadataFor(`
208
+ 'use client'
209
+ import { createSignal } from '@barefootjs/client'
210
+ function C(props: { label?: string }) {
211
+ const mid = props.label
212
+ const [label, setLabel] = createSignal(mid ?? 'Default')
213
+ return <span>{label()}</span>
214
+ }
215
+ `)
216
+
217
+ const defaults = extractSsrDefaults(metadata)
218
+ expect(defaults?.label).toEqual({ propName: 'label', value: null })
219
+ })
220
+
221
+ test('self-derived memo collision THROUGH a component-scope const (#2685 review)', () => {
222
+ const metadata = metadataFor(`
223
+ 'use client'
224
+ import { createMemo } from '@barefootjs/client'
225
+ function C(props: { label?: string }) {
226
+ const mid = props.label
227
+ const label = createMemo(() => mid ?? 'Default')
228
+ return <span>{label()}</span>
229
+ }
230
+ `)
231
+
232
+ const defaults = extractSsrDefaults(metadata)
233
+ expect(defaults?.label).toEqual({ propName: 'label', value: null })
234
+ })
235
+
236
+ test('multi-hop const chain resolves transitively (#2685 review): `const a = props.x; const b = a`', () => {
237
+ // Two hops of indirection (`b` reads `a`, `a` reads `props.x`) — the
238
+ // signal here is named `count`, a DIFFERENT name from the prop `x`, so
239
+ // this exercises the bare-props safety net's transitive resolution
240
+ // (not the self-derivation `propName` collision path above): `x` must
241
+ // still be seeded, or the template-stash adapters' bare-scalar
242
+ // recompute for `x` aborts at render time (#1297/#2126).
243
+ const metadata = metadataFor(`
244
+ 'use client'
245
+ import { createSignal } from '@barefootjs/client'
246
+ function C(props: { x?: number }) {
247
+ const a = props.x
248
+ const b = a
249
+ const [count, setCount] = createSignal(b ?? 1)
250
+ return <span>{count()}</span>
251
+ }
252
+ `)
253
+
254
+ const defaults = extractSsrDefaults(metadata)
255
+ expect(defaults?.count).toEqual({ value: 1 })
256
+ expect(defaults?.x).toEqual({ propName: 'x', value: null })
257
+ })
258
+
259
+ test('safety-net seeding sees a prop through a component-scope const (#2685 review)', () => {
260
+ // The #1297/#2126 bare-props safety net (`collectPropRefs` at the
261
+ // bottom of `extractSsrDefaults`) only saw a DIRECT `props.X` access
262
+ // too — this is that same gap, single hop: `const mid = props.initial;
263
+ // createSignal(mid ?? 0)` misses seeding `initial`, which is a
264
+ // `Global symbol "$initial" requires explicit package name` render
265
+ // abort on Perl-strict backends. `count` (the signal's own name) is
266
+ // unrelated to `initial`, so this is NOT the self-collision path —
267
+ // purely the safety net.
268
+ const metadata = metadataFor(`
269
+ 'use client'
270
+ import { createSignal } from '@barefootjs/client'
271
+ function Counter(props: { initial?: number }) {
272
+ const mid = props.initial
273
+ const [count, setCount] = createSignal(mid ?? 0)
274
+ return <p>{count()}</p>
275
+ }
276
+ `)
277
+
278
+ const defaults = extractSsrDefaults(metadata)
279
+ expect(defaults?.count).toEqual({ value: 0 })
280
+ expect(defaults?.initial).toEqual({ propName: 'initial', value: null })
281
+ })
282
+
283
+ test('NEGATIVE (#2685 review): a const NOT derived from props keeps the signal-wins behavior', () => {
284
+ // Same-named `label` getter and `label` prop, wrapped in a const — but
285
+ // the const's OWN value doesn't read `props` at all, so
286
+ // `collectPropRefsTransitive` must NOT report a collision here. Must
287
+ // stay the plain evaluated-signal-value shape with no `propName` — the
288
+ // const-chain widening only ever ADDS prop references it finds by
289
+ // actually walking through `props.X`, never invents one.
290
+ //
291
+ // (The value resolves to `null`, not `'sig'`: `tryStaticEval` doesn't
292
+ // bind component-scope const locals into its evaluation `bindings` —
293
+ // see the signal loop's `bindings` comment above — so `mid` itself is
294
+ // unresolved for VALUE purposes independent of this fix; only the
295
+ // separate `propName`-detection walk this test is pinning follows the
296
+ // const chain.)
297
+ const metadata = metadataFor(`
298
+ 'use client'
299
+ import { createSignal } from '@barefootjs/client'
300
+ function C(props: { label?: string }) {
301
+ const mid = 'sig'
302
+ const [label, setLabel] = createSignal(mid)
303
+ return <span>{label()}{props.label}</span>
304
+ }
305
+ `)
306
+
307
+ const defaults = extractSsrDefaults(metadata)
308
+ expect(defaults?.label).toEqual({ value: null })
309
+ })
310
+
106
311
  test('aliased destructured prop (#2460): `propName` is the CALLER-facing key, not the local binding', () => {
107
312
  // `{ n: count }` — the caller passes `n`, the template variable (and
108
313
  // stash entry key) is the local binding `count`. The manifest
@@ -302,6 +507,31 @@ describe('deriveStashFromDefaults', () => {
302
507
  expect(deriveStashFromDefaults(defaults, { n: 0 })).toEqual({ count: 0 })
303
508
  })
304
509
 
510
+ test('self-derived signal collision (#2669): the caller-supplied prop now wins, and an absent prop falls back to null', () => {
511
+ // End-to-end proof of the fix's effect at the `deriveStashFromDefaults`
512
+ // layer: run the real `extractSsrDefaults` output for shape A through
513
+ // the seeding function. Pre-fix, this entry was `{ value: 'Default' }`
514
+ // (propName-less) so a caller's `label: 'Hello'` was IGNORED —
515
+ // `deriveStashFromDefaults` had no `propName` to resolve against and
516
+ // always used the static value. Post-fix the entry is a PROP entry, so
517
+ // the caller's value wins, and an absent prop resolves to the RAW
518
+ // fallback `null` (not the old evaluated `'Default'`) — the emitted
519
+ // template's own `?? 'Default'` guard supplies the real default from
520
+ // there.
521
+ const metadata = metadataFor(`
522
+ 'use client'
523
+ import { createSignal } from '@barefootjs/client'
524
+ function C(props: { label?: string }) {
525
+ const [label, setLabel] = createSignal(props.label ?? 'Default')
526
+ return <span>{label()}</span>
527
+ }
528
+ `)
529
+ const defaults = extractSsrDefaults(metadata)!
530
+
531
+ expect(deriveStashFromDefaults(defaults, { label: 'Hello' })).toEqual({ label: 'Hello' })
532
+ expect(deriveStashFromDefaults(defaults, {})).toEqual({ label: null })
533
+ })
534
+
305
535
  test('propName-less entry (signal / memo local): always uses the static value', () => {
306
536
  // A caller cannot override an internal signal/memo by construction —
307
537
  // even a same-named caller prop must not leak in.
@@ -209,4 +209,76 @@ describe('computeSsrSeedPlan', () => {
209
209
  expect(on.frees).toEqual(['props'])
210
210
  }
211
211
  })
212
+
213
+ test('prop-derived signal init THROUGH a component-scope const (#2685 review): still derived, resolves to the prop free', () => {
214
+ // Pre-fix, `mid` (a component-scope const, not part of `baseScope`)
215
+ // was a free identifier `classify` couldn't find in `available`, so
216
+ // this step fell to `opaque` — no adapter emits an in-template
217
+ // recompute for it at all (a DIFFERENT, worse-than-#2669 defect: even
218
+ // with `propName` restored on the manifest side, an absent caller prop
219
+ // would render permanently empty instead of the signal's own
220
+ // `'Default'`). `resolveThroughLocalConsts` inlines `mid` to
221
+ // `props.defaultOn` before classification, so this now derives exactly
222
+ // like the direct-access form above.
223
+ const plan = planFor(`
224
+ 'use client'
225
+ import { createSignal } from '@barefootjs/client'
226
+ function Toggle(props: { defaultOn?: boolean }) {
227
+ const mid = props.defaultOn
228
+ const [on, setOn] = createSignal(mid ?? false)
229
+ return <button aria-pressed={on()}>t</button>
230
+ }
231
+ `)
232
+
233
+ const on = step(plan, 'on')
234
+ expect(on.kind).toBe('derived')
235
+ if (on.kind === 'derived') {
236
+ expect(on.origin).toBe('signal')
237
+ // The RESOLVED free set names `props` (what the inlined form reads),
238
+ // never the intermediate const `mid` — a consumer emitting `on`'s
239
+ // seed line never needs to know `mid` existed.
240
+ expect(on.frees).toEqual(['props'])
241
+ }
242
+ })
243
+
244
+ test('multi-hop const chain THROUGH TWO component-scope consts (#2685 review): still derived', () => {
245
+ const plan = planFor(`
246
+ 'use client'
247
+ import { createSignal } from '@barefootjs/client'
248
+ function C(props: { x?: number }) {
249
+ const a = props.x
250
+ const b = a
251
+ const [count, setCount] = createSignal(b ?? 1)
252
+ return <p>{count()}</p>
253
+ }
254
+ `)
255
+
256
+ const count = step(plan, 'count')
257
+ expect(count.kind).toBe('derived')
258
+ if (count.kind === 'derived') {
259
+ expect(count.frees).toEqual(['props'])
260
+ }
261
+ })
262
+
263
+ test('const chain that bottoms out on an UNAVAILABLE name → opaque (fails safe, not a wrong substitution)', () => {
264
+ // `mid` inlines to `laterMemo`, a memo declared AFTER `on` in source —
265
+ // per the plan's ordering guarantee, `laterMemo` is not yet in
266
+ // `available` when `on` is classified, so this must stay `opaque`
267
+ // exactly like a direct forward-reference would (mirrors the existing
268
+ // "forward reference … → opaque" case above, one hop of const
269
+ // indirection removed).
270
+ const plan = planFor(`
271
+ 'use client'
272
+ import { createSignal, createMemo } from '@barefootjs/client'
273
+ function C() {
274
+ const mid = laterMemo()
275
+ const [on, setOn] = createSignal(mid ?? false)
276
+ const laterMemo = createMemo(() => true)
277
+ return <button aria-pressed={on()}>{laterMemo()}</button>
278
+ }
279
+ `)
280
+
281
+ const on = step(plan, 'on')
282
+ expect(on.kind).toBe('opaque')
283
+ })
212
284
  })
package/src/analyzer.ts CHANGED
@@ -3429,6 +3429,82 @@ function collectKeysFromMembers(
3429
3429
  return keys
3430
3430
  }
3431
3431
 
3432
+ /**
3433
+ * Decide whether a member's `TypeInfo` is admissible for
3434
+ * `collectMemberTypes`'s destructured-parameter resolution (#2677 —
3435
+ * widened from the original string/number/boolean-only gate).
3436
+ *
3437
+ * #2150 originally restricted the gate to string/number/boolean, because a
3438
+ * non-primitive `TypeInfo` there used to mean "the typed adapters will emit
3439
+ * an unchecked scalar assertion (`in.X.(int)`) that panics" for a shape the
3440
+ * template layer had no representation for. That reasoning does NOT extend
3441
+ * to a rich type with a CATALOGUED lowering (#2274: `Date` → the `date`
3442
+ * helper): its propsParams `TypeInfo` is consumed only as call-site
3443
+ * evidence for `resolveReceiverType` (rich-type-evidence.ts) — never
3444
+ * emitted as a concrete field type (`typeInfoToGo`'s `interface` case falls
3445
+ * through to `interface{}` for an unbacked host name exactly as `unknown`
3446
+ * already did) — so there is no assertion to panic. Gating on
3447
+ * `CATALOGUED_RICH_TYPE_NAMES` specifically (not `HOST_RICH_TYPE_NAMES`
3448
+ * wholesale) keeps an un-catalogued rich type (`Map`, `Set`, …) at
3449
+ * `unknown`, i.e. avoids resurrecting the #2150 mistake for a shape no
3450
+ * lowering plugin exists for yet.
3451
+ *
3452
+ * Nor does it extend to a STRUCTURAL shape (`kind: 'array'` /
3453
+ * `kind: 'object'`) any more. `typeNodeToTypeInfo` already builds these
3454
+ * fully recursively (`elementType`, `properties`) — the same walk the
3455
+ * `props`-object form (`extractPropsFromTypeMembers`) always got — and
3456
+ * go-template's `emitSynthPropStructs` (#2674/#2676) synthesizes a real,
3457
+ * json-tagged Go struct for any anonymous object type it reaches through
3458
+ * `ir.metadata.propsParams[].type`, array-element positions included. A
3459
+ * structural `TypeInfo` is representable today, so admitting it here no
3460
+ * longer resurrects #2150 either — it lets `propsParams[].type` carry the
3461
+ * SAME shape the props-object form already resolved, closing the
3462
+ * asymmetry #2677 reports (`{ items }: { items: {...}[] }` degrading to
3463
+ * `unknown` while `(props: { items: {...}[] })` resolved fully).
3464
+ *
3465
+ * `array`/`object` admit only when EVERY reachable leaf resolves — an
3466
+ * array's element type and an object's every property type must pass this
3467
+ * same check, recursively. A union, a function, or an un-catalogued named
3468
+ * type reachable ANYWHERE inside the shape declines the WHOLE member (not
3469
+ * just that one leaf): `collectMemberTypes` is an all-or-nothing gate per
3470
+ * member, so a partially-resolvable structural type must still decline —
3471
+ * per-field graceful degradation is `typeInfoToGo`'s job downstream, not
3472
+ * this gate's.
3473
+ *
3474
+ * The `object` arm requires `properties` to be PRESENT, not merely falsy-
3475
+ * coalesced to `[]` — an absent-members claim and a zero-members claim are
3476
+ * different things, and `.every()` on a defaulted-to-`[]` array would admit
3477
+ * both identically (vacuously `true`), which is the #2150 failure mode
3478
+ * wearing a different hat: a concrete representation with no evidence
3479
+ * behind it. `typeNodeToTypeInfo`'s `object` arm DOES omit `properties`
3480
+ * entirely in its `synthetic` mode (the checker-driven `tsTypeToTypeInfo`
3481
+ * path, used for e.g. `createMemo` inference off a resolved `ts.Type`,
3482
+ * which has no source positions for `membersToProperties`' `getText()`).
3483
+ * This gate's only caller (`collectMemberTypes`'s `fromMembers`) always
3484
+ * calls the NON-synthetic 2-arg form, so `properties` is in practice always
3485
+ * at least `[]` here today — but `isResolvableMemberType` is a module-level
3486
+ * function, not a closure scoped to that one caller, so it guards the
3487
+ * general case rather than trust a caller-specific invariant. A genuinely
3488
+ * EMPTY object literal (`{}`, real zero members) DOES resolve — an empty
3489
+ * synthesized struct is exactly as faithful a representation as the type
3490
+ * itself claims, nothing is being guessed.
3491
+ */
3492
+ function isResolvableMemberType(info: TypeInfo): boolean {
3493
+ switch (info.kind) {
3494
+ case 'primitive':
3495
+ return info.primitive === 'string' || info.primitive === 'number' || info.primitive === 'boolean'
3496
+ case 'interface':
3497
+ return CATALOGUED_RICH_TYPE_NAMES.has(baseTypeName(info.raw))
3498
+ case 'array':
3499
+ return !!info.elementType && isResolvableMemberType(info.elementType)
3500
+ case 'object':
3501
+ return info.properties !== undefined && info.properties.every(prop => isResolvableMemberType(prop.type))
3502
+ // Unions, functions, and `unknown` all decline — see docstring.
3503
+ default:
3504
+ return false
3505
+ }
3506
+ }
3507
+
3432
3508
  /**
3433
3509
  * Build a property-name -> { type, optional } map from a props type
3434
3510
  * annotation, for destructured params (`{ value }: Props`) so they carry the
@@ -3443,36 +3519,17 @@ function collectKeysFromMembers(
3443
3519
  * degraded to `unknown` -> `interface{}` for attribute omission, because
3444
3520
  * #2252's nullish-flip machinery supplies the nillable representation
3445
3521
  * exactly where absence is semantically observable (#2259).
3446
- * - Only PRIMITIVE members (string/number/boolean) resolve a type. Those are
3447
- * the types that otherwise produce an unchecked scalar assertion (`in.X.(int)`)
3448
- * that panics. Arrays/objects/functions are left as `unknown` because typed
3449
- * adapters lower them through interface{}-based helpers (`bf_flat`, spread,
3450
- * `bf_json`); giving them concrete types would break that lowering and is a
3451
- * larger, separate change.
3522
+ * - A member resolves a type when it is a PRIMITIVE (string/number/boolean),
3523
+ * a CATALOGUED rich type (`Date`), or a STRUCTURAL shape (array/object)
3524
+ * built entirely out of those, recursively (#2677). Unions, functions, and
3525
+ * un-catalogued named types (`Map`, `Set`, …) are left as `unknown` — see
3526
+ * `isResolvableMemberType`'s own docstring for why the structural cases
3527
+ * are safe to admit and why those three still are not.
3452
3528
  */
3453
3529
  function collectMemberTypes(
3454
3530
  typeNode: ts.TypeNode,
3455
3531
  ctx: AnalyzerContext
3456
3532
  ): Map<string, { type: TypeInfo | null; optional: boolean }> | null {
3457
- // #2150 originally restricted this gate to string/number/boolean only,
3458
- // because a non-primitive TypeInfo here used to mean "the typed adapters
3459
- // will emit an unchecked scalar assertion (`in.X.(int)`) that panics" for
3460
- // a shape the template layer has no representation for. That reasoning
3461
- // does NOT extend to a rich type with a CATALOGUED lowering (#2274: `Date`
3462
- // → the `date` helper): its propsParams TypeInfo is consumed only as
3463
- // call-site evidence for `resolveReceiverType` (rich-type-evidence.ts) —
3464
- // never emitted as a concrete field type (`typeInfoToGo`'s `interface`
3465
- // case falls through to `interface{}` for an unbacked host name exactly
3466
- // as `unknown` already did) — so there is no assertion to panic. Gating on
3467
- // `CATALOGUED_RICH_TYPE_NAMES` specifically (not `HOST_RICH_TYPE_NAMES`
3468
- // wholesale) keeps an un-catalogued rich type (`Map`, `Set`, …) at
3469
- // `unknown`, i.e. avoids resurrecting the #2150 mistake for a shape no
3470
- // lowering plugin exists for yet.
3471
- const isResolvablePrimitive = (info: TypeInfo): boolean =>
3472
- (info.kind === 'primitive' &&
3473
- (info.primitive === 'string' || info.primitive === 'number' || info.primitive === 'boolean')) ||
3474
- (info.kind === 'interface' && CATALOGUED_RICH_TYPE_NAMES.has(baseTypeName(info.raw)))
3475
-
3476
3533
  const fromMembers = (
3477
3534
  members: ts.NodeArray<ts.TypeElement>
3478
3535
  ): Map<string, { type: TypeInfo | null; optional: boolean }> => {
@@ -3481,7 +3538,7 @@ function collectMemberTypes(
3481
3538
  if (ts.isPropertySignature(member) && member.name) {
3482
3539
  const info = member.type ? typeNodeToTypeInfo(member.type, ctx.sourceFile) : null
3483
3540
  map.set(member.name.getText(ctx.sourceFile), {
3484
- type: info && isResolvablePrimitive(info) ? info : null,
3541
+ type: info && isResolvableMemberType(info) ? info : null,
3485
3542
  optional: !!member.questionToken,
3486
3543
  })
3487
3544
  }
@@ -3037,8 +3037,17 @@ function usesPerPath(name: string, expr: ParsedExpr): { min: number; max: number
3037
3037
  * leaves that inner reference untouched (it is the parameter, not the binding).
3038
3038
  * Mirrors the structural walk of `substituteDestructuredFields`; every
3039
3039
  * `ParsedExpr` kind is handled so a new kind surfaces as a compile error here.
3040
+ *
3041
+ * Exported for `computeSsrSeedPlan` (`ssr-seed-plan.ts`), which reuses this
3042
+ * SAME let-inline step to resolve a signal/memo initializer through
3043
+ * component-scope `const` locals sitting between it and the prop it reads
3044
+ * (`const mid = props.label; createSignal(mid ?? 'Default')`) — the
3045
+ * structural (never string-splicing) analogue of what `foldBlockToExpr`
3046
+ * already does for a block-bodied memo's own internal `const`s, and of what
3047
+ * the CSR client-JS emitter's `csrSubstitute` does over TS source text for
3048
+ * the compiled JS domain (#2685 review).
3040
3049
  */
3041
- function inlineBinding(
3050
+ export function inlineBinding(
3042
3051
  expr: ParsedExpr,
3043
3052
  name: string,
3044
3053
  value: ParsedExpr,
package/src/index.ts CHANGED
@@ -97,7 +97,6 @@ export { emitParsedExpr, groupBinaryOperand, isStringTypedOperand, isStringConca
97
97
  export type { ParsedExprEmitter, HigherOrderMethod, ArrayMethod, SortMethod, LiteralType } from './adapters/parsed-expr-emitter.ts'
98
98
  export { collectLoopBoundNames } from './adapters/loop-bound-names.ts'
99
99
  export { derivesScopeFromSlot } from './adapters/child-scope.ts'
100
- export { evaluateSignalInit, tryEvaluateSignalInit, type SignalInitEvalResult } from './signal-init-eval.ts'
101
100
  export { evaluateStaticLiteral, isFullyStaticLiteral, resolveStaticLoopSource } from './static-literal.ts'
102
101
  export { importsSearchParams, searchParamsLocalNames, envSignalLocalNames, envSignalReaderFor, ENV_SIGNAL_READERS, queryHrefLocalNames, formatDateLocalNames, matchSearchParamsMethodCall } from './adapters/env-signal.ts'
103
102
  export type { EnvSignalReader } from './adapters/env-signal.ts'