@barefootjs/go-template 0.18.5 → 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 +116 -2
- package/dist/adapter/go-template-adapter.d.ts.map +1 -1
- package/dist/adapter/index.js +339 -32
- package/dist/adapter/lib/compile-state.d.ts +8 -0
- package/dist/adapter/lib/compile-state.d.ts.map +1 -1
- package/dist/adapter/props/prop-classes.d.ts +28 -9
- package/dist/adapter/props/prop-classes.d.ts.map +1 -1
- package/dist/build.js +339 -32
- package/dist/conformance-pins.d.ts.map +1 -1
- package/dist/index.js +341 -41
- 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 +707 -3
- 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 +435 -25
- package/src/adapter/lib/compile-state.ts +9 -0
- package/src/adapter/props/prop-classes.ts +34 -9
- package/src/conformance-pins.ts +30 -31
- package/src/render-divergences.ts +12 -0
- package/src/test-render.ts +109 -6
|
@@ -66,6 +66,15 @@ export class CompileState {
|
|
|
66
66
|
*/
|
|
67
67
|
localConstants: IRMetadata['localConstants'] = []
|
|
68
68
|
|
|
69
|
+
/**
|
|
70
|
+
* Every name a `.map()`/`.filter()` loop callback binds as its item/index
|
|
71
|
+
* parameter anywhere in the component (#2208 fable review). Consulted by
|
|
72
|
+
* static loop-source resolution (`getBakedStaticChildLoop`) so a const
|
|
73
|
+
* whose name a DIFFERENT, enclosing loop's own callback param shadows is
|
|
74
|
+
* never resolved as that const's static value.
|
|
75
|
+
*/
|
|
76
|
+
staticLoopSourceBoundNames: Set<string> = new Set()
|
|
77
|
+
|
|
69
78
|
/**
|
|
70
79
|
* Names of component-scope arrow-const helpers (`const sortClass = …`),
|
|
71
80
|
* eligible for call-site inlining.
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* function over `ir.metadata`; no adapter instance state.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import type
|
|
10
|
+
import { collectLoopBoundNames, type ComponentIR, type TypeInfo } from '@barefootjs/jsx'
|
|
11
11
|
|
|
12
12
|
/** True when `type` is the `string` primitive. */
|
|
13
13
|
function isStringTypeInfo(type: TypeInfo): boolean {
|
|
@@ -22,14 +22,33 @@ function isBareStringLiteral(initialValue: string | undefined): boolean {
|
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
/**
|
|
25
|
-
* String-typed signals
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
25
|
+
* String-typed signals, props, and same-file local consts (#2212, ported
|
|
26
|
+
* here for #2236). A signal is string-typed when its inferred type is
|
|
27
|
+
* `string` (or, defensively, when its initial value is a bare string
|
|
28
|
+
* literal); a prop when its annotated type is `string`; a local const the
|
|
29
|
+
* same way. Drives `isStringName` for `isStringConcatBinary` — the shared
|
|
30
|
+
* helper (`@barefootjs/jsx`) that decides whether a JS `+` is string
|
|
31
|
+
* concatenation rather than numeric addition (Go's `html/template` has no
|
|
32
|
+
* native `+` at all; `binary()` always emits a runtime call, `bf_add` for
|
|
33
|
+
* addition or `bf_concat_str` for concatenation — see
|
|
34
|
+
* `go-template-adapter.ts`'s `binary()`). Local consts matter for exactly
|
|
35
|
+
* the shadowing shape this exclusion exists for: with a loop-bound `label`
|
|
36
|
+
* subtracted, an outer `{label + suffix}` (where `suffix = '!'`) must
|
|
37
|
+
* still classify as string concat via its OTHER operand, or it would fall
|
|
38
|
+
* back to `bf_add` and render `0`.
|
|
39
|
+
*
|
|
40
|
+
* Excludes any name bound as a `.map()`/`.filter()` loop callback's item or
|
|
41
|
+
* index parameter ANYWHERE in the component (#2212, ported here for #2236):
|
|
42
|
+
* this lookup is a flat, scope-blind `Set<string>` with no notion of a loop
|
|
43
|
+
* param shadowing an outer string-typed binding of the same name
|
|
44
|
+
* (`values.map((label) => 1 + label)` inside a component that also has a
|
|
45
|
+
* string `label` prop) — left unguarded, that shadowed `label` would be
|
|
46
|
+
* misdetected as string-typed and `1 + label` would silently lower to
|
|
47
|
+
* `bf_concat_str` instead of staying numeric `bf_add`. Subtracting loop-bound
|
|
48
|
+
* names is coarse (it also suppresses a genuinely non-shadowed same-named
|
|
49
|
+
* string elsewhere in the component) but safe: the suppressed case just
|
|
50
|
+
* falls back to today's numeric `bf_add` — the same, already-accepted
|
|
51
|
+
* residual as an unresolvable operand — never silently-wrong output.
|
|
33
52
|
*/
|
|
34
53
|
export function collectStringValueNames(ir: ComponentIR): Set<string> {
|
|
35
54
|
const names = new Set<string>()
|
|
@@ -41,5 +60,11 @@ export function collectStringValueNames(ir: ComponentIR): Set<string> {
|
|
|
41
60
|
for (const p of ir.metadata.propsParams) {
|
|
42
61
|
if (isStringTypeInfo(p.type)) names.add(p.name)
|
|
43
62
|
}
|
|
63
|
+
for (const c of ir.metadata.localConstants) {
|
|
64
|
+
if ((c.type !== null && isStringTypeInfo(c.type)) || isBareStringLiteral(c.value)) {
|
|
65
|
+
names.add(c.name)
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
for (const bound of collectLoopBoundNames(ir)) names.delete(bound)
|
|
44
69
|
return names
|
|
45
70
|
}
|
package/src/conformance-pins.ts
CHANGED
|
@@ -15,19 +15,21 @@ export const conformancePins: ConformancePins = {
|
|
|
15
15
|
// `style={{ … }}` object literal now lowers to a CSS string with dynamic
|
|
16
16
|
// values interpolated (`background-color:{{.Color}};padding:8px`) via
|
|
17
17
|
// `tryLowerStyleObject` (#1322).
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
|
|
25
|
-
//
|
|
26
|
-
//
|
|
27
|
-
//
|
|
28
|
-
//
|
|
29
|
-
|
|
30
|
-
|
|
18
|
+
// `todo-app` / `todo-app-ssr` no longer pinned (#2205) — the conformance
|
|
19
|
+
// harness now passes `siblingTemplatesRegistered: true` for fixtures with
|
|
20
|
+
// sibling `components`, matching `bf build`'s real semantics, so the
|
|
21
|
+
// BF103 loop-body cross-template check no longer fires spuriously.
|
|
22
|
+
// (`todo-app-ssr` is still skipped on this adapter via
|
|
23
|
+
// `render-divergences.ts` — #2209 — for an unrelated signal-seeding gap;
|
|
24
|
+
// `todo-app`'s pre-hydration empty render is unaffected.)
|
|
25
|
+
// `static-array-children` no longer pinned (#2208) — `items`'s
|
|
26
|
+
// array-literal initializer is now recognized as fully-static and its
|
|
27
|
+
// per-item ListItem props/data-key are baked directly into
|
|
28
|
+
// `NewStaticListProps`'s constructor (`analyzeBakeableStaticChildLoop`),
|
|
29
|
+
// since the loop body is a single child component with a plain-value
|
|
30
|
+
// prop set. See #2224 for the narrower remaining gap (a plain-ELEMENT
|
|
31
|
+
// loop body over a static array, or an inline/unnamed array literal —
|
|
32
|
+
// still refused).
|
|
31
33
|
// `([emoji, users]) => ...` is an array-index tuple destructure — #2087
|
|
32
34
|
// Phase B's widened gate now admits this shape (`destructure-array-index-in-map`
|
|
33
35
|
// exercises the same `segments`-based lowering). The remaining refusal here
|
|
@@ -40,13 +42,11 @@ export const conformancePins: ConformancePins = {
|
|
|
40
42
|
// array bound to such a const. See the `renderLoop` comment at the check
|
|
41
43
|
// site; Jinja / ERB apply the same narrow check for the same reason.
|
|
42
44
|
'static-array-from-props': [{ code: 'BF101', severity: 'error' }],
|
|
43
|
-
// Same computed-const array as above
|
|
44
|
-
//
|
|
45
|
-
//
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
{ code: 'BF101', severity: 'error' },
|
|
49
|
-
],
|
|
45
|
+
// Same computed-const array as above — the destructure param itself no
|
|
46
|
+
// longer contributes a diagnostic, and BF103 (sibling-imported child
|
|
47
|
+
// component in the loop body) no longer fires either now that the
|
|
48
|
+
// conformance harness passes `siblingTemplatesRegistered: true` (#2205).
|
|
49
|
+
'static-array-from-props-with-component': [{ code: 'BF101', severity: 'error' }],
|
|
50
50
|
// (`style-3-signals` graduated alongside `style-object-dynamic` — see note
|
|
51
51
|
// above; the `style={{ … }}` object now lowers to a CSS string.)
|
|
52
52
|
// (`tagged-template-classname` graduated by #2092 — the tag resolves
|
|
@@ -127,15 +127,14 @@ export const conformancePins: ConformancePins = {
|
|
|
127
127
|
// `string-trim` no longer pinned — pre-existing `bf_trim`
|
|
128
128
|
// (wraps `strings.TrimSpace`) handles the strip (#1448 Tier A
|
|
129
129
|
// ninth PR, closing out Tier A).
|
|
130
|
-
//
|
|
131
|
-
//
|
|
132
|
-
//
|
|
133
|
-
//
|
|
134
|
-
|
|
135
|
-
//
|
|
136
|
-
//
|
|
137
|
-
//
|
|
138
|
-
//
|
|
139
|
-
|
|
140
|
-
'dangerous-inner-html': [{ code: 'BF101', severity: 'error' }],
|
|
130
|
+
// `array-map-function-reference` no longer pinned — a bare-identifier
|
|
131
|
+
// `.map(format)` callback now resolves one hop to its declaration
|
|
132
|
+
// (`resolveCallbackMethodFunctionReferences`, #2206), the same mechanism
|
|
133
|
+
// #2090 established for `.sort(fnref)`.
|
|
134
|
+
// `dangerous-inner-html` no longer pinned — a compile-time string-literal
|
|
135
|
+
// `dangerouslySetInnerHTML={{ __html: '...' }}` is spliced directly into
|
|
136
|
+
// the template as trusted raw text (`resolveDangerousInnerHtml`, #2207).
|
|
137
|
+
// A dynamic/signal-derived value still refuses with BF101 — see the
|
|
138
|
+
// `dangerous-inner-html-dynamic` fixture/pin below (tracked: #2215).
|
|
139
|
+
'dangerous-inner-html-dynamic': [{ code: 'BF101', severity: 'error', issue: 'https://github.com/piconic-ai/barefootjs/issues/2215' }],
|
|
141
140
|
}
|
|
@@ -17,4 +17,16 @@
|
|
|
17
17
|
import type { RenderDivergences } from '@barefootjs/jsx'
|
|
18
18
|
|
|
19
19
|
export const renderDivergences: RenderDivergences = {
|
|
20
|
+
// `todo-app-ssr` no longer diverges (#2209). Two parts: (1) `.Todos`
|
|
21
|
+
// (the loop's DATUM slice) is already seeded straight from the caller's
|
|
22
|
+
// Input — the constructor derives it from `initialTodos`, and `[]Todo`
|
|
23
|
+
// zero-fills `Editing: false`, so the `.map(t => ({ ...t, editing:
|
|
24
|
+
// false }))` transform in the signal initializer was never actually the
|
|
25
|
+
// gap on Go, unlike the 7 template-string adapters. (2) The real gap was
|
|
26
|
+
// `.TodoItems []TodoItemProps` — the loop-body CHILD COMPONENT slice the
|
|
27
|
+
// template actually ranges over — which has no server-side population
|
|
28
|
+
// path in this harness (documented as route-handler-populated in
|
|
29
|
+
// production). `buildDynamicChildLoopSeeding` (this package's
|
|
30
|
+
// `test-render.ts`) now replicates that documented contract for a
|
|
31
|
+
// signal-backed dynamic child-component loop.
|
|
20
32
|
}
|
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, goFieldNameForKey } 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
|
*/
|