@kb-labs/quality-core 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +63 -0
- package/dist/builds/index.d.ts +34 -0
- package/dist/builds/index.js +133 -0
- package/dist/builds/index.js.map +1 -0
- package/dist/dead-code/index.d.ts +121 -0
- package/dist/dead-code/index.js +837 -0
- package/dist/dead-code/index.js.map +1 -0
- package/dist/dependencies/index.d.ts +50 -0
- package/dist/dependencies/index.js +200 -0
- package/dist/dependencies/index.js.map +1 -0
- package/dist/graph/index.d.ts +50 -0
- package/dist/graph/index.js +248 -0
- package/dist/graph/index.js.map +1 -0
- package/dist/health/index.d.ts +39 -0
- package/dist/health/index.js +130 -0
- package/dist/health/index.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +1921 -0
- package/dist/index.js.map +1 -0
- package/dist/stale/index.d.ts +31 -0
- package/dist/stale/index.js +273 -0
- package/dist/stale/index.js.map +1 -0
- package/dist/stats/index.d.ts +33 -0
- package/dist/stats/index.js +74 -0
- package/dist/stats/index.js.map +1 -0
- package/dist/tests/index.d.ts +47 -0
- package/dist/tests/index.js +200 -0
- package/dist/tests/index.js.map +1 -0
- package/dist/types/index.d.ts +32 -0
- package/dist/types/index.js +159 -0
- package/dist/types/index.js.map +1 -0
- package/package.json +90 -0
package/README.md
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# @kb-labs/quality-core
|
|
2
|
+
|
|
3
|
+
Core business logic for KB Labs Quality Plugin. This package contains all the analysis and computation logic for monorepo quality analysis, separated from CLI and REST handlers.
|
|
4
|
+
|
|
5
|
+
## Architecture
|
|
6
|
+
|
|
7
|
+
This package follows the **commit-plugin** pattern where business logic is extracted into a separate `core` package:
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
quality-core/ # Business logic (this package)
|
|
11
|
+
quality-cli/ # CLI commands + REST handlers
|
|
12
|
+
quality-contracts/ # Shared types and schemas
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Modules
|
|
16
|
+
|
|
17
|
+
### `stats` - Monorepo Statistics
|
|
18
|
+
- `calculateStats(rootDir: string): Promise<StatsResult>`
|
|
19
|
+
- Package counting, LOC analysis, size calculation
|
|
20
|
+
|
|
21
|
+
### `health` - Health Scoring
|
|
22
|
+
- `calculateHealth(rootDir: string): Promise<HealthResult>`
|
|
23
|
+
- Dependency health, structure validation, grade A-F
|
|
24
|
+
|
|
25
|
+
### `dependencies` - Dependency Analysis
|
|
26
|
+
- `analyzeDependencies(rootDir: string): Promise<DependencyAnalysis>`
|
|
27
|
+
- Find duplicates, unused, missing workspace dependencies
|
|
28
|
+
|
|
29
|
+
### `build-order` - Topological Sort
|
|
30
|
+
- `calculateBuildOrder(rootDir: string): Promise<BuildOrderResult>`
|
|
31
|
+
- Topological sort with parallel build layers
|
|
32
|
+
- Circular dependency detection
|
|
33
|
+
|
|
34
|
+
### `graph` - Dependency Graph
|
|
35
|
+
- `buildDependencyGraph(rootDir: string): DependencyGraph`
|
|
36
|
+
- Tree view, reverse dependencies, impact analysis
|
|
37
|
+
|
|
38
|
+
## Usage
|
|
39
|
+
|
|
40
|
+
```typescript
|
|
41
|
+
import { calculateStats } from '@kb-labs/quality-core/stats';
|
|
42
|
+
import { calculateHealth } from '@kb-labs/quality-core/health';
|
|
43
|
+
import { analyzeDependencies } from '@kb-labs/quality-core/dependencies';
|
|
44
|
+
|
|
45
|
+
// In CLI command
|
|
46
|
+
const stats = await calculateStats(ctx.cwd);
|
|
47
|
+
ctx.ui.success('Stats calculated', { data: stats });
|
|
48
|
+
|
|
49
|
+
// In REST handler
|
|
50
|
+
const health = await calculateHealth(ctx.cwd);
|
|
51
|
+
return { score: health.score, grade: health.grade };
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Why Separate Core?
|
|
55
|
+
|
|
56
|
+
- **Reusability**: CLI, REST handlers, and Studio all use same logic
|
|
57
|
+
- **Testability**: Easy to unit test without CLI/REST overhead
|
|
58
|
+
- **Maintainability**: Business logic changes don't affect handlers
|
|
59
|
+
- **Type Safety**: Single source of truth for calculations
|
|
60
|
+
|
|
61
|
+
## License
|
|
62
|
+
|
|
63
|
+
MIT
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Check build status across monorepo packages
|
|
3
|
+
*
|
|
4
|
+
* Pure computation - no CLI, no dependencies on devkit
|
|
5
|
+
*/
|
|
6
|
+
interface BuildCheckResult {
|
|
7
|
+
totalPackages: number;
|
|
8
|
+
passing: number;
|
|
9
|
+
failing: number;
|
|
10
|
+
failures: Array<{
|
|
11
|
+
package: string;
|
|
12
|
+
error: string;
|
|
13
|
+
exitCode: number;
|
|
14
|
+
}>;
|
|
15
|
+
staleBuilds: Array<{
|
|
16
|
+
package: string;
|
|
17
|
+
distMtime: number;
|
|
18
|
+
srcMtime: number;
|
|
19
|
+
}>;
|
|
20
|
+
duration: number;
|
|
21
|
+
}
|
|
22
|
+
interface BuildCheckOptions {
|
|
23
|
+
packageFilter?: string;
|
|
24
|
+
includeDevDeps?: boolean;
|
|
25
|
+
timeout?: number;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Check builds across monorepo
|
|
29
|
+
*
|
|
30
|
+
* @returns Build check results with failures and stale builds
|
|
31
|
+
*/
|
|
32
|
+
declare function checkBuilds(rootDir: string, options?: BuildCheckOptions): Promise<BuildCheckResult>;
|
|
33
|
+
|
|
34
|
+
export { type BuildCheckOptions, type BuildCheckResult, checkBuilds };
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { exec } from 'child_process';
|
|
2
|
+
import { promisify } from 'util';
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
|
|
6
|
+
// src/builds/check-builds.ts
|
|
7
|
+
var execAsync = promisify(exec);
|
|
8
|
+
function findPackagesWithBuildScript(rootDir, filter) {
|
|
9
|
+
const packages = [];
|
|
10
|
+
if (!fs.existsSync(rootDir)) {
|
|
11
|
+
return packages;
|
|
12
|
+
}
|
|
13
|
+
const entries = fs.readdirSync(rootDir, { withFileTypes: true });
|
|
14
|
+
for (const entry of entries) {
|
|
15
|
+
if (!entry.isDirectory() || !entry.name.startsWith("kb-labs-")) {
|
|
16
|
+
continue;
|
|
17
|
+
}
|
|
18
|
+
const repoPath = path.join(rootDir, entry.name);
|
|
19
|
+
const packagesDir = path.join(repoPath, "packages");
|
|
20
|
+
if (!fs.existsSync(packagesDir)) {
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
const packageDirs = fs.readdirSync(packagesDir, { withFileTypes: true });
|
|
24
|
+
for (const pkgDir of packageDirs) {
|
|
25
|
+
if (!pkgDir.isDirectory()) {
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
const packageJsonPath = path.join(packagesDir, pkgDir.name, "package.json");
|
|
29
|
+
if (fs.existsSync(packageJsonPath)) {
|
|
30
|
+
const pkgJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"));
|
|
31
|
+
if (!pkgJson.scripts?.build) {
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
if (filter && !pkgJson.name.includes(filter) && !pkgDir.name.includes(filter)) {
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
packages.push(packageJsonPath);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return packages;
|
|
42
|
+
}
|
|
43
|
+
function isDistStale(packageDir) {
|
|
44
|
+
const distFile = path.join(packageDir, "dist/index.js");
|
|
45
|
+
const srcDir = path.join(packageDir, "src");
|
|
46
|
+
if (!fs.existsSync(distFile)) {
|
|
47
|
+
return { stale: false };
|
|
48
|
+
}
|
|
49
|
+
if (!fs.existsSync(srcDir)) {
|
|
50
|
+
return { stale: false };
|
|
51
|
+
}
|
|
52
|
+
const distMtime = fs.statSync(distFile).mtime.getTime();
|
|
53
|
+
let newestSrcMtime = 0;
|
|
54
|
+
function walkDir(dir) {
|
|
55
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
56
|
+
for (const entry of entries) {
|
|
57
|
+
const fullPath = path.join(dir, entry.name);
|
|
58
|
+
if (entry.isDirectory()) {
|
|
59
|
+
walkDir(fullPath);
|
|
60
|
+
} else if (/\.(ts|tsx|js|jsx)$/.test(entry.name)) {
|
|
61
|
+
const mtime = fs.statSync(fullPath).mtime.getTime();
|
|
62
|
+
if (mtime > newestSrcMtime) {
|
|
63
|
+
newestSrcMtime = mtime;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
walkDir(srcDir);
|
|
69
|
+
return {
|
|
70
|
+
stale: newestSrcMtime > distMtime,
|
|
71
|
+
distMtime,
|
|
72
|
+
srcMtime: newestSrcMtime
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
async function tryBuildPackage(packageDir, packageName, timeout) {
|
|
76
|
+
try {
|
|
77
|
+
await execAsync("pnpm run build", {
|
|
78
|
+
cwd: packageDir,
|
|
79
|
+
timeout,
|
|
80
|
+
encoding: "utf-8"
|
|
81
|
+
});
|
|
82
|
+
return { success: true };
|
|
83
|
+
} catch (err) {
|
|
84
|
+
return {
|
|
85
|
+
success: false,
|
|
86
|
+
error: err.stderr?.trim() || err.message,
|
|
87
|
+
exitCode: err.code || 1
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
async function checkBuilds(rootDir, options = {}) {
|
|
92
|
+
const startTime = Date.now();
|
|
93
|
+
const timeout = options.timeout || 3e4;
|
|
94
|
+
const packagePaths = findPackagesWithBuildScript(rootDir, options.packageFilter);
|
|
95
|
+
const result = {
|
|
96
|
+
totalPackages: packagePaths.length,
|
|
97
|
+
passing: 0,
|
|
98
|
+
failing: 0,
|
|
99
|
+
failures: [],
|
|
100
|
+
staleBuilds: [],
|
|
101
|
+
duration: 0
|
|
102
|
+
};
|
|
103
|
+
for (const pkgPath of packagePaths) {
|
|
104
|
+
const pkgJson = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
|
|
105
|
+
const packageName = pkgJson.name;
|
|
106
|
+
const packageDir = path.dirname(pkgPath);
|
|
107
|
+
const staleCheck = isDistStale(packageDir);
|
|
108
|
+
if (staleCheck.stale) {
|
|
109
|
+
result.staleBuilds.push({
|
|
110
|
+
package: packageName,
|
|
111
|
+
distMtime: staleCheck.distMtime,
|
|
112
|
+
srcMtime: staleCheck.srcMtime
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
const buildResult = await tryBuildPackage(packageDir, packageName, timeout);
|
|
116
|
+
if (buildResult.success) {
|
|
117
|
+
result.passing++;
|
|
118
|
+
} else {
|
|
119
|
+
result.failing++;
|
|
120
|
+
result.failures.push({
|
|
121
|
+
package: packageName,
|
|
122
|
+
error: buildResult.error,
|
|
123
|
+
exitCode: buildResult.exitCode
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
result.duration = Date.now() - startTime;
|
|
128
|
+
return result;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export { checkBuilds };
|
|
132
|
+
//# sourceMappingURL=index.js.map
|
|
133
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/builds/check-builds.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * Check build status across monorepo packages\n *\n * Pure computation - no CLI, no dependencies on devkit\n */\n\nimport { exec } from 'node:child_process';\nimport { promisify } from 'node:util';\nimport fs from 'node:fs';\nimport path from 'node:path';\n\nconst execAsync = promisify(exec);\n\nexport interface BuildCheckResult {\n totalPackages: number;\n passing: number;\n failing: number;\n failures: Array<{\n package: string;\n error: string;\n exitCode: number;\n }>;\n staleBuilds: Array<{\n package: string;\n distMtime: number;\n srcMtime: number;\n }>;\n duration: number;\n}\n\nexport interface BuildCheckOptions {\n packageFilter?: string;\n includeDevDeps?: boolean;\n timeout?: number; // per-package timeout in ms\n}\n\n/**\n * Find all packages with build scripts\n */\nfunction findPackagesWithBuildScript(rootDir: string, filter?: string): string[] {\n const packages: string[] = [];\n\n if (!fs.existsSync(rootDir)) {\n return packages;\n }\n\n const entries = fs.readdirSync(rootDir, { withFileTypes: true });\n\n for (const entry of entries) {\n if (!entry.isDirectory() || !entry.name.startsWith('kb-labs-')) {continue;}\n\n const repoPath = path.join(rootDir, entry.name);\n const packagesDir = path.join(repoPath, 'packages');\n\n if (!fs.existsSync(packagesDir)) {continue;}\n\n const packageDirs = fs.readdirSync(packagesDir, { withFileTypes: true });\n\n for (const pkgDir of packageDirs) {\n if (!pkgDir.isDirectory()) {continue;}\n\n const packageJsonPath = path.join(packagesDir, pkgDir.name, 'package.json');\n\n if (fs.existsSync(packageJsonPath)) {\n const pkgJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));\n\n // Skip if no build script\n if (!pkgJson.scripts?.build) {continue;}\n\n // Skip if filter doesn't match\n if (filter && !pkgJson.name.includes(filter) && !pkgDir.name.includes(filter)) {\n continue;\n }\n\n packages.push(packageJsonPath);\n }\n }\n }\n\n return packages;\n}\n\n/**\n * Check if dist/ is stale (older than src/)\n */\nfunction isDistStale(packageDir: string): {\n stale: boolean;\n distMtime?: number;\n srcMtime?: number;\n} {\n const distFile = path.join(packageDir, 'dist/index.js');\n const srcDir = path.join(packageDir, 'src');\n\n if (!fs.existsSync(distFile)) {\n return { stale: false };\n }\n\n if (!fs.existsSync(srcDir)) {\n return { stale: false };\n }\n\n const distMtime = fs.statSync(distFile).mtime.getTime();\n\n // Find newest file in src/\n let newestSrcMtime = 0;\n\n function walkDir(dir: string) {\n const entries = fs.readdirSync(dir, { withFileTypes: true });\n for (const entry of entries) {\n const fullPath = path.join(dir, entry.name);\n if (entry.isDirectory()) {\n walkDir(fullPath);\n } else if (/\\.(ts|tsx|js|jsx)$/.test(entry.name)) {\n const mtime = fs.statSync(fullPath).mtime.getTime();\n if (mtime > newestSrcMtime) {\n newestSrcMtime = mtime;\n }\n }\n }\n }\n\n walkDir(srcDir);\n\n return {\n stale: newestSrcMtime > distMtime,\n distMtime,\n srcMtime: newestSrcMtime,\n };\n}\n\n/**\n * Try to build a single package\n */\nasync function tryBuildPackage(\n packageDir: string,\n packageName: string,\n timeout: number\n): Promise<{ success: boolean; error?: string; exitCode?: number }> {\n try {\n await execAsync('pnpm run build', {\n cwd: packageDir,\n timeout,\n encoding: 'utf-8',\n });\n\n return { success: true };\n } catch (err: any) {\n return {\n success: false,\n error: err.stderr?.trim() || err.message,\n exitCode: err.code || 1,\n };\n }\n}\n\n/**\n * Check builds across monorepo\n *\n * @returns Build check results with failures and stale builds\n */\nexport async function checkBuilds(\n rootDir: string,\n options: BuildCheckOptions = {}\n): Promise<BuildCheckResult> {\n const startTime = Date.now();\n const timeout = options.timeout || 30000; // 30s default\n\n const packagePaths = findPackagesWithBuildScript(rootDir, options.packageFilter);\n\n const result: BuildCheckResult = {\n totalPackages: packagePaths.length,\n passing: 0,\n failing: 0,\n failures: [],\n staleBuilds: [],\n duration: 0,\n };\n\n // Check builds sequentially (parallel would be too heavy)\n for (const pkgPath of packagePaths) {\n const pkgJson = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));\n const packageName = pkgJson.name;\n const packageDir = path.dirname(pkgPath);\n\n // Check if dist is stale\n const staleCheck = isDistStale(packageDir);\n if (staleCheck.stale) {\n result.staleBuilds.push({\n package: packageName,\n distMtime: staleCheck.distMtime!,\n srcMtime: staleCheck.srcMtime!,\n });\n }\n\n // Try to build\n const buildResult = await tryBuildPackage(packageDir, packageName, timeout);\n\n if (buildResult.success) {\n result.passing++;\n } else {\n result.failing++;\n result.failures.push({\n package: packageName,\n error: buildResult.error!,\n exitCode: buildResult.exitCode!,\n });\n }\n }\n\n result.duration = Date.now() - startTime;\n\n return result;\n}\n"]}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { DeadCodeOptions, DeadCodeResult, DeadCodeRemovalResult, DeadCodeBackupManifest } from '@kb-labs/quality-contracts';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Dead code file scanner — main orchestrator
|
|
5
|
+
*
|
|
6
|
+
* Per-package analysis: collect entry points, build import graph,
|
|
7
|
+
* BFS reachability, report unreachable files.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Scan the monorepo for dead (unreachable) source files.
|
|
12
|
+
*
|
|
13
|
+
* For each package:
|
|
14
|
+
* 1. Collect entry points (package.json, tsup, manifest, tests, configs)
|
|
15
|
+
* 2. Build file-level import graph
|
|
16
|
+
* 3. BFS from entry points to find reachable files
|
|
17
|
+
* 4. Everything not reachable = dead
|
|
18
|
+
*/
|
|
19
|
+
declare function scanDeadFiles(rootDir: string, options?: DeadCodeOptions): Promise<DeadCodeResult>;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Entry point collection for dead code detection
|
|
23
|
+
*
|
|
24
|
+
* Collects all files that are "alive by definition" — entry points from
|
|
25
|
+
* package.json, tsup.config.ts, manifest.ts, tests, and configs.
|
|
26
|
+
*
|
|
27
|
+
* Fail-open: if we can't parse a config, we emit a warning and treat
|
|
28
|
+
* ALL files in the package as alive (zero false positives).
|
|
29
|
+
*/
|
|
30
|
+
interface EntryPointResult {
|
|
31
|
+
/** Absolute paths of entry point files (roots for BFS) */
|
|
32
|
+
entryFiles: Set<string>;
|
|
33
|
+
/** Files alive by convention (tests, configs) — not part of graph traversal */
|
|
34
|
+
aliveByConvention: Set<string>;
|
|
35
|
+
/** Non-fatal issues during parsing */
|
|
36
|
+
warnings: string[];
|
|
37
|
+
/** If true, couldn't parse configs — all files should be treated as alive */
|
|
38
|
+
failOpen: boolean;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Collect all entry points for a package.
|
|
42
|
+
*
|
|
43
|
+
* A file is an entry point if it's referenced by package.json, tsup.config.ts,
|
|
44
|
+
* or manifest.ts. Tests and config files are always alive by convention.
|
|
45
|
+
*/
|
|
46
|
+
declare function collectEntryPoints(packageDir: string, packageJson: Record<string, unknown>): Promise<EntryPointResult>;
|
|
47
|
+
/**
|
|
48
|
+
* Map a dist/ path to its src/ counterpart.
|
|
49
|
+
* dist/index.js → src/index.ts
|
|
50
|
+
* dist/sandbox/bootstrap.js → src/sandbox/bootstrap.ts
|
|
51
|
+
*/
|
|
52
|
+
declare function distPathToSrcPath(distPath: string, packageDir: string): string | null;
|
|
53
|
+
/**
|
|
54
|
+
* Parse entry points from tsup config file content.
|
|
55
|
+
* Handles: string, array of strings, object of strings.
|
|
56
|
+
*/
|
|
57
|
+
declare function parseTsupEntries(content: string): string[];
|
|
58
|
+
/**
|
|
59
|
+
* Extract handler paths from manifest.ts content.
|
|
60
|
+
* Matches: handler: './path/to/file.js#export' and handlerPath: './path/to/file.js'
|
|
61
|
+
*/
|
|
62
|
+
declare function parseManifestHandlers(content: string): string[];
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* File-level import graph and BFS reachability analysis
|
|
66
|
+
*
|
|
67
|
+
* Builds a directed graph of file → imported files, then walks from
|
|
68
|
+
* entry points to find all reachable (alive) files.
|
|
69
|
+
*/
|
|
70
|
+
/**
|
|
71
|
+
* Extract all import paths from a source file's content.
|
|
72
|
+
* Returns raw import specifiers (both relative and package).
|
|
73
|
+
*/
|
|
74
|
+
declare function extractFileImports(content: string): string[];
|
|
75
|
+
/**
|
|
76
|
+
* Resolve a relative import to an absolute file path.
|
|
77
|
+
*
|
|
78
|
+
* Handles ESM convention where source code imports './foo.js'
|
|
79
|
+
* but the actual file is './foo.ts'.
|
|
80
|
+
*/
|
|
81
|
+
declare function resolveRelativeImport(specifier: string, sourceFile: string): string | null;
|
|
82
|
+
/**
|
|
83
|
+
* Build a file-level import graph for all source files.
|
|
84
|
+
*
|
|
85
|
+
* Returns a map: absolute file path → set of absolute imported file paths.
|
|
86
|
+
* Only includes relative imports (not external packages).
|
|
87
|
+
*/
|
|
88
|
+
declare function buildFileImportGraph(sourceFiles: string[]): Map<string, Set<string>>;
|
|
89
|
+
/**
|
|
90
|
+
* BFS from entry points through the import graph.
|
|
91
|
+
* Returns the set of all reachable file paths.
|
|
92
|
+
*/
|
|
93
|
+
declare function findReachableFiles(entryPoints: Set<string>, importGraph: Map<string, Set<string>>): Set<string>;
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Backup, restore, and auto-removal for dead code files
|
|
97
|
+
*
|
|
98
|
+
* Creates timestamped backups before deletion, supports full restore,
|
|
99
|
+
* and cleans up empty directories + package.json exports.
|
|
100
|
+
*/
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Remove dead files with full backup.
|
|
104
|
+
* Creates a timestamped backup directory, copies files, then deletes.
|
|
105
|
+
*/
|
|
106
|
+
declare function removeDeadFiles(rootDir: string, scanResult: DeadCodeResult, options?: {
|
|
107
|
+
dryRun?: boolean;
|
|
108
|
+
}): Promise<DeadCodeRemovalResult>;
|
|
109
|
+
/**
|
|
110
|
+
* Restore files from a backup.
|
|
111
|
+
*/
|
|
112
|
+
declare function restoreFromBackup(rootDir: string, backupId: string): Promise<{
|
|
113
|
+
restoredFiles: number;
|
|
114
|
+
restoredExports: number;
|
|
115
|
+
}>;
|
|
116
|
+
/**
|
|
117
|
+
* List all available backups.
|
|
118
|
+
*/
|
|
119
|
+
declare function listBackups(rootDir: string): DeadCodeBackupManifest[];
|
|
120
|
+
|
|
121
|
+
export { buildFileImportGraph, collectEntryPoints, distPathToSrcPath, extractFileImports, findReachableFiles, listBackups, parseManifestHandlers, parseTsupEntries, removeDeadFiles, resolveRelativeImport, restoreFromBackup, scanDeadFiles };
|