@barefootjs/xslate 0.17.0 → 0.17.1

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.
@@ -13,13 +13,10 @@ import {
13
13
  type ComponentIR,
14
14
  type ContextConsumer,
15
15
  collectContextConsumers,
16
- extractArrowBodyExpression,
17
- isSupported,
18
- parseExpression,
16
+ computeSsrSeedPlan,
19
17
  } from '@barefootjs/jsx'
20
18
 
21
19
  import type { XslateMemoContext } from '../emit-context.ts'
22
- import { referencedVarsAreAvailable } from '../lib/ir-scope.ts'
23
20
 
24
21
  /** Kolon literal for a context-consumer's `createContext` default. */
25
22
  export function contextDefaultKolon(c: ContextConsumer): string {
@@ -49,77 +46,33 @@ export function generateContextConsumerSeed(ir: ComponentIR): string {
49
46
  }
50
47
 
51
48
  /**
52
- * Seed memos whose SSR default is `null` (not statically evaluable) by
53
- * computing them in-template from the already-seeded prop / signal vars
54
- * (`createMemo(() => props.value * 10)` → `: my $x = $value * 10;`). Without
55
- * this the memo's `$x` renders empty the reason
56
- * `props-reactivity-comparison` was skipped. Only emitted when every var the
57
- * lowering references is already in scope. (#1297)
49
+ * Emit `: my $<name> = <kolon>;` line-statements for every `derived` step of
50
+ * the backend-neutral SSR seed plan the scope/availability/ordering
51
+ * analysis lives in `computeSsrSeedPlan` (packages/jsx/src/ssr-seed-plan.ts);
52
+ * this only lowers each step's expression to Kolon and applies the
53
+ * backend-specific emit guards: skip an empty lowering, skip a lowering that
54
+ * references no `$var` at all (a constant init/body — e.g. a `derived` step
55
+ * with empty `frees` — keeps the existing static ssr-defaults seed instead),
56
+ * and skip a self-referencing lowering (Kolon's `my` shadows the RHS, so
57
+ * `: my $x = … $x …` would read the just-declared undefined lexical rather
58
+ * than the render var — the plan rules out SOURCE-level self-refs, but a
59
+ * lowered canonical name could still collide, so this stays as the cheap
60
+ * defense it always was). `env-reader` and `opaque` steps emit nothing (the
61
+ * runtime supplies the reader, or the adapter's ssr-defaults path already
62
+ * covers it). (#1297, #2075)
58
63
  */
59
64
  export function generateDerivedMemoSeed(ctx: XslateMemoContext, ir: ComponentIR): string {
60
- const memos = ir.metadata.memos ?? []
61
- const signals = ir.metadata.signals ?? []
62
- if (memos.length === 0 && signals.length === 0) return ''
63
- // Props seed first; each signal/memo adds its own name as it lands.
64
- const available = new Set<string>(ir.metadata.propsParams.map(p => p.name))
65
+ // Package G attached this to metadata at compile time; the `??` fallback
66
+ // only covers hand-built metadata in older tests that predate the attached
67
+ // plan same shared function, so there's no divergence from the compiler.
68
+ const plan = ir.metadata.ssrSeedPlan ?? computeSsrSeedPlan(ir.metadata)
65
69
  const lines: string[] = []
66
-
67
- // Prop/signal-derived signals (`createSignal(props.defaultOn ?? false)`):
68
- // a loop-child render gets no stash seed, so its `$on` would render nil;
69
- // and the static default can't capture the per-call prop. Seed it
70
- // in-template when the init lowers cleanly AND references an in-scope var.
71
- // Object/array/constant inits keep the existing ssr-defaults seeding.
72
- for (const signal of signals) {
73
- const kolon = tryLowerToKolon(ctx, signal.initialValue, available)
74
- // Kolon can't express `: my $x = … $x …` — declaring `my $x` makes the
75
- // RHS `$x` an undefined lexical rather than the render var. A same-name
76
- // signal (`createSignal(props.x ?? d)`, getter == prop) is just the prop
77
- // with a default, which the harness already seeds correctly from the
78
- // passed prop — skip the in-template seed for it. (Different-name
79
- // prop-derived signals like toggle's `on` from `defaultOn` are unaffected.)
80
- const refsSelf = kolon !== null && new RegExp(`\\$${signal.getter}\\b`).test(kolon)
81
- if (kolon !== null && !refsSelf) lines.push(`: my $${signal.getter} = ${kolon};`)
82
- available.add(signal.getter)
83
- }
84
-
85
- for (const memo of memos) {
86
- // Seed every memo whose body lowers cleanly — not just the ones whose
87
- // static SSR default is null. A statically-foldable prop-derived memo
88
- // (`createMemo(() => props.disabled ?? false)` → default `false`)
89
- // still depends on the per-call prop: the static stash seed bakes in
90
- // the absent-prop fold, so a caller passing `disabled => 1` would
91
- // render the default branch (#1897, select's disabled item). The
92
- // in-template recomputation reads the prop lexical already in scope;
93
- // block-bodied arrows / out-of-scope references fall back to the
94
- // static ssr-defaults seed. Same self-reference guard as the signal
95
- // loop above — Kolon's `my` shadows the render var on the RHS.
96
- const body = extractArrowBodyExpression(memo.computation)
97
- if (body !== null) {
98
- const kolon = tryLowerToKolon(ctx, body, available)
99
- const refsSelf = kolon !== null && new RegExp(`\\$${memo.name}\\b`).test(kolon)
100
- if (kolon !== null && !refsSelf) lines.push(`: my $${memo.name} = ${kolon};`)
101
- }
102
- available.add(memo.name)
70
+ for (const step of plan.steps) {
71
+ if (step.kind !== 'derived') continue
72
+ const kolon = ctx.convertExpressionToKolon(step.expr, step.parsed)
73
+ if (kolon === '' || !/\$[A-Za-z_]\w*/.test(kolon)) continue
74
+ if (new RegExp(`\\$${step.name}\\b`).test(kolon)) continue
75
+ lines.push(`: my $${step.name} = ${kolon};`)
103
76
  }
104
77
  return lines.length > 0 ? lines.join('\n') + '\n' : ''
105
78
  }
106
-
107
- /**
108
- * Lower a signal init / memo body to Kolon for an in-template SSR seed, or
109
- * `null` when it shouldn't be seeded this way: not a supported shape
110
- * (`isSupported` pre-check, so object/array literals don't fail the build),
111
- * references no in-scope var (a constant — keep ssr-defaults seeding), or
112
- * references an out-of-scope binding. (#1297)
113
- */
114
- export function tryLowerToKolon(
115
- ctx: XslateMemoContext,
116
- expr: string,
117
- available: ReadonlySet<string>,
118
- ): string | null {
119
- const trimmed = expr.trim()
120
- if (!trimmed) return null
121
- if (!isSupported(parseExpression(trimmed)).supported) return null
122
- const kolon = ctx.convertExpressionToKolon(trimmed)
123
- if (kolon === '' || !/\$[A-Za-z_]\w*/.test(kolon)) return null
124
- return referencedVarsAreAvailable(kolon, available) ? kolon : null
125
- }
@@ -568,8 +568,15 @@ export class XslateAdapter extends BaseAdapter implements IRNodeEmitter<XslateRe
568
568
  // ===========================================================================
569
569
 
570
570
  renderLoop(loop: IRLoop): string {
571
- // Client-only loops: skip SSR rendering entirely
572
- if (loop.clientOnly) return ''
571
+ // clientOnly loops must not render items at SSR time, but must still emit
572
+ // the `loop:`/`/loop:` boundary marker pair (Hono and Go parity) so the
573
+ // client runtime's mapArray() can locate the insertion anchor when
574
+ // hydrating the array. Without the markers, mapArray() resolves
575
+ // anchor = null and appends after sibling markers (#872). The marker id
576
+ // disambiguates sibling `.map()` calls under the same parent (#1087).
577
+ if (loop.clientOnly) {
578
+ return `<: $bf.comment("loop:${loop.markerId}") | mark_raw :><: $bf.comment("/loop:${loop.markerId}") | mark_raw :>`
579
+ }
573
580
 
574
581
  // An array/object-destructure loop param (`([emoji, users]) => ...` or
575
582
  // `({ name, age }) => ...`) lowers to invalid Kolon — Kolon's `for LIST
@@ -1147,7 +1154,18 @@ export class XslateAdapter extends BaseAdapter implements IRNodeEmitter<XslateRe
1147
1154
  param: string,
1148
1155
  localVarMap: Map<string, string> = new Map(),
1149
1156
  ): string {
1150
- return emitParsedExpr(expr, new XslateFilterEmitter(param, localVarMap, n => this._isStringValueName(n)))
1157
+ return emitParsedExpr(
1158
+ expr,
1159
+ new XslateFilterEmitter(
1160
+ param,
1161
+ localVarMap,
1162
+ n => this._isStringValueName(n),
1163
+ // A nested callback method inside the predicate has no Kolon scalar
1164
+ // form — surface BF101 (#2038) instead of silently degrading it to
1165
+ // its receiver.
1166
+ (message, reason) => this._recordExprBF101(message, reason),
1167
+ ),
1168
+ )
1151
1169
  }
1152
1170
 
1153
1171
  // ===========================================================================