@barefootjs/jsx 0.18.5 → 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapters/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.map +1 -1
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/expression-parser.d.ts +8 -0
- package/dist/expression-parser.d.ts.map +1 -1
- package/dist/index.d.ts +15 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1002 -284
- package/dist/ir-to-client-js/collect-elements.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/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/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 +23 -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/phases/props-extraction.d.ts +0 -1
- package/dist/ir-to-client-js/phases/props-extraction.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 +21 -0
- package/dist/ir-to-client-js/types.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/package.json +2 -2
- package/src/__tests__/__snapshots__/doc-examples.test.ts.snap +19 -13
- package/src/__tests__/client-js-generation.test.ts +40 -0
- 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__/inner-loop-svg-namespace.test.ts +245 -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__/nested-loop-index-param.test.ts +285 -0
- package/src/__tests__/props-destructuring.test.ts +61 -8
- 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 +9 -0
- package/src/analyzer.ts +36 -22
- package/src/expression-parser.ts +33 -0
- package/src/index.ts +20 -3
- package/src/ir-to-client-js/collect-elements.ts +15 -3
- 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/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/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 +53 -0
- package/src/ir-to-client-js/html-template.ts +235 -2
- package/src/ir-to-client-js/imports.ts +1 -0
- package/src/ir-to-client-js/phases/props-extraction.ts +5 -9
- 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 +21 -0
- package/src/jsx-to-ir.ts +230 -39
- package/src/signal-init-eval.ts +165 -0
- package/src/static-literal.ts +128 -0
package/src/index.ts
CHANGED
|
@@ -49,6 +49,7 @@ export type {
|
|
|
49
49
|
ParamInfo,
|
|
50
50
|
PropertyInfo,
|
|
51
51
|
MemoInfo,
|
|
52
|
+
ConstantInfo,
|
|
52
53
|
TypeInfo,
|
|
53
54
|
TypeDefinition,
|
|
54
55
|
SourceLocation,
|
|
@@ -87,6 +88,9 @@ export type { JsxAdapterConfig } from './adapters/jsx-adapter.ts'
|
|
|
87
88
|
export { rewriteImportsForTemplate } from './adapters/template-imports.ts'
|
|
88
89
|
export { emitParsedExpr, groupBinaryOperand, isStringTypedOperand, isStringConcatBinary } from './adapters/parsed-expr-emitter.ts'
|
|
89
90
|
export type { ParsedExprEmitter, HigherOrderMethod, ArrayMethod, SortMethod, LiteralType } from './adapters/parsed-expr-emitter.ts'
|
|
91
|
+
export { collectLoopBoundNames } from './adapters/loop-bound-names.ts'
|
|
92
|
+
export { evaluateSignalInit, tryEvaluateSignalInit, type SignalInitEvalResult } from './signal-init-eval.ts'
|
|
93
|
+
export { evaluateStaticLiteral, isFullyStaticLiteral, resolveStaticLoopSource } from './static-literal.ts'
|
|
90
94
|
export { importsSearchParams, searchParamsLocalNames, envSignalLocalNames, envSignalReaderFor, ENV_SIGNAL_READERS, queryHrefLocalNames, matchSearchParamsMethodCall } from './adapters/env-signal.ts'
|
|
91
95
|
export type { EnvSignalReader } from './adapters/env-signal.ts'
|
|
92
96
|
export { matchQueryHrefCall, queryHrefArgs, type QueryHrefCall, type QueryHrefTriple } from './query-href-lowering.ts'
|
|
@@ -116,6 +120,13 @@ export { emitIRNode } from './adapters/ir-node-emitter.ts'
|
|
|
116
120
|
export type { IRNodeEmitter, EmitIRNode } from './adapters/ir-node-emitter.ts'
|
|
117
121
|
export { emitAttrValue } from './adapters/attr-value-emitter.ts'
|
|
118
122
|
export type { AttrValueEmitter } from './adapters/attr-value-emitter.ts'
|
|
123
|
+
export {
|
|
124
|
+
isDangerousInnerHtmlAttr,
|
|
125
|
+
resolveDangerousInnerHtml,
|
|
126
|
+
dangerousInnerHtmlMetacharViolation,
|
|
127
|
+
dangerousInnerHtmlDiagnostic,
|
|
128
|
+
} from './adapters/dangerous-inner-html.ts'
|
|
129
|
+
export type { DangerousInnerHtmlResolution } from './adapters/dangerous-inner-html.ts'
|
|
119
130
|
|
|
120
131
|
// Client JS Generator
|
|
121
132
|
export { generateClientJs, generateClientJsWithSourceMap, analyzeClientNeeds } from './ir-to-client-js/index.ts'
|
|
@@ -272,15 +283,20 @@ export interface BuildOptions {
|
|
|
272
283
|
* - `'treeshake'` (default) — bundle only the runtime exports this
|
|
273
284
|
* project's compiled client JS actually imports, plus a small
|
|
274
285
|
* always-kept public mount API (`render`, `hydrate`, etc.).
|
|
286
|
+
* - `'treeshake-exact'` — same collection, but without the always-kept
|
|
287
|
+
* set. Smaller output; a hand-written page script the CLI never
|
|
288
|
+
* compiles must list any runtime names it calls directly in
|
|
289
|
+
* `runtimeKeep`, or they're silently dropped.
|
|
275
290
|
* - `'full'` — copy the entire prebuilt runtime bundle verbatim.
|
|
276
291
|
* See `@barefootjs/cli`'s `runtime-treeshake.ts` for the collector and
|
|
277
292
|
* `ALWAYS_KEEP_RUNTIME_EXPORTS` for the always-kept names.
|
|
278
293
|
*/
|
|
279
|
-
runtimeBundle?: 'treeshake' | 'full'
|
|
294
|
+
runtimeBundle?: 'treeshake' | 'treeshake-exact' | 'full'
|
|
280
295
|
/**
|
|
281
296
|
* Extra `@barefootjs/client*` export names to force-keep in `barefoot.js`
|
|
282
|
-
* under `runtimeBundle: 'treeshake'` — for names
|
|
283
|
-
* from hand-written page scripts the CLI never
|
|
297
|
+
* under `runtimeBundle: 'treeshake'` or `'treeshake-exact'` — for names
|
|
298
|
+
* only ever referenced from hand-written page scripts the CLI never
|
|
299
|
+
* compiles.
|
|
284
300
|
*/
|
|
285
301
|
runtimeKeep?: string[]
|
|
286
302
|
}
|
|
@@ -306,6 +322,7 @@ export { ErrorCodes, createError, formatError, generateCodeFrame } from './error
|
|
|
306
322
|
// Expression Parser
|
|
307
323
|
export { parseExpression, tsNodeToParsedExpr, asCallbackMethodCall, CALLBACK_METHODS, sortComparatorFromArrow, serializeParsedExpr, freeVarsInBody, freeIdentifiers, materializeGetterCalls, isSupported, exprToString, stringifyParsedExpr, identifierPath, parseBlockBody, parseBlockBodyTolerant, foldBlockToExpr, predicateTernaryToLogical, containsHigherOrder, extractArrowBodyExpression, parseStyleObjectEntries, parseProviderObjectLiteral, type ProviderObjectMember, type FoldBlockOptions } from './expression-parser.ts'
|
|
308
324
|
export type { StyleObjectEntry } from './expression-parser.ts'
|
|
325
|
+
export { PARSED_EXPR_KINDS } from './expression-parser.ts'
|
|
309
326
|
export type { ParsedExpr, ObjectLiteralProperty, ParsedStatement, SortComparator, SortKey, FlatDepth, SupportLevel, SupportResult, TemplatePart } from './expression-parser.ts'
|
|
310
327
|
export { buildLoopChainExpr } from './loop-chain.ts'
|
|
311
328
|
export type { LoopChainInputs } from './loop-chain.ts'
|
|
@@ -6,7 +6,8 @@ import { type IRNode, type IRElement, type IRComponent, type IRLoop, type IRProp
|
|
|
6
6
|
import type { ClientJsContext, ConditionalBranchChildComponent, ConditionalBranchReactiveAttr, BranchLoop, ConditionalBranchTextEffect, ConditionalElement, LoopChildBindings, LoopChildBranchSummary, LoopChildConditional, LoopOffset, NestedLoop } from './types.ts'
|
|
7
7
|
import { attrValueToString, freeIdsFromRefs, quotePropName, PROPS_PARAM } from './utils.ts'
|
|
8
8
|
import { classifyReactivity, decideWrapForAttr, decideWrapForChildProp, decideWrapFromAstFlags, collectEventHandlersFromIR, collectConditionalBranchEvents, collectConditionalBranchRefs, collectConditionalBranchChildComponents, collectLoopChildEventsWithNesting, collectLoopChildReactiveAttrs, collectLoopChildReactiveTexts, collectLoopChildRefs, emptyLoopChildBindings } from './reactivity.ts'
|
|
9
|
-
import { irToHtmlTemplate, irToPlaceholderTemplate, irChildrenToJsExpr, buildLoopSkeletonTemplate } from './html-template.ts'
|
|
9
|
+
import { irToHtmlTemplate, irToPlaceholderTemplate, irChildrenToJsExpr, buildLoopSkeletonTemplate, computeSkeletonSlotPaths, type SkeletonSlotPaths } from './html-template.ts'
|
|
10
|
+
import { templateRootIsSvg } from './control-flow/stringify/template-parse.ts'
|
|
10
11
|
import { expandDynamicPropValue, expandConstantForReactivity } from './prop-handling.ts'
|
|
11
12
|
import { walkIR, stopAt } from './walker.ts'
|
|
12
13
|
import { buildLoopChainExpr } from '../loop-chain.ts'
|
|
@@ -385,6 +386,7 @@ export function collectInnerLoops(
|
|
|
385
386
|
arrayFreeIdentifiers: n.arrayFreeIdentifiers,
|
|
386
387
|
param: n.param,
|
|
387
388
|
paramBindings: n.paramBindings,
|
|
389
|
+
index: n.index,
|
|
388
390
|
key: n.key,
|
|
389
391
|
markerId: n.markerId,
|
|
390
392
|
bodyIsMultiRoot: n.bodyIsMultiRoot,
|
|
@@ -673,6 +675,7 @@ export function collectElements(
|
|
|
673
675
|
let template = ''
|
|
674
676
|
let staticItemTemplate: string | undefined
|
|
675
677
|
let skeletonTemplate: string | undefined
|
|
678
|
+
let skeletonPaths: SkeletonSlotPaths | undefined
|
|
676
679
|
if (l.childComponent) {
|
|
677
680
|
template = '' // childComponent path uses createComponent directly
|
|
678
681
|
// CSR materialize fallback (#1268): when the loop array references an
|
|
@@ -724,10 +727,18 @@ export function collectElements(
|
|
|
724
727
|
// expressions, …) and returns `null` for anything it can't prove
|
|
725
728
|
// safe; the plan builder (`build-loop.ts`) falls back to the
|
|
726
729
|
// per-row `template` above whenever this stays `undefined`.
|
|
727
|
-
|
|
730
|
+
const skeletonSafeSlots = {
|
|
728
731
|
reactiveAttrKeys: new Set(bindings.reactiveAttrs.map(a => `${a.childSlotId}::${a.attrName}`)),
|
|
729
732
|
reactiveTextSlotIds: new Set(bindings.reactiveTexts.map(t => t.slotId)),
|
|
730
|
-
}
|
|
733
|
+
}
|
|
734
|
+
skeletonTemplate = buildLoopSkeletonTemplate(l.children[0], skeletonSafeSlots) ?? undefined
|
|
735
|
+
// Direct child-index paths (perf, #2143): only attempted when the
|
|
736
|
+
// skeleton itself hoisted, and skipped for SVG roots for now (the
|
|
737
|
+
// `<svg>`-wrap namespace fix-up is orthogonal and untested against
|
|
738
|
+
// this path model — safe fallback to qsa/$t for those loops).
|
|
739
|
+
if (skeletonTemplate && !templateRootIsSvg(skeletonTemplate)) {
|
|
740
|
+
skeletonPaths = computeSkeletonSlotPaths(l.children[0], skeletonSafeSlots) ?? undefined
|
|
741
|
+
}
|
|
731
742
|
}
|
|
732
743
|
}
|
|
733
744
|
|
|
@@ -748,6 +759,7 @@ export function collectElements(
|
|
|
748
759
|
template,
|
|
749
760
|
staticItemTemplate,
|
|
750
761
|
skeletonTemplate,
|
|
762
|
+
skeletonPaths,
|
|
751
763
|
childEventHandlers: childHandlers,
|
|
752
764
|
bindings,
|
|
753
765
|
childComponent: l.childComponent,
|
|
@@ -58,6 +58,7 @@ import type { DepthLevel } from '../shared.ts'
|
|
|
58
58
|
import {
|
|
59
59
|
destructureLoopParam,
|
|
60
60
|
loopKeyFn,
|
|
61
|
+
nestedLoopIndexAlias,
|
|
61
62
|
} from '../shared.ts'
|
|
62
63
|
import type {
|
|
63
64
|
InnerLoopPlan,
|
|
@@ -114,8 +115,8 @@ export function buildInnerLoopsPlan(args: BuildInnerLoopsArgs): InnerLoopsPlan {
|
|
|
114
115
|
const useReactive = refsParent && !!inner.template
|
|
115
116
|
|
|
116
117
|
const emit: InnerLoopReactiveEmit | InnerLoopStaticEmit = useReactive
|
|
117
|
-
? buildReactiveEmit(inner, level, wrapOuter)
|
|
118
|
-
: buildStaticEmit(inner, level)
|
|
118
|
+
? buildReactiveEmit(inner, level, wrapOuter, uidSuffix)
|
|
119
|
+
: buildStaticEmit(inner, level, uidSuffix)
|
|
119
120
|
|
|
120
121
|
const arrayExpr = useReactive ? wrapOuter(inner.array) : inner.array
|
|
121
122
|
|
|
@@ -153,6 +154,7 @@ function buildReactiveEmit(
|
|
|
153
154
|
inner: NestedLoop,
|
|
154
155
|
level: DepthLevel,
|
|
155
156
|
wrapOuter: (expr: string) => string,
|
|
157
|
+
uidSuffix: string,
|
|
156
158
|
): InnerLoopReactiveEmit {
|
|
157
159
|
const wrapInner = (expr: string) => wrapLoopParamAsAccessor(expr, inner.param, inner.paramBindings)
|
|
158
160
|
const { head: paramHead, unwrap: paramUnwrap } = destructureLoopParam(inner.param, inner.paramBindings)
|
|
@@ -218,7 +220,14 @@ function buildReactiveEmit(
|
|
|
218
220
|
// The destructure unwrap (when `inner.param` is a binding pattern)
|
|
219
221
|
// has to land before the preamble so the preamble's bare-binding
|
|
220
222
|
// references resolve.
|
|
223
|
+
//
|
|
224
|
+
// The index alias (#2218) lands first — before both the unwrap and the
|
|
225
|
+
// preamble — since either may reference the user's index name (e.g. a
|
|
226
|
+
// `const rowTag = \`row-${i}\`` preamble line), and both run before the
|
|
227
|
+
// cloned-template IIFE that may also reference it.
|
|
221
228
|
const preludeStatements: string[] = []
|
|
229
|
+
const indexAlias = nestedLoopIndexAlias(inner, `__innerIdx${uidSuffix}`, paramHead, level.comps, level.events)
|
|
230
|
+
if (indexAlias) preludeStatements.push(indexAlias)
|
|
222
231
|
if (paramUnwrap) preludeStatements.push(paramUnwrap)
|
|
223
232
|
if (inner.mapPreamble) preludeStatements.push(wrapInner(wrapOuter(inner.mapPreamble)))
|
|
224
233
|
|
|
@@ -240,7 +249,7 @@ function buildReactiveEmit(
|
|
|
240
249
|
}
|
|
241
250
|
}
|
|
242
251
|
|
|
243
|
-
function buildStaticEmit(inner: NestedLoop, level: DepthLevel): InnerLoopStaticEmit {
|
|
252
|
+
function buildStaticEmit(inner: NestedLoop, level: DepthLevel, uidSuffix: string): InnerLoopStaticEmit {
|
|
244
253
|
// Static `forEach` iterates with the literal item as its first param, so
|
|
245
254
|
// no signal-accessor rewrite is needed — emit the preamble verbatim
|
|
246
255
|
// before the component/event setup so prop getters and event handlers
|
|
@@ -248,7 +257,15 @@ function buildStaticEmit(inner: NestedLoop, level: DepthLevel): InnerLoopStaticE
|
|
|
248
257
|
// wrapping the callback would rewrite `s.x` to `s().x` and throw at
|
|
249
258
|
// runtime when the callback closes over the static inner param (#1244,
|
|
250
259
|
// PR #1352 Copilot review).
|
|
251
|
-
|
|
260
|
+
//
|
|
261
|
+
// The index alias (#2218) lands first, same rationale as the reactive
|
|
262
|
+
// path: `forEach`'s second param is the synthetic `__innerIdx<uid>`, not
|
|
263
|
+
// the user's index name, so a preamble/prop/event that reads it needs the
|
|
264
|
+
// alias bound before anything else runs.
|
|
265
|
+
const preludeStatements: string[] = []
|
|
266
|
+
const indexAlias = nestedLoopIndexAlias(inner, `__innerIdx${uidSuffix}`, inner.param, level.comps, level.events)
|
|
267
|
+
if (indexAlias) preludeStatements.push(indexAlias)
|
|
268
|
+
if (inner.mapPreamble) preludeStatements.push(inner.mapPreamble)
|
|
252
269
|
return {
|
|
253
270
|
mode: 'static',
|
|
254
271
|
rawKey: inner.key ?? null,
|
|
@@ -57,7 +57,7 @@ function wrapAttrValueExpression(value: AttrValue, wrap: (s: string) => string):
|
|
|
57
57
|
return AttrValueOf.spread(wrap(value.expr), value.templateExpr ? wrap(value.templateExpr) : undefined)
|
|
58
58
|
}
|
|
59
59
|
}
|
|
60
|
-
import { destructureLoopParam, loopKeyFn, buildCompSelector } from '../shared.ts'
|
|
60
|
+
import { destructureLoopParam, loopKeyFn, buildCompSelector, nestedLoopIndexAlias } from '../shared.ts'
|
|
61
61
|
import { BF_HOST, BF_AT } from '@barefootjs/shared'
|
|
62
62
|
import type {
|
|
63
63
|
BranchChildComponentInit,
|
|
@@ -269,6 +269,17 @@ export function buildBranchInnerLoopsPlan(
|
|
|
269
269
|
handler: wrapInner(ev.handler),
|
|
270
270
|
}))
|
|
271
271
|
|
|
272
|
+
// Index-param gating runs against the RAW (pre-wrap) component/event
|
|
273
|
+
// data — wrapping only rewrites loop-param references, never the
|
|
274
|
+
// index name, so it doesn't affect whether the index is referenced.
|
|
275
|
+
const indexAlias = nestedLoopIndexAlias(
|
|
276
|
+
inner,
|
|
277
|
+
`__bidxbr_${i}`,
|
|
278
|
+
paramHead,
|
|
279
|
+
inner.childComponents ?? [],
|
|
280
|
+
inner.bindings.events,
|
|
281
|
+
)
|
|
282
|
+
|
|
272
283
|
const reactiveTexts: BranchInnerLoopText[] = inner.bindings.reactiveTexts.map(text => ({
|
|
273
284
|
slotId: text.slotId,
|
|
274
285
|
wrappedExpression: wrapBoth(text.expression),
|
|
@@ -286,6 +297,7 @@ export function buildBranchInnerLoopsPlan(
|
|
|
286
297
|
keyFn: loopKeyFn(inner),
|
|
287
298
|
paramHead,
|
|
288
299
|
paramUnwrap,
|
|
300
|
+
indexAlias,
|
|
289
301
|
wrappedTemplate: inner.template!,
|
|
290
302
|
wrappedKey,
|
|
291
303
|
keyDepth: 1,
|
|
@@ -103,6 +103,7 @@ export function buildPlainLoopPlan(elem: TopLevelLoop, profileComponentName?: st
|
|
|
103
103
|
mapPreambleWrapped: elem.mapPreamble ? wrap(elem.mapPreamble) : '',
|
|
104
104
|
template: elem.template,
|
|
105
105
|
skeletonTemplate: elem.skeletonTemplate,
|
|
106
|
+
skeletonPaths: elem.skeletonPaths,
|
|
106
107
|
reactiveEffects: hasReactive ? buildLoopReactiveEffectsPlan(elem, profileComponentName) : null,
|
|
107
108
|
childRefs: buildChildRefBindings(elem.bindings.refs, elem.param, elem.paramBindings),
|
|
108
109
|
bodyIsMultiRoot: elem.bodyIsMultiRoot ?? false,
|
|
@@ -108,6 +108,15 @@ export interface BranchInnerLoop {
|
|
|
108
108
|
paramHead: string
|
|
109
109
|
/** Body-entry unwrap statement (empty when no destructured param). */
|
|
110
110
|
paramUnwrap: string
|
|
111
|
+
/**
|
|
112
|
+
* Body-entry index-alias statement (#2218), or `null` when the inner
|
|
113
|
+
* loop declares no index param or never references it. Binds the user's
|
|
114
|
+
* index name to the synthetic `__bidx<uid>` renderItem param — mirrors
|
|
115
|
+
* `InnerLoopReactiveEmit`'s prelude alias in `plan/inner-loop.ts`. Must
|
|
116
|
+
* be emitted before `paramUnwrap` and the template clone, since either
|
|
117
|
+
* may reference the index.
|
|
118
|
+
*/
|
|
119
|
+
indexAlias: string | null
|
|
111
120
|
/** Already-wrapped HTML template for one inner-loop item. */
|
|
112
121
|
wrappedTemplate: string
|
|
113
122
|
/** Pre-wrapped key expression for setAttribute, or null when no key. */
|
|
@@ -23,6 +23,7 @@ import type {
|
|
|
23
23
|
TopLevelLoop,
|
|
24
24
|
} from '../../types.ts'
|
|
25
25
|
import type { IRLoopChildComponent } from '../../../types.ts'
|
|
26
|
+
import type { SkeletonSlotPaths } from '../../html-template.ts'
|
|
26
27
|
import type { ReactiveEffectsPlan } from './reactive-effects.ts'
|
|
27
28
|
import type { InnerLoopsPlan } from './inner-loop.ts'
|
|
28
29
|
|
|
@@ -103,6 +104,14 @@ interface PlainLoopVariant extends DynamicLoopCommon {
|
|
|
103
104
|
* stringifier falls back to the legacy per-row `emitTemplateCloneInline`.
|
|
104
105
|
*/
|
|
105
106
|
skeletonTemplate?: string
|
|
107
|
+
/**
|
|
108
|
+
* Compile-time child-index paths for `skeletonTemplate`'s dynamic slots
|
|
109
|
+
* (perf, #2143). When present, the stringifier resolves attr/text/ref
|
|
110
|
+
* slots via direct `.firstChild`/`.nextSibling` property chains on a
|
|
111
|
+
* fresh clone instead of `qsa`/`$t`, falling back to the runtime lookup
|
|
112
|
+
* only for hydration (`__existing`) or any slot missing from the map.
|
|
113
|
+
*/
|
|
114
|
+
skeletonPaths?: SkeletonSlotPaths
|
|
106
115
|
/** Resolved reactive-effects plan — null forces the single-line renderItem shape. */
|
|
107
116
|
reactiveEffects: ReactiveEffectsPlan | null
|
|
108
117
|
/**
|
|
@@ -20,21 +20,118 @@ import { emitListenerBlock } from './stringify/event-listener.ts'
|
|
|
20
20
|
import { nameForRegistryRef } from '../component-scope.ts'
|
|
21
21
|
import { BF_SCOPE, BF_HOST, BF_AT } from '@barefootjs/shared'
|
|
22
22
|
import type { LoopChildRefBinding } from './plan/loop.ts'
|
|
23
|
+
import {
|
|
24
|
+
extractFreeIdentifiersFromText,
|
|
25
|
+
extractFreeIdentifiersFromStatementText,
|
|
26
|
+
extractFreeIdentifiersFromTemplateText,
|
|
27
|
+
} from '../csr-substitute.ts'
|
|
23
28
|
|
|
24
29
|
/**
|
|
25
30
|
* Build the `keyFn` argument for mapArray / reconcileElements. `null` when
|
|
26
|
-
* the loop has no key expression.
|
|
27
|
-
*
|
|
28
|
-
*
|
|
31
|
+
* the loop has no key expression. Every `CollectedLoop` variant (top-level /
|
|
32
|
+
* branch / nested) carries an `index: string | null` field (#2218 threaded
|
|
33
|
+
* it onto `NestedLoop` too), so the index param — when present — is always
|
|
34
|
+
* appended to the keyFn's own parameter list. mapArray invokes `keyFn(item,
|
|
35
|
+
* i)` directly with the numeric index, independent of the synthetic
|
|
36
|
+
* `__innerIdx<uid>` binding used inside a nested loop's renderItem body
|
|
37
|
+
* (see `nestedLoopReferencesIndex` below), so no aliasing is needed here.
|
|
29
38
|
*/
|
|
30
39
|
export function loopKeyFn(loop: CollectedLoop): string {
|
|
31
40
|
if (loop.key === null) return 'null'
|
|
32
|
-
const params = loop.
|
|
33
|
-
? loop.param
|
|
34
|
-
: `${loop.param}${loop.index ? `, ${loop.index}` : ''}`
|
|
41
|
+
const params = `${loop.param}${loop.index ? `, ${loop.index}` : ''}`
|
|
35
42
|
return `(${params}) => String(${loop.key})`
|
|
36
43
|
}
|
|
37
44
|
|
|
45
|
+
/**
|
|
46
|
+
* Does a nested inner loop's body reference its own declared index
|
|
47
|
+
* parameter (e.g. `i` from `.map((item, i) => ...)`)? Scans every surface
|
|
48
|
+
* the renderItem/forEach body can read the index from — the key
|
|
49
|
+
* expression, reactive text/attr expressions, the map-preamble statements,
|
|
50
|
+
* the per-item HTML template's `${...}` interpolations, per-item events,
|
|
51
|
+
* ref callbacks, and child-component props — so the byte-stable gate only
|
|
52
|
+
* fires when the index is truly used (mirrors #2189's handler-only gate
|
|
53
|
+
* for delegated events, `indexBindingLine` in `stringify/event-delegation.ts`).
|
|
54
|
+
*
|
|
55
|
+
* `comps` / `events` are passed separately (rather than read off `inner`)
|
|
56
|
+
* because callers hold them in different shapes at different points in the
|
|
57
|
+
* build pipeline — `DepthLevel.comps`/`.events` for the plain inner-loop
|
|
58
|
+
* path, `inner.childComponents`/`inner.bindings.events` for the loop-child-
|
|
59
|
+
* arm (conditional-branch) path.
|
|
60
|
+
*
|
|
61
|
+
* AST-based throughout (never regex) per repo convention — a token-level
|
|
62
|
+
* scan would false-match a property access like `item.i` as a reference to
|
|
63
|
+
* an index param named `i`.
|
|
64
|
+
*/
|
|
65
|
+
export function nestedLoopReferencesIndex(
|
|
66
|
+
inner: NestedLoop,
|
|
67
|
+
comps: readonly IRLoopChildComponent[],
|
|
68
|
+
events: readonly LoopChildEvent[],
|
|
69
|
+
): boolean {
|
|
70
|
+
const index = inner.index
|
|
71
|
+
if (!index) return false
|
|
72
|
+
|
|
73
|
+
const exprRefs = (text: string | null | undefined, precomputed?: ReadonlySet<string>): boolean => {
|
|
74
|
+
if (precomputed) return precomputed.has(index)
|
|
75
|
+
if (!text) return false
|
|
76
|
+
return extractFreeIdentifiersFromText(text).has(index)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (exprRefs(inner.key)) return true
|
|
80
|
+
for (const t of inner.bindings.reactiveTexts) {
|
|
81
|
+
if (exprRefs(t.expression, t.freeIdentifiers)) return true
|
|
82
|
+
}
|
|
83
|
+
for (const a of inner.bindings.reactiveAttrs) {
|
|
84
|
+
if (exprRefs(a.expression, a.freeIdentifiers)) return true
|
|
85
|
+
}
|
|
86
|
+
if (inner.mapPreamble && extractFreeIdentifiersFromStatementText(inner.mapPreamble).has(index)) return true
|
|
87
|
+
if (inner.template && extractFreeIdentifiersFromTemplateText(inner.template).has(index)) return true
|
|
88
|
+
for (const ev of events) {
|
|
89
|
+
if (exprRefs(ev.handler)) return true
|
|
90
|
+
}
|
|
91
|
+
for (const r of inner.bindings.refs) {
|
|
92
|
+
if (exprRefs(r.callback)) return true
|
|
93
|
+
}
|
|
94
|
+
for (const c of comps) {
|
|
95
|
+
for (const p of c.props) {
|
|
96
|
+
if (exprRefs(attrValueToString(p.value))) return true
|
|
97
|
+
}
|
|
98
|
+
if (c.children?.length) {
|
|
99
|
+
const childrenExpr = irChildrenToJsExpr(c.children)
|
|
100
|
+
if (childrenExpr && exprRefs(childrenExpr)) return true
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return false
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Build the prelude alias statement that binds a nested inner loop's index
|
|
108
|
+
* param to the synthetic numeric-index value mapArray/forEach passes into
|
|
109
|
+
* renderItem (#2218), or `null` when no alias is needed. Gated by
|
|
110
|
+
* `nestedLoopReferencesIndex` for byte-for-byte stability — loops that
|
|
111
|
+
* declare an index but never read it emit no extra line. `paramHead` guards
|
|
112
|
+
* against the (JS-illegal, can't actually happen) case where the index name
|
|
113
|
+
* collides with the loop's own emitted item-parameter identifier — mirrors
|
|
114
|
+
* the self-alias guard in #2189's `indexBindingLine`.
|
|
115
|
+
*
|
|
116
|
+
* `syntheticIndexVar` is the already-formatted variable name the caller's
|
|
117
|
+
* stringifier binds the numeric index to (`__innerIdx<uid>` in
|
|
118
|
+
* `stringify/inner-loop.ts`, `__bidx<uid>` in `stringify/loop-child-arm.ts`)
|
|
119
|
+
* — passed in rather than assembled here so this helper doesn't need to
|
|
120
|
+
* know which emission family is calling it.
|
|
121
|
+
*/
|
|
122
|
+
export function nestedLoopIndexAlias(
|
|
123
|
+
inner: NestedLoop,
|
|
124
|
+
syntheticIndexVar: string,
|
|
125
|
+
paramHead: string,
|
|
126
|
+
comps: readonly IRLoopChildComponent[],
|
|
127
|
+
events: readonly LoopChildEvent[],
|
|
128
|
+
): string | null {
|
|
129
|
+
const index = inner.index
|
|
130
|
+
if (!index || index === paramHead) return null
|
|
131
|
+
if (!nestedLoopReferencesIndex(inner, comps, events)) return null
|
|
132
|
+
return `const ${index} = ${syntheticIndexVar}`
|
|
133
|
+
}
|
|
134
|
+
|
|
38
135
|
/**
|
|
39
136
|
* Wrap each ref's `callback` expression with the loop-param accessor and
|
|
40
137
|
* return `LoopChildRefBinding[]` ready for the stringifier. For dynamic
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
import { keyAttrName, profileBindingId } from '../../utils.ts'
|
|
34
34
|
import { emitComponentAndEventSetup } from '../shared.ts'
|
|
35
35
|
import { emitAttrUpdate } from '../../emit-reactive.ts'
|
|
36
|
-
import { emitMultiRootTemplateCloneLines } from './template-parse.ts'
|
|
36
|
+
import { emitMultiRootTemplateCloneLines, templateRootIsSvg } from './template-parse.ts'
|
|
37
37
|
import { emitLoopChildRefs } from './loop.ts'
|
|
38
38
|
import type {
|
|
39
39
|
InnerLoopPlan,
|
|
@@ -82,7 +82,16 @@ function emitReactive(lines: string[], inner: InnerLoopPlan, indent: string, pc:
|
|
|
82
82
|
lines.push(`${innerIndent} __innerEl${uid}.__bfExtras = __innerExtras${uid}`)
|
|
83
83
|
lines.push(`${indent} }`)
|
|
84
84
|
} else {
|
|
85
|
-
|
|
85
|
+
// SVG-rooted item templates must parse inside a synthetic `<svg>` wrap
|
|
86
|
+
// (#2219): `template.innerHTML` parses in the HTML namespace, so a bare
|
|
87
|
+
// `<line>`/`<circle>` root clones as an HTMLUnknownElement and the SVG
|
|
88
|
+
// renderer silently draws nothing. Mirrors `templateRootIsSvg` handling
|
|
89
|
+
// on the top-level (#135/#1088) and branch-arm paths; HTML-rooted
|
|
90
|
+
// templates keep byte-identical output.
|
|
91
|
+
const isSvg = templateRootIsSvg(emit.wrappedTemplate)
|
|
92
|
+
const innerHtml = isSvg ? `<svg>${emit.wrappedTemplate}</svg>` : emit.wrappedTemplate
|
|
93
|
+
const childPath = isSvg ? '.firstElementChild.firstElementChild' : '.firstElementChild'
|
|
94
|
+
lines.push(`${indent} let __innerEl${uid} = __existing ?? (() => { const __t = document.createElement('template'); __t.innerHTML = \`${innerHtml}\`; return __t.content${childPath}.cloneNode(true) })()`)
|
|
86
95
|
}
|
|
87
96
|
if (emit.wrappedKey) {
|
|
88
97
|
lines.push(`${indent} __innerEl${uid}.setAttribute('${keyAttrName(inner.keyDepth)}', String(${emit.wrappedKey}))`)
|
|
@@ -86,6 +86,11 @@ export function stringifyBranchInnerLoops(
|
|
|
86
86
|
const uid = inner.uidSuffix
|
|
87
87
|
lines.push(`${indent}{ const __bic${uid} = ${inner.containerExpr}`)
|
|
88
88
|
lines.push(`${indent}if (__bic${uid}) mapArray(() => ${inner.arrayExpr} || [], __bic${uid}, ${inner.keyFn}, (${inner.paramHead}, __bidx${uid}, __existing) => {`)
|
|
89
|
+
// Index alias (#2218) lands first — before the destructure unwrap and
|
|
90
|
+
// the template clone, either of which may reference the index.
|
|
91
|
+
if (inner.indexAlias) {
|
|
92
|
+
lines.push(`${indent} ${inner.indexAlias}`)
|
|
93
|
+
}
|
|
89
94
|
if (inner.paramUnwrap) {
|
|
90
95
|
lines.push(`${indent} ${inner.paramUnwrap}`)
|
|
91
96
|
}
|
|
@@ -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
|
|