@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,349 @@
1
+ /**
2
+ * PackageJsonAnalyzer - Validates package.json configuration for best practices.
3
+ *
4
+ * Detects issues such as:
5
+ * - Missing required fields (types, exports, etc.)
6
+ * - Inconsistent entry points
7
+ * - Invalid dependency versions
8
+ * - Missing peer dependency declarations
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 path from 'node:path'
18
+
19
+ import {ok} from '@bfra.me/es/result'
20
+
21
+ import {createIssue, filterIssues} from './analyzer'
22
+
23
+ /**
24
+ * Configuration options specific to PackageJsonAnalyzer.
25
+ */
26
+ export interface PackageJsonAnalyzerOptions {
27
+ /** Whether to require types field for TypeScript packages */
28
+ readonly requireTypes?: boolean
29
+ /** Whether to require exports field for ESM packages */
30
+ readonly requireExports?: boolean
31
+ /** Whether to check for common scripts like build, test, lint */
32
+ readonly checkScripts?: boolean
33
+ /** Package names exempt from certain checks */
34
+ readonly exemptPackages?: readonly string[]
35
+ }
36
+
37
+ const DEFAULT_OPTIONS: PackageJsonAnalyzerOptions = {
38
+ requireTypes: true,
39
+ requireExports: true,
40
+ checkScripts: true,
41
+ exemptPackages: [],
42
+ }
43
+
44
+ const METADATA: AnalyzerMetadata = {
45
+ id: 'package-json',
46
+ name: 'Package.json Analyzer',
47
+ description: 'Validates package.json configuration for best practices and consistency',
48
+ categories: ['configuration'],
49
+ defaultSeverity: 'warning',
50
+ }
51
+
52
+ /**
53
+ * Creates a PackageJsonAnalyzer instance.
54
+ */
55
+ export function createPackageJsonAnalyzer(options: PackageJsonAnalyzerOptions = {}): Analyzer {
56
+ const resolvedOptions = {...DEFAULT_OPTIONS, ...options}
57
+
58
+ return {
59
+ metadata: METADATA,
60
+ analyze: async (context: AnalysisContext): Promise<Result<readonly Issue[], AnalyzerError>> => {
61
+ const issues: Issue[] = []
62
+
63
+ for (const pkg of context.packages) {
64
+ if (isExemptPackage(pkg.name, resolvedOptions.exemptPackages)) {
65
+ continue
66
+ }
67
+
68
+ const packageIssues = analyzePackage(pkg, resolvedOptions)
69
+ issues.push(...packageIssues)
70
+ }
71
+
72
+ return ok(filterIssues(issues, context.config))
73
+ },
74
+ }
75
+ }
76
+
77
+ function isExemptPackage(name: string, exemptPackages: readonly string[] | undefined): boolean {
78
+ return exemptPackages?.includes(name) ?? false
79
+ }
80
+
81
+ function createLocation(pkg: WorkspacePackage): IssueLocation {
82
+ return {
83
+ filePath: pkg.packageJsonPath,
84
+ }
85
+ }
86
+
87
+ function analyzePackage(pkg: WorkspacePackage, options: PackageJsonAnalyzerOptions): Issue[] {
88
+ const issues: Issue[] = []
89
+ const pkgJson = pkg.packageJson as ParsedPackageJson
90
+
91
+ // Check for types field in TypeScript packages
92
+ if (options.requireTypes && pkg.hasTsConfig) {
93
+ issues.push(...checkTypesField(pkg, pkgJson))
94
+ }
95
+
96
+ // Check for exports field in ESM packages
97
+ if (options.requireExports) {
98
+ issues.push(...checkExportsField(pkg, pkgJson))
99
+ }
100
+
101
+ // Check for essential scripts
102
+ if (options.checkScripts) {
103
+ issues.push(...checkScripts(pkg, pkgJson))
104
+ }
105
+
106
+ // Check for invalid dependencies
107
+ issues.push(...checkDependencies(pkg, pkgJson))
108
+
109
+ // Check entry point consistency
110
+ issues.push(...checkEntryPoints(pkg, pkgJson))
111
+
112
+ return issues
113
+ }
114
+
115
+ function checkTypesField(pkg: WorkspacePackage, pkgJson: ParsedPackageJson): Issue[] {
116
+ const issues: Issue[] = []
117
+
118
+ // Check main types field
119
+ if (pkgJson.types === undefined) {
120
+ // Check if types are declared in exports
121
+ const hasTypesInExports = hasTypesExport(pkgJson.exports)
122
+
123
+ if (!hasTypesInExports) {
124
+ issues.push(
125
+ createIssue({
126
+ id: 'missing-types',
127
+ title: 'Missing types field',
128
+ description: `Package "${pkg.name}" is a TypeScript package but does not declare a "types" field or types in exports`,
129
+ severity: 'warning',
130
+ category: 'configuration',
131
+ location: createLocation(pkg),
132
+ suggestion:
133
+ 'Add "types" field pointing to your .d.ts entry point, or add "types" to exports',
134
+ }),
135
+ )
136
+ }
137
+ }
138
+
139
+ return issues
140
+ }
141
+
142
+ function hasTypesExport(exports: Record<string, unknown> | undefined): boolean {
143
+ if (exports === undefined) {
144
+ return false
145
+ }
146
+
147
+ // Check if root export has types
148
+ const rootExport = exports['.']
149
+ if (typeof rootExport === 'object' && rootExport !== null && 'types' in rootExport) {
150
+ return true
151
+ }
152
+
153
+ // Check nested exports
154
+ for (const value of Object.values(exports)) {
155
+ if (typeof value === 'object' && value !== null && 'types' in value) {
156
+ return true
157
+ }
158
+ }
159
+
160
+ return false
161
+ }
162
+
163
+ function checkExportsField(pkg: WorkspacePackage, pkgJson: ParsedPackageJson): Issue[] {
164
+ const issues: Issue[] = []
165
+
166
+ // Check if package uses ESM
167
+ const isEsm = pkgJson.type === 'module'
168
+
169
+ if (isEsm && pkgJson.exports === undefined) {
170
+ issues.push(
171
+ createIssue({
172
+ id: 'missing-exports',
173
+ title: 'ESM package missing exports field',
174
+ description: `Package "${pkg.name}" uses ESM ("type": "module") but does not declare an "exports" field`,
175
+ severity: 'warning',
176
+ category: 'configuration',
177
+ location: createLocation(pkg),
178
+ suggestion: 'Add "exports" field with explicit entry points for better Node.js resolution',
179
+ }),
180
+ )
181
+ }
182
+
183
+ // Check for wildcard exports in non-library packages
184
+ if (pkgJson.exports !== undefined) {
185
+ issues.push(...checkWildcardExports(pkg, pkgJson.exports))
186
+ }
187
+
188
+ return issues
189
+ }
190
+
191
+ function checkWildcardExports(pkg: WorkspacePackage, exports: Record<string, unknown>): Issue[] {
192
+ const issues: Issue[] = []
193
+
194
+ for (const [key, value] of Object.entries(exports)) {
195
+ if (key.includes('*')) {
196
+ issues.push(
197
+ createIssue({
198
+ id: 'wildcard-export',
199
+ title: 'Wildcard export pattern detected',
200
+ description: `Package "${pkg.name}" uses wildcard export pattern "${key}" which may expose internal modules`,
201
+ severity: 'info',
202
+ category: 'configuration',
203
+ location: createLocation(pkg),
204
+ suggestion:
205
+ 'Consider using explicit export paths for better control over public API surface',
206
+ metadata: {exportKey: key, exportValue: value},
207
+ }),
208
+ )
209
+ }
210
+ }
211
+
212
+ return issues
213
+ }
214
+
215
+ function checkScripts(pkg: WorkspacePackage, pkgJson: ParsedPackageJson): Issue[] {
216
+ const issues: Issue[] = []
217
+ const scripts = pkgJson.scripts ?? {}
218
+
219
+ // Check for build script
220
+ if (scripts.build === undefined && pkg.hasTsConfig) {
221
+ issues.push(
222
+ createIssue({
223
+ id: 'missing-build-script',
224
+ title: 'Missing build script',
225
+ description: `Package "${pkg.name}" has TypeScript config but no "build" script`,
226
+ severity: 'info',
227
+ category: 'configuration',
228
+ location: createLocation(pkg),
229
+ suggestion: 'Add a "build" script to compile TypeScript sources',
230
+ }),
231
+ )
232
+ }
233
+
234
+ return issues
235
+ }
236
+
237
+ function checkDependencies(pkg: WorkspacePackage, pkgJson: ParsedPackageJson): Issue[] {
238
+ const issues: Issue[] = []
239
+
240
+ // Check for deprecated version formats
241
+ const allDeps = {
242
+ ...(pkgJson.dependencies ?? {}),
243
+ ...(pkgJson.devDependencies ?? {}),
244
+ ...(pkgJson.peerDependencies ?? {}),
245
+ ...(pkgJson.optionalDependencies ?? {}),
246
+ }
247
+
248
+ for (const [depName, version] of Object.entries(allDeps)) {
249
+ // Check for file: protocol
250
+ if (version.startsWith('file:')) {
251
+ issues.push(
252
+ createIssue({
253
+ id: 'file-dependency',
254
+ title: 'File protocol dependency',
255
+ description: `Package "${pkg.name}" has a file: dependency on "${depName}"`,
256
+ severity: 'warning',
257
+ category: 'dependency',
258
+ location: createLocation(pkg),
259
+ suggestion: 'Use workspace: protocol for monorepo packages or publish as a package',
260
+ metadata: {dependency: depName, version},
261
+ }),
262
+ )
263
+ }
264
+
265
+ // Check for git: protocol in production deps
266
+ if (version.startsWith('git:') || version.includes('github:')) {
267
+ const isDev = pkgJson.devDependencies !== undefined && depName in pkgJson.devDependencies
268
+ if (!isDev) {
269
+ issues.push(
270
+ createIssue({
271
+ id: 'git-dependency',
272
+ title: 'Git protocol dependency in production',
273
+ description: `Package "${pkg.name}" has a git dependency on "${depName}" in production dependencies`,
274
+ severity: 'warning',
275
+ category: 'dependency',
276
+ location: createLocation(pkg),
277
+ suggestion: 'Use a published npm package version for production dependencies',
278
+ metadata: {dependency: depName, version},
279
+ }),
280
+ )
281
+ }
282
+ }
283
+
284
+ // Check for latest or * versions
285
+ if (version === 'latest' || version === '*') {
286
+ issues.push(
287
+ createIssue({
288
+ id: 'unpinned-dependency',
289
+ title: 'Unpinned dependency version',
290
+ description: `Package "${pkg.name}" has an unpinned dependency "${depName}" with version "${version}"`,
291
+ severity: 'error',
292
+ category: 'dependency',
293
+ location: createLocation(pkg),
294
+ suggestion: 'Use a specific version or range for reproducible builds',
295
+ metadata: {dependency: depName, version},
296
+ }),
297
+ )
298
+ }
299
+ }
300
+
301
+ return issues
302
+ }
303
+
304
+ function checkEntryPoints(pkg: WorkspacePackage, pkgJson: ParsedPackageJson): Issue[] {
305
+ const issues: Issue[] = []
306
+
307
+ // Check if main and module both exist but point to same format
308
+ if (pkgJson.main !== undefined && pkgJson.module !== undefined) {
309
+ const mainExt = path.extname(pkgJson.main)
310
+ const moduleExt = path.extname(pkgJson.module)
311
+
312
+ // Both pointing to same extension might indicate misconfiguration
313
+ if (mainExt === moduleExt && mainExt !== '') {
314
+ issues.push(
315
+ createIssue({
316
+ id: 'duplicate-entry-format',
317
+ title: 'Entry points may have same format',
318
+ description: `Package "${pkg.name}" has both "main" and "module" pointing to ${mainExt} files`,
319
+ severity: 'info',
320
+ category: 'configuration',
321
+ location: createLocation(pkg),
322
+ suggestion:
323
+ '"main" should typically point to CJS format (.cjs) and "module" to ESM format (.mjs or .js)',
324
+ metadata: {main: pkgJson.main, module: pkgJson.module},
325
+ }),
326
+ )
327
+ }
328
+ }
329
+
330
+ // Check for ESM package without module field
331
+ if (pkgJson.type === 'module' && pkgJson.module === undefined && pkgJson.exports === undefined) {
332
+ issues.push(
333
+ createIssue({
334
+ id: 'esm-without-module-or-exports',
335
+ title: 'ESM package without module or exports field',
336
+ description: `Package "${pkg.name}" is ESM but has neither "module" nor "exports" field`,
337
+ severity: 'warning',
338
+ category: 'configuration',
339
+ location: createLocation(pkg),
340
+ suggestion:
341
+ 'Add "exports" field for modern ESM packages, or "module" for backwards compatibility',
342
+ }),
343
+ )
344
+ }
345
+
346
+ return issues
347
+ }
348
+
349
+ export {METADATA as packageJsonAnalyzerMetadata}
@@ -0,0 +1,275 @@
1
+ /**
2
+ * PeerDependencyAnalyzer - Validates peer dependency declarations in workspace packages.
3
+ *
4
+ * Ensures workspace packages correctly declare peer dependencies for:
5
+ * - Packages that should be provided by consumers
6
+ * - Plugin architectures where host version matters
7
+ * - Framework dependencies that consumers must provide
8
+ *
9
+ * Reports:
10
+ * - Missing peer dependency declarations
11
+ * - Peer dependency version mismatches
12
+ * - Invalid peer dependency ranges
13
+ * - Workspace packages not declared as peers when they should be
14
+ */
15
+
16
+ import type {WorkspacePackage} from '../scanner/workspace-scanner'
17
+ import type {Issue, IssueLocation} from '../types/index'
18
+ import type {Result} from '../types/result'
19
+ import type {AnalysisContext, Analyzer, AnalyzerError, AnalyzerMetadata} from './analyzer'
20
+
21
+ import {ok} from '@bfra.me/es/result'
22
+
23
+ import {createIssue, filterIssues} from './analyzer'
24
+
25
+ /**
26
+ * Configuration options specific to PeerDependencyAnalyzer.
27
+ */
28
+ export interface PeerDependencyAnalyzerOptions {
29
+ /** Packages that should always be declared as peer dependencies */
30
+ readonly requiredPeers?: readonly string[]
31
+ /** Packages that should never be peer dependencies */
32
+ readonly disallowedPeers?: readonly string[]
33
+ /** Whether to check for workspace packages that should be peers */
34
+ readonly checkWorkspacePeers?: boolean
35
+ /** Workspace package prefixes */
36
+ readonly workspacePrefixes?: readonly string[]
37
+ /** Check for peer dependencies that are also in dependencies (dual declaration) */
38
+ readonly checkDualDeclarations?: boolean
39
+ }
40
+
41
+ const DEFAULT_OPTIONS: Required<PeerDependencyAnalyzerOptions> = {
42
+ requiredPeers: [],
43
+ disallowedPeers: [],
44
+ checkWorkspacePeers: true,
45
+ workspacePrefixes: ['@bfra.me/'],
46
+ checkDualDeclarations: true,
47
+ }
48
+
49
+ export const peerDependencyAnalyzerMetadata: AnalyzerMetadata = {
50
+ id: 'peer-dependency',
51
+ name: 'Peer Dependency Analyzer',
52
+ description: 'Validates peer dependency declarations for proper package consumption',
53
+ categories: ['dependency'],
54
+ defaultSeverity: 'warning',
55
+ }
56
+
57
+ /**
58
+ * Creates a PeerDependencyAnalyzer instance.
59
+ *
60
+ * @example
61
+ * ```ts
62
+ * const analyzer = createPeerDependencyAnalyzer({
63
+ * requiredPeers: ['react', 'typescript'],
64
+ * checkDualDeclarations: true,
65
+ * })
66
+ * const result = await analyzer.analyze(context)
67
+ * ```
68
+ */
69
+ export function createPeerDependencyAnalyzer(
70
+ options: PeerDependencyAnalyzerOptions = {},
71
+ ): Analyzer {
72
+ const resolvedOptions = {...DEFAULT_OPTIONS, ...options}
73
+
74
+ return {
75
+ metadata: peerDependencyAnalyzerMetadata,
76
+ analyze: async (context: AnalysisContext): Promise<Result<readonly Issue[], AnalyzerError>> => {
77
+ const issues: Issue[] = []
78
+
79
+ const workspacePackageNames = new Set(context.packages.map(pkg => pkg.name))
80
+
81
+ for (const pkg of context.packages) {
82
+ context.reportProgress?.(`Analyzing peer dependencies for ${pkg.name}...`)
83
+
84
+ const packageIssues = analyzePackagePeerDependencies(
85
+ pkg,
86
+ workspacePackageNames,
87
+ resolvedOptions,
88
+ )
89
+ issues.push(...packageIssues)
90
+ }
91
+
92
+ return ok(filterIssues(issues, context.config))
93
+ },
94
+ }
95
+ }
96
+
97
+ /**
98
+ * Analyzes peer dependencies for a single package.
99
+ */
100
+ function analyzePackagePeerDependencies(
101
+ pkg: WorkspacePackage,
102
+ workspacePackageNames: Set<string>,
103
+ options: Required<PeerDependencyAnalyzerOptions>,
104
+ ): Issue[] {
105
+ const issues: Issue[] = []
106
+ const pkgJson = pkg.packageJson
107
+
108
+ const dependencies = pkgJson.dependencies ?? {}
109
+ const peerDependencies = pkgJson.peerDependencies ?? {}
110
+ const devDependencies = pkgJson.devDependencies ?? {}
111
+
112
+ // Check for required peer dependencies that are missing
113
+ for (const requiredPeer of options.requiredPeers) {
114
+ const isDep = requiredPeer in dependencies
115
+ const isDevDep = requiredPeer in devDependencies
116
+ const isPeer = requiredPeer in peerDependencies
117
+
118
+ if ((isDep || isDevDep) && !isPeer) {
119
+ issues.push(
120
+ createIssue({
121
+ id: 'missing-peer-declaration',
122
+ title: `Missing peer dependency: ${requiredPeer}`,
123
+ description: `Package "${pkg.name}" uses "${requiredPeer}" but does not declare it as a peer dependency`,
124
+ severity: 'warning',
125
+ category: 'dependency',
126
+ location: createLocation(pkg),
127
+ suggestion: `Add "${requiredPeer}" to peerDependencies with an appropriate version range`,
128
+ metadata: {
129
+ packageName: pkg.name,
130
+ missingPeer: requiredPeer,
131
+ },
132
+ }),
133
+ )
134
+ }
135
+ }
136
+
137
+ // Check for disallowed peer dependencies
138
+ for (const disallowed of options.disallowedPeers) {
139
+ if (disallowed in peerDependencies) {
140
+ issues.push(
141
+ createIssue({
142
+ id: 'disallowed-peer-dependency',
143
+ title: `Disallowed peer dependency: ${disallowed}`,
144
+ description: `Package "${pkg.name}" should not declare "${disallowed}" as a peer dependency`,
145
+ severity: 'error',
146
+ category: 'dependency',
147
+ location: createLocation(pkg),
148
+ suggestion: `Remove "${disallowed}" from peerDependencies`,
149
+ metadata: {
150
+ packageName: pkg.name,
151
+ disallowedPeer: disallowed,
152
+ },
153
+ }),
154
+ )
155
+ }
156
+ }
157
+
158
+ // Check for dual declarations (dependency + peer dependency)
159
+ if (options.checkDualDeclarations) {
160
+ for (const peerName of Object.keys(peerDependencies)) {
161
+ if (peerName in dependencies) {
162
+ issues.push(
163
+ createIssue({
164
+ id: 'dual-dependency-declaration',
165
+ title: `Dual declaration: ${peerName}`,
166
+ description: `Package "${pkg.name}" declares "${peerName}" in both dependencies and peerDependencies`,
167
+ severity: 'info',
168
+ category: 'dependency',
169
+ location: createLocation(pkg),
170
+ suggestion: `Remove "${peerName}" from dependencies if consumers should provide it, or from peerDependencies if you bundle it`,
171
+ metadata: {
172
+ packageName: pkg.name,
173
+ dualDependency: peerName,
174
+ dependencyVersion: dependencies[peerName],
175
+ peerVersion: peerDependencies[peerName],
176
+ },
177
+ }),
178
+ )
179
+ }
180
+ }
181
+ }
182
+
183
+ // Check workspace packages used as dependencies that might need to be peers
184
+ if (options.checkWorkspacePeers) {
185
+ for (const [depName, depVersion] of Object.entries(dependencies)) {
186
+ const isWorkspaceDep =
187
+ workspacePackageNames.has(depName) ||
188
+ options.workspacePrefixes.some(prefix => depName.startsWith(prefix))
189
+ const isWorkspaceVersion = depVersion.startsWith('workspace:')
190
+ const isPeer = depName in peerDependencies
191
+
192
+ // Workspace dependencies with non-workspace versions might indicate a peer dep need
193
+ if (isWorkspaceDep && !isWorkspaceVersion && !isPeer) {
194
+ issues.push(
195
+ createIssue({
196
+ id: 'workspace-peer-candidate',
197
+ title: `Workspace package should be peer: ${depName}`,
198
+ description: `Package "${pkg.name}" uses workspace package "${depName}" with version "${depVersion}" instead of workspace: protocol`,
199
+ severity: 'info',
200
+ category: 'dependency',
201
+ location: createLocation(pkg),
202
+ suggestion: `Consider using "workspace:*" for internal packages or declaring as peerDependency if consumers should provide it`,
203
+ metadata: {
204
+ packageName: pkg.name,
205
+ dependencyName: depName,
206
+ currentVersion: depVersion,
207
+ },
208
+ }),
209
+ )
210
+ }
211
+ }
212
+ }
213
+
214
+ // Check peer dependency version ranges for validity
215
+ for (const [peerName, peerVersion] of Object.entries(peerDependencies)) {
216
+ if (!isValidVersionRange(peerVersion)) {
217
+ issues.push(
218
+ createIssue({
219
+ id: 'invalid-peer-version',
220
+ title: `Invalid peer version range: ${peerName}`,
221
+ description: `Package "${pkg.name}" has an invalid version range "${peerVersion}" for peer dependency "${peerName}"`,
222
+ severity: 'error',
223
+ category: 'dependency',
224
+ location: createLocation(pkg),
225
+ suggestion: `Use a valid semver range like ">=1.0.0", "^2.0.0", or ">=1.0.0 <3.0.0"`,
226
+ metadata: {
227
+ packageName: pkg.name,
228
+ peerName,
229
+ invalidVersion: peerVersion,
230
+ },
231
+ }),
232
+ )
233
+ }
234
+ }
235
+
236
+ return issues
237
+ }
238
+
239
+ /**
240
+ * Creates a location pointing to the package.json file.
241
+ */
242
+ function createLocation(pkg: WorkspacePackage): IssueLocation {
243
+ return {
244
+ filePath: pkg.packageJsonPath,
245
+ }
246
+ }
247
+
248
+ /**
249
+ * Basic validation for semver version ranges.
250
+ */
251
+ function isValidVersionRange(version: string): boolean {
252
+ if (version.length === 0) {
253
+ return false
254
+ }
255
+
256
+ // workspace: protocol is valid
257
+ if (version.startsWith('workspace:')) {
258
+ return true
259
+ }
260
+
261
+ // Common invalid patterns
262
+ if (version === 'latest' || version === '*' || version === '') {
263
+ return false
264
+ }
265
+
266
+ // Basic semver-like patterns (use non-capturing groups for validation only)
267
+ // Avoid catastrophic backtracking by removing ambiguous optional matches/repetitions
268
+ // Accept major, major.minor, major.minor.patch. Use up to two dot-separated numbers.
269
+ const semverPattern =
270
+ /^[\^~>=<]?\d+(?:\.\d+){0,2}(?:-[\w.]+)?(?:\+[\w.]+)?(?:\s*(?:&&|\|\|)\s*[\^~>=<]?\d+(?:\.\d+){0,2}(?:-[\w.]+)?(?:\+[\w.]+)?)*$/
271
+ const rangePattern = /^>=?\d+(?:\.\d+){0,2}\s+<?=?\d+(?:\.\d+){0,2}$/
272
+ const xRangePattern = /^\d+(?:\.\d+)?\.x$/
273
+
274
+ return semverPattern.test(version) || rangePattern.test(version) || xRangePattern.test(version)
275
+ }