diff --git a/INIT.md b/INIT.md index 112ca86eb77..8dd9a3a7099 100644 --- a/INIT.md +++ b/INIT.md @@ -1,6 +1,6 @@ # webpack-cli init -`webpack-cli init` is used to initialize `webpack` projects quickly by scaffolding configuration and installing modules required for the project as per user preferences. +`webpack-cli init` is used to initialize `webpack` projects quickly by scaffolding configuration and creating a runnable project with all the dependencies based on the user preferences. ## Initial Setup diff --git a/SECURITY.md b/SECURITY.md index c54aedcf910..92c3d610560 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -8,8 +8,8 @@ webpack CLI is currently supporting webpack v4 and webpack v5. Security fixes ar | webpack version | webpack-cli version | Supported | | --------------- | ----------------------------- | ------------------ | -| >= 4.20.x | ^3.1.2 | :white_check_mark: | -| <= 4.19.x | ^3.1.1 | :white_check_mark: | +| >= 4.20.x | ^3.1.2 | :white_check_mark: | +| <= 4.19.x | ^3.1.1 | :white_check_mark: | | 5.x.0 | ^3.1.2 | :white_check_mark: | | 5.0.x | ^3.1.2 | :white_check_mark: | | < 4.x.x | (CLI included in webpack < 4) | :x: | diff --git a/package-lock.json b/package-lock.json index 7b47fe3b74d..4e17dad8491 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13179,8 +13179,7 @@ "prettier": { "version": "1.17.0", "resolved": "https://registry.npmjs.org/prettier/-/prettier-1.17.0.tgz", - "integrity": "sha512-sXe5lSt2WQlCbydGETgfm1YBShgOX4HxQkFPvbxkcwgDvGDeqVau8h+12+lmSVlP3rHPz0oavfddSZg/q+Szjw==", - "dev": true + "integrity": "sha512-sXe5lSt2WQlCbydGETgfm1YBShgOX4HxQkFPvbxkcwgDvGDeqVau8h+12+lmSVlP3rHPz0oavfddSZg/q+Szjw==" }, "prettier-eslint": { "version": "8.8.2", diff --git a/packages/generators/.gitignore b/packages/generators/.gitignore index 74dcaf3ce86..b3cdf36cae7 100644 --- a/packages/generators/.gitignore +++ b/packages/generators/.gitignore @@ -2,3 +2,4 @@ **/*.js !*.test.js !/**/*.test.js +!/templates/*.js diff --git a/packages/generators/add-generator.ts b/packages/generators/add-generator.ts index 705afb0da0c..ced8cd62311 100644 --- a/packages/generators/add-generator.ts +++ b/packages/generators/add-generator.ts @@ -125,7 +125,7 @@ export default class AddGenerator extends Generator { .then( (entryTypeAnswer: { entryType: boolean }): Promise => { // Ask different questions for entry points - return entryQuestions(self, entryTypeAnswer); + return entryQuestions(self, entryTypeAnswer.entryType); } ) .then( diff --git a/packages/generators/init-generator.ts b/packages/generators/init-generator.ts index 715b44e8797..9d5b33f0b51 100644 --- a/packages/generators/init-generator.ts +++ b/packages/generators/init-generator.ts @@ -1,16 +1,18 @@ import chalk from "chalk"; import * as logSymbols from "log-symbols"; import * as Generator from "yeoman-generator"; -import * as Inquirer from "inquirer"; +import * as path from "path"; import { getPackageManager } from "@webpack-cli/utils/package-manager"; import { Confirm, Input, List } from "@webpack-cli/webpack-scaffold"; +import { getDefaultOptimization } from "./utils/webpackConfig"; import { WebpackOptions } from "./types"; import entryQuestions from "./utils/entry"; -import getBabelPlugin from "./utils/module"; -import getDefaultPlugins from "./utils/plugins"; +import langQuestionHandler, { LangType } from "./utils/languageSupport"; +import styleQuestionHandler, { Loader, StylingType } from "./utils/styleSupport"; import tooltip from "./utils/tooltip"; +import { generatePluginName } from "./utils/plugins"; /** * @@ -32,390 +34,261 @@ export default class InitGenerator extends Generator { webpackOptions?: WebpackOptions; }; }; + private langType: string; public constructor(args, opts) { super(args, opts); - this.isProd = false; - (this.usingDefaults = false), - (this.dependencies = [ - "webpack", - "webpack-cli", - "terser-webpack-plugin", - "babel-plugin-syntax-dynamic-import" - ]); + this.usingDefaults = false, + this.isProd = this.usingDefaults ? true : false; + + this.dependencies = [ + "webpack", + "webpack-cli", + "babel-plugin-syntax-dynamic-import", + ]; + if (this.isProd) { + this.dependencies.push("terser-webpack-plugin"); + } else { + this.dependencies.push("webpack-dev-server"); + } + this.configuration = { config: { + configName: this.isProd ? "prod" : "config", topScope: [], - webpackOptions: {} - } + webpackOptions: { + mode: this.isProd ? "'production'" : "'development'", + entry: undefined, + output: undefined, + plugins: [], + module: { + rules: [], + }, + }, + }, }; + + // add splitChunks options for transparency + // defaults coming from: https://webpack.js.org/plugins/split-chunks-plugin/#optimization-splitchunks + this.configuration.config.topScope.push( + "const path = require('path');", + "const webpack = require('webpack');", + "\n", + tooltip.splitChunks(), + ); + + if (this.isProd) { + this.configuration.config.topScope.push( + tooltip.terser(), + "const TerserPlugin = require('terser-webpack-plugin');", + "\n", + ); + } + + (this.configuration.config.webpackOptions.plugins as string[]).push( + "new webpack.ProgressPlugin()", + ); + + let optimizationConfig = getDefaultOptimization(this.isProd); + this.configuration.config.webpackOptions.optimization = optimizationConfig; + + if (!this.isProd) { + this.configuration.config.webpackOptions.devServer = { + open: true, + }; + } } - // eslint-disable-next-line - public prompting(): any { + public async prompting() { const done: () => {} = this.async(); const self: this = this; let regExpForStyles: string; - let ExtractUseProps: object[]; + let ExtractUseProps: Loader[]; process.stdout.write( - "\n" + - logSymbols.info + - chalk.blue(" INFO ") + - "For more information and a detailed description of each question, have a look at " + - chalk.bold.green("https://github.com/webpack/webpack-cli/blob/master/INIT.md") + - "\n" + `\n${logSymbols.info}${chalk.blue(" INFO ")} ` + + `For more information and a detailed description of each question, have a look at: ` + + `${chalk.bold.green("https://github.com/webpack/webpack-cli/blob/master/INIT.md")}\n`, ); process.stdout.write( - logSymbols.info + - chalk.blue(" INFO ") + - "Alternatively, run `webpack(-cli) --help` for usage info." + - "\n\n" - ); - - this.configuration.config.webpackOptions.module = { - rules: [] - }; - this.configuration.config.topScope.push( - "const webpack = require('webpack')", - "const path = require('path')", - "\n" + `${logSymbols.info}${chalk.blue(" INFO ")} ` + + `Alternatively, run "webpack(-cli) --help" for usage info\n\n`, ); - return this.prompt([Confirm("entryType", "Will your application have multiple bundles?", false)]) - .then( - (entryTypeAnswer: { entryType: boolean }): Promise => { - // Ask different questions for entry points - return entryQuestions(self, entryTypeAnswer); - } - ) - .then( - (entryOptions: object | string): Promise<{}> => { - if (typeof entryOptions === "string" && entryOptions.length > 0) { - if (entryOptions !== "\"\"" && entryOptions !== "\'\'") { - this.configuration.config.webpackOptions.entry = entryOptions; - } - } + const { multiEntries } = await this.prompt([ + Confirm( + "multiEntries", + "Will your application have multiple bundles?", + false + ), + ]); - return this.prompt([ - Input("outputType", "In which folder do you want to store your generated bundles? (dist):") - ]); - } - ) - .then( - (outputTypeAnswer: { outputType: string }): void => { - // As entry is not required anymore and we dont set it to be an empty string or """"" - // it can be undefined so falsy check is enough (vs entry.length); - if (!this.configuration.config.webpackOptions.entry && !this.usingDefaults) { - this.configuration.config.webpackOptions.output = { - chunkFilename: "'[name].[chunkhash].js'", - filename: "'[name].[chunkhash].js'" - }; - } else if (!this.usingDefaults) { - this.configuration.config.webpackOptions.output = { - filename: "'[name].[chunkhash].js'" - }; - } - if (!this.usingDefaults && outputTypeAnswer.outputType.length) { - this.configuration.config.webpackOptions.output.path = `path.resolve(__dirname, '${ - outputTypeAnswer.outputType - }')`; - } - } - ) - .then( - (): Promise => { - this.isProd = this.usingDefaults ? true : false; - this.configuration.config.configName = this.isProd ? "prod" : "config"; - if (!this.isProd) { - this.configuration.config.webpackOptions.mode = "'development'"; - } - this.configuration.config.webpackOptions.plugins = this.isProd ? [] : getDefaultPlugins(); - return this.prompt([Confirm("babelConfirm", "Will you be using ES2015?")]); - } - ) - .then( - (babelConfirmAnswer: { babelConfirm: boolean }): void => { - if (babelConfirmAnswer.babelConfirm) { - this.configuration.config.webpackOptions.module.rules.push(getBabelPlugin()); - this.dependencies.push("babel-loader", "@babel/core", "@babel/preset-env"); - } - } - ) - .then( - (): Promise => { - return this.prompt([ - List("stylingType", "Will you use one of the below CSS solutions?", [ - "No", - "CSS", - "SASS", - "LESS", - "PostCSS" - ]) - ]); - } - ) - .then( - (stylingTypeAnswer: { stylingType: string }): void => { - ExtractUseProps = []; - switch (stylingTypeAnswer.stylingType) { - case "SASS": - this.dependencies.push("sass-loader", "node-sass", "style-loader", "css-loader"); - regExpForStyles = `${new RegExp(/\.(scss|css)$/)}`; - if (this.isProd) { - ExtractUseProps.push( - { - loader: "'css-loader'", - options: { - sourceMap: true - } - }, - { - loader: "'sass-loader'", - options: { - sourceMap: true - } - } - ); - } else { - ExtractUseProps.push( - { - loader: "'style-loader'" - }, - { - loader: "'css-loader'" - }, - { - loader: "'sass-loader'" - } - ); - } - break; - case "LESS": - regExpForStyles = `${new RegExp(/\.(less|css)$/)}`; - this.dependencies.push("less-loader", "less", "style-loader", "css-loader"); - if (this.isProd) { - ExtractUseProps.push( - { - loader: "'css-loader'", - options: { - sourceMap: true - } - }, - { - loader: "'less-loader'", - options: { - sourceMap: true - } - } - ); - } else { - ExtractUseProps.push( - { - loader: "'css-loader'", - options: { - sourceMap: true - } - }, - { - loader: "'less-loader'", - options: { - sourceMap: true - } - } - ); - } - break; - case "PostCSS": - this.configuration.config.topScope.push( - tooltip.postcss(), - "const autoprefixer = require('autoprefixer');", - "const precss = require('precss');", - "\n" - ); - this.dependencies.push( - "style-loader", - "css-loader", - "postcss-loader", - "precss", - "autoprefixer" - ); - regExpForStyles = `${new RegExp(/\.css$/)}`; - if (this.isProd) { - ExtractUseProps.push( - { - loader: "'css-loader'", - options: { - importLoaders: 1, - sourceMap: true - } - }, - { - loader: "'postcss-loader'", - options: { - plugins: `function () { - return [ - precss, - autoprefixer - ]; - }` - } - } - ); - } else { - ExtractUseProps.push( - { - loader: "'style-loader'" - }, - { - loader: "'css-loader'", - options: { - importLoaders: 1, - sourceMap: true - } - }, - { - loader: "'postcss-loader'", - options: { - plugins: `function () { - return [ - precss, - autoprefixer - ]; - }` - } - } - ); - } - break; - case "CSS": - this.dependencies.push("style-loader", "css-loader"); - regExpForStyles = `${new RegExp(/\.css$/)}`; - if (this.isProd) { - ExtractUseProps.push({ - loader: "'css-loader'", - options: { - sourceMap: true - } - }); - } else { - ExtractUseProps.push( - { - loader: "'style-loader'", - options: { - sourceMap: true - } - }, - { - loader: "'css-loader'" - } - ); - } - break; - default: - regExpForStyles = null; - } - } - ) - .then( - (): Promise => { - if (this.isProd) { - // Ask if the user wants to use extractPlugin - return this.prompt([ - Input( - "extractPlugin", - "If you want to bundle your CSS files, what will you name the bundle? (press enter to skip)" - ) - ]); - } - } - ) - .then( - (extractPluginAnswer: { extractPlugin: string }): void => { - if (regExpForStyles) { - if (this.isProd) { - const cssBundleName: string = extractPluginAnswer.extractPlugin; - this.configuration.config.topScope.push(tooltip.cssPlugin()); - this.dependencies.push("mini-css-extract-plugin"); + // TODO string | object + const entryOption: void | {} = await entryQuestions(self, multiEntries); + + if (typeof entryOption === "string" && entryOption.length > 0) { + this.configuration.config.webpackOptions.entry = `${entryOption}`; + } else if (typeof entryOption === "object") { + this.configuration.config.webpackOptions.entry = entryOption; + } - if (cssBundleName.length !== 0) { - (this.configuration.config.webpackOptions.plugins as string[]).push( - // TODO: use [contenthash] after it is supported - `new MiniCssExtractPlugin({ filename:'${cssBundleName}.[chunkhash].css' })` - ); - } else { - (this.configuration.config.webpackOptions.plugins as string[]).push( - "new MiniCssExtractPlugin({ filename:'style.css' })" - ); - } + const { outputDir } = await this.prompt([ + Input( + "outputDir", + "In which folder do you want to store your generated bundles?", + "dist", + ), + ]); - ExtractUseProps.unshift({ - loader: "MiniCssExtractPlugin.loader" - }); + // As entry is not required anymore and we dont set it to be an empty string or """"" + // it can be undefined so falsy check is enough (vs entry.length); + if ( + !this.configuration.config.webpackOptions.entry && + !this.usingDefaults + ) { + this.configuration.config.webpackOptions.output = { + chunkFilename: "'[name].[chunkhash].js'", + filename: "'[name].[chunkhash].js'", + }; + } else if (!this.usingDefaults) { + this.configuration.config.webpackOptions.output = { + filename: "'[name].[chunkhash].js'", + }; + } + if (!this.usingDefaults && outputDir.length) { + this.configuration.config.webpackOptions.output.path = + `path.resolve(__dirname, '${outputDir}')`; + } + + const { langType } = await this.prompt([ + List("langType", "Will you use one of the below JS solutions?", [ + LangType.ES6, + LangType.Typescript, + "No", + ]), + ]); + + langQuestionHandler(this, langType); + this.langType = langType; - const moduleRulesObj = { - test: regExpForStyles, - use: ExtractUseProps - }; + const { stylingType } = await this.prompt([ + List("stylingType", "Will you use one of the below CSS solutions?", [ + "No", + StylingType.CSS, + StylingType.SASS, + StylingType.LESS, + StylingType.PostCSS, + ]), + ]); - this.configuration.config.webpackOptions.module.rules.push(moduleRulesObj); - this.configuration.config.topScope.push( - "const MiniCssExtractPlugin = require('mini-css-extract-plugin');", - "\n" - ); - } else { - const moduleRulesObj: { - test: string; - use: object[]; - } = { - test: regExpForStyles, - use: ExtractUseProps - }; + ({ ExtractUseProps, regExpForStyles } = styleQuestionHandler(self, stylingType)); - this.configuration.config.webpackOptions.module.rules.push(moduleRulesObj); - } + if (this.isProd) { + // Ask if the user wants to use extractPlugin + const { useExtractPlugin } = await this.prompt([ + Input( + "useExtractPlugin", + "If you want to bundle your CSS files, what will you name the bundle? (press enter to skip)", + ), + ]); + + if (regExpForStyles) { + if (this.isProd) { + const cssBundleName: string = useExtractPlugin; + this.dependencies.push("mini-css-extract-plugin"); + this.configuration.config.topScope.push( + tooltip.cssPlugin(), + "const MiniCssExtractPlugin = require('mini-css-extract-plugin');", + "\n", + ); + if (cssBundleName.length !== 0) { + (this.configuration.config.webpackOptions.plugins as string[]).push( + // TODO: use [contenthash] after it is supported + `new MiniCssExtractPlugin({ filename:'${cssBundleName}.[chunkhash].css' })`, + ); + } else { + (this.configuration.config.webpackOptions.plugins as string[]).push( + "new MiniCssExtractPlugin({ filename:'style.css' })", + ); } - // add splitChunks options for transparency - // defaults coming from: https://webpack.js.org/plugins/split-chunks-plugin/#optimization-splitchunks - this.configuration.config.topScope.push(tooltip.splitChunks()); - this.configuration.config.webpackOptions.optimization = { - splitChunks: { - cacheGroups: { - vendors: { - priority: -10, - test: "/[\\\\/]node_modules[\\\\/]/" - } - }, - chunks: "'async'", - minChunks: 1, - minSize: 30000, - // for production name is recommended to be off - name: !this.isProd - } - }; - done(); + + ExtractUseProps.unshift({ + loader: "MiniCssExtractPlugin.loader", + }); } + + this.configuration.config.webpackOptions.module.rules.push( + { + test: regExpForStyles, + use: ExtractUseProps, + }, + ); + } + } + if(!this.isProd) { + this.dependencies.push("html-webpack-plugin"); + const htmlWebpackDependency = "html-webpack-plugin"; + const htmlwebpackPlugin = generatePluginName(htmlWebpackDependency); + (this.configuration.config.topScope as string[]).push( + `const ${htmlwebpackPlugin} = require('${htmlWebpackDependency}')`, + "\n", + tooltip.html(), ); - } - public installPlugins(): void { - if (this.isProd) { - this.dependencies = this.dependencies.filter((p: string): boolean => p !== "terser-webpack-plugin"); - } else { - this.configuration.config.topScope.push( - tooltip.terser(), - "const TerserPlugin = require('terser-webpack-plugin');", - "\n" + (this.configuration.config.webpackOptions.plugins as string[]).push( + `new ${htmlwebpackPlugin}()`, ); } + done(); + } + + public installPlugins(): void { const packager = getPackageManager(); const opts: { - dev?: boolean; - "save-dev"?: boolean; - } = packager === "yarn" ? { dev: true } : { "save-dev": true }; + dev?: boolean, + "save-dev"?: boolean, + } = packager === "yarn" ? + { dev: true } : + { "save-dev": true }; + this.scheduleInstallTask(packager, this.dependencies, opts); } public writing(): void { this.config.set("configuration", this.configuration); + + const packageJsonTemplatePath = "./templates/package.json.js"; + this.fs.extendJSON(this.destinationPath("package.json"), require(packageJsonTemplatePath)(this.isProd)); + + const generateEntryFile = (entryPath: string, name: string): void => { + entryPath = entryPath.replace(/'/g, ""); + this.fs.copyTpl( + path.resolve(__dirname, "./templates/index.js"), + this.destinationPath(entryPath), + { name }, + ); + }; + + // Generate entry file/files + const entry = this.configuration.config.webpackOptions.entry; + if ( typeof entry === "string" ) { + generateEntryFile(entry, "your main file!"); + } else if (typeof entry === "object") { + Object.keys(entry).forEach((name: string): void => + generateEntryFile(entry[name], `${name} main file!`), + ); + } + + // Generate README + this.fs.copyTpl( + path.resolve(__dirname, "./templates/README.md"), + this.destinationPath("README.md"), + {} + ); + + // Genrate tsconfig + if (this.langType === LangType.Typescript) { + const tsConfigTemplatePath = "./templates/tsconfig.json.js"; + this.fs.extendJSON(this.destinationPath("tsconfig.json"), require(tsConfigTemplatePath)); + } } } diff --git a/packages/generators/templates/README.md b/packages/generators/templates/README.md new file mode 100644 index 00000000000..92053bba6ec --- /dev/null +++ b/packages/generators/templates/README.md @@ -0,0 +1,15 @@ +# 🚀 Welcome to your new awesome project! + +This project has been created using **webpack scaffold**, you can now run + +``` +npm run build +``` + +or + +``` +yarn build +``` + +to bundle your application diff --git a/packages/generators/templates/index.js b/packages/generators/templates/index.js new file mode 100644 index 00000000000..0eec583fb7e --- /dev/null +++ b/packages/generators/templates/index.js @@ -0,0 +1 @@ +console.log("Hello World from <%= name %>"); diff --git a/packages/generators/templates/package.json.js b/packages/generators/templates/package.json.js new file mode 100644 index 00000000000..54089edb516 --- /dev/null +++ b/packages/generators/templates/package.json.js @@ -0,0 +1,15 @@ +module.exports = (isProd) => { + + let scripts = { + build: "webpack" + }; + if (!isProd) { + scripts.start = "webpack-dev-server"; + } + + return { + version: "1.0.0", + description: "My webpack project", + scripts, + }; +}; diff --git a/packages/generators/templates/tsconfig.json.js b/packages/generators/templates/tsconfig.json.js new file mode 100644 index 00000000000..0eae50cb36f --- /dev/null +++ b/packages/generators/templates/tsconfig.json.js @@ -0,0 +1,9 @@ +module.exports = { + compilerOptions: { + allowSyntheticDefaultImports: true, + noImplicitAny: true, + module: "es6", + target: "es5", + allowJs: true + } +}; diff --git a/packages/generators/types/index.ts b/packages/generators/types/index.ts index 449dc0cc477..32ed902ab23 100644 --- a/packages/generators/types/index.ts +++ b/packages/generators/types/index.ts @@ -214,7 +214,7 @@ export interface WebpackOptions { }; } -interface Rule { +export interface Rule { enforce?: "pre" | "post"; exclude?: IRuleSetCondition; include?: IRuleSetCondition; diff --git a/packages/generators/utils/entry.ts b/packages/generators/utils/entry.ts index 47762928337..2d29c7dda6b 100644 --- a/packages/generators/utils/entry.ts +++ b/packages/generators/utils/entry.ts @@ -1,5 +1,5 @@ import * as Generator from "yeoman-generator"; -import { InputValidate } from "@webpack-cli/webpack-scaffold"; +import { Input, InputValidate } from "@webpack-cli/webpack-scaffold"; import validate from "./validate"; @@ -18,19 +18,18 @@ interface CustomGenerator extends Generator { export default function entry( self: CustomGenerator, - answer: { - entryType: boolean; - } + multiEntries: boolean, ): Promise { let entryIdentifiers: string[]; let result: Promise; - if (answer.entryType) { + if (multiEntries) { result = self .prompt([ InputValidate( "multipleEntries", - "Type the names you want for your modules (entry files), separated by comma [example: app,vendor]", - validate + "What do you want to name your bundles? (separated by comma)", + validate, + "pageOne, pageTwo" ) ]) .then( @@ -57,7 +56,7 @@ export default function entry( !n[val].includes("path") && !n[val].includes("process") ) { - n[val] = `\'${n[val].replace(/"|'/g, "").concat(".js")}\'`; + n[val] = `\'./${n[val].replace(/"|'/g, "").concat(".js")}\'`; } webpackEntryPoint[val] = n[val]; } @@ -70,44 +69,52 @@ export default function entry( ); }, Promise.resolve()); } + return forEachPromise( entryIdentifiers, (entryProp: string): Promise => self.prompt([ InputValidate( `${entryProp}`, - `What is the location of "${entryProp}"? [example: ./src/${entryProp}]`, - validate - ) - ]) - ).then( - (entryPropAnswer: object): object => { - Object.keys(entryPropAnswer).forEach( - (val: string): void => { - if ( - entryPropAnswer[val].charAt(0) !== "(" && - entryPropAnswer[val].charAt(0) !== "[" && - !entryPropAnswer[val].includes("function") && - !entryPropAnswer[val].includes("path") && - !entryPropAnswer[val].includes("process") - ) { - entryPropAnswer[val] = `\'${entryPropAnswer[val].replace(/"|'/g, "")}\'`; + `What is the location of "${entryProp}"?`, + validate, + `src/${entryProp}`, + ), + ])) + .then( + (entryPropAnswer: object): object => { + Object.keys(entryPropAnswer).forEach( + (val: string): void => { + if ( + entryPropAnswer[val].charAt(0) !== "(" && + entryPropAnswer[val].charAt(0) !== "[" && + !entryPropAnswer[val].includes("function") && + !entryPropAnswer[val].includes("path") && + !entryPropAnswer[val].includes("process") + ) { + entryPropAnswer[val] = `\'./${entryPropAnswer[val].replace(/"|'/g, "").concat(".js")}\'`; + } + webpackEntryPoint[val] = entryPropAnswer[val]; } - webpackEntryPoint[val] = entryPropAnswer[val]; - } - ); - return webpackEntryPoint; - } - ); + ); + return webpackEntryPoint; + } + ); } ); } else { result = self - .prompt([InputValidate("singularEntry", "Which will be your application entry point? (src/index)")]) + .prompt([ + Input( + "singularEntry", + "Which will be your application entry point?", + "src/index", + ) + ]) .then( (singularEntryAnswer: { singularEntry: string }): string => { let { singularEntry } = singularEntryAnswer; - singularEntry = `\'${singularEntry.replace(/"|'/g, "")}\'`; + singularEntry = `\'./${singularEntry.replace(/"|'/g, "").concat(".js")}\'`; if (singularEntry.length <= 0) { self.usingDefaults = true; } diff --git a/packages/generators/utils/languageSupport.ts b/packages/generators/utils/languageSupport.ts new file mode 100644 index 00000000000..7d3c1f901cf --- /dev/null +++ b/packages/generators/utils/languageSupport.ts @@ -0,0 +1,117 @@ +import { Rule } from "../types"; + +export enum LangType { + ES6 = "ES6", + Typescript = "Typescript", +} + +const replaceExt = (path: string, ext: string): string => + path.substr(0, path.lastIndexOf(".")) + `${ext}'`; + +function updateEntryExt(self, newExt: string): void { + const jsEntryOption = self.configuration.config.webpackOptions.entry; + let tsEntryOption = {}; + if (typeof jsEntryOption === "string") { + tsEntryOption = replaceExt(jsEntryOption, newExt); + } else if (typeof jsEntryOption === "object") { + Object.keys(jsEntryOption).forEach((entry: string): void => { + tsEntryOption[entry] = replaceExt(jsEntryOption[entry], newExt); + }); + } + self.configuration.config.webpackOptions.entry = tsEntryOption; +} + +const getFolder = (path: string): string => + path.replace("'./", "").split("/").slice(0, -1).join("/"); + +function getEntryFolders(self): string[] { + const entryOption = self.configuration.config.webpackOptions.entry; + let entryFolders = {}; + if (typeof entryOption === "string") { + const folder = getFolder(entryOption); + if (folder.length > 0) entryFolders[folder] = true; + } else if (typeof entryOption === "object") { + Object.keys(entryOption).forEach((entry: string): void => { + const folder = getFolder(entryOption[entry]); + if (folder.length > 0) entryFolders[folder] = true; + }); + } + return Object.keys(entryFolders); +} + +/** + * + * Returns an module.rule object for the babel loader + * @param {string[]} includeFolders An array of folders to include + * @returns {Rule} A configuration containing the babel-loader with env preset + */ +export function getBabelLoader(includeFolders: string[]): Rule { + const include = includeFolders.map((folder: string): string => + `path.resolve(__dirname, '${folder}')` + ); + return { + test: "/\.(js|jsx)$/", + include, + loader: "'babel-loader'", + options: { + plugins: ["'syntax-dynamic-import'"], + presets: [ + [ + "'@babel/preset-env'", + { + "'modules'": false + } + ] + ] + }, + }; +} + +/** + * + * Returns an module.rule object for the typescript loader + * @param {string[]} includeFolders An array of folders to include + * @returns {Rule} A configuration containing the ts-loader + */ +export function getTypescriptLoader(includeFolders: string[]): Rule { + const include = includeFolders.map((folder: string): string => + `path.resolve(__dirname, '${folder}')` + ); + return { + test: "/\.(ts|tsx)?$/", + loader: "'ts-loader'", + include, + exclude: ["/node_modules/"], + }; +} + +export default function language(self, langType: string): void { + const entryFolders = getEntryFolders(self); + switch (langType) { + case LangType.ES6: + self.dependencies.push( + "babel-loader", + "@babel/core", + "@babel/preset-env", + ); + self.configuration.config.webpackOptions.module.rules.push( + getBabelLoader(entryFolders), + ); + break; + + case LangType.Typescript: + self.dependencies.push( + "typescript", + "ts-loader", + ); + self.configuration.config.webpackOptions.module.rules.push( + getTypescriptLoader(entryFolders), + ); + self.configuration.config.webpackOptions.resolve = { + extensions: [ "'.tsx'", "'.ts'", "'.js'" ], + }; + + updateEntryExt(self, ".ts"); + break; + } +} diff --git a/packages/generators/utils/module.ts b/packages/generators/utils/module.ts deleted file mode 100644 index 25b846723f1..00000000000 --- a/packages/generators/utils/module.ts +++ /dev/null @@ -1,36 +0,0 @@ -interface Module extends Object { - include: string[]; - loader: string; - options: { - plugins: string[]; - presets: Preset[][]; - }; - test: string; -} - -type Preset = string | object; - -/** - * - * Returns an module.rule object that has the babel loader if invoked - * - * @returns {Function} A callable function that adds the babel-loader with env preset - */ -export default function(): Module { - return { - include: ["path.resolve(__dirname, 'src')"], - loader: "'babel-loader'", - options: { - plugins: ["'syntax-dynamic-import'"], - presets: [ - [ - "'@babel/preset-env'", - { - "'modules'": false - } - ] - ] - }, - test: `${new RegExp(/\.js$/)}` - }; -} diff --git a/packages/generators/utils/styleSupport.ts b/packages/generators/utils/styleSupport.ts new file mode 100644 index 00000000000..d05a5241a75 --- /dev/null +++ b/packages/generators/utils/styleSupport.ts @@ -0,0 +1,183 @@ +import tooltip from "./tooltip"; + +export enum StylingType { + CSS = "CSS", + SASS = "SASS", + LESS = "LESS", + PostCSS = "PostCSS", +} + +export enum LoaderName { + CSS = "css-loader", + SASS = "sass-loader", + STYLE = "style-loader", + LESS = "less-loader", + POSTCSS = "postcss-loader", +} + +export enum StyleRegex { + CSS = "/\.css$/", + SASS = "/\.(scss|css)$/", + LESS = "/\.(less|css)$/", + PostCSS = "/\.css$/", +} + +export interface Loader { + loader: string; + options?: { + importLoaders?: number; + sourceMap?: boolean; + plugins?: string; + }; +} + +export default function style(self, stylingType: string): { + ExtractUseProps: Loader[], + regExpForStyles: StyleRegex, +} { + const ExtractUseProps: Loader[] = []; + let regExpForStyles: StyleRegex = null; + + switch (stylingType) { + case StylingType.CSS: + regExpForStyles = StyleRegex.CSS; + + self.dependencies.push( + LoaderName.CSS, + ); + if (!self.isProd) { + self.dependencies.push( + LoaderName.STYLE, + ); + ExtractUseProps.push( + { + loader: `"${LoaderName.STYLE}"`, + }, + ); + } + ExtractUseProps.push({ + loader: `"${LoaderName.CSS}"`, + options: { + sourceMap: true, + }, + }); + break; + + case StylingType.SASS: + regExpForStyles = StyleRegex.SASS; + + self.dependencies.push( + "node-sass", + LoaderName.SASS, + LoaderName.CSS, + ); + if (!self.isProd) { + self.dependencies.push( + LoaderName.STYLE, + ); + ExtractUseProps.push( + { + loader: `"${LoaderName.STYLE}"`, + }, + ); + } + ExtractUseProps.push( + { + loader: `"${LoaderName.CSS}"`, + options: { + sourceMap: true, + }, + }, + { + loader: `"${LoaderName.SASS}"`, + options: { + sourceMap: true, + }, + }, + ); + break; + + case StylingType.LESS: + regExpForStyles = StyleRegex.LESS; + + self.dependencies.push( + "less", + LoaderName.LESS, + LoaderName.CSS, + ); + if (!self.isProd) { + self.dependencies.push( + LoaderName.STYLE, + ); + ExtractUseProps.push( + { + loader: `"${LoaderName.STYLE}"`, + }, + ); + } + ExtractUseProps.push( + { + loader: `"${LoaderName.CSS}"`, + options: { + sourceMap: true, + }, + }, + { + loader: `"${LoaderName.LESS}"`, + options: { + sourceMap: true, + }, + }, + ); + break; + + case StylingType.PostCSS: + regExpForStyles = StyleRegex.PostCSS; + + self.configuration.config.topScope.push( + tooltip.postcss(), + "const autoprefixer = require('autoprefixer');", + "const precss = require('precss');", + "\n", + ); + + self.dependencies.push( + "precss", + "autoprefixer", + LoaderName.CSS, + LoaderName.POSTCSS, + ); + if (!self.isProd) { + self.dependencies.push( + LoaderName.STYLE, + ); + ExtractUseProps.push( + { + loader: `"${LoaderName.STYLE}"`, + }, + ); + } + ExtractUseProps.push( + { + loader: `"${LoaderName.CSS}"`, + options: { + importLoaders: 1, + sourceMap: true, + }, + }, + { + loader: `"${LoaderName.POSTCSS}"`, + options: { + plugins: `function () { + return [ + precss, + autoprefixer + ]; + }`, + }, + }, + ); + break; + } + return { ExtractUseProps, regExpForStyles }; +} diff --git a/packages/generators/utils/tooltip.ts b/packages/generators/utils/tooltip.ts index cd9dba4e7ec..c58c7fba922 100644 --- a/packages/generators/utils/tooltip.ts +++ b/packages/generators/utils/tooltip.ts @@ -57,6 +57,17 @@ export default { * * https://github.com/webpack-contrib/terser-webpack-plugin * + */`; + }, + + html: (): string => { + return `/* + * We've enabled HtmlWebpackPlugin for you! This generates a html + * page for you when you compile webpack, which will make you start + * developing and prototyping faster. + * + * https://github.com/jantimon/html-webpack-plugin + * */`; } }; diff --git a/packages/generators/utils/webpackConfig.ts b/packages/generators/utils/webpackConfig.ts new file mode 100644 index 00000000000..b67d3c6d0c5 --- /dev/null +++ b/packages/generators/utils/webpackConfig.ts @@ -0,0 +1,31 @@ +import { WebpackOptions } from '../types'; + +export function getDefaultOptimization(isProd: boolean): WebpackOptions["optimization"] { + let optimizationOptions; + if (isProd) { + optimizationOptions = { + minimizer: [ + "new TerserPlugin()", + ], + splitChunks: { + chunks: "'all'", + }, + }; + } else { + optimizationOptions = { + splitChunks: { + cacheGroups: { + vendors: { + priority: -10, + test: "/[\\\\/]node_modules[\\\\/]/", + }, + }, + chunks: "'async'", + minChunks: 1, + minSize: 30000, + name: !this.isProd, + }, + }; + } + return optimizationOptions; +} \ No newline at end of file diff --git a/packages/utils/package-lock.json b/packages/utils/package-lock.json index 30e688d29c6..efe1c450fdd 100644 --- a/packages/utils/package-lock.json +++ b/packages/utils/package-lock.json @@ -121,9 +121,9 @@ "dev": true }, "@types/prettier": { - "version": "1.15.2", - "resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-1.15.2.tgz", - "integrity": "sha512-XIB0ZCaFZmWUHAa9dBqP5UKXXHwuukmVlP+XcyU94dui2k+l2lG+CHAbt2ffenHPUqoIs5Beh8Pdf2YEq/CZ7A==", + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-1.16.1.tgz", + "integrity": "sha512-db6pZL5QY3JrlCHBhYQzYDci0xnoDuxfseUuguLRr3JNk+bnCfpkK6p8quiUDyO8A0vbpBKkk59Fw125etrNeA==", "dev": true }, "@types/through": { @@ -5665,9 +5665,9 @@ "integrity": "sha1-gV7R9uvGWSb4ZbMQwHE7yzMVzks=" }, "prettier": { - "version": "1.15.3", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-1.15.3.tgz", - "integrity": "sha512-gAU9AGAPMaKb3NNSUUuhhFAS7SCO4ALTN4nRIn6PJ075Qd28Yn2Ig2ahEJWdJwJmlEBTUfC7mMUSFy8MwsOCfg==" + "version": "1.17.0", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-1.17.0.tgz", + "integrity": "sha512-sXe5lSt2WQlCbydGETgfm1YBShgOX4HxQkFPvbxkcwgDvGDeqVau8h+12+lmSVlP3rHPz0oavfddSZg/q+Szjw==" }, "pretty-bytes": { "version": "5.1.0", diff --git a/packages/utils/package.json b/packages/utils/package.json index 3a243bbb1ad..f234ab8f8d1 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -19,7 +19,7 @@ "jscodeshift": "^0.5.1", "log-symbols": "^2.2.0", "p-each-series": "^1.0.0", - "prettier": "^1.15.2", + "prettier": "^1.17.0", "yeoman-environment": "^2.3.4", "yeoman-generator": "^3.1.1" }, @@ -31,7 +31,7 @@ "@types/log-symbols": "^2.0.0", "@types/node": "^10.12.9", "@types/p-each-series": "^1.0.0", - "@types/prettier": "^1.15.0", + "@types/prettier": "^1.16.1", "@types/yeoman-generator": "^3.1.2", "typescript": "^3.1.6" }, diff --git a/packages/utils/run-prettier.ts b/packages/utils/run-prettier.ts index 794f86923b8..bf4980150d9 100644 --- a/packages/utils/run-prettier.ts +++ b/packages/utils/run-prettier.ts @@ -19,18 +19,17 @@ export default function runPrettier(outputPath: string, source: string, cb?: Fun try { prettySource = prettier.format(source, { filepath: outputPath, - parser: "babylon", + parser: "babel", singleQuote: true, tabWidth: 1, useTabs: true, }); } catch (err) { process.stdout.write( - "\n" + - chalk.yellow( - `WARNING: Could not apply prettier to ${outputPath}` + - " due validation error, but the file has been created\n", - ), + `\n${chalk.yellow( + `WARNING: Could not apply prettier to ${outputPath}` + + " due validation error, but the file has been created\n", + )}`, ); prettySource = source; error = err; diff --git a/packages/utils/scaffold.ts b/packages/utils/scaffold.ts index 2d2ece8a390..7f0eb1acd48 100644 --- a/packages/utils/scaffold.ts +++ b/packages/utils/scaffold.ts @@ -70,37 +70,35 @@ export default function runTransform(transformConfig: TransformConfig, action: s return astTransform(j, ast, f, config[f] as any, transformAction); } return astTransform(j, ast, f, config.webpackOptions[f], transformAction); - } - ) - .then( - (): void | PromiseLike => { - let configurationName: string; - if (!config.configName) { - configurationName = "webpack.config.js"; - } else { - configurationName = "webpack." + config.configName + ".js"; - } - - const projectRoot = findProjectRoot(); - const outputPath: string = initActionNotDefined - ? transformConfig.configPath - : path.join(projectRoot || process.cwd(), configurationName); - const source: string = ast.toSource({ - quote: "single" - }); - runPrettier(outputPath, source); - } - ) - .catch( - (err: Error): void => { - console.error(err.message ? err.message : err); + }) + .then((): void | PromiseLike => { + let configurationName: string; + if (!config.configName) { + configurationName = "webpack.config.js"; + } else { + configurationName = "webpack." + config.configName + ".js"; } - ); - } - ); - let successMessage: string = `Congratulations! Your new webpack configuration file has been created!\n`; + const projectRoot = findProjectRoot(); + const outputPath: string = initActionNotDefined + ? transformConfig.configPath + : path.join(projectRoot || process.cwd(), configurationName); + const source: string = ast.toSource({ + quote: "single", + }); + runPrettier(outputPath, source); + }) + .catch((err: Error): void => { + console.error(err.message ? err.message : err); + }); + }); + let successMessage: string = + chalk.green(`Congratulations! Your new webpack configuration file has been created!\n\n`) + + `You can now run ${chalk.green("npm run start")} to run your project!\n\n`; + if (initActionNotDefined && transformConfig.config.item) { - successMessage = `Congratulations! ${transformConfig.config.item} has been ${action}ed!\n`; + successMessage = chalk.green(`Congratulations! ${ + transformConfig.config.item + } has been ${action}ed!\n`); } - process.stdout.write("\n" + chalk.green(successMessage)); + process.stdout.write(`\n${successMessage}`); } diff --git a/packages/webpack-scaffold/README.md b/packages/webpack-scaffold/README.md index a0a92441b40..413c223a51a 100755 --- a/packages/webpack-scaffold/README.md +++ b/packages/webpack-scaffold/README.md @@ -12,14 +12,23 @@ npm i -D webpack-cli @webpack-cli/webpack-scaffold # API -- [parseValue](#parsevalue) -- [createArrowFunction](#createarrowfunction) -- [createRegularFunction](#createregularfunction) -- [createDynamicPromise](#createdynamicpromise) -- [createAssetFilterFunction](#createassetfilterfunction) -- [createExternalFunction](#createexternalfunction) -- [createRequire](#createrequire) -- [Inquirer](#inquirer) - [List](#list) - [RawList](#rawlist) - [CheckList](#checklist) - [Input](#input) - [InputValidate](#inputvalidate) - [Confirm](#confirm) +- [webpack-scaffold](#webpack-scaffold) +- [Installation](#installation) +- [API](#api) + - [parseValue](#parsevalue) + - [createArrowFunction](#createarrowfunction) + - [createRegularFunction](#createregularfunction) + - [createDynamicPromise](#createdynamicpromise) + - [createAssetFilterFunction](#createassetfilterfunction) + - [createExternalFunction](#createexternalfunction) + - [createRequire](#createrequire) + - [Inquirer](#inquirer) + - [List](#list) + - [RawList](#rawlist) + - [CheckList](#checklist) + - [Input](#input) + - [InputValidate](#inputvalidate) + - [Confirm](#confirm) ## parseValue @@ -163,19 +172,19 @@ CheckList("entry", "what kind of entry do you want?", ["Array", "Function"]); ### Input -Param: `name, message` +Param: `name, message, [default]` Creates an Input from Inquirer ```js const Input = require("@webpack-cli/webpack-scaffold").Input; -Input("entry", "what is your entry point?"); +Input("entry", "what is your entry point?", "src/index"); ``` ### InputValidate -Param: `name, message, validate` +Param: `name, message, [validate, default]` Creates an Input from Inquirer @@ -186,15 +195,15 @@ const validation = value => { if (value.length > 4) { return true; } else { - return "Wow, that was short!"; + return "Your answer must be longer than 4 characters, try again"; } }; -InputValidate("entry", "what is your entry point?", validation); +InputValidate("entry", "what is your entry point?", validation, "src/index"); ``` ### Confirm -Param: `name, message, default` +Param: `name, message, [default]` Creates an Input from Inquirer diff --git a/packages/webpack-scaffold/__tests__/__snapshots__/index.test.ts.snap b/packages/webpack-scaffold/__tests__/__snapshots__/index.test.ts.snap index 0304203ef64..2bb1ed2117f 100755 --- a/packages/webpack-scaffold/__tests__/__snapshots__/index.test.ts.snap +++ b/packages/webpack-scaffold/__tests__/__snapshots__/index.test.ts.snap @@ -9,6 +9,16 @@ Object { } `; +exports[`utils Inquirer should make an Input object with validation and default value 1`] = ` +Object { + "default": "my-plugin", + "message": "what is your plugin?", + "name": "plugins", + "type": "input", + "validate": [Function], +} +`; + exports[`utils createArrowFunction should stringify an arrow function 1`] = `"() => 'app.js'"`; exports[`utils createAssetFilterFunction should stringify an assetFilterFunction 1`] = ` diff --git a/packages/webpack-scaffold/__tests__/index.test.ts b/packages/webpack-scaffold/__tests__/index.test.ts index 0db475e179a..2d0ccef8582 100755 --- a/packages/webpack-scaffold/__tests__/index.test.ts +++ b/packages/webpack-scaffold/__tests__/index.test.ts @@ -81,6 +81,14 @@ describe("utils", () => { type: "input", }); }); + it("should make an Input object", () => { + expect(utils.Input("plugins", "what is your plugin?", "my-plugin")).toEqual({ + default: "my-plugin", + message: "what is your plugin?", + name: "plugins", + type: "input", + }); + }); it("should make a Confirm object", () => { expect(utils.Confirm("context", "what is your context?")).toEqual({ default: true, @@ -102,5 +110,10 @@ describe("utils", () => { utils.InputValidate("plugins", "what is your plugin?", () => true), ).toMatchSnapshot(); }); + it("should make an Input object with validation and default value", () => { + expect( + utils.InputValidate("plugins", "what is your plugin?", () => true, "my-plugin"), + ).toMatchSnapshot(); + }); }); }); diff --git a/packages/webpack-scaffold/index.ts b/packages/webpack-scaffold/index.ts index 0a4ceb38d5c..6c609b8e522 100755 --- a/packages/webpack-scaffold/index.ts +++ b/packages/webpack-scaffold/index.ts @@ -76,21 +76,29 @@ export function CheckList(name: string, message: string, choices: string[]): Gen }; } -export function Input(name: string, message: string): Generator.Question { +export function Input(name: string, message: string, defaultChoice?: string): Generator.Question { return { + default: defaultChoice, message, name, type: "input" }; } -export function InputValidate(name: string, message: string, cb?: (input: string) => string | boolean): Generator.Question { - return { +export function InputValidate( + name: string, + message: string, + cb?: (input: string) => string | boolean, + defaultChoice?: string, + ): Generator.Question { + const input: Generator.Question = { message, name, type: "input", validate: cb }; + if (defaultChoice) input.default = defaultChoice; + return input; } export function Confirm(name: string, message: string, defaultChoice: boolean = true): Generator.Question {