diff --git a/lib/migrate/extractTextPlugin/__snapshots__/extractTextPlugin.test.js.snap b/lib/migrate/extractTextPlugin/__snapshots__/extractTextPlugin.test.js.snap index c60c600d9c2..1d9b439a0e5 100644 --- a/lib/migrate/extractTextPlugin/__snapshots__/extractTextPlugin.test.js.snap +++ b/lib/migrate/extractTextPlugin/__snapshots__/extractTextPlugin.test.js.snap @@ -1,23 +1,71 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`extractTextPlugin transforms correctly 1`] = ` -"let ExtractTextPlugin = require('extract-text-webpack-plugin'); -let HTMLWebpackPlugin = require('html-webpack-plugin'); +exports[`extractTextPlugin transforms correctly using "extractTextPlugin-0" data 1`] = ` +"const MiniCssExtractPlugin = require('mini-css-extract-plugin'); module.export = { module: { rules: [ { test: /\\\\.css$/, - use: ExtractTextPlugin.extract({ - fallback: 'style-loader', - use: 'css-loader' - }) + use: (process.env.NODE_ENV === 'production' ? [MiniCssExtractPlugin.loader, 'css-loader'] : ['style-loader', { loader: 'test-object-loader' }, 'css-loader']) } ] }, plugins: [ - new ExtractTextPlugin(\\"styles.css\\"), + new Foo(), + new MiniCssExtractPlugin({ + filename: 'foo.css' + }) + ] +} +" +`; + +exports[`extractTextPlugin transforms correctly using "extractTextPlugin-1" data 1`] = ` +"const MiniCssExtractPlugin = require('mini-css-extract-plugin'); + +module.export = { + module: { + rules: [ + { + test: /\\\\.css$/, + use: ['css-hot-loader'].concat((process.env.NODE_ENV === 'production' ? [MiniCssExtractPlugin.loader, 'css-loader'] : ['style-loader', 'css-loader'])) + } + ] + }, + plugins: [ + new Foo(), + new MiniCssExtractPlugin({ + filename: 'foo.css' + }) + ] +} +" +`; + +exports[`extractTextPlugin transforms correctly using "extractTextPlugin-2" data 1`] = ` +"const MiniCssExtractPlugin = require('mini-css-extract-plugin'); + +module.export = { + module: { + rules: [ + { + test: /\\\\.css$/, + use: [ + 'foo-loader', + (process.env.NODE_ENV === 'production' ? [MiniCssExtractPlugin.loader, 'css-loader'] : ['style-loader', 'css-loader']), + 'bar-loader' + ] + } + ] + }, + plugins: [ + new Foo(), + new MiniCssExtractPlugin({ + filename: 'foo.css', + publicPath: 'public' + }) ] } " diff --git a/lib/migrate/extractTextPlugin/__testfixtures__/extractTextPlugin-0.input.js b/lib/migrate/extractTextPlugin/__testfixtures__/extractTextPlugin-0.input.js new file mode 100644 index 00000000000..4345d5da1a7 --- /dev/null +++ b/lib/migrate/extractTextPlugin/__testfixtures__/extractTextPlugin-0.input.js @@ -0,0 +1,25 @@ +const ExtractTextPlugin = require('extract-text-webpack-plugin'); + +module.export = { + module: { + rules: [ + { + test: /\.css$/, + use: ExtractTextPlugin.extract({ + use: 'css-loader', + fallback: ['style-loader', { loader: 'test-object-loader' }] + }) + } + ] + }, + plugins: [ + new Foo(), + new ExtractTextPlugin({ + id: 'foo', + filename: 'foo.css', + allChunks: true, + disable: false, + ignoreOrder: true + }) + ] +} diff --git a/lib/migrate/extractTextPlugin/__testfixtures__/extractTextPlugin-1.input.js b/lib/migrate/extractTextPlugin/__testfixtures__/extractTextPlugin-1.input.js new file mode 100644 index 00000000000..b84d1ea29ef --- /dev/null +++ b/lib/migrate/extractTextPlugin/__testfixtures__/extractTextPlugin-1.input.js @@ -0,0 +1,25 @@ +const ExtractTextPlugin = require('extract-text-webpack-plugin'); + +module.export = { + module: { + rules: [ + { + test: /\.css$/, + use: ['css-hot-loader'].concat(ExtractTextPlugin.extract({ + use: 'css-loader', + fallback: 'style-loader' + })) + } + ] + }, + plugins: [ + new Foo(), + new ExtractTextPlugin({ + id: 'foo', + filename: 'foo.css', + allChunks: true, + disable: false, + ignoreOrder: true + }) + ] +} diff --git a/lib/migrate/extractTextPlugin/__testfixtures__/extractTextPlugin-2.input.js b/lib/migrate/extractTextPlugin/__testfixtures__/extractTextPlugin-2.input.js new file mode 100644 index 00000000000..c89af82aa9a --- /dev/null +++ b/lib/migrate/extractTextPlugin/__testfixtures__/extractTextPlugin-2.input.js @@ -0,0 +1,30 @@ +const ExtractTextPlugin = require('extract-text-webpack-plugin'); + +module.export = { + module: { + rules: [ + { + test: /\.css$/, + use: [ + 'foo-loader', + ExtractTextPlugin.extract({ + use: 'css-loader', + fallback: 'style-loader', + publicPath: 'public' + }), + 'bar-loader' + ] + } + ] + }, + plugins: [ + new Foo(), + new ExtractTextPlugin({ + id: 'foo', + filename: 'foo.css', + allChunks: true, + disable: false, + ignoreOrder: true + }) + ] +} diff --git a/lib/migrate/extractTextPlugin/__testfixtures__/extractTextPlugin.input.js b/lib/migrate/extractTextPlugin/__testfixtures__/extractTextPlugin.input.js deleted file mode 100644 index f578bb4342d..00000000000 --- a/lib/migrate/extractTextPlugin/__testfixtures__/extractTextPlugin.input.js +++ /dev/null @@ -1,16 +0,0 @@ -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/migrate/extractTextPlugin/extractTextPlugin.js b/lib/migrate/extractTextPlugin/extractTextPlugin.js index 20158d9081c..ef9a38431b4 100644 --- a/lib/migrate/extractTextPlugin/extractTextPlugin.js +++ b/lib/migrate/extractTextPlugin/extractTextPlugin.js @@ -1,58 +1,97 @@ -const utils = require("../../utils/ast-utils"); +const { + addConditionalLoader, + addRequire, + createPluginByName, + findLoaderFunction, + getLoaderOptionValue, + getPluginOptionValue, + pathToArray, + removePluginByName, + removeRequire +} = require("../../utils/ast-utils"); /** * - * Check whether `node` is the invocation of the plugin denoted by `pluginName` - * - * @param {Object} j - jscodeshift top-level import - * @param {Node} node - ast node to check - * @param {String} pluginName - name of the plugin - * @returns {Boolean} isPluginInvocation - whether `node` is the invocation of the plugin denoted by `pluginName` - */ - -function findInvocation(j, node, pluginName) { - let found = false; - found = - j(node) - .find(j.MemberExpression) - .filter(p => p.get("object").value.name === pluginName) - .size() > 0; - return found; -} - -/** - * - * Transform for ExtractTextPlugin arguments. Consolidates arguments into single options object. + * Transform for ExtractTextPlugin. Replaces it with MiniCssExtractPlugin and translates + * configuration where possible. * * @param {Object} j - jscodeshift top-level import * @param {Node} ast - jscodeshift ast to transform * @returns {Node} ast - jscodeshift ast */ +module.exports = function transformExtractTextPlugin(j, ast) { + const devLoaders = []; + const prodLoaders = []; + + let elements; + let loaderIndex; + let loaderPath; + let newElements; + let pluginExists; + let requireExists; + + const filename = getPluginOptionValue(j, ast, "ExtractTextPlugin", "filename"); + const publicPath = getLoaderOptionValue(j, ast, "ExtractTextPlugin", "publicPath"); + const currentFallbackLoader = getLoaderOptionValue(j, ast, "ExtractTextPlugin", "fallback"); + const currentLoader = getLoaderOptionValue(j, ast, "ExtractTextPlugin", "use"); -module.exports = function(j, ast) { - const changeArguments = function(p) { - const args = p.value.arguments; - - 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]; + const loaderOptions = filename || publicPath ? {} : null; + filename && Object.assign(loaderOptions, { filename }); + publicPath && Object.assign(loaderOptions, { publicPath: publicPath.value }); + + // Remove require for old plugin + requireExists = removeRequire(j, ast, "ExtractTextPlugin"); + + // Add require for new plugin + requireExists && addRequire(j, ast, "MiniCssExtractPlugin", "mini-css-extract-plugin"); + + // Remove old plugin + pluginExists = removePluginByName(j, ast, "ExtractTextPlugin"); + + // Add new plugin + pluginExists && createPluginByName(j, ast, "MiniCssExtractPlugin", loaderOptions); + + // Remove old loader + const loaderCallPath = findLoaderFunction(j, ast, "ExtractTextPlugin"); + if (loaderCallPath) { + loaderPath = loaderCallPath.parent.value; + loaderPath.elements && (elements = [...loaderPath.elements]); + j(loaderCallPath).remove(); + } + + // Build loader arrays + currentLoader && pathToArray(currentLoader, switchedLoader => { + const loader = j.memberExpression(j.identifier("MiniCssExtractPlugin"), j.identifier("loader"), false); + prodLoaders.push(loader, ...switchedLoader); + currentFallbackLoader && pathToArray(currentFallbackLoader, switchedFallbackLoader => { + devLoaders.push(...switchedFallbackLoader, ...switchedLoader); + }); + }); + + // Add new loader + const loader = currentFallbackLoader ? + addConditionalLoader(j, prodLoaders, devLoaders) : + j.arrayExpression(prodLoaders); + + if (loaderPath) { + switch (loaderPath.type) { + case "CallExpression": + loaderPath.arguments = [loader]; + break; + case "Property": + loaderPath.value = loader; + break; + case "ArrayExpression": + loaderIndex = elements.findIndex(e => e.callee && e.callee.object.name === "ExtractTextPlugin"); + newElements = [ + ...elements.slice(0, loaderIndex), + loader, + ...elements.slice(loaderIndex + 1) + ]; + loaderPath.elements = newElements; + break; } - 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); + } + + return ast; }; diff --git a/lib/migrate/extractTextPlugin/extractTextPlugin.test.js b/lib/migrate/extractTextPlugin/extractTextPlugin.test.js index f5a141d5562..153121a1824 100644 --- a/lib/migrate/extractTextPlugin/extractTextPlugin.test.js +++ b/lib/migrate/extractTextPlugin/extractTextPlugin.test.js @@ -2,4 +2,6 @@ const defineTest = require("../../utils//defineTest"); -defineTest(__dirname, "extractTextPlugin"); +defineTest(__dirname, "extractTextPlugin", "extractTextPlugin-0"); +defineTest(__dirname, "extractTextPlugin", "extractTextPlugin-1"); +defineTest(__dirname, "extractTextPlugin", "extractTextPlugin-2"); diff --git a/lib/utils/ast-utils.js b/lib/utils/ast-utils.js index 43657059acf..4e88b71dcac 100644 --- a/lib/utils/ast-utils.js +++ b/lib/utils/ast-utils.js @@ -736,6 +736,208 @@ function createFunctionWithArguments(j, p, name) { ]); } +/** + * Adds a require statement for a given package + * + * @param {any} j — jscodeshift API + * @param {Node} node - Node to start search from + * @param {String} constName - Name of require + * @param {String} packagePath - path of required package + * @returns {Void} + */ + +function addRequire(j, node, constName, packagePath) { + const pathRequire = getRequire(j, constName, packagePath); + node.find(j.Program).replaceWith(p => j.program([].concat(pathRequire).concat(p.value.body))); +} + +/** + * + * Creates a new plugin with the specified options + * + * @param {any} j — jscodeshift API + * @param {Node} node - Node to start search from + * @param {String} pluginName - ex. `webpack.LoaderOptionsPlugin` + * @param {Object} options - plugin options + * @returns {Void} + */ + +function createPluginByName(j, node, pluginName, options) { + node.find(j.Identifier, { name: "plugins" }) + .filter(path => safeTraverse(path, ["parent", "value"])) + .forEach(path => { + createOrUpdatePluginByName(j, path.parent.value, pluginName, options); + }); +} + +/** + * + * Gets the value associated with a plugin option + * + * @param {any} j — jscodeshift API + * @param {Node} node - Node to start search from + * @param {String} pluginName - Name of plugin on which to look for options + * @param {String} name - Key of an option to grab from the plugin + * @returns {any} - Plugin option value + */ + +function getPluginOptionValue(j, node, pluginName, name) { + let value; + const parentPath = ["parent", "parent", "parent", "value", "callee"]; + const valuePath = ["parent", "value", "value", "value"]; + node.find(j.Identifier, { name }) + .filter(path => safeTraverse(path, parentPath)) + .filter(path => j.match(parentPath.reduce((a, b) => a[b], path), { name: pluginName })) + .filter(path => safeTraverse(path, valuePath)) + .forEach(path => { + value = valuePath.reduce((a, b) => a[b], path); + }); + return value; +} + +/** + * + * Gets the value associated with a loader option + * + * @param {any} j — jscodeshift API + * @param {Node} node - Node to start search from + * @param {String} pluginName - Name of plugin on which to look for options + * @param {String} name - Key of an option to grab from the plugin + * @returns {any} - Loader option value + */ + +function getLoaderOptionValue(j, node, loaderName, name) { + let value = null; + const parentPath = ["parent", "parent", "parent", "value", "callee", "object"]; + const valuePath = ["parent", "value", "value"]; + node.find(j.Identifier, { name }) + .filter(path => safeTraverse(path, parentPath)) + .filter(path => j.match(parentPath.reduce((a, b) => a[b], path), { name: loaderName })) + .filter(path => safeTraverse(path, valuePath)) + .forEach(path => { + value = valuePath.reduce((a, b) => a[b], path); + }); + return value; +} + +/** + * + * Finds and removes a node for a given plugin name. If the plugin + * is the last in the plugins array, the array is also removed. + * + * @param {any} j — jscodeshift API + * @param {Node} node - Node to start search from + * @param {String} pluginName - name of the plugin to remove + * @returns {Node | Void} - path to the root webpack configuration object if plugin is found + */ + +function removePluginByName(j, node, pluginName) { + let rootPath; + + findPluginsByName(j, node, [pluginName]) + .filter(path => safeTraverse(path, ["parent", "value"])) + .forEach(path => { + rootPath = safeTraverse(path, ["parent", "parent", "parent", "value"]); + const arrayPath = path.parent.value; + if (arrayPath.elements && arrayPath.elements.length === 1) { + j(path.parent.parent).remove(); + } else { + j(path).remove(); + } + }); + + return rootPath; +} + +/** + * + * Removes a require statement for a given package + * + * @param {any} j — jscodeshift API + * @param {Node} node - Node to start search from + * @param {String} name - dependancy to remove + * @returns {Boolean} - true if the require statement was found + */ + +function removeRequire(j, node, name) { + let found = false; + node.find(j.Identifier, { name }) + .filter(path => safeTraverse(path, ["parent", "parent", "value"])) + .filter(path => isType(path.parent.parent.value, "VariableDeclaration")) + .forEach(path => { + j(path.parent.parent).remove(); + found = true; + }); + return found; +} + +/** + * + * Calls an function with a path wrapped in an array, using path.elements if available + * + * @param {NodePath} path - path to wrap in array + * @param {Function} action - called with array path + * @returns {Void} + */ + +function pathToArray(path, action) { + if (isType(path, "ArrayExpression")) { + action(path.elements); + return; + } + action([path]); +} + +/** + * Finds a loader call expression, e.g. use: ExtractTextPlugin.extract({ ... }) + * + * @param {any} j — jscodeshift API + * @param {Node} node - Node to start search from + * @param {String} name - loader call to remove + * @returns {Node | Void} - path to the targeted loader call expression if found + */ + +function findLoaderFunction(j, node, name) { + let loaderPath; + node.find(j.Identifier, { name }) + .filter(path => safeTraverse(path, ["parent", "value"])) + .filter(path => isType(path.parent.value, "MemberExpression")) + .forEach(path => { + loaderPath = safeTraverse(path, ["parent", "parent"]); + + + // loaderPath = safeTraverse(path, ["parent", "parent", "parent", "value"]); + // loaderPath.elements && (elements = [...loaderPath.elements]); + // j(path.parent.parent).remove(); + }); + return loaderPath; +} + +/** + * Creates a new conditional loader array node based on NODE_ENV + * + * @param {any} j — jscodeshift API + * @param {Array} prodLoaders - Nodes for production loaders + * @param {Array} devLoaders - Nods for development loaders + * @returns {Node} - New loader node + */ + +function addConditionalLoader(j, prodLoaders, devLoaders) { + return j.conditionalExpression( + j.binaryExpression( + "===", + j.memberExpression( + j.memberExpression(j.identifier("process"), j.identifier("env"), false), + j.identifier("NODE_ENV"), + false + ), + j.literal("production") + ), + j.arrayExpression(prodLoaders), + j.arrayExpression(devLoaders) + ); +} + module.exports = { safeTraverse, createProperty, @@ -760,5 +962,14 @@ module.exports = { pushCreateProperty, pushObjectKeys, isAssignment, - loopThroughObjects + loopThroughObjects, + addRequire, + createPluginByName, + getPluginOptionValue, + getLoaderOptionValue, + removePluginByName, + removeRequire, + pathToArray, + findLoaderFunction, + addConditionalLoader }; diff --git a/package.json b/package.json index 9eba422c4a1..45cbc3a6950 100644 --- a/package.json +++ b/package.json @@ -69,7 +69,7 @@ }, { "path": "./lib/utils/**.js", - "maxSize": "5.32 kB" + "maxSize": "6.25 kB" } ], "config": {