@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,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}