Skip to content

Commit

Permalink
Replace json-parse-helpfulerror with jsonc-parser (#1493)
Browse files Browse the repository at this point in the history
  • Loading branch information
Torathion authored Jan 26, 2025
1 parent cdc8258 commit 35300f0
Show file tree
Hide file tree
Showing 8 changed files with 751 additions and 549 deletions.
1,035 changes: 525 additions & 510 deletions package-lock.json

Large diffs are not rendered by default.

4 changes: 1 addition & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,6 @@
"@types/hosted-git-info": "^3.0.5",
"@types/ini": "^4.1.1",
"@types/js-yaml": "^4.0.9",
"@types/json-parse-helpfulerror": "^1.0.3",
"@types/jsonlines": "^0.1.5",
"@types/lodash": "^4.17.10",
"@types/mocha": "^10.0.9",
Expand Down Expand Up @@ -106,7 +105,7 @@
"hosted-git-info": "^8.0.0",
"ini": "^5.0.0",
"js-yaml": "^4.1.0",
"json-parse-helpfulerror": "^1.0.3",
"jsonc-parser": "^3.3.1",
"jsonlines": "^0.1.1",
"lockfile-lint": "^4.14.0",
"lodash": "^4.17.21",
Expand All @@ -132,7 +131,6 @@
"source-map-support": "^0.5.21",
"spawn-please": "^3.0.0",
"strip-ansi": "^7.1.0",
"strip-json-comments": "^5.0.1",
"ts-node": "^10.9.2",
"typescript": "^5.6.3",
"typescript-json-schema": "^0.65.1",
Expand Down
42 changes: 21 additions & 21 deletions src/lib/runLocal.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import fs from 'fs/promises'
import jph from 'json-parse-helpfulerror'
import prompts from 'prompts-ncu'
import nodeSemver from 'semver'
import { DependencyGroup } from '../types/DependencyGroup'
import { Index } from '../types/IndexType'
import { Maybe } from '../types/Maybe'
import { Options } from '../types/Options'
Expand Down Expand Up @@ -29,6 +29,7 @@ import programError from './programError'
import resolveDepSections from './resolveDepSections'
import upgradePackageData from './upgradePackageData'
import upgradePackageDefinitions from './upgradePackageDefinitions'
import parseJson from './utils/parseJson'
import { getDependencyGroups } from './version-util'

const INTERACTIVE_HINT = `
Expand All @@ -37,6 +38,18 @@ const INTERACTIVE_HINT = `
a: Toggle all
Enter: Upgrade`

/**
* Fetches how many options per page can be listed in the dependency table.
*
* @param groups - found dependency groups.
* @returns the amount of options that can be displayed per page.
*/
function getOptionsPerPage(groups?: DependencyGroup[]): number {
return process.stdout.rows
? Math.max(3, process.stdout.rows - INTERACTIVE_HINT.split('\n').length - 1 - (groups?.length ?? 0) * 2)
: 50
}

/**
* Return a promise which resolves to object storing package owner changed status for each dependency.
*
Expand Down Expand Up @@ -106,17 +119,13 @@ const chooseUpgrades = async (
]
})

const optionsPerPage = process.stdout.rows
? Math.max(3, process.stdout.rows - INTERACTIVE_HINT.split('\n').length - 1 - groups.length * 2)
: 50

const response = await prompts({
choices: [...choices, { title: ' ', heading: true }],
hint: INTERACTIVE_HINT,
instructions: false,
message: 'Choose which packages to update',
name: 'value',
optionsPerPage,
optionsPerPage: getOptionsPerPage(groups),
type: 'multiselect',
onState: (state: any) => {
if (state.aborted) {
Expand All @@ -135,17 +144,13 @@ const chooseUpgrades = async (
selected: true,
}))

const optionsPerPage = process.stdout.rows
? Math.max(3, process.stdout.rows - INTERACTIVE_HINT.split('\n').length - 1)
: 50

const response = await prompts({
choices: [...choices, { title: ' ', heading: true }],
hint: INTERACTIVE_HINT + '\n',
instructions: false,
message: 'Choose which packages to update',
name: 'value',
optionsPerPage,
optionsPerPage: getOptionsPerPage(),
type: 'multiselect',
onState: (state: any) => {
if (state.aborted) {
Expand All @@ -162,7 +167,7 @@ const chooseUpgrades = async (
}

/** Checks local project dependencies for upgrades. */
async function runLocal(
export default async function runLocal(
options: Options,
pkgData?: Maybe<string>,
pkgFile?: Maybe<string>,
Expand All @@ -176,10 +181,7 @@ async function runLocal(
if (!pkgData) {
programError(options, 'Missing package data')
} else {
// strip comments from jsonc files
const pkgDataStripped =
pkgFile?.endsWith('.jsonc') && pkgData ? (await import('strip-json-comments')).default(pkgData) : pkgData
pkg = jph.parse(pkgDataStripped)
pkg = parseJson(pkgData)
}
} catch (e: any) {
programError(
Expand Down Expand Up @@ -291,13 +293,13 @@ async function runLocal(
const newPkgData = await upgradePackageData(pkgData, current, chosenUpgraded, options)

const output: PackageFile | Index<VersionSpec> = options.jsonAll
? (jph.parse(newPkgData) as PackageFile)
? (parseJson(newPkgData) as PackageFile)
: options.jsonDeps
? pick(jph.parse(newPkgData) as PackageFile, resolveDepSections(options.dep))
? pick(parseJson(newPkgData) as PackageFile, resolveDepSections(options.dep))
: chosenUpgraded

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

if (options.json && !options.deep) {
printJson(options, output)
Expand Down Expand Up @@ -330,5 +332,3 @@ async function runLocal(

return output
}

export default runLocal
89 changes: 89 additions & 0 deletions src/lib/utils/parseJson.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { ParseError, ParseErrorCode, parse, stripComments } from 'jsonc-parser'

const stdoutColumns = process.stdout.columns || 80

/**
* Ensures the code line or a hint is always displayed for the code snippet.
* If the line is empty, it outputs `<empty>`.
* If the line is larger than a line of the terminal windows, it will cut it off. This also prevents too much
* garbage data from being displayed.
*
* @param line - target line to check.
* @returns either the hint or the actual line for the code snippet.
*/
function ensureLineDisplay(line: string): string {
return `${line.length ? line.slice(0, Math.min(line.length, stdoutColumns)) : '<empty>'}\n`
}

/**
* Builds a marker line to point to the position of the found error.
*
* @param length - positions to the right of the error line.
* @returns the marker line.
*/
function getMarker(length: number): string {
return length > stdoutColumns ? '' : `${' '.repeat(length - 1)}^\n`
}

/**
* Builds a json code snippet to mark and contextualize the found error.
* This snippet consists of 5 lines with the erroneous line in the middle.
*
* @param lines - all lines of the json file.
* @param errorLine - erroneous line.
* @param columnNumber - the error position inside the line.
* @returns the entire code snippet.
*/
function showSnippet(lines: string[], errorLine: number, columnNumber: number): string {
const len = lines.length
if (len === 0) return '<empty>'
if (len === 1) return `${ensureLineDisplay(lines[0])}${getMarker(columnNumber)}`
// Show an area of lines around the error line for a more detailed snippet.
const snippetEnd = Math.min(errorLine + 2, len)
let snippet = ''
for (let i = Math.max(errorLine - 2, 1); i <= snippetEnd; i++) {
// Lines in the output are counted starting from one, so choose the previous line
snippet += ensureLineDisplay(lines[i - 1])
if (i === errorLine) snippet += getMarker(columnNumber)
}
return `${snippet}\n`
}

/**
* Parses a json string, while also handling errors and comments.
*
* @param jsonString - target json string.
* @returns the parsed json object.
*/
export default function parseJson(jsonString: string) {
jsonString = stripComments(jsonString)
try {
return JSON.parse(jsonString)
} catch {
const errors: ParseError[] = []
const json = parse(jsonString, errors)

// If no errors were found, just return the parsed json file
if (errors.length === 0) return json
let errorString = ''
const lines = jsonString.split('\n')
for (const error of errors) {
const offset = error.offset
let lineNumber = 1
let columnNumber = 1
let currentOffset = 0
// Calculate line and column from the offset
for (const line of lines) {
if (currentOffset + line.length >= offset) {
columnNumber = offset - currentOffset + 1
break
}
currentOffset += line.length + 1 // +1 for the newline character
lineNumber++
}
// @ts-expect-error due to --isolatedModules forbidding to implement ambient constant enums.
errorString += `Error at line ${lineNumber}, column ${columnNumber}: ${ParseErrorCode[error.error]}\n${showSnippet(lines, lineNumber, columnNumber)}\n`
}
throw new SyntaxError(errorString)
}
}
3 changes: 2 additions & 1 deletion src/lib/version-util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import parseGithubUrl from 'parse-github-url'
import semver from 'semver'
import semverutils, { SemVer, parse, parseRange } from 'semver-utils'
import util from 'util'
import { DependencyGroup } from '../types/DependencyGroup'
import { Index } from '../types/IndexType'
import { Maybe } from '../types/Maybe'
import { Options } from '../types/Options'
Expand Down Expand Up @@ -191,7 +192,7 @@ export function getDependencyGroups(
newDependencies: Index<string>,
oldDependencies: Index<string>,
options: Options,
): { heading: string; groupName: string; packages: Index<string> }[] {
): DependencyGroup[] {
const groups = keyValueBy<string, Index<string>>(newDependencies, (dep, to, accum) => {
const from = oldDependencies[dep]
const defaultGroup = partChanged(from, to)
Expand Down
7 changes: 7 additions & 0 deletions src/types/DependencyGroup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { Index } from './IndexType'

export interface DependencyGroup {
heading: string
groupName: string
packages: Index<string>
}
31 changes: 17 additions & 14 deletions test/package-managers/deno/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import fs from 'fs/promises'
import jph from 'json-parse-helpfulerror'
import os from 'os'
import path from 'path'
import fs from 'node:fs/promises'
import os from 'node:os'
import path from 'node:path'
import spawn from 'spawn-please'
import parseJson from '../../../src/lib/utils/parseJson'
import chaiSetup from '../../helpers/chaiSetup'

chaiSetup()
Expand All @@ -20,12 +20,15 @@ describe('deno', async function () {
}
await fs.writeFile(pkgFile, JSON.stringify(pkg))
try {
const { stdout } = await spawn(
'node',
[bin, '--jsonUpgraded', '--packageManager', 'deno', '--packageFile', pkgFile],
undefined,
)
const pkg = jph.parse(stdout)
const { stdout } = await spawn('node', [
bin,
'--jsonUpgraded',
'--packageManager',
'deno',
'--packageFile',
pkgFile,
])
const pkg = parseJson(stdout)
pkg.should.have.property('ncu-test-v2')
} finally {
await fs.rm(tempDir, { recursive: true, force: true })
Expand All @@ -45,7 +48,7 @@ describe('deno', async function () {
const { stdout } = await spawn('node', [bin, '--jsonUpgraded'], undefined, {
cwd: tempDir,
})
const pkg = jph.parse(stdout)
const pkg = parseJson(stdout)
pkg.should.have.property('ncu-test-v2')
} finally {
await fs.rm(tempDir, { recursive: true, force: true })
Expand All @@ -64,7 +67,7 @@ describe('deno', async function () {
try {
await spawn('node', [bin, '-u'], undefined, { cwd: tempDir })
const pkgDataNew = await fs.readFile(pkgFile, 'utf-8')
const pkg = jph.parse(pkgDataNew)
const pkg = parseJson(pkgDataNew)
pkg.should.deep.equal({
imports: {
'ncu-test-v2': 'npm:[email protected]',
Expand All @@ -89,7 +92,7 @@ describe('deno', async function () {
const { stdout } = await spawn('node', [bin, '--jsonUpgraded'], undefined, {
cwd: tempDir,
})
const pkg = jph.parse(stdout)
const pkg = parseJson(stdout)
pkg.should.have.property('ncu-test-v2')
} finally {
await fs.rm(tempDir, { recursive: true, force: true })
Expand All @@ -108,7 +111,7 @@ describe('deno', async function () {
try {
await spawn('node', [bin, '-u'], undefined, { cwd: tempDir })
const pkgDataNew = await fs.readFile(pkgFile, 'utf-8')
const pkg = jph.parse(pkgDataNew)
const pkg = parseJson(pkgDataNew)
pkg.should.deep.equal({
imports: {
'ncu-test-v2': 'npm:[email protected]',
Expand Down
Loading

0 comments on commit 35300f0

Please sign in to comment.