diff --git a/docs/generated/devkit/index.md b/docs/generated/devkit/index.md index 404cb11b2f453..ba45108edc0c5 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 404cb11b2f453..ba45108edc0c5 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 5011524567e5f..75e64ca85aaf4 100644 --- a/packages/nx/package.json +++ b/packages/nx/package.json @@ -51,6 +51,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 a385c96469622..7965d47365f16 100644 --- a/packages/nx/src/project-graph/build-project-graph.spec.ts +++ b/packages/nx/src/project-graph/build-project-graph.spec.ts @@ -192,9 +192,12 @@ 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).toMatchInlineSnapshot(` + "InvalidSymbol in /root/tsconfig.base.json at 1:248 + 1: {\\"compilerOptions\\":{\\"baseUrl\\":\\".\\",\\"paths\\":{\\"@nrwl/shared/util\\":[\\"libs/shared/util/src/index.ts\\"],\\"@nrwl/shared-util-data\\":[\\"libs/shared/util/data/src/index.ts\\"],\\"@nrwl/ui\\":[\\"libs/ui/src/index.ts\\"],\\"@nrwl/lazy-lib\\":[\\"libs/lazy-lib/src/index.ts\\"]}}}invalid + ^^^^^^^ InvalidSymbol + " + `); } }); diff --git a/packages/nx/src/utils/json.spec.ts b/packages/nx/src/utils/json.spec.ts index 8d053badd466a..25e0639d413e3 100644 --- a/packages/nx/src/utils/json.spec.ts +++ b/packages/nx/src/utils/json.spec.ts @@ -58,7 +58,17 @@ describe('parseJson', () => { }`, { disallowComments: true } ) - ).toThrowError(); + ).toThrowErrorMatchingInlineSnapshot(` + "InvalidCommentToken in JSON at 2:7 + 1: { + 2: //\\"test\\": 123, + ^^^^^^^^^^^^^^ InvalidCommentToken + 3: \\"nested\\": { + 4: \\"test\\": 123 + 5: /* + ... + " + `); }); it('should throw when JSON with comments gets parsed and disallowComments and expectComments is true', () => { @@ -77,7 +87,44 @@ describe('parseJson', () => { }`, { disallowComments: true, expectComments: true } ) - ).toThrowError(); + ).toThrowErrorMatchingInlineSnapshot(` + "InvalidCommentToken in JSON at 2:7 + 1: { + 2: //\\"test\\": 123, + ^^^^^^^^^^^^^^ InvalidCommentToken + 3: \\"nested\\": { + 4: \\"test\\": 123 + 5: /* + ... + " + `); + }); + + 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 } + ) + ).toThrowErrorMatchingInlineSnapshot(` + "PropertyNameExpected in JSON at 6:6 + ... + 3: \\"nested\\": { + 4: \\"test\\": 123, + 5: \\"more\\": 456, + 6: }, + ^ PropertyNameExpected + 7: \\"array\\": [1, 2, 3,] + 8: } + " + `); }); it('should handle trailing commas', () => { diff --git a/packages/nx/src/utils/json.ts b/packages/nx/src/utils/json.ts index b619f67fd031f..dd9796d54793c 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 }; @@ -23,7 +24,7 @@ export interface JsonParseOptions extends ParseOptions { export interface JsonSerializeOptions { /** - * the whitespaces to add as intentation to make the output more readable. + * the whitespaces to add as indentation to make the output more readable. * @default 2 */ spaces?: number; @@ -49,18 +50,64 @@ 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 + 1}:${column + 1}`, + ]; + + 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 + 1}: `; + 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('...'); + } + + 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 cd9337f36caee..6afbe8683fc94 100644 --- a/packages/workspace/src/utils/ast-utils.spec.ts +++ b/packages/workspace/src/utils/ast-utils.spec.ts @@ -48,9 +48,13 @@ 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')) + .toThrowErrorMatchingInlineSnapshot(` + "Cannot parse data.json: InvalidSymbol in JSON at 1:3 + 1: { data: 'data' + ^^^^ InvalidSymbol + " + `); }); });