pr-checkmate 1.22.3 → 1.22.5

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 (52) hide show
  1. package/.eslintrc.js +1 -1
  2. package/README.md +36 -5
  3. package/dist/config/constants.d.ts +24 -0
  4. package/dist/config/constants.js +53 -1
  5. package/dist/config/index.d.ts +0 -8
  6. package/dist/config/index.js +1 -7
  7. package/dist/config/loader.d.ts +0 -8
  8. package/dist/config/loader.js +12 -8
  9. package/dist/core/checks/circular-deps.js +1 -1
  10. package/dist/core/checks/commitlint.js +1 -1
  11. package/dist/core/checks/cpp-format.js +1 -1
  12. package/dist/core/checks/dead-code.js +47 -8
  13. package/dist/core/checks/dependency-check.js +12 -2
  14. package/dist/core/checks/duplicate-check.js +18 -2
  15. package/dist/core/checks/findings.js +1 -1
  16. package/dist/core/checks/kotlin-lint.js +1 -1
  17. package/dist/core/checks/license-check.js +1 -1
  18. package/dist/core/checks/lint.js +1 -1
  19. package/dist/core/checks/markdown-lint.js +1 -1
  20. package/dist/core/checks/npm-audit.js +1 -1
  21. package/dist/core/checks/outdated-deps.js +7 -2
  22. package/dist/core/checks/pr-size-check.js +1 -1
  23. package/dist/core/checks/prettier.js +1 -1
  24. package/dist/core/checks/python-format.js +3 -4
  25. package/dist/core/checks/python-lint.js +3 -4
  26. package/dist/core/checks/ruff-shared.d.ts +10 -0
  27. package/dist/core/checks/ruff-shared.js +14 -0
  28. package/dist/core/checks/security.js +1 -1
  29. package/dist/core/checks/sensitive-file-guard.js +1 -1
  30. package/dist/core/checks/spellcheck.js +1 -1
  31. package/dist/core/checks/swift-lint.js +1 -1
  32. package/dist/core/checks/typecheck.js +1 -1
  33. package/dist/core/context.js +1 -1
  34. package/dist/core/reporters.d.ts +1 -2
  35. package/dist/core/run-checks.js +6 -1
  36. package/dist/core/targets.js +1 -1
  37. package/dist/init.d.ts +0 -1
  38. package/dist/init.js +25 -6
  39. package/dist/interfaces/index.d.ts +48 -22
  40. package/dist/scripts/copy-configs.d.ts +1 -5
  41. package/dist/scripts/copy-configs.js +0 -1
  42. package/dist/utils/delta.js +1 -1
  43. package/dist/utils/detect-language.js +1 -1
  44. package/dist/utils/execa.d.ts +22 -0
  45. package/dist/utils/execa.js +13 -0
  46. package/dist/utils/own-deps.js +14 -17
  47. package/eslint.config.mjs +1 -1
  48. package/package.json +1 -1
  49. package/src/config/.eslintrc.js +1 -1
  50. package/src/config/eslint.config.mjs +1 -1
  51. package/dist/config/config.d.ts +0 -2
  52. package/dist/config/config.js +0 -12
package/.eslintrc.js CHANGED
@@ -81,7 +81,7 @@ module.exports = {
81
81
  { blankLine: 'always', prev: '*', next: 'return' },
82
82
  ],
83
83
  // Complexity analysis (#61)
84
- complexity: ['warn', 15],
84
+ complexity: ['warn', 30],
85
85
  // Import ordering (#65)
