@saasmakers/eslint 1.0.19 → 1.0.20

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.mjs CHANGED
@@ -2,138 +2,62 @@ import { d as distExports } from './shared/eslint.DOaqyhfZ.mjs';
2
2
  import 'eslint';
3
3
  import 'eslint/use-at-your-own-risk';
4
4
 
5
- const rule$c = {
6
- meta: {
7
- docs: {
8
- description: "Enforce one-line ternaries if under a character limit, multiline otherwise",
9
- recommended: true
10
- },
11
- fixable: "whitespace",
12
- messages: {
13
- multiline: "Ternary expression should be multiline when it exceeds max characters.",
14
- singleLine: "Ternary expression should be single-line when it is under max characters."
15
- },
16
- schema: [
17
- {
18
- additionalProperties: false,
19
- properties: { maxCharacters: { type: "number" } },
20
- type: "object"
21
- }
22
- ],
23
- type: "layout"
24
- },
25
- create(context) {
26
- const maxCharacters = context.options[0]?.maxCharacters ?? 100;
27
- return {
28
- ConditionalExpression(node) {
29
- if (node.type !== distExports.AST_NODE_TYPES.ConditionalExpression) {
30
- return;
31
- }
32
- const sourceCode = context.sourceCode;
33
- const ternaryText = sourceCode.getText(node);
34
- const isMultiline = ternaryText.includes("\n");
35
- const isOverMaxLength = ternaryText.length > maxCharacters;
36
- if (isOverMaxLength && !isMultiline) {
37
- context.report({
38
- fix: (fixer) => {
39
- const [
40
- test,
41
- consequent,
42
- alternate
43
- ] = [
44
- node.test,
45
- node.consequent,
46
- node.alternate
47
- ].map((part) => sourceCode.getText(part));
48
- return fixer.replaceText(node, `${test} ?
5
+ const defaultMaxCharacters = 100;
6
+ const defaultMaxLineLength = 100;
7
+ const defaultMinItems = 5;
8
+ function checkTernary(context, node, maxCharacters) {
9
+ const sourceCode = context.sourceCode;
10
+ const ternaryText = sourceCode.getText(node);
11
+ const isMultiline = ternaryText.includes("\n");
12
+ const isOverMaxLength = ternaryText.length > maxCharacters;
13
+ if (isOverMaxLength && !isMultiline) {
14
+ context.report({
15
+ fix: (fixer) => {
16
+ const [test, consequent, alternate] = [node.test, node.consequent, node.alternate].map((part) => sourceCode.getText(part));
17
+ return fixer.replaceText(node, `${test} ?
49
18
  ${consequent} :
50
19
  ${alternate}`);
51
- },
52
- messageId: "multiline",
53
- node
54
- });
55
- } else if (!isOverMaxLength && isMultiline) {
56
- context.report({
57
- fix: (fixer) => fixer.replaceText(node, ternaryText.replaceAll(/\s+/g, " ")),
58
- messageId: "singleLine",
59
- node
60
- });
61
- }
62
- }
63
- };
20
+ },
21
+ messageId: "ternaryMultiline",
22
+ node
23
+ });
24
+ } else if (!isOverMaxLength && isMultiline) {
25
+ context.report({
26
+ fix: (fixer) => fixer.replaceText(node, ternaryText.replaceAll(/\s+/g, " ")),
27
+ messageId: "ternarySingleLine",
28
+ node
29
+ });
64
30
  }
65
- };
66
-
67
- const rule$b = {
68
- meta: {
69
- docs: {
70
- description: "Enforce multiline formatting for union types with more than N items",
71
- recommended: false
72
- },
73
- fixable: "whitespace",
74
- messages: {
75
- multiline: "Union type should be multiline when it exceeds max length or has too many items.",
76
- singleLine: "Union type should be single-line when it is under max length and has few items."
77
- },
78
- schema: [
79
- {
80
- additionalProperties: false,
81
- properties: {
82
- maxLineLength: { type: "number" },
83
- minItems: { type: "number" }
84
- },
85
- type: "object"
86
- }
87
- ],
88
- type: "layout"
89
- },
90
- create(context) {
91
- const minItems = context.options[0]?.minItems ?? 5;
92
- const maxLineLength = context.options[0]?.maxLineLength ?? 100;
93
- function reportAndFix(node, messageId, fixFunction) {
94
- context.report({
95
- fix: fixFunction,
96
- messageId,
97
- node
98
- });
99
- }
100
- return {
101
- // @ts-expect-error TSUnionType is not the right type
102
- TSUnionType(node) {
103
- const sourceCode = context.sourceCode;
104
- const unionText = sourceCode.getText(node);
105
- const isMultiline = unionText.includes("\n");
106
- const isOverMaxLength = unionText.length > maxLineLength;
107
- const hasEnoughItems = node.types.length >= minItems;
108
- if ((isOverMaxLength || hasEnoughItems) && !isMultiline) {
109
- reportAndFix(
110
- node,
111
- "multiline",
112
- (fixer) => {
113
- const types = node.types.map((type) => {
114
- return sourceCode.getText(type);
115
- });
116
- return fixer.replaceText(node, `
31
+ }
32
+ function checkUnion(context, node, maxLineLength, minItems) {
33
+ const sourceCode = context.sourceCode;
34
+ const unionText = sourceCode.getText(node);
35
+ const isMultiline = unionText.includes("\n");
36
+ const isOverMaxLength = unionText.length > maxLineLength;
37
+ const hasEnoughItems = node.types.length >= minItems;
38
+ if ((isOverMaxLength || hasEnoughItems) && !isMultiline) {
39
+ context.report({
40
+ fix: (fixer) => {
41
+ const types = node.types.map((type) => sourceCode.getText(type));
42
+ return fixer.replaceText(node, `
117
43
  | ${types.join("\n | ")}`);
118
- }
119
- );
120
- } else if (!isOverMaxLength && !hasEnoughItems && isMultiline) {
121
- reportAndFix(
122
- node,
123
- "singleLine",
124
- (fixer) => {
125
- let singleLineText = unionText.replaceAll(/[ \t\n]+/g, " ");
126
- singleLineText = singleLineText.replaceAll(" |", "|").replaceAll("|", " |");
127
- singleLineText = singleLineText.replaceAll(" ", " ");
128
- return fixer.replaceText(node, singleLineText);
129
- }
130
- );
131
- }
132
- }
133
- };
44
+ },
45
+ messageId: "unionMultiline",
46
+ node
47
+ });
48
+ } else if (!isOverMaxLength && !hasEnoughItems && isMultiline) {
49
+ context.report({
50
+ fix: (fixer) => {
51
+ let singleLineText = unionText.replaceAll(/[ \t\n]+/g, " ");
52
+ singleLineText = singleLineText.replaceAll(" |", "|").replaceAll("|", " |");
53
+ singleLineText = singleLineText.replaceAll(" ", " ");
54
+ return fixer.replaceText(node, singleLineText);
55
+ },
56
+ messageId: "unionSingleLine",
57
+ node
58
+ });
134
59
  }
135
- };
136
-
60
+ }
137
61
  function collapseBlankLine(text) {
138
62
  return text.replace(/\n[^\S\n]*\n[^\S\n]*/, "\n");
139
63
  }
@@ -327,21 +251,38 @@ function isThisReceiver(node) {
327
251
  function isVoidExpression(expression) {
328
252
  return expression.type === distExports.AST_NODE_TYPES.UnaryExpression && expression.operator === "void";
329
253
  }
330
- const rule$a = {
331
- defaultOptions: [],
254
+ const rule$4 = {
255
+ defaultOptions: [{}],
332
256
  meta: {
333
- docs: { description: "Require or disallow padding lines between const, let, await, and expression statements" },
257
+ docs: { description: "Format TypeScript layout: single/multiline ternaries and union types, and blank lines between statements" },
334
258
  fixable: "whitespace",
335
259
  messages: {
336
260
  expectedBlankLine: "Expected blank line before this statement.",
337
261
  expectedBlankLineBeforeComment: "Expected blank line before this comment.",
338
- unexpectedBlankLine: "Unexpected blank line before this statement."
262
+ ternaryMultiline: "Ternary expression should be multiline when it exceeds max characters.",
263
+ ternarySingleLine: "Ternary expression should be single-line when it is under max characters.",
264
+ unexpectedBlankLine: "Unexpected blank line before this statement.",
265
+ unionMultiline: "Union type should be multiline when it exceeds max length or has too many items.",
266
+ unionSingleLine: "Union type should be single-line when it is under max length and has few items."
339
267
  },
340
- schema: [],
268
+ schema: [
269
+ {
270
+ additionalProperties: false,
271
+ properties: {
272
+ maxCharacters: { type: "number" },
273
+ maxLineLength: { type: "number" },
274
+ minItems: { type: "number" }
275
+ },
276
+ type: "object"
277
+ }
278
+ ],
341
279
  type: "layout"
342
280
  },
343
281
  create(context) {
344
282
  const sourceCode = context.sourceCode;
283
+ const maxCharacters = context.options[0]?.maxCharacters ?? defaultMaxCharacters;
284
+ const maxLineLength = context.options[0]?.maxLineLength ?? defaultMaxLineLength;
285
+ const minItems = context.options[0]?.minItems ?? defaultMinItems;
345
286
  let scopeInfo = null;
346
287
  function enterScope() {
347
288
  scopeInfo = {
@@ -440,6 +381,7 @@ const rule$a = {
440
381
  ":statement": verify,
441
382
  "BlockStatement": enterScope,
442
383
  "BlockStatement:exit": exitScope,
384
+ "ConditionalExpression": (node) => checkTernary(context, node, maxCharacters),
443
385
  "Program": enterScope,
444
386
  "Program:exit": function() {
445
387
  for (const comment of sourceCode.getAllComments()) {
@@ -450,7 +392,8 @@ const rule$a = {
450
392
  "StaticBlock": enterScope,
451
393
  "StaticBlock:exit": exitScope,
452
394
  "SwitchCase": verifyThenEnterScope,
453
- "SwitchCase:exit": exitScope
395
+ "SwitchCase:exit": exitScope,
396
+ "TSUnionType": (node) => checkUnion(context, node, maxLineLength, minItems)
454
397
  };
455
398
  }
456
399
  };
@@ -503,7 +446,7 @@ function getTestPriority(testName) {
503
446
  return 1;
504
447
  }
505
448
  }
506
- const rule$9 = {
449
+ const rule$3 = {
507
450
  defaultOptions: [],
508
451
  meta: {
509
452
  docs: { description: "Enforce sorted test functions grouped by method with sorted metrics, errors, exceptions and middlewares" },
@@ -570,63 +513,68 @@ const rule$9 = {
570
513
  }
571
514
  };
572
515
 
573
- function checkInvalidLocales(context, parsed, locales, startOffset, i18nContent) {
516
+ const defaultLocales = ["en", "fr"];
517
+ const indentSpaces = 2;
518
+ const i18nRegex = /(<i18n\s+lang=["']json["']>)([\s\S]*?)<\/i18n>/i;
519
+ const templateRegex$1 = /<template>([\s\S]*)<\/template>/i;
520
+ const scriptRegex = /<script[^>]*>([\s\S]*?)<\/script>/i;
521
+ function checkInvalidLocales(context, parsed, locales, content, contentOffset) {
574
522
  for (const locale of Object.keys(parsed)) {
575
- if (!locales.includes(locale)) {
576
- const escapedLocale = locale.replaceAll(/[.*+?^${}()|[\]\\]/g, String.raw`\$&`);
577
- const localeMatch = new RegExp(String.raw`"${escapedLocale}"\s*:`, "g").exec(i18nContent);
578
- if (localeMatch) {
579
- const localeOffset = startOffset + localeMatch.index;
580
- context.report({
581
- data: {
582
- allowed: locales.join(", "),
583
- locale
584
- },
585
- loc: {
586
- end: context.sourceCode.getLocFromIndex(localeOffset + locale.length + 2),
587
- start: context.sourceCode.getLocFromIndex(localeOffset)
588
- },
589
- messageId: "invalidLocale"
590
- });
591
- }
523
+ if (locales.includes(locale)) {
524
+ continue;
525
+ }
526
+ const localeMatch = new RegExp(String.raw`"${escapeRegExp(locale)}"\s*:`, "g").exec(content);
527
+ if (localeMatch) {
528
+ const localeOffset = contentOffset + localeMatch.index;
529
+ context.report({
530
+ data: {
531
+ allowed: locales.join(", "),
532
+ locale
533
+ },
534
+ loc: {
535
+ end: context.sourceCode.getLocFromIndex(localeOffset + locale.length + 2),
536
+ start: context.sourceCode.getLocFromIndex(localeOffset)
537
+ },
538
+ messageId: "invalidLocale"
539
+ });
592
540
  }
593
541
  }
594
542
  }
595
- function checkMissingLocales(context, parsed, locales, startOffset, i18nContent) {
543
+ function checkMissingLocales(context, parsed, locales, content, contentOffset) {
596
544
  for (const locale of locales) {
597
545
  if (!parsed[locale]) {
598
546
  context.report({
599
547
  data: { locale },
600
548
  loc: {
601
- end: context.sourceCode.getLocFromIndex(startOffset + i18nContent.length),
602
- start: context.sourceCode.getLocFromIndex(startOffset)
549
+ end: context.sourceCode.getLocFromIndex(contentOffset + content.length),
550
+ start: context.sourceCode.getLocFromIndex(contentOffset)
603
551
  },
604
552
  messageId: "missingLocale"
605
553
  });
606
554
  }
607
555
  }
608
556
  }
609
- function checkMissingTranslations(context, parsed, locales, startOffset, i18nContent) {
557
+ function checkMissingTranslations(context, parsed, locales, content, contentOffset) {
610
558
  const allKeys = /* @__PURE__ */ new Set();
611
559
  for (const locale of locales) {
612
560
  if (parsed[locale]) {
613
- const keys = getAllKeys$1(parsed[locale]);
614
- for (const key of keys) allKeys.add(key);
561
+ for (const key of getAllKeys(parsed[locale])) {
562
+ allKeys.add(key);
563
+ }
615
564
  }
616
565
  }
617
566
  for (const locale of locales) {
618
567
  if (!parsed[locale]) {
619
568
  continue;
620
569
  }
621
- const localeKeys = getAllKeys$1(parsed[locale]);
570
+ const localeKeys = getAllKeys(parsed[locale]);
622
571
  const missingKeys = [...allKeys].filter((key) => !localeKeys.includes(key));
623
572
  if (missingKeys.length === 0) {
624
573
  continue;
625
574
  }
626
- const escapedLocale = locale.replaceAll(/[.*+?^${}()|[\]\\]/g, String.raw`\$&`);
627
- const localeMatch = new RegExp(String.raw`"${escapedLocale}"\s*:\s*\{`, "g").exec(i18nContent);
575
+ const localeMatch = new RegExp(String.raw`"${escapeRegExp(locale)}"\s*:\s*\{`, "g").exec(content);
628
576
  if (localeMatch) {
629
- const localeOffset = startOffset + localeMatch.index;
577
+ const localeOffset = contentOffset + localeMatch.index;
630
578
  context.report({
631
579
  data: {
632
580
  locale,
@@ -641,152 +589,21 @@ function checkMissingTranslations(context, parsed, locales, startOffset, i18nCon
641
589
  }
642
590
  }
643
591
  }
644
- function getAllKeys$1(object, prefix = "") {
592
+ function escapeRegExp(value) {
593
+ return value.replaceAll(/[.*+?^${}()|[\]\\]/g, String.raw`\$&`);
594
+ }
595
+ function getAllKeys(object, prefix = "") {
645
596
  let keys = [];
646
597
  for (const key in object) {
647
598
  const newPrefix = prefix ? `${prefix}.${key}` : key;
648
599
  if (typeof object[key] === "object" && object[key] !== null) {
649
- keys = [...keys, ...getAllKeys$1(object[key], newPrefix)];
600
+ keys = [...keys, ...getAllKeys(object[key], newPrefix)];
650
601
  } else {
651
602
  keys.push(newPrefix);
652
603
  }
653
604
  }
654
605
  return keys;
655
606
  }
656
- const rule$8 = {
657
- meta: {
658
- docs: {
659
- description: "Enforce consistent i18n locale keys across translations",
660
- recommended: true
661
- },
662
- messages: {
663
- invalidJson: "Invalid JSON in i18n block: {{error}}",
664
- invalidLocale: "Invalid locale: {{locale}}. Allowed locales are: {{allowed}}",
665
- missingLocale: "Missing required locale: {{locale}}",
666
- missingTranslations: 'Missing translations in "{{locale}}" locale: {{missing}}'
667
- },
668
- schema: [
669
- {
670
- additionalProperties: false,
671
- properties: {
672
- locales: {
673
- items: { type: "string" },
674
- type: "array"
675
- }
676
- },
677
- type: "object"
678
- }
679
- ],
680
- type: "problem"
681
- },
682
- create(context) {
683
- if (!context.filename.endsWith(".vue")) {
684
- return {};
685
- }
686
- const locales = context.options[0]?.locales || ["en", "fr"];
687
- return {
688
- Program() {
689
- const source = context.sourceCode.getText();
690
- const i18nRegex = /<i18n\s+lang=["']json["']>([\s\S]*?)<\/i18n>/i;
691
- const i18nMatch = i18nRegex.exec(source);
692
- if (!i18nMatch) {
693
- return;
694
- }
695
- const tagLength = source.indexOf(">", i18nMatch.index) + 1 - i18nMatch.index;
696
- const startOffset = i18nMatch.index + tagLength;
697
- const i18nContent = i18nMatch[1].trim();
698
- try {
699
- const parsed = JSON.parse(i18nContent);
700
- checkMissingLocales(context, parsed, locales, startOffset, i18nContent);
701
- checkInvalidLocales(context, parsed, locales, startOffset, i18nContent);
702
- checkMissingTranslations(context, parsed, locales, startOffset, i18nContent);
703
- } catch (error) {
704
- const errorMessage = error instanceof Error ? error.message : String(error);
705
- context.report({
706
- data: { error: errorMessage },
707
- loc: {
708
- end: context.sourceCode.getLocFromIndex(startOffset + i18nContent.length),
709
- start: context.sourceCode.getLocFromIndex(startOffset)
710
- },
711
- messageId: "invalidJson"
712
- });
713
- }
714
- }
715
- };
716
- }
717
- };
718
-
719
- const rule$7 = {
720
- meta: {
721
- docs: {
722
- description: "Enforce using t() instead of $t() in Vue templates",
723
- recommended: true
724
- },
725
- fixable: "code",
726
- messages: { useT: "Use t() instead of $t() in Vue templates as it does not work with <i18n> tags." },
727
- schema: [],
728
- type: "problem"
729
- },
730
- create(context) {
731
- if (!context.filename.endsWith(".vue")) {
732
- return {};
733
- }
734
- return {
735
- Program() {
736
- const sourceCode = context.sourceCode;
737
- const source = sourceCode.getText();
738
- const templateRegex = /<template>([\s\S]*)<\/template>/i;
739
- const templateMatch = templateRegex.exec(source);
740
- if (!templateMatch) {
741
- return;
742
- }
743
- const templateContent = templateMatch[1];
744
- const tPatterns = [
745
- {
746
- pattern: / \$t\(/g,
747
- quote: " "
748
- },
749
- {
750
- pattern: /"\$t\(/g,
751
- quote: '"'
752
- },
753
- {
754
- pattern: /`\$t\(/g,
755
- quote: "`"
756
- },
757
- {
758
- pattern: /\{\$t\(/g,
759
- quote: "{"
760
- },
761
- {
762
- pattern: /\[\$t\(/g,
763
- quote: "["
764
- }
765
- ];
766
- for (const { pattern, quote } of tPatterns) {
767
- let match = pattern.exec(templateContent);
768
- while (match !== null) {
769
- const templateStart = templateMatch.index + templateMatch[0].indexOf(templateContent);
770
- const start = templateStart + match.index;
771
- const end = start + 4;
772
- context.report({
773
- fix(fixer) {
774
- return fixer.replaceTextRange([start, end], `${quote}t(`);
775
- },
776
- loc: {
777
- end: sourceCode.getLocFromIndex(end),
778
- start: sourceCode.getLocFromIndex(start)
779
- },
780
- messageId: "useT"
781
- });
782
- match = pattern.exec(templateContent);
783
- }
784
- }
785
- }
786
- };
787
- }
788
- };
789
-
790
607
  function sortObjectKeys(object) {
791
608
  if (Array.isArray(object)) {
792
609
  return object.map((item) => sortObjectKeys(item));
@@ -794,214 +611,135 @@ function sortObjectKeys(object) {
794
611
  if (object && typeof object === "object") {
795
612
  const sortedKeys = Object.keys(object).toSorted((key1, key2) => key1.localeCompare(key2));
796
613
  const result = {};
797
- const obj = object;
614
+ const record = object;
798
615
  for (const key of sortedKeys) {
799
- result[key] = sortObjectKeys(obj[key]);
616
+ result[key] = sortObjectKeys(record[key]);
800
617
  }
801
618
  return result;
802
619
  }
803
620
  return object;
804
621
  }
805
- const rule$6 = {
806
- meta: {
807
- docs: {
808
- description: "Enforce consistent indentation and sorted keys in i18n blocks",
809
- recommended: true
810
- },
811
- fixable: "whitespace",
812
- messages: {
813
- indentError: "Invalid indentation for i18n content. Expected {{expected}} spaces.",
814
- invalidJson: "Invalid JSON in i18n block: {{error}}",
815
- sortError: "Keys in i18n block should be sorted alphabetically."
622
+ function checkSortAndIndent(context, parsed, rawContent, contentOffset) {
623
+ const sortedParsed = sortObjectKeys(parsed);
624
+ const formattedContent = `
625
+ ${JSON.stringify(sortedParsed, void 0, indentSpaces).replaceAll(/\n{2,}/g, "\n")}
626
+ `;
627
+ if (formattedContent.trim() === rawContent.trim()) {
628
+ return;
629
+ }
630
+ const currentKeys = JSON.stringify(parsed, void 0, indentSpaces).trim();
631
+ const sortedKeys = JSON.stringify(sortedParsed, void 0, indentSpaces).trim();
632
+ context.report({
633
+ data: { expected: String(indentSpaces) },
634
+ fix: (fixer) => fixer.replaceTextRange([contentOffset, contentOffset + rawContent.length], formattedContent),
635
+ loc: {
636
+ end: context.sourceCode.getLocFromIndex(contentOffset + rawContent.length),
637
+ start: context.sourceCode.getLocFromIndex(contentOffset)
816
638
  },
817
- schema: [],
818
- type: "problem"
819
- },
820
- create(context) {
821
- if (!context.filename.endsWith(".vue")) {
822
- return {};
639
+ messageId: currentKeys === sortedKeys ? "indentError" : "sortError"
640
+ });
641
+ }
642
+ function checkUnusedStrings(context, parsed, source, content, contentOffset) {
643
+ if (!parsed.en) {
644
+ return;
645
+ }
646
+ const templateContent = templateRegex$1.exec(source)?.[1] ?? "";
647
+ const scriptContent = scriptRegex.exec(source)?.[1] ?? "";
648
+ for (const key of getAllKeys(parsed.en)) {
649
+ if (templateContent.includes(key) || scriptContent.includes(key)) {
650
+ continue;
823
651
  }
824
- return {
825
- Program() {
826
- const source = context.sourceCode.getText();
827
- const i18nRegex = /(<i18n\s+lang=["']json["']>)([\s\S]*?)<\/i18n>/i;
828
- const i18nMatch = i18nRegex.exec(source);
829
- if (i18nMatch) {
830
- const startOffset = i18nMatch.index + i18nMatch[1].length;
831
- const i18nContent = i18nMatch[2];
832
- try {
833
- const parsed = JSON.parse(i18nContent.trim());
834
- const totalSpaces = 2;
835
- const sortedParsed = sortObjectKeys(parsed);
836
- const formattedContent = `
837
- ${JSON.stringify(sortedParsed, void 0, totalSpaces).replaceAll(/\n{2,}/g, "\n")}
838
- `;
839
- if (formattedContent.trim() !== i18nContent.trim()) {
840
- const currentKeys = JSON.stringify(parsed, void 0, totalSpaces).trim();
841
- const sortedKeys = JSON.stringify(sortedParsed, void 0, totalSpaces).trim();
842
- context.report({
843
- data: { expected: String(totalSpaces) },
844
- fix(fixer) {
845
- return fixer.replaceTextRange(
846
- [startOffset, startOffset + i18nContent.length],
847
- formattedContent
848
- );
849
- },
850
- loc: {
851
- end: context.sourceCode.getLocFromIndex(startOffset + i18nContent.length),
852
- start: context.sourceCode.getLocFromIndex(startOffset)
853
- },
854
- messageId: currentKeys === sortedKeys ? "indentError" : "sortError"
855
- });
856
- }
857
- } catch (error) {
858
- const errorMessage = error instanceof Error ? error.message : String(error);
859
- context.report({
860
- data: { error: errorMessage },
861
- loc: {
862
- end: context.sourceCode.getLocFromIndex(startOffset + i18nContent.length),
863
- start: context.sourceCode.getLocFromIndex(startOffset)
864
- },
865
- messageId: "invalidJson"
866
- });
867
- }
868
- }
869
- }
870
- };
871
- }
872
- };
873
-
874
- function getAllKeys(object, prefix = "") {
875
- let keys = [];
876
- for (const key in object) {
877
- const newPrefix = prefix ? `${prefix}.${key}` : key;
878
- if (typeof object[key] === "object" && object[key] !== null) {
879
- keys = [...keys, ...getAllKeys(object[key], newPrefix)];
880
- } else {
881
- keys.push(newPrefix);
652
+ const keyMatch = new RegExp(String.raw`"${escapeRegExp(key)}"\s*:`, "g").exec(content);
653
+ if (keyMatch) {
654
+ const keyOffset = contentOffset + keyMatch.index;
655
+ context.report({
656
+ data: { key },
657
+ loc: {
658
+ end: context.sourceCode.getLocFromIndex(keyOffset + key.length + 2),
659
+ start: context.sourceCode.getLocFromIndex(keyOffset)
660
+ },
661
+ messageId: "unusedString"
662
+ });
882
663
  }
883
664
  }
884
- return keys;
885
665
  }
886
- const rule$5 = {
666
+ const rule$2 = {
667
+ defaultOptions: [{ locales: defaultLocales }],
887
668
  meta: {
888
- docs: {
889
- description: "Detect unused strings in the English locale",
890
- recommended: true
891
- },
669
+ docs: { description: "Format Vue i18n blocks: enforce valid/consistent locales, sorted keys, and no unused strings" },
670
+ fixable: "code",
892
671
  messages: {
672
+ indentError: "Invalid indentation for i18n content. Expected {{expected}} spaces.",
893
673
  invalidJson: "Invalid JSON in i18n block: {{error}}",
674
+ invalidLocale: "Invalid locale: {{locale}}. Allowed locales are: {{allowed}}",
675
+ missingLocale: "Missing required locale: {{locale}}",
676
+ missingTranslations: 'Missing translations in "{{locale}}" locale: {{missing}}',
677
+ sortError: "Keys in i18n block should be sorted alphabetically.",
894
678
  unusedString: 'Unused string in English locale: "{{key}}"'
895
679
  },
896
- schema: [],
680
+ schema: [
681
+ {
682
+ additionalProperties: false,
683
+ properties: {
684
+ locales: {
685
+ items: { type: "string" },
686
+ type: "array"
687
+ }
688
+ },
689
+ type: "object"
690
+ }
691
+ ],
897
692
  type: "problem"
898
693
  },
899
694
  create(context) {
900
695
  if (!context.filename.endsWith(".vue")) {
901
696
  return {};
902
697
  }
698
+ const locales = context.options[0]?.locales ?? defaultLocales;
903
699
  return {
904
700
  Program() {
905
701
  const source = context.sourceCode.getText();
906
- const i18nRegex = /(<i18n\s+lang=["']json["']>)([\s\S]*?)<\/i18n>/i;
907
702
  const i18nMatch = i18nRegex.exec(source);
908
703
  if (!i18nMatch) {
909
704
  return;
910
705
  }
911
- const startOffset = i18nMatch.index + i18nMatch[1].length;
912
- const i18nContent = i18nMatch[2].trim();
706
+ const contentOffset = i18nMatch.index + i18nMatch[1].length;
707
+ const rawContent = i18nMatch[2];
708
+ const trimmedContent = rawContent.trim();
709
+ let parsed;
913
710
  try {
914
- const parsed = JSON.parse(i18nContent);
915
- if (!parsed.en) {
916
- return;
917
- }
918
- const templateRegex = /<template>([\s\S]*)<\/template>/i;
919
- const templateMatch = templateRegex.exec(source);
920
- const templateContent = templateMatch ? templateMatch[1] : "";
921
- const scriptRegex = /<script[^>]*>([\s\S]*?)<\/script>/i;
922
- const scriptMatch = scriptRegex.exec(source);
923
- const scriptContent = scriptMatch ? scriptMatch[1] : "";
924
- const enKeys = getAllKeys(parsed.en);
925
- for (const key of enKeys) {
926
- const isUsed = templateContent.includes(key) || scriptContent.includes(key);
927
- if (!isUsed) {
928
- const keyMatch = new RegExp(String.raw`"${key}"\s*:`, "g").exec(i18nContent);
929
- if (keyMatch) {
930
- const keyOffset = startOffset + keyMatch.index;
931
- context.report({
932
- data: { key },
933
- loc: {
934
- end: context.sourceCode.getLocFromIndex(keyOffset + key.length + 2),
935
- start: context.sourceCode.getLocFromIndex(keyOffset)
936
- },
937
- messageId: "unusedString"
938
- });
939
- }
940
- }
941
- }
711
+ parsed = JSON.parse(trimmedContent);
942
712
  } catch (error) {
943
- const errorMessage = error instanceof Error ? error.message : String(error);
944
713
  context.report({
945
- data: { error: errorMessage },
714
+ data: { error: error instanceof Error ? error.message : String(error) },
946
715
  loc: {
947
- end: context.sourceCode.getLocFromIndex(startOffset + i18nContent.length),
948
- start: context.sourceCode.getLocFromIndex(startOffset)
716
+ end: context.sourceCode.getLocFromIndex(contentOffset + rawContent.length),
717
+ start: context.sourceCode.getLocFromIndex(contentOffset)
949
718
  },
950
719
  messageId: "invalidJson"
951
720
  });
721
+ return;
952
722
  }
723
+ checkMissingLocales(context, parsed, locales, rawContent, contentOffset);
724
+ checkInvalidLocales(context, parsed, locales, rawContent, contentOffset);
725
+ checkMissingTranslations(context, parsed, locales, rawContent, contentOffset);
726
+ checkSortAndIndent(context, parsed, rawContent, contentOffset);
727
+ checkUnusedStrings(context, parsed, source, rawContent, contentOffset);
953
728
  }
954
729
  };
955
730
  }
956
731
  };
957
732
 
958
- const rule$4 = {
733
+ const untypedEmitsRegex = /const\s+emit\s*=\s*defineEmits\(\[([^\]]+)\]\)/g;
734
+ const rule$1 = {
959
735
  defaultOptions: [],
960
736
  meta: {
961
- docs: { description: "Enforce multiline style for Vue computed properties" },
737
+ docs: { description: "Format Vue component scripts: enforce multiline computed properties and typed emits" },
962
738
  fixable: "code",
963
- messages: { multilineComputed: "Computed properties should use explicit return with blocks" },
964
- schema: [],
965
- type: "layout"
966
- },
967
- create(context) {
968
- return {
969
- CallExpression(node) {
970
- if (node.callee.type === distExports.AST_NODE_TYPES.Identifier && node.callee.name === "computed") {
971
- const [argument] = node.arguments;
972
- if (argument?.type === distExports.AST_NODE_TYPES.ArrowFunctionExpression) {
973
- const sourceCode = context.sourceCode;
974
- const functionBody = sourceCode.getText(argument.body);
975
- if (!functionBody.startsWith("{")) {
976
- const bodyText = functionBody.startsWith("(") && functionBody.endsWith(")") ? functionBody.slice(1, -1) : functionBody;
977
- context.report({
978
- fix(fixer) {
979
- return fixer.replaceText(
980
- argument.body,
981
- `{
982
- return ${bodyText}
983
- }`
984
- );
985
- },
986
- messageId: "multilineComputed",
987
- node
988
- });
989
- }
990
- }
991
- }
992
- }
993
- };
994
- }
995
- };
996
-
997
- const rule$3 = {
998
- meta: {
999
- docs: {
1000
- description: "Enforce typed emits in Vue components",
1001
- recommended: true
739
+ messages: {
740
+ multilineComputed: "Computed properties should use explicit return with blocks",
741
+ untypedEmits: "Use typed emits instead of untyped emits"
1002
742
  },
1003
- fixable: "code",
1004
- messages: { untypedEmits: "Use typed emits instead of untyped emits" },
1005
743
  schema: [],
1006
744
  type: "problem"
1007
745
  },
@@ -1010,9 +748,32 @@ const rule$3 = {
1010
748
  return {};
1011
749
  }
1012
750
  return {
751
+ // Rewrite computed(() => expr) into an explicit return block.
752
+ CallExpression(node) {
753
+ if (node.callee.type !== distExports.AST_NODE_TYPES.Identifier || node.callee.name !== "computed") {
754
+ return;
755
+ }
756
+ const [argument] = node.arguments;
757
+ if (argument?.type !== distExports.AST_NODE_TYPES.ArrowFunctionExpression) {
758
+ return;
759
+ }
760
+ const functionBody = context.sourceCode.getText(argument.body);
761
+ if (functionBody.startsWith("{")) {
762
+ return;
763
+ }
764
+ const bodyText = functionBody.startsWith("(") && functionBody.endsWith(")") ? functionBody.slice(1, -1) : functionBody;
765
+ context.report({
766
+ fix: (fixer) => fixer.replaceText(argument.body, `{
767
+ return ${bodyText}
768
+ }`),
769
+ messageId: "multilineComputed",
770
+ node
771
+ });
772
+ },
773
+ // Rewrite array-syntax defineEmits([...]) into the typed generic form.
1013
774
  Program(node) {
1014
775
  const source = context.sourceCode.getText();
1015
- const untypedEmitsRegex = /const\s+emit\s*=\s*defineEmits\(\[([^\]]+)\]\)/g;
776
+ untypedEmitsRegex.lastIndex = 0;
1016
777
  let match = untypedEmitsRegex.exec(source);
1017
778
  while (match !== null) {
1018
779
  const [fullMatch, eventsString] = match;
@@ -1023,12 +784,7 @@ const rule$3 = {
1023
784
  ${typedEvents}
1024
785
  }>()`;
1025
786
  context.report({
1026
- fix: (fixer) => {
1027
- return fixer.replaceTextRange(
1028
- [matchIndex, matchIndex + fullMatch.length],
1029
- typedEmits
1030
- );
1031
- },
787
+ fix: (fixer) => fixer.replaceTextRange([matchIndex, matchIndex + fullMatch.length], typedEmits),
1032
788
  loc: {
1033
789
  end: context.sourceCode.getLocFromIndex(matchIndex + fullMatch.length),
1034
790
  start: context.sourceCode.getLocFromIndex(matchIndex)
@@ -1043,38 +799,42 @@ ${typedEvents}
1043
799
  }
1044
800
  };
1045
801
 
1046
- const rule$2 = {
1047
- meta: {
1048
- docs: {
1049
- description: "enforce order of compiler macros (`defineProps`, `defineEmits`, etc.)",
1050
- recommended: true
1051
- },
1052
- fixable: "code",
1053
- hasSuggestions: true,
1054
- messages: {
1055
- defineExposeNotTheLast: "`defineExpose` should be the last statement in `<script setup>`.",
1056
- macrosNotOnTop: "{{macro}} should be the first statement in `<script setup>` (after any potential import statements or type definitions).",
1057
- putExposeAtTheLast: "Put `defineExpose` as the last statement in `<script setup>`."
1058
- },
1059
- schema: [],
1060
- type: "problem"
802
+ const templateRegex = /<template>([\s\S]*)<\/template>/i;
803
+ const templateTagLength = "<template>".length;
804
+ const propsPrefixRegex = /\$?props\.(\w+)/g;
805
+ const trueAttributeRegex = /(?<![\w-]):?(?!aria-)([a-z0-9-]+)="true"/gi;
806
+ const dollarTPatterns = [
807
+ {
808
+ pattern: / \$t\(/g,
809
+ prefix: " "
1061
810
  },
1062
- create(context) {
1063
- if (!context.filename.endsWith(".vue")) {
1064
- return {};
1065
- }
1066
- return {};
811
+ {
812
+ pattern: /"\$t\(/g,
813
+ prefix: '"'
814
+ },
815
+ {
816
+ pattern: /`\$t\(/g,
817
+ prefix: "`"
818
+ },
819
+ {
820
+ pattern: /\{\$t\(/g,
821
+ prefix: "{"
822
+ },
823
+ {
824
+ pattern: /\[\$t\(/g,
825
+ prefix: "["
1067
826
  }
1068
- };
1069
-
1070
- const rule$1 = {
827
+ ];
828
+ const rule = {
829
+ defaultOptions: [],
1071
830
  meta: {
1072
- docs: {
1073
- description: "Prevent unnecessary props. and $props. prefixes in Vue templates",
1074
- recommended: true
1075
- },
831
+ docs: { description: 'Format Vue templates: strip unnecessary props/$props prefixes, redundant ="true" attributes (except aria- attributes), and enforce t() over $t()' },
1076
832
  fixable: "code",
1077
- messages: { noPropsPrefix: "Unnecessary props/$props prefix in template. Props are automatically available in template scope." },
833
+ messages: {
834
+ noPropsPrefix: "Unnecessary props/$props prefix in template. Props are automatically available in template scope.",
835
+ removeTrueAttribute: 'Unnecessary ="true" attribute. Use the attribute name directly instead.',
836
+ useT: "Use t() instead of $t() in Vue templates as it does not work with <i18n> tags."
837
+ },
1078
838
  schema: [],
1079
839
  type: "problem"
1080
840
  },
@@ -1083,31 +843,23 @@ const rule$1 = {
1083
843
  return {};
1084
844
  }
1085
845
  return {
1086
- CallExpression(node) {
1087
- if (node.callee.type !== "Identifier" || node.callee.name !== "defineProps") {
1088
- return;
1089
- }
846
+ Program() {
1090
847
  const sourceCode = context.sourceCode;
1091
848
  const source = sourceCode.getText();
1092
- const templateRegex = /<template>([\s\S]*)<\/template>/i;
1093
849
  const templateMatch = templateRegex.exec(source);
1094
850
  if (!templateMatch) {
1095
851
  return;
1096
852
  }
1097
853
  const templateContent = templateMatch[1];
1098
- const propsPrefixRegex = /\$?props\.(\w+)/g;
854
+ const templateStartIndex = templateMatch.index + templateTagLength;
855
+ propsPrefixRegex.lastIndex = 0;
1099
856
  let prefixMatch = propsPrefixRegex.exec(templateContent);
1100
857
  while (prefixMatch !== null) {
1101
858
  const propName = prefixMatch[1];
1102
859
  const fullMatch = prefixMatch[0];
1103
- const matchIndex = templateMatch.index + 10 + (prefixMatch.index ?? 0);
860
+ const matchIndex = templateStartIndex + prefixMatch.index;
1104
861
  context.report({
1105
- fix: (fixer) => {
1106
- return fixer.replaceTextRange(
1107
- [matchIndex, matchIndex + fullMatch.length],
1108
- propName
1109
- );
1110
- },
862
+ fix: (fixer) => fixer.replaceTextRange([matchIndex, matchIndex + fullMatch.length], propName),
1111
863
  loc: {
1112
864
  end: sourceCode.getLocFromIndex(matchIndex + fullMatch.length),
1113
865
  start: sourceCode.getLocFromIndex(matchIndex)
@@ -1116,38 +868,7 @@ const rule$1 = {
1116
868
  });
1117
869
  prefixMatch = propsPrefixRegex.exec(templateContent);
1118
870
  }
1119
- }
1120
- };
1121
- }
1122
- };
1123
-
1124
- const rule = {
1125
- meta: {
1126
- docs: {
1127
- description: 'Remove unnecessary ="true" attributes in Vue templates, except for aria- attributes',
1128
- recommended: true
1129
- },
1130
- fixable: "code",
1131
- messages: { removeTrueAttribute: 'Unnecessary ="true" attribute. Use the attribute name directly instead.' },
1132
- schema: [],
1133
- type: "problem"
1134
- },
1135
- create(context) {
1136
- if (!context.filename.endsWith(".vue")) {
1137
- return {};
1138
- }
1139
- return {
1140
- Program() {
1141
- const sourceCode = context.sourceCode;
1142
- const source = sourceCode.getText();
1143
- const templateRegex = /<template>([\s\S]*)<\/template>/i;
1144
- const templateMatch = templateRegex.exec(source);
1145
- if (!templateMatch) {
1146
- return;
1147
- }
1148
- const templateContent = templateMatch[1];
1149
- const templateStartIndex = templateMatch.index + 10;
1150
- const trueAttributeRegex = /(?:^|\s):?(?!aria-)([a-zA-Z0-9-]+)="true"/g;
871
+ trueAttributeRegex.lastIndex = 0;
1151
872
  let trueAttributeMatch = trueAttributeRegex.exec(templateContent);
1152
873
  while (trueAttributeMatch !== null) {
1153
874
  const fullMatch = trueAttributeMatch[0];
@@ -1159,12 +880,7 @@ const rule = {
1159
880
  continue;
1160
881
  }
1161
882
  context.report({
1162
- fix: (fixer) => {
1163
- return fixer.replaceTextRange(
1164
- [matchIndex, matchIndex + fullMatch.length],
1165
- ` ${attributeName}`
1166
- );
1167
- },
883
+ fix: (fixer) => fixer.replaceTextRange([matchIndex, matchIndex + fullMatch.length], attributeName),
1168
884
  loc: {
1169
885
  end: sourceCode.getLocFromIndex(matchIndex + fullMatch.length),
1170
886
  start: sourceCode.getLocFromIndex(matchIndex)
@@ -1173,6 +889,23 @@ const rule = {
1173
889
  });
1174
890
  trueAttributeMatch = trueAttributeRegex.exec(templateContent);
1175
891
  }
892
+ for (const { pattern, prefix } of dollarTPatterns) {
893
+ pattern.lastIndex = 0;
894
+ let dollarTMatch = pattern.exec(templateContent);
895
+ while (dollarTMatch !== null) {
896
+ const start = templateStartIndex + dollarTMatch.index;
897
+ const end = start + 4;
898
+ context.report({
899
+ fix: (fixer) => fixer.replaceTextRange([start, end], `${prefix}t(`),
900
+ loc: {
901
+ end: sourceCode.getLocFromIndex(end),
902
+ start: sourceCode.getLocFromIndex(start)
903
+ },
904
+ messageId: "useT"
905
+ });
906
+ dollarTMatch = pattern.exec(templateContent);
907
+ }
908
+ }
1176
909
  }
1177
910
  };
1178
911
  }
@@ -1180,19 +913,11 @@ const rule = {
1180
913
 
1181
914
  const index = {
1182
915
  rules: {
1183
- "ts-multiline-ternary": rule$c,
1184
- "ts-multiline-union": rule$b,
1185
- "ts-padding-statements": rule$a,
1186
- "ts-sort-tests": rule$9,
1187
- "vue-i18n-consistent-locales": rule$8,
1188
- "vue-i18n-consistent-t": rule$7,
1189
- "vue-i18n-sort-keys": rule$6,
1190
- "vue-i18n-unused-strings": rule$5,
1191
- "vue-script-format-computed": rule$4,
1192
- "vue-script-format-emits": rule$3,
1193
- "vue-script-order": rule$2,
1194
- "vue-template-format-props": rule$1,
1195
- "vue-template-remove-true-attributes": rule
916
+ "ts-format-layout": rule$4,
917
+ "ts-format-tests": rule$3,
918
+ "vue-format-i18n": rule$2,
919
+ "vue-format-script": rule$1,
920
+ "vue-format-template": rule
1196
921
  }
1197
922
  };
1198
923