UNPKG

@ui5/builder

Version:
625 lines (579 loc) 20.8 kB
import semver from "semver"; import {getLogger} from "@ui5/logger"; const log = getLogger("builder:tasks:bundlers:generateLibraryPreload"); import moduleBundler from "../../processors/bundlers/moduleBundler.js"; import {applyDefaultsToBundleDefinition} from "./utils/applyDefaultsToBundleDefinition.js"; import {negateFilters} from "../../lbt/resources/ResourceFilterList.js"; import createModuleNameMapping from "./utils/createModuleNameMapping.js"; function getDefaultLibraryPreloadFilters(namespace, excludes) { const filters = [ `${namespace}/`, `${namespace}/**/manifest.json`, `!${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 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) // TODO: Remove this hardcoded bundle definition once support for relevant versions has ended. // sap.ui.core ui5.yaml contains a configuration since UI5 1.103.0 (specVersion 2.4) // so this is still required to build UI5 versions <= 1.102.0 (such as 1.84 and 1.96) if (namespace === "sap/ui/core") { return { name: `${namespace}/library-preload.js`, sections: [ { // exclude the content of sap-ui-core by declaring it as 'provided' mode: "provided", filters: [ "ui5loader-autoconfig.js", "sap/ui/core/Core.js" ], resolve: true }, { mode: "preload", filters: [ // Note: Don't pass configured preload excludes for sap.ui.core // as they are already hardcoded below. ...getDefaultLibraryPreloadFilters(namespace), `!${namespace}/cldr/`, "*.js", "sap/base/", "sap/ui/base/", "sap/ui/dom/", "sap/ui/events/", "sap/ui/model/", "sap/ui/security/", "sap/ui/util/", "sap/ui/Global.js", // include only thirdparty that is very likely to be used "sap/ui/thirdparty/crossroads.js", "sap/ui/thirdparty/caja-html-sanitizer.js", "sap/ui/thirdparty/hasher.js", "sap/ui/thirdparty/signals.js", "sap/ui/thirdparty/jquery-mobile-custom.js", "sap/ui/thirdparty/jqueryui/jquery-ui-core.js", "sap/ui/thirdparty/jqueryui/jquery-ui-position.js", // other excludes (not required for productive scenarios) "!sap-ui-*.js", "!sap/ui/core/support/", "!sap/ui/core/plugin/DeclarativeSupport.js", "!sap/ui/core/plugin/LessSupport.js" ], resolve: false, resolveConditional: false, renderer: true } ] }; } return { name: `${namespace}/library-preload.js`, sections: [ { mode: "preload", filters: getDefaultLibraryPreloadFilters(namespace, excludes), resolve: false, resolveConditional: false, renderer: true } ] }; } 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`, sections: [ { mode: "preload", filters: [ `${namespace}/**/*.designtime.js`, `${namespace}/designtime/`, `!${namespace}/**/*-preload.designtime.js`, `!${namespace}/designtime/**/*.properties`, `!${namespace}/designtime/**/*.svg`, `!${namespace}/designtime/**/*.xml` ], resolve: false, resolveConditional: false, renderer: false } ] }; } function getSupportFilesBundleDefinition(namespace) { return { name: `${namespace}/library-preload.support.js`, sections: [ { mode: "preload", filters: [ `${namespace}/**/*.support.js`, `!${namespace}/**/*-preload.support.js` ], resolve: false, resolveConditional: false, renderer: false } ] }; } function getModuleBundlerOptions(config) { const moduleBundlerOptions = {}; // required in sap-ui-core-nojQuery.js and sap-ui-core-nojQuery-dbg.js const providedSection = { mode: "provided", filters: [ "jquery-ui-core.js", "jquery-ui-datepicker.js", "jquery-ui-position.js", "sap/ui/thirdparty/jquery.js", "sap/ui/thirdparty/jquery/*", "sap/ui/thirdparty/jqueryui/*" ] }; moduleBundlerOptions.bundleOptions = { optimize: config.preload, decorateBootstrapModule: config.preload, addTryCatchRestartWrapper: config.preload }; moduleBundlerOptions.bundleDefinition = getSapUiCoreBunDef(config.name, config.filters, config.preload); if (config.provided) { moduleBundlerOptions.bundleDefinition.sections.unshift(providedSection); } if (config.moduleNameMapping) { moduleBundlerOptions.moduleNameMapping = config.moduleNameMapping; } return moduleBundlerOptions; } function getSapUiCoreBunDef(name, filters, preload) { const bundleDefinition = { name, sections: [] }; // add raw section bundleDefinition.sections.push({ // include all 'raw' modules that are needed for the UI5 loader mode: "raw", filters, resolve: true, // dependencies for raw modules are taken from shims in .library files sort: true, // topological sort on raw modules is mandatory declareModules: false }); if (preload) { // add preload section bundleDefinition.sections.push({ mode: "preload", filters: [ "sap/ui/core/Core.js" ], resolve: true }); } // add require section bundleDefinition.sections.push({ mode: "require", filters: [ "sap/ui/core/Core.js" ] }); return bundleDefinition; } /** * @public * @module @ui5/builder/tasks/bundlers/generateLibraryPreload */ /** * Task for library bundling. * * @public * @function default * @static * * @param {object} parameters Parameters * @param {@ui5/fs/DuplexCollection} parameters.workspace DuplexCollection to read and write files * @param {@ui5/project/build/helpers/TaskUtil} [parameters.taskUtil] TaskUtil * @param {object} parameters.options Options * @param {string} parameters.options.projectName Project name * @param {string[]} [parameters.options.skipBundles] Names of bundles that should not be created * @param {string[]} [parameters.options.excludes=[]] List of modules declared as glob patterns (resource name patterns) * that should be excluded from the library-preload.js bundle. * A pattern ending with a slash '/' will, similarly to the use of a single '*' or double '**' asterisk, * denote an arbitrary number of characters or folder names. * Re-includes should be marked with a leading exclamation mark '!'. The order of filters is relevant; a later * inclusion overrides an earlier exclusion, and vice versa. * @returns {Promise<undefined>} Promise resolving with <code>undefined</code> once data has been written */ export default async function({workspace, taskUtil, options: {skipBundles = [], excludes = [], projectName}}) { let nonDbgWorkspace = workspace; if (taskUtil) { nonDbgWorkspace = taskUtil.resourceFactory.createFilterReader({ reader: workspace, callback: function(resource) { // Remove any debug variants return !taskUtil.getTag(resource, taskUtil.STANDARD_TAGS.IsDebugVariant); } }); } 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}`); return null; } if (coreVersion) { options.targetUi5CoreVersion = coreVersion; } options.bundleDefinition = applyDefaultsToBundleDefinition(options.bundleDefinition, taskUtil); options.allowStringBundling = allowStringBundling; return moduleBundler({options, resources}); }; return nonDbgWorkspace.byGlob("/**/*.{js,json,xml,html,properties,library,js.map}").then(async (resources) => { // Find all libraries and create a library-preload.js bundle let p = Promise.resolve(); // Create core bundles for older versions (<1.97.0) which don't define bundle configuration in the ui5.yaml // See: https://github.com/SAP/openui5/commit/ff127fd2d009162ea43ad312dec99d759ebc23a0 if (projectName === "sap.ui.core") { // Instead of checking the sap.ui.core library version, the specVersion is checked against all versions // that have been defined for sap.ui.core before the bundle configuration has been introduced. // This is mainly to have an easier check without version parsing or using semver. // If no project/specVersion is available, the bundles should also be created to not break potential // existing use cases without a properly formed/formatted project tree. if (!taskUtil || taskUtil.getProject().getSpecVersion().lte("2.0")) { const isEvo = resources.find((resource) => { return resource.getPath() === "/resources/ui5loader.js"; }); let unoptimizedModuleNameMapping; let unoptimizedResources = resources; if (taskUtil) { const unoptimizedWorkspace = taskUtil.resourceFactory.createFilterReader({ reader: workspace, callback: function(resource) { // Remove any non-debug variants return !taskUtil.getTag(resource, taskUtil.STANDARD_TAGS.HasDebugVariant); } }); unoptimizedResources = await unoptimizedWorkspace.byGlob("/**/*.{js,json,xml,html,properties,library,js.map}"); unoptimizedModuleNameMapping = createModuleNameMapping({ resources: unoptimizedResources, taskUtil }); } let filters; if (isEvo) { filters = ["ui5loader-autoconfig.js"]; } else { filters = ["jquery.sap.global.js"]; } p = Promise.all([ execModuleBundlerIfNeeded({ options: getModuleBundlerOptions({name: "sap-ui-core.js", filters, preload: true}), resources }), execModuleBundlerIfNeeded({ options: getModuleBundlerOptions({ name: "sap-ui-core-dbg.js", filters, preload: false, moduleNameMapping: unoptimizedModuleNameMapping }), resources: unoptimizedResources }), execModuleBundlerIfNeeded({ options: getModuleBundlerOptions({ name: "sap-ui-core-nojQuery.js", filters, preload: true, provided: true }), resources }), execModuleBundlerIfNeeded({ options: getModuleBundlerOptions({ name: "sap-ui-core-nojQuery-dbg.js", filters, preload: false, provided: true, moduleNameMapping: unoptimizedModuleNameMapping }), resources: unoptimizedResources }), ]).then((results) => { const bundles = Array.prototype.concat.apply([], results).filter(Boolean); return Promise.all(bundles.map(({bundle, sourceMap}) => { if (taskUtil) { taskUtil.setTag(bundle, taskUtil.STANDARD_TAGS.IsBundle); if (sourceMap) { // Clear tag that might have been set by the minify task, in cases where // the bundle name is identical to a source file taskUtil.clearTag(sourceMap, taskUtil.STANDARD_TAGS.OmitFromBuildResult); } } const writes = [workspace.write(bundle)]; if (sourceMap) { writes.push(workspace.write(sourceMap)); } return Promise.all(writes); })); }); } } return p.then(() => { return workspace.byGlob("/resources/**/.library").then((libraryIndicatorResources) => { if (libraryIndicatorResources.length > 0) { return libraryIndicatorResources; } else { // Fallback to "library.js" as library indicator log.verbose( `Could not find a ".library" file for project ${projectName}, ` + `falling back to "library.js".`); return workspace.byGlob("/resources/**/library.js"); } }).then((libraryIndicatorResources) => { if (libraryIndicatorResources.length < 1) { // No library found - nothing to do log.verbose( `Could not find a ".library" or "library.js" file for project ${projectName}. ` + `Skipping library preload bundling.`); return; } return Promise.all(libraryIndicatorResources.map(async (libraryIndicatorResource) => { // Determine library namespace from library indicator file path // ending with either ".library" or "library.js" (see fallback logic above) // e.g. /resources/sap/foo/.library => sap/foo // /resources/sap/bar/library.js => sap/bar const libraryNamespacePattern = /^\/resources\/(.*)\/(?:\.library|library\.js)$/; const libraryIndicatorPath = libraryIndicatorResource.getPath(); const libraryNamespaceMatch = libraryIndicatorPath.match(libraryNamespacePattern); if (libraryNamespaceMatch && libraryNamespaceMatch[1]) { const libraryNamespace = libraryNamespaceMatch[1]; 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"}`); if (skipBundles.includes(`${libraryNamespace}/library-preload.js`) && !skipBundles.includes(`${libraryNamespace}/_library-content.js`)) { // If the standard preload bundle is skipped, ensure to also skip the content bundle, // since they depend on each other skipBundles.push(`${libraryNamespace}/_library-content.js`); } if (skipBundles.includes(`${libraryNamespace}/_library-content.js`) && !skipBundles.includes(`${libraryNamespace}/library-preload.js`)) { // If the content bundle is skipped, the default preload bundle must be skipped as well throw new Error( `A custom bundle '${libraryNamespace}/_library-content.js' has been defined, ` + `but it also requires a corresponding custom bundle definition for ` + `'${libraryNamespace}/library-preload.js'`); } // 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) { if (taskUtil) { taskUtil.setTag(bundle, taskUtil.STANDARD_TAGS.IsBundle); if (sourceMap) { // Clear tag that might have been set by the minify task, in cases where // the bundle name is identical to a source file taskUtil.clearTag(sourceMap, taskUtil.STANDARD_TAGS.OmitFromBuildResult); } } const writes = [workspace.write(bundle)]; if (sourceMap) { writes.push(workspace.write(sourceMap)); } return Promise.all(writes); } })); } else { log.verbose( `Could not determine library namespace from file "${libraryIndicatorPath}" ` + `for project ${projectName}. Skipping library preload bundling.`); return Promise.resolve(); } })); }); }); }); }