@fabdeh/eslint-config 0.9.0 → 0.10.0

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.
Files changed (4) hide show
  1. package/README.md +115 -60
  2. package/dist/index.d.mts +1748 -449
  3. package/dist/index.mjs +327 -286
  4. package/package.json +59 -79
package/dist/index.mjs CHANGED
@@ -1,11 +1,12 @@
1
1
  import tseslint from "typescript-eslint";
2
2
  import { statSync } from "node:fs";
3
- import { dirname, join } from "node:path";
3
+ import { readFile } from "node:fs/promises";
4
+ import { dirname, join, normalize, resolve } from "node:path";
4
5
  import process from "node:process";
6
+ import { glob } from "glob";
5
7
  import { isPackageExists } from "local-pkg";
6
8
  import eslintComments from "@eslint-community/eslint-plugin-eslint-comments";
7
9
  import * as importX from "eslint-plugin-import-x";
8
- import eslint from "@eslint/js";
9
10
  import preferArrowFunctions from "eslint-plugin-prefer-arrow-functions";
10
11
  import unusedImports from "eslint-plugin-unused-imports";
11
12
  import globals from "globals";
@@ -15,16 +16,10 @@ import nodePlugin from "eslint-plugin-n";
15
16
  import perfectionistPlugin from "eslint-plugin-perfectionist";
16
17
  import { configs } from "eslint-plugin-regexp";
17
18
  import unicornPlugin from "eslint-plugin-unicorn";
18
-
19
- //#region src/shared/globs.ts
20
- /**
21
- * A glob pattern that matches the extension of JavaScript and TypeScript source files.
22
- */
23
- const GLOB_SRC_EXT = "?([cm])[jt]s?(x)";
24
19
  /**
25
20
  * A glob pattern that matches JavaScript and TypeScript source files (including JSX/TSX).
26
21
  */
27
- const GLOB_SRC = `**/*.${GLOB_SRC_EXT}`;
22
+ const GLOB_SRC = `**/*.?([cm])[jt]s?(x)`;
28
23
  /**
29
24
  * A glob pattern that matches JavaScript source files (including JSX).
30
25
  */
@@ -38,22 +33,6 @@ const GLOB_TS_EXT = "?([cm])ts?(x)";
38
33
  */
39
34
  const GLOB_TS = `**/*.${GLOB_TS_EXT}`;
40
35
  /**
41
- * A glob pattern that matches all CSS files.
42
- */
43
- const GLOB_CSS = "**/*.css";
44
- /**
45
- * A glob pattern that matches all PostCSS files.
46
- */
47
- const GLOB_POSTCSS = "**/*.{p,post}css";
48
- /**
49
- * A glob pattern that matches all LESS files.
50
- */
51
- const GLOB_LESS = "**/*.less";
52
- /**
53
- * A glob pattern that matches all SCSS files.
54
- */
55
- const GLOB_SCSS = "**/*.scss";
56
- /**
57
36
  * A glob pattern that matches all JSON files.
58
37
  */
59
38
  const GLOB_JSON = "**/*.json";
@@ -86,29 +65,14 @@ const GLOB_YAML = "**/*.y?(a)ml";
86
65
  */
87
66
  const GLOB_TOML = "**/*.toml";
88
67
  /**
89
- * A glob pattern that matches XML files.
90
- */
91
- const GLOB_XML = "**/*.xml";
92
- /**
93
- * A glob pattern that matches SVG files.
94
- */
95
- const GLOB_SVG = "**/*.svg";
96
- /**
97
- * A glob pattern that matches GraphQL files.
98
- */
99
- const GLOB_GRAPHQL = "**/*.{g,graph}ql";
100
- /**
101
68
  * A glob pattern that matches HTML files.
102
69
  */
103
70
  const GLOB_HTML = "**/*.htm?(l)";
104
71
  /**
105
72
  * A glob pattern that matches test files.
106
73
  */
107
- const GLOB_TESTS = [
108
- `**/*.spec.?([cm])[jt]s`,
109
- `**/*.test.?([cm])[jt]s`,
110
- `**/test-setup.?([cm])[jt]s`
111
- ];
74
+ const GLOB_TESTS = [`**/*.spec.?([cm])[jt]s`, `**/*.test.?([cm])[jt]s`];
75
+ const GLOB_VITEST = [...GLOB_TESTS, `**/test-setup.?([cm])[jt]s`];
112
76
  /**
113
77
  * A glob pattern that matches files to exclude from linting.
114
78
  */
@@ -118,7 +82,7 @@ const GLOB_EXCLUDE = [
118
82
  "**/package-lock.json",
119
83
  "**/yarn.lock",
120
84
  "**/pnpm-lock.yaml",
121
- "**/bun.lockb",
85
+ "**/bun.lock?(b)",
122
86
  "**/output",
123
87
  "**/coverage",
124
88
  "**/temp",
@@ -144,10 +108,8 @@ const GLOB_EXCLUDE = [
144
108
  "**/LICENSE*",
145
109
  "**/__snapshots__",
146
110
  "**/auto-import?(s).d.ts",
147
- "**/components.d.ts",
148
- "**/prettier-types.ts"
111
+ "**/components.d.ts"
149
112
  ];
150
-
151
113
  //#endregion
152
114
  //#region src/shared/utils.ts
153
115
  const SCOPE_URL = import.meta.dirname;
@@ -180,6 +142,19 @@ function fileExists(path) {
180
142
  }
181
143
  }
182
144
  /**
145
+ * Checks whether the given path points to an existing directory.
146
+ *
147
+ * @param path - Filesystem path to validate.
148
+ * @returns `true` when the path exists and is a directory; otherwise `false`.
149
+ */
150
+ function isDirectory(path) {
151
+ try {
152
+ return statSync(path).isDirectory();
153
+ } catch {
154
+ return false;
155
+ }
156
+ }
157
+ /**
183
158
  * Retrieves the root directory of the workspace.
184
159
  *
185
160
  * This function determines the root directory of the workspace by checking for specific files
@@ -244,7 +219,7 @@ async function ensurePackages(packages) {
244
219
  /**
245
220
  * Returns the object representation of the configuration or an empty object if it is a boolean.
246
221
  *
247
- * @param options - The {@link CreateConfigOptions} object to extract the sub-options from.
222
+ * @param options - The {@link DefineConfigOptions} object to extract the sub-options from.
248
223
  * @param key - The property name.
249
224
  * @returns An object representing the required options.
250
225
  */
