@barefootjs/jsx 0.34.0 → 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/analyzer.d.ts.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +344 -215
- 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/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__/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/analyzer.ts +4 -0
- package/src/index.ts +8 -2
- 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/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,243 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Escape-by-default for client-JS HTML template strings (#2795).
|
|
3
|
+
*
|
|
4
|
+
* Every builder in `html-template.ts` assembles a JS template literal whose
|
|
5
|
+
* `${...}` holes are evaluated in the browser and parsed as HTML
|
|
6
|
+
* (`innerHTML`). Whether a hole's value is escaped used to be an OPT-IN each
|
|
7
|
+
* builder remembered separately — `escapeInClientTemplate` on the IR node, a
|
|
8
|
+
* pre-baked `escapeText(...)` string in `templateExpr`, an
|
|
9
|
+
* `escapeLeafTextExpressions` rewrite pass, and a hand-rolled `escapeText`
|
|
10
|
+
* in `buildPreambleRegionPlans` — and a forgotten opt-in shipped raw text
|
|
11
|
+
* twice (#1694, #2765/#2792). This module inverts that:
|
|
12
|
+
*
|
|
13
|
+
* - `SafeHtml` is a compile-time nominal brand (the `TsxSourceText`
|
|
14
|
+
* precedent, `types.ts`) on a JS EXPRESSION STRING meaning "when this
|
|
15
|
+
* evaluates in the client template, the result is HTML that is safe to
|
|
16
|
+
* concatenate in". Nothing outside this module can construct one, so a
|
|
17
|
+
* `SafeHtml` value is the compiler vouching for itself — unlike the
|
|
18
|
+
* runtime `bfMarkup()` brand (`@barefootjs/client/runtime`), which
|
|
19
|
+
* vouches for a value the compiler never saw.
|
|
20
|
+
* - `interp` is the ONLY way to turn a value into a `${...}` hole, and it
|
|
21
|
+
* accepts `SafeHtml` only. A builder holding a plain `string` cannot
|
|
22
|
+
* splice it: it must either go through a named producer below (each of
|
|
23
|
+
* which documents WHY its output is safe) or through `spliceChildValue`,
|
|
24
|
+
* the door for a child-position expression, whose default is
|
|
25
|
+
* `escapeText(...)`.
|
|
26
|
+
*
|
|
27
|
+
* The runtime dispatch that already existed for values the compiler cannot
|
|
28
|
+
* classify statically is unchanged: a branch-slot value still goes through
|
|
29
|
+
* `__bfSlot` (may be a live Node), a claim-plan `'markup'` slot still goes
|
|
30
|
+
* through `escapeTextOrMarkup` (may be a `bfMarkup()`-branded prop). Those
|
|
31
|
+
* are producers here too — they escape internally, so their output is safe
|
|
32
|
+
* by construction.
|
|
33
|
+
*
|
|
34
|
+
* Attribute-value holes (`escapeAttr`, `spreadAttrs`) are a separate,
|
|
35
|
+
* always-on path in `html-template.ts` and are not routed through this
|
|
36
|
+
* module; `client-template-escape-soundness.test.ts` pins their count.
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
import type { IRExpression } from '../types.ts'
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* A JS expression string that evaluates, in the client template, to HTML
|
|
43
|
+
* safe to splice raw. Branded at construction by the producers below; only
|
|
44
|
+
* `interp` consumes it.
|
|
45
|
+
*/
|
|
46
|
+
export type SafeHtml = string & { readonly __safeHtmlBrand: unique symbol }
|
|
47
|
+
|
|
48
|
+
/** The single place the brand is applied. Module-private on purpose. */
|
|
49
|
+
function safeHtml(expr: string): SafeHtml {
|
|
50
|
+
return expr as SafeHtml
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Emit a `${...}` hole. The parameter type is the whole mechanism: a builder
|
|
55
|
+
* that has only a `string` in hand cannot call this, so "forgot to escape"
|
|
56
|
+
* is a type error at the splice, not a runtime surprise in the browser.
|
|
57
|
+
*/
|
|
58
|
+
export function interp(span: SafeHtml): string {
|
|
59
|
+
return `\${${span}}`
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ---------------------------------------------------------------------------
|
|
63
|
+
// Producers — escaping forms. Each wraps a runtime helper from
|
|
64
|
+
// `@barefootjs/client/runtime` (see `imports.ts`'s allowlist) that escapes
|
|
65
|
+
// its own input.
|
|
66
|
+
// ---------------------------------------------------------------------------
|
|
67
|
+
|
|
68
|
+
/** `escapeText(expr)` — the default for any child-position value. */
|
|
69
|
+
export function escapedText(expr: string): SafeHtml {
|
|
70
|
+
return safeHtml(`escapeText(${expr})`)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* `escapeTextOrMarkup(expr)` — `escapeText`'s strict superset that unwraps a
|
|
75
|
+
* `bfMarkup()`-branded value raw (#2651). Used only for a slot whose
|
|
76
|
+
* REACTIVE claim writer is `kind: 'markup'` (`markup-slots.ts`), so the
|
|
77
|
+
* initial render and the update agree on whether the slot may carry a
|
|
78
|
+
* compiler-built JSX prop value.
|
|
79
|
+
*/
|
|
80
|
+
export function escapedTextOrMarkup(expr: string): SafeHtml {
|
|
81
|
+
return safeHtml(`escapeTextOrMarkup(${expr})`)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* `__bfSlot(expr, slots)` — a conditional-branch `template()` value. The
|
|
86
|
+
* runtime returns raw `<!--bf-slot:N-->` markers for live Nodes (spliced
|
|
87
|
+
* back by `insert()`) and `escapeText`s every string itself
|
|
88
|
+
* (`branch-slot.ts`). Wrapping this in another escape corrupts the markers
|
|
89
|
+
* and drops slotted content (the #1694 regression) — hence a producer, not
|
|
90
|
+
* an input to `escapedText`.
|
|
91
|
+
*/
|
|
92
|
+
export function branchSlotValue(expr: string, slotsVar: string): SafeHtml {
|
|
93
|
+
return safeHtml(`__bfSlot(${expr}, ${slotsVar})`)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ---------------------------------------------------------------------------
|
|
97
|
+
// Producers — already-HTML forms. The value is markup the compiler or
|
|
98
|
+
// runtime built (and escaped piecewise) BEFORE it reaches the hole.
|
|
99
|
+
// ---------------------------------------------------------------------------
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* `markupOrEmpty(expr)` — a bare `{children}` passthrough (#2775). The value
|
|
103
|
+
* is the HTML string `materializeComponent` joined from the caller's
|
|
104
|
+
* children, or `undefined` when none were passed; the helper's entire job is
|
|
105
|
+
* the nullish case. Never escaped: escaping would render real child markup
|
|
106
|
+
* as visible `<span>` text.
|
|
107
|
+
*/
|
|
108
|
+
export function childrenMarkup(expr: string): SafeHtml {
|
|
109
|
+
return safeHtml(`markupOrEmpty(${expr})`)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Stage 3 / D4 — an element-array child (`{out}`) built by an arbitrary
|
|
114
|
+
* `.map()` preamble is an array of compiled-leaf HTML strings; join it
|
|
115
|
+
* rather than let `${[...]}` `String`-comma-collapse it. Each leaf's own
|
|
116
|
+
* holes already went through `interp`, so the concatenation is exactly as
|
|
117
|
+
* safe as any other compiler-emitted fragment. Also used by
|
|
118
|
+
* `buildPreambleRegionPlans` for the region-patch effect's value so the
|
|
119
|
+
* re-render matches the row template byte-for-byte.
|
|
120
|
+
*/
|
|
121
|
+
export function joinedMarkup(expr: string): SafeHtml {
|
|
122
|
+
return safeHtml(`Array.isArray(${expr}) ? ${expr}.join('') : (${expr} ?? '')`)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* `renderChild('Name', props, ...scopeArgs)` — a child component rendered
|
|
127
|
+
* from its registered template at runtime. Its output is that component's
|
|
128
|
+
* own compiled template, whose holes went through this module in turn.
|
|
129
|
+
*/
|
|
130
|
+
export function renderChildCall(registryName: string, propsExpr: string, tailArgs: string): SafeHtml {
|
|
131
|
+
return safeHtml(`renderChild('${registryName}', ${propsExpr}${tailArgs})`)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* `dangerouslySetInnerHTML={{ __html: E }}` — the intentional, React-style,
|
|
136
|
+
* author-facing escape hatch, and the ONLY one: the element's content is
|
|
137
|
+
* whatever `E.__html` holds, unescaped by design (mirroring every SSR
|
|
138
|
+
* adapter's native handling). The name carries the warning so an audit of
|
|
139
|
+
* raw-markup producers (`grep dangerousInnerHtml`) finds it.
|
|
140
|
+
*/
|
|
141
|
+
export function dangerousInnerHtml(expr: string): SafeHtml {
|
|
142
|
+
return safeHtml(`((${expr}) ?? {}).__html ?? ''`)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* `cond ? \`whenTrue\` : \`whenFalse\`` — both branches are compiled
|
|
147
|
+
* template-literal BODIES (already-assembled HTML with `interp`ed holes),
|
|
148
|
+
* not values; the condition only selects between them.
|
|
149
|
+
*/
|
|
150
|
+
export function conditionalMarkup(condition: string, whenTrue: string, whenFalse: string): SafeHtml {
|
|
151
|
+
return safeHtml(`${condition} ? \`${whenTrue}\` : \`${whenFalse}\``)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* `array.method(params => body).join('')` — a loop rendered inline. `body`
|
|
156
|
+
* is either a compiled row template literal (\`...\`), a preamble block
|
|
157
|
+
* returning one, or a flatMap descriptor body rendered through
|
|
158
|
+
* `renderPreamble`; every hole inside it went through `interp`.
|
|
159
|
+
*/
|
|
160
|
+
export function mappedRowsMarkup(arrayExpr: string, method: string, params: string, body: string): SafeHtml {
|
|
161
|
+
return safeHtml(`${arrayExpr}.${method}(${params} => ${body}).join('')`)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** The empty string literal — a deferred placeholder the init effect fills (#1128). */
|
|
165
|
+
export const EMPTY_MARKUP: SafeHtml = safeHtml("''")
|
|
166
|
+
|
|
167
|
+
// ---------------------------------------------------------------------------
|
|
168
|
+
// The door for a child-position expression node.
|
|
169
|
+
// ---------------------------------------------------------------------------
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Recognizes a JSX child-position expression that is exactly a reference to
|
|
173
|
+
* the reserved `children` prop — bare `children` (destructured) or
|
|
174
|
+
* `<receiver>.children` for any single-identifier receiver (`props.children`,
|
|
175
|
+
* a custom props-param name, a loop-scoped alias closing over props, ...).
|
|
176
|
+
*
|
|
177
|
+
* Deliberately LOOSER than `isTransparentFragment` (`jsx-to-ir.ts`), which
|
|
178
|
+
* runs on the TS AST with the analyzer-resolved props name in hand. This
|
|
179
|
+
* layer works on IR text with no analyzer, so any single-identifier receiver
|
|
180
|
+
* is the available approximation. The looseness costs nothing measurable: an
|
|
181
|
+
* unrelated `.children` member (a tree node's own `children` array, say) is a
|
|
182
|
+
* reactive member expression that gets a `slotId` and takes the slotted arm
|
|
183
|
+
* of `spliceChildValue` before this predicate is consulted.
|
|
184
|
+
*/
|
|
185
|
+
function isChildrenPassthroughExpr(expr: string): boolean {
|
|
186
|
+
return /^([A-Za-z_$][\w$]*\.)?children$/.test(expr.trim())
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export interface ChildSpliceContext {
|
|
190
|
+
/**
|
|
191
|
+
* Set while emitting inside a conditional-branch `template()` arrow
|
|
192
|
+
* (`irToHtmlTemplate`'s `branchSlotsVar`): every value in that context is
|
|
193
|
+
* routed through `__bfSlot`, which owns its own coercion.
|
|
194
|
+
*/
|
|
195
|
+
readonly branchSlotsVar?: string
|
|
196
|
+
/**
|
|
197
|
+
* Slot ids whose reactive claim writer is `kind: 'markup'`
|
|
198
|
+
* (`markupSlotIdsOf(ctx)`, `markup-slots.ts`). Absent for the loop-row /
|
|
199
|
+
* branch-row builders, whose reactive twin is a `kind: 'text'` writer
|
|
200
|
+
* (`String(...)` into `nodeValue`) — plain `escapeText` is the consistent
|
|
201
|
+
* choice there, and it is also the choice that never unwraps a
|
|
202
|
+
* `bfMarkup()`-shaped object arriving in JSON-sourced loop data.
|
|
203
|
+
*/
|
|
204
|
+
readonly markupSlotIds?: ReadonlySet<string>
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* THE door for a child-position `IRExpression` value entering a client HTML
|
|
209
|
+
* template. Takes the caller's already-wrapped/transformed expression text
|
|
210
|
+
* (`wrapExpr` / `transformExpr` output) and decides, in exactly one place,
|
|
211
|
+
* which producer it goes through. The last arm is the default, and it
|
|
212
|
+
* escapes: there is no arm that returns `valueExpr` unwrapped.
|
|
213
|
+
*
|
|
214
|
+
* Arms, first match wins:
|
|
215
|
+
* 1. `joinArrayChild` → `joinedMarkup` (array of compiled leaves)
|
|
216
|
+
* 2. `branchSlotsVar` → `branchSlotValue` (runtime coercion, may be a Node)
|
|
217
|
+
* 3. `slotId` ∈ markupSlotIds → `escapedTextOrMarkup`
|
|
218
|
+
* 4. `slotId` → `escapedText`
|
|
219
|
+
* 5. `{children}` passthrough → `childrenMarkup` (bare splice only, #2775/#2786)
|
|
220
|
+
* 6. otherwise → `escapedText`
|
|
221
|
+
*
|
|
222
|
+
* Arm 5 tests BOTH `node.expr` (the original source text, stable across
|
|
223
|
+
* builders) and the caller's resolved form (parens stripped): a
|
|
224
|
+
* destructured-and-renamed children (`const { children: kids } = props`)
|
|
225
|
+
* reads `kids` in the source and `(_p.children)` after substitution, and
|
|
226
|
+
* either alone misses one of the two shapes (#2786).
|
|
227
|
+
*/
|
|
228
|
+
export function spliceChildValue(
|
|
229
|
+
node: Pick<IRExpression, 'expr' | 'slotId' | 'joinArrayChild'>,
|
|
230
|
+
valueExpr: string,
|
|
231
|
+
cx: ChildSpliceContext,
|
|
232
|
+
): SafeHtml {
|
|
233
|
+
if (node.joinArrayChild) return joinedMarkup(valueExpr)
|
|
234
|
+
if (cx.branchSlotsVar) return branchSlotValue(valueExpr, cx.branchSlotsVar)
|
|
235
|
+
if (node.slotId) {
|
|
236
|
+
return cx.markupSlotIds?.has(node.slotId) ? escapedTextOrMarkup(valueExpr) : escapedText(valueExpr)
|
|
237
|
+
}
|
|
238
|
+
const resolved = valueExpr.trim().replace(/^\(+|\)+$/g, '')
|
|
239
|
+
if (isChildrenPassthroughExpr(node.expr) || isChildrenPassthroughExpr(resolved)) {
|
|
240
|
+
return childrenMarkup(valueExpr)
|
|
241
|
+
}
|
|
242
|
+
return escapedText(valueExpr)
|
|
243
|
+
}
|
|
@@ -581,17 +581,63 @@ function renderLoopBindingAccess(b: LoopParamBinding, base: string): string {
|
|
|
581
581
|
* binding name is rewritten to `__bfItem()${path}` instead of wrapping the
|
|
582
582
|
* raw pattern text. `paramName` is ignored in that case — destructured
|
|
583
583
|
* callbacks never expose the pattern itself as a local.
|
|
584
|
-
|
|
585
|
-
|
|
584
|
+
*
|
|
585
|
+
* `indexParam` (#2859): the same `.map()` callback's index parameter
|
|
586
|
+
* (`(item, i) => ...`'s `i`), when the loop declares one. `mapArray`/
|
|
587
|
+
* `mapArrayAnchored` hand `renderItem` an INDEX ACCESSOR (mirroring the item
|
|
588
|
+
* accessor) precisely so a same-key reorder — which never re-invokes
|
|
589
|
+
* `renderItem`, only pushes fresh values through the item/index signals —
|
|
590
|
+
* can keep a row's index-derived output live. A bare reference to
|
|
591
|
+
* `indexParam` is therefore rewritten the same way as the item param; a
|
|
592
|
+
* caller that never threads an index through (e.g. no second callback
|
|
593
|
+
* param) simply omits it and behavior is unchanged. Rewritten as a SEPARATE
|
|
594
|
+
* pass after the item/bindings rewrite so an index name that happens to
|
|
595
|
+
* collide with a destructured binding name is a no-op here (the binding
|
|
596
|
+
* rewrite already consumed it; `!== paramName` below only guards the plain,
|
|
597
|
+
* non-destructured case, since `bindings` and `paramName` are mutually
|
|
598
|
+
* exclusive inputs to the item rewrite above).
|
|
599
|
+
*/
|
|
600
|
+
export function wrapLoopParamAsAccessor(
|
|
601
|
+
expr: string,
|
|
602
|
+
paramName: string,
|
|
603
|
+
bindings?: readonly LoopParamBinding[],
|
|
604
|
+
indexParam?: string | null,
|
|
605
|
+
): string {
|
|
606
|
+
let result: string
|
|
586
607
|
if (bindings && bindings.length > 0) {
|
|
587
|
-
|
|
608
|
+
result = rewriteLoopBindingRefs(expr, bindings, '__bfItem()')
|
|
609
|
+
} else {
|
|
610
|
+
// `paramName` is a legal JS identifier and may itself start with `$`
|
|
611
|
+
// (`$1`, `$&`, …) — a plain string replacement would risk `String.replace`
|
|
612
|
+
// reading those as backreference/whole-match sequences, so use a replacer
|
|
613
|
+
// function to insert the accessor text literally (#2592).
|
|
614
|
+
const re = new RegExp(`${ID_BOUNDARY_BEFORE}${escapeIdentifierForRegex(paramName)}(?!\\s*\\()(?!-)${ID_BOUNDARY_AFTER}`, 'gu')
|
|
615
|
+
result = replaceInExprContexts(expr, re, () => `${paramName}()`)
|
|
616
|
+
}
|
|
617
|
+
if (indexParam && indexParam !== paramName) {
|
|
618
|
+
result = wrapIndexParamAsAccessor(result, indexParam)
|
|
588
619
|
}
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
620
|
+
return result
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
/**
|
|
624
|
+
* The index-only half of `wrapLoopParamAsAccessor` — same regex shape, own
|
|
625
|
+
* identifier. Factored out so `wrapLoopParamAsAccessor` can run it as a
|
|
626
|
+
* second pass regardless of which item-rewrite branch (bindings vs. plain
|
|
627
|
+
* param) ran first (#2859).
|
|
628
|
+
*
|
|
629
|
+
* Also exported standalone for `build-loop.ts`/`build-branch-loop.ts`: a
|
|
630
|
+
* plain loop's `template`/`mapPreambleWrapped` are built ONCE, before lazy
|
|
631
|
+
* eligibility is known, because the lazy row plan reuses them verbatim when
|
|
632
|
+
* this loop turns out to be lazy-eligible (`mapArrayLazy`'s `createRow`
|
|
633
|
+
* still hands the row a plain index NUMBER, never an accessor). So those two
|
|
634
|
+
* builders wrap item references up front (safe either way) and apply this
|
|
635
|
+
* index-only pass afterward, ONLY once `buildLazyRowPlan` confirms the loop
|
|
636
|
+
* is NOT going lazy.
|
|
637
|
+
*/
|
|
638
|
+
export function wrapIndexParamAsAccessor(expr: string, indexParam: string): string {
|
|
639
|
+
const re = new RegExp(`${ID_BOUNDARY_BEFORE}${escapeIdentifierForRegex(indexParam)}(?!\\s*\\()(?!-)${ID_BOUNDARY_AFTER}`, 'gu')
|
|
640
|
+
return replaceInExprContexts(expr, re, () => `${indexParam}()`)
|
|
595
641
|
}
|
|
596
642
|
|
|
597
643
|
/**
|
|
@@ -718,6 +764,8 @@ export function substituteLoopBindings(
|
|
|
718
764
|
export interface LoopParamSpec {
|
|
719
765
|
param: string
|
|
720
766
|
bindings?: readonly LoopParamBinding[]
|
|
767
|
+
/** This loop's index parameter name, when it declares one (#2859). */
|
|
768
|
+
index?: string | null
|
|
721
769
|
}
|
|
722
770
|
|
|
723
771
|
/**
|
|
@@ -747,7 +795,7 @@ export function wrapExprWithLoopParams(expr: string, loopParams?: ReadonlyArray<
|
|
|
747
795
|
let result = expr
|
|
748
796
|
for (const p of loopParams) {
|
|
749
797
|
const spec = typeof p === 'string' ? { param: p } : p
|
|
750
|
-
result = wrapLoopParamAsAccessor(result, spec.param, spec.bindings)
|
|
798
|
+
result = wrapLoopParamAsAccessor(result, spec.param, spec.bindings, spec.index)
|
|
751
799
|
}
|
|
752
800
|
return result
|
|
753
801
|
}
|
package/src/jsx-to-ir.ts
CHANGED
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
type IRIfStatement,
|
|
19
19
|
type IRProvider,
|
|
20
20
|
type IRAttribute,
|
|
21
|
+
type ExpressionAttr,
|
|
21
22
|
type IREvent,
|
|
22
23
|
type IRProp,
|
|
23
24
|
type AttrValue,
|
|
@@ -606,12 +607,14 @@ function collectBranchLocalPropRefsViaSubstitution(
|
|
|
606
607
|
let acc: Set<string> | undefined
|
|
607
608
|
function visit(n: ts.Node, parent?: ts.Node) {
|
|
608
609
|
if (ts.isIdentifier(n) && propDepsMap!.has(n.text)) {
|
|
609
|
-
// Same skip rules as `collectAstPropRefs` — only value
|
|
610
|
-
//
|
|
610
|
+
// Same skip rules as `collectAstPropRefs` — only value positions
|
|
611
|
+
// count, not object keys / property-access names. A shorthand
|
|
612
|
+
// property (`{ page }`) IS a value reference (#2828) — its name
|
|
613
|
+
// is simultaneously the key and the value — so it participates
|
|
614
|
+
// here too, same as `collectAstPropRefs`.
|
|
611
615
|
const isObjectKey = parent && ts.isPropertyAssignment(parent) && parent.name === n
|
|
612
|
-
const isShorthand = parent && ts.isShorthandPropertyAssignment(parent) && parent.name === n
|
|
613
616
|
const isAccessName = parent && ts.isPropertyAccessExpression(parent) && parent.name === n
|
|
614
|
-
if (!isObjectKey && !
|
|
617
|
+
if (!isObjectKey && !isAccessName) {
|
|
615
618
|
const deps = propDepsMap!.get(n.text)
|
|
616
619
|
if (deps && deps.size > 0) {
|
|
617
620
|
if (!acc) acc = new Set()
|
|
@@ -1484,15 +1487,13 @@ function lowerFormControlValueSsr(
|
|
|
1484
1487
|
|
|
1485
1488
|
if (tagName === 'textarea') {
|
|
1486
1489
|
if (children.length > 0) return
|
|
1490
|
+
// A plain text child: SSR engines escape text children natively, and
|
|
1491
|
+
// the client builders escape every child-position value by default
|
|
1492
|
+
// (`ir-to-client-js/safe-html.ts`, #2795) — nothing to mark here.
|
|
1487
1493
|
children.push({
|
|
1488
1494
|
type: 'expression',
|
|
1489
1495
|
expr,
|
|
1490
|
-
|
|
1491
|
-
// engines escape text children natively.
|
|
1492
|
-
templateExpr: `escapeText(${templateExpr ?? expr})`,
|
|
1493
|
-
// Init-scope builders can't just swap in `templateExpr` (its `_p.`
|
|
1494
|
-
// binding differs) — see `escapeInClientTemplate`'s docstring.
|
|
1495
|
-
escapeInClientTemplate: true,
|
|
1496
|
+
templateExpr,
|
|
1496
1497
|
typeInfo: null,
|
|
1497
1498
|
reactive: false,
|
|
1498
1499
|
slotId: null,
|
|
@@ -1502,7 +1503,7 @@ function lowerFormControlValueSsr(
|
|
|
1502
1503
|
return
|
|
1503
1504
|
}
|
|
1504
1505
|
|
|
1505
|
-
const selectedForLiteral = (optValue: string):
|
|
1506
|
+
const selectedForLiteral = (optValue: string): ExpressionAttr =>
|
|
1506
1507
|
AttrValueOf.expression(
|
|
1507
1508
|
`(${expr}) === ${JSON.stringify(optValue)}`,
|
|
1508
1509
|
templateExpr !== undefined
|
|
@@ -1519,35 +1520,129 @@ function lowerFormControlValueSsr(
|
|
|
1519
1520
|
// `classifyLazyBinding` already know how to place into both `applyItem`
|
|
1520
1521
|
// (row changed) and `applyOuter` (controlled value changed) — no new loop
|
|
1521
1522
|
// machinery, just one more per-row reactive attribute (#2466).
|
|
1522
|
-
const selectedForExpr = (optExpr: string, optTemplateExpr: string | undefined):
|
|
1523
|
+
const selectedForExpr = (optExpr: string, optTemplateExpr: string | undefined): ExpressionAttr =>
|
|
1523
1524
|
AttrValueOf.expression(
|
|
1524
1525
|
`(${expr}) === (${optExpr})`,
|
|
1525
1526
|
templateExpr !== undefined || optTemplateExpr !== undefined
|
|
1526
1527
|
? { templateExpr: `(${templateExpr ?? expr}) === (${optTemplateExpr ?? optExpr})` }
|
|
1527
1528
|
: undefined,
|
|
1528
1529
|
)
|
|
1530
|
+
// #2758: a bound value outside the option set makes SSR and hydration
|
|
1531
|
+
// disagree about what "no match" means. SSR (below) never marks any
|
|
1532
|
+
// `<option>` `selected`, so the browser falls back to its own default —
|
|
1533
|
+
// the FIRST option. Hydration's controlled-value effect then assigns
|
|
1534
|
+
// `select.value` directly (`emitValueUpdateStatements` in
|
|
1535
|
+
// `ir-to-client-js/emit-reactive.ts`), and a value matching no option
|
|
1536
|
+
// yields `selectedIndex = -1` (nothing selected). So the page renders
|
|
1537
|
+
// showing the first option, and the moment hydration runs the selection
|
|
1538
|
+
// visibly disappears.
|
|
1539
|
+
//
|
|
1540
|
+
// Resolution: treat the client's reading as correct ("no match" means no
|
|
1541
|
+
// selection) and make SSR able to express it. When every `<option>` here
|
|
1542
|
+
// is statically enumerable (no `.map()` loop, no conditional/component/
|
|
1543
|
+
// slot child, no author-supplied `selected`), collect each option's match
|
|
1544
|
+
// condition and, for a single-selection `<select>` (not `multiple`, not
|
|
1545
|
+
// `size` > 1 — a list-box already shows no implicit default, so SSR and
|
|
1546
|
+
// hydration already agree there), inject a hidden `disabled` placeholder
|
|
1547
|
+
// option whose own `selected` is the negation of every real option's
|
|
1548
|
+
// condition ORed together. It is only ever actually selected when nothing
|
|
1549
|
+
// else matched, so it changes nothing for the common case where the value
|
|
1550
|
+
// does match an option.
|
|
1551
|
+
//
|
|
1552
|
+
// Bails (leaves prior behaviour) whenever the option set isn't fully
|
|
1553
|
+
// statically known — a `.map()`-rendered list has an unknown number of
|
|
1554
|
+
// rows at compile time, so there is no finite OR to negate.
|
|
1555
|
+
const matchConditions: ExpressionAttr[] = []
|
|
1556
|
+
let optionSetIsDynamic = false
|
|
1557
|
+
|
|
1529
1558
|
const distribute = (nodes: IRNode[]): void => {
|
|
1530
1559
|
for (const n of nodes) {
|
|
1560
|
+
if (n.type === 'text') continue
|
|
1531
1561
|
if (n.type === 'element' && n.tag === 'option') {
|
|
1532
|
-
if (n.attrs.some(a => a.name === 'selected'))
|
|
1562
|
+
if (n.attrs.some(a => a.name === 'selected')) {
|
|
1563
|
+
optionSetIsDynamic = true
|
|
1564
|
+
continue
|
|
1565
|
+
}
|
|
1533
1566
|
const optValue = n.attrs.find(a => a.name === 'value')
|
|
1534
|
-
if (!optValue)
|
|
1567
|
+
if (!optValue) {
|
|
1568
|
+
// No `value` attr: the browser falls back to the option's text
|
|
1569
|
+
// content as its implicit value, which this pass can't compare
|
|
1570
|
+
// against without re-deriving that text — bail rather than
|
|
1571
|
+
// silently dropping this option out of the "no match" OR.
|
|
1572
|
+
optionSetIsDynamic = true
|
|
1573
|
+
continue
|
|
1574
|
+
}
|
|
1535
1575
|
if (optValue.value.kind === 'literal') {
|
|
1536
|
-
|
|
1576
|
+
const selected = selectedForLiteral(optValue.value.value)
|
|
1577
|
+
n.attrs.push({ name: 'selected', value: selected, loc: n.loc })
|
|
1578
|
+
matchConditions.push(selected)
|
|
1537
1579
|
} else if (optValue.value.kind === 'expression') {
|
|
1538
1580
|
const selected = selectedForExpr(optValue.value.expr, optValue.value.templateExpr)
|
|
1539
1581
|
n.attrs.push({ name: 'selected', value: selected, loc: n.loc })
|
|
1582
|
+
matchConditions.push(selected)
|
|
1583
|
+
} else {
|
|
1584
|
+
// A `value` shape neither builder above can compare (e.g. a
|
|
1585
|
+
// structured `template` part) — same bail, not a silent drop.
|
|
1586
|
+
optionSetIsDynamic = true
|
|
1540
1587
|
}
|
|
1541
|
-
} else if (
|
|
1542
|
-
n.
|
|
1543
|
-
|
|
1544
|
-
(
|
|
1545
|
-
|
|
1588
|
+
} else if (n.type === 'fragment' || (n.type === 'element' && n.tag === 'optgroup')) {
|
|
1589
|
+
distribute(n.children)
|
|
1590
|
+
} else if (n.type === 'loop') {
|
|
1591
|
+
// A `.map()`-rendered option list still needs its per-row `selected`
|
|
1592
|
+
// distributed (#2466, preserved below) — but its row count is
|
|
1593
|
+
// unknown at compile time, so it can't feed the "no match" OR.
|
|
1594
|
+
optionSetIsDynamic = true
|
|
1546
1595
|
distribute(n.children)
|
|
1596
|
+
} else {
|
|
1597
|
+
// Conditionals/if-statements, components, slots — an option here
|
|
1598
|
+
// may or may not exist at runtime, so the option set can't be
|
|
1599
|
+
// exhaustively enumerated at compile time.
|
|
1600
|
+
optionSetIsDynamic = true
|
|
1547
1601
|
}
|
|
1548
1602
|
}
|
|
1549
1603
|
}
|
|
1550
1604
|
distribute(children)
|
|
1605
|
+
|
|
1606
|
+
if (optionSetIsDynamic || matchConditions.length === 0) return
|
|
1607
|
+
if (isMultiSelection(attrs)) return
|
|
1608
|
+
|
|
1609
|
+
const orExpr = matchConditions.map(c => `(${c.expr})`).join(' || ')
|
|
1610
|
+
const orTemplateExpr = matchConditions.some(c => c.templateExpr !== undefined)
|
|
1611
|
+
? matchConditions.map(c => `(${c.templateExpr ?? c.expr})`).join(' || ')
|
|
1612
|
+
: undefined
|
|
1613
|
+
|
|
1614
|
+
children.unshift({
|
|
1615
|
+
type: 'element',
|
|
1616
|
+
tag: 'option',
|
|
1617
|
+
attrs: [
|
|
1618
|
+
{ name: 'value', value: AttrValueOf.literal(''), loc: valueAttr.loc },
|
|
1619
|
+
{ name: 'disabled', value: AttrValueOf.booleanAttr(), loc: valueAttr.loc },
|
|
1620
|
+
{ name: 'hidden', value: AttrValueOf.booleanAttr(), loc: valueAttr.loc },
|
|
1621
|
+
{
|
|
1622
|
+
name: 'selected',
|
|
1623
|
+
value: AttrValueOf.expression(
|
|
1624
|
+
`!(${orExpr})`,
|
|
1625
|
+
orTemplateExpr !== undefined ? { templateExpr: `!(${orTemplateExpr})` } : undefined,
|
|
1626
|
+
),
|
|
1627
|
+
loc: valueAttr.loc,
|
|
1628
|
+
},
|
|
1629
|
+
],
|
|
1630
|
+
events: [],
|
|
1631
|
+
ref: null,
|
|
1632
|
+
children: [],
|
|
1633
|
+
slotId: null,
|
|
1634
|
+
needsScope: false,
|
|
1635
|
+
loc: valueAttr.loc,
|
|
1636
|
+
})
|
|
1637
|
+
}
|
|
1638
|
+
|
|
1639
|
+
/** `<select multiple>` or `size` > 1: a list box has no implicit default
|
|
1640
|
+
* selection, so SSR (no option `selected`) and hydration (`selectedIndex`
|
|
1641
|
+
* left at whatever it was) already agree without a placeholder option. */
|
|
1642
|
+
function isMultiSelection(attrs: IRAttribute[]): boolean {
|
|
1643
|
+
if (attrs.some(a => a.name === 'multiple')) return true
|
|
1644
|
+
const sizeAttr = attrs.find(a => a.name === 'size')
|
|
1645
|
+
return sizeAttr?.value.kind === 'literal' && Number(sizeAttr.value.value) > 1
|
|
1551
1646
|
}
|
|
1552
1647
|
|
|
1553
1648
|
function transformHtmlElement(
|
package/src/prop-rewrite.ts
CHANGED
|
@@ -105,9 +105,14 @@ function isNonValuePosition(n: ts.Identifier, parent: ts.Node | undefined): bool
|
|
|
105
105
|
/**
|
|
106
106
|
* Walk an AST node for destructured-prop value references and add
|
|
107
107
|
* each found name to `out`. Same skip rules as the rewrite path —
|
|
108
|
-
* object-literal keys,
|
|
109
|
-
*
|
|
110
|
-
*
|
|
108
|
+
* object-literal keys, property-access names, and names shadowed by a
|
|
109
|
+
* binding inside `node` are excluded so only true value references get
|
|
110
|
+
* picked up. A shorthand property (`{ page }`) IS a value reference —
|
|
111
|
+
* its name is simultaneously the key and the value, and
|
|
112
|
+
* `applyScopedPropRefRewrite` below already expands it correctly
|
|
113
|
+
* (`{ page }` → `{ page: _p.page }`) — so it must be discovered here
|
|
114
|
+
* too, or the rewrite is never even attempted for text whose only prop
|
|
115
|
+
* reference is a shorthand one (#2828). Exported for callers that need
|
|
111
116
|
* the raw discovery set (e.g. the branch-local prop-dep cache from
|
|
112
117
|
* #1425).
|
|
113
118
|
*/
|
|
@@ -118,7 +123,6 @@ export function collectAstPropRefs(
|
|
|
118
123
|
): void {
|
|
119
124
|
walkWithScope(node, (n, parent, shadowed) => {
|
|
120
125
|
if (shadowed || !propNames.has(n.text)) return
|
|
121
|
-
if (parent && ts.isShorthandPropertyAssignment(parent) && parent.name === n) return
|
|
122
126
|
if (isNonValuePosition(n, parent)) return
|
|
123
127
|
out.add(n.text)
|
|
124
128
|
})
|
package/src/props-binding.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import ts from 'typescript'
|
|
2
|
-
import type { ParamInfo } from './types.ts'
|
|
2
|
+
import type { ConstantInfo, ParamInfo } from './types.ts'
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Authoritative IdentifierName classification for a destructure-pattern
|
|
@@ -69,6 +69,42 @@ export function buildPropAliasMap(params: readonly ParamInfo[]): Map<string, str
|
|
|
69
69
|
return map
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
+
/**
|
|
73
|
+
* Local-name → caller-facing-key map for a BARE-PROPS-form component
|
|
74
|
+
* (`function Foo(props: Props)`) whose BODY destructures a prop under a
|
|
75
|
+
* different local name (`const { children: kids } = props`) — the
|
|
76
|
+
* body-level twin of `buildPropAliasMap` above, which only sees
|
|
77
|
+
* PARAMETER-destructuring aliases (`{ n: count }`, tracked via
|
|
78
|
+
* `ParamInfo.sourceName`). For the bare-props form, `propsParams` comes
|
|
79
|
+
* from the TYPE annotation (`extractPropsFromTypeMembers`) and has no
|
|
80
|
+
* notion of a body-level rename at all — but the analyzer already
|
|
81
|
+
* resolves such a destructuring statement into an ordinary
|
|
82
|
+
* `localConstants` entry whose `parsed` is a plain `props.<key>` member
|
|
83
|
+
* read (`const { children: kids } = props` → a `kids` local const valued
|
|
84
|
+
* `props.children`). Recognizing that shape here is enough to answer
|
|
85
|
+
* "what caller-facing prop key does this local alias" without a second,
|
|
86
|
+
* dedicated AST walk of the destructuring pattern itself (#2788).
|
|
87
|
+
*
|
|
88
|
+
* Returns an empty map for a parameter-destructuring component
|
|
89
|
+
* (`propsObjectName === null`) — that shape's aliasing is already fully
|
|
90
|
+
* covered by `buildPropAliasMap`.
|
|
91
|
+
*/
|
|
92
|
+
export function resolveBodyDestructuredPropAliases(
|
|
93
|
+
localConstants: readonly ConstantInfo[],
|
|
94
|
+
propsObjectName: string | null,
|
|
95
|
+
): Map<string, string> {
|
|
96
|
+
const aliases = new Map<string, string>()
|
|
97
|
+
if (propsObjectName === null) return aliases
|
|
98
|
+
for (const c of localConstants) {
|
|
99
|
+
if (c.isModule) continue
|
|
100
|
+
const m = c.parsed
|
|
101
|
+
if (m?.kind === 'member' && !m.computed && m.object.kind === 'identifier' && m.object.name === propsObjectName) {
|
|
102
|
+
aliases.set(c.name, m.property)
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return aliases
|
|
106
|
+
}
|
|
107
|
+
|
|
72
108
|
/**
|
|
73
109
|
* The props-parameter shape needed to answer "is `name` an actual local
|
|
74
110
|
* binding introduced by the props parameter" — as opposed to
|
package/src/ssr-defaults.ts
CHANGED
|
@@ -31,6 +31,8 @@
|
|
|
31
31
|
|
|
32
32
|
import ts from 'typescript'
|
|
33
33
|
import { extractFreeIdentifiersFromNode } from './analyzer.ts'
|
|
34
|
+
import { resolveGetterAliases, collectAliasableGetterNames } from './ir-to-client-js/csr-substitute.ts'
|
|
35
|
+
import { resolveBodyDestructuredPropAliases } from './props-binding.ts'
|
|
34
36
|
import type { IRMetadata } from './types.ts'
|
|
35
37
|
|
|
36
38
|
/**
|
|
@@ -311,6 +313,49 @@ export function extractSsrDefaults(metadata: IRMetadata): Record<string, SsrDefa
|
|
|
311
313
|
bindings[memo.name] = value
|
|
312
314
|
}
|
|
313
315
|
|
|
316
|
+
// #2813: a bare local-const alias of a signal/memo getter
|
|
317
|
+
// (`const items__alias = items`) is a DIFFERENT identifier from the
|
|
318
|
+
// getter it aliases, but a template-stash adapter's `.map()` loop over
|
|
319
|
+
// `items__alias()` looks up the stash entry BY THAT NAME — which the
|
|
320
|
+
// loops above never create, since they only seed the getter's own
|
|
321
|
+
// name. Reuses `resolveGetterAliases` (the same alias-hop walker
|
|
322
|
+
// #2778's CSR-template fix introduced) rather than a third alias-hop
|
|
323
|
+
// walker: every alias of a getter seeded above gets the SAME entry
|
|
324
|
+
// under its own name, so the stash has both `items` and `items__alias`
|
|
325
|
+
// pointing at one value. Env-signal getters are excluded from the
|
|
326
|
+
// getter-name set (they're never given an `out` entry above — an
|
|
327
|
+
// adapter's own env-signal binding seeds them instead).
|
|
328
|
+
{
|
|
329
|
+
const getterNames = collectAliasableGetterNames(metadata.signals, metadata.memos)
|
|
330
|
+
for (const [alias, origin] of resolveGetterAliases(metadata.localConstants ?? [], (n) => getterNames.has(n))) {
|
|
331
|
+
if (alias in out) continue
|
|
332
|
+
out[alias] = out[origin]
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// #2788: a bare-props-form component (`function Foo(props: Props)`)
|
|
337
|
+
// whose BODY destructures a prop under a different local name
|
|
338
|
+
// (`const { children: kids } = props`) has no `ParamInfo.sourceName` to
|
|
339
|
+
// key off above — `propsParams` here comes from the TYPE annotation
|
|
340
|
+
// (`extractPropsFromTypeMembers`), which has no notion of a body-level
|
|
341
|
+
// rename. `resolveBodyDestructuredPropAliases` recognizes such a
|
|
342
|
+
// destructuring statement's `localConstants` shape; seeding the LOCAL
|
|
343
|
+
// name too (alongside the caller-facing key already seeded above) is
|
|
344
|
+
// enough to fix what a template-stash adapter's compiled output
|
|
345
|
+
// actually reads (`v[:kids]` / `$kids`). Without this, a stash-based
|
|
346
|
+
// adapter reading the renamed local sees an undefined/nil value instead
|
|
347
|
+
// of the prop — fatal under Perl strict mode (Mojolicious), a
|
|
348
|
+
// nonexistent Go struct field, or (on an adapter whose runtime
|
|
349
|
+
// tolerates a missing key) a silent wrong render the moment a caller
|
|
350
|
+
// actually supplies a value. Parameter-destructuring aliases (`{ n:
|
|
351
|
+
// count }`) need no such entry — `sourceName`/`name` already handle
|
|
352
|
+
// that shape via `callerPropName` above.
|
|
353
|
+
for (const [local, callerKey] of resolveBodyDestructuredPropAliases(metadata.localConstants ?? [], metadata.propsObjectName)) {
|
|
354
|
+
if (local in out) continue
|
|
355
|
+
const origin = out[callerKey]
|
|
356
|
+
if (origin) out[local] = origin
|
|
357
|
+
}
|
|
358
|
+
|
|
314
359
|
// Bare-props-arg safety net: the prop block above covers every prop
|
|
315
360
|
// *declared* on the props type, but `propsParams` can miss props read
|
|
316
361
|
// through an untyped / inline-typed `props` object. A signal / memo
|