@barefootjs/jsx 0.33.1 → 0.33.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/expression-parser.d.ts +14 -0
- package/dist/expression-parser.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +123 -39
- package/dist/ir-to-client-js/collect-elements.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 +7 -7
- package/dist/ir-to-client-js/html-template.d.ts.map +1 -1
- package/dist/ir-to-client-js/index.d.ts.map +1 -1
- package/dist/ir-to-client-js/prop-handling.d.ts +33 -0
- package/dist/ir-to-client-js/prop-handling.d.ts.map +1 -1
- package/dist/ir-to-client-js/reactivity.d.ts +5 -0
- package/dist/ir-to-client-js/reactivity.d.ts.map +1 -1
- package/dist/ir-to-client-js/rewrite-props-object.d.ts +36 -8
- package/dist/ir-to-client-js/rewrite-props-object.d.ts.map +1 -1
- package/dist/types.d.ts +14 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/__snapshots__/doc-examples.test.ts.snap +21 -2
- package/src/__tests__/binding-scope-ratchet.test.ts +5 -1
- package/src/__tests__/client-js-generation.test.ts +11 -0
- package/src/__tests__/issue-2723-prop-alias-reactivity.test.ts +124 -0
- package/src/__tests__/rewrite-props-object.test.ts +41 -4
- package/src/expression-parser.ts +26 -0
- package/src/index.ts +1 -1
- package/src/ir-to-client-js/collect-elements.ts +18 -25
- package/src/ir-to-client-js/emit-registration.ts +26 -7
- package/src/ir-to-client-js/generate-init.ts +1 -1
- package/src/ir-to-client-js/html-template.ts +8 -8
- package/src/ir-to-client-js/index.ts +6 -4
- package/src/ir-to-client-js/prop-handling.ts +94 -0
- package/src/ir-to-client-js/reactivity.ts +59 -0
- package/src/ir-to-client-js/rewrite-props-object.ts +50 -10
- package/src/jsx-to-ir.ts +47 -1
- package/src/types.ts +14 -0
|
@@ -10,6 +10,7 @@ import { PROPS_PARAM } from './utils.ts'
|
|
|
10
10
|
import { computeInlinability, toLegacyInlinability } from './compute-inlinability.ts'
|
|
11
11
|
import { canGenerateStaticTemplate, irToComponentTemplate, generateCsrTemplate, createStringProtector } from './html-template.ts'
|
|
12
12
|
import { nameForRegistryRef } from './component-scope.ts'
|
|
13
|
+
import { resolveRestSpreadNames } from './prop-handling.ts'
|
|
13
14
|
|
|
14
15
|
/**
|
|
15
16
|
* Resolve chained references within a constants map.
|
|
@@ -191,14 +192,29 @@ export function emitRegistrationAndHydration(
|
|
|
191
192
|
const propNamesForStaticCheck = new Set(ctx.propsParams.map((p) => p.name))
|
|
192
193
|
const { inlinableConstants, unsafeLocalNames } = inlinability ?? buildInlinableConstants(ctx, graph, _ir.root)
|
|
193
194
|
|
|
194
|
-
// Build rest spread names: these are rest/props spreads handled by
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
195
|
+
// Build rest spread names: these are rest/props spreads handled by
|
|
196
|
+
// applyRestAttrs, not spreadAttrs — #2723: includes any `const x__alias
|
|
197
|
+
// = x` hop onto the rest/props binding (see `resolveRestSpreadNames`'s
|
|
198
|
+
// docstring in prop-handling.ts).
|
|
199
|
+
const restSpreadNames = resolveRestSpreadNames(ctx)
|
|
198
200
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
201
|
+
// Two distinct shapes share the `comment: true` (proxy-scoped) def flag,
|
|
202
|
+
// but need OPPOSITE runtime treatment of the def's own scope id
|
|
203
|
+
// (component.ts's `materializeComponent`, #2722):
|
|
204
|
+
// - `root.type === 'fragment'`: a genuine fragment root. Its rendered
|
|
205
|
+
// markup carries NO scope id of its own (SSR moves it into the
|
|
206
|
+
// wrapping `<!--bf-scope:-->` comment, `wrapWithScopeComment` in
|
|
207
|
+
// hono-adapter.ts) — CSR mount must generate one just the same, or
|
|
208
|
+
// every nested `renderChild()` call loses the parent-prefixed naming
|
|
209
|
+
// `_parentScopeId` provides and falls back to a random per-child id
|
|
210
|
+
// (#1627's fallback), diverging from SSR/hydrate.
|
|
211
|
+
// - `root.type === 'component'`: the render-prop / "root is a single
|
|
212
|
+
// child call" case (#2649). The child's OWN markup already carries
|
|
213
|
+
// ITS OWN real scope id — the wrapping comment marks a scope with no
|
|
214
|
+
// DOM presence of its own, and `materializeComponent` must leave
|
|
215
|
+
// `scopeId` null so it doesn't stamp over (or duplicate) the child's.
|
|
216
|
+
const isFragmentRoot = _ir.root.type === 'fragment' && !!(_ir.root as IRFragment).needsScopeComment
|
|
217
|
+
const isCommentScope = isFragmentRoot || _ir.root.type === 'component'
|
|
202
218
|
|
|
203
219
|
// Build ComponentDef object for hydrate()
|
|
204
220
|
const defParts: string[] = [`init: init${name}`]
|
|
@@ -232,6 +248,9 @@ export function emitRegistrationAndHydration(
|
|
|
232
248
|
if (isCommentScope) {
|
|
233
249
|
defParts.push('comment: true')
|
|
234
250
|
}
|
|
251
|
+
if (isFragmentRoot) {
|
|
252
|
+
defParts.push('fragmentRoot: true')
|
|
253
|
+
}
|
|
235
254
|
|
|
236
255
|
const registryKey = nameForRegistryRef(name)
|
|
237
256
|
// When the registry key was file-scoped (`Name__<8hex>`, for a
|
|
@@ -97,7 +97,7 @@ export function generateInitFunction(
|
|
|
97
97
|
// into the analyzer / IR construction stage and introduce a
|
|
98
98
|
// `PropRewritten<T>` brand type so missing the rewrite becomes a
|
|
99
99
|
// compile-time error. ---
|
|
100
|
-
let generatedCode = rewritePropsObjectRef(lines.join('\n'), ctx.propsObjectName)
|
|
100
|
+
let generatedCode = rewritePropsObjectRef(lines.join('\n'), ctx.propsObjectName, ctx.restPropsName)
|
|
101
101
|
generatedCode += '\n' + hydrateLine
|
|
102
102
|
|
|
103
103
|
// Substitute module-level declarations BEFORE import detection: a
|
|
@@ -400,7 +400,7 @@ function renderTemplateAttrPart(
|
|
|
400
400
|
attr: IRAttribute,
|
|
401
401
|
attrName: string,
|
|
402
402
|
wrap: (expr: string) => string,
|
|
403
|
-
restSpreadNames?:
|
|
403
|
+
restSpreadNames?: ReadonlySet<string>,
|
|
404
404
|
): string {
|
|
405
405
|
const v = attr.value
|
|
406
406
|
switch (v.kind) {
|
|
@@ -667,7 +667,7 @@ function stripLeafKeyAttr(ir: IRNode): IRNode {
|
|
|
667
667
|
*/
|
|
668
668
|
export function renderFlatMapClientBody(
|
|
669
669
|
cb: Pick<FlatMapCallback, 'segments'>,
|
|
670
|
-
restSpreadNames?:
|
|
670
|
+
restSpreadNames?: ReadonlySet<string>,
|
|
671
671
|
): string {
|
|
672
672
|
return renderPreamble(cb, {
|
|
673
673
|
textVariant: 'client',
|
|
@@ -699,7 +699,7 @@ export function flatMapCallbackHasKeyedLeaf(cb: Pick<FlatMapCallback, 'segments'
|
|
|
699
699
|
*/
|
|
700
700
|
export function renderFlatMapProjectionClientBody(
|
|
701
701
|
inner: Extract<IRNode, { type: 'loop' }>,
|
|
702
|
-
restSpreadNames?:
|
|
702
|
+
restSpreadNames?: ReadonlySet<string>,
|
|
703
703
|
): string {
|
|
704
704
|
const chained = applyLoopChain(inner)
|
|
705
705
|
const params = inner.index ? `(${inner.param}, ${inner.index})` : `(${inner.param})`
|
|
@@ -747,7 +747,7 @@ function escapeLeafTextExpressions(ir: IRNode): IRNode {
|
|
|
747
747
|
// docstring (`ir-to-client-js/utils.ts`) for why this stays outside #2482's
|
|
748
748
|
// migration (it's the client-JS-emitter twin of the Go adapter's
|
|
749
749
|
// `loopBindingStack`).
|
|
750
|
-
export function irToHtmlTemplate(node: IRNode, restSpreadNames?:
|
|
750
|
+
export function irToHtmlTemplate(node: IRNode, restSpreadNames?: ReadonlySet<string>, loopDepth = 0, loopParams?: ReadonlyArray<string | LoopParamSpec>, branchSlotsVar?: string, inHoistedChildren = false): string {
|
|
751
751
|
const recurse = (n: IRNode): string => irToHtmlTemplate(n, restSpreadNames, loopDepth, loopParams, branchSlotsVar, inHoistedChildren)
|
|
752
752
|
const wrapExpr = (expr: string) => wrapExprWithLoopParams(expr, loopParams)
|
|
753
753
|
const wrapInterpolation = (expr: string): string => branchSlotsVar
|
|
@@ -1373,7 +1373,7 @@ function walkSkeletonPathChildren(
|
|
|
1373
1373
|
* elements (`<div data-bf-ph="sN"></div>`) instead of renderChild() calls.
|
|
1374
1374
|
* The placeholders are replaced with real createComponent() elements at runtime.
|
|
1375
1375
|
*/
|
|
1376
|
-
export function irToPlaceholderTemplate(node: IRNode, restSpreadNames?:
|
|
1376
|
+
export function irToPlaceholderTemplate(node: IRNode, restSpreadNames?: ReadonlySet<string>, loopDepth = 0, loopParams?: ReadonlyArray<string | LoopParamSpec>): string {
|
|
1377
1377
|
const recurse = (n: IRNode): string => irToPlaceholderTemplate(n, restSpreadNames, loopDepth, loopParams)
|
|
1378
1378
|
const wrapExpr = (expr: string) => wrapExprWithLoopParams(expr, loopParams)
|
|
1379
1379
|
|
|
@@ -1637,7 +1637,7 @@ function isSingleRootElement(html: string): boolean {
|
|
|
1637
1637
|
*/
|
|
1638
1638
|
export interface TemplateOptions {
|
|
1639
1639
|
inlinableConstants?: Map<string, string>
|
|
1640
|
-
restSpreadNames?:
|
|
1640
|
+
restSpreadNames?: ReadonlySet<string>
|
|
1641
1641
|
propsObjectName?: string | null
|
|
1642
1642
|
/**
|
|
1643
1643
|
* Names that exist only in the init-body scope (or were demoted to unsafe
|
|
@@ -1728,7 +1728,7 @@ export interface TemplateOptions {
|
|
|
1728
1728
|
export function irToComponentTemplate(
|
|
1729
1729
|
node: IRNode,
|
|
1730
1730
|
inlinableConstants?: Map<string, string>,
|
|
1731
|
-
restSpreadNames?:
|
|
1731
|
+
restSpreadNames?: ReadonlySet<string>,
|
|
1732
1732
|
propsObjectName?: string | null,
|
|
1733
1733
|
markupSlotIds?: ReadonlySet<string>
|
|
1734
1734
|
): string {
|
|
@@ -2133,7 +2133,7 @@ export function generateCsrTemplate(
|
|
|
2133
2133
|
node: IRNode,
|
|
2134
2134
|
inlinableConstants: Map<string, string> | undefined,
|
|
2135
2135
|
ctx: ClientJsContext,
|
|
2136
|
-
restSpreadNames?:
|
|
2136
|
+
restSpreadNames?: ReadonlySet<string>,
|
|
2137
2137
|
propsObjectName?: string | null,
|
|
2138
2138
|
unsafeLocalNames?: Set<string>,
|
|
2139
2139
|
deferredChildSlots?: ReadonlySet<string>,
|
|
@@ -19,6 +19,7 @@ import { PROPS_PARAM } from './utils.ts'
|
|
|
19
19
|
import { buildInlinableConstants, csrInlinableConstantsFromCtx } from './emit-registration.ts'
|
|
20
20
|
import { buildEnvFromCtx } from './compute-inlinability.ts'
|
|
21
21
|
import { nameForRegistryRef } from './component-scope.ts'
|
|
22
|
+
import { resolveRestSpreadNames } from './prop-handling.ts'
|
|
22
23
|
import { IMPORT_PLACEHOLDER, RUNTIME_MODULE, detectUsedImports, collectExternalImports } from './imports.ts'
|
|
23
24
|
import { isInlinableInTemplate } from '../relocate.ts'
|
|
24
25
|
import { buildSourceMapFromIR, type SourceMapV3 } from './source-map.ts'
|
|
@@ -255,10 +256,11 @@ function generateTemplateOnlyMount(ir: ComponentIR, ctx: ClientJsContext): strin
|
|
|
255
256
|
const graph = buildReferencesGraph(ctx, ir.root)
|
|
256
257
|
const { inlinableConstants, unsafeLocalNames } = buildInlinableConstants(ctx, graph, ir.root)
|
|
257
258
|
|
|
258
|
-
// Build rest spread names: these are rest/props spreads handled by
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
259
|
+
// Build rest spread names: these are rest/props spreads handled by
|
|
260
|
+
// applyRestAttrs, not spreadAttrs — #2723: includes any `const x__alias
|
|
261
|
+
// = x` hop onto the rest/props binding (see `resolveRestSpreadNames`'s
|
|
262
|
+
// docstring in prop-handling.ts).
|
|
263
|
+
const restSpreadNames = resolveRestSpreadNames(ctx)
|
|
262
264
|
|
|
263
265
|
let templateHtml: string | undefined
|
|
264
266
|
|
|
@@ -6,6 +6,100 @@ import type { ParamInfo, SignalInfo } from '../types.ts'
|
|
|
6
6
|
import type { ClientJsContext } from './types.ts'
|
|
7
7
|
import type { BindingScope } from '../scope/binding-scope.ts'
|
|
8
8
|
|
|
9
|
+
/**
|
|
10
|
+
* Which of the component's two "forwards the caller's leftover props"
|
|
11
|
+
* bindings `name` ultimately names — `'rest'` for `ctx.restPropsName`
|
|
12
|
+
* (the destructured `...rest` binding), `'props'` for `ctx.propsObjectName`
|
|
13
|
+
* (a whole undestructured `(props)` parameter spread whole), or `null` when
|
|
14
|
+
* `name` is neither, walking through any bare `const x__alias = <name>`
|
|
15
|
+
* alias chain to get there (#2723's `alias-props` mutation aliases every
|
|
16
|
+
* destructured binding, the rest parameter included, e.g.
|
|
17
|
+
* `const props__alias = props`).
|
|
18
|
+
*
|
|
19
|
+
* A `{...spread}` attribute is recognised as "forwards the caller's
|
|
20
|
+
* leftover props" (routed to the `applyRestAttrs` runtime helper /
|
|
21
|
+
* excluded from the SSR template's `spreadAttrs({...})` merge, and — only
|
|
22
|
+
* for the `'rest'` case — given the destructured prop names to exclude
|
|
23
|
+
* from what it forwards) by comparing its source expression against
|
|
24
|
+
* exactly `ctx.restPropsName` / `ctx.propsObjectName`. Without this
|
|
25
|
+
* resolver an alias hop makes that comparison fail even though the spread
|
|
26
|
+
* still forwards the SAME object — `collect-elements.ts` then never
|
|
27
|
+
* registers the rest-attrs application at all, and `html-template.ts`'s
|
|
28
|
+
* merge path stops filtering the spread out, folding it into a
|
|
29
|
+
* `spreadAttrs({...})` call keyed by the alias name instead of the
|
|
30
|
+
* runtime-visible one.
|
|
31
|
+
*
|
|
32
|
+
* Walks the chain hop by hop (not a precomputed set) so a multi-hop alias
|
|
33
|
+
* (`const p2 = props; const p3 = p2`) resolves through every link; `visited`
|
|
34
|
+
* guards a constant cycle (`const a = b; const b = a`) the same way
|
|
35
|
+
* `free-refs.ts`'s `resolveConstantInitializerRefs` does. A constant whose
|
|
36
|
+
* value isn't a bare identifier already on the chain (e.g. a real computed
|
|
37
|
+
* object) stops the walk, so a genuinely different spread expression is
|
|
38
|
+
* never mistaken for the rest object.
|
|
39
|
+
*/
|
|
40
|
+
export function resolveRestSpreadOrigin(ctx: ClientJsContext, name: string): 'rest' | 'props' | null {
|
|
41
|
+
const byName = localConstantValues(ctx)
|
|
42
|
+
const visited = new Set<string>()
|
|
43
|
+
let current: string | undefined = name.trim()
|
|
44
|
+
while (current !== undefined && !visited.has(current)) {
|
|
45
|
+
if (ctx.restPropsName && current === ctx.restPropsName) return 'rest'
|
|
46
|
+
if (ctx.propsObjectName && current === ctx.propsObjectName) return 'props'
|
|
47
|
+
visited.add(current)
|
|
48
|
+
current = byName.get(current)?.trim()
|
|
49
|
+
}
|
|
50
|
+
return null
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* `ctx.localConstants` indexed by name, memoized per `ctx`.
|
|
55
|
+
*
|
|
56
|
+
* A `Map` rather than the `.find(` this file's other two constant lookups
|
|
57
|
+
* use, deliberately: those two are SHADOW-GUARDED lookups that
|
|
58
|
+
* `binding-scope-ratchet.test.ts` deliberately counts, and that ledger is
|
|
59
|
+
* shrink-only and at its floor. Resolving an alias chain hop-by-hop would
|
|
60
|
+
* have added a third counted use — and a hot one, since the walk queries
|
|
61
|
+
* once per hop — so it indexes instead, which is both outside the ledger's
|
|
62
|
+
* concern and cheaper than a linear scan per hop.
|
|
63
|
+
*/
|
|
64
|
+
const _localConstantValuesCache: WeakMap<ClientJsContext, ReadonlyMap<string, string | undefined>> = new WeakMap()
|
|
65
|
+
|
|
66
|
+
function localConstantValues(ctx: ClientJsContext): ReadonlyMap<string, string | undefined> {
|
|
67
|
+
const cached = _localConstantValuesCache.get(ctx)
|
|
68
|
+
if (cached) return cached
|
|
69
|
+
const byName = new Map<string, string | undefined>()
|
|
70
|
+
for (const constant of ctx.localConstants) {
|
|
71
|
+
if (!byName.has(constant.name)) byName.set(constant.name, constant.value)
|
|
72
|
+
}
|
|
73
|
+
_localConstantValuesCache.set(ctx, byName)
|
|
74
|
+
return byName
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Every name that resolves (via `resolveRestSpreadOrigin`) to either of the
|
|
79
|
+
* component's "forwards the caller's leftover props" bindings — used where
|
|
80
|
+
* callers need SET membership (`restSpreadNames?.has(...)` in
|
|
81
|
+
* `html-template.ts`) rather than a per-name resolution. Memoized per
|
|
82
|
+
* `ctx` (`WeakMap`, mirroring `free-refs.ts`'s `_bindingMapCache`) since
|
|
83
|
+
* some callers build this once per component and query it while walking
|
|
84
|
+
* the whole tree.
|
|
85
|
+
*/
|
|
86
|
+
const _restSpreadNamesCache: WeakMap<ClientJsContext, ReadonlySet<string>> = new WeakMap()
|
|
87
|
+
|
|
88
|
+
export function resolveRestSpreadNames(ctx: ClientJsContext): ReadonlySet<string> {
|
|
89
|
+
const cached = _restSpreadNamesCache.get(ctx)
|
|
90
|
+
if (cached) return cached
|
|
91
|
+
|
|
92
|
+
const names = new Set<string>()
|
|
93
|
+
if (ctx.restPropsName) names.add(ctx.restPropsName)
|
|
94
|
+
if (ctx.propsObjectName) names.add(ctx.propsObjectName)
|
|
95
|
+
for (const constant of ctx.localConstants) {
|
|
96
|
+
if (resolveRestSpreadOrigin(ctx, constant.name) !== null) names.add(constant.name)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
_restSpreadNamesCache.set(ctx, names)
|
|
100
|
+
return names
|
|
101
|
+
}
|
|
102
|
+
|
|
9
103
|
/**
|
|
10
104
|
* Expand dynamic prop value by resolving local constants.
|
|
11
105
|
*
|
|
@@ -174,11 +174,30 @@ export function decideWrapForChildProp(
|
|
|
174
174
|
* The signal-getter and memo regexes (`\b<name>\s*\(`) still run against
|
|
175
175
|
* the raw string — those are call-shape patterns, not bare-identifier
|
|
176
176
|
* checks, and are outside the scope of #1267.
|
|
177
|
+
*
|
|
178
|
+
* A reference that only reaches a signal/memo/prop through an intervening
|
|
179
|
+
* `const x__alias = x` hop is walked via `ctx.localConstants` below (#2723)
|
|
180
|
+
* — see that block's own comment for why this lives here rather than on
|
|
181
|
+
* `BindingScope` or a new tracking structure.
|
|
177
182
|
*/
|
|
178
183
|
export function needsEffectWrapper(
|
|
179
184
|
expr: string,
|
|
180
185
|
ctx: ClientJsContext,
|
|
181
186
|
freeIdentifiers?: ReadonlySet<string>,
|
|
187
|
+
): boolean {
|
|
188
|
+
return needsEffectWrapperCore(expr, ctx, freeIdentifiers, new Set())
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* `needsEffectWrapper`'s actual body, with a `visitedConstants` accumulator
|
|
193
|
+
* threaded through the local-constant recursion below so a cycle
|
|
194
|
+
* (`const a = b; const b = a`) terminates instead of looping forever.
|
|
195
|
+
*/
|
|
196
|
+
function needsEffectWrapperCore(
|
|
197
|
+
expr: string,
|
|
198
|
+
ctx: ClientJsContext,
|
|
199
|
+
freeIdentifiers: ReadonlySet<string> | undefined,
|
|
200
|
+
visitedConstants: Set<string>,
|
|
182
201
|
): boolean {
|
|
183
202
|
for (const signal of ctx.signals) {
|
|
184
203
|
if (identifierCallPattern(signal.getter).test(expr)) {
|
|
@@ -209,6 +228,46 @@ export function needsEffectWrapper(
|
|
|
209
228
|
if (propsAccess.test(expr)) return true
|
|
210
229
|
}
|
|
211
230
|
|
|
231
|
+
// #2723: a bare `const x__alias = x` hop between a destructured prop and
|
|
232
|
+
// its use site breaks every check above — `expr` (or its precomputed
|
|
233
|
+
// `freeIdentifiers`) names the ALIAS, never the prop it stands for, so
|
|
234
|
+
// none of the direct prop/signal/memo checks fire even though the value
|
|
235
|
+
// is exactly as reactive as `x` itself. Phase 1's `isReactiveExpression`
|
|
236
|
+
// already sees through this (`isPropsReference` / `isSignalOrMemoReference`
|
|
237
|
+
// in jsx-to-ir.ts recursively walk `ctx.patterns.constants`, the
|
|
238
|
+
// `TransformContext` twin of this function's `ctx.localConstants`) —
|
|
239
|
+
// this is that SAME constant-chain walk, ported onto Phase 2's string
|
|
240
|
+
// expression so its independent wrap decision agrees with Phase 1's
|
|
241
|
+
// `hasReactiveAttributes` slotId decision instead of silently
|
|
242
|
+
// disagreeing on whether the attribute gets a `createEffect` at all.
|
|
243
|
+
//
|
|
244
|
+
// This is NOT loop/callback binding resolution — `BindingScope` (#2482)
|
|
245
|
+
// answers "what name is this loop row's own item/index/destructure
|
|
246
|
+
// binding," a question with no bearing on a component-body `const`
|
|
247
|
+
// aliasing a prop — so it does not belong there. It also isn't a new ad
|
|
248
|
+
// hoc tracking structure: `ctx.localConstants` is the existing per-
|
|
249
|
+
// component constant list this file already reads (see
|
|
250
|
+
// `expandConstantForReactivity` above), walked here with a `visited`
|
|
251
|
+
// guard for the same reason `free-refs.ts`'s `resolveConstantInitializerRefs`
|
|
252
|
+
// carries one — a constant cycle must terminate, not loop forever.
|
|
253
|
+
//
|
|
254
|
+
// Constants whose initializer contains an arrow/function expression are
|
|
255
|
+
// skipped, mirroring `resolveConstantInitializerRefs`'s `containsArrow`
|
|
256
|
+
// skip: refs inside a function body run when (and if) the function is
|
|
257
|
+
// invoked, not merely because something reads the bare function value.
|
|
258
|
+
for (const constant of ctx.localConstants) {
|
|
259
|
+
if (visitedConstants.has(constant.name)) continue
|
|
260
|
+
if (constant.value === undefined || constant.containsArrow) continue
|
|
261
|
+
const referenced = freeIdentifiers
|
|
262
|
+
? freeIdentifiers.has(constant.name)
|
|
263
|
+
: tokenContainsIdent(expr, constant.name)
|
|
264
|
+
if (!referenced) continue
|
|
265
|
+
visitedConstants.add(constant.name)
|
|
266
|
+
if (needsEffectWrapperCore(constant.value, ctx, constant.freeIdentifiers, visitedConstants)) {
|
|
267
|
+
return true
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
212
271
|
return false
|
|
213
272
|
}
|
|
214
273
|
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* AST-based rename of the source-level props object name (e.g. `props
|
|
3
|
-
*
|
|
4
|
-
* `_p` across the joined
|
|
2
|
+
* AST-based rename of the source-level props object name(s) (e.g. `props`,
|
|
3
|
+
* a user-supplied destructure name, or a destructured rest binding like
|
|
4
|
+
* `...rest`) → the generated parameter name `_p` across the joined
|
|
5
|
+
* init-body string.
|
|
5
6
|
*
|
|
6
7
|
* Replaces the pre-C2 regex hack `\\b<propsObjectName>\\b` which silently
|
|
7
8
|
* matched contexts that should NOT have been rewritten:
|
|
@@ -27,15 +28,54 @@ import { PROPS_PARAM } from './utils.ts'
|
|
|
27
28
|
import { identifierPattern } from '../identifier-pattern.ts'
|
|
28
29
|
|
|
29
30
|
/**
|
|
30
|
-
* Rename every value-position reference to `propsObjectName`
|
|
31
|
-
* to `
|
|
32
|
-
*
|
|
33
|
-
*
|
|
31
|
+
* Rename every value-position reference to `propsObjectName` — and,
|
|
32
|
+
* independently, to `restPropsName` (#2723) — in `code` to `_p`.
|
|
33
|
+
*
|
|
34
|
+
* `restPropsName` matters even in destructured-prop mode (where
|
|
35
|
+
* `propsObjectName` is null and the analyzer already pre-rewrites bare
|
|
36
|
+
* NAMED-prop refs into `templateXxx` fields, per the historical
|
|
37
|
+
* `propsObjectName ?? 'props'` fallback this replaces): a destructured
|
|
38
|
+
* rest binding (`const { className, ...rest } = props` /
|
|
39
|
+
* `function F({ className, ...rest })`) is not itself a "named prop," so
|
|
40
|
+
* nothing pre-rewrites a bare reference to it. Such a reference reaches
|
|
41
|
+
* this pass whenever the init body needs `rest`'s OWN VALUE rather than
|
|
42
|
+
* just recognising a `{...rest}` spread by name — e.g. a `const
|
|
43
|
+
* rest__alias = rest` hop (#2723's `alias-props` mutation aliases the
|
|
44
|
+
* rest parameter along with every named one). A rest binding named
|
|
45
|
+
* anything but "props" (`...rest`, `...leftover`) left such a reference
|
|
46
|
+
* dangling as a `ReferenceError`, so the analyzer's actual
|
|
47
|
+
* `restPropsName` is passed in to remove that guesswork.
|
|
48
|
+
*
|
|
49
|
+
* The `propsObjectName ?? 'props'` fallback is KEPT alongside it, not
|
|
50
|
+
* replaced by it. The two do different jobs, and reading the fallback as
|
|
51
|
+
* merely a lucky guess at the rest binding's name regressed the
|
|
52
|
+
* doc-example `StatementExample`: a component that destructures its
|
|
53
|
+
* parameters can still write `props.itemId` inside a handler body, and
|
|
54
|
+
* `propsObjectName` is null for exactly that shape — so dropping the
|
|
55
|
+
* fallback left `props.itemId` in the emitted init, where no `props`
|
|
56
|
+
* binding exists to satisfy it.
|
|
57
|
+
*
|
|
58
|
+
* Each candidate name is rewritten independently and skipped when null,
|
|
59
|
+
* already `_p`, or a duplicate of one already processed (a component
|
|
60
|
+
* whose props param IS its own rest destructure target, if that shape
|
|
61
|
+
* ever arises, would otherwise walk the AST twice for the same name).
|
|
34
62
|
*/
|
|
35
|
-
export function rewritePropsObjectRef(
|
|
36
|
-
|
|
37
|
-
|
|
63
|
+
export function rewritePropsObjectRef(
|
|
64
|
+
code: string,
|
|
65
|
+
propsObjectName: string | null,
|
|
66
|
+
restPropsName: string | null = null,
|
|
67
|
+
): string {
|
|
68
|
+
let result = code
|
|
69
|
+
const seen = new Set<string>()
|
|
70
|
+
for (const srcPropsName of [propsObjectName ?? 'props', restPropsName]) {
|
|
71
|
+
if (srcPropsName === null || srcPropsName === PROPS_PARAM || seen.has(srcPropsName)) continue
|
|
72
|
+
seen.add(srcPropsName)
|
|
73
|
+
result = rewriteOneName(result, srcPropsName)
|
|
74
|
+
}
|
|
75
|
+
return result
|
|
76
|
+
}
|
|
38
77
|
|
|
78
|
+
function rewriteOneName(code: string, srcPropsName: string): string {
|
|
39
79
|
// Quick exit when the name doesn't appear at all.
|
|
40
80
|
if (!identifierPattern(srcPropsName).test(code)) return code
|
|
41
81
|
|
package/src/jsx-to-ir.ts
CHANGED
|
@@ -1944,6 +1944,52 @@ function unwrapHoistedFragment(node: IRNode): IRNode {
|
|
|
1944
1944
|
return { ...only, needsScope: true }
|
|
1945
1945
|
}
|
|
1946
1946
|
|
|
1947
|
+
// #2732: a `needsScopeComment` fragment's five hydration markers move to
|
|
1948
|
+
// the wrapping comment, but `data-key` needs to stay on an element (see
|
|
1949
|
+
// `IRElement.carriesDataKey`'s docstring for why). Mark the first ELEMENT
|
|
1950
|
+
// among `children` — immutably (`{ ...el, carriesDataKey: true }`), matching
|
|
1951
|
+
// the rest of this file's post-hoc IR tagging (e.g. `unwrapHoistedFragment`)
|
|
1952
|
+
// rather than mutating the child in place.
|
|
1953
|
+
function markDataKeyCarrier(children: IRNode[]): IRNode[] {
|
|
1954
|
+
for (let i = 0; i < children.length; i++) {
|
|
1955
|
+
const marked = markCarrierIn(children[i])
|
|
1956
|
+
if (!marked) continue
|
|
1957
|
+
const out = children.slice()
|
|
1958
|
+
out[i] = marked
|
|
1959
|
+
return out
|
|
1960
|
+
}
|
|
1961
|
+
return children
|
|
1962
|
+
}
|
|
1963
|
+
|
|
1964
|
+
/**
|
|
1965
|
+
* The marked copy of `node` if this subtree can render an element in first
|
|
1966
|
+
* position, or null if it cannot.
|
|
1967
|
+
*
|
|
1968
|
+
* Descends through `conditional` because a fragment whose only top-level
|
|
1969
|
+
* child is a ternary or `&&` is still a single-visual-root row —
|
|
1970
|
+
* `{done ? <li class="done"/> : <li/>}` renders exactly one `<li>`. A flat
|
|
1971
|
+
* `children.findIndex(c => c.type === 'element')` returns -1 there and
|
|
1972
|
+
* silently reproduces #2732's own symptom for a shape the fix was supposed
|
|
1973
|
+
* to cover.
|
|
1974
|
+
*
|
|
1975
|
+
* BOTH branches are marked, not just one: they are mutually exclusive at
|
|
1976
|
+
* render time, so whichever is taken carries the key, and marking only
|
|
1977
|
+
* `whenTrue` would drop it exactly when the condition is false.
|
|
1978
|
+
*/
|
|
1979
|
+
function markCarrierIn(node: IRNode): IRNode | null {
|
|
1980
|
+
if (node.type === 'element') {
|
|
1981
|
+
return { ...(node as IRElement), carriesDataKey: true }
|
|
1982
|
+
}
|
|
1983
|
+
if (node.type === 'conditional') {
|
|
1984
|
+
const cond = node as IRConditional
|
|
1985
|
+
const whenTrue = markCarrierIn(cond.whenTrue)
|
|
1986
|
+
const whenFalse = markCarrierIn(cond.whenFalse)
|
|
1987
|
+
if (!whenTrue && !whenFalse) return null
|
|
1988
|
+
return { ...cond, whenTrue: whenTrue ?? cond.whenTrue, whenFalse: whenFalse ?? cond.whenFalse }
|
|
1989
|
+
}
|
|
1990
|
+
return null
|
|
1991
|
+
}
|
|
1992
|
+
|
|
1947
1993
|
function transformFragment(
|
|
1948
1994
|
node: ts.JsxFragment,
|
|
1949
1995
|
ctx: TransformContext
|
|
@@ -1968,7 +2014,7 @@ function transformFragment(
|
|
|
1968
2014
|
|
|
1969
2015
|
return {
|
|
1970
2016
|
type: 'fragment',
|
|
1971
|
-
children,
|
|
2017
|
+
children: needsScopeComment ? markDataKeyCarrier(children) : children,
|
|
1972
2018
|
transparent: isTransparent || undefined,
|
|
1973
2019
|
needsScopeComment,
|
|
1974
2020
|
loc: getSourceLocation(node, ctx.sourceFile, ctx.filePath),
|
package/src/types.ts
CHANGED
|
@@ -347,6 +347,20 @@ export interface IRElement {
|
|
|
347
347
|
children: IRNode[]
|
|
348
348
|
slotId: string | null
|
|
349
349
|
needsScope: boolean
|
|
350
|
+
/**
|
|
351
|
+
* Set on the first ELEMENT among a `needsScopeComment` fragment root's own
|
|
352
|
+
* top-level children (#2732) — the fragment's five hydration markers
|
|
353
|
+
* (`bf-s`/`bf-h`/`bf-m`/`bf-r`/`bf-p`) move to the wrapping
|
|
354
|
+
* `<!--bf-scope:...-->` comment instead of an element attribute, but
|
|
355
|
+
* `data-key` has to stay on an element because the client runtime's
|
|
356
|
+
* `mapArray` adopt loop reads it as a DOM attribute
|
|
357
|
+
* (`primaryEl.dataset.key`, map-array.ts). "First element, not first
|
|
358
|
+
* node" mirrors the CSR runtime's own resolution of the identical
|
|
359
|
+
* ambiguity (`component.ts`'s `roots.find(isElement)`, #2735) rather than
|
|
360
|
+
* inventing a second answer. Always `undefined` when `needsScope` is
|
|
361
|
+
* true — the two are mutually exclusive ways of carrying the same key.
|
|
362
|
+
*/
|
|
363
|
+
carriesDataKey?: boolean
|
|
350
364
|
/**
|
|
351
365
|
* Page-lifecycle boundary id for an element lowered from `<Region>`
|
|
352
366
|
* (spec/router.md). Set only on region host elements; adapters emit it as
|