builder/lib/processors/bundlers/manifestBundler.js

  1. const posixPath = require("path").posix;
  2. const yazl = require("yazl");
  3. const resourceFactory = require("@ui5/fs").resourceFactory;
  4. const log = require("@ui5/logger").getLogger("builder:processors:bundlers:manifestBundler");
  5. /**
  6. * Repository to handle i18n resource files
  7. *
  8. * @private
  9. */
  10. class I18nResourceList {
  11. /**
  12. * Constructor
  13. */
  14. constructor() {
  15. this.propertyFiles = new Map();
  16. }
  17. /**
  18. * Adds a i18n resource to the repository
  19. *
  20. * @param {string} directory Path to the i18n resource
  21. * @param {module:@ui5/fs.Resource} resource i18n resource
  22. */
  23. add(directory, resource) {
  24. const normalizedDirectory = posixPath.normalize(directory);
  25. if (!this.propertyFiles.has(normalizedDirectory)) {
  26. this.propertyFiles.set(normalizedDirectory, [resource]);
  27. } else {
  28. this.propertyFiles.get(normalizedDirectory).push(resource);
  29. }
  30. }
  31. /**
  32. * Gets all registered i18n files within the provided path
  33. *
  34. * @param {string} directory Path to search for
  35. * @returns {Array} Array of resources files
  36. */
  37. get(directory) {
  38. return this.propertyFiles.get(posixPath.normalize(directory)) || [];
  39. }
  40. }
  41. /**
  42. * Creates a manifest bundle from the provided resources.
  43. *
  44. * @alias module:@ui5/builder.processors.manifestBundler
  45. * @public
  46. * @param {object} parameters Parameters
  47. * @param {module:@ui5/fs.Resource[]} parameters.resources List of resources to be processed
  48. * @param {object} parameters.options Options
  49. * @param {string} parameters.options.namespace Namespace of the project
  50. * @param {string} parameters.options.bundleName Name of the bundled zip file
  51. * @param {string} parameters.options.propertiesExtension Extension name of the properties files, e.g. ".properties"
  52. * @param {string} parameters.options.descriptor Descriptor name
  53. * @returns {Promise<module:@ui5/fs.Resource[]>} Promise resolving with manifest bundle resources
  54. */
  55. module.exports = ({resources, options: {namespace, bundleName, propertiesExtension, descriptor}}) => {
  56. function bundleNameToUrl(bundleName, appId) {
  57. if (!bundleName.startsWith(appId)) {
  58. return null;
  59. }
  60. const relativeBundleName = bundleName.substring(appId.length + 1);
  61. return relativeBundleName.replace(/\./g, "/") + propertiesExtension;
  62. }
  63. function addDescriptorI18nInfos(descriptorI18nInfos, manifest) {
  64. function addI18nInfo(i18nPath) {
  65. if (i18nPath.startsWith("ui5:")) {
  66. log.warn(`Using the ui5:// protocol for i18n bundles is currently not supported ('${i18nPath}' in ${manifest.path})`);
  67. return;
  68. }
  69. descriptorI18nInfos.set(
  70. posixPath.join(posixPath.dirname(manifest.path), posixPath.dirname(i18nPath)),
  71. posixPath.basename(i18nPath, propertiesExtension)
  72. );
  73. }
  74. const content = JSON.parse(manifest.content);
  75. const appI18n = content["sap.app"]["i18n"];
  76. let bundleUrl;
  77. // i18n section in sap.app can be either a string or an object with bundleUrl
  78. if (typeof appI18n === "object") {
  79. if (appI18n.bundleUrl) {
  80. bundleUrl = appI18n.bundleUrl;
  81. } else if (appI18n.bundleName) {
  82. bundleUrl = bundleNameToUrl(appI18n.bundleName, content["sap.app"]["id"]);
  83. }
  84. } else if (typeof appI18n === "string") {
  85. bundleUrl = appI18n;
  86. } else {
  87. bundleUrl = "i18n/i18n.properties";
  88. }
  89. if (bundleUrl) {
  90. addI18nInfo(bundleUrl);
  91. }
  92. if (typeof appI18n === "object" && Array.isArray(appI18n.enhanceWith)) {
  93. appI18n.enhanceWith.forEach((enhanceWithEntry) => {
  94. let bundleUrl;
  95. if (enhanceWithEntry.bundleUrl) {
  96. bundleUrl = enhanceWithEntry.bundleUrl;
  97. } else if (enhanceWithEntry.bundleName) {
  98. bundleUrl = bundleNameToUrl(enhanceWithEntry.bundleName, content["sap.app"]["id"]);
  99. }
  100. if (bundleUrl) {
  101. addI18nInfo(bundleUrl);
  102. }
  103. });
  104. }
  105. }
  106. return Promise.all(resources.map((resource) =>
  107. resource.getBuffer().then((content) => {
  108. const basename = posixPath.basename(resource.getPath());
  109. return {
  110. name: basename,
  111. isManifest: basename === descriptor,
  112. path: resource.getPath(),
  113. content: content
  114. };
  115. })
  116. )).then((resources) => {
  117. const archiveContent = new Map();
  118. const descriptorI18nInfos = new Map();
  119. const i18nResourceList = new I18nResourceList();
  120. resources.forEach((resource) => {
  121. if (resource.isManifest) {
  122. addDescriptorI18nInfos(descriptorI18nInfos, resource);
  123. archiveContent.set(resource.path, resource.content);
  124. } else {
  125. const directory = posixPath.dirname(resource.path);
  126. i18nResourceList.add(directory, resource);
  127. }
  128. });
  129. descriptorI18nInfos.forEach((rootName, directory) => {
  130. const i18nResources = i18nResourceList.get(directory)
  131. .filter((resource) => resource.name.startsWith(rootName));
  132. if (i18nResources.length) {
  133. i18nResources.forEach((resource) => archiveContent.set(resource.path, resource.content));
  134. } else {
  135. log.warn(`Could not find any resources for i18n bundle '${directory}'`);
  136. }
  137. });
  138. return archiveContent;
  139. }).then((archiveContent) => new Promise((resolve) => {
  140. const zip = new yazl.ZipFile();
  141. const basePath = `/resources/${namespace}/`;
  142. archiveContent.forEach((content, path) => {
  143. if (!path.startsWith(basePath)) {
  144. log.verbose(`Not bundling resource with path ${path} since it is not based on path ${basePath}`);
  145. return;
  146. }
  147. // Remove base path. Absolute paths are not allowed in ZIP files
  148. const normalizedPath = path.replace(basePath, "");
  149. zip.addBuffer(content, normalizedPath);
  150. });
  151. zip.end();
  152. const pathPrefix = "/resources/" + namespace + "/";
  153. const res = resourceFactory.createResource({
  154. path: pathPrefix + bundleName,
  155. stream: zip.outputStream
  156. });
  157. resolve([res]);
  158. }));
  159. };