@barefootjs/client 0.33.0 → 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/csr-adapter.js +6 -0
- package/dist/runtime/component.d.ts +27 -1
- package/dist/runtime/component.d.ts.map +1 -1
- package/dist/runtime/index.d.ts +1 -1
- package/dist/runtime/index.d.ts.map +1 -1
- package/dist/runtime/index.js +141 -32
- package/dist/runtime/insert.d.ts +21 -0
- package/dist/runtime/insert.d.ts.map +1 -1
- package/dist/runtime/map-array.d.ts.map +1 -1
- package/dist/runtime/qsa-item.d.ts.map +1 -1
- package/dist/runtime/registry.d.ts.map +1 -1
- package/dist/runtime/standalone.js +141 -32
- package/dist/runtime/types.d.ts +19 -1
- package/dist/runtime/types.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/runtime/component.ts +304 -41
- package/src/runtime/hydrate.ts +11 -2
- package/src/runtime/index.ts +1 -1
- package/src/runtime/insert.ts +30 -0
- package/src/runtime/map-array.ts +136 -21
- package/src/runtime/qsa-item.ts +3 -1
- package/src/runtime/registry.ts +4 -1
- package/src/runtime/types.ts +19 -1
package/src/runtime/map-array.ts
CHANGED
|
@@ -26,11 +26,38 @@ import {
|
|
|
26
26
|
BF_LOOP_START,
|
|
27
27
|
BF_LOOP_END,
|
|
28
28
|
BF_LOOP_ITEM,
|
|
29
|
+
BF_SCOPE_COMMENT_PREFIX,
|
|
30
|
+
BF_SCOPE_COMMENT_END_PREFIX,
|
|
29
31
|
loopStartMarker,
|
|
30
32
|
loopEndMarker,
|
|
31
33
|
loopItemMarker,
|
|
32
34
|
} from '@barefootjs/shared'
|
|
33
35
|
|
|
36
|
+
/**
|
|
37
|
+
* A fragment-rooted component's own `<!--bf-scope:ID-->` /
|
|
38
|
+
* `<!--bf-/scope:ID-->` boundary pair (`wrapWithScopeComment`,
|
|
39
|
+
* hono-adapter.ts), captured when that component is used as a keyed loop
|
|
40
|
+
* row's `primaryEl` (#2733). Distinct from `ItemScope.startMarker` (the
|
|
41
|
+
* `<!--bf-loop-i-->` marker for a multi-root loop BODY): that marks a
|
|
42
|
+
* loop-body concept this module owns; this marks a component's own scope
|
|
43
|
+
* identity that the row's SSR/CSR emission owns and `commentScopeRegistry`
|
|
44
|
+
* (scope.ts) keys off. `insertScope`/`removeScope` carry it as part of the
|
|
45
|
+
* row's atomic unit so a reorder/removal doesn't orphan it.
|
|
46
|
+
*/
|
|
47
|
+
type ScopeCommentPair = { start: Comment; end: Comment }
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* The scope id inside a `<!--bf-scope:ID|h=…|m=…|props-->` comment — the
|
|
51
|
+
* `|`-free head, matching `getCommentScopeBoundary` (scope.ts) and
|
|
52
|
+
* `parseCommentScopeId` (query.ts). Used to pair a start comment with its
|
|
53
|
+
* OWN end marker by id, never with a sibling scope's.
|
|
54
|
+
*/
|
|
55
|
+
function scopeIdOf(start: Comment): string {
|
|
56
|
+
const rest = (start.nodeValue ?? '').slice(BF_SCOPE_COMMENT_PREFIX.length)
|
|
57
|
+
const pipe = rest.indexOf('|')
|
|
58
|
+
return pipe >= 0 ? rest.slice(0, pipe) : rest
|
|
59
|
+
}
|
|
60
|
+
|
|
34
61
|
type ItemScope<T> = {
|
|
35
62
|
/**
|
|
36
63
|
* `<!--bf-loop-i-->` Comment that anchors a multi-root item. `null` for
|
|
@@ -49,6 +76,8 @@ type ItemScope<T> = {
|
|
|
49
76
|
* two or more peers). Empty for single-root items.
|
|
50
77
|
*/
|
|
51
78
|
extras: HTMLElement[]
|
|
79
|
+
/** See `ScopeCommentPair`. `null` when `primaryEl` isn't a fragment-root scope. */
|
|
80
|
+
scopeComments: ScopeCommentPair | null
|
|
52
81
|
dispose: () => void
|
|
53
82
|
setItem: (v: T) => void
|
|
54
83
|
}
|
|
@@ -111,46 +140,103 @@ export function findLoopMarkers(
|
|
|
111
140
|
* are present (single-root loops, the common case), each Element forms
|
|
112
141
|
* its own range with `startMarker: null` and `extras: []` — preserving
|
|
113
142
|
* legacy behavior verbatim.
|
|
143
|
+
*
|
|
144
|
+
* Also recognizes a fragment-rooted component's own `<!--bf-scope:ID-->` /
|
|
145
|
+
* `<!--bf-/scope:ID-->` pair immediately bracketing an item's `primaryEl`
|
|
146
|
+
* (#2733) and captures it as `scopeComments`, so the row's caller can pass
|
|
147
|
+
* it through to `createItemScope` and keep it moving with the row.
|
|
148
|
+
*
|
|
149
|
+
* A `|h=` child segment does NOT disqualify a comment here, though
|
|
150
|
+
* `hydrate.ts::hydrateCommentScope` skips exactly those. The two ask
|
|
151
|
+
* different questions. That walker runs at the top level, where a `|h=`
|
|
152
|
+
* comment means "some parent's `initChild` owns this scope, not me." This
|
|
153
|
+
* one runs BETWEEN a loop's own markers, where every row IS a child: real
|
|
154
|
+
* SSR emits each row as `<!--bf-scope:TodoRow_x|h=<host>|m=<slot>|<props>-->`
|
|
155
|
+
* (measured), so requiring a root-shaped comment matched nothing a server
|
|
156
|
+
* ever produces and left the pair behind on every reorder — the exact
|
|
157
|
+
* orphaning this field exists to prevent.
|
|
158
|
+
*
|
|
159
|
+
* The end comment is matched by scope ID rather than by "the next
|
|
160
|
+
* `bf-/scope:` seen", so a sibling root that is itself a fragment-rooted
|
|
161
|
+
* child cannot close this row's pair early.
|
|
114
162
|
*/
|
|
115
163
|
function findItemRanges(start: Comment, end: Comment): Array<{
|
|
116
164
|
startMarker: Comment | null
|
|
117
165
|
primaryEl: HTMLElement
|
|
118
166
|
extras: HTMLElement[]
|
|
167
|
+
scopeComments: ScopeCommentPair | null
|
|
119
168
|
}> {
|
|
169
|
+
type PendingPair = { start: Comment; end: Comment | null }
|
|
120
170
|
const ranges: Array<{
|
|
121
171
|
startMarker: Comment | null
|
|
122
172
|
primaryEl: HTMLElement | null
|
|
123
173
|
extras: HTMLElement[]
|
|
174
|
+
scopeComments: PendingPair | null
|
|
124
175
|
}> = []
|
|
125
|
-
let current:
|
|
176
|
+
let current: (typeof ranges)[number] | null = null
|
|
126
177
|
let sawItemMarker = false
|
|
178
|
+
// A fragment-root row's own start comment, seen but not yet matched to
|
|
179
|
+
// the element it brackets — consumed the instant the next Element is seen.
|
|
180
|
+
let pendingScopeStart: Comment | null = null
|
|
181
|
+
// The pair still awaiting its end comment, so a later `bf-/scope:` knows
|
|
182
|
+
// which range to close — there is at most one open at a time since a row's
|
|
183
|
+
// own scope comment never nests another row's.
|
|
184
|
+
let openScopeComments: PendingPair | null = null
|
|
127
185
|
let node: Node | null = start.nextSibling
|
|
128
186
|
while (node && node !== end) {
|
|
129
|
-
if (node.nodeType === Node.COMMENT_NODE
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
187
|
+
if (node.nodeType === Node.COMMENT_NODE) {
|
|
188
|
+
const value = (node as Comment).nodeValue ?? ''
|
|
189
|
+
if (value === BF_LOOP_ITEM) {
|
|
190
|
+
sawItemMarker = true
|
|
191
|
+
current = { startMarker: node as Comment, primaryEl: null, extras: [], scopeComments: null }
|
|
192
|
+
ranges.push(current)
|
|
193
|
+
} else if (value.startsWith(BF_SCOPE_COMMENT_PREFIX)) {
|
|
194
|
+
pendingScopeStart = node as Comment
|
|
195
|
+
} else if (
|
|
196
|
+
openScopeComments &&
|
|
197
|
+
value === BF_SCOPE_COMMENT_END_PREFIX + scopeIdOf(openScopeComments.start)
|
|
198
|
+
) {
|
|
199
|
+
openScopeComments.end = node as Comment
|
|
200
|
+
openScopeComments = null
|
|
201
|
+
}
|
|
133
202
|
} else if (node.nodeType === Node.ELEMENT_NODE) {
|
|
134
203
|
const el = node as HTMLElement
|
|
204
|
+
let scopeComments: PendingPair | null = null
|
|
205
|
+
if (pendingScopeStart) {
|
|
206
|
+
scopeComments = { start: pendingScopeStart, end: null }
|
|
207
|
+
openScopeComments = scopeComments
|
|
208
|
+
pendingScopeStart = null
|
|
209
|
+
}
|
|
135
210
|
if (sawItemMarker) {
|
|
136
|
-
if (!current!.primaryEl)
|
|
137
|
-
|
|
211
|
+
if (!current!.primaryEl) {
|
|
212
|
+
current!.primaryEl = el
|
|
213
|
+
current!.scopeComments = scopeComments
|
|
214
|
+
} else current!.extras.push(el)
|
|
138
215
|
} else {
|
|
139
|
-
ranges.push({ startMarker: null, primaryEl: el, extras: [] })
|
|
216
|
+
ranges.push({ startMarker: null, primaryEl: el, extras: [], scopeComments })
|
|
140
217
|
}
|
|
141
218
|
}
|
|
142
219
|
node = node.nextSibling
|
|
143
220
|
}
|
|
144
|
-
return ranges
|
|
145
|
-
(
|
|
146
|
-
r
|
|
147
|
-
|
|
221
|
+
return ranges
|
|
222
|
+
.filter(
|
|
223
|
+
(r): r is { startMarker: Comment | null; primaryEl: HTMLElement; extras: HTMLElement[]; scopeComments: PendingPair | null } =>
|
|
224
|
+
r.primaryEl !== null,
|
|
225
|
+
)
|
|
226
|
+
.map(r => ({
|
|
227
|
+
...r,
|
|
228
|
+
// Drop a scope-comment pair whose end was never found (malformed/
|
|
229
|
+
// truncated DOM) rather than carry a half-formed pair forward — the
|
|
230
|
+
// row still hydrates correctly, it just won't track the boundary.
|
|
231
|
+
scopeComments: r.scopeComments?.end ? (r.scopeComments as ScopeCommentPair) : null,
|
|
232
|
+
}))
|
|
148
233
|
}
|
|
149
234
|
|
|
150
235
|
/**
|
|
151
236
|
* Insert a scope's nodes into `target` in their canonical order
|
|
152
|
-
* (startMarker → primaryEl →
|
|
153
|
-
* node already at the target
|
|
237
|
+
* (startMarker → scopeComments.start → primaryEl → scopeComments.end →
|
|
238
|
+
* extras). Idempotent — `insertBefore` on a node already at the target
|
|
239
|
+
* position is a no-op.
|
|
154
240
|
*
|
|
155
241
|
* `target` is typed as `Node` (not `HTMLElement`) so callers can pass a
|
|
156
242
|
* `DocumentFragment` to batch several scopes into one subsequent
|
|
@@ -159,7 +245,9 @@ function findItemRanges(start: Comment, end: Comment): Array<{
|
|
|
159
245
|
*/
|
|
160
246
|
function insertScope<T>(scope: ItemScope<T>, target: Node, anchor: Node | null): void {
|
|
161
247
|
if (scope.startMarker) target.insertBefore(scope.startMarker, anchor)
|
|
248
|
+
if (scope.scopeComments) target.insertBefore(scope.scopeComments.start, anchor)
|
|
162
249
|
target.insertBefore(scope.primaryEl, anchor)
|
|
250
|
+
if (scope.scopeComments) target.insertBefore(scope.scopeComments.end, anchor)
|
|
163
251
|
for (const ex of scope.extras) target.insertBefore(ex, anchor)
|
|
164
252
|
}
|
|
165
253
|
|
|
@@ -211,7 +299,9 @@ export function longestIncreasingSubsequenceIndices(arr: number[]): number[] {
|
|
|
211
299
|
/** Detach all of a scope's nodes from the DOM. */
|
|
212
300
|
function removeScope<T>(scope: ItemScope<T>): void {
|
|
213
301
|
if (scope.startMarker?.parentNode) scope.startMarker.remove()
|
|
302
|
+
if (scope.scopeComments?.start.parentNode) scope.scopeComments.start.remove()
|
|
214
303
|
if (scope.primaryEl.parentNode) scope.primaryEl.remove()
|
|
304
|
+
if (scope.scopeComments?.end.parentNode) scope.scopeComments.end.remove()
|
|
215
305
|
for (const ex of scope.extras) {
|
|
216
306
|
if (ex.parentNode) ex.remove()
|
|
217
307
|
}
|
|
@@ -226,6 +316,13 @@ function removeScope<T>(scope: ItemScope<T>): void {
|
|
|
226
316
|
* sibling roots on the returned element via a `__bfExtras` property that
|
|
227
317
|
* we read-and-delete here. On hydration the caller passes `existingExtras`
|
|
228
318
|
* + `existingStart` collected from the SSR partition.
|
|
319
|
+
*
|
|
320
|
+
* Fragment-root row handling (#2733): on CSR, `createComponent`
|
|
321
|
+
* (component.ts's `rowMount` branch) stashes the row's own
|
|
322
|
+
* `<!--bf-scope:ID-->` boundary pair on the returned element via a
|
|
323
|
+
* `__bfScopeComments` property, read-and-deleted here exactly like
|
|
324
|
+
* `__bfExtras`. On hydration the caller passes `existingScopeComments`
|
|
325
|
+
* collected from the SSR partition (`findItemRanges`).
|
|
229
326
|
*/
|
|
230
327
|
function createItemScope<T>(
|
|
231
328
|
item: T,
|
|
@@ -234,6 +331,7 @@ function createItemScope<T>(
|
|
|
234
331
|
existingPrimary?: HTMLElement,
|
|
235
332
|
existingExtras?: HTMLElement[],
|
|
236
333
|
existingStart?: Comment | null,
|
|
334
|
+
existingScopeComments?: ScopeCommentPair | null,
|
|
237
335
|
rowMount?: RowMountPoint | null,
|
|
238
336
|
): ItemScope<T> {
|
|
239
337
|
let primaryEl!: HTMLElement
|
|
@@ -241,6 +339,7 @@ function createItemScope<T>(
|
|
|
241
339
|
let setItem!: (v: T) => void
|
|
242
340
|
let extras: HTMLElement[] = []
|
|
243
341
|
let startMarker: Comment | null = null
|
|
342
|
+
let scopeComments: ScopeCommentPair | null = null
|
|
244
343
|
|
|
245
344
|
createRoot((d) => {
|
|
246
345
|
dispose = d
|
|
@@ -274,6 +373,7 @@ function createItemScope<T>(
|
|
|
274
373
|
if (existingPrimary) {
|
|
275
374
|
extras = existingExtras ?? []
|
|
276
375
|
startMarker = existingStart ?? null
|
|
376
|
+
scopeComments = existingScopeComments ?? null
|
|
277
377
|
} else {
|
|
278
378
|
const stashed = (primaryEl as unknown as { __bfExtras?: HTMLElement[] }).__bfExtras
|
|
279
379
|
if (stashed && stashed.length > 0) {
|
|
@@ -281,6 +381,11 @@ function createItemScope<T>(
|
|
|
281
381
|
startMarker = document.createComment(BF_LOOP_ITEM)
|
|
282
382
|
}
|
|
283
383
|
delete (primaryEl as unknown as { __bfExtras?: HTMLElement[] }).__bfExtras
|
|
384
|
+
|
|
385
|
+
const stashedScopeComments = (primaryEl as unknown as { __bfScopeComments?: ScopeCommentPair })
|
|
386
|
+
.__bfScopeComments
|
|
387
|
+
if (stashedScopeComments) scopeComments = stashedScopeComments
|
|
388
|
+
delete (primaryEl as unknown as { __bfScopeComments?: ScopeCommentPair }).__bfScopeComments
|
|
284
389
|
}
|
|
285
390
|
return undefined
|
|
286
391
|
})
|
|
@@ -294,7 +399,7 @@ function createItemScope<T>(
|
|
|
294
399
|
primaryEl.remove()
|
|
295
400
|
}
|
|
296
401
|
|
|
297
|
-
return { startMarker, primaryEl, extras, dispose, setItem }
|
|
402
|
+
return { startMarker, primaryEl, extras, scopeComments, dispose, setItem }
|
|
298
403
|
}
|
|
299
404
|
|
|
300
405
|
/**
|
|
@@ -352,7 +457,7 @@ export function mapArray<T>(
|
|
|
352
457
|
const existingRanges = startMarker
|
|
353
458
|
? findItemRanges(startMarker, endMarker!)
|
|
354
459
|
: Array.from(container.children).map(
|
|
355
|
-
(el) => ({ startMarker: null, primaryEl: el as HTMLElement, extras: [] as HTMLElement[] }),
|
|
460
|
+
(el) => ({ startMarker: null, primaryEl: el as HTMLElement, extras: [] as HTMLElement[], scopeComments: null as ScopeCommentPair | null }),
|
|
356
461
|
)
|
|
357
462
|
|
|
358
463
|
// SSR elements need initialization when they haven't been adopted into scopes yet.
|
|
@@ -375,6 +480,7 @@ export function mapArray<T>(
|
|
|
375
480
|
range.primaryEl,
|
|
376
481
|
range.extras,
|
|
377
482
|
range.startMarker,
|
|
483
|
+
range.scopeComments,
|
|
378
484
|
)
|
|
379
485
|
scopes.set(key, scope)
|
|
380
486
|
hydratedScopes.add(range.primaryEl)
|
|
@@ -386,7 +492,7 @@ export function mapArray<T>(
|
|
|
386
492
|
const key = getKey ? getKey(item, i) : String(i)
|
|
387
493
|
// Final position is known here (append before the loop's trailing
|
|
388
494
|
// anchor), so the row can be mounted at it before its init runs.
|
|
389
|
-
const scope = createItemScope(item, i, renderItem, undefined, undefined, undefined, { container, anchor })
|
|
495
|
+
const scope = createItemScope(item, i, renderItem, undefined, undefined, undefined, undefined, { container, anchor })
|
|
390
496
|
if (!scope.primaryEl.dataset.key) scope.primaryEl.setAttribute(BF_KEY, key)
|
|
391
497
|
scopes.set(key, scope)
|
|
392
498
|
insertScope(scope, container, anchor)
|
|
@@ -396,7 +502,9 @@ export function mapArray<T>(
|
|
|
396
502
|
for (let i = items.length; i < existingRanges.length; i++) {
|
|
397
503
|
const range = existingRanges[i]
|
|
398
504
|
if (range.startMarker?.parentNode) range.startMarker.remove()
|
|
505
|
+
if (range.scopeComments?.start.parentNode) range.scopeComments.start.remove()
|
|
399
506
|
if (range.primaryEl.parentNode) range.primaryEl.remove()
|
|
507
|
+
if (range.scopeComments?.end.parentNode) range.scopeComments.end.remove()
|
|
400
508
|
for (const ex of range.extras) {
|
|
401
509
|
if (ex.parentNode) ex.remove()
|
|
402
510
|
}
|
|
@@ -411,7 +519,7 @@ export function mapArray<T>(
|
|
|
411
519
|
const loopRanges = startMarker
|
|
412
520
|
? findItemRanges(startMarker, endMarker!)
|
|
413
521
|
: Array.from(container.children).map(
|
|
414
|
-
(el) => ({ startMarker: null, primaryEl: el as HTMLElement, extras: [] as HTMLElement[] }),
|
|
522
|
+
(el) => ({ startMarker: null, primaryEl: el as HTMLElement, extras: [] as HTMLElement[], scopeComments: null as ScopeCommentPair | null }),
|
|
415
523
|
)
|
|
416
524
|
for (const range of loopRanges) {
|
|
417
525
|
const existingKey = range.primaryEl.dataset?.key
|
|
@@ -420,6 +528,7 @@ export function mapArray<T>(
|
|
|
420
528
|
startMarker: range.startMarker,
|
|
421
529
|
primaryEl: range.primaryEl,
|
|
422
530
|
extras: range.extras,
|
|
531
|
+
scopeComments: range.scopeComments,
|
|
423
532
|
dispose: () => {},
|
|
424
533
|
setItem: () => {},
|
|
425
534
|
})
|
|
@@ -450,6 +559,7 @@ export function mapArray<T>(
|
|
|
450
559
|
let expectedNodeCount = 0
|
|
451
560
|
for (const scope of scopes.values()) {
|
|
452
561
|
expectedNodeCount += 1 + scope.extras.length + (scope.startMarker ? 1 : 0)
|
|
562
|
+
+ (scope.scopeComments ? 2 : 0)
|
|
453
563
|
}
|
|
454
564
|
let actualNodeCount = 0
|
|
455
565
|
for (let node = container.firstChild; node; node = node.nextSibling) actualNodeCount++
|
|
@@ -501,7 +611,7 @@ export function mapArray<T>(
|
|
|
501
611
|
// the loop range before its init runs; the LIS reorder below moves it
|
|
502
612
|
// to its final position (it participates in the walk like any other
|
|
503
613
|
// attached scope, so the resulting order is unchanged).
|
|
504
|
-
const scope = createItemScope(item, i, renderItem, undefined, undefined, undefined, { container, anchor })
|
|
614
|
+
const scope = createItemScope(item, i, renderItem, undefined, undefined, undefined, undefined, { container, anchor })
|
|
505
615
|
if (!scope.primaryEl.dataset.key) scope.primaryEl.setAttribute(BF_KEY, key)
|
|
506
616
|
scopes.set(key, scope)
|
|
507
617
|
desiredOrder.push(scope)
|
|
@@ -586,9 +696,14 @@ export function mapArray<T>(
|
|
|
586
696
|
while (j < desiredOrder.length && !stationary[j]) j++
|
|
587
697
|
// Insert this run immediately before the next stationary scope (which
|
|
588
698
|
// is already exactly where it needs to be), or before the loop's
|
|
589
|
-
// trailing anchor when the run reaches the end of the list.
|
|
699
|
+
// trailing anchor when the run reaches the end of the list. Preferring
|
|
700
|
+
// `scopeComments.start` over `primaryEl` here matters when that next
|
|
701
|
+
// scope is a fragment-root row (#2733): inserting directly before its
|
|
702
|
+
// `primaryEl` would land the new run BETWEEN its still-attached
|
|
703
|
+
// `<!--bf-scope:-->` start comment and its element, splitting a
|
|
704
|
+
// stationary scope's own boundary pair.
|
|
590
705
|
const before = j < desiredOrder.length
|
|
591
|
-
? (desiredOrder[j].startMarker ?? desiredOrder[j].primaryEl)
|
|
706
|
+
? (desiredOrder[j].startMarker ?? desiredOrder[j].scopeComments?.start ?? desiredOrder[j].primaryEl)
|
|
592
707
|
: anchor
|
|
593
708
|
if (j - i === 1) {
|
|
594
709
|
insertScope(desiredOrder[i], container, before)
|
package/src/runtime/qsa-item.ts
CHANGED
|
@@ -137,7 +137,9 @@ export function upsertChildItem(
|
|
|
137
137
|
const ph = qsaItem(primaryEl, `[data-bf-ph="${phId}"]`) as HTMLElement | null
|
|
138
138
|
if (ph) {
|
|
139
139
|
const slot = slotId ? buildSlotInfo(primaryEl, slotId, anchorScope) : undefined
|
|
140
|
-
// Connect before init — see the same call in `upsertChild`.
|
|
140
|
+
// Connect before init — see the same call in `upsertChild`. `mountAt`
|
|
141
|
+
// (`ph`) is always given here, so this lands on `createComponent`'s
|
|
142
|
+
// non-null-`mountAt` overload and is typed `HTMLElement`.
|
|
141
143
|
return createComponent(name, props, key, slot, ph)
|
|
142
144
|
}
|
|
143
145
|
return null
|
package/src/runtime/registry.ts
CHANGED
|
@@ -161,7 +161,10 @@ export function upsertChild(
|
|
|
161
161
|
// Hand the placeholder to `createComponent` so the new element is
|
|
162
162
|
// connected before its init runs — `useContext` resolves by DOM
|
|
163
163
|
// position, and a detached init silently fell back to the global
|
|
164
|
-
// context store.
|
|
164
|
+
// context store. `mountAt` (`ph`) is always given here, so this lands
|
|
165
|
+
// on `createComponent`'s non-null-`mountAt` overload and is typed
|
|
166
|
+
// `HTMLElement` — the bare-fragment-root `DocumentFragment` shape is
|
|
167
|
+
// unreachable from a call that names its mount point.
|
|
165
168
|
return createComponent(name, props, key, slot, ph)
|
|
166
169
|
}
|
|
167
170
|
return null
|
package/src/runtime/types.ts
CHANGED
|
@@ -22,6 +22,24 @@ export interface ComponentDef {
|
|
|
22
22
|
init: InitFn
|
|
23
23
|
/** Template function for client-side component creation */
|
|
24
24
|
template?: (props: Record<string, unknown>) => string
|
|
25
|
-
/**
|
|
25
|
+
/**
|
|
26
|
+
* When true, this component's scope has no `bf-s`-carrying element of
|
|
27
|
+
* its own — a proxy element stands in for it. Set for TWO distinct
|
|
28
|
+
* shapes (see `fragmentRoot` below, which tells them apart):
|
|
29
|
+
* - a genuine fragment root (`<>...</>`), where the proxy is the
|
|
30
|
+
* fragment's own rendered content;
|
|
31
|
+
* - a root that is itself a single child component call, where the
|
|
32
|
+
* proxy is that child's own already-scoped element (#2649).
|
|
33
|
+
*/
|
|
26
34
|
comment?: boolean
|
|
35
|
+
/**
|
|
36
|
+
* True only for the genuine-fragment-root shape of `comment` above
|
|
37
|
+
* (`ir.root.type === 'fragment'`) — never for the root-is-a-child-call
|
|
38
|
+
* shape. `materializeComponent` (component.ts) uses this to decide
|
|
39
|
+
* whether a CSR mount must generate its OWN scope id (fragment root: yes,
|
|
40
|
+
* so nested `renderChild()` calls get parent-prefixed naming matching
|
|
41
|
+
* SSR/hydrate) or leave the scope id null to avoid overwriting the
|
|
42
|
+
* child's own (#2649's shape, #2722).
|
|
43
|
+
*/
|
|
44
|
+
fragmentRoot?: boolean
|
|
27
45
|
}
|