User:Jdlrobson/Developing with Webpack and ResourceLoader
This is a high-level (in progress) walkthrough of how front-end developers can build software in MediaWiki using Webpack. Webpack is used alongside ResourceLoader in Extension:MobileFrontend and Extension:Popups
For more about how ResourceLoader is designed, its features and why it works the way it does, see ResourceLoader/Features.
For complicated codebases, using Webpack inside MediaWiki can help with a variety of things:
- Ability to use npm packages
- Reduces the amount of edits needed to extension/skin.json
- Allows you to rename files freely and cheaply (many IDE's these days including Visual Code Studio can update import paths for you when you rename file but they cannot automatically update extension.json)
- Unit test discovery (no need to update a hook every time you add a unit test)
- Manages dependency trees for you
- Quicker unit tests
- Ability to get JavaScript code coverage reports from the command line
- Less excuses for not writing unit tests - no need for exposing public interfaces and complaining about speed
- Ability to use ES6 JavaScript
- Ability to use JavaScript map files.
There is currently one minor trade off we'll hoping to resolve:
- Built assets have to be committed on every commit (see phab:T199004 and Build step)
Porting from an existing RL module to Webpack
editImagine an extremely simplistic extension with 3 files.
// resources/foo.js
mw.foo = function() {}
// resources/bar.js
mw.bar = "hello"
// resources/my-rl-module.js
mw.foo();
alert( mw.bar );
In ResourceLoader this might look like this:
"ResourceModules": {
"ext.yourextname": {
"scripts": [
"resources/foo.js",
"resources/bar.js",
"resources/my-rl-module.js",
]
}
}
If foo, bar and myrlmodule grow in size, this pattern might not be ideal and Webpack may provide a solution for you. Imagine if those files were rewritten as source (src) files.
// src/foo.js
module.exports = function() {}
// src/bar.js
module.exports = "hello"
// src/my-rl-module.js
var foo = require( './foo' ),
bar = require( './bar' );
foo();
alert( bar );
The problem now becomes, how to compile that code into one single file that can be shipped by ResourceLoader. For this you can use Webpack!
First you'll need to configure Webpack in the extension repo. How to configure Webpack is out of scope for this task, but MobileFrontend and Popup webpack configurations will provide inspiration. The following snippet provides the basics:
glob = require( 'glob' ),
distDir = path.resolve( __dirname, 'resources/dist' ),
.....
.....
output: {
// Specify the destination of all build products created inside `entry`
path: distDir,
// Store outputs per module in files named after the modules. For the JavaScript entry
// itself, append .js to each ResourceLoader module entry name.
filename: '[name].js',
libraryTarget: 'this'
},
entry: {
// If you want to output tests to Special:JavaScript/qunit you'll need to build an entry point
// that bundles all test files inside the tests/node-qunit folder.
'tests.yourextname': glob.sync( './tests/node-qunit/*/*.test.js' ),
// mobile.startup will be the name of your RL module. src/mobile.startup/my-rl-module.js defines the entry
// point for this file. In ResourceLoader this would be the last file you list in "scripts"
'my-rl-module': './src/mobile.startup/my-rl-module.js',
},
After setting up Webpack you'll need to define your ResourceLoader module to ship the file built by Webpack. In the example above, my-rl-module will be outputted to distDir (resources/dist/) with the name of the entry point passed through output.filename (my-rl-module.js).
Shipping this in MediaWiki becomes a case of shipping the dist file. Currently this dist file needs to be committed to the code repository (see phab:T199004).
"ResourceModules": {
"ext.yourextname": {
"scripts": [
"resources/dist/my-rl-module.js",
]
}
}
Note because we are shipping a minified dist file, we have added a vulnerability to our code. It is imperative to ensure during code review that the distributed bundle can be recompiled from package.json by checking the file contents of the committed file match with those built by Webpack. You can automate this with a script such as check_bundle.
Writing unit tests and getting code coverage reports
editNow you have require and module.exports writing unit tests and getting code coverage reports becomes much easier.
Updating your package.json will provide you 2 command line tools that can be run to run unit tests and get code coverage reports.
"scripts": {
"coverage": "nyc npm -s run test:unit",
"test:unit": "qunit 'tests/node-qunit/**/*.test.js'",
},
"devDependencies": {
"nyc": "13.0.1",
"qunit": "2.7.0"
}
Given your src files export modules and require modules, your tests can simply reference them like so without much modifications to any existing tests. The only problem remains providing the minimal environment (Sinon, QUnit and mediaWiki) for these tests to run. The Reading Web team are working on (and using!) https://github.com/wikimedia/mw-node-qunit to successfully provide this functionality.
// tests/node-qunit/foo.test.js
var foo = require( './../../src/foo' );
QUnit.module( 'foo.js', {
})
QUnit.test( 'foo', function ( assert ) {
})
See also: RFC: Let's stop using QUnit as a mechanism for integration tests for a proposal to make this the standard for all MediaWiki extensions.