init-generator.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421
  1. "use strict";
  2. const Generator = require("yeoman-generator");
  3. const chalk = require("chalk");
  4. const logSymbols = require("log-symbols");
  5. const Input = require("webpack-addons").Input;
  6. const Confirm = require("webpack-addons").Confirm;
  7. const List = require("webpack-addons").List;
  8. const getPackageManager = require("../utils/package-manager").getPackageManager;
  9. const entryQuestions = require("./utils/entry");
  10. const getBabelPlugin = require("./utils/module");
  11. const getDefaultPlugins = require("./utils/plugins");
  12. const tooltip = require("./utils/tooltip");
  13. /**
  14. *
  15. * Generator for initializing a webpack config
  16. *
  17. * @class InitGenerator
  18. * @extends Generator
  19. * @returns {Void} After execution, transforms are triggered
  20. *
  21. */
  22. module.exports = class InitGenerator extends Generator {
  23. constructor(args, opts) {
  24. super(args, opts);
  25. this.isProd = false;
  26. this.dependencies = ["webpack", "webpack-cli", "uglifyjs-webpack-plugin"];
  27. this.configuration = {
  28. config: {
  29. webpackOptions: {},
  30. topScope: []
  31. }
  32. };
  33. }
  34. prompting() {
  35. const done = this.async();
  36. const self = this;
  37. let regExpForStyles;
  38. let ExtractUseProps;
  39. let outputPath = "dist";
  40. process.stdout.write(
  41. "\n" +
  42. logSymbols.info +
  43. chalk.blue(" INFO ") +
  44. "For more information and a detailed description of each question, have a look at " +
  45. chalk.bold.green(
  46. "https://github.com/webpack/webpack-cli/blob/master/INIT.md"
  47. ) +
  48. "\n"
  49. );
  50. process.stdout.write(
  51. logSymbols.info +
  52. chalk.blue(" INFO ") +
  53. "Alternatively, run `webpack(-cli) --help` for usage info." +
  54. "\n\n"
  55. );
  56. this.configuration.config.webpackOptions.module = {
  57. rules: []
  58. };
  59. this.configuration.config.webpackOptions.plugins = getDefaultPlugins();
  60. this.configuration.config.topScope.push(
  61. "const webpack = require('webpack')",
  62. "const path = require('path')",
  63. tooltip.uglify(),
  64. "const UglifyJSPlugin = require('uglifyjs-webpack-plugin');",
  65. "\n"
  66. );
  67. this.prompt([
  68. Confirm("entryType", "Will your application have multiple bundles?")
  69. ])
  70. .then(entryTypeAnswer => {
  71. // Ask different questions for entry points
  72. return entryQuestions(self, entryTypeAnswer);
  73. })
  74. .then(entryOptions => {
  75. if (entryOptions !== "\"\"") {
  76. this.configuration.config.webpackOptions.entry = entryOptions;
  77. }
  78. return this.prompt([
  79. Input(
  80. "outputType",
  81. "Which folder will your generated bundles be in? [default: dist]:"
  82. )
  83. ]);
  84. })
  85. .then(outputTypeAnswer => {
  86. // As entry is not required anymore and we dont set it to be an empty string or """""
  87. // it can be undefined so falsy check is enough (vs entry.length);
  88. if (!this.configuration.config.webpackOptions.entry) {
  89. this.configuration.config.webpackOptions.output = {
  90. filename: "'[name].[chunkhash].js'",
  91. chunkFilename: "'[name].[chunkhash].js'"
  92. };
  93. } else {
  94. this.configuration.config.webpackOptions.output = {
  95. filename: "'[name].[chunkhash].js'"
  96. };
  97. }
  98. if (outputTypeAnswer["outputType"].length) {
  99. outputPath = outputTypeAnswer["outputType"];
  100. }
  101. this.configuration.config.webpackOptions.output.path = `path.resolve(__dirname, '${outputPath}')`;
  102. })
  103. .then(() => {
  104. return this.prompt([
  105. Confirm("prodConfirm", "Are you going to use this in production?")
  106. ]);
  107. })
  108. .then(prodConfirmAnswer => {
  109. this.isProd = prodConfirmAnswer["prodConfirm"];
  110. this.configuration.config.webpackOptions.mode = this.isProd
  111. ? "'production'"
  112. : "'development'";
  113. })
  114. .then(() => {
  115. return this.prompt([
  116. Confirm("babelConfirm", "Will you be using ES2015?")
  117. ]);
  118. })
  119. .then(babelConfirmAnswer => {
  120. if (babelConfirmAnswer["babelConfirm"] === true) {
  121. this.configuration.config.webpackOptions.module.rules.push(
  122. getBabelPlugin()
  123. );
  124. this.dependencies.push(
  125. "babel-core",
  126. "babel-loader",
  127. "babel-preset-env"
  128. );
  129. }
  130. })
  131. .then(() => {
  132. return this.prompt([
  133. List("stylingType", "Will you use one of the below CSS solutions?", [
  134. "SASS",
  135. "LESS",
  136. "CSS",
  137. "PostCSS",
  138. "No"
  139. ])
  140. ]);
  141. })
  142. .then(stylingTypeAnswer => {
  143. ExtractUseProps = [];
  144. switch (stylingTypeAnswer["stylingType"]) {
  145. case "SASS":
  146. this.dependencies.push(
  147. "sass-loader",
  148. "node-sass",
  149. "style-loader",
  150. "css-loader"
  151. );
  152. regExpForStyles = `${new RegExp(/\.(scss|css)$/)}`;
  153. if (this.isProd) {
  154. ExtractUseProps.push(
  155. {
  156. loader: "'css-loader'",
  157. options: {
  158. sourceMap: true
  159. }
  160. },
  161. {
  162. loader: "'sass-loader'",
  163. options: {
  164. sourceMap: true
  165. }
  166. }
  167. );
  168. } else {
  169. ExtractUseProps.push(
  170. {
  171. loader: "'style-loader'"
  172. },
  173. {
  174. loader: "'css-loader'"
  175. },
  176. {
  177. loader: "'sass-loader'"
  178. }
  179. );
  180. }
  181. break;
  182. case "LESS":
  183. regExpForStyles = `${new RegExp(/\.(less|css)$/)}`;
  184. this.dependencies.push(
  185. "less-loader",
  186. "less",
  187. "style-loader",
  188. "css-loader"
  189. );
  190. if (this.isProd) {
  191. ExtractUseProps.push(
  192. {
  193. loader: "'css-loader'",
  194. options: {
  195. sourceMap: true
  196. }
  197. },
  198. {
  199. loader: "'less-loader'",
  200. options: {
  201. sourceMap: true
  202. }
  203. }
  204. );
  205. } else {
  206. ExtractUseProps.push(
  207. {
  208. loader: "'css-loader'",
  209. options: {
  210. sourceMap: true
  211. }
  212. },
  213. {
  214. loader: "'less-loader'",
  215. options: {
  216. sourceMap: true
  217. }
  218. }
  219. );
  220. }
  221. break;
  222. case "PostCSS":
  223. this.configuration.config.topScope.push(
  224. tooltip.postcss(),
  225. "const autoprefixer = require('autoprefixer');",
  226. "const precss = require('precss');",
  227. "\n"
  228. );
  229. this.dependencies.push(
  230. "style-loader",
  231. "css-loader",
  232. "postcss-loader",
  233. "precss",
  234. "autoprefixer"
  235. );
  236. regExpForStyles = `${new RegExp(/\.css$/)}`;
  237. if (this.isProd) {
  238. ExtractUseProps.push(
  239. {
  240. loader: "'css-loader'",
  241. options: {
  242. sourceMap: true,
  243. importLoaders: 1
  244. }
  245. },
  246. {
  247. loader: "'postcss-loader'",
  248. options: {
  249. plugins: `function () {
  250. return [
  251. precss,
  252. autoprefixer
  253. ];
  254. }`
  255. }
  256. }
  257. );
  258. } else {
  259. ExtractUseProps.push(
  260. {
  261. loader: "'style-loader'"
  262. },
  263. {
  264. loader: "'css-loader'",
  265. options: {
  266. sourceMap: true,
  267. importLoaders: 1
  268. }
  269. },
  270. {
  271. loader: "'postcss-loader'",
  272. options: {
  273. plugins: `function () {
  274. return [
  275. precss,
  276. autoprefixer
  277. ];
  278. }`
  279. }
  280. }
  281. );
  282. }
  283. break;
  284. case "CSS":
  285. this.dependencies.push("style-loader", "css-loader");
  286. regExpForStyles = `${new RegExp(/\.css$/)}`;
  287. if (this.isProd) {
  288. ExtractUseProps.push({
  289. loader: "'css-loader'",
  290. options: {
  291. sourceMap: true
  292. }
  293. });
  294. } else {
  295. ExtractUseProps.push(
  296. {
  297. loader: "'style-loader'",
  298. options: {
  299. sourceMap: true
  300. }
  301. },
  302. {
  303. loader: "'css-loader'"
  304. }
  305. );
  306. }
  307. break;
  308. default:
  309. regExpForStyles = null;
  310. }
  311. })
  312. .then(() => {
  313. if (this.isProd) {
  314. // Ask if the user wants to use extractPlugin
  315. return this.prompt([
  316. Input(
  317. "extractPlugin",
  318. "If you want to bundle your CSS files, what will you name the bundle? (press enter to skip)"
  319. )
  320. ]);
  321. }
  322. })
  323. .then(extractPluginAnswer => {
  324. if (regExpForStyles) {
  325. if (this.isProd) {
  326. const cssBundleName = extractPluginAnswer["extractPlugin"];
  327. this.configuration.config.topScope.push(tooltip.cssPlugin());
  328. this.dependencies.push("mini-css-extract-plugin");
  329. if (cssBundleName.length !== 0) {
  330. this.configuration.config.webpackOptions.plugins.push(
  331. // TODO: use [contenthash] after it is supported
  332. `new MiniCssExtractPlugin({ filename:'${cssBundleName}.[chunkhash].css' })`
  333. );
  334. } else {
  335. this.configuration.config.webpackOptions.plugins.push(
  336. "new MiniCssExtractPlugin({ filename:'style.css' })"
  337. );
  338. }
  339. ExtractUseProps.unshift({
  340. loader: "MiniCssExtractPlugin.loader"
  341. });
  342. const moduleRulesObj = {
  343. test: regExpForStyles,
  344. use: ExtractUseProps
  345. };
  346. this.configuration.config.webpackOptions.module.rules.push(
  347. moduleRulesObj
  348. );
  349. this.configuration.config.topScope.push(
  350. "const MiniCssExtractPlugin = require('mini-css-extract-plugin');",
  351. "\n"
  352. );
  353. } else {
  354. const moduleRulesObj = {
  355. test: regExpForStyles,
  356. use: ExtractUseProps
  357. };
  358. this.configuration.config.webpackOptions.module.rules.push(
  359. moduleRulesObj
  360. );
  361. }
  362. }
  363. // add splitChunks options for transparency
  364. // defaults coming from: https://webpack.js.org/plugins/split-chunks-plugin/#optimization-splitchunks
  365. this.configuration.config.topScope.push(tooltip.splitChunks());
  366. this.configuration.config.webpackOptions.optimization = {
  367. splitChunks: {
  368. chunks: "'async'",
  369. minSize: 30000,
  370. minChunks: 1,
  371. // for production name is recommended to be off
  372. name: !this.isProd,
  373. cacheGroups: {
  374. vendors: {
  375. test: "/[\\\\/]node_modules[\\\\/]/",
  376. priority: -10
  377. }
  378. }
  379. }
  380. };
  381. done();
  382. });
  383. }
  384. installPlugins() {
  385. const asyncNamePrompt = this.async();
  386. const defaultName = this.isProd ? "prod" : "config";
  387. this.prompt([
  388. Input(
  389. "nameType",
  390. `Name your 'webpack.[name].js?' [default: '${defaultName}']:`
  391. )
  392. ])
  393. .then(nameTypeAnswer => {
  394. this.configuration.config.configName = nameTypeAnswer["nameType"].length
  395. ? nameTypeAnswer["nameType"]
  396. : defaultName;
  397. })
  398. .then(() => {
  399. asyncNamePrompt();
  400. const packager = getPackageManager();
  401. const opts = packager === "yarn" ? { dev: true } : { "save-dev": true };
  402. this.runInstall(packager, this.dependencies, opts);
  403. });
  404. }
  405. writing() {
  406. this.config.set("configuration", this.configuration);
  407. }
  408. };