@saasmakers/eslint 1.0.25 → 1.0.26

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/index.cjs CHANGED
@@ -810,9 +810,61 @@ const propsPrefixRegex = /\$?props\.(\w+)/g;
810
810
  const trueAttributeRegex = /(?<![\w-]):?(?!aria-)([a-z0-9-]+)="true"/gi;
811
811
  const eventHandlerRegex = /(?:@|v-on:)[^\s="'<>/]+\s*=\s*"([^"]*)"/g;
812
812
  const bareIdentifierRegex = /^[a-z_$][\w$]*$/i;
813
+ const classBindingRegex = /(?<![\w-])(?::|v-bind:)class\s*=\s*"([^"]*)"/g;
814
+ const simpleTernaryRegex = /^([^?]+)\?\s*'([^']*)'\s*:\s*'([^']*)'$/;
813
815
  const singleCallRegex = /^([a-z_$][\w$]*)\s*\(.*\)$/is;
814
816
  const onPrefixRegex = /^on[A-Z]/;
815
817
  const exemptCallees = /* @__PURE__ */ new Set(["$emit", "emit"]);
818
+ function splitClassObjectProperties(body) {
819
+ const properties = [];
820
+ let depth = 0;
821
+ let quote;
822
+ let current = "";
823
+ for (let index = 0; index < body.length; index++) {
824
+ const char = body[index];
825
+ if (quote !== void 0) {
826
+ current += char;
827
+ if (char === quote && body[index - 1] !== "\\") {
828
+ quote = void 0;
829
+ }
830
+ continue;
831
+ }
832
+ if (char === "'" || char === '"' || char === "`") {
833
+ quote = char;
834
+ current += char;
835
+ continue;
836
+ }
837
+ if (char === "(" || char === "[" || char === "{") {
838
+ depth++;
839
+ } else if (char === ")" || char === "]" || char === "}") {
840
+ depth--;
841
+ }
842
+ if (char === "," && depth === 0) {
843
+ const property2 = current.trim();
844
+ if (property2 !== "") {
845
+ properties.push(property2);
846
+ }
847
+ current = "";
848
+ continue;
849
+ }
850
+ current += char;
851
+ }
852
+ const property = current.trim();
853
+ if (property !== "") {
854
+ properties.push(property);
855
+ }
856
+ return properties;
857
+ }
858
+ function buildClassObjectText(properties, baseIndent) {
859
+ if (properties.length <= 1) {
860
+ return `{ ${properties.join(", ")} }`;
861
+ }
862
+ const propertyIndent = `${baseIndent} `;
863
+ const lines = properties.map((property) => `${propertyIndent}${property},`);
864
+ return `{
865
+ ${lines.join("\n")}
866
+ ${baseIndent}}`;
867
+ }
816
868
  const dollarTPatterns = [
817
869
  {
818
870
  pattern: / \$t\(/g,
@@ -838,13 +890,16 @@ const dollarTPatterns = [
838
890
  const rule = {
839
891
  defaultOptions: [],
840
892
  meta: {
841
- docs: { description: 'Format Vue templates: strip unnecessary props/$props prefixes, redundant ="true" attributes (except aria- attributes), enforce t() over $t(), require "on" prefix on named event listener handlers, and disallow inline event handler expressions' },
893
+ docs: { description: 'Format Vue templates: strip unnecessary props/$props prefixes, redundant ="true" attributes (except aria- attributes), enforce t() over $t(), require "on" prefix on named event listener handlers, disallow inline event handler expressions, require object syntax for dynamic :class bindings, and keep single-property :class objects on one line while spreading multi-property objects across lines' },
842
894
  fixable: "code",
843
895
  messages: {
896
+ dynamicClassMustBeObject: `Dynamic :class binding must use object syntax (e.g. :class="{ 'foo': condition }"). Ternaries, arrays, strings and bare variables are not allowed.`,
844
897
  eventHandlerMustBeFunction: 'Event listener must reference a named handler function (e.g. @click="onClose") or call one (e.g. @click="onClose($event)"). Inline expressions are not allowed.',
898
+ multilineClassObject: "Dynamic :class object with multiple properties must span multiple lines, one property per line.",
845
899
  noPropsPrefix: "Unnecessary props/$props prefix in template. Props are automatically available in template scope.",
846
900
  prefixEventHandlerWithOn: 'Event listener handler "{{name}}" must be prefixed with "on" (e.g. @click="onClick").',
847
901
  removeTrueAttribute: 'Unnecessary ="true" attribute. Use the attribute name directly instead.',
902
+ singleLineClassObject: "Dynamic :class object with a single property must stay on one line.",
848
903
  useT: "Use t() instead of $t() in Vue templates as it does not work with <i18n> tags."
849
904
  },
850
905
  schema: [],
@@ -957,6 +1012,68 @@ const rule = {
957
1012
  }
958
1013
  eventHandlerMatch = eventHandlerRegex.exec(templateContent);
959
1014
  }
1015
+ classBindingRegex.lastIndex = 0;
1016
+ let classMatch = classBindingRegex.exec(templateContent);
1017
+ while (classMatch !== null) {
1018
+ const rawValue = classMatch[1];
1019
+ const trimmedValue = rawValue.trim();
1020
+ if (trimmedValue === "") {
1021
+ classMatch = classBindingRegex.exec(templateContent);
1022
+ continue;
1023
+ }
1024
+ const valueStart = templateStartIndex + classMatch.index + classMatch[0].length - 1 - rawValue.length;
1025
+ const lineStart = templateContent.lastIndexOf("\n", classMatch.index) + 1;
1026
+ const baseIndent = (/^[ \t]*/.exec(templateContent.slice(lineStart)) ?? [""])[0];
1027
+ if (trimmedValue.startsWith("{")) {
1028
+ const objectStartInRaw = rawValue.indexOf("{");
1029
+ const objectEndInRaw = rawValue.lastIndexOf("}");
1030
+ if (objectEndInRaw > objectStartInRaw) {
1031
+ const objectText = rawValue.slice(objectStartInRaw, objectEndInRaw + 1);
1032
+ const properties = splitClassObjectProperties(rawValue.slice(objectStartInRaw + 1, objectEndInRaw));
1033
+ const isMultiline = objectText.includes("\n");
1034
+ const shouldBeMultiline = properties.length >= 2;
1035
+ if (properties.length > 0 && shouldBeMultiline !== isMultiline) {
1036
+ const objectStart = valueStart + objectStartInRaw;
1037
+ const objectEnd = valueStart + objectEndInRaw + 1;
1038
+ context.report({
1039
+ fix: (fixer) => fixer.replaceTextRange([objectStart, objectEnd], buildClassObjectText(properties, baseIndent)),
1040
+ loc: {
1041
+ end: sourceCode.getLocFromIndex(objectEnd),
1042
+ start: sourceCode.getLocFromIndex(objectStart)
1043
+ },
1044
+ messageId: shouldBeMultiline ? "multilineClassObject" : "singleLineClassObject"
1045
+ });
1046
+ }
1047
+ }
1048
+ classMatch = classBindingRegex.exec(templateContent);
1049
+ continue;
1050
+ }
1051
+ const valueEnd = valueStart + rawValue.length;
1052
+ const ternaryMatch = simpleTernaryRegex.exec(trimmedValue);
1053
+ let fix;
1054
+ if (ternaryMatch) {
1055
+ const condition = ternaryMatch[1].trim();
1056
+ const truthyClass = ternaryMatch[2];
1057
+ const falsyClass = ternaryMatch[3];
1058
+ const entries = [];
1059
+ if (truthyClass !== "") {
1060
+ entries.push(`'${truthyClass}': ${condition}`);
1061
+ }
1062
+ if (falsyClass !== "") {
1063
+ entries.push(`'${falsyClass}': !(${condition})`);
1064
+ }
1065
+ fix = (fixer) => fixer.replaceTextRange([valueStart, valueEnd], buildClassObjectText(entries, baseIndent));
1066
+ }
1067
+ context.report({
1068
+ ...fix ? { fix } : {},
1069
+ loc: {
1070
+ end: sourceCode.getLocFromIndex(valueEnd),
1071
+ start: sourceCode.getLocFromIndex(valueStart)
1072
+ },
1073
+ messageId: "dynamicClassMustBeObject"
1074
+ });
1075
+ classMatch = classBindingRegex.exec(templateContent);
1076
+ }
960
1077
  }
961
1078
  };
962
1079
  }
package/dist/index.d.cts CHANGED
@@ -5203,7 +5203,7 @@ declare const _default: {
5203
5203
  locales?: string[];
5204
5204
  } | undefined)?], unknown, RuleListener>;
5205
5205
  'vue-format-script': RuleModule<"multilineComputed" | "untypedEmits", [], unknown, RuleListener>;
5206
- 'vue-format-template': RuleModule<"eventHandlerMustBeFunction" | "noPropsPrefix" | "prefixEventHandlerWithOn" | "removeTrueAttribute" | "useT", [], unknown, RuleListener>;
5206
+ 'vue-format-template': RuleModule<"dynamicClassMustBeObject" | "eventHandlerMustBeFunction" | "multilineClassObject" | "noPropsPrefix" | "prefixEventHandlerWithOn" | "removeTrueAttribute" | "singleLineClassObject" | "useT", [], unknown, RuleListener>;
5207
5207
  };
5208
5208
  };