86
86
  'import-x/order': [
87
87
  'warn',
package/README.md CHANGED
@@ -219,15 +219,15 @@ jobs:
219
219
  |---|---|---|
220
220
  | ESLint + SonarJS + JSDoc | blocking | Lint with complexity, import order, and doc coverage rules |
221
221
  | TypeScript | blocking | `tsc --noEmit` type check |
222
- | Dependency Check | blocking | Unused / missing deps via depcheck |
222
+ | Dependency Check | blocking | Unused / missing deps via depcheck; silence CLI-only false positives with `dependency.ignore` |
223
223
  | NPM Audit | blocking | Vulnerability scan (`moderate`+) |
224
224
  | Circular Deps | blocking | Circular dependency detection |
225
225
  | License Check | blocking | Blocks GPL / AGPL / LGPL packages |
226
226
  | Ruff Lint | informational | Python linting via WASM |
227
227
  | SwiftLint | informational | Swift linting |
228
228
  | ktlint | informational | Kotlin linting |
229
- | Dead Code | informational | Unused exports (`ts-unused-exports`) |
230
- | Duplicate Code | informational | `jscpd` — 5 lines / 50 tokens threshold |
229
+ | Dead Code | informational | Unused exports (`ts-unused-exports`); test and mock files are excluded from the report by default — override with `deadCode.ignoreFiles` (regex) |
230
+ | Duplicate Code | informational | `jscpd` — 5 lines / 50 tokens threshold; test files ignored by default (set `duplicate.ignore`); only source-code formats are scanned, so YAML/JSON/Markdown are skipped unless you override `duplicate.formats` |
231
231
  | Diff Smell | informational | `console.*`, `debugger`, `TODO/FIXME` in added lines |
232
232
  | Diff Security | informational | XSS, `eval`, SQLi, weak crypto in added lines |
233
233
  | Sensitive Files | informational | `.env`, private keys, CI configs touched in PR |
@@ -259,8 +259,20 @@ All checks can be tuned or disabled per-repo via `pr-checkmate.json`:
259
259
  "swift": { "enabled": true },
260
260
  "kotlin": { "enabled": true },
261
261
  "cpp": { "enabled": true, "clangFormat": { "style": "Google" } },
262
- "duplicate": { "enabled": true, "minLines": 10 },
263
- "deadCode": { "enabled": false },
262
+ "duplicate": {
263
+ "enabled": true,
264
+ "minLines": 10,
265
+ "formats": ["javascript", "typescript", "jsx", "tsx", "python", "kotlin", "swift", "c", "cpp"],
266
+ "ignore": ["**/__tests__/**", "**/*.test.*", "**/*.spec.*", "**/test/**", "**/tests/**"]
267
+ },
268
+ "deadCode": {
269
+ "enabled": true,
270
+ "ignoreFiles": ["src/index\\.ts", "handler"]
271
+ },
272
+ "dependency": {
273
+ "enabled": true,
274
+ "ignore": ["@types/*", "some-cli-tool"]
275
+ },
264
276
  "commitlint": {
265
277
  "enabled": true,
266
278
  "types": ["feat", "fix", "docs", "chore", "ci", "refactor", "test"]
@@ -273,6 +285,25 @@ All checks can be tuned or disabled per-repo via `pr-checkmate.json`:
273
285
  }
274
286
  ```
275
287
 
288
+ A note on `duplicate.ignore`:
289
+
290
+ - Setting it replaces the default test-file list rather than merging with it. If you add your own patterns, include any test globs you still want excluded.
291
+ - `node_modules/**`, `dist/**`, and `.git/**` are always excluded, regardless of config.
292
+ - `duplicate.formats` defaults to source-code formats only, so data files like YAML, JSON, and Markdown are not scanned. Setting it replaces the default list, so add `"yaml"` if you want duplication flagged in those files too.
293
+
294
+ A note on `dependency.ignore`:
295
+
296
+ - List dependency names or globs (such as `"@types/*"`) to exclude from the unused/missing check. The values are passed straight to depcheck's `--ignores`.
297
+ - Use it for packages that only run as CLI binaries or are referenced from config files. depcheck sees no `import` for them and would otherwise flag them as unused.
298
+ - When set, `dependency.ignore` takes precedence over any `.depcheckrc` file. Leave it empty or unset and your existing `.depcheckrc` still applies.
299
+
300
+ A note on `deadCode.ignoreFiles`:
301
+
302
+ - The values are regex sources, not globs, and each one is matched against the file path. So `\.test\.` matches any file with `.test.` in its name.
303
+ - It defaults to test and mock files: `/__tests__/`, `/__mocks__/`, `/__mock__/`, `/tests?/`, `\.test\.`, `\.spec\.`. Their exports are consumed only by the test runner, so they showed up as false-positive "unused" before.
304
+ - Setting it replaces the default list rather than merging with it. Reach for it to exempt entry points or barrels like `src/index.ts` or a lambda `handler` whose exports are the public surface.
305
+ - It applies to the report only and never drops files from analysis, so a match here cannot cascade into missed dead code elsewhere.
306
+
276
307
  ---
277
308
 
278
309
  ## Delta Mode
@@ -2,7 +2,31 @@
2
2
  * Common patterns to ignore in all checks
3
3
  */
4
4
  export declare const IGNORE_PATTERNS: string[];
5
+ /**
6
+ * Test files/folders excluded from duplicate-code detection by default.
7
+ * Test suites legitimately repeat setup/arrange/assert boilerplate, so counting
8
+ * them as clones is noise that drowns out real production duplication. Clients can
9
+ * override this list via `duplicate.ignore` in pr-checkmate.json.
10
+ */
11
+ export declare const DUPLICATE_IGNORE_DEFAULTS: string[];
12
+ /**
13
+ * Files excluded from the dead-code (unused-exports) report by default. These are
14
+ * REGEX sources (matched against each reported file path), not globs — dead-code's
15
+ * `ignoreFiles` is regex-based. Test and mock files legitimately export helpers and
16
+ * mocks consumed only by the test runner (jest.mock, dynamic imports), which
17
+ * ts-unused-exports cannot see as usage, so flagging them is noise. Clients can
18
+ * override via `deadCode.ignoreFiles` in pr-checkmate.json (setting it replaces
19
+ * this list).
20
+ */
21
+ export declare const DEAD_CODE_IGNORE_DEFAULTS: string[];
5
22
  export declare const CONFIG_FILE_PATTERNS: string[];
23
+ /**
24
+ * jscpd formats the duplicate-CODE check scans. Restricting to code formats keeps
25
+ * config/CI YAML and JSON out of the report — they are data, not code, and their
26
+ * repetition is not meaningful duplication. This mirrors the code-only file set the
27
+ * check already uses when scanning a git diff, so full-tree and diff scans agree.
28
+ */
29
+ export declare const DUPLICATE_CODE_FORMATS: string[];
6
30
  /**
7
31
  * File extensions to check
8
32
  */
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.CONFIG_FILE = exports.documentation = exports.DEFAULT_ALLOW_PACKAGES = exports.DEFAULT_COMMANDS = exports.SOURCE_EXTENSIONS = exports.CONFIG_FILE_PATTERNS = exports.IGNORE_PATTERNS = void 0;
6
+ exports.CONFIG_FILE = exports.documentation = exports.DEFAULT_ALLOW_PACKAGES = exports.DEFAULT_COMMANDS = exports.SOURCE_EXTENSIONS = exports.DUPLICATE_CODE_FORMATS = exports.CONFIG_FILE_PATTERNS = exports.DEAD_CODE_IGNORE_DEFAULTS = exports.DUPLICATE_IGNORE_DEFAULTS = exports.IGNORE_PATTERNS = void 0;
7
7
  const node_path_1 = __importDefault(require("node:path"));
8
8
  /**
9
9
  * Common patterns to ignore in all checks
@@ -57,14 +57,66 @@ exports.IGNORE_PATTERNS = [
57
57
  '**/lint-report/**',
58
58
  '**/audit-report/**',
59
59
  ];
60
+ /**
61
+ * Test files/folders excluded from duplicate-code detection by default.
62
+ * Test suites legitimately repeat setup/arrange/assert boilerplate, so counting
63
+ * them as clones is noise that drowns out real production duplication. Clients can
64
+ * override this list via `duplicate.ignore` in pr-checkmate.json.
65
+ */
66
+ exports.DUPLICATE_IGNORE_DEFAULTS = [
67
+ '**/__tests__/**',
68
+ '**/__mocks__/**',
69
+ '**/*.test.*',
70
+ '**/*.spec.*',
71
+ '**/test/**',
72
+ '**/tests/**',
73
+ ];
74
+ /**
75
+ * Files excluded from the dead-code (unused-exports) report by default. These are
76
+ * REGEX sources (matched against each reported file path), not globs — dead-code's
77
+ * `ignoreFiles` is regex-based. Test and mock files legitimately export helpers and
78
+ * mocks consumed only by the test runner (jest.mock, dynamic imports), which
79
+ * ts-unused-exports cannot see as usage, so flagging them is noise. Clients can
80
+ * override via `deadCode.ignoreFiles` in pr-checkmate.json (setting it replaces
81
+ * this list).
82
+ */
83
+ exports.DEAD_CODE_IGNORE_DEFAULTS = [
84
+ '/__tests__/',
85
+ '/__mocks__/',
86
+ '/__mock__/',
87
+ '/tests?/',
88
+ '\\.test\\.',
89
+ '\\.spec\\.',
90
+ ];
60
91
  exports.CONFIG_FILE_PATTERNS = [
61
92
  '**/*.config.js',
93
+ '**/*.config.cjs',
94
+ '**/*.config.mjs',
62
95
  '**/*.config.ts',
96
+ '**/.eslintrc.*',
97
+ '**/eslint.config.*',
63
98
  '**/jest.config.*',
64
99
  '**/wdio.config.*',
65
100
  '**/playwright.config.*',
66
101
  '**/cypress.config.*',
67
102
  ];
103
+ /**
104
+ * jscpd formats the duplicate-CODE check scans. Restricting to code formats keeps
105
+ * config/CI YAML and JSON out of the report — they are data, not code, and their
106
+ * repetition is not meaningful duplication. This mirrors the code-only file set the
107
+ * check already uses when scanning a git diff, so full-tree and diff scans agree.
108
+ */
109
+ exports.DUPLICATE_CODE_FORMATS = [
110
+ 'javascript',
111
+ 'typescript',
112
+ 'jsx',
113
+ 'tsx',
114
+ 'python',
115
+ 'kotlin',
116
+ 'swift',
117
+ 'c',
118
+ 'cpp',
119
+ ];
68
120
  /**
69
121
  * File extensions to check
70
122
  */
@@ -13,11 +13,3 @@ export declare function getPackageConfigPath(fileName: string): string;
13
13
  * Check if project has local config
14
14
  */
15
15
  export declare function hasLocalConfig(configNames: string[]): boolean;
16
- /**
17
- * For backward compatibility, also export as constant
18
- * But this will be evaluated at module load time
19
- */
20
- export declare const config: {
21
- sourcePath: string;
22
- };
23
- export declare const SOURCE_PATH: string;
@@ -17,7 +17,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
17
17
  return (mod && mod.__esModule) ? mod : { "default": mod };
18
18
  };
19
19
  Object.defineProperty(exports, "__esModule", { value: true });
20
- exports.SOURCE_PATH = exports.config = exports.loadProjectConfig = void 0;
20
+ exports.loadProjectConfig = void 0;
21
21
  exports.getSourcePath = getSourcePath;
22
22
  exports.getPackageConfigPath = getPackageConfigPath;
23
23
  exports.hasLocalConfig = hasLocalConfig;
@@ -65,9 +65,3 @@ function hasLocalConfig(configNames) {
65
65
  const projectRoot = process.cwd();
66
66
  return configNames.some(name => node_fs_1.default.existsSync(node_path_1.default.join(projectRoot, name)));
67
67
  }
