@barefootjs/mojolicious 0.5.1 → 0.5.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.
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);
@@ -574,7 +581,7 @@ ${children}`;
574
581
  return `${BF_COND}="${condId}"`;
575
582
  }
576
583
  renderPerlFilterExpr(expr, param, localVarMap = new Map) {
577
- return emitParsedExpr(expr, new MojoFilterEmitter(param, localVarMap));
584
+ return emitParsedExpr(expr, new MojoFilterEmitter(param, localVarMap, (n) => this._isStringValueName(n)));
578
585
  }
579
586
  renderBlockBodyCondition(statements, param) {
580
587
  const localVarMap = new Map;
@@ -707,154 +714,53 @@ ${reason}` : "";
707
714
  return true;
708
715
  }
709
716
  convertExpressionToPerl(expr) {
710
- if (/\.\s*(?:filter|every|some|reduce|reduceRight|forEach|flatMap|flat)\s*\(/.test(expr)) {
711
- return this.convertHigherOrderExpr(expr);
712
- }
713
- if (/\.\s*(?:includes|indexOf|lastIndexOf|at|concat|slice|reverse|toReversed|toLowerCase|toUpperCase|trim)\s*\(/.test(expr)) {
714
- return this.convertHigherOrderExpr(expr);
715
- }
716
- if (/\.\s*join\s*\(/.test(expr)) {
717
- return this.convertHigherOrderExpr(expr);
718
- }
719
- const mojoOnlyMatch = /\.\s*(?<method>find|findIndex|findLast|findLastIndex)\s*\(/.exec(expr);
720
- if (mojoOnlyMatch) {
721
- 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) {
722
723
  this.errors.push({
723
724
  code: "BF101",
724
725
  severity: "error",
725
- message: `Mojo adapter has not lowered Array.prototype.${methodName} yet: ${expr.trim()}`,
726
+ message: `Expression not supported: ${trimmed}`,
726
727
  loc: { file: this.componentName + ".tsx", start: { line: 1, column: 0 }, end: { line: 1, column: 0 } },
727
728
  suggestion: {
728
- 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:
729
734
  1. Use /* @client */ for client-side evaluation
730
735
  2. Pre-compute the value in Perl`
731
736
  }
732
737
  });
733
738
  return "''";
734
739
  }
735
- expr = this.rewriteTemplatePrimitives(expr);
736
- let result = expr.replace(/\b([a-z_]\w*)\(\)/g, (_, name) => `$${name}`);
737
- result = result.replace(/\bprops\.(\w+)/g, (_, prop) => `$${prop}`);
738
- result = result.replace(/(?<!\$)\b([a-z_]\w*)\.(\w+)/g, (match, obj, field) => {
739
- if (match.startsWith("$"))
740
- return match;
741
- return `$${obj}->{${field}}`;
742
- });
743
- result = result.replace(/\$(\w+)\.(\w+)/g, (_, obj, field) => `$${obj}->{${field}}`);
744
- result = result.replace(/\}->\{(\w+)\}\.(\w+)/g, (_, f1, f2) => `}->{${f1}}->{${f2}}`);
745
- result = result.replace(/\$(\w+)->\{length\}/g, (_, arr) => `scalar(@{$${arr}})`);
746
- result = result.replace(/\?\?/g, "//");
747
- result = result.replace(/\s*===\s*(['"])/g, " eq $1");
748
- result = result.replace(/\s*!==\s*(['"])/g, " ne $1");
749
- result = result.replace(/(['"])\s*===\s*/g, "$1 eq ");
750
- result = result.replace(/(['"])\s*!==\s*/g, "$1 ne ");
751
- result = result.replace(/===/g, "==");
752
- result = result.replace(/!==/g, "!=");
753
- result = result.replace(/`([^`]*)`/g, (_, content) => {
754
- const perlStr = content.replace(/\$\{([^}]+)\}/g, (_2, e) => `${this.convertExpressionToPerl(e)}`);
755
- return `"${perlStr}"`;
756
- });
757
- if (/^[a-z_]\w*$/i.test(result) && !result.startsWith("$")) {
758
- result = `$${result}`;
759
- }
760
- return result;
740
+ return this.renderParsedExprToPerl(parsed);
761
741
  }
762
- rewriteTemplatePrimitives(expr) {
763
- if (!PRIMITIVE_SUBSTRING_RE.test(expr))
764
- return expr;
765
- const parsed = parseExpression2(expr);
766
- if (parsed.kind === "unsupported")
767
- return expr;
768
- let mutated = false;
769
- const walk = (n) => {
770
- if (n.kind === "call") {
771
- const path = identifierPath(n.callee);
772
- const spec = path ? MOJO_TEMPLATE_PRIMITIVES[path] : undefined;
773
- if (path && spec) {
774
- if (n.args.length !== spec.arity) {
775
- this.errors.push({
776
- code: "BF101",
777
- severity: "error",
778
- message: `templatePrimitive '${path}' expects ${spec.arity} arg(s), got ${n.args.length}`,
779
- loc: { file: this.componentName + ".tsx", start: { line: 1, column: 0 }, end: { line: 1, column: 0 } },
780
- suggestion: {
781
- message: `Call '${path}' with exactly ${spec.arity} argument(s), or wrap the JSX expression in /* @client */ to defer evaluation.`
782
- }
783
- });
784
- return { kind: "call", callee: walk(n.callee), args: n.args.map(walk) };
785
- }
786
- const renderedArgs = n.args.map((a) => this.convertExpressionToPerl(stringifyParsedExpr(walk(a))));
787
- mutated = true;
788
- return { kind: "identifier", name: spec.emit(renderedArgs) };
789
- }
790
- }
791
- switch (n.kind) {
792
- case "call":
793
- return { kind: "call", callee: walk(n.callee), args: n.args.map(walk) };
794
- case "member":
795
- return { kind: "member", object: walk(n.object), property: n.property, computed: n.computed };
796
- case "binary":
797
- return { kind: "binary", op: n.op, left: walk(n.left), right: walk(n.right) };
798
- case "unary":
799
- return { kind: "unary", op: n.op, argument: walk(n.argument) };
800
- case "logical":
801
- return { kind: "logical", op: n.op, left: walk(n.left), right: walk(n.right) };
802
- case "conditional":
803
- return { kind: "conditional", test: walk(n.test), consequent: walk(n.consequent), alternate: walk(n.alternate) };
804
- default:
805
- return n;
806
- }
807
- };
808
- const transformed = walk(parsed);
809
- if (!mutated)
810
- return expr;
811
- return stringifyParsedExpr(transformed);
742
+ renderParsedExprToPerl(expr) {
743
+ return emitParsedExpr(expr, new MojoTopLevelEmitter(this));
812
744
  }
813
- convertHigherOrderExpr(expr) {
814
- if (this.higherOrderInFlight.has(expr)) {
815
- this.errors.push({
816
- code: "BF101",
817
- severity: "error",
818
- message: `Cannot lower higher-order chain to Embedded Perl: ${expr.trim()}`,
819
- loc: { file: this.componentName + ".tsx", start: { line: 1, column: 0 }, end: { line: 1, column: 0 } },
820
- suggestion: {
821
- 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."
822
- }
823
- });
824
- return "''";
825
- }
826
- this.higherOrderInFlight.add(expr);
827
- try {
828
- const parsed = parseExpression2(expr);
829
- const support = isSupported(parsed);
830
- if (!support.supported) {
831
- this.errors.push({
832
- code: "BF101",
833
- severity: "error",
834
- message: `Cannot lower higher-order chain to Embedded Perl: ${expr.trim()}`,
835
- loc: { file: this.componentName + ".tsx", start: { line: 1, column: 0 }, end: { line: 1, column: 0 } },
836
- suggestion: {
837
- 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}
838
756
 
839
757
  Options:
840
758
  1. Use /* @client */ for client-side evaluation
841
759
  2. Pre-compute the value in Perl` : `Options:
842
760
  1. Use /* @client */ for client-side evaluation
843
761
  2. Pre-compute the value in Perl`
844
- }
845
- });
846
- return "''";
847
762
  }
848
- return this.renderParsedExprToPerl(parsed);
849
- } finally {
850
- this.higherOrderInFlight.delete(expr);
851
- }
852
- }
853
- renderParsedExprToPerl(expr) {
854
- return emitParsedExpr(expr, new MojoTopLevelEmitter(this));
855
- }
856
- _convertExpressionToPerlPublic(raw) {
857
- return this.convertExpressionToPerl(raw);
763
+ });
858
764
  }
859
765
  _renderPerlFilterExprPublic(expr, param) {
860
766
  return this.renderPerlFilterExpr(expr, param);
@@ -928,13 +834,35 @@ function renderSortMethod(recv, c) {
928
834
  });
929
835
  return `bf->sort(${recv}, { keys => [${keyHashes.join(", ")}] })`;
930
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
+ }
931
857
 
932
858
  class MojoFilterEmitter {
933
859
  param;
934
860
  localVarMap;
935
- constructor(param, localVarMap) {
861
+ isStringName;
862
+ constructor(param, localVarMap, isStringName = () => false) {
936
863
  this.param = param;
937
864
  this.localVarMap = localVarMap;
865
+ this.isStringName = isStringName;
938
866
  }
939
867
  identifier(name) {
940
868
  if (name === this.param)
@@ -978,10 +906,12 @@ class MojoFilterEmitter {
978
906
  binary(op, left, right, emit) {
979
907
  const l = emit(left);
980
908
  const r = emit(right);
981
- 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) {
982
912
  return `${l} eq ${r}`;
983
913
  }
984
- if ((op === "!==" || op === "!=") && right.kind === "literal" && right.literalType === "string") {
914
+ if ((op === "!==" || op === "!=") && stringCmp) {
985
915
  return `${l} ne ${r}`;
986
916
  }
987
917
  const opMap = {
@@ -1009,7 +939,7 @@ class MojoFilterEmitter {
1009
939
  }
1010
940
  higherOrder(method, object, param, predicate, emit) {
1011
941
  const arrayExpr = emit(object);
1012
- const predBody = emitParsedExpr(predicate, new MojoFilterEmitter(param, this.localVarMap));
942
+ const predBody = emitParsedExpr(predicate, new MojoFilterEmitter(param, this.localVarMap, this.isStringName));
1013
943
  const grepBody = predBody.replace(new RegExp(`\\$${param}\\b`, "g"), "$_");
1014
944
  if (method === "filter")
1015
945
  return `[grep { ${grepBody} } @{${arrayExpr}}]`;
@@ -1060,6 +990,9 @@ class MojoTopLevelEmitter {
1060
990
  return String(value);
1061
991
  }
1062
992
  member(object, property, _computed, emit) {
993
+ if (object.kind === "identifier" && object.name === "props") {
994
+ return `$${property}`;
995
+ }
1063
996
  const obj = emit(object);
1064
997
  if (property === "length")
1065
998
  return `scalar(@{${obj}})`;
@@ -1069,6 +1002,15 @@ class MojoTopLevelEmitter {
1069
1002
  if (callee.kind === "identifier" && args.length === 0) {
1070
1003
  return `$${callee.name}`;
1071
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
+ }
1072
1014
  return emit(callee);
1073
1015
  }
1074
1016
  unary(op, argument, emit) {
@@ -1082,10 +1024,12 @@ class MojoTopLevelEmitter {
1082
1024
  binary(op, left, right, emit) {
1083
1025
  const l = emit(left);
1084
1026
  const r = emit(right);
1085
- 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) {
1086
1030
  return `${l} eq ${r}`;
1087
1031
  }
1088
- if ((op === "!==" || op === "!=") && right.kind === "literal" && right.literalType === "string") {
1032
+ if ((op === "!==" || op === "!=") && stringCmp) {
1089
1033
  return `${l} ne ${r}`;
1090
1034
  }
1091
1035
  const opMap = {
@@ -1111,6 +1055,10 @@ class MojoTopLevelEmitter {
1111
1055
  return `(${l} // ${r})`;
1112
1056
  }
1113
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
+ }
1114
1062
  const arrayExpr = emit(object);
1115
1063
  const predBody = this.adapter._renderPerlFilterExprPublic(predicate, param);
1116
1064
  const grepBody = predBody.replace(new RegExp(`\\$${param}\\b`, "g"), "$_");
@@ -1134,14 +1082,28 @@ class MojoTopLevelEmitter {
1134
1082
  conditional(test, consequent, alternate, emit) {
1135
1083
  return `(${emit(test)} ? ${emit(consequent)} : ${emit(alternate)})`;
1136
1084
  }
1137
- templateLiteral(_parts) {
1138
- 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(" . ");
1139
1101
  }
1140
1102
  arrowFn(_param, _body) {
1141
- return "";
1103
+ return "''";
1142
1104
  }
1143
- unsupported(raw, _reason) {
1144
- return this.adapter._convertExpressionToPerlPublic(raw);
1105
+ unsupported(_raw, _reason) {
1106
+ return "''";
1145
1107
  }
1146
1108
  }
1147
1109
  var mojoAdapter = new MojoAdapter;