/*
|
MIT License http://www.opensource.org/licenses/mit-license.php
|
*/
|
|
"use strict";
|
|
/** @typedef {import("../util/fs").JsonValue} JsonValue */
|
|
// Inspired by https://github.com/npm/json-parse-even-better-errors
|
|
// 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.
|
/**
|
* @param {string | Buffer} txt text
|
* @returns {string} text without BOM
|
*/
|
const stripBOM = (txt) => String(txt).replace(/^\uFEFF/, "");
|
|
class JSONParseError extends SyntaxError {
|
/**
|
* @param {Error} err err
|
* @param {EXPECTED_ANY} raw raw
|
* @param {string} txt text
|
* @param {number=} context context
|
* @param {EXPECTED_FUNCTION=} caller caller
|
*/
|
constructor(err, raw, txt, context = 20, caller = parseJson) {
|
let originalMessage = err.message;
|
/** @type {string} */
|
let message;
|
/** @type {number} */
|
let position;
|
|
if (typeof raw !== "string") {
|
message = `Cannot parse ${Array.isArray(raw) && raw.length === 0 ? "an empty array" : String(raw)}`;
|
position = 0;
|
} else if (!txt) {
|
message = `${originalMessage} while parsing empty string`;
|
position = 0;
|
} else {
|
// Node 20 puts single quotes around the token and a comma after it
|
const UNEXPECTED_TOKEN = /^Unexpected token '?(.)'?(,)? /i;
|
const badTokenMatch = originalMessage.match(UNEXPECTED_TOKEN);
|
const badIndexMatch = originalMessage.match(/ position\s+(\d+)/i);
|
|
if (badTokenMatch) {
|
const h = badTokenMatch[1].charCodeAt(0).toString(16).toUpperCase();
|
const hex = `0x${h.length % 2 ? "0" : ""}${h}`;
|
|
originalMessage = originalMessage.replace(
|
UNEXPECTED_TOKEN,
|
`Unexpected token ${JSON.stringify(badTokenMatch[1])} (${hex})$2 `
|
);
|
}
|
|
/** @type {number | undefined} */
|
let errIdx;
|
|
if (badIndexMatch) {
|
errIdx = Number(badIndexMatch[1]);
|
} else if (
|
// doesn't happen in Node 22+
|
/^Unexpected end of JSON.*/i.test(originalMessage)
|
) {
|
errIdx = txt.length - 1;
|
}
|
|
if (errIdx === undefined) {
|
message = `${originalMessage} while parsing '${txt.slice(0, context * 2)}'`;
|
position = 0;
|
} else {
|
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 ? "" : "..."}`;
|
|
message = `${originalMessage} while parsing ${txt === slice ? "" : "near "}${JSON.stringify(slice)}`;
|
position = errIdx;
|
}
|
}
|
|
super(message);
|
|
this.name = "JSONParseError";
|
this.systemError = err;
|
this.position = position;
|
|
Error.captureStackTrace(this, caller || this.constructor);
|
}
|
}
|
|
/**
|
* @template [R=JsonValue]
|
* @callback ParseJsonFn
|
* @param {string} raw text
|
* @param {(this: EXPECTED_ANY, key: string, value: EXPECTED_ANY) => EXPECTED_ANY=} reviver reviver
|
* @returns {R} parsed JSON
|
*/
|
|
/** @type {ParseJsonFn} */
|
const parseJson = (raw, reviver) => {
|
const txt = stripBOM(raw);
|
|
try {
|
return JSON.parse(txt, reviver);
|
} catch (err) {
|
throw new JSONParseError(/** @type {Error} */ (err), raw, txt);
|
}
|
};
|
|
module.exports = parseJson;
|