@barefootjs/jsx 0.28.1 → 0.30.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/child-scope.d.ts +17 -0
- package/dist/adapters/child-scope.d.ts.map +1 -0
- package/dist/adapters/loop-bound-names.d.ts.map +1 -1
- package/dist/analyzer-context.d.ts.map +1 -1
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +890 -357
- package/dist/ir-to-client-js/collect-elements.d.ts +5 -25
- package/dist/ir-to-client-js/collect-elements.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/build-lazy-row.d.ts +66 -2
- package/dist/ir-to-client-js/control-flow/plan/build-lazy-row.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/lazy-conditional.d.ts +92 -0
- package/dist/ir-to-client-js/control-flow/plan/lazy-conditional.d.ts.map +1 -0
- package/dist/ir-to-client-js/control-flow/plan/lazy-preamble.d.ts +134 -0
- package/dist/ir-to-client-js/control-flow/plan/lazy-preamble.d.ts.map +1 -0
- package/dist/ir-to-client-js/control-flow/plan/lazy-row-eligibility.d.ts +46 -11
- package/dist/ir-to-client-js/control-flow/plan/lazy-row-eligibility.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/reactive-effects.d.ts +7 -0
- package/dist/ir-to-client-js/control-flow/plan/reactive-effects.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/composite-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/control-flow/stringify/template-parse.d.ts +14 -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.map +1 -1
- package/dist/ir-to-client-js/html-template.d.ts +2 -3
- 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/declaration-emit.d.ts +6 -0
- package/dist/ir-to-client-js/plan/declaration-emit.d.ts.map +1 -1
- package/dist/ir-to-client-js/reactivity.d.ts +1 -10
- package/dist/ir-to-client-js/reactivity.d.ts.map +1 -1
- package/dist/ir-to-client-js/types.d.ts +12 -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/rich-type-evidence.d.ts.map +1 -1
- package/dist/ssr-defaults.d.ts.map +1 -1
- package/dist/to-locale-date-lowering.d.ts.map +1 -1
- package/dist/types.d.ts +78 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/value-references.d.ts +17 -4
- package/dist/value-references.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/__snapshots__/doc-examples.test.ts.snap +6 -24
- package/src/__tests__/branch-loop-plain.test.ts +28 -45
- package/src/__tests__/csr-substitution-safety-divergence.test.ts +9 -3
- package/src/__tests__/csr-template-scope-soundness.test.ts +96 -0
- package/src/__tests__/early-return-branch-switch.test.ts +96 -0
- package/src/__tests__/form-control-value-ssr.test.ts +102 -0
- package/src/__tests__/ir-to-client-js/imports.test.ts +18 -0
- package/src/__tests__/lazy-conditional.test.ts +190 -0
- package/src/__tests__/lazy-preamble.test.ts +297 -0
- package/src/__tests__/lazy-row-eligibility.test.ts +97 -28
- package/src/__tests__/loop-item-root-scope.test.ts +213 -0
- package/src/__tests__/loop-row-creation-paths.test.ts +169 -0
- package/src/__tests__/map-multi-return-body.test.ts +30 -3
- package/src/__tests__/nested-loop-conditional.test.ts +27 -6
- package/src/__tests__/preamble-attr-reactivity.test.ts +217 -0
- package/src/__tests__/preamble-declarations.test.ts +207 -0
- package/src/__tests__/ssr-defaults.test.ts +44 -0
- package/src/__tests__/value-references.test.ts +88 -0
- package/src/adapters/child-scope.ts +22 -0
- package/src/adapters/loop-bound-names.ts +6 -0
- package/src/analyzer-context.ts +43 -0
- package/src/analyzer.ts +33 -3
- package/src/index.ts +1 -0
- package/src/ir-to-client-js/collect-elements.ts +51 -10
- package/src/ir-to-client-js/control-flow/plan/build-branch-loop.ts +1 -1
- package/src/ir-to-client-js/control-flow/plan/build-composite-loop.ts +2 -2
- package/src/ir-to-client-js/control-flow/plan/build-event-delegation.ts +3 -3
- package/src/ir-to-client-js/control-flow/plan/build-inner-loop.ts +2 -2
- package/src/ir-to-client-js/control-flow/plan/build-lazy-row.ts +147 -5
- package/src/ir-to-client-js/control-flow/plan/build-loop.ts +2 -2
- package/src/ir-to-client-js/control-flow/plan/build-reactive-effects.ts +1 -0
- package/src/ir-to-client-js/control-flow/plan/lazy-conditional.ts +151 -0
- package/src/ir-to-client-js/control-flow/plan/lazy-preamble.ts +287 -0
- package/src/ir-to-client-js/control-flow/plan/lazy-row-eligibility.ts +85 -22
- package/src/ir-to-client-js/control-flow/plan/reactive-effects.ts +7 -0
- package/src/ir-to-client-js/control-flow/stringify/composite-loop.ts +4 -0
- package/src/ir-to-client-js/control-flow/stringify/lazy-row.ts +133 -35
- package/src/ir-to-client-js/control-flow/stringify/reactive-effects.ts +24 -4
- package/src/ir-to-client-js/control-flow/stringify/template-parse.ts +23 -4
- package/src/ir-to-client-js/csr-substitute.ts +6 -1
- package/src/ir-to-client-js/emit-registration.ts +1 -1
- package/src/ir-to-client-js/html-template.ts +51 -40
- package/src/ir-to-client-js/imports.ts +5 -0
- package/src/ir-to-client-js/index.ts +1 -1
- package/src/ir-to-client-js/plan/build-declaration-emit.ts +2 -0
- package/src/ir-to-client-js/plan/build-static-array-child-init.ts +1 -1
- package/src/ir-to-client-js/plan/declaration-emit.ts +6 -0
- package/src/ir-to-client-js/reactivity.ts +25 -0
- package/src/ir-to-client-js/stringify/declaration-emit.ts +9 -4
- package/src/ir-to-client-js/types.ts +12 -0
- package/src/jsx-to-ir.ts +400 -24
- package/src/rich-type-evidence.ts +6 -7
- package/src/ssr-defaults.ts +7 -2
- package/src/to-locale-date-lowering.ts +8 -2
- package/src/types.ts +80 -0
- package/src/value-references.ts +26 -4
|
@@ -12,6 +12,7 @@ import { buildSignalMemoEnv, csrSubstitute, applyPropsRewrite, type CsrEnv } fro
|
|
|
12
12
|
import type { ClientJsContext } from './types.ts'
|
|
13
13
|
import { BF_PARENT_SCOPE_PLACEHOLDER, BF_SCOPE, escapeHtml } from '@barefootjs/shared'
|
|
14
14
|
import { buildLoopChainExpr } from '../loop-chain.ts'
|
|
15
|
+
import { derivesScopeFromSlot } from '../adapters/child-scope.ts'
|
|
15
16
|
|
|
16
17
|
/**
|
|
17
18
|
* Protect string literals from regex-based replacements.
|
|
@@ -547,12 +548,11 @@ function buildSpreadAttrsMergeCall(args: {
|
|
|
547
548
|
* are wrapped with `__bfSlot(EXPR, <branchSlotsVar>)` so the runtime can
|
|
548
549
|
* splice live `Node` returns into the parsed fragment instead of
|
|
549
550
|
* stringifying them via the template literal (#1213).
|
|
550
|
-
*
|
|
551
|
-
*
|
|
552
|
-
*
|
|
553
|
-
*
|
|
554
|
-
*
|
|
555
|
-
* the per-iteration `staticItemTemplate` for static loops.
|
|
551
|
+
* Component nodes omit their `slotId` from the `renderChild` call exactly
|
|
552
|
+
* when `derivesScopeFromSlot()` says so — a loop-item-root component (its
|
|
553
|
+
* own scope, identified by `data-key`) never derives from the parent slot;
|
|
554
|
+
* a component nested below the row root does (#2444). See
|
|
555
|
+
* `IRComponent.loopItemRoot` / `./adapters/child-scope.ts`.
|
|
556
556
|
*/
|
|
557
557
|
/**
|
|
558
558
|
* Build the per-item `<!--bf-loop-i:KEY-->` anchor comment for a whole-item
|
|
@@ -660,7 +660,7 @@ export function renderFlatMapClientBody(
|
|
|
660
660
|
rawLeaf: true,
|
|
661
661
|
renderLeaf: (ir) => {
|
|
662
662
|
const key = flatMapLeafKeyExpr(ir)
|
|
663
|
-
const html = irToHtmlTemplate(stripLeafKeyAttr(ir), restSpreadNames, 1, undefined, undefined
|
|
663
|
+
const html = irToHtmlTemplate(stripLeafKeyAttr(ir), restSpreadNames, 1, undefined, undefined)
|
|
664
664
|
return `({ k: ${key ?? 'undefined'}, h: \`${html}\` })`
|
|
665
665
|
},
|
|
666
666
|
})
|
|
@@ -691,7 +691,7 @@ export function renderFlatMapProjectionClientBody(
|
|
|
691
691
|
const params = inner.index ? `(${inner.param}, ${inner.index})` : `(${inner.param})`
|
|
692
692
|
const key = inner.key ? `(${inner.key})` : 'undefined'
|
|
693
693
|
const html = inner.children
|
|
694
|
-
.map((c) => irToHtmlTemplate(escapeLeafTextExpressions(c), restSpreadNames, 1, undefined, undefined
|
|
694
|
+
.map((c) => irToHtmlTemplate(escapeLeafTextExpressions(c), restSpreadNames, 1, undefined, undefined))
|
|
695
695
|
.join('')
|
|
696
696
|
return `${chained}.map(${params} => ({ k: ${key}, h: \`${html}\` }))`
|
|
697
697
|
}
|
|
@@ -728,8 +728,8 @@ function escapeLeafTextExpressions(ir: IRNode): IRNode {
|
|
|
728
728
|
}
|
|
729
729
|
}
|
|
730
730
|
|
|
731
|
-
export function irToHtmlTemplate(node: IRNode, restSpreadNames?: Set<string>, loopDepth = 0, loopParams?: ReadonlyArray<string | LoopParamSpec>, branchSlotsVar?: string,
|
|
732
|
-
const recurse = (n: IRNode): string => irToHtmlTemplate(n, restSpreadNames, loopDepth, loopParams, branchSlotsVar,
|
|
731
|
+
export function irToHtmlTemplate(node: IRNode, restSpreadNames?: Set<string>, loopDepth = 0, loopParams?: ReadonlyArray<string | LoopParamSpec>, branchSlotsVar?: string, inHoistedChildren = false): string {
|
|
732
|
+
const recurse = (n: IRNode): string => irToHtmlTemplate(n, restSpreadNames, loopDepth, loopParams, branchSlotsVar, inHoistedChildren)
|
|
733
733
|
const wrapExpr = (expr: string) => wrapExprWithLoopParams(expr, loopParams)
|
|
734
734
|
const wrapInterpolation = (expr: string): string => branchSlotsVar
|
|
735
735
|
? `__bfSlot(${expr}, ${branchSlotsVar})`
|
|
@@ -781,7 +781,7 @@ export function irToHtmlTemplate(node: IRNode, restSpreadNames?: Set<string>, lo
|
|
|
781
781
|
}
|
|
782
782
|
|
|
783
783
|
const attrs = attrParts.join(' ')
|
|
784
|
-
const childrenRecurse = (n: IRNode): string => irToHtmlTemplate(n, restSpreadNames, loopDepth, loopParams, branchSlotsVar,
|
|
784
|
+
const childrenRecurse = (n: IRNode): string => irToHtmlTemplate(n, restSpreadNames, loopDepth, loopParams, branchSlotsVar, false)
|
|
785
785
|
const children = dangerouslyHtmlChildren(node.attrs, v => wrapExpr(v.expr)) ?? node.children.map(childrenRecurse).join('')
|
|
786
786
|
|
|
787
787
|
// Non-void elements must use open+close tags (HTML parsers ignore self-closing on div, span, etc.)
|
|
@@ -853,13 +853,17 @@ export function irToHtmlTemplate(node: IRNode, restSpreadNames?: Set<string>, lo
|
|
|
853
853
|
// instead of emitting empty placeholders (#435).
|
|
854
854
|
const propsEntries = node.props
|
|
855
855
|
.filter(p => p.name !== '...' && !p.name.startsWith('...') && p.name !== 'key')
|
|
856
|
-
|
|
856
|
+
// `ref` joins the `on*` handlers here: a function prop cannot run
|
|
857
|
+
// during module-scope string rendering, and a ref closing over an
|
|
858
|
+
// init-scope setter would leak it into the template (#2468). The
|
|
859
|
+
// init path re-applies refs once real DOM exists.
|
|
860
|
+
.filter(p => p.name !== 'ref' && !(p.name.startsWith('on') && p.name.length > 2 && p.name[2] === p.name[2].toUpperCase()))
|
|
857
861
|
.map(p => {
|
|
858
862
|
// `/* @client */` defers the prop to hydrate via initChild.
|
|
859
863
|
if (p.clientOnly) return null
|
|
860
864
|
switch (p.value.kind) {
|
|
861
865
|
case 'jsx-children': {
|
|
862
|
-
const hoistedRecurse = (n: IRNode): string => irToHtmlTemplate(n, restSpreadNames, loopDepth, loopParams, branchSlotsVar,
|
|
866
|
+
const hoistedRecurse = (n: IRNode): string => irToHtmlTemplate(n, restSpreadNames, loopDepth, loopParams, branchSlotsVar, true)
|
|
863
867
|
const childHtml = p.value.children.map(c => hoistedRecurse(c)).join('')
|
|
864
868
|
return `${quotePropName(p.name)}: \`${childHtml}\``
|
|
865
869
|
}
|
|
@@ -872,7 +876,15 @@ export function irToHtmlTemplate(node: IRNode, restSpreadNames?: Set<string>, lo
|
|
|
872
876
|
case 'expression':
|
|
873
877
|
case 'template':
|
|
874
878
|
case 'spread': {
|
|
875
|
-
|
|
879
|
+
// Template-variant form (`useTemplate`): this props object
|
|
880
|
+
// lands inside the module-scope registration template, which
|
|
881
|
+
// is not a closure over init's destructured-prop extractions
|
|
882
|
+
// — the raw form leaked bare refs like `${className}` into
|
|
883
|
+
// `renderChild(...)` (#2468). Mirrors the sibling component
|
|
884
|
+
// emit sites (`irToComponentTemplateWithOpts`,
|
|
885
|
+
// `generateCsrTemplateWithOpts`), which already pick the
|
|
886
|
+
// prop-rewritten variant.
|
|
887
|
+
const expr = attrValueToString(p.value, { useTemplate: true }) ?? 'undefined'
|
|
876
888
|
return `${quotePropName(p.name)}: ${expr}`
|
|
877
889
|
}
|
|
878
890
|
}
|
|
@@ -884,11 +896,12 @@ export function irToHtmlTemplate(node: IRNode, restSpreadNames?: Set<string>, lo
|
|
|
884
896
|
const keyProp = node.props.find(p => p.name === 'key')
|
|
885
897
|
const keyArg = keyProp ? `, ${attrValueToString(keyProp.value) ?? 'undefined'}` : ''
|
|
886
898
|
// Pass slotId as suffix so $c() can find the child component by slot
|
|
887
|
-
// after branch swap.
|
|
888
|
-
//
|
|
889
|
-
//
|
|
890
|
-
//
|
|
891
|
-
|
|
899
|
+
// after branch swap. A loop-item-root component owns a separate scope
|
|
900
|
+
// per iteration (identified by `data-key`), so the parent-slot suffix
|
|
901
|
+
// is dropped to avoid anchoring it to a wrong component-wrapper scope
|
|
902
|
+
// (#1268) — a component nested BELOW the row root still derives from
|
|
903
|
+
// its slot, matching the Hono reference (#2444).
|
|
904
|
+
const slotArg = derivesScopeFromSlot(node) ? `, '${node.slotId}'` : ''
|
|
892
905
|
return `\${renderChild('${nameForRegistryRef(node.name)}', ${propsExpr}${keyArg || (slotArg ? ', undefined' : '')}${slotArg})}`
|
|
893
906
|
}
|
|
894
907
|
|
|
@@ -897,13 +910,7 @@ export function irToHtmlTemplate(node: IRNode, restSpreadNames?: Set<string>, lo
|
|
|
897
910
|
// Increment loopDepth so inner key attrs become data-key-N
|
|
898
911
|
// Forward loopParams so expressions referencing outer/inner loop params
|
|
899
912
|
// get wrapped as signal accessors (e.g., task.title → task().title).
|
|
900
|
-
|
|
901
|
-
// inner-loop templates depend on the legacy slot-suffix-keeping shape
|
|
902
|
-
// to find scopes via `findSsrScopeBySlotIn`. The opt-in at the entry
|
|
903
|
-
// call site is the only place that wants the suffix dropped (#1268
|
|
904
|
-
// Case 1 — childComponent body materialize); propagating it through
|
|
905
|
-
// every nested loop regressed form-builder's inner-loop Select wiring.
|
|
906
|
-
const innerRecurse = (n: IRNode): string => irToHtmlTemplate(n, restSpreadNames, loopDepth + 1, loopParams, branchSlotsVar, insideLoop)
|
|
913
|
+
const innerRecurse = (n: IRNode): string => irToHtmlTemplate(n, restSpreadNames, loopDepth + 1, loopParams, branchSlotsVar)
|
|
907
914
|
let childTemplate = node.children.map(innerRecurse).join('')
|
|
908
915
|
// Whole-item conditional loops (#1665): prepend an always-present
|
|
909
916
|
// `<!--bf-loop-i:KEY-->` anchor before each item's (possibly empty)
|
|
@@ -938,13 +945,13 @@ export function irToHtmlTemplate(node: IRNode, restSpreadNames?: Set<string>, lo
|
|
|
938
945
|
// identity is stamped by mapArray via setAttribute — emitting it
|
|
939
946
|
// here was the CSR/SSR data-key asymmetry (unescaped, client-only).
|
|
940
947
|
const body = renderPreamble(node.flatMapCallback, {
|
|
941
|
-
renderLeaf: (ir) => irToHtmlTemplate(stripLeafKeyAttr(ir), restSpreadNames, loopDepth + 1, loopParams, branchSlotsVar
|
|
948
|
+
renderLeaf: (ir) => irToHtmlTemplate(stripLeafKeyAttr(ir), restSpreadNames, loopDepth + 1, loopParams, branchSlotsVar),
|
|
942
949
|
})
|
|
943
950
|
mapExpr = `\${${wrappedArray}.flatMap(${node.flatMapCallback.params} => ${body}).join('')}`
|
|
944
951
|
} else if (node.preamble) {
|
|
945
952
|
// Stage 3 / D4 — render JSX leaves in an arbitrary array-builder
|
|
946
953
|
// preamble (the hydrate-template context uses the bare loop param).
|
|
947
|
-
const preamble = renderPreamble(node.preamble, { textVariant: 'client', renderLeaf: (ir) => irToHtmlTemplate(ir, restSpreadNames, loopDepth + 1, loopParams, branchSlotsVar
|
|
954
|
+
const preamble = renderPreamble(node.preamble, { textVariant: 'client', renderLeaf: (ir) => irToHtmlTemplate(ir, restSpreadNames, loopDepth + 1, loopParams, branchSlotsVar) })
|
|
948
955
|
mapExpr = `\${${wrappedArray}.${iterMethod}(${callbackParam} => { ${preamble} return \`${childTemplate}\` }).join('')}`
|
|
949
956
|
} else {
|
|
950
957
|
mapExpr = `\${${wrappedArray}.${iterMethod}(${callbackParam} => \`${childTemplate}\`).join('')}`
|
|
@@ -1563,7 +1570,6 @@ export interface TemplateOptions {
|
|
|
1563
1570
|
* `compute-inlinability` (#1277).
|
|
1564
1571
|
*/
|
|
1565
1572
|
csrEnv?: CsrEnv
|
|
1566
|
-
insideLoop?: boolean
|
|
1567
1573
|
loopDepth?: number
|
|
1568
1574
|
/**
|
|
1569
1575
|
* Names bound by the ENCLOSING loops' callback params (item / index /
|
|
@@ -1799,7 +1805,11 @@ function irToComponentTemplateWithOpts(node: IRNode, opts: TemplateOptions): str
|
|
|
1799
1805
|
// Use renderChild() to render child component's template at runtime (#435)
|
|
1800
1806
|
const propsEntries = node.props
|
|
1801
1807
|
.filter(p => p.name !== '...' && !p.name.startsWith('...') && p.name !== 'key')
|
|
1802
|
-
|
|
1808
|
+
// `ref` joins the `on*` handlers here: a function prop cannot run
|
|
1809
|
+
// during module-scope string rendering, and a ref closing over an
|
|
1810
|
+
// init-scope setter would leak it into the template (#2468). The
|
|
1811
|
+
// init path re-applies refs once real DOM exists.
|
|
1812
|
+
.filter(p => p.name !== 'ref' && !(p.name.startsWith('on') && p.name.length > 2 && p.name[2] === p.name[2].toUpperCase()))
|
|
1803
1813
|
.map(p => {
|
|
1804
1814
|
// `/* @client */` defers the prop to hydrate via initChild.
|
|
1805
1815
|
if (p.clientOnly) return null
|
|
@@ -1990,7 +2000,6 @@ export function generateCsrTemplate(
|
|
|
1990
2000
|
node: IRNode,
|
|
1991
2001
|
inlinableConstants: Map<string, string> | undefined,
|
|
1992
2002
|
ctx: ClientJsContext,
|
|
1993
|
-
insideLoop?: boolean,
|
|
1994
2003
|
restSpreadNames?: Set<string>,
|
|
1995
2004
|
propsObjectName?: string | null,
|
|
1996
2005
|
unsafeLocalNames?: Set<string>,
|
|
@@ -2010,7 +2019,7 @@ export function generateCsrTemplate(
|
|
|
2010
2019
|
}
|
|
2011
2020
|
}
|
|
2012
2021
|
const effectiveUnsafeLocalNames = mergeCsrNullUnsafe(ctx, unsafeLocalNames)
|
|
2013
|
-
return generateCsrTemplateWithOpts(node, { inlinableConstants, restSpreadNames, propsObjectName, csrEnv,
|
|
2022
|
+
return generateCsrTemplateWithOpts(node, { inlinableConstants, restSpreadNames, propsObjectName, csrEnv, unsafeLocalNames: effectiveUnsafeLocalNames, deferredChildSlots, loopDepth: -1 })
|
|
2014
2023
|
}
|
|
2015
2024
|
|
|
2016
2025
|
/**
|
|
@@ -2171,6 +2180,7 @@ export function computeDeferredChildSlots(
|
|
|
2171
2180
|
// filter set MUST mirror the `propsEntries` filter in the CSR
|
|
2172
2181
|
// `component` emit below so the deferral decision matches output.
|
|
2173
2182
|
if (p.name === '...' || p.name.startsWith('...') || p.name === 'key') return false
|
|
2183
|
+
if (p.name === 'ref') return false
|
|
2174
2184
|
if (p.name.startsWith('on') && p.name.length > 2 && p.name[2] === p.name[2].toUpperCase()) return false
|
|
2175
2185
|
if (p.clientOnly) return false
|
|
2176
2186
|
return propResolvesUnsafe(p, env, unsafeLocalNames)
|
|
@@ -2204,7 +2214,7 @@ export function computeDeferredChildSlots(
|
|
|
2204
2214
|
}
|
|
2205
2215
|
|
|
2206
2216
|
function generateCsrTemplateWithOpts(node: IRNode, opts: TemplateOptions): string {
|
|
2207
|
-
const { restSpreadNames, propsObjectName, csrEnv,
|
|
2217
|
+
const { restSpreadNames, propsObjectName, csrEnv, unsafeLocalNames, loopDepth = 0 } = opts
|
|
2208
2218
|
const env: CsrEnv = csrEnv ?? { substitutions: new Map(), propsObjectName: propsObjectName ?? null }
|
|
2209
2219
|
const transformExpr = (expr: string, templateExpr?: string): string => {
|
|
2210
2220
|
// Single AST substitution pass: replaces signal getter calls
|
|
@@ -2241,12 +2251,10 @@ function generateCsrTemplateWithOpts(node: IRNode, opts: TemplateOptions): strin
|
|
|
2241
2251
|
}
|
|
2242
2252
|
|
|
2243
2253
|
// `recurse` preserves `inHoistedChildren` for structural pass-through
|
|
2244
|
-
// (fragment / conditional); `childrenRecurse`
|
|
2245
|
-
//
|
|
2246
|
-
// scope. (#1320)
|
|
2254
|
+
// (fragment / conditional); `childrenRecurse` clears it — a new element
|
|
2255
|
+
// root starts a fresh scope. (#1320)
|
|
2247
2256
|
const recurse = (n: IRNode): string => generateCsrTemplateWithOpts(n, opts)
|
|
2248
2257
|
const childrenRecurse = (n: IRNode): string => generateCsrTemplateWithOpts(n, { ...opts, inHoistedChildren: false })
|
|
2249
|
-
const recurseInLoop = (n: IRNode): string => generateCsrTemplateWithOpts(n, { ...opts, insideLoop: true, loopDepth: loopDepth + 1, inHoistedChildren: false })
|
|
2250
2258
|
|
|
2251
2259
|
switch (node.type) {
|
|
2252
2260
|
case 'element': {
|
|
@@ -2399,7 +2407,11 @@ function generateCsrTemplateWithOpts(node: IRNode, opts: TemplateOptions): strin
|
|
|
2399
2407
|
|
|
2400
2408
|
const propsEntries = node.props
|
|
2401
2409
|
.filter(p => p.name !== '...' && !p.name.startsWith('...') && p.name !== 'key')
|
|
2402
|
-
|
|
2410
|
+
// `ref` joins the `on*` handlers here: a function prop cannot run
|
|
2411
|
+
// during module-scope string rendering, and a ref closing over an
|
|
2412
|
+
// init-scope setter would leak it into the template (#2468). The
|
|
2413
|
+
// init path re-applies refs once real DOM exists.
|
|
2414
|
+
.filter(p => p.name !== 'ref' && !(p.name.startsWith('on') && p.name.length > 2 && p.name[2] === p.name[2].toUpperCase()))
|
|
2403
2415
|
.map(p => {
|
|
2404
2416
|
// `/* @client */` defers the prop to hydrate. Drop from the
|
|
2405
2417
|
// SSR `renderChild` props — `initChild`'s `propsExpr` getter
|
|
@@ -2441,7 +2453,7 @@ function generateCsrTemplateWithOpts(node: IRNode, opts: TemplateOptions): strin
|
|
|
2441
2453
|
const propsExpr = propsEntries.length > 0 ? `{${propsEntries.join(', ')}}` : '{}'
|
|
2442
2454
|
const keyProp = node.props.find(p => p.name === 'key')
|
|
2443
2455
|
const keyArg = keyProp ? `, ${transformKeyValue(keyProp.value, transformExpr)}` : ''
|
|
2444
|
-
const slotArg = (
|
|
2456
|
+
const slotArg = derivesScopeFromSlot(node) ? `, '${node.slotId}'` : ''
|
|
2445
2457
|
return `\${renderChild('${nameForRegistryRef(node.name)}', ${propsExpr}${keyArg || (slotArg ? ', undefined' : '')}${slotArg})}`
|
|
2446
2458
|
}
|
|
2447
2459
|
|
|
@@ -2469,7 +2481,6 @@ function generateCsrTemplateWithOpts(node: IRNode, opts: TemplateOptions): strin
|
|
|
2469
2481
|
}
|
|
2470
2482
|
const recurseInLoopBody = (n: IRNode): string => generateCsrTemplateWithOpts(n, {
|
|
2471
2483
|
...opts,
|
|
2472
|
-
insideLoop: true,
|
|
2473
2484
|
loopDepth: loopDepth + 1,
|
|
2474
2485
|
inHoistedChildren: false,
|
|
2475
2486
|
loopBoundNames: boundHere,
|
|
@@ -11,6 +11,11 @@ export const RUNTIME_IMPORT_CANDIDATES = [
|
|
|
11
11
|
'createSignal', 'createMemo', 'createEffect', 'onCleanup', 'onMount',
|
|
12
12
|
'hydrate', 'insert', 'getLoopChildren', 'getLoopNodes', 'mapArray', 'mapArrayAnchored', 'mapArrayLazy', 'patchLeaf', 'createDisposableEffect',
|
|
13
13
|
'createComponent', 'renderChild', 'registerComponent', 'registerTemplate', 'initChild', 'upsertChild',
|
|
14
|
+
// Connects a template-clone loop row before the body's tail runs, so a child
|
|
15
|
+
// that inits inside it resolves context against real ancestors rather than
|
|
16
|
+
// falling through to the global store. The clone-root counterpart of the
|
|
17
|
+
// mount point `createComponent` consumes for component-root rows.
|
|
18
|
+
'mountRowRoot',
|
|
14
19
|
'createPortal',
|
|
15
20
|
'provideContext', 'createContext', 'useContext',
|
|
16
21
|
'forwardProps', 'applyRestAttrs', 'splitProps', 'spreadAttrs', 'styleToCss', 'escapeAttr', 'escapeText', 'escapeTextOrNode',
|
|
@@ -267,7 +267,7 @@ function generateTemplateOnlyMount(ir: ComponentIR, ctx: ClientJsContext): strin
|
|
|
267
267
|
if (!templateHtml) {
|
|
268
268
|
const csrInlinableConstants = csrInlinableConstantsFromCtx(ctx)
|
|
269
269
|
templateHtml = generateCsrTemplate(
|
|
270
|
-
ir.root, csrInlinableConstants, ctx,
|
|
270
|
+
ir.root, csrInlinableConstants, ctx, restSpreadNames, ctx.propsObjectName, unsafeLocalNames
|
|
271
271
|
)
|
|
272
272
|
}
|
|
273
273
|
|
|
@@ -95,6 +95,7 @@ function buildSignalPlan(
|
|
|
95
95
|
kind: 'signal',
|
|
96
96
|
getter: signal.getter,
|
|
97
97
|
setter: signal.setter,
|
|
98
|
+
getterElided: signal.getterElided,
|
|
98
99
|
initialValueExpr: '',
|
|
99
100
|
controlledEffect: null,
|
|
100
101
|
initializerOverride: `${factory}()`,
|
|
@@ -109,6 +110,7 @@ function buildSignalPlan(
|
|
|
109
110
|
kind: 'signal',
|
|
110
111
|
getter: signal.getter,
|
|
111
112
|
setter: signal.setter,
|
|
113
|
+
getterElided: signal.getterElided,
|
|
112
114
|
initialValueExpr: resolveSignalInitialValue(signal, ctx, lookups),
|
|
113
115
|
controlledEffect,
|
|
114
116
|
branchCondition: signal.branchCondition,
|
|
@@ -30,7 +30,7 @@ import { irChildrenToJsExpr, renderPreamble, irToHtmlTemplate } from '../html-te
|
|
|
30
30
|
function staticPreludeStatements(preamble: MapCallbackPreamble | undefined): string[] {
|
|
31
31
|
return preamble
|
|
32
32
|
? [renderPreamble(preamble, {
|
|
33
|
-
renderLeaf: (ir) => irToHtmlTemplate(ir, undefined, 1, undefined, undefined
|
|
33
|
+
renderLeaf: (ir) => irToHtmlTemplate(ir, undefined, 1, undefined, undefined),
|
|
34
34
|
})]
|
|
35
35
|
: []
|
|
36
36
|
}
|
|
@@ -27,6 +27,12 @@ export interface SignalEmitPlan {
|
|
|
27
27
|
getter: string
|
|
28
28
|
/** Setter identifier — `null` for read-only signals. */
|
|
29
29
|
setter: string | null
|
|
30
|
+
/**
|
|
31
|
+
* Source used the getter-elided form (`const [, setX] = ...`). `getter`
|
|
32
|
+
* holds the synthesized internal name (see `SignalInfo.getterElided`);
|
|
33
|
+
* stringify reproduces the source's hole instead of emitting it.
|
|
34
|
+
*/
|
|
35
|
+
getterElided?: boolean
|
|
30
36
|
/**
|
|
31
37
|
* Fully-resolved initial value expression. Prop rewrites
|
|
32
38
|
* (`<propsName>.X` → `_p.X`) and `?? <default>` insertions for controlled
|
|
@@ -16,6 +16,7 @@ import type {
|
|
|
16
16
|
} from './types.ts'
|
|
17
17
|
import { attrValueToString, freeIdsFromRefs, tokenContainsIdent } from './utils.ts'
|
|
18
18
|
import { expandConstantForReactivity } from './prop-handling.ts'
|
|
19
|
+
import { extractFreeIdentifiersFromText } from './csr-substitute.ts'
|
|
19
20
|
import { walkIR, stopAt } from './walker.ts'
|
|
20
21
|
|
|
21
22
|
/**
|
|
@@ -621,12 +622,20 @@ export function collectLoopChildReactiveTexts(
|
|
|
621
622
|
* same semantics. Pass true only when the caller also separately collects
|
|
622
623
|
* nested reactive conditionals and gives each its own `insert()`.
|
|
623
624
|
*/
|
|
625
|
+
/** Does any name in `names` appear in `set`? Iterates rather than
|
|
626
|
+
* spreading — this runs per attribute (Copilot review). */
|
|
627
|
+
function anyNameIn(names: Iterable<string>, set: ReadonlySet<string>): boolean {
|
|
628
|
+
for (const n of names) if (set.has(n)) return true
|
|
629
|
+
return false
|
|
630
|
+
}
|
|
631
|
+
|
|
624
632
|
export function collectLoopChildReactiveAttrs(
|
|
625
633
|
node: IRNode,
|
|
626
634
|
ctx: ClientJsContext,
|
|
627
635
|
loopParam?: string,
|
|
628
636
|
loopParamBindings?: readonly LoopParamBinding[],
|
|
629
637
|
stopAtReactiveConditionals = false,
|
|
638
|
+
preambleNames?: ReadonlySet<string>,
|
|
630
639
|
): LoopChildReactiveAttr[] {
|
|
631
640
|
const attrs: LoopChildReactiveAttr[] = []
|
|
632
641
|
traverseElements(node, (el) => {
|
|
@@ -664,8 +673,23 @@ export function collectLoopChildReactiveAttrs(
|
|
|
664
673
|
// / `hasFunctionCalls` fallback so the loop-child path matches the
|
|
665
674
|
// top-level one — a harmless over-wrap at worst (an effect that
|
|
666
675
|
// subscribes to nothing runs once).
|
|
676
|
+
// A `.map()` callback preamble local (#2447 follow-up). The
|
|
677
|
+
// classifier cannot see it: `const cls = row.done ? …` makes `cls`
|
|
678
|
+
// neither a signal read nor a loop-param read, so `class={cls}`
|
|
679
|
+
// scored 'none' and the value froze at its row-construction value —
|
|
680
|
+
// a same-key item update rewrote the sibling `{row.label}` text and
|
|
681
|
+
// left the class stale. Its reactivity is the PREAMBLE's: whatever
|
|
682
|
+
// the initializer read. Flag it and let the emitter re-run the
|
|
683
|
+
// preamble ahead of the write (`stringifyReactiveEffects`), which is
|
|
684
|
+
// the child-position `preambleRegions` treatment (#2389) applied to
|
|
685
|
+
// the attribute position.
|
|
686
|
+
const readsPreamble =
|
|
687
|
+
preambleNames !== undefined &&
|
|
688
|
+
preambleNames.size > 0 &&
|
|
689
|
+
anyNameIn(expanded.freeIds ?? extractFreeIdentifiersFromText(expanded.expr), preambleNames)
|
|
667
690
|
const reactive =
|
|
668
691
|
classifyReactivity(expanded.expr, ctx, loopParam, loopParamBindings, expanded.freeIds).kind !== 'none'
|
|
692
|
+
|| readsPreamble
|
|
669
693
|
|| attr.callsReactiveGetters
|
|
670
694
|
|| attr.hasFunctionCalls
|
|
671
695
|
if (!attr.clientOnly && !reactive) continue
|
|
@@ -675,6 +699,7 @@ export function collectLoopChildReactiveAttrs(
|
|
|
675
699
|
expression: expanded.expr,
|
|
676
700
|
...pickAttrMetaFromIR(attr),
|
|
677
701
|
...(expanded.freeIds !== undefined && { freeIdentifiers: expanded.freeIds }),
|
|
702
|
+
...(readsPreamble && { readsPreamble: true }),
|
|
678
703
|
})
|
|
679
704
|
}
|
|
680
705
|
}
|
|
@@ -65,12 +65,17 @@ function bfIdArg(bfId: string | undefined): string {
|
|
|
65
65
|
}
|
|
66
66
|
|
|
67
67
|
function emitSignal(lines: string[], plan: SignalEmitPlan): void {
|
|
68
|
+
// Getter-elided source form (`const [, setX] = createSignal(...)`):
|
|
69
|
+
// reproduce the hole instead of introducing the synthesized internal
|
|
70
|
+
// getter name into output (#2468 sweep — dropping the declaration
|
|
71
|
+
// entirely left the setter as a guaranteed ReferenceError).
|
|
72
|
+
const getterSlot = plan.getterElided ? '' : plan.getter
|
|
68
73
|
// Env signal (#2057): emit its own factory call verbatim (`createSearchParams()`)
|
|
69
74
|
// — a stable request-scoped view, so no baked initial value, profile id,
|
|
70
75
|
// controlled effect, or branch condition applies.
|
|
71
76
|
if (plan.initializerOverride) {
|
|
72
77
|
if (plan.setter) {
|
|
73
|
-
lines.push(` const [${
|
|
78
|
+
lines.push(` const [${getterSlot}, ${plan.setter}] = ${plan.initializerOverride}`)
|
|
74
79
|
} else {
|
|
75
80
|
lines.push(` const [${plan.getter}] = ${plan.initializerOverride}`)
|
|
76
81
|
}
|
|
@@ -85,9 +90,9 @@ function emitSignal(lines: string[], plan: SignalEmitPlan): void {
|
|
|
85
90
|
// runs when the branch is taken. Sibling branches with different
|
|
86
91
|
// signals coexist because each gets its own `let` + guarded assign.
|
|
87
92
|
if (plan.setter) {
|
|
88
|
-
lines.push(` let ${plan.getter}, ${plan.setter}`)
|
|
93
|
+
lines.push(plan.getterElided ? ` let ${plan.setter}` : ` let ${plan.getter}, ${plan.setter}`)
|
|
89
94
|
lines.push(` if (${plan.branchCondition}) {`)
|
|
90
|
-
lines.push(` ;[${
|
|
95
|
+
lines.push(` ;[${getterSlot}, ${plan.setter}] = createSignal(${plan.initialValueExpr}${id})`)
|
|
91
96
|
lines.push(` }`)
|
|
92
97
|
} else {
|
|
93
98
|
lines.push(` let ${plan.getter}`)
|
|
@@ -101,7 +106,7 @@ function emitSignal(lines: string[], plan: SignalEmitPlan): void {
|
|
|
101
106
|
return
|
|
102
107
|
}
|
|
103
108
|
if (plan.setter) {
|
|
104
|
-
lines.push(` const [${
|
|
109
|
+
lines.push(` const [${getterSlot}, ${plan.setter}] = createSignal(${plan.initialValueExpr}${id})`)
|
|
105
110
|
} else {
|
|
106
111
|
lines.push(` const [${plan.getter}] = createSignal(${plan.initialValueExpr}${id})`)
|
|
107
112
|
}
|
|
@@ -494,6 +494,18 @@ export interface LoopChildReactiveAttr extends AttrMeta {
|
|
|
494
494
|
childSlotId: string // bf slot ID of the element with reactive attr
|
|
495
495
|
attrName: string // 'className', 'disabled', etc.
|
|
496
496
|
expression: string // Expression that reads signals
|
|
497
|
+
/**
|
|
498
|
+
* The expression reads a `.map()` callback preamble local (#2447
|
|
499
|
+
* follow-up), so it is reactive by way of whatever the preamble read — the
|
|
500
|
+
* classifier cannot see through the declaration, and the local is not in
|
|
501
|
+
* scope in the update body until the preamble is re-run there.
|
|
502
|
+
*
|
|
503
|
+
* Two consequences, both handled by the emitter rather than here: the row
|
|
504
|
+
* effect runs `mapPreambleWrapped` BEFORE the attr writes, and the lazy row
|
|
505
|
+
* graph refuses the loop (`lazyRowEligibility`'s `readsPreamble` gate), so
|
|
506
|
+
* such a row keeps the eager per-row effect.
|
|
507
|
+
*/
|
|
508
|
+
readsPreamble?: boolean
|
|
497
509
|
}
|
|
498
510
|
|
|
499
511
|
export interface LoopChildReactiveText {
|