@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.
- package/dist/adapter/analysis/static-child-loop-bake.d.ts +61 -0
- package/dist/adapter/analysis/static-child-loop-bake.d.ts.map +1 -0
- package/dist/adapter/analysis/static-element-loop-bake.d.ts +83 -0
- package/dist/adapter/analysis/static-element-loop-bake.d.ts.map +1 -0
- package/dist/adapter/go-template-adapter.d.ts +151 -3
- package/dist/adapter/go-template-adapter.d.ts.map +1 -1
- package/dist/adapter/index.js +500 -52
- package/dist/adapter/lib/compile-state.d.ts +15 -0
- package/dist/adapter/lib/compile-state.d.ts.map +1 -1
- package/dist/adapter/lib/constants.d.ts.map +1 -1
- package/dist/adapter/memo/memo-compute.d.ts.map +1 -1
- package/dist/adapter/props/prop-classes.d.ts +40 -0
- package/dist/adapter/props/prop-classes.d.ts.map +1 -0
- package/dist/adapter/props/prop-types.d.ts.map +1 -1
- package/dist/adapter/type/type-codegen.d.ts +5 -1
- package/dist/adapter/type/type-codegen.d.ts.map +1 -1
- package/dist/adapter/value/value-lowering.d.ts.map +1 -1
- package/dist/build.js +500 -52
- package/dist/conformance-pins.d.ts.map +1 -1
- package/dist/index.js +503 -79
- package/dist/render-divergences.d.ts.map +1 -1
- package/dist/test-render.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/__tests__/go-template-adapter.test.ts +708 -4
- package/src/adapter/analysis/static-child-loop-bake.ts +119 -0
- package/src/adapter/analysis/static-element-loop-bake.ts +211 -0
- package/src/adapter/go-template-adapter.ts +661 -34
- package/src/adapter/lib/compile-state.ts +17 -0
- package/src/adapter/lib/constants.ts +1 -0
- package/src/adapter/memo/memo-compute.ts +37 -9
- package/src/adapter/props/prop-classes.ts +70 -0
- package/src/adapter/props/prop-types.ts +69 -1
- package/src/adapter/type/type-codegen.ts +19 -2
- package/src/adapter/value/value-lowering.ts +27 -2
- package/src/conformance-pins.ts +30 -36
- package/src/render-divergences.ts +12 -30
- package/src/test-render.ts +131 -13
package/src/test-render.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
//
|
|
598
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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}`)
|