'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;
+ /* eslint-disable no-bitwise */
for ( i = 0; 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() {
// <https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set>
- StringSet = window.Set || ( function () {
- /**
- * @private
- * @class
- */
- function StringSet() {
- this.set = Object.create( null );
- }
- StringSet.prototype.add = function ( value ) {
- this.set[ value ] = true;
+ /**
+ * @private
+ * @class
+ */
+ StringSet = window.Set || function StringSet() {
+ var set = Object.create( null );
+ this.add = function ( value ) {
+ set[ value ] = true;
};
- StringSet.prototype.has = function ( value ) {
- return value in this.set;
+ this.has = function ( value ) {
+ return value in set;
};
- return StringSet;
- }() );
+ };
}
/**
* @param {string} [data.module] Name of module which caused the error
*/
function logError( topic, data ) {
- /* eslint-disable no-console */
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
console.warn( e );
}
}
- /* eslint-enable no-console */
}
/**
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 ];
}
name = logName || key;
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.
},
/**
* @property
* @private
*/
- var registry = {},
+ var registry = Object.create( null ),
// Mapping of sources, keyed by source-id, values are strings.
//
// Format:
// 'sourceId': 'http://example.org/w/load.php'
// }
//
- sources = {},
+ sources = Object.create( null ),
// For queueModuleScript()
handlingPendingRequests = false,
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();
}
function sortDependencies( module, resolved, unresolved ) {
var i, deps, skip;
- if ( !hasOwn.call( registry, module ) ) {
+ if ( !( module in registry ) ) {
throw new Error( 'Unknown dependency: ' + module );
}
// Add base modules
if ( baseModules.indexOf( module ) === -1 ) {
- baseModules.forEach( function ( baseModule ) {
- if ( resolved.indexOf( baseModule ) === -1 ) {
- resolved.push( baseModule );
+ for ( i = 0; i < baseModules.length; i++ ) {
+ if ( resolved.indexOf( baseModules[ i ] ) === -1 ) {
+ resolved.push( baseModules[ i ] );
}
- } );
+ }
}
// Tracks down dependencies
return resolved;
}
+ /**
+ * Resolve a relative file path.
+ *
+ * For example, resolveRelativePath( '../foo.js', 'resources/src/bar/bar.js' )
+ * returns 'resources/src/foo.js'.
+ *
+ * @param {string} relativePath Relative file path, starting with ./ or ../
+ * @param {string} basePath Path of the file (not directory) relativePath is relative to
+ * @return {string|null} Resolved path, or null if relativePath does not start with ./ or ../
+ */
+ function resolveRelativePath( relativePath, basePath ) {
+ var prefixes, prefix, baseDirParts,
+ relParts = relativePath.match( /^((?:\.\.?\/)+)(.*)$/ );
+
+ if ( !relParts ) {
+ return null;
+ }
+
+ baseDirParts = basePath.split( '/' );
+ // basePath looks like 'foo/bar/baz.js', so baseDirParts looks like [ 'foo', 'bar, 'baz.js' ]
+ // Remove the file component at the end, so that we are left with only the directory path
+ baseDirParts.pop();
+
+ prefixes = relParts[ 1 ].split( '/' );
+ // relParts[ 1 ] looks like '../../', so prefixes looks like [ '..', '..', '' ]
+ // Remove the empty element at the end
+ prefixes.pop();
+
+ // For every ../ in the path prefix, remove one directory level from baseDirParts
+ while ( ( prefix = prefixes.pop() ) !== undefined ) {
+ if ( prefix === '..' ) {
+ baseDirParts.pop();
+ }
+ }
+
+ // If there's anything left of the base path, prepend it to the file path
+ return ( baseDirParts.length ? baseDirParts.join( '/' ) + '/' : '' ) + relParts[ 2 ];
+ }
+
+ /**
+ * Make a require() function scoped to a package file
+ * @private
+ * @param {Object} moduleObj Module object from the registry
+ * @param {string} basePath Path of the file this is scoped to. Used for relative paths.
+ * @return {Function}
+ */
+ function makeRequireFunction( moduleObj, basePath ) {
+ return function require( moduleName ) {
+ var fileName, fileContent, result, moduleParam,
+ scriptFiles = moduleObj.script.files;
+ fileName = resolveRelativePath( moduleName, basePath );
+ if ( fileName === null ) {
+ // Not a relative path, so it's a module name
+ return mw.loader.require( moduleName );
+ }
+
+ if ( !hasOwn.call( scriptFiles, fileName ) ) {
+ throw new Error( 'Cannot require() undefined file ' + fileName );
+ }
+ if ( hasOwn.call( moduleObj.packageExports, fileName ) ) {
+ // File has already been executed, return the cached result
+ return moduleObj.packageExports[ fileName ];
+ }
+
+ fileContent = scriptFiles[ fileName ];
+ if ( typeof fileContent === 'function' ) {
+ moduleParam = { exports: {} };
+ fileContent( makeRequireFunction( moduleObj, fileName ), moduleParam );
+ result = moduleParam.exports;
+ } else {
+ // fileContent is raw data, just pass it through
+ result = fileContent;
+ }
+ moduleObj.packageExports[ fileName ] = result;
+ return result;
+ };
+ }
+
/**
* Load and execute a script.
*
function queueModuleScript( src, moduleName, callback ) {
pendingRequests.push( function () {
// Keep in sync with execute()/runScript().
- if ( moduleName !== 'jquery' && hasOwn.call( registry, moduleName ) ) {
+ if ( moduleName !== 'jquery' ) {
window.require = mw.loader.require;
window.module = registry[ moduleName ].module;
}
// 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 );
}
} );
var key, value, media, i, urls, cssHandle, siteDeps, siteDepErr, runScript,
cssPending = 0;
- if ( !hasOwn.call( registry, module ) ) {
- throw new Error( 'Module has not been registered yet: ' + module );
- }
if ( registry[ module ].state !== 'loaded' ) {
throw new Error( 'Module in state "' + registry[ module ].state + '" may not be executed: ' + module );
}
$CODE.profileExecuteStart();
runScript = function () {
- var script, markModuleReady, nestedAddScript;
+ var script, markModuleReady, nestedAddScript, mainScript;
$CODE.profileScriptStart();
script = registry[ module ].script;
try {
if ( Array.isArray( script ) ) {
nestedAddScript( script, markModuleReady, 0 );
- } else if ( typeof script === 'function' ) {
- // Keep in sync with queueModuleScript() for debug mode
- if ( module === 'jquery' ) {
- // This is a special case for when 'jquery' itself is being loaded.
- // - The standard jquery.js distribution does not set `window.jQuery`
- // in CommonJS-compatible environments (Node.js, AMD, RequireJS, etc.).
- // - MediaWiki's 'jquery' module also bundles jquery.migrate.js, which
- // in a CommonJS-compatible environment, will use require('jquery'),
- // but that can't work when we're still inside that module.
- script();
+ } else if (
+ typeof script === 'function' || (
+ typeof script === 'object' &&
+ script !== null
+ )
+ ) {
+ if ( typeof script === 'function' ) {
+ // Keep in sync with queueModuleScript() for debug mode
+ if ( module === 'jquery' ) {
+ // This is a special case for when 'jquery' itself is being loaded.
+ // - The standard jquery.js distribution does not set `window.jQuery`
+ // in CommonJS-compatible environments (Node.js, AMD, RequireJS, etc.).
+ // - MediaWiki's 'jquery' module also bundles jquery.migrate.js, which
+ // in a CommonJS-compatible environment, will use require('jquery'),
+ // but that can't work when we're still inside that module.
+ script();
+ } else {
+ // Pass jQuery twice so that the signature of the closure which wraps
+ // the script can bind both '$' and 'jQuery'.
+ script( window.$, window.$, mw.loader.require, registry[ module ].module );
+ }
} else {
- // Pass jQuery twice so that the signature of the closure which wraps
- // the script can bind both '$' and 'jQuery'.
- script( window.$, window.$, mw.loader.require, registry[ module ].module );
+ mainScript = script.files[ script.main ];
+ if ( typeof mainScript !== 'function' ) {
+ 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(
+ makeRequireFunction( registry[ module ], script.main ),
+ registry[ module ].module
+ );
}
markModuleReady();
-
} else if ( typeof script === 'string' ) {
// 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
* or null if the module does not exist
*/
function getModuleKey( module ) {
- return hasOwn.call( registry, module ) ?
- ( module + '@' + registry[ module ].version ) : null;
+ return module in registry ? ( module + '@' + registry[ module ].version ) : null;
}
/**
* @param {string} [skip]
*/
function registerOne( module, version, dependencies, group, source, skip ) {
- if ( hasOwn.call( registry, module ) ) {
+ if ( module in registry ) {
throw new Error( 'module already registered: ' + module );
}
registry[ module ] = {
module: {
exports: {}
},
+ // module.export objects for each package file inside this module
+ packageExports: {},
version: String( version || '' ),
dependencies: dependencies || [],
group: typeof group === 'string' ? group : null,
// Appends a list of modules from the queue to the batch
for ( q = 0; q < queue.length; q++ ) {
// Only load modules which are registered
- if ( hasOwn.call( registry, queue[ q ] ) && registry[ queue[ q ] ].state === 'registered' ) {
+ if ( queue[ q ] in registry && registry[ queue[ q ] ].state === 'registered' ) {
// Prevent duplicate entries
if ( batch.indexOf( queue[ q ] ) === -1 ) {
batch.push( queue[ q ] );
addSource: function ( ids ) {
var id;
for ( id in ids ) {
- if ( hasOwn.call( sources, id ) ) {
+ if ( id in sources ) {
throw new Error( 'source already registered: ' + id );
}
sources[ id ] = ids[ id ];
* as '`[name]@[version]`". This version should match the requested version
* (from #batchRequest and #registry). This avoids race conditions (T117587).
* For back-compat with MediaWiki 1.27 and earlier, the version may be omitted.
- * @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 {Function|Array|string|Object} [script] Module code. This can be a function,
+ * a list of URLs to load via `<script src>`, a string for `$.globalEval()`, or an
+ * object like {"files": {"foo.js":function, "bar.js": function, ...}, "main": "foo.js"}.
+ * If an object is provided, the main file will be executed immediately, and the other
+ * files will only be executed if loaded via require(). If a function or string is
+ * provided, it will be executed/evaluated immediately. If an array is provided, all
+ * URLs in the array will be loaded immediately, and executed as soon as they arrive.
* @param {Object} [style] Should follow one of the following patterns:
*
* { "css": [css, ..] }
name = split.name,
version = split.version;
// Automatically register module
- if ( !hasOwn.call( registry, name ) ) {
+ if ( !( name in registry ) ) {
mw.loader.register( name );
}
// Check for duplicate implementation
return;
}
// 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 ];
/**
* Change the state of one or more modules.
*
- * @param {Object} modules Object of module name/state pairs
+ * @param {Object} states Object of module name/state pairs
*/
- state: function ( modules ) {
+ state: function ( states ) {
var module, state;
- for ( module in modules ) {
- state = modules[ module ];
- if ( !hasOwn.call( registry, module ) ) {
+ for ( module in states ) {
+ state = states[ module ];
+ if ( !( module in registry ) ) {
mw.loader.register( module );
}
setAndPropagate( module, state );
* in the registry.
*/
getVersion: function ( module ) {
- return hasOwn.call( registry, module ) ? registry[ module ].version : null;
+ return module in registry ? registry[ module ].version : null;
},
/**
* in the registry.
*/
getState: function ( module ) {
- return hasOwn.call( registry, module ) ? registry[ module ].state : null;
+ return module in registry ? registry[ module ].state : null;
},
/**
// 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;
this.stats.hits++;
return this.items[ key ];
}
+
this.stats.misses++;
return false;
},
*/
set: function ( module ) {
var key, args, src,
+ encodedScript,
descriptor = mw.loader.moduleRegistry[ module ];
key = getModuleKey( module );
}
try {
+ if ( typeof descriptor.script === 'function' ) {
+ encodedScript = String( descriptor.script );
+ } else if (
+ // Plain object: an object that is not null and is not an array
+ typeof descriptor.script === 'object' &&
+ descriptor.script &&
+ !Array.isArray( descriptor.script )
+ ) {
+ encodedScript = '{' +
+ '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 ) + ':' +
+ ( typeof value === 'function' ? value : JSON.stringify( value ) );
+ } ).join( ',' ) +
+ '}}';
+ } else {
+ encodedScript = JSON.stringify( descriptor.script );
+ }
args = [
JSON.stringify( key ),
- typeof descriptor.script === 'function' ?
- String( descriptor.script ) :
- JSON.stringify( descriptor.script ),
+ encodedScript,
JSON.stringify( descriptor.style ),
JSON.stringify( descriptor.messages ),
JSON.stringify( descriptor.templates )