@barefootjs/go-template 0.8.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -68,9 +68,22 @@ export async function renderGoTemplateComponent(options: RenderOptions): Promise
68
68
  throw new Error('Go Template adapter must implement generateTypes()')
69
69
  }
70
70
 
71
- // Compile child components first
72
- const childTemplates: string[] = []
73
- const childTypeBlocks: string[] = []
71
+ // Compile child components first, keeping per-component template defines /
72
+ // type blocks keyed by component name. A child *file* can export many
73
+ // components (e.g. `../icon` exports 30+ icons); only the ones the parent
74
+ // transitively references are emitted into the combined unit. Emitting all
75
+ // of them concatenates dead components whose own codegen may not compile
76
+ // (e.g. `ChevronDownIcon`'s `strokePaths['chevron-down']` lowers to an
77
+ // invalid `{{.StrokePaths.Chevron-down}}` field reference), which would
78
+ // break the whole template parse even though the parent never uses them.
79
+ // (#checkbox)
80
+ interface ChildComponentArtifacts {
81
+ define: string
82
+ typeBlock: string | null
83
+ /** Component names this child component imports from sibling files. */
84
+ importedNames: string[]
85
+ }
86
+ const childArtifacts = new Map<string, ChildComponentArtifacts>()
74
87
  if (components) {
75
88
  for (const [filename, childSource] of Object.entries(components)) {
76
89
  const childResult = compileJSX(childSource, filename, { adapter, outputIR: true })
@@ -80,19 +93,30 @@ export async function renderGoTemplateComponent(options: RenderOptions): Promise
80
93
  }
81
94
  const childTemplate = childResult.files.find(f => f.type === 'markedTemplate')
82
95
  if (!childTemplate) throw new Error(`No marked template for ${filename}`)
83
- childTemplates.push(childTemplate.content)
96
+ const defineBlocks = splitTemplateDefines(childTemplate.content)
84
97
 
85
98
  const childIrFiles = childResult.files.filter(f => f.type === 'ir')
86
99
  for (const childIrFile of childIrFiles) {
87
100
  const childIR = JSON.parse(childIrFile.content) as ComponentIR
88
- let childTypes = adapter.generateTypes!(childIR)
89
- if (childTypes) {
101
+ const name = childIR.metadata.componentName
102
+ // (#checkbox) Register each child's cross-component shape so the
103
+ // parent's static-child-init codegen can route a non-param attribute
104
+ // (`<CheckIcon data-slot=.../>`) into the child's rest bag instead of
105
+ // an invalid hyphenated Go field. No-op on adapters without the hook.
106
+ registerChildShape(adapter, childIR)
107
+ let typeBlock: string | null = adapter.generateTypes!(childIR)
108
+ if (typeBlock) {
90
109
  // Strip package declaration and imports — will be merged into main types
91
- childTypes = childTypes.replace(/^package \w+\n*/, '')
92
- childTypes = childTypes.replace(/import\s*\([^)]*\)\n*/g, '')
93
- childTypes = childTypes.replace(/\t"math\/rand"\n/g, '')
94
- childTypeBlocks.push(childTypes.trim())
110
+ typeBlock = typeBlock.replace(/^package \w+\n*/, '')
111
+ typeBlock = typeBlock.replace(/import\s*\([^)]*\)\n*/g, '')
112
+ typeBlock = typeBlock.replace(/\t"math\/rand"\n/g, '')
113
+ typeBlock = typeBlock.trim()
95
114
  }
115
+ childArtifacts.set(name, {
116
+ define: defineBlocks.get(name) ?? '',
117
+ typeBlock,
118
+ importedNames: collectImportedComponentNames(childIR),
119
+ })
96
120
  }
97
121
  }
98
122
  }
@@ -120,6 +144,37 @@ export async function renderGoTemplateComponent(options: RenderOptions): Promise
120
144
  irs.find(i => i.metadata.isExported) ??
121
145
  irs[0]
122
146
 
