@barefootjs/mojolicious 0.5.0 → 0.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/build.js CHANGED
@@ -5,7 +5,6 @@ import {
5
5
  parseExpression as parseExpression2,
6
6
  isSupported,
7
7
  identifierPath,
8
- stringifyParsedExpr,
9
8
  emitParsedExpr,
10
9
  emitIRNode,
11
10
  emitAttrValue
@@ -72,7 +71,6 @@ var MOJO_TEMPLATE_PRIMITIVES = {
72
71
  "Math.ceil": { arity: 1, emit: (args) => `bf->ceil(${args[0]})` },
73
72
  "Math.round": { arity: 1, emit: (args) => `bf->round(${args[0]})` }
74
73
  };
75
- var PRIMITIVE_SUBSTRING_RE = new RegExp(Object.keys(MOJO_TEMPLATE_PRIMITIVES).map((k) => k.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|"));
76
74
  var MOJO_PRIMITIVE_EMIT_MAP = Object.fromEntries(Object.entries(MOJO_TEMPLATE_PRIMITIVES).map(([k, v]) => [k, v.emit]));
77
75
  function resolveJsxChildrenProp(props) {
78
76
  const prop = props.find((p) => p.name === "children");
@@ -93,9 +91,9 @@ class MojoAdapter extends BaseAdapter {
93
91
  options;
94
92
  errors = [];
95
93
  inLoop = false;
96
- higherOrderInFlight = new Set;
97
94
  propsObjectName = null;
98
95
  propsParams = [];
96
+ stringValueNames = new Set;
99
97
  constructor(options = {}) {
100
98
  super();
101
99
  this.options = {
@@ -107,8 +105,17 @@ class MojoAdapter extends BaseAdapter {
107
105
  this.componentName = ir.metadata.componentName;
108
106
  this.propsObjectName = ir.metadata.propsObjectName ?? null;
109
107
  this.propsParams = ir.metadata.propsParams.map((p) => ({ name: p.name }));
108
+ this.stringValueNames = new Set;
109
+ for (const s of ir.metadata.signals) {
110
+ if (isStringTypeInfo(s.type) || isBareStringLiteral(s.initialValue)) {
111
+ this.stringValueNames.add(s.getter);
112
+ }
113
+ }
114
+ for (const p of ir.metadata.propsParams) {
115
+ if (isStringTypeInfo(p.type))
116
+ this.stringValueNames.add(p.name);
117
+ }
110
118
  this.errors = [];
111
- this.higherOrderInFlight = new Set;
112
119
  this.childrenCaptureCounter = 0;
113
120
  if (!options?.siblingTemplatesRegistered) {
114
121
  this.checkImportedLoopChildComponents(ir);
@@ -400,8 +407,10 @@ ${whenTrue}
400
407
  const indexVar = loop.iterationShape === "keys" ? `$${param}` : loop.index ? `$${loop.index}` : "$_i";
401
408
  const prevInLoop = this.inLoop;
402
409
  this.inLoop = true;
403
- const children = this.renderChildren(loop.children);
410
+ const renderedChildren = this.renderChildren(loop.children);
404
411
  this.inLoop = prevInLoop;
412
+ const children = loop.bodyIsItemConditional && loop.key ? `<%== bf->comment("loop-i:" . ${this.convertExpressionToPerl(loop.key)}) %>
413
+ ${renderedChildren}` : renderedChildren;
405
414
  const lines = [];
406
415
  lines.push(`<%== bf->comment("loop:${loop.markerId}") %>`);
407
416
  if (sortedHoist && loop.sortComparator) {
@@ -572,7 +581,7 @@ ${children}`;
572
581
  return `${BF_COND}="${condId}"`;
573
582
  }
574
583
  renderPerlFilterExpr(expr, param, localVarMap = new Map) {
575
- return emitParsedExpr(expr, new MojoFilterEmitter(param, localVarMap));
584
+ return emitParsedExpr(expr, new MojoFilterEmitter(param, localVarMap, (n) => this._isStringValueName(n)));
576
585
  }
577
586
  renderBlockBodyCondition(statements, param) {
578
587
  const localVarMap = new Map;
@@ -705,154 +714,53 @@ ${reason}` : "";
705
714
  return true;
706
715
  }
707
716
  convertExpressionToPerl(expr) {
708
- if (/\.\s*(?:filter|every|some|reduce|reduceRight|forEach|flatMap|flat)\s*\(/.test(expr)) {
709
- return this.convertHigherOrderExpr(expr);
710
- }
711
- if (/\.\s*(?:includes|indexOf|lastIndexOf|at|concat|slice|reverse|toReversed|toLowerCase|toUpperCase|trim)\s*\(/.test(expr)) {
712
- return this.convertHigherOrderExpr(expr);
713
- }
714
- if (/\.\s*join\s*\(/.test(expr)) {
715
- return this.convertHigherOrderExpr(expr);
716
- }
717
- const mojoOnlyMatch = /\.\s*(?<method>find|findIndex|findLast|findLastIndex)\s*\(/.exec(expr);
718
- if (mojoOnlyMatch) {
719
- const methodName = mojoOnlyMatch.groups.method;
717
+ const trimmed = expr.trim();
718
+ if (trimmed === "")
719
+ return "''";
720
+ const parsed = parseExpression2(trimmed);
721
+ const support = isSupported(parsed);
722
+ if (!support.supported) {
720
723
  this.errors.push({
721
724
  code: "BF101",
722
725
  severity: "error",
723
- message: `Mojo adapter has not lowered Array.prototype.${methodName} yet: ${expr.trim()}`,
726
+ message: `Expression not supported: ${trimmed}`,
724
727
  loc: { file: this.componentName + ".tsx", start: { line: 1, column: 0 }, end: { line: 1, column: 0 } },
725
728
  suggestion: {
726
- message: `Options:
729
+ message: support.reason ? `${support.reason}
730
+
731
+ Options:
732
+ 1. Use /* @client */ for client-side evaluation
733
+ 2. Pre-compute the value in Perl` : `Options:
727
734
  1. Use /* @client */ for client-side evaluation
728
735
  2. Pre-compute the value in Perl`
729
736
  }
730
737
  });
731
738
  return "''";
732
739
  }
733
- expr = this.rewriteTemplatePrimitives(expr);
734
- let result = expr.replace(/\b([a-z_]\w*)\(\)/g, (_, name) => `$${name}`);
735
- result = result.replace(/\bprops\.(\w+)/g, (_, prop) => `$${prop}`);
736
- result = result.replace(/(?<!\$)\b([a-z_]\w*)\.(\w+)/g, (match, obj, field) => {
737
- if (match.startsWith("$"))
738
- return match;
739
- return `$${obj}->{${field}}`;
740
- });
741
- result = result.replace(/\$(\w+)\.(\w+)/g, (_, obj, field) => `$${obj}->{${field}}`);
742
- result = result.replace(/\}->\{(\w+)\}\.(\w+)/g, (_, f1, f2) => `}->{${f1}}->{${f2}}`);
743
- result = result.replace(/\$(\w+)->\{length\}/g, (_, arr) => `scalar(@{$${arr}})`);
744
- result = result.replace(/\?\?/g, "//");
745
- result = result.replace(/\s*===\s*(['"])/g, " eq $1");
746
- result = result.replace(/\s*!==\s*(['"])/g, " ne $1");
747
- result = result.replace(/(['"])\s*===\s*/g, "$1 eq ");
748
- result = result.replace(/(['"])\s*!==\s*/g, "$1 ne ");
749
- result = result.replace(/===/g, "==");
750
- result = result.replace(/!==/g, "!=");
751
- result = result.replace(/`([^`]*)`/g, (_, content) => {
752
- const perlStr = content.replace(/\$\{([^}]+)\}/g, (_2, e) => `${this.convertExpressionToPerl(e)}`);
753
- return `"${perlStr}"`;
754
- });
755
- if (/^[a-z_]\w*$/i.test(result) && !result.startsWith("$")) {
756
- result = `$${result}`;
757
- }
758
- return result;
740
+ return this.renderParsedExprToPerl(parsed);
759
741
  }
760
- rewriteTemplatePrimitives(expr) {
761
- if (!PRIMITIVE_SUBSTRING_RE.test(expr))
762
- return expr;
763
- const parsed = parseExpression2(expr);
764
- if (parsed.kind === "unsupported")
765
- return expr;
766
- let mutated = false;
767
- const walk = (n) => {
768
- if (n.kind === "call") {
769
- const path = identifierPath(n.callee);
770
- const spec = path ? MOJO_TEMPLATE_PRIMITIVES[path] : undefined;
771
- if (path && spec) {
772
- if (n.args.length !== spec.arity) {
773
- this.errors.push({
774
- code: "BF101",
775
- severity: "error",
776
- message: `templatePrimitive '${path}' expects ${spec.arity} arg(s), got ${n.args.length}`,
777
- loc: { file: this.componentName + ".tsx", start: { line: 1, column: 0 }, end: { line: 1, column: 0 } },
778
- suggestion: {
779
- message: `Call '${path}' with exactly ${spec.arity} argument(s), or wrap the JSX expression in /* @client */ to defer evaluation.`
780
- }
781
- });
782
- return { kind: "call", callee: walk(n.callee), args: n.args.map(walk) };
783
- }
784
- const renderedArgs = n.args.map((a) => this.convertExpressionToPerl(stringifyParsedExpr(walk(a))));
785
- mutated = true;
786
- return { kind: "identifier", name: spec.emit(renderedArgs) };
787
- }
788
- }
789
- switch (n.kind) {
790
- case "call":
791
- return { kind: "call", callee: walk(n.callee), args: n.args.map(walk) };
792
- case "member":
793
- return { kind: "member", object: walk(n.object), property: n.property, computed: n.computed };
794
- case "binary":
795
- return { kind: "binary", op: n.op, left: walk(n.left), right: walk(n.right) };
796
- case "unary":
797
- return { kind: "unary", op: n.op, argument: walk(n.argument) };
798
- case "logical":
799
- return { kind: "logical", op: n.op, left: walk(n.left), right: walk(n.right) };
800
- case "conditional":
801
- return { kind: "conditional", test: walk(n.test), consequent: walk(n.consequent), alternate: walk(n.alternate) };
802
- default:
803
- return n;
804
- }
805
- };
806
- const transformed = walk(parsed);
807
- if (!mutated)
808
- return expr;
809
- return stringifyParsedExpr(transformed);
742
+ renderParsedExprToPerl(expr) {
743
+ return emitParsedExpr(expr, new MojoTopLevelEmitter(this));
810
744
  }
811
- convertHigherOrderExpr(expr) {
812
- if (this.higherOrderInFlight.has(expr)) {
813
- this.errors.push({
814
- code: "BF101",
815
- severity: "error",
816
- message: `Cannot lower higher-order chain to Embedded Perl: ${expr.trim()}`,
817
- loc: { file: this.componentName + ".tsx", start: { line: 1, column: 0 }, end: { line: 1, column: 0 } },
818
- suggestion: {
819
- message: "The Mojo adapter cannot lower this `.filter()` / `.every()` / `.some()` chain — typically because the array source is a JS array literal or a non-signal expression the AST classifier doesn't recognise. Move the expression into a `'use client'` component (so hydration computes it client-side), or rewrite it to operate on a signal getter or a prop directly."
820
- }
821
- });
822
- return "''";
823
- }
824
- this.higherOrderInFlight.add(expr);
825
- try {
826
- const parsed = parseExpression2(expr);
827
- const support = isSupported(parsed);
828
- if (!support.supported) {
829
- this.errors.push({
830
- code: "BF101",
831
- severity: "error",
832
- message: `Cannot lower higher-order chain to Embedded Perl: ${expr.trim()}`,
833
- loc: { file: this.componentName + ".tsx", start: { line: 1, column: 0 }, end: { line: 1, column: 0 } },
834
- suggestion: {
835
- message: support.reason ? `${support.reason}
745
+ _isStringValueName(name) {
746
+ return this.stringValueNames.has(name);
747
+ }
748
+ _recordExprBF101(message, reason) {
749
+ this.errors.push({
750
+ code: "BF101",
751
+ severity: "error",
752
+ message,
753
+ loc: { file: this.componentName + ".tsx", start: { line: 1, column: 0 }, end: { line: 1, column: 0 } },
754
+ suggestion: {
755
+ message: reason ? `${reason}
836
756
 
837
757
  Options:
838
758
  1. Use /* @client */ for client-side evaluation
839
759
  2. Pre-compute the value in Perl` : `Options:
840
760
  1. Use /* @client */ for client-side evaluation
841
761
  2. Pre-compute the value in Perl`
842
- }
843
- });
844
- return "''";
845
762
  }
846
- return this.renderParsedExprToPerl(parsed);
847
- } finally {
848
- this.higherOrderInFlight.delete(expr);
849
- }
850
- }
851
- renderParsedExprToPerl(expr) {
852
- return emitParsedExpr(expr, new MojoTopLevelEmitter(this));
853
- }
854
- _convertExpressionToPerlPublic(raw) {
855
- return this.convertExpressionToPerl(raw);
763
+ });
856
764
  }
857
765
  _renderPerlFilterExprPublic(expr, param) {
858
766
  return this.renderPerlFilterExpr(expr, param);
@@ -926,13 +834,35 @@ function renderSortMethod(recv, c) {
926
834
  });
927
835
  return `bf->sort(${recv}, { keys => [${keyHashes.join(", ")}] })`;
928
836
  }
837
+ function isStringTypeInfo(type) {
838
+ return type?.kind === "primitive" && type.primitive === "string";
839
+ }
840
+ function isBareStringLiteral(initialValue) {
841
+ if (!initialValue)
842
+ return false;
843
+ const v = initialValue.trim();
844
+ return v.startsWith("'") && v.endsWith("'") || v.startsWith('"') && v.endsWith('"');
845
+ }
846
+ function isStringTypedOperand(expr, isStringName) {
847
+ if (expr.kind === "literal" && expr.literalType === "string")
848
+ return true;
849
+ if (expr.kind === "call" && expr.callee.kind === "identifier" && expr.args.length === 0) {
850
+ return isStringName(expr.callee.name);
851
+ }
852
+ if (expr.kind === "member" && expr.object.kind === "identifier" && expr.object.name === "props") {
853
+ return isStringName(expr.property);
854
+ }
855
+ return false;
856
+ }
929
857
 
930
858
  class MojoFilterEmitter {
931
859
  param;
932
860
  localVarMap;
933
- constructor(param, localVarMap) {
861
+ isStringName;
862
+ constructor(param, localVarMap, isStringName = () => false) {
934
863
  this.param = param;
935
864
  this.localVarMap = localVarMap;
865
+ this.isStringName = isStringName;
936
866
  }
937
867
  identifier(name) {
938
868
  if (name === this.param)
@@ -976,10 +906,12 @@ class MojoFilterEmitter {
976
906
  binary(op, left, right, emit) {
977
907
  const l = emit(left);
978
908
  const r = emit(right);
979
- if ((op === "===" || op === "==") && right.kind === "literal" && right.literalType === "string") {
909
+ const isStr = (e) => isStringTypedOperand(e, this.isStringName);
910
+ const stringCmp = isStr(left) || isStr(right);
911
+ if ((op === "===" || op === "==") && stringCmp) {
980
912
  return `${l} eq ${r}`;
981
913
  }
982
- if ((op === "!==" || op === "!=") && right.kind === "literal" && right.literalType === "string") {
914
+ if ((op === "!==" || op === "!=") && stringCmp) {
983
915
  return `${l} ne ${r}`;
984
916
  }
985
917
  const opMap = {
@@ -1007,7 +939,7 @@ class MojoFilterEmitter {
1007
939
  }
1008
940
  higherOrder(method, object, param, predicate, emit) {
1009
941
  const arrayExpr = emit(object);
1010
- const predBody = emitParsedExpr(predicate, new MojoFilterEmitter(param, this.localVarMap));
942
+ const predBody = emitParsedExpr(predicate, new MojoFilterEmitter(param, this.localVarMap, this.isStringName));
1011
943
  const grepBody = predBody.replace(new RegExp(`\\$${param}\\b`, "g"), "$_");
1012
944
  if (method === "filter")
1013
945
  return `[grep { ${grepBody} } @{${arrayExpr}}]`;
@@ -1058,6 +990,9 @@ class MojoTopLevelEmitter {
1058
990
  return String(value);
1059
991
  }
1060
992
  member(object, property, _computed, emit) {
993
+ if (object.kind === "identifier" && object.name === "props") {
994
+ return `$${property}`;
995
+ }
1061
996
  const obj = emit(object);
1062
997
  if (property === "length")
1063
998
  return `scalar(@{${obj}})`;
@@ -1067,6 +1002,15 @@ class MojoTopLevelEmitter {
1067
1002
  if (callee.kind === "identifier" && args.length === 0) {
1068
1003
  return `$${callee.name}`;
1069
1004
  }
1005
+ const path = identifierPath(callee);
1006
+ const spec = path ? MOJO_TEMPLATE_PRIMITIVES[path] : undefined;
1007
+ if (path && spec) {
1008
+ if (args.length === spec.arity) {
1009
+ return spec.emit(args.map(emit));
1010
+ }
1011
+ this.adapter._recordExprBF101(`templatePrimitive '${path}' expects ${spec.arity} arg(s), got ${args.length}`, `Call '${path}' with exactly ${spec.arity} argument(s).`);
1012
+ return "''";
1013
+ }
1070
1014
  return emit(callee);
1071
1015
  }
1072
1016
  unary(op, argument, emit) {
@@ -1080,10 +1024,12 @@ class MojoTopLevelEmitter {
1080
1024
  binary(op, left, right, emit) {
1081
1025
  const l = emit(left);
1082
1026
  const r = emit(right);
1083
- if ((op === "===" || op === "==") && right.kind === "literal" && right.literalType === "string") {
1027
+ const isStr = (e) => isStringTypedOperand(e, (n) => this.adapter._isStringValueName(n));
1028
+ const stringCmp = isStr(left) || isStr(right);
1029
+ if ((op === "===" || op === "==") && stringCmp) {
1084
1030
  return `${l} eq ${r}`;
1085
1031
  }
1086
- if ((op === "!==" || op === "!=") && right.kind === "literal" && right.literalType === "string") {
1032
+ if ((op === "!==" || op === "!=") && stringCmp) {
1087
1033
  return `${l} ne ${r}`;
1088
1034
  }
1089
1035
  const opMap = {
@@ -1109,6 +1055,10 @@ class MojoTopLevelEmitter {
1109
1055
  return `(${l} // ${r})`;
1110
1056
  }
1111
1057
  higherOrder(method, object, param, predicate, emit) {
1058
+ if (method === "find" || method === "findIndex" || method === "findLast" || method === "findLastIndex") {
1059
+ this.adapter._recordExprBF101(`Mojo adapter has not lowered Array.prototype.${method} yet`);
1060
+ return "''";
1061
+ }
1112
1062
  const arrayExpr = emit(object);
1113
1063
  const predBody = this.adapter._renderPerlFilterExprPublic(predicate, param);
1114
1064
  const grepBody = predBody.replace(new RegExp(`\\$${param}\\b`, "g"), "$_");
@@ -1132,14 +1082,28 @@ class MojoTopLevelEmitter {
1132
1082
  conditional(test, consequent, alternate, emit) {
1133
1083
  return `(${emit(test)} ? ${emit(consequent)} : ${emit(alternate)})`;
1134
1084
  }
1135
- templateLiteral(_parts) {
1136
- return "";
1085
+ templateLiteral(parts, emit) {
1086
+ const terms = [];
1087
+ for (const part of parts) {
1088
+ if (part.type === "string") {
1089
+ if (part.value !== "") {
1090
+ terms.push(`"${part.value.replace(/[\\"$@]/g, (m) => `\\${m}`)}"`);
1091
+ }
1092
+ } else {
1093
+ const rendered = emit(part.expr);
1094
+ const needsParens = part.expr.kind === "binary" || part.expr.kind === "logical" || part.expr.kind === "conditional";
1095
+ terms.push(needsParens ? `(${rendered})` : rendered);
1096
+ }
1097
+ }
1098
+ if (terms.length === 0)
1099
+ return '""';
1100
+ return terms.join(" . ");
1137
1101
  }
1138
1102
  arrowFn(_param, _body) {
1139
- return "";
1103
+ return "''";
1140
1104
  }
1141
- unsupported(raw, _reason) {
1142
- return this.adapter._convertExpressionToPerlPublic(raw);
1105
+ unsupported(_raw, _reason) {
1106
+ return "''";
1143
1107
  }
1144
1108
  }
1145
1109
  var mojoAdapter = new MojoAdapter;