@code-pushup/eslint-plugin 0.1.1 → 0.3.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
@@ -35,6 +35,8 @@ Detected ESLint rules are mapped to Code PushUp audits. Audit reports are calcul
35
35
  Remember that Code PushUp only collects and uploads the results, it doesn't fail if errors are found.
36
36
  So you can be more strict than in most linter setups, the idea is to set aspirational goals and track your progress.
37
37
 
38
+ > 💡 We recommend extending our own [`@code-pushup/eslint-config`](https://www.npmjs.com/package/@code-pushup/eslint-config). 😇
39
+
38
40
  4. Add this plugin to the `plugins` array in your Code PushUp CLI config file (e.g. `code-pushup.config.js`).
39
41
 
40
42
  Pass in the path to your ESLint config file, along with glob patterns for which files you wish to target (relative to `process.cwd()`).
@@ -51,6 +53,34 @@ Detected ESLint rules are mapped to Code PushUp audits. Audit reports are calcul
51
53
  };
52
54
  ```
53
55
 
56
+ If you're using an Nx monorepo, additional helper functions are provided to simplify your configuration:
57
+
58
+ - If you wish to combine all projects in your workspace into one report, use the `eslintConfigFromNxProjects` helper:
59
+
60
+ ```js
61
+ import eslintPlugin, { eslintConfigFromNxProjects } from '@code-pushup/eslint-plugin';
62
+
63
+ export default {
64
+ plugins: [
65
+ // ...
66
+ await eslintPlugin(await eslintConfigFromNxProjects()),
67
+ ],
68
+ };
69
+ ```
70
+
71
+ - If you wish to target a specific project along with other projects it depends on, use the `eslintConfigFromNxProject` helper and pass in in your project name:
72
+
73
+ ```js
74
+ import eslintPlugin, { eslintConfigFromNxProject } from '@code-pushup/eslint-plugin';
75
+
76
+ export default {
77
+ plugins: [
78
+ // ...
79
+ await eslintPlugin(await eslintConfigFromNxProject('<PROJECT-NAME>')),
80
+ ],
81
+ };
82
+ ```
83
+
54
84
  5. (Optional) Reference audits (or groups) which you wish to include in custom categories (use `npx code-pushup print-config` to list audits and groups).
55
85
 
56
86
  Assign weights based on what influence each ESLint rule should have on the overall category score (assign weight 0 to only include as extra info, without influencing category score).
package/bin.js CHANGED
@@ -9,6 +9,11 @@ import { z as z2 } from "zod";
9
9
  import { z } from "zod";
10
10
  import { MATERIAL_ICONS } from "@code-pushup/portal-client";
11
11
 
12
+ // packages/models/src/lib/implementation/limits.ts
13
+ var MAX_SLUG_LENGTH = 128;
14
+ var MAX_TITLE_LENGTH = 256;
15
+ var MAX_DESCRIPTION_LENGTH = 65536;
16
+
12
17
  // packages/models/src/lib/implementation/utils.ts
13
18
  var slugRegex = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
14
19
  var filenameRegex = /^(?!.*[ \\/:*?"<>|]).+$/;
@@ -44,12 +49,12 @@ function executionMetaSchema(options = {
44
49
  function slugSchema(description = "Unique ID (human-readable, URL-safe)") {
45
50
  return z.string({ description }).regex(slugRegex, {
46
51
  message: "The slug has to follow the pattern [0-9a-z] followed by multiple optional groups of -[0-9a-z]. e.g. my-slug"
47
- }).max(128, {
48
- message: "slug can be max 128 characters long"
52
+ }).max(MAX_SLUG_LENGTH, {
53
+ message: `slug can be max ${MAX_SLUG_LENGTH} characters long`
49
54
  });
50
55
  }
51
56
  function descriptionSchema(description = "Description (markdown)") {
52
- return z.string({ description }).max(65536).optional();
57
+ return z.string({ description }).max(MAX_DESCRIPTION_LENGTH).optional();
53
58
  }
54
59
  function docsUrlSchema(description = "Documentation site") {
55
60
  return urlSchema(description).optional().or(z.string().max(0));
@@ -58,7 +63,7 @@ function urlSchema(description) {
58
63
  return z.string({ description }).url();
59
64
  }
60
65
  function titleSchema(description = "Descriptive name") {
61
- return z.string({ description }).max(256);
66
+ return z.string({ description }).max(MAX_TITLE_LENGTH);
62
67
  }
63
68
  function metaSchema(options) {
64
69
  const {
@@ -550,13 +555,6 @@ function pluralizeToken(token, times = 0) {
550
555
  }
551
556
 
552
557
  // packages/utils/src/lib/file-system.ts
553
- function toUnixPath(path, options) {
554
- const unixPath = path.replace(/\\/g, "/");
555
- if (options?.toRelative) {
556
- return unixPath.replace(process.cwd().replace(/\\/g, "/") + "/", "");
557
- }
558
- return unixPath;
559
- }
560
558
  async function readTextFile(path) {
561
559
  const buffer = await readFile(path);
562
560
  return buffer.toString();
@@ -565,6 +563,13 @@ async function readJsonFile(path) {
565
563
  const text = await readTextFile(path);
566
564
  return JSON.parse(text);
567
565
  }
566
+ function toUnixPath(path, options) {
567
+ const unixPath = path.replace(/\\/g, "/");
568
+ if (options?.toRelative) {
569
+ return unixPath.replace(process.cwd().replace(/\\/g, "/") + "/", "");
570
+ }
571
+ return unixPath;
572
+ }
568
573
  function pluginWorkDir(slug) {
569
574
  return join("node_modules", ".code-pushup", slug);
570
575
  }
package/index.js CHANGED
@@ -1,11 +1,11 @@
1
1
  // packages/plugin-eslint/src/lib/eslint-plugin.ts
2
- import { mkdir, writeFile } from "fs/promises";
2
+ import { mkdir as mkdir2, writeFile } from "fs/promises";
3
3
  import { dirname as dirname2, join as join3 } from "path";
4
4
  import { fileURLToPath } from "url";
5
5
 
6
6
  // packages/plugin-eslint/package.json
7
7
  var name = "@code-pushup/eslint-plugin";
8
- var version = "0.1.1";
8
+ var version = "0.3.0";
9
9
 
10
10
  // packages/plugin-eslint/src/lib/config.ts
11
11
  import { z } from "zod";
@@ -31,6 +31,11 @@ import { z as z3 } from "zod";
31
31
  import { z as z2 } from "zod";
32
32
  import { MATERIAL_ICONS } from "@code-pushup/portal-client";
33
33
 
34
+ // packages/models/src/lib/implementation/limits.ts
35
+ var MAX_SLUG_LENGTH = 128;
36
+ var MAX_TITLE_LENGTH = 256;
37
+ var MAX_DESCRIPTION_LENGTH = 65536;
38
+
34
39
  // packages/models/src/lib/implementation/utils.ts
35
40
  var slugRegex = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
36
41
  var filenameRegex = /^(?!.*[ \\/:*?"<>|]).+$/;
@@ -66,12 +71,12 @@ function executionMetaSchema(options = {
66
71
  function slugSchema(description = "Unique ID (human-readable, URL-safe)") {
67
72
  return z2.string({ description }).regex(slugRegex, {
68
73
  message: "The slug has to follow the pattern [0-9a-z] followed by multiple optional groups of -[0-9a-z]. e.g. my-slug"
69
- }).max(128, {
70
- message: "slug can be max 128 characters long"
74
+ }).max(MAX_SLUG_LENGTH, {
75
+ message: `slug can be max ${MAX_SLUG_LENGTH} characters long`
71
76
  });
72
77
  }
73
78
  function descriptionSchema(description = "Description (markdown)") {
74
- return z2.string({ description }).max(65536).optional();
79
+ return z2.string({ description }).max(MAX_DESCRIPTION_LENGTH).optional();
75
80
  }
76
81
  function docsUrlSchema(description = "Documentation site") {
77
82
  return urlSchema(description).optional().or(z2.string().max(0));
@@ -80,7 +85,7 @@ function urlSchema(description) {
80
85
  return z2.string({ description }).url();
81
86
  }
82
87
  function titleSchema(description = "Descriptive name") {
83
- return z2.string({ description }).max(256);
88
+ return z2.string({ description }).max(MAX_TITLE_LENGTH);
84
89
  }
85
90
  function metaSchema(options) {
86
91
  const {
@@ -551,14 +556,36 @@ var reportSchema = packageVersionSchema({
551
556
  // packages/utils/src/lib/file-system.ts
552
557
  import { bundleRequire } from "bundle-require";
553
558
  import chalk from "chalk";
559
+ import { mkdir, readFile, readdir, stat } from "fs/promises";
554
560
  import { join } from "path";
555
561
 
556
562
  // packages/utils/src/lib/formatting.ts
557
563
  function slugify(text) {
558
564
  return text.trim().toLowerCase().replace(/\s+|\//g, "-").replace(/[^a-z0-9-]/g, "");
559
565
  }
566
+ function truncateText(text, maxChars) {
567
+ if (text.length <= maxChars) {
568
+ return text;
569
+ }
570
+ const ellipsis = "...";
571
+ return text.slice(0, maxChars - ellipsis.length) + ellipsis;
572
+ }
573
+ function truncateTitle(text) {
574
+ return truncateText(text, MAX_TITLE_LENGTH);
575
+ }
576
+ function truncateDescription(text) {
577
+ return truncateText(text, MAX_DESCRIPTION_LENGTH);
578
+ }
560
579
 
561
580
  // packages/utils/src/lib/file-system.ts
581
+ async function fileExists(path) {
582
+ try {
583
+ const stats = await stat(path);
584
+ return stats.isFile();
585
+ } catch {
586
+ return false;
587
+ }
588
+ }
562
589
  function pluginWorkDir(slug) {
563
590
  return join("node_modules", ".code-pushup", slug);
564
591
  }
@@ -781,8 +808,8 @@ function ruleToAudit({ ruleId, meta, options }) {
781
808
  ];
782
809
  return {
783
810
  slug: ruleIdToSlug(ruleId, options),
784
- title: meta.docs?.description ?? name2,
785
- description: lines.join("\n\n"),
811
+ title: truncateTitle(meta.docs?.description ?? name2),
812
+ description: truncateDescription(lines.join("\n\n")),
786
813
  ...meta.docs?.url && {
787
814
  docsUrl: meta.docs.url
788
815
  }
@@ -840,7 +867,7 @@ async function eslintPlugin(config) {
840
867
  const eslint = setupESLint(eslintrc);
841
868
  const { audits, groups } = await listAuditsAndGroups(eslint, patterns);
842
869
  if (typeof eslintrc !== "string") {
843
- await mkdir(dirname2(ESLINTRC_PATH), { recursive: true });
870
+ await mkdir2(dirname2(ESLINTRC_PATH), { recursive: true });
844
871
  await writeFile(ESLINTRC_PATH, JSON.stringify(eslintrc));
845
872
  }
846
873
  const eslintrcPath = typeof eslintrc === "string" ? eslintrc : ESLINTRC_PATH;
@@ -853,7 +880,7 @@ async function eslintPlugin(config) {
853
880
  title: "ESLint",
854
881
  icon: "eslint",
855
882
  description: "Official Code PushUp ESLint plugin",
856
- // TODO: docsUrl (package README, once published)
883
+ docsUrl: "https://www.npmjs.com/package/@code-pushup/eslint-plugin",
857
884
  packageName: name,
858
885
  version,
859
886
  audits,
@@ -867,8 +894,101 @@ async function eslintPlugin(config) {
867
894
  };
868
895
  }
869
896
 
897
+ // packages/plugin-eslint/src/lib/nx/utils.ts
898
+ import { join as join4 } from "node:path";
899
+ async function findCodePushupEslintrc(project) {
900
+ const name2 = "code-pushup.eslintrc";
901
+ const extensions = ["json", "js", "cjs", "yml", "yaml"];
902
+ for (const ext of extensions) {
903
+ const filename = `./${project.root}/${name2}.${ext}`;
904
+ if (await fileExists(join4(process.cwd(), filename))) {
905
+ return filename;
906
+ }
907
+ }
908
+ return null;
909
+ }
910
+ function getLintFilePatterns(project) {
911
+ const options = project.targets?.["lint"]?.options;
912
+ return options?.lintFilePatterns == null ? [] : toArray(options.lintFilePatterns);
913
+ }
914
+ function getEslintConfig(project) {
915
+ const options = project.targets?.["lint"]?.options;
916
+ return options?.eslintConfig ?? `./${project.root}/.eslintrc.json`;
917
+ }
918
+
919
+ // packages/plugin-eslint/src/lib/nx/projects-to-config.ts
920
+ async function nxProjectsToConfig(projectGraph, predicate = () => true) {
921
+ const { readProjectsConfigurationFromProjectGraph } = await import("@nx/devkit");
922
+ const projectsConfiguration = readProjectsConfigurationFromProjectGraph(projectGraph);
923
+ const projects = Object.values(projectsConfiguration.projects).filter((project) => "lint" in (project.targets ?? {})).filter(predicate).sort((a, b) => a.root.localeCompare(b.root));
924
+ const eslintConfig = {
925
+ root: true,
926
+ overrides: await Promise.all(
927
+ projects.map(async (project) => ({
928
+ files: getLintFilePatterns(project),
929
+ extends: await findCodePushupEslintrc(project) ?? getEslintConfig(project)
930
+ }))
931
+ )
932
+ };
933
+ const patterns = projects.flatMap((project) => [
934
+ ...getLintFilePatterns(project),
935
+ // HACK: ESLint.calculateConfigForFile won't find rules included only for subsets of *.ts when globs used
936
+ // so we explicitly provide additional patterns used by @code-pushup/eslint-config to ensure those rules are included
937
+ // this workaround won't be necessary once flat configs are stable (much easier to find all rules)
938
+ `${project.sourceRoot}/*.spec.ts`,
939
+ // jest/* and vitest/* rules
940
+ `${project.sourceRoot}/*.cy.ts`,
941
+ // cypress/* rules
942
+ `${project.sourceRoot}/*.stories.ts`,
943
+ // storybook/* rules
944
+ `${project.sourceRoot}/.storybook/main.ts`
945
+ // storybook/no-uninstalled-addons rule
946
+ ]);
947
+ return {
948
+ eslintrc: eslintConfig,
949
+ patterns
950
+ };
951
+ }
952
+
953
+ // packages/plugin-eslint/src/lib/nx/find-all-projects.ts
954
+ async function eslintConfigFromNxProjects() {
955
+ const { createProjectGraphAsync } = await import("@nx/devkit");
956
+ const projectGraph = await createProjectGraphAsync({ exitOnError: false });
957
+ return nxProjectsToConfig(projectGraph);
958
+ }
959
+
960
+ // packages/plugin-eslint/src/lib/nx/traverse-graph.ts
961
+ function findAllDependencies(entry, projectGraph) {
962
+ const results = /* @__PURE__ */ new Set();
963
+ const queue = [entry];
964
+ while (queue.length > 0) {
965
+ const source = queue.shift();
966
+ const dependencies = projectGraph.dependencies[source];
967
+ for (const { target } of dependencies ?? []) {
968
+ if (!results.has(target) && target !== entry) {
969
+ results.add(target);
970
+ queue.push(target);
971
+ }
972
+ }
973
+ }
974
+ return results;
975
+ }
976
+
977
+ // packages/plugin-eslint/src/lib/nx/find-project-with-deps.ts
978
+ async function eslintConfigFromNxProject(projectName) {
979
+ const { createProjectGraphAsync } = await import("@nx/devkit");
980
+ const projectGraph = await createProjectGraphAsync({ exitOnError: false });
981
+ const dependencies = findAllDependencies(projectName, projectGraph);
982
+ return nxProjectsToConfig(
983
+ projectGraph,
984
+ (project) => !!project.name && (project.name === projectName || dependencies.has(project.name))
985
+ );
986
+ }
987
+
870
988
  // packages/plugin-eslint/src/index.ts
