@fabdeh/eslint-config 0.9.1 → 0.11.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 +310 -69
  2. package/dist/index.d.mts +15161 -13266
  3. package/dist/index.mjs +611 -408
  4. package/package.json +26 -37
package/dist/index.mjs CHANGED
@@ -1,19 +1,60 @@
1
- import tseslint from "typescript-eslint";
2
- import { statSync } from "node:fs";
3
- import { dirname, join } from "node:path";
1
+ import { readFileSync, statSync } from "node:fs";
2
+ import { dirname, join, normalize, resolve } from "node:path";
4
3
  import process from "node:process";
4
+ import { globSync } from "glob";
5
5
  import { isPackageExists } from "local-pkg";
6
6
  import eslintComments from "@eslint-community/eslint-plugin-eslint-comments";
7
7
  import * as importX from "eslint-plugin-import-x";
8
8
  import preferArrowFunctions from "eslint-plugin-prefer-arrow-functions";
9
9
  import unusedImports from "eslint-plugin-unused-imports";
10
10
  import globals from "globals";
11
+ import tseslint from "typescript-eslint";
11
12
  import jsdocPlugin from "eslint-plugin-jsdoc";
12
13
  import { mergeProcessors, processorPassThrough } from "eslint-merge-processors";
13
14
  import nodePlugin from "eslint-plugin-n";
14
15
  import perfectionistPlugin from "eslint-plugin-perfectionist";
16
+ import { readFile } from "node:fs/promises";
17
+ import { findUp, findUpSync } from "find-up-simple";
15
18
  import { configs } from "eslint-plugin-regexp";
16
19
  import unicornPlugin from "eslint-plugin-unicorn";
20
+ import { FlatConfigComposer } from "eslint-flat-config-utils";
21
+ //#region src/config-presets.ts
22
+ const CONFIG_PRESET_FULL_ON = {
23
+ angular: true,
24
+ gitignore: true,
25
+ jsdoc: true,
26
+ jsonc: true,
27
+ markdown: true,
28
+ ngrx: true,
29
+ playwright: true,
30
+ pnpm: true,
31
+ regexp: true,
32
+ stylistic: { experimental: true },
33
+ tailwindcss: true,
34
+ toml: true,
35
+ typescript: { enableErasableSyntaxOnly: true },
36
+ unicorn: true,
37
+ vitest: true,
38
+ yaml: true
39
+ };
40
+ const CONFIG_PRESET_FULL_OFF = {
41
+ angular: false,
42
+ gitignore: false,
43
+ jsdoc: false,
44
+ jsonc: false,
45
+ markdown: false,
46
+ ngrx: false,
47
+ playwright: false,
48
+ pnpm: false,
49
+ regexp: false,
50
+ stylistic: false,
51
+ tailwindcss: false,
52
+ toml: false,
53
+ typescript: false,
54
+ unicorn: false,
55
+ vitest: false,
56
+ yaml: false
57
+ };
17
58
  /**
18
59
  * A glob pattern that matches JavaScript and TypeScript source files (including JSX/TSX).
19
60
  */
@@ -31,22 +72,6 @@ const GLOB_TS_EXT = "?([cm])ts?(x)";
31
72
  */
32
73
  const GLOB_TS = `**/*.${GLOB_TS_EXT}`;
33
74
  /**
34
- * A glob pattern that matches all CSS files.
35
- */
36
- const GLOB_CSS = "**/*.css";
37
- /**
38
- * A glob pattern that matches all PostCSS files.
39
- */
40
- const GLOB_POSTCSS = "**/*.{p,post}css";
41
- /**
42
- * A glob pattern that matches all LESS files.
43
- */
44
- const GLOB_LESS = "**/*.less";
45
- /**
46
- * A glob pattern that matches all SCSS files.
47
- */
48
- const GLOB_SCSS = "**/*.scss";
49
- /**
50
75
  * A glob pattern that matches all JSON files.
51
76
  */
52
77
  const GLOB_JSON = "**/*.json";
@@ -79,29 +104,14 @@ const GLOB_YAML = "**/*.y?(a)ml";
79
104
  */
80
105
  const GLOB_TOML = "**/*.toml";
81
106
  /**
82
- * A glob pattern that matches XML files.
83
- */
84
- const GLOB_XML = "**/*.xml";
85
- /**
86
- * A glob pattern that matches SVG files.
87
- */
88
- const GLOB_SVG = "**/*.svg";
89
- /**
90
- * A glob pattern that matches GraphQL files.
91
- */
92
- const GLOB_GRAPHQL = "**/*.{g,graph}ql";
93
- /**
94
107
  * A glob pattern that matches HTML files.
95
108
  */
96
109
  const GLOB_HTML = "**/*.htm?(l)";
97
110
  /**
98
111
  * A glob pattern that matches test files.
99
112
  */
100
- const GLOB_TESTS = [
101
- `**/*.spec.?([cm])[jt]s`,
102
- `**/*.test.?([cm])[jt]s`,
103
- `**/test-setup.?([cm])[jt]s`
104
- ];
113
+ const GLOB_TESTS = [`**/*.spec.?([cm])[jt]s`, `**/*.test.?([cm])[jt]s`];
114
+ const GLOB_VITEST = [...GLOB_TESTS, `**/test-setup.?([cm])[jt]s`];
105
115
  /**
106
116
  * A glob pattern that matches files to exclude from linting.
107
117
  */
@@ -111,7 +121,7 @@ const GLOB_EXCLUDE = [
111
121
  "**/package-lock.json",
112
122
  "**/yarn.lock",
113
123
  "**/pnpm-lock.yaml",
114
- "**/bun.lockb",
124
+ "**/bun.lock?(b)",
115
125
  "**/output",
116
126
  "**/coverage",
117
127
  "**/temp",
@@ -137,8 +147,7 @@ const GLOB_EXCLUDE = [
137
147
  "**/LICENSE*",
138
148
  "**/__snapshots__",
139
149
  "**/auto-import?(s).d.ts",
140
- "**/components.d.ts",
141
- "**/prettier-types.ts"
150
+ "**/components.d.ts"
142
151
  ];
143
152
  //#endregion
144
153
  //#region src/shared/utils.ts
@@ -172,6 +181,19 @@ function fileExists(path) {
172
181
  }
173
182
  }
174
183
  /**
184
+ * Checks whether the given path points to an existing directory.
185
+ *
186
+ * @param path - Filesystem path to validate.
187
+ * @returns `true` when the path exists and is a directory; otherwise `false`.
188
+ */
189
+ function isDirectory(path) {
190
+ try {
191
+ return statSync(path).isDirectory();
192
+ } catch {
193
+ return false;
194
+ }
195
+ }
196
+ /**
175
197
  * Retrieves the root directory of the workspace.
176
198
  *
177
199
  * This function determines the root directory of the workspace by checking for specific files
@@ -184,7 +206,7 @@ function fileExists(path) {
184
206
  * @returns The root directory of the workspace.
185
207
  */
