nestjs-doctor 0.1.1 → 0.1.3
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 +9 -3
- package/dist/api/index.cjs +257 -79
- package/dist/api/index.d.cts +40 -1
- package/dist/api/index.d.cts.map +1 -1
- package/dist/api/index.d.mts +40 -1
- package/dist/api/index.d.mts.map +1 -1
- package/dist/api/index.mjs +226 -80
- package/dist/api/index.mjs.map +1 -1
- package/dist/cli/index.mjs +177 -78
- package/package.json +11 -2
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
|
-
|
|
257
|
-
|
|
258
|
-
|
|
262
|
+
pnpm install
|
|
263
|
+
pnpm run build
|
|
264
|
+
pnpm test
|
|
259
265
|
```
|
|
260
266
|
|
|
261
267
|
### Adding a rule
|
package/dist/api/index.cjs
CHANGED
|
@@ -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
|
|
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
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
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 (
|
|
1848
|
-
if (
|
|
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 (
|
|
1864
|
-
if (
|
|
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 *
|
|
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
|
-
|
|
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 =
|
|
2382
|
+
const { diagnostics: rawDiagnostics, errors } = runRules(astProject, files, filterRules(config), {
|
|
2304
2383
|
moduleGraph,
|
|
2305
2384
|
providers,
|
|
2306
2385
|
config
|
|
2307
|
-
})
|
|
2308
|
-
|
|
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
|
|
2396
|
+
score,
|
|
2311
2397
|
diagnostics,
|
|
2312
|
-
project
|
|
2313
|
-
|
|
2314
|
-
|
|
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
|
|
2342
|
-
const filesByProject = await collectMonorepoFiles(targetPath, monorepo,
|
|
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
|
|
2349
|
-
project
|
|
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 =
|
|
2446
|
+
const { diagnostics: rawDiagnostics, errors } = runRules(astProject, files, filterRules(projectConfig), {
|
|
2354
2447
|
moduleGraph,
|
|
2355
2448
|
providers,
|
|
2356
|
-
config
|
|
2357
|
-
})
|
|
2358
|
-
|
|
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
|
|
2459
|
+
score,
|
|
2361
2460
|
diagnostics,
|
|
2362
|
-
project
|
|
2363
|
-
|
|
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
|
-
|
|
2398
|
-
total:
|
|
2399
|
-
errors:
|
|
2400
|
-
warnings:
|
|
2401
|
-
info:
|
|
2510
|
+
const summary = {
|
|
2511
|
+
total: 0,
|
|
2512
|
+
errors: 0,
|
|
2513
|
+
warnings: 0,
|
|
2514
|
+
info: 0,
|
|
2402
2515
|
byCategory: {
|
|
2403
|
-
security:
|
|
2404
|
-
performance:
|
|
2405
|
-
correctness:
|
|
2406
|
-
architecture:
|
|
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((
|
|
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((
|
|
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;
|
package/dist/api/index.d.cts
CHANGED
|
@@ -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
|
package/dist/api/index.d.cts.map
CHANGED
|
@@ -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;;;
|
|
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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"}
|