From 94c1162400ceabf8ba78a56fd0ffd90e63fee9be Mon Sep 17 00:00:00 2001 From: jdlrobson Date: Tue, 29 Dec 2015 09:32:46 -1000 Subject: [PATCH] resourceloader: Implement modern module loading (1/2) This defines mw.loader.require() and 'module.exports'. These will be exposed to mw.loader.implement() closures as local 'require' and 'module' parameters. Changes: * This alters nestedAddScript to maintain a single queue to ensure scripts from different modules are never downloaded in parallel (used in debug mode). Note: A further patch will start passing module and require to module definitions. Bug: T108655 Change-Id: Ia925844cc22f143f531216f2fe3efead08885b5d --- .jshintrc | 2 + resources/src/mediawiki/mediawiki.js | 74 ++++++++++++++++++- .../resources/mediawiki/mediawiki.test.js | 42 +++++++++++ 3 files changed, 115 insertions(+), 3 deletions(-) diff --git a/.jshintrc b/.jshintrc index 62b9d82314..441c4e310b 100644 --- a/.jshintrc +++ b/.jshintrc @@ -20,6 +20,8 @@ "browser": true, "globals": { + "require": false, + "module": false, "mediaWiki": true, "JSON": true, "OO": true, diff --git a/resources/src/mediawiki/mediawiki.js b/resources/src/mediawiki/mediawiki.js index 2aada9e66f..f282db607c 100644 --- a/resources/src/mediawiki/mediawiki.js +++ b/resources/src/mediawiki/mediawiki.js @@ -714,6 +714,7 @@ * 'group': 'somegroup', (or) null * 'source': 'local', (or) 'anotherwiki' * 'skip': 'return !!window.Example', (or) null + * 'module': export Object * * // Set from execute() or mw.loader.state() * 'state': 'registered', 'loaded', 'loading', 'ready', 'error', or 'missing' @@ -767,6 +768,10 @@ // List of modules which will be loaded as when ready batch = [], + // Pending queueModuleScript() requests + handlingPendingRequests = false, + pendingRequests = [], + // List of modules to be loaded queue = [], @@ -1176,6 +1181,43 @@ } ); } + /** + * Queue the loading and execution of a script for a particular module. + * + * @private + * @param {string} src URL of the script + * @param {string} [moduleName] Name of currently executing module + * @return {jQuery.Promise} + */ + 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) + if ( pendingRequests[ 0 ] ) { + pendingRequests.shift()(); + } else { + handlingPendingRequests = false; + } + } ); + } ); + if ( !handlingPendingRequests && pendingRequests[ 0 ] ) { + handlingPendingRequests = true; + pendingRequests.shift()(); + } + return r.promise(); + } + /** * Utility function for execute() * @@ -1226,7 +1268,7 @@ handlePending( module ); }; nestedAddScript = function ( arr, callback, i ) { - // Recursively call addScript() in its own callback + // Recursively call queueModuleScript() in its own callback // for each element of arr. if ( i >= arr.length ) { // We're at the end of the array @@ -1234,7 +1276,7 @@ return; } - addScript( arr[ i ] ).always( function () { + queueModuleScript( arr[ i ], module ).always( function () { nestedAddScript( arr, callback, i + 1 ); } ); }; @@ -1249,8 +1291,9 @@ } else if ( $.isFunction( script ) ) { // Pass jQuery twice so that the signature of the closure which wraps // the script can bind both '$' and 'jQuery'. - script( $, $ ); + script( $, $, mw.loader.require, registry[ module ].module ); markModuleReady(); + } else if ( typeof script === 'string' ) { // Site and user modules are legacy scripts that run in the global scope. // This is transported as a string instead of a function to avoid needing @@ -1742,6 +1785,11 @@ } // List the module as registered registry[ module ] = { + // Exposed to execute() for mw.loader.implement() closures. + // Import happens via require(). + module: { + exports: {} + }, version: version !== undefined ? String( version ) : '', dependencies: [], group: typeof group === 'string' ? group : null, @@ -2009,6 +2057,26 @@ } ); }, + /** + * Get the exported value of a module. + * + * Module provide this value via their local `module.exports`. + * + * @since 1.27 + * @return {Array} + */ + require: function ( moduleName ) { + var state = mw.loader.getState( moduleName ); + + // Only ready mudules can be required + if ( state !== 'ready' ) { + // Module may've forgotten to declare a dependency + throw new Error( 'Module "' + moduleName + '" is not loaded.' ); + } + + return registry[ moduleName ].module.exports; + }, + /** * @inheritdoc mw.inspect#runReports * @method diff --git a/tests/qunit/suites/resources/mediawiki/mediawiki.test.js b/tests/qunit/suites/resources/mediawiki/mediawiki.test.js index fe5530b285..ce4ea8b147 100644 --- a/tests/qunit/suites/resources/mediawiki/mediawiki.test.js +++ b/tests/qunit/suites/resources/mediawiki/mediawiki.test.js @@ -1085,4 +1085,46 @@ ); } ); + QUnit.test( 'mw.loader.require', 6, function ( assert ) { + var module1, module2, module3, module4; + + mw.loader.register( [ + [ 'test.module.require1', '0' ], + [ 'test.module.require2', '0' ], + [ 'test.module.require3', '0' ], + [ 'test.module.require4', '0', [ 'test.module.require3' ] ] + ] ); + mw.loader.implement( 'test.module.require1', function () {} ); + mw.loader.implement( 'test.module.require2', function ( $, jQuery, require, module ) { + module.exports = 1; + } ); + mw.loader.implement( 'test.module.require3', function ( $, jQuery, require, module ) { + module.exports = function () { + return 'hello world'; + }; + } ); + mw.loader.implement( 'test.module.require4', function ( $, jQuery, require, module ) { + var other = require( 'test.module.require3' ); + module.exports = { + pizza: function () { + return other(); + } + }; + } ); + module1 = mw.loader.require( 'test.module.require1' ); + module2 = mw.loader.require( 'test.module.require2' ); + module3 = mw.loader.require( 'test.module.require3' ); + module4 = mw.loader.require( 'test.module.require4' ); + + assert.strictEqual( typeof module1, 'object', 'export of module with no export' ); + assert.strictEqual( module2, 1, 'export a number' ); + assert.strictEqual( module3(), 'hello world', 'export a function' ); + assert.strictEqual( typeof module4.pizza, 'function', 'export an object' ); + assert.strictEqual( module4.pizza(), 'hello world', 'module can require other modules' ); + + assert.throws( function () { + mw.loader.require( '_badmodule' ); + }, /is not loaded/, 'Requesting non-existent modules throws error.' ); + } ); + }( mediaWiki, jQuery ) ); -- 2.20.1