5209
5209
 
package/dist/index.d.mts CHANGED
@@ -5203,7 +5203,7 @@ declare const _default: {
5203
5203
  locales?: string[];
5204
5204
  } | undefined)?], unknown, RuleListener>;
5205
5205
  'vue-format-script': RuleModule<"multilineComputed" | "untypedEmits", [], unknown, RuleListener>;
5206
- 'vue-format-template': RuleModule<"eventHandlerMustBeFunction" | "noPropsPrefix" | "prefixEventHandlerWithOn" | "removeTrueAttribute" | "useT", [], unknown, RuleListener>;
5206
+ 'vue-format-template': RuleModule<"dynamicClassMustBeObject" | "eventHandlerMustBeFunction" | "multilineClassObject" | "noPropsPrefix" | "prefixEventHandlerWithOn" | "removeTrueAttribute" | "singleLineClassObject" | "useT", [], unknown, RuleListener>;
5207
5207
  };
5208
5208
  };
5209
5209
 
package/dist/index.d.ts CHANGED
@@ -5203,7 +5203,7 @@ declare const _default: {
5203
5203
  locales?: string[];
5204
5204
  } | undefined)?], unknown, RuleListener>;
5205
5205
  'vue-format-script': RuleModule<"multilineComputed" | "untypedEmits", [], unknown, RuleListener>;
