@barefootjs/mojolicious 0.6.1 → 0.7.0

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,4 +1,5 @@
1
1
  // src/adapter/mojo-adapter.ts
2
+ import ts from "typescript";
2
3
  import {
3
4
  BaseAdapter,
4
5
  isBooleanAttr,
@@ -80,6 +81,22 @@ function resolveJsxChildrenProp(props) {
80
81
  return [];
81
82
  return prop.value.children;
82
83
  }
84
+ function parsePureStringLiteral(source) {
85
+ const sf = ts.createSourceFile("__const.ts", `const __x = (${source});`, ts.ScriptTarget.Latest, false);
86
+ const stmt = sf.statements[0];
87
+ if (!stmt || !ts.isVariableStatement(stmt))
88
+ return null;
89
+ const decl = stmt.declarationList.declarations[0];
90
+ let init = decl?.initializer;
91
+ while (init && ts.isParenthesizedExpression(init))
92
+ init = init.expression;
93
+ if (!init)
94
+ return null;
95
+ if (ts.isStringLiteral(init) || ts.isNoSubstitutionTemplateLiteral(init)) {
96
+ return init.text;
97
+ }
98
+ return null;
99
+ }
83
100
 
84
101
  class MojoAdapter extends BaseAdapter {
85
102
  name = "mojolicious";
@@ -94,6 +111,8 @@ class MojoAdapter extends BaseAdapter {
94
111
  propsObjectName = null;
95
112
  propsParams = [];
96
113
  stringValueNames = new Set;
114
+ moduleStringConsts = new Map;
115
+ loopBoundNames = new Map;
97
116
  constructor(options = {}) {
98
117
  super();
99
118
  this.options = {
@@ -115,6 +134,8 @@ class MojoAdapter extends BaseAdapter {
115
134
  if (isStringTypeInfo(p.type))
116
135
  this.stringValueNames.add(p.name);
117
136
  }
137
+ this.moduleStringConsts = this.collectModuleStringConsts(ir.metadata.localConstants);
138
+ this.loopBoundNames.clear();
118
139
  this.errors = [];
119
140
  this.childrenCaptureCounter = 0;
120
141
  if (!options?.siblingTemplatesRegistered) {
@@ -139,6 +160,27 @@ class MojoAdapter extends BaseAdapter {
139
160
  extension: this.extension
140
161
  };
141
162
  }
163
+ collectModuleStringConsts(constants) {
164
+ const map = new Map;
165
+ for (const c of constants ?? []) {
166
+ if (!c.isModule)
167
+ continue;
168
+ if (c.value === undefined)
169
+ continue;
170
+ const literal = parsePureStringLiteral(c.value);
171
+ if (literal !== null)
172
+ map.set(c.name, literal);
173
+ }
174
+ return map;
175
+ }
176
+ resolveModuleStringConst(name) {
177
+ if (this.loopBoundNames.has(name))
178
+ return null;
179
+ const value = this.moduleStringConsts.get(name);
180
+ if (value === undefined)
181
+ return null;
182
+ return `'${value.replace(/[\\']/g, (m) => `\\${m}`)}'`;
183
+ }
142
184
  generateScriptRegistrations(ir, scriptBaseName) {
143
185
  const hasInteractivity = this.hasClientInteractivity(ir);
144
186
  if (!hasInteractivity)
@@ -405,6 +447,10 @@ ${whenTrue}
405
447
  }
406
448
  const param = loop.param;
407
449
  const indexVar = loop.iterationShape === "keys" ? `$${param}` : loop.index ? `$${loop.index}` : "$_i";
450
+ const loopBound = loop.iterationShape === "keys" ? [param] : [param, loop.index ?? "_i"];
451
+ for (const n of loopBound) {
452
+ this.loopBoundNames.set(n, (this.loopBoundNames.get(n) ?? 0) + 1);
453
+ }
408
454
  const prevInLoop = this.inLoop;
409
455
  this.inLoop = true;
410
456
  const renderedChildren = this.renderChildren(loop.children);
@@ -438,6 +484,13 @@ ${renderedChildren}` : renderedChildren;
438
484
  } else {
439
485
  lines.push(children);
440
486
  }
487
+ for (const n of loopBound) {
488
+ const c = (this.loopBoundNames.get(n) ?? 1) - 1;
489
+ if (c <= 0)
490
+ this.loopBoundNames.delete(n);
491
+ else
492
+ this.loopBoundNames.set(n, c);
493
+ }
441
494
  lines.push(`% }`);
442
495
  lines.push(`<%== bf->comment("/loop:${loop.markerId}") %>`);
443
496
  return lines.join(`
@@ -1056,6 +1109,9 @@ class MojoTopLevelEmitter {
1056
1109
  this.adapter = adapter;
1057
1110
  }
1058
1111
  identifier(name) {
1112
+ const inlined = this.adapter.resolveModuleStringConst(name);
1113
+ if (inlined !== null)
1114
+ return inlined;
1059
1115
  return `$${name}`;
1060
1116
  }
1061
1117
  literal(value, literalType) {
@@ -58,8 +58,45 @@ export declare class MojoAdapter extends BaseAdapter implements IRNodeEmitter<Mo
58
58
  * true — selecting the string operator from the operand's type avoids that.
59
59
  */
60
60
  private stringValueNames;
61
+ /**
62
+ * Module-scope pure string-literal constants (`const X = 'literal'` at
63
+ * file top-level), keyed by name → resolved literal value. Populated at
64
+ * `generate()` entry from `ir.metadata.localConstants`. When an identifier
65
+ * in an expression resolves to one of these, the adapter inlines the
66
+ * literal instead of emitting `$X` against a stash variable that is never
67
+ * bound (a module const isn't a prop, signal, or local — the value would
68
+ * render empty). Hono inlines it for free; this restores parity. Only
69
+ * module-scope pure string literals qualify (see `collectModuleStringConsts`).
70
+ */
71
+ private moduleStringConsts;
72
+ /**
73
+ * Names currently bound by an enclosing loop body — the `my $<param>` and
74
+ * `my $<index>` bindings `renderLoop` introduces — ref-counted so nested
75
+ * loops compose. `resolveModuleStringConst` consults this so a loop
76
+ * variable whose name happens to match a module string const is NOT
77
+ * inlined as the const literal (mirrors the Go adapter's loop-param /
78
+ * loop-var shadowing guards). (#1749 review)
79
+ */
80
+ private loopBoundNames;
61
81
  constructor(options?: MojoAdapterOptions);
62
82
  generate(ir: ComponentIR, options?: AdapterGenerateOptions): AdapterOutput;
83
+ /**
84
+ * Build the module pure-string-const map from the IR's localConstants.
85
+ * A const qualifies only when it is module-scope (`isModule`) and its
86
+ * initializer parses to a single string literal (`ts.StringLiteral` or
87
+ * `ts.NoSubstitutionTemplateLiteral`). Template literals with `${}`,
88
+ * numeric/object initializers, `Record<T,string>` maps, memos, and
89
+ * signals are all excluded — only a pure compile-time string can be
90
+ * inlined byte-for-byte.
91
+ */
92
+ private collectModuleStringConsts;
93
+ /**
94
+ * Resolve an identifier to its inlined Perl single-quoted string literal
95
+ * when it names a module pure-string const, else `null` (the caller then
96
+ * falls back to its normal `$name` stash lowering). Returns the Perl
97
+ * literal form `'<escaped>'` ready to drop into an expression.
98
+ */
99
+ resolveModuleStringConst(name: string): string | null;
63
100
  private generateScriptRegistrations;
64
101
  private hasClientInteractivity;
65
102
  /**
@@ -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.js CHANGED
@@ -1,4 +1,5 @@
1
1
  // src/adapter/mojo-adapter.ts
2
+ import ts from "typescript";
2
3
  import {
3
4
  BaseAdapter,
4
5
  isBooleanAttr,
@@ -80,6 +81,22 @@ function resolveJsxChildrenProp(props) {
80
81
  return [];
81
82
  return prop.value.children;
82
83
  }
84
+ function parsePureStringLiteral(source) {
85
+ const sf = ts.createSourceFile("__const.ts", `const __x = (${source});`, ts.ScriptTarget.Latest, false);
86
+ const stmt = sf.statements[0];
87
+ if (!stmt || !ts.isVariableStatement(stmt))
88
+ return null;
89
+ const decl = stmt.declarationList.declarations[0];
90
+ let init = decl?.initializer;
91
+ while (init && ts.isParenthesizedExpression(init))
92
+ init = init.expression;
93
+ if (!init)
94
+ return null;
95
+ if (ts.isStringLiteral(init) || ts.isNoSubstitutionTemplateLiteral(init)) {
96
+ return init.text;
97
+ }
98
+ return null;
99
+ }
83
100
 
84
101
  class MojoAdapter extends BaseAdapter {
85
102
  name = "mojolicious";
@@ -94,6 +111,8 @@ class MojoAdapter extends BaseAdapter {
94
111
  propsObjectName = null;
95
112
  propsParams = [];
96
113
  stringValueNames = new Set;
114
+ moduleStringConsts = new Map;
115
+ loopBoundNames = new Map;
97
116
  constructor(options = {}) {
98
117
  super();
99
118
  this.options = {
@@ -115,6 +134,8 @@ class MojoAdapter extends BaseAdapter {
115
134
  if (isStringTypeInfo(p.type))
116
135
  this.stringValueNames.add(p.name);
117
136
  }
137
+ this.moduleStringConsts = this.collectModuleStringConsts(ir.metadata.localConstants);
138
+ this.loopBoundNames.clear();
118
139
  this.errors = [];
119
140
  this.childrenCaptureCounter = 0;
120
141
  if (!options?.siblingTemplatesRegistered) {
@@ -139,6 +160,27 @@ class MojoAdapter extends BaseAdapter {
139
160
  extension: this.extension
140
161
  };
141
162
  }
163
+ collectModuleStringConsts(constants) {
164
+ const map = new Map;
165
+ for (const c of constants ?? []) {
166
+ if (!c.isModule)
167
+ continue;
168
+ if (c.value === undefined)
169
+ continue;
170
+ const literal = parsePureStringLiteral(c.value);
171
+ if (literal !== null)
172
+ map.set(c.name, literal);
173
+ }
174
+ return map;
175
+ }
176
+ resolveModuleStringConst(name) {
177
+ if (this.loopBoundNames.has(name))
178
+ return null;
179
+ const value = this.moduleStringConsts.get(name);
180
+ if (value === undefined)
181
+ return null;
182
+ return `'${value.replace(/[\\']/g, (m) => `\\${m}`)}'`;
183
+ }
142
184
  generateScriptRegistrations(ir, scriptBaseName) {
143
185
  const hasInteractivity = this.hasClientInteractivity(ir);
144
186
  if (!hasInteractivity)
@@ -405,6 +447,10 @@ ${whenTrue}
405
447
  }
406
448
  const param = loop.param;
407
449
  const indexVar = loop.iterationShape === "keys" ? `$${param}` : loop.index ? `$${loop.index}` : "$_i";
450
+ const loopBound = loop.iterationShape === "keys" ? [param] : [param, loop.index ?? "_i"];
451
+ for (const n of loopBound) {
452
+ this.loopBoundNames.set(n, (this.loopBoundNames.get(n) ?? 0) + 1);
453
+ }
408
454
  const prevInLoop = this.inLoop;
409
455
  this.inLoop = true;
410
456
  const renderedChildren = this.renderChildren(loop.children);
@@ -438,6 +484,13 @@ ${renderedChildren}` : renderedChildren;
438
484
  } else {
439
485
  lines.push(children);
440
486
  }
487
+ for (const n of loopBound) {
488
+ const c = (this.loopBoundNames.get(n) ?? 1) - 1;
489
+ if (c <= 0)
490
+ this.loopBoundNames.delete(n);
491
+ else
492
+ this.loopBoundNames.set(n, c);
493
+ }
441
494
  lines.push(`% }`);
442
495
  lines.push(`<%== bf->comment("/loop:${loop.markerId}") %>`);
443
496
  return lines.join(`
@@ -1056,6 +1109,9 @@ class MojoTopLevelEmitter {
1056
1109
  this.adapter = adapter;
1057
1110
  }
1058
1111
  identifier(name) {
1112
+ const inlined = this.adapter.resolveModuleStringConst(name);
1113
+ if (inlined !== null)
1114
+ return inlined;
1059
1115
  return `$${name}`;
1060
1116
  }
1061
1117
  literal(value, literalType) {
package/dist/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  // src/adapter/mojo-adapter.ts
2
+ import ts from "typescript";
2
3
  import {
3
4
  BaseAdapter,
4
5
  isBooleanAttr,
@@ -80,6 +81,22 @@ function resolveJsxChildrenProp(props) {
80
81
  return [];
81
82
  return prop.value.children;
82
83
  }
84
+ function parsePureStringLiteral(source) {
85
+ const sf = ts.createSourceFile("__const.ts", `const __x = (${source});`, ts.ScriptTarget.Latest, false);
86
+ const stmt = sf.statements[0];
87
+ if (!stmt || !ts.isVariableStatement(stmt))
88
+ return null;
89
+ const decl = stmt.declarationList.declarations[0];
90
+ let init = decl?.initializer;
91
+ while (init && ts.isParenthesizedExpression(init))
92
+ init = init.expression;
93
+ if (!init)
94
+ return null;
95
+ if (ts.isStringLiteral(init) || ts.isNoSubstitutionTemplateLiteral(init)) {
96
+ return init.text;
97
+ }
98
+ return null;
99
+ }
83
100
 
84
101
  class MojoAdapter extends BaseAdapter {
85
102
  name = "mojolicious";
@@ -94,6 +111,8 @@ class MojoAdapter extends BaseAdapter {
94
111
  propsObjectName = null;
95
112
  propsParams = [];
96
113
  stringValueNames = new Set;
114
+ moduleStringConsts = new Map;
115
+ loopBoundNames = new Map;
97
116
  constructor(options = {}) {
98
117
  super();
99
118
  this.options = {
@@ -115,6 +134,8 @@ class MojoAdapter extends BaseAdapter {
115
134
  if (isStringTypeInfo(p.type))
116
135
  this.stringValueNames.add(p.name);
117
136
  }
137
+ this.moduleStringConsts = this.collectModuleStringConsts(ir.metadata.localConstants);
138
+ this.loopBoundNames.clear();
118
139
  this.errors = [];
119
140
  this.childrenCaptureCounter = 0;
120
141
  if (!options?.siblingTemplatesRegistered) {
@@ -139,6 +160,27 @@ class MojoAdapter extends BaseAdapter {
139
160
  extension: this.extension
140
161
  };
141
162
  }
163
+ collectModuleStringConsts(constants) {
164
+ const map = new Map;
165
+ for (const c of constants ?? []) {
166
+ if (!c.isModule)
167
+ continue;
168
+ if (c.value === undefined)
169
+ continue;
170
+ const literal = parsePureStringLiteral(c.value);
171
+ if (literal !== null)
172
+ map.set(c.name, literal);
173
+ }
174
+ return map;
175
+ }
176
+ resolveModuleStringConst(name) {
177
+ if (this.loopBoundNames.has(name))
178
+ return null;
179
+ const value = this.moduleStringConsts.get(name);
180
+ if (value === undefined)
181
+ return null;
182
+ return `'${value.replace(/[\\']/g, (m) => `\\${m}`)}'`;
183
+ }
142
184
  generateScriptRegistrations(ir, scriptBaseName) {
143
185
  const hasInteractivity = this.hasClientInteractivity(ir);
144
186
  if (!hasInteractivity)
@@ -405,6 +447,10 @@ ${whenTrue}
405
447
  }
406
448
  const param = loop.param;
407
449
  const indexVar = loop.iterationShape === "keys" ? `$${param}` : loop.index ? `$${loop.index}` : "$_i";
450
+ const loopBound = loop.iterationShape === "keys" ? [param] : [param, loop.index ?? "_i"];
451
+ for (const n of loopBound) {
452
+ this.loopBoundNames.set(n, (this.loopBoundNames.get(n) ?? 0) + 1);
453
+ }
408
454
  const prevInLoop = this.inLoop;
409
455
  this.inLoop = true;
410
456
  const renderedChildren = this.renderChildren(loop.children);
@@ -438,6 +484,13 @@ ${renderedChildren}` : renderedChildren;
438
484
  } else {
439
485
  lines.push(children);
440
486
  }
487
+ for (const n of loopBound) {
488
+ const c = (this.loopBoundNames.get(n) ?? 1) - 1;
489
+ if (c <= 0)
490
+ this.loopBoundNames.delete(n);
491
+ else
492
+ this.loopBoundNames.set(n, c);
493
+ }
441
494
  lines.push(`% }`);
442
495
  lines.push(`<%== bf->comment("/loop:${loop.markerId}") %>`);
443
496
  return lines.join(`
@@ -1056,6 +1109,9 @@ class MojoTopLevelEmitter {
1056
1109
  this.adapter = adapter;
1057
1110
  }
1058
1111
  identifier(name) {
1112
+ const inlined = this.adapter.resolveModuleStringConst(name);
1113
+ if (inlined !== null)
1114
+ return inlined;
1059
1115
  return `$${name}`;
1060
1116
  }
1061
1117
  literal(value, literalType) {
@@ -1 +1 @@
1
- {"version":3,"file":"test-render.d.ts","sourceRoot":"","sources":["../src/test-render.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAUH,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;AAgND;;;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;CACpC;AAED,wBAAsB,mBAAmB,CAAC,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,MAAM,CAAC,CA2MjF;AAsOD;;;GAGG;AACH,wBAAgB,kBAAkB,CAChC,IAAI,EAAE,MAAM,EACZ,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC9B,OAAO,CAuBT"}
@@ -0,0 +1,91 @@
1
+ package BarefootJS::Backend::Mojo;
2
+ our $VERSION = "0.01";
3
+ use Mojo::Base -base, -signatures;
4
+
5
+ use Mojo::ByteStream qw(b);
6
+ use Mojo::JSON qw(to_json);
7
+ use Scalar::Util qw(weaken);
8
+
9
+ # ---------------------------------------------------------------------------
10
+ # Reference rendering backend (Mojolicious / Mojo::Template).
11
+ # ---------------------------------------------------------------------------
12
+ #
13
+ # BarefootJS.pm holds all the template-engine-agnostic logic (the JS-compat
14
+ # value helpers, array/string methods, hydration markers). Everything that is
15
+ # specific to *how a template is rendered* — JSON marshalling, raw-string
16
+ # marking, JSX-children materialisation, and named-template rendering — lives
17
+ # behind this backend object so the same runtime can drive a different Perl
18
+ # template engine (Text::Xslate, Template Toolkit, …) without rewriting the
19
+ # helper surface.
20
+ #
21
+ # A backend MUST implement:
22
+ # - encode_json($data) -> string
23
+ # - mark_raw($str) -> value the engine emits without escaping
24
+ # - materialize($value) -> string (resolve a captured-children ref)
25
+ # - render_named($name, $bf, \%vars) -> string
26
+ #
27
+ # This Mojo implementation is the reference. To target another engine, write a
28
+ # sibling backend (BarefootJS::Backend::Xslate, …) implementing the same four
29
+ # methods and pass it via `BarefootJS->new($c, { backend => $b })`.
30
+
31
+ # The Mojolicious controller. Optional: the value-marshalling helpers
32
+ # (`encode_json` / `mark_raw` / `materialize`) work without it; only
33
+ # `render_named` reaches into the controller's renderer + stash.
34
+ has 'c';
35
+
36
+ # Pluggable JSON encoder (#engine-abstraction). Defaults to
37
+ # `Mojo::JSON::to_json`, which returns a *character* string (not bytes)
38
+ # suitable for embedding in HTML output via `<%==` / Mojo::ByteStream.
39
+ #
40
+ # Override with any `sub ($data) { ... }` to swap in a faster XS encoder —
41
+ # e.g. `json_encoder => sub { Cpanel::JSON::XS->new->canonical->encode($_[0]) }`.
42
+ # The pure-Perl JSON::PP fallback Mojo::JSON uses can be a hot spot for large
43
+ # props payloads; the seam lets a host pick its own implementation without
44
+ # touching the runtime.
45
+ has 'json_encoder' => sub { \&to_json };
46
+
47
+ # Hold the controller weakly for the same reason BarefootJS does: the
48
+ # controller owns the bf instance (which owns this backend) via its stash,
49
+ # so a strong back-reference would close a per-request cycle the refcount GC
50
+ # can't reclaim. `render_named` only touches `$self->c` mid-render, while the
51
+ # controller is still alive on the request stack.
52
+ sub new ($class, %args) {
53
+ my $self = $class->SUPER::new(%args);
54
+ weaken($self->{c}) if $self->{c};
55
+ return $self;
56
+ }
57
+
58
+ sub encode_json ($self, $data) {
59
+ return $self->json_encoder->($data);
60
+ }
61
+
62
+ # Mark a string as already-safe so the template engine emits it verbatim
63
+ # (no re-escaping). In Mojo this is a Mojo::ByteStream, which the calling
64
+ # template's `<%==` raw-emit passes through unescaped.
65
+ sub mark_raw ($self, $str) {
66
+ return b($str);
67
+ }
68
+
69
+ # JSX children / async fallbacks arrive via Mojo's `begin %>...<% end`
70
+ # capture, which produces a CODE ref returning a Mojo::ByteStream. Resolve
71
+ # it to a string before embedding. Plain (already-rendered) strings pass
72
+ # through unchanged.
73
+ sub materialize ($self, $value) {
74
+ return ref($value) eq 'CODE' ? $value->() : $value;
75
+ }
76
+
77
+ # Render a named template with `$child_bf` bound as the active runtime
78
+ # instance for that render. The Mojo `bf` helper resolves the current
79
+ # instance off `$c->stash->{'bf.instance'}`; swap it for the duration of
80
+ # the nested render and restore it afterwards so sibling renders are
81
+ # unaffected.
82
+ sub render_named ($self, $template_name, $child_bf, $vars) {
83
+ my $c = $self->c;
84
+ my $prev = $c->stash->{'bf.instance'};
85
+ $c->stash->{'bf.instance'} = $child_bf;
86
+ my $html = $c->render_to_string(template => $template_name, %$vars);
87
+ $c->stash->{'bf.instance'} = $prev;
88
+ return $html;
89
+ }
90
+
91
+ 1;
@@ -1,4 +1,5 @@
1
1
  package Mojolicious::Plugin::BarefootJS::DevReload;
2
+ our $VERSION = "0.01";
2
3
  use Mojo::Base 'Mojolicious::Plugin', -signatures;
3
4
 
4
5
  =head1 NAME
@@ -1,4 +1,5 @@
1
1
  package Mojolicious::Plugin::BarefootJS;
2
+ our $VERSION = "0.01";
2
3
  use Mojo::Base 'Mojolicious::Plugin', -signatures;
3
4
 
4
5
  use Mojo::File qw(path);
@@ -102,3 +103,58 @@ sub _load_manifest ($app, $config) {
102
103
  }
103
104
 
104
105
  1;
106
+ __END__
107
+
108
+ =encoding utf8
109
+
110
+ =head1 NAME
111
+
112
+ Mojolicious::Plugin::BarefootJS - Mojolicious integration for BarefootJS
113
+
114
+ =head1 SYNOPSIS
115
+
116
+ # Mojolicious application
117
+ $self->plugin('BarefootJS');
118
+
119
+ # In a controller / template, the `bf` helper exposes a per-request
120
+ # BarefootJS runtime backed by BarefootJS::Backend::Mojo.
121
+
122
+ =head1 DESCRIPTION
123
+
124
+ Wires the L<BarefootJS> server runtime into L<Mojolicious>. It registers a
125
+ C<bf> controller helper that lazily instantiates one BarefootJS object per
126
+ request (rendering via L<BarefootJS::Backend::Mojo>), and supports rendering
127
+ compiled marked templates as Mojolicious templates.
128
+
129
+ For non-Mojolicious / PSGI hosts, see L<BarefootJS::Backend::Xslate>, which
130
+ drives the same runtime with Text::Xslate and no web framework.
131
+
132
+ =head1 METHODS
133
+
134
+ L<Mojolicious::Plugin::BarefootJS> inherits all methods from
135
+ L<Mojolicious::Plugin> and implements the following new one.
136
+
137
+ =head2 register
138
+
139
+ $plugin->register(Mojolicious->new, \%conf);
140
+
141
+ Registers the plugin (the C<bf> helper and supporting hooks) in a Mojolicious
142
+ application.
143
+
144
+ =head1 SEE ALSO
145
+
146
+ L<BarefootJS>, L<BarefootJS::Backend::Mojo>, L<BarefootJS::Backend::Xslate>,
147
+ L<Mojolicious>, L<https://github.com/piconic-ai/barefootjs>
148
+
149
+ =head1 AUTHOR
150
+
151
+ kobaken E<lt>kentafly88@gmail.comE<gt>
152
+
153
+ =head1 LICENSE
154
+
155
+ Copyright (c) 2025-present BarefootJS Contributors.
156
+
157
+ This library is free software; you can redistribute it and/or modify it under
158
+ the MIT License. See the F<LICENSE> file in the distribution for the full text.
159
+
160
+ =cut
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@barefootjs/mojolicious",
3
- "version": "0.6.1",
3
+ "version": "0.7.0",
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",
@@ -29,7 +29,7 @@
29
29
  ],
30
30
  "scripts": {
31
31
  "build": "bun run build:js && bun run build:types",
32
- "build:js": "bun build ./src/index.ts ./src/adapter/index.ts ./src/build.ts --root ./src --outdir ./dist --format esm --external @barefootjs/jsx --external @barefootjs/shared",
32
+ "build:js": "bun build ./src/index.ts ./src/adapter/index.ts ./src/build.ts --root ./src --outdir ./dist --format esm --external @barefootjs/jsx --external @barefootjs/shared --external typescript",
33
33
  "build:types": "tsgo --emitDeclarationOnly --outDir ./dist",
34
34
  "test": "bun test",
35
35
  "clean": "rm -rf dist",
@@ -52,14 +52,15 @@
52
52
  "directory": "packages/adapter-mojolicious"
53
53
  },
54
54
  "dependencies": {
55
- "@barefootjs/shared": "0.6.1"
55
+ "@barefootjs/perl": "0.7.0",
56
+ "@barefootjs/shared": "0.7.0"
56
57
  },
57
58
  "peerDependencies": {
58
- "@barefootjs/jsx": ">=0.2.0"
59
+ "@barefootjs/jsx": ">=0.2.0",
60
+ "typescript": "^5.0.0"
59
61
  },
60
62
  "devDependencies": {
61
63
  "@barefootjs/adapter-tests": "0.1.0",
62
- "@barefootjs/jsx": "0.6.1",
63
- "typescript": "^5.0.0"
64
+ "@barefootjs/jsx": "0.7.0"
64
65
  }
65
66
  }
@@ -69,16 +69,17 @@ runAdapterConformanceTests({
69
69
  'toggle-shared',
70
70
  'reactive-props',
71
71
  'props-reactivity-comparison',
72
- // #1467 Phase 2a: first `site/ui` source-root fixture. Button
73
- // compiles cleanly on the Mojo adapter (no diagnostic), but its
74
- // variant/size class composition (`Record<…>[key]` indexed object
75
- // literals) and `applyRestAttrs` spread haven't been validated for
76
- // byte-parity against the Hono reference under Mojo's template
77
- // semantics. Cross-adapter parity for the `site/ui` corpus is
78
- // explicitly Phase 3 ("cross-adapter parity (Mojo/Go templates)"),
79
- // so Button participates only in Hono SSR conformance + the
80
- // fixture-hydrate runtime layer for now.
81
- 'button',
72
+ // #1467 Phase 2b: basic interactive `site/ui` primitives. Cross-adapter
73
+ // parity for the `site/ui` corpus is Phase 3, so these participate only
74
+ // in Hono SSR conformance + the fixture-hydrate runtime layer for now.
75
+ // (`label` and `kbd` are static helpers; `toggle` / `switch` /
76
+ // `checkbox` carry uncontrolled state; `input` / `textarea` are
77
+ // pass-through native controls.)
78
+ 'toggle',
79
+ 'switch',
80
+ 'checkbox',
81
+ 'textarea',
82
+ 'kbd',
82
83
  ],
83
84
  // Per-fixture build-time contracts for shapes the Mojo adapter
84
85
  // intentionally refuses to lower. Owned by this adapter test file
@@ -256,6 +257,42 @@ export function Hello() {
256
257
  expect(adapter.extension).toBe('.html.ep')
257
258
  })
258
259
 
260
+ test('module pure-string const referenced in className inlines the literal (#1467 Phase 2b)', () => {
261
+ // A module-scope `const X = 'literal'` used inside a className template
262
+ // literal must inline its value, NOT emit `$X` against a stash variable
263
+ // that is never bound (the value would render empty). Hono inlines it at
264
+ // runtime; this restores byte-parity.
265
+ const result = compileAndGenerate(`
266
+ "use client"
267
+ const labelClasses = 'flex items-center group-data-[disabled=true]:opacity-50'
268
+ export function Label({ className = '' }: { className?: string }) {
269
+ return <label className={\`\${labelClasses} \${className}\`} />
270
+ }
271
+ `)
272
+ // Inlined as a Perl single-quoted literal, escaped tokens intact.
273
+ expect(result.template).toContain(
274
+ "'flex items-center group-data-[disabled=true]:opacity-50'",
275
+ )
276
+ // No stash-variable reference to the const.
277
+ expect(result.template).not.toContain('$labelClasses')
278
+ })
279
+
280
+ test('module pure-string const is NOT inlined when shadowed by a loop variable (#1749 review)', () => {
281
+ // A loop param whose name matches a module const must keep its loop
282
+ // binding (`$label`) inside the body — the const literal must not leak
283
+ // in. `renderLoop` guards module-const inlining for the loop body.
284
+ const result = compileAndGenerate(`
285
+ "use client"
286
+ const label = 'MODULE_CONST'
287
+ export function List({ items }: { items: string[] }) {
288
+ return <ul>{items.map(label => <li>{label}</li>)}</ul>
289
+ }
290
+ `)
291
+ // Inside the loop the param wins — emit the loop variable, not the const.
292
+ expect(result.template).toContain('$label')
293
+ expect(result.template).not.toContain('MODULE_CONST')
294
+ })
295
+
259
296
  test('generates conditional with Perl if/else', () => {
260
297
  const result = compileAndGenerate(`
261
298
  "use client"