@barefootjs/go-template 0.14.0 → 0.15.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/src/build.ts CHANGED
@@ -165,6 +165,10 @@ export function combineGoTypes(options: {
165
165
  const usageScan = combinedContent + '\n' + (manualTypes ?? '')
166
166
  const stdlibImports: string[] = [`\t"math/rand"`]
167
167
  if (/\bfmt\./.test(usageScan)) stdlibImports.push(`\t"fmt"`)
168
+ // `strings.` shows up in generated constructors that normalize a prop, e.g.
169
+ // `searchParams()`-backed components emit `strings.TrimRight(in.Base, "/")`
170
+ // to derive the router base. Only imported when referenced, like the others.
171
+ if (/\bstrings\./.test(usageScan)) stdlibImports.push(`\t"strings"`)
168
172
  if (/\btemplate\.HTML\b/.test(usageScan)) stdlibImports.push(`\t"html/template"`)
169
173
  // gofmt sorts the stdlib group alphabetically by import path.
170
174
  stdlibImports.sort()
@@ -8,6 +8,7 @@
8
8
  import { compileJSX } from '@barefootjs/jsx'
9
9
  import type { TemplateAdapter, ComponentIR } from '@barefootjs/jsx'
10
10
  import { GoTemplateAdapter } from './adapter/go-template-adapter.ts'
11
+ import { deduplicateGoTypes } from './build.ts'
11
12
 
12
13
  /**
13
14
  * Capitalize a JSON key to its Go struct field name using the same
@@ -73,6 +74,14 @@ export interface RenderOptions {
73
74
  props?: Record<string, unknown>
74
75
  /** Additional component files (filename → source) */
75
76
  components?: Record<string, string>
77
+ /**
78
+ * Pre-compiled child SSR modules (import specifier → path) — consumed
79
+ * by the Hono renderer; here only the KEYS matter (#1896): they carry
80
+ * EVERY specifier a sibling is reachable by (`@ui/components/ui/icon`
81
+ * and `../icon`), whereas `components` keys carry one per sibling, so
82
+ * the sibling-import recognition uses both.
83
+ */
84
+ componentModules?: Record<string, string>
76
85
  /**
77
86
  * Explicit component to render when `source` declares multiple
78
87
  * exports (e.g. `ReactiveProps.tsx` → `PropsReactivityComparison`).
@@ -83,7 +92,7 @@ export interface RenderOptions {
83
92
  }
84
93
 
85
94
  export async function renderGoTemplateComponent(options: RenderOptions): Promise<string> {
86
- const { source, adapter, props, components, componentName: requestedName } = options
95
+ const { source, adapter, props, components, componentModules, componentName: requestedName } = options
87
96
 
88
97
  if (!adapter.generateTypes) {
89
98
  throw new Error('Go Template adapter must implement generateTypes()')
@@ -105,7 +114,22 @@ export async function renderGoTemplateComponent(options: RenderOptions): Promise
105
114
  importedNames: string[]
106
115
  }
107
116
  const childArtifacts = new Map<string, ChildComponentArtifacts>()
117
+ // Import specifiers the harness holds child sources for — recognised as
118
+ // sibling imports alongside relative ones (see
119
+ // `collectImportedComponentNames`).
120
+ const childSpecifiers = new Set([
121
+ ...Object.keys(components ?? {}),
122
+ ...Object.keys(componentModules ?? {}),
123
+ ])
108
124
  if (components) {
125
+ // Two passes (#1896): a child file may itself render a component from a
126
+ // *sibling* child file (data-table's `checkbox` renders `<CheckIcon
127
+ // data-slot=.../>` from `icon`). `generateTypes` routes such non-param
128
+ // attributes through the target's registered shape, so EVERY child's
129
+ // shape must be registered before ANY child's types are generated —
130
+ // otherwise processing order decides whether the attribute lands in
131
+ // the rest bag or becomes an invalid hyphenated Go field.
132
+ const compiledChildIrs: Array<{ ir: ComponentIR; defines: Map<string, string> }> = []
109
133
  for (const [filename, childSource] of Object.entries(components)) {
110
134
  const childResult = compileJSX(childSource, filename, { adapter, outputIR: true })
111
135
  const childErrors = childResult.errors.filter(e => e.severity === 'error')
@@ -119,26 +143,29 @@ export async function renderGoTemplateComponent(options: RenderOptions): Promise
119
143
  const childIrFiles = childResult.files.filter(f => f.type === 'ir')
120
144
  for (const childIrFile of childIrFiles) {
121
145
  const childIR = JSON.parse(childIrFile.content) as ComponentIR
122
- const name = childIR.metadata.componentName
123
146
  // (#checkbox) Register each child's cross-component shape so the
124
147
  // parent's static-child-init codegen can route a non-param attribute
125
148
  // (`<CheckIcon data-slot=.../>`) into the child's rest bag instead of
126
149
  // an invalid hyphenated Go field. No-op on adapters without the hook.
127
150
  registerChildShape(adapter, childIR)
128
- let typeBlock: string | null = adapter.generateTypes!(childIR)
129
- if (typeBlock) {
130
- // Strip package declaration and imports — will be merged into main types
131
- typeBlock = typeBlock.replace(/^package \w+\n*/, '')
132
- typeBlock = typeBlock.replace(/import\s*\([^)]*\)\n*/g, '')
133
- typeBlock = typeBlock.replace(/\t"math\/rand"\n/g, '')
134
- typeBlock = typeBlock.trim()
135
- }
136
- childArtifacts.set(name, {
137
- define: defineBlocks.get(name) ?? '',
138
- typeBlock,
139
- importedNames: collectImportedComponentNames(childIR),
140
- })
151
+ compiledChildIrs.push({ ir: childIR, defines: defineBlocks })
152
+ }
153
+ }
154
+ for (const { ir: childIR, defines } of compiledChildIrs) {
155
+ const name = childIR.metadata.componentName
156
+ let typeBlock: string | null = adapter.generateTypes!(childIR)
157
+ if (typeBlock) {
158
+ // Strip package declaration and imports — will be merged into main types
159
+ typeBlock = typeBlock.replace(/^package \w+\n*/, '')
160
+ typeBlock = typeBlock.replace(/import\s*\([^)]*\)\n*/g, '')
161
+ typeBlock = typeBlock.replace(/\t"math\/rand"\n/g, '')
162
+ typeBlock = typeBlock.trim()
141
163
  }
