@barefootjs/jsx 0.33.6 → 0.35.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/adapters/parsed-expr-emitter.d.ts +17 -0
- package/dist/adapters/parsed-expr-emitter.d.ts.map +1 -1
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/expression-parser.d.ts +17 -4
- package/dist/expression-parser.d.ts.map +1 -1
- package/dist/index.d.ts +4 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +445 -282
- package/dist/ir-to-client-js/collect-elements.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/build-branch-loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/build-component-loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/build-composite-loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/build-lazy-row.d.ts +11 -0
- 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-loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/build-plain-row.d.ts +52 -0
- package/dist/ir-to-client-js/control-flow/plan/build-plain-row.d.ts.map +1 -0
- package/dist/ir-to-client-js/control-flow/plan/build-reactive-effects.d.ts +2 -0
- 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 +6 -7
- package/dist/ir-to-client-js/control-flow/plan/lazy-conditional.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/lazy-preamble.d.ts +5 -7
- package/dist/ir-to-client-js/control-flow/plan/lazy-preamble.d.ts.map +1 -1
- 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/loop.d.ts +10 -6
- package/dist/ir-to-client-js/control-flow/plan/loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/shared.d.ts +11 -9
- package/dist/ir-to-client-js/control-flow/shared.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/composite-loop.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 +16 -0
- package/dist/ir-to-client-js/csr-substitute.d.ts.map +1 -1
- package/dist/ir-to-client-js/emit-reactive.d.ts.map +1 -1
- package/dist/ir-to-client-js/emit-registration.d.ts.map +1 -1
- package/dist/ir-to-client-js/html-template.d.ts +2 -2
- package/dist/ir-to-client-js/html-template.d.ts.map +1 -1
- package/dist/ir-to-client-js/imports.d.ts +2 -2
- package/dist/ir-to-client-js/imports.d.ts.map +1 -1
- package/dist/ir-to-client-js/index.d.ts.map +1 -1
- package/dist/ir-to-client-js/markup-slots.d.ts +21 -0
- package/dist/ir-to-client-js/markup-slots.d.ts.map +1 -0
- package/dist/ir-to-client-js/safe-html.d.ts +158 -0
- package/dist/ir-to-client-js/safe-html.d.ts.map +1 -0
- package/dist/ir-to-client-js/utils.d.ts +34 -1
- package/dist/ir-to-client-js/utils.d.ts.map +1 -1
- package/dist/jsx-to-ir.d.ts.map +1 -1
- package/dist/lowering-registry.d.ts +12 -0
- package/dist/lowering-registry.d.ts.map +1 -1
- package/dist/prop-rewrite.d.ts +8 -3
- package/dist/prop-rewrite.d.ts.map +1 -1
- package/dist/props-binding.d.ts +22 -1
- package/dist/props-binding.d.ts.map +1 -1
- package/dist/ssr-defaults.d.ts.map +1 -1
- package/dist/types.d.ts +5 -10
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/__snapshots__/doc-examples.test.ts.snap +10 -12
- package/src/__tests__/build-plain-row-consolidation.test.ts +120 -0
- package/src/__tests__/client-template-escape-soundness.test.ts +538 -0
- package/src/__tests__/csr-template-loop-shadowing.test.ts +4 -2
- package/src/__tests__/flatmap-segments.test.ts +1 -1
- package/src/__tests__/lazy-conditional.test.ts +52 -12
- package/src/__tests__/lazy-preamble.test.ts +11 -11
- package/src/__tests__/lazy-row-eligibility.test.ts +8 -3
- package/src/__tests__/loop-child-reactive-attr-const-shadow.test.ts +25 -3
- package/src/__tests__/loop-item-conditional-codegen.test.ts +4 -2
- package/src/__tests__/lowering-value-position.test.ts +232 -0
- package/src/__tests__/map-arbitrary-body.test.ts +1 -1
- package/src/__tests__/nested-loop-index-param.test.ts +14 -3
- package/src/__tests__/nested-loop-reactive-attrs.test.ts +4 -2
- package/src/__tests__/reactive-attrs-in-map.test.ts +13 -10
- package/src/__tests__/rewrite-destructured-props.test.ts +37 -0
- package/src/__tests__/safe-html-splice-door.test.ts +61 -0
- package/src/__tests__/ssr-defaults.test.ts +59 -0
- package/src/adapters/parsed-expr-emitter.ts +43 -0
- package/src/analyzer.ts +4 -0
- package/src/expression-parser.ts +63 -29
- package/src/index.ts +10 -3
- package/src/ir-to-client-js/collect-elements.ts +18 -2
- package/src/ir-to-client-js/control-flow/plan/build-branch-loop.ts +20 -28
- package/src/ir-to-client-js/control-flow/plan/build-component-loop.ts +9 -7
- package/src/ir-to-client-js/control-flow/plan/build-composite-loop.ts +10 -6
- package/src/ir-to-client-js/control-flow/plan/build-inner-loop.ts +8 -8
- package/src/ir-to-client-js/control-flow/plan/build-lazy-row.ts +18 -2
- package/src/ir-to-client-js/control-flow/plan/build-loop-child-arm.ts +3 -3
- package/src/ir-to-client-js/control-flow/plan/build-loop.ts +35 -35
- package/src/ir-to-client-js/control-flow/plan/build-plain-row.ts +110 -0
- package/src/ir-to-client-js/control-flow/plan/build-reactive-effects.ts +5 -2
- package/src/ir-to-client-js/control-flow/plan/lazy-conditional.ts +11 -10
- package/src/ir-to-client-js/control-flow/plan/lazy-preamble.ts +11 -13
- package/src/ir-to-client-js/control-flow/plan/lazy-row-eligibility.ts +13 -3
- package/src/ir-to-client-js/control-flow/plan/loop.ts +10 -6
- package/src/ir-to-client-js/control-flow/shared.ts +40 -17
- package/src/ir-to-client-js/control-flow/stringify/composite-loop.ts +2 -1
- package/src/ir-to-client-js/control-flow/stringify/lazy-row.ts +10 -0
- package/src/ir-to-client-js/csr-substitute.ts +29 -0
- package/src/ir-to-client-js/emit-reactive.ts +4 -1
- package/src/ir-to-client-js/emit-registration.ts +2 -4
- package/src/ir-to-client-js/html-template.ts +58 -244
- package/src/ir-to-client-js/imports.ts +3 -0
- package/src/ir-to-client-js/index.ts +4 -4
- package/src/ir-to-client-js/markup-slots.ts +25 -0
- package/src/ir-to-client-js/safe-html.ts +243 -0
- package/src/ir-to-client-js/utils.ts +58 -10
- package/src/jsx-to-ir.ts +115 -20
- package/src/lowering-registry.ts +21 -0
- package/src/prop-rewrite.ts +8 -4
- package/src/props-binding.ts +37 -1
- package/src/ssr-defaults.ts +45 -0
- package/src/types.ts +5 -10
- package/src/__tests__/text-slot-escaping.test.ts +0 -65
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Direct unit tests for `spliceChildValue`'s arm order (#2795 follow-up).
|
|
3
|
+
*
|
|
4
|
+
* The door's arm order changed during the #2795 migration:
|
|
5
|
+
* `node.joinArrayChild` is now checked BEFORE `cx.branchSlotsVar`. Before,
|
|
6
|
+
* `irToHtmlTemplate` wrapped an array in `__bfSlot` first and joined after —
|
|
7
|
+
* for an element-array child (`{out}`, built by an arbitrary `.map()`
|
|
8
|
+
* preamble) rendered INSIDE a conditional branch, that would have handed
|
|
9
|
+
* `__bfSlot` the whole array-join expression, which stringifies an array
|
|
10
|
+
* argument via `String(...)`-style coercion internally rather than joining
|
|
11
|
+
* compiled-leaf HTML — corrupting already-escaped leaf markup. Joining first
|
|
12
|
+
* (the current order) hands `__bfSlot` a single already-assembled HTML
|
|
13
|
+
* string instead.
|
|
14
|
+
*
|
|
15
|
+
* PR #2858 shipped this reorder without a test exercising the co-occurrence
|
|
16
|
+
* (join-array-child AND branch-slot at once) — this file is that test,
|
|
17
|
+
* added directly against the door function rather than a full JSX compile,
|
|
18
|
+
* since the arm order is entirely `spliceChildValue`'s own concern.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { describe, test, expect } from 'bun:test'
|
|
22
|
+
import { spliceChildValue } from '../ir-to-client-js/safe-html'
|
|
23
|
+
|
|
24
|
+
describe('spliceChildValue arm order', () => {
|
|
25
|
+
test('joinArrayChild wins over branchSlotsVar — the array is joined, not handed to __bfSlot', () => {
|
|
26
|
+
const result = spliceChildValue(
|
|
27
|
+
{ expr: 'out', slotId: null, joinArrayChild: true },
|
|
28
|
+
'out',
|
|
29
|
+
{ branchSlotsVar: 'slots' },
|
|
30
|
+
)
|
|
31
|
+
expect(result).toBe(`Array.isArray(out) ? out.join('') : (out ?? '')`)
|
|
32
|
+
expect(result).not.toContain('__bfSlot')
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
test('branchSlotsVar alone (no joinArrayChild) still routes through __bfSlot', () => {
|
|
36
|
+
const result = spliceChildValue(
|
|
37
|
+
{ expr: 'value', slotId: null, joinArrayChild: false },
|
|
38
|
+
'value',
|
|
39
|
+
{ branchSlotsVar: 'slots' },
|
|
40
|
+
)
|
|
41
|
+
expect(result).toBe('__bfSlot(value, slots)')
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
test('neither joinArrayChild nor branchSlotsVar nor slotId nor children — plain escapeText default', () => {
|
|
45
|
+
const result = spliceChildValue(
|
|
46
|
+
{ expr: 'label', slotId: null, joinArrayChild: false },
|
|
47
|
+
'label',
|
|
48
|
+
{},
|
|
49
|
+
)
|
|
50
|
+
expect(result).toBe('escapeText(label)')
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
test('a markup-claimed slotId still escapes-or-unwraps even with joinArrayChild absent', () => {
|
|
54
|
+
const result = spliceChildValue(
|
|
55
|
+
{ expr: 'title', slotId: 's0', joinArrayChild: false },
|
|
56
|
+
'(title)',
|
|
57
|
+
{ markupSlotIds: new Set(['s0']) },
|
|
58
|
+
)
|
|
59
|
+
expect(result).toBe('escapeTextOrMarkup((title))')
|
|
60
|
+
})
|
|
61
|
+
})
|
|
@@ -499,6 +499,65 @@ describe('extractSsrDefaults', () => {
|
|
|
499
499
|
// it does not abort the whole computation.
|
|
500
500
|
expect(defaults?.x).toEqual({ value: [1, null] })
|
|
501
501
|
})
|
|
502
|
+
|
|
503
|
+
// #2813 (pullfrog nitpick on the PR fixing it): a bare local-const alias
|
|
504
|
+
// of a signal getter must be seeded under its OWN name too, alongside
|
|
505
|
+
// the getter's — a template-stash adapter's `.map()` loop over
|
|
506
|
+
// `items__alias()` looks up the stash entry by that name, which the
|
|
507
|
+
// getter-name-only seeding above never creates.
|
|
508
|
+
test('a bare local-const alias of a signal getter is seeded under the alias name too', () => {
|
|
509
|
+
const metadata = metadataFor(`
|
|
510
|
+
'use client'
|
|
511
|
+
import { createSignal } from '@barefootjs/client'
|
|
512
|
+
function C() {
|
|
513
|
+
const [items] = createSignal([1, 2, 3])
|
|
514
|
+
const items__alias = items
|
|
515
|
+
return <ul>{items__alias().map(n => <li key={n}>{n}</li>)}</ul>
|
|
516
|
+
}
|
|
517
|
+
`)
|
|
518
|
+
|
|
519
|
+
const defaults = extractSsrDefaults(metadata)
|
|
520
|
+
expect(defaults?.items).toEqual({ value: [1, 2, 3] })
|
|
521
|
+
expect(defaults?.items__alias).toEqual({ value: [1, 2, 3] })
|
|
522
|
+
})
|
|
523
|
+
|
|
524
|
+
test('a multi-hop local-const alias chain resolves through every link', () => {
|
|
525
|
+
const metadata = metadataFor(`
|
|
526
|
+
'use client'
|
|
527
|
+
import { createSignal } from '@barefootjs/client'
|
|
528
|
+
function C() {
|
|
529
|
+
const [items] = createSignal([1, 2, 3])
|
|
530
|
+
const hop1 = items
|
|
531
|
+
const hop2 = hop1
|
|
532
|
+
return <ul>{hop2().map(n => <li key={n}>{n}</li>)}</ul>
|
|
533
|
+
}
|
|
534
|
+
`)
|
|
535
|
+
|
|
536
|
+
const defaults = extractSsrDefaults(metadata)
|
|
537
|
+
expect(defaults?.items).toEqual({ value: [1, 2, 3] })
|
|
538
|
+
expect(defaults?.hop1).toEqual({ value: [1, 2, 3] })
|
|
539
|
+
expect(defaults?.hop2).toEqual({ value: [1, 2, 3] })
|
|
540
|
+
})
|
|
541
|
+
|
|
542
|
+
test('a getter alias shares the ORIGIN entry object, including a self-derivation-collision propName shape', () => {
|
|
543
|
+
// The origin signal's OWN name collides with the prop its initializer
|
|
544
|
+
// derives from (#2669), so its entry is a propName-carrying prop
|
|
545
|
+
// entry, not a plain `{ value }` — the alias must share that exact
|
|
546
|
+
// shape (same object reference), not re-derive a plain value entry.
|
|
547
|
+
const metadata = metadataFor(`
|
|
548
|
+
'use client'
|
|
549
|
+
import { createSignal } from '@barefootjs/client'
|
|
550
|
+
function C(props: { label?: string }) {
|
|
551
|
+
const [label] = createSignal(props.label ?? 'Default')
|
|
552
|
+
const labelAlias = label
|
|
553
|
+
return <p>{labelAlias()}</p>
|
|
554
|
+
}
|
|
555
|
+
`)
|
|
556
|
+
|
|
557
|
+
const defaults = extractSsrDefaults(metadata)
|
|
558
|
+
expect(defaults?.label).toEqual({ propName: 'label', value: null })
|
|
559
|
+
expect(defaults?.labelAlias).toBe(defaults?.label)
|
|
560
|
+
})
|
|
502
561
|
})
|
|
503
562
|
|
|
504
563
|
// TS twin of the Ruby/Python/PHP/Perl/Rust `derive*FromDefaults` runtime
|
|
@@ -35,6 +35,7 @@
|
|
|
35
35
|
|
|
36
36
|
import type { ParsedExpr, FlatDepth, TemplatePart, ObjectLiteralProperty } from '../expression-parser.ts'
|
|
37
37
|
import { asCallbackMethodCall } from '../expression-parser.ts'
|
|
38
|
+
import type { LoweringMatcher, LoweringNode } from '../lowering-registry.ts'
|
|
38
39
|
|
|
39
40
|
export type HigherOrderMethod = 'filter' | 'every' | 'some' | 'find' | 'findIndex' | 'findLast' | 'findLastIndex'
|
|
40
41
|
|
|
@@ -95,6 +96,22 @@ export type SortMethod = 'sort' | 'toSorted'
|
|
|
95
96
|
|
|
96
97
|
export type LiteralType = 'string' | 'number' | 'boolean' | 'null'
|
|
97
98
|
|
|
99
|
+
/**
|
|
100
|
+
* An adapter's registered-lowering seam for the shared `call` dispatch
|
|
101
|
+
* (#2843). `matchers` are the same `ctx.state.loweringMatchers`-style list
|
|
102
|
+
* every adapter already binds at init; `render` maps one matched neutral
|
|
103
|
+
* node to this adapter's own template syntax (its body is typically what
|
|
104
|
+
* used to live inline in the adapter's `call()` method before this seam
|
|
105
|
+
* existed — see `renderLoweringNode` in the Go adapter's `url-builder.ts`
|
|
106
|
+
* for the shape). Returning null for a `kind`/`helper` this adapter has no
|
|
107
|
+
* mapping for lets the caller fall through to the ordinary `call()`
|
|
108
|
+
* dispatch, same as an unmatched call.
|
|
109
|
+
*/
|
|
110
|
+
export interface LoweringEmitter {
|
|
111
|
+
readonly matchers: readonly LoweringMatcher[]
|
|
112
|
+
render(node: LoweringNode, emit: (e: ParsedExpr) => string): string | null
|
|
113
|
+
}
|
|
114
|
+
|
|
98
115
|
/**
|
|
99
116
|
* Adapter-side ParsedExpr lowering surface. One method per
|
|
100
117
|
* `ParsedExpr.kind`. The shared `emitParsedExpr` runner dispatches
|
|
@@ -109,6 +126,17 @@ export interface ParsedExprEmitter {
|
|
|
109
126
|
identifier(name: string): string
|
|
110
127
|
literal(value: string | number | boolean | null, literalType: LiteralType): string
|
|
111
128
|
call(callee: ParsedExpr, args: ParsedExpr[], emit: (e: ParsedExpr) => string): string
|
|
129
|
+
// Optional (#2843): when present, `emitParsedExpr`'s `call` case tries
|
|
130
|
+
// every matcher FIRST — before `call()` itself, and before the
|
|
131
|
+
// higher-order `callbackMethod` recognition — so a registered lowering
|
|
132
|
+
// plugin's call is recognised no matter where in an expression tree it
|
|
133
|
+
// sits (a ternary branch, a template-literal interpolation, a binary
|
|
134
|
+
// operand, …), not only when the adapter's own top-level conversion entry
|
|
135
|
+
// point happens to consult the registry directly. Omitted entirely by an
|
|
136
|
+
// adapter that hasn't adopted the seam (e.g. Go, which still consults the
|
|
137
|
+
// registry inline in its own `call()` per #2842) — behaviour is then
|
|
138
|
+
// byte-identical to before this field existed.
|
|
139
|
+
lowering?: LoweringEmitter
|
|
112
140
|
// `optional` is true for a `?.`-written access (`user?.name`); see the
|
|
113
141
|
// `ParsedExpr` `member` variant's docstring in `expression-parser.ts`
|
|
114
142
|
// for the single-hop caveat. Every adapter's `member()` implementation
|
|
@@ -340,6 +368,21 @@ export function emitParsedExpr(expr: ParsedExpr, emitter: ParsedExprEmitter): st
|
|
|
340
368
|
case 'literal':
|
|
341
369
|
return emitter.literal(expr.value, expr.literalType)
|
|
342
370
|
case 'call': {
|
|
371
|
+
// #2843: a registered lowering plugin's call wins in EVERY value
|
|
372
|
+
// position this dispatcher reaches, not only an adapter's own
|
|
373
|
+
// top-level conversion entry point — see `LoweringEmitter`'s doc.
|
|
374
|
+
// First-match-wins; a matched node whose `render` declines (helper id
|
|
375
|
+
// this adapter has no mapping for) falls through to the ordinary
|
|
376
|
+
// dispatch below, same as an unmatched call.
|
|
377
|
+
if (emitter.lowering) {
|
|
378
|
+
for (const matcher of emitter.lowering.matchers) {
|
|
379
|
+
const node = matcher(expr.callee, expr.args)
|
|
380
|
+
if (!node) continue
|
|
381
|
+
const rendered = emitter.lowering.render(node, emit)
|
|
382
|
+
if (rendered !== null) return rendered
|
|
383
|
+
break
|
|
384
|
+
}
|
|
385
|
+
}
|
|
343
386
|
// A higher-order callback call (`arr.filter(p)` / `arr.sort(cmp)` / …)
|
|
344
387
|
// routes to the dedicated `callbackMethod` arm; any other call is generic.
|
|
345
388
|
const cb = asCallbackMethodCall(expr)
|
package/src/analyzer.ts
CHANGED
|
@@ -1998,6 +1998,9 @@ const CLIENT_EXPORTS = new Set([
|
|
|
1998
1998
|
'forwardProps', 'unwrap', '__slot',
|
|
1999
1999
|
'createContext', 'useContext', 'provideContext',
|
|
2000
2000
|
'createPortal', 'isSSRPortal', 'findSiblingSlot', 'cleanupPortalPlaceholder',
|
|
2001
|
+
// Floating-element position tracking (#2848) — same runtime-only shape
|
|
2002
|
+
// as the portal entries above.
|
|
2003
|
+
'trackPosition',
|
|
2001
2004
|
// Request-scoped environment signal factory (router v0.5) — `createSignal`-
|
|
2002
2005
|
// shaped, recognised structurally (#2057) so its getter is just a signal
|
|
2003
2006
|
// getter; the compiler lowers the reader value per adapter via the signal's
|
|
@@ -4221,6 +4224,7 @@ export const BROWSER_ONLY_CLIENT_APIS = new Set([
|
|
|
4221
4224
|
'isSSRPortal',
|
|
4222
4225
|
'findSiblingSlot',
|
|
4223
4226
|
'cleanupPortalPlaceholder',
|
|
4227
|
+
'trackPosition',
|
|
4224
4228
|
])
|
|
4225
4229
|
|
|
4226
4230
|
function importsBrowserOnlyClientApi(ctx: AnalyzerContext): boolean {
|
package/src/expression-parser.ts
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import ts from 'typescript'
|
|
10
|
+
import { loweringNodeChildren, type LoweringMatcher } from './lowering-registry.ts'
|
|
10
11
|
|
|
11
12
|
// =============================================================================
|
|
12
13
|
// Parsed Expression Types
|
|
@@ -1049,7 +1050,7 @@ function convertNode(node: ts.Node, raw: string): ParsedExpr {
|
|
|
1049
1050
|
// surface — the depth must be *expressible*, even if not
|
|
1050
1051
|
// known until render time.
|
|
1051
1052
|
const parsedDepth = convertNode(depthNode, raw)
|
|
1052
|
-
if (checkSupport(parsedDepth, 'rendered').supported) {
|
|
1053
|
+
if (checkSupport(parsedDepth, 'rendered', []).supported) {
|
|
1053
1054
|
depthExpr = parsedDepth
|
|
1054
1055
|
flatDepth = 1 // unused placeholder — consumers must check `depthExpr` first
|
|
1055
1056
|
} else {
|
|
@@ -2419,9 +2420,20 @@ function getUnaryOperatorString(op: ts.PrefixUnaryOperator): string {
|
|
|
2419
2420
|
* reaches the page. See {@link isSupportedValue} for the sibling VALUE-
|
|
2420
2421
|
* position entry point, and `checkSupport`'s `pos` parameter for why the two
|
|
2421
2422
|
* never converge partway through a tree.
|
|
2423
|
+
*
|
|
2424
|
+
* `opts.loweringMatchers` (#2843), when given, lets a call recognised by a
|
|
2425
|
+
* registered lowering plugin (`queryHref`, or any userland plugin) admit
|
|
2426
|
+
* its params regardless of nesting position — a ternary branch, a
|
|
2427
|
+
* template-literal interpolation, a binary operand, … — matching the
|
|
2428
|
+
* top-level attribute path, which already consulted the same registry
|
|
2429
|
+
* before this gate ever ran. Omit it (every existing caller does) for
|
|
2430
|
+
* byte-identical prior behaviour.
|
|
2422
2431
|
*/
|
|
2423
|
-
export function isSupported(
|
|
2424
|
-
|
|
2432
|
+
export function isSupported(
|
|
2433
|
+
expr: ParsedExpr,
|
|
2434
|
+
opts?: { loweringMatchers?: readonly LoweringMatcher[] },
|
|
2435
|
+
): SupportResult {
|
|
2436
|
+
return checkSupport(expr, 'rendered', opts?.loweringMatchers ?? [])
|
|
2425
2437
|
}
|
|
2426
2438
|
|
|
2427
2439
|
/**
|
|
@@ -2430,10 +2442,13 @@ export function isSupported(expr: ParsedExpr): SupportResult {
|
|
|
2430
2442
|
* assignment, never a render). The one behavioural difference from
|
|
2431
2443
|
* {@link isSupported}: an `object-literal` anywhere in the tree is admitted
|
|
2432
2444
|
* when every property value is itself supported, instead of being refused
|
|
2433
|
-
* outright.
|
|
2445
|
+
* outright. See {@link isSupported}'s doc for `opts.loweringMatchers`.
|
|
2434
2446
|
*/
|
|
2435
|
-
export function isSupportedValue(
|
|
2436
|
-
|
|
2447
|
+
export function isSupportedValue(
|
|
2448
|
+
expr: ParsedExpr,
|
|
2449
|
+
opts?: { loweringMatchers?: readonly LoweringMatcher[] },
|
|
2450
|
+
): SupportResult {
|
|
2451
|
+
return checkSupport(expr, 'value', opts?.loweringMatchers ?? [])
|
|
2437
2452
|
}
|
|
2438
2453
|
|
|
2439
2454
|
/**
|
|
@@ -2453,7 +2468,7 @@ export function isSupportedValue(expr: ParsedExpr): SupportResult {
|
|
|
2453
2468
|
*/
|
|
2454
2469
|
type ExprPos = 'rendered' | 'value'
|
|
2455
2470
|
|
|
2456
|
-
function checkSupport(expr: ParsedExpr, pos: ExprPos): SupportResult {
|
|
2471
|
+
function checkSupport(expr: ParsedExpr, pos: ExprPos, matchers: readonly LoweringMatcher[]): SupportResult {
|
|
2457
2472
|
switch (expr.kind) {
|
|
2458
2473
|
case 'unsupported':
|
|
2459
2474
|
return { supported: false, reason: expr.reason }
|
|
@@ -2471,7 +2486,7 @@ function checkSupport(expr: ParsedExpr, pos: ExprPos): SupportResult {
|
|
|
2471
2486
|
// A spread entry's SOURCE must itself be supported at `value`
|
|
2472
2487
|
// position — the same criterion as an explicit property's value,
|
|
2473
2488
|
// since the spread source is read (not rendered) at merge time.
|
|
2474
|
-
const propSupport = checkSupport(prop.kind === 'spread' ? prop.expr : prop.value, pos)
|
|
2489
|
+
const propSupport = checkSupport(prop.kind === 'spread' ? prop.expr : prop.value, pos, matchers)
|
|
2475
2490
|
if (!propSupport.supported) return propSupport
|
|
2476
2491
|
}
|
|
2477
2492
|
return { supported: true, level: 'L2' }
|
|
@@ -2498,7 +2513,7 @@ function checkSupport(expr: ParsedExpr, pos: ExprPos): SupportResult {
|
|
|
2498
2513
|
// method, not here, because we can't see which adapter is consuming
|
|
2499
2514
|
// the IR at this point.
|
|
2500
2515
|
for (const el of expr.elements) {
|
|
2501
|
-
const elSupport = checkSupport(el, pos)
|
|
2516
|
+
const elSupport = checkSupport(el, pos, matchers)
|
|
2502
2517
|
if (!elSupport.supported) return elSupport
|
|
2503
2518
|
}
|
|
2504
2519
|
return { supported: true, level: 'L2' }
|
|
@@ -2518,10 +2533,10 @@ function checkSupport(expr: ParsedExpr, pos: ExprPos): SupportResult {
|
|
|
2518
2533
|
}
|
|
2519
2534
|
}
|
|
2520
2535
|
// Receiver/args inherit `pos` (see `ExprPos`'s doc).
|
|
2521
|
-
const objSupport = checkSupport(expr.object, pos)
|
|
2536
|
+
const objSupport = checkSupport(expr.object, pos, matchers)
|
|
2522
2537
|
if (!objSupport.supported) return objSupport
|
|
2523
2538
|
for (const arg of expr.args) {
|
|
2524
|
-
const argSupport = checkSupport(arg, pos)
|
|
2539
|
+
const argSupport = checkSupport(arg, pos, matchers)
|
|
2525
2540
|
if (!argSupport.supported) return argSupport
|
|
2526
2541
|
}
|
|
2527
2542
|
// A dynamic `.flat(depth)` carries its depth expression outside
|
|
@@ -2532,7 +2547,7 @@ function checkSupport(expr: ParsedExpr, pos: ExprPos): SupportResult {
|
|
|
2532
2547
|
// any `array-method`/`flat` node regardless of how it was constructed
|
|
2533
2548
|
// (e.g. by a rewrite walker).
|
|
2534
2549
|
if (expr.method === 'flat' && expr.depthExpr) {
|
|
2535
|
-
const depthSupport = checkSupport(expr.depthExpr, pos)
|
|
2550
|
+
const depthSupport = checkSupport(expr.depthExpr, pos, matchers)
|
|
2536
2551
|
if (!depthSupport.supported) return depthSupport
|
|
2537
2552
|
}
|
|
2538
2553
|
return { supported: true, level: 'L2' }
|
|
@@ -2540,6 +2555,25 @@ function checkSupport(expr: ParsedExpr, pos: ExprPos): SupportResult {
|
|
|
2540
2555
|
|
|
2541
2556
|
|
|
2542
2557
|
case 'call': {
|
|
2558
|
+
// #2843: a call a registered lowering plugin recognises (`queryHref`,
|
|
2559
|
+
// or any userland plugin) is admitted regardless of where it sits in
|
|
2560
|
+
// the tree — the neutral node's OWN children (`loweringNodeChildren`)
|
|
2561
|
+
// are re-checked at the current `pos` instead of the call's raw
|
|
2562
|
+
// `args`, so e.g. `queryHref`'s params object literal (refused at
|
|
2563
|
+
// `rendered` position by the `object-literal` case above) doesn't
|
|
2564
|
+
// reach that refusal at all: the plugin already took responsibility
|
|
2565
|
+
// for it. Tried before `asCallbackMethodCall` and the generic per-arg
|
|
2566
|
+
// loop below, matching the precedence the top-level attribute path
|
|
2567
|
+
// already gives the registry.
|
|
2568
|
+
for (const matcher of matchers) {
|
|
2569
|
+
const node = matcher(expr.callee, expr.args)
|
|
2570
|
+
if (!node) continue
|
|
2571
|
+
for (const child of loweringNodeChildren(node)) {
|
|
2572
|
+
const childSupport = checkSupport(child, pos, matchers)
|
|
2573
|
+
if (!childSupport.supported) return childSupport
|
|
2574
|
+
}
|
|
2575
|
+
return { supported: true, level: 'L2' }
|
|
2576
|
+
}
|
|
2543
2577
|
// Higher-order callback methods (`.filter`/`.sort`/`.reduce`/… with an
|
|
2544
2578
|
// arrow argument) lower via the runtime evaluator (#2018). Supported iff
|
|
2545
2579
|
// the receiver and the callback BODY are supported. Recognised before the
|
|
@@ -2558,9 +2592,9 @@ function checkSupport(expr: ParsedExpr, pos: ExprPos): SupportResult {
|
|
|
2558
2592
|
// one (a signal/memo initializer) admits it.
|
|
2559
2593
|
const cb = asCallbackMethodCall(expr)
|
|
2560
2594
|
if (cb) {
|
|
2561
|
-
const objSupport = checkSupport(cb.object, pos)
|
|
2595
|
+
const objSupport = checkSupport(cb.object, pos, matchers)
|
|
2562
2596
|
if (!objSupport.supported) return objSupport
|
|
2563
|
-
const bodySupport = checkSupport(cb.arrow.body, pos)
|
|
2597
|
+
const bodySupport = checkSupport(cb.arrow.body, pos, matchers)
|
|
2564
2598
|
if (!bodySupport.supported) {
|
|
2565
2599
|
return {
|
|
2566
2600
|
supported: false,
|
|
@@ -2569,7 +2603,7 @@ function checkSupport(expr: ParsedExpr, pos: ExprPos): SupportResult {
|
|
|
2569
2603
|
}
|
|
2570
2604
|
}
|
|
2571
2605
|
for (const rest of cb.args) {
|
|
2572
|
-
const restSupport = checkSupport(rest, pos)
|
|
2606
|
+
const restSupport = checkSupport(rest, pos, matchers)
|
|
2573
2607
|
if (!restSupport.supported) return restSupport
|
|
2574
2608
|
}
|
|
2575
2609
|
return { supported: true, level: 'L5' }
|
|
@@ -2577,7 +2611,7 @@ function checkSupport(expr: ParsedExpr, pos: ExprPos): SupportResult {
|
|
|
2577
2611
|
|
|
2578
2612
|
// Check if callee is supported. Not container contents — inherits
|
|
2579
2613
|
// the CURRENT `pos` (see `ExprPos`'s doc).
|
|
2580
|
-
const calleeSupport = checkSupport(expr.callee, pos)
|
|
2614
|
+
const calleeSupport = checkSupport(expr.callee, pos, matchers)
|
|
2581
2615
|
if (!calleeSupport.supported) {
|
|
2582
2616
|
return calleeSupport
|
|
2583
2617
|
}
|
|
@@ -2610,7 +2644,7 @@ function checkSupport(expr: ParsedExpr, pos: ExprPos): SupportResult {
|
|
|
2610
2644
|
// recognised callback/array-method (that path returned above) —
|
|
2611
2645
|
// inherits the CURRENT `pos` (see `ExprPos`'s doc).
|
|
2612
2646
|
for (const arg of expr.args) {
|
|
2613
|
-
const argSupport = checkSupport(arg, pos)
|
|
2647
|
+
const argSupport = checkSupport(arg, pos, matchers)
|
|
2614
2648
|
if (!argSupport.supported) {
|
|
2615
2649
|
return argSupport
|
|
2616
2650
|
}
|
|
@@ -2620,7 +2654,7 @@ function checkSupport(expr: ParsedExpr, pos: ExprPos): SupportResult {
|
|
|
2620
2654
|
|
|
2621
2655
|
case 'member': {
|
|
2622
2656
|
// Not container contents — inherits the CURRENT `pos`.
|
|
2623
|
-
const objSupport = checkSupport(expr.object, pos)
|
|
2657
|
+
const objSupport = checkSupport(expr.object, pos, matchers)
|
|
2624
2658
|
if (!objSupport.supported) {
|
|
2625
2659
|
return objSupport
|
|
2626
2660
|
}
|
|
@@ -2636,9 +2670,9 @@ function checkSupport(expr: ParsedExpr, pos: ExprPos): SupportResult {
|
|
|
2636
2670
|
// expression are themselves supported (the index is typically a
|
|
2637
2671
|
// loop variable or arithmetic over one). #1897 (data-table). Neither
|
|
2638
2672
|
// is container contents — both inherit the CURRENT `pos`.
|
|
2639
|
-
const objSupport = checkSupport(expr.object, pos)
|
|
2673
|
+
const objSupport = checkSupport(expr.object, pos, matchers)
|
|
2640
2674
|
if (!objSupport.supported) return objSupport
|
|
2641
|
-
const indexSupport = checkSupport(expr.index, pos)
|
|
2675
|
+
const indexSupport = checkSupport(expr.index, pos, matchers)
|
|
2642
2676
|
if (!indexSupport.supported) return indexSupport
|
|
2643
2677
|
return { supported: true, level: 'L2' }
|
|
2644
2678
|
}
|
|
@@ -2646,9 +2680,9 @@ function checkSupport(expr: ParsedExpr, pos: ExprPos): SupportResult {
|
|
|
2646
2680
|
case 'binary': {
|
|
2647
2681
|
// Operands inherit the CURRENT `pos` (see `ExprPos`'s doc) — a binary
|
|
2648
2682
|
// operand isn't container contents.
|
|
2649
|
-
const leftSupport = checkSupport(expr.left, pos)
|
|
2683
|
+
const leftSupport = checkSupport(expr.left, pos, matchers)
|
|
2650
2684
|
if (!leftSupport.supported) return leftSupport
|
|
2651
|
-
const rightSupport = checkSupport(expr.right, pos)
|
|
2685
|
+
const rightSupport = checkSupport(expr.right, pos, matchers)
|
|
2652
2686
|
if (!rightSupport.supported) return rightSupport
|
|
2653
2687
|
|
|
2654
2688
|
// Comparison operators are L3
|
|
@@ -2666,7 +2700,7 @@ function checkSupport(expr: ParsedExpr, pos: ExprPos): SupportResult {
|
|
|
2666
2700
|
|
|
2667
2701
|
case 'unary': {
|
|
2668
2702
|
// Inherits the CURRENT `pos` — not container contents.
|
|
2669
|
-
const argSupport = checkSupport(expr.argument, pos)
|
|
2703
|
+
const argSupport = checkSupport(expr.argument, pos, matchers)
|
|
2670
2704
|
if (!argSupport.supported) return argSupport
|
|
2671
2705
|
|
|
2672
2706
|
// Negation is L4
|
|
@@ -2683,7 +2717,7 @@ function checkSupport(expr: ParsedExpr, pos: ExprPos): SupportResult {
|
|
|
2683
2717
|
|
|
2684
2718
|
case 'logical': {
|
|
2685
2719
|
// Operands inherit the CURRENT `pos` — not container contents.
|
|
2686
|
-
const leftSupport = checkSupport(expr.left, pos)
|
|
2720
|
+
const leftSupport = checkSupport(expr.left, pos, matchers)
|
|
2687
2721
|
if (!leftSupport.supported) return leftSupport
|
|
2688
2722
|
|
|
2689
2723
|
// `x ?? {}` — admit an EMPTY object-literal fallback as the right
|
|
@@ -2710,7 +2744,7 @@ function checkSupport(expr: ParsedExpr, pos: ExprPos): SupportResult {
|
|
|
2710
2744
|
return { supported: true, level: 'L4' }
|
|
2711
2745
|
}
|
|
2712
2746
|
|
|
2713
|
-
const rightSupport = checkSupport(expr.right, pos)
|
|
2747
|
+
const rightSupport = checkSupport(expr.right, pos, matchers)
|
|
2714
2748
|
if (!rightSupport.supported) return rightSupport
|
|
2715
2749
|
|
|
2716
2750
|
return { supported: true, level: 'L4' }
|
|
@@ -2722,11 +2756,11 @@ function checkSupport(expr: ParsedExpr, pos: ExprPos): SupportResult {
|
|
|
2722
2756
|
// position both branches are themselves rendered — `cond ? {a:1} :
|
|
2723
2757
|
// {b:2}` stays refused there, not admitted just for being a
|
|
2724
2758
|
// sub-expression.
|
|
2725
|
-
const testSupport = checkSupport(expr.test, pos)
|
|
2759
|
+
const testSupport = checkSupport(expr.test, pos, matchers)
|
|
2726
2760
|
if (!testSupport.supported) return testSupport
|
|
2727
|
-
const consSupport = checkSupport(expr.consequent, pos)
|
|
2761
|
+
const consSupport = checkSupport(expr.consequent, pos, matchers)
|
|
2728
2762
|
if (!consSupport.supported) return consSupport
|
|
2729
|
-
const altSupport = checkSupport(expr.alternate, pos)
|
|
2763
|
+
const altSupport = checkSupport(expr.alternate, pos, matchers)
|
|
2730
2764
|
if (!altSupport.supported) return altSupport
|
|
2731
2765
|
|
|
2732
2766
|
return { supported: true, level: 'L4' }
|
|
@@ -2738,7 +2772,7 @@ function checkSupport(expr: ParsedExpr, pos: ExprPos): SupportResult {
|
|
|
2738
2772
|
// object-literal hole has no sensible rendering either way).
|
|
2739
2773
|
for (const part of expr.parts) {
|
|
2740
2774
|
if (part.type === 'expression') {
|
|
2741
|
-
const partSupport = checkSupport(part.expr, pos)
|
|
2775
|
+
const partSupport = checkSupport(part.expr, pos, matchers)
|
|
2742
2776
|
if (!partSupport.supported) return partSupport
|
|
2743
2777
|
}
|
|
2744
2778
|
}
|
package/src/index.ts
CHANGED
|
@@ -11,10 +11,16 @@ export type { CompileResult, CompileOptions, CompileOptionsWithAdapter, FileOutp
|
|
|
11
11
|
// SSR template-variable defaults (manifest seeds for stash-based adapters)
|
|
12
12
|
export { extractSsrDefaults, deriveStashFromDefaults } from './ssr-defaults.ts'
|
|
13
13
|
|
|
14
|
-
// Shared props-destructure binding + alias-map helpers (#2524)
|
|
15
|
-
export { propsDestructureBinding, buildPropAliasMap, isIdentifierName } from './props-binding.ts'
|
|
14
|
+
// Shared props-destructure binding + alias-map helpers (#2524, #2788)
|
|
15
|
+
export { propsDestructureBinding, buildPropAliasMap, isIdentifierName, resolveBodyDestructuredPropAliases } from './props-binding.ts'
|
|
16
16
|
export type { SsrDefault } from './ssr-defaults.ts'
|
|
17
17
|
|
|
18
|
+
// Bare local-const alias-hop resolution for a signal/memo getter name
|
|
19
|
+
// (`const items__alias = items`) — #2778's CSR-template fix, reused (not
|
|
20
|
+
// re-walked) by SSR-side alias resolution (#2813: DSL stash seeding,
|
|
21
|
+
// Go's zero-arg-call field routing).
|
|
22
|
+
export { resolveGetterAliases, collectAliasableGetterNames } from './ir-to-client-js/csr-substitute.ts'
|
|
23
|
+
|
|
18
24
|
// Import-alias resolution (#2777 client-JS registry key; #2822 SSR cross-template
|
|
19
25
|
// call names) — a local alias -> declared/exported name map built from
|
|
20
26
|
// `ir.metadata.imports`. DSL adapters build this once per compile and look up
|
|
@@ -101,7 +107,7 @@ export { JsxAdapter } from './adapters/jsx-adapter.ts'
|
|
|
101
107
|
export type { JsxAdapterConfig } from './adapters/jsx-adapter.ts'
|
|
102
108
|
export { rewriteImportsForTemplate, rewriteDynamicImportsInSource } from './adapters/template-imports.ts'
|
|
103
109
|
export { emitParsedExpr, groupBinaryOperand, groupObjectLiteralSegments, isStringTypedOperand, isStringConcatBinary } from './adapters/parsed-expr-emitter.ts'
|
|
104
|
-
export type { ParsedExprEmitter, HigherOrderMethod, ArrayMethod, SortMethod, LiteralType } from './adapters/parsed-expr-emitter.ts'
|
|
110
|
+
export type { ParsedExprEmitter, LoweringEmitter, HigherOrderMethod, ArrayMethod, SortMethod, LiteralType } from './adapters/parsed-expr-emitter.ts'
|
|
105
111
|
export { collectLoopBoundNames } from './adapters/loop-bound-names.ts'
|
|
106
112
|
export { derivesScopeFromSlot } from './adapters/child-scope.ts'
|
|
107
113
|
export { evaluateStaticLiteral, isFullyStaticLiteral, resolveStaticLoopSource } from './static-literal.ts'
|
|
@@ -114,6 +120,7 @@ export {
|
|
|
114
120
|
prepareLoweringMatchers,
|
|
115
121
|
matchLoweringCall,
|
|
116
122
|
isValidHelperId,
|
|
123
|
+
loweringNodeChildren,
|
|
117
124
|
__resetLoweringPluginsForTest,
|
|
118
125
|
type LoweringPlugin,
|
|
119
126
|
type LoweringNode,
|
|
@@ -309,7 +309,7 @@ export function collectInnerLoops(
|
|
|
309
309
|
// Forward destructured bindings (#951) so inner-loop template
|
|
310
310
|
// references to the destructured locals are rewritten.
|
|
311
311
|
const loopParamsForTemplate = outerLoopParam
|
|
312
|
-
? [outerLoopParam, { param: n.param, bindings: n.paramBindings }]
|
|
312
|
+
? [outerLoopParam, { param: n.param, bindings: n.paramBindings, index: n.index }]
|
|
313
313
|
: undefined
|
|
314
314
|
const template = n.children.map(c => irToPlaceholderTemplate(c, undefined, emitDepth, loopParamsForTemplate)).join('')
|
|
315
315
|
// Check if array expression references the outer loop param
|
|
@@ -755,6 +755,18 @@ export function collectElements(
|
|
|
755
755
|
// avoiding post-hoc regex wrapping that corrupts literal attribute values.
|
|
756
756
|
// Forward destructured bindings (#951) so references like `cfg.color`
|
|
757
757
|
// in the emitted template literal are rewritten to `__bfItem()[1].color`.
|
|
758
|
+
// NOT `index: l.index` here (#2859 follow-up): this exact template
|
|
759
|
+
// string is reused verbatim by the LAZY row plan when this loop is
|
|
760
|
+
// lazy-eligible (`LazyRowPlanData`'s clone source, and — via
|
|
761
|
+
// `mapPreambleWrapped` — its `preambleStatements`) — `mapArrayLazy`'s
|
|
762
|
+
// `createRow(entry, index)` still hands the row a plain NUMBER
|
|
763
|
+
// (map-array-lazy.ts is untouched by #2859), so baking `i()` in here
|
|
764
|
+
// would call a number. Lazy-row eligibility only refuses a REACTIVE
|
|
765
|
+
// binding that references the index — a non-reactive use like a
|
|
766
|
+
// `data-key` built from the index is not gated the same way, so this
|
|
767
|
+
// can't assume "lazy-eligible ⇒ no index in the template" either.
|
|
768
|
+
// `buildPlainLoopPlan`/`buildBranchLoopPlan` apply the index wrap
|
|
769
|
+
// themselves, AFTER lazy eligibility is decided, only when it's `null`.
|
|
758
770
|
const loopParamSpec = [{ param: l.param, bindings: l.paramBindings }]
|
|
759
771
|
template = useElementReconciliation
|
|
760
772
|
? irToPlaceholderTemplate(l.children[0], resolveRestSpreadNames(ctx), 0, loopParamSpec)
|
|
@@ -1165,6 +1177,10 @@ function collectBranchLoops(
|
|
|
1165
1177
|
// keeping the template consistent with reactive effect expressions that
|
|
1166
1178
|
// use `param()` to read the current item value.
|
|
1167
1179
|
let childTemplate: string
|
|
1180
|
+
// NOT `index: n.index` here — see the identical note on `loopParamSpec`
|
|
1181
|
+
// above; `BranchLoop.template` is reused verbatim by the lazy row plan
|
|
1182
|
+
// too (`callSite: 'branch-plain'`), and `buildBranchLoopPlan` applies
|
|
1183
|
+
// the index wrap itself once lazy eligibility is known (#2859 follow-up).
|
|
1168
1184
|
const branchLoopParamSpec = [{ param: n.param, bindings: n.paramBindings }]
|
|
1169
1185
|
if (projectionInner) {
|
|
1170
1186
|
childTemplate = '' // descriptor renderItem builds from d.h, not a row template
|
|
@@ -1478,7 +1494,7 @@ export function collectLoopChildConditionals(
|
|
|
1478
1494
|
if (!readsPreamble && classifyReactivity(expanded.expr, ctx, loopParam, loopParamBindings, expanded.freeIds).kind === 'none') return
|
|
1479
1495
|
|
|
1480
1496
|
const loopParamsForCond = loopParam
|
|
1481
|
-
? [{ param: loopParam, bindings: loopParamBindings }]
|
|
1497
|
+
? [{ param: loopParam, bindings: loopParamBindings, index: loopIndex }]
|
|
1482
1498
|
: undefined
|
|
1483
1499
|
// `__slots` matches the closure variable emitted by
|
|
1484
1500
|
// `stringifyLoopChildConditional` — Child-position interpolations
|
|
@@ -11,14 +11,13 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import type { BranchLoop } from '../../types.ts'
|
|
14
|
-
import { buildChainedArrayExpr, varSlotId
|
|
14
|
+
import { buildChainedArrayExpr, varSlotId } from '../../utils.ts'
|
|
15
15
|
import { buildBranchCompositePlan } from './build-composite-loop.ts'
|
|
16
16
|
import { buildBranchLoopDelegationPlan } from './build-event-delegation.ts'
|
|
17
17
|
import { buildReactiveEffectsPlan } from './build-reactive-effects.ts'
|
|
18
|
-
import {
|
|
18
|
+
import { buildPlainRowCore } from './build-plain-row.ts'
|
|
19
19
|
import type { LazyRowScopeInfo } from './lazy-row-eligibility.ts'
|
|
20
|
-
import {
|
|
21
|
-
import { renderPreamble, irToHtmlTemplate } from '../../html-template.ts'
|
|
20
|
+
import { loopKeyFn, buildChildRefBindings } from '../shared.ts'
|
|
22
21
|
import type {
|
|
23
22
|
BranchCompositeLoopPlan,
|
|
24
23
|
BranchLoopPlan,
|
|
@@ -44,7 +43,6 @@ export function buildBranchLoopPlan(
|
|
|
44
43
|
return composite
|
|
45
44
|
}
|
|
46
45
|
|
|
47
|
-
const { head: paramHead, unwrap: paramUnwrap } = destructureLoopParam(loop.param, loop.paramBindings)
|
|
48
46
|
const hasReactiveEffects = loop.bindings.reactiveAttrs.length > 0
|
|
49
47
|
|| loop.bindings.reactiveTexts.length > 0
|
|
50
48
|
|| loop.bindings.conditionals.length > 0
|
|
@@ -54,14 +52,18 @@ export function buildBranchLoopPlan(
|
|
|
54
52
|
const arrayExpr = fm
|
|
55
53
|
? `(${buildChainedArrayExpr(loop)}).flatMap(${fm.params} => ${fm.body})`
|
|
56
54
|
: buildChainedArrayExpr(loop)
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
: ''
|
|
64
|
-
|
|
55
|
+
|
|
56
|
+
// Shared "wrap item → decide lazy → wrap index" core (#2859 follow-up) —
|
|
57
|
+
// see `build-plain-row.ts`'s module docstring for the full rationale.
|
|
58
|
+
const core = buildPlainRowCore({
|
|
59
|
+
loop,
|
|
60
|
+
arrayExpr,
|
|
61
|
+
callSite: 'branch-plain',
|
|
62
|
+
flatMapLeafItem: Boolean(fm),
|
|
63
|
+
anchored: false,
|
|
64
|
+
scope: lazyScope,
|
|
65
|
+
})
|
|
66
|
+
const { paramHead, paramUnwrap, indexParam, preambleRegions, lazyRow } = core
|
|
65
67
|
const plan: BranchPlainLoopPlan = {
|
|
66
68
|
kind: 'plain',
|
|
67
69
|
rowConstruction: 'string-template',
|
|
@@ -78,21 +80,10 @@ export function buildBranchLoopPlan(
|
|
|
78
80
|
indexParam,
|
|
79
81
|
// Wrap loop-param references to signal-accessor form so the preamble
|
|
80
82
|
// matches the template literal's already-wrapped reads (#1065).
|
|
81
|
-
mapPreambleWrapped,
|
|
83
|
+
mapPreambleWrapped: core.mapPreambleWrapped,
|
|
82
84
|
// Lazy row graph (§9, L3) — undefined for every ineligible loop.
|
|
83
|
-
lazyRow
|
|
84
|
-
|
|
85
|
-
arrayExpr,
|
|
86
|
-
indexParam,
|
|
87
|
-
paramUnwrap,
|
|
88
|
-
mapPreambleWrapped,
|
|
89
|
-
preambleRegionCount: preambleRegions.length,
|
|
90
|
-
callSite: 'branch-plain',
|
|
91
|
-
flatMapLeafItem: Boolean(fm),
|
|
92
|
-
anchored: false,
|
|
93
|
-
scope: lazyScope,
|
|
94
|
-
}) ?? undefined,
|
|
95
|
-
template: loop.template,
|
|
85
|
+
lazyRow,
|
|
86
|
+
template: core.template,
|
|
96
87
|
reactiveEffects: hasReactiveEffects
|
|
97
88
|
? buildReactiveEffectsPlan({
|
|
98
89
|
attrs: loop.bindings.reactiveAttrs,
|
|
@@ -100,11 +91,12 @@ export function buildBranchLoopPlan(
|
|
|
100
91
|
conditionals: loop.bindings.conditionals,
|
|
101
92
|
loopParam: loop.param,
|
|
102
93
|
loopParamBindings: loop.paramBindings,
|
|
94
|
+
loopIndex: loop.index,
|
|
103
95
|
profileComponentName,
|
|
104
96
|
})
|
|
105
97
|
: null,
|
|
106
98
|
eventDelegation: buildBranchLoopDelegationPlan(loop, cv, profileComponentName),
|
|
107
|
-
childRefs: buildChildRefBindings(loop.bindings.refs, loop.param, loop.paramBindings),
|
|
99
|
+
childRefs: buildChildRefBindings(loop.bindings.refs, loop.param, loop.paramBindings, loop.index),
|
|
108
100
|
preambleRegions,
|
|
109
101
|
bodyIsMultiRoot: loop.bodyIsMultiRoot ?? false,
|
|
110
102
|
profileLoopId: profileComponentName ? `${profileComponentName}#binding:${containerSlotId}` : undefined,
|