From 24d4272b35b8fb3c75a163d55bef75268c7460fe Mon Sep 17 00:00:00 2001 From: d3xter666 Date: Fri, 16 Oct 2026 16:59:32 +0300 Subject: [PATCH 1/5] refactor: Handle empty JSON (manifests) in builder --- .../lib/lbt/analyzer/ComponentAnalyzer.js | 3 +- .../lib/lbt/analyzer/SmartTemplateAnalyzer.js | 3 +- .../lib/processors/manifestEnhancer.js | 3 +- .../lib/processors/versionInfoGenerator.js | 3 +- .../bundlers/generateFlexChangesBundle.js | 7 +-- packages/builder/lib/utils/json.js | 24 ++++++++++ .../test/lib/processors/manifestEnhancer.js | 10 +++++ packages/builder/test/lib/utils/json.js | 45 +++++++++++++++++++ 8 files changed, 91 insertions(+), 7 deletions(-) create mode 100644 packages/builder/lib/utils/json.js create mode 100644 packages/builder/test/lib/utils/json.js 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..a9e458d4a46 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..201e1a97516 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..6daa2aded0c --- /dev/null +++ b/packages/builder/lib/utils/json.js @@ -0,0 +1,24 @@ +/** + * Parses the given JSON content, throwing a descriptive error if the content is empty. + * + * An empty JSON object ({}) is valid and parsed normally; only content-less + * files (empty or whitespace-only) are rejected. For non-empty but invalid JSON, the native + * JSON.parse error is thrown unchanged. + * + * @param {string|Buffer} content Raw file/resource content + * @param {string|function():string} filePath Path of the file the content was read from, used in the error + * message. May be a function that is only invoked (lazily) when an empty file is detected, to avoid + * computing the path on the success path. + * @returns {object} Parsed JSON + * @throws {Error} with code "ERR_EMPTY_JSON" if the content is empty; otherwise the native JSON.parse error + */ +export default function parseJSON(content, filePath) { + const text = typeof content === "string" ? content : content.toString(); + if (text.trim() === "") { + const path = typeof filePath === "function" ? filePath() : filePath; + const error = new Error(`The file "${path}" is empty.`); + error.code = "ERR_EMPTY_JSON"; + throw error; + } + return JSON.parse(content); +} 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..436d0b20ea3 --- /dev/null +++ b/packages/builder/test/lib/utils/json.js @@ -0,0 +1,45 @@ +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: Parses Buffer content", (t) => { + const result = parseJSON(Buffer.from(`{"foo": "bar"}`), "/some/file.json"); + t.deepEqual(result, {foo: "bar"}, "Returned the parsed object from Buffer input"); +}); + +test("parseJSON: Throws for empty content", (t) => { + const error = t.throws(() => parseJSON("", "webapp/manifest.json")); + 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.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 an empty Buffer", (t) => { + const error = t.throws(() => parseJSON(Buffer.from(""), "webapp/manifest.json")); + 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 native error for non-empty invalid JSON", (t) => { + const error = t.throws(() => parseJSON("no json", "/some/file.json")); + t.not(error.code, "ERR_EMPTY_JSON", + "Invalid (but non-empty) JSON is not reported as an empty file"); + t.true(error instanceof SyntaxError, "Threw the native JSON.parse SyntaxError"); +}); From 7f087154f4255c3709321a10c9309f3359a5943f Mon Sep 17 00:00:00 2001 From: d3xter666 Date: Fri, 16 Oct 2026 17:39:16 +0300 Subject: [PATCH 2/5] refactor: Handle empty JSON (manifests) in project --- .../lib/specifications/types/Application.js | 5 ++- .../lib/specifications/types/Component.js | 6 ++- .../lib/specifications/types/Library.js | 6 ++- packages/project/lib/utils/json.js | 24 ++++++++++ .../lib/specifications/types/Application.js | 20 +++++++++ .../lib/specifications/types/Component.js | 20 +++++++++ packages/project/test/lib/utils/json.js | 45 +++++++++++++++++++ 7 files changed, 122 insertions(+), 4 deletions(-) create mode 100644 packages/project/lib/utils/json.js create mode 100644 packages/project/test/lib/utils/json.js diff --git a/packages/project/lib/specifications/types/Application.js b/packages/project/lib/specifications/types/Application.js index f46c8a7fb9a..33ffdd8f1db 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,9 +245,9 @@ 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") { + if (err.code === "ENOENT" || err.code === "ERR_EMPTY_JSON") { throw err; } throw new Error( diff --git a/packages/project/lib/specifications/types/Component.js b/packages/project/lib/specifications/types/Component.js index 54a19df7f51..c3c4c92e75d 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 @@ -282,8 +283,11 @@ class Component extends ComponentProject { throw new Error( `Could not find resource ${filePath} in project ${this.getName()}`); } - return JSON.parse(await resource.getString()); + return parseJSON(await resource.getString(), `${this._srcPath}${filePath}`); }).catch((err) => { + if (err.code === "ERR_EMPTY_JSON") { + 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..660f1bf014a 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 @@ -507,10 +508,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 === "ERR_EMPTY_JSON") { + 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..6daa2aded0c --- /dev/null +++ b/packages/project/lib/utils/json.js @@ -0,0 +1,24 @@ +/** + * Parses the given JSON content, throwing a descriptive error if the content is empty. + * + * An empty JSON object ({}) is valid and parsed normally; only content-less + * files (empty or whitespace-only) are rejected. For non-empty but invalid JSON, the native + * JSON.parse error is thrown unchanged. + * + * @param {string|Buffer} content Raw file/resource content + * @param {string|function():string} filePath Path of the file the content was read from, used in the error + * message. May be a function that is only invoked (lazily) when an empty file is detected, to avoid + * computing the path on the success path. + * @returns {object} Parsed JSON + * @throws {Error} with code "ERR_EMPTY_JSON" if the content is empty; otherwise the native JSON.parse error + */ +export default function parseJSON(content, filePath) { + const text = typeof content === "string" ? content : content.toString(); + if (text.trim() === "") { + const path = typeof filePath === "function" ? filePath() : filePath; + const error = new Error(`The file "${path}" is empty.`); + error.code = "ERR_EMPTY_JSON"; + throw error; + } + return JSON.parse(content); +} diff --git a/packages/project/test/lib/specifications/types/Application.js b/packages/project/test/lib/specifications/types/Application.js index cf27337910d..59cd3cfffd8 100644 --- a/packages/project/test/lib/specifications/types/Application.js +++ b/packages/project/test/lib/specifications/types/Application.js @@ -581,6 +581,26 @@ test("_getManifest: invalid JSON", async (t) => { 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, `The file "webapp/some-manifest.json" is empty.`, + "Rejected with the clear empty-file error message (not wrapped)"); + t.is(error.code, "ERR_EMPTY_JSON", "Error carries the expected code"); +}); + 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..035f749a81c 100644 --- a/packages/project/test/lib/specifications/types/Component.js +++ b/packages/project/test/lib/specifications/types/Component.js @@ -579,6 +579,26 @@ test("_getManifest: invalid JSON", async (t) => { 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, `The file "src/some-manifest.json" is empty.`, + "Rejected with the clear empty-file error message (not wrapped)"); + t.is(error.code, "ERR_EMPTY_JSON", "Error carries the expected code"); +}); + 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/utils/json.js b/packages/project/test/lib/utils/json.js new file mode 100644 index 00000000000..436d0b20ea3 --- /dev/null +++ b/packages/project/test/lib/utils/json.js @@ -0,0 +1,45 @@ +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: Parses Buffer content", (t) => { + const result = parseJSON(Buffer.from(`{"foo": "bar"}`), "/some/file.json"); + t.deepEqual(result, {foo: "bar"}, "Returned the parsed object from Buffer input"); +}); + +test("parseJSON: Throws for empty content", (t) => { + const error = t.throws(() => parseJSON("", "webapp/manifest.json")); + 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.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 an empty Buffer", (t) => { + const error = t.throws(() => parseJSON(Buffer.from(""), "webapp/manifest.json")); + 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 native error for non-empty invalid JSON", (t) => { + const error = t.throws(() => parseJSON("no json", "/some/file.json")); + t.not(error.code, "ERR_EMPTY_JSON", + "Invalid (but non-empty) JSON is not reported as an empty file"); + t.true(error instanceof SyntaxError, "Threw the native JSON.parse SyntaxError"); +}); From 5c4d28335679bedf856261acd3c77c3f99b566fa Mon Sep 17 00:00:00 2001 From: d3xter666 Date: Mon, 19 Oct 2026 11:31:25 +0300 Subject: [PATCH 3/5] refactor: Simplify parseJSON util --- .../builder/lib/processors/versionInfoGenerator.js | 2 +- .../lib/tasks/bundlers/generateFlexChangesBundle.js | 6 +++--- packages/builder/lib/utils/json.js | 12 ++++-------- packages/builder/test/lib/utils/json.js | 12 ------------ packages/project/lib/utils/json.js | 12 ++++-------- packages/project/test/lib/utils/json.js | 12 ------------ 6 files changed, 12 insertions(+), 44 deletions(-) diff --git a/packages/builder/lib/processors/versionInfoGenerator.js b/packages/builder/lib/processors/versionInfoGenerator.js index a9e458d4a46..21ff0d36c88 100644 --- a/packages/builder/lib/processors/versionInfoGenerator.js +++ b/packages/builder/lib/processors/versionInfoGenerator.js @@ -60,7 +60,7 @@ function getTimestamp() { */ const processManifest = async (manifestResource) => { const manifestContent = await manifestResource.getString(); - const manifestObject = parseJSON(manifestContent, () => manifestResource.getPath()); + 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 201e1a97516..99b77bc3a7a 100644 --- a/packages/builder/lib/tasks/bundlers/generateFlexChangesBundle.js +++ b/packages/builder/lib/tasks/bundlers/generateFlexChangesBundle.js @@ -80,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 = parseJSON(await manifestResource.getString(), () => manifestResource.getPath()); + const manifestContent = parseJSON(await manifestResource.getString(), manifestResource.getPath()); updateJson(manifestContent, flexBundleFlag); manifestResource.setString(JSON.stringify(manifestContent, null, "\t")); @@ -94,7 +94,7 @@ export default async function({workspace, taskUtil, options = {}}) { log.verbose("No manifest.json found, cannot read minUI5Version"); return []; } - const manifestContent = parseJSON(await manifestResource.getString(), () => manifestResource.getPath()); + const manifestContent = parseJSON(await manifestResource.getString(), manifestResource.getPath()); manifestContent["sap.ui5"] = manifestContent["sap.ui5"] || {}; manifestContent["sap.ui5"].dependencies = manifestContent["sap.ui5"].dependencies || {}; @@ -124,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 = parseJSON(await flexBundleResource.getString(), () => flexBundleResource.getPath()); + 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 index 6daa2aded0c..187da939de5 100644 --- a/packages/builder/lib/utils/json.js +++ b/packages/builder/lib/utils/json.js @@ -5,18 +5,14 @@ * files (empty or whitespace-only) are rejected. For non-empty but invalid JSON, the native * JSON.parse error is thrown unchanged. * - * @param {string|Buffer} content Raw file/resource content - * @param {string|function():string} filePath Path of the file the content was read from, used in the error - * message. May be a function that is only invoked (lazily) when an empty file is detected, to avoid - * computing the path on the success path. + * @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 {Error} with code "ERR_EMPTY_JSON" if the content is empty; otherwise the native JSON.parse error */ export default function parseJSON(content, filePath) { - const text = typeof content === "string" ? content : content.toString(); - if (text.trim() === "") { - const path = typeof filePath === "function" ? filePath() : filePath; - const error = new Error(`The file "${path}" is empty.`); + if (content.trim() === "") { + const error = new Error(`The file "${filePath}" is empty.`); error.code = "ERR_EMPTY_JSON"; throw error; } diff --git a/packages/builder/test/lib/utils/json.js b/packages/builder/test/lib/utils/json.js index 436d0b20ea3..f2d014899e9 100644 --- a/packages/builder/test/lib/utils/json.js +++ b/packages/builder/test/lib/utils/json.js @@ -11,11 +11,6 @@ test("parseJSON: Parses an empty JSON object", (t) => { t.deepEqual(result, {}, "An empty JSON object is valid and parsed correctly"); }); -test("parseJSON: Parses Buffer content", (t) => { - const result = parseJSON(Buffer.from(`{"foo": "bar"}`), "/some/file.json"); - t.deepEqual(result, {foo: "bar"}, "Returned the parsed object from Buffer input"); -}); - test("parseJSON: Throws for empty content", (t) => { const error = t.throws(() => parseJSON("", "webapp/manifest.json")); t.is(error.message, `The file "webapp/manifest.json" is empty.`, @@ -30,13 +25,6 @@ test("parseJSON: Throws for whitespace-only content", (t) => { t.is(error.code, "ERR_EMPTY_JSON", "Threw with the expected error code"); }); -test("parseJSON: Throws for an empty Buffer", (t) => { - const error = t.throws(() => parseJSON(Buffer.from(""), "webapp/manifest.json")); - 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 native error for non-empty invalid JSON", (t) => { const error = t.throws(() => parseJSON("no json", "/some/file.json")); t.not(error.code, "ERR_EMPTY_JSON", diff --git a/packages/project/lib/utils/json.js b/packages/project/lib/utils/json.js index 6daa2aded0c..187da939de5 100644 --- a/packages/project/lib/utils/json.js +++ b/packages/project/lib/utils/json.js @@ -5,18 +5,14 @@ * files (empty or whitespace-only) are rejected. For non-empty but invalid JSON, the native * JSON.parse error is thrown unchanged. * - * @param {string|Buffer} content Raw file/resource content - * @param {string|function():string} filePath Path of the file the content was read from, used in the error - * message. May be a function that is only invoked (lazily) when an empty file is detected, to avoid - * computing the path on the success path. + * @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 {Error} with code "ERR_EMPTY_JSON" if the content is empty; otherwise the native JSON.parse error */ export default function parseJSON(content, filePath) { - const text = typeof content === "string" ? content : content.toString(); - if (text.trim() === "") { - const path = typeof filePath === "function" ? filePath() : filePath; - const error = new Error(`The file "${path}" is empty.`); + if (content.trim() === "") { + const error = new Error(`The file "${filePath}" is empty.`); error.code = "ERR_EMPTY_JSON"; throw error; } diff --git a/packages/project/test/lib/utils/json.js b/packages/project/test/lib/utils/json.js index 436d0b20ea3..f2d014899e9 100644 --- a/packages/project/test/lib/utils/json.js +++ b/packages/project/test/lib/utils/json.js @@ -11,11 +11,6 @@ test("parseJSON: Parses an empty JSON object", (t) => { t.deepEqual(result, {}, "An empty JSON object is valid and parsed correctly"); }); -test("parseJSON: Parses Buffer content", (t) => { - const result = parseJSON(Buffer.from(`{"foo": "bar"}`), "/some/file.json"); - t.deepEqual(result, {foo: "bar"}, "Returned the parsed object from Buffer input"); -}); - test("parseJSON: Throws for empty content", (t) => { const error = t.throws(() => parseJSON("", "webapp/manifest.json")); t.is(error.message, `The file "webapp/manifest.json" is empty.`, @@ -30,13 +25,6 @@ test("parseJSON: Throws for whitespace-only content", (t) => { t.is(error.code, "ERR_EMPTY_JSON", "Threw with the expected error code"); }); -test("parseJSON: Throws for an empty Buffer", (t) => { - const error = t.throws(() => parseJSON(Buffer.from(""), "webapp/manifest.json")); - 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 native error for non-empty invalid JSON", (t) => { const error = t.throws(() => parseJSON("no json", "/some/file.json")); t.not(error.code, "ERR_EMPTY_JSON", From 85ac8423b1b39574a49ad51a38479db27b87c3dc Mon Sep 17 00:00:00 2001 From: d3xter666 Date: Tue, 20 Oct 2026 09:13:05 +0300 Subject: [PATCH 4/5] refactor: Improve parseJSON util to also handle malformed JSON Extend the parseJSON helper in project and builder packages to use try/catch around JSON.parse, distinguishing empty content (ERR_EMPTY_JSON) from malformed content (ERR_INVALID_JSON, thrown as SyntaxError). Update manifest readers in Application and Component to re-throw ERR_INVALID_JSON unwrapped, so the file path is preserved in the message shown to users instead of being hidden behind a generic "Failed to read" wrapper. --- packages/builder/lib/utils/json.js | 24 ++++++++++----- packages/builder/test/lib/utils/json.js | 25 ++++++++++++---- .../lib/specifications/types/Application.js | 2 +- .../lib/specifications/types/Component.js | 2 +- .../lib/specifications/types/Library.js | 2 +- packages/project/lib/utils/json.js | 24 ++++++++++----- .../lib/specifications/types/Application.js | 6 ++-- .../lib/specifications/types/Component.js | 6 ++-- .../test/lib/specifications/types/Library.js | 29 +++++++++++++++++-- packages/project/test/lib/utils/json.js | 25 ++++++++++++---- 10 files changed, 110 insertions(+), 35 deletions(-) diff --git a/packages/builder/lib/utils/json.js b/packages/builder/lib/utils/json.js index 187da939de5..2a3329f9e67 100644 --- a/packages/builder/lib/utils/json.js +++ b/packages/builder/lib/utils/json.js @@ -1,20 +1,28 @@ /** - * Parses the given JSON content, throwing a descriptive error if the content is empty. + * 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) are rejected. For non-empty but invalid JSON, the native - * JSON.parse error is thrown unchanged. + * 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 {Error} with code "ERR_EMPTY_JSON" if the content is empty; otherwise the native JSON.parse error + * @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) { - if (content.trim() === "") { - const error = new Error(`The file "${filePath}" is empty.`); - error.code = "ERR_EMPTY_JSON"; + 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; } - return JSON.parse(content); } diff --git a/packages/builder/test/lib/utils/json.js b/packages/builder/test/lib/utils/json.js index f2d014899e9..46598a295ed 100644 --- a/packages/builder/test/lib/utils/json.js +++ b/packages/builder/test/lib/utils/json.js @@ -13,6 +13,7 @@ test("parseJSON: Parses an empty JSON object", (t) => { 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"); @@ -20,14 +21,28 @@ test("parseJSON: Throws for empty content", (t) => { 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 native error for non-empty invalid JSON", (t) => { - const error = t.throws(() => parseJSON("no json", "/some/file.json")); - t.not(error.code, "ERR_EMPTY_JSON", - "Invalid (but non-empty) JSON is not reported as an empty file"); - t.true(error instanceof SyntaxError, "Threw the native JSON.parse SyntaxError"); +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 33ffdd8f1db..be1b41bbeb4 100644 --- a/packages/project/lib/specifications/types/Application.js +++ b/packages/project/lib/specifications/types/Application.js @@ -247,7 +247,7 @@ class Application extends ComponentProject { } return parseJSON(await resource.getString(), `${this._webappPath}${filePath}`); }).catch((err) => { - if (err.code === "ENOENT" || err.code === "ERR_EMPTY_JSON") { + if (err.code === "ENOENT" || err.code === "ERR_EMPTY_JSON" || err.code === "ERR_INVALID_JSON") { throw err; } throw new Error( diff --git a/packages/project/lib/specifications/types/Component.js b/packages/project/lib/specifications/types/Component.js index c3c4c92e75d..ef2f4fdfa7b 100644 --- a/packages/project/lib/specifications/types/Component.js +++ b/packages/project/lib/specifications/types/Component.js @@ -285,7 +285,7 @@ class Component extends ComponentProject { } return parseJSON(await resource.getString(), `${this._srcPath}${filePath}`); }).catch((err) => { - if (err.code === "ERR_EMPTY_JSON") { + if (err.code === "ERR_EMPTY_JSON" || err.code === "ERR_INVALID_JSON") { throw err; } throw new Error( diff --git a/packages/project/lib/specifications/types/Library.js b/packages/project/lib/specifications/types/Library.js index 660f1bf014a..092b945e554 100644 --- a/packages/project/lib/specifications/types/Library.js +++ b/packages/project/lib/specifications/types/Library.js @@ -512,7 +512,7 @@ class Library extends ComponentProject { filePath: resource.getPath() }; } catch (err) { - if (err.code === "ERR_EMPTY_JSON") { + if (err.code === "ERR_EMPTY_JSON" || err.code === "ERR_INVALID_JSON") { throw err; } throw new Error( diff --git a/packages/project/lib/utils/json.js b/packages/project/lib/utils/json.js index 187da939de5..2a3329f9e67 100644 --- a/packages/project/lib/utils/json.js +++ b/packages/project/lib/utils/json.js @@ -1,20 +1,28 @@ /** - * Parses the given JSON content, throwing a descriptive error if the content is empty. + * 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) are rejected. For non-empty but invalid JSON, the native - * JSON.parse error is thrown unchanged. + * 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 {Error} with code "ERR_EMPTY_JSON" if the content is empty; otherwise the native JSON.parse error + * @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) { - if (content.trim() === "") { - const error = new Error(`The file "${filePath}" is empty.`); - error.code = "ERR_EMPTY_JSON"; + 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; } - return JSON.parse(content); } diff --git a/packages/project/test/lib/specifications/types/Application.js b/packages/project/test/lib/specifications/types/Application.js index 59cd3cfffd8..5d82e6067e4 100644 --- a/packages/project/test/lib/specifications/types/Application.js +++ b/packages/project/test/lib/specifications/types/Application.js @@ -575,8 +575,10 @@ 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.is(error.code, "ERR_INVALID_JSON", + "Rejected with the ERR_INVALID_JSON code (not wrapped)"); + t.true(error.message.startsWith(`Failed to parse "webapp/some-manifest.json" as JSON:`), + "Rejected with the clear invalid-JSON error message (not wrapped)"); 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"); }); diff --git a/packages/project/test/lib/specifications/types/Component.js b/packages/project/test/lib/specifications/types/Component.js index 035f749a81c..d04d7f1bd37 100644 --- a/packages/project/test/lib/specifications/types/Component.js +++ b/packages/project/test/lib/specifications/types/Component.js @@ -573,8 +573,10 @@ 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.is(error.code, "ERR_INVALID_JSON", + "Rejected with the ERR_INVALID_JSON code (not wrapped)"); + t.true(error.message.startsWith(`Failed to parse "src/some-manifest.json" as JSON:`), + "Rejected with the clear invalid-JSON error message (not wrapped)"); 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"); }); diff --git a/packages/project/test/lib/specifications/types/Library.js b/packages/project/test/lib/specifications/types/Library.js index 0101e019408..1af23ccef5a 100644 --- a/packages/project/test/lib/specifications/types/Library.js +++ b/packages/project/test/lib/specifications/types/Library.js @@ -715,8 +715,33 @@ 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.is(error.code, "ERR_INVALID_JSON", + "Rejected with the ERR_INVALID_JSON code (not wrapped)"); + t.true(error.message.startsWith(`Failed to parse "some path" as JSON:`), + "Rejected with the clear invalid-JSON error message (not wrapped)"); + 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, `The file "some path" is empty.`, + "Rejected with the clear empty-file error message (not wrapped)"); + t.is(error.code, "ERR_EMPTY_JSON", "Error carries the expected code"); 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 index f2d014899e9..46598a295ed 100644 --- a/packages/project/test/lib/utils/json.js +++ b/packages/project/test/lib/utils/json.js @@ -13,6 +13,7 @@ test("parseJSON: Parses an empty JSON object", (t) => { 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"); @@ -20,14 +21,28 @@ test("parseJSON: Throws for empty content", (t) => { 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 native error for non-empty invalid JSON", (t) => { - const error = t.throws(() => parseJSON("no json", "/some/file.json")); - t.not(error.code, "ERR_EMPTY_JSON", - "Invalid (but non-empty) JSON is not reported as an empty file"); - t.true(error instanceof SyntaxError, "Threw the native JSON.parse SyntaxError"); +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"); +}); + From fca52ec923d0f410891500ff062005568f3139f4 Mon Sep 17 00:00:00 2001 From: d3xter666 Date: Tue, 20 Oct 2026 17:24:40 +0300 Subject: [PATCH 5/5] refactor: Displayed messages --- .../lib/specifications/types/Application.js | 2 +- .../lib/specifications/types/Component.js | 6 ++++-- .../lib/specifications/types/Library.js | 6 ++++-- .../lib/specifications/types/Application.js | 14 +++++++------- .../test/lib/specifications/types/Component.js | 18 +++++++++--------- .../test/lib/specifications/types/Library.js | 13 ++++++------- 6 files changed, 31 insertions(+), 28 deletions(-) diff --git a/packages/project/lib/specifications/types/Application.js b/packages/project/lib/specifications/types/Application.js index be1b41bbeb4..140e06b81dd 100644 --- a/packages/project/lib/specifications/types/Application.js +++ b/packages/project/lib/specifications/types/Application.js @@ -247,7 +247,7 @@ class Application extends ComponentProject { } return parseJSON(await resource.getString(), `${this._webappPath}${filePath}`); }).catch((err) => { - if (err.code === "ENOENT" || err.code === "ERR_EMPTY_JSON" || err.code === "ERR_INVALID_JSON") { + if (err.code === "ENOENT") { throw err; } throw new Error( diff --git a/packages/project/lib/specifications/types/Component.js b/packages/project/lib/specifications/types/Component.js index ef2f4fdfa7b..5f0388215b1 100644 --- a/packages/project/lib/specifications/types/Component.js +++ b/packages/project/lib/specifications/types/Component.js @@ -280,12 +280,14 @@ 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 parseJSON(await resource.getString(), `${this._srcPath}${filePath}`); }).catch((err) => { - if (err.code === "ERR_EMPTY_JSON" || err.code === "ERR_INVALID_JSON") { + if (err.code === "ENOENT") { throw err; } throw new Error( diff --git a/packages/project/lib/specifications/types/Library.js b/packages/project/lib/specifications/types/Library.js index 092b945e554..3d0bfddad53 100644 --- a/packages/project/lib/specifications/types/Library.js +++ b/packages/project/lib/specifications/types/Library.js @@ -499,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 ` + @@ -512,7 +514,7 @@ class Library extends ComponentProject { filePath: resource.getPath() }; } catch (err) { - if (err.code === "ERR_EMPTY_JSON" || err.code === "ERR_INVALID_JSON") { + if (err.code === "ENOENT") { throw err; } throw new Error( diff --git a/packages/project/test/lib/specifications/types/Application.js b/packages/project/test/lib/specifications/types/Application.js index 5d82e6067e4..7d72e4694a7 100644 --- a/packages/project/test/lib/specifications/types/Application.js +++ b/packages/project/test/lib/specifications/types/Application.js @@ -575,10 +575,10 @@ test("_getManifest: invalid JSON", async (t) => { }; const error = await t.throwsAsync(project._getManifest("/some-manifest.json")); - t.is(error.code, "ERR_INVALID_JSON", - "Rejected with the ERR_INVALID_JSON code (not wrapped)"); - t.true(error.message.startsWith(`Failed to parse "webapp/some-manifest.json" as JSON:`), - "Rejected with the clear invalid-JSON error message (not wrapped)"); + 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"); }); @@ -598,9 +598,9 @@ test("_getManifest: empty file", async (t) => { }; const error = await t.throwsAsync(project._getManifest("/some-manifest.json")); - t.is(error.message, `The file "webapp/some-manifest.json" is empty.`, - "Rejected with the clear empty-file error message (not wrapped)"); - t.is(error.code, "ERR_EMPTY_JSON", "Error carries the expected code"); + 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) => { diff --git a/packages/project/test/lib/specifications/types/Component.js b/packages/project/test/lib/specifications/types/Component.js index d04d7f1bd37..8659e65e40b 100644 --- a/packages/project/test/lib/specifications/types/Component.js +++ b/packages/project/test/lib/specifications/types/Component.js @@ -573,10 +573,10 @@ test("_getManifest: invalid JSON", async (t) => { }; const error = await t.throwsAsync(project._getManifest("/some-manifest.json")); - t.is(error.code, "ERR_INVALID_JSON", - "Rejected with the ERR_INVALID_JSON code (not wrapped)"); - t.true(error.message.startsWith(`Failed to parse "src/some-manifest.json" as JSON:`), - "Rejected with the clear invalid-JSON error message (not wrapped)"); + 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"); }); @@ -596,9 +596,9 @@ test("_getManifest: empty file", async (t) => { }; const error = await t.throwsAsync(project._getManifest("/some-manifest.json")); - t.is(error.message, `The file "src/some-manifest.json" is empty.`, - "Rejected with the clear empty-file error message (not wrapped)"); - t.is(error.code, "ERR_EMPTY_JSON", "Error carries the expected code"); + 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) => { @@ -606,10 +606,10 @@ test.serial("_getManifest: File does not exist", async (t) => { 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 1af23ccef5a..2370e4276ef 100644 --- a/packages/project/test/lib/specifications/types/Library.js +++ b/packages/project/test/lib/specifications/types/Library.js @@ -715,10 +715,9 @@ test("_getManifest: Invalid JSON", async (t) => { }; project._pManifest = null; // Clear cache from instantiation const error = await t.throwsAsync(project._getManifest()); - t.is(error.code, "ERR_INVALID_JSON", - "Rejected with the ERR_INVALID_JSON code (not wrapped)"); - t.true(error.message.startsWith(`Failed to parse "some path" as JSON:`), - "Rejected with the clear invalid-JSON error message (not wrapped)"); + 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"); }); @@ -739,9 +738,9 @@ test("_getManifest: Empty file", async (t) => { }; project._pManifest = null; // Clear cache from instantiation const error = await t.throwsAsync(project._getManifest()); - t.is(error.message, `The file "some path" is empty.`, - "Rejected with the clear empty-file error message (not wrapped)"); - t.is(error.code, "ERR_EMPTY_JSON", "Error carries the expected code"); + 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"); });