5206
- 'vue-format-template': RuleModule<"eventHandlerMustBeFunction" | "noPropsPrefix" | "prefixEventHandlerWithOn" | "removeTrueAttribute" | "useT", [], unknown, RuleListener>;
5206
+ 'vue-format-template': RuleModule<"dynamicClassMustBeObject" | "eventHandlerMustBeFunction" | "multilineClassObject" | "noPropsPrefix" | "prefixEventHandlerWithOn" | "removeTrueAttribute" | "singleLineClassObject" | "useT", [], unknown, RuleListener>;
5207
5207
  };
5208
5208
  };
5209
5209
 
package/dist/index.mjs CHANGED
@@ -808,9 +808,61 @@ const propsPrefixRegex = /\$?props\.(\w+)/g;
808
808
  const trueAttributeRegex = /(?<![\w-]):?(?!aria-)([a-z0-9-]+)="true"/gi;
809
809
  const eventHandlerRegex = /(?:@|v-on:)[^\s="'<>/]+\s*=\s*"([^"]*)"/g;
810
810
  const bareIdentifierRegex = /^[a-z_$][\w$]*$/i;
811
+ const classBindingRegex = /(?<![\w-])(?::|v-bind:)class\s*=\s*"([^"]*)"/g;
812
+ const simpleTernaryRegex = /^([^?]+)\?\s*'([^']*)'\s*:\s*'([^']*)'$/;
811
813
  const singleCallRegex = /^([a-z_$][\w$]*)\s*\(.*\)$/is;
812
814
  const onPrefixRegex = /^on[A-Z]/;
813
815
  const exemptCallees = /* @__PURE__ */ new Set(["$emit", "emit"]);
816
+ function splitClassObjectProperties(body) {
817
+ const properties = [];
818
+ let depth = 0;
819
+ let quote;
820
+ let current = "";
821
+ for (let index = 0; index < body.length; index++) {
822
+ const char = body[index];
823
+ if (quote !== void 0) {
824
+ current += char;
825
+ if (char === quote && body[index - 1] !== "\\") {
826
+ quote = void 0;
827
+ }
828
+ continue;
829
+ }
830
+ if (char === "'" || char === '"' || char === "`") {
831
+ quote = char;
832
+ current += char;
833
+ continue;
834
+ }
835
+ if (char === "(" || char === "[" || char === "{") {
836
+ depth++;
837
+ } else if (char === ")" || char === "]" || char === "}") {
838
+ depth--;
839
+ }
840
+ if (char === "," && depth === 0) {
841
+ const property2 = current.trim();
842
+ if (property2 !== "") {
843
+ properties.push(property2);
844
+ }
845
+ current = "";
846
+ continue;
847
+ }
848
+ current += char;
849
+ }
850
+ const property = current.trim();
851
+ if (property !== "") {
852
+ properties.push(property);
853
+ }
854
+ return properties;
855
+ }
856
+ function buildClassObjectText(properties, baseIndent) {
857
+ if (properties.length <= 1) {
858
+ return `{ ${properties.join(", ")} }`;
859
+ }
860
+ const propertyIndent = `${baseIndent} `;
861
+ const lines = properties.map((property) => `${propertyIndent}${property},`);
862
+ return `{
863
+ ${lines.join("\n")}
864
+ ${baseIndent}}`;
865
+ }
814
866
  const dollarTPatterns = [
815
867
  {
816
868
  pattern: / \$t\(/g,
@@ -836,13 +888,16 @@ const dollarTPatterns = [
836
888
  const rule = {
837
889
  defaultOptions: [],
838
890
  meta: {
839
- docs: { description: 'Format Vue templates: strip unnecessary props/$props prefixes, redundant ="true" attributes (except aria- attributes), enforce t() over $t(), require "on" prefix on named event listener handlers, and disallow inline event handler expressions' },
891
+ docs: { description: 'Format Vue templates: strip unnecessary props/$props prefixes, redundant ="true" attributes (except aria- attributes), enforce t() over $t(), require "on" prefix on named event listener handlers, disallow inline event handler expressions, require object syntax for dynamic :class bindings, and keep single-property :class objects on one line while spreading multi-property objects across lines' },
840
892
  fixable: "code",
841
893
  messages: {
894
+ dynamicClassMustBeObject: `Dynamic :class binding must use object syntax (e.g. :class="{ 'foo': condition }"). Ternaries, arrays, strings and bare variables are not allowed.`,
842
895
  eventHandlerMustBeFunction: 'Event listener must reference a named handler function (e.g. @click="onClose") or call one (e.g. @click="onClose($event)"). Inline expressions are not allowed.',
896
+ multilineClassObject: "Dynamic :class object with multiple properties must span multiple lines, one property per line.",
843
897
  noPropsPrefix: "Unnecessary props/$props prefix in template. Props are automatically available in template scope.",
844
898
  prefixEventHandlerWithOn: 'Event listener handler "{{name}}" must be prefixed with "on" (e.g. @click="onClick").',
845
899
  removeTrueAttribute: 'Unnecessary ="true" attribute. Use the attribute name directly instead.',
900
+ singleLineClassObject: "Dynamic :class object with a single property must stay on one line.",
846
901
  useT: "Use t() instead of $t() in Vue templates as it does not work with <i18n> tags."
847
902
  },
848
903
  schema: [],
@@ -955,6 +1010,68 @@ const rule = {
955
1010
  }
956
1011
  eventHandlerMatch = eventHandlerRegex.exec(templateContent);
957
1012
  }
1013
+ classBindingRegex.lastIndex = 0;
1014
+ let classMatch = classBindingRegex.exec(templateContent);
1015
+ while (classMatch !== null) {
1016
+ const rawValue = classMatch[1];
1017
+ const trimmedValue = rawValue.trim();
1018
+ if (trimmedValue === "") {
1019
+ classMatch = classBindingRegex.exec(templateContent);
1020
+ continue;
1021
+ }
1022
+ const valueStart = templateStartIndex + classMatch.index + classMatch[0].length - 1 - rawValue.length;
1023
+ const lineStart = templateContent.lastIndexOf("\n", classMatch.index) + 1;
1024
+ const baseIndent = (/^[ \t]*/.exec(templateContent.slice(lineStart)) ?? [""])[0];
1025
+ if (trimmedValue.startsWith("{")) {
1026
+ const objectStartInRaw = rawValue.indexOf("{");
1027
+ const objectEndInRaw = rawValue.lastIndexOf("}");
1028
+ if (objectEndInRaw > objectStartInRaw) {
1029
+ const objectText = rawValue.slice(objectStartInRaw, objectEndInRaw + 1);
1030
+ const properties = splitClassObjectProperties(rawValue.slice(objectStartInRaw + 1, objectEndInRaw));
1031
+ const isMultiline = objectText.includes("\n");
1032
+ const shouldBeMultiline = properties.length >= 2;
1033
+ if (properties.length > 0 && shouldBeMultiline !== isMultiline) {
1034
+ const objectStart = valueStart + objectStartInRaw;
1035
+ const objectEnd = valueStart + objectEndInRaw + 1;
1036
+ context.report({
1037
+ fix: (fixer) => fixer.replaceTextRange([objectStart, objectEnd], buildClassObjectText(properties, baseIndent)),
1038
+ loc: {
1039
+ end: sourceCode.getLocFromIndex(objectEnd),
1040
+ start: sourceCode.getLocFromIndex(objectStart)
1041
+ },
1042
+ messageId: shouldBeMultiline ? "multilineClassObject" : "singleLineClassObject"
1043
+ });
1044
+ }
1045
+ }
1046
+ classMatch = classBindingRegex.exec(templateContent);
1047
+ continue;
1048
+ }
1049
+ const valueEnd = valueStart + rawValue.length;
1050
+ const ternaryMatch = simpleTernaryRegex.exec(trimmedValue);
1051
+ let fix;
1052
+ if (ternaryMatch) {
1053
+ const condition = ternaryMatch[1].trim();
1054
+ const truthyClass = ternaryMatch[2];
1055
+ const falsyClass = ternaryMatch[3];
1056
+ const entries = [];
1057
+ if (truthyClass !== "") {
1058
+ entries.push(`'${truthyClass}': ${condition}`);
1059
+ }
1060
+ if (falsyClass !== "") {
1061
+ entries.push(`'${falsyClass}': !(${condition})`);
1062
+ }
1063
+ fix = (fixer) => fixer.replaceTextRange([valueStart, valueEnd], buildClassObjectText(entries, baseIndent));
1064
+ }
1065
+ context.report({
1066
+ ...fix ? { fix } : {},
1067
+ loc: {
1068
+ end: sourceCode.getLocFromIndex(valueEnd),
1069
+ start: sourceCode.getLocFromIndex(valueStart)
1070
+ },
1071
+ messageId: "dynamicClassMustBeObject"
1072
+ });
1073
+ classMatch = classBindingRegex.exec(templateContent);
1074
+ }
958
1075
  }
959
1076
  };
960
1077
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@saasmakers/eslint",
3
- "version": "1.0.25",
3
+ "version": "1.0.26",
4
4
  "private": false,
5
5
  "description": "Shared ESLint config and rules for SaaS Makers projects",
6
6
  "license": "MIT",