@barefootjs/mojolicious 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
  * Mojolicious EP Template Adapter Exports
3
3
  */
4
- export { MojoAdapter, mojoAdapter } from './mojo-adapter';
5
- export type { MojoAdapterOptions } from './mojo-adapter';
4
+ export { MojoAdapter, mojoAdapter } from './mojo-adapter.ts';
5
+ export type { MojoAdapterOptions } from './mojo-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,WAAW,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAA;AACzD,YAAY,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/adapter/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AAC5D,YAAY,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAA"}
@@ -11,7 +11,10 @@ import {
11
11
  emitAttrValue,
12
12
  augmentInheritedPropAccesses,
13
13
  parseRecordIndexAccess,
14
- evalStringArrayJoin
14
+ evalStringArrayJoin,
15
+ extractArrowBodyExpression,
16
+ collectContextConsumers,
17
+ extractSsrDefaults
15
18
  } from "@barefootjs/jsx";
16
19
 
17
20
  // src/adapter/boolean-result.ts
@@ -103,6 +106,36 @@ function parsePureStringLiteral(source) {
103
106
  function perlHashKey(name) {
104
107
  return /^[A-Za-z_][A-Za-z0-9_]*$/.test(name) ? name : `'${name.replace(/'/g, "\\'")}'`;
105
108
  }
109
+ function collectRootScopeNodes(node) {
110
+ const out = new Set;
111
+ const visit = (n) => {
112
+ if (!n)
113
+ return;
114
+ if (n.type === "element") {
115
+ out.add(n);
116
+ return;
117
+ }
118
+ if (n.type === "if-statement") {
119
+ const s = n;
120
+ visit(s.consequent);
121
+ visit(s.alternate);
122
+ return;
123
+ }
124
+ if (n.type === "fragment") {
125
+ for (const c of n.children)
126
+ visit(c);
127
+ }
128
+ };
129
+ visit(node);
130
+ return out;
131
+ }
132
+ function referencedVarsAreAvailable(expr, available) {
133
+ for (const m of expr.matchAll(/\$([A-Za-z_]\w*)/g)) {
134
+ if (!available.has(m[1]))
135
+ return false;
136
+ }
137
+ return true;
138
+ }
106
139
 
107
140
  class MojoAdapter extends BaseAdapter {
108
141
  name = "mojolicious";
@@ -111,6 +144,7 @@ class MojoAdapter extends BaseAdapter {
111
144
  importMapInjection = "html-snippet";
112
145
  templatePrimitives = MOJO_PRIMITIVE_EMIT_MAP;
113
146
  componentName = "";
147
+ rootScopeNodes = new Set;
114
148
  options;
115
149
  errors = [];
116
150
  inLoop = false;
@@ -152,9 +186,12 @@ class MojoAdapter extends BaseAdapter {
152
186
  if (!options?.siblingTemplatesRegistered) {
153
187
  this.checkImportedLoopChildComponents(ir);
154
188
  }
189
+ this.rootScopeNodes = collectRootScopeNodes(ir.root);
155
190
  const templateBody = ir.root.type === "if-statement" ? this.renderIfStatement(ir.root) : this.renderNode(ir.root);
156
191
  const scriptReg = options?.skipScriptRegistration ? "" : this.generateScriptRegistrations(ir, options?.scriptBaseName);
157
- const template = `${scriptReg}${templateBody}
192
+ const ctxSeed = this.generateContextConsumerSeed(ir);
193
+ const memoSeed = this.generateDerivedMemoSeed(ir);
194
+ const template = `${scriptReg}${ctxSeed}${memoSeed}${templateBody}
158
195
  `;
159
196
  if (this.errors.length > 0) {
160
197
  ir.errors.push(...this.errors);
@@ -240,7 +277,83 @@ class MojoAdapter extends BaseAdapter {
240
277
  return this.renderIfStatement(node);
241
278
  }
242
279
  emitProvider(node, _ctx, _emit) {
243
- return this.renderChildren(node.children);
280
+ const value = this.providerValuePerl(node.valueProp);
281
+ const children = this.renderChildren(node.children);
282
+ const name = node.contextName;
283
+ return `<% bf->provide_context('${name}', ${value}); %>` + children + `<% bf->revoke_context('${name}'); %>`;
284
+ }
285
+ providerValuePerl(valueProp) {
286
+ const v = valueProp.value;
287
+ if (v.kind === "literal") {
288
+ return typeof v.value === "string" ? `'${v.value.replace(/[\\']/g, (m) => `\\${m}`)}'` : String(v.value);
289
+ }
290
+ if (v.kind === "expression")
291
+ return this.convertExpressionToPerl(v.expr);
292
+ if (v.kind === "template")
293
+ return this.convertTemplateLiteralPartsToPerl(v.parts);
294
+ return "undef";
295
+ }
296
+ contextDefaultPerl(c) {
297
+ const d = c.defaultValue;
298
+ if (d === null || d === undefined)
299
+ return "undef";
300
+ if (typeof d === "string")
301
+ return `'${d.replace(/[\\']/g, (m) => `\\${m}`)}'`;
302
+ if (typeof d === "boolean")
303
+ return d ? "1" : "0";
304
+ return String(d);
305
+ }
306
+ generateContextConsumerSeed(ir) {
307
+ const consumers = collectContextConsumers(ir.metadata);
308
+ if (consumers.length === 0)
309
+ return "";
310
+ return consumers.map((c) => `% my $${c.localName} = bf->use_context('${c.contextName}', ${this.contextDefaultPerl(c)});`).join(`
311
+ `) + `
312
+ `;
313
+ }
314
+ generateDerivedMemoSeed(ir) {
315
+ const memos = ir.metadata.memos ?? [];
316
+ const signals = ir.metadata.signals ?? [];
317
+ if (memos.length === 0 && signals.length === 0)
318
+ return "";
319
+ const ssrDefaults = extractSsrDefaults(ir.metadata) ?? {};
320
+ const available = new Set(ir.metadata.propsParams.map((p) => p.name));
321
+ const lines = [];
322
+ for (const signal of signals) {
323
+ const perl = this.tryLowerToPerl(signal.initialValue, available);
324
+ if (perl !== null)
325
+ lines.push(`% my $${signal.getter} = ${perl};`);
326
+ available.add(signal.getter);
327
+ }
328
+ for (const memo of memos) {
329
+ const def = ssrDefaults[memo.name];
330
+ const isNull = !def || typeof def === "object" && "value" in def && def.value === null;
331
+ if (!isNull) {
332
+ available.add(memo.name);
333
+ continue;
334
+ }
335
+ const body = extractArrowBodyExpression(memo.computation);
336
+ if (body === null)
337
+ continue;
338
+ const perl = this.tryLowerToPerl(body, available);
339
+ if (perl !== null)
340
+ lines.push(`% my $${memo.name} = ${perl};`);
341
+ available.add(memo.name);
342
+ }
343
+ return lines.length > 0 ? lines.join(`
344
+ `) + `
345
+ ` : "";
346
+ }
347
+ tryLowerToPerl(expr, available) {
348
+ const trimmed = expr.trim();
349
+ if (!trimmed)
350
+ return null;
351
+ if (!isSupported(parseExpression2(trimmed)).supported)
352
+ return null;
353
+ const perl = this.convertExpressionToPerl(trimmed);
354
+ if (perl === "" || !/\$[A-Za-z_]\w*/.test(perl))
355
+ return null;
356
+ return referencedVarsAreAvailable(perl, available) ? perl : null;
244
357
  }
