@barefootjs/go-template 0.31.10 → 0.32.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.
- package/dist/adapter/emit-context.d.ts +16 -0
- package/dist/adapter/emit-context.d.ts.map +1 -1
- package/dist/adapter/go-template-adapter.d.ts +74 -0
- package/dist/adapter/go-template-adapter.d.ts.map +1 -1
- package/dist/adapter/index.js +100 -4
- package/dist/adapter/lib/types.d.ts +13 -0
- package/dist/adapter/lib/types.d.ts.map +1 -1
- package/dist/adapter/props/prop-types.d.ts.map +1 -1
- package/dist/index.js +101 -8
- package/dist/render-divergences.d.ts.map +1 -1
- package/dist/vite.js +100 -4
- package/package.json +5 -5
- package/src/__tests__/go-template-adapter.test.ts +246 -6
- package/src/adapter/emit-context.ts +14 -0
- package/src/adapter/go-template-adapter.ts +254 -2
- package/src/adapter/lib/types.ts +10 -0
- package/src/adapter/props/prop-types.ts +26 -1
- package/src/render-divergences.ts +1 -25
package/dist/vite.js
CHANGED
|
@@ -7777,7 +7777,18 @@ function collectNullishConsumedPropNames(ctx, ir) {
|
|
|
7777
7777
|
};
|
|
7778
7778
|
walk(ir.root);
|
|
7779
7779
|
for (const signal of ir.metadata.signals) {
|
|
7780
|
-
const
|
|
7780
|
+
const resolvedParsed = resolveSignalParsedThroughSeedPlan(ctx.state, signal);
|
|
7781
|
+
let match = ctx.extractPropFallback(signal.initialValue, resolvedParsed);
|
|
7782
|
+
if (!match) {
|
|
7783
|
+
const collision = ctx.extractCollisionDerivation(resolvedParsed);
|
|
7784
|
+
if (collision) {
|
|
7785
|
+
const collisionParam = ir.metadata.propsParams.find((p) => p.name === collision.propName);
|
|
7786
|
+
const collisionField = collisionParam ? capitalizeFieldName(collisionParam.sourceName ?? collision.propName) : null;
|
|
7787
|
+
if (collisionField && capitalizeFieldName(signal.getter) === collisionField) {
|
|
7788
|
+
match = { propName: collision.propName, goFallback: collision.goFallback };
|
|
7789
|
+
}
|
|
7790
|
+
}
|
|
7791
|
+
}
|
|
7781
7792
|
if (!match || !optionalParams.has(match.propName))
|
|
7782
7793
|
continue;
|
|
7783
7794
|
const f = match.goFallback;
|
|
@@ -7962,6 +7973,7 @@ class GoTemplateAdapter extends BaseAdapter {
|
|
|
7962
7973
|
convertConditionToGo: (jsCondition, preParsed) => this.convertConditionToGo(jsCondition, preParsed),
|
|
7963
7974
|
extractPropNameFromInitialValue: (initialValue, preParsed) => this.extractPropNameFromInitialValue(initialValue, preParsed),
|
|
7964
7975
|
extractPropFallback: (initialValue, preParsed) => this.extractPropFallback(initialValue, preParsed),
|
|
7976
|
+
extractCollisionDerivation: (parsed) => this.extractCollisionDerivation(parsed),
|
|
7965
7977
|
resolveModuleStringConst: (name) => this.resolveModuleStringConst(name)
|
|
7966
7978
|
};
|
|
7967
7979
|
get errors() {
|
|
@@ -8740,6 +8752,7 @@ ${goFields.join(`
|
|
|
8740
8752
|
}
|
|
8741
8753
|
lines.push("");
|
|
8742
8754
|
}
|
|
8755
|
+
this.emitCallerPropsInit(lines, ir, nestedComponents, staticWithoutBody, staticWithBody, dynamicWithBody, emittedWrapperVars, propTypeOverrides);
|
|
8743
8756
|
lines.push(` return ${propsTypeName}{`);
|
|
8744
8757
|
lines.push("\t\tScopeID: scopeID,");
|
|
8745
8758
|
lines.push("\t\tBfParent: in.BfParent,");
|
|
@@ -8747,6 +8760,7 @@ ${goFields.join(`
|
|
|
8747
8760
|
if (this.usesSearchParams(ir)) {
|
|
8748
8761
|
lines.push("\t\tSearchParams: in.SearchParams,");
|
|
8749
8762
|
}
|
|
8763
|
+
lines.push("\t\tBfCallerProps: bfCallerProps,");
|
|
8750
8764
|
const nestedArrayFields = this.propDerivedNestedArrayFields(nestedComponents);
|
|
8751
8765
|
const memoFallbacks = new Map;
|
|
8752
8766
|
for (const memo of ir.metadata.memos) {
|
|
@@ -8772,7 +8786,8 @@ ${goFields.join(`
|
|
|
8772
8786
|
continue;
|
|
8773
8787
|
const hoisted = propFallbackVars.get(param.name);
|
|
8774
8788
|
if (hoisted) {
|
|
8775
|
-
|
|
8789
|
+
const value = hoisted.collisionWrap ? `${hoisted.varName} ${hoisted.collisionWrap.operator} ${hoisted.collisionWrap.operand}` : hoisted.varName;
|
|
8790
|
+
lines.push(` ${fieldName}: ${value},`);
|
|
8776
8791
|
} else {
|
|
8777
8792
|
const paramDefault = goPropDefault(param.defaultValue);
|
|
8778
8793
|
const memoFold = memoFallbacks.get(fieldName);
|
|
@@ -8851,6 +8866,53 @@ ${goFields.join(`
|
|
|
8851
8866
|
lines.push("\t}");
|
|
8852
8867
|
lines.push("}");
|
|
8853
8868
|
}
|
|
8869
|
+
emitCallerPropsInit(lines, ir, nestedComponents, staticWithoutBody, staticWithBody, dynamicWithBody, emittedWrapperVars, propTypeOverrides) {
|
|
8870
|
+
lines.push("\tbfCallerProps := map[string]interface{}{}");
|
|
8871
|
+
const bfCallerPropsTakenTags = new Set;
|
|
8872
|
+
const nestedArrayFields = this.propDerivedNestedArrayFields(nestedComponents);
|
|
8873
|
+
for (const param of ir.metadata.propsParams) {
|
|
8874
|
+
if (param.name === "children")
|
|
8875
|
+
continue;
|
|
8876
|
+
if (this.isNestedArrayShadowed(param, nestedArrayFields))
|
|
8877
|
+
continue;
|
|
8878
|
+
const callerKey = this.claimJsonTag(this.toJsonTag(param.sourceName ?? param.name), bfCallerPropsTakenTags);
|
|
8879
|
+
if (callerKey === "-")
|
|
8880
|
+
continue;
|
|
8881
|
+
const inputField = `in.${capitalizeFieldName(param.sourceName ?? param.name)}`;
|
|
8882
|
+
const goType = resolvePropGoType(this.emitCtx, param, propTypeOverrides);
|
|
8883
|
+
const isNillable = goType === "interface{}" || goType === "map[string]interface{}" || goType.startsWith("[]");
|
|
8884
|
+
if (param.optional && isNillable) {
|
|
8885
|
+
lines.push(` if ${inputField} != nil {`);
|
|
8886
|
+
lines.push(` bfCallerProps["${callerKey}"] = ${inputField}`);
|
|
8887
|
+
lines.push(` }`);
|
|
8888
|
+
} else {
|
|
8889
|
+
lines.push(` bfCallerProps["${callerKey}"] = ${inputField}`);
|
|
8890
|
+
}
|
|
8891
|
+
}
|
|
8892
|
+
for (const nested of [...staticWithoutBody, ...staticWithBody, ...dynamicWithBody]) {
|
|
8893
|
+
if (!nested.isPropDerived)
|
|
8894
|
+
continue;
|
|
8895
|
+
const varName = `${nested.name.charAt(0).toLowerCase()}${nested.name.slice(1)}s`;
|
|
8896
|
+
const isBuilt = staticWithoutBody.includes(nested) || emittedWrapperVars.has(varName);
|
|
8897
|
+
if (!isBuilt)
|
|
8898
|
+
continue;
|
|
8899
|
+
const arrayFieldName = capitalizeFieldName(`${nested.name}s`);
|
|
8900
|
+
const param = ir.metadata.propsParams.find((p) => capitalizeFieldName(p.name) === arrayFieldName || capitalizeFieldName(p.sourceName ?? p.name) === arrayFieldName);
|
|
8901
|
+
if (!param || !this.isNestedArrayShadowed(param, nestedArrayFields))
|
|
8902
|
+
continue;
|
|
8903
|
+
const callerKey = this.claimJsonTag(this.toJsonTag(param.sourceName ?? param.name), bfCallerPropsTakenTags);
|
|
8904
|
+
if (callerKey === "-")
|
|
8905
|
+
continue;
|
|
8906
|
+
if (param.optional) {
|
|
8907
|
+
lines.push(` if in.${nested.name}s != nil {`);
|
|
8908
|
+
lines.push(` bfCallerProps["${callerKey}"] = ${varName}`);
|
|
8909
|
+
lines.push(` }`);
|
|
8910
|
+
} else {
|
|
8911
|
+
lines.push(` bfCallerProps["${callerKey}"] = ${varName}`);
|
|
8912
|
+
}
|
|
8913
|
+
}
|
|
8914
|
+
lines.push("");
|
|
8915
|
+
}
|
|
8854
8916
|
emitStaticChildInstances(lines, ir) {
|
|
8855
8917
|
const staticChildren = this.collectStaticChildInstances(ir.root, ir.metadata.propsParams);
|
|
8856
8918
|
for (const child of staticChildren) {
|
|
@@ -9302,6 +9364,7 @@ ${goFields.join(`
|
|
|
9302
9364
|
if (this.usesSearchParams(ir)) {
|
|
9303
9365
|
lines.push('\tSearchParams bf.SearchParams `json:"-"`');
|
|
9304
9366
|
}
|
|
9367
|
+
lines.push('\tBfCallerProps map[string]interface{} `json:"-"`');
|
|
9305
9368
|
}
|
|
9306
9369
|
emitPropsDataFields(lines, ir, nestedComponents, propTypeOverrides, takenJsonTags) {
|
|
9307
9370
|
const nestedArrayFields = this.propDerivedNestedArrayFields(nestedComponents);
|
|
@@ -9713,7 +9776,19 @@ ${goFields.join(`
|
|
|
9713
9776
|
}
|
|
9714
9777
|
const propTypeOverrides = buildPropTypeOverrides(this.emitCtx, ir);
|
|
9715
9778
|
for (const signal of ir.metadata.signals) {
|
|
9716
|
-
|
|
9779
|
+
let match = this.extractPropFallback(signal.initialValue, this.resolvedSignalParsed(signal));
|
|
9780
|
+
let collisionWrap;
|
|
9781
|
+
if (!match) {
|
|
9782
|
+
const collision = this.extractCollisionDerivation(this.resolvedSignalParsed(signal));
|
|
9783
|
+
if (collision) {
|
|
9784
|
+
const collisionParam = ir.metadata.propsParams.find((p) => p.name === collision.propName);
|
|
9785
|
+
const collisionField = collisionParam ? capitalizeFieldName(collisionParam.sourceName ?? collision.propName) : null;
|
|
9786
|
+
if (collisionField && capitalizeFieldName(signal.getter) === collisionField) {
|
|
9787
|
+
match = { propName: collision.propName, goFallback: collision.goFallback };
|
|
9788
|
+
collisionWrap = { operator: collision.operator, operand: collision.operand };
|
|
9789
|
+
}
|
|
9790
|
+
}
|
|
9791
|
+
}
|
|
9717
9792
|
if (!match)
|
|
9718
9793
|
continue;
|
|
9719
9794
|
if (result.has(match.propName))
|
|
@@ -9752,7 +9827,8 @@ ${goFields.join(`
|
|
|
9752
9827
|
fieldName,
|
|
9753
9828
|
goFallback: match.goFallback,
|
|
9754
9829
|
zeroLiteral,
|
|
9755
|
-
...nullishLowered ? { assertType: concreteType } : {}
|
|
9830
|
+
...nullishLowered ? { assertType: concreteType } : {},
|
|
9831
|
+
...collisionWrap ? { collisionWrap } : {}
|
|
9756
9832
|
});
|
|
9757
9833
|
}
|
|
9758
9834
|
return result;
|
|
@@ -9802,6 +9878,26 @@ ${goFields.join(`
|
|
|
9802
9878
|
return null;
|
|
9803
9879
|
return { propName, goFallback };
|
|
9804
9880
|
}
|
|
9881
|
+
extractCollisionDerivation(parsed) {
|
|
9882
|
+
if (!parsed || parsed.kind !== "binary")
|
|
9883
|
+
return null;
|
|
9884
|
+
if (!["*", "+", "-", "/"].includes(parsed.op))
|
|
9885
|
+
return null;
|
|
9886
|
+
const { right } = parsed;
|
|
9887
|
+
if (right.kind !== "literal" || right.literalType !== "number" || typeof right.value !== "number" || !Number.isInteger(right.value) || right.value < 0) {
|
|
9888
|
+
return null;
|
|
9889
|
+
}
|
|
9890
|
+
if (parsed.op === "/" && right.value === 0)
|
|
9891
|
+
return null;
|
|
9892
|
+
const coalesce = parsed.left;
|
|
9893
|
+
if (coalesce.kind !== "logical" || coalesce.op !== "??" || coalesce.right.kind !== "literal" || coalesce.right.literalType !== "number") {
|
|
9894
|
+
return null;
|
|
9895
|
+
}
|
|
9896
|
+
const inner = this.extractPropFallbackFromParsed(parsed.left);
|
|
9897
|
+
if (!inner)
|
|
9898
|
+
return null;
|
|
9899
|
+
return { ...inner, operator: parsed.op, operand: String(right.value) };
|
|
9900
|
+
}
|
|
9805
9901
|
extractPropNameFromInitialValue(initialValue, preParsed) {
|
|
9806
9902
|
if (!this.state.propsObjectName) {
|
|
9807
9903
|
if (preParsed?.kind === "logical" && (preParsed.op === "??" || preParsed.op === "||") && preParsed.left.kind === "identifier") {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@barefootjs/go-template",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.32.0",
|
|
4
4
|
"description": "Go html/template adapter for BarefootJS - generates Go template files from IR",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
"directory": "packages/adapter-go-template"
|
|
50
50
|
},
|
|
51
51
|
"dependencies": {
|
|
52
|
-
"@barefootjs/shared": "0.
|
|
52
|
+
"@barefootjs/shared": "0.32.0"
|
|
53
53
|
},
|
|
54
54
|
"peerDependencies": {
|
|
55
55
|
"@barefootjs/jsx": ">=0.2.0",
|
|
@@ -67,9 +67,9 @@
|
|
|
67
67
|
},
|
|
68
68
|
"devDependencies": {
|
|
69
69
|
"@barefootjs/adapter-tests": "0.1.0",
|
|
70
|
-
"@barefootjs/client": "0.
|
|
71
|
-
"@barefootjs/jsx": "0.
|
|
72
|
-
"@barefootjs/vite": "0.
|
|
70
|
+
"@barefootjs/client": "0.32.0",
|
|
71
|
+
"@barefootjs/jsx": "0.32.0",
|
|
72
|
+
"@barefootjs/vite": "0.32.0",
|
|
73
73
|
"vite": "^6.0.0"
|
|
74
74
|
}
|
|
75
75
|
}
|
|
@@ -1121,7 +1121,11 @@ export function Rows() {
|
|
|
1121
1121
|
expect(types).toContain('DataX string `json:"data-x"`')
|
|
1122
1122
|
expect(types).not.toContain('"Data-x"')
|
|
1123
1123
|
expect(types).toContain('Row{ID: "r1", Meta: RowMeta{DataX: "v"}}')
|
|
1124
|
-
|
|
1124
|
+
// `Meta` resolved to the real synthesized struct, not the
|
|
1125
|
+
// `map[string]interface{}` fallback — scoped to the field itself
|
|
1126
|
+
// since `BfCallerProps map[string]interface{}` (#2684) legitimately
|
|
1127
|
+
// appears elsewhere in every generated Props struct now.
|
|
1128
|
+
expect(types).not.toMatch(/Meta map\[string\]interface\{\}/)
|
|
1125
1129
|
})
|
|
1126
1130
|
|
|
1127
1131
|
test('snake_case keys keep their underscore in the generated field name (#2089 review)', () => {
|
|
@@ -5011,7 +5015,10 @@ export function Foo({ q: searchParams }: { q: string }) {
|
|
|
5011
5015
|
expect((result.errors ?? []).filter(e => e.code === 'BF101')).toHaveLength(0)
|
|
5012
5016
|
const types = result.files.find(f => f.type === 'types')!.content
|
|
5013
5017
|
const structStart = types.indexOf('type FooProps struct')
|
|
5014
|
-
|
|
5018
|
+
// The struct's actual closing brace is the first `}` that starts its
|
|
5019
|
+
// OWN line (fields are tab-indented) — a bare `indexOf('}', ...)` would
|
|
5020
|
+
// stop early at the embedded `}` inside `BfCallerProps map[string]interface{}` (#2684).
|
|
5021
|
+
const fooPropsBody = types.slice(structStart, types.indexOf('\n}', structStart))
|
|
5015
5022
|
// Exactly one "SearchParams" field in the struct body — the prop's
|
|
5016
5023
|
// own — never a second `SearchParams bf.SearchParams` reader field
|
|
5017
5024
|
// colliding with it (a Go redeclaration error).
|
|
@@ -5021,6 +5028,188 @@ export function Foo({ q: searchParams }: { q: string }) {
|
|
|
5021
5028
|
})
|
|
5022
5029
|
})
|
|
5023
5030
|
|
|
5031
|
+
// #2684: `bf-p` must carry only what the caller actually passed — required
|
|
5032
|
+
// props always, optional ones only when supplied — never the author's
|
|
5033
|
+
// baked default (`x ?? 7`), never `null` for an omitted optional, never a
|
|
5034
|
+
// Go zero value standing in for "unset". `NewXxxProps` now ALSO populates a
|
|
5035
|
+
// `BfCallerProps map[string]interface{}` sidecar (marshaled by `BfPropsAttr`
|
|
5036
|
+
// INSTEAD OF the whole struct) with exactly the caller-supplied, raw
|
|
5037
|
+
// (undefaulted) values — see the field's doc comment in
|
|
5038
|
+
// `emitPropsStructHeader` for the two-consumers rationale.
|
|
5039
|
+
describe('GoTemplateAdapter - BfCallerProps hydration sidecar (#2684)', () => {
|
|
5040
|
+
test('NewXxxProps populates BfCallerProps for the three classes: required, nullish-consumed optional, concrete-typed optional residual', () => {
|
|
5041
|
+
const result = compileJSX(`
|
|
5042
|
+
'use client'
|
|
5043
|
+
import { createSignal } from '@barefootjs/client'
|
|
5044
|
+
export function C(props: { x?: number; label?: string; name: string }) {
|
|
5045
|
+
const [x] = createSignal(props.x ?? 7)
|
|
5046
|
+
return <div data-name={props.name}>{x()}</div>
|
|
5047
|
+
}
|
|
5048
|
+
`.trimStart(), 'test.tsx', { adapter: new GoTemplateAdapter(), outputIR: false })
|
|
5049
|
+
expect((result.errors ?? []).filter(e => e.code === 'BF101')).toHaveLength(0)
|
|
5050
|
+
const types = result.files.find(f => f.type === 'types')!.content
|
|
5051
|
+
// The sidecar field itself, on the Props struct.
|
|
5052
|
+
expect(types).toContain('BfCallerProps map[string]interface{} `json:"-"`')
|
|
5053
|
+
|
|
5054
|
+
const newProps = types.slice(types.indexOf('func NewCProps'))
|
|
5055
|
+
expect(newProps).toContain('bfCallerProps := map[string]interface{}{}')
|
|
5056
|
+
// Class 1 — required prop: always included, raw `in.Name` (never a
|
|
5057
|
+
// baked/defaulted value — there is none for a required prop anyway).
|
|
5058
|
+
expect(newProps).toContain('bfCallerProps["name"] = in.Name')
|
|
5059
|
+
// Class 2 — optional, nullish-consumed (`??`) prop: flips to `interface{}`
|
|
5060
|
+
// (#2248) and is included ONLY when the caller actually passed something.
|
|
5061
|
+
// Critically, the RAW `in.X` goes in the map, never the baked default
|
|
5062
|
+
// `7` the hoisted fallback var applies to the template-facing field.
|
|
5063
|
+
expect(newProps).toContain('if in.X != nil {')
|
|
5064
|
+
expect(newProps).toContain('bfCallerProps["x"] = in.X')
|
|
5065
|
+
expect(newProps).not.toMatch(/bfCallerProps\["x"\]\s*=\s*7/)
|
|
5066
|
+
// Class 3 — optional prop that resolves to a CONCRETE type (`label` is
|
|
5067
|
+
// never consumed nullish/attr/text/presence-wise, so it stays a plain
|
|
5068
|
+
// `string`): presence is unknowable from Input alone, so it's included
|
|
5069
|
+
// unconditionally — a documented residual, not silently dropped.
|
|
5070
|
+
expect(newProps).toContain('bfCallerProps["label"] = in.Label')
|
|
5071
|
+
|
|
5072
|
+
// The struct literal wires the local into the field.
|
|
5073
|
+
expect(newProps).toContain('BfCallerProps: bfCallerProps,')
|
|
5074
|
+
})
|
|
5075
|
+
|
|
5076
|
+
test('a required nested-array-shadowed prop (#2672/#2525) is carried via its reshaped array local, not silently dropped', () => {
|
|
5077
|
+
const result = compileJSX(`
|
|
5078
|
+
type ToggleItemProps = { label: string; defaultOn?: boolean }
|
|
5079
|
+
function ToggleItem(props: ToggleItemProps) {
|
|
5080
|
+
return <div>{props.label}</div>
|
|
5081
|
+
}
|
|
5082
|
+
type ToggleProps = { toggleItems: ToggleItemProps[] }
|
|
5083
|
+
export function Toggle({ toggleItems }: ToggleProps) {
|
|
5084
|
+
return <div>{toggleItems.map((item) => <ToggleItem key={item.label} label={item.label} defaultOn={item.defaultOn} />)}</div>
|
|
5085
|
+
}
|
|
5086
|
+
`.trimStart(), 'test.tsx', { adapter: new GoTemplateAdapter(), outputIR: false })
|
|
5087
|
+
expect((result.errors ?? []).filter(e => e.code === 'BF101')).toHaveLength(0)
|
|
5088
|
+
const types = result.files.find(f => f.type === 'types')!.content
|
|
5089
|
+
const newProps = types.slice(types.indexOf('func NewToggleProps'))
|
|
5090
|
+
// The shadowed prop has no `in.ToggleItems` scalar field to read — its
|
|
5091
|
+
// data is carried via the already-built reshaped array local instead.
|
|
5092
|
+
// `toggleItems` is REQUIRED here, so it's unconditional — same
|
|
5093
|
+
// required-stays-unconditional rule as the main loop.
|
|
5094
|
+
expect(newProps).toContain('bfCallerProps["toggleItems"] = toggleItems')
|
|
5095
|
+
expect(newProps).not.toContain('if in.ToggleItems != nil {')
|
|
5096
|
+
})
|
|
5097
|
+
})
|
|
5098
|
+
|
|
5099
|
+
describe('GoTemplateAdapter - collision-derivation lowering (#2683)', () => {
|
|
5100
|
+
test('a signal colliding with its own prop composes the presence-check fold with the surrounding arithmetic', () => {
|
|
5101
|
+
const result = compileJSX(`
|
|
5102
|
+
'use client'
|
|
5103
|
+
import { createSignal } from '@barefootjs/client'
|
|
5104
|
+
export function C(props: { count?: number }) {
|
|
5105
|
+
const [count, setCount] = createSignal((props.count ?? 1) * 2)
|
|
5106
|
+
return <span>{count()}</span>
|
|
5107
|
+
}
|
|
5108
|
+
`.trimStart(), 'test.tsx', { adapter: new GoTemplateAdapter(), outputIR: false })
|
|
5109
|
+
expect((result.errors ?? []).filter(e => e.code === 'BF101')).toHaveLength(0)
|
|
5110
|
+
const types = result.files.find(f => f.type === 'types')!.content
|
|
5111
|
+
// The collision flips the field to the nillable representation — the
|
|
5112
|
+
// SAME flip an ordinary `??`-consumed optional prop gets (#2248) — so an
|
|
5113
|
+
// absent prop (nil) and an explicit `0` stay distinguishable.
|
|
5114
|
+
expect(types).toContain('Count interface{}')
|
|
5115
|
+
const newProps = types.slice(types.indexOf('func NewCProps'))
|
|
5116
|
+
// The presence-check fold (`extractPropFallbackFromParsed`'s shape,
|
|
5117
|
+
// reused unchanged) hoists the RAW coalesced value into a local...
|
|
5118
|
+
expect(newProps).toContain('var count int = 1')
|
|
5119
|
+
expect(newProps).toContain('if in.Count != nil {')
|
|
5120
|
+
expect(newProps).toContain('count = bf.ToInt(in.Count)')
|
|
5121
|
+
// ...and the shared field carries the FULLY DERIVED value — the local
|
|
5122
|
+
// with the surrounding `* 2` composed back on, not just the coalesce
|
|
5123
|
+
// result.
|
|
5124
|
+
expect(newProps).toContain('Count: count * 2,')
|
|
5125
|
+
// `BfCallerProps` (#2684) keeps carrying the RAW caller value, never the
|
|
5126
|
+
// derived one.
|
|
5127
|
+
expect(newProps).toContain('if in.Count != nil {')
|
|
5128
|
+
expect(newProps).toContain('bfCallerProps["count"] = in.Count')
|
|
5129
|
+
expect(newProps).not.toMatch(/bfCallerProps\["count"\]\s*=\s*count/)
|
|
5130
|
+
})
|
|
5131
|
+
|
|
5132
|
+
test('a NON-NUMERIC `??` fallback declines collision-derivation — raw passthrough, never invalid Go (Copilot review, #2694)', () => {
|
|
5133
|
+
// `(props.label ?? 'x') + 2` would otherwise hoist `var label string =
|
|
5134
|
+
// "x"` and emit `Label: label + 2,` — invalid Go (string + int), a
|
|
5135
|
+
// compile BREAK where the pre-fix behavior at least built. JS semantics
|
|
5136
|
+
// are string concatenation here besides, so a numeric compose could
|
|
5137
|
+
// never be faithful; the shape stays on the raw-passthrough path.
|
|
5138
|
+
const result = compileJSX(`
|
|
5139
|
+
'use client'
|
|
5140
|
+
import { createSignal } from '@barefootjs/client'
|
|
5141
|
+
export function C(props: { label?: string }) {
|
|
5142
|
+
const [label, setLabel] = createSignal((props.label ?? 'x') + 2)
|
|
5143
|
+
return <span>{label()}</span>
|
|
5144
|
+
}
|
|
5145
|
+
`.trimStart(), 'test.tsx', { adapter: new GoTemplateAdapter(), outputIR: false })
|
|
5146
|
+
const types = result.files.find(f => f.type === 'types')!.content
|
|
5147
|
+
const newProps = types.slice(types.indexOf('func NewCProps'))
|
|
5148
|
+
expect(newProps).not.toContain('label + 2')
|
|
5149
|
+
expect(newProps).not.toContain('var label string = "x"')
|
|
5150
|
+
// The field keeps the pre-existing raw passthrough.
|
|
5151
|
+
expect(newProps).toContain('Label: in.Label,')
|
|
5152
|
+
})
|
|
5153
|
+
|
|
5154
|
+
test('a `/ 0` operand declines collision-derivation — Go constant division by zero is a compile error', () => {
|
|
5155
|
+
const result = compileJSX(`
|
|
5156
|
+
'use client'
|
|
5157
|
+
import { createSignal } from '@barefootjs/client'
|
|
5158
|
+
export function C(props: { count?: number }) {
|
|
5159
|
+
const [count, setCount] = createSignal((props.count ?? 1) / 0)
|
|
5160
|
+
return <span>{count()}</span>
|
|
5161
|
+
}
|
|
5162
|
+
`.trimStart(), 'test.tsx', { adapter: new GoTemplateAdapter(), outputIR: false })
|
|
5163
|
+
const types = result.files.find(f => f.type === 'types')!.content
|
|
5164
|
+
const newProps = types.slice(types.indexOf('func NewCProps'))
|
|
5165
|
+
expect(newProps).not.toContain('count / 0')
|
|
5166
|
+
expect(newProps).toContain('Count: in.Count,')
|
|
5167
|
+
})
|
|
5168
|
+
|
|
5169
|
+
test('the same collision reached through a component-scope const hop (#2685) lowers identically', () => {
|
|
5170
|
+
const direct = compileJSX(`
|
|
5171
|
+
'use client'
|
|
5172
|
+
import { createSignal } from '@barefootjs/client'
|
|
5173
|
+
export function Direct(props: { count?: number }) {
|
|
5174
|
+
const [count, setCount] = createSignal((props.count ?? 1) * 2)
|
|
5175
|
+
return <span>{count()}</span>
|
|
5176
|
+
}
|
|
5177
|
+
`.trimStart(), 'test.tsx', { adapter: new GoTemplateAdapter(), outputIR: false })
|
|
5178
|
+
const viaConst = compileJSX(`
|
|
5179
|
+
'use client'
|
|
5180
|
+
import { createSignal } from '@barefootjs/client'
|
|
5181
|
+
export function ViaConst(props: { count?: number }) {
|
|
5182
|
+
const mid = props.count
|
|
5183
|
+
const [count, setCount] = createSignal((mid ?? 1) * 2)
|
|
5184
|
+
return <span>{count()}</span>
|
|
5185
|
+
}
|
|
5186
|
+
`.trimStart(), 'test.tsx', { adapter: new GoTemplateAdapter(), outputIR: false })
|
|
5187
|
+
const directTypes = direct.files.find(f => f.type === 'types')!.content
|
|
5188
|
+
const viaConstTypes = viaConst.files.find(f => f.type === 'types')!.content
|
|
5189
|
+
expect(directTypes.slice(directTypes.indexOf('func NewDirectProps'))).toContain('Count: count * 2,')
|
|
5190
|
+
expect(viaConstTypes.slice(viaConstTypes.indexOf('func NewViaConstProps'))).toContain('Count: count * 2,')
|
|
5191
|
+
})
|
|
5192
|
+
|
|
5193
|
+
test('a differently-named signal deriving from the same prop (no collision) is untouched — its own field, not the shared one', () => {
|
|
5194
|
+
const result = compileJSX(`
|
|
5195
|
+
'use client'
|
|
5196
|
+
import { createSignal } from '@barefootjs/client'
|
|
5197
|
+
export function NonCollide(props: { count?: number }) {
|
|
5198
|
+
const [doubled, setDoubled] = createSignal((props.count ?? 1) * 2)
|
|
5199
|
+
return <span>{doubled()}</span>
|
|
5200
|
+
}
|
|
5201
|
+
`.trimStart(), 'test.tsx', { adapter: new GoTemplateAdapter(), outputIR: false })
|
|
5202
|
+
const types = result.files.find(f => f.type === 'types')!.content
|
|
5203
|
+
// No collision here — `count`'s own field stays the plain concrete type,
|
|
5204
|
+
// byte-identical to before this fix.
|
|
5205
|
+
expect(types).toContain('Count int')
|
|
5206
|
+
expect(types).not.toContain('Count interface{}')
|
|
5207
|
+
const newProps = types.slice(types.indexOf('func NewNonCollideProps'))
|
|
5208
|
+
expect(newProps).not.toContain('var count int')
|
|
5209
|
+
expect(newProps).toContain('Count: in.Count,')
|
|
5210
|
+
})
|
|
5211
|
+
})
|
|
5212
|
+
|
|
5024
5213
|
// #2228: a `.filter(t => …).map(todo => <Child todo={todo} .../>)` loop whose
|
|
5025
5214
|
// body is a single child component ranges the WRAPPER slice (`.TodoItems`,
|
|
5026
5215
|
// `.{ChildName}s` — see #2130 above), so `{{if}}`'s dot context for the
|
|
@@ -6032,9 +6221,11 @@ export { TaggedList }
|
|
|
6032
6221
|
expect(types).toContain('Title string `json:"title"`')
|
|
6033
6222
|
expect(types).toContain('Tags []string `json:"tags"`')
|
|
6034
6223
|
// The Input/Props field is a typed struct slice, not the old
|
|
6035
|
-
// `[]map[string]interface{}` fallback.
|
|
6224
|
+
// `[]map[string]interface{}` fallback. Scoped to the field itself —
|
|
6225
|
+
// `BfCallerProps map[string]interface{}` (#2684) legitimately appears
|
|
6226
|
+
// elsewhere in every generated Props struct now.
|
|
6036
6227
|
expect(types).toMatch(/Items \[\]TaggedListItemsItem/)
|
|
6037
|
-
expect(types).not.
|
|
6228
|
+
expect(types).not.toMatch(/Items \[?\]?map\[string\]interface\{\}/)
|
|
6038
6229
|
})
|
|
6039
6230
|
|
|
6040
6231
|
test('nested anonymous property inside a NAMED type synthesizes a named struct (case ii, Row.user)', () => {
|
|
@@ -6068,7 +6259,10 @@ export function NestedNames() {
|
|
|
6068
6259
|
// The signal's inline initial value bakes through the STRUCT literal
|
|
6069
6260
|
// path, not `bakeInlineObjectAsGoMap`'s capitalized-key map convention.
|
|
6070
6261
|
expect(types).toContain('Row{ID: "r1", User: RowUser{Name: "Ada"}}')
|
|
6071
|
-
|
|
6262
|
+
// Scoped to the `User` field itself — `BfCallerProps
|
|
6263
|
+
// map[string]interface{}` (#2684) legitimately appears elsewhere in
|
|
6264
|
+
// every generated Props struct now.
|
|
6265
|
+
expect(types).not.toMatch(/User map\[string\]interface\{\}/)
|
|
6072
6266
|
})
|
|
6073
6267
|
|
|
6074
6268
|
test('a synthesized-name collision gracefully falls back to the pre-#2674 map convention, not a regression', () => {
|
|
@@ -6165,7 +6359,53 @@ export function NestedNames() {
|
|
|
6165
6359
|
.replace(/"/g, '"')
|
|
6166
6360
|
.replace(/"/g, '"')
|
|
6167
6361
|
const payload = JSON.parse(decoded)
|
|
6168
|
-
// No props at all on this component — bf-p carries nothing.
|
|
6362
|
+
// No props at all on this component — bf-p carries nothing. Still a
|
|
6363
|
+
// real, present `bf-p="{}"` (#2684's sidecar substitution isn't gated
|
|
6364
|
+
// on emptiness — see `BfPropsAttr`'s doc comment for why).
|
|
6169
6365
|
expect(payload).toEqual({})
|
|
6170
6366
|
})
|
|
6367
|
+
|
|
6368
|
+
// #2684 end-to-end: an omitted optional prop must not resurrect the
|
|
6369
|
+
// author's default, or a `null`, in the wire payload — and an EXPLICIT
|
|
6370
|
+
// falsy/zero value the caller DID pass must still come through.
|
|
6371
|
+
test('real go-run render: bf-p carries only caller-supplied keys — omitted optional absent, explicit zero present, required always present', async () => {
|
|
6372
|
+
const source = `
|
|
6373
|
+
'use client'
|
|
6374
|
+
import { createSignal } from '@barefootjs/client'
|
|
6375
|
+
export function C(props: { x?: number; label?: string; name: string }) {
|
|
6376
|
+
const [x] = createSignal(props.x ?? 7)
|
|
6377
|
+
return <div data-name={props.name}>{x()}</div>
|
|
6378
|
+
}
|
|
6379
|
+
export { C }
|
|
6380
|
+
`
|
|
6381
|
+
let htmlOmitted: string
|
|
6382
|
+
let htmlExplicitZero: string
|
|
6383
|
+
try {
|
|
6384
|
+
htmlOmitted = await renderGoTemplateComponent({
|
|
6385
|
+
source,
|
|
6386
|
+
adapter: new GoTemplateAdapter(),
|
|
6387
|
+
props: { name: 'Ada' },
|
|
6388
|
+
})
|
|
6389
|
+
htmlExplicitZero = await renderGoTemplateComponent({
|
|
6390
|
+
source,
|
|
6391
|
+
adapter: new GoTemplateAdapter(),
|
|
6392
|
+
props: { name: 'Ada', x: 0 },
|
|
6393
|
+
})
|
|
6394
|
+
} catch (err) {
|
|
6395
|
+
if (err instanceof GoNotAvailableError) return // Go toolchain not installed on this host
|
|
6396
|
+
throw err
|
|
6397
|
+
}
|
|
6398
|
+
const decode = (html: string) => {
|
|
6399
|
+
const m = html.match(/bf-p="([^"]*)"/)
|
|
6400
|
+
expect(m).not.toBeNull()
|
|
6401
|
+
return JSON.parse(m![1].replace(/"/g, '"').replace(/"/g, '"'))
|
|
6402
|
+
}
|
|
6403
|
+
// `x` omitted: no baked `7`, no `null` — the key is simply absent.
|
|
6404
|
+
// `label` (class-3 residual, no consumption anywhere) still shows up at
|
|
6405
|
+
// its Go zero value — documented, not silently dropped.
|
|
6406
|
+
expect(decode(htmlOmitted)).toEqual({ name: 'Ada', label: '' })
|
|
6407
|
+
// `x: 0` explicitly passed: present with the caller's real value, not
|
|
6408
|
+
// coalesced away or confused with "absent".
|
|
6409
|
+
expect(decode(htmlExplicitZero)).toEqual({ name: 'Ada', label: '', x: 0 })
|
|
6410
|
+
})
|
|
6171
6411
|
})
|
|
@@ -59,6 +59,20 @@ export interface GoEmitContext {
|
|
|
59
59
|
preParsed?: ParsedExpr,
|
|
60
60
|
): { propName: string; goFallback: string } | null
|
|
61
61
|
|
|
62
|
+
/**
|
|
63
|
+
* #2683: match the collision-derivation shape `(props.X ?? <lit>) <op>
|
|
64
|
+
* <int>` against an already-resolved `ParsedExpr` — the ONE non-idempotent
|
|
65
|
+
* form this adapter faithfully lowers when a signal's Go field name
|
|
66
|
+
* collides with its own prop's field. Composes
|
|
67
|
+
* {@link extractPropFallback}'s structural presence-check recognition
|
|
68
|
+
* (applied to the embedded `??` subtree) with the same non-negative-
|
|
69
|
+
* integer arithmetic wrap the memo-computation emitter already supports
|
|
70
|
+
* for a bare `props.X <op> N`. Returns null for any other shape.
|
|
71
|
+
*/
|
|
72
|
+
extractCollisionDerivation(
|
|
73
|
+
parsed: ParsedExpr | undefined,
|
|
74
|
+
): { propName: string; goFallback: string; operator: string; operand: string } | null
|
|
75
|
+
|
|
62
76
|
/**
|
|
63
77
|
* Inline a module string const by name as a Go double-quoted literal
|
|
64
78
|
* (`"<escaped>"`), or null when the name is not such a const (loop vars and
|