@bfra.me/workspace-analyzer 0.1.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 +402 -0
- package/lib/chunk-4LSFAAZW.js +1 -0
- package/lib/chunk-JDF7DQ4V.js +27 -0
- package/lib/chunk-WOJ4C7N7.js +7122 -0
- package/lib/cli.d.ts +1 -0
- package/lib/cli.js +318 -0
- package/lib/index.d.ts +3701 -0
- package/lib/index.js +1262 -0
- package/lib/types/index.d.ts +146 -0
- package/lib/types/index.js +28 -0
- package/package.json +89 -0
- package/src/analyzers/analyzer.ts +201 -0
- package/src/analyzers/architectural-analyzer.ts +304 -0
- package/src/analyzers/build-config-analyzer.ts +334 -0
- package/src/analyzers/circular-import-analyzer.ts +463 -0
- package/src/analyzers/config-consistency-analyzer.ts +335 -0
- package/src/analyzers/dead-code-analyzer.ts +565 -0
- package/src/analyzers/duplicate-code-analyzer.ts +626 -0
- package/src/analyzers/duplicate-dependency-analyzer.ts +381 -0
- package/src/analyzers/eslint-config-analyzer.ts +281 -0
- package/src/analyzers/exports-field-analyzer.ts +324 -0
- package/src/analyzers/index.ts +388 -0
- package/src/analyzers/large-dependency-analyzer.ts +535 -0
- package/src/analyzers/package-json-analyzer.ts +349 -0
- package/src/analyzers/peer-dependency-analyzer.ts +275 -0
- package/src/analyzers/tree-shaking-analyzer.ts +623 -0
- package/src/analyzers/tsconfig-analyzer.ts +382 -0
- package/src/analyzers/unused-dependency-analyzer.ts +356 -0
- package/src/analyzers/version-alignment-analyzer.ts +308 -0
- package/src/api/analyze-workspace.ts +245 -0
- package/src/api/index.ts +11 -0
- package/src/cache/cache-manager.ts +495 -0
- package/src/cache/cache-schema.ts +247 -0
- package/src/cache/change-detector.ts +169 -0
- package/src/cache/file-hasher.ts +65 -0
- package/src/cache/index.ts +47 -0
- package/src/cli/commands/analyze.ts +240 -0
- package/src/cli/commands/index.ts +5 -0
- package/src/cli/index.ts +61 -0
- package/src/cli/types.ts +65 -0
- package/src/cli/ui.ts +213 -0
- package/src/cli.ts +9 -0
- package/src/config/defaults.ts +183 -0
- package/src/config/index.ts +81 -0
- package/src/config/loader.ts +270 -0
- package/src/config/merger.ts +229 -0
- package/src/config/schema.ts +263 -0
- package/src/core/incremental-analyzer.ts +462 -0
- package/src/core/index.ts +34 -0
- package/src/core/orchestrator.ts +416 -0
- package/src/graph/dependency-graph.ts +408 -0
- package/src/graph/index.ts +19 -0
- package/src/index.ts +417 -0
- package/src/parser/config-parser.ts +491 -0
- package/src/parser/import-extractor.ts +340 -0
- package/src/parser/index.ts +54 -0
- package/src/parser/typescript-parser.ts +95 -0
- package/src/performance/bundle-estimator.ts +444 -0
- package/src/performance/index.ts +27 -0
- package/src/reporters/console-reporter.ts +355 -0
- package/src/reporters/index.ts +49 -0
- package/src/reporters/json-reporter.ts +273 -0
- package/src/reporters/markdown-reporter.ts +349 -0
- package/src/reporters/reporter.ts +399 -0
- package/src/rules/builtin-rules.ts +709 -0
- package/src/rules/index.ts +52 -0
- package/src/rules/rule-engine.ts +409 -0
- package/src/scanner/index.ts +18 -0
- package/src/scanner/workspace-scanner.ts +403 -0
- package/src/types/index.ts +176 -0
- package/src/types/result.ts +19 -0
- package/src/utils/index.ts +7 -0
- package/src/utils/pattern-matcher.ts +48 -0
package/lib/index.js
ADDED
|
@@ -0,0 +1,1262 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BUILTIN_ANALYZER_IDS,
|
|
3
|
+
BUILTIN_RULE_IDS,
|
|
4
|
+
CATEGORY_CONFIG,
|
|
5
|
+
DEFAULT_ANALYZER_CONFIG,
|
|
6
|
+
DEFAULT_CONCURRENCY,
|
|
7
|
+
DEFAULT_EXCLUDE_PATTERNS,
|
|
8
|
+
DEFAULT_INCLUDE_PATTERNS,
|
|
9
|
+
DEFAULT_LAYER_CONFIG,
|
|
10
|
+
DEFAULT_PACKAGE_PATTERNS,
|
|
11
|
+
DEFAULT_REPORT_OPTIONS,
|
|
12
|
+
DEFAULT_WORKSPACE_OPTIONS,
|
|
13
|
+
METADATA,
|
|
14
|
+
METADATA2,
|
|
15
|
+
METADATA3,
|
|
16
|
+
METADATA4,
|
|
17
|
+
METADATA5,
|
|
18
|
+
METADATA6,
|
|
19
|
+
METADATA7,
|
|
20
|
+
SEVERITY_CONFIG,
|
|
21
|
+
aggregatePackageImports,
|
|
22
|
+
analyzePackages,
|
|
23
|
+
analyzeWorkspace,
|
|
24
|
+
architecturalAnalyzerMetadata,
|
|
25
|
+
barrelExportRuleMetadata,
|
|
26
|
+
buildDependencyGraph,
|
|
27
|
+
builtinAnalyzers,
|
|
28
|
+
calculateSummary,
|
|
29
|
+
circularImportAnalyzerMetadata,
|
|
30
|
+
computeCycleStats,
|
|
31
|
+
computeDeadCodeStats,
|
|
32
|
+
computeDuplicateStats,
|
|
33
|
+
computeGraphStatistics,
|
|
34
|
+
createAnalyzerRegistry,
|
|
35
|
+
createArchitecturalAnalyzer,
|
|
36
|
+
createBarrelExportRule,
|
|
37
|
+
createBuildConfigAnalyzer,
|
|
38
|
+
createCircularImportAnalyzer,
|
|
39
|
+
createConfigConsistencyAnalyzer,
|
|
40
|
+
createConsoleReporter,
|
|
41
|
+
createDeadCodeAnalyzer,
|
|
42
|
+
createDefaultRegistry,
|
|
43
|
+
createDuplicateCodeAnalyzer,
|
|
44
|
+
createDuplicateDependencyAnalyzer,
|
|
45
|
+
createEslintConfigAnalyzer,
|
|
46
|
+
createExportsFieldAnalyzer,
|
|
47
|
+
createIssue,
|
|
48
|
+
createJsonReporter,
|
|
49
|
+
createLargeDependencyAnalyzer,
|
|
50
|
+
createLayerViolationRule,
|
|
51
|
+
createMarkdownReporter,
|
|
52
|
+
createOrchestrator,
|
|
53
|
+
createPackageBoundaryRule,
|
|
54
|
+
createPackageJsonAnalyzer,
|
|
55
|
+
createPathAliasRule,
|
|
56
|
+
createPeerDependencyAnalyzer,
|
|
57
|
+
createProject,
|
|
58
|
+
createPublicApiRule,
|
|
59
|
+
createRuleEngine,
|
|
60
|
+
createSideEffectRule,
|
|
61
|
+
createTreeShakingBlockerAnalyzer,
|
|
62
|
+
createTsconfigAnalyzer,
|
|
63
|
+
createUnusedDependencyAnalyzer,
|
|
64
|
+
createVersionAlignmentAnalyzer,
|
|
65
|
+
createWorkspaceScanner,
|
|
66
|
+
deadCodeAnalyzerMetadata,
|
|
67
|
+
defineConfig,
|
|
68
|
+
duplicateCodeAnalyzerMetadata,
|
|
69
|
+
duplicateDependencyAnalyzerMetadata,
|
|
70
|
+
extractImports,
|
|
71
|
+
filterIssues,
|
|
72
|
+
filterIssuesForReport,
|
|
73
|
+
filterPackagesByPattern,
|
|
74
|
+
findConfigFile,
|
|
75
|
+
findCycles,
|
|
76
|
+
formatDuration,
|
|
77
|
+
formatLocation,
|
|
78
|
+
generateCycleVisualization,
|
|
79
|
+
getAllDependencies,
|
|
80
|
+
getAllSourceFiles,
|
|
81
|
+
getAnalyzerOptions,
|
|
82
|
+
getDefaultConfig,
|
|
83
|
+
getFileLayer,
|
|
84
|
+
getKnownLargePackages,
|
|
85
|
+
getPackageInfo,
|
|
86
|
+
getPackageNameFromSpecifier,
|
|
87
|
+
getPackageScope,
|
|
88
|
+
getRelativePath,
|
|
89
|
+
getSourceFile,
|
|
90
|
+
getTransitiveDependencies,
|
|
91
|
+
getTransitiveDependents,
|
|
92
|
+
getUniqueDependencies,
|
|
93
|
+
getUnscopedName,
|
|
94
|
+
groupIssues,
|
|
95
|
+
groupPackagesByScope,
|
|
96
|
+
isJavaScriptFile,
|
|
97
|
+
isLayerImportAllowed,
|
|
98
|
+
isRelativeImport,
|
|
99
|
+
isSourceFile,
|
|
100
|
+
isTypeScriptFile,
|
|
101
|
+
isWorkspacePackageImport,
|
|
102
|
+
largeDependencyAnalyzerMetadata,
|
|
103
|
+
layerViolationRuleMetadata,
|
|
104
|
+
loadConfig,
|
|
105
|
+
loadConfigFile,
|
|
106
|
+
matchAnyPattern,
|
|
107
|
+
matchPattern,
|
|
108
|
+
meetsMinSeverity,
|
|
109
|
+
mergeAnalyzerConfigs,
|
|
110
|
+
mergeConfig,
|
|
111
|
+
normalizePath,
|
|
112
|
+
packageBoundaryRuleMetadata,
|
|
113
|
+
parsePackageJson,
|
|
114
|
+
parsePackageJsonContent,
|
|
115
|
+
parseSourceContent,
|
|
116
|
+
parseSourceFile,
|
|
117
|
+
parseSourceFiles,
|
|
118
|
+
parseTsConfig,
|
|
119
|
+
parseTsConfigContent,
|
|
120
|
+
parseWorkspaceAnalyzerConfig,
|
|
121
|
+
pathAliasRuleMetadata,
|
|
122
|
+
peerDependencyAnalyzerMetadata,
|
|
123
|
+
publicApiRuleMetadata,
|
|
124
|
+
resolveRelativeImport,
|
|
125
|
+
resolveTsConfigExtends,
|
|
126
|
+
safeParseAnalyzeOptions,
|
|
127
|
+
shouldAnalyzeCategory,
|
|
128
|
+
sideEffectRuleMetadata,
|
|
129
|
+
treeShakingBlockerAnalyzerMetadata,
|
|
130
|
+
truncateText,
|
|
131
|
+
unusedDependencyAnalyzerMetadata
|
|
132
|
+
} from "./chunk-WOJ4C7N7.js";
|
|
133
|
+
import "./chunk-4LSFAAZW.js";
|
|
134
|
+
import {
|
|
135
|
+
err,
|
|
136
|
+
flatMap,
|
|
137
|
+
fromPromise,
|
|
138
|
+
fromThrowable,
|
|
139
|
+
isErr,
|
|
140
|
+
isOk,
|
|
141
|
+
map,
|
|
142
|
+
mapErr,
|
|
143
|
+
ok,
|
|
144
|
+
unwrap,
|
|
145
|
+
unwrapOr
|
|
146
|
+
} from "./chunk-JDF7DQ4V.js";
|
|
147
|
+
|
|
148
|
+
// src/cache/cache-manager.ts
|
|
149
|
+
import { Buffer } from "buffer";
|
|
150
|
+
import { mkdir, readFile, rm, stat, writeFile } from "fs/promises";
|
|
151
|
+
import { join } from "path";
|
|
152
|
+
import { promisify } from "util";
|
|
153
|
+
import { gunzip, gzip } from "zlib";
|
|
154
|
+
|
|
155
|
+
// src/cache/cache-schema.ts
|
|
156
|
+
var CACHE_SCHEMA_VERSION = 1;
|
|
157
|
+
var CONFIG_FILE_PATTERNS = [
|
|
158
|
+
"package.json",
|
|
159
|
+
"tsconfig.json",
|
|
160
|
+
"tsconfig.*.json",
|
|
161
|
+
"eslint.config.ts",
|
|
162
|
+
"eslint.config.js",
|
|
163
|
+
"eslint.config.mjs",
|
|
164
|
+
".eslintrc.*",
|
|
165
|
+
"tsup.config.ts",
|
|
166
|
+
"tsup.config.js",
|
|
167
|
+
"vitest.config.ts",
|
|
168
|
+
"vitest.config.js",
|
|
169
|
+
"workspace-analyzer.config.ts",
|
|
170
|
+
"workspace-analyzer.config.js"
|
|
171
|
+
];
|
|
172
|
+
var DEFAULT_CACHE_OPTIONS = {
|
|
173
|
+
cacheDir: ".workspace-analyzer-cache",
|
|
174
|
+
maxAge: 7 * 24 * 60 * 60 * 1e3,
|
|
175
|
+
// 7 days
|
|
176
|
+
compress: true,
|
|
177
|
+
hashAlgorithm: "sha256"
|
|
178
|
+
};
|
|
179
|
+
function createEmptyCache(workspacePath, configHash, analyzerVersion) {
|
|
180
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
181
|
+
return {
|
|
182
|
+
metadata: {
|
|
183
|
+
version: CACHE_SCHEMA_VERSION,
|
|
184
|
+
workspacePath,
|
|
185
|
+
createdAt: now,
|
|
186
|
+
updatedAt: now,
|
|
187
|
+
configHash,
|
|
188
|
+
analyzerVersion
|
|
189
|
+
},
|
|
190
|
+
files: {},
|
|
191
|
+
packages: {},
|
|
192
|
+
workspaceIssues: [],
|
|
193
|
+
configFiles: []
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
function createFileAnalysisEntry(path, contentHash, modifiedAt, size, issues, analyzersRun) {
|
|
197
|
+
return {
|
|
198
|
+
fileState: {
|
|
199
|
+
path,
|
|
200
|
+
contentHash,
|
|
201
|
+
modifiedAt: modifiedAt.toISOString(),
|
|
202
|
+
size
|
|
203
|
+
},
|
|
204
|
+
issues,
|
|
205
|
+
analyzersRun,
|
|
206
|
+
analyzedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
function createPackageAnalysisEntry(packageName, packagePath, packageJsonHash, issues, analyzersRun) {
|
|
210
|
+
return {
|
|
211
|
+
packageName,
|
|
212
|
+
packagePath,
|
|
213
|
+
packageJsonHash,
|
|
214
|
+
issues,
|
|
215
|
+
analyzersRun,
|
|
216
|
+
analyzedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// src/cache/change-detector.ts
|
|
221
|
+
import { createChangeDetector } from "@bfra.me/es/watcher";
|
|
222
|
+
|
|
223
|
+
// src/cache/file-hasher.ts
|
|
224
|
+
import { createFileHasher } from "@bfra.me/es/watcher";
|
|
225
|
+
function createWorkspaceHasher(options = {}) {
|
|
226
|
+
const { algorithm = "sha256" } = options;
|
|
227
|
+
const baseHasher = createFileHasher(algorithm);
|
|
228
|
+
return {
|
|
229
|
+
...baseHasher,
|
|
230
|
+
hashJson(obj) {
|
|
231
|
+
const normalized = JSON.stringify(obj, Object.keys(obj).sort(), 0);
|
|
232
|
+
return baseHasher.hashContent(normalized);
|
|
233
|
+
},
|
|
234
|
+
async hashFiles(paths) {
|
|
235
|
+
const hashes = await Promise.all(paths.map(async (path) => baseHasher.hash(path)));
|
|
236
|
+
const combined = hashes.join(":");
|
|
237
|
+
return baseHasher.hashContent(combined);
|
|
238
|
+
}
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// src/cache/change-detector.ts
|
|
243
|
+
function createAnalysisChangeDetector(options = {}) {
|
|
244
|
+
const baseDetector = createChangeDetector(options);
|
|
245
|
+
const hasher = createWorkspaceHasher({ algorithm: options.algorithm });
|
|
246
|
+
const recordedPaths = /* @__PURE__ */ new Set();
|
|
247
|
+
return {
|
|
248
|
+
async hasChanged(path) {
|
|
249
|
+
return baseDetector.hasChanged(path);
|
|
250
|
+
},
|
|
251
|
+
async record(path) {
|
|
252
|
+
recordedPaths.add(path);
|
|
253
|
+
return baseDetector.record(path);
|
|
254
|
+
},
|
|
255
|
+
clear(path) {
|
|
256
|
+
recordedPaths.delete(path);
|
|
257
|
+
baseDetector.clear(path);
|
|
258
|
+
},
|
|
259
|
+
clearAll() {
|
|
260
|
+
recordedPaths.clear();
|
|
261
|
+
baseDetector.clearAll();
|
|
262
|
+
},
|
|
263
|
+
getRecordedPaths() {
|
|
264
|
+
return Array.from(recordedPaths);
|
|
265
|
+
},
|
|
266
|
+
async validateCache(cachedFiles, currentFiles) {
|
|
267
|
+
const cachedPaths = new Set(cachedFiles.map((f) => f.path));
|
|
268
|
+
const currentPaths = new Set(currentFiles);
|
|
269
|
+
const changedFiles = [];
|
|
270
|
+
const newFiles = [];
|
|
271
|
+
const deletedFiles = [];
|
|
272
|
+
for (const path of currentPaths) {
|
|
273
|
+
if (!cachedPaths.has(path)) {
|
|
274
|
+
newFiles.push(path);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
for (const cached of cachedFiles) {
|
|
278
|
+
if (!currentPaths.has(cached.path)) {
|
|
279
|
+
deletedFiles.push(cached.path);
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
try {
|
|
283
|
+
const currentHash = await hasher.hash(cached.path);
|
|
284
|
+
if (currentHash !== cached.contentHash) {
|
|
285
|
+
changedFiles.push(cached.path);
|
|
286
|
+
}
|
|
287
|
+
} catch {
|
|
288
|
+
deletedFiles.push(cached.path);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
const hasChanges = changedFiles.length > 0 || newFiles.length > 0 || deletedFiles.length > 0;
|
|
292
|
+
let invalidationReason;
|
|
293
|
+
if (hasChanges) {
|
|
294
|
+
const reasons = [];
|
|
295
|
+
if (changedFiles.length > 0) reasons.push(`${changedFiles.length} files changed`);
|
|
296
|
+
if (newFiles.length > 0) reasons.push(`${newFiles.length} new files`);
|
|
297
|
+
if (deletedFiles.length > 0) reasons.push(`${deletedFiles.length} files deleted`);
|
|
298
|
+
invalidationReason = reasons.join(", ");
|
|
299
|
+
}
|
|
300
|
+
return {
|
|
301
|
+
isValid: !hasChanges,
|
|
302
|
+
changedFiles,
|
|
303
|
+
newFiles,
|
|
304
|
+
deletedFiles,
|
|
305
|
+
invalidatedPackages: [],
|
|
306
|
+
// Computed by cache manager based on file paths
|
|
307
|
+
changedConfigFiles: [],
|
|
308
|
+
invalidationReason
|
|
309
|
+
};
|
|
310
|
+
},
|
|
311
|
+
async hasConfigChanged(configFiles) {
|
|
312
|
+
for (const config of configFiles) {
|
|
313
|
+
try {
|
|
314
|
+
const currentHash = await hasher.hash(config.path);
|
|
315
|
+
if (currentHash !== config.contentHash) {
|
|
316
|
+
return true;
|
|
317
|
+
}
|
|
318
|
+
} catch {
|
|
319
|
+
return true;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
return false;
|
|
323
|
+
}
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// src/cache/cache-manager.ts
|
|
328
|
+
var gzipAsync = promisify(gzip);
|
|
329
|
+
var gunzipAsync = promisify(gunzip);
|
|
330
|
+
var CACHE_FILE_NAME = "analysis-cache.json";
|
|
331
|
+
var COMPRESSED_CACHE_FILE_NAME = "analysis-cache.json.gz";
|
|
332
|
+
function createCacheManager(options) {
|
|
333
|
+
const {
|
|
334
|
+
workspacePath,
|
|
335
|
+
analyzerVersion,
|
|
336
|
+
cacheDir = DEFAULT_CACHE_OPTIONS.cacheDir,
|
|
337
|
+
maxAge = DEFAULT_CACHE_OPTIONS.maxAge,
|
|
338
|
+
compress = DEFAULT_CACHE_OPTIONS.compress,
|
|
339
|
+
hashAlgorithm = DEFAULT_CACHE_OPTIONS.hashAlgorithm
|
|
340
|
+
} = options;
|
|
341
|
+
const cachePath = join(workspacePath, cacheDir);
|
|
342
|
+
const cacheFile = join(cachePath, compress ? COMPRESSED_CACHE_FILE_NAME : CACHE_FILE_NAME);
|
|
343
|
+
const hasher = createWorkspaceHasher({ algorithm: hashAlgorithm });
|
|
344
|
+
const changeDetector = createAnalysisChangeDetector({ algorithm: hashAlgorithm });
|
|
345
|
+
async function ensureCacheDir() {
|
|
346
|
+
await mkdir(cachePath, { recursive: true });
|
|
347
|
+
}
|
|
348
|
+
async function readCacheFile() {
|
|
349
|
+
try {
|
|
350
|
+
const content = await readFile(cacheFile);
|
|
351
|
+
if (compress) {
|
|
352
|
+
const decompressed = await gunzipAsync(content);
|
|
353
|
+
return ok(decompressed.toString("utf-8"));
|
|
354
|
+
}
|
|
355
|
+
return ok(content.toString("utf-8"));
|
|
356
|
+
} catch (error) {
|
|
357
|
+
if (error.code === "ENOENT") {
|
|
358
|
+
return err({ code: "CACHE_NOT_FOUND", message: "Cache file not found" });
|
|
359
|
+
}
|
|
360
|
+
return err({
|
|
361
|
+
code: "CACHE_READ_FAILED",
|
|
362
|
+
message: `Failed to read cache file: ${error.message}`,
|
|
363
|
+
cause: error
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
async function writeCacheFile(content) {
|
|
368
|
+
try {
|
|
369
|
+
await ensureCacheDir();
|
|
370
|
+
const buffer = Buffer.from(content, "utf-8");
|
|
371
|
+
const data = compress ? await gzipAsync(buffer) : buffer;
|
|
372
|
+
await writeFile(cacheFile, data);
|
|
373
|
+
return ok(void 0);
|
|
374
|
+
} catch (error) {
|
|
375
|
+
return err({
|
|
376
|
+
code: "CACHE_WRITE_FAILED",
|
|
377
|
+
message: `Failed to write cache file: ${error.message}`,
|
|
378
|
+
cause: error
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
async function getFileState(path) {
|
|
383
|
+
const stats = await stat(path);
|
|
384
|
+
const contentHash = await hasher.hash(path);
|
|
385
|
+
return {
|
|
386
|
+
path,
|
|
387
|
+
contentHash,
|
|
388
|
+
modifiedAt: stats.mtime.toISOString(),
|
|
389
|
+
size: stats.size
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
return {
|
|
393
|
+
async load() {
|
|
394
|
+
const readResult = await readCacheFile();
|
|
395
|
+
if (readResult.success === false) {
|
|
396
|
+
return readResult;
|
|
397
|
+
}
|
|
398
|
+
try {
|
|
399
|
+
const cache = JSON.parse(readResult.data);
|
|
400
|
+
const cacheVersion = cache.metadata.version;
|
|
401
|
+
if (cacheVersion !== CACHE_SCHEMA_VERSION) {
|
|
402
|
+
return err({
|
|
403
|
+
code: "CACHE_VERSION_MISMATCH",
|
|
404
|
+
message: `Cache version ${String(cacheVersion)} does not match current version ${String(CACHE_SCHEMA_VERSION)}`
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
const cacheAge = Date.now() - new Date(cache.metadata.createdAt).getTime();
|
|
408
|
+
if (cacheAge > maxAge) {
|
|
409
|
+
return err({
|
|
410
|
+
code: "CACHE_EXPIRED",
|
|
411
|
+
message: `Cache is ${Math.round(cacheAge / 1e3 / 60 / 60 / 24)} days old (max: ${Math.round(maxAge / 1e3 / 60 / 60 / 24)} days)`
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
return ok(cache);
|
|
415
|
+
} catch (error) {
|
|
416
|
+
return err({
|
|
417
|
+
code: "CACHE_CORRUPTED",
|
|
418
|
+
message: `Failed to parse cache file: ${error.message}`,
|
|
419
|
+
cause: error
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
},
|
|
423
|
+
async save(cache) {
|
|
424
|
+
const updatedCache = {
|
|
425
|
+
...cache,
|
|
426
|
+
metadata: {
|
|
427
|
+
...cache.metadata,
|
|
428
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
429
|
+
}
|
|
430
|
+
};
|
|
431
|
+
const content = JSON.stringify(updatedCache, null, compress ? 0 : 2);
|
|
432
|
+
return writeCacheFile(content);
|
|
433
|
+
},
|
|
434
|
+
async validate(cache, currentFiles) {
|
|
435
|
+
const configChanged = await changeDetector.hasConfigChanged(cache.configFiles);
|
|
436
|
+
if (configChanged) {
|
|
437
|
+
return {
|
|
438
|
+
isValid: false,
|
|
439
|
+
changedFiles: [],
|
|
440
|
+
newFiles: [],
|
|
441
|
+
deletedFiles: [],
|
|
442
|
+
invalidatedPackages: [],
|
|
443
|
+
changedConfigFiles: cache.configFiles.map((f) => f.path),
|
|
444
|
+
invalidationReason: "Configuration files changed"
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
const cachedFileStates = Object.values(cache.files).map((f) => f.fileState);
|
|
448
|
+
const validation = await changeDetector.validateCache(cachedFileStates, currentFiles);
|
|
449
|
+
const affectedPackages = /* @__PURE__ */ new Set();
|
|
450
|
+
const allChangedPaths = [
|
|
451
|
+
...validation.changedFiles,
|
|
452
|
+
...validation.newFiles,
|
|
453
|
+
...validation.deletedFiles
|
|
454
|
+
];
|
|
455
|
+
for (const changedPath of allChangedPaths) {
|
|
456
|
+
for (const [pkgName, pkg] of Object.entries(cache.packages)) {
|
|
457
|
+
const pkgFullPath = join(workspacePath, pkg.packagePath);
|
|
458
|
+
if (changedPath.startsWith(pkgFullPath)) {
|
|
459
|
+
affectedPackages.add(pkgName);
|
|
460
|
+
break;
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
return {
|
|
465
|
+
...validation,
|
|
466
|
+
invalidatedPackages: Array.from(affectedPackages)
|
|
467
|
+
};
|
|
468
|
+
},
|
|
469
|
+
async updateFile(cache, path, issues, analyzersRun) {
|
|
470
|
+
try {
|
|
471
|
+
const fileState = await getFileState(path);
|
|
472
|
+
const entry = createFileAnalysisEntry(
|
|
473
|
+
path,
|
|
474
|
+
fileState.contentHash,
|
|
475
|
+
new Date(fileState.modifiedAt),
|
|
476
|
+
fileState.size,
|
|
477
|
+
issues,
|
|
478
|
+
analyzersRun
|
|
479
|
+
);
|
|
480
|
+
return ok({
|
|
481
|
+
...cache,
|
|
482
|
+
files: {
|
|
483
|
+
...cache.files,
|
|
484
|
+
[path]: entry
|
|
485
|
+
},
|
|
486
|
+
metadata: {
|
|
487
|
+
...cache.metadata,
|
|
488
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
489
|
+
}
|
|
490
|
+
});
|
|
491
|
+
} catch (error) {
|
|
492
|
+
return err({
|
|
493
|
+
code: "CACHE_WRITE_FAILED",
|
|
494
|
+
message: `Failed to update cache for file ${path}: ${error.message}`,
|
|
495
|
+
cause: error
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
},
|
|
499
|
+
async updatePackage(cache, packageName, packagePath, issues, analyzersRun) {
|
|
500
|
+
try {
|
|
501
|
+
const packageJsonPath = join(workspacePath, packagePath, "package.json");
|
|
502
|
+
const packageJsonHash = await hasher.hash(packageJsonPath);
|
|
503
|
+
const entry = createPackageAnalysisEntry(
|
|
504
|
+
packageName,
|
|
505
|
+
packagePath,
|
|
506
|
+
packageJsonHash,
|
|
507
|
+
issues,
|
|
508
|
+
analyzersRun
|
|
509
|
+
);
|
|
510
|
+
return ok({
|
|
511
|
+
...cache,
|
|
512
|
+
packages: {
|
|
513
|
+
...cache.packages,
|
|
514
|
+
[packageName]: entry
|
|
515
|
+
},
|
|
516
|
+
metadata: {
|
|
517
|
+
...cache.metadata,
|
|
518
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
519
|
+
}
|
|
520
|
+
});
|
|
521
|
+
} catch (error) {
|
|
522
|
+
return err({
|
|
523
|
+
code: "CACHE_WRITE_FAILED",
|
|
524
|
+
message: `Failed to update cache for package ${packageName}: ${error.message}`,
|
|
525
|
+
cause: error
|
|
526
|
+
});
|
|
527
|
+
}
|
|
528
|
+
},
|
|
529
|
+
async clear() {
|
|
530
|
+
try {
|
|
531
|
+
await rm(cachePath, { recursive: true, force: true });
|
|
532
|
+
return ok(void 0);
|
|
533
|
+
} catch (error) {
|
|
534
|
+
return err({
|
|
535
|
+
code: "CACHE_WRITE_FAILED",
|
|
536
|
+
message: `Failed to clear cache: ${error.message}`,
|
|
537
|
+
cause: error
|
|
538
|
+
});
|
|
539
|
+
}
|
|
540
|
+
},
|
|
541
|
+
getStatistics(cache) {
|
|
542
|
+
const cachedFiles = Object.keys(cache.files).length;
|
|
543
|
+
const cachedPackages = Object.keys(cache.packages).length;
|
|
544
|
+
const ageMs = Date.now() - new Date(cache.metadata.createdAt).getTime();
|
|
545
|
+
const fileIssueCount = Object.values(cache.files).reduce((sum, f) => sum + f.issues.length, 0);
|
|
546
|
+
const packageIssueCount = Object.values(cache.packages).reduce(
|
|
547
|
+
(sum, p) => sum + p.issues.length,
|
|
548
|
+
0
|
|
549
|
+
);
|
|
550
|
+
const totalSizeBytes = (fileIssueCount + packageIssueCount) * 200 + cachedFiles * 100;
|
|
551
|
+
return {
|
|
552
|
+
cachedFiles,
|
|
553
|
+
cachedPackages,
|
|
554
|
+
totalSizeBytes,
|
|
555
|
+
ageMs,
|
|
556
|
+
hitCount: 0,
|
|
557
|
+
// Updated during analysis
|
|
558
|
+
missCount: 0,
|
|
559
|
+
// Updated during analysis
|
|
560
|
+
hitRate: 0
|
|
561
|
+
// Calculated: hitCount / (hitCount + missCount)
|
|
562
|
+
};
|
|
563
|
+
},
|
|
564
|
+
quickValidate(cache, configHash) {
|
|
565
|
+
if (cache.metadata.version !== CACHE_SCHEMA_VERSION) return false;
|
|
566
|
+
if (cache.metadata.workspacePath !== workspacePath) return false;
|
|
567
|
+
if (cache.metadata.analyzerVersion !== analyzerVersion) return false;
|
|
568
|
+
if (cache.metadata.configHash !== configHash) return false;
|
|
569
|
+
const cacheAge = Date.now() - new Date(cache.metadata.createdAt).getTime();
|
|
570
|
+
if (cacheAge > maxAge) return false;
|
|
571
|
+
return true;
|
|
572
|
+
}
|
|
573
|
+
};
|
|
574
|
+
}
|
|
575
|
+
function initializeCache(workspacePath, configHash, analyzerVersion) {
|
|
576
|
+
return createEmptyCache(workspacePath, configHash, analyzerVersion);
|
|
577
|
+
}
|
|
578
|
+
async function collectConfigFileStates(workspacePath, packagePaths) {
|
|
579
|
+
const hasher = createWorkspaceHasher();
|
|
580
|
+
const configFiles = [];
|
|
581
|
+
for (const pattern of CONFIG_FILE_PATTERNS) {
|
|
582
|
+
if (!pattern.includes("*")) {
|
|
583
|
+
const configPath = join(workspacePath, pattern);
|
|
584
|
+
try {
|
|
585
|
+
const stats = await stat(configPath);
|
|
586
|
+
const contentHash = await hasher.hash(configPath);
|
|
587
|
+
configFiles.push({
|
|
588
|
+
path: configPath,
|
|
589
|
+
contentHash,
|
|
590
|
+
modifiedAt: stats.mtime.toISOString(),
|
|
591
|
+
size: stats.size
|
|
592
|
+
});
|
|
593
|
+
} catch {
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
for (const pkgPath of packagePaths) {
|
|
598
|
+
const fullPkgPath = join(workspacePath, pkgPath);
|
|
599
|
+
for (const pattern of CONFIG_FILE_PATTERNS) {
|
|
600
|
+
if (!pattern.includes("*")) {
|
|
601
|
+
const configPath = join(fullPkgPath, pattern);
|
|
602
|
+
try {
|
|
603
|
+
const stats = await stat(configPath);
|
|
604
|
+
const contentHash = await hasher.hash(configPath);
|
|
605
|
+
configFiles.push({
|
|
606
|
+
path: configPath,
|
|
607
|
+
contentHash,
|
|
608
|
+
modifiedAt: stats.mtime.toISOString(),
|
|
609
|
+
size: stats.size
|
|
610
|
+
});
|
|
611
|
+
} catch {
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
return configFiles;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
// src/core/incremental-analyzer.ts
|
|
620
|
+
import process from "process";
|
|
621
|
+
import { pLimit } from "@bfra.me/es/async";
|
|
622
|
+
import { consola } from "consola";
|
|
623
|
+
var DEFAULT_INCREMENTAL_OPTIONS = {
|
|
624
|
+
useCache: true,
|
|
625
|
+
concurrency: 4,
|
|
626
|
+
maxCacheAge: 7 * 24 * 60 * 60 * 1e3,
|
|
627
|
+
// 7 days
|
|
628
|
+
minSeverity: "info",
|
|
629
|
+
cacheDir: ".workspace-analyzer-cache",
|
|
630
|
+
hashAlgorithm: "sha256"
|
|
631
|
+
};
|
|
632
|
+
function createIncrementalAnalyzer(options) {
|
|
633
|
+
const {
|
|
634
|
+
workspacePath,
|
|
635
|
+
analyzerVersion,
|
|
636
|
+
useCache = DEFAULT_INCREMENTAL_OPTIONS.useCache,
|
|
637
|
+
concurrency = DEFAULT_INCREMENTAL_OPTIONS.concurrency,
|
|
638
|
+
maxCacheAge = DEFAULT_INCREMENTAL_OPTIONS.maxCacheAge,
|
|
639
|
+
minSeverity = DEFAULT_INCREMENTAL_OPTIONS.minSeverity,
|
|
640
|
+
onProgress,
|
|
641
|
+
cacheDir = DEFAULT_INCREMENTAL_OPTIONS.cacheDir,
|
|
642
|
+
hashAlgorithm = DEFAULT_INCREMENTAL_OPTIONS.hashAlgorithm
|
|
643
|
+
} = options;
|
|
644
|
+
const hasher = createWorkspaceHasher({ algorithm: hashAlgorithm });
|
|
645
|
+
const cacheManager = createCacheManager({
|
|
646
|
+
workspacePath,
|
|
647
|
+
analyzerVersion,
|
|
648
|
+
cacheDir,
|
|
649
|
+
maxAge: maxCacheAge,
|
|
650
|
+
hashAlgorithm
|
|
651
|
+
});
|
|
652
|
+
const limit = pLimit(concurrency);
|
|
653
|
+
function reportProgress(phase, current, processed, total) {
|
|
654
|
+
onProgress?.({ phase, current, processed, total });
|
|
655
|
+
}
|
|
656
|
+
async function computeConfigHash(packagePaths) {
|
|
657
|
+
try {
|
|
658
|
+
const configFiles = await collectConfigFileStates(workspacePath, packagePaths);
|
|
659
|
+
const hashes = configFiles.map((f) => f.contentHash);
|
|
660
|
+
return hasher.hashContent(hashes.join(":"));
|
|
661
|
+
} catch {
|
|
662
|
+
return hasher.hashContent(Date.now().toString());
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
return {
|
|
666
|
+
async analyze(files, packages, analyzers) {
|
|
667
|
+
const startTime = Date.now();
|
|
668
|
+
let cache;
|
|
669
|
+
let validation;
|
|
670
|
+
let cacheUsed = false;
|
|
671
|
+
let hitCount = 0;
|
|
672
|
+
let missCount = 0;
|
|
673
|
+
const packagePaths = packages.map((p) => p.packagePath);
|
|
674
|
+
reportProgress("scanning", workspacePath, 0, files.length);
|
|
675
|
+
const configHash = await computeConfigHash(packagePaths);
|
|
676
|
+
if (useCache) {
|
|
677
|
+
const cacheResult = await cacheManager.load();
|
|
678
|
+
if (cacheResult.success && cacheManager.quickValidate(cacheResult.data, configHash)) {
|
|
679
|
+
cache = cacheResult.data;
|
|
680
|
+
validation = await cacheManager.validate(cache, files);
|
|
681
|
+
cacheUsed = true;
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
const filesToAnalyze = [];
|
|
685
|
+
const cachedFiles = [];
|
|
686
|
+
if (validation?.isValid && cache != null) {
|
|
687
|
+
for (const file of files) {
|
|
688
|
+
if (file in cache.files) {
|
|
689
|
+
cachedFiles.push(file);
|
|
690
|
+
hitCount++;
|
|
691
|
+
} else {
|
|
692
|
+
filesToAnalyze.push(file);
|
|
693
|
+
missCount++;
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
} else {
|
|
697
|
+
filesToAnalyze.push(...files);
|
|
698
|
+
missCount = files.length;
|
|
699
|
+
if (useCache) {
|
|
700
|
+
cache = initializeCache(workspacePath, configHash, analyzerVersion);
|
|
701
|
+
const configFiles = await collectConfigFileStates(workspacePath, packagePaths);
|
|
702
|
+
cache = { ...cache, configFiles };
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
reportProgress("parsing", "", 0, filesToAnalyze.length);
|
|
706
|
+
const allIssues = [];
|
|
707
|
+
if (cache != null) {
|
|
708
|
+
for (const file of cachedFiles) {
|
|
709
|
+
const cached = cache.files[file];
|
|
710
|
+
if (cached != null) {
|
|
711
|
+
allIssues.push(...cached.issues);
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
allIssues.push(...cache.workspaceIssues);
|
|
715
|
+
}
|
|
716
|
+
const analyzerContext = {
|
|
717
|
+
workspacePath,
|
|
718
|
+
packages,
|
|
719
|
+
config: {
|
|
720
|
+
minSeverity,
|
|
721
|
+
include: [],
|
|
722
|
+
exclude: []
|
|
723
|
+
},
|
|
724
|
+
reportProgress: (message) => {
|
|
725
|
+
reportProgress("analyzing", message, 0);
|
|
726
|
+
}
|
|
727
|
+
};
|
|
728
|
+
reportProgress("analyzing", "", 0, analyzers.length);
|
|
729
|
+
const analyzerResults = await Promise.all(
|
|
730
|
+
analyzers.map(
|
|
731
|
+
async (analyzer, index) => limit(async () => {
|
|
732
|
+
const analyzerId = analyzer.metadata.id;
|
|
733
|
+
reportProgress("analyzing", analyzerId, index + 1, analyzers.length);
|
|
734
|
+
try {
|
|
735
|
+
const result = await analyzer.analyze(analyzerContext);
|
|
736
|
+
return result;
|
|
737
|
+
} catch (error) {
|
|
738
|
+
const analyzerError = {
|
|
739
|
+
code: "ANALYZER_ERROR",
|
|
740
|
+
message: `Analyzer ${analyzerId} failed: ${error.message}`
|
|
741
|
+
};
|
|
742
|
+
return { success: false, error: analyzerError };
|
|
743
|
+
}
|
|
744
|
+
})
|
|
745
|
+
)
|
|
746
|
+
);
|
|
747
|
+
for (const result of analyzerResults) {
|
|
748
|
+
if (!result.success) {
|
|
749
|
+
consola.warn(`Analyzer error: ${result.error.message}`);
|
|
750
|
+
continue;
|
|
751
|
+
}
|
|
752
|
+
allIssues.push(...result.data);
|
|
753
|
+
}
|
|
754
|
+
const severityOrder = ["info", "warning", "error", "critical"];
|
|
755
|
+
const minSeverityIndex = severityOrder.indexOf(minSeverity);
|
|
756
|
+
const filteredIssues = allIssues.filter(
|
|
757
|
+
(issue) => severityOrder.indexOf(issue.severity) >= minSeverityIndex
|
|
758
|
+
);
|
|
759
|
+
if (useCache && cache != null) {
|
|
760
|
+
const issuesByFile = /* @__PURE__ */ new Map();
|
|
761
|
+
for (const issue of filteredIssues) {
|
|
762
|
+
const existing = issuesByFile.get(issue.location.filePath) ?? [];
|
|
763
|
+
existing.push(issue);
|
|
764
|
+
issuesByFile.set(issue.location.filePath, existing);
|
|
765
|
+
}
|
|
766
|
+
let updatedCache = cache;
|
|
767
|
+
for (const file of filesToAnalyze) {
|
|
768
|
+
const fileIssues = issuesByFile.get(file) ?? [];
|
|
769
|
+
const updateResult = await cacheManager.updateFile(
|
|
770
|
+
updatedCache,
|
|
771
|
+
file,
|
|
772
|
+
fileIssues,
|
|
773
|
+
analyzers.map((a) => a.metadata.id)
|
|
774
|
+
);
|
|
775
|
+
if (updateResult.success) {
|
|
776
|
+
updatedCache = updateResult.data;
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
reportProgress("reporting", "Saving cache", 0);
|
|
780
|
+
await cacheManager.save(updatedCache);
|
|
781
|
+
}
|
|
782
|
+
reportProgress("reporting", "", files.length, files.length);
|
|
783
|
+
const durationMs = Date.now() - startTime;
|
|
784
|
+
return ok({
|
|
785
|
+
issues: filteredIssues,
|
|
786
|
+
filesAnalyzed: filesToAnalyze.length,
|
|
787
|
+
filesFromCache: cachedFiles.length,
|
|
788
|
+
packagesAnalyzed: packagePaths.length,
|
|
789
|
+
durationMs,
|
|
790
|
+
cacheUsed,
|
|
791
|
+
cacheStats: cacheUsed ? {
|
|
792
|
+
hitRate: hitCount + missCount > 0 ? hitCount / (hitCount + missCount) : 0,
|
|
793
|
+
hitCount,
|
|
794
|
+
missCount
|
|
795
|
+
} : void 0
|
|
796
|
+
});
|
|
797
|
+
},
|
|
798
|
+
async invalidateFiles(paths) {
|
|
799
|
+
const cacheResult = await cacheManager.load();
|
|
800
|
+
if (!cacheResult.success) return;
|
|
801
|
+
const cache = cacheResult.data;
|
|
802
|
+
const updatedFiles = { ...cache.files };
|
|
803
|
+
for (const path of paths) {
|
|
804
|
+
delete updatedFiles[path];
|
|
805
|
+
}
|
|
806
|
+
await cacheManager.save({
|
|
807
|
+
...cache,
|
|
808
|
+
files: updatedFiles
|
|
809
|
+
});
|
|
810
|
+
},
|
|
811
|
+
async clearCache() {
|
|
812
|
+
const result = await cacheManager.clear();
|
|
813
|
+
if (!result.success) {
|
|
814
|
+
return err({
|
|
815
|
+
code: "CACHE_ERROR",
|
|
816
|
+
message: result.error.message
|
|
817
|
+
});
|
|
818
|
+
}
|
|
819
|
+
return ok(void 0);
|
|
820
|
+
},
|
|
821
|
+
async getCacheStats() {
|
|
822
|
+
const cacheResult = await cacheManager.load();
|
|
823
|
+
if (!cacheResult.success) {
|
|
824
|
+
if (cacheResult.error.code === "CACHE_NOT_FOUND") {
|
|
825
|
+
return ok({ cachedFiles: 0, cachedPackages: 0, ageMs: 0 });
|
|
826
|
+
}
|
|
827
|
+
return err({
|
|
828
|
+
code: "CACHE_ERROR",
|
|
829
|
+
message: cacheResult.error.message
|
|
830
|
+
});
|
|
831
|
+
}
|
|
832
|
+
const stats = cacheManager.getStatistics(cacheResult.data);
|
|
833
|
+
return ok({
|
|
834
|
+
cachedFiles: stats.cachedFiles,
|
|
835
|
+
cachedPackages: stats.cachedPackages,
|
|
836
|
+
ageMs: stats.ageMs
|
|
837
|
+
});
|
|
838
|
+
}
|
|
839
|
+
};
|
|
840
|
+
}
|
|
841
|
+
function createConsoleProgressCallback() {
|
|
842
|
+
let lastPhase;
|
|
843
|
+
return (progress) => {
|
|
844
|
+
if (progress.phase !== lastPhase) {
|
|
845
|
+
lastPhase = progress.phase;
|
|
846
|
+
const phaseNames = {
|
|
847
|
+
scanning: "Scanning workspace",
|
|
848
|
+
parsing: "Parsing source files",
|
|
849
|
+
analyzing: "Running analyzers",
|
|
850
|
+
reporting: "Generating report"
|
|
851
|
+
};
|
|
852
|
+
console.error(`
|
|
853
|
+
${phaseNames[progress.phase]}...`);
|
|
854
|
+
}
|
|
855
|
+
if (progress.total != null && progress.total > 0) {
|
|
856
|
+
const percent = Math.round(progress.processed / progress.total * 100);
|
|
857
|
+
process.stderr.write(`\r ${progress.processed}/${progress.total} (${percent}%)`);
|
|
858
|
+
}
|
|
859
|
+
};
|
|
860
|
+
}
|
|
861
|
+
function createSilentProgressCallback() {
|
|
862
|
+
return () => {
|
|
863
|
+
};
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
// src/performance/bundle-estimator.ts
|
|
867
|
+
import fs from "fs/promises";
|
|
868
|
+
var DEFAULT_OPTIONS = {
|
|
869
|
+
includeNodeModules: false,
|
|
870
|
+
maxFiles: 1e4,
|
|
871
|
+
minificationRatio: 0.6,
|
|
872
|
+
gzipRatio: 0.3,
|
|
873
|
+
largeFileThreshold: 5e4,
|
|
874
|
+
largeDependencyThreshold: 1e5
|
|
875
|
+
};
|
|
876
|
+
var KNOWN_PACKAGE_SIZES = {
|
|
877
|
+
lodash: 71,
|
|
878
|
+
"lodash-es": 71,
|
|
879
|
+
moment: 67,
|
|
880
|
+
"moment-timezone": 95,
|
|
881
|
+
rxjs: 40,
|
|
882
|
+
"@angular/core": 90,
|
|
883
|
+
"@angular/common": 45,
|
|
884
|
+
react: 6,
|
|
885
|
+
"react-dom": 42,
|
|
886
|
+
vue: 34,
|
|
887
|
+
d3: 80,
|
|
888
|
+
"chart.js": 65,
|
|
889
|
+
three: 150,
|
|
890
|
+
"@mui/material": 120,
|
|
891
|
+
antd: 200,
|
|
892
|
+
"date-fns": 25,
|
|
893
|
+
dayjs: 3,
|
|
894
|
+
axios: 13,
|
|
895
|
+
zod: 12,
|
|
896
|
+
yup: 22,
|
|
897
|
+
"class-validator": 15,
|
|
898
|
+
typeorm: 180,
|
|
899
|
+
prisma: 40,
|
|
900
|
+
"@prisma/client": 40,
|
|
901
|
+
express: 30,
|
|
902
|
+
fastify: 25,
|
|
903
|
+
"ts-morph": 150,
|
|
904
|
+
typescript: 150
|
|
905
|
+
};
|
|
906
|
+
async function estimatePackageBundleSize(pkg, importResults, options) {
|
|
907
|
+
const opts = { ...DEFAULT_OPTIONS, ...options };
|
|
908
|
+
const fileEstimates = [];
|
|
909
|
+
let totalSourceSize = 0;
|
|
910
|
+
let totalMinifiedSize = 0;
|
|
911
|
+
let totalGzippedSize = 0;
|
|
912
|
+
const filesToAnalyze = pkg.sourceFiles.slice(0, opts.maxFiles);
|
|
913
|
+
for (const filePath of filesToAnalyze) {
|
|
914
|
+
try {
|
|
915
|
+
const stats = await fs.stat(filePath);
|
|
916
|
+
const sourceSize = stats.size;
|
|
917
|
+
const fileImports = importResults.find((r) => r.filePath === filePath);
|
|
918
|
+
const importCount = fileImports?.imports.length ?? 0;
|
|
919
|
+
const estimate = createSizeEstimate(
|
|
920
|
+
filePath,
|
|
921
|
+
sourceSize,
|
|
922
|
+
importCount,
|
|
923
|
+
0,
|
|
924
|
+
false,
|
|
925
|
+
opts.minificationRatio,
|
|
926
|
+
opts.gzipRatio
|
|
927
|
+
);
|
|
928
|
+
fileEstimates.push(estimate);
|
|
929
|
+
totalSourceSize += sourceSize;
|
|
930
|
+
totalMinifiedSize += estimate.estimatedMinifiedSize;
|
|
931
|
+
totalGzippedSize += estimate.estimatedGzippedSize;
|
|
932
|
+
} catch {
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
const dependencyEstimates = collectDependencyEstimates(importResults, opts);
|
|
936
|
+
const topContributors = [...fileEstimates].sort((a, b) => b.sourceSize - a.sourceSize).slice(0, 10);
|
|
937
|
+
return {
|
|
938
|
+
packageName: pkg.name,
|
|
939
|
+
totalSourceSize,
|
|
940
|
+
totalMinifiedSize,
|
|
941
|
+
totalGzippedSize,
|
|
942
|
+
fileCount: fileEstimates.length,
|
|
943
|
+
files: fileEstimates,
|
|
944
|
+
dependencies: dependencyEstimates,
|
|
945
|
+
topContributors
|
|
946
|
+
};
|
|
947
|
+
}
|
|
948
|
+
async function estimateFileSize(filePath, options) {
|
|
949
|
+
const opts = { ...DEFAULT_OPTIONS, ...options };
|
|
950
|
+
try {
|
|
951
|
+
const stats = await fs.stat(filePath);
|
|
952
|
+
return createSizeEstimate(
|
|
953
|
+
filePath,
|
|
954
|
+
stats.size,
|
|
955
|
+
0,
|
|
956
|
+
0,
|
|
957
|
+
false,
|
|
958
|
+
opts.minificationRatio,
|
|
959
|
+
opts.gzipRatio
|
|
960
|
+
);
|
|
961
|
+
} catch {
|
|
962
|
+
return null;
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
function estimateDependencySize(packageName) {
|
|
966
|
+
const baseName = getBasePackageName(packageName);
|
|
967
|
+
const knownSize = KNOWN_PACKAGE_SIZES[baseName];
|
|
968
|
+
if (knownSize === void 0) {
|
|
969
|
+
return {
|
|
970
|
+
packageName,
|
|
971
|
+
estimatedSize: void 0,
|
|
972
|
+
sizeKnown: false,
|
|
973
|
+
importCount: 0,
|
|
974
|
+
locations: []
|
|
975
|
+
};
|
|
976
|
+
}
|
|
977
|
+
return {
|
|
978
|
+
packageName,
|
|
979
|
+
estimatedSize: knownSize * 1024,
|
|
980
|
+
sizeKnown: true,
|
|
981
|
+
importCount: 0,
|
|
982
|
+
locations: []
|
|
983
|
+
};
|
|
984
|
+
}
|
|
985
|
+
function findLargeFiles(stats, threshold) {
|
|
986
|
+
const actualThreshold = threshold ?? DEFAULT_OPTIONS.largeFileThreshold;
|
|
987
|
+
return stats.files.filter((file) => file.sourceSize > actualThreshold);
|
|
988
|
+
}
|
|
989
|
+
function findLargeDependencies(stats, threshold) {
|
|
990
|
+
const actualThreshold = threshold ?? DEFAULT_OPTIONS.largeDependencyThreshold;
|
|
991
|
+
return stats.dependencies.filter(
|
|
992
|
+
(dep) => dep.sizeKnown && dep.estimatedSize !== void 0 && dep.estimatedSize > actualThreshold
|
|
993
|
+
);
|
|
994
|
+
}
|
|
995
|
+
function estimateTreeShakingSavings(imports) {
|
|
996
|
+
let potentialSavings = 0;
|
|
997
|
+
const optimizableImports = [];
|
|
998
|
+
for (const imp of imports) {
|
|
999
|
+
if (imp.namespaceImport !== void 0) {
|
|
1000
|
+
const baseName = getBasePackageName(imp.moduleSpecifier);
|
|
1001
|
+
const knownSize = KNOWN_PACKAGE_SIZES[baseName];
|
|
1002
|
+
if (knownSize !== void 0) {
|
|
1003
|
+
const savings = Math.floor(knownSize * 1024 * 0.5);
|
|
1004
|
+
potentialSavings += savings;
|
|
1005
|
+
optimizableImports.push({
|
|
1006
|
+
moduleSpecifier: imp.moduleSpecifier,
|
|
1007
|
+
currentImportStyle: "namespace",
|
|
1008
|
+
suggestedImportStyle: "named",
|
|
1009
|
+
estimatedSavings: savings,
|
|
1010
|
+
line: imp.line,
|
|
1011
|
+
column: imp.column
|
|
1012
|
+
});
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
if (imp.defaultImport !== void 0 && imp.importedNames === void 0) {
|
|
1016
|
+
const baseName = getBasePackageName(imp.moduleSpecifier);
|
|
1017
|
+
const knownSize = KNOWN_PACKAGE_SIZES[baseName];
|
|
1018
|
+
if (knownSize !== void 0 && !imp.isRelative) {
|
|
1019
|
+
const savings = Math.floor(knownSize * 1024 * 0.3);
|
|
1020
|
+
potentialSavings += savings;
|
|
1021
|
+
optimizableImports.push({
|
|
1022
|
+
moduleSpecifier: imp.moduleSpecifier,
|
|
1023
|
+
currentImportStyle: "default",
|
|
1024
|
+
suggestedImportStyle: "named",
|
|
1025
|
+
estimatedSavings: savings,
|
|
1026
|
+
line: imp.line,
|
|
1027
|
+
column: imp.column
|
|
1028
|
+
});
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
return {
|
|
1033
|
+
potentialSavings,
|
|
1034
|
+
optimizableImports,
|
|
1035
|
+
hasPotentialOptimizations: optimizableImports.length > 0
|
|
1036
|
+
};
|
|
1037
|
+
}
|
|
1038
|
+
function createSizeEstimate(identifier, sourceSize, importCount, exportCount, isTransitive, minificationRatio, gzipRatio) {
|
|
1039
|
+
const estimatedMinifiedSize = Math.floor(sourceSize * minificationRatio);
|
|
1040
|
+
const estimatedGzippedSize = Math.floor(estimatedMinifiedSize * gzipRatio);
|
|
1041
|
+
return {
|
|
1042
|
+
identifier,
|
|
1043
|
+
sourceSize,
|
|
1044
|
+
estimatedMinifiedSize,
|
|
1045
|
+
estimatedGzippedSize,
|
|
1046
|
+
importCount,
|
|
1047
|
+
exportCount,
|
|
1048
|
+
isTransitive
|
|
1049
|
+
};
|
|
1050
|
+
}
|
|
1051
|
+
function collectDependencyEstimates(importResults, _options) {
|
|
1052
|
+
const depMap = /* @__PURE__ */ new Map();
|
|
1053
|
+
for (const result of importResults) {
|
|
1054
|
+
for (const dep of result.externalDependencies) {
|
|
1055
|
+
const baseName = getBasePackageName(dep);
|
|
1056
|
+
const existing = depMap.get(baseName);
|
|
1057
|
+
if (existing === void 0) {
|
|
1058
|
+
const knownSize = KNOWN_PACKAGE_SIZES[baseName];
|
|
1059
|
+
const sizeKnown = knownSize !== void 0;
|
|
1060
|
+
depMap.set(baseName, {
|
|
1061
|
+
estimatedSize: sizeKnown ? knownSize * 1024 : void 0,
|
|
1062
|
+
sizeKnown,
|
|
1063
|
+
locations: [result.filePath]
|
|
1064
|
+
});
|
|
1065
|
+
} else {
|
|
1066
|
+
existing.locations.push(result.filePath);
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
return Array.from(depMap.entries()).map(([packageName, data]) => ({
|
|
1071
|
+
packageName,
|
|
1072
|
+
estimatedSize: data.estimatedSize,
|
|
1073
|
+
sizeKnown: data.sizeKnown,
|
|
1074
|
+
importCount: data.locations.length,
|
|
1075
|
+
locations: data.locations
|
|
1076
|
+
}));
|
|
1077
|
+
}
|
|
1078
|
+
function getBasePackageName(specifier) {
|
|
1079
|
+
if (specifier.startsWith("@")) {
|
|
1080
|
+
const parts = specifier.split("/");
|
|
1081
|
+
if (parts.length >= 2) {
|
|
1082
|
+
return `${parts[0]}/${parts[1]}`;
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
return specifier.split("/")[0] ?? specifier;
|
|
1086
|
+
}
|
|
1087
|
+
function formatBytes(bytes) {
|
|
1088
|
+
if (bytes === 0) return "0 B";
|
|
1089
|
+
const units = ["B", "KB", "MB", "GB"];
|
|
1090
|
+
const k = 1024;
|
|
1091
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
1092
|
+
const size = bytes / k ** i;
|
|
1093
|
+
return `${size.toFixed(i === 0 ? 0 : 1)} ${units[i]}`;
|
|
1094
|
+
}
|
|
1095
|
+
export {
|
|
1096
|
+
BUILTIN_ANALYZER_IDS,
|
|
1097
|
+
BUILTIN_RULE_IDS,
|
|
1098
|
+
CACHE_SCHEMA_VERSION,
|
|
1099
|
+
CATEGORY_CONFIG,
|
|
1100
|
+
CONFIG_FILE_PATTERNS,
|
|
1101
|
+
DEFAULT_ANALYZER_CONFIG,
|
|
1102
|
+
DEFAULT_CACHE_OPTIONS,
|
|
1103
|
+
DEFAULT_CONCURRENCY,
|
|
1104
|
+
DEFAULT_EXCLUDE_PATTERNS,
|
|
1105
|
+
DEFAULT_INCLUDE_PATTERNS,
|
|
1106
|
+
DEFAULT_INCREMENTAL_OPTIONS,
|
|
1107
|
+
DEFAULT_LAYER_CONFIG,
|
|
1108
|
+
DEFAULT_PACKAGE_PATTERNS,
|
|
1109
|
+
DEFAULT_REPORT_OPTIONS,
|
|
1110
|
+
DEFAULT_WORKSPACE_OPTIONS,
|
|
1111
|
+
SEVERITY_CONFIG,
|
|
1112
|
+
aggregatePackageImports,
|
|
1113
|
+
analyzePackages,
|
|
1114
|
+
analyzeWorkspace,
|
|
1115
|
+
architecturalAnalyzerMetadata,
|
|
1116
|
+
barrelExportRuleMetadata,
|
|
1117
|
+
METADATA as buildConfigAnalyzerMetadata,
|
|
1118
|
+
buildDependencyGraph,
|
|
1119
|
+
builtinAnalyzers,
|
|
1120
|
+
calculateSummary,
|
|
1121
|
+
circularImportAnalyzerMetadata,
|
|
1122
|
+
collectConfigFileStates,
|
|
1123
|
+
computeCycleStats,
|
|
1124
|
+
computeDeadCodeStats,
|
|
1125
|
+
computeDuplicateStats,
|
|
1126
|
+
computeGraphStatistics,
|
|
1127
|
+
METADATA2 as configConsistencyAnalyzerMetadata,
|
|
1128
|
+
createAnalysisChangeDetector,
|
|
1129
|
+
createAnalyzerRegistry,
|
|
1130
|
+
createArchitecturalAnalyzer,
|
|
1131
|
+
createBarrelExportRule,
|
|
1132
|
+
createBuildConfigAnalyzer,
|
|
1133
|
+
createCacheManager,
|
|
1134
|
+
createChangeDetector,
|
|
1135
|
+
createCircularImportAnalyzer,
|
|
1136
|
+
createConfigConsistencyAnalyzer,
|
|
1137
|
+
createConsoleProgressCallback,
|
|
1138
|
+
createConsoleReporter,
|
|
1139
|
+
createDeadCodeAnalyzer,
|
|
1140
|
+
createDefaultRegistry,
|
|
1141
|
+
createDuplicateCodeAnalyzer,
|
|
1142
|
+
createDuplicateDependencyAnalyzer,
|
|
1143
|
+
createEmptyCache,
|
|
1144
|
+
createEslintConfigAnalyzer,
|
|
1145
|
+
createExportsFieldAnalyzer,
|
|
1146
|
+
createFileAnalysisEntry,
|
|
1147
|
+
createFileHasher,
|
|
1148
|
+
createIncrementalAnalyzer,
|
|
1149
|
+
createIssue,
|
|
1150
|
+
createJsonReporter,
|
|
1151
|
+
createLargeDependencyAnalyzer,
|
|
1152
|
+
createLayerViolationRule,
|
|
1153
|
+
createMarkdownReporter,
|
|
1154
|
+
createOrchestrator,
|
|
1155
|
+
createPackageAnalysisEntry,
|
|
1156
|
+
createPackageBoundaryRule,
|
|
1157
|
+
createPackageJsonAnalyzer,
|
|
1158
|
+
createPathAliasRule,
|
|
1159
|
+
createPeerDependencyAnalyzer,
|
|
1160
|
+
createProject,
|
|
1161
|
+
createPublicApiRule,
|
|
1162
|
+
createRuleEngine,
|
|
1163
|
+
createSideEffectRule,
|
|
1164
|
+
createSilentProgressCallback,
|
|
1165
|
+
createTreeShakingBlockerAnalyzer,
|
|
1166
|
+
createTsconfigAnalyzer,
|
|
1167
|
+
createUnusedDependencyAnalyzer,
|
|
1168
|
+
createVersionAlignmentAnalyzer,
|
|
1169
|
+
createWorkspaceHasher,
|
|
1170
|
+
createWorkspaceScanner,
|
|
1171
|
+
deadCodeAnalyzerMetadata,
|
|
1172
|
+
defineConfig,
|
|
1173
|
+
duplicateCodeAnalyzerMetadata,
|
|
1174
|
+
duplicateDependencyAnalyzerMetadata,
|
|
1175
|
+
err,
|
|
1176
|
+
METADATA3 as eslintConfigAnalyzerMetadata,
|
|
1177
|
+
estimateDependencySize,
|
|
1178
|
+
estimateFileSize,
|
|
1179
|
+
estimatePackageBundleSize,
|
|
1180
|
+
estimateTreeShakingSavings,
|
|
1181
|
+
METADATA4 as exportsFieldAnalyzerMetadata,
|
|
1182
|
+
extractImports,
|
|
1183
|
+
filterIssues,
|
|
1184
|
+
filterIssuesForReport,
|
|
1185
|
+
filterPackagesByPattern,
|
|
1186
|
+
findConfigFile,
|
|
1187
|
+
findCycles,
|
|
1188
|
+
findLargeDependencies,
|
|
1189
|
+
findLargeFiles,
|
|
1190
|
+
flatMap,
|
|
1191
|
+
formatBytes,
|
|
1192
|
+
formatDuration,
|
|
1193
|
+
formatLocation,
|
|
1194
|
+
fromPromise,
|
|
1195
|
+
fromThrowable,
|
|
1196
|
+
generateCycleVisualization,
|
|
1197
|
+
getAllDependencies,
|
|
1198
|
+
getAllSourceFiles,
|
|
1199
|
+
getAnalyzerOptions,
|
|
1200
|
+
getDefaultConfig,
|
|
1201
|
+
getFileLayer,
|
|
1202
|
+
getKnownLargePackages,
|
|
1203
|
+
getPackageInfo,
|
|
1204
|
+
getPackageNameFromSpecifier,
|
|
1205
|
+
getPackageScope,
|
|
1206
|
+
getRelativePath,
|
|
1207
|
+
getSourceFile,
|
|
1208
|
+
getTransitiveDependencies,
|
|
1209
|
+
getTransitiveDependents,
|
|
1210
|
+
getUniqueDependencies,
|
|
1211
|
+
getUnscopedName,
|
|
1212
|
+
groupIssues,
|
|
1213
|
+
groupPackagesByScope,
|
|
1214
|
+
initializeCache,
|
|
1215
|
+
isErr,
|
|
1216
|
+
isJavaScriptFile,
|
|
1217
|
+
isLayerImportAllowed,
|
|
1218
|
+
isOk,
|
|
1219
|
+
isRelativeImport,
|
|
1220
|
+
isSourceFile,
|
|
1221
|
+
isTypeScriptFile,
|
|
1222
|
+
isWorkspacePackageImport,
|
|
1223
|
+
largeDependencyAnalyzerMetadata,
|
|
1224
|
+
layerViolationRuleMetadata,
|
|
1225
|
+
loadConfig,
|
|
1226
|
+
loadConfigFile,
|
|
1227
|
+
map,
|
|
1228
|
+
mapErr,
|
|
1229
|
+
matchAnyPattern,
|
|
1230
|
+
matchPattern,
|
|
1231
|
+
meetsMinSeverity,
|
|
1232
|
+
mergeAnalyzerConfigs,
|
|
1233
|
+
mergeConfig,
|
|
1234
|
+
normalizePath,
|
|
1235
|
+
ok,
|
|
1236
|
+
packageBoundaryRuleMetadata,
|
|
1237
|
+
METADATA5 as packageJsonAnalyzerMetadata,
|
|
1238
|
+
parsePackageJson,
|
|
1239
|
+
parsePackageJsonContent,
|
|
1240
|
+
parseSourceContent,
|
|
1241
|
+
parseSourceFile,
|
|
1242
|
+
parseSourceFiles,
|
|
1243
|
+
parseTsConfig,
|
|
1244
|
+
parseTsConfigContent,
|
|
1245
|
+
parseWorkspaceAnalyzerConfig,
|
|
1246
|
+
pathAliasRuleMetadata,
|
|
1247
|
+
peerDependencyAnalyzerMetadata,
|
|
1248
|
+
publicApiRuleMetadata,
|
|
1249
|
+
resolveRelativeImport,
|
|
1250
|
+
resolveTsConfigExtends,
|
|
1251
|
+
safeParseAnalyzeOptions,
|
|
1252
|
+
shouldAnalyzeCategory,
|
|
1253
|
+
sideEffectRuleMetadata,
|
|
1254
|
+
treeShakingBlockerAnalyzerMetadata,
|
|
1255
|
+
truncateText,
|
|
1256
|
+
METADATA6 as tsconfigAnalyzerMetadata,
|
|
1257
|
+
unusedDependencyAnalyzerMetadata,
|
|
1258
|
+
unwrap,
|
|
1259
|
+
unwrapOr,
|
|
1260
|
+
METADATA7 as versionAlignmentAnalyzerMetadata
|
|
1261
|
+
};
|
|
1262
|
+
//# sourceMappingURL=index.js.map
|