@code-pushup/eslint-plugin 0.53.1 → 0.55.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.
package/README.md CHANGED
@@ -72,6 +72,12 @@ Detected ESLint rules are mapped to Code PushUp audits. Audit reports are calcul
72
72
  };
73
73
  ```
74
74
 
75
+ You can also exclude specific projects if needed by passing their names in the `exclude` option:
76
+
77
+ ```js
78
+ await eslintConfigFromAllNxProjects({ exclude: ['server'] });
79
+ ```
80
+
75
81
  - If you wish to target a specific project along with other projects it depends on, use the `eslintConfigFromNxProjectAndDeps` helper and pass in in your project name:
76
82
 
77
83
  ```js
package/bin.js CHANGED
@@ -1,6 +1,6 @@
1
1
  // packages/plugin-eslint/src/lib/runner/index.ts
2
- import { writeFile as writeFile2 } from "node:fs/promises";
3
- import { dirname, join as join3 } from "node:path";
2
+ import { writeFile } from "node:fs/promises";
3
+ import { dirname as dirname2, join as join2 } from "node:path";
4
4
 
5
5
  // packages/models/src/lib/implementation/schemas.ts
6
6
  import { MATERIAL_ICONS } from "vscode-material-icons";
@@ -33,7 +33,7 @@ function exists(value) {
33
33
  return value != null;
34
34
  }
