@barefootjs/xslate 0.9.2 → 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.
- package/dist/adapter/index.d.ts +2 -2
- package/dist/adapter/index.d.ts.map +1 -1
- package/dist/adapter/index.js +120 -3
- package/dist/adapter/xslate-adapter.d.ts +32 -0
- package/dist/adapter/xslate-adapter.d.ts.map +1 -1
- package/dist/build.d.ts +2 -2
- package/dist/build.d.ts.map +1 -1
- package/dist/build.js +120 -3
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +120 -3
- package/package.json +3 -3
- package/src/__tests__/xslate-adapter.test.ts +134 -39
- package/src/__tests__/xslate-counter.test.ts +4 -1
- package/src/adapter/index.ts +2 -2
- package/src/adapter/xslate-adapter.ts +190 -4
- package/src/build.ts +2 -2
- package/src/index.ts +2 -2
- package/src/test-render.ts +8 -1
package/dist/adapter/index.d.ts
CHANGED
|
@@ -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,
|
|
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"}
|
package/dist/adapter/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
|
|
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
|
-
|
|
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":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,KAAK,EACV,WAAW,EACX,MAAM,EACN,SAAS,EACT,MAAM,EACN,YAAY,EACZ,aAAa,EACb,MAAM,EACN,WAAW,EACX,UAAU,EACV,MAAM,EACN,aAAa,EACb,UAAU,EACV,OAAO,EAKP,yBAAyB,EAE1B,MAAM,iBAAiB,CAAA;AACxB,OAAO,EACL,WAAW,EACX,KAAK,aAAa,EAClB,KAAK,sBAAsB,EAM3B,KAAK,aAAa,EAClB,KAAK,UAAU,
|
|
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;
|
package/dist/build.d.ts.map
CHANGED
|
@@ -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,
|
|
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
|
|
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
|
-
|
|
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
|
package/dist/index.d.ts.map
CHANGED
|
@@ -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,
|
|
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
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
65
|
+
"@barefootjs/jsx": "0.9.3",
|
|
66
66
|
"typescript": "^5.0.0"
|
|
67
67
|
}
|
|
68
68
|
}
|
|
@@ -12,56 +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
|
-
//
|
|
27
|
-
//
|
|
28
|
-
// `
|
|
29
|
-
|
|
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
|
-
// `toggle-shared`: the parent maps a `ToggleItemProps[]` prop into
|
|
41
|
-
// sibling `ToggleItem` children inside a keyed `.map`. Three gaps
|
|
42
|
-
// remain (same as mojo): the loop-child `on = props.defaultOn ??
|
|
43
|
-
// false` signal isn't seeded server-side (so every item renders OFF
|
|
44
|
-
// instead of honouring per-item `defaultOn`), the child scope id is
|
|
45
|
-
// the snake-case `toggle_item_<rand>` rather than the `ToggleItem_*`
|
|
46
|
-
// PascalCase the reference pins, and `key=` → `data-key` isn't
|
|
47
|
-
// emitted. Kolon resolves the unseeded vars to nil rather than
|
|
48
|
-
// aborting, so this surfaces as a render mismatch (not a hard error).
|
|
49
|
-
// Separate follow-up.
|
|
50
|
-
'toggle-shared',
|
|
51
|
-
// `props-reactivity-comparison` (the `PropsReactivityComparison`
|
|
52
|
-
// export of `ReactiveProps.tsx`): componentName selection is now
|
|
53
|
-
// honoured, but the child `PropsStyleChild`'s `displayValue =
|
|
54
|
-
// props.value * 10` memo has no static SSR default
|
|
55
|
-
// (`extractSsrDefaults` → `null` for a prop-derived expression) and
|
|
56
|
-
// the Perl SSR model seeds child memos from static defaults. Kolon
|
|
57
|
-
// renders the unseeded `$displayValue` as empty, so `child-computed-
|
|
58
|
-
// value` is blank where Hono / Go emit `10` (Go computes it in a
|
|
59
|
-
// generated child constructor — the Perl static path has no
|
|
60
|
-
// equivalent). (Same reason mojo skips.)
|
|
61
|
-
'props-reactivity-comparison',
|
|
62
|
-
// (`kbd` is not skipped here — it's a BF101 refusal pinned in
|
|
63
|
-
// `expectedDiagnostics` below, not a render-mismatch.)
|
|
64
|
-
],
|
|
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: [],
|
|
65
32
|
// Per-fixture build-time contracts for shapes the Xslate adapter
|
|
66
33
|
// intentionally refuses to lower. Mirrors mojo's set — the lowering
|
|
67
34
|
// gates are shared code paths in the ported adapter.
|
|
@@ -143,3 +110,131 @@ runAdapterConformanceTests({
|
|
|
143
110
|
return false
|
|
144
111
|
},
|
|
145
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
|
-
|
|
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.
|
package/src/adapter/index.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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'
|
package/src/test-render.ts
CHANGED
|
@@ -367,7 +367,14 @@ function buildChildRenderers(
|
|
|
367
367
|
}
|
|
368
368
|
lines.push(` my $slot_id = delete $child_props->{_bf_slot};`)
|
|
369
369
|
lines.push(` my $child_bf = BarefootJS->new(undef, { backend => $backend });`)
|
|
370
|
-
|
|
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));`)
|
|
371
378
|
lines.push(` $child_bf->_is_child(1);`)
|
|
372
379
|
lines.push(` if ($slot_id) { $child_bf->_bf_parent('${rootChildScopePrefix(snakeName)}'); $child_bf->_bf_mount($slot_id); }`)
|
|
373
380
|
lines.push(` $child_bf->_scripts($bf->_scripts);`)
|