builder/lib/processors/libraryLessGenerator.js

  1. const log = require("@ui5/logger").getLogger("builder:processors:libraryLessGenerator");
  2. const {promisify} = require("util");
  3. const posixPath = require("path").posix;
  4. const Resource = require("@ui5/fs").Resource;
  5. const IMPORT_PATTERN = /@import .*"(.*)";/g;
  6. const BASE_LESS_PATTERN = /^\/resources\/sap\/ui\/core\/themes\/([^/]+)\/base\.less$/;
  7. const GLOBAL_LESS_PATTERN = /^\/resources\/sap\/ui\/core\/themes\/([^/]+)\/global\.less$/;
  8. class ImportError extends Error {
  9. constructor(message) {
  10. super();
  11. this.name = "ImportError";
  12. this.message = message;
  13. }
  14. }
  15. class LibraryLessGenerator {
  16. constructor({fs}) {
  17. const readFile = promisify(fs.readFile);
  18. this.readFile = async (filePath) => readFile(filePath, {encoding: "utf8"});
  19. }
  20. async generate({filePath, fileContent}) {
  21. return `/* NOTE: This file was generated as an optimized version of ` +
  22. `"library.source.less" for the Theme Designer. */\n\n` +
  23. await this.resolveLessImports({
  24. filePath,
  25. fileContent
  26. });
  27. }
  28. static getPathToRoot(baseDir) {
  29. return posixPath.relative(baseDir, "/") + "/";
  30. }
  31. async resolveLessImports({filePath, fileContent}) {
  32. const imports = this.findLessImports(fileContent);
  33. if (!imports.length) {
  34. // Skip processing when no imports are found
  35. return fileContent;
  36. }
  37. const replacements = await Promise.all(imports.map(async (importMatch) => {
  38. const baseDir = posixPath.dirname(filePath);
  39. const resolvedFilePath = posixPath.resolve(baseDir, importMatch.path);
  40. try {
  41. importMatch.content = await this.resolveLessImport(importMatch.path, resolvedFilePath, baseDir);
  42. } catch (error) {
  43. if (error instanceof ImportError) {
  44. // Add message of import errors after the import statements
  45. // Currently those errors should not break the build (see comments in resolveLessImport)
  46. importMatch.content = importMatch.fullMatch + ` /* ${error} */`;
  47. } else {
  48. throw error;
  49. }
  50. }
  51. return importMatch;
  52. }));
  53. // Apply replacements in reverse order to not modify the relevant indices
  54. const array = Array.from(fileContent);
  55. for (let i = replacements.length - 1; i >= 0; i--) {
  56. const replacement = replacements[i];
  57. if (!replacement.content) {
  58. continue;
  59. }
  60. array.splice(
  61. /* index */ replacement.matchStart,
  62. /* count */ replacement.matchLength,
  63. /* insert */ replacement.content
  64. );
  65. }
  66. return array.join("");
  67. }
  68. async resolveLessImport(originalFilePath, resolvedFilePath, baseDir) {
  69. // Rewrite base.less imports
  70. const baseLessMatch = BASE_LESS_PATTERN.exec(resolvedFilePath);
  71. if (baseLessMatch) {
  72. let baseLessThemeName = baseLessMatch[1];
  73. if (baseLessThemeName === "base") {
  74. baseLessThemeName = "baseTheme";
  75. }
  76. const baseLessPath = LibraryLessGenerator.getPathToRoot(baseDir) +
  77. "Base/baseLib/" + baseLessThemeName + "/base.less";
  78. return "@import \"" + baseLessPath + "\"; /* ORIGINAL IMPORT PATH: \"" + originalFilePath + "\" */\n";
  79. }
  80. // Rewrite library imports to correct file name
  81. if (posixPath.basename(resolvedFilePath) === "library.source.less") {
  82. return `@import "${originalFilePath.replace(/library\.source\.less$/, "library.less")}";`;
  83. }
  84. // Excluding global.less within sap.ui.core
  85. // It must be imported by the Theme Designer (also see declaration in sap/ui/core/.theming)
  86. if (GLOBAL_LESS_PATTERN.test(resolvedFilePath)) {
  87. return null;
  88. }
  89. /*
  90. * Log error in case of files which are not in the same directory as the current file because
  91. * inlining them would break relative URLs.
  92. * A possible solution would be to rewrite relative URLs when inlining the content.
  93. *
  94. * Keeping the import will cause errors since only "library.less" and "global.less" are
  95. * configured to be available to the Theme Designer (.theming generated in generateThemeDesignerResources).
  96. * However, the previous implementation did not break the build.
  97. * In many cases the library.less file is currently not relevant so breaking the build would cause
  98. * unnecessary issues.
  99. *
  100. */
  101. const relativeFilePath = posixPath.relative(baseDir, resolvedFilePath);
  102. if (relativeFilePath.includes(posixPath.sep)) {
  103. log.error(
  104. `Could not inline import '${resolvedFilePath}' outside of theme directory '${baseDir}'. ` +
  105. `Stylesheets must be located in the theme directory (no sub-directories). ` +
  106. `The generated '${baseDir}/library.less' will cause errors when compiled with the Theme Designer.`
  107. );
  108. // Throw error to be added as comment to the import statement
  109. throw new ImportError("Could not inline import outside of theme directory");
  110. }
  111. let importedFileContent;
  112. try {
  113. importedFileContent = await this.readFile(resolvedFilePath);
  114. } catch (err) {
  115. if (err.code === "ENOENT") {
  116. throw new Error(
  117. `libraryLessGenerator: Unable to resolve import '${originalFilePath}' from '${baseDir}'\n` +
  118. err.message
  119. );
  120. } else {
  121. throw err;
  122. }
  123. }
  124. return `/* START "${originalFilePath}" */\n` +
  125. await this.resolveLessImports({
  126. filePath: resolvedFilePath,
  127. fileContent: importedFileContent
  128. }) +
  129. `\n/* END "${originalFilePath}" */\n`;
  130. }
  131. findLessImports(fileContent) {
  132. const imports = [];
  133. let match;
  134. while ((match = IMPORT_PATTERN.exec(fileContent)) !== null) {
  135. imports.push({
  136. path: match[1],
  137. fullMatch: match[0],
  138. matchStart: match.index,
  139. matchLength: match[0].length
  140. });
  141. }
  142. return imports;
  143. }
  144. }
  145. /**
  146. * Creates a "library.less" file for the SAP Theme Designer based on a "library.source.less" file.
  147. *
  148. * <ul>
  149. * <li>Bundles all *.less file of the theme by replacing the import with the corresponding file content</li>
  150. * <li>Imports to "base.less" are adopted so that they point to the "BaseLib" that is available within
  151. * the Theme Designer infrastructure</li>
  152. * <li>Imports to "global.less" are kept as they should not be bundled</li>
  153. * <li>Imports to "library.source.less" are adopted to "library.less"</li>
  154. * </ul>
  155. *
  156. * @public
  157. * @alias module:@ui5/builder.processors.libraryLessGenerator
  158. * @param {object} parameters Parameters
  159. * @param {module:@ui5/fs.Resource[]} parameters.resources List of <code>library.source.less</code>
  160. * resources
  161. * @param {fs|module:@ui5/fs.fsInterface} parameters.fs Node fs or custom
  162. * [fs interface]{@link module:resources/module:@ui5/fs.fsInterface}
  163. * @returns {Promise<module:@ui5/fs.Resource[]>} Promise resolving with library.less resources
  164. */
  165. module.exports = async function({resources, fs}) {
  166. const generator = new LibraryLessGenerator({fs});
  167. return Promise.all(resources.map(async (librarySourceLessResource) => {
  168. const filePath = librarySourceLessResource.getPath();
  169. const fileContent = await librarySourceLessResource.getString();
  170. log.verbose(`Generating library.less file based on ${filePath}`);
  171. const libraryLessFileContent = await generator.generate({filePath, fileContent});
  172. const libraryLessFilePath = posixPath.join(posixPath.dirname(filePath), "library.less");
  173. return new Resource({
  174. path: libraryLessFilePath,
  175. string: libraryLessFileContent
  176. });
  177. }));
  178. };
  179. // Export class for testing only
  180. /* istanbul ignore else */
  181. if (process.env.NODE_ENV === "test") {
  182. module.exports._LibraryLessGenerator = LibraryLessGenerator;
  183. }