diff --git a/docs/generated/devkit/index.md b/docs/generated/devkit/index.md index 404cb11b2f4539..ba45108edc0c53 100644 --- a/docs/generated/devkit/index.md +++ b/docs/generated/devkit/index.md @@ -2089,7 +2089,7 @@ Note that the return value is a promise of an iterator, so you need to await bef ▸ **serializeJson**<`T`\>(`input`, `options?`): `string` Serializes the given data to a JSON string. -By default the JSON string is formatted with a 2 space intendation to be easy readable. +By default the JSON string is formatted with a 2 space indentation to be easy readable. #### Type parameters diff --git a/docs/generated/packages/devkit/documents/index.md b/docs/generated/packages/devkit/documents/index.md index 404cb11b2f4539..ba45108edc0c53 100644 --- a/docs/generated/packages/devkit/documents/index.md +++ b/docs/generated/packages/devkit/documents/index.md @@ -2089,7 +2089,7 @@ Note that the return value is a promise of an iterator, so you need to await bef ▸ **serializeJson**<`T`\>(`input`, `options?`): `string` Serializes the given data to a JSON string. -By default the JSON string is formatted with a 2 space intendation to be easy readable. +By default the JSON string is formatted with a 2 space indentation to be easy readable. #### Type parameters diff --git a/packages/nx/package.json b/packages/nx/package.json index d3db0bfa095cff..7f989bb4777460 100644 --- a/packages/nx/package.json +++ b/packages/nx/package.json @@ -52,6 +52,7 @@ "ignore": "^5.0.4", "js-yaml": "4.1.0", "jsonc-parser": "3.2.0", + "lines-and-columns": "~2.0.3", "minimatch": "3.0.5", "npm-run-path": "^4.0.1", "open": "^8.4.0", diff --git a/packages/nx/src/project-graph/build-project-graph.spec.ts b/packages/nx/src/project-graph/build-project-graph.spec.ts index a385c964696229..7cca6d4c84f32b 100644 --- a/packages/nx/src/project-graph/build-project-graph.spec.ts +++ b/packages/nx/src/project-graph/build-project-graph.spec.ts @@ -192,8 +192,8 @@ describe('project graph', () => { await buildProjectGraph(); fail('Invalid tsconfigs should cause project graph to throw error'); } catch (e) { - expect(e.message).toMatchInlineSnapshot( - `"InvalidSymbol in /root/tsconfig.base.json at position 247"` + expect(e.message).toMatch( + /InvalidSymbol in \/root\/tsconfig\.base\.json at 0:247/ ); } }); diff --git a/packages/nx/src/utils/__snapshots__/json.spec.ts.snap b/packages/nx/src/utils/__snapshots__/json.spec.ts.snap new file mode 100644 index 00000000000000..9ce8f9660de5f5 --- /dev/null +++ b/packages/nx/src/utils/__snapshots__/json.spec.ts.snap @@ -0,0 +1,35 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`parseJson should throw when JSON has trailing commas 1`] = ` +"PropertyNameExpected in JSON at 5:5 +... +2: \\"nested\\": { +3: \\"test\\": 123, +4: \\"more\\": 456, +5: }, + ^ PropertyNameExpected +6: \\"array\\": [1, 2, 3,] +7: }" +`; + +exports[`parseJson should throw when JSON with comments gets parsed and disallow comments is true 1`] = ` +"InvalidCommentToken in JSON at 1:6 +0: { +1: //\\"test\\": 123, + ^^^^^^^^^^^^^^ InvalidCommentToken +2: \\"nested\\": { +3: \\"test\\": 123 +4: /* +..." +`; + +exports[`parseJson should throw when JSON with comments gets parsed and disallowComments and expectComments is true 1`] = ` +"InvalidCommentToken in JSON at 1:6 +0: { +1: //\\"test\\": 123, + ^^^^^^^^^^^^^^ InvalidCommentToken +2: \\"nested\\": { +3: \\"test\\": 123 +4: /* +..." +`; diff --git a/packages/nx/src/utils/json.spec.ts b/packages/nx/src/utils/json.spec.ts index 8d053badd466a3..756fa1b372d04c 100644 --- a/packages/nx/src/utils/json.spec.ts +++ b/packages/nx/src/utils/json.spec.ts @@ -58,7 +58,7 @@ describe('parseJson', () => { }`, { disallowComments: true } ) - ).toThrowError(); + ).toThrowErrorMatchingSnapshot(); }); it('should throw when JSON with comments gets parsed and disallowComments and expectComments is true', () => { @@ -77,7 +77,23 @@ describe('parseJson', () => { }`, { disallowComments: true, expectComments: true } ) - ).toThrowError(); + ).toThrowErrorMatchingSnapshot(); + }); + + it('should throw when JSON has trailing commas', () => { + expect(() => + parseJson( + `{ + "test": 123, + "nested": { + "test": 123, + "more": 456, + }, + "array": [1, 2, 3,] + }`, + { disallowComments: true, expectComments: true } + ) + ).toThrowErrorMatchingSnapshot(); }); it('should handle trailing commas', () => { diff --git a/packages/nx/src/utils/json.ts b/packages/nx/src/utils/json.ts index b619f67fd031f0..04ca93d27c6a88 100644 --- a/packages/nx/src/utils/json.ts +++ b/packages/nx/src/utils/json.ts @@ -1,5 +1,6 @@ import { parse, printParseErrorCode, stripComments } from 'jsonc-parser'; import type { ParseError, ParseOptions } from 'jsonc-parser'; +import LinesAndColumn from 'lines-and-columns'; export { stripComments as stripJsonComments }; @@ -49,18 +50,62 @@ export function parseJson( const result: T = parse(input, errors, options); if (errors.length > 0) { - const { error, offset } = errors[0]; - throw new Error( - `${printParseErrorCode(error)} in JSON at position ${offset}` - ); + throw new Error(formatParseError(input, errors[0], 3)); } return result; } +/** + * Nicely formats a JSON error with context + * + * @param input JSON content as string + * @param parseError jsonc ParseError + * @param numContextLine Number of context lines to show before and after + * @returns + */ +function formatParseError( + input: string, + parseError: ParseError, + numContextLine: number +) { + const { error, offset, length } = parseError; + const { line, column } = new LinesAndColumn(input).locationForIndex(offset); + + const errorLines = [ + `${printParseErrorCode(error)} in JSON at ${line}:${column}`, + ]; + + const inputLines = input.split(/\r?\n/); + const firstLine = Math.max(0, line - numContextLine); + const lastLine = Math.min(inputLines.length - 1, line + numContextLine); + + if (firstLine > 0) { + errorLines.push('...'); + } + + for (let currentLine = firstLine; currentLine <= lastLine; currentLine++) { + const lineNumber = `${currentLine}: `; + errorLines.push(`${lineNumber}${inputLines[currentLine]}`); + if (line == currentLine) { + errorLines.push( + `${' '.repeat(column + lineNumber.length)}${'^'.repeat( + length + )} ${printParseErrorCode(error)}` + ); + } + } + + if (lastLine < inputLines.length - 1) { + errorLines.push('...'); + } + + return errorLines.join('\n'); +} + /** * Serializes the given data to a JSON string. - * By default the JSON string is formatted with a 2 space intendation to be easy readable. + * By default the JSON string is formatted with a 2 space indentation to be easy readable. * * @param input Object which should be serialized to JSON * @param options JSON serialize options diff --git a/packages/workspace/src/utils/ast-utils.spec.ts b/packages/workspace/src/utils/ast-utils.spec.ts index cd9337f36caeea..e4513bbcb0a105 100644 --- a/packages/workspace/src/utils/ast-utils.spec.ts +++ b/packages/workspace/src/utils/ast-utils.spec.ts @@ -48,8 +48,8 @@ describe('readJsonInTree', () => { it('should throw an error if the file cannot be parsed', () => { tree.create('data.json', `{ data: 'data'`); - expect(() => readJsonInTree(tree, 'data.json')).toThrow( - 'Cannot parse data.json: InvalidSymbol in JSON at position 2' + expect(() => readJsonInTree(tree, 'data.json')).toThrowError( + /Cannot parse data\.json: InvalidSymbol in JSON at 0:2/ ); }); });