@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 +118 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.mts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.mjs +118 -1
- package/package.json +1 -1
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,
|
|
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,
|
|
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
|
}
|