@barefootjs/jsx 0.17.0 → 0.18.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.
Files changed (88) hide show
  1. package/dist/adapters/env-signal.d.ts +42 -7
  2. package/dist/adapters/env-signal.d.ts.map +1 -1
  3. package/dist/adapters/interface.d.ts +20 -4
  4. package/dist/adapters/interface.d.ts.map +1 -1
  5. package/dist/adapters/parsed-expr-emitter.d.ts +3 -1
  6. package/dist/adapters/parsed-expr-emitter.d.ts.map +1 -1
  7. package/dist/analyzer-context.d.ts.map +1 -1
  8. package/dist/analyzer.d.ts.map +1 -1
  9. package/dist/augment-inherited-props.d.ts +19 -0
  10. package/dist/augment-inherited-props.d.ts.map +1 -1
  11. package/dist/compiler.d.ts.map +1 -1
  12. package/dist/expression-parser.d.ts +48 -2
  13. package/dist/expression-parser.d.ts.map +1 -1
  14. package/dist/index.d.ts +24 -5
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +1728 -969
  17. package/dist/ir-to-client-js/collect-elements.d.ts.map +1 -1
  18. package/dist/ir-to-client-js/compute-inlinability.d.ts.map +1 -1
  19. package/dist/ir-to-client-js/control-flow/plan/build-loop.d.ts.map +1 -1
  20. package/dist/ir-to-client-js/control-flow/plan/loop.d.ts +9 -0
  21. package/dist/ir-to-client-js/control-flow/plan/loop.d.ts.map +1 -1
  22. package/dist/ir-to-client-js/control-flow/stringify/loop.d.ts.map +1 -1
  23. package/dist/ir-to-client-js/control-flow/stringify/template-parse.d.ts +19 -0
  24. package/dist/ir-to-client-js/control-flow/stringify/template-parse.d.ts.map +1 -1
  25. package/dist/ir-to-client-js/emit-registration.d.ts +4 -2
  26. package/dist/ir-to-client-js/emit-registration.d.ts.map +1 -1
  27. package/dist/ir-to-client-js/html-template.d.ts +41 -0
  28. package/dist/ir-to-client-js/html-template.d.ts.map +1 -1
  29. package/dist/ir-to-client-js/types.d.ts +22 -1
  30. package/dist/ir-to-client-js/types.d.ts.map +1 -1
  31. package/dist/jsx-to-ir.d.ts.map +1 -1
  32. package/dist/loop-destructure.d.ts +55 -18
  33. package/dist/loop-destructure.d.ts.map +1 -1
  34. package/dist/lowering-registry.d.ts +13 -0
  35. package/dist/lowering-registry.d.ts.map +1 -1
  36. package/dist/relocate.d.ts +28 -0
  37. package/dist/relocate.d.ts.map +1 -1
  38. package/dist/ssr-defaults.d.ts.map +1 -1
  39. package/dist/ssr-seed-plan.d.ts +84 -0
  40. package/dist/ssr-seed-plan.d.ts.map +1 -0
  41. package/dist/types.d.ts +79 -0
  42. package/dist/types.d.ts.map +1 -1
  43. package/package.json +2 -2
  44. package/src/__tests__/__snapshots__/doc-examples.test.ts.snap +284 -12
  45. package/src/__tests__/augment-inherited-props.test.ts +96 -0
  46. package/src/__tests__/compiler-runtime-contract.test.ts +11 -1
  47. package/src/__tests__/compiler-stress-1244.test.ts +13 -4
  48. package/src/__tests__/csr-substitution-safety-divergence.test.ts +137 -0
  49. package/src/__tests__/destructured-map-params.test.ts +11 -1
  50. package/src/__tests__/expression-parser.test.ts +74 -3
  51. package/src/__tests__/free-identifiers.test.ts +55 -0
  52. package/src/__tests__/ir-sort-comparator.test.ts +261 -0
  53. package/src/__tests__/loop-destructure.test.ts +313 -0
  54. package/src/__tests__/loop-hoisted-template.test.ts +235 -0
  55. package/src/__tests__/materialize-getter-calls.test.ts +58 -0
  56. package/src/__tests__/props-destructuring.test.ts +110 -0
  57. package/src/__tests__/serialize-parsed-expr.test.ts +88 -2
  58. package/src/__tests__/ssr-defaults.test.ts +20 -0
  59. package/src/__tests__/ssr-seed-plan.test.ts +212 -0
  60. package/src/__tests__/staged-ir/11-template-primitive-registry.test.ts +231 -1
  61. package/src/__tests__/tagged-template-interleave.test.ts +268 -0
  62. package/src/__tests__/unsupported-expression.test.ts +194 -7
  63. package/src/adapters/env-signal.ts +57 -9
  64. package/src/adapters/interface.ts +20 -4
  65. package/src/adapters/parsed-expr-emitter.ts +19 -2
  66. package/src/analyzer-context.ts +20 -0
  67. package/src/analyzer.ts +74 -1
  68. package/src/augment-inherited-props.ts +139 -9
  69. package/src/compiler.ts +10 -1
  70. package/src/expression-parser.ts +421 -50
  71. package/src/index.ts +30 -3
  72. package/src/ir-to-client-js/collect-elements.ts +15 -1
  73. package/src/ir-to-client-js/compute-inlinability.ts +6 -1
  74. package/src/ir-to-client-js/control-flow/plan/build-loop.ts +1 -0
  75. package/src/ir-to-client-js/control-flow/plan/loop.ts +9 -0
  76. package/src/ir-to-client-js/control-flow/stringify/loop.ts +30 -8
  77. package/src/ir-to-client-js/control-flow/stringify/template-parse.ts +30 -0
  78. package/src/ir-to-client-js/emit-registration.ts +4 -2
  79. package/src/ir-to-client-js/html-template.ts +198 -1
  80. package/src/ir-to-client-js/index.ts +1 -0
  81. package/src/ir-to-client-js/types.ts +22 -0
  82. package/src/jsx-to-ir.ts +409 -24
  83. package/src/loop-destructure.ts +89 -36
  84. package/src/lowering-registry.ts +16 -0
  85. package/src/relocate.ts +201 -14
  86. package/src/ssr-defaults.ts +34 -32
  87. package/src/ssr-seed-plan.ts +146 -0
  88. package/src/types.ts +76 -0
