@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,491 @@
1
+ /**
2
+ * Configuration file parser for package.json, tsconfig.json, and other config files.
3
+ *
4
+ * Provides utilities for parsing and extracting information from various
5
+ * configuration files used in TypeScript/JavaScript projects.
6
+ */
7
+
8
+ import type {Result} from '../types/result'
9
+
10
+ import fs from 'node:fs/promises'
11
+ import path from 'node:path'
12
+
13
+ import {err, ok} from '@bfra.me/es/result'
14
+
15
+ /**
16
+ * Error codes for configuration parsing.
17
+ */
18
+ export type ConfigErrorCode = 'FILE_NOT_FOUND' | 'INVALID_JSON' | 'INVALID_CONFIG' | 'READ_ERROR'
19
+
20
+ /**
21
+ * Error that occurred during configuration parsing.
22
+ */
23
+ export interface ConfigError {
24
+ /** Error code for programmatic handling */
25
+ readonly code: ConfigErrorCode
26
+ /** Human-readable error message */
27
+ readonly message: string
28
+ /** Path to the config file */
29
+ readonly filePath: string
30
+ /** Underlying cause */
31
+ readonly cause?: unknown
32
+ }
33
+
34
+ /**
35
+ * Parsed package.json structure with relevant fields for analysis.
36
+ */
37
+ export interface ParsedPackageJson {
38
+ /** Package name */
39
+ readonly name: string
40
+ /** Package version */
41
+ readonly version: string
42
+ /** Package description */
43
+ readonly description?: string
44
+ /** Main entry point */
45
+ readonly main?: string
46
+ /** Module entry point (ESM) */
47
+ readonly module?: string
48
+ /** Types entry point */
49
+ readonly types?: string
50
+ /** Exports map */
51
+ readonly exports?: Record<string, unknown>
52
+ /** Dependencies */
53
+ readonly dependencies?: Readonly<Record<string, string>>
54
+ /** Development dependencies */
55
+ readonly devDependencies?: Readonly<Record<string, string>>
56
+ /** Peer dependencies */
57
+ readonly peerDependencies?: Readonly<Record<string, string>>
58
+ /** Optional dependencies */
59
+ readonly optionalDependencies?: Readonly<Record<string, string>>
60
+ /** Package type (module or commonjs) */
61
+ readonly type?: 'module' | 'commonjs'
62
+ /** Scripts */
63
+ readonly scripts?: Readonly<Record<string, string>>
64
+ /** Files to include in package */
65
+ readonly files?: readonly string[]
66
+ /** Raw package.json data */
67
+ readonly raw: Readonly<Record<string, unknown>>
68
+ }
69
+
70
+ /**
71
+ * Parsed tsconfig.json structure with relevant fields for analysis.
72
+ */
73
+ export interface ParsedTsConfig {
74
+ /** Extends from another config */
75
+ readonly extends?: string | readonly string[]
76
+ /** Compiler options */
77
+ readonly compilerOptions?: TsCompilerOptions
78
+ /** Include patterns */
79
+ readonly include?: readonly string[]
80
+ /** Exclude patterns */
81
+ readonly exclude?: readonly string[]
82
+ /** Project references */
83
+ readonly references?: readonly TsProjectReference[]
84
+ /** File path of the config */
85
+ readonly filePath: string
86
+ /** Raw tsconfig data */
87
+ readonly raw: Readonly<Record<string, unknown>>
88
+ }
89
+
90
+ /**
91
+ * TypeScript compiler options subset relevant for analysis.
92
+ */
93
+ export interface TsCompilerOptions {
94
+ /** Target ECMAScript version */
95
+ readonly target?: string
96
+ /** Module system */
97
+ readonly module?: string
98
+ /** Module resolution strategy */
99
+ readonly moduleResolution?: string
100
+ /** Path mappings */
101
+ readonly paths?: Readonly<Record<string, readonly string[]>>
102
+ /** Base URL for path resolution */
103
+ readonly baseUrl?: string
104
+ /** Root directory */
105
+ readonly rootDir?: string
106
+ /** Output directory */
107
+ readonly outDir?: string
108
+ /** Strict mode */
109
+ readonly strict?: boolean
110
+ /** Declaration files */
111
+ readonly declaration?: boolean
112
+ /** Source maps */
113
+ readonly sourceMap?: boolean
114
+ /** ESM interop */
115
+ readonly esModuleInterop?: boolean
116
+ /** Allow synthetic default imports */
117
+ readonly allowSyntheticDefaultImports?: boolean
118
+ /** Skip library check */
119
+ readonly skipLibCheck?: boolean
120
+ /** Resolve JSON modules */
121
+ readonly resolveJsonModule?: boolean
122
+ /** Isolated modules */
123
+ readonly isolatedModules?: boolean
124
+ }
125
+
126
+ /**
127
+ * TypeScript project reference.
128
+ */
129
+ export interface TsProjectReference {
130
+ /** Path to referenced project */
131
+ readonly path: string
132
+ }
133
+
134
+ /**
135
+ * Parses a package.json file.
136
+ *
137
+ * @example
138
+ * ```ts
139
+ * const result = await parsePackageJson('/path/to/package.json')
140
+ * if (result.success) {
141
+ * console.log(`Package: ${result.data.name}@${result.data.version}`)
142
+ * }
143
+ * ```
144
+ */
145
+ export async function parsePackageJson(
146
+ packageJsonPath: string,
147
+ ): Promise<Result<ParsedPackageJson, ConfigError>> {
148
+ const normalizedPath = packageJsonPath.endsWith('package.json')
149
+ ? packageJsonPath
150
+ : path.join(packageJsonPath, 'package.json')
151
+
152
+ let content: string
153
+ try {
154
+ content = await fs.readFile(normalizedPath, 'utf-8')
155
+ } catch (error) {
156
+ if (isNodeError(error) && error.code === 'ENOENT') {
157
+ return err({
158
+ code: 'FILE_NOT_FOUND',
159
+ message: `package.json not found: ${normalizedPath}`,
160
+ filePath: normalizedPath,
161
+ cause: error,
162
+ })
163
+ }
164
+ return err({
165
+ code: 'READ_ERROR',
166
+ message: `Failed to read package.json: ${normalizedPath}`,
167
+ filePath: normalizedPath,
168
+ cause: error,
169
+ })
170
+ }
171
+
172
+ return parsePackageJsonContent(content, normalizedPath)
173
+ }
174
+
175
+ /**
176
+ * Parses package.json content from a string.
177
+ */
178
+ export function parsePackageJsonContent(
179
+ content: string,
180
+ filePath: string,
181
+ ): Result<ParsedPackageJson, ConfigError> {
182
+ let raw: unknown
183
+ try {
184
+ raw = JSON.parse(content)
185
+ } catch (error) {
186
+ return err({
187
+ code: 'INVALID_JSON',
188
+ message: `Invalid JSON in package.json: ${filePath}`,
189
+ filePath,
190
+ cause: error,
191
+ })
192
+ }
193
+
194
+ if (!isValidPackageJson(raw)) {
195
+ return err({
196
+ code: 'INVALID_CONFIG',
197
+ message: 'package.json is missing required fields (name, version)',
198
+ filePath,
199
+ })
200
+ }
201
+
202
+ const pkg = raw as Record<string, unknown>
203
+
204
+ return ok({
205
+ name: pkg.name as string,
206
+ version: pkg.version as string,
207
+ description: pkg.description as string | undefined,
208
+ main: pkg.main as string | undefined,
209
+ module: pkg.module as string | undefined,
210
+ types: pkg.types as string | undefined,
211
+ exports: pkg.exports as Record<string, unknown> | undefined,
212
+ dependencies: pkg.dependencies as Record<string, string> | undefined,
213
+ devDependencies: pkg.devDependencies as Record<string, string> | undefined,
214
+ peerDependencies: pkg.peerDependencies as Record<string, string> | undefined,
215
+ optionalDependencies: pkg.optionalDependencies as Record<string, string> | undefined,
216
+ type: pkg.type as 'module' | 'commonjs' | undefined,
217
+ scripts: pkg.scripts as Record<string, string> | undefined,
218
+ files: pkg.files as string[] | undefined,
219
+ raw: pkg,
220
+ })
221
+ }
222
+
223
+ /**
224
+ * Parses a tsconfig.json file.
225
+ *
226
+ * @example
227
+ * ```ts
228
+ * const result = await parseTsConfig('/path/to/tsconfig.json')
229
+ * if (result.success) {
230
+ * console.log(`Target: ${result.data.compilerOptions?.target}`)
231
+ * }
232
+ * ```
233
+ */
234
+ export async function parseTsConfig(
235
+ tsconfigPath: string,
236
+ ): Promise<Result<ParsedTsConfig, ConfigError>> {
237
+ const normalizedPath = tsconfigPath.endsWith('.json')
238
+ ? tsconfigPath
239
+ : path.join(tsconfigPath, 'tsconfig.json')
240
+
241
+ let content: string
242
+ try {
243
+ content = await fs.readFile(normalizedPath, 'utf-8')
244
+ } catch (error) {
245
+ if (isNodeError(error) && error.code === 'ENOENT') {
246
+ return err({
247
+ code: 'FILE_NOT_FOUND',
248
+ message: `tsconfig.json not found: ${normalizedPath}`,
249
+ filePath: normalizedPath,
250
+ cause: error,
251
+ })
252
+ }
253
+ return err({
254
+ code: 'READ_ERROR',
255
+ message: `Failed to read tsconfig.json: ${normalizedPath}`,
256
+ filePath: normalizedPath,
257
+ cause: error,
258
+ })
259
+ }
260
+
261
+ return parseTsConfigContent(content, normalizedPath)
262
+ }
263
+
264
+ /**
265
+ * Parses tsconfig.json content from a string.
266
+ *
267
+ * Note: This does basic JSON parsing. tsconfig.json supports comments
268
+ * and trailing commas which this parser strips before parsing.
269
+ */
270
+ export function parseTsConfigContent(
271
+ content: string,
272
+ filePath: string,
273
+ ): Result<ParsedTsConfig, ConfigError> {
274
+ // Strip comments and trailing commas for JSON5-like parsing
275
+ const cleanedContent = stripJsonComments(content)
276
+
277
+ let raw: unknown
278
+ try {
279
+ raw = JSON.parse(cleanedContent)
280
+ } catch (error) {
281
+ return err({
282
+ code: 'INVALID_JSON',
283
+ message: `Invalid JSON in tsconfig.json: ${filePath}`,
284
+ filePath,
285
+ cause: error,
286
+ })
287
+ }
288
+
289
+ if (typeof raw !== 'object' || raw === null) {
290
+ return err({
291
+ code: 'INVALID_CONFIG',
292
+ message: 'tsconfig.json must be an object',
293
+ filePath,
294
+ })
295
+ }
296
+
297
+ const config = raw as Record<string, unknown>
298
+
299
+ return ok({
300
+ extends: config.extends as string | string[] | undefined,
301
+ compilerOptions: config.compilerOptions as TsCompilerOptions | undefined,
302
+ include: config.include as string[] | undefined,
303
+ exclude: config.exclude as string[] | undefined,
304
+ references: config.references as TsProjectReference[] | undefined,
305
+ filePath,
306
+ raw: config,
307
+ })
308
+ }
309
+
310
+ /**
311
+ * Gets all dependencies from a package.json (combined).
312
+ */
313
+ export function getAllDependencies(
314
+ pkg: ParsedPackageJson,
315
+ ): Readonly<Record<string, {version: string; type: 'prod' | 'dev' | 'peer' | 'optional'}>> {
316
+ const deps: Record<string, {version: string; type: 'prod' | 'dev' | 'peer' | 'optional'}> = {}
317
+
318
+ if (pkg.dependencies !== undefined) {
319
+ for (const [name, version] of Object.entries(pkg.dependencies)) {
320
+ deps[name] = {version, type: 'prod'}
321
+ }
322
+ }
323
+
324
+ if (pkg.devDependencies !== undefined) {
325
+ for (const [name, version] of Object.entries(pkg.devDependencies)) {
326
+ deps[name] = {version, type: 'dev'}
327
+ }
328
+ }
329
+
330
+ if (pkg.peerDependencies !== undefined) {
331
+ for (const [name, version] of Object.entries(pkg.peerDependencies)) {
332
+ deps[name] = {version, type: 'peer'}
333
+ }
334
+ }
335
+
336
+ if (pkg.optionalDependencies !== undefined) {
337
+ for (const [name, version] of Object.entries(pkg.optionalDependencies)) {
338
+ deps[name] = {version, type: 'optional'}
339
+ }
340
+ }
341
+
342
+ return deps
343
+ }
344
+
345
+ /**
346
+ * Resolves tsconfig extends chain.
347
+ */
348
+ export async function resolveTsConfigExtends(
349
+ tsconfigPath: string,
350
+ maxDepth = 10,
351
+ ): Promise<Result<ParsedTsConfig[], ConfigError>> {
352
+ const chain: ParsedTsConfig[] = []
353
+ let currentPath = tsconfigPath
354
+ let depth = 0
355
+
356
+ while (depth < maxDepth) {
357
+ const result = await parseTsConfig(currentPath)
358
+ if (!result.success) {
359
+ return result.success ? result : err(result.error)
360
+ }
361
+
362
+ chain.push(result.data)
363
+
364
+ const extendsValue = result.data.extends
365
+ if (extendsValue === undefined) {
366
+ break
367
+ }
368
+
369
+ let extendsPath: string | undefined
370
+ if (Array.isArray(extendsValue)) {
371
+ const firstExtends: unknown = extendsValue[0]
372
+ extendsPath = typeof firstExtends === 'string' ? firstExtends : undefined
373
+ } else if (typeof extendsValue === 'string') {
374
+ extendsPath = extendsValue
375
+ }
376
+
377
+ if (extendsPath === undefined) {
378
+ break
379
+ }
380
+
381
+ // Resolve relative to current config directory
382
+ const configDir = path.dirname(currentPath)
383
+ currentPath = resolveExtendsPath(extendsPath, configDir)
384
+ depth++
385
+ }
386
+
387
+ return ok(chain)
388
+ }
389
+
390
+ /**
391
+ * Resolves the extends path for tsconfig.
392
+ */
393
+ function resolveExtendsPath(extendsValue: string, configDir: string): string {
394
+ if (extendsValue.startsWith('.')) {
395
+ return path.resolve(configDir, extendsValue)
396
+ }
397
+
398
+ // Node module path resolution
399
+ if (!extendsValue.endsWith('.json')) {
400
+ return path.join(configDir, 'node_modules', extendsValue, 'tsconfig.json')
401
+ }
402
+
403
+ return path.join(configDir, 'node_modules', extendsValue)
404
+ }
405
+
406
+ /**
407
+ * Type guard for valid package.json.
408
+ */
409
+ function isValidPackageJson(value: unknown): boolean {
410
+ if (typeof value !== 'object' || value === null) {
411
+ return false
412
+ }
413
+
414
+ const obj = value as Record<string, unknown>
415
+ return typeof obj.name === 'string' && typeof obj.version === 'string'
416
+ }
417
+
418
+ /**
419
+ * Type guard for Node.js errors with code property.
420
+ */
421
+ function isNodeError(error: unknown): error is Error & {code: string} {
422
+ return error instanceof Error && 'code' in error
423
+ }
424
+
425
+ /**
426
+ * Strips JSON comments (// and /* *\/) and trailing commas.
427
+ */
428
+ function stripJsonComments(content: string): string {
429
+ // Use character-based scanning to safely remove comments
430
+ // This avoids ReDoS vulnerabilities from regex patterns like /\/\/.*$/gm
431
+ let result = ''
432
+ let i = 0
433
+ let inString = false
434
+ let stringChar = ''
435
+
436
+ while (i < content.length) {
437
+ const char = content[i]
438
+ const nextChar = content[i + 1]
439
+
440
+ // Track string boundaries to avoid removing // inside strings
441
+ if ((char === '"' || char === "'") && (i === 0 || content[i - 1] !== '\\')) {
442
+ if (!inString) {
443
+ inString = true
444
+ stringChar = char
445
+ } else if (char === stringChar) {
446
+ inString = false
447
+ stringChar = ''
448
+ }
449
+ result += char
450
+ i++
451
+ continue
452
+ }
453
+
454
+ // Skip comments only when not inside a string
455
+ if (!inString) {
456
+ // Single-line comment
457
+ if (char === '/' && nextChar === '/') {
458
+ // Skip until end of line
459
+ while (i < content.length && content[i] !== '\n') {
460
+ i++
461
+ }
462
+ // Include the newline
463
+ if (i < content.length) {
464
+ result += content[i]
465
+ i++
466
+ }
467
+ continue
468
+ }
469
+
470
+ // Multi-line comment
471
+ if (char === '/' && nextChar === '*') {
472
+ // Skip until */
473
+ i += 2
474
+ while (i < content.length - 1) {
475
+ if (content[i] === '*' && content[i + 1] === '/') {
476
+ i += 2
477
+ break
478
+ }
479
+ i++
480
+ }
481
+ continue
482
+ }
483
+ }
484
+
485
+ result += char
486
+ i++
487
+ }
488
+
489
+ // Remove trailing commas before } or ]
490
+ return result.replaceAll(/,(\s*[}\]])/g, '$1')
491
+ }