@bfra.me/workspace-analyzer 0.1.0 → 0.2.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 +218 -0
- package/lib/{chunk-WOJ4C7N7.js → chunk-4V5KYQED.js} +3697 -10
- package/lib/cli.js +315 -9
- package/lib/index.d.ts +566 -5
- package/lib/index.js +49 -1
- package/package.json +2 -1
- package/src/cli/commands/analyze.ts +8 -5
- package/src/cli/commands/visualize.ts +406 -0
- package/src/cli/index.ts +45 -1
- package/src/cli/types.ts +43 -0
- package/src/config/schema.ts +3 -3
- package/src/index.ts +49 -0
- package/src/visualizer/graph-builder.ts +397 -0
- package/src/visualizer/html-renderer.ts +556 -0
- package/src/visualizer/index.ts +83 -0
- package/src/visualizer/mermaid-exporter.ts +234 -0
- package/src/visualizer/templates/d3-bundle.ts +1566 -0
- package/src/visualizer/templates/graph-template.ts +959 -0
- package/src/visualizer/templates/styles.ts +928 -0
- package/src/visualizer/types.ts +226 -0
- package/src/visualizer/violation-collector.ts +246 -0
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type definitions for dependency graph visualization.
|
|
3
|
+
*
|
|
4
|
+
* These types define the data structures used to transform a DependencyGraph
|
|
5
|
+
* into a format suitable for interactive D3.js visualization.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type {Severity} from '../types/index'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* A node in the visualization graph representing a module.
|
|
12
|
+
*/
|
|
13
|
+
export interface VisualizationNode {
|
|
14
|
+
/** Unique identifier (typically the normalized file path) */
|
|
15
|
+
readonly id: string
|
|
16
|
+
/** Display name for the node */
|
|
17
|
+
readonly name: string
|
|
18
|
+
/** Full file path of the module */
|
|
19
|
+
readonly filePath: string
|
|
20
|
+
/** Package name if the module belongs to a workspace package */
|
|
21
|
+
readonly packageName: string | undefined
|
|
22
|
+
/** Architectural layer (e.g., 'domain', 'application', 'infrastructure') */
|
|
23
|
+
readonly layer: string | undefined
|
|
24
|
+
/** Number of modules this node imports */
|
|
25
|
+
readonly importsCount: number
|
|
26
|
+
/** Number of modules that import this node */
|
|
27
|
+
readonly importedByCount: number
|
|
28
|
+
/** Whether this node is part of a dependency cycle */
|
|
29
|
+
readonly isInCycle: boolean
|
|
30
|
+
/** Violations associated with this node */
|
|
31
|
+
readonly violations: readonly VisualizationViolation[]
|
|
32
|
+
/** Highest severity level among violations (undefined if no violations) */
|
|
33
|
+
readonly highestViolationSeverity: Severity | undefined
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* An edge in the visualization graph representing an import relationship.
|
|
38
|
+
*/
|
|
39
|
+
export interface VisualizationEdge {
|
|
40
|
+
/** Source node ID (the importing module) */
|
|
41
|
+
readonly source: string
|
|
42
|
+
/** Target node ID (the imported module) */
|
|
43
|
+
readonly target: string
|
|
44
|
+
/** Import type classification */
|
|
45
|
+
readonly type: 'static' | 'dynamic' | 'type-only' | 'require'
|
|
46
|
+
/** Whether this edge is part of a dependency cycle */
|
|
47
|
+
readonly isInCycle: boolean
|
|
48
|
+
/** ID of the cycle this edge belongs to (undefined if not in a cycle) */
|
|
49
|
+
readonly cycleId: string | undefined
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* A violation displayed on a node or edge in the visualization.
|
|
54
|
+
*/
|
|
55
|
+
export interface VisualizationViolation {
|
|
56
|
+
/** Unique identifier for this violation */
|
|
57
|
+
readonly id: string
|
|
58
|
+
/** Human-readable message describing the violation */
|
|
59
|
+
readonly message: string
|
|
60
|
+
/** Severity level of the violation */
|
|
61
|
+
readonly severity: Severity
|
|
62
|
+
/** ID of the rule that generated this violation */
|
|
63
|
+
readonly ruleId: string
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Cycle information for visualization highlighting.
|
|
68
|
+
*/
|
|
69
|
+
export interface VisualizationCycle {
|
|
70
|
+
/** Unique identifier for this cycle */
|
|
71
|
+
readonly id: string
|
|
72
|
+
/** Node IDs participating in the cycle, in order */
|
|
73
|
+
readonly nodes: readonly string[]
|
|
74
|
+
/** Edges forming the cycle path */
|
|
75
|
+
readonly edges: readonly {readonly from: string; readonly to: string}[]
|
|
76
|
+
/** Number of nodes in the cycle */
|
|
77
|
+
readonly length: number
|
|
78
|
+
/** Human-readable description of the cycle path */
|
|
79
|
+
readonly description: string
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Statistics summary for the visualization.
|
|
84
|
+
*/
|
|
85
|
+
export interface VisualizationStatistics {
|
|
86
|
+
/** Total number of nodes in the graph */
|
|
87
|
+
readonly totalNodes: number
|
|
88
|
+
/** Total number of edges (import relationships) */
|
|
89
|
+
readonly totalEdges: number
|
|
90
|
+
/** Total number of dependency cycles detected */
|
|
91
|
+
readonly totalCycles: number
|
|
92
|
+
/** Count of nodes grouped by architectural layer */
|
|
93
|
+
readonly nodesByLayer: Readonly<Record<string, number>>
|
|
94
|
+
/** Count of violations grouped by severity level */
|
|
95
|
+
readonly violationsBySeverity: Readonly<Record<Severity, number>>
|
|
96
|
+
/** Number of workspace packages analyzed */
|
|
97
|
+
readonly packagesAnalyzed: number
|
|
98
|
+
/** Number of source files analyzed */
|
|
99
|
+
readonly filesAnalyzed: number
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Metadata about the visualization generation.
|
|
104
|
+
*/
|
|
105
|
+
export interface VisualizationMetadata {
|
|
106
|
+
/** Root path of the analyzed workspace */
|
|
107
|
+
readonly workspacePath: string
|
|
108
|
+
/** ISO 8601 timestamp when the visualization was generated */
|
|
109
|
+
readonly generatedAt: string
|
|
110
|
+
/** Version of the workspace-analyzer that generated this data */
|
|
111
|
+
readonly analyzerVersion: string
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Layer definition for architectural boundary visualization.
|
|
116
|
+
*/
|
|
117
|
+
export interface VisualizationLayerDefinition {
|
|
118
|
+
/** Layer name (e.g., 'domain', 'application') */
|
|
119
|
+
readonly name: string
|
|
120
|
+
/** Layers this layer is allowed to depend on */
|
|
121
|
+
readonly allowedDependencies: readonly string[]
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Complete visualization data ready for rendering.
|
|
126
|
+
*/
|
|
127
|
+
export interface VisualizationData {
|
|
128
|
+
/** All nodes in the visualization graph */
|
|
129
|
+
readonly nodes: readonly VisualizationNode[]
|
|
130
|
+
/** All edges (import relationships) in the graph */
|
|
131
|
+
readonly edges: readonly VisualizationEdge[]
|
|
132
|
+
/** Detected dependency cycles */
|
|
133
|
+
readonly cycles: readonly VisualizationCycle[]
|
|
134
|
+
/** Summary statistics */
|
|
135
|
+
readonly statistics: VisualizationStatistics
|
|
136
|
+
/** Architectural layer definitions for filtering */
|
|
137
|
+
readonly layers: readonly VisualizationLayerDefinition[]
|
|
138
|
+
/** Generation metadata */
|
|
139
|
+
readonly metadata: VisualizationMetadata
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Filter configuration for the visualization UI.
|
|
144
|
+
*/
|
|
145
|
+
export interface VisualizationFilters {
|
|
146
|
+
/** Layers to display (empty = all layers) */
|
|
147
|
+
readonly layers: readonly string[]
|
|
148
|
+
/** Severity levels to display (empty = all severities) */
|
|
149
|
+
readonly severities: readonly Severity[]
|
|
150
|
+
/** Package scopes to display (e.g., '@bfra.me/*') */
|
|
151
|
+
readonly packages: readonly string[]
|
|
152
|
+
/** Show only nodes that are part of cycles */
|
|
153
|
+
readonly showCyclesOnly: boolean
|
|
154
|
+
/** Show only nodes with violations */
|
|
155
|
+
readonly showViolationsOnly: boolean
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Options for configuring visualization generation.
|
|
160
|
+
*/
|
|
161
|
+
export interface VisualizerOptions {
|
|
162
|
+
/** Output path for the generated HTML file */
|
|
163
|
+
readonly outputPath: string
|
|
164
|
+
/** Output format(s) to generate */
|
|
165
|
+
readonly format: 'html' | 'json' | 'both'
|
|
166
|
+
/** Whether to auto-open the generated file in the browser */
|
|
167
|
+
readonly autoOpen: boolean
|
|
168
|
+
/** Title displayed in the visualization */
|
|
169
|
+
readonly title: string
|
|
170
|
+
/** Pre-applied filters for the initial visualization state */
|
|
171
|
+
readonly filters: Partial<VisualizationFilters>
|
|
172
|
+
/** Include type-only imports in the graph */
|
|
173
|
+
readonly includeTypeImports: boolean
|
|
174
|
+
/** Maximum number of nodes to render (for performance) */
|
|
175
|
+
readonly maxNodes: number
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Default visualization options.
|
|
180
|
+
*/
|
|
181
|
+
export const DEFAULT_VISUALIZER_OPTIONS: VisualizerOptions = {
|
|
182
|
+
outputPath: './workspace-graph.html',
|
|
183
|
+
format: 'html',
|
|
184
|
+
autoOpen: true,
|
|
185
|
+
title: 'Workspace Dependency Graph',
|
|
186
|
+
filters: {},
|
|
187
|
+
includeTypeImports: true,
|
|
188
|
+
maxNodes: 1000,
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Severity levels ordered by importance (highest first).
|
|
193
|
+
*/
|
|
194
|
+
export const SEVERITY_ORDER: readonly Severity[] = ['critical', 'error', 'warning', 'info'] as const
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Error type for visualization generation failures.
|
|
198
|
+
*/
|
|
199
|
+
export interface VisualizationError {
|
|
200
|
+
/** Error code for programmatic handling */
|
|
201
|
+
readonly code: 'INVALID_GRAPH' | 'TRANSFORM_FAILED' | 'LIMIT_EXCEEDED'
|
|
202
|
+
/** Human-readable error message */
|
|
203
|
+
readonly message: string
|
|
204
|
+
/** Optional details about the error */
|
|
205
|
+
readonly details?: Record<string, unknown>
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Gets the highest severity from a list of severities.
|
|
210
|
+
*
|
|
211
|
+
* @param severities - Array of severity levels
|
|
212
|
+
* @returns The highest severity, or undefined if the array is empty
|
|
213
|
+
*/
|
|
214
|
+
export function getHighestSeverity(severities: readonly Severity[]): Severity | undefined {
|
|
215
|
+
if (severities.length === 0) {
|
|
216
|
+
return undefined
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
for (const level of SEVERITY_ORDER) {
|
|
220
|
+
if (severities.includes(level)) {
|
|
221
|
+
return level
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return severities[0]
|
|
226
|
+
}
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Violation collection utilities for gathering architectural issues from RuleEngine.
|
|
3
|
+
*
|
|
4
|
+
* Provides functions to collect violations from the rule engine and associate them
|
|
5
|
+
* with visualization nodes for display in the interactive graph.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type {Project, SourceFile} from 'ts-morph'
|
|
9
|
+
|
|
10
|
+
import type {createRuleEngine, RuleContext} from '../rules/rule-engine'
|
|
11
|
+
import type {WorkspacePackage} from '../scanner/workspace-scanner'
|
|
12
|
+
import type {Issue} from '../types/index'
|
|
13
|
+
import type {Result} from '../types/result'
|
|
14
|
+
import type {VisualizationError, VisualizationNode} from './types'
|
|
15
|
+
|
|
16
|
+
import path from 'node:path'
|
|
17
|
+
|
|
18
|
+
import {createProject} from '@bfra.me/doc-sync'
|
|
19
|
+
import {err, isErr, ok} from '@bfra.me/es/result'
|
|
20
|
+
|
|
21
|
+
import {getHighestSeverity} from './types'
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Context for collecting violations from the workspace.
|
|
25
|
+
*/
|
|
26
|
+
export interface ViolationCollectionContext {
|
|
27
|
+
/** The rule engine instance to use for evaluation */
|
|
28
|
+
readonly ruleEngine: ReturnType<typeof createRuleEngine>
|
|
29
|
+
/** All packages in the workspace */
|
|
30
|
+
readonly packages: readonly WorkspacePackage[]
|
|
31
|
+
/** Root path of the workspace */
|
|
32
|
+
readonly workspacePath: string
|
|
33
|
+
/** Optional tsconfig path mappings */
|
|
34
|
+
readonly tsconfigPaths?: Readonly<Record<string, readonly string[]>>
|
|
35
|
+
/** Optional progress reporting callback */
|
|
36
|
+
readonly reportProgress?: (message: string) => void
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Options for violation collection.
|
|
41
|
+
*/
|
|
42
|
+
export interface ViolationCollectionOptions {
|
|
43
|
+
/** Whether to include info-level issues */
|
|
44
|
+
readonly includeInfo: boolean
|
|
45
|
+
/** Maximum number of issues to collect (for performance) */
|
|
46
|
+
readonly maxIssues: number
|
|
47
|
+
/** File patterns to exclude from analysis */
|
|
48
|
+
readonly excludePatterns: readonly string[]
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Default options for violation collection.
|
|
53
|
+
*/
|
|
54
|
+
export const DEFAULT_VIOLATION_COLLECTION_OPTIONS: ViolationCollectionOptions = {
|
|
55
|
+
includeInfo: true,
|
|
56
|
+
maxIssues: 10000,
|
|
57
|
+
excludePatterns: ['**/*.test.ts', '**/*.spec.ts', '**/node_modules/**'],
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Collects architectural violations using the RuleEngine.
|
|
62
|
+
*
|
|
63
|
+
* Evaluates all source files in the workspace packages against the configured
|
|
64
|
+
* architectural rules and collects the resulting issues for visualization.
|
|
65
|
+
*
|
|
66
|
+
* @param context - Context containing rule engine and workspace information
|
|
67
|
+
* @param options - Options for controlling collection behavior
|
|
68
|
+
* @returns Result containing collected issues or an error
|
|
69
|
+
*/
|
|
70
|
+
export async function collectVisualizationViolations(
|
|
71
|
+
context: ViolationCollectionContext,
|
|
72
|
+
options: Partial<ViolationCollectionOptions> = {},
|
|
73
|
+
): Promise<Result<readonly Issue[], VisualizationError>> {
|
|
74
|
+
const opts: ViolationCollectionOptions = {
|
|
75
|
+
...DEFAULT_VIOLATION_COLLECTION_OPTIONS,
|
|
76
|
+
...options,
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const {ruleEngine, packages, workspacePath, tsconfigPaths, reportProgress} = context
|
|
80
|
+
|
|
81
|
+
const allIssues: Issue[] = []
|
|
82
|
+
|
|
83
|
+
try {
|
|
84
|
+
for (const pkg of packages) {
|
|
85
|
+
reportProgress?.(`Collecting violations from ${pkg.name}...`)
|
|
86
|
+
|
|
87
|
+
const tsconfigPath = path.join(pkg.packagePath, 'tsconfig.json')
|
|
88
|
+
|
|
89
|
+
let project: Project
|
|
90
|
+
try {
|
|
91
|
+
project = createProject({
|
|
92
|
+
tsConfigPath: tsconfigPath,
|
|
93
|
+
})
|
|
94
|
+
} catch {
|
|
95
|
+
continue
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const sourceFiles = getSourceFiles(project)
|
|
99
|
+
|
|
100
|
+
for (const sourceFile of sourceFiles) {
|
|
101
|
+
const filePath = sourceFile.getFilePath()
|
|
102
|
+
|
|
103
|
+
if (shouldSkipFile(filePath, opts.excludePatterns)) {
|
|
104
|
+
continue
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const ruleContext: RuleContext = {
|
|
108
|
+
sourceFile,
|
|
109
|
+
pkg,
|
|
110
|
+
workspacePath,
|
|
111
|
+
allPackages: packages,
|
|
112
|
+
tsconfigPaths,
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const result = await ruleEngine.evaluateFile(ruleContext)
|
|
116
|
+
|
|
117
|
+
if (isErr(result)) {
|
|
118
|
+
continue
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const issues = result.data
|
|
122
|
+
|
|
123
|
+
const filteredIssues = opts.includeInfo
|
|
124
|
+
? issues
|
|
125
|
+
: issues.filter(issue => issue.severity !== 'info')
|
|
126
|
+
|
|
127
|
+
allIssues.push(...filteredIssues)
|
|
128
|
+
|
|
129
|
+
if (allIssues.length >= opts.maxIssues) {
|
|
130
|
+
reportProgress?.(`Reached maximum issue limit (${opts.maxIssues})`)
|
|
131
|
+
break
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (allIssues.length >= opts.maxIssues) {
|
|
136
|
+
break
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return ok(allIssues)
|
|
141
|
+
} catch (error) {
|
|
142
|
+
return err({
|
|
143
|
+
code: 'TRANSFORM_FAILED',
|
|
144
|
+
message: `Error collecting violations: ${error instanceof Error ? error.message : String(error)}`,
|
|
145
|
+
})
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Maps issues to their corresponding visualization nodes.
|
|
151
|
+
*
|
|
152
|
+
* Associates each issue with the node(s) it affects by matching file paths.
|
|
153
|
+
* Updates nodes with their violations and highest severity level.
|
|
154
|
+
*
|
|
155
|
+
* @param nodes - Visualization nodes to annotate with violations
|
|
156
|
+
* @param issues - Issues collected from rule engine
|
|
157
|
+
* @returns Updated nodes with violation information
|
|
158
|
+
*/
|
|
159
|
+
export function mapIssuesToNodes(
|
|
160
|
+
nodes: readonly VisualizationNode[],
|
|
161
|
+
issues: readonly Issue[],
|
|
162
|
+
): readonly VisualizationNode[] {
|
|
163
|
+
const nodesByPath = new Map<string, VisualizationNode>()
|
|
164
|
+
|
|
165
|
+
for (const node of nodes) {
|
|
166
|
+
const normalizedPath = normalizePath(node.filePath)
|
|
167
|
+
nodesByPath.set(normalizedPath, node)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const issuesByPath = new Map<string, Issue[]>()
|
|
171
|
+
for (const issue of issues) {
|
|
172
|
+
const normalizedPath = normalizePath(issue.location.filePath)
|
|
173
|
+
const existing = issuesByPath.get(normalizedPath) ?? []
|
|
174
|
+
existing.push(issue)
|
|
175
|
+
issuesByPath.set(normalizedPath, existing)
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return nodes.map(node => {
|
|
179
|
+
const normalizedPath = normalizePath(node.filePath)
|
|
180
|
+
const nodeIssues = issuesByPath.get(normalizedPath) ?? []
|
|
181
|
+
|
|
182
|
+
if (nodeIssues.length === 0) {
|
|
183
|
+
return node
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const violations = nodeIssues.map((issue, index) => ({
|
|
187
|
+
id: `${issue.id}-${index}`,
|
|
188
|
+
message: issue.description,
|
|
189
|
+
severity: issue.severity,
|
|
190
|
+
ruleId: issue.id,
|
|
191
|
+
}))
|
|
192
|
+
|
|
193
|
+
const severities = violations.map(v => v.severity)
|
|
194
|
+
const highestSeverity = getHighestSeverity(severities)
|
|
195
|
+
|
|
196
|
+
return {
|
|
197
|
+
...node,
|
|
198
|
+
violations,
|
|
199
|
+
highestViolationSeverity: highestSeverity,
|
|
200
|
+
}
|
|
201
|
+
})
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Gets source files from a TypeScript project.
|
|
206
|
+
*
|
|
207
|
+
* @param project - The ts-morph project
|
|
208
|
+
* @returns Array of source files for analysis
|
|
209
|
+
*/
|
|
210
|
+
function getSourceFiles(project: Project): readonly SourceFile[] {
|
|
211
|
+
return project.getSourceFiles().filter(sf => {
|
|
212
|
+
const filePath = sf.getFilePath()
|
|
213
|
+
return (
|
|
214
|
+
(filePath.endsWith('.ts') ||
|
|
215
|
+
filePath.endsWith('.tsx') ||
|
|
216
|
+
filePath.endsWith('.js') ||
|
|
217
|
+
filePath.endsWith('.jsx')) &&
|
|
218
|
+
!filePath.includes('node_modules') &&
|
|
219
|
+
!filePath.includes('.test.') &&
|
|
220
|
+
!filePath.includes('.spec.')
|
|
221
|
+
)
|
|
222
|
+
})
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Determines if a file should be skipped based on exclude patterns.
|
|
227
|
+
*/
|
|
228
|
+
function shouldSkipFile(filePath: string, excludePatterns: readonly string[]): boolean {
|
|
229
|
+
if (excludePatterns.length === 0) {
|
|
230
|
+
return false
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const normalized = normalizePath(filePath)
|
|
234
|
+
|
|
235
|
+
return excludePatterns.some(pattern => {
|
|
236
|
+
const normalizedPattern = pattern.replaceAll('\\', '/').toLowerCase()
|
|
237
|
+
return normalized.includes(normalizedPattern.replaceAll('**', '').replaceAll('*', ''))
|
|
238
|
+
})
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Normalizes a file path for comparison.
|
|
243
|
+
*/
|
|
244
|
+
function normalizePath(filePath: string): string {
|
|
245
|
+
return filePath.replaceAll('\\', '/').toLowerCase()
|
|
246
|
+
}
|