Skip to content

Commit 35300f0

Browse files
authored
Replace json-parse-helpfulerror with jsonc-parser (#1493)
1 parent cdc8258 commit 35300f0

File tree

8 files changed

+751
-549
lines changed

8 files changed

+751
-549
lines changed

package-lock.json

+525-510
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+1-3
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,6 @@
6565
"@types/hosted-git-info": "^3.0.5",
6666
"@types/ini": "^4.1.1",
6767
"@types/js-yaml": "^4.0.9",
68-
"@types/json-parse-helpfulerror": "^1.0.3",
6968
"@types/jsonlines": "^0.1.5",
7069
"@types/lodash": "^4.17.10",
7170
"@types/mocha": "^10.0.9",
@@ -106,7 +105,7 @@
106105
"hosted-git-info": "^8.0.0",
107106
"ini": "^5.0.0",
108107
"js-yaml": "^4.1.0",
109-
"json-parse-helpfulerror": "^1.0.3",
108+
"jsonc-parser": "^3.3.1",
110109
"jsonlines": "^0.1.1",
111110
"lockfile-lint": "^4.14.0",
112111
"lodash": "^4.17.21",
@@ -132,7 +131,6 @@
132131
"source-map-support": "^0.5.21",
133132
"spawn-please": "^3.0.0",
134133
"strip-ansi": "^7.1.0",
135-
"strip-json-comments": "^5.0.1",
136134
"ts-node": "^10.9.2",
137135
"typescript": "^5.6.3",
138136
"typescript-json-schema": "^0.65.1",

src/lib/runLocal.ts

+21-21
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import fs from 'fs/promises'
2-
import jph from 'json-parse-helpfulerror'
32
import prompts from 'prompts-ncu'
43
import nodeSemver from 'semver'
4+
import { DependencyGroup } from '../types/DependencyGroup'
55
import { Index } from '../types/IndexType'
66
import { Maybe } from '../types/Maybe'
77
import { Options } from '../types/Options'
@@ -29,6 +29,7 @@ import programError from './programError'
2929
import resolveDepSections from './resolveDepSections'
3030
import upgradePackageData from './upgradePackageData'
3131
import upgradePackageDefinitions from './upgradePackageDefinitions'
32+
import parseJson from './utils/parseJson'
3233
import { getDependencyGroups } from './version-util'
3334

3435
const INTERACTIVE_HINT = `
@@ -37,6 +38,18 @@ const INTERACTIVE_HINT = `
3738
a: Toggle all
3839
Enter: Upgrade`
3940

41+
/**
42+
* Fetches how many options per page can be listed in the dependency table.
43+
*
44+
* @param groups - found dependency groups.
45+
* @returns the amount of options that can be displayed per page.
46+
*/
47+
function getOptionsPerPage(groups?: DependencyGroup[]): number {
48+
return process.stdout.rows
49+
? Math.max(3, process.stdout.rows - INTERACTIVE_HINT.split('\n').length - 1 - (groups?.length ?? 0) * 2)
50+
: 50
51+
}
52+
4053
/**
4154
* Return a promise which resolves to object storing package owner changed status for each dependency.
4255
*
@@ -106,17 +119,13 @@ const chooseUpgrades = async (
106119
]
107120
})
108121

109-
const optionsPerPage = process.stdout.rows
110-
? Math.max(3, process.stdout.rows - INTERACTIVE_HINT.split('\n').length - 1 - groups.length * 2)
111-
: 50
112-
113122
const response = await prompts({
114123
choices: [...choices, { title: ' ', heading: true }],
115124
hint: INTERACTIVE_HINT,
116125
instructions: false,
117126
message: 'Choose which packages to update',
118127
name: 'value',
119-
optionsPerPage,
128+
optionsPerPage: getOptionsPerPage(groups),
120129
type: 'multiselect',
121130
onState: (state: any) => {
122131
if (state.aborted) {
@@ -135,17 +144,13 @@ const chooseUpgrades = async (
135144
selected: true,
136145
}))
137146

138-
const optionsPerPage = process.stdout.rows
139-
? Math.max(3, process.stdout.rows - INTERACTIVE_HINT.split('\n').length - 1)
140-
: 50
141-
142147
const response = await prompts({
143148
choices: [...choices, { title: ' ', heading: true }],
144149
hint: INTERACTIVE_HINT + '\n',
145150
instructions: false,
146151
message: 'Choose which packages to update',
147152
name: 'value',
148-
optionsPerPage,
153+
optionsPerPage: getOptionsPerPage(),
149154
type: 'multiselect',
150155
onState: (state: any) => {
151156
if (state.aborted) {
@@ -162,7 +167,7 @@ const chooseUpgrades = async (
162167
}
163168

164169
/** Checks local project dependencies for upgrades. */
165-
async function runLocal(
170+
export default async function runLocal(
166171
options: Options,
167172
pkgData?: Maybe<string>,
168173
pkgFile?: Maybe<string>,
@@ -176,10 +181,7 @@ async function runLocal(
176181
if (!pkgData) {
177182
programError(options, 'Missing package data')
178183
} else {
179-
// strip comments from jsonc files
180-
const pkgDataStripped =
181-
pkgFile?.endsWith('.jsonc') && pkgData ? (await import('strip-json-comments')).default(pkgData) : pkgData
182-
pkg = jph.parse(pkgDataStripped)
184+
pkg = parseJson(pkgData)
183185
}
184186
} catch (e: any) {
185187
programError(
@@ -291,13 +293,13 @@ async function runLocal(
291293
const newPkgData = await upgradePackageData(pkgData, current, chosenUpgraded, options)
292294

293295
const output: PackageFile | Index<VersionSpec> = options.jsonAll
294-
? (jph.parse(newPkgData) as PackageFile)
296+
? (parseJson(newPkgData) as PackageFile)
295297
: options.jsonDeps
296-
? pick(jph.parse(newPkgData) as PackageFile, resolveDepSections(options.dep))
298+
? pick(parseJson(newPkgData) as PackageFile, resolveDepSections(options.dep))
297299
: chosenUpgraded
298300

299301
// will be overwritten with the result of fs.writeFile so that the return promise waits for the package file to be written
300-
let writePromise = Promise.resolve()
302+
let writePromise
301303

302304
if (options.json && !options.deep) {
303305
printJson(options, output)
@@ -330,5 +332,3 @@ async function runLocal(
330332

331333
return output
332334
}
333-
334-
export default runLocal

src/lib/utils/parseJson.ts

+89
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import { ParseError, ParseErrorCode, parse, stripComments } from 'jsonc-parser'
2+
3+
const stdoutColumns = process.stdout.columns || 80
4+
5+
/**
6+
* Ensures the code line or a hint is always displayed for the code snippet.
7+
* If the line is empty, it outputs `<empty>`.
8+
* If the line is larger than a line of the terminal windows, it will cut it off. This also prevents too much
9+
* garbage data from being displayed.
10+
*
11+
* @param line - target line to check.
12+
* @returns either the hint or the actual line for the code snippet.
13+
*/
14+
function ensureLineDisplay(line: string): string {
15+
return `${line.length ? line.slice(0, Math.min(line.length, stdoutColumns)) : '<empty>'}\n`
16+
}
17+
18+
/**
19+
* Builds a marker line to point to the position of the found error.
20+
*
21+
* @param length - positions to the right of the error line.
22+
* @returns the marker line.
23+
*/
24+
function getMarker(length: number): string {
25+
return length > stdoutColumns ? '' : `${' '.repeat(length - 1)}^\n`
26+
}
27+
28+
/**
29+
* Builds a json code snippet to mark and contextualize the found error.
30+
* This snippet consists of 5 lines with the erroneous line in the middle.
31+
*
32+
* @param lines - all lines of the json file.
33+
* @param errorLine - erroneous line.
34+
* @param columnNumber - the error position inside the line.
35+
* @returns the entire code snippet.
36+
*/
37+
function showSnippet(lines: string[], errorLine: number, columnNumber: number): string {
38+
const len = lines.length
39+
if (len === 0) return '<empty>'
40+
if (len === 1) return `${ensureLineDisplay(lines[0])}${getMarker(columnNumber)}`
41+
// Show an area of lines around the error line for a more detailed snippet.
42+
const snippetEnd = Math.min(errorLine + 2, len)
43+
let snippet = ''
44+
for (let i = Math.max(errorLine - 2, 1); i <= snippetEnd; i++) {
45+
// Lines in the output are counted starting from one, so choose the previous line
46+
snippet += ensureLineDisplay(lines[i - 1])
47+
if (i === errorLine) snippet += getMarker(columnNumber)
48+
}
49+
return `${snippet}\n`
50+
}
51+
52+
/**
53+
* Parses a json string, while also handling errors and comments.
54+
*
55+
* @param jsonString - target json string.
56+
* @returns the parsed json object.
57+
*/
58+
export default function parseJson(jsonString: string) {
59+
jsonString = stripComments(jsonString)
60+
try {
61+
return JSON.parse(jsonString)
62+
} catch {
63+
const errors: ParseError[] = []
64+
const json = parse(jsonString, errors)
65+
66+
// If no errors were found, just return the parsed json file
67+
if (errors.length === 0) return json
68+
let errorString = ''
69+
const lines = jsonString.split('\n')
70+
for (const error of errors) {
71+
const offset = error.offset
72+
let lineNumber = 1
73+
let columnNumber = 1
74+
let currentOffset = 0
75+
// Calculate line and column from the offset
76+
for (const line of lines) {
77+
if (currentOffset + line.length >= offset) {
78+
columnNumber = offset - currentOffset + 1
79+
break
80+
}
81+
currentOffset += line.length + 1 // +1 for the newline character
82+
lineNumber++
83+
}
84+
// @ts-expect-error due to --isolatedModules forbidding to implement ambient constant enums.
85+
errorString += `Error at line ${lineNumber}, column ${columnNumber}: ${ParseErrorCode[error.error]}\n${showSnippet(lines, lineNumber, columnNumber)}\n`
86+
}
87+
throw new SyntaxError(errorString)
88+
}
89+
}

src/lib/version-util.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import parseGithubUrl from 'parse-github-url'
33
import semver from 'semver'
44
import semverutils, { SemVer, parse, parseRange } from 'semver-utils'
55
import util from 'util'
6+
import { DependencyGroup } from '../types/DependencyGroup'
67
import { Index } from '../types/IndexType'
78
import { Maybe } from '../types/Maybe'
89
import { Options } from '../types/Options'
@@ -191,7 +192,7 @@ export function getDependencyGroups(
191192
newDependencies: Index<string>,
192193
oldDependencies: Index<string>,
193194
options: Options,
194-
): { heading: string; groupName: string; packages: Index<string> }[] {
195+
): DependencyGroup[] {
195196
const groups = keyValueBy<string, Index<string>>(newDependencies, (dep, to, accum) => {
196197
const from = oldDependencies[dep]
197198
const defaultGroup = partChanged(from, to)

src/types/DependencyGroup.ts

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { Index } from './IndexType'
2+
3+
export interface DependencyGroup {
4+
heading: string
5+
groupName: string
6+
packages: Index<string>
7+
}

test/package-managers/deno/index.test.ts

+17-14
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
import fs from 'fs/promises'
2-
import jph from 'json-parse-helpfulerror'
3-
import os from 'os'
4-
import path from 'path'
1+
import fs from 'node:fs/promises'
2+
import os from 'node:os'
3+
import path from 'node:path'
54
import spawn from 'spawn-please'
5+
import parseJson from '../../../src/lib/utils/parseJson'
66
import chaiSetup from '../../helpers/chaiSetup'
77

88
chaiSetup()
@@ -20,12 +20,15 @@ describe('deno', async function () {
2020
}
2121
await fs.writeFile(pkgFile, JSON.stringify(pkg))
2222
try {
23-
const { stdout } = await spawn(
24-
'node',
25-
[bin, '--jsonUpgraded', '--packageManager', 'deno', '--packageFile', pkgFile],
26-
undefined,
27-
)
28-
const pkg = jph.parse(stdout)
23+
const { stdout } = await spawn('node', [
24+
bin,
25+
'--jsonUpgraded',
26+
'--packageManager',
27+
'deno',
28+
'--packageFile',
29+
pkgFile,
30+
])
31+
const pkg = parseJson(stdout)
2932
pkg.should.have.property('ncu-test-v2')
3033
} finally {
3134
await fs.rm(tempDir, { recursive: true, force: true })
@@ -45,7 +48,7 @@ describe('deno', async function () {
4548
const { stdout } = await spawn('node', [bin, '--jsonUpgraded'], undefined, {
4649
cwd: tempDir,
4750
})
48-
const pkg = jph.parse(stdout)
51+
const pkg = parseJson(stdout)
4952
pkg.should.have.property('ncu-test-v2')
5053
} finally {
5154
await fs.rm(tempDir, { recursive: true, force: true })
@@ -64,7 +67,7 @@ describe('deno', async function () {
6467
try {
6568
await spawn('node', [bin, '-u'], undefined, { cwd: tempDir })
6669
const pkgDataNew = await fs.readFile(pkgFile, 'utf-8')
67-
const pkg = jph.parse(pkgDataNew)
70+
const pkg = parseJson(pkgDataNew)
6871
pkg.should.deep.equal({
6972
imports: {
7073
'ncu-test-v2': 'npm:[email protected]',
@@ -89,7 +92,7 @@ describe('deno', async function () {
8992
const { stdout } = await spawn('node', [bin, '--jsonUpgraded'], undefined, {
9093
cwd: tempDir,
9194
})
92-
const pkg = jph.parse(stdout)
95+
const pkg = parseJson(stdout)
9396
pkg.should.have.property('ncu-test-v2')
9497
} finally {
9598
await fs.rm(tempDir, { recursive: true, force: true })
@@ -108,7 +111,7 @@ describe('deno', async function () {
108111
try {
109112
await spawn('node', [bin, '-u'], undefined, { cwd: tempDir })
110113
const pkgDataNew = await fs.readFile(pkgFile, 'utf-8')
111-
const pkg = jph.parse(pkgDataNew)
114+
const pkg = parseJson(pkgDataNew)
112115
pkg.should.deep.equal({
113116
imports: {
114117
'ncu-test-v2': 'npm:[email protected]',

0 commit comments

Comments
 (0)