* Made Resources.php return a pure-data array instead of an ugly mix of data and...
authorTim Starling <tstarling@users.mediawiki.org>
Fri, 19 Nov 2010 10:41:06 +0000 (10:41 +0000)
committerTim Starling <tstarling@users.mediawiki.org>
Fri, 19 Nov 2010 10:41:06 +0000 (10:41 +0000)
* Modified ResourceLoader to lazy-initialise module objects, for a further performance advantage.
* Deleted ResourceLoader::getModules(), provided getModuleNames() instead. Although the startup module needs this functionality, it's slow to generate, so to avoid misuse, it's better to provide a foolproof fast interface and let the startup module do the slow thing itself.
* Modified ResourceLoader::register() to optionally accept an info array instead of an object.
* Added $wgResourceModules, allowing extensions to efficiently define their own resource loader modules. The trouble with hooks is that they contain code, and code is slow. We've been through all this before with i18n. Hooks are useful as a performance tool only if you call them very rarely.
* Moved ResourceLoader settings to their own section in DefaultSettings.php
* Added options to ResourceLoaderFileModule equivalent to the $localBasePath and $remoteBasePath parameters, to allow it to be instantiated via the new array style. Also added remoteExtPath, which allows modules to be registered before $wgExtensionAssetsPath is known.
* Added OutputPage::getResourceLoader(), mostly for debugging.
* The time saving at the moment is about 5ms per request with no extensions, which is significant already with 6 load.php requests for a cold cache page view. This is a much more scalable interface; the relative saving will grow as more extensions are added which use this interface, especially for non-APC installs.

Although the interface is backwards compatible, extension updates will follow in a subsequent commit.

includes/DefaultSettings.php
includes/OutputPage.php
includes/resourceloader/ResourceLoader.php
includes/resourceloader/ResourceLoaderFileModule.php
includes/resourceloader/ResourceLoaderStartUpModule.php
resources/Resources.php

index cda21f9..c4014d6 100644 (file)
@@ -1646,42 +1646,6 @@ $wgUseETag = false;
  */
 $wgClockSkewFudge = 5;
 
-/**
- * Maximum time in seconds to cache resources served by the resource loader
- */
-$wgResourceLoaderMaxage = array(
-       'versioned' => array(
-               // Squid/Varnish but also any other public proxy cache between the client and MediaWiki
-               'server' => 30 * 24 * 60 * 60, // 30 days
-               // On the client side (e.g. in the browser cache).
-               'client' => 30 * 24 * 60 * 60, // 30 days
-       ),
-       'unversioned' => array(
-               'server' => 5 * 60, // 5 minutes
-               'client' => 5 * 60, // 5 minutes
-       ),
-);
-
-/**
- * Whether to embed private modules inline with HTML output or to bypass 
- * caching and check the user parameter against $wgUser to prevent 
- * unauthorized access to private modules.
- */
-$wgResourceLoaderInlinePrivateModules = true;
-
-/**
- * The default debug mode (on/off) for of ResourceLoader requests. This will still
- * be overridden when the debug URL parameter is used.
- */
-$wgResourceLoaderDebug = false;
-
-/**
- * Enable embedding of certain resources using Edge Side Includes. This will
- * improve performance but only works if there is something in front of the
- * web server (e..g a Squid or Varnish server) configured to process the ESI.
- */
-$wgResourceLoaderUseESI = false;
-
 /** @} */ # end of cache settings
 
 /************************************************************************//**
@@ -2319,6 +2283,65 @@ $wgBetterDirectionality = false;
 
 /** @} */ # End of output format settings }
 
