@saasmakers/eslint 1.0.18 → 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
@@ -1,141 +1,65 @@
1
1
  'use strict';
2
2
 
3
- const index$1 = require('./shared/eslint.DhFjwkxh.cjs');
3
+ 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
  }
@@ -193,7 +117,7 @@ function getBlankLineRequirement(prevNode, nextNode, sourceCode) {
193
117
  return null;
194
118
  }
195
119
  if (hasLineCommentBetween(prevNode, nextNode, sourceCode)) {
196
- return "always";
120
+ return null;
197
121
  }
198
122
  const nextKind = getStatementKind(nextNode);
199
123
  if (!nextKind) {
@@ -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 = {
@@ -395,7 +336,7 @@ const rule$a = {
395
336
  return;
396
337
  }
397
338
  const previousLine = sourceCode.lines[commentLine - 2] ?? "";
398
- if (previousLine.trim() === "" || previousLine.trimEnd().endsWith("{")) {
339
+ if (previousLine.trim() === "" || previousLine.trimEnd().endsWith("{") || previousLine.trimStart().startsWith("//")) {
399
340
  return;
400
341
  }
401
342
  context.report({
@@ -442,13 +383,19 @@ const rule$a = {
442
383
  ":statement": verify,
443
384
  "BlockStatement": enterScope,
444
385
  "BlockStatement:exit": exitScope,
445
- "LineComment": verifyComment,
386
+ "ConditionalExpression": (node) => checkTernary(context, node, maxCharacters),
446
387
  "Program": enterScope,
447
- "Program:exit": exitScope,
388
+ "Program:exit": function() {
389
+ for (const comment of sourceCode.getAllComments()) {
390
+ verifyComment(comment);
391
+ }
392
+ exitScope();
393
+ },
448
394
  "StaticBlock": enterScope,
449
395
  "StaticBlock:exit": exitScope,
450
396
  "SwitchCase": verifyThenEnterScope,
451
- "SwitchCase:exit": exitScope
397
+ "SwitchCase:exit": exitScope,
398
+ "TSUnionType": (node) => checkUnion(context, node, maxLineLength, minItems)
452
399
  };
453
400
  }
454
401
  };
@@ -482,8 +429,9 @@ function getSpecificName(testName) {
482
429
  return parts.slice(specialIndex + 1).join(".");
483
430
  }
484
431
  function getTestName(node) {
485
- if (node.arguments && node.arguments[0] && node.arguments[0].type === "Literal") {
486
- return node.arguments[0].value;
432
+ const firstArgument = node.arguments[0];
433
+ if (firstArgument?.type === index$1.distExports.AST_NODE_TYPES.Literal && typeof firstArgument.value === "string") {
434
+ return firstArgument.value;
487
435
  }
488
436
  return "";
489
437
  }
@@ -500,12 +448,10 @@ function getTestPriority(testName) {
500
448
  return 1;
501
449
  }
502
450
  }
503
- const rule$9 = {
451
+ const rule$3 = {
452
+ defaultOptions: [],
504
453
  meta: {
505
- docs: {
506
- description: "Enforce sorted test functions grouped by method with sorted metrics, errors, exceptions and middlewares",
507
- recommended: true
508
- },
454
+ docs: { description: "Enforce sorted test functions grouped by method with sorted metrics, errors, exceptions and middlewares" },
509
455
  fixable: "code",
510
456
  messages: { sortError: "Test functions should be grouped by method with sorted metrics, errors, exceptions and middlewares." },
511
457
  schema: [],
@@ -521,15 +467,15 @@ const rule$9 = {
521
467
  return;
522
468
  }
523
469
  const groupCallback = node.arguments[1];
524
- if (groupCallback.type !== "FunctionExpression" && groupCallback.type !== "ArrowFunctionExpression") {
470
+ if (groupCallback.type !== index$1.distExports.AST_NODE_TYPES.FunctionExpression && groupCallback.type !== index$1.distExports.AST_NODE_TYPES.ArrowFunctionExpression) {
525
471
  return;
526
472
  }
527
473
  const callbackBody = groupCallback.body;
528
- if (callbackBody.type !== "BlockStatement" || !callbackBody.body) {
474
+ if (callbackBody.type !== index$1.distExports.AST_NODE_TYPES.BlockStatement || !callbackBody.body) {
529
475
  return;
530
476
  }
531
477
  const tests = callbackBody.body.filter((statement) => {
532
- return statement.type === "ExpressionStatement" && statement.expression.type === "CallExpression" && statement.expression.callee.type === "Identifier" && (statement.expression.callee.name === "test" || statement.expression.callee.name === "it");
478
+ return statement.type === index$1.distExports.AST_NODE_TYPES.ExpressionStatement && statement.expression.type === index$1.distExports.AST_NODE_TYPES.CallExpression && statement.expression.callee.type === index$1.distExports.AST_NODE_TYPES.Identifier && (statement.expression.callee.name === "test" || statement.expression.callee.name === "it");
533
479
  });
534
480
  const testNames = tests.map((test) => getTestName(test.expression));
535
481
  const sortedTestNames = [...testNames].toSorted(compareTests);
@@ -569,63 +515,68 @@ const rule$9 = {
569
515
  }
570
516
  };
571
517
 
572
- 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) {
573
524
  for (const locale of Object.keys(parsed)) {
574
- if (!locales.includes(locale)) {
575
- const escapedLocale = locale.replaceAll(/[.*+?^${}()|[\]\\]/g, String.raw`\$&`);
576
- const localeMatch = new RegExp(String.raw`"${escapedLocale}"\s*:`, "g").exec(i18nContent);
577
- if (localeMatch) {
578
- const localeOffset = startOffset + localeMatch.index;
579
- context.report({
580
- data: {
581
- allowed: locales.join(", "),
582
- locale
583
- },
584
- loc: {
585
- end: context.sourceCode.getLocFromIndex(localeOffset + locale.length + 2),
586
- start: context.sourceCode.getLocFromIndex(localeOffset)
587
- },
588
- messageId: "invalidLocale"
589
- });
590
- }
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
+ });
591
542
  }
592
543
  }
593
544
  }
594
- function checkMissingLocales(context, parsed, locales, startOffset, i18nContent) {
545
+ function checkMissingLocales(context, parsed, locales, content, contentOffset) {
595
546
  for (const locale of locales) {
596
547
  if (!parsed[locale]) {
597
548
  context.report({
598
549
  data: { locale },
599
550
  loc: {
600
- end: context.sourceCode.getLocFromIndex(startOffset + i18nContent.length),
601
- start: context.sourceCode.getLocFromIndex(startOffset)
551
+ end: context.sourceCode.getLocFromIndex(contentOffset + content.length),
552
+ start: context.sourceCode.getLocFromIndex(contentOffset)
602
553
  },
603
554
  messageId: "missingLocale"
604
555
  });
605
556
  }
606
557
  }
607
558
  }
608
- function checkMissingTranslations(context, parsed, locales, startOffset, i18nContent) {
559
+ function checkMissingTranslations(context, parsed, locales, content, contentOffset) {
609
560
  const allKeys = /* @__PURE__ */ new Set();
610
561
  for (const locale of locales) {
611
562
  if (parsed[locale]) {
612
- const keys = getAllKeys$1(parsed[locale]);
613
- for (const key of keys) allKeys.add(key);
563
+ for (const key of getAllKeys(parsed[locale])) {
564
+ allKeys.add(key);
565
+ }
614
566
  }
615
567
  }
616
568
  for (const locale of locales) {
617
569
  if (!parsed[locale]) {
618
570
  continue;
619
571
  }
620
- const localeKeys = getAllKeys$1(parsed[locale]);
572
+ const localeKeys = getAllKeys(parsed[locale]);
621
573
  const missingKeys = [...allKeys].filter((key) => !localeKeys.includes(key));
622
574
  if (missingKeys.length === 0) {
623
575
  continue;
624
576
  }
625
- const escapedLocale = locale.replaceAll(/[.*+?^${}()|[\]\\]/g, String.raw`\$&`);
626
- 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);
627
578
  if (localeMatch) {
628
- const localeOffset = startOffset + localeMatch.index;
579
+ const localeOffset = contentOffset + localeMatch.index;
629
580
  context.report({
630
581
  data: {
631
582
  locale,
@@ -640,152 +591,21 @@ function checkMissingTranslations(context, parsed, locales, startOffset, i18nCon
640
591
  }
641
592
  }
642
593
  }
643
- function getAllKeys$1(object, prefix = "") {
594
+ function escapeRegExp(value) {
595
+ return value.replaceAll(/[.*+?^${}()|[\]\\]/g, String.raw`\$&`);
596
+ }
597
+ function getAllKeys(object, prefix = "") {
644
598
  let keys = [];
645
599
  for (const key in object) {
646
600
  const newPrefix = prefix ? `${prefix}.${key}` : key;
647
601
  if (typeof object[key] === "object" && object[key] !== null) {
648
- keys = [...keys, ...getAllKeys$1(object[key], newPrefix)];
602
+ keys = [...keys, ...getAllKeys(object[key], newPrefix)];
649
603
  } else {
650
604
  keys.push(newPrefix);
651
605
  }
652
606
  }
653
607
  return keys;
654
608
  }
655
- const rule$8 = {
656
- meta: {
657
- docs: {
658
- description: "Enforce consistent i18n locale keys across translations",
659
- recommended: true
660
- },
661
- messages: {
662
- invalidJson: "Invalid JSON in i18n block: {{error}}",
663
- invalidLocale: "Invalid locale: {{locale}}. Allowed locales are: {{allowed}}",
664
- missingLocale: "Missing required locale: {{locale}}",
665
- missingTranslations: 'Missing translations in "{{locale}}" locale: {{missing}}'
666
- },
667
- schema: [
668
- {
669
- additionalProperties: false,
670
- properties: {
671
- locales: {
672
- items: { type: "string" },
673
- type: "array"
674
- }
675
- },
676
- type: "object"
677
- }
678
- ],
679
- type: "problem"
680
- },
681
- create(context) {
682
- if (!context.filename.endsWith(".vue")) {
683
- return {};
684
- }
685
- const locales = context.options[0]?.locales || ["en", "fr"];
686
- return {
687
- Program() {
688
- const source = context.sourceCode.getText();
689
- const i18nRegex = /<i18n\s+lang=["']json["']>([\s\S]*?)<\/i18n>/i;
690
- const i18nMatch = i18nRegex.exec(source);
691
- if (!i18nMatch) {
692
- return;
693
- }
694
- const tagLength = source.indexOf(">", i18nMatch.index) + 1 - i18nMatch.index;
695
- const startOffset = i18nMatch.index + tagLength;
696
- const i18nContent = i18nMatch[1].trim();
697
- try {
698
- const parsed = JSON.parse(i18nContent);
699
- checkMissingLocales(context, parsed, locales, startOffset, i18nContent);
700
- checkInvalidLocales(context, parsed, locales, startOffset, i18nContent);
701
- checkMissingTranslations(context, parsed, locales, startOffset, i18nContent);
702
- } catch (error) {
703
- const errorMessage = error instanceof Error ? error.message : String(error);
704
- context.report({
705
- data: { error: errorMessage },
706
- loc: {
707
- end: context.sourceCode.getLocFromIndex(startOffset + i18nContent.length),
708
- start: context.sourceCode.getLocFromIndex(startOffset)
709
- },
710
- messageId: "invalidJson"
711
- });
712
- }
713
- }
714
- };
715
- }
716
- };
717
-
718
- const rule$7 = {
719
- meta: {
720
- docs: {
721
- description: "Enforce using t() instead of $t() in Vue templates",
722
- recommended: true
723
- },
724
- fixable: "code",
725
- messages: { useT: "Use t() instead of $t() in Vue templates as it does not work with <i18n> tags." },
726
- schema: [],
727
- type: "problem"
728
- },
729
- create(context) {
730
- if (!context.filename.endsWith(".vue")) {
731
- return {};
732
- }
733
- return {
734
- Program() {
735
- const sourceCode = context.sourceCode;
736
- const source = sourceCode.getText();
737
- const templateRegex = /<template>([\s\S]*)<\/template>/i;
738
- const templateMatch = templateRegex.exec(source);
739
- if (!templateMatch) {
740
- return;
741
- }
742
- const templateContent = templateMatch[1];
743
- const tPatterns = [
744
- {
745
- pattern: / \$t\(/g,
746
- quote: " "
747
- },
748
- {
749
- pattern: /"\$t\(/g,
750
- quote: '"'
751
- },
752
- {
753
- pattern: /`\$t\(/g,
754
- quote: "`"
755
- },
756
- {
757
- pattern: /\{\$t\(/g,
758
- quote: "{"
759
- },
760
- {
761
- pattern: /\[\$t\(/g,
762
- quote: "["
763
- }
764
- ];
765
- for (const { pattern, quote } of tPatterns) {
766
- let match = pattern.exec(templateContent);
767
- while (match !== null) {
768
- const templateStart = templateMatch.index + templateMatch[0].indexOf(templateContent);
769
- const start = templateStart + match.index;
770
- const end = start + 4;
771
- context.report({
772
- fix(fixer) {
773
- return fixer.replaceTextRange([start, end], `${quote}t(`);
774
- },
775
- loc: {
776
- end: sourceCode.getLocFromIndex(end),
777
- start: sourceCode.getLocFromIndex(start)
778
- },
779
- messageId: "useT"
780
- });
781
- match = pattern.exec(templateContent);
782
- }
783
- }
784
- }
785
- };
786
- }
787
- };
788
-
789
609
  function sortObjectKeys(object) {
790
610
  if (Array.isArray(object)) {
791
611
  return object.map((item) => sortObjectKeys(item));
@@ -793,214 +613,135 @@ function sortObjectKeys(object) {
793
613
  if (object && typeof object === "object") {
794
614
  const sortedKeys = Object.keys(object).toSorted((key1, key2) => key1.localeCompare(key2));
795
615
  const result = {};
796
- const obj = object;
616
+ const record = object;
797
617
  for (const key of sortedKeys) {
798
- result[key] = sortObjectKeys(obj[key]);
618
+ result[key] = sortObjectKeys(record[key]);
799
619
  }
800
620
  return result;
801
621
  }
802
622
  return object;
803
623
  }
804
- const rule$6 = {
805
- meta: {
806
- docs: {
807
- description: "Enforce consistent indentation and sorted keys in i18n blocks",
808
- recommended: true
809
- },
810
- fixable: "whitespace",
811
- messages: {
812
- indentError: "Invalid indentation for i18n content. Expected {{expected}} spaces.",
813
- invalidJson: "Invalid JSON in i18n block: {{error}}",
814
- 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)
815
640
  },
816
- schema: [],
817
- type: "problem"
818
- },
819
- create(context) {
820
- if (!context.filename.endsWith(".vue")) {
821
- 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;
822
653
  }
823
- return {
824
- Program() {
825
- const source = context.sourceCode.getText();
826
- const i18nRegex = /(<i18n\s+lang=["']json["']>)([\s\S]*?)<\/i18n>/i;
827
- const i18nMatch = i18nRegex.exec(source);
828
- if (i18nMatch) {
829
- const startOffset = i18nMatch.index + i18nMatch[1].length;
830
- const i18nContent = i18nMatch[2];
831
- try {
832
- const parsed = JSON.parse(i18nContent.trim());
833
- const totalSpaces = 2;
834
- const sortedParsed = sortObjectKeys(parsed);
835
- const formattedContent = `
836
- ${JSON.stringify(sortedParsed, void 0, totalSpaces).replaceAll(/\n{2,}/g, "\n")}
837
- `;
838
- if (formattedContent.trim() !== i18nContent.trim()) {
839
- const currentKeys = JSON.stringify(parsed, void 0, totalSpaces).trim();
840
- const sortedKeys = JSON.stringify(sortedParsed, void 0, totalSpaces).trim();
841
- context.report({
842
- data: { expected: String(totalSpaces) },
843
- fix(fixer) {
844
- return fixer.replaceTextRange(
845
- [startOffset, startOffset + i18nContent.length],
846
- formattedContent
847
- );
848
- },
849
- loc: {
850
- end: context.sourceCode.getLocFromIndex(startOffset + i18nContent.length),
851
- start: context.sourceCode.getLocFromIndex(startOffset)
852
- },
853
- messageId: currentKeys === sortedKeys ? "indentError" : "sortError"
854
- });
855
- }
856
- } catch (error) {
857
- const errorMessage = error instanceof Error ? error.message : String(error);
858
- context.report({
859
- data: { error: errorMessage },
860
- loc: {
861
- end: context.sourceCode.getLocFromIndex(startOffset + i18nContent.length),
862
- start: context.sourceCode.getLocFromIndex(startOffset)
863
- },
864
- messageId: "invalidJson"
865
- });
866
- }
867
- }
868
- }
869
- };
870
- }
871
- };
872
-
873
- function getAllKeys(object, prefix = "") {
874
- let keys = [];
875
- for (const key in object) {
876
- const newPrefix = prefix ? `${prefix}.${key}` : key;
877
- if (typeof object[key] === "object" && object[key] !== null) {
878
- keys = [...keys, ...getAllKeys(object[key], newPrefix)];
879
- } else {
880
- 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
+ });
881
665
  }
882
666
  }
883
- return keys;
884
667
  }
885
- const rule$5 = {
668
+ const rule$2 = {
669
+ defaultOptions: [{ locales: defaultLocales }],
886
670
  meta: {
887
- docs: {
888
- description: "Detect unused strings in the English locale",
889
- recommended: true
890
- },
671
+ docs: { description: "Format Vue i18n blocks: enforce valid/consistent locales, sorted keys, and no unused strings" },
672
+ fixable: "code",
891
673
  messages: {
674
+ indentError: "Invalid indentation for i18n content. Expected {{expected}} spaces.",
892
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.",
893
680
  unusedString: 'Unused string in English locale: "{{key}}"'
894
681
  },
895
- 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
+ ],
896
694
  type: "problem"
897
695
  },
898
696
  create(context) {
899
697
  if (!context.filename.endsWith(".vue")) {
900
698
  return {};
901
699
  }
700
+ const locales = context.options[0]?.locales ?? defaultLocales;
902
701
  return {
903
702
  Program() {
904
703
  const source = context.sourceCode.getText();
905
- const i18nRegex = /(<i18n\s+lang=["']json["']>)([\s\S]*?)<\/i18n>/i;
906
704
  const i18nMatch = i18nRegex.exec(source);
907
705
  if (!i18nMatch) {
908
706
  return;
909
707
  }
910
- const startOffset = i18nMatch.index + i18nMatch[1].length;
911
- 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;
912
712
  try {
913
- const parsed = JSON.parse(i18nContent);
914
- if (!parsed.en) {
915
- return;
916
- }
917
- const templateRegex = /<template>([\s\S]*)<\/template>/i;
918
- const templateMatch = templateRegex.exec(source);
919
- const templateContent = templateMatch ? templateMatch[1] : "";
920
- const scriptRegex = /<script[^>]*>([\s\S]*?)<\/script>/i;
921
- const scriptMatch = scriptRegex.exec(source);
922
- const scriptContent = scriptMatch ? scriptMatch[1] : "";
923
- const enKeys = getAllKeys(parsed.en);
924
- for (const key of enKeys) {
925
- const isUsed = templateContent.includes(key) || scriptContent.includes(key);
926
- if (!isUsed) {
927
- const keyMatch = new RegExp(String.raw`"${key}"\s*:`, "g").exec(i18nContent);
928
- if (keyMatch) {
929
- const keyOffset = startOffset + keyMatch.index;
930
- context.report({
931
- data: { key },
932
- loc: {
933
- end: context.sourceCode.getLocFromIndex(keyOffset + key.length + 2),
934
- start: context.sourceCode.getLocFromIndex(keyOffset)
935
- },
936
- messageId: "unusedString"
937
- });
938
- }
939
- }
940
- }
713
+ parsed = JSON.parse(trimmedContent);
941
714
  } catch (error) {
942
- const errorMessage = error instanceof Error ? error.message : String(error);
943
715
  context.report({
944
- data: { error: errorMessage },
716
+ data: { error: error instanceof Error ? error.message : String(error) },
945
717
  loc: {
946
- end: context.sourceCode.getLocFromIndex(startOffset + i18nContent.length),
947
- start: context.sourceCode.getLocFromIndex(startOffset)
718
+ end: context.sourceCode.getLocFromIndex(contentOffset + rawContent.length),
719
+ start: context.sourceCode.getLocFromIndex(contentOffset)
948
720
  },
949
721
  messageId: "invalidJson"
950
722
  });
723
+ return;
951
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);
952
730
  }
953
731
  };
954
732
  }
955
733
  };
956
734
 
957
- const rule$4 = {
735
+ const untypedEmitsRegex = /const\s+emit\s*=\s*defineEmits\(\[([^\]]+)\]\)/g;
736
+ const rule$1 = {
958
737
  defaultOptions: [],
959
738
  meta: {
960
- docs: { description: "Enforce multiline style for Vue computed properties" },
739
+ docs: { description: "Format Vue component scripts: enforce multiline computed properties and typed emits" },
961
740
  fixable: "code",
962
- messages: { multilineComputed: "Computed properties should use explicit return with blocks" },
963
- schema: [],
964
- type: "layout"
965
- },
966
- create(context) {
967
- return {
968
- CallExpression(node) {
969
- if (node.callee.type === index$1.distExports.AST_NODE_TYPES.Identifier && node.callee.name === "computed") {
970
- const [argument] = node.arguments;
971
- if (argument?.type === index$1.distExports.AST_NODE_TYPES.ArrowFunctionExpression) {
972
- const sourceCode = context.sourceCode;
973
- const functionBody = sourceCode.getText(argument.body);
974
- if (!functionBody.startsWith("{")) {
975
- const bodyText = functionBody.startsWith("(") && functionBody.endsWith(")") ? functionBody.slice(1, -1) : functionBody;
976
- context.report({
977
- fix(fixer) {
978
- return fixer.replaceText(
979
- argument.body,
980
- `{
981
- return ${bodyText}
982
- }`
983
- );
984
- },
985
- messageId: "multilineComputed",
986
- node
987
- });
988
- }
989
- }
990
- }
991
- }
992
- };
993
- }
994
- };
995
-
996
- const rule$3 = {
997
- meta: {
998
- docs: {
999
- description: "Enforce typed emits in Vue components",
1000
- recommended: true
741
+ messages: {
742
+ multilineComputed: "Computed properties should use explicit return with blocks",
743
+ untypedEmits: "Use typed emits instead of untyped emits"
1001
744
  },
1002
- fixable: "code",
1003
- messages: { untypedEmits: "Use typed emits instead of untyped emits" },
1004
745
  schema: [],
1005
746
  type: "problem"
1006
747
  },
@@ -1009,9 +750,32 @@ const rule$3 = {
1009
750
  return {};
1010
751
  }
1011
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.
1012
776
  Program(node) {
1013
777
  const source = context.sourceCode.getText();
1014
- const untypedEmitsRegex = /const\s+emit\s*=\s*defineEmits\(\[([^\]]+)\]\)/g;
778
+ untypedEmitsRegex.lastIndex = 0;
1015
779
  let match = untypedEmitsRegex.exec(source);
1016
780
  while (match !== null) {
1017
781
  const [fullMatch, eventsString] = match;
@@ -1022,12 +786,7 @@ const rule$3 = {
1022
786
  ${typedEvents}
1023
787
  }>()`;
1024
788
  context.report({
1025
- fix: (fixer) => {
1026
- return fixer.replaceTextRange(
1027
- [matchIndex, matchIndex + fullMatch.length],
1028
- typedEmits
1029
- );
1030
- },
789
+ fix: (fixer) => fixer.replaceTextRange([matchIndex, matchIndex + fullMatch.length], typedEmits),
1031
790
  loc: {
1032
791
  end: context.sourceCode.getLocFromIndex(matchIndex + fullMatch.length),
1033
792
  start: context.sourceCode.getLocFromIndex(matchIndex)
@@ -1042,38 +801,42 @@ ${typedEvents}
1042
801
  }
1043
802
  };
1044
803
 
1045
- const rule$2 = {
1046
- meta: {
1047
- docs: {
1048
- description: "enforce order of compiler macros (`defineProps`, `defineEmits`, etc.)",
1049
- recommended: true
1050
- },
1051
- fixable: "code",
1052
- hasSuggestions: true,
1053
- messages: {
1054
- defineExposeNotTheLast: "`defineExpose` should be the last statement in `<script setup>`.",
1055
- macrosNotOnTop: "{{macro}} should be the first statement in `<script setup>` (after any potential import statements or type definitions).",
1056
- putExposeAtTheLast: "Put `defineExpose` as the last statement in `<script setup>`."
1057
- },
1058
- schema: [],
1059
- 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: " "
1060
812
  },
1061
- create(context) {
1062
- if (!context.filename.endsWith(".vue")) {
1063
- return {};
1064
- }
1065
- 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: "["
1066
828
  }
1067
- };
1068
-
1069
- const rule$1 = {
829
+ ];
830
+ const rule = {
831
+ defaultOptions: [],
1070
832
  meta: {
1071
- docs: {
1072
- description: "Prevent unnecessary props. and $props. prefixes in Vue templates",
1073
- recommended: true
1074
- },
833
+ docs: { description: 'Format Vue templates: strip unnecessary props/$props prefixes, redundant ="true" attributes (except aria- attributes), and enforce t() over $t()' },
1075
834
  fixable: "code",
1076
- 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
+ },
1077
840
  schema: [],
1078
841
  type: "problem"
1079
842
  },
@@ -1082,31 +845,23 @@ const rule$1 = {
1082
845
  return {};
1083
846
  }
1084
847
  return {
1085
- CallExpression(node) {
1086
- if (node.callee.type !== "Identifier" || node.callee.name !== "defineProps") {
1087
- return;
1088
- }
848
+ Program() {
1089
849
  const sourceCode = context.sourceCode;
1090
850
  const source = sourceCode.getText();
1091
- const templateRegex = /<template>([\s\S]*)<\/template>/i;
1092
851
  const templateMatch = templateRegex.exec(source);
1093
852
  if (!templateMatch) {
1094
853
  return;
1095
854
  }
1096
855
  const templateContent = templateMatch[1];
1097
- const propsPrefixRegex = /\$?props\.(\w+)/g;
856
+ const templateStartIndex = templateMatch.index + templateTagLength;
857
+ propsPrefixRegex.lastIndex = 0;
1098
858
  let prefixMatch = propsPrefixRegex.exec(templateContent);
1099
859
  while (prefixMatch !== null) {
1100
860
  const propName = prefixMatch[1];
1101
861
  const fullMatch = prefixMatch[0];
1102
- const matchIndex = templateMatch.index + 10 + (prefixMatch.index ?? 0);
862
+ const matchIndex = templateStartIndex + prefixMatch.index;
1103
863
  context.report({
1104
- fix: (fixer) => {
1105
- return fixer.replaceTextRange(
1106
- [matchIndex, matchIndex + fullMatch.length],
1107
- propName
1108
- );
1109
- },
864
+ fix: (fixer) => fixer.replaceTextRange([matchIndex, matchIndex + fullMatch.length], propName),
1110
865
  loc: {
1111
866
  end: sourceCode.getLocFromIndex(matchIndex + fullMatch.length),
1112
867
  start: sourceCode.getLocFromIndex(matchIndex)
@@ -1115,38 +870,7 @@ const rule$1 = {
1115
870
  });
1116
871
  prefixMatch = propsPrefixRegex.exec(templateContent);
1117
872
  }
1118
- }
1119
- };
1120
- }
1121
- };
1122
-
1123
- const rule = {
1124
- meta: {
1125
- docs: {
1126
- description: 'Remove unnecessary ="true" attributes in Vue templates, except for aria- attributes',
1127
- recommended: true
1128
- },
1129
- fixable: "code",
1130
- messages: { removeTrueAttribute: 'Unnecessary ="true" attribute. Use the attribute name directly instead.' },
1131
- schema: [],
1132
- type: "problem"
1133
- },
1134
- create(context) {
1135
- if (!context.filename.endsWith(".vue")) {
1136
- return {};
1137
- }
1138
- return {
1139
- Program() {
1140
- const sourceCode = context.sourceCode;
1141
- const source = sourceCode.getText();
1142
- const templateRegex = /<template>([\s\S]*)<\/template>/i;
1143
- const templateMatch = templateRegex.exec(source);
1144
- if (!templateMatch) {
1145
- return;
1146
- }
1147
- const templateContent = templateMatch[1];
1148
- const templateStartIndex = templateMatch.index + 10;
1149
- const trueAttributeRegex = /(?:^|\s):?(?!aria-)([a-zA-Z0-9-]+)="true"/g;
873
+ trueAttributeRegex.lastIndex = 0;
1150
874
  let trueAttributeMatch = trueAttributeRegex.exec(templateContent);
1151
875
  while (trueAttributeMatch !== null) {
1152
876
  const fullMatch = trueAttributeMatch[0];
@@ -1158,12 +882,7 @@ const rule = {
1158
882
  continue;
1159
883
  }
1160
884
  context.report({
1161
- fix: (fixer) => {
1162
- return fixer.replaceTextRange(
1163
- [matchIndex, matchIndex + fullMatch.length],
1164
- ` ${attributeName}`
1165
- );
1166
- },
885
+ fix: (fixer) => fixer.replaceTextRange([matchIndex, matchIndex + fullMatch.length], attributeName),
1167
886
  loc: {
1168
887
  end: sourceCode.getLocFromIndex(matchIndex + fullMatch.length),
1169
888
  start: sourceCode.getLocFromIndex(matchIndex)
@@ -1172,6 +891,23 @@ const rule = {
1172
891
  });
1173
892
  trueAttributeMatch = trueAttributeRegex.exec(templateContent);
1174
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
+ }
1175
911
  }
1176
912
  };
1177
913
  }
@@ -1179,19 +915,11 @@ const rule = {
1179
915
 
1180
916
  const index = {
1181
917
  rules: {
1182
- "ts-multiline-ternary": rule$c,
1183
- "ts-multiline-union": rule$b,
1184
- "ts-padding-statements": rule$a,
1185
- "ts-sort-tests": rule$9,
1186
- "vue-i18n-consistent-locales": rule$8,
1187
- "vue-i18n-consistent-t": rule$7,
1188
- "vue-i18n-sort-keys": rule$6,
1189
- "vue-i18n-unused-strings": rule$5,
1190
- "vue-script-format-computed": rule$4,
1191
- "vue-script-format-emits": rule$3,
1192
- "vue-script-order": rule$2,
1193
- "vue-template-format-props": rule$1,
1194
- "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
1195
923
  }
1196
924
  };
1197
925