Merge "resourceloader: Compile documentElement.className server-side"
[lhc/web/wiklou.git] / includes / resourceloader / ResourceLoaderClientHtml.php
index 7f2f85f..8303896 100644 (file)
@@ -143,15 +143,15 @@ class ResourceLoaderClientHtml {
 
                        $group = $module->getGroup();
                        $context = $this->getContext( $group, ResourceLoaderModule::TYPE_COMBINED );
-                       if ( $module->isKnownEmpty( $context ) ) {
-                               // Avoid needless request or embed for empty module
-                               $data['states'][$name] = 'ready';
-                               continue;
-                       }
+                       $shouldEmbed = $module->shouldEmbedModule( $this->context );
 
-                       if ( $group === 'user' || $module->shouldEmbedModule( $this->context ) ) {
-                               // Call makeLoad() to decide how to load these, instead of
-                               // loading via mw.loader.load().
+                       if ( ( $group === 'user' || $shouldEmbed ) && $module->isKnownEmpty( $context ) ) {
+                               // This is a user-specific or embedded module, which means its output
+                               // can be specific to the current page or user. As such, we can optimise
+                               // the way we load it based on the current version of the module.
+                               // Avoid needless embed for empty module, preset ready state.
+                               $data['states'][$name] = 'ready';
+                       } elseif ( $group === 'user' || $shouldEmbed ) {
                                // - For group=user: We need to provide a pre-generated load.php
                                //   url to the client that has the 'user' and 'version' parameters
                                //   filled in. Without this, the client would wrongly use the static
@@ -187,15 +187,30 @@ class ResourceLoaderClientHtml {
 
                        $group = $module->getGroup();
                        $context = $this->getContext( $group, ResourceLoaderModule::TYPE_STYLES );
-                       // Avoid needless request for empty module
-                       if ( !$module->isKnownEmpty( $context ) ) {
-                               if ( $module->shouldEmbedModule( $this->context ) ) {
-                                       // Embed via style element
+                       if ( $module->shouldEmbedModule( $this->context ) ) {
+                               // Avoid needless embed for private embeds we know are empty.
+                               // (Set "ready" state directly instead, which we do a few lines above.)
+                               if ( !$module->isKnownEmpty( $context ) ) {
+                                       // Embed via <style> element
                                        $data['embed']['styles'][] = $name;
-                               } else {
-                                       // Load from load.php?only=styles via <link rel=stylesheet>
-                                       $data['styles'][] = $name;
                                }
+                       // For other style modules, always request them, regardless of whether they are
+                       // currently known to be empty. Because:
+                       // 1. Those modules are requested in batch, so there is no extra request overhead
+                       //    or extra HTML element to be avoided.
+                       // 2. Checking isKnownEmpty for those can be expensive and slow down page view
+                       //    generation (T230260).
+                       // 3. We don't want cached HTML to vary on the current state of a module.
+                       //    If the module becomes non-empty a few minutes later, it should start working
+                       //    on cached HTML without requiring a purge.
+                       //
+                       // But, user-specific modules:
+                       // * ... are used on page views not publicly cached.
+                       // * ... are in their own group and thus a require a request we can avoid
+                       // * ... have known-empty status preloaded by ResourceLoader.
+                       } elseif ( $group !== 'user' || !$module->isKnownEmpty( $context ) ) {
+                               // Load from load.php?only=styles via <link rel=stylesheet>
+                               $data['styles'][] = $name;
                        }
                        $deprecation = $module->getDeprecationInformation();
                        if ( $deprecation ) {
@@ -225,19 +240,22 @@ class ResourceLoaderClientHtml {
         * - Inline scripts can't be asynchronous.
         * - For styles, earlier is better.
         *
+        * @param string|null $nojsClass Class name that caller uses on HTML document element
         * @return string|WrappedStringList HTML
         */
-       public function getHeadHtml() {
+       public function getHeadHtml( $nojsClass = null ) {
                $nonce = $this->options['nonce'];
                $data = $this->getData();
                $chunks = [];
 
                // Change "client-nojs" class to client-js. This allows easy toggling of UI components.
                // This must happen synchronously on every page view to avoid flashes of wrong content.
-               // See also #getDocumentAttributes() and /resources/src/startup.js.
+               // See also startup/startup.js.
+               $nojsClass = $nojsClass ?? $this->getDocumentAttributes()['class'];
+               $jsClass = preg_replace( '/(^|\s)client-nojs(\s|$)/', '$1client-js$2', $nojsClass );
+               $jsClassJson = ResourceLoader::encodeJsonForScript( $jsClass );
                $script = <<<JAVASCRIPT
-document.documentElement.className = document.documentElement.className
-       .replace( /(^|\s)client-nojs(\s|$)/, "$1client-js$2" );
+document.documentElement.className = {$jsClassJson};
 JAVASCRIPT;
 
                // Inline script: Declare mw.config variables for this page.
@@ -265,15 +283,12 @@ RLPAGEMODULES = {$pageModulesJson};
 JAVASCRIPT;
                }
 
-               if ( $this->context->getDebug() ) {
-                       $chunks[] = Html::inlineScript( $script, $nonce );
-               } else {
-                       $chunks[] = Html::inlineScript(
-                               ResourceLoader::filter( 'minify-js', $script, [ 'cache' => false ] ),
-                               $nonce
-                       );
+               if ( !$this->context->getDebug() ) {
+                       $script = ResourceLoader::filter( 'minify-js', $script, [ 'cache' => false ] );
                }
 
+               $chunks[] = Html::inlineScript( $script, $nonce );
+
                // Inline RLQ: Embedded modules
                if ( $data['embed']['general'] ) {
                        $chunks[] = $this->getLoad(
@@ -348,7 +363,7 @@ JAVASCRIPT;
        private static function makeContext( ResourceLoaderContext $mainContext, $group, $type,
                array $extraQuery = []
        ) {
-               // Create new ResourceLoaderContext so that $extraQuery may trigger isRaw().
+               // Create new ResourceLoaderContext so that $extraQuery is supported (eg. for 'sync=1').
                $req = new FauxRequest( array_merge( $mainContext->getRequest()->getValues(), $extraQuery ) );
                // Set 'only' if not combined
                $req->setVal( 'only', $type === ResourceLoaderModule::TYPE_COMBINED ? null : $type );
@@ -412,6 +427,7 @@ JAVASCRIPT;
                                $idx = -1;
                                foreach ( $grpModules as $name => $module ) {
                                        $shouldEmbed = $module->shouldEmbedModule( $context );
+                                       // @phan-suppress-next-line PhanTypeInvalidDimOffset
                                        if ( !$moduleSets || $moduleSets[$idx][0] !== $shouldEmbed ) {
                                                $moduleSets[++$idx] = [ $shouldEmbed, [] ];
                                        }
@@ -434,12 +450,6 @@ JAVASCRIPT;
                                                        );
                                                }
                                        } else {
-                                               // See if we have one or more raw modules
-                                               $isRaw = false;
-                                               foreach ( $moduleSet as $key => $module ) {
-                                                       $isRaw |= $module->isRaw();
-                                               }
-
                                                // Special handling for the user group; because users might change their stuff
                                                // on-wiki like user pages, or user preferences; we need to find the highest
                                                // timestamp of these user-changeable modules so we can ensure cache misses on change
@@ -455,9 +465,15 @@ JAVASCRIPT;
                                                // Decide whether to use 'style' or 'script' element
                                                if ( $only === ResourceLoaderModule::TYPE_STYLES ) {
                                                        $chunk = Html::linkedStyle( $url );
-                                               } elseif ( $context->getRaw() || $isRaw ) {
+                                               } elseif ( $context->getRaw() ) {
+                                                       // This request is asking for the module to be delivered standalone,
+                                                       // (aka "raw") without communicating to any mw.loader client.
+                                                       // Use cases:
+                                                       // - startup (naturally because this is what will define mw.loader)
+                                                       // - html5shiv (loads synchronously in old IE before the async startup module arrives)
+                                                       // - QUnit (needed in SpecialJavaScriptTest before async startup)
                                                        $chunk = Html::element( 'script', [
-                                                               // In SpecialJavaScriptTest, QUnit must load synchronous
+                                                               // The 'sync' option is only supported in combination with 'raw'.
                                                                'async' => !isset( $extraQuery['sync'] ),
                                                                'src' => $url
                                                        ] );