Problem statement
When ResourceLoader was first released in 2011, the web was mostly still in a state where front-end JavaScript uses the global object as way to "connect the dots" between pieces of code. Whether it be module registration, plugin registration, public/static classes, private methods. They are mostly executed in either the global scope, or in a private closure.
As such, any code split up over multiple files could be safely concatenated. Private scopes could not cross the file-on-disk boundary because there was no good way to connect two in-Git-maintained files on the client-side without also making them completely public/global.
Since then, CommonJS and other module patterns have become popular, especially through the Node eco system.
As of 2016, we support require() and module.exports for entire modules, but ResourceLoader still concatenates all the files within a module. This has resulted in a confusing situation where, if you do decide to split up the files within a module, they effectively share the same exports object. This is non-standard and basically means use of module.exports is only useful if the ResourceLoader module consists of a single file (e.g. a simple module, a hard to maintain module, or a module maintained elsewhere that is included as external library).
Objectives
- Get rid of non-standard behaviour of having a "shared exports object" between files. This was unintentionally introduced, is undocumented/unsupported, and will go away as part of this RFC without deprecation.
- Allow one to implement a module with multiple files.
- Be compatible with Node.js, RequireJS, and WebPack.
Proposal
This RFC proposes to allow modules in ResourceLoader that are capable of exporting multiple files to the client-side that can individually be accessed within the module scope in a standard-compatible way, e.g. through require('./foo.js'). This will allow modules to be composed of multiple files, and also makes those modules interoperable between other environments such as RequireJS, WebPack, and Node.js.
This makes it easy for modules written for MediaWiki to be published on npm, whilst also making it easy for MediaWiki developers to integrate external libraries (like we do already with Composer for PHP).
Note that the question of automatically downloading and managing such packages is orthogonal (T107561). Regardless of whether upstream code needs to be manually checked into Git, we still currently only support single-file modules. This RFC deals with that. After T107561 is resolved, the model introduced by this RFC (T133462) would allow those external modules to be registered and loaded by ResourceLoader without any modifications. Currently we are unable to check in modules in their package form, and instead have to find classic style "single-file" combined distributions of upstream libraries. While libraries targeted at the browser landscape do often still provide such distributions, newer/generic libraries published on npm do not.
Usage examples
Old model with separate files
"module.name": { "styles": [ filePaths, .. ], "scripts": [ "foo.js", "bar.js" ] }
var other = require( 'other.module' ); mw.foo = other.make( 'foo' );
mw.bar = mw.foo.make( 'bar' );
function (mw, $, require) { var other = require( 'other.module' ); mw.foo = other.make( 'foo' ); mw.bar = mw.foo.make( 'bar' ); }
Old model with a single module export
"module.name": { "styles": [ filePaths, .. ], "scripts": [ "module.js" ] }
var other = require( 'other.module' ); var foo = other.make( 'foo' ); module.exports = foo.make( 'bar' );
function (mw, $, require, module) { var other = require( 'other.module' ); var foo = other.make( 'foo' ); module.exports = foo.make( 'bar' ); }
Proposed model supporting multi-file package
"module.name": { "styles": [ filePaths, .. ], "package": { "files": [ "foo.js" ], "main": "index.js" } }
var other = require( 'other.module' ); module.exports = other.make( 'foo' );
var foo = require( './foo.js' ); module.exports = foo.make( 'bar' );
{ main: 'index.js', files: { 'foo.js': function (mw, $, module, require) { var other = require( 'other.module' ); module.exports = other.make( 'foo' ); }, 'index.js': function (mw, $, module, require) { var foo = require( './foo' ); module.exports = foo.make( 'bar' ); } } }
See also:
- T108655: Standardise on how to access/register JavaScript interfaces - This introduced the current minimal require() and module.exports behaviours in ResourceLoader.
- T32956: Make ResourceLoader a standalone library
- T133388: How to update JavaScript components