builder/lib/processors/bundlers/moduleBundler.js

import BundleBuilder from "../../lbt/bundle/Builder.js";
import LocatorResourcePool from "../../lbt/resources/LocatorResourcePool.js";
import EvoResource from "@ui5/fs/Resource";
import {getLogger} from "@ui5/logger";
const log = getLogger("builder:processors:bundlers:moduleBundler");

/**
 * @public
 * @module @ui5/builder/processors/bundlers/moduleBundler
 */

/**
 * A ModuleBundleDefinitionSection specifies the embedding mode (either 'provided', 'raw', 'preload', 'require'
 * or 'bundleInfo') and lists the resources that should be in- or excluded from the section.
 * <p>
 * <b>Module bundle section modes</b><br>
 * <ul>
 * 	<li>
 *		<code>provided</code>: A section of mode 'provided' defines a set of modules that should not be included in
 * the bundle file itself, but which should be assumed to be already loaded (or 'provided') by the environment into
 * which the bundle module is loaded.
 * 	</li>
 *	<li>
 *		<code>raw</code>: A 'raw' section determines the set of modules that should be embedded, sorts them according
 *		to their dependencies and writes them out 1:1 without any transformation or wrapping (raw). Only JavaScript
 *		sources can be embedded in a raw section.
 *	</li>
 *	<li>
 *		<code>preload</code>: A 'preload' section packages resources that should be stored in the preload cache in the
 *		client. They can embed any textual resource type (JavaScript, XML, JSON and .properties files) that the
 *		bundling supports. UI5 modules are wrapped into a 'sap.ui.predefine' call. Other JavaScript modules will be
 *		embedded into a 'jQuery.sap.registerPreload' call, or in a "sap.ui.require.preload" call when
 *      the ui5loader is available.
 *	</li>
 *	<li>
 *		<code>require</code>: A 'require' section is transformed into a sequence of jQuery.sap.require calls. The
 *		list will be resolved like an include pattern list in any of the other sections and for each of the resolved
 *		modules, a jQuery.sap.require will be created. In case the ui5loader is available, 'sap.ui.requireSync' is
 *		used instead.
 *	</li>
 *	<li>
 *		<code>bundleInfo</code>: A 'bundleInfo' section describes the content of another named bundle. This information
 *		is transformed into a ui5loader-"bundlesUI5" configuration.
 *		At runtime, if a module is known to be contained in a bundle, the loader will require that bundle before
 *		the module itself.
 *		This requires the ui5loader to be available at build time and UI5 version 1.74.0 or higher at runtime.
 *	</li>
 * </ul>
 * </p>
 *
 * @public
 * @typedef {object} ModuleBundleDefinitionSection
 * @property {string} mode The embedding mode. Either 'provided', 'raw', 'preload', 'require' or 'bundleInfo'
 * @property {string[]} filters List of modules declared as glob patterns (resource name patterns) that should be
 *		in- or excluded.
 * 		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.
 * 		Excludes should be marked with a leading exclamation mark '!'. The order of filters is relevant; a later
 *		exclusion overrides an earlier inclusion, and vice versa.
 * @example <caption>List of modules as glob patterns that should be in- or excluded</caption>
 * // Includes everything from "some/path/to/module/",
 * // but excludes the subfolder "some/path/to/module/to/be/excluded/"
 * const section = {
 * 	"filters": [
 * 		"some/path/to/module/",
 * 		"!some/path/to/module/to/be/excluded/"
 * 	]
 * };
 *
 * @property {boolean} [resolve=false] Whether (transitive) dependencies of modules that match the given filters
 *		should be resolved and added to the module set
 * @property {boolean} [resolveConditional=false] Whether conditional dependencies of modules should be resolved
 * 		and added to the module set for this section
 * @property {boolean} [renderer=false] Whether renderers for controls should be added to the module set
 * @property {boolean} [declareRawModules=false] Whether raw modules should be declared after jQuery.sap.global
 *		became available. With the usage of the ui5loader, this flag should be set to 'false'
 * @property {boolean} [sort=true] Whether the modules should be sorted by their dependencies
 */

