UNPKG

@openui5/sap.ui.core

Version:

OpenUI5 Core Library sap.ui.core

1,516 lines (1,323 loc) 116 kB
/*! * OpenUI5 * (c) Copyright 2009-2021 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, console, document, Promise, XMLHttpRequest */ (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); } /* * Helper function that returns the document base URL without search parameters and hash. * * @returns {string} */ function docBase() { return pathOnly(document.baseURI); } /* * Whether the current browser is IE11, derived from the compatibility check for the URL Web API. */ var isIE11 = false; /** * 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. * * @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 */ var resolveURL = (function(_URL) { // feature check: URI support // Can URL be used as a constructor (fails in IE 11)? try { new _URL('index.html', 'http://localhost:8080/'); } catch (e) { isIE11 = true; _URL = null; } if ( _URL ) { return function(sURI, sBase) { // For a spec see https://url.spec.whatwg.org/ // For browser support see https://developer.mozilla.org/en/docs/Web/API/URL return new _URL(sURI, sBase ? new _URL(sBase, docBase()) : docBase()).toString(); }; } // fallback for IE11: use a shadow document with <base> and <a>nchor tag var doc = document.implementation.createHTMLDocument("Dummy doc for resolveURI"); var base = doc.createElement('base'); base.href = docBase(); doc.head.appendChild(base); var anchor = doc.createElement("A"); doc.body.appendChild(anchor); return function (sURI, sBase) { base.href = docBase(); if ( sBase != null ) { // first resolve sBase relative to location anchor.href = sBase; // then use it as base base.href = anchor.href; } anchor.href = sURI; // console.log("(" + sURI + "," + sBase + ") => (" + base.href + "," + anchor.href + ")"); return anchor.href; }; }(__global.URL || __global.webkitURL)); // ---- 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 t 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. * @type {Object.<string,{url:string,absoluteUrl:string}>} * @see jQuery.sap.registerModulePath * @private */ var mUrlPrefixes = Object.create(null); mUrlPrefixes[''] = { url: DEFAULT_BASE_URL, absoluteUrl: resolveURL(DEFAULT_BASE_URL, document.baseURI) }; /** * 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, /** * IE only: max size a script should have when executing it with execScript, otherwise fallback to eval. * @type {int} * @const * @private */ MAX_EXEC_SCRIPT_LENGTH = 512 * 1024; // ---- 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; } function guessResourceName(sURL) { 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.indexOf(sUrlPrefix) === 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 ( 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.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) { return mModules[sModuleName] || (mModules[sModuleName] = new Module(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 ensureStacktrace(oError) { if (!oError.stack) { try { throw oError; } catch (ex) { return ex; } } return oError; } function makeNestedError(msg, cause) { var oError = new Error(msg + ": " + cause.message); oError.cause = cause; oError.loadError = cause.loadError; ensureStacktrace(oError); ensureStacktrace(cause); // concat the error stack for better traceability of loading issues if ( oError.stack && cause.stack ) { oError.stack = oError.stack + "\nCaused by: " + cause.stack; } // @evo-todo // for non Chrome browsers we log the caused by stack manually in the console // if (__global.console && !Device.browser.chrome) { // /*eslint-disable no-console */ // console.error(oError.message + "\nCaused by: " + oCausedByStack); // /*eslint-enable no-console */ // } return oError; } 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 */ function ModuleDefinitionQueue() { 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); } aQueue.push({ name: name, deps: deps, factory: factory, _export: _export, guess: document.currentScript && document.currentScript.getAttribute('data-sap-ui-module') }); // trigger queue processing via a timer in case the currently executing script is not managed by the loader if ( !vTimer ) { 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 enrichXHRError(error) { error = error || ensureStacktrace(new Error(xhr.status + " - " + xhr.statusText)); error.status = xhr.status; error.statusText = xhr.statusText; error.loadError = true; 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 = enrichXHRError(); } }); // 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 = enrichXHRError(); }); xhr.open('GET', oModule.url, false); try { xhr.send(); } catch (error) { oModule.error = enrichXHRError(error); } } /** * Global event handler to detect script execution errors. * Only works for browsers that support <code>document.currentScript</code>. * @private */ if ( 'currentScript' in document ) { 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) { 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) { 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.fail( ensureStacktrace(new Error("failed to load '" + oModule.name + "' from " + oModule.url + ": 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) ) { log.debug("preload dependencies for " + sModuleName + ": " + knownDependencies); knownDependencies.forEach(function(dep) { dep = getMappedName(dep, sModuleName); if ( /\.js$/.test(dep) ) { requireModule(null, dep, /* always async */ true); } // else: TODO handle non-JS resources, e.g. link rel=prefetch }); } } /** * Loads the given module if needed and returns the module export or a promise on it. * * If loading is still ongoing for the requested module and if there is a cycle detected between * the requesting module and the module to be loaded, then <code>undefined</code> (or a promise on * <code>undefined</code>) will be returned as intermediate module export to resolve the cycle. * * @param {Module} oRequestingModule The module in whose context the new module has to be loaded; * this is needed to detect cycles * @param {string} sModuleName Name of the module to be loaded, in URN form and with '.js' extension * @param {boolean} bAsync Whether the operation can be executed asynchronously * @param {boolean} [bSkipShimDeps=false] Whether shim dependencies should be ignored (used by recursive calls) * @param {boolean} [bSkipBundle=false] Whether bundle information should be ignored (used by recursive calls) * @returns {any|Promise} Returns the module export in sync mode or a promise on it in async mode * @throws {Error} When loading failed in sync mode * * @private */ function requireModule(oRequestingModule, sModuleName, bAsync, bSkipShimDeps, bSkipBundle) { var bLoggable = log.isLoggable(), oSplitName = urnToBaseIDAndSubType(sModuleName), oShim = mShims[sModuleName], oModule, aExtensions, i, sMsg, bExecutedNow; // only for robustness, should not be possible by design (all callers append '.js') if ( !oSplitName ) { throw new Error("can only require Javascript module, not " + sModuleName); } // Module names should not start with a "/" if (sModuleName[0] == "/") { log.error("Module names that start with a slash should not be used, as they are reserved for future use."); } oModule = Module.get(sModuleName); // when there's a shim with dependencies for the module // resolve them first before requiring the module again with bSkipShimDeps = true if ( oShim && oShim.deps && !bSkipShimDeps ) { if ( bLoggable ) { log.debug("require dependencies of raw module " + sModuleName); } return requireAll(oModule, oShim.deps, function() { // set bSkipShimDeps to true to prevent endless recursion return requireModule(oRequestingModule, sModuleName, bAsync, /* bSkipShimDeps = */ true, bSkipBundle); }, function(oErr) { oModule.fail(oErr); if ( bAsync ) { return; } throw oErr; }, bAsync); } // when there's bundle information for the module // require the bundle first before requiring the module again with bSkipBundle = true if ( oModule.state === INITIAL && oModule.group && oModule.group !== sModuleName && !bSkipBundle ) { if ( bLoggable ) { log.debug(sLogPrefix + "require bundle '" + oModule.group + "'" + " containing '" + sModuleName + "'"); } if ( bAsync ) { return requireModule(null, oModule.group, bAsync).catch(noop).then(function() { // set bSkipBundle to true to prevent endless recursion return requireModule(oRequestingModule, sModuleName, bAsync, bSkipShimDeps, /* bSkipBundle = */ true); }); } else { try { requireModule(null, oModule.group, bAsync); } catch (oError) { if ( bLoggable ) { log.error(sLogPrefix + "require bundle '" + oModule.group + "' failed (ignored)"); } } } } if ( bLoggable ) { log.debug(sLogPrefix + "require '" + sModuleName + "'" + (oRequestingModule ? " (dependency of '" + oRequestingModule.name + "')" : "")); } // check if module has been loaded already if ( oModule.state !== INITIAL ) { if ( oModule.state === EXECUTING && oModule.data != null && !bAsync && oModule.async ) { oModule.state = PRELOADED; oModule.async = bAsync; oModule.pending = null; // TODO or is this still needed ? } if ( oModule.state === PRELOADED ) { oModule.state = LOADED; oModule.async = bAsync; bExecutedNow = true; measure && measure.start(sModuleName, "Require module " + sModuleName + " (preloaded)", ["require"]); execModule(sModuleName, bAsync); measure && measure.end(sModuleName); } if ( oModule.state === READY ) { if ( !bExecutedNow && bLoggable ) { log.debug(sLogPrefix + "module '" + sModuleName + "' has already been loaded (skipped)."); } // Note: this intentionally does not return oModule.promise() as the export might be temporary in case of cycles // or it might have changed after repeated module execution return bAsync ? Promise.resolve(wrapExport(oModule.value())) : wrapExport(oModule.value()); } else if ( oModule.state === FAILED ) { if ( bAsync ) { return oModule.deferred().promise; } else { throw (bExecutedNow ? oModule.error : makeNestedError("found in negative cache: '" + sModuleName + "' from " + oModule.url, oModule.error)); } } else { // currently loading or executing if ( bAsync ) { // break up cyclic dependencies if ( oRequestingModule && oModule.dependsOn(oRequestingModule) ) { if ( log.isLoggable() ) { log.debug("cycle detected between '" + oRequestingModule.name + "' and '" + sModuleName + "', returning undefined for '" + sModuleName + "'"); } // Note: this must be a separate promise as the fulfillment is not the final one return Promise.resolve(wrapExport(undefined)); } return oModule.deferred().promise; } if ( !bAsync && !oModule.async ) { // sync pending, return undefined if ( log.isLoggable() ) { log.debug("cycle detected between '" + (oRequestingModule ? oRequestingModule.name : "unknown") + "' and '" + sModuleName + "', returning undefined for '" + sModuleName + "'"); } return wrapExport(undefined); } // async pending, load sync again log.warning("Sync request triggered for '" + sModuleName + "' while async request was already pending." + " Loading a module twice might cause issues and should be avoided by fully migrating to async APIs."); } } if ( isIE11 && bAsync && oShim && oShim.amd ) { // in IE11, we force AMD/UMD modules into sync loading to apply the define.amd workaround // in other browsers, we intercept read access to window.define, see ensureDefineInterceptor bAsync = false; } measure && measure.start(sModuleName, "Require module " + sModuleName, ["require"]); //