nestjs-doctor 0.1.1 → 0.1.2

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
@@ -24,6 +24,12 @@
24
24
  npx nestjs-doctor@latest .
25
25
  ```
26
26
 
27
+ For file paths and line numbers:
28
+
29
+ ```bash
30
+ npx nestjs-doctor@latest . --verbose
31
+ ```
32
+
27
33
  No config, no plugins, no setup.
28
34
 
29
35
  ```
@@ -253,9 +259,9 @@ mono.combined; // Merged DiagnoseResult
253
259
  ```bash
254
260
  git clone https://github.com/RoloBits/nestjs-doctor.git
255
261
  cd nestjs-doctor
256
- npm install
257
- npm run build
258
- npm test
262
+ pnpm install
263
+ pnpm run build
264
+ pnpm test
259
265
  ```
260
266
 
261
267
  ### Adding a rule
@@ -1,9 +1,39 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
2
+ //#region \0rolldown/runtime.js
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __copyProps = (to, from, except, desc) => {
10
+ if (from && typeof from === "object" || typeof from === "function") {
11
+ for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
12
+ key = keys[i];
13
+ if (!__hasOwnProp.call(to, key) && key !== except) {
14
+ __defProp(to, key, {
15
+ get: ((k) => from[k]).bind(null, key),
16
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
17
+ });
18
+ }
19
+ }
20
+ }
21
+ return to;
22
+ };
23
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
24
+ value: mod,
25
+ enumerable: true
26
+ }) : target, mod));
27
+
28
+ //#endregion
29
+ let node_fs = require("node:fs");
2
30
  let node_path = require("node:path");
3
31
  let node_perf_hooks = require("node:perf_hooks");
4
32
  let ts_morph = require("ts-morph");
5
33
  let node_fs_promises = require("node:fs/promises");
6
34
  let tinyglobby = require("tinyglobby");
35
+ let picomatch = require("picomatch");
36
+ picomatch = __toESM(picomatch);
7
37
 
8
38
  //#region src/engine/ast-parser.ts
9
39
  function createAstParser(files) {
@@ -60,9 +90,12 @@ function buildModuleGraph(project, files) {
60
90
  for (const imp of node.imports) if (modules.has(imp)) importSet.add(imp);
61
91
  edges.set(name, importSet);
62
92
  }
93
+ const providerToModule = /* @__PURE__ */ new Map();
94
+ for (const mod of modules.values()) for (const provider of mod.providers) providerToModule.set(provider, mod);
63
95
  return {
64
96
  modules,
65
- edges
97
+ edges,
98
+ providerToModule
66
99
  };
67
100
  }
68
101
  function extractArrayPropertyNames(obj, propertyName) {
@@ -110,6 +143,7 @@ function isProjectRule(rule) {
110
143
  //#region src/engine/rule-runner.ts
111
144
  function runRules(project, files, rules, options) {
112
145
  const diagnostics = [];
146
+ const errors = [];
113
147
  const fileRules = [];
114
148
  const projectRules = [];
115
149
  for (const rule of rules) if (isProjectRule(rule)) projectRules.push(rule);
@@ -132,7 +166,12 @@ function runRules(project, files, rules, options) {
132
166
  };
133
167
  try {
134
168
  rule.check(context);
135
- } catch {}
169
+ } catch (error) {
170
+ errors.push({
171
+ ruleId: rule.meta.id,
172
+ error
173
+ });
174
+ }
136
175
  }
137
176
  }
138
177
  for (const rule of projectRules) {
@@ -153,9 +192,17 @@ function runRules(project, files, rules, options) {
153
192
  };
154
193
  try {
155
194
  rule.check(context);
156
- } catch {}
195
+ } catch (error) {
196
+ errors.push({
197
+ ruleId: rule.meta.id,
198
+ error
199
+ });
200
+ }
157
201
  }
158
- return diagnostics;
202
+ return {
203
+ diagnostics,
204
+ errors
205
+ };
159
206
  }
160
207
 
161
208
  //#endregion
@@ -987,19 +1034,33 @@ const noMissingInjectable = {
987
1034
  },