+/*************************************************************************//**
+ * @name   Resource loader settings
+ * @{
+ */
+
+/**
+ * Client-side resource modules. Extensions should add their module definitions 
+ * here.
+ *
+ * Example:
+ *   $wgResourceModules['ext.myExtension'] = array(
+ *      'scripts' => 'myExtension.js',
+ *      'styles' => 'myExtension.css',
+ *      'dependencies' => array( 'jquery.cookie', 'jquery.tabIndex' ),
+ *      'localBasePath' => dirname( __FILE ),
+ *      'remoteExtPath' => 'MyExtension',
+ *   );
+ */
+$wgResourceModules = array();
+
+/**
+ * Maximum time in seconds to cache resources served by the resource loader
+ */
+$wgResourceLoaderMaxage = array(
+       'versioned' => array(
+               // Squid/Varnish but also any other public proxy cache between the client and MediaWiki
+               'server' => 30 * 24 * 60 * 60, // 30 days
+               // On the client side (e.g. in the browser cache).
+               'client' => 30 * 24 * 60 * 60, // 30 days
+       ),
+       'unversioned' => array(
+               'server' => 5 * 60, // 5 minutes
+               'client' => 5 * 60, // 5 minutes
+       ),
+);
+
+/**
+ * Whether to embed private modules inline with HTML output or to bypass 
+ * caching and check the user parameter against $wgUser to prevent 
+ * unauthorized access to private modules.
+ */
+$wgResourceLoaderInlinePrivateModules = true;
+
+/**
+ * The default debug mode (on/off) for of ResourceLoader requests. This will still
+ * be overridden when the debug URL parameter is used.
+ */
+$wgResourceLoaderDebug = false;
+
+/**
+ * Enable embedding of certain resources using Edge Side Includes. This will
+ * improve performance but only works if there is something in front of the
+ * web server (e..g a Squid or Varnish server) configured to process the ESI.
+ */
+$wgResourceLoaderUseESI = false;
+
+/** @} */ # End of resource loader settings }
+
+
 /*************************************************************************//**
  * @name   Page title and interwiki link settings
  * @{
index 9c2f992..a80d4eb 100644 (file)
@@ -2289,6 +2289,16 @@ class OutputPage {
                return $ret;
        }
 
+       /**
+        * Get a ResourceLoader object associated with this OutputPage
+        */
+       public function getResourceLoader() {
+               if ( is_null( $this->mResourceLoader ) ) {
+                       $this->mResourceLoader = new ResourceLoader();
+               }
+               return $this->mResourceLoader;
+       }               
+
        /**
         * TODO: Document
         * @param $skin Skin
@@ -2301,9 +2311,6 @@ class OutputPage {
                global $wgUser, $wgLang, $wgLoadScript, $wgResourceLoaderUseESI,
                        $wgResourceLoaderInlinePrivateModules;
                // Lazy-load ResourceLoader
-               if ( is_null( $this->mResourceLoader ) ) {
-                       $this->mResourceLoader = new ResourceLoader();
-               }
                // TODO: Should this be a static function of ResourceLoader instead?
                // TODO: Divide off modules starting with "user", and add the user parameter to them
                $query = array(
@@ -2335,8 +2342,9 @@ class OutputPage {
                
                // Create keyed-by-group list of module objects from modules list
                $groups = array();
+               $resourceLoader = $this->getResourceLoader();
                foreach ( (array) $modules as $name ) {
-                       $module = $this->mResourceLoader->getModule( $name );
+                       $module = $resourceLoader->getModule( $name );
                        $group = $module->getGroup();
                        if ( !isset( $groups[$group] ) ) {
                                $groups[$group] = array();
@@ -2352,15 +2360,15 @@ class OutputPage {
                        }
                        // Support inlining of private modules if configured as such
                        if ( $group === 'private' && $wgResourceLoaderInlinePrivateModules ) {
-                               $context = new ResourceLoaderContext( $this->mResourceLoader, new FauxRequest( $query ) );
+                               $context = new ResourceLoaderContext( $resourceLoader, new FauxRequest( $query ) );
                                if ( $only == 'styles' ) {
                                        $links .= Html::inlineStyle(
-                                               $this->mResourceLoader->makeModuleResponse( $context, $modules )
+                                               $resourceLoader->makeModuleResponse( $context, $modules )
                                        );
                                } else {
                                        $links .= Html::inlineScript(
                                                ResourceLoader::makeLoaderConditionalScript(
-                                                       $this->mResourceLoader->makeModuleResponse( $context, $modules )
+                                                       $resourceLoader->makeModuleResponse( $context, $modules )
                                                )
                                        );
                                }
@@ -2371,7 +2379,7 @@ class OutputPage {
                        // we can ensure cache misses on change
                        if ( $group === 'user' || $group === 'site' ) {
                                // Create a fake request based on the one we are about to make so modules return correct times
-                               $context = new ResourceLoaderContext( $this->mResourceLoader, new FauxRequest( $query ) );
+                               $context = new ResourceLoaderContext( $resourceLoader, new FauxRequest( $query ) );
                                // Get the maximum timestamp
                                $timestamp = 1;
                                foreach ( $modules as $module ) {
index 5a9d220..677543c 100644 (file)
@@ -32,6 +32,8 @@ class ResourceLoader {
 
        /** Array: List of module name/ResourceLoaderModule object pairs */
        protected $modules = array();
+       /** Associative array mapping module name to info associative array */
+       protected $moduleInfos = array();
 
        /* Protected Methods */
 