186
208
  function getWorkspaceRoot(dir, candidateRoot) {
187
- if (process.env.NX_WORKSPACE_ROOT) return process.env.NX_WORKSPACE_ROOT;
209
+ if (process.env["NX_WORKSPACE_ROOT"]) return process.env["NX_WORKSPACE_ROOT"];
188
210
  if (dirname(dir) === dir) return candidateRoot;
189
211
  if ([
190
212
  join(dir, "nx.json"),
@@ -228,15 +250,41 @@ function isPackageInScope(name) {
228
250
  * @returns A promise that resolves when the operation is complete.
229
251
  */
230
252
  async function ensurePackages(packages) {
231
- if (process.env.CI || !process.stdout.isTTY || !IS_CWD_IN_SCOPE) return;
253
+ if (process.env["CI"] || !process.stdout.isTTY || !IS_CWD_IN_SCOPE) return;
232
254
  const nonExistingPackages = packages.filter((i) => i && !isPackageInScope(i));
233
255
  if (nonExistingPackages.length === 0) return;
234
256
  if (await (await import("@clack/prompts")).confirm({ message: `${nonExistingPackages.length === 1 ? "Package is" : "Packages are"} required for this config: ${nonExistingPackages.join(", ")}. Do you want to install them?` })) await import("@antfu/install-pkg").then((i) => i.installPackage(nonExistingPackages, { dev: true }));
235
257
  }
236
258
  /**
259
+ * Detects whether the current process is running in an interactive editor environment.
260
+ *
261
+ * The check returns `false` in CI and Git hook\/`lint-staged` contexts to avoid
262
+ * editor-specific behavior in non-interactive executions.
263
+ *
264
+ * Editor detection is based on common environment variables from VS Code,
265
+ * JetBrains IDEs, Vim\/Neovim, and Zed.
266
+ *
267
+ * @returns `true` when execution appears to be inside a supported editor; otherwise `false`.
268
+ */
269
+ function isInEditorEnv() {
270
+ if (process.env["CI"]) return false;
271
+ if (isInGitHooksOrLintStaged()) return false;
272
+ return !!(process.env["VSCODE_PID"] || process.env["VSCODE_CWD"] || process.env["JETBRAINS_IDE"] || process.env["VIM"] || process.env["NVIM"] || process.env["ZED_ENVIRONMENT"] && !process.env["ZED_TERM"]);
273
+ }
274
+ /**
275
+ * Detects whether the current process is running from a Git hook or `lint\-staged`.
276
+ *
277
+ * It checks common environment variables set by Git tooling and npm lifecycle scripts.
278
+ *
279
+ * @returns `true` when execution appears to be in a Git hook or `lint\-staged` context.
280
+ */
281
+ function isInGitHooksOrLintStaged() {
282
+ return !!(process.env["GIT_PARAMS"] || process.env["VSCODE_GIT_COMMAND"] || process.env["npm_lifecycle_script"]?.startsWith("lint-staged") || process.env["npm_lifecycle_script"]?.startsWith("nano-staged"));
283
+ }
284
+ /**
237
285
  * Returns the object representation of the configuration or an empty object if it is a boolean.
238
286
  *
239
- * @param options - The {@link CreateConfigOptions} object to extract the sub-options from.
287
+ * @param options - The {@link DefineConfigOptions} object to extract the sub-options from.
240
288
  * @param key - The property name.
241
289
  * @returns An object representing the required options.
242
290
  */
@@ -245,6 +293,67 @@ function resolveSubOptions(options, key) {
245
293
  if (typeof option === "boolean") return {};
246
294
  return option ?? {};
247
295
  }
296
+ /**
297
+ * Replace Windows with posix style paths
298
+ *
299
+ * @param filePath - Path to convert
300
+ * @returns Converted filepath
301
+ */
302
+ function convertPathToPosix(filePath) {
303
+ return normalize(filePath).replaceAll("\\", "/");
304
+ }
305
+ /**
306
+ * Checks if a provided path is a directory and returns a glob string matching
307
+ * all files under that directory if so, the path itself otherwise.
308
+ *
309
+ * Reason for this is that `glob` needs `/**` to collect all the files under a
310
+ * directory where as our previous implementation without `glob` simply walked
311
+ * a directory that is passed. So this is to maintain backwards compatibility.
312
+ *
313
+ * Also makes sure all path separators are POSIX style for `glob` compatibility.
314
+ *
315
+ * @param [options] - An options object
316
+ * @param [options.extensions] - An array of accepted extensions
317
+ * @param [options.cwd] - The cwd to use to resolve relative pathnames
318
+ * @returns A function that takes a pathname and returns a glob that
319
+ * matches all files with the provided extensions if
320
+ * pathname is a directory.
321
+ */
322
+ function pathToGlobPattern(options) {
323
+ const cwd = options?.cwd ?? process.cwd();
324
+ const extensions = options?.extensions?.map((ext) => ext.replace(/^\./, "")) ?? [];
325
+ const suffix = extensions.length === 0 ? "/**/*" : extensions.length === 1 ? `/**/*.${extensions[0]}` : `/**/*.{${extensions.join(",")}}`;
326
+ return (path) => {
327
+ let newPath = path;
328
+ if (isDirectory(resolve(cwd, path))) newPath = path.replace(/[/\\]$/, "") + suffix;
329
+ return convertPathToPosix(newPath);
330
+ };
331
+ }
332
+ /**
333
+ * Finds the first valid Playwright test directory declared via `testDir` in any
334
+ * `playwright.config.ts` file under the current workspace.
335
+ *
336
+ * The function:
337
+ * - Locates Playwright config files, excluding `node_modules`.
338
+ * - Reads each config as text.
339
+ * - Extracts `testDir` with a regex.
340
+ * - Resolves `testDir` relative to the config file location.
341
+ * - Returns the first resolved path that exists as a directory.
342
+ *
343
+ * @returns Absolute path to the first existing Playwright test directory, or `undefined` if none is found.
344
+ */
345
+ function getPlaywrightDirectory() {
346
+ const playwrightConfigs = globSync("**/playwright.config.ts", { ignore: ["**/node_modules/**"] });
347
+ for (const config of playwrightConfigs) try {
348
+ const configContent = readFileSync(config, "utf-8");
349
+ const match = /testDir:\s*["'](.+)["'],?/.exec(configContent);
350
+ if (match) {
351
+ const testDir = match[1];
352
+ const resolvedTestDir = resolve(dirname(config), testDir);
353
+ if (isDirectory(resolvedTestDir)) return resolvedTestDir;
354
+ }
355
+ } catch {}
356
+ }
248
357
  //#endregion
249
358
  //#region src/configs/angular.ts
250
359
  /**
@@ -252,15 +361,17 @@ function resolveSubOptions(options, key) {
252
361
  *
253
362
  * @param options - Configuration options for Angular ESLint.
254
363
  * @param options.enableAccessibilityRules - Whether to enable accessibility rules for HTML templates.
255
- * @param options.tsOverrides - Additional TypeScript rule overrides.
256
- * @param options.htmlOverrides - Additional HTML rule overrides.
257
364
  * @param options.prefix - The prefix to use for Angular component and directive selectors.
258
365
  * @returns A promise that resolves to an array of ESLint configurations.
259
366
  */
260
367
  async function angular(options = {}) {
261
368
  const angularEslint = await interopDefault(import("angular-eslint"));
262
- const { enableAccessibilityRules = true, tsOverrides = {}, htmlOverrides = {}, prefix = "app", ignoreClassNamePatternForInjectableProvidedIn: ignoreClassNamePattern, componentStylesMode = "string", preferOnPushOnly = true, banExperimentalApi = true, banDeveloperPreviewApi = true, inlineTemplateAndStyles = false } = options;
263
- return tseslint.config({
369
+ const { enableAccessibilityRules = true, overrides = {}, prefix = "app", ignoreClassNamePatternForInjectableProvidedIn: ignoreClassNamePattern, componentStylesMode = "string", preferOnPushOnly = true, banExperimentalApi = true, banDeveloperPreviewApi = true, inlineTemplateAndStyles = false } = options;
370
+ const tsOverrides = {};
371
+ const htmlOverrides = {};
372
+ for (const [key, value] of Object.entries(overrides)) if (key.startsWith("@angular-eslint/template/")) htmlOverrides[key] = value;
373
+ else if (key.startsWith("@angular-eslint/")) tsOverrides[key] = value;
374
+ return [{
264
375
  name: "fabdeh/angular/rules",
265
376
  plugins: { "@angular-eslint": angularEslint.tsPlugin },
266
377
  processor: angularEslint.processInlineTemplates,
@@ -348,7 +459,7 @@ async function angular(options = {}) {
348
459
  ...enableAccessibilityRules ? angularEslint.configs.templateAccessibility.find((c) => c.name === "angular-eslint/template-accessibility")?.rules : {},
349
460
  ...htmlOverrides
350
461
  }
351
- });
462
+ }];
352
463
  }
353
464
  //#endregion
354
465
  //#region src/configs/comments.ts
@@ -362,7 +473,7 @@ async function angular(options = {}) {
362
473
  * @returns The configuration array for ESLint comments rules.
363
474
  */
364
475
  function comments() {
365
- return tseslint.config({
476
+ return [{
366
477
  name: "fabdeh/comments/rules",
367
478
  files: [GLOB_SRC],
368
479
  plugins: { "@eslint-community/eslint-comments": eslintComments },
@@ -374,196 +485,7 @@ function comments() {
374
485
  "@eslint-community/eslint-comments/no-unused-disable": "error",
375
486
  "@eslint-community/eslint-comments/no-unused-enable": "error"
376
487
  }
377
- });
378
- }
379
- //#endregion
380
- //#region src/configs/stylistic.ts
381
- const STYLISTIC_CONFIG_DEFAULT = {
382
- semi: true,
383
- arrowParens: true,
384
- braceStyle: "1tbs",
385
- quoteProps: "as-needed"
386
- };
387
- /**
388
- * Generates a stylistic ESLint configuration.
389
- *
390
- * @param [options] - Optional configuration options to customize the stylistic rules.
391
- * @returns A promise that resolves to an array of ESLint configurations.
392
- * @example
393
- * const config = await stylistic({ semi: false });
394
- */
395
- async function stylistic(options = {}) {
396
- const stylisticOptions = {
397
- ...STYLISTIC_CONFIG_DEFAULT,
398
- ...typeof options.stylistic === "boolean" ? {} : options.stylistic
399
- };
400
- const stylisticPlugin = await interopDefault(import("@stylistic/eslint-plugin"));
401
- const config = stylisticPlugin.configs.customize(stylisticOptions);
402
- return tseslint.config({
403
- name: "fabdeh/stylistic/rules",
404
- files: [GLOB_SRC],
405
- plugins: { "@stylistic": stylisticPlugin },
406
- rules: {
407
- ...config.rules,
408
- "@stylistic/comma-dangle": ["error", {
409
- arrays: "always-multiline",
410
- objects: "always-multiline"
411
- }],
412
- "@stylistic/no-extra-semi": "error",
413
- "@stylistic/operator-linebreak": [
414
- "error",
415
- "after",
416
- { overrides: {
417
- "?": "before",
418
- ":": "before"
419
- } }
420
- ]
421
- }
422
- });
423
- }
424
- //#endregion
425
- //#region src/configs/formatters.ts
426
- /**
427
- * Merges the provided Prettier options with any overrides.
428
- *
429
- * @param options - The base Prettier options to merge.
430
- * @param overrides - Required overrides for the Prettier options. Must include a `parser` property.
431
- * @returns The merged Prettier options.
432
- */
433
- function mergePrettierOptions(options, overrides) {
434
- return {
435
- ...options,
436
- ...overrides,
437
- plugins: [...overrides.plugins ?? [], ...options.plugins ?? []]
438
- };
439
- }
440
- /**
441
- * Configures and returns an array of formatters based on the provided options.
442
- *
443
- * @param options - The options for configuring the formatters. If set to `true`, default options will be used.
444
- * @param stylistic - The stylistic options for the formatters.
445
- * @param hasAngularTemplateParser - Optional argument which indicates whether angular rules already registered the template parser or not.
446
- * @returns A promise that resolves to a `TypedConfigArray` containing the configured formatters.
447
- */
448
- async function formatters(options = {}, stylistic = {}, hasAngularTemplateParser = false) {
449
- if (options === true) {
450
- const isPrettierPluginXmlInScope = isPackageInScope("@prettier/plugin-xml");
451
- options = {
452
- css: true,
453
- graphql: true,
454
- html: true,
455
- markdown: true,
456
- slidev: isPackageExists("@slidev/cli"),
457
- svg: isPrettierPluginXmlInScope,
458
- xml: isPrettierPluginXmlInScope
459
- };
460
- }
461
- await ensurePackages([
462
- "eslint-plugin-format",
463
- options.markdown && options.slidev ? "prettier-plugin-slidev" : void 0,
464
- options.xml || options.svg ? "@prettier/plugin-xml" : void 0
465
- ]);
466
- const { indent, quotes, semi } = {
467
- ...STYLISTIC_CONFIG_DEFAULT,
468
- ...stylistic
469
- };
470
- const prettierOptions = {
471
- endOfLine: "auto",
472
- printWidth: 120,
473
- semi,
474
- singleQuote: quotes === "single",
475
- tabWidth: typeof indent === "number" ? indent : 2,
476
- trailingComma: "all",
477
- useTabs: indent === "tab",
478
- ...options.options
479
- };
480
- const prettierXmlOptions = {
481
- xmlQuoteAttributes: "double",
482
- xmlSelfClosingSpace: true,
483
- xmlSortAttributesByKey: false,
484
- xmlWhitespaceSensitivity: "ignore"
485
- };
486
- const formatPlugin = await interopDefault(import("eslint-plugin-format"));
487
- const configs = [{
488
- name: "fabdeh/formatter/setup",
489
- plugins: { format: formatPlugin }
490
488
  }];
491
- if (options.css) configs.push({
492
- name: "fabdeh/formatter/css",
493
- languageOptions: { parser: formatPlugin.parserPlain },
494
- files: [GLOB_CSS, GLOB_POSTCSS],
495
- rules: { "format/prettier": ["error", mergePrettierOptions(prettierOptions, { parser: "css" })] }
496
- }, {
497
- name: "fabdeh/formatter/scss",
498
- languageOptions: { parser: formatPlugin.parserPlain },
499
- files: [GLOB_SCSS],
500
- rules: { "format/prettier": ["error", mergePrettierOptions(prettierOptions, { parser: "scss" })] }
501
- }, {
502
- name: "fabdeh/formatter/less",
503
- languageOptions: { parser: formatPlugin.parserPlain },
504
- files: [GLOB_LESS],
505
- rules: { "format/prettier": ["error", mergePrettierOptions(prettierOptions, { parser: "less" })] }
506
- });
507
- if (options.html) configs.push({
508
- name: "fabdeh/formatter/html",
509
- ...hasAngularTemplateParser ? {} : { languageOptions: { parser: formatPlugin.parserPlain } },
510
- files: [GLOB_HTML],
511
- rules: { "format/prettier": ["error", mergePrettierOptions(prettierOptions, { parser: "html" })] }
512
- });
513
- if (options.xml) configs.push({
514
- name: "fabdeh/formatter/xml",
515
- languageOptions: { parser: formatPlugin.parserPlain },
516
- files: [GLOB_XML],
517
- rules: { "format/prettier": ["error", mergePrettierOptions({
518
- ...prettierXmlOptions,
519
- ...prettierOptions
520
- }, {
521
- parser: "xml",
522
- plugins: ["@prettier/plugin-xml"]
523
- })] }
524
- });
525
- if (options.svg) configs.push({
526
- name: "fabdeh/formatter/svg",
527
- languageOptions: { parser: formatPlugin.parserPlain },
528
- files: [GLOB_SVG],
529
- rules: { "format/prettier": ["error", mergePrettierOptions({
530
- ...prettierXmlOptions,
531
- ...prettierOptions
532
- }, {
533
- parser: "xml",
534
- plugins: ["@prettier/plugin-xml"]
535
- })] }
536
- });
537
- if (options.markdown) {
538
- const GLOB_SLIDEV = options.slidev ? options.slidev === true ? ["**/.slides.md"] : options.slidev.files : [];
539
- configs.push({
540
- name: "fabdeh/formatter/markdown",
541
- languageOptions: { parser: formatPlugin.parserPlain },
542
- files: [GLOB_MARKDOWN],
543
- ignores: GLOB_SLIDEV,
544
- rules: { "format/prettier": ["error", mergePrettierOptions(prettierOptions, {
545
- parser: "markdown",
546
- embeddedLanguageFormatting: "off"
547
- })] }
548
- });
549
- if (options.slidev) configs.push({
550
- name: "fabdeh/formatter/slidev",
551
- languageOptions: { parser: formatPlugin.parserPlain },
552
- files: GLOB_SLIDEV,
553
- rules: { "format/prettier": ["error", mergePrettierOptions(prettierOptions, {
554
- embeddedLanguageFormatting: "off",
555
- parser: "slidev",
556
- plugins: ["prettier-plugin-slidev"]
557
- })] }
558
- });
559
- }
560
- if (options.graphql) configs.push({
561
- name: "fabdeh/formatter/graphql",
562
- languageOptions: { parser: formatPlugin.parserPlain },
563
- files: [GLOB_GRAPHQL],
564
- rules: { "format/prettier": ["error", mergePrettierOptions(prettierOptions, { parser: "graphql" })] }
565
- });
566
- return tseslint.config(configs);
567
489
  }
568
490
  //#endregion
569
491
  //#region src/configs/ignore.ts
@@ -572,13 +494,13 @@ async function formatters(options = {}, stylistic = {}, hasAngularTemplateParser
572
494
  *
573
495
  * @param userIgnores - An array of user-defined glob patterns to ignore. Defaults to an empty array.
574
496
  * @param fromFactory - An optional suffix to add to the config name.
575
- * @returns A TypedConfigArray object containing the combined "ignore" patterns.
497
+ * @returns A TypedConfig[] object containing the combined "ignore" patterns.
576
498
  */
577
499
  function ignores(userIgnores = [], fromFactory) {
578
- return tseslint.config({
500
+ return [{
579
501
  name: `fabdeh/ignores${fromFactory ? `/${fromFactory}` : ""}`,
580
502
  ignores: [...!fromFactory || fromFactory === "workspace" ? GLOB_EXCLUDE : [], ...userIgnores]
581
- });
503
+ }];
582
504
  }
583
505
  //#endregion
584
506
  //#region src/configs/imports.ts
@@ -587,11 +509,11 @@ function ignores(userIgnores = [], fromFactory) {
587
509
  *
588
510
  * @param options - An object containing stylistic options.
589
511
  * @param options.stylistic - A boolean indicating whether to include stylistic rules. Defaults to true.
590
- * @returns A TypedConfigArray object with the specified import rules.
512
+ * @returns A TypedConfig[] object with the specified import rules.
591
513
  */
592
514
  function imports(options = {}) {
593
515
  const { stylistic = true } = options;
594
- return tseslint.config({
516
+ return [{
595
517
  name: "fabdeh/imports/rules",
596
518
  files: [GLOB_SRC],
597
519
  plugins: { "import-x": importX },
@@ -625,7 +547,7 @@ function imports(options = {}) {
625
547
  "import-x/named": "off",
626
548
  "import-x/no-deprecated": "off"
627
549
  }
628
- });
550
+ }];
629
551
  }
630
552
  //#endregion
631
553
  //#region src/configs/javascript.ts
@@ -645,8 +567,8 @@ function imports(options = {}) {
645
567
  * ```
646
568
  */
647
569
  function javascript(options = {}) {
648
- const { overrides = {} } = options;
649
- return tseslint.config({
570
+ const { isInEditor = false, overrides = {} } = options;
571
+ return [{
650
572
  name: "fabdeh/javascript/setup",
651
573
  languageOptions: {
652
574
  ecmaVersion: "latest",
@@ -861,7 +783,10 @@ function javascript(options = {}) {
861
783
  singleReturnOnly: true,
862
784
  allowNamedFunctions: true
863
785
  }],
864
- "prefer-const": "error",
786
+ "prefer-const": [isInEditor ? "warn" : "error", {
787
+ destructuring: "all",
788
+ ignoreReadBeforeAssign: true
789
+ }],
865
790
  "prefer-exponentiation-operator": "error",
866
791
  "prefer-object-spread": "error",
867
792
  "prefer-promise-reject-errors": "error",
@@ -874,7 +799,7 @@ function javascript(options = {}) {
874
799
  "symbol-description": "error",
875
800
  "unicode-bom": ["error", "never"],
876
801
  "use-isnan": ["error", { enforceForIndexOf: true }],
877
- "unused-imports/no-unused-imports": "error",
802
+ "unused-imports/no-unused-imports": isInEditor ? "warn" : "error",
878
803
  "unused-imports/no-unused-vars": ["error", {
879
804
  args: "after-used",
880
805
  argsIgnorePattern: "^_",
@@ -897,7 +822,7 @@ function javascript(options = {}) {
897
822
  "@typescript-eslint/no-this-alias": "error",
898
823
  ...overrides
899
824
  }
900
- });
825
+ }];
901
826
  }
902
827
  //#endregion
903
828
  //#region src/configs/jsdoc.ts
@@ -973,7 +898,7 @@ function getJsDocRules(level, stylistic, mode) {
973
898
  */
974
899
  function jsdoc(options = {}) {
975
900
  const { stylistic = true } = options;
976
- return tseslint.config({
901
+ return [{
977
902
  name: "fabdeh/jsdoc/rules",
978
903
  files: [GLOB_SRC],
979
904
  plugins: { jsdoc: jsdocPlugin },
@@ -982,7 +907,7 @@ function jsdoc(options = {}) {
982
907
  name: "fabdeh/jsdoc/ts-only/rules",
983
908
  files: [GLOB_TS],
984
909
  rules: getJsDocRules("warn", !!stylistic, "tsOnly")
985
- });
910
+ }];
986
911
  }
987
912
  //#endregion
988
913
  //#region src/configs/jsonc.ts
@@ -993,7 +918,7 @@ function jsdoc(options = {}) {
993
918
  * @param options.files - An array of glob patterns to specify the JSONC files to lint. Defaults to `[GLOB_JSON, GLOB_JSON5, GLOB_JSONC]`.
994
919
  * @param options.overrides - Additional rules or configurations to override the default settings.
995
920
  * @param options.stylistic - A boolean or object to enable or configure stylistic rules. Defaults to `true`.
996
- * @returns A promise that resolves to a `TypedConfigArray` containing the ESLint configuration.
921
+ * @returns A promise that resolves to a `TypedConfig[]` containing the ESLint configuration.
997
922
  * @example
998
923
  * ```typescript
999
924
  * const config = await jsonc({
@@ -1012,7 +937,7 @@ async function jsonc(options = {}) {
1012
937
  ], overrides = {}, stylistic = true } = options;
1013
938
  const { indent = 2 } = typeof stylistic === "object" ? stylistic : {};
1014
939
  const [jsoncPlugin, jsoncParser] = await Promise.all([interopDefault(import("eslint-plugin-jsonc")), interopDefault(import("jsonc-eslint-parser"))]);
1015
- return tseslint.config({
940
+ return [{
1016
941
  name: "fabdeh/jsonc/setup",
1017
942
  plugins: { jsonc: jsoncPlugin }
1018
943
  }, {
@@ -1066,7 +991,7 @@ async function jsonc(options = {}) {
1066
991
  } : {},
1067
992
  ...overrides
1068
993
  }
1069
- });
994
+ }];
1070
995
  }
1071
996
  //#endregion
1072
997
  //#region src/configs/markdown.ts
@@ -1079,7 +1004,7 @@ async function jsonc(options = {}) {
1079
1004
  * @param options - An object containing file and override options.
1080
1005
  * @param options.files - An array of glob patterns to specify the Markdown files to lint.
1081
1006
  * @param options.overrides - An object containing rule overrides.
1082
- * @returns A promise that resolves to a TypedConfigArray containing the ESLint configurations.
1007
+ * @returns A promise that resolves to a TypedConfig[] containing the ESLint configurations.
1083
1008
  * @example
1084
1009
  * ```typescript
1085
1010
  * const config = await markdown({
@@ -1093,75 +1018,81 @@ async function jsonc(options = {}) {
1093
1018
  async function markdown(options = {}) {
1094
1019
  const { files = [GLOB_MARKDOWN], overrides = {} } = options;
1095
1020
  const markdownPlugin = await interopDefault(import("@eslint/markdown"));
1096
- return tseslint.config({
1097
- name: "fabdeh/markdown/setup",
1098
- plugins: { markdown: markdownPlugin }
1099
- }, {
1100
- name: "fabdeh/markdown/processor",
1101
- files,
1102
- ignores: [GLOB_MARKDOWN_IN_MARKDOWN],
1103
- processor: mergeProcessors([markdownPlugin.processors.markdown, processorPassThrough])
1104
- }, {
1105
- name: "fabdeh/markdown/parser",
1106
- files,
1107
- languageOptions: { parser: {
1108
- meta: { name: "parser-plain" },
1109
- parseForESLint: (code) => ({
1110
- ast: {
1111
- body: [],
1112
- comments: [],
1113
- loc: {
1114
- end: code.length,
1115
- start: 0
1021
+ return [
1022
+ {
1023
+ name: "fabdeh/markdown/setup",
1024
+ plugins: { markdown: markdownPlugin }
1025
+ },
1026
+ {
1027
+ name: "fabdeh/markdown/processor",
1028
+ files,
1029
+ ignores: [GLOB_MARKDOWN_IN_MARKDOWN],
1030
+ processor: mergeProcessors([markdownPlugin.processors.markdown, processorPassThrough])
1031
+ },
1032
+ {
1033
+ name: "fabdeh/markdown/parser",
1034
+ files,
1035
+ languageOptions: { parser: {
1036
+ meta: { name: "parser-plain" },
1037
+ parseForESLint: (code) => ({
1038
+ ast: {
1039
+ body: [],
1040
+ comments: [],
1041
+ loc: {
1042
+ end: code.length,
1043
+ start: 0
1044
+ },
1045
+ range: [0, code.length],
1046
+ tokens: [],
1047
+ type: "Program"
1116
1048
  },
1117
- range: [0, code.length],
1118
- tokens: [],
1119
- type: "Program"
1120
- },
1121
- scopeManager: null,
1122
- services: { isPlain: true },
1123
- visitorKeys: { Program: [] }
1124
- })
1125
- } }
1126
- }, {
1127
- name: "fabdeh/markdown/disables",
1128
- languageOptions: { parserOptions: { ecmaFeatures: { impliedStrict: true } } },
1129
- files: [GLOB_MARKDOWN_CODE],
1130
- rules: {
1131
- ...tseslint.configs.disableTypeChecked.rules,
1132
- "import-x/newline-after-import": "off",
1133
- "no-alert": "off",
1134
- "no-console": "off",
1135
- "no-labels": "off",
1136
- "no-lone-blocks": "off",
1137
- "no-restricted-syntax": "off",
1138
- "no-undef": "off",
1139
- "no-unused-expressions": "off",
1140
- "no-unused-labels": "off",
1141
- "no-unused-vars": "off",
1142
- "@stylistic/comma-dangle": "off",
1143
- "@stylistic/eol-last": "off",
1144
- "@typescript-eslint/consistent-type-imports": "off",
1145
- "@typescript-eslint/explicit-function-return-type": "off",
1146
- "@typescript-eslint/no-namespace": "off",
1147
- "@typescript-eslint/no-redeclare": "off",
1148
- "@typescript-eslint/no-require-imports": "off",
1149
- "@typescript-eslint/no-unused-expressions": "off",
1150
- "@typescript-eslint/no-unused-vars": "off",
1151
- "@typescript-eslint/no-use-before-define": "off",
1152
- "unicode-bom": "off",
1153
- "unused-imports/no-unused-imports": "off",
1154
- "unused-imports/no-unused-vars": "off",
1155
- ...overrides
1156
- }
1157
- }, {
1158
- name: "fabdeh/markdown/rules",
1159
- files,
1160
- rules: {
1161
- ...markdownPlugin.configs.recommended.at(0)?.rules,
1162
- "markdown/no-duplicate-headings": "error"
1049
+ scopeManager: null,
1050
+ services: { isPlain: true },
1051
+ visitorKeys: { Program: [] }
1052
+ })
1053
+ } }
1054
+ },
1055
+ {
1056
+ name: "fabdeh/markdown/disables",
1057
+ languageOptions: { parserOptions: { ecmaFeatures: { impliedStrict: true } } },
1058
+ files: [GLOB_MARKDOWN_CODE],
1059
+ rules: {
1060
+ ...tseslint.configs.disableTypeChecked.rules,
1061
+ "import-x/newline-after-import": "off",
1062
+ "no-alert": "off",
1063
+ "no-console": "off",
1064
+ "no-labels": "off",
1065
+ "no-lone-blocks": "off",
1066
+ "no-restricted-syntax": "off",
1067
+ "no-undef": "off",
1068
+ "no-unused-expressions": "off",
1069
+ "no-unused-labels": "off",
1070
+ "no-unused-vars": "off",
1071
+ "@stylistic/comma-dangle": "off",
1072
+ "@stylistic/eol-last": "off",
1073
+ "@typescript-eslint/consistent-type-imports": "off",
1074
+ "@typescript-eslint/explicit-function-return-type": "off",
1075
+ "@typescript-eslint/no-namespace": "off",
1076
+ "@typescript-eslint/no-redeclare": "off",
1077
+ "@typescript-eslint/no-require-imports": "off",
1078
+ "@typescript-eslint/no-unused-expressions": "off",
1079
+ "@typescript-eslint/no-unused-vars": "off",
1080
+ "@typescript-eslint/no-use-before-define": "off",
1081
+ "unicode-bom": "off",
1082
+ "unused-imports/no-unused-imports": "off",
1083
+ "unused-imports/no-unused-vars": "off",
1084
+ ...overrides
1085
+ }
1086
+ },
1087
+ {
1088
+ name: "fabdeh/markdown/rules",
1089
+ files,
1090
+ rules: {
1091
+ ...markdownPlugin.configs.recommended.at(0)?.rules,
1092
+ "markdown/no-duplicate-headings": "error"
1093
+ }
1163
1094
  }
1164
- });
1095
+ ];
1165
1096
  }
1166
1097
  //#endregion
1167
1098
  //#region src/configs/rules-configs/naming-convention.ts
@@ -1272,15 +1203,16 @@ async function ngrx(options = {}) {
1272
1203
  const { store = isPackageExists("@ngrx/store"), effects = isPackageExists("@ngrx/effects"), signals = isPackageExists("@ngrx/signals"), useRelaxedNamingConventionForCamelAndPascalCases = false } = options;
1273
1204
  const configs = [];
1274
1205
  let addOperatorsRules = false;
1206
+ const registerPluginFiles = [];
1275
1207
  const ngrxOperatorsFiles = [];
1276
1208
  if (store) {
1277
1209
  const { files = DEFAULT_STORE_GLOB, enforceOperatorsRules = isPackageExists("@ngrx/operators"), overrides = {} } = typeof store === "object" ? store : {};
1278
1210
  addOperatorsRules ||= enforceOperatorsRules;
1279
1211
  ngrxOperatorsFiles.push(files);
1212
+ registerPluginFiles.push(files);
1280
1213
  configs.push({
1281
1214
  name: "fabdeh/ngrx-store/rules",
1282
1215
  files,
1283
- plugins: { "@ngrx": ngrxPlugin },
1284
1216
  rules: {
1285
1217
  ...ngrxPlugin.configs.store.find((c) => c.name === "ngrx/store")?.rules,
1286
1218
  "@typescript-eslint/naming-convention": [
@@ -1314,10 +1246,10 @@ async function ngrx(options = {}) {
1314
1246
  const { files = DEFAULT_EFFECTS_GLOB, enforceOperatorsRules = isPackageExists("@ngrx/operators"), overrides = {} } = typeof effects === "object" ? effects : {};
1315
1247
  addOperatorsRules ||= enforceOperatorsRules;
1316
1248
  ngrxOperatorsFiles.push(files);
1249
+ registerPluginFiles.push(files);
1317
1250
  configs.push({
1318
1251
  name: "fabdeh/ngrx-effects/rules",
1319
1252
  files,
1320
- plugins: { "@ngrx": ngrxPlugin },
1321
1253
  rules: {
1322
1254
  ...ngrxPlugin.configs.effects.find((c) => c.name === "ngrx/effects")?.rules,
1323
1255
  "@typescript-eslint/naming-convention": [
@@ -1344,10 +1276,10 @@ async function ngrx(options = {}) {
1344
1276
  const { files = DEFAULT_SIGNALS_GLOB, enforceOperatorsRules = isPackageExists("@ngrx/operators"), overrides = {} } = typeof signals === "object" ? signals : {};
1345
1277
  addOperatorsRules ||= enforceOperatorsRules;
1346
1278
  ngrxOperatorsFiles.push(files);
1279
+ registerPluginFiles.push(files);
1347
1280
  configs.push({
1348
1281
  name: "fabdeh/ngrx-signals/rules",
1349
1282
  files,
1350
- plugins: { "@ngrx": ngrxPlugin },
1351
1283
  rules: {
1352
1284
  ...ngrxPlugin.configs.signals.find((c) => c.name === "ngrx/signals")?.rules,
1353
1285
  "@typescript-eslint/naming-convention": [
@@ -1378,10 +1310,14 @@ async function ngrx(options = {}) {
1378
1310
  if (addOperatorsRules) configs.push({
1379
1311
  name: "fabdeh/ngrx-operators/rules",
1380
1312
  files: [...ngrxOperatorsFiles],
1381
- plugins: { "@ngrx": ngrxPlugin },
1382
1313
  rules: { ...ngrxPlugin.configs.operators.find((c) => c.name === "ngrx/operators")?.rules }
1383
1314
  });
1384
- return tseslint.config(configs);
1315
+ if (registerPluginFiles.length > 0) configs.unshift({
1316
+ name: "fabdeh/ngrx/setup",
1317
+ files: [...registerPluginFiles],
1318
+ plugins: { "@ngrx": ngrxPlugin.configs.store.find((c) => c.name === "ngrx/base")?.plugins?.["@ngrx"] ?? {} }
1319
+ });
1320
+ return configs;
1385
1321
  }
1386
1322
  //#endregion
1387
1323
  //#region src/configs/node.ts
@@ -1391,7 +1327,7 @@ async function ngrx(options = {}) {
1391
1327
  * @returns An array defining Node.js rules, plugins, and their configurations.
1392
1328
  */
1393
1329
  function node() {
1394
- return tseslint.config({
1330
+ return [{
1395
1331
  name: "fabdeh/node/rules",
1396
1332
  files: [GLOB_SRC],
1397
1333
  plugins: { n: nodePlugin },
@@ -1408,7 +1344,7 @@ function node() {
1408
1344
  "n/prefer-promises/fs": "error",
1409
1345
  "n/process-exit-as-throw": "error"
1410
1346
  }
1411
- });
1347
+ }];
1412
1348
  }
1413
1349
  //#endregion
1414
1350
  //#region src/configs/rules-configs/perfectionist-groups.ts
@@ -1470,7 +1406,7 @@ const SORT_UNION_OR_INTERSECTION_GROUPS = [
1470
1406
  */
1471
1407
  function perfectionist() {
1472
1408
  const rootDir = getWorkspaceRoot(process.cwd(), process.cwd());
1473
- return tseslint.config({
1409
+ return [{
1474
1410
  name: "fabdeh/perfectionist/rules",
1475
1411
  files: [GLOB_SRC],
1476
1412
  plugins: { perfectionist: perfectionistPlugin },
@@ -1512,41 +1448,204 @@ function perfectionist() {
1512
1448
  type: "natural"
1513
1449
  }]
1514
1450
  }
1515
- });
1451
+ }];
1452
+ }
1453
+ //#endregion
1454
+ //#region src/configs/playwright.ts
1455
+ /**
1456
+ * Configures and returns an ESLint flat config for Playwright test files.
1457
+ *
1458
+ * @param [options] - Options used to customize the Playwright config.
1459
+ * @param [options.e2eFolderPath] - The path to the e2e folder to prepend to each `options.files` glob pattern (defaults to `e2e`).
1460
+ * @param [options.files] - File globs to target (defaults to `GLOB_TESTS`).
1461
+ * @param [options.overrides] - Custom ESLint rule overrides merged last.
1462
+ * @returns A promise that resolves to a single-entry ESLint config array.
1463
+ * @example
1464
+ * const config = await playwright({
1465
+ * e2eFolderPath: 'tests/e2e',
1466
+ * files: ['**\/*.test.ts'], // will become 'tests\/e2e\/**\/*.test.ts' because of the e2eFolderPath option
1467
+ * overrides: {
1468
+ * 'playwright/no-page-pause': 'off',
1469
+ * },
1470
+ * });
1471
+ */
1472
+ async function playwright(options = {}) {
1473
+ const { e2eFolderPath = "e2e", files = GLOB_TESTS.map((glob) => convertPathToPosix(join(e2eFolderPath, glob))), overrides = {} } = options;
1474
+ if (!isDirectory(e2eFolderPath)) return [];
1475
+ const playwrightPlugin = await interopDefault(import("eslint-plugin-playwright"));
1476
+ return [{
1477
+ name: "fabdeh/playwright/rules",
1478
+ plugins: { playwright: playwrightPlugin },
1479
+ files,
1480
+ rules: {
1481
+ ...playwrightPlugin.configs.recommended.rules,
1482
+ "max-lines": "off",
1483
+ "@typescript-eslint/consistent-type-assertions": "off",
1484
+ "@typescript-eslint/no-empty-function": "off",
1485
+ "@typescript-eslint/no-unsafe-assignment": "off",
1486
+ "@typescript-eslint/no-unsafe-call": "off",
1487
+ "@typescript-eslint/no-unsafe-member-access": "off",
1488
+ "@typescript-eslint/unbound-method": "off",
1489
+ "unicorn/no-null": "off",
1490
+ "playwright/no-get-by-title": "error",
1491
+ "playwright/no-nth-methods": "error",
1492
+ "playwright/prefer-comparison-matcher": "error",
1493
+ "playwright/prefer-equality-matcher": "error",
1494
+ "playwright/prefer-lowercase-title": ["error", { allowedPrefixes: [
1495
+ "GET",
1496
+ "POST",
1497
+ "PUT",
1498
+ "DELETE",
1499
+ "PATCH",
1500
+ "HEAD",
1501
+ "OPTIONS"
1502
+ ] }],
1503
+ "playwright/prefer-native-locators": "error",
1504
+ "playwright/prefer-to-be": "error",
1505
+ "playwright/prefer-to-contain": "error",
1506
+ ...getJsDocRules("off", true, "both"),
1507
+ ...overrides
1508
+ }
1509
+ }];
1516
1510
  }
1517
1511
  //#endregion
1518
1512
  //#region src/configs/pnpm.ts
1519
1513
  /**
1514
+ * Detects whether the current PNPM workspace uses catalog definitions.
1515
+ *
1516
+ * Searches for a `pnpm-workspace.yaml` file and checks whether it contains
1517
+ * either the `catalog:` or `catalogs:` keys, which are used to define shared
1518
+ * dependency version catalogs in PNPM workspaces.
1519
+ *
1520
+ * @returns `true` when catalog configuration is present; otherwise `false`.
1521
+ */
1522
+ async function detectCatalogUsage() {
1523
+ const workspaceFile = await findUp("pnpm-workspace.yaml");
1524
+ if (!workspaceFile) return false;
1525
+ const yaml = await readFile(workspaceFile, "utf-8");
1526
+ return yaml.includes("catalog:") || yaml.includes("catalogs:");
1527
+ }
1528
+ /**
1520
1529
  * Generates an array containing configuration settings for PNPM workspace-specific rules and plugins.
1521
1530
  *
1531
+ * @param options - The options
1522
1532
  * @returns An array defining PNPM workspace rules, plugins, and their configurations.
1523
1533
  */
1524
- async function pnpm() {
1525
- const [pluginPnpm, yamlParser, jsoncParser] = await Promise.all([
1534
+ async function pnpm(options = {}) {
1535
+ const [pluginPnpm, pluginYaml, yamlParser] = await Promise.all([
1526
1536
  interopDefault(import("eslint-plugin-pnpm")),
1527
- interopDefault(import("yaml-eslint-parser")),
1528
- interopDefault(import("jsonc-eslint-parser"))
1537
+ interopDefault(import("eslint-plugin-yml")),
1538
+ interopDefault(import("yaml-eslint-parser"))
1529
1539
  ]);
1530
- return tseslint.config({
1540
+ const { catalogs = await detectCatalogUsage(), isInEditor = false, json = true, sort = true, yaml = true } = options;
1541
+ const configs = [];
1542
+ if (json) configs.push({
1531
1543
  name: "fabdeh/pnpm/package-json",
1532
1544
  files: ["package.json", "**/package.json"],
1533
- languageOptions: { parser: jsoncParser },
1545
+ language: "jsonc/x",
1534
1546
  plugins: { pnpm: pluginPnpm },
1535
1547
  rules: {
1536
- "pnpm/json-enforce-catalog": "error",
1537
- "pnpm/json-prefer-workspace-settings": "error",
1538
- "pnpm/json-valid-catalog": "error"
1548
+ ...catalogs ? { "pnpm/json-enforce-catalog": ["error", {
1549
+ autofix: !isInEditor,
1550
+ ignores: ["@types/vscode"]
1551
+ }] } : {},
1552
+ "pnpm/json-prefer-workspace-settings": ["error", { autofix: !isInEditor }],
1553
+ "pnpm/json-valid-catalog": ["error", { autofix: !isInEditor }]
1539
1554
  }
1540
- }, {
1555
+ });
1556
+ if (yaml) configs.push({
1541
1557
  name: "fabdeh/pnpm/pnpm-workspace-yaml",
1542
1558
  files: ["pnpm-workspace.yaml"],
1543
1559
  languageOptions: { parser: yamlParser },
1544
1560
  plugins: { pnpm: pluginPnpm },
1545
1561
  rules: {
1562
+ "pnpm/yaml-enforce-settings": ["error", { settings: {
1563
+ shellEmulator: true,
1564
+ trustPolicy: "no-downgrade"
1565
+ } }],
1546
1566
  "pnpm/yaml-no-duplicate-catalog-item": "error",
1547
1567
  "pnpm/yaml-no-unused-catalog-item": "error"
1548
1568
  }
1549
1569
  });
1570
+ if (sort) configs.push({
1571
+ files: ["pnpm-workspace.yaml"],
1572
+ languageOptions: { parser: yamlParser },
1573
+ name: "antfu/pnpm/pnpm-workspace-yaml-sort",
1574
+ plugins: { yaml: pluginYaml },
1575
+ rules: { "yaml/sort-keys": [
1576
+ "error",
1577
+ {
1578
+ order: [
1579
+ ...[
1580
+ "cacheDir",
1581
+ "catalogMode",
1582
+ "cleanupUnusedCatalogs",
1583
+ "dedupeDirectDeps",
1584
+ "deployAllFiles",
1585
+ "enablePrePostScripts",
1586
+ "engineStrict",
1587
+ "extendNodePath",
1588
+ "hoist",
1589
+ "hoistPattern",
1590
+ "hoistWorkspacePackages",
1591
+ "ignoreCompatibilityDb",
1592
+ "ignoreDepScripts",
1593
+ "ignoreScripts",
1594
+ "ignoreWorkspaceRootCheck",
1595
+ "managePackageManagerVersions",
1596
+ "minimumReleaseAge",
1597
+ "minimumReleaseAgeExclude",
1598
+ "modulesDir",
1599
+ "nodeLinker",
1600
+ "nodeVersion",
1601
+ "optimisticRepeatInstall",
1602
+ "packageManagerStrict",
1603
+ "packageManagerStrictVersion",
1604
+ "preferSymlinkedExecutables",
1605
+ "preferWorkspacePackages",
1606
+ "publicHoistPattern",
1607
+ "registrySupportsTimeField",
1608
+ "requiredScripts",
1609
+ "resolutionMode",
1610
+ "savePrefix",
1611
+ "scriptShell",
1612
+ "shamefullyHoist",
1613
+ "shellEmulator",
1614
+ "stateDir",
1615
+ "supportedArchitectures",
1616
+ "symlink",
1617
+ "tag",
1618
+ "trustPolicy",
1619
+ "trustPolicyExclude",
1620
+ "updateNotifier"
1621
+ ],
1622
+ "packages",
1623
+ "overrides",
1624
+ "patchedDependencies",
1625
+ "catalog",
1626
+ "catalogs",
1627
+ ...[
1628
+ "allowedDeprecatedVersions",
1629
+ "allowNonAppliedPatches",
1630
+ "configDependencies",
1631
+ "ignoredBuiltDependencies",
1632
+ "ignoredOptionalDependencies",
1633
+ "neverBuiltDependencies",
1634
+ "onlyBuiltDependencies",
1635
+ "onlyBuiltDependenciesFile",
1636
+ "packageExtensions",
1637
+ "peerDependencyRules"
1638
+ ]
1639
+ ],
1640
+ pathPattern: "^$"
1641
+ },
1642
+ {
1643
+ order: { type: "asc" },
1644
+ pathPattern: ".*"
1645
+ }
1646
+ ] }
1647
+ });
1648
+ return configs;
1550
1649
  }
1551
1650
  //#endregion
1552
1651
  //#region src/configs/regexp.ts
@@ -1560,7 +1659,7 @@ function regexp(options = {}) {
1560
1659
  const { level, overrides } = options;
1561
1660
  const config = configs["flat/recommended"];
1562
1661
  const rules = Object.fromEntries(Object.entries(config.rules).map(([ruleName, ruleLevel]) => [ruleName, level === "warn" ? level : ruleLevel]));
1563
- return tseslint.config({
1662
+ return [{
1564
1663
  name: "fabdeh/regexp/rules",
1565
1664
  files: [GLOB_SRC],
1566
1665
  plugins: { ...config.plugins },
@@ -1568,7 +1667,7 @@ function regexp(options = {}) {
1568
1667
  ...rules,
1569
1668
  ...overrides
1570
1669
  }
1571
- });
1670
+ }];
1572
1671
  }
1573
1672
  //#endregion
1574
1673
  //#region src/configs/sort.ts
@@ -1586,7 +1685,7 @@ function regexp(options = {}) {
1586
1685
  * @returns The ESLint configuration array with sorting rules.
1587
1686
  */
1588
1687
  function sortPackageJson() {
1589
- return tseslint.config({
1688
+ return [{
1590
1689
  name: "fabdeh/sort/package-json",
1591
1690
  files: ["**/package.json"],
1592
1691
  rules: {
@@ -1681,7 +1780,7 @@ function sortPackageJson() {
1681
1780
  }
1682
1781
  ]
1683
1782
  }
1684
- });
1783
+ }];
1685
1784
  }
1686
1785
  /**
1687
1786
  * Generates a TypeScript ESLint configuration array that enforces sorting rules for `tsconfig.json` files.
@@ -1693,7 +1792,7 @@ function sortPackageJson() {
1693
1792
  * @returns The TypeScript ESLint configuration array with sorting rules.
1694
1793
  */
1695
1794
  function sortTsConfig() {
1696
- return tseslint.config({
1795
+ return [{
1697
1796
  name: "fabdeh/sort/tsconfig-json",
1698
1797
  files: ["**/tsconfig.json", "**/tsconfig.*.json"],
1699
1798
  rules: { "jsonc/sort-keys": [
@@ -1804,7 +1903,52 @@ function sortTsConfig() {
1804
1903
  pathPattern: "^compilerOptions$"
1805
1904
  }
1806
1905
  ] }
1807
- });
1906
+ }];
1907
+ }
1908
+ //#endregion
1909
+ //#region src/configs/stylistic.ts
1910
+ const STYLISTIC_CONFIG_DEFAULT = {
1911
+ semi: true,
1912
+ arrowParens: true,
1913
+ braceStyle: "1tbs",
1914
+ quoteProps: "as-needed"
1915
+ };
1916
+ /**
1917
+ * Generates a stylistic ESLint configuration.
1918
+ *
1919
+ * @param [options] - Optional configuration options to customize the stylistic rules.
1920
+ * @returns A promise that resolves to an array of ESLint configurations.
1921
+ * @example
1922
+ * const config = await stylistic({ semi: false });
1923
+ */
1924
+ async function stylistic(options = {}) {
1925
+ const stylisticOptions = {
1926
+ ...STYLISTIC_CONFIG_DEFAULT,
1927
+ ...typeof options.stylistic === "boolean" ? {} : options.stylistic
1928
+ };
1929
+ const stylisticPlugin = await interopDefault(import("@stylistic/eslint-plugin"));
1930
+ const config = stylisticPlugin.configs.customize(stylisticOptions);
1931
+ return [{
1932
+ name: "fabdeh/stylistic/rules",
1933
+ files: [GLOB_SRC],
1934
+ plugins: { "@stylistic": stylisticPlugin },
1935
+ rules: {
1936
+ ...config.rules,
1937
+ "@stylistic/comma-dangle": ["error", {
1938
+ arrays: "always-multiline",
1939
+ objects: "always-multiline"
1940
+ }],
1941
+ "@stylistic/no-extra-semi": "error",
1942
+ "@stylistic/operator-linebreak": [
1943
+ "error",
1944
+ "after",
1945
+ { overrides: {
1946
+ "?": "before",
1947
+ ":": "before"
1948
+ } }
1949
+ ]
1950
+ }
1951
+ }];
1808
1952
  }
1809
1953
  //#endregion
1810
1954
  //#region src/configs/tailwindcss.ts
@@ -1838,8 +1982,8 @@ async function tailwindcss(options = {}) {
1838
1982
  files = { files: filesGlob ?? [GLOB_SRC, "**/*.htm?(l)"] };
1839
1983
  parserConfigs = [];
1840
1984
  }
1841
- return tseslint.config(...parserConfigs, {
1842
- name: "@fabdeh/tailwindcss/rules",
1985
+ return [...parserConfigs, {
1986
+ name: "fabdeh/tailwindcss/rules",
1843
1987
  ...files,
1844
1988
  plugins: { "better-tailwindcss": betterTailwindcssPlugin },
1845
1989
  settings: { "better-tailwindcss": settings },
@@ -1852,7 +1996,7 @@ async function tailwindcss(options = {}) {
1852
1996
  "better-tailwindcss/no-restricted-classes": "error"
1853
1997
  } : {}
1854
1998
  }
1855
- });
1999
+ }];
1856
2000
  }
1857
2001
  //#endregion
1858
2002
  //#region src/configs/toml.ts
@@ -1869,10 +2013,9 @@ async function tailwindcss(options = {}) {
1869
2013
  async function toml(options = {}) {
1870
2014
  const { files = [GLOB_TOML], overrides = {}, stylistic = true } = options;
1871
2015
  const { indent = 2 } = typeof stylistic === "object" ? stylistic : {};
1872
- const tomlPlugin = await interopDefault(import("eslint-plugin-toml"));
1873
- return tseslint.config({
2016
+ return [{
1874
2017
  name: "fabdeh/toml/setup",
1875
- plugins: { toml: tomlPlugin }
2018
+ plugins: { toml: await interopDefault(import("eslint-plugin-toml")) }
1876
2019
  }, {
1877
2020
  name: "fabdeh/toml/rules",
1878
2021
  files,
@@ -1902,7 +2045,7 @@ async function toml(options = {}) {
1902
2045
  } : {},
1903
2046
  ...overrides
1904
2047
  }
1905
- });
2048
+ }];
1906
2049
  }
1907
2050
  //#endregion
1908
2051
  //#region src/configs/rules-configs/member-ordering.ts
@@ -1950,7 +2093,7 @@ const MEMBER_ORDERING_OPTIONS = { default: [
1950
2093
  * @param options.parserOptions - Options for the TypeScript parser.
1951
2094
  * @param options.overrides - Additional rule overrides.
1952
2095
  * @param isWorkspaceProject - A boolean indicating whether the project is a workspace project. Defaults to false.
1953
- * @returns A TypedConfigArray containing the TypeScript ESLint configuration.
2096
+ * @returns A TypedConfig[] containing the TypeScript ESLint configuration.
1954
2097
  */
1955
2098
  async function typescript(options = {}, isWorkspaceProject = false) {
1956
2099
  const { enableErasableSyntaxOnly = false, overrides = {}, parserOptions = {}, stylistic = true, type = "app", useRelaxedNamingConventionForCamelAndPascalCases = false } = options;
@@ -1964,7 +2107,7 @@ async function typescript(options = {}, isWorkspaceProject = false) {
1964
2107
  }
1965
2108
  const tsconfigRootDir = getWorkspaceRoot(process.cwd(), process.cwd());
1966
2109
  const defaultProject = getTsConfigFileName(tsconfigRootDir);
1967
- return tseslint.config({
2110
+ return [{
1968
2111
  name: `fabdeh/${type}/typescript/setup`,
1969
2112
  files: [GLOB_TS],
1970
2113
  ignores: [`${GLOB_MARKDOWN}/**`],
@@ -2066,7 +2209,7 @@ async function typescript(options = {}, isWorkspaceProject = false) {
2066
2209
  ...erasableSyntaxOnlyRules,
2067
2210
  ...overrides
2068
2211
  }
2069
- });
2212
+ }];
2070
2213
  }
2071
2214
  //#endregion
2072
2215
  //#region src/configs/unicorn.ts
@@ -2077,7 +2220,7 @@ async function typescript(options = {}, isWorkspaceProject = false) {
2077
2220
  * @returns The configuration array for the Unicorn plugin.
2078
2221
  */
2079
2222
  function unicorn(options = {}) {
2080
- return tseslint.config({
2223
+ return [{
2081
2224
  name: "fabdeh/unicorn/rules",
2082
2225
  files: [GLOB_SRC],
2083
2226
  plugins: { unicorn: unicornPlugin },
@@ -2178,7 +2321,7 @@ function unicorn(options = {}) {
2178
2321
  "unicorn/require-post-message-target-origin": "error",
2179
2322
  "unicorn/throw-new-error": "error"
2180
2323
  } }
2181
- });
2324
+ }];
2182
2325
  }
2183
2326
  //#endregion
2184
2327
  //#region src/configs/vitest.ts
@@ -2186,27 +2329,30 @@ function unicorn(options = {}) {
2186
2329
  * Configures and returns an ESLint configuration array for Vitest.
2187
2330
  *
2188
2331
  * @param [options] - The options to customize the configuration.
2332
+ * @param [options.files] - File globs to target (defaults to `GLOB_VITEST`).
2189
2333
  * @param [options.overrides] - Custom rule overrides.
2190
2334
  * @param [options.useJestDom] - Whether to use the `@testing-library/jest-dom` plugin.
2191
2335
  * @param [options.useTestingLibrary] - Whether to use the `@testing-library/angular` plugin.
2336
+ * @param [options.e2eFolderPath] - The path to the e2e folder to ignore (this is only used internally by the `defineConfig` function, not by the user).
2192
2337
  * @returns A promise that resolves to the ESLint configuration array.
2193
2338
  * @example
2194
2339
  * const config = await vitest({
2340
+ * files: ['**\/*.spec.ts'],
2195
2341
  * overrides: {
2196
- * 'no-console': 'warn',
2342
+ * 'vitest/prefer-lowercase-title': 'warn',
2197
2343
  * },
2198
2344
  * useJestDom: true,
2199
2345
  * useTestingLibrary: false,
2200
2346
  * });
2201
2347
  */
2202
2348
  async function vitest(options = {}) {
2203
- const { overrides = {}, useJestDom = isPackageExists("@testing-library/jest-dom"), useTestingLibrary = isPackageExists("@testing-library/angular") } = options;
2349
+ const { files = GLOB_VITEST, overrides = {}, useJestDom = isPackageExists("@testing-library/jest-dom"), useTestingLibrary = isPackageExists("@testing-library/angular"), enableVitestGlobals = true, e2eFolderPath } = options;
2204
2350
  const [vitestPlugin, jestDomPlugin, testingLibraryPlugin] = await Promise.all([
2205
2351
  interopDefault(import("@vitest/eslint-plugin")),
2206
2352
  useJestDom ? interopDefault(import("eslint-plugin-jest-dom")) : Promise.resolve(void 0),
2207
2353
  useTestingLibrary ? interopDefault(import("eslint-plugin-testing-library")) : Promise.resolve(void 0)
2208
2354
  ]);
2209
- return tseslint.config({
2355
+ return [{
2210
2356
  name: "fabdeh/vitest/rules",
2211
2357
  plugins: {
2212
2358
  vitest: vitestPlugin,
@@ -2215,11 +2361,11 @@ async function vitest(options = {}) {
2215
2361
  },
2216
2362
  languageOptions: { globals: {
2217
2363
  ...globals.node,
2218
- ...globals.vitest,
2219
- ...vitestPlugin.environments.env.globals
2364
+ ...enableVitestGlobals ? vitestPlugin.environments.env.globals : {}
2220
2365
  } },
2221
2366
  settings: { vitest: { typecheck: true } },
2222
- files: [...GLOB_TESTS],
2367
+ files,
2368
+ ...e2eFolderPath ? { ignores: [pathToGlobPattern()(e2eFolderPath)] } : {},
2223
2369
  rules: {
2224
2370
  ...vitestPlugin.configs.recommended.rules,
2225
2371
  ...jestDomPlugin?.configs["flat/recommended"].rules,
@@ -2253,7 +2399,7 @@ async function vitest(options = {}) {
2253
2399
  ...getJsDocRules("off", true, "both"),
2254
2400
  ...overrides
2255
2401
  }
2256
- });
2402
+ }];
2257
2403
  }
2258
2404
  //#endregion
2259
2405
  //#region src/configs/yaml.ts
@@ -2266,15 +2412,14 @@ async function vitest(options = {}) {
2266
2412
  * @param options.stylistic - A boolean or object to specify stylistic rules. Defaults to `true`.
2267
2413
  * @param options.stylistic.indent - The number of spaces for indentation or 'tab'. Defaults to `2`.
2268
2414
  * @param options.stylistic.quotes - The type of quotes to use ('single', 'double', or 'backtick'). Defaults to `'single'`.
2269
- * @returns A promise that resolves to a `TypedConfigArray` containing the ESLint configuration.
2415
+ * @returns A promise that resolves to a `TypedConfig[]` containing the ESLint configuration.
2270
2416
  */
2271
2417
  async function yaml(options = {}) {
2272
2418
  const { files = [GLOB_YAML], overrides = {}, stylistic = true } = options;
2273
2419
  const { indent = 2, quotes = "single" } = typeof stylistic === "object" ? stylistic : {};
2274
- const yamlPlugin = await interopDefault(import("eslint-plugin-yml"));
2275
- return tseslint.config({
2420
+ return [{
2276
2421
  name: "fabdeh/yaml/setup",
2277
- plugins: { yaml: yamlPlugin }
2422
+ plugins: { yaml: await interopDefault(import("eslint-plugin-yml")) }
2278
2423
  }, {
2279
2424
  name: "fabdeh/yaml/rules",
2280
2425
  language: "yaml/yaml",
@@ -2306,7 +2451,7 @@ async function yaml(options = {}) {
2306
2451
  } : {},
2307
2452
  ...overrides
2308
2453
  }
2309
- });
2454
+ }];
2310
2455
  }
2311
2456
  //#endregion
2312
2457
  //#region src/shared/constants.ts
@@ -2316,6 +2461,7 @@ const NGRX_PACKAGES = [
2316
2461
  "@ngrx/signals",
2317
2462
  "@ngrx/operators"
2318
2463
  ];
2464
+ const PLAYWRIGHT_PACKAGES = ["@playwright/test", "playwright"];
2319
2465
  const OPTIONS_SYMBOL = Symbol("options");
2320
2466
  //#endregion
2321
2467
  //#region src/factories/project-config.ts
@@ -2327,18 +2473,24 @@ const OPTIONS_SYMBOL = Symbol("options");
2327
2473
  * @param baseConfig - The base configuration to extend from.
2328
2474
  * @param options - Configuration options.
2329
2475
  * @param userConfigs - Additional user configurations that can be awaited.
2330
- * @returns A promise that resolves to a `TypedConfigArray`.
2476
+ * @returns The merged ESLint configurations.
2331
2477
  * @example
2332
2478
  * ```typescript
2479
+ * import { defineProjectConfig } from '@fabdeh/eslint-config';
2480
+ *
2333
2481
  * import baseConfig from '../../eslint.base.js';
2334
2482
  *
2335
- * const config = await defineProjectConfig(baseConfig, { vitest: true, typescript: { parserOptions: { project: './tsconfig.json' } } });
2483
+ * export default defineProjectConfig(baseConfig, { vitest: true, typescript: { parserOptions: { project: './tsconfig.json' } } });
2336
2484
  * ```
2337
2485
  */
2338
- async function defineProjectConfig(baseConfig, options = {}, ...userConfigs) {
2339
- const resolvedBaseConfig = await baseConfig;
2340
- const workspaceOptions = resolvedBaseConfig[OPTIONS_SYMBOL];
2486
+ function defineProjectConfig(baseConfig, options = {}, ...userConfigs) {
2487
+ const workspaceOptions = baseConfig[OPTIONS_SYMBOL];
2341
2488
  const { angular: enableAngular = isPackageExists("@angular/core"), jsdoc: enableJsdoc = options.type === "lib", ngrx: enableNgrx = NGRX_PACKAGES.some((p) => isPackageExists(p)), tailwindcss: enableTailwind = false, typescript: typescriptOptions, vitest: enableVitest = isPackageExists("vitest") } = options;
2489
+ let isInEditor = workspaceOptions.isInEditor;
2490
+ if (isInEditor === void 0) {
2491
+ isInEditor = isInEditorEnv();
2492
+ if (isInEditor) console.log("[@fabdeh/eslint-config] Detected running in editor, some rules are disabled.");
2493
+ }
2342
2494
  if (enableNgrx && !enableAngular) throw new Error("NgRx rules can only be enabled if Angular rules are also enabled.");
2343
2495
  const configs = [];
2344
2496
  const stylisticOptions = workspaceOptions.stylistic === false ? false : typeof workspaceOptions.stylistic === "object" ? workspaceOptions.stylistic : {};
@@ -2354,10 +2506,6 @@ async function defineProjectConfig(baseConfig, options = {}, ...userConfigs) {
2354
2506
  if (enableAngular) {
2355
2507
  const angularOptions = resolveSubOptions(options, "angular");
2356
2508
  configs.push(angular(angularOptions));
2357
- if (workspaceOptions.formatters) {
2358
- const htmlFormatters = resolvedBaseConfig.find((c) => c.name === "fabdeh/formatter/html");
2359
- if (htmlFormatters) delete htmlFormatters.languageOptions;
2360
- }
2361
2509
  }
2362
2510
  if (enableNgrx) {
2363
2511
  const ngrxOptions = resolveSubOptions(options, "ngrx");
@@ -2378,7 +2526,10 @@ async function defineProjectConfig(baseConfig, options = {}, ...userConfigs) {
2378
2526
  stylistic: stylisticOptions
2379
2527
  }));
2380
2528
  }
2381
- return tseslint.config(...resolvedBaseConfig, ...await Promise.all(configs), ...await Promise.all(userConfigs));
2529
+ let composer = new FlatConfigComposer();
2530
+ composer = composer.append(baseConfig, ...configs, ...userConfigs);
2531
+ if (isInEditor) composer = composer.disableRulesFix(["prefer-const", "unused-imports/no-unused-imports"], { builtinRules: () => import("eslint/use-at-your-own-risk").then((m) => m.builtinRules) });
2532
+ return composer;
2382
2533
  }
2383
2534
  //#endregion
2384
2535
  //#region src/factories/standard-config.ts
@@ -2388,26 +2539,36 @@ async function defineProjectConfig(baseConfig, options = {}, ...userConfigs) {
2388
2539
  *
2389
2540
  * @param options - Configuration options.
2390
2541
  * @param userConfigs - Additional user configurations that can be awaited.
2391
- * @returns A promise that resolves to a `ConfigArrayWithOptions`.
2542
+ * @returns The merged ESLint configurations.
2392
2543
  * @example
2393
2544
  * ```typescript
2394
- * const config = await defineConfig({ vitest: true, typescript: { parserOptions: { project: './tsconfig.json' } } });
2545
+ * import { defineConfig } from '@fabdeh/eslint-config';
2546
+ *
2547
+ * export default defineConfig({ vitest: true, typescript: { parserOptions: { project: './tsconfig.json' } } });
2395
2548
  * ```
2396
2549
  */
2397
- async function defineConfig(options = {}, ...userConfigs) {
2398
- 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;
2550
+ function defineConfig(options = {}, ...userConfigs) {
2551
+ 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 = !!findUpSync("pnpm-workspace.yaml"), regexp: enableRegexp = true, tailwindcss: enableTailwind = false, typescript: enableTypescript = isPackageExists("typescript"), unicorn: enableUnicorn = true, vitest: enableVitest = isPackageExists("vitest") } = options;
2552
+ let isInEditor = options.isInEditor;
2553
+ if (isInEditor === void 0) {
2554
+ isInEditor = isInEditorEnv();
2555
+ if (isInEditor) console.log("[@fabdeh/eslint-config] Detected running in editor, some rules are disabled.");
2556
+ }
2399
2557
  if (enableNgrx && !enableAngular) throw new Error("NgRx rules can only be enabled if Angular rules are also enabled.");
2400
2558
  const stylisticOptions = options.stylistic === false ? false : typeof options.stylistic === "object" ? options.stylistic : {};
2401
2559
  const configs = [];
2402
- if (enableGitignore) if (typeof enableGitignore === "object") configs.push(interopDefault(import("eslint-config-flat-gitignore")).then((r) => tseslint.config(r({
2560
+ if (enableGitignore) if (typeof enableGitignore === "object") configs.push(interopDefault(import("eslint-config-flat-gitignore")).then((r) => [r({
2403
2561
  name: "fabdeh/gitignore",
2404
2562
  ...enableGitignore
2405
- }))));
2406
- else configs.push(interopDefault(import("eslint-config-flat-gitignore")).then((r) => tseslint.config(r({
2563
+ })]));
2564
+ else configs.push(interopDefault(import("eslint-config-flat-gitignore")).then((r) => [r({
2407
2565
  name: "fabdeh/gitignore",
2408
2566
  strict: true
2409
- }))));
2410
- configs.push(ignores(options.ignores), javascript({ overrides: options.javascript?.overrides }), comments(), node(), imports({ stylistic: stylisticOptions }), perfectionist());
2567
+ })]));
2568
+ configs.push(ignores(options.ignores), javascript({
2569
+ isInEditor,
2570
+ overrides: options.javascript?.overrides
2571
+ }), comments(), node(), imports({ stylistic: stylisticOptions }), perfectionist());
2411
2572
  if (enableJsdoc) configs.push(jsdoc({ stylistic: stylisticOptions }));
2412
2573
  if (enableUnicorn) {
2413
2574
  const unicornOptions = resolveSubOptions(options, "unicorn");
@@ -2438,9 +2599,21 @@ async function defineConfig(options = {}, ...userConfigs) {
2438
2599
  useRelaxedNamingConventionForCamelAndPascalCases: typescriptOptions.useRelaxedNamingConventionForCamelAndPascalCases
2439
2600
  }));
2440
2601
  }
2602
+ let e2eFolderPath;
2603
+ if (enablePlaywright) {
2604
+ const playwrightOptions = resolveSubOptions(options, "playwright");
2605
+ e2eFolderPath = playwrightOptions.e2eFolderPath ?? getPlaywrightDirectory() ?? "e2e";
2606
+ configs.push(playwright({
2607
+ ...playwrightOptions,
2608
+ e2eFolderPath
2609
+ }));
2610
+ }
2441
2611
  if (enableVitest) {
2442
2612
  const vitestOptions = resolveSubOptions(options, "vitest");
2443
- configs.push(vitest(vitestOptions));
2613
+ configs.push(vitest({
2614
+ ...vitestOptions,
2615
+ e2eFolderPath
2616
+ }));
2444
2617
  }
2445
2618
  if (enableTailwind) {
2446
2619
  const tailwindcssOptions = resolveSubOptions(options, "tailwindcss");
@@ -2456,7 +2629,15 @@ async function defineConfig(options = {}, ...userConfigs) {
2456
2629
  stylistic: stylisticOptions
2457
2630
  }), sortPackageJson(), sortTsConfig());
2458
2631
  }
2459
- if (enableCatalogs) configs.push(pnpm());
2632
+ if (enableCatalogs) {
2633
+ const pnpmOptions = resolveSubOptions(options, "pnpm");
2634
+ configs.push(pnpm({
2635
+ isInEditor,
2636
+ json: options.jsonc !== false,
2637
+ yaml: options.yaml !== false,
2638
+ ...pnpmOptions
2639
+ }));
2640
+ }
2460
2641
  if (options.yaml ?? true) {
2461
2642
  const yamlOptions = resolveSubOptions(options, "yaml");
2462
2643
  configs.push(yaml({
@@ -2475,8 +2656,10 @@ async function defineConfig(options = {}, ...userConfigs) {
2475
2656
  const markdownOptions = resolveSubOptions(options, "markdown");
2476
2657
  configs.push(markdown(markdownOptions));
2477
2658
  }
2478
- if (options.formatters) configs.push(formatters(options.formatters, typeof stylisticOptions === "boolean" ? {} : stylisticOptions, Boolean(enableAngular)));
2479
- return tseslint.config(...await Promise.all(configs), ...await Promise.all(userConfigs));
2659
+ let composer = new FlatConfigComposer();
2660
+ composer = composer.append(...configs, ...userConfigs);
2661
+ if (isInEditor) composer = composer.disableRulesFix(["prefer-const", "unused-imports/no-unused-imports"], { builtinRules: () => import("eslint/use-at-your-own-risk").then((m) => m.builtinRules) });
2662
+ return composer;
2480
2663
  }
2481
2664
  //#endregion
2482
2665
  //#region src/factories/workspace-config.ts
@@ -2487,25 +2670,35 @@ async function defineConfig(options = {}, ...userConfigs) {
2487
2670
  *
2488
2671
  * @param options - Configuration options.
2489
2672
  * @param userConfigs - Additional user configurations that can be awaited.
2490
- * @returns A promise that resolves to a `ConfigArrayWithOptions`.
2673
+ * @returns The merged ESLint configurations.
2491
2674
  * @example
2492
2675
  * ```typescript
2493
- * const config = await defineWorkspaceConfig({ vitest: true, typescript: { parserOptions: { project: './tsconfig.json' } } });
2676
+ * import { defineWorkspaceConfig } from '@fabdeh/eslint-config';
2677
+ *
2678
+ * export default defineWorkspaceConfig({ typescript: { parserOptions: { project: './tsconfig.json' } } });
2494
2679
  * ```
2495
2680
  */
2496
- async function defineWorkspaceConfig(options = {}, ...userConfigs) {
2497
- const { gitignore: enableGitignore = true, pnpm: enableCatalogs = false, regexp: enableRegexp = true, typescript: enableTypescript = isPackageExists("typescript"), unicorn: enableUnicorn = true } = options;
2681
+ function defineWorkspaceConfig(options = {}, ...userConfigs) {
2682
+ const { gitignore: enableGitignore = true, pnpm: enableCatalogs = !!findUpSync("pnpm-workspace.yaml"), regexp: enableRegexp = true, typescript: enableTypescript = isPackageExists("typescript"), unicorn: enableUnicorn = true } = options;
2683
+ let isInEditor = options.isInEditor;
2684
+ if (isInEditor === void 0) {
2685
+ isInEditor = isInEditorEnv();
2686
+ if (isInEditor) console.log("[@fabdeh/eslint-config] Detected running in editor, some rules are disabled.");
2687
+ }
2498
2688
  const stylisticOptions = options.stylistic === false ? false : typeof options.stylistic === "object" ? options.stylistic : {};
2499
2689
  const configs = [];
2500
- if (enableGitignore) if (typeof enableGitignore === "object") configs.push(interopDefault(import("eslint-config-flat-gitignore")).then((r) => tseslint.config(r({
2690
+ if (enableGitignore) if (typeof enableGitignore === "object") configs.push(interopDefault(import("eslint-config-flat-gitignore")).then((r) => [r({
2501
2691
  name: "fabdeh/gitignore",
2502
2692
  ...enableGitignore
2503
- }))));
2504
- else configs.push(interopDefault(import("eslint-config-flat-gitignore")).then((r) => tseslint.config(r({
2693
+ })]));
2694
+ else configs.push(interopDefault(import("eslint-config-flat-gitignore")).then((r) => [r({
2505
2695
  name: "fabdeh/gitignore",
2506
2696
  strict: true
2507
- }))));
2508
- configs.push(ignores(options.ignores, "workspace"), javascript({ overrides: options.javascript?.overrides }), comments(), node(), imports({ stylistic: stylisticOptions }), perfectionist());
2697
+ })]));
2698
+ configs.push(ignores(options.ignores, "workspace"), javascript({
2699
+ isInEditor,
2700
+ overrides: options.javascript?.overrides
2701
+ }), comments(), node(), imports({ stylistic: stylisticOptions }), perfectionist());
2509
2702
  if (enableUnicorn) {
2510
2703
  const unicornOptions = resolveSubOptions(options, "unicorn");
2511
2704
  configs.push(unicorn(unicornOptions));
@@ -2530,7 +2723,15 @@ async function defineWorkspaceConfig(options = {}, ...userConfigs) {
2530
2723
  stylistic: stylisticOptions
2531
2724
  }), sortPackageJson(), sortTsConfig());
2532
2725
  }
2533
- if (enableCatalogs) configs.push(pnpm());
2726
+ if (enableCatalogs) {
2727
+ const pnpmOptions = resolveSubOptions(options, "pnpm");
2728
+ configs.push(pnpm({
2729
+ isInEditor,
2730
+ json: options.jsonc !== false,
2731
+ yaml: options.yaml !== false,
2732
+ ...pnpmOptions
2733
+ }));
2734
+ }
2534
2735
  if (options.yaml ?? true) {
2535
2736
  const yamlOptions = resolveSubOptions(options, "yaml");
2536
2737
  configs.push(yaml({
@@ -2549,10 +2750,12 @@ async function defineWorkspaceConfig(options = {}, ...userConfigs) {
2549
2750
  const markdownOptions = resolveSubOptions(options, "markdown");
2550
2751
  configs.push(markdown(markdownOptions));
2551
2752
  }
2552
- if (options.formatters) configs.push(formatters(options.formatters, typeof stylisticOptions === "boolean" ? {} : stylisticOptions, false));
2553
- const eslintConfigWithOptions = tseslint.config(...await Promise.all(configs), ...await Promise.all(userConfigs));
2554
- eslintConfigWithOptions[OPTIONS_SYMBOL] = options;
2555
- return eslintConfigWithOptions;
2753
+ let composer = new FlatConfigComposer();
2754
+ composer = composer.append(...configs, ...userConfigs);
2755
+ if (isInEditor) composer = composer.disableRulesFix(["prefer-const", "unused-imports/no-unused-imports"], { builtinRules: () => import("eslint/use-at-your-own-risk").then((m) => m.builtinRules) });
2756
+ const composerWithOptions = composer;
2757
+ composerWithOptions[OPTIONS_SYMBOL] = options;
2758
+ return composerWithOptions;
2556
2759
  }
2557
2760
  //#endregion
2558
- 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 };
2761
+ export { CONFIG_PRESET_FULL_OFF, CONFIG_PRESET_FULL_ON, 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, isInEditorEnv, isInGitHooksOrLintStaged, isPackageInScope, javascript, jsdoc, jsonc, markdown, ngrx, node, pathToGlobPattern, perfectionist, playwright, pnpm, regexp, resolveSubOptions, sortPackageJson, sortTsConfig, stylistic, tailwindcss, toml, typescript, unicorn, vitest, yaml };