├── .babelrc ├── .npmignore ├── .travis.yml ├── README.md ├── lib ├── .gitignore └── .npmignore ├── package.json ├── src └── index.js └── test ├── .babelrc ├── README.md ├── dist └── .gitignore ├── package.json └── src └── index.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["stage-0", "es2015"], 3 | "ignore": [ 4 | "node_modules" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | * 2 | !lib 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "4.2" 4 | - "stable" 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # babel-plugin-discard-module-references [![Build Status](https://travis-ci.org/ArnaudRinquin/babel-plugin-discard-module-references.svg)](https://travis-ci.org/ArnaudRinquin/babel-plugin-discard-module-references) 2 | 3 | Babel plugin to discard all code using specified imported modules. 4 | 5 | If other imported modules are not used anymore, they are discarded as well. 6 | 7 | ## Use cases 8 | 9 | * write your tests along your code, run them in development but discard them on production 10 | * discard analytics code in dev mode 11 | * _???_ 12 | 13 | ## Usage 14 | 15 | 1. Install the plugin 16 | 17 | ```bash 18 | npm i -D babel-plugin-discard-module-references 19 | ``` 20 | 1. Update your `.babelrc` with plugin settings 21 | 22 | ```json 23 | { 24 | "presets": ["es2015"], 25 | "plugins": [ 26 | ["discard-module-references", { 27 | "targets": [ "some-module", "./or-even/relative-path" ] 28 | }] 29 | ] 30 | } 31 | ``` 32 | 33 | You can restrict the plugin to specific environments (like, `NODE_ENV=production`) using babel `env` config: 34 | 35 | ```json 36 | { 37 | "presets": ["es2015"], 38 | "env": { 39 | "production": { 40 | "plugins": [ 41 | ["discard-module-references", { 42 | "targets": [ "my-test-framework" ] 43 | }] 44 | ] 45 | } 46 | } 47 | } 48 | ``` 49 | 50 | 1. ... or any config you're using, seek help from [doc](https://babeljs.io/docs/setup/) 51 | 52 | ### Whitelisting unused imports 53 | 54 | By default, all unused module imports will be discarded, wether or not it's because you target the only code that were using them. By example, if you import `sinon` for you tests but discard all of them, `sinon` becomes useless and gets discarded as well. 55 | 56 | There is a potential issue with that when a module has expected side effects when imported. 57 | 58 | To whitelist a module so its import never gets discarded, simply use the `unusedWhitelist` options: 59 | 60 | ```json 61 | { 62 | "presets": ["es2015"], 63 | "plugins": [ 64 | ["discard-module-references", { 65 | "targets": [ "assert" ], 66 | "unusedWhitelist": [ "sinon" ] 67 | }] 68 | ] 69 | } 70 | ``` 71 | 72 | Note: unspecified `imports` such as `import 'foobar';` are kept by default as they obviously must have some expected side effects. 73 | 74 | **Note for React with JSX** 75 | 76 | If you're using React with JSX, you will probably need to whitelist `react`. 77 | 78 | Explanation: When using babel with JSX, you need to have `import React from 'react'` in your files because JSX will be converted to `React.doSomething()` call. This happens after the plugin runs, as a result, the `import` will be discarded as it is seen as unused your app will fail with `React is undefined`. 79 | 80 | Just whitelist it and you'll be fine: 81 | 82 | ```json 83 | { 84 | "presets": ["es2015", "react"], 85 | "env": { 86 | "production": { 87 | "plugins": [ 88 | ["discard-module-references", { 89 | "targets": [ "tape" ], 90 | "unusedWhitelist": [ "react" ] 91 | }] 92 | ] 93 | } 94 | } 95 | } 96 | ``` 97 | 98 | ## Example 99 | 100 | ### Writing tests right in the tested code file 101 | 102 | The original scenario that motivated the plugin was to be able to write tests along tested code, run them in development mode (so we don't need to run another tool, just use the code and see if it breaks) but of course remove all of them for production code. 103 | 104 | With the following code, a production build that would use `babel-plugin-discard-module-references` with `assert` would just do the trick. 105 | 106 | ```js 107 | import assert, { deepEqual } from 'assert'; 108 | import _ from 'lodash'; 109 | import path from 'path'; 110 | 111 | export default function add(n1, n2) { 112 | return n1 + n2; 113 | } 114 | 115 | function doSomethingWithLodash() { 116 | return _.pick({nose: 'big'}, 'nose'); 117 | } 118 | 119 | assert(add(1, 2) === 3); 120 | assert.equal(typeof add, 'function'); 121 | deepEqual({a:1}, {a:1}); 122 | assert(path.basename('foo/bar.html') === 'something'); 123 | ``` 124 | 125 | Would be compiled to the following, where all tests are removed; 126 | 127 | ```js 128 | 'use strict'; 129 | 130 | Object.defineProperty(exports, "__esModule", { 131 | value: true 132 | }); 133 | exports.default = add; 134 | 135 | var _lodash = require('lodash'); 136 | 137 | var _lodash2 = _interopRequireDefault(_lodash); 138 | 139 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 140 | 141 | function add(n1, n2) { 142 | return n1 + n2; 143 | } 144 | 145 | function doSomethingWithLodash() { 146 | return _lodash2.default.pick({ nose: 'big' }, 'nose'); 147 | } 148 | ``` 149 | 150 | Note how the import of `path` has been discarded. 151 | -------------------------------------------------------------------------------- /lib/.gitignore: -------------------------------------------------------------------------------- 1 | index.js 2 | -------------------------------------------------------------------------------- /lib/.npmignore: -------------------------------------------------------------------------------- 1 | !index.js 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "babel-plugin-discard-module-references", 3 | "version": "1.1.2", 4 | "description": "Babel plugin to remove all code using specified imported modules", 5 | "main": "lib/index.js", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/ArnaudRinquin/babel-plugin-discard-module-references" 9 | }, 10 | "scripts": { 11 | "pretest": "npm run build", 12 | "test": "(cd ./test && npm start)", 13 | "build": "babel src/index.js -o lib/index.js", 14 | "prepublish": "npm run build" 15 | }, 16 | "keywords": [ 17 | "babel-plugin" 18 | ], 19 | "author": "Arnaud Rinquin", 20 | "license": "MIT", 21 | "devDependencies": { 22 | "babel-cli": "^6.4.0", 23 | "babel-preset-es2015": "^6.3.13", 24 | "babel-preset-stage-0": "^6.3.13" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | export default function (babel) { 2 | return { 3 | visitor: { 4 | Program: { 5 | exit(path, state) { 6 | path.traverse(removeTargetModuleReferences, state); 7 | path.scope.crawl(); 8 | path.traverse(removeUnusedModulesReferences, state); 9 | } 10 | } 11 | } 12 | }; 13 | } 14 | 15 | const removeTargetModuleReferences = { 16 | ImportDeclaration(path, state) { 17 | 18 | const { targets = [] } = state.opts; 19 | const { 20 | source, 21 | specifiers, 22 | } = path.node; 23 | 24 | const moduleSource = source.value; 25 | 26 | if (targets.indexOf(source.value) < 0) { 27 | return; 28 | } 29 | 30 | specifiers.forEach(function(specifier) { 31 | const importedIdentifierName = specifier.local.name; 32 | const { referencePaths } = path.scope.getBinding(importedIdentifierName); 33 | 34 | referencePaths.forEach(function removeExpression(referencePath){ 35 | let pathToRemove = referencePath; 36 | do { 37 | if (pathToRemove.type === 'ExpressionStatement') { 38 | break; 39 | } 40 | } while(pathToRemove = pathToRemove.parentPath); 41 | 42 | pathToRemove.remove(); 43 | }); 44 | }); 45 | 46 | path.remove(); 47 | } 48 | }; 49 | 50 | const removeUnusedModulesReferences = { 51 | ImportDeclaration(path, state) { 52 | 53 | const unusedWhitelist = state.opts.unusedWhitelist || []; 54 | const { 55 | source, 56 | specifiers, 57 | } = path.node; 58 | 59 | const moduleSource = source.value; 60 | 61 | if (unusedWhitelist.indexOf(source.value) > -1) { 62 | return; 63 | } 64 | 65 | // don't remove imports with no specifiers as they certainly have side effects 66 | if (specifiers.length === 0) { 67 | return; 68 | } 69 | 70 | const usedSpecifiers = specifiers.reduce(function(usedSpecifiers, specifier) { 71 | 72 | const importedIdentifierName = specifier.local.name; 73 | const { referencePaths } = path.scope.getBinding(importedIdentifierName); 74 | 75 | if (referencePaths.length > 0) { 76 | return [...usedSpecifiers, specifier]; 77 | } 78 | return usedSpecifiers; 79 | }, []); 80 | 81 | if (usedSpecifiers.length === 0) { 82 | path.remove(); 83 | } else { 84 | // only keep used specifiers 85 | // path.node.specifiers = usedSpecifiers; 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /test/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["stage-0", "es2015"], 3 | "ignore": [ 4 | "node_modules" 5 | ], 6 | "plugins": [ 7 | ["discard-module-references", { 8 | "targets": ["assert"], 9 | "unusedWhitelist": ["path"] 10 | }] 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /test/README.md: -------------------------------------------------------------------------------- 1 | # babel-plugin-discard-module-references test project 2 | 3 | This project builds tests for `babel-plugin-discard-module-references` where the `assert` module and references should be discarded in order to pass. 4 | -------------------------------------------------------------------------------- /test/dist/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /test/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "babel-plugin-discard-module-references-test", 3 | "version": "0.0.0", 4 | "description": "A babel-plugin-discard-module-references example", 5 | "main": "index.js", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/ArnaudRinquin/babel-plugin-discard-module-references" 9 | }, 10 | "scripts": { 11 | "postinstall": "rm -rf node_modules/babel-plugin-discard-module-references && mkdir -p node_modules/babel-plugin-discard-module-references && cp -r ../lib node_modules/babel-plugin-discard-module-references && cp ../package.json node_modules/babel-plugin-discard-module-references", 12 | "build": "mkdir -p dist && babel src/index.js -o dist/index.js", 13 | "prestart": "npm install", 14 | "start": "npm run build && node dist/index.js | tap-spec" 15 | }, 16 | "author": "", 17 | "license": "MIT", 18 | "devDependencies": { 19 | "babel-cli": "^6.4.0", 20 | "babel-preset-es2015": "^6.3.13", 21 | "babel-preset-stage-0": "^6.3.13", 22 | "deep-extend": "^0.4.0", 23 | "object-assign": "^4.0.1", 24 | "sinon": "^1.17.2", 25 | "tap-spec": "^4.1.1", 26 | "tape": "^4.4.0" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /test/src/index.js: -------------------------------------------------------------------------------- 1 | import tap from 'tape'; 2 | 3 | // FOLLOWING MODULES ALL HAVE A SPECIFIC ROLE IN THE TEST: 4 | 5 | // target module to remove: must discard 6 | import assert, { equal } from 'assert'; 7 | 8 | // used on in target module code: must keep 9 | import sinon, { spy } from 'sinon'; 10 | 11 | // no import specifier: must keep 12 | import 'deep-extend'; 13 | 14 | // used only in target but whitelisted: must keep 15 | import path from 'path'; 16 | 17 | // used both in target and outside: must keep 18 | import assign from 'object-assign'; 19 | 20 | // not used in target: must keep 21 | import { readFileSync } from 'fs'; 22 | 23 | // TESTS START 24 | 25 | const thisVeryFile = readFileSync(__filename).toString(); 26 | 27 | tap.test('target modules', function(t){ 28 | t.plan(1); 29 | // the following lines are breaking the tests on purpose 30 | // they should be removed from compiled code 31 | // if they are not, the tests will fail 32 | 33 | assert(false, 'should not run default import function calls'); 34 | assert.fail(1, 2, 'should not run default import member calls'); 35 | equal(1, 2, 'should not run import non-default function calls'); 36 | assert(function(){ 37 | sinon.doesNotEvenExist(); 38 | }); 39 | equal(function(){ 40 | spy('whatever'); 41 | }); 42 | equal(function(){ 43 | path.resolve('I dont care'); 44 | path.yolo('I dont care'); 45 | path.yeaheee('I dont care'); 46 | }); 47 | 48 | // ok, let's wrap up 49 | t.ok(true, 'code using them is removed'); 50 | }); 51 | 52 | function occurencesCount(str, substring) { 53 | const matcher = new RegExp(substring, 'g'); 54 | return (str.match(matcher) || []).length 55 | } 56 | 57 | tap.test('module used only by target mode code', function(t){ 58 | t.plan(2); 59 | // 1 because it's just right here 60 | t.equal(occurencesCount(thisVeryFile, 'sinon'), 1, 'default specifies are removed'); 61 | t.equal(occurencesCount(thisVeryFile, 'spy'), 1, 'non-default specifies are removed'); 62 | }); 63 | 64 | tap.test('whitelisted modules', function(t){ 65 | t.plan(1); 66 | // must have more 67 | t.notEqual(occurencesCount(thisVeryFile, 'path'), 1, 'are kept even if not used'); 68 | }); 69 | 70 | tap.test('modules used outside of target code', function(t){ 71 | t.plan(1); 72 | t.ok(assign({a:1}, {b:2}), 'are kept'); 73 | }); 74 | 75 | tap.test('unrelated references', function(t){ 76 | t.plan(1); 77 | let called = false; 78 | // different scope, different `assert` 79 | function assert(){ 80 | called = true; 81 | } 82 | 83 | assert(); // that'd better remain 84 | t.ok(called, 'are kept'); 85 | }); 86 | --------------------------------------------------------------------------------