import {getLogger} from "@ui5/logger";
const log = getLogger("builder:tasks:bundlers:generateLibraryPreload");
import moduleBundler from "../../processors/bundlers/moduleBundler.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 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.
// 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.
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 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,
usePredefineCalls: 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 execModuleBundlerIfNeeded = ({options, resources}) => {
if (skipBundles.includes(options.bundleDefinition.name)) {
log.verbose(`Skipping generation of bundle ${options.bundleDefinition.name}`);
return null;
}
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];
const results = await Promise.all([
execModuleBundlerIfNeeded({
options: {
bundleDefinition: getBundleDefinition(libraryNamespace, excludes),
bundleOptions: {
optimize: true,
usePredefineCalls: true,
ignoreMissingModules: true
}
},
resources
}),
execModuleBundlerIfNeeded({
options: {
bundleDefinition: getDesigntimeBundleDefinition(libraryNamespace),
bundleOptions: {
optimize: true,
usePredefineCalls: true,
ignoreMissingModules: true,
skipIfEmpty: true
}
},
resources
}),
execModuleBundlerIfNeeded({
options: {
bundleDefinition: getSupportFilesBundleDefinition(libraryNamespace),
bundleOptions: {
optimize: false,
usePredefineCalls: true,
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();
}
}));
});
});
});
}