/* eslint-disable max-len */
/**
 * Module bundle definition
 *
 * @public
 * @typedef {object} ModuleBundleDefinition
 * @property {string} name The module bundle name
 * @property {string[]} [defaultFileTypes=[".js", ".control.xml", ".fragment.html", ".fragment.json", ".fragment.xml", ".view.html", ".view.json", ".view.xml"]]
 *   List of default file types to be included in the bundle
 * @property {module:@ui5/builder/processors/bundlers/moduleBundler~ModuleBundleDefinitionSection[]} sections List of module bundle definition sections.
 */
/* eslint-enable max-len */

/**
 * Module bundle options
 *
 * @public
 * @typedef {object} ModuleBundleOptions
 * @property {boolean} [optimize=true] Whether the module bundle gets minified
 * @property {boolean} [sourceMap=true] Whether to generate a source map file for the bundle
 * @property {boolean} [decorateBootstrapModule=false] If set to 'false', bootable bundles won't be decorated
 *   with an optimization marker
 * @property {boolean} [addTryCatchRestartWrapper=false] Whether to wrap bootable bundles with
 *   a try/catch to filter out "Restart" errors
 * @property {boolean} [usePredefineCalls=false] If set to 'true', sap.ui.predefine is used for UI5 modules
 * @property {number} [numberOfParts=1] The number of parts the module bundle should be splitted
 * @property {boolean} [ignoreMissingModules=false] When searching for modules which are optional for further
 *   processing, do not throw in case they are missing
 */

/**
 * Result set
 *
 * @public
 * @typedef {object} ModuleBundlerResult
 * @property {@ui5/fs/Resource} bundle Bundle resource
 * @property {@ui5/fs/Resource} sourceMap Source Map
 */

/* eslint-disable max-len */
/**
 * Legacy module bundler.
 *
 * @public
 * @function default
 * @static
 *
 * @param {object} parameters Parameters
 * @param {@ui5/fs/Resource[]} parameters.resources Resources
 * @param {object} parameters.options Options
 * @param {object} [parameters.options.moduleNameMapping]
 				Optional mapping of resource paths to module name in order to overwrite the default determination
 * @param {module:@ui5/builder/processors/bundlers/moduleBundler~ModuleBundleDefinition} parameters.options.bundleDefinition Module
				bundle definition
 * @param {module:@ui5/builder/processors/bundlers/moduleBundler~ModuleBundleOptions} [parameters.options.bundleOptions] Module
				bundle options
 * @returns {Promise<module:@ui5/builder/processors/bundlers/moduleBundler~ModuleBundlerResult[]>}
 * Promise resolving with module bundle resources
 */
/* eslint-enable max-len */
export default function({resources, options: {bundleDefinition, bundleOptions, moduleNameMapping}}) {
	// Apply defaults without modifying the passed object
	bundleOptions = Object.assign({}, {
		optimize: true,
		sourceMap: true,
		decorateBootstrapModule: false,
		addTryCatchRestartWrapper: false,
		usePredefineCalls: false,
		numberOfParts: 1,
		ignoreMissingModules: false
	}, bundleOptions);

	const pool = new LocatorResourcePool({
		ignoreMissingModules: bundleOptions.ignoreMissingModules
	});
	const builder = new BundleBuilder(pool);

	if (log.isLevelEnabled("verbose")) {
		log.verbose(`Generating bundle:`);
		log.verbose(`bundleDefinition: ${JSON.stringify(bundleDefinition, null, 2)}`);
		log.verbose(`bundleOptions: ${JSON.stringify(bundleOptions, null, 2)}`);
	}
	return pool.prepare( resources, moduleNameMapping ).
		then( () => builder.createBundle(bundleDefinition, bundleOptions) ).
		then( (results) => {
			let bundles;
			if (results instanceof Array) {
				bundles = results;
			} else {
				bundles = [results];
			}

			return Promise.all(bundles.map((bundleObj) => {
				if ( bundleObj ) {
					const {name, content, sourceMap} = bundleObj;
					// console.log("creating bundle as '%s'", "/resources/" + name);
					const res = {};
					res.bundle = new EvoResource({
						path: "/resources/" + name,
						string: content
					});
					if (sourceMap) {
						res.sourceMap = new EvoResource({
							path: "/resources/" + name + ".map",
							string: sourceMap
						});
					}
					return res;
				}
			}));
		});
}