@barefootjs/go-template 0.29.0 → 0.30.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/adapter/go-template-adapter.d.ts +224 -0
- package/dist/adapter/go-template-adapter.d.ts.map +1 -1
- package/dist/adapter/index.js +294 -8
- package/dist/adapter/memo/memo-compute.d.ts +36 -0
- package/dist/adapter/memo/memo-compute.d.ts.map +1 -1
- package/dist/adapter/type/type-codegen.d.ts +21 -1
- package/dist/adapter/type/type-codegen.d.ts.map +1 -1
- package/dist/adapter/value/value-lowering.d.ts +1 -1
- package/dist/adapter/value/value-lowering.d.ts.map +1 -1
- package/dist/build.js +294 -8
- package/dist/conformance-pins.d.ts.map +1 -1
- package/dist/index.js +300 -10
- package/dist/render-divergences.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/__tests__/go-template-adapter.test.ts +626 -6
- package/src/adapter/go-template-adapter.ts +597 -13
- package/src/adapter/memo/memo-compute.ts +125 -0
- package/src/adapter/type/type-codegen.ts +44 -1
- package/src/adapter/value/value-lowering.ts +6 -1
- package/src/conformance-pins.ts +0 -5
- package/src/render-divergences.ts +24 -9
|
@@ -133,7 +133,7 @@ import { typeInfoToGo } from "./type/type-codegen.ts"
|
|
|
133
133
|
import { isBooleanMemo, isListFilterMemo, isStringTernaryMemo } from "./memo/memo-type.ts"
|
|
134
134
|
import { lowerCtorExpr } from "./memo/ctor-lowering.ts"
|
|
135
135
|
import { resolveBlockBodyMemoModuleConst } from "./memo/memo-value.ts"
|
|
136
|
-
import { computeMemoInitialValue, computeMemoInitialValueOrNull, filterArmEarlierSiblingRefs } from "./memo/memo-compute.ts"
|
|
136
|
+
import { computeMemoInitialValue, computeMemoInitialValueOrNull, filterArmEarlierSiblingRefs, collectPropsReadByCtorInit } from "./memo/memo-compute.ts"
|
|
137
137
|
import { collectSpreadSlots, buildSpreadInitializer } from "./spread/spread-codegen.ts"
|
|
138
138
|
import { buildPropTypeOverrides, resolvePropGoType, collectNillablePropNames, collectNullishConsumedPropNames, collectOmittableAttrConsumedPropNames, collectTextConsumedPropNames, collectPresenceCheckedPropNames, NULLISH_SCALAR_GO_TYPES } from "./props/prop-types.ts"
|
|
139
139
|
import { collectStringValueNames } from "./props/prop-classes.ts"
|
|
@@ -155,6 +155,23 @@ type HigherOrderShape = {
|
|
|
155
155
|
predicate: ParsedExpr
|
|
156
156
|
}
|
|
157
157
|
|
|
158
|
+
/**
|
|
159
|
+
* Everything needed to emit a component's #2448 props rebuilder, captured when
|
|
160
|
+
* its own types are generated so a PARENT can emit the registration into its
|
|
161
|
+
* own type block (`emitOwnedReprops`). `params` are this component's own
|
|
162
|
+
* Input fields — since #2457, the PARENT (`loopRowChildPropOverrides`, via
|
|
163
|
+
* `childPropFieldNames`) already resolves the JSX attribute name to the
|
|
164
|
+
* child's own field name before emitting the call, so the switch this spec
|
|
165
|
+
* drives is keyed by that one name on both sides; there is no longer a
|
|
166
|
+
* separate "wire" name to carry (an aliased destructure used to make the
|
|
167
|
+
* parent's JSX-attribute name and the child's field name differ — `"N"` vs
|
|
168
|
+
* `Count` — and this spec used to carry both).
|
|
169
|
+
*/
|
|
170
|
+
type RepropsSpec = {
|
|
171
|
+
params: string[]
|
|
172
|
+
usesSearchParams: boolean
|
|
173
|
+
}
|
|
174
|
+
|
|
158
175
|
/**
|
|
159
176
|
* String-returning array/string methods. `.get(...)` stays a generic `call`;
|
|
160
177
|
* the rest fold into `array-method`. Module-level so `isStringExpr` (which
|
|
@@ -441,6 +458,11 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
441
458
|
this.state.pendingChildrenDefines = []
|
|
442
459
|
this.primeCompileState(ir)
|
|
443
460
|
this.state.stringValueNames = collectStringValueNames(ir)
|
|
461
|
+
// #2448: self-register this component's derived-memo dependencies, so a
|
|
462
|
+
// SAME-FILE child (generated before its parent in the same run) is
|
|
463
|
+
// visible to `loopRowChildPropOverrides`. The cross-file pre-pass door is
|
|
464
|
+
// `registerChildComponentShape`; `compileJSX` only goes through here.
|
|
465
|
+
this.recordDerivedFieldDeps(ir, new Set((ir.metadata.propsParams ?? []).map(p => p.name)))
|
|
444
466
|
|
|
445
467
|
// Surface loop-body usages of sibling-imported components (see
|
|
446
468
|
// `checkImportedLoopChildComponents`). The barefoot CLI compiles a
|
|
@@ -618,10 +640,178 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
618
640
|
* (the CLI's cross-file shape pre-pass, #2131) can register from a bare
|
|
619
641
|
* analyzer pass — a full `ComponentIR` still satisfies it structurally.
|
|
620
642
|
*/
|
|
643
|
+
/**
|
|
644
|
+
* #2448: component name -> (memo field -> the INPUT prop Go field names its
|
|
645
|
+
* initializer reads). Deliberately SEPARATE from `childComponentShapes`,
|
|
646
|
+
* which only the CLI's cross-file pre-pass
|
|
647
|
+
* (`registerChildComponentShape`) populates — `compileJSX` never calls that
|
|
648
|
+
* hook, so a SAME-FILE child (the only kind a loop row may contain: a
|
|
649
|
+
* sibling-module child is BF103-refused there) would never have an entry
|
|
650
|
+
* and the #2448 refusal would never fire. That is exactly how the first cut
|
|
651
|
+
* of this fix shipped a conformance pin for a diagnostic that was never
|
|
652
|
+
* emitted.
|
|
653
|
+
*
|
|
654
|
+
* Populated from BOTH doors: `registerChildComponentShape` for the
|
|
655
|
+
* cross-file pre-pass, and `generate()` for every component compiled in
|
|
656
|
+
* this run — a same-file child is generated before its parent, so its entry
|
|
657
|
+
* is present by the time `loopRowChildPropOverrides` looks. Kept out of
|
|
658
|
+
* `ChildComponentShape` on purpose: that struct's presence/absence already
|
|
659
|
+
* drives rest-bag routing and map-typed-param baking, and widening WHEN it
|
|
660
|
+
* exists would change emission for every same-file child in the corpus.
|
|
661
|
+
* This map has exactly one reader.
|
|
662
|
+
*/
|
|
663
|
+
private childDerivedFieldDeps: Map<string, Map<string, ReadonlySet<string>>> = new Map()
|
|
664
|
+
|
|
665
|
+
/**
|
|
666
|
+
* component name -> (the prop name as WRITTEN AT THE JSX CALL SITE -> that
|
|
667
|
+
* component's own Go field name). #2457.
|
|
668
|
+
*
|
|
669
|
+
* `generateInputStruct` / `emitPropsDataFields` name a child's Go field from
|
|
670
|
+
* its LOCAL binding (`capitalizeFieldName(param.name)`), but a parent
|
|
671
|
+
* emitting a per-row override
|
|
672
|
+
* (`loopRowChildPropOverrides`) only knows the JSX attribute it wrote. For
|
|
673
|
+
* an un-aliased prop those are the same string, so this map is an identity
|
|
674
|
+
* for every existing shape and every currently-passing fixture stays
|
|
675
|
+
* byte-identical. For an ALIASED destructure (`{ n: count }`) they differ
|
|
676
|
+
* (`"N"` vs `Count`) — `bf.WithProps` documents unknown-field pairs as a
|
|
677
|
+
* silent passthrough, so emitting the JSX name against a struct that has no
|
|
678
|
+
* such field drops the override with no diagnostic. Resolving through this
|
|
679
|
+
* map once, at the parent's emission site (the only place that has both
|
|
680
|
+
* the JSX name and the child's shape), replaces that silent drop with the
|
|
681
|
+
* correct field.
|
|
682
|
+
*
|
|
683
|
+
* Key = `p.sourceName ?? p.name` (identity for an un-aliased param, the JSX
|
|
684
|
+
* attribute name for an aliased one). Value = `capitalizeFieldName(p.name)`
|
|
685
|
+
* — the child's own field, from its LOCAL binding, same as
|
|
686
|
+
* `generateInputStruct` uses.
|
|
687
|
+
*
|
|
688
|
+
* Populated from the same two doors as `childDerivedFieldDeps` — inside
|
|
689
|
+
* `recordDerivedFieldDeps`, unconditionally (before that method's own
|
|
690
|
+
* `deps.size > 0` gate), so every component gets an entry here regardless
|
|
691
|
+
* of whether it has a derived field. `recordRepropsSpec` (the other nearby
|
|
692
|
+
* populator) returns early for components with no derived field or an
|
|
693
|
+
* ineligible shape — this map must NOT inherit either gate, since an
|
|
694
|
+
* aliased child with no derived field at all is exactly the #2457 shape.
|
|
695
|
+
*/
|
|
696
|
+
private childPropFieldNames: Map<string, Map<string, string>> = new Map()
|
|
697
|
+
|
|
698
|
+
/**
|
|
699
|
+
* Components a `bf.RegisterReprops` rebuilder CAN be emitted for
|
|
700
|
+
* (`recordRepropsSpec`, #2448), with everything needed to emit it. A parent
|
|
701
|
+
* overriding a prop that feeds one of the child's derived fields emits
|
|
702
|
+
* `bf_reprops` when the child is in here, and falls back to the BF101
|
|
703
|
+
* refusal when it is not — the rebuilder is declined for shapes whose Input
|
|
704
|
+
* can't be reconstructed from Props, and emitting the call anyway would fail
|
|
705
|
+
* at template execute time with "no props rebuilder registered".
|
|
706
|
+
*
|
|
707
|
+
* Populated from `generateTypes`, so a same-file child (the only kind a loop
|
|
708
|
+
* row may contain) is recorded before its parent renders.
|
|
709
|
+
*/
|
|
710
|
+
private childRepropsReady: Map<string, RepropsSpec> = new Map()
|
|
711
|
+
|
|
712
|
+
/**
|
|
713
|
+
* child component name -> the component whose type block carries its
|
|
714
|
+
* rebuilder registration. First parent to need the child wins, so two
|
|
715
|
+
* parents overriding the same child don't both emit an `init()` for it.
|
|
716
|
+
*
|
|
717
|
+
* An assignment, not a queue: `generateTypes` is called more than once for
|
|
718
|
+
* the same component (the conformance harness re-generates the entry's types
|
|
719
|
+
* after `compileJSX` already did), and a queue drained by the first call
|
|
720
|
+
* would leave the second call's output — the one that actually gets used —
|
|
721
|
+
* missing the registration.
|
|
722
|
+
*/
|
|
723
|
+
private repropsOwner: Map<string, string> = new Map()
|
|
724
|
+
|
|
725
|
+
/**
|
|
726
|
+
* Record which of `ir`'s constructor-computed fields derive from one of its
|
|
727
|
+
* own input props (#2448). See `childDerivedFieldDeps`.
|
|
728
|
+
*
|
|
729
|
+
* BOTH constructor-evaluated declaration forms count, because
|
|
730
|
+
* `generateNewPropsFunction` bakes both into the struct once:
|
|
731
|
+
* a `createMemo` BODY (`createMemo(() => props.n * 2)` → `Dbl: in.N * 2`)
|
|
732
|
+
* and a `createSignal` INITIAL VALUE (`createSignal(props.n)` → `Dbl: in.N`).
|
|
733
|
+
* `bf_with_props` re-runs neither, so a per-row override of `n` leaves
|
|
734
|
+
* either one holding the shared instance's one-shot value.
|
|
735
|
+
*
|
|
736
|
+
* A declaration whose Go field name collides with a prop's own field name is
|
|
737
|
+
* a same-named "shadow" (`size = createMemo(() => props.size ?? 'icon')`) —
|
|
738
|
+
* `generateNewPropsFunction` folds it into the PROP's passthrough field
|
|
739
|
+
* rather than emitting a separate derived one, so a per-row override of
|
|
740
|
+
* that prop lands on the same field and nothing goes stale. A declaration
|
|
741
|
+
* with no structurally-resolvable `parsed` initializer is skipped too: this
|
|
742
|
+
* map is best-effort, and its absence costs a missed refusal (today's
|
|
743
|
+
* behaviour), never a wrong one.
|
|
744
|
+
*
|
|
745
|
+
* Dependencies are keyed by the prop's CANONICAL source name
|
|
746
|
+
* (`ParamInfo.sourceName ?? name`), not the child's local binding. An
|
|
747
|
+
* ALIASED destructure (`function Badge({ n: count }: { n: number })`) reads
|
|
748
|
+
* `count` in the memo body, but the only name the parent knows is the JSX
|
|
749
|
+
* attribute `n` — `loopRowChildPropOverrides` looks THAT up in
|
|
750
|
+
* `childPropFieldNames` before checking this map. Keying on the local name
|
|
751
|
+
* would file the dependency under `Count`, the lookup would miss, and the
|
|
752
|
+
* refusal would silently not fire on exactly the shape it exists for.
|
|
753
|
+
*
|
|
754
|
+
* This canonicalization used to cover the DERIVED case only — an aliased
|
|
755
|
+
* prop with no derived field still had the parent emitting `"N" .N`
|
|
756
|
+
* against a child whose field is `Count`, silently passed over by
|
|
757
|
+
* `bf.WithProps` (#2457). `childPropFieldNames`, populated a few lines
|
|
758
|
+
* below in this same method, closes that residual gap by resolving every
|
|
759
|
+
* prop's field name at the parent's emission site, not just the ones a
|
|
760
|
+
* derived field depends on.
|
|
761
|
+
*/
|
|
762
|
+
private recordDerivedFieldDeps(
|
|
763
|
+
ir: Pick<ComponentIR, 'metadata'>,
|
|
764
|
+
paramNames: ReadonlySet<string>,
|
|
765
|
+
): void {
|
|
766
|
+
const name = ir.metadata.componentName
|
|
767
|
+
if (!name) return
|
|
768
|
+
// Local binding → the prop name the PARENT writes at the call site.
|
|
769
|
+
// Identity for every un-aliased param (`sourceName` is set only on a
|
|
770
|
+
// renaming destructure).
|
|
771
|
+
const sourceOf = new Map(
|
|
772
|
+
(ir.metadata.propsParams ?? []).map(p => [p.name, p.sourceName ?? p.name]),
|
|
773
|
+
)
|
|
774
|
+
const canonical = (local: string): string => capitalizeFieldName(sourceOf.get(local) ?? local)
|
|
775
|
+
const propFieldNames = new Set([...paramNames].map(canonical))
|
|
776
|
+
// #2457: record the JSX-attribute-name -> child's-own-field-name map for
|
|
777
|
+
// EVERY component, unconditionally — see `childPropFieldNames`'s
|
|
778
|
+
// docstring for why this can't share `recordRepropsSpec`'s gates.
|
|
779
|
+
const fieldNames = new Map<string, string>()
|
|
780
|
+
for (const p of ir.metadata.propsParams ?? []) {
|
|
781
|
+
fieldNames.set(p.sourceName ?? p.name, capitalizeFieldName(p.name))
|
|
782
|
+
}
|
|
783
|
+
this.childPropFieldNames.set(name, fieldNames)
|
|
784
|
+
const deps = new Map<string, ReadonlySet<string>>()
|
|
785
|
+
const ctorInits: { field: string; init: ParsedExpr | undefined }[] = [
|
|
786
|
+
...(ir.metadata.memos ?? []).map(m => ({ field: m.name, init: m.parsed })),
|
|
787
|
+
...(ir.metadata.signals ?? []).map(s => ({ field: s.getter, init: s.parsed })),
|
|
788
|
+
]
|
|
789
|
+
for (const { field, init } of ctorInits) {
|
|
790
|
+
if (propFieldNames.has(capitalizeFieldName(field))) continue
|
|
791
|
+
if (!init) continue
|
|
792
|
+
const read = collectPropsReadByCtorInit(
|
|
793
|
+
init,
|
|
794
|
+
ir.metadata.propsObjectName ?? null,
|
|
795
|
+
paramNames,
|
|
796
|
+
)
|
|
797
|
+
if (read.size === 0) continue
|
|
798
|
+
deps.set(field, new Set([...read].map(canonical)))
|
|
799
|
+
}
|
|
800
|
+
if (deps.size > 0) this.childDerivedFieldDeps.set(name, deps)
|
|
801
|
+
}
|
|
802
|
+
|
|
621
803
|
registerChildComponentShape(ir: Pick<ComponentIR, 'metadata'>): void {
|
|
622
804
|
const name = ir.metadata.componentName
|
|
623
805
|
if (!name) return
|
|
624
|
-
|
|
806
|
+
// Both sets on `ChildComponentShape` are looked up by a PARENT against the
|
|
807
|
+
// name it wrote at the JSX call site, so they are keyed by
|
|
808
|
+
// `sourceName ?? name` — identity for an un-aliased param, the original
|
|
809
|
+
// property name for an aliased destructure (`{ n: count }` → `n`). Keying
|
|
810
|
+
// them by the local binding meant an aliased prop looked undeclared to
|
|
811
|
+
// `emitChildField` (misrouted into the rest bag) and to
|
|
812
|
+
// `loopRowChildPropOverrides` (skipped as rest-bag-only) — the same
|
|
813
|
+
// wrong-name-at-a-parent-side-lookup bug as #2457, one function over.
|
|
814
|
+
const paramNames = new Set((ir.metadata.propsParams ?? []).map(p => p.sourceName ?? p.name))
|
|
625
815
|
const restPropsName = ir.metadata.restPropsName ?? null
|
|
626
816
|
const restBagField = restPropsName ? capitalizeFieldName(restPropsName) : null
|
|
627
817
|
// Optional object/named-interface params lower to `map[string]interface{}`
|
|
@@ -635,9 +825,16 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
635
825
|
(p.type.kind === 'object' ||
|
|
636
826
|
(p.type.kind === 'interface' && !!p.type.raw)),
|
|
637
827
|
)
|
|
638
|
-
.map(p => p.name),
|
|
828
|
+
.map(p => p.sourceName ?? p.name),
|
|
639
829
|
)
|
|
640
830
|
this.childComponentShapes.set(name, { paramNames, restBagField, mapTypedParamNames })
|
|
831
|
+
// NOT `paramNames`: `recordDerivedFieldDeps` forwards this set to
|
|
832
|
+
// `collectPropsReadByCtorInit`, which in destructured mode matches BARE
|
|
833
|
+
// IDENTIFIERS in the memo/signal body — those are the LOCAL bindings
|
|
834
|
+
// (`count`), not the call-site names. Handing it the canonical set would
|
|
835
|
+
// make an aliased child's derived field look dependency-free and silently
|
|
836
|
+
// un-refuse / un-rebuild it.
|
|
837
|
+
this.recordDerivedFieldDeps(ir, new Set((ir.metadata.propsParams ?? []).map(p => p.name)))
|
|
641
838
|
// Contexts this child consumes, so a parent `<Ctx.Provider value>` wrapping
|
|
642
839
|
// it can set the matching field on the child's slot input.
|
|
643
840
|
this.childContextConsumers.set(name, collectContextConsumers(ir.metadata))
|
|
@@ -761,9 +958,172 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
761
958
|
|
|
762
959
|
this.generateNewPropsFunction(lines, ir, componentName, nestedComponents, spreadSlots, propTypeOverrides)
|
|
763
960
|
|
|
961
|
+
this.recordRepropsSpec(ir, componentName, nestedComponents, spreadSlots)
|
|
962
|
+
this.emitOwnedReprops(lines, componentName)
|
|
963
|
+
|
|
764
964
|
return this.composeFileHeader(lines)
|
|
765
965
|
}
|
|
766
966
|
|
|
967
|
+
/**
|
|
968
|
+
* #2448: emit a props REBUILDER for a component whose constructor derives a
|
|
969
|
+
* field from one of its own input props, and register it from `init()`.
|
|
970
|
+
*
|
|
971
|
+
* `bf_with_props` patches fields on the child's already-constructed shared
|
|
972
|
+
* instance; it cannot re-run `New<Child>Props`, where a `createMemo` body and
|
|
973
|
+
* a `createSignal` initial value are both baked. This closure reconstructs
|
|
974
|
+
* the Input from the base Props, folds the row's overrides in, and re-runs
|
|
975
|
+
* the real constructor — so every derived field recomputes per row. The
|
|
976
|
+
* parent's call site then emits `bf_reprops` instead of `bf_with_props`
|
|
977
|
+
* (`loopRowChildPropOverrides`).
|
|
978
|
+
*
|
|
979
|
+
* Two invariants the emitted code depends on:
|
|
980
|
+
*
|
|
981
|
+
* - **Input is recoverable from Props.** `emitPropsDataFields` and
|
|
982
|
+
* `generateInputStruct` emit each props param with the SAME field name and
|
|
983
|
+
* the SAME `resolvePropGoType`, so `in.<F> = b.<F>` is exact. A prop read
|
|
984
|
+
* only by a memo still gets its passthrough field, so nothing is lost.
|
|
985
|
+
* Shapes that add Input fields with no Props counterpart (a rest bag, a
|
|
986
|
+
* spread slot, context consumers, nested child Inputs) are declined —
|
|
987
|
+
* `eligible` below — and the parent falls back to today's BF101.
|
|
988
|
+
* - **Identity is carried, never re-derived.** `New<Child>Props` mints a
|
|
989
|
+
* random ScopeID when handed an empty one, so re-running it naively would
|
|
990
|
+
* give every row its own scope and break hydration. ScopeID/BfParent/
|
|
991
|
+
* BfMount come from the base, and the Props-only fields (Scripts,
|
|
992
|
+
* BfIsRoot/BfIsChild/BfDataKey) are reapplied after the constructor.
|
|
993
|
+
*
|
|
994
|
+
* The override switch is keyed by the child's own Input field
|
|
995
|
+
* (`ParamInfo.name`) on both the case label and the assignment target. That
|
|
996
|
+
* used to differ under an aliased destructure — the case label carried the
|
|
997
|
+
* name the PARENT wrote (the JSX attribute, `ParamInfo.sourceName ?? name`)
|
|
998
|
+
* while the assignment targeted the child's own field, so this switch was
|
|
999
|
+
* where those two sides were reconciled. #2457 moved that reconciliation to
|
|
1000
|
+
* the PARENT (`loopRowChildPropOverrides`, via `childPropFieldNames`): the
|
|
1001
|
+
* parent now emits the child's own field name at the call site, so by the
|
|
1002
|
+
* time a `bf_reprops` pipeline reaches this switch both sides already agree
|
|
1003
|
+
* and there's exactly one place — the parent's emission — where the naming
|
|
1004
|
+
* gets reconciled, instead of two.
|
|
1005
|
+
*/
|
|
1006
|
+
private recordRepropsSpec(
|
|
1007
|
+
ir: ComponentIR,
|
|
1008
|
+
componentName: string,
|
|
1009
|
+
nestedComponents: NestedComponentInfo[],
|
|
1010
|
+
spreadSlots: SpreadSlotInfo[],
|
|
1011
|
+
): void {
|
|
1012
|
+
if (!this.childDerivedFieldDeps.has(componentName)) return
|
|
1013
|
+
|
|
1014
|
+
const nestedArrayFields = new Set(nestedComponents.map(n => `${n.name}s`))
|
|
1015
|
+
const params = (ir.metadata.propsParams ?? []).filter(
|
|
1016
|
+
p => !nestedArrayFields.has(capitalizeFieldName(p.name)),
|
|
1017
|
+
)
|
|
1018
|
+
const takenInput = new Set((ir.metadata.propsParams ?? []).map(p => capitalizeFieldName(p.name)))
|
|
1019
|
+
const eligible =
|
|
1020
|
+
nestedComponents.every(n => n.isDynamic && !n.isPropDerived) &&
|
|
1021
|
+
spreadSlots.length === 0 &&
|
|
1022
|
+
!ir.metadata.restPropsName &&
|
|
1023
|
+
this.nonCollidingContextConsumers(takenInput).length === 0
|
|
1024
|
+
if (!eligible) return
|
|
1025
|
+
|
|
1026
|
+
this.childRepropsReady.set(componentName, {
|
|
1027
|
+
params: params.map(p => capitalizeFieldName(p.name)),
|
|
1028
|
+
usesSearchParams: this.usesSearchParams(ir),
|
|
1029
|
+
})
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
/**
|
|
1033
|
+
* Emit the rebuilders assigned to `owner` — the children its own render
|
|
1034
|
+
* asked for (`loopRowChildPropOverrides` → `repropsOwner`).
|
|
1035
|
+
*
|
|
1036
|
+
* Emitted from the PARENT rather than the child on purpose. The prop-seeded
|
|
1037
|
+
* signal (`createSignal(props.initial)`) is one of the most common shapes in
|
|
1038
|
+
* the corpus, so emitting a rebuilder for every component that merely HAS a
|
|
1039
|
+
* derived field added ~250 lines of never-called Go to each integration's
|
|
1040
|
+
* generated file. Only a parent knows whether a child is actually overridden
|
|
1041
|
+
* per row inside a composite loop row, and it knows before its own type block
|
|
1042
|
+
* is built — `generate()` renders the template first. Every component's type
|
|
1043
|
+
* block lands in the same package (`combineGoTypes`, `build.ts`), so the
|
|
1044
|
+
* child's Input/Props types are in scope from here.
|
|
1045
|
+
*
|
|
1046
|
+
* Idempotent: driven by the recorded assignment, so re-generating the same
|
|
1047
|
+
* component's types produces the same block.
|
|
1048
|
+
*/
|
|
1049
|
+
private emitOwnedReprops(lines: string[], owner: string): void {
|
|
1050
|
+
for (const [childName, ownerName] of this.repropsOwner) {
|
|
1051
|
+
if (ownerName !== owner) continue
|
|
1052
|
+
const spec = this.childRepropsReady.get(childName)
|
|
1053
|
+
if (!spec) continue
|
|
1054
|
+
this.emitRepropsRegistration(lines, childName, spec)
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
private emitRepropsRegistration(
|
|
1059
|
+
lines: string[],
|
|
1060
|
+
componentName: string,
|
|
1061
|
+
spec: RepropsSpec,
|
|
1062
|
+
): void {
|
|
1063
|
+
const { params, usesSearchParams } = spec
|
|
1064
|
+
const inputTypeName = `${componentName}Input`
|
|
1065
|
+
const q = JSON.stringify(componentName)
|
|
1066
|
+
|
|
1067
|
+
lines.push(`// ${componentName} computes at least one field from an input prop when its`)
|
|
1068
|
+
lines.push('// props are constructed, so a per-row override inside a composite loop row')
|
|
1069
|
+
lines.push('// cannot be applied by patching fields — the derived field would keep the')
|
|
1070
|
+
lines.push('// shared instance\'s one-shot value on every row (#2448). This rebuilder')
|
|
1071
|
+
lines.push(`// re-runs New${componentName}Props with the row's overrides folded into the`)
|
|
1072
|
+
lines.push('// Input; the parent calls it through bf_reprops.')
|
|
1073
|
+
lines.push('func init() {')
|
|
1074
|
+
lines.push(`\tbf.RegisterReprops(${q}, func(base interface{}, kv ...interface{}) (interface{}, error) {`)
|
|
1075
|
+
lines.push(`\t\tb, ok := base.(${componentName}Props)`)
|
|
1076
|
+
lines.push('\t\tif !ok {')
|
|
1077
|
+
lines.push(`\t\t\treturn nil, bf.RepropsTypeError(${q}, base)`)
|
|
1078
|
+
lines.push('\t\t}')
|
|
1079
|
+
lines.push(`\t\tin := ${inputTypeName}{`)
|
|
1080
|
+
// Identity comes from the base instance — re-deriving it would re-mint the
|
|
1081
|
+
// ScopeID and break hydration.
|
|
1082
|
+
lines.push('\t\t\tScopeID: b.ScopeID,')
|
|
1083
|
+
lines.push('\t\t\tBfParent: b.BfParent,')
|
|
1084
|
+
lines.push('\t\t\tBfMount: b.BfMount,')
|
|
1085
|
+
if (usesSearchParams) lines.push('\t\t\tSearchParams: b.SearchParams,')
|
|
1086
|
+
for (const field of params) {
|
|
1087
|
+
lines.push(`\t\t\t${field}: b.${field},`)
|
|
1088
|
+
}
|
|
1089
|
+
lines.push('\t\t}')
|
|
1090
|
+
lines.push('\t\tfor i := 0; i < len(kv); i += 2 {')
|
|
1091
|
+
lines.push('\t\t\tname, _ := kv[i].(string)')
|
|
1092
|
+
lines.push('\t\t\tvar err error')
|
|
1093
|
+
lines.push('\t\t\tswitch name {')
|
|
1094
|
+
for (const field of params) {
|
|
1095
|
+
// Case label and assignment target are the same name: the child's own
|
|
1096
|
+
// Input field. The parent (`loopRowChildPropOverrides`, via
|
|
1097
|
+
// `childPropFieldNames`, #2457) already resolved the JSX attribute to
|
|
1098
|
+
// this field before emitting the call, so there is nothing left to
|
|
1099
|
+
// reconcile here.
|
|
1100
|
+
lines.push(`\t\t\tcase ${JSON.stringify(field)}:`)
|
|
1101
|
+
lines.push(`\t\t\t\terr = bf.RepropsAssign(${q}, ${JSON.stringify(field)}, &in.${field}, kv[i+1])`)
|
|
1102
|
+
}
|
|
1103
|
+
// No silent drop. `bf_with_props` passes an unknown field through because
|
|
1104
|
+
// a rest-bag prop legitimately has no named field — but a rebuilder is
|
|
1105
|
+
// only emitted for components with no rest bag, and there is a case for
|
|
1106
|
+
// every prop the parent can override, so an unknown name here means the
|
|
1107
|
+
// override would go unapplied.
|
|
1108
|
+
lines.push('\t\t\tdefault:')
|
|
1109
|
+
lines.push(`\t\t\t\terr = bf.RepropsUnknownFieldError(${q}, name)`)
|
|
1110
|
+
lines.push('\t\t\t}')
|
|
1111
|
+
lines.push('\t\t\tif err != nil {')
|
|
1112
|
+
lines.push('\t\t\t\treturn nil, err')
|
|
1113
|
+
lines.push('\t\t\t}')
|
|
1114
|
+
lines.push('\t\t}')
|
|
1115
|
+
lines.push(`\t\tp := New${componentName}Props(in)`)
|
|
1116
|
+
lines.push('\t\t// Props-only state, absent from Input and therefore not rebuilt.')
|
|
1117
|
+
lines.push('\t\tp.Scripts = b.Scripts')
|
|
1118
|
+
lines.push('\t\tp.BfIsRoot = b.BfIsRoot')
|
|
1119
|
+
lines.push('\t\tp.BfIsChild = b.BfIsChild')
|
|
1120
|
+
lines.push('\t\tp.BfDataKey = b.BfDataKey')
|
|
1121
|
+
lines.push('\t\treturn p, nil')
|
|
1122
|
+
lines.push('\t})')
|
|
1123
|
+
lines.push('}')
|
|
1124
|
+
lines.push('')
|
|
1125
|
+
}
|
|
1126
|
+
|
|
767
1127
|
/** Convert a TS type definition to Go: object types → structs, string-literal unions → a `string` alias. */
|
|
768
1128
|
private typeDefinitionToGo(td: TypeDefinition): string | null {
|
|
769
1129
|
// A string-literal union (`type Filter = 'all' | 'active'`) carries no
|
|
@@ -1506,7 +1866,15 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
1506
1866
|
// A hyphenated attr (`aria-label`) can't be a Go field and, with no rest
|
|
1507
1867
|
// bag to route it into, has nowhere to go — skip over emitting invalid Go.
|
|
1508
1868
|
if (jsxName.includes('-')) return
|
|
1509
|
-
|
|
1869
|
+
// Same resolution as `loopRowChildPropOverrides` (#2457): the child's
|
|
1870
|
+
// own Go field, which differs from the JSX attribute under an aliased
|
|
1871
|
+
// destructure (`{ n: count }` → field `Count`). Emitting the attribute
|
|
1872
|
+
// name here put an unknown field in a Go struct literal — a build
|
|
1873
|
+
// error rather than a silent drop, but wrong either way. Falls back to
|
|
1874
|
+
// the capitalized attribute for a child this run never registered.
|
|
1875
|
+
const fieldName =
|
|
1876
|
+
this.childPropFieldNames.get(child.name)?.get(jsxName) ?? capitalizeFieldName(jsxName)
|
|
1877
|
+
lines.push(`\t\t\t${fieldName}: ${goValue},`)
|
|
1510
1878
|
}
|
|
1511
1879
|
for (const prop of child.props) {
|
|
1512
1880
|
switch (prop.value.kind) {
|
|
@@ -4993,6 +5361,186 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
4993
5361
|
)
|
|
4994
5362
|
}
|
|
4995
5363
|
|
|
5364
|
+
/**
|
|
5365
|
+
* #2445: per-row Go pipeline arguments for a child component nested inside
|
|
5366
|
+
* a COMPOSITE loop row (row root is a plain element — `<li><Badge
|
|
5367
|
+
* text={row.label}/></li>` — as opposed to the wrapper-slice case where the
|
|
5368
|
+
* loop body IS the component). `collectStaticChildInstancesRecursive`
|
|
5369
|
+
* registers such a child as an ordinary once-per-slot instance
|
|
5370
|
+
* (`$.<Name>SlotN`), built ONCE outside `{{range}}` by
|
|
5371
|
+
* `emitStaticChildInstances` — correct for props that don't depend on the
|
|
5372
|
+
* row, but stale for one that does (`text={row.label}` reads the same
|
|
5373
|
+
* value on every row). A loop-dependent prop is therefore re-applied per
|
|
5374
|
+
* row, at template-execution time, via the `bf_with_props` runtime helper
|
|
5375
|
+
* (the props-argument sibling of `bf_with_children`, which already does
|
|
5376
|
+
* this for per-row JSX children on the same shared instance).
|
|
5377
|
+
*
|
|
5378
|
+
* A prop with no loop-bound free identifier (`IRProp.freeIdentifiers`, IR-
|
|
5379
|
+
* build-time computed, #1267) is left on the constructor path — it's
|
|
5380
|
+
* already correct there, and skipping it keeps every currently-passing
|
|
5381
|
+
* fixture's emitted template byte-identical (no fixture in the corpus
|
|
5382
|
+
* reaches this method with a loop-dependent prop today).
|
|
5383
|
+
*
|
|
5384
|
+
* #2448 — `bf_with_props` overrides fields on the ALREADY-CONSTRUCTED
|
|
5385
|
+
* shared instance; it does not re-run `New<Child>Props`. A field the child
|
|
5386
|
+
* derives FROM the overridden prop at construction time (a memo body or a
|
|
5387
|
+
* signal's initial value) would keep whatever the one-shot constructor
|
|
5388
|
+
* computed and never update per row. Two outcomes, decided per child:
|
|
5389
|
+
*
|
|
5390
|
+
* - The child has a generated props rebuilder (`childRepropsReady`): the
|
|
5391
|
+
* caller emits `bf_reprops` instead, which re-runs the real constructor
|
|
5392
|
+
* per row so every derived field recomputes. Nothing is refused.
|
|
5393
|
+
* - It does not (its Input can't be reconstructed from Props — see
|
|
5394
|
+
* `generateRepropsRegistration`'s eligibility gate): BF101 + `continue`,
|
|
5395
|
+
* exactly like the unsupported-expression refusal a few lines below.
|
|
5396
|
+
* Refusing beats emitting silently-stale output.
|
|
5397
|
+
*
|
|
5398
|
+
* #2457 — the `"Field"` half of each argument pair is the child's OWN Go
|
|
5399
|
+
* field name (resolved through `childPropFieldNames`), not the JSX
|
|
5400
|
+
* attribute name. For an un-aliased prop those are the same string; for an
|
|
5401
|
+
* aliased destructure (`{ n: count }`) the child's field is `Count` while
|
|
5402
|
+
* the JSX attribute is `n`, and emitting the attribute name left
|
|
5403
|
+
* `bf.WithProps`/`bf.RepropsAssign` with a name the struct has no field
|
|
5404
|
+
* for — silently dropped by the former, an unknown-field error from the
|
|
5405
|
+
* latter. Resolving it here, once, means both helpers only ever see a name
|
|
5406
|
+
* the struct actually has.
|
|
5407
|
+
*
|
|
5408
|
+
* Returns the space-joined `"Field" value …` argument list plus the helper
|
|
5409
|
+
* the caller must wrap it in, or null when nothing needs overriding (caller
|
|
5410
|
+
* keeps the bare `$.<Name>SlotN` reference).
|
|
5411
|
+
*/
|
|
5412
|
+
private loopRowChildPropOverrides(
|
|
5413
|
+
comp: IRComponent,
|
|
5414
|
+
): { args: string; helper: 'bf_with_props' | 'bf_reprops' } | null {
|
|
5415
|
+
const childShape = this.childComponentShapes.get(comp.name)
|
|
5416
|
+
const args: string[] = []
|
|
5417
|
+
// Set by the derived-field check below when at least one overridden prop
|
|
5418
|
+
// feeds a constructor-derived field AND the child can rebuild itself.
|
|
5419
|
+
let needsRebuild = false
|
|
5420
|
+
for (const prop of comp.props) {
|
|
5421
|
+
// Client-only props never reach SSR output; `key`/`children` aren't
|
|
5422
|
+
// Props-struct fields; event handlers have no Go field (same
|
|
5423
|
+
// `isEventHandler` predicate `jsx-to-ir.ts` uses for component props);
|
|
5424
|
+
// a hyphenated name can't be a Go field (same guard as `emitChildField`).
|
|
5425
|
+
if (prop.clientOnly) continue
|
|
5426
|
+
if (prop.name === 'key' || prop.name === 'children') continue
|
|
5427
|
+
if (prop.name.startsWith('on') && prop.name.length > 2) continue
|
|
5428
|
+
if (prop.name.includes('-')) continue
|
|
5429
|
+
// A prop that routes into the child's rest bag (`emitChildField`'s
|
|
5430
|
+
// same routing rule) has no named Go field to override — `bf_with_props`
|
|
5431
|
+
// would silently no-op the pair via its unknown-field passthrough.
|
|
5432
|
+
// Leave it on the constructor-only path (unchanged from before this
|
|
5433
|
+
// fix) rather than emit a pipeline argument that can never land.
|
|
5434
|
+
if (childShape?.restBagField && !childShape.paramNames.has(prop.name)) continue
|
|
5435
|
+
// `literal` / `boolean-shorthand` / `boolean-attr` carry no runtime
|
|
5436
|
+
// expression to re-evaluate per row; `spread` / `jsx-children` are
|
|
5437
|
+
// handled elsewhere (`emitSpreadBagInits`, `queueLoopBodyChildrenDefine`).
|
|
5438
|
+
if (prop.value.kind !== 'expression') continue
|
|
5439
|
+
const free = prop.freeIdentifiers
|
|
5440
|
+
if (!free || ![...free].some(name => this.isLoopShadowedName(name))) continue
|
|
5441
|
+
// #2448: does this prop feed a field the child's CONSTRUCTOR derives?
|
|
5442
|
+
// If so, patching fields on the shared instance leaves that field at the
|
|
5443
|
+
// one-shot value on every row. Re-run the constructor per row when the
|
|
5444
|
+
// child has a rebuilder; refuse when it doesn't.
|
|
5445
|
+
{
|
|
5446
|
+
const derived = this.childDerivedFieldDeps.get(comp.name)
|
|
5447
|
+
const overriddenField = capitalizeFieldName(prop.name)
|
|
5448
|
+
const staleField = derived
|
|
5449
|
+
? [...derived].find(([, deps]) => deps.has(overriddenField))?.[0]
|
|
5450
|
+
: undefined
|
|
5451
|
+
if (staleField && !this.childRepropsReady.has(comp.name)) {
|
|
5452
|
+
this.state.errors.push({
|
|
5453
|
+
code: 'BF101',
|
|
5454
|
+
severity: 'error',
|
|
5455
|
+
message: `Prop '${prop.name}' on <${comp.name}> nested inside a dynamic loop row is overridden per row, but <${comp.name}>'s '${staleField}' field is computed from '${prop.name}' when the shared instance is first constructed and won't recompute per row — it would keep the first row's value on every row.`,
|
|
5456
|
+
loc: prop.loc,
|
|
5457
|
+
suggestion: {
|
|
5458
|
+
message: `Mark this loop position '@client' to render <${comp.name}> client-side, or compute '${staleField}' in the parent and pass it to <${comp.name}> as a plain prop instead of deriving it inside <${comp.name}>.`,
|
|
5459
|
+
},
|
|
5460
|
+
})
|
|
5461
|
+
continue
|
|
5462
|
+
}
|
|
5463
|
+
if (staleField) {
|
|
5464
|
+
needsRebuild = true
|
|
5465
|
+
// The rebuilder is emitted into THIS component's type block, not the
|
|
5466
|
+
// child's — only here do we know it is actually needed. First parent
|
|
5467
|
+
// to claim a child owns the registration, so two parents overriding
|
|
5468
|
+
// the same child don't both emit an `init()` for it.
|
|
5469
|
+
if (!this.repropsOwner.has(comp.name)) {
|
|
5470
|
+
this.repropsOwner.set(comp.name, this.state.componentName)
|
|
5471
|
+
}
|
|
5472
|
+
}
|
|
5473
|
+
}
|
|
5474
|
+
const exprOut: { parsed?: ParsedExpr } = {}
|
|
5475
|
+
const errorCountBefore = this.state.errors.length
|
|
5476
|
+
let go = this.convertExpressionToGo(prop.value.expr, exprOut, prop.value.parsed)
|
|
5477
|
+
if (this.state.errors.length > errorCountBefore) {
|
|
5478
|
+
// `convertExpressionToGo` already pushed its own BF101 for an
|
|
5479
|
+
// unsupported expression and returned the `""` sentinel — using it
|
|
5480
|
+
// here would silently clobber the field with an empty string (or,
|
|
5481
|
+
// for a non-string field, fail at template EXECUTE time instead of
|
|
5482
|
+
// this compile time). Leave the prop on the constructor path; the
|
|
5483
|
+
// reported error already surfaces the real problem. `loc` defaults
|
|
5484
|
+
// to `convertExpressionToGo`'s own `this.makeLoc()` placeholder —
|
|
5485
|
+
// repoint the newly-pushed error(s) at the prop's real source
|
|
5486
|
+
// location, same as the fragment-refusal error below.
|
|
5487
|
+
for (let i = errorCountBefore; i < this.state.errors.length; i++) {
|
|
5488
|
+
this.state.errors[i].loc = prop.loc
|
|
5489
|
+
}
|
|
5490
|
+
continue
|
|
5491
|
+
}
|
|
5492
|
+
// A ternary / single-interpolation template literal with STRING-typed
|
|
5493
|
+
// branches (`row.on ? "yes" : "no"`, `${row.x}` alone) parses to a
|
|
5494
|
+
// ParsedExpr `template-literal` whose sole part is non-string —
|
|
5495
|
+
// `templateLiteral()` wraps that one dynamic part's already-bare
|
|
5496
|
+
// pipeline value (e.g. `(bf_ternary ...)`, #2335) in a bare `{{...}}`
|
|
5497
|
+
// shell meant for TEXT-position embedding. A multi-part template
|
|
5498
|
+
// literal (mixed literal text and interpolation) has no such reduction
|
|
5499
|
+
// and stays refused below. Unwrap the single-part case back to the
|
|
5500
|
+
// bare pipeline value this call site (a function ARGUMENT position,
|
|
5501
|
+
// not a text position) needs.
|
|
5502
|
+
const singlePartTemplateLiteral =
|
|
5503
|
+
exprOut.parsed?.kind === 'template-literal' &&
|
|
5504
|
+
exprOut.parsed.parts.length === 1 &&
|
|
5505
|
+
exprOut.parsed.parts[0].type !== 'string' &&
|
|
5506
|
+
go.startsWith('{{') &&
|
|
5507
|
+
go.endsWith('}}')
|
|
5508
|
+
if (singlePartTemplateLiteral) {
|
|
5509
|
+
go = go.slice(2, -2)
|
|
5510
|
+
}
|
|
5511
|
+
// Skip the fragment re-check for the just-unwrapped single-part case —
|
|
5512
|
+
// `kind` is still (accurately) `template-literal`, which would
|
|
5513
|
+
// otherwise re-trip `isTemplateFragment`'s `kind === 'template-literal'`
|
|
5514
|
+
// branch on the ALREADY-unwrapped bare value.
|
|
5515
|
+
if (!singlePartTemplateLiteral && this.isTemplateFragment(go, exprOut.parsed?.kind)) {
|
|
5516
|
+
// A genuine multi-part `{{if}}...{{end}}`-shaped fragment can't be a
|
|
5517
|
+
// bare pipeline argument — refuse loudly rather than silently
|
|
5518
|
+
// dropping the prop (the #2445 bug was exactly a silent drop one
|
|
5519
|
+
// level up).
|
|
5520
|
+
this.state.errors.push({
|
|
5521
|
+
code: 'BF101',
|
|
5522
|
+
severity: 'error',
|
|
5523
|
+
message: `Prop '${prop.name}' on <${comp.name}> nested inside a dynamic loop row reads the row but can't be lowered to a Go template pipeline argument`,
|
|
5524
|
+
loc: prop.loc,
|
|
5525
|
+
})
|
|
5526
|
+
continue
|
|
5527
|
+
}
|
|
5528
|
+
// #2457: emit the CHILD's own Go field name, not the JSX attribute name.
|
|
5529
|
+
// They differ under an aliased destructure (`{ n: count }` → attribute
|
|
5530
|
+
// `n`, field `Count`); resolving through `childPropFieldNames` here —
|
|
5531
|
+
// the parent's emission site, the only place that has both the JSX
|
|
5532
|
+
// name and the child's shape — means `bf.WithProps`/`bf.RepropsAssign`
|
|
5533
|
+
// never see a name the struct doesn't have. Falls back to today's
|
|
5534
|
+
// capitalized-attribute behaviour for a cross-file child this run's
|
|
5535
|
+
// pre-pass never registered (`childPropFieldNames` has no entry).
|
|
5536
|
+
const fieldName =
|
|
5537
|
+
this.childPropFieldNames.get(comp.name)?.get(prop.name) ?? capitalizeFieldName(prop.name)
|
|
5538
|
+
args.push(`${JSON.stringify(fieldName)} ${wrapIfMultiToken(go)}`)
|
|
5539
|
+
}
|
|
5540
|
+
if (args.length === 0) return null
|
|
5541
|
+
return { args: args.join(' '), helper: needsRebuild ? 'bf_reprops' : 'bf_with_props' }
|
|
5542
|
+
}
|
|
5543
|
+
|
|
4996
5544
|
/**
|
|
4997
5545
|
* Resolve `IDENT['key']` / `IDENT["key"]` where `IDENT` is a module-scope
|
|
4998
5546
|
* object-literal const and the key is a string literal — a compile-time-static
|
|
@@ -5744,6 +6292,17 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
5744
6292
|
// nested loops with the same index var name from clobbering the outer entry
|
|
5745
6293
|
// on cleanup.
|
|
5746
6294
|
const addedLoopVars: string[] = []
|
|
6295
|
+
// A `.map()` callback preamble lowers to one `{{$cls := …}}` per-row
|
|
6296
|
+
// variable per declaration (#2447). Registering the names on
|
|
6297
|
+
// `loopVarRefCount` is what makes `identifier()` resolve each read as
|
|
6298
|
+
// `$cls`; without it the read falls through to `rootFieldRef` and emits
|
|
6299
|
+
// `$.Cls` — a PARENT-struct field, never populated, which rendered the
|
|
6300
|
+
// attribute empty on every row (the same hoisted-to-parent class of
|
|
6301
|
+
// defect as #2445).
|
|
6302
|
+
for (const d of loop.preamble?.declarations ?? []) {
|
|
6303
|
+
this.loopVarRefCount.set(d.name, (this.loopVarRefCount.get(d.name) ?? 0) + 1)
|
|
6304
|
+
addedLoopVars.push(d.name)
|
|
6305
|
+
}
|
|
5747
6306
|
let pushedBindingMap = false
|
|
5748
6307
|
if (supportableDestructure) {
|
|
5749
6308
|
// Bindings resolve against the synthetic `$__bf_item` range var; don't push
|
|
@@ -5777,6 +6336,12 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
5777
6336
|
)
|
|
5778
6337
|
this.loopWrapperStack.push(!!loop.childComponent)
|
|
5779
6338
|
this.loopKeyDepthStack.push(loop.depth)
|
|
6339
|
+
// Rendered inside the pushed loop scope so each initializer resolves
|
|
6340
|
+
// against the range item (`.Done`), in source order so a later
|
|
6341
|
+
// initializer sees an earlier `$` variable — same as the source block.
|
|
6342
|
+
const preambleAssignments = (loop.preamble?.declarations ?? [])
|
|
6343
|
+
.map(d => `{{$${d.name} := ${this.renderParsedExpr(d.valueParsed)}}}`)
|
|
6344
|
+
.join('')
|
|
5780
6345
|
const children = this.renderChildren(loop.children)
|
|
5781
6346
|
this.loopKeyDepthStack.pop()
|
|
5782
6347
|
this.loopWrapperStack.pop()
|
|
@@ -5838,10 +6403,13 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
5838
6403
|
filterCond = 'true'
|
|
5839
6404
|
}
|
|
5840
6405
|
|
|
5841
|
-
|
|
6406
|
+
// The preamble assignments sit INSIDE the `{{if}}`, matching JS
|
|
6407
|
+
// evaluation order: a `.filter(p).map(cb)` chain never runs `cb`'s
|
|
6408
|
+
// body for a filtered-out item.
|
|
6409
|
+
return `{{bfComment "loop:${loop.markerId}"}}{{range $${rangeIndex}, $${rangeValue} := ${goArray}}}{{if ${filterCond}}}${preambleAssignments}${itemMarker}${children}{{end}}{{end}}{{bfComment "/loop:${loop.markerId}"}}`
|
|
5842
6410
|
}
|
|
5843
6411
|
|
|
5844
|
-
return `{{bfComment "loop:${loop.markerId}"}}{{range $${rangeIndex}, $${rangeValue} := ${goArray}}}${itemMarker}${children}{{end}}{{bfComment "/loop:${loop.markerId}"}}`
|
|
6412
|
+
return `{{bfComment "loop:${loop.markerId}"}}{{range $${rangeIndex}, $${rangeValue} := ${goArray}}}${preambleAssignments}${itemMarker}${children}{{end}}{{bfComment "/loop:${loop.markerId}"}}`
|
|
5845
6413
|
}
|
|
5846
6414
|
|
|
5847
6415
|
/**
|
|
@@ -6013,17 +6581,33 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
6013
6581
|
} else if (this.inLoop && comp.slotId) {
|
|
6014
6582
|
// Non-wrapper loop (component nested inside an element item, #2130):
|
|
6015
6583
|
// the range iterates the REAL collection, so `.` is the raw datum and
|
|
6016
|
-
// carries none of the child's props. Call through the
|
|
6017
|
-
// once-per-slot instance via the root context (`$` — the
|
|
6018
|
-
// data, i.e. the parent's Props)
|
|
6019
|
-
//
|
|
6020
|
-
//
|
|
6584
|
+
// carries none of the child's props by default. Call through the
|
|
6585
|
+
// parent's once-per-slot instance via the root context (`$` — the
|
|
6586
|
+
// define's own data, i.e. the parent's Props). Two kinds of per-row
|
|
6587
|
+
// content have to reach that shared instance at template-execution
|
|
6588
|
+
// time: JSX children, injected via a loop-body children define
|
|
6589
|
+
// executed with the datum (`bf_with_children`), and any PROP that
|
|
6590
|
+
// reads the row (`text={row.label}`, #2445) — the instance is built
|
|
6591
|
+
// ONCE outside `{{range}}`, so a loop-dependent prop is stale there
|
|
6592
|
+
// and is reapplied per row via `bf_with_props`
|
|
6593
|
+
// (`loopRowChildPropOverrides`). The shared BASE instance (built once)
|
|
6594
|
+
// still carries identical child scope IDs across rows — the same
|
|
6021
6595
|
// contract the wrapper machinery's `bodyChildInstances` already uses.
|
|
6022
6596
|
const suffix = slotIdToFieldSuffix(comp.slotId)
|
|
6597
|
+
const overrides = this.loopRowChildPropOverrides(comp)
|
|
6023
6598
|
const loopBodyDefine = this.queueLoopBodyChildrenDefine(comp)
|
|
6599
|
+
// `bf_reprops` re-runs the child's constructor and so takes the
|
|
6600
|
+
// component name; `bf_with_props` patches fields and doesn't (#2448).
|
|
6601
|
+
// Either way the props helper stays INNER — `bf_with_children` applies
|
|
6602
|
+
// the row's children last, on the rebuilt value.
|
|
6603
|
+
const base = overrides
|
|
6604
|
+
? overrides.helper === 'bf_reprops'
|
|
6605
|
+
? `(bf_reprops ${JSON.stringify(comp.name)} $.${comp.name}${suffix} ${overrides.args})`
|
|
6606
|
+
: `(bf_with_props $.${comp.name}${suffix} ${overrides.args})`
|
|
6607
|
+
: `$.${comp.name}${suffix}`
|
|
6024
6608
|
templateCall = loopBodyDefine
|
|
6025
|
-
? `{{template "${comp.name}" (bf_with_children
|
|
6026
|
-
: `{{template "${comp.name}"
|
|
6609
|
+
? `{{template "${comp.name}" (bf_with_children ${base} (bf_tmpl "${loopBodyDefine}" .))}}`
|
|
6610
|
+
: `{{template "${comp.name}" ${base}}}`
|
|
6027
6611
|
} else if (this.inLoop) {
|
|
6028
6612
|
// Loop-nested component without a slotId: no parent field to route
|
|
6029
6613
|
// through — legacy passthrough of the current dot.
|