resourceloader: Implement modern module loading (2/2)
authorjdlrobson <jdlrobson@gmail.com>
Fri, 22 Jan 2016 19:29:28 +0000 (11:29 -0800)
committerJdlrobson <jrobson@wikimedia.org>
Wed, 13 Apr 2016 15:43:41 +0000 (15:43 +0000)
* Send 'module' and 'require' parameters to module closures.
  This depends on Ia925844cc22f143 being deployed one cycle earlier.
* Patch Moment and OOjs to ensure these libraries continue to expose
  their module as globals as well. AMD/UMD-compatible libraries
  only expose a global *OR* an export, not both. We need both
  for back-compat.
* Update pluralRuleParser to make use of module export to allow
  usage via require().

To test, check out the patch and run:
> mw.loader.load('moment');
> mw.loader.require('moment')()
> mw.loader.require('moment')('2011-04-01').fromNow()

Bug: T108655
Change-Id: Idbd054880ee70d659ec760aef8fcb38d0704a394

13 files changed:
includes/resourceloader/ResourceLoader.php
resources/Resources.php
resources/src/mediawiki.libs/CLDRPluralRuleParser.js
resources/src/mediawiki/mediawiki.js
resources/src/moment-global.js [new file with mode: 0644]
resources/src/oojs-global.js [new file with mode: 0644]
tests/phpunit/includes/OutputPageTest.php
tests/phpunit/includes/resourceloader/ResourceLoaderTest.php
tests/qunit/QUnitTestResources.php
tests/qunit/data/defineCallMwLoaderTestCallback.js [new file with mode: 0644]
tests/qunit/data/requireCallMwLoaderTestCallback.js [new file with mode: 0644]
tests/qunit/suites/resources/mediawiki/mediawiki.test.js
tests/qunit/suites/resources/test.sinonjs/index.js [new file with mode: 0644]

