@barefootjs/go-template 0.18.7 → 0.19.1
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/emit-context.d.ts +11 -5
- package/dist/adapter/emit-context.d.ts.map +1 -1
- package/dist/adapter/go-template-adapter.d.ts +45 -6
- package/dist/adapter/go-template-adapter.d.ts.map +1 -1
- package/dist/adapter/index.js +326 -38
- package/dist/adapter/lib/compile-state.d.ts +32 -0
- package/dist/adapter/lib/compile-state.d.ts.map +1 -1
- package/dist/adapter/lib/types.d.ts +9 -0
- package/dist/adapter/lib/types.d.ts.map +1 -1
- package/dist/adapter/memo/memo-compute.d.ts +8 -0
- package/dist/adapter/memo/memo-compute.d.ts.map +1 -1
- package/dist/adapter/props/prop-types.d.ts +97 -0
- package/dist/adapter/props/prop-types.d.ts.map +1 -1
- package/dist/adapter/value/value-lowering.d.ts +9 -1
- package/dist/adapter/value/value-lowering.d.ts.map +1 -1
- package/dist/build.js +326 -38
- package/dist/index.js +326 -38
- package/package.json +3 -3
- package/src/__tests__/derived-state-memo.test.ts +10 -3
- package/src/__tests__/go-template-adapter.test.ts +282 -23
- package/src/adapter/emit-context.ts +14 -5
- package/src/adapter/go-template-adapter.ts +291 -50
- package/src/adapter/lib/compile-state.ts +36 -0
- package/src/adapter/lib/types.ts +9 -0
- package/src/adapter/memo/memo-compute.ts +123 -9
- package/src/adapter/props/prop-types.ts +310 -1
- package/src/adapter/value/value-lowering.ts +58 -5
- package/src/test-render.ts +18 -4
|
@@ -119,6 +119,7 @@ runAdapterConformanceTests({
|
|
|
119
119
|
// (#1897) data-table no longer skipped — loop body children + wrapper
|
|
120
120
|
// struct + block-body memo baking render correctly on Go.
|
|
121
121
|
]),
|
|
122
|
+
skipDataPoints: new Set<string>(),
|
|
122
123
|
onRenderError: (err, id) => {
|
|
123
124
|
if (err instanceof GoNotAvailableError) {
|
|
124
125
|
console.log(`Skipping [${id}]: ${err.message}`)
|
|
@@ -506,9 +507,11 @@ export function Counter(props: { initial?: number }) {
|
|
|
506
507
|
expect(result.types).toBeDefined()
|
|
507
508
|
const types = result.types!
|
|
508
509
|
|
|
509
|
-
//
|
|
510
|
-
|
|
511
|
-
expect(types).
|
|
510
|
+
// The fallback applies only when the prop is ABSENT: an explicit
|
|
511
|
+
// `Initial: 0` is honoured (JS `0 ?? 99` is `0`, #2248).
|
|
512
|
+
expect(types).toContain('Initial interface{}')
|
|
513
|
+
expect(types).toContain('var initial int = 99')
|
|
514
|
+
expect(types).toMatch(/if in\.Initial != nil \{\s*initial = bf\.ToInt\(in\.Initial\)\s*\}/)
|
|
512
515
|
|
|
513
516
|
// Prop, signal, and memo all reference the hoisted variable.
|
|
514
517
|
expect(types).toContain('Initial: initial,')
|
|
@@ -553,8 +556,11 @@ export function Label(props: { label?: string }) {
|
|
|
553
556
|
`)
|
|
554
557
|
const result = adapter.generate(ir)
|
|
555
558
|
const types = result.types!
|
|
556
|
-
|
|
557
|
-
|
|
559
|
+
// An explicit `Label: ""` input stays empty (JS `'' ?? 'Default'` is
|
|
560
|
+
// `''`, #2248).
|
|
561
|
+
expect(types).toContain('Label interface{}')
|
|
562
|
+
expect(types).toContain('var label string = "Default"')
|
|
563
|
+
expect(types).toMatch(/if in\.Label != nil \{\s*label = in\.Label\.\(string\)\s*\}/)
|
|
558
564
|
expect(types).toContain('Label: label,')
|
|
559
565
|
// Signal name `text` differs from prop name `label`, so the
|
|
560
566
|
// signal field gets its own entry that resolves through the
|
|
@@ -562,12 +568,11 @@ export function Label(props: { label?: string }) {
|
|
|
562
568
|
expect(types).toContain('Text: label,')
|
|
563
569
|
})
|
|
564
570
|
|
|
565
|
-
test('hoists `props.X ?? true` against
|
|
566
|
-
// Bool-true falls through the same hoist path as int / string
|
|
567
|
-
//
|
|
568
|
-
//
|
|
569
|
-
//
|
|
570
|
-
// memo can inherit it.
|
|
571
|
+
test('hoists `props.X ?? true` against nil (#1423 review, #2248)', () => {
|
|
572
|
+
// Bool-true falls through the same hoist path as int / string. The
|
|
573
|
+
// interface{} field is nil only when the caller omitted the prop, so
|
|
574
|
+
// an explicit `Checked: false` survives (JS `false ?? true` is
|
|
575
|
+
// `false`).
|
|
571
576
|
const adapter = new GoTemplateAdapter()
|
|
572
577
|
const ir = compileToIR(`
|
|
573
578
|
"use client"
|
|
@@ -580,12 +585,124 @@ export function Check(props: { checked?: boolean }) {
|
|
|
580
585
|
`)
|
|
581
586
|
const result = adapter.generate(ir)
|
|
582
587
|
const types = result.types!
|
|
583
|
-
expect(types).toContain('
|
|
584
|
-
expect(types).
|
|
588
|
+
expect(types).toContain('Checked interface{}')
|
|
589
|
+
expect(types).toContain('var checked bool = true')
|
|
590
|
+
expect(types).toMatch(/if in\.Checked != nil \{\s*checked = in\.Checked\.\(bool\)\s*\}/)
|
|
585
591
|
expect(types).toContain('Checked: checked,')
|
|
586
592
|
expect(types).toContain('C: checked,')
|
|
587
593
|
})
|
|
588
594
|
|
|
595
|
+
test('hoists the DESTRUCTURED `x ?? N` seed via signal.parsed (#2259)', () => {
|
|
596
|
+
// Destructured components have no `propsObjectName`, so the seed's
|
|
597
|
+
// prop reference is a bare identifier — matched structurally on the
|
|
598
|
+
// signal's ParsedExpr, then routed through the same nullish flip +
|
|
599
|
+
// hoisted-var machinery as `props.size ?? 1` (#2248/#2252 parity).
|
|
600
|
+
const adapter = new GoTemplateAdapter()
|
|
601
|
+
const ir = compileToIR(`
|
|
602
|
+
"use client"
|
|
603
|
+
import { createSignal } from "@barefootjs/client"
|
|
604
|
+
|
|
605
|
+
export function Counter({ size }: { size?: number }) {
|
|
606
|
+
const [count, setCount] = createSignal(size ?? 1)
|
|
607
|
+
return <div>{count()}</div>
|
|
608
|
+
}
|
|
609
|
+
`)
|
|
610
|
+
const result = adapter.generate(ir)
|
|
611
|
+
const types = result.types!
|
|
612
|
+
expect(types).toContain('Size interface{}')
|
|
613
|
+
expect(types).toContain('var size int = 1')
|
|
614
|
+
expect(types).toMatch(/if in\.Size != nil \{\s*size = bf\.ToInt\(in\.Size\)\s*\}/)
|
|
615
|
+
expect(types).toContain('Size: size,')
|
|
616
|
+
expect(types).toContain('Count: size,')
|
|
617
|
+
})
|
|
618
|
+
|
|
619
|
+
test('destructured `x ?? 0` stays concrete and seeds the prop directly (#2259)', () => {
|
|
620
|
+
// A zero-equivalent fallback earns no nillable flip (nil and the Go
|
|
621
|
+
// zero value land on the same output), so the field keeps its scalar
|
|
622
|
+
// type and the signal seeds straight off the input — NOT the literal
|
|
623
|
+
// `0` the pre-#2259 lowering emitted for the unrecognized identifier
|
|
624
|
+
// form.
|
|
625
|
+
const adapter = new GoTemplateAdapter()
|
|
626
|
+
const ir = compileToIR(`
|
|
627
|
+
"use client"
|
|
628
|
+
import { createSignal } from "@barefootjs/client"
|
|
629
|
+
|
|
630
|
+
export function Counter({ size }: { size?: number }) {
|
|
631
|
+
const [count, setCount] = createSignal(size ?? 0)
|
|
632
|
+
return <div>{count()}</div>
|
|
633
|
+
}
|
|
634
|
+
`)
|
|
635
|
+
const result = adapter.generate(ir)
|
|
636
|
+
const types = result.types!
|
|
637
|
+
expect(types).toContain('Size int')
|
|
638
|
+
expect(types).not.toContain('Size interface{}')
|
|
639
|
+
expect(types).toContain('Size: in.Size,')
|
|
640
|
+
expect(types).toContain('Count: in.Size,')
|
|
641
|
+
})
|
|
642
|
+
|
|
643
|
+
test('negative fallback (`?? -1`) hoists in both prop styles (#2259 review)', () => {
|
|
644
|
+
// `-1` parses as unary minus around a number literal, not a literal —
|
|
645
|
+
// the structural seed match must reconstruct the sign, in the
|
|
646
|
+
// props-object form (where it preempts the regex path) and the
|
|
647
|
+
// destructured form alike.
|
|
648
|
+
for (const [params, ref] of [
|
|
649
|
+
['props: { size?: number }', 'props.size'],
|
|
650
|
+
['{ size }: { size?: number }', 'size'],
|
|
651
|
+
]) {
|
|
652
|
+
const adapter = new GoTemplateAdapter()
|
|
653
|
+
const ir = compileToIR(`
|
|
654
|
+
"use client"
|
|
655
|
+
import { createSignal } from "@barefootjs/client"
|
|
656
|
+
|
|
657
|
+
export function Counter(${params}) {
|
|
658
|
+
const [count, setCount] = createSignal(${ref} ?? -1)
|
|
659
|
+
return <div>{count()}</div>
|
|
660
|
+
}
|
|
661
|
+
`, adapter)
|
|
662
|
+
const types = adapter.generate(ir).types!
|
|
663
|
+
expect(types).toContain('Size interface{}')
|
|
664
|
+
expect(types).toContain('var size int = -1')
|
|
665
|
+
expect(types).toContain('Count: size,')
|
|
666
|
+
}
|
|
667
|
+
})
|
|
668
|
+
|
|
669
|
+
test('string fallback with quotes survives the structural match unmangled (#2259 review)', () => {
|
|
670
|
+
const adapter = new GoTemplateAdapter()
|
|
671
|
+
const ir = compileToIR(`
|
|
672
|
+
"use client"
|
|
673
|
+
import { createSignal } from "@barefootjs/client"
|
|
674
|
+
|
|
675
|
+
export function Label({ label }: { label?: string }) {
|
|
676
|
+
const [text, setText] = createSignal(label ?? 'say "hi"')
|
|
677
|
+
return <div>{text()}</div>
|
|
678
|
+
}
|
|
679
|
+
`, adapter)
|
|
680
|
+
const types = adapter.generate(ir).types!
|
|
681
|
+
expect(types).toContain('var label string = "say \\"hi\\""')
|
|
682
|
+
expect(types).not.toContain('\\\\')
|
|
683
|
+
})
|
|
684
|
+
|
|
685
|
+
test('destructure DEFAULT keeps the concrete-typed applyGoFallback baking (#2259)', () => {
|
|
686
|
+
// `{ size = 5 }` never sees a nullish binding in JS (the default
|
|
687
|
+
// already applied), so it must stay excluded from the nillable flip
|
|
688
|
+
// and keep the zero-check baking on the concrete field.
|
|
689
|
+
const adapter = new GoTemplateAdapter()
|
|
690
|
+
const ir = compileToIR(`
|
|
691
|
+
"use client"
|
|
692
|
+
import { createSignal } from "@barefootjs/client"
|
|
693
|
+
|
|
694
|
+
export function Counter({ size = 5 }: { size?: number }) {
|
|
695
|
+
const [count, setCount] = createSignal(size)
|
|
696
|
+
return <div>{count()}</div>
|
|
697
|
+
}
|
|
698
|
+
`)
|
|
699
|
+
const result = adapter.generate(ir)
|
|
700
|
+
const types = result.types!
|
|
701
|
+
expect(types).toContain('Size int')
|
|
702
|
+
expect(types).not.toContain('Size interface{}')
|
|
703
|
+
expect(types).toContain('Size: func() int { if in.Size == 0 { return 5 }; return in.Size }(),')
|
|
704
|
+
})
|
|
705
|
+
|
|
589
706
|
test('skips hoist for zero-equivalent string and float fallbacks (#1423 review)', () => {
|
|
590
707
|
// The skip predicate compares the Go fallback against the
|
|
591
708
|
// type's zero literal — covers `?? ''` (string) and `?? 0.0`
|
|
@@ -979,10 +1096,13 @@ function Box({ describedBy }: { describedBy?: string }) {
|
|
|
979
1096
|
// Template emission is unchanged from the proven {...props} path.
|
|
980
1097
|
expect(template).toContain('{{bf_spread_attrs .Spread_0}}')
|
|
981
1098
|
// The bag value is a conditional map built in NewBoxProps. The
|
|
982
|
-
// prop
|
|
983
|
-
//
|
|
1099
|
+
// optional prop resolves to a concrete `string` (#2259) — it is
|
|
1100
|
+
// consumed only inside the spread condition, not as a bare attr, so
|
|
1101
|
+
// no nillable flip — and the field-typed condition is a faithful
|
|
1102
|
+
// `!= ""` truthiness test rather than reflection: JS string
|
|
1103
|
+
// truthiness IS non-emptiness, and an absent prop zeroes to `""`.
|
|
984
1104
|
expect(types).toContain('Spread_0: func() map[string]any {')
|
|
985
|
-
expect(types).toContain('if
|
|
1105
|
+
expect(types).toContain('if in.DescribedBy != "" {')
|
|
986
1106
|
expect(types).toContain('return map[string]any{"aria-describedby": in.DescribedBy}')
|
|
987
1107
|
expect(types).toContain('return map[string]any{}')
|
|
988
1108
|
})
|
|
@@ -1151,10 +1271,15 @@ export function Widget(props: P) {
|
|
|
1151
1271
|
expect(types).toContain('Classes: "a b" + " " + "c d" + " " + in.ClassName + " tail"')
|
|
1152
1272
|
})
|
|
1153
1273
|
|
|
1154
|
-
// A boolean ternary memo (`isChecked = ctrl() ? c() : i()`)
|
|
1155
|
-
// SSR
|
|
1156
|
-
// matches Hono's `aria-checked="false"
|
|
1157
|
-
|
|
1274
|
+
// A boolean ternary memo (`isChecked = ctrl() ? c() : i()`) types its
|
|
1275
|
+
// SSR field as `bool` (not `int`), so `aria-checked={isChecked()}`
|
|
1276
|
+
// matches Hono's `aria-checked="false"` shape. Since #2260, `checked`'s
|
|
1277
|
+
// presence is expressible (nillable `interface{}`), so the constructor
|
|
1278
|
+
// bakes a runtime presence check + type-asserted read instead of the
|
|
1279
|
+
// pre-#2260 unconditional `false` — see the `Controlled/derived
|
|
1280
|
+
// boolean props honour a caller-supplied true` describe block below for
|
|
1281
|
+
// the caller-supplied-`true` case this unlocks.
|
|
1282
|
+
test('boolean ternary memo types as bool, resolves controlled presence at SSR', () => {
|
|
1158
1283
|
const adapter = new GoTemplateAdapter()
|
|
1159
1284
|
const source = `
|
|
1160
1285
|
"use client"
|
|
@@ -1169,7 +1294,9 @@ export function Toggle(props: { checked?: boolean; defaultChecked?: boolean }) {
|
|
|
1169
1294
|
`
|
|
1170
1295
|
const types = adapter.generateTypes(compileToIR(source, adapter))!
|
|
1171
1296
|
expect(types).toContain('IsChecked bool')
|
|
1172
|
-
expect(types).toContain(
|
|
1297
|
+
expect(types).toContain(
|
|
1298
|
+
'IsChecked: func() bool { if in.Checked != nil { return func() bool { if v, ok := in.Checked.(bool); ok { return v }; return false }() }; return in.DefaultChecked }(),',
|
|
1299
|
+
)
|
|
1173
1300
|
})
|
|
1174
1301
|
})
|
|
1175
1302
|
|
|
@@ -3631,8 +3758,9 @@ export function C(props: Props) {
|
|
|
3631
3758
|
const template = result.files?.find(f => f.path.endsWith('.tmpl'))?.content ?? ''
|
|
3632
3759
|
// The `{}` fallback lowers to the safe `""` Go string sentinel — never the
|
|
3633
3760
|
// `[UNSUPPORTED: …]` marker text, which would break `text/template` parsing
|
|
3634
|
-
// once spliced as an
|
|
3635
|
-
|
|
3761
|
+
// once spliced as an operand. `??` on a nillable prop lowers to the
|
|
3762
|
+
// nil-testing `bf_nullish` (#2248), equally valid template syntax.
|
|
3763
|
+
expect(template).toContain('{{bf_nullish .Config ""}}')
|
|
3636
3764
|
expect(template).not.toContain('UNSUPPORTED')
|
|
3637
3765
|
})
|
|
3638
3766
|
|
|
@@ -4569,3 +4697,134 @@ export function List() {
|
|
|
4569
4697
|
}
|
|
4570
4698
|
})
|
|
4571
4699
|
})
|
|
4700
|
+
|
|
4701
|
+
// #2254: `??` in CONDITION position (ternaries/`{{if}}` via
|
|
4702
|
+
// `convertConditionToGo` → `renderConditionExpr`'s `logical` case) still
|
|
4703
|
+
// emitted Go's truthiness-based `or` even for a nillable-lowered prop, so a
|
|
4704
|
+
// present-but-empty (`''`) value wrongly fell back to the `??` default —
|
|
4705
|
+
// diverging from JS, where `??` only falls back on null/undefined. The
|
|
4706
|
+
// sibling `logical()` emitter (used for non-condition expression positions,
|
|
4707
|
+
// e.g. plain interpolation) already routed nillable operands through
|
|
4708
|
+
// `bf_nullish` since #2248/#2252; this brings the condition-position
|
|
4709
|
+
// emitter in line with it.
|
|
4710
|
+
describe('GoTemplateAdapter - #2254 `??` in condition position on a nillable prop', () => {
|
|
4711
|
+
test('ternary test comparing `props.label ?? "Default"` lowers to bf_nullish, not or', () => {
|
|
4712
|
+
const adapter = new GoTemplateAdapter()
|
|
4713
|
+
const result = compileJSX(`
|
|
4714
|
+
"use client"
|
|
4715
|
+
type Props = { label?: string }
|
|
4716
|
+
export function C(props: Props) {
|
|
4717
|
+
return <div>{(props.label ?? 'Default') === 'Default' ? <span>fallback</span> : <span>set</span>}</div>
|
|
4718
|
+
}
|
|
4719
|
+
`.trimStart(), 'test.tsx', { adapter })
|
|
4720
|
+
expect(result.errors ?? []).toEqual([])
|
|
4721
|
+
const template = result.files?.find(f => f.path.endsWith('.tmpl'))?.content ?? ''
|
|
4722
|
+
expect(template).toContain('bf_nullish .Label "Default"')
|
|
4723
|
+
expect(template).not.toContain('or .Label "Default"')
|
|
4724
|
+
})
|
|
4725
|
+
|
|
4726
|
+
test('end-to-end via real `go run`: a present-but-empty label does NOT take the ?? default branch', async () => {
|
|
4727
|
+
try {
|
|
4728
|
+
const html = await renderGoTemplateComponent({
|
|
4729
|
+
source: `
|
|
4730
|
+
'use client'
|
|
4731
|
+
type Props = { label?: string }
|
|
4732
|
+
export function C(props: Props) {
|
|
4733
|
+
return <div>{(props.label ?? 'Default') === 'Default' ? <span>fallback</span> : <span>set</span>}</div>
|
|
4734
|
+
}
|
|
4735
|
+
`,
|
|
4736
|
+
adapter: new GoTemplateAdapter(),
|
|
4737
|
+
props: { label: '' },
|
|
4738
|
+
})
|
|
4739
|
+
// JS: '' ?? 'Default' keeps '' (present, not nullish) → '' === 'Default'
|
|
4740
|
+
// is false → the "set" branch renders. The pre-fix `or` lowering would
|
|
4741
|
+
// truthiness-fallback the empty string to 'Default' and wrongly render
|
|
4742
|
+
// "fallback" instead.
|
|
4743
|
+
expect(html).toContain('>set</span>')
|
|
4744
|
+
expect(html).not.toContain('>fallback</span>')
|
|
4745
|
+
} catch (err) {
|
|
4746
|
+
if (err instanceof GoNotAvailableError) {
|
|
4747
|
+
console.log('Skipping #2254 e2e: go command not found')
|
|
4748
|
+
return
|
|
4749
|
+
}
|
|
4750
|
+
throw err
|
|
4751
|
+
}
|
|
4752
|
+
})
|
|
4753
|
+
|
|
4754
|
+
test('an omitted label DOES take the ?? default branch', async () => {
|
|
4755
|
+
try {
|
|
4756
|
+
const html = await renderGoTemplateComponent({
|
|
4757
|
+
source: `
|
|
4758
|
+
'use client'
|
|
4759
|
+
type Props = { label?: string }
|
|
4760
|
+
export function C(props: Props) {
|
|
4761
|
+
return <div>{(props.label ?? 'Default') === 'Default' ? <span>fallback</span> : <span>set</span>}</div>
|
|
4762
|
+
}
|
|
4763
|
+
`,
|
|
4764
|
+
adapter: new GoTemplateAdapter(),
|
|
4765
|
+
props: {},
|
|
4766
|
+
})
|
|
4767
|
+
expect(html).toContain('>fallback</span>')
|
|
4768
|
+
expect(html).not.toContain('>set</span>')
|
|
4769
|
+
} catch (err) {
|
|
4770
|
+
if (err instanceof GoNotAvailableError) {
|
|
4771
|
+
console.log('Skipping #2254 e2e: go command not found')
|
|
4772
|
+
return
|
|
4773
|
+
}
|
|
4774
|
+
throw err
|
|
4775
|
+
}
|
|
4776
|
+
})
|
|
4777
|
+
})
|
|
4778
|
+
|
|
4779
|
+
// Review finding on #2271 (the #2254 fix): `renderConditionExpr`'s `logical`
|
|
4780
|
+
// case wrapped operands via `needsParens` (AST-kind-based — only
|
|
4781
|
+
// `logical`/`unary`/`conditional`), missing any OTHER multi-token rendering
|
|
4782
|
+
// (`len .X`, `bf_add a b`, a call). Unwrapped, Go's `and`/`or`/`bf_nullish`
|
|
4783
|
+
// (all prefix builtins) parse a multi-token operand as extra sibling args
|
|
4784
|
+
// instead of one operand. Switched to `wrapIfMultiToken` (whitespace-based,
|
|
4785
|
+
// on the rendered string), matching the main `logical()` emitter.
|
|
4786
|
+
describe('GoTemplateAdapter - condition-position logical operand wrapping (#2271 review)', () => {
|
|
4787
|
+
test('a `.length` (member → "len .X") operand of `&&` inside a ternary TEST is parenthesized', () => {
|
|
4788
|
+
// A bare `{cond && <Elem/>}` is recognized as JSX conditional-rendering
|
|
4789
|
+
// shorthand and its test is extracted directly, never reaching
|
|
4790
|
+
// `renderConditionExpr`'s `logical` case as a literal `&&` node — so the
|
|
4791
|
+
// `&&` must be nested inside an explicit ternary test to exercise it.
|
|
4792
|
+
const adapter = new GoTemplateAdapter()
|
|
4793
|
+
const result = compileJSX(`
|
|
4794
|
+
"use client"
|
|
4795
|
+
type Item = { id: number }
|
|
4796
|
+
export function C({ items, enabled }: { items: Item[]; enabled: boolean }) {
|
|
4797
|
+
return <div>{(items.length && enabled) ? <span>on</span> : <span>off</span>}</div>
|
|
4798
|
+
}
|
|
4799
|
+
`.trimStart(), 'test.tsx', { adapter })
|
|
4800
|
+
expect(result.errors ?? []).toEqual([])
|
|
4801
|
+
const template = result.files?.find(f => f.path.endsWith('.tmpl'))?.content ?? ''
|
|
4802
|
+
// `and (len .Items) .Enabled` — NOT the broken 3-sibling-arg
|
|
4803
|
+
// `and len .Items .Enabled` a bare `needsParens` miss would produce.
|
|
4804
|
+
expect(template).toContain('and (len .Items) .Enabled')
|
|
4805
|
+
})
|
|
4806
|
+
|
|
4807
|
+
test('end-to-end via real `go run`: the multi-token `&&` ternary-test operand compiles and renders correctly', async () => {
|
|
4808
|
+
try {
|
|
4809
|
+
const html = await renderGoTemplateComponent({
|
|
4810
|
+
source: `
|
|
4811
|
+
'use client'
|
|
4812
|
+
type Item = { id: number }
|
|
4813
|
+
export function C({ items, enabled }: { items: Item[]; enabled: boolean }) {
|
|
4814
|
+
return <div>{(items.length && enabled) ? <span>on</span> : <span>off</span>}</div>
|
|
4815
|
+
}
|
|
4816
|
+
`,
|
|
4817
|
+
adapter: new GoTemplateAdapter(),
|
|
4818
|
+
props: { items: [{ id: 1 }], enabled: true },
|
|
4819
|
+
})
|
|
4820
|
+
expect(html).toContain('>on</span>')
|
|
4821
|
+
expect(html).not.toContain('>off</span>')
|
|
4822
|
+
} catch (err) {
|
|
4823
|
+
if (err instanceof GoNotAvailableError) {
|
|
4824
|
+
console.log('Skipping #2271 review e2e: go command not found')
|
|
4825
|
+
return
|
|
4826
|
+
}
|
|
4827
|
+
throw err
|
|
4828
|
+
}
|
|
4829
|
+
})
|
|
4830
|
+
})
|
|
@@ -41,14 +41,23 @@ export interface GoEmitContext {
|
|
|
41
41
|
preParsed?: ParsedExpr,
|
|
42
42
|
): { condition: string; preamble: string }
|
|
43
43
|
|
|
44
|
-
/**
|
|
45
|
-
|
|
44
|
+
/**
|
|
45
|
+
* Extract the prop name from a `props.X ?? …` initial value — or, given
|
|
46
|
+
* `preParsed` in a destructured component, an `x ?? …` identifier form —
|
|
47
|
+
* or null. Callers validate the name against `propsParams`.
|
|
48
|
+
*/
|
|
49
|
+
extractPropNameFromInitialValue(initialValue: string, preParsed?: ParsedExpr): string | null
|
|
46
50
|
|
|
47
51
|
/**
|
|
48
|
-
* Parse a signal-time initial value `props.X ?? <literal>`
|
|
49
|
-
*
|
|
52
|
+
* Parse a signal-time initial value `props.X ?? <literal>` (or the
|
|
53
|
+
* destructured `x ?? <literal>` form when `preParsed` is given) into the
|
|
54
|
+
* source prop name and the Go-formatted fallback, or null when it isn't
|
|
55
|
+
* that shape. Callers validate the name against `propsParams`.
|
|
50
56
|
*/
|
|
51
|
-
extractPropFallback(
|
|
57
|
+
extractPropFallback(
|
|
58
|
+
initialValue: string,
|
|
59
|
+
preParsed?: ParsedExpr,
|
|
60
|
+
): { propName: string; goFallback: string } | null
|
|
52
61
|
|
|
53
62
|
/**
|
|
54
63
|
* Inline a module string const by name as a Go double-quoted literal
|