@@ -253,7 +228,67 @@ function resolveSubOptions(options, key) {
253
228
  if (typeof option === "boolean") return {};
254
229
  return option ?? {};
255
230
  }
256
-
231
+ /**
232
+ * Replace Windows with posix style paths
233
+ *
234
+ * @param filePath - Path to convert
235
+ * @returns Converted filepath
236
+ */
237
+ function convertPathToPosix(filePath) {
238
+ return normalize(filePath).replaceAll("\\", "/");
239
+ }
240
+ /**
241
+ * Checks if a provided path is a directory and returns a glob string matching
242
+ * all files under that directory if so, the path itself otherwise.
243
+ *
244
+ * Reason for this is that `glob` needs `/**` to collect all the files under a
245
+ * directory where as our previous implementation without `glob` simply walked
246
+ * a directory that is passed. So this is to maintain backwards compatibility.
247
+ *
248
+ * Also makes sure all path separators are POSIX style for `glob` compatibility.
249
+ *
250
+ * @param [options] - An options object
251
+ * @param [options.extensions] - An array of accepted extensions
252
+ * @param [options.cwd] - The cwd to use to resolve relative pathnames
253
+ * @returns A function that takes a pathname and returns a glob that
254
+ * matches all files with the provided extensions if
255
+ * pathname is a directory.
256
+ */
257
+ function pathToGlobPattern(options) {
258
+ const cwd = options?.cwd ?? process.cwd();
259
+ const extensions = options?.extensions?.map((ext) => ext.replace(/^\./, "")) ?? [];
260
+ const suffix = extensions.length === 0 ? "/**/*" : extensions.length === 1 ? `/**/*.${extensions[0]}` : `/**/*.{${extensions.join(",")}}`;
261
+ return (path) => {
262
+ let newPath = path;
263
+ if (isDirectory(resolve(cwd, path))) newPath = path.replace(/[/\\]$/, "") + suffix;
264
+ return convertPathToPosix(newPath);
265
+ };
266
+ }
267
+ /**
268
+ * Finds the first valid Playwright test directory declared via `testDir` in any
269
+ * `playwright.config.ts` file under the current workspace.
270
+ *
271
+ * The function:
272
+ * - Locates Playwright config files, excluding `node_modules`.
273
+ * - Reads each config as text.
274
+ * - Extracts `testDir` with a regex.
275
+ * - Resolves `testDir` relative to the config file location.
276
+ * - Returns the first resolved path that exists as a directory.
277
+ *
278
+ * @returns Absolute path to the first existing Playwright test directory, or `undefined` if none is found.
279
+ */
280
+ async function getPlaywrightDirectory() {
281
+ const playwrightConfigs = await glob("**/playwright.config.ts", { ignore: ["**/node_modules/**"] });
282
+ for (const config of playwrightConfigs) try {
283
+ const configContent = await readFile(config, "utf-8");
284
+ const match = /testDir:\s*["'](.+)["'],?/.exec(configContent);
285
+ if (match) {
286
+ const testDir = match[1];
287
+ const resolvedTestDir = resolve(dirname(config), testDir);
288
+ if (isDirectory(resolvedTestDir)) return resolvedTestDir;
289
+ }
290
+ } catch {}
291
+ }
257
292
  //#endregion
258
293
  //#region src/configs/angular.ts
259
294
  /**
@@ -359,7 +394,6 @@ async function angular(options = {}) {
359
394
  }
360
395
  });
361
396
  }
362
-
363
397
  //#endregion
364
398
  //#region src/configs/comments.ts
365
399
  /**
@@ -386,198 +420,6 @@ function comments() {
386
420
  }
387
421
  });
388
422
  }
389
-
390
- //#endregion
391
- //#region src/configs/stylistic.ts
392
- const STYLISTIC_CONFIG_DEFAULT = {
393
- semi: true,
394
- arrowParens: true,
395
- braceStyle: "1tbs",
396
- quoteProps: "as-needed"
397
- };
398
- /**
399
- * Generates a stylistic ESLint configuration.
400
- *
401
- * @param [options] - Optional configuration options to customize the stylistic rules.
402
- * @returns A promise that resolves to an array of ESLint configurations.
403
- * @example
404
- * const config = await stylistic({ semi: false });
405
- */
406
- async function stylistic(options = {}) {
407
- const stylisticOptions = {
408
- ...STYLISTIC_CONFIG_DEFAULT,
409
- ...typeof options.stylistic === "boolean" ? {} : options.stylistic
410
- };
411
- const stylisticPlugin = await interopDefault(import("@stylistic/eslint-plugin"));
412
- const config = stylisticPlugin.configs.customize(stylisticOptions);
413
- return tseslint.config({
414
- name: "fabdeh/stylistic/rules",
415
- files: [GLOB_SRC],
416
- plugins: { "@stylistic": stylisticPlugin },
417
- rules: {
418
- ...config.rules,
419
- "@stylistic/comma-dangle": ["error", {
420
- arrays: "always-multiline",
421
- objects: "always-multiline"
422
- }],
423
- "@stylistic/no-extra-semi": "error",
424
- "@stylistic/operator-linebreak": [
425
- "error",
426
- "after",
427
- { overrides: {
428
- "?": "before",
429
- ":": "before"
430
- } }
431
- ]
432
- }
433
- });
434
- }
435
-
436
- //#endregion
437
- //#region src/configs/formatters.ts
438
- /**
439
- * Merges the provided Prettier options with any overrides.
440
- *
441
- * @param options - The base Prettier options to merge.
442
- * @param overrides - Optional overrides for the Prettier options.
443
- * @returns The merged Prettier options.
444
- */
445
- function mergePrettierOptions(options, overrides = {}) {
446
- return {
447
- ...options,
448
- ...overrides,
449
- plugins: [...overrides.plugins ?? [], ...options.plugins ?? []]
450
- };
451
- }
452
- /**
453
- * Configures and returns an array of formatters based on the provided options.
454
- *
455
- * @param options - The options for configuring the formatters. If set to `true`, default options will be used.
456
- * @param stylistic - The stylistic options for the formatters.
457
- * @param hasAngularTemplateParser - Optional argument which indicates whether angular rules already registered the template parser or not.
458
- * @returns A promise that resolves to a `TypedConfigArray` containing the configured formatters.
459
- */
460
- async function formatters(options = {}, stylistic = {}, hasAngularTemplateParser = false) {
461
- if (options === true) {
462
- const isPrettierPluginXmlInScope = isPackageInScope("@prettier/plugin-xml");
463
- options = {
464
- css: true,
465
- graphql: true,
466
- html: true,
467
- markdown: true,
468
- slidev: isPackageExists("@slidev/cli"),
469
- svg: isPrettierPluginXmlInScope,
470
- xml: isPrettierPluginXmlInScope
471
- };
472
- }
473
- await ensurePackages([
474
- "eslint-plugin-format",
475
- options.markdown && options.slidev ? "prettier-plugin-slidev" : void 0,
476
- options.xml || options.svg ? "@prettier/plugin-xml" : void 0
477
- ]);
478
- const { indent, quotes, semi } = {
479
- ...STYLISTIC_CONFIG_DEFAULT,
480
- ...stylistic
481
- };
482
- const prettierOptions = {
483
- endOfLine: "auto",
484
- printWidth: 120,
485
- semi,
486
- singleQuote: quotes === "single",
487
- tabWidth: typeof indent === "number" ? indent : 2,
488
- trailingComma: "all",
489
- useTabs: indent === "tab",
490
- ...options.options
491
- };
492
- const prettierXmlOptions = {
493
- xmlQuoteAttributes: "double",
494
- xmlSelfClosingSpace: true,
495
- xmlSortAttributesByKey: false,
496
- xmlWhitespaceSensitivity: "ignore"
497
- };
498
- const formatPlugin = await interopDefault(import("eslint-plugin-format"));
499
- const configs = [{
500
- name: "fabdeh/formatter/setup",
501
- plugins: { format: formatPlugin }
502
- }];
503
- if (options.css) configs.push({
504
- name: "fabdeh/formatter/css",
505
- languageOptions: { parser: formatPlugin.parserPlain },
506
- files: [GLOB_CSS, GLOB_POSTCSS],
507
- rules: { "format/prettier": ["error", mergePrettierOptions(prettierOptions, { parser: "css" })] }
508
- }, {
509
- name: "fabdeh/formatter/scss",
510
- languageOptions: { parser: formatPlugin.parserPlain },
511
- files: [GLOB_SCSS],
512
- rules: { "format/prettier": ["error", mergePrettierOptions(prettierOptions, { parser: "scss" })] }
513
- }, {
514
- name: "fabdeh/formatter/less",
515
- languageOptions: { parser: formatPlugin.parserPlain },
516
- files: [GLOB_LESS],
517
- rules: { "format/prettier": ["error", mergePrettierOptions(prettierOptions, { parser: "less" })] }
518
- });
519
- if (options.html) configs.push({
520
- name: "fabdeh/formatter/html",
521
- ...hasAngularTemplateParser ? {} : { languageOptions: { parser: formatPlugin.parserPlain } },
522
- files: [GLOB_HTML],
523
- rules: { "format/prettier": ["error", mergePrettierOptions(prettierOptions, { parser: "html" })] }
524
- });
525
- if (options.xml) configs.push({
526
- name: "fabdeh/formatter/xml",
527
- languageOptions: { parser: formatPlugin.parserPlain },
528
- files: [GLOB_XML],
529
- rules: { "format/prettier": ["error", mergePrettierOptions({
530
- ...prettierXmlOptions,
531
- ...prettierOptions
532
- }, {
533
- parser: "xml",
534
- plugins: ["@prettier/plugin-xml"]
535
- })] }
536
- });
537
- if (options.svg) configs.push({
538
- name: "fabdeh/formatter/svg",
539
- languageOptions: { parser: formatPlugin.parserPlain },
540
- files: [GLOB_SVG],
541
- rules: { "format/prettier": ["error", mergePrettierOptions({
542
- ...prettierXmlOptions,
543
- ...prettierOptions
544
- }, {
545
- parser: "xml",
546
- plugins: ["@prettier/plugin-xml"]
547
- })] }
548
- });
549
- if (options.markdown) {
550
- const GLOB_SLIDEV = options.slidev ? options.slidev === true ? ["**/.slides.md"] : options.slidev.files : [];
551
- configs.push({
552
- name: "fabdeh/formatter/markdown",
553
- languageOptions: { parser: formatPlugin.parserPlain },
554
- files: [GLOB_MARKDOWN],
555
- ignores: GLOB_SLIDEV,
556
- rules: { "format/prettier": ["error", mergePrettierOptions(prettierOptions, {
557
- parser: "markdown",
558
- embeddedLanguageFormatting: "off"
559
- })] }
560
- });
561
- if (options.slidev) configs.push({
562
- name: "fabdeh/formatter/slidev",
563
- languageOptions: { parser: formatPlugin.parserPlain },
564
- files: GLOB_SLIDEV,
565
- rules: { "format/prettier": ["error", mergePrettierOptions(prettierOptions, {
566
- embeddedLanguageFormatting: "off",
567
- parser: "slidev",
568
- plugins: ["prettier-plugin-slidev"]
569
- })] }
570
- });
571
- }
572
- if (options.graphql) configs.push({
573
- name: "fabdeh/formatter/graphql",
574
- languageOptions: { parser: formatPlugin.parserPlain },
575
- files: [GLOB_GRAPHQL],
576
- rules: { "format/prettier": ["error", mergePrettierOptions(prettierOptions, { parser: "graphql" })] }
577
- });
578
- return tseslint.config(configs);
579
- }
580
-
581
423
  //#endregion
582
424
  //#region src/configs/ignore.ts
583
425
  /**
@@ -593,7 +435,6 @@ function ignores(userIgnores = [], fromFactory) {
593
435
  ignores: [...!fromFactory || fromFactory === "workspace" ? GLOB_EXCLUDE : [], ...userIgnores]
594
436
  });
595
437
  }
596
-
597
438
  //#endregion
598
439
  //#region src/configs/imports.ts
599
440
  /**
@@ -641,7 +482,6 @@ function imports(options = {}) {
641
482
  }
642
483
  });
643
484
  }
644
-
645
485
  //#endregion
646
486
  //#region src/configs/javascript.ts
647
487
  /**
@@ -664,7 +504,7 @@ function javascript(options = {}) {
664
504
  return tseslint.config({
665
505
  name: "fabdeh/javascript/setup",
666
506
  languageOptions: {
667
- ecmaVersion: 2022,
507
+ ecmaVersion: "latest",
668
508
  globals: {
669
509
  ...globals.browser,
670
510
  ...globals.es2021,
@@ -675,7 +515,7 @@ function javascript(options = {}) {
675
515
  },
676
516
  parserOptions: {
677
517
  ecmaFeatures: { jsx: true },
678
- ecmaVersion: 2022,
518
+ ecmaVersion: "latest",
679
519
  sourceType: "module"
680
520
  },
681
521
  sourceType: "module"
@@ -690,13 +530,15 @@ function javascript(options = {}) {
690
530
  "unused-imports": unusedImports
691
531
  },
692
532
  rules: {
693
- ...eslint.configs.recommended.rules,
694
533
  "accessor-pairs": "error",
695
534
  "array-callback-return": "error",
696
535
  "block-scoped-var": "error",
536
+ "constructor-super": "error",
697
537
  "default-case-last": "error",
698
538
  "dot-notation": "error",
699
539
  eqeqeq: "error",
540
+ "for-direction": "error",
541
+ "getter-return": "error",
700
542
  "id-denylist": [
701
543
  "error",
702
544
  "any",
@@ -711,48 +553,156 @@ function javascript(options = {}) {
711
553
  ],
712
554
  "new-cap": "error",
713
555
  "no-alert": "error",
556
+ "no-async-promise-executor": "error",
714
557
  "no-caller": "error",
558
+ "no-case-declarations": "error",
559
+ "no-class-assign": "error",
715
560
  "no-cond-assign": ["error", "always"],
561
+ "no-compare-neg-zero": "error",
716
562
  "no-console": ["error", { allow: ["warn", "error"] }],
563
+ "no-const-assign": "error",
564
+ "no-constant-binary-expression": "error",
565
+ "no-constant-condition": "error",
566
+ "no-control-regex": "error",
567
+ "no-debugger": "error",
568
+ "no-delete-var": "error",
569
+ "no-dupe-args": "error",
570
+ "no-dupe-class-members": "error",
571
+ "no-dupe-else-if": "error",
572
+ "no-dupe-keys": "error",
573
+ "no-duplicate-case": "error",
717
574
  "no-empty": ["error", { allowEmptyCatch: true }],
575
+ "no-empty-character-class": "error",
576
+ "no-empty-pattern": "error",
577
+ "no-empty-static-block": "error",
718
578
  "no-eval": "error",
579
+ "no-ex-assign": "error",
719
580
  "no-extend-native": "error",
720
581
  "no-extra-bind": "error",
582
+ "no-extra-boolean-cast": "error",
583
+ "no-fallthrough": "error",
584
+ "no-func-assign": "error",
585
+ "no-global-assign": "error",
586
+ "no-import-assign": "error",
721
587
  "no-implied-eval": "error",
588
+ "no-invalid-regexp": "error",
589
+ "no-irregular-whitespace": "error",
722
590
  "no-iterator": "error",
723
591
  "no-labels": "error",
724
592
  "no-lone-blocks": "error",
725
593
  "no-lonely-if": "error",
594
+ "no-loss-of-precision": "error",
595
+ "no-misleading-character-class": "error",
726
596
  "no-multi-str": "error",
727
597
  "no-new": "error",
728
598
  "no-new-func": "error",
599
+ "no-new-native-nonconstructor": "error",
729
600
  "no-new-wrappers": "error",
601
+ "no-nonoctal-decimal-escape": "error",
602
+ "no-obj-calls": "error",
603
+ "no-octal": "error",
730
604
  "no-octal-escape": "error",
731
605
  "no-plusplus": ["error", { allowForLoopAfterthoughts: true }],
606
+ "no-prototype-builtins": "error",
732
607
  "no-proto": "error",
608
+ "no-redeclare": "error",
609
+ "no-regex-spaces": "error",
610
+ "no-restricted-globals": [
611
+ "error",
612
+ {
613
+ message: "Use `globalThis` instead.",
614
+ name: "global"
615
+ },
616
+ {
617
+ message: "Use `globalThis` instead.",
618
+ name: "self"
619
+ }
620
+ ],
621
+ "no-restricted-properties": [
622
+ "error",
623
+ {
624
+ message: "Use `Object.getPrototypeOf` or `Object.setPrototypeOf` instead.",
625
+ property: "__proto__"
626
+ },
627
+ {
628
+ message: "Use `Object.defineProperty` instead.",
629
+ property: "__defineGetter__"
630
+ },
631
+ {
632
+ message: "Use `Object.defineProperty` instead.",
633
+ property: "__defineSetter__"
634
+ },
635
+ {
636
+ message: "Use `Object.getOwnPropertyDescriptor` instead.",
637
+ property: "__lookupGetter__"
638
+ },
639
+ {
640
+ message: "Use `Object.getOwnPropertyDescriptor` instead.",
641
+ property: "__lookupSetter__"
642
+ }
643
+ ],
644
+ "no-restricted-syntax": [
645
+ "error",
646
+ {
647
+ selector: "TSEnumDeclaration",
648
+ message: "Use `as const` objects instead of enums."
649
+ },
650
+ {
651
+ selector: "TSExportAssignment",
652
+ message: "Avoid `export =`. Use ESM-style exports instead (for example, `export default` or named exports)."
653
+ },
654
+ {
655
+ selector: "ForInStatement",
656
+ message: "Avoid using `for...in` statements. Use `Object.keys()`, `Object.values()`, or `Object.entries()` instead."
657
+ },
658
+ {
659
+ selector: ":matches(PropertyDefinition, MethodDefinition)[accessibility=\"private\"]",
660
+ message: "Use `#private` members instead."
661
+ }
662
+ ],
663
+ "no-self-assign": "error",
733
664
  "no-self-compare": "error",
734
665
  "no-sequences": "error",
666
+ "no-setter-return": "error",
667
+ "no-shadow-restricted-names": "error",
668
+ "no-sparse-arrays": "error",
735
669
  "no-template-curly-in-string": "error",
670
+ "no-this-before-super": "error",
736
671
  "no-throw-literal": "error",
672
+ "no-undef": "error",
737
673
  "no-undef-init": "error",
674
+ "no-unassigned-vars": "error",
675
+ "no-unexpected-multiline": "error",
738
676
  "no-unmodified-loop-condition": "error",
739
677
  "no-unneeded-ternary": ["error", { defaultAssignment: false }],
678
+ "no-unreachable": "error",
740
679
  "no-unreachable-loop": "error",
680
+ "no-unsafe-finally": "error",
681
+ "no-unsafe-negation": "error",
682
+ "no-unsafe-optional-chaining": "error",
741
683
  "no-unused-expressions": ["error", {
742
684
  allowShortCircuit: true,
743
685
  allowTaggedTemplates: true,
744
686
  allowTernary: true
745
687
  }],
688
+ "no-unused-labels": "error",
689
+ "no-unused-private-class-members": "error",
690
+ "no-unused-vars": "off",
746
691
  "no-use-before-define": ["error", {
747
692
  classes: false,
748
693
  functions: false
749
694
  }],
695
+ "no-useless-assignment": "error",
696
+ "no-useless-backreference": "error",
750
697
  "no-useless-call": "error",
698
+ "no-useless-catch": "error",
751
699
  "no-useless-computed-key": "error",
752
700
  "no-useless-constructor": "error",
701
+ "no-useless-escape": "error",
753
702
  "no-useless-rename": "error",
754
703
  "no-useless-return": "error",
755
704
  "no-var": "error",
705
+ "no-with": "error",
756
706
  "object-shorthand": [
757
707
  "error",
758
708
  "always",
@@ -774,10 +724,12 @@ function javascript(options = {}) {
774
724
  "prefer-rest-params": "error",
775
725
  "prefer-spread": "error",
776
726
  "prefer-template": "error",
727
+ "preserve-caught-error": "error",
728
+ "require-yield": "error",
777
729
  "symbol-description": "error",
778
730
  "unicode-bom": ["error", "never"],
731
+ "use-isnan": ["error", { enforceForIndexOf: true }],
779
732
  "unused-imports/no-unused-imports": "error",
780
- "no-unused-vars": "off",
781
733
  "unused-imports/no-unused-vars": ["error", {
782
734
  args: "after-used",
783
735
  argsIgnorePattern: "^_",
@@ -785,7 +737,6 @@ function javascript(options = {}) {
785
737
  vars: "all",
786
738
  varsIgnorePattern: "^_"
787
739
  }],
788
- "use-isnan": ["error", { enforceForIndexOf: true }],
789
740
  "valid-typeof": ["error", { requireStringLiterals: true }],
790
741
  "vars-on-top": "error",
791
742
  yoda: [
@@ -803,7 +754,6 @@ function javascript(options = {}) {
803
754
  }
804
755
  });
805
756
  }
806
-
807
757
  //#endregion
808
758
  //#region src/configs/jsdoc.ts
809
759
  /**
@@ -889,7 +839,6 @@ function jsdoc(options = {}) {
889
839
  rules: getJsDocRules("warn", !!stylistic, "tsOnly")
890
840
  });
891
841
  }
892
-
893
842
  //#endregion
894
843
  //#region src/configs/jsonc.ts
895
844
  /**
@@ -974,7 +923,6 @@ async function jsonc(options = {}) {
974
923
  }
975
924
  });
976
925
  }
977
-
978
926
  //#endregion
979
927
  //#region src/configs/markdown.ts
980
928
  /**
@@ -1070,7 +1018,6 @@ async function markdown(options = {}) {
1070
1018
  }
1071
1019
  });
1072
1020
  }
1073
-
1074
1021
  //#endregion
1075
1022
  //#region src/configs/rules-configs/naming-convention.ts
1076
1023
  /**
@@ -1144,7 +1091,6 @@ function namingConvention(strict, allowJsx = false) {
1144
1091
  }
1145
1092
  ];
1146
1093
  }
1147
-
1148
1094
  //#endregion
1149
1095
  //#region src/configs/ngrx.ts
1150
1096
  const DEFAULT_STORE_GLOB = [
@@ -1292,7 +1238,6 @@ async function ngrx(options = {}) {
1292
1238
  });
1293
1239
  return tseslint.config(configs);
1294
1240
  }
1295
-
1296
1241
  //#endregion
1297
1242
  //#region src/configs/node.ts
1298
1243
  /**
@@ -1320,7 +1265,6 @@ function node() {
1320
1265
  }
1321
1266
  });
1322
1267
  }
1323
-
1324
1268
  //#endregion
1325
1269
  //#region src/configs/rules-configs/perfectionist-groups.ts
1326
1270
  const SORT_IMPORT_GROUPS = [
@@ -1366,7 +1310,6 @@ const SORT_UNION_OR_INTERSECTION_GROUPS = [
1366
1310
  "nullish",
1367
1311
  "unknown"
1368
1312
  ];
1369
-
1370
1313
  //#endregion
1371
1314
  //#region src/configs/perfectionist.ts
1372
1315
  /**
@@ -1426,7 +1369,64 @@ function perfectionist() {
1426
1369
  }
1427
1370
  });
1428
1371
  }
1429
-
1372
+ //#endregion
1373
+ //#region src/configs/playwright.ts
1374
+ /**
1375
+ * Configures and returns an ESLint flat config for Playwright test files.
1376
+ *
1377
+ * @param [options] - Options used to customize the Playwright config.
1378
+ * @param [options.e2eFolderPath] - The path to the e2e folder to prepend to each `options.files` glob pattern (defaults to `e2e`).
1379
+ * @param [options.files] - File globs to target (defaults to `GLOB_TESTS`).
1380
+ * @param [options.overrides] - Custom ESLint rule overrides merged last.
1381
+ * @returns A promise that resolves to a single-entry ESLint config array.
1382
+ * @example
1383
+ * const config = await playwright({
1384
+ * e2eFolderPath: 'tests/e2e',
1385
+ * files: ['**\/*.test.ts'], // will become 'tests\/e2e\/**\/*.test.ts' because of the e2eFolderPath option
1386
+ * overrides: {
1387
+ * 'playwright/no-page-pause': 'off',
1388
+ * },
1389
+ * });
1390
+ */
1391
+ async function playwright(options = {}) {
1392
+ const { e2eFolderPath = "e2e", files = GLOB_TESTS.map((glob) => convertPathToPosix(join(e2eFolderPath, glob))), overrides = {} } = options;
1393
+ if (!isDirectory(e2eFolderPath)) return [];
1394
+ const playwrightPlugin = await interopDefault(import("eslint-plugin-playwright"));
1395
+ return [{
1396
+ name: "fabdeh/playwright/rules",
1397
+ plugins: { playwright: playwrightPlugin },
1398
+ files,
1399
+ rules: {
1400
+ ...playwrightPlugin.configs.recommended.rules,
1401
+ "max-lines": "off",
1402
+ "@typescript-eslint/consistent-type-assertions": "off",
1403
+ "@typescript-eslint/no-empty-function": "off",
1404
+ "@typescript-eslint/no-unsafe-assignment": "off",
1405
+ "@typescript-eslint/no-unsafe-call": "off",
1406
+ "@typescript-eslint/no-unsafe-member-access": "off",
1407
+ "@typescript-eslint/unbound-method": "off",
1408
+ "unicorn/no-null": "off",
1409
+ "playwright/no-get-by-title": "error",
1410
+ "playwright/no-nth-methods": "error",
1411
+ "playwright/prefer-comparison-matcher": "error",
1412
+ "playwright/prefer-equality-matcher": "error",
1413
+ "playwright/prefer-lowercase-title": ["error", { allowedPrefixes: [
1414
+ "GET",
1415
+ "POST",
1416
+ "PUT",
1417
+ "DELETE",
1418
+ "PATCH",
1419
+ "HEAD",
1420
+ "OPTIONS"
1421
+ ] }],
1422
+ "playwright/prefer-native-locators": "error",
1423
+ "playwright/prefer-to-be": "error",
1424
+ "playwright/prefer-to-contain": "error",
1425
+ ...getJsDocRules("off", true, "both"),
1426
+ ...overrides
1427
+ }
1428
+ }];
1429
+ }
1430
1430
  //#endregion
1431
1431
  //#region src/configs/pnpm.ts
1432
1432
  /**
@@ -1461,7 +1461,6 @@ async function pnpm() {
1461
1461
  }
1462
1462
  });
1463
1463
  }
1464
-
1465
1464
  //#endregion
1466
1465
  //#region src/configs/regexp.ts
1467
1466
  /**
@@ -1484,7 +1483,6 @@ function regexp(options = {}) {
1484
1483
  }
1485
1484
  });
1486
1485
  }
1487
-
1488
1486
  //#endregion
1489
1487
  //#region src/configs/sort.ts
1490
1488
  /**
@@ -1721,7 +1719,51 @@ function sortTsConfig() {
1721
1719
  ] }
1722
1720
  });
1723
1721
  }
1724
-
1722
+ //#endregion
1723
+ //#region src/configs/stylistic.ts
1724
+ const STYLISTIC_CONFIG_DEFAULT = {
1725
+ semi: true,
1726
+ arrowParens: true,
1727
+ braceStyle: "1tbs",
1728
+ quoteProps: "as-needed"
1729
+ };
1730
+ /**
1731
+ * Generates a stylistic ESLint configuration.
1732
+ *
1733
+ * @param [options] - Optional configuration options to customize the stylistic rules.
1734
+ * @returns A promise that resolves to an array of ESLint configurations.
1735
+ * @example
1736
+ * const config = await stylistic({ semi: false });
1737
+ */
1738
+ async function stylistic(options = {}) {
1739
+ const stylisticOptions = {
1740
+ ...STYLISTIC_CONFIG_DEFAULT,
1741
+ ...typeof options.stylistic === "boolean" ? {} : options.stylistic
1742
+ };
1743
+ const stylisticPlugin = await interopDefault(import("@stylistic/eslint-plugin"));
1744
+ const config = stylisticPlugin.configs.customize(stylisticOptions);
1745
+ return tseslint.config({
1746
+ name: "fabdeh/stylistic/rules",
1747
+ files: [GLOB_SRC],
1748
+ plugins: { "@stylistic": stylisticPlugin },
1749
+ rules: {
1750
+ ...config.rules,
1751
+ "@stylistic/comma-dangle": ["error", {
1752
+ arrays: "always-multiline",
1753
+ objects: "always-multiline"
1754
+ }],
1755
+ "@stylistic/no-extra-semi": "error",
1756
+ "@stylistic/operator-linebreak": [
1757
+ "error",
1758
+ "after",
1759
+ { overrides: {
1760
+ "?": "before",
1761
+ ":": "before"
1762
+ } }
1763
+ ]
1764
+ }
1765
+ });
1766
+ }
1725
1767
  //#endregion
1726
1768
  //#region src/configs/tailwindcss.ts
1727
1769
  /**
@@ -1751,7 +1793,7 @@ async function tailwindcss(options = {}) {
1751
1793
  languageOptions: { parser }
1752
1794
  }));
1753
1795
  } else {
1754
- files = { files: filesGlob ?? [GLOB_SRC, GLOB_HTML] };
1796
+ files = { files: filesGlob ?? [GLOB_SRC, "**/*.htm?(l)"] };
1755
1797
  parserConfigs = [];
1756
1798
  }
