@saasmakers/eslint 1.0.26 → 1.0.28

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.
@@ -10872,6 +10872,7 @@ const eslint_config = antfu__default(
10872
10872
  "saasmakers/ts-format-tests": "error",
10873
10873
  "saasmakers/vue-format-i18n": ["error", { locales: shared.localeCodes }],
10874
10874
  "saasmakers/vue-format-script": "error",
10875
+ "saasmakers/vue-format-style": "error",
10875
10876
  "saasmakers/vue-format-template": "error"
10876
10877
  }
10877
10878
  },
@@ -152,6 +152,7 @@ var eslint_config = antfu(
152
152
  "saasmakers/ts-format-tests": "error",
153
153
  "saasmakers/vue-format-i18n": ["error", { locales: localeCodes }],
154
154
  "saasmakers/vue-format-script": "error",
155
+ "saasmakers/vue-format-style": "error",
155
156
  "saasmakers/vue-format-template": "error"
156
157
  }
157
158
  },
@@ -152,6 +152,7 @@ var eslint_config = antfu(
152
152
  "saasmakers/ts-format-tests": "error",
153
153
  "saasmakers/vue-format-i18n": ["error", { locales: localeCodes }],
154
154
  "saasmakers/vue-format-script": "error",
155
+ "saasmakers/vue-format-style": "error",
155
156
  "saasmakers/vue-format-template": "error"
156
157
  }
157
158
  },
@@ -152,6 +152,7 @@ var eslint_config = antfu(
152
152
  "saasmakers/ts-format-tests": "error",
153
153
  "saasmakers/vue-format-i18n": ["error", { locales: localeCodes }],
154
154
  "saasmakers/vue-format-script": "error",
155
+ "saasmakers/vue-format-style": "error",
155
156
  "saasmakers/vue-format-template": "error"
156
157
  }
157
158
  },
@@ -10846,6 +10846,7 @@ const eslint_config = antfu(
10846
10846
  "saasmakers/ts-format-tests": "error",
10847
10847
  "saasmakers/vue-format-i18n": ["error", { locales: localeCodes }],
10848
10848
  "saasmakers/vue-format-script": "error",
10849
+ "saasmakers/vue-format-style": "error",
10849
10850
  "saasmakers/vue-format-template": "error"
10850
10851
  }
10851
10852
  },
package/dist/index.cjs CHANGED
@@ -63,7 +63,7 @@ function checkUnion(context, node, maxLineLength, minItems) {
63
63
  function collapseBlankLine(text) {
64
64
  return text.replace(/\n[^\S\n]*\n[^\S\n]*/, "\n");
65
65
  }
66
- function expressionStartsWithIdentifier(node, identifier) {
66
+ function expressionStartsWithIdentifier$1(node, identifier) {
67
67
  if (node.type === index$1.distExports.AST_NODE_TYPES.Identifier && node.name === identifier) {
68
68
  return true;
69
69
  }
@@ -73,17 +73,17 @@ function expressionStartsWithIdentifier(node, identifier) {
73
73
  return true;
74
74
  }
75
75
  if (callee.type === index$1.distExports.AST_NODE_TYPES.MemberExpression) {
76
- return expressionStartsWithIdentifier(callee.object, identifier);
76
+ return expressionStartsWithIdentifier$1(callee.object, identifier);
77
77
  }
78
78
  if (callee.type === index$1.distExports.AST_NODE_TYPES.ChainExpression) {
79
- return expressionStartsWithIdentifier(callee.expression, identifier);
79
+ return expressionStartsWithIdentifier$1(callee.expression, identifier);
80
80
  }
81
81
  }
82
82
  if (node.type === index$1.distExports.AST_NODE_TYPES.MemberExpression) {
83
- return expressionStartsWithIdentifier(node.object, identifier);
83
+ return expressionStartsWithIdentifier$1(node.object, identifier);
84
84
  }
85
85
  if (node.type === index$1.distExports.AST_NODE_TYPES.ChainExpression) {
86
- return expressionStartsWithIdentifier(node.expression, identifier);
86
+ return expressionStartsWithIdentifier$1(node.expression, identifier);
87
87
  }
88
88
  return false;
89
89
  }
@@ -211,10 +211,10 @@ function hasLineCommentBetween(prevNode, nextNode, sourceCode) {
211
211
  });
212
212
  }
213
213
  function isAssertExpression(expression) {
214
- return expressionStartsWithIdentifier(expression, "assert");
214
+ return expressionStartsWithIdentifier$1(expression, "assert");
215
215
  }
216
216
  function isConsoleExpression(expression) {
217
- return expressionStartsWithIdentifier(expression, "console");
217
+ return expressionStartsWithIdentifier$1(expression, "console");
218
218
  }
