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