@barefootjs/jsx 0.29.0 → 0.30.2
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/adapters/child-scope.d.ts +17 -0
- package/dist/adapters/child-scope.d.ts.map +1 -0
- package/dist/adapters/loop-bound-names.d.ts.map +1 -1
- package/dist/analyzer-context.d.ts.map +1 -1
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +990 -367
- package/dist/ir-to-client-js/collect-elements.d.ts +5 -25
- package/dist/ir-to-client-js/collect-elements.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/build-lazy-row.d.ts +66 -2
- package/dist/ir-to-client-js/control-flow/plan/build-lazy-row.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/build-reactive-effects.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/lazy-conditional.d.ts +92 -0
- package/dist/ir-to-client-js/control-flow/plan/lazy-conditional.d.ts.map +1 -0
- package/dist/ir-to-client-js/control-flow/plan/lazy-preamble.d.ts +134 -0
- package/dist/ir-to-client-js/control-flow/plan/lazy-preamble.d.ts.map +1 -0
- package/dist/ir-to-client-js/control-flow/plan/lazy-row-eligibility.d.ts +46 -11
- package/dist/ir-to-client-js/control-flow/plan/lazy-row-eligibility.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/reactive-effects.d.ts +7 -0
- package/dist/ir-to-client-js/control-flow/plan/reactive-effects.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/lazy-row.d.ts.map +1 -1
- package/dist/ir-to-client-js/csr-substitute.d.ts.map +1 -1
- package/dist/ir-to-client-js/html-template.d.ts +2 -3
- package/dist/ir-to-client-js/html-template.d.ts.map +1 -1
- package/dist/ir-to-client-js/plan/declaration-emit.d.ts +6 -0
- package/dist/ir-to-client-js/plan/declaration-emit.d.ts.map +1 -1
- package/dist/ir-to-client-js/reactivity.d.ts +1 -10
- package/dist/ir-to-client-js/reactivity.d.ts.map +1 -1
- package/dist/ir-to-client-js/types.d.ts +12 -0
- package/dist/ir-to-client-js/types.d.ts.map +1 -1
- package/dist/jsx-to-ir.d.ts.map +1 -1
- package/dist/prop-rewrite.d.ts +24 -7
- package/dist/prop-rewrite.d.ts.map +1 -1
- package/dist/rich-type-evidence.d.ts.map +1 -1
- package/dist/ssr-defaults.d.ts.map +1 -1
- package/dist/to-locale-date-lowering.d.ts.map +1 -1
- package/dist/types.d.ts +78 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/__snapshots__/doc-examples.test.ts.snap +6 -24
- package/src/__tests__/branch-loop-plain.test.ts +28 -45
- package/src/__tests__/csr-substitution-safety-divergence.test.ts +9 -3
- package/src/__tests__/csr-template-scope-soundness.test.ts +167 -0
- package/src/__tests__/early-return-branch-switch.test.ts +96 -0
- package/src/__tests__/form-control-value-ssr.test.ts +102 -0
- package/src/__tests__/lazy-conditional.test.ts +190 -0
- package/src/__tests__/lazy-preamble.test.ts +297 -0
- package/src/__tests__/lazy-row-eligibility.test.ts +97 -28
- package/src/__tests__/loop-item-root-scope.test.ts +213 -0
- package/src/__tests__/map-multi-return-body.test.ts +30 -3
- package/src/__tests__/nested-loop-conditional.test.ts +27 -6
- package/src/__tests__/preamble-attr-reactivity.test.ts +217 -0
- package/src/__tests__/preamble-declarations.test.ts +207 -0
- package/src/__tests__/ssr-defaults.test.ts +44 -0
- package/src/adapters/child-scope.ts +22 -0
- package/src/adapters/loop-bound-names.ts +6 -0
- package/src/analyzer-context.ts +43 -0
- package/src/analyzer.ts +33 -3
- package/src/index.ts +1 -0
- package/src/ir-to-client-js/collect-elements.ts +51 -10
- package/src/ir-to-client-js/control-flow/plan/build-branch-loop.ts +1 -1
- package/src/ir-to-client-js/control-flow/plan/build-composite-loop.ts +2 -2
- package/src/ir-to-client-js/control-flow/plan/build-event-delegation.ts +3 -3
- package/src/ir-to-client-js/control-flow/plan/build-inner-loop.ts +2 -2
- package/src/ir-to-client-js/control-flow/plan/build-lazy-row.ts +147 -5
- package/src/ir-to-client-js/control-flow/plan/build-loop.ts +2 -2
- package/src/ir-to-client-js/control-flow/plan/build-reactive-effects.ts +1 -0
- package/src/ir-to-client-js/control-flow/plan/lazy-conditional.ts +151 -0
- package/src/ir-to-client-js/control-flow/plan/lazy-preamble.ts +287 -0
- package/src/ir-to-client-js/control-flow/plan/lazy-row-eligibility.ts +85 -22
- package/src/ir-to-client-js/control-flow/plan/reactive-effects.ts +7 -0
- package/src/ir-to-client-js/control-flow/stringify/lazy-row.ts +133 -35
- package/src/ir-to-client-js/control-flow/stringify/reactive-effects.ts +24 -4
- package/src/ir-to-client-js/csr-substitute.ts +6 -1
- package/src/ir-to-client-js/emit-registration.ts +1 -1
- package/src/ir-to-client-js/html-template.ts +51 -40
- package/src/ir-to-client-js/index.ts +1 -1
- package/src/ir-to-client-js/plan/build-declaration-emit.ts +2 -0
- package/src/ir-to-client-js/plan/build-static-array-child-init.ts +1 -1
- package/src/ir-to-client-js/plan/declaration-emit.ts +6 -0
- package/src/ir-to-client-js/reactivity.ts +25 -0
- package/src/ir-to-client-js/stringify/declaration-emit.ts +9 -4
- package/src/ir-to-client-js/types.ts +12 -0
- package/src/jsx-to-ir.ts +409 -24
- package/src/prop-rewrite.ts +149 -19
- package/src/rich-type-evidence.ts +6 -7
- package/src/ssr-defaults.ts +7 -2
- package/src/to-locale-date-lowering.ts +8 -2
- package/src/types.ts +80 -0
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `.map()` callback preamble → backend-neutral value declarations (#2447).
|
|
3
|
+
*
|
|
4
|
+
* ## What was broken
|
|
5
|
+
*
|
|
6
|
+
* A block-body `.map()` whose preamble computes a value used in the row
|
|
7
|
+
* (`const cls = row.done ? 'done' : 'open'`) carried that preamble only as
|
|
8
|
+
* `segments` — JS text, which a template language cannot execute. Every DSL
|
|
9
|
+
* adapter emitted the row anyway, reading a name it never assigned: ERB read
|
|
10
|
+
* `v[:cls]` (an unseeded vars-Hash key), Go read `$.Cls` (a PARENT-struct
|
|
11
|
+
* field), the rest read a bare undefined local. The class rendered empty, on
|
|
12
|
+
* eight backends, with no diagnostic — the exact silent divergence the
|
|
13
|
+
* sound-or-loud invariant forbids.
|
|
14
|
+
*
|
|
15
|
+
* ## The two halves pinned here
|
|
16
|
+
*
|
|
17
|
+
* 1. **Lowerable → carried.** `MapCallbackPreamble.declarations` holds one
|
|
18
|
+
* `{ name, valueParsed }` per declaration, so each adapter emits it in its
|
|
19
|
+
* own per-row local syntax through the `ParsedExpr` door it already has.
|
|
20
|
+
* 2. **Not lowerable → loud.** Anything that is not a sequence of value
|
|
21
|
+
* declarations leaves `declarations` unset, and a DSL target refuses with
|
|
22
|
+
* the `/* @client *\/` escape rather than emitting a row that reads
|
|
23
|
+
* nothing. A JS runtime keeps running the preamble verbatim either way.
|
|
24
|
+
*
|
|
25
|
+
* All-or-nothing is the point of the shape assertions below: lowering the
|
|
26
|
+
* declarable prefix of a mixed preamble and dropping the rest would be a new
|
|
27
|
+
* silent divergence wearing the fix's clothes.
|
|
28
|
+
*
|
|
29
|
+
* The per-adapter emission is asserted end-to-end by the
|
|
30
|
+
* `loop-preamble-attr-value` conformance fixture (real ERB / Jinja / Twig /
|
|
31
|
+
* Blade / Kolon / Mojo / minijinja / Go renders vs. the JS reference), not
|
|
32
|
+
* here; this file pins the IR contract and the gate.
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
import { describe, test, expect, beforeAll } from 'bun:test'
|
|
36
|
+
import { analyzeComponent } from '../analyzer'
|
|
37
|
+
import { jsxToIR } from '../jsx-to-ir'
|
|
38
|
+
import { compileJSX } from '../compiler'
|
|
39
|
+
import { TestAdapter } from '../adapters/test-adapter'
|
|
40
|
+
import type { IRElement, IRLoop, IRNode } from '../types'
|
|
41
|
+
|
|
42
|
+
const ROWS = (preamble: string, row = `<li key={r.id} class={cls}>{r.label}</li>`) => `
|
|
43
|
+
'use client'
|
|
44
|
+
import { createSignal } from '@barefootjs/client'
|
|
45
|
+
type Row = { id: number; label: string; done: boolean }
|
|
46
|
+
export function Rows(props: { rows: Row[] }) {
|
|
47
|
+
const [rows, setRows] = createSignal<Row[]>(props.rows)
|
|
48
|
+
return (
|
|
49
|
+
<ul>
|
|
50
|
+
{rows().map(r => {
|
|
51
|
+
${preamble}
|
|
52
|
+
return ${row}
|
|
53
|
+
})}
|
|
54
|
+
</ul>
|
|
55
|
+
)
|
|
56
|
+
}
|
|
57
|
+
`
|
|
58
|
+
|
|
59
|
+
function findLoop(node: IRNode | null): IRLoop | null {
|
|
60
|
+
if (!node) return null
|
|
61
|
+
if (node.type === 'loop') return node
|
|
62
|
+
const kids = (node as IRElement).children
|
|
63
|
+
if (!Array.isArray(kids)) return null
|
|
64
|
+
for (const c of kids) {
|
|
65
|
+
const hit = findLoop(c)
|
|
66
|
+
if (hit) return hit
|
|
67
|
+
}
|
|
68
|
+
return null
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function loopOf(source: string): IRLoop {
|
|
72
|
+
const loop = findLoop(jsxToIR(analyzeComponent(source, 'Rows.tsx')))
|
|
73
|
+
if (!loop) throw new Error('no loop in IR')
|
|
74
|
+
return loop
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Compile against a DSL-tier adapter (one that cannot run a callback body). */
|
|
78
|
+
function dslErrors(source: string): string[] {
|
|
79
|
+
const adapter = new TestAdapter()
|
|
80
|
+
adapter.acceptsCallbackBody = () => false
|
|
81
|
+
return compileJSX(source, 'Rows.tsx', { adapter })
|
|
82
|
+
.errors.filter(e => e.severity !== 'warning')
|
|
83
|
+
.map(e => `${e.code}: ${e.message}`)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// The first `analyzeComponent` in a process pays the TS program setup, which
|
|
87
|
+
// alone exceeds the default per-test timeout. Pay it once, outside a test.
|
|
88
|
+
beforeAll(() => {
|
|
89
|
+
analyzeComponent(ROWS(`const warm = r.label`), 'Warmup.tsx')
|
|
90
|
+
}, 60_000)
|
|
91
|
+
|
|
92
|
+
describe('preamble value declarations — carried as neutral IR', () => {
|
|
93
|
+
test('a single ternary declaration', () => {
|
|
94
|
+
const loop = loopOf(ROWS(`const cls = r.done ? 'done' : 'open'`))
|
|
95
|
+
expect(loop.preamble?.declarations).toEqual([
|
|
96
|
+
expect.objectContaining({ name: 'cls', raw: `r.done ? 'done' : 'open'` }),
|
|
97
|
+
])
|
|
98
|
+
expect(loop.preamble?.declarations?.[0].valueParsed.kind).toBe('conditional')
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
test('declarations keep SOURCE order, so a later one can read an earlier one', () => {
|
|
102
|
+
const loop = loopOf(`
|
|
103
|
+
'use client'
|
|
104
|
+
import { createSignal } from '@barefootjs/client'
|
|
105
|
+
type Row = { id: number; label: string; done: boolean }
|
|
106
|
+
export function Rows(props: { rows: Row[] }) {
|
|
107
|
+
const [rows, setRows] = createSignal<Row[]>(props.rows)
|
|
108
|
+
return (
|
|
109
|
+
<ul>
|
|
110
|
+
{rows().map(r => {
|
|
111
|
+
const base = r.done ? 'done' : 'open'
|
|
112
|
+
const cls = base + ' row'
|
|
113
|
+
return <li key={r.id} class={cls}>{r.label}</li>
|
|
114
|
+
})}
|
|
115
|
+
</ul>
|
|
116
|
+
)
|
|
117
|
+
}
|
|
118
|
+
`)
|
|
119
|
+
expect(loop.preamble?.declarations?.map(d => d.name)).toEqual(['base', 'cls'])
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
test('several declarators in one statement each become a declaration', () => {
|
|
123
|
+
const loop = loopOf(ROWS(`const a = r.label, b = r.id`))
|
|
124
|
+
expect(loop.preamble?.declarations?.map(d => d.name)).toEqual(['a', 'b'])
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
test('the names are loop-bound for the body, like a loop param', async () => {
|
|
128
|
+
// `collectLoopBoundNames` is what stops a same-named module const from
|
|
129
|
+
// inlining over the local the loop just declared.
|
|
130
|
+
const { collectLoopBoundNames } = await import('../adapters/loop-bound-names.ts')
|
|
131
|
+
const ctx = analyzeComponent(ROWS(`const cls = r.done ? 'done' : 'open'`), 'Rows.tsx')
|
|
132
|
+
const root = jsxToIR(ctx)
|
|
133
|
+
expect(root).not.toBeNull()
|
|
134
|
+
const names = collectLoopBoundNames({ ...ctx.componentIR!, root: root! })
|
|
135
|
+
expect(names.has('cls')).toBe(true)
|
|
136
|
+
})
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
describe('preamble value declarations — all-or-nothing', () => {
|
|
140
|
+
// Each of these has SOMETHING declarable in it. Carrying just that part and
|
|
141
|
+
// dropping the rest would put the missing statement's effect nowhere, with
|
|
142
|
+
// no diagnostic — so the whole preamble must decline.
|
|
143
|
+
const notDeclarable: Array<[string, string]> = [
|
|
144
|
+
['an assignment after a declaration', `let n = 0\n n = r.id`],
|
|
145
|
+
['an imperative loop', `const out = []\n for (const c of r.label) out.push(c)`],
|
|
146
|
+
['a destructuring binding', `const { label } = r`],
|
|
147
|
+
['a declaration with no initializer', `let cls\n const other = r.label`],
|
|
148
|
+
[
|
|
149
|
+
'an initializer the expression subset cannot model',
|
|
150
|
+
`const cls = (() => r.done)()`,
|
|
151
|
+
],
|
|
152
|
+
]
|
|
153
|
+
|
|
154
|
+
for (const [name, preamble] of notDeclarable) {
|
|
155
|
+
test(name, () => {
|
|
156
|
+
const loop = loopOf(ROWS(preamble, `<li key={r.id}>{r.label}</li>`))
|
|
157
|
+
expect(loop.preamble).toBeDefined()
|
|
158
|
+
expect(loop.preamble?.declarations).toBeUndefined()
|
|
159
|
+
})
|
|
160
|
+
}
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
describe('the DSL gate — loud, not an empty attribute', () => {
|
|
164
|
+
test('a declarable preamble compiles clean on a DSL target', () => {
|
|
165
|
+
expect(dslErrors(ROWS(`const cls = r.done ? 'done' : 'open'`))).toEqual([])
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
test('a non-declarable preamble refuses, with the /* @client */ escape', () => {
|
|
169
|
+
const errors = dslErrors(
|
|
170
|
+
ROWS(`let n = 0\n n = r.id`, `<li key={r.id}>{r.label}</li>`),
|
|
171
|
+
)
|
|
172
|
+
expect(errors.length).toBeGreaterThan(0)
|
|
173
|
+
expect(errors[0]).toMatch(/not a sequence of value declarations/)
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
test('a JS runtime runs the same preamble verbatim — no refusal', () => {
|
|
177
|
+
// The whole point of the per-backend fidelity model: the price is paid by
|
|
178
|
+
// the backend that genuinely cannot express the shape.
|
|
179
|
+
const r = compileJSX(
|
|
180
|
+
ROWS(`let n = 0\n n = r.id`, `<li key={r.id}>{r.label}</li>`),
|
|
181
|
+
'Rows.tsx',
|
|
182
|
+
{ adapter: new TestAdapter() },
|
|
183
|
+
)
|
|
184
|
+
expect(r.errors.filter(e => e.severity !== 'warning')).toEqual([])
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
test('/* @client */ silences the DSL refusal', () => {
|
|
188
|
+
const source = `
|
|
189
|
+
'use client'
|
|
190
|
+
import { createSignal } from '@barefootjs/client'
|
|
191
|
+
type Row = { id: number; label: string; done: boolean }
|
|
192
|
+
export function Rows(props: { rows: Row[] }) {
|
|
193
|
+
const [rows, setRows] = createSignal<Row[]>(props.rows)
|
|
194
|
+
return (
|
|
195
|
+
<ul>
|
|
196
|
+
{/* @client */ rows().map(r => {
|
|
197
|
+
let n = 0
|
|
198
|
+
n = r.id
|
|
199
|
+
return <li key={r.id}>{r.label}</li>
|
|
200
|
+
})}
|
|
201
|
+
</ul>
|
|
202
|
+
)
|
|
203
|
+
}
|
|
204
|
+
`
|
|
205
|
+
expect(dslErrors(source)).toEqual([])
|
|
206
|
+
})
|
|
207
|
+
})
|
|
@@ -102,6 +102,50 @@ describe('extractSsrDefaults', () => {
|
|
|
102
102
|
expect(defaults?.n).toEqual({ value: 0 })
|
|
103
103
|
})
|
|
104
104
|
|
|
105
|
+
test('aliased destructured prop (#2460): `propName` is the CALLER-facing key, not the local binding', () => {
|
|
106
|
+
// `{ n: count }` — the caller passes `n`, the template variable (and
|
|
107
|
+
// stash entry key) is the local binding `count`. The manifest
|
|
108
|
+
// consumer (`_derive_stash_from_defaults` in the Perl runtime) reads
|
|
109
|
+
// the caller's props by `propName`, so `propName` must be `n`, not
|
|
110
|
+
// `count` — using the local name here silently drops the caller's
|
|
111
|
+
// value (the stash entry falls back to the static `value`, `null`).
|
|
112
|
+
const metadata = metadataFor(`
|
|
113
|
+
function Badge({ text, n: count }: { text: string; n: number }) {
|
|
114
|
+
return <span>{text}:{count}</span>
|
|
115
|
+
}
|
|
116
|
+
`)
|
|
117
|
+
|
|
118
|
+
const defaults = extractSsrDefaults(metadata)
|
|
119
|
+
expect(defaults).toBeDefined()
|
|
120
|
+
// Keyed by the LOCAL binding (the template variable name)...
|
|
121
|
+
expect(defaults?.count).toEqual({ propName: 'n', value: null })
|
|
122
|
+
// ...but `propName` is the CALLER-facing key.
|
|
123
|
+
expect(defaults?.text).toEqual({ propName: 'text', value: null })
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
test('aliased destructured prop with a default (#2460): `propName` still resolves to the source key', () => {
|
|
127
|
+
const metadata = metadataFor(`
|
|
128
|
+
function Badge({ n: count = 7 }: { n?: number }) {
|
|
129
|
+
return <span>{count}</span>
|
|
130
|
+
}
|
|
131
|
+
`)
|
|
132
|
+
|
|
133
|
+
const defaults = extractSsrDefaults(metadata)
|
|
134
|
+
expect(defaults?.count).toEqual({ propName: 'n', value: 7 })
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
test('un-aliased destructured prop (regression): `propName` equals the local name', () => {
|
|
138
|
+
const metadata = metadataFor(`
|
|
139
|
+
function Badge({ text, n }: { text: string; n: number }) {
|
|
140
|
+
return <span>{text}:{n}</span>
|
|
141
|
+
}
|
|
142
|
+
`)
|
|
143
|
+
|
|
144
|
+
const defaults = extractSsrDefaults(metadata)
|
|
145
|
+
expect(defaults?.n).toEqual({ propName: 'n', value: null })
|
|
146
|
+
expect(defaults?.text).toEqual({ propName: 'text', value: null })
|
|
147
|
+
})
|
|
148
|
+
|
|
105
149
|
test('memo derived from a signal evaluates through the chain', () => {
|
|
106
150
|
const metadata = metadataFor(`
|
|
107
151
|
'use client'
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether a child component derives its `bf-s` scope id from its parent
|
|
3
|
+
* scope + mount slot (`${parentScopeId}_${slotId}`) rather than getting a
|
|
4
|
+
* freshly randomized one (`${name}_${random}`).
|
|
5
|
+
*
|
|
6
|
+
* A component derives from its slot unless it is the DIRECT root of a loop
|
|
7
|
+
* row (`IRComponent.loopItemRoot`, set once in `jsx-to-ir.ts`'s loop
|
|
8
|
+
* builder) — a row root owns its own per-row identity, matching Hono's
|
|
9
|
+
* reference behaviour (#2444). Every backend (Hono, each DSL adapter, and
|
|
10
|
+
* both CSR template emitters) MUST consult this single predicate instead of
|
|
11
|
+
* re-deriving the fact from a mutable "am I inside a loop" flag, which is
|
|
12
|
+
* what caused #2444: such flags stay true for an entire loop subtree and
|
|
13
|
+
* can't distinguish a row root from a component nested below it.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import type { IRComponent } from '../types.ts'
|
|
17
|
+
|
|
18
|
+
export function derivesScopeFromSlot(
|
|
19
|
+
comp: Pick<IRComponent, 'slotId' | 'loopItemRoot'>
|
|
20
|
+
): boolean {
|
|
21
|
+
return comp.slotId != null && comp.loopItemRoot !== true
|
|
22
|
+
}
|
|
@@ -50,6 +50,12 @@ export function collectLoopBoundNames(ir: ComponentIR): Set<string> {
|
|
|
50
50
|
// param (which may differ from the map callback's `param`) before
|
|
51
51
|
// any rename to the loop param happens.
|
|
52
52
|
if (node.filterPredicate) names.add(node.filterPredicate.param)
|
|
53
|
+
// A `.map()` callback preamble's declarations lower to per-row locals
|
|
54
|
+
// in the loop body on every DSL adapter (#2447), so a preamble name is
|
|
55
|
+
// bound in body scope exactly like a param — and must get the same
|
|
56
|
+
// exclusion, or a same-named module const would inline over the local
|
|
57
|
+
// the loop just declared.
|
|
58
|
+
for (const name of node.preamble?.declaredNames ?? []) names.add(name)
|
|
53
59
|
for (const child of node.children) visit(child)
|
|
54
60
|
if (node.childComponent) {
|
|
55
61
|
for (const child of node.childComponent.children) visit(child)
|
package/src/analyzer-context.ts
CHANGED
|
@@ -448,6 +448,49 @@ export function typeNodeToTypeInfo(
|
|
|
448
448
|
return { kind: 'array', raw, elementType: recurse(typeNode.elementType) }
|
|
449
449
|
}
|
|
450
450
|
|
|
451
|
+
// Literal types (`'a'`, `42`, `-1`, `true`, `null`) — including as union
|
|
452
|
+
// members, which is where the absence of this arm hurt: `'a' | 'b'`
|
|
453
|
+
// members fell to `{kind:'unknown', raw:"'a'"}`, forcing every consumer
|
|
454
|
+
// that needed the member's family or value to re-parse `raw` with a
|
|
455
|
+
// regex (the Go adapter's literal-union collapse, #2324's
|
|
456
|
+
// `unionMemberLiteral`, `rich-type-evidence`'s nullish-arm match — all
|
|
457
|
+
// three carried one). A literal is structurally a primitive of its
|
|
458
|
+
// family carrying its value; consumers that only care about the family
|
|
459
|
+
// see a plain primitive, and the value rides along in `literalValue`
|
|
460
|
+
// for the ones that need it. `null` inside a union arrives as
|
|
461
|
+
// `LiteralTypeNode(NullKeyword)` — NOT the bare `NullKeyword` the
|
|
462
|
+
// switch above catches — which is exactly why nullish union arms were
|
|
463
|
+
// invisible before.
|
|
464
|
+
if (ts.isLiteralTypeNode(typeNode)) {
|
|
465
|
+
const lit = typeNode.literal
|
|
466
|
+
if (ts.isStringLiteral(lit) || ts.isNoSubstitutionTemplateLiteral(lit)) {
|
|
467
|
+
return { kind: 'primitive', raw, primitive: 'string', literalValue: lit.text }
|
|
468
|
+
}
|
|
469
|
+
if (ts.isNumericLiteral(lit)) {
|
|
470
|
+
return { kind: 'primitive', raw, primitive: 'number', literalValue: lit.text }
|
|
471
|
+
}
|
|
472
|
+
if (
|
|
473
|
+
ts.isPrefixUnaryExpression(lit) &&
|
|
474
|
+
lit.operator === ts.SyntaxKind.MinusToken &&
|
|
475
|
+
ts.isNumericLiteral(lit.operand)
|
|
476
|
+
) {
|
|
477
|
+
return { kind: 'primitive', raw, primitive: 'number', literalValue: `-${lit.operand.text}` }
|
|
478
|
+
}
|
|
479
|
+
if (lit.kind === ts.SyntaxKind.TrueKeyword || lit.kind === ts.SyntaxKind.FalseKeyword) {
|
|
480
|
+
return {
|
|
481
|
+
kind: 'primitive',
|
|
482
|
+
raw,
|
|
483
|
+
primitive: 'boolean',
|
|
484
|
+
literalValue: lit.kind === ts.SyntaxKind.TrueKeyword ? 'true' : 'false',
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
if (lit.kind === ts.SyntaxKind.NullKeyword) {
|
|
488
|
+
return { kind: 'primitive', raw, primitive: 'null' }
|
|
489
|
+
}
|
|
490
|
+
// BigInt and anything newer: opaque, as before.
|
|
491
|
+
return { kind: 'unknown', raw }
|
|
492
|
+
}
|
|
493
|
+
|
|
451
494
|
// Union types
|
|
452
495
|
if (ts.isUnionTypeNode(typeNode)) {
|
|
453
496
|
return { kind: 'union', raw, unionTypes: typeNode.types.map(recurse) }
|
package/src/analyzer.ts
CHANGED
|
@@ -1201,10 +1201,21 @@ function collectSignal(node: ts.VariableDeclaration, ctx: AnalyzerContext): void
|
|
|
1201
1201
|
const callExpr = node.initializer as ts.CallExpression
|
|
1202
1202
|
|
|
1203
1203
|
const elements = pattern.elements
|
|
1204
|
+
// Getter-elided form: `const [, setActive] = createSignal(0)`. The first
|
|
1205
|
+
// element is a hole (ts.OmittedExpression), not a BindingElement.
|
|
1206
|
+
// Rejecting it here used to drop the whole declaration from the emitted
|
|
1207
|
+
// init while the setter stayed referenced by handlers — a guaranteed
|
|
1208
|
+
// ReferenceError (adapter-tests scope-gate, #2468 sweep). Accept it,
|
|
1209
|
+
// synthesize an internal getter name for the IR (never referenced by any
|
|
1210
|
+
// expression, so substitution/SSR consumers see an unused entry), and
|
|
1211
|
+
// mark `getterElided` so emit reproduces the source's `[, setter]` shape.
|
|
1212
|
+
const getterElided = elements.length === 2 && ts.isOmittedExpression(elements[0])
|
|
1204
1213
|
if (
|
|
1205
1214
|
elements.length < 1 || elements.length > 2 ||
|
|
1206
|
-
!
|
|
1207
|
-
|
|
1215
|
+
(!getterElided && (
|
|
1216
|
+
!ts.isBindingElement(elements[0]) ||
|
|
1217
|
+
!ts.isIdentifier(elements[0].name)
|
|
1218
|
+
))
|
|
1208
1219
|
) {
|
|
1209
1220
|
return
|
|
1210
1221
|
}
|
|
@@ -1216,10 +1227,11 @@ function collectSignal(node: ts.VariableDeclaration, ctx: AnalyzerContext): void
|
|
|
1216
1227
|
return
|
|
1217
1228
|
}
|
|
1218
1229
|
|
|
1219
|
-
const getter = elements[0].name.text
|
|
1220
1230
|
const setter = elements.length === 2 && ts.isBindingElement(elements[1]) && ts.isIdentifier(elements[1].name)
|
|
1221
1231
|
? elements[1].name.text
|
|
1222
1232
|
: null
|
|
1233
|
+
if (getterElided && !setter) return
|
|
1234
|
+
const getter = getterElided ? `__bfGet_${setter}` : (elements[0] as ts.BindingElement & { name: ts.Identifier }).name.text
|
|
1223
1235
|
const initialValue = callExpr.arguments[0] ? ctx.getJS(callExpr.arguments[0]) : ''
|
|
1224
1236
|
const typedInitialValue = callExpr.arguments[0] ? callExpr.arguments[0].getText(ctx.sourceFile) : undefined
|
|
1225
1237
|
|
|
@@ -1257,6 +1269,7 @@ function collectSignal(node: ts.VariableDeclaration, ctx: AnalyzerContext): void
|
|
|
1257
1269
|
ctx.signals.push({
|
|
1258
1270
|
getter,
|
|
1259
1271
|
setter,
|
|
1272
|
+
getterElided: getterElided || undefined,
|
|
1260
1273
|
initialValue,
|
|
1261
1274
|
typedInitialValue: typedInitialValue !== initialValue ? typedInitialValue : undefined,
|
|
1262
1275
|
templateInitialValue,
|
|
@@ -1577,9 +1590,26 @@ function collectMemo(node: ts.VariableDeclaration, ctx: AnalyzerContext): void {
|
|
|
1577
1590
|
const parsedBlockComplete =
|
|
1578
1591
|
parsedBlock && blockBody ? parsedBlock.length === blockBody.statements.length : undefined
|
|
1579
1592
|
|
|
1593
|
+
// Destructured-arg components only — the memo twin of the signal
|
|
1594
|
+
// `templateInitialValue` rewrite (#2265) a few hundred lines up: a memo
|
|
1595
|
+
// body referencing a bare destructured prop (`createMemo(() => value *
|
|
1596
|
+
// 10)` with `{ value }: { value: number }`) is inlined into the CSR
|
|
1597
|
+
// `template:` arrow by `buildSignalMemoEnv`, and that arrow runs at
|
|
1598
|
+
// module scope — it is not a closure over `initXxx`'s `const value =
|
|
1599
|
+
// _p.value` extraction, so the bare ref must become `_p.value`
|
|
1600
|
+
// (adapter-tests scope-gate, #2468 sweep).
|
|
1601
|
+
let templateComputation: string | undefined
|
|
1602
|
+
if (!ctx.propsObjectName && callExpr.arguments[0]) {
|
|
1603
|
+
const propNames = new Set(ctx.propsParams.map(p => p.name))
|
|
1604
|
+
if (propNames.size > 0) {
|
|
1605
|
+
templateComputation = rewriteBarePropRefs(computation, callExpr.arguments[0], propNames)
|
|
1606
|
+
}
|
|
1607
|
+
}
|
|
1608
|
+
|
|
1580
1609
|
ctx.memos.push({
|
|
1581
1610
|
name,
|
|
1582
1611
|
computation,
|
|
1612
|
+
templateComputation,
|
|
1583
1613
|
parsedBlock,
|
|
1584
1614
|
parsedBlockComplete,
|
|
1585
1615
|
typedComputation: typedComputation !== computation ? typedComputation : undefined,
|
package/src/index.ts
CHANGED
|
@@ -90,6 +90,7 @@ export { rewriteImportsForTemplate } from './adapters/template-imports.ts'
|
|
|
90
90
|
export { emitParsedExpr, groupBinaryOperand, isStringTypedOperand, isStringConcatBinary } from './adapters/parsed-expr-emitter.ts'
|
|
91
91
|
export type { ParsedExprEmitter, HigherOrderMethod, ArrayMethod, SortMethod, LiteralType } from './adapters/parsed-expr-emitter.ts'
|
|
92
92
|
export { collectLoopBoundNames } from './adapters/loop-bound-names.ts'
|
|
93
|
+
export { derivesScopeFromSlot } from './adapters/child-scope.ts'
|
|
93
94
|
export { evaluateSignalInit, tryEvaluateSignalInit, type SignalInitEvalResult } from './signal-init-eval.ts'
|
|
94
95
|
export { evaluateStaticLiteral, isFullyStaticLiteral, resolveStaticLoopSource } from './static-literal.ts'
|
|
95
96
|
export { importsSearchParams, searchParamsLocalNames, envSignalLocalNames, envSignalReaderFor, ENV_SIGNAL_READERS, queryHrefLocalNames, formatDateLocalNames, matchSearchParamsMethodCall } from './adapters/env-signal.ts'
|
|
@@ -328,10 +328,12 @@ export function collectInnerLoops(
|
|
|
328
328
|
// `NestedLoop` because event delegation handles them through
|
|
329
329
|
// the parent's bindings instead.
|
|
330
330
|
const bindings: LoopChildBindings = emptyLoopChildBindings()
|
|
331
|
+
// Hoisted: one Set per loop, not one per child (Copilot review).
|
|
332
|
+
const innerPreambleNames = preambleNamesOf(n)
|
|
331
333
|
if (ctx) {
|
|
332
334
|
for (const child of n.children) {
|
|
333
335
|
bindings.reactiveTexts.push(...collectLoopChildReactiveTexts(child, ctx, n.param, n.paramBindings))
|
|
334
|
-
bindings.reactiveAttrs.push(...collectLoopChildReactiveAttrs(child, ctx, n.param, n.paramBindings))
|
|
336
|
+
bindings.reactiveAttrs.push(...collectLoopChildReactiveAttrs(child, ctx, n.param, n.paramBindings, false, innerPreambleNames))
|
|
335
337
|
bindings.refs.push(...collectLoopChildRefs(child))
|
|
336
338
|
}
|
|
337
339
|
}
|
|
@@ -669,7 +671,7 @@ export function collectElements(
|
|
|
669
671
|
const childHandlers: string[] = []
|
|
670
672
|
const bindings = projectionInner
|
|
671
673
|
? emptyLoopChildBindings()
|
|
672
|
-
: collectLoopChildBindings(l.children, ctx, siblingOffsets, l.param, l.paramBindings)
|
|
674
|
+
: collectLoopChildBindings(l.children, ctx, siblingOffsets, l.param, l.paramBindings, preambleNamesOf(l))
|
|
673
675
|
if (!projectionInner) {
|
|
674
676
|
for (const child of l.children) {
|
|
675
677
|
childHandlers.push(...collectEventHandlersFromIR(child))
|
|
@@ -708,10 +710,12 @@ export function collectElements(
|
|
|
708
710
|
// Loop-param refs use the raw destructured binding (no loopParams
|
|
709
711
|
// passed), matching the plain-element path's `staticItemTemplate`.
|
|
710
712
|
if (l.isStaticArray && l.children[0]) {
|
|
711
|
-
// `
|
|
712
|
-
//
|
|
713
|
-
//
|
|
714
|
-
|
|
713
|
+
// `l.children[0]` is the loop's row-root component — already
|
|
714
|
+
// tagged `loopItemRoot` in the IR, so `derivesScopeFromSlot`
|
|
715
|
+
// drops the parent's slot suffix automatically. Each iteration
|
|
716
|
+
// owns a distinct scope identified by `data-key`, mirroring the
|
|
717
|
+
// SSR template's renderChild emit.
|
|
718
|
+
staticItemTemplate = irToHtmlTemplate(l.children[0], buildRestSpreadNames(ctx), 0, undefined, undefined)
|
|
715
719
|
}
|
|
716
720
|
} else if (l.children[0] && !projectionInner) {
|
|
717
721
|
// Pass loopParams so expressions are wrapped at generation time,
|
|
@@ -728,8 +732,8 @@ export function collectElements(
|
|
|
728
732
|
// accessor wrap so the CSR materialize fallback (#1247) can clone
|
|
729
733
|
// items inside the forEach body without rewriting the template.
|
|
730
734
|
if (l.isStaticArray) {
|
|
731
|
-
// Plain-element body:
|
|
732
|
-
//
|
|
735
|
+
// Plain-element body: nested component nodes are never tagged
|
|
736
|
+
// `loopItemRoot`, so they keep their parent-slot context in the
|
|
733
737
|
// per-iteration renderChild call. Per #1249, the
|
|
734
738
|
// `outer-nested` static-array-child-init plan addresses
|
|
735
739
|
// children by the (bf-h, bf-m) pair against the enclosing
|
|
@@ -1139,7 +1143,7 @@ function collectBranchLoops(
|
|
|
1139
1143
|
// which caused reactive reads inside simple loop bodies to silently
|
|
1140
1144
|
// no-op for existing items.
|
|
1141
1145
|
const branchBindings = ctx && !projectionInner
|
|
1142
|
-
? collectLoopChildBindings(n.children, ctx, siblingOffsets, n.param, n.paramBindings)
|
|
1146
|
+
? collectLoopChildBindings(n.children, ctx, siblingOffsets, n.param, n.paramBindings, preambleNamesOf(n))
|
|
1143
1147
|
: emptyLoopChildBindings()
|
|
1144
1148
|
|
|
1145
1149
|
loops.push({
|
|
@@ -1296,12 +1300,49 @@ function collectBranchConditionals(
|
|
|
1296
1300
|
* resolve child slot ids inside nested loops; the rest of the per-item
|
|
1297
1301
|
* collectors don't read them but pass them through for symmetry.
|
|
1298
1302
|
*/
|
|
1303
|
+
/**
|
|
1304
|
+
* The names a loop's `.map()` callback preamble declares, or `undefined` when
|
|
1305
|
+
* there are none to consider — the input to the attribute classifier's
|
|
1306
|
+
* preamble-local check (#2447 follow-up). `undefined` rather than an empty set
|
|
1307
|
+
* so the classifier's fast path stays a single `!== undefined` for the
|
|
1308
|
+
* overwhelmingly common preamble-free loop.
|
|
1309
|
+
*
|
|
1310
|
+
* A STATIC-ARRAY loop returns `undefined` even when it has a preamble, and
|
|
1311
|
+
* this is load-bearing rather than an optimisation. Its items are emitted
|
|
1312
|
+
* through `static-array-child-init` — a `forEach` over the SSR-rendered
|
|
1313
|
+
* children, with no per-row body and therefore no preamble in scope. Wiring an
|
|
1314
|
+
* attribute there would emit `setAttribute('class', active ? … : …)` against a
|
|
1315
|
+
* free variable: a ReferenceError at init that takes the whole component's
|
|
1316
|
+
* hydration with it, including any nested child. (Caught exactly that way —
|
|
1317
|
+
* the gallery nav shells are static arrays whose rows read
|
|
1318
|
+
* `const active = item.key === currentRoute`, and the badge components mounted
|
|
1319
|
+
* inside them stopped appearing.)
|
|
1320
|
+
*
|
|
1321
|
+
* Nothing is lost by declining: a static array's items are rendered once at
|
|
1322
|
+
* SSR and never recreated from a signal, so there is no same-key update for a
|
|
1323
|
+
* patch to be missing from. The same reasoning gates `markPreambleAttrSlots`
|
|
1324
|
+
* and `preambleRegions` in Phase 1, and the two gates must agree — a slot
|
|
1325
|
+
* granted without wiring, or wiring without a slot, is a silent hole either
|
|
1326
|
+
* way.
|
|
1327
|
+
*/
|
|
1328
|
+
function preambleNamesOf(loop: IRLoop): ReadonlySet<string> | undefined {
|
|
1329
|
+
if (loop.isStaticArray) return undefined
|
|
1330
|
+
const declared = loop.preamble?.declaredNames
|
|
1331
|
+
return declared && declared.length > 0 ? new Set(declared) : undefined
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1299
1334
|
export function collectLoopChildBindings(
|
|
1300
1335
|
children: readonly IRNode[],
|
|
1301
1336
|
ctx: ClientJsContext,
|
|
1302
1337
|
siblingOffsets: Map<IRLoop, IRNode[]>,
|
|
1303
1338
|
loopParam: string,
|
|
1304
1339
|
loopParamBindings: readonly import('../types.ts').LoopParamBinding[] | undefined,
|
|
1340
|
+
/**
|
|
1341
|
+
* Names the loop's `.map()` callback preamble declares, so an attribute
|
|
1342
|
+
* reading one is classified reactive (#2447 follow-up) — see
|
|
1343
|
+
* `collectLoopChildReactiveAttrs`. Omitted for a loop with no preamble.
|
|
1344
|
+
*/
|
|
1345
|
+
preambleNames?: ReadonlySet<string>,
|
|
1305
1346
|
): LoopChildBindings {
|
|
1306
1347
|
const bindings = emptyLoopChildBindings()
|
|
1307
1348
|
for (const child of children) {
|
|
@@ -1311,7 +1352,7 @@ export function collectLoopChildBindings(
|
|
|
1311
1352
|
// `collectLoopChildConditionals`, which gives each its own insert() +
|
|
1312
1353
|
// arm-scoped attrs/texts (`LoopChildBranchSummary.reactiveAttrs` /
|
|
1313
1354
|
// `.reactiveTexts`) — descending into them here too would double-bind.
|
|
1314
|
-
bindings.reactiveAttrs.push(...collectLoopChildReactiveAttrs(child, ctx, loopParam, loopParamBindings, true))
|
|
1355
|
+
bindings.reactiveAttrs.push(...collectLoopChildReactiveAttrs(child, ctx, loopParam, loopParamBindings, true, preambleNames))
|
|
1315
1356
|
bindings.reactiveTexts.push(...collectLoopChildReactiveTexts(child, ctx, loopParam, loopParamBindings, true))
|
|
1316
1357
|
bindings.refs.push(...collectLoopChildRefs(child))
|
|
1317
1358
|
bindings.conditionals.push(...collectLoopChildConditionals(child, ctx, siblingOffsets, loopParam, loopParamBindings))
|
|
@@ -58,7 +58,7 @@ export function buildBranchLoopPlan(
|
|
|
58
58
|
const mapPreambleWrapped = loop.preamble
|
|
59
59
|
? renderPreamble(loop.preamble, {
|
|
60
60
|
transformJs: (t) => wrapLoopParamAsAccessor(t, loop.param, loop.paramBindings),
|
|
61
|
-
renderLeaf: (ir) => irToHtmlTemplate(ir, undefined, 1, [{ param: loop.param, bindings: loop.paramBindings }], undefined
|
|
61
|
+
renderLeaf: (ir) => irToHtmlTemplate(ir, undefined, 1, [{ param: loop.param, bindings: loop.paramBindings }], undefined),
|
|
62
62
|
})
|
|
63
63
|
: ''
|
|
64
64
|
const preambleRegions = buildPreambleRegionPlans(loop.preambleRegions, loop.param, loop.paramBindings)
|
|
@@ -48,7 +48,7 @@ export function buildTopLevelCompositePlan(elem: TopLevelLoop, profileComponentN
|
|
|
48
48
|
mapPreambleWrapped: elem.preamble
|
|
49
49
|
? renderPreamble(elem.preamble, {
|
|
50
50
|
transformJs: wrap,
|
|
51
|
-
renderLeaf: (ir) => irToHtmlTemplate(ir, undefined, 1, [{ param: elem.param, bindings: elem.paramBindings }], undefined
|
|
51
|
+
renderLeaf: (ir) => irToHtmlTemplate(ir, undefined, 1, [{ param: elem.param, bindings: elem.paramBindings }], undefined),
|
|
52
52
|
})
|
|
53
53
|
: '',
|
|
54
54
|
template: elem.template,
|
|
@@ -108,7 +108,7 @@ export function buildBranchCompositePlan(loop: BranchLoop, cv: string, profileCo
|
|
|
108
108
|
mapPreambleWrapped: loop.preamble
|
|
109
109
|
? renderPreamble(loop.preamble, {
|
|
110
110
|
transformJs: wrap,
|
|
111
|
-
renderLeaf: (ir) => irToHtmlTemplate(ir, undefined, 1, [{ param: loop.param, bindings: loop.paramBindings }], undefined
|
|
111
|
+
renderLeaf: (ir) => irToHtmlTemplate(ir, undefined, 1, [{ param: loop.param, bindings: loop.paramBindings }], undefined),
|
|
112
112
|
})
|
|
113
113
|
: '',
|
|
114
114
|
template: loop.template,
|
|
@@ -44,7 +44,7 @@ export function buildDynamicLoopDelegationPlan(
|
|
|
44
44
|
// which throws since `t` is a plain object in this scope.
|
|
45
45
|
mapPreamble: elem.preamble
|
|
46
46
|
? renderPreamble(elem.preamble, {
|
|
47
|
-
renderLeaf: (ir) => irToHtmlTemplate(ir, undefined, 1, undefined, undefined
|
|
47
|
+
renderLeaf: (ir) => irToHtmlTemplate(ir, undefined, 1, undefined, undefined),
|
|
48
48
|
})
|
|
49
49
|
: null,
|
|
50
50
|
mapPreambleDeclaredNames: elem.preamble?.declaredNames ?? [],
|
|
@@ -76,7 +76,7 @@ export function buildBranchLoopDelegationPlan(
|
|
|
76
76
|
// loopParams spec — leaf refs must stay in plain-object form here.
|
|
77
77
|
mapPreamble: loop.preamble
|
|
78
78
|
? renderPreamble(loop.preamble, {
|
|
79
|
-
renderLeaf: (ir) => irToHtmlTemplate(ir, undefined, 1, undefined, undefined
|
|
79
|
+
renderLeaf: (ir) => irToHtmlTemplate(ir, undefined, 1, undefined, undefined),
|
|
80
80
|
})
|
|
81
81
|
: null,
|
|
82
82
|
mapPreambleDeclaredNames: loop.preamble?.declaredNames ?? [],
|
|
@@ -109,7 +109,7 @@ export function buildStaticArrayDelegationPlan(
|
|
|
109
109
|
// loopParams spec — leaf refs must stay in plain-object form here.
|
|
110
110
|
mapPreamble: elem.preamble
|
|
111
111
|
? renderPreamble(elem.preamble, {
|
|
112
|
-
renderLeaf: (ir) => irToHtmlTemplate(ir, undefined, 1, undefined, undefined
|
|
112
|
+
renderLeaf: (ir) => irToHtmlTemplate(ir, undefined, 1, undefined, undefined),
|
|
113
113
|
})
|
|
114
114
|
: null,
|
|
115
115
|
mapPreambleDeclaredNames: elem.preamble?.declaredNames ?? [],
|
|
@@ -243,7 +243,7 @@ function buildReactiveEmit(
|
|
|
243
243
|
: [{ param: inner.param, bindings: inner.paramBindings }]
|
|
244
244
|
preludeStatements.push(renderPreamble(inner.preamble, {
|
|
245
245
|
transformJs: (t) => wrapInner(wrapOuter(t)),
|
|
246
|
-
renderLeaf: (ir) => irToHtmlTemplate(ir, undefined, 1, leafLoopParams, undefined
|
|
246
|
+
renderLeaf: (ir) => irToHtmlTemplate(ir, undefined, 1, leafLoopParams, undefined),
|
|
247
247
|
}))
|
|
248
248
|
}
|
|
249
249
|
|
|
@@ -286,7 +286,7 @@ function buildStaticEmit(inner: NestedLoop, level: DepthLevel, uidSuffix: string
|
|
|
286
286
|
// of the js segments above).
|
|
287
287
|
if (inner.preamble) {
|
|
288
288
|
preludeStatements.push(renderPreamble(inner.preamble, {
|
|
289
|
-
renderLeaf: (ir) => irToHtmlTemplate(ir, undefined, 1, undefined, undefined
|
|
289
|
+
renderLeaf: (ir) => irToHtmlTemplate(ir, undefined, 1, undefined, undefined),
|
|
290
290
|
}))
|
|
291
291
|
}
|
|
292
292
|
return {
|