35
35
  function getMissingRefsForCategories(categories, plugins) {
36
- if (categories.length === 0) {
36
+ if (!categories || categories.length === 0) {
37
37
  return false;
38
38
  }
39
39
  const auditRefsFromCategory = categories.flatMap(
@@ -110,7 +110,6 @@ var fileNameSchema = z.string().trim().regex(filenameRegex, {
110
110
  message: `The filename has to be valid`
111
111
  }).min(1, { message: "file name is invalid" });
112
112
  var positiveIntSchema = z.number().int().positive();
113
- var nonnegativeIntSchema = z.number().int().nonnegative();
114
113
  var nonnegativeNumberSchema = z.number().nonnegative();
115
114
  function packageVersionSchema(options) {
116
115
  const { versionDescription = "NPM version of the package", required } = options ?? {};
@@ -533,12 +532,9 @@ var unrefinedCoreConfigSchema = z14.object({
533
532
  var coreConfigSchema = refineCoreConfig(unrefinedCoreConfigSchema);
534
533
  function refineCoreConfig(schema) {
535
534
  return schema.refine(
536
- (coreCfg) => !getMissingRefsForCategories(coreCfg.categories ?? [], coreCfg.plugins),
537
- (coreCfg) => ({
538
- message: missingRefsForCategoriesErrorMsg(
539
- coreCfg.categories ?? [],
540
- coreCfg.plugins
541
- )
535
+ ({ categories, plugins }) => !getMissingRefsForCategories(categories, plugins),
536
+ ({ categories, plugins }) => ({
537
+ message: missingRefsForCategoriesErrorMsg(categories, plugins)
542
538
  })
543
539
  );
544
540
  }
@@ -590,19 +586,16 @@ var reportSchema = packageVersionSchema({
590
586
  ).merge(
591
587
  z15.object(
592
588
  {
593
- categories: z15.array(categoryConfigSchema),
594
589
  plugins: z15.array(pluginReportSchema).min(1),
590
+ categories: z15.array(categoryConfigSchema).optional(),
595
591
  commit: commitSchema.describe("Git commit for which report was collected").nullable()
596
592
  },
597
593
  { description: "Collect output data" }
598
594
  )
599
595
  ).refine(
600
- (report) => !getMissingRefsForCategories(report.categories, report.plugins),
601
- (report) => ({
602
- message: missingRefsForCategoriesErrorMsg(
603
- report.categories,
604
- report.plugins
605
- )
596
+ ({ categories, plugins }) => !getMissingRefsForCategories(categories, plugins),
597
+ ({ categories, plugins }) => ({
598
+ message: missingRefsForCategoriesErrorMsg(categories, plugins)
606
599
  })
607
600
  );
608
601
 
@@ -654,7 +647,7 @@ var auditDiffSchema = scorableWithPluginDiffSchema.merge(
654
647
  z16.object({
655
648
  values: makeComparisonSchema(auditValueSchema).merge(
656
649
  z16.object({
657
- diff: z16.number().int().describe("Value change (`values.after - values.before`)")
650
+ diff: z16.number().describe("Value change (`values.after - values.before`)")
658
651
  })
659
652
  ).describe("Audit `value` comparison"),
660
653
  displayValues: makeComparisonSchema(auditDisplayValueSchema).describe(
@@ -747,6 +740,7 @@ function executeProcess(cfg) {
747
740
  return new Promise((resolve, reject) => {
748
741
  const spawnedProcess = spawn(command, args ?? [], {
749
742
  shell: true,
743
+ windowsHide: true,
750
744
  ...options
751
745
  });
752
746
  let stdout = "";
@@ -780,7 +774,7 @@ function executeProcess(cfg) {
780
774
  import { bold, gray } from "ansis";
781
775
  import { bundleRequire } from "bundle-require";
782
776
  import { mkdir, readFile, readdir, rm, stat } from "node:fs/promises";
783
- import { join } from "node:path";
777
+ import { dirname, join } from "node:path";
784
778
 
785
779
  // packages/utils/src/lib/formatting.ts
786
780
  function slugify(text) {
@@ -934,22 +928,24 @@ import { MarkdownDocument as MarkdownDocument4, md as md5 } from "build-md";
934
928
  import { bold as bold4, cyan, cyanBright, green as green2, red } from "ansis";
935
929
 
936
930
  // packages/plugin-eslint/src/lib/runner/lint.ts
937
- import { rm as rm2, writeFile } from "node:fs/promises";
938
931
  import { platform } from "node:os";
939
- import { join as join2 } from "node:path";
940
932
 
941
933
  // packages/plugin-eslint/src/lib/setup.ts
942
934
  import { ESLint } from "eslint";
943
- function setupESLint(eslintrc) {
944
- return new ESLint({
945
- ...typeof eslintrc === "string" && { overrideConfigFile: eslintrc },
946
- ...typeof eslintrc === "object" && {
947
- baseConfig: eslintrc,
948
- useEslintrc: false
949
- },
935
+ async function setupESLint(eslintrc) {
936
+ const eslintConstructor = await loadESLint();
937
+ return new eslintConstructor({
938
+ overrideConfigFile: eslintrc,
950
939
  errorOnUnmatchedPattern: false
951
940
  });
952
941
  }
942
+ async function loadESLint() {
943
+ const eslint = await import("eslint");
944
+ if ("loadESLint" in eslint && typeof eslint.loadESLint === "function") {
945
+ return await eslint.loadESLint();
946
+ }
947
+ return ESLint;
948
+ }
953
949
 
954
950
  // packages/plugin-eslint/src/lib/runner/lint.ts
955
951
  async function lint({
@@ -957,37 +953,35 @@ async function lint({
957
953
  patterns
958
954
  }) {
959
955
  const results = await executeLint({ eslintrc, patterns });
960
- const ruleOptionsPerFile = await loadRuleOptionsPerFile(eslintrc, results);
956
+ const eslint = await setupESLint(eslintrc);
957
+ const ruleOptionsPerFile = await loadRuleOptionsPerFile(eslint, results);
961
958
  return { results, ruleOptionsPerFile };
962
959
  }
963
- function executeLint({
960
+ async function executeLint({
964
961
  eslintrc,
965
962
  patterns
966
963
  }) {
967
- return withConfig(eslintrc, async (configPath) => {
968
- const { stdout } = await executeProcess({
969
- command: "npx",
970
- args: [
971
- "eslint",
972
- ...configPath ? [`--config=${filePathToCliArg(configPath)}`] : [],
973
- ...typeof eslintrc === "object" ? ["--no-eslintrc"] : [],
974
- "--no-error-on-unmatched-pattern",
975
- "--format=json",
976
- ...toArray(patterns).map(
977
- (pattern) => (
978
- // globs need to be escaped on Unix
979
- platform() === "win32" ? pattern : `'${pattern}'`
980
- )
964
+ const { stdout } = await executeProcess({
965
+ command: "npx",
966
+ args: [
967
+ "eslint",
968
+ ...eslintrc ? [`--config=${filePathToCliArg(eslintrc)}`] : [],
969
+ ...typeof eslintrc === "object" ? ["--no-eslintrc"] : [],
970
+ "--no-error-on-unmatched-pattern",
971
+ "--format=json",
972
+ ...toArray(patterns).map(
973
+ (pattern) => (
974
+ // globs need to be escaped on Unix
975
+ platform() === "win32" ? pattern : `'${pattern}'`
981
976
  )
982
- ],
983
- ignoreExitCode: true,
984
- cwd: process.cwd()
985
- });
986
- return JSON.parse(stdout);
977
+ )
978
+ ],
979
+ ignoreExitCode: true,
980
+ cwd: process.cwd()
987
981
  });
982
+ return JSON.parse(stdout);
988
983
  }
989
- function loadRuleOptionsPerFile(eslintrc, results) {
990
- const eslint = setupESLint(eslintrc);
984
+ function loadRuleOptionsPerFile(eslint, results) {
991
985
  return results.reduce(async (acc, { filePath, messages }) => {
992
986
  const filesMap = await acc;
993
987
  const config = await eslint.calculateConfigForFile(
@@ -1011,24 +1005,6 @@ function loadRuleOptionsPerFile(eslintrc, results) {
1011
1005
  };
1012
1006
  }, Promise.resolve({}));
1013
1007
  }
1014
- async function withConfig(eslintrc, fn) {
1015
- if (typeof eslintrc !== "object") {
1016
- return fn(eslintrc);
1017
- }
1018
- const configPath = generateTempConfigPath();
1019
- await writeFile(configPath, JSON.stringify(eslintrc));
1020
- try {
1021
- return await fn(configPath);
1022
- } finally {
1023
- await rm2(configPath);
1024
- }
1025
- }
1026
- function generateTempConfigPath() {
1027
- return join2(
1028
- process.cwd(),
1029
- `.eslintrc.${Math.random().toString().slice(2)}.json`
1030
- );
1031
- }
1032
1008
 
1033
1009
  // packages/plugin-eslint/src/lib/meta/hash.ts
1034
1010
  import { createHash } from "node:crypto";
@@ -1122,16 +1098,14 @@ function convertSeverity(severity) {
1122
1098
 
1123
1099
  // packages/plugin-eslint/src/lib/runner/index.ts
1124
1100
  var WORKDIR = pluginWorkDir("eslint");
1125
- var RUNNER_OUTPUT_PATH = join3(WORKDIR, "runner-output.json");
1126
- var PLUGIN_CONFIG_PATH = join3(
1101
+ var RUNNER_OUTPUT_PATH = join2(WORKDIR, "runner-output.json");
1102
+ var PLUGIN_CONFIG_PATH = join2(
1127
1103
  process.cwd(),
1128
1104
  WORKDIR,
1129
1105
  "plugin-config.json"
1130
1106
  );
1131
1107
  async function executeRunner() {
1132
- const { slugs, targets } = await readJsonFile(
1133
- PLUGIN_CONFIG_PATH
1134
- );
1108
+ const { slugs, targets } = await readJsonFile(PLUGIN_CONFIG_PATH);
1135
1109
  const linterOutputs = await targets.reduce(
1136
1110
  async (acc, target) => [...await acc, await lint(target)],
1137
1111
  Promise.resolve([])
@@ -1147,8 +1121,8 @@ async function executeRunner() {
1147
1121
  details: { issues: [] }
1148
1122
  }
1149
1123
  );
1150
- await ensureDirectoryExists(dirname(RUNNER_OUTPUT_PATH));
1151
- await writeFile2(RUNNER_OUTPUT_PATH, JSON.stringify(audits));
1124
+ await ensureDirectoryExists(dirname2(RUNNER_OUTPUT_PATH));
1125
+ await writeFile(RUNNER_OUTPUT_PATH, JSON.stringify(audits));
1152
1126
  }
1153
1127
 
1154
1128
  // packages/plugin-eslint/src/bin.ts
package/index.js CHANGED
@@ -1,10 +1,10 @@
1
1
  // packages/plugin-eslint/src/lib/eslint-plugin.ts
2
- import { dirname as dirname2, join as join3 } from "node:path";
2
+ import { dirname as dirname3, join as join4 } from "node:path";
3
3
  import { fileURLToPath } from "node:url";
4
4
 
5
5
  // packages/plugin-eslint/package.json
6
6
  var name = "@code-pushup/eslint-plugin";
7
- var version = "0.53.1";
7
+ var version = "0.55.0";
8
8
 
9
9
  // packages/plugin-eslint/src/lib/config.ts
10
10
  import { z as z17 } from "zod";
@@ -40,7 +40,7 @@ function exists(value) {
40
40
  return value != null;
41
41
  }
42
42
  function getMissingRefsForCategories(categories, plugins) {
43
- if (categories.length === 0) {
43
+ if (!categories || categories.length === 0) {
44
44
  return false;
45
45
  }
46
46
  const auditRefsFromCategory = categories.flatMap(
@@ -117,7 +117,6 @@ var fileNameSchema = z.string().trim().regex(filenameRegex, {
117
117
  message: `The filename has to be valid`
118
118
  }).min(1, { message: "file name is invalid" });
119
119
  var positiveIntSchema = z.number().int().positive();
120
- var nonnegativeIntSchema = z.number().int().nonnegative();
121
120
  var nonnegativeNumberSchema = z.number().nonnegative();
122
121
  function packageVersionSchema(options) {
123
122
  const { versionDescription = "NPM version of the package", required } = options ?? {};
@@ -540,12 +539,9 @@ var unrefinedCoreConfigSchema = z14.object({
540
539
  var coreConfigSchema = refineCoreConfig(unrefinedCoreConfigSchema);
541
540
  function refineCoreConfig(schema) {
542
541
  return schema.refine(
543
- (coreCfg) => !getMissingRefsForCategories(coreCfg.categories ?? [], coreCfg.plugins),
544
- (coreCfg) => ({
545
- message: missingRefsForCategoriesErrorMsg(
546
- coreCfg.categories ?? [],
547
- coreCfg.plugins
548
- )
542
+ ({ categories, plugins }) => !getMissingRefsForCategories(categories, plugins),
543
+ ({ categories, plugins }) => ({
544
+ message: missingRefsForCategoriesErrorMsg(categories, plugins)
549
545
  })
550
546
  );
551
547
  }
@@ -597,19 +593,16 @@ var reportSchema = packageVersionSchema({
597
593
  ).merge(
598
594
  z15.object(
599
595
  {
600
- categories: z15.array(categoryConfigSchema),
601
596
  plugins: z15.array(pluginReportSchema).min(1),
597
+ categories: z15.array(categoryConfigSchema).optional(),
602
598
  commit: commitSchema.describe("Git commit for which report was collected").nullable()
603
599
  },
604
600
  { description: "Collect output data" }
605
601
  )
606
602
  ).refine(
607
- (report) => !getMissingRefsForCategories(report.categories, report.plugins),
608
- (report) => ({
609
- message: missingRefsForCategoriesErrorMsg(
610
- report.categories,
611
- report.plugins
612
- )
603
+ ({ categories, plugins }) => !getMissingRefsForCategories(categories, plugins),
604
+ ({ categories, plugins }) => ({
605
+ message: missingRefsForCategoriesErrorMsg(categories, plugins)
613
606
  })
614
607
  );
615
608
 
@@ -661,7 +654,7 @@ var auditDiffSchema = scorableWithPluginDiffSchema.merge(
661
654
  z16.object({
662
655
  values: makeComparisonSchema(auditValueSchema).merge(
663
656
  z16.object({
664
- diff: z16.number().int().describe("Value change (`values.after - values.before`)")
657
+ diff: z16.number().describe("Value change (`values.after - values.before`)")
665
658
  })
666
659
  ).describe("Audit `value` comparison"),
667
660
  displayValues: makeComparisonSchema(auditDisplayValueSchema).describe(
@@ -720,7 +713,7 @@ var TERMINAL_WIDTH = 80;
720
713
  import { bold, gray } from "ansis";
721
714
  import { bundleRequire } from "bundle-require";
722
715
  import { mkdir, readFile, readdir, rm, stat } from "node:fs/promises";
723
- import { join } from "node:path";
716
+ import { dirname, join } from "node:path";
724
717
 
725
718
  // packages/utils/src/lib/formatting.ts
726
719
  function slugify(text) {
@@ -803,6 +796,16 @@ async function ensureDirectoryExists(baseDir) {
803
796
  function pluginWorkDir(slug) {
804
797
  return join("node_modules", ".code-pushup", slug);
805
798
  }
799
+ async function findNearestFile(fileNames, cwd = process.cwd()) {
800
+ for (let directory = cwd; directory !== dirname(directory); directory = dirname(directory)) {
801
+ for (const file of fileNames) {
802
+ if (await fileExists(join(directory, file))) {
803
+ return join(directory, file);
804
+ }
805
+ }
806
+ }
807
+ return void 0;
808
+ }
806
809
  function filePathToCliArg(path) {
807
810
  return `"${path}"`;
808
811
  }
@@ -859,15 +862,7 @@ import { bold as bold4, cyan, cyanBright, green as green2, red } from "ansis";
859
862
  var patternsSchema = z17.union([z17.string(), z17.array(z17.string()).min(1)], {
860
863
  description: "Lint target files. May contain file paths, directory paths or glob patterns"
861
864
  });
862
- var eslintrcSchema = z17.union(
863
- [
864
- z17.string({ description: "Path to ESLint config file" }),
865
- z17.record(z17.string(), z17.unknown(), {
866
- description: "ESLint config object"
867
- })
868
- ],
869
- { description: "ESLint config as file path or inline object" }
870
- );
865
+ var eslintrcSchema = z17.string({ description: "Path to ESLint config file" });
871
866
  var eslintTargetObjectSchema = z17.object({
872
867
  eslintrc: eslintrcSchema.optional(),
873
868
  patterns: patternsSchema
@@ -890,80 +885,16 @@ function jsonHash(data, bytes = 8) {
890
885
  return createHash("shake256", { outputLength: bytes }).update(JSON.stringify(data) || "null").digest("hex");
891
886
  }
892
887
 
893
- // packages/plugin-eslint/src/lib/setup.ts
894
- import { ESLint } from "eslint";
895
- function setupESLint(eslintrc) {
896
- return new ESLint({
897
- ...typeof eslintrc === "string" && { overrideConfigFile: eslintrc },
898
- ...typeof eslintrc === "object" && {
899
- baseConfig: eslintrc,
900
- useEslintrc: false
901
- },
902
- errorOnUnmatchedPattern: false
903
- });
904
- }
905
-
906
- // packages/plugin-eslint/src/lib/meta/rules.ts
907
- async function listRules(targets) {
908
- const rulesMap = await targets.reduce(async (acc, { eslintrc, patterns }) => {
909
- const eslint = setupESLint(eslintrc);
910
- const prev = await acc;
911
- const curr = await loadRulesMap(eslint, patterns);
912
- return mergeRulesMaps(prev, curr);
913
- }, Promise.resolve({}));
914
- return Object.values(rulesMap).flatMap(Object.values);
915
- }
916
- async function loadRulesMap(eslint, patterns) {
917
- const configs = await toArray(patterns).reduce(
918
- async (acc, pattern) => [
919
- ...await acc,
920
- await eslint.calculateConfigForFile(pattern)
921
- ],
922
- Promise.resolve([])
923
- );
924
- const rulesIds = distinct(
925
- configs.flatMap((config) => Object.keys(config.rules ?? {}))
926
- );
927
- const rulesMeta = eslint.getRulesMetaForResults([
928
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
929
- {
930
- messages: rulesIds.map((ruleId) => ({ ruleId })),
931
- suppressedMessages: []
932
- }
933
- ]);
934
- return configs.flatMap((config) => Object.entries(config.rules ?? {})).filter(([, ruleEntry]) => ruleEntry != null && !isRuleOff(ruleEntry)).reduce((acc, [ruleId, ruleEntry]) => {
935
- const meta = rulesMeta[ruleId];
936
- if (!meta) {
937
- ui().logger.warning(`Metadata not found for ESLint rule ${ruleId}`);
938
- return acc;
939
- }
940
- const options = toArray(ruleEntry).slice(1);
941
- const optionsHash = jsonHash(options);
942
- const ruleData = {
943
- ruleId,
944
- meta,
945
- options
946
- };
947
- return {
948
- ...acc,
949
- [ruleId]: {
950
- ...acc[ruleId],
951
- [optionsHash]: ruleData
952
- }
953
- };
954
- }, {});
955
- }
956
- function mergeRulesMaps(prev, curr) {
957
- return Object.entries(curr).reduce(
958
- (acc, [ruleId, ruleVariants]) => ({
959
- ...acc,
960
- [ruleId]: {
961
- ...acc[ruleId],
962
- ...ruleVariants
963
- }
964
- }),
965
- prev
966
- );
888
+ // packages/plugin-eslint/src/lib/meta/parse.ts
889
+ function parseRuleId(ruleId) {
890
+ const i = ruleId.lastIndexOf("/");
891
+ if (i < 0) {
892
+ return { name: ruleId };
893
+ }
894
+ return {
895
+ plugin: ruleId.slice(0, i),
896
+ name: ruleId.slice(i + 1)
897
+ };
967
898
  }
968
899
  function isRuleOff(entry) {
969
900
  const level = Array.isArray(entry) ? entry[0] : entry;
@@ -978,15 +909,8 @@ function isRuleOff(entry) {
978
909
  return false;
979
910
  }
980
911
  }
981
- function parseRuleId(ruleId) {
982
- const i = ruleId.lastIndexOf("/");
983
- if (i < 0) {
984
- return { name: ruleId };
985
- }
986
- return {
987
- plugin: ruleId.slice(0, i),
988
- name: ruleId.slice(i + 1)
989
- };
912
+ function optionsFromRuleEntry(entry) {
913
+ return toArray(entry).slice(1);
990
914
  }
991
915
 
992
916
  // packages/plugin-eslint/src/lib/meta/groups.ts
@@ -1054,6 +978,191 @@ function groupsFromRuleCategories(rules) {
1054
978
  return groups.toSorted((a, b) => a.slug.localeCompare(b.slug));
1055
979
  }
1056
980
 
981
+ // packages/plugin-eslint/src/lib/meta/versions/flat.ts
982
+ import { builtinRules } from "eslint/use-at-your-own-risk";
983
+ import { isAbsolute, join as join2 } from "node:path";
984
+ import { pathToFileURL } from "node:url";
985
+ async function loadRulesForFlatConfig({
986
+ eslintrc
987
+ }) {
988
+ const config = eslintrc ? await loadConfigByPath(eslintrc) : await loadConfigByDefaultLocation();
989
+ const configs = toArray(config);
990
+ const rules = findEnabledRulesWithOptions(configs);
991
+ return rules.map((rule) => {
992
+ const meta = findRuleMeta(rule.ruleId, configs);
993
+ if (!meta) {
994
+ ui().logger.warning(`Cannot find metadata for rule ${rule.ruleId}`);
995
+ return null;
996
+ }
997
+ return { ...rule, meta };
998
+ }).filter(exists);
999
+ }
1000
+ async function loadConfigByDefaultLocation() {
1001
+ const flatConfigFileNames = [
1002
+ "eslint.config.js",
1003
+ "eslint.config.mjs",
1004
+ "eslint.config.cjs"
1005
+ ];
1006
+ const configPath = await findNearestFile(flatConfigFileNames);
1007
+ if (configPath) {
1008
+ return loadConfigByPath(configPath);
1009
+ }
1010
+ throw new Error(
1011
+ [
1012
+ `ESLint config file not found - expected ${flatConfigFileNames.join("/")} in ${process.cwd()} or some parent directory`,
1013
+ "If your ESLint config is in a non-standard location, use the `eslintrc` parameter to specify the path."
1014
+ ].join("\n")
1015
+ );
1016
+ }
1017
+ async function loadConfigByPath(path) {
1018
+ const absolutePath = isAbsolute(path) ? path : join2(process.cwd(), path);
1019
+ const url = pathToFileURL(absolutePath).toString();
1020
+ const mod = await import(url);
1021
+ return "default" in mod ? mod.default : mod;
1022
+ }
1023
+ function findEnabledRulesWithOptions(configs) {
1024
+ const enabledRules = configs.flatMap(({ rules }) => Object.entries(rules ?? {})).filter(([, entry]) => entry != null && !isRuleOff(entry)).map(([ruleId, entry]) => ({
1025
+ ruleId,
1026
+ options: entry ? optionsFromRuleEntry(entry) : []
1027
+ }));
1028
+ const uniqueRulesMap = new Map(
1029
+ enabledRules.map(({ ruleId, options }) => [
1030
+ `${ruleId}::${jsonHash(options)}`,
1031
+ { ruleId, options }
1032
+ ])
1033
+ );
1034
+ return [...uniqueRulesMap.values()];
1035
+ }
1036
+ function findRuleMeta(ruleId, configs) {
1037
+ const { plugin, name: name2 } = parseRuleId(ruleId);
1038
+ if (!plugin) {
1039
+ return findBuiltinRuleMeta(name2);
1040
+ }
1041
+ return findPluginRuleMeta(plugin, name2, configs);
1042
+ }
1043
+ function findBuiltinRuleMeta(name2) {
1044
+ const rule = builtinRules.get(name2);
1045
+ return rule?.meta;
1046
+ }
1047
+ function findPluginRuleMeta(plugin, name2, configs) {
1048
+ const config = configs.find(({ plugins = {} }) => plugin in plugins);
1049
+ const rule = config?.plugins?.[plugin]?.rules?.[name2];
1050
+ if (typeof rule === "function") {
1051
+ ui().logger.warning(
1052
+ `Cannot parse metadata for rule ${plugin}/${name2}, plugin registers it as a function`
1053
+ );
1054
+ return void 0;
1055
+ }
1056
+ return rule?.meta;
1057
+ }
1058
+
1059
+ // packages/plugin-eslint/src/lib/setup.ts
1060
+ import { ESLint } from "eslint";
1061
+ async function setupESLint(eslintrc) {
1062
+ const eslintConstructor = await loadESLint();
1063
+ return new eslintConstructor({
1064
+ overrideConfigFile: eslintrc,
1065
+ errorOnUnmatchedPattern: false
1066
+ });
1067
+ }
1068
+ async function loadESLint() {
1069
+ const eslint = await import("eslint");
1070
+ if ("loadESLint" in eslint && typeof eslint.loadESLint === "function") {
1071
+ return await eslint.loadESLint();
1072
+ }
1073
+ return ESLint;
1074
+ }
1075
+
1076
+ // packages/plugin-eslint/src/lib/meta/versions/legacy.ts
1077
+ async function loadRulesForLegacyConfig({
1078
+ eslintrc,
1079
+ patterns
1080
+ }) {
1081
+ const eslint = await setupESLint(eslintrc);
1082
+ const configs = await toArray(patterns).reduce(
1083
+ async (acc, pattern) => [
1084
+ ...await acc,
1085
+ await eslint.calculateConfigForFile(pattern)
1086
+ ],
1087
+ Promise.resolve([])
1088
+ );
1089
+ const rulesIds = distinct(
1090
+ configs.flatMap((config) => Object.keys(config.rules ?? {}))
1091
+ );
1092
+ const rulesMeta = eslint.getRulesMetaForResults([
1093
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
1094
+ {
1095
+ messages: rulesIds.map((ruleId) => ({ ruleId })),
1096
+ suppressedMessages: []
1097
+ }
1098
+ ]);
1099
+ return configs.flatMap((config) => Object.entries(config.rules ?? {})).map(([ruleId, ruleEntry]) => {
1100
+ if (ruleEntry == null || isRuleOff(ruleEntry)) {
1101
+ return null;
1102
+ }
1103
+ const meta = rulesMeta[ruleId];
1104
+ if (!meta) {
1105
+ ui().logger.warning(`Metadata not found for ESLint rule ${ruleId}`);
1106
+ return null;
1107
+ }
1108
+ const options = optionsFromRuleEntry(ruleEntry);
1109
+ return {
1110
+ ruleId,
1111
+ meta,
1112
+ options
1113
+ };
1114
+ }).filter(exists);
1115
+ }
1116
+
1117
+ // packages/plugin-eslint/src/lib/meta/versions/detect.ts
1118
+ import { ESLint as ESLint2 } from "eslint";
1119
+ async function detectConfigVersion() {
1120
+ if (process.env["ESLINT_USE_FLAT_CONFIG"] === "true") {
1121
+ return "flat";
1122
+ }
1123
+ if (process.env["ESLINT_USE_FLAT_CONFIG"] === "false") {
1124
+ return "legacy";
1125
+ }
1126
+ if (ESLint2.version.startsWith("8.")) {
1127
+ if (await fileExists("eslint.config.js")) {
1128
+ return "flat";
1129
+ }
1130
+ return "legacy";
1131
+ }
1132
+ return "flat";
1133
+ }
1134
+
1135
+ // packages/plugin-eslint/src/lib/meta/versions/index.ts
1136
+ function selectRulesLoader(version2) {
1137
+ switch (version2) {
1138
+ case "flat":
1139
+ return loadRulesForFlatConfig;
1140
+ case "legacy":
1141
+ return loadRulesForLegacyConfig;
1142
+ }
1143
+ }
1144
+
1145
+ // packages/plugin-eslint/src/lib/meta/rules.ts
1146
+ async function listRules(targets) {
1147
+ const version2 = await detectConfigVersion();
1148
+ const loadRulesMap = selectRulesLoader(version2);
1149
+ const rulesMap = await targets.reduce(async (acc, target) => {
1150
+ const map = await acc;
1151
+ const rules = await loadRulesMap(target);
1152
+ return rules.reduce(mergeRuleIntoMap, map);
1153
+ }, Promise.resolve({}));
1154
+ return Object.values(rulesMap).flatMap(Object.values);
1155
+ }
1156
+ function mergeRuleIntoMap(map, rule) {
1157
+ return {
1158
+ ...map,
1159
+ [rule.ruleId]: {
1160
+ ...map[rule.ruleId],
1161
+ [jsonHash(rule.options)]: rule
1162
+ }
1163
+ };
1164
+ }
1165
+
1057
1166
  // packages/plugin-eslint/src/lib/meta/transform.ts
1058
1167
  function ruleToAudit({ ruleId, meta, options }) {
1059
1168
  const name2 = ruleId.split("/").at(-1) ?? ruleId;
@@ -1089,10 +1198,10 @@ async function listAuditsAndGroups(targets) {
1089
1198
 
1090
1199
  // packages/plugin-eslint/src/lib/runner/index.ts
1091
1200
  import { writeFile } from "node:fs/promises";
1092
- import { dirname, join as join2 } from "node:path";
1201
+ import { dirname as dirname2, join as join3 } from "node:path";
1093
1202
  var WORKDIR = pluginWorkDir("eslint");
1094
- var RUNNER_OUTPUT_PATH = join2(WORKDIR, "runner-output.json");
1095
- var PLUGIN_CONFIG_PATH = join2(
1203
+ var RUNNER_OUTPUT_PATH = join3(WORKDIR, "runner-output.json");
1204
+ var PLUGIN_CONFIG_PATH = join3(
1096
1205
  process.cwd(),
1097
1206
  WORKDIR,
1098
1207
  "plugin-config.json"
@@ -1102,7 +1211,7 @@ async function createRunnerConfig(scriptPath, audits, targets) {
1102
1211
  targets,
1103
1212
  slugs: audits.map((audit) => audit.slug)
1104
1213
  };
1105
- await ensureDirectoryExists(dirname(PLUGIN_CONFIG_PATH));
1214
+ await ensureDirectoryExists(dirname2(PLUGIN_CONFIG_PATH));
1106
1215
  await writeFile(PLUGIN_CONFIG_PATH, JSON.stringify(config));
1107
1216
  return {
1108
1217
  command: "node",
@@ -1115,8 +1224,8 @@ async function createRunnerConfig(scriptPath, audits, targets) {
1115
1224
  async function eslintPlugin(config) {
1116
1225
  const targets = eslintPluginConfigSchema.parse(config);
1117
1226
  const { audits, groups } = await listAuditsAndGroups(targets);
1118
- const runnerScriptPath = join3(
1119
- fileURLToPath(dirname2(import.meta.url)),
1227
+ const runnerScriptPath = join4(
1228
+ fileURLToPath(dirname3(import.meta.url)),
1120
1229
  "bin.js"
1121
1230
  );
1122
1231
  return {
@@ -1133,14 +1242,33 @@ async function eslintPlugin(config) {
1133
1242
  };
1134
1243
  }
1135
1244
 
1245
+ // packages/plugin-eslint/src/lib/nx/filter-project-graph.ts
1246
+ function filterProjectGraph(projectGraph, exclude = []) {
1247
+ const filteredNodes = Object.entries(
1248
+ projectGraph.nodes
1249
+ ).reduce(
1250
+ (acc, [projectName, projectNode]) => exclude.includes(projectName) ? acc : { ...acc, [projectName]: projectNode },
1251
+ {}
1252
+ );
1253
+ const filteredDependencies = Object.entries(projectGraph.dependencies).reduce(
1254
+ (acc, [key, deps]) => exclude.includes(key) ? acc : { ...acc, [key]: deps },
1255
+ {}
1256
+ );
1257
+ return {
1258
+ nodes: filteredNodes,
1259
+ dependencies: filteredDependencies,
1260
+ version: projectGraph.version
1261
+ };
1262
+ }
1263
+
1136
1264
  // packages/plugin-eslint/src/lib/nx/utils.ts
1137
- import { join as join4 } from "node:path";
1265
+ import { join as join5 } from "node:path";
1138
1266
  async function findCodePushupEslintrc(project) {
1139
1267
  const name2 = "code-pushup.eslintrc";
1140
1268
  const extensions = ["json", "js", "cjs", "yml", "yaml"];
1141
1269
  for (const ext of extensions) {
1142
1270
  const filename = `./${project.root}/${name2}.${ext}`;
1143
- if (await fileExists(join4(process.cwd(), filename))) {
1271
+ if (await fileExists(join5(process.cwd(), filename))) {
1144
1272
  return filename;
1145
1273
  }
1146
1274
  }
@@ -1184,10 +1312,14 @@ async function nxProjectsToConfig(projectGraph, predicate = () => true) {
1184
1312
  }
1185
1313
 
1186
1314
  // packages/plugin-eslint/src/lib/nx/find-all-projects.ts
1187
- async function eslintConfigFromAllNxProjects() {
1315
+ async function eslintConfigFromAllNxProjects(options = {}) {
1188
1316
  const { createProjectGraphAsync } = await import("@nx/devkit");
1189
1317
  const projectGraph = await createProjectGraphAsync({ exitOnError: false });
1190
- return nxProjectsToConfig(projectGraph);
1318
+ const filteredProjectGraph = filterProjectGraph(
1319
+ projectGraph,
1320
+ options.exclude
1321
+ );
1322
+ return nxProjectsToConfig(filteredProjectGraph);
1191
1323
  }
1192
1324
  var eslintConfigFromNxProjects = eslintConfigFromAllNxProjects;
1193
1325
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@code-pushup/eslint-plugin",
3
- "version": "0.53.1",
3
+ "version": "0.55.0",
4
4
  "license": "MIT",
5
5
  "description": "Code PushUp plugin for detecting problems in source code using ESLint.📋",
6
6
  "homepage": "https://github.com/code-pushup/cli/tree/main/packages/plugin-eslint#readme",
@@ -40,13 +40,13 @@
40
40
  "main": "./index.js",
41
41
  "types": "./src/index.d.ts",
42
42
  "dependencies": {
43
- "@code-pushup/utils": "0.53.1",
44
- "@code-pushup/models": "0.53.1",
45
- "eslint": "^8.46.0",
43
+ "@code-pushup/utils": "0.55.0",
44
+ "@code-pushup/models": "0.55.0",
46
45
  "zod": "^3.22.4"
47
46
  },
48
47
  "peerDependencies": {
49
- "@nx/devkit": "^17.0.0 || ^18.0.0 || ^19.0.0"
48
+ "@nx/devkit": ">=17.0.0",
49
+ "eslint": "^8.46.0 || ^9.0.0"
50
50
  },
51
51
  "peerDependenciesMeta": {
52
52
  "@nx/devkit": {
@@ -1,61 +1,60 @@
1
- import type { ESLint } from 'eslint';
2
- import { type ZodType, z } from 'zod';
1
+ import { z } from 'zod';
3
2
  export declare const eslintTargetSchema: z.ZodEffects<z.ZodUnion<[z.ZodUnion<[z.ZodString, z.ZodArray<z.ZodString, "many">]>, z.ZodObject<{
4
- eslintrc: z.ZodOptional<z.ZodUnion<[z.ZodString, ZodType<ESLint.ConfigData<import("eslint").Linter.RulesRecord>, z.ZodTypeDef, ESLint.ConfigData<import("eslint").Linter.RulesRecord>>]>>;
3
+ eslintrc: z.ZodOptional<z.ZodString>;
5
4
  patterns: z.ZodUnion<[z.ZodString, z.ZodArray<z.ZodString, "many">]>;
6
5
  }, "strip", z.ZodTypeAny, {
7
6
  patterns: string | string[];
8
- eslintrc?: string | ESLint.ConfigData<import("eslint").Linter.RulesRecord> | undefined;
7
+ eslintrc?: string | undefined;
9
8
  }, {
10
9
  patterns: string | string[];
11
- eslintrc?: string | ESLint.ConfigData<import("eslint").Linter.RulesRecord> | undefined;
10
+ eslintrc?: string | undefined;
12
11
  }>]>, {
13
12
  patterns: string | string[];
14
- eslintrc?: string | ESLint.ConfigData<import("eslint").Linter.RulesRecord> | undefined;
13
+ eslintrc?: string | undefined;
15
14
  }, string | string[] | {
16
15
  patterns: string | string[];
17
- eslintrc?: string | ESLint.ConfigData<import("eslint").Linter.RulesRecord> | undefined;
16
+ eslintrc?: string | undefined;
18
17
  }>;
19
18
  export type ESLintTarget = z.infer<typeof eslintTargetSchema>;
20
19
  export declare const eslintPluginConfigSchema: z.ZodEffects<z.ZodUnion<[z.ZodEffects<z.ZodUnion<[z.ZodUnion<[z.ZodString, z.ZodArray<z.ZodString, "many">]>, z.ZodObject<{
21
- eslintrc: z.ZodOptional<z.ZodUnion<[z.ZodString, ZodType<ESLint.ConfigData<import("eslint").Linter.RulesRecord>, z.ZodTypeDef, ESLint.ConfigData<import("eslint").Linter.RulesRecord>>]>>;
20
+ eslintrc: z.ZodOptional<z.ZodString>;
22
21
  patterns: z.ZodUnion<[z.ZodString, z.ZodArray<z.ZodString, "many">]>;
23
22
  }, "strip", z.ZodTypeAny, {
24
23
  patterns: string | string[];
25
- eslintrc?: string | ESLint.ConfigData<import("eslint").Linter.RulesRecord> | undefined;
24
+ eslintrc?: string | undefined;
26
25
  }, {
27
26
  patterns: string | string[];
28
- eslintrc?: string | ESLint.ConfigData<import("eslint").Linter.RulesRecord> | undefined;
27
+ eslintrc?: string | undefined;
29
28
  }>]>, {
30
29
  patterns: string | string[];
31
- eslintrc?: string | ESLint.ConfigData<import("eslint").Linter.RulesRecord> | undefined;
30
+ eslintrc?: string | undefined;
32
31
  }, string | string[] | {
33
32
  patterns: string | string[];
34
- eslintrc?: string | ESLint.ConfigData<import("eslint").Linter.RulesRecord> | undefined;
33
+ eslintrc?: string | undefined;
35
34
  }>, z.ZodArray<z.ZodEffects<z.ZodUnion<[z.ZodUnion<[z.ZodString, z.ZodArray<z.ZodString, "many">]>, z.ZodObject<{
36
- eslintrc: z.ZodOptional<z.ZodUnion<[z.ZodString, ZodType<ESLint.ConfigData<import("eslint").Linter.RulesRecord>, z.ZodTypeDef, ESLint.ConfigData<import("eslint").Linter.RulesRecord>>]>>;
35
+ eslintrc: z.ZodOptional<z.ZodString>;
37
36
  patterns: z.ZodUnion<[z.ZodString, z.ZodArray<z.ZodString, "many">]>;
38
37
  }, "strip", z.ZodTypeAny, {
39
38
  patterns: string | string[];
40
- eslintrc?: string | ESLint.ConfigData<import("eslint").Linter.RulesRecord> | undefined;
39
+ eslintrc?: string | undefined;
41
40
  }, {
42
41
  patterns: string | string[];
43
- eslintrc?: string | ESLint.ConfigData<import("eslint").Linter.RulesRecord> | undefined;
42
+ eslintrc?: string | undefined;
44
43
  }>]>, {
45
44
  patterns: string | string[];
46
- eslintrc?: string | ESLint.ConfigData<import("eslint").Linter.RulesRecord> | undefined;
45
+ eslintrc?: string | undefined;
47
46
  }, string | string[] | {
48
47
  patterns: string | string[];
49
- eslintrc?: string | ESLint.ConfigData<import("eslint").Linter.RulesRecord> | undefined;
48
+ eslintrc?: string | undefined;
50
49
  }>, "many">]>, {
51
50
  patterns: string | string[];
52
- eslintrc?: string | ESLint.ConfigData<import("eslint").Linter.RulesRecord> | undefined;
51
+ eslintrc?: string | undefined;
53
52
  }[], string | string[] | {
54
53
  patterns: string | string[];
55
- eslintrc?: string | ESLint.ConfigData<import("eslint").Linter.RulesRecord> | undefined;
54
+ eslintrc?: string | undefined;
56
55
  } | (string | string[] | {
57
56
  patterns: string | string[];
58
- eslintrc?: string | ESLint.ConfigData<import("eslint").Linter.RulesRecord> | undefined;
57
+ eslintrc?: string | undefined;
59
58
  })[]>;
60
59
  export type ESLintPluginConfig = z.input<typeof eslintPluginConfigSchema>;
61
60
  export type ESLintPluginRunnerConfig = {
@@ -1,4 +1,4 @@
1
1
  import type { Group } from '@code-pushup/models';
2
- import { type RuleData } from './rules';
2
+ import { type RuleData } from './parse';
3
3
  export declare function groupsFromRuleTypes(rules: RuleData[]): Group[];
4
4
  export declare function groupsFromRuleCategories(rules: RuleData[]): Group[];
@@ -0,0 +1,12 @@
1
+ import type { Linter, Rule } from 'eslint';
2
+ export type RuleData = {
3
+ ruleId: string;
4
+ meta: Rule.RuleMetaData;
5
+ options: unknown[] | undefined;
6
+ };
7
+ export declare function parseRuleId(ruleId: string): {
8
+ plugin?: string;
9
+ name: string;
10
+ };
11
+ export declare function isRuleOff(entry: Linter.RuleEntry<unknown[]>): boolean;
12
+ export declare function optionsFromRuleEntry(entry: Linter.RuleEntry<unknown[]>): unknown[];
@@ -1,12 +1,3 @@
1
- import type { Rule } from 'eslint';
2
1
  import type { ESLintTarget } from '../config';
3
- export type RuleData = {
4
- ruleId: string;
5
- meta: Rule.RuleMetaData;
6
- options: unknown[] | undefined;
7
- };
2
+ import type { RuleData } from './parse';
8
3
  export declare function listRules(targets: ESLintTarget[]): Promise<RuleData[]>;
9
- export declare function parseRuleId(ruleId: string): {
10
- plugin?: string;
11
- name: string;
12
- };
@@ -1,3 +1,3 @@
1
1
  import type { Audit } from '@code-pushup/models';
2
- import type { RuleData } from './rules';
2
+ import type { RuleData } from './parse';
3
3
  export declare function ruleToAudit({ ruleId, meta, options }: RuleData): Audit;
@@ -0,0 +1,2 @@
1
+ import type { ConfigFormat } from './formats';
2
+ export declare function detectConfigVersion(): Promise<ConfigFormat>;
@@ -0,0 +1,3 @@
1
+ import type { ESLintTarget } from '../../config';
2
+ import { type RuleData } from '../parse';
3
+ export declare function loadRulesForFlatConfig({ eslintrc, }: Pick<ESLintTarget, 'eslintrc'>): Promise<RuleData[]>;
@@ -0,0 +1 @@
1
+ export type ConfigFormat = 'flat' | 'legacy';
@@ -0,0 +1,6 @@
1
+ import type { ESLintTarget } from '../../config';
2
+ import type { RuleData } from '../parse';
3
+ import type { ConfigFormat } from './formats';
4
+ export { detectConfigVersion } from './detect';
5
+ export type { ConfigFormat } from './formats';
6
+ export declare function selectRulesLoader(version: ConfigFormat): (target: ESLintTarget) => Promise<RuleData[]>;
@@ -0,0 +1,3 @@
1
+ import type { ESLintTarget } from '../../config';
2
+ import { type RuleData } from '../parse';
3
+ export declare function loadRulesForLegacyConfig({ eslintrc, patterns, }: ESLintTarget): Promise<RuleData[]>;
@@ -0,0 +1,2 @@
1
+ import type { ProjectGraph } from '@nx/devkit';
2
+ export declare function filterProjectGraph(projectGraph: ProjectGraph, exclude?: string[]): ProjectGraph;
@@ -2,8 +2,11 @@ import type { ESLintTarget } from '../config';
2
2
  /**
3
3
  * Finds all Nx projects in workspace and converts their lint configurations to Code PushUp ESLint plugin parameters.
4
4
  *
5
+ * Allows excluding certain projects from the configuration using the `options.exclude` parameter.
6
+ *
5
7
  * Use when you wish to automatically include every Nx project in a single Code PushUp project.
6
- * If you prefer to only include a subset of your Nx monorepo, refer to {@link eslintConfigFromNxProjectAndDeps} instead.
8
+ * If you prefer to include only a subset of your Nx monorepo, specify projects to exclude using the `exclude` option
9
+ * or consider using {@link eslintConfigFromNxProjectAndDeps} for finer control.
7
10
  *
8
11
  * @example
9
12
  * import eslintPlugin, {
@@ -13,14 +16,18 @@ import type { ESLintTarget } from '../config';
13
16
  * export default {
14
17
  * plugins: [
15
18
  * await eslintPlugin(
16
- * await eslintConfigFromAllNxProjects()
19
+ * await eslintConfigFromAllNxProjects({ exclude: ['server'] })
17
20
  * )
18
21
  * ]
19
22
  * }
20
23
  *
24
+ * @param options - Configuration options to filter projects
25
+ * @param options.exclude - Array of project names to exclude from the ESLint configuration
21
26
  * @returns ESLint config and patterns, intended to be passed to {@link eslintPlugin}
22
27
  */
23
- export declare function eslintConfigFromAllNxProjects(): Promise<ESLintTarget[]>;
28
+ export declare function eslintConfigFromAllNxProjects(options?: {
29
+ exclude?: string[];
30
+ }): Promise<ESLintTarget[]>;
24
31
  /**
25
32
  * @deprecated
26
33
  * Helper is renamed, please use `eslintConfigFromAllNxProjects` function instead.
@@ -1,3 +1,3 @@
1
1
  import { ESLint } from 'eslint';
2
2
  import type { ESLintTarget } from './config';
3
- export declare function setupESLint(eslintrc: ESLintTarget['eslintrc']): ESLint;
3
+ export declare function setupESLint(eslintrc: ESLintTarget['eslintrc']): Promise<ESLint>;