@@ -0,0 +1,212 @@
1
+ // Backend-neutral SSR seed plan (`computeSsrSeedPlan`, attached to
2
+ // `IRMetadata.ssrSeedPlan` by `buildMetadata`). The plan ports the
3
+ // derived/opaque/env-reader scope analysis the template adapters' seed paths
4
+ // perform, so these tests pin the decision rules against metadata built by
5
+ // the real pipeline (`analyzeComponent` + `buildMetadata`).
6
+
7
+ import { describe, test, expect } from 'bun:test'
8
+ import { analyzeComponent } from '../analyzer'
9
+ import { buildMetadata } from '../compiler'
10
+ import type { SsrSeedPlan, SsrSeedStep } from '../ssr-seed-plan'
11
+
12
+ function planFor(source: string, componentName?: string): SsrSeedPlan {
13
+ const ctx = analyzeComponent(source, 'test.tsx', componentName)
14
+ const plan = buildMetadata(ctx).ssrSeedPlan
15
+ expect(plan).toBeDefined()
16
+ return plan!
17
+ }
18
+
19
+ function step(plan: SsrSeedPlan, name: string): SsrSeedStep {
20
+ const found = plan.steps.find(s => s.name === name)
21
+ expect(found).toBeDefined()
22
+ return found!
23
+ }
24
+
25
+ describe('computeSsrSeedPlan', () => {
26
+ test('env signal (aliased) → env-reader step; derived memo over it', () => {
27
+ const plan = planFor(`
28
+ 'use client'
29
+ import { createMemo, createSearchParams } from '@barefootjs/client'
30
+ function List() {
31
+ const [sp] = createSearchParams()
32
+ const sort = createMemo(() => sp().get('sort') ?? 'date')
33
+ return <p>{sort()}</p>
34
+ }
35
+ `)
36
+
37
+ const sp = step(plan, 'sp')
38
+ expect(sp.kind).toBe('env-reader')
39
+ if (sp.kind === 'env-reader') {
40
+ expect(sp.reader.canonicalName).toBe('searchParams')
41
+ expect(sp.reader.key).toBe('search')
42
+ }
43
+
44
+ const sort = step(plan, 'sort')
45
+ expect(sort.kind).toBe('derived')
46
+ if (sort.kind === 'derived') {
47
+ expect(sort.origin).toBe('memo')
48
+ expect(sort.frees).toEqual(['sp'])
49
+ expect(sort.expr).toBe("sp().get('sort') ?? 'date'")
50
+ expect(sort.parsed).toBeDefined()
51
+ }
52
+ })
53
+
54
+ test('chained memos in a props-object component: declaration order, scope accumulates', () => {
55
+ const plan = planFor(`
56
+ 'use client'
57
+ import { createMemo, createSearchParams } from '@barefootjs/client'
58
+ function List(props: { items: { tag: string; name: string }[] }) {
59
+ const [sp] = createSearchParams()
60
+ const tag = createMemo(() => sp().get('tag') ?? '')
61
+ const visible = createMemo(() => props.items.filter((p) => p.tag === tag()))
62
+ return <ul>{visible().map((i) => <li>{i.name}</li>)}</ul>
63
+ }
64
+ `)
65
+
66
+ expect(plan.baseScope).toContain('props')
67
+ expect(plan.steps.map(s => s.name)).toEqual(['sp', 'tag', 'visible'])
68
+
69
+ expect(step(plan, 'tag').kind).toBe('derived')
70
+ const visible = step(plan, 'visible')
71
+ expect(visible.kind).toBe('derived')
72
+ if (visible.kind === 'derived') {
73
+ for (const free of visible.frees) {
74
+ expect(['props', 'tag']).toContain(free)
75
+ }
76
+ }
77
+ })
78
+
79
+ test('forward reference to a later memo → opaque; the later memo itself is derived', () => {
80
+ const plan = planFor(`
81
+ 'use client'
82
+ import { createMemo, createSignal } from '@barefootjs/client'
83
+ function C() {
84
+ const [n, setN] = createSignal(1)
85
+ const early = createMemo(() => late() + 1)
86
+ const late = createMemo(() => n() * 2)
87
+ return <p>{early()}</p>
88
+ }
89
+ `)
90
+
91
+ expect(step(plan, 'early').kind).toBe('opaque')
92
+ expect(step(plan, 'late').kind).toBe('derived')
93
+ })
94
+
95
+ test('self reference → opaque (name enters scope only after its own step)', () => {
96
+ const plan = planFor(`
97
+ 'use client'
98
+ import { createMemo } from '@barefootjs/client'
99
+ function C() {
100
+ const loop = createMemo(() => loop())
101
+ return <p>{loop()}</p>
102
+ }
103
+ `)
104
+
105
+ expect(step(plan, 'loop').kind).toBe('opaque')
106
+ })
107
+
108
+ test('shadowed callback param leaking as an outer free identifier → opaque', () => {
109
+ const plan = planFor(`
110
+ 'use client'
111
+ import { createMemo } from '@barefootjs/client'
112
+ function C(props: { items: { ok: boolean }[] }) {
113
+ const bad = createMemo(() => props.items.filter((p) => p.ok) && p)
114
+ return <p>{bad()}</p>
115
+ }
116
+ `)
117
+
118
+ expect(step(plan, 'bad').kind).toBe('opaque')
119
+ })
120
+
121
+ test('module string const counts as base scope; memo referencing it is derived', () => {
122
+ const plan = planFor(`
123
+ 'use client'
124
+ import { createMemo, createSignal } from '@barefootjs/client'
125
+ const activeCls = 'text-bold'
126
+ function C() {
127
+ const [on, setOn] = createSignal(false)
128
+ const cls = createMemo(() => on() ? activeCls : 'text-dim')
129
+ return <p class={cls()}>x</p>
130
+ }
131
+ `, 'C')
132
+
133
+ expect(plan.baseScope).toContain('activeCls')
134
+ const cls = step(plan, 'cls')
135
+ expect(cls.kind).toBe('derived')
136
+ if (cls.kind === 'derived') {
137
+ expect(cls.frees).toContain('on')
138
+ expect(cls.frees).toContain('activeCls')
139
+ }
140
+ })
141
+
142
+ test('block-bodied memo → opaque (v1 gates to expression-bodied memos)', () => {
143
+ const plan = planFor(`
144
+ 'use client'
145
+ import { createMemo, createSignal } from '@barefootjs/client'
146
+ function C() {
147
+ const [on, setOn] = createSignal(false)
148
+ const label = createMemo(() => {
149
+ const v = on()
150
+ return v ? 'yes' : 'no'
151
+ })
152
+ return <p>{label()}</p>
153
+ }
154
+ `)
155
+
156
+ const label = step(plan, 'label')
157
+ expect(label.kind).toBe('opaque')
158
+ if (label.kind === 'opaque') expect(label.origin).toBe('memo')
159
+ })
160
+
161
+ test('unsupported body (object literal) → opaque', () => {
162
+ const plan = planFor(`
163
+ 'use client'
164
+ import { createMemo, createSignal } from '@barefootjs/client'
165
+ function C() {
166
+ const [n, setN] = createSignal(1)
167
+ const obj = createMemo(() => ({ value: n() }))
168
+ return <p>{obj().value}</p>
169
+ }
170
+ `)
171
+
172
+ expect(step(plan, 'obj').kind).toBe('opaque')
173
+ })
174
+
175
+ test('literal signal init → derived with empty frees (constant-skip is emit-side)', () => {
176
+ const plan = planFor(`
177
+ 'use client'
178
+ import { createSignal } from '@barefootjs/client'
179
+ function C() {
180
+ const [v, setV] = createSignal('b')
181
+ return <p>{v()}</p>
182
+ }
183
+ `)
184
+
185
+ const v = step(plan, 'v')
186
+ expect(v.kind).toBe('derived')
187
+ if (v.kind === 'derived') {
188
+ expect(v.origin).toBe('signal')
189
+ expect(v.frees).toEqual([])
190
+ expect(v.expr).toBe("'b'")
191
+ }
192
+ })
193
+
194
+ test('prop-derived signal init → derived with the prop free', () => {
195
+ const plan = planFor(`
196
+ 'use client'
197
+ import { createSignal } from '@barefootjs/client'
198
+ function Toggle(props: { defaultOn?: boolean }) {
199
+ const [on, setOn] = createSignal(props.defaultOn ?? false)
200
+ return <button aria-pressed={on()}>t</button>
201
+ }
202
+ `)
203
+
204
+ expect(plan.baseScope).toContain('props')
205
+ const on = step(plan, 'on')
206
+ expect(on.kind).toBe('derived')
207
+ if (on.kind === 'derived') {
208
+ expect(on.origin).toBe('signal')
209
+ expect(on.frees).toEqual(['props'])
210
+ }
211
+ })
212
+ })
@@ -5,14 +5,25 @@
5
5
  * rejections for callees the adapter promises it can render in template
