├── .gitignore ├── LICENSE ├── README.md ├── examples └── simple │ └── webpack.config.js ├── index.js ├── lib ├── default │ └── merge.js ├── mergeLoader.js └── resolve │ ├── loader.js │ └── plugin.js ├── package.json └── test ├── init.spec.js ├── loader.spec.js ├── merge.spec.js └── plugin.spec.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # node-waf configuration 20 | .lock-wscript 21 | 22 | # Compiled binary addons (http://nodejs.org/api/addons.html) 23 | build/Release 24 | 25 | # Dependency directory 26 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 27 | node_modules 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Lewis Barnes 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Webpack Configurator 2 | 3 | ## Notice 4 | 5 | In the coming weeks, this module will hit v1.0.0! These changes are quite significant, causing a number of **breaking changes** in comparison to the current API. For a preview of the upcoming changes, see the [v1.0.0 branch](https://github.com/lewie9021/webpack-configurator/tree/1.0.0). Feedback is much appreciated! 6 | 7 | ## Install 8 | 9 | ``` 10 | $ npm install webpack-configurator 11 | ```` 12 | 13 | ## Motivation 14 | 15 | In a number of my old projects, I found it difficult to DRY up the configuration files. My setup often contained a number of build modes (e.g. dev, test, and production), each sharing similar parts to one another. These common chunks were placed in a 'base' build mode. I wanted to still maintain the flexibility of including build mode specific configuration, while at the same time making slight changes to things such as loader query strings. In the end, I still found that my build mode files contained repetitive boilerplate code that I really wanted to avoid. 16 | 17 | ## API 18 | 19 | ### config.merge(config) 20 | 21 | 22 | 23 | **Arguments** 24 | 25 | 1. `config` *(Object|Function)*: If an object is passed, this will be merged with the current value of config._config using the default way of merging (concat arrays nested within the data structure). If a function is passed, the first parameter will be a copy of the value contained within config._config. You can then make all the necessary changes to the data structure before returning the new value. 26 | 27 | **Returns** 28 | 29 | *`(Object)`*: The config object to allow function chaining. 30 | 31 | **Example** 32 | 33 | ```javascript 34 | // Config as an object. 35 | config.merge({ 36 | entry: "./app.entry.js" 37 | }); 38 | 39 | // Config as a function. 40 | config.merge(function(current) { 41 | current.entry = "./app.entry.js"; 42 | 43 | return current; 44 | }); 45 | ``` 46 | 47 | ### config.loader(key, config, resolver) 48 | 49 | Provides a way of adding loaders to the config. You can add two other types of loaders using `config.preLoader` and `config.postLoader`. 50 | 51 | **Arguments** 52 | 53 | 1. `key` *(String)*: Name of the loader. This is used to differentiate between loaders when merging/extending. When resolving, this value is used as a fallback for the 'loader' property value. 54 | 2. `config` *(Object|Function)*: If an object is passed, this will be merged with the current value of the loader's config using the default way of merging (concat arrays nested within the data structure). If a function is passed, the first parameter will be a copy of the loader's config. You can then make all the necessary changes to the data structure before returning the new value. 55 | 3. `resolver` *(Function)*: This works in a similar way to the `config` parameter, however, it is only called when resolving. It provides an opportunity to make final changes once the configuration is has been completely merged. **Note**: If the loader already has a resolver, the value will simply get replaced. 56 | 57 | **Returns** 58 | 59 | *`(Object)`*: The config object to allow function chaining. 60 | 61 | **Examples** 62 | 63 | Config as an object. 64 | ```javascript 65 | config.loader("dustjs-linkedin", { 66 | test: /\.dust$/ 67 | }); 68 | ``` 69 | 70 | Config as a function. 71 | ```javascript 72 | config.loader("dustjs-linkedin", function(current) { 73 | current.test = /\.dust$/; 74 | 75 | return current; 76 | }); 77 | ``` 78 | 79 | Config as an object with a resolver function. 80 | ```javascript 81 | var ExtractTextPlugin = require('extract-text-webpack-plugin'); 82 | 83 | config.loader("sass", { 84 | test: /\.scss$/, 85 | queries: { 86 | css: { 87 | sourceMap: true 88 | }, 89 | sass: { 90 | sourceMap: true 91 | } 92 | } 93 | }, function(config) { 94 | var loaders = []; 95 | 96 | for (var key in config.queries) 97 | loaders.push(key + "?" + JSON.stringify(config.queries[key])); 98 | 99 | config.loader = ExtractTextPlugin.extract(loaders.join("!")); 100 | 101 | return config; 102 | }); 103 | ``` 104 | 105 | 106 | ### config.removeLoader(key) 107 | 108 | Provides a way to remove loaders without directly modifying internal data structures on the instance. You can remove two other types of loaders using the following: `config.removePreLoader(key)` and `config.removePostLoader(key)`. 109 | 110 | **Arguments** 111 | 112 | 1. `key` *(String)*: Name of the loader you wish to remove. This is the same value used when calling the 'loader' method. 113 | 114 | **Returns** 115 | 116 | *`(Object)`*: The config object to allow function chaining. 117 | 118 | **Example** 119 | 120 | ```javascript 121 | // Create a loader with the key 'dustjs-linkedin' 122 | config.loader("dustjs-linkedin", { 123 | test: /\.dust$/ 124 | }); 125 | 126 | // Remove the loader using the same key as above. 127 | config.removeLoader("dustjs-linkedin"); 128 | ``` 129 | 130 | ### config.plugin(key, constructor, parameters) 131 | 132 | 133 | 134 | **Arguments** 135 | 136 | 1. `key` *(String)*: Name of the plugin. This is used to differentiate between plugins when merging/extending. 137 | 2. `constructor` *(Class)*: The class constructor that you wish to be instantiated when resolving. **Note**: If the plugin already has a constructor, the value will simply get replaced. You may merge/extend `parameters` by passing null for this parameter. 138 | 3. `parameters` *(Array|Function)*: If an array is passed, this will be merged with the current value of the plugin's parameters using the default way of merging (concat arrays nested within the data structure). If a function is passed, the first parameter will be a copy of the plugin's parameters array. You can then make all the necessary changes to the data structure before returning the new value. **Note** This must be an array. 139 | 140 | **Returns** 141 | 142 | *`(Object)`*: The config object to allow function chaining. 143 | 144 | **Examples** 145 | 146 | Parameters as an array. 147 | ```javascript 148 | var Webpack = require("webpack"); 149 | 150 | config.plugin("webpack-define", Webpack.DefinePlugin, [{ 151 | __DEV__: true 152 | }]); 153 | ``` 154 | 155 | Parameters as a function. 156 | ```javascript 157 | var Webpack = require("webpack"); 158 | 159 | config.plugin("webpack-define", Webpack.DefinePlugin, function(current) { 160 | return [{ 161 | __DEV__: true 162 | }]; 163 | }); 164 | ``` 165 | 166 | ### config.removePlugin(key) 167 | 168 | 169 | 170 | **Arguments** 171 | 172 | 1. `key` *(String)*: Name of the plugin you wish to remove. This is the same value used when calling the 'plugin' method. 173 | 174 | **Returns** 175 | 176 | *`(Object)`*: The config object to allow function chaining. 177 | 178 | **Example** 179 | 180 | ```javascript 181 | var Webpack = require("webpack"); 182 | 183 | // Create a plugin with the key 'webpack-define'. 184 | config.plugin("webpack-define", Webpack.DefinePlugin, [{ 185 | __DEV__: true 186 | }]); 187 | 188 | // Remove the plugin using the same key as above. 189 | config.removePlugin("webpack-define"); 190 | ``` 191 | 192 | 193 | 194 | ### config.resolve() 195 | 196 | Call when you want to return a complete Webpack configuration object, typically at the end. It can be called numerous times since it doesn't produce any side effects. 197 | 198 | **Returns** 199 | 200 | *`(Object)`*: A valid Webpack configuration object 201 | 202 | **Examples** 203 | 204 | A simple webpack.config.js file demonstrating the module's use. 205 | ```javascript 206 | var Config = require("webpack-configurator"); 207 | var Webpack = require("webpack"); 208 | 209 | module.exports = (function() { 210 | var config = new Config(); 211 | 212 | config.merge({ 213 | entry: "./main.js", 214 | output: { 215 | filename: "bundle.js" 216 | } 217 | }); 218 | 219 | config.loader("dustjs-linkedin", { 220 | test: /\.dust$/ 221 | }); 222 | 223 | config.loader("sass", { 224 | test: /\.scss$/, 225 | loader: "style!css!sass?indentedSyntax" 226 | }); 227 | 228 | config.plugin("webpack-define", Webpack.DefinePlugin, [{ 229 | VERSION: "1.0.0" 230 | }]); 231 | 232 | return config.resolve(); 233 | })(); 234 | ``` 235 | -------------------------------------------------------------------------------- /examples/simple/webpack.config.js: -------------------------------------------------------------------------------- 1 | var Path = require("path"); 2 | var Webpack = require("webpack"); 3 | var Config = require("../../"); 4 | 5 | module.exports = (function() { 6 | var config = new Config(); 7 | 8 | config.merge({ 9 | entry: "./main.js", 10 | output: { 11 | filename: "bundle.js" 12 | } 13 | }); 14 | 15 | config.loader("dustjs-linkedin", { 16 | test: /\.dust$/, 17 | query: { 18 | path: Path.join(__dirname, "views") 19 | } 20 | }); 21 | 22 | config.loader("sass", { 23 | test: /\.scss$/, 24 | loader: "style!css!sass?indentedSyntax" 25 | }); 26 | 27 | config.plugin("webpack-define", Webpack.DefinePlugin, [{ 28 | VERSION: "1.0.0" 29 | }]); 30 | 31 | return config.resolve(); 32 | })(); 33 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var _ = require("lodash"); 2 | var resolveLoader = require("./lib/resolve/loader"); 3 | var resolvePlugin = require("./lib/resolve/plugin"); 4 | var defaultMerge = require("./lib/default/merge"); 5 | var mergeLoader = require("./lib/mergeLoader"); 6 | 7 | function Config() { 8 | this._config = {}; 9 | 10 | this._preLoaders = {}; 11 | this._loaders = {}; 12 | this._postLoaders = {}; 13 | 14 | this._plugins = {}; 15 | } 16 | 17 | // This method provides full customisation of how the configuration object is built. It is also used as a base when resolving. 18 | // TODO: If the config function doesn't return a valid, use the value we passed in, assuming the user forgot to return. 19 | Config.prototype.merge = function(config) { 20 | if (!_.isObject(config) && !_.isFunction(config)) 21 | throw new Error("Invalid Parameter. You must provide either an object or a function."); 22 | 23 | if (typeof config == "function") { 24 | var clonedConfig = _.clone(this._config, true, function(value) { 25 | if (!_.isPlainObject(value)) 26 | return value; 27 | }); 28 | this._config = config(clonedConfig || {}); 29 | } else { 30 | _.merge(this._config, config, defaultMerge); 31 | } 32 | 33 | return this; 34 | }; 35 | 36 | // Almost an alias of loader only we assign to this._preLoaders. 37 | Config.prototype.preLoader = function(name, config, resolver) { 38 | var args = Array.prototype.slice.call(arguments); 39 | 40 | // Add the current value of the loader. On creation of a loader, this will be undefined. 41 | args.unshift(this._preLoaders[name]); 42 | 43 | // Assign the newly merged loader to the internal data store. 44 | this._preLoaders[name] = mergeLoader.apply(this, args); 45 | 46 | // Return 'this' to allow chaining. 47 | return this; 48 | }; 49 | 50 | Config.prototype.removePreLoader = function(key) { 51 | delete this._preLoaders[key]; 52 | 53 | return this; 54 | }; 55 | 56 | // This method is a helper for creating loaders. It requires a name to make merging easier when identifying loaders. 57 | // There may be some cases where a resolver is needed. A good example is the ExtractTextPlugin. Using the resolver 58 | // parameter, it is possible to call ExtractTextPlugin.extract when resolving, to ensure we have the fully merged 59 | // loader config. 60 | Config.prototype.loader = function(name, config, resolver) { 61 | var args = Array.prototype.slice.call(arguments); 62 | 63 | // Append the loader object. 64 | args.unshift(this._loaders[name]); 65 | 66 | // Assign the newly merged loader to the internal data store. 67 | this._loaders[name] = mergeLoader.apply(this, args); 68 | 69 | // Return 'this' to allow chaining. 70 | return this; 71 | }; 72 | 73 | Config.prototype.removeLoader = function(key) { 74 | delete this._loaders[key]; 75 | 76 | return this; 77 | }; 78 | 79 | // Almost an alias of loader only we assign to this._postLoaders. 80 | Config.prototype.postLoader = function(name, config, resolver) { 81 | var args = Array.prototype.slice.call(arguments); 82 | 83 | // Add the current value of the loader. On creation of a loader, this will be undefined. 84 | args.unshift(this._postLoaders[name]); 85 | 86 | // Assign the newly merged loader to the internal data store. 87 | this._postLoaders[name] = mergeLoader.apply(this, args); 88 | 89 | // Return 'this' to allow chaining. 90 | return this; 91 | }; 92 | 93 | Config.prototype.removePostLoader = function(key) { 94 | delete this._postLoaders[key]; 95 | 96 | return this; 97 | }; 98 | 99 | // A method for creating/exending a plugin. It is similar to a loader in respect to the name parameter. 100 | // From all the examples of plugins I've seen, it seems constructors are always used. This will be instantiated when 101 | // resolving, passing the value of parameters. 102 | Config.prototype.plugin = function(name, constructor, parameters) { 103 | var plugin = (_.clone(this._plugins[name], true) || {}); 104 | var resolvedParameters; 105 | 106 | if (!_.isString(name)) 107 | throw new Error("Invalid 'name' parameter. You must provide a string."); 108 | 109 | if (!_.isNull(constructor) && !_.isFunction(constructor)) 110 | throw new Error("Invalid 'constructor' parameter. You must provide either a function or null."); 111 | 112 | if (parameters && !_.isFunction(parameters) && !_.isArray(parameters)) 113 | throw new TypeError("The optional 'parameters' argument must be an array or a function."); 114 | 115 | if (parameters) { 116 | if (_.isFunction(parameters)) { 117 | resolvedParameters = parameters(_.clone(plugin.parameters, true) || []); 118 | 119 | if (!_.isArray(resolvedParameters)) 120 | throw new TypeError("The 'parameters' argument must return an array."); 121 | 122 | plugin.parameters = resolvedParameters; 123 | } else { 124 | _.merge(plugin, {parameters: parameters}, defaultMerge); 125 | } 126 | } 127 | 128 | if (constructor) 129 | plugin.klass = constructor; 130 | 131 | this._plugins[name] = plugin; 132 | 133 | return this; 134 | }; 135 | 136 | Config.prototype.removePlugin = function(key) { 137 | delete this._plugins[key]; 138 | 139 | return this; 140 | }; 141 | 142 | // This method returns a valid Webpack object. It should not produce any side-effects and therefore can be called as 143 | // many times as you want. 144 | Config.prototype.resolve = function() { 145 | // It's possible that non-plain objects (such as instances) could exist within the config (e.g. calling the merge 146 | // method to define plugins). 147 | var config = _.clone(this._config, true, function(value) { 148 | if (!_.isPlainObject(value)) 149 | return value; 150 | }); 151 | var plugins = []; 152 | 153 | // Resolve each type of loader. 154 | ["preLoaders", "loaders", "postLoaders"].forEach(function(property) { 155 | var map = this["_" + property]; 156 | var loaders = []; 157 | var module = {}; 158 | var loader; 159 | 160 | if (!Object.keys(map).length) 161 | return; 162 | 163 | for (var name in map) { 164 | loader = map[name]; 165 | 166 | loaders.push(resolveLoader(name, loader)); 167 | } 168 | 169 | config.module = (config.module || {}); 170 | 171 | module[property] = loaders; 172 | 173 | _.merge(config.module, module, defaultMerge); 174 | }, this); 175 | 176 | // Resolve each plugin. This will basically do: new MyPlugin.apply(MyPlugin, parameters). 177 | for (var name in this._plugins) { 178 | var plugin = this._plugins[name]; 179 | 180 | plugins.push(resolvePlugin(name, plugin)); 181 | } 182 | 183 | if (plugins.length) 184 | config.plugins = (config.plugins || []).concat(plugins); 185 | 186 | return config; 187 | }; 188 | 189 | module.exports = Config; 190 | -------------------------------------------------------------------------------- /lib/default/merge.js: -------------------------------------------------------------------------------- 1 | var _ = require("lodash"); 2 | 3 | // Used whenever _.merge is called to for consistency. 4 | module.exports = function(a, b) { 5 | if (_.isArray(a)) { 6 | return a.concat(b); 7 | } 8 | }; 9 | -------------------------------------------------------------------------------- /lib/mergeLoader.js: -------------------------------------------------------------------------------- 1 | var _ = require("lodash"); 2 | var defaultMerge = require("./default/merge"); 3 | 4 | // Returns a successfully merged loader. This function helps DRY up the following: preLoader, loader, and postLoader. 5 | // They all perform the same task but attach the returned value to a different data structure. 6 | // Used by [pre|post]loader methods to merge the given configuration into the the internal data stores. 7 | module.exports = function(loader, name, config, resolver) { 8 | loader = (_.clone(loader, true) || {}); 9 | 10 | if (!_.isString(name)) 11 | throw new Error("Invalid 'name' parameter. You must provide a string."); 12 | 13 | if (_.isArray(config) || (!_.isNull(config) && !_.isObject(config) && !_.isFunction(config))) 14 | throw new Error("Invalid 'config' parameter. You must provide either an object, function, or null."); 15 | 16 | if (typeof config == "function") 17 | loader.config = config(_.clone(loader.config, true) || {}); 18 | else 19 | _.merge(loader, {config: config}, defaultMerge); 20 | 21 | if (resolver) 22 | loader.resolver = resolver; 23 | 24 | return loader; 25 | }; 26 | -------------------------------------------------------------------------------- /lib/resolve/loader.js: -------------------------------------------------------------------------------- 1 | var _ = require("lodash"); 2 | 3 | // Used by the resolve method to modify the loader config before appending to the module.[pre|post]loaders. 4 | module.exports = function(name, loader) { 5 | var config; 6 | 7 | loader = _.clone(loader, true); 8 | 9 | if (loader.resolver) 10 | loader.config = loader.resolver(_.clone(loader.config, true), name); 11 | 12 | config = loader.config; 13 | 14 | if (!config.loader && !config.loaders) 15 | config.loader = name; 16 | 17 | return config; 18 | }; 19 | -------------------------------------------------------------------------------- /lib/resolve/plugin.js: -------------------------------------------------------------------------------- 1 | // Used by the resolve method to execute the plugin constructor with the parameters provided. 2 | module.exports = function(name, plugin) { 3 | if (!plugin.klass) 4 | throw new Error("Failed to resolve '" + name + "'. Expected constructor not to be null."); 5 | 6 | return (function() { 7 | function F(args) { 8 | return plugin.klass.apply(this, args); 9 | } 10 | 11 | F.prototype = plugin.klass.prototype; 12 | 13 | return new F(arguments); 14 | }).apply(this, plugin.parameters); 15 | }; 16 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webpack-configurator", 3 | "version": "0.3.1", 4 | "description": "Helper for creating and extending Webpack configuration structures.", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "node_modules/.bin/mocha" 8 | }, 9 | "keywords": [ 10 | "webpack", 11 | "config", 12 | "extend", 13 | "merge", 14 | "loaders", 15 | "plugins" 16 | ], 17 | "author": "Lewis Barnes", 18 | "repository": { 19 | "type": "git", 20 | "url": "http://github.com/lewie9021/webpack-configurator" 21 | }, 22 | "license": "MIT", 23 | "dependencies": { 24 | "lodash": "3.10.1" 25 | }, 26 | "devDependencies": { 27 | "chai": "3.4.0", 28 | "extract-text-webpack-plugin": "0.8.2", 29 | "mocha": "2.3.3" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /test/init.spec.js: -------------------------------------------------------------------------------- 1 | var expect = require("chai").expect; 2 | var Config = require("../"); 3 | 4 | describe("instantiation", function() { 5 | 6 | beforeEach(function() { 7 | this.config = new Config(); 8 | }); 9 | 10 | it("should provide functionality for merging existing configuration objects", function() { 11 | expect(this.config.merge).to.be.an.instanceof(Function); 12 | }); 13 | 14 | it("should provide functionality for creating loaders, pre-loaders, and post-loaders", function() { 15 | var methods = [ 16 | "preLoader", 17 | "loader", 18 | "postLoader", 19 | ]; 20 | 21 | methods.forEach(function(method) { 22 | expect(this.config[method]).to.be.an.instanceof(Function); 23 | }, this); 24 | 25 | }); 26 | 27 | it("should provide functionality for creating plugins", function() { 28 | expect(this.config.plugin).to.be.an.instanceof(Function); 29 | }); 30 | 31 | it("should provide a way to remove loaders and plugins", function() { 32 | var methods = [ 33 | "removePreLoader", 34 | "removeLoader", 35 | "removePostLoader", 36 | "removePlugin" 37 | ]; 38 | 39 | methods.forEach(function(method) { 40 | expect(this.config[method]).to.be.an.instanceof(Function); 41 | }, this); 42 | }); 43 | 44 | it("should provide the ability to resolve the composed configuration into an object for Webpack", function() { 45 | expect(this.config.resolve).to.be.an.instanceof(Function); 46 | }); 47 | 48 | }); 49 | -------------------------------------------------------------------------------- /test/loader.spec.js: -------------------------------------------------------------------------------- 1 | var expect = require("chai").expect; 2 | var Config = require("../"); 3 | 4 | describe("loader", function() { 5 | 6 | beforeEach(function() { 7 | this.config = new Config(); 8 | }); 9 | 10 | it("should require a name (that's a string) to enable referencing when merging", function() { 11 | var config = this.config; 12 | var invalid = [5, function() {}, [], null, undefined, {}]; 13 | 14 | invalid.forEach(function(invalidParameter) { 15 | expect(function() { 16 | config.loader(invalidParameter); 17 | }).to.throw("Invalid 'name' parameter. You must provide a string."); 18 | }); 19 | 20 | expect(function() { 21 | config.loader("my-loader", {}); 22 | }).to.not.throw(); 23 | }); 24 | 25 | it("should accept a config parameter that's an object, function, or null", function() { 26 | var config = this.config; 27 | var valid = [{}, function() {}, null]; 28 | var invalid = ["", [], true, false, 5, undefined]; 29 | 30 | valid.forEach(function(validParameter) { 31 | expect(function() { 32 | config.loader("my-loader", validParameter); 33 | }).not.to.throw(); 34 | }); 35 | 36 | invalid.forEach(function(invalidParameter) { 37 | expect(function() { 38 | config.loader("my-loader", invalidParameter); 39 | }).to.throw("Invalid 'config' parameter. You must provide either an object, function, or null."); 40 | }); 41 | }); 42 | 43 | it("should, when resolving, create a module.loaders array if not already created", function() { 44 | var resolved; 45 | 46 | this.config.loader("my-loader", { 47 | test: /\.myldr/ 48 | }); 49 | 50 | resolved = this.config.resolve(); 51 | 52 | expect(resolved).to.have.property("module"); 53 | expect(resolved.module).to.have.property("loaders"); 54 | expect(resolved.module.loaders).to.be.an.instanceof(Array); 55 | expect(resolved.module.loaders[0]).to.be.an.instanceof(Object); 56 | }); 57 | 58 | it("should use the value of 'name' for the 'loader' property when resolving if no loader is defined", function() { 59 | var loaders; 60 | 61 | this.config.loader("my-loader", { 62 | test: /\.myldr/ 63 | }); 64 | 65 | loaders = this.config.resolve().module.loaders; 66 | 67 | expect(loaders[0]).to.have.property("loader", "my-loader"); 68 | }); 69 | 70 | it("should not assign the 'loader' property if a 'loader' property already exists", function() { 71 | var loaders; 72 | 73 | // This should cancel out the auto 'loader' assign. 74 | this.config.loader("my-loader", { 75 | test: /\.myldr/, 76 | loader: "some-other-loader" 77 | }); 78 | 79 | loaders = this.config.resolve().module.loaders; 80 | 81 | expect(loaders[0].loader).to.eq("some-other-loader"); 82 | }); 83 | 84 | it("should not assign the 'loader' property if a 'loaders' property already exists", function() { 85 | var loaders; 86 | 87 | // This should cancel out the auto 'loader' assign. 88 | this.config.loader("my-loader", { 89 | test: /\.myldr/, 90 | loaders: ["my-loader", "some-other-loader"] 91 | }); 92 | 93 | loaders = this.config.resolve().module.loaders; 94 | 95 | expect(loaders[0]).to.not.have.property("loader"); 96 | }); 97 | 98 | it("should enable function chaining by returning the config instance", function() { 99 | var config = this.config.loader("my-loader", {}); 100 | 101 | // Reference equality. 102 | expect(config).to.eq(this.config); 103 | }); 104 | 105 | describe("examples (using objects)", function() { 106 | 107 | it("should successfully create a simple loader", function() { 108 | this.config.loader("babel", { 109 | test: /\.jsx?$/, 110 | exclude: /node_modules/, 111 | query: { 112 | optional: ["runtime"], 113 | stage: 2 114 | } 115 | }); 116 | 117 | expect(this.config.resolve()).to.eql({ 118 | module: { 119 | loaders: [ 120 | { 121 | test: /\.jsx?$/, 122 | exclude: /node_modules/, 123 | loader: "babel", 124 | query: { 125 | optional: ["runtime"], 126 | stage: 2 127 | } 128 | } 129 | ] 130 | } 131 | }); 132 | 133 | }); 134 | 135 | it("should successfully merge configurations for loaders with the same name", function() { 136 | this.config 137 | .loader("babel", { 138 | test: /\.jsx?$/, 139 | exclude: /node_modules/, 140 | query: { 141 | optional: ["runtime"], 142 | stage: 2 143 | } 144 | }) 145 | .loader("babel", { 146 | query: { 147 | optional: ["es7.classProperties"] 148 | } 149 | }); 150 | 151 | expect(this.config.resolve()).to.eql({ 152 | module: { 153 | loaders: [ 154 | { 155 | test: /\.jsx?$/, 156 | exclude: /node_modules/, 157 | loader: "babel", 158 | query: { 159 | optional: ["runtime", "es7.classProperties"], 160 | stage: 2 161 | } 162 | } 163 | ] 164 | } 165 | }); 166 | 167 | }); 168 | 169 | it("should support the ExtractText plugin that wraps the value of the 'loader' property", function() { 170 | var ExtractTextPlugin = require("extract-text-webpack-plugin"); 171 | 172 | function extractTextResolver(config) { 173 | // Build up a loader string. 174 | var loaders = config.loaders.map(function(loader) { 175 | return loader.name + "?" + JSON.stringify(loader.query); 176 | }); 177 | 178 | config.loader = ExtractTextPlugin.extract(loaders.join("!")); 179 | 180 | // Clean up before resolving. 181 | delete config.loaders; 182 | 183 | // Return the correctly resolved sass-loader configuration. 184 | return config; 185 | } 186 | 187 | this.config.loader("sass", { 188 | test: /\.scss$/, 189 | exclude: /node_modules/, 190 | loaders: [ 191 | { 192 | name: "css", 193 | query: {} 194 | }, 195 | { 196 | name: "sass", 197 | query: {} 198 | } 199 | ] 200 | }, extractTextResolver); 201 | 202 | expect(this.config.resolve()).to.eql({ 203 | module: { 204 | loaders: [ 205 | { 206 | test: /\.scss$/, 207 | exclude: /node_modules/, 208 | loader: ExtractTextPlugin.extract("css?{}!sass?{}") 209 | } 210 | ] 211 | } 212 | }); 213 | }); 214 | 215 | }); 216 | 217 | }); 218 | -------------------------------------------------------------------------------- /test/merge.spec.js: -------------------------------------------------------------------------------- 1 | var Webpack = require("webpack"); 2 | var expect = require("chai").expect; 3 | var Config = require("../"); 4 | 5 | describe("merge", function() { 6 | 7 | beforeEach(function() { 8 | this.config = new Config(); 9 | }); 10 | 11 | it("should accept either an object or a function", function() { 12 | var config = this.config; 13 | var valid = [{}, function() {}, []]; 14 | var invalid = ["", true, false, 5, undefined, null]; 15 | 16 | valid.forEach(function(validParameter) { 17 | expect(function() { 18 | config.merge(validParameter); 19 | }).not.to.throw(); 20 | }); 21 | 22 | invalid.forEach(function(invalidParameter) { 23 | expect(function() { 24 | config.merge(invalidParameter); 25 | }).to.throw("Invalid Parameter. You must provide either an object or a function."); 26 | }); 27 | }); 28 | 29 | it("should provide a copy of the current configuration when a function is passed", function() { 30 | this.config.merge(function(current) { 31 | expect(current).to.exist; 32 | 33 | // Mutating this object should not cause any side effects unless returned. 34 | current.entry = "./main.js"; 35 | 36 | // Show the object isn't in any way connected. 37 | expect(this.config.resolve()).to.eql({}); 38 | 39 | // Completely ignore the current configuration to demonstrate the first claim. 40 | return { 41 | devtool: "inline-source-map" 42 | }; 43 | }.bind(this)); 44 | 45 | expect(this.config.resolve()).to.eql({ 46 | devtool: "inline-source-map" 47 | }); 48 | }); 49 | 50 | it("should reference (not clone) complex objects when a function is passed", function() { 51 | var definePlugin = new Webpack.DefinePlugin({ 52 | VERSION: JSON.stringify("1.0.0") 53 | }); 54 | var plugins; 55 | 56 | this.config.merge(function() { 57 | return { 58 | plugins: [ 59 | definePlugin 60 | ] 61 | }; 62 | }); 63 | 64 | plugins = this.config.resolve().plugins; 65 | 66 | // Ensure it's a reference and not a clone. 67 | expect(plugins[0]).to.eq(definePlugin); 68 | 69 | // Just to for sanity sake. If it was cloned, this would fail. 70 | expect(plugins[0]).not.to.eq( 71 | new Webpack.DefinePlugin({ 72 | VERSION: JSON.stringify("1.0.0") 73 | }) 74 | ); 75 | 76 | }); 77 | 78 | it("should return the config instance to allow chaining", function() { 79 | var config = this.config.merge({ 80 | entry: "./main.js" 81 | }); 82 | 83 | expect(config).to.eq(this.config); 84 | }); 85 | 86 | describe("examples (using objects)", function() { 87 | 88 | it("should successfully merge a simple configuration object", function() { 89 | var config = { 90 | entry: "./main.js", 91 | output: { 92 | filename: "bundle.js" 93 | } 94 | }; 95 | 96 | this.config.merge(config); 97 | 98 | // Shouldn't be a reference, just needs to deeply equal. 99 | expect(this.config.resolve()).to.eql(config); 100 | }); 101 | 102 | it("should allow multiple calls to config.merge", function() { 103 | this.config 104 | .merge({ 105 | entry: "./main.js", 106 | output: { 107 | filename: "bundle.js" 108 | } 109 | }) 110 | .merge({ 111 | output: { 112 | path: __dirname + "/dist" 113 | } 114 | }); 115 | 116 | expect(this.config.resolve()).to.eql({ 117 | entry: "./main.js", 118 | output: { 119 | path: __dirname + "/dist", 120 | filename: "bundle.js" 121 | } 122 | }); 123 | }); 124 | 125 | it("should merge arrays using concatenation", function() { 126 | this.config.merge({ 127 | entry: [ 128 | "screens/a.js", 129 | "screens/b.js", 130 | "screens/c.js" 131 | ], 132 | output: { 133 | filename: "bundle.js" 134 | } 135 | }); 136 | 137 | this.config.merge({ 138 | entry: [ 139 | "screens/d.js", 140 | "screens/e.js", 141 | "screens/f.js" 142 | ] 143 | }); 144 | 145 | expect(this.config.resolve()).to.eql({ 146 | entry: [ 147 | "screens/a.js", 148 | "screens/b.js", 149 | "screens/c.js", 150 | "screens/d.js", 151 | "screens/e.js", 152 | "screens/f.js" 153 | ], 154 | output: { 155 | filename: "bundle.js" 156 | } 157 | }); 158 | }); 159 | 160 | }); 161 | 162 | describe("examples (using functions)", function() { 163 | 164 | it("should successfully merge a simple configuration function", function() { 165 | this.config.merge(function(current) { 166 | current.entry = "./main.js", 167 | current.output = { 168 | filename: "bundle.js" 169 | }; 170 | 171 | return current; 172 | }); 173 | 174 | expect(this.config.resolve()).to.eql({ 175 | entry: "./main.js", 176 | output: { 177 | filename: "bundle.js" 178 | } 179 | }); 180 | }); 181 | 182 | it("should allow multiple calls to config.merge", function() { 183 | this.config 184 | .merge(function(current) { 185 | current.entry = "./main.js", 186 | current.output = { 187 | filename: "bundle.js" 188 | }; 189 | 190 | return current; 191 | }) 192 | .merge(function(current) { 193 | current.output.path = __dirname + "/dist"; 194 | 195 | return current; 196 | }); 197 | 198 | expect(this.config.resolve()).to.eql({ 199 | entry: "./main.js", 200 | output: { 201 | path: __dirname + "/dist", 202 | filename: "bundle.js" 203 | } 204 | }); 205 | }); 206 | 207 | it("should replace the current configuration with the returned value", function() { 208 | this.config 209 | .merge(function(current) { 210 | current.entry = "./main.js", 211 | current.output = { 212 | filename: "bundle.js" 213 | }; 214 | 215 | return current; 216 | }) 217 | .merge(function() { 218 | return { 219 | devtool: "inline-source-map" 220 | }; 221 | }); 222 | 223 | expect(this.config.resolve()).to.eql({ 224 | devtool: "inline-source-map" 225 | }); 226 | }); 227 | 228 | }); 229 | 230 | }); 231 | -------------------------------------------------------------------------------- /test/plugin.spec.js: -------------------------------------------------------------------------------- 1 | var Webpack = require("webpack"); 2 | var expect = require("chai").expect; 3 | var Config = require("../"); 4 | 5 | describe("plugin", function() { 6 | 7 | beforeEach(function() { 8 | this.config = new Config(); 9 | }); 10 | 11 | it("should require a name (that's a string) to enable referencing when merging", function() { 12 | var config = this.config; 13 | var invalid = [5, function() {}, [], null, undefined, {}]; 14 | 15 | invalid.forEach(function(invalidParameter) { 16 | expect(function() { 17 | config.plugin(invalidParameter); 18 | }).to.throw("Invalid 'name' parameter. You must provide a string."); 19 | }); 20 | 21 | expect(function() { 22 | config.plugin("my-plugin", function() {}); 23 | }).to.not.throw(); 24 | }); 25 | 26 | it("should accept a constructor that's either a function or null", function() { 27 | var config = this.config; 28 | var valid = [function() {}, null]; 29 | var invalid = ["", [], {}, true, false, 5, undefined]; 30 | 31 | valid.forEach(function(validParameter) { 32 | expect(function() { 33 | config.plugin("my-plugin", validParameter); 34 | }).not.to.throw(); 35 | }); 36 | 37 | invalid.forEach(function(invalidParameter) { 38 | expect(function() { 39 | config.plugin("my-plugin", invalidParameter); 40 | }).to.throw("Invalid 'constructor' parameter. You must provide either a function or null."); 41 | }); 42 | }); 43 | 44 | it("should accept an optional 'parameters' argument that's either a function or an array", function() { 45 | var config = this.config; 46 | var constructor = function() {}; 47 | var valid = [function() { return []; }, [], "", null, undefined, false]; 48 | var invalid = ["test", {}, true, 5]; 49 | 50 | valid.forEach(function(validParameter) { 51 | expect(function() { 52 | config.plugin("my-plugin", constructor, validParameter); 53 | }).not.to.throw(); 54 | }); 55 | 56 | invalid.forEach(function(invalidParameter) { 57 | expect(function() { 58 | config.plugin("my-plugin", constructor, invalidParameter); 59 | }).to.throw("The optional 'parameters' argument must be an array or a function."); 60 | }); 61 | }); 62 | 63 | it("should throw if 'parameters' is passed a function that doesn't return an array", function() { 64 | var config = this.config; 65 | var constructor = function() {}; 66 | var invalid = ["", {}, true, 5, null, undefined, false, function() {}]; 67 | 68 | expect(function() { 69 | config.plugin("my-plugin", constructor, function() { 70 | return []; 71 | }); 72 | }).to.not.throw(); 73 | 74 | invalid.forEach(function(invalidReturnValue, i) { 75 | expect(function() { 76 | config.plugin("my-plugin", constructor, function() { 77 | return invalidReturnValue; 78 | }); 79 | }).to.throw("The 'parameters' argument must return an array."); 80 | }); 81 | }); 82 | 83 | it("should enable function chaining by returning the config instance", function() { 84 | var config = this.config.plugin("my-plugin", function() {}); 85 | 86 | // Reference equality. 87 | expect(config).to.eq(this.config); 88 | }); 89 | 90 | it("should create a 'plugins' array property on the resolved config object", function() { 91 | var result; 92 | 93 | this.config.plugin("my-plugin", function() {}); 94 | 95 | result = this.config.resolve(); 96 | 97 | expect(result).to.have.property("plugins"); 98 | expect(result.plugins).to.be.an.instanceof(Array); 99 | expect(result.plugins.length).to.eq(1); 100 | }); 101 | 102 | it("should not throw if no parameters are passed", function() { 103 | var config = this.config; 104 | 105 | expect(function() { 106 | config 107 | .plugin("my-plugin", function() {}) 108 | .resolve(); 109 | }).to.not.throw(); 110 | }); 111 | 112 | it("should throw when resolving a plugin that has a null constuctor value", function() { 113 | var config = this.config; 114 | var config2 = new Config(); 115 | 116 | expect(function() { 117 | config 118 | .plugin("my-plugin", null) 119 | .resolve(); 120 | }).to.throw("Failed to resolve 'my-plugin'. Expected constructor not to be null."); 121 | 122 | // Ensure the error message is dynamic. 123 | expect(function() { 124 | config2 125 | .plugin("another-plugin", null) 126 | .resolve(); 127 | }).to.throw("Failed to resolve 'another-plugin'. Expected constructor not to be null."); 128 | }); 129 | 130 | describe("examples", function() { 131 | 132 | it("should successfully create a simple plugin", function() { 133 | var DefinePlugin = Webpack.DefinePlugin; 134 | var result; 135 | 136 | this.config.plugin("webpack-define", DefinePlugin, [{ 137 | VERSION: "1.0.0" 138 | }]); 139 | 140 | result = this.config.resolve(); 141 | 142 | expect(result.plugins[0]).to.be.instanceof(DefinePlugin); 143 | expect(result.plugins[0]).to.eql(new DefinePlugin({ 144 | VERSION: "1.0.0" 145 | })); 146 | }); 147 | 148 | it("should allow parameters to be modified after initial definition", function() { 149 | var DefinePlugin = Webpack.DefinePlugin; 150 | var result; 151 | 152 | this.config 153 | .plugin("webpack-define", DefinePlugin, [{ 154 | VERSION: "1.0.0" 155 | }]) 156 | .plugin("webpack-define", null, function(current) { 157 | current[0].APP_NAME = "My App"; 158 | 159 | return current; 160 | }); 161 | 162 | result = this.config.resolve(); 163 | 164 | expect(result.plugins[0]).to.be.instanceof(DefinePlugin); 165 | expect(result.plugins[0]).to.eql(new DefinePlugin({ 166 | VERSION: "1.0.0", 167 | APP_NAME: "My App" 168 | })); 169 | }); 170 | 171 | }); 172 | 173 | }); 174 | --------------------------------------------------------------------------------