diff --git a/README.md b/README.md index b25a301..e3da590 100644 --- a/README.md +++ b/README.md @@ -183,6 +183,7 @@ has(key) ```js // Returns an array of all the values stored in the Map. +// This will order values if before() or after() are used. // returns: Array values() ``` @@ -190,27 +191,39 @@ values() ```js // Returns an object of all the entries in the backing Map // where the key is the object property, and the value -// corresponding to the key. Will return `undefined` if the backing -// Map is empty. +// corresponding to the key. +// Will return `undefined` if the backing Map is empty. +// This will order keys if before() or after() are used. // returns: Object, undefined if empty entries() ```` ```js // Provide an object which maps its properties and values -// into the backing Map as keys and values. +// into the backing Map as keys and values. Can specify +// an array of keys to omit from the merge. // obj: Object -merge(obj) +// omit: Optional Array +merge(obj, omit) ``` ```js // Conditionally execute a function to continue configuration // condition: Boolean -// truthyHandler: Function -> ChainedMap +// whenTruthy: Function -> ChainedMap // invoked when condition is truthy, given a single argument of the ChainedMap instance -// falsyHandler: Function -> ChainedMap +// whenFalsy: Optional Function -> ChainedMap // invoked when condition is falsy, given a single argument of the ChainedMap instance -when(condition, truthyHandler, falsyHandler) +when(condition, whenTruthy, whenFalsy) +``` + +```js +// Returns an object containing the entries of the ChainedMap, +// as well as an array containing an ordering of the keys based +// on usage of before() and after(). Used internally by webpack-chain +// to generate the output for values() and entries(). +// Schema: { entries: Object, order: [String] } +order() ``` ## ChainedSet @@ -267,11 +280,11 @@ merge(arr) ```js // Conditionally execute a function to continue configuration // condition: Boolean -// truthyHandler: Function -> ChainedSet +// whenTruthy: Function -> ChainedSet // invoked when condition is truthy, given a single argument of the ChainedSet instance -// falsyHandler: Function -> ChainedSet +// whenFalsy: Optional Function -> ChainedSet // invoked when condition is falsy, given a single argument of the ChainedSet instance -when(condition, truthyHandler, falsyHandler) +when(condition, whenTruthy, whenFalsy) ``` ## Shorthand methods @@ -600,6 +613,48 @@ config .init((Plugin, args) => new Plugin(...args)); ``` +#### Config plugins: ordering before + +Specify that the current `plugin` context should operate before another named `plugin`. +You cannot use `before()` and `after()` on the same plugin. + +```js +config + .plugin(name) + .before(otherName) + +// Example + +config + .plugin('html-template') + .use(HtmlWebpackTemplate) + .end() + .plugin('script-ext') + .use(ScriptExtWebpackPlugin) + .before('html-template'); +``` + +#### Config plugins: ordering after + +Specify that the current `plugin` context should operate after another named `plugin`. +You cannot use `before()` and `after()` on the same plugin. + +```js +config + .plugin(name) + .after(otherName) + +// Example + +config + .plugin('html-template') + .after('script-ext') + .use(HtmlWebpackTemplate) + .end() + .plugin('script-ext') + .use(ScriptExtWebpackPlugin); +``` + #### Config resolve plugins ```js @@ -633,6 +688,48 @@ config.resolve .init((Plugin, args) => new Plugin(...args)) ``` +#### Config resolve plugins: ordering before + +Specify that the current `plugin` context should operate before another named `plugin`. +You cannot use `before()` and `after()` on the same plugin. + +```js +config.resolve + .plugin(name) + .before(otherName) + +// Example + +config.resolve + .plugin('beta') + .use(BetaWebpackPlugin) + .end() + .plugin('alpha') + .use(AlphaWebpackPlugin) + .before('beta'); +``` + +#### Config resolve plugins: ordering after + +Specify that the current `plugin` context should operate after another named `plugin`. +You cannot use `before()` and `after()` on the same plugin. + +```js +config.resolve + .plugin(name) + .after(otherName) + +// Example + +config.resolve + .plugin('beta') + .after('alpha') + .use(BetaWebpackTemplate) + .end() + .plugin('alpha') + .use(AlphaWebpackPlugin); +``` + #### Config node ```js @@ -750,6 +847,52 @@ config.module .options({ presets: ['babel-preset-es2015'] }); ``` +#### Config module rules uses (loaders): ordering before + +Specify that the current `use` context should operate before another named `use`. +You cannot use `before()` and `after()` on the same `use`. + +```js +config.module + .rule(name) + .use(name) + .before(otherName) + +// Example + +config.module + .rule('compile') + .use('babel') + .loader('babel-loader') + .end() + .use('cache') + .loader('cache-loader') + .before('babel'); +``` + +#### Config module rules uses (loaders): ordering after + +Specify that the current `use` context should operate after another named `use`. +You cannot use `before()` and `after()` on the same `use`. + +```js +config.module + .rule(name) + .use(name) + .after(otherName) + +// Example + +config.module + .rule('compile') + .use('babel') + .loader('babel-loader') + .after('cache') + .end() + .use('cache') + .loader('cache-loader'); +``` + #### Config module rules uses (loaders): modifying options ```js @@ -833,7 +976,9 @@ config.merge({ plugin: { [name]: { plugin: WebpackPlugin, - args: [...args] + args: [...args], + before, + after } }, @@ -891,7 +1036,9 @@ config.merge({ plugin: { [name]: { plugin: WebpackPlugin, - args: [...args] + args: [...args], + before, + after } } }, @@ -929,7 +1076,9 @@ config.merge({ use: { [name]: { loader: LoaderString, - options: LoaderOptions + options: LoaderOptions, + before, + after } } } diff --git a/src/ChainedMap.js b/src/ChainedMap.js index 9ad90ec..57951a8 100644 --- a/src/ChainedMap.js +++ b/src/ChainedMap.js @@ -1,6 +1,7 @@ const Chainable = require('./Chainable'); +const merge = require('deepmerge'); -module.exports = class extends Chainable { +class ChainedMap extends Chainable { constructor(parent) { super(parent); this.store = new Map(); @@ -23,21 +24,41 @@ module.exports = class extends Chainable { return this; } - entries() { - const entries = [...this.store]; - - if (!entries.length) { - return; - } - - return entries.reduce((acc, [key, value]) => { + order() { + const entries = [...this.store].reduce((acc, [key, value]) => { acc[key] = value; return acc; }, {}); + const names = Object.keys(entries); + const order = [...names]; + + names.forEach(name => { + const { __before, __after } = entries[name]; + + if (__before && order.includes(__before)) { + order.splice(order.indexOf(name), 1); + order.splice(order.indexOf(__before), 0, name); + } else if (__after && order.includes(__after)) { + order.splice(order.indexOf(name), 1); + order.splice(order.indexOf(__after) + 1, 0, name); + } + }); + + return { entries, order }; } values() { - return [...this.store.values()]; + const { entries, order } = this.order(); + + return order.map(name => entries[name]); + } + + entries() { + const { entries, order } = this.order(); + + if (order.length) { + return entries; + } } get(key) { @@ -53,8 +74,23 @@ module.exports = class extends Chainable { return this; } - merge(obj) { - Object.keys(obj).forEach(key => this.set(key, obj[key])); + merge(obj, omit = []) { + Object + .keys(obj) + .forEach(key => { + if (omit.includes(key)) { + return; + } + + const value = obj[key]; + + if ((!Array.isArray(value) && typeof value !== 'object') || value === null || !this.has(key)) { + this.set(key, value); + } else { + this.set(key, merge(this.get(key), value)); + } + }); + return this; } @@ -82,13 +118,43 @@ module.exports = class extends Chainable { }, {}); } - when(condition, trueBrancher = Function.prototype, falseBrancher = Function.prototype) { + when(condition, whenTruthy = Function.prototype, whenFalsy = Function.prototype) { if (condition) { - trueBrancher(this); + whenTruthy(this); } else { - falseBrancher(this); + whenFalsy(this); } return this; } +} + +ChainedMap.orderable = (Class) => class extends Class { + before(name) { + if (this.__after) { + throw new Error(`Unable to set before(${JSON.stringify(name)}) with existing value for after`); + } + + this.__before = name; + return this; + } + + after(name) { + if (this.__after) { + throw new Error(`Unable to set after(${JSON.stringify(name)}) with existing value for before`); + } + + this.__after = name; + return this; + } + + merge(obj, omit = []) { + if (obj.before && obj.after) { + throw new Error(`Unable to merge before: ${JSON.stringify(obj.before)} and after: ${JSON.stringify(obj.after)} on the same object`); + } + + return super.merge(obj, [...omit, 'before', 'after']); + } }; + +module.exports = ChainedMap; diff --git a/src/ChainedSet.js b/src/ChainedSet.js index d95c462..100a0c4 100644 --- a/src/ChainedSet.js +++ b/src/ChainedSet.js @@ -39,11 +39,11 @@ module.exports = class extends Chainable { return this; } - when(condition, trueBrancher = Function.prototype, falseBrancher = Function.prototype) { + when(condition, whenTruthy = Function.prototype, whenFalsy = Function.prototype) { if (condition) { - trueBrancher(this); + whenTruthy(this); } else { - falseBrancher(this); + whenFalsy(this); } return this; diff --git a/src/Config.js b/src/Config.js index ee2c779..44c707b 100644 --- a/src/Config.js +++ b/src/Config.js @@ -74,41 +74,33 @@ module.exports = class extends ChainedMap { })); } - merge(obj = {}) { - Object - .keys(obj) - .forEach(key => { - const value = obj[key]; + merge(obj = {}, omit = []) { + const omissions = [ + 'node', + 'output', + 'resolve', + 'resolveLoader', + 'devServer', + 'performance', + 'module' + ]; - switch (key) { - case 'node': - case 'output': - case 'resolve': - case 'resolveLoader': - case 'devServer': - case 'performance': - case 'module': { - return this[key].merge(value); - } - - case 'entry': { - return Object - .keys(value) - .forEach(name => this.entry(name).merge(value[name])); - } - - case 'plugin': { - return Object - .keys(value) - .forEach(name => this.plugin(name).merge(value[name])); - } + if (!omit.includes('entry') && obj.entry) { + Object + .keys(obj.entry) + .forEach(name => this.entry(name).merge(obj.entry[name])); + } else if (!omit.includes('plugin') && obj.plugin) { + Object + .keys(obj.plugin) + .forEach(name => this.plugin(name).merge(obj.plugin[name])); + } - default: { - this.set(key, value); - } - } - }); + omissions.forEach(key => { + if (!omit.includes(key) && obj[key]) { + this[key].merge(obj[key]); + } + }); - return this; + return super.merge(obj, [...omit, ...omissions, 'entry', 'plugin']); } }; diff --git a/src/DevServer.js b/src/DevServer.js index 4a84842..f9b24fc 100644 --- a/src/DevServer.js +++ b/src/DevServer.js @@ -54,27 +54,11 @@ module.exports = class extends ChainedMap { }, this.entries() || {})); } - merge(obj) { - Object - .keys(obj) - .forEach(key => { - const value = obj[key]; + merge(obj, omit = []) { + if (!omit.includes('allowedHosts') && obj.allowedHosts) { + this.allowedHosts.merge(obj.allowedHosts); + } - switch (key) { - case 'allowedHosts': { - return this[key].merge(value); - } - - default: { - if (this.has(key)) { - this.set(key, merge(this.get(key), value)); - } else { - this.set(key, value); - } - } - } - }); - - return this; + return super.merge(obj, ['allowedHosts']); } }; diff --git a/src/Module.js b/src/Module.js index d31b273..d3c5b3e 100644 --- a/src/Module.js +++ b/src/Module.js @@ -22,25 +22,13 @@ module.exports = class extends ChainedMap { })); } - merge(obj) { - Object - .keys(obj) - .forEach(key => { - const value = obj[key]; - - switch (key) { - case 'rule': { - return Object - .keys(value) - .forEach(name => this.rule(name).merge(value[name])); - } - - default: { - this.set(key, value); - } - } - }); + merge(obj, omit = []) { + if (!omit.includes('rule') && obj.rule) { + Object + .keys(obj.rule) + .forEach(name => this.rule(name).merge(obj.rule[name])); + } - return this; + return super.merge(obj, ['rule']); } }; diff --git a/src/Plugin.js b/src/Plugin.js index e9fb212..605372c 100644 --- a/src/Plugin.js +++ b/src/Plugin.js @@ -1,6 +1,6 @@ const ChainedMap = require('./ChainedMap'); -module.exports = class extends ChainedMap { +module.exports = ChainedMap.orderable(class extends ChainedMap { constructor(parent) { super(parent); this.extend(['init']); @@ -19,7 +19,7 @@ module.exports = class extends ChainedMap { return this; } - merge(obj) { + merge(obj, omit = []) { if (obj.plugin) { this.set('plugin', obj.plugin); } @@ -28,7 +28,7 @@ module.exports = class extends ChainedMap { this.set('args', obj.args); } - return this; + return super.merge(obj, [...omit, 'args', 'plugin']) } toConfig() { @@ -36,4 +36,4 @@ module.exports = class extends ChainedMap { return init(this.get('plugin'), this.get('args')); } -}; +}); diff --git a/src/Resolve.js b/src/Resolve.js index acc56da..14e933f 100644 --- a/src/Resolve.js +++ b/src/Resolve.js @@ -45,34 +45,24 @@ module.exports = class extends ChainedMap { })); } - merge(obj) { - Object - .keys(obj) - .forEach(key => { - const value = obj[key]; + merge(obj, omit = []) { + const omissions = [ + 'alias', + 'aliasFields', + 'descriptionFiles', + 'extensions', + 'mainFields', + 'mainFiles', + 'modules', + 'plugins' + ]; - switch (key) { - case 'alias': - case 'aliasFields': - case 'descriptionFiles': - case 'extensions': - case 'mainFields': - case 'mainFiles': - case 'modules': - case 'plugins': { - return this[key].merge(value); - } + omissions.forEach(key => { + if (!omit.includes(key) && obj[key]) { + this[key].merge(obj[key]); + } + }); - default: { - if (this.has(key)) { - this.set(key, merge(this.get(key), value)); - } else { - this.set(key, value); - } - } - } - }); - - return this; + return super.merge(obj, [...omit, ...omissions]); } }; diff --git a/src/ResolveLoader.js b/src/ResolveLoader.js index 6f1ce92..ffe230a 100644 --- a/src/ResolveLoader.js +++ b/src/ResolveLoader.js @@ -20,30 +20,20 @@ module.exports = class extends ChainedMap { }, this.entries() || {})); } - merge(obj) { - Object - .keys(obj) - .forEach(key => { - const value = obj[key]; + merge(obj, omit = []) { + const omissions = [ + 'extensions', + 'modules', + 'moduleExtensions', + 'packageMains' + ]; - switch (key) { - case 'extensions': - case 'modules': - case 'moduleExtensions': - case 'packageMains': { - return this[key].merge(value); - } + omissions.forEach(key => { + if (!omit.includes(key) && key in obj) { + this[key].merge(obj[key]); + } + }); - default: { - if (this.has(key)) { - this.set(key, merge(this.get(key), value)); - } else { - this.set(key, value); - } - } - } - }); - - return this; + return super.merge(obj, [...omit, ...omissions]); } }; diff --git a/src/Rule.js b/src/Rule.js index edbfe95..921e2e2 100644 --- a/src/Rule.js +++ b/src/Rule.js @@ -40,45 +40,36 @@ module.exports = class Rule extends ChainedMap { return this.clean(Object.assign(this.entries() || {}, { include: this.include.values(), exclude: this.exclude.values(), - oneOf: this.oneOfs.values().map(r => r.toConfig()), + oneOf: this.oneOfs.values().map(oneOf => oneOf.toConfig()), use: this.uses.values().map(use => use.toConfig()) })); } - merge(obj) { - Object - .keys(obj) - .forEach(key => { - const value = obj[key]; - - switch (key) { - case 'include': - case 'exclude': { - return this[key].merge(value); - } + merge(obj, omit = []) { + if (!omit.includes('include') && obj.include) { + this.include.merge(obj.include); + } - case 'use': { - return Object - .keys(value) - .forEach(name => this.use(name).merge(value[name])); - } + if (!omit.includes('exclude') && obj.exclude) { + this.exclude.merge(obj.exclude); + } - case 'oneOf': { - return Object - .keys(value) - .forEach(name => this.oneOf(name).merge(value[name])) - } + if (!omit.includes('use') && obj.use) { + Object + .keys(obj.use) + .forEach(name => this.use(name).merge(obj.use[name])); + } - case 'test': { - return this.test(value instanceof RegExp ? value : new RegExp(value)); - } + if (!omit.includes('oneOf') && obj.oneOf) { + Object + .keys(obj.oneOf) + .forEach(name => this.oneOf(name).merge(obj.oneOf[name])) + } - default: { - this.set(key, value); - } - } - }); + if (!omit.includes('test') && obj.test) { + this.test(obj.test instanceof RegExp ? obj.test : new RegExp(obj.test)); + } - return this; + return super.merge(obj, [...omit, 'include', 'exclude', 'use', 'oneOf', 'test']); } }; diff --git a/src/Use.js b/src/Use.js index dd131a1..4a0a733 100644 --- a/src/Use.js +++ b/src/Use.js @@ -1,7 +1,7 @@ const ChainedMap = require('./ChainedMap'); const merge = require('deepmerge'); -module.exports = class extends ChainedMap { +module.exports = ChainedMap.orderable(class extends ChainedMap { constructor(parent) { super(parent); this.extend(['loader', 'options']); @@ -12,19 +12,19 @@ module.exports = class extends ChainedMap { return this; } - merge(obj) { - if (obj.loader) { + merge(obj, omit = []) { + if (!omit.includes('loader') && obj.loader) { this.loader(obj.loader); } - if (obj.options) { + if (!omit.includes('options') && obj.options) { this.options(merge(this.store.get('options') || {}, obj.options)); } - return this; + return super.merge(obj, [...omit, 'loader', 'options']); } toConfig() { return this.clean(this.entries() || {}); } -}; +}); diff --git a/test/ChainedMap.js b/test/ChainedMap.js index d8e99cb..748d6e6 100644 --- a/test/ChainedMap.js +++ b/test/ChainedMap.js @@ -115,7 +115,7 @@ test('merge with overriding values', t => { map.set('b', 'delta'); t.is(map.merge(obj), map); - t.deepEqual(map.entries(), { a: 'alpha', b: 'beta', c: 'gamma' }); + t.deepEqual(map.entries(), obj); }); test('when true', t => { diff --git a/test/Rule.js b/test/Rule.js index 026ec83..4bddec3 100644 --- a/test/Rule.js +++ b/test/Rule.js @@ -113,6 +113,106 @@ test('toConfig with values', t => { }); }); +test('toConfig sorts before', t => { + const rule = new Rule(); + + rule + .use('babel') + .loader('babel-loader') + .options({ presets: ['alpha'] }) + .end() + .use('alpha') + .loader('alpha-loader') + .end() + .use('cache') + .loader('cache-loader') + .before('babel'); + + t.deepEqual(rule.toConfig(), { + use: [ + { + loader: 'cache-loader' + }, + { + loader: 'babel-loader', + options: { + presets: ['alpha'] + } + }, + { + loader: 'alpha-loader' + } + ] + }); +}); + +test('toConfig sorts after', t => { + const rule = new Rule(); + + rule + .use('babel') + .loader('babel-loader') + .options({ presets: ['alpha'] }) + .end() + .use('alpha') + .loader('alpha-loader') + .end() + .use('cache') + .loader('cache-loader') + .after('babel'); + + t.deepEqual(rule.toConfig(), { + use: [ + { + loader: 'babel-loader', + options: { + presets: ['alpha'] + } + }, + { + loader: 'cache-loader' + }, + { + loader: 'alpha-loader' + } + ] + }); +}); + +test('toConfig sorts before and after', t => { + const rule = new Rule(); + + rule + .use('babel') + .after('alpha') + .loader('babel-loader') + .options({ presets: ['alpha'] }) + .end() + .use('alpha') + .loader('alpha-loader') + .end() + .use('cache') + .loader('cache-loader') + .before('babel'); + + t.deepEqual(rule.toConfig(), { + use: [ + { + loader: 'alpha-loader' + }, + { + loader: 'cache-loader' + }, + { + loader: 'babel-loader', + options: { + presets: ['alpha'] + } + } + ] + }); +}); + test('merge empty', t => { const rule = new Rule(); const obj = { diff --git a/test/Use.js b/test/Use.js index 64ce722..4228fc3 100644 --- a/test/Use.js +++ b/test/Use.js @@ -34,3 +34,23 @@ test('tap', t => { t.deepEqual(use.store.get('options'), { presets: ['beta'] }); }); + +test('before', t => { + const use = new Use(); + const instance = use + .loader('babel-loader') + .before('cache-loader'); + + t.is(instance, use); + t.is(use.__before, 'cache-loader'); +}); + +test('after', t => { + const use = new Use(); + const instance = use + .loader('babel-loader') + .after('cache-loader'); + + t.is(instance, use); + t.is(use.__after, 'cache-loader'); +});