@barefootjs/mojolicious 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.
@@ -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
  }
@@ -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;IACnC;;;;;OAKG;IACH,aAAa,CAAC,EAAE,MAAM,CAAA;CACvB;AAED,wBAAsB,mBAAmB,CAAC,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,MAAM,CAAC,CAgNjF;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.2",
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.2"
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.2"
63
+ "@barefootjs/jsx": "0.9.3"
64
64
  }
65
65
  }
@@ -17,39 +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
- // `toggle-shared`: the parent maps a `ToggleItemProps[]` prop into
26
- // sibling `ToggleItem` children inside a keyed `.map`. Each child's
27
- // `on = props.defaultOn ?? false` signal is never seeded into the
28
- // loop-child stash, so the rendered child template trips Perl strict
29
- // mode (`Global symbol "$on" requires explicit package name`). The
30
- // harness child renderer forwards only the explicitly-passed props;
31
- // it does not run the `_derive_stash_from_defaults` seeding the
32
- // production runtime applies. Plus the loop-child scope id is
33
- // `toggle_item_<rand>` (snake-case template name) rather than the
34
- // `ToggleItem_*` PascalCase the reference pins, and the `key=` →
35
- // `data-key` attribute isn't emitted. Separate follow-up. (xslate
36
- // skips this for the same reasons.)
37
- 'toggle-shared',
38
- // `props-reactivity-comparison` (the `PropsReactivityComparison`
39
- // export of `ReactiveProps.tsx`): componentName selection is now
40
- // honoured (the test-render picks the requested export), but the
41
- // child `PropsStyleChild` has a `displayValue = props.value * 10`
42
- // memo. `extractSsrDefaults` cannot statically evaluate a
43
- // prop-derived expression (it yields `null`), and the Perl SSR model
44
- // seeds child memos from those *static* defaults — so `$displayValue`
45
- // is never declared and the child render aborts under strict mode.
46
- // Go matches only because it generates a child constructor that
47
- // computes the memo from the passed prop at render time; the Perl
48
- // adapters have no equivalent runtime memo-init in the static path
49
- // (an opt-in `signal_init` sub would be required). (xslate skips this
50
- // for the same reason.)
51
- 'props-reactivity-comparison',
52
- ],
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: [],
53
24
  // Per-fixture build-time contracts for shapes the Mojo adapter
54
25
  // intentionally refuses to lower. Owned by this adapter test file
55
26
  // (not by the shared fixtures) so adding a new adapter doesn't
@@ -398,6 +369,122 @@ function C({ value = '' }: { value?: string }) {
398
369
  })
399
370
  })
400
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
+
401
488
  describe('MojoAdapter - Template Generation', () => {
402
489
  test('generates basic element with scope marker', () => {
403
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'
@@ -314,7 +314,7 @@ print $output;
314
314
  */
315
315
  function buildChildRenderers(
316
316
  childTemplates: Map<string, { template: string; ir: ComponentIR }>,
317
- parentIR: ComponentIR,
317
+ _parentIR: ComponentIR,
318
318
  tempDir: string,
319
319
  ): string {
320
320
  if (childTemplates.size === 0) return ''
@@ -326,21 +326,12 @@ function buildChildRenderers(
326
326
  const snakeName = toSnakeCase(componentName)
327
327
  const childTemplatePath = resolve(tempDir, `${snakeName}.html.ep`)
328
328
 
329
- // Compute child scope ID from parent IR
330
- const childSlotIds = findChildSlotIds(parentIR, componentName)
331
-
332
329
  lines.push(`{`)
333
330
  lines.push(` open my $child_fh, '<:utf8', '${childTemplatePath}' or die "Cannot open child template: $!";`)
334
331
  lines.push(` my $child_tmpl = do { local $/; <$child_fh> };`)
335
332
  lines.push(` close $child_fh;`)
336
333
  lines.push(` my $child_mt = Mojo::Template->new(vars => 1, auto_escape => 1);`)
337
334
 
338
- // Track instance counter for multiple-instances support
339
- lines.push(` my $instance_idx = 0;`)
340
- const slotIdsPerl = childSlotIds.length > 0
341
- ? `my @slot_ids = (${childSlotIds.map(id => `'${id}'`).join(', ')}); my $sid = $slot_ids[$instance_idx] // $slot_ids[-1]; $instance_idx++;`
342
- : `my $sid = '${snakeName}';`
343
-
344
335
  lines.push(` $bf->register_child_renderer('${snakeName}', sub {`)
345
336
  lines.push(` my ($child_props) = @_;`)
346
337
  // (#1652) A child that destructures a rest-spread bag
@@ -359,14 +350,22 @@ function buildChildRenderers(
359
350
  const rest = childIR.metadata.restPropsName
360
351
  lines.push(` $child_props->{${rest}} = {} unless defined $child_props->{${rest}};`)
361
352
  }
362
- lines.push(` ${slotIdsPerl}`)
363
353
  lines.push(` my $child_bf = BarefootJS->new($c, {});`)
364
- // Child scope id derives from the PARENT's live scope id, not a literal
365
- // `test_` prefix so `<ReactiveProps_test>_s5` matches Hono / CSR (and
366
- // survives an explicit `__instanceId`). Mirrors xslate's
367
- // `rootChildScopePrefix` (`$bf->_scope_id`); `$bf` is the parent instance
368
- // captured by this renderer closure.
369
- 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(` }`)
370
369
  lines.push(` my $rendered = $child_mt->render($child_tmpl, { %$child_props, bf => $child_bf });`)
371
370
  lines.push(` die $rendered->to_string if ref $rendered;`)
372
371
  lines.push(` chomp $rendered;`)
@@ -379,27 +378,6 @@ function buildChildRenderers(
379
378
  return lines.join('\n')
380
379
  }
381
380
 
382
- /**
383
- * Find slot IDs assigned to a child component in the parent IR.
384
- */
385
- function findChildSlotIds(parentIR: ComponentIR, childName: string): string[] {
386
- const ids: string[] = []
387
- function walk(node: import('@barefootjs/jsx').IRNode): void {
388
- if (node.type === 'component' && node.name === childName && node.slotId) {
389
- ids.push(node.slotId)
390
- }
391
- if ('children' in node && Array.isArray(node.children)) {
392
- for (const child of node.children) walk(child)
393
- }
394
- if (node.type === 'conditional') {
395
- walk(node.whenTrue)
396
- walk(node.whenFalse)
397
- }
398
- }
399
- walk(parentIR.root)
400
- return ids
401
- }
402
-
403
381
  /**
404
382
  * Convert PascalCase to snake_case for Mojo template naming.
405
383
  */