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