147
+ // (#checkbox) Register each in-source sibling's shape too, so a parent
148
+ // rendering an inline sibling routes non-param attributes into the
149
+ // sibling's rest bag (same fix as the auto-inferred `../<name>` children).
150
+ for (const siblingIR of irs) registerChildShape(adapter, siblingIR)
151
+
152
+ // (#checkbox) Resolve which child components the combined unit actually
153
+ // needs: start from the entry component's cross-file imports, then close
154
+ // transitively over each included child's own imports. Components a child
155
+ // *file* exports but nobody references are dropped (see comment at the
156
+ // child-collection loop above).
157
+ const reachableChildNames = new Set<string>()
158
+ {
159
+ const queue = [...collectImportedComponentNames(ir)]
160
+ while (queue.length > 0) {
161
+ const name = queue.shift()!
162
+ if (reachableChildNames.has(name)) continue
163
+ const artifact = childArtifacts.get(name)
164
+ if (!artifact) continue // not a compiled child (e.g. an in-source sibling)
165
+ reachableChildNames.add(name)
166
+ for (const dep of artifact.importedNames) queue.push(dep)
167
+ }
168
+ }
169
+ const childTemplates: string[] = []
170
+ const childTypeBlocks: string[] = []
171
+ for (const name of reachableChildNames) {
172
+ const artifact = childArtifacts.get(name)
173
+ if (!artifact) continue
174
+ if (artifact.define) childTemplates.push(artifact.define)
175
+ if (artifact.typeBlock) childTypeBlocks.push(artifact.typeBlock)
176
+ }
177
+
123
178
  // Generate types for the entry-point component first, then append
124
179
  // types for every sibling component in the same source file so the
125
180
  // generated `types.go` is self-contained (multi-component test
@@ -149,6 +204,18 @@ export async function renderGoTemplateComponent(options: RenderOptions): Promise
149
204
  goTypes += '\n\n' + childTypeBlocks.join('\n\n')
150
205
  }
151
206
 
207
+ // Sibling / child type blocks have their own `import (...)` stripped above
208
+ // (their package decl + imports are merged into the entry component's
209
+ // single `types.go` block). When a sibling's generated constructor uses a
210
+ // standard-library symbol the entry component doesn't — e.g. CheckIcon's
211
+ // `NewCheckIconProps` calls `fmt.Sprint(...)` for a `Record[key]` spread
212
+ // lookup (#checkbox icon) — that import would be lost, producing
213
+ // `undefined: fmt`. Re-add any such import to the entry component's import
214
+ // block so the combined compilation unit resolves the symbol. Only `fmt`
215
+ // is needed today; extend `MERGED_STDLIB_IMPORTS` if a future sibling pulls
216
+ // in another stdlib package.
217
+ goTypes = ensureMergedStdlibImports(goTypes)
218
+
152
219
  const componentName = ir.metadata.componentName
153
220
  // Concatenate all templates (child define blocks + parent)
154
221
  const template = [...childTemplates, templateFile.content].join('\n')
@@ -274,6 +341,118 @@ ${propsInit}
274
341
  }
275
342
  }
276
343
 
