'use strict';
var mw, StringSet, log,
- hasOwn = Object.prototype.hasOwnProperty,
- trackQueue = [];
+ hasOwn = Object.prototype.hasOwnProperty;
/**
* FNV132 hash function
* @return {string} hash as an seven-character base 36 string
*/
function fnv132( str ) {
- /* eslint-disable no-bitwise */
var hash = 0x811C9DC5,
- i;
+ i = 0;
- for ( i = 0; i < str.length; i++ ) {
+ /* eslint-disable no-bitwise */
+ for ( ; i < str.length; i++ ) {
hash += ( hash << 1 ) + ( hash << 4 ) + ( hash << 7 ) + ( hash << 8 ) + ( hash << 24 );
hash ^= str.charCodeAt( i );
}
while ( hash.length < 7 ) {
hash = '0' + hash;
}
+ /* eslint-enable no-bitwise */
return hash;
- /* eslint-enable no-bitwise */
}
function defineFallbacks() {
* @private
* @class
*/
- StringSet = window.Set || function StringSet() {
+ StringSet = window.Set || function () {
var set = Object.create( null );
- this.add = function ( value ) {
- set[ value ] = true;
- };
- this.has = function ( value ) {
- return value in set;
+ return {
+ add: function ( value ) {
+ set[ value ] = true;
+ },
+ has: function ( value ) {
+ return value in set;
+ }
};
};
}
function logError( topic, data ) {
var msg,
e = data.exception,
- source = data.source,
- module = data.module,
console = window.console;
if ( console && console.log ) {
- msg = ( e ? 'Exception' : 'Error' ) + ' in ' + source;
- if ( module ) {
- msg += ' in module ' + module;
- }
- msg += ( e ? ':' : '.' );
+ msg = ( e ? 'Exception' : 'Error' ) +
+ ' in ' + data.source +
+ ( data.module ? ' in module ' + data.module : '' ) +
+ ( e ? ':' : '.' );
+
console.log( msg );
// If we have an exception object, log it to the warning channel to trigger
this.set = function ( selection, value ) {
var s;
if ( arguments.length > 1 ) {
- if ( typeof selection !== 'string' ) {
- return false;
+ if ( typeof selection === 'string' ) {
+ setGlobalMapValue( this, selection, value );
+ return true;
}
- setGlobalMapValue( this, selection, value );
- return true;
- }
- if ( typeof selection === 'object' ) {
+ } else if ( typeof selection === 'object' ) {
for ( s in selection ) {
setGlobalMapValue( this, s, selection[ s ] );
}
var s;
// Use `arguments.length` because `undefined` is also a valid value.
if ( arguments.length > 1 ) {
- if ( typeof selection !== 'string' ) {
- return false;
+ // Set one key
+ if ( typeof selection === 'string' ) {
+ this.values[ selection ] = value;
+ return true;
}
- this.values[ selection ] = value;
- return true;
- }
- if ( typeof selection === 'object' ) {
+ } else if ( typeof selection === 'object' ) {
+ // Set multiple keys
for ( s in selection ) {
this.values[ s ] = selection[ s ];
}
mw.track( 'mw.deprecate', name );
}
mw.log.warn(
- 'Use of "' + name + '" is deprecated.' + ( msg ? ( ' ' + msg ) : '' )
+ 'Use of "' + name + '" is deprecated.' + ( msg ? ' ' + msg : '' )
);
}
}
mw = {
redefineFallbacksForTest: function () {
if ( !window.QUnit ) {
- throw new Error( 'Reset not allowed outside unit tests' );
+ throw new Error( 'Not allowed' );
}
defineFallbacks();
},
* @return {number} Current time
*/
now: function () {
- // Optimisation: Define the shortcut on first call, not at module definition.
+ // Optimisation: Make startup initialisation faster by defining the
+ // shortcut on first call, not at module definition.
var perf = window.performance,
navStart = perf && perf.timing && perf.timing.navigationStart;
// Define the relevant shortcut
- mw.now = navStart && typeof perf.now === 'function' ?
+ mw.now = navStart && perf.now ?
function () { return navStart + perf.now(); } :
Date.now;
/**
* List of all analytic events emitted so far.
*
+ * Exposed only for use by mediawiki.base.
+ *
* @private
* @property {Array}
*/
- trackQueue: trackQueue,
+ trackQueue: [],
track: function ( topic, data ) {
- trackQueue.push( { topic: topic, timeStamp: mw.now(), data: data } );
- // The base module extends this method to fire events here
+ mw.trackQueue.push( { topic: topic, timeStamp: mw.now(), data: data } );
+ // This method is extended by mediawiki.base to also fire events.
},
/**
* 'dependencies': ['required.foo', 'bar.also', ...]
* 'group': 'somegroup', (or) null
* 'source': 'local', (or) 'anotherwiki'
- * 'skip': 'return !!window.Example', (or) null
+ * 'skip': 'return !!window.Example', (or) null, (or) boolean result of skip
* 'module': export Object
*
* // Set from execute() or mw.loader.state()
* 'state': 'registered', 'loaded', 'loading', 'ready', 'error', or 'missing'
*
* // Optionally added at run-time by mw.loader.implement()
- * 'skipped': true
* 'script': closure, array of urls, or string
* 'style': { ... } (see #execute)
* 'messages': { 'key': 'value', ... }
/**
* @private
- * @param {Array} modules List of module names
+ * @param {string[]} modules List of module names
* @return {string} Hash of concatenated version hashes.
*/
function getCombinedVersion( modules ) {
* execute the module or job now.
*
* @private
- * @param {Array} modules Names of modules to be checked
+ * @param {string[]} modules Names of modules to be checked
* @return {boolean} True if all modules are in state 'ready', false otherwise
*/
function allReady( modules ) {
- var i;
- for ( i = 0; i < modules.length; i++ ) {
+ var i = 0;
+ for ( ; i < modules.length; i++ ) {
if ( mw.loader.getState( modules[ i ] ) !== 'ready' ) {
return false;
}
* @return {boolean} True if no modules are in state 'error' or 'missing', false otherwise
*/
function anyFailed( modules ) {
- var i, state;
- for ( i = 0; i < modules.length; i++ ) {
+ var state,
+ i = 0;
+ for ( ; i < modules.length; i++ ) {
state = mw.loader.getState( modules[ i ] );
if ( state === 'error' || state === 'missing' ) {
return true;
i -= 1;
try {
if ( failed && job.error ) {
- job.error( new Error( 'Module has failed dependencies' ), job.dependencies );
+ job.error( new Error( 'Failed dependencies' ), job.dependencies );
} else if ( !failed && job.ready ) {
job.ready();
}
* dependencies, such that later modules depend on earlier modules. The array
* contains the module names. If the array contains already some module names,
* this function appends its result to the pre-existing array.
- * @param {StringSet} [unresolved] Used to track the current dependency
- * chain, and to report loops in the dependency graph.
- * @throws {Error} If any unregistered module or a dependency loop is encountered
+ * @param {StringSet} [unresolved] Used to detect loops in the dependency graph.
+ * @throws {Error} If an unknown module or a circular dependency is encountered
*/
function sortDependencies( module, resolved, unresolved ) {
- var i, deps, skip;
+ var i, skip, deps;
if ( !( module in registry ) ) {
- throw new Error( 'Unknown dependency: ' + module );
+ throw new Error( 'Unknown module: ' + module );
}
- if ( registry[ module ].skip !== null ) {
+ if ( typeof registry[ module ].skip === 'string' ) {
// eslint-disable-next-line no-new-func
- skip = new Function( registry[ module ].skip );
- registry[ module ].skip = null;
- if ( skip() ) {
- registry[ module ].skipped = true;
+ skip = ( new Function( registry[ module ].skip )() );
+ registry[ module ].skip = !!skip;
+ if ( skip ) {
registry[ module ].dependencies = [];
setAndPropagate( module, 'ready' );
return;
}
}
- if ( resolved.indexOf( module ) !== -1 ) {
- // Module already resolved; nothing to do
- return;
- }
// Create unresolved if not passed in
if ( !unresolved ) {
unresolved = new StringSet();
}
- // Add base modules
- if ( baseModules.indexOf( module ) === -1 ) {
- baseModules.forEach( function ( baseModule ) {
- if ( resolved.indexOf( baseModule ) === -1 ) {
- resolved.push( baseModule );
- }
- } );
- }
-
- // Tracks down dependencies
+ // Track down dependencies
deps = registry[ module ].dependencies;
unresolved.add( module );
for ( i = 0; i < deps.length; i++ ) {
sortDependencies( deps[ i ], resolved, unresolved );
}
}
+
resolved.push( module );
}
* @throws {Error} If an unregistered module or a dependency loop is encountered
*/
function resolve( modules ) {
- var i, resolved = [];
- for ( i = 0; i < modules.length; i++ ) {
+ // Always load base modules
+ var resolved = baseModules.slice(),
+ i = 0;
+ for ( ; i < modules.length; i++ ) {
sortDependencies( modules[ i ], resolved );
}
return resolved;
* @return {Array} List of dependencies.
*/
function resolveStubbornly( modules ) {
- var i, saved, resolved = [];
- for ( i = 0; i < modules.length; i++ ) {
+ var saved,
+ // Always load base modules
+ resolved = baseModules.slice(),
+ i = 0;
+ for ( ; i < modules.length; i++ ) {
saved = resolved.slice();
try {
sortDependencies( modules[ i ], resolved );
* @param {Function} [callback] Callback to run after request resolution
*/
function addScript( src, callback ) {
+ // Use a <script> element rather than XHR. Using XHR changes the request
+ // headers (potentially missing a cache hit), and reduces caching in general
+ // since browsers cache XHR much less (if at all). And XHR means we retrieve
+ // text, so we'd need to eval, which then messes up line numbers.
+ // The drawback is that <script> does not offer progress events, feedback is
+ // only given after downloading, parsing, and execution have completed.
var script = document.createElement( 'script' );
script.src = src;
script.onload = script.onerror = function () {
if ( script.parentNode ) {
script.parentNode.removeChild( script );
}
- script = null;
if ( callback ) {
callback();
callback = null;
* Utility function for execute()
*
* @ignore
- * @param {string} [media] Media attribute
* @param {string} url URL
+ * @param {string} [media] Media attribute
+ * @param {Node|null} [nextNode]
*/
- function addLink( media, url ) {
+ function addLink( url, media, nextNode ) {
var el = document.createElement( 'link' );
el.rel = 'stylesheet';
// see #addEmbeddedCSS, T33676, T43331, and T49277 for details.
el.href = url;
- if ( marker && marker.parentNode ) {
- marker.parentNode.insertBefore( el, marker );
+ if ( nextNode && nextNode.parentNode ) {
+ nextNode.parentNode.insertBefore( el, nextNode );
} else {
document.head.appendChild( el );
}
// these as the server will deny them anyway (T101806).
if ( registry[ module ].group === 'private' ) {
setAndPropagate( module, 'error' );
- return;
+ } else {
+ queue.push( module );
}
- queue.push( module );
}
} );
} else {
mainScript = script.files[ script.main ];
if ( typeof mainScript !== 'function' ) {
- throw new Error( 'Main script file ' + script.main + ' in module ' + module +
- 'must be of type function, is of type ' + typeof mainScript );
+ throw new Error( 'Main file ' + script.main + ' in module ' + module +
+ ' must be of type function, found ' + typeof mainScript );
}
// jQuery parameters are not passed for multi-file modules
mainScript(
for ( i = 0; i < value.length; i++ ) {
if ( key === 'bc-url' ) {
// back-compat: { <media>: [url, ..] }
- addLink( media, value[ i ] );
+ addLink( value[ i ], media, marker );
} else if ( key === 'css' ) {
// { "css": [css, ..] }
addEmbeddedCSS( value[ i ], cssHandle() );
for ( media in value ) {
urls = value[ media ];
for ( i = 0; i < urls.length; i++ ) {
- addLink( media, urls[ i ] );
+ addLink( urls[ i ], media, marker );
}
}
}
* to a query string of the form `foo.bar,baz|bar.baz,quux`.
*
* See `ResourceLoader::makePackedModulesString()` in PHP, of which this is a port.
- * On the server, unpacking is done by `ResourceLoaderContext::expandModuleNames()`.
+ * On the server, unpacking is done by `ResourceLoader::expandModuleNames()`.
*
* Note: This is only half of the logic, the other half has to be in #batchRequest(),
* because its implementation needs to keep track of potential string size in order
* @param {string[]} batch
*/
function batchRequest( batch ) {
- var reqBase, splits, maxQueryLength, b, bSource, bGroup,
+ var reqBase, splits, b, bSource, bGroup,
source, group, i, modules, sourceLoadScript,
currReqBase, currReqBaseLength, moduleMap, currReqModules, l,
lastDotIndex, prefix, suffix, bytesAdded;
batch.sort();
// Query parameters common to all requests
- reqBase = {
- skin: mw.config.get( 'skin' ),
- lang: mw.config.get( 'wgUserLanguage' ),
- debug: mw.config.get( 'debug' )
- };
- maxQueryLength = mw.config.get( 'wgResourceLoaderMaxQueryLength', 2000 );
+ reqBase = $VARS.reqBase;
// Split module list by source and by group.
splits = Object.create( null );
modules[ i ].length + 3; // '%7C'.length == 3
// If the url would become too long, create a new one, but don't create empty requests
- if ( maxQueryLength > 0 && currReqModules.length && l + bytesAdded > maxQueryLength ) {
+ if ( currReqModules.length && l + bytesAdded > mw.loader.maxQueryLength ) {
// Dispatch what we've got...
doRequest();
// .. and start again.
moduleMap = Object.create( null );
currReqModules = [];
- mw.track( 'resourceloader.splitRequest', { maxQueryLength: maxQueryLength } );
+ mw.track( 'resourceloader.splitRequest', { maxQueryLength: mw.loader.maxQueryLength } );
}
if ( !moduleMap[ prefix ] ) {
moduleMap[ prefix ] = [];
*/
moduleRegistry: registry,
+ /**
+ * Exposed for testing and debugging only.
+ *
+ * @see #batchRequest
+ * @property
+ * @private
+ */
+ maxQueryLength: $VARS.maxQueryLength,
+
/**
* @inheritdoc #newStyleTag
* @method
* @private
*/
work: function () {
- var q, batch, implementations, sourceModules;
-
- batch = [];
+ var implementations, sourceModules,
+ batch = [],
+ q = 0;
// Appends a list of modules from the queue to the batch
- for ( q = 0; q < queue.length; q++ ) {
+ for ( ; q < queue.length; q++ ) {
// Only load modules which are registered
if ( queue[ q ] in registry && registry[ queue[ q ] ].state === 'registered' ) {
// Prevent duplicate entries
* "text/javascript"; if no type is provided, text/javascript is assumed.
*/
load: function ( modules, type ) {
- var filtered, l;
-
- // Allow calling with a url or single dependency as a string
- if ( typeof modules === 'string' ) {
- // "https://example.org/x.js", "http://example.org/x.js", "//example.org/x.js", "/x.js"
- if ( /^(https?:)?\/?\//.test( modules ) ) {
- if ( type === 'text/css' ) {
- l = document.createElement( 'link' );
- l.rel = 'stylesheet';
- l.href = modules;
- document.head.appendChild( l );
- return;
- }
- if ( type === 'text/javascript' || type === undefined ) {
- addScript( modules );
- return;
- }
+ if ( typeof modules === 'string' && /^(https?:)?\/?\//.test( modules ) ) {
+ // Called with a url like so:
+ // - "https://example.org/x.js"
+ // - "http://example.org/x.js"
+ // - "//example.org/x.js"
+ // - "/x.js"
+ if ( type === 'text/css' ) {
+ addLink( modules );
+ } else if ( type === 'text/javascript' || type === undefined ) {
+ addScript( modules );
+ } else {
// Unknown type
- throw new Error( 'invalid type for external url, must be text/css or text/javascript. not ' + type );
+ throw new Error( 'type must be text/css or text/javascript, found ' + type );
}
- // Called with single module
- modules = [ modules ];
+ } else {
+ // One or more modules
+ modules = typeof modules === 'string' ? [ modules ] : modules;
+ // Resolve modules into flat list for internal queuing.
+ // This also filters out unknown modules and modules with
+ // unknown dependencies, allowing the rest to continue. (T36853)
+ enqueue( resolveStubbornly( modules ), undefined, undefined );
}
-
- // Filter out top-level modules that are unknown or failed to load before.
- filtered = modules.filter( function ( module ) {
- var state = mw.loader.getState( module );
- return state !== 'error' && state !== 'missing';
- } );
- // Resolve remaining list using the known dependency tree.
- // This also filters out modules with unknown dependencies. (T36853)
- filtered = resolveStubbornly( filtered );
- // Some modules are not yet ready, add to module load queue.
- enqueue( filtered, undefined, undefined );
},
/**
// Only ready modules can be required
if ( state !== 'ready' ) {
// Module may've forgotten to declare a dependency
- throw new Error( 'Module "' + moduleName + '" is not loaded.' );
+ throw new Error( 'Module "' + moduleName + '" is not loaded' );
}
return registry[ moduleName ].module.exports;
return;
}
} catch ( e ) {
- mw.trackError( 'resourceloader.exception', {
- exception: e,
- source: 'store-localstorage-init'
- } );
+ // Perhaps localStorage was disabled by the user, or got corrupted.
+ // See point 3 and 4 below. (T195647)
}
// If we get here, one of four things happened:
this.stats.hits++;
return this.items[ key ];
}
+
this.stats.misses++;
return false;
},
try {
if ( typeof descriptor.script === 'function' ) {
+ // Function literal: cast to string
encodedScript = String( descriptor.script );
} else if (
- // Plain object: an object that is not null and is not an array
+ // Plain object: serialise as object literal (not JSON),
+ // making sure to preserve the functions.
typeof descriptor.script === 'object' &&
descriptor.script &&
!Array.isArray( descriptor.script )
) {
encodedScript = '{' +
- '"main":' + JSON.stringify( descriptor.script.main ) + ',' +
- '"files":{' +
+ 'main:' + JSON.stringify( descriptor.script.main ) + ',' +
+ 'files:{' +
Object.keys( descriptor.script.files ).map( function ( key ) {
var value = descriptor.script.files[ key ];
return JSON.stringify( key ) + ':' +
} ).join( ',' ) +
'}}';
} else {
+ // Array of urls, or null.
encodedScript = JSON.stringify( descriptor.script );
}
args = [