@barefootjs/jsx 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/analyzer.d.ts.map +1 -1
- package/dist/errors.d.ts +1 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/expression-parser.d.ts +14 -0
- package/dist/expression-parser.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +268 -79
- 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 +8 -0
- 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-reactive-effects.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/inner-loop.d.ts +12 -0
- package/dist/ir-to-client-js/control-flow/plan/inner-loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/inner-loop.d.ts +1 -0
- package/dist/ir-to-client-js/control-flow/stringify/inner-loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/lazy-row.d.ts.map +1 -1
- package/dist/ir-to-client-js/element-refs.d.ts.map +1 -1
- package/dist/ir-to-client-js/emit-reactive.d.ts.map +1 -1
- package/dist/ir-to-client-js/emit-registration.d.ts.map +1 -1
- package/dist/ir-to-client-js/html-template.d.ts +7 -7
- package/dist/ir-to-client-js/html-template.d.ts.map +1 -1
- package/dist/ir-to-client-js/imports.d.ts +2 -2
- package/dist/ir-to-client-js/imports.d.ts.map +1 -1
- package/dist/ir-to-client-js/index.d.ts.map +1 -1
- package/dist/ir-to-client-js/phases/provider-and-child-inits.d.ts.map +1 -1
- package/dist/ir-to-client-js/prop-handling.d.ts +33 -0
- package/dist/ir-to-client-js/prop-handling.d.ts.map +1 -1
- package/dist/ir-to-client-js/reactivity.d.ts +5 -0
- package/dist/ir-to-client-js/reactivity.d.ts.map +1 -1
- package/dist/ir-to-client-js/rewrite-props-object.d.ts +36 -8
- package/dist/ir-to-client-js/rewrite-props-object.d.ts.map +1 -1
- package/dist/ir-to-client-js/types.d.ts +21 -0
- package/dist/ir-to-client-js/types.d.ts.map +1 -1
- package/dist/types.d.ts +14 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/__snapshots__/doc-examples.test.ts.snap +23 -6
- package/src/__tests__/binding-scope-ratchet.test.ts +5 -1
- package/src/__tests__/child-components-in-map.test.ts +11 -3
- package/src/__tests__/client-js-generation.test.ts +48 -1
- package/src/__tests__/inline-jsx-callback.test.ts +55 -0
- package/src/__tests__/ir-jsx-props.test.ts +148 -0
- package/src/__tests__/issue-2705-branch-inner-loop-container.test.ts +91 -0
- package/src/__tests__/issue-2723-prop-alias-reactivity.test.ts +124 -0
- package/src/__tests__/markup-prop-brand.test.ts +49 -0
- package/src/__tests__/nested-loop-conditional.test.ts +20 -11
- package/src/__tests__/return-through-local-var.test.ts +269 -0
- package/src/__tests__/rewrite-props-object.test.ts +41 -4
- package/src/analyzer.ts +71 -0
- package/src/errors.ts +17 -1
- package/src/expression-parser.ts +26 -0
- package/src/index.ts +1 -1
- package/src/ir-to-client-js/collect-elements.ts +49 -45
- package/src/ir-to-client-js/control-flow/plan/build-inner-loop.ts +19 -0
- package/src/ir-to-client-js/control-flow/plan/build-loop-child-arm.ts +22 -3
- package/src/ir-to-client-js/control-flow/plan/build-reactive-effects.ts +5 -2
- package/src/ir-to-client-js/control-flow/plan/inner-loop.ts +12 -0
- package/src/ir-to-client-js/control-flow/stringify/inner-loop.ts +9 -0
- package/src/ir-to-client-js/control-flow/stringify/lazy-row.ts +7 -1
- package/src/ir-to-client-js/element-refs.ts +8 -0
- package/src/ir-to-client-js/emit-reactive.ts +91 -23
- package/src/ir-to-client-js/emit-registration.ts +26 -7
- package/src/ir-to-client-js/generate-init.ts +1 -1
- package/src/ir-to-client-js/html-template.ts +8 -8
- package/src/ir-to-client-js/imports.ts +5 -0
- package/src/ir-to-client-js/index.ts +10 -4
- package/src/ir-to-client-js/phases/provider-and-child-inits.ts +5 -1
- package/src/ir-to-client-js/prop-handling.ts +94 -0
- package/src/ir-to-client-js/reactivity.ts +59 -0
- package/src/ir-to-client-js/rewrite-props-object.ts +50 -10
- package/src/ir-to-client-js/types.ts +21 -0
- package/src/jsx-to-ir.ts +204 -9
- package/src/types.ts +14 -0
package/src/errors.ts
CHANGED
|
@@ -22,11 +22,24 @@ export const ErrorCodes = {
|
|
|
22
22
|
// Signal/Memo errors (BF011-BF019)
|
|
23
23
|
SIGNAL_OUTSIDE_COMPONENT: 'BF011',
|
|
24
24
|
|
|
25
|
-
// JSX errors (BF021-BF029)
|
|
25
|
+
// JSX errors (BF021-BF029). BF022 was retired (see
|
|
26
|
+
// `invalid-jsx-attribute.audit.test.ts`) and BF026 is reserved by
|
|
27
|
+
// `spec/callback-fidelity.md` for a future `.map()`-callback-shape
|
|
28
|
+
// diagnostic — BF027 is the next free slot.
|
|
26
29
|
UNSUPPORTED_JSX_PATTERN: 'BF021',
|
|
27
30
|
MISSING_KEY_IN_LIST: 'BF023',
|
|
28
31
|
MISSING_KEY_IN_NESTED_LIST: 'BF024',
|
|
29
32
|
UNSUPPORTED_DESTRUCTURE_REST: 'BF025',
|
|
33
|
+
// The component's return statement resolves to a bare identifier that
|
|
34
|
+
// refers to a local `const`/`let` whose initializer IS JSX (or a
|
|
35
|
+
// JSX-shaped ternary/`&&`/`||`/`??`), e.g. `const __root = <div/>; return
|
|
36
|
+
// __root`. JSX-child position resolves such identifiers through
|
|
37
|
+
// `jsxConstants` / `inlineableJsxConsts` (#547 / #1409), but return
|
|
38
|
+
// position deliberately does not (see `transformExpressionInner`'s
|
|
39
|
+
// docstring) — so the dispatcher's scalar-leaf fallback silently produces
|
|
40
|
+
// no IR and no diagnostic (#2720). Loud stopgap until the analyzer learns
|
|
41
|
+
// to resolve the identifier at return position too.
|
|
42
|
+
RETURN_VALUE_NOT_JSX: 'BF027',
|
|
30
43
|
|
|
31
44
|
// Component errors (BF043-BF049)
|
|
32
45
|
PROPS_DESTRUCTURING: 'BF043',
|
|
@@ -152,6 +165,9 @@ const errorMessages: Record<ErrorCode, string> = {
|
|
|
152
165
|
// stable.
|
|
153
166
|
'Computed property key in .map() callback destructure is not supported. Rewrite the callback to destructure explicit bindings (e.g., `({ a, b }) => ...`) so the compiler can rewrite references to per-item signal accessors.',
|
|
154
167
|
|
|
168
|
+
[ErrorCodes.RETURN_VALUE_NOT_JSX]:
|
|
169
|
+
"Component's return value is not recognized as JSX — return the JSX expression directly instead of binding it to a local variable first.",
|
|
170
|
+
|
|
155
171
|
[ErrorCodes.PROPS_DESTRUCTURING]:
|
|
156
172
|
'Props destructuring in function parameters breaks reactivity. Use props object directly.',
|
|
157
173
|
[ErrorCodes.SIGNAL_GETTER_NOT_CALLED]:
|
package/src/expression-parser.ts
CHANGED
|
@@ -317,6 +317,32 @@ export type SortComparator = {
|
|
|
317
317
|
keys: SortKey[]
|
|
318
318
|
}
|
|
319
319
|
|
|
320
|
+
/**
|
|
321
|
+
* Runtime registries of the sort-comparator catalogue's finite dimensions —
|
|
322
|
+
* the denominators for the coverage ledger's sort floor
|
|
323
|
+
* (`packages/adapter-tests/src/__tests__/coverage-map.test.ts`). This closes
|
|
324
|
+
* the comparator half of the change-time coupling rule
|
|
325
|
+
* (`spec/subset-conformance.md`) mechanically: the exhaustiveness pins below
|
|
326
|
+
* make widening {@link SortKey} without listing the new member here a compile
|
|
327
|
+
* error, and the floor test then makes shipping a listed member with no
|
|
328
|
+
* covering fixture a test failure — same drift defence `PARSED_EXPR_KINDS`
|
|
329
|
+
* and `ARRAY_METHOD_NAMES` provide for their halves.
|
|
330
|
+
*/
|
|
331
|
+
export const SORT_KEY_TYPES = ['numeric', 'string', 'auto'] as const satisfies ReadonlyArray<SortKey['type']>
|
|
332
|
+
type MissingFromSortKeyTypes = Exclude<SortKey['type'], (typeof SORT_KEY_TYPES)[number]>
|
|
333
|
+
const _sortKeyTypeRegistryIsExhaustive: MissingFromSortKeyTypes extends never ? true : never = true
|
|
334
|
+
void _sortKeyTypeRegistryIsExhaustive
|
|
335
|
+
|
|
336
|
+
export const SORT_KEY_TARGETS = ['self', 'field'] as const satisfies ReadonlyArray<SortKey['key']['kind']>
|
|
337
|
+
type MissingFromSortKeyTargets = Exclude<SortKey['key']['kind'], (typeof SORT_KEY_TARGETS)[number]>
|
|
338
|
+
const _sortKeyTargetRegistryIsExhaustive: MissingFromSortKeyTargets extends never ? true : never = true
|
|
339
|
+
void _sortKeyTargetRegistryIsExhaustive
|
|
340
|
+
|
|
341
|
+
export const SORT_KEY_DIRECTIONS = ['asc', 'desc'] as const satisfies ReadonlyArray<SortKey['direction']>
|
|
342
|
+
type MissingFromSortKeyDirections = Exclude<SortKey['direction'], (typeof SORT_KEY_DIRECTIONS)[number]>
|
|
343
|
+
const _sortKeyDirectionRegistryIsExhaustive: MissingFromSortKeyDirections extends never ? true : never = true
|
|
344
|
+
void _sortKeyDirectionRegistryIsExhaustive
|
|
345
|
+
|
|
320
346
|
/**
|
|
321
347
|
* Flatten depth for `.flat(depth?)` (#1448 Tier C). A finite non-negative
|
|
322
348
|
* integer flattens that many levels (`.flat()` defaults to `1`; a `0` or
|
package/src/index.ts
CHANGED
|
@@ -213,7 +213,7 @@ export { isValueReferenceIdentifier, collectValueReferencedNames } from './value
|
|
|
213
213
|
// Expression Parser
|
|
214
214
|
export { parseExpression, tsNodeToParsedExpr, asCallbackMethodCall, CALLBACK_METHODS, sortComparatorFromArrow, serializeParsedExpr, freeVarsInBody, freeIdentifiers, materializeGetterCalls, isSupported, isSupportedValue, exprToString, stringifyParsedExpr, identifierPath, parseBlockBody, parseBlockBodyTolerant, foldBlockToExpr, predicateTernaryToLogical, containsHigherOrder, extractArrowBodyExpression, parseStyleObjectEntries, hasUnsafeStyleValue, parseProviderObjectLiteral, type ProviderObjectMember, type FoldBlockOptions } from './expression-parser.ts'
|
|
215
215
|
export type { StyleObjectEntry } from './expression-parser.ts'
|
|
216
|
-
export { PARSED_EXPR_KINDS, ARRAY_METHOD_NAMES } from './expression-parser.ts'
|
|
216
|
+
export { PARSED_EXPR_KINDS, ARRAY_METHOD_NAMES, SORT_KEY_TYPES, SORT_KEY_TARGETS, SORT_KEY_DIRECTIONS } from './expression-parser.ts'
|
|
217
217
|
export type { ParsedExpr, ObjectLiteralProperty, ParsedStatement, SortComparator, SortKey, FlatDepth, SupportLevel, SupportResult, TemplatePart } from './expression-parser.ts'
|
|
218
218
|
export { buildLoopChainExpr } from './loop-chain.ts'
|
|
219
219
|
export type { LoopChainInputs } from './loop-chain.ts'
|
|
@@ -8,7 +8,7 @@ import { attrValueToString, freeIdsFromRefs, quotePropName, PROPS_PARAM } from '
|
|
|
8
8
|
import { classifyReactivity, decideWrapForAttr, decideWrapForChildProp, decideWrapFromAstFlags, collectEventHandlersFromIR, collectConditionalBranchEvents, collectConditionalBranchRefs, collectConditionalBranchChildComponents, collectLoopChildEventsWithNesting, collectLoopChildReactiveAttrs, collectLoopChildReactiveTexts, collectLoopChildRefs, emptyLoopChildBindings, buildLoopRowScope, anyNameIn } from './reactivity.ts'
|
|
9
9
|
import { irToHtmlTemplate, irToPlaceholderTemplate, irChildrenToJsExpr, buildLoopSkeletonTemplate, computeSkeletonSlotPaths, renderFlatMapClientBody, renderFlatMapProjectionClientBody, flatMapCallbackHasKeyedLeaf, type SkeletonSlotPaths } from './html-template.ts'
|
|
10
10
|
import { detectRootNamespaceWrapTag } from './control-flow/stringify/template-parse.ts'
|
|
11
|
-
import { expandDynamicPropValue, expandConstantForReactivity } from './prop-handling.ts'
|
|
11
|
+
import { expandDynamicPropValue, expandConstantForReactivity, resolveRestSpreadOrigin, resolveRestSpreadNames } from './prop-handling.ts'
|
|
12
12
|
import { extractFreeIdentifiersFromText } from './csr-substitute.ts'
|
|
13
13
|
import { walkIR, stopAt } from './walker.ts'
|
|
14
14
|
import { buildLoopChainExpr } from '../loop-chain.ts'
|
|
@@ -325,24 +325,47 @@ export function collectInnerLoops(
|
|
|
325
325
|
// param) silently dropped its text-child update effect while the
|
|
326
326
|
// sibling attribute effect (ungated) still fired. Refs need to fire
|
|
327
327
|
// on every renderItem invocation (#1244).
|
|
328
|
-
// - events
|
|
329
|
-
//
|
|
330
|
-
//
|
|
331
|
-
//
|
|
328
|
+
// - events: only in `collectBindings` (branch) mode; the
|
|
329
|
+
// legacy non-branch path didn't wire them on `NestedLoop`
|
|
330
|
+
// because event delegation handles them through the parent's
|
|
331
|
+
// bindings instead.
|
|
332
|
+
// - conditionals: collected for EVERY inner loop, branch or not
|
|
333
|
+
// (#2706) — see the `stopAtReactiveConditionals: true` note
|
|
334
|
+
// below for why this must not be gated the same way events are.
|
|
332
335
|
const bindings: LoopChildBindings = emptyLoopChildBindings()
|
|
333
336
|
// Hoisted: one Set per loop, not one per child (Copilot review).
|
|
334
337
|
const innerPreambleNames = preambleNamesOf(n)
|
|
335
338
|
if (ctx) {
|
|
336
339
|
for (const child of n.children) {
|
|
337
|
-
|
|
338
|
-
|
|
340
|
+
// `stopAtReactiveConditionals: true` (#2347's parameter, #2706's
|
|
341
|
+
// fix here) — a per-item conditional inside THIS loop's own row
|
|
342
|
+
// now always gets its own `bindings.conditionals` entry (below)
|
|
343
|
+
// and its own `insert()`, regardless of branch/general mode.
|
|
344
|
+
// Descending past it here too (the pre-#2706 default) would
|
|
345
|
+
// double-bind: once via insert()'s bindEvents, once via this
|
|
346
|
+
// flat `insideConditional`-flagged reclaim-on-every-run effect
|
|
347
|
+
// — the latter is also unsound on its own, since nothing
|
|
348
|
+
// guarantees the branch's marker is mounted the moment this
|
|
349
|
+
// effect first runs (issue-2706-nested-loop-conditional-slot
|
|
350
|
+
// .test.ts's original repro).
|
|
351
|
+
bindings.reactiveTexts.push(...collectLoopChildReactiveTexts(child, ctx, n.param, n.paramBindings, true, innerPreambleNames, n.index))
|
|
352
|
+
bindings.reactiveAttrs.push(...collectLoopChildReactiveAttrs(child, ctx, n.param, n.paramBindings, true, innerPreambleNames, n.index))
|
|
339
353
|
bindings.refs.push(...collectLoopChildRefs(child))
|
|
340
354
|
}
|
|
355
|
+
bindings.conditionals.push(...collectLoopChildConditionals(
|
|
356
|
+
{ type: 'fragment', children: n.children, loc: n.loc } as unknown as IRNode,
|
|
357
|
+
ctx,
|
|
358
|
+
siblingOffsets,
|
|
359
|
+
n.param,
|
|
360
|
+
n.paramBindings,
|
|
361
|
+
innerPreambleNames,
|
|
362
|
+
n.index,
|
|
363
|
+
))
|
|
341
364
|
}
|
|
342
365
|
|
|
343
366
|
// Per-item bindings for branch-mode callers (child components,
|
|
344
|
-
// events
|
|
345
|
-
//
|
|
367
|
+
// events) — matches the pre-Phase 2 `collectBranchInnerLoops`
|
|
368
|
+
// behaviour. Conditionals are collected above, uniformly.
|
|
346
369
|
let childComponents: import('../types.ts').IRLoopChildComponent[] | undefined
|
|
347
370
|
if (collectBindings) {
|
|
348
371
|
// skipConditionals=true: components inside conditional branches
|
|
@@ -369,18 +392,6 @@ export function collectInnerLoops(
|
|
|
369
392
|
for (const child of n.children) {
|
|
370
393
|
bindings.events.push(...collectLoopChildEventsWithNesting(child))
|
|
371
394
|
}
|
|
372
|
-
|
|
373
|
-
if (ctx) {
|
|
374
|
-
bindings.conditionals.push(...collectLoopChildConditionals(
|
|
375
|
-
{ type: 'fragment', children: n.children, loc: n.loc } as unknown as IRNode,
|
|
376
|
-
ctx,
|
|
377
|
-
siblingOffsets,
|
|
378
|
-
n.param,
|
|
379
|
-
n.paramBindings,
|
|
380
|
-
innerPreambleNames,
|
|
381
|
-
n.index,
|
|
382
|
-
))
|
|
383
|
-
}
|
|
384
395
|
}
|
|
385
396
|
|
|
386
397
|
result.push({
|
|
@@ -486,23 +497,15 @@ function isSingleElementJsxChildren(nodes: IRNode[]): boolean {
|
|
|
486
497
|
return nodes.length === 1 && nodes[0].type === 'element'
|
|
487
498
|
}
|
|
488
499
|
|
|
489
|
-
/** Build rest spread names from context (rest/props spreads handled by applyRestAttrs, not spreadAttrs). */
|
|
490
|
-
function buildRestSpreadNames(ctx: ClientJsContext): Set<string> {
|
|
491
|
-
const names = new Set<string>()
|
|
492
|
-
if (ctx.restPropsName) names.add(ctx.restPropsName)
|
|
493
|
-
if (ctx.propsObjectName) names.add(ctx.propsObjectName)
|
|
494
|
-
return names
|
|
495
|
-
}
|
|
496
|
-
|
|
497
500
|
/** Build propsExpr for a child component from its IR props. */
|
|
498
501
|
function buildComponentPropsExpr(props: IRProp[], ctx: ClientJsContext): string {
|
|
499
|
-
const restName = ctx.restPropsName
|
|
500
|
-
const propsObjName = ctx.propsObjectName
|
|
501
502
|
const knownSpreadProp = props.find(p => {
|
|
502
503
|
if (p.name !== '...' && !p.name.startsWith('...')) return false
|
|
503
504
|
if (p.value.kind !== 'spread' && p.value.kind !== 'expression') return false
|
|
504
505
|
const expr = p.value.kind === 'spread' ? p.value.expr : p.value.expr
|
|
505
|
-
|
|
506
|
+
// #2723: resolve through any `const x__alias = x` hop onto the
|
|
507
|
+
// rest/props binding — see `resolveRestSpreadOrigin`'s docstring.
|
|
508
|
+
return resolveRestSpreadOrigin(ctx, expr) !== null
|
|
506
509
|
})
|
|
507
510
|
const spreadSource = knownSpreadProp ? PROPS_PARAM : null
|
|
508
511
|
|
|
@@ -751,7 +754,7 @@ export function collectElements(
|
|
|
751
754
|
// drops the parent's slot suffix automatically. Each iteration
|
|
752
755
|
// owns a distinct scope identified by `data-key`, mirroring the
|
|
753
756
|
// SSR template's renderChild emit.
|
|
754
|
-
staticItemTemplate = irToHtmlTemplate(l.children[0],
|
|
757
|
+
staticItemTemplate = irToHtmlTemplate(l.children[0], resolveRestSpreadNames(ctx), 0, undefined, undefined)
|
|
755
758
|
}
|
|
756
759
|
} else if (l.children[0] && !projectionInner) {
|
|
757
760
|
// Pass loopParams so expressions are wrapped at generation time,
|
|
@@ -760,8 +763,8 @@ export function collectElements(
|
|
|
760
763
|
// in the emitted template literal are rewritten to `__bfItem()[1].color`.
|
|
761
764
|
const loopParamSpec = [{ param: l.param, bindings: l.paramBindings }]
|
|
762
765
|
template = useElementReconciliation
|
|
763
|
-
? irToPlaceholderTemplate(l.children[0],
|
|
764
|
-
: irToHtmlTemplate(l.children[0],
|
|
766
|
+
? irToPlaceholderTemplate(l.children[0], resolveRestSpreadNames(ctx), 0, loopParamSpec)
|
|
767
|
+
: irToHtmlTemplate(l.children[0], resolveRestSpreadNames(ctx), 0, loopParamSpec)
|
|
765
768
|
// Static-array loops emit a `forEach((param, idx) => ...)` whose body
|
|
766
769
|
// references the destructured param directly — `__bfItem()` is not in
|
|
767
770
|
// scope there. Build a second template that skips the loop-param
|
|
@@ -777,8 +780,8 @@ export function collectElements(
|
|
|
777
780
|
// markers, so SSR's parent-anchored shape and CSR's random-id
|
|
778
781
|
// shape both resolve through the same lookup.
|
|
779
782
|
staticItemTemplate = useElementReconciliation
|
|
780
|
-
? irToPlaceholderTemplate(l.children[0],
|
|
781
|
-
: irToHtmlTemplate(l.children[0],
|
|
783
|
+
? irToPlaceholderTemplate(l.children[0], resolveRestSpreadNames(ctx), 0)
|
|
784
|
+
: irToHtmlTemplate(l.children[0], resolveRestSpreadNames(ctx), 0)
|
|
782
785
|
} else if (!useElementReconciliation && !l.bodyIsMultiRoot && !l.bodyIsItemConditional) {
|
|
783
786
|
// Hoisted shared-template fast path (perf): only for the plain
|
|
784
787
|
// `mapArray` shape — single-root, dynamic array, no element
|
|
@@ -844,13 +847,13 @@ export function collectElements(
|
|
|
844
847
|
flatMapClient: projectionInner
|
|
845
848
|
? {
|
|
846
849
|
params: l.index ? `(${l.param}, ${l.index})` : `(${l.param})`,
|
|
847
|
-
body: renderFlatMapProjectionClientBody(projectionInner,
|
|
850
|
+
body: renderFlatMapProjectionClientBody(projectionInner, resolveRestSpreadNames(ctx)),
|
|
848
851
|
keyed: projectionInner.key !== null,
|
|
849
852
|
}
|
|
850
853
|
: l.flatMapCallback
|
|
851
854
|
? {
|
|
852
855
|
params: l.flatMapCallback.params,
|
|
853
|
-
body: renderFlatMapClientBody(l.flatMapCallback,
|
|
856
|
+
body: renderFlatMapClientBody(l.flatMapCallback, resolveRestSpreadNames(ctx)),
|
|
854
857
|
keyed: flatMapCallbackHasKeyedLeaf(l.flatMapCallback),
|
|
855
858
|
}
|
|
856
859
|
: undefined,
|
|
@@ -953,9 +956,10 @@ function collectFromElement(element: IRElement, ctx: ClientJsContext, insideCond
|
|
|
953
956
|
// Always use PROPS_PARAM as the source since the init function parameter is PROPS_PARAM.
|
|
954
957
|
if (attr.name === '...' && attr.value) {
|
|
955
958
|
const spreadVal = attrValueToString(attr.value) ?? ''
|
|
956
|
-
const
|
|
957
|
-
|
|
958
|
-
|
|
959
|
+
// #2723: resolve through any `const x__alias = x` hop onto the
|
|
960
|
+
// rest/props binding — see `resolveRestSpreadOrigin`'s docstring.
|
|
961
|
+
const spreadOrigin = spreadVal ? resolveRestSpreadOrigin(ctx, spreadVal) : null
|
|
962
|
+
if (spreadOrigin !== null) {
|
|
959
963
|
// `applyRestAttrs(_el, _p, exclude)` is handed the FULL props
|
|
960
964
|
// object (`PROPS_PARAM`), not a computed JS rest binding, and the
|
|
961
965
|
// runtime filters by SOURCE KEY (`source[key]`). So `exclude` must
|
|
@@ -974,7 +978,7 @@ function collectFromElement(element: IRElement, ctx: ClientJsContext, insideCond
|
|
|
974
978
|
// caller-keyed (#2524 CSR half) — so the exclude list must use
|
|
975
979
|
// the caller-facing key too, not the local binding name.
|
|
976
980
|
const consumedKeys =
|
|
977
|
-
|
|
981
|
+
spreadOrigin === 'rest' ? ctx.propsParams.map(p => p.sourceName ?? p.name) : []
|
|
978
982
|
const staticAttrKeys = element.attrs
|
|
979
983
|
.filter(a => a.name !== '...')
|
|
980
984
|
.map(a => a.name)
|
|
@@ -1124,7 +1128,7 @@ function collectBranchLoops(
|
|
|
1124
1128
|
siblingOffsets: Map<IRLoop, IRNode[]>,
|
|
1125
1129
|
): BranchLoop[] {
|
|
1126
1130
|
const loops: BranchLoop[] = []
|
|
1127
|
-
const restNames = ctx ?
|
|
1131
|
+
const restNames = ctx ? resolveRestSpreadNames(ctx) : undefined
|
|
1128
1132
|
|
|
1129
1133
|
walkIR<string | null>(node, null, {
|
|
1130
1134
|
// Don't recurse into nested conditionals / if-statements.
|
|
@@ -1247,7 +1251,7 @@ function buildConditionalMetadata(
|
|
|
1247
1251
|
ctx: ClientJsContext,
|
|
1248
1252
|
siblingOffsets: Map<IRLoop, IRNode[]>,
|
|
1249
1253
|
): ConditionalElement {
|
|
1250
|
-
const restNames =
|
|
1254
|
+
const restNames = resolveRestSpreadNames(ctx)
|
|
1251
1255
|
// Use loopDepth=-1 so the first loop encountered inside the branch emits
|
|
1252
1256
|
// data-key (depth 0) for its items, matching the mapArray item template
|
|
1253
1257
|
// and event dispatcher convention. Matches irToComponentTemplate/generateCsrTemplate.
|
|
@@ -30,6 +30,8 @@ import {
|
|
|
30
30
|
} from '../../utils.ts'
|
|
31
31
|
import { buildChildRefBindings, buildStaticChildRefBindings } from '../shared.ts'
|
|
32
32
|
import { renderPreamble, irToHtmlTemplate } from '../../html-template.ts'
|
|
33
|
+
import { buildLoopChildConditionalsPlan } from './build-loop-child-arm.ts'
|
|
34
|
+
import type { LoopChildConditionalPlan } from './loop-child-arm.ts'
|
|
33
35
|
|
|
34
36
|
/**
|
|
35
37
|
* Mirror of the helper in `build-loop-child-arm.ts` — kept local to avoid
|
|
@@ -160,6 +162,7 @@ function buildReactiveEmit(
|
|
|
160
162
|
outerLoopParamBindings?: readonly LoopParamBinding[],
|
|
161
163
|
): InnerLoopReactiveEmit {
|
|
162
164
|
const wrapInner = (expr: string) => wrapLoopParamAsAccessor(expr, inner.param, inner.paramBindings)
|
|
165
|
+
const wrapBoth = (expr: string) => wrapLoopParamAsAccessor(wrapOuter(expr), inner.param, inner.paramBindings)
|
|
163
166
|
const { head: paramHead, unwrap: paramUnwrap } = destructureLoopParam(inner.param, inner.paramBindings)
|
|
164
167
|
const wrappedKey = inner.key
|
|
165
168
|
? wrapLoopParamAsAccessor(inner.key, inner.param, inner.paramBindings)
|
|
@@ -249,6 +252,21 @@ function buildReactiveEmit(
|
|
|
249
252
|
|
|
250
253
|
const childRefs = buildChildRefBindings(inner.bindings.refs, inner.param, inner.paramBindings)
|
|
251
254
|
|
|
255
|
+
// Per-item conditionals inside THIS loop's own row (#2706) — same
|
|
256
|
+
// insert()-parity treatment the top-level loop's row conditionals
|
|
257
|
+
// already get (`buildLoopReactiveEffectsPlan`), now extended to a
|
|
258
|
+
// NESTED loop's row too. `scopeVar` is the row's own element
|
|
259
|
+
// (`__innerEl<uidSuffix>`, matching `stringifyInnerLoops`'s emission),
|
|
260
|
+
// and `wrapBoth` matches every other per-item expression in this emit
|
|
261
|
+
// (outer accessor, then inner accessor).
|
|
262
|
+
const conditionals: LoopChildConditionalPlan[] = buildLoopChildConditionalsPlan({
|
|
263
|
+
conditionals: inner.bindings.conditionals,
|
|
264
|
+
scopeVar: `__innerEl${uidSuffix}`,
|
|
265
|
+
wrap: wrapBoth,
|
|
266
|
+
loopParam: inner.param,
|
|
267
|
+
loopParamBindings: inner.paramBindings,
|
|
268
|
+
})
|
|
269
|
+
|
|
252
270
|
return {
|
|
253
271
|
mode: 'reactive',
|
|
254
272
|
keyFn: loopKeyFn(inner),
|
|
@@ -261,6 +279,7 @@ function buildReactiveEmit(
|
|
|
261
279
|
events,
|
|
262
280
|
reactiveTexts,
|
|
263
281
|
reactiveAttrs,
|
|
282
|
+
conditionals,
|
|
264
283
|
childRefs,
|
|
265
284
|
}
|
|
266
285
|
}
|
|
@@ -190,6 +190,14 @@ export interface BuildBranchInnerLoopsArgs {
|
|
|
190
190
|
innerLoops: readonly NestedLoop[] | undefined
|
|
191
191
|
/** The variable expression naming the parent scope element (e.g. `__branchScope`). */
|
|
192
192
|
scopeVar: string
|
|
193
|
+
/**
|
|
194
|
+
* The enclosing conditional's own slot id — every call site of this
|
|
195
|
+
* builder originates from a conditional branch's arm, so this is always
|
|
196
|
+
* a real id. Used as the `containerExpr` fallback (`findCondContainer`,
|
|
197
|
+
* #2705) for an inner loop whose IR never got a `containerSlotId` of its
|
|
198
|
+
* own (its wrapper element sits outside the branch's IR subtree).
|
|
199
|
+
*/
|
|
200
|
+
condSlotId: string
|
|
193
201
|
/** Outer loop param identifier (the conditional's enclosing loop). */
|
|
194
202
|
outerLoopParam: string
|
|
195
203
|
/** Outer loop param destructuring metadata. */
|
|
@@ -214,6 +222,7 @@ export function buildBranchInnerLoopsPlan(
|
|
|
214
222
|
const {
|
|
215
223
|
innerLoops,
|
|
216
224
|
scopeVar,
|
|
225
|
+
condSlotId,
|
|
217
226
|
outerLoopParam,
|
|
218
227
|
outerLoopParamBindings,
|
|
219
228
|
wrapOuter,
|
|
@@ -230,10 +239,15 @@ export function buildBranchInnerLoopsPlan(
|
|
|
230
239
|
|
|
231
240
|
const csl = inner.containerSlotId
|
|
232
241
|
// Inner loop's container: host-side `bf="<slot>"` slot marker first,
|
|
233
|
-
// then (bf-h, bf-m) when the container is itself a child scope.
|
|
242
|
+
// then (bf-h, bf-m) when the container is itself a child scope. When
|
|
243
|
+
// neither exists — the loop's own IR never got a `containerSlotId`
|
|
244
|
+
// because its wrapper element sits outside the branch's IR subtree
|
|
245
|
+
// (#2705) — resolve via the conditional's OWN comment marker instead
|
|
246
|
+
// of falling back to the whole branch scope, which may be several
|
|
247
|
+
// elements wider than the loop's actual container.
|
|
234
248
|
const containerExpr = csl
|
|
235
249
|
? `(${scopeVar}.querySelector('[bf="${csl}"]') ?? ${scopeVar}.querySelector(\`[${BF_HOST}="\${__scopeId}"][${BF_AT}="${csl}"]\`) ?? ${scopeVar})`
|
|
236
|
-
: scopeVar
|
|
250
|
+
: `findCondContainer(${scopeVar}, '${condSlotId}')`
|
|
237
251
|
|
|
238
252
|
const { head: paramHead, unwrap: paramUnwrap } = destructureLoopParam(inner.param, inner.paramBindings)
|
|
239
253
|
const wrappedKey = inner.key
|
|
@@ -372,12 +386,14 @@ export function buildLoopChildConditionalsPlan(
|
|
|
372
386
|
wrap,
|
|
373
387
|
loopParam,
|
|
374
388
|
loopParamBindings,
|
|
389
|
+
condId: cond.slotId,
|
|
375
390
|
}),
|
|
376
391
|
whenFalseArm: buildLoopChildArmPlan({
|
|
377
392
|
branch: cond.whenFalse,
|
|
378
393
|
wrap,
|
|
379
394
|
loopParam,
|
|
380
395
|
loopParamBindings,
|
|
396
|
+
condId: cond.slotId,
|
|
381
397
|
}),
|
|
382
398
|
})
|
|
383
399
|
}
|
|
@@ -439,10 +455,12 @@ interface BuildLoopChildArmArgs {
|
|
|
439
455
|
wrap: (expr: string) => string
|
|
440
456
|
loopParam: string
|
|
441
457
|
loopParamBindings?: readonly LoopParamBinding[]
|
|
458
|
+
/** The enclosing conditional's own slot id — threaded to `buildBranchInnerLoopsPlan`'s `condSlotId` (#2705). */
|
|
459
|
+
condId: string
|
|
442
460
|
}
|
|
443
461
|
|
|
444
462
|
function buildLoopChildArmPlan(args: BuildLoopChildArmArgs): LoopChildArmPlan {
|
|
445
|
-
const { branch, wrap, loopParam, loopParamBindings } = args
|
|
463
|
+
const { branch, wrap, loopParam, loopParamBindings, condId } = args
|
|
446
464
|
return {
|
|
447
465
|
events: buildBranchEventBindingsPlan({
|
|
448
466
|
events: branch.events,
|
|
@@ -455,6 +473,7 @@ function buildLoopChildArmPlan(args: BuildLoopChildArmArgs): LoopChildArmPlan {
|
|
|
455
473
|
innerLoops: buildBranchInnerLoopsPlan({
|
|
456
474
|
innerLoops: branch.innerLoops,
|
|
457
475
|
scopeVar: '__branchScope',
|
|
476
|
+
condSlotId: condId,
|
|
458
477
|
outerLoopParam: loopParam,
|
|
459
478
|
outerLoopParamBindings: loopParamBindings,
|
|
460
479
|
wrapOuter: wrap,
|
|
@@ -110,8 +110,8 @@ export function buildReactiveEffectsPlan(
|
|
|
110
110
|
wrappedCondition: wrap(cond.condition),
|
|
111
111
|
whenTrueTemplateHtml: addCondAttrToTemplate(wrap(cond.whenTrueHtml), cond.slotId),
|
|
112
112
|
whenFalseTemplateHtml: addCondAttrToTemplate(wrap(cond.whenFalseHtml), cond.slotId),
|
|
113
|
-
whenTrueArm: buildOuterArm(cond.whenTrue, wrap, loopParam, loopParamBindings, profileComponentName),
|
|
114
|
-
whenFalseArm: buildOuterArm(cond.whenFalse, wrap, loopParam, loopParamBindings, profileComponentName),
|
|
113
|
+
whenTrueArm: buildOuterArm(cond.whenTrue, wrap, loopParam, loopParamBindings, cond.slotId, profileComponentName),
|
|
114
|
+
whenFalseArm: buildOuterArm(cond.whenFalse, wrap, loopParam, loopParamBindings, cond.slotId, profileComponentName),
|
|
115
115
|
...(cond.readsPreamble && { readsPreamble: true }),
|
|
116
116
|
})
|
|
117
117
|
}
|
|
@@ -130,6 +130,8 @@ function buildOuterArm(
|
|
|
130
130
|
wrap: (expr: string) => string,
|
|
131
131
|
loopParam: string,
|
|
132
132
|
loopParamBindings: readonly LoopParamBinding[] | undefined,
|
|
133
|
+
/** The conditional's own slot id — threaded to `buildBranchInnerLoopsPlan`'s `condSlotId` (#2705). */
|
|
134
|
+
condSlotId: string,
|
|
133
135
|
profileComponentName?: string,
|
|
134
136
|
): LoopChildArmPlan {
|
|
135
137
|
return {
|
|
@@ -145,6 +147,7 @@ function buildOuterArm(
|
|
|
145
147
|
innerLoops: buildBranchInnerLoopsPlan({
|
|
146
148
|
innerLoops: branch.innerLoops,
|
|
147
149
|
scopeVar: '__branchScope',
|
|
150
|
+
condSlotId,
|
|
148
151
|
outerLoopParam: loopParam,
|
|
149
152
|
outerLoopParamBindings: loopParamBindings,
|
|
150
153
|
wrapOuter: wrap,
|
|
@@ -17,6 +17,7 @@ import type {
|
|
|
17
17
|
LoopParamBinding,
|
|
18
18
|
} from '../../../types.ts'
|
|
19
19
|
import type { LoopChildRefBinding } from './loop.ts'
|
|
20
|
+
import type { LoopChildConditionalPlan } from './loop-child-arm.ts'
|
|
20
21
|
|
|
21
22
|
/**
|
|
22
23
|
* Body-entry statements emitted in order at the top of a `mapArray`
|
|
@@ -148,6 +149,17 @@ export interface InnerLoopReactiveEmit {
|
|
|
148
149
|
reactiveTexts: readonly InnerLoopText[]
|
|
149
150
|
/** Pre-wrapped reactive attribute effects for the inner-item body. */
|
|
150
151
|
reactiveAttrs: readonly InnerLoopReactiveAttr[]
|
|
152
|
+
/**
|
|
153
|
+
* Per-item conditionals inside THIS loop's own row (#2706) — each gets
|
|
154
|
+
* its own `insert()` call, the same insert()-parity the top-level loop's
|
|
155
|
+
* row conditionals already have (`ReactiveEffectsPlan.conditionals`).
|
|
156
|
+
* Before this field existed, a per-item conditional inside a nested
|
|
157
|
+
* loop's row was baked into the static row template ONCE at row
|
|
158
|
+
* creation and never revisited — silently frozen against later signal
|
|
159
|
+
* changes — while its reactive text still (unsoundly) assumed insert()
|
|
160
|
+
* kept the branch's marker around.
|
|
161
|
+
*/
|
|
162
|
+
conditionals: readonly LoopChildConditionalPlan[]
|
|
151
163
|
/** Pre-wrapped imperative ref callbacks for the inner-item body (#1244). */
|
|
152
164
|
childRefs: readonly LoopChildRefBinding[]
|
|
153
165
|
}
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
* <indent> emitComponentAndEventSetup(...)
|
|
15
15
|
* <indent> recurse on childLevels
|
|
16
16
|
* <indent> reactive text effects
|
|
17
|
+
* <indent> per-item conditionals: insert() over __innerEl<uid> (#2706)
|
|
17
18
|
* <indent> return __innerEl<uid>
|
|
18
19
|
* <indent>}) }
|
|
19
20
|
*
|
|
@@ -36,6 +37,7 @@ import { emitAttrUpdate } from '../../emit-reactive.ts'
|
|
|
36
37
|
import { emitMultiRootTemplateCloneLines, namespaceWrapForTemplate } from './template-parse.ts'
|
|
37
38
|
import { emitLoopChildRefs } from './loop.ts'
|
|
38
39
|
import { claimPlanLiteral, claimWriterVarName, type ClaimSlotSpec } from './claim-plan.ts'
|
|
40
|
+
import { stringifyLoopChildConditionals } from './loop-child-arm.ts'
|
|
39
41
|
import type {
|
|
40
42
|
InnerLoopPlan,
|
|
41
43
|
InnerLoopsPlan,
|
|
@@ -139,6 +141,13 @@ function emitReactive(lines: string[], inner: InnerLoopPlan, indent: string, pc:
|
|
|
139
141
|
}
|
|
140
142
|
lines.push(`${indent} }${profileBindingId(pc, attr.slotId)}) }`)
|
|
141
143
|
}
|
|
144
|
+
// Per-item conditionals inside THIS loop's own row (#2706) — each is a
|
|
145
|
+
// real `insert()` over `__innerEl<uid>`, not a bake-once-at-creation
|
|
146
|
+
// ternary. Mirrors `stringifyBranchInnerLoops`'s identical call for a
|
|
147
|
+
// branch-scoped inner loop's own conditionals.
|
|
148
|
+
if (emit.conditionals.length > 0) {
|
|
149
|
+
stringifyLoopChildConditionals(lines, emit.conditionals, `${indent} `, pc)
|
|
150
|
+
}
|
|
142
151
|
// Imperative ref callbacks fire on every renderItem invocation, which
|
|
143
152
|
// means every mount: SSR hydration, initial CSR creation, and same-key
|
|
144
153
|
// remount after unmount (#1244).
|
|
@@ -553,7 +553,13 @@ function seedDiffersExpr(target: string, a: LazyRowAttrBinding): string {
|
|
|
553
553
|
if (a.attrName === 'dangerouslySetInnerHTML' || html === 'dangerouslySetInnerHTML') return 'true'
|
|
554
554
|
if (html === 'style') return `${target}.getAttribute('style') !== styleToCss(__x)`
|
|
555
555
|
if (html === 'class') return `${target}.getAttribute('class') !== (__x != null ? String(__x) : null)`
|
|
556
|
-
|
|
556
|
+
// Mirrors `emitValueUpdateStatements`'s runtime `'value' in target` gate
|
|
557
|
+
// (#2716): a target with no native `.value` property never gets one
|
|
558
|
+
// written, so the seed-diff must compare against the ATTRIBUTE it would
|
|
559
|
+
// actually receive there, not the (absent) property.
|
|
560
|
+
if (html === 'value') {
|
|
561
|
+
return `('value' in ${target} ? ${target}.value !== String(__x) : ${target}.getAttribute('value') !== String(__x))`
|
|
562
|
+
}
|
|
557
563
|
if (isBooleanAttr(html)) return `${target}.${html} !== !!(__x)`
|
|
558
564
|
if (a.meta.presenceOrUndefined) {
|
|
559
565
|
// Compare the VALUE the writer would produce, not just presence.
|
|
@@ -71,6 +71,14 @@ export function generateElementRefs(ctx: ClientJsContext): string {
|
|
|
71
71
|
regularSlots.delete(slotId)
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
+
// The component's own `comment: true` root slot needs no `$c` ref at
|
|
75
|
+
// all — its consumers (provider-and-child-inits.ts, emit-reactive.ts)
|
|
76
|
+
// reference `__scope` directly instead (#2649, see
|
|
77
|
+
// `ClientJsContext.commentScopeRootSlotId`'s docstring).
|
|
78
|
+
if (ctx.commentScopeRootSlotId) {
|
|
79
|
+
componentSlots.delete(ctx.commentScopeRootSlotId)
|
|
80
|
+
}
|
|
81
|
+
|
|
74
82
|
if (regularSlots.size === 0 && componentSlots.size === 0) return ''
|
|
75
83
|
|
|
76
84
|
const refLines: string[] = []
|