@barefootjs/xslate 0.9.1 → 0.9.3

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.
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Text::Xslate (Kolon) Template Adapter Exports
3
3
  */
4
- export { XslateAdapter, xslateAdapter } from './xslate-adapter';
5
- export type { XslateAdapterOptions } from './xslate-adapter';
4
+ export { XslateAdapter, xslateAdapter } from './xslate-adapter.ts';
5
+ export type { XslateAdapterOptions } from './xslate-adapter.ts';
6
6
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/adapter/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAA;AAC/D,YAAY,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/adapter/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAA;AAClE,YAAY,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAA"}
@@ -187299,7 +187299,10 @@ import {
187299
187299
  emitAttrValue,
187300
187300
  augmentInheritedPropAccesses,
187301
187301
  parseRecordIndexAccess,
187302
- evalStringArrayJoin
187302
+ evalStringArrayJoin,
187303
+ extractArrowBodyExpression,
187304
+ collectContextConsumers,
187305
+ extractSsrDefaults
187303
187306
  } from "@barefootjs/jsx";
187304
187307
 
187305
187308
  // src/adapter/boolean-result.ts
@@ -187379,6 +187382,36 @@ function resolveJsxChildrenProp(props) {
187379
187382
  return [];
187380
187383
  return prop.value.children;
187381
187384
  }
187385
+ function collectRootScopeNodes(node) {
187386
+ const out = new Set;
187387
+ const visit = (n) => {
187388
+ if (!n)
187389
+ return;
187390
+ if (n.type === "element") {
187391
+ out.add(n);
187392
+ return;
187393
+ }
187394
+ if (n.type === "if-statement") {
187395
+ const s = n;
187396
+ visit(s.consequent);
187397
+ visit(s.alternate);
187398
+ return;
187399
+ }
187400
+ if (n.type === "fragment") {
187401
+ for (const c of n.children)
187402
+ visit(c);
187403
+ }
187404
+ };
187405
+ visit(node);
187406
+ return out;
187407
+ }
187408
+ function referencedVarsAreAvailable(expr, available) {
187409
+ for (const m of expr.matchAll(/\$([A-Za-z_]\w*)/g)) {
187410
+ if (!available.has(m[1]))
187411
+ return false;
187412
+ }
187413
+ return true;
187414
+ }
187382
187415
 
187383
187416
  class XslateAdapter extends BaseAdapter {
187384
187417
  name = "xslate";
@@ -187387,6 +187420,7 @@ class XslateAdapter extends BaseAdapter {
187387
187420
  importMapInjection = "html-snippet";
187388
187421
  templatePrimitives = XSLATE_PRIMITIVE_EMIT_MAP;
187389
187422
  componentName = "";
187423
+ rootScopeNodes = new Set;
187390
187424
  options;
187391
187425
  errors = [];
187392
187426
  inLoop = false;
@@ -187426,9 +187460,12 @@ class XslateAdapter extends BaseAdapter {
187426
187460
  if (!options?.siblingTemplatesRegistered) {
187427
187461
  this.checkImportedLoopChildComponents(ir);
187428
187462
  }
187463
+ this.rootScopeNodes = collectRootScopeNodes(ir.root);
187429
187464
  const templateBody = ir.root.type === "if-statement" ? this.renderIfStatement(ir.root) : this.renderNode(ir.root);
187430
187465
  const scriptReg = options?.skipScriptRegistration ? "" : this.generateScriptRegistrations(ir, options?.scriptBaseName);
187431
- const template = `${scriptReg}${templateBody}
187466
+ const ctxSeed = this.generateContextConsumerSeed(ir);
187467
+ const memoSeed = this.generateDerivedMemoSeed(ir);
187468
+ const template = `${scriptReg}${ctxSeed}${memoSeed}${templateBody}
187432
187469
  `;
187433
187470
  if (this.errors.length > 0) {
187434
187471
  ir.errors.push(...this.errors);
@@ -187493,7 +187530,84 @@ class XslateAdapter extends BaseAdapter {
187493
187530
  return this.renderIfStatement(node);
187494
187531
  }
187495
187532
  emitProvider(node, _ctx, _emit) {
187496
- return this.renderChildren(node.children);
187533
+ const value = this.providerValueKolon(node.valueProp);
187534
+ const children = this.renderChildren(node.children);
187535
+ const name = node.contextName;
187536
+ return `<: $bf.provide_context('${name}', ${value}) :>` + children + `<: $bf.revoke_context('${name}') :>`;
187537
+ }
187538
+ providerValueKolon(valueProp) {
187539
+ const v = valueProp.value;
187540
+ if (v.kind === "literal") {
187541
+ return typeof v.value === "string" ? `'${v.value.replace(/[\\']/g, (m) => `\\${m}`)}'` : String(v.value);
187542
+ }
187543
+ if (v.kind === "expression")
187544
+ return this.convertExpressionToKolon(v.expr);
187545
+ if (v.kind === "template")
187546
+ return this.convertTemplateLiteralPartsToKolon(v.parts);
187547
+ return "nil";
187548
+ }
187549
+ contextDefaultKolon(c) {
187550
+ const d = c.defaultValue;
187551
+ if (d === null || d === undefined)
187552
+ return "nil";
187553
+ if (typeof d === "string")
187554
+ return `'${d.replace(/[\\']/g, (m) => `\\${m}`)}'`;
187555
+ if (typeof d === "boolean")
187556
+ return d ? "1" : "0";
187557
+ return String(d);
187558
+ }
187559
+ generateContextConsumerSeed(ir) {
187560
+ const consumers = collectContextConsumers(ir.metadata);
187561
+ if (consumers.length === 0)
187562
+ return "";
187563
+ return consumers.map((c) => `: my $${c.localName} = $bf.use_context('${c.contextName}', ${this.contextDefaultKolon(c)});`).join(`
187564
+ `) + `
187565
+ `;
187566
+ }
187567
+ generateDerivedMemoSeed(ir) {
187568
+ const memos = ir.metadata.memos ?? [];
187569
+ const signals = ir.metadata.signals ?? [];
187570
+ if (memos.length === 0 && signals.length === 0)
187571
+ return "";
187572
+ const ssrDefaults = extractSsrDefaults(ir.metadata) ?? {};
187573
+ const available = new Set(ir.metadata.propsParams.map((p) => p.name));
187574
+ const lines = [];
187575
+ for (const signal of signals) {
187576
+ const kolon = this.tryLowerToKolon(signal.initialValue, available);
187577
+ const refsSelf = kolon !== null && new RegExp(`\\$${signal.getter}\\b`).test(kolon);
187578
+ if (kolon !== null && !refsSelf)
187579
+ lines.push(`: my $${signal.getter} = ${kolon};`);
187580
+ available.add(signal.getter);
187581
+ }
187582
+ for (const memo of memos) {
187583
+ const def = ssrDefaults[memo.name];
187584
+ const isNull2 = !def || typeof def === "object" && "value" in def && def.value === null;
187585
+ if (!isNull2) {
187586
+ available.add(memo.name);
187587
+ continue;
187588
+ }
187589
+ const body = extractArrowBodyExpression(memo.computation);
187590
+ if (body === null)
187591
+ continue;
187592
+ const kolon = this.tryLowerToKolon(body, available);
187593
+ if (kolon !== null)
187594
+ lines.push(`: my $${memo.name} = ${kolon};`);
187595
+ available.add(memo.name);
187596
+ }
187597
+ return lines.length > 0 ? lines.join(`
187598
+ `) + `
187599
+ ` : "";
187600
+ }
187601
+ tryLowerToKolon(expr, available) {
187602
+ const trimmed = expr.trim();
187603
+ if (!trimmed)
187604
+ return null;
187605
+ if (!isSupported(parseExpression2(trimmed)).supported)
187606
+ return null;
187607
+ const kolon = this.convertExpressionToKolon(trimmed);
187608
+ if (kolon === "" || !/\$[A-Za-z_]\w*/.test(kolon))
187609
+ return null;
187610
+ return referencedVarsAreAvailable(kolon, available) ? kolon : null;
187497
187611
  }
