@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.
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/expression-parser.d.ts +20 -0
- package/dist/expression-parser.d.ts.map +1 -1
- package/dist/index.d.ts +0 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +99 -77
- package/dist/ssr-defaults.d.ts +10 -0
- package/dist/ssr-defaults.d.ts.map +1 -1
- package/dist/ssr-seed-plan.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/date-lowering.test.ts +1 -1
- package/src/__tests__/props-destructuring.test.ts +40 -3
- package/src/__tests__/ssr-defaults.test.ts +230 -0
- package/src/__tests__/ssr-seed-plan.test.ts +72 -0
- package/src/analyzer.ts +83 -26
- package/src/expression-parser.ts +10 -1
- package/src/index.ts +0 -1
- package/src/ssr-defaults.ts +171 -6
- package/src/ssr-seed-plan.ts +82 -2
- package/dist/signal-init-eval.d.ts +0 -82
- package/dist/signal-init-eval.d.ts.map +0 -1
- package/src/__tests__/signal-init-eval.test.ts +0 -138
- package/src/signal-init-eval.ts +0 -165
|
@@ -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
|
-
* -
|
|
3447
|
-
*
|
|
3448
|
-
*
|
|
3449
|
-
*
|
|
3450
|
-
* `
|
|
3451
|
-
*
|
|
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 &&
|
|
3541
|
+
type: info && isResolvableMemberType(info) ? info : null,
|
|
3485
3542
|
optional: !!member.questionToken,
|
|
3486
3543
|
})
|
|
3487
3544
|
}
|
package/src/expression-parser.ts
CHANGED
|
@@ -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'
|