diff --git a/bin/cli.js b/bin/cli.js index ddc147d3f03..d0ca03f8e3f 100755 --- a/bin/cli.js +++ b/bin/cli.js @@ -22,8 +22,8 @@ "init", "migrate", "add", - /* "remove", + /* "update", "make", */ diff --git a/package-lock.json b/package-lock.json index 9aef6ddd2d9..2e804041f53 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18530,9 +18530,9 @@ "dev": true }, "webpack": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-4.13.0.tgz", - "integrity": "sha512-3KMX0uPjJ4cXjl9V/AY+goRQPs7jtKRQn3hhNOG6s8Sx3mmGCQUjQJvjVoGNABVo5svgujIcSLBN8g62EzqIMA==", + "version": "4.14.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-4.14.0.tgz", + "integrity": "sha512-CgZPUwobJbQlZqpylDNtEazZLfNnGuyFmpk1dHIP2kFchtyMWB+W2wBKPImSnSQ2rbX/WZMKiQax+SZmlUXuQQ==", "dev": true, "requires": { "@webassemblyjs/ast": "1.5.12", diff --git a/packages/generators/remove-generator.js b/packages/generators/remove-generator.js index f249af53cfb..0a2727e5b8c 100644 --- a/packages/generators/remove-generator.js +++ b/packages/generators/remove-generator.js @@ -1,3 +1,122 @@ const Generator = require("yeoman-generator"); +const path = require("path"); +const fs = require("fs"); +const { List } = require("@webpack-cli/webpack-scaffold"); -module.exports = class RemoveGenerator extends Generator {}; +const PROP_TYPES = require("@webpack-cli/utils/prop-types"); + +/** + * + * Generator for removing properties + * @class RemoveGenerator + * @extends Generator + * @returns {Void} After execution, transforms are triggered + * + */ + +module.exports = class RemoveGenerator extends Generator { + constructor(args, opts) { + super(args, opts); + this.configuration = { + config: { + webpackOptions: {}, + } + }; + + let configPath = path.resolve(process.cwd(), "webpack.config.js"); + const webpackConfigExists = fs.existsSync(configPath); + if (!webpackConfigExists) { + configPath = null; + // end the generator stating webpack config not found or to specify the config + } + this.webpackOptions = require(configPath); + } + + getPropTypes() { + return Object.keys(this.webpackOptions); + } + + getModuleLoaders() { + if (this.webpackOptions.module && this.webpackOptions.module.rules) { + return this.webpackOptions.module.rules.map(rule => rule ? rule.loader : null); + } + } + + prompting() { + const done = this.async(); + let propValue; + + return this.prompt([ + List( + "propType", + "Which property do you want to remove?", + Array.from(this.getPropTypes()) + ) + ]) + .then(({ propType }) => { + if (!PROP_TYPES.has(propType)) { + console.log("Invalid webpack config prop"); + return; + } + + propValue = this.webpackOptions[propType]; + if (typeof propValue === "object") { + if (Array.isArray(propValue)) { + return this.prompt([ + List( + "keyType", + `Which key do you want to remove from ${propType}?`, + Array.from(propValue) + ) + ]).then(({ keyType }) => { + this.configuration.config.webpackOptions[propType] = [ keyType ]; + }); + } else { + return this.prompt([ + List( + "keyType", + `Which key do you want to remove from ${propType}?`, + Array.from(Object.keys(propValue)) + ) + ]) + .then(({ keyType }) => { + if (propType === "module" && keyType === "rules") { + return this.prompt([ + List( + "rule", + "Which loader do you want to remove?", + Array.from(this.getModuleLoaders()) + ) + ]) + .then(({ rule }) => { + const loaderIndex = this.getModuleLoaders().indexOf(rule); + const loader = this.webpackOptions.module.rules[loaderIndex]; + this.configuration.config.webpackOptions.module = { + rules: [ loader ] + }; + }); + } else { + // remove the complete prop object if there is only one key + if (Object.keys(this.webpackOptions[propType]).length <= 1) { + this.configuration.config.webpackOptions[propType] = null; + } else { + this.configuration.config.webpackOptions[propType] = { + [keyType]: null + }; + } + } + }); + } + } else { + this.configuration.config.webpackOptions[propType] = null; + } + }) + .then(() => { + done(); + }); + } + + writing() { + this.config.set("configuration", this.configuration); + } +}; diff --git a/packages/info/index.ts b/packages/info/index.ts index d764ba9ffdf..e21a74e7e56 100644 --- a/packages/info/index.ts +++ b/packages/info/index.ts @@ -1,11 +1,12 @@ import * as envinfo from "envinfo"; +import * as process from "process"; /** * Prints debugging information for webpack issue reporting */ export default async function info() { - console.log( + process.stdout.write( await envinfo.run({ Binaries: ["Node", "Yarn", "npm"], Browsers: ["Chrome", "Firefox", "Safari"], diff --git a/packages/utils/__snapshots__/recursive-parser.test.js.snap b/packages/utils/__snapshots__/recursive-parser.test.js.snap index 545dc19ed1f..882e9e5f327 100644 --- a/packages/utils/__snapshots__/recursive-parser.test.js.snap +++ b/packages/utils/__snapshots__/recursive-parser.test.js.snap @@ -69,3 +69,312 @@ exports[`init transforms correctly using "fixture-1" data 1`] = ` } };" `; + +exports[`remove transforms correctly using "fixture-3" data 1`] = ` +"module.exports = { + entry: { + a: \\"a.js\\", + b: \\"taddda\\", + }, + mode: \\"prod\\", + devServer: { + port: 9000, + }, + devtool: \\"eval\\", + plugins: [ + \\"plugin1\\", + \\"plugin2\\", + \\"plugin3\\", + ], + resolve: { + aliasFields: [\\"'browser'\\", \\"wars\\"], + descriptionFiles: [\\"'a'\\", \\"b\\", \\"'c'\\"], + enforceExtension: false, + enforceModuleExtension: false, + extensions: [\\"hey\\", \\"ho\\"], + mainFields: [\\"main\\", \\"'story'\\"], + mainFiles: [\\"'noMainFileHere'\\", \\"iGuess\\"], + modules: [\\"one\\", \\"'two'\\"], + unsafeCache: false, + + resolveLoader: { + modules: [\\"'node_modules'\\", \\"mode_nodules\\"], + extensions: [\\"jsVal\\", \\"'.json'\\"], + mainFields: [\\"loader\\", \\"'main'\\"], + moduleExtensions: [\\"'-loader'\\", \\"value\\"] + }, + + plugins: [\\"somePlugin\\", \\"'stringVal'\\"], + symlinks: true + }, + module: { + noParse: function(content) { + return /jquery|lodash/.test(content); + }, + rules: [ + { + loader: \\"eslint-loader\\", + options: { + formatter: \\"someOption\\" + } + }, + { + loader: \\"vue-loader\\", + options: \\"vueObject\\" + }, + { + loader: \\"babel-loader\\", + include: \\"asdf\\" + } + ] + } +}; +" +`; + +exports[`remove transforms correctly using "fixture-3" data 2`] = ` +"module.exports = { + entry: { + a: \\"a.js\\", + b: \\"taddda\\", + }, + mode: \\"prod\\", + devServer: { + port: 9000, + }, + devtool: \\"eval\\", + plugins: [\\"plugin1\\", \\"plugin3\\"], + resolve: { + alias: { + inject: \\"{{#if_eq build 'standalone'}}\\", + hello: \\"'world'\\", + inject_1: \\"{{/if_eq}}\\", + world: \\"hello\\" + }, + aliasFields: [\\"'browser'\\", \\"wars\\"], + descriptionFiles: [\\"'a'\\", \\"b\\", \\"'c'\\"], + enforceExtension: false, + enforceModuleExtension: false, + extensions: [\\"hey\\", \\"ho\\"], + mainFields: [\\"main\\", \\"'story'\\"], + mainFiles: [\\"'noMainFileHere'\\", \\"iGuess\\"], + modules: [\\"one\\", \\"'two'\\"], + unsafeCache: false, + resolveLoader: { + modules: [\\"'node_modules'\\", \\"mode_nodules\\"], + extensions: [\\"jsVal\\", \\"'.json'\\"], + mainFields: [\\"loader\\", \\"'main'\\"], + moduleExtensions: [\\"'-loader'\\", \\"value\\"] + }, + plugins: [\\"somePlugin\\", \\"'stringVal'\\"], + symlinks: true + }, + module: { + noParse: function(content) { + return /jquery|lodash/.test(content); + }, + rules: [ + { + loader: \\"eslint-loader\\", + options: { + formatter: \\"someOption\\" + } + }, + { + loader: \\"vue-loader\\", + options: \\"vueObject\\" + }, + { + loader: \\"babel-loader\\", + include: \\"asdf\\" + } + ] + } +}; +" +`; + +exports[`remove transforms correctly using "fixture-3" data 3`] = ` +"module.exports = { + entry: { + a: \\"a.js\\", + b: \\"taddda\\", + }, + mode: \\"prod\\", + devServer: { + port: 9000, + }, + devtool: \\"eval\\", + plugins: [ + \\"plugin1\\", + \\"plugin2\\", + \\"plugin3\\", + ], + resolve: { + alias: { + inject: \\"{{#if_eq build 'standalone'}}\\", + hello: \\"'world'\\", + inject_1: \\"{{/if_eq}}\\", + world: \\"hello\\" + }, + aliasFields: [\\"'browser'\\", \\"wars\\"], + descriptionFiles: [\\"'a'\\", \\"b\\", \\"'c'\\"], + enforceExtension: false, + enforceModuleExtension: false, + extensions: [\\"hey\\", \\"ho\\"], + mainFields: [\\"main\\", \\"'story'\\"], + mainFiles: [\\"'noMainFileHere'\\", \\"iGuess\\"], + modules: [\\"one\\", \\"'two'\\"], + unsafeCache: false, + resolveLoader: { + modules: [\\"'node_modules'\\", \\"mode_nodules\\"], + extensions: [\\"jsVal\\", \\"'.json'\\"], + mainFields: [\\"loader\\", \\"'main'\\"], + moduleExtensions: [\\"'-loader'\\", \\"value\\"] + }, + plugins: [\\"somePlugin\\", \\"'stringVal'\\"], + symlinks: true + }, + module: { + rules: [ + { + loader: \\"eslint-loader\\", + options: { + formatter: \\"someOption\\" + } + }, + { + loader: \\"vue-loader\\", + options: \\"vueObject\\" + }, + { + loader: \\"babel-loader\\", + include: \\"asdf\\" + } + ] + } +}; +" +`; + +exports[`remove transforms correctly using "fixture-3" data 4`] = ` +"module.exports = { + entry: { + b: \\"taddda\\" + }, + mode: \\"prod\\", + devServer: { + port: 9000, + }, + devtool: \\"eval\\", + plugins: [ + \\"plugin1\\", + \\"plugin2\\", + \\"plugin3\\", + ], + resolve: { + alias: { + inject: \\"{{#if_eq build 'standalone'}}\\", + hello: \\"'world'\\", + inject_1: \\"{{/if_eq}}\\", + world: \\"hello\\" + }, + aliasFields: [\\"'browser'\\", \\"wars\\"], + descriptionFiles: [\\"'a'\\", \\"b\\", \\"'c'\\"], + enforceExtension: false, + enforceModuleExtension: false, + extensions: [\\"hey\\", \\"ho\\"], + mainFields: [\\"main\\", \\"'story'\\"], + mainFiles: [\\"'noMainFileHere'\\", \\"iGuess\\"], + modules: [\\"one\\", \\"'two'\\"], + unsafeCache: false, + resolveLoader: { + modules: [\\"'node_modules'\\", \\"mode_nodules\\"], + extensions: [\\"jsVal\\", \\"'.json'\\"], + mainFields: [\\"loader\\", \\"'main'\\"], + moduleExtensions: [\\"'-loader'\\", \\"value\\"] + }, + plugins: [\\"somePlugin\\", \\"'stringVal'\\"], + symlinks: true + }, + module: { + noParse: function(content) { + return /jquery|lodash/.test(content); + }, + rules: [ + { + loader: \\"eslint-loader\\", + options: { + formatter: \\"someOption\\" + } + }, + { + loader: \\"vue-loader\\", + options: \\"vueObject\\" + }, + { + loader: \\"babel-loader\\", + include: \\"asdf\\" + } + ] + } +}; +" +`; + +exports[`remove transforms correctly using "fixture-3" data 5`] = ` +"module.exports = { + entry: { + a: \\"a.js\\", + b: \\"taddda\\", + }, + mode: \\"prod\\", + devServer: { + port: 9000, + }, + devtool: \\"eval\\", + plugins: [ + \\"plugin1\\", + \\"plugin2\\", + \\"plugin3\\", + ], + resolve: { + alias: { + inject: \\"{{#if_eq build 'standalone'}}\\", + hello: \\"'world'\\", + inject_1: \\"{{/if_eq}}\\", + world: \\"hello\\" + }, + aliasFields: [\\"'browser'\\", \\"wars\\"], + descriptionFiles: [\\"'a'\\", \\"b\\", \\"'c'\\"], + enforceExtension: false, + enforceModuleExtension: false, + extensions: [\\"hey\\", \\"ho\\"], + mainFields: [\\"main\\", \\"'story'\\"], + mainFiles: [\\"'noMainFileHere'\\", \\"iGuess\\"], + modules: [\\"one\\", \\"'two'\\"], + unsafeCache: false, + resolveLoader: { + modules: [\\"'node_modules'\\", \\"mode_nodules\\"], + extensions: [\\"jsVal\\", \\"'.json'\\"], + mainFields: [\\"loader\\", \\"'main'\\"], + moduleExtensions: [\\"'-loader'\\", \\"value\\"] + }, + plugins: [\\"somePlugin\\", \\"'stringVal'\\"], + symlinks: true + }, + module: { + noParse: function(content) { + return /jquery|lodash/.test(content); + }, + rules: [{ + loader: \\"vue-loader\\", + options: \\"vueObject\\" + }, { + loader: \\"babel-loader\\", + include: \\"asdf\\" + }] + } +}; +" +`; diff --git a/packages/utils/__testfixtures__/fixture-3.input.js b/packages/utils/__testfixtures__/fixture-3.input.js new file mode 100644 index 00000000000..05830ad1166 --- /dev/null +++ b/packages/utils/__testfixtures__/fixture-3.input.js @@ -0,0 +1,62 @@ +module.exports = { + entry: { + a: "a.js", + b: "taddda", + }, + mode: "prod", + devServer: { + port: 9000, + }, + devtool: "eval", + plugins: [ + "plugin1", + "plugin2", + "plugin3", + ], + resolve: { + alias: { + inject: "{{#if_eq build 'standalone'}}", + hello: "'world'", + inject_1: "{{/if_eq}}", + world: "hello" + }, + aliasFields: ["'browser'", "wars"], + descriptionFiles: ["'a'", "b", "'c'"], + enforceExtension: false, + enforceModuleExtension: false, + extensions: ["hey", "ho"], + mainFields: ["main", "'story'"], + mainFiles: ["'noMainFileHere'", "iGuess"], + modules: ["one", "'two'"], + unsafeCache: false, + resolveLoader: { + modules: ["'node_modules'", "mode_nodules"], + extensions: ["jsVal", "'.json'"], + mainFields: ["loader", "'main'"], + moduleExtensions: ["'-loader'", "value"] + }, + plugins: ["somePlugin", "'stringVal'"], + symlinks: true + }, + module: { + noParse: function(content) { + return /jquery|lodash/.test(content); + }, + rules: [ + { + loader: "eslint-loader", + options: { + formatter: "someOption" + } + }, + { + loader: "vue-loader", + options: "vueObject" + }, + { + loader: "babel-loader", + include: "asdf" + } + ] + } +}; diff --git a/packages/utils/ast-utils.js b/packages/utils/ast-utils.js index b7d5a24a5b6..16af8aa9bbc 100644 --- a/packages/utils/ast-utils.js +++ b/packages/utils/ast-utils.js @@ -537,6 +537,67 @@ function parseMerge(j, ast, value, action) { } } +/** + * + * Removes an object/property from the config + * @param {any} j — jscodeshift API + * @param {Node} ast - AST node + * @param {String} key - key of a key/val object + * @param {Any} value - Any type of object + * @returns {Node} - the created ast + */ + +function removeProperty(j, ast, key, value) { + + // override for module.rules / loaders + if (key === "module" && value.rules) { + return ast + .find(j.Property, { + value: { + type: "Literal", + value: value.rules[0].loader + } + }) + .forEach(p =>{ + j(p.parent).remove(); + }); + } + + // value => array + if (Array.isArray(value)) { + return ast + .find(j.Literal, { + value: value[0] + }) + .forEach(p =>{ + const configKey = safeTraverse(p, ["parent", "parent", "node", "key", "name"]); + if (configKey === key) { + j(p).remove(); + } + }); + } + + // value => literal string / boolean / nested object + let objKeyToRemove = null; + if (value === null) { + objKeyToRemove = key; + } else if (typeof value === "object") { + for (const innerKey in value) { + if (value[innerKey] === null) objKeyToRemove = innerKey; + } + } + return ast + .find(j.Property, { + key: { + type: "Identifier", + name: objKeyToRemove + }, + }) + .forEach(p => { + j(p).remove(); + }); +} + module.exports = { safeTraverse, safeTraverseAndGetType, @@ -555,5 +616,6 @@ module.exports = { getRequire, addProperty, parseTopScope, - parseMerge + parseMerge, + removeProperty }; diff --git a/packages/utils/modify-config-helper.js b/packages/utils/modify-config-helper.js index 6c47514ea63..f1749321729 100644 --- a/packages/utils/modify-config-helper.js +++ b/packages/utils/modify-config-helper.js @@ -8,6 +8,8 @@ const Generator = require("yeoman-generator"); const logSymbols = require("log-symbols"); const runTransform = require("./scaffold"); +const DEFAULT_WEBPACK_CONFIG_FILENAME = "webpack.config.js"; + /** * * Looks up the webpack.config in the user's path and runs a given @@ -20,7 +22,7 @@ const runTransform = require("./scaffold"); * @returns {Function} runTransform - Returns a transformation instance */ -module.exports = function modifyHelperUtil(action, generator, configFile, packages) { +module.exports = function modifyHelperUtil(action, generator, configFile = DEFAULT_WEBPACK_CONFIG_FILENAME, packages) { let configPath = null; if (action !== "init") { diff --git a/packages/utils/recursive-parser.js b/packages/utils/recursive-parser.js index 2c78bd76a7e..e52a34cdf28 100644 --- a/packages/utils/recursive-parser.js +++ b/packages/utils/recursive-parser.js @@ -34,13 +34,15 @@ module.exports = function recursiveTransform(j, ast, key, value, action) { .filter(p => p.value.properties); if (node.size() !== 0) { - // select node with existing key - return utils.findRootNodesByName(j, root, key).forEach(p => { - if (action === "add") { - // update property/replace - j(p).replaceWith(utils.addProperty(j, p, key, value, action)); - } - }); + if (action === "add") { + return utils.findRootNodesByName(j, root, key) + .forEach(p => { + // update property/replace + j(p).replaceWith(utils.addProperty(j, p, key, value, action)); + }); + } else if (action === "remove") { + return utils.removeProperty(j, root, key, value); + } } else { return root.forEach(p => { if (value) { diff --git a/packages/utils/recursive-parser.test.js b/packages/utils/recursive-parser.test.js index 5d13b4d7af7..9b263d9a871 100644 --- a/packages/utils/recursive-parser.test.js +++ b/packages/utils/recursive-parser.test.js @@ -2,41 +2,108 @@ const defineTest = require("./defineTest"); -defineTest(__dirname, "init", "fixture-1", "entry", { - objects: "are", - super: [ - "yeah", - { - test: new RegExp(/\.(js|vue)$/), - loader: "'eslint-loader'", - enforce: "'pre'", - include: ["customObj", "'Stringy'"], - options: { - formatter: "'someOption'" + +defineTest( + __dirname, + "init", + "fixture-1", + "entry", + { + objects: "are", + super: [ + "yeah", + { + test: new RegExp(/\.(js|vue)$/), + loader: "'eslint-loader'", + enforce: "'pre'", + include: ["customObj", "'Stringy'"], + options: { + formatter: "'someOption'" + } } - } - ], - nice: "':)'", - foo: "Promise.resolve()", - man: "() => duper" -}); - -defineTest(__dirname, "add", "fixture-2", "entry", { - objects: "are not", - super: [ - "op", - { - test: new RegExp(/\.(wasm|c)$/), - loader: "'pia-loader'", - enforce: "'pre'", - include: ["asd", "'Stringy'"], - options: { - formatter: "'nao'" + ], + nice: "':)'", + foo: "Promise.resolve()", + man: "() => duper" + } +); + +defineTest( + __dirname, + "add", + "fixture-2", + "entry", + { + objects: "are not", + super: [ + "op", + { + test: new RegExp(/\.(wasm|c)$/), + loader: "'pia-loader'", + enforce: "'pre'", + include: ["asd", "'Stringy'"], + options: { + formatter: "'nao'" + } } - } - ], - nice: "'=)'", - foo: "Promise.resolve()", - man: "() => nice!!", - mode: "super-man" -}); + ], + nice: "'=)'", + foo: "Promise.resolve()", + man: "() => nice!!", + mode: "super-man" + } +); + +defineTest( + __dirname, + "remove", + "fixture-3", + "resolve", + { + alias: null + } +); + +defineTest( + __dirname, + "remove", + "fixture-3", + "plugins", + [ + "plugin2" + ] +); + +defineTest( + __dirname, + "remove", + "fixture-3", + "module", + { + noParse: null + } +); + +defineTest( + __dirname, + "remove", + "fixture-3", + "entry", + { + a: null, + } +); + +defineTest( + __dirname, + "remove", + "fixture-3", + "module", + { + rules: [ + { + loader: "eslint-loader", + }, + ] + } +);