@barefootjs/go-template 0.18.4 → 0.18.7

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.
Files changed (37) hide show
  1. package/dist/adapter/analysis/static-child-loop-bake.d.ts +61 -0
  2. package/dist/adapter/analysis/static-child-loop-bake.d.ts.map +1 -0
  3. package/dist/adapter/analysis/static-element-loop-bake.d.ts +83 -0
  4. package/dist/adapter/analysis/static-element-loop-bake.d.ts.map +1 -0
  5. package/dist/adapter/go-template-adapter.d.ts +151 -3
  6. package/dist/adapter/go-template-adapter.d.ts.map +1 -1
  7. package/dist/adapter/index.js +500 -52
  8. package/dist/adapter/lib/compile-state.d.ts +15 -0
  9. package/dist/adapter/lib/compile-state.d.ts.map +1 -1
  10. package/dist/adapter/lib/constants.d.ts.map +1 -1
  11. package/dist/adapter/memo/memo-compute.d.ts.map +1 -1
  12. package/dist/adapter/props/prop-classes.d.ts +40 -0
  13. package/dist/adapter/props/prop-classes.d.ts.map +1 -0
  14. package/dist/adapter/props/prop-types.d.ts.map +1 -1
  15. package/dist/adapter/type/type-codegen.d.ts +5 -1
  16. package/dist/adapter/type/type-codegen.d.ts.map +1 -1
  17. package/dist/adapter/value/value-lowering.d.ts.map +1 -1
  18. package/dist/build.js +500 -52
  19. package/dist/conformance-pins.d.ts.map +1 -1
  20. package/dist/index.js +503 -79
  21. package/dist/render-divergences.d.ts.map +1 -1
  22. package/dist/test-render.d.ts.map +1 -1
  23. package/package.json +3 -3
  24. package/src/__tests__/go-template-adapter.test.ts +708 -4
  25. package/src/adapter/analysis/static-child-loop-bake.ts +119 -0
  26. package/src/adapter/analysis/static-element-loop-bake.ts +211 -0
  27. package/src/adapter/go-template-adapter.ts +661 -34
  28. package/src/adapter/lib/compile-state.ts +17 -0
  29. package/src/adapter/lib/constants.ts +1 -0
  30. package/src/adapter/memo/memo-compute.ts +37 -9
  31. package/src/adapter/props/prop-classes.ts +70 -0
  32. package/src/adapter/props/prop-types.ts +69 -1
  33. package/src/adapter/type/type-codegen.ts +19 -2
  34. package/src/adapter/value/value-lowering.ts +27 -2
  35. package/src/conformance-pins.ts +30 -36
  36. package/src/render-divergences.ts +12 -30
  37. package/src/test-render.ts +131 -13
@@ -6,10 +6,12 @@
6
6
  */
7
7
 
8
8
  import { compileJSX } from '@barefootjs/jsx'
9
- import type { TemplateAdapter, ComponentIR } from '@barefootjs/jsx'
9
+ import type { TemplateAdapter, ComponentIR, ParsedExpr } from '@barefootjs/jsx'
10
10
  import { GoTemplateAdapter } from './adapter/go-template-adapter.ts'
11
11
  import { deduplicateGoTypes } from './build.ts'
12
- import { capitalizeFieldName } from './adapter/lib/go-naming.ts'
12
+ import { capitalizeFieldName, goFieldNameForKey, loopKeyToGoFieldPath } from './adapter/lib/go-naming.ts'
13
+ import { findNestedComponents } from './adapter/analysis/component-tree.ts'
14
+ import type { NestedComponentInfo } from './adapter/lib/types.ts'
13
15
  import { mkdir, rm } from 'node:fs/promises'
14
16
  import { resolve } from 'node:path'
15
17
 
@@ -157,8 +159,15 @@ export async function renderGoTemplateComponent(options: RenderOptions): Promise
157
159
  }
158
160
  }
159
161
 
