parseJsonSafely<T>() with line/column errors

TypeScript
import { readFile } from 'node:fs/promises';

/** Error carrying the character offset reported by JSON.parse. */
export class JsonParseError extends Error {
  readonly position: number | undefined;

  constructor(message: string, position?: number) {
    super(message);
    this.name = 'JsonParseError';
    this.position = position;
  }
}

/** Parse JSON text, turning SyntaxError into a message with line and column. */
export function parseJsonSafely<T = unknown>(text: string, source = '<input>'): T {
  try {
    return JSON.parse(text) as T;
  } catch (error) {
    if (!(error instanceof SyntaxError)) throw error;
    const match = /position\s+(\d+)/i.exec(error.message);
    const position = match ? Number(match[1]) : undefined;
    if (position === undefined || Number.isNaN(position)) {
      throw new JsonParseError(`Invalid JSON from ${source}: ${error.message}`);
    }
    const before = text.slice(0, position);
    const line = before.split('\n').length;
    const column = position - before.lastIndexOf('\n');
    const near = text.slice(Math.max(0, position - 20), position + 20).replace(/\s+/g, ' ');
    throw new JsonParseError(`Invalid JSON at ${source}:${line}:${column} near '${near}'`, position);
  }
}

/** Read a JSON file from disk and parse it with the same diagnostics. */
export async function parseJsonFile<T>(path: string): Promise<T> {
  return parseJsonSafely<T>(await readFile(path, 'utf8'), path);
}