Skip to content

Commit 089d812

Browse files
committed
Handle malformed package.json errors
1 parent 0d253f1 commit 089d812

File tree

3 files changed

+84
-2
lines changed

3 files changed

+84
-2
lines changed
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import {parseJSON} from './json.js'
2+
import {AbortError} from '../node/error.js'
3+
import {describe, expect, test} from 'vitest'
4+
5+
describe('parseJSON', () => {
6+
test('parses valid JSON with nested objects', () => {
7+
// Given
8+
const jsonString = '{"user": {"name": "Alice", "age": 30}}'
9+
10+
// When
11+
const result = parseJSON(jsonString)
12+
13+
// Then
14+
expect(result).toEqual({user: {name: 'Alice', age: 30}})
15+
})
16+
17+
test('throws AbortError for malformed JSON without context', () => {
18+
// Given
19+
const malformedJSON = '{"name": "test", invalid}'
20+
21+
// When/Then
22+
expect(() => parseJSON(malformedJSON)).toThrow(AbortError)
23+
expect(() => parseJSON(malformedJSON)).toThrow(/Failed to parse JSON/)
24+
})
25+
26+
test('throws AbortError for malformed JSON with context', () => {
27+
// Given
28+
const malformedJSON = '{"name": "test", invalid}'
29+
const context = '/path/to/config.json'
30+
31+
// When/Then
32+
expect(() => parseJSON(malformedJSON, context)).toThrow(AbortError)
33+
expect(() => parseJSON(malformedJSON, context)).toThrow(/Failed to parse JSON from \/path\/to\/config\.json/)
34+
})
35+
36+
test('throws AbortError with original error message', () => {
37+
// Given
38+
const malformedJSON = '{"trailing comma":,}'
39+
40+
// When/Then
41+
expect(() => parseJSON(malformedJSON)).toThrow(/Unexpected token/)
42+
})
43+
44+
test('handles null value', () => {
45+
// Given
46+
const jsonString = 'null'
47+
48+
// When
49+
const result = parseJSON(jsonString)
50+
51+
// Then
52+
expect(result).toBeNull()
53+
})
54+
})
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import {AbortError} from '../node/error.js'
2+
3+
/**
4+
* Safely parse JSON with helpful error messages.
5+
*
6+
* @param jsonString - The JSON string to parse.
7+
* @param context - Optional context about what's being parsed (e.g., file path, "API response").
8+
* @returns The parsed JSON object.
9+
* @throws AbortError if JSON is malformed.
10+
*
11+
* @example
12+
* // Parse with context
13+
* const data = parseJSON(jsonString, '/path/to/config.json')
14+
*
15+
* @example
16+
* // Parse without context
17+
* const data = parseJSON(jsonString)
18+
*/
19+
export function parseJSON<T = unknown>(jsonString: string, context?: string): T {
20+
try {
21+
return JSON.parse(jsonString) as T
22+
} catch (error) {
23+
const errorMessage = error instanceof Error ? error.message : String(error)
24+
const contextMessage = context ? ` from ${context}` : ''
25+
throw new AbortError(`Failed to parse JSON${contextMessage}.\n${errorMessage}`)
26+
}
27+
}

packages/cli-kit/src/public/node/node-package-manager.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {runWithTimer} from './metadata.js'
77
import {inferPackageManagerForGlobalCLI} from './is-global.js'
88
import {outputToken, outputContent, outputDebug} from '../../public/node/output.js'
99
import {PackageVersionKey, cacheRetrieve, cacheRetrieveOrRepopulate} from '../../private/node/conf-store.js'
10+
import {parseJSON} from '../common/json.js'
1011
import latestVersion from 'latest-version'
1112
import {SemVer, satisfies as semverSatisfies} from 'semver'
1213
import type {Writable} from 'stream'
@@ -406,7 +407,7 @@ export async function readAndParsePackageJson(packageJsonPath: string): Promise<
406407
if (!(await fileExists(packageJsonPath))) {
407408
throw new PackageJsonNotFoundError(dirname(packageJsonPath))
408409
}
409-
return JSON.parse(await readFile(packageJsonPath))
410+
return parseJSON(await readFile(packageJsonPath), packageJsonPath)
410411
}
411412

412413
interface AddNPMDependenciesIfNeededOptions {
@@ -673,7 +674,7 @@ function argumentsToAddDependenciesWithBun(dependencies: string[], type: Depende
673674
export async function findUpAndReadPackageJson(fromDirectory: string): Promise<{path: string; content: PackageJson}> {
674675
const packageJsonPath = await findPathUp('package.json', {cwd: fromDirectory, type: 'file'})
675676
if (packageJsonPath) {
676-
const packageJson = JSON.parse(await readFile(packageJsonPath))
677+
const packageJson = parseJSON<PackageJson>(await readFile(packageJsonPath), packageJsonPath)
677678
return {path: packageJsonPath, content: packageJson}
678679
} else {
679680
throw new FindUpAndReadPackageJsonNotFoundError(fromDirectory)

0 commit comments

Comments
 (0)