├── .eslintignore ├── .npmignore ├── .babelrc ├── .eslintrc ├── .gitignore ├── test ├── helper │ └── chai.js └── spec │ ├── .eslintrc │ ├── plugin.spec.js │ ├── output.spec.js │ ├── tap.spec.js │ ├── alias.spec.js │ ├── root.spec.js │ ├── entry.spec.js │ └── loader.spec.js ├── lib ├── tap.js ├── alias.js ├── output.js ├── index.js ├── root.js ├── plugin.js ├── entry.js └── loader.js ├── .travis.yml ├── package.json ├── .editorconfig └── README.md /.eslintignore: -------------------------------------------------------------------------------- 1 | *.js 2 | node_modules 3 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | lib 2 | node_modules 3 | test 4 | coverage 5 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "metalab" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "metalab/base" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules 3 | *.log 4 | coverage 5 | /*.js 6 | *.map 7 | -------------------------------------------------------------------------------- /test/helper/chai.js: -------------------------------------------------------------------------------- 1 | import chai from 'chai'; 2 | import sinon from 'sinon-chai'; 3 | 4 | chai.use(sinon); 5 | -------------------------------------------------------------------------------- /test/spec/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "mocha": true 4 | }, 5 | "rules": { 6 | "no-magic-numbers": 0 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /lib/tap.js: -------------------------------------------------------------------------------- 1 | import curry from 'lodash/curry'; 2 | 3 | export default curry((tap, config) => { 4 | tap(config); 5 | return config; 6 | }); 7 | -------------------------------------------------------------------------------- /lib/alias.js: -------------------------------------------------------------------------------- 1 | import curry from 'lodash/curry'; 2 | import set from 'lodash/fp/set'; 3 | 4 | export default curry((src, target, config) => 5 | set(['resolve', 'alias', src], target, config) 6 | ); 7 | -------------------------------------------------------------------------------- /lib/output.js: -------------------------------------------------------------------------------- 1 | import curry from 'lodash/curry'; 2 | 3 | export default curry((output, config) => { 4 | return { 5 | ...config, 6 | output: { 7 | ...config.output, 8 | ...output, 9 | }, 10 | }; 11 | }); 12 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | import entry from './entry'; 2 | import tap from './tap'; 3 | import plugin from './plugin'; 4 | import output from './output'; 5 | import loader from './loader'; 6 | import alias from './alias'; 7 | import root from './root'; 8 | 9 | export {alias, root, entry, tap, plugin, output, loader}; 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | matrix: 4 | include: 5 | - node_js: 4 6 | - node_js: 5 7 | - node_js: 6 8 | 9 | after_script: 10 | - npm install coveralls 11 | - cat ./coverage/coverage.json | ./node_modules/.bin/adana --format lcov | ./node_modules/coveralls/bin/coveralls.js 12 | -------------------------------------------------------------------------------- /test/spec/plugin.spec.js: -------------------------------------------------------------------------------- 1 | import {expect} from 'chai'; 2 | import plugin from '../../lib/plugin'; 3 | 4 | describe('plugin', () => { 5 | it('should add a plugin to the config', () => { 6 | const conf = {plugins: []}; 7 | expect(plugin({}, conf)).to.have.property('plugins') 8 | .to.have.length(1); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /test/spec/output.spec.js: -------------------------------------------------------------------------------- 1 | import {expect} from 'chai'; 2 | import output from '../../lib/output'; 3 | 4 | describe('output', () => { 5 | it('should update `output` in the config', () => { 6 | const conf = {output: {foo: 'a', bar: 'b'}}; 7 | const result = output({foo: 'x', baz: 'y'}, conf); 8 | expect(result).to.have.property('output').to.have.property('foo', 'x'); 9 | expect(result).to.have.property('output').to.have.property('baz', 'y'); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /lib/root.js: -------------------------------------------------------------------------------- 1 | import curry from 'lodash/curry'; 2 | import update from 'lodash/fp/update'; 3 | import path from 'path'; 4 | 5 | export default curry((root, config) => 6 | update(['resolve', 'root'], (existing) => { 7 | const x = root.charAt(0) === '/' ? root : path.join(config.context, root); 8 | if (Array.isArray(existing)) { 9 | return [...existing, x]; 10 | } else if (existing) { 11 | return [existing, x]; 12 | } 13 | return [x]; 14 | }, config) 15 | ); 16 | -------------------------------------------------------------------------------- /test/spec/tap.spec.js: -------------------------------------------------------------------------------- 1 | import {expect} from 'chai'; 2 | import sinon from 'sinon'; 3 | import tap from '../../lib/tap'; 4 | 5 | describe('tap', () => { 6 | it('should return the original config', () => { 7 | const conf = {message: 'hello'}; 8 | expect(tap(() => false, conf)).to.equal(conf); 9 | }); 10 | 11 | it('should call the given function', () => { 12 | const stub = sinon.stub(); 13 | tap(stub, 'test'); 14 | expect(stub).to.be.calledWith('test'); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /lib/plugin.js: -------------------------------------------------------------------------------- 1 | import curry from 'lodash/curry'; 2 | 3 | /** 4 | * Add a plugin to a webpack configuration. 5 | * @param {Object} plugin Plugin to add to the webpack configuration. 6 | * @param {Object} config Webpack configuration. 7 | * @returns {Object} New webpack configuration with the plugin added. 8 | */ 9 | export default curry((options, config) => { 10 | const {plugins = []} = config; 11 | return { 12 | ...config, 13 | plugins: [ 14 | ...plugins, 15 | options, 16 | ], 17 | }; 18 | }); 19 | -------------------------------------------------------------------------------- /test/spec/alias.spec.js: -------------------------------------------------------------------------------- 1 | import {expect} from 'chai'; 2 | import alias from '../../lib/alias'; 3 | 4 | describe('alias', () => { 5 | it('should handle empty configs', () => { 6 | expect(alias('foo', 'bar', {})) 7 | .to.have.property('resolve') 8 | .to.have.property('alias') 9 | .to.have.property('foo', 'bar'); 10 | }); 11 | 12 | it('should handle empty resolve options', () => { 13 | expect(alias('foo', 'bar', {resolve: {}})) 14 | .to.have.property('resolve') 15 | .to.have.property('alias') 16 | .to.have.property('foo', 'bar'); 17 | }); 18 | 19 | it('should overwrite by default', () => { 20 | expect(alias('foo', 'baz', {resolve: {alias: {foo: 'bar'}}})) 21 | .to.have.property('resolve') 22 | .to.have.property('alias') 23 | .to.have.property('foo', 'baz'); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /test/spec/root.spec.js: -------------------------------------------------------------------------------- 1 | import {expect} from 'chai'; 2 | import root from '../../lib/root'; 3 | 4 | describe('alias', () => { 5 | it('should handle empty configs', () => { 6 | expect(root('/foo', {})) 7 | .to.have.property('resolve') 8 | .to.have.property('root') 9 | .to.contain('/foo'); 10 | }); 11 | 12 | it('should handle empty resolve options', () => { 13 | expect(root('/foo', {resolve: {}})) 14 | .to.have.property('resolve') 15 | .to.have.property('root') 16 | .to.contain('/foo'); 17 | }); 18 | 19 | it('should join relative paths', () => { 20 | expect(root('foo', {context: '/baz'})) 21 | .to.have.property('resolve') 22 | .to.have.property('root') 23 | .to.contain('/baz/foo'); 24 | }); 25 | 26 | it('should append to single values', () => { 27 | expect(root('/foo', {resolve: {root: '/bar'}})) 28 | .to.have.property('resolve') 29 | .to.have.property('root') 30 | .to.contain('/foo') 31 | .to.contain('/bar'); 32 | }); 33 | 34 | it('should append to arrays', () => { 35 | expect(root('/foo', {resolve: {root: ['/bar']}})) 36 | .to.have.property('resolve') 37 | .to.have.property('root') 38 | .to.contain('/foo') 39 | .to.contain('/bar'); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webpack-partial", 3 | "version": "2.2.0", 4 | "author": "Izaak Schroeder ", 5 | "description": "Webpack partials.", 6 | "license": "CC0-1.0", 7 | "main": "index.js", 8 | "scripts": { 9 | "lint": "./node_modules/.bin/eslint .", 10 | "prepublish": "./node_modules/.bin/babel -s -d ./ lib", 11 | "spec": "NODE_ENV=test ./node_modules/.bin/mocha --compilers js:babel-core/register -r test/helper/chai -r adana-dump -R spec test/spec", 12 | "test": "npm run lint && npm run spec" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "https://github.com/izaakschroeder/webpack-partial" 17 | }, 18 | "bugs": { 19 | "url": "https://github.com/izaakschroeder/webpack-partial/issues" 20 | }, 21 | "keywords": [ 22 | "webpack", 23 | "partial", 24 | "include" 25 | ], 26 | "dependencies": { 27 | "lodash": "^4.12.0" 28 | }, 29 | "devDependencies": { 30 | "adana-cli": "^0.1.1", 31 | "adana-dump": "^0.1.0", 32 | "adana-format-lcov": "^0.1.1", 33 | "babel-cli": "^6.9.0", 34 | "babel-core": "^6.9.0", 35 | "babel-preset-metalab": "^0.2.1", 36 | "chai": "^3.5.0", 37 | "eslint": "^2.10.2", 38 | "eslint-config-metalab": "^4.0.1", 39 | "eslint-plugin-filenames": "^0.2.0", 40 | "eslint-plugin-import": "^1.8.0", 41 | "eslint-plugin-lodash-fp": "^1.2.0", 42 | "eslint-plugin-react": "^5.1.1", 43 | "mocha": "^2.4.5", 44 | "sinon": "^1.17.4", 45 | "sinon-chai": "^2.8.0", 46 | "webpack": "^3.10.0" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /lib/entry.js: -------------------------------------------------------------------------------- 1 | import curry from 'lodash/curry'; 2 | import isFunction from 'lodash/isFunction'; 3 | import isArray from 'lodash/isArray'; 4 | import isString from 'lodash/isString'; 5 | import isObject from 'lodash/isObject'; 6 | import isUndefined from 'lodash/isUndefined'; 7 | import castArray from 'lodash/castArray'; 8 | 9 | const _flatMap = (fn, key, entries) => { 10 | if (isString(entries)) { 11 | return fn([entries], key); 12 | } else if (isArray(entries)) { 13 | return fn(entries, key); 14 | } else if (isObject(entries)) { 15 | const res = {}; 16 | for (const key of Object.keys(entries)) { 17 | res[key] = _flatMap(fn, key, entries[key]); 18 | } 19 | return res; 20 | } 21 | else if (isUndefined(entries)) { 22 | return fn([], key); 23 | } 24 | throw new TypeError('Unknown shape of `entry` object.'); 25 | }; 26 | 27 | export const flatMap = (fn, config) => ({ 28 | ...config, 29 | entry: _flatMap(fn, null, config.entry), 30 | }); 31 | 32 | const normalize = (values, previous, key, config) => { 33 | if (isFunction(values)) { 34 | return values(previous, key, config); 35 | } 36 | return values; 37 | } 38 | 39 | export const append = curry((values, config) => 40 | flatMap((modules, key) => [ 41 | ...modules, 42 | ...castArray(normalize(values, modules, key, config)) 43 | ], config) 44 | ); 45 | 46 | export const prepend = curry((values, config) => 47 | flatMap((modules, key) => [ 48 | ...castArray(normalize(values, modules, key, config)), 49 | ...modules, 50 | ], config) 51 | ); 52 | 53 | export const replace = curry((values, config) => 54 | flatMap((modules, key) => 55 | normalize(values, modules, key, config), 56 | config 57 | ) 58 | ); 59 | 60 | replace.append = append; 61 | replace.prepend = prepend; 62 | 63 | export default replace; 64 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # 2 | # EditorConfig: http://EditorConfig.org 3 | # 4 | # This files specifies some basic editor conventions for the files in this 5 | # project. Many editors support this standard, you simply need to find a plugin 6 | # for your favorite! 7 | # 8 | # For a full list of possible values consult the reference. 9 | # https://github.com/editorconfig/editorconfig/wiki/EditorConfig-Properties 10 | # 11 | 12 | # Stop searching for other .editorconfig files above this folder. 13 | root = true 14 | 15 | # Pick some sane defaults for all files. 16 | [*] 17 | 18 | # UNIX line-endings are preferred. 19 | # http://adaptivepatchwork.com/2012/03/01/mind-the-end-of-your-line/ 20 | end_of_line = lf 21 | 22 | # No reason in these modern times to use anything other than UTF-8. 23 | charset = utf-8 24 | 25 | # Ensure that there's no bogus whitespace in the file. 26 | trim_trailing_whitespace = true 27 | 28 | # A little esoteric, but it's kind of a standard now. 29 | # http://stackoverflow.com/questions/729692/why-should-files-end-with-a-newline 30 | insert_final_newline = true 31 | 32 | # Pragmatism today. 33 | # http://programmers.stackexchange.com/questions/57 34 | indent_style = 2 35 | 36 | # Personal preference here. Smaller indent size means you can fit more on a line 37 | # which can be nice when there are lines with several indentations. 38 | indent_size = 2 39 | 40 | # Prefer a more conservative default line length – this allows editors with 41 | # sidebars, minimaps, etc. to show at least two documents side-by-side. 42 | # Hard wrapping by default for code is useful since many editors don't support 43 | # an elegant soft wrap; however, soft wrap is fine for things where text just 44 | # flows normally, like Markdown documents or git commit messages. Hard wrap 45 | # is also easier for line-based diffing tools to consume. 46 | # See: http://tex.stackexchange.com/questions/54140 47 | max_line_length = 80 48 | 49 | # Markdown uses trailing spaces to create line breaks. 50 | [*.md] 51 | trim_trailing_whitespace = false 52 | -------------------------------------------------------------------------------- /test/spec/entry.spec.js: -------------------------------------------------------------------------------- 1 | import {expect} from 'chai'; 2 | import {flatMap, append, prepend, replace} from '../../lib/entry'; 3 | 4 | describe('entry', () => { 5 | 6 | const configs = { 7 | string: { 8 | entry: 'a.js', 9 | }, 10 | array: { 11 | entry: ['a.js', 'b.js'], 12 | }, 13 | object: { 14 | entry: { 15 | a: 'a.js', 16 | b: ['b.js', 'c.js'], 17 | }, 18 | }, 19 | error: { 20 | entry: false, 21 | }, 22 | empty: { 23 | 24 | }, 25 | }; 26 | 27 | describe('flatMap', () => { 28 | it('should handle strings', () => { 29 | expect(flatMap( 30 | (entries) => [...entries, 'b.js', 'c.js'], 31 | configs.string 32 | )).to.have.property('entry').to.deep.equal([ 33 | 'a.js', 'b.js', 'c.js' 34 | ]); 35 | }); 36 | 37 | it('should handle arrays', () => { 38 | expect(flatMap( 39 | (entries) => [...entries, 'c.js', 'd.js'], 40 | configs.array 41 | )).to.have.property('entry').to.deep.equal([ 42 | 'a.js', 'b.js', 'c.js', 'd.js' 43 | ]); 44 | }); 45 | 46 | it('should handle objects', () => { 47 | expect(flatMap( 48 | (entries, key) => key === 'a' ? 49 | [...entries, 'b.js', 'c.js'] : ['a.js', ...entries], 50 | configs.object 51 | )) 52 | .to.have.property('entry') 53 | .to.deep.equal({ 54 | a: ['a.js', 'b.js', 'c.js'], 55 | b: ['a.js', 'b.js', 'c.js'], 56 | }) 57 | }); 58 | 59 | it('should throw on error cases', () => { 60 | expect(() => { 61 | flatMap(() => [], configs.error); 62 | }).to.throw(TypeError); 63 | }); 64 | }); 65 | 66 | describe('append', () => { 67 | it('should work', () => { 68 | expect(append( 69 | 'b.js', 70 | configs.string 71 | )).to.have.property('entry').to.deep.equal(['a.js', 'b.js']); 72 | }); 73 | it('should work on empty configs', () => { 74 | expect(append( 75 | 'a.js', 76 | configs.empty 77 | )).to.have.property('entry').to.deep.equal(['a.js']); 78 | }); 79 | }); 80 | 81 | describe('prepend', () => { 82 | it('should work', () => { 83 | expect(prepend( 84 | 'b.js', 85 | configs.string 86 | )).to.have.property('entry').to.deep.equal(['b.js', 'a.js']); 87 | }); 88 | }); 89 | 90 | describe('replace', () => { 91 | it('should work', () => { 92 | expect(replace( 93 | 'foo.js', 94 | configs.string 95 | )).to.have.property('entry').to.equal('foo.js'); 96 | }); 97 | it('should accept functions work', () => { 98 | expect(replace( 99 | () => 'foo.js', 100 | configs.string 101 | )).to.have.property('entry').to.equal('foo.js'); 102 | }); 103 | }); 104 | 105 | 106 | }); 107 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # webpack-partial 2 | 3 | Intelligently compose [webpack] configuration files. 4 | 5 | ![build status](http://img.shields.io/travis/webpack-config/webpack-partial/master.svg?style=flat) 6 | ![coverage](http://img.shields.io/coveralls/webpack-config/webpack-partial/master.svg?style=flat) 7 | ![license](http://img.shields.io/npm/l/webpack-partial.svg?style=flat) 8 | ![version](http://img.shields.io/npm/v/webpack-partial.svg?style=flat) 9 | ![downloads](http://img.shields.io/npm/dm/webpack-partial.svg?style=flat) 10 | 11 | 12 | ## Usage 13 | 14 | ```sh 15 | npm install --save webpack-partial 16 | ``` 17 | 18 | Making [webpack] configurations composable is tricky. So we provide several helpers to ease the composition process. Each helper is of the form `(arg1, arg2, ..., webpackConfig)`. All functions are curried and the last argument is always a webpack configuration object allowing you to chain helpers together easily with `compose` (or `flow`). 19 | 20 | Generally: 21 | 22 | ```javascript 23 | import compose from 'lodash/fp/compose'; 24 | import plugin from 'webpack-partial/plugin'; 25 | import StatsWebpackPlugin from 'stats-webpack-plugin'; 26 | import ExtractTextWebpackPlugin from 'extract-text-webpack-plugin'; 27 | 28 | const buildConfig = compose( 29 | plugin(new StatsWebpackPlugin()), 30 | plugin(new ExtractTextWebpackPlugin()) 31 | ); 32 | 33 | const config = buildConfig({/* webpack config here */}); 34 | ``` 35 | 36 | But you can also use them individually if you need to: 37 | 38 | ```javascript 39 | import plugin from 'webpack-partial/plugin'; 40 | import StatsWebpackPlugin from 'stats-webpack-plugin'; 41 | 42 | const config = plugin(new StatsWebpackPlugin(), {/* webpack config here */}); 43 | ``` 44 | 45 | The available helpers are: 46 | 47 | * entry 48 | * loader 49 | * output 50 | * plugin 51 | * tap 52 | 53 | ### entry(values, config) 54 | 55 | Modify the webpack config `entry` object. 56 | 57 | ```javascript 58 | // Set the `entry` to `index.js` 59 | entry('index.js'); 60 | 61 | // Append `foo.js` to all existing entrypoints. 62 | entry.append('foo.js'); 63 | entry((previous) => [...previous, 'foo.js']) 64 | ``` 65 | 66 | The `entry` function takes either a value to add to entry _or_ a function that maps the existing entry values to new ones. The values property is _always_ an array for consistency even though internally webpack can use objects, strings or arrays. 67 | 68 | The callback has this signature: 69 | 70 | ```javascript 71 | (previous: Array, key: ?String, config: Object) => { ... } 72 | ``` 73 | 74 | The `key` property represents the key in object-style entrypoints and `config` is the existing webpack configuration object. 75 | 76 | ### loader(loader, config) 77 | 78 | Add a loader configuration to an existing webpack configuration. 79 | 80 | ```javascript 81 | import loader from 'webpack-partial/loader'; 82 | 83 | const babel = loader({ 84 | test: /\.js$/, 85 | loader: 'babel-loader', 86 | }) 87 | babel(webpackConfig); 88 | ``` 89 | 90 | ### output(object, config) 91 | 92 | Merge the given output options into the output options in a webpack configuration. You can use it to do things like quickly change the `publicPath`. 93 | 94 | ```javascript 95 | import output from 'webpack-partial/output'; 96 | const rebase = output({publicPath: '/somewhere'}); 97 | rebase(webpackConfig); 98 | ``` 99 | 100 | ### plugin(object, config) 101 | 102 | Append a plugin to the existing plugins in a webpack configuration. 103 | 104 | ```javascript 105 | import plugin from 'webpack-partial/plugin'; 106 | import StatsWebpackPlugin from 'stats-webpack-plugin'; 107 | 108 | const stats = plugin(new StatsWebpackPlugin()) 109 | stats(webpackConfig); 110 | ``` 111 | 112 | ### tap(func, config) 113 | 114 | Observe the configuration but ignore any modifications. Can be useful to do things like dumping out a configuration. 115 | 116 | ```javascript 117 | import tap from 'webpack-partial/tap'; 118 | const debug = tap((config) => console.log(config)); 119 | debug(webpackConfig); 120 | ``` 121 | 122 | [webpack]: http://webpack.github.io/ 123 | -------------------------------------------------------------------------------- /lib/loader.js: -------------------------------------------------------------------------------- 1 | import curry from 'lodash/curry'; 2 | import findIndex from 'lodash/findIndex'; 3 | import u from 'lodash/fp/update'; 4 | import r from 'lodash/fp/remove'; 5 | import f from 'lodash/fp/find'; 6 | import set from 'lodash/fp/set'; 7 | import flow from 'lodash/fp/flow'; 8 | import isEqual from 'lodash/fp/isEqual'; 9 | import isMatch from 'lodash/fp/isMatch'; 10 | import isFunction from 'lodash/fp/isFunction'; 11 | import map from 'lodash/fp/map'; 12 | import getOr from 'lodash/fp/getOr'; 13 | 14 | export const __config = {webpackMajorVersion: 1, verify: false}; 15 | 16 | let verify = (x) => x; 17 | 18 | try { 19 | const pkg = require('webpack/package.json'); 20 | const [majorVersion] = pkg.version.match(/\d+\./); 21 | __config.webpackMajorVersion = parseInt(majorVersion); 22 | __config.verify = __config.webpackMajorVersion >= 2; 23 | } catch (err) /* adana: +ignore */ { 24 | // No webpack2 so RIP. 25 | } 26 | 27 | try { 28 | // Use webpack2 to automatically verify loaders when possible. This allows 29 | // us to catch errors early on when the configuration is made instead of 30 | // when it's used. 31 | const validate = require('webpack/lib/validateSchema'); 32 | const schema = require('webpack/schemas/webpackOptionsSchema.json'); 33 | const Err = require('webpack/lib/WebpackOptionsValidationError'); 34 | verify = (loader) => { 35 | if (!__config.verify || __config.webpackMajorVersion < 2) { 36 | return loader; 37 | } 38 | const config = { 39 | entry: 'stub.js', 40 | }; 41 | 42 | const errors = validate(schema, set( 43 | path(loader), 44 | [loader], 45 | config, 46 | )); 47 | if (errors.length) { 48 | throw new Err(errors); 49 | } 50 | return loader; 51 | }; 52 | } catch (err) /* adana: +ignore */ { 53 | // No webpack2 so RIP. 54 | } 55 | 56 | const normalize = (loader) => { 57 | if (__config.webpackMajorVersion > 1) { 58 | return loader; 59 | } 60 | if (!loader.loaders || loader.loaders.length === 0) { 61 | return loader; 62 | } 63 | return { 64 | ...loader, 65 | loaders: loader.loaders.map((entry) => { 66 | if (typeof entry === 'string') { 67 | return entry; 68 | } else if (typeof entry === 'object') { 69 | if (entry.query) { 70 | const s = JSON.stringify(entry.query); 71 | if (!isEqual(JSON.parse(s), entry.query)) { 72 | throw new TypeError(`Invalid query as part of ${entry.loader}.`); 73 | } 74 | return `${entry.loader}?${s}`; 75 | } 76 | return entry.loader; 77 | } 78 | throw new TypeError(); 79 | }), 80 | }; 81 | }; 82 | 83 | export const matcher = (loader) => { 84 | if (!isFunction(loader)) { 85 | throw new TypeError("Invalid match function."); 86 | } 87 | return loader; 88 | } 89 | 90 | export const remove = curry((loader, config) => { 91 | return u(path(loader), (all = []) => r(matcher(loader), all), config); 92 | }); 93 | 94 | export const update = curry((loader, fn, config) => { 95 | const m = matcher(loader); 96 | return u(path(loader), (all = []) => map((entry) => { 97 | return m(entry) ? verify(normalize(fn(entry, config))) : entry; 98 | }, all), config); 99 | }); 100 | 101 | export const find = curry((loader, config) => { 102 | return f(matcher(loader), getOr([], path(loader), config)); 103 | }); 104 | 105 | const path = (loader) => { 106 | // webpack 3, 4+ use module.rules 107 | if (__config.webpackMajorVersion > 2) { 108 | return ['module', 'rules']; 109 | } 110 | // webpack 2+ uses module.loaders and supports loader.enforce 111 | if (__config.webpackMajorVersion > 1) { 112 | return ['module', 'loaders']; 113 | } 114 | // webpack 1 has preLoaders and loaders 115 | if (loader.enforce === 'pre') { 116 | return ['module', 'preLoaders']; 117 | } 118 | return ['module', 'loaders']; 119 | } 120 | 121 | let counter = 0; 122 | 123 | const loader = (combine) => curry((loader, config) => { 124 | return u(path(loader), (all = []) => combine( 125 | all, 126 | verify(normalize(isFunction(loader) ? loader(config) : loader)), 127 | ), config); 128 | }); 129 | 130 | export const append = loader((all, next) => [...all, next]); 131 | 132 | export const prepend = loader((all, next) => [next, ...all]); 133 | 134 | export default Object.assign(append, { 135 | append, 136 | prepend, 137 | update, 138 | find, 139 | remove, 140 | }); 141 | -------------------------------------------------------------------------------- /test/spec/loader.spec.js: -------------------------------------------------------------------------------- 1 | import {expect} from 'chai'; 2 | import loader, { 3 | __config, 4 | update, 5 | remove, 6 | find, 7 | prepend, 8 | } from '../../lib/loader'; 9 | import flow from 'lodash/fp/flow'; 10 | import isMatch from "lodash/fp/isMatch"; 11 | 12 | describe('loader', () => { 13 | it('should handle empty configs', () => { 14 | expect(loader({test: /foo/}, {})) 15 | .to.have.property('module') 16 | .to.have.property('rules') 17 | .to.have.length(1); 18 | }); 19 | 20 | it('should handle empty module options', () => { 21 | expect(loader({test: /foo/}, {module: {}})) 22 | .to.have.property('module') 23 | .to.have.property('rules') 24 | .to.have.length(1); 25 | }); 26 | 27 | it('should append by default', () => { 28 | expect(loader({test: /foo/}, {module: {rules: [1]}})) 29 | .to.have.property('module') 30 | .to.have.property('rules') 31 | .to.have.length(2); 32 | }); 33 | 34 | it('should fail with extra properties', () => { 35 | expect(() => { 36 | loader( 37 | {name: 'x', color: 'green'}, 38 | {module: {rules: [{name: 'x', test: 'bar', color: 'red'}]}} 39 | ) 40 | }).to.throw(Error) 41 | }); 42 | 43 | describe('webpack@1', () => { 44 | let oldVersion; 45 | beforeEach(() => { 46 | oldVersion = __config.webpackMajorVersion; 47 | __config.webpackMajorVersion = 1; 48 | }); 49 | afterEach(() => { 50 | __config.webpackMajorVersion = oldVersion; 51 | }); 52 | 53 | it('should use `preLoaders` when `enforce` is `pre`', () => { 54 | expect(loader({test: /foo/, enforce: 'pre'}, {})) 55 | .to.have.property('module') 56 | .to.have.property('preLoaders') 57 | .to.have.length(1); 58 | }); 59 | 60 | it('should use `loaders` when no enforcement present', () => { 61 | expect(loader({test: /foo/}, {})) 62 | .to.have.property('module') 63 | .to.have.property('loaders') 64 | .to.have.length(1); 65 | }); 66 | 67 | it('should turn `loader` with multiple `loaders` into strings', () => { 68 | const result = loader({ 69 | test: /foo/, 70 | loaders: [ 71 | 'a', 72 | {loader: 'foo'}, 73 | {loader: 'bar', query: {message: 'hello'}} 74 | ], 75 | }, {}); 76 | const entry = result.module.loaders[0].loaders; 77 | expect(entry).to.have.length(3); 78 | expect(entry).to.have.property(0).to.equal('a'); 79 | expect(entry).to.have.property(1).to.equal('foo'); 80 | expect(entry).to.have.property(2).to.equal('bar?{"message":"hello"}'); 81 | }); 82 | 83 | it('should fail serializing functions', () => { 84 | expect(() => { 85 | loader( 86 | {loaders:[{loader: 'a', query: {x: () => {}}}]}, 87 | {} 88 | ) 89 | }).to.throw(TypeError); 90 | }); 91 | 92 | it('should fail on garbage in loaders', () => { 93 | expect(() => { 94 | loader( 95 | {loaders:[5]}, 96 | {} 97 | ) 98 | }).to.throw(TypeError); 99 | }); 100 | }); 101 | 102 | describe('update', () => { 103 | it('should update the matching target', () => { 104 | const result = flow( 105 | loader({ 106 | test: /foo/, 107 | loader: 'a', 108 | }), 109 | update(isMatch({loader: 'a'}), (loader) => { 110 | return { 111 | ...loader, 112 | loader: 'b', 113 | }; 114 | }), 115 | )({}); 116 | const entry = result.module.rules[0]; 117 | expect(entry).to.have.property('loader').to.equal('b'); 118 | }); 119 | it('should ignore non-matching targets', () => { 120 | const result = flow( 121 | loader({ 122 | test: /foo/, 123 | loader: 'a', 124 | }), 125 | update(isMatch({loader: 'z'}), (loader) => { 126 | return { 127 | ...loader, 128 | loader: 'b', 129 | }; 130 | }), 131 | )({}); 132 | const entry = result.module.rules[0]; 133 | expect(entry).to.have.property('loader').to.equal('a'); 134 | }); 135 | }); 136 | 137 | describe('remove', () => { 138 | it('should remove the matching target', () => { 139 | const result = flow( 140 | loader({ 141 | test: /foo/, 142 | loader: 'a', 143 | }), 144 | remove(isMatch({loader: 'a'})) 145 | )({}); 146 | const entry = expect(result.module.rules).to.have.length(0); 147 | }); 148 | it('should ignore non-matching targets', () => { 149 | const result = flow( 150 | loader({ 151 | test: /foo/, 152 | loader: 'a', 153 | }), 154 | remove(isMatch({loader: 'z'})), 155 | )({}); 156 | expect(result.module.rules).to.have.length(1); 157 | }); 158 | }); 159 | 160 | describe('find', () => { 161 | it('should find things', () => { 162 | const result = loader({ 163 | test: /foo/, 164 | loader: 'a', 165 | }, {}); 166 | expect( 167 | find(isMatch({loader: 'a'}), result) 168 | ).to.have.property('loader', 'a'); 169 | }); 170 | it('should fail on bad matcher', () => { 171 | expect(() => loader.find('potato', {})).to.throw(TypeError); 172 | }); 173 | }); 174 | 175 | describe('prepend', () => { 176 | it('should prepend things', () => { 177 | const result = prepend({ 178 | test: /foo/, 179 | loader: 'a', 180 | }, {module: {loaders: [{name: 'x', test: 'bar', color: 'red'}]}}); 181 | expect(result) 182 | .to.have.property('module') 183 | .to.have.property('rules') 184 | .to.have.property(0) 185 | .to.have.property('loader', 'a'); 186 | }); 187 | }); 188 | 189 | describe('functions', () => { 190 | it('should let you use functions for configs', () => { 191 | expect(loader(() => ({test: /foo/}), {})) 192 | .to.have.property('module') 193 | .to.have.property('rules') 194 | .to.have.length(1); 195 | }); 196 | }); 197 | }); 198 | --------------------------------------------------------------------------------