Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Replace json-parse-helpfulerror with jsonc-parser #1493

Merged
merged 7 commits into from
Jan 26, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading