From 0a08ff0fa2ab7a3d9865ff9807bfbd47f7342efe Mon Sep 17 00:00:00 2001 From: 11suixing11 <11suixing11@users.noreply.github.com> Date: Mon, 27 Jul 2026 03:50:09 +0800 Subject: [PATCH] fix: explain malformed blueprint JSON errors --- package.json | 1 + src/commands/generate.ts | 56 +++++++++++++++++++++++++++++-- tests/generate-json-error.test.ts | 23 +++++++++++++ 3 files changed, 78 insertions(+), 2 deletions(-) create mode 100644 tests/generate-json-error.test.ts diff --git a/package.json b/package.json index 508e7ef..bdbec30 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "dev": "ts-node src/cli.ts", "lint": "eslint .", "lint:fix": "eslint . --fix", + "test": "node -r ts-node/register --test tests/*.test.ts", "start": "node dist/cli.js" }, "keywords": [ diff --git a/src/commands/generate.ts b/src/commands/generate.ts index 581e449..b79f2cd 100644 --- a/src/commands/generate.ts +++ b/src/commands/generate.ts @@ -25,6 +25,56 @@ function resolveContent(value: unknown): string { return ""; } +function getJsonParseLocation( + raw: string, + error: unknown, +): { line: number; column: number } | null { + if (!(error instanceof Error)) return null; + + const directLocation = error.message.match(/line (\d+) column (\d+)/i); + if (directLocation) { + return { + line: Number(directLocation[1]), + column: Number(directLocation[2]), + }; + } + + const positionMatch = error.message.match(/position (\d+)/i); + if (!positionMatch) return null; + + const position = Number(positionMatch[1]); + if (!Number.isInteger(position) || position < 0) return null; + + const beforeError = raw.slice(0, position); + const lines = beforeError.split(/\r\n|\r|\n/); + + return { + line: lines.length, + column: (lines[lines.length - 1] ?? "").length + 1, + }; +} + +function getErrorMessage(error: unknown): string { + return error instanceof Error ? error.message : String(error); +} + +export function formatBlueprintJsonParseError( + filePath: string, + raw: string, + error: unknown, +): string { + const location = getJsonParseLocation(raw, error); + const locationText = location + ? ` at line ${location.line}, column ${location.column}` + : ""; + + return [ + `Invalid JSON in blueprint file: ${filePath}${locationText}`, + `JSON parser error: ${getErrorMessage(error)}`, + "Validate the blueprint JSON syntax before retrying.", + ].join("\n"); +} + async function processNode( node: StructureNode, currentPath: string, @@ -93,8 +143,10 @@ export async function generateCommand( try { structre = JSON.parse(raw); - } catch { - spinner.fail(chalk.red("Invalid Json in blueprint file")); + } catch (error) { + spinner.fail( + chalk.red(formatBlueprintJsonParseError(jsonFile, raw, error)), + ); process.exit(1); } diff --git a/tests/generate-json-error.test.ts b/tests/generate-json-error.test.ts new file mode 100644 index 0000000..81075c7 --- /dev/null +++ b/tests/generate-json-error.test.ts @@ -0,0 +1,23 @@ +import assert from "node:assert/strict"; +import test from "node:test"; +import { formatBlueprintJsonParseError } from "../src/commands/generate"; + +test("formatBlueprintJsonParseError reports the blueprint path, location, and hint", () => { + const error = new SyntaxError("Unexpected token } in JSON at position 8"); + + const message = formatBlueprintJsonParseError( + "blueprints/app.json", + "first\nsecond\nthird", + error, + ); + + assert.match( + message, + /Invalid JSON in blueprint file: blueprints\/app\.json at line 2, column 3/, + ); + assert.match( + message, + /JSON parser error: Unexpected token } in JSON at position 8/, + ); + assert.match(message, /Validate the blueprint JSON syntax before retrying\./); +});