diff --git a/lib/processors/manifestEnhancer.js b/lib/processors/manifestEnhancer.js new file mode 100644 index 000000000..b7152087c --- /dev/null +++ b/lib/processors/manifestEnhancer.js @@ -0,0 +1,420 @@ +import semver from "semver"; +const {SemVer: Version, lt} = semver; +import {promisify} from "node:util"; +import path from "node:path/posix"; +import {getLogger} from "@ui5/logger"; +const log = getLogger("builder:processors:manifestEnhancer"); + +const APP_DESCRIPTOR_V22 = new Version("1.21.0"); + +function isAbsoluteUrl(url) { + if (url.startsWith("/")) { + return true; + } + try { + const parsedUrl = new URL(url); + // URL with ui5 protocol shouldn't be treated as absolute URL and will be handled separately + return parsedUrl.protocol !== "ui5:"; + } catch (err) { + // URL constructor without base requires absolute URL and throws an error for relative URLs + return false; + } +} + +/** + * Returns a bundle URL from the given bundle name, relative to the given namespace. + * + * @param {string} bundleName Bundle name (e.g. "sap.ui.demo.app.i18n.i18n") to be resolved to a relative URL + * @param {string} sapAppId Project namespace from sap.app/id (e.g. "sap.ui.demo.app") + * to which a bundleName should be resolved to + * @returns {string} Relative bundle URL (e.g. "i18n/i18n.properties") + */ +function getRelativeBundleUrlFromName(bundleName, sapAppId) { + const bundleUrl = "/resources/" + bundleName.replace(/\./g, "/") + ".properties"; + return normalizeBundleUrl(bundleUrl, sapAppId); +} + +// Copied from sap/base/util/LoaderExtensions.resolveUI5Url +// Adjusted to not resolve the URL, but create an absolute path prefixed with /resources +function resolveUI5Url(sUrl) { + // check for ui5 scheme + if (sUrl.startsWith("ui5:")) { + let sNoScheme = sUrl.replace("ui5:", ""); + + // check for authority + if (!sNoScheme.startsWith("//")) { + // URLs using the 'ui5' protocol must be absolute. + // Relative and server absolute URLs are reserved for future use. + return null; + } + + sNoScheme = sNoScheme.replace("//", ""); + + return "/resources/" + sNoScheme; + } else { + // not a ui5 url + return sUrl; + } +} + +/** + * Normalizes a bundle URL relative to the project namespace. + * + * @param {string} bundleUrl Relative bundle URL to be normalized + * @param {string} sapAppId Project namespace from sap.app/id (e.g. "sap.ui.demo.app") + * to which the URL is relative to + * @returns {string} Normalized relative bundle URL (e.g. "i18n/i18n.properties") + */ +function normalizeBundleUrl(bundleUrl, sapAppId) { + // Create absolute path with namespace from sap.app/id + const absoluteNamespace = `/resources/${sapAppId.replaceAll(/\./g, "/")}`; + + const resolvedAbsolutePath = path.resolve(absoluteNamespace, bundleUrl); + const resolvedRelativePath = path.relative(absoluteNamespace, resolvedAbsolutePath); + return resolvedRelativePath; +} + +/** + * Returns the bundle URL from the given bundle configuration. + * + * @param {object} bundleConfig Bundle configuration + * @param {string} sapAppId Project namespace from sap.app/id (e.g. "sap.ui.demo.app") + * to which a bundleName should be resolved to + * @param {string} [defaultBundleUrl] Default bundle url in case bundleConfig is not defined + */ +function getBundleUrlFromConfig(bundleConfig, sapAppId, defaultBundleUrl) { + if (!bundleConfig) { + // Use default URL (or undefined if argument is not provided) + return defaultBundleUrl; + } else if (typeof bundleConfig === "string") { + return bundleConfig; + } else if (typeof bundleConfig === "object") { + return getBundleUrlFromConfigObject(bundleConfig, sapAppId); + } +} + +// Same as above, but does only accept objects, not strings or defaults +function getBundleUrlFromConfigObject(bundleConfig, sapAppId, fallbackBundleUrl) { + if (typeof bundleConfig === "object") { + if (bundleConfig.bundleName) { + return getRelativeBundleUrlFromName(bundleConfig.bundleName, sapAppId); + } else if (bundleConfig.bundleUrl) { + return bundleConfig.bundleUrl; + } + } + return fallbackBundleUrl; +} + +// See runtime logic in sap/ui/core/Lib#_normalizeI18nSettings +function getBundleUrlFromSapUi5LibraryI18n(vI18n) { + if (vI18n == null || vI18n === true) { + return "messagebundle.properties"; + } else if (typeof vI18n === "string") { + return vI18n; + } else if (typeof vI18n === "object") { + return vI18n.bundleUrl; + } else { + return null; + } +} + +class ManifestEnhancer { + /** + * @param {string} manifest manifest.json content + * @param {string} filePath manifest.json file path + * @param {fs} fs Node fs or custom [fs interface]{@link module:@ui5/fs/fsInterface} + */ + constructor(manifest, filePath, fs) { + this.fsReadDir = promisify(fs.readdir); + this.cwd = path.dirname(filePath); + this.filePath = filePath; + this.manifest = JSON.parse(manifest); + + this.isModified = false; + this.runInvoked = false; + } + + markModified() { + this.isModified = true; + } + + async readdir(relativePath) { + const absolutePath = path.resolve(this.cwd, relativePath); + try { + return await this.fsReadDir(absolutePath); + } catch (err) { + if (err?.code === "ENOENT") { + return []; + } else { + throw err; + } + } + } + + async findSupportedLocales(i18nBundleUrl) { + const i18nBundleName = path.basename(i18nBundleUrl, ".properties"); + const i18nBundlePrefix = `${i18nBundleName}_`; + const i18nBundleDir = path.dirname(i18nBundleUrl); + const i18nBundleFiles = await this.readdir(i18nBundleDir); + const supportedLocales = []; + i18nBundleFiles.forEach((fileName) => { + if (!fileName.endsWith(".properties")) { + return; + } + const fileNameWithoutExtension = path.basename(fileName, ".properties"); + if (fileNameWithoutExtension === i18nBundleName) { + supportedLocales.push(""); + } else if (fileNameWithoutExtension.startsWith(i18nBundlePrefix)) { + const locale = fileNameWithoutExtension.replace(i18nBundlePrefix, ""); + supportedLocales.push(locale); + } + }); + return supportedLocales.sort(); + } + + async processBundleConfig({bundleConfig, fallbackBundleUrl, isTerminologyBundle = false, fallbackLocale}) { + const bundleUrl = getBundleUrlFromConfigObject(bundleConfig, this.manifest["sap.app"].id, fallbackBundleUrl); + if (!bundleUrl) { + return; + } + if (bundleConfig.supportedLocales) { + return; + } + + const supportedLocales = await this.getSupportedLocales( + bundleUrl, fallbackLocale ?? bundleConfig.fallbackLocale, isTerminologyBundle + ); + if (supportedLocales.length > 0) { + bundleConfig.supportedLocales = supportedLocales; + this.markModified(); + } + } + + async getSupportedLocales(bundleUrl, fallbackLocale, isTerminologyBundle = false) { + // Ignore absolute URLs + if (isAbsoluteUrl(bundleUrl)) { + return []; + } + const resolvedBundleUrl = resolveUI5Url(bundleUrl); + if (!resolvedBundleUrl) { + // In case of a relative ui5-protocol URL + return []; + } + const sapAppId = this.manifest["sap.app"].id; + const normalizedBundleUrl = normalizeBundleUrl(resolvedBundleUrl, sapAppId); + if (normalizedBundleUrl.startsWith("../")) { + log.verbose( + `${this.filePath}: ` + + `bundleUrl '${bundleUrl}' points to a bundle outside of the ` + + `current namespace '${sapAppId}', enhancement of 'supportedLocales' is skipped` + ); + return []; + } + const supportedLocales = await this.findSupportedLocales(normalizedBundleUrl); + if (!isTerminologyBundle && supportedLocales.length > 0) { + if (fallbackLocale && !supportedLocales.includes(fallbackLocale)) { + log.error( + `${this.filePath}: ` + + `Generated supported locales ('${supportedLocales.join("', '")}') for ` + + `bundle '${normalizedBundleUrl}' ` + + "not containing the defined fallback locale '" + fallbackLocale + "'. Either provide a " + + "properties file for defined fallbackLocale or configure another available fallbackLocale" + ); + return []; + } else if (!fallbackLocale && !supportedLocales.includes("en")) { + log.warn( + `${this.filePath}: ` + + `Generated supported locales ('${supportedLocales.join("', '")}') for ` + + `bundle '${normalizedBundleUrl}' ` + + "do not contain default fallback locale 'en'. Either provide a " + + "properties file for 'en' or configure another available fallbackLocale" + ); + } + } + return supportedLocales; + } + + async processSapAppI18n() { + const sapApp = this.manifest["sap.app"]; + let sapAppI18n = sapApp.i18n; + + // Process enhanceWith bundles first, as they check for an existing supportedLocales property + // defined by the developer, but not the one generated by the tooling. + await this.processTerminologiesAndEnhanceWith(sapAppI18n); + + const i18nBundleUrl = getBundleUrlFromConfig(sapAppI18n, sapApp.id, "i18n/i18n.properties"); + + if (!sapAppI18n?.supportedLocales && i18nBundleUrl) { + const supportedLocales = await this.getSupportedLocales(i18nBundleUrl, sapAppI18n?.fallbackLocale); + if (supportedLocales.length > 0) { + if (!sapAppI18n || typeof sapAppI18n === "string") { + sapAppI18n = sapApp.i18n = { + bundleUrl: i18nBundleUrl + }; + } + sapAppI18n.supportedLocales = supportedLocales; + this.markModified(); + } + } + } + + /** + * Processes the terminologies and enhanceWith bundles of a bundle configuration. + * + * @param {object} bundleConfig + */ + async processTerminologiesAndEnhanceWith(bundleConfig) { + const bundleConfigs = []; + const terminologyBundleConfigs = []; + + if (bundleConfig?.terminologies) { + terminologyBundleConfigs.push(...Object.values(bundleConfig.terminologies)); + } + + bundleConfig?.enhanceWith?.forEach((config) => { + // The runtime logic propagates supportedLocales information to the enhanceWith bundles. + // In order to not break existing behavior, we do not generate supportedLocales for enhanceWith bundles + // in case the parent bundle does have supportedLocales defined. + if (!bundleConfig.supportedLocales) { + bundleConfigs.push({config, fallbackLocale: bundleConfig.fallbackLocale}); + } + if (config.terminologies) { + terminologyBundleConfigs.push(...Object.values(config.terminologies)); + } + }); + + await Promise.all( + bundleConfigs.map(({config, fallbackLocale}) => this.processBundleConfig({ + bundleConfig: config, + fallbackLocale + })) + ); + await Promise.all( + terminologyBundleConfigs.map((bundleConfig) => this.processBundleConfig({ + bundleConfig, isTerminologyBundle: true + })) + ); + } + + async processSapUi5Models() { + const sapUi5Models = this.manifest["sap.ui5"]?.models; + if (typeof sapUi5Models !== "object") { + return; + } + const modelConfigs = Object.values(sapUi5Models) + .filter((modelConfig) => modelConfig.type === "sap.ui.model.resource.ResourceModel"); + + await Promise.all( + modelConfigs.map(async (modelConfig) => { + // Process enhanceWith bundles first, as they check for an existing supportedLocales property + // defined by the developer, but not the one generated by the tooling. + await this.processTerminologiesAndEnhanceWith(modelConfig.settings); + + // Fallback to empty settings object in case only a "uri" is defined which will be converted + // to a settings object at runtime. + const settings = modelConfig.settings || {}; + + // Ensure to pass the "uri" property as fallback bundle URL according to the runtime logic. + // It is only taken into account if no "bundleUrl" or "bundleName" is defined. + await this.processBundleConfig({bundleConfig: settings, fallbackBundleUrl: modelConfig.uri}); + + // Ensure that the settings object is assigned back to the modelConfig + // in case it didn't existing before. + if (!modelConfig.settings) { + modelConfig.settings = settings; + } + }) + ); + } + + async processSapUi5LibraryI18n() { + let sapUi5LibraryI18n = this.manifest["sap.ui5"]?.library?.i18n; + + // Process enhanceWith bundles first, as they check for an existing supportedLocales property + // defined by the developer, but not the one generated by the tooling. + await this.processTerminologiesAndEnhanceWith(sapUi5LibraryI18n); + + const i18nBundleUrl = getBundleUrlFromSapUi5LibraryI18n(sapUi5LibraryI18n); + if (i18nBundleUrl && !sapUi5LibraryI18n?.supportedLocales) { + const supportedLocales = await this.getSupportedLocales(i18nBundleUrl, sapUi5LibraryI18n?.fallbackLocale); + if (supportedLocales.length > 0) { + if (!sapUi5LibraryI18n || typeof sapUi5LibraryI18n !== "object") { + this.manifest["sap.ui5"] ??= {}; + this.manifest["sap.ui5"].library ??= {}; + sapUi5LibraryI18n = this.manifest["sap.ui5"].library.i18n = { + bundleUrl: i18nBundleUrl + }; + } + sapUi5LibraryI18n.supportedLocales = supportedLocales; + this.markModified(); + } + } + } + + async run() { + // Prevent multiple invocations + if (this.runInvoked) { + throw new Error("ManifestEnhancer#run can only be invoked once per instance"); + } + this.runInvoked = true; + + if (!this.manifest._version) { + log.verbose(`${this.filePath}: _version is not defined. No supportedLocales are generated`); + return; + } + + if (lt(this.manifest._version, APP_DESCRIPTOR_V22)) { + log.verbose(`${this.filePath}: _version is lower than 1.21.0 so no supportedLocales can be generated`); + return; + } + + if (this.manifest["sap.app"].type === "library") { + await this.processSapUi5LibraryI18n(); + } else { + await Promise.all([ + this.processSapAppI18n(), + this.processSapUi5Models() + ]); + } + + if (this.isModified) { + return this.manifest; + } + } +} + +/** + * @module @ui5/builder/processors/manifestEnhancer + */ + +/** + * Enriches the content of the manifest.json file. + * + * @public + * @function default + * @static + * + * @param {object} parameters Parameters + * @param {@ui5/fs/Resource[]} parameters.resources List of manifest.json resources to be processed + * @param {fs|module:@ui5/fs/fsInterface} parameters.fs Node fs or custom + * [fs interface]{@link module:@ui5/fs/fsInterface}. + * @returns {Promise>} Promise resolving with an array of modified resources + */ +export default async function({resources, fs}) { + const res = await Promise.all( + resources.map(async (resource) => { + const manifest = await resource.getString(); + const filePath = resource.getPath(); + const manifestEnhancer = new ManifestEnhancer(manifest, filePath, fs); + const enrichedManifest = await manifestEnhancer.run(); + if (enrichedManifest) { + resource.setString(JSON.stringify(enrichedManifest, null, 2)); + return resource; + } + }) + ); + return res.filter(($) => $); +} + +export const __internals__ = (process.env.NODE_ENV === "test") ? + {ManifestEnhancer, getRelativeBundleUrlFromName, normalizeBundleUrl, resolveUI5Url} : undefined; diff --git a/lib/tasks/enhanceManifest.js b/lib/tasks/enhanceManifest.js new file mode 100644 index 000000000..db94b772f --- /dev/null +++ b/lib/tasks/enhanceManifest.js @@ -0,0 +1,32 @@ +import manifestEnhancer from "../processors/manifestEnhancer.js"; +import fsInterface from "@ui5/fs/fsInterface"; + +/* eslint "jsdoc/check-param-names": ["error", {"disableExtraPropertyReporting":true}] */ +/** + * Task for transforming the manifest.json file. + * Adds missing information based on the available project resources, + * for example the locales supported by the present i18n resources. + * + * @public + * @function default + * @static + * + * @param {object} parameters Parameters + * @param {@ui5/fs/DuplexCollection} parameters.workspace DuplexCollection to read and write files + * @param {object} parameters.options Options + * @param {string} parameters.options.projectNamespace Namespace of the application + * @returns {Promise} Promise resolving with undefined once data has been written + */ +export default async function({workspace, options}) { + const {projectNamespace} = options; + + // Note: all "manifest.json" files in the given namespace + const resources = await workspace.byGlob(`/resources/${projectNamespace}/**/manifest.json`); + + const processedResources = await manifestEnhancer({ + resources, + fs: fsInterface(workspace), + }); + + await Promise.all(processedResources.map((resource) => workspace.write(resource))); +} diff --git a/lib/tasks/taskRepository.js b/lib/tasks/taskRepository.js index bf4f46293..05ff4ced9 100644 --- a/lib/tasks/taskRepository.js +++ b/lib/tasks/taskRepository.js @@ -17,6 +17,7 @@ const taskInfos = { replaceCopyright: {path: "./replaceCopyright.js"}, replaceVersion: {path: "./replaceVersion.js"}, replaceBuildtime: {path: "./replaceBuildtime.js"}, + enhanceManifest: {path: "./enhanceManifest.js"}, escapeNonAsciiCharacters: {path: "./escapeNonAsciiCharacters.js"}, executeJsdocSdkTransformation: {path: "./jsdoc/executeJsdocSdkTransformation.js"}, generateApiIndex: {path: "./jsdoc/generateApiIndex.js"}, diff --git a/test/expected/build/application.o/dest/Component-preload.js b/test/expected/build/application.o/dest/Component-preload.js new file mode 100644 index 000000000..a721ea556 --- /dev/null +++ b/test/expected/build/application.o/dest/Component-preload.js @@ -0,0 +1,10 @@ +//@ui5-bundle application/o/Component-preload.js +sap.ui.predefine("application/o/test", [],()=>{test(e=>{const s=e;console.log(s)});test()}); +sap.ui.require.preload({ + "application/o/i18n/i18n.properties":'welcome=Hello world', + "application/o/i18n/i18n_en.properties":'welcome=Hello EN world', + "application/o/i18n/i18n_en_US.properties":'welcome=Hello EN US world', + "application/o/i18n/i18n_en_US_sapprc.properties":'welcome=Hello EN US sapprc world', + "application/o/manifest.json":'{"_version":"1.22.0","sap.app":{"id":"application.o","type":"application","applicationVersion":{"version":"1.0.0"},"title":"{{title}}","i18n":{"bundleUrl":"i18n/i18n.properties","supportedLocales":["","en","en_US","en_US_sapprc"]}},"sap.ui5":{"models":{"i18n":{"type":"sap.ui.model.resource.ResourceModel","settings":{"bundleName":"application.o.i18n.i18n","supportedLocales":["","en","en_US","en_US_sapprc"]}},"i18n-ui5":{"type":"sap.ui.model.resource.ResourceModel","settings":{"bundleUrl":"ui5://application/o/i18n/i18n.properties","supportedLocales":["","en","en_US","en_US_sapprc"]}}}}}' +}); +//# sourceMappingURL=Component-preload.js.map diff --git a/test/expected/build/application.o/dest/Component-preload.js.map b/test/expected/build/application.o/dest/Component-preload.js.map new file mode 100644 index 000000000..e9ca86cab --- /dev/null +++ b/test/expected/build/application.o/dest/Component-preload.js.map @@ -0,0 +1 @@ +{"version":3,"file":"Component-preload.js","sections":[{"offset":{"line":1,"column":0},"map":{"version":3,"names":["sap","ui","define","test","paramA","variableA","console","log"],"sources":["test-dbg.js"],"mappings":"AAAAA,IAAIC,GAAGC,gCAAO,GACX,KACFC,KAAMC,IACL,MAAMC,EAAYD,EAClBE,QAAQC,IAAIF,EAAU,GAEvBF,MAAM","ignoreList":[],"sourceRoot":""}},{"offset":{"line":2,"column":0},"map":{"version":3,"names":[],"sources":["Component-preload.js?bundle-code-0"],"mappings":"AAAA;AACA","sourcesContent":["sap.ui.require.preload({\n"],"sourceRoot":""}}]} \ No newline at end of file diff --git a/test/expected/build/application.o/dest/i18n/i18n.properties b/test/expected/build/application.o/dest/i18n/i18n.properties new file mode 100644 index 000000000..4fa9396ef --- /dev/null +++ b/test/expected/build/application.o/dest/i18n/i18n.properties @@ -0,0 +1 @@ +welcome=Hello world \ No newline at end of file diff --git a/test/expected/build/application.o/dest/i18n/i18n_en.properties b/test/expected/build/application.o/dest/i18n/i18n_en.properties new file mode 100644 index 000000000..f03a373b0 --- /dev/null +++ b/test/expected/build/application.o/dest/i18n/i18n_en.properties @@ -0,0 +1 @@ +welcome=Hello EN world \ No newline at end of file diff --git a/test/expected/build/application.o/dest/i18n/i18n_en_US.properties b/test/expected/build/application.o/dest/i18n/i18n_en_US.properties new file mode 100644 index 000000000..60d20479d --- /dev/null +++ b/test/expected/build/application.o/dest/i18n/i18n_en_US.properties @@ -0,0 +1 @@ +welcome=Hello EN US world \ No newline at end of file diff --git a/test/expected/build/application.o/dest/i18n/i18n_en_US_sapprc.properties b/test/expected/build/application.o/dest/i18n/i18n_en_US_sapprc.properties new file mode 100644 index 000000000..2376090d4 --- /dev/null +++ b/test/expected/build/application.o/dest/i18n/i18n_en_US_sapprc.properties @@ -0,0 +1 @@ +welcome=Hello EN US sapprc world \ No newline at end of file diff --git a/test/expected/build/application.o/dest/index.html b/test/expected/build/application.o/dest/index.html new file mode 100644 index 000000000..991958fa5 --- /dev/null +++ b/test/expected/build/application.o/dest/index.html @@ -0,0 +1,11 @@ + + + + Application O + + + + + + diff --git a/test/expected/build/application.o/dest/manifest.json b/test/expected/build/application.o/dest/manifest.json new file mode 100644 index 000000000..a64786ccc --- /dev/null +++ b/test/expected/build/application.o/dest/manifest.json @@ -0,0 +1,48 @@ +{ + "_version": "1.22.0", + "sap.app": { + "id": "application.o", + "type": "application", + "applicationVersion": { + "version": "1.0.0" + }, + "title": "{{title}}", + "i18n": { + "bundleUrl": "i18n/i18n.properties", + "supportedLocales": [ + "", + "en", + "en_US", + "en_US_sapprc" + ] + } + }, + "sap.ui5": { + "models": { + "i18n": { + "type": "sap.ui.model.resource.ResourceModel", + "settings": { + "bundleName": "application.o.i18n.i18n", + "supportedLocales": [ + "", + "en", + "en_US", + "en_US_sapprc" + ] + } + }, + "i18n-ui5": { + "type": "sap.ui.model.resource.ResourceModel", + "settings": { + "bundleUrl": "ui5://application/o/i18n/i18n.properties", + "supportedLocales": [ + "", + "en", + "en_US", + "en_US_sapprc" + ] + } + } + } + } +} \ No newline at end of file diff --git a/test/expected/build/application.o/dest/test-dbg.js b/test/expected/build/application.o/dest/test-dbg.js new file mode 100644 index 000000000..399a1b6a1 --- /dev/null +++ b/test/expected/build/application.o/dest/test-dbg.js @@ -0,0 +1,8 @@ +sap.ui.define([ +], () => { + test((paramA) => { + const variableA = paramA; + console.log(variableA); + }) + test(); +}); diff --git a/test/expected/build/application.o/dest/test.js b/test/expected/build/application.o/dest/test.js new file mode 100644 index 000000000..2a39c7bc3 --- /dev/null +++ b/test/expected/build/application.o/dest/test.js @@ -0,0 +1,2 @@ +sap.ui.define([],()=>{test(e=>{const s=e;console.log(s)});test()}); +//# sourceMappingURL=test.js.map \ No newline at end of file diff --git a/test/expected/build/application.o/dest/test.js.map b/test/expected/build/application.o/dest/test.js.map new file mode 100644 index 000000000..8e36c70d0 --- /dev/null +++ b/test/expected/build/application.o/dest/test.js.map @@ -0,0 +1 @@ +{"version":3,"file":"test.js","names":["sap","ui","define","test","paramA","variableA","console","log"],"sources":["test-dbg.js"],"mappings":"AAAAA,IAAIC,GAAGC,OAAO,GACX,KACFC,KAAMC,IACL,MAAMC,EAAYD,EAClBE,QAAQC,IAAIF,EAAU,GAEvBF,MAAM","ignoreList":[]} \ No newline at end of file diff --git a/test/expected/build/library.o/dest/resources/library/o/.library b/test/expected/build/library.o/dest/resources/library/o/.library new file mode 100644 index 000000000..c36ef6f95 --- /dev/null +++ b/test/expected/build/library.o/dest/resources/library/o/.library @@ -0,0 +1,12 @@ + + + + library.o + SAP SE + ${copyright} + 1.0.0 + + {{title}} + {{description}} + + diff --git a/test/expected/build/library.o/dest/resources/library/o/library-dbg.js b/test/expected/build/library.o/dest/resources/library/o/library-dbg.js new file mode 100644 index 000000000..d8fcee8a3 --- /dev/null +++ b/test/expected/build/library.o/dest/resources/library/o/library-dbg.js @@ -0,0 +1,17 @@ +/*! + * Some fancy copyright + */ +sap.ui.define([ + 'sap/ui/core/Core', +], (Core) => { + "use strict"; + + sap.ui.getCore().initLibrary({ + name : "library.o", + version: "1.0.0", + dependencies : [] + }); + + return thisLib; + +}); diff --git a/test/expected/build/library.o/dest/resources/library/o/library-preload.js b/test/expected/build/library.o/dest/resources/library/o/library-preload.js new file mode 100644 index 000000000..13fbc0911 --- /dev/null +++ b/test/expected/build/library.o/dest/resources/library/o/library-preload.js @@ -0,0 +1,9 @@ +//@ui5-bundle library/o/library-preload.js +/*! + * Some fancy copyright + */ +sap.ui.predefine("library/o/library", ["sap/ui/core/Core"],e=>{"use strict";sap.ui.getCore().initLibrary({name:"library.o",version:"1.0.0",dependencies:[]});return thisLib}); +sap.ui.require.preload({ + "library/o/manifest.json":'{"_version":"1.58.0","sap.app":{"id":"library.o","type":"library","embeds":[],"applicationVersion":{"version":"1.0.0"},"title":"{{title}}","description":"{{description}}","resources":"resources.json","offline":true},"sap.ui":{"technology":"UI5","supportedThemes":[]},"sap.ui5":{"dependencies":{"libs":{}},"library":{"i18n":{"bundleUrl":"messagebundle.properties","terminologies":{"sports":{"bundleUrl":"sports.properties","bundleUrlRelativeTo":"manifest","supportedLocales":["","de","en"]},"travel":{"bundleUrl":"travel.properties","bundleUrlRelativeTo":"manifest","supportedLocales":["","de","en"]}},"enhanceWith":[{"bundleUrl":"myfolder1/i18n.properties","bundleUrlRelativeTo":"manifest","terminologies":{"sports":{"bundleUrl":"myfolder1/soccer.properties","bundleUrlRelativeTo":"manifest","supportedLocales":["","de","en"]},"travel":{"bundleUrl":"myfolder1/vehicles.properties","bundleUrlRelativeTo":"manifest","supportedLocales":["","de","en"]}},"supportedLocales":["","en"]},{"bundleUrl":"myfolder2/i18n.properties","bundleUrlRelativeTo":"manifest","terminologies":{"travel":{"bundleUrl":"myfolder2/bicycles.properties","bundleUrlRelativeTo":"manifest","supportedLocales":["","de","en"]}},"supportedLocales":["","en"]}],"supportedLocales":["","de","en"]}}}}' +}); +//# sourceMappingURL=library-preload.js.map diff --git a/test/expected/build/library.o/dest/resources/library/o/library-preload.js.map b/test/expected/build/library.o/dest/resources/library/o/library-preload.js.map new file mode 100644 index 000000000..3cdb3a93d --- /dev/null +++ b/test/expected/build/library.o/dest/resources/library/o/library-preload.js.map @@ -0,0 +1 @@ +{"version":3,"file":"library-preload.js","sections":[{"offset":{"line":1,"column":0},"map":{"version":3,"names":["sap","ui","define","Core","getCore","initLibrary","name","version","dependencies","thisLib"],"sources":["library-dbg.js"],"mappings":"AAAA;;;AAGAA,IAAIC,GAAGC,+BAAO,CACb,oBACGC,IACH,aAEAH,IAAIC,GAAGG,UAAUC,YAAY,CAC5BC,KAAO,YACPC,QAAS,QACTC,aAAe,KAGhB,OAAOC,OAAO","ignoreList":[],"sourceRoot":""}},{"offset":{"line":5,"column":0},"map":{"version":3,"names":[],"sources":["library-preload.js?bundle-code-0"],"mappings":"AAAA;AACA","sourcesContent":["sap.ui.require.preload({\n"],"sourceRoot":""}}]} \ No newline at end of file diff --git a/test/expected/build/library.o/dest/resources/library/o/library.js b/test/expected/build/library.o/dest/resources/library/o/library.js new file mode 100644 index 000000000..cfdec4396 --- /dev/null +++ b/test/expected/build/library.o/dest/resources/library/o/library.js @@ -0,0 +1,5 @@ +/*! + * Some fancy copyright + */ +sap.ui.define(["sap/ui/core/Core"],e=>{"use strict";sap.ui.getCore().initLibrary({name:"library.o",version:"1.0.0",dependencies:[]});return thisLib}); +//# sourceMappingURL=library.js.map \ No newline at end of file diff --git a/test/expected/build/library.o/dest/resources/library/o/library.js.map b/test/expected/build/library.o/dest/resources/library/o/library.js.map new file mode 100644 index 000000000..d9b6ae5c7 --- /dev/null +++ b/test/expected/build/library.o/dest/resources/library/o/library.js.map @@ -0,0 +1 @@ +{"version":3,"file":"library.js","names":["sap","ui","define","Core","getCore","initLibrary","name","version","dependencies","thisLib"],"sources":["library-dbg.js"],"mappings":";;;AAGAA,IAAIC,GAAGC,OAAO,CACb,oBACGC,IACH,aAEAH,IAAIC,GAAGG,UAAUC,YAAY,CAC5BC,KAAO,YACPC,QAAS,QACTC,aAAe,KAGhB,OAAOC,OAAO","ignoreList":[]} \ No newline at end of file diff --git a/test/expected/build/library.o/dest/resources/library/o/manifest.json b/test/expected/build/library.o/dest/resources/library/o/manifest.json new file mode 100644 index 000000000..a75c7c1f4 --- /dev/null +++ b/test/expected/build/library.o/dest/resources/library/o/manifest.json @@ -0,0 +1,103 @@ +{ + "_version": "1.58.0", + "sap.app": { + "id": "library.o", + "type": "library", + "embeds": [], + "applicationVersion": { + "version": "1.0.0" + }, + "title": "{{title}}", + "description": "{{description}}", + "resources": "resources.json", + "offline": true + }, + "sap.ui": { + "technology": "UI5", + "supportedThemes": [] + }, + "sap.ui5": { + "dependencies": { + "libs": {} + }, + "library": { + "i18n": { + "bundleUrl": "messagebundle.properties", + "terminologies": { + "sports": { + "bundleUrl": "sports.properties", + "bundleUrlRelativeTo": "manifest", + "supportedLocales": [ + "", + "de", + "en" + ] + }, + "travel": { + "bundleUrl": "travel.properties", + "bundleUrlRelativeTo": "manifest", + "supportedLocales": [ + "", + "de", + "en" + ] + } + }, + "enhanceWith": [ + { + "bundleUrl": "myfolder1/i18n.properties", + "bundleUrlRelativeTo": "manifest", + "terminologies": { + "sports": { + "bundleUrl": "myfolder1/soccer.properties", + "bundleUrlRelativeTo": "manifest", + "supportedLocales": [ + "", + "de", + "en" + ] + }, + "travel": { + "bundleUrl": "myfolder1/vehicles.properties", + "bundleUrlRelativeTo": "manifest", + "supportedLocales": [ + "", + "de", + "en" + ] + } + }, + "supportedLocales": [ + "", + "en" + ] + }, + { + "bundleUrl": "myfolder2/i18n.properties", + "bundleUrlRelativeTo": "manifest", + "terminologies": { + "travel": { + "bundleUrl": "myfolder2/bicycles.properties", + "bundleUrlRelativeTo": "manifest", + "supportedLocales": [ + "", + "de", + "en" + ] + } + }, + "supportedLocales": [ + "", + "en" + ] + } + ], + "supportedLocales": [ + "", + "de", + "en" + ] + } + } + } +} \ No newline at end of file diff --git a/test/expected/build/library.o/dest/resources/library/o/messagebundle.properties b/test/expected/build/library.o/dest/resources/library/o/messagebundle.properties new file mode 100644 index 000000000..b23c5ecd1 --- /dev/null +++ b/test/expected/build/library.o/dest/resources/library/o/messagebundle.properties @@ -0,0 +1,2 @@ +title=a title +description=a description \ No newline at end of file diff --git a/test/expected/build/library.o/dest/resources/library/o/messagebundle_de.properties b/test/expected/build/library.o/dest/resources/library/o/messagebundle_de.properties new file mode 100644 index 000000000..b23c5ecd1 --- /dev/null +++ b/test/expected/build/library.o/dest/resources/library/o/messagebundle_de.properties @@ -0,0 +1,2 @@ +title=a title +description=a description \ No newline at end of file diff --git a/test/expected/build/library.o/dest/resources/library/o/messagebundle_en.properties b/test/expected/build/library.o/dest/resources/library/o/messagebundle_en.properties new file mode 100644 index 000000000..b23c5ecd1 --- /dev/null +++ b/test/expected/build/library.o/dest/resources/library/o/messagebundle_en.properties @@ -0,0 +1,2 @@ +title=a title +description=a description \ No newline at end of file diff --git a/test/expected/build/library.o/dest/resources/library/o/myfolder1/i18n.properties b/test/expected/build/library.o/dest/resources/library/o/myfolder1/i18n.properties new file mode 100644 index 000000000..7a2a6e8bb --- /dev/null +++ b/test/expected/build/library.o/dest/resources/library/o/myfolder1/i18n.properties @@ -0,0 +1 @@ +title=my company 1 title \ No newline at end of file diff --git a/test/expected/build/library.o/dest/resources/library/o/myfolder1/i18n_en.properties b/test/expected/build/library.o/dest/resources/library/o/myfolder1/i18n_en.properties new file mode 100644 index 000000000..7a2a6e8bb --- /dev/null +++ b/test/expected/build/library.o/dest/resources/library/o/myfolder1/i18n_en.properties @@ -0,0 +1 @@ +title=my company 1 title \ No newline at end of file diff --git a/test/expected/build/library.o/dest/resources/library/o/myfolder1/soccer.properties b/test/expected/build/library.o/dest/resources/library/o/myfolder1/soccer.properties new file mode 100644 index 000000000..5ca5f663b --- /dev/null +++ b/test/expected/build/library.o/dest/resources/library/o/myfolder1/soccer.properties @@ -0,0 +1 @@ +title=my soccer title \ No newline at end of file diff --git a/test/expected/build/library.o/dest/resources/library/o/myfolder1/soccer_de.properties b/test/expected/build/library.o/dest/resources/library/o/myfolder1/soccer_de.properties new file mode 100644 index 000000000..1c9b39ba5 --- /dev/null +++ b/test/expected/build/library.o/dest/resources/library/o/myfolder1/soccer_de.properties @@ -0,0 +1 @@ +title=Mein Fussball Titel \ No newline at end of file diff --git a/test/expected/build/library.o/dest/resources/library/o/myfolder1/soccer_en.properties b/test/expected/build/library.o/dest/resources/library/o/myfolder1/soccer_en.properties new file mode 100644 index 000000000..5ca5f663b --- /dev/null +++ b/test/expected/build/library.o/dest/resources/library/o/myfolder1/soccer_en.properties @@ -0,0 +1 @@ +title=my soccer title \ No newline at end of file diff --git a/test/expected/build/library.o/dest/resources/library/o/myfolder1/vehicles.properties b/test/expected/build/library.o/dest/resources/library/o/myfolder1/vehicles.properties new file mode 100644 index 000000000..2e42e0f6f --- /dev/null +++ b/test/expected/build/library.o/dest/resources/library/o/myfolder1/vehicles.properties @@ -0,0 +1 @@ +title=my vehicle title \ No newline at end of file diff --git a/test/expected/build/library.o/dest/resources/library/o/myfolder1/vehicles_de.properties b/test/expected/build/library.o/dest/resources/library/o/myfolder1/vehicles_de.properties new file mode 100644 index 000000000..5cc4cab70 --- /dev/null +++ b/test/expected/build/library.o/dest/resources/library/o/myfolder1/vehicles_de.properties @@ -0,0 +1 @@ +title=Mein Fahrzeugstitel \ No newline at end of file diff --git a/test/expected/build/library.o/dest/resources/library/o/myfolder1/vehicles_en.properties b/test/expected/build/library.o/dest/resources/library/o/myfolder1/vehicles_en.properties new file mode 100644 index 000000000..2e42e0f6f --- /dev/null +++ b/test/expected/build/library.o/dest/resources/library/o/myfolder1/vehicles_en.properties @@ -0,0 +1 @@ +title=my vehicle title \ No newline at end of file diff --git a/test/expected/build/library.o/dest/resources/library/o/myfolder2/bicycles.properties b/test/expected/build/library.o/dest/resources/library/o/myfolder2/bicycles.properties new file mode 100644 index 000000000..b288e4bfd --- /dev/null +++ b/test/expected/build/library.o/dest/resources/library/o/myfolder2/bicycles.properties @@ -0,0 +1 @@ +title=my bicycle title \ No newline at end of file diff --git a/test/expected/build/library.o/dest/resources/library/o/myfolder2/bicycles_de.properties b/test/expected/build/library.o/dest/resources/library/o/myfolder2/bicycles_de.properties new file mode 100644 index 000000000..f613ffa9d --- /dev/null +++ b/test/expected/build/library.o/dest/resources/library/o/myfolder2/bicycles_de.properties @@ -0,0 +1 @@ +title=Mein Fahrradtitel \ No newline at end of file diff --git a/test/expected/build/library.o/dest/resources/library/o/myfolder2/bicycles_en.properties b/test/expected/build/library.o/dest/resources/library/o/myfolder2/bicycles_en.properties new file mode 100644 index 000000000..b288e4bfd --- /dev/null +++ b/test/expected/build/library.o/dest/resources/library/o/myfolder2/bicycles_en.properties @@ -0,0 +1 @@ +title=my bicycle title \ No newline at end of file diff --git a/test/expected/build/library.o/dest/resources/library/o/myfolder2/i18n.properties b/test/expected/build/library.o/dest/resources/library/o/myfolder2/i18n.properties new file mode 100644 index 000000000..375641827 --- /dev/null +++ b/test/expected/build/library.o/dest/resources/library/o/myfolder2/i18n.properties @@ -0,0 +1 @@ +title=my company 2 title \ No newline at end of file diff --git a/test/expected/build/library.o/dest/resources/library/o/myfolder2/i18n_en.properties b/test/expected/build/library.o/dest/resources/library/o/myfolder2/i18n_en.properties new file mode 100644 index 000000000..375641827 --- /dev/null +++ b/test/expected/build/library.o/dest/resources/library/o/myfolder2/i18n_en.properties @@ -0,0 +1 @@ +title=my company 2 title \ No newline at end of file diff --git a/test/expected/build/library.o/dest/resources/library/o/sports.properties b/test/expected/build/library.o/dest/resources/library/o/sports.properties new file mode 100644 index 000000000..4b5622875 --- /dev/null +++ b/test/expected/build/library.o/dest/resources/library/o/sports.properties @@ -0,0 +1,2 @@ +title=a sports title +description=a sports description \ No newline at end of file diff --git a/test/expected/build/library.o/dest/resources/library/o/sports_de.properties b/test/expected/build/library.o/dest/resources/library/o/sports_de.properties new file mode 100644 index 000000000..c6ce6e0ff --- /dev/null +++ b/test/expected/build/library.o/dest/resources/library/o/sports_de.properties @@ -0,0 +1,2 @@ +title=ein Sporttitel +description=eine Sportbeschreibung \ No newline at end of file diff --git a/test/expected/build/library.o/dest/resources/library/o/sports_en.properties b/test/expected/build/library.o/dest/resources/library/o/sports_en.properties new file mode 100644 index 000000000..4b5622875 --- /dev/null +++ b/test/expected/build/library.o/dest/resources/library/o/sports_en.properties @@ -0,0 +1,2 @@ +title=a sports title +description=a sports description \ No newline at end of file diff --git a/test/expected/build/library.o/dest/resources/library/o/travel.properties b/test/expected/build/library.o/dest/resources/library/o/travel.properties new file mode 100644 index 000000000..1e5ad188e --- /dev/null +++ b/test/expected/build/library.o/dest/resources/library/o/travel.properties @@ -0,0 +1,2 @@ +title=a travel title +description=a travel description \ No newline at end of file diff --git a/test/expected/build/library.o/dest/resources/library/o/travel_de.properties b/test/expected/build/library.o/dest/resources/library/o/travel_de.properties new file mode 100644 index 000000000..67689b27f --- /dev/null +++ b/test/expected/build/library.o/dest/resources/library/o/travel_de.properties @@ -0,0 +1,2 @@ +title=ein Reisetitel +description=eine Reisebeschreibung \ No newline at end of file diff --git a/test/expected/build/library.o/dest/resources/library/o/travel_en.properties b/test/expected/build/library.o/dest/resources/library/o/travel_en.properties new file mode 100644 index 000000000..1e5ad188e --- /dev/null +++ b/test/expected/build/library.o/dest/resources/library/o/travel_en.properties @@ -0,0 +1,2 @@ +title=a travel title +description=a travel description \ No newline at end of file diff --git a/test/fixtures/application.o/package.json b/test/fixtures/application.o/package.json new file mode 100644 index 000000000..b4acde06e --- /dev/null +++ b/test/fixtures/application.o/package.json @@ -0,0 +1,8 @@ +{ + "name": "application.o", + "version": "1.0.0", + "description": "Simple SAPUI5 based application", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + } +} diff --git a/test/fixtures/application.o/ui5.yaml b/test/fixtures/application.o/ui5.yaml new file mode 100644 index 000000000..28318b58d --- /dev/null +++ b/test/fixtures/application.o/ui5.yaml @@ -0,0 +1,5 @@ +--- +specVersion: "3.2" +type: application +metadata: + name: application.o diff --git a/test/fixtures/application.o/webapp/i18n/i18n.properties b/test/fixtures/application.o/webapp/i18n/i18n.properties new file mode 100644 index 000000000..4fa9396ef --- /dev/null +++ b/test/fixtures/application.o/webapp/i18n/i18n.properties @@ -0,0 +1 @@ +welcome=Hello world \ No newline at end of file diff --git a/test/fixtures/application.o/webapp/i18n/i18n_en.properties b/test/fixtures/application.o/webapp/i18n/i18n_en.properties new file mode 100644 index 000000000..f03a373b0 --- /dev/null +++ b/test/fixtures/application.o/webapp/i18n/i18n_en.properties @@ -0,0 +1 @@ +welcome=Hello EN world \ No newline at end of file diff --git a/test/fixtures/application.o/webapp/i18n/i18n_en_US.properties b/test/fixtures/application.o/webapp/i18n/i18n_en_US.properties new file mode 100644 index 000000000..60d20479d --- /dev/null +++ b/test/fixtures/application.o/webapp/i18n/i18n_en_US.properties @@ -0,0 +1 @@ +welcome=Hello EN US world \ No newline at end of file diff --git a/test/fixtures/application.o/webapp/i18n/i18n_en_US_sapprc.properties b/test/fixtures/application.o/webapp/i18n/i18n_en_US_sapprc.properties new file mode 100644 index 000000000..2376090d4 --- /dev/null +++ b/test/fixtures/application.o/webapp/i18n/i18n_en_US_sapprc.properties @@ -0,0 +1 @@ +welcome=Hello EN US sapprc world \ No newline at end of file diff --git a/test/fixtures/application.o/webapp/index.html b/test/fixtures/application.o/webapp/index.html new file mode 100644 index 000000000..991958fa5 --- /dev/null +++ b/test/fixtures/application.o/webapp/index.html @@ -0,0 +1,11 @@ + + + + Application O + + + + + + diff --git a/test/fixtures/application.o/webapp/manifest.json b/test/fixtures/application.o/webapp/manifest.json new file mode 100644 index 000000000..6f103ad8d --- /dev/null +++ b/test/fixtures/application.o/webapp/manifest.json @@ -0,0 +1,27 @@ +{ + "_version": "1.22.0", + "sap.app": { + "id": "application.o", + "type": "application", + "applicationVersion": { + "version": "1.0.0" + }, + "title": "{{title}}" + }, + "sap.ui5": { + "models": { + "i18n": { + "type": "sap.ui.model.resource.ResourceModel", + "settings": { + "bundleName": "application.o.i18n.i18n" + } + }, + "i18n-ui5": { + "type": "sap.ui.model.resource.ResourceModel", + "settings": { + "bundleUrl": "ui5://application/o/i18n/i18n.properties" + } + } + } + } +} diff --git a/test/fixtures/application.o/webapp/test.js b/test/fixtures/application.o/webapp/test.js new file mode 100644 index 000000000..399a1b6a1 --- /dev/null +++ b/test/fixtures/application.o/webapp/test.js @@ -0,0 +1,8 @@ +sap.ui.define([ +], () => { + test((paramA) => { + const variableA = paramA; + console.log(variableA); + }) + test(); +}); diff --git a/test/fixtures/library.o/package.json b/test/fixtures/library.o/package.json new file mode 100644 index 000000000..384c8b0fa --- /dev/null +++ b/test/fixtures/library.o/package.json @@ -0,0 +1,9 @@ +{ + "name": "library.o", + "version": "1.0.0", + "description": "Simple SAPUI5 based library with Terminologies", + "dependencies": {}, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + } +} diff --git a/test/fixtures/library.o/src/library/o/.library b/test/fixtures/library.o/src/library/o/.library new file mode 100644 index 000000000..7214eea68 --- /dev/null +++ b/test/fixtures/library.o/src/library/o/.library @@ -0,0 +1,12 @@ + + + + library.o + SAP SE + ${copyright} + ${version} + + {{title}} + {{description}} + + diff --git a/test/fixtures/library.o/src/library/o/library.js b/test/fixtures/library.o/src/library/o/library.js new file mode 100644 index 000000000..d8fcee8a3 --- /dev/null +++ b/test/fixtures/library.o/src/library/o/library.js @@ -0,0 +1,17 @@ +/*! + * Some fancy copyright + */ +sap.ui.define([ + 'sap/ui/core/Core', +], (Core) => { + "use strict"; + + sap.ui.getCore().initLibrary({ + name : "library.o", + version: "1.0.0", + dependencies : [] + }); + + return thisLib; + +}); diff --git a/test/fixtures/library.o/src/library/o/manifest.json b/test/fixtures/library.o/src/library/o/manifest.json new file mode 100644 index 000000000..f2200523f --- /dev/null +++ b/test/fixtures/library.o/src/library/o/manifest.json @@ -0,0 +1,65 @@ +{ + "_version": "1.58.0", + "sap.app": { + "id": "library.o", + "type": "library", + "embeds": [], + "applicationVersion": { + "version": "1.0.0" + }, + "title": "{{title}}", + "description": "{{description}}", + "resources": "resources.json", + "offline": true + }, + "sap.ui": { + "technology": "UI5", + "supportedThemes": [] + }, + "sap.ui5": { + "dependencies": { + "libs": {} + }, + "library": { + "i18n": { + "bundleUrl": "messagebundle.properties", + "terminologies": { + "sports": { + "bundleUrl": "sports.properties", + "bundleUrlRelativeTo": "manifest" + }, + "travel": { + "bundleUrl": "travel.properties", + "bundleUrlRelativeTo": "manifest" + } + }, + "enhanceWith": [ + { + "bundleUrl": "myfolder1/i18n.properties", + "bundleUrlRelativeTo": "manifest", + "terminologies": { + "sports": { + "bundleUrl": "myfolder1/soccer.properties", + "bundleUrlRelativeTo": "manifest" + }, + "travel": { + "bundleUrl": "myfolder1/vehicles.properties", + "bundleUrlRelativeTo": "manifest" + } + } + }, + { + "bundleUrl": "myfolder2/i18n.properties", + "bundleUrlRelativeTo": "manifest", + "terminologies": { + "travel": { + "bundleUrl": "myfolder2/bicycles.properties", + "bundleUrlRelativeTo": "manifest" + } + } + } + ] + } + } + } +} diff --git a/test/fixtures/library.o/src/library/o/messagebundle.properties b/test/fixtures/library.o/src/library/o/messagebundle.properties new file mode 100644 index 000000000..b23c5ecd1 --- /dev/null +++ b/test/fixtures/library.o/src/library/o/messagebundle.properties @@ -0,0 +1,2 @@ +title=a title +description=a description \ No newline at end of file diff --git a/test/fixtures/library.o/src/library/o/messagebundle_de.properties b/test/fixtures/library.o/src/library/o/messagebundle_de.properties new file mode 100644 index 000000000..b23c5ecd1 --- /dev/null +++ b/test/fixtures/library.o/src/library/o/messagebundle_de.properties @@ -0,0 +1,2 @@ +title=a title +description=a description \ No newline at end of file diff --git a/test/fixtures/library.o/src/library/o/messagebundle_en.properties b/test/fixtures/library.o/src/library/o/messagebundle_en.properties new file mode 100644 index 000000000..b23c5ecd1 --- /dev/null +++ b/test/fixtures/library.o/src/library/o/messagebundle_en.properties @@ -0,0 +1,2 @@ +title=a title +description=a description \ No newline at end of file diff --git a/test/fixtures/library.o/src/library/o/myfolder1/i18n.properties b/test/fixtures/library.o/src/library/o/myfolder1/i18n.properties new file mode 100644 index 000000000..7a2a6e8bb --- /dev/null +++ b/test/fixtures/library.o/src/library/o/myfolder1/i18n.properties @@ -0,0 +1 @@ +title=my company 1 title \ No newline at end of file diff --git a/test/fixtures/library.o/src/library/o/myfolder1/i18n_en.properties b/test/fixtures/library.o/src/library/o/myfolder1/i18n_en.properties new file mode 100644 index 000000000..7a2a6e8bb --- /dev/null +++ b/test/fixtures/library.o/src/library/o/myfolder1/i18n_en.properties @@ -0,0 +1 @@ +title=my company 1 title \ No newline at end of file diff --git a/test/fixtures/library.o/src/library/o/myfolder1/soccer.properties b/test/fixtures/library.o/src/library/o/myfolder1/soccer.properties new file mode 100644 index 000000000..5ca5f663b --- /dev/null +++ b/test/fixtures/library.o/src/library/o/myfolder1/soccer.properties @@ -0,0 +1 @@ +title=my soccer title \ No newline at end of file diff --git a/test/fixtures/library.o/src/library/o/myfolder1/soccer_de.properties b/test/fixtures/library.o/src/library/o/myfolder1/soccer_de.properties new file mode 100644 index 000000000..1c9b39ba5 --- /dev/null +++ b/test/fixtures/library.o/src/library/o/myfolder1/soccer_de.properties @@ -0,0 +1 @@ +title=Mein Fussball Titel \ No newline at end of file diff --git a/test/fixtures/library.o/src/library/o/myfolder1/soccer_en.properties b/test/fixtures/library.o/src/library/o/myfolder1/soccer_en.properties new file mode 100644 index 000000000..5ca5f663b --- /dev/null +++ b/test/fixtures/library.o/src/library/o/myfolder1/soccer_en.properties @@ -0,0 +1 @@ +title=my soccer title \ No newline at end of file diff --git a/test/fixtures/library.o/src/library/o/myfolder1/vehicles.properties b/test/fixtures/library.o/src/library/o/myfolder1/vehicles.properties new file mode 100644 index 000000000..2e42e0f6f --- /dev/null +++ b/test/fixtures/library.o/src/library/o/myfolder1/vehicles.properties @@ -0,0 +1 @@ +title=my vehicle title \ No newline at end of file diff --git a/test/fixtures/library.o/src/library/o/myfolder1/vehicles_de.properties b/test/fixtures/library.o/src/library/o/myfolder1/vehicles_de.properties new file mode 100644 index 000000000..5cc4cab70 --- /dev/null +++ b/test/fixtures/library.o/src/library/o/myfolder1/vehicles_de.properties @@ -0,0 +1 @@ +title=Mein Fahrzeugstitel \ No newline at end of file diff --git a/test/fixtures/library.o/src/library/o/myfolder1/vehicles_en.properties b/test/fixtures/library.o/src/library/o/myfolder1/vehicles_en.properties new file mode 100644 index 000000000..2e42e0f6f --- /dev/null +++ b/test/fixtures/library.o/src/library/o/myfolder1/vehicles_en.properties @@ -0,0 +1 @@ +title=my vehicle title \ No newline at end of file diff --git a/test/fixtures/library.o/src/library/o/myfolder2/bicycles.properties b/test/fixtures/library.o/src/library/o/myfolder2/bicycles.properties new file mode 100644 index 000000000..b288e4bfd --- /dev/null +++ b/test/fixtures/library.o/src/library/o/myfolder2/bicycles.properties @@ -0,0 +1 @@ +title=my bicycle title \ No newline at end of file diff --git a/test/fixtures/library.o/src/library/o/myfolder2/bicycles_de.properties b/test/fixtures/library.o/src/library/o/myfolder2/bicycles_de.properties new file mode 100644 index 000000000..f613ffa9d --- /dev/null +++ b/test/fixtures/library.o/src/library/o/myfolder2/bicycles_de.properties @@ -0,0 +1 @@ +title=Mein Fahrradtitel \ No newline at end of file diff --git a/test/fixtures/library.o/src/library/o/myfolder2/bicycles_en.properties b/test/fixtures/library.o/src/library/o/myfolder2/bicycles_en.properties new file mode 100644 index 000000000..b288e4bfd --- /dev/null +++ b/test/fixtures/library.o/src/library/o/myfolder2/bicycles_en.properties @@ -0,0 +1 @@ +title=my bicycle title \ No newline at end of file diff --git a/test/fixtures/library.o/src/library/o/myfolder2/i18n.properties b/test/fixtures/library.o/src/library/o/myfolder2/i18n.properties new file mode 100644 index 000000000..375641827 --- /dev/null +++ b/test/fixtures/library.o/src/library/o/myfolder2/i18n.properties @@ -0,0 +1 @@ +title=my company 2 title \ No newline at end of file diff --git a/test/fixtures/library.o/src/library/o/myfolder2/i18n_en.properties b/test/fixtures/library.o/src/library/o/myfolder2/i18n_en.properties new file mode 100644 index 000000000..375641827 --- /dev/null +++ b/test/fixtures/library.o/src/library/o/myfolder2/i18n_en.properties @@ -0,0 +1 @@ +title=my company 2 title \ No newline at end of file diff --git a/test/fixtures/library.o/src/library/o/sports.properties b/test/fixtures/library.o/src/library/o/sports.properties new file mode 100644 index 000000000..4b5622875 --- /dev/null +++ b/test/fixtures/library.o/src/library/o/sports.properties @@ -0,0 +1,2 @@ +title=a sports title +description=a sports description \ No newline at end of file diff --git a/test/fixtures/library.o/src/library/o/sports_de.properties b/test/fixtures/library.o/src/library/o/sports_de.properties new file mode 100644 index 000000000..c6ce6e0ff --- /dev/null +++ b/test/fixtures/library.o/src/library/o/sports_de.properties @@ -0,0 +1,2 @@ +title=ein Sporttitel +description=eine Sportbeschreibung \ No newline at end of file diff --git a/test/fixtures/library.o/src/library/o/sports_en.properties b/test/fixtures/library.o/src/library/o/sports_en.properties new file mode 100644 index 000000000..4b5622875 --- /dev/null +++ b/test/fixtures/library.o/src/library/o/sports_en.properties @@ -0,0 +1,2 @@ +title=a sports title +description=a sports description \ No newline at end of file diff --git a/test/fixtures/library.o/src/library/o/travel.properties b/test/fixtures/library.o/src/library/o/travel.properties new file mode 100644 index 000000000..1e5ad188e --- /dev/null +++ b/test/fixtures/library.o/src/library/o/travel.properties @@ -0,0 +1,2 @@ +title=a travel title +description=a travel description \ No newline at end of file diff --git a/test/fixtures/library.o/src/library/o/travel_de.properties b/test/fixtures/library.o/src/library/o/travel_de.properties new file mode 100644 index 000000000..67689b27f --- /dev/null +++ b/test/fixtures/library.o/src/library/o/travel_de.properties @@ -0,0 +1,2 @@ +title=ein Reisetitel +description=eine Reisebeschreibung \ No newline at end of file diff --git a/test/fixtures/library.o/src/library/o/travel_en.properties b/test/fixtures/library.o/src/library/o/travel_en.properties new file mode 100644 index 000000000..1e5ad188e --- /dev/null +++ b/test/fixtures/library.o/src/library/o/travel_en.properties @@ -0,0 +1,2 @@ +title=a travel title +description=a travel description \ No newline at end of file diff --git a/test/fixtures/library.o/ui5.yaml b/test/fixtures/library.o/ui5.yaml new file mode 100644 index 000000000..ac1f5d168 --- /dev/null +++ b/test/fixtures/library.o/ui5.yaml @@ -0,0 +1,5 @@ +--- +specVersion: "0.1" +type: library +metadata: + name: library.o diff --git a/test/lib/builder/builder.js b/test/lib/builder/builder.js index 29128844e..464734f10 100644 --- a/test/lib/builder/builder.js +++ b/test/lib/builder/builder.js @@ -24,6 +24,7 @@ const applicationKPath = path.join(__dirname, "..", "..", "fixtures", "applicati const applicationLPath = path.join(__dirname, "..", "..", "fixtures", "application.l"); const applicationMPath = path.join(__dirname, "..", "..", "fixtures", "application.m"); const applicationØPath = path.join(__dirname, "..", "..", "fixtures", "application.ø"); +const applicationOPath = path.join(__dirname, "..", "..", "fixtures", "application.o"); const collectionPath = path.join(__dirname, "..", "..", "fixtures", "collection"); const libraryDPath = path.join(__dirname, "..", "..", "fixtures", "library.d"); const libraryEPath = path.join(__dirname, "..", "..", "fixtures", "library.e"); @@ -32,6 +33,7 @@ const libraryIPath = path.join(__dirname, "..", "..", "fixtures", "library.i"); const libraryJPath = path.join(__dirname, "..", "..", "fixtures", "library.j"); const libraryLPath = path.join(__dirname, "..", "..", "fixtures", "library.l"); const libraryØPath = path.join(__dirname, "..", "..", "fixtures", "library.ø"); +const libraryOPath = path.join(__dirname, "..", "..", "fixtures", "library.o"); const libraryCore = path.join(__dirname, "..", "..", "fixtures", "sap.ui.core-evo"); const libraryCoreBuildtime = path.join(__dirname, "..", "..", "fixtures", "sap.ui.core-buildtime"); const themeJPath = path.join(__dirname, "..", "..", "fixtures", "theme.j"); @@ -1256,6 +1258,46 @@ test.serial("Build theme-library with CSS variables and theme designer resources t.pass(); }); +test.serial("Build library.o with terminologies and supportedLocales", async (t) => { + const destPath = path.join("test", "tmp", "build", "library.o", "dest"); + const expectedPath = path.join("test", "expected", "build", "library.o", "dest"); + + const graph = await graphFromPackageDependencies({ + cwd: libraryOPath + }); + graph.setTaskRepository(taskRepository); + await graph.build({ + destPath + }); + + const expectedFiles = await findFiles(expectedPath); + // Check for all directories and files + await directoryDeepEqual(t, destPath, expectedPath); + // Check for all file contents + await checkFileContentsIgnoreLineFeeds(t, expectedFiles, expectedPath, destPath); + t.pass(); +}); + +test.serial("Build application.o with terminologies and supportedLocales", async (t) => { + const destPath = path.join("test", "tmp", "build", "application.o", "dest"); + const expectedPath = path.join("test", "expected", "build", "application.o", "dest"); + + const graph = await graphFromPackageDependencies({ + cwd: applicationOPath + }); + graph.setTaskRepository(taskRepository); + await graph.build({ + destPath + }); + + const expectedFiles = await findFiles(expectedPath); + // Check for all directories and files + await directoryDeepEqual(t, destPath, expectedPath); + // Check for all file contents + await checkFileContentsIgnoreLineFeeds(t, expectedFiles, expectedPath, destPath); + t.pass(); +}); + const libraryDTree = { "id": "library.d", "version": "1.0.0", diff --git a/test/lib/package-exports.js b/test/lib/package-exports.js index ca4ac2464..e07b7b594 100644 --- a/test/lib/package-exports.js +++ b/test/lib/package-exports.js @@ -26,6 +26,7 @@ test("check number of exports", (t) => { "processors/minifier", "processors/libraryLessGenerator", "processors/manifestCreator", + "processors/manifestEnhancer", "processors/nonAsciiEscaper", "processors/stringReplacer", "processors/themeBuilder", @@ -48,6 +49,7 @@ test("check number of exports", (t) => { "tasks/replaceVersion", "tasks/replaceBuildtime", "tasks/transformBootstrapHtml", + "tasks/enhanceManifest", // Internal modules (only to be used by @ui5/* packages) {exportedSpecifier: "internal/taskRepository", mappedModule: "../../lib/tasks/taskRepository.js"}, diff --git a/test/lib/processors/manifestEnhancer.js b/test/lib/processors/manifestEnhancer.js new file mode 100644 index 000000000..c09324f30 --- /dev/null +++ b/test/lib/processors/manifestEnhancer.js @@ -0,0 +1,2958 @@ +import test from "ava"; +import sinonGlobal from "sinon"; +import esmock from "esmock"; + +test.beforeEach(async (t) => { + const sinon = t.context.sinon = sinonGlobal.createSandbox(); + t.context.logWarnSpy = sinon.spy(); + t.context.logVerboseSpy = sinon.spy(); + t.context.logErrorSpy = sinon.spy(); + const loggerStub = { + warn: t.context.logWarnSpy, + verbose: t.context.logVerboseSpy, + error: t.context.logErrorSpy + }; + const manifestEnhancerImport = await esmock("../../../lib/processors/manifestEnhancer.js", { + "@ui5/logger": { + getLogger: sinon.stub().withArgs("builder:processors:manifestEnhancer").returns(loggerStub) + } + }); + t.context.manifestEnhancer = manifestEnhancerImport.default; + t.context.__internals__ = manifestEnhancerImport.__internals__; + + t.context.fs = { + readdir: sinon.stub().callsArgWith(1, null, []) + }; + + t.context.createResource = (path, bNamespaced, input) => { + return { + getString: () => Promise.resolve(input), + setString: sinon.stub(), + getProject() { + return { + getNamespace() { + const namespace = path.substring(0, path.lastIndexOf("/")).replace("/resources/", ""); + return bNamespaced ? namespace : ""; + } + }; + }, + getPath() { + return path; + } + }; + }; +}); + +test.afterEach.always((t) => { + t.context.sinon.restore(); +}); + + +// ####################################################### +// Type: Application +// ####################################################### + +test("Application: No replacement (No properties files)", async (t) => { + const {manifestEnhancer, fs, createResource} = t.context; + + const input = JSON.stringify({ + "_version": "1.58.0", + "sap.app": { + "id": "sap.ui.demo.app", + "type": "application" + } + }, null, 2); + + const resource = createResource("/resources/sap/ui/demo/app/manifest.json", true, input); + + const processedResources = await manifestEnhancer({ + resources: [resource], + fs + }); + + t.deepEqual(processedResources, [], "Only enhanced resources are returned"); + + t.is(resource.setString.callCount, 0, "setString should not be called"); + + t.true(t.context.logVerboseSpy.notCalled, "No verbose messages should be logged"); + t.true(t.context.logWarnSpy.notCalled, "No warnings should be logged"); + t.true(t.context.logErrorSpy.notCalled, "No errors should be logged"); +}); + + +test("Application: sap.app/i18n (without templates, default bundle): " + + "Adds supportedLocales based on available properties files", +async (t) => { + const {manifestEnhancer, fs, createResource} = t.context; + const input = JSON.stringify({ + "_version": "1.58.0", + "sap.app": { + "id": "sap.ui.demo.app", + "type": "application" + } + }, null, 2); + + const expected = JSON.stringify({ + "_version": "1.58.0", + "sap.app": { + "id": "sap.ui.demo.app", + "type": "application", + "i18n": { + "bundleUrl": "i18n/i18n.properties", + "supportedLocales": ["de", "en"] + } + } + }, null, 2); + + const resource = createResource("/resources/sap/ui/demo/app/manifest.json", true, input); + + fs.readdir.withArgs("/resources/sap/ui/demo/app/i18n") + .callsArgWith(1, null, ["i18n_de.properties", "i18n_en.properties"]); + + const processedResources = await manifestEnhancer({ + resources: [resource], + fs + }); + + t.deepEqual(processedResources, [resource], "Input resource is returned"); + + t.is(resource.setString.callCount, 1, "setString should be called once"); + t.deepEqual(resource.setString.getCall(0).args, [expected], "Correct file content should be set"); + + t.true(t.context.logVerboseSpy.notCalled, "No verbose messages should be logged"); + t.true(t.context.logWarnSpy.notCalled, "No warnings should be logged"); + t.true(t.context.logErrorSpy.notCalled, "No errors should be logged"); +}); + +test("Application: sap.app/i18n (with templates, default bundle): " + + "Adds supportedLocales based on available properties files", +async (t) => { + const {manifestEnhancer, fs, createResource} = t.context; + const input = JSON.stringify({ + "_version": "1.58.0", + "sap.app": { + "id": "sap.ui.demo.app", + "type": "application", + "title": "{{title}}" + } + }, null, 2); + + const expected = JSON.stringify({ + "_version": "1.58.0", + "sap.app": { + "id": "sap.ui.demo.app", + "type": "application", + "title": "{{title}}", + "i18n": { + "bundleUrl": "i18n/i18n.properties", + "supportedLocales": ["de", "en"] + } + } + }, null, 2); + + const resource = createResource("/resources/sap/ui/demo/app/manifest.json", true, input); + + fs.readdir.withArgs("/resources/sap/ui/demo/app/i18n") + .callsArgWith(1, null, ["i18n_de.properties", "i18n_en.properties"]); + + const processedResources = await manifestEnhancer({ + resources: [resource], + fs + }); + + t.deepEqual(processedResources, [resource], "Input resource is returned"); + + t.is(resource.setString.callCount, 1, "setString should be called once"); + t.deepEqual(resource.setString.getCall(0).args, [expected], "Correct file content should be set"); + + t.true(t.context.logVerboseSpy.notCalled, "No verbose messages should be logged"); + t.true(t.context.logWarnSpy.notCalled, "No warnings should be logged"); + t.true(t.context.logErrorSpy.notCalled, "No errors should be logged"); +}); + +test("Application: sap.app/i18n (with templates, custom bundle): " + + "Adds supportedLocales based on available properties files", +async (t) => { + const {manifestEnhancer, fs, createResource} = t.context; + const input = JSON.stringify({ + "_version": "1.58.0", + "sap.app": { + "id": "sap.ui.demo.app", + "type": "application", + "title": "{{title}}", + "i18n": "mybundle.properties" + } + }, null, 2); + + const expected = JSON.stringify({ + "_version": "1.58.0", + "sap.app": { + "id": "sap.ui.demo.app", + "type": "application", + "title": "{{title}}", + "i18n": { + "bundleUrl": "mybundle.properties", + "supportedLocales": ["de", "en"] + } + } + }, null, 2); + + const resource = createResource("/resources/sap/ui/demo/app/manifest.json", true, input); + + fs.readdir.withArgs("/resources/sap/ui/demo/app") + .callsArgWith(1, null, ["mybundle_de.properties", "mybundle_en.properties"]); + + const processedResources = await manifestEnhancer({ + resources: [resource], + fs + }); + + t.deepEqual(processedResources, [resource], "Input resource is returned"); + + t.is(resource.setString.callCount, 1, "setString should be called once"); + t.deepEqual(resource.setString.getCall(0).args, [expected], "Correct file content should be set"); + + t.true(t.context.logVerboseSpy.notCalled, "No verbose messages should be logged"); + t.true(t.context.logWarnSpy.notCalled, "No warnings should be logged"); + t.true(t.context.logErrorSpy.notCalled, "No errors should be logged"); +}); + +test("Application: sap.ui5/models: " + + "Adds supportedLocales based on available properties files", +async (t) => { + const {manifestEnhancer, fs, createResource} = t.context; + const input = JSON.stringify({ + "_version": "1.58.0", + "sap.app": { + "id": "sap.ui.demo.app", + "type": "application" + }, + "sap.ui5": { + "models": { + "i18n": { + "type": "sap.ui.model.resource.ResourceModel", + "settings": { + "bundleName": "sap.ui.demo.app.i18nModel.i18n", + "fallbackLocale": "de" + } + } + } + } + }, null, 2); + + const expected = JSON.stringify({ + "_version": "1.58.0", + "sap.app": { + "id": "sap.ui.demo.app", + "type": "application" + }, + "sap.ui5": { + "models": { + "i18n": { + "type": "sap.ui.model.resource.ResourceModel", + "settings": { + "bundleName": "sap.ui.demo.app.i18nModel.i18n", + "fallbackLocale": "de", + "supportedLocales": ["de", "en"] + } + } + } + } + }, null, 2); + + const resource = createResource("/resources/sap/ui/demo/app/manifest.json", true, input); + + // "i18n_fr.txt" was placed into the i18nModel folder but should not be analyzed by the processor + fs.readdir.withArgs("/resources/sap/ui/demo/app/i18nModel") + .callsArgWith(1, null, ["i18n_de.properties", "i18n_en.properties", "i18n_fr.txt"]); + + const processedResources = await manifestEnhancer({ + resources: [resource], + fs + }); + + t.deepEqual(processedResources, [resource], "Input resource is returned"); + + t.is(resource.setString.callCount, 1, "setString should be called once"); + t.deepEqual(resource.setString.getCall(0).args, [expected], "Correct file content should be set"); + + t.true(t.context.logVerboseSpy.notCalled, "No verbose messages should be logged"); + t.true(t.context.logWarnSpy.notCalled, "No warnings should be logged"); + t.true(t.context.logErrorSpy.notCalled, "No errors should be logged"); +}); + +test("Application: sap.ui5/models: " + + "Adds supportedLocales based on available properties files (properties files on root level)", +async (t) => { + const {manifestEnhancer, fs, createResource} = t.context; + const input = JSON.stringify({ + "_version": "1.58.0", + "sap.app": { + "id": "sap.ui.demo.app", + "type": "application" + }, + "sap.ui5": { + "models": { + "i18n": { + "type": "sap.ui.model.resource.ResourceModel", + "settings": { + "bundleName": "sap.ui.demo.app.i18n" + } + } + } + } + }, null, 2); + + const expected = JSON.stringify({ + "_version": "1.58.0", + "sap.app": { + "id": "sap.ui.demo.app", + "type": "application" + }, + "sap.ui5": { + "models": { + "i18n": { + "type": "sap.ui.model.resource.ResourceModel", + "settings": { + "bundleName": "sap.ui.demo.app.i18n", + "supportedLocales": ["de", "en"] + } + } + } + } + }, null, 2); + + const resource = createResource("/resources/sap/ui/demo/app/manifest.json", true, input); + + fs.readdir.withArgs("/resources/sap/ui/demo/app") + .callsArgWith(1, null, ["i18n_de.properties", "i18n_en.properties"]); + + const processedResources = await manifestEnhancer({ + resources: [resource], + fs + }); + + t.deepEqual(processedResources, [resource], "Input resource is returned"); + + t.is(resource.setString.callCount, 1, "setString should be called once"); + t.deepEqual(resource.setString.getCall(0).args, [expected], "Correct file content should be set"); + + t.true(t.context.logVerboseSpy.notCalled, "No verbose messages should be logged"); + t.true(t.context.logWarnSpy.notCalled, "No warnings should be logged"); + t.true(t.context.logErrorSpy.notCalled, "No errors should be logged"); +}); + +test("Application: sap.ui5/models (bundleUrl): " + + "Adds supportedLocales based on available properties files", +async (t) => { + const {manifestEnhancer, fs, createResource} = t.context; + const input = JSON.stringify({ + "_version": "1.58.0", + "sap.app": { + "id": "sap.ui.demo.app", + "type": "application" + }, + "sap.ui5": { + "models": { + "i18n": { + "type": "sap.ui.model.resource.ResourceModel", + "settings": { + "bundleUrl": "i18nModel/i18n.properties", + "fallbackLocale": "de" + } + } + } + } + }, null, 2); + + const expected = JSON.stringify({ + "_version": "1.58.0", + "sap.app": { + "id": "sap.ui.demo.app", + "type": "application" + }, + "sap.ui5": { + "models": { + "i18n": { + "type": "sap.ui.model.resource.ResourceModel", + "settings": { + "bundleUrl": "i18nModel/i18n.properties", + "fallbackLocale": "de", + "supportedLocales": ["de", "en"] + } + } + } + } + }, null, 2); + + const resource = createResource("/resources/sap/ui/demo/app/manifest.json", true, input); + + fs.readdir.withArgs("/resources/sap/ui/demo/app/i18nModel") + .callsArgWith(1, null, ["i18n_de.properties", "i18n_en.properties"]); + + const processedResources = await manifestEnhancer({ + resources: [resource], + fs + }); + + t.deepEqual(processedResources, [resource], "Input resource is returned"); + + t.is(resource.setString.callCount, 1, "setString should be called once"); + t.deepEqual(resource.setString.getCall(0).args, [expected], "Correct file content should be set"); + + t.true(t.context.logVerboseSpy.notCalled, "No verbose messages should be logged"); + t.true(t.context.logWarnSpy.notCalled, "No warnings should be logged"); + t.true(t.context.logErrorSpy.notCalled, "No errors should be logged"); +}); + +test("Application: sap.ui5/models (bundleUrl with ui5 protocol): " + + "Adds supportedLocales based on available properties files", +async (t) => { + const {manifestEnhancer, fs, createResource} = t.context; + const input = JSON.stringify({ + "_version": "1.58.0", + "sap.app": { + "id": "sap.ui.demo.app", + "type": "application" + }, + "sap.ui5": { + "models": { + "i18n": { + "type": "sap.ui.model.resource.ResourceModel", + "settings": { + "bundleUrl": "ui5://sap/ui/demo/app/i18nModel/i18n.properties", + "fallbackLocale": "de" + } + } + } + } + }, null, 2); + + const expected = JSON.stringify({ + "_version": "1.58.0", + "sap.app": { + "id": "sap.ui.demo.app", + "type": "application" + }, + "sap.ui5": { + "models": { + "i18n": { + "type": "sap.ui.model.resource.ResourceModel", + "settings": { + "bundleUrl": "ui5://sap/ui/demo/app/i18nModel/i18n.properties", + "fallbackLocale": "de", + "supportedLocales": ["de", "en"] + } + } + } + } + }, null, 2); + + const resource = createResource("/resources/sap/ui/demo/app/manifest.json", true, input); + + fs.readdir.withArgs("/resources/sap/ui/demo/app/i18nModel") + .callsArgWith(1, null, ["i18n_de.properties", "i18n_en.properties"]); + + const processedResources = await manifestEnhancer({ + resources: [resource], + fs + }); + + t.deepEqual(processedResources, [resource], "Input resource is returned"); + + t.is(resource.setString.callCount, 1, "setString should be called once"); + t.deepEqual(resource.setString.getCall(0).args, [expected], "Correct file content should be set"); + + t.true(t.context.logVerboseSpy.notCalled, "No verbose messages should be logged"); + t.true(t.context.logWarnSpy.notCalled, "No warnings should be logged"); + t.true(t.context.logErrorSpy.notCalled, "No errors should be logged"); +}); + +test("Application: sap.ui5/models (uri): " + + "Adds supportedLocales with available properties files", +async (t) => { + const {manifestEnhancer, fs, createResource} = t.context; + const input = JSON.stringify({ + "_version": "1.58.0", + "sap.app": { + "id": "sap.ui.demo.app", + "type": "application" + }, + "sap.ui5": { + "models": { + "i18n": { + "type": "sap.ui.model.resource.ResourceModel", + "uri": "i18nModel/i18n.properties" + } + } + } + }, null, 2); + + const expected = JSON.stringify({ + "_version": "1.58.0", + "sap.app": { + "id": "sap.ui.demo.app", + "type": "application" + }, + "sap.ui5": { + "models": { + "i18n": { + "type": "sap.ui.model.resource.ResourceModel", + "uri": "i18nModel/i18n.properties", + "settings": { + "supportedLocales": ["de", "en"] + } + } + } + } + }, null, 2); + + const resource = createResource("/resources/sap/ui/demo/app/manifest.json", true, input); + + fs.readdir.withArgs("/resources/sap/ui/demo/app/i18nModel") + .callsArgWith(1, null, ["i18n_de.properties", "i18n_en.properties"]); + + const processedResources = await manifestEnhancer({ + resources: [resource], + fs + }); + + t.deepEqual(processedResources, [resource], "Input resource is returned"); + + t.is(resource.setString.callCount, 1, "setString should be called once"); + t.deepEqual(resource.setString.getCall(0).args, [expected], "Correct file content should be set"); + + t.true(t.context.logVerboseSpy.notCalled, "No verbose messages should be logged"); + t.true(t.context.logWarnSpy.notCalled, "No warnings should be logged"); + t.true(t.context.logErrorSpy.notCalled, "No errors should be logged"); +}); + +test("Application: sap.ui5/models (with terminologies and enhanceWith): " + + "Adds supportedLocales based on available properties files", +async (t) => { + const {manifestEnhancer, fs, createResource} = t.context; + const input = JSON.stringify({ + "_version": "1.58.0", + "sap.app": { + "id": "sap.ui.demo.app", + "type": "application" + }, + "sap.ui5": { + "models": { + "i18n": { + "type": "sap.ui.model.resource.ResourceModel", + "settings": { + "bundleUrl": "i18nModel/i18n.properties", + "terminologies": { + "oil": { + "bundleUrl": "i18n/terminologies.oil.i18n.properties", + }, + "retail": { + "bundleUrl": "i18n/terminologies.retail.i18n.properties", + } + }, + "enhanceWith": [ + { + "bundleUrl": "./enhancements/i18n/i18n.properties", + "bundleUrlRelativeTo": "manifest", + "terminologies": { + "oil": { + "bundleUrl": "./enhancements/i18n/terminologies.oil.i18n.properties", + }, + "retail": { + "bundleUrl": "./enhancements/i18n/terminologies.retail.i18n.properties", + "bundleUrlRelativeTo": "manifest" + } + } + }, + { + "bundleUrl": "../some/path/to/i18n/i18n.properties", + "bundleUrlRelativeTo": "manifest", + "terminologies": { + "oil": { + "bundleUrl": "../some/path/to/terminologies.oil.i18n.properties", + }, + "retail": { + "bundleUrl": "../some/path/to/terminologies.retail.i18n.properties", + "bundleUrlRelativeTo": "manifest" + } + } + }, + { + "bundleName": "appvar2.i18n.i18n.properties", + "terminologies": { + "oil": { + "bundleName": "appvar2.i18n.terminologies.oil.i18n", + }, + "retail": { + "bundleName": "appvar2.i18n.terminologies.retail.i18n", + } + } + } + ] + } + } + } + } + }, null, 2); + + const expected = JSON.stringify({ + "_version": "1.58.0", + "sap.app": { + "id": "sap.ui.demo.app", + "type": "application" + }, + "sap.ui5": { + "models": { + "i18n": { + "type": "sap.ui.model.resource.ResourceModel", + "settings": { + "bundleUrl": "i18nModel/i18n.properties", + "terminologies": { + "oil": { + "bundleUrl": "i18n/terminologies.oil.i18n.properties", + "supportedLocales": ["", "de", "en", "fr"] + }, + "retail": { + "bundleUrl": "i18n/terminologies.retail.i18n.properties", + "supportedLocales": ["en"] + } + }, + "enhanceWith": [ + { + "bundleUrl": "./enhancements/i18n/i18n.properties", + "bundleUrlRelativeTo": "manifest", + "terminologies": { + "oil": { + "bundleUrl": "./enhancements/i18n/terminologies.oil.i18n.properties", + "supportedLocales": ["", "en", "fr"] + }, + "retail": { + "bundleUrl": "./enhancements/i18n/terminologies.retail.i18n.properties", + "bundleUrlRelativeTo": "manifest", + "supportedLocales": ["de", "en"] + } + }, + "supportedLocales": ["de", "en", "es"] + }, + { + "bundleUrl": "../some/path/to/i18n/i18n.properties", + "bundleUrlRelativeTo": "manifest", + "terminologies": { + "oil": { + "bundleUrl": "../some/path/to/terminologies.oil.i18n.properties", + }, + "retail": { + "bundleUrl": "../some/path/to/terminologies.retail.i18n.properties", + "bundleUrlRelativeTo": "manifest" + } + } + }, + { + "bundleName": "appvar2.i18n.i18n.properties", + "terminologies": { + "oil": { + "bundleName": "appvar2.i18n.terminologies.oil.i18n", + }, + "retail": { + "bundleName": "appvar2.i18n.terminologies.retail.i18n", + } + } + } + ], + "supportedLocales": ["de", "en"] + } + } + } + } + }, null, 2); + + const resource = createResource("/resources/sap/ui/demo/app/manifest.json", true, input); + + fs.readdir.withArgs("/resources/sap/ui/demo/app/i18nModel") + .callsArgWith(1, null, [ + "i18n_de.properties", + "i18n_en.properties" + ]); + + fs.readdir.withArgs("/resources/sap/ui/demo/app/i18n") + .callsArgWith(1, null, [ + "terminologies.oil.i18n.properties", + "terminologies.oil.i18n_de.properties", + "terminologies.oil.i18n_en.properties", + "terminologies.oil.i18n_fr.properties", + + "terminologies.retail.i18n_en.properties" + ]); + + fs.readdir.withArgs("/resources/sap/ui/demo/app/enhancements/i18n") + .callsArgWith(1, null, [ + "i18n_de.properties", + "i18n_en.properties", + "i18n_es.properties", + + "terminologies.oil.i18n.properties", + "terminologies.oil.i18n_en.properties", + "terminologies.oil.i18n_fr.properties", + + "terminologies.retail.i18n_de.properties", + "terminologies.retail.i18n_en.properties" + ]); + + const processedResources = await manifestEnhancer({ + resources: [resource], + fs + }); + + t.deepEqual(processedResources, [resource], "Input resource is returned"); + + t.is(resource.setString.callCount, 1, "setString should be called once"); + t.deepEqual(resource.setString.getCall(0).args, [expected], "Correct file content should be set"); + + t.is(t.context.logVerboseSpy.callCount, 6, "One verbose messages should be logged"); + t.is(t.context.logVerboseSpy.getCall(0).args[0], + "/resources/sap/ui/demo/app/manifest.json: bundleUrl '../some/path/to/i18n/i18n.properties' points to a " + + "bundle outside of the current namespace 'sap.ui.demo.app', enhancement of 'supportedLocales' is skipped"); + t.is(t.context.logVerboseSpy.getCall(1).args[0], + "/resources/sap/ui/demo/app/manifest.json: bundleUrl " + + "'../../../../appvar2/i18n/i18n/properties.properties' points to a bundle outside of " + + "the current namespace 'sap.ui.demo.app', enhancement of 'supportedLocales' is skipped"); + t.is(t.context.logVerboseSpy.getCall(2).args[0], + "/resources/sap/ui/demo/app/manifest.json: bundleUrl " + + "'../some/path/to/terminologies.oil.i18n.properties' points to a bundle outside of the " + + "current namespace 'sap.ui.demo.app', enhancement of 'supportedLocales' is skipped"); + t.is(t.context.logVerboseSpy.getCall(3).args[0], + "/resources/sap/ui/demo/app/manifest.json: bundleUrl " + + "'../some/path/to/terminologies.retail.i18n.properties' points to a bundle outside of " + + "the current namespace 'sap.ui.demo.app', enhancement of 'supportedLocales' is skipped"); + t.is(t.context.logVerboseSpy.getCall(4).args[0], + "/resources/sap/ui/demo/app/manifest.json: bundleUrl " + + "'../../../../appvar2/i18n/terminologies/oil/i18n.properties' points to a bundle outside " + + "of the current namespace 'sap.ui.demo.app', enhancement of 'supportedLocales' is skipped"); + t.is(t.context.logVerboseSpy.getCall(5).args[0], + "/resources/sap/ui/demo/app/manifest.json: bundleUrl " + + "'../../../../appvar2/i18n/terminologies/retail/i18n.properties' points to a bundle " + + "outside of the current namespace 'sap.ui.demo.app', enhancement of 'supportedLocales' is skipped"); + t.true(t.context.logWarnSpy.notCalled, "No warnings should be logged"); + t.true(t.context.logErrorSpy.notCalled, "No errors should be logged"); +}); + +test("Application: sap.ui5/models: " + + "Do not replace supportedLocales when supportedLocales are already defined", +async (t) => { + const {manifestEnhancer, fs, createResource} = t.context; + const input = JSON.stringify({ + "_version": "1.58.0", + "sap.app": { + "id": "sap.ui.demo.app", + "type": "application" + }, + "sap.ui5": { + "models": { + "i18n": { + "type": "sap.ui.model.resource.ResourceModel", + "settings": { + "bundleName": "sap.ui.demo.app.i18nModel.i18n", + "supportedLocales": ["en", "fr"], + "fallbackLocale": "fr" + } + } + } + } + }, null, 2); + + const resource = createResource("/resources/sap/ui/demo/app/manifest.json", true, input); + + fs.readdir.withArgs("/resources/sap/ui/demo/app/i18nModel") + .callsArgWith(1, null, ["i18n_de.properties", "i18n_en.properties"]); + + const processedResources = await manifestEnhancer({ + resources: [resource], + fs + }); + + t.deepEqual(processedResources, [], "Only enhanced resources are returned"); + + t.is(resource.setString.callCount, 0, "setString should not be called"); + + t.true(t.context.logVerboseSpy.notCalled, "No verbose messages should be logged"); + t.true(t.context.logWarnSpy.notCalled, "No warnings should be logged"); + t.true(t.context.logErrorSpy.notCalled, "No errors should be logged"); +}); + +test("Application: sap.ui5/models: " + + "Do not replace supportedLocales when supportedLocales are set to array with empty string", +async (t) => { + const {manifestEnhancer, fs, createResource} = t.context; + const input = JSON.stringify({ + "_version": "1.58.0", + "sap.app": { + "id": "sap.ui.demo.app", + "type": "application" + }, + "sap.ui5": { + "models": { + "i18n": { + "type": "sap.ui.model.resource.ResourceModel", + "settings": { + "bundleName": "sap.ui.demo.app.i18nModel.i18n", + "supportedLocales": [""], + "fallbackLocale": "" + } + } + } + } + }, null, 2); + + const resource = createResource("/resources/sap/ui/demo/app/manifest.json", true, input); + + fs.readdir.withArgs("/resources/sap/ui/demo/app/i18nModel") + .callsArgWith(1, null, ["i18n_de.properties", "i18n_en.properties"]); + + const processedResources = await manifestEnhancer({ + resources: [resource], + fs + }); + + t.deepEqual(processedResources, [], "Only enhanced resources are returned"); + + t.is(resource.setString.callCount, 0, "setString should not be called"); + + t.true(t.context.logVerboseSpy.notCalled, "No verbose messages should be logged"); + t.true(t.context.logWarnSpy.notCalled, "No warnings should be logged"); + t.true(t.context.logErrorSpy.notCalled, "No errors should be logged"); +}); + +test("Application: sap.ui5/models: " + + "Do not replace supportedLocales when an invalid bundle config is defined (missing bundleUrl or bundleName)", +async (t) => { + const {manifestEnhancer, fs, createResource} = t.context; + const input = JSON.stringify({ + "_version": "1.58.0", + "sap.app": { + "id": "sap.ui.demo.app", + "type": "application" + }, + "sap.ui5": { + "models": { + "i18n": { + "type": "sap.ui.model.resource.ResourceModel" + } + } + } + }, null, 2); + + const resource = createResource("/resources/sap/ui/demo/app/manifest.json", true, input); + + fs.readdir.withArgs("/resources/sap/ui/demo/app/i18nModel") + .callsArgWith(1, null, ["i18n_de.properties", "i18n_en.properties"]); + + const processedResources = await manifestEnhancer({ + resources: [resource], + fs + }); + + t.deepEqual(processedResources, [], "Only enhanced resources are returned"); + + t.is(resource.setString.callCount, 0, "setString should not be called"); + + t.true(t.context.logVerboseSpy.notCalled, "No verbose messages should be logged"); + t.true(t.context.logWarnSpy.notCalled, "No warnings should be logged"); + t.true(t.context.logErrorSpy.notCalled, "No errors should be logged"); +}); + +test("Application: sap.ui5/models: " + + "Log error, no supportedLocales generation if fallbackLocale is not part of generation", +async (t) => { + const {manifestEnhancer, fs, createResource} = t.context; + const input = JSON.stringify({ + "_version": "1.58.0", + "sap.app": { + "id": "sap.ui.demo.app", + "type": "application" + }, + "sap.ui5": { + "models": { + "i18n": { + "type": "sap.ui.model.resource.ResourceModel", + "settings": { + "bundleName": "sap.ui.demo.app.i18nModel.i18n", + "fallbackLocale": "fr" + } + } + } + } + }, null, 2); + + const resource = createResource("/resources/sap/ui/demo/app/manifest.json", true, input); + + fs.readdir.withArgs("/resources/sap/ui/demo/app/i18nModel") + .callsArgWith(1, null, ["i18n_de.properties", "i18n_en.properties"]); + + const processedResources = await manifestEnhancer({ + resources: [resource], + fs + }); + + t.deepEqual(processedResources, [], "Only enhanced resources are returned"); + + t.is(resource.setString.callCount, 0, "setString should not be called"); + + t.true(t.context.logVerboseSpy.notCalled, "No verbose messages should be logged"); + t.true(t.context.logWarnSpy.notCalled, "No warnings should be logged"); + t.is(t.context.logErrorSpy.callCount, 1, "1 error should be logged"); + t.is(t.context.logErrorSpy.getCall(0).args[0], + "/resources/sap/ui/demo/app/manifest.json: Generated supported locales ('de', 'en') for " + + "bundle 'i18nModel/i18n.properties' not containing the defined fallback locale 'fr'. "+ + "Either provide a properties file for defined fallbackLocale or configure another available fallbackLocale", + "Error message should be correct"); +}); + +test("Application: sap.ui5/models: " + + "Log warning, but generate locales if default fallbackLocale is not part of generation", +async (t) => { + const {manifestEnhancer, fs, createResource} = t.context; + const input = JSON.stringify({ + "_version": "1.58.0", + "sap.app": { + "id": "sap.ui.demo.app", + "type": "application" + }, + "sap.ui5": { + "models": { + "i18n": { + "type": "sap.ui.model.resource.ResourceModel", + "settings": { + "bundleName": "sap.ui.demo.app.i18nModel.i18n" + } + } + } + } + }, null, 2); + + const expected = JSON.stringify({ + "_version": "1.58.0", + "sap.app": { + "id": "sap.ui.demo.app", + "type": "application" + }, + "sap.ui5": { + "models": { + "i18n": { + "type": "sap.ui.model.resource.ResourceModel", + "settings": { + "bundleName": "sap.ui.demo.app.i18nModel.i18n", + "supportedLocales": ["de", "fr"] + } + } + } + } + }, null, 2); + + const resource = createResource("/resources/sap/ui/demo/app/manifest.json", true, input); + + fs.readdir.withArgs("/resources/sap/ui/demo/app/i18nModel") + .callsArgWith(1, null, ["i18n_de.properties", "i18n_fr.properties"]); + + const processedResources = await manifestEnhancer({ + resources: [resource], + fs + }); + + t.deepEqual(processedResources, [resource], "Input resource is returned"); + + t.is(resource.setString.callCount, 1, "setString should be called once"); + t.deepEqual(resource.setString.getCall(0).args, [expected], "Correct file content should be set"); + + t.true(t.context.logVerboseSpy.notCalled, "No verbose messages should be logged"); + t.is(t.context.logWarnSpy.callCount, 1, "1 warning should be logged"); + t.is(t.context.logWarnSpy.getCall(0).args[0], + "/resources/sap/ui/demo/app/manifest.json: Generated supported locales ('de', 'fr') " + + "for bundle 'i18nModel/i18n.properties' do not contain default fallback locale 'en'. " + + "Either provide a properties file for 'en' or configure another available fallbackLocale", + "1 warning should be logged"); + t.true(t.context.logErrorSpy.notCalled, "No errors should be logged"); +}); + +test("Application: sap.ui5/models: Log verbose if manifest version is not defined at all", async (t) => { + const {manifestEnhancer, fs, createResource} = t.context; + const input = JSON.stringify({ + "sap.app": { + "id": "sap.ui.demo.app", + "type": "application" + }, + "sap.ui5": { + "models": { + "i18n": { + "type": "sap.ui.model.resource.ResourceModel", + "settings": { + "bundleName": "sap.ui.demo.app.i18n.i18n" + } + } + } + } + }, null, 2); + + const resource = createResource("/resources/sap/ui/demo/app/manifest.json", true, input); + + const processedResources = await manifestEnhancer({ + resources: [resource], + fs + }); + + t.deepEqual(processedResources, [], "Only enhanced resources are returned"); + + t.is(resource.setString.callCount, 0, "setString should not be called"); + + t.is(t.context.logVerboseSpy.callCount, 1, "1 verbose should be logged"); + t.is(t.context.logVerboseSpy.getCall(0).args[0], + "/resources/sap/ui/demo/app/manifest.json: _version is not defined. No supportedLocales are generated"); + t.true(t.context.logWarnSpy.notCalled, "No warning should be logged"); + t.true(t.context.logErrorSpy.notCalled, "No errors should be logged"); + t.is(fs.readdir.callCount, 0, "readdir should not be called because _version is not defined"); +}); + +test("Application: sap.ui5/models: Log verbose if manifest version is below 1.21.0", async (t) => { + const {manifestEnhancer, fs, createResource} = t.context; + const input = JSON.stringify({ + "_version": "1.20.0", + "sap.app": { + "id": "sap.ui.demo.app", + "type": "application" + }, + "sap.ui5": { + "models": { + "i18n": { + "type": "sap.ui.model.resource.ResourceModel", + "settings": { + "bundleName": "sap.ui.demo.app.i18n.i18n" + } + } + } + } + }, null, 2); + + const resource = createResource("/resources/sap/ui/demo/app/manifest.json", true, input); + + fs.readdir.withArgs("/resources/sap/ui/demo/app/i18n") + .callsArgWith(1, null, ["i18n_de.properties", "i18n_en.properties"]); + + const processedResources = await manifestEnhancer({ + resources: [resource], + fs + }); + + t.deepEqual(processedResources, [], "Only enhanced resources are returned"); + + t.is(resource.setString.callCount, 0, "setString should not be called"); + + t.is(t.context.logVerboseSpy.callCount, 1, "1 verbose should be logged"); + t.is(t.context.logVerboseSpy.getCall(0).args[0], + "/resources/sap/ui/demo/app/manifest.json: _version is lower than 1.21.0 " + + "so no supportedLocales can be generated"); + t.true(t.context.logWarnSpy.notCalled, "No warning should be logged"); + t.true(t.context.logErrorSpy.notCalled, "No errors should be logged"); + t.is(fs.readdir.callCount, 0, "readdir should not be called because _version is lower than 1.21.0"); +}); + +test("Application: sap.ui5/models: " + + "Do not generate supportedLocales when bundleUrl pointing to a location outside the current project", +async (t) => { + const {manifestEnhancer, fs, createResource} = t.context; + const input = JSON.stringify({ + "_version": "1.58.0", + "sap.app": { + "id": "sap.ui.demo.app", + "type": "application" + }, + "sap.ui5": { + "models": { + "i18n": { + "type": "sap.ui.model.resource.ResourceModel", + "settings": { + "bundleUrl": "../../myapp2/i18n/i18n.properties" + } + } + } + } + }, null, 2); + + const resource = createResource("/resources/sap/ui/demo/app/manifest.json", true, input); + + const processedResources = await manifestEnhancer({ + resources: [resource], + fs + }); + + t.deepEqual(processedResources, [], "Only enhanced resources are returned"); + + t.is(resource.setString.callCount, 0, "setString should not be called"); + + t.is(t.context.logVerboseSpy.callCount, 1, "1 verbose should be logged"); + t.is(t.context.logVerboseSpy.getCall(0).args[0], + "/resources/sap/ui/demo/app/manifest.json: bundleUrl '../../myapp2/i18n/i18n.properties' points to a bundle " + + "outside of the current namespace " + "'sap.ui.demo.app', enhancement of 'supportedLocales' is skipped"); + t.true(t.context.logWarnSpy.notCalled, "No warning should be logged"); + t.true(t.context.logErrorSpy.notCalled, "No errors should be logged"); +}); + +test("Application: sap.ui5/models: " + + "Do not generate supportedLocales when bundleUrl pointing to a location inside the current project", +async (t) => { + const {manifestEnhancer, fs, createResource} = t.context; + const input = JSON.stringify({ + "_version": "1.58.0", + "sap.app": { + "id": "sap.ui.demo.app", + "type": "application" + }, + "sap.ui5": { + "models": { + "i18n": { + "type": "sap.ui.model.resource.ResourceModel", + "settings": { + "bundleUrl": "../i18n/i18n.properties", + "fallbackLocale": "de" + } + } + } + } + }, null, 2); + + const resource = createResource("/resources/sap/ui/demo/app/manifest.json", true, input); + + const processedResources = await manifestEnhancer({ + resources: [resource], + fs + }); + + t.deepEqual(processedResources, [], "Only enhanced resources are returned"); + + t.is(resource.setString.callCount, 0, "setString should not be called"); + + t.is(t.context.logVerboseSpy.callCount, 1, "1 verbose should be logged"); + t.is(t.context.logVerboseSpy.getCall(0).args[0], + "/resources/sap/ui/demo/app/manifest.json: bundleUrl '../i18n/i18n.properties' points to a bundle " + + "outside of the current namespace 'sap.ui.demo.app', enhancement of 'supportedLocales' is skipped"); + t.true(t.context.logWarnSpy.notCalled, "No warning should be logged"); + t.true(t.context.logErrorSpy.notCalled, "No errors should be logged"); +}); + +test("Application: sap.ui5/models: " + + "Do not replace supportedLocales when bundle is not part of the namespace", +async (t) => { + const {manifestEnhancer, fs, createResource} = t.context; + const input = JSON.stringify({ + "_version": "1.58.0", + "sap.app": { + "id": "sap.ui.demo.app", + "type": "application" + }, + "sap.ui5": { + "models": { + "i18n": { + "type": "sap.ui.model.resource.ResourceModel", + "settings": { + "bundleName": "sap.ui.demo.app.i18nModel.i18n", + "fallbackLocale": "de" + } + }, + "i18n_reuse_lib": { + "type": "sap.ui.model.resource.ResourceModel", + "settings": { + "bundleName": "sap.ui.demo.lib.i18n.i18n", + "async": true + } + } + } + } + }, null, 2); + + const expected = JSON.stringify({ + "_version": "1.58.0", + "sap.app": { + "id": "sap.ui.demo.app", + "type": "application" + }, + "sap.ui5": { + "models": { + "i18n": { + "type": "sap.ui.model.resource.ResourceModel", + "settings": { + "bundleName": "sap.ui.demo.app.i18nModel.i18n", + "fallbackLocale": "de", + "supportedLocales": ["de", "en"] + } + }, + "i18n_reuse_lib": { + "type": "sap.ui.model.resource.ResourceModel", + "settings": { + "bundleName": "sap.ui.demo.lib.i18n.i18n", + "async": true + } + } + } + } + }, null, 2); + + const resource = createResource("/resources/sap/ui/demo/app/manifest.json", true, input); + + fs.readdir.withArgs("/resources/sap/ui/demo/app/i18nModel") + .callsArgWith(1, null, ["i18n_de.properties", "i18n_en.properties"]); + + const processedResources = await manifestEnhancer({ + resources: [resource], + fs + }); + + t.deepEqual(processedResources, [resource], "Input resource is returned"); + + t.is(resource.setString.callCount, 1, "setString should be called once"); + t.deepEqual(resource.setString.getCall(0).args, [expected], "Correct file content should be set"); + + t.is(t.context.logVerboseSpy.callCount, 1); + t.is(t.context.logVerboseSpy.getCall(0).args[0], + "/resources/sap/ui/demo/app/manifest.json: bundleUrl '../lib/i18n/i18n.properties' points to a bundle " + + "outside of the current namespace 'sap.ui.demo.app', enhancement of 'supportedLocales' is skipped"); + t.true(t.context.logWarnSpy.notCalled, "No warnings should be logged"); + t.true(t.context.logErrorSpy.notCalled, "No errors should be logged"); +}); + +test("Application: sap.ui5/models: " + + "Do not generate supportedLocales when bundle is not part of the namespace (bundleUrl with ui5 protocol)", +async (t) => { + const {manifestEnhancer, fs, createResource} = t.context; + const input = JSON.stringify({ + "_version": "1.58.0", + "sap.app": { + "id": "sap.ui.demo.app", + "type": "application" + }, + "sap.ui5": { + "models": { + "i18n": { + "type": "sap.ui.model.resource.ResourceModel", + "settings": { + "bundleName": "sap.ui.demo.app.i18nModel.i18n", + "fallbackLocale": "de" + } + }, + "i18n_reuse_lib": { + "type": "sap.ui.model.resource.ResourceModel", + "settings": { + "bundleUrl": "ui5://sap/ui/demo/lib/i18n/i18n.properties", + "async": true + } + } + } + } + }, null, 2); + + const expected = JSON.stringify({ + "_version": "1.58.0", + "sap.app": { + "id": "sap.ui.demo.app", + "type": "application" + }, + "sap.ui5": { + "models": { + "i18n": { + "type": "sap.ui.model.resource.ResourceModel", + "settings": { + "bundleName": "sap.ui.demo.app.i18nModel.i18n", + "fallbackLocale": "de", + "supportedLocales": ["de", "en"] + } + }, + "i18n_reuse_lib": { + "type": "sap.ui.model.resource.ResourceModel", + "settings": { + "bundleUrl": "ui5://sap/ui/demo/lib/i18n/i18n.properties", + "async": true + } + } + } + } + }, null, 2); + + const resource = createResource("/resources/sap/ui/demo/app/manifest.json", true, input); + + fs.readdir.withArgs("/resources/sap/ui/demo/app/i18nModel") + .callsArgWith(1, null, ["i18n_de.properties", "i18n_en.properties"]); + + const processedResources = await manifestEnhancer({ + resources: [resource], + fs + }); + + t.deepEqual(processedResources, [resource], "Input resource is returned"); + + t.is(resource.setString.callCount, 1, "setString should be called once"); + t.deepEqual(resource.setString.getCall(0).args, [expected], "Correct file content should be set"); + + t.is(t.context.logVerboseSpy.callCount, 1); + t.is(t.context.logVerboseSpy.getCall(0).args[0], + "/resources/sap/ui/demo/app/manifest.json: bundleUrl 'ui5://sap/ui/demo/lib/i18n/i18n.properties' points to a bundle " + + "outside of the current namespace 'sap.ui.demo.app', enhancement of 'supportedLocales' is skipped"); + t.true(t.context.logWarnSpy.notCalled, "No warnings should be logged"); + t.true(t.context.logErrorSpy.notCalled, "No errors should be logged"); +}); + +test("Application: sap.app/i18n: " + + "Adds supportedLocales for terminologies and enhanceWith bundles", +async (t) => { + const {manifestEnhancer, fs, createResource} = t.context; + const input = JSON.stringify({ + "_version": "1.58.0", + "sap.app": { + "id": "sap.ui.demo.app", + "type": "application", + "i18n": { + "bundleUrl": "i18n/i18n.properties", + "terminologies": { + "oil": { + "bundleUrl": "i18n/terminologies.oil.i18n.properties", + }, + "retail": { + "bundleUrl": "i18n/terminologies.retail.i18n.properties", + } + }, + "enhanceWith": [ + { + "bundleUrl": "./enhancements/i18n/i18n.properties", + "bundleUrlRelativeTo": "manifest", + "terminologies": { + "oil": { + "bundleUrl": "./enhancements/i18n/terminologies.oil.i18n.properties", + }, + "retail": { + "bundleUrl": "./enhancements/i18n/terminologies.retail.i18n.properties", + "bundleUrlRelativeTo": "manifest" + } + } + }, + { + "bundleUrl": "../some/path/to/i18n/i18n.properties", + "bundleUrlRelativeTo": "manifest", + "terminologies": { + "oil": { + "bundleUrl": "../some/path/to/terminologies.oil.i18n.properties", + }, + "retail": { + "bundleUrl": "../some/path/to/terminologies.retail.i18n.properties", + "bundleUrlRelativeTo": "manifest" + } + } + }, + { + "bundleName": "appvar2.i18n.i18n.properties", + "terminologies": { + "oil": { + "bundleName": "appvar2.i18n.terminologies.oil.i18n", + }, + "retail": { + "bundleName": "appvar2.i18n.terminologies.retail.i18n", + } + } + } + ] + } + } + }, null, 2); + + const expected = JSON.stringify({ + "_version": "1.58.0", + "sap.app": { + "id": "sap.ui.demo.app", + "type": "application", + "i18n": { + "bundleUrl": "i18n/i18n.properties", + "terminologies": { + "oil": { + "bundleUrl": "i18n/terminologies.oil.i18n.properties", + "supportedLocales": ["", "de", "en", "fr"] + }, + "retail": { + "bundleUrl": "i18n/terminologies.retail.i18n.properties", + "supportedLocales": ["en"] + } + }, + "enhanceWith": [ + { + "bundleUrl": "./enhancements/i18n/i18n.properties", + "bundleUrlRelativeTo": "manifest", + "terminologies": { + "oil": { + "bundleUrl": "./enhancements/i18n/terminologies.oil.i18n.properties", + "supportedLocales": ["", "en", "fr"] + }, + "retail": { + "bundleUrl": "./enhancements/i18n/terminologies.retail.i18n.properties", + "bundleUrlRelativeTo": "manifest", + "supportedLocales": ["de", "en"] + } + }, + "supportedLocales": ["de", "en", "es"] + }, + { + "bundleUrl": "../some/path/to/i18n/i18n.properties", + "bundleUrlRelativeTo": "manifest", + "terminologies": { + "oil": { + "bundleUrl": "../some/path/to/terminologies.oil.i18n.properties", + }, + "retail": { + "bundleUrl": "../some/path/to/terminologies.retail.i18n.properties", + "bundleUrlRelativeTo": "manifest" + } + } + }, + { + "bundleName": "appvar2.i18n.i18n.properties", + "terminologies": { + "oil": { + "bundleName": "appvar2.i18n.terminologies.oil.i18n", + }, + "retail": { + "bundleName": "appvar2.i18n.terminologies.retail.i18n", + } + } + } + ], + "supportedLocales": ["de", "en"] + } + } + }, null, 2); + + const resource = createResource("/resources/sap/ui/demo/app/manifest.json", true, input); + + fs.readdir.withArgs("/resources/sap/ui/demo/app/i18n") + .callsArgWith(1, null, [ + "i18n_de.properties", + "i18n_en.properties", + + "terminologies.oil.i18n.properties", + "terminologies.oil.i18n_de.properties", + "terminologies.oil.i18n_en.properties", + "terminologies.oil.i18n_fr.properties", + + "terminologies.retail.i18n_en.properties" + ]); + + fs.readdir.withArgs("/resources/sap/ui/demo/app/enhancements/i18n") + .callsArgWith(1, null, [ + "i18n_de.properties", + "i18n_en.properties", + "i18n_es.properties", + + "terminologies.oil.i18n.properties", + "terminologies.oil.i18n_en.properties", + "terminologies.oil.i18n_fr.properties", + + "terminologies.retail.i18n_de.properties", + "terminologies.retail.i18n_en.properties" + ]); + + const processedResources = await manifestEnhancer({ + resources: [resource], + fs + }); + + t.deepEqual(processedResources, [resource], "Input resource is returned"); + + t.is(resource.setString.callCount, 1, "setString should be called once"); + t.deepEqual(resource.setString.getCall(0).args, [expected], "Correct file content should be set"); + + t.is(t.context.logVerboseSpy.callCount, 6, "One verbose messages should be logged"); + t.is(t.context.logVerboseSpy.getCall(0).args[0], + "/resources/sap/ui/demo/app/manifest.json: bundleUrl '../some/path/to/i18n/i18n.properties' points to a " + + "bundle outside of the current namespace 'sap.ui.demo.app', enhancement of 'supportedLocales' is skipped"); + t.is(t.context.logVerboseSpy.getCall(1).args[0], + "/resources/sap/ui/demo/app/manifest.json: bundleUrl '../../../../appvar2/i18n/i18n/properties.properties' " + + "points to a bundle outside of the current namespace 'sap.ui.demo.app', " + + "enhancement of 'supportedLocales' is skipped"); + t.is(t.context.logVerboseSpy.getCall(2).args[0], + "/resources/sap/ui/demo/app/manifest.json: bundleUrl '../some/path/to/terminologies.oil.i18n.properties' " + + "points to a bundle outside of the current namespace 'sap.ui.demo.app', " + + "enhancement of 'supportedLocales' is skipped"); + t.is(t.context.logVerboseSpy.getCall(3).args[0], + "/resources/sap/ui/demo/app/manifest.json: bundleUrl '../some/path/to/terminologies.retail.i18n.properties' " + + "points to a bundle outside of the current namespace 'sap.ui.demo.app', " + + "enhancement of 'supportedLocales' is skipped"); + t.is(t.context.logVerboseSpy.getCall(4).args[0], + "/resources/sap/ui/demo/app/manifest.json: bundleUrl " + + "'../../../../appvar2/i18n/terminologies/oil/i18n.properties' " + + "points to a bundle outside of the current namespace 'sap.ui.demo.app', " + + "enhancement of 'supportedLocales' is skipped"); + t.is(t.context.logVerboseSpy.getCall(5).args[0], + "/resources/sap/ui/demo/app/manifest.json: bundleUrl " + + "'../../../../appvar2/i18n/terminologies/retail/i18n.properties' " + + "points to a bundle outside of the current namespace 'sap.ui.demo.app', " + + "enhancement of 'supportedLocales' is skipped"); + t.true(t.context.logWarnSpy.notCalled, "No warnings should be logged"); + t.true(t.context.logErrorSpy.notCalled, "No errors should be logged"); +}); + +test("Application: supportedLocales are not added for bundles with absolute url", async (t) => { + const {manifestEnhancer, fs, createResource} = t.context; + const input = JSON.stringify({ + "_version": "1.58.0", + "sap.app": { + "id": "sap.ui.demo.app", + "type": "application", + "i18n": "/resources/sap/ui/demo/app/i18n/i18n.properties" + }, + "sap.ui5": { + "models": { + "i18n": { + "type": "sap.ui.model.resource.ResourceModel", + "settings": { + "bundleUrl": "https://example.com/i18nModel/i18n.properties", + "terminologies": { + "oil": { + "bundleUrl": "/i18n/terminologies.oil.i18n.properties", + }, + "retail": { + "bundleUrl": "/i18n/terminologies.retail.i18n.properties", + } + }, + "enhanceWith": [ + { + "bundleUrl": "/enhancements/i18n/i18n.properties", + "bundleUrlRelativeTo": "manifest", + "terminologies": { + "oil": { + "bundleUrl": "/enhancements/i18n/terminologies.oil.i18n.properties", + }, + "retail": { + "bundleUrl": "/enhancements/i18n/terminologies.retail.i18n.properties", + "bundleUrlRelativeTo": "manifest" + } + } + }, + { + "bundleUrl": "/some/path/to/i18n/i18n.properties", + "bundleUrlRelativeTo": "manifest", + "terminologies": { + "oil": { + "bundleUrl": "/some/path/to/terminologies.oil.i18n.properties", + }, + "retail": { + "bundleUrl": "/some/path/to/terminologies.retail.i18n.properties", + "bundleUrlRelativeTo": "manifest" + } + } + } + ] + } + } + } + } + }, null, 2); + + const resource = createResource("/resources/sap/ui/demo/app/manifest.json", true, input); + + const processedResources = await manifestEnhancer({ + resources: [resource], + fs + }); + + t.deepEqual(processedResources, [], "Resource is not changed, therefore not returned"); + + t.is(resource.setString.callCount, 0, "setString should not be called"); + + t.true(t.context.logVerboseSpy.notCalled, "No verbose messages should be logged"); + t.true(t.context.logWarnSpy.notCalled, "No warnings should be logged"); + t.true(t.context.logErrorSpy.notCalled, "No errors should be logged"); + + t.is(fs.readdir.callCount, 0, "readdir should not be called for absolute bundle urls"); +}); + +// ####################################################### +// Type: Component +// ####################################################### + +// Currently we have no specific coding for components, should be treated the same way as type application + +// ####################################################### +// Type: Card +// ####################################################### + +// Currently we have no specific coding for cards, should be treated the same way as type application + +// ####################################################### +// Type: Library +// ####################################################### + +test("Library: No replacement at all", async (t) => { + const {manifestEnhancer, fs, createResource} = t.context; + const input = JSON.stringify({ + "_version": "1.58.0", + "sap.app": { + "id": "sap.ui.demo.lib", + "type": "library" + } + }, null, 2); + + const resource = createResource("/resources/sap/ui/demo/lib/manifest.json", true, input); + + fs.readdir.withArgs("/resources/sap/ui/demo/app/i18n") + .callsArgWith(1, null, ["i18n_de.properties", "i18n_en.properties"]); + + const processedResources = await manifestEnhancer({ + resources: [resource], + fs + }); + + t.deepEqual(processedResources, [], "Only enhanced resources are returned"); + + t.is(resource.setString.callCount, 0, "setString should not be called"); + + t.true(t.context.logVerboseSpy.notCalled, "No verbose messages should be logged"); + t.true(t.context.logWarnSpy.notCalled, "No warnings should be logged"); + t.true(t.context.logErrorSpy.notCalled, "No errors should be logged"); +}); + +test("Library: sap.app/i18n (with templates, no bundle defined): " + + "Does not add supportedLocales, as sap.app/i18n is not valid for libraries", +async (t) => { + const {manifestEnhancer, fs, createResource} = t.context; + const input = JSON.stringify({ + "_version": "1.58.0", + "sap.app": { + "id": "sap.ui.demo.lib", + "type": "library", + "title": "{{title}}" + } + }, null, 2); + + const resource = createResource("/resources/sap/ui/demo/lib/manifest.json", true, input); + + fs.readdir.withArgs("/resources/sap/ui/demo/app/i18n") + .callsArgWith(1, null, ["i18n_de.properties", "i18n_en.properties"]); + + const processedResources = await manifestEnhancer({ + resources: [resource], + fs + }); + + t.deepEqual(processedResources, [], "Only enhanced resources are returned"); + + t.is(resource.setString.callCount, 0, "setString should not be called"); + + t.true(t.context.logVerboseSpy.notCalled, "No verbose messages should be logged"); + t.true(t.context.logWarnSpy.notCalled, "No warnings should be logged"); + t.true(t.context.logErrorSpy.notCalled, "No errors should be logged"); +}); + +test("Library: sap.app/i18n (with custom bundle): " + + "Does not add supportedLocales, as sap.app/i18n is not valid for libraries", +async (t) => { + const {manifestEnhancer, fs, createResource} = t.context; + const input = JSON.stringify({ + "_version": "1.58.0", + "sap.app": { + "id": "sap.ui.demo.lib", + "type": "library", + "i18n": "mybundle.properties" + } + }, null, 2); + + const resource = createResource("/resources/sap/ui/demo/lib/manifest.json", true, input, + (actual) => t.is(actual, "", "Correct file content should be set")); + + fs.readdir.withArgs("/resources/sap/ui/demo/app/i18n") + .callsArgWith(1, null, ["mybundle_de.properties", "mybundle_en.properties"]); + + const processedResources = await manifestEnhancer({ + resources: [resource], + fs + }); + + t.deepEqual(processedResources, [], "Only enhanced resources are returned"); + + t.is(resource.setString.callCount, 0, "setString should not be called"); + + t.true(t.context.logVerboseSpy.notCalled, "No verbose messages should be logged"); + t.true(t.context.logWarnSpy.notCalled, "No warnings should be logged"); + t.true(t.context.logErrorSpy.notCalled, "No errors should be logged"); +}); + +test("Library: sap.ui5/library: Adds supportedLocales based on available properties files", async (t) => { + const {manifestEnhancer, fs, createResource} = t.context; + const input = JSON.stringify({ + "_version": "1.58.0", + "sap.app": { + "id": "sap.ui.demo.lib", + "type": "library" + }, + "sap.ui5": { + "library": { + "i18n": { + "bundleUrl": "i18nc/messagebundlec.properties", + "fallbackLocale": "de" + } + } + } + }, null, 2); + + const expected = JSON.stringify({ + "_version": "1.58.0", + "sap.app": { + "id": "sap.ui.demo.lib", + "type": "library" + }, + "sap.ui5": { + "library": { + "i18n": { + "bundleUrl": "i18nc/messagebundlec.properties", + "fallbackLocale": "de", + "supportedLocales": ["de", "en"] + } + } + } + }, null, 2); + + const resource = createResource("/resources/sap/ui/demo/lib/manifest.json", true, input); + + fs.readdir.withArgs("/resources/sap/ui/demo/lib/i18nc") + .callsArgWith(1, null, ["messagebundlec_de.properties", "messagebundlec_en.properties"]); + + const processedResources = await manifestEnhancer({ + resources: [resource], + fs + }); + + t.deepEqual(processedResources, [resource], "Input resource is returned"); + + t.is(resource.setString.callCount, 1, "setString should be called once"); + t.deepEqual(resource.setString.getCall(0).args, [expected], "Correct file content should be set"); + + t.true(t.context.logVerboseSpy.notCalled, "No verbose messages should be logged"); + t.true(t.context.logWarnSpy.notCalled, "No warnings should be logged"); + t.true(t.context.logErrorSpy.notCalled, "No errors should be logged"); +}); + +test("Library: sap.ui5/library: Adds supportedLocales based on available properties files (i18n=string)", async (t) => { + const {manifestEnhancer, fs, createResource} = t.context; + const input = JSON.stringify({ + "_version": "1.58.0", + "sap.app": { + "id": "sap.ui.demo.lib", + "type": "library" + }, + "sap.ui5": { + "library": { + "i18n": "i18nc/messagebundlec.properties" + } + } + }, null, 2); + + const expected = JSON.stringify({ + "_version": "1.58.0", + "sap.app": { + "id": "sap.ui.demo.lib", + "type": "library" + }, + "sap.ui5": { + "library": { + "i18n": { + "bundleUrl": "i18nc/messagebundlec.properties", + "supportedLocales": ["de", "en"] + } + } + } + }, null, 2); + + const resource = createResource("/resources/sap/ui/demo/lib/manifest.json", true, input); + + fs.readdir.withArgs("/resources/sap/ui/demo/lib/i18nc") + .callsArgWith(1, null, ["messagebundlec_de.properties", "messagebundlec_en.properties"]); + + const processedResources = await manifestEnhancer({ + resources: [resource], + fs + }); + + t.deepEqual(processedResources, [resource], "Input resource is returned"); + + t.is(resource.setString.callCount, 1, "setString should be called once"); + t.deepEqual(resource.setString.getCall(0).args, [expected], "Correct file content should be set"); + + t.true(t.context.logVerboseSpy.notCalled, "No verbose messages should be logged"); + t.true(t.context.logWarnSpy.notCalled, "No warnings should be logged"); + t.true(t.context.logErrorSpy.notCalled, "No errors should be logged"); +}); + +test("Library: sap.ui5/library: " + + "Adds supportedLocales based on available properties files (i18n=true)", +async (t) => { + const {manifestEnhancer, fs, createResource} = t.context; + const input = JSON.stringify({ + "_version": "1.58.0", + "sap.app": { + "id": "sap.ui.demo.lib", + "type": "library" + }, + "sap.ui5": { + "library": { + "i18n": true + } + } + }, null, 2); + + const expected = JSON.stringify({ + "_version": "1.58.0", + "sap.app": { + "id": "sap.ui.demo.lib", + "type": "library" + }, + "sap.ui5": { + "library": { + "i18n": { + "bundleUrl": "messagebundle.properties", + "supportedLocales": ["de", "en"] + } + } + } + }, null, 2); + + const resource = createResource("/resources/sap/ui/demo/lib/manifest.json", true, input); + + fs.readdir.withArgs("/resources/sap/ui/demo/lib") + .callsArgWith(1, null, ["messagebundle_de.properties", "messagebundle_en.properties"]); + + const processedResources = await manifestEnhancer({ + resources: [resource], + fs + }); + + t.deepEqual(processedResources, [resource], "Input resource is returned"); + + t.is(resource.setString.callCount, 1, "setString should be called once"); + t.deepEqual(resource.setString.getCall(0).args, [expected], "Correct file content should be set"); + + t.true(t.context.logVerboseSpy.notCalled, "No verbose messages should be logged"); + t.true(t.context.logWarnSpy.notCalled, "No warnings should be logged"); + t.true(t.context.logErrorSpy.notCalled, "No errors should be logged"); +}); + +test("Library: sap.ui5/library: Do not generate supportedLocales with disabled i18n feature", async (t) => { + const {manifestEnhancer, fs, createResource} = t.context; + const input = JSON.stringify({ + "_version": "1.58.0", + "sap.app": { + "id": "sap.ui.demo.lib", + "type": "library" + }, + "sap.ui5": { + "library": { + "i18n": false + } + } + }, null, 2); + + const resource = createResource("/resources/sap/ui/demo/lib/manifest.json", true, input); + + fs.readdir.withArgs("/resources/sap/ui/demo/lib") + .callsArgWith(1, null, ["messagebundle_de.properties", "messagebundle_en.properties"]); + + const processedResources = await manifestEnhancer({ + resources: [resource], + fs + }); + + t.deepEqual(processedResources, [], "Only enhanced resources are returned"); + + t.is(resource.setString.callCount, 0, "setString should not be called"); + + t.true(t.context.logVerboseSpy.notCalled, "No verbose messages should be logged"); + t.true(t.context.logWarnSpy.notCalled, "No warnings should be logged"); + t.true(t.context.logErrorSpy.notCalled, "No errors should be logged"); +}); + +test("Library: sap.ui5/library: Adds supportedLocales to terminologies", async (t) => { + const {manifestEnhancer, fs, createResource} = t.context; + const input = JSON.stringify({ + "_version": "1.58.0", + "sap.app": { + "id": "sap.ui.demo.lib", + "type": "library" + }, + "sap.ui5": { + "library": { + "i18n": { + "bundleUrl": "i18nc/messagebundlec.properties", + "terminologies": { + "sports": { + "bundleUrl": "i18nc_sports/messagebundle.sports.properties" + } + } + } + } + } + }, null, 2); + + const expected = JSON.stringify({ + "_version": "1.58.0", + "sap.app": { + "id": "sap.ui.demo.lib", + "type": "library" + }, + "sap.ui5": { + "library": { + "i18n": { + "bundleUrl": "i18nc/messagebundlec.properties", + "terminologies": { + "sports": { + "bundleUrl": "i18nc_sports/messagebundle.sports.properties", + "supportedLocales": ["", "de", "en"] + } + }, + "supportedLocales": ["", "de", "en"], + } + } + } + }, null, 2); + + const resource = createResource("/resources/sap/ui/demo/lib/manifest.json", true, input); + + fs.readdir.withArgs("/resources/sap/ui/demo/lib/i18nc") + .callsArgWith(1, null, [ + "messagebundlec_de.properties", + "messagebundlec_en.properties", + "messagebundlec.properties" + ]); + + fs.readdir.withArgs("/resources/sap/ui/demo/lib/i18nc_sports") + .callsArgWith(1, null, [ + "messagebundle.sports_de.properties", + "messagebundle.sports_en.properties", + "messagebundle.sports.properties" + ]); + + const processedResources = await manifestEnhancer({ + resources: [resource], + fs + }); + + t.deepEqual(processedResources, [resource], "Input resource is returned"); + + t.is(resource.setString.callCount, 1, "setString should be called once"); + t.deepEqual(resource.setString.getCall(0).args, [expected], "Correct file content should be set"); + + t.true(t.context.logVerboseSpy.notCalled, "No verbose messages should be logged"); + t.true(t.context.logWarnSpy.notCalled, "No warnings should be logged"); + t.true(t.context.logErrorSpy.notCalled, "No errors should be logged"); +}); + +test("Library: sap.ui5/library: Adds supportedLocales for terminologies not bundle level", async (t) => { + const {manifestEnhancer, fs, createResource} = t.context; + const input = JSON.stringify({ + "_version": "1.58.0", + "sap.app": { + "id": "sap.ui.demo.lib", + "type": "library" + }, + "sap.ui5": { + "library": { + "i18n": { + "bundleUrl": "i18nc/messagebundlec.properties", + "supportedLocales": ["pt"], + "terminologies": { + "sports": { + "bundleUrl": "i18nc_sports/messagebundle.sports.properties" + } + } + } + } + } + }, null, 2); + + const expected = JSON.stringify({ + "_version": "1.58.0", + "sap.app": { + "id": "sap.ui.demo.lib", + "type": "library" + }, + "sap.ui5": { + "library": { + "i18n": { + "bundleUrl": "i18nc/messagebundlec.properties", + "supportedLocales": ["pt"], + "terminologies": { + "sports": { + "bundleUrl": "i18nc_sports/messagebundle.sports.properties", + "supportedLocales": ["", "de", "en"] + } + } + } + } + } + }, null, 2); + + const resource = createResource("/resources/sap/ui/demo/lib/manifest.json", true, input); + + fs.readdir.withArgs("/resources/sap/ui/demo/lib/i18nc") + .callsArgWith(1, null, [ + "messagebundlec_de.properties", + "messagebundlec_en.properties", + "messagebundlec.properties" + ]); + + fs.readdir.withArgs("/resources/sap/ui/demo/lib/i18nc_sports") + .callsArgWith(1, null, [ + "messagebundle.sports_de.properties", + "messagebundle.sports_en.properties", + "messagebundle.sports.properties" + ]); + + const processedResources = await manifestEnhancer({ + resources: [resource], + fs + }); + + t.deepEqual(processedResources, [resource], "Input resource is returned"); + + t.is(resource.setString.callCount, 1, "setString should be called once"); + t.deepEqual(resource.setString.getCall(0).args, [expected], "Correct file content should be set"); + + t.true(t.context.logVerboseSpy.notCalled, "No verbose messages should be logged"); + t.true(t.context.logWarnSpy.notCalled, "No warnings should be logged"); + t.true(t.context.logErrorSpy.notCalled, "No errors should be logged"); +}); + +test("Library: sap.ui5/library: Adds supportedLocales (with deactivated terminologies)", async (t) => { + const {manifestEnhancer, fs, createResource} = t.context; + const input = JSON.stringify({ + "_version": "1.58.0", + "sap.app": { + "id": "sap.ui.demo.lib", + "type": "library" + }, + "sap.ui5": { + "library": { + "i18n": { + "bundleUrl": "i18nc/messagebundlec.properties", + "terminologies": { + "sports": { + "bundleUrl": "i18nc_sports/messagebundle.sports.properties", + "supportedLocales": ["pt"] + } + } + } + } + } + }, null, 2); + + const expected = JSON.stringify({ + "_version": "1.58.0", + "sap.app": { + "id": "sap.ui.demo.lib", + "type": "library" + }, + "sap.ui5": { + "library": { + "i18n": { + "bundleUrl": "i18nc/messagebundlec.properties", + "terminologies": { + "sports": { + "bundleUrl": "i18nc_sports/messagebundle.sports.properties", + "supportedLocales": ["pt"] + } + }, + "supportedLocales": ["", "de", "en"], + } + } + } + }, null, 2); + + const resource = createResource("/resources/sap/ui/demo/lib/manifest.json", true, input); + + fs.readdir.withArgs("/resources/sap/ui/demo/lib/i18nc") + .callsArgWith(1, null, [ + "messagebundlec_de.properties", + "messagebundlec_en.properties", + "messagebundlec.properties" + ]); + + fs.readdir.withArgs("/resources/sap/ui/demo/lib/i18nc_sports") + .callsArgWith(1, null, [ + "messagebundle.sports_de.properties", + "messagebundle.sports_en.properties", + "messagebundle.sports.properties" + ]); + + const processedResources = await manifestEnhancer({ + resources: [resource], + fs + }); + + t.deepEqual(processedResources, [resource], "Input resource is returned"); + + t.is(resource.setString.callCount, 1, "setString should be called once"); + t.deepEqual(resource.setString.getCall(0).args, [expected], "Correct file content should be set"); + + t.true(t.context.logVerboseSpy.notCalled, "No verbose messages should be logged"); + t.true(t.context.logWarnSpy.notCalled, "No warnings should be logged"); + t.true(t.context.logErrorSpy.notCalled, "No errors should be logged"); +}); + +test("Library: sap.ui5/library: Adds supportedLocales (with enhanceWith)", async (t) => { + const {manifestEnhancer, fs, createResource} = t.context; + const input = JSON.stringify({ + "_version": "1.58.0", + "sap.app": { + "id": "sap.ui.demo.lib", + "type": "library" + }, + "sap.ui5": { + "library": { + "i18n": { + "bundleUrl": "i18nc/messagebundlec.properties", + "enhanceWith": [ + { + "bundleUrl": "myfolder1/messagebundlenc1.properties" + }, + { + "bundleUrl": "myfolder2/messagebundlenc2.properties" + } + ] + } + } + } + }, null, 2); + + const expected = JSON.stringify({ + "_version": "1.58.0", + "sap.app": { + "id": "sap.ui.demo.lib", + "type": "library" + }, + "sap.ui5": { + "library": { + "i18n": { + "bundleUrl": "i18nc/messagebundlec.properties", + "enhanceWith": [ + { + "bundleUrl": "myfolder1/messagebundlenc1.properties", + "supportedLocales": ["", "de", "en"] + }, + { + "bundleUrl": "myfolder2/messagebundlenc2.properties", + "supportedLocales": ["", "de", "en"] + } + ], + "supportedLocales": ["", "de", "en"], + } + } + } + }, null, 2); + + const resource = createResource("/resources/sap/ui/demo/lib/manifest.json", true, input); + + fs.readdir.withArgs("/resources/sap/ui/demo/lib/i18nc") + .callsArgWith(1, null, [ + "messagebundlec_de.properties", + "messagebundlec_en.properties", + "messagebundlec.properties" + ]); + + fs.readdir.withArgs("/resources/sap/ui/demo/lib/myfolder1") + .callsArgWith(1, null, [ + "messagebundlenc1_de.properties", + "messagebundlenc1_en.properties", + "messagebundlenc1.properties" + ]); + + fs.readdir.withArgs("/resources/sap/ui/demo/lib/myfolder2") + .callsArgWith(1, null, [ + "messagebundlenc2_de.properties", + "messagebundlenc2_en.properties", + "messagebundlenc2.properties" + ]); + + const processedResources = await manifestEnhancer({ + resources: [resource], + fs + }); + + t.deepEqual(processedResources, [resource], "Input resource is returned"); + + t.is(resource.setString.callCount, 1, "setString should be called once"); + t.deepEqual(resource.setString.getCall(0).args, [expected], "Correct file content should be set"); + + t.true(t.context.logVerboseSpy.notCalled, "No verbose messages should be logged"); + t.true(t.context.logWarnSpy.notCalled, "No warnings should be logged"); + t.true(t.context.logErrorSpy.notCalled, "No errors should be logged"); +}); + +test("Library: sap.ui5/library: Adds supportedLocales (with enhanceWith and terminologies)", async (t) => { + const {manifestEnhancer, fs, createResource} = t.context; + const input = JSON.stringify({ + "_version": "1.58.0", + "sap.app": { + "id": "sap.ui.demo.lib", + "type": "library" + }, + "sap.ui5": { + "library": { + "i18n": { + "bundleUrl": "i18nc/messagebundlec.properties", + "terminologies": { + "sports": { + "bundleUrl": "i18nc_sports/messagebundle.sports.properties" + } + }, + "enhanceWith": [ + { + "bundleUrl": "myfolder1/messagebundlenc1.properties", + "terminologies": { + "sports": { + "bundleUrl": "i18nc_sports_soccer/messagebundle.soccer.properties" + } + } + }, + { + "bundleUrl": "myfolder2/messagebundlenc2.properties", + "terminologies": { + "sports": { + "bundleUrl": "i18nc_sports_soccer_el/messagebundle.elsoccer.properties" + } + } + } + ] + } + } + } + }, null, 2); + + const expected = JSON.stringify({ + "_version": "1.58.0", + "sap.app": { + "id": "sap.ui.demo.lib", + "type": "library" + }, + "sap.ui5": { + "library": { + "i18n": { + "bundleUrl": "i18nc/messagebundlec.properties", + "terminologies": { + "sports": { + "bundleUrl": "i18nc_sports/messagebundle.sports.properties", + "supportedLocales": ["", "de", "en"] + } + }, + "enhanceWith": [ + { + "bundleUrl": "myfolder1/messagebundlenc1.properties", + "terminologies": { + "sports": { + "bundleUrl": "i18nc_sports_soccer/messagebundle.soccer.properties", + "supportedLocales": ["", "de", "en"] + } + }, + "supportedLocales": ["", "de", "en"] + }, + { + "bundleUrl": "myfolder2/messagebundlenc2.properties", + "terminologies": { + "sports": { + "bundleUrl": "i18nc_sports_soccer_el/messagebundle.elsoccer.properties", + "supportedLocales": ["", "de", "en"] + } + }, + "supportedLocales": ["", "de", "en"] + } + ], + "supportedLocales": ["", "de", "en"], + } + } + } + }, null, 2); + + const resource = createResource("/resources/sap/ui/demo/lib/manifest.json", true, input); + + fs.readdir.withArgs("/resources/sap/ui/demo/lib/myfolder1") + .callsArgWith(1, null, [ + "messagebundlenc1_de.properties", + "messagebundlenc1_en.properties", + "messagebundlenc1.properties" + ]); + + fs.readdir.withArgs("/resources/sap/ui/demo/lib/myfolder2") + .callsArgWith(1, null, [ + "messagebundlenc2_de.properties", + "messagebundlenc2_en.properties", + "messagebundlenc2.properties" + ]); + + fs.readdir.withArgs("/resources/sap/ui/demo/lib/i18nc_sports") + .callsArgWith(1, null, [ + "messagebundle.sports_de.properties", + "messagebundle.sports_en.properties", + "messagebundle.sports.properties" + ]); + + fs.readdir.withArgs("/resources/sap/ui/demo/lib/i18nc_sports_soccer") + .callsArgWith(1, null, [ + "messagebundle.soccer_de.properties", + "messagebundle.soccer_en.properties", + "messagebundle.soccer.properties" + ]); + + fs.readdir.withArgs("/resources/sap/ui/demo/lib/i18nc_sports_soccer_el") + .callsArgWith(1, null, [ + "messagebundle.elsoccer_de.properties", + "messagebundle.elsoccer_en.properties", + "messagebundle.elsoccer.properties" + ]); + + fs.readdir.withArgs("/resources/sap/ui/demo/lib/i18nc") + .callsArgWith(1, null, [ + "messagebundlec_de.properties", + "messagebundlec_en.properties", + "messagebundlec.properties" + ]); + + const processedResources = await manifestEnhancer({ + resources: [resource], + fs + }); + + t.deepEqual(processedResources, [resource], "Input resource is returned"); + + t.is(resource.setString.callCount, 1, "setString should be called once"); + t.deepEqual(resource.setString.getCall(0).args, [expected], "Correct file content should be set"); + + t.true(t.context.logVerboseSpy.notCalled, "No verbose messages should be logged"); + t.true(t.context.logWarnSpy.notCalled, "No warnings should be logged"); + t.true(t.context.logErrorSpy.notCalled, "No errors should be logged"); +}); + +test("Library: sap.ui5/library: Ignores fallbackLocale for terminologies", async (t) => { + const {manifestEnhancer, fs, createResource} = t.context; + const input = JSON.stringify({ + "_version": "1.58.0", + "sap.app": { + "id": "sap.ui.demo.lib", + "type": "library" + }, + "sap.ui5": { + "library": { + "i18n": { + "bundleUrl": "i18nc/messagebundlec.properties", + "terminologies": { + "sports": { + "bundleUrl": "i18nc_sports/messagebundle.sports.properties", + // Note: manifest.json schema does not allow "fallbackLocale" for terminologies + // UI5 runtime and tooling should ignore this property + "fallbackLocale": "es" + } + }, + "enhanceWith": [ + { + "bundleUrl": "myfolder1/messagebundlenc1.properties", + "terminologies": { + "sports": { + "bundleUrl": "i18nc_sports_soccer/messagebundle.soccer.properties", + // Note: manifest.json schema does not allow "fallbackLocale" for terminologies + // UI5 runtime and tooling should ignore this property + "fallbackLocale": "es" + } + } + } + ] + } + } + } + }, null, 2); + + const expected = JSON.stringify({ + "_version": "1.58.0", + "sap.app": { + "id": "sap.ui.demo.lib", + "type": "library" + }, + "sap.ui5": { + "library": { + "i18n": { + "bundleUrl": "i18nc/messagebundlec.properties", + "terminologies": { + "sports": { + "bundleUrl": "i18nc_sports/messagebundle.sports.properties", + // Note: manifest.json schema does not allow "fallbackLocale" for terminologies + // UI5 runtime and tooling should ignore this property + "fallbackLocale": "es", + "supportedLocales": ["", "de", "en"] + } + }, + "enhanceWith": [ + { + "bundleUrl": "myfolder1/messagebundlenc1.properties", + "terminologies": { + "sports": { + "bundleUrl": "i18nc_sports_soccer/messagebundle.soccer.properties", + // Note: manifest.json schema does not allow "fallbackLocale" for terminologies + // UI5 runtime and tooling should ignore this property + "fallbackLocale": "es", + "supportedLocales": ["", "de", "en"] + } + }, + "supportedLocales": ["", "de", "en"] + } + ], + "supportedLocales": ["", "de", "en"], + } + } + } + }, null, 2); + + const resource = createResource("/resources/sap/ui/demo/lib/manifest.json", true, input); + + fs.readdir.withArgs("/resources/sap/ui/demo/lib/myfolder1") + .callsArgWith(1, null, [ + "messagebundlenc1_de.properties", + "messagebundlenc1_en.properties", + "messagebundlenc1.properties" + ]); + + fs.readdir.withArgs("/resources/sap/ui/demo/lib/i18nc_sports") + .callsArgWith(1, null, [ + "messagebundle.sports_de.properties", + "messagebundle.sports_en.properties", + "messagebundle.sports.properties" + ]); + + fs.readdir.withArgs("/resources/sap/ui/demo/lib/i18nc_sports_soccer") + .callsArgWith(1, null, [ + "messagebundle.soccer_de.properties", + "messagebundle.soccer_en.properties", + "messagebundle.soccer.properties" + ]); + + fs.readdir.withArgs("/resources/sap/ui/demo/lib/i18nc") + .callsArgWith(1, null, [ + "messagebundlec_de.properties", + "messagebundlec_en.properties", + "messagebundlec.properties" + ]); + + const processedResources = await manifestEnhancer({ + resources: [resource], + fs + }); + + t.deepEqual(processedResources, [resource], "Input resource is returned"); + + t.is(resource.setString.callCount, 1, "setString should be called once"); + t.deepEqual(resource.setString.getCall(0).args, [expected], "Correct file content should be set"); + + t.true(t.context.logVerboseSpy.notCalled, "No verbose messages should be logged"); + t.true(t.context.logWarnSpy.notCalled, "No warnings should be logged"); + t.true(t.context.logErrorSpy.notCalled, "No errors should be logged"); +}); + +test("Library: sap.ui5/library: " + +"Does not not add supportedLocales for enhanceWith when bundle has supportedLocales defined", async (t) => { + const {manifestEnhancer, fs, createResource} = t.context; + const input = JSON.stringify({ + "_version": "1.58.0", + "sap.app": { + "id": "sap.ui.demo.lib", + "type": "library" + }, + "sap.ui5": { + "library": { + "i18n": { + "bundleUrl": "i18nc/messagebundlec.properties", + "terminologies": { + "sports": { + "bundleUrl": "i18nc_sports/messagebundle.sports.properties" + } + }, + "enhanceWith": [ + { + "bundleUrl": "myfolder1/messagebundlenc1.properties", + "terminologies": { + "sports": { + "bundleUrl": "i18nc_sports_soccer/messagebundle.soccer.properties" + } + } + }, + { + "bundleUrl": "myfolder2/messagebundlenc2.properties", + "terminologies": { + "sports": { + "bundleUrl": "i18nc_sports_soccer_el/messagebundle.elsoccer.properties" + } + } + } + ], + "supportedLocales": ["en"] + } + } + } + }, null, 2); + + const expected = JSON.stringify({ + "_version": "1.58.0", + "sap.app": { + "id": "sap.ui.demo.lib", + "type": "library" + }, + "sap.ui5": { + "library": { + "i18n": { + "bundleUrl": "i18nc/messagebundlec.properties", + "terminologies": { + "sports": { + "bundleUrl": "i18nc_sports/messagebundle.sports.properties", + "supportedLocales": ["", "de", "en"] + } + }, + "enhanceWith": [ + { + "bundleUrl": "myfolder1/messagebundlenc1.properties", + "terminologies": { + "sports": { + "bundleUrl": "i18nc_sports_soccer/messagebundle.soccer.properties", + "supportedLocales": ["", "de", "en"] + } + } + }, + { + "bundleUrl": "myfolder2/messagebundlenc2.properties", + "terminologies": { + "sports": { + "bundleUrl": "i18nc_sports_soccer_el/messagebundle.elsoccer.properties", + "supportedLocales": ["", "de", "en"] + } + } + } + ], + "supportedLocales": ["en"] + } + } + } + }, null, 2); + + const resource = createResource("/resources/sap/ui/demo/lib/manifest.json", true, input); + + fs.readdir.withArgs("/resources/sap/ui/demo/lib/myfolder1") + .callsArgWith(1, null, [ + "messagebundlenc1_de.properties", + "messagebundlenc1_en.properties", + "messagebundlenc1.properties" + ]); + + fs.readdir.withArgs("/resources/sap/ui/demo/lib/myfolder2") + .callsArgWith(1, null, [ + "messagebundlenc2_de.properties", + "messagebundlenc2_en.properties", + "messagebundlenc2.properties" + ]); + + fs.readdir.withArgs("/resources/sap/ui/demo/lib/i18nc_sports") + .callsArgWith(1, null, [ + "messagebundle.sports_de.properties", + "messagebundle.sports_en.properties", + "messagebundle.sports.properties" + ]); + + fs.readdir.withArgs("/resources/sap/ui/demo/lib/i18nc_sports_soccer") + .callsArgWith(1, null, [ + "messagebundle.soccer_de.properties", + "messagebundle.soccer_en.properties", + "messagebundle.soccer.properties" + ]); + + fs.readdir.withArgs("/resources/sap/ui/demo/lib/i18nc_sports_soccer_el") + .callsArgWith(1, null, [ + "messagebundle.elsoccer_de.properties", + "messagebundle.elsoccer_en.properties", + "messagebundle.elsoccer.properties" + ]); + + fs.readdir.withArgs("/resources/sap/ui/demo/lib/i18nc") + .callsArgWith(1, null, [ + "messagebundlec_de.properties", + "messagebundlec_en.properties", + "messagebundlec.properties" + ]); + + const processedResources = await manifestEnhancer({ + resources: [resource], + fs + }); + + t.deepEqual(processedResources, [resource], "Input resource is returned"); + + t.is(resource.setString.callCount, 1, "setString should be called once"); + t.deepEqual(resource.setString.getCall(0).args, [expected], "Correct file content should be set"); + + t.true(t.context.logVerboseSpy.notCalled, "No verbose messages should be logged"); + t.true(t.context.logWarnSpy.notCalled, "No warnings should be logged"); + t.true(t.context.logErrorSpy.notCalled, "No errors should be logged"); + + t.is(fs.readdir.withArgs("/resources/sap/ui/demo/lib/myfolder1").callCount, 0, + "folder should not be read as parent bundle defines supportedLocales"); + t.is(fs.readdir.withArgs("/resources/sap/ui/demo/lib/myfolder2").callCount, 0, + "folder should not be read as parent bundle defines supportedLocales"); + t.is(fs.readdir.withArgs("/resources/sap/ui/demo/lib/i18nc").callCount, 0, + "folder should not be read as bundle defines supportedLocales"); +}); + +test("Library: sap.ui5/library: " + +"Does not not add supportedLocales for enhanceWith when bundle has invalid fallbackLocale defined", async (t) => { + const {manifestEnhancer, fs, createResource} = t.context; + const input = JSON.stringify({ + "_version": "1.58.0", + "sap.app": { + "id": "sap.ui.demo.lib", + "type": "library" + }, + "sap.ui5": { + "library": { + "i18n": { + "bundleUrl": "i18nc/messagebundlec.properties", + "terminologies": { + "sports": { + "bundleUrl": "i18nc_sports/messagebundle.sports.properties" + } + }, + "enhanceWith": [ + { + "bundleUrl": "myfolder1/messagebundlenc1.properties", + "terminologies": { + "sports": { + "bundleUrl": "i18nc_sports_soccer/messagebundle.soccer.properties" + } + } + }, + { + "bundleUrl": "myfolder2/messagebundlenc2.properties", + "terminologies": { + "sports": { + "bundleUrl": "i18nc_sports_soccer_el/messagebundle.elsoccer.properties" + } + } + } + ], + "fallbackLocale": "es" + } + } + } + }, null, 2); + + const expected = JSON.stringify({ + "_version": "1.58.0", + "sap.app": { + "id": "sap.ui.demo.lib", + "type": "library" + }, + "sap.ui5": { + "library": { + "i18n": { + "bundleUrl": "i18nc/messagebundlec.properties", + "terminologies": { + "sports": { + "bundleUrl": "i18nc_sports/messagebundle.sports.properties", + "supportedLocales": ["", "de", "en"] + } + }, + "enhanceWith": [ + { + "bundleUrl": "myfolder1/messagebundlenc1.properties", + "terminologies": { + "sports": { + "bundleUrl": "i18nc_sports_soccer/messagebundle.soccer.properties", + "supportedLocales": ["", "de", "en"] + } + }, + "supportedLocales": ["", "de", "en", "es"] + }, + { + "bundleUrl": "myfolder2/messagebundlenc2.properties", + "terminologies": { + "sports": { + "bundleUrl": "i18nc_sports_soccer_el/messagebundle.elsoccer.properties", + "supportedLocales": ["", "de", "en"] + } + } + } + ], + "fallbackLocale": "es", + "supportedLocales": ["", "de", "en", "es"] + } + } + } + }, null, 2); + + const resource = createResource("/resources/sap/ui/demo/lib/manifest.json", true, input); + + fs.readdir.withArgs("/resources/sap/ui/demo/lib/myfolder1") + .callsArgWith(1, null, [ + "messagebundlenc1_de.properties", + "messagebundlenc1_en.properties", + "messagebundlenc1_es.properties", + "messagebundlenc1.properties" + ]); + + fs.readdir.withArgs("/resources/sap/ui/demo/lib/myfolder2") + .callsArgWith(1, null, [ + "messagebundlenc2_de.properties", + "messagebundlenc2_en.properties", + "messagebundlenc2.properties" + ]); + + fs.readdir.withArgs("/resources/sap/ui/demo/lib/i18nc_sports") + .callsArgWith(1, null, [ + "messagebundle.sports_de.properties", + "messagebundle.sports_en.properties", + "messagebundle.sports.properties" + ]); + + fs.readdir.withArgs("/resources/sap/ui/demo/lib/i18nc_sports_soccer") + .callsArgWith(1, null, [ + "messagebundle.soccer_de.properties", + "messagebundle.soccer_en.properties", + "messagebundle.soccer.properties" + ]); + + fs.readdir.withArgs("/resources/sap/ui/demo/lib/i18nc_sports_soccer_el") + .callsArgWith(1, null, [ + "messagebundle.elsoccer_de.properties", + "messagebundle.elsoccer_en.properties", + "messagebundle.elsoccer.properties" + ]); + + fs.readdir.withArgs("/resources/sap/ui/demo/lib/i18nc") + .callsArgWith(1, null, [ + "messagebundlec_de.properties", + "messagebundlec_en.properties", + "messagebundlec_es.properties", + "messagebundlec.properties" + ]); + + const processedResources = await manifestEnhancer({ + resources: [resource], + fs + }); + + t.deepEqual(processedResources, [resource], "Input resource is returned"); + + t.is(resource.setString.callCount, 1, "setString should be called once"); + t.deepEqual(resource.setString.getCall(0).args, [expected], "Correct file content should be set"); + + t.true(t.context.logVerboseSpy.notCalled, "No verbose messages should be logged"); + t.true(t.context.logWarnSpy.notCalled, "No warnings should be logged"); + t.is(t.context.logErrorSpy.callCount, 1, "One error should be logged"); + t.deepEqual(t.context.logErrorSpy.getCall(0).args, [ + "/resources/sap/ui/demo/lib/manifest.json: Generated supported locales ('', 'de', 'en') for bundle " + + "'myfolder2/messagebundlenc2.properties' not containing the " + + "defined fallback locale 'es'. Either provide a properties file for defined fallbackLocale " + + "or configure another available fallbackLocale"]); +}); + +test("fs.readdir error handling", async (t) => { + const {manifestEnhancer, fs, createResource} = t.context; + const input = JSON.stringify({ + "_version": "1.58.0", + "sap.app": { + "id": "sap.ui.demo.app", + "type": "application", + "title": "{{title}}" + } + }, null, 2); + + const resource = createResource("/resources/sap/ui/demo/app/manifest.json", true, input); + + // NOTE: @ui5/fs fsInterface currently does not throw ENOENT errors but instead returns an empty array + // However, this is not guaranteed and might change in the future. + // In addition, the might be low-level use cases with a real "fs" that would throw ENOENT + const error = new Error("ENOENT: no such file or directory, scandir '/resources/sap/ui/demo/app/i18n'"); + error.code = "ENOENT"; + fs.readdir.withArgs("/resources/sap/ui/demo/app/i18n") + .callsArgWith(1, error); + + const processedResources = await manifestEnhancer({ + resources: [resource], + fs + }); + + t.deepEqual(processedResources, [], "Only enhanced resources are returned"); + + t.is(resource.setString.callCount, 0, "setString should not be called"); + + t.true(t.context.logVerboseSpy.notCalled, "No verbose messages should be logged"); + t.true(t.context.logWarnSpy.notCalled, "No warnings should be logged"); + t.true(t.context.logErrorSpy.notCalled, "No errors should be logged"); + + t.is(fs.readdir.withArgs("/resources/sap/ui/demo/app/i18n").callCount, 1, + "readdir has been called with expected path that causes a ENOENT error"); +}); + +test("ManifestEnhancer#run: multiple parallel executions are not supported", async (t) => { + const {fs} = t.context; + const {ManifestEnhancer} = t.context.__internals__; + + const manifest = JSON.stringify({ + "_version": "1.58.0", + "sap.app": { + "id": "sap.ui.demo.app" + } + }); + const filePath = "/manifest.json"; + + const manifestEnhancer = new ManifestEnhancer(manifest, filePath, fs); + + manifestEnhancer.run(); + await t.throwsAsync(manifestEnhancer.run(), { + message: "ManifestEnhancer#run can only be invoked once per instance" + }); +}); + +test("manifestEnhancer#getSupportedLocales", async (t) => { + const {fs} = t.context; + const {ManifestEnhancer} = t.context.__internals__; + + const manifest = JSON.stringify({ + "_version": "1.58.0", + "sap.app": { + "id": "sap.ui.demo.app" + } + }); + const filePath = "/manifest.json"; + + const manifestEnhancer = new ManifestEnhancer(manifest, filePath, fs); + + fs.readdir.withArgs("/i18n") + .callsArgWith(1, null, [ + "i18n.properties", + "i18n_en.properties" + ]); + + t.deepEqual(await manifestEnhancer.getSupportedLocales("./i18n/i18n.properties"), ["", "en"]); + t.deepEqual(await manifestEnhancer.getSupportedLocales("i18n/../i18n/i18n.properties"), ["", "en"]); + t.deepEqual(await manifestEnhancer.getSupportedLocales("ui5://sap/ui/demo/app/i18n/i18n.properties"), ["", "en"]); + + // Path traversal to root and then into application namespace + // This works, but is not recommended at all! It also likely fails at runtime + t.deepEqual(await manifestEnhancer.getSupportedLocales( + "../../../../../../../../../../../../resources/sap/ui/demo/app/i18n/i18n.properties" + ), ["", "en"]); + + t.is(fs.readdir.callCount, 4); +}); + +test("manifestEnhancer#getSupportedLocales (absolute / invalid URLs)", async (t) => { + const {fs} = t.context; + const {ManifestEnhancer} = t.context.__internals__; + + const manifest = JSON.stringify({ + "_version": "1.58.0", + "sap.app": { + "id": "sap.ui.demo.app" + } + }); + const filePath = "/manifest.json"; + + const manifestEnhancer = new ManifestEnhancer(manifest, filePath, fs); + + // Server-absolute URLs + t.deepEqual(await manifestEnhancer.getSupportedLocales("/i18n/i18n.properties"), []); + t.deepEqual(await manifestEnhancer.getSupportedLocales("/../i18n/i18n.properties"), []); + + // Server-absolute URL within application namespace + t.deepEqual(await manifestEnhancer.getSupportedLocales("/resources/sap/ui/demo/app/i18n/i18n.properties"), []); + + // Absolute URLs + t.deepEqual(await manifestEnhancer.getSupportedLocales("http://example.com/i18n.properties"), []); + t.deepEqual(await manifestEnhancer.getSupportedLocales("https://example.com/i18n.properties"), []); + t.deepEqual(await manifestEnhancer.getSupportedLocales("ftp://example.com/i18n.properties"), []); + t.deepEqual(await manifestEnhancer.getSupportedLocales("sftp:i18n.properties"), []); + t.deepEqual(await manifestEnhancer.getSupportedLocales("file://i18n.properties"), []); + + // Path traversal to root + t.deepEqual(await manifestEnhancer.getSupportedLocales("../../../../../../../../../../../../i18n.properties"), []); + + // Relative ui5-protocol URL + t.deepEqual(await manifestEnhancer.getSupportedLocales("ui5:i18n.properties"), []); + + t.is(fs.readdir.callCount, 0, "readdir should not be called for any absolute / invalid URL"); +}); + +test("manifestEnhancer#getSupportedLocales (error handling)", async (t) => { + const {fs} = t.context; + const {ManifestEnhancer} = t.context.__internals__; + + const manifest = JSON.stringify({ + "_version": "1.58.0", + "sap.app": { + "id": "sap.ui.demo.app" + } + }); + const filePath = "/manifest.json"; + + const manifestEnhancer = new ManifestEnhancer(manifest, filePath, fs); + + // NOTE: @ui5/fs fsInterface currently does not throw ENOENT errors but instead returns an empty array + // However, this is not guaranteed and might change in the future. + // In addition, the might be low-level use cases with a real "fs" that would throw ENOENT + const error = new Error("ENOENT: no such file or directory, scandir '/i18n'"); + error.code = "ENOENT"; + fs.readdir.withArgs("/i18n") + .callsArgWith(1, error); + + const unexpectedError = new Error("Unexpected error"); + fs.readdir.withArgs("/i18n-unexpected-error") + .callsArgWith(1, unexpectedError); + + // Error handling ENOENT + t.deepEqual(await manifestEnhancer.getSupportedLocales("i18n/i18n.properties"), []); + + // Unexpected errors should be thrown + await t.throwsAsync(manifestEnhancer.getSupportedLocales("i18n-unexpected-error/i18n.properties"), { + is: unexpectedError + }); + + t.is(fs.readdir.callCount, 2, "readdir should be called once"); +}); + +test("getRelativeBundleUrlFromName", (t) => { + const {getRelativeBundleUrlFromName} = t.context.__internals__; + + const bundleUrl = getRelativeBundleUrlFromName("sap.ui.demo.app.i18n.i18n", "sap.ui.demo.app"); + t.is(bundleUrl, "i18n/i18n.properties"); +}); + +test("normalizeBundleUrl", (t) => { + const {normalizeBundleUrl} = t.context.__internals__; + + t.is( + normalizeBundleUrl("./i18n/i18n.properties", "sap.ui.demo.app"), + "i18n/i18n.properties" + ); + t.is( + normalizeBundleUrl("i18n/i18n.properties", "sap.ui.demo.app"), + "i18n/i18n.properties" + ); + t.is( + normalizeBundleUrl("./i18n/../i18n/i18n.properties", "sap.ui.demo.app"), + "i18n/i18n.properties" + ); + t.is( + normalizeBundleUrl("./i18n/../../other/namespace/i18n.properties", "sap.ui.demo.app"), + "../other/namespace/i18n.properties" + ); +}); + +test("resolveUI5Url", (t) => { + const {resolveUI5Url} = t.context.__internals__; + + t.is( + resolveUI5Url("ui5://sap/ui/demo/app/i18n/i18n.properties", "sap.ui.demo.app"), + "/resources/sap/ui/demo/app/i18n/i18n.properties" + ); +}); diff --git a/test/lib/tasks/enhanceManifest.js b/test/lib/tasks/enhanceManifest.js new file mode 100644 index 000000000..25d307067 --- /dev/null +++ b/test/lib/tasks/enhanceManifest.js @@ -0,0 +1,348 @@ +import test from "ava"; +import sinonGlobal from "sinon"; +import esmock from "esmock"; +import {createAdapter, createResource} from "@ui5/fs/resourceFactory"; + +function createWorkspace() { + return createAdapter({ + virBasePath: "/", + project: { + getName: () => "test.lib", + getVersion: () => "2.0.0", + } + }); +} + +test.beforeEach(async (t) => { + const sinon = t.context.sinon = sinonGlobal.createSandbox(); + + t.context.log = { + verbose: sinon.stub(), + warn: sinon.stub(), + error: sinon.stub() + }; + + t.context.manifestEnhancerStub = sinon.stub(); + t.context.fsInterfaceStub = sinon.stub().returns("fs interface"); + t.context.enhanceManifest = await esmock("../../../lib/tasks/enhanceManifest.js", { + "@ui5/logger": { + getLogger: sinon.stub().withArgs("builder:tasks:enhanceManifest").returns(t.context.log) + }, + "@ui5/fs/fsInterface": t.context.fsInterfaceStub, + "../../../lib/processors/manifestEnhancer": t.context.manifestEnhancerStub, + }); + t.context.workspace = createWorkspace(); +}); + +test.afterEach.always((t) => { + t.context.sinon.restore(); +}); + +test.serial("Transforms single manifest.json resource", async (t) => { + const {enhanceManifest, log} = t.context; + + t.plan(6); + + const resource = createResource({ + path: "/resources/sap/ui/demo/app/manifest.json", + string: `{ +"_version": "1.58.0", +"sap.app": { + "id": "sap.ui.demo.app", + "type": "application", + "title": "{{title}}" +} +`, + project: t.context.workspace._project + }); + + const workspace = { + byGlob: (actualPath) => { + t.is(actualPath, "/resources/sap/ui/demo/app/**/manifest.json", + "Reads all manifest.json files"); + return Promise.resolve([resource]); + }, + write: (actualResource) => { + t.deepEqual(actualResource, resource, + "Expected resource is written back to workspace"); + } + }; + + t.context.manifestEnhancerStub.returns([resource]); + + await enhanceManifest({ + workspace, + options: { + projectNamespace: "sap/ui/demo/app" + } + }); + + t.is(t.context.manifestEnhancerStub.callCount, 1, + "Processor should be called once"); + + t.true(t.context.manifestEnhancerStub.calledWithExactly({ + resources: [resource], + fs: "fs interface" + }), "Processor should be called with expected arguments"); + + t.true(log.warn.notCalled, "No warnings should be logged"); + t.true(log.error.notCalled, "No errors should be logged"); +}); + +test.serial("Transforms all manifest.json resources", async (t) => { + const {enhanceManifest, log} = t.context; + + t.plan(6); + + const resourceLib = createResource({ + path: "/resources/sap/ui/demo/lib/manifest.json", + string: `{ + "_version": "1.58.0", + "sap.app": { + "id": "sap.ui.demo.lib", + "type": "library" + }, + "sap.ui5": { + "library": { + "i18n": { + "bundleUrl": "i18n/i18n.properties" + } + } + } +}`, + project: t.context.workspace._project + }); + + const resourceReuseComp1 = createResource({ + path: "/resources/sap/ui/demo/lib/comp1/manifest.json", + string: `{ + "_version": "1.58.0", + "sap.app": { + "id": "sap.ui.demo.lib", + "type": "component" + }, + "sap.ui5": { + "models": { + "i18n": { + "bundleUrl": "i18n/i18n.properties" + } + } + } +}`, + project: t.context.workspace._project + }); + + const resourceReuseComp2 = createResource({ + path: "/resources/sap/ui/demo/lib/comp2/manifest.json", + string: `{ + "_version": "1.58.0", + "sap.app": { + "id": "sap.ui.demo.lib", + "type": "component" + }, + "sap.ui5": { + "models": { + "i18n": { + "bundleUrl": "i18n/i18n.properties", + "supportedLocales": ["fr", "en"] + } + } + } +}`, + project: t.context.workspace._project + }); + + const workspace = { + byGlob: () => { + return Promise.resolve([resourceLib, resourceReuseComp1, resourceReuseComp2]); + }, + write: (actualResource) => { + const path = actualResource.getPath(); + let expectedResource; + if (path === "/resources/sap/ui/demo/lib/manifest.json") { + expectedResource = resourceLib; + } else if (path === "/resources/sap/ui/demo/lib/comp1/manifest.json") { + expectedResource = resourceReuseComp1; + } else if (path === "/resources/sap/ui/demo/lib/comp2/manifest.json") { + t.fail("Resoure should be written, because it was not returned by the processor"); + } else { + t.fail("No other resoure should be written"); + } + t.deepEqual(actualResource, expectedResource, + "Expected resource is written back to workspace"); + } + }; + + t.context.manifestEnhancerStub.returns([resourceLib, resourceReuseComp1]); + + await enhanceManifest({ + workspace, + options: { + projectNamespace: "sap/ui/demo/lib" + } + }); + + t.is(t.context.manifestEnhancerStub.callCount, 1, + "Processor should be called once"); + + t.true(t.context.manifestEnhancerStub.calledWithExactly({ + resources: [resourceLib, resourceReuseComp1, resourceReuseComp2], + fs: "fs interface" + }), "Processor should be called with expected arguments"); + + t.true(log.warn.notCalled, "No warnings should be logged"); + t.true(log.error.notCalled, "No errors should be logged"); +}); + +test.serial("Transforms multiple manifest.json resources", async (t) => { + const {enhanceManifest, log} = t.context; + + t.plan(7); + + const resourceLib = createResource({ + path: "/resources/sap/ui/demo/lib/manifest.json", + string: `{ + "_version": "1.58.0", + "sap.app": { + "id": "sap.ui.demo.lib", + "type": "library" + }, + "sap.ui5": { + "library": { + "i18n": { + "bundleUrl": "i18n/i18n.properties" + } + } + } +}`, + project: t.context.workspace._project + }); + + const resourceReuseComp1 = createResource({ + path: "/resources/sap/ui/demo/lib/comp1/manifest.json", + string: `{ + "_version": "1.58.0", + "sap.app": { + "id": "sap.ui.demo.lib", + "type": "component" + }, + "sap.ui5": { + "models": { + "i18n": { + "bundleUrl": "i18n/i18n.properties" + } + } + } +}`, + project: t.context.workspace._project + }); + + const resourceReuseComp2 = createResource({ + path: "/resources/sap/ui/demo/lib/comp2/manifest.json", + string: `{ + "_version": "1.58.0", + "sap.app": { + "id": "sap.ui.demo.lib", + "type": "component" + }, + "sap.ui5": { + "models": { + "i18n": { + "bundleUrl": "i18n/i18n.properties" + } + } + } +}`, + project: t.context.workspace._project + }); + + const workspace = { + byGlob: () => { + return Promise.resolve([resourceLib, resourceReuseComp1, resourceReuseComp2]); + }, + write: (actualResource) => { + const path = actualResource.getPath(); + let expectedResource; + if (path === "/resources/sap/ui/demo/lib/manifest.json") { + expectedResource = resourceLib; + } else if (path === "/resources/sap/ui/demo/lib/comp1/manifest.json") { + expectedResource = resourceReuseComp1; + } else if (path === "/resources/sap/ui/demo/lib/comp2/manifest.json") { + expectedResource = resourceReuseComp2; + } else { + t.fail("No other resoure should be written"); + } + t.deepEqual(actualResource, expectedResource, + "Expected resource is written back to workspace"); + } + }; + + t.context.manifestEnhancerStub.returns([resourceLib, resourceReuseComp1, resourceReuseComp2]); + + await enhanceManifest({ + workspace, + options: { + projectNamespace: "sap/ui/demo/lib" + } + }); + + t.is(t.context.manifestEnhancerStub.callCount, 1, + "Processor should be called once"); + + t.true(t.context.manifestEnhancerStub.calledWithExactly({ + resources: [resourceLib, resourceReuseComp1, resourceReuseComp2], + fs: "fs interface" + }), "Processor should be called with expected arguments"); + + t.true(log.warn.notCalled, "No warnings should be logged"); + t.true(log.error.notCalled, "No errors should be logged"); +}); + +test.serial("Should not rewrite the manifest.json if no changes were made", async (t) => { + const {enhanceManifest, log} = t.context; + + t.plan(5); + + const resource = createResource({ + path: "/resources/sap/ui/demo/app/manifest.json", + string: `{ +"_version": "1.58.0", +"sap.app": { + "id": "sap.ui.demo.app", + "type": "application" +} +`, + project: t.context.workspace._project + }); + + const workspace = { + byGlob: (actualPath) => { + t.is(actualPath, "/resources/sap/ui/demo/app/**/manifest.json", + "Reads all manifest.json files"); + return Promise.resolve([resource]); + }, + write: (actualResource) => { + t.fail("No resource should be rewritten"); + } + }; + + t.context.manifestEnhancerStub.returns([]); + + await enhanceManifest({ + workspace, + options: { + projectNamespace: "sap/ui/demo/app" + } + }); + + t.is(t.context.manifestEnhancerStub.callCount, 1, + "Processor should be called once"); + + t.true(t.context.manifestEnhancerStub.calledWithExactly({ + resources: [resource], + fs: "fs interface" + }), "Processor should be called with expected arguments"); + + t.true(log.warn.notCalled, "No warnings should be logged"); + t.true(log.error.notCalled, "No errors should be logged"); +}); diff --git a/test/lib/tasks/taskRepository.js b/test/lib/tasks/taskRepository.js index 4b161a104..86f400038 100644 --- a/test/lib/tasks/taskRepository.js +++ b/test/lib/tasks/taskRepository.js @@ -18,6 +18,7 @@ test("getAllTaskNames", (t) => { "replaceCopyright", "replaceVersion", "replaceBuildtime", + "enhanceManifest", "escapeNonAsciiCharacters", "executeJsdocSdkTransformation", "generateApiIndex",