1757
1799
  return tseslint.config(...parserConfigs, {
@@ -1770,7 +1812,6 @@ async function tailwindcss(options = {}) {
1770
1812
  }
1771
1813
  });
1772
1814
  }
1773
-
1774
1815
  //#endregion
1775
1816
  //#region src/configs/toml.ts
1776
1817
  /**
@@ -1821,7 +1862,6 @@ async function toml(options = {}) {
1821
1862
  }
1822
1863
  });
1823
1864
  }
1824
-
1825
1865
  //#endregion
1826
1866
  //#region src/configs/rules-configs/member-ordering.ts
1827
1867
  const MEMBER_ORDERING_OPTIONS = { default: [
@@ -1858,7 +1898,6 @@ const MEMBER_ORDERING_OPTIONS = { default: [
1858
1898
  "private-method",
1859
1899
  "#private-method"
1860
1900
  ] };
1861
-
1862
1901
  //#endregion
1863
1902
  //#region src/configs/typescript.ts
1864
1903
  /**
@@ -1987,7 +2026,6 @@ async function typescript(options = {}, isWorkspaceProject = false) {
1987
2026
  }
1988
2027
  });
1989
2028
  }
1990
-
1991
2029
  //#endregion
1992
2030
  //#region src/configs/unicorn.ts
1993
2031
  /**
@@ -2100,28 +2138,30 @@ function unicorn(options = {}) {
2100
2138
  } }
2101
2139
  });
2102
2140
  }
2103
-
2104
2141
  //#endregion
2105
2142
  //#region src/configs/vitest.ts
2106
2143
  /**
2107
2144
  * Configures and returns an ESLint configuration array for Vitest.
2108
2145
  *
2109
2146
  * @param [options] - The options to customize the configuration.
2147
+ * @param [options.files] - File globs to target (defaults to `GLOB_VITEST`).
2110
2148
  * @param [options.overrides] - Custom rule overrides.
2111
2149
  * @param [options.useJestDom] - Whether to use the `@testing-library/jest-dom` plugin.
2112
2150
  * @param [options.useTestingLibrary] - Whether to use the `@testing-library/angular` plugin.
2151
+ * @param [options.e2eFolderPath] - The path to the e2e folder to ignore (this is only used internally by the `defineConfig` function, not by the user).
2113
2152
  * @returns A promise that resolves to the ESLint configuration array.
2114
2153
  * @example
2115
2154
  * const config = await vitest({
2155
+ * files: ['**\/*.spec.ts'],
2116
2156
  * overrides: {
2117
- * 'no-console': 'warn',
2157
+ * 'vitest/prefer-lowercase-title': 'warn',
2118
2158
  * },
2119
2159
  * useJestDom: true,
2120
2160
  * useTestingLibrary: false,
2121
2161
  * });
2122
2162
  */