187498
187612
  emitAsync(node, _ctx, _emit) {
187499
187613
  return this.renderAsync(node);
@@ -187506,6 +187620,9 @@ class XslateAdapter extends BaseAdapter {
187506
187620
  if (element.needsScope) {
187507
187621
  hydrationAttrs += ` ${this.renderScopeMarker("")}`;
187508
187622
  }
187623
+ if (this.rootScopeNodes.has(element) && element.needsScope) {
187624
+ hydrationAttrs += ` <: $bf.data_key_attr() | mark_raw :>`;
187625
+ }
187509
187626
  if (element.slotId) {
187510
187627
  hydrationAttrs += ` ${this.renderSlotMarker(element.slotId)}`;
187511
187628
  }
@@ -48,6 +48,11 @@ export declare class XslateAdapter extends BaseAdapter implements IRNodeEmitter<
48
48
  */
49
49
  templatePrimitives: TemplatePrimitiveRegistry;
50
50
  private componentName;
51
+ /** Component root scope element(s) — each carries `data-key` for a keyed loop
52
+ * item (set by the child renderer from the JSX `key` prop). A plain element
53
+ * root is one node; an `if-statement` (early-return) root contributes the
54
+ * top element of every branch. */
55
+ private rootScopeNodes;
51
56
  private options;
52
57
  private errors;
53
58
  private inLoop;
@@ -110,6 +115,33 @@ export declare class XslateAdapter extends BaseAdapter implements IRNodeEmitter<
110
115
  emitSlot(node: IRSlot): string;
111
116
  emitIfStatement(node: IRIfStatement, _ctx: XslateRenderCtx, _emit: EmitIRNode<XslateRenderCtx>): string;
112
117
  emitProvider(node: IRProvider, _ctx: XslateRenderCtx, _emit: EmitIRNode<XslateRenderCtx>): string;
118
+ /** Lower a `<Ctx.Provider value>` value prop to a Kolon expression. */
119
+ private providerValueKolon;
120
+ /** Kolon literal for a context-consumer's `createContext` default. */
121
+ private contextDefaultKolon;
122
+ /**
123
+ * Emit one `: my $<local> = $bf.use_context(...)` line-statement per
124
+ * context consumer so the body's bare `$<local>` resolves to the active
125
+ * provider value (or the `createContext` default). (#1297)
126
+ */
127
+ private generateContextConsumerSeed;
128
+ /**
129
+ * Seed memos whose SSR default is `null` (not statically evaluable) by
130
+ * computing them in-template from the already-seeded prop / signal vars
131
+ * (`createMemo(() => props.value * 10)` → `: my $x = $value * 10;`). Without
132
+ * this the memo's `$x` renders empty — the reason
133
+ * `props-reactivity-comparison` was skipped. Only emitted when every var the
134
+ * lowering references is already in scope. (#1297)
135
+ */
136
+ private generateDerivedMemoSeed;
137
+ /**
138
+ * Lower a signal init / memo body to Kolon for an in-template SSR seed, or
139
+ * `null` when it shouldn't be seeded this way: not a supported shape
140
+ * (`isSupported` pre-check, so object/array literals don't fail the build),
141
+ * references no in-scope var (a constant — keep ssr-defaults seeding), or
142
+ * references an out-of-scope binding. (#1297)
143
+ */
144
+ private tryLowerToKolon;
113
145
  emitAsync(node: IRAsync, _ctx: XslateRenderCtx, _emit: EmitIRNode<XslateRenderCtx>): string;
114
146
  renderElement(element: IRElement): string;
115
147
  renderExpression(expr: IRExpression): string;
@@ -1 +1 @@
1
- {"version":3,"file":"xslate-adapter.d.ts","sourceRoot":"","sources":["../../src/adapter/xslate-adapter.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"xslate-adapter.d.ts","sourceRoot":"","sources":["../../src/adapter/xslate-adapter.ts"],"names":[],"mappings":"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"}
package/dist/build.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import type { BuildOptions } from '@barefootjs/jsx';
2
- import { XslateAdapter } from './adapter';
3
- import type { XslateAdapterOptions } from './adapter';
2
+ import { XslateAdapter } from './adapter/index.ts';
3
+ import type { XslateAdapterOptions } from './adapter/index.ts';
4
4
  export interface XslateBuildOptions extends BuildOptions {
5
5
  /** Adapter-specific options passed to XslateAdapter */
6
6
  adapterOptions?: XslateAdapterOptions;
@@ -1 +1 @@
1
- {"version":3,"file":"build.d.ts","sourceRoot":"","sources":["../src/build.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAA;AACnD,OAAO,EAAE,aAAa,EAAE,MAAM,WAAW,CAAA;AACzC,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,WAAW,CAAA;AAErD,MAAM,WAAW,kBAAmB,SAAQ,YAAY;IACtD,uDAAuD;IACvD,cAAc,CAAC,EAAE,oBAAoB,CAAA;CACtC;AAED;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,OAAO,GAAE,kBAAuB;IAEzD,OAAO;IACP,KAAK;IACL,UAAU;IACV,MAAM;IACN,MAAM;IACN,WAAW;IACX,SAAS;IACT,iBAAiB;IACjB,aAAa;IACb,mBAAmB;IACnB,YAAY;IAKZ,SAAS;EAEZ"}
1
+ {"version":3,"file":"build.d.ts","sourceRoot":"","sources":["../src/build.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAA;AACnD,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAA;AAClD,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,oBAAoB,CAAA;AAE9D,MAAM,WAAW,kBAAmB,SAAQ,YAAY;IACtD,uDAAuD;IACvD,cAAc,CAAC,EAAE,oBAAoB,CAAA;CACtC;AAED;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,OAAO,GAAE,kBAAuB;IAEzD,OAAO;IACP,KAAK;IACL,UAAU;IACV,MAAM;IACN,MAAM;IACN,WAAW;IACX,SAAS;IACT,iBAAiB;IACjB,aAAa;IACb,mBAAmB;IACnB,YAAY;IAKZ,SAAS;EAEZ"}
package/dist/build.js CHANGED
@@ -187299,7 +187299,10 @@ import {
187299
187299
  emitAttrValue,
187300
187300
  augmentInheritedPropAccesses,
187301
187301
  parseRecordIndexAccess,
187302
- evalStringArrayJoin
187302
+ evalStringArrayJoin,
187303
+ extractArrowBodyExpression,
187304
+ collectContextConsumers,
187305
+ extractSsrDefaults
187303
187306
  } from "@barefootjs/jsx";
187304
187307
 
187305
187308
  // src/adapter/boolean-result.ts
@@ -187379,6 +187382,36 @@ function resolveJsxChildrenProp(props) {
187379
187382
  return [];
187380
187383
  return prop.value.children;
187381
187384
  }
187385
+ function collectRootScopeNodes(node) {
187386
+ const out = new Set;
187387
+ const visit = (n) => {
187388
+ if (!n)
187389
+ return;
187390
+ if (n.type === "element") {
187391
+ out.add(n);
187392
+ return;
187393
+ }
187394
+ if (n.type === "if-statement") {
187395
+ const s = n;
187396
+ visit(s.consequent);
187397
+ visit(s.alternate);
187398
+ return;
187399
+ }
187400
+ if (n.type === "fragment") {
187401
+ for (const c of n.children)
187402
+ visit(c);
187403
+ }
187404
+ };
187405
+ visit(node);
187406
+ return out;
187407
+ }
187408
+ function referencedVarsAreAvailable(expr, available) {
187409
+ for (const m of expr.matchAll(/\$([A-Za-z_]\w*)/g)) {
187410
+ if (!available.has(m[1]))
187411
+ return false;
187412
+ }
187413
+ return true;
187414
+ }
187382
187415
 
187383
187416
  class XslateAdapter extends BaseAdapter {
187384
187417
  name = "xslate";
@@ -187387,6 +187420,7 @@ class XslateAdapter extends BaseAdapter {
187387
187420
  importMapInjection = "html-snippet";
187388
187421
  templatePrimitives = XSLATE_PRIMITIVE_EMIT_MAP;
187389
187422
  componentName = "";
187423
+ rootScopeNodes = new Set;
187390
187424
  options;
187391
187425
  errors = [];
187392
187426
  inLoop = false;
@@ -187426,9 +187460,12 @@ class XslateAdapter extends BaseAdapter {
187426
187460
  if (!options?.siblingTemplatesRegistered) {
187427
187461
  this.checkImportedLoopChildComponents(ir);
187428
187462
  }
187463
+ this.rootScopeNodes = collectRootScopeNodes(ir.root);
187429
187464
  const templateBody = ir.root.type === "if-statement" ? this.renderIfStatement(ir.root) : this.renderNode(ir.root);
187430
187465
  const scriptReg = options?.skipScriptRegistration ? "" : this.generateScriptRegistrations(ir, options?.scriptBaseName);
187431
- const template = `${scriptReg}${templateBody}
187466
+ const ctxSeed = this.generateContextConsumerSeed(ir);
187467
+ const memoSeed = this.generateDerivedMemoSeed(ir);
187468
+ const template = `${scriptReg}${ctxSeed}${memoSeed}${templateBody}
187432
187469
  `;
187433
187470
  if (this.errors.length > 0) {
187434
187471
  ir.errors.push(...this.errors);
@@ -187493,7 +187530,84 @@ class XslateAdapter extends BaseAdapter {
187493
187530
  return this.renderIfStatement(node);
187494
187531
  }
187495
187532
  emitProvider(node, _ctx, _emit) {
187496
- return this.renderChildren(node.children);
187533
+ const value = this.providerValueKolon(node.valueProp);
187534
+ const children = this.renderChildren(node.children);
187535
+ const name = node.contextName;
187536
+ return `<: $bf.provide_context('${name}', ${value}) :>` + children + `<: $bf.revoke_context('${name}') :>`;
187537
+ }
187538
+ providerValueKolon(valueProp) {
187539
+ const v = valueProp.value;
187540
+ if (v.kind === "literal") {
187541
+ return typeof v.value === "string" ? `'${v.value.replace(/[\\']/g, (m) => `\\${m}`)}'` : String(v.value);
187542
+ }
187543
+ if (v.kind === "expression")
187544
+ return this.convertExpressionToKolon(v.expr);
187545
+ if (v.kind === "template")
187546
+ return this.convertTemplateLiteralPartsToKolon(v.parts);
187547
+ return "nil";
187548
+ }
187549
+ contextDefaultKolon(c) {
187550
+ const d = c.defaultValue;
187551
+ if (d === null || d === undefined)
187552
+ return "nil";
187553
+ if (typeof d === "string")
187554
+ return `'${d.replace(/[\\']/g, (m) => `\\${m}`)}'`;
187555
+ if (typeof d === "boolean")
187556
+ return d ? "1" : "0";
187557
+ return String(d);
187558
+ }
187559
+ generateContextConsumerSeed(ir) {
187560
+ const consumers = collectContextConsumers(ir.metadata);
187561
+ if (consumers.length === 0)
187562
+ return "";
187563
+ return consumers.map((c) => `: my $${c.localName} = $bf.use_context('${c.contextName}', ${this.contextDefaultKolon(c)});`).join(`
187564
+ `) + `
187565
+ `;
187566
+ }
187567
+ generateDerivedMemoSeed(ir) {
187568
+ const memos = ir.metadata.memos ?? [];
187569
+ const signals = ir.metadata.signals ?? [];
187570
+ if (memos.length === 0 && signals.length === 0)
187571
+ return "";
187572
+ const ssrDefaults = extractSsrDefaults(ir.metadata) ?? {};
187573
+ const available = new Set(ir.metadata.propsParams.map((p) => p.name));
187574
+ const lines = [];
187575
+ for (const signal of signals) {
187576
+ const kolon = this.tryLowerToKolon(signal.initialValue, available);
187577
+ const refsSelf = kolon !== null && new RegExp(`\\$${signal.getter}\\b`).test(kolon);
187578
+ if (kolon !== null && !refsSelf)
187579
+ lines.push(`: my $${signal.getter} = ${kolon};`);
187580
+ available.add(signal.getter);
187581
+ }
187582
+ for (const memo of memos) {
187583
+ const def = ssrDefaults[memo.name];
187584
+ const isNull2 = !def || typeof def === "object" && "value" in def && def.value === null;
187585
+ if (!isNull2) {
187586
+ available.add(memo.name);
187587
+ continue;
187588
+ }
187589
+ const body = extractArrowBodyExpression(memo.computation);
187590
+ if (body === null)
187591
+ continue;
187592
+ const kolon = this.tryLowerToKolon(body, available);
187593
+ if (kolon !== null)
187594
+ lines.push(`: my $${memo.name} = ${kolon};`);
187595
+ available.add(memo.name);
187596
+ }
187597
+ return lines.length > 0 ? lines.join(`
187598
+ `) + `
187599
+ ` : "";
187600
+ }
187601
+ tryLowerToKolon(expr, available) {
187602
+ const trimmed = expr.trim();
187603
+ if (!trimmed)
187604
+ return null;
187605
+ if (!isSupported(parseExpression2(trimmed)).supported)
187606
+ return null;
187607
+ const kolon = this.convertExpressionToKolon(trimmed);
187608
+ if (kolon === "" || !/\$[A-Za-z_]\w*/.test(kolon))
187609
+ return null;
187610
+ return referencedVarsAreAvailable(kolon, available) ? kolon : null;
187497
187611
  }
187498
187612
  emitAsync(node, _ctx, _emit) {
187499
187613
  return this.renderAsync(node);
@@ -187506,6 +187620,9 @@ class XslateAdapter extends BaseAdapter {
187506
187620
  if (element.needsScope) {
187507
187621
  hydrationAttrs += ` ${this.renderScopeMarker("")}`;
187508
187622
  }
187623
+ if (this.rootScopeNodes.has(element) && element.needsScope) {
187624
+ hydrationAttrs += ` <: $bf.data_key_attr() | mark_raw :>`;
187625
+ }
187509
187626
  if (element.slotId) {
187510
187627
  hydrationAttrs += ` ${this.renderSlotMarker(element.slotId)}`;
187511
187628
  }
package/dist/index.d.ts CHANGED
@@ -3,6 +3,6 @@
3
3
  *
4
4
  * Generates Text::Xslate Kolon template files (.tx) from BarefootJS IR.
5
5
  */
6
- export { XslateAdapter, xslateAdapter } from './adapter';
7
- export type { XslateAdapterOptions } from './adapter';
6
+ export { XslateAdapter, xslateAdapter } from './adapter/index.ts';
7
+ export type { XslateAdapterOptions } from './adapter/index.ts';
8
8
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,WAAW,CAAA;AACxD,YAAY,EAAE,oBAAoB,EAAE,MAAM,WAAW,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAA;AACjE,YAAY,EAAE,oBAAoB,EAAE,MAAM,oBAAoB,CAAA"}
package/dist/index.js CHANGED
@@ -187299,7 +187299,10 @@ import {
187299
187299
  emitAttrValue,
187300
187300
  augmentInheritedPropAccesses,
187301
187301
  parseRecordIndexAccess,
187302
- evalStringArrayJoin
187302
+ evalStringArrayJoin,
187303
+ extractArrowBodyExpression,
187304
+ collectContextConsumers,
187305
+ extractSsrDefaults
187303
187306
  } from "@barefootjs/jsx";
187304
187307
 
187305
187308
  // src/adapter/boolean-result.ts
@@ -187379,6 +187382,36 @@ function resolveJsxChildrenProp(props) {
187379
187382
  return [];
187380
187383
  return prop.value.children;
187381
187384
  }
187385
+ function collectRootScopeNodes(node) {
187386
+ const out = new Set;
187387
+ const visit = (n) => {
187388
+ if (!n)
187389
+ return;
187390
+ if (n.type === "element") {
187391
+ out.add(n);
187392
+ return;
187393
+ }
187394
+ if (n.type === "if-statement") {
187395
+ const s = n;
187396
+ visit(s.consequent);
187397
+ visit(s.alternate);
187398
+ return;
187399
+ }
187400
+ if (n.type === "fragment") {
187401
+ for (const c of n.children)
187402
+ visit(c);
187403
+ }
187404
+ };
187405
+ visit(node);
187406
+ return out;
187407
+ }
187408
+ function referencedVarsAreAvailable(expr, available) {
187409
+ for (const m of expr.matchAll(/\$([A-Za-z_]\w*)/g)) {
187410
+ if (!available.has(m[1]))
187411
+ return false;
187412
+ }
187413
+ return true;
187414
+ }
187382
187415
 
187383
187416
  class XslateAdapter extends BaseAdapter {
187384
187417
  name = "xslate";
@@ -187387,6 +187420,7 @@ class XslateAdapter extends BaseAdapter {
187387
187420
  importMapInjection = "html-snippet";
187388
187421
  templatePrimitives = XSLATE_PRIMITIVE_EMIT_MAP;
187389
187422
  componentName = "";
187423
+ rootScopeNodes = new Set;
187390
187424
  options;
187391
187425
  errors = [];
187392
187426
  inLoop = false;
@@ -187426,9 +187460,12 @@ class XslateAdapter extends BaseAdapter {
187426
187460
  if (!options?.siblingTemplatesRegistered) {
187427
187461
  this.checkImportedLoopChildComponents(ir);
187428
187462
  }
187463
+ this.rootScopeNodes = collectRootScopeNodes(ir.root);
187429
187464
  const templateBody = ir.root.type === "if-statement" ? this.renderIfStatement(ir.root) : this.renderNode(ir.root);
187430
187465
  const scriptReg = options?.skipScriptRegistration ? "" : this.generateScriptRegistrations(ir, options?.scriptBaseName);
187431
- const template = `${scriptReg}${templateBody}
187466
+ const ctxSeed = this.generateContextConsumerSeed(ir);
187467
+ const memoSeed = this.generateDerivedMemoSeed(ir);
187468
+ const template = `${scriptReg}${ctxSeed}${memoSeed}${templateBody}
187432
187469
  `;
187433
187470
  if (this.errors.length > 0) {
187434
187471
  ir.errors.push(...this.errors);
@@ -187493,7 +187530,84 @@ class XslateAdapter extends BaseAdapter {
187493
187530
  return this.renderIfStatement(node);
187494
187531
  }
187495
187532
  emitProvider(node, _ctx, _emit) {
187496
- return this.renderChildren(node.children);
187533
+ const value = this.providerValueKolon(node.valueProp);
187534
+ const children = this.renderChildren(node.children);
187535
+ const name = node.contextName;
187536
+ return `<: $bf.provide_context('${name}', ${value}) :>` + children + `<: $bf.revoke_context('${name}') :>`;
187537
+ }
187538
+ providerValueKolon(valueProp) {
187539
+ const v = valueProp.value;
187540
+ if (v.kind === "literal") {
187541
+ return typeof v.value === "string" ? `'${v.value.replace(/[\\']/g, (m) => `\\${m}`)}'` : String(v.value);
187542
+ }
187543
+ if (v.kind === "expression")
187544
+ return this.convertExpressionToKolon(v.expr);
187545
+ if (v.kind === "template")
187546
+ return this.convertTemplateLiteralPartsToKolon(v.parts);
187547
+ return "nil";
187548
+ }
187549
+ contextDefaultKolon(c) {
187550
+ const d = c.defaultValue;
187551
+ if (d === null || d === undefined)
187552
+ return "nil";
187553
+ if (typeof d === "string")
187554
+ return `'${d.replace(/[\\']/g, (m) => `\\${m}`)}'`;
187555
+ if (typeof d === "boolean")
187556
+ return d ? "1" : "0";
187557
+ return String(d);
187558
+ }
187559
+ generateContextConsumerSeed(ir) {
187560
+ const consumers = collectContextConsumers(ir.metadata);
187561
+ if (consumers.length === 0)
187562
+ return "";
187563
+ return consumers.map((c) => `: my $${c.localName} = $bf.use_context('${c.contextName}', ${this.contextDefaultKolon(c)});`).join(`
187564
+ `) + `
187565
+ `;
187566
+ }
187567
+ generateDerivedMemoSeed(ir) {
187568
+ const memos = ir.metadata.memos ?? [];
187569
+ const signals = ir.metadata.signals ?? [];
187570
+ if (memos.length === 0 && signals.length === 0)
187571
+ return "";
187572
+ const ssrDefaults = extractSsrDefaults(ir.metadata) ?? {};
187573
+ const available = new Set(ir.metadata.propsParams.map((p) => p.name));
187574
+ const lines = [];
187575
+ for (const signal of signals) {
187576
+ const kolon = this.tryLowerToKolon(signal.initialValue, available);
187577
+ const refsSelf = kolon !== null && new RegExp(`\\$${signal.getter}\\b`).test(kolon);
187578
+ if (kolon !== null && !refsSelf)
187579
+ lines.push(`: my $${signal.getter} = ${kolon};`);
187580
+ available.add(signal.getter);
187581
+ }
187582
+ for (const memo of memos) {
187583
+ const def = ssrDefaults[memo.name];
187584
+ const isNull2 = !def || typeof def === "object" && "value" in def && def.value === null;
187585
+ if (!isNull2) {
187586
+ available.add(memo.name);
187587
+ continue;
187588
+ }
187589
+ const body = extractArrowBodyExpression(memo.computation);
187590
+ if (body === null)
187591
+ continue;
187592
+ const kolon = this.tryLowerToKolon(body, available);
187593
+ if (kolon !== null)
187594
+ lines.push(`: my $${memo.name} = ${kolon};`);
187595
+ available.add(memo.name);
187596
+ }
187597
+ return lines.length > 0 ? lines.join(`
187598
+ `) + `
187599
+ ` : "";
187600
+ }
187601
+ tryLowerToKolon(expr, available) {
187602
+ const trimmed = expr.trim();
187603
+ if (!trimmed)
187604
+ return null;
187605
+ if (!isSupported(parseExpression2(trimmed)).supported)
187606
+ return null;
187607
+ const kolon = this.convertExpressionToKolon(trimmed);
187608
+ if (kolon === "" || !/\$[A-Za-z_]\w*/.test(kolon))
187609
+ return null;
187610
+ return referencedVarsAreAvailable(kolon, available) ? kolon : null;
187497
187611
  }
187498
187612
  emitAsync(node, _ctx, _emit) {
187499
187613
  return this.renderAsync(node);
@@ -187506,6 +187620,9 @@ class XslateAdapter extends BaseAdapter {
187506
187620
  if (element.needsScope) {
187507
187621
  hydrationAttrs += ` ${this.renderScopeMarker("")}`;
187508
187622
  }
187623
+ if (this.rootScopeNodes.has(element) && element.needsScope) {
187624
+ hydrationAttrs += ` <: $bf.data_key_attr() | mark_raw :>`;
187625
+ }
187509
187626
  if (element.slotId) {
187510
187627
  hydrationAttrs += ` ${this.renderSlotMarker(element.slotId)}`;
187511
187628
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@barefootjs/xslate",
3
- "version": "0.9.1",
3
+ "version": "0.9.3",
4
4
  "description": "Text::Xslate (Kolon) adapter for BarefootJS — compiles IR to .tx templates and ships the Xslate rendering backend; runs under any PSGI/Plack app",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -55,14 +55,14 @@
55
55
  "directory": "packages/adapter-xslate"
56
56
  },
57
57
  "dependencies": {
58
- "@barefootjs/shared": "0.9.1"
58
+ "@barefootjs/shared": "0.9.3"
59
59
  },
60
60
  "peerDependencies": {
61
61
  "@barefootjs/jsx": ">=0.2.0"
62
62
  },
63
63
  "devDependencies": {
64
64
  "@barefootjs/adapter-tests": "0.1.0",
65
- "@barefootjs/jsx": "0.9.1",
65
+ "@barefootjs/jsx": "0.9.3",
66
66
  "typescript": "^5.0.0"
67
67
  }
68
68
  }
@@ -12,42 +12,23 @@
12
12
  * genuinely differs. Every divergence carries a one-line rationale.
13
13
  */
14
14
 
15
+ import { describe, test, expect } from 'bun:test'
15
16
  import {
16
17
  runAdapterConformanceTests,
17
18
  TemplatePrimitiveCaseId,
18
19
  } from '@barefootjs/adapter-tests'
19
20
  import { XslateAdapter } from '../adapter'
20
21
  import { renderXslateComponent, XslateNotAvailableError } from '../test-render'
22
+ import { compileJSX, type ComponentIR } from '@barefootjs/jsx'
21
23
 
22
24
  runAdapterConformanceTests({
23
25
  name: 'xslate',
24
26
  factory: () => new XslateAdapter(),
25
27
  render: renderXslateComponent,
26
- // Skips here are VERIFIED, not inherited from mojo. Notably, the six
27
- // fixtures mojo skips for Perl-EP scoping faults `logical-or-jsx`,
28
- // `nullish-coalescing-jsx`, `branch-map`, `return-logical-or`,
29
- // `return-nullish-coalescing`, `return-map` (bare `$label` / `$items`
30
- // without a `my` binding) — all PASS on Xslate, because Kolon resolves
31
- // `$label` from the per-render vars rather than a Perl lexical, so there
32
- // is no undefined-symbol fault. Xslate therefore skips strictly fewer
33
- // fixtures than mojo. Each entry below was confirmed to fail with
34
- // skipJsx emptied.
35
- skipJsx: [
36
- // SSR context propagation (`<Ctx.Provider value>` → `useContext`): the
37
- // template reads a stash key that's never seeded. Implemented on Go; the
38
- // Perl stash-seed path is a follow-up port, so Xslate stays skipped (#1297).
39
- 'context-provider',
40
- // Multi-component shared-state pairs whose children render inside a keyed
41
- // `.map` (loop children, no `_bf_slot`): the test harness derives a
42
- // non-deterministic `<child>_<rand>` scope id instead of the canonical
43
- // `<ChildName>_*` the reference HTML pins, and per-item loop state isn't
44
- // seeded server-side. Harness-level scope-id plumbing; single-component
45
- // `reactive-props` passes. (Same pair mojo skips.)
46
- 'toggle-shared',
47
- 'props-reactivity-comparison',
48
- // (`kbd` is not skipped here — it's a BF101 refusal pinned in
49
- // `expectedDiagnostics` below, not a render-mismatch.)
50
- ],
28
+ // No JSX-render skips: every shared conformance fixture renders to Hono
29
+ // parity on real Text::Xslate. Shapes the adapter intentionally refuses at
30
+ // build time are pinned in `expectedDiagnostics` below (e.g. `kbd`, `button`).
31
+ skipJsx: [],
51
32
  // Per-fixture build-time contracts for shapes the Xslate adapter
52
33
  // intentionally refuses to lower. Mirrors mojo's set — the lowering
53
34
  // gates are shared code paths in the ported adapter.
@@ -129,3 +110,131 @@ runAdapterConformanceTests({
129
110
  return false
130
111
  },
131
112
  })
113
+
114
+ // =============================================================================
115
+ // Helpers
116
+ // =============================================================================
117
+
118
+ function compileToIR(source: string): ComponentIR {
119
+ const result = compileJSX(source.trimStart(), 'test.tsx', {
120
+ adapter: new XslateAdapter(),
121
+ outputIR: true,
122
+ })
123
+ const irFile = result.files.find(f => f.type === 'ir')
124
+ if (!irFile) throw new Error('No IR output')
125
+ return JSON.parse(irFile.content) as ComponentIR
126
+ }
127
+
128
+ function compileAndGenerate(source: string) {
129
+ return new XslateAdapter().generate(compileToIR(source))
130
+ }
131
+
132
+ // =============================================================================
133
+ // Xslate-Specific Tests
134
+ // =============================================================================
135
+
136
+ describe('XslateAdapter - SSR context propagation (#1297)', () => {
137
+ // `<Ctx.Provider value>` brackets its children with inline provide/revoke
138
+ // calls (both return '' so the `<: … :>` discards them); descendant
139
+ // `useContext` consumers read the value during the same render.
140
+ test('provider brackets children with provide_context / revoke_context', () => {
141
+ const { template } = compileAndGenerate(`
142
+ 'use client'
143
+ import { createContext, useContext } from '@barefootjs/client'
144
+ const ThemeContext = createContext('light')
145
+ export function ThemeRoot() {
146
+ return <div><ThemeContext.Provider value="dark"><ThemeLabel /></ThemeContext.Provider></div>
147
+ }
148
+ function ThemeLabel() { const theme = useContext(ThemeContext); return <span>{theme}</span> }
149
+ `)
150
+ expect(template).toContain("$bf.provide_context('ThemeContext', 'dark')")
151
+ expect(template).toContain("$bf.revoke_context('ThemeContext')")
152
+ expect(template.indexOf('provide_context')).toBeLessThan(template.indexOf('render_child'))
153
+ expect(template.indexOf('render_child')).toBeLessThan(template.indexOf('revoke_context'))
154
+ })
155
+
156
+ test('consumer seeds its local from use_context with the createContext default', () => {
157
+ const { template } = compileAndGenerate(`
158
+ 'use client'
159
+ import { createContext, useContext } from '@barefootjs/client'
160
+ const ThemeContext = createContext('light')
161
+ export function ThemeLabel() { const theme = useContext(ThemeContext); return <span>{theme}</span> }
162
+ `)
163
+ expect(template).toContain(": my $theme = $bf.use_context('ThemeContext', 'light');")
164
+ })
165
+ })
166
+
167
+ describe('XslateAdapter - prop-derived memo SSR seeding (#1297)', () => {
168
+ // A memo whose body can't be statically folded (`props.value * 10`) gets a
169
+ // `null` SSR default; the adapter computes it in-template from the seeded
170
+ // prop var so the child renders the value instead of empty.
171
+ test('seeds a prop-derived memo from the prop var', () => {
172
+ const { template } = compileAndGenerate(`
173
+ 'use client'
174
+ import { createMemo } from '@barefootjs/client'
175
+ export function Child(props: { value: number }) {
176
+ const displayValue = createMemo(() => props.value * 10)
177
+ return <span>{displayValue()}</span>
178
+ }
179
+ `)
180
+ expect(template).toContain(': my $displayValue = $value * 10;')
181
+ })
182
+
183
+ test('seeds a memo over a destructured prop', () => {
184
+ const { template } = compileAndGenerate(`
185
+ 'use client'
186
+ import { createMemo } from '@barefootjs/client'
187
+ export function Child({ value }: { value: number }) {
188
+ const displayValue = createMemo(() => value * 10)
189
+ return <span>{displayValue()}</span>
190
+ }
191
+ `)
192
+ expect(template).toContain(': my $displayValue = $value * 10;')
193
+ })
194
+ })
195
+
196
+ describe('XslateAdapter - prop-derived signal SSR seeding + data-key (#1297, toggle-shared)', () => {
197
+ test('seeds a prop-derived (different-name) signal from the prop var', () => {
198
+ const { template } = compileAndGenerate(`
199
+ 'use client'
200
+ import { createSignal } from '@barefootjs/client'
201
+ export function Item(props: { defaultOn?: boolean }) {
202
+ const [on, setOn] = createSignal(props.defaultOn ?? false)
203
+ return <button>{on() ? 'ON' : 'OFF'}</button>
204
+ }
205
+ `)
206
+ expect(template).toContain(': my $on = ($defaultOn // 0);')
207
+ })
208
+
209
+ // Kolon can't `: my $x = … $x …`; a same-name signal stays on the existing
210
+ // (harness/manifest) seeding rather than an in-template seed.
211
+ test('does NOT in-template-seed a same-name signal', () => {
212
+ const { template } = compileAndGenerate(`
213
+ 'use client'
214
+ import { createSignal } from '@barefootjs/client'
215
+ export function C(props: { x?: number }) {
216
+ const [x, setX] = createSignal(props.x ?? 7)
217
+ return <span>{x()}</span>
218
+ }
219
+ `)
220
+ expect(template).not.toContain(': my $x =')
221
+ })
222
+
223
+ test('emits data_key_attr on the component root', () => {
224
+ const { template } = compileAndGenerate(`
225
+ export function Item() { return <div class="x">hi</div> }
226
+ `)
227
+ expect(template).toContain('$bf.data_key_attr()')
228
+ })
229
+
230
+ test('emits data_key_attr on each branch root of an if-statement root', () => {
231
+ const { template } = compileAndGenerate(`
232
+ export function Item({ on }: { on?: boolean }) {
233
+ if (on) return <div class="a">A</div>
234
+ return <div class="b">B</div>
235
+ }
236
+ `)
237
+ const count = (template.match(/\$bf\.data_key_attr\(\)/g) ?? []).length
238
+ expect(count).toBe(2)
239
+ })
240
+ })
@@ -96,7 +96,10 @@ my $bf = BarefootJS->new(undef, { backend => $backend });
96
96
  $bf->_scope_id('Counter_test');
97
97
 
98
98
  binmode(STDOUT, ':utf8');
99
- my \$html = \$backend->render_named('counter', \$bf, { count => 3, doubled => 6 });
99
+ # The count signal is seeded in-template from the prop it derives from (#1297),
100
+ # so the host seeds initial (not the count signal value directly); doubled is a
101
+ # memo with a non-null static default and stays a directly-seeded stash var.
102
+ my \$html = \$backend->render_named('counter', \$bf, { initial => 3, doubled => 6 });
100
103
  print \$html;
101
104
  # The template's register_script calls populated \$bf's script list during
102
105
  # render; emit the resulting <script> tags so the test can assert them.
@@ -2,5 +2,5 @@
2
2
  * Text::Xslate (Kolon) Template Adapter Exports
3
3
  */
4
4
 
5
- export { XslateAdapter, xslateAdapter } from './xslate-adapter'
6
- export type { XslateAdapterOptions } from './xslate-adapter'
5
+ export { XslateAdapter, xslateAdapter } from './xslate-adapter.ts'
6
+ export type { XslateAdapterOptions } from './xslate-adapter.ts'
@@ -63,8 +63,12 @@ import {
63
63
  augmentInheritedPropAccesses,
64
64
  parseRecordIndexAccess,
65
65
  evalStringArrayJoin,
66
+ extractArrowBodyExpression,
67
+ collectContextConsumers,
68
+ type ContextConsumer,
69
+ extractSsrDefaults,
66
70
  } from '@barefootjs/jsx'
67
- import { isAriaBooleanAttr, isBooleanResultExpr } from './boolean-result'
71
+ import { isAriaBooleanAttr, isBooleanResultExpr } from './boolean-result.ts'
68
72
  import ts from 'typescript'
69
73
 
70
74
  /**
@@ -139,6 +143,43 @@ function resolveJsxChildrenProp(props: readonly IRProp[]): IRNode[] {
139
143
  return prop.value.children
140
144
  }
141
145
 
146
+ /**
147
+ * Collect the component's root scope element node(s) — the elements that
148
+ * become the rendered root and so carry `data-key` for a keyed loop item. A
149
+ * plain element root is itself; an `if-statement` (early-return) root
150
+ * contributes the top element of each branch, since exactly one renders at
151
+ * runtime. (#1297)
152
+ */
153
+ function collectRootScopeNodes(node: IRNode): Set<IRNode> {
154
+ const out = new Set<IRNode>()
155
+ const visit = (n: IRNode | null): void => {
156
+ if (!n) return
157
+ if (n.type === 'element') { out.add(n); return }
158
+ if (n.type === 'if-statement') {
159
+ const s = n as IRIfStatement
160
+ visit(s.consequent)
161
+ visit(s.alternate)
162
+ return
163
+ }
164
+ if (n.type === 'fragment') {
165
+ for (const c of (n as IRFragment).children) visit(c)
166
+ }
167
+ }
168
+ visit(node)
169
+ return out
170
+ }
171
+
172
+ /**
173
+ * True when every `$var` the lowered Kolon expression references is already in
174
+ * scope — guards in-template memo seeding against an out-of-scope binding. (#1297)
175
+ */
176
+ function referencedVarsAreAvailable(expr: string, available: ReadonlySet<string>): boolean {
177
+ for (const m of expr.matchAll(/\$([A-Za-z_]\w*)/g)) {
178
+ if (!available.has(m[1])) return false
179
+ }
180
+ return true
181
+ }
182
+
142
183
  export interface XslateAdapterOptions {
143
184
  /** Base path for client JS files (default: '/static/components/') */
144
185
  clientJsBasePath?: string
@@ -164,6 +205,11 @@ export class XslateAdapter extends BaseAdapter implements IRNodeEmitter<XslateRe
164
205
  templatePrimitives: TemplatePrimitiveRegistry = XSLATE_PRIMITIVE_EMIT_MAP
165
206
 
166
207
  private componentName: string = ''
208
+ /** Component root scope element(s) — each carries `data-key` for a keyed loop
209
+ * item (set by the child renderer from the JSX `key` prop). A plain element
210
+ * root is one node; an `if-statement` (early-return) root contributes the
211
+ * top element of every branch. */
212
+ private rootScopeNodes: Set<IRNode> = new Set()
167
213
  private options: Required<XslateAdapterOptions>
168
214
  private errors: CompilerError[] = []
169
215
  private inLoop: boolean = false
@@ -267,6 +313,7 @@ export class XslateAdapter extends BaseAdapter implements IRNodeEmitter<XslateRe
267
313
  this.checkImportedLoopChildComponents(ir)
268
314
  }
269
315
 
316
+ this.rootScopeNodes = collectRootScopeNodes(ir.root)
270
317
  const templateBody = ir.root.type === 'if-statement'
271
318
  ? this.renderIfStatement(ir.root as IRIfStatement)
272
319
  : this.renderNode(ir.root)
@@ -276,7 +323,18 @@ export class XslateAdapter extends BaseAdapter implements IRNodeEmitter<XslateRe
276
323
  ? ''
277
324
  : this.generateScriptRegistrations(ir, options?.scriptBaseName)
278
325
 
279
- const template = `${scriptReg}${templateBody}\n`
326
+ // SSR context consumers (`const x = useContext(Ctx)`): seed each local
327
+ // from the active provider value (or the `createContext` default). The
328
+ // provider side pushes the value via `emitProvider`. (#1297)
329
+ const ctxSeed = this.generateContextConsumerSeed(ir)
330
+
331
+ // Prop/signal-derived memos with a `null` static SSR default (e.g.
332
+ // `createMemo(() => props.value * 10)`) are computed in-template from the
333
+ // already-seeded prop/signal vars — mirroring Go's generated child
334
+ // constructor. (#1297)
335
+ const memoSeed = this.generateDerivedMemoSeed(ir)
336
+
337
+ const template = `${scriptReg}${ctxSeed}${memoSeed}${templateBody}\n`
280
338
 
281
339
  // Merge collected errors into IR errors
282
340
  if (this.errors.length > 0) {
@@ -389,7 +447,128 @@ export class XslateAdapter extends BaseAdapter implements IRNodeEmitter<XslateRe
389
447
  }
390
448
 
391
449
  emitProvider(node: IRProvider, _ctx: XslateRenderCtx, _emit: EmitIRNode<XslateRenderCtx>): string {
392
- return this.renderChildren(node.children)
450
+ // SSR context propagation (#1297): bracket the children with a
451
+ // provide/revoke pair on the shared controller-stash context stack so a
452
+ // descendant `useContext` consumer reads the value during the same
453
+ // render. Both helpers return '' (empty), so the inline `<: … :>`
454
+ // expression form discards their output cleanly — no extra whitespace,
455
+ // no line-statement needed inside the element body.
456
+ const value = this.providerValueKolon(node.valueProp)
457
+ const children = this.renderChildren(node.children)
458
+ const name = node.contextName
459
+ return (
460
+ `<: $bf.provide_context('${name}', ${value}) :>` +
461
+ children +
462
+ `<: $bf.revoke_context('${name}') :>`
463
+ )
464
+ }
465
+
466
+ /** Lower a `<Ctx.Provider value>` value prop to a Kolon expression. */
467
+ private providerValueKolon(valueProp: IRProvider['valueProp']): string {
468
+ const v = valueProp.value
469
+ if (v.kind === 'literal') {
470
+ return typeof v.value === 'string'
471
+ ? `'${v.value.replace(/[\\']/g, m => `\\${m}`)}'`
472
+ : String(v.value)
473
+ }
474
+ if (v.kind === 'expression') return this.convertExpressionToKolon(v.expr)
475
+ if (v.kind === 'template') return this.convertTemplateLiteralPartsToKolon(v.parts)
476
+ // Out-of-shape value (spread / jsx-children) — nil; consumer defaults.
477
+ return 'nil'
478
+ }
479
+
480
+ /** Kolon literal for a context-consumer's `createContext` default. */
481
+ private contextDefaultKolon(c: ContextConsumer): string {
482
+ const d = c.defaultValue
483
+ if (d === null || d === undefined) return 'nil'
484
+ if (typeof d === 'string') return `'${d.replace(/[\\']/g, m => `\\${m}`)}'`
485
+ if (typeof d === 'boolean') return d ? '1' : '0'
486
+ return String(d)
487
+ }
488
+
489
+ /**
490
+ * Emit one `: my $<local> = $bf.use_context(...)` line-statement per
491
+ * context consumer so the body's bare `$<local>` resolves to the active
492
+ * provider value (or the `createContext` default). (#1297)
493
+ */
494
+ private generateContextConsumerSeed(ir: ComponentIR): string {
495
+ const consumers = collectContextConsumers(ir.metadata)
496
+ if (consumers.length === 0) return ''
497
+ return (
498
+ consumers
499
+ .map(
500
+ c =>
501
+ `: my $${c.localName} = $bf.use_context('${c.contextName}', ${this.contextDefaultKolon(c)});`,
502
+ )
503
+ .join('\n') + '\n'
504
+ )
505
+ }
506
+
507
+ /**
508
+ * Seed memos whose SSR default is `null` (not statically evaluable) by
509
+ * computing them in-template from the already-seeded prop / signal vars
510
+ * (`createMemo(() => props.value * 10)` → `: my $x = $value * 10;`). Without
511
+ * this the memo's `$x` renders empty — the reason
512
+ * `props-reactivity-comparison` was skipped. Only emitted when every var the
513
+ * lowering references is already in scope. (#1297)
514
+ */
515
+ private generateDerivedMemoSeed(ir: ComponentIR): string {
516
+ const memos = ir.metadata.memos ?? []
517
+ const signals = ir.metadata.signals ?? []
518
+ if (memos.length === 0 && signals.length === 0) return ''
519
+ const ssrDefaults = extractSsrDefaults(ir.metadata) ?? {}
520
+ // Props seed first; each signal/memo adds its own name as it lands.
521
+ const available = new Set<string>(ir.metadata.propsParams.map(p => p.name))
522
+ const lines: string[] = []
523
+
524
+ // Prop/signal-derived signals (`createSignal(props.defaultOn ?? false)`):
525
+ // a loop-child render gets no stash seed, so its `$on` would render nil;
526
+ // and the static default can't capture the per-call prop. Seed it
527
+ // in-template when the init lowers cleanly AND references an in-scope var.
528
+ // Object/array/constant inits keep the existing ssr-defaults seeding.
529
+ for (const signal of signals) {
530
+ const kolon = this.tryLowerToKolon(signal.initialValue, available)
531
+ // Kolon can't express `: my $x = … $x …` — declaring `my $x` makes the
532
+ // RHS `$x` an undefined lexical rather than the render var. A same-name
533
+ // signal (`createSignal(props.x ?? d)`, getter == prop) is just the prop
534
+ // with a default, which the harness already seeds correctly from the
535
+ // passed prop — skip the in-template seed for it. (Different-name
536
+ // prop-derived signals like toggle's `on` from `defaultOn` are unaffected.)
537
+ const refsSelf = kolon !== null && new RegExp(`\\$${signal.getter}\\b`).test(kolon)
538
+ if (kolon !== null && !refsSelf) lines.push(`: my $${signal.getter} = ${kolon};`)
539
+ available.add(signal.getter)
540
+ }
541
+
542
+ for (const memo of memos) {
543
+ const def = ssrDefaults[memo.name]
544
+ const isNull = !def || (typeof def === 'object' && 'value' in def && def.value === null)
545
+ if (!isNull) {
546
+ available.add(memo.name)
547
+ continue
548
+ }
549
+ const body = extractArrowBodyExpression(memo.computation)
550
+ if (body === null) continue
551
+ const kolon = this.tryLowerToKolon(body, available)
552
+ if (kolon !== null) lines.push(`: my $${memo.name} = ${kolon};`)
553
+ available.add(memo.name)
554
+ }
555
+ return lines.length > 0 ? lines.join('\n') + '\n' : ''
556
+ }
557
+
558
+ /**
559
+ * Lower a signal init / memo body to Kolon for an in-template SSR seed, or
560
+ * `null` when it shouldn't be seeded this way: not a supported shape
561
+ * (`isSupported` pre-check, so object/array literals don't fail the build),
562
+ * references no in-scope var (a constant — keep ssr-defaults seeding), or
563
+ * references an out-of-scope binding. (#1297)
564
+ */
565
+ private tryLowerToKolon(expr: string, available: ReadonlySet<string>): string | null {
566
+ const trimmed = expr.trim()
567
+ if (!trimmed) return null
568
+ if (!isSupported(parseExpression(trimmed)).supported) return null
569
+ const kolon = this.convertExpressionToKolon(trimmed)
570
+ if (kolon === '' || !/\$[A-Za-z_]\w*/.test(kolon)) return null
571
+ return referencedVarsAreAvailable(kolon, available) ? kolon : null
393
572
  }
394
573
 
395
574
  emitAsync(node: IRAsync, _ctx: XslateRenderCtx, _emit: EmitIRNode<XslateRenderCtx>): string {
@@ -409,6 +588,13 @@ export class XslateAdapter extends BaseAdapter implements IRNodeEmitter<XslateRe
409
588
  if (element.needsScope) {
410
589
  hydrationAttrs += ` ${this.renderScopeMarker('')}`
411
590
  }
591
+ // A root scope element carries `data-key` for a keyed loop item (set on the
592
+ // bf instance by the child renderer from the JSX `key` prop); non-keyed
593
+ // renders add nothing. Mirrors Hono stamping data-key on each loop item's
594
+ // root, including early-return (if-statement) roots. (#1297)
595
+ if (this.rootScopeNodes.has(element) && element.needsScope) {
596
+ hydrationAttrs += ` <: $bf.data_key_attr() | mark_raw :>`
597
+ }
412
598
  if (element.slotId) {
413
599
  hydrationAttrs += ` ${this.renderSlotMarker(element.slotId)}`
414
600
  }
@@ -868,7 +1054,7 @@ export class XslateAdapter extends BaseAdapter implements IRNodeEmitter<XslateRe
868
1054
  return `<: $children | mark_raw :>`
869
1055
  }
870
1056
 
871
- renderAsync(node: IRAsync): string {
1057
+ override renderAsync(node: IRAsync): string {
872
1058
  const fallback = this.renderNode(node.fallback)
873
1059
  const children = this.renderChildren(node.children)
874
1060
  // Capture the fallback into a Kolon macro and pass its rendered HTML to
package/src/build.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  // Text::Xslate build config factory for barefoot.config.ts
2
2
 
3
3
  import type { BuildOptions } from '@barefootjs/jsx'
4
- import { XslateAdapter } from './adapter'
5
- import type { XslateAdapterOptions } from './adapter'
4
+ import { XslateAdapter } from './adapter/index.ts'
5
+ import type { XslateAdapterOptions } from './adapter/index.ts'
6
6
 
7
7
  export interface XslateBuildOptions extends BuildOptions {
8
8
  /** Adapter-specific options passed to XslateAdapter */
package/src/index.ts CHANGED
@@ -4,5 +4,5 @@
4
4
  * Generates Text::Xslate Kolon template files (.tx) from BarefootJS IR.
5
5
  */
6
6
 
7
- export { XslateAdapter, xslateAdapter } from './adapter'
8
- export type { XslateAdapterOptions } from './adapter'
7
+ export { XslateAdapter, xslateAdapter } from './adapter/index.ts'
8
+ export type { XslateAdapterOptions } from './adapter/index.ts'
@@ -87,10 +87,17 @@ export interface RenderOptions {
87
87
  props?: Record<string, unknown>
88
88
  /** Additional component files (filename → source) */
89
89
  components?: Record<string, string>
90
+ /**
91
+ * Explicit component to render when `source` declares multiple
92
+ * exports (e.g. `ReactiveProps.tsx` → `PropsReactivityComparison`).
93
+ * Mirrors the Hono reference's `componentName`; omitted for
94
+ * single-export fixtures, which fall back to the default/first export.
95
+ */
96
+ componentName?: string
90
97
  }
91
98
 
92
99
  export async function renderXslateComponent(options: RenderOptions): Promise<string> {
93
- const { source, adapter, props, components } = options
100
+ const { source, adapter, props, components, componentName: requestedName } = options
94
101
 
95
102
  // Compile child components first.
96
103
  //
@@ -142,7 +149,12 @@ export async function renderXslateComponent(options: RenderOptions): Promise<str
142
149
  const irFiles = result.files.filter(f => f.type === 'ir')
143
150
  if (irFiles.length === 0) throw new Error('No IR output (set outputIR: true)')
144
151
  const irs = irFiles.map(f => JSON.parse(f.content) as ComponentIR)
152
+ // Explicit `componentName` wins (multi-export sources pin the render
153
+ // target); otherwise default-export, first inline-exported, first IR.
154
+ // Mirrors the Hono reference so multi-component fixtures render the
155
+ // same export across adapters.
145
156
  const ir =
157
+ (requestedName ? irs.find(i => i.metadata.componentName === requestedName) : undefined) ??
146
158
  irs.find(i => i.metadata.hasDefaultExport) ??
147
159
  irs.find(i => i.metadata.isExported) ??
148
160
  irs[0]
@@ -355,7 +367,14 @@ function buildChildRenderers(
355
367
  }
356
368
  lines.push(` my $slot_id = delete $child_props->{_bf_slot};`)
357
369
  lines.push(` my $child_bf = BarefootJS->new(undef, { backend => $backend });`)
358
- lines.push(` $child_bf->_scope_id($slot_id ? '${rootChildScopePrefix(snakeName)}' . '_' . $slot_id : '${snakeName}_' . substr(rand() =~ s/^0\\.//r, 0, 6));`)
370
+ // JSX `key` (reserved prop) data-key on the child scope root, for keyed
371
+ // loop reconciliation parity with Hono.
372
+ lines.push(` my $data_key = delete $child_props->{key};`)
373
+ lines.push(` $child_bf->_data_key($data_key) if defined $data_key;`)
374
+ // A loop child (no slot) gets a fresh `<ComponentName>_<rand>` id per
375
+ // iteration — the PascalCase name is what `normalizeHTML` canonicalises to
376
+ // `<ComponentName>_*`; a slotted child derives from the parent scope.
377
+ lines.push(` $child_bf->_scope_id($slot_id ? '${rootChildScopePrefix(snakeName)}' . '_' . $slot_id : '${componentName}_' . substr(rand() =~ s/^0\\.//r, 0, 6));`)
359
378
  lines.push(` $child_bf->_is_child(1);`)
360
379
  lines.push(` if ($slot_id) { $child_bf->_bf_parent('${rootChildScopePrefix(snakeName)}'); $child_bf->_bf_mount($slot_id); }`)
361
380
  lines.push(` $child_bf->_scripts($bf->_scripts);`)