2 * Defines mw.loader, the infrastructure for loading ResourceLoader
5 * This file is appended directly to the code in startup/mediawiki.js
7 /* global $VARS, $CODE, mw */
13 hasOwn = Object.hasOwnProperty;
16 * Client for ResourceLoader server end point.
18 * This client is in charge of maintaining the module registry and state
19 * machine, initiating network (batch) requests for loading modules, as
20 * well as dependency resolution and execution of source code.
22 * @see <https://www.mediawiki.org/wiki/ResourceLoader/Features>
23 * @namespace mw.loader
27 * FNV132 hash function
29 * This function implements the 32-bit version of FNV-1.
30 * It is equivalent to hash( 'fnv132', ... ) in PHP, except
31 * its output is base 36 rather than hex.
32 * See <https://en.wikipedia.org/wiki/Fowler–Noll–Vo_hash_function>
35 * @param {string} str String to hash
36 * @return {string} hash as a five-character base 36 string
38 function fnv132( str ) {
39 var hash = 0x811C9DC5;
41 /* eslint-disable no-bitwise */
42 for ( var i = 0; i < str.length; i++ ) {
43 hash += ( hash << 1 ) + ( hash << 4 ) + ( hash << 7 ) + ( hash << 8 ) + ( hash << 24 );
44 hash ^= str.charCodeAt( i );
47 hash = ( hash >>> 0 ).toString( 36 ).slice( 0, 5 );
48 /* eslint-enable no-bitwise */
50 while ( hash.length < 5 ) {
57 * Fired via mw.track on various resource loading errors.
59 * eslint-disable jsdoc/valid-types
61 * @event ~'resourceloader.exception'
63 * @param {Error|Mixed} e The error that was thrown. Almost always an Error
64 * object, but in theory module code could manually throw something else, and that
65 * might also end up here.
66 * @param {string} [module] Name of the module which caused the error. Omitted if the
67 * error is not module-related or the module cannot be easily identified due to
69 * @param {string} source Source of the error. Possible values:
71 * - load-callback: exception thrown by user callback
72 * - module-execute: exception thrown by module code
73 * - resolve: failed to sort dependencies for a module in mw.loader.load
74 * - store-eval: could not evaluate module code cached in localStorage
75 * - store-localstorage-json: JSON conversion error in mw.loader.store
76 * - store-localstorage-update: localStorage conversion error in mw.loader.store.
80 * Mapping of registered modules.
82 * See #implement and #execute for exact details on support for script, style and messages.
87 * // From mw.loader.register()
88 * 'version': '#####' (five-character hash)
89 * 'dependencies': ['required.foo', 'bar.also', ...]
90 * 'group': string, integer, (or) null
91 * 'source': 'local', (or) 'anotherwiki'
92 * 'skip': 'return !!window.Example;', (or) null, (or) boolean result of skip
93 * 'module': export Object
95 * // Set by execute() or mw.loader.state()
96 * // See mw.loader.getState() for documentation of the state machine
97 * 'state': 'registered', 'loading', 'loaded', 'executing', 'ready', 'error', or 'missing'
99 * // Optionally added at run-time by mw.loader.impl()
100 * 'script': closure, array of urls, or string
101 * 'style': { ... } (see #execute)
102 * 'messages': { 'key': 'value', ... }
109 var registry = Object.create( null ),
110 // Mapping of sources, keyed by source-id, values are strings.
115 // 'sourceId': 'http://example.org/w/load.php'
118 sources = Object.create( null ),
120 // For queueModuleScript()
121 handlingPendingRequests = false,
122 pendingRequests = [],
124 // List of modules to be loaded
128 * List of callback jobs waiting for modules to be ready.
130 * Jobs are created by #enqueue() and run by #doPropagation().
131 * Typically when a job is created for a module, the job's dependencies contain
132 * both the required module and all its recursive dependencies.
134 * @example // Format:
136 * 'dependencies': [ module names ],
137 * 'ready': Function callback
138 * 'error': Function callback
141 * @property {Object[]} jobs
146 // For #setAndPropagate() and #doPropagation()
147 willPropagate = false,
152 * @property {Array} baseModules
154 baseModules = $VARS.baseModules,
157 * For #addEmbeddedCSS() and #addLink()
160 * @property {HTMLElement|null} marker
162 marker = document.querySelector( 'meta[name="ResourceLoaderDynamicStyles"]' ),
164 // For #addEmbeddedCSS()
168 * Append an HTML element to `document.head` or before a specified node.
171 * @param {HTMLElement} el
172 * @param {Node|null} [nextNode]
174 function addToHead( el, nextNode ) {
175 if ( nextNode && nextNode.parentNode ) {
176 nextNode.parentNode.insertBefore( el, nextNode );
178 document.head.appendChild( el );
183 * Create a new style element and add it to the DOM.
184 * Stable for use in gadgets.
186 * @method mw.loader.addStyleTag
187 * @param {string} text CSS text
188 * @param {Node|null} [nextNode] The element where the style tag
189 * should be inserted before
190 * @return {HTMLStyleElement} Reference to the created style element
192 function newStyleTag( text, nextNode ) {
193 var el = document.createElement( 'style' );
194 el.appendChild( document.createTextNode( text ) );
195 addToHead( el, nextNode );
201 * @param {Object} cssBuffer
203 function flushCssBuffer( cssBuffer ) {
204 // Make sure the next call to addEmbeddedCSS() starts a new buffer.
205 // This must be done before we run the callbacks, as those may end up
206 // queueing new chunks which would be lost otherwise (T105973).
208 // There can be more than one buffer in-flight (given "@import", and
209 // generally due to race conditions). Only tell addEmbeddedCSS() to
210 // start a new buffer if we're currently flushing the last one that it
211 // started. If we're flushing an older buffer, keep the last one open.
212 if ( cssBuffer === lastCssBuffer ) {
213 lastCssBuffer = null;
215 newStyleTag( cssBuffer.cssText, marker );
216 for ( var i = 0; i < cssBuffer.callbacks.length; i++ ) {
217 cssBuffer.callbacks[ i ]();
222 * Add a bit of CSS text to the current browser page.
224 * The creation and insertion of the `<style>` element is debounced for two reasons:
226 * - Performing the insertion before the next paint round via requestAnimationFrame
227 * avoids forced or wasted style recomputations, which are expensive in browsers.
228 * - Reduce how often new stylesheets are inserted by letting additional calls to this
229 * function accumulate into a buffer for at least one JavaScript tick. Modules are
230 * received from the server in batches, which means there is likely going to be many
231 * calls to this function in a row within the same tick / the same call stack.
235 * @param {string} cssText CSS text to be added in a `<style>` tag.
236 * @param {Function} callback Called after the insertion has occurred.
238 function addEmbeddedCSS( cssText, callback ) {
239 // Start a new buffer if one of the following is true:
240 // - We've never started a buffer before, this will be our first.
241 // - The last buffer we created was flushed meanwhile, so start a new one.
242 // - The next CSS chunk syntactically needs to be at the start of a stylesheet (T37562).
243 if ( !lastCssBuffer || cssText.startsWith( '@import' ) ) {
248 requestAnimationFrame( flushCssBuffer.bind( null, lastCssBuffer ) );
251 // Linebreak for somewhat distinguishable sections
252 lastCssBuffer.cssText += '\n' + cssText;
253 lastCssBuffer.callbacks.push( callback );
257 * See also `ResourceLoader.php#makeVersionQuery` on the server.
260 * @param {string[]} modules List of module names
261 * @return {string} Hash of concatenated version hashes.
263 function getCombinedVersion( modules ) {
264 var hashes = modules.reduce( function ( result, module ) {
265 return result + registry[ module ].version;
267 return fnv132( hashes );
271 * Determine whether all dependencies are in state 'ready', which means we may
272 * execute the module or job now.
275 * @param {string[]} modules Names of modules to be checked
276 * @return {boolean} True if all modules are in state 'ready', false otherwise
278 function allReady( modules ) {
279 for ( var i = 0; i < modules.length; i++ ) {
280 if ( mw.loader.getState( modules[ i ] ) !== 'ready' ) {
288 * Determine whether all direct and base dependencies are in state 'ready'
291 * @param {string} module Name of the module to be checked
292 * @return {boolean} True if all direct/base dependencies are in state 'ready'; false otherwise
294 function allWithImplicitReady( module ) {
295 return allReady( registry[ module ].dependencies ) &&
296 ( baseModules.indexOf( module ) !== -1 || allReady( baseModules ) );
300 * Determine whether all dependencies are in state 'ready', which means we may
301 * execute the module or job now.
304 * @param {string[]} modules Names of modules to be checked
305 * @return {boolean|string} False if no modules are in state 'error' or 'missing';
306 * failed module otherwise
308 function anyFailed( modules ) {
309 for ( var i = 0; i < modules.length; i++ ) {
310 var state = mw.loader.getState( modules[ i ] );
311 if ( state === 'error' || state === 'missing' ) {
319 * Handle propagation of module state changes and reactions to them.
321 * - When a module reaches a failure state, this should be propagated to
322 * modules that depend on the failed module.
323 * - When a module reaches a final state, pending job callbacks for the
324 * module from mw.loader.using() should be called.
325 * - When a module reaches the 'ready' state from #execute(), consider
326 * executing dependent modules now having their dependencies satisfied.
327 * - When a module reaches the 'loaded' state from mw.loader.impl,
328 * consider executing it, if it has no unsatisfied dependencies.
332 function doPropagation() {
333 var didPropagate = true;
336 // Keep going until the last iteration performed no actions.
337 while ( didPropagate ) {
338 didPropagate = false;
340 // Stage 1: Propagate failures
341 while ( errorModules.length ) {
342 var errorModule = errorModules.shift(),
343 baseModuleError = baseModules.indexOf( errorModule ) !== -1;
344 for ( module in registry ) {
345 if ( registry[ module ].state !== 'error' && registry[ module ].state !== 'missing' ) {
346 if ( baseModuleError && baseModules.indexOf( module ) === -1 ) {
347 // Propate error from base module to all regular (non-base) modules
348 registry[ module ].state = 'error';
350 } else if ( registry[ module ].dependencies.indexOf( errorModule ) !== -1 ) {
351 // Propagate error from dependency to depending module
352 registry[ module ].state = 'error';
353 // .. and propagate it further
354 errorModules.push( module );
361 // Stage 2: Execute 'loaded' modules with no unsatisfied dependencies
362 for ( module in registry ) {
363 if ( registry[ module ].state === 'loaded' && allWithImplicitReady( module ) ) {
364 // Recursively execute all dependent modules that were already loaded
365 // (waiting for execution) and no longer have unsatisfied dependencies.
366 // Base modules may have dependencies amongst eachother to ensure correct
367 // execution order. Regular modules wait for all base modules.
373 // Stage 3: Invoke job callbacks that are no longer blocked
374 for ( var i = 0; i < jobs.length; i++ ) {
376 var failed = anyFailed( job.dependencies );
377 if ( failed !== false || allReady( job.dependencies ) ) {
381 if ( failed !== false && job.error ) {
382 job.error( new Error( 'Failed dependency: ' + failed ), job.dependencies );
383 } else if ( failed === false && job.ready ) {
387 // A user-defined callback raised an exception.
388 // Swallow it to protect our state machine!
391 source: 'load-callback'
399 willPropagate = false;
403 * Update a module's state in the registry and make sure any necessary
404 * propagation will occur, by adding a (debounced) call to doPropagation().
405 * See #doPropagation for more about propagation.
406 * See #registry for more about how states are used.
409 * @param {string} module
410 * @param {string} state
412 function setAndPropagate( module, state ) {
413 registry[ module ].state = state;
414 if ( state === 'ready' ) {
415 // Queue to later be synced to the local module store.
417 } else if ( state === 'error' || state === 'missing' ) {
418 errorModules.push( module );
419 } else if ( state !== 'loaded' ) {
420 // We only have something to do in doPropagation for the
421 // 'loaded', 'ready', 'error', and 'missing' states.
422 // Avoid scheduling and propagation cost for frequent and short-lived
423 // transition states, such as 'loading' and 'executing'.
426 if ( willPropagate ) {
427 // Already scheduled, or, we're already in a doPropagation stack.
430 willPropagate = true;
431 // Yield for two reasons:
432 // * Allow successive calls to mw.loader.impl() from the same
433 // load.php response, or from the same asyncEval() to be in the
434 // propagation batch.
435 // * Allow the browser to breathe between the reception of
436 // module source code and the execution of it.
438 // Use a high priority because the user may be waiting for interactions
439 // to start being possible. But, first provide a moment (up to 'timeout')
440 // for native input event handling (e.g. scrolling/typing/clicking).
441 mw.requestIdleCallback( doPropagation, { timeout: 1 } );
445 * Resolve dependencies and detect circular references.
448 * @param {string} module Name of the top-level module whose dependencies shall be
449 * resolved and sorted.
450 * @param {Array} resolved Returns a topological sort of the given module and its
451 * dependencies, such that later modules depend on earlier modules. The array
452 * contains the module names. If the array contains already some module names,
453 * this function appends its result to the pre-existing array.
454 * @param {Set} [unresolved] Used to detect loops in the dependency graph.
455 * @throws {Error} If an unknown module or a circular dependency is encountered
457 function sortDependencies( module, resolved, unresolved ) {
458 if ( !( module in registry ) ) {
459 throw new Error( 'Unknown module: ' + module );
462 if ( typeof registry[ module ].skip === 'string' ) {
463 // eslint-disable-next-line no-new-func
464 var skip = ( new Function( registry[ module ].skip )() );
465 registry[ module ].skip = !!skip;
467 registry[ module ].dependencies = [];
468 setAndPropagate( module, 'ready' );
473 // Create unresolved if not passed in
475 unresolved = new Set();
478 // Track down dependencies
479 var deps = registry[ module ].dependencies;
480 unresolved.add( module );
481 for ( var i = 0; i < deps.length; i++ ) {
482 if ( resolved.indexOf( deps[ i ] ) === -1 ) {
483 if ( unresolved.has( deps[ i ] ) ) {
485 'Circular reference detected: ' + module + ' -> ' + deps[ i ]
489 sortDependencies( deps[ i ], resolved, unresolved );
493 resolved.push( module );
497 * Get names of module that a module depends on, in their proper dependency order.
500 * @param {string[]} modules Array of string module names
501 * @return {Array} List of dependencies, including 'module'.
502 * @throws {Error} If an unregistered module or a dependency loop is encountered
504 function resolve( modules ) {
505 // Always load base modules
506 var resolved = baseModules.slice();
507 for ( var i = 0; i < modules.length; i++ ) {
508 sortDependencies( modules[ i ], resolved );
514 * Like #resolve(), except it will silently ignore modules that
515 * are missing or have missing dependencies.
518 * @param {string[]} modules Array of string module names
519 * @return {Array} List of dependencies.
521 function resolveStubbornly( modules ) {
522 // Always load base modules
523 var resolved = baseModules.slice();
524 for ( var i = 0; i < modules.length; i++ ) {
525 var saved = resolved.slice();
527 sortDependencies( modules[ i ], resolved );
530 // This module is not currently known, or has invalid dependencies.
532 // Most likely due to a cached reference after the module was
533 // removed, otherwise made redundant, or omitted from the registry
534 // by the ResourceLoader "target" system.
536 // These errors can be common, e.g. queuing an unavailable module
537 // unconditionally from the server-side is OK and should fail gracefully.
538 mw.log.warn( 'Skipped unavailable module ' + modules[ i ] );
540 // Do not track this error as an exception when the module:
541 // - Is valid, but gracefully filtered out by target system.
542 // - Was recently valid, but is still referenced in stale cache.
544 // Basically the only reason to track this as exception is when the error
545 // was circular or invalid dependencies. What the above scenarios have in
546 // common is that they don't register the module client-side.
547 if ( modules[ i ] in registry ) {
559 * Resolve a relative file path.
561 * For example, resolveRelativePath( '../foo.js', 'resources/src/bar/bar.js' )
562 * returns 'resources/src/foo.js'.
565 * @param {string} relativePath Relative file path, starting with ./ or ../
566 * @param {string} basePath Path of the file (not directory) relativePath is relative to
567 * @return {string|null} Resolved path, or null if relativePath does not start with ./ or ../
569 function resolveRelativePath( relativePath, basePath ) {
571 var relParts = relativePath.match( /^((?:\.\.?\/)+)(.*)$/ );
576 var baseDirParts = basePath.split( '/' );
577 // basePath looks like 'foo/bar/baz.js', so baseDirParts looks like [ 'foo', 'bar, 'baz.js' ]
578 // Remove the file component at the end, so that we are left with only the directory path
581 var prefixes = relParts[ 1 ].split( '/' );
582 // relParts[ 1 ] looks like '../../', so prefixes looks like [ '..', '..', '' ]
583 // Remove the empty element at the end
586 // For every ../ in the path prefix, remove one directory level from baseDirParts
588 var reachedRoot = false;
589 while ( ( prefix = prefixes.pop() ) !== undefined ) {
590 if ( prefix === '..' ) {
591 // Once we reach the package's base dir, preserve all remaining "..".
592 reachedRoot = !baseDirParts.length || reachedRoot;
593 if ( !reachedRoot ) {
596 baseDirParts.push( prefix );
601 // If there's anything left of the base path, prepend it to the file path
602 return ( baseDirParts.length ? baseDirParts.join( '/' ) + '/' : '' ) + relParts[ 2 ];
606 * Make a require() function scoped to a package file
609 * @param {Object} moduleObj Module object from the registry
610 * @param {string} basePath Path of the file this is scoped to. Used for relative paths.
613 function makeRequireFunction( moduleObj, basePath ) {
614 return function require( moduleName ) {
615 var fileName = resolveRelativePath( moduleName, basePath );
616 if ( fileName === null ) {
617 // Not a relative path, so it's either a module name or,
618 // (if in test mode) a private file imported from another module.
619 return mw.loader.require( moduleName );
622 if ( hasOwn.call( moduleObj.packageExports, fileName ) ) {
623 // File has already been executed, return the cached result
624 return moduleObj.packageExports[ fileName ];
627 var scriptFiles = moduleObj.script.files;
628 if ( !hasOwn.call( scriptFiles, fileName ) ) {
629 throw new Error( 'Cannot require undefined file ' + fileName );
633 fileContent = scriptFiles[ fileName ];
634 if ( typeof fileContent === 'function' ) {
635 var moduleParam = { exports: {} };
636 fileContent( makeRequireFunction( moduleObj, fileName ), moduleParam, moduleParam.exports );
637 result = moduleParam.exports;
639 // fileContent is raw data (such as a JSON object), just pass it through
640 result = fileContent;
642 moduleObj.packageExports[ fileName ] = result;
648 * Load and execute a script.
651 * @param {string} src URL to script, will be used as the src attribute in the script tag
652 * @param {Function} [callback] Callback to run after request resolution
653 * @param {string[]} [modules] List of modules being requested, for state to be marked as error
654 * in case the script fails to load
655 * @return {HTMLElement}
657 function addScript( src, callback, modules ) {
658 // Use a <script> element rather than XHR. Using XHR changes the request
659 // headers (potentially missing a cache hit), and reduces caching in general
660 // since browsers cache XHR much less (if at all). And XHR means we retrieve
661 // text, so we'd need to eval, which then messes up line numbers.
662 // The drawback is that <script> does not offer progress events, feedback is
663 // only given after downloading, parsing, and execution have completed.
664 var script = document.createElement( 'script' );
666 function onComplete() {
667 if ( script.parentNode ) {
668 script.parentNode.removeChild( script );
675 script.onload = onComplete;
676 script.onerror = function () {
679 for ( var i = 0; i < modules.length; i++ ) {
680 setAndPropagate( modules[ i ], 'error' );
684 document.head.appendChild( script );
689 * Queue the loading and execution of a script for a particular module.
691 * This does for legacy debug mode what runScript() does for production.
694 * @param {string} src URL of the script
695 * @param {string} moduleName Name of currently executing module
696 * @param {Function} callback Callback to run after addScript() resolution
698 function queueModuleScript( src, moduleName, callback ) {
699 pendingRequests.push( function () {
700 // Keep in sync with execute()/runScript().
701 if ( moduleName !== 'jquery' ) {
702 window.require = mw.loader.require;
703 window.module = registry[ moduleName ].module;
705 addScript( src, function () {
706 // 'module.exports' should not persist after the file is executed to
707 // avoid leakage to unrelated code. 'require' should be kept, however,
708 // as asynchronous access to 'require' is allowed and expected. (T144879)
709 delete window.module;
711 // Start the next one (if any)
712 if ( pendingRequests[ 0 ] ) {
713 pendingRequests.shift()();
715 handlingPendingRequests = false;
719 if ( !handlingPendingRequests && pendingRequests[ 0 ] ) {
720 handlingPendingRequests = true;
721 pendingRequests.shift()();
726 * Utility function for execute()
729 * @param {string} url URL
730 * @param {string} [media] Media attribute
731 * @param {Node|null} [nextNode]
732 * @return {HTMLElement}
734 function addLink( url, media, nextNode ) {
735 var el = document.createElement( 'link' );
737 el.rel = 'stylesheet';
741 // If you end up here from an IE exception "SCRIPT: Invalid property value.",
742 // see #addEmbeddedCSS, T33676, T43331, and T49277 for details.
745 addToHead( el, nextNode );
750 * Evaluate in the global scope.
752 * This is used by MediaWiki user scripts, where it is (for example)
753 * important that `var` makes a global variable.
756 * @param {string} code JavaScript code
758 function globalEval( code ) {
759 var script = document.createElement( 'script' );
761 document.head.appendChild( script );
762 script.parentNode.removeChild( script );
766 * Evaluate JS code using indirect eval().
768 * This is used by mw.loader.store. It is important that we protect the
769 * integrity of mw.loader's private variables (from accidental clashes
770 * or re-assignment), which means we can't use regular `eval()`.
772 * Optimization: This exists separately from globalEval(), because that
773 * involves slow DOM overhead.
776 * @param {string} code JavaScript code
778 function indirectEval( code ) {
779 // See http://perfectionkills.com/global-eval-what-are-the-options/
780 // for an explanation of this syntax.
781 // eslint-disable-next-line no-eval
786 * Add one or more modules to the module load queue.
791 * @param {string[]} dependencies Array of module names in the registry
792 * @param {Function} [ready] Callback to execute when all dependencies are ready
793 * @param {Function} [error] Callback to execute when any dependency fails
795 function enqueue( dependencies, ready, error ) {
796 if ( allReady( dependencies ) ) {
797 // Run ready immediately
804 var failed = anyFailed( dependencies );
805 if ( failed !== false ) {
807 // Execute error immediately if any dependencies have errors
809 new Error( 'Dependency ' + failed + ' failed to load' ),
816 // Not all dependencies are ready, add to the load queue...
818 // Add ready and error callbacks if they were given
819 if ( ready || error ) {
821 // Narrow down the list to modules that are worth waiting for
822 dependencies: dependencies.filter( function ( module ) {
823 var state = registry[ module ].state;
824 return state === 'registered' || state === 'loaded' || state === 'loading' || state === 'executing';
831 dependencies.forEach( function ( module ) {
832 // Only queue modules that are still in the initial 'registered' state
833 // (e.g. not ones already loading or loaded etc.).
834 if ( registry[ module ].state === 'registered' && queue.indexOf( module ) === -1 ) {
835 queue.push( module );
843 * Executes a loaded module, making it ready to use
846 * @param {string} module Module name to execute
848 function execute( module ) {
849 if ( registry[ module ].state !== 'loaded' ) {
850 throw new Error( 'Module in state "' + registry[ module ].state + '" may not execute: ' + module );
853 registry[ module ].state = 'executing';
854 $CODE.profileExecuteStart();
856 var runScript = function () {
857 $CODE.profileScriptStart();
858 var script = registry[ module ].script;
859 var markModuleReady = function () {
860 $CODE.profileScriptEnd();
861 setAndPropagate( module, 'ready' );
863 var nestedAddScript = function ( arr, offset ) {
864 // Recursively call queueModuleScript() in its own callback
865 // for each element of arr.
866 if ( offset >= arr.length ) {
867 // We're at the end of the array
872 queueModuleScript( arr[ offset ], module, function () {
873 nestedAddScript( arr, offset + 1 );
878 if ( Array.isArray( script ) ) {
879 nestedAddScript( script, 0 );
880 } else if ( typeof script === 'function' ) {
881 // Keep in sync with queueModuleScript() for debug mode
882 if ( module === 'jquery' ) {
883 // This is a special case for when 'jquery' itself is being loaded.
884 // - The standard jquery.js distribution does not set `window.jQuery`
885 // in CommonJS-compatible environments (Node.js, AMD, RequireJS, etc.).
886 // - MediaWiki's 'jquery' module also bundles jquery.migrate.js, which
887 // in a CommonJS-compatible environment, will use require('jquery'),
888 // but that can't work when we're still inside that module.
891 // Pass jQuery twice so that the signature of the closure which wraps
892 // the script can bind both '$' and 'jQuery'.
893 script( window.$, window.$, mw.loader.require, registry[ module ].module );
896 } else if ( typeof script === 'object' && script !== null ) {
897 var mainScript = script.files[ script.main ];
898 if ( typeof mainScript !== 'function' ) {
899 throw new Error( 'Main file in module ' + module + ' must be a function' );
901 // jQuery parameters are not passed for multi-file modules
903 makeRequireFunction( registry[ module ], script.main ),
904 registry[ module ].module,
905 registry[ module ].module.exports
908 } else if ( typeof script === 'string' ) {
909 // Site and user modules are legacy scripts that run in the global scope.
910 // This is transported as a string instead of a function to avoid needing
911 // to use string manipulation to undo the function wrapper.
912 globalEval( script );
916 // Module without script
920 // Use mw.trackError instead of mw.log because these errors are common in production mode
921 // (e.g. undefined variable), and mw.log is only enabled in debug mode.
922 setAndPropagate( module, 'error' );
923 $CODE.profileScriptEnd();
927 source: 'module-execute'
932 // Emit deprecation warnings
933 if ( registry[ module ].deprecationWarning ) {
934 mw.log.warn( registry[ module ].deprecationWarning );
937 // Add localizations to message system
938 if ( registry[ module ].messages ) {
939 mw.messages.set( registry[ module ].messages );
942 // Initialise templates
943 if ( registry[ module ].templates ) {
944 mw.templates.set( module, registry[ module ].templates );
947 // Adding of stylesheets is asynchronous via addEmbeddedCSS().
948 // The below function uses a counting semaphore to make sure we don't call
949 // runScript() until after this module's stylesheets have been inserted
952 var cssHandle = function () {
953 // Increase semaphore, when creating a callback for addEmbeddedCSS.
956 // Decrease semaphore, when said callback is invoked.
958 if ( cssPending === 0 ) {
960 // This callback is exposed to addEmbeddedCSS, which is outside the execute()
961 // function and is not concerned with state-machine integrity. In turn,
962 // addEmbeddedCSS() actually exposes stuff further via requestAnimationFrame.
963 // If increment and decrement callbacks happen in the wrong order, or start
964 // again afterwards, then this branch could be reached multiple times.
965 // To protect the integrity of the state-machine, prevent that from happening
966 // by making runScript() cannot be called more than once. We store a private
967 // reference when we first reach this branch, then deference the original, and
968 // call our reference to it.
969 var runScriptCopy = runScript;
970 runScript = undefined;
976 // Process styles (see also mw.loader.impl)
977 // * { "css": [css, ..] }
978 // * { "url": { <media>: [url, ..] } }
979 var style = registry[ module ].style;
981 // Array of CSS strings under key 'css'
982 // { "css": [css, ..] }
983 if ( 'css' in style ) {
984 for ( var i = 0; i < style.css.length; i++ ) {
985 addEmbeddedCSS( style.css[ i ], cssHandle() );
989 // Plain object with array of urls under a media-type key
990 // { "url": { <media>: [url, ..] } }
991 if ( 'url' in style ) {
992 for ( var media in style.url ) {
993 var urls = style.url[ media ];
994 for ( var j = 0; j < urls.length; j++ ) {
995 addLink( urls[ j ], media, marker );
1001 // End profiling of execute()-self before we call runScript(),
1002 // which we want to measure separately without overlap.
1003 $CODE.profileExecuteEnd();
1005 if ( module === 'user' ) {
1006 // Implicit dependency on the site module. Not a real dependency because it should
1007 // run after 'site' regardless of whether it succeeds or fails.
1008 // Note: This is a simplified version of mw.loader.using(), inlined here because
1009 // mw.loader.using() is part of mediawiki.base (depends on jQuery; T192623).
1013 siteDeps = resolve( [ 'site' ] );
1018 if ( !siteDepErr ) {
1019 enqueue( siteDeps, runScript, runScript );
1021 } else if ( cssPending === 0 ) {
1022 // Regular module without styles
1025 // else: runScript will get called via cssHandle()
1028 function sortQuery( o ) {
1032 for ( var key in o ) {
1036 for ( var i = 0; i < list.length; i++ ) {
1037 sorted[ list[ i ] ] = o[ list[ i ] ];
1043 * Converts a module map of the form `{ foo: [ 'bar', 'baz' ], bar: [ 'baz, 'quux' ] }`
1044 * to a query string of the form `foo.bar,baz|bar.baz,quux`.
1046 * See `ResourceLoader::makePackedModulesString()` in PHP, of which this is a port.
1047 * On the server, unpacking is done by `ResourceLoader::expandModuleNames()`.
1049 * Note: This is only half of the logic, the other half has to be in #batchRequest(),
1050 * because its implementation needs to keep track of potential string size in order
1051 * to decide when to split the requests due to url size.
1053 * @typedef {Object} ModuleString
1054 * @property {string} str Module query string
1055 * @property {Array} list List of module names in matching order
1058 * @param {Object} moduleMap Module map
1059 * @return {ModuleString}
1061 function buildModulesString( moduleMap ) {
1066 function restore( suffix ) {
1070 for ( var prefix in moduleMap ) {
1071 p = prefix === '' ? '' : prefix + '.';
1072 str.push( p + moduleMap[ prefix ].join( ',' ) );
1073 list.push.apply( list, moduleMap[ prefix ].map( restore ) );
1076 str: str.join( '|' ),
1083 * @param {Object} params Map of parameter names to values
1086 function makeQueryString( params ) {
1087 // Optimisation: This is a fairly hot code path with batchRequest() loops.
1088 // Avoid overhead from Object.keys and Array.forEach.
1089 // String concatenation is faster than array pushing and joining, see
1090 // https://phabricator.wikimedia.org/P19931
1092 for ( var key in params ) {
1093 // Parameters are separated by &, added before all parameters other than
1095 str += ( str ? '&' : '' ) + encodeURIComponent( key ) + '=' +
1096 encodeURIComponent( params[ key ] );
1102 * Create network requests for a batch of modules.
1104 * This is an internal method for #work(). This must not be called directly
1105 * unless the modules are already registered, and no request is in progress,
1106 * and the module state has already been set to `loading`.
1109 * @param {string[]} batch
1111 function batchRequest( batch ) {
1112 if ( !batch.length ) {
1116 var sourceLoadScript, currReqBase, moduleMap;
1119 * Start the currently drafted request to the server.
1123 function doRequest() {
1124 // Optimisation: Inherit (Object.create), not copy ($.extend)
1125 var query = Object.create( currReqBase ),
1126 packed = buildModulesString( moduleMap );
1127 query.modules = packed.str;
1128 // The packing logic can change the effective order, even if the input was
1129 // sorted. As such, the call to getCombinedVersion() must use this
1130 // effective order to ensure that the combined version will match the hash
1131 // expected by the server based on combining versions from the module
1132 // query string in-order. (T188076)
1133 query.version = getCombinedVersion( packed.list );
1134 query = sortQuery( query );
1135 addScript( sourceLoadScript + '?' + makeQueryString( query ), null, packed.list );
1138 // Always order modules alphabetically to help reduce cache
1139 // misses for otherwise identical content.
1142 // Query parameters common to all requests
1143 var reqBase = $VARS.reqBase;
1145 // Split module list by source and by group.
1146 var splits = Object.create( null );
1147 for ( var b = 0; b < batch.length; b++ ) {
1148 var bSource = registry[ batch[ b ] ].source;
1149 var bGroup = registry[ batch[ b ] ].group;
1150 if ( !splits[ bSource ] ) {
1151 splits[ bSource ] = Object.create( null );
1153 if ( !splits[ bSource ][ bGroup ] ) {
1154 splits[ bSource ][ bGroup ] = [];
1156 splits[ bSource ][ bGroup ].push( batch[ b ] );
1159 for ( var source in splits ) {
1160 sourceLoadScript = sources[ source ];
1162 for ( var group in splits[ source ] ) {
1164 // Cache access to currently selected list of
1165 // modules for this group from this source.
1166 var modules = splits[ source ][ group ];
1168 // Query parameters common to requests for this module group
1169 // Optimisation: Inherit (Object.create), not copy ($.extend)
1170 currReqBase = Object.create( reqBase );
1171 // User modules require a user name in the query string.
1172 if ( group === $VARS.groupUser && mw.config.get( 'wgUserName' ) !== null ) {
1173 currReqBase.user = mw.config.get( 'wgUserName' );
1176 // In addition to currReqBase, doRequest() will also add 'modules' and 'version'.
1177 // > '&modules='.length === 9
1178 // > '&version=12345'.length === 14
1180 var currReqBaseLength = makeQueryString( currReqBase ).length + 23;
1182 // We may need to split up the request to honor the query string length limit,
1183 // so build it piece by piece. `length` does not include the characters from
1184 // the request base, see below
1186 moduleMap = Object.create( null ); // { prefix: [ suffixes ] }
1188 for ( var i = 0; i < modules.length; i++ ) {
1189 // Determine how many bytes this module would add to the query string
1190 var lastDotIndex = modules[ i ].lastIndexOf( '.' ),
1191 prefix = modules[ i ].slice( 0, Math.max( 0, lastDotIndex ) ),
1192 suffix = modules[ i ].slice( lastDotIndex + 1 ),
1193 bytesAdded = moduleMap[ prefix ] ?
1194 suffix.length + 3 : // '%2C'.length == 3
1195 modules[ i ].length + 3; // '%7C'.length == 3
1197 // If the url would become too long, create a new one, but don't create empty requests.
1198 // The value of `length` only reflects the request-specific bytes relating to the
1199 // accumulated entries in moduleMap so far. It does not include the base length,
1200 // which we account for separately with `currReqBaseLength` so that length is 0
1201 // when moduleMap is empty.
1202 if ( length && length + currReqBaseLength + bytesAdded > mw.loader.maxQueryLength ) {
1203 // Dispatch what we've got...
1205 // .. and start preparing a new request.
1207 moduleMap = Object.create( null );
1209 if ( !moduleMap[ prefix ] ) {
1210 moduleMap[ prefix ] = [];
1212 length += bytesAdded;
1213 moduleMap[ prefix ].push( suffix );
1215 // Optimization: Skip `length` check.
1216 // moduleMap will contain at least one module here. The loop above leaves the last module
1217 // undispatched (and maybe some before it), so for moduleMap to be empty here, there must
1218 // have been no modules to iterate in the current group to start with, but we only create
1219 // a group in `splits` when the first module in the group is seen, so there are always
1220 // modules in the group when this code is reached.
1228 * @param {string[]} implementations Array containing pieces of JavaScript code in the
1229 * form of calls to mw.loader#impl().
1230 * @param {Function} cb Callback in case of failure
1231 * @param {Error} cb.err
1232 * @param {number} [offset] Integer offset into implementations to start at
1234 function asyncEval( implementations, cb, offset ) {
1235 if ( !implementations.length ) {
1238 offset = offset || 0;
1239 mw.requestIdleCallback( function ( deadline ) {
1240 asyncEvalTask( deadline, implementations, cb, offset );
1245 * Idle callback for asyncEval
1248 * @param {IdleDeadline} deadline
1249 * @param {string[]} implementations
1250 * @param {Function} cb
1251 * @param {Error} cb.err
1252 * @param {number} offset
1254 function asyncEvalTask( deadline, implementations, cb, offset ) {
1255 for ( var i = offset; i < implementations.length; i++ ) {
1256 if ( deadline.timeRemaining() <= 0 ) {
1257 asyncEval( implementations, cb, i );
1261 indirectEval( implementations[ i ] );
1269 * Make a versioned key for a specific module.
1272 * @param {string} module Module name
1273 * @return {string|null} Module key in format '`[name]@[version]`',
1274 * or null if the module does not exist
1276 function getModuleKey( module ) {
1277 return module in registry ? ( module + '@' + registry[ module ].version ) : null;
1282 * @param {string} key Module name or '`[name]@[version]`'
1285 function splitModuleKey( key ) {
1286 // Module names may contain '@' but version strings may not, so the last '@' is the delimiter
1287 var index = key.lastIndexOf( '@' );
1288 // If the key doesn't contain '@' or starts with it, the whole thing is the module name
1289 if ( index === -1 || index === 0 ) {
1296 name: key.slice( 0, index ),
1297 version: key.slice( index + 1 )
1303 * @param {string} module
1304 * @param {string} [version]
1305 * @param {string[]} [dependencies]
1306 * @param {string} [group]
1307 * @param {string} [source]
1308 * @param {string} [skip]
1310 function registerOne( module, version, dependencies, group, source, skip ) {
1311 if ( module in registry ) {
1312 throw new Error( 'module already registered: ' + module );
1315 registry[ module ] = {
1316 // Exposed to execute() for mw.loader.impl() closures.
1317 // Import happens via require().
1321 // module.export objects for each package file inside this module
1323 version: version || '',
1324 dependencies: dependencies || [],
1325 group: typeof group === 'undefined' ? null : group,
1326 source: typeof source === 'string' ? source : 'local',
1327 state: 'registered',
1328 skip: typeof skip === 'string' ? skip : null
1332 /* Public Members */
1336 * The module registry is exposed as an aid for debugging and inspecting page
1337 * state; it is not a public interface for modifying the registry.
1340 * @property {Object}
1343 moduleRegistry: registry,
1346 * Exposed for testing and debugging only.
1348 * @see #batchRequest
1349 * @property {number}
1352 maxQueryLength: $VARS.maxQueryLength,
1354 addStyleTag: newStyleTag,
1356 // Exposed for internal use only. Documented as @private.
1357 addScriptTag: addScript,
1358 // Exposed for internal use only. Documented as @private.
1359 addLinkTag: addLink,
1361 // Exposed for internal use only. Documented as @private.
1364 // Exposed for internal use only. Documented as @private.
1368 * Start loading of all queued module dependencies.
1375 var q = queue.length,
1376 storedImplementations = [],
1381 // Iterate the list of requested modules, and do one of three things:
1382 // - 1) Nothing (if already loaded or being loaded).
1383 // - 2) Eval the cached implementation from the module store.
1384 // - 3) Request from network.
1386 var module = queue[ q ];
1387 // Only consider modules which are the initial 'registered' state,
1388 // and ignore duplicates
1389 if ( mw.loader.getState( module ) === 'registered' &&
1390 !batch.has( module )
1392 // Progress the state machine
1393 registry[ module ].state = 'loading';
1394 batch.add( module );
1396 var implementation = store.get( module );
1397 if ( implementation ) {
1398 // Module store enabled and contains this module/version
1399 storedImplementations.push( implementation );
1400 storedNames.push( module );
1402 // Module store disabled or doesn't have this module/version
1403 requestNames.push( module );
1408 // Now that the queue has been processed into a batch, clear the queue.
1409 // This MUST happen before we initiate any eval or network request. Otherwise,
1410 // it is possible for a cached script to instantly trigger the same work queue
1411 // again; all before we've cleared it causing each request to include modules
1412 // which are already loaded.
1415 asyncEval( storedImplementations, function ( err ) {
1416 // Not good, the cached mw.loader.impl calls failed! This should
1417 // never happen, barring ResourceLoader bugs, browser bugs and PEBKACs.
1418 // Depending on how corrupt the string is, it is likely that some
1419 // modules' impl() succeeded while the ones after the error will
1420 // never run and leave their modules in the 'loading' state forever.
1421 store.stats.failed++;
1423 // Since this is an error not caused by an individual module but by
1424 // something that infected the implement call itself, don't take any
1425 // risks and clear everything in this cache.
1430 source: 'store-eval'
1432 // For any failed ones, fallback to requesting from network
1433 var failed = storedNames.filter( function ( name ) {
1434 return registry[ name ].state === 'loading';
1436 batchRequest( failed );
1439 batchRequest( requestNames );
1443 * Register a source.
1445 * The #work() method will use this information to split up requests by source.
1448 * mw.loader.addSource( { mediawikiwiki: 'https://www.mediawiki.org/w/load.php' } );
1451 * @param {Object} ids An object mapping ids to load.php end point urls
1452 * @throws {Error} If source id is already registered
1454 addSource: function ( ids ) {
1455 for ( var id in ids ) {
1456 if ( id in sources ) {
1457 throw new Error( 'source already registered: ' + id );
1459 sources[ id ] = ids[ id ];
1464 * Register a module, letting the system know about it and its properties.
1466 * The startup module calls this method.
1468 * When using multiple module registration by passing an array, dependencies that
1469 * are specified as references to modules within the array will be resolved before
1470 * the modules are registered.
1472 * @param {string|Array} modules Module name or array of arrays, each containing
1473 * a list of arguments compatible with this method
1474 * @param {string} [version] Module version hash (falls backs to empty string)
1475 * @param {string[]} [dependencies] Array of module names on which this module depends.
1476 * @param {string} [group=null] Group which the module is in
1477 * @param {string} [source='local'] Name of the source
1478 * @param {string} [skip=null] Script body of the skip function
1481 register: function ( modules ) {
1482 if ( typeof modules !== 'object' ) {
1483 registerOne.apply( null, arguments );
1486 // Need to resolve indexed dependencies:
1487 // ResourceLoader uses an optimisation to save space which replaces module
1488 // names in dependency lists with the index of that module within the
1489 // array of module registration data if it exists. The benefit is a significant
1490 // reduction in the data size of the startup module. This loop changes
1491 // those dependency lists back to arrays of strings.
1492 function resolveIndex( dep ) {
1493 return typeof dep === 'number' ? modules[ dep ][ 0 ] : dep;
1496 for ( var i = 0; i < modules.length; i++ ) {
1497 var deps = modules[ i ][ 2 ];
1499 for ( var j = 0; j < deps.length; j++ ) {
1500 deps[ j ] = resolveIndex( deps[ j ] );
1503 // Optimisation: Up to 55% faster.
1504 // Typically register() is called exactly once on a page, and with a batch.
1505 // See <https://gist.github.com/Krinkle/f06fdb3de62824c6c16f02a0e6ce0e66>
1506 // Benchmarks taught us that the code for adding an object to `registry`
1507 // should be in a function that has only one signature and does no arguments
1509 // JS semantics make it hard to optimise recursion to a different
1510 // signature of itself, hence we moved this out.
1511 registerOne.apply( null, modules[ i ] );
1516 * Implement a module given the components of the module.
1518 * See #impl for a full description of the parameters.
1520 * Prior to MW 1.41, this was used internally, but now it is only kept
1521 * for backwards compatibility.
1523 * Does not support mw.loader.store caching.
1525 * @param {string} module
1526 * @param {Function|Array|string|Object} [script]
1527 * @param {Object} [style]
1528 * @param {Object} [messages] List of key/value pairs to be added to mw#messages.
1529 * @param {Object} [templates] List of key/value pairs to be added to mw#templates.
1530 * @param {string|null} [deprecationWarning] Deprecation warning if any
1533 implement: function ( module, script, style, messages, templates, deprecationWarning ) {
1534 var split = splitModuleKey( module ),
1536 version = split.version;
1538 // Automatically register module
1539 if ( !( name in registry ) ) {
1540 mw.loader.register( name );
1542 // Check for duplicate implementation
1543 if ( registry[ name ].script !== undefined ) {
1544 throw new Error( 'module already implemented: ' + name );
1546 registry[ name ].version = version;
1547 registry[ name ].declarator = null; // not supported
1548 registry[ name ].script = script;
1549 registry[ name ].style = style;
1550 registry[ name ].messages = messages;
1551 registry[ name ].templates = templates;
1552 registry[ name ].deprecationWarning = deprecationWarning;
1553 // The module may already have been marked as erroneous
1554 if ( registry[ name ].state !== 'error' && registry[ name ].state !== 'missing' ) {
1555 setAndPropagate( name, 'loaded' );
1560 * Implement a module given a function which returns the components of the module
1562 * @param {Function} declarator
1564 * The declarator should return an array with the following keys:
1566 * - 0. {string} module Name of module and current module version. Formatted
1567 * as '`[name]@[version]`". This version should match the requested version
1568 * (from #batchRequest and #registry). This avoids race conditions (T117587).
1570 * - 1. {Function|Array|string|Object} [script] Module code. This can be a function,
1571 * a list of URLs to load via `<script src>`, a string for `globalEval()`, or an
1572 * object like {"files": {"foo.js":function, "bar.js": function, ...}, "main": "foo.js"}.
1573 * If an object is provided, the main file will be executed immediately, and the other
1574 * files will only be executed if loaded via require(). If a function or string is
1575 * provided, it will be executed/evaluated immediately. If an array is provided, all
1576 * URLs in the array will be loaded immediately, and executed as soon as they arrive.
1578 * - 2. {Object} [style] Should follow one of the following patterns:
1580 * { "css": [css, ..] }
1581 * { "url": { (media): [url, ..] } }
1583 * The reason css strings are not concatenated anymore is T33676. We now check
1584 * whether it's safe to extend the stylesheet.
1586 * - 3. {Object} [messages] List of key/value pairs to be added to mw#messages.
1587 * - 4. {Object} [templates] List of key/value pairs to be added to mw#templates.
1588 * - 5. {String|null} [deprecationWarning] Deprecation warning if any
1590 * The declarator must not use any scope variables, since it will be serialized with
1591 * Function.prototype.toString() and later restored and executed in the global scope.
1593 * The elements are all optional except the name.
1596 impl: function ( declarator ) {
1597 var data = declarator(),
1599 script = data[ 1 ] || null,
1600 style = data[ 2 ] || null,
1601 messages = data[ 3 ] || null,
1602 templates = data[ 4 ] || null,
1603 deprecationWarning = data[ 5 ] || null,
1604 split = splitModuleKey( module ),
1606 version = split.version;
1608 // Automatically register module
1609 if ( !( name in registry ) ) {
1610 mw.loader.register( name );
1612 // Check for duplicate implementation
1613 if ( registry[ name ].script !== undefined ) {
1614 throw new Error( 'module already implemented: ' + name );
1616 // Without this reset, if there is a version mismatch between the
1617 // requested and received module version, then mw.loader.store would
1618 // cache the response under the requested key. Thus poisoning the cache
1619 // indefinitely with a stale value. (T117587)
1620 registry[ name ].version = version;
1621 // Attach components
1622 registry[ name ].declarator = declarator;
1623 registry[ name ].script = script;
1624 registry[ name ].style = style;
1625 registry[ name ].messages = messages;
1626 registry[ name ].templates = templates;
1627 registry[ name ].deprecationWarning = deprecationWarning;
1628 // The module may already have been marked as erroneous
1629 if ( registry[ name ].state !== 'error' && registry[ name ].state !== 'missing' ) {
1630 setAndPropagate( name, 'loaded' );
1635 * Load an external script or one or more modules.
1637 * This method takes a list of unrelated modules. Use cases:
1639 * - A web page will be composed of many different widgets. These widgets independently
1640 * queue their ResourceLoader modules (`OutputPage::addModules()`). If any of them
1641 * have problems, or are no longer known (e.g. cached HTML), the other modules
1642 * should still be loaded.
1643 * - This method is used for preloading, which must not throw. Later code that
1644 * calls #using() will handle the error.
1646 * @param {string|Array} modules Either the name of a module, array of modules,
1647 * or a URL of an external script or style
1648 * @param {string} [type='text/javascript'] MIME type to use if calling with a URL of an
1649 * external script or style; acceptable values are "text/css" and
1650 * "text/javascript"; if no type is provided, text/javascript is assumed.
1651 * @throws {Error} If type is invalid
1653 load: function ( modules, type ) {
1655 if ( typeof modules === 'string' && /^(https?:)?\/?\//.test( modules ) ) {
1656 // Called with a url like so:
1657 // - "https://example.org/x.js"
1658 // - "http://example.org/x.js"
1659 // - "//example.org/x.js"
1661 if ( type === 'text/css' ) {
1663 } else if ( type === 'text/javascript' || type === undefined ) {
1664 addScript( modules );
1667 throw new Error( 'Invalid type ' + type );
1670 // One or more modules
1671 modules = typeof modules === 'string' ? [ modules ] : modules;
1672 // Resolve modules into a flat list for internal queuing.
1673 // This also filters out unknown modules and modules with
1674 // unknown dependencies, allowing the rest to continue. (T36853)
1675 // Omit ready and error parameters, we don't have callbacks
1676 enqueue( resolveStubbornly( modules ) );
1681 * Change the state of one or more modules.
1683 * @param {Object} states Object of module name/state pairs
1686 state: function ( states ) {
1687 for ( var module in states ) {
1688 if ( !( module in registry ) ) {
1689 mw.loader.register( module );
1691 setAndPropagate( module, states[ module ] );
1696 * Get the state of a module.
1698 * Possible states for the public API:
1700 * - `registered`: The module is available for loading but not yet requested.
1701 * - `loading`, `loaded`, or `executing`: The module is currently being loaded.
1702 * - `ready`: The module was successfully and fully loaded.
1703 * - `error`: The module or one its dependencies has failed to load, e.g. due to
1704 * uncaught error from the module's script files.
1705 * - `missing`: The module was requested but is not defined according to the server.
1707 * Internal mw.loader state machine:
1710 * The module is known to the system but not yet required.
1711 * Meta data is stored by `register()`.
1712 * Calls to that method are generated server-side by StartupModule.
1714 * The module was required through mw.loader (either directly or as dependency of
1715 * another module). The client will fetch module contents from mw.loader.store
1716 * or from the server. The contents should later be received by `implement()`.
1718 * The module has been received by `implement()`.
1719 * Once the module has no more dependencies in-flight, the module will be executed,
1720 * controlled via `setAndPropagate()` and `doPropagation()`.
1722 * The module is being executed (apply messages and stylesheets, execute scripts)
1725 * The module has been successfully executed.
1727 * The module (or one of its dependencies) produced an uncaught error during execution.
1729 * The module was registered client-side and requested, but the server denied knowledge
1730 * of the module's existence.
1732 * @param {string} module Name of module
1733 * @return {string|null} The state, or null if the module (or its state) is not
1736 getState: function ( module ) {
1737 return module in registry ? registry[ module ].state : null;
1741 * Get the exported value of a module.
1743 * This static method is publicly exposed for debugging purposes
1744 * only and must not be used in production code. In production code,
1745 * please use the dynamically provided `require()` function instead.
1747 * In case of lazy-loaded modules via mw.loader#using(), the returned
1748 * Promise provides the function, see #using() for examples.
1752 * @param {string} moduleName Module name
1753 * @return {any} Exported value
1755 require: function ( moduleName ) {
1757 if ( window.QUnit ) {
1758 // Comply with Node specification
1759 // https://nodejs.org/docs/v20.1.0/api/modules.html#all-together
1761 // > Interpret X as a combination of NAME and SUBPATH, where the NAME
1762 // > may have a "@scope/" prefix and the subpath begins with a slash (`/`).
1764 // Regex inspired by Node [1], but simplified to suite our purposes
1765 // and split in two in order to keep the Regex Star Height under 2,
1766 // as per ESLint security/detect-unsafe-regex.
1768 // These patterns match "@scope/module/dir/file.js" and "module/dir/file.js"
1769 // respectively. They must not match "module.name" or "@scope/module.name".
1771 // [1] https://github.com/nodejs/node/blob/v20.1.0/lib/internal/modules/cjs/loader.js#L554-L560
1772 var paths = moduleName.startsWith( '@' ) ?
1773 /^(@[^/]+\/[^/]+)\/(.*)$/.exec( moduleName ) :
1774 // eslint-disable-next-line no-mixed-spaces-and-tabs
1775 /^([^/]+)\/(.*)$/.exec( moduleName );
1777 moduleName = paths[ 1 ];
1782 // Only ready modules can be required
1783 if ( mw.loader.getState( moduleName ) !== 'ready' ) {
1784 // Module may've forgotten to declare a dependency
1785 throw new Error( 'Module "' + moduleName + '" is not loaded' );
1789 makeRequireFunction( registry[ moduleName ], '' )( './' + path ) :
1790 registry[ moduleName ].module.exports;
1794 var hasPendingFlush = false,
1795 hasPendingWrites = false;
1798 * Actually update the store
1800 * @see #requestUpdate
1803 function flushWrites() {
1804 // Process queued module names, serialise their contents to the in-memory store.
1805 while ( store.queue.length ) {
1806 store.set( store.queue.shift() );
1809 // Optimization: Don't reserialize the entire store and rewrite localStorage,
1810 // if no module was added or changed.
1811 if ( hasPendingWrites ) {
1812 // Remove anything from the in-memory store that came from previous page
1813 // loads that no longer corresponds with current module names and versions.
1817 // Replacing the content of the module store might fail if the new
1818 // contents would exceed the browser's localStorage size limit. To
1819 // avoid clogging the browser with stale data, always remove the old
1820 // value before attempting to store a new one.
1821 localStorage.removeItem( store.key );
1822 localStorage.setItem( store.key, JSON.stringify( {
1825 // Store with 1e7 ms accuracy (1e4 seconds, or ~ 2.7 hours),
1826 // which is enough for the purpose of expiring after ~ 30 days.
1827 asOf: Math.ceil( Date.now() / 1e7 )
1832 source: 'store-localstorage-update'
1837 // Let the next call to requestUpdate() create a new timer.
1838 hasPendingFlush = hasPendingWrites = false;
1841 // We use a local variable `store` so that its easier to access, but also need to set
1842 // this in mw.loader so its exported - combine the two
1845 * On browsers that implement the localStorage API, the module store serves as a
1846 * smart complement to the browser cache. Unlike the browser cache, the module store
1847 * can slice a concatenated response from ResourceLoader into its constituent
1848 * modules and cache each of them separately, using each module's versioning scheme
1849 * to determine when the cache should be invalidated.
1853 * @class mw.loader.store
1856 mw.loader.store = store = {
1857 // Whether the store is in use on this page.
1860 // The contents of the store, mapping '[name]@[version]' keys
1861 // to module implementations.
1864 // Names of modules to be stored during the next update.
1865 // See add() and update().
1869 stats: { hits: 0, misses: 0, expired: 0, failed: 0 },
1872 * The localStorage key for the entire module store. The key references
1873 * $wgDBname to prevent clashes between wikis which share a common host.
1875 * @property {string}
1877 key: $VARS.storeKey,
1880 * A string containing various factors by which the module cache should vary.
1882 * Defined by ResourceLoader\StartupModule::getStoreVary() in PHP.
1884 * @property {string}
1886 vary: $VARS.storeVary,
1889 * Initialize the store.
1891 * Retrieves store from localStorage and (if successfully retrieved) decoding
1892 * the stored JSON value to a plain object.
1895 // Init only once per page
1896 if ( this.enabled === null ) {
1897 this.enabled = false;
1898 if ( $VARS.storeEnabled ) {
1901 // Clear any previous store to free up space. (T66721)
1909 * Internal helper for init(). Separated for ease of testing.
1912 // These are the scenarios to think about:
1914 // 1. localStorage is disallowed by the browser.
1915 // This means `localStorage.getItem` throws.
1916 // The store stays disabled.
1918 // 2. localStorage did not contain our store key.
1919 // This usually means the browser has a cold cache for this site,
1920 // and thus localStorage.getItem returns null.
1921 // The store will be enabled, and `items` starts fresh.
1923 // 3. localStorage contains parseable data, but it's not usable.
1924 // This means the data is too old, or is not valid for mw.loader.store.vary
1925 // (e.g. user switched skin or language).
1926 // The store will be enabled, and `items` starts fresh.
1928 // 4. localStorage contains invalid JSON data.
1929 // This means the data was corrupted, and `JSON.parse` throws.
1930 // The store will be enabled, and `items` starts fresh.
1932 // 5. localStorage contains valid and usable JSON.
1933 // This means we have a warm cache from a previous visit.
1934 // The store will be enabled, and `items` starts with the stored data.
1937 var raw = localStorage.getItem( this.key );
1939 // If we make it here, localStorage is enabled and available.
1940 // The rest of the function may fail, but that only affects what we load from
1941 // the cache. We'll still enable the store to allow storing new modules.
1942 this.enabled = true;
1944 // If getItem returns null, JSON.parse() will cast to string and re-parse, still null.
1945 var data = JSON.parse( raw );
1947 data.vary === this.vary &&
1949 // Only use if it's been less than 30 days since the data was written
1950 // 30 days = 2,592,000 s = 2,592,000,000 ms = ± 259e7 ms
1951 Date.now() < ( data.asOf * 1e7 ) + 259e7
1953 // The data is not corrupt, matches our vary context, and has not expired.
1954 this.items = data.items;
1957 // Ignore error from localStorage or JSON.parse.
1958 // Don't print any warning (T195647).
1963 * Retrieve a module from the store and update cache hit stats.
1965 * @param {string} module Module name
1966 * @return {string|boolean} Module implementation or false if unavailable
1968 get: function ( module ) {
1969 if ( this.enabled ) {
1970 var key = getModuleKey( module );
1971 if ( key in this.items ) {
1973 return this.items[ key ];
1976 this.stats.misses++;
1983 * Queue the name of a module that the next update should consider storing.
1986 * @param {string} module Module name
1988 add: function ( module ) {
1989 if ( this.enabled ) {
1990 this.queue.push( module );
1991 this.requestUpdate();
1996 * Add the contents of the named module to the in-memory store.
1998 * This method does not guarantee that the module will be stored.
1999 * Inspection of the module's meta data and size will ultimately decide that.
2001 * This method is considered internal to mw.loader.store and must only
2002 * be called if the store is enabled.
2005 * @param {string} module Module name
2007 set: function ( module ) {
2008 var descriptor = registry[ module ],
2009 key = getModuleKey( module );
2012 // Already stored a copy of this exact version
2013 key in this.items ||
2014 // Module failed to load
2016 descriptor.state !== 'ready' ||
2017 // Unversioned, private, or site-/user-specific
2018 !descriptor.version ||
2019 descriptor.group === $VARS.groupPrivate ||
2020 descriptor.group === $VARS.groupUser ||
2021 // Legacy descriptor, registered with mw.loader.implement
2022 !descriptor.declarator
2028 var script = String( descriptor.declarator );
2029 // Modules whose serialised form exceeds 100 kB won't be stored (T66721).
2030 if ( script.length > 1e5 ) {
2039 if ( $VARS.sourceMapLinks ) {
2040 srcParts.push( '// Saved in localStorage at ', ( new Date() ).toISOString(), '\n' );
2041 var sourceLoadScript = sources[ descriptor.source ];
2042 var query = Object.create( $VARS.reqBase );
2043 query.modules = module;
2044 query.version = getCombinedVersion( [ module ] );
2045 query = sortQuery( query );
2048 // Use absolute URL so that Firefox console stack trace links will work
2049 ( new URL( sourceLoadScript, location ) ).href,
2051 makeQueryString( query ),
2055 query.sourcemap = '1';
2056 query = sortQuery( query );
2058 '//# sourceMappingURL=',
2061 makeQueryString( query )
2064 this.items[ key ] = srcParts.join( '' );
2065 hasPendingWrites = true;
2069 * Iterate through the module store, removing any item that does not correspond
2070 * (in name and version) to an item in the module registry.
2072 prune: function () {
2073 for ( var key in this.items ) {
2074 // key is in the form [name]@[version], slice to get just the name
2075 // to provide to getModuleKey, which will return a key in the same
2076 // form but with the latest version
2077 if ( getModuleKey( splitModuleKey( key ).name ) !== key ) {
2078 this.stats.expired++;
2079 delete this.items[ key ];
2085 * Clear the entire module store right now.
2087 clear: function () {
2090 localStorage.removeItem( this.key );
2095 * Request a sync of the in-memory store back to persisted localStorage.
2097 * This function debounces updates. The debouncing logic should account
2098 * for the following factors:
2100 * - Writing to localStorage is an expensive operation that must not happen
2101 * during the critical path of initialising and executing module code.
2102 * Instead, it should happen at a later time after modules have been given
2103 * time and priority to do their thing first.
2105 * - This method is called from mw.loader.store.add(), which will be called
2106 * hundreds of times on a typical page, including within the same call-stack
2107 * and eventloop-tick. This is because responses from load.php happen in
2108 * batches. As such, we want to allow all modules from the same load.php
2109 * response to be written to disk with a single flush, not many.
2111 * - Repeatedly deleting and creating timers is non-trivial.
2113 * - localStorage is shared by all pages from the same origin, if multiple
2114 * pages are loaded with different module sets, the possibility exists that
2115 * modules saved by one page will be clobbered by another. The impact of
2116 * this is minor, it merely causes a less efficient cache use, and the
2117 * problem would be corrected by subsequent page views.
2119 * This method is considered internal to mw.loader.store and must only
2120 * be called if the store is enabled.
2125 requestUpdate: function () {
2126 // On the first call to requestUpdate(), create a timer that
2127 // waits at least two seconds, then calls onTimeout.
2128 // The main purpose is to allow the current batch of load.php
2129 // responses to complete before we do anything. This batch can
2130 // trigger many hundreds of calls to requestUpdate().
2131 if ( !hasPendingFlush ) {
2132 hasPendingFlush = setTimeout(
2133 // Defer the actual write via requestIdleCallback
2135 mw.requestIdleCallback( flushWrites );