index.js 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137
  1. 'use strict'
  2. const INDENT = Symbol.for('indent')
  3. const NEWLINE = Symbol.for('newline')
  4. const DEFAULT_NEWLINE = '\n'
  5. const DEFAULT_INDENT = ' '
  6. const BOM = /^\uFEFF/
  7. // only respect indentation if we got a line break, otherwise squash it
  8. // things other than objects and arrays aren't indented, so ignore those
  9. // Important: in both of these regexps, the $1 capture group is the newline
  10. // or undefined, and the $2 capture group is the indent, or undefined.
  11. const FORMAT = /^\s*[{[]((?:\r?\n)+)([\s\t]*)/
  12. const EMPTY = /^(?:\{\}|\[\])((?:\r?\n)+)?$/
  13. // Node 20 puts single quotes around the token and a comma after it
  14. const UNEXPECTED_TOKEN = /^Unexpected token '?(.)'?(,)? /i
  15. const hexify = (char) => {
  16. const h = char.charCodeAt(0).toString(16).toUpperCase()
  17. return `0x${h.length % 2 ? '0' : ''}${h}`
  18. }
  19. // Remove byte order marker. This catches EF BB BF (the UTF-8 BOM)
  20. // because the buffer-to-string conversion in `fs.readFileSync()`
  21. // translates it to FEFF, the UTF-16 BOM.
  22. const stripBOM = (txt) => String(txt).replace(BOM, '')
  23. const makeParsedError = (msg, parsing, position = 0) => ({
  24. message: `${msg} while parsing ${parsing}`,
  25. position,
  26. })
  27. const parseError = (e, txt, context = 20) => {
  28. let msg = e.message
  29. if (!txt) {
  30. return makeParsedError(msg, 'empty string')
  31. }
  32. const badTokenMatch = msg.match(UNEXPECTED_TOKEN)
  33. const badIndexMatch = msg.match(/ position\s+(\d+)/i)
  34. if (badTokenMatch) {
  35. msg = msg.replace(
  36. UNEXPECTED_TOKEN,
  37. `Unexpected token ${JSON.stringify(badTokenMatch[1])} (${hexify(badTokenMatch[1])})$2 `
  38. )
  39. }
  40. let errIdx
  41. if (badIndexMatch) {
  42. errIdx = +badIndexMatch[1]
  43. } else /* istanbul ignore next - doesnt happen in Node 22 */ if (
  44. msg.match(/^Unexpected end of JSON.*/i)
  45. ) {
  46. errIdx = txt.length - 1
  47. }
  48. if (errIdx == null) {
  49. return makeParsedError(msg, `'${txt.slice(0, context * 2)}'`)
  50. }
  51. const start = errIdx <= context ? 0 : errIdx - context
  52. const end = errIdx + context >= txt.length ? txt.length : errIdx + context
  53. const slice = `${start ? '...' : ''}${txt.slice(start, end)}${end === txt.length ? '' : '...'}`
  54. return makeParsedError(
  55. msg,
  56. `${txt === slice ? '' : 'near '}${JSON.stringify(slice)}`,
  57. errIdx
  58. )
  59. }
  60. class JSONParseError extends SyntaxError {
  61. constructor (er, txt, context, caller) {
  62. const metadata = parseError(er, txt, context)
  63. super(metadata.message)
  64. Object.assign(this, metadata)
  65. this.code = 'EJSONPARSE'
  66. this.systemError = er
  67. Error.captureStackTrace(this, caller || this.constructor)
  68. }
  69. get name () {
  70. return this.constructor.name
  71. }
  72. set name (n) {}
  73. get [Symbol.toStringTag] () {
  74. return this.constructor.name
  75. }
  76. }
  77. const parseJson = (txt, reviver) => {
  78. const result = JSON.parse(txt, reviver)
  79. if (result && typeof result === 'object') {
  80. // get the indentation so that we can save it back nicely
  81. // if the file starts with {" then we have an indent of '', ie, none
  82. // otherwise, pick the indentation of the next line after the first \n If the
  83. // pattern doesn't match, then it means no indentation. JSON.stringify ignores
  84. // symbols, so this is reasonably safe. if the string is '{}' or '[]', then
  85. // use the default 2-space indent.
  86. const match = txt.match(EMPTY) || txt.match(FORMAT) || [null, '', '']
  87. result[NEWLINE] = match[1] ?? DEFAULT_NEWLINE
  88. result[INDENT] = match[2] ?? DEFAULT_INDENT
  89. }
  90. return result
  91. }
  92. const parseJsonError = (raw, reviver, context) => {
  93. const txt = stripBOM(raw)
  94. try {
  95. return parseJson(txt, reviver)
  96. } catch (e) {
  97. if (typeof raw !== 'string' && !Buffer.isBuffer(raw)) {
  98. const msg = Array.isArray(raw) && raw.length === 0 ? 'an empty array' : String(raw)
  99. throw Object.assign(
  100. new TypeError(`Cannot parse ${msg}`),
  101. { code: 'EJSONPARSE', systemError: e }
  102. )
  103. }
  104. throw new JSONParseError(e, txt, context, parseJsonError)
  105. }
  106. }
  107. module.exports = parseJsonError
  108. parseJsonError.JSONParseError = JSONParseError
  109. parseJsonError.noExceptions = (raw, reviver) => {
  110. try {
  111. return parseJson(stripBOM(raw), reviver)
  112. } catch {
  113. // no exceptions
  114. }
  115. }