builder/lib/tasks/bundlers/generateLibraryPreload.js

  1. import {getLogger} from "@ui5/logger";
  2. const log = getLogger("builder:tasks:bundlers:generateLibraryPreload");
  3. import moduleBundler from "../../processors/bundlers/moduleBundler.js";
  4. import {applyDefaultsToBundleDefinition} from "./utils/applyDefaultsToBundleDefinition.js";
  5. import {negateFilters} from "../../lbt/resources/ResourceFilterList.js";
  6. import createModuleNameMapping from "./utils/createModuleNameMapping.js";
  7. function getDefaultLibraryPreloadFilters(namespace, excludes) {
  8. const filters = [
  9. `${namespace}/`,
  10. `${namespace}/**/manifest.json`,
  11. `!${namespace}/**/*-preload.js`, // exclude all bundles
  12. `!${namespace}/designtime/`,
  13. `!${namespace}/**/*.designtime.js`,
  14. `!${namespace}/**/*.support.js`
  15. ];
  16. if (Array.isArray(excludes)) {
  17. const allFilterExcludes = negateFilters(excludes);
  18. // Add configured excludes at the end of filter list
  19. allFilterExcludes.forEach((filterExclude) => {
  20. // Allow all excludes (!) and limit re-includes (+) to the library namespace
  21. if (filterExclude.startsWith("!") || filterExclude.startsWith(`+${namespace}/`)) {
  22. filters.push(filterExclude);
  23. } else {
  24. log.warn(`Configured preload exclude contains invalid re-include: !${filterExclude.substr(1)}. ` +
  25. `Re-includes must start with the library's namespace ${namespace}`);
  26. }
  27. });
  28. }
  29. return filters;
  30. }
  31. function getBundleDefinition(namespace, excludes) {
  32. // Note: This configuration is only used when no bundle definition in ui5.yaml exists (see "skipBundles" parameter)
  33. // TODO: Remove this hardcoded bundle definition once support for relevant versions has ended.
  34. // sap.ui.core ui5.yaml contains a configuration since UI5 1.103.0 (specVersion 2.4)
  35. // so this is still required to build UI5 versions <= 1.102.0 (such as 1.84 and 1.96)
  36. if (namespace === "sap/ui/core") {
  37. return {
  38. name: `${namespace}/library-preload.js`,
  39. sections: [
  40. {
  41. // exclude the content of sap-ui-core by declaring it as 'provided'
  42. mode: "provided",
  43. filters: [
  44. "ui5loader-autoconfig.js",
  45. "sap/ui/core/Core.js"
  46. ],
  47. resolve: true
  48. },
  49. {
  50. mode: "preload",
  51. filters: [
  52. // Note: Don't pass configured preload excludes for sap.ui.core
  53. // as they are already hardcoded below.
  54. ...getDefaultLibraryPreloadFilters(namespace),
  55. `!${namespace}/cldr/`,
  56. "*.js",
  57. "sap/base/",
  58. "sap/ui/base/",
  59. "sap/ui/dom/",
  60. "sap/ui/events/",
  61. "sap/ui/model/",
  62. "sap/ui/security/",
  63. "sap/ui/util/",
  64. "sap/ui/Global.js",
  65. // include only thirdparty that is very likely to be used
  66. "sap/ui/thirdparty/crossroads.js",
  67. "sap/ui/thirdparty/caja-html-sanitizer.js",
  68. "sap/ui/thirdparty/hasher.js",
  69. "sap/ui/thirdparty/signals.js",
  70. "sap/ui/thirdparty/jquery-mobile-custom.js",
  71. "sap/ui/thirdparty/jqueryui/jquery-ui-core.js",
  72. "sap/ui/thirdparty/jqueryui/jquery-ui-position.js",
  73. // other excludes (not required for productive scenarios)
  74. "!sap-ui-*.js",
  75. "!sap/ui/core/support/",
  76. "!sap/ui/core/plugin/DeclarativeSupport.js",
  77. "!sap/ui/core/plugin/LessSupport.js"
  78. ],
  79. resolve: false,
  80. resolveConditional: false,
  81. renderer: true
  82. }
  83. ]
  84. };
  85. }
  86. return {
  87. name: `${namespace}/library-preload.js`,
  88. sections: [
  89. {
  90. mode: "preload",
  91. filters: getDefaultLibraryPreloadFilters(namespace, excludes),
  92. resolve: false,
  93. resolveConditional: false,
  94. renderer: true
  95. }
  96. ]
  97. };
  98. }
  99. function getDesigntimeBundleDefinition(namespace) {
  100. return {
  101. name: `${namespace}/designtime/library-preload.designtime.js`,
  102. sections: [
  103. {
  104. mode: "preload",
  105. filters: [
  106. `${namespace}/**/*.designtime.js`,
  107. `${namespace}/designtime/`,
  108. `!${namespace}/**/*-preload.designtime.js`,
  109. `!${namespace}/designtime/**/*.properties`,
  110. `!${namespace}/designtime/**/*.svg`,
  111. `!${namespace}/designtime/**/*.xml`
  112. ],
  113. resolve: false,
  114. resolveConditional: false,
  115. renderer: false
  116. }
  117. ]
  118. };
  119. }
  120. function getSupportFilesBundleDefinition(namespace) {
  121. return {
  122. name: `${namespace}/library-preload.support.js`,
  123. sections: [
  124. {
  125. mode: "preload",
  126. filters: [
  127. `${namespace}/**/*.support.js`,
  128. `!${namespace}/**/*-preload.support.js`
  129. ],
  130. resolve: false,
  131. resolveConditional: false,
  132. renderer: false
  133. }
  134. ]
  135. };
  136. }
  137. function getModuleBundlerOptions(config) {
  138. const moduleBundlerOptions = {};
  139. // required in sap-ui-core-nojQuery.js and sap-ui-core-nojQuery-dbg.js
  140. const providedSection = {
  141. mode: "provided",
  142. filters: [
  143. "jquery-ui-core.js",
  144. "jquery-ui-datepicker.js",
  145. "jquery-ui-position.js",
  146. "sap/ui/thirdparty/jquery.js",
  147. "sap/ui/thirdparty/jquery/*",
  148. "sap/ui/thirdparty/jqueryui/*"
  149. ]
  150. };
  151. moduleBundlerOptions.bundleOptions = {
  152. optimize: config.preload,
  153. decorateBootstrapModule: config.preload,
  154. addTryCatchRestartWrapper: config.preload
  155. };
  156. moduleBundlerOptions.bundleDefinition = getSapUiCoreBunDef(config.name, config.filters, config.preload);
  157. if (config.provided) {
  158. moduleBundlerOptions.bundleDefinition.sections.unshift(providedSection);
  159. }
  160. if (config.moduleNameMapping) {
  161. moduleBundlerOptions.moduleNameMapping = config.moduleNameMapping;
  162. }
  163. return moduleBundlerOptions;
  164. }
  165. function getSapUiCoreBunDef(name, filters, preload) {
  166. const bundleDefinition = {
  167. name,
  168. sections: []
  169. };
  170. // add raw section
  171. bundleDefinition.sections.push({
  172. // include all 'raw' modules that are needed for the UI5 loader
  173. mode: "raw",
  174. filters,
  175. resolve: true, // dependencies for raw modules are taken from shims in .library files
  176. sort: true, // topological sort on raw modules is mandatory
  177. declareModules: false
  178. });
  179. if (preload) {
  180. // add preload section
  181. bundleDefinition.sections.push({
  182. mode: "preload",
  183. filters: [
  184. "sap/ui/core/Core.js"
  185. ],
  186. resolve: true
  187. });
  188. }
  189. // add require section
  190. bundleDefinition.sections.push({
  191. mode: "require",
  192. filters: [
  193. "sap/ui/core/Core.js"
  194. ]
  195. });
  196. return bundleDefinition;
  197. }
  198. /**
  199. * @public
  200. * @module @ui5/builder/tasks/bundlers/generateLibraryPreload
  201. */
  202. /**
  203. * Task for library bundling.
  204. *
  205. * @public
  206. * @function default
  207. * @static
  208. *
  209. * @param {object} parameters Parameters
  210. * @param {@ui5/fs/DuplexCollection} parameters.workspace DuplexCollection to read and write files
  211. * @param {@ui5/project/build/helpers/TaskUtil} [parameters.taskUtil] TaskUtil
  212. * @param {object} parameters.options Options
  213. * @param {string} parameters.options.projectName Project name
  214. * @param {string[]} [parameters.options.skipBundles] Names of bundles that should not be created
  215. * @param {string[]} [parameters.options.excludes=[]] List of modules declared as glob patterns (resource name patterns)
  216. * that should be excluded from the library-preload.js bundle.
  217. * A pattern ending with a slash '/' will, similarly to the use of a single '*' or double '**' asterisk,
  218. * denote an arbitrary number of characters or folder names.
  219. * Re-includes should be marked with a leading exclamation mark '!'. The order of filters is relevant; a later
  220. * inclusion overrides an earlier exclusion, and vice versa.
  221. * @returns {Promise<undefined>} Promise resolving with <code>undefined</code> once data has been written
  222. */
  223. export default async function({workspace, taskUtil, options: {skipBundles = [], excludes = [], projectName}}) {
  224. let nonDbgWorkspace = workspace;
  225. if (taskUtil) {
  226. nonDbgWorkspace = taskUtil.resourceFactory.createFilterReader({
  227. reader: workspace,
  228. callback: function(resource) {
  229. // Remove any debug variants
  230. return !taskUtil.getTag(resource, taskUtil.STANDARD_TAGS.IsDebugVariant);
  231. }
  232. });
  233. }
  234. const coreVersion = taskUtil?.getProject("sap.ui.core")?.getVersion();
  235. const allowStringBundling = taskUtil?.getProject().getSpecVersion().lt("4.0");
  236. const execModuleBundlerIfNeeded = ({options, resources}) => {
  237. if (skipBundles.includes(options.bundleDefinition.name)) {
  238. log.verbose(`Skipping generation of bundle ${options.bundleDefinition.name}`);
  239. return null;
  240. }
  241. if (coreVersion) {
  242. options.targetUi5CoreVersion = coreVersion;
  243. }
  244. options.bundleDefinition = applyDefaultsToBundleDefinition(options.bundleDefinition, taskUtil);
  245. options.allowStringBundling = allowStringBundling;
  246. return moduleBundler({options, resources});
  247. };
  248. return nonDbgWorkspace.byGlob("/**/*.{js,json,xml,html,properties,library,js.map}").then(async (resources) => {
  249. // Find all libraries and create a library-preload.js bundle
  250. let p = Promise.resolve();
  251. // Create core bundles for older versions (<1.97.0) which don't define bundle configuration in the ui5.yaml
  252. // See: https://github.com/SAP/openui5/commit/ff127fd2d009162ea43ad312dec99d759ebc23a0
  253. if (projectName === "sap.ui.core") {
  254. // Instead of checking the sap.ui.core library version, the specVersion is checked against all versions
  255. // that have been defined for sap.ui.core before the bundle configuration has been introduced.
  256. // This is mainly to have an easier check without version parsing or using semver.
  257. // If no project/specVersion is available, the bundles should also be created to not break potential
  258. // existing use cases without a properly formed/formatted project tree.
  259. if (!taskUtil || taskUtil.getProject().getSpecVersion().lte("2.0")) {
  260. const isEvo = resources.find((resource) => {
  261. return resource.getPath() === "/resources/ui5loader.js";
  262. });
  263. let unoptimizedModuleNameMapping;
  264. let unoptimizedResources = resources;
  265. if (taskUtil) {
  266. const unoptimizedWorkspace = taskUtil.resourceFactory.createFilterReader({
  267. reader: workspace,
  268. callback: function(resource) {
  269. // Remove any non-debug variants
  270. return !taskUtil.getTag(resource, taskUtil.STANDARD_TAGS.HasDebugVariant);
  271. }
  272. });
  273. unoptimizedResources =
  274. await unoptimizedWorkspace.byGlob("/**/*.{js,json,xml,html,properties,library,js.map}");
  275. unoptimizedModuleNameMapping = createModuleNameMapping({
  276. resources: unoptimizedResources,
  277. taskUtil
  278. });
  279. }
  280. let filters;
  281. if (isEvo) {
  282. filters = ["ui5loader-autoconfig.js"];
  283. } else {
  284. filters = ["jquery.sap.global.js"];
  285. }
  286. p = Promise.all([
  287. execModuleBundlerIfNeeded({
  288. options: getModuleBundlerOptions({name: "sap-ui-core.js", filters, preload: true}),
  289. resources
  290. }),
  291. execModuleBundlerIfNeeded({
  292. options: getModuleBundlerOptions({
  293. name: "sap-ui-core-dbg.js", filters, preload: false,
  294. moduleNameMapping: unoptimizedModuleNameMapping
  295. }),
  296. resources: unoptimizedResources
  297. }),
  298. execModuleBundlerIfNeeded({
  299. options: getModuleBundlerOptions({
  300. name: "sap-ui-core-nojQuery.js", filters, preload: true, provided: true
  301. }),
  302. resources
  303. }),
  304. execModuleBundlerIfNeeded({
  305. options: getModuleBundlerOptions({
  306. name: "sap-ui-core-nojQuery-dbg.js", filters, preload: false, provided: true,
  307. moduleNameMapping: unoptimizedModuleNameMapping
  308. }),
  309. resources: unoptimizedResources
  310. }),
  311. ]).then((results) => {
  312. const bundles = Array.prototype.concat.apply([], results).filter(Boolean);
  313. return Promise.all(bundles.map(({bundle, sourceMap}) => {
  314. if (taskUtil) {
  315. taskUtil.setTag(bundle, taskUtil.STANDARD_TAGS.IsBundle);
  316. if (sourceMap) {
  317. // Clear tag that might have been set by the minify task, in cases where
  318. // the bundle name is identical to a source file
  319. taskUtil.clearTag(sourceMap, taskUtil.STANDARD_TAGS.OmitFromBuildResult);
  320. }
  321. }
  322. const writes = [workspace.write(bundle)];
  323. if (sourceMap) {
  324. writes.push(workspace.write(sourceMap));
  325. }
  326. return Promise.all(writes);
  327. }));
  328. });
  329. }
  330. }
  331. return p.then(() => {
  332. return workspace.byGlob("/resources/**/.library").then((libraryIndicatorResources) => {
  333. if (libraryIndicatorResources.length > 0) {
  334. return libraryIndicatorResources;
  335. } else {
  336. // Fallback to "library.js" as library indicator
  337. log.verbose(
  338. `Could not find a ".library" file for project ${projectName}, ` +
  339. `falling back to "library.js".`);
  340. return workspace.byGlob("/resources/**/library.js");
  341. }
  342. }).then((libraryIndicatorResources) => {
  343. if (libraryIndicatorResources.length < 1) {
  344. // No library found - nothing to do
  345. log.verbose(
  346. `Could not find a ".library" or "library.js" file for project ${projectName}. ` +
  347. `Skipping library preload bundling.`);
  348. return;
  349. }
  350. return Promise.all(libraryIndicatorResources.map(async (libraryIndicatorResource) => {
  351. // Determine library namespace from library indicator file path
  352. // ending with either ".library" or "library.js" (see fallback logic above)
  353. // e.g. /resources/sap/foo/.library => sap/foo
  354. // /resources/sap/bar/library.js => sap/bar
  355. const libraryNamespacePattern = /^\/resources\/(.*)\/(?:\.library|library\.js)$/;
  356. const libraryIndicatorPath = libraryIndicatorResource.getPath();
  357. const libraryNamespaceMatch = libraryIndicatorPath.match(libraryNamespacePattern);
  358. if (libraryNamespaceMatch && libraryNamespaceMatch[1]) {
  359. const libraryNamespace = libraryNamespaceMatch[1];
  360. const results = await Promise.all([
  361. execModuleBundlerIfNeeded({
  362. options: {
  363. bundleDefinition: getBundleDefinition(libraryNamespace, excludes),
  364. bundleOptions: {
  365. optimize: true,
  366. ignoreMissingModules: true
  367. }
  368. },
  369. resources
  370. }),
  371. execModuleBundlerIfNeeded({
  372. options: {
  373. bundleDefinition: getDesigntimeBundleDefinition(libraryNamespace),
  374. bundleOptions: {
  375. optimize: true,
  376. ignoreMissingModules: true,
  377. skipIfEmpty: true
  378. }
  379. },
  380. resources
  381. }),
  382. execModuleBundlerIfNeeded({
  383. options: {
  384. bundleDefinition: getSupportFilesBundleDefinition(libraryNamespace),
  385. bundleOptions: {
  386. optimize: false,
  387. ignoreMissingModules: true,
  388. skipIfEmpty: true
  389. }
  390. // Note: Although the bundle uses optimize=false, there is
  391. // no moduleNameMapping needed, as support files are excluded from minification.
  392. },
  393. resources
  394. })
  395. ]);
  396. const bundles = Array.prototype.concat.apply([], results).filter(Boolean);
  397. return Promise.all(bundles.map(({bundle, sourceMap} = {}) => {
  398. if (bundle) {
  399. if (taskUtil) {
  400. taskUtil.setTag(bundle, taskUtil.STANDARD_TAGS.IsBundle);
  401. if (sourceMap) {
  402. // Clear tag that might have been set by the minify task, in cases where
  403. // the bundle name is identical to a source file
  404. taskUtil.clearTag(sourceMap,
  405. taskUtil.STANDARD_TAGS.OmitFromBuildResult);
  406. }
  407. }
  408. const writes = [workspace.write(bundle)];
  409. if (sourceMap) {
  410. writes.push(workspace.write(sourceMap));
  411. }
  412. return Promise.all(writes);
  413. }
  414. }));
  415. } else {
  416. log.verbose(
  417. `Could not determine library namespace from file "${libraryIndicatorPath}" ` +
  418. `for project ${projectName}. Skipping library preload bundling.`);
  419. return Promise.resolve();
  420. }
  421. }));
  422. });
  423. });
  424. });
  425. }