6
6
  * scope.
7
7
  *
8
+ * #2069 extends this with two more acceptance mechanisms, tested in the
9
+ * two `describe` blocks at the bottom of this file:
10
+ * - `RelocateEnv.loweringMatchers` — a THIRD acceptance path alongside
11
+ * `templatePrimitives` / `acceptsTemplateCall`, for calls a
12
+ * `LoweringPlugin` recognises structurally (never string-keyed).
13
+ * - `RelocateEnv.aliasTargets` — one-hop alias resolution, so
14
+ * `const fmt = customSerialize; fmt(x)` keys/matches as
15
+ * `customSerialize`, not `fmt`.
16
+ *
8
17
  * No adapter populates the registry yet — the wiring is exercised here
9
- * via hand-built env objects. Phase 3 will fill the Hono predicate.
18
+ * via hand-built env objects. Phase 3 filled the Hono predicate; #2069
19
+ * adds the plugin-matcher and alias seams.
10
20
  */
11
21
 
12
22
  import { describe, test, expect } from 'bun:test'
13
23
  import type { RelocateEnv } from '../../relocate'
14
24
  import { isInlinableInTemplate } from '../../relocate'
15
25
  import type { BindingKind } from '../../types'
26
+ import type { LoweringMatcher } from '../../lowering-registry'
16
27
 