2123
2163
  async function vitest(options = {}) {
2124
- const { overrides = {}, useJestDom = isPackageExists("@testing-library/jest-dom"), useTestingLibrary = isPackageExists("@testing-library/angular") } = options;
2164
+ const { files = GLOB_VITEST, overrides = {}, useJestDom = isPackageExists("@testing-library/jest-dom"), useTestingLibrary = isPackageExists("@testing-library/angular"), enableVitestGlobals = true, e2eFolderPath } = options;
2125
2165
  const [vitestPlugin, jestDomPlugin, testingLibraryPlugin] = await Promise.all([
2126
2166
  interopDefault(import("@vitest/eslint-plugin")),
2127
2167
  useJestDom ? interopDefault(import("eslint-plugin-jest-dom")) : Promise.resolve(void 0),
@@ -2136,11 +2176,11 @@ async function vitest(options = {}) {
2136
2176
  },
2137
2177
  languageOptions: { globals: {
2138
2178
  ...globals.node,
2139
- ...globals.vitest,
2140
- ...vitestPlugin.environments.env.globals
2179
+ ...enableVitestGlobals ? vitestPlugin.environments.env.globals : {}
2141
2180
  } },
2142
2181
  settings: { vitest: { typecheck: true } },
2143
- files: [...GLOB_TESTS],
2182
+ files,
2183
+ ...e2eFolderPath ? { ignores: [pathToGlobPattern()(e2eFolderPath)] } : {},
2144
2184
  rules: {
2145
2185
  ...vitestPlugin.configs.recommended.rules,
2146
2186
  ...jestDomPlugin?.configs["flat/recommended"].rules,
@@ -2176,7 +2216,6 @@ async function vitest(options = {}) {
2176
2216
  }
2177
2217
  });
2178
2218
  }
2179
-
2180
2219
  //#endregion
2181
2220
  //#region src/configs/yaml.ts
2182
2221
  /**
@@ -2230,7 +2269,6 @@ async function yaml(options = {}) {
2230
2269
  }
2231
2270
  });
2232
2271
  }
2233
-
2234
2272
  //#endregion
2235
2273
  //#region src/shared/constants.ts
2236
2274
  const NGRX_PACKAGES = [
@@ -2239,8 +2277,8 @@ const NGRX_PACKAGES = [
2239
2277
  "@ngrx/signals",
2240
2278
  "@ngrx/operators"
2241
2279
  ];
2280
+ const PLAYWRIGHT_PACKAGES = ["@playwright/test", "playwright"];
2242
2281
  const OPTIONS_SYMBOL = Symbol("options");
2243
-
2244
2282
  //#endregion
2245
2283
  //#region src/factories/project-config.ts
2246
2284
  /**
@@ -2278,10 +2316,6 @@ async function defineProjectConfig(baseConfig, options = {}, ...userConfigs) {
2278
2316
  if (enableAngular) {
2279
2317
  const angularOptions = resolveSubOptions(options, "angular");
2280
2318
  configs.push(angular(angularOptions));
2281
- if (workspaceOptions.formatters) {
2282
- const htmlFormatters = resolvedBaseConfig.find((c) => c.name === "fabdeh/formatter/html");
2283
- if (htmlFormatters) delete htmlFormatters.languageOptions;
2284
- }
2285
2319
  }
2286
2320
  if (enableNgrx) {
2287
2321
  const ngrxOptions = resolveSubOptions(options, "ngrx");
@@ -2304,7 +2338,6 @@ async function defineProjectConfig(baseConfig, options = {}, ...userConfigs) {
2304
2338
  }
2305
2339
  return tseslint.config(...resolvedBaseConfig, ...await Promise.all(configs), ...await Promise.all(userConfigs));
2306
2340
  }
2307
-
2308
2341
  //#endregion
2309
2342
  //#region src/factories/standard-config.ts
2310
2343
  /**
@@ -2320,7 +2353,7 @@ async function defineProjectConfig(baseConfig, options = {}, ...userConfigs) {
2320
2353
  * ```
2321
2354
  */
2322
2355
  async function defineConfig(options = {}, ...userConfigs) {
2323
- const { angular: enableAngular = isPackageExists("@angular/core"), gitignore: enableGitignore = true, jsdoc: enableJsdoc = options.type === "lib", ngrx: enableNgrx = NGRX_PACKAGES.some((p) => isPackageExists(p)), pnpm: enableCatalogs = false, regexp: enableRegexp = true, tailwindcss: enableTailwind = false, typescript: enableTypescript = isPackageExists("typescript"), unicorn: enableUnicorn = true, vitest: enableVitest = isPackageExists("vitest") } = options;
2356
+ const { angular: enableAngular = isPackageExists("@angular/core"), gitignore: enableGitignore = true, jsdoc: enableJsdoc = options.type === "lib", ngrx: enableNgrx = NGRX_PACKAGES.some((p) => isPackageExists(p)), playwright: enablePlaywright = PLAYWRIGHT_PACKAGES.some((p) => isPackageExists(p)), pnpm: enableCatalogs = false, regexp: enableRegexp = true, tailwindcss: enableTailwind = false, typescript: enableTypescript = isPackageExists("typescript"), unicorn: enableUnicorn = true, vitest: enableVitest = isPackageExists("vitest") } = options;
2324
2357
  if (enableNgrx && !enableAngular) throw new Error("NgRx rules can only be enabled if Angular rules are also enabled.");
2325
2358
  const stylisticOptions = options.stylistic === false ? false : typeof options.stylistic === "object" ? options.stylistic : {};
2326
2359
  const configs = [];
@@ -2363,9 +2396,21 @@ async function defineConfig(options = {}, ...userConfigs) {
2363
2396
  useRelaxedNamingConventionForCamelAndPascalCases: typescriptOptions.useRelaxedNamingConventionForCamelAndPascalCases
2364
2397
  }));