245
358
  emitAsync(node, _ctx, _emit) {
246
359
  return this.renderAsync(node);
@@ -253,6 +366,9 @@ class MojoAdapter extends BaseAdapter {
253
366
  if (element.needsScope) {
254
367
  hydrationAttrs += ` ${this.renderScopeMarker("")}`;
255
368
  }
369
+ if (this.rootScopeNodes.has(element) && element.needsScope) {
370
+ hydrationAttrs += ` <%== bf->data_key_attr %>`;
371
+ }
256
372
  if (element.slotId) {
257
373
  hydrationAttrs += ` ${this.renderSlotMarker(element.slotId)}`;
258
374
  }
@@ -39,6 +39,12 @@ export declare class MojoAdapter extends BaseAdapter implements IRNodeEmitter<Mo
39
39
  */
40
40
  templatePrimitives: TemplatePrimitiveRegistry;
41
41
  private componentName;
42
+ /** The component's root scope element(s) — each carries `data-key` for a
43
+ * keyed loop item (set by the child renderer from the JSX `key` prop). A
44
+ * plain element root is a single node; an `if-statement` (early-return) root
45
+ * contributes the top element of every branch, since any one of them can be
46
+ * the rendered root at runtime. */
47
+ private rootScopeNodes;
42
48
  private options;
43
49
  private errors;
44
50
  private inLoop;
@@ -140,6 +146,38 @@ export declare class MojoAdapter extends BaseAdapter implements IRNodeEmitter<Mo
140
146
  emitSlot(node: IRSlot): string;
141
147
  emitIfStatement(node: IRIfStatement, _ctx: MojoRenderCtx, _emit: EmitIRNode<MojoRenderCtx>): string;
142
148
  emitProvider(node: IRProvider, _ctx: MojoRenderCtx, _emit: EmitIRNode<MojoRenderCtx>): string;
149
+ /** Lower a `<Ctx.Provider value>` value prop to a Perl expression. */
150
+ private providerValuePerl;
151
+ /** Perl literal for a context-consumer's `createContext` default. */
152
+ private contextDefaultPerl;
153
+ /**
154
+ * Emit one `% my $<local> = bf->use_context(...)` seed line per context
155
+ * consumer so the template body's bare `$<local>` resolves to the active
156
+ * provider value (or the `createContext` default). (#1297)
157
+ */
158
+ private generateContextConsumerSeed;
159
+ /**
160
+ * Seed memos whose SSR default is `null` (not statically evaluable) by
161
+ * computing them in-template from the already-seeded prop / signal vars.
162
+ * Targets the prop-derived memo shape (`createMemo(() => props.value * 10)`)
163
+ * that the static `extractSsrDefaults` evaluator can't fold — without this
164
+ * the memo's `$x` renders empty (the reason `props-reactivity-comparison`
165
+ * was skipped). Only emitted when the lowered expression references vars the
166
+ * template already has in scope (props params + signals + prior memos), so a
167
+ * memo over an out-of-scope binding stays on the null path rather than
168
+ * tripping Perl strict mode. (#1297)
169
+ */
170
+ private generateDerivedMemoSeed;
171
+ /**
172
+ * Lower a signal init / memo body to Perl for an in-template SSR seed, or
173
+ * `null` when it shouldn't be seeded this way. Returns null — without
174
+ * recording a BF101 — when the expression isn't a supported shape
175
+ * (`isSupported` pre-check, so object/array literals don't fail the build),
176
+ * when the lowering references no in-scope var (a constant — keep the
177
+ * existing ssr-defaults seeding), or when it references an out-of-scope
178
+ * binding. (#1297)
179
+ */
180
+ private tryLowerToPerl;
143
181
  emitAsync(node: IRAsync, _ctx: MojoRenderCtx, _emit: EmitIRNode<MojoRenderCtx>): string;
144
182
  renderElement(element: IRElement): string;
145
183
  renderExpression(expr: IRExpression): string;
@@ -1 +1 @@
1
- {"version":3,"file":"mojo-adapter.d.ts","sourceRoot":"","sources":["../../src/adapter/mojo-adapter.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"mojo-adapter.d.ts","sourceRoot":"","sources":["../../src/adapter/mojo-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 { MojoAdapter } from './adapter';
3
- import type { MojoAdapterOptions } from './adapter';
2
+ import { MojoAdapter } from './adapter/index.ts';
3
+ import type { MojoAdapterOptions } from './adapter/index.ts';
4
4
  export interface MojoBuildOptions extends BuildOptions {
5
5
  /** Adapter-specific options passed to MojoAdapter */
6
6
  adapterOptions?: MojoAdapterOptions;
@@ -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,WAAW,EAAE,MAAM,WAAW,CAAA;AACvC,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,WAAW,CAAA;AAEnD,MAAM,WAAW,gBAAiB,SAAQ,YAAY;IACpD,qDAAqD;IACrD,cAAc,CAAC,EAAE,kBAAkB,CAAA;CACpC;AAED;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,OAAO,GAAE,gBAAqB;IAEvD,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,WAAW,EAAE,MAAM,oBAAoB,CAAA;AAChD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAA;AAE5D,MAAM,WAAW,gBAAiB,SAAQ,YAAY;IACpD,qDAAqD;IACrD,cAAc,CAAC,EAAE,kBAAkB,CAAA;CACpC;AAED;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,OAAO,GAAE,gBAAqB;IAEvD,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
@@ -11,7 +11,10 @@ import {
11
11
  emitAttrValue,
12
12
  augmentInheritedPropAccesses,
13
13
  parseRecordIndexAccess,
14
- evalStringArrayJoin
14
+ evalStringArrayJoin,
15
+ extractArrowBodyExpression,
16
+ collectContextConsumers,
17
+ extractSsrDefaults
15
18
  } from "@barefootjs/jsx";
16
19
 
17
20
  // src/adapter/boolean-result.ts
@@ -103,6 +106,36 @@ function parsePureStringLiteral(source) {
103
106
  function perlHashKey(name) {
104
107
  return /^[A-Za-z_][A-Za-z0-9_]*$/.test(name) ? name : `'${name.replace(/'/g, "\\'")}'`;
105
108
  }
109
+ function collectRootScopeNodes(node) {
110
+ const out = new Set;
111
+ const visit = (n) => {
112
+ if (!n)
113
+ return;
114
+ if (n.type === "element") {
115
+ out.add(n);
116
+ return;
117
+ }
118
+ if (n.type === "if-statement") {
119
+ const s = n;
120
+ visit(s.consequent);
121
+ visit(s.alternate);
122
+ return;
123
+ }
124
+ if (n.type === "fragment") {
125
+ for (const c of n.children)
126
+ visit(c);
127
+ }
128
+ };
129
+ visit(node);
130
+ return out;
131
+ }
132
+ function referencedVarsAreAvailable(expr, available) {
133
+ for (const m of expr.matchAll(/\$([A-Za-z_]\w*)/g)) {
134
+ if (!available.has(m[1]))
135
+ return false;
136
+ }
137
+ return true;
138
+ }
106
139
 
107
140
  class MojoAdapter extends BaseAdapter {
108
141
  name = "mojolicious";
@@ -111,6 +144,7 @@ class MojoAdapter extends BaseAdapter {
111
144
  importMapInjection = "html-snippet";
112
145
  templatePrimitives = MOJO_PRIMITIVE_EMIT_MAP;
113
146
  componentName = "";
147
+ rootScopeNodes = new Set;
114
148
  options;
115
149
  errors = [];
116
150
  inLoop = false;
@@ -152,9 +186,12 @@ class MojoAdapter extends BaseAdapter {
152
186
  if (!options?.siblingTemplatesRegistered) {
153
187
  this.checkImportedLoopChildComponents(ir);
154
188
  }
189
+ this.rootScopeNodes = collectRootScopeNodes(ir.root);
155
190
  const templateBody = ir.root.type === "if-statement" ? this.renderIfStatement(ir.root) : this.renderNode(ir.root);
156
191
  const scriptReg = options?.skipScriptRegistration ? "" : this.generateScriptRegistrations(ir, options?.scriptBaseName);
157
- const template = `${scriptReg}${templateBody}
192
+ const ctxSeed = this.generateContextConsumerSeed(ir);
193
+ const memoSeed = this.generateDerivedMemoSeed(ir);
194
+ const template = `${scriptReg}${ctxSeed}${memoSeed}${templateBody}
158
195
  `;
159
196
  if (this.errors.length > 0) {
160
197
  ir.errors.push(...this.errors);
@@ -240,7 +277,83 @@ class MojoAdapter extends BaseAdapter {
240
277
  return this.renderIfStatement(node);
241
278
  }
242
279
  emitProvider(node, _ctx, _emit) {
243
- return this.renderChildren(node.children);
280
+ const value = this.providerValuePerl(node.valueProp);
281
+ const children = this.renderChildren(node.children);
282
+ const name = node.contextName;
283
+ return `<% bf->provide_context('${name}', ${value}); %>` + children + `<% bf->revoke_context('${name}'); %>`;
284
+ }
285
+ providerValuePerl(valueProp) {
286
+ const v = valueProp.value;
287
+ if (v.kind === "literal") {
288
+ return typeof v.value === "string" ? `'${v.value.replace(/[\\']/g, (m) => `\\${m}`)}'` : String(v.value);
289
+ }
290
+ if (v.kind === "expression")
291
+ return this.convertExpressionToPerl(v.expr);
292
+ if (v.kind === "template")
293
+ return this.convertTemplateLiteralPartsToPerl(v.parts);
294
+ return "undef";
295
+ }
296
+ contextDefaultPerl(c) {
297
+ const d = c.defaultValue;
298
+ if (d === null || d === undefined)
299
+ return "undef";
300
+ if (typeof d === "string")
301
+ return `'${d.replace(/[\\']/g, (m) => `\\${m}`)}'`;
302
+ if (typeof d === "boolean")
303
+ return d ? "1" : "0";
304
+ return String(d);
305
+ }
306
+ generateContextConsumerSeed(ir) {
307
+ const consumers = collectContextConsumers(ir.metadata);
308
+ if (consumers.length === 0)
309
+ return "";
310
+ return consumers.map((c) => `% my $${c.localName} = bf->use_context('${c.contextName}', ${this.contextDefaultPerl(c)});`).join(`
311
+ `) + `
312
+ `;
313
+ }
314
+ generateDerivedMemoSeed(ir) {
315
+ const memos = ir.metadata.memos ?? [];
316
+ const signals = ir.metadata.signals ?? [];
317
+ if (memos.length === 0 && signals.length === 0)
318
+ return "";
319
+ const ssrDefaults = extractSsrDefaults(ir.metadata) ?? {};
320
+ const available = new Set(ir.metadata.propsParams.map((p) => p.name));
321
+ const lines = [];
322
+ for (const signal of signals) {
323
+ const perl = this.tryLowerToPerl(signal.initialValue, available);
324
+ if (perl !== null)
325
+ lines.push(`% my $${signal.getter} = ${perl};`);
326
+ available.add(signal.getter);
327
+ }
328
+ for (const memo of memos) {
329
+ const def = ssrDefaults[memo.name];
330
+ const isNull = !def || typeof def === "object" && "value" in def && def.value === null;
331
+ if (!isNull) {
332
+ available.add(memo.name);
333
+ continue;
334
+ }
335
+ const body = extractArrowBodyExpression(memo.computation);
336
+ if (body === null)
337
+ continue;
338
+ const perl = this.tryLowerToPerl(body, available);
339
+ if (perl !== null)
340
+ lines.push(`% my $${memo.name} = ${perl};`);
341
+ available.add(memo.name);
342
+ }
343
+ return lines.length > 0 ? lines.join(`
344
+ `) + `
345
+ ` : "";
346
+ }
347
+ tryLowerToPerl(expr, available) {
348
+ const trimmed = expr.trim();
349
+ if (!trimmed)
350
+ return null;
351
+ if (!isSupported(parseExpression2(trimmed)).supported)
352
+ return null;
353
+ const perl = this.convertExpressionToPerl(trimmed);
354
+ if (perl === "" || !/\$[A-Za-z_]\w*/.test(perl))
355
+ return null;
356
+ return referencedVarsAreAvailable(perl, available) ? perl : null;
244
357
  }
245
358
  emitAsync(node, _ctx, _emit) {
246
359
  return this.renderAsync(node);
@@ -253,6 +366,9 @@ class MojoAdapter extends BaseAdapter {
253
366
  if (element.needsScope) {
254
367
  hydrationAttrs += ` ${this.renderScopeMarker("")}`;
255
368
  }
369
+ if (this.rootScopeNodes.has(element) && element.needsScope) {
370
+ hydrationAttrs += ` <%== bf->data_key_attr %>`;
371
+ }
256
372
  if (element.slotId) {
257
373
  hydrationAttrs += ` ${this.renderSlotMarker(element.slotId)}`;
258
374
  }
package/dist/index.d.ts CHANGED
@@ -3,6 +3,6 @@
3
3
  *
4
4
  * Generates Mojolicious EP template files from BarefootJS IR.
5
5
  */
6
- export { MojoAdapter, mojoAdapter } from './adapter';
7
- export type { MojoAdapterOptions } from './adapter';
6
+ export { MojoAdapter, mojoAdapter } from './adapter/index.ts';
7
+ export type { MojoAdapterOptions } 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,WAAW,EAAE,WAAW,EAAE,MAAM,WAAW,CAAA;AACpD,YAAY,EAAE,kBAAkB,EAAE,MAAM,WAAW,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAA;AAC7D,YAAY,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAA"}
package/dist/index.js CHANGED
@@ -11,7 +11,10 @@ import {
11
11
  emitAttrValue,
12
12
  augmentInheritedPropAccesses,
13
13
  parseRecordIndexAccess,
14
- evalStringArrayJoin
14
+ evalStringArrayJoin,
15
+ extractArrowBodyExpression,
16
+ collectContextConsumers,
17
+ extractSsrDefaults
15
18
  } from "@barefootjs/jsx";
16
19
 
17
20
  // src/adapter/boolean-result.ts
@@ -103,6 +106,36 @@ function parsePureStringLiteral(source) {
103
106
  function perlHashKey(name) {
104
107
  return /^[A-Za-z_][A-Za-z0-9_]*$/.test(name) ? name : `'${name.replace(/'/g, "\\'")}'`;
105
108
  }
109
+ function collectRootScopeNodes(node) {
110
+ const out = new Set;
111
+ const visit = (n) => {
112
+ if (!n)
113
+ return;
114
+ if (n.type === "element") {
115
+ out.add(n);
116
+ return;
117
+ }
118
+ if (n.type === "if-statement") {
119
+ const s = n;
120
+ visit(s.consequent);
121
+ visit(s.alternate);
122
+ return;
123
+ }
124
+ if (n.type === "fragment") {
125
+ for (const c of n.children)
126
+ visit(c);
127
+ }
128
+ };
129
+ visit(node);
130
+ return out;
131
+ }
132
+ function referencedVarsAreAvailable(expr, available) {
133
+ for (const m of expr.matchAll(/\$([A-Za-z_]\w*)/g)) {
134
+ if (!available.has(m[1]))
135
+ return false;
136
+ }
137
+ return true;
138
+ }
106
139
 
107
140
  class MojoAdapter extends BaseAdapter {
108
141
  name = "mojolicious";
@@ -111,6 +144,7 @@ class MojoAdapter extends BaseAdapter {
111
144
  importMapInjection = "html-snippet";
112
145
  templatePrimitives = MOJO_PRIMITIVE_EMIT_MAP;
113
146
  componentName = "";
147
+ rootScopeNodes = new Set;
114
148
  options;
115
149
  errors = [];
116
150
  inLoop = false;
@@ -152,9 +186,12 @@ class MojoAdapter extends BaseAdapter {
152
186
  if (!options?.siblingTemplatesRegistered) {
153
187
  this.checkImportedLoopChildComponents(ir);
154
188
  }
189
+ this.rootScopeNodes = collectRootScopeNodes(ir.root);
155
190
  const templateBody = ir.root.type === "if-statement" ? this.renderIfStatement(ir.root) : this.renderNode(ir.root);
156
191
  const scriptReg = options?.skipScriptRegistration ? "" : this.generateScriptRegistrations(ir, options?.scriptBaseName);
157
- const template = `${scriptReg}${templateBody}
192
+ const ctxSeed = this.generateContextConsumerSeed(ir);
193
+ const memoSeed = this.generateDerivedMemoSeed(ir);
194
+ const template = `${scriptReg}${ctxSeed}${memoSeed}${templateBody}
158
195
  `;
159
196
  if (this.errors.length > 0) {
160
197
  ir.errors.push(...this.errors);
@@ -240,7 +277,83 @@ class MojoAdapter extends BaseAdapter {
240
277
  return this.renderIfStatement(node);
241
278
  }
242
279
  emitProvider(node, _ctx, _emit) {
243
- return this.renderChildren(node.children);
280
+ const value = this.providerValuePerl(node.valueProp);
281
+ const children = this.renderChildren(node.children);
282
+ const name = node.contextName;
283
+ return `<% bf->provide_context('${name}', ${value}); %>` + children + `<% bf->revoke_context('${name}'); %>`;
284
+ }
285
+ providerValuePerl(valueProp) {
286
+ const v = valueProp.value;
287
+ if (v.kind === "literal") {
288
+ return typeof v.value === "string" ? `'${v.value.replace(/[\\']/g, (m) => `\\${m}`)}'` : String(v.value);
289
+ }
290
+ if (v.kind === "expression")
291
+ return this.convertExpressionToPerl(v.expr);
292
+ if (v.kind === "template")
293
+ return this.convertTemplateLiteralPartsToPerl(v.parts);
294
+ return "undef";
295
+ }
296
+ contextDefaultPerl(c) {
297
+ const d = c.defaultValue;
298
+ if (d === null || d === undefined)
299
+ return "undef";
300
+ if (typeof d === "string")
301
+ return `'${d.replace(/[\\']/g, (m) => `\\${m}`)}'`;
302
+ if (typeof d === "boolean")
303
+ return d ? "1" : "0";
304
+ return String(d);
305
+ }
306
+ generateContextConsumerSeed(ir) {
307
+ const consumers = collectContextConsumers(ir.metadata);
308
+ if (consumers.length === 0)
309
+ return "";
310
+ return consumers.map((c) => `% my $${c.localName} = bf->use_context('${c.contextName}', ${this.contextDefaultPerl(c)});`).join(`
311
+ `) + `
312
+ `;
313
+ }
314
+ generateDerivedMemoSeed(ir) {
315
+ const memos = ir.metadata.memos ?? [];
316
+ const signals = ir.metadata.signals ?? [];
317
+ if (memos.length === 0 && signals.length === 0)
318
+ return "";
319
+ const ssrDefaults = extractSsrDefaults(ir.metadata) ?? {};
320
+ const available = new Set(ir.metadata.propsParams.map((p) => p.name));
321
+ const lines = [];
322
+ for (const signal of signals) {
323
+ const perl = this.tryLowerToPerl(signal.initialValue, available);
324
+ if (perl !== null)
325
+ lines.push(`% my $${signal.getter} = ${perl};`);
326
+ available.add(signal.getter);
327
+ }
328
+ for (const memo of memos) {
329
+ const def = ssrDefaults[memo.name];
330
+ const isNull = !def || typeof def === "object" && "value" in def && def.value === null;
331
+ if (!isNull) {
332
+ available.add(memo.name);
333
+ continue;
334
+ }
335
+ const body = extractArrowBodyExpression(memo.computation);
336
+ if (body === null)
337
+ continue;
338
+ const perl = this.tryLowerToPerl(body, available);
339
+ if (perl !== null)
340
+ lines.push(`% my $${memo.name} = ${perl};`);
341
+ available.add(memo.name);
342
+ }
343
+ return lines.length > 0 ? lines.join(`
344
+ `) + `
345
+ ` : "";
346
+ }
347
+ tryLowerToPerl(expr, available) {
348
+ const trimmed = expr.trim();
349
+ if (!trimmed)
350
+ return null;
351
+ if (!isSupported(parseExpression2(trimmed)).supported)
352
+ return null;
353
+ const perl = this.convertExpressionToPerl(trimmed);
354
+ if (perl === "" || !/\$[A-Za-z_]\w*/.test(perl))
355
+ return null;
356
+ return referencedVarsAreAvailable(perl, available) ? perl : null;
244
357
  }
245
358
  emitAsync(node, _ctx, _emit) {
246
359
  return this.renderAsync(node);
@@ -253,6 +366,9 @@ class MojoAdapter extends BaseAdapter {
253
366
  if (element.needsScope) {
254
367
  hydrationAttrs += ` ${this.renderScopeMarker("")}`;
255
368
  }
369
+ if (this.rootScopeNodes.has(element) && element.needsScope) {
370
+ hydrationAttrs += ` <%== bf->data_key_attr %>`;
371
+ }
256
372
  if (element.slotId) {
257
373
  hydrationAttrs += ` ${this.renderSlotMarker(element.slotId)}`;
258
374
  }
@@ -33,6 +33,13 @@ export interface RenderOptions {
33
33
  props?: Record<string, unknown>;
34
34
  /** Additional component files (filename → source) */
35
35
  components?: Record<string, string>;
36
+ /**
37
+ * Explicit component to render when `source` declares multiple
38
+ * exports (e.g. `ReactiveProps.tsx` → `PropsReactivityComparison`).
39
+ * Mirrors the Hono reference's `componentName`; omitted for
40
+ * single-export fixtures, which fall back to the default/first export.
41
+ */
42
+ componentName?: string;
36
43
  }
37
44
  export declare function renderMojoComponent(options: RenderOptions): Promise<string>;
38
45
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"test-render.d.ts","sourceRoot":"","sources":["../src/test-render.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAeH,qBAAa,qBAAsB,SAAQ,KAAK;IAC9C,YAAY,OAAO,EAAE,MAAM,EAG1B;CACF;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAKxE;AAkCD,MAAM,WAAW,aAAa;IAC5B,sBAAsB;IACtB,MAAM,EAAE,MAAM,CAAA;IACd,8BAA8B;IAC9B,OAAO,EAAE,OAAO,iBAAiB,EAAE,eAAe,CAAA;IAClD,iCAAiC;IACjC,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IAC/B,qDAAqD;IACrD,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CACpC;AAED,wBAAsB,mBAAmB,CAAC,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,MAAM,CAAC,CA2MjF;AA+RD;;;GAGG;AACH,wBAAgB,kBAAkB,CAChC,IAAI,EAAE,MAAM,EACZ,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC9B,OAAO,CAuBT"}
1
+ {"version":3,"file":"test-render.d.ts","sourceRoot":"","sources":["../src/test-render.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAeH,qBAAa,qBAAsB,SAAQ,KAAK;IAC9C,YAAY,OAAO,EAAE,MAAM,EAG1B;CACF;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAKxE;AAkCD,MAAM,WAAW,aAAa;IAC5B,sBAAsB;IACtB,MAAM,EAAE,MAAM,CAAA;IACd,8BAA8B;IAC9B,OAAO,EAAE,OAAO,iBAAiB,EAAE,eAAe,CAAA;IAClD,iCAAiC;IACjC,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IAC/B,qDAAqD;IACrD,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IACnC;;;;;OAKG;IACH,aAAa,CAAC,EAAE,MAAM,CAAA;CACvB;AAED,wBAAsB,mBAAmB,CAAC,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,MAAM,CAAC,CAgNjF;AAyQD;;;GAGG;AACH,wBAAgB,kBAAkB,CAChC,IAAI,EAAE,MAAM,EACZ,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC9B,OAAO,CAuBT"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@barefootjs/mojolicious",
3
- "version": "0.9.1",
3
+ "version": "0.9.3",
4
4
  "description": "Mojolicious EP template adapter for BarefootJS - generates .html.ep files from IR",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -52,7 +52,7 @@
52
52
  "directory": "packages/adapter-mojolicious"
53
53
  },
54
54
  "dependencies": {
55
- "@barefootjs/shared": "0.9.1"
55
+ "@barefootjs/shared": "0.9.3"
56
56
  },
57
57
  "peerDependencies": {
58
58
  "@barefootjs/jsx": ">=0.2.0",
@@ -60,6 +60,6 @@
60
60
  },
61
61
  "devDependencies": {
62
62
  "@barefootjs/adapter-tests": "0.1.0",
63
- "@barefootjs/jsx": "0.9.1"
63
+ "@barefootjs/jsx": "0.9.3"
64
64
  }
65
65
  }
@@ -17,20 +17,10 @@ runAdapterConformanceTests({
17
17
  name: 'mojo',
18
18
  factory: () => new MojoAdapter(),
19
19
  render: renderMojoComponent,
20
- skipJsx: [
21
- // SSR context propagation (`<Ctx.Provider value>` `useContext`): the
22
- // template reads a stash key that's never seeded. Implemented on Go; the
23
- // Perl stash-seed path is a follow-up port, so Mojo stays skipped (#1297).
24
- 'context-provider',
25
- // Multi-component shared fixtures with per-item loop state. The child
26
- // scope-id plumbing is now fixed (children derive `<parentScope>_<sN>`
27
- // from `$bf->_scope_id` in `test-render.ts`, which unblocked
28
- // `reactive-props`); these two still diverge on additional per-item
29
- // loop-state seeding the Mojo harness doesn't yet replicate. (xslate
30
- // skips the same pair.)
31
- 'toggle-shared',
32
- 'props-reactivity-comparison',
33
- ],
20
+ // No JSX-render skips: every shared conformance fixture renders to Hono
21
+ // parity on real Mojolicious. Shapes the adapter intentionally refuses at
22
+ // build time are pinned in `expectedDiagnostics` below.
23
+ skipJsx: [],
34
24
  // Per-fixture build-time contracts for shapes the Mojo adapter
35
25
  // intentionally refuses to lower. Owned by this adapter test file
36
26
  // (not by the shared fixtures) so adding a new adapter doesn't
@@ -379,6 +369,122 @@ function C({ value = '' }: { value?: string }) {
379
369
  })
380
370
  })
381
371
 
372
+ describe('MojoAdapter - SSR context propagation (#1297)', () => {
373
+ // `<Ctx.Provider value>` brackets its children with a provide/revoke pair
374
+ // on the shared package-level context stack; descendant `useContext`
375
+ // consumers read it during the same render (mirrors the client
376
+ // `provideContext` / `useContext`).
377
+ test('provider brackets children with provide_context / revoke_context', () => {
378
+ const { template } = compileAndGenerate(`
379
+ 'use client'
380
+ import { createContext, useContext } from '@barefootjs/client'
381
+ const ThemeContext = createContext('light')
382
+ export function ThemeRoot() {
383
+ return <div><ThemeContext.Provider value="dark"><ThemeLabel /></ThemeContext.Provider></div>
384
+ }
385
+ function ThemeLabel() { const theme = useContext(ThemeContext); return <span>{theme}</span> }
386
+ `)
387
+ expect(template).toContain("bf->provide_context('ThemeContext', 'dark');")
388
+ expect(template).toContain("bf->revoke_context('ThemeContext');")
389
+ // The provide precedes the child render, the revoke follows it.
390
+ expect(template.indexOf('provide_context')).toBeLessThan(template.indexOf('render_child'))
391
+ expect(template.indexOf('render_child')).toBeLessThan(template.indexOf('revoke_context'))
392
+ })
393
+
394
+ test('consumer seeds its local from use_context with the createContext default', () => {
395
+ const { template } = compileAndGenerate(`
396
+ 'use client'
397
+ import { createContext, useContext } from '@barefootjs/client'
398
+ const ThemeContext = createContext('light')
399
+ export function ThemeLabel() { const theme = useContext(ThemeContext); return <span>{theme}</span> }
400
+ `)
401
+ expect(template).toContain("% my $theme = bf->use_context('ThemeContext', 'light');")
402
+ })
403
+ })
404
+
405
+ describe('MojoAdapter - prop-derived memo SSR seeding (#1297)', () => {
406
+ // A memo whose body can't be statically folded (`props.value * 10`) gets a
407
+ // `null` SSR default; the adapter computes it in-template from the seeded
408
+ // prop var so the child renders the value instead of empty.
409
+ test('seeds a prop-derived memo from the prop var', () => {
410
+ const { template } = compileAndGenerate(`
411
+ 'use client'
412
+ import { createMemo } from '@barefootjs/client'
413
+ export function Child(props: { value: number }) {
414
+ const displayValue = createMemo(() => props.value * 10)
415
+ return <span>{displayValue()}</span>
416
+ }
417
+ `)
418
+ expect(template).toContain('% my $displayValue = $value * 10;')
419
+ })
420
+
421
+ test('seeds a memo over a destructured prop', () => {
422
+ const { template } = compileAndGenerate(`
423
+ 'use client'
424
+ import { createMemo } from '@barefootjs/client'
425
+ export function Child({ value }: { value: number }) {
426
+ const displayValue = createMemo(() => value * 10)
427
+ return <span>{displayValue()}</span>
428
+ }
429
+ `)
430
+ expect(template).toContain('% my $displayValue = $value * 10;')
431
+ })
432
+ })
433
+
434
+ describe('MojoAdapter - prop-derived signal SSR seeding + data-key (#1297, toggle-shared)', () => {
435
+ // A prop-derived signal (`createSignal(props.defaultOn ?? false)`) is seeded
436
+ // in-template from the passed prop, so a loop child honours its own
437
+ // per-item prop instead of the static default.
438
+ test('seeds a prop-derived signal from the prop var', () => {
439
+ const { template } = compileAndGenerate(`
440
+ 'use client'
441
+ import { createSignal } from '@barefootjs/client'
442
+ export function Item(props: { defaultOn?: boolean }) {
443
+ const [on, setOn] = createSignal(props.defaultOn ?? false)
444
+ return <button>{on() ? 'ON' : 'OFF'}</button>
445
+ }
446
+ `)
447
+ expect(template).toContain('% my $on = ($defaultOn // 0);')
448
+ })
449
+
450
+ // An object/array-valued signal can't lower to Perl and must NOT be seeded
451
+ // in-template (it would record a BF101) — it keeps the existing ssr-defaults
452
+ // seeding.
453
+ test('does not in-template-seed an object-valued signal', () => {
454
+ const { template } = compileAndGenerate(`
455
+ 'use client'
456
+ import { createSignal } from '@barefootjs/client'
457
+ export function Spread() {
458
+ const [attrs, setAttrs] = createSignal<Record<string, string>>({ id: 'a' })
459
+ return <div {...attrs()} />
460
+ }
461
+ `)
462
+ expect(template).not.toContain('my $attrs =')
463
+ })
464
+
465
+ // The component root carries data-key, emitted from the bf instance
466
+ // (render_child sets it from the JSX key); non-keyed renders add nothing.
467
+ test('emits data_key_attr on the component root', () => {
468
+ const { template } = compileAndGenerate(`
469
+ export function Item() { return <div className="x">hi</div> }
470
+ `)
471
+ expect(template).toContain('bf->data_key_attr')
472
+ })
473
+
474
+ // An early-return (if-statement) root has no single root element; data-key
475
+ // must land on each branch's top element so a keyed loop item still stamps it.
476
+ test('emits data_key_attr on each branch root of an if-statement root', () => {
477
+ const { template } = compileAndGenerate(`
478
+ export function Item({ on }: { on?: boolean }) {
479
+ if (on) return <div className="a">A</div>
480
+ return <div className="b">B</div>
481
+ }
482
+ `)
483
+ const count = (template.match(/bf->data_key_attr/g) ?? []).length
484
+ expect(count).toBe(2)
485
+ })
486
+ })
487
+
382
488
  describe('MojoAdapter - Template Generation', () => {
383
489
  test('generates basic element with scope marker', () => {
384
490
  const result = compileAndGenerate(`
@@ -119,11 +119,14 @@ describe('MojoAdapter - Streaming SSR', () => {
119
119
  expect(output).toContain('Please wait...')
120
120
  })
121
121
 
122
- test('renderNode dispatches provider type (transparent)', () => {
122
+ test('renderNode dispatches provider type (brackets children with provide/revoke)', () => {
123
+ // SSR context propagation (#1297): the provider is no longer transparent —
124
+ // it pushes its value before the children and pops it after so a
125
+ // descendant `useContext` consumer reads it during the same render.
123
126
  const providerNode = {
124
127
  type: 'provider' as const,
125
128
  contextName: 'ThemeContext',
126
- valueProp: { name: 'value', value: 'dark', dynamic: false },
129
+ valueProp: { name: 'value', value: { kind: 'literal' as const, value: 'dark' } },
127
130
  children: [
128
131
  { type: 'text' as const, value: 'child content', loc: { file: '', start: { line: 1, column: 0 }, end: { line: 1, column: 0 } } },
129
132
  ],
@@ -131,6 +134,8 @@ describe('MojoAdapter - Streaming SSR', () => {
131
134
  }
132
135
 
133
136
  const output = adapter.renderNode(providerNode)
134
- expect(output).toBe('child content')
137
+ expect(output).toBe(
138
+ "<% bf->provide_context('ThemeContext', 'dark'); %>child content<% bf->revoke_context('ThemeContext'); %>",
139
+ )
135
140
  })
136
141
  })
@@ -2,5 +2,5 @@
2
2
  * Mojolicious EP Template Adapter Exports
3
3
  */
4
4
 
5
- export { MojoAdapter, mojoAdapter } from './mojo-adapter'
6
- export type { MojoAdapterOptions } from './mojo-adapter'
5
+ export { MojoAdapter, mojoAdapter } from './mojo-adapter.ts'
6
+ export type { MojoAdapterOptions } from './mojo-adapter.ts'
@@ -50,8 +50,12 @@ import {
50
50
  augmentInheritedPropAccesses,
51
51
  parseRecordIndexAccess,
52
52
  evalStringArrayJoin,
53
+ extractArrowBodyExpression,
54
+ collectContextConsumers,
55
+ type ContextConsumer,
56
+ extractSsrDefaults,
53
57
  } from '@barefootjs/jsx'
54
- import { isAriaBooleanAttr, isBooleanResultExpr } from './boolean-result'
58
+ import { isAriaBooleanAttr, isBooleanResultExpr } from './boolean-result.ts'
55
59
 
56
60
  /**
57
61
  * Mojo adapter's IRNode render context. Mojo's lowering currently
@@ -161,6 +165,47 @@ function perlHashKey(name: string): string {
161
165
  return /^[A-Za-z_][A-Za-z0-9_]*$/.test(name) ? name : `'${name.replace(/'/g, "\\'")}'`
162
166
  }
163
167
 
168
+ /**
169
+ * Collect the component's root scope element node(s) — the elements that
170
+ * become the rendered root and so carry `data-key` for a keyed loop item. A
171
+ * plain element root is itself; an `if-statement` (early-return) root
172
+ * contributes the top element of each branch (`consequent` + the `alternate`
173
+ * chain), since exactly one branch renders at runtime. Non-element branch
174
+ * tops (fragments / nested shapes) are walked one level so an
175
+ * `if (…) return <A/>` still resolves to `<A>`. (#1297)
176
+ */
177
+ function collectRootScopeNodes(node: IRNode): Set<IRNode> {
178
+ const out = new Set<IRNode>()
179
+ const visit = (n: IRNode | null): void => {
180
+ if (!n) return
181
+ if (n.type === 'element') { out.add(n); return }
182
+ if (n.type === 'if-statement') {
183
+ const s = n as IRIfStatement
184
+ visit(s.consequent)
185
+ visit(s.alternate)
186
+ return
187
+ }
188
+ if (n.type === 'fragment') {
189
+ for (const c of (n as IRFragment).children) visit(c)
190
+ }
191
+ }
192
+ visit(node)
193
+ return out
194
+ }
195
+
196
+ /**
197
+ * True when every `$var` the lowered (Perl / Kolon) expression references is
198
+ * in the available set — i.e. the template already has that var in scope.
199
+ * Guards in-template memo seeding from referencing an out-of-scope binding
200
+ * (which would trip Perl strict mode). (#1297)
201
+ */
202
+ function referencedVarsAreAvailable(expr: string, available: ReadonlySet<string>): boolean {
203
+ for (const m of expr.matchAll(/\$([A-Za-z_]\w*)/g)) {
204
+ if (!available.has(m[1])) return false
205
+ }
206
+ return true
207
+ }
208
+
164
209
  export class MojoAdapter extends BaseAdapter implements IRNodeEmitter<MojoRenderCtx> {
165
210
  name = 'mojolicious'
166
211
  extension = '.html.ep'
@@ -184,6 +229,12 @@ export class MojoAdapter extends BaseAdapter implements IRNodeEmitter<MojoRender
184
229
  templatePrimitives: TemplatePrimitiveRegistry = MOJO_PRIMITIVE_EMIT_MAP
185
230
 
186
231
  private componentName: string = ''
232
+ /** The component's root scope element(s) — each carries `data-key` for a
233
+ * keyed loop item (set by the child renderer from the JSX `key` prop). A
234
+ * plain element root is a single node; an `if-statement` (early-return) root
235
+ * contributes the top element of every branch, since any one of them can be
236
+ * the rendered root at runtime. */
237
+ private rootScopeNodes: Set<IRNode> = new Set()
187
238
  private options: Required<MojoAdapterOptions>
188
239
  private errors: CompilerError[] = []
189
240
  private inLoop: boolean = false
@@ -329,6 +380,7 @@ export class MojoAdapter extends BaseAdapter implements IRNodeEmitter<MojoRender
329
380
  this.checkImportedLoopChildComponents(ir)
330
381
  }
331
382
 
383
+ this.rootScopeNodes = collectRootScopeNodes(ir.root)
332
384
  const templateBody = ir.root.type === 'if-statement'
333
385
  ? this.renderIfStatement(ir.root as IRIfStatement)
334
386
  : this.renderNode(ir.root)
@@ -338,7 +390,20 @@ export class MojoAdapter extends BaseAdapter implements IRNodeEmitter<MojoRender
338
390
  ? ''
339
391
  : this.generateScriptRegistrations(ir, options?.scriptBaseName)
340
392
 
341
- const template = `${scriptReg}${templateBody}\n`
393
+ // SSR context consumers (`const x = useContext(Ctx)`): seed each local
394
+ // from the active provider value (or the `createContext` default) so the
395
+ // body's `$x` resolves. The provider side pushes the value via
396
+ // `emitProvider`; here the consumer reads it. (#1297)
397
+ const ctxSeed = this.generateContextConsumerSeed(ir)
398
+
399
+ // Prop/signal-derived memos that aren't statically evaluable (e.g.
400
+ // `createMemo(() => props.value * 10)`) have a `null` SSR default, so
401
+ // their `$x` would render empty. Compute them in-template from the
402
+ // already-seeded prop/signal vars — mirroring Go's generated child
403
+ // constructor that evaluates the memo from the passed prop. (#1297)
404
+ const memoSeed = this.generateDerivedMemoSeed(ir)
405
+
406
+ const template = `${scriptReg}${ctxSeed}${memoSeed}${templateBody}\n`
342
407
 
343
408
  // Merge collected errors into IR errors
344
409
  if (this.errors.length > 0) {
@@ -480,7 +545,132 @@ export class MojoAdapter extends BaseAdapter implements IRNodeEmitter<MojoRender
480
545
  }
481
546
 
482
547
  emitProvider(node: IRProvider, _ctx: MojoRenderCtx, _emit: EmitIRNode<MojoRenderCtx>): string {
483
- return this.renderChildren(node.children)
548
+ // SSR context propagation (#1297): push the provider value onto the
549
+ // shared controller-stash context stack, render the children (descendant
550
+ // `useContext` consumers read it via `bf->use_context`), then pop. The
551
+ // push/pop bracket the children in the same render so the value scopes
552
+ // exactly to the subtree — mirroring the client `provideContext`.
553
+ const value = this.providerValuePerl(node.valueProp)
554
+ const children = this.renderChildren(node.children)
555
+ const name = node.contextName
556
+ return (
557
+ `<% bf->provide_context('${name}', ${value}); %>` +
558
+ children +
559
+ `<% bf->revoke_context('${name}'); %>`
560
+ )
561
+ }
562
+
563
+ /** Lower a `<Ctx.Provider value>` value prop to a Perl expression. */
564
+ private providerValuePerl(valueProp: IRProvider['valueProp']): string {
565
+ const v = valueProp.value
566
+ if (v.kind === 'literal') {
567
+ return typeof v.value === 'string'
568
+ ? `'${v.value.replace(/[\\']/g, m => `\\${m}`)}'`
569
+ : String(v.value)
570
+ }
571
+ if (v.kind === 'expression') return this.convertExpressionToPerl(v.expr)
572
+ if (v.kind === 'template') return this.convertTemplateLiteralPartsToPerl(v.parts)
573
+ // Out-of-shape value (spread / jsx-children) — render as undef rather
574
+ // than emit invalid Perl; the consumer falls back to its default.
575
+ return 'undef'
576
+ }
577
+
578
+ /** Perl literal for a context-consumer's `createContext` default. */
579
+ private contextDefaultPerl(c: ContextConsumer): string {
580
+ const d = c.defaultValue
581
+ if (d === null || d === undefined) return 'undef'
582
+ if (typeof d === 'string') return `'${d.replace(/[\\']/g, m => `\\${m}`)}'`
583
+ if (typeof d === 'boolean') return d ? '1' : '0'
584
+ return String(d)
585
+ }
586
+
587
+ /**
588
+ * Emit one `% my $<local> = bf->use_context(...)` seed line per context
589
+ * consumer so the template body's bare `$<local>` resolves to the active
590
+ * provider value (or the `createContext` default). (#1297)
591
+ */
592
+ private generateContextConsumerSeed(ir: ComponentIR): string {
593
+ const consumers = collectContextConsumers(ir.metadata)
594
+ if (consumers.length === 0) return ''
595
+ return (
596
+ consumers
597
+ .map(
598
+ c =>
599
+ `% my $${c.localName} = bf->use_context('${c.contextName}', ${this.contextDefaultPerl(c)});`,
600
+ )
601
+ .join('\n') + '\n'
602
+ )
603
+ }
604
+
605
+ /**
606
+ * Seed memos whose SSR default is `null` (not statically evaluable) by
607
+ * computing them in-template from the already-seeded prop / signal vars.
608
+ * Targets the prop-derived memo shape (`createMemo(() => props.value * 10)`)
609
+ * that the static `extractSsrDefaults` evaluator can't fold — without this
610
+ * the memo's `$x` renders empty (the reason `props-reactivity-comparison`
611
+ * was skipped). Only emitted when the lowered expression references vars the
612
+ * template already has in scope (props params + signals + prior memos), so a
613
+ * memo over an out-of-scope binding stays on the null path rather than
614
+ * tripping Perl strict mode. (#1297)
615
+ */
616
+ private generateDerivedMemoSeed(ir: ComponentIR): string {
617
+ const memos = ir.metadata.memos ?? []
618
+ const signals = ir.metadata.signals ?? []
619
+ if (memos.length === 0 && signals.length === 0) return ''
620
+ const ssrDefaults = extractSsrDefaults(ir.metadata) ?? {}
621
+ // Props seed first; each signal/memo adds its own name as it lands so a
622
+ // later one can reference an earlier one.
623
+ const available = new Set<string>(ir.metadata.propsParams.map(p => p.name))
624
+ const lines: string[] = []
625
+
626
+ // Prop/signal-derived signals (`createSignal(props.defaultOn ?? false)`):
627
+ // a loop-child render receives no stash seed for the signal, so its `$on`
628
+ // would trip strict mode; and even when an entry render seeds it, the
629
+ // static default can't capture the per-call prop. Seed it in-template from
630
+ // the passed prop — but ONLY when the init lowers cleanly AND references an
631
+ // in-scope var (i.e. it's genuinely derived). Object/array/constant inits
632
+ // (`createSignal({…})`, `createSignal([…])`, `createSignal('b')`) keep the
633
+ // existing ssr-defaults seeding, so the spread / loop fixtures are
634
+ // untouched.
635
+ for (const signal of signals) {
636
+ const perl = this.tryLowerToPerl(signal.initialValue, available)
637
+ if (perl !== null) lines.push(`% my $${signal.getter} = ${perl};`)
638
+ available.add(signal.getter)
639
+ }
640
+
641
+ for (const memo of memos) {
642
+ const def = ssrDefaults[memo.name]
643
+ const isNull = !def || (typeof def === 'object' && 'value' in def && def.value === null)
644
+ if (!isNull) {
645
+ available.add(memo.name)
646
+ continue
647
+ }
648
+ const body = extractArrowBodyExpression(memo.computation)
649
+ // Block-bodied arrows / non-expression shapes stay on the null path.
650
+ if (body === null) continue
651
+ const perl = this.tryLowerToPerl(body, available)
652
+ if (perl !== null) lines.push(`% my $${memo.name} = ${perl};`)
653
+ available.add(memo.name)
654
+ }
655
+ return lines.length > 0 ? lines.join('\n') + '\n' : ''
656
+ }
657
+
658
+ /**
659
+ * Lower a signal init / memo body to Perl for an in-template SSR seed, or
660
+ * `null` when it shouldn't be seeded this way. Returns null — without
661
+ * recording a BF101 — when the expression isn't a supported shape
662
+ * (`isSupported` pre-check, so object/array literals don't fail the build),
663
+ * when the lowering references no in-scope var (a constant — keep the
664
+ * existing ssr-defaults seeding), or when it references an out-of-scope
665
+ * binding. (#1297)
666
+ */
667
+ private tryLowerToPerl(expr: string, available: ReadonlySet<string>): string | null {
668
+ const trimmed = expr.trim()
669
+ if (!trimmed) return null
670
+ if (!isSupported(parseExpression(trimmed)).supported) return null
671
+ const perl = this.convertExpressionToPerl(trimmed)
672
+ if (perl === '' || !/\$[A-Za-z_]\w*/.test(perl)) return null
673
+ return referencedVarsAreAvailable(perl, available) ? perl : null
484
674
  }
485
675
 
486
676
  emitAsync(node: IRAsync, _ctx: MojoRenderCtx, _emit: EmitIRNode<MojoRenderCtx>): string {
@@ -500,6 +690,14 @@ export class MojoAdapter extends BaseAdapter implements IRNodeEmitter<MojoRender
500
690
  if (element.needsScope) {
501
691
  hydrationAttrs += ` ${this.renderScopeMarker('')}`
502
692
  }
693
+ // A root scope element carries `data-key` for a keyed loop item — emitted
694
+ // from the bf instance (the child renderer sets it from the JSX `key`
695
+ // prop), so a non-keyed render adds nothing. Mirrors Hono stamping
696
+ // data-key on each loop item's scope root, including early-return
697
+ // (if-statement) roots where every branch's top element qualifies. (#1297)
698
+ if (this.rootScopeNodes.has(element) && element.needsScope) {
699
+ hydrationAttrs += ` <%== bf->data_key_attr %>`
700
+ }
503
701
  if (element.slotId) {
504
702
  hydrationAttrs += ` ${this.renderSlotMarker(element.slotId)}`
505
703
  }
@@ -969,7 +1167,7 @@ export class MojoAdapter extends BaseAdapter implements IRNodeEmitter<MojoRender
969
1167
  return `<%= content %>`
970
1168
  }
971
1169
 
972
- renderAsync(node: IRAsync): string {
1170
+ override renderAsync(node: IRAsync): string {
973
1171
  const fallback = this.renderNode(node.fallback)
974
1172
  const children = this.renderChildren(node.children)
975
1173
  // Use the BarefootJS.pm streaming helpers for OOS streaming.
package/src/build.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  // Mojolicious build config factory for barefoot.config.ts
2
2
 
3
3
  import type { BuildOptions } from '@barefootjs/jsx'
4
- import { MojoAdapter } from './adapter'
5
- import type { MojoAdapterOptions } from './adapter'
4
+ import { MojoAdapter } from './adapter/index.ts'
5
+ import type { MojoAdapterOptions } from './adapter/index.ts'
6
6
 
7
7
  export interface MojoBuildOptions extends BuildOptions {
8
8
  /** Adapter-specific options passed to MojoAdapter */
package/src/index.ts CHANGED
@@ -4,5 +4,5 @@
4
4
  * Generates Mojolicious EP template files from BarefootJS IR.
5
5
  */
6
6
 
7
- export { MojoAdapter, mojoAdapter } from './adapter'
8
- export type { MojoAdapterOptions } from './adapter'
7
+ export { MojoAdapter, mojoAdapter } from './adapter/index.ts'
8
+ export type { MojoAdapterOptions } from './adapter/index.ts'
@@ -89,10 +89,17 @@ export interface RenderOptions {
89
89
  props?: Record<string, unknown>
90
90
  /** Additional component files (filename → source) */
91
91
  components?: Record<string, string>
92
+ /**
93
+ * Explicit component to render when `source` declares multiple
94
+ * exports (e.g. `ReactiveProps.tsx` → `PropsReactivityComparison`).
95
+ * Mirrors the Hono reference's `componentName`; omitted for
96
+ * single-export fixtures, which fall back to the default/first export.
97
+ */
98
+ componentName?: string
92
99
  }
93
100
 
94
101
  export async function renderMojoComponent(options: RenderOptions): Promise<string> {
95
- const { source, adapter, props, components } = options
102
+ const { source, adapter, props, components, componentName: requestedName } = options
96
103
 
97
104
  // Compile child components first
98
105
  const childTemplates: Map<string, { template: string; ir: ComponentIR }> = new Map()
@@ -152,7 +159,12 @@ export async function renderMojoComponent(options: RenderOptions): Promise<strin
152
159
  const irFiles = result.files.filter(f => f.type === 'ir')
153
160
  if (irFiles.length === 0) throw new Error('No IR output (set outputIR: true)')
154
161
  const irs = irFiles.map(f => JSON.parse(f.content) as ComponentIR)
162
+ // Explicit `componentName` wins (multi-export sources pin the render
163
+ // target); otherwise default-export, first inline-exported, first IR.
164
+ // Mirrors the Hono reference so multi-component fixtures render the
165
+ // same export across adapters.
155
166
  const ir =
167
+ (requestedName ? irs.find(i => i.metadata.componentName === requestedName) : undefined) ??
156
168
  irs.find(i => i.metadata.hasDefaultExport) ??
157
169
  irs.find(i => i.metadata.isExported) ??
158
170
  irs[0]
@@ -302,7 +314,7 @@ print $output;
302
314
  */
303
315
  function buildChildRenderers(
304
316
  childTemplates: Map<string, { template: string; ir: ComponentIR }>,
305
- parentIR: ComponentIR,
317
+ _parentIR: ComponentIR,
306
318
  tempDir: string,
307
319
  ): string {
308
320
  if (childTemplates.size === 0) return ''
@@ -314,21 +326,12 @@ function buildChildRenderers(
314
326
  const snakeName = toSnakeCase(componentName)
315
327
  const childTemplatePath = resolve(tempDir, `${snakeName}.html.ep`)
316
328
 
317
- // Compute child scope ID from parent IR
318
- const childSlotIds = findChildSlotIds(parentIR, componentName)
319
-
320
329
  lines.push(`{`)
321
330
  lines.push(` open my $child_fh, '<:utf8', '${childTemplatePath}' or die "Cannot open child template: $!";`)
322
331
  lines.push(` my $child_tmpl = do { local $/; <$child_fh> };`)
323
332
  lines.push(` close $child_fh;`)
324
333
  lines.push(` my $child_mt = Mojo::Template->new(vars => 1, auto_escape => 1);`)
325
334
 
326
- // Track instance counter for multiple-instances support
327
- lines.push(` my $instance_idx = 0;`)
328
- const slotIdsPerl = childSlotIds.length > 0
329
- ? `my @slot_ids = (${childSlotIds.map(id => `'${id}'`).join(', ')}); my $sid = $slot_ids[$instance_idx] // $slot_ids[-1]; $instance_idx++;`
330
- : `my $sid = '${snakeName}';`
331
-
332
335
  lines.push(` $bf->register_child_renderer('${snakeName}', sub {`)
333
336
  lines.push(` my ($child_props) = @_;`)
334
337
  // (#1652) A child that destructures a rest-spread bag
@@ -347,14 +350,22 @@ function buildChildRenderers(
347
350
  const rest = childIR.metadata.restPropsName
348
351
  lines.push(` $child_props->{${rest}} = {} unless defined $child_props->{${rest}};`)
349
352
  }
350
- lines.push(` ${slotIdsPerl}`)
351
353
  lines.push(` my $child_bf = BarefootJS->new($c, {});`)
352
- // Child scope id derives from the PARENT's live scope id, not a literal
353
- // `test_` prefix so `<ReactiveProps_test>_s5` matches Hono / CSR (and
354
- // survives an explicit `__instanceId`). Mirrors xslate's
355
- // `rootChildScopePrefix` (`$bf->_scope_id`); `$bf` is the parent instance
356
- // captured by this renderer closure.
357
- lines.push(` $child_bf->_scope_id($bf->_scope_id . "_$sid");`)
354
+ // JSX `key` (reserved prop) data-key on the child's scope root, for
355
+ // keyed-loop reconciliation parity with Hono.
356
+ lines.push(` my $data_key = delete $child_props->{key};`)
357
+ lines.push(` $child_bf->_data_key($data_key) if defined $data_key;`)
358
+ // Scope id: a slotted child (`_bf_slot` passed) derives from the PARENT's
359
+ // live scope id (`<Parent_test>_s5`); a loop child (no slot) gets a fresh
360
+ // `<ComponentName>_<rand>` id per iteration, matching Hono — the
361
+ // PascalCase component name is what `normalizeHTML` canonicalises to
362
+ // `<ComponentName>_*`.
363
+ lines.push(` my $slot = delete $child_props->{_bf_slot};`)
364
+ lines.push(` if (defined $slot) {`)
365
+ lines.push(` $child_bf->_scope_id($bf->_scope_id . "_$slot");`)
366
+ lines.push(` } else {`)
367
+ lines.push(` $child_bf->_scope_id('${componentName}_' . substr(rand() =~ s/^0\\.//r, 0, 6));`)
368
+ lines.push(` }`)
358
369
  lines.push(` my $rendered = $child_mt->render($child_tmpl, { %$child_props, bf => $child_bf });`)
359
370
  lines.push(` die $rendered->to_string if ref $rendered;`)
360
371
  lines.push(` chomp $rendered;`)
@@ -367,27 +378,6 @@ function buildChildRenderers(
367
378
  return lines.join('\n')
368
379
  }
369
380
 
370
- /**
371
- * Find slot IDs assigned to a child component in the parent IR.
372
- */
373
- function findChildSlotIds(parentIR: ComponentIR, childName: string): string[] {
374
- const ids: string[] = []
375
- function walk(node: import('@barefootjs/jsx').IRNode): void {
376
- if (node.type === 'component' && node.name === childName && node.slotId) {
377
- ids.push(node.slotId)
378
- }
379
- if ('children' in node && Array.isArray(node.children)) {
380
- for (const child of node.children) walk(child)
381
- }
382
- if (node.type === 'conditional') {
383
- walk(node.whenTrue)
384
- walk(node.whenFalse)
385
- }
386
- }
387
- walk(parentIR.root)
388
- return ids
389
- }
390
-
391
381
  /**
392
382
  * Convert PascalCase to snake_case for Mojo template naming.
393
383
  */