@barefootjs/mojolicious 0.6.1 → 0.8.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,9 @@ class MojoAdapter extends BaseAdapter {
94
111
  propsObjectName = null;
95
112
  propsParams = [];
96
113
  stringValueNames = new Set;
114
+ moduleStringConsts = new Map;
115
+ loopBoundNames = new Map;
116
+ nullableOptionalProps = new Set;
97
117
  constructor(options = {}) {
98
118
  super();
99
119
  this.options = {
@@ -105,6 +125,7 @@ class MojoAdapter extends BaseAdapter {
105
125
  this.componentName = ir.metadata.componentName;
106
126
  this.propsObjectName = ir.metadata.propsObjectName ?? null;
107
127
  this.propsParams = ir.metadata.propsParams.map((p) => ({ name: p.name }));
128
+ this.nullableOptionalProps = new Set(ir.metadata.propsParams.filter((p) => p.defaultValue === undefined && !p.isRest && p.type?.kind !== "primitive").map((p) => p.name));
108
129
  this.stringValueNames = new Set;
109
130
  for (const s of ir.metadata.signals) {
110
131
  if (isStringTypeInfo(s.type) || isBareStringLiteral(s.initialValue)) {
@@ -115,6 +136,8 @@ class MojoAdapter extends BaseAdapter {
115
136
  if (isStringTypeInfo(p.type))
116
137
  this.stringValueNames.add(p.name);
117
138
  }
139
+ this.moduleStringConsts = this.collectModuleStringConsts(ir.metadata.localConstants);
140
+ this.loopBoundNames.clear();
118
141
  this.errors = [];
119
142
  this.childrenCaptureCounter = 0;
120
143
  if (!options?.siblingTemplatesRegistered) {
@@ -139,6 +162,27 @@ class MojoAdapter extends BaseAdapter {
139
162
  extension: this.extension
140
163
  };
141
164
  }
165
+ collectModuleStringConsts(constants) {
166
+ const map = new Map;
167
+ for (const c of constants ?? []) {
168
+ if (!c.isModule)
169
+ continue;
170
+ if (c.value === undefined)
171
+ continue;
172
+ const literal = parsePureStringLiteral(c.value);
173
+ if (literal !== null)
174
+ map.set(c.name, literal);
175
+ }
176
+ return map;
177
+ }
178
+ resolveModuleStringConst(name) {
179
+ if (this.loopBoundNames.has(name))
180
+ return null;
181
+ const value = this.moduleStringConsts.get(name);
182
+ if (value === undefined)
183
+ return null;
184
+ return `'${value.replace(/[\\']/g, (m) => `\\${m}`)}'`;
185
+ }
142
186
  generateScriptRegistrations(ir, scriptBaseName) {
143
187
  const hasInteractivity = this.hasClientInteractivity(ir);
144
188
  if (!hasInteractivity)
@@ -405,6 +449,10 @@ ${whenTrue}
405
449
  }
406
450
  const param = loop.param;
407
451
  const indexVar = loop.iterationShape === "keys" ? `$${param}` : loop.index ? `$${loop.index}` : "$_i";
452
+ const loopBound = loop.iterationShape === "keys" ? [param] : [param, loop.index ?? "_i"];
453
+ for (const n of loopBound) {
454
+ this.loopBoundNames.set(n, (this.loopBoundNames.get(n) ?? 0) + 1);
455
+ }
408
456
  const prevInLoop = this.inLoop;
409
457
  this.inLoop = true;
410
458
  const renderedChildren = this.renderChildren(loop.children);
@@ -438,6 +486,13 @@ ${renderedChildren}` : renderedChildren;
438
486
  } else {
439
487
  lines.push(children);
440
488
  }
489
+ for (const n of loopBound) {
490
+ const c = (this.loopBoundNames.get(n) ?? 1) - 1;
491
+ if (c <= 0)
492
+ this.loopBoundNames.delete(n);
493
+ else
494
+ this.loopBoundNames.set(n, c);
495
+ }
441
496
  lines.push(`% }`);
442
497
  lines.push(`<%== bf->comment("/loop:${loop.markerId}") %>`);
443
498
  return lines.join(`
@@ -529,6 +584,12 @@ ${children}`;
529
584
  if (this.refuseUnsupportedAttrExpression(value.expr, name)) {
530
585
  return "";
531
586
  }
587
+ const bareId = value.expr.trim();
588
+ if (!isBooleanAttr(name) && !value.presenceOrUndefined && this.nullableOptionalProps.has(bareId)) {
589
+ const perl2 = this.convertExpressionToPerl(value.expr);
590
+ const body = isBooleanResultExpr(value.expr) || isAriaBooleanAttr(name) ? `${name}="<%= bf->bool_str(${perl2}) %>"` : `${name}="<%= ${perl2} %>"`;
591
+ return `<% if (defined ${perl2}) { %>${body}<% } %>`;
592
+ }
532
593
  if (isBooleanAttr(name) || value.presenceOrUndefined) {
533
594
  return `<%= ${this.convertExpressionToPerl(value.expr)} ? '${name}' : '' %>`;
534
595
  }
@@ -549,6 +610,10 @@ ${children}`;
549
610
  const entries = this.propsParams.map((p) => `${JSON.stringify(p.name)} => $${p.name}`);
550
611
  return `<%== bf->spread_attrs({${entries.join(", ")}}) %>`;
551
612
  }
613
+ const ternaryHashref = this.conditionalSpreadToPerl(trimmed);
614
+ if (ternaryHashref !== null) {
615
+ return `<%== bf->spread_attrs(${ternaryHashref}) %>`;
616
+ }
552
617
  const perlExpr = this.convertExpressionToPerl(value.expr);
553
618
  return `<%== bf->spread_attrs(${perlExpr}) %>`;
554
619
  },
@@ -713,6 +778,54 @@ ${reason}` : "";
713
778
  });
714
779
  return true;
715
780
  }
781
+ conditionalSpreadToPerl(expr) {
782
+ const sf = ts.createSourceFile("__spread.ts", `(${expr})`, ts.ScriptTarget.Latest, true);
783
+ if (sf.statements.length !== 1)
784
+ return null;
785
+ const stmt = sf.statements[0];
786
+ if (!ts.isExpressionStatement(stmt))
787
+ return null;
788
+ let node = stmt.expression;
789
+ while (ts.isParenthesizedExpression(node))
790
+ node = node.expression;
791
+ if (!ts.isConditionalExpression(node))
792
+ return null;
793
+ const unwrap = (e) => {
794
+ let n = e;
795
+ while (ts.isParenthesizedExpression(n))
796
+ n = n.expression;
797
+ return n;
798
+ };
799
+ const whenTrue = unwrap(node.whenTrue);
800
+ const whenFalse = unwrap(node.whenFalse);
801
+ if (!ts.isObjectLiteralExpression(whenTrue) || !ts.isObjectLiteralExpression(whenFalse)) {
802
+ return null;
803
+ }
804
+ const condPerl = this.convertExpressionToPerl(node.condition.getText(sf));
805
+ const truePerl = this.objectLiteralToPerlHashref(whenTrue, sf);
806
+ const falsePerl = this.objectLiteralToPerlHashref(whenFalse, sf);
807
+ if (truePerl === null || falsePerl === null)
808
+ return null;
809
+ return `${condPerl} ? ${truePerl} : ${falsePerl}`;
810
+ }
811
+ objectLiteralToPerlHashref(obj, sf) {
812
+ const entries = [];
813
+ for (const prop of obj.properties) {
814
+ if (!ts.isPropertyAssignment(prop))
815
+ return null;
816
+ let key;
817
+ if (ts.isIdentifier(prop.name)) {
818
+ key = prop.name.text;
819
+ } else if (ts.isStringLiteral(prop.name) || ts.isNoSubstitutionTemplateLiteral(prop.name)) {
820
+ key = prop.name.text;
821
+ } else {
822
+ return null;
823
+ }
824
+ const valPerl = this.convertExpressionToPerl(prop.initializer.getText(sf));
825
+ entries.push(`'${key.replace(/'/g, "\\'")}' => ${valPerl}`);
826
+ }
827
+ return entries.length === 0 ? "{}" : `{ ${entries.join(", ")} }`;
828
+ }
716
829
  convertExpressionToPerl(expr) {
717
830
  const trimmed = expr.trim();
718
831
  if (trimmed === "")
@@ -1056,6 +1169,9 @@ class MojoTopLevelEmitter {
1056
1169
  this.adapter = adapter;
1057
1170
  }
1058
1171
  identifier(name) {
1172
+ const inlined = this.adapter.resolveModuleStringConst(name);
1173
+ if (inlined !== null)
1174
+ return inlined;
1059
1175
  return `$${name}`;
1060
1176
  }
1061
1177
  literal(value, literalType) {
@@ -58,8 +58,62 @@ 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;
81
+ /**
82
+ * Prop names whose value is `undef` in the template body when the caller
83
+ * omits them — so a bare-reference attribute should be dropped rather
84
+ * than rendered as `attr=""`. The actual population criterion (see
85
+ * `generate()`) is: NO destructure default (`defaultValue === undefined`)
86
+ * AND non-rest (`!isRest`) AND non-primitive type (`type.kind !==
87
+ * 'primitive'`). It deliberately does NOT consult `p.optional`: the
88
+ * analyzer derives `optional` from the presence of a default initializer,
89
+ * not the `?` token, so it's not the right witness here. Excluding
90
+ * concrete primitives (`string`/`number`/`boolean`) mirrors the Go
91
+ * adapter's scope, which guards only `interface{}` (nillable) fields.
92
+ * Used by `elementAttrEmitter.emitExpression` to guard such an attribute
93
+ * with a Perl `defined $x` check (`<textarea>` omits `rows`), matching
94
+ * Hono's nullish-attribute omission. Concrete/defaulted props are
95
+ * excluded and always emit unconditionally.
96
+ */
97
+ private nullableOptionalProps;
61
98
  constructor(options?: MojoAdapterOptions);
62
99
  generate(ir: ComponentIR, options?: AdapterGenerateOptions): AdapterOutput;
100
+ /**
101
+ * Build the module pure-string-const map from the IR's localConstants.
102
+ * A const qualifies only when it is module-scope (`isModule`) and its
103
+ * initializer parses to a single string literal (`ts.StringLiteral` or
104
+ * `ts.NoSubstitutionTemplateLiteral`). Template literals with `${}`,
105
+ * numeric/object initializers, `Record<T,string>` maps, memos, and
106
+ * signals are all excluded — only a pure compile-time string can be
107
+ * inlined byte-for-byte.
108
+ */
109
+ private collectModuleStringConsts;
110
+ /**
111
+ * Resolve an identifier to its inlined Perl single-quoted string literal
112
+ * when it names a module pure-string const, else `null` (the caller then
113
+ * falls back to its normal `$name` stash lowering). Returns the Perl
114
+ * literal form `'<escaped>'` ready to drop into an expression.
115
+ */
116
+ resolveModuleStringConst(name: string): string | null;
63
117
  private generateScriptRegistrations;
64
118
  private hasClientInteractivity;
65
119
  /**
@@ -165,6 +219,32 @@ export declare class MojoAdapter extends BaseAdapter implements IRNodeEmitter<Mo
165
219
  * should drop the attribute / skip the emit).
166
220
  */
167
221
  private refuseUnsupportedAttrExpression;
222
+ /**
223
+ * Lower a conditional inline-object spread expression
224
+ * `(COND ? { 'aria-describedby': describedBy } : {})`
225
+ * (either branch possibly `{}`) into a Perl inline ternary of
226
+ * hashrefs for `bf->spread_attrs`:
227
+ * `$describedBy ? { 'aria-describedby' => $describedBy } : {}`
228
+ *
229
+ * The condition is translated via `convertExpressionToPerl` (a bare
230
+ * prop ident becomes `$describedBy`; Perl truthiness handles the
231
+ * test). Object literals become Perl hashrefs with `=>`; string-
232
+ * literal keys are quoted, values resolve via `convertExpressionToPerl`.
233
+ *
234
+ * Returns null when the expression is NOT this shape, or when a part
235
+ * can't be faithfully lowered (non-static key, etc.) so the caller
236
+ * falls back to the standard `convertExpressionToPerl` path (which
237
+ * records BF101). Scoped strictly to ternary-of-object-literals so no
238
+ * other spread shape regresses.
239
+ */
240
+ private conditionalSpreadToPerl;
241
+ /**
242
+ * Convert a static object literal into a Perl hashref string for a
243
+ * conditional spread. Only static string/identifier keys are allowed;
244
+ * values resolve via `convertExpressionToPerl`. Returns null for any
245
+ * computed/spread/dynamic key. Empty object → `{}`.
246
+ */
247
+ private objectLiteralToPerlHashref;
168
248
  private convertExpressionToPerl;
169
249
  /**
170
250
  * Render a full ParsedExpr tree to Perl for top-level (non-filter)
@@ -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,9 @@ class MojoAdapter extends BaseAdapter {
94
111
  propsObjectName = null;
95
112
  propsParams = [];
96
113
  stringValueNames = new Set;
114
+ moduleStringConsts = new Map;
115
+ loopBoundNames = new Map;
116
+ nullableOptionalProps = new Set;
97
117
  constructor(options = {}) {
98
118
  super();
99
119
  this.options = {
@@ -105,6 +125,7 @@ class MojoAdapter extends BaseAdapter {
105
125
  this.componentName = ir.metadata.componentName;
106
126
  this.propsObjectName = ir.metadata.propsObjectName ?? null;
107
127
  this.propsParams = ir.metadata.propsParams.map((p) => ({ name: p.name }));
128
+ this.nullableOptionalProps = new Set(ir.metadata.propsParams.filter((p) => p.defaultValue === undefined && !p.isRest && p.type?.kind !== "primitive").map((p) => p.name));
108
129
  this.stringValueNames = new Set;
109
130
  for (const s of ir.metadata.signals) {
110
131
  if (isStringTypeInfo(s.type) || isBareStringLiteral(s.initialValue)) {
@@ -115,6 +136,8 @@ class MojoAdapter extends BaseAdapter {
115
136
  if (isStringTypeInfo(p.type))
116
137
  this.stringValueNames.add(p.name);
117
138
  }
139
+ this.moduleStringConsts = this.collectModuleStringConsts(ir.metadata.localConstants);
140
+ this.loopBoundNames.clear();
118
141
  this.errors = [];
119
142
  this.childrenCaptureCounter = 0;
120
143
  if (!options?.siblingTemplatesRegistered) {
@@ -139,6 +162,27 @@ class MojoAdapter extends BaseAdapter {
139
162
  extension: this.extension
140
163
  };
141
164
  }
165
+ collectModuleStringConsts(constants) {
166
+ const map = new Map;
167
+ for (const c of constants ?? []) {
168
+ if (!c.isModule)
169
+ continue;
170
+ if (c.value === undefined)
171
+ continue;
172
+ const literal = parsePureStringLiteral(c.value);
173
+ if (literal !== null)
174
+ map.set(c.name, literal);
175
+ }
176
+ return map;
177
+ }
178
+ resolveModuleStringConst(name) {
179
+ if (this.loopBoundNames.has(name))
180
+ return null;
181
+ const value = this.moduleStringConsts.get(name);
182
+ if (value === undefined)
183
+ return null;
184
+ return `'${value.replace(/[\\']/g, (m) => `\\${m}`)}'`;
185
+ }
142
186
  generateScriptRegistrations(ir, scriptBaseName) {
143
187
  const hasInteractivity = this.hasClientInteractivity(ir);
144
188
  if (!hasInteractivity)
@@ -405,6 +449,10 @@ ${whenTrue}
405
449
  }
406
450
  const param = loop.param;
407
451
  const indexVar = loop.iterationShape === "keys" ? `$${param}` : loop.index ? `$${loop.index}` : "$_i";
452
+ const loopBound = loop.iterationShape === "keys" ? [param] : [param, loop.index ?? "_i"];
453
+ for (const n of loopBound) {
454
+ this.loopBoundNames.set(n, (this.loopBoundNames.get(n) ?? 0) + 1);
455
+ }
408
456
  const prevInLoop = this.inLoop;
409
457
  this.inLoop = true;
410
458
  const renderedChildren = this.renderChildren(loop.children);
@@ -438,6 +486,13 @@ ${renderedChildren}` : renderedChildren;
438
486
  } else {
439
487
  lines.push(children);
440
488
  }
489
+ for (const n of loopBound) {
490
+ const c = (this.loopBoundNames.get(n) ?? 1) - 1;
491
+ if (c <= 0)
492
+ this.loopBoundNames.delete(n);
493
+ else
494
+ this.loopBoundNames.set(n, c);
495
+ }
441
496
  lines.push(`% }`);
442
497
  lines.push(`<%== bf->comment("/loop:${loop.markerId}") %>`);
443
498
  return lines.join(`
@@ -529,6 +584,12 @@ ${children}`;
529
584
  if (this.refuseUnsupportedAttrExpression(value.expr, name)) {
530
585
  return "";
531
586
  }
587
+ const bareId = value.expr.trim();
588
+ if (!isBooleanAttr(name) && !value.presenceOrUndefined && this.nullableOptionalProps.has(bareId)) {
589
+ const perl2 = this.convertExpressionToPerl(value.expr);
590
+ const body = isBooleanResultExpr(value.expr) || isAriaBooleanAttr(name) ? `${name}="<%= bf->bool_str(${perl2}) %>"` : `${name}="<%= ${perl2} %>"`;
591
+ return `<% if (defined ${perl2}) { %>${body}<% } %>`;
592
+ }
532
593
  if (isBooleanAttr(name) || value.presenceOrUndefined) {
533
594
  return `<%= ${this.convertExpressionToPerl(value.expr)} ? '${name}' : '' %>`;
534
595
  }
@@ -549,6 +610,10 @@ ${children}`;
549
610
  const entries = this.propsParams.map((p) => `${JSON.stringify(p.name)} => $${p.name}`);
550
611
  return `<%== bf->spread_attrs({${entries.join(", ")}}) %>`;
551
612
  }
613
+ const ternaryHashref = this.conditionalSpreadToPerl(trimmed);
614
+ if (ternaryHashref !== null) {
615
+ return `<%== bf->spread_attrs(${ternaryHashref}) %>`;
616
+ }
552
617
  const perlExpr = this.convertExpressionToPerl(value.expr);
553
618
  return `<%== bf->spread_attrs(${perlExpr}) %>`;
554
619
  },
@@ -713,6 +778,54 @@ ${reason}` : "";
713
778
  });
714
779
  return true;
715
780
  }
781
+ conditionalSpreadToPerl(expr) {
782
+ const sf = ts.createSourceFile("__spread.ts", `(${expr})`, ts.ScriptTarget.Latest, true);
783
+ if (sf.statements.length !== 1)
784
+ return null;
785
+ const stmt = sf.statements[0];
786
+ if (!ts.isExpressionStatement(stmt))
787
+ return null;
788
+ let node = stmt.expression;
789
+ while (ts.isParenthesizedExpression(node))
790
+ node = node.expression;
791
+ if (!ts.isConditionalExpression(node))
792
+ return null;
793
+ const unwrap = (e) => {
794
+ let n = e;
795
+ while (ts.isParenthesizedExpression(n))
796
+ n = n.expression;
797
+ return n;
798
+ };
799
+ const whenTrue = unwrap(node.whenTrue);
800
+ const whenFalse = unwrap(node.whenFalse);
801
+ if (!ts.isObjectLiteralExpression(whenTrue) || !ts.isObjectLiteralExpression(whenFalse)) {
802
+ return null;
803
+ }
804
+ const condPerl = this.convertExpressionToPerl(node.condition.getText(sf));
805
+ const truePerl = this.objectLiteralToPerlHashref(whenTrue, sf);
806
+ const falsePerl = this.objectLiteralToPerlHashref(whenFalse, sf);
807
+ if (truePerl === null || falsePerl === null)
808
+ return null;
809
+ return `${condPerl} ? ${truePerl} : ${falsePerl}`;
810
+ }
811
+ objectLiteralToPerlHashref(obj, sf) {
812
+ const entries = [];
813
+ for (const prop of obj.properties) {
814
+ if (!ts.isPropertyAssignment(prop))
815
+ return null;
816
+ let key;
817
+ if (ts.isIdentifier(prop.name)) {
818
+ key = prop.name.text;
819
+ } else if (ts.isStringLiteral(prop.name) || ts.isNoSubstitutionTemplateLiteral(prop.name)) {
820
+ key = prop.name.text;
821
+ } else {
822
+ return null;
823
+ }
824
+ const valPerl = this.convertExpressionToPerl(prop.initializer.getText(sf));
825
+ entries.push(`'${key.replace(/'/g, "\\'")}' => ${valPerl}`);
826
+ }
827
+ return entries.length === 0 ? "{}" : `{ ${entries.join(", ")} }`;
828
+ }
716
829
  convertExpressionToPerl(expr) {
717
830
  const trimmed = expr.trim();
718
831
  if (trimmed === "")
@@ -1056,6 +1169,9 @@ class MojoTopLevelEmitter {
1056
1169
  this.adapter = adapter;
1057
1170
  }
1058
1171
  identifier(name) {
1172
+ const inlined = this.adapter.resolveModuleStringConst(name);
1173
+ if (inlined !== null)
1174
+ return inlined;
1059
1175
  return `$${name}`;
1060
1176
  }
1061
1177
  literal(value, literalType) {