├── .editorconfig ├── .gitignore ├── .lintstagedrc.yml ├── .travis.yml ├── LICENSE ├── README.md ├── babel.config.json ├── package.json ├── rollup.config.js ├── src ├── external-to-fn.js ├── external-to-fn.spec.js ├── get-deps.js ├── get-modules-matcher.js ├── get-modules-matcher.spec.js └── index.js └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | 8 | [*] 9 | 10 | # Change these settings to your own preference 11 | indent_style = space 12 | indent_size = 2 13 | 14 | # We recommend you to keep these unchanged 15 | end_of_line = lf 16 | charset = utf-8 17 | trim_trailing_whitespace = true 18 | insert_final_newline = true 19 | 20 | [*.md] 21 | trim_trailing_whitespace = false 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ 3 | .idea/ 4 | -------------------------------------------------------------------------------- /.lintstagedrc.yml: -------------------------------------------------------------------------------- 1 | /*.{js,css}: 2 | - yarn format 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 10 4 | notifications: 5 | email: false 6 | before_install: 7 | - curl -o- -L https://yarnpkg.com/install.sh | bash 8 | - export PATH="$HOME/.yarn/bin:$PATH" 9 | after_success: 10 | - npx semantic-release-github-pr 11 | - npx semantic-release --debug 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 pmowrer 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Build Status](https://travis-ci.org/pmowrer/rollup-plugin-peer-deps-external.svg?branch=master) [![npm](https://img.shields.io/npm/v/rollup-plugin-peer-deps-external.svg)](https://www.npmjs.com/package/rollup-plugin-peer-deps-external) [![semantic-release](https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg)](https://github.com/semantic-release/semantic-release) 2 | 3 | # Rollup Plugin Peer Deps External 4 | Automatically externalize `peerDependencies` in a `rollup` bundle. 5 | 6 | ## Motivation 7 | When bundling a library using [`rollup`](https://github.com/rollup/rollup), we generally want to keep from including [`peerDependencies`](https://nodejs.org/en/blog/npm/peer-dependencies/) since they are expected to be provided by the consumer of the library. By excluding these dependencies, we keep bundle size down and avoid bundling duplicate dependencies. 8 | 9 | We can achieve this using the rollup [`external`](https://github.com/rollup/rollup/wiki/JavaScript-API#external) configuration option, providing it a list of the peer dependencies to exclude from the bundle. This plugin automates the process, automatically adding a library's `peerDependencies` to the `external` configuration. 10 | 11 | ## Installation 12 | ```bash 13 | npm install --save-dev rollup-plugin-peer-deps-external 14 | ``` 15 | 16 | ## Usage 17 | ```javascript 18 | // Add to plugins array in rollup.config.js 19 | import peerDepsExternal from 'rollup-plugin-peer-deps-external'; 20 | 21 | export default { 22 | plugins: [ 23 | // Preferably set as first plugin. 24 | peerDepsExternal(), 25 | ], 26 | } 27 | ``` 28 | 29 | ## Options 30 | ### packageJsonPath 31 | If your `package.json` is not in the current working directory you can specify the path to the file 32 | ```javascript 33 | // Add to plugins array in rollup.config.js 34 | import peerDepsExternal from 'rollup-plugin-peer-deps-external'; 35 | 36 | export default { 37 | plugins: [ 38 | // Preferably set as first plugin. 39 | peerDepsExternal({ 40 | packageJsonPath: 'my/folder/package.json' 41 | }), 42 | ], 43 | } 44 | ``` 45 | 46 | ### includeDependencies \*\**deprecated*\*\* 47 | Set `includeDependencies` to `true` to also externalize regular dependencies in addition to peer deps. 48 | 49 | ```javascript 50 | // Add to plugins array in rollup.config.js 51 | import peerDepsExternal from 'rollup-plugin-peer-deps-external'; 52 | 53 | export default { 54 | plugins: [ 55 | // Preferably set as first plugin. 56 | peerDepsExternal({ 57 | includeDependencies: true, 58 | }), 59 | ], 60 | } 61 | ``` 62 | 63 | ## Module paths 64 | This plugin is compatible with module path format applied by, for example, [`babel-plugin-lodash`](https://github.com/lodash/babel-plugin-lodash). For any module name in `peerDependencies`, all paths beginning with that module name will also be added to `external`. 65 | 66 | E.g.: If `lodash` is in `peerDependencies`, an import of `lodash/map` would be added to externals. 67 | -------------------------------------------------------------------------------- /babel.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/preset-env", 5 | { 6 | "modules": false 7 | } 8 | ] 9 | ], 10 | "env": { 11 | "test": { 12 | "presets": [ 13 | [ 14 | "@babel/preset-env" 15 | ] 16 | ] 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rollup-plugin-peer-deps-external", 3 | "version": "0.0.0-development", 4 | "description": "Rollup plugin to automatically add a library's peerDependencies to its bundle's external config.", 5 | "repository": "pmowrer/rollup-plugin-peer-deps-external", 6 | "keywords": [ 7 | "rollup", 8 | "plugin", 9 | "rollup-plugin", 10 | "peerDependencies", 11 | "external", 12 | "optimize", 13 | "exclude" 14 | ], 15 | "files": [ 16 | "src", 17 | "dist", 18 | "README.md", 19 | "LICENSE" 20 | ], 21 | "scripts": { 22 | "clear": "rimraf ./dist", 23 | "build": "rollup -c", 24 | "format": "prettier --write --single-quote --trailing-comma es5", 25 | "format:all": "yarn format \"src/**/*.js\"", 26 | "prepublishOnly": "yarn test && yarn build", 27 | "test": "jest" 28 | }, 29 | "main": "./dist/rollup-plugin-peer-deps-external.js", 30 | "module": "./dist/rollup-plugin-peer-deps-external.module.js", 31 | "license": "MIT", 32 | "peerDependencies": { 33 | "rollup": "*" 34 | }, 35 | "devDependencies": { 36 | "@babel/core": "^7.8.4", 37 | "@babel/preset-env": "^7.8.4", 38 | "@rollup/plugin-babel": "^5.0.2", 39 | "@rollup/plugin-node-resolve": "^8.0.0", 40 | "husky": "^4.2.1", 41 | "jest": "^25.1.0", 42 | "lint-staged": "^10.0.7", 43 | "lodash-es": "^4.17.15", 44 | "prettier": "^1.19.1", 45 | "ramda": "^0.26.1", 46 | "rimraf": "^3.0.1", 47 | "rollup": "^2.13.1", 48 | "semantic-release": "^17.0.2", 49 | "semantic-release-github-pr": "^6.0.0" 50 | }, 51 | "jest": { 52 | "transformIgnorePatterns": [ 53 | "/node_modules/(?!lodash-es/)" 54 | ] 55 | }, 56 | "husky": { 57 | "hooks": { 58 | "pre-commit": "lint-staged" 59 | } 60 | }, 61 | "lint-staged": { 62 | "*.js": [ 63 | "yarn format" 64 | ] 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import babel from '@rollup/plugin-babel'; 2 | import resolve from '@rollup/plugin-node-resolve'; 3 | 4 | export default { 5 | input: './src/index.js', 6 | plugins: [ 7 | resolve(), 8 | babel({ 9 | include: 'src/**/*.js', 10 | exclude: 'node_modules/**', 11 | }), 12 | ], 13 | output: [ 14 | { 15 | file: './dist/rollup-plugin-peer-deps-external.js', 16 | format: 'cjs', 17 | }, 18 | { 19 | file: './dist/rollup-plugin-peer-deps-external.module.js', 20 | format: 'es', 21 | } 22 | ], 23 | } 24 | -------------------------------------------------------------------------------- /src/external-to-fn.js: -------------------------------------------------------------------------------- 1 | import { isFunction } from 'lodash-es'; 2 | 3 | /** 4 | * Utility function mapping a Rollup config's `external` option into a function. 5 | 6 | * In Rollup, the `external` config option can be provided as "either a function 7 | * that takes an id and returns true (external) or false (not external), or an 8 | * Array of module IDs, or regular expressions to match module IDs, that should 9 | * remain external to the bundle. Can also be just a single ID or regular 10 | * expression." (https://rollupjs.org/guide/en/#external) 11 | * 12 | * An `external` configuration in string/regexp/array format can be represented 13 | * in the function format, but not vice-versa. This utility accepts either format 14 | * and returns the function representation such that we can easily retain the user's 15 | * configuration while simultaneously appending peer dependencies to it. 16 | * 17 | * @param {String|RegExp|Array|Function} external The `external` property from Rollup's config. 18 | * @returns {Function} Function equivalent of the passed in `external`. 19 | */ 20 | export default function externalToFn(external) { 21 | if (isFunction(external)) { 22 | return external; 23 | } else if (typeof external === 'string') { 24 | return id => external === id; 25 | } else if (external instanceof RegExp) { 26 | return id => external.test(id); 27 | } else if (Array.isArray(external)) { 28 | return id => 29 | external.some(module => 30 | module instanceof RegExp ? module.test(id) : module === id 31 | ); 32 | } 33 | // Per the rollup docs, `undefined` isn't a valid value for the `external` option, 34 | // but it has been reported to have been passed in configs starting with 2.11.0. 35 | // It's unclear why it's happening so we'll support it for now: 36 | // https://github.com/pmowrer/rollup-plugin-peer-deps-external/issues/29 37 | else if (typeof external === 'undefined') { 38 | return () => false; 39 | } else { 40 | throw new Error( 41 | `rollup-plugin-peer-deps-external: 'external' option must be a function or an array.` 42 | ); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/external-to-fn.spec.js: -------------------------------------------------------------------------------- 1 | import externalToFn from './external-to-fn'; 2 | 3 | describe('#externalToFn', () => { 4 | describe('when passed a function', () => { 5 | it('returns the same function', () => { 6 | const fn = () => true; 7 | expect(externalToFn(fn)).toBe(fn); 8 | }); 9 | }); 10 | 11 | describe('when passed an a module name', () => { 12 | it('returns a predicate returning true if passed the module name', () => { 13 | const fn = externalToFn('lodash'); 14 | 15 | expect(fn('lodash')).toBe(true); 16 | expect(fn('lodash-')).toBe(false); 17 | expect(fn('lodash-es')).toBe(false); 18 | expect(fn('lodash/map')).toBe(false); 19 | }); 20 | }); 21 | 22 | describe('when passed a regex name', () => { 23 | it('returns a predicate returning true if passed a module name matching the regex', () => { 24 | const fn = externalToFn(/-/); 25 | 26 | expect(fn('lodash')).toBe(false); 27 | expect(fn('lodash-')).toBe(true); 28 | expect(fn('lodash-es')).toBe(true); 29 | expect(fn('lodash/map')).toBe(false); 30 | }); 31 | }); 32 | 33 | describe('when passed an array of module names', () => { 34 | it('returns a predicate returning true if passed one of the module names', () => { 35 | const modules = ['lodash', 'lodash-es']; 36 | const fn = externalToFn(modules); 37 | 38 | expect(fn('lodash')).toBe(true); 39 | expect(fn('lodash-')).toBe(false); 40 | expect(fn('lodash-es')).toBe(true); 41 | expect(fn('lodash/map')).toBe(false); 42 | }); 43 | }); 44 | 45 | describe('when passed an array of regexes', () => { 46 | it('returns a predicate returning true if passed a module name matching one of the regexes', () => { 47 | const regexes = [/es/, /\//]; 48 | const fn = externalToFn(regexes); 49 | 50 | expect(fn('lodash')).toBe(false); 51 | expect(fn('lodash-')).toBe(false); 52 | expect(fn('lodash-es')).toBe(true); 53 | expect(fn('lodash/map')).toBe(true); 54 | }); 55 | }); 56 | 57 | describe('when passed undefined', () => { 58 | it('returns false', () => { 59 | const fn = externalToFn(undefined); 60 | 61 | expect(fn('lodash')).toBe(false); 62 | expect(fn('lodash-')).toBe(false); 63 | expect(fn('lodash-es')).toBe(false); 64 | expect(fn('lodash/map')).toBe(false); 65 | }); 66 | }); 67 | 68 | describe('when passed anything else', () => { 69 | it('throws an error', () => { 70 | expect(() => externalToFn(null)).toThrow(); 71 | }); 72 | }); 73 | }); 74 | -------------------------------------------------------------------------------- /src/get-deps.js: -------------------------------------------------------------------------------- 1 | const { resolve } = require('path'); 2 | 3 | export default function getDeps( 4 | path = resolve(process.cwd(), 'package.json'), 5 | type = 'peerDependencies' 6 | ) { 7 | try { 8 | const pkg = require(path); 9 | return Object.keys(pkg[type]); 10 | } catch (err) { 11 | return []; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/get-modules-matcher.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Creates a test function from a list of module names. The resulting function 3 | * accepts a string id and returns whether it matches a module in the list. 4 | * 5 | * The string id can be a module name (e.g. `lodash`) or a 6 | * "module path" (e.g. `lodash/map`). 7 | * 8 | * @param {Array} modulesNames Array of module names to match against. 9 | * @returns {function(String): (boolean)} Predicate function accepting a string id. 10 | */ 11 | export default function getModulesMatcher(modulesNames) { 12 | const regexps = modulesNames.map(moduleRegExp); 13 | return id => regexps.some(regexp => regexp.test(id)); 14 | } 15 | 16 | const moduleRegExp = module => new RegExp(`^${module}(\\/\.+)*$`); 17 | -------------------------------------------------------------------------------- /src/get-modules-matcher.spec.js: -------------------------------------------------------------------------------- 1 | import getModulesMatcher from './get-modules-matcher'; 2 | 3 | describe('#getModulesMatcher', () => { 4 | describe('when passed an empty array', () => { 5 | it('returns a predicate that always returns false', () => { 6 | const fn = getModulesMatcher([]); 7 | 8 | expect(fn('lodash')).toBe(false); 9 | expect(fn('lodash/map')).toBe(false); 10 | }); 11 | }); 12 | 13 | describe('when passed an array of module names', () => { 14 | it('returns a predicate that returns true for matching modules', () => { 15 | const fn = getModulesMatcher(['lodash', 'ramda', 'antd']); 16 | 17 | expect(fn('lodash')).toBe(true); 18 | expect(fn('lodash/map')).toBe(true); 19 | expect(fn('lodash/object/pick')).toBe(true); 20 | expect(fn('ramda')).toBe(true); 21 | expect(fn('antd/lib/date-picker')).toBe(true); 22 | expect(fn('babel-plugin-lodash')).toBe(false); 23 | expect(fn('lodash-es')).toBe(false); 24 | }); 25 | }); 26 | 27 | describe('when passed anything but an array', () => { 28 | it('throws an error', () => { 29 | expect(() => getModulesMatcher()).toThrow(); 30 | expect(() => getModulesMatcher(null)).toThrow(); 31 | expect(() => getModulesMatcher('string')).toThrow(); 32 | }); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import { either, pipe } from 'ramda'; 2 | import externalToFn from './external-to-fn'; 3 | import getModulesMatcher from './get-modules-matcher'; 4 | import getDeps from './get-deps'; 5 | 6 | export default function PeerDepsExternalPlugin({ 7 | packageJsonPath, 8 | includeDependencies, 9 | } = {}) { 10 | return { 11 | name: 'peer-deps-external', 12 | options: opts => { 13 | opts.external = either( 14 | // Retain existing `external` config 15 | externalToFn(opts.external), 16 | // Add `peerDependencies` to `external` config 17 | getModulesMatcher( 18 | getDeps(packageJsonPath, 'peerDependencies').concat( 19 | includeDependencies ? getDeps(packageJsonPath, 'dependencies') : [] 20 | ) 21 | ) 22 | ); 23 | 24 | return opts; 25 | }, 26 | }; 27 | } 28 | --------------------------------------------------------------------------------