@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,340 @@
1
+ /**
2
+ * Import statement extractor for analyzing module dependencies.
3
+ *
4
+ * Extracts static imports, dynamic imports, and require() calls from TypeScript/JavaScript
5
+ * source files for dependency analysis.
6
+ *
7
+ * Uses ts-morph types (SourceFile, SyntaxKind) which are provided as a peer dependency
8
+ * via @bfra.me/doc-sync's transitive dependency on ts-morph.
9
+ */
10
+
11
+ import type {SourceFile} from 'ts-morph'
12
+
13
+ import path from 'node:path'
14
+
15
+ import {SyntaxKind} from 'ts-morph'
16
+
17
+ /**
18
+ * Type of import statement.
19
+ */
20
+ export type ImportType = 'static' | 'dynamic' | 'require' | 'type-only'
21
+
22
+ /**
23
+ * Represents an extracted import statement.
24
+ */
25
+ export interface ExtractedImport {
26
+ /** The module specifier (path or package name) */
27
+ readonly moduleSpecifier: string
28
+ /** Type of import */
29
+ readonly type: ImportType
30
+ /** Imported names (for named imports) */
31
+ readonly importedNames?: readonly string[]
32
+ /** Default import name (if present) */
33
+ readonly defaultImport?: string
34
+ /** Namespace import name (if present) */
35
+ readonly namespaceImport?: string
36
+ /** Whether this is a side-effect only import */
37
+ readonly isSideEffectOnly: boolean
38
+ /** Whether the import is from a relative path */
39
+ readonly isRelative: boolean
40
+ /** Whether the import is from a workspace package */
41
+ readonly isWorkspacePackage: boolean
42
+ /** Line number in the source file */
43
+ readonly line: number
44
+ /** Column number in the source file */
45
+ readonly column: number
46
+ }
47
+
48
+ /**
49
+ * Result of extracting imports from a source file.
50
+ */
51
+ export interface ImportExtractionResult {
52
+ /** All extracted imports */
53
+ readonly imports: readonly ExtractedImport[]
54
+ /** The source file path */
55
+ readonly filePath: string
56
+ /** External package dependencies (non-relative, non-workspace) */
57
+ readonly externalDependencies: readonly string[]
58
+ /** Workspace package dependencies */
59
+ readonly workspaceDependencies: readonly string[]
60
+ /** Relative imports within the package */
61
+ readonly relativeImports: readonly string[]
62
+ }
63
+
64
+ /**
65
+ * Options for import extraction.
66
+ */
67
+ export interface ImportExtractorOptions {
68
+ /** Workspace package name prefixes (e.g., ['@bfra.me/']) */
69
+ readonly workspacePrefixes?: readonly string[]
70
+ /** Include type-only imports */
71
+ readonly includeTypeImports?: boolean
72
+ /** Include dynamic imports */
73
+ readonly includeDynamicImports?: boolean
74
+ /** Include require() calls */
75
+ readonly includeRequireCalls?: boolean
76
+ }
77
+
78
+ const DEFAULT_OPTIONS: Required<ImportExtractorOptions> = {
79
+ workspacePrefixes: ['@bfra.me/'],
80
+ includeTypeImports: true,
81
+ includeDynamicImports: true,
82
+ includeRequireCalls: true,
83
+ }
84
+
85
+ /**
86
+ * Extracts all imports from a TypeScript/JavaScript source file.
87
+ *
88
+ * @example
89
+ * ```ts
90
+ * const project = createProject()
91
+ * const sourceFile = project.addSourceFileAtPath('./src/index.ts')
92
+ * const result = extractImports(sourceFile)
93
+ *
94
+ * for (const imp of result.imports) {
95
+ * console.log(`Import: ${imp.moduleSpecifier} (${imp.type})`)
96
+ * }
97
+ * ```
98
+ */
99
+ export function extractImports(
100
+ sourceFile: SourceFile,
101
+ options?: ImportExtractorOptions,
102
+ ): ImportExtractionResult {
103
+ const opts = {...DEFAULT_OPTIONS, ...options}
104
+ const imports: ExtractedImport[] = []
105
+ const filePath = sourceFile.getFilePath()
106
+
107
+ // Extract static import declarations
108
+ for (const importDecl of sourceFile.getImportDeclarations()) {
109
+ const moduleSpecifier = importDecl.getModuleSpecifierValue()
110
+ const isTypeOnly = importDecl.isTypeOnly()
111
+
112
+ if (isTypeOnly && !opts.includeTypeImports) {
113
+ continue
114
+ }
115
+
116
+ const {line, column} = sourceFile.getLineAndColumnAtPos(importDecl.getStart())
117
+
118
+ const importedNames: string[] = []
119
+ let defaultImport: string | undefined
120
+ let namespaceImport: string | undefined
121
+
122
+ const defaultImportNode = importDecl.getDefaultImport()
123
+ if (defaultImportNode !== undefined) {
124
+ defaultImport = defaultImportNode.getText()
125
+ }
126
+
127
+ const namespaceImportNode = importDecl.getNamespaceImport()
128
+ if (namespaceImportNode !== undefined) {
129
+ namespaceImport = namespaceImportNode.getText()
130
+ }
131
+
132
+ for (const namedImport of importDecl.getNamedImports()) {
133
+ importedNames.push(namedImport.getName())
134
+ }
135
+
136
+ const isSideEffectOnly =
137
+ defaultImport === undefined && namespaceImport === undefined && importedNames.length === 0
138
+
139
+ imports.push({
140
+ moduleSpecifier,
141
+ type: isTypeOnly ? 'type-only' : 'static',
142
+ importedNames: importedNames.length > 0 ? importedNames : undefined,
143
+ defaultImport,
144
+ namespaceImport,
145
+ isSideEffectOnly,
146
+ isRelative: isRelativeImport(moduleSpecifier),
147
+ isWorkspacePackage: isWorkspacePackageImport(moduleSpecifier, opts.workspacePrefixes),
148
+ line,
149
+ column,
150
+ })
151
+ }
152
+
153
+ // Extract dynamic imports
154
+ if (opts.includeDynamicImports) {
155
+ sourceFile.forEachDescendant(node => {
156
+ if (node.getKind() === SyntaxKind.CallExpression) {
157
+ const callExpr = node.asKind(SyntaxKind.CallExpression)
158
+ if (callExpr === undefined) return
159
+
160
+ const expr = callExpr.getExpression()
161
+ if (expr.getKind() === SyntaxKind.ImportKeyword) {
162
+ const args = callExpr.getArguments()
163
+ if (args.length > 0) {
164
+ const firstArg = args[0]
165
+ if (firstArg !== undefined && firstArg.getKind() === SyntaxKind.StringLiteral) {
166
+ const stringLiteral = firstArg.asKind(SyntaxKind.StringLiteral)
167
+ if (stringLiteral !== undefined) {
168
+ const moduleSpecifier = stringLiteral.getLiteralValue()
169
+ const {line: dynamicLine, column: dynamicColumn} = sourceFile.getLineAndColumnAtPos(
170
+ node.getStart(),
171
+ )
172
+
173
+ imports.push({
174
+ moduleSpecifier,
175
+ type: 'dynamic',
176
+ isSideEffectOnly: false,
177
+ isRelative: isRelativeImport(moduleSpecifier),
178
+ isWorkspacePackage: isWorkspacePackageImport(
179
+ moduleSpecifier,
180
+ opts.workspacePrefixes,
181
+ ),
182
+ line: dynamicLine,
183
+ column: dynamicColumn,
184
+ })
185
+ }
186
+ }
187
+ }
188
+ }
189
+ }
190
+ })
191
+ }
192
+
193
+ // Extract require() calls
194
+ if (opts.includeRequireCalls) {
195
+ sourceFile.forEachDescendant(node => {
196
+ if (node.getKind() === SyntaxKind.CallExpression) {
197
+ const callExpr = node.asKind(SyntaxKind.CallExpression)
198
+ if (callExpr === undefined) return
199
+
200
+ const expr = callExpr.getExpression()
201
+ if (expr.getKind() === SyntaxKind.Identifier && expr.getText() === 'require') {
202
+ const args = callExpr.getArguments()
203
+ if (args.length > 0) {
204
+ const firstArg = args[0]
205
+ if (firstArg !== undefined && firstArg.getKind() === SyntaxKind.StringLiteral) {
206
+ const stringLiteral = firstArg.asKind(SyntaxKind.StringLiteral)
207
+ if (stringLiteral !== undefined) {
208
+ const moduleSpecifier = stringLiteral.getLiteralValue()
209
+ const {line: requireLine, column: requireColumn} = sourceFile.getLineAndColumnAtPos(
210
+ node.getStart(),
211
+ )
212
+
213
+ imports.push({
214
+ moduleSpecifier,
215
+ type: 'require',
216
+ isSideEffectOnly: false,
217
+ isRelative: isRelativeImport(moduleSpecifier),
218
+ isWorkspacePackage: isWorkspacePackageImport(
219
+ moduleSpecifier,
220
+ opts.workspacePrefixes,
221
+ ),
222
+ line: requireLine,
223
+ column: requireColumn,
224
+ })
225
+ }
226
+ }
227
+ }
228
+ }
229
+ }
230
+ })
231
+ }
232
+
233
+ // Categorize imports
234
+ const externalDependencies: string[] = []
235
+ const workspaceDependencies: string[] = []
236
+ const relativeImports: string[] = []
237
+
238
+ for (const imp of imports) {
239
+ const pkgName = getPackageNameFromSpecifier(imp.moduleSpecifier)
240
+
241
+ if (imp.isRelative) {
242
+ if (!relativeImports.includes(imp.moduleSpecifier)) {
243
+ relativeImports.push(imp.moduleSpecifier)
244
+ }
245
+ } else if (imp.isWorkspacePackage) {
246
+ if (!workspaceDependencies.includes(pkgName)) {
247
+ workspaceDependencies.push(pkgName)
248
+ }
249
+ } else if (!externalDependencies.includes(pkgName)) {
250
+ externalDependencies.push(pkgName)
251
+ }
252
+ }
253
+
254
+ return {
255
+ imports,
256
+ filePath,
257
+ externalDependencies,
258
+ workspaceDependencies,
259
+ relativeImports,
260
+ }
261
+ }
262
+
263
+ /**
264
+ * Checks if a module specifier is a relative import.
265
+ */
266
+ export function isRelativeImport(moduleSpecifier: string): boolean {
267
+ return moduleSpecifier.startsWith('.') || moduleSpecifier.startsWith('/')
268
+ }
269
+
270
+ /**
271
+ * Checks if a module specifier is a workspace package import.
272
+ */
273
+ export function isWorkspacePackageImport(
274
+ moduleSpecifier: string,
275
+ workspacePrefixes: readonly string[],
276
+ ): boolean {
277
+ return workspacePrefixes.some(prefix => moduleSpecifier.startsWith(prefix))
278
+ }
279
+
280
+ /**
281
+ * Extracts the package name from a module specifier.
282
+ *
283
+ * For scoped packages like '@scope/pkg/path', returns '@scope/pkg'.
284
+ * For unscoped packages like 'lodash/fp', returns 'lodash'.
285
+ */
286
+ export function getPackageNameFromSpecifier(moduleSpecifier: string): string {
287
+ if (isRelativeImport(moduleSpecifier)) {
288
+ return moduleSpecifier
289
+ }
290
+
291
+ // Scoped package
292
+ if (moduleSpecifier.startsWith('@')) {
293
+ const parts = moduleSpecifier.split('/')
294
+ if (parts.length >= 2) {
295
+ return `${parts[0]}/${parts[1]}`
296
+ }
297
+ return moduleSpecifier
298
+ }
299
+
300
+ // Unscoped package
301
+ const slashIndex = moduleSpecifier.indexOf('/')
302
+ if (slashIndex > 0) {
303
+ return moduleSpecifier.slice(0, slashIndex)
304
+ }
305
+
306
+ return moduleSpecifier
307
+ }
308
+
309
+ /**
310
+ * Resolves a relative import to an absolute file path.
311
+ */
312
+ export function resolveRelativeImport(fromFile: string, moduleSpecifier: string): string {
313
+ const fromDir = path.dirname(fromFile)
314
+ return path.resolve(fromDir, moduleSpecifier)
315
+ }
316
+
317
+ /**
318
+ * Gets unique package dependencies from imports.
319
+ */
320
+ export function getUniqueDependencies(results: readonly ImportExtractionResult[]): {
321
+ readonly external: readonly string[]
322
+ readonly workspace: readonly string[]
323
+ } {
324
+ const external = new Set<string>()
325
+ const workspace = new Set<string>()
326
+
327
+ for (const result of results) {
328
+ for (const dep of result.externalDependencies) {
329
+ external.add(dep)
330
+ }
331
+ for (const dep of result.workspaceDependencies) {
332
+ workspace.add(dep)
333
+ }
334
+ }
335
+
336
+ return {
337
+ external: [...external].sort(),
338
+ workspace: [...workspace].sort(),
339
+ }
340
+ }
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Parser module exports.
3
+ *
4
+ * Provides unified access to all parsing utilities for TypeScript source files,
5
+ * configuration files, and import extraction.
6
+ */
7
+
8
+ // Configuration parser exports
9
+ export {
10
+ getAllDependencies,
11
+ parsePackageJson,
12
+ parsePackageJsonContent,
13
+ parseTsConfig,
14
+ parseTsConfigContent,
15
+ resolveTsConfigExtends,
16
+ } from './config-parser'
17
+ export type {
18
+ ConfigError,
19
+ ConfigErrorCode,
20
+ ParsedPackageJson,
21
+ ParsedTsConfig,
22
+ TsCompilerOptions,
23
+ TsProjectReference,
24
+ } from './config-parser'
25
+
26
+ // Import extractor exports
27
+ export {
28
+ extractImports,
29
+ getPackageNameFromSpecifier,
30
+ getUniqueDependencies,
31
+ isRelativeImport,
32
+ isWorkspacePackageImport,
33
+ resolveRelativeImport,
34
+ } from './import-extractor'
35
+ export type {
36
+ ExtractedImport,
37
+ ImportExtractionResult,
38
+ ImportExtractorOptions,
39
+ ImportType,
40
+ } from './import-extractor'
41
+
42
+ // TypeScript parser exports
43
+ export {
44
+ createProject,
45
+ getAllSourceFiles,
46
+ getSourceFile,
47
+ isJavaScriptFile,
48
+ isSourceFile,
49
+ isTypeScriptFile,
50
+ parseSourceContent,
51
+ parseSourceFile,
52
+ parseSourceFiles,
53
+ } from './typescript-parser'
54
+ export type {ParseError, ParseErrorCode, TypeScriptParserOptions} from './typescript-parser'
@@ -0,0 +1,95 @@
1
+ /**
2
+ * TypeScript AST parser utilities for workspace analysis.
3
+ *
4
+ * Re-exports core parsing utilities from @bfra.me/doc-sync/parsers and provides
5
+ * additional workspace-analyzer-specific helpers for multi-file analysis.
6
+ */
7
+
8
+ import type {ParseError} from '@bfra.me/doc-sync/types'
9
+ import type {Result} from '@bfra.me/es/result'
10
+ import type {Project, SourceFile} from 'ts-morph'
11
+
12
+ import {
13
+ createProject as createDocSyncProject,
14
+ parseSourceFile as parseDocSyncSourceFile,
15
+ } from '@bfra.me/doc-sync/parsers'
16
+ import {err, ok} from '@bfra.me/es/result'
17
+
18
+ // Re-export core TypeScript parsing utilities from @bfra.me/doc-sync
19
+ export {createProject, parseSourceContent, parseSourceFile} from '@bfra.me/doc-sync/parsers'
20
+
21
+ export type {TypeScriptParserOptions} from '@bfra.me/doc-sync/parsers'
22
+
23
+ // Re-export ParseError type for consumers
24
+ export type {ParseError, ParseErrorCode} from '@bfra.me/doc-sync/types'
25
+
26
+ /**
27
+ * Gets the source file at a path, creating the project if needed.
28
+ *
29
+ * Convenience function for one-off file parsing without manually managing a project.
30
+ */
31
+ export function getSourceFile(
32
+ filePath: string,
33
+ options?: {tsConfigPath?: string; compilerOptions?: Record<string, unknown>},
34
+ ): Result<SourceFile, ParseError> {
35
+ const project = createDocSyncProject(options)
36
+ return parseDocSyncSourceFile(project, filePath)
37
+ }
38
+
39
+ /**
40
+ * Parses multiple source files into a single project.
41
+ *
42
+ * More efficient than parsing files individually when analyzing many files,
43
+ * as the project can share type resolution and compiler settings.
44
+ */
45
+ export function parseSourceFiles(
46
+ filePaths: readonly string[],
47
+ options?: {tsConfigPath?: string; compilerOptions?: Record<string, unknown>},
48
+ ): Result<Project, ParseError> {
49
+ const project = createDocSyncProject(options)
50
+
51
+ for (const filePath of filePaths) {
52
+ try {
53
+ project.addSourceFileAtPath(filePath)
54
+ } catch (error) {
55
+ return err({
56
+ code: 'FILE_NOT_FOUND',
57
+ message: `Failed to add source file: ${filePath}`,
58
+ filePath,
59
+ cause: error,
60
+ })
61
+ }
62
+ }
63
+
64
+ return ok(project)
65
+ }
66
+
67
+ /**
68
+ * Gets all source files from a project.
69
+ */
70
+ export function getAllSourceFiles(project: Project): readonly SourceFile[] {
71
+ return project.getSourceFiles()
72
+ }
73
+
74
+ /**
75
+ * Checks if a file path represents a TypeScript file.
76
+ */
77
+ export function isTypeScriptFile(filePath: string): boolean {
78
+ const ext = filePath.toLowerCase()
79
+ return ext.endsWith('.ts') || ext.endsWith('.tsx') || ext.endsWith('.mts') || ext.endsWith('.cts')
80
+ }
81
+
82
+ /**
83
+ * Checks if a file path represents a JavaScript file.
84
+ */
85
+ export function isJavaScriptFile(filePath: string): boolean {
86
+ const ext = filePath.toLowerCase()
87
+ return ext.endsWith('.js') || ext.endsWith('.jsx') || ext.endsWith('.mjs') || ext.endsWith('.cjs')
88
+ }
89
+
90
+ /**
91
+ * Checks if a file path is a parseable source file (TypeScript or JavaScript).
92
+ */
93
+ export function isSourceFile(filePath: string): boolean {
94
+ return isTypeScriptFile(filePath) || isJavaScriptFile(filePath)
95
+ }