2365
2398
  }
2399
+ let e2eFolderPath;
2400
+ if (enablePlaywright) {
2401
+ const playwrightOptions = resolveSubOptions(options, "playwright");
2402
+ e2eFolderPath = playwrightOptions.e2eFolderPath ?? await getPlaywrightDirectory() ?? "e2e";
2403
+ configs.push(playwright({
2404
+ ...playwrightOptions,
2405
+ e2eFolderPath
2406
+ }));
2407
+ }
2366
2408
  if (enableVitest) {
2367
2409
  const vitestOptions = resolveSubOptions(options, "vitest");
2368
- configs.push(vitest(vitestOptions));
2410
+ configs.push(vitest({
2411
+ ...vitestOptions,
2412
+ e2eFolderPath
2413
+ }));
2369
2414
  }
2370
2415
  if (enableTailwind) {
2371
2416
  const tailwindcssOptions = resolveSubOptions(options, "tailwindcss");
@@ -2400,10 +2445,8 @@ async function defineConfig(options = {}, ...userConfigs) {
2400
2445
  const markdownOptions = resolveSubOptions(options, "markdown");
2401
2446
  configs.push(markdown(markdownOptions));
2402
2447
  }
2403
- if (options.formatters) configs.push(formatters(options.formatters, typeof stylisticOptions === "boolean" ? {} : stylisticOptions, Boolean(enableAngular)));
2404
2448
  return tseslint.config(...await Promise.all(configs), ...await Promise.all(userConfigs));
2405
2449
  }