17
28
  function envWith(
18
29
  bindings: Array<[string, BindingKind]>,
@@ -28,6 +39,8 @@ function envWith(
28
39
  allowFallback: options?.allowFallback ?? true,
29
40
  templatePrimitives: options?.templatePrimitives,
30
41
  acceptsTemplateCall: options?.acceptsTemplateCall,
42
+ loweringMatchers: options?.loweringMatchers,
43
+ aliasTargets: options?.aliasTargets,
31
44
  }
32
45
  }
33
46
 
@@ -256,3 +269,220 @@ describe('isInlinableInTemplate — adapter-aware acceptance (#1187 phase 2)', (
256
269
  })
257
270
  })
258
271
  })
272
+
273
+ /** A `LoweringMatcher` that recognises a bare-identifier call to `name`. */
274
+ function matcherFor(name: string): LoweringMatcher {
275
+ return (callee, args) => {
276
+ if (callee.kind !== 'identifier' || callee.name !== name) return null
277
+ return { kind: 'helper-call', helper: 'test_helper', args }
278
+ }
279
+ }
280
+
281
+ describe('isInlinableInTemplate — loweringMatchers acceptance (#2069)', () => {
282
+ test('a call recognised by a registered matcher is accepted', () => {
283
+ const env = envWith(
284
+ [
285
+ ['customSerialize', 'module-import'],
286
+ ['config', 'prop'],
287
+ ],
288
+ {
289
+ propsObjectName: null,
290
+ loweringMatchers: [matcherFor('customSerialize')],
291
+ },
292
+ )
293
+ const r = isInlinableInTemplate('customSerialize(config)', env)
294
+ expect(r.ok).toBe(true)
295
+ expect(r.rewrittenValue).toContain('customSerialize')
296
+ })
297
+
298
+ test('without a matching matcher, the call is still rejected', () => {
299
+ const env = envWith(
300
+ [
301
+ ['customSerialize', 'module-import'],
302
+ ['config', 'prop'],
303
+ ],
304
+ {
305
+ propsObjectName: null,
306
+ loweringMatchers: [matcherFor('someOtherHelper')],
307
+ },
308
+ )
309
+ const r = isInlinableInTemplate('customSerialize(config)', env)
310
+ expect(r.ok).toBe(false)
311
+ })
312
+
313
+ test('templatePrimitives / acceptsTemplateCall / loweringMatchers are all independent acceptance paths', () => {
314
+ // Only the matcher accepts here — no string-keyed entry exists at all.
315
+ const env = envWith(
316
+ [
317
+ ['customSerialize', 'module-import'],
318
+ ['config', 'prop'],
319
+ ],
320
+ {
321
+ propsObjectName: null,
322
+ templatePrimitives: {},
323
+ acceptsTemplateCall: () => false,
324
+ loweringMatchers: [matcherFor('customSerialize')],
325
+ },
326
+ )
327
+ const r = isInlinableInTemplate('customSerialize(config)', env)
328
+ expect(r.ok).toBe(true)
329
+ })
330
+
331
+ describe('shadow guard applies to matcher acceptance too', () => {
332
+ test('a local binding shadowing the recognised name is refused', () => {
333
+ // `customSerialize` here is a component-local (e.g. reassigned from
334
+ // a prop), NOT the module import the plugin's `prepare()` resolved
335
+ // against. The matcher itself has no way to know this — the shadow
336
+ // guard in `isCallAcceptedByAdapter` is what keeps it from firing.
337
+ const env = envWith(
338
+ [
339
+ ['customSerialize', 'init-local'], // shadows the import
340
+ ['config', 'prop'],
341
+ ],
342
+ {
343
+ propsObjectName: null,
344
+ loweringMatchers: [matcherFor('customSerialize')],
345
+ },
346
+ )
347
+ const r = isInlinableInTemplate('customSerialize(config)', env)
348
+ expect(r.ok).toBe(false)
349
+ })
350
+
351
+ test('module-import / module-local bindings still activate the matcher', () => {
352
+ const env = envWith(
353
+ [
354
+ ['customSerialize', 'module-local'],
355
+ ['config', 'prop'],
356
+ ],
357
+ {
358
+ propsObjectName: null,
359
+ loweringMatchers: [matcherFor('customSerialize')],
360
+ },
361
+ )
362
+ const r = isInlinableInTemplate('customSerialize(config)', env)
363
+ expect(r.ok).toBe(true)
364
+ })
365
+ })
366
+ })
367
+
368
+ describe('isInlinableInTemplate — one-hop alias resolution (#2069 R2)', () => {
369
+ test('alias → registered builtin (templatePrimitives) is accepted', () => {
370
+ // const fmt = Math.floor; fmt(score)
371
+ const env = envWith(
372
+ [
373
+ ['fmt', 'init-local'],
374
+ ['Math', 'global'],
375
+ ['score', 'prop'],
376
+ ],
377
+ {
378
+ propsObjectName: null,
379
+ aliasTargets: new Map([['fmt', 'Math.floor']]),
380
+ templatePrimitives: {
381
+ 'Math.floor': (args) => `Math.floor(${args[0]})`,
382
+ },
383
+ },
384
+ )
385
+ const r = isInlinableInTemplate('fmt(score)', env)
386
+ expect(r.ok).toBe(true)
387
+ expect(r.rewrittenValue).toContain('Math.floor')
388
+ })
389
+
390
+ test('aliased-namespace MEMBER callee resolves too (const m = Math; m.floor)', () => {
391
+ // The leftmost segment is spliced and the `.path` tail carried over —
392
+ // `m.floor(score)` keys the registry as `Math.floor` (Copilot review
393
+ // on #2097 pinned that this is intended, not bare-identifier-only).
394
+ const env = envWith(
395
+ [
396
+ ['m', 'init-local'],
397
+ ['Math', 'global'],
398
+ ['score', 'prop'],
399
+ ],
400
+ {
401
+ propsObjectName: null,
402
+ aliasTargets: new Map([['m', 'Math']]),
403
+ templatePrimitives: {
404
+ 'Math.floor': (args) => `Math.floor(${args[0]})`,
405
+ },
406
+ },
407
+ )
408
+ const r = isInlinableInTemplate('m.floor(score)', env)
409
+ expect(r.ok).toBe(true)
410
+ })
411
+
412
+ test('alias → a loweringMatchers-recognised import is accepted', () => {
413
+ // const serialize = customSerialize; serialize(config)
414
+ const env = envWith(
415
+ [
416
+ ['serialize', 'init-local'],
417
+ ['customSerialize', 'module-import'],
418
+ ['config', 'prop'],
419
+ ],
420
+ {
421
+ propsObjectName: null,
422
+ aliasTargets: new Map([['serialize', 'customSerialize']]),
423
+ loweringMatchers: [matcherFor('customSerialize')],
424
+ },
425
+ )
426
+ const r = isInlinableInTemplate('serialize(config)', env)
427
+ expect(r.ok).toBe(true)
428
+ expect(r.rewrittenValue).toContain('customSerialize')
429
+ })
430
+
431
+ test('alias → alias chain is refused — only ONE hop is resolved', () => {
432
+ // const g = f (f is itself just another local, never resolved further)
433
+ const env = envWith(
434
+ [
435
+ ['g', 'init-local'],
436
+ ['f', 'init-local'],
437
+ ['config', 'prop'],
438
+ ],
439
+ {
440
+ propsObjectName: null,
441
+ aliasTargets: new Map([['g', 'f']]),
442
+ // Maximally permissive predicate — even this can't help, because
443
+ // the shadow guard rejects on `f`'s own (unsafe) binding kind
444
+ // before the predicate is ever consulted.
445
+ acceptsTemplateCall: () => true,
446
+ },
447
+ )
448
+ const r = isInlinableInTemplate('g(config)', env)
449
+ expect(r.ok).toBe(false)
450
+ })
451
+
452
+ test('alias → a non-import component-local helper is refused', () => {
453
+ // const h = helper (helper is a local function/const, not module-scope)
454
+ const env = envWith(
455
+ [
456
+ ['h', 'init-local'],
457
+ ['helper', 'init-local'],
458
+ ['config', 'prop'],
459
+ ],
460
+ {
461
+ propsObjectName: null,
462
+ aliasTargets: new Map([['h', 'helper']]),
463
+ acceptsTemplateCall: () => true,
464
+ },
465
+ )
466
+ const r = isInlinableInTemplate('h(config)', env)
467
+ expect(r.ok).toBe(false)
468
+ })
469
+
470
+ test('a plain non-alias init-local is unaffected by aliasTargets being present', () => {
471
+ // Baseline: aliasTargets has entries for OTHER names, but this
472
+ // reference isn't one of them — falls through to the ordinary
473
+ // init-local fallback, same as if aliasTargets were absent.
474
+ const env = envWith(
475
+ [
476
+ ['count', 'init-local'],
477
+ ['config', 'prop'],
478
+ ],
479
+ {
480
+ propsObjectName: null,
481
+ aliasTargets: new Map([['fmt', 'Math.floor']]),
482
+ },
483
+ )
484
+ const r = isInlinableInTemplate('count', env)
485
+ expect(r.ok).toBe(true) // fallback to 'undefined', not a rejection
486
+ expect(r.rewrittenValue).toBe('undefined')
487
+ })
488
+ })