@kb-labs/quality-core 2.94.0 → 2.98.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/dead-code/index.d.ts +20 -2
- package/dist/dead-code/index.js +55 -1
- package/dist/dead-code/index.js.map +1 -1
- package/dist/health/index.d.ts +21 -34
- package/dist/health/index.js +83 -111
- package/dist/health/index.js.map +1 -1
- package/dist/index.d.ts +100 -3
- package/dist/index.js +618 -245
- package/dist/index.js.map +1 -1
- package/dist/stale/index.js.map +1 -1
- package/dist/stats/index.d.ts +4 -9
- package/dist/stats/index.js +66 -36
- package/dist/stats/index.js.map +1 -1
- package/dist/tests/index.js.map +1 -1
- package/dist/types/index.js +1 -1
- package/dist/types/index.js.map +1 -1
- package/package.json +5 -5
package/dist/index.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
import fs8 from 'fs';
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
4
|
-
import
|
|
5
|
-
import
|
|
1
|
+
import fs8, { existsSync, readFileSync, mkdirSync, writeFileSync } from 'fs';
|
|
2
|
+
import path9, { join, dirname } from 'path';
|
|
3
|
+
import { stat, readFile } from 'fs/promises';
|
|
4
|
+
import { exec, execSync, execFile } from 'child_process';
|
|
5
|
+
import globby2 from 'globby';
|
|
6
|
+
import { defaultQualityConfig, DIMENSION_WEIGHTS, MAX_SNAPSHOTS_DEFAULT, SNAPSHOT_DIR, SNAPSHOT_FILE, HEALTH_GRADES } from '@kb-labs/quality-contracts';
|
|
6
7
|
import { promisify } from 'util';
|
|
7
8
|
import ts from 'typescript';
|
|
9
|
+
import { randomUUID } from 'crypto';
|
|
8
10
|
|
|
9
11
|
// src/graph/dependency-graph.ts
|
|
10
12
|
function buildDependencyGraph(rootDir) {
|
|
@@ -18,8 +20,8 @@ function buildDependencyGraph(rootDir) {
|
|
|
18
20
|
if (!entry.isDirectory() || !entry.name.startsWith("kb-labs-")) {
|
|
19
21
|
continue;
|
|
20
22
|
}
|
|
21
|
-
const repoPath =
|
|
22
|
-
const packagesDir =
|
|
23
|
+
const repoPath = path9.join(rootDir, entry.name);
|
|
24
|
+
const packagesDir = path9.join(repoPath, "packages");
|
|
23
25
|
if (!fs8.existsSync(packagesDir)) {
|
|
24
26
|
continue;
|
|
25
27
|
}
|
|
@@ -28,7 +30,7 @@ function buildDependencyGraph(rootDir) {
|
|
|
28
30
|
if (!pkgDir.isDirectory()) {
|
|
29
31
|
continue;
|
|
30
32
|
}
|
|
31
|
-
const packageJsonPath =
|
|
33
|
+
const packageJsonPath = path9.join(packagesDir, pkgDir.name, "package.json");
|
|
32
34
|
if (fs8.existsSync(packageJsonPath)) {
|
|
33
35
|
const pkgJson = JSON.parse(fs8.readFileSync(packageJsonPath, "utf-8"));
|
|
34
36
|
const packageName = pkgJson.name;
|
|
@@ -42,8 +44,8 @@ function buildDependencyGraph(rootDir) {
|
|
|
42
44
|
if (!entry.isDirectory() || !entry.name.startsWith("kb-labs-")) {
|
|
43
45
|
continue;
|
|
44
46
|
}
|
|
45
|
-
const repoPath =
|
|
46
|
-
const packagesDir =
|
|
47
|
+
const repoPath = path9.join(rootDir, entry.name);
|
|
48
|
+
const packagesDir = path9.join(repoPath, "packages");
|
|
47
49
|
if (!fs8.existsSync(packagesDir)) {
|
|
48
50
|
continue;
|
|
49
51
|
}
|
|
@@ -52,7 +54,7 @@ function buildDependencyGraph(rootDir) {
|
|
|
52
54
|
if (!pkgDir.isDirectory()) {
|
|
53
55
|
continue;
|
|
54
56
|
}
|
|
55
|
-
const packageJsonPath =
|
|
57
|
+
const packageJsonPath = path9.join(packagesDir, pkgDir.name, "package.json");
|
|
56
58
|
if (fs8.existsSync(packageJsonPath)) {
|
|
57
59
|
const pkgJson = JSON.parse(fs8.readFileSync(packageJsonPath, "utf-8"));
|
|
58
60
|
const packageName = pkgJson.name;
|
|
@@ -80,7 +82,7 @@ function buildDependencyGraph(rootDir) {
|
|
|
80
82
|
nodes.set(packageName, {
|
|
81
83
|
name: packageName,
|
|
82
84
|
path: packageJsonPath,
|
|
83
|
-
dir:
|
|
85
|
+
dir: path9.dirname(packageJsonPath),
|
|
84
86
|
deps,
|
|
85
87
|
devDeps,
|
|
86
88
|
dependents: /* @__PURE__ */ new Set()
|
|
@@ -158,10 +160,10 @@ function findCircularDependencies(graph, subset) {
|
|
|
158
160
|
const visited = /* @__PURE__ */ new Set();
|
|
159
161
|
const recStack = /* @__PURE__ */ new Set();
|
|
160
162
|
const cycles = [];
|
|
161
|
-
function dfs(node,
|
|
163
|
+
function dfs(node, path12) {
|
|
162
164
|
visited.add(node);
|
|
163
165
|
recStack.add(node);
|
|
164
|
-
|
|
166
|
+
path12.push(node);
|
|
165
167
|
const nodeData = nodes.get(node);
|
|
166
168
|
if (!nodeData) {
|
|
167
169
|
return;
|
|
@@ -174,11 +176,11 @@ function findCircularDependencies(graph, subset) {
|
|
|
174
176
|
continue;
|
|
175
177
|
}
|
|
176
178
|
if (!visited.has(dep)) {
|
|
177
|
-
dfs(dep, [...
|
|
179
|
+
dfs(dep, [...path12]);
|
|
178
180
|
} else if (recStack.has(dep)) {
|
|
179
|
-
const cycleStart =
|
|
181
|
+
const cycleStart = path12.indexOf(dep);
|
|
180
182
|
if (cycleStart !== -1) {
|
|
181
|
-
const cycle =
|
|
183
|
+
const cycle = path12.slice(cycleStart);
|
|
182
184
|
cycles.push([...cycle, dep]);
|
|
183
185
|
}
|
|
184
186
|
}
|
|
@@ -247,33 +249,54 @@ function getImpactAnalysis(graph, packageName) {
|
|
|
247
249
|
}
|
|
248
250
|
return affected;
|
|
249
251
|
}
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
"**/build/**",
|
|
259
|
-
"**/*.test.{ts,tsx,js,jsx}",
|
|
260
|
-
"**/*.spec.{ts,tsx,js,jsx}"
|
|
261
|
-
],
|
|
262
|
-
absolute: true,
|
|
263
|
-
deep: 8
|
|
264
|
-
});
|
|
265
|
-
const contents = await Promise.all(files.map((f) => readFile(f, "utf-8").catch(() => null)));
|
|
266
|
-
return contents.reduce((sum, c) => sum + (c ? c.split("\n").length : 0), 0);
|
|
252
|
+
var LOC_SAMPLE_SIZE = 200;
|
|
253
|
+
var FALLBACK_BYTES_PER_LINE = 45;
|
|
254
|
+
async function readTextFile(f) {
|
|
255
|
+
try {
|
|
256
|
+
return await readFile(f, "utf8");
|
|
257
|
+
} catch {
|
|
258
|
+
return null;
|
|
259
|
+
}
|
|
267
260
|
}
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
261
|
+
var SOURCE_PATTERNS = ["*.ts", "*.tsx", "*.js", "*.jsx", "*.go"];
|
|
262
|
+
async function getSourceFiles(rootDir) {
|
|
263
|
+
return new Promise((resolve) => {
|
|
264
|
+
execFile(
|
|
265
|
+
"git",
|
|
266
|
+
["ls-files", "-z", "--cached", "--others", "--exclude-standard", "--", ...SOURCE_PATTERNS],
|
|
267
|
+
{ cwd: rootDir, maxBuffer: 64 * 1024 * 1024 },
|
|
268
|
+
(err, stdout) => {
|
|
269
|
+
if (err || !stdout) {
|
|
270
|
+
return resolve([]);
|
|
271
|
+
}
|
|
272
|
+
resolve(
|
|
273
|
+
stdout.split("\0").filter(Boolean).map((f) => path9.join(rootDir, f))
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
);
|
|
274
277
|
});
|
|
275
|
-
|
|
276
|
-
|
|
278
|
+
}
|
|
279
|
+
async function estimateLoc(files, totalSize) {
|
|
280
|
+
if (files.length === 0) {
|
|
281
|
+
return 0;
|
|
282
|
+
}
|
|
283
|
+
const indices = /* @__PURE__ */ new Set();
|
|
284
|
+
while (indices.size < Math.min(LOC_SAMPLE_SIZE, files.length)) {
|
|
285
|
+
indices.add(Math.floor(Math.random() * files.length));
|
|
286
|
+
}
|
|
287
|
+
const sample = Array.from(indices).map((i) => files[i]).filter((f) => f !== void 0);
|
|
288
|
+
const contents = await Promise.all(sample.map(readTextFile));
|
|
289
|
+
let sampleBytes = 0;
|
|
290
|
+
let sampleLines = 0;
|
|
291
|
+
for (const c of contents) {
|
|
292
|
+
if (!c) {
|
|
293
|
+
continue;
|
|
294
|
+
}
|
|
295
|
+
sampleBytes += Buffer.byteLength(c, "utf-8");
|
|
296
|
+
sampleLines += c.split("\n").length;
|
|
297
|
+
}
|
|
298
|
+
const bytesPerLine = sampleLines > 0 ? sampleBytes / sampleLines : FALLBACK_BYTES_PER_LINE;
|
|
299
|
+
return Math.round(totalSize / bytesPerLine);
|
|
277
300
|
}
|
|
278
301
|
function formatBytes(bytes) {
|
|
279
302
|
if (bytes === 0) {
|
|
@@ -286,7 +309,7 @@ function formatBytes(bytes) {
|
|
|
286
309
|
return `${value.toFixed(2)} ${units[i]}`;
|
|
287
310
|
}
|
|
288
311
|
async function countPackages(rootDir) {
|
|
289
|
-
const packageJsonFiles = await
|
|
312
|
+
const packageJsonFiles = await globby2("**/package.json", {
|
|
290
313
|
cwd: rootDir,
|
|
291
314
|
ignore: ["**/node_modules/**", "**/.git/**", "**/.kb/**"],
|
|
292
315
|
absolute: false,
|
|
@@ -295,17 +318,13 @@ async function countPackages(rootDir) {
|
|
|
295
318
|
return packageJsonFiles.filter((p) => p !== "package.json").length;
|
|
296
319
|
}
|
|
297
320
|
async function calculateStats(rootDir) {
|
|
298
|
-
const
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
absolute: true,
|
|
302
|
-
deep: 8
|
|
303
|
-
});
|
|
304
|
-
const [packages, loc, size] = await Promise.all([
|
|
305
|
-
countPackages(rootDir),
|
|
306
|
-
calculateLinesOfCode(rootDir, sourceFiles),
|
|
307
|
-
calculateSize(rootDir, sourceFiles)
|
|
321
|
+
const [files, packages] = await Promise.all([
|
|
322
|
+
getSourceFiles(rootDir),
|
|
323
|
+
countPackages(rootDir)
|
|
308
324
|
]);
|
|
325
|
+
const fileStats = await Promise.all(files.map((f) => stat(f).catch(() => null)));
|
|
326
|
+
const size = fileStats.reduce((sum, s) => sum + (s ? s.size : 0), 0);
|
|
327
|
+
const loc = await estimateLoc(files, size);
|
|
309
328
|
return {
|
|
310
329
|
packages,
|
|
311
330
|
loc,
|
|
@@ -313,129 +332,114 @@ async function calculateStats(rootDir) {
|
|
|
313
332
|
sizeFormatted: formatBytes(size)
|
|
314
333
|
};
|
|
315
334
|
}
|
|
316
|
-
async function
|
|
317
|
-
const
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
});
|
|
322
|
-
const depVersions = /* @__PURE__ */ new Map();
|
|
323
|
-
const contents = await Promise.all(
|
|
324
|
-
packageJsonFiles.map((f) => readFile(f, "utf-8").catch(() => null))
|
|
325
|
-
);
|
|
326
|
-
for (const content of contents) {
|
|
327
|
-
if (!content) {
|
|
328
|
-
continue;
|
|
329
|
-
}
|
|
330
|
-
try {
|
|
331
|
-
const pkg = JSON.parse(content);
|
|
332
|
-
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
333
|
-
for (const [name, version] of Object.entries(allDeps)) {
|
|
334
|
-
if (typeof version !== "string") {
|
|
335
|
-
continue;
|
|
336
|
-
}
|
|
337
|
-
if (!depVersions.has(name)) {
|
|
338
|
-
depVersions.set(name, /* @__PURE__ */ new Set());
|
|
339
|
-
}
|
|
340
|
-
depVersions.get(name).add(version);
|
|
341
|
-
}
|
|
342
|
-
} catch {
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
|
-
const duplicates = Array.from(depVersions.entries()).filter(
|
|
346
|
-
([, versions]) => versions.size > 1
|
|
347
|
-
);
|
|
348
|
-
if (duplicates.length === 0) {
|
|
349
|
-
return null;
|
|
350
|
-
}
|
|
351
|
-
const penalty = Math.min(duplicates.length * 2, 30);
|
|
352
|
-
return {
|
|
353
|
-
type: "duplicate",
|
|
354
|
-
severity: duplicates.length > 20 ? "high" : duplicates.length > 10 ? "medium" : "low",
|
|
355
|
-
message: `Found ${duplicates.length} duplicate dependencies with different versions`,
|
|
356
|
-
count: duplicates.length,
|
|
357
|
-
penalty
|
|
358
|
-
};
|
|
335
|
+
async function calculateLinesOfCode(rootDir) {
|
|
336
|
+
const files = await getSourceFiles(rootDir);
|
|
337
|
+
const fileStats = await Promise.all(files.map((f) => stat(f).catch(() => null)));
|
|
338
|
+
const size = fileStats.reduce((sum, s) => sum + (s ? s.size : 0), 0);
|
|
339
|
+
return estimateLoc(files, size);
|
|
359
340
|
}
|
|
360
|
-
async function
|
|
361
|
-
const
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
absolute: true
|
|
365
|
-
});
|
|
366
|
-
const hasReadme = async (pkgPath) => {
|
|
367
|
-
const dir = join(pkgPath, "..");
|
|
368
|
-
for (const name of ["README.md", "readme.md", "Readme.md"]) {
|
|
369
|
-
try {
|
|
370
|
-
await access(join(dir, name));
|
|
371
|
-
return true;
|
|
372
|
-
} catch {
|
|
373
|
-
}
|
|
374
|
-
}
|
|
375
|
-
return false;
|
|
376
|
-
};
|
|
377
|
-
const results = await Promise.all(pkgFiles.map(hasReadme));
|
|
378
|
-
const missingCount = results.filter((has) => !has).length;
|
|
379
|
-
if (missingCount === 0) {
|
|
380
|
-
return null;
|
|
381
|
-
}
|
|
382
|
-
const penalty = Math.min(missingCount, 15);
|
|
383
|
-
return {
|
|
384
|
-
type: "readme",
|
|
385
|
-
severity: missingCount > 20 ? "high" : missingCount > 10 ? "medium" : "low",
|
|
386
|
-
message: `Found ${missingCount} packages without README`,
|
|
387
|
-
count: missingCount,
|
|
388
|
-
penalty
|
|
389
|
-
};
|
|
390
|
-
}
|
|
391
|
-
function calculateHealthScore(issues) {
|
|
392
|
-
const baseScore = 100;
|
|
393
|
-
const totalPenalty = issues.reduce((sum, issue) => sum + issue.penalty, 0);
|
|
394
|
-
return Math.max(0, baseScore - totalPenalty);
|
|
341
|
+
async function calculateSize(rootDir) {
|
|
342
|
+
const files = await getSourceFiles(rootDir);
|
|
343
|
+
const fileStats = await Promise.all(files.map((f) => stat(f).catch(() => null)));
|
|
344
|
+
return fileStats.reduce((sum, s) => sum + (s ? s.size : 0), 0);
|
|
395
345
|
}
|
|
396
|
-
function
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
return "B";
|
|
402
|
-
}
|
|
403
|
-
if (score >= 70) {
|
|
404
|
-
return "C";
|
|
405
|
-
}
|
|
406
|
-
if (score >= 60) {
|
|
407
|
-
return "D";
|
|
346
|
+
function gradeFromScore(score) {
|
|
347
|
+
for (const [g, { min }] of Object.entries(HEALTH_GRADES)) {
|
|
348
|
+
if (score >= min) {
|
|
349
|
+
return g;
|
|
350
|
+
}
|
|
408
351
|
}
|
|
409
352
|
return "F";
|
|
410
353
|
}
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
const
|
|
430
|
-
const
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
354
|
+
function clamp(n) {
|
|
355
|
+
return Math.max(0, Math.min(100, Math.round(n)));
|
|
356
|
+
}
|
|
357
|
+
function scoreArchitecture(layering, coupling, thresholds) {
|
|
358
|
+
const details = [];
|
|
359
|
+
const layeringPenalty = Math.min(layering.totalViolations * 5, 60);
|
|
360
|
+
if (layering.totalViolations > 0) {
|
|
361
|
+
details.push(`${layering.totalViolations} layering violation(s) in ${layering.affectedPackages.length} package(s)`);
|
|
362
|
+
}
|
|
363
|
+
const highInstability = coupling.packages.filter((p) => p.instability > thresholds.instability);
|
|
364
|
+
const couplingPenalty = Math.min(highInstability.length * 3, 30);
|
|
365
|
+
if (highInstability.length > 0) {
|
|
366
|
+
details.push(`${highInstability.length} package(s) with instability > ${thresholds.instability}`);
|
|
367
|
+
}
|
|
368
|
+
const score = clamp(100 - layeringPenalty - couplingPenalty);
|
|
369
|
+
return { score, grade: gradeFromScore(score), details };
|
|
370
|
+
}
|
|
371
|
+
function scoreTypeScript(types) {
|
|
372
|
+
const details = [];
|
|
373
|
+
const totalAny = types.packages.reduce((s, p) => s + p.anyCount, 0);
|
|
374
|
+
const anyPenalty = Math.min(totalAny * 0.5, 50);
|
|
375
|
+
if (totalAny > 0) {
|
|
376
|
+
details.push(`${totalAny} \`any\` usage(s)`);
|
|
377
|
+
}
|
|
378
|
+
const totalIgnore = types.packages.reduce((s, p) => s + p.tsIgnoreCount, 0);
|
|
379
|
+
const ignorePenalty = Math.min(totalIgnore * 2, 30);
|
|
380
|
+
if (totalIgnore > 0) {
|
|
381
|
+
details.push(`${totalIgnore} @ts-ignore(s)`);
|
|
382
|
+
}
|
|
383
|
+
const errorPenalty = Math.min(types.totalErrors * 3, 20);
|
|
384
|
+
if (types.totalErrors > 0) {
|
|
385
|
+
details.push(`${types.totalErrors} type error(s)`);
|
|
386
|
+
}
|
|
387
|
+
const score = clamp(100 - anyPenalty - ignorePenalty - errorPenalty);
|
|
388
|
+
return { score, grade: gradeFromScore(score), details };
|
|
389
|
+
}
|
|
390
|
+
function scoreDeadCode(knip) {
|
|
391
|
+
const details = [];
|
|
392
|
+
const filePenalty = Math.min(knip.unusedFiles.length * 2, 50);
|
|
393
|
+
if (knip.unusedFiles.length > 0) {
|
|
394
|
+
details.push(`${knip.unusedFiles.length} unused file(s)`);
|
|
395
|
+
}
|
|
396
|
+
const exportPenalty = Math.min(knip.unusedExports.length * 0.5, 30);
|
|
397
|
+
if (knip.unusedExports.length > 0) {
|
|
398
|
+
details.push(`${knip.unusedExports.length} unused export(s)`);
|
|
399
|
+
}
|
|
400
|
+
const score = clamp(100 - filePenalty - exportPenalty);
|
|
401
|
+
return { score, grade: gradeFromScore(score), details };
|
|
402
|
+
}
|
|
403
|
+
function scoreDepHygiene(knip) {
|
|
404
|
+
const details = [];
|
|
405
|
+
const unusedPenalty = Math.min(knip.unusedDependencies.length * 5, 50);
|
|
406
|
+
if (knip.unusedDependencies.length > 0) {
|
|
407
|
+
details.push(`${knip.unusedDependencies.length} unused dep(s)`);
|
|
408
|
+
}
|
|
409
|
+
const unlistedPenalty = Math.min(knip.unlistedDependencies.length * 10, 40);
|
|
410
|
+
if (knip.unlistedDependencies.length > 0) {
|
|
411
|
+
details.push(`${knip.unlistedDependencies.length} unlisted dep(s)`);
|
|
412
|
+
}
|
|
413
|
+
const score = clamp(100 - unusedPenalty - unlistedPenalty);
|
|
414
|
+
return { score, grade: gradeFromScore(score), details };
|
|
415
|
+
}
|
|
416
|
+
function scoreTestCoverage(avgCoverage) {
|
|
417
|
+
if (avgCoverage === null) {
|
|
418
|
+
return { score: 100, grade: "A", details: ["no coverage data \u2014 skipped"] };
|
|
419
|
+
}
|
|
420
|
+
const score = clamp(avgCoverage);
|
|
421
|
+
const details = avgCoverage < 80 ? [`avg coverage ${avgCoverage.toFixed(1)}%`] : [];
|
|
422
|
+
return { score, grade: gradeFromScore(score), details };
|
|
423
|
+
}
|
|
424
|
+
function calculateHealth(input) {
|
|
425
|
+
const { layering, coupling, types, knip, avgTestCoverage, thresholds } = input;
|
|
426
|
+
const dimensions = {
|
|
427
|
+
architecture: scoreArchitecture(layering, coupling, thresholds),
|
|
428
|
+
typescript: scoreTypeScript(types),
|
|
429
|
+
deadCode: scoreDeadCode(knip),
|
|
430
|
+
depHygiene: scoreDepHygiene(knip),
|
|
431
|
+
testCoverage: scoreTestCoverage(avgTestCoverage)
|
|
435
432
|
};
|
|
433
|
+
const score = clamp(
|
|
434
|
+
Object.entries(DIMENSION_WEIGHTS).reduce(
|
|
435
|
+
(sum, [key, weight]) => sum + dimensions[key].score * weight,
|
|
436
|
+
0
|
|
437
|
+
)
|
|
438
|
+
);
|
|
439
|
+
return { score, grade: gradeFromScore(score), dimensions };
|
|
436
440
|
}
|
|
437
441
|
async function analyzeDuplicateDependencies(rootDir) {
|
|
438
|
-
const packageJsonFiles = await
|
|
442
|
+
const packageJsonFiles = await globby2("**/package.json", {
|
|
439
443
|
cwd: rootDir,
|
|
440
444
|
ignore: ["**/node_modules/**", "**/.git/**"],
|
|
441
445
|
absolute: true
|
|
@@ -490,7 +494,7 @@ async function analyzeDuplicateDependencies(rootDir) {
|
|
|
490
494
|
return duplicates;
|
|
491
495
|
}
|
|
492
496
|
async function analyzeUnusedDependencies(rootDir) {
|
|
493
|
-
const packageJsonFiles = await
|
|
497
|
+
const packageJsonFiles = await globby2("**/package.json", {
|
|
494
498
|
cwd: rootDir,
|
|
495
499
|
ignore: ["**/node_modules/**", "**/.git/**"],
|
|
496
500
|
absolute: true
|
|
@@ -506,7 +510,7 @@ async function analyzeUnusedDependencies(rootDir) {
|
|
|
506
510
|
...pkg.dependencies,
|
|
507
511
|
...pkg.devDependencies
|
|
508
512
|
};
|
|
509
|
-
const sourceFiles = await
|
|
513
|
+
const sourceFiles = await globby2("src/**/*.{ts,tsx,js,jsx}", {
|
|
510
514
|
cwd: pkgDir,
|
|
511
515
|
absolute: true,
|
|
512
516
|
ignore: ["**/*.test.*", "**/*.spec.*"]
|
|
@@ -543,7 +547,7 @@ async function analyzeUnusedDependencies(rootDir) {
|
|
|
543
547
|
return unused;
|
|
544
548
|
}
|
|
545
549
|
async function analyzeMissingDependencies(rootDir) {
|
|
546
|
-
const packageJsonFiles = await
|
|
550
|
+
const packageJsonFiles = await globby2("**/package.json", {
|
|
547
551
|
cwd: rootDir,
|
|
548
552
|
ignore: ["**/node_modules/**", "**/.git/**"],
|
|
549
553
|
absolute: true
|
|
@@ -573,7 +577,7 @@ async function analyzeMissingDependencies(rootDir) {
|
|
|
573
577
|
...Object.keys(pkg.dependencies || {}),
|
|
574
578
|
...Object.keys(pkg.devDependencies || {})
|
|
575
579
|
]);
|
|
576
|
-
const sourceFiles = await
|
|
580
|
+
const sourceFiles = await globby2("src/**/*.{ts,tsx,js,jsx}", {
|
|
577
581
|
cwd: pkgDir,
|
|
578
582
|
absolute: true
|
|
579
583
|
});
|
|
@@ -636,8 +640,8 @@ function findPackagesWithBuildScript(rootDir, filter) {
|
|
|
636
640
|
if (!entry.isDirectory() || !entry.name.startsWith("kb-labs-")) {
|
|
637
641
|
continue;
|
|
638
642
|
}
|
|
639
|
-
const repoPath =
|
|
640
|
-
const packagesDir =
|
|
643
|
+
const repoPath = path9.join(rootDir, entry.name);
|
|
644
|
+
const packagesDir = path9.join(repoPath, "packages");
|
|
641
645
|
if (!fs8.existsSync(packagesDir)) {
|
|
642
646
|
continue;
|
|
643
647
|
}
|
|
@@ -646,7 +650,7 @@ function findPackagesWithBuildScript(rootDir, filter) {
|
|
|
646
650
|
if (!pkgDir.isDirectory()) {
|
|
647
651
|
continue;
|
|
648
652
|
}
|
|
649
|
-
const packageJsonPath =
|
|
653
|
+
const packageJsonPath = path9.join(packagesDir, pkgDir.name, "package.json");
|
|
650
654
|
if (fs8.existsSync(packageJsonPath)) {
|
|
651
655
|
const pkgJson = JSON.parse(fs8.readFileSync(packageJsonPath, "utf-8"));
|
|
652
656
|
if (!pkgJson.scripts?.build) {
|
|
@@ -662,8 +666,8 @@ function findPackagesWithBuildScript(rootDir, filter) {
|
|
|
662
666
|
return packages;
|
|
663
667
|
}
|
|
664
668
|
function isDistStale(packageDir) {
|
|
665
|
-
const distFile =
|
|
666
|
-
const srcDir =
|
|
669
|
+
const distFile = path9.join(packageDir, "dist/index.js");
|
|
670
|
+
const srcDir = path9.join(packageDir, "src");
|
|
667
671
|
if (!fs8.existsSync(distFile)) {
|
|
668
672
|
return { stale: false };
|
|
669
673
|
}
|
|
@@ -675,7 +679,7 @@ function isDistStale(packageDir) {
|
|
|
675
679
|
function walkDir(dir) {
|
|
676
680
|
const entries = fs8.readdirSync(dir, { withFileTypes: true });
|
|
677
681
|
for (const entry of entries) {
|
|
678
|
-
const fullPath =
|
|
682
|
+
const fullPath = path9.join(dir, entry.name);
|
|
679
683
|
if (entry.isDirectory()) {
|
|
680
684
|
walkDir(fullPath);
|
|
681
685
|
} else if (/\.(ts|tsx|js|jsx)$/.test(entry.name)) {
|
|
@@ -725,7 +729,7 @@ async function checkBuilds(rootDir, options = {}) {
|
|
|
725
729
|
for (const pkgPath of packagePaths) {
|
|
726
730
|
const pkgJson = JSON.parse(fs8.readFileSync(pkgPath, "utf-8"));
|
|
727
731
|
const packageName = pkgJson.name;
|
|
728
|
-
const packageDir =
|
|
732
|
+
const packageDir = path9.dirname(pkgPath);
|
|
729
733
|
const staleCheck = isDistStale(packageDir);
|
|
730
734
|
if (staleCheck.stale) {
|
|
731
735
|
result.staleBuilds.push({
|
|
@@ -759,8 +763,8 @@ function findPackagesWithTsConfig(rootDir, filter) {
|
|
|
759
763
|
if (!entry.isDirectory() || !entry.name.startsWith("kb-labs-")) {
|
|
760
764
|
continue;
|
|
761
765
|
}
|
|
762
|
-
const repoPath =
|
|
763
|
-
const packagesDir =
|
|
766
|
+
const repoPath = path9.join(rootDir, entry.name);
|
|
767
|
+
const packagesDir = path9.join(repoPath, "packages");
|
|
764
768
|
if (!fs8.existsSync(packagesDir)) {
|
|
765
769
|
continue;
|
|
766
770
|
}
|
|
@@ -769,8 +773,8 @@ function findPackagesWithTsConfig(rootDir, filter) {
|
|
|
769
773
|
if (!pkgDir.isDirectory()) {
|
|
770
774
|
continue;
|
|
771
775
|
}
|
|
772
|
-
const packageJsonPath =
|
|
773
|
-
const tsconfigPath =
|
|
776
|
+
const packageJsonPath = path9.join(packagesDir, pkgDir.name, "package.json");
|
|
777
|
+
const tsconfigPath = path9.join(packagesDir, pkgDir.name, "tsconfig.json");
|
|
774
778
|
if (fs8.existsSync(packageJsonPath) && fs8.existsSync(tsconfigPath)) {
|
|
775
779
|
const pkgJson = JSON.parse(fs8.readFileSync(packageJsonPath, "utf-8"));
|
|
776
780
|
if (filter && !pkgJson.name.includes(filter) && !pkgDir.name.includes(filter)) {
|
|
@@ -778,7 +782,7 @@ function findPackagesWithTsConfig(rootDir, filter) {
|
|
|
778
782
|
}
|
|
779
783
|
packages.push({
|
|
780
784
|
name: pkgJson.name,
|
|
781
|
-
dir:
|
|
785
|
+
dir: path9.dirname(packageJsonPath),
|
|
782
786
|
tsconfigPath
|
|
783
787
|
});
|
|
784
788
|
}
|
|
@@ -804,7 +808,7 @@ function createProgram(packageDir, tsconfigPath) {
|
|
|
804
808
|
rootNames: parsedConfig.fileNames,
|
|
805
809
|
options: parsedConfig.options
|
|
806
810
|
});
|
|
807
|
-
} catch
|
|
811
|
+
} catch {
|
|
808
812
|
return null;
|
|
809
813
|
}
|
|
810
814
|
}
|
|
@@ -910,8 +914,8 @@ function findPackagesWithTests(rootDir, filter) {
|
|
|
910
914
|
if (!entry.isDirectory() || !entry.name.startsWith("kb-labs-")) {
|
|
911
915
|
continue;
|
|
912
916
|
}
|
|
913
|
-
const repoPath =
|
|
914
|
-
const packagesDir =
|
|
917
|
+
const repoPath = path9.join(rootDir, entry.name);
|
|
918
|
+
const packagesDir = path9.join(repoPath, "packages");
|
|
915
919
|
if (!fs8.existsSync(packagesDir)) {
|
|
916
920
|
continue;
|
|
917
921
|
}
|
|
@@ -920,7 +924,7 @@ function findPackagesWithTests(rootDir, filter) {
|
|
|
920
924
|
if (!pkgDir.isDirectory()) {
|
|
921
925
|
continue;
|
|
922
926
|
}
|
|
923
|
-
const packageJsonPath =
|
|
927
|
+
const packageJsonPath = path9.join(packagesDir, pkgDir.name, "package.json");
|
|
924
928
|
if (fs8.existsSync(packageJsonPath)) {
|
|
925
929
|
const pkgJson = JSON.parse(fs8.readFileSync(packageJsonPath, "utf-8"));
|
|
926
930
|
if (filter && !pkgJson.name.includes(filter) && !pkgDir.name.includes(filter)) {
|
|
@@ -928,7 +932,7 @@ function findPackagesWithTests(rootDir, filter) {
|
|
|
928
932
|
}
|
|
929
933
|
packages.push({
|
|
930
934
|
name: pkgJson.name,
|
|
931
|
-
dir:
|
|
935
|
+
dir: path9.dirname(packageJsonPath),
|
|
932
936
|
hasTestScript: !!pkgJson.scripts?.test
|
|
933
937
|
});
|
|
934
938
|
}
|
|
@@ -985,7 +989,7 @@ function parseTestOutput(output) {
|
|
|
985
989
|
return {};
|
|
986
990
|
}
|
|
987
991
|
function readCoverage(packageDir) {
|
|
988
|
-
const coveragePath =
|
|
992
|
+
const coveragePath = path9.join(packageDir, "coverage", "coverage-summary.json");
|
|
989
993
|
if (!fs8.existsSync(coveragePath)) {
|
|
990
994
|
return null;
|
|
991
995
|
}
|
|
@@ -1122,7 +1126,7 @@ async function collectEntryPoints(packageDir, packageJson) {
|
|
|
1122
1126
|
await collectTestFiles(packageDir, aliveByConvention);
|
|
1123
1127
|
collectConfigFiles(packageDir, aliveByConvention);
|
|
1124
1128
|
if (entryFiles.size === 0) {
|
|
1125
|
-
const defaultEntry =
|
|
1129
|
+
const defaultEntry = path9.join(packageDir, "src", "index.ts");
|
|
1126
1130
|
if (fs8.existsSync(defaultEntry)) {
|
|
1127
1131
|
entryFiles.add(defaultEntry);
|
|
1128
1132
|
warnings.push("No entry points found, using src/index.ts as default");
|
|
@@ -1136,11 +1140,11 @@ function distPathToSrcPath(distPath, packageDir) {
|
|
|
1136
1140
|
normalized = "src/" + normalized.slice(5);
|
|
1137
1141
|
}
|
|
1138
1142
|
normalized = normalized.replace(/\.d\.ts$/, ".ts").replace(/\.js$/, ".ts").replace(/\.mjs$/, ".ts").replace(/\.cjs$/, ".ts");
|
|
1139
|
-
const absolute =
|
|
1143
|
+
const absolute = path9.resolve(packageDir, normalized);
|
|
1140
1144
|
if (fs8.existsSync(absolute)) {
|
|
1141
1145
|
return absolute;
|
|
1142
1146
|
}
|
|
1143
|
-
const indexPath =
|
|
1147
|
+
const indexPath = path9.join(absolute.replace(/\.ts$/, ""), "index.ts");
|
|
1144
1148
|
if (fs8.existsSync(indexPath)) {
|
|
1145
1149
|
return indexPath;
|
|
1146
1150
|
}
|
|
@@ -1219,7 +1223,7 @@ async function collectTsupEntries(packageDir, entryFiles, warnings) {
|
|
|
1219
1223
|
];
|
|
1220
1224
|
let foundAny = false;
|
|
1221
1225
|
for (const configName of tsupConfigs) {
|
|
1222
|
-
const configPath =
|
|
1226
|
+
const configPath = path9.join(packageDir, configName);
|
|
1223
1227
|
if (!fs8.existsSync(configPath)) {
|
|
1224
1228
|
continue;
|
|
1225
1229
|
}
|
|
@@ -1229,7 +1233,7 @@ async function collectTsupEntries(packageDir, entryFiles, warnings) {
|
|
|
1229
1233
|
const entries = parseTsupEntries(content);
|
|
1230
1234
|
for (const entry of entries) {
|
|
1231
1235
|
if (entry.includes("*") || entry.includes("{")) {
|
|
1232
|
-
const expanded = await
|
|
1236
|
+
const expanded = await globby2(entry, {
|
|
1233
1237
|
cwd: packageDir,
|
|
1234
1238
|
absolute: true
|
|
1235
1239
|
});
|
|
@@ -1237,7 +1241,7 @@ async function collectTsupEntries(packageDir, entryFiles, warnings) {
|
|
|
1237
1241
|
entryFiles.add(file);
|
|
1238
1242
|
}
|
|
1239
1243
|
} else {
|
|
1240
|
-
const absolute =
|
|
1244
|
+
const absolute = path9.resolve(packageDir, entry);
|
|
1241
1245
|
if (fs8.existsSync(absolute)) {
|
|
1242
1246
|
entryFiles.add(absolute);
|
|
1243
1247
|
}
|
|
@@ -1277,7 +1281,7 @@ function parseTsupEntries(content) {
|
|
|
1277
1281
|
return entries;
|
|
1278
1282
|
}
|
|
1279
1283
|
function collectManifestHandlers(packageDir, entryFiles, warnings) {
|
|
1280
|
-
const manifestPath =
|
|
1284
|
+
const manifestPath = path9.join(packageDir, "src", "manifest.ts");
|
|
1281
1285
|
if (!fs8.existsSync(manifestPath)) {
|
|
1282
1286
|
return;
|
|
1283
1287
|
}
|
|
@@ -1287,7 +1291,7 @@ function collectManifestHandlers(packageDir, entryFiles, warnings) {
|
|
|
1287
1291
|
for (const handlerPath of handlers) {
|
|
1288
1292
|
const pathOnly = handlerPath.split("#")[0] ?? handlerPath;
|
|
1289
1293
|
const srcRelative = "src/" + pathOnly.replace(/^\.\//, "").replace(/\.js$/, ".ts");
|
|
1290
|
-
const absolute =
|
|
1294
|
+
const absolute = path9.resolve(packageDir, srcRelative);
|
|
1291
1295
|
if (fs8.existsSync(absolute)) {
|
|
1292
1296
|
entryFiles.add(absolute);
|
|
1293
1297
|
}
|
|
@@ -1316,11 +1320,11 @@ function parseManifestHandlers(content) {
|
|
|
1316
1320
|
return handlers;
|
|
1317
1321
|
}
|
|
1318
1322
|
async function collectDynamicImportTargets(packageDir, entryFiles, _warnings) {
|
|
1319
|
-
const srcDir =
|
|
1323
|
+
const srcDir = path9.join(packageDir, "src");
|
|
1320
1324
|
if (!fs8.existsSync(srcDir)) {
|
|
1321
1325
|
return;
|
|
1322
1326
|
}
|
|
1323
|
-
const sourceFiles = await
|
|
1327
|
+
const sourceFiles = await globby2("**/*.{ts,tsx}", {
|
|
1324
1328
|
cwd: srcDir,
|
|
1325
1329
|
absolute: true,
|
|
1326
1330
|
ignore: ["**/*.test.ts", "**/*.spec.ts", "**/__tests__/**"]
|
|
@@ -1345,11 +1349,11 @@ async function collectDynamicImportTargets(packageDir, entryFiles, _warnings) {
|
|
|
1345
1349
|
}
|
|
1346
1350
|
}
|
|
1347
1351
|
async function collectTestFiles(packageDir, aliveByConvention) {
|
|
1348
|
-
const srcDir =
|
|
1352
|
+
const srcDir = path9.join(packageDir, "src");
|
|
1349
1353
|
if (!fs8.existsSync(srcDir)) {
|
|
1350
1354
|
return;
|
|
1351
1355
|
}
|
|
1352
|
-
const testFiles = await
|
|
1356
|
+
const testFiles = await globby2(
|
|
1353
1357
|
["**/*.test.ts", "**/*.test.tsx", "**/*.spec.ts", "**/*.spec.tsx", "**/__tests__/**/*.ts"],
|
|
1354
1358
|
{ cwd: srcDir, absolute: true }
|
|
1355
1359
|
);
|
|
@@ -1359,15 +1363,15 @@ async function collectTestFiles(packageDir, aliveByConvention) {
|
|
|
1359
1363
|
}
|
|
1360
1364
|
function collectConfigFiles(packageDir, aliveByConvention) {
|
|
1361
1365
|
for (const pattern of CONFIG_FILE_PATTERNS) {
|
|
1362
|
-
const configPath =
|
|
1366
|
+
const configPath = path9.join(packageDir, pattern);
|
|
1363
1367
|
if (fs8.existsSync(configPath)) {
|
|
1364
1368
|
aliveByConvention.add(configPath);
|
|
1365
1369
|
}
|
|
1366
1370
|
}
|
|
1367
1371
|
}
|
|
1368
1372
|
function resolveFilePath(importPath, fromFile) {
|
|
1369
|
-
const dir =
|
|
1370
|
-
const base =
|
|
1373
|
+
const dir = path9.dirname(fromFile);
|
|
1374
|
+
const base = path9.resolve(dir, importPath);
|
|
1371
1375
|
const tsPath = base.replace(/\.js$/, ".ts");
|
|
1372
1376
|
if (fs8.existsSync(tsPath)) {
|
|
1373
1377
|
return tsPath;
|
|
@@ -1381,8 +1385,8 @@ function resolveFilePath(importPath, fromFile) {
|
|
|
1381
1385
|
if (fs8.existsSync(base + ".tsx")) {
|
|
1382
1386
|
return base + ".tsx";
|
|
1383
1387
|
}
|
|
1384
|
-
if (fs8.existsSync(
|
|
1385
|
-
return
|
|
1388
|
+
if (fs8.existsSync(path9.join(base, "index.ts"))) {
|
|
1389
|
+
return path9.join(base, "index.ts");
|
|
1386
1390
|
}
|
|
1387
1391
|
return null;
|
|
1388
1392
|
}
|
|
@@ -1415,8 +1419,8 @@ function isRelativeImport(specifier) {
|
|
|
1415
1419
|
return specifier.startsWith("./") || specifier.startsWith("../");
|
|
1416
1420
|
}
|
|
1417
1421
|
function resolveRelativeImport(specifier, sourceFile) {
|
|
1418
|
-
const dir =
|
|
1419
|
-
const basePath =
|
|
1422
|
+
const dir = path9.dirname(sourceFile);
|
|
1423
|
+
const basePath = path9.resolve(dir, specifier);
|
|
1420
1424
|
if (specifier.endsWith(".js")) {
|
|
1421
1425
|
const tsPath = basePath.slice(0, -3) + ".ts";
|
|
1422
1426
|
if (fs8.existsSync(tsPath)) {
|
|
@@ -1438,14 +1442,14 @@ function resolveRelativeImport(specifier, sourceFile) {
|
|
|
1438
1442
|
}
|
|
1439
1443
|
if (fs8.existsSync(basePath) && fs8.statSync(basePath).isDirectory()) {
|
|
1440
1444
|
for (const ext of RESOLVE_EXTENSIONS) {
|
|
1441
|
-
const indexPath =
|
|
1445
|
+
const indexPath = path9.join(basePath, "index" + ext);
|
|
1442
1446
|
if (fs8.existsSync(indexPath)) {
|
|
1443
1447
|
return indexPath;
|
|
1444
1448
|
}
|
|
1445
1449
|
}
|
|
1446
1450
|
}
|
|
1447
1451
|
for (const ext of RESOLVE_EXTENSIONS) {
|
|
1448
|
-
const indexPath =
|
|
1452
|
+
const indexPath = path9.join(basePath, "index" + ext);
|
|
1449
1453
|
if (fs8.existsSync(indexPath)) {
|
|
1450
1454
|
return indexPath;
|
|
1451
1455
|
}
|
|
@@ -1530,8 +1534,8 @@ function findPackagesInMonorepo(rootDir, filter) {
|
|
|
1530
1534
|
if (!entry.isDirectory() || !entry.name.startsWith("kb-labs-")) {
|
|
1531
1535
|
continue;
|
|
1532
1536
|
}
|
|
1533
|
-
const repoPath =
|
|
1534
|
-
const packagesDir =
|
|
1537
|
+
const repoPath = path9.join(rootDir, entry.name);
|
|
1538
|
+
const packagesDir = path9.join(repoPath, "packages");
|
|
1535
1539
|
if (!fs8.existsSync(packagesDir)) {
|
|
1536
1540
|
continue;
|
|
1537
1541
|
}
|
|
@@ -1540,15 +1544,15 @@ function findPackagesInMonorepo(rootDir, filter) {
|
|
|
1540
1544
|
if (!pkgDir.isDirectory()) {
|
|
1541
1545
|
continue;
|
|
1542
1546
|
}
|
|
1543
|
-
const packageJsonPath =
|
|
1547
|
+
const packageJsonPath = path9.join(packagesDir, pkgDir.name, "package.json");
|
|
1544
1548
|
if (!fs8.existsSync(packageJsonPath)) {
|
|
1545
1549
|
continue;
|
|
1546
1550
|
}
|
|
1547
1551
|
try {
|
|
1548
1552
|
const pkgJson = JSON.parse(fs8.readFileSync(packageJsonPath, "utf-8"));
|
|
1549
1553
|
const pkgName = pkgJson.name || pkgDir.name;
|
|
1550
|
-
const packageDir =
|
|
1551
|
-
const srcDir =
|
|
1554
|
+
const packageDir = path9.join(packagesDir, pkgDir.name);
|
|
1555
|
+
const srcDir = path9.join(packageDir, "src");
|
|
1552
1556
|
if (!fs8.existsSync(srcDir)) {
|
|
1553
1557
|
continue;
|
|
1554
1558
|
}
|
|
@@ -1564,8 +1568,8 @@ function findPackagesInMonorepo(rootDir, filter) {
|
|
|
1564
1568
|
}
|
|
1565
1569
|
async function analyzePackage(pkg) {
|
|
1566
1570
|
const { packageDir, packageJson, packageName } = pkg;
|
|
1567
|
-
const srcDir =
|
|
1568
|
-
const allSourceFiles = await
|
|
1571
|
+
const srcDir = path9.join(packageDir, "src");
|
|
1572
|
+
const allSourceFiles = await globby2("**/*.{ts,tsx}", {
|
|
1569
1573
|
cwd: srcDir,
|
|
1570
1574
|
absolute: true,
|
|
1571
1575
|
ignore: ["**/*.d.ts"]
|
|
@@ -1586,7 +1590,7 @@ async function analyzePackage(pkg) {
|
|
|
1586
1590
|
totalFiles: allSourceFiles.length,
|
|
1587
1591
|
aliveFiles: allSourceFiles.length,
|
|
1588
1592
|
deadFiles: [],
|
|
1589
|
-
entryPoints: [...entryFiles].map((f) =>
|
|
1593
|
+
entryPoints: [...entryFiles].map((f) => path9.relative(packageDir, f)),
|
|
1590
1594
|
graphEdgeCount: 0,
|
|
1591
1595
|
warnings: [...warnings, "FAIL-OPEN: All files treated as alive due to config parse errors"]
|
|
1592
1596
|
};
|
|
@@ -1602,7 +1606,7 @@ async function analyzePackage(pkg) {
|
|
|
1602
1606
|
const stats = safeFileStat(file);
|
|
1603
1607
|
deadFiles.push({
|
|
1604
1608
|
absolutePath: file,
|
|
1605
|
-
relativePath:
|
|
1609
|
+
relativePath: path9.relative(packageDir, file),
|
|
1606
1610
|
packageName,
|
|
1607
1611
|
packageDir,
|
|
1608
1612
|
sizeBytes: stats?.size ?? 0
|
|
@@ -1616,7 +1620,7 @@ async function analyzePackage(pkg) {
|
|
|
1616
1620
|
totalFiles: allSourceFiles.length,
|
|
1617
1621
|
aliveFiles: allSourceFiles.length - deadFiles.length,
|
|
1618
1622
|
deadFiles,
|
|
1619
|
-
entryPoints: [...entryFiles].map((f) =>
|
|
1623
|
+
entryPoints: [...entryFiles].map((f) => path9.relative(packageDir, f)),
|
|
1620
1624
|
graphEdgeCount,
|
|
1621
1625
|
warnings
|
|
1622
1626
|
};
|
|
@@ -1650,14 +1654,14 @@ function findPotentialEmptyDirs(results, _rootDir) {
|
|
|
1650
1654
|
}
|
|
1651
1655
|
const deadByDir = /* @__PURE__ */ new Map();
|
|
1652
1656
|
for (const deadFile of pkg.deadFiles) {
|
|
1653
|
-
const dir =
|
|
1657
|
+
const dir = path9.dirname(deadFile.absolutePath);
|
|
1654
1658
|
deadByDir.set(dir, (deadByDir.get(dir) ?? 0) + 1);
|
|
1655
1659
|
}
|
|
1656
1660
|
for (const [dir, deadCount] of deadByDir) {
|
|
1657
1661
|
try {
|
|
1658
1662
|
const allFiles = fs8.readdirSync(dir);
|
|
1659
1663
|
if (allFiles.length === deadCount) {
|
|
1660
|
-
emptyDirs.push(
|
|
1664
|
+
emptyDirs.push(path9.relative(pkg.packageDir, dir));
|
|
1661
1665
|
}
|
|
1662
1666
|
} catch {
|
|
1663
1667
|
}
|
|
@@ -1672,11 +1676,65 @@ function safeFileStat(filePath) {
|
|
|
1672
1676
|
return null;
|
|
1673
1677
|
}
|
|
1674
1678
|
}
|
|
1679
|
+
|
|
1680
|
+
// src/dead-code/knip-runner.ts
|
|
1681
|
+
async function runKnip(opts) {
|
|
1682
|
+
const { shell, rootDir } = opts;
|
|
1683
|
+
const result = await shell.exec("pnpm", ["knip", "--reporter", "json"], { cwd: rootDir }).catch(() => null);
|
|
1684
|
+
if (!result) {
|
|
1685
|
+
return emptyReport();
|
|
1686
|
+
}
|
|
1687
|
+
const raw = result.stdout.trim();
|
|
1688
|
+
if (!raw || raw === "{}" || raw === "[]") {
|
|
1689
|
+
return emptyReport();
|
|
1690
|
+
}
|
|
1691
|
+
let parsed;
|
|
1692
|
+
try {
|
|
1693
|
+
parsed = JSON.parse(raw);
|
|
1694
|
+
} catch {
|
|
1695
|
+
return emptyReport();
|
|
1696
|
+
}
|
|
1697
|
+
return parseKnipOutput(parsed, rootDir);
|
|
1698
|
+
}
|
|
1699
|
+
function parseKnipOutput(out, rootDir) {
|
|
1700
|
+
const unusedFiles = (out.files ?? []).map(
|
|
1701
|
+
(f) => f.startsWith("/") ? f : `${rootDir}/${f}`
|
|
1702
|
+
);
|
|
1703
|
+
const unusedExports = (out.exports ?? []).flatMap(
|
|
1704
|
+
(entry) => (entry.exports ?? []).map((exp) => ({
|
|
1705
|
+
file: entry.file,
|
|
1706
|
+
symbol: exp.symbol
|
|
1707
|
+
}))
|
|
1708
|
+
);
|
|
1709
|
+
const unusedDependencies = (out.dependencies ?? []).flatMap(
|
|
1710
|
+
(dep) => (dep.packages ?? [rootDir]).map((workspace) => ({
|
|
1711
|
+
package: dep.name,
|
|
1712
|
+
workspace
|
|
1713
|
+
}))
|
|
1714
|
+
);
|
|
1715
|
+
const unlistedDependencies = (out.unlisted ?? []).flatMap(
|
|
1716
|
+
(dep) => (dep.packages ?? [rootDir]).map((workspace) => ({
|
|
1717
|
+
package: dep.name,
|
|
1718
|
+
workspace
|
|
1719
|
+
}))
|
|
1720
|
+
);
|
|
1721
|
+
const totalIssues = unusedFiles.length + unusedExports.length + unusedDependencies.length + unlistedDependencies.length;
|
|
1722
|
+
return { unusedFiles, unusedExports, unusedDependencies, unlistedDependencies, totalIssues };
|
|
1723
|
+
}
|
|
1724
|
+
function emptyReport() {
|
|
1725
|
+
return {
|
|
1726
|
+
unusedFiles: [],
|
|
1727
|
+
unusedExports: [],
|
|
1728
|
+
unusedDependencies: [],
|
|
1729
|
+
unlistedDependencies: [],
|
|
1730
|
+
totalIssues: 0
|
|
1731
|
+
};
|
|
1732
|
+
}
|
|
1675
1733
|
var BACKUP_DIR = ".dead-code-backup";
|
|
1676
1734
|
async function removeDeadFiles(rootDir, scanResult, options) {
|
|
1677
1735
|
const dryRun = options?.dryRun ?? false;
|
|
1678
1736
|
const backupId = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
1679
|
-
const backupPath =
|
|
1737
|
+
const backupPath = path9.join(rootDir, BACKUP_DIR, backupId);
|
|
1680
1738
|
const gitSha = safeExec("git rev-parse HEAD", rootDir) ?? "unknown";
|
|
1681
1739
|
const gitBranch = safeExec("git rev-parse --abbrev-ref HEAD", rootDir) ?? "unknown";
|
|
1682
1740
|
const allDeadFiles = [];
|
|
@@ -1711,7 +1769,7 @@ async function removeDeadFiles(rootDir, scanResult, options) {
|
|
|
1711
1769
|
gitBranch,
|
|
1712
1770
|
removedFiles: allDeadFiles.map((f) => ({
|
|
1713
1771
|
originalPath: f.absolutePath,
|
|
1714
|
-
backupPath:
|
|
1772
|
+
backupPath: path9.relative(rootDir, f.absolutePath),
|
|
1715
1773
|
packageName: f.packageName,
|
|
1716
1774
|
sizeBytes: f.sizeBytes
|
|
1717
1775
|
})),
|
|
@@ -1731,45 +1789,45 @@ async function removeDeadFiles(rootDir, scanResult, options) {
|
|
|
1731
1789
|
manifest
|
|
1732
1790
|
};
|
|
1733
1791
|
}
|
|
1734
|
-
const filesDir =
|
|
1792
|
+
const filesDir = path9.join(backupPath, "files");
|
|
1735
1793
|
for (const deadFile of allDeadFiles) {
|
|
1736
|
-
const relPath =
|
|
1737
|
-
const destPath =
|
|
1738
|
-
const destDir =
|
|
1794
|
+
const relPath = path9.relative(rootDir, deadFile.absolutePath);
|
|
1795
|
+
const destPath = path9.join(filesDir, relPath);
|
|
1796
|
+
const destDir = path9.dirname(destPath);
|
|
1739
1797
|
fs8.mkdirSync(destDir, { recursive: true });
|
|
1740
1798
|
fs8.copyFileSync(deadFile.absolutePath, destPath);
|
|
1741
1799
|
}
|
|
1742
1800
|
fs8.writeFileSync(
|
|
1743
|
-
|
|
1801
|
+
path9.join(backupPath, "manifest.json"),
|
|
1744
1802
|
JSON.stringify(manifest, null, 2) + "\n"
|
|
1745
1803
|
);
|
|
1746
1804
|
for (const deadFile of allDeadFiles) {
|
|
1747
1805
|
fs8.unlinkSync(deadFile.absolutePath);
|
|
1748
1806
|
}
|
|
1749
1807
|
const removedDirs = [];
|
|
1750
|
-
const deadFileDirs = new Set(allDeadFiles.map((f) =>
|
|
1808
|
+
const deadFileDirs = new Set(allDeadFiles.map((f) => path9.dirname(f.absolutePath)));
|
|
1751
1809
|
for (const dir of deadFileDirs) {
|
|
1752
1810
|
removeEmptyDirsUpward(dir, rootDir, removedDirs);
|
|
1753
1811
|
}
|
|
1754
|
-
manifest.removedEmptyDirs = removedDirs.map((d) =>
|
|
1812
|
+
manifest.removedEmptyDirs = removedDirs.map((d) => path9.relative(rootDir, d));
|
|
1755
1813
|
const deletedPaths = new Set(allDeadFiles.map((f) => f.absolutePath));
|
|
1756
1814
|
let exportsCleanedUp = 0;
|
|
1757
1815
|
for (const pkg of scanResult.packages) {
|
|
1758
1816
|
if (pkg.deadFiles.length === 0) {
|
|
1759
1817
|
continue;
|
|
1760
1818
|
}
|
|
1761
|
-
const pkgJsonPath =
|
|
1819
|
+
const pkgJsonPath = path9.join(pkg.packageDir, "package.json");
|
|
1762
1820
|
const cleaned = cleanPackageJsonExports(pkgJsonPath, deletedPaths, pkg.packageDir);
|
|
1763
1821
|
if (cleaned.length > 0) {
|
|
1764
1822
|
manifest.cleanedExports.push({
|
|
1765
|
-
packageJsonPath:
|
|
1823
|
+
packageJsonPath: path9.relative(rootDir, pkgJsonPath),
|
|
1766
1824
|
removedExportKeys: cleaned
|
|
1767
1825
|
});
|
|
1768
1826
|
exportsCleanedUp += cleaned.length;
|
|
1769
1827
|
}
|
|
1770
1828
|
}
|
|
1771
1829
|
fs8.writeFileSync(
|
|
1772
|
-
|
|
1830
|
+
path9.join(backupPath, "manifest.json"),
|
|
1773
1831
|
JSON.stringify(manifest, null, 2) + "\n"
|
|
1774
1832
|
);
|
|
1775
1833
|
return {
|
|
@@ -1783,8 +1841,8 @@ async function removeDeadFiles(rootDir, scanResult, options) {
|
|
|
1783
1841
|
};
|
|
1784
1842
|
}
|
|
1785
1843
|
async function restoreFromBackup(rootDir, backupId) {
|
|
1786
|
-
const backupPath =
|
|
1787
|
-
const manifestPath =
|
|
1844
|
+
const backupPath = path9.join(rootDir, BACKUP_DIR, backupId);
|
|
1845
|
+
const manifestPath = path9.join(backupPath, "manifest.json");
|
|
1788
1846
|
if (!fs8.existsSync(manifestPath)) {
|
|
1789
1847
|
throw new Error(`Backup not found: ${backupId}`);
|
|
1790
1848
|
}
|
|
@@ -1793,18 +1851,18 @@ async function restoreFromBackup(rootDir, backupId) {
|
|
|
1793
1851
|
);
|
|
1794
1852
|
let restoredFiles = 0;
|
|
1795
1853
|
for (const entry of manifest.removedFiles) {
|
|
1796
|
-
const backupFilePath =
|
|
1854
|
+
const backupFilePath = path9.join(backupPath, "files", entry.backupPath);
|
|
1797
1855
|
if (!fs8.existsSync(backupFilePath)) {
|
|
1798
1856
|
continue;
|
|
1799
1857
|
}
|
|
1800
|
-
const parentDir =
|
|
1858
|
+
const parentDir = path9.dirname(entry.originalPath);
|
|
1801
1859
|
fs8.mkdirSync(parentDir, { recursive: true });
|
|
1802
1860
|
fs8.copyFileSync(backupFilePath, entry.originalPath);
|
|
1803
1861
|
restoredFiles++;
|
|
1804
1862
|
}
|
|
1805
1863
|
let restoredExports = 0;
|
|
1806
1864
|
for (const exportEntry of manifest.cleanedExports) {
|
|
1807
|
-
const pkgJsonPath =
|
|
1865
|
+
const pkgJsonPath = path9.resolve(rootDir, exportEntry.packageJsonPath);
|
|
1808
1866
|
if (!fs8.existsSync(pkgJsonPath)) {
|
|
1809
1867
|
continue;
|
|
1810
1868
|
}
|
|
@@ -1813,7 +1871,7 @@ async function restoreFromBackup(rootDir, backupId) {
|
|
|
1813
1871
|
return { restoredFiles, restoredExports };
|
|
1814
1872
|
}
|
|
1815
1873
|
function listBackups(rootDir) {
|
|
1816
|
-
const backupDir =
|
|
1874
|
+
const backupDir = path9.join(rootDir, BACKUP_DIR);
|
|
1817
1875
|
if (!fs8.existsSync(backupDir)) {
|
|
1818
1876
|
return [];
|
|
1819
1877
|
}
|
|
@@ -1823,7 +1881,7 @@ function listBackups(rootDir) {
|
|
|
1823
1881
|
if (!entry.isDirectory()) {
|
|
1824
1882
|
continue;
|
|
1825
1883
|
}
|
|
1826
|
-
const manifestPath =
|
|
1884
|
+
const manifestPath = path9.join(backupDir, entry.name, "manifest.json");
|
|
1827
1885
|
if (!fs8.existsSync(manifestPath)) {
|
|
1828
1886
|
continue;
|
|
1829
1887
|
}
|
|
@@ -1855,7 +1913,7 @@ function removeEmptyDirsUpward(dir, rootBoundary, removed) {
|
|
|
1855
1913
|
}
|
|
1856
1914
|
fs8.rmdirSync(current);
|
|
1857
1915
|
removed.push(current);
|
|
1858
|
-
current =
|
|
1916
|
+
current = path9.dirname(current);
|
|
1859
1917
|
} catch {
|
|
1860
1918
|
break;
|
|
1861
1919
|
}
|
|
@@ -1915,9 +1973,324 @@ function distToSrcForExportCheck(distPath, packageDir) {
|
|
|
1915
1973
|
}
|
|
1916
1974
|
normalized = "src/" + normalized.slice(5);
|
|
1917
1975
|
normalized = normalized.replace(/\.d\.ts$/, ".ts").replace(/\.js$/, ".ts").replace(/\.mjs$/, ".ts").replace(/\.cjs$/, ".ts");
|
|
1918
|
-
return
|
|
1976
|
+
return path9.resolve(packageDir, normalized);
|
|
1977
|
+
}
|
|
1978
|
+
function resolveLayer(pkgDir, rootDir, layerMap = defaultQualityConfig.layers) {
|
|
1979
|
+
const rel = path9.relative(rootDir, pkgDir).replace(/\\/g, "/");
|
|
1980
|
+
for (const [prefix, layer] of Object.entries(layerMap)) {
|
|
1981
|
+
if (rel.startsWith(prefix)) {
|
|
1982
|
+
return layer;
|
|
1983
|
+
}
|
|
1984
|
+
}
|
|
1985
|
+
return -1;
|
|
1986
|
+
}
|
|
1987
|
+
function scanWorkspace(rootDir, layerMap = defaultQualityConfig.layers) {
|
|
1988
|
+
const packages = [];
|
|
1989
|
+
if (!fs8.existsSync(rootDir)) {
|
|
1990
|
+
return packages;
|
|
1991
|
+
}
|
|
1992
|
+
const topDirs = ["core", "sdk", "shared", "cli", "adapters", "studio", "infra"];
|
|
1993
|
+
const pluginDirs = ["plugins"];
|
|
1994
|
+
const collect = (dir, maxDepth) => {
|
|
1995
|
+
if (!fs8.existsSync(dir)) {
|
|
1996
|
+
return;
|
|
1997
|
+
}
|
|
1998
|
+
const pkgJson = path9.join(dir, "package.json");
|
|
1999
|
+
if (fs8.existsSync(pkgJson)) {
|
|
2000
|
+
tryAddPackage(pkgJson);
|
|
2001
|
+
return;
|
|
2002
|
+
}
|
|
2003
|
+
if (maxDepth <= 0) {
|
|
2004
|
+
return;
|
|
2005
|
+
}
|
|
2006
|
+
for (const entry of fs8.readdirSync(dir, { withFileTypes: true })) {
|
|
2007
|
+
if (!entry.isDirectory() || entry.name === "node_modules" || entry.name.startsWith(".")) {
|
|
2008
|
+
continue;
|
|
2009
|
+
}
|
|
2010
|
+
collect(path9.join(dir, entry.name), maxDepth - 1);
|
|
2011
|
+
}
|
|
2012
|
+
};
|
|
2013
|
+
const tryAddPackage = (pkgJsonPath) => {
|
|
2014
|
+
try {
|
|
2015
|
+
const raw = JSON.parse(fs8.readFileSync(pkgJsonPath, "utf-8"));
|
|
2016
|
+
if (!raw.name) {
|
|
2017
|
+
return;
|
|
2018
|
+
}
|
|
2019
|
+
const dir = path9.dirname(pkgJsonPath);
|
|
2020
|
+
packages.push({
|
|
2021
|
+
name: raw.name,
|
|
2022
|
+
dir,
|
|
2023
|
+
packageJsonPath: pkgJsonPath,
|
|
2024
|
+
layer: resolveLayer(dir, rootDir, layerMap),
|
|
2025
|
+
deps: Object.keys(raw.dependencies ?? {}),
|
|
2026
|
+
devDeps: Object.keys(raw.devDependencies ?? {})
|
|
2027
|
+
});
|
|
2028
|
+
} catch {
|
|
2029
|
+
}
|
|
2030
|
+
};
|
|
2031
|
+
for (const top of topDirs) {
|
|
2032
|
+
collect(path9.join(rootDir, top), 1);
|
|
2033
|
+
}
|
|
2034
|
+
for (const top of pluginDirs) {
|
|
2035
|
+
collect(path9.join(rootDir, top), 2);
|
|
2036
|
+
}
|
|
2037
|
+
return packages;
|
|
2038
|
+
}
|
|
2039
|
+
function buildPackageMap(packages) {
|
|
2040
|
+
return new Map(packages.map((p) => [p.name, p]));
|
|
2041
|
+
}
|
|
2042
|
+
function extractImportSpecifiers(filePath) {
|
|
2043
|
+
let source;
|
|
2044
|
+
try {
|
|
2045
|
+
source = fs8.readFileSync(filePath, "utf-8");
|
|
2046
|
+
} catch {
|
|
2047
|
+
return [];
|
|
2048
|
+
}
|
|
2049
|
+
const sf = ts.createSourceFile(filePath, source, ts.ScriptTarget.Latest, true);
|
|
2050
|
+
const specifiers = [];
|
|
2051
|
+
const visit = (node) => {
|
|
2052
|
+
if ((ts.isImportDeclaration(node) || ts.isExportDeclaration(node)) && node.moduleSpecifier && ts.isStringLiteral(node.moduleSpecifier)) {
|
|
2053
|
+
specifiers.push(node.moduleSpecifier.text);
|
|
2054
|
+
}
|
|
2055
|
+
ts.forEachChild(node, visit);
|
|
2056
|
+
};
|
|
2057
|
+
visit(sf);
|
|
2058
|
+
return specifiers;
|
|
2059
|
+
}
|
|
2060
|
+
function resolveToPackageName(specifier, workspaceNames) {
|
|
2061
|
+
if (specifier.startsWith(".") || specifier.startsWith("node:")) {
|
|
2062
|
+
return null;
|
|
2063
|
+
}
|
|
2064
|
+
const scoped = specifier.match(/^(@[^/]+\/[^/]+)/);
|
|
2065
|
+
if (scoped) {
|
|
2066
|
+
const pkg = scoped[1];
|
|
2067
|
+
return workspaceNames.has(pkg) ? pkg : null;
|
|
2068
|
+
}
|
|
2069
|
+
const name = specifier.split("/")[0];
|
|
2070
|
+
if (name) {
|
|
2071
|
+
return workspaceNames.has(name) ? name : null;
|
|
2072
|
+
}
|
|
2073
|
+
return null;
|
|
1919
2074
|
}
|
|
2075
|
+
async function analyzeLayering(opts) {
|
|
2076
|
+
const { rootDir, layerMap } = opts;
|
|
2077
|
+
const packages = scanWorkspace(rootDir, layerMap);
|
|
2078
|
+
const packageMap = buildPackageMap(packages);
|
|
2079
|
+
const workspaceNames = new Set(packageMap.keys());
|
|
2080
|
+
const violations = [];
|
|
2081
|
+
await Promise.all(
|
|
2082
|
+
packages.filter((pkg) => pkg.layer >= 0).map(async (pkg) => {
|
|
2083
|
+
const srcDir = path9.join(pkg.dir, "src");
|
|
2084
|
+
if (!fs8.existsSync(srcDir)) {
|
|
2085
|
+
return;
|
|
2086
|
+
}
|
|
2087
|
+
const files = await globby2("**/*.{ts,tsx}", {
|
|
2088
|
+
cwd: srcDir,
|
|
2089
|
+
ignore: ["**/*.test.ts", "**/*.spec.ts", "**/__tests__/**", "**/node_modules/**"],
|
|
2090
|
+
absolute: true,
|
|
2091
|
+
deep: 10
|
|
2092
|
+
});
|
|
2093
|
+
for (const file of files) {
|
|
2094
|
+
const specifiers = extractImportSpecifiers(file);
|
|
2095
|
+
for (const spec of specifiers) {
|
|
2096
|
+
const importedPkg = resolveToPackageName(spec, workspaceNames);
|
|
2097
|
+
if (!importedPkg) {
|
|
2098
|
+
continue;
|
|
2099
|
+
}
|
|
2100
|
+
const importedLayer = packageMap.get(importedPkg)?.layer ?? resolveLayer(importedPkg, rootDir, layerMap);
|
|
2101
|
+
if (importedLayer < 0) {
|
|
2102
|
+
continue;
|
|
2103
|
+
}
|
|
2104
|
+
if (importedLayer > pkg.layer) {
|
|
2105
|
+
violations.push({
|
|
2106
|
+
file,
|
|
2107
|
+
fromPackage: pkg.name,
|
|
2108
|
+
fromLayer: pkg.layer,
|
|
2109
|
+
toPackage: importedPkg,
|
|
2110
|
+
toLayer: importedLayer,
|
|
2111
|
+
importSpecifier: spec
|
|
2112
|
+
});
|
|
2113
|
+
}
|
|
2114
|
+
}
|
|
2115
|
+
}
|
|
2116
|
+
})
|
|
2117
|
+
);
|
|
2118
|
+
const affectedPackages = [...new Set(violations.map((v) => v.fromPackage))];
|
|
2119
|
+
return {
|
|
2120
|
+
violations,
|
|
2121
|
+
totalViolations: violations.length,
|
|
2122
|
+
affectedPackages,
|
|
2123
|
+
layerMap: layerMap ?? defaultQualityConfig.layers
|
|
2124
|
+
};
|
|
2125
|
+
}
|
|
2126
|
+
|
|
2127
|
+
// src/architecture/coupling.ts
|
|
2128
|
+
function analyzeCoupling(opts) {
|
|
2129
|
+
const { rootDir, layerMap, topN = 5 } = opts;
|
|
2130
|
+
const packages = scanWorkspace(rootDir, layerMap);
|
|
2131
|
+
const allNames = new Set(packages.map((p) => p.name));
|
|
2132
|
+
const afferentCount = new Map(packages.map((p) => [p.name, 0]));
|
|
2133
|
+
for (const pkg of packages) {
|
|
2134
|
+
const workspaceDeps = [...pkg.deps, ...pkg.devDeps].filter((d) => allNames.has(d));
|
|
2135
|
+
for (const dep of workspaceDeps) {
|
|
2136
|
+
afferentCount.set(dep, (afferentCount.get(dep) ?? 0) + 1);
|
|
2137
|
+
}
|
|
2138
|
+
}
|
|
2139
|
+
const result = packages.map((pkg) => {
|
|
2140
|
+
const ce = [...pkg.deps, ...pkg.devDeps].filter((d) => allNames.has(d)).length;
|
|
2141
|
+
const ca = afferentCount.get(pkg.name) ?? 0;
|
|
2142
|
+
const total = ca + ce;
|
|
2143
|
+
const instability = total === 0 ? 0 : ce / total;
|
|
2144
|
+
return {
|
|
2145
|
+
name: pkg.name,
|
|
2146
|
+
afferent: ca,
|
|
2147
|
+
efferent: ce,
|
|
2148
|
+
instability: Math.round(instability * 100) / 100
|
|
2149
|
+
};
|
|
2150
|
+
});
|
|
2151
|
+
const avgInstability = result.length === 0 ? 0 : Math.round(result.reduce((s, p) => s + p.instability, 0) / result.length * 100) / 100;
|
|
2152
|
+
const sorted = [...result].sort((a, b) => b.instability - a.instability);
|
|
2153
|
+
const mostUnstable = sorted.slice(0, topN).map((p) => ({ ...p }));
|
|
2154
|
+
const mostCoupled = [...result].sort((a, b) => b.afferent + b.efferent - (a.afferent + a.efferent)).slice(0, topN).map((p) => ({ ...p }));
|
|
2155
|
+
return { packages: result, avgInstability, mostUnstable, mostCoupled };
|
|
2156
|
+
}
|
|
2157
|
+
|
|
2158
|
+
// src/architecture/build-order.ts
|
|
2159
|
+
function analyzeBuildOrder(opts) {
|
|
2160
|
+
const { rootDir, layerMap, filterPackage } = opts;
|
|
2161
|
+
const packages = scanWorkspace(rootDir, layerMap);
|
|
2162
|
+
const allNames = new Set(packages.map((p) => p.name));
|
|
2163
|
+
const deps = /* @__PURE__ */ new Map();
|
|
2164
|
+
for (const pkg of packages) {
|
|
2165
|
+
const wsDeps = new Set(pkg.deps.filter((d) => allNames.has(d)));
|
|
2166
|
+
deps.set(pkg.name, wsDeps);
|
|
2167
|
+
}
|
|
2168
|
+
let names;
|
|
2169
|
+
if (filterPackage && allNames.has(filterPackage)) {
|
|
2170
|
+
const visited = /* @__PURE__ */ new Set();
|
|
2171
|
+
const visit = (n) => {
|
|
2172
|
+
if (visited.has(n)) {
|
|
2173
|
+
return;
|
|
2174
|
+
}
|
|
2175
|
+
visited.add(n);
|
|
2176
|
+
for (const d of deps.get(n) ?? []) {
|
|
2177
|
+
visit(d);
|
|
2178
|
+
}
|
|
2179
|
+
};
|
|
2180
|
+
visit(filterPackage);
|
|
2181
|
+
names = [...visited];
|
|
2182
|
+
} else {
|
|
2183
|
+
names = packages.map((p) => p.name);
|
|
2184
|
+
}
|
|
2185
|
+
const nameSet = new Set(names);
|
|
2186
|
+
const inDegree = /* @__PURE__ */ new Map();
|
|
2187
|
+
for (const n of names) {
|
|
2188
|
+
const count = [...deps.get(n) ?? []].filter((d) => nameSet.has(d)).length;
|
|
2189
|
+
inDegree.set(n, count);
|
|
2190
|
+
}
|
|
2191
|
+
const layers = [];
|
|
2192
|
+
const sorted = [];
|
|
2193
|
+
const enqueued = /* @__PURE__ */ new Set();
|
|
2194
|
+
let queue = names.filter((n) => (inDegree.get(n) ?? 0) === 0);
|
|
2195
|
+
for (const n of queue) {
|
|
2196
|
+
enqueued.add(n);
|
|
2197
|
+
}
|
|
2198
|
+
while (queue.length > 0) {
|
|
2199
|
+
layers.push([...queue]);
|
|
2200
|
+
sorted.push(...queue);
|
|
2201
|
+
const next = [];
|
|
2202
|
+
for (const d of queue) {
|
|
2203
|
+
for (const n of names) {
|
|
2204
|
+
if (enqueued.has(n)) {
|
|
2205
|
+
continue;
|
|
2206
|
+
}
|
|
2207
|
+
if ((deps.get(n) ?? /* @__PURE__ */ new Set()).has(d)) {
|
|
2208
|
+
const deg = (inDegree.get(n) ?? 1) - 1;
|
|
2209
|
+
inDegree.set(n, deg);
|
|
2210
|
+
if (deg === 0) {
|
|
2211
|
+
next.push(n);
|
|
2212
|
+
enqueued.add(n);
|
|
2213
|
+
}
|
|
2214
|
+
}
|
|
2215
|
+
}
|
|
2216
|
+
}
|
|
2217
|
+
queue = next;
|
|
2218
|
+
}
|
|
2219
|
+
const circular = names.filter((n) => !sorted.includes(n));
|
|
2220
|
+
return {
|
|
2221
|
+
layers,
|
|
2222
|
+
sorted,
|
|
2223
|
+
circular: circular.length > 0 ? [circular] : [],
|
|
2224
|
+
packageCount: sorted.length,
|
|
2225
|
+
layerCount: layers.length,
|
|
2226
|
+
hasCircular: circular.length > 0
|
|
2227
|
+
};
|
|
2228
|
+
}
|
|
2229
|
+
function readSnapshots(filePath) {
|
|
2230
|
+
if (!existsSync(filePath)) {
|
|
2231
|
+
return [];
|
|
2232
|
+
}
|
|
2233
|
+
try {
|
|
2234
|
+
return JSON.parse(readFileSync(filePath, "utf-8"));
|
|
2235
|
+
} catch {
|
|
2236
|
+
return [];
|
|
2237
|
+
}
|
|
2238
|
+
}
|
|
2239
|
+
function writeSnapshots(filePath, data) {
|
|
2240
|
+
const dir = dirname(filePath);
|
|
2241
|
+
if (!existsSync(dir)) {
|
|
2242
|
+
mkdirSync(dir, { recursive: true });
|
|
2243
|
+
}
|
|
2244
|
+
writeFileSync(filePath, JSON.stringify(data, null, 2));
|
|
2245
|
+
}
|
|
2246
|
+
function computeDelta(current, previous) {
|
|
2247
|
+
return {
|
|
2248
|
+
score: current.score - previous.score,
|
|
2249
|
+
layeringViolations: current.counters.layeringViolations - previous.counters.layeringViolations,
|
|
2250
|
+
anyCount: current.counters.anyCount - previous.counters.anyCount,
|
|
2251
|
+
tsIgnoreCount: current.counters.tsIgnoreCount - previous.counters.tsIgnoreCount,
|
|
2252
|
+
unusedFiles: current.counters.unusedFiles - previous.counters.unusedFiles,
|
|
2253
|
+
unusedDeps: current.counters.unusedDeps - previous.counters.unusedDeps,
|
|
2254
|
+
avgInstability: Math.round((current.counters.avgInstability - previous.counters.avgInstability) * 100) / 100
|
|
2255
|
+
};
|
|
2256
|
+
}
|
|
2257
|
+
var QualitySnapshotStore = class {
|
|
2258
|
+
snapshotPath;
|
|
2259
|
+
maxEntries;
|
|
2260
|
+
constructor(rootDir, maxEntries = MAX_SNAPSHOTS_DEFAULT) {
|
|
2261
|
+
this.snapshotPath = join(rootDir, SNAPSHOT_DIR, SNAPSHOT_FILE);
|
|
2262
|
+
this.maxEntries = maxEntries;
|
|
2263
|
+
}
|
|
2264
|
+
load() {
|
|
2265
|
+
return readSnapshots(this.snapshotPath);
|
|
2266
|
+
}
|
|
2267
|
+
save(input) {
|
|
2268
|
+
const snap = {
|
|
2269
|
+
id: randomUUID(),
|
|
2270
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2271
|
+
...input
|
|
2272
|
+
};
|
|
2273
|
+
const history = this.load();
|
|
2274
|
+
history.push(snap);
|
|
2275
|
+
while (history.length > this.maxEntries) {
|
|
2276
|
+
history.shift();
|
|
2277
|
+
}
|
|
2278
|
+
writeSnapshots(this.snapshotPath, history);
|
|
2279
|
+
return snap;
|
|
2280
|
+
}
|
|
2281
|
+
latest() {
|
|
2282
|
+
const h = this.load();
|
|
2283
|
+
return h.length > 0 ? h[h.length - 1] ?? null : null;
|
|
2284
|
+
}
|
|
2285
|
+
history() {
|
|
2286
|
+
const snapshots = this.load();
|
|
2287
|
+
const latest = snapshots.length > 0 ? snapshots[snapshots.length - 1] ?? null : null;
|
|
2288
|
+
const previous = snapshots.length > 1 ? snapshots[snapshots.length - 2] ?? null : null;
|
|
2289
|
+
const delta = latest && previous ? computeDelta(latest, previous) : null;
|
|
2290
|
+
return { snapshots, delta, latest };
|
|
2291
|
+
}
|
|
2292
|
+
};
|
|
1920
2293
|
|
|
1921
|
-
export { analyzeDependencies, analyzeDuplicateDependencies, analyzeMissingDependencies, analyzeTypes, analyzeUnusedDependencies, buildDependencyGraph, buildFileImportGraph,
|
|
2294
|
+
export { QualitySnapshotStore, analyzeBuildOrder, analyzeCoupling, analyzeDependencies, analyzeDuplicateDependencies, analyzeLayering, analyzeMissingDependencies, analyzeTypes, analyzeUnusedDependencies, buildDependencyGraph, buildFileImportGraph, buildPackageMap, calculateHealth, calculateLinesOfCode, calculateSize, calculateStats, checkBuilds, collectEntryPoints, countPackages, distPathToSrcPath, extractFileImports, findCircularDependencies, findReachableFiles, formatBytes, getBuildOrderForPackage, getImpactAnalysis, getReverseDependencies, listBackups, parseManifestHandlers, parseTsupEntries, removeDeadFiles, resolveLayer, resolveRelativeImport, restoreFromBackup, runKnip, runTests, scanDeadFiles, scanWorkspace, topologicalSort };
|
|
1922
2295
|
//# sourceMappingURL=index.js.map
|
|
1923
2296
|
//# sourceMappingURL=index.js.map
|