@barefootjs/jsx 0.18.4 → 0.18.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapters/dangerous-inner-html.d.ts +77 -0
- package/dist/adapters/dangerous-inner-html.d.ts.map +1 -0
- package/dist/adapters/loop-bound-names.d.ts +22 -0
- package/dist/adapters/loop-bound-names.d.ts.map +1 -0
- package/dist/adapters/parsed-expr-emitter.d.ts +2 -2
- package/dist/adapters/parsed-expr-emitter.d.ts.map +1 -1
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/expression-parser.d.ts +2 -1
- package/dist/expression-parser.d.ts.map +1 -1
- package/dist/index.d.ts +14 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1108 -307
- package/dist/ir-to-client-js/collect-elements.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/build-event-delegation.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/build-inner-loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/build-loop-child-arm.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/event-delegation.d.ts +10 -0
- package/dist/ir-to-client-js/control-flow/plan/event-delegation.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/loop-child-arm.d.ts +9 -0
- package/dist/ir-to-client-js/control-flow/plan/loop-child-arm.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/loop.d.ts +9 -0
- 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 +45 -3
- package/dist/ir-to-client-js/control-flow/shared.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/event-delegation.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/loop-child-arm.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/loop.d.ts +1 -0
- package/dist/ir-to-client-js/control-flow/stringify/loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/reactive-effects.d.ts +10 -0
- package/dist/ir-to-client-js/control-flow/stringify/reactive-effects.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/skeleton-paths.d.ts +38 -0
- package/dist/ir-to-client-js/control-flow/stringify/skeleton-paths.d.ts.map +1 -0
- package/dist/ir-to-client-js/control-flow/stringify/template-parse.d.ts +19 -0
- package/dist/ir-to-client-js/control-flow/stringify/template-parse.d.ts.map +1 -1
- package/dist/ir-to-client-js/csr-substitute.d.ts +24 -0
- package/dist/ir-to-client-js/csr-substitute.d.ts.map +1 -1
- package/dist/ir-to-client-js/html-template.d.ts +42 -0
- 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/plan/static-array-child-init.d.ts +20 -1
- package/dist/ir-to-client-js/plan/static-array-child-init.d.ts.map +1 -1
- package/dist/ir-to-client-js/reactivity.d.ts.map +1 -1
- package/dist/ir-to-client-js/stringify/static-array-child-init.d.ts +1 -1
- package/dist/ir-to-client-js/types.d.ts +30 -0
- package/dist/ir-to-client-js/types.d.ts.map +1 -1
- package/dist/ir-to-client-js/utils.d.ts +25 -0
- package/dist/ir-to-client-js/utils.d.ts.map +1 -1
- package/dist/jsx-to-ir.d.ts.map +1 -1
- package/dist/signal-init-eval.d.ts +82 -0
- package/dist/signal-init-eval.d.ts.map +1 -0
- package/dist/static-literal.d.ts +45 -0
- package/dist/static-literal.d.ts.map +1 -0
- package/dist/types.d.ts +66 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/__snapshots__/doc-examples.test.ts.snap +19 -13
- package/src/__tests__/create-selector.test.ts +110 -0
- package/src/__tests__/csr-template-loop-shadowing.test.ts +214 -0
- package/src/__tests__/dangerous-inner-html-resolver.test.ts +197 -0
- package/src/__tests__/event-delegation-index-param.test.ts +130 -0
- package/src/__tests__/expression-parser.test.ts +38 -0
- package/src/__tests__/inner-loop-svg-namespace.test.ts +245 -0
- package/src/__tests__/ir-walker.test.ts +1 -0
- package/src/__tests__/loop-bound-names.test.ts +102 -0
- package/src/__tests__/loop-plan-classification.test.ts +2 -0
- package/src/__tests__/map-function-reference.test.ts +303 -0
- package/src/__tests__/materialize-getter-calls.test.ts +1 -0
- package/src/__tests__/nested-loop-index-param.test.ts +285 -0
- package/src/__tests__/signal-init-eval.test.ts +138 -0
- package/src/__tests__/static-array-inner-loop-index-param.test.ts +180 -0
- package/src/__tests__/static-literal.test.ts +126 -0
- package/src/__tests__/string-concat-identifier.test.ts +55 -0
- package/src/adapters/dangerous-inner-html.ts +186 -0
- package/src/adapters/loop-bound-names.ts +80 -0
- package/src/adapters/parsed-expr-emitter.ts +19 -1
- package/src/analyzer.ts +6 -0
- package/src/expression-parser.ts +59 -25
- package/src/index.ts +19 -3
- package/src/ir-to-client-js/collect-elements.ts +18 -3
- package/src/ir-to-client-js/control-flow/plan/build-event-delegation.ts +6 -0
- package/src/ir-to-client-js/control-flow/plan/build-inner-loop.ts +21 -4
- package/src/ir-to-client-js/control-flow/plan/build-loop-child-arm.ts +13 -1
- package/src/ir-to-client-js/control-flow/plan/build-loop.ts +1 -0
- package/src/ir-to-client-js/control-flow/plan/event-delegation.ts +10 -0
- package/src/ir-to-client-js/control-flow/plan/loop-child-arm.ts +9 -0
- package/src/ir-to-client-js/control-flow/plan/loop.ts +9 -0
- package/src/ir-to-client-js/control-flow/shared.ts +103 -6
- package/src/ir-to-client-js/control-flow/stringify/event-delegation.ts +31 -7
- package/src/ir-to-client-js/control-flow/stringify/inner-loop.ts +11 -2
- package/src/ir-to-client-js/control-flow/stringify/loop-child-arm.ts +5 -0
- package/src/ir-to-client-js/control-flow/stringify/loop.ts +53 -10
- package/src/ir-to-client-js/control-flow/stringify/reactive-effects.ts +25 -4
- package/src/ir-to-client-js/control-flow/stringify/skeleton-paths.ts +70 -0
- package/src/ir-to-client-js/control-flow/stringify/template-parse.ts +25 -1
- package/src/ir-to-client-js/csr-substitute.ts +54 -1
- package/src/ir-to-client-js/html-template.ts +292 -13
- package/src/ir-to-client-js/imports.ts +1 -0
- package/src/ir-to-client-js/plan/build-static-array-child-init.ts +12 -1
- package/src/ir-to-client-js/plan/static-array-child-init.ts +20 -1
- package/src/ir-to-client-js/reactivity.ts +6 -0
- package/src/ir-to-client-js/stringify/static-array-child-init.ts +11 -6
- package/src/ir-to-client-js/types.ts +31 -0
- package/src/ir-to-client-js/utils.ts +34 -1
- package/src/jsx-to-ir.ts +322 -48
- package/src/signal-init-eval.ts +165 -0
- package/src/static-literal.ts +128 -0
- package/src/types.ts +68 -0
|
@@ -27,7 +27,8 @@
|
|
|
27
27
|
import { emitRefCall, varSlotId, profileBindingId } from '../../utils.ts'
|
|
28
28
|
import { emitAttrUpdate } from '../../emit-reactive.ts'
|
|
29
29
|
import { stringifyReactiveEffects } from './reactive-effects.ts'
|
|
30
|
-
import { emitTemplateCloneInline, emitLoopItemElementSetup, emitHoistedTemplateDecl, hoistedCloneExpr } from './template-parse.ts'
|
|
30
|
+
import { emitTemplateCloneInline, emitLoopItemElementSetup, emitHoistedTemplateDecl, hoistedCloneExpr, templateRootIsSvg, multiRootTemplateNeedsSvgWrap } from './template-parse.ts'
|
|
31
|
+
import { buildSkeletonPathPlan, type SkeletonPathPlan } from './skeleton-paths.ts'
|
|
31
32
|
import { stringifyComponentLoop } from './component-loop.ts'
|
|
32
33
|
import { stringifyCompositeLoop } from './composite-loop.ts'
|
|
33
34
|
import type { LoopChildRefBinding, LoopPlan, PlainLoopPlan, StaticLoopPlan } from '../plan/types.ts'
|
|
@@ -47,14 +48,18 @@ import type { LoopChildRefBinding, LoopPlan, PlainLoopPlan, StaticLoopPlan } fro
|
|
|
47
48
|
export function emitLoopChildRefs(
|
|
48
49
|
lines: string[],
|
|
49
50
|
refs: readonly LoopChildRefBinding[],
|
|
50
|
-
opts: { indent: string; elVar: string; bodyIsMultiRoot: boolean },
|
|
51
|
+
opts: { indent: string; elVar: string; bodyIsMultiRoot: boolean; elementIndexBySlot?: ReadonlyMap<string, number> },
|
|
51
52
|
): void {
|
|
52
53
|
if (refs.length === 0) return
|
|
53
|
-
const { indent, elVar, bodyIsMultiRoot } = opts
|
|
54
|
+
const { indent, elVar, bodyIsMultiRoot, elementIndexBySlot } = opts
|
|
54
55
|
const lookup = bodyIsMultiRoot ? 'qsaItem' : 'qsa'
|
|
55
56
|
for (const ref of refs) {
|
|
56
57
|
const varName = `__rf_${varSlotId(ref.childSlotId)}`
|
|
57
|
-
|
|
58
|
+
const pIdx = elementIndexBySlot?.get(ref.childSlotId)
|
|
59
|
+
const lookupExpr = pIdx !== undefined
|
|
60
|
+
? `__p ? __p[${pIdx}] : ${lookup}(${elVar}, '[bf="${ref.childSlotId}"]')`
|
|
61
|
+
: `${lookup}(${elVar}, '[bf="${ref.childSlotId}"]')`
|
|
62
|
+
lines.push(`${indent}{ const ${varName} = ${lookupExpr}`)
|
|
58
63
|
lines.push(`${indent}if (${varName}) ${emitRefCall(ref.callback, varName)} }`)
|
|
59
64
|
}
|
|
60
65
|
}
|
|
@@ -181,10 +186,37 @@ export function stringifyPlainLoop(
|
|
|
181
186
|
singleRootLayout: 'inline',
|
|
182
187
|
})
|
|
183
188
|
}
|
|
189
|
+
// Direct child-index paths (perf, #2143): only reachable when hoistedTpl
|
|
190
|
+
// is set, which itself requires a single-root, conditional-free body — so
|
|
191
|
+
// `reactiveEffects.conditionals` is always empty here and needs no path
|
|
192
|
+
// handling. `__p` is `null` on the hydration branch (`__existing` truthy)
|
|
193
|
+
// since the skeleton's empty markers/omitted attrs don't describe the
|
|
194
|
+
// SSR-rendered tree; every resolution falls back to qsa/$t there.
|
|
195
|
+
let pathPlan: SkeletonPathPlan | null = null
|
|
196
|
+
if (hoistedTpl && plan.skeletonPaths) {
|
|
197
|
+
const elementSlotIds = [
|
|
198
|
+
...(reactiveEffects?.attrSlots.map(s => s.slotId) ?? []),
|
|
199
|
+
...childRefs.map(r => r.childSlotId),
|
|
200
|
+
]
|
|
201
|
+
const textSlotIds = reactiveEffects?.outerTexts.map(t => t.slotId) ?? []
|
|
202
|
+
if (elementSlotIds.length > 0 || textSlotIds.length > 0) {
|
|
203
|
+
const built = buildSkeletonPathPlan(plan.skeletonPaths, '__el', { elementSlotIds, textSlotIds })
|
|
204
|
+
if (built.arrayElems.length > 0) {
|
|
205
|
+
pathPlan = built
|
|
206
|
+
lines.push(`${bodyIndent}const __p = __existing ? null : [${built.arrayElems.join(', ')}]`)
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
184
210
|
if (reactiveEffects !== null) {
|
|
185
|
-
stringifyReactiveEffects(lines, reactiveEffects, {
|
|
211
|
+
stringifyReactiveEffects(lines, reactiveEffects, {
|
|
212
|
+
indent: bodyIndent,
|
|
213
|
+
elVar: '__el',
|
|
214
|
+
bodyIsMultiRoot,
|
|
215
|
+
elementIndexBySlot: pathPlan?.elementIndexBySlot,
|
|
216
|
+
textIndexBySlot: pathPlan?.textIndexBySlot,
|
|
217
|
+
})
|
|
186
218
|
}
|
|
187
|
-
emitLoopChildRefs(lines, childRefs, { indent: bodyIndent, elVar: '__el', bodyIsMultiRoot })
|
|
219
|
+
emitLoopChildRefs(lines, childRefs, { indent: bodyIndent, elVar: '__el', bodyIsMultiRoot, elementIndexBySlot: pathPlan?.elementIndexBySlot })
|
|
188
220
|
lines.push(`${bodyIndent}return __el`)
|
|
189
221
|
lines.push(`${topIndent}}, '${markerId}'${loopBfId})`)
|
|
190
222
|
}
|
|
@@ -283,15 +315,26 @@ export function stringifyStaticLoop(lines: string[], plan: StaticLoopPlan): void
|
|
|
283
315
|
lines.push(` let __iterEl = ${containerVar}.children[${childIndexExpr}]`)
|
|
284
316
|
if (csrMaterialize) {
|
|
285
317
|
lines.push(` if (!__iterEl) {`)
|
|
318
|
+
// SVG-rooted item templates parse inside a synthetic `<svg>` wrap and
|
|
319
|
+
// descend one extra level (#2219) — `template.innerHTML` alone parses in
|
|
320
|
+
// the HTML namespace and the materialized elements would never draw.
|
|
321
|
+
// Mirrors `templateRootIsSvg` handling on the reactive clone paths;
|
|
322
|
+
// HTML-rooted templates keep byte-identical output. Multi-root fragments
|
|
323
|
+
// use the fragment-aware predicate so an `<svg>`-container-first fragment
|
|
324
|
+
// isn't over-wrapped (#2233 Copilot review).
|
|
325
|
+
const isSvg = csrMaterialize.bodyIsMultiRoot
|
|
326
|
+
? multiRootTemplateNeedsSvgWrap(csrMaterialize.itemTemplate)
|
|
327
|
+
: templateRootIsSvg(csrMaterialize.itemTemplate)
|
|
328
|
+
const itemHtml = isSvg ? `<svg>${csrMaterialize.itemTemplate}</svg>` : csrMaterialize.itemTemplate
|
|
286
329
|
if (csrMaterialize.bodyIsMultiRoot) {
|
|
287
330
|
// Multi-root: clone every top-level sibling of the per-item template and
|
|
288
331
|
// insert them in order. `__iterEl` is the first root (the one reactive
|
|
289
332
|
// bindings attach to); the rest land alongside it via insertBefore.
|
|
290
333
|
lines.push(` const __mtpl = document.createElement('template')`)
|
|
291
|
-
lines.push(` __mtpl.innerHTML = \`${
|
|
334
|
+
lines.push(` __mtpl.innerHTML = \`${itemHtml}\``)
|
|
292
335
|
lines.push(` const __anchor = ${containerVar}.children[${childIndexExpr}] ?? null`)
|
|
293
336
|
lines.push(` let __first = null`)
|
|
294
|
-
lines.push(` let __sib = __mtpl.content.firstElementChild`)
|
|
337
|
+
lines.push(` let __sib = __mtpl.content${isSvg ? '.firstElementChild' : ''}.firstElementChild`)
|
|
295
338
|
lines.push(` while (__sib) {`)
|
|
296
339
|
lines.push(` const __next = __sib.nextElementSibling`)
|
|
297
340
|
lines.push(` const __cloned = __sib.cloneNode(true)`)
|
|
@@ -302,8 +345,8 @@ export function stringifyStaticLoop(lines: string[], plan: StaticLoopPlan): void
|
|
|
302
345
|
lines.push(` __iterEl = __first`)
|
|
303
346
|
} else {
|
|
304
347
|
lines.push(` const __tpl = document.createElement('template')`)
|
|
305
|
-
lines.push(` __tpl.innerHTML = \`${
|
|
306
|
-
lines.push(` const __cloned = __tpl.content.firstElementChild`)
|
|
348
|
+
lines.push(` __tpl.innerHTML = \`${itemHtml}\``)
|
|
349
|
+
lines.push(` const __cloned = __tpl.content${isSvg ? '.firstElementChild' : ''}.firstElementChild`)
|
|
307
350
|
lines.push(` if (__cloned) {`)
|
|
308
351
|
lines.push(` const __anchor = ${containerVar}.children[${childIndexExpr}] ?? null`)
|
|
309
352
|
lines.push(` ${containerVar}.insertBefore(__cloned, __anchor)`)
|
|
@@ -40,6 +40,16 @@ export interface StringifyReactiveEffectsOptions {
|
|
|
40
40
|
* `<!--bf-loop-i-->`-bounded siblings). Optional — defaults to `false`.
|
|
41
41
|
*/
|
|
42
42
|
bodyIsMultiRoot?: boolean
|
|
43
|
+
/**
|
|
44
|
+
* Compile-time `__p` index for each attr slotId (perf, #2143) — see
|
|
45
|
+
* `buildSkeletonPathPlan`. When a slot has an entry, the emitted lookup
|
|
46
|
+
* becomes `__p ? __p[i] : qsa(...)` instead of the bare `qsa(...)` call;
|
|
47
|
+
* `__p` is `null` on the hydration branch (`__existing`), so hydration
|
|
48
|
+
* still resolves through the battle-tested runtime lookup.
|
|
49
|
+
*/
|
|
50
|
+
elementIndexBySlot?: ReadonlyMap<string, number>
|
|
51
|
+
/** Same as `elementIndexBySlot`, for text-marker slots (`$t`). */
|
|
52
|
+
textIndexBySlot?: ReadonlyMap<string, number>
|
|
43
53
|
}
|
|
44
54
|
|
|
45
55
|
export function stringifyReactiveEffects(
|
|
@@ -47,7 +57,7 @@ export function stringifyReactiveEffects(
|
|
|
47
57
|
plan: ReactiveEffectsPlan,
|
|
48
58
|
opts: StringifyReactiveEffectsOptions,
|
|
49
59
|
): void {
|
|
50
|
-
const { indent, elVar, bodyIsMultiRoot } = opts
|
|
60
|
+
const { indent, elVar, bodyIsMultiRoot, elementIndexBySlot, textIndexBySlot } = opts
|
|
51
61
|
const lookup = bodyIsMultiRoot ? 'qsaItem' : 'qsa'
|
|
52
62
|
const pc = plan.profileComponentName
|
|
53
63
|
const bindingBfId = (slotId: string): string => profileBindingId(pc, slotId)
|
|
@@ -55,7 +65,11 @@ export function stringifyReactiveEffects(
|
|
|
55
65
|
// 1. Reactive attribute effects (one qsa per slot, then per-attr createEffect).
|
|
56
66
|
for (const slot of plan.attrSlots) {
|
|
57
67
|
const varName = `__ra_${varSlotId(slot.slotId)}`
|
|
58
|
-
|
|
68
|
+
const pIdx = elementIndexBySlot?.get(slot.slotId)
|
|
69
|
+
const lookupExpr = pIdx !== undefined
|
|
70
|
+
? `__p ? __p[${pIdx}] : ${lookup}(${elVar}, '[bf="${slot.slotId}"]')`
|
|
71
|
+
: `${lookup}(${elVar}, '[bf="${slot.slotId}"]')`
|
|
72
|
+
lines.push(`${indent}{ const ${varName} = ${lookupExpr}`)
|
|
59
73
|
lines.push(`${indent}if (${varName}) {`)
|
|
60
74
|
for (const attr of slot.attrs) {
|
|
61
75
|
lines.push(`${indent} createEffect(() => {`)
|
|
@@ -69,7 +83,7 @@ export function stringifyReactiveEffects(
|
|
|
69
83
|
|
|
70
84
|
// 2. Outer text effects (slots NOT inside any conditional branch).
|
|
71
85
|
for (const text of plan.outerTexts) {
|
|
72
|
-
emitOuterText(lines, indent, elVar, text, bindingBfId(text.slotId))
|
|
86
|
+
emitOuterText(lines, indent, elVar, text, bindingBfId(text.slotId), textIndexBySlot?.get(text.slotId))
|
|
73
87
|
}
|
|
74
88
|
|
|
75
89
|
// 3. Reactive conditionals — each emits an insert(...) over `elVar` whose
|
|
@@ -85,9 +99,16 @@ function emitOuterText(
|
|
|
85
99
|
elVar: string,
|
|
86
100
|
text: ReactiveTextEffect,
|
|
87
101
|
bfId: string = '',
|
|
102
|
+
pIdx?: number,
|
|
88
103
|
): void {
|
|
89
104
|
const varName = `__rt_${varSlotId(text.slotId)}`
|
|
90
|
-
|
|
105
|
+
if (pIdx !== undefined) {
|
|
106
|
+
// Direct-path resolution (#2143): `__p` holds the marker Comment node;
|
|
107
|
+
// `tAfter` mirrors `$t`'s create-if-absent Text-node semantics exactly.
|
|
108
|
+
lines.push(`${indent}{ const ${varName} = __p ? tAfter(__p[${pIdx}]) : $t(${elVar}, '${text.slotId}')[0]`)
|
|
109
|
+
} else {
|
|
110
|
+
lines.push(`${indent}{ const [${varName}] = $t(${elVar}, '${text.slotId}')`)
|
|
111
|
+
}
|
|
91
112
|
lines.push(`${indent}if (${varName}) createEffect(() => { ${varName}.textContent = String(${text.wrappedExpression}) }${bfId}) }`)
|
|
92
113
|
}
|
|
93
114
|
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Turn compile-time `SkeletonSlotPaths` (html-template.ts) into the `__p`
|
|
3
|
+
* array-literal contents plus slot -> index maps consumed by
|
|
4
|
+
* `stringifyPlainLoop` / `stringifyReactiveEffects` / `emitLoopChildRefs`
|
|
5
|
+
* (perf: direct child-index paths for hoisted single-root loops, #2143).
|
|
6
|
+
*
|
|
7
|
+
* Single source of truth for `__p` ordering: `loop.ts` (which emits the
|
|
8
|
+
* array literal) and `reactive-effects.ts` (which emits the per-slot index
|
|
9
|
+
* reads) both derive their indices from the SAME `SkeletonPathPlan`, so the
|
|
10
|
+
* two can never drift out of sync.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { SkeletonSlotPaths } from '../../html-template.ts'
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Convert a root-relative child-index path into a `.firstChild.nextSibling…`
|
|
17
|
+
* property chain off `base`. An empty path resolves to `base` itself (the
|
|
18
|
+
* slot lives on the clone root, e.g. an attr on the loop body's own tag).
|
|
19
|
+
*/
|
|
20
|
+
export function pathExpr(base: string, path: readonly number[]): string {
|
|
21
|
+
let expr = base
|
|
22
|
+
for (const idx of path) {
|
|
23
|
+
expr += '.firstChild'
|
|
24
|
+
if (idx > 0) expr += '.nextSibling'.repeat(idx)
|
|
25
|
+
}
|
|
26
|
+
return expr
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface SkeletonPathPlan {
|
|
30
|
+
/** Expressions for the `const __p = [...]` array literal, in stable order. */
|
|
31
|
+
arrayElems: string[]
|
|
32
|
+
/** slotId -> index into `__p`, for element-anchored slots (reactive attrs, refs). */
|
|
33
|
+
elementIndexBySlot: Map<string, number>
|
|
34
|
+
/** slotId -> index into `__p`, for text-marker-anchored slots (reactive texts). */
|
|
35
|
+
textIndexBySlot: Map<string, number>
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Build the `__p` plan for a hoisted loop's reactive attr / text / ref
|
|
40
|
+
* slots. A slotId absent from `skeletonPaths` (shouldn't happen for a
|
|
41
|
+
* successfully-hoisted skeleton, but handled defensively) is simply left
|
|
42
|
+
* out of both maps — its stringifier call site falls back to `qsa`/`$t`
|
|
43
|
+
* for that one slot without affecting any other slot's resolution.
|
|
44
|
+
*/
|
|
45
|
+
export function buildSkeletonPathPlan(
|
|
46
|
+
skeletonPaths: SkeletonSlotPaths,
|
|
47
|
+
elVar: string,
|
|
48
|
+
opts: { elementSlotIds: readonly string[]; textSlotIds: readonly string[] },
|
|
49
|
+
): SkeletonPathPlan {
|
|
50
|
+
const arrayElems: string[] = []
|
|
51
|
+
const elementIndexBySlot = new Map<string, number>()
|
|
52
|
+
const textIndexBySlot = new Map<string, number>()
|
|
53
|
+
|
|
54
|
+
for (const slotId of opts.elementSlotIds) {
|
|
55
|
+
if (elementIndexBySlot.has(slotId)) continue
|
|
56
|
+
const path = skeletonPaths.elementPaths.get(slotId)
|
|
57
|
+
if (!path) continue
|
|
58
|
+
elementIndexBySlot.set(slotId, arrayElems.length)
|
|
59
|
+
arrayElems.push(pathExpr(elVar, path))
|
|
60
|
+
}
|
|
61
|
+
for (const slotId of opts.textSlotIds) {
|
|
62
|
+
if (textIndexBySlot.has(slotId)) continue
|
|
63
|
+
const path = skeletonPaths.textMarkerPaths.get(slotId)
|
|
64
|
+
if (!path) continue
|
|
65
|
+
textIndexBySlot.set(slotId, arrayElems.length)
|
|
66
|
+
arrayElems.push(pathExpr(elVar, path))
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return { arrayElems, elementIndexBySlot, textIndexBySlot }
|
|
70
|
+
}
|
|
@@ -82,6 +82,30 @@ export function templateRootIsSvg(template: string): boolean {
|
|
|
82
82
|
return branches.every(templateRootIsSvg)
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
+
/**
|
|
86
|
+
* Wrap decision for MULTI-ROOT (fragment) templates, where the synthetic
|
|
87
|
+
* `<svg>` wrap swallows every sibling root at once (#2233 Copilot review).
|
|
88
|
+
*
|
|
89
|
+
* `templateRootIsSvg` inspects only the FIRST root tag. For single-root
|
|
90
|
+
* templates that's exact, but a fragment whose first root is an `<svg>`
|
|
91
|
+
* CONTAINER (`<><svg/><span/></>`) doesn't need the wrap at all — the
|
|
92
|
+
* HTML parser enters foreign content at `<svg>` on its own — and wrapping
|
|
93
|
+
* would drag the HTML siblings into the SVG namespace (`<span>` becomes an
|
|
94
|
+
* SVGUnknownElement, silently undrawn). So `<svg>`-first fragments skip
|
|
95
|
+
* the wrap; only leaf-rooted fragments (`<line>`, `<circle>`, ...) get it.
|
|
96
|
+
*
|
|
97
|
+
* Known edge (degenerate, pre-existing): an `<svg>`-first fragment with
|
|
98
|
+
* SVG-LEAF siblings (`<><svg/><line/></>`) leaves the bare leaf siblings
|
|
99
|
+
* in the HTML namespace — exactly the pre-#2219 behavior. Deciding that
|
|
100
|
+
* shape correctly needs a scan of every top-level root tag; not worth the
|
|
101
|
+
* parser until a real component hits it.
|
|
102
|
+
*/
|
|
103
|
+
export function multiRootTemplateNeedsSvgWrap(template: string): boolean {
|
|
104
|
+
const m = stripLeadingNonContent(template).match(/^<\s*([A-Za-z][A-Za-z0-9-]*)/)
|
|
105
|
+
if (m && m[1].toLowerCase() === 'svg') return false
|
|
106
|
+
return templateRootIsSvg(template)
|
|
107
|
+
}
|
|
108
|
+
|
|
85
109
|
/**
|
|
86
110
|
* Strip leading whitespace and HTML comment markers (`<!-- ... -->`) so
|
|
87
111
|
* that a branch like `<!--bf-cond-start:s0-->${...}<!--bf-cond-end:s0-->`
|
|
@@ -269,7 +293,7 @@ export function emitMultiRootTemplateCloneLines(
|
|
|
269
293
|
varEl: string,
|
|
270
294
|
varExtras: string,
|
|
271
295
|
): string[] {
|
|
272
|
-
const isSvg =
|
|
296
|
+
const isSvg = multiRootTemplateNeedsSvgWrap(template)
|
|
273
297
|
// Wrap in `<svg>` so the parser walks into SVG foreign content; we then
|
|
274
298
|
// descend one level to pick up the per-item roots.
|
|
275
299
|
const innerHtmlExpr = isSvg ? `\`<svg>${template}</svg>\`` : `\`${template}\``
|
|
@@ -323,7 +323,7 @@ export function applyPropsRewrite(text: string, propsObjectName: string | null):
|
|
|
323
323
|
return text.replace(new RegExp(`\\b${propsObjectName}\\.`, 'g'), `${PROPS_PARAM}.`)
|
|
324
324
|
}
|
|
325
325
|
|
|
326
|
-
function extractFreeIdentifiersFromText(text: string): Set<string> {
|
|
326
|
+
export function extractFreeIdentifiersFromText(text: string): Set<string> {
|
|
327
327
|
if (!text || text.trim().length === 0) return new Set()
|
|
328
328
|
const sf = ts.createSourceFile(
|
|
329
329
|
'__free_ids__.ts',
|
|
@@ -338,6 +338,59 @@ function extractFreeIdentifiersFromText(text: string): Set<string> {
|
|
|
338
338
|
return extractFreeIdentifiersFromNode(expr)
|
|
339
339
|
}
|
|
340
340
|
|
|
341
|
+
/**
|
|
342
|
+
* Free identifiers referenced by a block of JS *statements* — e.g. an inner
|
|
343
|
+
* `.map()` callback's block-body preamble (`mapPreamble`, #1052). Unlike
|
|
344
|
+
* `extractFreeIdentifiersFromText`, which wraps its input in `(...)` to
|
|
345
|
+
* force single-expression parsing, statement text (`const x = ...;`) is
|
|
346
|
+
* not a valid expression and would fail to parse inside parens — parse it
|
|
347
|
+
* as top-level source instead. Used by nested-loop index-param reference
|
|
348
|
+
* gating (#2218).
|
|
349
|
+
*/
|
|
350
|
+
export function extractFreeIdentifiersFromStatementText(text: string): Set<string> {
|
|
351
|
+
if (!text || text.trim().length === 0) return new Set()
|
|
352
|
+
const sf = ts.createSourceFile(
|
|
353
|
+
'__free_ids_stmt__.ts',
|
|
354
|
+
text,
|
|
355
|
+
ts.ScriptTarget.Latest,
|
|
356
|
+
/* setParentNodes */ true,
|
|
357
|
+
ts.ScriptKind.TS,
|
|
358
|
+
)
|
|
359
|
+
return extractFreeIdentifiersFromNode(sf)
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Free identifiers referenced inside a template-literal-style string's
|
|
364
|
+
* `${...}` interpolations — e.g. a nested loop's per-item HTML `template`
|
|
365
|
+
* (#2218). Wraps the text in backticks and parses it as a real
|
|
366
|
+
* `TemplateExpression` so brace balancing, string literals, and nested
|
|
367
|
+
* object/array literals inside `${...}` are handled correctly by the TS
|
|
368
|
+
* parser (AST-based — never the char-class regex `extractTemplateExpressions`
|
|
369
|
+
* in `identifiers.ts` uses for its looser references-graph pass). Safe by
|
|
370
|
+
* construction: the same `template` string is embedded verbatim inside a
|
|
371
|
+
* real backtick literal at emit time (`__t.innerHTML = \`${template}\``),
|
|
372
|
+
* so wrapping it here to parse mirrors exactly how it's already used.
|
|
373
|
+
*/
|
|
374
|
+
export function extractFreeIdentifiersFromTemplateText(template: string): Set<string> {
|
|
375
|
+
if (!template || template.length === 0) return new Set()
|
|
376
|
+
const sf = ts.createSourceFile(
|
|
377
|
+
'__free_ids_template__.ts',
|
|
378
|
+
`(\`${template}\`);`,
|
|
379
|
+
ts.ScriptTarget.Latest,
|
|
380
|
+
/* setParentNodes */ true,
|
|
381
|
+
ts.ScriptKind.TS,
|
|
382
|
+
)
|
|
383
|
+
const stmt = sf.statements[0]
|
|
384
|
+
if (!stmt || !ts.isExpressionStatement(stmt)) return new Set()
|
|
385
|
+
const expr = ts.isParenthesizedExpression(stmt.expression) ? stmt.expression.expression : stmt.expression
|
|
386
|
+
if (!ts.isTemplateExpression(expr)) return new Set()
|
|
387
|
+
const ids = new Set<string>()
|
|
388
|
+
for (const span of expr.templateSpans) {
|
|
389
|
+
for (const id of extractFreeIdentifiersFromNode(span.expression)) ids.add(id)
|
|
390
|
+
}
|
|
391
|
+
return ids
|
|
392
|
+
}
|
|
393
|
+
|
|
341
394
|
/**
|
|
342
395
|
* Reduce a memo's `() => expr` source to the expression that should be
|
|
343
396
|
* substituted in for `memoName()`. Matches the extraction done by the
|