@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.
Files changed (73) hide show
  1. package/README.md +402 -0
  2. package/lib/chunk-4LSFAAZW.js +1 -0
  3. package/lib/chunk-JDF7DQ4V.js +27 -0
  4. package/lib/chunk-WOJ4C7N7.js +7122 -0
  5. package/lib/cli.d.ts +1 -0
  6. package/lib/cli.js +318 -0
  7. package/lib/index.d.ts +3701 -0
  8. package/lib/index.js +1262 -0
  9. package/lib/types/index.d.ts +146 -0
  10. package/lib/types/index.js +28 -0
  11. package/package.json +89 -0
  12. package/src/analyzers/analyzer.ts +201 -0
  13. package/src/analyzers/architectural-analyzer.ts +304 -0
  14. package/src/analyzers/build-config-analyzer.ts +334 -0
  15. package/src/analyzers/circular-import-analyzer.ts +463 -0
  16. package/src/analyzers/config-consistency-analyzer.ts +335 -0
  17. package/src/analyzers/dead-code-analyzer.ts +565 -0
  18. package/src/analyzers/duplicate-code-analyzer.ts +626 -0
  19. package/src/analyzers/duplicate-dependency-analyzer.ts +381 -0
  20. package/src/analyzers/eslint-config-analyzer.ts +281 -0
  21. package/src/analyzers/exports-field-analyzer.ts +324 -0
  22. package/src/analyzers/index.ts +388 -0
  23. package/src/analyzers/large-dependency-analyzer.ts +535 -0
  24. package/src/analyzers/package-json-analyzer.ts +349 -0
  25. package/src/analyzers/peer-dependency-analyzer.ts +275 -0
  26. package/src/analyzers/tree-shaking-analyzer.ts +623 -0
  27. package/src/analyzers/tsconfig-analyzer.ts +382 -0
  28. package/src/analyzers/unused-dependency-analyzer.ts +356 -0
  29. package/src/analyzers/version-alignment-analyzer.ts +308 -0
  30. package/src/api/analyze-workspace.ts +245 -0
  31. package/src/api/index.ts +11 -0
  32. package/src/cache/cache-manager.ts +495 -0
  33. package/src/cache/cache-schema.ts +247 -0
  34. package/src/cache/change-detector.ts +169 -0
  35. package/src/cache/file-hasher.ts +65 -0
  36. package/src/cache/index.ts +47 -0
  37. package/src/cli/commands/analyze.ts +240 -0
  38. package/src/cli/commands/index.ts +5 -0
  39. package/src/cli/index.ts +61 -0
  40. package/src/cli/types.ts +65 -0
  41. package/src/cli/ui.ts +213 -0
  42. package/src/cli.ts +9 -0
  43. package/src/config/defaults.ts +183 -0
  44. package/src/config/index.ts +81 -0
  45. package/src/config/loader.ts +270 -0
  46. package/src/config/merger.ts +229 -0
  47. package/src/config/schema.ts +263 -0
  48. package/src/core/incremental-analyzer.ts +462 -0
  49. package/src/core/index.ts +34 -0
  50. package/src/core/orchestrator.ts +416 -0
  51. package/src/graph/dependency-graph.ts +408 -0
  52. package/src/graph/index.ts +19 -0
  53. package/src/index.ts +417 -0
  54. package/src/parser/config-parser.ts +491 -0
  55. package/src/parser/import-extractor.ts +340 -0
  56. package/src/parser/index.ts +54 -0
  57. package/src/parser/typescript-parser.ts +95 -0
  58. package/src/performance/bundle-estimator.ts +444 -0
  59. package/src/performance/index.ts +27 -0
  60. package/src/reporters/console-reporter.ts +355 -0
  61. package/src/reporters/index.ts +49 -0
  62. package/src/reporters/json-reporter.ts +273 -0
  63. package/src/reporters/markdown-reporter.ts +349 -0
  64. package/src/reporters/reporter.ts +399 -0
  65. package/src/rules/builtin-rules.ts +709 -0
  66. package/src/rules/index.ts +52 -0
  67. package/src/rules/rule-engine.ts +409 -0
  68. package/src/scanner/index.ts +18 -0
  69. package/src/scanner/workspace-scanner.ts +403 -0
  70. package/src/types/index.ts +176 -0
  71. package/src/types/result.ts +19 -0
  72. package/src/utils/index.ts +7 -0
  73. 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,7 @@
1
+ /**
2
+ * Utility module exports.
3
+ *
4
+ * Provides shared utility functions used across the workspace analyzer package.
5
+ */
6
+
7
+ export {matchAnyPattern, matchPattern, normalizePath} from './pattern-matcher'
@@ -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
+ }