diff --git a/.eslintignore b/.eslintignore index f48045523db..a7cecca09b8 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1 +1 @@ -__testfixtures__ +**/__testfixtures__/* diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a44a5803912..844a5065c7b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -128,6 +128,69 @@ to read on GitHub as well as in several git tools. For more information about what each part of the template mean, head up to the documentation in the [angular repo](https://github.com/angular/angular.js/blob/master/CONTRIBUTING.md#commit-message-format) +## --migrate with the CLI + +This is a new feature in development for the CLI. + +``` +webpack --migrate +``` + +The expected result of the above command is to take the mentioned `webpack` configuration and create a new configuration file which is compatible with webpack 2. +It should be a valid new config and should keep intact all the features from the original config. +The new config will be as readable as possible (may be add some comments). + +With [#40](https://github.com/webpack/webpack-cli/pull/40), we have been able to add basic scaffolding and do many of the conversions recommended in the [docs](https://webpack.js.org/guides/migrating/). + +### How it's being done + +We use [`jscodeshift`](https://github.com/facebook/jscodeshift) transforms called `codemods` to accomplish this. +We have written a bunch of transformations under [/lib/transformations](https://github.com/webpack/webpack-cli/tree/master/lib/transformations),divided logically. +We convert the existing webpack config to [AST](https://developer.mozilla.org/en-US/docs/Mozilla/Projects/SpiderMonkey/Parser_API). We then parse this tree for the specific features and modify it to conform to webpack v2... + +#### Structure of a transform + +The directory structure of a transform looks as follows - + +```sh +| +|--__snapshots__ +|--__testfixtures__ +| | +| |--transform-name.input.js +| +|--transform-name.js +|--transform-name.test.js +``` + +`transform-name.js` + +This file contains the actual transformation codemod. It applies specific transformation and parsing logic to accomplish its job +There are utilities available under `/lib/utils.js` which can help you with this. + +`transform-name.test.js` + +This is where you declare a new test case for your transformation. +Each test will refer to an input webpack config snippet. +Conventionally we write them in `\_\_testfixtures\_\_`. + +``` +const defineTest = require('../defineTest'); + +defineTest(__dirname, 'transform-name.input1.js'); +defineTest(__dirname, 'transform-name.input2.js'); +``` + +`defineTest` is a helper test method which helps us to run tests on all the transforms uniformly. +It takes the input file given as parameter and uses jest to create a snapshot of the output. This effectively tests the correctness of our transformation. + +### TODO + +This is still in a very raw form. We'd like to take this as close to a truly useful tool as possible. +We will still need to + - Support all kinds of webpack configuration(made using merge tools) + - Test these transforms against real world configurations. + ## Contributor License Agreement When submitting your contribution, a CLA (Contributor License Agreement) bot will come by to verify that you signed the CLA. If you are submitting a PR for the first time, it will link you to the right place to sign it. If you have committed your contributions using an email that is not the same as your email used on GitHub, the CLA bot can't accept your contribution. diff --git a/lib/migrate.js b/lib/migrate.js index 0ced71a064d..953ece40967 100644 --- a/lib/migrate.js +++ b/lib/migrate.js @@ -1,35 +1,36 @@ const fs = require('fs'); -const jscodeshift = require('jscodeshift'); const diff = require('diff'); const chalk = require('chalk'); -const transformations = require('./transformations'); +const transform = require('./transformations').transform; const inquirer = require('inquirer'); module.exports = (currentConfigLoc, outputConfigLoc) => { let currentConfig = fs.readFileSync(currentConfigLoc, 'utf8'); - let ast = jscodeshift(currentConfig); - let transformNames = Object.keys(transformations); - transformNames.forEach(key => transformations[key](jscodeshift, ast)); - const outputConfig = ast.toSource(); + const outputConfig = transform(currentConfig); const diffOutput = diff.diffLines(currentConfig, outputConfig); diffOutput.map(diffLine => { - if(diffLine.added) { + if (diffLine.added) { process.stdout.write(chalk.green(`+ ${diffLine.value}`)); - } else if(diffLine.removed){ + } else if (diffLine.removed) { process.stdout.write(chalk.red(`- ${diffLine.value}`)); } }); - inquirer.prompt([{ - type: 'confirm', - name: 'confirmMigration', - message: 'Are you sure these changes are fine?', - default: 'Y'}]).then(answers => { - if(answers['confirmMigration']){ - // TODO validate the config + inquirer + .prompt([ + { + type: 'confirm', + name: 'confirmMigration', + message: 'Are you sure these changes are fine?', + default: 'Y' + } + ]) + .then(answers => { + if (answers['confirmMigration']) { + // TODO validate the config fs.writeFileSync(outputConfigLoc, outputConfig, 'utf8'); process.stdout.write(chalk.green(`Congratulations! Your new webpack v2 config file is at ${outputConfigLoc}`)); } else { - process.stdout.write(chalk.yellow('You aborted the migration')); + process.stdout.write(chalk.yellow('Migration aborted')); } }); -}; \ No newline at end of file +}; diff --git a/lib/transformations/__snapshots__/index.test.js.snap b/lib/transformations/__snapshots__/index.test.js.snap new file mode 100644 index 00000000000..ec9de1ff3f7 --- /dev/null +++ b/lib/transformations/__snapshots__/index.test.js.snap @@ -0,0 +1,100 @@ +exports[`transform should respect recast options 1`] = ` +" +module.exports = { + devtool: \'eval\', + entry: [ + \'./src/index\' + ], + output: { + path: path.join(__dirname, \'dist\'), + filename: \'index.js\' + }, + module: { + rules: [{ + test: /.js$/, + use: \"babel\", + include: path.join(__dirname, \'src\') + }] + }, + resolve: { + modules: [\'node_modules\', path.resolve(\'/src\')], + }, + plugins: [ + new webpack.optimize.UglifyJsPlugin({ + sourceMap: true, + }), + new webpack.optimize.LoaderOptionsPlugin({ + \"debug\": true, + \"minimize\": true, + }) + ], + debug: true +}; +" +`; + +exports[`transform should transform only using specified transformations 1`] = ` +" +module.exports = { + devtool: \'eval\', + entry: [ + \'./src/index\' + ], + output: { + path: path.join(__dirname, \'dist\'), + filename: \'index.js\' + }, + module: { + rules: [{ + test: /.js$/, + use: [\'babel\'], + include: path.join(__dirname, \'src\') + }] + }, + resolve: { + root: path.resolve(\'/src\'), + modules: [\'node_modules\'] + }, + plugins: [ + new webpack.optimize.UglifyJsPlugin(), + new webpack.optimize.OccurrenceOrderPlugin() + ], + debug: true +}; +" +`; + +exports[`transform should transform using all transformations 1`] = ` +" +module.exports = { + devtool: \'eval\', + entry: [ + \'./src/index\' + ], + output: { + path: path.join(__dirname, \'dist\'), + filename: \'index.js\' + }, + module: { + rules: [{ + test: /.js$/, + use: \'babel\', + include: path.join(__dirname, \'src\') + }] + }, + resolve: { + modules: [\'node_modules\', path.resolve(\'/src\')] + }, + plugins: [ + new webpack.optimize.UglifyJsPlugin({ + sourceMap: true + }), + new webpack.optimize.LoaderOptionsPlugin({ + \'debug\': true, + \'minimize\': true + }) + ], + debug: true +}; +" +`; diff --git a/lib/transformations/__snapshots__/utils.test.js.snap b/lib/transformations/__snapshots__/utils.test.js.snap new file mode 100644 index 00000000000..f245f10773c --- /dev/null +++ b/lib/transformations/__snapshots__/utils.test.js.snap @@ -0,0 +1,71 @@ +exports[`utils createLiteral should create basic literal 1`] = ` +Object { + "comments": null, + "loc": null, + "regex": null, + "type": "Literal", + "value": "strintLiteral", +} +`; + +exports[`utils createLiteral should create boolean 1`] = ` +Object { + "comments": null, + "loc": null, + "regex": null, + "type": "Literal", + "value": true, +} +`; + +exports[`utils createOrUpdatePluginByName should add an object as an argument 1`] = ` +"[new Plugin({ + \"foo\": true +})]" +`; + +exports[`utils createOrUpdatePluginByName should create a new plugin with arguments 1`] = ` +"{ plugins: [new Plugin({ + \"foo\": \"bar\" +})] }" +`; + +exports[`utils createOrUpdatePluginByName should create a new plugin without arguments 1`] = `"{ plugins: [new Plugin()] }"`; + +exports[`utils createOrUpdatePluginByName should merge options objects 1`] = ` +"[new Plugin({ + \"foo\": false, + \"bar\": \"baz\", + \"baz-long\": true +})]" +`; + +exports[`utils createProperty should create properties for Boolean 1`] = ` +"{ + \"foo\": true +}" +`; + +exports[`utils createProperty should create properties for Number 1`] = ` +"{ + \"foo\": -1 +}" +`; + +exports[`utils createProperty should create properties for String 1`] = ` +"{ + \"foo\": \"bar\" +}" +`; + +exports[`utils createProperty should create properties for complex keys 1`] = ` +"{ + \"foo-bar\": \"bar\" +}" +`; + +exports[`utils createProperty should create properties for non-literal keys 1`] = ` +"{ + 1: \"bar\" +}" +`; diff --git a/lib/transformations/bannerPlugin/__snapshots__/bannerPlugin.test.js.snap b/lib/transformations/bannerPlugin/__snapshots__/bannerPlugin.test.js.snap new file mode 100644 index 00000000000..8eb8caf991e --- /dev/null +++ b/lib/transformations/bannerPlugin/__snapshots__/bannerPlugin.test.js.snap @@ -0,0 +1,30 @@ +exports[`bannerPlugin transforms correctly using "bannerPlugin-0" data 1`] = ` +"module.exports = { + plugins: [ + new webpack.BannerPlugin({ + raw: true, + entryOnly: true, + \'banner\': \'Banner\' + }) + ] +} +" +`; + +exports[`bannerPlugin transforms correctly using "bannerPlugin-1" data 1`] = ` +"// Should do nothing if there is no banner plugin +module.exports = { + plugins: [] +} +" +`; + +exports[`bannerPlugin transforms correctly using "bannerPlugin-2" data 1`] = ` +"// Only transform if it uses the old format +module.exports = { + plugins: [ + new webpack.BannerPlugin({}) + ] +} +" +`; diff --git a/lib/transformations/bannerPlugin/__testfixtures__/.editorconfig b/lib/transformations/bannerPlugin/__testfixtures__/.editorconfig new file mode 100644 index 00000000000..adbdb1ba476 --- /dev/null +++ b/lib/transformations/bannerPlugin/__testfixtures__/.editorconfig @@ -0,0 +1,3 @@ +[*] +indent_style = space +indent_size = 4 diff --git a/lib/transformations/bannerPlugin/__testfixtures__/bannerPlugin-0.input.js b/lib/transformations/bannerPlugin/__testfixtures__/bannerPlugin-0.input.js new file mode 100644 index 00000000000..56c89e72d84 --- /dev/null +++ b/lib/transformations/bannerPlugin/__testfixtures__/bannerPlugin-0.input.js @@ -0,0 +1,5 @@ +module.exports = { + plugins: [ + new webpack.BannerPlugin('Banner', { raw: true, entryOnly: true }) + ] +} diff --git a/lib/transformations/bannerPlugin/__testfixtures__/bannerPlugin-1.input.js b/lib/transformations/bannerPlugin/__testfixtures__/bannerPlugin-1.input.js new file mode 100644 index 00000000000..0d66b9de1ac --- /dev/null +++ b/lib/transformations/bannerPlugin/__testfixtures__/bannerPlugin-1.input.js @@ -0,0 +1,4 @@ +// Should do nothing if there is no banner plugin +module.exports = { + plugins: [] +} diff --git a/lib/transformations/bannerPlugin/__testfixtures__/bannerPlugin-2.input.js b/lib/transformations/bannerPlugin/__testfixtures__/bannerPlugin-2.input.js new file mode 100644 index 00000000000..90ecde8f0de --- /dev/null +++ b/lib/transformations/bannerPlugin/__testfixtures__/bannerPlugin-2.input.js @@ -0,0 +1,6 @@ +// Only transform if it uses the old format +module.exports = { + plugins: [ + new webpack.BannerPlugin({}) + ] +} diff --git a/lib/transformations/bannerPlugin/bannerPlugin.js b/lib/transformations/bannerPlugin/bannerPlugin.js new file mode 100644 index 00000000000..9f40d172f7d --- /dev/null +++ b/lib/transformations/bannerPlugin/bannerPlugin.js @@ -0,0 +1,19 @@ +const utils = require('../utils'); + +module.exports = function(j, ast) { + return utils.findPluginsByName(j, ast, ['webpack.BannerPlugin']) + .forEach(path => { + const args = path.value.arguments; + // If the first argument is a literal replace it with object notation + // See https://webpack.js.org/guides/migrating/#bannerplugin-breaking-change + if (args && args.length > 1 && args[0].type === j.Literal.name) { + // and remove the first argument + path.value.arguments = [path.value.arguments[1]]; + utils.createOrUpdatePluginByName(j, path.parent, 'webpack.BannerPlugin', { + banner: args[0].value + }); + } + }); + + +}; diff --git a/lib/transformations/bannerPlugin/bannerPlugin.test.js b/lib/transformations/bannerPlugin/bannerPlugin.test.js new file mode 100644 index 00000000000..fee919e427e --- /dev/null +++ b/lib/transformations/bannerPlugin/bannerPlugin.test.js @@ -0,0 +1,5 @@ +const defineTest = require('../defineTest'); + +defineTest(__dirname, 'bannerPlugin', 'bannerPlugin-0'); +defineTest(__dirname, 'bannerPlugin', 'bannerPlugin-1'); +defineTest(__dirname, 'bannerPlugin', 'bannerPlugin-2'); diff --git a/lib/transformations/defineTest.js b/lib/transformations/defineTest.js index d7ab908c482..64a2a77301d 100644 --- a/lib/transformations/defineTest.js +++ b/lib/transformations/defineTest.js @@ -13,10 +13,6 @@ const fs = require('fs'); const path = require('path'); -const cli_engine = require('eslint').CLIEngine; //eslint-disable-line -const eslintrules = require(process.cwd() + '/.eslintrc.json'); -eslintrules.fix = true; -const cli = new cli_engine(eslintrules); /** * Utility function to run a jscodeshift script within a unit test. This makes @@ -37,51 +33,42 @@ const cli = new cli_engine(eslintrules); * - Test data should be located in a directory called __testfixtures__ * alongside the transform and __tests__ directory. */ -function runTest(dirName, transformName, options, testFilePrefix) { +function runTest(dirName, transformName, testFilePrefix) { if (!testFilePrefix) { testFilePrefix = transformName; } - const fixtureDir = path.join(dirName, '..', '__testfixtures__'); + const fixtureDir = path.join(dirName, '__testfixtures__'); const inputPath = path.join(fixtureDir, testFilePrefix + '.input.js'); const source = fs.readFileSync(inputPath, 'utf8'); - const expectedOutput = fs.readFileSync( - path.join(fixtureDir, testFilePrefix + '.output.js'), - 'utf8' - ); - // Assumes transform is one level up from __tests__ directory - const module = require(path.join(dirName, '..', transformName + '.js')); - // Handle ES6 modules using default export for the transform + // Assumes transform and test are on the same level + const module = require(path.join(dirName, transformName + '.js')); + // Handle ES6 modules using default export for the transform const transform = module.default ? module.default : module; - // Jest resets the module registry after each test, so we need to always get - // a fresh copy of jscodeshift on every test run. + // Jest resets the module registry after each test, so we need to always get + // a fresh copy of jscodeshift on every test run. let jscodeshift = require('jscodeshift/dist/core'); if (module.parser) { jscodeshift = jscodeshift.withParser(module.parser); } const ast = jscodeshift(source); - const output = transform( - jscodeshift, - ast - ); - const newOutput = cli.executeOnText(output).results[0].output; - expect((newOutput || '').trim()).toEqual(expectedOutput.trim()); + const output = transform(jscodeshift, ast).toSource({ quote: 'single' }); + expect(output).toMatchSnapshot(); } -module.exports.runTest = runTest; /** * Handles some boilerplate around defining a simple jest/Jasmine test for a * jscodeshift transform. */ -function defineTest(dirName, transformName, options, testFilePrefix) { +function defineTest(dirName, transformName, testFilePrefix) { const testName = testFilePrefix - ? `transforms correctly using "${testFilePrefix}" data` - : 'transforms correctly'; + ? `transforms correctly using "${testFilePrefix}" data` + : 'transforms correctly'; describe(transformName, () => { it(testName, () => { - runTest(dirName, transformName, options, testFilePrefix); + runTest(dirName, transformName, testFilePrefix); }); }); } -module.exports.defineTest = defineTest; +module.exports = defineTest; diff --git a/lib/transformations/extractTextPlugin/__snapshots__/extractTextPlugin.test.js.snap b/lib/transformations/extractTextPlugin/__snapshots__/extractTextPlugin.test.js.snap new file mode 100644 index 00000000000..17c8af0764e --- /dev/null +++ b/lib/transformations/extractTextPlugin/__snapshots__/extractTextPlugin.test.js.snap @@ -0,0 +1,22 @@ +exports[`extractTextPlugin transforms correctly 1`] = ` +"let ExtractTextPlugin = require(\'extract-text-webpack-plugin\'); +let HTMLWebpackPlugin = require(\'html-webpack-plugin\'); + +module.export = { + module: { + rules: [ + { + test: /\\.css$/, + use: ExtractTextPlugin.extract({ + \'fallback\': \'style-loader\', + \'use\': \'css-loader\' + }) + } + ] + }, + plugins: [ + new ExtractTextPlugin(\"styles.css\"), + ] +} +" +`; diff --git a/lib/transformations/extractTextPlugin/__testfixtures__/.editorconfig b/lib/transformations/extractTextPlugin/__testfixtures__/.editorconfig new file mode 100644 index 00000000000..adbdb1ba476 --- /dev/null +++ b/lib/transformations/extractTextPlugin/__testfixtures__/.editorconfig @@ -0,0 +1,3 @@ +[*] +indent_style = space +indent_size = 4 diff --git a/lib/transformations/extractTextPlugin/__testfixtures__/extractTextPlugin.input.js b/lib/transformations/extractTextPlugin/__testfixtures__/extractTextPlugin.input.js new file mode 100644 index 00000000000..f578bb4342d --- /dev/null +++ b/lib/transformations/extractTextPlugin/__testfixtures__/extractTextPlugin.input.js @@ -0,0 +1,16 @@ +let ExtractTextPlugin = require('extract-text-webpack-plugin'); +let HTMLWebpackPlugin = require('html-webpack-plugin'); + +module.export = { + module: { + rules: [ + { + test: /\.css$/, + use: ExtractTextPlugin.extract('style-loader', 'css-loader') + } + ] + }, + plugins: [ + new ExtractTextPlugin("styles.css"), + ] +} diff --git a/lib/transformations/extractTextPlugin/extractTextPlugin.js b/lib/transformations/extractTextPlugin/extractTextPlugin.js new file mode 100644 index 00000000000..3d60fedffdb --- /dev/null +++ b/lib/transformations/extractTextPlugin/extractTextPlugin.js @@ -0,0 +1,32 @@ +const utils = require('../utils'); + +function findInvocation(j, node, pluginName) { + return j(node) + .find(j.MemberExpression) + .filter(p => p.get('object').value.name === pluginName).size() > 0; +} + +module.exports = function(j, ast) { + const changeArguments = function(p) { + const args = p.value.arguments; + // if(args.length === 1) { + // return p; + // } else + const literalArgs = args.filter(p => utils.isType(p, 'Literal')); + if (literalArgs && literalArgs.length > 1) { + const newArgs = j.objectExpression(literalArgs.map((p, index) => + utils.createProperty(j, index === 0 ? 'fallback': 'use', p.value) + )); + p.value.arguments = [newArgs]; + } + return p; + }; + const name = utils.findVariableToPlugin(j, ast, 'extract-text-webpack-plugin'); + if(!name) return ast; + + return ast.find(j.CallExpression) + .filter(p => findInvocation(j, p, name)) + .forEach(changeArguments); +}; + + diff --git a/lib/transformations/extractTextPlugin/extractTextPlugin.test.js b/lib/transformations/extractTextPlugin/extractTextPlugin.test.js new file mode 100644 index 00000000000..66d74802356 --- /dev/null +++ b/lib/transformations/extractTextPlugin/extractTextPlugin.test.js @@ -0,0 +1,3 @@ +const defineTest = require('../defineTest'); + +defineTest(__dirname, 'extractTextPlugin'); diff --git a/lib/transformations/index.js b/lib/transformations/index.js index 5cebd4f274e..38d42103b11 100644 --- a/lib/transformations/index.js +++ b/lib/transformations/index.js @@ -1,7 +1,47 @@ -const loaderTransform = require('./loaders/loaders'); +const jscodeshift = require('jscodeshift'); + +const loadersTransform = require('./loaders/loaders'); const resolveTransform = require('./resolve/resolve'); +const removeJsonLoaderTransform = require('./removeJsonLoader/removeJsonLoader'); +const uglifyJsPluginTransform = require('./uglifyJsPlugin/uglifyJsPlugin'); +const loaderOptionsPluginTransform = require('./loaderOptionsPlugin/loaderOptionsPlugin'); +const bannerPluginTransform = require('./bannerPlugin/bannerPlugin'); +const extractTextPluginTransform = require('./extractTextPlugin/extractTextPlugin'); +const removeDeprecatedPluginsTransform = require('./removeDeprecatedPlugins/removeDeprecatedPlugins'); + +const transformations = { + loadersTransform, + resolveTransform, + removeJsonLoaderTransform, + uglifyJsPluginTransform, + loaderOptionsPluginTransform, + bannerPluginTransform, + extractTextPluginTransform, + removeDeprecatedPluginsTransform +}; + +/* +* @function transform +* +* Tranforms a given source code by applying selected transformations to the AST +* +* @param { String } source - Source file contents +* @param { Array } transformations - List of trnasformation functions in defined the +* order to apply. By default all defined transfomations. +* @param { Object } options - Reacst formatting options +* @returns { String } Transformed source code +* */ +function transform(source, transforms, options) { + const ast = jscodeshift(source); + const recastOptions = Object.assign({ + quote: 'single' + }, options); + transforms = transforms || Object.keys(transformations).map(k => transformations[k]); + transforms.forEach(f => f(jscodeshift, ast)); + return ast.toSource(recastOptions); +} module.exports = { - loaderTransform: loaderTransform, - resolveTransform: resolveTransform -}; \ No newline at end of file + transform, + transformations +}; diff --git a/lib/transformations/index.test.js b/lib/transformations/index.test.js new file mode 100644 index 00000000000..6058db357b7 --- /dev/null +++ b/lib/transformations/index.test.js @@ -0,0 +1,56 @@ +const transform = require('./index').transform; +const transformations = require('./index').transformations; + +const input = ` +module.exports = { + devtool: 'eval', + entry: [ + './src/index' + ], + output: { + path: path.join(__dirname, 'dist'), + filename: 'index.js' + }, + module: { + loaders: [{ + test: /\.js$/, + loaders: ['babel'], + include: path.join(__dirname, 'src') + }] + }, + resolve: { + root: path.resolve('/src'), + modules: ['node_modules'] + }, + plugins: [ + new webpack.optimize.UglifyJsPlugin(), + new webpack.optimize.OccurrenceOrderPlugin() + ], + debug: true +}; +`; + +describe('transform', () => { + it('should not transform if no transformations defined', () => { + const output = transform(input, []); + expect(output).toEqual(input); + }); + + it('should transform using all transformations', () => { + const output = transform(input); + expect(output).toMatchSnapshot(); + }); + + it('should transform only using specified transformations', () => { + const output = transform(input, [transformations.loadersTransform]); + expect(output).toMatchSnapshot(); + }); + + it('should respect recast options', () => { + const output = transform(input, undefined, { + quote: 'double', + trailingComma: true + }); + expect(output).toMatchSnapshot(); + }); +}); diff --git a/lib/transformations/loaderOptionsPlugin/__snapshots__/loaderOptionsPlugin.test.js.snap b/lib/transformations/loaderOptionsPlugin/__snapshots__/loaderOptionsPlugin.test.js.snap new file mode 100644 index 00000000000..db38d48df07 --- /dev/null +++ b/lib/transformations/loaderOptionsPlugin/__snapshots__/loaderOptionsPlugin.test.js.snap @@ -0,0 +1,14 @@ +exports[`loaderOptionsPlugin transforms correctly 1`] = ` +"module.exports = { + debug: true, + plugins: [ + new webpack.optimize.UglifyJsPlugin(), + new webpack.optimize.LoaderOptionsPlugin({ + foo: \'bar\', + \'debug\': true, + \'minimize\': true + }) + ] +} +" +`; diff --git a/lib/transformations/loaderOptionsPlugin/__testfixtures__/.editorconfig b/lib/transformations/loaderOptionsPlugin/__testfixtures__/.editorconfig new file mode 100644 index 00000000000..adbdb1ba476 --- /dev/null +++ b/lib/transformations/loaderOptionsPlugin/__testfixtures__/.editorconfig @@ -0,0 +1,3 @@ +[*] +indent_style = space +indent_size = 4 diff --git a/lib/transformations/loaderOptionsPlugin/__testfixtures__/loaderOptionsPlugin.input.js b/lib/transformations/loaderOptionsPlugin/__testfixtures__/loaderOptionsPlugin.input.js new file mode 100644 index 00000000000..60493cf28ab --- /dev/null +++ b/lib/transformations/loaderOptionsPlugin/__testfixtures__/loaderOptionsPlugin.input.js @@ -0,0 +1,9 @@ +module.exports = { + debug: true, + plugins: [ + new webpack.optimize.UglifyJsPlugin(), + new webpack.optimize.LoaderOptionsPlugin({ + foo: 'bar' + }) + ] +} diff --git a/lib/transformations/loaderOptionsPlugin/loaderOptionsPlugin.js b/lib/transformations/loaderOptionsPlugin/loaderOptionsPlugin.js new file mode 100644 index 00000000000..910f19f3bb4 --- /dev/null +++ b/lib/transformations/loaderOptionsPlugin/loaderOptionsPlugin.js @@ -0,0 +1,25 @@ +const findPluginsByName = require('../utils').findPluginsByName; +const createOrUpdatePluginByName = require('../utils').createOrUpdatePluginByName; + +module.exports = function(j, ast) { + const loaderOptions = {}; + + // If there is debug: true, set debug: true in the plugin + // TODO: remove global debug setting + // TODO: I can't figure out how to find the topmost `debug: true`. help! + if (ast.find(j.Identifier, { name: 'debug' }).size()) { + loaderOptions.debug = true; + } + + // If there is UglifyJsPlugin, set minimize: true + if (findPluginsByName(j, ast, ['webpack.optimize.UglifyJsPlugin']).size()) { + loaderOptions.minimize = true; + } + + return ast + .find(j.ArrayExpression) + .filter(path => path.parent.value.key.name === 'plugins') + .forEach(path => { + createOrUpdatePluginByName(j, path, 'webpack.optimize.LoaderOptionsPlugin', loaderOptions); + }); +}; diff --git a/lib/transformations/loaderOptionsPlugin/loaderOptionsPlugin.test.js b/lib/transformations/loaderOptionsPlugin/loaderOptionsPlugin.test.js new file mode 100644 index 00000000000..d0ef4fbd923 --- /dev/null +++ b/lib/transformations/loaderOptionsPlugin/loaderOptionsPlugin.test.js @@ -0,0 +1,3 @@ +const defineTest = require('../defineTest'); + +defineTest(__dirname, 'loaderOptionsPlugin'); diff --git a/lib/transformations/loaders/__snapshots__/loaders.test.js.snap b/lib/transformations/loaders/__snapshots__/loaders.test.js.snap new file mode 100644 index 00000000000..4df1f857b19 --- /dev/null +++ b/lib/transformations/loaders/__snapshots__/loaders.test.js.snap @@ -0,0 +1,167 @@ +exports[`loaders transforms correctly using "loaders-0" data 1`] = ` +"export default [{ + module: { + rules: [{ + test: /\\.js$/, + use: \'babel-loader\' + }] + } +}, { + module: { + rules: [{ + test: /\\.css$/, + use: [{ + \'loader\': \'style\' + }, { + \'loader\': \'css?modules&importLoaders=1&string=test123\' + }] + }] + } +}, { + module: { + rules: [{ + test: /\\.css$/, + use: [{ + loader: \'style-loader\' + }, { + loader: \'css-loader\', + options: { + modules: true + } + }] + }] + } +}, { + module: { + rules:[{ + test: /\\.js$/, + use: \'eslint-loader\', + \'enforce\': \'pre\' + }] + } +}, { + module: { + rules:[{ + test: /\\.js$/, + use: \'my-post-loader\', + \'enforce\': \'post\' + }] + } +}, { + module: { + rules: [{ + test: /\\.js$/, + use: \'babel-loader\' + }, { + test: /\\.js$/, + use: \'eslint-loader\', + \'enforce\': \'pre\' + }] + } +}, { + module: { + rules: [{ + test: /\\.js$/, + use: \'babel-loader\' + }, { + test: /\\.js$/, + use: \'my-post-loader\', + \'enforce\': \'post\' + }] + } +}]; +" +`; + +exports[`loaders transforms correctly using "loaders-1" data 1`] = ` +"export default { + module: { + rules: [{ + test: /\\.css$/, + use: [{ + \'loader\': \'style\' + }, { + \'loader\': \'css?modules&importLoaders=1&string=test123\' + }] + }] + } +} +" +`; + +exports[`loaders transforms correctly using "loaders-2" data 1`] = ` +"export default { + module: { + rules: [{ + test: /\\.css$/, + use: [{ + loader: \'style-loader\' + }, { + loader: \'css-loader\', + options: { + modules: true + } + }] + }] + } +} +" +`; + +exports[`loaders transforms correctly using "loaders-3" data 1`] = ` +"export default { + module: { + rules:[{ + test: /\\.js$/, + use: \'eslint-loader\', + \'enforce\': \'pre\' + }] + } +} +" +`; + +exports[`loaders transforms correctly using "loaders-4" data 1`] = ` +"export default { + module: { + rules:[{ + test: /\\.js$/, + use: \'my-post-loader\', + \'enforce\': \'post\' + }] + } +} +" +`; + +exports[`loaders transforms correctly using "loaders-5" data 1`] = ` +"export default { + module: { + rules: [{ + test: /\\.js$/, + use: \'babel-loader\' + }, { + test: /\\.js$/, + use: \'eslint-loader\', + \'enforce\': \'pre\' + }] + } +} +" +`; + +exports[`loaders transforms correctly using "loaders-6" data 1`] = ` +"export default { + module: { + rules: [{ + test: /\\.js$/, + use: \'babel-loader\' + }, { + test: /\\.js$/, + use: \'my-post-loader\', + \'enforce\': \'post\' + }] + } +} +" +`; diff --git a/lib/transformations/loaders/__testfixtures__/.editorconfig b/lib/transformations/loaders/__testfixtures__/.editorconfig new file mode 100644 index 00000000000..adbdb1ba476 --- /dev/null +++ b/lib/transformations/loaders/__testfixtures__/.editorconfig @@ -0,0 +1,3 @@ +[*] +indent_style = space +indent_size = 4 diff --git a/lib/transformations/loaders/__testfixtures__/loaders-0.input.js b/lib/transformations/loaders/__testfixtures__/loaders-0.input.js new file mode 100644 index 00000000000..e0d498f6506 --- /dev/null +++ b/lib/transformations/loaders/__testfixtures__/loaders-0.input.js @@ -0,0 +1,65 @@ +export default [{ + module: { + loaders: [{ + test: /\.js$/, + loader: 'babel' + }] + } +}, { + module: { + loaders: [{ + test: /\.css$/, + loader: 'style!css?modules&importLoaders=1&string=test123' + }] + } +}, { + module: { + loaders: [{ + test: /\.css$/, + loaders: [{ + loader: 'style' + }, { + loader: 'css', + query: { + modules: true + } + }] + }] + } +}, { + module: { + preLoaders:[{ + test: /\.js$/, + loader: 'eslint' + }] + } +}, { + module: { + postLoaders:[{ + test: /\.js$/, + loader: 'my-post' + }] + } +}, { + module: { + preLoaders:[{ + test: /\.js$/, + loader: 'eslint-loader' + }], + loaders: [{ + test: /\.js$/, + loader: 'babel-loader' + }] + } +}, { + module: { + loaders: [{ + test: /\.js$/, + loader: 'babel-loader' + }], + postLoaders:[{ + test: /\.js$/, + loader: 'my-post-loader' + }] + } +}]; diff --git a/lib/transformations/loaders/__testfixtures__/loaders-1.input.js b/lib/transformations/loaders/__testfixtures__/loaders-1.input.js new file mode 100644 index 00000000000..eae75024e61 --- /dev/null +++ b/lib/transformations/loaders/__testfixtures__/loaders-1.input.js @@ -0,0 +1,8 @@ +export default { + module: { + loaders: [{ + test: /\.css$/, + loader: 'style!css?modules&importLoaders=1&string=test123' + }] + } +} diff --git a/lib/transformations/loaders/__testfixtures__/loaders-2.input.js b/lib/transformations/loaders/__testfixtures__/loaders-2.input.js new file mode 100644 index 00000000000..771404a300c --- /dev/null +++ b/lib/transformations/loaders/__testfixtures__/loaders-2.input.js @@ -0,0 +1,15 @@ +export default { + module: { + loaders: [{ + test: /\.css$/, + loaders: [{ + loader: 'style' + }, { + loader: 'css', + query: { + modules: true + } + }] + }] + } +} diff --git a/lib/transformations/loaders/__testfixtures__/loaders-3.input.js b/lib/transformations/loaders/__testfixtures__/loaders-3.input.js new file mode 100644 index 00000000000..4d49e89a89b --- /dev/null +++ b/lib/transformations/loaders/__testfixtures__/loaders-3.input.js @@ -0,0 +1,8 @@ +export default { + module: { + preLoaders:[{ + test: /\.js$/, + loader: 'eslint' + }] + } +} diff --git a/lib/transformations/loaders/__testfixtures__/loaders-4.input.js b/lib/transformations/loaders/__testfixtures__/loaders-4.input.js new file mode 100644 index 00000000000..cc3e076bed9 --- /dev/null +++ b/lib/transformations/loaders/__testfixtures__/loaders-4.input.js @@ -0,0 +1,8 @@ +export default { + module: { + postLoaders:[{ + test: /\.js$/, + loader: 'my-post' + }] + } +} diff --git a/lib/transformations/loaders/__testfixtures__/loaders-5.input.js b/lib/transformations/loaders/__testfixtures__/loaders-5.input.js new file mode 100644 index 00000000000..6fd315e4d08 --- /dev/null +++ b/lib/transformations/loaders/__testfixtures__/loaders-5.input.js @@ -0,0 +1,12 @@ +export default { + module: { + preLoaders:[{ + test: /\.js$/, + loader: 'eslint-loader' + }], + loaders: [{ + test: /\.js$/, + loader: 'babel-loader' + }] + } +} diff --git a/lib/transformations/loaders/__testfixtures__/loaders-6.input.js b/lib/transformations/loaders/__testfixtures__/loaders-6.input.js new file mode 100644 index 00000000000..184e4e1ad08 --- /dev/null +++ b/lib/transformations/loaders/__testfixtures__/loaders-6.input.js @@ -0,0 +1,12 @@ +export default { + module: { + loaders: [{ + test: /\.js$/, + loader: 'babel-loader' + }], + postLoaders:[{ + test: /\.js$/, + loader: 'my-post-loader' + }] + } +} diff --git a/lib/transformations/loaders/__testfixtures__/loaders.input.js b/lib/transformations/loaders/__testfixtures__/loaders.input.js deleted file mode 100644 index ea18dc9b021..00000000000 --- a/lib/transformations/loaders/__testfixtures__/loaders.input.js +++ /dev/null @@ -1,65 +0,0 @@ -export default [{ - module: { - loaders: [{ - test: /\.js$/, - loader: 'babel' - }] - } -}, { - module: { - loaders: [{ - test: /\.css$/, - loader: 'style!css?modules=true' - }] - } -}, { - module: { - loaders: [{ - test: /\.css$/, - loaders: [{ - loader: 'style' - }, { - loader: 'css', - query: { - modules: true - } - }] - }] - } -}, { - module: { - preLoaders:[{ - test: /\.js$/, - loader: 'eslint' - }] - } -}, { - module: { - postLoaders:[{ - test: /\.js$/, - loader: 'my-post' - }] - } -}, { - module: { - preLoaders:[{ - test: /\.js$/, - loader: 'eslint-loader' - }], - loaders: [{ - test: /\.js$/, - loader: 'babel-loader' - }] - } -}, { - module: { - loaders: [{ - test: /\.js$/, - loader: 'babel-loader' - }], - postLoaders:[{ - test: /\.js$/, - loader: 'my-post-loader' - }] - } -}]; \ No newline at end of file diff --git a/lib/transformations/loaders/__testfixtures__/loaders.output.js b/lib/transformations/loaders/__testfixtures__/loaders.output.js deleted file mode 100644 index 1d79bfd6418..00000000000 --- a/lib/transformations/loaders/__testfixtures__/loaders.output.js +++ /dev/null @@ -1,74 +0,0 @@ -export default [{ - module: { - rules: [{ - test: /\.js$/, - use: 'babel-loader' - }] - } -}, { - module: { - rules: [{ - test: /\.css$/, - use: [{ - loader: 'style-loader' - }, { - loader: 'css-loader', - options: { - modules: true - } - }] - }] - } -}, { - module: { - rules: [{ - test: /\.css$/, - use: [{ - loader: 'style-loader' - }, { - loader: 'css-loader', - options: { - modules: true - } - }] - }] - } -}, { - module: { - rules:[{ - test: /\.js$/, - use: 'eslint-loader', - enforce: 'pre' - }] - } -}, { - module: { - rules:[{ - test: /\.js$/, - use: 'my-post-loader', - enforce: 'post' - }] - } -}, { - module: { - rules: [{ - test: /\.js$/, - use: 'babel-loader' - }, { - test: /\.js$/, - use: 'eslint-loader', - enforce: 'pre' - }] - } -}, { - module: { - rules: [{ - test: /\.js$/, - use: 'babel-loader' - }, { - test: /\.js$/, - use: 'my-post-loader', - enforce: 'post' - }] - } -}]; \ No newline at end of file diff --git a/lib/transformations/loaders/__tests__/loaders.test.js b/lib/transformations/loaders/__tests__/loaders.test.js deleted file mode 100644 index 2ac7f34f3d9..00000000000 --- a/lib/transformations/loaders/__tests__/loaders.test.js +++ /dev/null @@ -1,3 +0,0 @@ -const defineTest = require('../../defineTest').defineTest; - -defineTest(__dirname, 'loaders'); diff --git a/lib/transformations/loaders/loaders.js b/lib/transformations/loaders/loaders.js index 2559c3ec3da..2bb15f17a24 100644 --- a/lib/transformations/loaders/loaders.js +++ b/lib/transformations/loaders/loaders.js @@ -1,66 +1,62 @@ -const safeTraverse = require('../safeTraverse'); +const utils = require('../utils'); module.exports = function(j, ast) { const createArrayExpression = function(p) { - var objs = p.parent.node.value.value.split('!').map(val => j.objectExpression([j.property('init', - j.identifier('loader'), - j.literal(val) - )])); - var loaderArray = j.arrayExpression(objs); + let objs = p.parent.node.value.value.split('!') + .map(val => j.objectExpression([ + utils.createProperty(j, 'loader', val) + ])); + let loaderArray = j.arrayExpression(objs); p.parent.node.value = loaderArray; return p; }; - const createLiteral = val => { - var literalVal = val; - if(val === 'true') literalVal = true; - if(val === 'false') literalVal = false; - return j.literal(literalVal); - }; - const createLoaderWithQuery = p => { - var properties = p.value.properties; - var loaderValue = properties.reduce((val, prop) => prop.key.name === 'loader' ? prop.value.value : val, ''); - var loader = loaderValue.split('?')[0]; - var query = loaderValue.split('?')[1]; - var options = query.split('&').map(option => - j.objectProperty(j.identifier(option.split('=')[0]),createLiteral(option.split('=')[1])) - ); - var loaderProp = j.property('init', j.identifier('loader'), j.literal(loader)); - var queryProp = j.property('init', j.identifier('options'), j.objectExpression(options)); + let properties = p.value.properties; + let loaderValue = properties + .reduce((val, prop) => prop.key.name === 'loader' ? prop.value.value : val, ''); + let loader = loaderValue.split('?')[0]; + let query = loaderValue.split('?')[1]; + let options = query.split('&').map(option => { + const param = option.split('='); + const key = param[0]; + const val = param[1] || true; // No value in query string means it is truthy value + return j.objectProperty(j.identifier(key), utils.createLiteral(val)); + }); + let loaderProp = utils.createProperty(j, 'loader', loader); + let queryProp = j.property('init', j.identifier('options'), j.objectExpression(options)); return j.objectExpression([loaderProp, queryProp]); }; - const findObjWithPrePostLoaders = p => { - return p.value.properties.reduce((predicate, prop) => prop.key.name === 'preLoaders' || prop.key.name === 'postLoaders' || predicate , false); - }; - const findObjWithLoaderProp = p => { - return p.value.properties.reduce((predicate, prop) => prop.key.name === 'loader' || predicate , false); - }; - const findLoaderWithQueryString = p => { - return p.value.properties.reduce((predicate, prop) => safeTraverse(prop, ['value', 'value', 'indexOf']) && prop.value.value.indexOf('?') > -1 || predicate, false); + return p.value.properties + .reduce((predicate, prop) => { + return utils.safeTraverse(prop, ['value', 'value', 'indexOf']) + && prop.value.value.indexOf('?') > -1 + || predicate; + }, false); }; - - const checkForLoader = p => p.value.name === 'loaders' && safeTraverse(p, ['parent', 'parent', 'parent', 'node', 'key', 'name']) === 'module'; + + const checkForLoader = p => p.value.name === 'loaders' && utils.safeTraverse(p, + ['parent', 'parent', 'parent', 'node', 'key', 'name']) === 'module'; const fitIntoLoaders = p => { let loaders; p.value.properties.map(prop => { const keyName = prop.key.name; - if(keyName === 'loaders') { - loaders = prop.value; + if (keyName === 'loaders') { + loaders = prop.value; } }); p.value.properties.map(prop => { const keyName = prop.key.name; - if(keyName !== 'loaders'){ + if (keyName !== 'loaders') { const enforceVal = keyName === 'preLoaders' ? 'pre' : 'post'; - + prop.value.elements.map(elem => { - elem.properties.push( j.property( 'init', j.identifier('enforce'), j.literal(enforceVal) ) ); - if(loaders && loaders.type === 'ArrayExpression') { + elem.properties.push(utils.createProperty(j, 'enforce', enforceVal)); + if (loaders && loaders.type === 'ArrayExpression') { loaders.elements.push(elem); } else { prop.key.name = 'loaders'; @@ -68,49 +64,74 @@ module.exports = function(j, ast) { }); } }); - if(loaders){ + if (loaders) { p.value.properties = p.value.properties.filter(prop => prop.key.name === 'loaders'); } return p; }; - + const prepostLoaders = () => ast - .find(j.ObjectExpression) - .filter(findObjWithPrePostLoaders) - .forEach(p => p = fitIntoLoaders(p)); + .find(j.ObjectExpression) + .filter(p => utils.findObjWithOneOfKeys(p, ['preLoaders', 'postLoaders'])) + .forEach(p => p = fitIntoLoaders(p)); const loadersToRules = () => ast - .find(j.Identifier) - .filter(checkForLoader) - .forEach(p => p.value.name = 'rules'); + .find(j.Identifier) + .filter(checkForLoader) + .forEach(p => p.value.name = 'rules'); const loaderToUse = () => ast - .find(j.Identifier) - .filter(p => (p.value.name === 'loaders' || p.value.name === 'loader') && safeTraverse(p, ['parent', 'parent', 'parent', 'parent', 'node', 'key', 'name']) === 'rules') - .forEach(p => p.value.name = 'use'); - + .find(j.Identifier) + .filter(p => { + return (p.value.name === 'loaders' || p.value.name === 'loader') + && utils.safeTraverse(p, + ['parent', 'parent', 'parent', 'parent', 'node', 'key', 'name']) === 'rules'; + }) + .forEach(p => p.value.name = 'use'); const loadersInArray = () => ast - .find(j.Identifier) - .filter(p => p.value.name === 'use' && p.parent.node.value.type === 'Literal' && p.parent.node.value.value.indexOf('!') > 0) - .forEach(createArrayExpression); + .find(j.Identifier) + .filter(p => { + return p.value.name === 'use' + && p.parent.node.value.type === 'Literal' + && p.parent.node.value.value.indexOf('!') > 0; + }) + .forEach(createArrayExpression); const loaderWithQueryParam = () => ast - .find(j.ObjectExpression) - .filter(findObjWithLoaderProp) - .filter(findLoaderWithQueryString) - .replaceWith(createLoaderWithQuery); - - const loaderWithQueryProp = () => ast.find(j.Identifier) - .filter(p => p.value.name === 'query') - .replaceWith(j.identifier('options')); - - const addLoaderSuffix = () => ast.find(j.ObjectExpression) - .forEach(path => path.value.properties.map(prop => {if((prop.key.name === 'loader' || prop.key.name ==='use') && safeTraverse(prop, ['value', 'value']) && prop.value.value.indexOf('-loader') === -1) prop.value = j.literal(prop.value.value + '-loader');})) - .toSource(); - - const transforms = [ prepostLoaders, loadersToRules, loaderToUse, loadersInArray, loaderWithQueryParam, loaderWithQueryProp, addLoaderSuffix]; + .find(j.ObjectExpression) + .filter(p => utils.findObjWithOneOfKeys(p, 'loader')) + .filter(findLoaderWithQueryString) + .replaceWith(createLoaderWithQuery); + + const loaderWithQueryProp = () => ast + .find(j.Identifier) + .filter(p => p.value.name === 'query') + .replaceWith(j.identifier('options')); + + const addLoaderSuffix = () => ast + .find(j.ObjectExpression) + .forEach(path => { + path.value.properties.forEach(prop => { + if ((prop.key.name === 'loader' || prop.key.name === 'use') + && utils.safeTraverse(prop, ['value', 'value']) + && prop.value.value.indexOf('-loader') === -1) { + prop.value = j.literal(prop.value.value + '-loader'); + } + }); + }) + .toSource(); + + const transforms = [ + prepostLoaders, + loadersToRules, + loaderToUse, + loadersInArray, + loaderWithQueryParam, + loaderWithQueryProp, + addLoaderSuffix + ]; transforms.forEach(t => t()); - return ast.toSource({quote:'single'}); -}; \ No newline at end of file + return ast; +}; diff --git a/lib/transformations/loaders/loaders.test.js b/lib/transformations/loaders/loaders.test.js new file mode 100644 index 00000000000..3e77665cbe2 --- /dev/null +++ b/lib/transformations/loaders/loaders.test.js @@ -0,0 +1,9 @@ +const defineTest = require('../defineTest'); + +defineTest(__dirname, 'loaders', 'loaders-0'); +defineTest(__dirname, 'loaders', 'loaders-1'); +defineTest(__dirname, 'loaders', 'loaders-2'); +defineTest(__dirname, 'loaders', 'loaders-3'); +defineTest(__dirname, 'loaders', 'loaders-4'); +defineTest(__dirname, 'loaders', 'loaders-5'); +defineTest(__dirname, 'loaders', 'loaders-6'); diff --git a/lib/transformations/removeDeprecatedPlugins/__snapshots__/removeDeprecatedPlugins.test.js.snap b/lib/transformations/removeDeprecatedPlugins/__snapshots__/removeDeprecatedPlugins.test.js.snap new file mode 100644 index 00000000000..da083b8d972 --- /dev/null +++ b/lib/transformations/removeDeprecatedPlugins/__snapshots__/removeDeprecatedPlugins.test.js.snap @@ -0,0 +1,19 @@ +exports[`removeDeprecatedPlugins transforms correctly using "removeDeprecatedPlugins-0" data 1`] = ` +"// Works for OccurrenceOrderPlugin +module.exports = {} +" +`; + +exports[`removeDeprecatedPlugins transforms correctly using "removeDeprecatedPlugins-1" data 1`] = ` +"// Works for DedupePlugin +module.exports = {} +" +`; + +exports[`removeDeprecatedPlugins transforms correctly using "removeDeprecatedPlugins-2" data 1`] = ` +"// Doesn\'t remove unmatched plugins +module.exports = { + plugins: [new webpack.optimize.UglifyJsPlugin()] +} +" +`; diff --git a/lib/transformations/removeDeprecatedPlugins/__testfixtures__/.editorconfig b/lib/transformations/removeDeprecatedPlugins/__testfixtures__/.editorconfig new file mode 100644 index 00000000000..adbdb1ba476 --- /dev/null +++ b/lib/transformations/removeDeprecatedPlugins/__testfixtures__/.editorconfig @@ -0,0 +1,3 @@ +[*] +indent_style = space +indent_size = 4 diff --git a/lib/transformations/removeDeprecatedPlugins/__testfixtures__/removeDeprecatedPlugins-0.input.js b/lib/transformations/removeDeprecatedPlugins/__testfixtures__/removeDeprecatedPlugins-0.input.js new file mode 100644 index 00000000000..133c4984bfd --- /dev/null +++ b/lib/transformations/removeDeprecatedPlugins/__testfixtures__/removeDeprecatedPlugins-0.input.js @@ -0,0 +1,6 @@ +// Works for OccurrenceOrderPlugin +module.exports = { + plugins: [ + new webpack.optimize.OccurrenceOrderPlugin(), + ] +} diff --git a/lib/transformations/removeDeprecatedPlugins/__testfixtures__/removeDeprecatedPlugins-1.input.js b/lib/transformations/removeDeprecatedPlugins/__testfixtures__/removeDeprecatedPlugins-1.input.js new file mode 100644 index 00000000000..a64dab79b37 --- /dev/null +++ b/lib/transformations/removeDeprecatedPlugins/__testfixtures__/removeDeprecatedPlugins-1.input.js @@ -0,0 +1,6 @@ +// Works for DedupePlugin +module.exports = { + plugins: [ + new webpack.optimize.DedupePlugin(), + ] +} diff --git a/lib/transformations/removeDeprecatedPlugins/__testfixtures__/removeDeprecatedPlugins-2.input.js b/lib/transformations/removeDeprecatedPlugins/__testfixtures__/removeDeprecatedPlugins-2.input.js new file mode 100644 index 00000000000..26150117db4 --- /dev/null +++ b/lib/transformations/removeDeprecatedPlugins/__testfixtures__/removeDeprecatedPlugins-2.input.js @@ -0,0 +1,8 @@ +// Doesn't remove unmatched plugins +module.exports = { + plugins: [ + new webpack.optimize.OccurrenceOrderPlugin(), + new webpack.optimize.UglifyJsPlugin(), + new webpack.optimize.DedupePlugin() + ] +} diff --git a/lib/transformations/removeDeprecatedPlugins/removeDeprecatedPlugins.js b/lib/transformations/removeDeprecatedPlugins/removeDeprecatedPlugins.js new file mode 100644 index 00000000000..c4b5a6c93be --- /dev/null +++ b/lib/transformations/removeDeprecatedPlugins/removeDeprecatedPlugins.js @@ -0,0 +1,21 @@ +const findPluginsByName = require('../utils').findPluginsByName; + +module.exports = function(j, ast) { + // List of deprecated plugins to remove + // each item refers to webpack.optimize.[NAME] construct + const deprecatedPlugingsList = [ + 'webpack.optimize.OccurrenceOrderPlugin', + 'webpack.optimize.DedupePlugin' + ]; + + return findPluginsByName(j, ast, deprecatedPlugingsList) + .forEach(path => { + // Check how many plugins are defined and + // if there is only last plugin left remove `plugins: []` completely + if (path.parent.value.elements.length === 1) { + j(path.parent.parent).remove(); + } else { + j(path).remove(); + } + }); +}; diff --git a/lib/transformations/removeDeprecatedPlugins/removeDeprecatedPlugins.test.js b/lib/transformations/removeDeprecatedPlugins/removeDeprecatedPlugins.test.js new file mode 100644 index 00000000000..64b89af0af3 --- /dev/null +++ b/lib/transformations/removeDeprecatedPlugins/removeDeprecatedPlugins.test.js @@ -0,0 +1,5 @@ +const defineTest = require('../defineTest'); + +defineTest(__dirname, 'removeDeprecatedPlugins', 'removeDeprecatedPlugins-0'); +defineTest(__dirname, 'removeDeprecatedPlugins', 'removeDeprecatedPlugins-1'); +defineTest(__dirname, 'removeDeprecatedPlugins', 'removeDeprecatedPlugins-2'); diff --git a/lib/transformations/removeJsonLoader/__snapshots__/removeJsonLoader.test.js.snap b/lib/transformations/removeJsonLoader/__snapshots__/removeJsonLoader.test.js.snap new file mode 100644 index 00000000000..0897d9afd2e --- /dev/null +++ b/lib/transformations/removeJsonLoader/__snapshots__/removeJsonLoader.test.js.snap @@ -0,0 +1,49 @@ +exports[`removeJsonLoader transforms correctly using "removeJsonLoader-0" data 1`] = ` +"export default { + module: { + rules: [{ + test: /\\.yml/, + use: [\'another-loader\', \'yml-loader\'] + }] + } +} + +" +`; + +exports[`removeJsonLoader transforms correctly using "removeJsonLoader-1" data 1`] = ` +"export default { + module: { + rules: [{ + test: /\\.yml/, + use: \'yml-loader\' + }] + } +} +" +`; + +exports[`removeJsonLoader transforms correctly using "removeJsonLoader-2" data 1`] = ` +"export default { + module: { + rules: [] + } +} +" +`; + +exports[`removeJsonLoader transforms correctly using "removeJsonLoader-3" data 1`] = ` +"export default { + module: { + rules: [{ + test: /\\.yml/, + use: [\'another-loader\', \'yml-loader\'] + }, { + test: /\\.yml/, + use: \'yml-loader\' + }] + } +} + +" +`; diff --git a/lib/transformations/removeJsonLoader/__testfixtures__/.editorconfig b/lib/transformations/removeJsonLoader/__testfixtures__/.editorconfig new file mode 100644 index 00000000000..adbdb1ba476 --- /dev/null +++ b/lib/transformations/removeJsonLoader/__testfixtures__/.editorconfig @@ -0,0 +1,3 @@ +[*] +indent_style = space +indent_size = 4 diff --git a/lib/transformations/removeJsonLoader/__testfixtures__/removeJsonLoader-0.input.js b/lib/transformations/removeJsonLoader/__testfixtures__/removeJsonLoader-0.input.js new file mode 100644 index 00000000000..f6c9a9da3ab --- /dev/null +++ b/lib/transformations/removeJsonLoader/__testfixtures__/removeJsonLoader-0.input.js @@ -0,0 +1,9 @@ +export default { + module: { + rules: [{ + test: /\.yml/, + use: ['json-loader', 'another-loader', 'yml-loader'] + }] + } +} + diff --git a/lib/transformations/removeJsonLoader/__testfixtures__/removeJsonLoader-1.input.js b/lib/transformations/removeJsonLoader/__testfixtures__/removeJsonLoader-1.input.js new file mode 100644 index 00000000000..05d06f79820 --- /dev/null +++ b/lib/transformations/removeJsonLoader/__testfixtures__/removeJsonLoader-1.input.js @@ -0,0 +1,8 @@ +export default { + module: { + rules: [{ + test: /\.yml/, + use: ['json-loader', 'yml-loader'] + }] + } +} diff --git a/lib/transformations/removeJsonLoader/__testfixtures__/removeJsonLoader-2.input.js b/lib/transformations/removeJsonLoader/__testfixtures__/removeJsonLoader-2.input.js new file mode 100644 index 00000000000..cc35396659d --- /dev/null +++ b/lib/transformations/removeJsonLoader/__testfixtures__/removeJsonLoader-2.input.js @@ -0,0 +1,10 @@ +export default { + module: { + rules: [ + { + test: /\.json/, + use: 'json-loader' + } + ] + } +} diff --git a/lib/transformations/removeJsonLoader/__testfixtures__/removeJsonLoader-3.input.js b/lib/transformations/removeJsonLoader/__testfixtures__/removeJsonLoader-3.input.js new file mode 100644 index 00000000000..247cb56f567 --- /dev/null +++ b/lib/transformations/removeJsonLoader/__testfixtures__/removeJsonLoader-3.input.js @@ -0,0 +1,15 @@ +export default { + module: { + rules: [{ + test: /\.yml/, + use: ['json-loader', 'another-loader', 'yml-loader'] + }, { + test: /\.yml/, + use: ['json-loader', 'yml-loader'] + }, { + test: /\.json/, + use: 'json-loader' + }] + } +} + diff --git a/lib/transformations/removeJsonLoader/removeJsonLoader.js b/lib/transformations/removeJsonLoader/removeJsonLoader.js new file mode 100644 index 00000000000..0b7bcbac56f --- /dev/null +++ b/lib/transformations/removeJsonLoader/removeJsonLoader.js @@ -0,0 +1,48 @@ +module.exports = function(j, ast) { + function getLoadersPropertyPaths(ast) { + return ast.find(j.Property, { key: { name: 'use' } }); + } + + function removeLoaderByName(path, name) { + const loadersNode = path.value.value; + switch (loadersNode.type) { + case j.ArrayExpression.name: { + let loaders = loadersNode.elements.map(p => p.value); + const loaderIndex = loaders.indexOf(name); + if (loaders.length && loaderIndex > -1) { + // Remove loader from the array + loaders.splice(loaderIndex, 1); + // and from AST + loadersNode.elements.splice(loaderIndex, 1); + } + + // If there is only one element left, convert to string + if (loaders.length === 1) { + j(path.get('value')).replaceWith(j.literal(loaders[0])); + } + break; + } + case j.Literal.name: { + // If only the loader with the matching name was used + // we can remove the whole Property node completely + if (loadersNode.value === name) { + j(path.parent).remove(); + } + break; + } + } + } + + function removeLoaders(ast) { + getLoadersPropertyPaths(ast) + .forEach(path => removeLoaderByName(path, 'json-loader')); + } + + const transforms = [ + removeLoaders + ]; + + transforms.forEach(t => t(ast)); + + return ast; +}; diff --git a/lib/transformations/removeJsonLoader/removeJsonLoader.test.js b/lib/transformations/removeJsonLoader/removeJsonLoader.test.js new file mode 100644 index 00000000000..164f0045ee1 --- /dev/null +++ b/lib/transformations/removeJsonLoader/removeJsonLoader.test.js @@ -0,0 +1,6 @@ +const defineTest = require('../defineTest'); + +defineTest(__dirname, 'removeJsonLoader', 'removeJsonLoader-0'); +defineTest(__dirname, 'removeJsonLoader', 'removeJsonLoader-1'); +defineTest(__dirname, 'removeJsonLoader', 'removeJsonLoader-2'); +defineTest(__dirname, 'removeJsonLoader', 'removeJsonLoader-3'); diff --git a/lib/transformations/resolve/__snapshots__/resolve.test.js.snap b/lib/transformations/resolve/__snapshots__/resolve.test.js.snap new file mode 100644 index 00000000000..ca978abfbcb --- /dev/null +++ b/lib/transformations/resolve/__snapshots__/resolve.test.js.snap @@ -0,0 +1,22 @@ +exports[`resolve transforms correctly 1`] = ` +"import path from \'path\'; + +export default [{ + resolve: { + modules: [path.resolve(\'/src\')] + } +}, { + resolve: { + modules: [path.resolve(\'/src\')] + } +}, { + resolve: { + modules: [path.resolve(\'/src\'), \'node_modules\'] + } +}, { + resolve: { + modules: [\'node_modules\', path.resolve(\'/src\')] + } +}]; +" +`; diff --git a/lib/transformations/resolve/__testfixtures__/.editorconfig b/lib/transformations/resolve/__testfixtures__/.editorconfig new file mode 100644 index 00000000000..adbdb1ba476 --- /dev/null +++ b/lib/transformations/resolve/__testfixtures__/.editorconfig @@ -0,0 +1,3 @@ +[*] +indent_style = space +indent_size = 4 diff --git a/lib/transformations/resolve/__testfixtures__/resolve.input.js b/lib/transformations/resolve/__testfixtures__/resolve.input.js index e0e0527a935..2b83fcf26ce 100644 --- a/lib/transformations/resolve/__testfixtures__/resolve.input.js +++ b/lib/transformations/resolve/__testfixtures__/resolve.input.js @@ -1,20 +1,20 @@ import path from 'path'; export default [{ - resolve: { - root: path.resolve('/src') - } + resolve: { + root: path.resolve('/src') + } }, { - resolve: { - root: [path.resolve('/src')] - } + resolve: { + root: [path.resolve('/src')] + } }, { - resolve: { - root: [path.resolve('/src'), 'node_modules'] - } + resolve: { + root: [path.resolve('/src'), 'node_modules'] + } }, { - resolve: { - root: path.resolve('/src'), - modules: ['node_modules'] - } -}]; \ No newline at end of file + resolve: { + root: path.resolve('/src'), + modules: ['node_modules'] + } +}]; diff --git a/lib/transformations/resolve/__testfixtures__/resolve.output.js b/lib/transformations/resolve/__testfixtures__/resolve.output.js deleted file mode 100644 index a20165e694d..00000000000 --- a/lib/transformations/resolve/__testfixtures__/resolve.output.js +++ /dev/null @@ -1,19 +0,0 @@ -import path from 'path'; - -export default [{ - resolve: { - modules: [path.resolve('/src')] - } -}, { - resolve: { - modules: [path.resolve('/src')] - } -}, { - resolve: { - modules: [path.resolve('/src'), 'node_modules'] - } -}, { - resolve: { - modules: ['node_modules', path.resolve('/src')] - } -}]; \ No newline at end of file diff --git a/lib/transformations/resolve/__tests__/resolve.test.js b/lib/transformations/resolve/__tests__/resolve.test.js deleted file mode 100644 index 9096049f910..00000000000 --- a/lib/transformations/resolve/__tests__/resolve.test.js +++ /dev/null @@ -1,3 +0,0 @@ -const defineTest = require('../../defineTest').defineTest; - -defineTest(__dirname, 'resolve'); diff --git a/lib/transformations/resolve/resolve.js b/lib/transformations/resolve/resolve.js index 77329ded59e..3912703e9e0 100644 --- a/lib/transformations/resolve/resolve.js +++ b/lib/transformations/resolve/resolve.js @@ -1,29 +1,32 @@ module.exports = function transformer(j, ast) { const getRootVal = p => { - return p.node.value.properties.filter(prop => prop.key.name ==='root')[0]; + return p.node.value.properties.filter(prop => prop.key.name === 'root')[0]; }; - + const getRootIndex = p => { - return p.node.value.properties.reduce((rootIndex, prop, index) => prop.key.name === 'root' ? index: rootIndex, -1); + return p.node.value.properties + .reduce((rootIndex, prop, index) => { + return prop.key.name === 'root' ? index : rootIndex; + }, -1); }; - + const isModulePresent = p => { - const modules = p.node.value.properties.filter(prop => prop.key.name ==='modules'); + const modules = p.node.value.properties.filter(prop => prop.key.name === 'modules'); return modules.length > 0 && modules[0]; }; - + const createModuleArray = p => { const rootVal = getRootVal(p); let modulesVal = null; - if(rootVal.value.type === 'ArrayExpression') { + if (rootVal.value.type === 'ArrayExpression') { modulesVal = rootVal.value.elements; } else { modulesVal = [rootVal.value]; } let module = isModulePresent(p); - if(!module) { + if (!module) { module = j.property('init', j.identifier('modules'), j.arrayExpression(modulesVal)); p.node.value.properties = p.node.value.properties.concat([module]); } else { @@ -35,8 +38,12 @@ module.exports = function transformer(j, ast) { }; return ast - .find(j.Property) - .filter(p => p.node.key.name === 'resolve' && p.node.value.properties.filter(prop => prop.key.name ==='root').length === 1) - .forEach(createModuleArray) - .toSource(); + .find(j.Property) + .filter(p => { + return p.node.key.name === 'resolve' + && p.node.value.properties + .filter(prop => prop.key.name === 'root') + .length === 1; + }) + .forEach(createModuleArray); }; diff --git a/lib/transformations/resolve/resolve.test.js b/lib/transformations/resolve/resolve.test.js new file mode 100644 index 00000000000..30a26b5348b --- /dev/null +++ b/lib/transformations/resolve/resolve.test.js @@ -0,0 +1,3 @@ +const defineTest = require('../defineTest'); + +defineTest(__dirname, 'resolve'); diff --git a/lib/transformations/safeTraverse.js b/lib/transformations/safeTraverse.js deleted file mode 100644 index 63dae968a27..00000000000 --- a/lib/transformations/safeTraverse.js +++ /dev/null @@ -1,13 +0,0 @@ -module.exports = function safeTraverse (obj, paths) { - let val = obj; - let idx = 0; - - while (idx < paths.length) { - if (!val) { - return null; - } - val = val[paths[idx]]; - idx++; - } - return val; -}; diff --git a/lib/transformations/uglifyJsPlugin/__snapshots__/uglifyJsPlugin.test.js.snap b/lib/transformations/uglifyJsPlugin/__snapshots__/uglifyJsPlugin.test.js.snap new file mode 100644 index 00000000000..92ef1d87a12 --- /dev/null +++ b/lib/transformations/uglifyJsPlugin/__snapshots__/uglifyJsPlugin.test.js.snap @@ -0,0 +1,35 @@ +exports[`uglifyJsPlugin transforms correctly using "uglifyJsPlugin-0" data 1`] = ` +"module.exports = { + plugins: [ + new webpack.optimize.UglifyJsPlugin({ + sourceMap: true + }) + ] +} +" +`; + +exports[`uglifyJsPlugin transforms correctly using "uglifyJsPlugin-1" data 1`] = ` +"module.exports = { + devtool: \"source-map\", + plugins: [ + new webpack.optimize.UglifyJsPlugin({ + sourceMap: true + }) + ] +} +" +`; + +exports[`uglifyJsPlugin transforms correctly using "uglifyJsPlugin-2" data 1`] = ` +"module.exports = { + devtool: \"cheap-source-map\", + plugins: [ + new webpack.optimize.UglifyJsPlugin({ + compress: {}, + sourceMap: true + }) + ] +} +" +`; diff --git a/lib/transformations/uglifyJsPlugin/__testfixtures__/.editorconfig b/lib/transformations/uglifyJsPlugin/__testfixtures__/.editorconfig new file mode 100644 index 00000000000..adbdb1ba476 --- /dev/null +++ b/lib/transformations/uglifyJsPlugin/__testfixtures__/.editorconfig @@ -0,0 +1,3 @@ +[*] +indent_style = space +indent_size = 4 diff --git a/lib/transformations/uglifyJsPlugin/__testfixtures__/uglifyJsPlugin-0.input.js b/lib/transformations/uglifyJsPlugin/__testfixtures__/uglifyJsPlugin-0.input.js new file mode 100644 index 00000000000..900f7042075 --- /dev/null +++ b/lib/transformations/uglifyJsPlugin/__testfixtures__/uglifyJsPlugin-0.input.js @@ -0,0 +1,5 @@ +module.exports = { + plugins: [ + new webpack.optimize.UglifyJsPlugin() + ] +} diff --git a/lib/transformations/uglifyJsPlugin/__testfixtures__/uglifyJsPlugin-1.input.js b/lib/transformations/uglifyJsPlugin/__testfixtures__/uglifyJsPlugin-1.input.js new file mode 100644 index 00000000000..57d7eb1c192 --- /dev/null +++ b/lib/transformations/uglifyJsPlugin/__testfixtures__/uglifyJsPlugin-1.input.js @@ -0,0 +1,6 @@ +module.exports = { + devtool: "source-map", + plugins: [ + new webpack.optimize.UglifyJsPlugin({}) + ] +} diff --git a/lib/transformations/uglifyJsPlugin/__testfixtures__/uglifyJsPlugin-2.input.js b/lib/transformations/uglifyJsPlugin/__testfixtures__/uglifyJsPlugin-2.input.js new file mode 100644 index 00000000000..3c13f02b203 --- /dev/null +++ b/lib/transformations/uglifyJsPlugin/__testfixtures__/uglifyJsPlugin-2.input.js @@ -0,0 +1,8 @@ +module.exports = { + devtool: "cheap-source-map", + plugins: [ + new webpack.optimize.UglifyJsPlugin({ + compress: {} + }) + ] +} diff --git a/lib/transformations/uglifyJsPlugin/uglifyJsPlugin.js b/lib/transformations/uglifyJsPlugin/uglifyJsPlugin.js new file mode 100644 index 00000000000..f5c4a12af3b --- /dev/null +++ b/lib/transformations/uglifyJsPlugin/uglifyJsPlugin.js @@ -0,0 +1,25 @@ +const findPluginsByName = require('../utils').findPluginsByName; + +module.exports = function(j, ast) { + + function createSourceMapsProperty() { + return j.property('init', j.identifier('sourceMap'), j.identifier('true')); + } + + return findPluginsByName(j, ast, ['webpack.optimize.UglifyJsPlugin']) + .forEach(path => { + const args = path.value.arguments; + + if (args.length) { + // Plugin is called with object as arguments + j(path) + .find(j.ObjectExpression) + .get('properties') + .value + .push(createSourceMapsProperty()); + } else { + // Plugin is called without arguments + args.push(j.objectExpression([createSourceMapsProperty()])); + } + }); +}; diff --git a/lib/transformations/uglifyJsPlugin/uglifyJsPlugin.test.js b/lib/transformations/uglifyJsPlugin/uglifyJsPlugin.test.js new file mode 100644 index 00000000000..a0c309ccb1e --- /dev/null +++ b/lib/transformations/uglifyJsPlugin/uglifyJsPlugin.test.js @@ -0,0 +1,5 @@ +const defineTest = require('../defineTest'); + +defineTest(__dirname, 'uglifyJsPlugin', 'uglifyJsPlugin-0'); +defineTest(__dirname, 'uglifyJsPlugin', 'uglifyJsPlugin-1'); +defineTest(__dirname, 'uglifyJsPlugin', 'uglifyJsPlugin-2'); diff --git a/lib/transformations/utils.js b/lib/transformations/utils.js new file mode 100644 index 00000000000..5bf580d1de8 --- /dev/null +++ b/lib/transformations/utils.js @@ -0,0 +1,237 @@ +function safeTraverse(obj, paths) { + let val = obj; + let idx = 0; + + while (idx < paths.length) { + if (!val) { + return null; + } + val = val[paths[idx]]; + idx++; + } + return val; +} + +// Convert nested MemberExpressions to strings like webpack.optimize.DedupePlugin +function memberExpressionToPathString(path) { + if (path && path.object) { + return [memberExpressionToPathString(path.object), path.property.name].join('.'); + } + return path.name; +} + +// Convert Array like ['webpack', 'optimize', 'DedupePlugin'] to nested MemberExpressions +function pathsToMemberExpression(j, paths) { + if (!paths.length) { + return null; + } else if (paths.length === 1) { + return j.identifier(paths[0]); + } else { + const first = paths.slice(0, 1); + const rest = paths.slice(1); + return j.memberExpression( + pathsToMemberExpression(j, rest), + pathsToMemberExpression(j, first) + ); + } +} + +/* +* @function findPluginsByName +* +* Find paths that match `new name.space.PluginName()` for a given array of plugin names +* +* @param j — jscodeshift API +* @param { Node } node - Node to start search from +* @param { Array } pluginNamesArray - Array of plugin names like `webpack.optimize.LoaderOptionsPlugin` +* @returns Path + * */ +function findPluginsByName(j, node, pluginNamesArray) { + return node + .find(j.NewExpression) + .filter(path => { + return pluginNamesArray.some( + plugin => memberExpressionToPathString(path.get('callee').value) === plugin + ); + }); +} + +/* + * @function findPluginsRootNodes + * + * Finds the path to the `plugins: []` node + * + * @param j — jscodeshift API + * @param { Node } node - Node to start search from + * @returns Path + * */ +function findPluginsRootNodes(j, node) { + return node.find(j.Property, { key: { name: 'plugins' } }); +} + +/* + * @function createProperty + * + * Creates an Object's property with a given key and value + * + * @param j — jscodeshift API + * @param { string | number } key - Property key + * @param { string | number | boolean } value - Property value + * @returns Node + * */ +function createProperty(j, key, value) { + return j.property( + 'init', + createLiteral(j, key), + createLiteral(j, value) + ); +} + +/* + * @function createLiteral + * + * Creates an appropriate literal property + * + * @param j — jscodeshift API + * @param { string | boolean | number } val + * @returns { Node } + * */ + +function createLiteral(j, val) { + let literalVal = val; + // We'll need String to native type conversions + if (typeof val === 'string') { + // 'true' => true + if (val === 'true') literalVal = true; + // 'false' => false + if (val === 'false') literalVal = false; + // '1' => 1 + if (!isNaN(Number(val))) literalVal = Number(val); + } + return j.literal(literalVal); +} + +/* + * @function createOrUpdatePluginByName + * + * Findes or creates a node for a given plugin name string with options object + * If plugin decalaration already exist, options are merged. + * + * @param j — jscodeshift API + * @param { NodePath } rooNodePath - `plugins: []` NodePath where plugin should be added. See https://github.com/facebook/jscodeshift/wiki/jscodeshift-Documentation#nodepaths + * @param { string } pluginName - ex. `webpack.optimize.LoaderOptionsPlugin` + * @param { Object } options - plugin options + * @returns void + * */ +function createOrUpdatePluginByName(j, rootNodePath, pluginName, options) { + const pluginInstancePath = findPluginsByName(j, j(rootNodePath), [pluginName]); + let optionsProps; + if (options) { + optionsProps = Object.keys(options).map(key => { + return createProperty(j, key, options[key]); + }); + } + + // If plugin declaration already exist + if (pluginInstancePath.size()) { + pluginInstancePath.forEach(path => { + // There are options we want to pass as argument + if (optionsProps) { + const args = path.value.arguments; + if (args.length) { + // Plugin is called with object as arguments + // we will merge those objects + let currentProps = j(path) + .find(j.ObjectExpression) + .get('properties'); + + optionsProps.forEach(opt => { + // Search for same keys in the existing object + const existingProps = j(currentProps) + .find(j.Identifier) + .filter(path => opt.key.value === path.value.name); + + if (existingProps.size()) { + // Replacing values for the same key + existingProps.forEach(path => { + j(path.parent).replaceWith(opt); + }); + } else { + // Adding new key:values + currentProps.value.push(opt); + } + }); + + } else { + // Plugin is called without arguments + args.push( + j.objectExpression(optionsProps) + ); + } + } + }); + } else { + let argumentsArray = []; + if (optionsProps) { + argumentsArray = [j.objectExpression(optionsProps)]; + } + const loaderPluginInstance = j.newExpression( + pathsToMemberExpression(j, pluginName.split('.').reverse()), + argumentsArray + ); + rootNodePath.value.elements.push(loaderPluginInstance); + } +} + +/* + * @function findVariableToPlugin + * + * Finds the variable to which a third party plugin is assigned to + * + * @param j — jscodeshift API + * @param { Node } rootNode - `plugins: []` Root Node. See https://github.com/facebook/jscodeshift/wiki/jscodeshift-Documentation#nodepaths + * @param { string } pluginPackageName - ex. `extract-text-plugin` + * @returns { string } variable name - ex. 'var s = require(s) gives "s"` + * */ + +function findVariableToPlugin(j, rootNode, pluginPackageName){ + const moduleVarNames = rootNode.find(j.VariableDeclarator) + .filter(j.filters.VariableDeclarator.requiresModule(pluginPackageName)) + .nodes(); + if (moduleVarNames.length === 0) return null; + return moduleVarNames.pop().id.name; +} + +/* +* @function isType +* +* Returns true if type is given type +* @param { Node} path - pathNode +* @param { string } type - node type +* @returns {boolean} +*/ + +function isType(path, type) { + return path.type === type; +} + +function findObjWithOneOfKeys (p, keyNames) { + return p.value.properties + .reduce((predicate, prop) => { + const name = prop.key.name; + return keyNames.indexOf(name) > -1 + || predicate; + }, false); +} + +module.exports = { + safeTraverse, + createProperty, + findPluginsByName, + findPluginsRootNodes, + createOrUpdatePluginByName, + findVariableToPlugin, + isType, + createLiteral, + findObjWithOneOfKeys +}; diff --git a/lib/transformations/utils.test.js b/lib/transformations/utils.test.js new file mode 100644 index 00000000000..24160eeaf1d --- /dev/null +++ b/lib/transformations/utils.test.js @@ -0,0 +1,160 @@ +const j = require('jscodeshift/dist/core'); +const utils = require('./utils'); + +describe('utils', () => { + describe('createProperty', () => { + it('should create properties for Boolean', () => { + const res = utils.createProperty(j, 'foo', true); + expect(j(j.objectExpression([res])).toSource()).toMatchSnapshot(); + }); + + it('should create properties for Number', () => { + const res = utils.createProperty(j, 'foo', -1); + expect(j(j.objectExpression([res])).toSource()).toMatchSnapshot(); + }); + + it('should create properties for String', () => { + const res = utils.createProperty(j, 'foo', 'bar'); + expect(j(j.objectExpression([res])).toSource()).toMatchSnapshot(); + }); + + it('should create properties for complex keys', () => { + const res = utils.createProperty(j, 'foo-bar', 'bar'); + expect(j(j.objectExpression([res])).toSource()).toMatchSnapshot(); + }); + + it('should create properties for non-literal keys', () => { + const res = utils.createProperty(j, 1, 'bar'); + expect(j(j.objectExpression([res])).toSource()).toMatchSnapshot(); + }); + }); + + describe('findPluginsByName', () => { + it('should find plugins in AST', () => { + const ast = j(` +{ foo: new webpack.optimize.UglifyJsPlugin() } +`); + const res = utils.findPluginsByName(j, ast, + ['webpack.optimize.UglifyJsPlugin']); + expect(res.size()).toEqual(1); + }); + + it('should find all plugins in AST', () => { + const ast = j(` +[ + new UglifyJsPlugin(), + new TestPlugin() +] +`); + const res = utils.findPluginsByName(j, ast, + ['UglifyJsPlugin', 'TestPlugin']); + expect(res.size()).toEqual(2); + }); + + it('should not find false positives', () => { + const ast = j(` +{ foo: new UglifyJsPlugin() } +`); + const res = utils.findPluginsByName(j, ast, + ['webpack.optimize.UglifyJsPlugin']); + expect(res.size()).toEqual(0); + }); + }); + + describe('findPluginsRootNodes', () => { + it('should find plugins: [] nodes', () => { + const ast = j(` +var a = { plugins: [], foo: { plugins: [] } } +`); + const res = utils.findPluginsRootNodes(j, ast); + expect(res.size()).toEqual(2); + }); + + it('should not find plugins: [] nodes', () => { + const ast = j(` +var a = { plugs: [] } +`); + const res = utils.findPluginsRootNodes(j, ast); + expect(res.size()).toEqual(0); + }); + }); + + describe('createOrUpdatePluginByName', () => { + it('should create a new plugin without arguments', () => { + const ast = j('{ plugins: [] }'); + ast + .find(j.ArrayExpression) + .forEach(node => { + utils.createOrUpdatePluginByName(j, node, 'Plugin'); + }); + expect(ast.toSource()).toMatchSnapshot(); + }); + + it('should create a new plugin with arguments', () => { + const ast = j('{ plugins: [] }'); + ast + .find(j.ArrayExpression) + .forEach(node => { + utils.createOrUpdatePluginByName(j, node, 'Plugin', { foo: 'bar' }); + }); + expect(ast.toSource()).toMatchSnapshot(); + }); + + it('should add an object as an argument', () => { + const ast = j('[new Plugin()]'); + ast + .find(j.ArrayExpression) + .forEach(node => { + utils.createOrUpdatePluginByName(j, node, 'Plugin', { foo: true }); + }); + expect(ast.toSource()).toMatchSnapshot(); + }); + + it('should merge options objects', () => { + const ast = j('[new Plugin({ foo: true })]'); + ast + .find(j.ArrayExpression) + .forEach(node => { + utils.createOrUpdatePluginByName(j, node, 'Plugin', { bar: 'baz', foo: false }); + utils.createOrUpdatePluginByName(j, node, 'Plugin', { 'baz-long': true }); + }); + expect(ast.toSource()).toMatchSnapshot(); + }); + }); + + describe('findVariableToPlugin', () => { + it('should find the variable name of a plugin', () => { + const ast = j(` + var packageName = require('package-name'); + var someOtherVar = somethingElse; + var otherPackage = require('other-package'); + `); + const foundVar = utils.findVariableToPlugin(j, ast, 'other-package'); + expect(foundVar).toEqual('otherPackage'); + }); + }); + + describe('createLiteral', () => { + it('should create basic literal', () => { + const literal = utils.createLiteral(j, 'strintLiteral'); + expect(literal).toMatchSnapshot(); + }); + it('should create boolean', () => { + const literal = utils.createLiteral(j, 'true'); + expect(literal).toMatchSnapshot(); + }); + }); + + describe('findObjWithOneOfKeys', () => { + it('should find keys', () => { + const ast = j(` + var ab = { + a: 1, + b: 2 + } + `); + expect(ast.find(j.ObjectExpression) + .filter(p => utils.findObjWithOneOfKeys(p, ['a'])).size()).toEqual(1); + }); + }); +});