builder/lib/tasks/bundlers/generateComponentPreload.js

  1. import path from "node:path";
  2. import moduleBundler from "../../processors/bundlers/moduleBundler.js";
  3. import {applyDefaultsToBundleDefinition} from "./utils/applyDefaultsToBundleDefinition.js";
  4. import {getLogger} from "@ui5/logger";
  5. const log = getLogger("builder:tasks:bundlers:generateComponentPreload");
  6. import {negateFilters} from "../../lbt/resources/ResourceFilterList.js";
  7. /**
  8. * @public
  9. * @module @ui5/builder/tasks/bundlers/generateComponentPreload
  10. */
  11. /**
  12. * Task to for application bundling.
  13. *
  14. * @public
  15. * @function default
  16. * @static
  17. *
  18. * @param {object} parameters Parameters
  19. * @param {@ui5/fs/DuplexCollection} parameters.workspace DuplexCollection to read and write files
  20. * @param {@ui5/project/build/helpers/TaskUtil|object} [parameters.taskUtil] TaskUtil
  21. * @param {object} parameters.options Options
  22. * @param {string} parameters.options.projectName Project name
  23. * @param {string[]} [parameters.options.excludes=[]] List of modules declared as glob patterns (resource name patterns)
  24. * that should be excluded.
  25. * A pattern ending with a slash '/' will, similarly to the use of a single '*' or double '**' asterisk,
  26. * denote an arbitrary number of characters or folder names.
  27. * Re-includes should be marked with a leading exclamation mark '!'. The order of filters is relevant; a later
  28. * inclusion overrides an earlier exclusion, and vice versa.
  29. * @param {string[]} [parameters.options.paths] Array of paths (or glob patterns) for component files
  30. * @param {string[]} [parameters.options.namespaces] Array of component namespaces
  31. * @param {string[]} [parameters.options.skipBundles] Names of bundles that should not be created
  32. * @returns {Promise<undefined>} Promise resolving with <code>undefined</code> once data has been written
  33. */
  34. export default async function({
  35. workspace, taskUtil, options: {projectName, paths, namespaces, skipBundles = [], excludes = []}
  36. }) {
  37. let nonDbgWorkspace = workspace;
  38. if (taskUtil) {
  39. nonDbgWorkspace = taskUtil.resourceFactory.createFilterReader({
  40. reader: workspace,
  41. callback: function(resource) {
  42. // Remove any debug variants
  43. return !taskUtil.getTag(resource, taskUtil.STANDARD_TAGS.IsDebugVariant);
  44. }
  45. });
  46. }
  47. return nonDbgWorkspace.byGlob("/resources/**/*.{js,json,xml,html,properties,library,js.map}")
  48. .then(async (resources) => {
  49. let allNamespaces = [];
  50. if (paths) {
  51. allNamespaces = await Promise.all(paths.map(async (componentPath) => {
  52. const globPath = "/resources/" + componentPath;
  53. log.verbose(`Globbing for Components directories with configured path ${globPath}...`);
  54. const components = await nonDbgWorkspace.byGlob(globPath);
  55. return components.map((component) => {
  56. const compDir = path.dirname(component.getPath()).replace(/^\/resources\//i, "");
  57. log.verbose(`Found component namespace ${compDir}`);
  58. return compDir;
  59. });
  60. }));
  61. }
  62. if (namespaces) {
  63. allNamespaces.push(...namespaces);
  64. }
  65. allNamespaces = Array.prototype.concat.apply([], allNamespaces);
  66. // As this task is often called with a single namespace, also check
  67. // for bad calls like "namespaces: [undefined]"
  68. if (!allNamespaces || !allNamespaces.length || !allNamespaces[0]) {
  69. throw new Error("generateComponentPreload: No component namespace(s) " +
  70. `found for project: ${projectName}`);
  71. }
  72. const allFilterExcludes = negateFilters(excludes);
  73. const unusedFilterExcludes = new Set(allFilterExcludes);
  74. const bundleDefinitions = allNamespaces.map((namespace) => {
  75. const bundleName = `${namespace}/Component-preload.js`;
  76. if (skipBundles.includes(bundleName)) {
  77. log.verbose(`Skipping generation of bundle ${bundleName}`);
  78. return null;
  79. }
  80. const filters = [
  81. `${namespace}/`,
  82. `${namespace}/**/manifest.json`,
  83. `${namespace}/changes/changes-bundle.json`,
  84. `${namespace}/changes/flexibility-bundle.json`,
  85. `!${namespace}/test/`
  86. ];
  87. // Add configured excludes for namespace
  88. allFilterExcludes.forEach((filterExclude) => {
  89. // Allow all excludes (!) and limit re-includes (+) to the component namespace
  90. if (filterExclude.startsWith("!") || filterExclude.startsWith(`+${namespace}/`)) {
  91. filters.push(filterExclude);
  92. unusedFilterExcludes.delete(filterExclude);
  93. }
  94. });
  95. // Exclude other namespaces at the end of filter list to override potential re-includes
  96. // from "excludes" config
  97. allNamespaces.forEach((ns) => {
  98. if (ns !== namespace && ns.startsWith(`${namespace}/`)) {
  99. filters.push(`!${ns}/`);
  100. // Explicitly exclude manifest.json files of subcomponents since the general exclude above this
  101. // comment only applies to the configured default file types, which do not include ".json"
  102. filters.push(`!${ns}/**/manifest.json`);
  103. }
  104. });
  105. return {
  106. name: bundleName,
  107. defaultFileTypes: [
  108. ".js",
  109. ".control.xml",
  110. ".fragment.html",
  111. ".fragment.json",
  112. ".fragment.xml",
  113. ".view.html",
  114. ".view.json",
  115. ".view.xml",
  116. ".properties"
  117. ],
  118. sections: [
  119. {
  120. mode: "preload",
  121. filters: filters,
  122. resolve: false,
  123. resolveConditional: false,
  124. renderer: false
  125. }
  126. ]
  127. };
  128. });
  129. if (unusedFilterExcludes.size > 0) {
  130. unusedFilterExcludes.forEach((filterExclude) => {
  131. log.warn(
  132. `Configured preload exclude contains invalid re-include: !${filterExclude.substr(1)}. ` +
  133. `Re-includes must start with a component namespace (${allNamespaces.join(" or ")})`
  134. );
  135. });
  136. }
  137. const coreVersion = taskUtil?.getProject("sap.ui.core")?.getVersion();
  138. const allowStringBundling = taskUtil?.getProject().getSpecVersion().lt("4.0");
  139. return Promise.all(bundleDefinitions.filter(Boolean).map((bundleDefinition) => {
  140. log.verbose(`Generating ${bundleDefinition.name}...`);
  141. const options = {
  142. bundleDefinition: applyDefaultsToBundleDefinition(bundleDefinition, taskUtil),
  143. bundleOptions: {
  144. ignoreMissingModules: true,
  145. optimize: true
  146. },
  147. allowStringBundling
  148. };
  149. if (coreVersion) {
  150. options.targetUi5CoreVersion = coreVersion;
  151. }
  152. return moduleBundler({
  153. resources,
  154. options
  155. });
  156. }));
  157. })
  158. .then((results) => {
  159. const bundles = Array.prototype.concat.apply([], results);
  160. return Promise.all(bundles.map(({bundle, sourceMap}) => {
  161. if (taskUtil) {
  162. taskUtil.setTag(bundle, taskUtil.STANDARD_TAGS.IsBundle);
  163. // Clear tag that might have been set by the minify task, in cases where
  164. // the bundle name is identical to a source file
  165. taskUtil.clearTag(sourceMap, taskUtil.STANDARD_TAGS.OmitFromBuildResult);
  166. }
  167. return Promise.all([
  168. workspace.write(bundle),
  169. workspace.write(sourceMap)
  170. ]);
  171. }));
  172. });
  173. }