index 0aa08be..086ab17 100644 (file)
@@ -1098,7 +1098,7 @@ MESSAGE;
                                        $scripts = self::filter( 'minify-js', $scripts );
                                }
                        } else {
-                               $scripts = new XmlJsCode( "function ( $, jQuery ) {\n{$scripts}\n}" );
+                               $scripts = new XmlJsCode( "function ( $, jQuery, require, module ) {\n{$scripts}\n}" );
                        }
                } elseif ( !is_array( $scripts ) ) {
                        throw new MWException( 'Invalid scripts error. Array of URLs or string of code expected.' );
index 42a0746..cb7adbe 100644 (file)
@@ -734,6 +734,7 @@ return [
        'moment' => [
                'scripts' => [
                        'resources/lib/moment/moment.js',
+                       'resources/src/moment-global.js',
                        'resources/src/moment-local-dmy.js',
                ],
                'languageScripts' => [
@@ -2278,6 +2279,7 @@ return [
        'oojs' => [
                'scripts' => [
                        'resources/lib/oojs/oojs.jquery.js',
+                       'resources/src/oojs-global.js',
                ],
                'targets' => [ 'desktop', 'mobile' ],
                'dependencies' => [
index 31c8fef..549a9ab 100644 (file)
@@ -591,5 +591,6 @@ function pluralRuleParser(rule, number) {
 
 /* pluralRuleParser ends here */
 mw.libs.pluralRuleParser = pluralRuleParser;
+module.exports = pluralRuleParser;
 
 } )( mediaWiki );
index 9d799db..4aad2ba 100644 (file)
                         * @param {string} [moduleName] Name of currently executing module
                         * @return {jQuery.Promise}
                         */
-                       function queueModuleScript( src ) {
+                       function queueModuleScript( src, moduleName ) {
                                var r = $.Deferred();
 
                                pendingRequests.push( function () {
+                                       if ( moduleName && hasOwn.call( registry, moduleName ) ) {
+                                               window.require = mw.loader.require;
+                                               window.module = registry[ moduleName ].module;
+                                       }
                                        addScript( src ).always( function () {
+                                               // Clear environment
+                                               delete window.require;
+                                               delete window.module;
                                                r.resolve();
 
                                                // Start the next one (if any)
diff --git a/resources/src/moment-global.js b/resources/src/moment-global.js
new file mode 100644 (file)
index 0000000..ba01a24
--- /dev/null
@@ -0,0 +1,2 @@
+// Back-compat: Export module as global
+window.moment = module.exports;
diff --git a/resources/src/oojs-global.js b/resources/src/oojs-global.js
new file mode 100644 (file)
index 0000000..de156f0
--- /dev/null
@@ -0,0 +1,2 @@
+// Back-compat: Export module as global
+window.OO = module.exports;
index 2f63ca8..8d4a347 100644 (file)
@@ -169,7 +169,7 @@ class OutputPageTest extends MediaWikiTestCase {
                        [
                                [ 'test.quux', ResourceLoaderModule::TYPE_COMBINED ],
                                "<script>(window.RLQ=window.RLQ||[]).push(function(){"
-                                       . "mw.loader.implement(\"test.quux\",function($,jQuery){"
+                                       . "mw.loader.implement(\"test.quux\",function($,jQuery,require,module){"
                                        . "mw.test.baz({token:123});},{\"css\":[\".mw-icon{transition:none}"
                                        . "\"]});});</script>"
                        ],
index 2dfed62..65cd6ed 100644 (file)
@@ -188,7 +188,7 @@ class ResourceLoaderTest extends ResourceLoaderTestCase {
                                'messages' => [ 'example' => '' ],
                                'templates' => [],
 
-                               'expected' => 'mw.loader.implement( "test.example", function ( $, jQuery ) {
+                               'expected' => 'mw.loader.implement( "test.example", function ( $, jQuery, require, module ) {
 mw.example();
 }, {
     "css": [
@@ -207,7 +207,7 @@ mw.example();
                                'messages' => new XmlJsCode( '{}' ),
                                'templates' => [],
 
-                               'expected' => 'mw.loader.implement( "test.example", function ( $, jQuery ) {
+                               'expected' => 'mw.loader.implement( "test.example", function ( $, jQuery, require, module ) {
 mw.example();
 } );',
                        ] ],
@@ -235,7 +235,7 @@ mw.example();
                                'messages' => [ 'example' => '' ],
                                'templates' => [],
 
-                               'expected' => 'mw.loader.implement( "test.example", function ( $, jQuery ) {
+                               'expected' => 'mw.loader.implement( "test.example", function ( $, jQuery, require, module ) {
 mw.example();
 }, {}, {
     "example": ""
@@ -250,7 +250,7 @@ mw.example();
                                'messages' => new XmlJsCode( '{}' ),
                                'templates' => [ 'example.html' => '' ],
 
-                               'expected' => 'mw.loader.implement( "test.example", function ( $, jQuery ) {
+                               'expected' => 'mw.loader.implement( "test.example", function ( $, jQuery, require, module ) {
 mw.example();
 }, {}, {}, {
     "example.html": ""
index b7161b1..a2d76e0 100644 (file)
@@ -8,7 +8,15 @@ return [
 
        'test.sinonjs' => [
                'scripts' => [
+                       'tests/qunit/suites/resources/test.sinonjs/index.js',
                        'resources/lib/sinonjs/sinon-1.17.3.js',
+                       // We want tests to work in IE, but can't include this as it
+                       // will break the placeholders in Sinon because the hack it uses
+                       // to hijack IE globals relies on running in the global scope
+                       // and in ResourceLoader this won't be running in the global scope.
+                       // Including it results (among other things) in sandboxed timers
+                       // being broken due to Date inheritance being undefined.
+                       // 'resources/lib/sinonjs/sinon-ie-1.15.4.js',
                ],
                'targets' => [ 'desktop', 'mobile' ],
        ],
diff --git a/tests/qunit/data/defineCallMwLoaderTestCallback.js b/tests/qunit/data/defineCallMwLoaderTestCallback.js
new file mode 100644 (file)
index 0000000..afd886c
--- /dev/null
@@ -0,0 +1 @@
+module.exports = 'Define worked.';
diff --git a/tests/qunit/data/requireCallMwLoaderTestCallback.js b/tests/qunit/data/requireCallMwLoaderTestCallback.js
new file mode 100644 (file)
index 0000000..8bc087b
--- /dev/null
@@ -0,0 +1,2 @@
+var x = require( 'test.require.define' );
+module.exports = 'Require worked.' + x;
index ce4ea8b..dd43c55 100644 (file)
                }, /is not loaded/, 'Requesting non-existent modules throws error.' );
        } );
 
+       QUnit.asyncTest( 'mw.loader require in debug mode', 1, function ( assert ) {
+               var path = mw.config.get( 'wgScriptPath' );
+               mw.loader.register( [
+                       [ 'test.require.define', '0' ],
+                       [ 'test.require.callback', '0', [ 'test.require.define' ] ]
+               ] );
+               mw.loader.implement( 'test.require.callback', [ QUnit.fixurl( path + '/tests/qunit/data/requireCallMwLoaderTestCallback.js' ) ] );
+               mw.loader.implement( 'test.require.define', [ QUnit.fixurl( path + '/tests/qunit/data/defineCallMwLoaderTestCallback.js' ) ] );
+
+               mw.loader.using( 'test.require.callback', function () {
+                       QUnit.start();
+                       var exported = mw.loader.require( 'test.require.callback' );
+                       assert.strictEqual( exported, 'Require worked.Define worked.',
+                               'module.exports worked in debug mode' );
+               }, function () {
+                       QUnit.start();
+                       assert.ok( false, 'Error callback fired while loader.using "test.require.callback" module' );
+               } );
+       } );
+
 }( mediaWiki, jQuery ) );
diff --git a/tests/qunit/suites/resources/test.sinonjs/index.js b/tests/qunit/suites/resources/test.sinonjs/index.js
new file mode 100644 (file)
index 0000000..b1be9d1
--- /dev/null
@@ -0,0 +1,3 @@
+// Hack: Disable 'module.exports' from ResourceLoader
+// (Otherwise Sinon assumes context as Node.js instead of a browser)
+module.exports = null;