219
219
  function isDirectivePrologue(node) {
220
220
  if (node.expression.type !== index$1.distExports.AST_NODE_TYPES.Literal) {
@@ -223,7 +223,7 @@ function isDirectivePrologue(node) {
223
223
  return typeof node.expression.value === "string";
224
224
  }
225
225
  function isExpectExpression(expression) {
226
- return expressionStartsWithIdentifier(expression, "expect");
226
+ return expressionStartsWithIdentifier$1(expression, "expect");
227
227
  }
228
228
  function isOwnLineComment(comment, sourceCode) {
229
229
  if (comment.type !== "Line") {
@@ -253,7 +253,7 @@ function isThisReceiver(node) {
253
253
  function isVoidExpression(expression) {
254
254
  return expression.type === index$1.distExports.AST_NODE_TYPES.UnaryExpression && expression.operator === "void";
255
255
  }
256
- const rule$4 = {
256
+ const rule$5 = {
257
257
  defaultOptions: [{}],
258
258
  meta: {
259
259
  docs: { description: "Format TypeScript layout: single/multiline ternaries and union types, and blank lines between statements" },
@@ -403,6 +403,7 @@ const rule$4 = {
403
403
  }
404
404
  };
405
405
 
406
+ const teardownViMethods = /* @__PURE__ */ new Set(["restoreAllMocks", "unstubAllGlobals", "useRealTimers"]);
406
407
  function compareTests(testA, testB) {
407
408
  const functionA = getFunctionName(testA);
408
409
  const functionB = getFunctionName(testB);
@@ -418,6 +419,40 @@ function compareTests(testA, testB) {
418
419
  const specificNameB = getSpecificName(testB);
419
420
  return specificNameA.localeCompare(specificNameB);
420
421
  }
422
+ function expressionStartsWithIdentifier(node, identifier) {
423
+ if (node.type === index$1.distExports.AST_NODE_TYPES.Identifier && node.name === identifier) {
424
+ return true;
425
+ }
426
+ if (node.type === index$1.distExports.AST_NODE_TYPES.CallExpression) {
427
+ const { callee } = node;
428
+ if (callee.type === index$1.distExports.AST_NODE_TYPES.Identifier && callee.name === identifier) {
429
+ return true;
430
+ }
431
+ if (callee.type === index$1.distExports.AST_NODE_TYPES.MemberExpression) {
432
+ return expressionStartsWithIdentifier(callee.object, identifier);
433
+ }
434
+ if (callee.type === index$1.distExports.AST_NODE_TYPES.ChainExpression) {
435
+ return expressionStartsWithIdentifier(callee.expression, identifier);
436
+ }
437
+ }
438
+ if (node.type === index$1.distExports.AST_NODE_TYPES.MemberExpression) {
439
+ return expressionStartsWithIdentifier(node.object, identifier);
440
+ }
441
+ if (node.type === index$1.distExports.AST_NODE_TYPES.ChainExpression) {
442
+ return expressionStartsWithIdentifier(node.expression, identifier);
443
+ }
444
+ return false;
445
+ }
446
+ function getExpressionFromStatement(statement) {
447
+ if (statement.type !== index$1.distExports.AST_NODE_TYPES.ExpressionStatement) {
448
+ return null;
449
+ }
450
+ let { expression } = statement;
451
+ if (expression.type === index$1.distExports.AST_NODE_TYPES.AwaitExpression) {
452
+ expression = expression.argument;
453
+ }
454
+ return expression;
455
+ }
421
456
  function getFunctionName(testName) {
422
457
  const parts = testName.split(".");
423
458
  return parts.slice(0, 2).join(".");
@@ -431,6 +466,20 @@ function getSpecificName(testName) {
431
466
  }
432
467
  return parts.slice(specialIndex + 1).join(".");
433
468
  }
469
+ function getTestCallbackBlockBody(node) {
470
+ const callback = node.arguments[1];
471
+ if (!callback) {
472
+ return null;
473
+ }
474
+ if (callback.type !== index$1.distExports.AST_NODE_TYPES.FunctionExpression && callback.type !== index$1.distExports.AST_NODE_TYPES.ArrowFunctionExpression) {
475
+ return null;
476
+ }
477
+ const { body } = callback;
478
+ if (body.type !== index$1.distExports.AST_NODE_TYPES.BlockStatement) {
479
+ return null;
480
+ }
481
+ return body;
482
+ }
434
483
  function getTestName(node) {
435
484
  const firstArgument = node.arguments[0];
436
485
  if (firstArgument?.type === index$1.distExports.AST_NODE_TYPES.Literal && typeof firstArgument.value === "string") {
@@ -451,12 +500,78 @@ function getTestPriority(testName) {
451
500
  return 1;
452
501
  }
453
502
  }
454
- const rule$3 = {
503
+ function hasAssertMethodCall(expression) {
504
+ if (expression.type !== index$1.distExports.AST_NODE_TYPES.CallExpression) {
505
+ return false;
506
+ }
507
+ const { callee } = expression;
508
+ if (callee.type === index$1.distExports.AST_NODE_TYPES.MemberExpression && callee.property.type === index$1.distExports.AST_NODE_TYPES.Identifier && callee.property.name.startsWith("assert")) {
509
+ if (callee.object.type === index$1.distExports.AST_NODE_TYPES.Identifier && callee.object.name === "expect") {
510
+ return false;
511
+ }
512
+ return true;
513
+ }
514
+ return false;
515
+ }
516
+ function isAssertionCountExpression(expression) {
517
+ if (expression.type !== index$1.distExports.AST_NODE_TYPES.CallExpression) {
518
+ return false;
519
+ }
520
+ const { callee } = expression;
521
+ if (callee.type !== index$1.distExports.AST_NODE_TYPES.MemberExpression || callee.object.type !== index$1.distExports.AST_NODE_TYPES.Identifier || callee.property.type !== index$1.distExports.AST_NODE_TYPES.Identifier) {
522
+ return false;
523
+ }
524
+ if (callee.object.name === "expect" && (callee.property.name === "assertions" || callee.property.name === "hasAssertions")) {
525
+ return true;
526
+ }
527
+ return callee.object.name === "assert" && callee.property.name === "plan";
528
+ }
529
+ function isAssertionCountStatement(statement) {
530
+ const expression = getExpressionFromStatement(statement);
531
+ if (!expression) {
532
+ return false;
533
+ }
534
+ return isAssertionCountExpression(expression);
535
+ }
536
+ function isAssertionExpression(expression) {
537
+ if (isAssertionCountExpression(expression)) {
538
+ return false;
539
+ }
540
+ if (expressionStartsWithIdentifier(expression, "assert") || expressionStartsWithIdentifier(expression, "expect")) {
541
+ return true;
542
+ }
543
+ return hasAssertMethodCall(expression);
544
+ }
545
+ function isAssertionStatement(statement) {
546
+ const expression = getExpressionFromStatement(statement);
547
+ if (!expression) {
548
+ return false;
549
+ }
550
+ return isAssertionExpression(expression);
551
+ }
552
+ function isTeardownStatement(statement) {
553
+ const expression = getExpressionFromStatement(statement);
554
+ if (!expression || expression.type !== index$1.distExports.AST_NODE_TYPES.CallExpression) {
555
+ return false;
556
+ }
557
+ const { callee } = expression;
558
+ if (callee.type === index$1.distExports.AST_NODE_TYPES.Identifier && callee.name === "cleanup") {
559
+ return true;
560
+ }
561
+ if (callee.type === index$1.distExports.AST_NODE_TYPES.MemberExpression && callee.object.type === index$1.distExports.AST_NODE_TYPES.Identifier && callee.object.name === "vi" && callee.property.type === index$1.distExports.AST_NODE_TYPES.Identifier && teardownViMethods.has(callee.property.name)) {
562
+ return true;
563
+ }
564
+ return false;
565
+ }
566
+ const rule$4 = {
455
567
  defaultOptions: [],
456
568
  meta: {
457
- docs: { description: "Enforce sorted test functions grouped by method with sorted metrics, errors, exceptions and middlewares" },
569
+ docs: { description: "Enforce sorted test functions and setup-before-assertions layout in test bodies" },
458
570
  fixable: "code",
459
- messages: { sortError: "Test functions should be grouped by method with sorted metrics, errors, exceptions and middlewares." },
571
+ messages: {
572
+ setupAfterAssertion: "Setup statements must appear before assertions in test bodies.",
573
+ sortError: "Test functions should be grouped by method with sorted metrics, errors, exceptions and middlewares."
574
+ },
460
575
  schema: [],
461
576
  type: "suggestion"
462
577
  },
@@ -464,6 +579,25 @@ const rule$3 = {
464
579
  if (!context.filename.endsWith(".spec.ts") && !context.filename.endsWith(".test.ts")) {
465
580
  return {};
466
581
  }
582
+ function handleTestBody(node) {
583
+ const callbackBody = getTestCallbackBlockBody(node);
584
+ if (!callbackBody) {
585
+ return;
586
+ }
587
+ const firstAssertionIndex = callbackBody.body.findIndex(isAssertionStatement);
588
+ if (firstAssertionIndex === -1) {
589
+ return;
590
+ }
591
+ for (const statement of callbackBody.body.slice(firstAssertionIndex + 1)) {
592
+ if (isAssertionStatement(statement) || isAssertionCountStatement(statement) || isTeardownStatement(statement)) {
593
+ continue;
594
+ }
595
+ context.report({
596
+ messageId: "setupAfterAssertion",
597
+ node: statement
598
+ });
599
+ }
600
+ }
467
601
  function handleTestGroup(node) {
468
602
  const testGroup = node.arguments[0];
469
603
  if (!testGroup || !node.arguments[1]) {
@@ -512,6 +646,8 @@ const rule$3 = {
512
646
  }
513
647
  return {
514
648
  'CallExpression[callee.name="describe"]': handleTestGroup,
649
+ 'CallExpression[callee.name="it"]': handleTestBody,
650
+ 'CallExpression[callee.name="test"]': handleTestBody,
515
651
  'CallExpression[callee.object.name="describe"][callee.property.name="concurrent"]': handleTestGroup,
516
652
  'CallExpression[callee.object.name="test"][callee.property.name="group"]': handleTestGroup
517
653
  };
@@ -668,7 +804,7 @@ function checkUnusedStrings(context, parsed, source, content, contentOffset) {
668
804
  }
669
805
  }
670
806
  }
671
- const rule$2 = {
807
+ const rule$3 = {
672
808
  defaultOptions: [{ locales: defaultLocales }],
673
809
  meta: {
674
810
  docs: { description: "Format Vue i18n blocks: enforce valid/consistent locales, sorted keys, and no unused strings" },
@@ -736,7 +872,7 @@ const rule$2 = {
736
872
  };
737
873
 
738
874
  const untypedEmitsRegex = /const\s+emit\s*=\s*defineEmits\(\[([^\]]+)\]\)/g;
739
- const rule$1 = {
875
+ const rule$2 = {
740
876
  defaultOptions: [],
741
877
  meta: {
742
878
  docs: { description: "Format Vue component scripts: enforce multiline computed properties and typed emits" },
@@ -804,6 +940,42 @@ ${typedEvents}
804
940
  }
805
941
  };
806
942
 
943
+ const styleOpenTagRegex = /<style(?:\s[^>]*)?>/gi;
944
+ const rule$1 = {
945
+ defaultOptions: [],
946
+ meta: {
947
+ docs: { description: "Disallow <style> blocks in Vue SFCs. Styling must be done with Tailwind utility classes as much as possible." },
948
+ messages: { noStyleBlock: "Avoid <style> blocks in Vue components. Use Tailwind utility classes instead." },
949
+ schema: [],
950
+ type: "problem"
951
+ },
952
+ create(context) {
953
+ if (!context.filename.endsWith(".vue")) {
954
+ return {};
955
+ }
956
+ return {
957
+ Program() {
958
+ const sourceCode = context.sourceCode;
959
+ const source = sourceCode.getText();
960
+ styleOpenTagRegex.lastIndex = 0;
961
+ let styleMatch = styleOpenTagRegex.exec(source);
962
+ while (styleMatch !== null) {
963
+ const matchIndex = styleMatch.index;
964
+ const matchEnd = matchIndex + styleMatch[0].length;
965
+ context.report({
966
+ loc: {
967
+ end: sourceCode.getLocFromIndex(matchEnd),
968
+ start: sourceCode.getLocFromIndex(matchIndex)
969
+ },
970
+ messageId: "noStyleBlock"
971
+ });
972
+ styleMatch = styleOpenTagRegex.exec(source);
973
+ }
974
+ }
975
+ };
976
+ }
977
+ };
978
+
807
979
  const templateRegex = /<template>([\s\S]*)<\/template>/i;
808
980
  const templateTagLength = "<template>".length;
809
981
  const propsPrefixRegex = /\$?props\.(\w+)/g;
@@ -1081,10 +1253,11 @@ const rule = {
1081
1253
 
1082
1254
  const index = {
1083
1255
  rules: {
1084
- "ts-format-layout": rule$4,
1085
- "ts-format-tests": rule$3,
1086
- "vue-format-i18n": rule$2,
1087
- "vue-format-script": rule$1,
1256
+ "ts-format-layout": rule$5,
1257
+ "ts-format-tests": rule$4,
1258
+ "vue-format-i18n": rule$3,
1259
+ "vue-format-script": rule$2,
1260
+ "vue-format-style": rule$1,
1088
1261
  "vue-format-template": rule
1089
1262
  }
1090
1263
  };
package/dist/index.d.cts CHANGED
@@ -5198,11 +5198,12 @@ declare const _default: {
5198
5198
  maxLineLength?: number;
5199
5199
  minItems?: number;
5200
5200
  } | undefined)?], unknown, RuleListener>;
5201
- 'ts-format-tests': RuleModule<"sortError", [], unknown, RuleListener>;
5201
+ 'ts-format-tests': RuleModule<"setupAfterAssertion" | "sortError", [], unknown, RuleListener>;
5202
5202
  'vue-format-i18n': RuleModule<"sortError" | "indentError" | "invalidJson" | "invalidLocale" | "missingLocale" | "missingTranslations" | "unusedString", [({
5203
5203
  locales?: string[];
5204
5204
  } | undefined)?], unknown, RuleListener>;
5205
5205
  'vue-format-script': RuleModule<"multilineComputed" | "untypedEmits", [], unknown, RuleListener>;
5206
+ 'vue-format-style': RuleModule<"noStyleBlock", [], unknown, RuleListener>;
5206
5207
  'vue-format-template': RuleModule<"dynamicClassMustBeObject" | "eventHandlerMustBeFunction" | "multilineClassObject" | "noPropsPrefix" | "prefixEventHandlerWithOn" | "removeTrueAttribute" | "singleLineClassObject" | "useT", [], unknown, RuleListener>;
5207
5208
  };
5208
5209
  };
package/dist/index.d.mts CHANGED
@@ -5198,11 +5198,12 @@ declare const _default: {
5198
5198
  maxLineLength?: number;
5199
5199
  minItems?: number;
5200
5200
  } | undefined)?], unknown, RuleListener>;
5201
- 'ts-format-tests': RuleModule<"sortError", [], unknown, RuleListener>;
5201
+ 'ts-format-tests': RuleModule<"setupAfterAssertion" | "sortError", [], unknown, RuleListener>;
5202
5202
  'vue-format-i18n': RuleModule<"sortError" | "indentError" | "invalidJson" | "invalidLocale" | "missingLocale" | "missingTranslations" | "unusedString", [({
5203
5203
  locales?: string[];
5204
5204
  } | undefined)?], unknown, RuleListener>;
5205
5205
  'vue-format-script': RuleModule<"multilineComputed" | "untypedEmits", [], unknown, RuleListener>;
5206
+ 'vue-format-style': RuleModule<"noStyleBlock", [], unknown, RuleListener>;
5206
5207
  'vue-format-template': RuleModule<"dynamicClassMustBeObject" | "eventHandlerMustBeFunction" | "multilineClassObject" | "noPropsPrefix" | "prefixEventHandlerWithOn" | "removeTrueAttribute" | "singleLineClassObject" | "useT", [], unknown, RuleListener>;
5207
5208
  };
5208
5209
  };
package/dist/index.d.ts CHANGED
@@ -5198,11 +5198,12 @@ declare const _default: {
5198
5198
  maxLineLength?: number;
5199
5199
  minItems?: number;
5200
5200
  } | undefined)?], unknown, RuleListener>;
5201
- 'ts-format-tests': RuleModule<"sortError", [], unknown, RuleListener>;
5201
+ 'ts-format-tests': RuleModule<"setupAfterAssertion" | "sortError", [], unknown, RuleListener>;
5202
5202
  'vue-format-i18n': RuleModule<"sortError" | "indentError" | "invalidJson" | "invalidLocale" | "missingLocale" | "missingTranslations" | "unusedString", [({
5203
5203
  locales?: string[];
5204
5204
  } | undefined)?], unknown, RuleListener>;
5205
5205
  'vue-format-script': RuleModule<"multilineComputed" | "untypedEmits", [], unknown, RuleListener>;
5206
+ 'vue-format-style': RuleModule<"noStyleBlock", [], unknown, RuleListener>;
5206
5207
  'vue-format-template': RuleModule<"dynamicClassMustBeObject" | "eventHandlerMustBeFunction" | "multilineClassObject" | "noPropsPrefix" | "prefixEventHandlerWithOn" | "removeTrueAttribute" | "singleLineClassObject" | "useT", [], unknown, RuleListener>;
5207
5208
  };
5208
5209
  };
package/dist/index.mjs CHANGED
@@ -61,7 +61,7 @@ function checkUnion(context, node, maxLineLength, minItems) {
61
61
  function collapseBlankLine(text) {
62
62
  return text.replace(/\n[^\S\n]*\n[^\S\n]*/, "\n");
63
63
  }
64
- function expressionStartsWithIdentifier(node, identifier) {
64
+ function expressionStartsWithIdentifier$1(node, identifier) {
65
65
  if (node.type === distExports.AST_NODE_TYPES.Identifier && node.name === identifier) {
66
66
  return true;
67
67
  }
@@ -71,17 +71,17 @@ function expressionStartsWithIdentifier(node, identifier) {
71
71
  return true;
72
72
  }
73
73
  if (callee.type === distExports.AST_NODE_TYPES.MemberExpression) {
74
- return expressionStartsWithIdentifier(callee.object, identifier);
74
+ return expressionStartsWithIdentifier$1(callee.object, identifier);
75
75
  }
76
76
  if (callee.type === distExports.AST_NODE_TYPES.ChainExpression) {
77
- return expressionStartsWithIdentifier(callee.expression, identifier);
77
+ return expressionStartsWithIdentifier$1(callee.expression, identifier);
78
78
  }
79
79
  }
80
80
  if (node.type === distExports.AST_NODE_TYPES.MemberExpression) {
81
- return expressionStartsWithIdentifier(node.object, identifier);
81
+ return expressionStartsWithIdentifier$1(node.object, identifier);
82
82
  }
83
83
  if (node.type === distExports.AST_NODE_TYPES.ChainExpression) {
84
- return expressionStartsWithIdentifier(node.expression, identifier);
84
+ return expressionStartsWithIdentifier$1(node.expression, identifier);
85
85
  }
86
86
  return false;
87
87
  }
@@ -209,10 +209,10 @@ function hasLineCommentBetween(prevNode, nextNode, sourceCode) {
209
209
  });
210
210
  }
211
211
  function isAssertExpression(expression) {
212
- return expressionStartsWithIdentifier(expression, "assert");
212
+ return expressionStartsWithIdentifier$1(expression, "assert");
213
213
  }
214
214
  function isConsoleExpression(expression) {
215
- return expressionStartsWithIdentifier(expression, "console");
215
+ return expressionStartsWithIdentifier$1(expression, "console");
216
216
  }
217
217
  function isDirectivePrologue(node) {
218
218
  if (node.expression.type !== distExports.AST_NODE_TYPES.Literal) {
@@ -221,7 +221,7 @@ function isDirectivePrologue(node) {
221
221
  return typeof node.expression.value === "string";
222
222
  }
223
223
  function isExpectExpression(expression) {
224
- return expressionStartsWithIdentifier(expression, "expect");
224
+ return expressionStartsWithIdentifier$1(expression, "expect");
225
225
  }
226
226
  function isOwnLineComment(comment, sourceCode) {
227
227
  if (comment.type !== "Line") {
@@ -251,7 +251,7 @@ function isThisReceiver(node) {
251
251
  function isVoidExpression(expression) {
252
252
  return expression.type === distExports.AST_NODE_TYPES.UnaryExpression && expression.operator === "void";
253
253
  }
254
- const rule$4 = {
254
+ const rule$5 = {
255
255
  defaultOptions: [{}],
256
256
  meta: {
257
257
  docs: { description: "Format TypeScript layout: single/multiline ternaries and union types, and blank lines between statements" },
@@ -401,6 +401,7 @@ const rule$4 = {
401
401
  }
402
402
  };
403
403
 
404
+ const teardownViMethods = /* @__PURE__ */ new Set(["restoreAllMocks", "unstubAllGlobals", "useRealTimers"]);
404
405
  function compareTests(testA, testB) {
405
406
  const functionA = getFunctionName(testA);
406
407
  const functionB = getFunctionName(testB);
@@ -416,6 +417,40 @@ function compareTests(testA, testB) {
416
417
  const specificNameB = getSpecificName(testB);
417
418
  return specificNameA.localeCompare(specificNameB);
418
419
  }
420
+ function expressionStartsWithIdentifier(node, identifier) {
421
+ if (node.type === distExports.AST_NODE_TYPES.Identifier && node.name === identifier) {
422
+ return true;
423
+ }
424
+ if (node.type === distExports.AST_NODE_TYPES.CallExpression) {
425
+ const { callee } = node;
426
+ if (callee.type === distExports.AST_NODE_TYPES.Identifier && callee.name === identifier) {
427
+ return true;
428
+ }
429
+ if (callee.type === distExports.AST_NODE_TYPES.MemberExpression) {
430
+ return expressionStartsWithIdentifier(callee.object, identifier);
431
+ }
432
+ if (callee.type === distExports.AST_NODE_TYPES.ChainExpression) {
433
+ return expressionStartsWithIdentifier(callee.expression, identifier);
434
+ }
435
+ }
436
+ if (node.type === distExports.AST_NODE_TYPES.MemberExpression) {
437
+ return expressionStartsWithIdentifier(node.object, identifier);
438
+ }
439
+ if (node.type === distExports.AST_NODE_TYPES.ChainExpression) {
440
+ return expressionStartsWithIdentifier(node.expression, identifier);
441
+ }
442
+ return false;
443
+ }
444
+ function getExpressionFromStatement(statement) {
445
+ if (statement.type !== distExports.AST_NODE_TYPES.ExpressionStatement) {
446
+ return null;
447
+ }
448
+ let { expression } = statement;
449
+ if (expression.type === distExports.AST_NODE_TYPES.AwaitExpression) {
450
+ expression = expression.argument;
451
+ }
452
+ return expression;
453
+ }
419
454
  function getFunctionName(testName) {
420
455
  const parts = testName.split(".");
421
456
  return parts.slice(0, 2).join(".");
@@ -429,6 +464,20 @@ function getSpecificName(testName) {
429
464
  }
430
465
  return parts.slice(specialIndex + 1).join(".");
431
466
  }
467
+ function getTestCallbackBlockBody(node) {
468
+ const callback = node.arguments[1];
469
+ if (!callback) {
470
+ return null;
471
+ }
472
+ if (callback.type !== distExports.AST_NODE_TYPES.FunctionExpression && callback.type !== distExports.AST_NODE_TYPES.ArrowFunctionExpression) {
473
+ return null;
474
+ }
475
+ const { body } = callback;
476
+ if (body.type !== distExports.AST_NODE_TYPES.BlockStatement) {
477
+ return null;
478
+ }
479
+ return body;
480
+ }
432
481
  function getTestName(node) {
433
482
  const firstArgument = node.arguments[0];
434
483
  if (firstArgument?.type === distExports.AST_NODE_TYPES.Literal && typeof firstArgument.value === "string") {
@@ -449,12 +498,78 @@ function getTestPriority(testName) {
449
498
  return 1;
450
499
  }
451
500
  }
452
- const rule$3 = {
501
+ function hasAssertMethodCall(expression) {
502
+ if (expression.type !== distExports.AST_NODE_TYPES.CallExpression) {
503
+ return false;
504
+ }
505
+ const { callee } = expression;
506
+ if (callee.type === distExports.AST_NODE_TYPES.MemberExpression && callee.property.type === distExports.AST_NODE_TYPES.Identifier && callee.property.name.startsWith("assert")) {
507
+ if (callee.object.type === distExports.AST_NODE_TYPES.Identifier && callee.object.name === "expect") {
508
+ return false;
509
+ }
510
+ return true;
511
+ }
512
+ return false;
513
+ }
514
+ function isAssertionCountExpression(expression) {
515
+ if (expression.type !== distExports.AST_NODE_TYPES.CallExpression) {
516
+ return false;
517
+ }
518
+ const { callee } = expression;
519
+ if (callee.type !== distExports.AST_NODE_TYPES.MemberExpression || callee.object.type !== distExports.AST_NODE_TYPES.Identifier || callee.property.type !== distExports.AST_NODE_TYPES.Identifier) {
520
+ return false;
521
+ }
522
+ if (callee.object.name === "expect" && (callee.property.name === "assertions" || callee.property.name === "hasAssertions")) {
523
+ return true;
524
+ }
525
+ return callee.object.name === "assert" && callee.property.name === "plan";
526
+ }
527
+ function isAssertionCountStatement(statement) {
528
+ const expression = getExpressionFromStatement(statement);
529
+ if (!expression) {
530
+ return false;
531
+ }
532
+ return isAssertionCountExpression(expression);
533
+ }
534
+ function isAssertionExpression(expression) {
535
+ if (isAssertionCountExpression(expression)) {
536
+ return false;
537
+ }
538
+ if (expressionStartsWithIdentifier(expression, "assert") || expressionStartsWithIdentifier(expression, "expect")) {
539
+ return true;
540
+ }
541
+ return hasAssertMethodCall(expression);
542
+ }
543
+ function isAssertionStatement(statement) {
544
+ const expression = getExpressionFromStatement(statement);
545
+ if (!expression) {
546
+ return false;
547
+ }
548
+ return isAssertionExpression(expression);
549
+ }
550
+ function isTeardownStatement(statement) {
551
+ const expression = getExpressionFromStatement(statement);
552
+ if (!expression || expression.type !== distExports.AST_NODE_TYPES.CallExpression) {
553
+ return false;
554
+ }
555
+ const { callee } = expression;
556
+ if (callee.type === distExports.AST_NODE_TYPES.Identifier && callee.name === "cleanup") {
557
+ return true;
558
+ }
559
+ if (callee.type === distExports.AST_NODE_TYPES.MemberExpression && callee.object.type === distExports.AST_NODE_TYPES.Identifier && callee.object.name === "vi" && callee.property.type === distExports.AST_NODE_TYPES.Identifier && teardownViMethods.has(callee.property.name)) {
560
+ return true;
561
+ }
562
+ return false;
563
+ }
564
+ const rule$4 = {
453
565
  defaultOptions: [],
454
566
  meta: {
455
- docs: { description: "Enforce sorted test functions grouped by method with sorted metrics, errors, exceptions and middlewares" },
567
+ docs: { description: "Enforce sorted test functions and setup-before-assertions layout in test bodies" },
456
568
  fixable: "code",
457
- messages: { sortError: "Test functions should be grouped by method with sorted metrics, errors, exceptions and middlewares." },
569
+ messages: {
570
+ setupAfterAssertion: "Setup statements must appear before assertions in test bodies.",
571
+ sortError: "Test functions should be grouped by method with sorted metrics, errors, exceptions and middlewares."
572
+ },
458
573
  schema: [],
459
574
  type: "suggestion"
460
575
  },
@@ -462,6 +577,25 @@ const rule$3 = {
462
577
  if (!context.filename.endsWith(".spec.ts") && !context.filename.endsWith(".test.ts")) {
463
578
  return {};
464
579
  }
580
+ function handleTestBody(node) {
581
+ const callbackBody = getTestCallbackBlockBody(node);
582
+ if (!callbackBody) {
583
+ return;
584
+ }
585
+ const firstAssertionIndex = callbackBody.body.findIndex(isAssertionStatement);
586
+ if (firstAssertionIndex === -1) {
587
+ return;
588
+ }
589
+ for (const statement of callbackBody.body.slice(firstAssertionIndex + 1)) {
590
+ if (isAssertionStatement(statement) || isAssertionCountStatement(statement) || isTeardownStatement(statement)) {
591
+ continue;
592
+ }
593
+ context.report({
594
+ messageId: "setupAfterAssertion",
595
+ node: statement
596
+ });
597
+ }
598
+ }
465
599
  function handleTestGroup(node) {
466
600
  const testGroup = node.arguments[0];
467
601
  if (!testGroup || !node.arguments[1]) {
@@ -510,6 +644,8 @@ const rule$3 = {
510
644
  }
511
645
  return {
512
646
  'CallExpression[callee.name="describe"]': handleTestGroup,
647
+ 'CallExpression[callee.name="it"]': handleTestBody,
648
+ 'CallExpression[callee.name="test"]': handleTestBody,
513
649
  'CallExpression[callee.object.name="describe"][callee.property.name="concurrent"]': handleTestGroup,
514
650
  'CallExpression[callee.object.name="test"][callee.property.name="group"]': handleTestGroup
515
651
  };
@@ -666,7 +802,7 @@ function checkUnusedStrings(context, parsed, source, content, contentOffset) {
666
802
  }
667
803
  }
668
804
  }
669
- const rule$2 = {
805
+ const rule$3 = {
670
806
  defaultOptions: [{ locales: defaultLocales }],
671
807
  meta: {
672
808
  docs: { description: "Format Vue i18n blocks: enforce valid/consistent locales, sorted keys, and no unused strings" },
@@ -734,7 +870,7 @@ const rule$2 = {
734
870
  };
735
871
 
736
872
  const untypedEmitsRegex = /const\s+emit\s*=\s*defineEmits\(\[([^\]]+)\]\)/g;
737
- const rule$1 = {
873
+ const rule$2 = {
738
874
  defaultOptions: [],
739
875
  meta: {
740
876
  docs: { description: "Format Vue component scripts: enforce multiline computed properties and typed emits" },
@@ -802,6 +938,42 @@ ${typedEvents}
802
938
  }
803
939
  };
804
940
 
941
+ const styleOpenTagRegex = /<style(?:\s[^>]*)?>/gi;
942
+ const rule$1 = {
943
+ defaultOptions: [],
944
+ meta: {
945
+ docs: { description: "Disallow <style> blocks in Vue SFCs. Styling must be done with Tailwind utility classes as much as possible." },
946
+ messages: { noStyleBlock: "Avoid <style> blocks in Vue components. Use Tailwind utility classes instead." },
947
+ schema: [],
948
+ type: "problem"
949
+ },
950
+ create(context) {
951
+ if (!context.filename.endsWith(".vue")) {
952
+ return {};
953
+ }
954
+ return {
955
+ Program() {
956
+ const sourceCode = context.sourceCode;
957
+ const source = sourceCode.getText();
958
+ styleOpenTagRegex.lastIndex = 0;
959
+ let styleMatch = styleOpenTagRegex.exec(source);
960
+ while (styleMatch !== null) {
961
+ const matchIndex = styleMatch.index;
962
+ const matchEnd = matchIndex + styleMatch[0].length;
963
+ context.report({
964
+ loc: {
965
+ end: sourceCode.getLocFromIndex(matchEnd),
966
+ start: sourceCode.getLocFromIndex(matchIndex)
967
+ },
968
+ messageId: "noStyleBlock"
969
+ });
970
+ styleMatch = styleOpenTagRegex.exec(source);
971
+ }
972
+ }
973
+ };
974
+ }
975
+ };
976
+
805
977
  const templateRegex = /<template>([\s\S]*)<\/template>/i;
806
978
  const templateTagLength = "<template>".length;
807
979
  const propsPrefixRegex = /\$?props\.(\w+)/g;
@@ -1079,10 +1251,11 @@ const rule = {
1079
1251
 
1080
1252
  const index = {
1081
1253
  rules: {
1082
- "ts-format-layout": rule$4,
1083
- "ts-format-tests": rule$3,
1084
- "vue-format-i18n": rule$2,
1085
- "vue-format-script": rule$1,
1254
+ "ts-format-layout": rule$5,
1255
+ "ts-format-tests": rule$4,
1256
+ "vue-format-i18n": rule$3,
1257
+ "vue-format-script": rule$2,
1258
+ "vue-format-style": rule$1,
1086
1259
  "vue-format-template": rule
1087
1260
  }
1088
1261
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@saasmakers/eslint",
3
- "version": "1.0.26",
3
+ "version": "1.0.28",
4
4
  "private": false,
5
5
  "description": "Shared ESLint config and rules for SaaS Makers projects",
6
6
  "license": "MIT",