resourceloader: Async all the way
authorTimo Tijhof <krinklemail@gmail.com>
Tue, 28 Jul 2015 02:46:00 +0000 (19:46 -0700)
committerOri Livneh <ori@wikimedia.org>
Tue, 4 Aug 2015 17:20:10 +0000 (10:20 -0700)
Page startup:
* Due to the startup module and top queue being asynchronous now,
  move client-nojs/client-js class handling to OutputPage to ensure
  there is no flashes of wrongly styled or unstyled content.

  To preserve compatibility for unsupported browsers, undo the
  class swap at runtime after the isCompatible() check.

ResourceLoader startup module:
* Load the startup module with <script async>.
* Use DOM methods instead of 'document.write' to create base module request (jquery|mediawiki).

mw.loader:
* Drop 'async' parameter from mw.loader.load().
* Remove the now-unused code paths for synchronous requests.

OutputPage:

* Drop '$loadCall' parameter from makeResourceLoaderLink().
  Asynchronous is now the default and only way to load JavaScript.
  This means the 'user' module "conditional document-write scripts"
  are now a simple "mw.loader.load( url )" call.

* Fix incorrect @return of makeResourceLoaderLink(). This returns
  an array not a string.

* Improve documentation of makeResourceLoaderLink().

* Drop '$inHead' parameter from getScriptsForBottomQueue(). No longer used.
  Compatibility with the $wgResourceLoaderExperimentalAsyncLoading
  feature is maintained. It just no longer needs to change the
  way the queue works since it's always asynchronous. The feature
  flag now only controls whether the bottom queue starts at the bottom
  or starts at the top.

* Remove jQuery.ready() optimisation.
  This was mostly there to avoid the setTimeout() loop jQuery does to detect
  dom-ready in IE6/IE7 (which we no longer serve JavaScript at all).
  And for a bug in Firefox with document.write (which is no longer used as of
  this commit).

Bug: T107399
Change-Id: Icba6d7a87b239bf127a221bc6bc432cfa71a4a72

includes/OutputPage.php
includes/resourceloader/ResourceLoaderStartUpModule.php
includes/specials/SpecialJavaScriptTest.php
resources/src/mediawiki.page/mediawiki.page.patrol.ajax.js
resources/src/mediawiki.page/mediawiki.page.startup.js
resources/src/mediawiki.page/mediawiki.page.watch.ajax.js
resources/src/mediawiki/mediawiki.js
resources/src/startup.js
tests/phpunit/includes/OutputPageTest.php

index 07c1a8a..0f0b2f5 100644 (file)
@@ -2703,7 +2703,6 @@ class OutputPage extends ContextSource {
                }
 
                $ret .= $this->buildCssLinks() . "\n";
