@barefootjs/go-template 0.7.0 → 0.9.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/adapter/go-template-adapter.d.ts +222 -0
- package/dist/adapter/go-template-adapter.d.ts.map +1 -1
- package/dist/adapter/index.js +516 -26
- package/dist/build.js +516 -26
- package/dist/index.js +516 -26
- package/dist/test-render.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/go-template-adapter.test.ts +273 -22
- package/src/adapter/go-template-adapter.ts +907 -24
- package/src/test-render.ts +189 -10
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"test-render.d.ts","sourceRoot":"","sources":["../src/test-render.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,KAAK,EAAE,eAAe,EAAe,MAAM,iBAAiB,CAAA;AAOnE,qBAAa,mBAAoB,SAAQ,KAAK;IAC5C,YAAY,OAAO,EAAE,MAAM,EAG1B;CACF;AAgCD,MAAM,WAAW,aAAa;IAC5B,sBAAsB;IACtB,MAAM,EAAE,MAAM,CAAA;IACd,8BAA8B;IAC9B,OAAO,EAAE,eAAe,CAAA;IACxB,iCAAiC;IACjC,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IAC/B,qDAAqD;IACrD,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CACpC;AAED,wBAAsB,yBAAyB,CAAC,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,MAAM,CAAC,
|
|
1
|
+
{"version":3,"file":"test-render.d.ts","sourceRoot":"","sources":["../src/test-render.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,KAAK,EAAE,eAAe,EAAe,MAAM,iBAAiB,CAAA;AAOnE,qBAAa,mBAAoB,SAAQ,KAAK;IAC5C,YAAY,OAAO,EAAE,MAAM,EAG1B;CACF;AAgCD,MAAM,WAAW,aAAa;IAC5B,sBAAsB;IACtB,MAAM,EAAE,MAAM,CAAA;IACd,8BAA8B;IAC9B,OAAO,EAAE,eAAe,CAAA;IACxB,iCAAiC;IACjC,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IAC/B,qDAAqD;IACrD,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CACpC;AAED,wBAAsB,yBAAyB,CAAC,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,MAAM,CAAC,CAsRvF"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@barefootjs/go-template",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
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",
|
|
@@ -54,6 +54,6 @@
|
|
|
54
54
|
},
|
|
55
55
|
"devDependencies": {
|
|
56
56
|
"@barefootjs/adapter-tests": "0.1.0",
|
|
57
|
-
"@barefootjs/jsx": "0.
|
|
57
|
+
"@barefootjs/jsx": "0.9.0"
|
|
58
58
|
}
|
|
59
59
|
}
|
|
@@ -35,17 +35,6 @@ runAdapterConformanceTests({
|
|
|
35
35
|
// `BF104` at build time instead of silently emitting invalid
|
|
36
36
|
// template syntax (#1266).
|
|
37
37
|
skipJsx: [
|
|
38
|
-
'return-map',
|
|
39
|
-
// #1297 fixed the harness-side IR emission gate (multi-component
|
|
40
|
-
// sources now emit one `ir` file per component, and the harness
|
|
41
|
-
// picks the entry-point IR). The remaining gap is adapter-side:
|
|
42
|
-
// the go-template adapter has no SSR context-propagation
|
|
43
|
-
// mechanism, so `<Ctx.Provider value="dark">` doesn't make
|
|
44
|
-
// `useContext(Ctx)` resolve to `"dark"` at template-eval time —
|
|
45
|
-
// the template emits `.Theme` against a struct that has no
|
|
46
|
-
// `Theme` field. Provider SSR coverage on go-template waits on
|
|
47
|
-
// that adapter feature; see #1297 follow-up.
|
|
48
|
-
'context-provider',
|
|
49
38
|
// #1244 stress catalog (#1326): `children={<span/>}` — the IR
|
|
50
39
|
// hoists the span with `needsScope: true` so the Hono reference
|
|
51
40
|
// emits `bf-s` on the inner `<span>`. The Go adapter renders the
|
|
@@ -76,17 +65,6 @@ runAdapterConformanceTests({
|
|
|
76
65
|
// option fixed on the Hono side. Separate follow-up.
|
|
77
66
|
'toggle-shared',
|
|
78
67
|
'props-reactivity-comparison',
|
|
79
|
-
// #1467 Phase 2b: basic interactive `site/ui` primitives. Cross-adapter
|
|
80
|
-
// parity for the `site/ui` corpus is Phase 3, so these participate only
|
|
81
|
-
// in Hono SSR conformance + the fixture-hydrate runtime layer for now.
|
|
82
|
-
// (`label` and `kbd` are static helpers; `toggle` / `switch` /
|
|
83
|
-
// `checkbox` carry uncontrolled state; `input` / `textarea` are
|
|
84
|
-
// pass-through native controls.)
|
|
85
|
-
'toggle',
|
|
86
|
-
'switch',
|
|
87
|
-
'checkbox',
|
|
88
|
-
'textarea',
|
|
89
|
-
'kbd',
|
|
90
68
|
],
|
|
91
69
|
// Per-fixture build-time contracts for shapes the Go template
|
|
92
70
|
// adapter intentionally refuses to lower. Lives here (not on the
|
|
@@ -818,6 +796,279 @@ export function List() {
|
|
|
818
796
|
})
|
|
819
797
|
})
|
|
820
798
|
|
|
799
|
+
describe('conditional inline-object spread (textarea aria-describedby)', () => {
|
|
800
|
+
// `{...(cond ? { 'aria-describedby': cond } : {})}` lowers to an
|
|
801
|
+
// IIFE-of-maps in `NewXxxProps` so the falsy branch OMITS the key
|
|
802
|
+
// (SpreadAttrs does not filter empty strings). The fixture only
|
|
803
|
+
// exercises the falsy branch; this pins the TRUTHY branch.
|
|
804
|
+
test('lowers to a conditional map IIFE and keeps the {{bf_spread_attrs}} template', () => {
|
|
805
|
+
const source = `
|
|
806
|
+
function Box({ describedBy }: { describedBy?: string }) {
|
|
807
|
+
return <div {...(describedBy ? { 'aria-describedby': describedBy } : {})} />
|
|
808
|
+
}
|
|
809
|
+
`
|
|
810
|
+
const { template, types } = compileAndGenerate(source)
|
|
811
|
+
// Template emission is unchanged from the proven {...props} path.
|
|
812
|
+
expect(template).toContain('{{bf_spread_attrs .Spread_0}}')
|
|
813
|
+
// The bag value is a conditional map built in NewBoxProps. The
|
|
814
|
+
// prop type is unresolved (interface{}), so the condition routes
|
|
815
|
+
// through `bf.Truthy` for a faithful JS `Boolean(x)` test.
|
|
816
|
+
expect(types).toContain('Spread_0: func() map[string]any {')
|
|
817
|
+
expect(types).toContain('if bf.Truthy(in.DescribedBy) {')
|
|
818
|
+
expect(types).toContain('return map[string]any{"aria-describedby": in.DescribedBy}')
|
|
819
|
+
expect(types).toContain('return map[string]any{}')
|
|
820
|
+
})
|
|
821
|
+
|
|
822
|
+
test('resolves the object value reference and key for a second prop', () => {
|
|
823
|
+
const source = `
|
|
824
|
+
function Box({ label }: { label: string }) {
|
|
825
|
+
return <div {...(label ? { 'data-label': label } : {})} />
|
|
826
|
+
}
|
|
827
|
+
`
|
|
828
|
+
const { types } = compileAndGenerate(source)
|
|
829
|
+
// The condition prop and the value reference resolve to `in.<Field>`,
|
|
830
|
+
// the static key is preserved. (The analyzer surfaces these
|
|
831
|
+
// destructured props as `unknown`/`interface{}`, so the condition
|
|
832
|
+
// routes through `bf.Truthy` for a faithful JS truthiness test.)
|
|
833
|
+
expect(types).toContain('if bf.Truthy(in.Label) {')
|
|
834
|
+
expect(types).toContain('return map[string]any{"data-label": in.Label}')
|
|
835
|
+
})
|
|
836
|
+
|
|
837
|
+
test('refuses a non-identifier condition with BF101 (out-of-shape fallback)', () => {
|
|
838
|
+
const adapter = new GoTemplateAdapter()
|
|
839
|
+
const source = `
|
|
840
|
+
function Box({ a, b }: { a?: string; b?: string }) {
|
|
841
|
+
return <div {...(a === b ? { 'data-x': a } : {})} />
|
|
842
|
+
}
|
|
843
|
+
`
|
|
844
|
+
const ir = compileToIR(source, adapter)
|
|
845
|
+
adapter.generate(ir)
|
|
846
|
+
const errs = (adapter as unknown as { errors: { code: string }[] }).errors
|
|
847
|
+
expect(errs.some(e => e.code === 'BF101')).toBe(true)
|
|
848
|
+
})
|
|
849
|
+
})
|
|
850
|
+
|
|
851
|
+
describe('local-const conditional-spread resolution (#checkbox icon)', () => {
|
|
852
|
+
// A FUNCTION-scope const holding a `cond ? {…} : {}` ternary, then
|
|
853
|
+
// spread as a bare identifier (`{...sizeAttrs}`), resolves through the
|
|
854
|
+
// same conditional-spread lowering as the inline form. CheckIcon's
|
|
855
|
+
// `const sizeAttrs = size ? {…} : {}` is exactly this shape.
|
|
856
|
+
test('resolves a bare-identifier spread of a function-scope conditional const', () => {
|
|
857
|
+
const source = `
|
|
858
|
+
function Box({ flag }: { flag?: boolean }) {
|
|
859
|
+
const attrs = flag ? { 'data-on': 'yes' } : {}
|
|
860
|
+
return <div {...attrs} />
|
|
861
|
+
}
|
|
862
|
+
`
|
|
863
|
+
const { template, types } = compileAndGenerate(source)
|
|
864
|
+
expect(template).toContain('{{bf_spread_attrs .Spread_0}}')
|
|
865
|
+
expect(types).toContain('Spread_0: func() map[string]any {')
|
|
866
|
+
expect(types).toContain('return map[string]any{"data-on": "yes"}')
|
|
867
|
+
expect(types).toContain('return map[string]any{}')
|
|
868
|
+
})
|
|
869
|
+
|
|
870
|
+
// A const that resolves to another bare identifier must NOT be
|
|
871
|
+
// forwarded (loop guard) — it falls through to BF101 like any other
|
|
872
|
+
// unsupported spread identifier.
|
|
873
|
+
test('does not forward a const that aliases another identifier (loop guard)', () => {
|
|
874
|
+
const adapter = new GoTemplateAdapter()
|
|
875
|
+
const source = `
|
|
876
|
+
function Box({ other }: { other?: object }) {
|
|
877
|
+
const attrs = other
|
|
878
|
+
return <div {...attrs} />
|
|
879
|
+
}
|
|
880
|
+
`
|
|
881
|
+
const ir = compileToIR(source, adapter)
|
|
882
|
+
adapter.generate(ir)
|
|
883
|
+
const errs = (adapter as unknown as { errors: { code: string }[] }).errors
|
|
884
|
+
expect(errs.some(e => e.code === 'BF101')).toBe(true)
|
|
885
|
+
})
|
|
886
|
+
})
|
|
887
|
+
|
|
888
|
+
describe('Record<staticKeys,scalar>[propKey] spread value (#checkbox icon)', () => {
|
|
889
|
+
// `const sizeMap: Record<IconSize, number> = { sm: 16, ... }` indexed
|
|
890
|
+
// by a prop inside a conditional-spread object value lowers to an
|
|
891
|
+
// inline indexed Go map keyed via `fmt.Sprint(in.<Field>)`. This is
|
|
892
|
+
// CheckIcon's `{ width: sizeMap[size], height: sizeMap[size] }` shape.
|
|
893
|
+
test('lowers an indexed module-const map to an inline fmt.Sprint-keyed map and adds the fmt import', () => {
|
|
894
|
+
const source = `
|
|
895
|
+
const sizeMap: Record<string, number> = { sm: 16, md: 20, lg: 24, xl: 32 }
|
|
896
|
+
function Box({ size }: { size?: string }) {
|
|
897
|
+
const attrs = size ? { width: sizeMap[size] } : {}
|
|
898
|
+
return <div {...attrs} />
|
|
899
|
+
}
|
|
900
|
+
`
|
|
901
|
+
const { types } = compileAndGenerate(source)
|
|
902
|
+
expect(types).toContain(
|
|
903
|
+
'map[string]any{"sm": 16, "md": 20, "lg": 24, "xl": 32}[fmt.Sprint(in.Size)]',
|
|
904
|
+
)
|
|
905
|
+
// The `"fmt"` import is emitted only when this lowering fires.
|
|
906
|
+
expect(types).toContain('\t"fmt"')
|
|
907
|
+
})
|
|
908
|
+
|
|
909
|
+
test('lowers string-valued record maps too', () => {
|
|
910
|
+
const source = `
|
|
911
|
+
const labelMap: Record<string, string> = { a: 'Alpha', b: 'Beta' }
|
|
912
|
+
function Box({ k }: { k?: string }) {
|
|
913
|
+
const attrs = k ? { 'data-label': labelMap[k] } : {}
|
|
914
|
+
return <div {...attrs} />
|
|
915
|
+
}
|
|
916
|
+
`
|
|
917
|
+
const { types } = compileAndGenerate(source)
|
|
918
|
+
expect(types).toContain('map[string]any{"a": "Alpha", "b": "Beta"}[fmt.Sprint(in.K)]')
|
|
919
|
+
})
|
|
920
|
+
|
|
921
|
+
// A non-scalar record value (object / array / call) is out of shape:
|
|
922
|
+
// the spread object value can't lower, so the whole spread falls back
|
|
923
|
+
// to BF101 rather than emitting an invalid map.
|
|
924
|
+
test('refuses a non-scalar record value with BF101 (out-of-shape fallback)', () => {
|
|
925
|
+
const adapter = new GoTemplateAdapter()
|
|
926
|
+
const source = `
|
|
927
|
+
const sizeMap: Record<string, object> = { sm: { w: 1 } }
|
|
928
|
+
function Box({ size }: { size?: string }) {
|
|
929
|
+
const attrs = size ? { width: sizeMap[size] } : {}
|
|
930
|
+
return <div {...attrs} />
|
|
931
|
+
}
|
|
932
|
+
`
|
|
933
|
+
const ir = compileToIR(source, adapter)
|
|
934
|
+
adapter.generate(ir)
|
|
935
|
+
const errs = (adapter as unknown as { errors: { code: string }[] }).errors
|
|
936
|
+
expect(errs.some(e => e.code === 'BF101')).toBe(true)
|
|
937
|
+
})
|
|
938
|
+
})
|
|
939
|
+
|
|
940
|
+
describe('props-object inherited-attribute enumeration (#checkbox)', () => {
|
|
941
|
+
// A SolidJS props-object component (`function C(props: P)`) that reads
|
|
942
|
+
// inherited attributes (`props.className` in a memo, `props.id` /
|
|
943
|
+
// `props.disabled` on the root) must expose Input/Props fields for them,
|
|
944
|
+
// even though `propsParams` only enumerates `P`'s own members. Without
|
|
945
|
+
// this the caller's `className: ''` has no field — `unknown field
|
|
946
|
+
// ClassName in struct literal of type CInput`.
|
|
947
|
+
test('reads of props.className / props.id / props.disabled become Input fields', () => {
|
|
948
|
+
const adapter = new GoTemplateAdapter()
|
|
949
|
+
const source = `
|
|
950
|
+
"use client"
|
|
951
|
+
import { createMemo } from "@barefootjs/client"
|
|
952
|
+
interface P { tone?: string }
|
|
953
|
+
export function Widget(props: P) {
|
|
954
|
+
const classes = createMemo(() => \`base \${props.className ?? ''}\`)
|
|
955
|
+
return <button id={props.id} disabled={props.disabled ?? false} class={classes()}>x</button>
|
|
956
|
+
}
|
|
957
|
+
`
|
|
958
|
+
const ir = compileToIR(source, adapter)
|
|
959
|
+
const types = adapter.generateTypes(ir)!
|
|
960
|
+
expect(types).toContain('ClassName string')
|
|
961
|
+
// `id` is a bare-reference optional → interface{} (nillable, omittable).
|
|
962
|
+
expect(types).toContain('ID interface{}')
|
|
963
|
+
expect(types).toContain('Disabled bool')
|
|
964
|
+
})
|
|
965
|
+
|
|
966
|
+
// The className memo's SSR initial value must inline module string consts
|
|
967
|
+
// (incl. `[...].join(' ')`) and resolve `props.className ?? ''` to the
|
|
968
|
+
// ClassName field — not render the historical `0` placeholder.
|
|
969
|
+
test('template-literal className memo inlines consts + props.className field', () => {
|
|
970
|
+
const adapter = new GoTemplateAdapter()
|
|
971
|
+
const source = `
|
|
972
|
+
"use client"
|
|
973
|
+
import { createMemo } from "@barefootjs/client"
|
|
974
|
+
const base = 'a b'
|
|
975
|
+
const states = ['c', 'd'].join(' ')
|
|
976
|
+
interface P { tone?: string }
|
|
977
|
+
export function Widget(props: P) {
|
|
978
|
+
const classes = createMemo(() => \`\${base} \${states} \${props.className ?? ''} tail\`)
|
|
979
|
+
return <button class={classes()}>x</button>
|
|
980
|
+
}
|
|
981
|
+
`
|
|
982
|
+
const types = adapter.generateTypes(compileToIR(source, adapter))!
|
|
983
|
+
expect(types).toContain('Classes: "a b" + " " + "c d" + " " + in.ClassName + " tail"')
|
|
984
|
+
})
|
|
985
|
+
|
|
986
|
+
// A boolean ternary memo (`isChecked = ctrl() ? c() : i()`) renders its
|
|
987
|
+
// SSR zero as `false`, not the int `0`, so `aria-checked={isChecked()}`
|
|
988
|
+
// matches Hono's `aria-checked="false"`.
|
|
989
|
+
test('boolean ternary memo defaults to false, not 0', () => {
|
|
990
|
+
const adapter = new GoTemplateAdapter()
|
|
991
|
+
const source = `
|
|
992
|
+
"use client"
|
|
993
|
+
import { createSignal, createMemo } from "@barefootjs/client"
|
|
994
|
+
export function Toggle(props: { checked?: boolean; defaultChecked?: boolean }) {
|
|
995
|
+
const [internal] = createSignal(props.defaultChecked ?? false)
|
|
996
|
+
const [controlled] = createSignal<boolean | undefined>(props.checked)
|
|
997
|
+
const isControlled = createMemo(() => props.checked !== undefined)
|
|
998
|
+
const isChecked = createMemo(() => isControlled() ? controlled() : internal())
|
|
999
|
+
return <button aria-checked={isChecked()}>x</button>
|
|
1000
|
+
}
|
|
1001
|
+
`
|
|
1002
|
+
const types = adapter.generateTypes(compileToIR(source, adapter))!
|
|
1003
|
+
expect(types).toContain('IsChecked bool')
|
|
1004
|
+
expect(types).toContain('IsChecked: false,')
|
|
1005
|
+
})
|
|
1006
|
+
})
|
|
1007
|
+
|
|
1008
|
+
describe('cross-component child rest-bag routing (#checkbox)', () => {
|
|
1009
|
+
// A parent rendering a child with a non-param attribute whose name isn't a
|
|
1010
|
+
// valid Go identifier (`<CheckIcon data-slot="..."/>`) must route it into
|
|
1011
|
+
// the child's rest bag (`Props: map[string]any{...}`), not a hyphenated
|
|
1012
|
+
// top-level field (`Data-slot:`), when the child has a `...props` rest
|
|
1013
|
+
// spread. Requires the child's shape registered first.
|
|
1014
|
+
test('routes a hyphenated non-param child attr into the child rest bag', () => {
|
|
1015
|
+
const adapter = new GoTemplateAdapter()
|
|
1016
|
+
const childSource = `
|
|
1017
|
+
"use client"
|
|
1018
|
+
export function Leaf({ size, ...props }: { size?: string }) {
|
|
1019
|
+
return <span {...props}>{size}</span>
|
|
1020
|
+
}
|
|
1021
|
+
`
|
|
1022
|
+
const childIr = compileToIR(childSource, adapter)
|
|
1023
|
+
adapter.registerChildComponentShape(childIr)
|
|
1024
|
+
const parentSource = `
|
|
1025
|
+
"use client"
|
|
1026
|
+
import { Leaf } from './leaf'
|
|
1027
|
+
export function Host() {
|
|
1028
|
+
return <div><Leaf data-slot="indicator" size="sm" /></div>
|
|
1029
|
+
}
|
|
1030
|
+
`
|
|
1031
|
+
const types = adapter.generateTypes(compileToIR(parentSource, adapter))!
|
|
1032
|
+
// Non-param hyphenated attr lands in the rest bag, not a Data-slot field.
|
|
1033
|
+
expect(types).not.toContain('Data-slot:')
|
|
1034
|
+
expect(types).toContain('Props: map[string]any{"data-slot": "indicator"}')
|
|
1035
|
+
// A declared param (`size`) still binds as a top-level field.
|
|
1036
|
+
expect(types).toContain('Size: "sm"')
|
|
1037
|
+
})
|
|
1038
|
+
})
|
|
1039
|
+
|
|
1040
|
+
describe('nullish optional-attribute omission (textarea rows)', () => {
|
|
1041
|
+
// An optional, no-default prop whose Go field type resolves to
|
|
1042
|
+
// `interface{}` (nillable) is emitted with a `ne .X nil` guard so an
|
|
1043
|
+
// unset value DROPS the attribute instead of rendering `attr=""` —
|
|
1044
|
+
// matching Hono's nullish-attribute omission. Concrete/defaulted
|
|
1045
|
+
// props are never nil and stay unconditional.
|
|
1046
|
+
test('guards a nillable optional attr with {{if ne .X nil}}', () => {
|
|
1047
|
+
const source = `
|
|
1048
|
+
function C({ rows }: { rows?: number }) {
|
|
1049
|
+
return <textarea rows={rows} />
|
|
1050
|
+
}
|
|
1051
|
+
`
|
|
1052
|
+
const { template } = compileAndGenerate(source)
|
|
1053
|
+
expect(template).toContain('{{if ne .Rows nil}}rows="{{.Rows}}"{{end}}')
|
|
1054
|
+
// Must NOT emit the bare unconditional form.
|
|
1055
|
+
expect(template).not.toMatch(/(?<!if ne \.Rows nil}})rows="\{\{\.Rows\}\}"/)
|
|
1056
|
+
})
|
|
1057
|
+
|
|
1058
|
+
test('leaves a concrete/defaulted attr unconditional (scope did not widen)', () => {
|
|
1059
|
+
const source = `
|
|
1060
|
+
function C({ value = '' }: { value?: string }) {
|
|
1061
|
+
return <textarea value={value} />
|
|
1062
|
+
}
|
|
1063
|
+
`
|
|
1064
|
+
const { template } = compileAndGenerate(source)
|
|
1065
|
+
// `value` has a destructure default → concrete `string` field →
|
|
1066
|
+
// never nil → emitted unconditionally, exactly like Hono's value="".
|
|
1067
|
+
expect(template).toContain('value="{{.Value}}"')
|
|
1068
|
+
expect(template).not.toContain('if ne .Value nil')
|
|
1069
|
+
})
|
|
1070
|
+
})
|
|
1071
|
+
|
|
821
1072
|
describe('loop body outer-scope references (#1677)', () => {
|
|
822
1073
|
test('references an outer signal inside a loop via $ root scope, not the element', () => {
|
|
823
1074
|
// Inside `{{range $_, $t := .Items}}` the dot is rebound to the loop
|