68
- /**
69
- * For backward compatibility, also export as constant
70
- * But this will be evaluated at module load time
71
- */
72
- exports.config = loadConfig();
73
- exports.SOURCE_PATH = exports.config.sourcePath;
@@ -4,11 +4,3 @@ import { PRCheckMateConfig } from '../interfaces';
4
4
  * Falls back to defaults if not found
5
5
  */
6
6
  export declare function loadProjectConfig(): PRCheckMateConfig;
7
- /**
8
- * Get default configuration
9
- */
10
- export declare function getDefaultConfig(): PRCheckMateConfig;
11
- /**
12
- * Export config for external use
13
- */
14
- export declare const DEFAULT_CONFIG: PRCheckMateConfig;
@@ -3,9 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.DEFAULT_CONFIG = void 0;
7
6
  exports.loadProjectConfig = loadProjectConfig;
8
- exports.getDefaultConfig = getDefaultConfig;
9
7
  const node_fs_1 = __importDefault(require("node:fs"));
10
8
  const node_path_1 = __importDefault(require("node:path"));
11
9
  const utils_1 = require("../utils");
@@ -71,6 +69,8 @@ function mergeWithDefaults(userConfig) {
71
69
  diffSmell: { ...defaults.diffSmell, ...userConfig.diffSmell },
72
70
  prBody: { ...defaults.prBody, ...userConfig.prBody },
73
71
  duplicate: { ...defaults.duplicate, ...userConfig.duplicate },
72
+ dependency: { ...defaults.dependency, ...userConfig.dependency },
73
+ outdatedDeps: { ...defaults.outdatedDeps, ...userConfig.outdatedDeps },
74
74
  commitlint: { ...defaults.commitlint, ...userConfig.commitlint },
75
75
  deadCode: { ...defaults.deadCode, ...userConfig.deadCode },
76
76
  licenseCheck: {
@@ -146,8 +146,16 @@ function getDefaultConfig() {
146
146
  kotlin: { enabled: true },
147
147
  diffSmell: { enabled: true },
148
148
  prBody: { enabled: true, minLength: 20, requireTicket: true },
149
- duplicate: { enabled: true, minLines: 5, minTokens: 50 },
150
- deadCode: { enabled: true },
149
+ duplicate: {
150
+ enabled: true,
151
+ minLines: 5,
152
+ minTokens: 50,
153
+ ignore: constants_1.DUPLICATE_IGNORE_DEFAULTS,
154
+ formats: constants_1.DUPLICATE_CODE_FORMATS,
155
+ },
156
+ dependency: { enabled: true, ignore: [] },
157
+ outdatedDeps: { enabled: true, ignore: [] },
158
+ deadCode: { enabled: true, ignoreFiles: constants_1.DEAD_CODE_IGNORE_DEFAULTS },
151
159
  commitlint: {
152
160
  enabled: true,
153
161
  types: [
@@ -174,7 +182,3 @@ function getDefaultConfig() {
174
182
  },
175
183
  };
176
184
  }
177
- /**
178
- * Export config for external use
179
- */
180
- exports.DEFAULT_CONFIG = getDefaultConfig();
@@ -5,9 +5,9 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.circularDepsCheck = void 0;
7
7
  const node_path_1 = __importDefault(require("node:path"));
8
- const execa_1 = require("execa");
9
8
  const config_1 = require("../../config");
10
9
  const utils_1 = require("../../utils");
10
+ const execa_1 = require("../../utils/execa");
11
11
  const types_1 = require("../types");
12
12
  exports.circularDepsCheck = {
13
13
  name: 'Circular Deps',
@@ -7,9 +7,9 @@ exports.commitlintCheck = void 0;
7
7
  const node_fs_1 = __importDefault(require("node:fs"));
8
8
  const node_os_1 = __importDefault(require("node:os"));
9
9
  const node_path_1 = __importDefault(require("node:path"));
10
- const execa_1 = require("execa");
11
10
  const config_1 = require("../../config");
12
11
  const utils_1 = require("../../utils");
12
+ const execa_1 = require("../../utils/execa");
13
13
  const types_1 = require("../types");
14
14
  const LOCAL_CONFIG_FILES = [
15
15
  'commitlint.config.cjs',
@@ -6,8 +6,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.cppFormatCheck = void 0;
7
7
  const node_fs_1 = __importDefault(require("node:fs"));
8
8
  const node_path_1 = __importDefault(require("node:path"));
9
- const execa_1 = require("execa");
10
9
  const utils_1 = require("../../utils");
10
+ const execa_1 = require("../../utils/execa");
11
11
  const types_1 = require("../types");
12
12
  /**
13
13
  * C/C++ source & header extensions clang-format understands. `.h` is intentionally
@@ -6,8 +6,9 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.deadCodeCheck = void 0;
7
7
  const node_fs_1 = __importDefault(require("node:fs"));
8
8
  const node_path_1 = __importDefault(require("node:path"));
9
- const execa_1 = require("execa");
9
+ const constants_1 = require("../../config/constants");
10
10
  const utils_1 = require("../../utils");
11
+ const execa_1 = require("../../utils/execa");
11
12
  const types_1 = require("../types");
12
13
  exports.deadCodeCheck = {
13
14
  name: 'Dead Code',
@@ -24,6 +25,12 @@ exports.deadCodeCheck = {
24
25
  }
25
26
  const bin = (0, utils_1.resolvePackageBin)('ts-unused-exports', 'bin/ts-unused-exports');
26
27
  try {
28
+ // Run the full analysis unfiltered: ignore patterns are applied to the
29
+ // *report* below, not passed to ts-unused-exports' own `--ignoreFiles`.
30
+ // That flag drops files from analysis entirely, so a file's imports stop
31
+ // counting as usage — ignoring a barrel or entry point would then cascade
32
+ // and flag everything it re-exports. Filtering the output instead keeps
33
+ // every usage edge intact and only hides the exempted files' own reports.
27
34
  const result = await (0, execa_1.execa)(process.execPath, [bin, tsConfigPath], {
28
35
  cwd: ctx.cwd,
29
36
  reject: false,
@@ -33,14 +40,31 @@ exports.deadCodeCheck = {
33
40
  utils_1.logger.info('[Dead Code]: ✅ No unused exports found');
34
41
  return (0, types_1.pass)();
35
42
  }
36
- const lines = output.split('\n').filter(l => l.trim());
37
- const fileCount = lines.length;
38
- const exportCount = lines.reduce((sum, line) => {
39
- const match = line.match(/\[(\d+)\]:/);
40
- return sum + (match ? parseInt(match[1], 10) : 1);
41
- }, 0);
43
+ const ignore = compileIgnorePatterns(ctx.config.deadCode?.ignoreFiles ?? constants_1.DEAD_CODE_IGNORE_DEFAULTS);
44
+ // ts-unused-exports prints one `path: sym1, sym2` line per file, preceded
45
+ // by a `N modules with unused exports` summary line we skip.
46
+ const entries = output
47
+ .split('\n')
48
+ .map(line => line.trim().match(/^(.+\.[cm]?tsx?): (.+)$/))
49
+ .filter((match) => match !== null)
50
+ .map(match => ({
51
+ file: match[1],
52
+ symbols: match[2]
53
+ .split(',')
54
+ .map(sym => sym.trim())
55
+ .filter(Boolean),
56
+ }))
57
+ .filter(entry => !ignore.some(re => re.test(entry.file)));
58
+ if (entries.length === 0) {
59
+ utils_1.logger.info('[Dead Code]: ✅ No unused exports found');
60
+ return (0, types_1.pass)();
61
+ }
62
+ const fileCount = entries.length;
63
+ const exportCount = entries.reduce((sum, entry) => sum + entry.symbols.length, 0);
42
64
  utils_1.logger.warn(`[Dead Code]: ⚠️ ${exportCount} unused export(s) across ${fileCount} file(s):`);
43
- utils_1.logger.warn(output);
65
+ for (const entry of entries) {
66
+ utils_1.logger.warn(`${entry.file}: ${entry.symbols.join(', ')}`);
67
+ }
44
68
  return (0, types_1.warn)(`${exportCount} unused export(s) in ${fileCount} file(s)`);
45
69
  }
46
70
  catch (err) {
@@ -49,3 +73,18 @@ exports.deadCodeCheck = {
49
73
  }
50
74
  },
51
75
  };
76
+ /**
77
+ * Compile `deadCode.ignoreFiles` regex sources, skipping any that fail to parse
78
+ * (a bad pattern warns rather than aborting the whole check).
79
+ */
80
+ function compileIgnorePatterns(patterns) {
81
+ return (patterns ?? []).filter(Boolean).flatMap(pattern => {
82
+ try {
83
+ return [new RegExp(pattern)];
84
+ }
85
+ catch {
86
+ utils_1.logger.warn(`[Dead Code]: ⚠️ Ignoring invalid ignoreFiles pattern: ${pattern}`);
87
+ return [];
88
+ }
89
+ });
90
+ }
@@ -6,13 +6,15 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.dependencyCheck = void 0;
7
7
  const node_fs_1 = __importDefault(require("node:fs"));
8
8
  const node_path_1 = __importDefault(require("node:path"));
9
- const execa_1 = require("execa");
10
9
  const utils_1 = require("../../utils");
10
+ const execa_1 = require("../../utils/execa");
11
11
  const types_1 = require("../types");
12
12
  exports.dependencyCheck = {
13
13
  name: 'Dependencies',
14
14
  phase: 'blocking',
15
15
  applies(ctx) {
16
+ if (ctx.config.dependency?.enabled === false)
17
+ return false;
16
18
  return node_fs_1.default.existsSync(node_path_1.default.join(ctx.cwd, 'package.json'));
17
19
  },
18
20
  async run(ctx) {
@@ -25,8 +27,16 @@ exports.dependencyCheck = {
25
27
  catch {
26
28
  return (0, types_1.skip)('depcheck unavailable');
27
29
  }
30
+ // Dependency names/globs to skip, configured in pr-checkmate.json. Passed via
31
+ // --ignores so a single pr-checkmate.json is the source of truth; depcheck's
32
+ // CLI --ignores overrides any .depcheckrc, so we only pass it when non-empty
33
+ // (letting a client's own .depcheckrc still apply when nothing is configured).
34
+ const ignore = ctx.config.dependency?.ignore ?? [];
35
+ const args = [depcheckBin, '--json'];
36
+ if (ignore.length > 0)
37
+ args.push(`--ignores=${ignore.join(',')}`);
28
38
  // reject:false — depcheck exits non-zero when it finds issues but still emits JSON on stdout.
29
- const { stdout } = await (0, execa_1.execa)(process.execPath, [depcheckBin, '--json'], {
39
+ const { stdout } = await (0, execa_1.execa)(process.execPath, args, {
30
40
  cwd: ctx.cwd,
31
41
  reject: false,
32
42
  });
@@ -6,10 +6,16 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.duplicateCheck = void 0;
7
7
  const node_fs_1 = __importDefault(require("node:fs"));
8
8
  const node_path_1 = __importDefault(require("node:path"));
9
- const execa_1 = require("execa");
9
+ const constants_1 = require("../../config/constants");
10
10
  const utils_1 = require("../../utils");
11
11
  const delta_1 = require("../../utils/delta");
12
+ const execa_1 = require("../../utils/execa");
12
13
  const types_1 = require("../types");
14
+ // Always excluded regardless of client config — scanning these never yields
15
+ // meaningful production duplication. Config files (eslint/*.config.*) are data,
16
+ // not logic, and are commonly shipped in two formats (e.g. .eslintrc + flat
17
+ // config), so their repetition is expected rather than a code smell.
18
+ const BASE_IGNORE = ['node_modules/**', 'dist/**', '.git/**', ...constants_1.CONFIG_FILE_PATTERNS];
13
19
  // jscpd v5 appends promotional/funding lines after its report. Strip them so
14
20
  // they never reach the client's terminal when running our package.
15
21
  const JSCPD_PROMO_MARKERS = [
@@ -44,6 +50,14 @@ exports.duplicateCheck = {
44
50
  const srcPath = node_path_1.default.join(ctx.cwd, 'src');
45
51
  const targetPaths = changedFiles ?? (node_fs_1.default.existsSync(srcPath) ? [srcPath] : [ctx.cwd]);
46
52
  const jscpdBin = (0, utils_1.resolvePackageBin)('jscpd', 'run-jscpd.js');
53
+ // Client-configurable exclusions (pr-checkmate.json `duplicate.ignore`),
54
+ // defaulting to test files/folders, appended to the always-on base ignores.
55
+ const configuredIgnore = ctx.config.duplicate?.ignore ?? constants_1.DUPLICATE_IGNORE_DEFAULTS;
56
+ const ignore = [...BASE_IGNORE, ...configuredIgnore].join(',');
57
+ // Client-configurable formats (pr-checkmate.json `duplicate.formats`),
58
+ // defaulting to source-code formats so data files (yaml/json/markdown) are
59
+ // not scanned. Clients can add them back explicitly.
60
+ const formats = (ctx.config.duplicate?.formats ?? constants_1.DUPLICATE_CODE_FORMATS).join(',');
47
61
  try {
48
62
  const result = await (0, execa_1.execa)(process.execPath, [
49
63
  jscpdBin,
@@ -53,8 +67,10 @@ exports.duplicateCheck = {
53
67
  String(minTokens),
54
68
  '--reporters',
55
69
  'console',
70
+ '--format',
71
+ formats,
56
72
  '--ignore',
57
- 'node_modules/**,dist/**,.git/**',
73
+ ignore,
58
74
  ...targetPaths,
59
75
  ], { cwd: ctx.cwd, reject: false });
60
76
  const output = stripJscpdPromo([result.stdout, result.stderr].filter(Boolean).join('\n'));
@@ -4,8 +4,8 @@ exports.diffAddedLines = diffAddedLines;
4
4
  exports.bucketTotal = bucketTotal;
5
5
  exports.logFindings = logFindings;
6
6
  exports.reportSeverityBuckets = reportSeverityBuckets;
7
- const execa_1 = require("execa");
8
7
  const utils_1 = require("../../utils");
8
+ const execa_1 = require("../../utils/execa");
9
9
  const types_1 = require("../types");
10
10
  /**
11
11
  * Run `git diff` over the given file globs, returning the raw unified diff
@@ -1,8 +1,8 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.kotlinLintCheck = void 0;
4
- const execa_1 = require("execa");
5
4
  const utils_1 = require("../../utils");
5
+ const execa_1 = require("../../utils/execa");
6
6
  const targets_1 = require("../targets");
7
7
  const types_1 = require("../types");
8
8
  const KOTLIN_GLOBS = ['*.kt', '*.kts'];
@@ -6,8 +6,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.licenseCheck = void 0;
7
7
  const node_fs_1 = __importDefault(require("node:fs"));
8
8
  const node_path_1 = __importDefault(require("node:path"));
9
- const execa_1 = require("execa");
10
9
  const utils_1 = require("../../utils");
10
+ const execa_1 = require("../../utils/execa");
11
11
  const types_1 = require("../types");
12
12
  const FORBIDDEN_LICENSES = ['GPL-2.0', 'GPL-3.0', 'AGPL-3.0', 'LGPL-2.0', 'LGPL-2.1', 'LGPL-3.0'];
13
13
  /** Strip the trailing "@version" to get the bare package name (scoped-safe). */
@@ -5,10 +5,10 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.lintCheck = void 0;
7
7
  const node_path_1 = __importDefault(require("node:path"));
8
- const execa_1 = require("execa");
9
8
  const config_1 = require("../../config");
10
9
  const utils_1 = require("../../utils");
11
10
  const delta_1 = require("../../utils/delta");
11
+ const execa_1 = require("../../utils/execa");
12
12
  const types_1 = require("../types");
13
13
  async function getESLintMajorVersion(eslintBin) {
14
14
  try {
@@ -7,10 +7,10 @@ exports.markdownLintCheck = void 0;
7
7
  const node_fs_1 = __importDefault(require("node:fs"));
8
8
  const node_os_1 = __importDefault(require("node:os"));
9
9
  const node_path_1 = __importDefault(require("node:path"));
10
- const execa_1 = require("execa");
11
10
  const config_1 = require("../../config");
12
11
  const utils_1 = require("../../utils");
13
12
  const delta_1 = require("../../utils/delta");
13
+ const execa_1 = require("../../utils/execa");
14
14
  const types_1 = require("../types");
15
15
  function loadUserMarkdownConfig(cwd) {
16
16
  const configPath = node_path_1.default.join(cwd, 'pr-checkmate.json');
@@ -6,8 +6,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.npmAuditCheck = void 0;
7
7
  const node_fs_1 = __importDefault(require("node:fs"));
8
8
  const node_path_1 = __importDefault(require("node:path"));
9
- const execa_1 = require("execa");
10
9
  const utils_1 = require("../../utils");
10
+ const execa_1 = require("../../utils/execa");
11
11
  const types_1 = require("../types");
12
12
  const SEVERITY_ORDER = ['info', 'low', 'moderate', 'high', 'critical'];
13
13
  function isSeverityAtLeast(severity, level) {
@@ -6,13 +6,15 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.outdatedDepsCheck = void 0;
7
7
  const node_fs_1 = __importDefault(require("node:fs"));
8
8
  const node_path_1 = __importDefault(require("node:path"));
9
- const execa_1 = require("execa");
10
9
  const utils_1 = require("../../utils");
10
+ const execa_1 = require("../../utils/execa");
11
11
  const types_1 = require("../types");
12
12
  exports.outdatedDepsCheck = {
13
13
  name: 'Outdated Deps',
14
14
  phase: 'informational',
15
15
  applies(ctx) {
16
+ if (ctx.config.outdatedDeps?.enabled === false)
17
+ return false;
16
18
  return node_fs_1.default.existsSync(node_path_1.default.join(ctx.cwd, 'package.json'));
17
19
  },
18
20
  async run(ctx) {
@@ -27,7 +29,10 @@ exports.outdatedDepsCheck = {
27
29
  catch {
28
30
  return (0, types_1.pass)();
29
31
  }
30
- const entries = Object.entries(outdated);
32
+ // Packages deliberately pinned behind their latest major (see outdatedDeps.ignore
33
+ // in pr-checkmate.json) are dropped before counting, so they never warn.
34
+ const ignore = new Set(ctx.config.outdatedDeps?.ignore ?? []);
35
+ const entries = Object.entries(outdated).filter(([pkg]) => !ignore.has(pkg));
31
36
  if (entries.length === 0)
32
37
  return (0, types_1.pass)();
33
38
  const majorBehind = entries.filter(([, info]) => {
@@ -1,8 +1,8 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.prSizeCheck = void 0;
4
- const execa_1 = require("execa");
5
4
  const utils_1 = require("../../utils");
5
+ const execa_1 = require("../../utils/execa");
6
6
  const types_1 = require("../types");
7
7
  const WARN_FILES = 50;
8
8
  const WARN_LINES = 1000;
@@ -6,10 +6,10 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.prettierCheck = void 0;
7
7
  const node_fs_1 = __importDefault(require("node:fs"));
8
8
  const node_path_1 = __importDefault(require("node:path"));
9
- const execa_1 = require("execa");
10
9
  const config_1 = require("../../config");
11
10
  const utils_1 = require("../../utils");
12
11
  const delta_1 = require("../../utils/delta");
12
+ const execa_1 = require("../../utils/execa");
13
13
  const types_1 = require("../types");
14
14
  function loadUserPrettierConfig(cwd) {
15
15
  const configPath = node_path_1.default.join(cwd, 'pr-checkmate.json');
@@ -9,7 +9,7 @@ const node_path_1 = __importDefault(require("node:path"));
9
9
  const utils_1 = require("../../utils");
10
10
  const targets_1 = require("../targets");
11
11
  const types_1 = require("../types");
12
- const PY_GLOBS = ['*.py'];
12
+ const ruff_shared_1 = require("./ruff-shared");
13
13
  /**
14
14
  * Formats changed Python files with Ruff's formatter (WASM, no Python/pip needed).
15
15
  * Read-only in check mode (`warn` when files differ from formatted output); rewrites
@@ -24,13 +24,12 @@ exports.pythonFormatCheck = {
24
24
  ctx.languages.includes('python'));
25
25
  },
26
26
  async run(ctx) {
27
- const targets = await (0, targets_1.resolveTargetFiles)(ctx, PY_GLOBS);
27
+ const targets = await (0, targets_1.resolveTargetFiles)(ctx, ruff_shared_1.PY_GLOBS);
28
28
  if (targets === null)
29
29
  return (0, types_1.skip)('git unavailable');
30
30
  if (targets.length === 0)
31
31
  return (0, types_1.pass)('no Python files changed');
32
- const { Workspace, PositionEncoding } = require('@astral-sh/ruff-wasm-nodejs');
33
- const workspace = new Workspace(Workspace.defaultSettings(), PositionEncoding.Utf16);
32
+ const workspace = (0, ruff_shared_1.createRuffWorkspace)();
34
33
  let unformatted = 0;
35
34
  for (const rel of targets) {
36
35
  const abs = node_path_1.default.join(ctx.cwd, rel);
@@ -9,7 +9,7 @@ const node_path_1 = __importDefault(require("node:path"));
9
9
  const utils_1 = require("../../utils");
10
10
  const targets_1 = require("../targets");
11
11
  const types_1 = require("../types");
12
- const PY_GLOBS = ['*.py'];
12
+ const ruff_shared_1 = require("./ruff-shared");
13
13
  /**
14
14
  * Lints changed Python files with Ruff, bundled as a self-contained WASM module
15
15
  * (`@astral-sh/ruff-wasm-nodejs`) so it runs on a bare Node image with no Python
@@ -26,13 +26,12 @@ exports.pythonLintCheck = {
26
26
  ctx.languages.includes('python'));
27
27
  },
28
28
  async run(ctx) {
29
- const targets = await (0, targets_1.resolveTargetFiles)(ctx, PY_GLOBS);
29
+ const targets = await (0, targets_1.resolveTargetFiles)(ctx, ruff_shared_1.PY_GLOBS);
30
30
  if (targets === null)
31
31
  return (0, types_1.skip)('git unavailable');
32
32
  if (targets.length === 0)
33
33
  return (0, types_1.pass)('no Python files changed');
34
- const { Workspace, PositionEncoding } = require('@astral-sh/ruff-wasm-nodejs');
35
- const workspace = new Workspace(Workspace.defaultSettings(), PositionEncoding.Utf16);
34
+ const workspace = (0, ruff_shared_1.createRuffWorkspace)();
36
35
  let totalIssues = 0;
37
36
  let filesWithIssues = 0;
38
37
  for (const rel of targets) {
@@ -0,0 +1,10 @@
1
+ type RuffModule = typeof import('@astral-sh/ruff-wasm-nodejs');
2
+ type RuffWorkspace = InstanceType<RuffModule['Workspace']>;
3
+ export declare const PY_GLOBS: string[];
4
+ /**
5
+ * Instantiate a Ruff WASM workspace with default settings. Shared by the Ruff
6
+ * lint and format checks so their setup stays identical, and kept as a function
7
+ * (not a module-level singleton) so the WASM module loads only when a check runs.
8
+ */
9
+ export declare function createRuffWorkspace(): RuffWorkspace;
10
+ export {};
@@ -0,0 +1,14 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.PY_GLOBS = void 0;
4
+ exports.createRuffWorkspace = createRuffWorkspace;
5
+ exports.PY_GLOBS = ['*.py'];
6
+ /**
7
+ * Instantiate a Ruff WASM workspace with default settings. Shared by the Ruff
8
+ * lint and format checks so their setup stays identical, and kept as a function
9
+ * (not a module-level singleton) so the WASM module loads only when a check runs.
10
+ */
11
+ function createRuffWorkspace() {
12
+ const { Workspace, PositionEncoding } = require('@astral-sh/ruff-wasm-nodejs');
13
+ return new Workspace(Workspace.defaultSettings(), PositionEncoding.Utf16);
14
+ }
@@ -1,9 +1,9 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.securityCheck = void 0;
4
- const execa_1 = require("execa");
5
4
  const config_1 = require("../../config");
6
5
  const utils_1 = require("../../utils");
6
+ const execa_1 = require("../../utils/execa");
7
7
  const types_1 = require("../types");
8
8
  /**
9
9
  * Secret scanning via the bundled gitleaks package. Language-agnostic — runs on
@@ -1,8 +1,8 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.sensitiveFileGuardCheck = void 0;
4
- const execa_1 = require("execa");
5
4
  const utils_1 = require("../../utils");
5
+ const execa_1 = require("../../utils/execa");
6
6
  const types_1 = require("../types");
7
7
  const findings_1 = require("./findings");
8
8
  const SENSITIVE_CATEGORIES = [
@@ -6,10 +6,10 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.spellcheckCheck = void 0;
7
7
  const node_fs_1 = __importDefault(require("node:fs"));
8
8
  const node_path_1 = __importDefault(require("node:path"));
9
- const execa_1 = require("execa");
10
9
  const config_1 = require("../../config");
11
10
  const utils_1 = require("../../utils");
12
11
  const delta_1 = require("../../utils/delta");
12
+ const execa_1 = require("../../utils/execa");
13
13
  const types_1 = require("../types");
14
14
  function loadUserCspellConfig(cwd) {
15
15
  const configPath = node_path_1.default.join(cwd, 'pr-checkmate.json');
@@ -1,8 +1,8 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.swiftLintCheck = void 0;
4
- const execa_1 = require("execa");
5
4
  const utils_1 = require("../../utils");
5
+ const execa_1 = require("../../utils/execa");
6
6
  const targets_1 = require("../targets");
7
7
  const types_1 = require("../types");
8
8
  const SWIFT_GLOBS = ['*.swift'];
@@ -6,8 +6,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.typecheckCheck = void 0;
7
7
  const node_fs_1 = __importDefault(require("node:fs"));
8
8
  const node_path_1 = __importDefault(require("node:path"));
9
- const execa_1 = require("execa");
10
9
  const utils_1 = require("../../utils");
10
+ const execa_1 = require("../../utils/execa");
11
11
  const types_1 = require("../types");
12
12
  /**
13
13
  * Prefer the consumer project's own TypeScript so its tsconfig is compiled by the
@@ -1,10 +1,10 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.buildContext = buildContext;
4
- const execa_1 = require("execa");
5
4
  const loader_1 = require("../config/loader");
6
5
  const utils_1 = require("../utils");
7
6
  const detect_language_1 = require("../utils/detect-language");
7
+ const execa_1 = require("../utils/execa");
8
8
  /**
9
9
  * Resolve the diff range for delta-aware checks.
10
10
  * Prefers explicit opts, then GitHub PR event, then Bitbucket PR env, then git HEAD^.
@@ -1,7 +1,6 @@
1
- import type { CheckReport, CheckResult } from './types';
1
+ import type { CheckReport } from './types';
2
2
  /**
3
3
  * Emit the report through the selected channel.
4
4
  * none: nothing · stdout: console table · github: table + upsert PR comment.
5
5
  */
6
6
  export declare function report(kind: 'none' | 'stdout' | 'github', result: CheckReport): Promise<void>;
7
- export type { CheckResult };
@@ -54,7 +54,12 @@ function errMessage(err) {
54
54
  async function runOne(check, ctx, collector) {
55
55
  try {
56
56
  if (!(await check.applies(ctx))) {
57
- collector.register(check.name, 'skip', 'not applicable');
57
+ // A check that doesn't apply is irrelevant to this project/run — e.g. a
58
+ // language-specific check (SwiftLint, ktlint, Ruff, C++) in a repo of a
59
+ // different language, or a check disabled in config. Omit it from the report
60
+ // entirely rather than listing it as "not applicable": in a single-language
61
+ // repo those rows are pure noise and inflate the skipped count. Runtime skips
62
+ // from run() (e.g. "no relevant files changed") are still reported.
58
63
  return;
59
64
  }
60
65
  const outcome = await check.run(ctx);
@@ -1,8 +1,8 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.resolveTargetFiles = resolveTargetFiles;
4
- const execa_1 = require("execa");
5
4
  const utils_1 = require("../utils");
5
+ const execa_1 = require("../utils/execa");
6
6
  /**
7
7
  * Lists the files a language check should operate on, matched by pathspec `globs`
8
8
  * (e.g. `['*.py']`). Uses the PR diff range when available (delta mode), else
package/dist/init.d.ts CHANGED
@@ -1,4 +1,3 @@
1
- export declare const CONFIG_FILE: string;
2
1
  /**
3
2
  * Initialize (or refresh) pr-checkmate configuration in the current project.
4
3
  */
package/dist/init.js CHANGED
@@ -3,14 +3,13 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.CONFIG_FILE = void 0;
7
6
  exports.init = init;
8
7
  const fs_1 = __importDefault(require("fs"));
9
8
  const path_1 = __importDefault(require("path"));
10
9
  const inquirer_1 = __importDefault(require("inquirer"));
11
10
  const config_1 = require("./config");
12
11
  const utils_1 = require("./utils");
13
- exports.CONFIG_FILE = path_1.default.resolve(process.cwd(), 'pr-checkmate.json');
12
+ const CONFIG_FILE = path_1.default.resolve(process.cwd(), 'pr-checkmate.json');
14
13
  /**
15
14
  * Default configuration structure with customization options
16
15
  */
@@ -60,6 +59,26 @@ const DEFAULT_CONFIG = {
60
59
  enabled: true,
61
60
  minLines: 5,
62
61
  minTokens: 50,
62
+ // Test files/folders are excluded by default; edit to customise.
63
+ ignore: config_1.DUPLICATE_IGNORE_DEFAULTS,
64
+ // Only source-code formats are scanned. Data formats (yaml, json, markdown)
65
+ // are intentionally absent — add them here to detect duplication in them too.
66
+ formats: config_1.DUPLICATE_CODE_FORMATS,
67
+ },
68
+ // Dependency usage check (depcheck)
69
+ dependency: {
70
+ enabled: true,
71
+ // Dependency names/globs to skip (e.g. CLI-only tools invoked as binaries,
72
+ // which depcheck can't see as imports and would report as unused).
73
+ ignore: [],
74
+ },
75
+ // Unused-exports check (ts-unused-exports)
76
+ deadCode: {
77
+ enabled: true,
78
+ // Regex sources matched against file paths; matching files are dropped from
79
+ // the report. Defaults to test/mock files, whose exports are used only by the
80
+ // test runner. Add barrels/entry points here to exempt intentional exports.
81
+ ignoreFiles: config_1.DEAD_CODE_IGNORE_DEFAULTS,
63
82
  },
64
83
  documentation: config_1.documentation,
65
84
  };
@@ -135,7 +154,7 @@ async function createConfig(root) {
135
154
  sourcePath,
136
155
  languages,
137
156
  };
138
- fs_1.default.writeFileSync(exports.CONFIG_FILE, JSON.stringify(config, null, 2));
157
+ fs_1.default.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
139
158
  utils_1.logger.info(`[init]: ✅ Created pr-checkmate.json with sourcePath: ${sourcePath}`);
140
159
  utils_1.logger.info(`[init]: 💡 You can now customize ESLint rules, Prettier settings,
141
160
  and cspell words in pr-checkmate.json`);
@@ -146,7 +165,7 @@ async function createConfig(root) {
146
165
  async function updateConfig(root) {
147
166
  utils_1.logger.info('[init]: pr-checkmate.json already exists — updating it.');
148
167
  try {
149
- const raw = fs_1.default.readFileSync(exports.CONFIG_FILE, 'utf-8');
168
+ const raw = fs_1.default.readFileSync(CONFIG_FILE, 'utf-8');
150
169
  const json = JSON.parse(raw);
151
170
  const languages = await (0, utils_1.detectLanguages)(root);
152
171
  utils_1.logger.info(`[init]: 🔍 Detected languages: ${languages.length ? languages.join(', ') : 'none'}`);
@@ -164,7 +183,7 @@ async function updateConfig(root) {
164
183
  prBody: { ...DEFAULT_CONFIG.prBody, ...(json.prBody || {}) },
165
184
  duplicate: { ...DEFAULT_CONFIG.duplicate, ...(json.duplicate || {}) },
166
185
  };
167
- fs_1.default.writeFileSync(exports.CONFIG_FILE, JSON.stringify(updated, null, 2));
186
+ fs_1.default.writeFileSync(CONFIG_FILE, JSON.stringify(updated, null, 2));
168
187
  utils_1.logger.info('[init]: 🔧 Updated pr-checkmate.json with customization options.');
169
188
  utils_1.logger.info('[init]: 💡 You can now add custom ESLint rules, cspell words, and more!');
170
189
  }
@@ -177,7 +196,7 @@ async function updateConfig(root) {
177
196
  */
178
197
  async function init() {
179
198
  const root = process.cwd();
180
- if (fs_1.default.existsSync(exports.CONFIG_FILE)) {
199
+ if (fs_1.default.existsSync(CONFIG_FILE)) {
181
200
  await updateConfig(root);
182
201
  }
183
202
  else {
@@ -72,6 +72,42 @@ export interface PRCheckMateConfig {
72
72
  enabled?: boolean;
73
73
  minLines?: number;
74
74
  minTokens?: number;
75
+ /**
76
+ * Glob patterns excluded from duplicate detection, in addition to the always-on
77
+ * node_modules/dist/.git ignores. Defaults to test files/folders
78
+ * (see DUPLICATE_IGNORE_DEFAULTS). Setting this replaces the default list.
79
+ */
80
+ ignore?: string[];
81
+ /**
82
+ * jscpd formats to scan. Defaults to source-code formats only
83
+ * (see DUPLICATE_CODE_FORMATS) — data formats like `yaml`, `json` and
84
+ * `markdown` are intentionally NOT scanned, since their repetition is rarely
85
+ * meaningful duplication. Add them back (e.g. `["typescript", "yaml"]`) if you
86
+ * do want to detect duplication in those files. Setting this replaces the list.
87
+ */
88
+ formats?: string[];
89
+ };
90
+ dependency?: {
91
+ enabled?: boolean;
92
+ /**
93
+ * Dependency names to exclude from the unused/missing check (depcheck).
94
+ * Supports globs, e.g. "@types/*". Use for packages that are invoked only as
95
+ * CLI binaries or referenced from config — depcheck sees no `import` for them
96
+ * and would otherwise report them as unused. Passed to depcheck's `--ignores`,
97
+ * which takes precedence over any `.depcheckrc` file.
98
+ */
99
+ ignore?: string[];
100
+ };
101
+ outdatedDeps?: {
102
+ enabled?: boolean;
103
+ /**
104
+ * Exact package names to exclude from the outdated-deps warning, e.g.
105
+ * "commander". Use for dependencies deliberately pinned behind their latest
106
+ * major (a breaking upgrade you have chosen not to take, or one held back by
107
+ * an `overrides` entry) so `npm outdated` no longer flags them as major-behind.
108
+ * Matched by exact name — no globs or version ranges.
109
+ */
110
+ ignore?: string[];
75
111
  };
76
112
  commitlint?: {
77
113
  enabled?: boolean;
@@ -97,33 +133,23 @@ export interface PRCheckMateConfig {
97
133
  };
98
134
  deadCode?: {
99
135
  enabled?: boolean;
136
+ /**
137
+ * Regex sources matched against reported file paths; matching files are
138
+ * dropped from the dead-code report. Use to exempt intentional public
139
+ * surface — a package entry point (`src/index.ts`) or re-export barrels
140
+ * whose exports are consumed by external installers, not within the repo.
141
+ * Applied to the report (not ts-unused-exports' own `--ignoreFiles`), so it
142
+ * never removes a file from analysis and cannot cascade onto other files.
143
+ * Defaults to test/mock files (see DEAD_CODE_IGNORE_DEFAULTS), whose exports
144
+ * are consumed only by the test runner and would otherwise show as unused.
145
+ * Setting this replaces the default list.
146
+ */
147
+ ignoreFiles?: string[];
100
148
  };
101
149
  licenseCheck?: {
102
150
  allowPackages?: string[];
103
151
  allowLicenses?: string[];
104
152
  };
105
153
  }
106
- export interface CheckmateConfig {
107
- skipLint?: boolean;
108
- skipDeps?: boolean;
109
- skipFormat?: boolean;
110
- skipSpell?: boolean;
111
- skipSecurity?: boolean;
112
- skipNpmAudit?: boolean;
113
- skipCircularDeps?: boolean;
114
- skipLicense?: boolean;
115
- skipPrSize?: boolean;
116
- skipPrTitle?: boolean;
117
- skipOutdated?: boolean;
118
- skipMarkdown?: boolean;
119
- skipTypecheck?: boolean;
120
- skipDiffSmell?: boolean;
121
- skipPrBody?: boolean;
122
- skipDuplicate?: boolean;
123
- skipDiffSecurity?: boolean;
124
- skipSensitiveFiles?: boolean;
125
- skipDeadCode?: boolean;
126
- autoCommit?: boolean;
127
- }
128
154
  export type { CheckStatus, CheckResult } from '../utils/result-collector';
129
155
  export type { Language } from '../utils/detect-language';
@@ -1,5 +1 @@
1
- /**
2
- *
3
- */
4
- declare function copyConfigs(): void;
5
- export { copyConfigs };
1
+ export {};
@@ -3,7 +3,6 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.copyConfigs = copyConfigs;
7
6
  const node_fs_1 = __importDefault(require("node:fs"));
8
7
  const node_path_1 = __importDefault(require("node:path"));
9
8
  const utils_1 = require("../utils");
@@ -6,7 +6,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.getChangedFiles = getChangedFiles;
7
7
  const node_fs_1 = __importDefault(require("node:fs"));
8
8
  const node_path_1 = __importDefault(require("node:path"));
9
- const execa_1 = require("execa");
9
+ const execa_1 = require("./execa");
10
10
  const logger_1 = require("./logger");
11
11
  /**
12
12
  * Returns list of changed files in the current PR filtered by extension.
@@ -6,7 +6,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.detectLanguages = detectLanguages;
7
7
  const node_fs_1 = __importDefault(require("node:fs"));
8
8
  const node_path_1 = __importDefault(require("node:path"));
9
- const execa_1 = require("execa");
9
+ const execa_1 = require("./execa");
10
10
  const logger_1 = require("./logger");
11
11
  /**
12
12
  * Marker files that unambiguously indicate a language's presence, independent of
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Lazy loader for {@link https://github.com/sindresorhus/execa | execa}.
3
+ *
4
+ * execa v9 is ESM-only. Because this package compiles to CommonJS, importing
5
+ * execa at module scope forces every consumer to resolve execa's full
6
+ * dependency graph the moment they import pr-checkmate — even consumers that
7
+ * only need the types or a config helper and never run a check. That graph
8
+ * pulls in transitive packages (e.g. `unicorn-magic`) whose `exports` maps some
9
+ * loaders such as `tsx` fail to resolve, which crashes the import for those
10
+ * consumers before any check runs.
11
+ *
12
+ * Loading execa on first use keeps the library entry point cheap and avoids
13
+ * dragging the subprocess toolchain into consumers that never spawn a process.
14
+ */
15
+ import type { Options, Result } from 'execa';
16
+ /**
17
+ * Run a command via execa, loading the module lazily on first call.
18
+ *
19
+ * Drop-in replacement for `execa(file, args, options)` at every call site in
20
+ * this package — all of which await the result and read it synchronously.
21
+ */
22
+ export declare function execa<OptionsType extends Options = {}>(file: string, args?: readonly string[], options?: OptionsType): Promise<Result<OptionsType>>;
@@ -0,0 +1,13 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.execa = execa;
4
+ /**
5
+ * Run a command via execa, loading the module lazily on first call.
6
+ *
7
+ * Drop-in replacement for `execa(file, args, options)` at every call site in
8
+ * this package — all of which await the result and read it synchronously.
9
+ */
10
+ async function execa(file, args, options) {
11
+ const { execa: run } = await import('execa');
12
+ return (await run(file, args, options));
13
+ }
@@ -19,15 +19,19 @@ function readDependencyNames(pkgDir) {
19
19
  return [];
20
20
  }
21
21
  }
22
- /** Walk up from this module until we find pr-checkmate's own package root. */
23
- function findOwnPackageRoot() {
24
- let dir = __dirname;
22
+ /**
23
+ * Walk up from `startDir` (inclusive) toward the filesystem root, returning the
24
+ * first directory whose package.json declares `name === expectedName`, or null.
25
+ * Broken/missing package.json files are skipped rather than thrown.
26
+ */
27
+ function findPackageRootByName(startDir, expectedName) {
28
+ let dir = startDir;
25
29
  while (dir !== node_path_1.default.dirname(dir)) {
26
30
  const pkgJsonPath = node_path_1.default.join(dir, PACKAGE_JSON);
27
31
  if (node_fs_1.default.existsSync(pkgJsonPath)) {
28
32
  try {
29
33
  const pkg = JSON.parse(node_fs_1.default.readFileSync(pkgJsonPath, 'utf-8'));
30
- if (pkg.name === OWN_PACKAGE_NAME)
34
+ if (pkg.name === expectedName)
31
35
  return dir;
32
36
  }
33
37
  catch { }
@@ -36,6 +40,10 @@ function findOwnPackageRoot() {
36
40
  }
37
41
  return null;
38
42
  }
43
+ /** Walk up from this module until we find pr-checkmate's own package root. */
44
+ function findOwnPackageRoot() {
45
+ return findPackageRootByName(__dirname, OWN_PACKAGE_NAME);
46
+ }
39
47
  /**
40
48
  * Resolve a dependency's install directory by name, starting the search from
41
49
  * `fromDir`. Relies on Node's upward node_modules walk so hoisted packages are
@@ -50,19 +58,8 @@ function resolvePackageDir(name, fromDir) {
50
58
  // Fallback for packages whose exports map hides ./package.json: resolve the
51
59
  // entry point and walk up to the package's own root.
52
60
  try {
53
- let dir = node_path_1.default.dirname(require.resolve(name, { paths: [fromDir] }));
54
- while (dir !== node_path_1.default.dirname(dir)) {
55
- const pkgJsonPath = node_path_1.default.join(dir, PACKAGE_JSON);
56
- if (node_fs_1.default.existsSync(pkgJsonPath)) {
57
- try {
58
- const pkg = JSON.parse(node_fs_1.default.readFileSync(pkgJsonPath, 'utf-8'));
59
- if (pkg.name === name)
60
- return dir;
61
- }
62
- catch { }
63
- }
64
- dir = node_path_1.default.dirname(dir);
65
- }
61
+ const entryDir = node_path_1.default.dirname(require.resolve(name, { paths: [fromDir] }));
62
+ return findPackageRootByName(entryDir, name);
66
63
  }
67
64
  catch { }
68
65
  return null;
package/eslint.config.mjs CHANGED
@@ -36,7 +36,7 @@ export const defaultRules = {
36
36
  { blankLine: 'always', prev: '*', next: 'return' },
37
37
  ],
38
38
  // Complexity analysis (#61)
39
- 'sonarjs/cognitive-complexity': ['warn', 15],
39
+ 'sonarjs/cognitive-complexity': ['warn', 30],
40
40
  'sonarjs/no-duplicate-string': ['warn', { threshold: 3 }],
41
41
  'sonarjs/no-identical-functions': 'warn',
42
42
  // Import ordering (#65)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pr-checkmate",
3
- "version": "1.22.3",
3
+ "version": "1.22.5",
4
4
  "description": "Automated PR quality checks: linting, formatting, dependency analysis, and spellcheck",
5
5
  "keywords": [
6
6
  "github-actions",
@@ -81,7 +81,7 @@ module.exports = {
81
81
  { blankLine: 'always', prev: '*', next: 'return' },
82
82
  ],
83
83
  // Complexity analysis (#61)
84
- complexity: ['warn', 15],
84
+ complexity: ['warn', 30],
85
85
  // Import ordering (#65)
86
86
  'import-x/order': [
87
87
  'warn',
@@ -36,7 +36,7 @@ export const defaultRules = {
36
36
  { blankLine: 'always', prev: '*', next: 'return' },
37
37
  ],
38
38
  // Complexity analysis (#61)
39
- 'sonarjs/cognitive-complexity': ['warn', 15],
39
+ 'sonarjs/cognitive-complexity': ['warn', 30],
40
40
  'sonarjs/no-duplicate-string': ['warn', { threshold: 3 }],
41
41
  'sonarjs/no-identical-functions': 'warn',
42
42
  // Import ordering (#65)
@@ -1,2 +0,0 @@
1
- export declare const config: any;
2
- export declare const SOURCE_PATH: any;
@@ -1,12 +0,0 @@
1
- "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
- Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.SOURCE_PATH = exports.config = void 0;
7
- const node_fs_1 = __importDefault(require("node:fs"));
8
- const constants_1 = require("./constants");
9
- exports.config = node_fs_1.default.existsSync(constants_1.CONFIG_FILE)
10
- ? JSON.parse(node_fs_1.default.readFileSync(constants_1.CONFIG_FILE, 'utf-8'))
11
- : { sourcePath: 'src' };
12
- exports.SOURCE_PATH = exports.config.sourcePath;