├── .travis.yml ├── .gitignore ├── test ├── fixtures │ └── two │ │ ├── node_modules │ │ └── one │ │ │ ├── package.json │ │ │ └── index.js │ │ ├── package.json │ │ └── index.js └── index.js ├── lib ├── flatten.js ├── normalize-preset.js └── serialize.js ├── package.json ├── README.md └── index.js /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 5 4 | - 6 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /npm-debug.log 3 | /test/fixtures/two/node_modules/one/node_modules 4 | -------------------------------------------------------------------------------- /test/fixtures/two/node_modules/one/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "one", 3 | "main": "./index.js", 4 | "devDependencies": { 5 | "babel-plugin-transform-react-jsx": "^6.6.5" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /test/fixtures/two/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "two", 3 | "main": "./index.js", 4 | "dependencies": { 5 | "one": "*" 6 | }, 7 | "devDependencies": { 8 | "babel-plugin-transform-react-jsx": "^6.6.5" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /test/fixtures/two/node_modules/one/index.js: -------------------------------------------------------------------------------- 1 | var modify = require('../../../../../'); 2 | 3 | module.exports = modify('es2015', { 4 | // nameDrops: false, 5 | 'transform-react-jsx': { pragma:'h' }, 6 | 'transform-es2015-typeof-symbol': { loose:true } 7 | }); 8 | -------------------------------------------------------------------------------- /lib/flatten.js: -------------------------------------------------------------------------------- 1 | module.exports = function flatten(preset) { 2 | var plugins = [].concat(preset.plugins || []); 3 | 4 | (preset.presets || []).forEach(function(child) { 5 | var children = flatten(child); 6 | if (children) plugins = plugins.concat(children); 7 | }); 8 | 9 | return plugins; 10 | }; 11 | -------------------------------------------------------------------------------- /test/fixtures/two/index.js: -------------------------------------------------------------------------------- 1 | var path = require('path'), 2 | modify = require('../../../'); 3 | 4 | // module.exports = modify(path.resolve(__dirname+'/node_modules/one'), { 5 | module.exports = modify(require.resolve('one'), { 6 | // nameDrops: false, 7 | 'transform-es2015-typeof-symbol': false, 8 | 'transform-react-jsx': { pragma:'z' }, 9 | }); 10 | -------------------------------------------------------------------------------- /lib/normalize-preset.js: -------------------------------------------------------------------------------- 1 | var babelCore; 2 | try { babelCore = require('babel-core'); } catch(err) {} 3 | 4 | module.exports = function normalizePreset(preset, context, options) { 5 | if (!context) context = babelCore; 6 | 7 | if (preset && typeof preset==='object' && preset.buildPreset) { 8 | preset = preset.buildPreset; 9 | } 10 | 11 | if (typeof preset==='function') { 12 | preset = preset(context, options || {}); 13 | } 14 | 15 | return preset; 16 | }; 17 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "modify-babel-preset", 3 | "version": "3.2.1", 4 | "description": "Create a modified babel preset based on an an existing preset.", 5 | "main": "index.js", 6 | "scripts": { 7 | "prepublish": "cross-env NODE_ENV=development npm test", 8 | "pretest": "cd test/fixtures/two/node_modules/one && npm i", 9 | "test": "mocha test/*.js", 10 | "release": "git commit -am $npm_package_version && git tag $npm_package_version && git push && git push --tags && npm publish" 11 | }, 12 | "keywords": [ 13 | "babel", 14 | "preset" 15 | ], 16 | "author": "Jason Miller ", 17 | "repository": { 18 | "type": "git", 19 | "url": "https://github.com/developit/modify-babel-preset.git" 20 | }, 21 | "license": "MIT", 22 | "dependencies": { 23 | "require-relative": "^0.8.7" 24 | }, 25 | "devDependencies": { 26 | "babel-plugin-transform-react-jsx": "^6.8.0", 27 | "babel-preset-es2015": "^6.9.0", 28 | "chai": "^3.5.0", 29 | "cross-env": "^2.0.0", 30 | "mocha": "^2.5.3" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # modify-babel-preset 3 | 4 | [![npm](https://img.shields.io/npm/v/modify-babel-preset.svg)](http://npm.im/modify-babel-preset) 5 | [![npm](https://img.shields.io/npm/dm/modify-babel-preset.svg)](http://npm.im/modify-babel-preset) 6 | [![travis](https://travis-ci.org/developit/modify-babel-preset.svg?branch=master)](https://travis-ci.org/developit/modify-babel-preset) 7 | 8 | 9 | Create a modified babel preset based on an an existing preset. 10 | 11 | > Works best with `npm 3`. 12 | 13 | 14 | ```sh 15 | npm i -S modify-babel-preset 16 | ``` 17 | 18 | 19 | --- 20 | 21 | 22 | - [API](#api) 23 | - [Add/Update Plugins](#addupdate-plugins) 24 | - [Remove Plugins](#remove-plugins) 25 | - [Example](#example) 26 | 27 | 28 | --- 29 | 30 | 31 | ## API 32 | 33 | A single function that takes an existing preset name and a mapping of plugin modifications to apply to that preset. _Make sure you have the preset and any added plugins installed as dependencies._ 34 | 35 | ```js 36 | newPreset = modifyBabelPreset( 37 | 'existing-preset-name', 38 | { 39 | 'plugin-name': false, // remove 40 | 'other-plugin': true, // add 41 | 'foo': { loose:true } // add + config 42 | } 43 | ); 44 | ``` 45 | 46 | > Modification keys are babel plugin names _(you can exclude the `babel-plugin-` prefix)._ 47 | 48 | 49 | ### Add/Update Plugins 50 | 51 | To add a plugin, pass `true`, or a configuration object: 52 | 53 | ```js 54 | { 55 | // just add a plugin without config: 56 | 'plugin-name': true, 57 | 58 | // add a plugin and set its config 59 | 'other-plugin': { loose:true } 60 | } 61 | ``` 62 | 63 | > **Note:** adding a plugin that is already provided by the preset just overwrites its configuration. 64 | 65 | 66 | ### Remove Plugins 67 | 68 | To remove a plugin, pass `false`: 69 | 70 | ```js 71 | { 72 | 'plugin-name': false 73 | } 74 | ``` 75 | 76 | 77 | --- 78 | 79 | 80 | ## Example 81 | 82 | Here's a simple preset. Just this `index.js` and a package.json pointing to it with the preset and plugin installed as dependencies. 83 | 84 | ```js 85 | var modifyBabelPreset = require('modify-babel-preset'); 86 | 87 | // just export the cloned, modified preset config: 88 | module.exports = modifyBabelPreset('es2015', { 89 | 90 | // remove the typeof x==='symbol' transform: 91 | 'transform-es2015-typeof-symbol': false, 92 | 93 | // add the JSX transform: 94 | 'transform-react-jsx': true 95 | 96 | }); 97 | ``` 98 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect, 2 | requireRelative = require('require-relative'), 3 | modifyBabelPreset = require('..'), 4 | serializePreset = require('../lib/serialize'), 5 | es2015Preset = callFn(require('babel-preset-es2015')), 6 | transform = requireRelative('babel-preset-es2015', 'babel-plugin-transform-es2015-typeof-symbol'), 7 | jsxCore = require('babel-plugin-transform-react-jsx'), 8 | jsx = require('./fixtures/two/node_modules/one/node_modules/babel-plugin-transform-react-jsx'); 9 | 10 | function callFn(r) { 11 | if (typeof r==='function') r = r(); 12 | return r; 13 | } 14 | 15 | describe('modify-babel-preset', function() { 16 | it('should import string presets', function() { 17 | var out = modifyBabelPreset('babel-preset-es2015'); 18 | expect(out).to.deep.equal(es2015Preset); 19 | }); 20 | 21 | it('should import string presets without babel-preset- prefix', function() { 22 | var out = modifyBabelPreset('es2015'); 23 | expect(out).to.deep.equal(es2015Preset); 24 | }); 25 | 26 | it('should remove for false values', function() { 27 | var out = modifyBabelPreset('es2015', { 28 | 'transform-es2015-typeof-symbol': false 29 | }); 30 | expect(out.plugins).to.have.length(es2015Preset.plugins.length-1); 31 | expect(out.plugins).not.to.include(transform); 32 | }); 33 | 34 | it('should add for true values', function() { 35 | var out = modifyBabelPreset('es2015', { 36 | // nameDrops: false, 37 | 'transform-react-jsx': true 38 | }); 39 | expect(out.plugins).to.deep.equal( es2015Preset.plugins.concat(jsxCore) ); 40 | }); 41 | 42 | it('should add values with config', function() { 43 | var out = modifyBabelPreset('es2015', { 44 | nameDrops: false, 45 | 'transform-react-jsx': { pragma:'h' } 46 | }); 47 | expect(out.plugins).to.deep.equal( es2015Preset.plugins.concat([ 48 | [jsxCore, { pragma:'h' }] 49 | ]) ); 50 | }); 51 | 52 | xit('should work recursively', function() { 53 | var one = require('./fixtures/two/node_modules/one'); 54 | 55 | // var reference = serializePreset(require.resolve('babel-preset-es2015')).plugins.map( p => ( 56 | // Array.isArray(p) ? [getModuleName(p[0])].concat(p.slice(1)) : getModuleName(p) 57 | // )); 58 | // 59 | // var actual = one.plugins.map( p => ( 60 | // Array.isArray(p) ? [getModuleName(p[0]._original_name)].concat(p.slice(1)) : getModuleName(p._original_name) 61 | // )); 62 | // 63 | // // expect(reference).to.deep.equal(actual, 'Equal when serialized'); 64 | // 65 | // actual.forEach( (actual, i) => { 66 | // if (actual!==reference[i]) { 67 | // console.log(i+'\n actual: '+ JSON.stringify(actual)+'\n ref: '+ JSON.stringify(reference[i])); 68 | // console.log(' ===json? ', JSON.stringify(actual)===JSON.stringify(reference[i])); 69 | // //console.log(' ===func? ', one.plugins[i][0]===es2015Preset.plugins[i][0]); 70 | // } 71 | // }); 72 | // 73 | // function getModuleName(path) { 74 | // return path.replace(/^(.+\/)?node_modules\/([^\/]+)(\/.*)?$/g, '$2'); 75 | // } 76 | 77 | function stripNames(p) { 78 | if (Array.isArray(p)) delete p[0]._original_name; 79 | else delete p._original_name; 80 | return p; 81 | } 82 | 83 | var p = one.plugins.map(stripNames); 84 | 85 | expect(p).to.deep.equal( es2015Preset.plugins.concat([ 86 | [jsx, { pragma:'h' }] 87 | ]).map(function(p) { 88 | var fn = Array.isArray(p) ? p[0] : p; 89 | if (fn===transform || (fn._original_name && ~fn._original_name.indexOf('babel-plugin-transform-es2015-typeof-symbol'))) { 90 | return [fn, { loose:true }]; 91 | } 92 | return p; 93 | }).map(stripNames) ); 94 | 95 | console.log('one matched'); 96 | 97 | var two = require('./fixtures/two'); 98 | two.plugins.forEach(function(p) { 99 | var f = Array.isArray(p) ? p[0] : p; 100 | //delete p._original_name; 101 | }); 102 | 103 | // delete transform._original_name; 104 | // delete jsx._original_name; 105 | 106 | var target = es2015Preset.plugins.concat([ 107 | [jsx, { pragma:'z' }] 108 | ]); 109 | target.splice(17, 1); 110 | two.plugins = two.plugins.map(p => p+''); 111 | target = target.map(p => p+''); 112 | // console.log(two.plugins[17]===target[17]); 113 | expect(two.plugins).to.deep.equal(target); 114 | }); 115 | }); 116 | -------------------------------------------------------------------------------- /lib/serialize.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | var path = require('path'); 3 | var requireRelative = require('require-relative'); 4 | var normalizePreset = require('./normalize-preset'); 5 | var join = path.join; 6 | var dirname = path.dirname; 7 | var relative = path.relative; 8 | 9 | function getModulePath(filepath) { 10 | return filepath.replace(/(.*([\\/]node_modules|\.\.)[\\/](@[^\\/]+[\\/])?[^\\/]+)([\\/].*)?$/g, '$1') 11 | } 12 | 13 | // Attempt to require a module, returning false on error 14 | function req(name) { 15 | try { 16 | return require(name); 17 | } catch (err) { 18 | return false; 19 | } 20 | } 21 | 22 | // Attempt to resolve a module, returning `undefined` on error 23 | function resolve(name, relativeTo) { 24 | var path = false; 25 | try { 26 | path = requireRelative.resolve(name, relativeTo); 27 | } catch (err) { 28 | console.log('resolve failed for "'+name+'": '+err); 29 | } 30 | return path; 31 | } 32 | 33 | // fast npm always use symlink 34 | // and it resulted in module duplicated 35 | // or alias 36 | function fillAliasMap(path, list, map) { 37 | return list.reduce(function (symlinkedList, mod) { 38 | var realPath = relative(path, fs.realpathSync(join(path, mod))); 39 | if (realPath !== mod) { 40 | symlinkedList[realPath] = (mod); 41 | } 42 | return symlinkedList; 43 | }, map || {}); 44 | } 45 | 46 | // Get a list of child module names for the given module path 47 | function getChildren(path, type, alias) { 48 | var modules; 49 | try { 50 | modules = fs.readdirSync(join(path, 'node_modules')); 51 | } catch (err) { 52 | path = path.replace(/([\\/]node_modules)([\\/].*)?$/g, '$1'); 53 | modules = fs.readdirSync(path); 54 | } 55 | var children = (modules || []) 56 | .filter( realFile ) 57 | .sort( reverseSorter(type==='plugin' ? isPlugin : isPreset) ); 58 | fillAliasMap(path, children, alias); 59 | return children; 60 | } 61 | 62 | // is a filename an actual file 63 | function realFile(f) { 64 | return f && f.charAt(0)!=='.'; 65 | } 66 | 67 | // ascending sort based on the truthiness of a function 68 | function reverseSorter(comparison) { 69 | return function(a, b) { 70 | var ca = comparison(a), 71 | cb = comparison(b); 72 | return ca===cb ? 0 : (ca ? 1 : cb ? -1 : 0); 73 | } 74 | } 75 | 76 | function isPreset(name) { return name && name.match(/^babel\-preset\-/g); } 77 | 78 | function isPlugin(name) { return name && name.match(/^babel\-plugin\-/g); } 79 | 80 | // find the matching module *instance* in a list of module paths, remove and return it 81 | function findAndRemove(preset, path, list) { 82 | for (var i=list.length; i--; ) { 83 | var p = resolve(list[i], path), 84 | v = p && req(p); 85 | if (v && v===preset) { 86 | list.splice(i, 1); 87 | return p; 88 | } 89 | } 90 | } 91 | 92 | /** Resolve & serialize a Babel preset to a filename-based representation. 93 | * Nested filepaths are relative to `relativeTo` if specified. 94 | * Presets referenced as Strings (uncommon) are treated as dependencies of the preset that returned them. 95 | */ 96 | function loadPreset(name, opts, relativeTo) { 97 | var path = resolve(name, relativeTo), 98 | mod = normalizePreset(path && req(path), null, opts.options); 99 | if (!mod) throw new Error('Preset "'+name+'" not found.'); 100 | 101 | path = dirname(path); 102 | 103 | var out = { 104 | alias: {} 105 | }; 106 | if (mod.presets) { 107 | var availablePresets = getChildren(path, 'preset', out.alias); 108 | out.presets = mod.presets.map(function(preset) { 109 | if (typeof preset!=='string') { 110 | preset = findAndRemove(preset, path, availablePresets); 111 | } 112 | return loadPreset(preset, opts, path); 113 | }); 114 | } 115 | 116 | if (mod.plugins) { 117 | var availablePlugins = getChildren(path, 'plugin', out.alias); 118 | out.plugins = mod.plugins.map(function(plugin) { 119 | var name = Array.isArray(plugin) ? plugin[0] : plugin; 120 | if (typeof name!=='string') { 121 | if (name._original_name) { 122 | // console.log('using _original_name: ', name._original_name); 123 | name = name._original_name; 124 | } 125 | else { 126 | name = findAndRemove(name, path, availablePlugins); 127 | } 128 | } 129 | if (!name) return plugin; 130 | 131 | name = resolve(name, path); 132 | name = getModulePath(name); 133 | if (opts) { 134 | if (opts.cwd) name = relative(opts.cwd, name); 135 | if (opts.transform) name = opts.transform(name); 136 | } 137 | return Array.isArray(plugin) ? [name].concat(plugin.slice(1)) : name; 138 | }); 139 | } 140 | return out; 141 | } 142 | 143 | module.exports = loadPreset; 144 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var relative = require('require-relative'); 3 | var serialize = require('./lib/serialize'); 4 | var getFlattenedPlugins = require('./lib/flatten'); 5 | var normalizePreset = require('./lib/normalize-preset'); 6 | 7 | // strip a nested module path + filename down to just the innermost module's (file)name 8 | function getModuleName(path) { 9 | return path.replace(/(?:..[\\/])*(?:(?:.+[\\/])?node_modules[\\/]|[\\/]|\.\.[\\/])((@[^\\/]+[\\/])?[^\\/]+)([\\/].*)?$/g, '$1'); 10 | } 11 | 12 | 13 | function setHiddenProp(obj, prop, value) { 14 | if (Object.defineProperty) { 15 | Object.defineProperty(obj, prop, { 16 | enumerable: false, 17 | writable: true, 18 | value: value 19 | }); 20 | } 21 | else { 22 | obj[prop] = value; 23 | } 24 | } 25 | 26 | 27 | function extend(base, props) { 28 | for (var i in props) if (props.hasOwnProperty(i)) { 29 | base[i] = props[i]; 30 | } 31 | return base; 32 | } 33 | 34 | 35 | function requireBabelPlugin(name, relativeTo) { 36 | if (!name.match(/^babel-plugin-/)) { 37 | name = 'babel-plugin-' + name; 38 | } 39 | 40 | var relativeName; 41 | if (relativeTo) { 42 | try { 43 | relativeName = relative.resolve(name, relativeTo); 44 | } catch (err) {} 45 | } 46 | if (!relativeName) { 47 | try { 48 | relativeName = require.resolve(name); 49 | } catch(err) {} 50 | } 51 | 52 | name = relativeName || name; 53 | 54 | return { mod:require(name), name:name }; 55 | } 56 | 57 | 58 | module.exports = function(presetInput, modifications) { 59 | var options = {}; 60 | 61 | if (Array.isArray(presetInput)) { 62 | options = presetInput[1]; 63 | presetInput = presetInput[0]; 64 | } 65 | modifications = modifications || {}; 66 | 67 | var preset; 68 | if (typeof presetInput==='string') { 69 | if (!presetInput.match(/(^babel-preset-|[\\/])/)) { 70 | try { 71 | preset = relative.resolve('babel-preset-'+presetInput, __dirname); 72 | } catch(err) { 73 | console.log(err); 74 | } 75 | } 76 | if (!preset) { 77 | preset = require.resolve(presetInput); 78 | } 79 | } 80 | 81 | preset = path.resolve(preset); 82 | 83 | var presetModule = normalizePreset(require(preset), null, options); 84 | 85 | var orig = presetModule['modify-babel-preset']; 86 | if (orig) { 87 | console.log('Found nested modify-babel-preset configuration, flattening.'); 88 | return modify(orig.preset, extend(extend({}, orig.modifications), modifications)); 89 | } 90 | 91 | var cwd = path.dirname(preset) || process.cwd(); 92 | 93 | // console.log('cwd: ', cwd); 94 | 95 | var serialized = serialize(preset, { 96 | options: options, 97 | cwd: cwd 98 | }); 99 | 100 | var alias = serialized.alias; 101 | var plugins = getFlattenedPlugins(serialized); 102 | 103 | function isSameName(a, b) { 104 | if (typeof a!=='string' || typeof b!=='string') return false; 105 | return a.replace(/^babel-plugin-/i, '').toLowerCase() === b.replace(/^babel-plugin-/i, '').toLowerCase(); 106 | } 107 | 108 | function indexOf(plugins, key) { 109 | for (var i=plugins.length; i--; ) { 110 | var mod = Array.isArray(plugins[i]) ? plugins[i][0] : plugins[i]; 111 | var name = typeof mod==='string' && getModuleName(mod) || mod._original_name; 112 | name = alias[name] || name; 113 | if (isSameName(name, key)) { 114 | return i; 115 | } 116 | } 117 | return -1; 118 | } 119 | 120 | Object.keys(modifications).forEach(function(key) { 121 | if (key==='nameDrops' || key==='string') return; 122 | 123 | var value = modifications[key], 124 | index = indexOf(plugins, key); 125 | if (value===false) { 126 | if (index!==-1) { 127 | plugins.splice(index, 1); 128 | } 129 | else if (process.env.NODE_ENV==='development') { 130 | console.warn(key+' not found', __dirname); 131 | } 132 | } 133 | else { 134 | var imported = requireBabelPlugin(key, cwd), 135 | p = imported.mod; 136 | setHiddenProp(p, '_original_name', imported.name); 137 | if (value!==true) { 138 | p = [p].concat(value); 139 | } 140 | if (index<0) { 141 | plugins.push(p); 142 | } 143 | else { 144 | plugins[index] = p; 145 | } 146 | } 147 | }); 148 | 149 | if (modifications.string!==true) { 150 | plugins = plugins.map(function(plugin) { 151 | var mod = Array.isArray(plugin) ? plugin[0] : plugin; 152 | if (typeof mod==='string') { 153 | var p = path.resolve(cwd, mod); 154 | mod = require(p); 155 | setHiddenProp(mod, '_original_name', p); 156 | } 157 | return Array.isArray(plugin) ? [mod].concat(plugin.slice(1)) : mod; 158 | }); 159 | } 160 | 161 | var out = { plugins:plugins }; 162 | 163 | setHiddenProp(out, 'modify-babel-preset', { 164 | preset: path.dirname(path.resolve(preset)), 165 | modifications: modifications 166 | }); 167 | 168 | return out; 169 | }; 170 | --------------------------------------------------------------------------------