@barefootjs/go-template 0.31.4 → 0.31.6
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 +109 -8
- package/dist/adapter/go-template-adapter.d.ts.map +1 -1
- package/dist/adapter/index.js +47 -43
- package/dist/adapter/lib/compile-state.d.ts +6 -2
- package/dist/adapter/lib/compile-state.d.ts.map +1 -1
- package/dist/adapter/lib/types.d.ts +15 -0
- package/dist/adapter/lib/types.d.ts.map +1 -1
- package/dist/conformance-pins.d.ts.map +1 -1
- package/dist/index.js +61 -49
- package/dist/render-divergences.d.ts +9 -3
- package/dist/render-divergences.d.ts.map +1 -1
- package/dist/vite.js +167 -118
- package/package.json +5 -5
- package/src/adapter/analysis/component-tree.ts +1 -0
- package/src/adapter/go-template-adapter.ts +217 -76
- package/src/adapter/lib/compile-state.ts +6 -2
- package/src/adapter/lib/types.ts +15 -0
- package/src/conformance-pins.ts +21 -5
- package/src/render-divergences.ts +29 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@barefootjs/go-template",
|
|
3
|
-
"version": "0.31.
|
|
3
|
+
"version": "0.31.6",
|
|
4
4
|
"description": "Go html/template adapter for BarefootJS - generates Go template files from IR",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
"directory": "packages/adapter-go-template"
|
|
50
50
|
},
|
|
51
51
|
"dependencies": {
|
|
52
|
-
"@barefootjs/shared": "0.31.
|
|
52
|
+
"@barefootjs/shared": "0.31.6"
|
|
53
53
|
},
|
|
54
54
|
"peerDependencies": {
|
|
55
55
|
"@barefootjs/jsx": ">=0.2.0",
|
|
@@ -67,9 +67,9 @@
|
|
|
67
67
|
},
|
|
68
68
|
"devDependencies": {
|
|
69
69
|
"@barefootjs/adapter-tests": "0.1.0",
|
|
70
|
-
"@barefootjs/client": "0.31.
|
|
71
|
-
"@barefootjs/jsx": "0.31.
|
|
72
|
-
"@barefootjs/vite": "0.31.
|
|
70
|
+
"@barefootjs/client": "0.31.6",
|
|
71
|
+
"@barefootjs/jsx": "0.31.6",
|
|
72
|
+
"@barefootjs/vite": "0.31.6",
|
|
73
73
|
"vite": "^6.0.0"
|
|
74
74
|
}
|
|
75
75
|
}
|
|
@@ -127,6 +127,7 @@ function collectNestedComponents(node: IRNode, result: NestedComponentInfo[]): v
|
|
|
127
127
|
...loop.childComponent,
|
|
128
128
|
isDynamic: !loop.isStaticArray,
|
|
129
129
|
isPropDerived: !!loop.isPropDerivedArray,
|
|
130
|
+
clientOnly: loop.clientOnly,
|
|
130
131
|
loopKey: loop.key ?? undefined,
|
|
131
132
|
loopParam: loop.param ?? undefined,
|
|
132
133
|
bodyChildren: hasBodyChildren ? loop.childComponent.children : undefined,
|
|
@@ -32,6 +32,7 @@ import type {
|
|
|
32
32
|
IRMetadata,
|
|
33
33
|
TemplatePrimitiveRegistry,
|
|
34
34
|
LoopBindingPathSegment,
|
|
35
|
+
LoopBindingSource,
|
|
35
36
|
} from '@barefootjs/jsx'
|
|
36
37
|
import {
|
|
37
38
|
BaseAdapter,
|
|
@@ -74,6 +75,7 @@ import {
|
|
|
74
75
|
resolveStaticLoopSource,
|
|
75
76
|
collectLoopBoundNames,
|
|
76
77
|
evaluateStaticLiteral,
|
|
78
|
+
BindingScope,
|
|
77
79
|
} from '@barefootjs/jsx'
|
|
78
80
|
import { findInterpolationEnd } from '@barefootjs/jsx/scanner'
|
|
79
81
|
import { BF_REGION, escapeHtml } from '@barefootjs/shared'
|
|
@@ -263,7 +265,37 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
263
265
|
* populated it, not correctness across passes.
|
|
264
266
|
*/
|
|
265
267
|
private bakedStaticChildLoopCache = new Map<string, BakedStaticChildLoop | null>()
|
|
266
|
-
|
|
268
|
+
/**
|
|
269
|
+
* The one threaded, immutable scope of "names bound by an enclosing loop
|
|
270
|
+
* callback" (#2482 Stage 3 — replaces this adapter's own order-sensitive
|
|
271
|
+
* loop-param stack). Entered via `enterLoopRow(loop)` in `renderLoop` /
|
|
272
|
+
* `renderUnrolledStaticElementLoop`, bracketing preamble conversion,
|
|
273
|
+
* children rendering, AND the key-anchor (`loopItemMarker`) conversion —
|
|
274
|
+
* mirrors the Stage 1a/1b `ctx.scope` precedent in `jsx-to-ir.ts` and the
|
|
275
|
+
* Stage 2 template-string-adapter migration. `IRLoop` already structurally
|
|
276
|
+
* satisfies `LoopBindingSource` (`param`/`index`/`paramBindings`/
|
|
277
|
+
* `preamble`), so most call sites pass the loop node straight through.
|
|
278
|
+
*
|
|
279
|
+
* ONE Go-specific wrinkle `BindingScope`'s generic 'item' semantics don't
|
|
280
|
+
* know about: a `.keys()`-shape loop's callback param is the RANGE INDEX,
|
|
281
|
+
* not the range value — Go's `{{range}}` dot context there is the
|
|
282
|
+
* (discarded) value, not the key, so the key must never win the
|
|
283
|
+
* depth-0-item "resolves to `.`" fast path `isCurrentLoopItem` grants a
|
|
284
|
+
* plain `.map()` param. `renderLoop` enters such a loop's scope with an
|
|
285
|
+
* overridden EMPTY `param`, mirroring the historical empty-string push
|
|
286
|
+
* onto the old stack for the same shape — the key name stays
|
|
287
|
+
* resolvable only through `loopVarRefCount`'s existing `$name` machinery,
|
|
288
|
+
* unchanged by this migration.
|
|
289
|
+
*
|
|
290
|
+
* Destructured-binding ACCESSOR TEXT (`id` → `$__bf_item0.Id`) is Go-
|
|
291
|
+
* specific rendering payload `ScopeBinding` has no field for (by design —
|
|
292
|
+
* `BindingScope` only carries EXISTENCE/kind, not per-adapter accessor
|
|
293
|
+
* strings), so `loopBindingStack` stays as the accessor source of truth,
|
|
294
|
+
* pushed/popped in lockstep with `scope` at the same points. `scope`
|
|
295
|
+
* subsumes its EXISTENCE role (shadow-guard / dot-vs-`$name` decisions)
|
|
296
|
+
* everywhere except the accessor lookup itself.
|
|
297
|
+
*/
|
|
298
|
+
private scope: BindingScope = BindingScope.EMPTY
|
|
267
299
|
/**
|
|
268
300
|
* Stack of `IRLoop.depth` values (innermost last), pushed/popped around
|
|
269
301
|
* `renderChildren(loop.children)` in `renderLoop`. `renderAttributes`
|
|
@@ -450,6 +482,7 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
450
482
|
this.state.referencedDerivedConsts = new Set()
|
|
451
483
|
this.state.templateVarCounter = 0
|
|
452
484
|
this.state.pendingChildrenDefines = []
|
|
485
|
+
this.scope = BindingScope.EMPTY
|
|
453
486
|
this.primeCompileState(ir)
|
|
454
487
|
this.state.stringValueNames = collectStringValueNames(ir)
|
|
455
488
|
// #2448: self-register this component's derived-memo dependencies, so a
|
|
@@ -928,6 +961,14 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
928
961
|
* caller-facing while Props/NewProps key local, so a one-sided check lets
|
|
929
962
|
* the structs disagree on whether the field exists (a duplicate json tag,
|
|
930
963
|
* or a dead Input field whose caller writes are silently ignored).
|
|
964
|
+
*
|
|
965
|
+
* `nestedArrayFields` (built by each call site) is ALREADY restricted to
|
|
966
|
+
* prop-derived loops (#2627) — a same-name coincidence with a loop that
|
|
967
|
+
* merely transforms a differently-shaped prop (`Object.entries(props.tags)
|
|
968
|
+
* .filter(...)` feeding a `<Tag>` loop, whose plural `Tags` happens to
|
|
969
|
+
* match a `tags: Record<...>` prop) must NOT shadow the prop's own field;
|
|
970
|
+
* the two are different Go types and the prop has no other field to land
|
|
971
|
+
* in. See the call sites' `nestedArrayFields` construction.
|
|
931
972
|
*/
|
|
932
973
|
private isNestedArrayShadowed(
|
|
933
974
|
param: { name: string; sourceName?: string },
|
|
@@ -939,6 +980,61 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
939
980
|
)
|
|
940
981
|
}
|
|
941
982
|
|
|
983
|
+
/**
|
|
984
|
+
* True when `nested` is a `/* @client *\/` child-component loop (#2627)
|
|
985
|
+
* whose array is neither a real signal/memo (`isDynamic`) nor a direct
|
|
986
|
+
* prop reference (`isPropDerived`) — e.g. `Object.entries(props.tags)
|
|
987
|
+
* .filter(...)` feeding a `<Tag>` loop. `renderLoop`'s clientOnly branch
|
|
988
|
+
* (its very first check) renders such a loop as bare start/end comment
|
|
989
|
+
* markers on every backend — the SSR template NEVER references this
|
|
990
|
+
* nested component's Go field — and for a non-clientOnly loop with this
|
|
991
|
+
* same array shape, `renderLoop`'s own later BF101 gate (`arrayName` /
|
|
992
|
+
* `isBound` check) refuses to bind a function-scope computed local as a
|
|
993
|
+
* template variable at all. So there is no Go value anywhere to seed an
|
|
994
|
+
* Input field from or range over in `NewXxxProps` — treating `nested` as
|
|
995
|
+
* a normal static/prop-derived child loop emits a field/range/wrapper var
|
|
996
|
+
* for data nothing ever populates, and when its plural name happens to
|
|
997
|
+
* collide with the ACTUAL driving prop (`tags` -> `Tags`, `Tag` ->
|
|
998
|
+
* `Tags`), that dead field is worse than dead: either a duplicate Go
|
|
999
|
+
* field name (compile error) or — via `isNestedArrayShadowed` — silent
|
|
1000
|
+
* loss of the prop's own field, the one channel the client actually
|
|
1001
|
+
* reads hydration data from.
|
|
1002
|
+
*
|
|
1003
|
+
* `nested.isDynamic` is excluded from this check on purpose: a genuinely
|
|
1004
|
+
* signal-backed clientOnly loop keeps its existing (tested, harmless)
|
|
1005
|
+
* `json:"-"` Props field — only the "looks static but is actually an
|
|
1006
|
+
* unbindable local" shape needs full exclusion.
|
|
1007
|
+
*/
|
|
1008
|
+
private isOrphanedClientOnlyNested(nested: NestedComponentInfo): boolean {
|
|
1009
|
+
return !!nested.clientOnly && !nested.isDynamic && !nested.isPropDerived
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
/**
|
|
1013
|
+
* The auto-pluralized Go field names (`Row` -> `Rows`) that genuinely
|
|
1014
|
+
* SUBSUME a same-named props param, so that param must not also get its
|
|
1015
|
+
* own field. Four sites consume this — `generateInputStruct`,
|
|
1016
|
+
* `generatePropsStruct`, `emitPropsAuxFields` and `recordRepropsSpec` —
|
|
1017
|
+
* and they must agree exactly: a one-sided answer lets Input and
|
|
1018
|
+
* Props/NewProps disagree about whether a field exists, producing a
|
|
1019
|
+
* duplicate json tag or a dead Input field whose caller writes are
|
|
1020
|
+
* silently ignored. Extracted to one function so that agreement is
|
|
1021
|
+
* structural rather than four copies kept in step by hand (#2628 review).
|
|
1022
|
+
*
|
|
1023
|
+
* Only `isPropDerived` loops qualify (#2627). Such a loop ranges directly
|
|
1024
|
+
* over `props.X` (or a destructured binding of it), so the array field and
|
|
1025
|
+
* the prop are the same data, merely re-shaped into typed rows. A mere
|
|
1026
|
+
* NAME coincidence is not subsumption: `Object.entries(props.tags)
|
|
1027
|
+
* .filter(...)` feeding a `<Tag>` loop pluralizes to `Tags` and collides
|
|
1028
|
+
* with a `tags` prop, but the prop is a `Record` while the loop array is a
|
|
1029
|
+
* derived slice of tuples — dropping the prop's field there leaves it with
|
|
1030
|
+
* no Go field at all to receive the caller's value. Note the check is
|
|
1031
|
+
* purely name-based, so this is not specific to `Record`: any prop whose
|
|
1032
|
+
* capitalized name matches a child's plural hits the same path.
|
|
1033
|
+
*/
|
|
1034
|
+
private propDerivedNestedArrayFields(nestedComponents: readonly NestedComponentInfo[]): Set<string> {
|
|
1035
|
+
return new Set(nestedComponents.filter(n => n.isPropDerived).map(n => `${n.name}s`))
|
|
1036
|
+
}
|
|
1037
|
+
|
|
942
1038
|
/**
|
|
943
1039
|
* Every Go field name these props params could claim — LOCAL and
|
|
944
1040
|
* caller-facing. A context-consumer field must exist in ALL of
|
|
@@ -1055,10 +1151,7 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
1055
1151
|
): void {
|
|
1056
1152
|
if (!this.childDerivedFieldDeps.has(componentName)) return
|
|
1057
1153
|
|
|
1058
|
-
|
|
1059
|
-
// both walk the Input struct's actual (caller-keyed) field names, or the
|
|
1060
|
-
// reprops switch references fields the struct doesn't have.
|
|
1061
|
-
const nestedArrayFields = new Set(nestedComponents.map(n => `${n.name}s`))
|
|
1154
|
+
const nestedArrayFields = this.propDerivedNestedArrayFields(nestedComponents)
|
|
1062
1155
|
const params = (ir.metadata.propsParams ?? []).filter(
|
|
1063
1156
|
p => !this.isNestedArrayShadowed(p, nestedArrayFields),
|
|
1064
1157
|
)
|
|
@@ -1374,10 +1467,14 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
1374
1467
|
}
|
|
1375
1468
|
|
|
1376
1469
|
// Static + prop-derived nested components are in Input; signal-backed
|
|
1377
|
-
// dynamic ones are template-only.
|
|
1378
|
-
|
|
1470
|
+
// dynamic ones are template-only. An orphaned clientOnly nested loop
|
|
1471
|
+
// (#2627 — see `isOrphanedClientOnlyNested`) has no Go value to seed an
|
|
1472
|
+
// Input field from at all, so it's excluded from both buckets.
|
|
1473
|
+
const inputNested = nestedComponents.filter(
|
|
1474
|
+
n => (!n.isDynamic || n.isPropDerived) && !this.isOrphanedClientOnlyNested(n),
|
|
1475
|
+
)
|
|
1379
1476
|
|
|
1380
|
-
const nestedArrayFields =
|
|
1477
|
+
const nestedArrayFields = this.propDerivedNestedArrayFields(nestedComponents)
|
|
1381
1478
|
|
|
1382
1479
|
for (const param of ir.metadata.propsParams) {
|
|
1383
1480
|
// #2525: caller-facing name, not the local destructure binding — a
|
|
@@ -1615,8 +1712,13 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
1615
1712
|
lines.push('\t}')
|
|
1616
1713
|
lines.push('')
|
|
1617
1714
|
|
|
1618
|
-
// Static + prop-derived nested components auto-populate from input.
|
|
1619
|
-
|
|
1715
|
+
// Static + prop-derived nested components auto-populate from input. An
|
|
1716
|
+
// orphaned clientOnly nested loop (#2627 — see `isOrphanedClientOnlyNested`)
|
|
1717
|
+
// has no Input field to range over (excluded above in `generateInputStruct`),
|
|
1718
|
+
// so it's excluded here too.
|
|
1719
|
+
const staticNested = nestedComponents.filter(
|
|
1720
|
+
n => (!n.isDynamic || n.isPropDerived) && !this.isOrphanedClientOnlyNested(n),
|
|
1721
|
+
)
|
|
1620
1722
|
|
|
1621
1723
|
// Wrapper vars emitted (by either the static-with-body or dynamic-with-body
|
|
1622
1724
|
// path), so the return struct only includes the ones that were built.
|
|
@@ -1753,7 +1855,7 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
1753
1855
|
lines.push('\t\tSearchParams: in.SearchParams,')
|
|
1754
1856
|
}
|
|
1755
1857
|
|
|
1756
|
-
const nestedArrayFields =
|
|
1858
|
+
const nestedArrayFields = this.propDerivedNestedArrayFields(nestedComponents)
|
|
1757
1859
|
|
|
1758
1860
|
// Props params (field names tracked to skip duplicate signal assignments).
|
|
1759
1861
|
// A JSX-declared default (`variant = 'default'`) or signal-side fallback
|
|
@@ -2478,9 +2580,7 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
2478
2580
|
propTypeOverrides: Map<string, string>,
|
|
2479
2581
|
takenJsonTags: Set<string>,
|
|
2480
2582
|
): void {
|
|
2481
|
-
|
|
2482
|
-
// their raw prop; track them (and emitted names) to skip duplicates.
|
|
2483
|
-
const nestedArrayFields = new Set(nestedComponents.map(n => `${n.name}s`))
|
|
2583
|
+
const nestedArrayFields = this.propDerivedNestedArrayFields(nestedComponents)
|
|
2484
2584
|
const propFieldNames = new Set<string>()
|
|
2485
2585
|
|
|
2486
2586
|
for (const param of ir.metadata.propsParams) {
|
|
@@ -2603,6 +2703,15 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
2603
2703
|
}
|
|
2604
2704
|
|
|
2605
2705
|
for (const nested of nestedComponents) {
|
|
2706
|
+
// An orphaned clientOnly nested loop (#2627 — see
|
|
2707
|
+
// `isOrphanedClientOnlyNested`) gets NO Props field at all, not even
|
|
2708
|
+
// the dead `json:"-"` one the signal-backed dynamic branch below gets:
|
|
2709
|
+
// its Go field name (`${nested.name}s`) can collide with the ACTUAL
|
|
2710
|
+
// driving prop's own field (e.g. `tags` -> `Tags` colliding with
|
|
2711
|
+
// `Tag` -> `Tags`), which `emitPropsDataFields` already emitted for
|
|
2712
|
+
// real — a second same-named field here is a Go compile error
|
|
2713
|
+
// ("redeclared"), not just dead code.
|
|
2714
|
+
if (this.isOrphanedClientOnlyNested(nested)) continue
|
|
2606
2715
|
// Loop body with JSX children → use the wrapper struct type.
|
|
2607
2716
|
const elemType = nested.bodyChildren?.length
|
|
2608
2717
|
? this.loopBodyWrapperName(componentName, nested)
|
|
@@ -3757,8 +3866,7 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
3757
3866
|
// field that never exists. Mirrors the string-const inlining above.
|
|
3758
3867
|
const inlinedNum = this.resolveModuleNumericConst(name)
|
|
3759
3868
|
if (inlinedNum !== null) return inlinedNum
|
|
3760
|
-
|
|
3761
|
-
if (currentLoopParam && name === currentLoopParam) return '.'
|
|
3869
|
+
if (this.isCurrentLoopItem(name)) return '.'
|
|
3762
3870
|
// An *outer* loop's value variable (we're in a nested loop) is in scope as
|
|
3763
3871
|
// the Go range variable `$name` declared by that loop's `{{range … := …}}`;
|
|
3764
3872
|
// the inner dot no longer refers to it, and it's not a root field.
|
|
@@ -3858,18 +3966,31 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
3858
3966
|
return false
|
|
3859
3967
|
}
|
|
3860
3968
|
|
|
3969
|
+
/**
|
|
3970
|
+
* True when `name` is bound as the CURRENT (innermost) loop row's own
|
|
3971
|
+
* plain `.map()` item param — i.e. it resolves to Go template's dot
|
|
3972
|
+
* context (`.`), not a `$name` range variable or a root field. `depth 0`
|
|
3973
|
+
* is `scope`'s innermost frame; `source === 'item'` excludes a
|
|
3974
|
+
* destructured binding (source `'destructure'`, resolved through
|
|
3975
|
+
* `loopBindingStack` instead — see `scope`'s field docstring) and a
|
|
3976
|
+
* `.keys()`-shape loop's key param (never bound as `'item'` at all —
|
|
3977
|
+
* `renderLoop` enters that shape's scope with an overridden empty param).
|
|
3978
|
+
*/
|
|
3979
|
+
private isCurrentLoopItem(name: string): boolean {
|
|
3980
|
+
const hit = this.scope.lookup(name)
|
|
3981
|
+
return hit !== null && hit.depth === 0 && hit.binding.source === 'item'
|
|
3982
|
+
}
|
|
3983
|
+
|
|
3861
3984
|
/**
|
|
3862
3985
|
* True when `name` is a loop value variable from an enclosing (not the
|
|
3863
|
-
* current) loop — i.e. it
|
|
3864
|
-
*
|
|
3865
|
-
* the
|
|
3986
|
+
* current) loop — i.e. it's bound in `scope` as an OUTER frame's own
|
|
3987
|
+
* plain item param (`depth > 0`, source `'item'`). Such a reference
|
|
3988
|
+
* resolves to the Go range variable `$name`, not the inner dot or the
|
|
3989
|
+
* root data.
|
|
3866
3990
|
*/
|
|
3867
3991
|
private isOuterLoopParam(name: string): boolean {
|
|
3868
|
-
const
|
|
3869
|
-
|
|
3870
|
-
if (this.loopParamStack[i] === name) return true
|
|
3871
|
-
}
|
|
3872
|
-
return false
|
|
3992
|
+
const hit = this.scope.lookup(name)
|
|
3993
|
+
return hit !== null && hit.depth > 0 && hit.binding.source === 'item'
|
|
3873
3994
|
}
|
|
3874
3995
|
|
|
3875
3996
|
/**
|
|
@@ -3880,7 +4001,7 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
3880
4001
|
* rebinds. Outside any loop the root *is* the dot, so we emit `.Field`.
|
|
3881
4002
|
*/
|
|
3882
4003
|
private rootFieldRef(name: string): string {
|
|
3883
|
-
const prefix = this.
|
|
4004
|
+
const prefix = this.inLoop ? '$.' : '.'
|
|
3884
4005
|
return `${prefix}${capitalizeFieldName(name)}`
|
|
3885
4006
|
}
|
|
3886
4007
|
|
|
@@ -3922,9 +4043,7 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
3922
4043
|
* auto-escaping.
|
|
3923
4044
|
*/
|
|
3924
4045
|
private resolveModuleStringConst(name: string): string | null {
|
|
3925
|
-
if (this.
|
|
3926
|
-
return null
|
|
3927
|
-
}
|
|
4046
|
+
if (this.isCurrentLoopItem(name)) return null
|
|
3928
4047
|
if (this.loopVarRefCount.has(name)) return null
|
|
3929
4048
|
if (this.isOuterLoopParam(name)) return null
|
|
3930
4049
|
const value = this.state.moduleStringConsts.get(name)
|
|
@@ -3940,9 +4059,7 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
3940
4059
|
* range variable that shadows a const name still wins.
|
|
3941
4060
|
*/
|
|
3942
4061
|
private resolveModuleNumericConst(name: string): string | null {
|
|
3943
|
-
if (this.
|
|
3944
|
-
return null
|
|
3945
|
-
}
|
|
4062
|
+
if (this.isCurrentLoopItem(name)) return null
|
|
3946
4063
|
if (this.loopVarRefCount.has(name)) return null
|
|
3947
4064
|
if (this.isOuterLoopParam(name)) return null
|
|
3948
4065
|
const c = this.state.localConstants.find(
|
|
@@ -4033,7 +4150,7 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
4033
4150
|
const slice = this.state.memoBackedLoopSlice.get(object.callee.name)
|
|
4034
4151
|
if (slice) {
|
|
4035
4152
|
// Root field, so reach it through `$.` inside a loop.
|
|
4036
|
-
const prefix = this.
|
|
4153
|
+
const prefix = this.inLoop ? '$.' : '.'
|
|
4037
4154
|
return `len ${prefix}${slice}`
|
|
4038
4155
|
}
|
|
4039
4156
|
}
|
|
@@ -4094,8 +4211,7 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
4094
4211
|
// template syntax, let alone a reachable field (PR #2089 review). For
|
|
4095
4212
|
// identifier keys (snake_case included) the two functions agree, so
|
|
4096
4213
|
// nothing previously reachable changes shape.
|
|
4097
|
-
|
|
4098
|
-
if (object.kind === 'identifier' && currentLoopParam && object.name === currentLoopParam) {
|
|
4214
|
+
if (object.kind === 'identifier' && this.isCurrentLoopItem(object.name)) {
|
|
4099
4215
|
return `.${goFieldNameForKey(property)}`
|
|
4100
4216
|
}
|
|
4101
4217
|
|
|
@@ -5466,22 +5582,18 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
5466
5582
|
|
|
5467
5583
|
/**
|
|
5468
5584
|
* Whether `name` at the CURRENT emission position is bound by an enclosing
|
|
5469
|
-
* loop callback — its item param
|
|
5470
|
-
*
|
|
5471
|
-
* (`
|
|
5472
|
-
*
|
|
5585
|
+
* loop callback — its item param, an outer loop's range variable, a
|
|
5586
|
+
* destructured binding name, an index var, or a preamble local. `scope`
|
|
5587
|
+
* (`BindingScope`, #2482 Stage 3) covers all of those EXCEPT a
|
|
5588
|
+
* `.keys()`-shape loop's own key param (never bound in `scope` at all —
|
|
5589
|
+
* see `scope`'s field docstring), which stays reachable only through
|
|
5590
|
+
* `loopVarRefCount` — so both are still consulted. Shared by the
|
|
5473
5591
|
* string-keyed fast paths (#2236, #2242 Copilot review) that resolve raw
|
|
5474
5592
|
* `jsExpr` text before `identifier()`'s own guards can see it. Mirrors the
|
|
5475
5593
|
* checks in `resolveModuleStringConst` / `resolveModuleNumericConst`.
|
|
5476
5594
|
*/
|
|
5477
5595
|
private isLoopShadowedName(name: string): boolean {
|
|
5478
|
-
return (
|
|
5479
|
-
(this.loopParamStack.length > 0 &&
|
|
5480
|
-
this.loopParamStack[this.loopParamStack.length - 1] === name) ||
|
|
5481
|
-
this.loopVarRefCount.has(name) ||
|
|
5482
|
-
this.isOuterLoopParam(name) ||
|
|
5483
|
-
this.loopBindingStack.some(bindings => bindings.has(name))
|
|
5484
|
-
)
|
|
5596
|
+
return this.scope.isBound(name) || this.loopVarRefCount.has(name)
|
|
5485
5597
|
}
|
|
5486
5598
|
|
|
5487
5599
|
/**
|
|
@@ -5871,8 +5983,7 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
5871
5983
|
}
|
|
5872
5984
|
const inlined = this.resolveModuleStringConst(expr.name)
|
|
5873
5985
|
if (inlined !== null) return plain(inlined)
|
|
5874
|
-
|
|
5875
|
-
if (currentLoopParam && expr.name === currentLoopParam) {
|
|
5986
|
+
if (this.isCurrentLoopItem(expr.name)) {
|
|
5876
5987
|
return plain('.')
|
|
5877
5988
|
}
|
|
5878
5989
|
// Outer loop value variable (nested loop) → its range var `$name`.
|
|
@@ -5971,11 +6082,8 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
5971
6082
|
return plain(this.rootFieldRef(expr.property))
|
|
5972
6083
|
}
|
|
5973
6084
|
|
|
5974
|
-
{
|
|
5975
|
-
|
|
5976
|
-
if (expr.object.kind === 'identifier' && currentLoopParam && expr.object.name === currentLoopParam) {
|
|
5977
|
-
return plain(`.${capitalizeFieldName(expr.property)}`)
|
|
5978
|
-
}
|
|
6085
|
+
if (expr.object.kind === 'identifier' && this.isCurrentLoopItem(expr.object.name)) {
|
|
6086
|
+
return plain(`.${capitalizeFieldName(expr.property)}`)
|
|
5979
6087
|
}
|
|
5980
6088
|
|
|
5981
6089
|
const obj = this.renderConditionExpr(expr.object)
|
|
@@ -6241,6 +6349,14 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
6241
6349
|
if (this.bakedStaticChildLoopCache.has(markerId)) {
|
|
6242
6350
|
return this.bakedStaticChildLoopCache.get(markerId) ?? null
|
|
6243
6351
|
}
|
|
6352
|
+
// #2482 Stage 3: this method is called from TWO sites outside the live
|
|
6353
|
+
// `renderLoop` tree walk (`generateNewPropsFunction`'s Input-struct
|
|
6354
|
+
// field list and constructor-generation pass, per the docstring above)
|
|
6355
|
+
// where there is no live `this.scope` to consult — so, unlike
|
|
6356
|
+
// `renderLoop`'s own `analyzeBakeableStaticElementLoop` call, this stays
|
|
6357
|
+
// on the coarse whole-component shadow-name Set `primeCompileState`
|
|
6358
|
+
// populates. A genuinely-legitimate surviving use of the pre-#2482
|
|
6359
|
+
// device (flagged for Stage 4 rather than migrated here).
|
|
6244
6360
|
const result = analyzeBakeableStaticChildLoop(
|
|
6245
6361
|
{ props: childComponent.props, loopArrayParsed: arrayParsed, loopParam: param, loopKey: key },
|
|
6246
6362
|
this.state.localConstants,
|
|
@@ -6337,19 +6453,33 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
6337
6453
|
// exact (conservative) acceptance gate. `null` here means the shape
|
|
6338
6454
|
// isn't (yet) bakeable this way; the existing gates below keep firing
|
|
6339
6455
|
// exactly as before.
|
|
6456
|
+
// #2482 Stage 3: `renderLoop` is a live tree-walk, so `this.scope` (not
|
|
6457
|
+
// yet entered for THIS loop's own row — that happens further below)
|
|
6458
|
+
// gives the position-accurate ancestor-shadowing answer directly, unlike
|
|
6459
|
+
// `getBakedStaticChildLoop` above, which is memoized across sites that
|
|
6460
|
+
// run OUTSIDE the tree walk and so must keep its coarse whole-component
|
|
6461
|
+
// shadow-name Set (see that method's own comment).
|
|
6340
6462
|
const bakedElementLoop = loop.childComponent
|
|
6341
6463
|
? null
|
|
6342
6464
|
: analyzeBakeableStaticElementLoop(
|
|
6343
6465
|
loop,
|
|
6344
6466
|
this.state.localConstants,
|
|
6345
|
-
{ isNameShadowed:
|
|
6467
|
+
{ isNameShadowed: this.scope.asShadowPredicate() },
|
|
6346
6468
|
)
|
|
6347
6469
|
if (bakedElementLoop) {
|
|
6348
6470
|
return this.renderUnrolledStaticElementLoop(loop, bakedElementLoop.items)
|
|
6349
6471
|
}
|
|
6350
6472
|
|
|
6351
6473
|
const arrayName = loop.array.trim()
|
|
6352
|
-
|
|
6474
|
+
// #2482 Stage 4: guard against an ENCLOSING loop's own item param
|
|
6475
|
+
// shadowing a same-named module/component const (`items.map(items =>
|
|
6476
|
+
// items.map(...))`) — without this, a bare-identifier array reference
|
|
6477
|
+
// that resolves to the outer loop's per-row value could misresolve to
|
|
6478
|
+
// the shadowed const below and raise a false BF101. `this.scope` here
|
|
6479
|
+
// is the position-accurate ancestor scope (see the comment above
|
|
6480
|
+
// `bakedElementLoop`) — same shadow-guard shape as
|
|
6481
|
+
// `prop-handling.ts`'s `expandDynamicPropValue`.
|
|
6482
|
+
if (bakedChildLoop === null && /^[A-Za-z_$][\w$]*$/.test(arrayName) && !this.scope.isBound(arrayName)) {
|
|
6353
6483
|
const arrayConst = this.state.localConstants.find(c => c.name === arrayName)
|
|
6354
6484
|
if (arrayConst && !arrayConst.isModule && arrayConst.parsed && !this.isStringExpr(arrayConst.parsed, new Set())) {
|
|
6355
6485
|
this.state.errors.push({
|
|
@@ -6360,6 +6490,7 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
6360
6490
|
suggestion: {
|
|
6361
6491
|
message:
|
|
6362
6492
|
'Pre-compute the array server-side and pass it as a prop, or mark the loop position as @client-only so it runs in JS on the client.',
|
|
6493
|
+
escape: [{ kind: 'prop-precompute' }, { kind: 'client-directive' }],
|
|
6363
6494
|
},
|
|
6364
6495
|
})
|
|
6365
6496
|
}
|
|
@@ -6423,15 +6554,26 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
6423
6554
|
// of the row-scoped field) (#2487).
|
|
6424
6555
|
const wasInLoopOuter = this.inLoop
|
|
6425
6556
|
this.inLoop = true
|
|
6426
|
-
//
|
|
6427
|
-
//
|
|
6428
|
-
//
|
|
6429
|
-
//
|
|
6430
|
-
//
|
|
6431
|
-
//
|
|
6432
|
-
//
|
|
6433
|
-
//
|
|
6434
|
-
//
|
|
6557
|
+
// Enter this loop's row scope (#2482 Stage 3). `IRLoop` already
|
|
6558
|
+
// structurally satisfies `LoopBindingSource`, so the plain/destructured
|
|
6559
|
+
// cases pass `loop` straight through — `enterLoopRow` binds
|
|
6560
|
+
// `paramBindings` (destructure) OR `param` (plain item) automatically.
|
|
6561
|
+
// `.keys()` is the one shape `BindingScope`'s generic 'item' semantics
|
|
6562
|
+
// don't fit: the callback param there IS the index, not the row value
|
|
6563
|
+
// (the dot context is the discarded value), so it must never win
|
|
6564
|
+
// `isCurrentLoopItem`'s dot-shortcut — enter with an overridden empty
|
|
6565
|
+
// `param`, mirroring the historical empty-string push onto the old
|
|
6566
|
+
// loop-param stack for the same shape. The key stays reachable only
|
|
6567
|
+
// via `loopVarRefCount`'s `$name` bookkeeping below, unchanged by this
|
|
6568
|
+
// migration.
|
|
6569
|
+
const scopeLoop: LoopBindingSource = loop.iterationShape === 'keys' ? { ...loop, param: '' } : loop
|
|
6570
|
+
const prevScope = this.scope
|
|
6571
|
+
this.scope = prevScope.enterLoopRow(scopeLoop)
|
|
6572
|
+
// Track Go template loop variables. The range *index* variable needs
|
|
6573
|
+
// `$name` notation and goes on `loopVarRefCount` (the range *value*
|
|
6574
|
+
// variable's dot-context resolution now comes from `scope` above).
|
|
6575
|
+
// Ref-counting (not a flat Set) keeps nested loops with the same index
|
|
6576
|
+
// var name from clobbering the outer entry on cleanup.
|
|
6435
6577
|
const addedLoopVars: string[] = []
|
|
6436
6578
|
// A `.map()` callback preamble lowers to one `{{$cls := …}}` per-row
|
|
6437
6579
|
// variable per declaration (#2447). Registering the names on
|
|
@@ -6446,23 +6588,19 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
6446
6588
|
}
|
|
6447
6589
|
let pushedBindingMap = false
|
|
6448
6590
|
if (supportableDestructure) {
|
|
6449
|
-
// Bindings resolve against the synthetic `$__bf_item` range var
|
|
6450
|
-
// a loop param (the param is a pattern, not a name).
|
|
6591
|
+
// Bindings resolve against the synthetic `$__bf_item` range var.
|
|
6451
6592
|
const built = this.buildDestructureBindingMap(loop, rangeValue)
|
|
6452
6593
|
this.loopBindingStack.push(built.bindings)
|
|
6453
6594
|
this.loopRestExcludeStack.push(built.restExcludes)
|
|
6454
6595
|
pushedBindingMap = true
|
|
6455
|
-
this.loopParamStack.push('')
|
|
6456
6596
|
if (rangeIndex !== '_') {
|
|
6457
6597
|
this.loopVarRefCount.set(rangeIndex, (this.loopVarRefCount.get(rangeIndex) ?? 0) + 1)
|
|
6458
6598
|
addedLoopVars.push(rangeIndex)
|
|
6459
6599
|
}
|
|
6460
6600
|
} else if (loop.iterationShape === 'keys') {
|
|
6461
|
-
this.loopParamStack.push('')
|
|
6462
6601
|
this.loopVarRefCount.set(param, (this.loopVarRefCount.get(param) ?? 0) + 1)
|
|
6463
6602
|
addedLoopVars.push(param)
|
|
6464
6603
|
} else {
|
|
6465
|
-
this.loopParamStack.push(param)
|
|
6466
6604
|
if (rangeIndex !== '_') {
|
|
6467
6605
|
this.loopVarRefCount.set(rangeIndex, (this.loopVarRefCount.get(rangeIndex) ?? 0) + 1)
|
|
6468
6606
|
addedLoopVars.push(rangeIndex)
|
|
@@ -6487,9 +6625,9 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
6487
6625
|
this.loopKeyDepthStack.pop()
|
|
6488
6626
|
this.loopWrapperStack.pop()
|
|
6489
6627
|
this.loopScalarItemStack.pop()
|
|
6490
|
-
// Build the per-item anchor marker while the
|
|
6491
|
-
//
|
|
6492
|
-
// range item (`.` context) like `data-key` does —
|
|
6628
|
+
// Build the per-item anchor marker while the row scope is still active,
|
|
6629
|
+
// so a `bodyIsItemConditional` key expression resolves against the
|
|
6630
|
+
// range item (`.` context) like `data-key` does — restoring first would
|
|
6493
6631
|
// rewrite `t.id` to `.T.ID` instead of `.ID`.
|
|
6494
6632
|
const itemMarker = this.loopItemMarker(loop)
|
|
6495
6633
|
for (const v of addedLoopVars) {
|
|
@@ -6497,7 +6635,7 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
6497
6635
|
if (rc <= 0) this.loopVarRefCount.delete(v)
|
|
6498
6636
|
else this.loopVarRefCount.set(v, rc)
|
|
6499
6637
|
}
|
|
6500
|
-
this.
|
|
6638
|
+
this.scope = prevScope
|
|
6501
6639
|
if (pushedBindingMap) {
|
|
6502
6640
|
this.loopBindingStack.pop()
|
|
6503
6641
|
this.loopRestExcludeStack.pop()
|
|
@@ -6578,7 +6716,11 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
6578
6716
|
this.loopWrapperStack.push(false)
|
|
6579
6717
|
this.loopKeyDepthStack.push(loop.depth)
|
|
6580
6718
|
this.loopScalarItemStack.push(this.scalarLiteralLoopGoType(loop.arrayParsed, loop.itemType) !== null)
|
|
6581
|
-
|
|
6719
|
+
// The bake gate (`analyzeBakeableStaticElementLoop`) already refused a
|
|
6720
|
+
// destructured or `.keys()`-shape param, so `loop` binds via `enterLoopRow`
|
|
6721
|
+
// unadjusted — always a plain item (#2482 Stage 3).
|
|
6722
|
+
const prevScope = this.scope
|
|
6723
|
+
this.scope = prevScope.enterLoopRow(loop)
|
|
6582
6724
|
|
|
6583
6725
|
let body = ''
|
|
6584
6726
|
for (const item of items) {
|
|
@@ -6603,7 +6745,7 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
6603
6745
|
}
|
|
6604
6746
|
}
|
|
6605
6747
|
|
|
6606
|
-
this.
|
|
6748
|
+
this.scope = prevScope
|
|
6607
6749
|
this.loopScalarItemStack.pop()
|
|
6608
6750
|
this.loopKeyDepthStack.pop()
|
|
6609
6751
|
this.loopWrapperStack.pop()
|
|
@@ -6947,8 +7089,7 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
6947
7089
|
// (#2087), which lifted the flat-object-only restriction for the
|
|
6948
7090
|
// destructure-residual fixtures below.
|
|
6949
7091
|
const trimmed = value.expr.trim()
|
|
6950
|
-
|
|
6951
|
-
if (currentLoopParam && trimmed === currentLoopParam) {
|
|
7092
|
+
if (this.isCurrentLoopItem(trimmed)) {
|
|
6952
7093
|
// The row's keys are Go-cased by the field-access contract: the
|
|
6953
7094
|
// same lowering that makes `attrs.id` emit `{{.ID}}`
|
|
6954
7095
|
// (`goFieldNameForKey` → `capitalizeFieldName`) requires the
|
|
@@ -6,8 +6,12 @@
|
|
|
6
6
|
*
|
|
7
7
|
* NOT included (deliberately): cross-compile child-shape registries
|
|
8
8
|
* (`childComponentShapes`, `childContextConsumers`, populated before a parent
|
|
9
|
-
* compiles), the render-recursion cursor
|
|
10
|
-
* `filterExprDepth`, …), and constant config (`options`,
|
|
9
|
+
* compiles), the render-recursion cursor state (`scope: BindingScope`,
|
|
10
|
+
* `loopBindingStack`, `filterExprDepth`, …), and constant config (`options`,
|
|
11
|
+
* `templatePrimitives`). #2482 Stage 4: `scope` replaced the old mutable
|
|
12
|
+
* stack this comment used to name here (see the field's own docstring on
|
|
13
|
+
* `GoTemplateAdapter` for the full migration story) — this file never held
|
|
14
|
+
* that stack itself, only described where it lived.
|
|
11
15
|
*/
|
|
12
16
|
|
|
13
17
|
import type {
|
package/src/adapter/lib/types.ts
CHANGED
|
@@ -30,6 +30,21 @@ export type GoRenderCtx = {
|
|
|
30
30
|
export interface NestedComponentInfo extends IRLoopChildComponent {
|
|
31
31
|
isDynamic: boolean
|
|
32
32
|
isPropDerived: boolean
|
|
33
|
+
/**
|
|
34
|
+
* True when the enclosing loop carries a `/* @client *\/` directive
|
|
35
|
+
* (`loop.clientOnly`, #2627). `renderLoop`'s FIRST branch short-circuits a
|
|
36
|
+
* clientOnly loop to bare start/end comment markers — the SSR template
|
|
37
|
+
* never references this nested component's Go field, no matter how
|
|
38
|
+
* `isDynamic`/`isPropDerived` classify its array. Combined with
|
|
39
|
+
* `!isDynamic && !isPropDerived` (a clientOnly loop whose array is
|
|
40
|
+
* neither a real signal/memo NOR a direct prop reference — e.g.
|
|
41
|
+
* `Object.entries(props.tags).filter(...)`), there is no Go value to
|
|
42
|
+
* seed an Input/Props field FROM at all: the array is a function-scope
|
|
43
|
+
* computed local that `renderLoop`'s own BF101 gate refuses to bind as a
|
|
44
|
+
* template variable for a non-clientOnly loop. See
|
|
45
|
+
* `GoTemplateAdapter.isOrphanedClientOnlyNested`.
|
|
46
|
+
*/
|
|
47
|
+
clientOnly?: boolean
|
|
33
48
|
/** The enclosing loop's `key` expression (e.g. `item.label`) and map param
|
|
34
49
|
* name (`item`), so the loop-child init can stamp `data-key` per item. */
|
|
35
50
|
loopKey?: string
|