164
+ childArtifacts.set(name, {
165
+ define: defines.get(name) ?? '',
166
+ typeBlock,
167
+ importedNames: collectImportedComponentNames(childIR, childSpecifiers),
168
+ })
142
169
  }
143
170
  }
144
171
 
@@ -183,7 +210,13 @@ export async function renderGoTemplateComponent(options: RenderOptions): Promise
183
210
  // child-collection loop above).
184
211
  const reachableChildNames = new Set<string>()
185
212
  {
186
- const queue = [...collectImportedComponentNames(ir)]
213
+ // Seed from EVERY component of the parent source, not just the entry:
214
+ // the siblings' type blocks are merged into the unit too, and their
215
+ // constructors reference their own child components' Props types
216
+ // (#1896 — dropdown-menu-demo's profile sibling uses SettingsIcon,
217
+ // which the entry export never imports... the import list is per-file,
218
+ // but per-IR metadata can differ after analysis; union them all).
219
+ const queue = irs.flatMap(i => collectImportedComponentNames(i, childSpecifiers))
187
220
  while (queue.length > 0) {
188
221
  const name = queue.shift()!
189
222
  if (reachableChildNames.has(name)) continue
@@ -215,7 +248,8 @@ export async function renderGoTemplateComponent(options: RenderOptions): Promise
215
248
  // Remove "math/rand" import from types (randomID is defined in main.go)
216
249
  goTypes = goTypes.replace(/\t"math\/rand"\n/, '')
217
250
 
218
- // Append sibling-component type definitions (multi-component source).
251
+ // Collect sibling-component type definitions (multi-component source).
252
+ const siblingTypeBlocks: string[] = []
219
253
  for (const siblingIR of irs) {
220
254
  if (siblingIR === ir) continue
221
255
  let siblingTypes = adapter.generateTypes(siblingIR)
@@ -223,12 +257,27 @@ export async function renderGoTemplateComponent(options: RenderOptions): Promise
223
257
  siblingTypes = siblingTypes.replace(/^package \w+\n*/, '')
224
258
  siblingTypes = siblingTypes.replace(/import\s*\([^)]*\)\n*/g, '')
225
259
  siblingTypes = siblingTypes.replace(/\t"math\/rand"\n/g, '')
226
- goTypes += '\n\n' + siblingTypes.trim()
260
+ siblingTypeBlocks.push(siblingTypes.trim())
227
261
  }
228
262
 
229
- // Append child type definitions
230
- if (childTypeBlocks.length > 0) {
231
- goTypes += '\n\n' + childTypeBlocks.join('\n\n')
263
+ // Merge entry + sibling + child type blocks through the same
264
+ // `deduplicateGoTypes` helper `bf build` uses. Duplicates arise in two
265
+ // ways (#1896): a multi-component file emits its module-scope shared
266
+ // types (a context-value struct, a data `type Payment = …`) once per
267
+ // component IR — both across the entry source's own sibling exports
268
+ // (data-table-demo's `Payment redeclared`) and across a child file's
269
+ // components (radio-group's context value). The dedup helper re-inserts
270
+ // type definitions at the top of its input, so the entry's
271
+ // `package main` + import header is split off first and re-prepended.
272
+ if (siblingTypeBlocks.length > 0 || childTypeBlocks.length > 0) {
273
+ const headerMatch = goTypes.match(/^package main\n+import \([\s\S]*?\)\n+/)
274
+ const header = headerMatch ? headerMatch[0] : ''
275
+ const body = goTypes.slice(header.length)
276
+ goTypes =
277
+ header +
278
+ deduplicateGoTypes(
279
+ [body, ...siblingTypeBlocks, ...childTypeBlocks].join('\n\n'),
280
+ )
232
281
  }
233
282
 
234
283
  // Sibling / child type blocks have their own `import (...)` stripped above
@@ -322,7 +371,11 @@ func randomID(n int) string {
322
371
  }
323
372
 
324
373
  func main() {
325
- tmpl := template.Must(template.New("").Funcs(bfTestFuncMap()).Parse(tmplContent))
374
+ // Two-step Funcs: bf_tmpl (children defines, #1896) needs a closure
375
+ // over the same template set the defines are parsed into.
376
+ root := template.New("").Funcs(bfTestFuncMap())
377
+ root = root.Funcs(bf.TemplateFuncMap(root))
378
+ tmpl := template.Must(root.Parse(tmplContent))
326
379
  props := New${componentName}Props(${componentName}Input{
327
380
  ScopeID: ${JSON.stringify(rootScopeId)},
328
381
  ${propsInit}
@@ -360,7 +413,23 @@ ${propsInit}
360
413
 
361
414
  const exitCode = await proc.exited
362
415
  if (exitCode !== 0) {
363
- throw new Error(`go run failed (exit ${exitCode}):\n${stderr}`)
416
+ // Append the offending `types.go` lines (with two lines of context)
417
+ // for each `./types.go:NN` the compiler reported — the temp dir is
418
+ // deleted in `finally`, so without this the merged unit that
419
+ // actually failed is unrecoverable (#1896 debugging aid).
420
+ const typeLines = goTypes.split('\n')
421
+ const context: string[] = []
422
+ const seen = new Set<number>()
423
+ for (const m of stderr.matchAll(/\.\/types\.go:(\d+)/g)) {
424
+ const n = Number(m[1])
425
+ for (let i = Math.max(1, n - 2); i <= Math.min(typeLines.length, n + 2); i++) {
426
+ if (seen.has(i)) continue
427
+ seen.add(i)
428
+ context.push(`${i === n ? '>' : ' '} types.go:${i}: ${typeLines[i - 1]}`)
429
+ }
430
+ }
431
+ const detail = context.length > 0 ? `\n${context.join('\n')}` : ''
432
+ throw new Error(`go run failed (exit ${exitCode}):\n${stderr}${detail}`)
364
433
  }
365
434
 
366
435
  return stdout
@@ -418,14 +487,23 @@ function splitTemplateDefines(content: string): Map<string, string> {
418
487
 
419
488
  /**
420
489
  * Component names a component IR imports from sibling source files — i.e.
421
- * non-type imports from relative (`./` / `../`) specifiers. Used to compute
422
- * the transitive set of child components the combined Go unit needs.
490
+ * non-type imports from relative (`./` / `../`) specifiers, plus any
491
+ * specifier the harness was explicitly handed a child source for
492
+ * (`childSpecifiers`, the `components` map keys). The latter is how
493
+ * demo-corpus roots reach their children: they import primitives through
494
+ * the `@ui/components/ui/<name>` alias (#1467), which the relative-only
495
+ * test would silently drop — emitting a parent that references
496
+ * `New<Child>Props` without the child's type block. Used to compute the
497
+ * transitive set of child components the combined Go unit needs.
423
498
  */
424
- function collectImportedComponentNames(ir: ComponentIR): string[] {
499
+ function collectImportedComponentNames(
500
+ ir: ComponentIR,
501
+ childSpecifiers?: ReadonlySet<string>,
502
+ ): string[] {
425
503
  const names: string[] = []
426
504
  for (const imp of ir.metadata.imports ?? []) {
427
505
  if (imp.isTypeOnly) continue
428
- if (!imp.source.startsWith('.')) continue
506
+ if (!imp.source.startsWith('.') && !childSpecifiers?.has(imp.source)) continue
429
507
  for (const spec of imp.specifiers ?? []) {
430
508
  if (spec.isNamespace) continue
431
509
  // Imported binding name (the local alias is what JSX references, but the
@@ -457,6 +535,10 @@ function registerChildShape(adapter: TemplateAdapter, ir: ComponentIR): void {
457
535
  */
458
536
  const MERGED_STDLIB_IMPORTS: Array<{ path: string; usage: RegExp }> = [
459
537
  { path: 'fmt', usage: /\bfmt\./ },
538
+ // A child whose constructor bakes static JSX children references
539
+ // `template.HTML(...)` (#1896 — the `command` demo's Kbd child); the
540
+ // entry component may not use html/template itself.
541
+ { path: 'html/template', usage: /\btemplate\.HTML\(/ },
460
542
  ]
461
543
 
462
544
  /**