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..0f8fbaa7f 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"; @@ -32,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) @@ -106,6 +133,69 @@ function getBundleDefinition(namespace, excludes) { }; } +function getBundleInfoPreloadDefinition(namespace, excludes, coreVersion) { + const sections = [{ + mode: "preload", + filters: getExperimentalDefaultLibraryPreloadFilters(namespace, excludes), + 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: getExperimentalDefaultLibraryPreloadFilters(namespace, excludes), + 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 +348,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 +481,99 @@ 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}`); + log.info(`Detected sap.ui.core version is ${coreVersion || "unknown"}`); + // 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 = {