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