871
989
  var src_default = eslintPlugin;
872
990
  export {
873
- src_default as default
991
+ src_default as default,
992
+ eslintConfigFromNxProject,
993
+ eslintConfigFromNxProjects
874
994
  };
package/package.json CHANGED
@@ -1,11 +1,19 @@
1
1
  {
2
2
  "name": "@code-pushup/eslint-plugin",
3
- "version": "0.1.1",
3
+ "version": "0.3.0",
4
4
  "dependencies": {
5
5
  "@code-pushup/utils": "*",
6
+ "@code-pushup/models": "*",
6
7
  "eslint": "~8.46.0",
7
- "zod": "^3.22.4",
8
- "@code-pushup/models": "*"
8
+ "zod": "^3.22.4"
9
+ },
10
+ "peerDependencies": {
11
+ "@nx/devkit": "^17.0.0"
12
+ },
13
+ "peerDependenciesMeta": {
14
+ "@nx/devkit": {
15
+ "optional": true
16
+ }
9
17
  },
10
18
  "type": "module",
11
19
  "main": "./index.js",
package/src/index.d.ts CHANGED
@@ -1,3 +1,4 @@
1
1
  import { eslintPlugin } from './lib/eslint-plugin';
2
2
  export default eslintPlugin;
3
3
  export type { ESLintPluginConfig } from './lib/config';
4
+ export { eslintConfigFromNxProject, eslintConfigFromNxProjects, } from './lib/nx';
@@ -0,0 +1,23 @@
1
+ import type { ESLintPluginConfig } from '../config';
2
+ /**
3
+ * Finds all Nx projects in workspace and converts their lint configurations to Code PushUp ESLint plugin parameters.
4
+ *
5
+ * 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 eslintConfigFromNxProject} instead.
7
+ *
8
+ * @example
9
+ * import eslintPlugin, {
10
+ * eslintConfigFromNxProjects,
11
+ * } from '@code-pushup/eslint-plugin';
12
+ *
13
+ * export default {
14
+ * plugins: [
15
+ * await eslintPlugin(
16
+ * await eslintConfigFromNxProjects()
17
+ * )
18
+ * ]
19
+ * }
20
+ *
21
+ * @returns ESLint config and patterns, intended to be passed to {@link eslintPlugin}
22
+ */
23
+ export declare function eslintConfigFromNxProjects(): Promise<ESLintPluginConfig>;
@@ -0,0 +1,26 @@
1
+ import type { ESLintPluginConfig } from '../config';
2
+ /**
3
+ * Accepts a target Nx projects, finds projects it depends on, and converts lint configurations to Code PushUp ESLint plugin parameters.
4
+ *
5
+ * Use when you wish to include a targetted subset of your Nx monorepo in your Code PushUp project.
6
+ * If you prefer to include all Nx projects, refer to {@link eslintConfigFromNxProjects} instead.
7
+ *
8
+ * @example
9
+ * import eslintPlugin, {
10
+ * eslintConfigFromNxProject,
11
+ * } from '@code-pushup/eslint-plugin';
12
+ *
13
+ * const projectName = 'backoffice'; // <-- name from project.json
14
+ *
15
+ * export default {
16
+ * plugins: [
17
+ * await eslintPlugin(
18
+ * await eslintConfigFromNxProject(projectName)
19
+ * )
20
+ * ]
21
+ * }
22
+ *
23
+ * @param projectName Nx project serving as main entry point
24
+ * @returns ESLint config and patterns, intended to be passed to {@link eslintPlugin}
25
+ */
26
+ export declare function eslintConfigFromNxProject(projectName: string): Promise<ESLintPluginConfig>;
@@ -0,0 +1,2 @@
1
+ export { eslintConfigFromNxProjects } from './find-all-projects';
2
+ export { eslintConfigFromNxProject } from './find-project-with-deps';
@@ -0,0 +1,3 @@
1
+ import type { ProjectConfiguration, ProjectGraph } from '@nx/devkit';
2
+ import type { ESLintPluginConfig } from '../config';
3
+ export declare function nxProjectsToConfig(projectGraph: ProjectGraph, predicate?: (project: ProjectConfiguration) => boolean): Promise<ESLintPluginConfig>;
@@ -0,0 +1,2 @@
1
+ import type { ProjectGraph } from '@nx/devkit';
2
+ export declare function findAllDependencies(entry: string, projectGraph: ProjectGraph): ReadonlySet<string>;
@@ -0,0 +1,4 @@
1
+ import type { ProjectConfiguration } from '@nx/devkit';
2
+ export declare function findCodePushupEslintrc(project: ProjectConfiguration): Promise<string | null>;
3
+ export declare function getLintFilePatterns(project: ProjectConfiguration): string[];
4
+ export declare function getEslintConfig(project: ProjectConfiguration): string;