Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion packages/builder/lib/lbt/analyzer/ComponentAnalyzer.js
Original file line number Diff line number Diff line change
Expand Up @@ -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");

// ---------------------------------------------------------------------------------------------------------
Expand Down Expand Up @@ -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`);
}
Expand Down
3 changes: 2 additions & 1 deletion packages/builder/lib/lbt/analyzer/SmartTemplateAnalyzer.js
Original file line number Diff line number Diff line change
Expand Up @@ -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");

// ---------------------------------------------------------------------------------------------------------
Expand Down Expand Up @@ -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`);
}
Expand Down
3 changes: 2 additions & 1 deletion packages/builder/lib/processors/manifestEnhancer.js
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down Expand Up @@ -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;
Expand Down
3 changes: 2 additions & 1 deletion packages/builder/lib/processors/versionInfoGenerator.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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"));
Expand All @@ -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 || {};
Expand Down Expand Up @@ -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({
Expand Down
28 changes: 28 additions & 0 deletions packages/builder/lib/utils/json.js
Original file line number Diff line number Diff line change
@@ -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 (<code>{}</code>) is valid and parsed normally; only content-less
* files (empty or whitespace-only) throw <code>ERR_EMPTY_JSON</code>. Any other parse
* failure is re-thrown as <code>ERR_INVALID_JSON</code> 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;
}
}
10 changes: 10 additions & 0 deletions packages/builder/test/lib/processors/manifestEnhancer.js
Original file line number Diff line number Diff line change
Expand Up @@ -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__;
Expand Down
48 changes: 48 additions & 0 deletions packages/builder/test/lib/utils/json.js
Original file line number Diff line number Diff line change
@@ -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");
});

3 changes: 2 additions & 1 deletion packages/project/lib/specifications/types/Application.js
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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;
Expand Down
10 changes: 8 additions & 2 deletions packages/project/lib/specifications/types/Component.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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") {
Comment thread
d3xter666 marked this conversation as resolved.
throw err;
}
throw new Error(
`Failed to read ${filePath} for project ` +
`${this.getName()}: ${err.message}`);
Expand Down
10 changes: 8 additions & 2 deletions packages/project/lib/specifications/types/Library.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 ` +
Expand All @@ -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}`);
}
Expand Down
28 changes: 28 additions & 0 deletions packages/project/lib/utils/json.js
Original file line number Diff line number Diff line change
@@ -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 (<code>{}</code>) is valid and parsed normally; only content-less
* files (empty or whitespace-only) throw <code>ERR_EMPTY_JSON</code>. Any other parse
* failure is re-thrown as <code>ERR_INVALID_JSON</code> 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;
}
}
26 changes: 24 additions & 2 deletions packages/project/test/lib/specifications/types/Application.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
30 changes: 26 additions & 4 deletions packages/project/test/lib/specifications/types/Component.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down
Loading
Loading