160
- // Compile parent source
161
- const result = compileJSX(source, 'component.tsx', { adapter, outputIR: true })
162
+ // Compile parent source. `siblingTemplatesRegistered: Boolean(components)`
163
+ // matches this harness's real behavior every sibling child template is concatenated
164
+ // into `tmplContent` and parsed onto one `*template.Template` instance
165
+ // below, so a loop-body cross-template call resolves at render time (#2205).
166
+ const result = compileJSX(source, 'component.tsx', {
167
+ adapter,
168
+ outputIR: true,
169
+ siblingTemplatesRegistered: Boolean(components),
170
+ })
162
171
 
163
172
  const errors = result.errors.filter(e => e.severity === 'error')
164
173
  if (errors.length > 0) {
@@ -319,11 +328,16 @@ export async function renderGoTemplateComponent(options: RenderOptions): Promise
319
328
  // cross-adapter; default to 'test' otherwise.
320
329
  const rootScopeId = typeof props?.__instanceId === 'string' ? props.__instanceId : 'test'
321
330
 
331
+ // (#2209 part 2) Route-handler-equivalent seeding for a signal-backed
332
+ // dynamic child-component loop — see buildDynamicChildLoopSeeding's
333
+ // docstring.
334
+ const { lines: dynamicSeedingLines, needsFmt } = buildDynamicChildLoopSeeding(ir, template)
335
+
322
336
  // main.go — render program
323
337
  const mainGo = `package main
324
338
 
325
339
  import (
326
- "html/template"
340
+ ${needsFmt ? '\t"fmt"\n' : ''} "html/template"
327
341
  "math/rand"
328
342
  "os"
329
343
 
@@ -368,7 +382,7 @@ func main() {
368
382
  ScopeID: ${JSON.stringify(rootScopeId)},
369
383
  ${propsInit}
370
384
  })
371
- if err := tmpl.ExecuteTemplate(os.Stdout, "${componentName}", props); err != nil {
385
+ ${dynamicSeedingLines.length > 0 ? dynamicSeedingLines.join('\n') + '\n' : ''} if err := tmpl.ExecuteTemplate(os.Stdout, "${componentName}", props); err != nil {
372
386
  os.Stderr.WriteString("template error: " + err.Error() + "\\n")
373
387
  os.Exit(1)
374
388
  }
@@ -551,6 +565,95 @@ function ensureMergedStdlibImports(goTypes: string): string {
551
565
  return goTypes.replace(/import\s*\([^)]*\)/, newBlock)
552
566
  }
553
567
 
568
+ /**
569
+ * Recursively resolve `expr` (a loop's `arrayParsed`) down through
570
+ * `call`/`member` chains to the base signal getter it reads
571
+ * (`todos().filter(...)` → `todos`), or `null` when the base isn't a
572
+ * signal getter call. Structural `ParsedExpr` walk, not string/regex
573
+ * parsing (see CLAUDE.md's "never parse JS with regex" rule).
574
+ */
575
+ function findBaseSignalGetter(expr: ParsedExpr | undefined, signalGetters: ReadonlySet<string>): string | null {
576
+ if (!expr) return null
577
+ switch (expr.kind) {
578
+ case 'identifier':
579
+ return signalGetters.has(expr.name) ? expr.name : null
580
+ case 'call':
581
+ return findBaseSignalGetter(expr.callee, signalGetters)
582
+ case 'member':
583
+ return findBaseSignalGetter(expr.object, signalGetters)
584
+ default:
585
+ return null
586
+ }
587
+ }
588
+
589
+ /**
590
+ * (#2209 part 2) Replicate, in the generated `main.go`, the documented
591
+ * "the route handler populates the loop-body child-component slice at
592
+ * request time" contract for a signal-backed dynamic loop —
593
+ * `generateNewPropsFunction`'s doc comment on `<Name>s []<Name>Props`
594
+ * in `adapter/go-template-adapter.ts`. The constructor only ever seeds
595
+ * the loop's DATUM slice (e.g. `.Todos`, straight from the caller's
596
+ * Input); the child-component Props slice the template actually
597
+ * ranges over (`.TodoItems`) has no server-side population path in
598
+ * this harness — the Hono reference materializes it by literally
599
+ * executing the component, so this closes the gap the same way:
600
+ * derive each item's child Props from the datum slice, exactly as a
601
+ * real route handler is documented to.
602
+ *
603
+ * Deliberately narrow: only fires for a loop whose (a) array source
604
+ * resolves, through `call`/`member` chains, to a component signal
605
+ * getter, (b) generated Go TEMPLATE text actually ranges over
606
+ * `.<Name>s` (a plain substring check on GENERATED GO OUTPUT — not JS
607
+ * parsing — so a `/* @client *\/`-marked loop, whose SSR template has
608
+ * no such range, is untouched by construction), and (c) at least one
609
+ * child prop is a bare pass-through of the loop item (`todo={todo}`).
610
+ * Returns the Go statements to splice into `main()` plus whether `fmt`
611
+ * needs importing.
612
+ */
613
+ function buildDynamicChildLoopSeeding(
614
+ ir: ComponentIR,
615
+ template: string,
616
+ ): { lines: string[]; needsFmt: boolean } {
617
+ const signalGetters = new Set(ir.metadata.signals.map(s => s.getter))
618
+ const lines: string[] = []
619
+ let needsFmt = false
620
+ for (const nested of findNestedComponents(ir.root) as NestedComponentInfo[]) {
621
+ if (!nested.isDynamic || nested.isPropDerived) continue
622
+ if (nested.bodyChildren && nested.bodyChildren.length > 0) continue
623
+ if (!nested.loopParam) continue
624
+ if (!template.includes(`:= .${nested.name}s}}`)) continue
625
+ const datumField = findBaseSignalGetter(nested.loopArrayParsed, signalGetters)
626
+ if (!datumField) continue
627
+
628
+ const inputFields: string[] = []
629
+ for (const prop of nested.props) {
630
+ if (prop.isEventHandler) continue
631
+ if (prop.name === 'key' || prop.name.includes('-')) continue
632
+ if (
633
+ prop.value.kind === 'expression' &&
634
+ prop.value.parsed?.kind === 'identifier' &&
635
+ prop.value.parsed.name === nested.loopParam
636
+ ) {
637
+ inputFields.push(`${capitalizeFieldName(prop.name)}: item`)
638
+ }
639
+ }
640
+ if (inputFields.length === 0) continue
641
+
642
+ lines.push(`\tprops.${nested.name}s = make([]${nested.name}Props, len(props.${capitalizeFieldName(datumField)}))`)
643
+ lines.push(`\tfor i, item := range props.${capitalizeFieldName(datumField)} {`)
644
+ lines.push(`\t\tprops.${nested.name}s[i] = New${nested.name}Props(${nested.name}Input{${inputFields.join(', ')}})`)
645
+ lines.push(`\t\tprops.${nested.name}s[i].BfParent = props.ScopeID`)
646
+ lines.push(`\t\tprops.${nested.name}s[i].BfMount = ${JSON.stringify(nested.slotId ?? '')}`)
647
+ const keyField = loopKeyToGoFieldPath(nested.loopKey, nested.loopParam)
648
+ if (keyField) {
649
+ lines.push(`\t\tprops.${nested.name}s[i].BfDataKey = fmt.Sprint(${keyField})`)
650
+ needsFmt = true
651
+ }
652
+ lines.push(`\t}`)
653
+ }
654
+ return { lines, needsFmt }
655
+ }
656
+
554
657
  /**
555
658
  * Build Go struct field initializers from props.
556
659
  */
@@ -572,9 +675,7 @@ function buildGoPropsInit(
572
675
  // in struct literal of type InputInput`. (#1467 Phase 2b)
573
676
  const declaredParams = new Set((ir?.metadata.propsParams ?? []).map(p => p.name))
574
677
  const restPropsName = ir?.metadata.restPropsName ?? null
575
- const restBagField = restPropsName
576
- ? restPropsName.charAt(0).toUpperCase() + restPropsName.slice(1)
577
- : null
678
+ const restBagField = restPropsName ? capitalizeFieldName(restPropsName) : null
578
679
 
579
680
  const lines: string[] = []
580
681
  const restBagEntries: Array<[string, unknown]> = []
@@ -594,8 +695,9 @@ function buildGoPropsInit(
594
695
  restBagEntries.push([key, value])
595
696
  continue
596
697
  }
597
- // Capitalize first letter for Go field name
598
- const goField = key.charAt(0).toUpperCase() + key.slice(1)
698
+ // Same Go-initialism-aware capitalizer as the real adapter (`id` → `ID`,
699
+ // not the naive `Id`) see `goMapLiteralFromObject`'s identical fix.
700
+ const goField = capitalizeFieldName(key)
599
701
  if (typeof value === 'string') {
600
702
  lines.push(`\t\t${goField}: "${value}",`)
601
703
  } else if (typeof value === 'number') {
@@ -729,7 +831,11 @@ function goTypedMapSliceLiteralFromArray(arr: unknown[], elemType: string): stri
729
831
  function goStructLiteral(obj: Record<string, unknown>, typeName: string): string {
730
832
  const fields: string[] = []
731
833
  for (const [k, v] of Object.entries(obj)) {
732
- const goField = capitalizeFieldName(k)
834
+ // `goFieldNameForKey`, not the bare `capitalizeFieldName` — a data-driven
835
+ // key here can be non-identifier-shaped (`'data-x'`), and the real
836
+ // adapter's own struct-literal baking (`parsed-literal-to-go.ts`)
837
+ // sanitizes those to `DataX`, not `Data-x` (Copilot review, #2202).
838
+ const goField = goFieldNameForKey(k)
733
839
  if (typeof v === 'string') fields.push(`${goField}: "${v.replace(/"/g, '\\"')}"`)
734
840
  else if (typeof v === 'number' || typeof v === 'boolean') fields.push(`${goField}: ${v}`)
735
841
  else if (v === null) fields.push(`${goField}: nil`)
@@ -766,7 +872,19 @@ function goMapLiteralFromObject(
766
872
  ): string {
767
873
  const entries: string[] = []
768
874
  for (const [k, v] of Object.entries(obj)) {
769
- const emittedKey = capitalizeKeys ? k.charAt(0).toUpperCase() + k.slice(1) : k
875
+ // `goFieldNameForKey`, not a naive first-letter uppercase and not the
876
+ // bare `capitalizeFieldName` — #2168 nested-loop-triple-depth: a naive
877
+ // capitalize disagrees with the real adapter's Go-initialism-aware
878
+ // field naming for a key like `id` (naive → "Id", adapter's generated
879
+ // struct/template field → "ID"), so the harness baked a literal the
880
+ // template's `{{.ID}}` lookup could never match — the fixture's
881
+ // rendered fields came back empty at EVERY nesting depth for this
882
+ // reason, not because of any depth limit. `capitalizeFieldName` alone
883
+ // fixes the initialism case but still mis-bakes a non-identifier key
884
+ // (`'data-x'` → `"Data-x"`, not the adapter's own `"DataX"` —
885
+ // Copilot review, #2202); `goFieldNameForKey` is what the real adapter
886
+ // uses for exactly this key-to-Go-field sanitization.
887
+ const emittedKey = capitalizeKeys ? goFieldNameForKey(k) : k
770
888
  const key = JSON.stringify(emittedKey)
771
889
  if (typeof v === 'string') entries.push(`${key}: "${v.replace(/"/g, '\\"')}"`)
772
890
  else if (typeof v === 'number') entries.push(`${key}: ${v}`)