344
+ /**
345
+ * Split a marked-template file's content into per-component `{{define "X"}}…
346
+ * {{end}}` blocks, keyed by component name. A child file that exports multiple
347
+ * components yields one entry per component so the harness can emit only the
348
+ * referenced ones (#checkbox). Non-define preamble (e.g. `export type {...}`)
349
+ * is ignored — it isn't valid Go template text and the entry component carries
350
+ * its own.
351
+ */
352
+ function splitTemplateDefines(content: string): Map<string, string> {
353
+ const out = new Map<string, string>()
354
+ // Block actions that open a scope closed by `{{end}}`. A `{{define}}` body
355
+ // can contain nested `{{if}}` / `{{range}}` / `{{with}}` / `{{block}}`, so a
356
+ // non-greedy `.*?{{end}}` would stop at the first inner `{{end}}`. Track
357
+ // nesting depth and capture from each `{{define}}` to its matching `{{end}}`.
358
+ const actionRe = /\{\{[-\s]*(define|if|range|with|block|end)\b/g
359
+ let m: RegExpExecArray | null
360
+ let openName: string | null = null
361
+ let openStart = 0
362
+ let depth = 0
363
+ while ((m = actionRe.exec(content)) !== null) {
364
+ const kw = m[1]
365
+ if (kw === 'end') {
366
+ if (openName !== null) {
367
+ depth--
368
+ if (depth === 0) {
369
+ // Extend to the close of this `{{...end...}}` action.
370
+ const closeIdx = content.indexOf('}}', m.index)
371
+ const end = closeIdx === -1 ? content.length : closeIdx + 2
372
+ out.set(openName, content.slice(openStart, end))
373
+ openName = null
374
+ }
375
+ }
376
+ continue
377
+ }
378
+ if (kw === 'define' && openName === null) {
379
+ const nameMatch = content.slice(m.index).match(/^\{\{[-\s]*define\s+"([^"]+)"/)
380
+ openName = nameMatch ? nameMatch[1] : null
381
+ openStart = m.index
382
+ depth = 1
383
+ continue
384
+ }
385
+ // Any opening action while inside a define deepens the nesting.
386
+ if (openName !== null) depth++
387
+ }
388
+ return out
389
+ }
390
+
391
+ /**
392
+ * Component names a component IR imports from sibling source files — i.e.
393
+ * non-type imports from relative (`./` / `../`) specifiers. Used to compute
394
+ * the transitive set of child components the combined Go unit needs.
395
+ */
396
+ function collectImportedComponentNames(ir: ComponentIR): string[] {
397
+ const names: string[] = []
398
+ for (const imp of ir.metadata.imports ?? []) {
399
+ if (imp.isTypeOnly) continue
400
+ if (!imp.source.startsWith('.')) continue
401
+ for (const spec of imp.specifiers ?? []) {
402
+ if (spec.isNamespace) continue
403
+ // Imported binding name (the local alias is what JSX references, but the
404
+ // generated `{{define}}`/types are keyed by the exported component name —
405
+ // which equals the specifier name when un-aliased; aliased component
406
+ // imports aren't exercised by the UI fixtures).
407
+ names.push(spec.alias ?? spec.name)
408
+ }
409
+ }
410
+ return names
411
+ }
412
+
413
+ /**
414
+ * Register a child/sibling component's cross-component shape on the adapter
415
+ * when it supports the `registerChildComponentShape` hook (Go template). A
416
+ * no-op on adapters without it. Lets the parent's static-child-init codegen
417
+ * route a non-param attribute into the child's rest bag (#checkbox).
418
+ */
419
+ function registerChildShape(adapter: TemplateAdapter, ir: ComponentIR): void {
420
+ const hook = (adapter as { registerChildComponentShape?: (ir: ComponentIR) => void })
421
+ .registerChildComponentShape
422
+ if (typeof hook === 'function') hook.call(adapter, ir)
423
+ }
424
+
425
+ /**
426
+ * Standard-library packages that a merged sibling/child type block may
427
+ * reference but the entry component's own import block omits. Each entry maps
428
+ * the import path to a `<pkg>.` usage probe.
429
+ */
430
+ const MERGED_STDLIB_IMPORTS: Array<{ path: string; usage: RegExp }> = [
431
+ { path: 'fmt', usage: /\bfmt\./ },
432
+ ]
433
+
434
+ /**
435
+ * Ensure any standard-library import a merged sibling/child block needs is
436
+ * present in the entry component's `import (...)` block. Sibling/child blocks
437
+ * have their own imports stripped during merge, so a symbol they reference
438
+ * (e.g. `fmt.Sprint`) is otherwise `undefined` in the combined unit.
439
+ */
440
+ function ensureMergedStdlibImports(goTypes: string): string {
441
+ const importMatch = goTypes.match(/import\s*\(([^)]*)\)/)
442
+ if (!importMatch) return goTypes
443
+ const block = importMatch[1]
444
+ const additions: string[] = []
445
+ for (const { path, usage } of MERGED_STDLIB_IMPORTS) {
446
+ if (!usage.test(goTypes)) continue
447
+ // Already imported? (quoted path present in the import block)
448
+ if (block.includes(`"${path}"`)) continue
449
+ additions.push(`\t"${path}"`)
450
+ }
451
+ if (additions.length === 0) return goTypes
452
+ const newBlock = `import (\n${additions.join('\n')}\n${block.replace(/^\n+/, '')})`
453
+ return goTypes.replace(/import\s*\([^)]*\)/, newBlock)
454
+ }
455
+
277
456
  /**
278
457
  * Build Go struct field initializers from props.
279
458
  */