@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,183 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default configuration values for workspace analyzer.
|
|
3
|
+
*
|
|
4
|
+
* Provides sensible defaults that work well for typical TypeScript monorepo projects,
|
|
5
|
+
* with all options explicitly documented and type-safe.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type {IssueCategory, Severity} from '../types/index'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Default glob patterns for including source files in analysis.
|
|
12
|
+
*/
|
|
13
|
+
export const DEFAULT_INCLUDE_PATTERNS: readonly string[] = [
|
|
14
|
+
'**/*.ts',
|
|
15
|
+
'**/*.tsx',
|
|
16
|
+
'**/*.js',
|
|
17
|
+
'**/*.jsx',
|
|
18
|
+
'**/*.mts',
|
|
19
|
+
'**/*.cts',
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Default glob patterns for excluding files from analysis.
|
|
24
|
+
*/
|
|
25
|
+
export const DEFAULT_EXCLUDE_PATTERNS: readonly string[] = [
|
|
26
|
+
'**/node_modules/**',
|
|
27
|
+
'**/dist/**',
|
|
28
|
+
'**/lib/**',
|
|
29
|
+
'**/build/**',
|
|
30
|
+
'**/coverage/**',
|
|
31
|
+
'**/*.d.ts',
|
|
32
|
+
'**/*.test.ts',
|
|
33
|
+
'**/*.test.tsx',
|
|
34
|
+
'**/*.spec.ts',
|
|
35
|
+
'**/*.spec.tsx',
|
|
36
|
+
'**/__tests__/**',
|
|
37
|
+
'**/__mocks__/**',
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Default package location patterns for monorepo scanning.
|
|
42
|
+
*/
|
|
43
|
+
export const DEFAULT_PACKAGE_PATTERNS: readonly string[] = ['packages/*', 'apps/*']
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Default severity threshold for issue reporting.
|
|
47
|
+
*/
|
|
48
|
+
export const DEFAULT_MIN_SEVERITY: Severity = 'info'
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Default categories to analyze (all categories when empty).
|
|
52
|
+
*/
|
|
53
|
+
export const DEFAULT_CATEGORIES: readonly IssueCategory[] = []
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Default concurrency limit for parallel analysis.
|
|
57
|
+
*/
|
|
58
|
+
export const DEFAULT_CONCURRENCY = 4
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Default cache directory name.
|
|
62
|
+
*/
|
|
63
|
+
export const DEFAULT_CACHE_DIR = '.workspace-analyzer-cache'
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Default maximum cache age in milliseconds (7 days).
|
|
67
|
+
*/
|
|
68
|
+
export const DEFAULT_MAX_CACHE_AGE = 7 * 24 * 60 * 60 * 1000
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Default hash algorithm for file content hashing.
|
|
72
|
+
*/
|
|
73
|
+
export const DEFAULT_HASH_ALGORITHM = 'sha256' as const
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Complete default analyzer configuration.
|
|
77
|
+
*/
|
|
78
|
+
export interface DefaultAnalyzerConfig {
|
|
79
|
+
/** Glob patterns for files to include */
|
|
80
|
+
readonly include: readonly string[]
|
|
81
|
+
/** Glob patterns for files to exclude */
|
|
82
|
+
readonly exclude: readonly string[]
|
|
83
|
+
/** Minimum severity level to report */
|
|
84
|
+
readonly minSeverity: Severity
|
|
85
|
+
/** Categories of issues to check (empty means all) */
|
|
86
|
+
readonly categories: readonly IssueCategory[]
|
|
87
|
+
/** Whether to enable caching */
|
|
88
|
+
readonly cache: boolean
|
|
89
|
+
/** Custom rules configuration */
|
|
90
|
+
readonly rules: Readonly<Record<string, unknown>>
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Default analyzer configuration values.
|
|
95
|
+
*/
|
|
96
|
+
export const DEFAULT_ANALYZER_CONFIG: DefaultAnalyzerConfig = {
|
|
97
|
+
include: DEFAULT_INCLUDE_PATTERNS,
|
|
98
|
+
exclude: DEFAULT_EXCLUDE_PATTERNS,
|
|
99
|
+
minSeverity: DEFAULT_MIN_SEVERITY,
|
|
100
|
+
categories: DEFAULT_CATEGORIES,
|
|
101
|
+
cache: true,
|
|
102
|
+
rules: {},
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Default workspace analyzer options.
|
|
107
|
+
*/
|
|
108
|
+
export interface DefaultWorkspaceOptions {
|
|
109
|
+
/** Glob patterns for package locations */
|
|
110
|
+
readonly packagePatterns: readonly string[]
|
|
111
|
+
/** Maximum parallel analysis operations */
|
|
112
|
+
readonly concurrency: number
|
|
113
|
+
/** Cache directory path */
|
|
114
|
+
readonly cacheDir: string
|
|
115
|
+
/** Maximum cache age in milliseconds */
|
|
116
|
+
readonly maxCacheAge: number
|
|
117
|
+
/** Hash algorithm for file content */
|
|
118
|
+
readonly hashAlgorithm: 'sha256' | 'md5'
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Default workspace analyzer options values.
|
|
123
|
+
*/
|
|
124
|
+
export const DEFAULT_WORKSPACE_OPTIONS: DefaultWorkspaceOptions = {
|
|
125
|
+
packagePatterns: DEFAULT_PACKAGE_PATTERNS,
|
|
126
|
+
concurrency: DEFAULT_CONCURRENCY,
|
|
127
|
+
cacheDir: DEFAULT_CACHE_DIR,
|
|
128
|
+
maxCacheAge: DEFAULT_MAX_CACHE_AGE,
|
|
129
|
+
hashAlgorithm: DEFAULT_HASH_ALGORITHM,
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Complete default configuration combining analyzer and workspace options.
|
|
134
|
+
*/
|
|
135
|
+
export interface DefaultConfig extends DefaultAnalyzerConfig, DefaultWorkspaceOptions {
|
|
136
|
+
/** Configuration file path (optional) */
|
|
137
|
+
readonly configPath?: string
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Get the complete default configuration.
|
|
142
|
+
*/
|
|
143
|
+
export function getDefaultConfig(): DefaultConfig {
|
|
144
|
+
return {
|
|
145
|
+
...DEFAULT_ANALYZER_CONFIG,
|
|
146
|
+
...DEFAULT_WORKSPACE_OPTIONS,
|
|
147
|
+
configPath: undefined,
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Built-in analyzer IDs that are enabled by default.
|
|
153
|
+
*/
|
|
154
|
+
export const DEFAULT_ENABLED_ANALYZERS = [
|
|
155
|
+
'package-json',
|
|
156
|
+
'tsconfig',
|
|
157
|
+
'eslint-config',
|
|
158
|
+
'build-config',
|
|
159
|
+
'config-consistency',
|
|
160
|
+
'version-alignment',
|
|
161
|
+
'exports-field',
|
|
162
|
+
'unused-dependency',
|
|
163
|
+
'circular-import',
|
|
164
|
+
'peer-dependency',
|
|
165
|
+
'duplicate-dependency',
|
|
166
|
+
'architectural',
|
|
167
|
+
'dead-code',
|
|
168
|
+
'duplicate-code',
|
|
169
|
+
'large-dependency',
|
|
170
|
+
'tree-shaking-blocker',
|
|
171
|
+
] as const
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Built-in rule IDs that are enabled by default.
|
|
175
|
+
*/
|
|
176
|
+
export const DEFAULT_ENABLED_RULES = [
|
|
177
|
+
'layer-violation',
|
|
178
|
+
'barrel-export',
|
|
179
|
+
'public-api',
|
|
180
|
+
'side-effect',
|
|
181
|
+
'package-boundary',
|
|
182
|
+
'path-alias',
|
|
183
|
+
] as const
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration module for workspace analyzer.
|
|
3
|
+
*
|
|
4
|
+
* Provides configuration loading, validation, merging, and the defineConfig() helper
|
|
5
|
+
* for workspace-analyzer.config.ts files.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// Default configuration values
|
|
9
|
+
export {
|
|
10
|
+
DEFAULT_ANALYZER_CONFIG,
|
|
11
|
+
DEFAULT_CACHE_DIR,
|
|
12
|
+
DEFAULT_CATEGORIES,
|
|
13
|
+
DEFAULT_CONCURRENCY,
|
|
14
|
+
DEFAULT_ENABLED_ANALYZERS,
|
|
15
|
+
DEFAULT_ENABLED_RULES,
|
|
16
|
+
DEFAULT_EXCLUDE_PATTERNS,
|
|
17
|
+
DEFAULT_HASH_ALGORITHM,
|
|
18
|
+
DEFAULT_INCLUDE_PATTERNS,
|
|
19
|
+
DEFAULT_MAX_CACHE_AGE,
|
|
20
|
+
DEFAULT_MIN_SEVERITY,
|
|
21
|
+
DEFAULT_PACKAGE_PATTERNS,
|
|
22
|
+
DEFAULT_WORKSPACE_OPTIONS,
|
|
23
|
+
getDefaultConfig,
|
|
24
|
+
} from './defaults'
|
|
25
|
+
|
|
26
|
+
export type {DefaultAnalyzerConfig, DefaultConfig, DefaultWorkspaceOptions} from './defaults'
|
|
27
|
+
|
|
28
|
+
// Configuration file loader
|
|
29
|
+
export {CONFIG_FILE_NAMES, defineConfig, findConfigFile, loadConfig, loadConfigFile} from './loader'
|
|
30
|
+
|
|
31
|
+
export type {
|
|
32
|
+
ConfigLoadError,
|
|
33
|
+
ConfigLoadErrorCode,
|
|
34
|
+
ConfigLoadResult,
|
|
35
|
+
WorkspaceAnalyzerConfig,
|
|
36
|
+
} from './loader'
|
|
37
|
+
|
|
38
|
+
// Configuration merging
|
|
39
|
+
export {
|
|
40
|
+
getAnalyzerOptions,
|
|
41
|
+
getDefaultMergedConfig,
|
|
42
|
+
mergeAnalyzerConfigs,
|
|
43
|
+
mergeConfig,
|
|
44
|
+
} from './merger'
|
|
45
|
+
|
|
46
|
+
export type {MergedConfig} from './merger'
|
|
47
|
+
|
|
48
|
+
// Zod schemas and validation
|
|
49
|
+
export {
|
|
50
|
+
analyzerConfigSchema,
|
|
51
|
+
analyzerOptionsSchema,
|
|
52
|
+
analyzersConfigSchema,
|
|
53
|
+
analyzeWorkspaceOptionsSchema,
|
|
54
|
+
architecturalRulesSchema,
|
|
55
|
+
categorySchema,
|
|
56
|
+
layerConfigSchema,
|
|
57
|
+
parseAnalyzerConfig,
|
|
58
|
+
parseWorkspaceAnalyzerConfig,
|
|
59
|
+
ruleConfigSchema,
|
|
60
|
+
safeParseAnalyzeOptions,
|
|
61
|
+
safeParseWorkspaceAnalyzerConfig,
|
|
62
|
+
severitySchema,
|
|
63
|
+
workspaceAnalyzerConfigSchema,
|
|
64
|
+
workspaceOptionsSchema,
|
|
65
|
+
} from './schema'
|
|
66
|
+
|
|
67
|
+
export type {
|
|
68
|
+
AnalyzerConfigInput,
|
|
69
|
+
AnalyzerConfigOutput,
|
|
70
|
+
AnalyzeWorkspaceOptionsInput,
|
|
71
|
+
AnalyzeWorkspaceOptionsOutput,
|
|
72
|
+
CategoryInput,
|
|
73
|
+
CategoryOutput,
|
|
74
|
+
SafeParseResult,
|
|
75
|
+
SeverityInput,
|
|
76
|
+
SeverityOutput,
|
|
77
|
+
WorkspaceAnalyzerConfigInput,
|
|
78
|
+
WorkspaceAnalyzerConfigOutput,
|
|
79
|
+
WorkspaceOptionsInput,
|
|
80
|
+
WorkspaceOptionsOutput,
|
|
81
|
+
} from './schema'
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration file loader for workspace-analyzer.config.ts files.
|
|
3
|
+
*
|
|
4
|
+
* Provides functionality to discover, load, and parse TypeScript configuration files
|
|
5
|
+
* using the defineConfig() pattern similar to other tools in the ecosystem.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type {Result} from '../types/result'
|
|
9
|
+
import type {WorkspaceAnalyzerConfigOutput} from './schema'
|
|
10
|
+
|
|
11
|
+
import fs from 'node:fs/promises'
|
|
12
|
+
import path from 'node:path'
|
|
13
|
+
import {pathToFileURL} from 'node:url'
|
|
14
|
+
|
|
15
|
+
import {err, ok} from '@bfra.me/es/result'
|
|
16
|
+
|
|
17
|
+
import {safeParseWorkspaceAnalyzerConfig} from './schema'
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Configuration file names to search for, in order of precedence.
|
|
21
|
+
*/
|
|
22
|
+
export const CONFIG_FILE_NAMES = [
|
|
23
|
+
'workspace-analyzer.config.ts',
|
|
24
|
+
'workspace-analyzer.config.js',
|
|
25
|
+
'workspace-analyzer.config.mjs',
|
|
26
|
+
'workspace-analyzer.config.mts',
|
|
27
|
+
] as const
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Error codes for configuration loading operations.
|
|
31
|
+
*/
|
|
32
|
+
export type ConfigLoadErrorCode =
|
|
33
|
+
| 'FILE_NOT_FOUND'
|
|
34
|
+
| 'IMPORT_ERROR'
|
|
35
|
+
| 'VALIDATION_ERROR'
|
|
36
|
+
| 'INVALID_EXPORT'
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Error that occurred during configuration loading.
|
|
40
|
+
*/
|
|
41
|
+
export interface ConfigLoadError {
|
|
42
|
+
readonly code: ConfigLoadErrorCode
|
|
43
|
+
readonly message: string
|
|
44
|
+
readonly filePath?: string
|
|
45
|
+
readonly cause?: unknown
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Result of loading a configuration file.
|
|
50
|
+
*/
|
|
51
|
+
export interface ConfigLoadResult {
|
|
52
|
+
/** Loaded and validated configuration */
|
|
53
|
+
readonly config: WorkspaceAnalyzerConfigOutput
|
|
54
|
+
/** Path to the loaded configuration file */
|
|
55
|
+
readonly filePath: string
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* User-facing configuration interface for defineConfig().
|
|
60
|
+
* Allows partial configuration that will be merged with defaults.
|
|
61
|
+
*/
|
|
62
|
+
export interface WorkspaceAnalyzerConfig {
|
|
63
|
+
/** Glob patterns for files to include */
|
|
64
|
+
include?: readonly string[]
|
|
65
|
+
/** Glob patterns for files to exclude */
|
|
66
|
+
exclude?: readonly string[]
|
|
67
|
+
/** Minimum severity level to report */
|
|
68
|
+
minSeverity?: 'info' | 'warning' | 'error' | 'critical'
|
|
69
|
+
/** Categories of issues to check (empty means all) */
|
|
70
|
+
categories?: readonly (
|
|
71
|
+
| 'configuration'
|
|
72
|
+
| 'dependency'
|
|
73
|
+
| 'architecture'
|
|
74
|
+
| 'performance'
|
|
75
|
+
| 'circular-import'
|
|
76
|
+
| 'unused-export'
|
|
77
|
+
| 'type-safety'
|
|
78
|
+
)[]
|
|
79
|
+
/** Enable incremental analysis caching */
|
|
80
|
+
cache?: boolean
|
|
81
|
+
/** Custom rules configuration */
|
|
82
|
+
rules?: Record<string, unknown>
|
|
83
|
+
/** Glob patterns for package locations */
|
|
84
|
+
packagePatterns?: readonly string[]
|
|
85
|
+
/** Maximum parallel analysis operations */
|
|
86
|
+
concurrency?: number
|
|
87
|
+
/** Directory for analysis cache files */
|
|
88
|
+
cacheDir?: string
|
|
89
|
+
/** Maximum cache age in milliseconds */
|
|
90
|
+
maxCacheAge?: number
|
|
91
|
+
/** Hash algorithm for file content */
|
|
92
|
+
hashAlgorithm?: 'sha256' | 'md5'
|
|
93
|
+
/** Per-analyzer configuration */
|
|
94
|
+
analyzers?: Record<
|
|
95
|
+
string,
|
|
96
|
+
{
|
|
97
|
+
enabled?: boolean
|
|
98
|
+
severity?: 'info' | 'warning' | 'error' | 'critical'
|
|
99
|
+
options?: Record<string, unknown>
|
|
100
|
+
}
|
|
101
|
+
>
|
|
102
|
+
/** Architectural analysis rules */
|
|
103
|
+
architecture?: {
|
|
104
|
+
layers?: {
|
|
105
|
+
name: string
|
|
106
|
+
patterns: string[]
|
|
107
|
+
allowedImports: string[]
|
|
108
|
+
}[]
|
|
109
|
+
allowBarrelExports?: boolean | string[]
|
|
110
|
+
enforcePublicApi?: boolean
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Helper function for defining configuration with type safety.
|
|
116
|
+
*
|
|
117
|
+
* @example
|
|
118
|
+
* ```ts
|
|
119
|
+
* // workspace-analyzer.config.ts
|
|
120
|
+
* import {defineConfig} from '@bfra.me/workspace-analyzer'
|
|
121
|
+
*
|
|
122
|
+
* export default defineConfig({
|
|
123
|
+
* minSeverity: 'warning',
|
|
124
|
+
* categories: ['dependency', 'architecture'],
|
|
125
|
+
* analyzers: {
|
|
126
|
+
* 'circular-import': {enabled: true},
|
|
127
|
+
* 'unused-dependency': {enabled: true},
|
|
128
|
+
* },
|
|
129
|
+
* })
|
|
130
|
+
* ```
|
|
131
|
+
*/
|
|
132
|
+
export function defineConfig(config: WorkspaceAnalyzerConfig): WorkspaceAnalyzerConfig {
|
|
133
|
+
return config
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Finds a configuration file in the given directory or its parents.
|
|
138
|
+
*
|
|
139
|
+
* @param startDir - Directory to start searching from
|
|
140
|
+
* @param stopAt - Directory to stop searching at (defaults to filesystem root)
|
|
141
|
+
* @returns Path to the found configuration file, or undefined if not found
|
|
142
|
+
*/
|
|
143
|
+
export async function findConfigFile(
|
|
144
|
+
startDir: string,
|
|
145
|
+
stopAt?: string,
|
|
146
|
+
): Promise<string | undefined> {
|
|
147
|
+
let currentDir = path.resolve(startDir)
|
|
148
|
+
const root = stopAt == null ? path.parse(currentDir).root : path.resolve(stopAt)
|
|
149
|
+
|
|
150
|
+
while (currentDir !== root && currentDir !== path.dirname(currentDir)) {
|
|
151
|
+
for (const fileName of CONFIG_FILE_NAMES) {
|
|
152
|
+
const filePath = path.join(currentDir, fileName)
|
|
153
|
+
try {
|
|
154
|
+
await fs.access(filePath)
|
|
155
|
+
return filePath
|
|
156
|
+
} catch {
|
|
157
|
+
// File doesn't exist, continue searching
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
currentDir = path.dirname(currentDir)
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Check the root directory as well
|
|
164
|
+
for (const fileName of CONFIG_FILE_NAMES) {
|
|
165
|
+
const filePath = path.join(root, fileName)
|
|
166
|
+
try {
|
|
167
|
+
await fs.access(filePath)
|
|
168
|
+
return filePath
|
|
169
|
+
} catch {
|
|
170
|
+
// File doesn't exist
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return undefined
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Loads and validates a configuration file.
|
|
179
|
+
*
|
|
180
|
+
* @param filePath - Path to the configuration file
|
|
181
|
+
* @returns Result containing the loaded config or an error
|
|
182
|
+
*/
|
|
183
|
+
export async function loadConfigFile(
|
|
184
|
+
filePath: string,
|
|
185
|
+
): Promise<Result<ConfigLoadResult, ConfigLoadError>> {
|
|
186
|
+
const absolutePath = path.resolve(filePath)
|
|
187
|
+
|
|
188
|
+
// Verify file exists
|
|
189
|
+
try {
|
|
190
|
+
await fs.access(absolutePath)
|
|
191
|
+
} catch {
|
|
192
|
+
return err({
|
|
193
|
+
code: 'FILE_NOT_FOUND',
|
|
194
|
+
message: `Configuration file not found: ${absolutePath}`,
|
|
195
|
+
filePath: absolutePath,
|
|
196
|
+
})
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Import the module
|
|
200
|
+
let module: unknown
|
|
201
|
+
try {
|
|
202
|
+
const fileUrl = pathToFileURL(absolutePath).href
|
|
203
|
+
module = await import(fileUrl)
|
|
204
|
+
} catch (error) {
|
|
205
|
+
return err({
|
|
206
|
+
code: 'IMPORT_ERROR',
|
|
207
|
+
message: `Failed to import configuration file: ${(error as Error).message}`,
|
|
208
|
+
filePath: absolutePath,
|
|
209
|
+
cause: error,
|
|
210
|
+
})
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Extract the default export
|
|
214
|
+
const moduleObj = module as Record<string, unknown>
|
|
215
|
+
const rawConfig = moduleObj.default ?? module
|
|
216
|
+
|
|
217
|
+
if (rawConfig == null || typeof rawConfig !== 'object') {
|
|
218
|
+
return err({
|
|
219
|
+
code: 'INVALID_EXPORT',
|
|
220
|
+
message: 'Configuration file must export an object as default export',
|
|
221
|
+
filePath: absolutePath,
|
|
222
|
+
})
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Validate the configuration
|
|
226
|
+
const parseResult = safeParseWorkspaceAnalyzerConfig(rawConfig)
|
|
227
|
+
|
|
228
|
+
if (!parseResult.success) {
|
|
229
|
+
const issues = parseResult.error.issues
|
|
230
|
+
.map(issue => ` - ${issue.path.join('.')}: ${issue.message}`)
|
|
231
|
+
.join('\n')
|
|
232
|
+
return err({
|
|
233
|
+
code: 'VALIDATION_ERROR',
|
|
234
|
+
message: `Invalid configuration:\n${issues}`,
|
|
235
|
+
filePath: absolutePath,
|
|
236
|
+
cause: parseResult.error,
|
|
237
|
+
})
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return ok({
|
|
241
|
+
config: parseResult.data,
|
|
242
|
+
filePath: absolutePath,
|
|
243
|
+
})
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Discovers and loads configuration from the workspace.
|
|
248
|
+
*
|
|
249
|
+
* @param workspacePath - Root path of the workspace
|
|
250
|
+
* @param explicitPath - Optional explicit path to config file
|
|
251
|
+
* @returns Result containing the loaded config or an error
|
|
252
|
+
*/
|
|
253
|
+
export async function loadConfig(
|
|
254
|
+
workspacePath: string,
|
|
255
|
+
explicitPath?: string,
|
|
256
|
+
): Promise<Result<ConfigLoadResult | undefined, ConfigLoadError>> {
|
|
257
|
+
// Use explicit path if provided
|
|
258
|
+
if (explicitPath != null) {
|
|
259
|
+
return loadConfigFile(explicitPath)
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Search for config file
|
|
263
|
+
const configPath = await findConfigFile(workspacePath)
|
|
264
|
+
|
|
265
|
+
if (configPath == null) {
|
|
266
|
+
return ok(undefined)
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return loadConfigFile(configPath)
|
|
270
|
+
}
|