UNPKG

@openui5/sap.ui.core

Version:

OpenUI5 Core Library sap.ui.core

1,502 lines (1,311 loc) 123 kB
/*! * OpenUI5 * (c) Copyright 2009-2023 SAP SE or an SAP affiliate company. * Licensed under the Apache License, Version 2.0 - see LICENSE.txt. */ /* * IMPORTANT NOTICE * With 1.54, ui5loader.js and its new features are not yet a public API. * The loader must only be used via the well-known and documented UI5 APIs * such as sap.ui.define, sap.ui.require, etc. * Any direct usage of ui5loader.js or its features is not supported and * might break in future releases. */ /*global sap:true, Blob, console, document, Promise, URL, XMLHttpRequest */ (function(__global) { "use strict"; if ( __global.Promise === undefined || !__global.Promise.prototype.finally || __global.URLSearchParams === undefined ) { var page = document.documentElement, pageStyle = page.style, msg = "Microsoft Internet Explorer 11 and other legacy browsers are no longer supported. For more information, see ", hrefText = "Internet Explorer 11 will no longer be supported by various SAP UI technologies in newer releases", href = "https://blogs.sap.com/2021/02/02/internet-explorer-11-will-no-longer-be-supported-by-various-sap-ui-technologies-in-newer-releases/"; page.innerHTML = '<body style="margin:0;padding:0;overflow-y:hidden;background-color:#f7f7f7;text-align:center;width:100%;position:absolute;top:50%;transform:translate(0,-50%);"><div style="color:#32363a;font-family:Arial,Helvetica,sans-serif;font-size:.875rem;">' + msg + '<a href="' + href + '" style="color:#4076b4;">' + hrefText + '</a>.</div></body>'; pageStyle.margin = pageStyle.padding = "0"; pageStyle.width = pageStyle.height = "100%"; if (__global.stop) { // Check for __global.stop first because Safari 11 has __global.stop and document.execCommand but document.execCommand('Stop') does not work in Safari 11 __global.stop(); } else { document.execCommand('Stop'); } throw new Error(msg + href); } }(window)); (function(__global) { "use strict"; /* * Helper function that removes any query and/or hash parts from the given URL. * * @param {string} href URL to remove query and hash from * @returns {string} */ function pathOnly(href) { var p = href.search(/[?#]/); return p < 0 ? href : href.slice(0, p); } /** * Resolve a given URL, either against the base URL of the current document or against a given base URL. * * If no base URL is given, the URL will be resolved relative to the baseURI of the current document. * If a base URL is given, that base will first be resolved relative to the document's baseURI, * then the URL will be resolved relative to the resolved base. * * Search parameters or a hash of the chosen base will be ignored. * * @param {string} sURI Relative or absolute URL that should be resolved * @param {string} [sBase=document.baseURI] Base URL relative to which the URL should be resolved * @returns {string} Resolved URL */ function resolveURL(sURI, sBase) { sBase = pathOnly(sBase ? resolveURL(sBase) : document.baseURI); return new URL(sURI, sBase).href; } // ---- helpers ------------------------------------------------------------------------------- function noop() {} function forEach(obj, callback) { Object.keys(obj).forEach(function(key) { callback(key, obj[key]); }); } function executeInSeparateTask(fn) { setTimeout(fn, 0); } function executeInMicroTask(fn) { Promise.resolve().then(fn); } // ---- hooks & configuration ----------------------------------------------------------------- /** * Log functionality. * * Can be set to an object with the methods shown below (subset of sap/base/Log). * Logging methods never must fail. Should they ever throw errors, then the internal state * of the loader will be broken. * * By default, all methods are implemented as NOOPs. * * @type {{debug:function(),info:function(),warning:function(),error:function(),isLoggable:function():boolean}} * @private */ var log = { debug: noop, info: noop, warning: noop, error: noop, isLoggable: noop }; // Null Object pattern: dummy logger which is used as long as no logger is injected /** * Basic assert functionality. * * Can be set to a function that gets a value (the expression to be asserted) as first * parameter and a message as second parameter. When the expression coerces to false, * the assertion is violated and the message should be emitted (logged, thrown, whatever). * * By default, this is implemented as a NOOP. * @type {function(any,string)} * @private */ var assert = noop; // Null Object pattern: dummy assert which is used as long as no assert is injected /** * Callback for performance measurement. * * When set, it must be an object with methods <code>start</code> and <code>end</code>. * @type {{start:function(string,any),end:function(string)}} * @private */ var measure; /** * Source code transformation hook. * * To be used by code coverage, only supported in sync mode. * @private */ var translate; /** * Method used by sap.ui.require to simulate asynchronous behavior. * * The default executes the given function in a separate browser task. * Can be changed to execute in a micro task to save idle time in case of * many nested sap.ui.require calls. */ var simulateAsyncCallback = executeInSeparateTask; /* * Activates strictest possible compliance with AMD spec * - no multiple executions of the same module * - at most one anonymous module definition per file, zero for adhoc definitions */ var strictModuleDefinitions = true; /** * Whether asynchronous loading can be used at all. * When activated, require will load asynchronously, else synchronously. * @type {boolean} * @private */ var bGlobalAsyncMode = false; /** * Whether ui5loader currently exposes its AMD implementation as global properties * <code>define</code> and <code>require</code>. Defaults to <code>false</code>. * @type {boolean} * @private */ var bExposeAsAMDLoader = false; /** * How the loader should react to calls of sync APIs or when global names are accessed: * 0: tolerate * 1: warn * 2: reject * @type {int} * @private */ var syncCallBehavior = 0; /** * Default base URL for modules, used when no other configuration is provided. * In case the base url is removed via <code>registerResourcePath("", null)</code> * it will be reset to this URL instead. * @const * @type {string} * @private */ var DEFAULT_BASE_URL = "./"; /** * Temporarily saved reference to the original value of the global define variable. * * @type {any} * @private */ var vOriginalDefine; /** * Temporarily saved reference to the original value of the global require variable. * * @type {any} * @private */ var vOriginalRequire; /** * A map of URL prefixes keyed by the corresponding module name prefix. * * Note that the empty prefix ('') will always match and thus serves as a fallback. * See {@link sap.ui.loader.config}, option <code>paths</code>. * @type {Object<string,{url:string,absoluteUrl:string}>} * @private */ var mUrlPrefixes = Object.create(null); mUrlPrefixes[''] = { url: DEFAULT_BASE_URL, absoluteUrl: resolveURL(DEFAULT_BASE_URL) }; /** * Mapping of module IDs. * * Each entry is a map of its own, keyed by the module ID prefix for which it should be * applied. Each contained map maps module ID prefixes to module ID prefixes. * * All module ID prefixes must not have extensions. * @type {Object.<string,Object.<string,string>>} * @private */ var mMaps = Object.create(null), /** * Information about third party modules, keyed by the module's resource name (including extension '.js'). * * Each module shim object can have the following properties: * <ul> * <li><i>boolean</i>: [amd=false] Whether the module uses an AMD loader if present. If set to <code>true</code>, * UI5 will disable an AMD loader while loading such a module to force the module to expose its content * via global names.</li> * <li><i>string[]|string</i>: [exports=undefined] Global name (or names) that are exported by the module. * If one ore multiple names are defined, the first one will be read from the global object and will be * used as value of the module. Each name can be a dot separated hierarchical name (will be resolved with * <code>getGlobalProperty</code>)</li> * <li><i>string[]</i>: [deps=undefined] List of modules that the module depends on. The modules will be loaded * first before loading the module itself. Note that the stored dependencies also include the extension '.js' * for easier evaluation, but <code>config({shim:...})</code> expects them without the extension for * compatibility with the AMD-JS specification.</li> * </ul> * * @see config method * @type {Object.<string,{amd:boolean,exports:(string|string[]),deps:string[]}>} * @private */ mShims = Object.create(null), /** * Dependency Cache information. * Maps the name of a module to a list of its known dependencies. * @type {Object.<string,string[]>} * @private */ mDepCache = Object.create(null), /** * Whether the loader should try to load debug sources. * @type {boolean} * @private */ bDebugSources = false, /** * Indicates partial or total debug mode. * * Can be set to a function which checks whether preloads should be ignored for the given module. * If undefined, all preloads will be used. * @type {function(string):boolean|undefined} * @private */ fnIgnorePreload; // ---- internal state ------------------------------------------------------------------------ /** * Map of modules that have been loaded or required so far, keyed by their name. * * @type {Object<string,Module>} * @private */ var mModules = Object.create(null), /** * Whether (sap.ui.)define calls must be executed synchronously in the current context. * * The initial value is <code>null</code>. During the execution of a module loading operation * ((sap.ui.)require or (sap.ui.)define etc.), it is set to true or false depending on the * legacy synchronicity behavior of the operation. * * Problem: when AMD modules are loaded with hard coded script tags and when some later inline * script expects the module export synchronously, then the (sap.ui.)define must be executed * synchronously. * Most prominent example: unit tests that include QUnitUtils as a script tag and use qutils * in one of their inline scripts. * @type {boolean} * @private */ bForceSyncDefines = null, /** * Stack of modules that are currently being executed in case of synchronous processing. * * Allows to identify the executing module (e.g. when resolving dependencies or in case of * in case of bundles like sap-ui-core). * * @type {Array.<{name:string,used:boolean}>} * @private */ _execStack = [ ], /** * A prefix that will be added to module loading log statements and which reflects the nesting of module executions. * @type {string} * @private */ sLogPrefix = "", /** * Counter used to give anonymous modules a unique module ID. * @type {int} * @private */ iAnonymousModuleCount = 0; // ---- break preload execution into tasks ---------------------------------------------------- /** * Default value for `iMaxTaskDuration`. * * A value of -1 switched the scheduling off, a value of zero postpones each execution */ var DEFAULT_MAX_TASK_DURATION = -1; // off /** * Maximum accumulated task execution time (threshold) * Can be configured via the private API property `maxTaskDuration`. */ var iMaxTaskDuration = DEFAULT_MAX_TASK_DURATION; /** * The earliest elapsed time at which a new browser task will be enforced. * Will be updated when a new task starts. */ var iMaxTaskTime = Date.now() + iMaxTaskDuration; /** * A promise that fulfills when the new browser task has been reached. * All postponed callback executions will be executed after this promise. * `null` as long as the elapsed time threshold is not reached. */ var pWaitForNextTask; /** * Message channel which will be used to create a new browser task * without being subject to timer throttling. * Will be created lazily on first usage. */ var oNextTaskMessageChannel; /** * Update elapsed time threshold. * * The threshold will be updated only if executions currently are not postponed. * Otherwise, the next task will anyhow update the threshold. */ function updateMaxTaskTime() { if ( pWaitForNextTask == null ) { iMaxTaskTime = Date.now() + iMaxTaskDuration; } } /** * Update duration limit and elapsed time threshold. */ function updateMaxTaskDuration(v) { v = Number(v); var iBeginOfCurrentTask = iMaxTaskTime - iMaxTaskDuration; // limit to range [-1 ... Infinity], any other value incl. NaN restores the default iMaxTaskDuration = v >= -1 ? v : DEFAULT_MAX_TASK_DURATION; // Update the elapsed time threshold only if executions currently are not postponed. // Otherwise, the next task will be the first to honor the new maximum duration. if ( pWaitForNextTask == null ) { iMaxTaskTime = iBeginOfCurrentTask + iMaxTaskDuration; } } function waitForNextTask() { if ( pWaitForNextTask == null ) { /** * Post a message to a MessageChannel to create a new task, without suffering from timer throttling * In the new task, use a setTimeout(,0) to allow for better queuing of other events (like CSS loading) */ pWaitForNextTask = new Promise(function(resolve) { if ( oNextTaskMessageChannel == null ) { oNextTaskMessageChannel = new MessageChannel(); oNextTaskMessageChannel.port2.start(); } oNextTaskMessageChannel.port2.addEventListener("message", function() { setTimeout(function() { pWaitForNextTask = null; iMaxTaskTime = Date.now() + iMaxTaskDuration; resolve(); }, 0); }, { once: true }); oNextTaskMessageChannel.port1.postMessage(null); }); } return pWaitForNextTask; } /** * Creates a function which schedules the execution of the given callback. * * The scheduling tries to limit the duration of browser tasks. When the configurable * limit is reached, the creation of a new browser task is triggered and all subsequently * scheduled callbacks will be postponed until the new browser task starts executing. * In the new browser task, scheduling starts anew. * * The limit for the duration of browser tasks is configured via `iMaxTaskDuration`. * By setting `iMaxTaskDuration` to a negative value, the whole scheduling mechanism is * switched off. In that case, the returned function will execute the callback immediately. * * If a value of zero is set, each callback will be executed in a separate browser task. * For preloaded modules, this essentially mimics the browser behavior of single file loading, * but without the network and server delays. * * For larger values, at least one callback will be executed in each new browser task. When, * after the execution of the callback, the configured threshold has been reached, all further * callbacks will be postponed. * * Note: This is a heuristic only. Neither is the measurement of the task duration accurate, * nor is there a way to know in advance the execution time of a callback. * * @param {function(any):void} fnCallback * Function to schedule * @returns {function(any):void} * A function to call instead of the original callback; it takes care of scheduling * and executing the original callback. * @private */ function scheduleExecution(fnCallback) { if ( iMaxTaskDuration < 0 ) { return fnCallback; } return function() { if ( pWaitForNextTask == null ) { fnCallback.call(undefined, arguments[0]); // if time limit is reached now, postpone future task if ( Date.now() >= iMaxTaskTime ) { waitForNextTask(); } return; } pWaitForNextTask.then(scheduleExecution(fnCallback).bind(undefined, arguments[0])); }; } // ---- Names and Paths ----------------------------------------------------------------------- /** * Name conversion function that converts a name in unified resource name syntax to a name in UI5 module name syntax. * If the name cannot be converted (e.g. doesn't end with '.js'), then <code>undefined</code> is returned. * * @param {string} sName Name in unified resource name syntax * @returns {string|undefined} Name in UI5 (legacy) module name syntax (dot separated) * or <code>undefined</code> when the name can't be converted * @private */ function urnToUI5(sName) { // UI5 module name syntax is only defined for JS resources if ( !/\.js$/.test(sName) ) { return undefined; } sName = sName.slice(0, -3); if ( /^jquery\.sap\./.test(sName) ) { return sName; // do nothing } return sName.replace(/\//g, "."); } function urnToIDAndType(sResourceName) { var basenamePos = sResourceName.lastIndexOf('/'), dotPos = sResourceName.lastIndexOf('.'); if ( dotPos > basenamePos ) { return { id: sResourceName.slice(0, dotPos), type: sResourceName.slice(dotPos) }; } return { id: sResourceName, type: '' }; } var rJSSubTypes = /(\.controller|\.fragment|\.view|\.designtime|\.support)?.js$/; function urnToBaseIDAndSubType(sResourceName) { var m = rJSSubTypes.exec(sResourceName); if ( m ) { return { baseID: sResourceName.slice(0, m.index), subType: m[0] }; } } var rDotSegmentAnywhere = /(?:^|\/)\.+(?=\/|$)/; var rDotSegment = /^\.*$/; /** * Normalizes a resource name by resolving any relative name segments. * * A segment consisting of a single dot <code>./</code>, when used at the beginning of a name refers * to the containing package of the <code>sBaseName</code>. When used inside a name, it is ignored. * * A segment consisting of two dots <code>../</code> refers to the parent package. It can be used * anywhere in a name, but the resolved name prefix up to that point must not be empty. * * Example: A name <code>../common/validation.js</code> defined in <code>sap/myapp/controller/mycontroller.controller.js</code> * will resolve to <code>sap/myapp/common/validation.js</code>. * * When <code>sBaseName</code> is <code>null</code> (e.g. for a <code>sap.ui.require</code> call), * the resource name must not start with a relative name segment or an error will be thrown. * * @param {string} sResourceName Name to resolve * @param {string|null} sBaseName Name of a reference module relative to which the name will be resolved * @returns {string} Resolved name * @throws {Error} When a relative name should be resolved but not basename is given; * or when upward navigation (../) is requested on the root level * or when a name segment consists of 3 or more dots only * @private */ function normalize(sResourceName, sBaseName) { var p = sResourceName.search(rDotSegmentAnywhere), aSegments, sSegment, i,j,l; // check whether the name needs to be resolved at all - if not, just return the sModuleName as it is. if ( p < 0 ) { return sResourceName; } // if the name starts with a relative segment then there must be a base name (a global sap.ui.require doesn't support relative names) if ( p === 0 ) { if ( sBaseName == null ) { throw new Error("relative name not supported ('" + sResourceName + "'"); } // prefix module name with the parent package sResourceName = sBaseName.slice(0, sBaseName.lastIndexOf('/') + 1) + sResourceName; } aSegments = sResourceName.split('/'); // process path segments for (i = 0, j = 0, l = aSegments.length; i < l; i++) { sSegment = aSegments[i]; if ( rDotSegment.test(sSegment) ) { if (sSegment === '.' || sSegment === '') { // ignore '.' as it's just a pointer to current package. ignore '' as it results from double slashes (ignored by browsers as well) continue; } else if (sSegment === '..') { // move to parent directory if ( j === 0 ) { throw new Error("Can't navigate to parent of root ('" + sResourceName + "')"); } j--; } else { throw new Error("Illegal path segment '" + sSegment + "' ('" + sResourceName + "')"); } } else { aSegments[j++] = sSegment; } } aSegments.length = j; return aSegments.join('/'); } /** * Adds a resource path to the resources map. * * @param {string} sResourceNamePrefix prefix is used as map key * @param {string} sUrlPrefix path to the resource */ function registerResourcePath(sResourceNamePrefix, sUrlPrefix) { sResourceNamePrefix = String(sResourceNamePrefix || ""); if ( sUrlPrefix == null ) { // remove a registered URL prefix, if it wasn't for the empty resource name prefix if ( sResourceNamePrefix ) { if ( mUrlPrefixes[sResourceNamePrefix] ) { delete mUrlPrefixes[sResourceNamePrefix]; log.info("registerResourcePath ('" + sResourceNamePrefix + "') (registration removed)"); } return; } // otherwise restore the default sUrlPrefix = DEFAULT_BASE_URL; log.info("registerResourcePath ('" + sResourceNamePrefix + "') (default registration restored)"); } // cast to string and remove query parameters and/or hash sUrlPrefix = pathOnly(String(sUrlPrefix)); // ensure that the prefix ends with a '/' if ( sUrlPrefix.slice(-1) !== '/' ) { sUrlPrefix += '/'; } mUrlPrefixes[sResourceNamePrefix] = { url: sUrlPrefix, // calculate absolute URL, only to be used by 'guessResourceName' absoluteUrl: resolveURL(sUrlPrefix) }; } /** * Retrieves path to a given resource by finding the longest matching prefix for the resource name * * @param {string} sResourceName name of the resource stored in the resources map * @param {string} sSuffix url suffix * * @returns {string} resource path */ function getResourcePath(sResourceName, sSuffix) { var sNamePrefix = sResourceName, p = sResourceName.length, sPath; // search for a registered name prefix, starting with the full name and successively removing one segment while ( p > 0 && !mUrlPrefixes[sNamePrefix] ) { p = sNamePrefix.lastIndexOf('/'); // Note: an empty segment at p = 0 (leading slash) will be ignored sNamePrefix = p > 0 ? sNamePrefix.slice(0, p) : ''; } assert((p > 0 || sNamePrefix === '') && mUrlPrefixes[sNamePrefix], "there always must be a mapping"); sPath = mUrlPrefixes[sNamePrefix].url + sResourceName.slice(p + 1); // also skips a leading slash! //remove trailing slash if ( sPath.slice(-1) === '/' ) { sPath = sPath.slice(0, -1); } return sPath + (sSuffix || ''); } /** * Returns the reporting mode for synchronous calls * * @returns {int} sync call behavior */ function getSyncCallBehavior() { return syncCallBehavior; } /** * Try to find a resource name that would be mapped to the given URL. * * If multiple path mappings would create a match, the returned name is not necessarily * the best (longest) match. The first match which is found, will be returned. * * When <code>bLoadedResourcesOnly</code> is set, only those resources will be taken * into account for which content has been loaded already. * * @param {string} sURL URL to guess the resource name for * @param {boolean} [bLoadedResourcesOnly=false] Whether the guess should be limited to already loaded resources * @returns {string|undefined} Resource name or <code>undefined</code> if no matching name could be found * @private */ function guessResourceName(sURL, bLoadedResourcesOnly) { var sNamePrefix, sUrlPrefix, sResourceName; // Make sure to have an absolute URL without query parameters or hash // to check against absolute prefix URLs sURL = pathOnly(resolveURL(sURL)); for (sNamePrefix in mUrlPrefixes) { // Note: configured URL prefixes are guaranteed to end with a '/' // But to support the legacy scenario promoted by the application tools ( "registerModulePath('Application','Application')" ) // the prefix check here has to be done without the slash sUrlPrefix = mUrlPrefixes[sNamePrefix].absoluteUrl.slice(0, -1); if ( sURL.lastIndexOf(sUrlPrefix, 0) === 0 ) { // calc resource name sResourceName = sNamePrefix + sURL.slice(sUrlPrefix.length); // remove a leading '/' (occurs if name prefix is empty and if match was a full segment match if ( sResourceName.charAt(0) === '/' ) { sResourceName = sResourceName.slice(1); } if ( !bLoadedResourcesOnly || mModules[sResourceName] && mModules[sResourceName].data != undefined ) { return sResourceName; } } } } /** * Find the most specific map config that matches the given context resource * @param {string} sContext Resource name to be used as context * @returns {Object<string,string>|undefined} Most specific map or <code>undefined</code> */ function findMapForContext(sContext) { var p, mMap; if ( sContext != null ) { // maps are defined on module IDs, reduce URN to module ID sContext = urnToIDAndType(sContext).id; p = sContext.length; mMap = mMaps[sContext]; while ( p > 0 && mMap == null ) { p = sContext.lastIndexOf('/'); if ( p > 0 ) { // Note: an empty segment at p = 0 (leading slash) will be ignored sContext = sContext.slice(0, p); mMap = mMaps[sContext]; } } } // if none is found, fallback to '*' map return mMap || mMaps['*']; } function getMappedName(sResourceName, sRequestingResourceName) { var mMap = findMapForContext(sRequestingResourceName), sPrefix, p; // resolve relative names sResourceName = normalize(sResourceName, sRequestingResourceName); // if there's a map, search for the most specific matching entry if ( mMap != null ) { // start with the full ID and successively remove one segment sPrefix = urnToIDAndType(sResourceName).id; p = sPrefix.length; while ( p > 0 && mMap[sPrefix] == null ) { p = sPrefix.lastIndexOf('/'); // Note: an empty segment at p = 0 (leading slash) will be ignored sPrefix = p > 0 ? sPrefix.slice(0, p) : ''; } if ( p > 0 ) { if ( log.isLoggable() ) { log.debug('module ID ' + sResourceName + " mapped to " + mMap[sPrefix] + sResourceName.slice(p)); } return mMap[sPrefix] + sResourceName.slice(p); // also skips a leading slash! } } return sResourceName; } function getGlobalObject(oObject, aNames, l, bCreate) { for (var i = 0; oObject && i < l; i++) { if (!oObject[aNames[i]] && bCreate ) { oObject[aNames[i]] = {}; } oObject = oObject[aNames[i]]; } return oObject; } function getGlobalProperty(sName) { var aNames = sName ? sName.split(".") : []; if ( syncCallBehavior && aNames.length > 1 ) { log.error("[nosync] getGlobalProperty called to retrieve global name '" + sName + "'"); } return getGlobalObject(__global, aNames, aNames.length); } function setGlobalProperty(sName, vValue) { var aNames = sName ? sName.split(".") : [], oObject; if ( aNames.length > 0 ) { oObject = getGlobalObject(__global, aNames, aNames.length - 1, true); oObject[aNames[aNames.length - 1]] = vValue; } } // ---- Modules ------------------------------------------------------------------------------- function wrapExport(value) { return { moduleExport: value }; } function unwrapExport(wrapper) { return wrapper.moduleExport; } /** * Module neither has been required nor preloaded nor declared, but someone asked for it. */ var INITIAL = 0, /** * Module has been preloaded, but not required or declared. */ PRELOADED = -1, /** * Module has been declared. */ LOADING = 1, /** * Module has been loaded, but not yet executed. */ LOADED = 2, /** * Module is currently being executed */ EXECUTING = 3, /** * Module has been loaded and executed without errors. */ READY = 4, /** * Module either could not be loaded or execution threw an error */ FAILED = 5, /** * Special content value used internally until the content of a module has been determined */ NOT_YET_DETERMINED = {}; /** * A module/resource as managed by the module system. * * Each module is an object with the following properties * <ul> * <li>{int} state one of the module states defined in this function</li> * <li>{string} url URL where the module has been loaded from</li> * <li>{any} data temp. raw content of the module (between loaded and ready or when preloaded)</li> * <li>{string} group the bundle with which a resource was loaded or null</li> * <li>{string} error an error description for state <code>FAILED</code></li> * <li>{any} content the content of the module as exported via define()<(li> * </ul> * * @param {string} name Name of the module, including extension */ function Module(name) { this.name = name; this.state = INITIAL; /* * Whether processing of the module is complete. * This is very similar to, but not the same as state >= READY because declareModule() sets state=READY very early. * That state transition is 'legacy' from the library-all files; it needs to be checked whether it can be removed. */ this.settled = false; this.url = this._deferred = this.data = this.group = this.error = this.pending = null; this.content = NOT_YET_DETERMINED; } Module.prototype.deferred = function() { if ( this._deferred == null ) { var deferred = this._deferred = {}; deferred.promise = new Promise(function(resolve,reject) { deferred.resolve = resolve; deferred.reject = reject; }); // avoid 'Uncaught (in promise)' log entries deferred.promise.catch(noop); } return this._deferred; }; Module.prototype.api = function() { if ( this._api == null ) { this._exports = {}; this._api = { id: this.name.slice(0,-3), exports: this._exports, url: this.url, config: noop }; } return this._api; }; /** * Sets the module state to READY and either determines the value or sets * it from the given parameter. * @param {any} value Module value */ Module.prototype.ready = function(value) { // should throw, but some tests and apps would fail assert(!this.settled, "Module " + this.name + " is already settled"); this.state = READY; this.settled = true; if ( arguments.length > 0 ) { // check arguments.length to allow a value of undefined this.content = value; } this.deferred().resolve(wrapExport(this.value())); if ( this.aliases ) { value = this.value(); this.aliases.forEach(function(alias) { Module.get(alias).ready(value); }); } }; Module.prototype.failWith = function(msg, cause) { var err = makeModuleError(msg, this, cause); this.fail(err); return err; }; Module.prototype.fail = function(err) { // should throw, but some tests and apps would fail assert(!this.settled, "Module " + this.name + " is already settled"); this.settled = true; if ( this.state !== FAILED ) { this.state = FAILED; this.error = err; this.deferred().reject(err); if ( this.aliases ) { this.aliases.forEach(function(alias) { Module.get(alias).fail(err); }); } } }; Module.prototype.addPending = function(sDependency) { (this.pending || (this.pending = [])).push(sDependency); }; Module.prototype.addAlias = function(sAliasName) { (this.aliases || (this.aliases = [])).push(sAliasName); // add this module as pending dependency to the original Module.get(sAliasName).addPending(this.name); }; Module.prototype.preload = function(url, data, bundle) { if ( this.state === INITIAL && !(fnIgnorePreload && fnIgnorePreload(this.name)) ) { this.state = PRELOADED; this.url = url; this.data = data; this.group = bundle; } return this; }; /** * Determines the value of this module. * * If the module hasn't been loaded or executed yet, <code>undefined</code> will be returned. * * @returns {any} Export of the module or <code>undefined</code> * @private */ Module.prototype.value = function() { if ( this.state === READY ) { if ( this.content === NOT_YET_DETERMINED ) { // Determine the module value lazily. // For AMD modules this has already been done on execution of the factory function. // For other modules that are required synchronously, it has been done after execution. // For the few remaining scenarios (like global scripts), it is done here var oShim = mShims[this.name], sExport = oShim && (Array.isArray(oShim.exports) ? oShim.exports[0] : oShim.exports); // best guess for thirdparty modules or legacy modules that don't use sap.ui.define this.content = getGlobalProperty( sExport || urnToUI5(this.name) ); } return this.content; } return undefined; }; /** * Checks whether this module depends on the given module. * * When a module definition (define) is executed, the requested dependencies are added * as 'pending' to the Module instance. This function checks if the oDependantModule is * reachable from this module when following the pending dependency information. * * Note: when module aliases are introduced (all module definitions in a file use an ID that differs * from the request module ID), then the alias module is also added as a "pending" dependency. * * @param {Module} oDependantModule Module which has a dependency to <code>oModule</code> * @returns {boolean} Whether this module depends on the given one. * @private */ Module.prototype.dependsOn = function(oDependantModule) { var dependant = oDependantModule.name, visited = Object.create(null); // log.debug("checking for a cycle between", this.name, "and", dependant); function visit(mod) { if ( !visited[mod] ) { // log.debug(" ", mod); visited[mod] = true; var pending = mModules[mod] && mModules[mod].pending; return Array.isArray(pending) && (pending.indexOf(dependant) >= 0 || pending.some(visit)); } return false; } return this.name === dependant || visit(this.name); }; /** * Find or create a module by its unified resource name. * * If the module doesn't exist yet, a new one is created in state INITIAL. * * @param {string} sModuleName Name of the module in URN syntax * @returns {Module} Module with that name, newly created if it didn't exist yet * @static */ Module.get = function(sModuleName) { if (!mModules[sModuleName]) { mModules[sModuleName] = new Module(sModuleName); } return mModules[sModuleName]; }; /* * Determines the currently executing module. */ function getExecutingModule() { if ( _execStack.length > 0 ) { return _execStack[_execStack.length - 1].name; } return document.currentScript && document.currentScript.getAttribute("data-sap-ui-module"); } // -------------------------------------------------------------------------------------------- var _globalDefine, _globalDefineAMD; function updateDefineAndInterceptAMDFlag(newDefine) { // no change, do nothing if ( _globalDefine === newDefine ) { return; } // first cleanup on an old loader if ( _globalDefine ) { _globalDefine.amd = _globalDefineAMD; _globalDefine = _globalDefineAMD = undefined; } // remember the new define _globalDefine = newDefine; // intercept access to the 'amd' property of the new define, if it's not our own define if ( newDefine && !newDefine.ui5 ) { _globalDefineAMD = _globalDefine.amd; Object.defineProperty(_globalDefine, "amd", { get: function() { var sCurrentModule = getExecutingModule(); if ( sCurrentModule && mShims[sCurrentModule] && mShims[sCurrentModule].amd ) { log.debug("suppressing define.amd for " + sCurrentModule); return undefined; } return _globalDefineAMD; }, set: function(newDefineAMD) { _globalDefineAMD = newDefineAMD; log.debug("define.amd became " + (newDefineAMD ? "active" : "unset")); }, configurable: true // we have to allow a redefine for debug mode or restart from CDN etc. }); } } try { Object.defineProperty(__global, "define", { get: function() { return _globalDefine; }, set: function(newDefine) { updateDefineAndInterceptAMDFlag(newDefine); log.debug("define became " + (newDefine ? "active" : "unset")); }, configurable: true // we have to allow a redefine for debug mode or restart from CDN etc. }); } catch (e) { log.warning("could not intercept changes to window.define, ui5loader won't be able to a change of the AMD loader"); } updateDefineAndInterceptAMDFlag(__global.define); // -------------------------------------------------------------------------------------------- function isModuleError(err) { return err && err.name === "ModuleError"; } /** * Wraps the given 'cause' in a new error with the given message and with name 'ModuleError'. * * The new message and the message of the cause are combined. The stacktrace of the * new error and of the cause are combined (with a separating 'Caused by'). * * Instead of the final message string, a template is provided which can contain placeholders * for the module ID ({id}) and module URL ({url}). Providing a template without concrete * values allows to detect the repeated nesting of the same error. In such a case, only * the innermost cause will be kept (affects both, stack trace as well as the cause property). * The message, however, will contain the full chain of module IDs. * * @param {string} template Message string template with placeholders * @param {Module} module Module for which the error occurred * @param {Error} cause original error * @returns {Error} New module error */ function makeModuleError(template, module, cause) { var modules = "'" + module.name + "'"; if (isModuleError(cause)) { // update the chain of modules (increasing the indent) modules = modules + "\n -> " + cause._modules.replace(/ -> /g, " -> "); // omit repeated occurrences of the same kind of error if ( template === cause._template ) { cause = cause.cause; } } // create the message string from the template and the cause's message var message = template.replace(/\{id\}/, modules).replace(/\{url\}/, module.url) + (cause ? ": " + cause.message : ""); var error = new Error(message); error.name = "ModuleError"; error.cause = cause; if ( cause && cause.stack ) { error.stack = error.stack + "\nCaused by: " + cause.stack; } // the following properties are only for internal usage error._template = template; error._modules = modules; return error; } function declareModule(sModuleName) { var oModule; // sModuleName must be a unified resource name of type .js assert(/\.js$/.test(sModuleName), "must be a Javascript module"); oModule = Module.get(sModuleName); if ( oModule.state > INITIAL ) { return oModule; } if ( log.isLoggable() ) { log.debug(sLogPrefix + "declare module '" + sModuleName + "'"); } // avoid cycles oModule.state = READY; return oModule; } /** * Define an already loaded module synchronously. * Finds or creates a module by its unified resource name and resolves it with the given value. * * @param {string} sResourceName Name of the module in URN syntax * @param {any} vValue Content of the module */ function defineModuleSync(sResourceName, vValue) { Module.get(sResourceName).ready(vValue); } /** * Queue of modules for which sap.ui.define has been called (in async mode), but which have not been executed yet. * When loading modules via script tag, only the onload handler knows the relationship between executed sap.ui.define calls and * module name. It then resolves the pending modules in the queue. Only one entry can get the name of the module * if there are more entries, then this is an error * * @param {boolean} [nested] Whether this is a nested queue used during sync execution of a module */ function ModuleDefinitionQueue(nested) { var aQueue = [], iRun = 0, vTimer; this.push = function(name, deps, factory, _export) { if ( log.isLoggable() ) { log.debug(sLogPrefix + "pushing define() call" + (document.currentScript ? " from " + document.currentScript.src : "") + " to define queue #" + iRun); } var sModule = document.currentScript && document.currentScript.getAttribute('data-sap-ui-module'); aQueue.push({ name: name, deps: deps, factory: factory, _export: _export, guess: sModule }); // trigger queue processing via a timer in case the currently executing script is not managed by the loader if ( !vTimer && !nested && sModule == null ) { vTimer = setTimeout(this.process.bind(this, null, "timer")); } }; this.clear = function() { aQueue = []; if ( vTimer ) { clearTimeout(vTimer); vTimer = null; } }; /** * Process the queue of module definitions, assuming that the original request was for * <code>oRequestedModule</code>. If there is an unnamed module definition, it is assumed to be * the one for the requested module. * * When called via timer, <code>oRequestedModule</code> will be undefined. * * @param {Module} [oRequestedModule] Module for which the current script was loaded. * @param {string} [sInitiator] A string describing the caller of <code>process</code> */ this.process = function(oRequestedModule, sInitiator) { var bLoggable = log.isLoggable(), iCurrentRun = iRun++, aQueueCopy = aQueue, sModuleName = null; // clear the queue and timer early, we've already taken a copy of the queue this.clear(); if ( oRequestedModule ) { // if a module execution error was detected, stop processing the queue if ( oRequestedModule.execError ) { if ( bLoggable ) { log.debug("module execution error detected, ignoring queued define calls (" + aQueueCopy.length + ")"); } oRequestedModule.fail(oRequestedModule.execError); return; } } /* * Name of the requested module, null when unknown or already consumed. * * - when no module request is known (e.g. script was embedded in the page as an unmanaged script tag), * then no name is known and unnamed module definitions will be reported as an error * - multiple unnamed module definitions also are reported as an error * - when the name of a named module definition matches the name of requested module, the name is 'consumed'. * Any later unnamed module definition will be reported as an error, too */ sModuleName = oRequestedModule && oRequestedModule.name; // check whether there's a module definition for the requested module aQueueCopy.forEach(function(oEntry) { if ( oEntry.name == null ) { if ( sModuleName != null ) { oEntry.name = sModuleName; sModuleName = null; } else { // multiple modules have been queued, but only one module can inherit the name from the require call if ( strictModuleDefinitions ) { var oError = new Error( "Modules that use an anonymous define() call must be loaded with a require() call; " + "they must not be executed via script tag or nested into other modules. "); if ( oRequestedModule ) { oRequestedModule.fail(oError); } else { throw oError; } } // give anonymous modules a unique pseudo ID oEntry.name = '~anonymous~' + (++iAnonymousModuleCount) + '.js'; log.error( "Modules that use an anonymous define() call must be loaded with a require() call; " + "they must not be executed via script tag or nested into other modules. " + "All other usages will fail in future releases or when standard AMD loaders are used. " + "Now using substitute name " + oEntry.name); } } else if ( oRequestedModule && oEntry.name === oRequestedModule.name ) { if ( sModuleName == null && !strictModuleDefinitions ) { // if 'strictModuleDefinitions' is active, double execution will be reported anyhow log.error( "Duplicate module definition: both, an unnamed module and a module with the expected name exist." + "This use case will fail in future releases or when standard AMD loaders are used. "); } sModuleName = null; } }); // if not, assign an alias if there's at least one queued module definition if ( sModuleName && aQueueCopy.length > 0 ) { if ( bLoggable ) { log.debug( "No queued module definition matches the ID of the request. " + "Now assuming that the first definition '" + aQueueCopy[0].name + "' is an alias of '" + sModuleName + "'"); } Module.get(aQueueCopy[0].name).addAlias(sModuleName); sModuleName = null; } if ( bLoggable ) { log.debug(sLogPrefix + "[" + sInitiator + "] " + "processing define queue #" + iCurrentRun + (oRequestedModule ? " for '" + oRequestedModule.name + "'" : "") + " with entries [" + aQueueCopy.map(function(entry) { return "'" + entry.name + "'"; }) + "]"); } aQueueCopy.forEach(function(oEntry) { // start to resolve the dependencies executeModuleDefinition(oEntry.name, oEntry.deps, oEntry.factory, oEntry._export, /* bAsync = */ true); }); if ( sModuleName != null && !oRequestedModule.settled ) { // module name still not consumed, might be a non-UI5 module (e.g. in 'global' format) if ( bLoggable ) { log.debug(sLogPrefix + "no queued module definition for the requested module found, assume the module to be ready"); } oRequestedModule.data = undefined; // allow GC oRequestedModule.ready(); // no export known, has to be retrieved via global name } if ( bLoggable ) { log.debug(sLogPrefix + "processing define queue #" + iCurrentRun + " done"); } }; } var queue = new ModuleDefinitionQueue(); /** * Loads the source for the given module with a sync XHR. * @param {Module} oModule Module to load the source for * @throws {Error} When loading failed for some reason. */ function loadSyncXHR(oModule) { var xhr = new XMLHttpRequest(); function createXHRLoadError(error) { error = new Error(xhr.statusText ? xhr.status + " - " + xhr.statusText : xhr.status); error.name = "XHRLoadError"; error.status = xhr.status; error.statusText = xhr.statusText; return error; } xhr.addEventListener('load', function(e) { // File protocol (file://) always has status code 0 if ( xhr.status === 200 || xhr.status === 0 ) { oModule.state = LOADED; oModule.data = xhr.responseText; } else { oModule.error = createXHRLoadError(); } }); // Note: according to whatwg spec, error event doesn't fire for sync send(), instead an error is thrown // we register a handler, in case a browser doesn't follow the spec xhr.addEventListener('error', function(e) { oModule.error = createXHRLoadError(); }); xhr.open('GET', oModule.url, false); try { xhr.send(); } catch (error) { oModule.error = error; } } /** * Global event handler to detect script execution errors. * @private */ window.addEventListener('error', function onUncaughtError(errorEvent) { var sModuleName = document.currentScript && document.currentScript.getAttribute('data-sap-ui-module'); var oModule = sModuleName && Module.get(sModuleName); if ( oModule && oModule.execError == null ) { // if a currently executing module can be identified, attach the error to it and suppress reporting if ( log.isLoggable() ) { log.debug("unhandled exception occurred while executing " + sModuleName + ": " + errorEvent.message); } oModule.execError = errorEvent.error || { name: 'Error', message: errorEvent.message }; return false; } }); function loadScript(oModule, sAlternativeURL) { var oScript; function onload(e) { updateMaxTaskTime(); if ( log.isLoggable() ) { log.debug("JavaScript resource loaded: " + oModule.name); } oScript.removeEventListener('load', onload); oScript.removeEventListener('error', onerror); queue.process(oModule, "onload"); } function onerror(e) { updateMaxTaskTime(); oScript.removeEventListener('load', onload); oScript.removeEventListener('error', onerror); if (sAlternativeURL) { log.warning("retry loading JavaScript resource: " + oModule.name); if (oScript && oScript.parentNode) { oScript.parentNode.removeChild(oScript); } oModule.url = sAlternativeURL; loadScript(oModule, /* sAlternativeURL= */ null); return; } log.error("failed to load JavaScript resource: " + oModule.name); oModule.failWith("failed to load {id} from {url}", new Error("script load error")); } oScript = document.createElement('SCRIPT'); // Accessing the 'src' property of the script in this strange way prevents Safari 12 (or WebKit) from // wrongly optimizing access. SF12 seems to check at optimization time whether there's a setter for the // property and optimize accordingly. When a setter is defined or changed at a later point in time (e.g. // by the AppCacheBuster), then the optimization seems not to be updated and the new setter is ignored // BCP 1970035485 oScript["s" + "rc"] = oModule.url; //oScript.src = oModule.url; oScript.setAttribute("data-sap-ui-module", oModule.name); if ( sAlternativeURL !== undefined ) { if ( mShims[oModule.name] && mShims[oModule.name].amd ) { oScript.setAttribute("data-sap-ui-module-amd", "true"); } oScript.addEventListener('load', onload); oScript.addEventListener('error', onerror); } document.head.appendChild(oScript); } function preloadDependencies(sModuleName) { var knownDependencies = mDepCache[sModuleName]; if ( Array.isArray(knownDependencies) ) {