diff --git a/packages/builder/lib/lbt/bundle/Builder.js b/packages/builder/lib/lbt/bundle/Builder.js index 2e275e1d45f..aad526cee73 100644 --- a/packages/builder/lib/lbt/bundle/Builder.js +++ b/packages/builder/lib/lbt/bundle/Builder.js @@ -18,6 +18,7 @@ import {SectionType} from "./BundleDefinition.js"; import BundleWriter from "./BundleWriter.js"; import {getLogger} from "@ui5/logger"; import semver from "semver"; +import parseJSON from "../../utils/json.js"; const log = getLogger("lbt:bundle:Builder"); const sourceMappingUrlPattern = /\/\/# sourceMappingURL=(\S+)\s*$/; @@ -502,9 +503,9 @@ class BundleBuilder { let fileContent = stripBOM((await resource.buffer()).toString()); if ( this.optimize ) { try { - fileContent = JSON.stringify( JSON.parse( fileContent) ); + fileContent = JSON.stringify( parseJSON( fileContent, moduleName) ); } catch (e) { - log.verbose(`Failed to parse JSON file ${moduleName}. Ignoring error, skipping compression.`); + log.verbose(`Skipping compression of module ${moduleName}.`); log.verbose(e); } } @@ -673,6 +674,7 @@ class BundleBuilder { async getSourceMapForModule({moduleName, moduleContent, resourcePath}) { let moduleSourceMap = null; + let sourceMapOriginPath = null; let newModuleContent = moduleContent; const sourceMapUrlMatch = moduleContent.match(sourceMappingUrlPattern); @@ -691,6 +693,7 @@ class BundleBuilder { if (sourceMapUrl.startsWith(expectedTypeAndEncoding)) { const base64Content = sourceMapUrl.slice(expectedTypeAndEncoding.length); moduleSourceMap = Buffer.from(base64Content, "base64").toString(); + sourceMapOriginPath = `${moduleName} (inline source map)`; } else { log.warn( `Source map reference in module ${moduleName} is a data URI but has an unexpected` + @@ -709,6 +712,7 @@ class BundleBuilder { try { const sourceMapResource = await this.pool.findResource(sourceMapPath); moduleSourceMap = (await sourceMapResource.buffer()).toString(); + sourceMapOriginPath = sourceMapPath; } catch (e) { // No input source map log.warn(`Unable to read source map for module ${moduleName}: ${e.message}`); @@ -722,6 +726,7 @@ class BundleBuilder { try { const sourceMapResource = await this.pool.findResource(sourceMapFileCandidate); moduleSourceMap = (await sourceMapResource.buffer()).toString(); + sourceMapOriginPath = sourceMapFileCandidate; } catch (e) { // No input source map log.silly(`Could not find a source map for module ${moduleName}: ${e.message}`); @@ -730,7 +735,7 @@ class BundleBuilder { if (moduleSourceMap) { - moduleSourceMap = JSON.parse(moduleSourceMap); + moduleSourceMap = parseJSON(moduleSourceMap, sourceMapOriginPath); // Check for index map, which is currently not supported if (Array.isArray(moduleSourceMap.sections)) { diff --git a/packages/builder/lib/processors/bundlers/flexChangesBundler.js b/packages/builder/lib/processors/bundlers/flexChangesBundler.js index b5e619da2b9..cdb6597bb2d 100644 --- a/packages/builder/lib/processors/bundlers/flexChangesBundler.js +++ b/packages/builder/lib/processors/bundlers/flexChangesBundler.js @@ -1,6 +1,7 @@ import {getLogger} from "@ui5/logger"; const log = getLogger("builder:processors:bundlers:flexChangesBundler"); import {createResource} from "@ui5/fs/resourceFactory"; +import parseJSON from "../../utils/json.js"; /** * @public @@ -170,7 +171,7 @@ export default function({resources, options: {pathPrefix, hasFlexBundleVersion}, return Promise.all(resources.map((resource) => { return resource.getBuffer().then((buffer) => { - return JSON.parse(buffer.toString()); + return parseJSON(buffer.toString(), resource.getPath()); }); })).then((changesContent) => { const nNumberOfChanges = changesContent.length; diff --git a/packages/builder/lib/processors/minifier.js b/packages/builder/lib/processors/minifier.js index 376e22a6d0f..a9024173320 100644 --- a/packages/builder/lib/processors/minifier.js +++ b/packages/builder/lib/processors/minifier.js @@ -5,6 +5,7 @@ import os from "node:os"; import workerpool from "workerpool"; import Resource from "@ui5/fs/Resource"; import {getLogger} from "@ui5/logger"; +import parseJSON from "../utils/json.js"; const log = getLogger("builder:processors:minifier"); import {setTimeout as setTimeoutPromise} from "node:timers/promises"; @@ -230,7 +231,7 @@ export default async function({ }); if (sourceMapContent) { - const sourceMapJson = JSON.parse(sourceMapContent); + const sourceMapJson = parseJSON(sourceMapContent, resourcePath + ".map"); if (sourceMapJson.sections) { // TODO 5.0 diff --git a/packages/builder/lib/tasks/utils/dotTheming.js b/packages/builder/lib/tasks/utils/dotTheming.js index 91de8fac51c..ae4154df0e0 100644 --- a/packages/builder/lib/tasks/utils/dotTheming.js +++ b/packages/builder/lib/tasks/utils/dotTheming.js @@ -1,5 +1,7 @@ +import parseJSON from "../../utils/json.js"; + export async function updateLibraryDotTheming({resource, namespace, version, hasThemes}) { - const dotTheming = JSON.parse(await resource.getString()); + const dotTheming = parseJSON(await resource.getString(), resource.getPath()); if (!dotTheming.sEntity) { throw new Error(`Missing 'sEntity' property in ${resource.getPath()}`); diff --git a/packages/builder/test/lib/lbt/bundle/Builder.js b/packages/builder/test/lib/lbt/bundle/Builder.js index 2db9fd5878b..31f99fc908e 100644 --- a/packages/builder/test/lib/lbt/bundle/Builder.js +++ b/packages/builder/test/lib/lbt/bundle/Builder.js @@ -66,9 +66,10 @@ test.serial("writePreloadModule: with invalid json content", async (t) => { const result = await builder.writePreloadModule("invalid.json", undefined, invalidJsonResource); t.is(verboseLogStub.callCount, 2, "called 2 times"); - t.is(verboseLogStub.firstCall.args[0], "Failed to parse JSON file invalid.json. Ignoring error, skipping compression.", + t.is(verboseLogStub.firstCall.args[0], "Skipping compression of module invalid.json.", "first verbose log argument 0 is correct"); t.true(verboseLogStub.secondCall.args[0] instanceof SyntaxError, "second verbose log with SyntaxError"); + t.is(verboseLogStub.secondCall.args[0].code, "ERR_INVALID_JSON", "second verbose log error has ERR_INVALID_JSON code"); t.regex(verboseLogStub.secondCall.args[0].message, /JSON/, "second verbose log about incorrect JSON"); t.true(result, "result is true"); @@ -3288,6 +3289,75 @@ ${SOURCE_MAPPING_URL}=data:application/pony;charset=utf-8;base64,AAAA` }, "Expected transitive source map has been generated"); }); +test("getSourceMapForModule: Data URI with malformed JSON reports inline origin in error", async (t) => { + const pool = new ResourcePool(); + const builder = new Builder(pool); + await t.throwsAsync( + builder.getSourceMapForModule({ + moduleName: "my/test/module", + resourcePath: "/resources/my/test/module.js", + moduleContent: `// Some content +${SOURCE_MAPPING_URL}=data:application/json;charset=utf-8;base64,${Buffer.from("not json").toString("base64")}` + }), + { + instanceOf: SyntaxError, + message: /my\/test\/module \(inline source map\)/ + }, + "Error message references the inline source map origin" + ); +}); + +test("getSourceMapForModule: Relative URL with malformed JSON reports resolved path in error", async (t) => { + const pool = new ResourcePool(); + pool.addResource({ + name: "my/test/module.js.map", + getPath: () => "my/test/module.js.map", + string: function() { + return this.buffer(); + }, + buffer: async () => "not json" + }); + const builder = new Builder(pool); + await t.throwsAsync( + builder.getSourceMapForModule({ + moduleName: "my/test/module", + resourcePath: "/resources/my/test/module.js", + moduleContent: `// Some content +${SOURCE_MAPPING_URL}=module.js.map` + }), + { + instanceOf: SyntaxError, + message: /my\/test\/module\.js\.map/ + }, + "Error message references the resolved source map file path" + ); +}); + +test("getSourceMapForModule: Fallback file with malformed JSON reports candidate path in error", async (t) => { + const pool = new ResourcePool(); + pool.addResource({ + name: "my/test/module.js.map", + getPath: () => "my/test/module.js.map", + string: function() { + return this.buffer(); + }, + buffer: async () => "not json" + }); + const builder = new Builder(pool); + await t.throwsAsync( + builder.getSourceMapForModule({ + moduleName: "my/test/module", + resourcePath: "/resources/my/test/module.js", + moduleContent: "// Some content\n" + }), + { + instanceOf: SyntaxError, + message: /my\/test\/module\.js\.map/ + }, + "Error message references the fallback source map candidate path" + ); +}); + test("createTransientSourceMap: includeContent=false", (t) => { const {createTransientSourceMap} = __localFunctions__; diff --git a/packages/builder/test/lib/processors/bundlers/flexChangesBundler.js b/packages/builder/test/lib/processors/bundlers/flexChangesBundler.js index 6a8c7557aa0..e5e3ea0083c 100644 --- a/packages/builder/test/lib/processors/bundlers/flexChangesBundler.js +++ b/packages/builder/test/lib/processors/bundlers/flexChangesBundler.js @@ -91,6 +91,7 @@ test("flexChangesBundler with 2 changes", async (t) => { flexBundle.forEach((change) => { resources.push({ name: "flexChange", + getPath: () => "/test/change.json", getBuffer: async () => JSON.stringify(change) }); }); @@ -127,6 +128,7 @@ test("includes annotation_changes in flexibility-bundle when hasFlexBundleVersio const resources = [{ name: "flexChange", + getPath: () => "/test/change.json", getBuffer: async () => JSON.stringify(change) }]; @@ -156,6 +158,7 @@ test("annotation_changes triggers error when hasFlexBundleVersion = false", asyn const resources = [{ name: "flexChange", + getPath: () => "/test/change.json", getBuffer: async () => JSON.stringify(change) }]; @@ -431,6 +434,7 @@ test("flexChangesBundler has ctrl_variant and hasFlexBundleVersion = true", asyn entities.forEach((change) => { resources.push({ name: "flexChange", + getPath: () => "/test/change.json", getBuffer: async () => JSON.stringify(change) }); }); @@ -531,12 +535,14 @@ test("flexChangesBundler has ctrl_variant and hasFlexBundleVersion = false", asy change.forEach((change) => { resources.push({ name: "flexChange", + getPath: () => "/test/change.json", getBuffer: async () => JSON.stringify(change) }); }); ctrlVariant.forEach((change) => { resources.push({ name: "flexChange", + getPath: () => "/test/change.json", getBuffer: async () => JSON.stringify(change) }); }); @@ -937,36 +943,42 @@ test("flexChangesBundler with existing flexibility-bundle.json", async (t) => { changeList.forEach((change) => { resources.push({ name: "flexChange", + getPath: () => "/test/change.json", getBuffer: async () => JSON.stringify(change) }); }); compVariants.forEach((change) => { resources.push({ name: "flexChange", + getPath: () => "/test/change.json", getBuffer: async () => JSON.stringify(change) }); }); variantDependentControlChange.forEach((change) => { resources.push({ name: "flexChange", + getPath: () => "/test/change.json", getBuffer: async () => JSON.stringify(change) }); }); ctrlVariantChange.forEach((change) => { resources.push({ name: "flexChange", + getPath: () => "/test/change.json", getBuffer: async () => JSON.stringify(change) }); }); ctrlVariantManagementChange.forEach((change) => { resources.push({ name: "flexChange", + getPath: () => "/test/change.json", getBuffer: async () => JSON.stringify(change) }); }); ctrlVariant.forEach((change) => { resources.push({ name: "flexChange", + getPath: () => "/test/change.json", getBuffer: async () => JSON.stringify(change) }); }); @@ -1243,36 +1255,42 @@ test("flexChangesBundler with existing flexibility-bundle.json and missing/wrong changeList.forEach((change) => { resources.push({ name: "flexChange", + getPath: () => "/test/change.json", getBuffer: async () => JSON.stringify(change) }); }); compVariants.forEach((change) => { resources.push({ name: "flexChange", + getPath: () => "/test/change.json", getBuffer: async () => JSON.stringify(change) }); }); variantDependentControlChange.forEach((change) => { resources.push({ name: "flexChange", + getPath: () => "/test/change.json", getBuffer: async () => JSON.stringify(change) }); }); ctrlVariantChange.forEach((change) => { resources.push({ name: "flexChange", + getPath: () => "/test/change.json", getBuffer: async () => JSON.stringify(change) }); }); ctrlVariantManagementChange.forEach((change) => { resources.push({ name: "flexChange", + getPath: () => "/test/change.json", getBuffer: async () => JSON.stringify(change) }); }); ctrlVariant.forEach((change) => { resources.push({ name: "flexChange", + getPath: () => "/test/change.json", getBuffer: async () => JSON.stringify(change) }); }); @@ -1396,6 +1414,7 @@ test("flexChangesBundler with existing flexibility-bundle.json and version lower flexBundle.forEach((change) => { resources.push({ name: "flexChange", + getPath: () => "/test/change.json", getBuffer: async () => JSON.stringify(change) }); }); diff --git a/packages/builder/test/lib/tasks/generateThemeDesignerResources.js b/packages/builder/test/lib/tasks/generateThemeDesignerResources.js index 646b84dbdca..22252bb6e4e 100644 --- a/packages/builder/test/lib/tasks/generateThemeDesignerResources.js +++ b/packages/builder/test/lib/tasks/generateThemeDesignerResources.js @@ -252,6 +252,7 @@ test.serial("generateThemeDesignerResources: Library sap.ui.core with existing l }; const coreLibraryDotThemingResource = { + getPath: () => "/resources/sap/ui/core/.theming", getString: async () => JSON.stringify({ sEntity: "Library", sId: "sap/ui/core", @@ -348,6 +349,7 @@ test.serial("generateThemeDesignerResources: Library sap.ui.core without themes, const {sinon, generateThemeDesignerResources, libraryLessGeneratorStub, fsInterfaceStub, ResourceStub} = t.context; const coreLibraryDotThemingResource = { + getPath: () => "/resources/sap/ui/core/.theming", getString: async () => JSON.stringify({ sEntity: "Library", sId: "sap/ui/core", diff --git a/packages/project/lib/graph/Module.js b/packages/project/lib/graph/Module.js index 5130ef2acf0..838158352bd 100644 --- a/packages/project/lib/graph/Module.js +++ b/packages/project/lib/graph/Module.js @@ -7,6 +7,7 @@ import ui5YamlSchema from "./helpers/ui5YamlSchema.js"; import {createReader} from "@ui5/fs/resourceFactory"; import Specification from "../specifications/Specification.js"; import {validate} from "../validation/validator.js"; +import parseJSON from "../utils/json.js"; import {getLogger} from "@ui5/logger"; @@ -395,7 +396,7 @@ class Module { const reader = this.getReader(); const buildManifestResource = await reader.byPath("/.ui5/build-manifest.json"); if (buildManifestResource) { - return JSON.parse(await buildManifestResource.getString()); + return parseJSON(await buildManifestResource.getString(), buildManifestResource.getPath()); } } diff --git a/packages/project/lib/graph/Workspace.js b/packages/project/lib/graph/Workspace.js index d39629a1491..54558ee8e26 100644 --- a/packages/project/lib/graph/Workspace.js +++ b/packages/project/lib/graph/Workspace.js @@ -6,6 +6,7 @@ import {promisify} from "node:util"; import {getLogger} from "@ui5/logger"; import Module from "./Module.js"; import {validateWorkspace} from "../validation/validator.js"; +import parseJSON from "../utils/json.js"; const readFile = promisify(fs.readFile); const log = getLogger("graph:Workspace"); @@ -261,8 +262,9 @@ class Workspace { * @returns {object} Package json content */ async _readPackageJson(modulePath) { - const content = await readFile(path.join(modulePath, "package.json"), "utf8"); - return JSON.parse(content); + const packageJsonPath = path.join(modulePath, "package.json"); + const content = await readFile(packageJsonPath, "utf8"); + return parseJSON(content, packageJsonPath); } async _validateConfig() { diff --git a/packages/project/lib/graph/helpers/ui5Framework.js b/packages/project/lib/graph/helpers/ui5Framework.js index d7dd6893173..c412c22eb93 100644 --- a/packages/project/lib/graph/helpers/ui5Framework.js +++ b/packages/project/lib/graph/helpers/ui5Framework.js @@ -5,6 +5,7 @@ import {getLogger} from "@ui5/logger"; const log = getLogger("graph:helpers:ui5Framework"); import Configuration from "../../config/Configuration.js"; import path from "node:path"; +import parseJSON from "../../utils/json.js"; class ProjectProcessor { constructor({libraryMetadata, graph, workspace}) { @@ -233,7 +234,7 @@ const utils = { }); } else if (project.getId().startsWith("@openui5/")) { const packageResource = await project.getRootReader().byPath("/package.json"); - const packageInfo = JSON.parse(await packageResource.getString()); + const packageInfo = parseJSON(await packageResource.getString(), "/package.json"); dependencies = Object.keys( packageInfo.dependencies || {} diff --git a/packages/project/lib/ui5Framework/maven/Installer.js b/packages/project/lib/ui5Framework/maven/Installer.js index 2c8e45fb7f6..c77bd53b493 100644 --- a/packages/project/lib/ui5Framework/maven/Installer.js +++ b/packages/project/lib/ui5Framework/maven/Installer.js @@ -1,5 +1,6 @@ import path from "node:path"; import {mkdirp} from "../../utils/fs.js"; +import parseJSON from "../../utils/json.js"; import fs from "graceful-fs"; import _StreamZip from "node-stream-zip"; const StreamZip = _StreamZip.async; @@ -73,7 +74,7 @@ class Installer extends AbstractInstaller { } async readJson(jsonPath) { - return JSON.parse(await readFile(jsonPath, {encoding: "utf8"})); + return parseJSON(await readFile(jsonPath, {encoding: "utf8"}), jsonPath); } async _writeJson(jsonPath, jsonObject) { diff --git a/packages/project/lib/ui5Framework/npm/Installer.js b/packages/project/lib/ui5Framework/npm/Installer.js index 40d1dae9814..aa0207c6bee 100644 --- a/packages/project/lib/ui5Framework/npm/Installer.js +++ b/packages/project/lib/ui5Framework/npm/Installer.js @@ -1,5 +1,6 @@ import path from "node:path"; import {mkdirp} from "../../utils/fs.js"; +import parseJSON from "../../utils/json.js"; import fs from "graceful-fs"; import {promisify} from "node:util"; import Registry from "./Registry.js"; @@ -49,7 +50,7 @@ class Installer extends AbstractInstaller { } async readJson(jsonPath) { - return JSON.parse(await readFile(jsonPath, {encoding: "utf8"})); + return parseJSON(await readFile(jsonPath, {encoding: "utf8"}), jsonPath); } async fetchPackageVersions({pkgName}) { diff --git a/packages/project/lib/validation/validator.js b/packages/project/lib/validation/validator.js index b7f13fa031a..3917581bc22 100644 --- a/packages/project/lib/validation/validator.js +++ b/packages/project/lib/validation/validator.js @@ -1,5 +1,6 @@ import {fileURLToPath} from "node:url"; import {readFile} from "node:fs/promises"; +import parseJSON from "../utils/json.js"; /** * @module @ui5/project/validation/validator @@ -72,10 +73,10 @@ class Validator { // The root schemas declare a relative $id (e.g. "ui5.json"); nested schemas declare none. // AJV therefore resolves nested $ref values against the file system layout and passes them // here as paths relative to this schema directory (e.g. "specVersion/kind/extension/task.json"). - const schemaFile = await readFile( - fileURLToPath(new URL(`./schema/${schemaPath}`, import.meta.url)), {encoding: "utf8"} - ); - return JSON.parse(schemaFile); + const schemaUrl = new URL(`./schema/${schemaPath}`, import.meta.url); + const schemaFilePath = fileURLToPath(schemaUrl); + const schemaFile = await readFile(schemaFilePath, {encoding: "utf8"}); + return parseJSON(schemaFile, schemaFilePath); } }