@barefootjs/go-template 0.29.0 → 0.30.2
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/go-template-adapter.d.ts +224 -0
- package/dist/adapter/go-template-adapter.d.ts.map +1 -1
- package/dist/adapter/index.js +294 -8
- package/dist/adapter/memo/memo-compute.d.ts +36 -0
- package/dist/adapter/memo/memo-compute.d.ts.map +1 -1
- package/dist/adapter/type/type-codegen.d.ts +21 -1
- package/dist/adapter/type/type-codegen.d.ts.map +1 -1
- package/dist/adapter/value/value-lowering.d.ts +1 -1
- package/dist/adapter/value/value-lowering.d.ts.map +1 -1
- package/dist/build.js +294 -8
- package/dist/conformance-pins.d.ts.map +1 -1
- package/dist/index.js +300 -10
- package/dist/render-divergences.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/__tests__/go-template-adapter.test.ts +626 -6
- package/src/adapter/go-template-adapter.ts +597 -13
- package/src/adapter/memo/memo-compute.ts +125 -0
- package/src/adapter/type/type-codegen.ts +44 -1
- package/src/adapter/value/value-lowering.ts +6 -1
- package/src/conformance-pins.ts +0 -5
- package/src/render-divergences.ts +24 -9
|
@@ -801,3 +801,128 @@ function propsAccessNameFromParsed(ctx: GoEmitContext, node: ParsedExpr): string
|
|
|
801
801
|
if (!ctx.state.propsObjectName || node.object.name !== ctx.state.propsObjectName) return null
|
|
802
802
|
return node.property
|
|
803
803
|
}
|
|
804
|
+
|
|
805
|
+
/**
|
|
806
|
+
* #2448: every INPUT prop name a constructor-evaluated `parsed` expression
|
|
807
|
+
* reads, either via a `<propsObjectName>.<name>` member access (object-props
|
|
808
|
+
* signature, e.g. `props.n`) or a bare identifier bound to a destructured
|
|
809
|
+
* prop (no `propsObjectName`).
|
|
810
|
+
*
|
|
811
|
+
* Two kinds of expression reach here, because `New<Comp>Props` bakes BOTH
|
|
812
|
+
* into the struct at construction time: a `createMemo` body and a
|
|
813
|
+
* `createSignal` INITIAL VALUE (`const [dbl] = createSignal(props.n)` emits
|
|
814
|
+
* `Dbl: in.N`, exactly as `createMemo(() => props.n * 2)` emits
|
|
815
|
+
* `Dbl: in.N * 2`). Either one goes stale under a per-row override, so both
|
|
816
|
+
* feed the dependency map.
|
|
817
|
+
*
|
|
818
|
+
* Feeds `GoTemplateAdapter.childDerivedFieldDeps` (built by
|
|
819
|
+
* `recordDerivedFieldDeps`, `go-template-adapter.ts`) so a parent overriding
|
|
820
|
+
* one of THESE props per row (`bf_with_props`, #2445) can be refused loudly
|
|
821
|
+
* instead of silently leaving the derived field stale — see that map's
|
|
822
|
+
* docstring.
|
|
823
|
+
*
|
|
824
|
+
* Structural counterpart of {@link freeVarsInBody}: that walk reports a
|
|
825
|
+
* member's OBJECT identifier only (`props`), treating the property name as
|
|
826
|
+
* non-referential (fine for its own free-var-substitution purpose). Here the
|
|
827
|
+
* PROPERTY name is exactly what's wanted, so a `member` node contributes it
|
|
828
|
+
* once its object resolves to the props binding — walking stops at that
|
|
829
|
+
* first `propsObjectName.<name>` hop, so a deeper chain (`props.a.b`) still
|
|
830
|
+
* contributes only its BASE prop `a` (mirrors `collectPropRefs` in
|
|
831
|
+
* `ssr-defaults.ts`, the same first-level-only rule, for the same reason: an
|
|
832
|
+
* adapter that later re-derives a nested field still needs the base field
|
|
833
|
+
* seeded).
|
|
834
|
+
*
|
|
835
|
+
* This is a best-effort STRUCTURAL walk over the child's OWN analysis-time
|
|
836
|
+
* `parsed` tree — not a re-derivation of the Go initializer text, which
|
|
837
|
+
* isn't available yet at registration time (the cross-file shape pre-pass,
|
|
838
|
+
* #2131, runs before any component's codegen).
|
|
839
|
+
*/
|
|
840
|
+
export function collectPropsReadByCtorInit(
|
|
841
|
+
body: ParsedExpr,
|
|
842
|
+
propsObjectName: string | null,
|
|
843
|
+
propNames: ReadonlySet<string>,
|
|
844
|
+
): Set<string> {
|
|
845
|
+
const found = new Set<string>()
|
|
846
|
+
const visit = (e: ParsedExpr, bound: ReadonlySet<string>): void => {
|
|
847
|
+
switch (e.kind) {
|
|
848
|
+
case 'identifier':
|
|
849
|
+
// Destructured signature only: a bare identifier bound to a
|
|
850
|
+
// destructured prop param IS the reference (`(props) => props.n`
|
|
851
|
+
// has no bare `n` — that shape is the `member` branch below).
|
|
852
|
+
if (!propsObjectName && propNames.has(e.name) && !bound.has(e.name)) found.add(e.name)
|
|
853
|
+
return
|
|
854
|
+
case 'member':
|
|
855
|
+
if (
|
|
856
|
+
propsObjectName &&
|
|
857
|
+
!e.computed &&
|
|
858
|
+
e.object.kind === 'identifier' &&
|
|
859
|
+
e.object.name === propsObjectName
|
|
860
|
+
) {
|
|
861
|
+
found.add(e.property)
|
|
862
|
+
return
|
|
863
|
+
}
|
|
864
|
+
visit(e.object, bound)
|
|
865
|
+
return
|
|
866
|
+
case 'index-access':
|
|
867
|
+
visit(e.object, bound)
|
|
868
|
+
visit(e.index, bound)
|
|
869
|
+
return
|
|
870
|
+
case 'binary':
|
|
871
|
+
case 'logical':
|
|
872
|
+
visit(e.left, bound)
|
|
873
|
+
visit(e.right, bound)
|
|
874
|
+
return
|
|
875
|
+
case 'unary':
|
|
876
|
+
visit(e.argument, bound)
|
|
877
|
+
return
|
|
878
|
+
case 'conditional':
|
|
879
|
+
visit(e.test, bound)
|
|
880
|
+
visit(e.consequent, bound)
|
|
881
|
+
visit(e.alternate, bound)
|
|
882
|
+
return
|
|
883
|
+
case 'call':
|
|
884
|
+
visit(e.callee, bound)
|
|
885
|
+
e.args.forEach(a => visit(a, bound))
|
|
886
|
+
return
|
|
887
|
+
case 'template-literal':
|
|
888
|
+
for (const p of e.parts) if (p.type === 'expression') visit(p.expr, bound)
|
|
889
|
+
return
|
|
890
|
+
case 'array-literal':
|
|
891
|
+
e.elements.forEach(el => visit(el, bound))
|
|
892
|
+
return
|
|
893
|
+
case 'object-literal':
|
|
894
|
+
for (const p of e.properties) visit(p.value, bound)
|
|
895
|
+
return
|
|
896
|
+
case 'array-method':
|
|
897
|
+
visit(e.object, bound)
|
|
898
|
+
e.args.forEach(a => visit(a, bound))
|
|
899
|
+
if (e.method === 'flat' && e.depthExpr) visit(e.depthExpr, bound)
|
|
900
|
+
return
|
|
901
|
+
case 'arrow': {
|
|
902
|
+
const inner = e.params.length === 0 ? bound : new Set([...bound, ...e.params])
|
|
903
|
+
visit(e.body, inner)
|
|
904
|
+
return
|
|
905
|
+
}
|
|
906
|
+
// Non-referential leaves — nothing to collect.
|
|
907
|
+
case 'literal':
|
|
908
|
+
case 'regex':
|
|
909
|
+
case 'unsupported':
|
|
910
|
+
return
|
|
911
|
+
default: {
|
|
912
|
+
// Exhaustiveness pin. A NEW `ParsedExpr` kind that lands without a
|
|
913
|
+
// case here would silently contribute no dependencies, and this
|
|
914
|
+
// walk's whole job is to answer "does this memo read that prop?" —
|
|
915
|
+
// a missed dependency is a MISSED REFUSAL, i.e. the silently-stale
|
|
916
|
+
// derived field #2448 exists to prevent. Fail the build instead:
|
|
917
|
+
// `never` makes the omission a compile error at the point the kind
|
|
918
|
+
// is added, the same drift defence `PARSED_EXPR_KINDS` gives the
|
|
919
|
+
// registry (`expression-parser.ts`).
|
|
920
|
+
const _exhaustive: never = e
|
|
921
|
+
void _exhaustive
|
|
922
|
+
return
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
visit(body, new Set())
|
|
927
|
+
return found
|
|
928
|
+
}
|
|
@@ -15,6 +15,48 @@ import type { TypeInfo } from '@barefootjs/jsx'
|
|
|
15
15
|
|
|
16
16
|
import type { GoEmitContext } from '../emit-context.ts'
|
|
17
17
|
|
|
18
|
+
/**
|
|
19
|
+
* Collapse a homogeneous LITERAL union (`'a' | 'b'`, `1 | 2`, `true | false`)
|
|
20
|
+
* to the primitive that backs it, so a variant-typed signal or prop
|
|
21
|
+
* (`createSignal<'a' | 'b'>('a')`, the `{ variant?: 'a' | 'b' }` prop shape)
|
|
22
|
+
* gets a real Go type instead of `interface{}` — and, downstream in
|
|
23
|
+
* `convertInitialValue`, a real seed instead of `nil`. #2477's Go leg: the
|
|
24
|
+
* analyzer maps an explicit literal-union type argument to
|
|
25
|
+
* `{kind:'union'}` of literal members, and with no
|
|
26
|
+
* `union` arm here OR in `convertInitialValue` the field fell to
|
|
27
|
+
* `interface{}` and the seed to `nil` — which the child's `string` field
|
|
28
|
+
* then rejected at `go run` time (`cannot use nil as string value`).
|
|
29
|
+
*
|
|
30
|
+
* Only a union whose EVERY member is a literal of ONE primitive family
|
|
31
|
+
* (string / number / boolean; a same-family primitive keyword member like
|
|
32
|
+
* `'a' | string` also counts) collapses. Anything else — mixed families,
|
|
33
|
+
* `null` / `undefined` members, object members — returns the input
|
|
34
|
+
* unchanged and keeps today's `interface{}` fallback, so the collapse can
|
|
35
|
+
* never widen what Go accepts, only type what it already receives.
|
|
36
|
+
*/
|
|
37
|
+
export function collapseLiteralUnion(typeInfo: TypeInfo): TypeInfo {
|
|
38
|
+
if (typeInfo.kind !== 'union' || !typeInfo.unionTypes || typeInfo.unionTypes.length === 0) {
|
|
39
|
+
return typeInfo
|
|
40
|
+
}
|
|
41
|
+
// Purely structural: `typeNodeToTypeInfo` lowers a literal member to
|
|
42
|
+
// `kind: 'primitive'` + `literalValue`, so the member's family is just
|
|
43
|
+
// its `primitive` — no re-parsing of `raw` (the first cut of this
|
|
44
|
+
// helper regexed the member text because the analyzer had no literal
|
|
45
|
+
// arm; that gap is closed at the source).
|
|
46
|
+
const familyOf = (m: TypeInfo): 'string' | 'number' | 'boolean' | null => {
|
|
47
|
+
if (m.kind !== 'primitive') return null
|
|
48
|
+
return m.primitive === 'string' || m.primitive === 'number' || m.primitive === 'boolean'
|
|
49
|
+
? m.primitive
|
|
50
|
+
: null
|
|
51
|
+
}
|
|
52
|
+
const first = familyOf(typeInfo.unionTypes[0])
|
|
53
|
+
if (!first) return typeInfo
|
|
54
|
+
for (const m of typeInfo.unionTypes) {
|
|
55
|
+
if (familyOf(m) !== first) return typeInfo
|
|
56
|
+
}
|
|
57
|
+
return { kind: 'primitive', raw: typeInfo.raw, primitive: first }
|
|
58
|
+
}
|
|
59
|
+
|
|
18
60
|
/**
|
|
19
61
|
* Convert a `TypeInfo` to a Go type string.
|
|
20
62
|
*
|
|
@@ -27,9 +69,10 @@ import type { GoEmitContext } from '../emit-context.ts'
|
|
|
27
69
|
*/
|
|
28
70
|
export function typeInfoToGo(
|
|
29
71
|
ctx: GoEmitContext,
|
|
30
|
-
|
|
72
|
+
_typeInfo: TypeInfo,
|
|
31
73
|
defaultValue?: string,
|
|
32
74
|
): string {
|
|
75
|
+
const typeInfo = collapseLiteralUnion(_typeInfo)
|
|
33
76
|
switch (typeInfo.kind) {
|
|
34
77
|
case 'primitive':
|
|
35
78
|
switch (typeInfo.primitive) {
|
|
@@ -11,6 +11,7 @@ import type { GoEmitContext } from '../emit-context.ts'
|
|
|
11
11
|
import type { PropFallbackVar } from '../lib/types.ts'
|
|
12
12
|
import { capitalizeFieldName } from '../lib/go-naming.ts'
|
|
13
13
|
import { parsedLiteralToGo } from './parsed-literal-to-go.ts'
|
|
14
|
+
import { collapseLiteralUnion } from '../type/type-codegen.ts'
|
|
14
15
|
|
|
15
16
|
/** Default for `getSignalInitialValueAsGo`'s optional fallback-var map. */
|
|
16
17
|
const EMPTY_PROP_FALLBACK_VARS: ReadonlyMap<string, PropFallbackVar> = new Map()
|
|
@@ -61,10 +62,14 @@ function nillableAwarePropRef(ctx: GoEmitContext, propName: string, expectedType
|
|
|
61
62
|
export function convertInitialValue(
|
|
62
63
|
ctx: GoEmitContext,
|
|
63
64
|
value: string,
|
|
64
|
-
|
|
65
|
+
_typeInfo: TypeInfo,
|
|
65
66
|
propsParams?: { name: string }[],
|
|
66
67
|
preParsed?: ParsedExpr,
|
|
67
68
|
): string {
|
|
69
|
+
// Literal unions collapse to their backing primitive the same way
|
|
70
|
+
// `typeInfoToGo` collapses the field's type — the two MUST agree, or a
|
|
71
|
+
// `string` field gets a `nil` seed (#2477's `go run` failure).
|
|
72
|
+
const typeInfo = collapseLiteralUnion(_typeInfo)
|
|
68
73
|
const propRef = (propName: string): string => nillableAwarePropRef(ctx, propName, typeInfo)
|
|
69
74
|
|
|
70
75
|
if (/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(value)) {
|
package/src/conformance-pins.ts
CHANGED
|
@@ -15,11 +15,6 @@ export const conformancePins: ConformancePins = {
|
|
|
15
15
|
// JS-runtime target runs it, a DSL adapter surfaces BF021 + `/* @client */`.
|
|
16
16
|
// See spec/callback-fidelity.md.
|
|
17
17
|
'filter-typeof-predicate': [{ code: 'BF021', severity: 'error' }],
|
|
18
|
-
// A `.map()` body with a `const`/`let` preamble before its branches:
|
|
19
|
-
// a JS runtime folds it, a DSL adapter can't carry the loop-local into a
|
|
20
|
-
// conditional branch template, so it refuses with BF021 + `/* @client */`.
|
|
21
|
-
// See spec/callback-fidelity.md.
|
|
22
|
-
'map-preamble-branch-body': [{ code: 'BF021', severity: 'error' }],
|
|
23
18
|
'map-array-builder-body': [{ code: 'BF021', severity: 'error' }],
|
|
24
19
|
'map-array-builder-escaping': [{ code: 'BF021', severity: 'error' }],
|
|
25
20
|
// `.fill(value)` mutates the receiver in place — no template lowering
|
|
@@ -30,13 +30,28 @@ export const renderDivergences: RenderDivergences = {
|
|
|
30
30
|
// `test-render.ts`) now replicates that documented contract for a
|
|
31
31
|
// signal-backed dynamic child-component loop.
|
|
32
32
|
|
|
33
|
-
//
|
|
34
|
-
//
|
|
35
|
-
// the
|
|
36
|
-
//
|
|
37
|
-
//
|
|
38
|
-
//
|
|
39
|
-
//
|
|
40
|
-
|
|
41
|
-
|
|
33
|
+
// Onboarding TSX-fidelity fixtures (PR #2461): `expectedHtml` is
|
|
34
|
+
// hand-authored to the CORRECT output because the emission bug lives in
|
|
35
|
+
// the shared compiler layer — every adapter, including the Hono
|
|
36
|
+
// reference, currently emits the broken form (verified against this
|
|
37
|
+
// adapter's emitted template; see each fixture's docstring). Graduate
|
|
38
|
+
// by fixing the shared emission, regenerating `expectedHtml` from the
|
|
39
|
+
// fixed reference, and deleting these lines (and the matching hono
|
|
40
|
+
// `skipJsx` entries).
|
|
41
|
+
'aliased-destructured-prop':
|
|
42
|
+
'aliased destructured prop `{ n: count }` loses its rename — the Input struct field is Count `json:"count"`, so the caller-side struct literal keyed by the real prop name fails `go run` outright (unknown field N, exit 1) (https://github.com/piconic-ai/barefootjs/issues/2460)',
|
|
43
|
+
|
|
44
|
+
// #2482 audit follow-ups: loop-scope holes specific to this adapter's
|
|
45
|
+
// four-stack scope tracking and its SSR seeding. Graduate by applying
|
|
46
|
+
// the fix described in each issue and deleting the line.
|
|
47
|
+
'loop-destructured-param-condition':
|
|
48
|
+
'a destructured .map() param binding used as a row ternary CONDITION emits the root-scope `{{if $.Active}}` — `renderConditionExpr` omits `loopBindingStack`, unlike `identifierToGoRef` (text positions resolve the same binding correctly) (https://github.com/piconic-ai/barefootjs/issues/2486)',
|
|
49
|
+
'nested-loop-tail-content':
|
|
50
|
+
'outer-row content AFTER a nested inner loop renders through non-loop arms (spread lowers to the component-root `.Spread_0`) — `inLoop` is cleared, not restored, by the inner loop\'s exit; the same content BEFORE the inner loop emits correctly (https://github.com/piconic-ai/barefootjs/issues/2487)',
|
|
51
|
+
'loop-param-shadows-spread-const':
|
|
52
|
+
'spreading a loop row object mangles attribute names (`id` → `-i-d`, `title` → `-title`); the spread VALUE is correctly row-scoped, distinguishing this from the template-adapter const-shadow hole #2489 (https://github.com/piconic-ai/barefootjs/issues/2490)',
|
|
53
|
+
'loop-param-shadows-record-template-span':
|
|
54
|
+
'a dynamic-key element access on a loop row (`tone[k]`) renders empty at execute time — the emitted template contains no baked const (correct post-fix), but the row lookup resolves to nothing (https://github.com/piconic-ai/barefootjs/issues/2491)',
|
|
55
|
+
'callback-param-shadows-prop':
|
|
56
|
+
'JS-computed signal/memo initializers (`[…].map(…).join(…)`, memo over signal + prop) don\'t seed Go SSR — renders `[]` / empty where every other adapter renders the computed value; hydration snaps to correct (https://github.com/piconic-ai/barefootjs/issues/2492)',
|
|
42
57
|
}
|