( function ( $ ) {
'use strict';
- var mw,
+ var mw, StringSet, log,
hasOwn = Object.prototype.hasOwnProperty,
slice = Array.prototype.slice,
trackCallbacks = $.Callbacks( 'memory' ),
return hash;
}
+ StringSet = window.Set || ( function () {
+ /**
+ * @private
+ * @class
+ */
+ function StringSet() {
+ this.set = {};
+ }
+ StringSet.prototype.add = function ( value ) {
+ this.set[ value ] = true;
+ };
+ StringSet.prototype.has = function ( value ) {
+ return this.set.hasOwnProperty( value );
+ };
+ return StringSet;
+ }() );
+
/**
* Create an object that can be read from or written to from methods that allow
* interaction both with single and multiple properties at once.
}
};
+ log = ( function () {
+ // Also update the restoration of methods in mediawiki.log.js
+ // when adding or removing methods here.
+ var log = function () {},
+ console = window.console;
+
+ /**
+ * @class mw.log
+ * @singleton
+ */
+
+ /**
+ * Write a message to the console's warning channel.
+ * Actions not supported by the browser console are silently ignored.
+ *
+ * @param {...string} msg Messages to output to console
+ */
+ log.warn = console && console.warn && Function.prototype.bind ?
+ Function.prototype.bind.call( console.warn, console ) :
+ $.noop;
+
+ /**
+ * Write a message to the console's error channel.
+ *
+ * Most browsers provide a stacktrace by default if the argument
+ * is a caught Error object.
+ *
+ * @since 1.26
+ * @param {Error|...string} msg Messages to output to console
+ */
+ log.error = console && console.error && Function.prototype.bind ?
+ Function.prototype.bind.call( console.error, console ) :
+ $.noop;
+
+ /**
+ * Create a property in a host object that, when accessed, will produce
+ * a deprecation warning in the console.
+ *
+ * @param {Object} obj Host object of deprecated property
+ * @param {string} key Name of property to create in `obj`
+ * @param {Mixed} val The value this property should return when accessed
+ * @param {string} [msg] Optional text to include in the deprecation message
+ */
+ log.deprecate = !Object.defineProperty ? function ( obj, key, val ) {
+ obj[ key ] = val;
+ } : function ( obj, key, val, msg ) {
+ msg = 'Use of "' + key + '" is deprecated.' + ( msg ? ( ' ' + msg ) : '' );
+ var logged = new StringSet();
+ function uniqueTrace() {
+ var trace = new Error().stack;
+ if ( logged.has( trace ) ) {
+ return false;
+ }
+ logged.add( trace );
+ return true;
+ }
+ Object.defineProperty( obj, key, {
+ configurable: true,
+ enumerable: true,
+ get: function () {
+ if ( uniqueTrace() ) {
+ mw.track( 'mw.deprecate', key );
+ mw.log.warn( msg );
+ }
+ return val;
+ },
+ set: function ( newVal ) {
+ if ( uniqueTrace() ) {
+ mw.track( 'mw.deprecate', key );
+ mw.log.warn( msg );
+ }
+ val = newVal;
+ }
+ } );
+
+ };
+
+ return log;
+ }() );
+
/**
* @class mw
*/
},
/**
- * Dummy placeholder for {@link mw.log}
+ * No-op dummy placeholder for {@link mw.log} in debug mode.
*
* @method
*/
- log: ( function () {
- // Also update the restoration of methods in mediawiki.log.js
- // when adding or removing methods here.
- var log = function () {},
- console = window.console;
-
- /**
- * @class mw.log
- * @singleton
- */
-
- /**
- * Write a message to the console's warning channel.
- * Actions not supported by the browser console are silently ignored.
- *
- * @param {...string} msg Messages to output to console
- */
- log.warn = console && console.warn && Function.prototype.bind ?
- Function.prototype.bind.call( console.warn, console ) :
- $.noop;
-
- /**
- * Write a message to the console's error channel.
- *
- * Most browsers provide a stacktrace by default if the argument
- * is a caught Error object.
- *
- * @since 1.26
- * @param {Error|...string} msg Messages to output to console
- */
- log.error = console && console.error && Function.prototype.bind ?
- Function.prototype.bind.call( console.error, console ) :
- $.noop;
-
- /**
- * Create a property in a host object that, when accessed, will produce
- * a deprecation warning in the console with backtrace.
- *
- * @param {Object} obj Host object of deprecated property
- * @param {string} key Name of property to create in `obj`
- * @param {Mixed} val The value this property should return when accessed
- * @param {string} [msg] Optional text to include in the deprecation message
- */
- log.deprecate = !Object.defineProperty ? function ( obj, key, val ) {
- obj[ key ] = val;
- } : function ( obj, key, val, msg ) {
- /*globals Set */
- msg = 'Use of "' + key + '" is deprecated.' + ( msg ? ( ' ' + msg ) : '' );
- var logged, loggedIsSet, uniqueTrace;
- if ( window.Set ) {
- logged = new Set();
- loggedIsSet = true;
- } else {
- logged = {};
- loggedIsSet = false;
- }
- uniqueTrace = function () {
- var trace = new Error().stack;
- if ( loggedIsSet ) {
- if ( logged.has( trace ) ) {
- return false;
- }
- logged.add( trace );
- return true;
- } else {
- if ( logged.hasOwnProperty( trace ) ) {
- return false;
- }
- logged[ trace ] = 1;
- return true;
- }
- };
- Object.defineProperty( obj, key, {
- configurable: true,
- enumerable: true,
- get: function () {
- if ( uniqueTrace() ) {
- mw.track( 'mw.deprecate', key );
- mw.log.warn( msg );
- }
- return val;
- },
- set: function ( newVal ) {
- if ( uniqueTrace() ) {
- mw.track( 'mw.deprecate', key );
- mw.log.warn( msg );
- }
- val = newVal;
- }
- } );
-
- };
-
- return log;
- }() ),
+ log: log,
/**
* Client for ResourceLoader server end point.
cssBuffer = '',
cssBufferTimer = null,
cssCallbacks = $.Callbacks(),
- isIE9 = document.documentMode === 9;
+ isIE9 = document.documentMode === 9,
+ rAF = window.requestAnimationFrame || setTimeout;
function getMarker() {
if ( !marker ) {
if ( !cssBuffer || cssText.slice( 0, '@import'.length ) !== '@import' ) {
// Linebreak for somewhat distinguishable sections
cssBuffer += '\n' + cssText;
- // TODO: Using requestAnimationFrame would perform better by not injecting
- // styles while the browser is busy painting.
if ( !cssBufferTimer ) {
- cssBufferTimer = setTimeout( function () {
+ cssBufferTimer = rAF( function () {
+ // Wrap in anonymous function that takes no arguments
// Support: Firefox < 13
// Firefox 12 has non-standard behaviour of passing a number
// as first argument to a setTimeout callback.
j -= 1;
try {
if ( hasErrors ) {
- if ( $.isFunction( job.error ) ) {
+ if ( typeof job.error === 'function' ) {
job.error( new Error( 'Module ' + module + ' has failed dependencies' ), [ module ] );
}
} else {
- if ( $.isFunction( job.ready ) ) {
+ if ( typeof job.ready === 'function' ) {
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 {Object} [unresolved] Hash used to track the current dependency
- * chain; used to report loops in the dependency graph.
+ * @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
*/
function sortDependencies( module, resolved, unresolved ) {
}
// Resolves dynamic loader function and replaces it with its own results
- if ( $.isFunction( registry[ module ].dependencies ) ) {
+ if ( typeof registry[ module ].dependencies === 'function' ) {
registry[ module ].dependencies = registry[ module ].dependencies();
// Ensures the module's dependencies are always in an array
if ( typeof registry[ module ].dependencies !== 'object' ) {
}
// Create unresolved if not passed in
if ( !unresolved ) {
- unresolved = {};
+ unresolved = new StringSet();
}
// Tracks down dependencies
deps = registry[ module ].dependencies;
for ( i = 0; i < deps.length; i++ ) {
if ( $.inArray( deps[ i ], resolved ) === -1 ) {
- if ( unresolved[ deps[ i ] ] ) {
+ if ( unresolved.has( deps[ i ] ) ) {
throw new Error( mw.format(
'Circular reference detected: $1 -> $2',
module,
) );
}
- // Add to unresolved
- unresolved[ module ] = true;
+ unresolved.add( module );
sortDependencies( deps[ i ], resolved, unresolved );
}
}
// Force jQuery behaviour to be for crossDomain. Otherwise jQuery would use
// XHR for a same domain request instead of <script>, which 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 retreive
+ // since browsers cache XHR much less (if at all). And XHR means we retrieve
// text, so we'd need to $.globalEval, which then messes up line numbers.
crossDomain: true,
cache: true
registry[ module ].state = 'executing';
runScript = function () {
- var script, markModuleReady, nestedAddScript, legacyWait,
+ var script, markModuleReady, nestedAddScript, legacyWait, implicitDependencies,
// Expand to include dependencies since we have to exclude both legacy modules
// and their dependencies from the legacyWait (to prevent a circular dependency).
legacyModules = resolve( mw.config.get( 'wgResourceLoaderLegacyModules', [] ) );
- try {
- script = registry[ module ].script;
- markModuleReady = function () {
- registry[ module ].state = 'ready';
- handlePending( module );
- };
- nestedAddScript = function ( arr, callback, i ) {
- // Recursively call queueModuleScript() in its own callback
- // for each element of arr.
- if ( i >= arr.length ) {
- // We're at the end of the array
- callback();
- return;
- }
- queueModuleScript( arr[ i ], module ).always( function () {
- nestedAddScript( arr, callback, i + 1 );
- } );
- };
+ script = registry[ module ].script;
+ markModuleReady = function () {
+ registry[ module ].state = 'ready';
+ handlePending( module );
+ };
+ nestedAddScript = function ( arr, callback, i ) {
+ // Recursively call queueModuleScript() in its own callback
+ // for each element of arr.
+ if ( i >= arr.length ) {
+ // We're at the end of the array
+ callback();
+ return;
+ }
+
+ queueModuleScript( arr[ i ], module ).always( function () {
+ nestedAddScript( arr, callback, i + 1 );
+ } );
+ };
- legacyWait = ( $.inArray( module, legacyModules ) !== -1 )
- ? $.Deferred().resolve()
- : mw.loader.using( legacyModules );
+ implicitDependencies = ( $.inArray( module, legacyModules ) !== -1 )
+ ? []
+ : legacyModules;
+
+ if ( module === 'user' ) {
+ // Implicit dependency on the site module. Not real dependency because
+ // it should run after 'site' regardless of whether it succeeds or fails.
+ implicitDependencies.push( 'site' );
+ }
- legacyWait.always( function () {
+ legacyWait = implicitDependencies.length
+ ? mw.loader.using( implicitDependencies )
+ : $.Deferred().resolve();
+
+ legacyWait.always( function () {
+ try {
if ( $.isArray( script ) ) {
nestedAddScript( script, markModuleReady, 0 );
- } else if ( $.isFunction( script ) ) {
+ } else if ( typeof script === 'function' ) {
// Pass jQuery twice so that the signature of the closure which wraps
// the script can bind both '$' and 'jQuery'.
script( $, $, mw.loader.require, registry[ module ].module );
// Site and user modules are legacy scripts that run in the global scope.
// This is transported as a string instead of a function to avoid needing
// to use string manipulation to undo the function wrapper.
- if ( module === 'user' ) {
- // Implicit dependency on the site module. Not real dependency because
- // it should run after 'site' regardless of whether it succeeds or fails.
- mw.loader.using( 'site' ).always( function () {
- $.globalEval( script );
- markModuleReady();
- } );
- } else {
- $.globalEval( script );
- markModuleReady();
- }
+ $.globalEval( script );
+ markModuleReady();
+
} else {
// Module without script
markModuleReady();
}
- } );
- } catch ( e ) {
- // This needs to NOT use mw.log because these errors are common in production mode
- // and not in debug mode, such as when a symbol that should be global isn't exported
- registry[ module ].state = 'error';
- mw.track( 'resourceloader.exception', { exception: e, module: module, source: 'module-execute' } );
- handlePending( module );
- }
+ } catch ( e ) {
+ // Use mw.track instead of mw.log because these errors are common in production mode
+ // (e.g. undefined variable), and mw.log is only enabled in debug mode.
+ registry[ module ].state = 'error';
+ mw.track( 'resourceloader.exception', { exception: e, module: module, source: 'module-execute' } );
+ handlePending( module );
+ }
+ } );
};
// Add localizations to message system
* @param {Array} modules Modules array
*/
function resolveIndexedDependencies( modules ) {
- $.each( modules, function ( idx, module ) {
- if ( module[ 2 ] ) {
- module[ 2 ] = $.map( module[ 2 ], function ( dep ) {
- return typeof dep === 'number' ? modules[ dep ][ 0 ] : dep;
- } );
+ var i, j, deps;
+ function resolveIndex( dep ) {
+ return typeof dep === 'number' ? modules[ dep ][ 0 ] : dep;
+ }
+ for ( i = 0; i < modules.length; i++ ) {
+ deps = modules[ i ][ 2 ];
+ if ( deps ) {
+ for ( j = 0; j < deps.length; j++ ) {
+ deps[ j ] = resolveIndex( deps[ j ] );
+ }
}
- } );
+ }
}
/**
}
}
+ // Now that the queue has been processed into a batch, clear the queue.
+ // This MUST happen before we initiate any eval or network request. Otherwise,
+ // it is possible for a cached script to instantly trigger the same work queue
+ // again; all before we've cleared it causing each request to include modules
+ // which are already loaded.
+ queue = [];
+
+ if ( !batch.length ) {
+ return;
+ }
+
mw.loader.store.init();
if ( mw.loader.store.enabled ) {
concatSource = [];
}
}
- // Now that the queue has been processed into a batch, clear up the queue.
- // This MUST happen before we initiate any network request. Else it's possible
- // that a script will be locally cached, instantly load, and work the queue
- // again; all before we've cleared it causing each request to include modules
- // which are already loaded.
- queue = [];
-
batchRequest( batch );
},
* @param {string} [skip=null] Script body of the skip function
*/
register: function ( module, version, dependencies, group, source, skip ) {
- var i;
+ var i, deps;
// Allow multiple registration
if ( typeof module === 'object' ) {
resolveIndexedDependencies( module );
if ( hasOwn.call( registry, module ) ) {
throw new Error( 'module already registered: ' + module );
}
+ if ( typeof dependencies === 'string' ) {
+ // A single module name
+ deps = [ dependencies ];
+ } else if ( typeof dependencies === 'object' || typeof dependencies === 'function' ) {
+ // Array of module names or a function that returns an array
+ deps = dependencies;
+ }
// List the module as registered
registry[ module ] = {
// Exposed to execute() for mw.loader.implement() closures.
exports: {}
},
version: version !== undefined ? String( version ) : '',
- dependencies: [],
+ dependencies: deps || [],
group: typeof group === 'string' ? group : null,
source: typeof source === 'string' ? source : 'local',
state: 'registered',
skip: typeof skip === 'string' ? skip : null
};
- if ( typeof dependencies === 'string' ) {
- // Allow dependencies to be given as a single module name
- registry[ module ].dependencies = [ dependencies ];
- } else if ( typeof dependencies === 'object' || $.isFunction( dependencies ) ) {
- // Allow dependencies to be given as an array of module names
- // or a function which returns an array
- registry[ module ].dependencies = dependencies;
- }
},
/**
* response contain calls to this function.
*
* @param {string} module Name of module
- * @param {Function|Array} [script] Function with module code or Array of URLs to
- * be used as the src attribute of a new `<script>` tag.
+ * @param {Function|Array|string} [script] Function with module code, list of URLs
+ * to load via `<script src>`, or string of module code for `$.globalEval()`.
* @param {Object} [style] Should follow one of the following patterns:
*
* { "css": [css, ..] }
if ( !hasOwn.call( registry, module ) ) {
mw.loader.register( module );
}
- if ( $.inArray( state, [ 'ready', 'error', 'missing' ] ) !== -1
- && registry[ module ].state !== state ) {
+ registry[ module ].state = state;
+ if ( $.inArray( state, [ 'ready', 'error', 'missing' ] ) !== -1 ) {
// Make sure pending modules depending on this one get executed if their
// dependencies are now fulfilled!
- registry[ module ].state = state;
handlePending( module );
- } else {
- registry[ module ].state = state;
}
},
// Unversioned, private, or site-/user-specific
( !descriptor.version || $.inArray( descriptor.group, [ 'private', 'user' ] ) !== -1 ) ||
// Partial descriptor
+ // (e.g. skipped module, or style module with state=ready)
$.inArray( undefined, [ descriptor.script, descriptor.style,
descriptor.messages, descriptor.templates ] ) !== -1
) {
* reference, so that debugging tools loaded later are supported (e.g. Firebug Lite in IE).
*
* @private
- * @method log_
* @param {string} topic Stream name passed by mw.track
* @param {Object} data Data passed by mw.track
* @param {Error} [data.exception]
* @param {string} data.source Error source
* @param {string} [data.module] Name of module which caused the error
*/
- function log( topic, data ) {
+ function logError( topic, data ) {
var msg,
e = data.exception,
source = data.source,
}
// Subscribe to error streams
- mw.trackSubscribe( 'resourceloader.exception', log );
- mw.trackSubscribe( 'resourceloader.assert', log );
+ mw.trackSubscribe( 'resourceloader.exception', logError );
+ mw.trackSubscribe( 'resourceloader.assert', logError );
/**
* Fired when all modules associated with the page have finished loading.
var loading = $.grep( mw.loader.getModuleNames(), function ( module ) {
return mw.loader.getState( module ) === 'loading';
} );
- // In order to use jQuery.when (which stops early if one of the promises got rejected)
- // cast any loading failures into successes. We only need a callback, not the module.
- loading = $.map( loading, function ( module ) {
- return mw.loader.using( module ).then( null, function () {
- return $.Deferred().resolve();
+ // We only need a callback, not any actual module. First try a single using()
+ // for all loading modules. If one fails, fall back to tracking each module
+ // separately via $.when(), this is expensive.
+ loading = mw.loader.using( loading ).then( null, function () {
+ var all = $.map( loading, function ( module ) {
+ return mw.loader.using( module ).then( null, function () {
+ return $.Deferred().resolve();
+ } );
} );
+ return $.when.apply( $, all );
} );
- $.when.apply( $, loading ).then( function () {
+ loading.then( function () {
mwPerformance.mark( 'mwLoadEnd' );
mw.hook( 'resourceloader.loadEnd' ).fire();
} );