diff --git a/dev/tests/js/jasmine/tests/lib/mage/requirejs/mixins.test.js b/dev/tests/js/jasmine/tests/lib/mage/requirejs/mixins.test.js new file mode 100644 index 0000000000000..52374a24e8c68 --- /dev/null +++ b/dev/tests/js/jasmine/tests/lib/mage/requirejs/mixins.test.js @@ -0,0 +1,187 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +/* eslint max-nested-callbacks: 0 */ +// jscs:disable jsDoc + +require.config({ + paths: { + 'mixins': 'mage/requirejs/mixins' + } +}); + +define(['rjsResolver', 'mixins'], function (resolver, mixins) { + 'use strict'; + + describe('mixins module', function () { + beforeEach(function (done) { + spyOn(mixins, 'hasMixins').and.callThrough(); + spyOn(mixins, 'getMixins').and.callThrough(); + spyOn(mixins, 'load').and.callThrough(); + + // Wait for all modules to be loaded so they don't interfere with testing. + resolver(function () { + done(); + }); + }); + + it('does not affect modules without mixins', function (done) { + var name = 'tests/assets/mixins/no-mixins', + mixinName = 'tests/assets/mixins/no-mixins-ext'; + + mixins.hasMixins.and.returnValue(false); + + define(name, [], function () { + return { + value: 'original' + }; + }); + + define(mixinName, [], function () { + return function (module) { + module.value = 'changed'; + + return module; + }; + }); + + require([name], function (module) { + expect(module.value).toBe('original'); + + done(); + }); + }); + + it('does not affect modules that are loaded with plugins', function (done) { + var name = 'plugin!tests/assets/mixins/no-mixins', + mixinName = 'tests/assets/mixins/no-mixins-ext'; + + mixins.hasMixins.and.returnValue(true); + mixins.getMixins.and.returnValue([mixinName]); + + define('plugin', [], function () { + return { + load: function (module, req, onLoad) { + req(module, onLoad); + } + }; + }); + + define(name, [], function () { + return { + value: 'original' + }; + }); + + define(mixinName, [], function () { + return function (module) { + module.value = 'changed'; + + return module; + }; + }); + + require([name], function (module) { + expect(module.value).toBe('original'); + + done(); + }); + }); + + it('applies mixins for normal module with mixins', function (done) { + var name = 'tests/assets/mixins/mixins-applied', + mixinName = 'tests/assets/mixins/mixins-applied-ext'; + + mixins.hasMixins.and.returnValue(true); + mixins.getMixins.and.returnValue([mixinName]); + + define(name, [], function () { + return { + value: 'original' + }; + }); + + define(mixinName, [], function () { + return function (module) { + module.value = 'changed'; + + return module; + }; + }); + + require([name], function (module) { + expect(module.value).toBe('changed'); + + done(); + }); + }); + + it('applies mixins for module that is a dependency', function (done) { + var name = 'tests/assets/mixins/module-with-dependency', + dependencyName = 'tests/assets/mixins/dependency-module', + mixinName = 'tests/assets/mixins/dependency-module-ext'; + + mixins.hasMixins.and.returnValue(true); + mixins.getMixins.and.returnValue([mixinName]); + + define(dependencyName, [], function () { + return { + value: 'original' + }; + }); + + define(name, [dependencyName], function (module) { + expect(module.value).toBe('changed'); + + done(); + + return {}; + }); + + define(mixinName, [], function () { + return function (module) { + module.value = 'changed'; + + return module; + }; + }); + + require([name], function () {}); + }); + + it('applies mixins for module that is a relative dependency', function (done) { + var name = 'tests/assets/mixins/module-with-relative-dependency', + dependencyName = 'tests/assets/mixins/relative-module', + mixinName = 'tests/assets/mixins/relative-module-ext'; + + mixins.hasMixins.and.returnValue(true); + mixins.getMixins.and.returnValue([mixinName]); + + define(dependencyName, [], function () { + return { + value: 'original' + }; + }); + + define(name, ['./relative-module'], function (module) { + expect(module.value).toBe('changed'); + + done(); + + return {}; + }); + + define(mixinName, [], function () { + return function (module) { + module.value = 'changed'; + + return module; + }; + }); + + require([name], function () {}); + }); + }); +}); diff --git a/lib/web/mage/requirejs/mixins.js b/lib/web/mage/requirejs/mixins.js index 77d98e0f81394..613605038f4b9 100644 --- a/lib/web/mage/requirejs/mixins.js +++ b/lib/web/mage/requirejs/mixins.js @@ -7,7 +7,25 @@ define('mixins', [ ], function (module) { 'use strict'; - var rjsMixins; + var contexts = require.s.contexts, + defContextName = '_', + defContext = contexts[defContextName], + unbundledContext = require.s.newContext('$'), + defaultConfig = defContext.config, + unbundledConfig = { + baseUrl: defaultConfig.baseUrl, + paths: defaultConfig.paths, + shim: defaultConfig.shim, + config: defaultConfig.config, + map: defaultConfig.map + }, + rjsMixins; + + /** + * Prepare a separate context where modules are not assigned to bundles + * so we are able to get their true path and corresponding mixins. + */ + unbundledContext.configure(unbundledConfig); /** * Checks if specified string contains @@ -50,14 +68,14 @@ define('mixins', [ /** * Extracts url (without baseUrl prefix) - * from a modules' name. + * from a module name ignoring the fact that it may be bundled. * * @param {String} name - Name, path or alias of a module. - * @param {Object} config - Contexts' configuartion. + * @param {Object} config - Context's configuration. * @returns {String} */ function getPath(name, config) { - var url = require.toUrl(name); + var url = unbundledContext.require.toUrl(name); return removeBaseUrl(url, config); } @@ -73,11 +91,11 @@ define('mixins', [ } /** - * Iterativly calls mixins passing to them + * Iteratively calls mixins passing to them * current value of a 'target' parameter. * * @param {*} target - Value to be modified. - * @param {...Function} mixins + * @param {...Function} mixins - List of mixins to apply. * @returns {*} Modified 'target' value. */ function applyMixins(target) { @@ -94,8 +112,13 @@ define('mixins', [ /** * Loads specified module along with its' mixins. + * This method is called for each module defined with "mixins!" prefix + * in its name that was added by processNames method. * * @param {String} name - Module to be loaded. + * @param {Function} req - Local "require" function to use to load other modules. + * @param {Function} onLoad - A function to call with the value for name. + * @param {Object} config - RequireJS configuration object. */ load: function (name, req, onLoad, config) { var path = getPath(name, config), @@ -110,14 +133,14 @@ define('mixins', [ /** * Retrieves list of mixins associated with a specified module. * - * @param {String} path - Path to the module (without base url). + * @param {String} path - Path to the module (without base URL). * @returns {Array} An array of paths to mixins. */ getMixins: function (path) { var config = module.config() || {}, - mixins; + mixins; - // fix for when urlArgs is set + // Fix for when urlArgs is set. if (path.indexOf('?') !== -1) { path = path.substring(0, path.indexOf('?')); } @@ -131,7 +154,7 @@ define('mixins', [ /** * Checks if specified module has associated with it mixins. * - * @param {String} path - Path to the module (without base url). + * @param {String} path - Path to the module (without base URL). * @returns {Boolean} */ hasMixins: function (path) { @@ -139,11 +162,11 @@ define('mixins', [ }, /** - * Modifies provided names perpending to them + * Modifies provided names prepending to them * the 'mixins!' plugin prefix if it's necessary. * * @param {(Array|String)} names - Module names, paths or aliases. - * @param {Object} context - Current requirejs context. + * @param {Object} context - Current RequireJS context. * @returns {Array|String} */ processNames: function (names, context) { @@ -179,101 +202,40 @@ require([ ], function (mixins) { 'use strict'; - var originalRequire = window.require, - originalDefine = window.define, - contexts = originalRequire.s.contexts, - defContextName = '_', - hasOwn = Object.prototype.hasOwnProperty, - getLastInQueue; - - getLastInQueue = - '(function () {' + - 'var queue = globalDefQueue,' + - 'item = queue[queue.length - 1];' + - '' + - 'return item;' + - '})();'; - - /** - * Returns property of an object if - * it's not defined in it's prototype. - * - * @param {Object} obj - Object whose property should be retrieved. - * @param {String} prop - Name of the property. - * @returns {*} Value of the property or false. - */ - function getOwn(obj, prop) { - return hasOwn.call(obj, prop) && obj[prop]; - } - - /** - * Overrides global 'require' method adding to it dependencies modfication. - */ - window.require = function (deps, callback, errback, optional) { - var contextName = defContextName, - context, - config; - - if (!Array.isArray(deps) && typeof deps !== 'string') { - config = deps; - - if (Array.isArray(callback)) { - deps = callback; - callback = errback; - errback = optional; - } else { - deps = []; - } - } - - if (config && config.context) { - contextName = config.context; - } - - context = getOwn(contexts, contextName); - - if (!context) { - context = contexts[contextName] = require.s.newContext(contextName); - } - - if (config) { - context.configure(config); - } - - deps = mixins.processNames(deps, context); - - return context.require(deps, callback, errback); - }; + var contexts = require.s.contexts, + defContextName = '_', + defContext = contexts[defContextName], + originalContextRequire = defContext.require, + processNames = mixins.processNames; /** - * Overrides global 'define' method adding to it dependencies modfication. + * Wrap default context's require function which gets called every time + * module is requested using require call. The upside of this approach + * is that deps parameter is already normalized and guaranteed to be an array. */ - window.define = function (name, deps, callback) { // eslint-disable-line no-unused-vars - var context = getOwn(contexts, defContextName), - result = originalDefine.apply(this, arguments), - queueItem = require.exec(getLastInQueue), - lastDeps = queueItem && queueItem[1]; - - if (Array.isArray(lastDeps)) { - queueItem[1] = mixins.processNames(lastDeps, context); - } + defContext.require = function (deps, callback, errback) { + deps = processNames(deps, defContext); - return result; + return originalContextRequire(deps, callback, errback); }; /** * Copy properties of original 'require' method. */ - Object.keys(originalRequire).forEach(function (key) { - require[key] = originalRequire[key]; + Object.keys(originalContextRequire).forEach(function (key) { + defContext.require[key] = originalContextRequire[key]; }); /** - * Copy properties of original 'define' method. + * Wrap shift method from context's definitions queue. + * Items are added to the queue when a new module is defined and taken + * from it every time require call happens. */ - Object.keys(originalDefine).forEach(function (key) { - define[key] = originalDefine[key]; - }); + defContext.defQueue.shift = function () { + var queueItem = Array.prototype.shift.call(this); + + queueItem[1] = processNames(queueItem[1], defContext); - window.requirejs = window.require; + return queueItem; + }; });