builder/lib/processors/bundlers/moduleBundler.js

const BundleBuilder = require("../../lbt/bundle/Builder");
const LocatorResourcePool = require("../../lbt/resources/LocatorResourcePool");
const EvoResource = require("@ui5/fs").Resource;
const log = require("@ui5/logger").getLogger("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 {ModuleBundleDefinitionSection[]} sections List of module bundle definition sections.
 */
/* eslint-enable max-len */

/**
 * Module bundle options
 *
 * @public
 * @typedef {object} ModuleBundleOptions
 * @property {boolean} [optimize=false] If set to 'true' the module bundle gets minified
 * @property {boolean} [decorateBootstrapModule=false] If set to 'false', the module won't be decorated
 *   with an optimization marker
 * @property {boolean} [addTryCatchRestartWrapper=false] Whether to wrap bootable module 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
 */

/**
 * Legacy preload bundler.
 *
 * @public
 * @alias module:@ui5/builder.processors.moduleBundler
 * @param {object} parameters Parameters
 * @param {module:@ui5/fs.Resource[]} parameters.resources Resources
 * @param {object} parameters.options Options
 * @param {ModuleBundleDefinition} parameters.options.bundleDefinition Module bundle definition
 * @param {ModuleBundleOptions} parameters.options.bundleOptions Module bundle options
 * @returns {Promise<module:@ui5/fs.Resource[]>} Promise resolving with module bundle resources
 */
module.exports = function({resources, options: {bundleDefinition, bundleOptions}}) {
//	console.log("preloadBundler bundleDefinition:");
//	console.log(JSON.stringify(options.bundleDefinition, null, 4));

	// TODO 3.0: Fix defaulting behavior, align with JSDoc
	bundleOptions = bundleOptions || {optimize: true};

	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 ).
		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} = bundleObj;
					// console.log("creating bundle as '%s'", "/resources/" + name);
					const resource = new EvoResource({
						path: "/resources/" + name,
						string: content
					});
					return resource;
				}
			}));
		});
};