@barefootjs/go-template 0.16.0 → 0.17.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/analysis/component-tree.d.ts +23 -0
- package/dist/adapter/analysis/component-tree.d.ts.map +1 -0
- package/dist/adapter/emit-context.d.ts +53 -0
- package/dist/adapter/emit-context.d.ts.map +1 -0
- package/dist/adapter/expr/helper-inline.d.ts +31 -0
- package/dist/adapter/expr/helper-inline.d.ts.map +1 -0
- package/dist/adapter/expr/url-builder.d.ts +23 -0
- package/dist/adapter/expr/url-builder.d.ts.map +1 -0
- package/dist/adapter/go-template-adapter.d.ts +302 -1071
- package/dist/adapter/go-template-adapter.d.ts.map +1 -1
- package/dist/adapter/index.js +3116 -2692
- package/dist/adapter/lib/compile-state.d.ts +142 -0
- package/dist/adapter/lib/compile-state.d.ts.map +1 -0
- package/dist/adapter/lib/constants.d.ts +11 -0
- package/dist/adapter/lib/constants.d.ts.map +1 -0
- package/dist/adapter/lib/go-emit.d.ts +126 -0
- package/dist/adapter/lib/go-emit.d.ts.map +1 -0
- package/dist/adapter/lib/go-naming.d.ts +39 -0
- package/dist/adapter/lib/go-naming.d.ts.map +1 -0
- package/dist/adapter/lib/ir-scope.d.ts +13 -0
- package/dist/adapter/lib/ir-scope.d.ts.map +1 -0
- package/dist/adapter/lib/types.d.ts +165 -0
- package/dist/adapter/lib/types.d.ts.map +1 -0
- package/dist/adapter/memo/ctor-lowering.d.ts +39 -0
- package/dist/adapter/memo/ctor-lowering.d.ts.map +1 -0
- package/dist/adapter/memo/memo-compute.d.ts +173 -0
- package/dist/adapter/memo/memo-compute.d.ts.map +1 -0
- package/dist/adapter/memo/memo-type.d.ts +48 -0
- package/dist/adapter/memo/memo-type.d.ts.map +1 -0
- package/dist/adapter/memo/memo-value.d.ts +64 -0
- package/dist/adapter/memo/memo-value.d.ts.map +1 -0
- package/dist/adapter/memo/template-interp.d.ts +43 -0
- package/dist/adapter/memo/template-interp.d.ts.map +1 -0
- package/dist/adapter/props/prop-types.d.ts +31 -0
- package/dist/adapter/props/prop-types.d.ts.map +1 -0
- package/dist/adapter/spread/spread-codegen.d.ts +40 -0
- package/dist/adapter/spread/spread-codegen.d.ts.map +1 -0
- package/dist/adapter/type/type-codegen.d.ts +25 -0
- package/dist/adapter/type/type-codegen.d.ts.map +1 -0
- package/dist/adapter/value/parsed-literal-to-go.d.ts +17 -0
- package/dist/adapter/value/parsed-literal-to-go.d.ts.map +1 -0
- package/dist/adapter/value/value-lowering.d.ts +46 -0
- package/dist/adapter/value/value-lowering.d.ts.map +1 -0
- package/dist/build.js +3114 -2690
- package/dist/index.js +3116 -2692
- package/dist/test-render.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/__tests__/derived-state-memo.test.ts +155 -84
- package/src/__tests__/go-template-adapter.test.ts +398 -54
- package/src/__tests__/lowering-plugin.test.ts +109 -0
- package/src/__tests__/query-href.test.ts +179 -0
- package/src/adapter/analysis/component-tree.ts +174 -0
- package/src/adapter/emit-context.ts +59 -0
- package/src/adapter/expr/helper-inline.ts +274 -0
- package/src/adapter/expr/url-builder.ts +123 -0
- package/src/adapter/go-template-adapter.ts +2189 -5320
- package/src/adapter/lib/compile-state.ts +181 -0
- package/src/adapter/lib/constants.ts +24 -0
- package/src/adapter/lib/go-emit.ts +309 -0
- package/src/adapter/lib/go-naming.ts +84 -0
- package/src/adapter/lib/ir-scope.ts +31 -0
- package/src/adapter/lib/types.ts +182 -0
- package/src/adapter/memo/ctor-lowering.ts +267 -0
- package/src/adapter/memo/memo-compute.ts +661 -0
- package/src/adapter/memo/memo-type.ts +93 -0
- package/src/adapter/memo/memo-value.ts +197 -0
- package/src/adapter/memo/template-interp.ts +246 -0
- package/src/adapter/props/prop-types.ts +84 -0
- package/src/adapter/spread/spread-codegen.ts +458 -0
- package/src/adapter/type/type-codegen.ts +95 -0
- package/src/adapter/value/parsed-literal-to-go.ts +94 -0
- package/src/adapter/value/value-lowering.ts +162 -0
- package/src/test-render.ts +48 -22
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Registry → adapter end-to-end (#2057). Every lowering — first-party built-ins
|
|
3
|
+
* (like `queryHref`) and userland plugins alike — flows through the one
|
|
4
|
+
* lowering-plugin *registry*. This test guarantees that seam works all the way
|
|
5
|
+
* through: a SAMPLE plugin (registered via the public `registerLoweringPlugin`)
|
|
6
|
+
* recognises a call from an arbitrary package and returns a backend-neutral
|
|
7
|
+
* `guard-list` node, and the Go adapter renders it via its own `bf_query`
|
|
8
|
+
* mapping — no adapter code knows the sample plugin exists.
|
|
9
|
+
*
|
|
10
|
+
* The plugin reuses the `'query'` helper id so it exercises the exact same
|
|
11
|
+
* renderer path as the built-in `queryHref` plugin, proving that a userland
|
|
12
|
+
* plugin and a default-registered built-in are indistinguishable to the adapter
|
|
13
|
+
* (which is the whole point of the neutral-node layer).
|
|
14
|
+
*/
|
|
15
|
+
import { describe, test, expect, afterEach } from 'bun:test'
|
|
16
|
+
import {
|
|
17
|
+
compileJSX,
|
|
18
|
+
registerLoweringPlugin,
|
|
19
|
+
getLoweringPlugins,
|
|
20
|
+
__resetLoweringPluginsForTest,
|
|
21
|
+
type ComponentIR,
|
|
22
|
+
type LoweringPlugin,
|
|
23
|
+
} from '@barefootjs/jsx'
|
|
24
|
+
import { GoTemplateAdapter } from '../adapter/go-template-adapter'
|
|
25
|
+
|
|
26
|
+
function generate(src: string) {
|
|
27
|
+
const adapter = new GoTemplateAdapter()
|
|
28
|
+
const result = compileJSX(src.trimStart(), 'T.tsx', { adapter, outputIR: true })
|
|
29
|
+
const irFile = result.files.find(f => f.type === 'ir')
|
|
30
|
+
if (!irFile) throw new Error('no IR')
|
|
31
|
+
const ir = JSON.parse(irFile.content) as ComponentIR
|
|
32
|
+
return adapter.generate(ir)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// A userland-style plugin: active only when the component imports `demoUrl` from
|
|
36
|
+
// `@sample/pkg`, lowering `demoUrl(base, { key: value })` to a neutral
|
|
37
|
+
// `guard-list` on the `'query'` helper. It reuses the sub-parts of the parsed
|
|
38
|
+
// call directly (base = first arg, one triple per object property) so no
|
|
39
|
+
// adapter syntax leaks into the plugin.
|
|
40
|
+
const samplePlugin: LoweringPlugin = {
|
|
41
|
+
name: 'sample-demo-url',
|
|
42
|
+
prepare(metadata) {
|
|
43
|
+
const spec = metadata.imports
|
|
44
|
+
.filter(i => i.source === '@sample/pkg' && !i.isTypeOnly)
|
|
45
|
+
.flatMap(i => i.specifiers)
|
|
46
|
+
.filter(s => !s.isTypeOnly && !s.isNamespace && !s.isDefault)
|
|
47
|
+
.find(s => s.name === 'demoUrl')
|
|
48
|
+
if (!spec) return null
|
|
49
|
+
const local = spec.alias ?? spec.name // the name it's bound under in this file
|
|
50
|
+
return (callee, args) => {
|
|
51
|
+
if (callee.kind !== 'identifier' || callee.name !== local) return null
|
|
52
|
+
const [base, obj] = args
|
|
53
|
+
if (!base || obj?.kind !== 'object-literal') return null
|
|
54
|
+
return {
|
|
55
|
+
kind: 'guard-list',
|
|
56
|
+
helper: 'query',
|
|
57
|
+
base,
|
|
58
|
+
triples: obj.properties.map(p => ({ guard: null, key: p.key, value: p.value })),
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
afterEach(() => {
|
|
65
|
+
__resetLoweringPluginsForTest(getLoweringPlugins().filter(p => p.name !== 'sample-demo-url'))
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
describe('lowering-plugin registry → Go adapter (#2057)', () => {
|
|
69
|
+
test('a registered plugin lowers its call to the neutral node the adapter renders', () => {
|
|
70
|
+
registerLoweringPlugin(samplePlugin)
|
|
71
|
+
const src = `
|
|
72
|
+
'use client'
|
|
73
|
+
import { demoUrl } from '@sample/pkg'
|
|
74
|
+
export function P(props: { base: string; tag: string }) {
|
|
75
|
+
return <a href={demoUrl(props.base, { tag: props.tag })}>x</a>
|
|
76
|
+
}
|
|
77
|
+
`
|
|
78
|
+
const { template } = generate(src)
|
|
79
|
+
// Same render path as built-in queryHref: guard-list → bf_query.
|
|
80
|
+
expect(template).toContain('bf_query .Base (true) "tag" .Tag')
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
test('without the plugin registered the same call falls back to the generic lowering', () => {
|
|
84
|
+
// No registerLoweringPlugin call — the registry is empty for this component.
|
|
85
|
+
const src = `
|
|
86
|
+
'use client'
|
|
87
|
+
import { demoUrl } from '@sample/pkg'
|
|
88
|
+
export function P(props: { base: string; tag: string }) {
|
|
89
|
+
return <a href={demoUrl(props.base, { tag: props.tag })}>x</a>
|
|
90
|
+
}
|
|
91
|
+
`
|
|
92
|
+
const { template } = generate(src)
|
|
93
|
+
expect(template).not.toContain('bf_query')
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
test('the plugin is inert when the component does not import from its package', () => {
|
|
97
|
+
registerLoweringPlugin(samplePlugin)
|
|
98
|
+
const src = `
|
|
99
|
+
'use client'
|
|
100
|
+
import { queryHref } from '@barefootjs/client'
|
|
101
|
+
export function P(props: { base: string; tag: string }) {
|
|
102
|
+
return <a href={queryHref(props.base, { tag: props.tag })}>x</a>
|
|
103
|
+
}
|
|
104
|
+
`
|
|
105
|
+
const { template } = generate(src)
|
|
106
|
+
// Built-in queryHref still lowers; the sample plugin simply isn't active.
|
|
107
|
+
expect(template).toContain('bf_query .Base (true) "tag" .Tag')
|
|
108
|
+
})
|
|
109
|
+
})
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `queryHref(base, { … })` → `bf_query` lowering for the Go adapter (#2042).
|
|
3
|
+
*
|
|
4
|
+
* The pure functional URL builder is a structured `call` + `object-literal` in
|
|
5
|
+
* the IR, so the adapter lowers it directly — no block-body recognizer, no
|
|
6
|
+
* re-parse. The lowering emits `(include) "key" value` triples and lets the
|
|
7
|
+
* `bf_query` runtime helper own the non-empty check (so it can also append array
|
|
8
|
+
* values member-by-member, #2048): a plain `key: v` → `(true) "key" v`; a
|
|
9
|
+
* conditional `key: cond ? a : undefined` → `(cond) "key" a` (the helper drops an
|
|
10
|
+
* empty `a`). The full value semantics are conformance-tested against
|
|
11
|
+
* URLSearchParams in the shared golden vectors (TestHelperVectors, fn "query").
|
|
12
|
+
*/
|
|
13
|
+
import { describe, test, expect } from 'bun:test'
|
|
14
|
+
import { compileJSX, type ComponentIR } from '@barefootjs/jsx'
|
|
15
|
+
import { GoTemplateAdapter } from '../adapter/go-template-adapter'
|
|
16
|
+
|
|
17
|
+
function generate(src: string) {
|
|
18
|
+
const adapter = new GoTemplateAdapter()
|
|
19
|
+
const result = compileJSX(src.trimStart(), 'T.tsx', { adapter, outputIR: true })
|
|
20
|
+
const irFile = result.files.find(f => f.type === 'ir')
|
|
21
|
+
if (!irFile) throw new Error('no IR')
|
|
22
|
+
const ir = JSON.parse(irFile.content) as ComponentIR
|
|
23
|
+
return adapter.generate(ir)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
describe('queryHref → bf_query (#2042)', () => {
|
|
27
|
+
test('a plain value passes a `true` include — bf_query drops it if empty', () => {
|
|
28
|
+
const src = `
|
|
29
|
+
'use client'
|
|
30
|
+
import { queryHref } from '@barefootjs/client'
|
|
31
|
+
export function P(props: { base: string; tag: string }) {
|
|
32
|
+
return <a href={queryHref(props.base, { tag: props.tag })}>x</a>
|
|
33
|
+
}
|
|
34
|
+
`
|
|
35
|
+
const { template } = generate(src)
|
|
36
|
+
expect(template).toContain('bf_query .Base (true) "tag" .Tag')
|
|
37
|
+
expect(template).not.toContain('.QueryHref')
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
test('a conditional include lowers to `(cond)` — the helper applies the non-empty check', () => {
|
|
41
|
+
const src = `
|
|
42
|
+
'use client'
|
|
43
|
+
import { queryHref } from '@barefootjs/client'
|
|
44
|
+
export function P(props: { base: string; sort: string; tag: string }) {
|
|
45
|
+
return (
|
|
46
|
+
<a href={queryHref(props.base, {
|
|
47
|
+
sort: props.sort !== 'date' ? props.sort : undefined,
|
|
48
|
+
tag: props.tag,
|
|
49
|
+
})}>x</a>
|
|
50
|
+
)
|
|
51
|
+
}
|
|
52
|
+
`
|
|
53
|
+
const { template } = generate(src)
|
|
54
|
+
expect(template).toContain(
|
|
55
|
+
'bf_query .Base (ne (bf_string .Sort) "date") "sort" .Sort (true) "tag" .Tag',
|
|
56
|
+
)
|
|
57
|
+
// The `ne consequent ""` non-empty check is no longer folded into the
|
|
58
|
+
// include — bf_query owns it (so it can also append array values).
|
|
59
|
+
expect(template).not.toContain('(ne .Sort "")')
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
// A `&&` / `||` guard is NOT a comparison, so `lowerUrlGuard` can't emit it as
|
|
63
|
+
// a bare Go bool — `and`/`or` return one of their operands (a string), which
|
|
64
|
+
// `bf_query` type-asserts against. It must take the truthiness-wrap path,
|
|
65
|
+
// `ne (and …) ""`, yielding a real bool.
|
|
66
|
+
test('a `&&` guard is wrapped to a bool — `ne (and …) ""`, not a bare `and`', () => {
|
|
67
|
+
const src = `
|
|
68
|
+
'use client'
|
|
69
|
+
import { queryHref } from '@barefootjs/client'
|
|
70
|
+
export function P(props: { base: string; a: string; b: string }) {
|
|
71
|
+
return <a href={queryHref(props.base, { both: props.a && props.b ? props.a : undefined })}>x</a>
|
|
72
|
+
}
|
|
73
|
+
`
|
|
74
|
+
const { template } = generate(src)
|
|
75
|
+
expect(template).toContain('bf_query .Base (ne (and .A .B) "") "both" .A')
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
test('null / empty-string alternates are both treated as the omit branch', () => {
|
|
79
|
+
const src = `
|
|
80
|
+
'use client'
|
|
81
|
+
import { queryHref } from '@barefootjs/client'
|
|
82
|
+
export function P(props: { base: string; mode: string; a: string; b: string }) {
|
|
83
|
+
return <a href={queryHref(props.base, {
|
|
84
|
+
a: props.mode !== 'off' ? props.a : '',
|
|
85
|
+
b: props.mode !== 'off' ? props.b : null,
|
|
86
|
+
})}>x</a>
|
|
87
|
+
}
|
|
88
|
+
`
|
|
89
|
+
const { template } = generate(src)
|
|
90
|
+
// Both '' and null alternates fold to the same conditional-include form.
|
|
91
|
+
expect(template).toContain('(ne (bf_string .Mode) "off") "a" .A')
|
|
92
|
+
expect(template).toContain('(ne (bf_string .Mode) "off") "b" .B')
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
test('an array value lowers the slice expression; bf_query appends its members', () => {
|
|
96
|
+
const src = `
|
|
97
|
+
'use client'
|
|
98
|
+
import { queryHref } from '@barefootjs/client'
|
|
99
|
+
export function P(props: { base: string; tags: string[] }) {
|
|
100
|
+
return <a href={queryHref(props.base, { tag: props.tags })}>x</a>
|
|
101
|
+
}
|
|
102
|
+
`
|
|
103
|
+
const { template } = generate(src)
|
|
104
|
+
// The value is the raw slice field; member-append + non-empty omit happen in
|
|
105
|
+
// the helper at render time (verified against URLSearchParams in the golden
|
|
106
|
+
// vectors). The old `ne value ""` fold would have been invalid Go here.
|
|
107
|
+
expect(template).toContain('bf_query .Base (true) "tag" .Tags')
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
test('a conditional array value keeps the guard and passes the slice', () => {
|
|
111
|
+
const src = `
|
|
112
|
+
'use client'
|
|
113
|
+
import { queryHref } from '@barefootjs/client'
|
|
114
|
+
export function P(props: { base: string; on: string; tags: string[] }) {
|
|
115
|
+
return <a href={queryHref(props.base, { tag: props.on !== '' ? props.tags : undefined })}>x</a>
|
|
116
|
+
}
|
|
117
|
+
`
|
|
118
|
+
const { template } = generate(src)
|
|
119
|
+
expect(template).toContain('bf_query .Base (ne (bf_string .On) "") "tag" .Tags')
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
test('an aliased import is still recognised', () => {
|
|
123
|
+
const src = `
|
|
124
|
+
'use client'
|
|
125
|
+
import { queryHref as qh } from '@barefootjs/client'
|
|
126
|
+
export function P(props: { base: string; tag: string }) {
|
|
127
|
+
return <a href={qh(props.base, { tag: props.tag })}>x</a>
|
|
128
|
+
}
|
|
129
|
+
`
|
|
130
|
+
const { template } = generate(src)
|
|
131
|
+
expect(template).toContain('bf_query .Base')
|
|
132
|
+
expect(template).toContain('"tag" .Tag')
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
test('a param-free expression-bodied helper wrapping queryHref inlines + lowers', () => {
|
|
136
|
+
const src = `
|
|
137
|
+
'use client'
|
|
138
|
+
import { queryHref } from '@barefootjs/client'
|
|
139
|
+
export function P(props: { base: string }) {
|
|
140
|
+
const homeHref = () => queryHref(props.base, { view: 'home' })
|
|
141
|
+
return <a href={homeHref()}>x</a>
|
|
142
|
+
}
|
|
143
|
+
`
|
|
144
|
+
const { template } = generate(src)
|
|
145
|
+
expect(template).toContain('bf_query .Base (true) "view" "home"')
|
|
146
|
+
expect(template).not.toContain('.HomeHref')
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
// Known limitation (pre-existing, not queryHref-specific): the generic helper
|
|
150
|
+
// inliner declines a body whose object literal references the helper's params,
|
|
151
|
+
// because an object literal lowers opaquely from its `raw` source — so the
|
|
152
|
+
// param can't be substituted. queryHref's idiom is the direct call, so this is
|
|
153
|
+
// a minor gap; helper-delegation ergonomics are a follow-up (#2042).
|
|
154
|
+
test('a helper whose params-object references a param is not yet inlined (falls back)', () => {
|
|
155
|
+
const src = `
|
|
156
|
+
'use client'
|
|
157
|
+
import { queryHref } from '@barefootjs/client'
|
|
158
|
+
export function P(props: { base: string }) {
|
|
159
|
+
const hrefFor = (s: string) => queryHref(props.base, { sort: s })
|
|
160
|
+
return <a href={hrefFor('title')}>x</a>
|
|
161
|
+
}
|
|
162
|
+
`
|
|
163
|
+
const { template } = generate(src)
|
|
164
|
+
expect(template).not.toContain('bf_query')
|
|
165
|
+
expect(template).toContain('.HrefFor "title"')
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
test('a dynamic (non-literal) params object falls back to the generic lowering', () => {
|
|
169
|
+
const src = `
|
|
170
|
+
'use client'
|
|
171
|
+
import { queryHref } from '@barefootjs/client'
|
|
172
|
+
export function P(props: { base: string; q: Record<string, string> }) {
|
|
173
|
+
return <a href={queryHref(props.base, props.q)}>x</a>
|
|
174
|
+
}
|
|
175
|
+
`
|
|
176
|
+
const { template } = generate(src)
|
|
177
|
+
expect(template).not.toContain('bf_query')
|
|
178
|
+
})
|
|
179
|
+
})
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* IR component-tree analysis for the Go html/template adapter.
|
|
3
|
+
*
|
|
4
|
+
* Pure structural walks over the IR — no adapter instance state, no rendering.
|
|
5
|
+
* Only the two entry points (`hasClientInteractivity`,
|
|
6
|
+
* `findNestedComponents`) are exported; the recursive collectors stay
|
|
7
|
+
* module-internal.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type {
|
|
11
|
+
ComponentIR,
|
|
12
|
+
IRNode,
|
|
13
|
+
IRElement,
|
|
14
|
+
IRFragment,
|
|
15
|
+
IRConditional,
|
|
16
|
+
IRLoop,
|
|
17
|
+
IRIfStatement,
|
|
18
|
+
IRComponent,
|
|
19
|
+
} from '@barefootjs/jsx'
|
|
20
|
+
|
|
21
|
+
import type { NestedComponentInfo } from '../lib/types.ts'
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Does the component need client JS? True when it has reactive state
|
|
25
|
+
* (signals), effects, onMounts, any event handler in the tree, or any
|
|
26
|
+
* child component (which needs the parent's hydration).
|
|
27
|
+
*/
|
|
28
|
+
export function hasClientInteractivity(ir: ComponentIR): boolean {
|
|
29
|
+
if (ir.metadata.signals.length > 0) return true
|
|
30
|
+
if (ir.metadata.effects.length > 0) return true
|
|
31
|
+
if (ir.metadata.onMounts.length > 0) return true
|
|
32
|
+
if (hasEventsInTree(ir.root)) return true
|
|
33
|
+
// Child components need the parent's hydration.
|
|
34
|
+
if (findChildComponentNames(ir.root).size > 0) return true
|
|
35
|
+
return false
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Recursively check if any element in the tree has events. */
|
|
39
|
+
function hasEventsInTree(node: IRNode): boolean {
|
|
40
|
+
if (node.type === 'element') {
|
|
41
|
+
const element = node as IRElement
|
|
42
|
+
if (element.events.length > 0) return true
|
|
43
|
+
for (const child of element.children) {
|
|
44
|
+
if (hasEventsInTree(child)) return true
|
|
45
|
+
}
|
|
46
|
+
} else if (node.type === 'fragment') {
|
|
47
|
+
const fragment = node as IRFragment
|
|
48
|
+
for (const child of fragment.children) {
|
|
49
|
+
if (hasEventsInTree(child)) return true
|
|
50
|
+
}
|
|
51
|
+
} else if (node.type === 'conditional') {
|
|
52
|
+
const cond = node as IRConditional
|
|
53
|
+
if (hasEventsInTree(cond.whenTrue)) return true
|
|
54
|
+
if (cond.whenFalse && hasEventsInTree(cond.whenFalse)) return true
|
|
55
|
+
} else if (node.type === 'loop') {
|
|
56
|
+
const loop = node as IRLoop
|
|
57
|
+
for (const child of loop.children) {
|
|
58
|
+
if (hasEventsInTree(child)) return true
|
|
59
|
+
}
|
|
60
|
+
} else if (node.type === 'if-statement') {
|
|
61
|
+
const ifStmt = node as IRIfStatement
|
|
62
|
+
if (hasEventsInTree(ifStmt.consequent)) return true
|
|
63
|
+
if (ifStmt.alternate && hasEventsInTree(ifStmt.alternate)) return true
|
|
64
|
+
}
|
|
65
|
+
return false
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Find all child component names used in the IR tree. */
|
|
69
|
+
function findChildComponentNames(node: IRNode): Set<string> {
|
|
70
|
+
const names = new Set<string>()
|
|
71
|
+
collectChildComponentNames(node, names)
|
|
72
|
+
return names
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function collectChildComponentNames(node: IRNode, names: Set<string>): void {
|
|
76
|
+
if (node.type === 'component') {
|
|
77
|
+
const comp = node as IRComponent
|
|
78
|
+
names.add(comp.name)
|
|
79
|
+
} else if (node.type === 'element') {
|
|
80
|
+
const element = node as IRElement
|
|
81
|
+
for (const child of element.children) {
|
|
82
|
+
collectChildComponentNames(child, names)
|
|
83
|
+
}
|
|
84
|
+
} else if (node.type === 'fragment') {
|
|
85
|
+
const fragment = node as IRFragment
|
|
86
|
+
for (const child of fragment.children) {
|
|
87
|
+
collectChildComponentNames(child, names)
|
|
88
|
+
}
|
|
89
|
+
} else if (node.type === 'conditional') {
|
|
90
|
+
const cond = node as IRConditional
|
|
91
|
+
collectChildComponentNames(cond.whenTrue, names)
|
|
92
|
+
if (cond.whenFalse) {
|
|
93
|
+
collectChildComponentNames(cond.whenFalse, names)
|
|
94
|
+
}
|
|
95
|
+
} else if (node.type === 'loop') {
|
|
96
|
+
const loop = node as IRLoop
|
|
97
|
+
for (const child of loop.children) {
|
|
98
|
+
collectChildComponentNames(child, names)
|
|
99
|
+
}
|
|
100
|
+
} else if (node.type === 'if-statement') {
|
|
101
|
+
const ifStmt = node as IRIfStatement
|
|
102
|
+
collectChildComponentNames(ifStmt.consequent, names)
|
|
103
|
+
if (ifStmt.alternate) {
|
|
104
|
+
collectChildComponentNames(ifStmt.alternate, names)
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Find all nested components (loops with `childComponent`). Returns extended
|
|
111
|
+
* info that includes whether the component comes from a dynamic (signal) array
|
|
112
|
+
* loop vs a static one.
|
|
113
|
+
*/
|
|
114
|
+
export function findNestedComponents(node: IRNode): NestedComponentInfo[] {
|
|
115
|
+
const result: NestedComponentInfo[] = []
|
|
116
|
+
collectNestedComponents(node, result)
|
|
117
|
+
return result
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function collectNestedComponents(node: IRNode, result: NestedComponentInfo[]): void {
|
|
121
|
+
if (node.type === 'loop') {
|
|
122
|
+
const loop = node as IRLoop
|
|
123
|
+
if (loop.childComponent) {
|
|
124
|
+
if (!result.some(c => c.name === loop.childComponent!.name)) {
|
|
125
|
+
const hasBodyChildren = loop.childComponent.children.length > 0
|
|
126
|
+
result.push({
|
|
127
|
+
...loop.childComponent,
|
|
128
|
+
isDynamic: !loop.isStaticArray,
|
|
129
|
+
isPropDerived: !!loop.isPropDerivedArray,
|
|
130
|
+
loopKey: loop.key ?? undefined,
|
|
131
|
+
loopParam: loop.param ?? undefined,
|
|
132
|
+
bodyChildren: hasBodyChildren ? loop.childComponent.children : undefined,
|
|
133
|
+
loopArray: loop.array,
|
|
134
|
+
loopArrayParsed: loop.arrayParsed,
|
|
135
|
+
loopMarkerId: loop.markerId,
|
|
136
|
+
loopItemType: loop.itemType,
|
|
137
|
+
})
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
for (const child of loop.children) {
|
|
141
|
+
collectNestedComponents(child, result)
|
|
142
|
+
}
|
|
143
|
+
} else if (node.type === 'element') {
|
|
144
|
+
const element = node as IRElement
|
|
145
|
+
for (const child of element.children) {
|
|
146
|
+
collectNestedComponents(child, result)
|
|
147
|
+
}
|
|
148
|
+
} else if (node.type === 'fragment') {
|
|
149
|
+
const fragment = node as IRFragment
|
|
150
|
+
for (const child of fragment.children) {
|
|
151
|
+
collectNestedComponents(child, result)
|
|
152
|
+
}
|
|
153
|
+
} else if (node.type === 'conditional') {
|
|
154
|
+
const cond = node as IRConditional
|
|
155
|
+
collectNestedComponents(cond.whenTrue, result)
|
|
156
|
+
if (cond.whenFalse) {
|
|
157
|
+
collectNestedComponents(cond.whenFalse, result)
|
|
158
|
+
}
|
|
159
|
+
} else if (node.type === 'if-statement') {
|
|
160
|
+
const stmt = node as IRIfStatement
|
|
161
|
+
collectNestedComponents(stmt.consequent, result)
|
|
162
|
+
if (stmt.alternate) {
|
|
163
|
+
collectNestedComponents(stmt.alternate, result)
|
|
164
|
+
}
|
|
165
|
+
} else if (node.type === 'component') {
|
|
166
|
+
// JSX children passed to an imported component render via a companion
|
|
167
|
+
// define with the PARENT's data, so a keyed loop nested inside them needs
|
|
168
|
+
// its `<Name>s` slice on THIS component's props like any other nested loop.
|
|
169
|
+
const comp = node as IRComponent
|
|
170
|
+
for (const child of comp.children) {
|
|
171
|
+
collectNestedComponents(child, result)
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The contract extracted emit modules depend on instead of the concrete
|
|
3
|
+
* `GoTemplateAdapter`.
|
|
4
|
+
*
|
|
5
|
+
* The Go adapter's lowering is deeply mutually recursive (expression ↔
|
|
6
|
+
* condition ↔ rendering), so a module pulled out still needs to call back into
|
|
7
|
+
* the shared per-compile state and the recursive entry points. `GoEmitContext`
|
|
8
|
+
* is that seam: extracted free functions take it as their first argument, and
|
|
9
|
+
* the adapter — which owns the state and implements the entry points — passes
|
|
10
|
+
* `this`. Modules depend on this narrow interface, so they stay unit-testable
|
|
11
|
+
* against a stub.
|
|
12
|
+
*
|
|
13
|
+
* Keep this surface minimal: add a member only when an extracted module
|
|
14
|
+
* genuinely needs it, so the seam documents the real cross-module coupling.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import type { ParsedExpr } from '@barefootjs/jsx'
|
|
18
|
+
|
|
19
|
+
import type { CompileState } from './lib/compile-state.ts'
|
|
20
|
+
|
|
21
|
+
export interface GoEmitContext {
|
|
22
|
+
/** Per-compile mutable state (signals, consts, type tables, errors, …). */
|
|
23
|
+
readonly state: CompileState
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Lower a JS expression to its Go-template form (the core recursive entry).
|
|
27
|
+
* `preParsed` reuses an already-built tree instead of re-parsing `jsExpr`.
|
|
28
|
+
*/
|
|
29
|
+
convertExpressionToGo(
|
|
30
|
+
jsExpr: string,
|
|
31
|
+
out?: { parsed?: ParsedExpr },
|
|
32
|
+
preParsed?: ParsedExpr,
|
|
33
|
+
): string
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Lower a JS condition to a Go-template bool + any hoisted preamble.
|
|
37
|
+
* `preParsed` reuses an already-built tree instead of re-parsing `jsCondition`.
|
|
38
|
+
*/
|
|
39
|
+
convertConditionToGo(
|
|
40
|
+
jsCondition: string,
|
|
41
|
+
preParsed?: ParsedExpr,
|
|
42
|
+
): { condition: string; preamble: string }
|
|
43
|
+
|
|
44
|
+
/** Extract the prop name from a `props.X ?? …` initial value, or null. */
|
|
45
|
+
extractPropNameFromInitialValue(initialValue: string): string | null
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Parse a signal-time initial value `props.X ?? <literal>` into the source
|
|
49
|
+
* prop name and the Go-formatted fallback, or null when it isn't that shape.
|
|
50
|
+
*/
|
|
51
|
+
extractPropFallback(initialValue: string): { propName: string; goFallback: string } | null
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Inline a module string const by name as a Go double-quoted literal
|
|
55
|
+
* (`"<escaped>"`), or null when the name is not such a const (loop vars and
|
|
56
|
+
* outer-loop params are excluded).
|
|
57
|
+
*/
|
|
58
|
+
resolveModuleStringConst(name: string): string | null
|
|
59
|
+
}
|