@openui5/sap.ui.core
Version:
OpenUI5 Core Library sap.ui.core
1,530 lines (1,338 loc) • 125 kB
JavaScript
/*!
* OpenUI5
* (c) Copyright 2026 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 Blob, console, document, Promise, URL, 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) {
const 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((key) => callback(key, obj[key]));
}
function executeInSeparateTask(fn) {
setTimeout(fn, 0);
}
function executeInMicroTask(fn) {
Promise.resolve().then(fn);
}
// ---- hooks & configuration -----------------------------------------------------------------
const aEarlyLogs = [];
function earlyLog(level, message) {
aEarlyLogs.push({
level,
message
});
}
/**
* 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
*/
let log = {
debug: earlyLog.bind(this, 'debug'),
info: earlyLog.bind(this, 'info'),
warning: earlyLog.bind(this, 'warning'),
error: earlyLog.bind(this, 'error'),
isLoggable: noop
};
/**
* 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
*/
let 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
*/
let measure;
/**
* Source code transformation hook.
*
* To be used by code coverage, only supported in sync mode.
* @private
* @ui5-transform-hint replace-local undefined
*/
let 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.
*/
let 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
*/
const strictModuleDefinitions = true;
/**
* Whether asynchronous loading can be used at all.
* When activated, require will load asynchronously, else synchronously.
* @type {boolean}
* @private
* @ui5-transform-hint replace-local true
*/
let 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
*/
let 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
*/
let 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
*/
const DEFAULT_BASE_URL = "./";
/**
* Temporarily saved reference to the original value of the global define variable.
*
* @type {any}
* @private
*/
let vOriginalDefine;
/**
* Temporarily saved reference to the original value of the global require variable.
*
* @type {any}
* @private
*/
let 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
*/
const 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
*/
const 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
*/
const 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
*/
const mDepCache = Object.create(null);
/**
* Whether the loader should try to load debug sources.
* @type {boolean}
* @private
*/
let 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
*/
let fnIgnorePreload;
/**
* Whether the loader should try to load the debug variant
* of a module.
* This takes the standard and partial debug mode into account.
*
* @param {string} sModuleName Name of the module to be loaded
* @returns {boolean} Whether the debug variant should be loaded
*/
function shouldLoadDebugVariant(sModuleName) {
if (fnIgnorePreload) {
// if preload is ignored (= partial debug mode), load the debug module first
if (fnIgnorePreload(sModuleName)) {
return true;
} else {
// partial debug mode is active, but not for this module
return false;
}
} else {
// no debug mode or standard debug mode
return bDebugSources;
}
}
// ---- internal state ------------------------------------------------------------------------
/**
* Map of modules that have been loaded or required so far, keyed by their name.
*
* @type {Object<string,Module>}
* @private
*/
const 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|null}
* @private
*/
let 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
* bundles like sap-ui-core).
*
* @type {Array.<{name:string,used:boolean}>}
* @private
*/
const _execStack = [ ];
/**
* A prefix that will be added to module loading log statements and which reflects the nesting of module executions.
* @type {string}
* @private
*/
let sLogPrefix = "";
/**
* Counter used to give anonymous modules a unique module ID.
* @type {int}
* @private
*/
let 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
*/
const DEFAULT_MAX_TASK_DURATION = -1; // off
/**
* Maximum accumulated task execution time (threshold)
* Can be configured via the private API property `maxTaskDuration`.
*/
let 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.
*/
let 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.
*/
let 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.
*/
let 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);
const 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;
}
}
// remember original setTimeout for task splitting to avoid clashes using sinon.useFakeTimers()
const nativeSetTimeout = __global.setTimeout;
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() {
nativeSetTimeout(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) {
const basenamePos = sResourceName.lastIndexOf('/');
const dotPos = sResourceName.lastIndexOf('.');
if ( dotPos > basenamePos ) {
return {
id: sResourceName.slice(0, dotPos),
type: sResourceName.slice(dotPos)
};
}
return {
id: sResourceName,
type: ''
};
}
const rJSSubTypes = /(\.controller|\.fragment|\.view|\.designtime|\.support)?.js$/;
function urnToBaseIDAndSubType(sResourceName) {
const m = rJSSubTypes.exec(sResourceName);
if ( m ) {
return {
baseID: sResourceName.slice(0, m.index),
subType: m[0]
};
}
}
const rDotSegmentAnywhere = /(?:^|\/)\.+(?=\/|$)/;
const 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) {
const p = sResourceName.search(rDotSegmentAnywhere);
// 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;
}
const aSegments = sResourceName.split('/');
// process path segments
let j = 0;
const l = aSegments.length;
for (let i = 0; i < l; i++) {
const 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) {
let sNamePrefix = sResourceName;
let p = sResourceName.length;
// 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");
let 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) {
// Make sure to have an absolute URL without query parameters or hash
// to check against absolute prefix URLs
sURL = pathOnly(resolveURL(sURL));
for (const 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
const sUrlPrefix = mUrlPrefixes[sNamePrefix].absoluteUrl.slice(0, -1);
if ( sURL.startsWith(sUrlPrefix) ) {
// calc resource name
let 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]?.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) {
let 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) {
if (decodeURI(sResourceName) !== sResourceName) {
throw new TypeError(`URL encoded module IDs are not supported: '${sResourceName}'`);
}
const mMap = findMapForContext(sRequestingResourceName);
// 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
let sPrefix = urnToIDAndType(sResourceName).id;
let 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 ) {
const sMappedResourceName = mMap[sPrefix] + sResourceName.slice(p);
if ( log.isLoggable() ) {
log.debug(`module ID ${sResourceName} mapped to ${sMappedResourceName}`);
}
return sMappedResourceName; // also skips a leading slash!
}
}
return sResourceName;
}
function getGlobalObject(oObject, aNames, l, bCreate) {
for (let i = 0; oObject && i < l; i++) {
if (!oObject[aNames[i]] && bCreate ) {
oObject[aNames[i]] = {};
}
oObject = oObject[aNames[i]];
}
return oObject;
}
function getGlobalProperty(sName) {
const 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) {
const aNames = sName ? sName.split(".") : [];
if ( aNames.length > 0 ) {
const 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.
*/
const 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 has 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>
*/
class Module {
/**
* Creates a new Module.
*
* @param {string} name Name of the module, including extension
*/
constructor(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;
}
deferred() {
if ( this._deferred == null ) {
const 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;
}
api() {
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
*/
ready(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((alias) => Module.get(alias).ready(value));
}
}
failWith(msg, cause) {
const err = makeModuleError(msg, this, cause);
this.fail(err);
return err;
}
fail(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);
this.aliases?.forEach((alias) => Module.get(alias).fail(err));
}
}
addPending(sDependency) {
(this.pending ??= []).push(sDependency);
}
addAlias(sAliasName) {
(this.aliases ??= []).push(sAliasName);
// add this module as pending dependency to the original
Module.get(sAliasName).addPending(this.name);
}
preload(url, data, bundle) {
if ( this.state === INITIAL && !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
*/
value() {
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
const 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
*/
dependsOn(oDependantModule) {
const dependant = oDependantModule.name,
visited = Object.create(null),
stack = log.isLoggable() ? [this.name, dependant] : undefined;
// log.debug("checking for a cycle between", this.name, "and", dependant);
function visit(mod) {
if ( !visited[mod] ) {
// log.debug(" ", mod);
visited[mod] = true;
const pending = mModules[mod]?.pending;
if (Array.isArray(pending) &&
(pending.includes(dependant) || pending.some(visit)) ) {
stack?.push(mod);
return true;
}
}
return false;
}
const result = this.name === dependant || visit(this.name);
if ( result && stack ) {
log.error("Dependency cycle detected: ",
stack.reverse().map((entry, idx) => `${"".padEnd(idx)} -> ${entry}`).join("\n").slice(4)
);
}
return result;
}
/**
* 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 get(sModuleName) {
const oModule = mModules[sModuleName] ??= new Module(sModuleName);
return oModule;
}
}
/*
* Determines the currently executing module.
*/
function getExecutingModule() {
if ( _execStack.length > 0 ) {
return _execStack[_execStack.length - 1].name;
}
return document.currentScript?.getAttribute("data-sap-ui-module");
}
// --------------------------------------------------------------------------------------------
let _globalDefine,
_globalDefineAMD;
function updateDefineAndInterceptAMDFlag(newDefine) {
// no change, do nothing
if ( _globalDefine === newDefine ) {
return;
}
// first cleanup on an old loader
if ( _globalDefine ) {
// restore old amd flag as normal property
Object.defineProperty(_globalDefine, "amd", {
value: _globalDefineAMD,
configurable: true,
enumerable: true,
writable: true
});
_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() {
const sCurrentModule = getExecutingModule();
if ( 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.
});
}
}
updateDefineAndInterceptAMDFlag(__global.define);
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 detect a change of the AMD loader");
}
// --------------------------------------------------------------------------------------------
function isModuleError(err) {
return 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) {
let modules = `'${module.name}'`;
if (isModuleError(cause)) {
// update the chain of modules (increasing the indent)
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
const message =
template.replace(/\{id\}/, modules).replace(/\{url\}/, module.url)
+ (cause ? ": " + cause.message : "");
const error = new Error(message);
error.name = "ModuleError";
error.cause = cause;
if ( 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, fnDeprecationMessage) {
// sModuleName must be a unified resource name of type .js
assert(/\.js$/.test(sModuleName), "must be a Javascript module");
const oModule = Module.get(sModuleName);
if ( oModule.state > INITIAL ) {
return oModule;
}
if ( log.isLoggable() ) {
log.debug(`${sLogPrefix}declare module '${sModuleName}'`);
}
// avoid cycles
oModule.state = READY;
oModule.deprecation = fnDeprecationMessage || undefined;
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) {
let 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);
}
const sModule = 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) {
const bLoggable = log.isLoggable();
const aQueueCopy = aQueue;
const iCurrentRun = iRun++;
let sModuleName = null;
// clear the queue and timer early, we've already taken a copy of the queue
this.clear();
// 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?.name;
// check whether there's a module definition for the requested module
aQueueCopy.forEach((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 ) {
const 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((entry) => `'${entry.name}'`)}]`);
}
aQueueCopy.forEach((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`);
}
};
}
let 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) {
const 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?.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) {
const oScript = document.createElement('SCRIPT');
oScript.src = oModule.url;
oScript.setAttribute("data-sap-ui-module", oModule.name);
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}`);
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"));
}
if ( sAlternativeURL !== undefined ) {
if ( mShims[oModule.name]?.amd ) {
oScript.setAttribute("data-sap-ui-module-amd", "true");
}
oScript.addEventListener('load', onload);
oScript.addEventListener('error', onerror);
}
document.head.appendChild(oScript);
}
/**
* If we have knowledge about the dependencies of the given module,
* we require them upfront, in parallel to the request for the module or
* its containing bundle.
*
* Note: a dependency is required even when it is in state PRELOADED already.
* Reason is that its transitive dependencies might not have been required yet.
*/
function requireDependenciesUpfront(sModuleName) {
const knownDependencies = mDepCache[sModuleName];
if ( Array.isArray(knownDependencies) ) {
mDepCache[sModuleName] = undefined;
const missingDeps = [];
knownDependencies.forEach((dep) => {
dep = getMappedName(dep, sModuleName);
// even if a module is PRELOADED, its transitive dependencies might not
if ( Module.get(dep).state <= INITIAL ) {
missingDeps.push(dep);
}
});
if ( missingDeps.length > 0 ) {
log.info(`preload missing dependencies for ${sModuleName}: ${missingDeps}`);
missi