-
                $ret .= $this->getHeadScripts() . "\n";
 
                foreach ( $this->mHeadItems as $item ) {
@@ -2762,18 +2761,16 @@ class OutputPage extends ContextSource {
        }
 
        /**
-        * @todo Document
+        * Construct neccecary html and loader preset states to load modules on a page.
+        *
+        * Use getHtmlFromLoaderLinks() to convert this array to HTML.
+        *
         * @param array|string $modules One or more module names
         * @param string $only ResourceLoaderModule TYPE_ class constant
-        * @param array $extraQuery Array with extra query parameters to add to each
-        *   request. array( param => value ).
-        * @param bool $loadCall If true, output an (asynchronous) mw.loader.load()
-        *   call rather than a "<script src='...'>" tag.
-        * @return string The html "<script>", "<link>" and "<style>" tags
-        */
-       public function makeResourceLoaderLink( $modules, $only, array $extraQuery = array(),
-               $loadCall = false
-       ) {
+        * @param array $extraQuery [optional] Array with extra query parameters for the request
+        * @return array A list of HTML strings and array of client loader preset states
+        */
+       public function makeResourceLoaderLink( $modules, $only, array $extraQuery = array() ) {
                $modules = (array)$modules;
 
                $links = array(
@@ -2796,7 +2793,7 @@ class OutputPage extends ContextSource {
                        if ( ResourceLoader::inDebugMode() ) {
                                // Recursively call us for every item
                                foreach ( $modules as $name ) {
-                                       $link = $this->makeResourceLoaderLink( $name, $only );
+                                       $link = $this->makeResourceLoaderLink( $name, $only, $extraQuery );
                                        $links['html'] = array_merge( $links['html'], $link['html'] );
                                        $links['states'] += $link['states'];
                                }
@@ -2908,30 +2905,19 @@ class OutputPage extends ContextSource {
                                // Automatically select style/script elements
                                if ( $only === ResourceLoaderModule::TYPE_STYLES ) {
                                        $link = Html::linkedStyle( $url );
-                               } elseif ( $loadCall ) {
-                                       $link = ResourceLoader::makeInlineScript(
-                                               Xml::encodeJsCall( 'mw.loader.load', array( $url, 'text/javascript', true ) )
-                                       );
                                } else {
-                                       $link = Html::linkedScript( $url );
-                                       if ( !$context->getRaw() && !$isRaw ) {
-                                               // Wrap only=script / only=combined requests in a conditional as
-                                               // browsers not supported by the startup module would unconditionally
-                                               // execute this module. Otherwise users will get "ReferenceError: mw is
-                                               // undefined" or "jQuery is undefined" from e.g. a "site" module.
+                                       if ( $context->getRaw() || $isRaw ) {
+                                               // Startup module can't load itself, needs to use <script> instead of mw.loader.load
+                                               $link = Html::element( 'script', array(
+                                                       // In SpecialJavaScriptTest, QUnit must load synchronous
+                                                       'async' => !isset( $extraQuery['sync'] ),
+                                                       'src' => $url
+                                               ) );
+                                       } else {
                                                $link = ResourceLoader::makeInlineScript(
-                                                       Xml::encodeJsCall( 'document.write', array( $link ) )
+                                                       Xml::encodeJsCall( 'mw.loader.load', array( $url ) )
                                                );
                                        }
-
-                                       // For modules requested directly in the html via <link> or <script>,
-                                       // tell mw.loader they are being loading to prevent duplicate requests.
-                                       foreach ( $grpModules as $key => $module ) {
-                                               // Don't output state=loading for the startup module..
-                                               if ( $key !== 'startup' ) {
-                                                       $links['states'][$key] = 'loading';
-                                               }
-                                       }
                                }
 
                                if ( $group == 'noscript' ) {
@@ -2980,8 +2966,20 @@ class OutputPage extends ContextSource {
         * @return string HTML fragment
         */
        function getHeadScripts() {
-               // Startup - this will immediately load jquery and mediawiki modules
                $links = array();
+
+               // Client profile classes for <html>. Allows for easy hiding/showing of UI components.
+               // Must be done synchronously on every page to avoid flashes of wrong content.
+               // Note: This class distinguishes MediaWiki-supported JavaScript from the rest.
+               // The "rest" includes browsers that support JavaScript but not supported by our runtime.
+               // For the performance benefit of the majority, this is added unconditionally here and is
+               // then fixed up by the startup module for unsupported browsers.
+               $links[] = Html::inlineScript(
+                       'document.documentElement.className = document.documentElement.className'
+                       . '.replace( /(^|\s)client-nojs(\s|$)/, "$1client-js$2" );'
+               );
+
+               // Startup - this provides the client with the module manifest and loads jquery and mediawiki base modules
                $links[] = $this->makeResourceLoaderLink( 'startup', ResourceLoaderModule::TYPE_SCRIPTS );
 
                // Load config before anything else
@@ -3013,7 +3011,7 @@ class OutputPage extends ContextSource {
                );
 
                if ( $this->getConfig()->get( 'ResourceLoaderExperimentalAsyncLoading' ) ) {
-                       $links[] = $this->getScriptsForBottomQueue( true );
+                       $links[] = $this->getScriptsForBottomQueue();
                }
 
                return self::getHtmlFromLoaderLinks( $links );
@@ -3026,23 +3024,21 @@ class OutputPage extends ContextSource {
         * 'bottom', legacy scripts ($this->mScripts), user preferences, site JS
         * and user JS.
         *
-        * @param bool $inHead If true, this HTML goes into the "<head>",
-        *   if false it goes into the "<body>".
+        * @param bool $unused Previously used to let this method change its output based
+        *  on whether it was called by getHeadScripts() or getBottomScripts().
         * @return string
         */
-       function getScriptsForBottomQueue( $inHead ) {
+       function getScriptsForBottomQueue( $unused = null ) {
                // Scripts "only" requests marked for bottom inclusion
                // If we're in the <head>, use load() calls rather than <script src="..."> tags
                $links = array();
 
                $links[] = $this->makeResourceLoaderLink( $this->getModuleScripts( true, 'bottom' ),
-                       ResourceLoaderModule::TYPE_SCRIPTS, /* $extraQuery = */ array(),
-                       /* $loadCall = */ $inHead
+                       ResourceLoaderModule::TYPE_SCRIPTS
                );
 
                $links[] = $this->makeResourceLoaderLink( $this->getModuleStyles( true, 'bottom' ),
-                       ResourceLoaderModule::TYPE_STYLES, /* $extraQuery = */ array(),
-                       /* $loadCall = */ $inHead
+                       ResourceLoaderModule::TYPE_STYLES
                );
 
                // Modules requests - let the client calculate dependencies and batch requests as it likes
@@ -3050,7 +3046,7 @@ class OutputPage extends ContextSource {
                $modules = $this->getModules( true, 'bottom' );
                if ( $modules ) {
                        $links[] = ResourceLoader::makeInlineScript(
-                               Xml::encodeJsCall( 'mw.loader.load', array( $modules, null, true ) )
+                               Xml::encodeJsCall( 'mw.loader.load', array( $modules ) )
                        );
                }
 
@@ -3069,7 +3065,7 @@ class OutputPage extends ContextSource {
                        // We're on a preview of a JS subpage. Exclude this page from the user module (T28283)
                        // and include the draft contents as a raw script instead.
                        $links[] = $this->makeResourceLoaderLink( 'user', ResourceLoaderModule::TYPE_COMBINED,
-                               array( 'excludepage' => $this->getTitle()->getPrefixedDBkey() ), $inHead
+                               array( 'excludepage' => $this->getTitle()->getPrefixedDBkey() )
                        );
                        // Load the previewed JS
                        $links[] = ResourceLoader::makeInlineScript(
@@ -3093,15 +3089,11 @@ class OutputPage extends ContextSource {
                        // the excluded subpage.
                } else {
                        // Include the user module normally, i.e., raw to avoid it being wrapped in a closure.
-                       $links[] = $this->makeResourceLoaderLink( 'user', ResourceLoaderModule::TYPE_COMBINED,
-                               /* $extraQuery = */ array(), /* $loadCall = */ $inHead
-                       );
+                       $links[] = $this->makeResourceLoaderLink( 'user', ResourceLoaderModule::TYPE_COMBINED );
                }
 
                // Group JS is only enabled if site JS is enabled.
-               $links[] = $this->makeResourceLoaderLink( 'user.groups', ResourceLoaderModule::TYPE_COMBINED,
-                       /* $extraQuery = */ array(), /* $loadCall = */ $inHead
-               );
+               $links[] = $this->makeResourceLoaderLink( 'user.groups', ResourceLoaderModule::TYPE_COMBINED );
 
                return self::getHtmlFromLoaderLinks( $links );
        }
@@ -3114,17 +3106,11 @@ class OutputPage extends ContextSource {
                // In case the skin wants to add bottom CSS
                $this->getSkin()->setupSkinUserCss( $this );
 
-               // Optimise jQuery ready event cross-browser.
-               // This also enforces $.isReady to be true at </body> which fixes the
-               // mw.loader bug in Firefox with using document.write between </body>
-               // and the DOMContentReady event (bug 47457).
-               $html = Html::inlineScript( 'if(window.jQuery)jQuery.ready();' );
-
-               if ( !$this->getConfig()->get( 'ResourceLoaderExperimentalAsyncLoading' ) ) {
-                       $html .= $this->getScriptsForBottomQueue( false );
+               if ( $this->getConfig()->get( 'ResourceLoaderExperimentalAsyncLoading' ) ) {
+                       // Already handled by getHeadScripts()
+                       return '';
                }
-
-               return $html;
+               return  $this->getScriptsForBottomQueue();
        }
 
        /**
index 16424a0..bdf1b21 100644 (file)
@@ -335,7 +335,7 @@ class ResourceLoaderStartUpModule extends ResourceLoaderModule {
                }, array(
                        '$VARS.wgLegacyJavaScriptGlobals' => $this->getConfig()->get( 'LegacyJavaScriptGlobals' ),
                        '$VARS.configuration' => $this->getConfigSettings( $context ),
-                       '$VARS.baseModulesScript' => Html::linkedScript( self::getStartupModulesUrl( $context ) ),
+                       '$VARS.baseModulesUri' => self::getStartupModulesUrl( $context ),
                ) );
                $pairs['$CODE.registrations()'] = str_replace( "\n", "\n\t", trim( $this->getModuleRegistrations( $context ) ) );
 
index 442b764..b5f3815 100644 (file)
@@ -210,8 +210,21 @@ HTML;
                $query['only'] = 'scripts';
                $startupContext = new ResourceLoaderContext( $rl, new FauxRequest( $query ) );
 
+               $query['raw'] = true;
+
                $modules = $rl->getTestModuleNames( 'qunit' );
 
+               // Disable autostart because we load modules asynchronously. By default, QUnit would start
+               // at domready when there are no tests loaded and also fire 'QUnit.done' which then instructs
+               // Karma to end the run before the tests even started.
+               $qunitConfig = 'QUnit.config.autostart = false;'
+                       . 'if (window.__karma__) {'
+                       // karma-qunit's use of autostart=false and QUnit.start conflicts with ours.
+                       // Hack around this by replacing 'karma.loaded' with a no-op and call it ourselves later.
+                       // See <https://github.com/karma-runner/karma-qunit/issues/27>.
+                       . 'window.__karma__.loaded = function () {};'
+                       . '}';
+
                // The below is essentially a pure-javascript version of OutputPage::getHeadScripts.
                $startup = $rl->makeModuleResponse( $startupContext, array(
                        'startup' => $rl->getModule( 'startup' ),
@@ -225,35 +238,39 @@ HTML;
                        'user.options' => $rl->getModule( 'user.options' ),
                        'user.tokens' => $rl->getModule( 'user.tokens' ),
                ) );
-               $code .= Xml::encodeJsCall( 'mw.loader.load', array( $modules ) );
+               // Catch exceptions (such as "dependency missing" or "unknown module") so that we
+               // always start QUnit. Re-throw so that they are caught and reported as global exceptions
+               // by QUnit and Karma.
+               $code .= '(function () {'
+                       . 'var start = window.__karma__ ? window.__karma__.start : QUnit.start;'
+                       . 'try {'
+                       . 'mw.loader.using( ' . Xml::encodeJsVar( $modules ) . ' ).always( start );'
+                       . '} catch ( e ) { start(); throw e; }'
+                       . '}());';
 
                header( 'Content-Type: text/javascript; charset=utf-8' );
                header( 'Cache-Control: private, no-cache, must-revalidate' );
                header( 'Pragma: no-cache' );
+               echo $qunitConfig;
                echo $startup;
-               echo "\n";
-               // Note: The following has to be wrapped in a script tag because the startup module also
-               // writes a script tag (the one loading mediawiki.js). Script tags are synchronous, block
-               // each other, and run in order. But they don't nest. The code appended after the startup
-               // module runs before the added script tag is parsed and executed.
-               echo Xml::encodeJsCall( 'document.write', array( Html::inlineScript( $code ) ) );
+               // The following has to be deferred via RLQ because the startup module is asynchronous.
+               echo ResourceLoader::makeLoaderConditionalScript( $code );
        }
 
        private function plainQUnit() {
                $out = $this->getOutput();
                $out->disable();
 
-               $url = $this->getPageTitle( 'qunit/export' )->getFullURL( array(
-                       'debug' => ResourceLoader::inDebugMode() ? 'true' : 'false',
-               ) );
-
                $styles = $out->makeResourceLoaderLink( 'jquery.qunit',
                        ResourceLoaderModule::TYPE_STYLES
                );
-               // Use 'raw' since this is a plain HTML page without ResourceLoader
+
+               // Use 'raw' because QUnit loads before ResourceLoader initialises (omit mw.loader.state call)
+               // Use 'test' to ensure OutputPage doesn't use the "async" attribute because QUnit must
+               // load before qunit/export.
                $scripts = $out->makeResourceLoaderLink( 'jquery.qunit',
                        ResourceLoaderModule::TYPE_SCRIPTS,
-                       array( 'raw' => 'true' )
+                       array( 'raw' => true, 'sync' => true )
                );
 
                $head = implode( "\n", array_merge( $styles['html'], $scripts['html'] ) );
@@ -265,6 +282,10 @@ $head
 $summary
 <div id="qunit"></div>
 HTML;
+
+               $url = $this->getPageTitle( 'qunit/export' )->getFullURL( array(
+                       'debug' => ResourceLoader::inDebugMode() ? 'true' : 'false',
+               ) );
                $html .= "\n" . Html::linkedScript( $url );
 
                header( 'Content-Type: text/html; charset=utf-8' );
index cc72e16..3da1d14 100644 (file)
@@ -17,7 +17,7 @@
                        var $spinner, href, rcid, apiRequest;
 
                        // Start preloading the notification module (normally loaded by mw.notify())
-                       mw.loader.load( ['mediawiki.notification'], null, true );
+                       mw.loader.load( 'mediawiki.notification' );
 
                        // Hide the link and create a spinner to show it inside the brackets.
                        $spinner = $.createSpinner( {
index ddd4f0c..708dcb5 100644 (file)
@@ -1,12 +1,11 @@
 ( function ( mw, $ ) {
 
-       mw.page = {};
+       // Support: MediaWiki < 1.26
+       // Cached HTML will not yet have this from OutputPage::getHeadScripts.
+       document.documentElement.className = document.documentElement.className
+               .replace( /(^|\s)client-nojs(\s|$)/, '$1client-js$2' );
 
-       // Client profile classes for <html>
-       // Allows for easy hiding/showing of JS or no-JS-specific UI elements
-       $( document.documentElement )
-               .addClass( 'client-js' )
-               .removeClass( 'client-nojs' );
+       mw.page = {};
 
        $( function () {
                mw.util.init();
index 50f280a..cc1ffb7 100644 (file)
                        var action, api, $link;
 
                        // Start preloading the notification module (normally loaded by mw.notify())
-                       mw.loader.load( ['mediawiki.notification'], null, true );
+                       mw.loader.load( 'mediawiki.notification' );
 
                        action = mwUriGetAction( this.href );
 
index 7825f22..3c5a5f5 100644 (file)
                        }
 
                        /**
-                        * Adds a script tag to the DOM, either using document.write or low-level DOM manipulation,
-                        * depending on whether document-ready has occurred yet and whether we are in async mode.
+                        * Load and execute a script with callback.
                         *
                         * @private
                         * @param {string} src URL to script, will be used as the src attribute in the script tag
                         * @param {Function} [callback] Callback which will be run when the script is done
-                        * @param {boolean} [async=false] Whether to load modules asynchronously.
-                        *  Ignored (and defaulted to `true`) if the document-ready event has already occurred.
                         */
-                       function addScript( src, callback, async ) {
-                               // Using isReady directly instead of storing it locally from a $().ready callback (bug 31895)
-                               if ( $.isReady || async ) {
-                                       $.ajax( {
-                                               url: src,
-                                               dataType: 'script',
-                                               // 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
-                                               // text, so we'd need to $.globalEval, which then messes up line numbers.
-                                               crossDomain: true,
-                                               cache: true,
-                                               async: true
-                                       } ).always( callback );
-                               } else {
-                                       /*jshint evil:true */
-                                       document.write( mw.html.element( 'script', { 'src': src }, '' ) );
-                                       if ( callback ) {
-                                               // Document.write is synchronous, so this is called when it's done.
-                                               // FIXME: That's a lie. doc.write isn't actually synchronous.
-                                               callback();
-                                       }
-                               }
+                       function addScript( src, callback ) {
+                               $.ajax( {
+                                       url: src,
+                                       dataType: 'script',
+                                       // 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
+                                       // text, so we'd need to $.globalEval, which then messes up line numbers.
+                                       crossDomain: true,
+                                       cache: true
+                               } ).always( callback );
                        }
 
                        /**
                                                        registry[module].state = 'ready';
                                                        handlePending( module );
                                                };
-                                               nestedAddScript = function ( arr, callback, async, i ) {
+                                               nestedAddScript = function ( arr, callback, i ) {
                                                        // Recursively call addScript() in its own callback
                                                        // for each element of arr.
                                                        if ( i >= arr.length ) {
                                                        }
 
                                                        addScript( arr[i], function () {
-                                                               nestedAddScript( arr, callback, async, i + 1 );
-                                                       }, async );
+                                                               nestedAddScript( arr, callback, i + 1 );
+                                                       } );
                                                };
 
                                                if ( $.isArray( script ) ) {
-                                                       nestedAddScript( script, markModuleReady, registry[module].async, 0 );
+                                                       nestedAddScript( script, markModuleReady, 0 );
                                                } else if ( $.isFunction( script ) ) {
                                                        // Pass jQuery twice so that the signature of the closure which wraps
                                                        // the script can bind both '$' and 'jQuery'.
                                        mw.templates.set( module, registry[module].templates );
                                }
 
-                               if ( $.isReady || registry[module].async ) {
-                                       // Make sure we don't run the scripts until all (potentially asynchronous)
-                                       // stylesheet insertions have completed.
-                                       ( function () {
-                                               var pending = 0;
-                                               checkCssHandles = function () {
-                                                       // cssHandlesRegistered ensures we don't take off too soon, e.g. when
-                                                       // one of the cssHandles is fired while we're still creating more handles.
-                                                       if ( cssHandlesRegistered && pending === 0 && runScript ) {
-                                                               runScript();
-                                                               runScript = undefined; // Revoke
+                               // Make sure we don't run the scripts until all stylesheet insertions have completed.
+                               ( function () {
+                                       var pending = 0;
+                                       checkCssHandles = function () {
+                                               // cssHandlesRegistered ensures we don't take off too soon, e.g. when
+                                               // one of the cssHandles is fired while we're still creating more handles.
+                                               if ( cssHandlesRegistered && pending === 0 && runScript ) {
+                                                       runScript();
+                                                       runScript = undefined; // Revoke
+                                               }
+                                       };
+                                       cssHandle = function () {
+                                               var check = checkCssHandles;
+                                               pending++;
+                                               return function () {
+                                                       if ( check ) {
+                                                               pending--;
+                                                               check();
+                                                               check = undefined; // Revoke
                                                        }
                                                };
-                                               cssHandle = function () {
-                                                       var check = checkCssHandles;
-                                                       pending++;
-                                                       return function () {
-                                                               if ( check ) {
-                                                                       pending--;
-                                                                       check();
-                                                                       check = undefined; // Revoke
-                                                               }
-                                                       };
-                                               };
-                                       }() );
-                               } else {
-                                       // We are in blocking mode, and so we can't afford to wait for CSS
-                                       cssHandle = function () {};
-                                       // Run immediately
-                                       checkCssHandles = runScript;
-                               }
+                                       };
+                               }() );
 
                                // Process styles (see also mw.loader.implement)
                                // * back-compat: { <media>: css }
                         * @param {string|string[]} dependencies Module name or array of string module names
                         * @param {Function} [ready] Callback to execute when all dependencies are ready
                         * @param {Function} [error] Callback to execute when any dependency fails
-                        * @param {boolean} [async=false] Whether to load modules asynchronously.
-                        *  Ignored (and defaulted to `true`) if the document-ready event has already occurred.
                         */
-                       function request( dependencies, ready, error, async ) {
+                       function request( dependencies, ready, error ) {
                                // Allow calling by single module name
                                if ( typeof dependencies === 'string' ) {
                                        dependencies = [dependencies];
                                                        return;
                                                }
                                                queue.push( module );
-                                               if ( async ) {
-                                                       registry[module].async = true;
-                                               }
                                        }
                                } );
 
                        }
 
                        /**
-                        * Asynchronously append a script tag to the end of the body
-                        * that invokes load.php
+                        * Load modules from load.php
                         * @private
                         * @param {Object} moduleMap Module map, see #buildModulesString
                         * @param {Object} currReqBase Object with other parameters (other than 'modules') to use in the request
                         * @param {string} sourceLoadScript URL of load.php
-                        * @param {boolean} async Whether to load modules asynchronously.
-                        *  Ignored (and defaulted to `true`) if the document-ready event has already occurred.
                         */
-                       function doRequest( moduleMap, currReqBase, sourceLoadScript, async ) {
+                       function doRequest( moduleMap, currReqBase, sourceLoadScript ) {
                                var request = $.extend(
                                        { modules: buildModulesString( moduleMap ) },
                                        currReqBase
                                );
                                request = sortQuery( request );
                                // Support: IE6
-                               // Append &* to satisfy load.php's WebRequest::checkUrlExtension test. This script
-                               // isn't actually used in IE6, but MediaWiki enforces it in general.
-                               addScript( sourceLoadScript + '?' + $.param( request ) + '&*', null, async );
+                               // Append &* to satisfy load.php's WebRequest::checkUrlExtension test.
+                               // This script isn't actually used in IE6, but MediaWiki enforces it in general.
+                               addScript( sourceLoadScript + '?' + $.param( request ) + '&*' );
                        }
 
                        /**
                                        var     reqBase, splits, maxQueryLength, q, b, bSource, bGroup, bSourceGroup,
                                                source, concatSource, origBatch, group, i, modules, sourceLoadScript,
                                                currReqBase, currReqBaseLength, moduleMap, l,
-                                               lastDotIndex, prefix, suffix, bytesAdded, async;
+                                               lastDotIndex, prefix, suffix, bytesAdded;
 
                                        // Build a list of request parameters common to all requests.
                                        reqBase = {
                                                                currReqBase.user = mw.config.get( 'wgUserName' );
                                                        }
                                                        currReqBaseLength = $.param( currReqBase ).length;
-                                                       async = true;
                                                        // We may need to split up the request to honor the query string length limit,
                                                        // so build it piece by piece.
                                                        l = currReqBaseLength + 9; // '&modules='.length == 9
                                                                if ( maxQueryLength > 0 && !$.isEmptyObject( moduleMap ) && l + bytesAdded > maxQueryLength ) {
                                                                        // This request would become too long, create a new one
                                                                        // and fire off the old one
-                                                                       doRequest( moduleMap, currReqBase, sourceLoadScript, async );
+                                                                       doRequest( moduleMap, currReqBase, sourceLoadScript );
                                                                        moduleMap = {};
-                                                                       async = true;
                                                                        l = currReqBaseLength + 9;
                                                                        mw.track( 'resourceloader.splitRequest', { maxQueryLength: maxQueryLength } );
                                                                }
                                                                        moduleMap[prefix] = [];
                                                                }
                                                                moduleMap[prefix].push( suffix );
-                                                               if ( !registry[modules[i]].async ) {
-                                                                       // If this module is blocking, make the entire request blocking
-                                                                       // This is slightly suboptimal, but in practice mixing of blocking
-                                                                       // and async modules will only occur in debug mode.
-                                                                       async = false;
-                                                               }
                                                                l += bytesAdded;
                                                        }
                                                        // If there's anything left in moduleMap, request that too
                                                        if ( !$.isEmptyObject( moduleMap ) ) {
-                                                               doRequest( moduleMap, currReqBase, sourceLoadScript, async );
+                                                               doRequest( moduleMap, currReqBase, sourceLoadScript );
                                                        }
                                                }
                                        }
                                 * @param {string} [type='text/javascript'] MIME type to use if calling with a URL of an
                                 *  external script or style; acceptable values are "text/css" and
                                 *  "text/javascript"; if no type is provided, text/javascript is assumed.
-                                * @param {boolean} [async] Whether to load modules asynchronously.
-                                *  Ignored (and defaulted to `true`) if the document-ready event has already occurred.
-                                *  Defaults to `true` if loading a URL, `false` otherwise.
                                 */
-                               load: function ( modules, type, async ) {
+                               load: function ( modules, type ) {
                                        var filtered, l;
 
                                        // Validate input
                                        // Allow calling with an external url or single dependency as a string
                                        if ( typeof modules === 'string' ) {
                                                if ( /^(https?:)?\/\//.test( modules ) ) {
-                                                       if ( async === undefined ) {
-                                                               // Assume async for bug 34542
-                                                               async = true;
-                                                       }
                                                        if ( type === 'text/css' ) {
                                                                // Support: IE 7-8
                                                                // Use properties instead of attributes as IE throws security
                                                                return;
                                                        }
                                                        if ( type === 'text/javascript' || type === undefined ) {
-                                                               addScript( modules, null, async );
+                                                               addScript( modules );
                                                                return;
                                                        }
                                                        // Unknown type
                                                return;
                                        }
                                        // Since some modules are not yet ready, queue up a request.
-                                       request( filtered, undefined, undefined, async );
+                                       request( filtered, undefined, undefined );
                                },
 
                                /**
index 97fa134..d4cfa02 100644 (file)
@@ -93,5 +93,14 @@ function startUp() {
 
 // Conditional script injection
 if ( isCompatible() ) {
-       document.write( $VARS.baseModulesScript );
+       ( function () {
+               var script = document.createElement( 'script' );
+               script.src = $VARS.baseModulesUri;
+               document.getElementsByTagName( 'head' )[0].appendChild( script );
+       }() );
+} else {
+       // Undo class swapping in case of an unsupported browser.
+       // See OutputPage::getHeadScripts().
+       document.documentElement.className = document.documentElement.className
+               .replace( /(^|\s)client-js(\s|$)/, '$1client-nojs$2' );
 }
index 69f55c3..6a25d8b 100644 (file)
@@ -142,15 +142,13 @@ class OutputPageTest extends MediaWikiTestCase {
                        array(
                                array( 'test.foo', ResourceLoaderModule::TYPE_SCRIPTS ),
                                "<script>var RLQ = RLQ || []; RLQ.push( function () {\n"
-                                       . 'document.write("\u003Cscript src=\"http://127.0.0.1:8080/w/load.php?'
-                                       . 'debug=false\u0026amp;lang=en\u0026amp;modules=test.foo\u0026amp;only'
-                                       . '=scripts\u0026amp;skin=fallback\u0026amp;*\"\u003E\u003C/script\u003E");'
+                                       . 'mw.loader.load("http://127.0.0.1:8080/w/load.php?debug=false\u0026lang=en\u0026modules=test.foo\u0026only=scripts\u0026skin=fallback\u0026*");'
                                        . "\n} );</script>"
                        ),
                        array(
                                // Don't condition wrap raw modules (like the startup module)
                                array( 'test.raw', ResourceLoaderModule::TYPE_SCRIPTS ),
-                               '<script src="http://127.0.0.1:8080/w/load.php?debug=false&amp;lang=en&amp;modules=test.raw&amp;only=scripts&amp;skin=fallback&amp;*"></script>'
+                               '<script async src="http://127.0.0.1:8080/w/load.php?debug=false&amp;lang=en&amp;modules=test.raw&amp;only=scripts&amp;skin=fallback&amp;*"></script>'
                        ),
                        // Load module styles only
                        // This also tests the order the modules are put into the url
@@ -188,10 +186,10 @@ class OutputPageTest extends MediaWikiTestCase {
                        array(
                                array( array( 'test.group.foo', 'test.group.bar' ), ResourceLoaderModule::TYPE_COMBINED ),
                                "<script>var RLQ = RLQ || []; RLQ.push( function () {\n"
-                                       . 'document.write("\u003Cscript src=\"http://127.0.0.1:8080/w/load.php?debug=false\u0026amp;lang=en\u0026amp;modules=test.group.bar\u0026amp;skin=fallback\u0026amp;*\"\u003E\u003C/script\u003E");'
+                                       . 'mw.loader.load("http://127.0.0.1:8080/w/load.php?debug=false\u0026lang=en\u0026modules=test.group.bar\u0026skin=fallback\u0026*");'
                                        . "\n} );</script>\n"
                                        . "<script>var RLQ = RLQ || []; RLQ.push( function () {\n"
-                                       . 'document.write("\u003Cscript src=\"http://127.0.0.1:8080/w/load.php?debug=false\u0026amp;lang=en\u0026amp;modules=test.group.foo\u0026amp;skin=fallback\u0026amp;*\"\u003E\u003C/script\u003E");'
+                                       . 'mw.loader.load("http://127.0.0.1:8080/w/load.php?debug=false\u0026lang=en\u0026modules=test.group.foo\u0026skin=fallback\u0026*");'
                                        . "\n} );</script>"
                        ),
                );