@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,304 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ArchitecturalAnalyzer - Validates architectural patterns and enforces best practices.
|
|
3
|
+
*
|
|
4
|
+
* Integrates multiple architectural rules to detect:
|
|
5
|
+
* - Layer boundary violations
|
|
6
|
+
* - Barrel export (export *) misuse
|
|
7
|
+
* - Public API surface issues
|
|
8
|
+
* - Side effects in module initialization
|
|
9
|
+
* - Import path alias violations
|
|
10
|
+
* - Monorepo package boundary violations
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type {LayerConfiguration, RuleContext} from '../rules/rule-engine'
|
|
14
|
+
import type {WorkspacePackage} from '../scanner/workspace-scanner'
|
|
15
|
+
import type {Issue, Severity} from '../types/index'
|
|
16
|
+
import type {Result} from '../types/result'
|
|
17
|
+
import type {
|
|
18
|
+
AnalysisContext,
|
|
19
|
+
Analyzer,
|
|
20
|
+
AnalyzerError,
|
|
21
|
+
AnalyzerMetadata,
|
|
22
|
+
AnalyzerOptions,
|
|
23
|
+
} from './analyzer'
|
|
24
|
+
|
|
25
|
+
import path from 'node:path'
|
|
26
|
+
|
|
27
|
+
import {createProject} from '@bfra.me/doc-sync/parsers'
|
|
28
|
+
import {ok} from '@bfra.me/es/result'
|
|
29
|
+
|
|
30
|
+
import {getAllSourceFiles} from '../parser/typescript-parser'
|
|
31
|
+
import {
|
|
32
|
+
createBarrelExportRule,
|
|
33
|
+
createLayerViolationRule,
|
|
34
|
+
createPackageBoundaryRule,
|
|
35
|
+
createPathAliasRule,
|
|
36
|
+
createPublicApiRule,
|
|
37
|
+
createSideEffectRule,
|
|
38
|
+
} from '../rules/builtin-rules'
|
|
39
|
+
import {createRuleEngine, DEFAULT_LAYER_CONFIG} from '../rules/rule-engine'
|
|
40
|
+
import {matchAnyPattern} from '../utils/pattern-matcher'
|
|
41
|
+
import {createIssue, filterIssues} from './analyzer'
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Configuration options for the ArchitecturalAnalyzer.
|
|
45
|
+
*/
|
|
46
|
+
export interface ArchitecturalAnalyzerOptions extends AnalyzerOptions {
|
|
47
|
+
/** Layer configuration for architectural boundary enforcement */
|
|
48
|
+
readonly layerConfig?: LayerConfiguration
|
|
49
|
+
/** tsconfig.json paths for alias validation */
|
|
50
|
+
readonly tsconfigPaths?: Readonly<Record<string, readonly string[]>>
|
|
51
|
+
/** Enable layer violation detection */
|
|
52
|
+
readonly checkLayerViolations?: boolean
|
|
53
|
+
/** Enable barrel export detection */
|
|
54
|
+
readonly checkBarrelExports?: boolean
|
|
55
|
+
/** Enable public API validation */
|
|
56
|
+
readonly checkPublicApi?: boolean
|
|
57
|
+
/** Enable side effect detection */
|
|
58
|
+
readonly checkSideEffects?: boolean
|
|
59
|
+
/** Enable path alias validation */
|
|
60
|
+
readonly checkPathAliases?: boolean
|
|
61
|
+
/** Enable package boundary enforcement */
|
|
62
|
+
readonly checkPackageBoundaries?: boolean
|
|
63
|
+
/** File patterns for entry points */
|
|
64
|
+
readonly entryPointPatterns?: readonly string[]
|
|
65
|
+
/** Patterns allowed for barrel exports */
|
|
66
|
+
readonly allowedBarrelPatterns?: readonly string[]
|
|
67
|
+
/** Shared packages that can be imported from anywhere */
|
|
68
|
+
readonly sharedPackages?: readonly string[]
|
|
69
|
+
/** Severity for layer violations */
|
|
70
|
+
readonly layerViolationSeverity?: Severity
|
|
71
|
+
/** Severity for barrel export issues */
|
|
72
|
+
readonly barrelExportSeverity?: Severity
|
|
73
|
+
/** Severity for public API issues */
|
|
74
|
+
readonly publicApiSeverity?: Severity
|
|
75
|
+
/** Severity for side effect issues */
|
|
76
|
+
readonly sideEffectSeverity?: Severity
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const DEFAULT_OPTIONS: Required<
|
|
80
|
+
Omit<ArchitecturalAnalyzerOptions, keyof AnalyzerOptions | 'tsconfigPaths'>
|
|
81
|
+
> = {
|
|
82
|
+
layerConfig: DEFAULT_LAYER_CONFIG,
|
|
83
|
+
checkLayerViolations: true,
|
|
84
|
+
checkBarrelExports: true,
|
|
85
|
+
checkPublicApi: true,
|
|
86
|
+
checkSideEffects: true,
|
|
87
|
+
checkPathAliases: true,
|
|
88
|
+
checkPackageBoundaries: true,
|
|
89
|
+
entryPointPatterns: ['**/index.ts', '**/index.js'],
|
|
90
|
+
allowedBarrelPatterns: ['**/index.ts'],
|
|
91
|
+
sharedPackages: ['@bfra.me/es', '@bfra.me/tsconfig'],
|
|
92
|
+
layerViolationSeverity: 'warning',
|
|
93
|
+
barrelExportSeverity: 'warning',
|
|
94
|
+
publicApiSeverity: 'info',
|
|
95
|
+
sideEffectSeverity: 'warning',
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export const architecturalAnalyzerMetadata: AnalyzerMetadata = {
|
|
99
|
+
id: 'architectural',
|
|
100
|
+
name: 'Architectural Analyzer',
|
|
101
|
+
description:
|
|
102
|
+
'Validates architectural patterns including layer boundaries, exports, and package structure',
|
|
103
|
+
categories: ['architecture'],
|
|
104
|
+
defaultSeverity: 'warning',
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Creates an ArchitecturalAnalyzer instance with all built-in rules.
|
|
109
|
+
*
|
|
110
|
+
* @example
|
|
111
|
+
* ```ts
|
|
112
|
+
* const analyzer = createArchitecturalAnalyzer({
|
|
113
|
+
* checkLayerViolations: true,
|
|
114
|
+
* checkBarrelExports: true,
|
|
115
|
+
* layerConfig: {
|
|
116
|
+
* layers: [
|
|
117
|
+
* {name: 'domain', allowedDependencies: []},
|
|
118
|
+
* {name: 'application', allowedDependencies: ['domain']},
|
|
119
|
+
* ],
|
|
120
|
+
* patterns: [
|
|
121
|
+
* {pattern: '**\/domain\/**', layer: 'domain'},
|
|
122
|
+
* {pattern: '**\/application\/**', layer: 'application'},
|
|
123
|
+
* ],
|
|
124
|
+
* },
|
|
125
|
+
* })
|
|
126
|
+
*
|
|
127
|
+
* const result = await analyzer.analyze(context)
|
|
128
|
+
* ```
|
|
129
|
+
*/
|
|
130
|
+
export function createArchitecturalAnalyzer(options: ArchitecturalAnalyzerOptions = {}): Analyzer {
|
|
131
|
+
const resolvedOptions = {...DEFAULT_OPTIONS, ...options}
|
|
132
|
+
|
|
133
|
+
const engine = createRuleEngine()
|
|
134
|
+
|
|
135
|
+
if (resolvedOptions.checkLayerViolations) {
|
|
136
|
+
engine.register('layer-violation', {
|
|
137
|
+
rule: createLayerViolationRule({
|
|
138
|
+
severity: resolvedOptions.layerViolationSeverity,
|
|
139
|
+
options: {layerConfig: resolvedOptions.layerConfig},
|
|
140
|
+
}),
|
|
141
|
+
enabled: true,
|
|
142
|
+
priority: 10,
|
|
143
|
+
})
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (resolvedOptions.checkBarrelExports) {
|
|
147
|
+
engine.register('barrel-export', {
|
|
148
|
+
rule: createBarrelExportRule({
|
|
149
|
+
severity: resolvedOptions.barrelExportSeverity,
|
|
150
|
+
options: {
|
|
151
|
+
allowedPatterns: resolvedOptions.allowedBarrelPatterns,
|
|
152
|
+
allowWorkspaceReexports: false,
|
|
153
|
+
},
|
|
154
|
+
}),
|
|
155
|
+
enabled: true,
|
|
156
|
+
priority: 20,
|
|
157
|
+
})
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (resolvedOptions.checkPublicApi) {
|
|
161
|
+
engine.register('public-api', {
|
|
162
|
+
rule: createPublicApiRule({
|
|
163
|
+
severity: resolvedOptions.publicApiSeverity,
|
|
164
|
+
options: {
|
|
165
|
+
entryPoints: resolvedOptions.entryPointPatterns as string[],
|
|
166
|
+
requireReexport: false,
|
|
167
|
+
},
|
|
168
|
+
}),
|
|
169
|
+
enabled: true,
|
|
170
|
+
priority: 30,
|
|
171
|
+
})
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (resolvedOptions.checkSideEffects) {
|
|
175
|
+
engine.register('side-effect', {
|
|
176
|
+
rule: createSideEffectRule({
|
|
177
|
+
severity: resolvedOptions.sideEffectSeverity,
|
|
178
|
+
options: {
|
|
179
|
+
checkConsoleCalls: true,
|
|
180
|
+
checkGlobalAssignments: true,
|
|
181
|
+
},
|
|
182
|
+
}),
|
|
183
|
+
enabled: true,
|
|
184
|
+
priority: 40,
|
|
185
|
+
})
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (resolvedOptions.checkPathAliases) {
|
|
189
|
+
engine.register('path-alias', {
|
|
190
|
+
rule: createPathAliasRule({
|
|
191
|
+
options: {
|
|
192
|
+
requireAliasForDeepImports: true,
|
|
193
|
+
deepImportThreshold: 3,
|
|
194
|
+
},
|
|
195
|
+
}),
|
|
196
|
+
enabled: true,
|
|
197
|
+
priority: 50,
|
|
198
|
+
})
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (resolvedOptions.checkPackageBoundaries) {
|
|
202
|
+
engine.register('package-boundary', {
|
|
203
|
+
rule: createPackageBoundaryRule({
|
|
204
|
+
options: {
|
|
205
|
+
sharedPackages: resolvedOptions.sharedPackages as string[],
|
|
206
|
+
enforceEntryPointImports: true,
|
|
207
|
+
},
|
|
208
|
+
}),
|
|
209
|
+
enabled: true,
|
|
210
|
+
priority: 60,
|
|
211
|
+
})
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return {
|
|
215
|
+
metadata: architecturalAnalyzerMetadata,
|
|
216
|
+
analyze: async (context: AnalysisContext): Promise<Result<readonly Issue[], AnalyzerError>> => {
|
|
217
|
+
const issues: Issue[] = []
|
|
218
|
+
|
|
219
|
+
for (const pkg of context.packages) {
|
|
220
|
+
context.reportProgress?.(`Analyzing architecture in ${pkg.name}...`)
|
|
221
|
+
|
|
222
|
+
const packageIssues = await analyzePackageArchitecture(
|
|
223
|
+
pkg,
|
|
224
|
+
context.workspacePath,
|
|
225
|
+
context.packages,
|
|
226
|
+
engine,
|
|
227
|
+
resolvedOptions,
|
|
228
|
+
)
|
|
229
|
+
issues.push(...packageIssues)
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return ok(filterIssues(issues, context.config))
|
|
233
|
+
},
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Analyzes a single package's architectural patterns.
|
|
239
|
+
*/
|
|
240
|
+
async function analyzePackageArchitecture(
|
|
241
|
+
pkg: WorkspacePackage,
|
|
242
|
+
workspacePath: string,
|
|
243
|
+
allPackages: readonly WorkspacePackage[],
|
|
244
|
+
engine: ReturnType<typeof createRuleEngine>,
|
|
245
|
+
options: typeof DEFAULT_OPTIONS & ArchitecturalAnalyzerOptions,
|
|
246
|
+
): Promise<Issue[]> {
|
|
247
|
+
const issues: Issue[] = []
|
|
248
|
+
const tsconfigPath = path.join(pkg.packagePath, 'tsconfig.json')
|
|
249
|
+
|
|
250
|
+
try {
|
|
251
|
+
const project = createProject({
|
|
252
|
+
tsConfigPath: tsconfigPath,
|
|
253
|
+
})
|
|
254
|
+
|
|
255
|
+
const sourceFiles = getAllSourceFiles(project)
|
|
256
|
+
|
|
257
|
+
for (const sourceFile of sourceFiles) {
|
|
258
|
+
const filePath = sourceFile.getFilePath()
|
|
259
|
+
|
|
260
|
+
if (shouldSkipFile(filePath, options.exclude)) {
|
|
261
|
+
continue
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const ruleContext: RuleContext = {
|
|
265
|
+
sourceFile,
|
|
266
|
+
pkg,
|
|
267
|
+
workspacePath,
|
|
268
|
+
allPackages,
|
|
269
|
+
layerConfig: options.layerConfig,
|
|
270
|
+
tsconfigPaths: options.tsconfigPaths,
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const result = await engine.evaluateFile(ruleContext)
|
|
274
|
+
|
|
275
|
+
if (result.success) {
|
|
276
|
+
issues.push(...result.data)
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
} catch {
|
|
280
|
+
issues.push(
|
|
281
|
+
createIssue({
|
|
282
|
+
id: 'architectural-analysis-error',
|
|
283
|
+
title: `Failed to analyze architecture in ${pkg.name}`,
|
|
284
|
+
description: `Could not parse TypeScript project for architectural analysis`,
|
|
285
|
+
severity: 'warning',
|
|
286
|
+
category: 'architecture',
|
|
287
|
+
location: {filePath: tsconfigPath},
|
|
288
|
+
}),
|
|
289
|
+
)
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
return issues
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Determines if a file should be skipped based on exclude patterns.
|
|
297
|
+
*/
|
|
298
|
+
function shouldSkipFile(filePath: string, excludePatterns: readonly string[] | undefined): boolean {
|
|
299
|
+
if (excludePatterns === undefined || excludePatterns.length === 0) {
|
|
300
|
+
return false
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
return matchAnyPattern(filePath, excludePatterns)
|
|
304
|
+
}
|
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BuildConfigAnalyzer - Validates build configuration (tsup) for consistency and best practices.
|
|
3
|
+
*
|
|
4
|
+
* Detects issues such as:
|
|
5
|
+
* - Missing build configuration for publishable packages
|
|
6
|
+
* - Inconsistent build settings
|
|
7
|
+
* - Missing entry points
|
|
8
|
+
* - Format mismatches with package.json
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type {ParsedPackageJson} from '../parser/config-parser'
|
|
12
|
+
import type {WorkspacePackage} from '../scanner/workspace-scanner'
|
|
13
|
+
import type {Issue, IssueLocation} from '../types/index'
|
|
14
|
+
import type {Result} from '../types/result'
|
|
15
|
+
import type {AnalysisContext, Analyzer, AnalyzerError, AnalyzerMetadata} from './analyzer'
|
|
16
|
+
|
|
17
|
+
import fs from 'node:fs/promises'
|
|
18
|
+
import path from 'node:path'
|
|
19
|
+
|
|
20
|
+
import {ok} from '@bfra.me/es/result'
|
|
21
|
+
|
|
22
|
+
import {createIssue, filterIssues} from './analyzer'
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Configuration options specific to BuildConfigAnalyzer.
|
|
26
|
+
*/
|
|
27
|
+
export interface BuildConfigAnalyzerOptions {
|
|
28
|
+
/** Whether to require build config for packages with build scripts */
|
|
29
|
+
readonly requireConfig?: boolean
|
|
30
|
+
/** Expected config file pattern (e.g., 'tsup.config.ts') */
|
|
31
|
+
readonly expectedConfigFile?: string
|
|
32
|
+
/** Whether to check for DTS generation */
|
|
33
|
+
readonly requireDts?: boolean
|
|
34
|
+
/** Package names exempt from checks */
|
|
35
|
+
readonly exemptPackages?: readonly string[]
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const DEFAULT_OPTIONS: BuildConfigAnalyzerOptions = {
|
|
39
|
+
requireConfig: true,
|
|
40
|
+
expectedConfigFile: 'tsup.config.ts',
|
|
41
|
+
requireDts: true,
|
|
42
|
+
exemptPackages: [],
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const METADATA: AnalyzerMetadata = {
|
|
46
|
+
id: 'build-config',
|
|
47
|
+
name: 'Build Config Analyzer',
|
|
48
|
+
description: 'Validates build configuration (tsup) for consistency and best practices',
|
|
49
|
+
categories: ['configuration'],
|
|
50
|
+
defaultSeverity: 'warning',
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const BUILD_CONFIG_FILES = [
|
|
54
|
+
'tsup.config.ts',
|
|
55
|
+
'tsup.config.mts',
|
|
56
|
+
'tsup.config.cts',
|
|
57
|
+
'tsup.config.js',
|
|
58
|
+
'tsup.config.mjs',
|
|
59
|
+
'tsup.config.cjs',
|
|
60
|
+
'rollup.config.ts',
|
|
61
|
+
'rollup.config.js',
|
|
62
|
+
'rollup.config.mjs',
|
|
63
|
+
'vite.config.ts',
|
|
64
|
+
'vite.config.js',
|
|
65
|
+
'vite.config.mjs',
|
|
66
|
+
'esbuild.config.ts',
|
|
67
|
+
'esbuild.config.js',
|
|
68
|
+
] as const
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Creates a BuildConfigAnalyzer instance.
|
|
72
|
+
*/
|
|
73
|
+
export function createBuildConfigAnalyzer(options: BuildConfigAnalyzerOptions = {}): Analyzer {
|
|
74
|
+
const resolvedOptions = {...DEFAULT_OPTIONS, ...options}
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
metadata: METADATA,
|
|
78
|
+
analyze: async (context: AnalysisContext): Promise<Result<readonly Issue[], AnalyzerError>> => {
|
|
79
|
+
const issues: Issue[] = []
|
|
80
|
+
|
|
81
|
+
for (const pkg of context.packages) {
|
|
82
|
+
if (isExemptPackage(pkg.name, resolvedOptions.exemptPackages)) {
|
|
83
|
+
continue
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const packageIssues = await analyzePackageBuild(pkg, resolvedOptions)
|
|
87
|
+
issues.push(...packageIssues)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return ok(filterIssues(issues, context.config))
|
|
91
|
+
},
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function isExemptPackage(name: string, exemptPackages: readonly string[] | undefined): boolean {
|
|
96
|
+
return exemptPackages?.includes(name) ?? false
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function createLocation(filePath: string): IssueLocation {
|
|
100
|
+
return {filePath}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async function analyzePackageBuild(
|
|
104
|
+
pkg: WorkspacePackage,
|
|
105
|
+
options: BuildConfigAnalyzerOptions,
|
|
106
|
+
): Promise<Issue[]> {
|
|
107
|
+
const issues: Issue[] = []
|
|
108
|
+
const pkgJson = pkg.packageJson as ParsedPackageJson
|
|
109
|
+
|
|
110
|
+
// Check if package has a build script
|
|
111
|
+
const hasBuildScript = pkgJson.scripts?.build !== undefined
|
|
112
|
+
|
|
113
|
+
// Find existing build config
|
|
114
|
+
const configResult = await findBuildConfig(pkg.packagePath)
|
|
115
|
+
|
|
116
|
+
// Check if build config is needed but missing
|
|
117
|
+
if (options.requireConfig && hasBuildScript && !configResult.found) {
|
|
118
|
+
issues.push(
|
|
119
|
+
createIssue({
|
|
120
|
+
id: 'build-no-config',
|
|
121
|
+
title: 'Missing build configuration file',
|
|
122
|
+
description: `Package "${pkg.name}" has a build script but no build configuration file`,
|
|
123
|
+
severity: 'info',
|
|
124
|
+
category: 'configuration',
|
|
125
|
+
location: createLocation(path.join(pkg.packagePath, 'package.json')),
|
|
126
|
+
suggestion: `Create "${options.expectedConfigFile ?? 'tsup.config.ts'}" for explicit build configuration`,
|
|
127
|
+
}),
|
|
128
|
+
)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Check if expected config file is used
|
|
132
|
+
if (
|
|
133
|
+
configResult.found &&
|
|
134
|
+
options.expectedConfigFile !== undefined &&
|
|
135
|
+
configResult.fileName !== options.expectedConfigFile
|
|
136
|
+
) {
|
|
137
|
+
issues.push(
|
|
138
|
+
createIssue({
|
|
139
|
+
id: 'build-unexpected-config-file',
|
|
140
|
+
title: 'Unexpected build config file',
|
|
141
|
+
description: `Package "${pkg.name}" uses "${configResult.fileName}" instead of "${options.expectedConfigFile}"`,
|
|
142
|
+
severity: 'info',
|
|
143
|
+
category: 'configuration',
|
|
144
|
+
location: createLocation(configResult.filePath),
|
|
145
|
+
suggestion: `Consider using "${options.expectedConfigFile}" for consistency`,
|
|
146
|
+
}),
|
|
147
|
+
)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Analyze tsup config content
|
|
151
|
+
if (configResult.found && configResult.isTsup) {
|
|
152
|
+
const contentIssues = await analyzeTsupConfigContent(pkg, configResult.filePath, options)
|
|
153
|
+
issues.push(...contentIssues)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Check package.json and build output consistency
|
|
157
|
+
issues.push(...checkBuildOutputConsistency(pkg, pkgJson))
|
|
158
|
+
|
|
159
|
+
return issues
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
interface BuildConfigResult {
|
|
163
|
+
found: boolean
|
|
164
|
+
fileName: string
|
|
165
|
+
filePath: string
|
|
166
|
+
isTsup: boolean
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async function findBuildConfig(packagePath: string): Promise<BuildConfigResult> {
|
|
170
|
+
for (const configFile of BUILD_CONFIG_FILES) {
|
|
171
|
+
const configPath = path.join(packagePath, configFile)
|
|
172
|
+
if (await fileExists(configPath)) {
|
|
173
|
+
return {
|
|
174
|
+
found: true,
|
|
175
|
+
fileName: configFile,
|
|
176
|
+
filePath: configPath,
|
|
177
|
+
isTsup: configFile.startsWith('tsup'),
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return {
|
|
183
|
+
found: false,
|
|
184
|
+
fileName: '',
|
|
185
|
+
filePath: '',
|
|
186
|
+
isTsup: false,
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
async function fileExists(filePath: string): Promise<boolean> {
|
|
191
|
+
try {
|
|
192
|
+
await fs.access(filePath)
|
|
193
|
+
return true
|
|
194
|
+
} catch {
|
|
195
|
+
return false
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
async function analyzeTsupConfigContent(
|
|
200
|
+
pkg: WorkspacePackage,
|
|
201
|
+
configPath: string,
|
|
202
|
+
options: BuildConfigAnalyzerOptions,
|
|
203
|
+
): Promise<Issue[]> {
|
|
204
|
+
const issues: Issue[] = []
|
|
205
|
+
|
|
206
|
+
let content: string
|
|
207
|
+
try {
|
|
208
|
+
content = await fs.readFile(configPath, 'utf-8')
|
|
209
|
+
} catch {
|
|
210
|
+
return issues
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Check for DTS generation in TypeScript packages
|
|
214
|
+
if (options.requireDts && pkg.hasTsConfig) {
|
|
215
|
+
const hasDts = content.includes('dts') && content.includes('true')
|
|
216
|
+
if (!hasDts) {
|
|
217
|
+
issues.push(
|
|
218
|
+
createIssue({
|
|
219
|
+
id: 'build-no-dts',
|
|
220
|
+
title: 'Build config missing DTS generation',
|
|
221
|
+
description: `Package "${pkg.name}" is TypeScript but build config may not generate declaration files`,
|
|
222
|
+
severity: 'warning',
|
|
223
|
+
category: 'configuration',
|
|
224
|
+
location: createLocation(configPath),
|
|
225
|
+
suggestion: 'Add "dts: true" to tsup config for TypeScript declaration file generation',
|
|
226
|
+
}),
|
|
227
|
+
)
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Check for entry point configuration
|
|
232
|
+
const hasEntry = content.includes('entry')
|
|
233
|
+
if (!hasEntry) {
|
|
234
|
+
issues.push(
|
|
235
|
+
createIssue({
|
|
236
|
+
id: 'build-no-entry',
|
|
237
|
+
title: 'Build config missing explicit entry points',
|
|
238
|
+
description: `Package "${pkg.name}" build config does not specify explicit entry points`,
|
|
239
|
+
severity: 'info',
|
|
240
|
+
category: 'configuration',
|
|
241
|
+
location: createLocation(configPath),
|
|
242
|
+
suggestion: 'Add explicit "entry" configuration for predictable builds',
|
|
243
|
+
}),
|
|
244
|
+
)
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Check for format specification
|
|
248
|
+
const hasFormat = content.includes('format')
|
|
249
|
+
if (!hasFormat) {
|
|
250
|
+
issues.push(
|
|
251
|
+
createIssue({
|
|
252
|
+
id: 'build-no-format',
|
|
253
|
+
title: 'Build config missing format specification',
|
|
254
|
+
description: `Package "${pkg.name}" build config does not specify output format(s)`,
|
|
255
|
+
severity: 'info',
|
|
256
|
+
category: 'configuration',
|
|
257
|
+
location: createLocation(configPath),
|
|
258
|
+
suggestion: 'Add explicit "format" configuration (e.g., ["esm", "cjs"]) for clarity',
|
|
259
|
+
}),
|
|
260
|
+
)
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Check for clean option
|
|
264
|
+
const hasClean = content.includes('clean')
|
|
265
|
+
if (!hasClean) {
|
|
266
|
+
issues.push(
|
|
267
|
+
createIssue({
|
|
268
|
+
id: 'build-no-clean',
|
|
269
|
+
title: 'Build config missing clean option',
|
|
270
|
+
description: `Package "${pkg.name}" build config does not specify clean option`,
|
|
271
|
+
severity: 'info',
|
|
272
|
+
category: 'configuration',
|
|
273
|
+
location: createLocation(configPath),
|
|
274
|
+
suggestion: 'Add "clean: true" to ensure stale files are removed before building',
|
|
275
|
+
}),
|
|
276
|
+
)
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
return issues
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function checkBuildOutputConsistency(pkg: WorkspacePackage, pkgJson: ParsedPackageJson): Issue[] {
|
|
283
|
+
const issues: Issue[] = []
|
|
284
|
+
|
|
285
|
+
// Check if main/module point to expected build output directories
|
|
286
|
+
const commonOutputDirs = ['lib', 'dist', 'build', 'out']
|
|
287
|
+
|
|
288
|
+
if (pkgJson.main !== undefined) {
|
|
289
|
+
const mainDir = pkgJson.main.split('/')[0]
|
|
290
|
+
if (mainDir !== undefined && !commonOutputDirs.includes(mainDir) && !mainDir.startsWith('.')) {
|
|
291
|
+
issues.push(
|
|
292
|
+
createIssue({
|
|
293
|
+
id: 'build-unusual-main-path',
|
|
294
|
+
title: 'Unusual main entry path',
|
|
295
|
+
description: `Package "${pkg.name}" main entry "${pkgJson.main}" uses non-standard output directory`,
|
|
296
|
+
severity: 'info',
|
|
297
|
+
category: 'configuration',
|
|
298
|
+
location: createLocation(pkg.packageJsonPath),
|
|
299
|
+
suggestion: 'Consider using standard output directories like "lib", "dist", or "build"',
|
|
300
|
+
metadata: {main: pkgJson.main},
|
|
301
|
+
}),
|
|
302
|
+
)
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Check for ESM/CJS format mismatch
|
|
307
|
+
if (pkgJson.type === 'module' && pkgJson.main !== undefined) {
|
|
308
|
+
const mainExt = path.extname(pkgJson.main)
|
|
309
|
+
|
|
310
|
+
// .js in ESM package is treated as ESM - check if module field exists for CJS fallback
|
|
311
|
+
// .cjs is correct for ESM packages providing CJS fallback (no issue)
|
|
312
|
+
const needsCjsFallbackCheck = mainExt === '.js'
|
|
313
|
+
const hasCjsFallback = pkgJson.module !== undefined || pkgJson.exports !== undefined
|
|
314
|
+
|
|
315
|
+
if (needsCjsFallbackCheck && !hasCjsFallback) {
|
|
316
|
+
issues.push(
|
|
317
|
+
createIssue({
|
|
318
|
+
id: 'build-esm-no-cjs-fallback',
|
|
319
|
+
title: 'ESM package without CJS fallback',
|
|
320
|
+
description: `Package "${pkg.name}" is ESM but has no CJS fallback for older tools`,
|
|
321
|
+
severity: 'info',
|
|
322
|
+
category: 'configuration',
|
|
323
|
+
location: createLocation(pkg.packageJsonPath),
|
|
324
|
+
suggestion:
|
|
325
|
+
'Consider adding exports field with both ESM and CJS formats for compatibility',
|
|
326
|
+
}),
|
|
327
|
+
)
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
return issues
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
export {METADATA as buildConfigAnalyzerMetadata}
|