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
This commit is contained in:
jdlrobson 2015-12-29 09:32:46 -10:00 committed by Timo Tijhof
parent db47c86bc5
commit 94c1162400
3 changed files with 115 additions and 3 deletions

View file

@ -20,6 +20,8 @@
"browser": true,
"globals": {
"require": false,
"module": false,
"mediaWiki": true,
"JSON": true,
"OO": true,

View file

@ -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

View file

@ -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 ) );