Files
Benoit Goupilleau 6470e8361a Bug 2059673 - Move the JSON Lines helpers to a shared module r=devtools-reviewers,jdescottes
parseJsonl() was a private function inside json-viewer.mjs, a page
entry-point that renders React and reads the JSONView content global at
load time, so nothing else could import it. JsonlLineError lived in
jsonview/, and the JSON Lines content types were spelled out inline in
the sniffer and twice in fixSave().

The Netmonitor needs all three (bug 2059673), so move them to
devtools/client/shared/jsonl-utils.mjs and have jsonview import from
there. JsonlLineError also gains a Symbol.for() brand and an isInstance()
helper, so consumers loading the module through a different global still
recognise its instances.

The only behaviour change: a line which fails to parse is reparsed
preceded by the blank lines it comes after in the document, so that its
error message points at the line in the document rather than at line 1.

Differential Revision: https://phabricator.services.mozilla.com/D320918
2026-09-02 14:15:29 +00:00

97 lines
3.0 KiB
JavaScript

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import { parseJsonLossless } from "resource://devtools/client/shared/components/reps/reps/rep-utils.mjs";
// Brands JsonlLineError instances so they can be recognized across module
// instances, where `instanceof` would fail.
const JSONL_LINE_ERROR = Symbol.for("devtools.jsonl.lineError");
/**
* Represents one line of a JSON Lines document that failed to parse
* as JSON. `raw` and `message` are non-enumerable so this behaves as
* a leaf node in the JSON tree view (no expand toggle, no children).
*/
export class JsonlLineError {
/**
* @param {string} raw
* The line's original text.
* @param {string} message
* The parse error message reported for that line.
*/
constructor(raw, message) {
Object.defineProperties(this, {
raw: { value: raw, enumerable: false },
message: { value: message, enumerable: false },
[JSONL_LINE_ERROR]: { value: true, enumerable: false },
});
}
static isInstance(value) {
return !!value?.[JSONL_LINE_ERROR];
}
toJSON() {
return { error: this.message, raw: this.raw };
}
}
/**
* Parses a JSON Lines document (one JSON value per line) into an
* array of records, in document order. Blank lines are skipped, so
* the array is contiguous and displayed like any other JSON array.
* A line that fails to parse becomes a JsonlLineError instead of
* blocking the rest of the document.
*
* @param {string} jsonlinesText
* The whole JSON Lines document, as text.
* @returns {Array<object|JsonlLineError>}
* One entry per non-blank line, in document order.
*/
export function parseJsonl(jsonlinesText) {
const entries = [];
const lines = jsonlinesText.split("\n");
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
if (!line.trim()) {
continue;
}
try {
entries.push(parseJsonLossless(line));
} catch (err) {
entries.push(
new JsonlLineError(line, documentLineErrorMessage(err, line, i))
);
}
}
return entries;
}
/**
* Returns the parse error message for a line which failed to parse, pointing at
* the line's position in the document. The line is reparsed preceded by the
* blank lines it comes after, since a parser fed a single line can only ever
* report line 1.
*
* @param {Error} err
* The error thrown when parsing the line on its own.
* @param {string} line
* The line's text.
* @param {number} lineIndex
* The 0-based position of the line in the document.
* @returns {string}
* The parse error message.
*/
function documentLineErrorMessage(err, line, lineIndex) {
if (!lineIndex) {
return err.message;
}
try {
parseJsonLossless("\n".repeat(lineIndex) + line);
} catch (paddedErr) {
return paddedErr.message;
}
return err.message;
}