988
1035
  check(context) {
989
1036
  const providerNames = new Set([...context.providers.values()].map((p) => p.name));
1037
+ const classIndex = /* @__PURE__ */ new Map();
1038
+ for (const filePath of context.files) {
1039
+ const sourceFile = context.project.getSourceFile(filePath);
1040
+ if (!sourceFile) continue;
1041
+ for (const cls of sourceFile.getClasses()) {
1042
+ const name = cls.getName();
1043
+ if (name) {
1044
+ const entries = classIndex.get(name) ?? [];
1045
+ entries.push({
1046
+ cls,
1047
+ filePath
1048
+ });
1049
+ classIndex.set(name, entries);
1050
+ }
1051
+ }
1052
+ }
990
1053
  for (const mod of context.moduleGraph.modules.values()) for (const providerName of mod.providers) {
991
1054
  if (providerNames.has(providerName)) continue;
992
- for (const filePath of context.files) {
993
- const sourceFile = context.project.getSourceFile(filePath);
994
- if (!sourceFile) continue;
995
- for (const cls of sourceFile.getClasses()) if (cls.getName() === providerName && !cls.getDecorator("Injectable")) context.report({
996
- filePath,
997
- message: `Class '${providerName}' is listed in '${mod.name}' providers but is missing @Injectable() decorator.`,
998
- help: this.meta.help,
999
- line: cls.getStartLineNumber(),
1000
- column: 1
1001
- });
1002
- }
1055
+ const classEntries = classIndex.get(providerName);
1056
+ if (!classEntries) continue;
1057
+ for (const { cls, filePath } of classEntries) if (!cls.getDecorator("Injectable")) context.report({
1058
+ filePath,
1059
+ message: `Class '${providerName}' is listed in '${mod.name}' providers but is missing @Injectable() decorator.`,
1060
+ help: this.meta.help,
1061
+ line: cls.getStartLineNumber(),
1062
+ column: 1
1063
+ });
1003
1064
  }
1004
1065
  }
1005
1066
  };
@@ -1814,6 +1875,17 @@ const VARIABLE_NAME_PATTERNS = [
1814
1875
  /access[_-]?key/i,
1815
1876
  /client[_-]?secret/i
1816
1877
  ];
