resourceloader: Misc optimisations in startup module
[lhc/web/wiklou.git] / resources / src / startup / mediawiki.js
index c169487..5f02bd4 100644 (file)
@@ -13,8 +13,7 @@
        '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 ];
                                }
 
        defineFallbacks();
 
-       /* eslint-disable no-console */
        log = ( function () {
                /**
                 * Write a verbose message to the browser's console in debug mode.
                 *
                 * @param {...string} msg Messages to output to console
                 */
-               log.warn = console && console.warn && Function.prototype.bind ?
+               log.warn = console && console.warn ?
                        Function.prototype.bind.call( console.warn, console ) :
                        function () {};
 
                 * @since 1.26
                 * @param {...Mixed} msg Messages to output to console
                 */
-               log.error = console && console.error && Function.prototype.bind ?
+               log.error = console && console.error ?
                        Function.prototype.bind.call( console.error, console ) :
                        function () {};
 
                 * @param {string} [logName=key] Optional custom name for the feature.
                 *  This is used instead of `key` in the message and `mw.deprecate` tracking.
                 */
-               log.deprecate = !Object.defineProperty ? function ( obj, key, val ) {
-                       obj[ key ] = val;
-               } : function ( obj, key, val, msg, logName ) {
+               log.deprecate = function ( obj, key, val, msg, logName ) {
                        var stacks;
                        function maybeLog() {
                                var name,
                                        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 : '' )
                                        );
                                }
                        }
 
                return log;
        }() );
-       /* eslint-enable no-console */
 
        /**
         * @class mw
        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 )