/ src / cli / ndjsonSafeStringify.ts
ndjsonSafeStringify.ts
 1  import { jsonStringify } from '../utils/slowOperations.js'
 2  
 3  // JSON.stringify emits U+2028/U+2029 raw (valid per ECMA-404). When the
 4  // output is a single NDJSON line, any receiver that uses JavaScript
 5  // line-terminator semantics (ECMA-262 §11.3 — \n \r U+2028 U+2029) to
 6  // split the stream will cut the JSON mid-string. ProcessTransport now
 7  // silently skips non-JSON lines rather than crashing (gh-28405), but
 8  // the truncated fragment is still lost — the message is silently dropped.
 9  //
10  // The \uXXXX form is equivalent JSON (parses to the same string) but
11  // can never be mistaken for a line terminator by ANY receiver. This is
12  // what ES2019's "Subsume JSON" proposal and Node's util.inspect do.
13  //
14  // Single regex with alternation: the callback's one dispatch per match
15  // is cheaper than two full-string scans.
16  const JS_LINE_TERMINATORS = /\u2028|\u2029/g
17  
18  function escapeJsLineTerminators(json: string): string {
19    return json.replace(JS_LINE_TERMINATORS, c =>
20      c === '\u2028' ? '\\u2028' : '\\u2029',
21    )
22  }
23  
24  /**
25   * JSON.stringify for one-message-per-line transports. Escapes U+2028
26   * LINE SEPARATOR and U+2029 PARAGRAPH SEPARATOR so the serialized output
27   * cannot be broken by a line-splitting receiver. Output is still valid
28   * JSON and parses to the same value.
29   */
30  export function ndjsonSafeStringify(value: unknown): string {
31    return escapeJsLineTerminators(jsonStringify(value))
32  }