From 476e3bb4e1e60cd0e6694ad8357f0d6b36eb2347 Mon Sep 17 00:00:00 2001 From: Merlin Beutlberger Date: Tue, 28 Oct 2025 14:05:25 +0100 Subject: [PATCH 1/2] [INTERNAL] generateLibraryPreload: Add experimental bundle-info preload --- lib/lbt/bundle/Resolver.js | 3 +- lib/tasks/bundlers/generateLibraryPreload.js | 197 +++++++++++++++---- test/lib/lbt/bundle/AutoSplitter.js | 9 +- 3 files changed, 169 insertions(+), 40 deletions(-) diff --git a/lib/lbt/bundle/Resolver.js b/lib/lbt/bundle/Resolver.js index 45c3e7f32..54977c0d5 100644 --- a/lib/lbt/bundle/Resolver.js +++ b/lib/lbt/bundle/Resolver.js @@ -56,6 +56,7 @@ class BundleResolver { * in the resource pool. */ const missingModules = Object.create(null); + const ignoreMissingModules = pool.getIgnoreMissingModules(); /** * Names of modules that are included in non-decomposable bundles. * If they occur in the missingModules, then this is not an error. @@ -123,7 +124,7 @@ class BundleResolver { done = pool.findResourceWithInfo(resourceName) .catch( (err) => { // if the caller provided an error message, log it - if ( msg ) { + if ( msg && !ignoreMissingModules ) { missingModules[resourceName] ??= []; missingModules[resourceName].push(msg); } diff --git a/lib/tasks/bundlers/generateLibraryPreload.js b/lib/tasks/bundlers/generateLibraryPreload.js index c81f6f081..03dbb7be4 100644 --- a/lib/tasks/bundlers/generateLibraryPreload.js +++ b/lib/tasks/bundlers/generateLibraryPreload.js @@ -1,3 +1,4 @@ +import semver from "semver"; import {getLogger} from "@ui5/logger"; const log = getLogger("builder:tasks:bundlers:generateLibraryPreload"); import moduleBundler from "../../processors/bundlers/moduleBundler.js"; @@ -106,6 +107,73 @@ function getBundleDefinition(namespace, excludes) { }; } +function getBundleInfoPreloadDefinition(namespace, excludes, coreVersion) { + const sections = [{ + mode: "preload", + filters: [ + `${namespace}/library.js`, + ], + resolve: true + }, + { + mode: "bundleInfo", + name: `${namespace}/library-content.js`, + filters: getDefaultLibraryPreloadFilters(namespace, excludes), + resolve: false, + resolveConditional: false, + renderer: true + }]; + + if (coreVersion) { + const parsedVersion = semver.parse(coreVersion); + let targetUi5CoreVersionMajor = parsedVersion.major; + + // legacy-free versions include changes of the upcoming major version + // so we should treat them the same as the next major version + if ( + parsedVersion.prerelease.includes("legacy-free") || + parsedVersion.prerelease.includes("legacy-free-SNAPSHOT") // Maven snapshot version + ) { + targetUi5CoreVersionMajor += 1; + } + if (parsedVersion) { + if (targetUi5CoreVersionMajor >= 2) { + // Do not include manifest.json in UI5 2.x and higher to allow for loading it upfront for all libraries + sections.unshift({ + mode: "provided", + filters: [ + `${namespace}/manifest.json`, + ] + }); + } + } + } + + return { + name: `${namespace}/library-preload.js`, + sections, + }; +} + +function getContentBundleDefinition(namespace, excludes) { + return { + name: `${namespace}/library-content.js`, + sections: [{ + mode: "provided", + filters: [ + `${namespace}/library.js`, + ], + resolve: true + }, { + mode: "preload", + filters: getDefaultLibraryPreloadFilters(namespace, excludes), + resolve: false, + resolveConditional: false, + renderer: true + }] + }; +} + function getDesigntimeBundleDefinition(namespace) { return { name: `${namespace}/designtime/library-preload.designtime.js`, @@ -258,6 +326,7 @@ export default async function({workspace, taskUtil, options: {skipBundles = [], } const coreVersion = taskUtil?.getProject("sap.ui.core")?.getVersion(); const allowStringBundling = taskUtil?.getProject().getSpecVersion().lt("4.0"); + const createBundleInfoPreload = !!process.env.UI5_CLI_EXPERIMENTAL_BUNDLE_INFO_PRELOAD; const execModuleBundlerIfNeeded = ({options, resources}) => { if (skipBundles.includes(options.bundleDefinition.name)) { log.verbose(`Skipping generation of bundle ${options.bundleDefinition.name}`); @@ -390,42 +459,98 @@ export default async function({workspace, taskUtil, options: {skipBundles = [], const libraryNamespaceMatch = libraryIndicatorPath.match(libraryNamespacePattern); if (libraryNamespaceMatch && libraryNamespaceMatch[1]) { const libraryNamespace = libraryNamespaceMatch[1]; - const results = await Promise.all([ - execModuleBundlerIfNeeded({ - options: { - bundleDefinition: getBundleDefinition(libraryNamespace, excludes), - bundleOptions: { - optimize: true, - ignoreMissingModules: true - } - }, - resources - }), - execModuleBundlerIfNeeded({ - options: { - bundleDefinition: getDesigntimeBundleDefinition(libraryNamespace), - bundleOptions: { - optimize: true, - ignoreMissingModules: true, - skipIfEmpty: true - } - }, - resources - }), - execModuleBundlerIfNeeded({ - options: { - bundleDefinition: getSupportFilesBundleDefinition(libraryNamespace), - bundleOptions: { - optimize: false, - ignoreMissingModules: true, - skipIfEmpty: true - } - // Note: Although the bundle uses optimize=false, there is - // no moduleNameMapping needed, as support files are excluded from minification. - }, - resources - }) - ]); + let results; + if (!createBundleInfoPreload) { + // Regular bundling + results = await Promise.all([ + execModuleBundlerIfNeeded({ + options: { + bundleDefinition: getBundleDefinition(libraryNamespace, excludes), + bundleOptions: { + optimize: true, + ignoreMissingModules: true + } + }, + resources + }), + execModuleBundlerIfNeeded({ + options: { + bundleDefinition: getDesigntimeBundleDefinition(libraryNamespace), + bundleOptions: { + optimize: true, + ignoreMissingModules: true, + skipIfEmpty: true + } + }, + resources + }), + execModuleBundlerIfNeeded({ + options: { + bundleDefinition: getSupportFilesBundleDefinition(libraryNamespace), + bundleOptions: { + optimize: false, + ignoreMissingModules: true, + skipIfEmpty: true + } + // Note: Although the bundle uses optimize=false, there is + // no moduleNameMapping needed, as support files are excluded from minification. + }, + resources + }) + ]); + } else { + log.info( + `Using experimental bundling with bundle info preload ` + + `for library ${libraryNamespace} in project ${projectName}.`); + // Experimental bundling with bundle info preload + results = await Promise.all([ + execModuleBundlerIfNeeded({ + options: { + bundleDefinition: + getBundleInfoPreloadDefinition(libraryNamespace, excludes, coreVersion), + bundleOptions: { + optimize: true, + ignoreMissingModules: true + } + }, + resources + }), + execModuleBundlerIfNeeded({ + options: { + bundleDefinition: getContentBundleDefinition(libraryNamespace, excludes), + bundleOptions: { + optimize: true, + ignoreMissingModules: true + } + }, + resources + }), + execModuleBundlerIfNeeded({ + options: { + bundleDefinition: getDesigntimeBundleDefinition(libraryNamespace), + bundleOptions: { + optimize: true, + ignoreMissingModules: true, + skipIfEmpty: true + } + }, + resources + }), + execModuleBundlerIfNeeded({ + options: { + bundleDefinition: getSupportFilesBundleDefinition(libraryNamespace), + bundleOptions: { + optimize: false, + ignoreMissingModules: true, + skipIfEmpty: true + } + // Note: Although the bundle uses optimize=false, there is + // no moduleNameMapping needed, as support files are excluded from minification. + }, + resources + }) + ]); + } const bundles = Array.prototype.concat.apply([], results).filter(Boolean); return Promise.all(bundles.map(({bundle, sourceMap} = {}) => { if (bundle) { diff --git a/test/lib/lbt/bundle/AutoSplitter.js b/test/lib/lbt/bundle/AutoSplitter.js index 0cab42294..d042d19a7 100644 --- a/test/lib/lbt/bundle/AutoSplitter.js +++ b/test/lib/lbt/bundle/AutoSplitter.js @@ -38,7 +38,8 @@ function createMockPool(dependencies) { name: "x.view.xml" }, { name: "c.properties" - }] + }], + getIgnoreMissingModules: () => false, }; } @@ -165,7 +166,8 @@ test("integration: Extreme AutoSplitter with numberOfParts 50", async (t) => { }); return {info}; }, - resources: modules.map((res) => ({name: res})) + resources: modules.map((res) => ({name: res})), + getIgnoreMissingModules: () => false, }; const autoSplitter = new AutoSplitter(pool, new BundleResolver(pool)); const bundleDefinition = { @@ -208,7 +210,8 @@ test("integration: AutoSplitter with bundleInfo", async (t) => { const info = new ModuleInfo(name); return {info}; }, - resources: modules.map((res) => ({name: res})) + resources: modules.map((res) => ({name: res})), + getIgnoreMissingModules: () => false, }; const autoSplitter = new AutoSplitter(pool, new BundleResolver(pool)); const bundleDefinition = { From 44d254802c7073ab365434c6c1b997137ba43ad6 Mon Sep 17 00:00:00 2001 From: Merlin Beutlberger Date: Thu, 30 Oct 2025 14:05:42 +0100 Subject: [PATCH 2/2] [INTERNAL] generateLibraryPreload: Also apply default filters to experimental library-preload --- lib/tasks/bundlers/generateLibraryPreload.js | 37 ++++++++++++++++---- 1 file changed, 30 insertions(+), 7 deletions(-) diff --git a/lib/tasks/bundlers/generateLibraryPreload.js b/lib/tasks/bundlers/generateLibraryPreload.js index 03dbb7be4..0f8fbaa7f 100644 --- a/lib/tasks/bundlers/generateLibraryPreload.js +++ b/lib/tasks/bundlers/generateLibraryPreload.js @@ -33,6 +33,32 @@ function getDefaultLibraryPreloadFilters(namespace, excludes) { return filters; } +function getExperimentalDefaultLibraryPreloadFilters(namespace, excludes) { + const filters = [ + `${namespace}/library.js`, + `!${namespace}/**/*-preload.js`, // exclude all bundles + `!${namespace}/designtime/`, + `!${namespace}/**/*.designtime.js`, + `!${namespace}/**/*.support.js` + ]; + + if (Array.isArray(excludes)) { + const allFilterExcludes = negateFilters(excludes); + // Add configured excludes at the end of filter list + allFilterExcludes.forEach((filterExclude) => { + // Allow all excludes (!) and limit re-includes (+) to the library namespace + if (filterExclude.startsWith("!") || filterExclude.startsWith(`+${namespace}/`)) { + filters.push(filterExclude); + } else { + log.warn(`Configured preload exclude contains invalid re-include: !${filterExclude.substr(1)}. ` + + `Re-includes must start with the library's namespace ${namespace}`); + } + }); + } + + return filters; +} + function getBundleDefinition(namespace, excludes) { // Note: This configuration is only used when no bundle definition in ui5.yaml exists (see "skipBundles" parameter) @@ -110,9 +136,7 @@ function getBundleDefinition(namespace, excludes) { function getBundleInfoPreloadDefinition(namespace, excludes, coreVersion) { const sections = [{ mode: "preload", - filters: [ - `${namespace}/library.js`, - ], + filters: getExperimentalDefaultLibraryPreloadFilters(namespace, excludes), resolve: true }, { @@ -160,9 +184,7 @@ function getContentBundleDefinition(namespace, excludes) { name: `${namespace}/library-content.js`, sections: [{ mode: "provided", - filters: [ - `${namespace}/library.js`, - ], + filters: getExperimentalDefaultLibraryPreloadFilters(namespace, excludes), resolve: true }, { mode: "preload", @@ -501,7 +523,8 @@ export default async function({workspace, taskUtil, options: {skipBundles = [], } else { log.info( `Using experimental bundling with bundle info preload ` + - `for library ${libraryNamespace} in project ${projectName}.`); + `for library ${libraryNamespace} in project ${projectName}`); + log.info(`Detected sap.ui.core version is ${coreVersion || "unknown"}`); // Experimental bundling with bundle info preload results = await Promise.all([ execModuleBundlerIfNeeded({