2406
-
2407
2450
  //#endregion
2408
2451
  //#region src/factories/workspace-config.ts
2409
2452
  /**
@@ -2475,11 +2518,9 @@ async function defineWorkspaceConfig(options = {}, ...userConfigs) {
2475
2518
  const markdownOptions = resolveSubOptions(options, "markdown");
2476
2519
  configs.push(markdown(markdownOptions));
2477
2520
  }
2478
- if (options.formatters) configs.push(formatters(options.formatters, typeof stylisticOptions === "boolean" ? {} : stylisticOptions, false));
2479
2521
  const eslintConfigWithOptions = tseslint.config(...await Promise.all(configs), ...await Promise.all(userConfigs));
2480
2522
  eslintConfigWithOptions[OPTIONS_SYMBOL] = options;
2481
2523
  return eslintConfigWithOptions;
2482
2524
  }
2483
-
2484
2525
  //#endregion
2485
- export { GLOB_HTML, GLOB_JS, GLOB_SRC, GLOB_TESTS, GLOB_TS, STYLISTIC_CONFIG_DEFAULT, angular, comments, defineConfig, defineProjectConfig, defineWorkspaceConfig, ensurePackages, formatters, getTsConfigFileName, getWorkspaceRoot, ignores, imports, interopDefault, isPackageInScope, javascript, jsdoc, jsonc, markdown, ngrx, node, perfectionist, pnpm, regexp, resolveSubOptions, sortPackageJson, sortTsConfig, stylistic, tailwindcss, toml, typescript, unicorn, vitest, yaml };
2526
+ export { GLOB_HTML, GLOB_JS, GLOB_SRC, GLOB_TESTS, GLOB_TS, STYLISTIC_CONFIG_DEFAULT, angular, comments, convertPathToPosix, defineConfig, defineProjectConfig, defineWorkspaceConfig, ensurePackages, getPlaywrightDirectory, getTsConfigFileName, getWorkspaceRoot, ignores, imports, interopDefault, isDirectory, isPackageInScope, javascript, jsdoc, jsonc, markdown, ngrx, node, pathToGlobPattern, perfectionist, playwright, pnpm, regexp, resolveSubOptions, sortPackageJson, sortTsConfig, stylistic, tailwindcss, toml, typescript, unicorn, vitest, yaml };