This page documents a MediaWikidevelopment guideline, crafted over time by developer consensus (or sometimes by proclamation from a lead developer) |
This page describescoding conventions forJavaScript in theMediaWiki codebase.See also thegeneral conventions.
We useESLint as our code quality tool, with theeslint-config-wikimedia preset to encode most of our coding style and code quality rules.Refer toIntegrations on eslint.org for many text editors or IDEs with plugins to provide live feedback as you type.
To exclude files or directories from the analysis (e.g. third-party libraries), you canconfigure ignore patterns in ESLint, through an.eslintignore file.[1]Note thatnode_modules is excluded by default, so most repos would not need to set up any ignore rules.
Every repo needs an.eslintrc.json file in the root directory of the repository.The following shows an example ESLint config file:
{"root":true,"extends":["wikimedia/client","wikimedia/jquery","wikimedia/mediawiki"],"globals":{// Project-wide globals.},"rules":{// Project-wide rule variations. Keep to a minimum.}}
See.eslintrc.json in MediaWiki andVisualEditor for live examples.
Remember to set"root": true to avoid unintended "magic inheritance" of ESLint configurations in unrelated parent directories that you or the CI server might have set up on the disk (for example between a MediaWiki extension and MediaWiki core, or between your project directory and something in your home directory).
Theeslint-config-wikimedia preset provides several profiles that projects can choose from as they see fit, such as different language flavours, or run-time environments globals.For example:
wikimedia/client - for browser code in the current default ES environment (currently ES2016, so an alias ofwikimedia/client/es2016)wikimedia/client/es5 - for browser code that specifically expects ES5 supportwikimedia/client/es6 - for browser code that specifically expects ES6 supportwikimedia/server - for Node.js code with ES2018 supportwikimedia/qunit - a mixin for QUnit testsYou should expect to use multiple.eslintrc.json files in a repo to set different environmental expectations for different subdirectories.This makes sure that you don't accidentally usewindow methods in server code you expect to run on the server, or reference QUnit in production code..eslintrc.json files in subdirectories automatically inherit from the parent directory's configuration, so they only need to contain the things that are specific to that directory (example file).
Projects are encouraged to enforce ESLint passing by including it theirnpm test script via the "test" command inpackage.json.SeeContinuous integration/Entry points for more information about that.
If your project has a fairly heavy test pipeline, one could define annpm run lint or evennpm run lint:js script inpackage.json to make it easy to run just those from the command-line during local development (so that one doesn't have to open each file in an editor and/or to wait for CI).
To expose other command-line features from ESLint (such as linting individual files outside a text editor, or using the--fix feature), define "eslint" as its own script in package.json without any argument.You can then invoke it from the command-line as follows:
# Entire directory, recursive$npmruneslint--.$npmruneslint--resources/# Single file$npmruneslint--resources/ext.foo/bar.js# Fixer$npmruneslint----fixresources/ext.foo/
We use the following conventions:
(" (left parenthesis) must be separated by one space. This gives visual distinction between keywords and function invocations.delete,void,typeof,new,return, ..).These and other aspects of our style guide are enforced with ESLint.
| Correct | Wrong |
|---|---|
a.foo=bar+baz;if(foo){foo.bar=doBar();}functionfoo(){returnbar;}foo=function(){return'bar';};foo=typeofbar;functionbaz(foo,bar){return'gaz';}baz('banana','pear');foo=bar[0];foo=bar[baz];foo=[bar,baz]; | a.foo=bar+baz;if(foo){foo.bar=doBar();}functionfoo(){returnbar;};foo=function(){return('bar');};foo=typeof(bar);functionbaz(foo,bar){return'gaz';}baz('banana','pear');foo=bar[0];foo=bar[baz];foo=[bar,baz]; |
Lines should wrap at no more than 80–100 characters.If a statement does not fit on a single line, split the statement over multiple lines.The continuation of a statement should be indented one extra level.
Function calls and objects should either be on a single line or split over multiple lines with one line for each segment.Avoid closing a function call or object at a different indentation than its opening.
| Yes |
|---|
// One lineif(mw.foo.hasBar()&&mw.foo.getThis()==='that'){return{first:'Who',second:'What'};}else{mw.foo('first','second');}// Multi-line (one component per line)if(// Condition head indented one level.mw.foo.hasBar()&&mw.foo.getThis()==='that'&&!mw.foo.getThatFrom('this')){// ↖ Closing parenthesis at same level as opening.return{first:'Who',second:'What',third:'I don\'t know'};}else{mw.foo(['first','nested','value'],'second');} |
| No |
// No: Mixed one line and multi-lineif(mw.foo.hasBar()&&mw.foo.getThis()==='that'&&!mw.foo.getThatFrom('this')){// No: varying number of segments per line.return{first:'Who',second:'What',third:'I don\'t know'};}else{mw.foo('first','second','third');// No: Statements looking like they are split over multiple lines but still on line.// Visualy looks like a call with one parameter, or with an array as first parameter.mw.foo('first','second','third');mw.foo(['first','nested','value'],'second');} |
In a nutshell:
// Variables that have literal and cheap initial valuesconstbaz=42;constquux='apple';// Local functionsfunctionlocal(x){returnx*quux.length;}// Main statementsconstfoo=local(baz);constbar=baz+foo;
If a module bundle can't or otherwise isn't yet registered usingpackage files, then its individual JavaScript files should have a file-level closure around its code.[2]This gives the code its own scope, and avoids leakage of variables from or to other files, including in debug mode, and in a way that is understood by static analysis.This pattern is known as animmediately-invoked function expression (or "iffy").[3]
For package files this is not needed, as they are executed as a "module" file rather than a "script" file, which naturally have their own local scope.ESLint should also be configured accordingly (set"parserOptions": { "sourceType": "commonjs" }, like inthis example).
Variables must be declared before use.Each assignment must be on its own line.Variables may be declared near or at their first assignment.
constwaldo=42;constquux='apple';letfoo,bar;constflob=[waldo];if(isFound(waldo)){foo=1;bar=waldo*2;}else{foo=0;bar=0;}for(leti=0;i<flob.length;i++){// ...}
Functions should be declared before use.In the function body, function declarations should go after variable declarations and before any main statements.
Comments should be on their own line and go over the code they describe.
Within a comment, the opening syntax (e.g. slash-slash, or slash-star) should be separated from the text by a single space, and the text should start with a capital letter.If the comment is a valid sentence, then a full stop should be placed at the end of it.
Use line comments (// foo) within functions and other code blocks (including for multi-line comments).
Use block comments (/* foo */) only fordocumentation blocks.This helps maintain consistent formatting for inline comments (e.g. not some as blocks and some as multi-line comments, or having to convert from one to the other).It also avoids confusing documentation engines.It also makes it easy to disable parts of the code during development by simply moving the end-comment notation a few lines down, without being short-circuited by an inline block comment.
Be liberal with comments and don't fear file size because of it.All code is automatically minified byResourceLoader before being shipped.
/** * Get the user name. * * Elaborate in an extra paragraph after the first one-line summary in the imperative mood. * * @param {string} foo Description of a parameter that spans over on the next line of the comment. * @param {number} bar * @return {string} User name */
To document a class that usesES5 syntax, with the class and constructor defined together asfunction MyClass(…) {…}, use:
@classdesc tag to document the class@description tag to document the constructor/** * @classdesc Class description. * * @description Constructor description. * * @param {string} myParam * @return {string} Description */
To document a class that usesES6 syntax, with the constructor defined usingconstructor(), use separate comments to document the class and the constructor.
/** * Class description. */classmyClass{/** * Constructor description. * * @param {string} myParam * @return {string} Description */constructor(){...}}
UseJSDoc to build documentation (seedoc.wikimedia.org).To set up and publish JSDoc documentation, seeJSDoc.
=== and!==) instead of (loose) equality (== and!=). The latter does type coercion.typeof val === 'string'typeof val === 'number'typeof val === 'boolean'typeof val === 'function'val === Object( val )jQuery.isPlainObject( val )Array.isArray( val )obj.nodeType === Node.ELEMENT_NODEval === nullval === undefinedUse single quotes instead of double quotes for string literals.Remember there are no "magic quotes" in JavaScripti.e.\n and\t work everywhere.
To extract part of a string, use theslice() method for consistency.Avoid thesubstr(), orsubstring() methods which are redundant, easily mistaken, and may have unexpected side effects.[4][5][6]
Use globals exposed by the browser (such as document, location, navigator) directly and not as properties of the window object.This improves confidence in code through static analysis and may also power other IDE features.In addition to browser globals, the only globals that are safe to use aremw,$ andOO.
Avoid creating new global variables.Avoid modifying globals not "owned" by your code.For example, built-in globals such as String or Object must not be extended with additional utility methods, and similarly functionality relating to OOjs or jQuery should not be assigned to those globals.
To publicly expose functionality for re-use, usemodule.exports fromPackage files, and/or assign properties within themw hierarchy, e.g.mw.echo.Foo.
Note that configuration variables exposed by MediaWiki must be accessed viamw.config.
Modifying built-in protypes such asObject.prototype is considered harmful.This is not supported in MediaWiki code, and doing so will likely result in breakage of unrelated features.
All variables must be named usingCamelCase starting with a lowercase letter, or use all caps (with underscores for separation) if the variable represents some kind of constant value.
All functions must be named usingCamelCase, generally starting with a lowercase letter unless the function is a class constructor, in which case it must start with an uppercase letter.Method functions are preferred to start with verb, e.g.getFoo() instead offoo().
Names with acronyms in them should treat the acronym as a normal word and only uppercase the first letter as-needed.This applies to two-letter abbreviations as well, such asId.For example,getHtmlApiSource as opposed to "getHTMLAPISource".
Distinguish DOM nodes from jQuery objects by prefixing variables with a dollar sign if they will hold a jQuery object, e.g.$foo = $( '#bar' ).This helps reduce mistakes where conditions use incorrect conditional checks, such asif ( foo ) instead ofif ( $foo.length ).Where DOM methods often return null (which is falsey), jQuery methods return an empty collection object (which, like native arrays and other objects in JavaScript, are truthly).
When publishing a standalone project to npmjs.org, consider publishing it under the@wikimedia namespace.Note that some standalone projects, that are aimed at use outside the Wikimedia community and have a sufficiently unique name, currently use an unnamespaced package name (e.g. "oojs" and "visualeditor").T239742
To create a plain element, use the simple<tag> syntax in the jQuery constructor:
$hello=$('<div>').text('Hello');
When creating elements based on the tag name from a variable (which may contain arbitrary html):
// Fetch 'span' or 'div' etc.tag=randomTagName();$who=$(document.createElement(tag));
Only use$('<a title="valid html" href="#syntax">like this</a>'); when you need to parse HTML (as opposed to creating a plain element).
Different types of collections sometimes look similar but have different behaviour and should be treated as such.This confusion is mostly caused by the fact that arrays in JavaScript look a lot like arrays in other languages, but are in fact just an extension of Object.We use the following conventions:
| Intent | Arrays | Plain Objects | jQuery Objects |
|---|---|---|---|
| Declaration and empty initialisation | x=[]; | x={}; | $x=$([]); |
| Access value | x[0]; | x.key; | element = $x[ 0 ];or $y = $x.eq( 0 ); |
| Size | x.length; | Object.keys(x).length; | $x.length; |
| Iteration | for(i=0;i<x.length;i++){}or: x.forEach( ( value, i ) => {} ); | for(keyinx){} | $x.each( ( i, element ) => {} ); |
Avoid using afor-in loop to iterate over an array (as opposed to a plain object), becausefor-in will result in many unexpected behaviours, including: keys as strings, unstable iteration order, indexes may skip gaps, iteration may include other non-numerical properties.
Keys in localStorage and/or sessionStorage should be accessed viamw.storage ormw.storage.session.
Keys should start withmw and use camel case and/or hyphens.Do not use underscores or other separators.Examples of real keys:
mwuser-sessionIdmwedit-state-templatesUsedmwpreferences-prevTabBeware that contrary to cookies via mw.cookie, there isno wiki prefix or cookie prefix added by default.If values must vary by wiki, you must manually includewgCookiePrefix as part of the key.
Values must be strings. Beware that attempting to store other value types will silently cast to a string (e.g.false would become"false").
Space is limited.Use short and concise values over object structures where possible.A few example:
Number on the way out (avoidparseInt).Remember that Local storage does not have any eviction strategy by default.Therefore the following should be avoided:
For example, if a feature needs to store state about a variable entity (e.g. current page), it might make sense to use a single key for this feature as a whole and to limit the stored information only to the last few iterations (LRU).The slight increase in retrieval cost (the whole key, instead of separate smaller ones) is considered worth it, as otherwise the number of keys would grow uncontrollably.
Even if the keys are not dependent on user-input, you may still want to use a single key for your feature because otherwise past versions of your software will have stored data that you can't clean up.By using a single key you can roundtrip it in a way that naturally doesn't persist unknown sub properties.
Use of a tracking key is also an anti-pattern and does not avoid the above issues, as this would leak due to race conditions in HTML5 web storage being shared and non-atomic between multiple open browser tabs.
When feature's use of Local storage is to be removed, be sure to implement an eviction strategy first to clean up old values.Typicallymw.requestIdleCallback is used to gracefully look for the key and remove it.SeeT121646 for a more scalable approach.
Avoid storing personal information in Local storage as it remains when a user logs out or closes their browser.Use Session storage instead.SeeT179752.
Asynchronous code should follow, and be compatible with, the Promise standard.
When you define an asynchronous method for other code to call, you may internally construct the returnedthenable object either using$.Deferred or native Promise.
When you call an asynchronous method, use only standard Promise-compatible methods such asthen() andcatch().Avoid using jQuery-specific methods likedone() orfail(), which could stop working without warning if the method you are calling internally transitions from $.Deferred to native Promise.
Note that there are also subtle legacy behaviours in thedone andfail callbacks.Pay close attention when migrating existing code fromdone() tothen() as doing so may cause the code to stop working correctly.Specifically, thedone andfail callbacks invoke your callback synchronously if the Deferred was already settled by the time you attach your callback.This means your callback may be invoked before the attaching statement is finished running.
For example:
functiongetSqrt(num){return$.Deferred().resolve(Math.sqrt(num));}console.log("A");getSqrt(49).done((val)=>{console.log("C");// can be A C B, or, A B C});console.log("B");console.log("A");getSqrt(49).then((val)=>{console.log("C");// always A B C});console.log("B");consty=getSqrt(49).then((val)=>{console.log(y.state(),val);// "resolved", 7});constx=getSqrt(49).done((val)=>{console.log(x.state(),val);// Uncaught TypeError: x is undefined});constz=getSqrt(49);z.done((val)=>{console.log(x.state(),val);// "resolved", 7});
jQuery is still supported in MediaWiki and it is a useful tool, but it is slow and dated, and there are some functions and patterns to avoid.The following emit warnings or errors in eslint-config-wikimedia.
.addSelf,.bind,.unbind,.boxModel,.browser,.camelCase,.context,.delegate,.undelegate[7].each,.grep,.inArray,.map,.trim,.error,.extend,.noop.[8].animate,.stop,.finish,.fadeIn,.fadeOut,.fadeTo,.fadeToggle,.slideDown,.slideToggle,.slideUp[8]mw.hook( 'wikipage.content' ).add( ( $content ) => {, which provides a$content element. You can call.find() on it repeatedly. When you create elements, store them in a variable, then retrieve them later using the variable or$variable.find(). If you need to, you can make exceptions to this from time to time using comments such as// eslint-disable-next-line no-jquery/no-global-selector.$( '#mw-specialmute-form input:checkbox' ) and$input.is( ':visible' )..globalEval.$() function. For example, prefer$( '<a>' ).attr( 'href', 'https://test.com' ).text( 'Test' ) to$( '<a href="https://test.com">Test</a>' ). The latter is prone to errors that linters cannot detect, and triggers jQuery's HTML parser, which is slow.[11]Don't reinvent the wheel.Much JavaScript functionality and MediaWiki-related utilities ship with MediaWiki core that are stable and you can (literally) depend on them.SeeResourceLoader/Core modules before rolling your own code.
float: right ortext-align: left), especially when styling text containers. Putting those declarations in CSS file will allow them to be automatically flipped for RTL-languages byCSSJanus inResourceLoader.attr() andprop() appropriately.[foo="bar"] instead of[foo=bar] (jqbug 8229).$('<div>',{foo:'bar',click:()=>{},css:{..}});. Don't use this. It makes code harder to follow, fails on attributes (such as 'size') that are also methods, and is unstable due to this mixing of jQuery methods with element attributes. A future jQuery method or plugin called "title" might convert an element into a heading, which means the title attribute can also no longer be set through this method. Be explicit and call.attr(),.prop(),.on() etc. directly.Don't apply styling to lots of elements at once; this has poor performance.Instead use a common parent's class (or add one) and apply CSS in a.css or.less file.Thanks to ResourceLoader, this will all be loaded in the same HTTP request, so there's no performance penalty for having a separate CSS file.Do not set CSS into inline "style" attributes, don't insert "style" elements from JS either.