/*! * Defines mw.loader, the infrastructure for loading ResourceLoader * modules. * * This file is appended directly to the code in startup/mediawiki.js */ /* global $VARS, $CODE, mw */ ( function () { 'use strict'; var store, hasOwn = Object.hasOwnProperty; /** * Client for ResourceLoader server end point. * * This client is in charge of maintaining the module registry and state * machine, initiating network (batch) requests for loading modules, as * well as dependency resolution and execution of source code. * * @see * @namespace mw.loader */ /** * FNV132 hash function * * This function implements the 32-bit version of FNV-1. * It is equivalent to hash( 'fnv132', ... ) in PHP, except * its output is base 36 rather than hex. * See * * @private * @param {string} str String to hash * @return {string} hash as a five-character base 36 string */ function fnv132( str ) { var hash = 0x811C9DC5; /* eslint-disable no-bitwise */ for ( var i = 0; i < str.length; i++ ) { hash += ( hash << 1 ) + ( hash << 4 ) + ( hash << 7 ) + ( hash << 8 ) + ( hash << 24 ); hash ^= str.charCodeAt( i ); } hash = ( hash >>> 0 ).toString( 36 ).slice( 0, 5 ); /* eslint-enable no-bitwise */ while ( hash.length < 5 ) { hash = '0' + hash; } return hash; } /** * Fired via mw.track on various resource loading errors. * * eslint-disable jsdoc/valid-types * * @event ~'resourceloader.exception' * @ignore * @param {Error|Mixed} e The error that was thrown. Almost always an Error * object, but in theory module code could manually throw something else, and that * might also end up here. * @param {string} [module] Name of the module which caused the error. Omitted if the * error is not module-related or the module cannot be easily identified due to * batched handling. * @param {string} source Source of the error. Possible values: * * - load-callback: exception thrown by user callback * - module-execute: exception thrown by module code * - resolve: failed to sort dependencies for a module in mw.loader.load * - store-eval: could not evaluate module code cached in localStorage * - store-localstorage-json: JSON conversion error in mw.loader.store * - store-localstorage-update: localStorage conversion error in mw.loader.store. */ /** * Mapping of registered modules. * * See #implement and #execute for exact details on support for script, style and messages. * * @example // Format: * { * 'moduleName': { * // From mw.loader.register() * 'version': '#####' (five-character hash) * 'dependencies': ['required.foo', 'bar.also', ...] * 'group': string, integer, (or) null * 'source': 'local', (or) 'anotherwiki' * 'skip': 'return !!window.Example;', (or) null, (or) boolean result of skip * 'module': export Object * * // Set by execute() or mw.loader.state() * // See mw.loader.getState() for documentation of the state machine * 'state': 'registered', 'loading', 'loaded', 'executing', 'ready', 'error', or 'missing' * * // Optionally added at run-time by mw.loader.impl() * 'script': closure, array of urls, or string * 'style': { ... } (see #execute) * 'messages': { 'key': 'value', ... } * } * } * * @property {Object} * @private */ var registry = Object.create( null ), // Mapping of sources, keyed by source-id, values are strings. // // Format: // // { // 'sourceId': 'http://example.org/w/load.php' // } // sources = Object.create( null ), // For queueModuleScript() handlingPendingRequests = false, pendingRequests = [], // List of modules to be loaded queue = [], /** * List of callback jobs waiting for modules to be ready. * * Jobs are created by #enqueue() and run by #doPropagation(). * Typically when a job is created for a module, the job's dependencies contain * both the required module and all its recursive dependencies. * * @example // Format: * { * 'dependencies': [ module names ], * 'ready': Function callback * 'error': Function callback * } * * @property {Object[]} jobs * @private */ jobs = [], // For #setAndPropagate() and #doPropagation() willPropagate = false, errorModules = [], /** * @private * @property {Array} baseModules */ baseModules = $VARS.baseModules, /** * For #addEmbeddedCSS() and #addLink() * * @private * @property {HTMLElement|null} marker */ marker = document.querySelector( 'meta[name="ResourceLoaderDynamicStyles"]' ), // For #addEmbeddedCSS() lastCssBuffer; /** * Append an HTML element to `document.head` or before a specified node. * * @private * @param {HTMLElement} el * @param {Node|null} [nextNode] */ function addToHead( el, nextNode ) { if ( nextNode && nextNode.parentNode ) { nextNode.parentNode.insertBefore( el, nextNode ); } else { document.head.appendChild( el ); } } /** * Create a new style element and add it to the DOM. * Stable for use in gadgets. * * @method mw.loader.addStyleTag * @param {string} text CSS text * @param {Node|null} [nextNode] The element where the style tag * should be inserted before * @return {HTMLStyleElement} Reference to the created style element */ function newStyleTag( text, nextNode ) { var el = document.createElement( 'style' ); el.appendChild( document.createTextNode( text ) ); addToHead( el, nextNode ); return el; } /** * @private * @param {Object} cssBuffer */ function flushCssBuffer( cssBuffer ) { // Make sure the next call to addEmbeddedCSS() starts a new buffer. // This must be done before we run the callbacks, as those may end up // queueing new chunks which would be lost otherwise (T105973). // // There can be more than one buffer in-flight (given "@import", and // generally due to race conditions). Only tell addEmbeddedCSS() to // start a new buffer if we're currently flushing the last one that it // started. If we're flushing an older buffer, keep the last one open. if ( cssBuffer === lastCssBuffer ) { lastCssBuffer = null; } newStyleTag( cssBuffer.cssText, marker ); for ( var i = 0; i < cssBuffer.callbacks.length; i++ ) { cssBuffer.callbacks[ i ](); } } /** * Add a bit of CSS text to the current browser page. * * The creation and insertion of the `