json-parse-even-better-errors
Advanced tools
Comparing version 3.0.0 to 3.0.1
170
lib/index.js
'use strict' | ||
const hexify = char => { | ||
const INDENT = Symbol.for('indent') | ||
const NEWLINE = Symbol.for('newline') | ||
const DEFAULT_NEWLINE = '\n' | ||
const DEFAULT_INDENT = ' ' | ||
const BOM = /^\uFEFF/ | ||
// only respect indentation if we got a line break, otherwise squash it | ||
// things other than objects and arrays aren't indented, so ignore those | ||
// Important: in both of these regexps, the $1 capture group is the newline | ||
// or undefined, and the $2 capture group is the indent, or undefined. | ||
const FORMAT = /^\s*[{[]((?:\r?\n)+)([\s\t]*)/ | ||
const EMPTY = /^(?:\{\}|\[\])((?:\r?\n)+)?$/ | ||
// Node 20 puts single quotes around the token and a comma after it | ||
const UNEXPECTED_TOKEN = /^Unexpected token '?(.)'?(,)? /i | ||
const hexify = (char) => { | ||
const h = char.charCodeAt(0).toString(16).toUpperCase() | ||
return '0x' + (h.length % 2 ? '0' : '') + h | ||
return `0x${h.length % 2 ? '0' : ''}${h}` | ||
} | ||
const parseError = (e, txt, context) => { | ||
// Remove byte order marker. This catches EF BB BF (the UTF-8 BOM) | ||
// because the buffer-to-string conversion in `fs.readFileSync()` | ||
// translates it to FEFF, the UTF-16 BOM. | ||
const stripBOM = (txt) => String(txt).replace(BOM, '') | ||
const makeParsedError = (msg, parsing, position = 0) => ({ | ||
message: `${msg} while parsing ${parsing}`, | ||
position, | ||
}) | ||
const parseError = (e, txt, context = 20) => { | ||
let msg = e.message | ||
if (!txt) { | ||
return { | ||
message: e.message + ' while parsing empty string', | ||
position: 0, | ||
} | ||
return makeParsedError(msg, 'empty string') | ||
} | ||
const badToken = e.message.match(/^Unexpected token (.) .*position\s+(\d+)/i) | ||
const errIdx = badToken ? +badToken[2] | ||
: e.message.match(/^Unexpected end of JSON.*/i) ? txt.length - 1 | ||
: null | ||
const msg = badToken ? e.message.replace(/^Unexpected token ./, `Unexpected token ${ | ||
JSON.stringify(badToken[1]) | ||
} (${hexify(badToken[1])})`) | ||
: e.message | ||
const badTokenMatch = msg.match(UNEXPECTED_TOKEN) | ||
const badIndexMatch = msg.match(/ position\s+(\d+)/i) | ||
if (errIdx !== null && errIdx !== undefined) { | ||
const start = errIdx <= context ? 0 | ||
: errIdx - context | ||
if (badTokenMatch) { | ||
msg = msg.replace( | ||
UNEXPECTED_TOKEN, | ||
`Unexpected token ${JSON.stringify(badTokenMatch[1])} (${hexify(badTokenMatch[1])})$2 ` | ||
) | ||
} | ||
const end = errIdx + context >= txt.length ? txt.length | ||
: errIdx + context | ||
let errIdx | ||
if (badIndexMatch) { | ||
errIdx = +badIndexMatch[1] | ||
} else if (msg.match(/^Unexpected end of JSON.*/i)) { | ||
errIdx = txt.length - 1 | ||
} | ||
const slice = (start === 0 ? '' : '...') + | ||
txt.slice(start, end) + | ||
(end === txt.length ? '' : '...') | ||
if (errIdx == null) { | ||
return makeParsedError(msg, `'${txt.slice(0, context * 2)}'`) | ||
} | ||
const near = txt === slice ? '' : 'near ' | ||
const start = errIdx <= context ? 0 : errIdx - context | ||
const end = errIdx + context >= txt.length ? txt.length : errIdx + context | ||
const slice = `${start ? '...' : ''}${txt.slice(start, end)}${end === txt.length ? '' : '...'}` | ||
return { | ||
message: msg + ` while parsing ${near}${JSON.stringify(slice)}`, | ||
position: errIdx, | ||
} | ||
} else { | ||
return { | ||
message: msg + ` while parsing '${txt.slice(0, context * 2)}'`, | ||
position: 0, | ||
} | ||
} | ||
return makeParsedError( | ||
msg, | ||
`${txt === slice ? '' : 'near '}${JSON.stringify(slice)}`, | ||
errIdx | ||
) | ||
} | ||
@@ -52,3 +76,2 @@ | ||
constructor (er, txt, context, caller) { | ||
context = context || 20 | ||
const metadata = parseError(er, txt, context) | ||
@@ -67,2 +90,3 @@ super(metadata.message) | ||
set name (n) {} | ||
get [Symbol.toStringTag] () { | ||
@@ -73,60 +97,42 @@ return this.constructor.name | ||
const kIndent = Symbol.for('indent') | ||
const kNewline = Symbol.for('newline') | ||
// only respect indentation if we got a line break, otherwise squash it | ||
// things other than objects and arrays aren't indented, so ignore those | ||
// Important: in both of these regexps, the $1 capture group is the newline | ||
// or undefined, and the $2 capture group is the indent, or undefined. | ||
const formatRE = /^\s*[{[]((?:\r?\n)+)([\s\t]*)/ | ||
const emptyRE = /^(?:\{\}|\[\])((?:\r?\n)+)?$/ | ||
const parseJson = (txt, reviver, context) => { | ||
const parseText = stripBOM(txt) | ||
context = context || 20 | ||
try { | ||
const parseJson = (txt, reviver) => { | ||
const result = JSON.parse(txt, reviver) | ||
if (result && typeof result === 'object') { | ||
// get the indentation so that we can save it back nicely | ||
// if the file starts with {" then we have an indent of '', ie, none | ||
// otherwise, pick the indentation of the next line after the first \n | ||
// If the pattern doesn't match, then it means no indentation. | ||
// JSON.stringify ignores symbols, so this is reasonably safe. | ||
// if the string is '{}' or '[]', then use the default 2-space indent. | ||
const [, newline = '\n', indent = ' '] = parseText.match(emptyRE) || | ||
parseText.match(formatRE) || | ||
[null, '', ''] | ||
// otherwise, pick the indentation of the next line after the first \n If the | ||
// pattern doesn't match, then it means no indentation. JSON.stringify ignores | ||
// symbols, so this is reasonably safe. if the string is '{}' or '[]', then | ||
// use the default 2-space indent. | ||
const match = txt.match(EMPTY) || txt.match(FORMAT) || [null, '', ''] | ||
result[NEWLINE] = match[1] ?? DEFAULT_NEWLINE | ||
result[INDENT] = match[2] ?? DEFAULT_INDENT | ||
} | ||
return result | ||
} | ||
const result = JSON.parse(parseText, reviver) | ||
if (result && typeof result === 'object') { | ||
result[kNewline] = newline | ||
result[kIndent] = indent | ||
} | ||
return result | ||
const parseJsonError = (raw, reviver, context) => { | ||
const txt = stripBOM(raw) | ||
try { | ||
return parseJson(txt, reviver) | ||
} catch (e) { | ||
if (typeof txt !== 'string' && !Buffer.isBuffer(txt)) { | ||
const isEmptyArray = Array.isArray(txt) && txt.length === 0 | ||
throw Object.assign(new TypeError( | ||
`Cannot parse ${isEmptyArray ? 'an empty array' : String(txt)}` | ||
), { | ||
code: 'EJSONPARSE', | ||
systemError: e, | ||
}) | ||
if (typeof raw !== 'string' && !Buffer.isBuffer(raw)) { | ||
const msg = Array.isArray(raw) && raw.length === 0 ? 'an empty array' : String(raw) | ||
throw Object.assign( | ||
new TypeError(`Cannot parse ${msg}`), | ||
{ code: 'EJSONPARSE', systemError: e } | ||
) | ||
} | ||
throw new JSONParseError(e, parseText, context, parseJson) | ||
throw new JSONParseError(e, txt, context, parseJsonError) | ||
} | ||
} | ||
// Remove byte order marker. This catches EF BB BF (the UTF-8 BOM) | ||
// because the buffer-to-string conversion in `fs.readFileSync()` | ||
// translates it to FEFF, the UTF-16 BOM. | ||
const stripBOM = txt => String(txt).replace(/^\uFEFF/, '') | ||
module.exports = parseJson | ||
parseJson.JSONParseError = JSONParseError | ||
parseJson.noExceptions = (txt, reviver) => { | ||
module.exports = parseJsonError | ||
parseJsonError.JSONParseError = JSONParseError | ||
parseJsonError.noExceptions = (raw, reviver) => { | ||
try { | ||
return JSON.parse(stripBOM(txt), reviver) | ||
} catch (e) { | ||
return parseJson(stripBOM(raw), reviver) | ||
} catch { | ||
// no exceptions | ||
} | ||
} |
{ | ||
"name": "json-parse-even-better-errors", | ||
"version": "3.0.0", | ||
"version": "3.0.1", | ||
"description": "JSON.parse with context information on error", | ||
@@ -13,3 +13,3 @@ "main": "lib/index.js", | ||
"snap": "tap", | ||
"lint": "eslint \"**/*.js\"", | ||
"lint": "eslint \"**/*.{js,cjs,ts,mjs,jsx,tsx}\"", | ||
"postlint": "template-oss-check", | ||
@@ -31,4 +31,4 @@ "template-oss-apply": "template-oss-apply --force", | ||
"devDependencies": { | ||
"@npmcli/eslint-config": "^3.1.0", | ||
"@npmcli/template-oss": "4.5.1", | ||
"@npmcli/eslint-config": "^4.0.0", | ||
"@npmcli/template-oss": "4.20.0", | ||
"tap": "^16.3.0" | ||
@@ -48,4 +48,5 @@ }, | ||
"//@npmcli/template-oss": "This file is partially managed by @npmcli/template-oss. Edits may be overwritten.", | ||
"version": "4.5.1" | ||
"version": "4.20.0", | ||
"publish": true | ||
} | ||
} |
New author
Supply chain riskA new npm collaborator published a version of the package for the first time. New collaborators are usually benign additions to a project, but do indicate a change to the security surface area of a package.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
New author
Supply chain riskA new npm collaborator published a version of the package for the first time. New collaborators are usually benign additions to a project, but do indicate a change to the security surface area of a package.
Found 1 instance in 1 package
9873
113
1