@barefootjs/go-template 0.33.4 → 0.34.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/emit-context.d.ts +12 -0
- package/dist/adapter/emit-context.d.ts.map +1 -1
- package/dist/adapter/expr/url-builder.d.ts +53 -1
- package/dist/adapter/expr/url-builder.d.ts.map +1 -1
- package/dist/adapter/go-template-adapter.d.ts +153 -7
- package/dist/adapter/go-template-adapter.d.ts.map +1 -1
- package/dist/adapter/index.js +253 -99
- package/dist/adapter/lib/compile-state.d.ts +17 -0
- package/dist/adapter/lib/compile-state.d.ts.map +1 -1
- package/dist/adapter/value/parsed-literal-to-go.d.ts.map +1 -1
- package/dist/adapter/value/value-lowering.d.ts.map +1 -1
- package/dist/conformance-pins.d.ts.map +1 -1
- package/dist/index.js +261 -103
- package/dist/render-divergences.d.ts +10 -0
- package/dist/render-divergences.d.ts.map +1 -1
- package/dist/vite.js +420 -214
- package/package.json +5 -5
- package/src/__tests__/go-template-adapter.test.ts +333 -9
- package/src/__tests__/lowering-plugin.test.ts +38 -0
- package/src/__tests__/query-href.test.ts +126 -0
- package/src/adapter/emit-context.ts +14 -0
- package/src/adapter/expr/url-builder.ts +114 -8
- package/src/adapter/go-template-adapter.ts +514 -107
- package/src/adapter/lib/compile-state.ts +18 -0
- package/src/adapter/value/parsed-literal-to-go.ts +11 -7
- package/src/adapter/value/value-lowering.ts +17 -0
- package/src/conformance-pins.ts +19 -0
- package/src/render-divergences.ts +12 -6
- package/src/test-render.ts +32 -15
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@barefootjs/go-template",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.34.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",
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
"directory": "packages/adapter-go-template"
|
|
50
50
|
},
|
|
51
51
|
"dependencies": {
|
|
52
|
-
"@barefootjs/shared": "0.
|
|
52
|
+
"@barefootjs/shared": "0.34.0"
|
|
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.
|
|
71
|
-
"@barefootjs/jsx": "0.
|
|
72
|
-
"@barefootjs/vite": "0.
|
|
70
|
+
"@barefootjs/client": "0.34.0",
|
|
71
|
+
"@barefootjs/jsx": "0.34.0",
|
|
72
|
+
"@barefootjs/vite": "0.34.0",
|
|
73
73
|
"vite": "^6.0.0"
|
|
74
74
|
}
|
|
75
75
|
}
|
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
} from '@barefootjs/jsx'
|
|
19
19
|
import { conformancePins } from '../conformance-pins'
|
|
20
20
|
import { renderDivergences } from '../render-divergences'
|
|
21
|
+
import { findNestedComponents } from '../adapter/analysis/component-tree.ts'
|
|
21
22
|
|
|
22
23
|
runAdapterConformanceTests({
|
|
23
24
|
name: 'go-template',
|
|
@@ -128,14 +129,6 @@ runAdapterConformanceTests({
|
|
|
128
129
|
// produces no complete template.
|
|
129
130
|
'jsx-element-prop-rest-bag-dynamic',
|
|
130
131
|
]),
|
|
131
|
-
skipDataPoints: new Set<string>([
|
|
132
|
-
// #2743: html/template's URL-context autoescape percent-encodes the
|
|
133
|
-
// queryHref BASE in href position (`日本語` → `%e6%97%a5…`); the JS
|
|
134
|
-
// reference only HTML-escapes. The bf_query helper itself is faithful —
|
|
135
|
-
// the divergence is Go's contextual escaper on the whole href value.
|
|
136
|
-
'query-href:gen:base:markup',
|
|
137
|
-
'query-href:gen:base:multibyte',
|
|
138
|
-
]),
|
|
139
132
|
onRenderError: (err, id) => {
|
|
140
133
|
if (err instanceof GoNotAvailableError) {
|
|
141
134
|
console.log(`Skipping [${id}]: ${err.message}`)
|
|
@@ -987,7 +980,9 @@ export function List() {
|
|
|
987
980
|
|
|
988
981
|
test('keeps nil for an untyped object array with non-scalar values (#1680)', () => {
|
|
989
982
|
// A nested object/array value has no scalar Go type to infer, so the
|
|
990
|
-
// shape can't be synthesised — bail to nil.
|
|
983
|
+
// shape can't be synthesised — bail to nil. `tags` is a nested array
|
|
984
|
+
// of SCALARS (not objects) — #2800 only teaches synthesis to recurse
|
|
985
|
+
// into an array of OBJECT literals, so this shape is unchanged.
|
|
991
986
|
const adapter = new GoTemplateAdapter()
|
|
992
987
|
const ir = compileToIR(`
|
|
993
988
|
"use client"
|
|
@@ -1001,6 +996,85 @@ export function List() {
|
|
|
1001
996
|
expect(adapter.generate(ir).types!).toContain('Items: nil,')
|
|
1002
997
|
})
|
|
1003
998
|
|
|
999
|
+
// #2800: a nested array-of-objects field (`children: [{...}]`) recurses
|
|
1000
|
+
// into its own synthesized struct instead of aborting the whole
|
|
1001
|
+
// signal's synthesis.
|
|
1002
|
+
test('synthesises a nested struct for a nested array-of-objects field and bakes it (#2800)', () => {
|
|
1003
|
+
const adapter = new GoTemplateAdapter()
|
|
1004
|
+
const ir = compileToIR(`
|
|
1005
|
+
"use client"
|
|
1006
|
+
import { createSignal } from "@barefootjs/client"
|
|
1007
|
+
|
|
1008
|
+
export function NestedRefConst() {
|
|
1009
|
+
const [items] = createSignal([
|
|
1010
|
+
{ id: 1, label: 'Alpha', children: [{ id: 10, label: 'Alpha-child' }] },
|
|
1011
|
+
{ id: 2, label: 'Beta', children: [{ id: 20, label: 'Beta-child' }] },
|
|
1012
|
+
])
|
|
1013
|
+
return <ul>{items().map((row) => <li key={row.id}>{row.label}</li>)}</ul>
|
|
1014
|
+
}
|
|
1015
|
+
`)
|
|
1016
|
+
const types = adapter.generate(ir).types!
|
|
1017
|
+
expect(types).toContain('type NestedRefConstItemsItemChildrenItem struct')
|
|
1018
|
+
expect(types).toContain('Children []NestedRefConstItemsItemChildrenItem')
|
|
1019
|
+
expect(types).not.toContain('Items: nil,')
|
|
1020
|
+
expect(types).toContain(
|
|
1021
|
+
'Items: []NestedRefConstItemsItem{NestedRefConstItemsItem{ID: 1, Label: "Alpha", Children: []NestedRefConstItemsItemChildrenItem{NestedRefConstItemsItemChildrenItem{ID: 10, Label: "Alpha-child"}}}, NestedRefConstItemsItem{ID: 2, Label: "Beta", Children: []NestedRefConstItemsItemChildrenItem{NestedRefConstItemsItemChildrenItem{ID: 20, Label: "Beta-child"}}}}',
|
|
1022
|
+
)
|
|
1023
|
+
})
|
|
1024
|
+
|
|
1025
|
+
test('recurses to arbitrary depth, widening a numeric field at the innermost level (#2800)', () => {
|
|
1026
|
+
const adapter = new GoTemplateAdapter()
|
|
1027
|
+
const ir = compileToIR(`
|
|
1028
|
+
"use client"
|
|
1029
|
+
import { createSignal } from "@barefootjs/client"
|
|
1030
|
+
|
|
1031
|
+
export function Depth3() {
|
|
1032
|
+
const [items] = createSignal([
|
|
1033
|
+
{ id: 1, kids: [{ id: 10, grand: [{ id: 100, n: 1 }, { id: 101, n: 2.5 }] }] },
|
|
1034
|
+
])
|
|
1035
|
+
return <ul>{items().map((row) => <li key={row.id}>{row.id}</li>)}</ul>
|
|
1036
|
+
}
|
|
1037
|
+
`)
|
|
1038
|
+
const types = adapter.generate(ir).types!
|
|
1039
|
+
expect(types).toContain('type Depth3ItemsItemKidsItemGrandItem struct')
|
|
1040
|
+
expect(types).toMatch(/type Depth3ItemsItemKidsItemGrandItem struct \{\n\tID int[\s\S]*\tN float64/)
|
|
1041
|
+
expect(types).toContain('Kids []Depth3ItemsItemKidsItem')
|
|
1042
|
+
expect(types).toContain('Grand []Depth3ItemsItemKidsItemGrandItem')
|
|
1043
|
+
expect(types).not.toContain('Items: nil,')
|
|
1044
|
+
})
|
|
1045
|
+
|
|
1046
|
+
test('keeps nil when a nested struct name collides with a user type (#2800)', () => {
|
|
1047
|
+
const adapter = new GoTemplateAdapter()
|
|
1048
|
+
const ir = compileToIR(`
|
|
1049
|
+
"use client"
|
|
1050
|
+
import { createSignal } from "@barefootjs/client"
|
|
1051
|
+
|
|
1052
|
+
type ListItemsItemChildrenItem = { id: string }
|
|
1053
|
+
export function List() {
|
|
1054
|
+
const [items] = createSignal([{ id: 1, children: [{ id: 2 }] }])
|
|
1055
|
+
return <ul>{items().map((row) => <li key={row.id}>{row.id}</li>)}</ul>
|
|
1056
|
+
}
|
|
1057
|
+
`)
|
|
1058
|
+
expect(adapter.generate(ir).types!).toContain('Items: nil,')
|
|
1059
|
+
})
|
|
1060
|
+
|
|
1061
|
+
test('keeps nil when a key is a nested array in some rows and scalar in others (#2800)', () => {
|
|
1062
|
+
const adapter = new GoTemplateAdapter()
|
|
1063
|
+
const ir = compileToIR(`
|
|
1064
|
+
"use client"
|
|
1065
|
+
import { createSignal } from "@barefootjs/client"
|
|
1066
|
+
|
|
1067
|
+
export function List() {
|
|
1068
|
+
const [items] = createSignal([
|
|
1069
|
+
{ id: 1, children: [{ id: 2 }] },
|
|
1070
|
+
{ id: 3, children: 'not-an-array' },
|
|
1071
|
+
])
|
|
1072
|
+
return <ul>{items().map((row) => <li key={row.id}>{row.id}</li>)}</ul>
|
|
1073
|
+
}
|
|
1074
|
+
`)
|
|
1075
|
+
expect(adapter.generate(ir).types!).toContain('Items: nil,')
|
|
1076
|
+
})
|
|
1077
|
+
|
|
1004
1078
|
test('widens mixed int/float keys to float64 and keeps negatives (#1680)', () => {
|
|
1005
1079
|
// A key seen as both an integer and a fractional literal across elements
|
|
1006
1080
|
// can't be `int`; widen it to `float64`. Negative numeric literals keep
|
|
@@ -1407,6 +1481,201 @@ export function Widget(props: P) {
|
|
|
1407
1481
|
expect(types).toContain('Classes: "a b" + " " + "c d" + " " + in.ClassName + " tail"')
|
|
1408
1482
|
})
|
|
1409
1483
|
|
|
1484
|
+
// A signal seeded from a bare identifier referencing a module-level
|
|
1485
|
+
// const (`const PAYLOAD = '...'; createSignal(PAYLOAD)`) used to bake
|
|
1486
|
+
// to `nil` — the analyzer types it `unknown` since it never chases an
|
|
1487
|
+
// identifier to its declaration, so none of convertInitialValue's typed
|
|
1488
|
+
// branches saw it (#2794). Now resolved via the same
|
|
1489
|
+
// resolveModuleStringConst/resolveModuleNumericConst the adapter
|
|
1490
|
+
// already used for live template expressions (template-interp.ts).
|
|
1491
|
+
test('signal seeded from a module-level const bakes its literal value, not nil', () => {
|
|
1492
|
+
const adapter = new GoTemplateAdapter()
|
|
1493
|
+
const source = `
|
|
1494
|
+
"use client"
|
|
1495
|
+
import { createSignal } from "@barefootjs/client"
|
|
1496
|
+
const PAYLOAD = 'hello'
|
|
1497
|
+
const WIDTH = 8
|
|
1498
|
+
export function Demo() {
|
|
1499
|
+
const [value] = createSignal(PAYLOAD)
|
|
1500
|
+
const [width] = createSignal(WIDTH)
|
|
1501
|
+
return <textarea value={value()} data-w={width()} />
|
|
1502
|
+
}
|
|
1503
|
+
`
|
|
1504
|
+
const types = adapter.generateTypes(compileToIR(source, adapter))!
|
|
1505
|
+
expect(types).toContain('Value: "hello"')
|
|
1506
|
+
expect(types).toContain('Width: 8')
|
|
1507
|
+
})
|
|
1508
|
+
|
|
1509
|
+
// Same family, boolean shape (#2815 — filed and fixed in the same PR
|
|
1510
|
+
// as #2794 above, since it's the identical resolver-not-wired gap on
|
|
1511
|
+
// a third literal kind rather than a new mechanism).
|
|
1512
|
+
test('signal seeded from a module-level boolean const bakes true/false, not nil', () => {
|
|
1513
|
+
const adapter = new GoTemplateAdapter()
|
|
1514
|
+
const source = `
|
|
1515
|
+
"use client"
|
|
1516
|
+
import { createSignal } from "@barefootjs/client"
|
|
1517
|
+
const OPEN = true
|
|
1518
|
+
export function Demo() {
|
|
1519
|
+
const [open] = createSignal(OPEN)
|
|
1520
|
+
return <div>{open() ? 'y' : 'n'}</div>
|
|
1521
|
+
}
|
|
1522
|
+
`
|
|
1523
|
+
const types = adapter.generateTypes(compileToIR(source, adapter))!
|
|
1524
|
+
expect(types).toContain('Open: true')
|
|
1525
|
+
})
|
|
1526
|
+
|
|
1527
|
+
// #2700: a `derived` signal (non-empty free set, e.g. `{ ...base, done }`)
|
|
1528
|
+
// whose object literal the constructor-time baker can't reproduce
|
|
1529
|
+
// (identifier/member/call operands defer) used to silently keep the Go
|
|
1530
|
+
// zero value for every field the SSR template reads. Loud-ified instead
|
|
1531
|
+
// of taught a live-expression lowering — Go has none — since the
|
|
1532
|
+
// template's `.Merged.ID` / `.Merged.Done` reads would otherwise see a
|
|
1533
|
+
// wrong value with no diagnostic at all.
|
|
1534
|
+
test('signal object-literal spreading a live prop refuses with BF101 when the template reads it (#2700)', () => {
|
|
1535
|
+
const adapter = new GoTemplateAdapter()
|
|
1536
|
+
const result = compileAndGenerate(`
|
|
1537
|
+
"use client"
|
|
1538
|
+
import { createSignal } from "@barefootjs/client"
|
|
1539
|
+
type Item = { id: string; done: boolean }
|
|
1540
|
+
export function Widget({ base }: { base: Item }) {
|
|
1541
|
+
const [merged] = createSignal({ ...base, done: true })
|
|
1542
|
+
return (
|
|
1543
|
+
<div>
|
|
1544
|
+
<span>{merged().id}</span>
|
|
1545
|
+
<span>{merged().done ? 'yes' : 'no'}</span>
|
|
1546
|
+
</div>
|
|
1547
|
+
)
|
|
1548
|
+
}
|
|
1549
|
+
`, adapter)
|
|
1550
|
+
const bf101 = adapter.errors.filter(e => e.code === 'BF101')
|
|
1551
|
+
expect(bf101.length).toBe(1)
|
|
1552
|
+
expect(bf101[0].message).toContain("Signal 'merged'")
|
|
1553
|
+
expect(bf101[0].message).toContain('base')
|
|
1554
|
+
expect(bf101[0].suggestion?.escape).toEqual([{ kind: 'client-directive' }])
|
|
1555
|
+
expect(result.template).toBeTruthy()
|
|
1556
|
+
})
|
|
1557
|
+
|
|
1558
|
+
// Same refusal for an EXPLICITLY typed object signal (`createSignal<Item>`)
|
|
1559
|
+
// — the typed `interface` branch (`value-lowering.ts`) also defers to a
|
|
1560
|
+
// struct zero value (`Item{}`) for the identical reason.
|
|
1561
|
+
test('typed object signal spreading a live prop also refuses with BF101 (#2700)', () => {
|
|
1562
|
+
const adapter = new GoTemplateAdapter()
|
|
1563
|
+
compileAndGenerate(`
|
|
1564
|
+
"use client"
|
|
1565
|
+
import { createSignal } from "@barefootjs/client"
|
|
1566
|
+
type Item = { id: string; done: boolean }
|
|
1567
|
+
export function Widget({ base }: { base: Item }) {
|
|
1568
|
+
const [merged] = createSignal<Item>({ id: base.id, done: true })
|
|
1569
|
+
return <span>{merged().id}</span>
|
|
1570
|
+
}
|
|
1571
|
+
`, adapter)
|
|
1572
|
+
expect(adapter.errors.some(e => e.code === 'BF101')).toBe(true)
|
|
1573
|
+
})
|
|
1574
|
+
|
|
1575
|
+
// No memo-side counterpart: the analyzer deliberately never sets
|
|
1576
|
+
// `MemoInfo.parsed` to an `object-literal` for an object-returning memo
|
|
1577
|
+
// body (`analyzer.ts`, "isn't lowered from the parsed tree yet") — a
|
|
1578
|
+
// pre-existing, unrelated exclusion — so this refusal has no structural
|
|
1579
|
+
// handle to reach a memo without re-parsing `computation` as text,
|
|
1580
|
+
// which the repo's own convention rules out. #2700's own reproduction
|
|
1581
|
+
// and fixture are signal-only.
|
|
1582
|
+
|
|
1583
|
+
// The `/* @client */` escape: wrapping every SSR read defers evaluation
|
|
1584
|
+
// to the client, so the template never reaches `rootFieldRef` for this
|
|
1585
|
+
// signal — no refusal, even though the constructor still bakes `nil`.
|
|
1586
|
+
test('/* @client */ on every read suppresses the #2700 refusal', () => {
|
|
1587
|
+
const adapter = new GoTemplateAdapter()
|
|
1588
|
+
const result = compileAndGenerate(`
|
|
1589
|
+
"use client"
|
|
1590
|
+
import { createSignal } from "@barefootjs/client"
|
|
1591
|
+
type Item = { id: string; done: boolean }
|
|
1592
|
+
export function Widget({ base }: { base: Item }) {
|
|
1593
|
+
const [merged] = createSignal({ ...base, done: true })
|
|
1594
|
+
return (
|
|
1595
|
+
<div>
|
|
1596
|
+
<span>{/* @client */ merged().id}</span>
|
|
1597
|
+
<span>{/* @client */ merged().done ? 'yes' : 'no'}</span>
|
|
1598
|
+
</div>
|
|
1599
|
+
)
|
|
1600
|
+
}
|
|
1601
|
+
`, adapter)
|
|
1602
|
+
expect(adapter.errors.filter(e => e.code === 'BF101')).toEqual([])
|
|
1603
|
+
expect(result.types).toContain('Merged: nil,')
|
|
1604
|
+
})
|
|
1605
|
+
|
|
1606
|
+
// A signal that ONLY feeds a JSX spread (`{...merged()}`) never reaches
|
|
1607
|
+
// `rootFieldRef` — spread bags bake through their own `.Spread_<slot>`
|
|
1608
|
+
// route (`emitSpreadBagInits`) — so it must not trip the #2700 check even
|
|
1609
|
+
// though the constructor still can't bake the literal. (This shape DOES
|
|
1610
|
+
// still refuse, but with the PRE-EXISTING, unrelated "JSX spread has no
|
|
1611
|
+
// Go template lowering" BF101 for `{...merged()}` on an intrinsic
|
|
1612
|
+
// element — not a second, redundant #2700-shaped one.)
|
|
1613
|
+
test('a signal only spread into attrs does not double-refuse under #2700', () => {
|
|
1614
|
+
const adapter = new GoTemplateAdapter()
|
|
1615
|
+
compileAndGenerate(`
|
|
1616
|
+
"use client"
|
|
1617
|
+
import { createSignal } from "@barefootjs/client"
|
|
1618
|
+
type Item = { id: string; done: boolean }
|
|
1619
|
+
export function Widget({ base }: { base: Item }) {
|
|
1620
|
+
const [merged] = createSignal({ ...base, done: true })
|
|
1621
|
+
return <div {...merged()} />
|
|
1622
|
+
}
|
|
1623
|
+
`, adapter)
|
|
1624
|
+
const bf101 = adapter.errors.filter(e => e.code === 'BF101')
|
|
1625
|
+
expect(bf101.length).toBe(1)
|
|
1626
|
+
expect(bf101[0].message).not.toContain("Signal 'merged' is seeded")
|
|
1627
|
+
})
|
|
1628
|
+
|
|
1629
|
+
// A signal read ONLY from inside a `.filter()` predicate (never a plain
|
|
1630
|
+
// `{merged()}` text read) must still register in `templateReadRootFields`
|
|
1631
|
+
// and trip #2700's refusal — `renderFilterExprNode`'s zero-arg-call arm
|
|
1632
|
+
// used to build the `$.Merged` field reference by hand instead of
|
|
1633
|
+
// through `rootFieldRef`, so this exact shape was a false negative
|
|
1634
|
+
// (pullfrog review, PR #2818).
|
|
1635
|
+
test('a signal reachable only through a .filter() predicate still refuses with BF101 (#2700, #2818 pullfrog review)', () => {
|
|
1636
|
+
const adapter = new GoTemplateAdapter()
|
|
1637
|
+
compileAndGenerate(`
|
|
1638
|
+
"use client"
|
|
1639
|
+
import { createSignal } from "@barefootjs/client"
|
|
1640
|
+
type Item = { id: string; done: boolean }
|
|
1641
|
+
export function Widget({ base, items }: { base: Item; items: Item[] }) {
|
|
1642
|
+
const [merged] = createSignal({ ...base, done: true })
|
|
1643
|
+
return (
|
|
1644
|
+
<ul>
|
|
1645
|
+
{items().filter(i => i.id === merged().id).map(i => <li key={i.id}>{i.id}</li>)}
|
|
1646
|
+
</ul>
|
|
1647
|
+
)
|
|
1648
|
+
}
|
|
1649
|
+
`, adapter)
|
|
1650
|
+
const bf101 = adapter.errors.filter(e => e.code === 'BF101')
|
|
1651
|
+
expect(bf101.length).toBe(1)
|
|
1652
|
+
expect(bf101[0].message).toContain("Signal 'merged'")
|
|
1653
|
+
})
|
|
1654
|
+
|
|
1655
|
+
// A destructured prop sharing the module const's name must still win
|
|
1656
|
+
// (shadowing) — the const resolver is checked AFTER the prop lookup.
|
|
1657
|
+
test('a destructured prop shadows a same-named module const', () => {
|
|
1658
|
+
const adapter = new GoTemplateAdapter()
|
|
1659
|
+
const source = `
|
|
1660
|
+
"use client"
|
|
1661
|
+
import { createSignal } from "@barefootjs/client"
|
|
1662
|
+
const PAYLOAD = 'const-value'
|
|
1663
|
+
interface P { PAYLOAD?: string }
|
|
1664
|
+
export function Demo({ PAYLOAD }: P) {
|
|
1665
|
+
const [value] = createSignal(PAYLOAD)
|
|
1666
|
+
return <textarea value={value()} />
|
|
1667
|
+
}
|
|
1668
|
+
`
|
|
1669
|
+
const types = adapter.generateTypes(compileToIR(source, adapter))!
|
|
1670
|
+
expect(types).not.toContain('Value: "const-value"')
|
|
1671
|
+
// Positive pin (pullfrog review, #2816): the absence check above
|
|
1672
|
+
// would pass just as well if the value fell through to `nil`
|
|
1673
|
+
// instead of resolving to the prop — assert the actual expected
|
|
1674
|
+
// shadowing output too, matching the `Value: in.Value,` convention
|
|
1675
|
+
// used elsewhere in this file.
|
|
1676
|
+
expect(types).toContain('Value: in.PAYLOAD,')
|
|
1677
|
+
})
|
|
1678
|
+
|
|
1410
1679
|
// A boolean ternary memo (`isChecked = ctrl() ? c() : i()`) types its
|
|
1411
1680
|
// SSR field as `bool` (not `int`), so `aria-checked={isChecked()}`
|
|
1412
1681
|
// matches Hono's `aria-checked="false"` shape. Since #2260, `checked`'s
|
|
@@ -5112,6 +5381,61 @@ export function Toggle({ toggleItems }: ToggleProps) {
|
|
|
5112
5381
|
})
|
|
5113
5382
|
})
|
|
5114
5383
|
|
|
5384
|
+
describe('GoTemplateAdapter - #2835 name collision with a whole-props type member', () => {
|
|
5385
|
+
// Same-file sibling components (`Item` alongside the exported `List`) —
|
|
5386
|
+
// compiled with `compileJSX` directly, like the #2445 describe block above,
|
|
5387
|
+
// since `compileToIR`'s single-component IR round trip isn't set up to
|
|
5388
|
+
// pick a specific sibling out of a multi-component file.
|
|
5389
|
+
const collidingSource = `
|
|
5390
|
+
type ItemProps = { label: string }
|
|
5391
|
+
function Item(props: ItemProps) {
|
|
5392
|
+
return <div>{props.label}</div>
|
|
5393
|
+
}
|
|
5394
|
+
const base = [{ label: 'z' }]
|
|
5395
|
+
type Props = { base: ItemProps[] }
|
|
5396
|
+
export function List(props: Props) {
|
|
5397
|
+
return <div>{base.map((item) => <Item key={item.label} label={item.label} />)}</div>
|
|
5398
|
+
}
|
|
5399
|
+
`
|
|
5400
|
+
const controlSource = `
|
|
5401
|
+
type ItemProps = { label: string }
|
|
5402
|
+
function Item(props: ItemProps) {
|
|
5403
|
+
return <div>{props.label}</div>
|
|
5404
|
+
}
|
|
5405
|
+
const base = [{ label: 'z' }]
|
|
5406
|
+
type Props = { other: string }
|
|
5407
|
+
export function List(props: Props) {
|
|
5408
|
+
return <div>{base.map((item) => <Item key={item.label} label={item.label} />)}</div>
|
|
5409
|
+
}
|
|
5410
|
+
`
|
|
5411
|
+
|
|
5412
|
+
test('a module const colliding with `Props.base` is not classified prop-derived — the actual input `findNestedComponents` (component-tree.ts) feeds to Go codegen', () => {
|
|
5413
|
+
const ctx = analyzeComponent(collidingSource.trimStart(), 'test.tsx', 'List')
|
|
5414
|
+
const ir = jsxToIR(ctx)
|
|
5415
|
+
expect(ir).not.toBeNull()
|
|
5416
|
+
const item = findNestedComponents(ir!).find(c => c.name === 'Item')
|
|
5417
|
+
expect(item).toBeDefined()
|
|
5418
|
+
expect(item!.isPropDerived).toBe(false)
|
|
5419
|
+
expect(item!.isDynamic).toBe(false)
|
|
5420
|
+
})
|
|
5421
|
+
|
|
5422
|
+
test('Items stays a real hydrated field (`json:"items"`), matching the non-colliding control — misclassification previously hid it behind `json:"-"` and fabricated an unrelated `Base` prop read', () => {
|
|
5423
|
+
const collidingResult = compileJSX(collidingSource.trimStart(), 'test.tsx', { adapter: new GoTemplateAdapter(), outputIR: false })
|
|
5424
|
+
const controlResult = compileJSX(controlSource.trimStart(), 'test.tsx', { adapter: new GoTemplateAdapter(), outputIR: false })
|
|
5425
|
+
const collidingTypes = collidingResult.files.find(f => f.type === 'types')!.content
|
|
5426
|
+
const controlTypes = controlResult.files.find(f => f.type === 'types')!.content
|
|
5427
|
+
// `Props.base` is a real declared member independent of the loop
|
|
5428
|
+
// classification, so it legitimately gets its own `Base`/`bfCallerProps`
|
|
5429
|
+
// entries in BOTH cases — the loop-specific signal is the `Items` field,
|
|
5430
|
+
// which a misclassified `isPropDerived: true` hides from JSON entirely.
|
|
5431
|
+
expect(collidingTypes).toContain('Items []ItemProps `json:"items"`')
|
|
5432
|
+
expect(collidingTypes).not.toContain('Items []ItemProps `json:"-"`')
|
|
5433
|
+
const itemsFieldLine = (src: string) => src.split('\n').find(l => l.includes('[]ItemInput')) ?? ''
|
|
5434
|
+
expect(itemsFieldLine(collidingTypes)).not.toBe('')
|
|
5435
|
+
expect(itemsFieldLine(collidingTypes)).toBe(itemsFieldLine(controlTypes))
|
|
5436
|
+
})
|
|
5437
|
+
})
|
|
5438
|
+
|
|
5115
5439
|
describe('GoTemplateAdapter - collision-derivation lowering (#2683)', () => {
|
|
5116
5440
|
test('a signal colliding with its own prop composes the presence-check fold with the surrounding arithmetic', () => {
|
|
5117
5441
|
const result = compileJSX(`
|
|
@@ -149,6 +149,11 @@ export function P(props: { config: object }) {
|
|
|
149
149
|
// naming exactly — the formula generalises, it isn't a lookup table
|
|
150
150
|
// limited to `query`.
|
|
151
151
|
expect(template).toContain('bf_custom_serialize .Config')
|
|
152
|
+
// #2743: the whole-attribute `bf_attr` route is keyed on the neutral
|
|
153
|
+
// `guard-list` + `helper === 'query'` shape specifically — a
|
|
154
|
+
// `helper-call` node (this plugin's shape) is a different node kind and
|
|
155
|
+
// must not be routed through it.
|
|
156
|
+
expect(template).not.toContain('bf_attr')
|
|
152
157
|
})
|
|
153
158
|
|
|
154
159
|
test('without the plugin registered, the call falls back to the generic (unsupported) lowering', () => {
|
|
@@ -163,6 +168,39 @@ export function P(props: { config: object }) {
|
|
|
163
168
|
expect(template).not.toContain('bf_custom_serialize')
|
|
164
169
|
})
|
|
165
170
|
|
|
171
|
+
// #2842: a `helper-call` node (not the `query` guard-list) reached through
|
|
172
|
+
// the undef-alternate omission shape now routes through the fixed `call()`
|
|
173
|
+
// dispatcher — but NOT through `bf_attr` (only `query` needs the
|
|
174
|
+
// URL-context-escape bypass; a plain helper-call keeps the ordinary
|
|
175
|
+
// `name="{{…}}"` wrapper the `{{if}}` already builds around).
|
|
176
|
+
test('an undef-alternate helper-call consequent is registry-lowered without bf_attr (#2842)', () => {
|
|
177
|
+
registerLoweringPlugin(customSerializePlugin)
|
|
178
|
+
const src = `
|
|
179
|
+
'use client'
|
|
180
|
+
import { customSerialize } from './lib'
|
|
181
|
+
export function P(props: { on: boolean; config: object }) {
|
|
182
|
+
return <div data-config={props.on ? customSerialize(props.config) : undefined}>x</div>
|
|
183
|
+
}
|
|
184
|
+
`
|
|
185
|
+
const { template } = generate(src)
|
|
186
|
+
expect(template).toContain('{{if .On}}data-config="{{bf_custom_serialize .Config}}"{{end}}')
|
|
187
|
+
expect(template).not.toContain('bf_attr')
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
// Same shape, plugin NOT registered — pins that an unmatched call keeps the
|
|
191
|
+
// existing generic Go-method-call convention (no new BF10x refusal).
|
|
192
|
+
test('an undef-alternate helper-call consequent with no plugin registered keeps the generic convention', () => {
|
|
193
|
+
const src = `
|
|
194
|
+
'use client'
|
|
195
|
+
import { customSerialize } from './lib'
|
|
196
|
+
export function P(props: { on: boolean; config: object }) {
|
|
197
|
+
return <div data-config={props.on ? customSerialize(props.config) : undefined}>x</div>
|
|
198
|
+
}
|
|
199
|
+
`
|
|
200
|
+
const { template } = generate(src)
|
|
201
|
+
expect(template).toContain('{{if .On}}data-config="{{.CustomSerialize .Config}}"{{end}}')
|
|
202
|
+
})
|
|
203
|
+
|
|
166
204
|
test('a CONDITIONAL helper-call arg renders as pipeline-position bf_ternary, not an {{if}} action', () => {
|
|
167
205
|
// The #2324 union stage lowers a union-typed locale to a ternary
|
|
168
206
|
// pattern arg. Go templates have no expression-level conditional, and
|
|
@@ -176,4 +176,130 @@ export function P(props: { base: string; q: Record<string, string> }) {
|
|
|
176
176
|
const { template } = generate(src)
|
|
177
177
|
expect(template).not.toContain('bf_query')
|
|
178
178
|
})
|
|
179
|
+
|
|
180
|
+
// #2743: a `queryHref` value in an ATTRIBUTE position emits the whole
|
|
181
|
+
// attribute via `bf_attr` (template.HTMLAttr) so html/template's
|
|
182
|
+
// contextual URL-context autoescape (keyed off the attribute NAME) never
|
|
183
|
+
// percent-encodes the base — see `lowerRegisteredAttrCall`.
|
|
184
|
+
test('an href-attribute queryHref value routes through bf_attr, not `href="{{...}}"`', () => {
|
|
185
|
+
const src = `
|
|
186
|
+
'use client'
|
|
187
|
+
import { queryHref } from '@barefootjs/client'
|
|
188
|
+
export function P(props: { base: string; tag: string }) {
|
|
189
|
+
return <a href={queryHref(props.base, { tag: props.tag })}>x</a>
|
|
190
|
+
}
|
|
191
|
+
`
|
|
192
|
+
const { template } = generate(src)
|
|
193
|
+
expect(template).toContain('{{bf_attr "href" (bf_query .Base (true) "tag" .Tag)}}')
|
|
194
|
+
expect(template).not.toContain('href="{{')
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
// The route is keyed on the neutral `helper === 'query'` fact, not on the
|
|
198
|
+
// attribute name `href` — any attribute (e.g. `title`) gets the same
|
|
199
|
+
// treatment, since `queryHref` returns a plain string with nothing
|
|
200
|
+
// href-specific about it.
|
|
201
|
+
test('a non-href attribute (title) with a queryHref value also routes through bf_attr', () => {
|
|
202
|
+
const src = `
|
|
203
|
+
'use client'
|
|
204
|
+
import { queryHref } from '@barefootjs/client'
|
|
205
|
+
export function P(props: { base: string; tag: string }) {
|
|
206
|
+
return <a title={queryHref(props.base, { tag: props.tag })}>x</a>
|
|
207
|
+
}
|
|
208
|
+
`
|
|
209
|
+
const { template } = generate(src)
|
|
210
|
+
expect(template).toContain('{{bf_attr "title" (bf_query .Base (true) "tag" .Tag)}}')
|
|
211
|
+
expect(template).not.toContain('title="{{')
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
// Text-position (non-attribute) use is unaffected — `bf_attr` only wraps
|
|
215
|
+
// the whole-attribute case; a queryHref value read as text still lowers
|
|
216
|
+
// to a bare `bf_query` pipeline.
|
|
217
|
+
test('a queryHref value in text position is not wrapped in bf_attr', () => {
|
|
218
|
+
const src = `
|
|
219
|
+
'use client'
|
|
220
|
+
import { queryHref } from '@barefootjs/client'
|
|
221
|
+
export function P(props: { base: string; tag: string }) {
|
|
222
|
+
return <span>{queryHref(props.base, { tag: props.tag })}</span>
|
|
223
|
+
}
|
|
224
|
+
`
|
|
225
|
+
const { template } = generate(src)
|
|
226
|
+
expect(template).toContain('bf_query .Base (true) "tag" .Tag')
|
|
227
|
+
expect(template).not.toContain('bf_attr')
|
|
228
|
+
})
|
|
229
|
+
|
|
230
|
+
// #2743 follow-up (pullfrog review on #2841): a ternary attribute value
|
|
231
|
+
// with a real (non-`undefined`) alternate is syntactically valid and
|
|
232
|
+
// already lowers both branches correctly via `bf_ternary` — but was still
|
|
233
|
+
// wrapped in the ordinary `name="{{...}}"` form, leaving it exposed to
|
|
234
|
+
// html/template's URL-context percent-encoding. This must route through
|
|
235
|
+
// `bf_attr` too, wrapping the whole `bf_ternary` pipeline.
|
|
236
|
+
test('a queryHref value in a ternary branch (non-undefined alternate) routes through bf_attr', () => {
|
|
237
|
+
const src = `
|
|
238
|
+
'use client'
|
|
239
|
+
import { queryHref } from '@barefootjs/client'
|
|
240
|
+
export function P(props: { ok: boolean; base: string; tag: string }) {
|
|
241
|
+
return <a href={props.ok ? queryHref(props.base, { tag: props.tag }) : '/fallback'}>x</a>
|
|
242
|
+
}
|
|
243
|
+
`
|
|
244
|
+
const { template } = generate(src)
|
|
245
|
+
expect(template).toContain(
|
|
246
|
+
'{{bf_attr "href" (bf_ternary (bf_truthy .Ok) (bf_query .Base (true) "tag" .Tag) "/fallback")}}',
|
|
247
|
+
)
|
|
248
|
+
expect(template).not.toContain('href="{{')
|
|
249
|
+
})
|
|
250
|
+
|
|
251
|
+
// #2842: the `undefined`-alternate omission shape used to render only the
|
|
252
|
+
// consequent via a registry-blind path, emitting invalid Go syntax
|
|
253
|
+
// (`.QueryHref .Base bf_map "tag" .Tag`) with no diagnostic. The consequent
|
|
254
|
+
// now routes through `lowerRegisteredAttrCall` (the same whole-attribute
|
|
255
|
+
// `bf_attr` bypass the direct-call and non-undefined-ternary shapes use),
|
|
256
|
+
// inside the `{{if}}` that implements the omission.
|
|
257
|
+
test('the undefined-alternate omission shape routes the consequent through bf_attr inside the {{if}} (#2842)', () => {
|
|
258
|
+
const src = `
|
|
259
|
+
'use client'
|
|
260
|
+
import { queryHref } from '@barefootjs/client'
|
|
261
|
+
export function P(props: { ok: boolean; base: string; tag: string }) {
|
|
262
|
+
return <a href={props.ok ? queryHref(props.base, { tag: props.tag }) : undefined}>x</a>
|
|
263
|
+
}
|
|
264
|
+
`
|
|
265
|
+
const { template } = generate(src)
|
|
266
|
+
expect(template).toContain('{{if .Ok}}{{bf_attr "href" (bf_query .Base (true) "tag" .Tag)}}{{end}}')
|
|
267
|
+
expect(template).not.toContain('.QueryHref')
|
|
268
|
+
expect(template).not.toContain('bf_map')
|
|
269
|
+
expect(template).not.toContain('href="{{')
|
|
270
|
+
})
|
|
271
|
+
|
|
272
|
+
// #2842: a registered call nested in a template-literal interpolation
|
|
273
|
+
// (not just a direct attribute value) is registry-lowered too — the fix
|
|
274
|
+
// lives in the shared ParsedExpr `call()` dispatcher, so any nested
|
|
275
|
+
// position benefits, not only the ternary/attribute cases above.
|
|
276
|
+
test('a queryHref call nested in a template-literal interpolation is registry-lowered (#2842)', () => {
|
|
277
|
+
const src = `
|
|
278
|
+
'use client'
|
|
279
|
+
import { queryHref } from '@barefootjs/client'
|
|
280
|
+
export function P(props: { base: string; tag: string }) {
|
|
281
|
+
return <a title={\`pre \${queryHref(props.base, { tag: props.tag })}\`}>x</a>
|
|
282
|
+
}
|
|
283
|
+
`
|
|
284
|
+
const { template } = generate(src)
|
|
285
|
+
expect(template).toContain('title="pre {{bf_query .Base (true) "tag" .Tag}}"')
|
|
286
|
+
expect(template).not.toContain('.QueryHref')
|
|
287
|
+
})
|
|
288
|
+
|
|
289
|
+
// #2842: a nested ternary inside the undef-alternate consequent still
|
|
290
|
+
// recurses correctly — `lowerRegisteredAttrCall`'s `conditional` arm
|
|
291
|
+
// right-folds, matching `lowerTernary`'s own recursion.
|
|
292
|
+
test('a nested ternary inside the undef-alternate consequent still routes through bf_attr (#2842)', () => {
|
|
293
|
+
const src = `
|
|
294
|
+
'use client'
|
|
295
|
+
import { queryHref } from '@barefootjs/client'
|
|
296
|
+
export function P(props: { a: boolean; b: boolean; base: string; tag: string }) {
|
|
297
|
+
return <a href={props.a ? (props.b ? queryHref(props.base, { tag: props.tag }) : '/x') : undefined}>x</a>
|
|
298
|
+
}
|
|
299
|
+
`
|
|
300
|
+
const { template } = generate(src)
|
|
301
|
+
expect(template).toContain(
|
|
302
|
+
'{{if .A}}{{bf_attr "href" (bf_ternary (bf_truthy .B) (bf_query .Base (true) "tag" .Tag) "/x")}}{{end}}',
|
|
303
|
+
)
|
|
304
|
+
})
|
|
179
305
|
})
|
|
@@ -79,4 +79,18 @@ export interface GoEmitContext {
|
|
|
79
79
|
* outer-loop params are excluded).
|
|
80
80
|
*/
|
|
81
81
|
resolveModuleStringConst(name: string): string | null
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Inline a module numeric const by name as its Go literal text (e.g.
|
|
85
|
+
* `8`, `-3.5`), or null when the name is not such a const (loop vars and
|
|
86
|
+
* outer-loop params are excluded, same as `resolveModuleStringConst`).
|
|
87
|
+
*/
|
|
88
|
+
resolveModuleNumericConst(name: string): string | null
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Inline a module boolean const by name as its Go literal text
|
|
92
|
+
* (`true`/`false`), or null when the name is not such a const (same
|
|
93
|
+
* exclusions as `resolveModuleNumericConst`).
|
|
94
|
+
*/
|
|
95
|
+
resolveModuleBooleanConst(name: string): string | null
|
|
82
96
|
}
|