@barefootjs/go-template 0.18.3 → 0.18.5
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 +35 -1
- package/dist/adapter/go-template-adapter.d.ts.map +1 -1
- package/dist/adapter/index.js +162 -21
- package/dist/adapter/lib/compile-state.d.ts +7 -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 +21 -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 +162 -21
- package/dist/conformance-pins.d.ts.map +1 -1
- package/dist/index.js +164 -43
- package/dist/render-divergences.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/__tests__/go-template-adapter.test.ts +1 -1
- package/src/adapter/go-template-adapter.ts +226 -9
- package/src/adapter/lib/compile-state.ts +8 -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 +45 -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 +0 -5
- package/src/render-divergences.ts +0 -36
- package/src/test-render.ts +23 -8
|
@@ -66,9 +66,10 @@ import {
|
|
|
66
66
|
prepareLoweringMatchers,
|
|
67
67
|
envSignalReaderFor,
|
|
68
68
|
computeSsrSeedPlan,
|
|
69
|
+
isStringConcatBinary,
|
|
69
70
|
} from '@barefootjs/jsx'
|
|
70
71
|
import { findInterpolationEnd } from '@barefootjs/jsx/scanner'
|
|
71
|
-
import { BF_REGION } from '@barefootjs/shared'
|
|
72
|
+
import { BF_REGION, escapeHtml } from '@barefootjs/shared'
|
|
72
73
|
|
|
73
74
|
import {
|
|
74
75
|
GO_IDENTIFIER,
|
|
@@ -126,6 +127,7 @@ import { resolveBlockBodyMemoModuleConst } from "./memo/memo-value.ts"
|
|
|
126
127
|
import { computeMemoInitialValue, computeMemoInitialValueOrNull, filterArmEarlierSiblingRefs } from "./memo/memo-compute.ts"
|
|
127
128
|
import { collectSpreadSlots, buildSpreadInitializer } from "./spread/spread-codegen.ts"
|
|
128
129
|
import { buildPropTypeOverrides, resolvePropGoType, collectNillablePropNames } from "./props/prop-types.ts"
|
|
130
|
+
import { collectStringValueNames } from "./props/prop-classes.ts"
|
|
129
131
|
|
|
130
132
|
export type { GoTemplateAdapterOptions } from "./lib/types.ts"
|
|
131
133
|
|
|
@@ -225,6 +227,15 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
225
227
|
|
|
226
228
|
private inLoop: boolean = false
|
|
227
229
|
private loopParamStack: string[] = []
|
|
230
|
+
/**
|
|
231
|
+
* Stack of `IRLoop.depth` values (innermost last), pushed/popped around
|
|
232
|
+
* `renderChildren(loop.children)` in `renderLoop`. `renderAttributes`
|
|
233
|
+
* reads the top of this stack to derive the `key` → `data-key`/
|
|
234
|
+
* `data-key-N` suffix — the depth is IR-computed (jsx-to-ir.ts), not
|
|
235
|
+
* re-derived here; this stack only threads that value down to the
|
|
236
|
+
* loop body's root element (#2168 nested-loop-outer-binding).
|
|
237
|
+
*/
|
|
238
|
+
private loopKeyDepthStack: number[] = []
|
|
228
239
|
/**
|
|
229
240
|
* Per-loop: true when the body renders the bare range value (scalar-item
|
|
230
241
|
* inline-literal loop), so the `bf_tmpl` companion is fed `.BfLoopItem` (the
|
|
@@ -331,6 +342,7 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
331
342
|
this.state.pendingChildrenDefines = []
|
|
332
343
|
this.primeCompileState(ir)
|
|
333
344
|
this.state.nillablePropNames = collectNillablePropNames(this.emitCtx, ir)
|
|
345
|
+
this.state.stringValueNames = collectStringValueNames(ir)
|
|
334
346
|
|
|
335
347
|
// Surface loop-body usages of sibling-imported components (see
|
|
336
348
|
// `checkImportedLoopChildComponents`). The barefoot CLI compiles a
|
|
@@ -1369,6 +1381,35 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
1369
1381
|
break
|
|
1370
1382
|
}
|
|
1371
1383
|
}
|
|
1384
|
+
// A number/boolean JSX-EXPRESSION literal (`count={5}`,
|
|
1385
|
+
// `active={true}`) — as opposed to a plain quoted string attr
|
|
1386
|
+
// (`label="mail"`, which is `case 'literal':` above, an entirely
|
|
1387
|
+
// different `AttrValue` kind) — is still `kind: 'expression'`
|
|
1388
|
+
// here, since curly braces always parse to an expression
|
|
1389
|
+
// container regardless of what's inside them. #2168
|
|
1390
|
+
// child-primitive-props: `resolveDynamicPropValue` below only
|
|
1391
|
+
// recognizes a getter call or a comparison against one; a bare
|
|
1392
|
+
// `5`/`true` matches neither, so the field was silently OMITTED
|
|
1393
|
+
// and the Badge's `Count`/`Active` fields defaulted to Go's zero
|
|
1394
|
+
// value (`0`/`false`) regardless of the actual literal.
|
|
1395
|
+
//
|
|
1396
|
+
// Route through `parsedLiteralToGo` rather than hand-rolling a
|
|
1397
|
+
// switch on `.value`/`.literalType`: a numeric literal needs its
|
|
1398
|
+
// exact source `raw` token (Copilot review — `String(value)` can
|
|
1399
|
+
// change spelling/precision, e.g. a large integer or `-0`), and
|
|
1400
|
+
// `parsedLiteralToGo` also covers the LEADING-UNARY-MINUS shape
|
|
1401
|
+
// (`count={-5}` parses as `kind: 'unary'` wrapping the literal,
|
|
1402
|
+
// not `kind: 'literal'` itself — a case this branch's earlier
|
|
1403
|
+
// `parsedValue?.kind === 'literal'` gate missed entirely and
|
|
1404
|
+
// would have silently reintroduced the same omitted-field bug
|
|
1405
|
+
// for a negative numeric literal).
|
|
1406
|
+
if (parsedValue) {
|
|
1407
|
+
const goVal = parsedLiteralToGo(this.emitCtx, parsedValue)
|
|
1408
|
+
if (goVal !== null) {
|
|
1409
|
+
emitChildField(prop.name, goVal)
|
|
1410
|
+
break
|
|
1411
|
+
}
|
|
1412
|
+
}
|
|
1372
1413
|
const resolvedValue = this.resolveDynamicPropValue(
|
|
1373
1414
|
exprText,
|
|
1374
1415
|
ir.metadata.signals,
|
|
@@ -1381,7 +1422,40 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
1381
1422
|
break
|
|
1382
1423
|
}
|
|
1383
1424
|
case 'jsx-children':
|
|
1384
|
-
//
|
|
1425
|
+
// The RESERVED children slot (`comp.children.length > 0 ? comp.children
|
|
1426
|
+
// : jsxChildrenPropNodes(...)`) is handled below via
|
|
1427
|
+
// `child.childrenText` / `child.childrenHtml` and must not be
|
|
1428
|
+
// re-emitted here. A JSX-valued prop under any OTHER name
|
|
1429
|
+
// (`header={<strong>Title</strong>}`, #2168 jsx-element-prop) is a
|
|
1430
|
+
// named slot — bake it the same way real children are baked
|
|
1431
|
+
// (`extractTextChildren` / `extractHtmlChildren`) and emit it as
|
|
1432
|
+
// its own struct field, mirroring the `Children` field's
|
|
1433
|
+
// text-vs-HTML branching just below.
|
|
1434
|
+
if (prop.name !== 'children') {
|
|
1435
|
+
const text = this.extractTextChildren(prop.value.children)
|
|
1436
|
+
if (text !== null) {
|
|
1437
|
+
emitChildField(prop.name, JSON.stringify(text))
|
|
1438
|
+
} else {
|
|
1439
|
+
const html = this.extractHtmlChildren(prop.value.children)
|
|
1440
|
+
if (html !== null) {
|
|
1441
|
+
this.state.usesHtmlTemplate = true
|
|
1442
|
+
emitChildField(prop.name, `template.HTML(${JSON.stringify(html)})`)
|
|
1443
|
+
} else {
|
|
1444
|
+
// The value's root needs the PARENT's runtime scope id
|
|
1445
|
+
// (`<strong>` hoisted from the call site inherits the
|
|
1446
|
+
// caller's `bf-s`, not a bake-time constant) — same
|
|
1447
|
+
// needsScope case `childrenScopedHtmlExpr` handles for
|
|
1448
|
+
// the reserved children slot. The returned string is
|
|
1449
|
+
// already a Go concatenation expression (`"..." +
|
|
1450
|
+
// scopeID + "..."`), not a literal to re-quote.
|
|
1451
|
+
const scopedHtml = this.extractScopedHtmlChildren(prop.value.children)
|
|
1452
|
+
if (scopedHtml !== null) {
|
|
1453
|
+
this.state.usesHtmlTemplate = true
|
|
1454
|
+
emitChildField(prop.name, `template.HTML(${scopedHtml})`)
|
|
1455
|
+
}
|
|
1456
|
+
}
|
|
1457
|
+
}
|
|
1458
|
+
}
|
|
1385
1459
|
break
|
|
1386
1460
|
}
|
|
1387
1461
|
}
|
|
@@ -2371,6 +2445,66 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
2371
2445
|
}
|
|
2372
2446
|
}
|
|
2373
2447
|
|
|
2448
|
+
// A bare passthrough of the CALLER's own prop — `text={props.label}`
|
|
2449
|
+
// forwarding into a nested child-component invocation (or the
|
|
2450
|
+
// destructured form, `text={label}`) — #2168 grandchild-composition: a
|
|
2451
|
+
// prop re-forwarded through a second component layer matched neither
|
|
2452
|
+
// pattern above (not a getter call, not a literal — that's `case
|
|
2453
|
+
// 'literal'` above this function entirely) and fell through to `null`,
|
|
2454
|
+
// silently OMITTING the field so Go's zero value applied instead of the
|
|
2455
|
+
// threaded value. Only an EXACT `props.<name>` / bare `<name>` — no `??`
|
|
2456
|
+
// /`||` suffix — qualifies; a fallback-bearing expression isn't a pure
|
|
2457
|
+
// passthrough and must keep falling through to `null` rather than
|
|
2458
|
+
// silently dropping the fallback.
|
|
2459
|
+
//
|
|
2460
|
+
// Guarded against a LOCAL const/helper shadowing a propsParam's name
|
|
2461
|
+
// (`const label = compute(); ... text={label}` inside a component whose
|
|
2462
|
+
// OWN prop is also called `label`) — that bare `label` means the local,
|
|
2463
|
+
// not the prop, and must keep falling through to `null` rather than
|
|
2464
|
+
// being misresolved to `in.Label`.
|
|
2465
|
+
const propsObjectName = this.state.propsObjectName
|
|
2466
|
+
// `$` is a valid JS/TS identifier character (Copilot review, #2198) —
|
|
2467
|
+
// without it, a passthrough like `text={$label}` or `text={props.$label}`
|
|
2468
|
+
// never matches either shape below and silently falls through to `null`.
|
|
2469
|
+
const identifierPattern = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/
|
|
2470
|
+
const bareIdentifier = identifierPattern.test(expr) ? expr : null
|
|
2471
|
+
// Plain prefix/suffix check rather than an `expr`-interpolated `RegExp`:
|
|
2472
|
+
// `propsObjectName` is an arbitrary identifier and could itself contain
|
|
2473
|
+
// regex metacharacters (e.g. `$props`), which would silently build a
|
|
2474
|
+
// malformed pattern instead of erroring.
|
|
2475
|
+
const barePropAccess =
|
|
2476
|
+
propsObjectName &&
|
|
2477
|
+
expr.startsWith(`${propsObjectName}.`) &&
|
|
2478
|
+
identifierPattern.test(expr.slice(propsObjectName.length + 1))
|
|
2479
|
+
? expr.slice(propsObjectName.length + 1)
|
|
2480
|
+
: null
|
|
2481
|
+
const passthroughName = bareIdentifier ?? barePropAccess
|
|
2482
|
+
// A `localConstants` entry sharing this name is a genuine SHADOW only
|
|
2483
|
+
// when it's an independent local — NOT when it's the analyzer's own
|
|
2484
|
+
// body-level destructure alias (`const { label } = props`, expanded to
|
|
2485
|
+
// a `localConstants` entry named `label` valued exactly `props.label`,
|
|
2486
|
+
// #1138/analyzer.ts `collectConstant`). That alias IS the same prop
|
|
2487
|
+
// value, so a bare `label` reference is still a pure passthrough
|
|
2488
|
+
// (Copilot review, #2198 — the earlier blanket "any `localConstants`
|
|
2489
|
+
// match blocks it" check wrongly reintroduced the omitted-field bug for
|
|
2490
|
+
// exactly this common destructured-body pattern). A `?? default`-bearing
|
|
2491
|
+
// alias value does NOT match the exact-string check below, so it's
|
|
2492
|
+
// correctly still treated as a shadow (a fallback-bearing local isn't a
|
|
2493
|
+
// pure passthrough either — same reasoning as the outer `??`/`||` guard
|
|
2494
|
+
// above).
|
|
2495
|
+
const localConst = this.state.localConstants.find(c => c.name === passthroughName)
|
|
2496
|
+
const isPropsDestructureAlias =
|
|
2497
|
+
localConst !== undefined &&
|
|
2498
|
+
propsObjectName !== null &&
|
|
2499
|
+
localConst.value === `${propsObjectName}.${passthroughName}`
|
|
2500
|
+
const shadowedByLocal =
|
|
2501
|
+
passthroughName !== null &&
|
|
2502
|
+
((localConst !== undefined && !isPropsDestructureAlias) ||
|
|
2503
|
+
this.state.localHelperNames.has(passthroughName))
|
|
2504
|
+
if (passthroughName && !shadowedByLocal && propsParams.some(p => p.name === passthroughName)) {
|
|
2505
|
+
return `in.${capitalizeFieldName(passthroughName)}`
|
|
2506
|
+
}
|
|
2507
|
+
|
|
2374
2508
|
return null
|
|
2375
2509
|
}
|
|
2376
2510
|
|
|
@@ -2579,7 +2713,9 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
2579
2713
|
}
|
|
2580
2714
|
|
|
2581
2715
|
emitText(node: IRText): string {
|
|
2582
|
-
|
|
2716
|
+
// IRText carries the entity-DECODED value (Phase 1 decodes JSX
|
|
2717
|
+
// character references); re-escape for direct HTML emission.
|
|
2718
|
+
return escapeHtml(node.value)
|
|
2583
2719
|
}
|
|
2584
2720
|
|
|
2585
2721
|
emitExpression(node: IRExpression): string {
|
|
@@ -3026,6 +3162,7 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
3026
3162
|
object: ParsedExpr,
|
|
3027
3163
|
property: string,
|
|
3028
3164
|
_computed: boolean,
|
|
3165
|
+
optional: boolean,
|
|
3029
3166
|
emit: (e: ParsedExpr) => string,
|
|
3030
3167
|
): string {
|
|
3031
3168
|
// .length on a `.filter(...)` callback call → len (bf_filter ...)
|
|
@@ -3116,6 +3253,21 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
3116
3253
|
|
|
3117
3254
|
const obj = emit(object)
|
|
3118
3255
|
if (property === 'length') return `len ${obj}`
|
|
3256
|
+
// A `?.`-written access (`user?.name`, #2168 optional-chaining-prop):
|
|
3257
|
+
// a plain `.Field` dot-chain panics evaluating a field on a nil
|
|
3258
|
+
// interface/pointer (`nil pointer evaluating interface {}.Name`), so
|
|
3259
|
+
// route through the runtime's existing nil-safe reflection-based
|
|
3260
|
+
// getter instead — `bf_get`/`getFieldValue` (bf.go), already used for
|
|
3261
|
+
// map-rooted context chains above, guards the nil case and returns Go
|
|
3262
|
+
// `nil` (which `or`/`bf.truthy?`-style fallbacks treat as falsy) —
|
|
3263
|
+
// unlike that map-rooted call site, this passes the Go-cased field
|
|
3264
|
+
// name (`goFieldNameForKey`), matching `getFieldValue`'s struct-branch
|
|
3265
|
+
// exact-match fast path. Only guards the single written `?.` hop, not
|
|
3266
|
+
// a JS-style whole-chain short-circuit — see the `ParsedExpr` `member`
|
|
3267
|
+
// variant's docstring for the multi-hop caveat.
|
|
3268
|
+
if (optional) {
|
|
3269
|
+
return `bf_get ${wrapIfMultiToken(obj)} ${JSON.stringify(goFieldNameForKey(property))}`
|
|
3270
|
+
}
|
|
3119
3271
|
return `${obj}.${goFieldNameForKey(property)}`
|
|
3120
3272
|
}
|
|
3121
3273
|
|
|
@@ -3158,7 +3310,7 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
3158
3310
|
case '<=':
|
|
3159
3311
|
return `le ${wl} ${wr}`
|
|
3160
3312
|
case '+':
|
|
3161
|
-
return
|
|
3313
|
+
return this._emitPlus(left, right, wl, wr)
|
|
3162
3314
|
case '-':
|
|
3163
3315
|
return `bf_sub ${wl} ${wr}`
|
|
3164
3316
|
case '*':
|
|
@@ -3172,6 +3324,45 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
3172
3324
|
}
|
|
3173
3325
|
}
|
|
3174
3326
|
|
|
3327
|
+
/**
|
|
3328
|
+
* JS `+` with a string-typed operand is CONCATENATION, not addition — Go's
|
|
3329
|
+
* `bf_add` coerces both sides through `toFloat64` (#2168
|
|
3330
|
+
* string-concat-plus: `'Hello, ' + name` rendered "0", since a string
|
|
3331
|
+
* operand's `toFloat64` is 0). `html/template` has no infix `+` at all, so
|
|
3332
|
+
* both directions are a runtime call; route through `bf_concat_str` when
|
|
3333
|
+
* either operand is string-typed.
|
|
3334
|
+
*
|
|
3335
|
+
* Shared by all THREE `case '+':` sites in this file (`binary()` here, the
|
|
3336
|
+
* filter-predicate emitter's own `binary` case, and the condition-
|
|
3337
|
+
* expression emitter's own `binary` case) — each recurses over its own
|
|
3338
|
+
* `ParsedExpr` tree with its own rendered-operand strings, but the
|
|
3339
|
+
* string-vs-numeric decision itself must stay identical everywhere so the
|
|
3340
|
+
* three never drift out of sync (a Copilot review comment on #2197 flagged
|
|
3341
|
+
* exactly this risk when they were three independent inline checks).
|
|
3342
|
+
* Callers pass their own `leftExpr`/`rightExpr` (the raw `ParsedExpr` nodes
|
|
3343
|
+
* `isStringConcatBinary` inspects) and `leftRendered`/`rightRendered` (the
|
|
3344
|
+
* already-emitted, already-wrapped/parenthesised operand text for THIS
|
|
3345
|
+
* call site's Go form).
|
|
3346
|
+
*/
|
|
3347
|
+
private _emitPlus(
|
|
3348
|
+
leftExpr: ParsedExpr,
|
|
3349
|
+
rightExpr: ParsedExpr,
|
|
3350
|
+
leftRendered: string,
|
|
3351
|
+
rightRendered: string,
|
|
3352
|
+
): string {
|
|
3353
|
+
if (isStringConcatBinary('+', leftExpr, rightExpr, n => this._isStringValueName(n))) {
|
|
3354
|
+
return `bf_concat_str ${leftRendered} ${rightRendered}`
|
|
3355
|
+
}
|
|
3356
|
+
return `bf_add ${leftRendered} ${rightRendered}`
|
|
3357
|
+
}
|
|
3358
|
+
|
|
3359
|
+
/** Whether `name` (a signal getter or prop) holds a string value — drives
|
|
3360
|
+
* `isStringConcatBinary`'s string-vs-numeric `+` decision (#2168
|
|
3361
|
+
* string-concat-plus). */
|
|
3362
|
+
private _isStringValueName(name: string): boolean {
|
|
3363
|
+
return this.state.stringValueNames.has(name)
|
|
3364
|
+
}
|
|
3365
|
+
|
|
3175
3366
|
unary(op: string, argument: ParsedExpr, emit: (e: ParsedExpr) => string): string {
|
|
3176
3367
|
const arg = emit(argument)
|
|
3177
3368
|
if (op === '!') return `not ${arg}`
|
|
@@ -3509,6 +3700,15 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
3509
3700
|
const recv = emit(object)
|
|
3510
3701
|
return `bf_trim ${wrapIfMultiToken(recv)}`
|
|
3511
3702
|
}
|
|
3703
|
+
case 'trimStart':
|
|
3704
|
+
case 'trimEnd': {
|
|
3705
|
+
// `.trimStart()` / `.trimEnd()` — the one-sided siblings of `.trim()`
|
|
3706
|
+
// (#2183 follow-up). Dedicated `bf_trim_start` / `bf_trim_end`
|
|
3707
|
+
// helpers, not `bf_trim` with a flag.
|
|
3708
|
+
const fn = method === 'trimStart' ? 'bf_trim_start' : 'bf_trim_end'
|
|
3709
|
+
const recv = emit(object)
|
|
3710
|
+
return `${fn} ${wrapIfMultiToken(recv)}`
|
|
3711
|
+
}
|
|
3512
3712
|
case 'toFixed': {
|
|
3513
3713
|
// `.toFixed(digits?)` → `bf_to_fixed` (`fmt.Sprintf("%.*f", …)`); default
|
|
3514
3714
|
// 0 digits when the argument is omitted.
|
|
@@ -3555,6 +3755,16 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
3555
3755
|
const newS = emit(args[1])
|
|
3556
3756
|
return `bf_replace ${wrapIfMultiToken(recv)} ${wrapIfMultiToken(oldS)} ${wrapIfMultiToken(newS)}`
|
|
3557
3757
|
}
|
|
3758
|
+
case 'replaceAll': {
|
|
3759
|
+
// `.replaceAll(old, new)` — string-pattern form, EVERY occurrence, via
|
|
3760
|
+
// `bf_replace_all` (`strings.ReplaceAll`). A dedicated helper, not
|
|
3761
|
+
// `bf_replace` with a different n — the regex-pattern form is refused
|
|
3762
|
+
// upstream at the parser, same as `.replace`.
|
|
3763
|
+
const recv = emit(object)
|
|
3764
|
+
const oldS = emit(args[0])
|
|
3765
|
+
const newS = emit(args[1])
|
|
3766
|
+
return `bf_replace_all ${wrapIfMultiToken(recv)} ${wrapIfMultiToken(oldS)} ${wrapIfMultiToken(newS)}`
|
|
3767
|
+
}
|
|
3558
3768
|
case 'repeat': {
|
|
3559
3769
|
// `.repeat(n)` — string repeated `n` times. `bf_repeat` clamps a negative
|
|
3560
3770
|
// count to "" instead of letting `strings.Repeat` panic. No argument is
|
|
@@ -4063,7 +4273,7 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
4063
4273
|
case '<=':
|
|
4064
4274
|
return `le ${left} ${right}`
|
|
4065
4275
|
case '+':
|
|
4066
|
-
return
|
|
4276
|
+
return this._emitPlus(expr.left, expr.right, left, right)
|
|
4067
4277
|
case '-':
|
|
4068
4278
|
return `bf_sub ${left} ${right}`
|
|
4069
4279
|
case '*':
|
|
@@ -4627,7 +4837,7 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
4627
4837
|
case '<=':
|
|
4628
4838
|
result = `le ${left} ${right}`; break
|
|
4629
4839
|
case '+':
|
|
4630
|
-
result =
|
|
4840
|
+
result = this._emitPlus(expr.left, expr.right, left, right); break
|
|
4631
4841
|
case '-':
|
|
4632
4842
|
result = `bf_sub ${left} ${right}`; break
|
|
4633
4843
|
case '*':
|
|
@@ -4920,7 +5130,9 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
4920
5130
|
this.scalarLiteralLoopGoType(loop.arrayParsed, loop.itemType) !== null,
|
|
4921
5131
|
)
|
|
4922
5132
|
this.loopWrapperStack.push(!!loop.childComponent)
|
|
5133
|
+
this.loopKeyDepthStack.push(loop.depth)
|
|
4923
5134
|
const children = this.renderChildren(loop.children)
|
|
5135
|
+
this.loopKeyDepthStack.pop()
|
|
4924
5136
|
this.loopWrapperStack.pop()
|
|
4925
5137
|
this.loopScalarItemStack.pop()
|
|
4926
5138
|
// Build the per-item anchor marker while the loop param is still on the
|
|
@@ -5181,7 +5393,7 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
5181
5393
|
* doesn't share a contract with the intrinsic-attribute one.
|
|
5182
5394
|
*/
|
|
5183
5395
|
private readonly elementAttrEmitter: AttrValueEmitter = {
|
|
5184
|
-
emitLiteral: (value, name) => `${name}="${value.value}"`,
|
|
5396
|
+
emitLiteral: (value, name) => `${name}="${escapeHtml(value.value)}"`,
|
|
5185
5397
|
emitExpression: (value, name) => {
|
|
5186
5398
|
// `style={{ … }}` object literal → a CSS string with dynamic values
|
|
5187
5399
|
// interpolated, instead of refusing the bare object with BF101.
|
|
@@ -5365,10 +5577,15 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
5365
5577
|
// Rewrite JSX special-prop names to their HTML-attribute counterparts. The
|
|
5366
5578
|
// Go template adapter has no JSX runtime to strip `key` / emit `data-key`,
|
|
5367
5579
|
// so the rewrite happens at attribute-emit time. Mirror of the `key`
|
|
5368
|
-
// branch in `ir-to-client-js/html-template.ts`.
|
|
5580
|
+
// branch in `ir-to-client-js/html-template.ts`. The depth-suffix (plain
|
|
5581
|
+
// `data-key` at the outermost loop, `data-key-N` N levels deep) comes
|
|
5582
|
+
// from `IRLoop.depth` via `loopKeyDepthStack`, not re-derived here.
|
|
5369
5583
|
let attrName: string
|
|
5370
5584
|
if (attr.name === 'className') attrName = 'class'
|
|
5371
|
-
else if (attr.name === 'key')
|
|
5585
|
+
else if (attr.name === 'key') {
|
|
5586
|
+
const depth = this.loopKeyDepthStack.at(-1) ?? 0
|
|
5587
|
+
attrName = depth > 0 ? `data-key-${depth}` : 'data-key'
|
|
5588
|
+
}
|
|
5372
5589
|
else attrName = attr.name
|
|
5373
5590
|
const lowered = emitAttrValue(attr.value, this.elementAttrEmitter, attrName)
|
|
5374
5591
|
if (lowered) parts.push(lowered)
|
|
@@ -137,6 +137,14 @@ export class CompileState {
|
|
|
137
137
|
*/
|
|
138
138
|
nillablePropNames: Set<string> = new Set()
|
|
139
139
|
|
|
140
|
+
/**
|
|
141
|
+
* String-typed signal getter / prop names (#2168 string-concat-plus).
|
|
142
|
+
* Feeds `isStringName` for `isStringConcatBinary`, which decides whether a
|
|
143
|
+
* JS `+` operand chain is string concatenation rather than numeric
|
|
144
|
+
* addition — see `collectStringValueNames` (`props/prop-classes.ts`).
|
|
145
|
+
*/
|
|
146
|
+
stringValueNames: Set<string> = new Set()
|
|
147
|
+
|
|
140
148
|
/** Component root scope element(s) — each carries `data-key` for a keyed loop
|
|
141
149
|
* item. */
|
|
142
150
|
rootScopeNodes: Set<IRNode> = new Set()
|
|
@@ -17,6 +17,7 @@ export const GO_TEMPLATE_PRIMITIVES: Record<string, PrimitiveSpec> = {
|
|
|
17
17
|
'Math.floor': { arity: 1, emit: (args) => `bf_floor ${wrapGoArg(args[0])}` },
|
|
18
18
|
'Math.ceil': { arity: 1, emit: (args) => `bf_ceil ${wrapGoArg(args[0])}` },
|
|
19
19
|
'Math.round': { arity: 1, emit: (args) => `bf_round ${wrapGoArg(args[0])}` },
|
|
20
|
+
'Math.abs': { arity: 1, emit: (args) => `bf_abs ${wrapGoArg(args[0])}` },
|
|
20
21
|
// Two-arg forms only; an N-arg `Math.min(a, b, c)` falls through to the
|
|
21
22
|
// standard BF101 unsupported-call diagnostic via the arity gate.
|
|
22
23
|
'Math.min': { arity: 2, emit: (args) => `bf_min ${wrapGoArg(args[0])} ${wrapGoArg(args[1])}` },
|
|
@@ -366,13 +366,40 @@ export function memoInitialFromParsedBody(
|
|
|
366
366
|
const operator = body.op
|
|
367
367
|
const operand = String(body.right.value)
|
|
368
368
|
|
|
369
|
-
// getter() * N — return the signal's
|
|
369
|
+
// getter() * N — return the signal's (or, #2168 memo-chain, another
|
|
370
|
+
// memo's) Go initial value times N. `resolveGetterValueAsGo` checks
|
|
371
|
+
// `signals` first (unchanged behavior for a signal-derived memo like
|
|
372
|
+
// `doubled = createMemo(() => count() * 2)`), then falls back to
|
|
373
|
+
// `ctx.state.currentMemos` and recurses — needed for a memo derived from
|
|
374
|
+
// ANOTHER memo (`label = createMemo(() => doubled() + 1)`), which this
|
|
375
|
+
// branch previously couldn't recognize at all (a signals-only lookup),
|
|
376
|
+
// silently folding to the Go zero value instead of "7".
|
|
370
377
|
const depName = getterCallName(body.left)
|
|
371
378
|
if (depName) {
|
|
372
|
-
const
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
379
|
+
const depInitial = resolveGetterValueAsGo(ctx, depName, signals, propsParams, propFallbackVars, resolving)
|
|
380
|
+
// `resolveGetterValueAsGo` can return an IIFE (`func() string { ... }()`
|
|
381
|
+
// / `func() interface{} { ... }()`, e.g. a memo that shadows `props.X
|
|
382
|
+
// ?? <lit>`, #2075) rather than a plain atom — none of those return
|
|
383
|
+
// types support Go's `<op>` arithmetic operators, so splicing one in
|
|
384
|
+
// bare would emit invalid Go (Copilot review, #2200: e.g. `operator
|
|
385
|
+
// is not defined on interface{}`/`string`). Bail (fall through to the
|
|
386
|
+
// caller's zero-value default) rather than emit broken arithmetic.
|
|
387
|
+
const isArithmeticSafe = depInitial !== null && !depInitial.startsWith('func(')
|
|
388
|
+
if (isArithmeticSafe) {
|
|
389
|
+
// A signal's own initial value is always a simple atom (a literal,
|
|
390
|
+
// `in.Field`, a hoisted var) — never needs grouping. A MEMO's initial
|
|
391
|
+
// value can itself be a compound expression from this exact branch
|
|
392
|
+
// one level up (`"3 * 2"`), and splicing that in bare under a
|
|
393
|
+
// DIFFERENT outer operator can silently invert precedence (a memo
|
|
394
|
+
// chain shaped `inner = () => count() + 1` then `outer = () =>
|
|
395
|
+
// inner() * 2` would fold to `3 + 1 * 2` = 5 in Go, vs JS's `(3+1)*2`
|
|
396
|
+
// = 8). Parenthesize whenever `depInitial` is compound (contains
|
|
397
|
+
// whitespace — a simple atom never does) so precedence is
|
|
398
|
+
// preserved regardless of which operator combination a memo chain
|
|
399
|
+
// uses. A simple atom is left bare so existing exact-text
|
|
400
|
+
// expectations (`Doubled: 3 * 2,`) don't gain a no-op paren.
|
|
401
|
+
const wrapped = /\s/.test(depInitial) ? `(${depInitial})` : depInitial
|
|
402
|
+
return `${wrapped} ${operator} ${operand}`
|
|
376
403
|
}
|
|
377
404
|
}
|
|
378
405
|
|
|
@@ -408,12 +435,13 @@ export function memoInitialFromParsedBody(
|
|
|
408
435
|
}
|
|
409
436
|
}
|
|
410
437
|
|
|
411
|
-
// () => getter() — just return the signal's
|
|
438
|
+
// () => getter() — just return the signal's (or another memo's, #2168
|
|
439
|
+
// memo-chain) Go initial value.
|
|
412
440
|
const simpleDep = getterCallName(body)
|
|
413
441
|
if (simpleDep) {
|
|
414
|
-
const
|
|
415
|
-
if (
|
|
416
|
-
return
|
|
442
|
+
const depInitial = resolveGetterValueAsGo(ctx, simpleDep, signals, propsParams, propFallbackVars, resolving)
|
|
443
|
+
if (depInitial !== null) {
|
|
444
|
+
return depInitial
|
|
417
445
|
}
|
|
418
446
|
}
|
|
419
447
|
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prop classification for the Go template adapter.
|
|
3
|
+
*
|
|
4
|
+
* Ported from `packages/adapter-blade/src/adapter/props/prop-classes.ts` (itself
|
|
5
|
+
* ported from Jinja) — ONE function: string-typed signal/prop names, needed to
|
|
6
|
+
* decide `+` string-concat vs numeric addition (#2168 string-concat-plus). Pure
|
|
7
|
+
* function over `ir.metadata`; no adapter instance state.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { ComponentIR, TypeInfo } from '@barefootjs/jsx'
|
|
11
|
+
|
|
12
|
+
/** True when `type` is the `string` primitive. */
|
|
13
|
+
function isStringTypeInfo(type: TypeInfo): boolean {
|
|
14
|
+
return type.kind === 'primitive' && type.primitive === 'string'
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** True when `initialValue` is a bare string-literal expression. */
|
|
18
|
+
function isBareStringLiteral(initialValue: string | undefined): boolean {
|
|
19
|
+
if (!initialValue) return false
|
|
20
|
+
const v = initialValue.trim()
|
|
21
|
+
return (v.startsWith("'") && v.endsWith("'")) || (v.startsWith('"') && v.endsWith('"'))
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* String-typed signals and props. A signal is string-typed when its inferred
|
|
26
|
+
* type is `string` (or, defensively, when its initial value is a bare string
|
|
27
|
+
* literal); a prop when its annotated type is `string`. Drives `isStringName`
|
|
28
|
+
* for `isStringConcatBinary` — the shared helper (`@barefootjs/jsx`) that
|
|
29
|
+
* decides whether a JS `+` is string concatenation rather than numeric
|
|
30
|
+
* addition (Go's `html/template` has no native `+` at all; `binary()` always
|
|
31
|
+
* emits a runtime call, `bf_add` for addition or `bf_concat_str` for
|
|
32
|
+
* concatenation — see `go-template-adapter.ts`'s `binary()`).
|
|
33
|
+
*/
|
|
34
|
+
export function collectStringValueNames(ir: ComponentIR): Set<string> {
|
|
35
|
+
const names = new Set<string>()
|
|
36
|
+
for (const s of ir.metadata.signals) {
|
|
37
|
+
if (isStringTypeInfo(s.type) || isBareStringLiteral(s.initialValue)) {
|
|
38
|
+
names.add(s.getter)
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
for (const p of ir.metadata.propsParams) {
|
|
42
|
+
if (isStringTypeInfo(p.type)) names.add(p.name)
|
|
43
|
+
}
|
|
44
|
+
return names
|
|
45
|
+
}
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* generators and the nillable-field set so they can't drift.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import type { ComponentIR, IRMetadata } from '@barefootjs/jsx'
|
|
8
|
+
import type { ComponentIR, IRMetadata, IRNode, ParsedExpr } from '@barefootjs/jsx'
|
|
9
9
|
|
|
10
10
|
import type { GoEmitContext } from '../emit-context.ts'
|
|
11
11
|
import { typeInfoToGo } from '../type/type-codegen.ts'
|
|
@@ -35,9 +35,77 @@ export function buildPropTypeOverrides(ctx: GoEmitContext, ir: ComponentIR): Map
|
|
|
35
35
|
}
|
|
36
36
|
}
|
|
37
37
|
}
|
|
38
|
+
|
|
39
|
+
// A bare `number`-typed prop with no other evidence resolves to Go `int`
|
|
40
|
+
// (`typeInfoToGo`'s blind default) — but `.toFixed()` can only be called on
|
|
41
|
+
// a real JS number, and the runtime value it formats (e.g. a price, 19.5)
|
|
42
|
+
// may be fractional, which a Go `int` struct field can't hold (assigning a
|
|
43
|
+
// fractional untyped constant to it is a compile error: #2168
|
|
44
|
+
// number-tofixed). Unlike a signal's fractional LITERAL initial value
|
|
45
|
+
// (rescued by `typeInfoToGo`'s own `defaultValue` consultation — the
|
|
46
|
+
// math-methods half of the same divergence), a prop with no default has no
|
|
47
|
+
// literal to read the fraction off of; the usage of `.toFixed()` itself is
|
|
48
|
+
// the only available evidence, so it's collected by walking the JSX tree.
|
|
49
|
+
for (const propName of collectToFixedPropNames(ir.root)) {
|
|
50
|
+
const param = ir.metadata.propsParams.find(p => p.name === propName)
|
|
51
|
+
if (!param) continue
|
|
52
|
+
const resolved = overrides.get(propName) ?? typeInfoToGo(ctx, param.type, param.defaultValue)
|
|
53
|
+
if (resolved === 'int') {
|
|
54
|
+
overrides.set(propName, 'float64')
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
38
58
|
return overrides
|
|
39
59
|
}
|
|
40
60
|
|
|
61
|
+
/**
|
|
62
|
+
* Names of identifiers used as the receiver of `.toFixed(...)` anywhere in
|
|
63
|
+
* the component's JSX tree (text expressions, conditions, and attribute
|
|
64
|
+
* values). Deliberately narrow — `.toFixed()` is the one number-shape usage
|
|
65
|
+
* that needs this rescue (see `buildPropTypeOverrides` above); it isn't a
|
|
66
|
+
* general "infer number-ness from usage" walker.
|
|
67
|
+
*
|
|
68
|
+
* KNOWN LIMITATION: only a DIRECT identifier receiver in the JSX tree is
|
|
69
|
+
* caught (`{price.toFixed(2)}`). A bare `number` prop with no default is
|
|
70
|
+
* still silently typed `int` — and hits the exact same `go run` compile
|
|
71
|
+
* failure (#2168 number-tofixed) on a fractional runtime value — if the
|
|
72
|
+
* fraction only surfaces indirectly (`.toFixed()` inside a signal's
|
|
73
|
+
* initial value or a memo's computation, reached via `ir.metadata` rather
|
|
74
|
+
* than `ir.root`) or via any OTHER fraction-producing operation on the
|
|
75
|
+
* same bare prop (division, `Math.round`/`Math.floor`, etc. — none of
|
|
76
|
+
* which carry the same unambiguous "this must be a real JS number" signal
|
|
77
|
+
* `.toFixed()` does). Widening this walker to those cases is a real,
|
|
78
|
+
* currently-unaddressed gap, not a hypothetical one — flag it rather than
|
|
79
|
+
* treating a future occurrence as a fresh regression.
|
|
80
|
+
*/
|
|
81
|
+
function collectToFixedPropNames(root: IRNode): Set<string> {
|
|
82
|
+
const names = new Set<string>()
|
|
83
|
+
const checkExpr = (expr: ParsedExpr | undefined) => {
|
|
84
|
+
if (expr?.kind === 'array-method' && expr.method === 'toFixed' && expr.object.kind === 'identifier') {
|
|
85
|
+
names.add(expr.object.name)
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
const walk = (node: IRNode | null | undefined) => {
|
|
89
|
+
if (!node) return
|
|
90
|
+
if (node.type === 'expression') checkExpr(node.parsed)
|
|
91
|
+
if (node.type === 'conditional') {
|
|
92
|
+
checkExpr(node.parsedCondition)
|
|
93
|
+
walk(node.whenTrue)
|
|
94
|
+
walk(node.whenFalse)
|
|
95
|
+
}
|
|
96
|
+
if (node.type === 'element') {
|
|
97
|
+
for (const attr of node.attrs) {
|
|
98
|
+
if (attr.value.kind === 'expression') checkExpr(attr.value.parsed)
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
if ('children' in node && Array.isArray(node.children)) {
|
|
102
|
+
node.children.forEach(walk)
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
walk(root)
|
|
106
|
+
return names
|
|
107
|
+
}
|
|
108
|
+
|
|
41
109
|
/**
|
|
42
110
|
* Resolve a prop param's Go struct-field type using the SAME logic
|
|
43
111
|
* `generatePropsStruct` / `generateInputStruct` use: a `propTypeOverrides` entry
|
|
@@ -18,7 +18,11 @@ import type { GoEmitContext } from '../emit-context.ts'
|
|
|
18
18
|
/**
|
|
19
19
|
* Convert a `TypeInfo` to a Go type string.
|
|
20
20
|
*
|
|
21
|
-
* @param defaultValue used to infer the type when `typeInfo.kind` is
|
|
21
|
+
* @param defaultValue used to infer the type when `typeInfo.kind` is
|
|
22
|
+
* `unknown`, and to distinguish `int` vs `float64` when `kind` is
|
|
23
|
+
* `primitive`/`number` (#2168 math-methods/number-tofixed — a bare TS
|
|
24
|
+
* `number` blindly mapped to Go `int`, so a fractional signal initial
|
|
25
|
+
* value like `-7.6` silently truncated to the Go zero value)
|
|
22
26
|
* @returns the Go type, falling back to `interface{}` when unresolvable
|
|
23
27
|
*/
|
|
24
28
|
export function typeInfoToGo(
|
|
@@ -32,7 +36,7 @@ export function typeInfoToGo(
|
|
|
32
36
|
case 'string':
|
|
33
37
|
return 'string'
|
|
34
38
|
case 'number':
|
|
35
|
-
return 'int'
|
|
39
|
+
return defaultValue !== undefined ? numberPrimitiveGoType(defaultValue) : 'int'
|
|
36
40
|
case 'boolean':
|
|
37
41
|
return 'bool'
|
|
38
42
|
default:
|
|
@@ -99,6 +103,19 @@ export function tsTypeStringToGo(ctx: GoEmitContext, tsType: string): string {
|
|
|
99
103
|
return 'interface{}'
|
|
100
104
|
}
|
|
101
105
|
|
|
106
|
+
/**
|
|
107
|
+
* Distinguish Go `int` vs `float64` for a `number`-typed field from the
|
|
108
|
+
* literal source text of its default/initial value. Falls back to `int`
|
|
109
|
+
* when `value` isn't recognizably a bare numeric literal (e.g. a
|
|
110
|
+
* destructured default that's itself an expression, `props.initial ?? 0`)
|
|
111
|
+
* — `int` remains the blind fallback for `kind: 'primitive'`; only a
|
|
112
|
+
* literal fractional value (`-7.6`) is positive enough evidence to widen
|
|
113
|
+
* to `float64`.
|
|
114
|
+
*/
|
|
115
|
+
function numberPrimitiveGoType(value: string): string {
|
|
116
|
+
return /^-?\d+\.\d+$/.test(value) ? 'float64' : 'int'
|
|
117
|
+
}
|
|
118
|
+
|
|
102
119
|
/** Infer a Go type from a JS value literal; `interface{}` when unrecognized. */
|
|
103
120
|
export function inferTypeFromValue(value: string): string {
|
|
104
121
|
if (value === 'true' || value === 'false') return 'bool'
|