@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
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workspace scanner for discovering and analyzing packages in a monorepo.
|
|
3
|
+
*
|
|
4
|
+
* Adapts the createPackageScanner() pattern from @bfra.me/doc-sync for workspace analysis.
|
|
5
|
+
* Uses fs.readdir pattern (not fast-glob) for better control over directory traversal.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type {Result} from '../types/result'
|
|
9
|
+
|
|
10
|
+
import fs from 'node:fs/promises'
|
|
11
|
+
import path from 'node:path'
|
|
12
|
+
|
|
13
|
+
import {err, ok} from '@bfra.me/es/result'
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Options for configuring the workspace scanner.
|
|
17
|
+
*/
|
|
18
|
+
export interface WorkspaceScannerOptions {
|
|
19
|
+
/** Root directory of the workspace/monorepo */
|
|
20
|
+
readonly rootDir: string
|
|
21
|
+
/** Glob patterns for package locations (e.g., ['packages/*', 'apps/*']) */
|
|
22
|
+
readonly includePatterns?: readonly string[]
|
|
23
|
+
/** Package names to exclude from scanning */
|
|
24
|
+
readonly excludePackages?: readonly string[]
|
|
25
|
+
/** File extensions to include in source file collection */
|
|
26
|
+
readonly sourceExtensions?: readonly string[]
|
|
27
|
+
/** Directories to skip during source file collection */
|
|
28
|
+
readonly excludeDirs?: readonly string[]
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Minimal package.json structure for workspace analysis.
|
|
33
|
+
*/
|
|
34
|
+
export interface WorkspacePackageJson {
|
|
35
|
+
readonly name: string
|
|
36
|
+
readonly version: string
|
|
37
|
+
readonly description?: string
|
|
38
|
+
readonly main?: string
|
|
39
|
+
readonly module?: string
|
|
40
|
+
readonly types?: string
|
|
41
|
+
readonly exports?: Record<string, unknown>
|
|
42
|
+
readonly dependencies?: Readonly<Record<string, string>>
|
|
43
|
+
readonly devDependencies?: Readonly<Record<string, string>>
|
|
44
|
+
readonly peerDependencies?: Readonly<Record<string, string>>
|
|
45
|
+
readonly optionalDependencies?: Readonly<Record<string, string>>
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Information about a discovered workspace package.
|
|
50
|
+
*/
|
|
51
|
+
export interface WorkspacePackage {
|
|
52
|
+
/** Package name from package.json */
|
|
53
|
+
readonly name: string
|
|
54
|
+
/** Package version from package.json */
|
|
55
|
+
readonly version: string
|
|
56
|
+
/** Absolute path to the package directory */
|
|
57
|
+
readonly packagePath: string
|
|
58
|
+
/** Absolute path to package.json */
|
|
59
|
+
readonly packageJsonPath: string
|
|
60
|
+
/** Absolute path to source directory (if exists) */
|
|
61
|
+
readonly srcPath: string
|
|
62
|
+
/** Parsed package.json content */
|
|
63
|
+
readonly packageJson: WorkspacePackageJson
|
|
64
|
+
/** List of source file paths in the package */
|
|
65
|
+
readonly sourceFiles: readonly string[]
|
|
66
|
+
/** Whether the package has a tsconfig.json */
|
|
67
|
+
readonly hasTsConfig: boolean
|
|
68
|
+
/** Whether the package has an eslint config */
|
|
69
|
+
readonly hasEslintConfig: boolean
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Error that occurred during workspace scanning.
|
|
74
|
+
*/
|
|
75
|
+
export interface ScanError {
|
|
76
|
+
/** Error code for programmatic handling */
|
|
77
|
+
readonly code: 'INVALID_PATH' | 'NO_PACKAGE_JSON' | 'INVALID_PACKAGE_JSON' | 'READ_ERROR'
|
|
78
|
+
/** Human-readable error message */
|
|
79
|
+
readonly message: string
|
|
80
|
+
/** Path where the error occurred */
|
|
81
|
+
readonly path: string
|
|
82
|
+
/** Underlying error cause */
|
|
83
|
+
readonly cause?: unknown
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Result of a workspace scan operation.
|
|
88
|
+
*/
|
|
89
|
+
export interface WorkspaceScanResult {
|
|
90
|
+
/** All discovered packages */
|
|
91
|
+
readonly packages: readonly WorkspacePackage[]
|
|
92
|
+
/** Root workspace path */
|
|
93
|
+
readonly workspacePath: string
|
|
94
|
+
/** Errors encountered during scanning */
|
|
95
|
+
readonly errors: readonly ScanError[]
|
|
96
|
+
/** Duration of scan in milliseconds */
|
|
97
|
+
readonly durationMs: number
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const DEFAULT_OPTIONS = {
|
|
101
|
+
includePatterns: ['packages/*'],
|
|
102
|
+
excludePackages: [],
|
|
103
|
+
sourceExtensions: ['.ts', '.tsx', '.js', '.jsx', '.mts', '.cts', '.mjs', '.cjs'],
|
|
104
|
+
excludeDirs: ['node_modules', '__tests__', '__mocks__', 'test', 'tests', 'dist', 'lib', 'build'],
|
|
105
|
+
} as const
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Check if a file exists at the given path.
|
|
109
|
+
*/
|
|
110
|
+
async function fileExists(filePath: string): Promise<boolean> {
|
|
111
|
+
try {
|
|
112
|
+
await fs.access(filePath)
|
|
113
|
+
return true
|
|
114
|
+
} catch {
|
|
115
|
+
return false
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Creates a workspace scanner for discovering and analyzing packages.
|
|
121
|
+
*
|
|
122
|
+
* @example
|
|
123
|
+
* ```ts
|
|
124
|
+
* const scanner = createWorkspaceScanner({rootDir: '/path/to/monorepo'})
|
|
125
|
+
* const result = await scanner.scan()
|
|
126
|
+
*
|
|
127
|
+
* for (const pkg of result.packages) {
|
|
128
|
+
* console.log(`Found package: ${pkg.name}`)
|
|
129
|
+
* }
|
|
130
|
+
* ```
|
|
131
|
+
*/
|
|
132
|
+
export function createWorkspaceScanner(options: WorkspaceScannerOptions): {
|
|
133
|
+
/** Scan the entire workspace for packages */
|
|
134
|
+
readonly scan: () => Promise<WorkspaceScanResult>
|
|
135
|
+
/** Scan a single package directory */
|
|
136
|
+
readonly scanPackage: (packagePath: string) => Promise<Result<WorkspacePackage, ScanError>>
|
|
137
|
+
/** Collect source files from a directory */
|
|
138
|
+
readonly collectSourceFiles: (dir: string) => Promise<readonly string[]>
|
|
139
|
+
} {
|
|
140
|
+
const {
|
|
141
|
+
rootDir,
|
|
142
|
+
includePatterns = DEFAULT_OPTIONS.includePatterns,
|
|
143
|
+
excludePackages = DEFAULT_OPTIONS.excludePackages,
|
|
144
|
+
sourceExtensions = DEFAULT_OPTIONS.sourceExtensions,
|
|
145
|
+
excludeDirs = DEFAULT_OPTIONS.excludeDirs,
|
|
146
|
+
} = options
|
|
147
|
+
|
|
148
|
+
const extensionSet = new Set(sourceExtensions)
|
|
149
|
+
const excludeDirSet = new Set(excludeDirs)
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Discover package directories based on include patterns.
|
|
153
|
+
* Uses fs.readdir pattern for consistent behavior with doc-sync.
|
|
154
|
+
*/
|
|
155
|
+
async function discoverPackages(): Promise<string[]> {
|
|
156
|
+
const packagePaths: string[] = []
|
|
157
|
+
|
|
158
|
+
for (const pattern of includePatterns) {
|
|
159
|
+
const baseDir = path.join(rootDir, pattern.replace('/*', ''))
|
|
160
|
+
|
|
161
|
+
try {
|
|
162
|
+
const entries = await fs.readdir(baseDir, {withFileTypes: true})
|
|
163
|
+
|
|
164
|
+
for (const entry of entries) {
|
|
165
|
+
if (!entry.isDirectory()) {
|
|
166
|
+
continue
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const packagePath = path.join(baseDir, entry.name)
|
|
170
|
+
const packageJsonPath = path.join(packagePath, 'package.json')
|
|
171
|
+
|
|
172
|
+
try {
|
|
173
|
+
await fs.access(packageJsonPath)
|
|
174
|
+
packagePaths.push(packagePath)
|
|
175
|
+
} catch {
|
|
176
|
+
// Directory doesn't contain package.json, skip
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
} catch {
|
|
180
|
+
// Pattern directory doesn't exist, skip
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return packagePaths
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Recursively collect source files from a directory.
|
|
189
|
+
*/
|
|
190
|
+
async function collectSourceFiles(dir: string): Promise<readonly string[]> {
|
|
191
|
+
const files: string[] = []
|
|
192
|
+
|
|
193
|
+
try {
|
|
194
|
+
await collectSourceFilesRecursive(dir, files)
|
|
195
|
+
} catch {
|
|
196
|
+
// Source directory doesn't exist or is not accessible
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return files
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
async function collectSourceFilesRecursive(dir: string, files: string[]): Promise<void> {
|
|
203
|
+
const entries = await fs.readdir(dir, {withFileTypes: true})
|
|
204
|
+
|
|
205
|
+
for (const entry of entries) {
|
|
206
|
+
const fullPath = path.join(dir, entry.name)
|
|
207
|
+
|
|
208
|
+
if (entry.isDirectory()) {
|
|
209
|
+
if (excludeDirSet.has(entry.name)) {
|
|
210
|
+
continue
|
|
211
|
+
}
|
|
212
|
+
await collectSourceFilesRecursive(fullPath, files)
|
|
213
|
+
} else if (entry.isFile()) {
|
|
214
|
+
const ext = path.extname(entry.name).toLowerCase()
|
|
215
|
+
const isSourceFile = extensionSet.has(ext)
|
|
216
|
+
const isTestFile = entry.name.includes('.test.') || entry.name.includes('.spec.')
|
|
217
|
+
|
|
218
|
+
if (isSourceFile && !isTestFile) {
|
|
219
|
+
files.push(fullPath)
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Scan a single package directory.
|
|
227
|
+
*/
|
|
228
|
+
async function scanPackage(packagePath: string): Promise<Result<WorkspacePackage, ScanError>> {
|
|
229
|
+
const packageJsonPath = path.join(packagePath, 'package.json')
|
|
230
|
+
|
|
231
|
+
let content: string
|
|
232
|
+
try {
|
|
233
|
+
content = await fs.readFile(packageJsonPath, 'utf-8')
|
|
234
|
+
} catch (error) {
|
|
235
|
+
return err({
|
|
236
|
+
code: 'READ_ERROR',
|
|
237
|
+
message: `Failed to read package.json: ${packageJsonPath}`,
|
|
238
|
+
path: packageJsonPath,
|
|
239
|
+
cause: error,
|
|
240
|
+
})
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
let packageJson: unknown
|
|
244
|
+
try {
|
|
245
|
+
packageJson = JSON.parse(content)
|
|
246
|
+
} catch (error) {
|
|
247
|
+
return err({
|
|
248
|
+
code: 'INVALID_PACKAGE_JSON',
|
|
249
|
+
message: `Invalid JSON in package.json: ${packageJsonPath}`,
|
|
250
|
+
path: packageJsonPath,
|
|
251
|
+
cause: error,
|
|
252
|
+
})
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (!isValidPackageJson(packageJson)) {
|
|
256
|
+
return err({
|
|
257
|
+
code: 'INVALID_PACKAGE_JSON',
|
|
258
|
+
message: 'package.json is missing required fields (name, version)',
|
|
259
|
+
path: packageJsonPath,
|
|
260
|
+
})
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const srcPath = path.join(packagePath, 'src')
|
|
264
|
+
const sourceFiles = await collectSourceFiles(srcPath)
|
|
265
|
+
|
|
266
|
+
const [hasTsConfig, hasEslintTs, hasEslintJs] = await Promise.all([
|
|
267
|
+
fileExists(path.join(packagePath, 'tsconfig.json')),
|
|
268
|
+
fileExists(path.join(packagePath, 'eslint.config.ts')),
|
|
269
|
+
fileExists(path.join(packagePath, 'eslint.config.js')),
|
|
270
|
+
])
|
|
271
|
+
|
|
272
|
+
return ok({
|
|
273
|
+
name: packageJson.name,
|
|
274
|
+
version: packageJson.version,
|
|
275
|
+
packagePath,
|
|
276
|
+
packageJsonPath,
|
|
277
|
+
srcPath,
|
|
278
|
+
packageJson,
|
|
279
|
+
sourceFiles,
|
|
280
|
+
hasTsConfig,
|
|
281
|
+
hasEslintConfig: hasEslintTs === true || hasEslintJs === true,
|
|
282
|
+
})
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Scan the entire workspace for packages.
|
|
287
|
+
*/
|
|
288
|
+
async function scan(): Promise<WorkspaceScanResult> {
|
|
289
|
+
const startTime = Date.now()
|
|
290
|
+
const packagePaths = await discoverPackages()
|
|
291
|
+
const packages: WorkspacePackage[] = []
|
|
292
|
+
const errors: ScanError[] = []
|
|
293
|
+
|
|
294
|
+
for (const packagePath of packagePaths) {
|
|
295
|
+
const result = await scanPackage(packagePath)
|
|
296
|
+
|
|
297
|
+
if (result.success) {
|
|
298
|
+
const scanned = result.data
|
|
299
|
+
|
|
300
|
+
if (excludePackages.includes(scanned.name)) {
|
|
301
|
+
continue
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
packages.push(scanned)
|
|
305
|
+
} else {
|
|
306
|
+
errors.push(result.error)
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
return {
|
|
311
|
+
packages,
|
|
312
|
+
workspacePath: rootDir,
|
|
313
|
+
errors,
|
|
314
|
+
durationMs: Date.now() - startTime,
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
return {
|
|
319
|
+
scan,
|
|
320
|
+
scanPackage,
|
|
321
|
+
collectSourceFiles,
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Type guard for validating package.json structure.
|
|
327
|
+
*/
|
|
328
|
+
function isValidPackageJson(value: unknown): value is WorkspacePackageJson {
|
|
329
|
+
if (typeof value !== 'object' || value === null) {
|
|
330
|
+
return false
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const obj = value as Record<string, unknown>
|
|
334
|
+
|
|
335
|
+
if (typeof obj.name !== 'string' || obj.name.length === 0) {
|
|
336
|
+
return false
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
if (typeof obj.version !== 'string' || obj.version.length === 0) {
|
|
340
|
+
return false
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
return true
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Filter packages by name pattern.
|
|
348
|
+
*/
|
|
349
|
+
export function filterPackagesByPattern(
|
|
350
|
+
packages: readonly WorkspacePackage[],
|
|
351
|
+
pattern: string,
|
|
352
|
+
): WorkspacePackage[] {
|
|
353
|
+
const regex = new RegExp(pattern.replaceAll('*', '.*'), 'i')
|
|
354
|
+
return packages.filter(pkg => regex.test(pkg.name))
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Group packages by their npm scope.
|
|
359
|
+
*/
|
|
360
|
+
export function groupPackagesByScope(
|
|
361
|
+
packages: readonly WorkspacePackage[],
|
|
362
|
+
): Map<string, WorkspacePackage[]> {
|
|
363
|
+
const grouped = new Map<string, WorkspacePackage[]>()
|
|
364
|
+
|
|
365
|
+
for (const pkg of packages) {
|
|
366
|
+
const scope = getPackageScope(pkg.name) ?? '__unscoped__'
|
|
367
|
+
const existing = grouped.get(scope)
|
|
368
|
+
|
|
369
|
+
if (existing === undefined) {
|
|
370
|
+
grouped.set(scope, [pkg])
|
|
371
|
+
} else {
|
|
372
|
+
existing.push(pkg)
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
return grouped
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Extract the scope from a scoped package name.
|
|
381
|
+
*/
|
|
382
|
+
export function getPackageScope(packageName: string): string | undefined {
|
|
383
|
+
if (packageName.startsWith('@')) {
|
|
384
|
+
const slashIndex = packageName.indexOf('/')
|
|
385
|
+
if (slashIndex > 0) {
|
|
386
|
+
return packageName.slice(0, slashIndex)
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
return undefined
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* Get the unscoped name from a package name.
|
|
394
|
+
*/
|
|
395
|
+
export function getUnscopedName(packageName: string): string {
|
|
396
|
+
if (packageName.startsWith('@')) {
|
|
397
|
+
const slashIndex = packageName.indexOf('/')
|
|
398
|
+
if (slashIndex > 0) {
|
|
399
|
+
return packageName.slice(slashIndex + 1)
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
return packageName
|
|
403
|
+
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core type definitions for workspace analysis.
|
|
3
|
+
*
|
|
4
|
+
* These types define the structure of analysis results, issues, severity levels,
|
|
5
|
+
* and configuration options used throughout the analyzer.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type {Result} from './result'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Severity levels for analysis issues, from informational to critical errors.
|
|
12
|
+
*/
|
|
13
|
+
export type Severity = 'info' | 'warning' | 'error' | 'critical'
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Categories of analysis issues for grouping and filtering.
|
|
17
|
+
*/
|
|
18
|
+
export type IssueCategory =
|
|
19
|
+
| 'configuration'
|
|
20
|
+
| 'dependency'
|
|
21
|
+
| 'architecture'
|
|
22
|
+
| 'performance'
|
|
23
|
+
| 'circular-import'
|
|
24
|
+
| 'unused-export'
|
|
25
|
+
| 'type-safety'
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Location information for precise issue reporting.
|
|
29
|
+
*/
|
|
30
|
+
export interface IssueLocation {
|
|
31
|
+
/** Absolute file path where the issue was detected */
|
|
32
|
+
readonly filePath: string
|
|
33
|
+
/** Starting line number (1-indexed) */
|
|
34
|
+
readonly line?: number
|
|
35
|
+
/** Starting column number (1-indexed) */
|
|
36
|
+
readonly column?: number
|
|
37
|
+
/** Ending line number (1-indexed) */
|
|
38
|
+
readonly endLine?: number
|
|
39
|
+
/** Ending column number (1-indexed) */
|
|
40
|
+
readonly endColumn?: number
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Represents a single issue detected during analysis.
|
|
45
|
+
*/
|
|
46
|
+
export interface Issue {
|
|
47
|
+
/** Unique identifier for the issue type (e.g., 'circular-import', 'unused-dep') */
|
|
48
|
+
readonly id: string
|
|
49
|
+
/** Human-readable title summarizing the issue */
|
|
50
|
+
readonly title: string
|
|
51
|
+
/** Detailed description explaining the problem and potential impact */
|
|
52
|
+
readonly description: string
|
|
53
|
+
/** Severity level determining how critical this issue is */
|
|
54
|
+
readonly severity: Severity
|
|
55
|
+
/** Category for grouping related issues */
|
|
56
|
+
readonly category: IssueCategory
|
|
57
|
+
/** Location where the issue was detected */
|
|
58
|
+
readonly location: IssueLocation
|
|
59
|
+
/** Additional locations related to this issue (e.g., cycle participants) */
|
|
60
|
+
readonly relatedLocations?: readonly IssueLocation[]
|
|
61
|
+
/** Suggested fix or action to resolve the issue */
|
|
62
|
+
readonly suggestion?: string
|
|
63
|
+
/** Additional metadata for machine processing */
|
|
64
|
+
readonly metadata?: Readonly<Record<string, unknown>>
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Summary statistics for an analysis run.
|
|
69
|
+
*/
|
|
70
|
+
export interface AnalysisSummary {
|
|
71
|
+
/** Total number of issues found */
|
|
72
|
+
readonly totalIssues: number
|
|
73
|
+
/** Issues grouped by severity */
|
|
74
|
+
readonly bySeverity: Readonly<Record<Severity, number>>
|
|
75
|
+
/** Issues grouped by category */
|
|
76
|
+
readonly byCategory: Readonly<Record<IssueCategory, number>>
|
|
77
|
+
/** Number of packages analyzed */
|
|
78
|
+
readonly packagesAnalyzed: number
|
|
79
|
+
/** Number of source files analyzed */
|
|
80
|
+
readonly filesAnalyzed: number
|
|
81
|
+
/** Analysis duration in milliseconds */
|
|
82
|
+
readonly durationMs: number
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Complete result of a workspace analysis run.
|
|
87
|
+
*/
|
|
88
|
+
export interface AnalysisResult {
|
|
89
|
+
/** All issues detected during analysis */
|
|
90
|
+
readonly issues: readonly Issue[]
|
|
91
|
+
/** Summary statistics */
|
|
92
|
+
readonly summary: AnalysisSummary
|
|
93
|
+
/** Workspace root path that was analyzed */
|
|
94
|
+
readonly workspacePath: string
|
|
95
|
+
/** Timestamp when analysis started */
|
|
96
|
+
readonly startedAt: Date
|
|
97
|
+
/** Timestamp when analysis completed */
|
|
98
|
+
readonly completedAt: Date
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Configuration for controlling analyzer behavior.
|
|
103
|
+
*/
|
|
104
|
+
export interface AnalyzerConfig {
|
|
105
|
+
/** Glob patterns for files to include in analysis */
|
|
106
|
+
readonly include?: readonly string[]
|
|
107
|
+
/** Glob patterns for files to exclude from analysis */
|
|
108
|
+
readonly exclude?: readonly string[]
|
|
109
|
+
/** Minimum severity level to report */
|
|
110
|
+
readonly minSeverity?: Severity
|
|
111
|
+
/** Categories of issues to check */
|
|
112
|
+
readonly categories?: readonly IssueCategory[]
|
|
113
|
+
/** Whether to enable caching for incremental analysis */
|
|
114
|
+
readonly cache?: boolean
|
|
115
|
+
/** Custom rules configuration */
|
|
116
|
+
readonly rules?: Readonly<Record<string, unknown>>
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Options for the analyzeWorkspace function.
|
|
121
|
+
*/
|
|
122
|
+
export interface AnalyzeWorkspaceOptions extends AnalyzerConfig {
|
|
123
|
+
/** Path to workspace-analyzer.config.ts file */
|
|
124
|
+
readonly configPath?: string
|
|
125
|
+
/** Callback for progress reporting */
|
|
126
|
+
readonly onProgress?: (progress: AnalysisProgress) => void
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Progress information during analysis.
|
|
131
|
+
*/
|
|
132
|
+
export interface AnalysisProgress {
|
|
133
|
+
/** Current phase of analysis */
|
|
134
|
+
readonly phase: 'scanning' | 'parsing' | 'analyzing' | 'reporting'
|
|
135
|
+
/** Current item being processed */
|
|
136
|
+
readonly current?: string
|
|
137
|
+
/** Number of items processed so far */
|
|
138
|
+
readonly processed: number
|
|
139
|
+
/** Total number of items to process (if known) */
|
|
140
|
+
readonly total?: number
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Represents an error that occurred during analysis.
|
|
145
|
+
*/
|
|
146
|
+
export interface AnalysisError {
|
|
147
|
+
/** Error code for programmatic handling */
|
|
148
|
+
readonly code: string
|
|
149
|
+
/** Human-readable error message */
|
|
150
|
+
readonly message: string
|
|
151
|
+
/** Stack trace if available */
|
|
152
|
+
readonly stack?: string
|
|
153
|
+
/** Additional context about the error */
|
|
154
|
+
readonly context?: Readonly<Record<string, unknown>>
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Type alias for analysis operations that may fail.
|
|
159
|
+
*/
|
|
160
|
+
export type AnalysisResultType<T> = Result<T, AnalysisError>
|
|
161
|
+
|
|
162
|
+
// Re-export Result types for convenience
|
|
163
|
+
export type {Err, Ok, Result} from './result'
|
|
164
|
+
export {
|
|
165
|
+
err,
|
|
166
|
+
flatMap,
|
|
167
|
+
fromPromise,
|
|
168
|
+
fromThrowable,
|
|
169
|
+
isErr,
|
|
170
|
+
isOk,
|
|
171
|
+
map,
|
|
172
|
+
mapErr,
|
|
173
|
+
ok,
|
|
174
|
+
unwrap,
|
|
175
|
+
unwrapOr,
|
|
176
|
+
} from './result'
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Re-export Result types from @bfra.me/es/result for discriminated union error handling.
|
|
3
|
+
*
|
|
4
|
+
* The Result pattern provides type-safe error handling without exceptions.
|
|
5
|
+
* Prefer this pattern over throwing for expected/recoverable errors in analysis operations.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export {err, ok} from '@bfra.me/es/result'
|
|
9
|
+
export {isErr, isOk} from '@bfra.me/es/result'
|
|
10
|
+
export {
|
|
11
|
+
flatMap,
|
|
12
|
+
fromPromise,
|
|
13
|
+
fromThrowable,
|
|
14
|
+
map,
|
|
15
|
+
mapErr,
|
|
16
|
+
unwrap,
|
|
17
|
+
unwrapOr,
|
|
18
|
+
} from '@bfra.me/es/result'
|
|
19
|
+
export type {Err, Ok, Result} from '@bfra.me/es/result'
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pattern matching utilities for glob-style file path matching.
|
|
3
|
+
*
|
|
4
|
+
* Provides simple glob pattern matching supporting ** (any path segments)
|
|
5
|
+
* and * (single segment) without external dependencies.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Matches a file path against a glob pattern.
|
|
10
|
+
*
|
|
11
|
+
* Supported patterns:
|
|
12
|
+
* - `**` matches any number of path segments
|
|
13
|
+
* - `*` matches any characters except path separator
|
|
14
|
+
*/
|
|
15
|
+
export function matchPattern(filePath: string, pattern: string): boolean {
|
|
16
|
+
const normalizedPath = normalizePath(filePath)
|
|
17
|
+
|
|
18
|
+
const DOUBLE_STAR_PLACEHOLDER = '\0DOUBLE_STAR\0'
|
|
19
|
+
const regexPattern = pattern
|
|
20
|
+
.replaceAll('**', DOUBLE_STAR_PLACEHOLDER)
|
|
21
|
+
.replaceAll('*', '[^/]*')
|
|
22
|
+
.replaceAll(DOUBLE_STAR_PLACEHOLDER, '.*')
|
|
23
|
+
.replaceAll('/', String.raw`\/`)
|
|
24
|
+
|
|
25
|
+
const regex = new RegExp(regexPattern)
|
|
26
|
+
return regex.test(normalizedPath)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Checks if a file path matches any of the given patterns.
|
|
31
|
+
*/
|
|
32
|
+
export function matchAnyPattern(filePath: string, patterns: readonly string[]): boolean {
|
|
33
|
+
for (const pattern of patterns) {
|
|
34
|
+
if (matchPattern(filePath, pattern)) {
|
|
35
|
+
return true
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return false
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Normalizes a file path for consistent pattern matching.
|
|
43
|
+
*
|
|
44
|
+
* Converts backslashes to forward slashes for cross-platform compatibility.
|
|
45
|
+
*/
|
|
46
|
+
export function normalizePath(filePath: string): string {
|
|
47
|
+
return filePath.replaceAll('\\', '/')
|
|
48
|
+
}
|