diff --git a/index.js b/index.js index 73f11d149..77f967d03 100644 --- a/index.js +++ b/index.js @@ -19,6 +19,7 @@ module.exports = { bootstrapHtmlTransformer: require("./lib/processors/bootstrapHtmlTransformer"), debugFileCreator: require("./lib/processors/debugFileCreator"), resourceCopier: require("./lib/processors/resourceCopier"), + nonAsciiEscaper: require("./lib/processors/nonAsciiEscaper"), stringReplacer: require("./lib/processors/stringReplacer"), themeBuilder: require("./lib/processors/themeBuilder"), uglifier: require("./lib/processors/uglifier"), @@ -43,6 +44,7 @@ module.exports = { generateApiIndex: require("./lib/tasks/jsdoc/generateApiIndex"), generateJsdoc: require("./lib/tasks/jsdoc/generateJsdoc"), generateVersionInfo: require("./lib/tasks/generateVersionInfo"), + escapeNonAsciiCharacters: require("./lib/tasks/escapeNonAsciiCharacters"), replaceCopyright: require("./lib/tasks/replaceCopyright"), replaceVersion: require("./lib/tasks/replaceVersion"), transformBootstrapHtml: require("./lib/tasks/transformBootstrapHtml"), diff --git a/lib/lbt/bundle/AutoSplitter.js b/lib/lbt/bundle/AutoSplitter.js index da99f14c6..20c12a065 100644 --- a/lib/lbt/bundle/AutoSplitter.js +++ b/lib/lbt/bundle/AutoSplitter.js @@ -2,8 +2,10 @@ const uglify = require("uglify-es"); const {pd} = require("pretty-data"); + const ModuleName = require("../utils/ModuleName"); const {SectionType} = require("./BundleDefinition"); +const escapePropertiesFile = require("../utils/escapePropertiesFile"); const log = require("@ui5/logger").getLogger("lbt:bundle:AutoSplitter"); const copyrightCommentsPattern = /copyright|\(c\)(?:[0-9]+|\s+[0-9A-za-z])|released under|license|\u00a9/i; @@ -217,7 +219,6 @@ class AutoSplitter { // trace.debug("analyzed %s:%d%n", module, mw.getTargetLength()); return fileContent.length; } else if ( /\.properties$/.test(module) ) { - const fileContent = await resource.buffer(); /* NODE-TODO minimize *.properties Properties props = new Properties(); props.load(in); @@ -226,7 +227,12 @@ class AutoSplitter { props.store(out, ""); return out.toString().length(); */ - return fileContent.toString("latin1").length; + + // Since AutoSplitter is also used when splitting non-project resources (e.g. dependencies) + // *.properties files should be escaped if encoding option is specified + const fileContent = await escapePropertiesFile(resource); + + return fileContent.length; } else if ( this.optimizeXMLViews && /\.view.xml$/.test(module) ) { // needs to be activated when it gets activated in JSMergedModuleBuilderExt let fileContent = await resource.buffer(); diff --git a/lib/lbt/bundle/Builder.js b/lib/lbt/bundle/Builder.js index 0dd366b6e..74bc73f73 100644 --- a/lib/lbt/bundle/Builder.js +++ b/lib/lbt/bundle/Builder.js @@ -12,6 +12,7 @@ const {Syntax} = esprima; const {isMethodCall} = require("../utils/ASTUtils"); const ModuleName = require("../utils/ModuleName"); const UI5ClientConstants = require("../UI5ClientConstants"); +const escapePropertiesFile = require("../utils/escapePropertiesFile"); const BundleResolver = require("./Resolver"); const BundleSplitter = require("./AutoSplitter"); @@ -442,9 +443,11 @@ class BundleBuilder { } outW.write( makeStringLiteral( fileContent ) ); } else if ( /\.properties$/.test(module) ) { - // same as for other text files, but input encoding is ISO_8859_1 - const fileContent = await resource.buffer(); - outW.write( makeStringLiteral( fileContent.toString("latin1") ) ); + // Since the Builder is also used when building non-project resources (e.g. dependencies) + // *.properties files should be escaped if encoding option is specified + const fileContent = await escapePropertiesFile(resource); + + outW.write( makeStringLiteral( fileContent ) ); } else { log.error("don't know how to embed module " + module); // TODO throw? } diff --git a/lib/lbt/utils/escapePropertiesFile.js b/lib/lbt/utils/escapePropertiesFile.js new file mode 100644 index 000000000..3e8158398 --- /dev/null +++ b/lib/lbt/utils/escapePropertiesFile.js @@ -0,0 +1,29 @@ +const nonAsciiEscaper = require("../../processors/nonAsciiEscaper"); + +/** + * Can be used to escape *.properties files. + * + * Input encoding is read from project configuration. + * In case the resource belongs to no project (e.g. bundler is used standalone) the default is "ISO-8859-1". + * + * @private + * @param {Resource} resource the resource for which the content will be escaped + * @returns {Promise} resolves with the escaped string content of the given Resource + */ +module.exports = async function(resource) { + const propertiesFileSourceEncoding = resource.getProject() + && resource.getProject().resources + && resource.getProject().resources.configuration + && resource.getProject().resources.configuration.propertiesFileSourceEncoding; + const encoding = nonAsciiEscaper.getEncodingFromAlias(propertiesFileSourceEncoding || "ISO-8859-1"); + await nonAsciiEscaper({ + resources: [resource.resource], + options: { + encoding + } + }); + + const fileContent = await resource.buffer(); + + return fileContent.toString(); +}; diff --git a/lib/processors/bundlers/moduleBundler.js b/lib/processors/bundlers/moduleBundler.js index aef847673..a6bc5da4e 100644 --- a/lib/processors/bundlers/moduleBundler.js +++ b/lib/processors/bundlers/moduleBundler.js @@ -16,6 +16,10 @@ class LocatorResource extends Resource { buffer() { return this.resource.getBuffer(); } + + getProject() { + return this.resource._project; + } } class LocatorResourcePool extends ResourcePool { diff --git a/lib/processors/nonAsciiEscaper.js b/lib/processors/nonAsciiEscaper.js new file mode 100644 index 000000000..66b025d1d --- /dev/null +++ b/lib/processors/nonAsciiEscaper.js @@ -0,0 +1,103 @@ +const escapeUnicode = require("escape-unicode"); + +/** + * @see https://en.wikipedia.org/wiki/ASCII + * ascii contains 128 characters. + * its char codes reach from 0 to 127. + * @type {number} + */ +const CHAR_CODE_OF_LAST_ASCII_CHARACTER = 127; + +// use memoization for escapeUnicode function for performance +const memoizeEscapeUnicodeMap = {}; +const memoizeEscapeUnicode = function(sChar) { + if (memoizeEscapeUnicodeMap[sChar]) { + return memoizeEscapeUnicodeMap[sChar]; + } + memoizeEscapeUnicodeMap[sChar] = escapeUnicode(sChar); + return memoizeEscapeUnicodeMap[sChar]; +}; + +/** + * Escapes non ASCII characters with unicode escape sequences. + * + * @see https://en.wikipedia.org/wiki/ASCII + * @see https://tools.ietf.org/html/rfc5137#section-6.1 + * + * + * @param {string} string input string with non ascii characters, e.g. L♥VE + * @returns {{string: (string), modified: boolean}} output string with all non ascii + * characters being escaped by unicode sequence, e.g. L\u2665VE + */ +const escapeNonAscii = function(string) { + let result = ""; + let modified = false; + for (let i = 0; i < string.length; i++) { + const char = string[i]; + // check for non ascii characters (characters which have a char code + // greater than the ascii character code range) + if (string.charCodeAt(i) > CHAR_CODE_OF_LAST_ASCII_CHARACTER) { + result += memoizeEscapeUnicode(char); + modified = true; + } else { + result += char; + } + } + return { + modified, + string: result + }; +}; + +/** + * Escapes non ASCII characters with unicode escape sequences. + * + * @example + * const encoding = nonAsciiEscaper.getEncodingFromAlias("ISO-8859-1"); + * nonAsciiEscaper({resources, options: {encoding}}); + * + * + * @public + * @alias module:@ui5/builder.processors.nonAsciiEscaper + * @param {Object} parameters Parameters + * @param {module:@ui5/fs.Resource[]} parameters.resources List of resources to be processed + * @param {Object} [parameters.options] Options + * @param {string} [parameters.options.encoding="utf8"] resource file encoding (node.js based encodings). Use #getEncodingFromAlias to get the encoding string + * {@link https://nodejs.org/api/buffer.html#buffer_buffers_and_character_encodings Node.js character encodings}; + * @returns {Promise} Promise resolving with the processed resources + */ +module.exports = async function nonAsciiEscaper({resources, options={}}) { + const encoding = options.encoding || "utf8"; + + async function processResource(resource) { + const resourceString = (await resource.getBuffer()).toString(encoding); + const escaped = escapeNonAscii(resourceString); + // only modify the resource's string if it was changed + if (escaped.modified) { + resource.setString(escaped.string); + } + return resource; + } + + return Promise.all(resources.map(processResource)); +}; + +const encodingMap = { + "UTF-8": "utf8", + "ISO-8859-1": "latin1", +}; + +/** + * Provides a mapping from user-friendly encoding name (alias) such as "UTF-8" and "ISO-8859-1" to node + * specific encoding name such as "utf8" or "latin1". Simplifies usage of nonAsciiEscaper encoding + * option such that it can be used standalone without the respective task (e.g. in Splitter, Bundler and related projects). + * + * @param {string} encoding encoding labels: "UTF-8" and "ISO-8859-1" + * @returns {string} node.js character encoding string, e.g. utf8 and latin1 + */ +module.exports.getEncodingFromAlias = function(encoding) { + if (!encodingMap[encoding]) { + throw new Error(`Encoding "${encoding}" is not supported. Only ${Object.keys(encodingMap).join(", ")} are allowed values` ); + } + return encodingMap[encoding]; +}; diff --git a/lib/tasks/escapeNonAsciiCharacters.js b/lib/tasks/escapeNonAsciiCharacters.js new file mode 100644 index 000000000..5acb377b6 --- /dev/null +++ b/lib/tasks/escapeNonAsciiCharacters.js @@ -0,0 +1,30 @@ +const nonAsciiEscaper = require("../processors/nonAsciiEscaper"); + +/** + * Task to escape non ascii characters in properties files resources. + * + * @public + * @alias module:@ui5/builder.tasks.escapeNonAsciiCharacters + * @param {Object} parameters Parameters + * @param {module:@ui5/fs.DuplexCollection} parameters.workspace DuplexCollection to read and write files + * @param {Object} parameters.options Options + * @param {string} parameters.options.pattern Glob pattern to locate the files to be processed + * @param {string} parameters.options.encoding source file encoding either "UTF-8" or "ISO-8859-1" + * @returns {Promise} Promise resolving with undefined once data has been written + */ +module.exports = async function({workspace, options}) { + if (!options.encoding) { + throw new Error("[escapeNonAsciiCharacters] Mandatory option 'encoding' not provided"); + } + + const allResources = await workspace.byGlob(options.pattern); + + const processedResources = await nonAsciiEscaper({ + resources: allResources, + options: { + encoding: nonAsciiEscaper.getEncodingFromAlias(options.encoding) + } + }); + + await Promise.all(processedResources.map((resource) => workspace.write(resource))); +}; diff --git a/lib/tasks/taskRepository.js b/lib/tasks/taskRepository.js index b8a4dbb06..b45560f48 100644 --- a/lib/tasks/taskRepository.js +++ b/lib/tasks/taskRepository.js @@ -2,6 +2,7 @@ const tasks = { replaceCopyright: require("./replaceCopyright"), replaceVersion: require("./replaceVersion"), createDebugFiles: require("./createDebugFiles"), + escapeNonAsciiCharacters: require("./escapeNonAsciiCharacters"), executeJsdocSdkTransformation: require("./jsdoc/executeJsdocSdkTransformation"), generateApiIndex: require("./jsdoc/generateApiIndex"), generateJsdoc: require("./jsdoc/generateJsdoc"), diff --git a/lib/types/application/ApplicationBuilder.js b/lib/types/application/ApplicationBuilder.js index 0f7a393da..67043e786 100644 --- a/lib/types/application/ApplicationBuilder.js +++ b/lib/types/application/ApplicationBuilder.js @@ -8,6 +8,7 @@ const tasks = { // can't require index.js due to circular dependency generateStandaloneAppBundle: require("../../tasks/bundlers/generateStandaloneAppBundle"), generateBundle: require("../../tasks/bundlers/generateBundle"), generateCachebusterInfo: require("../../tasks/generateCachebusterInfo"), + escapeNonAsciiCharacters: require("../../tasks/escapeNonAsciiCharacters"), buildThemes: require("../../tasks/buildThemes"), createDebugFiles: require("../../tasks/createDebugFiles"), generateVersionInfo: require("../../tasks/generateVersionInfo"), @@ -26,6 +27,19 @@ class ApplicationBuilder extends AbstractBuilder { "Also see: https://github.com/SAP/ui5-builder#application"); } + this.addTask("escapeNonAsciiCharacters", () => { + const propertiesFileSourceEncoding = project.resources + && project.resources.configuration + && project.resources.configuration.propertiesFileSourceEncoding; + return tasks.escapeNonAsciiCharacters({ + workspace: resourceCollections.workspace, + options: { + encoding: propertiesFileSourceEncoding, + pattern: "/**/*.properties" + } + }); + }); + this.addTask("replaceCopyright", () => { return tasks.replaceCopyright({ workspace: resourceCollections.workspace, diff --git a/lib/types/application/ApplicationFormatter.js b/lib/types/application/ApplicationFormatter.js index b099bcffb..2a3fb0b3e 100644 --- a/lib/types/application/ApplicationFormatter.js +++ b/lib/types/application/ApplicationFormatter.js @@ -133,6 +133,15 @@ class ApplicationFormatter extends AbstractUi5Formatter { project.resources.configuration.paths.webapp = "webapp"; } + // default encoding to "ISO-8859-1" if not specified + if (!project.resources.configuration.propertiesFileSourceEncoding) { + project.resources.configuration.propertiesFileSourceEncoding = "ISO-8859-1"; + } + if (!["ISO-8859-1", "UTF-8"].includes(project.resources.configuration.propertiesFileSourceEncoding)) { + throw new Error(`Invalid properties file encoding specified for project ${project.id}: ` + + `encoding provided: ${project.resources.configuration.propertiesFileSourceEncoding}. Must be either "ISO-8859-1" or "UTF-8".`); + } + const absolutePath = path.join(project.path, project.resources.configuration.paths.webapp); return this.dirExists(absolutePath).then((bExists) => { if (!bExists) { diff --git a/lib/types/library/LibraryBuilder.js b/lib/types/library/LibraryBuilder.js index 806278ee8..b1125b794 100644 --- a/lib/types/library/LibraryBuilder.js +++ b/lib/types/library/LibraryBuilder.js @@ -6,6 +6,7 @@ const tasks = { // can't require index.js due to circular dependency generateLibraryPreload: require("../../tasks/bundlers/generateLibraryPreload"), generateManifestBundle: require("../../tasks/bundlers/generateManifestBundle"), generateStandaloneAppBundle: require("../../tasks/bundlers/generateStandaloneAppBundle"), + escapeNonAsciiCharacters: require("../../tasks/escapeNonAsciiCharacters"), buildThemes: require("../../tasks/buildThemes"), createDebugFiles: require("../../tasks/createDebugFiles"), generateJsdoc: require("../../tasks/jsdoc/generateJsdoc"), @@ -25,6 +26,19 @@ class LibraryBuilder extends AbstractBuilder { "Also see: https://github.com/SAP/ui5-builder#library"); } + this.addTask("escapeNonAsciiCharacters", () => { + const propertiesFileSourceEncoding = project.resources + && project.resources.configuration + && project.resources.configuration.propertiesFileSourceEncoding; + return tasks.escapeNonAsciiCharacters({ + workspace: resourceCollections.workspace, + options: { + encoding: propertiesFileSourceEncoding, + pattern: "/**/*.properties" + } + }); + }); + this.addTask("replaceCopyright", () => { const replaceCopyright = tasks.replaceCopyright; return replaceCopyright({ diff --git a/lib/types/library/LibraryFormatter.js b/lib/types/library/LibraryFormatter.js index 652fb8323..7339d7184 100644 --- a/lib/types/library/LibraryFormatter.js +++ b/lib/types/library/LibraryFormatter.js @@ -340,6 +340,15 @@ class LibraryFormatter extends AbstractUi5Formatter { project.resources.configuration.paths.test = "test"; } + // default encoding to "ISO-8859-1" if not specified + if (!project.resources.configuration.propertiesFileSourceEncoding) { + project.resources.configuration.propertiesFileSourceEncoding = "ISO-8859-1"; + } + if (!["ISO-8859-1", "UTF-8"].includes(project.resources.configuration.propertiesFileSourceEncoding)) { + throw new Error(`Invalid properties file encoding specified for project ${project.id}: ` + + `encoding provided: ${project.resources.configuration.propertiesFileSourceEncoding}. Must be either "ISO-8859-1" or "UTF-8".`); + } + const absoluteSrcPath = path.join(project.path, project.resources.configuration.paths.src); const absoluteTestPath = path.join(project.path, project.resources.configuration.paths.test); return Promise.all([ diff --git a/package-lock.json b/package-lock.json index a9bb42830..1b7bf02da 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2255,6 +2255,11 @@ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" }, + "escape-unicode": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/escape-unicode/-/escape-unicode-0.2.0.tgz", + "integrity": "sha512-7jMQuKb8nm0h/9HYLfu4NCLFwoUsd5XO6OZ1z86PbKcMf8zDK1m7nFR0iA2CCShq4TSValaLIveE8T1UBxgALQ==" + }, "escodegen": { "version": "1.11.1", "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.11.1.tgz", @@ -4402,7 +4407,7 @@ }, "find-up": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "resolved": false, "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", "dev": true, "requires": { @@ -4411,7 +4416,7 @@ }, "locate-path": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "resolved": false, "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", "dev": true, "requires": { @@ -4431,7 +4436,7 @@ }, "p-locate": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "resolved": false, "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", "dev": true, "requires": { @@ -4440,13 +4445,13 @@ }, "path-exists": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "resolved": false, "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", "dev": true }, "resolve-from": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "resolved": false, "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", "dev": true }, diff --git a/package.json b/package.json index 8fefaf0ed..852e98cba 100644 --- a/package.json +++ b/package.json @@ -99,6 +99,7 @@ "@ui5/fs": "^1.1.2", "@ui5/logger": "^1.0.1", "cheerio": "^0.22.0", + "escape-unicode": "^0.2.0", "escodegen": "^1.11.1", "escope": "^3.6.0", "esprima": "^4.0.1", diff --git a/test/expected/build/application.b/dest/i18n/i18n.properties b/test/expected/build/application.b/dest/i18n/i18n.properties index 575fb20d0..17772dbf5 100644 --- a/test/expected/build/application.b/dest/i18n/i18n.properties +++ b/test/expected/build/application.b/dest/i18n/i18n.properties @@ -1 +1,2 @@ -title=app-i18n \ No newline at end of file +title=app-i18n +fame=Stra\u00dfe \ No newline at end of file diff --git a/test/expected/build/application.b/dest/i18n/i18n_de.properties b/test/expected/build/application.b/dest/i18n/i18n_de.properties index f58858038..d2c23da4b 100644 --- a/test/expected/build/application.b/dest/i18n/i18n_de.properties +++ b/test/expected/build/application.b/dest/i18n/i18n_de.properties @@ -1 +1,2 @@ -title=app-i18n_de \ No newline at end of file +title=app-i18n_de +fame=Stra\u00dfe \ No newline at end of file diff --git a/test/expected/build/application.b/dest/manifest-bundle/i18n/i18n.properties b/test/expected/build/application.b/dest/manifest-bundle/i18n/i18n.properties index 575fb20d0..17772dbf5 100644 --- a/test/expected/build/application.b/dest/manifest-bundle/i18n/i18n.properties +++ b/test/expected/build/application.b/dest/manifest-bundle/i18n/i18n.properties @@ -1 +1,2 @@ -title=app-i18n \ No newline at end of file +title=app-i18n +fame=Stra\u00dfe \ No newline at end of file diff --git a/test/expected/build/application.b/dest/manifest-bundle/i18n/i18n_de.properties b/test/expected/build/application.b/dest/manifest-bundle/i18n/i18n_de.properties index f58858038..d2c23da4b 100644 --- a/test/expected/build/application.b/dest/manifest-bundle/i18n/i18n_de.properties +++ b/test/expected/build/application.b/dest/manifest-bundle/i18n/i18n_de.properties @@ -1 +1,2 @@ -title=app-i18n_de \ No newline at end of file +title=app-i18n_de +fame=Stra\u00dfe \ No newline at end of file diff --git a/test/expected/build/application.b/standalone/i18n.properties b/test/expected/build/application.b/standalone/i18n.properties index 88b84f602..d8d43be34 100644 --- a/test/expected/build/application.b/standalone/i18n.properties +++ b/test/expected/build/application.b/standalone/i18n.properties @@ -1 +1,2 @@ -title=app-i18n-wrong \ No newline at end of file +title=app-i18n-wrong +fame=Straße \ No newline at end of file diff --git a/test/expected/build/application.b/standalone/i18n/i18n.properties b/test/expected/build/application.b/standalone/i18n/i18n.properties index 575fb20d0..49f4756f9 100644 --- a/test/expected/build/application.b/standalone/i18n/i18n.properties +++ b/test/expected/build/application.b/standalone/i18n/i18n.properties @@ -1 +1,2 @@ -title=app-i18n \ No newline at end of file +title=app-i18n +fame=Straße \ No newline at end of file diff --git a/test/expected/build/application.b/standalone/i18n/i18n_de.properties b/test/expected/build/application.b/standalone/i18n/i18n_de.properties index f58858038..a81193360 100644 --- a/test/expected/build/application.b/standalone/i18n/i18n_de.properties +++ b/test/expected/build/application.b/standalone/i18n/i18n_de.properties @@ -1 +1,2 @@ -title=app-i18n_de \ No newline at end of file +title=app-i18n_de +fame=Straße \ No newline at end of file diff --git a/test/expected/build/application.b/standalone/i18n/l10n.properties b/test/expected/build/application.b/standalone/i18n/l10n.properties index 88b84f602..d8d43be34 100644 --- a/test/expected/build/application.b/standalone/i18n/l10n.properties +++ b/test/expected/build/application.b/standalone/i18n/l10n.properties @@ -1 +1,2 @@ -title=app-i18n-wrong \ No newline at end of file +title=app-i18n-wrong +fame=Straße \ No newline at end of file diff --git a/test/expected/build/application.b/standalone/resources/sap-ui-custom.js b/test/expected/build/application.b/standalone/resources/sap-ui-custom.js index 5da6683a2..2f1bb3225 100644 --- a/test/expected/build/application.b/standalone/resources/sap-ui-custom.js +++ b/test/expected/build/application.b/standalone/resources/sap-ui-custom.js @@ -5,10 +5,10 @@ jQuery.sap.registerPreloadedModules({ "id1/embedded/i18n/i18n_de.properties":'title=embedded-i18n_de', "id1/embedded/i18n_fr.properties":'title=embedded-i18n_fr-wrong', "id1/embedded/manifest.json":'{"_version":"1.1.0","sap.app":{"_version":"1.1.0","id":"id1.embedded","type":"component","applicationVersion":{"version":"1.2.2"},"embeddedBy":"../","title":"{{title}}"}}', - "id1/i18n.properties":'title=app-i18n-wrong', - "id1/i18n/i18n.properties":'title=app-i18n', - "id1/i18n/i18n_de.properties":'title=app-i18n_de', - "id1/i18n/l10n.properties":'title=app-i18n-wrong', + "id1/i18n.properties":'title=app-i18n-wrong\nfame=Stra\\u00dfe', + "id1/i18n/i18n.properties":'title=app-i18n\nfame=Stra\\u00dfe', + "id1/i18n/i18n_de.properties":'title=app-i18n_de\nfame=Stra\\u00dfe', + "id1/i18n/l10n.properties":'title=app-i18n-wrong\nfame=Stra\\u00dfe', "id1/manifest.json":'{"_version":"1.1.0","sap.app":{"_version":"1.1.0","id":"id1","type":"application","applicationVersion":{"version":"1.2.2"},"embeds":["embedded"],"title":"{{title}}"}}', "sap/ui/core/Core.js":function(){ } diff --git a/test/fixtures/application.b/webapp/i18n.properties b/test/fixtures/application.b/webapp/i18n.properties index 88b84f602..d8d43be34 100644 --- a/test/fixtures/application.b/webapp/i18n.properties +++ b/test/fixtures/application.b/webapp/i18n.properties @@ -1 +1,2 @@ -title=app-i18n-wrong \ No newline at end of file +title=app-i18n-wrong +fame=Straße \ No newline at end of file diff --git a/test/fixtures/application.b/webapp/i18n/i18n.properties b/test/fixtures/application.b/webapp/i18n/i18n.properties index 575fb20d0..49f4756f9 100644 --- a/test/fixtures/application.b/webapp/i18n/i18n.properties +++ b/test/fixtures/application.b/webapp/i18n/i18n.properties @@ -1 +1,2 @@ -title=app-i18n \ No newline at end of file +title=app-i18n +fame=Straße \ No newline at end of file diff --git a/test/fixtures/application.b/webapp/i18n/i18n_de.properties b/test/fixtures/application.b/webapp/i18n/i18n_de.properties index f58858038..a81193360 100644 --- a/test/fixtures/application.b/webapp/i18n/i18n_de.properties +++ b/test/fixtures/application.b/webapp/i18n/i18n_de.properties @@ -1 +1,2 @@ -title=app-i18n_de \ No newline at end of file +title=app-i18n_de +fame=Straße \ No newline at end of file diff --git a/test/fixtures/application.b/webapp/i18n/l10n.properties b/test/fixtures/application.b/webapp/i18n/l10n.properties index 88b84f602..d8d43be34 100644 --- a/test/fixtures/application.b/webapp/i18n/l10n.properties +++ b/test/fixtures/application.b/webapp/i18n/l10n.properties @@ -1 +1,2 @@ -title=app-i18n-wrong \ No newline at end of file +title=app-i18n-wrong +fame=Straße \ No newline at end of file diff --git a/test/lib/builder/builder.js b/test/lib/builder/builder.js index cc4af0b93..91292d72e 100644 --- a/test/lib/builder/builder.js +++ b/test/lib/builder/builder.js @@ -533,7 +533,8 @@ const applicationATree = { "configuration": { "paths": { "webapp": "webapp" - } + }, + "propertiesFileSourceEncoding": "ISO-8859-1" }, "pathMappings": { "/": "webapp" @@ -556,7 +557,8 @@ const applicationATreeBadType = { "configuration": { "paths": { "webapp": "webapp" - } + }, + "propertiesFileSourceEncoding": "ISO-8859-1" }, "pathMappings": { "/": "webapp" @@ -581,7 +583,8 @@ const applicationGTree = { "configuration": { "paths": { "webapp": "webapp" - } + }, + "propertiesFileSourceEncoding": "ISO-8859-1" }, "pathMappings": { "/": "webapp" @@ -615,7 +618,8 @@ const applicationGTreeWithExcludes = { "configuration": { "paths": { "webapp": "webapp" - } + }, + "propertiesFileSourceEncoding": "ISO-8859-1" }, "pathMappings": { "/": "webapp" @@ -650,7 +654,8 @@ const applicationGTreeComponentPreloadPaths = { "configuration": { "paths": { "webapp": "webapp" - } + }, + "propertiesFileSourceEncoding": "ISO-8859-1" }, "pathMappings": { "/": "webapp" @@ -681,7 +686,8 @@ const applicationHTree = { "configuration": { "paths": { "webapp": "webapp" - } + }, + "propertiesFileSourceEncoding": "ISO-8859-1" }, "pathMappings": { "/": "webapp" @@ -741,7 +747,8 @@ const applicationITree = { "configuration": { "paths": { "webapp": "webapp" - } + }, + "propertiesFileSourceEncoding": "ISO-8859-1" }, "pathMappings": { "/": "webapp" @@ -795,7 +802,8 @@ const libraryDTree = { "paths": { "src": "main/src", "test": "main/test" - } + }, + "propertiesFileSourceEncoding": "ISO-8859-1" }, "pathMappings": { "/resources/": "main/src", @@ -847,7 +855,8 @@ const libraryETree = { "paths": { "src": "src", "test": "test" - } + }, + "propertiesFileSourceEncoding": "ISO-8859-1" }, "pathMappings": { "/resources/": "src", @@ -899,7 +908,8 @@ const libraryHTree = { "paths": { "src": "main/src", "test": "main/test" - } + }, + "propertiesFileSourceEncoding": "ISO-8859-1" }, "pathMappings": { "/resources/": "main/src", @@ -984,7 +994,8 @@ const libraryITree = { "paths": { "src": "main/src", "test": "main/test" - } + }, + "propertiesFileSourceEncoding": "ISO-8859-1" }, "pathMappings": { "/resources/": "main/src" @@ -1035,7 +1046,8 @@ const themeJTree = { "paths": { "src": "main/src", "test": "main/test" - } + }, + "propertiesFileSourceEncoding": "ISO-8859-1" }, "pathMappings": { "/resources/": "main/src" diff --git a/test/lib/lbt/bundle/AutoSplitter.js b/test/lib/lbt/bundle/AutoSplitter.js index b17747613..d0d285213 100644 --- a/test/lib/lbt/bundle/AutoSplitter.js +++ b/test/lib/lbt/bundle/AutoSplitter.js @@ -16,7 +16,22 @@ function createMockPool(dependencies) { if (name === "c.js") { info.compressedSize = 512; } - return {info, buffer: async () => name.padStart(2048, "*")}; + return { + info, + buffer: async () => Buffer.from(name.padStart(2048, "*")), + getProject: () => { + return { + "resources": { + "configuration": { + "propertiesFileSourceEncoding": "ISO-8859-1" + } + } + }; + }, + resource: { + getBuffer: async () => Buffer.from(name.padStart(2048, "*")) + } + }; }, resources: [{ name: "a.js" @@ -152,7 +167,8 @@ test("_calcMinSize: js resource", async (t) => { size: 333, compressedSize: 333 }, - buffer: async () => "var test = 5;" + buffer: async () => "var test = 5;", + getProject: () => undefined }; } }; @@ -170,7 +186,8 @@ test.serial("_calcMinSize: uglify js resource", async (t) => { size: 333, compressedSize: 333 }, - buffer: async () => "var test = 5;" + buffer: async () => "var test = 5;", + getProject: () => undefined }; } }; @@ -183,20 +200,37 @@ test.serial("_calcMinSize: uglify js resource", async (t) => { test("_calcMinSize: properties resource", async (t) => { const pool = { findResourceWithInfo: function() { + let content = "1234ß"; return { - buffer: async () => "1234" + buffer: async () => Buffer.from(content), + resource: { + setString: (string) => { + content = string; + }, + getBuffer: async () => Buffer.from(content, "latin1") + }, + getProject: () => { + return { + "resources": { + "configuration": { + "propertiesFileSourceEncoding": "ISO-8859-1" + } + } + }; + } }; } }; const autpSplitter = new AutoSplitter(pool); - t.deepEqual(await autpSplitter._calcMinSize("mymodule.properties"), 4); + t.deepEqual(await autpSplitter._calcMinSize("mymodule.properties"), 10, "length of 1234\\u00df"); }); test("_calcMinSize: xml view resource", async (t) => { const pool = { findResourceWithInfo: function() { return { - buffer: async () => "12345" + buffer: async () => "12345", + getProject: () => undefined }; } }; @@ -209,7 +243,8 @@ test("_calcMinSize: xml view resource without optimizeXMLViews", async (t) => { const pool = { findResourceWithInfo: function() { return { - buffer: async () => "123456" + buffer: async () => "123456", + getProject: () => undefined }; } }; @@ -222,7 +257,8 @@ test.serial("_calcMinSize: optimize xml view resource", async (t) => { const pool = { findResourceWithInfo: function() { return { - buffer: async () => "xxx" + buffer: async () => "xxx", + getProject: () => undefined }; } }; @@ -238,7 +274,8 @@ test.serial("_calcMinSize: optimize xml view resource and pre tag", async (t) => const pool = { findResourceWithInfo: function() { return { - buffer: async () => "
asd
" + buffer: async () => "
asd
", + getProject: () => undefined }; } }; diff --git a/test/lib/processors/nonAsciiEscaper.js b/test/lib/processors/nonAsciiEscaper.js new file mode 100644 index 000000000..4b10c0c1b --- /dev/null +++ b/test/lib/processors/nonAsciiEscaper.js @@ -0,0 +1,127 @@ +const test = require("ava"); + +const nonAsciiEscaper = require("../../../lib/processors/nonAsciiEscaper"); +const Resource = require("@ui5/fs").Resource; + +/** + * Executes string escaping. Returns undefined if nothing was escaped. + * + * @param {string} input string + * @param {Object} [options] + * @param {string} encoding character encoding used, e.g. utf8, latin1, .. + * @returns {Promise} escaped string if non-ascii characters present, undefined otherwise + */ +const escape = async function(input, options={}, encoding="utf8") { + const resource = new Resource({ + path: "my.properties", + buffer: Buffer.from(input, encoding) + }); + return nonAsciiEscaper({resources: [resource], options}); +}; + +test("Escape characters (ISO-8859-1)", async (t) => { + t.plan(1); + + const input = `Viele Grüße`; + const expected = `Viele Gr\\u00fc\\u00dfe`; + const [resource] = await escape(input, { + encoding: "latin1" // escape encoding + }, "latin1"); // resource encoding + t.deepEqual(await resource.getString(), expected, "Correct file content should be set"); +}); + +test("Escape characters (UTF-8, explicit parameter)", async (t) => { + t.plan(1); + + const input = `These are 人物 characters`; + const expected = "These are \\u4eba\\u7269 characters"; + const [resource] = await escape(input, { + encoding: "utf8" // escape encoding + }, "utf8"); // resource encoding + t.deepEqual(await resource.getString(), expected, "Correct file content should be set"); +}); + +test("Escape characters (UTF-8, default)", async (t) => { + t.plan(1); + + const input = `Viele Grüße`; + const expected = `Viele Gr\\u00fc\\u00dfe`; + // default encoding is utf8 + const [resource] = await escape(input); + t.deepEqual(await resource.getString(), expected, "Correct file content should be set"); +}); + +test("Replace symbol characters", async (t) => { + t.plan(1); + + const input = `L♥VE is everywhere`; + const expected = `L\\u2665VE is everywhere`; + const [resource] = await escape(input); + t.deepEqual(await resource.getString(), expected, "Correct file content should be set"); +}); + +test("Replace chinese characters", async (t) => { + t.plan(1); + + const input = `These are 人物 characters`; + const expected = "These are \\u4eba\\u7269 characters"; + const [resource] = await escape(input); + t.deepEqual(await resource.getString(), expected, "Correct file content should be set"); +}); + +test("Replace umlaut characters", async (t) => { + t.plan(1); + + const input = `Achso Ähem`; + const expected = "Achso \\u00c4hem"; + const [resource] = await escape(input); + t.deepEqual(await resource.getString(), expected, "Correct file content should be set"); +}); + +test("Replace constructed characters", async (t) => { + t.plan(1); + + const input = `Oh ẛ̣ that's ẛ̣ yes`; + const expected = "Oh \\u1e9b\\u0323 that's \\u1e9b\\u0323 yes"; + const [resource] = await escape(input); + t.deepEqual(await resource.getString(), expected, "Correct file content should be set"); +}); + + +test("Replace multiple times same character", async (t) => { + t.plan(1); + + const input = `♥H L♥VE AND HARM♥NY ♥MG`; + const expected = "\\u2665H L\\u2665VE AND HARM\\u2665NY \\u2665MG"; + const [resource] = await escape(input); + t.deepEqual(await resource.getString(), expected, "Correct file content should be set"); +}); + +test("No Replace of characters", async (t) => { + t.plan(1); + + const input = `ONE LOVE`; + const expected = `ONE LOVE`; + const [resource] = await escape(input); + t.deepEqual(await resource.getString(), expected, "Correct file content should be set"); +}); + +test("Invalid encoding", async (t) => { + t.plan(2); + + const input = `ONE LOVE`; + const error = await t.throwsAsync(escape(input, {encoding: "asd"})); + t.is(error.message, "Unknown encoding: asd"); +}); + + +test("getEncodingFromAlias", (t) => { + t.is("utf8", nonAsciiEscaper.getEncodingFromAlias("UTF-8")); +}); + +test("getEncodingFromAlias invalid", (t) => { + const error = t.throws(function() { + nonAsciiEscaper.getEncodingFromAlias("asd"); + }); + t.is(error.message, `Encoding "asd" is not supported. Only UTF-8, ISO-8859-1 are allowed values`); +}); diff --git a/test/lib/tasks/bundlers/generateManifestBundle.js b/test/lib/tasks/bundlers/generateManifestBundle.js index 3cb5e9b5b..8ec5d5938 100644 --- a/test/lib/tasks/bundlers/generateManifestBundle.js +++ b/test/lib/tasks/bundlers/generateManifestBundle.js @@ -118,7 +118,7 @@ test("integration: Build application.b with manifestBundler", (t) => { const destBundle = path.resolve(path.join(destPath, "manifest-bundle")); const expectedPath = path.join("test", "expected", "build", "application.b", "dest", "manifest-bundle"); const excludedTasks = ["*"]; - const includedTasks = ["generateManifestBundle"]; + const includedTasks = ["escapeNonAsciiCharacters", "generateManifestBundle"]; return builder.build({ tree: applicationBTree, @@ -189,7 +189,8 @@ const applicationBTree = { "configuration": { "paths": { "webapp": "webapp" - } + }, + "propertiesFileSourceEncoding": "ISO-8859-1" }, "pathMappings": { "/": "webapp" diff --git a/test/lib/tasks/bundlers/generateStandaloneAppBundle.js b/test/lib/tasks/bundlers/generateStandaloneAppBundle.js index cb0a975ee..9415cb129 100644 --- a/test/lib/tasks/bundlers/generateStandaloneAppBundle.js +++ b/test/lib/tasks/bundlers/generateStandaloneAppBundle.js @@ -329,7 +329,8 @@ const applicationBTree = { "configuration": { "paths": { "webapp": "webapp" - } + }, + "propertiesFileSourceEncoding": "ISO-8859-1" }, "pathMappings": { "/": "webapp" diff --git a/test/lib/tasks/escapeNonAsciiCharacters.js b/test/lib/tasks/escapeNonAsciiCharacters.js new file mode 100644 index 000000000..9c16de7a4 --- /dev/null +++ b/test/lib/tasks/escapeNonAsciiCharacters.js @@ -0,0 +1,144 @@ +const test = require("ava"); + +const ui5Builder = require("../../../"); +const tasks = ui5Builder.builder.tasks; +const ui5Fs = require("@ui5/fs"); +const resourceFactory = ui5Fs.resourceFactory; +const DuplexCollection = ui5Fs.DuplexCollection; + +test("integration: escape non ascii characters (utf8, default)", (t) => { + const reader = resourceFactory.createAdapter({ + virBasePath: "/" + }); + const writer = resourceFactory.createAdapter({ + virBasePath: "/" + }); + const workspace = new DuplexCollection({reader, writer}); + + const content = `welcome=Willkommen {0}. Bitte geben Sie einen neuen Kontakt ein: +lastname=Nachname: +firstname=Vorname: +street=Straße:♥ +zip=PLZ: +city=Ort:`; + + const expected = `welcome=Willkommen {0}. Bitte geben Sie einen neuen Kontakt ein: +lastname=Nachname: +firstname=Vorname: +street=Stra\\u00dfe:\\u2665 +zip=PLZ: +city=Ort:`; + + const resource = resourceFactory.createResource({ + path: "/i18n.properties", + string: content + }); + + return workspace.write(resource).then(() => { + return tasks.escapeNonAsciiCharacters({ + workspace, + options: { + encoding: "UTF-8", + pattern: "/**/*.properties" + } + }).then(() => { + return writer.byPath("/i18n.properties").then((resource) => { + if (!resource) { + t.fail("Could not find /i18n.properties in target"); + } else { + return resource.getString(); + } + }); + }).then((result) => { + return t.deepEqual(result, expected); + }); + }); +}); + +test("integration: escape non ascii characters source encoding being (ISO-8859-1)", (t) => { + const reader = resourceFactory.createAdapter({ + virBasePath: "/" + }); + const writer = resourceFactory.createAdapter({ + virBasePath: "/" + }); + const workspace = new DuplexCollection({reader, writer}); + + // create buffer in ISO encoding + const content = Buffer.from(`welcome=Willkommen {0}. Bitte geben Sie einen neuen Kontakt ein: +lastname=Nachname: +firstname=Vorname: +street=Straße: +zip=PLZ: +city=Ort:`, "latin1"); + + const expected = `welcome=Willkommen {0}. Bitte geben Sie einen neuen Kontakt ein: +lastname=Nachname: +firstname=Vorname: +street=Stra\\u00dfe: +zip=PLZ: +city=Ort:`; + + const resource = resourceFactory.createResource({ + path: "/i18n.properties", + buffer: content + }); + + return workspace.write(resource).then(() => { + return tasks.escapeNonAsciiCharacters({ + workspace, + options: { + encoding: "ISO-8859-1", + pattern: "/**/*.properties" + } + }).then(() => { + return writer.byPath("/i18n.properties").then((resource) => { + if (!resource) { + t.fail("Could not find /i18n.properties in target"); + } else { + return resource.getString(); + } + }); + }).then((result) => { + return t.deepEqual(result, expected); + }); + }); +}); + +test("integration: escape non ascii characters source encoding being empty", async (t) => { + const reader = resourceFactory.createAdapter({ + virBasePath: "/" + }); + const writer = resourceFactory.createAdapter({ + virBasePath: "/" + }); + const workspace = new DuplexCollection({reader, writer}); + + const error = await t.throwsAsync(tasks.escapeNonAsciiCharacters({ + workspace, + options: { + encoding: "", + pattern: "/**/*.properties" + } + })); + return t.is(error.message, "[escapeNonAsciiCharacters] Mandatory option 'encoding' not provided"); +}); + +test("integration: escape non ascii characters source encoding being UTF-16", async (t) => { + const reader = resourceFactory.createAdapter({ + virBasePath: "/" + }); + const writer = resourceFactory.createAdapter({ + virBasePath: "/" + }); + const workspace = new DuplexCollection({reader, writer}); + + const error = await t.throwsAsync(tasks.escapeNonAsciiCharacters({ + workspace, + options: { + encoding: "utf16le", + pattern: "/**/*.properties" + } + })); + return t.is(error.message, `Encoding "utf16le" is not supported. Only UTF-8, ISO-8859-1 are allowed values`); +}); diff --git a/test/lib/tasks/generateCachebusterInfo.js b/test/lib/tasks/generateCachebusterInfo.js index 17ddaa9bb..52a403ced 100644 --- a/test/lib/tasks/generateCachebusterInfo.js +++ b/test/lib/tasks/generateCachebusterInfo.js @@ -27,7 +27,7 @@ const findFiles = (folder) => { test("integration: Build application.g with manifestBundler", (t) => { const destPath = path.join("test", "tmp", "build", "application.g", "cachebuster"); const expectedPath = path.join("test", "expected", "build", "application.g", "cachebuster"); - const excludedTasks = ["generateVersionInfo"]; + const excludedTasks = ["escapeNonAsciiCharacters", "generateVersionInfo"]; const includedTasks = ["generateCachebusterInfo"]; return builder.build({ @@ -60,7 +60,7 @@ test("integration: Build application.g with manifestBundler", (t) => { test("integration: Build application.g with manifestBundler and cachebuster using hashes", (t) => { const destPath = path.join("test", "tmp", "build", "application.g", "cachebuster_hash"); const expectedPath = path.join("test", "expected", "build", "application.g", "cachebuster"); - const excludedTasks = ["generateVersionInfo"]; + const excludedTasks = ["escapeNonAsciiCharacters", "generateVersionInfo"]; const includedTasks = ["generateCachebusterInfo"]; return builder.build({ diff --git a/test/lib/types/application/ApplicationBuilder.js b/test/lib/types/application/ApplicationBuilder.js index 57bf109d8..f05d56552 100644 --- a/test/lib/types/application/ApplicationBuilder.js +++ b/test/lib/types/application/ApplicationBuilder.js @@ -57,6 +57,7 @@ test("Instantiation", (t) => { const appBuilder = new ApplicationBuilder({parentLogger, project}); t.truthy(appBuilder); t.deepEqual(appBuilder.taskExecutionOrder, [ + "escapeNonAsciiCharacters", "replaceCopyright", "replaceVersion", "generateFlexChangesBundle", @@ -79,6 +80,7 @@ test("Instantiation without component preload project configuration", (t) => { const appBuilder = new ApplicationBuilder({parentLogger, project}); t.truthy(appBuilder); t.deepEqual(appBuilder.taskExecutionOrder, [ + "escapeNonAsciiCharacters", "replaceCopyright", "replaceVersion", "generateFlexChangesBundle", @@ -102,6 +104,7 @@ test("Instantiation without project namespace", (t) => { const appBuilder = new ApplicationBuilder({parentLogger, project}); t.truthy(appBuilder); t.deepEqual(appBuilder.taskExecutionOrder, [ + "escapeNonAsciiCharacters", "replaceCopyright", "replaceVersion", "generateFlexChangesBundle", @@ -124,6 +127,7 @@ test("Instantiation with custom tasks", (t) => { const appBuilder = new ApplicationBuilder({parentLogger, project}); t.truthy(appBuilder); t.deepEqual(appBuilder.taskExecutionOrder, [ + "escapeNonAsciiCharacters", "replaceCopyright", "uglify--1", "replaceVersion", diff --git a/test/lib/types/application/ApplicationFormatter.js b/test/lib/types/application/ApplicationFormatter.js index b65095a5c..67919da95 100644 --- a/test/lib/types/application/ApplicationFormatter.js +++ b/test/lib/types/application/ApplicationFormatter.js @@ -96,6 +96,25 @@ test("validate: empty resources", async (t) => { t.deepEqual(myProject.resources.configuration.paths.webapp, "webapp", "default webapp directory is set"); }); +test("validate: empty encoding", async (t) => { + const myProject = clone(applicationBTree); + delete myProject.resources.configuration.propertiesFileSourceEncoding; + const applicationFormatter = new ApplicationFormatter({project: myProject}); + + await applicationFormatter.validate(myProject); + t.deepEqual(myProject.resources.configuration.propertiesFileSourceEncoding, "ISO-8859-1", "default resources encoding is set"); +}); + +test("validate: test invalid encoding", async (t) => { + const myProject = clone(applicationBTree); + myProject.resources.configuration.propertiesFileSourceEncoding = "test"; + const applicationFormatter = new ApplicationFormatter({project: myProject}); + + const error = await t.throwsAsync(applicationFormatter.validate(myProject)); + t.is(error.message, `Invalid properties file encoding specified for project application.b: encoding provided: test. Must be either "ISO-8859-1" or "UTF-8".`, + "Missing source directory caused error"); +}); + function createMockProject() { return { resources: { diff --git a/test/lib/types/library/LibraryFormatter.js b/test/lib/types/library/LibraryFormatter.js index 70ec04309..5d13f6ebc 100644 --- a/test/lib/types/library/LibraryFormatter.js +++ b/test/lib/types/library/LibraryFormatter.js @@ -88,6 +88,15 @@ test("validate: empty resources", async (t) => { t.deepEqual(myProject.resources.configuration.paths.test, "test", "default test directory is set"); }); +test("validate: empty encoding", async (t) => { + const myProject = clone(libraryETree); + delete myProject.resources.configuration.propertiesFileSourceEncoding; + const libraryFormatter = new LibraryFormatter({project: myProject}); + + await libraryFormatter.validate(myProject); + t.deepEqual(myProject.resources.configuration.propertiesFileSourceEncoding, "ISO-8859-1", "default resources encoding is set"); +}); + test("validate: src directory does not exist", async (t) => { const myProject = clone(libraryETree); const libraryFormatter = new LibraryFormatter({project: myProject}); @@ -95,7 +104,7 @@ test("validate: src directory does not exist", async (t) => { dirExists.onFirstCall().resolves(false); dirExists.onSecondCall().resolves(true); - const error = await await t.throwsAsync(libraryFormatter.validate(myProject)); + const error = await t.throwsAsync(libraryFormatter.validate(myProject)); t.regex(error.message, /^Could not find source directory of project library\.e\.id: (?!(undefined))+/, "Missing source directory caused error"); }); @@ -109,7 +118,17 @@ test("validate: test directory does not exist", async (t) => { await libraryFormatter.validate(myProject); // Missing test directory is not an error - t.deepEqual(myProject.resources.configuration.paths.test, null, "Project test path configuration is set to nul"); + t.deepEqual(myProject.resources.configuration.paths.test, null, "Project test path configuration is set to null"); +}); + +test("validate: test invalid encoding", async (t) => { + const myProject = clone(libraryETree); + myProject.resources.configuration.propertiesFileSourceEncoding = "test"; + const libraryFormatter = new LibraryFormatter({project: myProject}); + + const error = await t.throwsAsync(libraryFormatter.validate(myProject)); + t.is(error.message, `Invalid properties file encoding specified for project library.e.id: encoding provided: test. Must be either "ISO-8859-1" or "UTF-8".`, + "Missing source directory caused error"); }); test("format: copyright already configured", async (t) => {