1878
+ const PLACEHOLDER_VALUES = new Set([
1879
+ "your-secret-here",
1880
+ "changeme",
1881
+ "password"
1882
+ ]);
1883
+ function isSuspiciousValue(value) {
1884
+ return value.length >= 8 && !value.includes("${") && !value.startsWith("process.env") && !PLACEHOLDER_VALUES.has(value);
1885
+ }
1886
+ function hasSuspiciousName(name) {
1887
+ return VARIABLE_NAME_PATTERNS.some((p) => p.test(name));
1888
+ }
1817
1889
  const noHardcodedSecrets = {
1818
1890
  meta: {
1819
1891
  id: "security/no-hardcoded-secrets",
@@ -1843,11 +1915,9 @@ const noHardcodedSecrets = {
1843
1915
  for (const decl of variableDeclarations) {
1844
1916
  const name = decl.getName();
1845
1917
  const initializer = decl.getInitializer();
1846
- if (!initializer) continue;
1847
- if (initializer.getKind() !== ts_morph.SyntaxKind.StringLiteral) continue;
1848
- if (!VARIABLE_NAME_PATTERNS.some((p) => p.test(name))) continue;
1849
- const value = initializer.getText().slice(1, -1);
1850
- if (value.length >= 8 && !value.includes("${") && !value.startsWith("process.env") && value !== "your-secret-here" && value !== "changeme" && value !== "password") context.report({
1918
+ if (!initializer || initializer.getKind() !== ts_morph.SyntaxKind.StringLiteral) continue;
1919
+ if (!hasSuspiciousName(name)) continue;
1920
+ if (isSuspiciousValue(initializer.getText().slice(1, -1))) context.report({
1851
1921
  filePath: context.filePath,
1852
1922
  message: `Variable '${name}' appears to contain a hardcoded secret.`,
1853
1923
  help: this.meta.help,
@@ -1859,11 +1929,9 @@ const noHardcodedSecrets = {
1859
1929
  for (const prop of propertyAssignments) {
1860
1930
  const name = prop.getName();
1861
1931
  const initializer = prop.getInitializer();
1862
- if (!initializer) continue;
1863
- if (initializer.getKind() !== ts_morph.SyntaxKind.StringLiteral) continue;
1864
- if (!VARIABLE_NAME_PATTERNS.some((p) => p.test(name))) continue;
1865
- const value = initializer.getText().slice(1, -1);
1866
- if (value.length >= 8 && !value.includes("${") && !value.startsWith("process.env")) context.report({
1932
+ if (!initializer || initializer.getKind() !== ts_morph.SyntaxKind.StringLiteral) continue;
1933
+ if (!hasSuspiciousName(name)) continue;
1934
+ if (isSuspiciousValue(initializer.getText().slice(1, -1))) context.report({
1867
1935
  filePath: context.filePath,
1868
1936
  message: `Property '${name}' appears to contain a hardcoded secret.`,
1869
1937
  help: this.meta.help,
@@ -2105,6 +2173,26 @@ const CATEGORY_MULTIPLIERS = {
2105
2173
 
2106
2174
  //#endregion
2107
2175
  //#region src/scorer/index.ts
2176
+ /**
2177
+ * Calculates a health score from 0-100 based on diagnostics and file count.
2178
+ *
2179
+ * Scoring formula: `score = 100 - (totalPenalty / fileCount) * PENALTY_SCALE`
2180
+ *
2181
+ * Each diagnostic contributes a penalty of `severityWeight * categoryMultiplier`:
2182
+ * - A security error costs 3.0 * 1.5 = 4.5 penalty points
2183
+ * - A performance info costs 0.5 * 0.8 = 0.4 penalty points
2184
+ *
2185
+ * The penalty is normalized by file count so that a 10-file project and a
2186
+ * 500-file project with the same issue density receive similar scores.
2187
+ *
2188
+ * PENALTY_SCALE (10) was calibrated so that an average of ~1 error per file
2189
+ * (normalized penalty ≈ 10) brings the score to 0. In practice:
2190
+ * - 1 error per 10 files → penalty/file ≈ 0.45 → score ≈ 95 (Excellent)
2191
+ * - 1 error per 3 files → penalty/file ≈ 1.5 → score ≈ 85 (Good)
2192
+ * - 1 error per file → penalty/file ≈ 4.5 → score ≈ 55 (Fair)
2193
+ * - 2 errors per file → penalty/file ≈ 9.0 → score ≈ 10 (Critical)
2194
+ */
2195
+ const PENALTY_SCALE = 10;
2108
2196
  function calculateScore(diagnostics, fileCount) {
2109
2197
  if (fileCount === 0) return {
2110
2198
  value: 100,
@@ -2117,7 +2205,7 @@ function calculateScore(diagnostics, fileCount) {
2117
2205
  totalPenalty += severityWeight * categoryMultiplier;
2118
2206
  }
2119
2207
  const normalizedPenalty = totalPenalty / fileCount;
2120
- const value = Math.max(0, Math.min(100, Math.round(100 - normalizedPenalty * 10)));
2208
+ const value = Math.max(0, Math.min(100, Math.round(100 - normalizedPenalty * PENALTY_SCALE)));
2121
2209
  return {
2122
2210
  value,
2123
2211
  label: getScoreLabel(value)
@@ -2159,6 +2247,16 @@ async function readConfigFile(path) {
2159
2247
  const raw = await (0, node_fs_promises.readFile)(path, "utf-8");
2160
2248
  return mergeConfig(JSON.parse(raw));
2161
2249
  }
2250
+ /**
2251
+ * Merges user config with defaults.
2252
+ *
2253
+ * Merge semantics:
2254
+ * - `include`: user replaces defaults entirely (user likely wants a specific scope)
2255
+ * - `exclude`: user values are appended to defaults (additive, keeps safe defaults)
2256
+ * - `ignore.rules`: user replaces defaults (no default ignored rules)
2257
+ * - `ignore.files`: user replaces defaults (no default ignored files)
2258
+ * - `rules`, `categories`, `thresholds`: shallow-merged with user taking precedence
2259
+ */
2162
2260
  function mergeConfig(userConfig) {
2163
2261
  return {
2164
2262
  ...DEFAULT_CONFIG,
@@ -2187,30 +2285,8 @@ async function collectMonorepoFiles(targetPath, monorepo, config = {}) {
2187
2285
 
2188
2286
  //#endregion
2189
2287
  //#region src/core/match-glob-pattern.ts
2190
- const REGEX_SPECIAL_CHARACTERS = /[.+^${}()|[\]\\]/g;
2191
2288
  const compileGlobPattern = (pattern) => {
2192
- const normalizedPattern = pattern.replace(/\\/g, "/");
2193
- let regexSource = "^";
2194
- let characterIndex = 0;
2195
- while (characterIndex < normalizedPattern.length) if (normalizedPattern[characterIndex] === "*" && normalizedPattern[characterIndex + 1] === "*") if (normalizedPattern[characterIndex + 2] === "/") {
2196
- regexSource += "(?:.+/)?";
2197
- characterIndex += 3;
2198
- } else {
2199
- regexSource += ".*";
2200
- characterIndex += 2;
2201
- }
2202
- else if (normalizedPattern[characterIndex] === "*") {
2203
- regexSource += "[^/]*";
2204
- characterIndex++;
2205
- } else if (normalizedPattern[characterIndex] === "?") {
2206
- regexSource += "[^/]";
2207
- characterIndex++;
2208
- } else {
2209
- regexSource += normalizedPattern[characterIndex].replace(REGEX_SPECIAL_CHARACTERS, "\\$&");
2210
- characterIndex++;
2211
- }
2212
- regexSource += "$";
2213
- return new RegExp(regexSource);
2289
+ return picomatch.default.makeRe(pattern, { windows: false });
2214
2290
  };
2215
2291
 
2216
2292
  //#endregion
@@ -2291,27 +2367,42 @@ function detectFramework(deps) {
2291
2367
 
2292
2368
  //#endregion
2293
2369
  //#region src/core/scanner.ts
2370
+ function formatRuleError(error) {
2371
+ if (error instanceof Error) return error.message;
2372
+ return String(error);
2373
+ }
2294
2374
  async function scan(targetPath, options = {}) {
2295
2375
  const startTime = node_perf_hooks.performance.now();
2296
2376
  const config = await loadConfig(targetPath, options.config);
2297
2377
  const project = await detectProject(targetPath);
2298
2378
  const files = await collectFiles(targetPath, config);
2299
- project.fileCount = files.length;
2300
2379
  const astProject = createAstParser(files);
2301
2380
  const moduleGraph = buildModuleGraph(astProject, files);
2302
2381
  const providers = resolveProviders(astProject, files);
2303
- const diagnostics = filterIgnoredDiagnostics(runRules(astProject, files, filterRules(config), {
2382
+ const { diagnostics: rawDiagnostics, errors } = runRules(astProject, files, filterRules(config), {
2304
2383
  moduleGraph,
2305
2384
  providers,
2306
2385
  config
2307
- }), config);
2308
- project.moduleCount = moduleGraph.modules.size;
2386
+ });
2387
+ const diagnostics = filterIgnoredDiagnostics(rawDiagnostics, config);
2388
+ const score = calculateScore(diagnostics, files.length);
2389
+ const summary = buildSummary(diagnostics);
2390
+ const ruleErrors = errors.map((e) => ({
2391
+ ruleId: e.ruleId,
2392
+ error: formatRuleError(e.error)
2393
+ }));
2394
+ const elapsedMs = node_perf_hooks.performance.now() - startTime;
2309
2395
  return {
2310
- score: calculateScore(diagnostics, files.length),
2396
+ score,
2311
2397
  diagnostics,
2312
- project,
2313
- summary: buildSummary(diagnostics),
2314
- elapsedMs: node_perf_hooks.performance.now() - startTime
2398
+ project: {
2399
+ ...project,
2400
+ fileCount: files.length,
2401
+ moduleCount: moduleGraph.modules.size
2402
+ },
2403
+ summary,
2404
+ ruleErrors,
2405
+ elapsedMs
2315
2406
  };
2316
2407
  }
2317
2408
  function filterRules(config) {
@@ -2338,29 +2429,42 @@ async function scanMonorepo(targetPath, options = {}) {
2338
2429
  elapsedMs: result.elapsedMs
2339
2430
  };
2340
2431
  }
2341
- const config = await loadConfig(targetPath, options.config);
2342
- const filesByProject = await collectMonorepoFiles(targetPath, monorepo, config);
2432
+ const rootConfig = await loadConfig(targetPath, options.config);
2433
+ const filesByProject = await collectMonorepoFiles(targetPath, monorepo, rootConfig);
2343
2434
  const subProjects = [];
2344
2435
  const allDiagnostics = [];
2436
+ const allRuleErrors = [];
2345
2437
  let totalFiles = 0;
2346
2438
  for (const [name, files] of filesByProject) {
2347
2439
  if (files.length === 0) continue;
2348
- const project = await detectProject((0, node_path.join)(targetPath, monorepo.projects.get(name)));
2349
- project.fileCount = files.length;
2440
+ const projectPath = (0, node_path.join)(targetPath, monorepo.projects.get(name));
2441
+ const project = await detectProject(projectPath);
2442
+ const projectConfig = await loadConfigWithFallback(projectPath, rootConfig);
2350
2443
  const astProject = createAstParser(files);
2351
2444
  const moduleGraph = buildModuleGraph(astProject, files);
2352
2445
  const providers = resolveProviders(astProject, files);
2353
- const diagnostics = filterIgnoredDiagnostics(runRules(astProject, files, filterRules(config), {
2446
+ const { diagnostics: rawDiagnostics, errors } = runRules(astProject, files, filterRules(projectConfig), {
2354
2447
  moduleGraph,
2355
2448
  providers,
2356
- config
2357
- }), config);
2358
- project.moduleCount = moduleGraph.modules.size;
2449
+ config: projectConfig
2450
+ });
2451
+ const diagnostics = filterIgnoredDiagnostics(rawDiagnostics, projectConfig);
2452
+ const score = calculateScore(diagnostics, files.length);
2453
+ const summary = buildSummary(diagnostics);
2454
+ const ruleErrors = errors.map((e) => ({
2455
+ ruleId: e.ruleId,
2456
+ error: formatRuleError(e.error)
2457
+ }));
2359
2458
  const result = {
2360
- score: calculateScore(diagnostics, files.length),
2459
+ score,
2361
2460
  diagnostics,
2362
- project,
2363
- summary: buildSummary(diagnostics),
2461
+ project: {
2462
+ ...project,
2463
+ fileCount: files.length,
2464
+ moduleCount: moduleGraph.modules.size
2465
+ },
2466
+ summary,
2467
+ ruleErrors,
2364
2468
  elapsedMs: 0
2365
2469
  };
2366
2470
  subProjects.push({
@@ -2368,6 +2472,7 @@ async function scanMonorepo(targetPath, options = {}) {
2368
2472
  result
2369
2473
  });
2370
2474
  allDiagnostics.push(...diagnostics);
2475
+ allRuleErrors.push(...ruleErrors);
2371
2476
  totalFiles += files.length;
2372
2477
  }
2373
2478
  const combinedScore = calculateScore(allDiagnostics, totalFiles);
@@ -2388,36 +2493,109 @@ async function scanMonorepo(targetPath, options = {}) {
2388
2493
  moduleCount: subProjects.reduce((sum, sp) => sum + sp.result.project.moduleCount, 0)
2389
2494
  },
2390
2495
  summary: combinedSummary,
2496
+ ruleErrors: allRuleErrors,
2391
2497
  elapsedMs
2392
2498
  },
2393
2499
  elapsedMs
2394
2500
  };
2395
2501
  }
2502
+ async function loadConfigWithFallback(projectPath, fallback) {
2503
+ try {
2504
+ return await loadConfig(projectPath);
2505
+ } catch {
2506
+ return fallback;
2507
+ }
2508
+ }
2396
2509
  function buildSummary(diagnostics) {
2397
- return {
2398
- total: diagnostics.length,
2399
- errors: diagnostics.filter((d) => d.severity === "error").length,
2400
- warnings: diagnostics.filter((d) => d.severity === "warning").length,
2401
- info: diagnostics.filter((d) => d.severity === "info").length,
2510
+ const summary = {
2511
+ total: 0,
2512
+ errors: 0,
2513
+ warnings: 0,
2514
+ info: 0,
2402
2515
  byCategory: {
2403
- security: diagnostics.filter((d) => d.category === "security").length,
2404
- performance: diagnostics.filter((d) => d.category === "performance").length,
2405
- correctness: diagnostics.filter((d) => d.category === "correctness").length,
2406
- architecture: diagnostics.filter((d) => d.category === "architecture").length
2516
+ security: 0,
2517
+ performance: 0,
2518
+ correctness: 0,
2519
+ architecture: 0
2407
2520
  }
2408
2521
  };
2522
+ for (const d of diagnostics) {
2523
+ summary.total++;
2524
+ if (d.severity === "error") summary.errors++;
2525
+ else if (d.severity === "warning") summary.warnings++;
2526
+ else summary.info++;
2527
+ summary.byCategory[d.category]++;
2528
+ }
2529
+ return summary;
2409
2530
  }
2410
2531
 
2532
+ //#endregion
2533
+ //#region src/types/errors.ts
2534
+ var NestjsDoctorError = class extends Error {
2535
+ constructor(message) {
2536
+ super(message);
2537
+ this.name = "NestjsDoctorError";
2538
+ }
2539
+ };
2540
+ var ConfigurationError = class extends NestjsDoctorError {
2541
+ constructor(message) {
2542
+ super(message);
2543
+ this.name = "ConfigurationError";
2544
+ }
2545
+ };
2546
+ var ScanError = class extends NestjsDoctorError {
2547
+ constructor(message) {
2548
+ super(message);
2549
+ this.name = "ScanError";
2550
+ }
2551
+ };
2552
+ var ValidationError = class extends NestjsDoctorError {
2553
+ constructor(message) {
2554
+ super(message);
2555
+ this.name = "ValidationError";
2556
+ }
2557
+ };
2558
+
2411
2559
  //#endregion
2412
2560
  //#region src/api/index.ts
2561
+ function validatePath(path) {
2562
+ if (!path || path.trim() === "") throw new ValidationError("Path must be a non-empty string. Received an empty path.");
2563
+ const resolved = (0, node_path.resolve)(path);
2564
+ if (!(0, node_fs.existsSync)(resolved)) throw new ValidationError(`Path does not exist: ${resolved}`);
2565
+ if (!(0, node_fs.statSync)(resolved).isDirectory()) throw new ValidationError(`Path must be a directory, not a file: ${resolved}`);
2566
+ return resolved;
2567
+ }
2568
+ /**
2569
+ * Scans a single NestJS project and returns a health diagnostic result.
2570
+ *
2571
+ * @param path - Path to the NestJS project root directory.
2572
+ * @param options - Optional configuration: `config` specifies a path to a config file.
2573
+ * @returns A `DiagnoseResult` containing the health score, diagnostics, and summary.
2574
+ * @throws {ValidationError} If the path is empty, doesn't exist, or isn't a directory.
2575
+ */
2413
2576
  async function diagnose(path, options = {}) {
2414
- return await scan((0, node_path.resolve)(path), options);
2577
+ return await scan(validatePath(path), options);
2415
2578
  }
2579
+ /**
2580
+ * Scans a NestJS monorepo and returns per-project and combined diagnostics.
2581
+ *
2582
+ * Auto-detects monorepo structure from `nest-cli.json`. If the target is not a
2583
+ * monorepo, falls back to a single-project scan wrapped in the monorepo result format.
2584
+ *
2585
+ * @param path - Path to the monorepo root directory.
2586
+ * @param options - Optional configuration: `config` specifies a path to a config file.
2587
+ * @returns A `MonorepoResult` with sub-project results and combined score.
2588
+ * @throws {ValidationError} If the path is empty, doesn't exist, or isn't a directory.
2589
+ */
2416
2590
  async function diagnoseMonorepo(path, options = {}) {
2417
- return await scanMonorepo((0, node_path.resolve)(path), options);
2591
+ return await scanMonorepo(validatePath(path), options);
2418
2592
  }
2419
2593
 
2420
2594
  //#endregion
2595
+ exports.ConfigurationError = ConfigurationError;
2596
+ exports.NestjsDoctorError = NestjsDoctorError;
2597
+ exports.ScanError = ScanError;
2598
+ exports.ValidationError = ValidationError;
2421
2599
  exports.diagnose = diagnose;
2422
2600
  exports.diagnoseMonorepo = diagnoseMonorepo;
2423
2601
  exports.getRules = getRules;
@@ -13,6 +13,7 @@ interface ModuleNode {
13
13
  interface ModuleGraph {
14
14
  edges: Map<string, Set<string>>;
15
15
  modules: Map<string, ModuleNode>;
16
+ providerToModule: Map<string, ModuleNode>;
16
17
  }
17
18
  //#endregion
18
19
  //#region src/engine/type-resolver.d.ts
@@ -97,6 +98,20 @@ type AnyRule = Rule | ProjectRule;
97
98
  //#region src/rules/index.d.ts
98
99
  declare function getRules(): AnyRule[];
99
100
  //#endregion
101
+ //#region src/types/errors.d.ts
102
+ declare class NestjsDoctorError extends Error {
103
+ constructor(message: string);
104
+ }
105
+ declare class ConfigurationError extends NestjsDoctorError {
106
+ constructor(message: string);
107
+ }
108
+ declare class ScanError extends NestjsDoctorError {
109
+ constructor(message: string);
110
+ }
111
+ declare class ValidationError extends NestjsDoctorError {
112
+ constructor(message: string);
113
+ }
114
+ //#endregion
100
115
  //#region src/types/result.d.ts
101
116
  interface Score {
102
117
  label: string;
@@ -117,10 +132,15 @@ interface DiagnoseSummary {
117
132
  total: number;
118
133
  warnings: number;
119
134
  }
135
+ interface RuleErrorInfo {
136
+ error: string;
137
+ ruleId: string;
138
+ }
120
139
  interface DiagnoseResult {
121
140
  diagnostics: Diagnostic[];
122
141
  elapsedMs: number;
123
142
  project: ProjectInfo;
143
+ ruleErrors: RuleErrorInfo[];
124
144
  score: Score;
125
145
  summary: DiagnoseSummary;
126
146
  }
@@ -136,12 +156,31 @@ interface MonorepoResult {
136
156
  }
137
157
  //#endregion
138
158
  //#region src/api/index.d.ts
159
+ /**
160
+ * Scans a single NestJS project and returns a health diagnostic result.
161
+ *
162
+ * @param path - Path to the NestJS project root directory.
163
+ * @param options - Optional configuration: `config` specifies a path to a config file.
164
+ * @returns A `DiagnoseResult` containing the health score, diagnostics, and summary.
165
+ * @throws {ValidationError} If the path is empty, doesn't exist, or isn't a directory.
166
+ */
139
167
  declare function diagnose(path: string, options?: {
140
168
  config?: string;
141
169
  }): Promise<DiagnoseResult>;
170
+ /**
171
+ * Scans a NestJS monorepo and returns per-project and combined diagnostics.
172
+ *
173
+ * Auto-detects monorepo structure from `nest-cli.json`. If the target is not a
174
+ * monorepo, falls back to a single-project scan wrapped in the monorepo result format.
175
+ *
176
+ * @param path - Path to the monorepo root directory.
177
+ * @param options - Optional configuration: `config` specifies a path to a config file.
178
+ * @returns A `MonorepoResult` with sub-project results and combined score.
179
+ * @throws {ValidationError} If the path is empty, doesn't exist, or isn't a directory.
180
+ */
142
181
  declare function diagnoseMonorepo(path: string, options?: {
143
182
  config?: string;
144
183
  }): Promise<MonorepoResult>;
145
184
  //#endregion
146
- export { type AnyRule, type Category, type DiagnoseResult, type DiagnoseSummary, type Diagnostic, type MonorepoResult, type NestjsDoctorConfig, type ProjectInfo, type ProjectRule, type ProjectRuleContext, type Rule, type RuleContext, type RuleMeta, type Score, type Severity, type SubProjectResult, diagnose, diagnoseMonorepo, getRules };
185
+ export { type AnyRule, type Category, ConfigurationError, type DiagnoseResult, type DiagnoseSummary, type Diagnostic, type MonorepoResult, type NestjsDoctorConfig, NestjsDoctorError, type ProjectInfo, type ProjectRule, type ProjectRuleContext, type Rule, type RuleContext, type RuleErrorInfo, type RuleMeta, ScanError, type Score, type Severity, type SubProjectResult, ValidationError, diagnose, diagnoseMonorepo, getRules };
147
186
  //# sourceMappingURL=index.d.cts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.cts","names":[],"sources":["../../src/engine/module-graph.ts","../../src/engine/type-resolver.ts","../../src/types/diagnostic.ts","../../src/types/config.ts","../../src/rules/types.ts","../../src/rules/index.ts","../../src/types/result.ts","../../src/api/index.ts"],"mappings":";;;UASiB,UAAA;EAChB,gBAAA,EAAkB,gBAAA;EAClB,WAAA;EACA,OAAA;EACA,QAAA;EACA,OAAA;EACA,IAAA;EACA,SAAA;AAAA;AAAA,UAGgB,WAAA;EAChB,KAAA,EAAO,GAAA,SAAY,GAAA;EACnB,OAAA,EAAS,GAAA,SAAY,UAAA;AAAA;;;UChBL,YAAA;EAChB,gBAAA,EAAkB,gBAAA;EAClB,YAAA;EACA,QAAA;EACA,IAAA;EACA,iBAAA;AAAA;;;KCVW,QAAA;AAAA,KACA,QAAA;AAAA,UAMK,UAAA;EAChB,QAAA,EAAU,QAAA;EACV,MAAA;EACA,QAAA;EACA,IAAA;EACA,IAAA;EACA,OAAA;EACA,IAAA;EACA,QAAA,EAAU,QAAA;AAAA;;;UCbM,YAAA;EAChB,OAAA;EACA,QAAA,GAAW,QAAA;AAAA;AAAA,UAGK,wBAAA;EAChB,KAAA;EACA,KAAA;AAAA;AAAA,UAGgB,kBAAA;EAChB,UAAA,GAAa,OAAA,CAAQ,MAAA,CAAO,QAAA;EAC5B,OAAA;EACA,MAAA,GAAS,wBAAA;EACT,OAAA;EACA,KAAA,GAAQ,MAAA,SAAe,YAAA;EACvB,UAAA;IACC,kBAAA;IACA,gBAAA;IACA,iBAAA;IACA,cAAA;EAAA;AAAA;;;KChBU,SAAA;AAAA,UAEK,QAAA;EAChB,QAAA,EAAU,QAAA;EACV,WAAA;EACA,IAAA;EACA,EAAA;EACA,KAAA,GAAQ,SAAA;EACR,QAAA,EAAU,QAAA;AAAA;AAAA,UAGM,WAAA;EAChB,QAAA;EACA,MAAA,CAAO,UAAA,EAAY,IAAA,CAAK,UAAA;EACxB,UAAA,EAAY,UAAA;AAAA;AAAA,UAGI,kBAAA;EAChB,MAAA,EAAQ,kBAAA;EACR,KAAA;EACA,WAAA,EAAa,WAAA;EACb,OAAA,EAAS,OAAA;EACT,SAAA,EAAW,GAAA,SAAY,YAAA;EACvB,MAAA,CAAO,UAAA,EAAY,IAAA,CAAK,UAAA;AAAA;AAAA,UAGR,IAAA;EAChB,KAAA,CAAM,OAAA,EAAS,WAAA;EACf,IAAA,EAAM,QAAA;AAAA;AAAA,UAGU,WAAA;EAChB,KAAA,CAAM,OAAA,EAAS,kBAAA;EACf,IAAA,EAAM,QAAA;AAAA;AAAA,KAGK,OAAA,GAAU,IAAA,GAAO,WAAA;;;iBCsEb,QAAA,CAAA,GAAY,OAAA;;;UC9GX,KAAA;EAChB,KAAA;EACA,KAAA;AAAA;AAAA,UAGgB,WAAA;EAChB,SAAA;EACA,SAAA;EACA,WAAA;EACA,IAAA;EACA,WAAA;EACA,GAAA;AAAA;AAAA,UAGgB,eAAA;EAChB,UAAA,EAAY,MAAA,CAAO,QAAA;EACnB,MAAA;EACA,IAAA;EACA,KAAA;EACA,QAAA;AAAA;AAAA,UAGgB,cAAA;EAChB,WAAA,EAAa,UAAA;EACb,SAAA;EACA,OAAA,EAAS,WAAA;EACT,KAAA,EAAO,KAAA;EACP,OAAA,EAAS,eAAA;AAAA;AAAA,UAGO,gBAAA;EAChB,IAAA;EACA,MAAA,EAAQ,cAAA;AAAA;AAAA,UAGQ,cAAA;EAChB,QAAA,EAAU,cAAA;EACV,SAAA;EACA,UAAA;EACA,WAAA,EAAa,gBAAA;AAAA;;;iBCjBQ,QAAA,CACrB,IAAA,UACA,OAAA;EAAW,MAAA;AAAA,IAAsB,OAAA,CAFJ,cAAA;AAAA,iBAQR,gBAAA,CACrB,IAAA,UACA,OAAA;EAAW,MAAA;AAAA,IAAsB,OAAA,CAFI,cAAA"}
1
+ {"version":3,"file":"index.d.cts","names":[],"sources":["../../src/engine/module-graph.ts","../../src/engine/type-resolver.ts","../../src/types/diagnostic.ts","../../src/types/config.ts","../../src/rules/types.ts","../../src/rules/index.ts","../../src/types/errors.ts","../../src/types/result.ts","../../src/api/index.ts"],"mappings":"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"}