@@ -67,7 +69,7 @@ class ResourceLoader {
                // Set modules' dependecies             
                $modulesWithDeps = array();
                foreach ( $res as $row ) {
-                       $this->modules[$row->md_module]->setFileDependencies( $skin,
+                       $this->getModule( $row->md_module )->setFileDependencies( $skin,
                                FormatJson::decode( $row->md_deps, true )
                        );
                        $modulesWithDeps[] = $row->md_module;
@@ -75,14 +77,14 @@ class ResourceLoader {
 
                // Register the absence of a dependency row too
                foreach ( array_diff( $modules, $modulesWithDeps ) as $name ) {
-                       $this->modules[$name]->setFileDependencies( $skin, array() );
+                       $this->getModule( $name )->setFileDependencies( $skin, array() );
                }
                
                // Get message blob mtimes. Only do this for modules with messages
                $modulesWithMessages = array();
                $modulesWithoutMessages = array();
                foreach ( $modules as $name ) {
-                       if ( count( $this->modules[$name]->getMessages() ) ) {
+                       if ( count( $this->getModule( $name )->getMessages() ) ) {
                                $modulesWithMessages[] = $name;
                        } else {
                                $modulesWithoutMessages[] = $name;
@@ -95,11 +97,11 @@ class ResourceLoader {
                                ), __METHOD__
                        );
                        foreach ( $res as $row ) {
-                               $this->modules[$row->mr_resource]->setMsgBlobMtime( $lang, $row->mr_timestamp );
+                               $this->getModule( $row->mr_resource )->setMsgBlobMtime( $lang, $row->mr_timestamp );
                        }
                }
                foreach ( $modulesWithoutMessages as $name ) {
-                       $this->modules[$name]->setMsgBlobMtime( $lang, 0 );
+                       $this->getModule( $name )->setMsgBlobMtime( $lang, 0 );
                }
        }
 
@@ -172,7 +174,7 @@ class ResourceLoader {
         * Registers core modules and runs registration hooks.
         */
        public function __construct() {
-               global $IP;
+               global $IP, $wgResourceModules;
                
                wfProfileIn( __METHOD__ );
                
@@ -180,6 +182,7 @@ class ResourceLoader {
                $this->register( include( "$IP/resources/Resources.php" ) );
                // Register extension modules
                wfRunHooks( 'ResourceLoaderRegisterModules', array( &$this ) );
+               $this->register( $wgResourceModules );
                
                wfProfileOut( __METHOD__ );
        }
@@ -188,30 +191,27 @@ class ResourceLoader {
         * Registers a module with the ResourceLoader system.
         * 
         * @param $name Mixed: Name of module as a string or List of name/object pairs as an array
-        * @param $object ResourceLoaderModule: Module object (optional when using 
-        *     multiple-registration calling style)
+        * @param $info Module info array. For backwards compatibility with 1.17alpha, 
+        *   this may also be a ResourceLoaderModule object. Optional when using 
+        *   multiple-registration calling style.
         * @throws MWException: If a duplicate module registration is attempted
         * @throws MWException: If something other than a ResourceLoaderModule is being registered
         * @return Boolean: False if there were any errors, in which case one or more modules were not
         *     registered
         */
-       public function register( $name, ResourceLoaderModule $object = null ) {
-
+       public function register( $name, $info = null ) {
                wfProfileIn( __METHOD__ );
 
                // Allow multiple modules to be registered in one call
-               if ( is_array( $name ) && !isset( $object ) ) {
+               if ( is_array( $name ) ) {
                        foreach ( $name as $key => $value ) {
                                $this->register( $key, $value );
                        }
-
-                       wfProfileOut( __METHOD__ );
-
                        return;
                }
 
                // Disallow duplicate registrations
-               if ( isset( $this->modules[$name] ) ) {
+               if ( isset( $this->moduleInfos[$name] ) ) {
                        // A module has already been registered by this name
                        throw new MWException(
                                'ResourceLoader duplicate registration error. ' . 
@@ -219,26 +219,32 @@ class ResourceLoader {
                        );
                }
 
-               // Validate the input (type hinting lets null through)
-               if ( !( $object instanceof ResourceLoaderModule ) ) {
-                       throw new MWException( 'ResourceLoader invalid module error. ' . 
-                               'Instances of ResourceLoaderModule expected.' );
+               // Attach module
+               if ( is_object( $info ) ) {
+                       // Old calling convention
+                       // Validate the input
+                       if ( !( $info instanceof ResourceLoaderModule ) ) {
+                               throw new MWException( 'ResourceLoader invalid module error. ' . 
+                                       'Instances of ResourceLoaderModule expected.' );
+                       }
+
+                       $this->moduleInfos[$name] = array( 'object' => $info );
+                       $this->modules[$name] = $info;
+               } else {
+                       // New calling convention
+                       $this->moduleInfos[$name] = $info;
                }
 
-               // Attach module
-               $this->modules[$name] = $object;
-               $object->setName( $name );
-               
                wfProfileOut( __METHOD__ );
        }
 
-       /**
-        * Gets a map of all modules and their options
+       /**
+        * Get a list of module names
         *
-        * @return Array: List of modules keyed by module name
+        * @return Array: List of module names
         */
-       public function getModules() {
-               return $this->modules;
+       public function getModuleNames() {
+               return array_keys( $this->moduleInfos );
        }
 
        /**
@@ -248,7 +254,29 @@ class ResourceLoader {
         * @return Mixed: ResourceLoaderModule if module has been registered, null otherwise
         */
        public function getModule( $name ) {
-               return isset( $this->modules[$name] ) ? $this->modules[$name] : null;
+               if ( !isset( $this->modules[$name] ) ) {
+                       if ( !isset( $this->moduleInfos[$name] ) ) {
+                               // No such module
+                               return null;
+                       }
+                       // Construct the requested object
+                       $info = $this->moduleInfos[$name];
+                       if ( isset( $info['object'] ) ) {
+                               // Object given in info array
+                               $object = $info['object'];
+                       } else {
+                               if ( !isset( $info['class'] ) ) {
+                                       $class = 'ResourceLoaderFileModule';
+                               } else {
+                                       $class = $info['class'];
+                               }
+                               $object = new $class( $info );
+                       }
+                       $object->setName( $name );
+                       $this->modules[$name] = $object;
+               }
+
+               return $this->modules[$name];
        }
 
        /**
@@ -265,8 +293,8 @@ class ResourceLoader {
                $modules = array();
                $missing = array();
                foreach ( $context->getModules() as $name ) {
-                       if ( isset( $this->modules[$name] ) ) {
-                               $modules[$name] = $this->modules[$name];
+                       if ( isset( $this->moduleInfos[$name] ) ) {
+                               $modules[$name] = $this->getModule( $name );
                        } else {
                                $missing[] = $name;
                        }
@@ -376,7 +404,7 @@ class ResourceLoader {
                        if ( $context->shouldIncludeStyles() ) {
                                $styles = $module->getStyles( $context );
                                // Flip CSS on a per-module basis
-                               if ( $styles && $this->modules[$name]->getFlip( $context ) ) {
+                               if ( $styles && $module->getFlip( $context ) ) {
                                        foreach ( $styles as $media => $style ) {
                                                $styles[$media] = $this->filter( 'flip-css', $style );
                                        }
index 8413601..b569d25 100644 (file)
@@ -105,6 +105,12 @@ class ResourceLoaderFileModule extends ResourceLoaderModule {
         * 
         * @example $options
         *      array(
+        *              // Base path to prepend to all local paths in $options. Defaults to $IP
+        *              'localBasePath' => [base path],
+        *              // Base path to prepend to all remote paths in $options. Defaults to $wgScriptPath
+        *      'remoteBasePath' => [base path],
+        *      // Equivalent of remoteBasePath, but relative to $wgExtensionAssetsPath
+        *      'remoteExtPath' => [base path],
         *              // Scripts to always include
         *              'scripts' => [file path string or array of file path strings],
         *              // Scripts to include in specific language contexts
@@ -139,6 +145,12 @@ class ResourceLoaderFileModule extends ResourceLoaderModule {
                global $IP, $wgScriptPath;
                $this->localBasePath = $localBasePath === null ? $IP : $localBasePath;
                $this->remoteBasePath = $remoteBasePath === null ? $wgScriptPath : $remoteBasePath;
+
+               if ( isset( $options['remoteExtPath'] ) ) {
+                       global $wgExtensionAssetsPath;
+                       $this->remoteBasePath = $wgExtensionAssetsPath . '/' . $options['remoteExtPath'];
+               }
+
                foreach ( $options as $member => $option ) {
                        switch ( $member ) {
                                // Lists of file paths
@@ -175,6 +187,8 @@ class ResourceLoaderFileModule extends ResourceLoaderModule {
                                        break;
                                // Single strings
                                case 'group':
+                               case 'localBasePath':
+                               case 'remoteBasePath':
                                        $this->{$member} = (string) $option;
                                        break;
                                // Single booleans
index 718831e..f0c1e80 100644 (file)
@@ -101,7 +101,9 @@ class ResourceLoaderStartUpModule extends ResourceLoaderModule {
                
                $out = '';
                $registrations = array();
-               foreach ( $context->getResourceLoader()->getModules() as $name => $module ) {
+               $resourceLoader = $context->getResourceLoader();
+               foreach ( $resourceLoader->getModuleNames() as $name ) {
+                       $module = $resourceLoader->getModule( $name );
                        // Support module loader scripts
                        $loader = $module->getLoaderScript();
                        if ( $loader !== false ) {
@@ -193,7 +195,9 @@ class ResourceLoaderStartUpModule extends ResourceLoaderModule {
                // infinite recursion - think carefully before making changes to this 
                // code!
                $time = wfTimestamp( TS_UNIX, $wgCacheEpoch );
-               foreach ( $context->getResourceLoader()->getModules() as $module ) {
+               $loader = $context->getResourceLoader();
+               foreach ( $loader->getModuleNames() as $name ) {
+                       $module = $loader->getModule( $name );
                        $time = max( $time, $module->getModifiedTime( $context ) );
                }
                return $this->modifiedTime[$hash] = $time;
index 84dc0c3..9af4c7d 100644 (file)
@@ -4,89 +4,85 @@ return array(
 
        /* Special resources who have their own classes */
        
-       'site' => new ResourceLoaderSiteModule,
-       'startup' => new ResourceLoaderStartUpModule,
-       'user' => new ResourceLoaderUserModule,
-       'user.options' => new ResourceLoaderUserOptionsModule,
+       'site' => array( 'class' => 'ResourceLoaderSiteModule' ),
+       'startup' => array( 'class' => 'ResourceLoaderStartUpModule' ),
+       'user' => array( 'class' => 'ResourceLoaderUserModule' ),
+       'user.options' => array( 'class' => 'ResourceLoaderUserOptionsModule' ),
        
        /* Skins */
        
-       'skins.vector' => new ResourceLoaderFileModule(
-               array( 'styles' => array( 'skins/vector/screen.css' => array( 'media' => 'screen' ) ) )
-       ),
-       'skins.monobook' => new ResourceLoaderFileModule(
-               array( 'styles' => array(
-                               'skins/monobook/main.css' => array( 'media' => 'screen' ),
-                               // Honor $wgHandheldStyle. This is kind of evil
-                               //$GLOBALS['wgHandheldStyle'] => array( 'media' => 'handheld' )
-                       )
+       'skins.vector' => array( 
+               'styles' => array( 'skins/vector/screen.css' => array( 'media' => 'screen' ) )
+       ),
+       'skins.monobook' => array(
+               'styles' => array(
+                       'skins/monobook/main.css' => array( 'media' => 'screen' ),
+                       // Honor $wgHandheldStyle. This is kind of evil
+                       //$GLOBALS['wgHandheldStyle'] => array( 'media' => 'handheld' )
                )
        ),
        
        /* jQuery */
        
-       'jquery' => new ResourceLoaderFileModule( 
-               array( 'scripts' => 'resources/jquery/jquery.js', 'debugRaw' => false ) 
+       'jquery' => array(
+               'scripts' => 'resources/jquery/jquery.js', 
+               'debugRaw' => false
        ),
        
        /* jQuery Plugins */
        
-       'jquery.async' => new ResourceLoaderFileModule(
-               array( 'scripts' => 'resources/jquery/jquery.async.js' )
+       'jquery.async' => array( 
+               'scripts' => 'resources/jquery/jquery.async.js'
        ),
-       'jquery.autoEllipsis' => new ResourceLoaderFileModule(
-               array( 'scripts' => 'resources/jquery/jquery.autoEllipsis.js' )
+       'jquery.autoEllipsis' => array( 
+               'scripts' => 'resources/jquery/jquery.autoEllipsis.js'
        ),
-       'jquery.checkboxShiftClick' => new ResourceLoaderFileModule(
-               array( 'scripts' => 'resources/jquery/jquery.checkboxShiftClick.js' )
+       'jquery.checkboxShiftClick' => array(
+               'scripts' => 'resources/jquery/jquery.checkboxShiftClick.js'
        ),
-       'jquery.client' => new ResourceLoaderFileModule(
-               array( 'scripts' => 'resources/jquery/jquery.client.js' )
+       'jquery.client' => array(
+               'scripts' => 'resources/jquery/jquery.client.js',
        ),
-       'jquery.collapsibleTabs' => new ResourceLoaderFileModule(
-               array( 'scripts' => 'resources/jquery/jquery.collapsibleTabs.js' )
+       'jquery.collapsibleTabs' => array(
+               'scripts' => 'resources/jquery/jquery.collapsibleTabs.js'
        ),
-       'jquery.color' => new ResourceLoaderFileModule(
-               array( 'scripts' => 'resources/jquery/jquery.color.js' )
+       'jquery.color' => array(
+               'scripts' => 'resources/jquery/jquery.color.js'
        ),
-       'jquery.cookie' => new ResourceLoaderFileModule(
-               array( 'scripts' => 'resources/jquery/jquery.cookie.js' )
+       'jquery.cookie' => array(
+               'scripts' => 'resources/jquery/jquery.cookie.js'
        ),
-       'jquery.delayedBind' => new ResourceLoaderFileModule(
-               array( 'scripts' => 'resources/jquery/jquery.delayedBind.js' )
+       'jquery.delayedBind' => array(
+               'scripts' => 'resources/jquery/jquery.delayedBind.js'
        ),
-       'jquery.expandableField' => new ResourceLoaderFileModule(
-               array( 'scripts' => 'resources/jquery/jquery.expandableField.js' )
+       'jquery.expandableField' => array(
+               'scripts' => 'resources/jquery/jquery.expandableField.js'
        ),
-       'jquery.highlightText' => new ResourceLoaderFileModule(
-               array( 'scripts' => 'resources/jquery/jquery.highlightText.js' )
+       'jquery.highlightText' => array(
+               'scripts' => 'resources/jquery/jquery.highlightText.js'
        ),
-       'jquery.placeholder' => new ResourceLoaderFileModule(
-               array( 'scripts' => 'resources/jquery/jquery.placeholder.js' )
+       'jquery.placeholder' => array(
+               'scripts' => 'resources/jquery/jquery.placeholder.js'
        ),
-       'jquery.suggestions' => new ResourceLoaderFileModule(
-               array(
-                       'scripts' => 'resources/jquery/jquery.suggestions.js',
-                       'styles' => 'resources/jquery/jquery.suggestions.css',
-               )
+       'jquery.suggestions' => array(
+               'scripts' => 'resources/jquery/jquery.suggestions.js',
+               'styles' => 'resources/jquery/jquery.suggestions.css',
        ),
-       'jquery.tabIndex' => new ResourceLoaderFileModule(
-               array( 'scripts' => 'resources/jquery/jquery.tabIndex.js' )
+       'jquery.tabIndex' => array(
+               'scripts' => 'resources/jquery/jquery.tabIndex.js'
        ),
-       'jquery.textSelection' => new ResourceLoaderFileModule(
-               array( 'scripts' => 'resources/jquery/jquery.textSelection.js' )
+       'jquery.textSelection' => array(
+               'scripts' => 'resources/jquery/jquery.textSelection.js'
        ),
-       'jquery.tipsy' => new ResourceLoaderFileModule(
-               array(
-                       'scripts' => 'resources/jquery.tipsy/jquery.tipsy.js',
-                       'styles' => 'resources/jquery.tipsy/jquery.tipsy.css',
-               )
+       'jquery.tipsy' => array(
+               'scripts' => 'resources/jquery.tipsy/jquery.tipsy.js',
+               'styles' => 'resources/jquery.tipsy/jquery.tipsy.css',
        ),
        
        /* jQuery UI */
        
        // Core
-       'jquery.ui.core' => new ResourceLoaderFileModule( array(
+       'jquery.ui.core' => array(
                'scripts' => 'resources/jquery.ui/jquery.ui.core.js',
                'skinStyles' => array(
                        'default' => array(
@@ -99,74 +95,74 @@ return array(
                        ),
                ),
                'dependencies' => 'jquery',
-       ) ),
-       'jquery.ui.widget' => new ResourceLoaderFileModule( array(
+       ),
+       'jquery.ui.widget' => array(
                'scripts' => 'resources/jquery.ui/jquery.ui.widget.js',
-       ) ),
-       'jquery.ui.mouse' => new ResourceLoaderFileModule( array(
+       ),
+       'jquery.ui.mouse' => array(
                'scripts' => 'resources/jquery.ui/jquery.ui.mouse.js',
                'dependencies' => 'jquery.ui.widget',
-       ) ),
-       'jquery.ui.position' => new ResourceLoaderFileModule( array(
+       ),
+       'jquery.ui.position' => array(
                'scripts' => 'resources/jquery.ui/jquery.ui.position.js',
-       ) ),
+       ),
        // Interactions
-       'jquery.ui.draggable' => new ResourceLoaderFileModule( array(
+       'jquery.ui.draggable' => array(
                'scripts' => 'resources/jquery.ui/jquery.ui.draggable.js',
                'dependencies' => array( 'jquery.ui.core', 'jquery.ui.mouse', 'jquery.ui.widget' ),
-       ) ),
-       'jquery.ui.droppable' => new ResourceLoaderFileModule( array(
+       ),
+       'jquery.ui.droppable' => array(
                'scripts' => 'resources/jquery.ui/jquery.ui.droppable.js',
                'dependencies' => array( 
                        'jquery.ui.core', 'jquery.ui.mouse', 'jquery.ui.widget', 'jquery.ui.draggable' 
                ),
-       ) ),
-       'jquery.ui.resizable' => new ResourceLoaderFileModule( array(
+       ),
+       'jquery.ui.resizable' => array(
                'scripts' => 'resources/jquery.ui/jquery.ui.resizable.js',
                'skinStyles' => array(
                        'default' => 'resources/jquery.ui/themes/default/jquery.ui.resizable.css',
                        'vector' => 'resources/jquery.ui/themes/vector/jquery.ui.resizable.css',
                ),
                'dependencies' => array( 'jquery.ui.core', 'jquery.ui.widget', 'jquery.ui.mouse' ),
-       ) ),
-       'jquery.ui.selectable' => new ResourceLoaderFileModule( array(
+       ),
+       'jquery.ui.selectable' => array(
                'scripts' => 'resources/jquery.ui/jquery.ui.selectable.js',
                'skinStyles' => array(
                        'default' => 'resources/jquery.ui/themes/default/jquery.ui.selectable.css',
                        'vector' => 'resources/jquery.ui/themes/vector/jquery.ui.selectable.css',
                ),
                'dependencies' => array( 'jquery.ui.core', 'jquery.ui.widget', 'jquery.ui.mouse' ),
-       ) ),
-       'jquery.ui.sortable' => new ResourceLoaderFileModule( array(
+       ),
+       'jquery.ui.sortable' => array(
                'scripts' => 'resources/jquery.ui/jquery.ui.sortable.js',
                'dependencies' => array( 'jquery.ui.core', 'jquery.ui.widget', 'jquery.ui.mouse' ),
-       ) ),
+       ),
        // Widgets
-       'jquery.ui.accordion' => new ResourceLoaderFileModule( array(
+       'jquery.ui.accordion' => array(
                'scripts' => 'resources/jquery.ui/jquery.ui.accordion.js',
                'dependencies' => array( 'jquery.ui.core', 'jquery.ui.widget' ),
                'skinStyles' => array(
                        'default' => 'resources/jquery.ui/themes/default/jquery.ui.accordion.css',
                        'vector' => 'resources/jquery.ui/themes/vector/jquery.ui.accordion.css',
                ),
-       ) ),
-       'jquery.ui.autocomplete' => new ResourceLoaderFileModule( array(
+       ),
+       'jquery.ui.autocomplete' => array(
                'scripts' => 'resources/jquery.ui/jquery.ui.autocomplete.js',
                'dependencies' => array( 'jquery.ui.core', 'jquery.ui.widget', 'jquery.ui.position' ),
                'skinStyles' => array(
                        'default' => 'resources/jquery.ui/themes/default/jquery.ui.autocomplete.css',
                        'vector' => 'resources/jquery.ui/themes/vector/jquery.ui.autocomplete.css',
                ),
-       ) ),
-       'jquery.ui.button' => new ResourceLoaderFileModule( array(
+       ),
+       'jquery.ui.button' => array(
                'scripts' => 'resources/jquery.ui/jquery.ui.button.js',
                'dependencies' => array( 'jquery.ui.core', 'jquery.ui.widget' ),
                'skinStyles' => array(
                        'default' => 'resources/jquery.ui/themes/default/jquery.ui.button.css',
                        'vector' => 'resources/jquery.ui/themes/vector/jquery.ui.button.css',
                ),
-       ) ),
-       'jquery.ui.datepicker' => new ResourceLoaderFileModule( array(
+       ),
+       'jquery.ui.datepicker' => array(
                'scripts' => 'resources/jquery.ui/jquery.ui.datepicker.js',
                'dependencies' => 'jquery.ui.core',
                'skinStyles' => array(
@@ -227,8 +223,8 @@ return array(
                        'zh-hk' => 'resources/jquery.ui/i18n/jquery.ui.datepicker-zh-HK.js',
                        'zh-tw' => 'resources/jquery.ui/i18n/jquery.ui.datepicker-zh-TW.js'
                ),
-       ) ),
-       'jquery.ui.dialog' => new ResourceLoaderFileModule( array(
+       ),
+       'jquery.ui.dialog' => array(
                'scripts' => 'resources/jquery.ui/jquery.ui.dialog.js',
                'dependencies' => array(
                        'jquery.ui.core',
@@ -243,112 +239,112 @@ return array(
                        'default' => 'resources/jquery.ui/themes/default/jquery.ui.dialog.css',
                        'vector' => 'resources/jquery.ui/themes/vector/jquery.ui.dialog.css',
                ),
-       ) ),
-       'jquery.ui.progressbar' => new ResourceLoaderFileModule( array(
+       ),
+       'jquery.ui.progressbar' => array(
                'scripts' => 'resources/jquery.ui/jquery.ui.progressbar.js',
                'dependencies' => array( 'jquery.ui.core', 'jquery.ui.widget' ),
                'skinStyles' => array(
                        'default' => 'resources/jquery.ui/themes/default/jquery.ui.progressbar.css',
                        'vector' => 'resources/jquery.ui/themes/vector/jquery.ui.progressbar.css',
                ),
-       ) ),
-       'jquery.ui.slider' => new ResourceLoaderFileModule( array(
+       ),
+       'jquery.ui.slider' => array(
                'scripts' => 'resources/jquery.ui/jquery.ui.slider.js',
                'dependencies' => array( 'jquery.ui.core', 'jquery.ui.widget', 'jquery.ui.mouse' ),
                'skinStyles' => array(
                        'default' => 'resources/jquery.ui/themes/default/jquery.ui.slider.css',
                        'vector' => 'resources/jquery.ui/themes/vector/jquery.ui.slider.css',
                ),
-       ) ),
-       'jquery.ui.tabs' => new ResourceLoaderFileModule( array(
+       ),
+       'jquery.ui.tabs' => array(
                'scripts' => 'resources/jquery.ui/jquery.ui.tabs.js',
                'dependencies' => array( 'jquery.ui.core', 'jquery.ui.widget' ),
                'skinStyles' => array(
                        'default' => 'resources/jquery.ui/themes/default/jquery.ui.tabs.css',
                        'vector' => 'resources/jquery.ui/themes/vector/jquery.ui.tabs.css',
                ),
-       ) ),
+       ),
        // Effects
-       'jquery.effects.core' => new ResourceLoaderFileModule( array(
+       'jquery.effects.core' => array(
                'scripts' => 'resources/jquery.effects/jquery.effects.core.js',
                'dependencies' => 'jquery',
-       ) ),
-       'jquery.effects.blind' => new ResourceLoaderFileModule( array(
+       ),
+       'jquery.effects.blind' => array(
                'scripts' => 'resources/jquery.effects/jquery.effects.blind.js',
                'dependencies' => 'jquery.effects.core',
-       ) ),
-       'jquery.effects.bounce' => new ResourceLoaderFileModule( array(
+       ),
+       'jquery.effects.bounce' => array(
                'scripts' => 'resources/jquery.effects/jquery.effects.bounce.js',
                'dependencies' => 'jquery.effects.core',
-       ) ),
-       'jquery.effects.clip' => new ResourceLoaderFileModule( array(
+       ),
+       'jquery.effects.clip' => array(
                'scripts' => 'resources/jquery.effects/jquery.effects.clip.js',
                'dependencies' => 'jquery.effects.core',
-       ) ),
-       'jquery.effects.drop' => new ResourceLoaderFileModule( array(
+       ),
+       'jquery.effects.drop' => array(
                'scripts' => 'resources/jquery.effects/jquery.effects.drop.js',
                'dependencies' => 'jquery.effects.core',
-       ) ),
-       'jquery.effects.explode' => new ResourceLoaderFileModule( array(
+       ),
+       'jquery.effects.explode' => array(
                'scripts' => 'resources/jquery.effects/jquery.effects.explode.js',
                'dependencies' => 'jquery.effects.core',
-       ) ),
-       'jquery.effects.fold' => new ResourceLoaderFileModule( array(
+       ),
+       'jquery.effects.fold' => array(
                'scripts' => 'resources/jquery.effects/jquery.effects.fold.js',
                'dependencies' => 'jquery.effects.core',
-       ) ),
-       'jquery.effects.highlight' => new ResourceLoaderFileModule( array(
+       ),
+       'jquery.effects.highlight' => array(
                'scripts' => 'resources/jquery.effects/jquery.effects.highlight.js',
                'dependencies' => 'jquery.effects.core',
-       ) ),
-       'jquery.effects.pulsate' => new ResourceLoaderFileModule( array(
+       ),
+       'jquery.effects.pulsate' => array(
                'scripts' => 'resources/jquery.effects/jquery.effects.pulsate.js',
                'dependencies' => 'jquery.effects.core',
-       ) ),
-       'jquery.effects.scale' => new ResourceLoaderFileModule( array(
+       ),
+       'jquery.effects.scale' => array(
                'scripts' => 'resources/jquery.effects/jquery.effects.scale.js',
                'dependencies' => 'jquery.effects.core',
-       ) ),
-       'jquery.effects.shake' => new ResourceLoaderFileModule( array(
+       ),
+       'jquery.effects.shake' => array(
                'scripts' => 'resources/jquery.effects/jquery.effects.shake.js',
                'dependencies' => 'jquery.effects.core',
-       ) ),
-       'jquery.effects.slide' => new ResourceLoaderFileModule( array(
+       ),
+       'jquery.effects.slide' => array(
                'scripts' => 'resources/jquery.effects/jquery.effects.slide.js',
                'dependencies' => 'jquery.effects.core',
-       ) ),
-       'jquery.effects.transfer' => new ResourceLoaderFileModule( array(
+       ),
+       'jquery.effects.transfer' => array(
                'scripts' => 'resources/jquery.effects/jquery.effects.transfer.js',
                'dependencies' => 'jquery.effects.core',
-       ) ),
+       ),
        
        /* MediaWiki */
        
-       'mediawiki' => new ResourceLoaderFileModule( array(
+       'mediawiki' => array(
                'scripts' => 'resources/mediawiki/mediawiki.js',
                'debugScripts' => 'resources/mediawiki/mediawiki.log.js',
                'debugRaw' => false
-       ) ),
-       'mediawiki.util' => new ResourceLoaderFileModule( array(
+       ),
+       'mediawiki.util' => array(
                'scripts' => 'resources/mediawiki.util/mediawiki.util.js',
                'dependencies' => array( 'jquery.checkboxShiftClick', 'jquery.client' ),
                'debugScripts' => 'resources/mediawiki.util/mediawiki.util.test.js',
-       ) ),
-       'mediawiki.action.view.rightClickEdit' => new ResourceLoaderFileModule( array(
+       ),
+       'mediawiki.action.view.rightClickEdit' => array(
                'scripts' => 'resources/mediawiki.action/mediawiki.action.view.rightClickEdit.js',
-       ) ),
-       'mediawiki.special.preferences' => new ResourceLoaderFileModule( array(
+       ),
+       'mediawiki.special.preferences' => array(
                'scripts' => 'resources/mediawiki.special/mediawiki.special.preferences.js',
                'styles' => 'resources/mediawiki.special/mediawiki.special.preferences.css',
-       ) ),
-       'mediawiki.special.search' => new ResourceLoaderFileModule( array(
+       ),
+       'mediawiki.special.search' => array(
                'scripts' => 'resources/mediawiki.special/mediawiki.special.search.js',
-       ) ),
-       'mediawiki.action.history' => new ResourceLoaderFileModule( array(
+       ),
+       'mediawiki.action.history' => array(
                'scripts' => 'resources/mediawiki.action/mediawiki.action.history.js',
                'dependencies' => 'mediawiki.legacy.history',
-       ) ),
-       'mediawiki.language' => new ResourceLoaderFileModule( array(
+       ),
+       'mediawiki.language' => array(
                'scripts' => 'resources/mediawiki.language/mediawiki.language.js',
                'languageScripts' => array(
                        'am' => 'resources/mediawiki.language/languages/am.js',
@@ -397,111 +393,111 @@ return array(
                        'uk' => 'resources/mediawiki.language/languages/uk.js',
                        'wa' => 'resources/mediawiki.language/languages/wa.js',
                ),
-       ) ),
+       ),
        
        /* mediawiki Legacy */
        
-       'mediawiki.legacy.ajax' => new ResourceLoaderFileModule( array(
+       'mediawiki.legacy.ajax' => array(
                'scripts' => 'skins/common/ajax.js',
                'messages' => array( 
                        'watch', 'unwatch', 'watching', 'unwatching', 'tooltip-ca-watch', 
                        'tooltip-ca-unwatch' 
                ),
                'dependencies' => 'mediawiki.legacy.wikibits',
-       ) ),
-       'mediawiki.legacy.ajaxwatch' => new ResourceLoaderFileModule( array(
+       ),
+       'mediawiki.legacy.ajaxwatch' => array(
                'scripts' => 'skins/common/ajaxwatch.js',
                'dependencies' => 'mediawiki.legacy.wikibits',
-       ) ),
-       'mediawiki.legacy.block' => new ResourceLoaderFileModule( array(
+       ),
+       'mediawiki.legacy.block' => array(
                'scripts' => 'skins/common/block.js',
                'dependencies' => 'mediawiki.legacy.wikibits',
-       ) ),
-       'mediawiki.legacy.changepassword' => new ResourceLoaderFileModule( array(
+       ),
+       'mediawiki.legacy.changepassword' => array(
                'scripts' => 'skins/common/changepassword.js',
                'dependencies' => 'mediawiki.legacy.wikibits',
-       ) ),
-       'mediawiki.legacy.commonPrint' => new ResourceLoaderFileModule( array(
+       ),
+       'mediawiki.legacy.commonPrint' => array(
                'styles' => array( 'skins/common/commonPrint.css' => array( 'media' => 'print' ) ),
-       ) ),
-       'mediawiki.legacy.config' => new ResourceLoaderFileModule( array(
+       ),
+       'mediawiki.legacy.config' => array(
                'scripts' => 'skins/common/config.js',
                'styles' => array( 'skins/common/config.css', 'skins/common/config-cc.css' ),
                'dependencies' => 'mediawiki.legacy.wikibits',
-       ) ),
-       'mediawiki.legacy.diff' => new ResourceLoaderFileModule( array(
+       ),
+       'mediawiki.legacy.diff' => array(
                'scripts' => 'skins/common/diff.js',
                'styles' => 'skins/common/diff.css',
                'dependencies' => 'mediawiki.legacy.wikibits',
-       ) ),
-       'mediawiki.legacy.edit' => new ResourceLoaderFileModule( array(
+       ),
+       'mediawiki.legacy.edit' => array(
                'scripts' => 'skins/common/edit.js',
                'dependencies' => 'mediawiki.legacy.wikibits',
-       ) ),
-       'mediawiki.legacy.enhancedchanges' => new ResourceLoaderFileModule( array(
+       ),
+       'mediawiki.legacy.enhancedchanges' => array(
                'scripts' => 'skins/common/enhancedchanges.js',
                'dependencies' => 'mediawiki.legacy.wikibits',
-       ) ),
-       'mediawiki.legacy.history' => new ResourceLoaderFileModule( array(
+       ),
+       'mediawiki.legacy.history' => array(
                'scripts' => 'skins/common/history.js',
                'dependencies' => 'mediawiki.legacy.wikibits',
-       ) ),
-       'mediawiki.legacy.htmlform' => new ResourceLoaderFileModule( array(
+       ),
+       'mediawiki.legacy.htmlform' => array(
                'scripts' => 'skins/common/htmlform.js',
                'dependencies' => 'mediawiki.legacy.wikibits',
-       ) ),
-       'mediawiki.legacy.IEFixes' => new ResourceLoaderFileModule( array(
+       ),
+       'mediawiki.legacy.IEFixes' => array(
                'scripts' => 'skins/common/IEFixes.js',
                'dependencies' => 'mediawiki.legacy.wikibits',
-       ) ),
-       'mediawiki.legacy.metadata' => new ResourceLoaderFileModule( array(
+       ),
+       'mediawiki.legacy.metadata' => array(
                'scripts' => 'skins/common/metadata.js',
                'dependencies' => 'mediawiki.legacy.wikibits',
                'messages' => array( 'metadata-expand', 'metadata-collapse' ),
-       ) ),
-       'mediawiki.legacy.mwsuggest' => new ResourceLoaderFileModule( array(
+       ),
+       'mediawiki.legacy.mwsuggest' => array(
                'scripts' => 'skins/common/mwsuggest.js',
                'dependencies' => 'mediawiki.legacy.wikibits',
                'messages' => array( 'search-mwsuggest-enabled', 'search-mwsuggest-disabled' ),
-       ) ),
-       'mediawiki.legacy.password' => new ResourceLoaderFileModule( array(
+       ),
+       'mediawiki.legacy.password' => array(
                'scripts' => 'skins/common/password.js',
                'styles' => 'skins/common/password.css',
                'dependencies' => 'mediawiki.legacy.wikibits',
-       ) ),
-       'mediawiki.legacy.prefs' => new ResourceLoaderFileModule( array(
+       ),
+       'mediawiki.legacy.prefs' => array(
                'scripts' => 'skins/common/prefs.js',
                'dependencies' => array( 'mediawiki.legacy.wikibits', 'mediawiki.legacy.htmlform' ),
-       ) ),
-       'mediawiki.legacy.preview' => new ResourceLoaderFileModule( array(
+       ),
+       'mediawiki.legacy.preview' => array(
                'scripts' => 'skins/common/preview.js',
                'dependencies' => 'mediawiki.legacy.wikibits',
-       ) ),
-       'mediawiki.legacy.protect' => new ResourceLoaderFileModule( array(
+       ),
+       'mediawiki.legacy.protect' => array(
                'scripts' => 'skins/common/protect.js',
                'dependencies' => 'mediawiki.legacy.wikibits',
-       ) ),
-       'mediawiki.legacy.search' => new ResourceLoaderFileModule( array(
+       ),
+       'mediawiki.legacy.search' => array(
                'scripts' => 'skins/common/search.js',
                'styles' => 'skins/common/search.css',
                'dependencies' => 'mediawiki.legacy.wikibits',
-       ) ),
-       'mediawiki.legacy.shared' => new ResourceLoaderFileModule( array(
+       ),
+       'mediawiki.legacy.shared' => array(
                'styles' => array( 'skins/common/shared.css' => array( 'media' => 'screen' ) ),
-       ) ),
-       'mediawiki.legacy.oldshared' => new ResourceLoaderFileModule( array(
+       ),
+       'mediawiki.legacy.oldshared' => array(
                'styles' => array( 'skins/common/oldshared.css' => array( 'media' => 'screen' ) ),
-       ) ),
-       'mediawiki.legacy.upload' => new ResourceLoaderFileModule( array(
+       ),
+       'mediawiki.legacy.upload' => array(
                'scripts' => 'skins/common/upload.js',
                'dependencies' => 'mediawiki.legacy.wikibits',
-       ) ),
-       'mediawiki.legacy.wikibits' => new ResourceLoaderFileModule( array(
+       ),
+       'mediawiki.legacy.wikibits' => array(
                'scripts' => 'skins/common/wikibits.js',
                'dependencies' => 'mediawiki.language',
                'messages' => array( 'showtoc', 'hidetoc' ),
-       ) ),
-       'mediawiki.legacy.wikiprintable' => new ResourceLoaderFileModule( array(
+       ),
+       'mediawiki.legacy.wikiprintable' => array(
                'styles' => array( 'skins/common/wikiprintable.css' => array( 'media' => 'print' ) ),
-       ) ),
+       ),
 );