resourceloader: Micro-optimise mw.loader.register version string setting
[lhc/web/wiklou.git] / resources / src / startup / mediawiki.js
index 8d42c0f..06eb80e 100644 (file)
@@ -7,6 +7,7 @@
  * @alternateClassName mediaWiki
  * @singleton
  */
+/* global $VARS, $CODE */
 
 ( function () {
        'use strict';
                                 */
                                jobs = [],
 
+                               /**
+                                * @private
+                                * @property {Array} baseModules
+                                */
+                               baseModules = $VARS.baseModules,
+
                                /**
                                 * For #addEmbeddedCSS() and #addLink()
                                 *
                         * @return {string} Hash of concatenated version hashes.
                         */
                        function getCombinedVersion( modules ) {
-                               var hashes = modules.map( function ( module ) {
-                                       return registry[ module ].version;
-                               } );
-                               return fnv132( hashes.join( '' ) );
+                               var hashes = modules.reduce( function ( result, module ) {
+                                       return result + registry[ module ].version;
+                               }, '' );
+                               return fnv132( hashes );
                        }
 
                        /**
                                return true;
                        }
 
+                       /**
+                        * Determine whether all direct and base dependencies are in state 'ready'
+                        *
+                        * @private
+                        * @param {string} module Name of the module to be checked
+                        * @return {boolean} True if all direct/base dependencies are in state 'ready'; false otherwise
+                        */
+                       function allWithImplicitReady( module ) {
+                               return allReady( registry[ module ].dependencies ) &&
+                                       ( baseModules.indexOf( module ) !== -1 || allReady( baseModules ) );
+                       }
+
                        /**
                         * Determine whether all dependencies are in state 'ready', which means we may
                         * execute the module or job now.
                         * @param {string} module Name of module that entered one of the states 'ready', 'error', or 'missing'.
                         */
                        function handlePending( module ) {
-                               var j, job, hasErrors, m, stateChange;
+                               var j, job, hasErrors, m, stateChange, fromBaseModule;
 
                                if ( registry[ module ].state === 'error' || registry[ module ].state === 'missing' ) {
+                                       fromBaseModule = baseModules.indexOf( module ) !== -1;
                                        // If the current module failed, mark all dependent modules also as failed.
                                        // Iterate until steady-state to propagate the error state upwards in the
                                        // dependency tree.
                                                stateChange = false;
                                                for ( m in registry ) {
                                                        if ( registry[ m ].state !== 'error' && registry[ m ].state !== 'missing' ) {
-                                                               if ( anyFailed( registry[ m ].dependencies ) ) {
+                                                               // Always propagate errors from base modules to regular modules (implicit dependency).
+                                                               // Between base modules or regular modules, consider direct dependencies only.
+                                                               if (
+                                                                       ( fromBaseModule && baseModules.indexOf( m ) === -1 ) ||
+                                                                       anyFailed( registry[ m ].dependencies )
+                                                               ) {
                                                                        registry[ m ].state = 'error';
                                                                        stateChange = true;
                                                                }
                                        }
                                }
 
+                               // The current module became 'ready'.
                                if ( registry[ module ].state === 'ready' ) {
-                                       // The current module became 'ready'. Set it in the module store, and recursively execute all
-                                       // dependent modules that are loaded and now have all dependencies satisfied.
+                                       // Save it to the module store.
                                        mw.loader.store.set( module, registry[ module ] );
+                                       // Recursively execute all dependent modules that were already loaded
+                                       // (waiting for execution) and no longer have unsatisfied dependencies.
                                        for ( m in registry ) {
-                                               if ( registry[ m ].state === 'loaded' && allReady( registry[ m ].dependencies ) ) {
+                                               // Base modules may have dependencies amongst eachother to ensure correct
+                                               // execution order. Regular modules wait for all base modules.
+                                               if ( registry[ m ].state === 'loaded' && allWithImplicitReady( m ) ) {
                                                        // eslint-disable-next-line no-use-before-define
                                                        execute( m );
                                                }
                                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
                                deps = registry[ module ].dependencies;
+                               unresolved.add( module );
                                for ( i = 0; i < deps.length; i++ ) {
                                        if ( resolved.indexOf( deps[ i ] ) === -1 ) {
                                                if ( unresolved.has( deps[ i ] ) ) {
                                                        );
                                                }
 
-                                               unresolved.add( module );
                                                sortDependencies( deps[ i ], resolved, unresolved );
                                        }
                                }
                        /**
                         * Queue the loading and execution of a script for a particular module.
                         *
+                        * This does for debug mode what runScript() does for production.
+                        *
                         * @private
                         * @param {string} src URL of the script
                         * @param {string} moduleName Name of currently executing module
                         */
                        function queueModuleScript( src, moduleName, callback ) {
                                pendingRequests.push( function () {
-                                       if ( hasOwn.call( registry, moduleName ) ) {
-                                               // Emulate runScript() part of execute()
+                                       // Keep in sync with execute()/runScript().
+                                       if ( moduleName !== 'jquery' && hasOwn.call( registry, moduleName ) ) {
                                                window.require = mw.loader.require;
                                                window.module = registry[ moduleName ].module;
                                        }
                         */
                        function domEval( code ) {
                                var script = document.createElement( 'script' );
+                               if ( mw.config.get( 'wgCSPNonce' ) !== false ) {
+                                       script.nonce = mw.config.get( 'wgCSPNonce' );
+                               }
                                script.text = code;
                                document.head.appendChild( script );
                                script.parentNode.removeChild( script );
                                }
 
                                registry[ module ].state = 'executing';
+                               $CODE.profileExecuteStart();
 
                                runScript = function () {
                                        var script, markModuleReady, nestedAddScript;
 
+                                       $CODE.profileScriptStart();
                                        script = registry[ module ].script;
                                        markModuleReady = function () {
+                                               $CODE.profileScriptEnd();
                                                registry[ module ].state = 'ready';
                                                handlePending( module );
                                        };
                                                if ( Array.isArray( script ) ) {
                                                        nestedAddScript( script, markModuleReady, 0 );
                                                } 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( window.$, window.$, mw.loader.require, registry[ module ].module );
+                                                       // 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 );
+                                                       }
                                                        markModuleReady();
 
                                                } else if ( typeof script === 'string' ) {
                                                // 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';
+                                               $CODE.profileScriptEnd();
                                                mw.trackError( 'resourceloader.exception', {
                                                        exception: e, module:
                                                        module, source: 'module-execute'
                                        }
                                }
 
+                               // End profiling of execute()-self before we call checkCssHandles(),
+                               // which (sometimes asynchronously) calls runScript(), which we want
+                               // to measure separately without overlap.
+                               $CODE.profileExecuteEnd();
+
                                // Kick off.
                                cssHandlesRegistered = true;
                                checkCssHandles();
                         * @param {string[]} batch
                         */
                        function batchRequest( batch ) {
-                               var reqBase, splits, maxQueryLength, b, bSource, bGroup, bSourceGroup,
+                               var reqBase, splits, maxQueryLength, b, bSource, bGroup,
                                        source, group, i, modules, sourceLoadScript,
                                        currReqBase, currReqBaseLength, moduleMap, currReqModules, l,
                                        lastDotIndex, prefix, suffix, bytesAdded;
                                maxQueryLength = mw.config.get( 'wgResourceLoaderMaxQueryLength', 2000 );
 
                                // Split module list by source and by group.
-                               splits = {};
+                               splits = Object.create( null );
                                for ( b = 0; b < batch.length; b++ ) {
                                        bSource = registry[ batch[ b ] ].source;
                                        bGroup = registry[ batch[ b ] ].group;
-                                       if ( !hasOwn.call( splits, bSource ) ) {
-                                               splits[ bSource ] = {};
+                                       if ( !splits[ bSource ] ) {
+                                               splits[ bSource ] = Object.create( null );
                                        }
-                                       if ( !hasOwn.call( splits[ bSource ], bGroup ) ) {
+                                       if ( !splits[ bSource ][ bGroup ] ) {
                                                splits[ bSource ][ bGroup ] = [];
                                        }
-                                       bSourceGroup = splits[ bSource ][ bGroup ];
-                                       bSourceGroup.push( batch[ b ] );
+                                       splits[ bSource ][ bGroup ].push( batch[ b ] );
                                }
 
                                for ( source in splits ) {
-
                                        sourceLoadScript = sources[ source ];
 
                                        for ( group in splits[ source ] ) {
                                                // We may need to split up the request to honor the query string length limit,
                                                // so build it piece by piece.
                                                l = currReqBaseLength;
-                                               moduleMap = {}; // { prefix: [ suffixes ] }
+                                               moduleMap = Object.create( null ); // { prefix: [ suffixes ] }
                                                currReqModules = [];
 
                                                for ( i = 0; i < modules.length; i++ ) {
                                                        // If lastDotIndex is -1, substr() returns an empty string
                                                        prefix = modules[ i ].substr( 0, lastDotIndex );
                                                        suffix = modules[ i ].slice( lastDotIndex + 1 );
-                                                       bytesAdded = hasOwn.call( moduleMap, prefix ) ?
+                                                       bytesAdded = moduleMap[ prefix ] ?
                                                                suffix.length + 3 : // '%2C'.length == 3
                                                                modules[ i ].length + 3; // '%7C'.length == 3
 
                                                                doRequest();
                                                                // .. and start again.
                                                                l = currReqBaseLength;
-                                                               moduleMap = {};
+                                                               moduleMap = Object.create( null );
                                                                currReqModules = [];
 
                                                                mw.track( 'resourceloader.splitRequest', { maxQueryLength: maxQueryLength } );
                                                        }
-                                                       if ( !hasOwn.call( moduleMap, prefix ) ) {
+                                                       if ( !moduleMap[ prefix ] ) {
                                                                moduleMap[ prefix ] = [];
                                                        }
                                                        l += bytesAdded;
                                                module: {
                                                        exports: {}
                                                },
-                                               version: version !== undefined ? String( version ) : '',
+                                               version: String( version || '' ),
                                                dependencies: deps || [],
                                                group: typeof group === 'string' ? group : null,
                                                source: typeof source === 'string' ? source : 'local',
                                        // The module may already have been marked as erroneous
                                        if ( registry[ name ].state !== 'error' && registry[ name ].state !== 'missing' ) {
                                                registry[ name ].state = 'loaded';
-                                               if ( allReady( registry[ name ].dependencies ) ) {
+                                               if ( allWithImplicitReady( name ) ) {
                                                        execute( name );
                                                }
                                        }
                                /**
                                 * Change the state of one or more modules.
                                 *
-                                * @param {string|Object} module Module name or object of module name/state pairs
-                                * @param {string} state State name
+                                * @param {Object} modules Object of module name/state pairs
                                 */
-                               state: function ( module, state ) {
-                                       var m;
-
-                                       if ( typeof module === 'object' ) {
-                                               for ( m in module ) {
-                                                       mw.loader.state( m, module[ m ] );
+                               state: function ( modules ) {
+                                       var module, state;
+                                       for ( module in modules ) {
+                                               state = modules[ module ];
+                                               if ( !hasOwn.call( registry, module ) ) {
+                                                       mw.loader.register( module );
+                                               }
+                                               registry[ module ].state = state;
+                                               if ( state === 'ready' || state === 'error' || state === 'missing' ) {
+                                                       // Make sure pending modules depending on this one get executed if their
+                                                       // dependencies are now fulfilled!
+                                                       handlePending( module );
                                                }
-                                               return;
-                                       }
-                                       if ( !hasOwn.call( registry, module ) ) {
-                                               mw.loader.register( module );
-                                       }
-                                       registry[ module ].state = state;
-                                       if ( state === 'ready' || state === 'error' || state === 'missing' ) {
-                                               // Make sure pending modules depending on this one get executed if their
-                                               // dependencies are now fulfilled!
-                                               handlePending( module );
                                        }
                                },