diff --git a/packages/builder/lib/lbt/analyzer/ComponentAnalyzer.js b/packages/builder/lib/lbt/analyzer/ComponentAnalyzer.js
index 1f32bcb08c4..a11dae084f0 100644
--- a/packages/builder/lib/lbt/analyzer/ComponentAnalyzer.js
+++ b/packages/builder/lib/lbt/analyzer/ComponentAnalyzer.js
@@ -21,6 +21,7 @@
import {fromUI5LegacyName} from "../utils/ModuleName.js";
import {getLogger} from "@ui5/logger";
+import parseJSON from "../../utils/json.js";
const log = getLogger("lbt:analyzer:ComponentAnalyzer");
// ---------------------------------------------------------------------------------------------------------
@@ -55,7 +56,7 @@ class ComponentAnalyzer {
const manifestResource = await this._pool.findResource(manifestName).catch(() => null);
if ( manifestResource ) {
const fileContent = await manifestResource.buffer();
- this._analyzeManifest( JSON.parse(fileContent.toString()), info );
+ this._analyzeManifest( parseJSON(fileContent.toString(), manifestName), info );
} else {
log.verbose(`No manifest found for '${resource.name}', skipping analysis`);
}
diff --git a/packages/builder/lib/lbt/analyzer/SmartTemplateAnalyzer.js b/packages/builder/lib/lbt/analyzer/SmartTemplateAnalyzer.js
index f33c86685ef..6afd4f29656 100644
--- a/packages/builder/lib/lbt/analyzer/SmartTemplateAnalyzer.js
+++ b/packages/builder/lib/lbt/analyzer/SmartTemplateAnalyzer.js
@@ -29,6 +29,7 @@ import SapUiDefine from "../calls/SapUiDefine.js";
import {parseJS, Syntax} from "../utils/parseUtils.js";
import {getValue, isMethodCall, getStringValue} from "../utils/ASTUtils.js";
import {getLogger} from "@ui5/logger";
+import parseJSON from "../../utils/json.js";
const log = getLogger("lbt:analyzer:SmartTemplateAnalyzer");
// ---------------------------------------------------------------------------------------------------------
@@ -57,7 +58,7 @@ class TemplateComponentAnalyzer {
const manifestResource = await this._pool.findResource(manifestName).catch(() => null);
if ( manifestResource ) {
const fileContent = await manifestResource.buffer();
- await this._analyzeManifest( JSON.parse(fileContent.toString()), info );
+ await this._analyzeManifest( parseJSON(fileContent.toString(), manifestName), info );
} else {
log.verbose(`No manifest found for '${resource.name}', skipping analysis`);
}
diff --git a/packages/builder/lib/processors/manifestEnhancer.js b/packages/builder/lib/processors/manifestEnhancer.js
index 5f687f694d7..73202a633e6 100644
--- a/packages/builder/lib/processors/manifestEnhancer.js
+++ b/packages/builder/lib/processors/manifestEnhancer.js
@@ -3,6 +3,7 @@ const {SemVer: Version, lt} = semver;
import {promisify} from "node:util";
import path from "node:path/posix";
import {getLogger} from "@ui5/logger";
+import parseJSON from "../utils/json.js";
const log = getLogger("builder:processors:manifestEnhancer");
const APP_DESCRIPTOR_V22 = new Version("1.21.0");
@@ -180,7 +181,7 @@ class ManifestEnhancer {
this.fsReadDir = promisify(fs.readdir);
this.cwd = path.dirname(filePath);
this.filePath = filePath;
- this.manifest = JSON.parse(manifest);
+ this.manifest = parseJSON(manifest, filePath);
this.isModified = false;
this.runInvoked = false;
diff --git a/packages/builder/lib/processors/versionInfoGenerator.js b/packages/builder/lib/processors/versionInfoGenerator.js
index 3edc80e8427..21ff0d36c88 100644
--- a/packages/builder/lib/processors/versionInfoGenerator.js
+++ b/packages/builder/lib/processors/versionInfoGenerator.js
@@ -2,6 +2,7 @@ import {getLogger} from "@ui5/logger";
const log = getLogger("builder:processors:versionInfoGenerator");
import {createResource} from "@ui5/fs/resourceFactory";
import posixPath from "node:path/posix";
+import parseJSON from "../utils/json.js";
/**
* @public
@@ -59,7 +60,7 @@ function getTimestamp() {
*/
const processManifest = async (manifestResource) => {
const manifestContent = await manifestResource.getString();
- const manifestObject = JSON.parse(manifestContent);
+ const manifestObject = parseJSON(manifestContent, manifestResource.getPath());
const manifestInfo = Object.create(null);
// sap.ui5/dependencies is used for the "manifestHints/libs"
diff --git a/packages/builder/lib/tasks/bundlers/generateFlexChangesBundle.js b/packages/builder/lib/tasks/bundlers/generateFlexChangesBundle.js
index 8c2f8b0feca..99b77bc3a7a 100644
--- a/packages/builder/lib/tasks/bundlers/generateFlexChangesBundle.js
+++ b/packages/builder/lib/tasks/bundlers/generateFlexChangesBundle.js
@@ -2,6 +2,7 @@ import {getLogger} from "@ui5/logger";
const log = getLogger("builder:tasks:bundlers:generateFlexChangesBundle");
import flexChangesBundler from "../../processors/bundlers/flexChangesBundler.js";
import semver from "semver";
+import parseJSON from "../../utils/json.js";
/**
* @public
@@ -79,7 +80,7 @@ export default async function({workspace, taskUtil, options = {}}) {
log.verbose("No manifest.json found, skipping update of sap.ui.fl dependency and flexBundle flag");
return;
}
- const manifestContent = JSON.parse(await manifestResource.getString());
+ const manifestContent = parseJSON(await manifestResource.getString(), manifestResource.getPath());
updateJson(manifestContent, flexBundleFlag);
manifestResource.setString(JSON.stringify(manifestContent, null, "\t"));
@@ -93,7 +94,7 @@ export default async function({workspace, taskUtil, options = {}}) {
log.verbose("No manifest.json found, cannot read minUI5Version");
return [];
}
- const manifestContent = JSON.parse(await manifestResource.getString());
+ const manifestContent = parseJSON(await manifestResource.getString(), manifestResource.getPath());
manifestContent["sap.ui5"] = manifestContent["sap.ui5"] || {};
manifestContent["sap.ui5"].dependencies = manifestContent["sap.ui5"].dependencies || {};
@@ -123,7 +124,7 @@ export default async function({workspace, taskUtil, options = {}}) {
hasFlexBundleVersion = true;
const flexBundleResource = await workspace.byPath(`${pathPrefix}/changes/flexibility-bundle.json`);
if (flexBundleResource) {
- flexBundle = JSON.parse(await flexBundleResource.getString());
+ flexBundle = parseJSON(await flexBundleResource.getString(), flexBundleResource.getPath());
}
}
flexChangesBundleResult = await flexChangesBundler({
diff --git a/packages/builder/lib/utils/json.js b/packages/builder/lib/utils/json.js
new file mode 100644
index 00000000000..2a3329f9e67
--- /dev/null
+++ b/packages/builder/lib/utils/json.js
@@ -0,0 +1,28 @@
+/**
+ * Parses JSON content, attaching the file path to any parse failure so the
+ * root cause (empty file vs. syntax error) is always identifiable.
+ *
+ * An empty JSON object ({}) is valid and parsed normally; only content-less
+ * files (empty or whitespace-only) throw ERR_EMPTY_JSON. Any other parse
+ * failure is re-thrown as ERR_INVALID_JSON with the file path prefixed.
+ *
+ * @param {string} content Raw file content
+ * @param {string} filePath Path of the file the content was read from, used in the error message
+ * @returns {object} Parsed JSON
+ * @throws {SyntaxError} with code "ERR_EMPTY_JSON" if content is empty/whitespace-only
+ * @throws {SyntaxError} with code "ERR_INVALID_JSON" if content is malformed JSON
+ */
+export default function parseJSON(content, filePath) {
+ try {
+ return JSON.parse(content);
+ } catch (err) {
+ if (typeof content === "string" && content.trim() === "") {
+ const error = new SyntaxError(`The file "${filePath}" is empty.`);
+ error.code = "ERR_EMPTY_JSON";
+ throw error;
+ }
+ const error = new SyntaxError(`Failed to parse "${filePath}" as JSON: ${err.message}`);
+ error.code = "ERR_INVALID_JSON";
+ throw error;
+ }
+}
diff --git a/packages/builder/test/lib/processors/manifestEnhancer.js b/packages/builder/test/lib/processors/manifestEnhancer.js
index 2c83f491ada..f7fca42de03 100644
--- a/packages/builder/test/lib/processors/manifestEnhancer.js
+++ b/packages/builder/test/lib/processors/manifestEnhancer.js
@@ -3043,6 +3043,16 @@ test("ManifestEnhancer#run: multiple parallel executions are not supported", asy
});
});
+test("ManifestEnhancer: empty manifest content throws a clear error", (t) => {
+ const {fs} = t.context;
+ const {ManifestEnhancer} = t.context.__internals__;
+
+ const error = t.throws(() => new ManifestEnhancer("", "/manifest.json", fs));
+ t.is(error.message, `The file "/manifest.json" is empty.`,
+ "Threw with the clear empty-file error message");
+ t.is(error.code, "ERR_EMPTY_JSON", "Threw with the expected error code");
+});
+
test("manifestEnhancer#getSupportedLocales", async (t) => {
const {fs} = t.context;
const {ManifestEnhancer} = t.context.__internals__;
diff --git a/packages/builder/test/lib/utils/json.js b/packages/builder/test/lib/utils/json.js
new file mode 100644
index 00000000000..46598a295ed
--- /dev/null
+++ b/packages/builder/test/lib/utils/json.js
@@ -0,0 +1,48 @@
+import test from "ava";
+import parseJSON from "../../../lib/utils/json.js";
+
+test("parseJSON: Parses valid JSON content", (t) => {
+ const result = parseJSON(`{"foo": "bar"}`, "/some/file.json");
+ t.deepEqual(result, {foo: "bar"}, "Returned the parsed object");
+});
+
+test("parseJSON: Parses an empty JSON object", (t) => {
+ const result = parseJSON("{}", "/some/file.json");
+ t.deepEqual(result, {}, "An empty JSON object is valid and parsed correctly");
+});
+
+test("parseJSON: Throws for empty content", (t) => {
+ const error = t.throws(() => parseJSON("", "webapp/manifest.json"));
+ t.true(error instanceof SyntaxError, "Threw a SyntaxError");
+ t.is(error.message, `The file "webapp/manifest.json" is empty.`,
+ "Threw with the expected error message");
+ t.is(error.code, "ERR_EMPTY_JSON", "Threw with the expected error code");
+});
+
+test("parseJSON: Throws for whitespace-only content", (t) => {
+ const error = t.throws(() => parseJSON(" \n\t ", "webapp/manifest.json"));
+ t.true(error instanceof SyntaxError, "Threw a SyntaxError");
+ t.is(error.message, `The file "webapp/manifest.json" is empty.`,
+ "Threw with the expected error message");
+ t.is(error.code, "ERR_EMPTY_JSON", "Threw with the expected error code");
+});
+
+test("parseJSON: Throws ERR_INVALID_JSON for malformed JSON", (t) => {
+ const error = t.throws(() => parseJSON("{bad json", "/some/file.json"));
+ t.true(error instanceof SyntaxError, "Threw a SyntaxError");
+ t.is(error.code, "ERR_INVALID_JSON", "Threw with the expected error code");
+ t.true(error.message.startsWith(`Failed to parse "/some/file.json" as JSON:`),
+ "Error message includes the file path and root cause");
+});
+
+test("parseJSON: null content is treated as valid JSON returning null", (t) => {
+ // JSON.parse(null) succeeds — null is valid JSON — so no error is thrown
+ const result = parseJSON(null, "/some/file.json");
+ t.is(result, null, "JSON.parse(null) returns null without throwing");
+});
+
+test("parseJSON: Throws ERR_INVALID_JSON for undefined content", (t) => {
+ const error = t.throws(() => parseJSON(undefined, "/some/file.json"));
+ t.is(error.code, "ERR_INVALID_JSON", "Threw with the expected error code");
+});
+
diff --git a/packages/project/lib/specifications/types/Application.js b/packages/project/lib/specifications/types/Application.js
index f46c8a7fb9a..140e06b81dd 100644
--- a/packages/project/lib/specifications/types/Application.js
+++ b/packages/project/lib/specifications/types/Application.js
@@ -1,6 +1,7 @@
import fsPath from "node:path";
import ComponentProject from "../ComponentProject.js";
import {createReader} from "@ui5/fs/resourceFactory";
+import parseJSON from "../../utils/json.js";
/**
* Application
@@ -244,7 +245,7 @@ class Application extends ComponentProject {
error.code = "ENOENT"; // "File or directory does not exist"
throw error;
}
- return JSON.parse(await resource.getString());
+ return parseJSON(await resource.getString(), `${this._webappPath}${filePath}`);
}).catch((err) => {
if (err.code === "ENOENT") {
throw err;
diff --git a/packages/project/lib/specifications/types/Component.js b/packages/project/lib/specifications/types/Component.js
index 54a19df7f51..5f0388215b1 100644
--- a/packages/project/lib/specifications/types/Component.js
+++ b/packages/project/lib/specifications/types/Component.js
@@ -2,6 +2,7 @@ import fsPath from "node:path";
import posixPath from "node:path/posix";
import ComponentProject from "../ComponentProject.js";
import {createReader} from "@ui5/fs/resourceFactory";
+import parseJSON from "../../utils/json.js";
/**
* Component
@@ -279,11 +280,16 @@ class Component extends ComponentProject {
return this._pManifests[filePath] = this._getRawSourceReader().byPath(filePath)
.then(async (resource) => {
if (!resource) {
- throw new Error(
+ const error = new Error(
`Could not find resource ${filePath} in project ${this.getName()}`);
+ error.code = "ENOENT"; // "File or directory does not exist"
+ throw error;
}
- return JSON.parse(await resource.getString());
+ return parseJSON(await resource.getString(), `${this._srcPath}${filePath}`);
}).catch((err) => {
+ if (err.code === "ENOENT") {
+ throw err;
+ }
throw new Error(
`Failed to read ${filePath} for project ` +
`${this.getName()}: ${err.message}`);
diff --git a/packages/project/lib/specifications/types/Library.js b/packages/project/lib/specifications/types/Library.js
index 45d1c098a1e..3d0bfddad53 100644
--- a/packages/project/lib/specifications/types/Library.js
+++ b/packages/project/lib/specifications/types/Library.js
@@ -3,6 +3,7 @@ import posixPath from "node:path/posix";
import {promisify} from "node:util";
import ComponentProject from "../ComponentProject.js";
import * as resourceFactory from "@ui5/fs/resourceFactory";
+import parseJSON from "../../utils/json.js";
/**
* Library
@@ -498,7 +499,9 @@ class Library extends ComponentProject {
return this._pManifest = this._getRawSourceReader().byGlob("**/manifest.json")
.then(async (manifestResources) => {
if (!manifestResources.length) {
- throw new Error(`Could not find manifest.json file for project ${this.getName()}`);
+ const error = new Error(`Could not find manifest.json file for project ${this.getName()}`);
+ error.code = "ENOENT"; // "File or directory does not exist"
+ throw error;
}
if (manifestResources.length > 1) {
throw new Error(`Found multiple (${manifestResources.length}) manifest.json files ` +
@@ -507,10 +510,13 @@ class Library extends ComponentProject {
const resource = manifestResources[0];
try {
return {
- content: JSON.parse(await resource.getString()),
+ content: parseJSON(await resource.getString(), resource.getPath()),
filePath: resource.getPath()
};
} catch (err) {
+ if (err.code === "ENOENT") {
+ throw err;
+ }
throw new Error(
`Failed to read ${resource.getPath()} for project ${this.getName()}: ${err.message}`);
}
diff --git a/packages/project/lib/utils/json.js b/packages/project/lib/utils/json.js
new file mode 100644
index 00000000000..2a3329f9e67
--- /dev/null
+++ b/packages/project/lib/utils/json.js
@@ -0,0 +1,28 @@
+/**
+ * Parses JSON content, attaching the file path to any parse failure so the
+ * root cause (empty file vs. syntax error) is always identifiable.
+ *
+ * An empty JSON object ({}) is valid and parsed normally; only content-less
+ * files (empty or whitespace-only) throw ERR_EMPTY_JSON. Any other parse
+ * failure is re-thrown as ERR_INVALID_JSON with the file path prefixed.
+ *
+ * @param {string} content Raw file content
+ * @param {string} filePath Path of the file the content was read from, used in the error message
+ * @returns {object} Parsed JSON
+ * @throws {SyntaxError} with code "ERR_EMPTY_JSON" if content is empty/whitespace-only
+ * @throws {SyntaxError} with code "ERR_INVALID_JSON" if content is malformed JSON
+ */
+export default function parseJSON(content, filePath) {
+ try {
+ return JSON.parse(content);
+ } catch (err) {
+ if (typeof content === "string" && content.trim() === "") {
+ const error = new SyntaxError(`The file "${filePath}" is empty.`);
+ error.code = "ERR_EMPTY_JSON";
+ throw error;
+ }
+ const error = new SyntaxError(`Failed to parse "${filePath}" as JSON: ${err.message}`);
+ error.code = "ERR_INVALID_JSON";
+ throw error;
+ }
+}
diff --git a/packages/project/test/lib/specifications/types/Application.js b/packages/project/test/lib/specifications/types/Application.js
index cf27337910d..7d72e4694a7 100644
--- a/packages/project/test/lib/specifications/types/Application.js
+++ b/packages/project/test/lib/specifications/types/Application.js
@@ -575,12 +575,34 @@ test("_getManifest: invalid JSON", async (t) => {
};
const error = await t.throwsAsync(project._getManifest("/some-manifest.json"));
- t.regex(error.message, /^Failed to read \/some-manifest\.json for project application\.a: /,
- "Rejected with correct error message");
+ t.true(error.message.startsWith(
+ `Failed to read /some-manifest.json for project application.a: ` +
+ `Failed to parse "webapp/some-manifest.json" as JSON`),
+ "Rejected with the wrapped invalid-JSON error message");
t.is(byPathStub.callCount, 1, "byPath got called once");
t.is(byPathStub.getCall(0).args[0], "/some-manifest.json", "byPath got called with the correct argument");
});
+test("_getManifest: empty file", async (t) => {
+ const {projectInput, sinon} = t.context;
+ const project = await Specification.create(projectInput);
+
+ const byPathStub = sinon.stub().resolves({
+ getString: async () => ""
+ });
+
+ project._getRawSourceReader = () => {
+ return {
+ byPath: byPathStub
+ };
+ };
+
+ const error = await t.throwsAsync(project._getManifest("/some-manifest.json"));
+ t.is(error.message,
+ `Failed to read /some-manifest.json for project application.a: The file "webapp/some-manifest.json" is empty.`,
+ "Rejected with the wrapped empty-file error message");
+});
+
test.serial("_getManifest: File does not exist", async (t) => {
const {projectInput} = t.context;
const project = await Specification.create(projectInput);
diff --git a/packages/project/test/lib/specifications/types/Component.js b/packages/project/test/lib/specifications/types/Component.js
index 55d78b22bcf..8659e65e40b 100644
--- a/packages/project/test/lib/specifications/types/Component.js
+++ b/packages/project/test/lib/specifications/types/Component.js
@@ -573,21 +573,43 @@ test("_getManifest: invalid JSON", async (t) => {
};
const error = await t.throwsAsync(project._getManifest("/some-manifest.json"));
- t.regex(error.message, /^Failed to read \/some-manifest\.json for project component\.a: /,
- "Rejected with correct error message");
+ t.true(error.message.startsWith(
+ `Failed to read /some-manifest.json for project component.a: ` +
+ `Failed to parse "src/some-manifest.json" as JSON`),
+ "Rejected with the wrapped invalid-JSON error message");
t.is(byPathStub.callCount, 1, "byPath got called once");
t.is(byPathStub.getCall(0).args[0], "/some-manifest.json", "byPath got called with the correct argument");
});
+test("_getManifest: empty file", async (t) => {
+ const {projectInput, sinon} = t.context;
+ const project = await Specification.create(projectInput);
+
+ const byPathStub = sinon.stub().resolves({
+ getString: async () => ""
+ });
+
+ project._getRawSourceReader = () => {
+ return {
+ byPath: byPathStub
+ };
+ };
+
+ const error = await t.throwsAsync(project._getManifest("/some-manifest.json"));
+ t.is(error.message,
+ `Failed to read /some-manifest.json for project component.a: The file "src/some-manifest.json" is empty.`,
+ "Rejected with the wrapped empty-file error message");
+});
+
test.serial("_getManifest: File does not exist", async (t) => {
const {projectInput} = t.context;
const project = await Specification.create(projectInput);
const error = await t.throwsAsync(project._getManifest("/does-not-exist.json"));
- t.deepEqual(error.message,
- "Failed to read /does-not-exist.json for project component.a: " +
+ t.is(error.message,
"Could not find resource /does-not-exist.json in project component.a",
"Rejected with correct error message");
+ t.is(error.code, "ENOENT");
});
test.serial("_getManifest: result is cached", async (t) => {
diff --git a/packages/project/test/lib/specifications/types/Library.js b/packages/project/test/lib/specifications/types/Library.js
index 0101e019408..2370e4276ef 100644
--- a/packages/project/test/lib/specifications/types/Library.js
+++ b/packages/project/test/lib/specifications/types/Library.js
@@ -715,8 +715,32 @@ test("_getManifest: Invalid JSON", async (t) => {
};
project._pManifest = null; // Clear cache from instantiation
const error = await t.throwsAsync(project._getManifest());
- t.regex(error.message, /^Failed to read some path for project library\.d: /,
- "Rejected with correct error message");
+ t.true(error.message.startsWith(
+ `Failed to read some path for project library.d: Failed to parse "some path" as JSON`),
+ "Rejected with the wrapped invalid-JSON error message");
+ t.is(byGlobStub.callCount, 1, "byGlob got called once");
+ t.is(byGlobStub.getCall(0).args[0], "**/manifest.json", "byGlob got called with the expected arguments");
+});
+
+test("_getManifest: Empty file", async (t) => {
+ const {projectInput, sinon} = t.context;
+
+ const project = await (new Library().init(projectInput));
+ const byGlobStub = sinon.stub().resolves([{
+ getString: async () => "",
+ getPath: () => "some path"
+ }]);
+
+ project._getRawSourceReader = () => {
+ return {
+ byGlob: byGlobStub
+ };
+ };
+ project._pManifest = null; // Clear cache from instantiation
+ const error = await t.throwsAsync(project._getManifest());
+ t.is(error.message,
+ `Failed to read some path for project library.d: The file "some path" is empty.`,
+ "Rejected with the wrapped empty-file error message");
t.is(byGlobStub.callCount, 1, "byGlob got called once");
t.is(byGlobStub.getCall(0).args[0], "**/manifest.json", "byGlob got called with the expected arguments");
});
diff --git a/packages/project/test/lib/utils/json.js b/packages/project/test/lib/utils/json.js
new file mode 100644
index 00000000000..46598a295ed
--- /dev/null
+++ b/packages/project/test/lib/utils/json.js
@@ -0,0 +1,48 @@
+import test from "ava";
+import parseJSON from "../../../lib/utils/json.js";
+
+test("parseJSON: Parses valid JSON content", (t) => {
+ const result = parseJSON(`{"foo": "bar"}`, "/some/file.json");
+ t.deepEqual(result, {foo: "bar"}, "Returned the parsed object");
+});
+
+test("parseJSON: Parses an empty JSON object", (t) => {
+ const result = parseJSON("{}", "/some/file.json");
+ t.deepEqual(result, {}, "An empty JSON object is valid and parsed correctly");
+});
+
+test("parseJSON: Throws for empty content", (t) => {
+ const error = t.throws(() => parseJSON("", "webapp/manifest.json"));
+ t.true(error instanceof SyntaxError, "Threw a SyntaxError");
+ t.is(error.message, `The file "webapp/manifest.json" is empty.`,
+ "Threw with the expected error message");
+ t.is(error.code, "ERR_EMPTY_JSON", "Threw with the expected error code");
+});
+
+test("parseJSON: Throws for whitespace-only content", (t) => {
+ const error = t.throws(() => parseJSON(" \n\t ", "webapp/manifest.json"));
+ t.true(error instanceof SyntaxError, "Threw a SyntaxError");
+ t.is(error.message, `The file "webapp/manifest.json" is empty.`,
+ "Threw with the expected error message");
+ t.is(error.code, "ERR_EMPTY_JSON", "Threw with the expected error code");
+});
+
+test("parseJSON: Throws ERR_INVALID_JSON for malformed JSON", (t) => {
+ const error = t.throws(() => parseJSON("{bad json", "/some/file.json"));
+ t.true(error instanceof SyntaxError, "Threw a SyntaxError");
+ t.is(error.code, "ERR_INVALID_JSON", "Threw with the expected error code");
+ t.true(error.message.startsWith(`Failed to parse "/some/file.json" as JSON:`),
+ "Error message includes the file path and root cause");
+});
+
+test("parseJSON: null content is treated as valid JSON returning null", (t) => {
+ // JSON.parse(null) succeeds — null is valid JSON — so no error is thrown
+ const result = parseJSON(null, "/some/file.json");
+ t.is(result, null, "JSON.parse(null) returns null without throwing");
+});
+
+test("parseJSON: Throws ERR_INVALID_JSON for undefined content", (t) => {
+ const error = t.throws(() => parseJSON(undefined, "/some/file.json"));
+ t.is(error.code, "ERR_INVALID_JSON", "Threw with the expected error code");
+});
+