@saasmakers/eslint 0.1.12 → 0.1.13

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,1161 +1,3 @@
1
- import { AST_NODE_TYPES } from '@typescript-eslint/utils';
2
-
3
- const rule$e = {
4
- meta: {
5
- docs: {
6
- category: "Stylistic Issues",
7
- description: "Enforce one-line ternaries if under a character limit, multiline otherwise",
8
- recommended: true
9
- },
10
- fixable: "whitespace",
11
- messages: {
12
- multiline: "Ternary expression should be multiline when it exceeds max characters.",
13
- singleLine: "Ternary expression should be single-line when it is under max characters."
14
- },
15
- schema: [
16
- {
17
- additionalProperties: false,
18
- properties: { maxCharacters: { type: "number" } },
19
- type: "object"
20
- }
21
- ],
22
- type: "layout"
23
- },
24
- create(context) {
25
- const maxCharacters = context.options[0]?.maxCharacters ?? 100;
26
- return {
27
- ConditionalExpression(node) {
28
- if (node.type !== AST_NODE_TYPES.ConditionalExpression) {
29
- return;
30
- }
31
- const sourceCode = context.sourceCode;
32
- const ternaryText = sourceCode.getText(node);
33
- const isMultiline = ternaryText.includes("\n");
34
- const isOverMaxLength = ternaryText.length > maxCharacters;
35
- if (isOverMaxLength && !isMultiline) {
36
- context.report({
37
- fix: (fixer) => {
38
- const [
39
- test,
40
- consequent,
41
- alternate
42
- ] = [
43
- node.test,
44
- node.consequent,
45
- node.alternate
46
- ].map((part) => sourceCode.getText(part));
47
- return fixer.replaceText(node, `${test} ?
48
- ${consequent} :
49
- ${alternate}`);
50
- },
51
- messageId: "multiline",
52
- node
53
- });
54
- } else if (!isOverMaxLength && isMultiline) {
55
- context.report({
56
- fix: (fixer) => fixer.replaceText(node, ternaryText.replace(/\s+/g, " ")),
57
- messageId: "singleLine",
58
- node
59
- });
60
- }
61
- }
62
- };
63
- }
64
- };
65
-
66
- const rule$d = {
67
- meta: {
68
- docs: {
69
- category: "Stylistic Issues",
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, `
117
- | ${types.join("\n | ")}`);
118
- }
119
- );
120
- } else if (!isOverMaxLength && !hasEnoughItems && isMultiline) {
121
- reportAndFix(
122
- node,
123
- "singleLine",
124
- (fixer) => fixer.replaceText(node, unionText.replace(/\s+\|\s+/g, " | "))
125
- );
126
- }
127
- }
128
- };
129
- }
130
- };
131
-
132
- const rule$c = {
133
- meta: {
134
- docs: {
135
- category: "Best Practices",
136
- description: "Enforce sorted test functions grouped by method with sorted errors, exceptions and middlewares",
137
- recommended: true
138
- },
139
- fixable: "code",
140
- messages: { sortError: "Test functions should be grouped by method with sorted errors, exceptions and middlewares." },
141
- schema: [],
142
- type: "suggestion"
143
- },
144
- create(context) {
145
- if (!context.filename.endsWith(".spec.ts")) {
146
- return {};
147
- }
148
- function getTestName(node) {
149
- if (node.arguments && node.arguments[0] && node.arguments[0].type === "Literal") {
150
- return node.arguments[0].value;
151
- }
152
- return "";
153
- }
154
- function getTestPriority(testName) {
155
- if (testName.includes(".middlewares.")) {
156
- return 4;
157
- }
158
- if (testName.includes(".exceptions.")) {
159
- return 3;
160
- }
161
- if (testName.includes(".errors.")) {
162
- return 2;
163
- }
164
- return 1;
165
- }
166
- function getFunctionName(testName) {
167
- const parts = testName.split(".");
168
- return parts.slice(0, 2).join(".");
169
- }
170
- function getSpecificName(testName) {
171
- const parts = testName.split(".");
172
- const specialIndex = parts.findIndex(
173
- (part) => part === "errors" || part === "exceptions" || part === "middlewares"
174
- );
175
- if (specialIndex === -1) {
176
- return parts.slice(2).join(".");
177
- }
178
- return parts.slice(specialIndex + 1).join(".");
179
- }
180
- function compareTests(testA, testB) {
181
- const functionA = getFunctionName(testA);
182
- const functionB = getFunctionName(testB);
183
- if (functionA !== functionB) {
184
- return functionA.localeCompare(functionB);
185
- }
186
- const priorityA = getTestPriority(testA);
187
- const priorityB = getTestPriority(testB);
188
- if (priorityA !== priorityB) {
189
- return priorityA - priorityB;
190
- }
191
- const specificNameA = getSpecificName(testA);
192
- const specificNameB = getSpecificName(testB);
193
- return specificNameA.localeCompare(specificNameB);
194
- }
195
- function handleTestGroup(node) {
196
- const testGroup = node.arguments[0];
197
- if (!testGroup || !node.arguments[1]) {
198
- return;
199
- }
200
- const groupCallback = node.arguments[1];
201
- if (groupCallback.type !== "FunctionExpression" && groupCallback.type !== "ArrowFunctionExpression") {
202
- return;
203
- }
204
- const callbackBody = groupCallback.body;
205
- if (callbackBody.type !== "BlockStatement" || !callbackBody.body) {
206
- return;
207
- }
208
- const tests = callbackBody.body.filter((statement) => {
209
- return statement.type === "ExpressionStatement" && statement.expression.type === "CallExpression" && statement.expression.callee.type === "Identifier" && (statement.expression.callee.name === "test" || statement.expression.callee.name === "it");
210
- });
211
- const testNames = tests.map((test) => getTestName(test.expression));
212
- const sortedTestNames = [...testNames].sort(compareTests);
213
- const isSorted = testNames.every(
214
- (name, index) => name === sortedTestNames[index]
215
- );
216
- if (!isSorted) {
217
- context.report({
218
- fix(fixer) {
219
- const testMap = new Map(
220
- tests.map((test) => [getTestName(test.expression), test])
221
- );
222
- const sortedTests = sortedTestNames.map((name) => testMap.get(name));
223
- const fixes = [];
224
- for (let index = 0; index < tests.length; index++) {
225
- const originalTest = tests[index];
226
- const sortedTest = sortedTests[index];
227
- if (originalTest !== sortedTest) {
228
- fixes.push(fixer.replaceText(
229
- originalTest,
230
- context.sourceCode.getText(sortedTest)
231
- ));
232
- }
233
- }
234
- return fixes;
235
- },
236
- loc: node.loc ?? void 0,
237
- messageId: "sortError",
238
- node
239
- });
240
- }
241
- }
242
- return {
243
- 'CallExpression[callee.name="describe"]': handleTestGroup,
244
- 'CallExpression[callee.object.name="describe"][callee.property.name="concurrent"]': handleTestGroup,
245
- 'CallExpression[callee.object.name="test"][callee.property.name="group"]': handleTestGroup
246
- };
247
- }
248
- };
249
-
250
- const rule$b = {
251
- meta: {
252
- docs: {
253
- category: "Possible Errors",
254
- description: "Enforce consistent i18n locale keys across translations",
255
- recommended: true
256
- },
257
- messages: {
258
- invalidJson: "Invalid JSON in i18n block: {{error}}",
259
- invalidLocale: "Invalid locale: {{locale}}. Allowed locales are: {{allowed}}",
260
- missingLocale: "Missing required locale: {{locale}}",
261
- missingTranslations: 'Missing translations in "{{locale}}" locale: {{missing}}'
262
- },
263
- schema: [
264
- {
265
- additionalProperties: false,
266
- properties: {
267
- locales: {
268
- items: { type: "string" },
269
- type: "array"
270
- }
271
- },
272
- type: "object"
273
- }
274
- ],
275
- type: "problem"
276
- },
277
- create(context) {
278
- if (!context.filename.endsWith(".vue")) {
279
- return {};
280
- }
281
- const locales = context.options[0]?.locales || ["en", "fr"];
282
- return {
283
- Program() {
284
- const source = context.sourceCode.getText();
285
- const i18nMatch = source.match(/(<i18n\s+lang=["']json["']>)([\s\S]*?)(<\/i18n>)/i);
286
- if (i18nMatch) {
287
- const startOffset = i18nMatch.index + i18nMatch[1].length;
288
- const i18nContent = i18nMatch[2].trim();
289
- try {
290
- const parsed = JSON.parse(i18nContent);
291
- for (const locale of locales) {
292
- if (!parsed[locale]) {
293
- context.report({
294
- data: { locale },
295
- loc: {
296
- end: context.sourceCode.getLocFromIndex(startOffset + i18nContent.length),
297
- start: context.sourceCode.getLocFromIndex(startOffset)
298
- },
299
- messageId: "missingLocale"
300
- });
301
- }
302
- }
303
- for (const locale of Object.keys(parsed)) {
304
- if (!locales.includes(locale)) {
305
- const localeMatch = new RegExp(`"${locale}"\\s*:`, "g").exec(i18nContent);
306
- if (localeMatch) {
307
- const localeOffset = startOffset + localeMatch.index;
308
- context.report({
309
- data: {
310
- allowed: locales.join(", "),
311
- locale
312
- },
313
- loc: {
314
- end: context.sourceCode.getLocFromIndex(localeOffset + locale.length + 2),
315
- start: context.sourceCode.getLocFromIndex(localeOffset)
316
- },
317
- messageId: "invalidLocale"
318
- });
319
- }
320
- }
321
- }
322
- const allKeys = /* @__PURE__ */ new Set();
323
- for (const locale of locales) {
324
- if (parsed[locale]) {
325
- const keys = getAllKeys(parsed[locale]);
326
- keys.forEach((key) => allKeys.add(key));
327
- }
328
- }
329
- for (const locale of locales) {
330
- if (parsed[locale]) {
331
- const localeKeys = getAllKeys(parsed[locale]);
332
- const missingKeys = [...allKeys].filter((key) => !localeKeys.includes(key));
333
- if (missingKeys.length > 0) {
334
- const localeMatch = new RegExp(`"${locale}"\\s*:\\s*{`, "g").exec(i18nContent);
335
- if (localeMatch) {
336
- const localeOffset = startOffset + localeMatch.index;
337
- context.report({
338
- data: {
339
- locale,
340
- missing: missingKeys.join(", ")
341
- },
342
- loc: {
343
- end: context.sourceCode.getLocFromIndex(localeOffset + locale.length + 2),
344
- start: context.sourceCode.getLocFromIndex(localeOffset)
345
- },
346
- messageId: "missingTranslations"
347
- });
348
- }
349
- }
350
- }
351
- }
352
- } catch (error) {
353
- const errorMessage = error instanceof Error ? error.message : String(error);
354
- context.report({
355
- data: { error: errorMessage },
356
- loc: {
357
- end: context.sourceCode.getLocFromIndex(startOffset + i18nContent.length),
358
- start: context.sourceCode.getLocFromIndex(startOffset)
359
- },
360
- messageId: "invalidJson"
361
- });
362
- }
363
- }
364
- function getAllKeys(object, prefix = "") {
365
- let keys = [];
366
- for (const key in object) {
367
- const newPrefix = prefix ? `${prefix}.${key}` : key;
368
- if (typeof object[key] === "object" && object[key] !== null) {
369
- keys = [...keys, ...getAllKeys(object[key], newPrefix)];
370
- } else {
371
- keys.push(newPrefix);
372
- }
373
- }
374
- return keys;
375
- }
376
- }
377
- };
378
- }
379
- };
380
-
381
- const rule$a = {
382
- meta: {
383
- docs: {
384
- category: "Best Practices",
385
- description: "Enforce using t() instead of $t() in Vue templates",
386
- recommended: true
387
- },
388
- fixable: "code",
389
- messages: { useT: "Use t() instead of $t() in Vue templates as it does not work with <i18n> tags." },
390
- schema: [],
391
- type: "problem"
392
- },
393
- create(context) {
394
- if (!context.filename.endsWith(".vue")) {
395
- return {};
396
- }
397
- return {
398
- Program() {
399
- const sourceCode = context.sourceCode;
400
- const source = sourceCode.getText();
401
- const templateMatch = source.match(/<template>([\s\S]*)<\/template>/i);
402
- if (!templateMatch || templateMatch.index === void 0) {
403
- return;
404
- }
405
- const templateContent = templateMatch[1];
406
- const tPatterns = [
407
- {
408
- pattern: / \$t\(/g,
409
- quote: " "
410
- },
411
- {
412
- pattern: /"\$t\(/g,
413
- quote: '"'
414
- },
415
- {
416
- pattern: /`\$t\(/g,
417
- quote: "`"
418
- },
419
- {
420
- pattern: /\{\$t\(/g,
421
- quote: "{"
422
- },
423
- {
424
- pattern: /\[\$t\(/g,
425
- quote: "["
426
- }
427
- ];
428
- for (const { pattern, quote } of tPatterns) {
429
- let match;
430
- while ((match = pattern.exec(templateContent)) !== null) {
431
- const templateStart = templateMatch.index + templateMatch[0].indexOf(templateContent);
432
- const start = templateStart + match.index;
433
- const end = start + 4;
434
- context.report({
435
- fix(fixer) {
436
- return fixer.replaceTextRange([start, end], `${quote}t(`);
437
- },
438
- loc: {
439
- end: sourceCode.getLocFromIndex(end),
440
- start: sourceCode.getLocFromIndex(start)
441
- },
442
- messageId: "useT"
443
- });
444
- }
445
- }
446
- }
447
- };
448
- }
449
- };
450
-
451
- const rule$9 = {
452
- meta: {
453
- docs: {
454
- category: "Best Practices",
455
- description: "Enforce consistent indentation and sorted keys in i18n blocks",
456
- recommended: true
457
- },
458
- fixable: "whitespace",
459
- messages: {
460
- indentError: "Invalid indentation for i18n content. Expected {{expected}} spaces.",
461
- invalidJson: "Invalid JSON in i18n block: {{error}}",
462
- sortError: "Keys in i18n block should be sorted alphabetically."
463
- },
464
- schema: [],
465
- type: "problem"
466
- },
467
- create(context) {
468
- if (!context.filename.endsWith(".vue")) {
469
- return {};
470
- }
471
- function sortObjectKeys(object) {
472
- if (Array.isArray(object)) {
473
- return object.map(sortObjectKeys);
474
- }
475
- if (object && typeof object === "object") {
476
- const sortedKeys = Object.keys(object).sort();
477
- const result = {};
478
- const obj = object;
479
- for (const key of sortedKeys) {
480
- result[key] = sortObjectKeys(obj[key]);
481
- }
482
- return result;
483
- }
484
- return object;
485
- }
486
- return {
487
- Program() {
488
- const source = context.sourceCode.getText();
489
- const i18nMatch = source.match(/(<i18n\s+lang=["']json["']>)([\s\S]*?)(<\/i18n>)/i);
490
- if (i18nMatch) {
491
- const startOffset = i18nMatch.index + i18nMatch[1].length;
492
- const i18nContent = i18nMatch[2];
493
- try {
494
- const parsed = JSON.parse(i18nContent.trim());
495
- const totalSpaces = 2;
496
- const sortedParsed = sortObjectKeys(parsed);
497
- const formattedContent = `
498
- ${JSON.stringify(sortedParsed, null, totalSpaces).replace(/\n\n+/g, "\n")}
499
- `;
500
- if (formattedContent.trim() !== i18nContent.trim()) {
501
- const currentKeys = JSON.stringify(parsed, null, totalSpaces).trim();
502
- const sortedKeys = JSON.stringify(sortedParsed, null, totalSpaces).trim();
503
- context.report({
504
- data: { expected: String(totalSpaces) },
505
- fix(fixer) {
506
- return fixer.replaceTextRange(
507
- [startOffset, startOffset + i18nContent.length],
508
- formattedContent
509
- );
510
- },
511
- loc: {
512
- end: context.sourceCode.getLocFromIndex(startOffset + i18nContent.length),
513
- start: context.sourceCode.getLocFromIndex(startOffset)
514
- },
515
- messageId: currentKeys !== sortedKeys ? "sortError" : "indentError"
516
- });
517
- }
518
- } catch (error) {
519
- const errorMessage = error instanceof Error ? error.message : String(error);
520
- context.report({
521
- data: { error: errorMessage },
522
- loc: {
523
- end: context.sourceCode.getLocFromIndex(startOffset + i18nContent.length),
524
- start: context.sourceCode.getLocFromIndex(startOffset)
525
- },
526
- messageId: "invalidJson"
527
- });
528
- }
529
- }
530
- }
531
- };
532
- }
533
- };
534
-
535
- const rule$8 = {
536
- meta: {
537
- docs: {
538
- category: "Best Practices",
539
- description: "Detect unused strings in the English locale",
540
- recommended: true
541
- },
542
- messages: {
543
- invalidJson: "Invalid JSON in i18n block: {{error}}",
544
- unusedString: 'Unused string in English locale: "{{key}}"'
545
- },
546
- schema: [],
547
- type: "problem"
548
- },
549
- create(context) {
550
- if (!context.filename.endsWith(".vue")) {
551
- return {};
552
- }
553
- return {
554
- Program() {
555
- const source = context.sourceCode.getText();
556
- const i18nMatch = source.match(/(<i18n\s+lang=["']json["']>)([\s\S]*?)(<\/i18n>)/i);
557
- if (!i18nMatch) {
558
- return;
559
- }
560
- const startOffset = i18nMatch.index + i18nMatch[1].length;
561
- const i18nContent = i18nMatch[2].trim();
562
- try {
563
- const parsed = JSON.parse(i18nContent);
564
- if (!parsed.en) {
565
- return;
566
- }
567
- const templateMatch = source.match(/<template>([\s\S]*)<\/template>/i);
568
- const templateContent = templateMatch ? templateMatch[1] : "";
569
- const scriptMatch = source.match(/<script[^>]*>([\s\S]*?)<\/script>/i);
570
- const scriptContent = scriptMatch ? scriptMatch[1] : "";
571
- const enKeys = getAllKeys(parsed.en);
572
- for (const key of enKeys) {
573
- const isUsed = templateContent.includes(key) || scriptContent.includes(key);
574
- if (!isUsed) {
575
- const keyMatch = new RegExp(`"${key}"\\s*:`, "g").exec(i18nContent);
576
- if (keyMatch) {
577
- const keyOffset = startOffset + keyMatch.index;
578
- context.report({
579
- data: { key },
580
- loc: {
581
- end: context.sourceCode.getLocFromIndex(keyOffset + key.length + 2),
582
- start: context.sourceCode.getLocFromIndex(keyOffset)
583
- },
584
- messageId: "unusedString"
585
- });
586
- }
587
- }
588
- }
589
- } catch (error) {
590
- const errorMessage = error instanceof Error ? error.message : String(error);
591
- context.report({
592
- data: { error: errorMessage },
593
- loc: {
594
- end: context.sourceCode.getLocFromIndex(startOffset + i18nContent.length),
595
- start: context.sourceCode.getLocFromIndex(startOffset)
596
- },
597
- messageId: "invalidJson"
598
- });
599
- }
600
- }
601
- };
602
- function getAllKeys(object, prefix = "") {
603
- let keys = [];
604
- for (const key in object) {
605
- const newPrefix = prefix ? `${prefix}.${key}` : key;
606
- if (typeof object[key] === "object" && object[key] !== null) {
607
- keys = [...keys, ...getAllKeys(object[key], newPrefix)];
608
- } else {
609
- keys.push(newPrefix);
610
- }
611
- }
612
- return keys;
613
- }
614
- }
615
- };
616
-
617
- const rule$7 = {
618
- defaultOptions: [],
619
- meta: {
620
- docs: { description: "Enforce multiline style for Vue computed properties" },
621
- fixable: "code",
622
- messages: { multilineComputed: "Computed properties should use explicit return with blocks" },
623
- schema: [],
624
- type: "layout"
625
- },
626
- create(context) {
627
- return {
628
- CallExpression(node) {
629
- if (node.callee.type === AST_NODE_TYPES.Identifier && node.callee.name === "computed") {
630
- const [argument] = node.arguments;
631
- if (argument?.type === AST_NODE_TYPES.ArrowFunctionExpression) {
632
- const sourceCode = context.sourceCode;
633
- const functionBody = sourceCode.getText(argument.body);
634
- if (!functionBody.startsWith("{")) {
635
- const bodyText = functionBody.startsWith("(") && functionBody.endsWith(")") ? functionBody.slice(1, -1) : functionBody;
636
- context.report({
637
- fix(fixer) {
638
- return fixer.replaceText(
639
- argument.body,
640
- `{
641
- return ${bodyText}
642
- }`
643
- );
644
- },
645
- messageId: "multilineComputed",
646
- node
647
- });
648
- }
649
- }
650
- }
651
- }
652
- };
653
- }
654
- };
655
-
656
- const rule$6 = {
657
- meta: {
658
- docs: {
659
- category: "Best Practices",
660
- description: "Enforce typed emits in Vue components",
661
- recommended: true
662
- },
663
- fixable: "code",
664
- messages: { untypedEmits: "Use typed emits instead of untyped emits" },
665
- schema: [],
666
- type: "problem"
667
- },
668
- create(context) {
669
- if (!context.filename.endsWith(".vue")) {
670
- return {};
671
- }
672
- return {
673
- Program(node) {
674
- const source = context.sourceCode.getText();
675
- const untypedEmitsRegex = /const\s+emit\s*=\s*defineEmits\(\[([^\]]+)\]\)/g;
676
- let match = untypedEmitsRegex.exec(source);
677
- while (match !== null) {
678
- const [fullMatch, eventsString] = match;
679
- const matchIndex = match.index;
680
- const events = eventsString.split(",").map((event) => event.trim().replace(/['"]/g, "")).filter(Boolean);
681
- const typedEvents = events.map((event) => ` ${event}: []`).join(",\n");
682
- const typedEmits = `const emit = defineEmits<{
683
- ${typedEvents}
684
- }>()`;
685
- context.report({
686
- fix: (fixer) => {
687
- return fixer.replaceTextRange(
688
- [matchIndex, matchIndex + fullMatch.length],
689
- typedEmits
690
- );
691
- },
692
- loc: {
693
- end: context.sourceCode.getLocFromIndex(matchIndex + fullMatch.length),
694
- start: context.sourceCode.getLocFromIndex(matchIndex)
695
- },
696
- messageId: "untypedEmits",
697
- node
698
- });
699
- match = untypedEmitsRegex.exec(source);
700
- }
701
- }
702
- };
703
- }
704
- };
705
-
706
- const rule$5 = {
707
- meta: {
708
- docs: {
709
- category: "Best Practices",
710
- description: "Prevent comments, empty lines, redundant required: false, enforce default: undefined for single-key props, and detect unused props inside defineProps",
711
- recommended: true
712
- },
713
- fixable: "code",
714
- messages: {
715
- noCommentsInProps: "Comments are not allowed inside defineProps.",
716
- noEmptyLinesInProps: "Empty lines are not allowed between props in defineProps.",
717
- noRedundantRequired: "required: false is redundant in props. Props are optional by default.",
718
- singleKeyProp: "Props with only one key should have default: undefined for better readability.",
719
- unusedProp: 'Unused prop: "{{propName}}"'
720
- },
721
- schema: [],
722
- type: "problem"
723
- },
724
- create(context) {
725
- if (!context.filename.endsWith(".vue")) {
726
- return {};
727
- }
728
- return {
729
- CallExpression(node) {
730
- if (node.callee.type !== "Identifier" || node.callee.name !== "defineProps") {
731
- return;
732
- }
733
- const sourceCode = context.sourceCode;
734
- const propsArg = node.arguments[0];
735
- if (!propsArg?.range) {
736
- return;
737
- }
738
- const propsArgRange = propsArg.range;
739
- const commentsInProps = [
740
- ...sourceCode.getCommentsBefore(propsArg),
741
- ...sourceCode.getCommentsInside(propsArg)
742
- ].filter((comment) => {
743
- return comment.range !== void 0 && comment.range[0] >= propsArgRange[0] && comment.range[1] <= propsArgRange[1];
744
- });
745
- for (const comment of commentsInProps) {
746
- context.report({
747
- fix: (fixer) => {
748
- return fixer.removeRange(comment.range);
749
- },
750
- loc: comment.loc ?? node.loc ?? {
751
- end: {
752
- column: 0,
753
- line: 0
754
- },
755
- start: {
756
- column: 0,
757
- line: 0
758
- }
759
- },
760
- messageId: "noCommentsInProps"
761
- });
762
- }
763
- const propsText = sourceCode.getText(propsArg);
764
- const emptyLineRegex = /(?:{\s*\n\s*\n|,\s*\n\s*\n)\s*(?:([a-zA-Z])|(?:\s*}))/g;
765
- let emptyLineMatch = emptyLineRegex.exec(propsText);
766
- while (emptyLineMatch !== null) {
767
- const matchIndex = emptyLineMatch.index;
768
- const matchLength = emptyLineMatch[0].length;
769
- const lastChar = emptyLineMatch[0].trim().charAt(0);
770
- const isEndBrace = emptyLineMatch[0].trim().endsWith("}");
771
- const firstLetter = emptyLineMatch[1];
772
- const startIndex = propsArg.range[0] + matchIndex;
773
- const endIndex = startIndex + matchLength;
774
- context.report({
775
- fix: (fixer) => {
776
- let replacement;
777
- if (isEndBrace) {
778
- replacement = "\n}";
779
- } else {
780
- replacement = (lastChar === "{" ? "{\n " : ",\n ") + firstLetter;
781
- }
782
- return fixer.replaceTextRange([startIndex, endIndex], replacement);
783
- },
784
- loc: {
785
- end: sourceCode.getLocFromIndex(endIndex),
786
- start: sourceCode.getLocFromIndex(startIndex)
787
- },
788
- messageId: "noEmptyLinesInProps"
789
- });
790
- emptyLineMatch = emptyLineRegex.exec(propsText);
791
- }
792
- const redundantRequiredRegex = /required:\s*false\s*,?/g;
793
- let redundantMatch = redundantRequiredRegex.exec(propsText);
794
- while (redundantMatch !== null) {
795
- const matchIndex = redundantMatch.index;
796
- const matchLength = redundantMatch[0].length;
797
- const startIndex = propsArg.range[0] + matchIndex;
798
- const endIndex = startIndex + matchLength;
799
- context.report({
800
- fix: (fixer) => {
801
- return fixer.removeRange([startIndex, endIndex]);
802
- },
803
- loc: {
804
- end: sourceCode.getLocFromIndex(endIndex),
805
- start: sourceCode.getLocFromIndex(startIndex)
806
- },
807
- messageId: "noRedundantRequired"
808
- });
809
- redundantMatch = redundantRequiredRegex.exec(propsText);
810
- }
811
- const source = sourceCode.getText();
812
- const templateMatch = source.match(/<template>([\s\S]*)<\/template>/i);
813
- const templateContent = templateMatch ? templateMatch[1] : "";
814
- const scriptMatch = source.match(/<script[^>]*>([\s\S]*?)<\/script>/i);
815
- const scriptContent = scriptMatch ? scriptMatch[1] : "";
816
- const propDefinitions = propsText.match(/(\w+):\s*{([^}]+)}/g) ?? [];
817
- for (const propDef of propDefinitions) {
818
- const propNameMatch = propDef.match(/^(\w+):\s*{([^}]+)}/);
819
- if (!propNameMatch) {
820
- continue;
821
- }
822
- const propName = propNameMatch[1];
823
- const propContent = propNameMatch[2].trim();
824
- const hasDefault = propContent.includes("default:");
825
- const hasRequired = propContent.includes("required:");
826
- if (!hasDefault && !hasRequired && propContent.includes("type:")) {
827
- const fullPropRegex = new RegExp(`${propName}:\\s*{[^}]+}`, "g");
828
- const fullMatch = propsText.match(fullPropRegex);
829
- if (!fullMatch) {
830
- continue;
831
- }
832
- const fullPropDef = fullMatch[0];
833
- const matchIndex = propsText.indexOf(fullPropDef);
834
- const matchLength = fullPropDef.length;
835
- const startIndex = propsArg.range[0] + matchIndex;
836
- const endIndex = startIndex + matchLength;
837
- context.report({
838
- fix: (fixer) => {
839
- const newText = fullPropDef.replace(/{\s*type:/, "{\n default: undefined,\n type:").replace(/,\s*\n\s*}/, "}");
840
- return fixer.replaceTextRange([startIndex, endIndex], newText);
841
- },
842
- loc: {
843
- end: sourceCode.getLocFromIndex(endIndex),
844
- start: sourceCode.getLocFromIndex(startIndex)
845
- },
846
- messageId: "singleKeyProp"
847
- });
848
- }
849
- const isUsedInTemplate = templateContent.includes(propName);
850
- const isUsedInScript = scriptContent.replace(propsText, "").includes(propName);
851
- if (!isUsedInTemplate && !isUsedInScript) {
852
- const propStartIndex = propsArg.range[0] + propsText.indexOf(propDef);
853
- const propEndIndex = propStartIndex + propDef.length;
854
- context.report({
855
- data: { propName },
856
- loc: {
857
- end: sourceCode.getLocFromIndex(propEndIndex),
858
- start: sourceCode.getLocFromIndex(propStartIndex)
859
- },
860
- messageId: "unusedProp"
861
- });
862
- }
863
- }
864
- }
865
- };
866
- }
867
- };
868
-
869
- const rule$4 = {
870
- meta: {
871
- docs: {
872
- category: "Best Practices",
873
- description: "enforce order of compiler macros (`defineProps`, `defineEmits`, etc.)",
874
- recommended: true
875
- },
876
- fixable: "code",
877
- hasSuggestions: true,
878
- messages: {
879
- defineExposeNotTheLast: "`defineExpose` should be the last statement in `<script setup>`.",
880
- macrosNotOnTop: "{{macro}} should be the first statement in `<script setup>` (after any potential import statements or type definitions).",
881
- putExposeAtTheLast: "Put `defineExpose` as the last statement in `<script setup>`."
882
- },
883
- schema: [],
884
- type: "problem"
885
- },
886
- create(context) {
887
- if (!context.filename.endsWith(".vue")) {
888
- return {};
889
- }
890
- return { Program() {
891
- } };
892
- }
893
- };
894
-
895
- const rule$3 = {
896
- meta: {
897
- docs: {
898
- category: "Best Practices",
899
- description: 'Prevent empty lines inside :class="{ bindings in Vue template tags',
900
- recommended: true
901
- },
902
- fixable: "code",
903
- messages: { noEmptyLinesInClass: 'Empty lines are not allowed inside :class="{ bindings.' },
904
- schema: [],
905
- type: "problem"
906
- },
907
- create(context) {
908
- if (!context.filename.endsWith(".vue")) {
909
- return {};
910
- }
911
- return {
912
- Program() {
913
- const sourceCode = context.sourceCode;
914
- const source = sourceCode.getText();
915
- const templateMatch = source.match(/<template>([\s\S]*)<\/template>/i);
916
- if (!templateMatch) {
917
- return;
918
- }
919
- const templateContent = templateMatch[1];
920
- const classBindingRegex = /:class="\{([\s\S]*?)\}"/g;
921
- let classBindingMatch;
922
- while ((classBindingMatch = classBindingRegex.exec(templateContent)) !== null) {
923
- const classContent = classBindingMatch[1];
924
- const emptyLineRegex = /\n\s*\n/g;
925
- let emptyLineMatch;
926
- while ((emptyLineMatch = emptyLineRegex.exec(classContent)) !== null) {
927
- const matchIndex = emptyLineMatch.index;
928
- const matchLength = emptyLineMatch[0].length;
929
- const templateStartIndex = source.indexOf(templateContent);
930
- const classBindingStartIndex = templateStartIndex + classBindingMatch.index;
931
- const classContentStartIndex = classBindingStartIndex + 8;
932
- const startIndex = classContentStartIndex + matchIndex;
933
- const endIndex = startIndex + matchLength;
934
- context.report({
935
- // TODO: Uncomment this when we want to fix the issue
936
- // fix: (fixer) => {
937
- // // Replace consecutive newlines with a single newline
938
- // return fixer.replaceTextRange([startIndex, endIndex], '\n')
939
- // },
940
- loc: {
941
- end: sourceCode.getLocFromIndex(endIndex),
942
- start: sourceCode.getLocFromIndex(startIndex)
943
- },
944
- messageId: "noEmptyLinesInClass"
945
- });
946
- }
947
- }
948
- }
949
- };
950
- }
951
- };
952
-
953
- const rule$2 = {
954
- meta: {
955
- docs: {
956
- category: "Best Practices",
957
- description: "Prevent unnecessary props. and $props. prefixes in Vue templates",
958
- recommended: true
959
- },
960
- fixable: "code",
961
- messages: { noPropsPrefix: "Unnecessary props/$props prefix in template. Props are automatically available in template scope." },
962
- schema: [],
963
- type: "problem"
964
- },
965
- create(context) {
966
- if (!context.filename.endsWith(".vue")) {
967
- return {};
968
- }
969
- return {
970
- CallExpression(node) {
971
- if (node.callee.type !== "Identifier" || node.callee.name !== "defineProps") {
972
- return;
973
- }
974
- const sourceCode = context.sourceCode;
975
- const source = sourceCode.getText();
976
- const templateMatch = source.match(/<template>([\s\S]*)<\/template>/i);
977
- if (!templateMatch) {
978
- return;
979
- }
980
- const templateContent = templateMatch[1];
981
- const propsPrefixRegex = /(?:\$)?props\.(\w+)/g;
982
- let prefixMatch = propsPrefixRegex.exec(templateContent);
983
- while (prefixMatch !== null) {
984
- const propName = prefixMatch[1];
985
- const fullMatch = prefixMatch[0];
986
- const matchIndex = templateMatch.index + 10 + prefixMatch.index;
987
- context.report({
988
- fix: (fixer) => {
989
- return fixer.replaceTextRange(
990
- [matchIndex, matchIndex + fullMatch.length],
991
- propName
992
- );
993
- },
994
- loc: {
995
- end: sourceCode.getLocFromIndex(matchIndex + fullMatch.length),
996
- start: sourceCode.getLocFromIndex(matchIndex)
997
- },
998
- messageId: "noPropsPrefix"
999
- });
1000
- prefixMatch = propsPrefixRegex.exec(templateContent);
1001
- }
1002
- }
1003
- };
1004
- }
1005
- };
1006
-
1007
- const rule$1 = {
1008
- meta: {
1009
- docs: {
1010
- category: "Best Practices",
1011
- description: "Remove comments inside Vue template tags",
1012
- recommended: true
1013
- },
1014
- fixable: "code",
1015
- messages: { removeComment: "Comments should be removed from template tags" },
1016
- schema: [],
1017
- type: "problem"
1018
- },
1019
- create(context) {
1020
- if (!context.filename.endsWith(".vue")) {
1021
- return {};
1022
- }
1023
- return {
1024
- Program() {
1025
- const sourceCode = context.sourceCode;
1026
- const source = sourceCode.getText();
1027
- const templateMatch = source.match(/<template>([\s\S]*)<\/template>/i);
1028
- if (!templateMatch) {
1029
- return;
1030
- }
1031
- const templateContent = templateMatch[1];
1032
- const templateStartIndex = templateMatch.index + 10;
1033
- const htmlCommentRegex = /<!--[\s\S]*?-->/g;
1034
- let htmlCommentMatch = htmlCommentRegex.exec(templateContent);
1035
- while (htmlCommentMatch !== null) {
1036
- const fullMatch = htmlCommentMatch[0];
1037
- const matchIndex = templateStartIndex + htmlCommentMatch.index;
1038
- context.report({
1039
- fix: (fixer) => {
1040
- return fixer.removeRange([
1041
- matchIndex,
1042
- matchIndex + fullMatch.length
1043
- ]);
1044
- },
1045
- loc: {
1046
- end: sourceCode.getLocFromIndex(matchIndex + fullMatch.length),
1047
- start: sourceCode.getLocFromIndex(matchIndex)
1048
- },
1049
- messageId: "removeComment"
1050
- });
1051
- htmlCommentMatch = htmlCommentRegex.exec(templateContent);
1052
- }
1053
- const jsCommentRegex = /(?:^|\s)\/\/[^\n]*/g;
1054
- let jsCommentMatch = jsCommentRegex.exec(templateContent);
1055
- while (jsCommentMatch !== null) {
1056
- const fullMatch = jsCommentMatch[0];
1057
- const matchIndex = templateStartIndex + jsCommentMatch.index;
1058
- const lineBeforeMatch = templateContent.substring(0, jsCommentMatch.index).split("\n").pop() ?? "";
1059
- const isInAttribute = lineBeforeMatch.includes('="') && !lineBeforeMatch.includes('"');
1060
- if (isInAttribute) {
1061
- jsCommentMatch = jsCommentRegex.exec(templateContent);
1062
- continue;
1063
- }
1064
- context.report({
1065
- fix: (fixer) => {
1066
- return fixer.removeRange([
1067
- matchIndex,
1068
- matchIndex + fullMatch.length
1069
- ]);
1070
- },
1071
- loc: {
1072
- end: sourceCode.getLocFromIndex(matchIndex + fullMatch.length),
1073
- start: sourceCode.getLocFromIndex(matchIndex)
1074
- },
1075
- messageId: "removeComment"
1076
- });
1077
- jsCommentMatch = jsCommentRegex.exec(templateContent);
1078
- }
1079
- }
1080
- };
1081
- }
1082
- };
1083
-
1084
- const rule = {
1085
- meta: {
1086
- docs: {
1087
- category: "Best Practices",
1088
- description: 'Remove unnecessary ="true" attributes in Vue templates, except for aria- attributes',
1089
- recommended: true
1090
- },
1091
- fixable: "code",
1092
- messages: { removeTrueAttribute: 'Unnecessary ="true" attribute. Use the attribute name directly instead.' },
1093
- schema: [],
1094
- type: "problem"
1095
- },
1096
- create(context) {
1097
- if (!context.filename.endsWith(".vue")) {
1098
- return {};
1099
- }
1100
- return {
1101
- Program() {
1102
- const sourceCode = context.sourceCode;
1103
- const source = sourceCode.getText();
1104
- const templateMatch = source.match(/<template>([\s\S]*)<\/template>/i);
1105
- if (!templateMatch) {
1106
- return;
1107
- }
1108
- const templateContent = templateMatch[1];
1109
- const templateStartIndex = templateMatch.index + 10;
1110
- const trueAttributeRegex = /(?:^|\s)(?::)?(?!aria-)([a-zA-Z0-9-]+)="true"/g;
1111
- let trueAttributeMatch = trueAttributeRegex.exec(templateContent);
1112
- while (trueAttributeMatch !== null) {
1113
- const fullMatch = trueAttributeMatch[0];
1114
- const attributeName = trueAttributeMatch[1];
1115
- const matchIndex = templateStartIndex + trueAttributeMatch.index;
1116
- const lineBeforeMatch = templateContent.substring(0, trueAttributeMatch.index).split("\n").pop() ?? "";
1117
- if (lineBeforeMatch.includes("{{") && !lineBeforeMatch.includes("}}")) {
1118
- trueAttributeMatch = trueAttributeRegex.exec(templateContent);
1119
- continue;
1120
- }
1121
- context.report({
1122
- fix: (fixer) => {
1123
- return fixer.replaceTextRange(
1124
- [matchIndex, matchIndex + fullMatch.length],
1125
- ` ${attributeName}`
1126
- );
1127
- },
1128
- loc: {
1129
- end: sourceCode.getLocFromIndex(matchIndex + fullMatch.length),
1130
- start: sourceCode.getLocFromIndex(matchIndex)
1131
- },
1132
- messageId: "removeTrueAttribute"
1133
- });
1134
- trueAttributeMatch = trueAttributeRegex.exec(templateContent);
1135
- }
1136
- }
1137
- };
1138
- }
1139
- };
1140
-
1141
- const index = {
1142
- rules: {
1143
- "ts-multiline-ternary": rule$e,
1144
- "ts-multiline-union": rule$d,
1145
- "ts-sort-tests": rule$c,
1146
- "vue-i18n-consistent-locales": rule$b,
1147
- "vue-i18n-consistent-t": rule$a,
1148
- "vue-i18n-sort-keys": rule$9,
1149
- "vue-i18n-unused-strings": rule$8,
1150
- "vue-script-format-computed": rule$7,
1151
- "vue-script-format-emits": rule$6,
1152
- "vue-script-format-props": rule$5,
1153
- "vue-script-order": rule$4,
1154
- "vue-template-format-classes": rule$3,
1155
- "vue-template-format-props": rule$2,
1156
- "vue-template-remove-comments": rule$1,
1157
- "vue-template-remove-true-attributes": rule
1158
- }
1159
- };
1160
-
1161
- export { index as default };
1
+ export { s as default } from './shared/eslint.poN7Ld_I.mjs';
2
+ import 'eslint/use-at-your-own-risk';
3
+ import 'eslint';