@barefootjs/go-template 0.33.2 → 0.33.4
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 +20 -15
- package/dist/adapter/go-template-adapter.d.ts.map +1 -1
- package/dist/adapter/index.js +53 -55
- package/dist/adapter/lib/compile-state.d.ts +1 -4
- package/dist/adapter/lib/compile-state.d.ts.map +1 -1
- package/dist/conformance-pins.d.ts.map +1 -1
- package/dist/index.js +64 -58
- package/dist/render-divergences.d.ts +10 -0
- package/dist/render-divergences.d.ts.map +1 -1
- package/dist/vite.js +132 -122
- package/package.json +5 -5
- package/src/__tests__/go-template-adapter.test.ts +9 -6
- package/src/adapter/go-template-adapter.ts +140 -63
- package/src/adapter/lib/compile-state.ts +0 -5
- package/src/conformance-pins.ts +17 -0
- package/src/render-divergences.ts +16 -2
- package/dist/adapter/lib/ir-scope.d.ts +0 -13
- package/dist/adapter/lib/ir-scope.d.ts.map +0 -1
- package/src/adapter/lib/ir-scope.ts +0 -31
|
@@ -80,7 +80,7 @@ import {
|
|
|
80
80
|
BindingScope,
|
|
81
81
|
} from '@barefootjs/jsx'
|
|
82
82
|
import { findInterpolationEnd } from '@barefootjs/jsx/scanner'
|
|
83
|
-
import { BF_REGION, escapeHtml } from '@barefootjs/shared'
|
|
83
|
+
import { BF_REGION, escapeHtml, resolveJsxChildrenProp } from '@barefootjs/shared'
|
|
84
84
|
|
|
85
85
|
import {
|
|
86
86
|
GO_IDENTIFIER,
|
|
@@ -118,7 +118,6 @@ import type {
|
|
|
118
118
|
CtorLowerEnv,
|
|
119
119
|
GoTemplateAdapterOptions,
|
|
120
120
|
} from "./lib/types.ts"
|
|
121
|
-
import { collectRootScopeNodes } from "./lib/ir-scope.ts"
|
|
122
121
|
import { GO_TEMPLATE_PRIMITIVES } from "./lib/constants.ts"
|
|
123
122
|
import { CompileState, resolveSignalParsedThroughSeedPlan } from "./lib/compile-state.ts"
|
|
124
123
|
import { hasClientInteractivity, findNestedComponents } from "./analysis/component-tree.ts"
|
|
@@ -313,15 +312,6 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
313
312
|
* everywhere except the accessor lookup itself.
|
|
314
313
|
*/
|
|
315
314
|
private scope: BindingScope = BindingScope.EMPTY
|
|
316
|
-
/**
|
|
317
|
-
* Stack of `IRLoop.depth` values (innermost last), pushed/popped around
|
|
318
|
-
* `renderChildren(loop.children)` in `renderLoop`. `renderAttributes`
|
|
319
|
-
* reads the top of this stack to derive the `key` → `data-key`/
|
|
320
|
-
* `data-key-N` suffix — the depth is IR-computed (jsx-to-ir.ts), not
|
|
321
|
-
* re-derived here; this stack only threads that value down to the
|
|
322
|
-
* loop body's root element (#2168 nested-loop-outer-binding).
|
|
323
|
-
*/
|
|
324
|
-
private loopKeyDepthStack: number[] = []
|
|
325
315
|
/**
|
|
326
316
|
* Per-loop: true when the body renders the bare range value (scalar-item
|
|
327
317
|
* inline-literal loop), so the `bf_tmpl` companion is fed `.BfLoopItem` (the
|
|
@@ -521,7 +511,6 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
521
511
|
const isRootComponent = ir.root.type === 'component'
|
|
522
512
|
const isIfStatement = ir.root.type === 'if-statement'
|
|
523
513
|
|
|
524
|
-
this.state.rootScopeNodes = collectRootScopeNodes(ir.root)
|
|
525
514
|
// Map each array memo backing a loop (`<memo>().map(...)`) to that loop's
|
|
526
515
|
// handler-filled slice field, so `<memo>().length` lowers to the slice's
|
|
527
516
|
// length. Built before rendering — `.length` can precede the loop in source.
|
|
@@ -2323,7 +2312,7 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
2323
2312
|
}
|
|
2324
2313
|
case 'jsx-children':
|
|
2325
2314
|
// The RESERVED children slot (`comp.children.length > 0 ? comp.children
|
|
2326
|
-
// :
|
|
2315
|
+
// : resolveJsxChildrenProp(...)`) is handled below via
|
|
2327
2316
|
// `child.childrenText` / `child.childrenHtml` and must not be
|
|
2328
2317
|
// re-emitted here. A JSX-valued prop under any OTHER name
|
|
2329
2318
|
// (`header={<strong>Title</strong>}`, #2168 jsx-element-prop) is a
|
|
@@ -2353,6 +2342,20 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
2353
2342
|
this.state.usesHtmlTemplate = true
|
|
2354
2343
|
emitChildField(prop.name, `template.HTML(${scopedHtml})`)
|
|
2355
2344
|
}
|
|
2345
|
+
// Else: none of the three bake attempts produced a static
|
|
2346
|
+
// Go string — the value contains a template action that
|
|
2347
|
+
// survived (#2746) or is otherwise genuinely dynamic
|
|
2348
|
+
// (#2703). No field is emitted here; `queueDynamicPropDefine`
|
|
2349
|
+
// (called from `renderComponent`'s static-call-site branch)
|
|
2350
|
+
// detects the SAME unbakeable shape and delivers it at
|
|
2351
|
+
// template-execution time via `bf_with_props` + `bf_tmpl`,
|
|
2352
|
+
// mirroring how the reserved `children` field's null case
|
|
2353
|
+
// just above is filled in by `bf_with_children`. A
|
|
2354
|
+
// component nested in a loop row never reaches this
|
|
2355
|
+
// function (`collectStaticChildInstancesRecursive` only
|
|
2356
|
+
// collects `!inLoop` components), so there is no dynamic
|
|
2357
|
+
// named-prop delivery for that shape yet — out of scope
|
|
2358
|
+
// here, tracked separately if it turns up.
|
|
2356
2359
|
}
|
|
2357
2360
|
}
|
|
2358
2361
|
}
|
|
@@ -3173,17 +3176,6 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
3173
3176
|
return out
|
|
3174
3177
|
}
|
|
3175
3178
|
|
|
3176
|
-
/**
|
|
3177
|
-
* Pull the IR nodes out of a `children={<…/>}` attribute (a `jsx-children`
|
|
3178
|
-
* prop value). Empty when the component takes no such prop.
|
|
3179
|
-
*/
|
|
3180
|
-
private jsxChildrenPropNodes(props: IRProp[]): IRNode[] {
|
|
3181
|
-
for (const p of props) {
|
|
3182
|
-
if (p.value.kind === 'jsx-children') return p.value.children
|
|
3183
|
-
}
|
|
3184
|
-
return []
|
|
3185
|
-
}
|
|
3186
|
-
|
|
3187
3179
|
private extractHtmlChildren(children: IRNode[]): string | null {
|
|
3188
3180
|
if (children.length === 0) return null
|
|
3189
3181
|
if (children.every(c => c.type === 'text')) return null
|
|
@@ -3212,7 +3204,8 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
3212
3204
|
*
|
|
3213
3205
|
* `ctx.isRoot` leaks into a JSX-valued prop's subtree, so such a fragment
|
|
3214
3206
|
* is already tagged `needsScopeComment` as if it were a component root —
|
|
3215
|
-
* which is why `
|
|
3207
|
+
* which is why a relay `element.keyAttr` (#2753; `carriesDataKey` before
|
|
3208
|
+
* it) reaches this path at all.
|
|
3216
3209
|
*/
|
|
3217
3210
|
private bakingStaticChildren = false
|
|
3218
3211
|
|
|
@@ -3274,7 +3267,7 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
3274
3267
|
// paths below see them.
|
|
3275
3268
|
const effectiveChildren = comp.children.length > 0
|
|
3276
3269
|
? comp.children
|
|
3277
|
-
:
|
|
3270
|
+
: resolveJsxChildrenProp(comp.props)
|
|
3278
3271
|
result.push({
|
|
3279
3272
|
name: comp.name,
|
|
3280
3273
|
slotId: comp.slotId,
|
|
@@ -4140,21 +4133,34 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
4140
4133
|
if (element.needsScope) {
|
|
4141
4134
|
hydrationAttrs += ` ${this.renderScopeMarker('.ScopeID')}`
|
|
4142
4135
|
}
|
|
4143
|
-
//
|
|
4144
|
-
//
|
|
4145
|
-
//
|
|
4146
|
-
//
|
|
4147
|
-
//
|
|
4148
|
-
//
|
|
4149
|
-
//
|
|
4150
|
-
|
|
4151
|
-
|
|
4152
|
-
|
|
4153
|
-
|
|
4154
|
-
|
|
4155
|
-
|
|
4156
|
-
|
|
4157
|
-
|
|
4136
|
+
// #2753: `element.keyAttr` is the ONE IR-resolved decision for this
|
|
4137
|
+
// element's row-key attribute — replacing both the `carriesDataKey`
|
|
4138
|
+
// boolean and this adapter's own `rootScopeNodes`/`needsScope` check
|
|
4139
|
+
// (mechanism 2: a render-root relay, including the #2732 fragment-root
|
|
4140
|
+
// case) and the separate `loopKeyDepthStack`-driven rewrite of a literal
|
|
4141
|
+
// `key` attribute this adapter used to do in `renderAttributes`
|
|
4142
|
+
// (mechanism 1: a `.map()` row root compiled inline here).
|
|
4143
|
+
if (element.keyAttr) {
|
|
4144
|
+
if (element.keyAttr.value !== undefined) {
|
|
4145
|
+
const lowered = emitAttrValue(
|
|
4146
|
+
{ kind: 'expression', expr: element.keyAttr.value },
|
|
4147
|
+
this.elementAttrEmitter,
|
|
4148
|
+
element.keyAttr.name,
|
|
4149
|
+
)
|
|
4150
|
+
if (lowered) hydrationAttrs += ` ${lowered}`
|
|
4151
|
+
} else if (!this.bakingStaticChildren) {
|
|
4152
|
+
// Relay: this element is one of THIS component's own render roots,
|
|
4153
|
+
// carrying whatever key its OWN caller supplies at runtime (the
|
|
4154
|
+
// parent's loop init stamped `.BfDataKey`), when this component is
|
|
4155
|
+
// itself invoked as a caller's keyed loop row. Applies to
|
|
4156
|
+
// early-return (if-statement) roots too, where every branch's top
|
|
4157
|
+
// element qualifies, and to #2732's fragment-root case.
|
|
4158
|
+
// `bakingStaticChildren` (see its docstring) excludes a hoisted
|
|
4159
|
+
// children value baked into a static Go string at compile time —
|
|
4160
|
+
// by construction not a loop row, and a surviving `{{if}}` action
|
|
4161
|
+
// there gets silently dropped by the caller rather than rendered.
|
|
4162
|
+
hydrationAttrs += `{{if .BfDataKey}} data-key="{{.BfDataKey}}"{{end}}`
|
|
4163
|
+
}
|
|
4158
4164
|
}
|
|
4159
4165
|
if (element.slotId) {
|
|
4160
4166
|
hydrationAttrs += ` ${this.renderSlotMarker(element.slotId)}`
|
|
@@ -7170,7 +7176,6 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
7170
7176
|
this.scalarLiteralLoopGoType(loop.arrayParsed, loop.itemType) !== null,
|
|
7171
7177
|
)
|
|
7172
7178
|
this.loopWrapperStack.push(!!loop.childComponent)
|
|
7173
|
-
this.loopKeyDepthStack.push(loop.depth)
|
|
7174
7179
|
// Rendered inside the pushed loop scope so each initializer resolves
|
|
7175
7180
|
// against the range item (`.Done`), in source order so a later
|
|
7176
7181
|
// initializer sees an earlier `$` variable — same as the source block.
|
|
@@ -7178,7 +7183,6 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
7178
7183
|
.map(d => `{{$${d.name} := ${this.renderParsedExpr(d.valueParsed)}}}`)
|
|
7179
7184
|
.join('')
|
|
7180
7185
|
const children = this.renderChildren(loop.children)
|
|
7181
|
-
this.loopKeyDepthStack.pop()
|
|
7182
7186
|
this.loopWrapperStack.pop()
|
|
7183
7187
|
this.loopScalarItemStack.pop()
|
|
7184
7188
|
// Build the per-item anchor marker while the row scope is still active,
|
|
@@ -7270,7 +7274,6 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
7270
7274
|
const wasInLoopOuter = this.inLoop
|
|
7271
7275
|
this.inLoop = true
|
|
7272
7276
|
this.loopWrapperStack.push(false)
|
|
7273
|
-
this.loopKeyDepthStack.push(loop.depth)
|
|
7274
7277
|
this.loopScalarItemStack.push(this.scalarLiteralLoopGoType(loop.arrayParsed, loop.itemType) !== null)
|
|
7275
7278
|
// The bake gate (`analyzeBakeableStaticElementLoop`) already refused a
|
|
7276
7279
|
// destructured or `.keys()`-shape param, so `loop` binds via `enterLoopRow`
|
|
@@ -7303,7 +7306,6 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
7303
7306
|
|
|
7304
7307
|
this.scope = prevScope
|
|
7305
7308
|
this.loopScalarItemStack.pop()
|
|
7306
|
-
this.loopKeyDepthStack.pop()
|
|
7307
7309
|
this.loopWrapperStack.pop()
|
|
7308
7310
|
this.inLoop = wasInLoopOuter
|
|
7309
7311
|
|
|
@@ -7317,7 +7319,10 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
7317
7319
|
* `mapArrayAnchored` can hydrate items that render no element.
|
|
7318
7320
|
*/
|
|
7319
7321
|
private loopItemMarker(loop: { bodyIsMultiRoot?: boolean; bodyIsItemConditional?: boolean; key?: string | null }): string {
|
|
7320
|
-
|
|
7322
|
+
// `bfComment` prepends `bf-`, so the argument is `"loop-i"`, not
|
|
7323
|
+
// `"bf-loop-i"` (which would double the prefix to `<!--bf-bf-loop-i-->`
|
|
7324
|
+
// — the same latent bug #2763's fixture caught on the Hono adapter).
|
|
7325
|
+
if (loop.bodyIsMultiRoot) return `{{bfComment "loop-i"}}`
|
|
7321
7326
|
if (loop.bodyIsItemConditional && loop.key) {
|
|
7322
7327
|
// `bfComment` prepends `bf-`, so `printf "loop-i:%v"` yields
|
|
7323
7328
|
// `<!--bf-loop-i:KEY-->`. The key expression resolves against the current
|
|
@@ -7339,7 +7344,7 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
7339
7344
|
private queueDynamicChildrenDefine(comp: IRComponent): string | null {
|
|
7340
7345
|
const effectiveChildren = comp.children.length > 0
|
|
7341
7346
|
? comp.children
|
|
7342
|
-
:
|
|
7347
|
+
: resolveJsxChildrenProp(comp.props)
|
|
7343
7348
|
if (effectiveChildren.length === 0) return null
|
|
7344
7349
|
if (this.extractTextChildren(effectiveChildren) !== null) return null
|
|
7345
7350
|
if (this.extractHtmlChildren(effectiveChildren) !== null) return null
|
|
@@ -7354,6 +7359,72 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
7354
7359
|
return name
|
|
7355
7360
|
}
|
|
7356
7361
|
|
|
7362
|
+
/**
|
|
7363
|
+
* #2703/B2: extend `queueDynamicChildrenDefine`'s companion-define delivery
|
|
7364
|
+
* from the reserved `children` slot to NAMED jsx-children props (any
|
|
7365
|
+
* JSX-valued prop other than `children`, e.g. `header={<strong>Title</strong>}`).
|
|
7366
|
+
* A named prop whose value can't be baked into a static Go string by
|
|
7367
|
+
* `emitStaticChildInstances`'s bake chain (`extractTextChildren` /
|
|
7368
|
+
* `extractHtmlChildren` / `extractScopedHtmlChildren`, all null) is queued
|
|
7369
|
+
* here instead, the same way; the caller composes the result into
|
|
7370
|
+
* `bf_with_props` + `bf_tmpl` at the STATIC (non-loop) call site — the one
|
|
7371
|
+
* shape `emitStaticChildInstances` bakes for at all (a component nested in
|
|
7372
|
+
* a loop row never reaches `collectStaticChildInstances`).
|
|
7373
|
+
*
|
|
7374
|
+
* Returns the flat `"FieldName" (bf_tmpl "name" .) ...` argument list ready
|
|
7375
|
+
* to splice into `bf_with_props`, or null when no named prop needs it —
|
|
7376
|
+
* batches every prop that needs dynamic delivery into ONE `bf_with_props`
|
|
7377
|
+
* call rather than nesting one per prop.
|
|
7378
|
+
*/
|
|
7379
|
+
private queueDynamicPropDefine(comp: IRComponent): string | null {
|
|
7380
|
+
const args: string[] = []
|
|
7381
|
+
const childShape = this.childComponentShapes.get(comp.name)
|
|
7382
|
+
for (const prop of comp.props) {
|
|
7383
|
+
if (prop.value.kind !== 'jsx-children' || prop.name === 'children') continue
|
|
7384
|
+
const children = prop.value.children
|
|
7385
|
+
if (this.extractTextChildren(children) !== null) continue
|
|
7386
|
+
if (this.extractHtmlChildren(children) !== null) continue
|
|
7387
|
+
if (this.extractScopedHtmlChildren(children) !== null) continue
|
|
7388
|
+
// A prop routed into the child's rest bag (no declared param —
|
|
7389
|
+
// `loopRowChildPropOverrides`'s identical guard, above) has no named
|
|
7390
|
+
// Go field for `bf_with_props`/`WithProps` to target at all; `WithProps`
|
|
7391
|
+
// silently no-ops on an unmatched field name. Refuse loudly instead of
|
|
7392
|
+
// delivering the value against a field name that can never land —
|
|
7393
|
+
// this shape had no dynamic-delivery route before this method existed
|
|
7394
|
+
// either (the blanket BF101 this method's caller replaced was
|
|
7395
|
+
// unconditional for any unbakeable named prop), so this keeps it loud
|
|
7396
|
+
// rather than trading that refusal for a silent dropped render.
|
|
7397
|
+
if (childShape?.restBagField && !childShape.paramNames.has(prop.name)) {
|
|
7398
|
+
this.state.errors.push({
|
|
7399
|
+
code: 'BF101',
|
|
7400
|
+
severity: 'error',
|
|
7401
|
+
message: `JSX-valued prop '${prop.name}' on <${comp.name}> is dynamic and routes into the child's rest-bag prop ('${childShape.restBagField}') rather than a declared field — there is no named Go struct field for 'bf_with_props' to target`,
|
|
7402
|
+
loc: prop.loc,
|
|
7403
|
+
})
|
|
7404
|
+
continue
|
|
7405
|
+
}
|
|
7406
|
+
const name = `${this.state.componentName}__prop_${prop.name}_${comp.slotId}`
|
|
7407
|
+
if (!this.state.pendingChildrenDefines.some(d => d.name === name)) {
|
|
7408
|
+
this.state.pendingChildrenDefines.push({
|
|
7409
|
+
name,
|
|
7410
|
+
content: this.renderChildren(children),
|
|
7411
|
+
})
|
|
7412
|
+
}
|
|
7413
|
+
// `bf_with_props`/`WithProps` patches the child's Props struct, keyed
|
|
7414
|
+
// by the child's own LOCAL destructured field name — not necessarily
|
|
7415
|
+
// the caller's JSX attribute name (`function Card({ header: h })`
|
|
7416
|
+
// reads `h`, whose field is `H`, not `Header`). `WithProps` silently
|
|
7417
|
+
// no-ops on an unmatched field name, so skipping this lookup would
|
|
7418
|
+
// turn an aliased prop's dynamic delivery into a silent dropped
|
|
7419
|
+
// render. `childPropFieldNames` resolves this exact hazard for the
|
|
7420
|
+
// sibling `bf_with_props` call site (`loopRowChildPropOverrides`,
|
|
7421
|
+
// below) — mirrored here.
|
|
7422
|
+
const fieldName = this.childPropFieldNames.get(comp.name)?.get(prop.name) ?? capitalizeFieldName(prop.name)
|
|
7423
|
+
args.push(`${JSON.stringify(fieldName)} (bf_tmpl ${JSON.stringify(name)} .)`)
|
|
7424
|
+
}
|
|
7425
|
+
return args.length > 0 ? args.join(' ') : null
|
|
7426
|
+
}
|
|
7427
|
+
|
|
7357
7428
|
/**
|
|
7358
7429
|
* Queue a companion define for a loop body component's JSX children. Like
|
|
7359
7430
|
* `queueDynamicChildrenDefine` but temporarily exits the `inLoop` context so
|
|
@@ -7366,7 +7437,7 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
7366
7437
|
private queueLoopBodyChildrenDefine(comp: IRComponent): string | null {
|
|
7367
7438
|
const effectiveChildren = comp.children.length > 0
|
|
7368
7439
|
? comp.children
|
|
7369
|
-
:
|
|
7440
|
+
: resolveJsxChildrenProp(comp.props)
|
|
7370
7441
|
if (effectiveChildren.length === 0) return null
|
|
7371
7442
|
if (this.extractTextChildren(effectiveChildren) !== null) return null
|
|
7372
7443
|
if (this.extractHtmlChildren(effectiveChildren) !== null) return null
|
|
@@ -7459,9 +7530,19 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
7459
7530
|
// Static children with slotId: unique field name based on slotId.
|
|
7460
7531
|
const suffix = slotIdToFieldSuffix(comp.slotId)
|
|
7461
7532
|
const childrenDefine = this.queueDynamicChildrenDefine(comp)
|
|
7533
|
+
// #2703/B2: a NAMED jsx-children prop (any JSX-valued prop other than
|
|
7534
|
+
// `children`) that couldn't be baked into the constructor gets the
|
|
7535
|
+
// same treatment `children` already has — delivered at the call site
|
|
7536
|
+
// via a companion define. Props helper stays INNER, same ordering
|
|
7537
|
+
// rule as the loop-nested branch above: `bf_with_children` applies
|
|
7538
|
+
// last, on the props-patched value.
|
|
7539
|
+
const propArgs = this.queueDynamicPropDefine(comp)
|
|
7540
|
+
const base = propArgs
|
|
7541
|
+
? `(bf_with_props .${comp.name}${suffix} ${propArgs})`
|
|
7542
|
+
: `.${comp.name}${suffix}`
|
|
7462
7543
|
templateCall = childrenDefine
|
|
7463
|
-
? `{{template "${comp.name}" (bf_with_children
|
|
7464
|
-
: `{{template "${comp.name}"
|
|
7544
|
+
? `{{template "${comp.name}" (bf_with_children ${base} (bf_tmpl "${childrenDefine}" .))}}`
|
|
7545
|
+
: `{{template "${comp.name}" ${base}}}`
|
|
7465
7546
|
} else {
|
|
7466
7547
|
// Static children without slotId: fall back to .ComponentName.
|
|
7467
7548
|
templateCall = `{{template "${comp.name}" .${comp.name}}}`
|
|
@@ -7749,19 +7830,15 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
7749
7830
|
// literal never reaches the generic object-literal BF101 refusal
|
|
7750
7831
|
// (which would double-report alongside the purpose-built one).
|
|
7751
7832
|
if (isDangerousInnerHtmlAttr(attr)) continue
|
|
7752
|
-
//
|
|
7753
|
-
//
|
|
7754
|
-
//
|
|
7755
|
-
//
|
|
7756
|
-
//
|
|
7757
|
-
//
|
|
7758
|
-
|
|
7759
|
-
|
|
7760
|
-
|
|
7761
|
-
const depth = this.loopKeyDepthStack.at(-1) ?? 0
|
|
7762
|
-
attrName = depth > 0 ? `data-key-${depth}` : 'data-key'
|
|
7763
|
-
}
|
|
7764
|
-
else attrName = attr.name
|
|
7833
|
+
// `key` never renders as a literal HTML attribute — #2753 resolves it
|
|
7834
|
+
// onto `element.keyAttr` (name AND value) at IR-build time, emitted
|
|
7835
|
+
// separately in `renderElement`. A stray `key` that jsx-to-ir.ts did
|
|
7836
|
+
// NOT recognize as a loop row root (so `keyAttr` was never set) has no
|
|
7837
|
+
// defined rendering outside a `.map()` anyway; drop it rather than
|
|
7838
|
+
// leak an invalid `key="..."` HTML attribute.
|
|
7839
|
+
if (attr.name === 'key') continue
|
|
7840
|
+
// Rewrite JSX special-prop names to their HTML-attribute counterparts.
|
|
7841
|
+
const attrName = attr.name === 'className' ? 'class' : attr.name
|
|
7765
7842
|
const lowered = emitAttrValue(attr.value, this.elementAttrEmitter, attrName)
|
|
7766
7843
|
if (lowered) parts.push(lowered)
|
|
7767
7844
|
}
|
|
@@ -19,7 +19,6 @@ import type {
|
|
|
19
19
|
ContextConsumer,
|
|
20
20
|
EnvSignalReader,
|
|
21
21
|
IRMetadata,
|
|
22
|
-
IRNode,
|
|
23
22
|
LoweringMatcher,
|
|
24
23
|
MemoInfo,
|
|
25
24
|
ParsedExpr,
|
|
@@ -205,10 +204,6 @@ export class CompileState {
|
|
|
205
204
|
*/
|
|
206
205
|
stringValueNames: Set<string> = new Set()
|
|
207
206
|
|
|
208
|
-
/** Component root scope element(s) — each carries `data-key` for a keyed loop
|
|
209
|
-
* item. */
|
|
210
|
-
rootScopeNodes: Set<IRNode> = new Set()
|
|
211
|
-
|
|
212
207
|
/** Array-memo name → the handler-filled loop slice field its `.map()` feeds
|
|
213
208
|
* (e.g. `visible` → `PostListItems`). Lets `<memo>().length` lower to the
|
|
214
209
|
* slice's length instead of a nil/unset memo field. */
|
package/src/conformance-pins.ts
CHANGED
|
@@ -59,4 +59,21 @@ export const conformancePins: ConformancePins = {
|
|
|
59
59
|
// `rich-prop-client-read` above.
|
|
60
60
|
'jsx-element-prop-ternary': [{ code: 'BF021', severity: 'error', issue: 'https://github.com/piconic-ai/barefootjs/issues/2667' }],
|
|
61
61
|
'jsx-element-prop-array': [{ code: 'BF021', severity: 'error', issue: 'https://github.com/piconic-ai/barefootjs/issues/2667' }],
|
|
62
|
+
// `jsx-element-prop-fragment-conditional` (#2703) is NOT pinned here — it
|
|
63
|
+
// renders correctly since `queueDynamicPropDefine` (go-template-adapter.ts)
|
|
64
|
+
// extended the reserved `children` slot's `bf_with_children`/`bf_tmpl`
|
|
65
|
+
// dynamic-delivery route to named jsx-children props.
|
|
66
|
+
// #2805: a named jsx-children prop routed into the child's rest bag (no
|
|
67
|
+
// declared param — only reachable via the child's `...rest` spread) has
|
|
68
|
+
// no named Go struct field for `bf_with_props`/`WithProps` to target at
|
|
69
|
+
// all. `queueDynamicPropDefine` refuses loudly with BF101 rather than
|
|
70
|
+
// silently no-op through `WithProps`'s unmatched-field passthrough.
|
|
71
|
+
// `unescapable`: the capability gap (a rest-bag delivery route) is #2805
|
|
72
|
+
// itself, not authored yet, so no `/* @client */` escape twin exists.
|
|
73
|
+
'jsx-element-prop-rest-bag-dynamic': [{
|
|
74
|
+
code: 'BF101',
|
|
75
|
+
severity: 'error',
|
|
76
|
+
issue: 'https://github.com/piconic-ai/barefootjs/issues/2805',
|
|
77
|
+
unescapable: { issue: 'https://github.com/piconic-ai/barefootjs/issues/2805' },
|
|
78
|
+
}],
|
|
62
79
|
}
|
|
@@ -13,13 +13,27 @@
|
|
|
13
13
|
* the same way it already seeded a signal-backed dynamic one: the adapter's
|
|
14
14
|
* own `emission` was never the bug, only this harness's route-handler
|
|
15
15
|
* stand-in was missing the prop-derived case.)
|
|
16
|
+
*
|
|
17
|
+
* (#2703's `jsx-element-prop-fragment-conditional` divergence graduated by
|
|
18
|
+
* reclassification, not a lowering fix: the underlying gap — a named
|
|
19
|
+
* jsx-children prop whose value can't be baked into a static Go string
|
|
20
|
+
* silently got no field at all, no diagnostic — is now a loud `BF101`
|
|
21
|
+
* refusal (see `conformance-pins.ts`) instead of a silent wrong render.
|
|
22
|
+
* "Compiles clean but renders divergent" no longer describes this fixture on
|
|
23
|
+
* Go, so it moved off this table. Dynamic delivery for named jsx-children
|
|
24
|
+
* props (the actual capability gap) is tracked separately at
|
|
25
|
+
* https://github.com/piconic-ai/barefootjs/issues/2703.)
|
|
16
26
|
*/
|
|
17
27
|
|
|
18
28
|
import type { RenderDivergences } from '@barefootjs/jsx'
|
|
19
29
|
|
|
20
30
|
export const renderDivergences: RenderDivergences = {
|
|
21
|
-
'
|
|
22
|
-
'
|
|
31
|
+
'children-passthrough-renamed':
|
|
32
|
+
'A `children` prop destructured under a different name (`const { children: kids } = props`) does not reach the SSR template on Go, tracked as https://github.com/piconic-ai/barefootjs/issues/2788. The same fixture also fails on Mojolicious, where the mechanism IS isolated: the `.html.ep` interpolates the LOCAL alias (`$kids`) while the stash defines only the caller-facing `children`, so the Perl render dies inside `Mojo::Template::process`. Go\'s own failure mode has NOT been read — `go` is not reachable from the local test process (the conformance case prints "go command not found" and skips), so this entry is declared from the CI failure on #2787 alone, not from a local reproduction. Whoever graduates this should read Go\'s actual output first rather than assume it shares Mojo\'s mechanism. Same alias family as `aliased-destructured-prop` (`{ n: count }`), whose Go half graduated in #2525 — worth checking whether the reserved `children` slot bypasses that fix or never had it. `children-passthrough-renamed` asserts the CORRECT (Hono-generated) output, so deleting this entry is the graduation.',
|
|
23
33
|
'signal-object-spread-init':
|
|
24
34
|
'PRE-EXISTING, unrelated to the #2696 Step 2 spread work this fixture pins: a `derived`-classified signal/memo whose value is an OBJECT literal has no live-template-expression lowering on Go — unlike the other six template-stash backends (e.g. minijinja emits `{% set merged = dict(base, done=true) %}`), Go always bakes an object-typed signal/memo field into Go SOURCE at `NewXxxProps` constructor time (`convertInitialValue`/`parsedLiteralToGo`), and that baker is STATIC-only (identifier/call/member operands defer, `parsed-literal-to-go.ts`\'s own docstring) — it cannot reference a live prop at all. Reproduced identically with the spread REMOVED (`createSignal({ id: base.id, done: true })`), confirming the gap predates and is independent of spread: the signal seeds `nil` and every field read (`.Merged.ID`/`.Merged.Done`) reads the Go zero value regardless of `initialTodos`. Graduate by teaching the baker to emit prop-referencing Go expressions (https://github.com/piconic-ai/barefootjs/issues/2700).',
|
|
35
|
+
'textarea-row-breakout':
|
|
36
|
+
'A signal seeded from a bare identifier referencing a MODULE-LEVEL const (`const PAYLOAD = \'...\'; createSignal(PAYLOAD)`) bakes to `nil` in the generated `New<Component>Props` constructor instead of the const\'s literal value: `convertInitialValue` (`value-lowering.ts`) only resolves a direct prop reference or a literal expression for a bare identifier, and the analyzer types this signal `{ kind: \'unknown\' }`, so every typed branch falls through to the final `nil` fallback. `resolveModuleStringConst` exists on the adapter for exactly this resolution (used by `template-interp.ts` for live template expressions) but isn\'t wired into this signal-baking path. Unrelated to what this fixture exists to cover (#2765\'s loop-row textarea-escaping fix, verified correct here) — every other adapter renders the fixture\'s controlled `<textarea>` correctly. Tracked at https://github.com/piconic-ai/barefootjs/issues/2794; graduate by wiring `resolveModuleStringConst` into `convertInitialValue`\'s bare-identifier case.',
|
|
37
|
+
'nested-loop-ref-const':
|
|
38
|
+
'A signal-backed object array whose elements have a NESTED array-of-objects field (`children: [{...}]`, producing this fixture\'s depth-2 `.map()`) bakes to `nil` in `New<Component>Props`, leaving the whole `{{range .Items}}` body empty on real Go — `synthesizeStructFromSignal` (`go-template-adapter.ts`) only synthesizes a struct when EVERY property value is a scalar literal (`scalarParsedGoType`), so a `children` array property aborts synthesis for the entire signal; `parsedLiteralToGo` then has no named struct to bake an object-literal element against and falls through to `nil`. Reproduced identically with #2750\'s fix reverted, confirming this predates and is independent of #2750 (which only touches client-JS reachability, never SSR baking) — every other adapter (confirmed: Hono) renders the nested loop correctly. Tracked at https://github.com/piconic-ai/barefootjs/issues/2800; graduate by teaching `synthesizeStructFromSignal` to recursively synthesize a nested struct for an array-valued property instead of bailing to `null`.',
|
|
25
39
|
}
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* IR traversal helpers for the Go html/template adapter. Pure functions over
|
|
3
|
-
* the IR tree — no adapter instance state.
|
|
4
|
-
*/
|
|
5
|
-
import type { IRNode } from '@barefootjs/jsx';
|
|
6
|
-
/**
|
|
7
|
-
* Collect the component's root scope element node(s) — the elements that become
|
|
8
|
-
* the rendered root and so carry `data-key` for a keyed loop item. A plain
|
|
9
|
-
* element root is itself; an `if-statement` (early-return) root contributes the
|
|
10
|
-
* top element of each branch, since exactly one renders at runtime.
|
|
11
|
-
*/
|
|
12
|
-
export declare function collectRootScopeNodes(node: IRNode): Set<IRNode>;
|
|
13
|
-
//# sourceMappingURL=ir-scope.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"ir-scope.d.ts","sourceRoot":"","sources":["../../../src/adapter/lib/ir-scope.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,MAAM,EAA6B,MAAM,iBAAiB,CAAA;AAExE;;;;;GAKG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,GAAG,GAAG,CAAC,MAAM,CAAC,CAiB/D"}
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* IR traversal helpers for the Go html/template adapter. Pure functions over
|
|
3
|
-
* the IR tree — no adapter instance state.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import type { IRNode, IRIfStatement, IRFragment } from '@barefootjs/jsx'
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Collect the component's root scope element node(s) — the elements that become
|
|
10
|
-
* the rendered root and so carry `data-key` for a keyed loop item. A plain
|
|
11
|
-
* element root is itself; an `if-statement` (early-return) root contributes the
|
|
12
|
-
* top element of each branch, since exactly one renders at runtime.
|
|
13
|
-
*/
|
|
14
|
-
export function collectRootScopeNodes(node: IRNode): Set<IRNode> {
|
|
15
|
-
const out = new Set<IRNode>()
|
|
16
|
-
const visit = (n: IRNode | null): void => {
|
|
17
|
-
if (!n) return
|
|
18
|
-
if (n.type === 'element') { out.add(n); return }
|
|
19
|
-
if (n.type === 'if-statement') {
|
|
20
|
-
const s = n as IRIfStatement
|
|
21
|
-
visit(s.consequent)
|
|
22
|
-
visit(s.alternate)
|
|
23
|
-
return
|
|
24
|
-
}
|
|
25
|
-
if (n.type === 'fragment') {
|
|
26
|
-
for (const c of (n as IRFragment).children) visit(c)
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
visit(node)
|
|
30
|
-
return out
|
|
31
|
-
}
|