diff --git a/.cspell.json b/.cspell.json index 156c2be7a33..02d12f393c6 100644 --- a/.cspell.json +++ b/.cspell.json @@ -64,6 +64,7 @@ "nwjs", "Oikawa", "pathinfo", + "plopfile", "pnpm", "postcss", "prebuild", @@ -101,7 +102,8 @@ "Yuuji", "Zangetsu", "Zenitsu", - "quickstart" + "quickstart", + "pinia" ], "dictionaries": ["npm", "software-terms"], "ignorePaths": [ diff --git a/.eslintrc.js b/.eslintrc.js index 4c008e454c6..1f312dc4c4f 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -57,5 +57,11 @@ module.exports = { "@typescript-eslint/no-require-imports": "off", }, }, + { + files: ["**/packages/create-webpack-app/**/*.js"], + parserOptions: { + sourceType: "module", + }, + }, ], }; diff --git a/packages/create-webpack-app/README.md b/packages/create-webpack-app/README.md new file mode 100644 index 00000000000..a23c5eee3f9 --- /dev/null +++ b/packages/create-webpack-app/README.md @@ -0,0 +1,48 @@ +
+ + + +
+ +# create-webpack-app CLI + +## About + +- `create-webpack-app` is a cli tool that enables developers to scaffold a new webpack project quickly. It provides developers with a flexible set of commands to increase speed when setting up a custom webpack project. webpack CLI addresses these needs by providing tools to improve the setup of custom webpack configuration. +- It also supports several front-end frameworks and libraries like React, Angular, Vue, Svelte, etc. +- Webpack Loader and Plugin scaffolding is also supported. + +## Supported arguments and commands + +### Usage + +```bash +npx create-webpack-app [command] [options] +``` + +### CLI options + +**To generate default template** + +```bash +npx create-webpack-app +``` + +**To generate with default answers** + +```bash +npx create-webpack-app -f, --force +``` + +**To generate in a specified path** + +```bash +npx create-webpack-app [generation-path] +``` + +**To generate a project according to a template** + +```bash +npx create-webpack-app --template + +``` diff --git a/packages/create-webpack-app/bin/cli.js b/packages/create-webpack-app/bin/cli.js new file mode 100755 index 00000000000..aaefc07b7d0 --- /dev/null +++ b/packages/create-webpack-app/bin/cli.js @@ -0,0 +1,4 @@ +#!/usr/bin/env node + +//eslint-disable-next-line +import * as cli from "../lib/index.js"; diff --git a/packages/create-webpack-app/package.json b/packages/create-webpack-app/package.json new file mode 100644 index 00000000000..11c658c11b0 --- /dev/null +++ b/packages/create-webpack-app/package.json @@ -0,0 +1,57 @@ +{ + "name": "create-webpack-app", + "version": "1.0.0", + "description": "CLI for scaffolding webpack projects using default config, framework templates, loader or plugins templates", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/webpack-cli/create-webpack-app.git" + }, + "homepage": "https://github.com/webpack/webpack-cli/tree/master/packages/create-webpack-app", + "bugs": "https://github.com/webpack/webpack-cli/issues", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "bin": { + "create-webpack-app": "./bin/cli.js" + }, + "type": "module", + "main": "./lib/index.js", + "scripts": { + "build": "tsc --build", + "watch": "tsc --watch" + }, + "engines": { + "node": ">=18.12.0" + }, + "keywords": [ + "webpack", + "cli", + "scaffolding", + "module", + "bundler", + "web", + "frameworks" + ], + "files": [ + "bin", + "lib", + "!**/*__tests__" + ], + "peerDependencies": { + "webpack-cli": "^5.x.x" + }, + "dependencies": { + "@inquirer/prompts": "^5.1.2", + "colorette": "^2.0.20", + "commander": "^12.1.0", + "cross-spawn": "^7.0.3", + "ejs": "^3.1.10", + "node-plop": "^0.32.0" + }, + "devDependencies": { + "@types/cross-spawn": "^6.0.6", + "@types/ejs": "^3.1.5" + } +} diff --git a/packages/create-webpack-app/src/generators/init/default.ts b/packages/create-webpack-app/src/generators/init/default.ts new file mode 100644 index 00000000000..b617059bd86 --- /dev/null +++ b/packages/create-webpack-app/src/generators/init/default.ts @@ -0,0 +1,205 @@ +import { Answers, ActionType, FileRecord } from "../../types"; +import { dirname, join, resolve } from "path"; +import { NodePlopAPI, DynamicActionsFunction } from "node-plop"; +import { fileURLToPath } from "url"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); + +export default async function (plop: NodePlopAPI) { + // dependencies to be installed + const devDependencies: Array = ["webpack", "webpack-cli"]; + + await plop.load("../../utils/pkgInstallAction.js", {}, true); + await plop.load("../../utils/fileGenerator.js", {}, true); + + plop.setDefaultInclude({ generators: true, actionTypes: true }); + plop.setPlopfilePath(resolve(__dirname, "../../plopfile.js")); + // Define a custom action for installing packages + + // Define a base generator for the project structure + plop.setGenerator("init-default", { + description: "Create a basic webpack project", + prompts: [ + { + type: "list", + name: "langType", + message: "Which of the following JS solutions do you want to use?", + choices: ["none", "ES6", "Typescript"], + default: "none", + }, + { + type: "confirm", + name: "devServer", + message: "Would you like to use Webpack Dev server?", + default: true, + }, + { + type: "confirm", + name: "htmlWebpackPlugin", + message: "Do you want to simplify the creation of HTML files for your bundle?", + default: true, + }, + { + type: "confirm", + name: "workboxWebpackPlugin", + message: "Do you want to add PWA support?", + default: true, + }, + { + type: "list", + name: "cssType", + message: "Which of the following CSS solution do you want to use?", + choices: ["none", "CSS only", "SASS", "LESS", "Stylus"], + default: "none", + filter: (input, answers) => { + if (input === "none") { + answers.isCSS = false; + answers.isPostCSS = false; + answers.extractPlugin = "No"; + } else if (input === "CSS only") { + answers.isCSS = true; + } + return input; + }, + }, + { + type: "confirm", + name: "isCSS", + message: (answers) => + `Will you be using CSS styles along with ${answers.cssType} in your project?`, + when: (answers) => answers.cssType !== "CSS only", + default: true, + }, + { + type: "confirm", + name: "isPostCSS", + message: "Do you want to use PostCSS in your project?", + default: (answers: Answers) => answers.cssType == "CSS only", + }, + { + type: "list", + name: "extractPlugin", + message: "Do you want to extract CSS into separate files?", + choices: ["No", "Only for Production", "Yes"], + default: "No", + }, + { + type: "list", + name: "packageManager", + message: "Which package manager do you want to use?", + choices: ["npm", "yarn", "pnpm"], + default: "npm", + validate(input) { + if (!input.trim()) { + return "Package manager cannot be empty"; + } + return true; + }, + }, + ], + actions: function (answers: Answers) { + const actions: ActionType[] = []; + + switch (answers.langType) { + case "ES6": + devDependencies.push("babel-loader", "@babel/core", "@babel/preset-env"); + break; + case "Typescript": + devDependencies.push("typescript", "ts-loader"); + break; + } + + if (answers.devServer) { + devDependencies.push("webpack-dev-server"); + } + + if (answers.htmlWebpackPlugin) { + devDependencies.push("html-webpack-plugin", "html-loader"); + } + + if (answers.workboxWebpackPlugin) { + devDependencies.push("workbox-webpack-plugin"); + } + + if (answers.isPostCSS) { + devDependencies.push("postcss-loader", "postcss", "autoprefixer"); + } + + if (answers.extractPlugin !== "No") { + devDependencies.push("mini-css-extract-plugin"); + } + + if (answers.cssType !== "none") { + devDependencies.push("style-loader", "css-loader"); + switch (answers.cssType) { + case "SASS": + devDependencies.push("sass-loader", "sass"); + break; + case "LESS": + devDependencies.push("less-loader", "less"); + break; + case "Stylus": + devDependencies.push("stylus-loader", "stylus"); + break; + } + } + if (answers.extractPlugin !== "No") { + devDependencies.push("mini-css-extract-plugin"); + } + + const files: Array = [ + { filePath: "./index.html", fileType: "text" }, + { filePath: "webpack.config.js", fileType: "text" }, + { filePath: "package.json", fileType: "text" }, + { filePath: "README.md", fileType: "text" }, + ]; + + switch (answers.langType) { + case "Typescript": + answers.entryPoint = "./src/index.ts"; + files.push( + { filePath: "tsconfig.json", fileType: "text" }, + { filePath: answers.entryPoint as string, fileType: "text" }, + ); + break; + case "ES6": + answers.entryPoint = "./src/index.js"; + files.push( + { filePath: "babel.config.json", fileType: "text" }, + { filePath: answers.entryPoint as string, fileType: "text" }, + ); + break; + default: + answers.entryPoint = "./src/index.js"; + files.push({ filePath: answers.entryPoint as string, fileType: "text" }); + break; + } + + if (answers.isPostCSS) { + files.push({ filePath: "postcss.config.js", fileType: "text" }); + } + + for (const file of files) { + actions.push({ + type: "fileGenerator", + path: join(answers.projectPath, file.filePath), + templateFile: join( + plop.getPlopfilePath(), + "../templates/init/default", + `${file.filePath}.tpl`, + ), + fileType: file.fileType, + data: answers, + }); + } + + actions.push({ + type: "pkgInstall", + path: answers.projectPath, + packages: devDependencies, + }); + + return actions; + } as DynamicActionsFunction, + }); +} diff --git a/packages/create-webpack-app/src/generators/init/react.ts b/packages/create-webpack-app/src/generators/init/react.ts new file mode 100644 index 00000000000..32b3a0a540e --- /dev/null +++ b/packages/create-webpack-app/src/generators/init/react.ts @@ -0,0 +1,224 @@ +import { Answers, ActionType, FileRecord } from "../../types"; +import { dirname, resolve, join } from "path"; +import { NodePlopAPI, DynamicActionsFunction } from "node-plop"; +import { fileURLToPath } from "url"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); + +export default async function (plop: NodePlopAPI) { + // dependencies to be installed + const devDependencies: Array = [ + "webpack", + "webpack-cli", + "react@18", + "react-dom@18", + "webpack-dev-server", + "html-webpack-plugin", + "react-router-dom", + "@types/react-router-dom", + ]; + + await plop.load("../../utils/pkgInstallAction.js", {}, true); + await plop.load("../../utils/fileGenerator.js", {}, true); + + plop.setDefaultInclude({ generators: true, actionTypes: true }); + plop.setPlopfilePath(resolve(__dirname, "../../plopfile.js")); + // Define a custom action for installing packages + + // Define a base generator for the project structure + plop.setGenerator("init-react", { + description: "Create a basic React-webpack project", + prompts: [ + { + type: "list", + name: "langType", + message: "Which of the following JS solutions do you want to use?", + choices: ["ES6", "Typescript"], + default: "ES6", + }, + { + type: "confirm", + name: "useReactState", + message: "Do you want to use React State in your project?", + default: true, + }, + { + type: "confirm", + name: "workboxWebpackPlugin", + message: "Do you want to add PWA support?", + default: true, + }, + { + type: "list", + name: "cssType", + message: "Which of the following CSS solution do you want to use?", + choices: ["none", "CSS only", "SASS", "LESS", "Stylus"], + default: "CSS only", + filter: (input, answers) => { + if (input === "none") { + answers.isCSS = false; + answers.isPostCSS = false; + answers.extractPlugin = "No"; + } else if (input === "CSS only") { + answers.isCSS = true; + } + return input; + }, + }, + { + type: "confirm", + name: "isCSS", + message: (answers) => + `Will you be using CSS styles along with ${answers.cssType} in your project?`, + when: (answers) => answers.cssType !== "CSS only", + default: true, + }, + { + type: "confirm", + name: "isPostCSS", + message: "Do you want to use PostCSS in your project?", + default: (answers: Answers) => answers.cssType == "CSS only", + }, + { + type: "list", + name: "extractPlugin", + message: "Do you want to extract CSS into separate files?", + choices: ["No", "Only for Production", "Yes"], + default: "No", + }, + { + type: "list", + name: "packageManager", + message: "Which package manager do you want to use?", + choices: ["npm", "yarn", "pnpm"], + default: "npm", + validate(input) { + if (!input.trim()) { + return "Package manager cannot be empty"; + } + return true; + }, + }, + ], + actions: function (answers: Answers) { + // setting some default values based on the answers + const actions: ActionType[] = []; + answers.htmlWebpackPlugin = true; + answers.devServer = true; + switch (answers.langType) { + case "ES6": + devDependencies.push( + "babel-loader", + "@babel/core", + "@babel/preset-env", + "@babel/preset-react", + ); + break; + case "Typescript": + devDependencies.push("typescript", "ts-loader", "@types/react", "@types/react-dom"); + break; + } + if (answers.isPostCSS) { + devDependencies.push("postcss-loader", "postcss", "autoprefixer"); + } + if (answers.cssType === "none") { + answers.isCSS = false; + answers.isPostCSS = false; + answers.extractPlugin = "No"; + } else { + devDependencies.push("style-loader", "css-loader"); + switch (answers.cssType) { + case "CSS only": + answers.isCSS = true; + break; + case "SASS": + devDependencies.push("sass-loader", "sass"); + break; + case "LESS": + devDependencies.push("less-loader", "less"); + break; + case "Stylus": + devDependencies.push("stylus-loader", "stylus"); + break; + } + } + if (answers.extractPlugin !== "No") { + devDependencies.push("mini-css-extract-plugin"); + } + if (answers.workboxWebpackPlugin) { + devDependencies.push("workbox-webpack-plugin"); + } + + const files: Array = [ + { filePath: "./index.html", fileType: "text" }, + { filePath: "webpack.config.js", fileType: "text" }, + { filePath: "package.json", fileType: "text" }, + { filePath: "README.md", fileType: "text" }, + { filePath: "./src/assets/webpack.png", fileType: "binary" }, + ]; + + switch (answers.langType) { + case "Typescript": + answers.entry = "./src/index.tsx"; + files.push( + { filePath: "tsconfig.json", fileType: "text" }, + { filePath: "index.d.ts", fileType: "text" }, + { filePath: "./src/App.tsx", fileType: "text" }, + { filePath: "./src/components/About.tsx", fileType: "text" }, + { filePath: "./src/components/Home.tsx", fileType: "text" }, + { filePath: "./src/components/Navbar.tsx", fileType: "text" }, + { filePath: "./src/router/index.tsx", fileType: "text" }, + { filePath: answers.entry as string, fileType: "text" }, + ); + break; + case "ES6": + answers.entry = "./src/index.jsx"; + files.push( + { filePath: "./src/App.jsx", fileType: "text" }, + { filePath: "./src/components/About.jsx", fileType: "text" }, + { filePath: "./src/components/Home.jsx", fileType: "text" }, + { filePath: "./src/components/Navbar.jsx", fileType: "text" }, + { filePath: "./src/router/index.jsx", fileType: "text" }, + { filePath: answers.entry as string, fileType: "text" }, + ); + break; + } + + switch (answers.cssType) { + case "CSS only": + files.push({ filePath: "./src/styles/global.css", fileType: "text" }); + break; + case "SASS": + files.push({ filePath: "./src/styles/global.scss", fileType: "text" }); + break; + case "LESS": + files.push({ filePath: "./src/styles/global.less", fileType: "text" }); + break; + case "Stylus": + files.push({ filePath: "./src/styles/global.styl", fileType: "text" }); + break; + } + + for (const file of files) { + actions.push({ + type: "fileGenerator", + path: join(answers.projectPath, file.filePath), + templateFile: join( + plop.getPlopfilePath(), + "../templates/init/react", + `${file.filePath}.tpl`, + ), + fileType: file.fileType, + data: answers, + }); + } + + actions.push({ + type: "pkgInstall", + path: answers.projectPath, + packages: devDependencies, + }); + return actions; + } as DynamicActionsFunction, + }); +} diff --git a/packages/create-webpack-app/src/generators/init/svelte.ts b/packages/create-webpack-app/src/generators/init/svelte.ts new file mode 100644 index 00000000000..98dc3aee228 --- /dev/null +++ b/packages/create-webpack-app/src/generators/init/svelte.ts @@ -0,0 +1,212 @@ +import { Answers, ActionType, FileRecord } from "../../types"; +import { dirname, join, resolve } from "path"; +import { NodePlopAPI, DynamicActionsFunction } from "node-plop"; +import { fileURLToPath } from "url"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); + +export default async function (plop: NodePlopAPI) { + // dependencies to be installed + const devDependencies: Array = [ + "webpack", + "webpack-cli", + "svelte", + "svelte-loader", + "webpack-dev-server", + "html-webpack-plugin", + ]; + + await plop.load("../../utils/pkgInstallAction.js", {}, true); + await plop.load("../../utils/fileGenerator.js", {}, true); + + plop.setDefaultInclude({ generators: true, actionTypes: true }); + plop.setPlopfilePath(resolve(__dirname, "../../plopfile.js")); + + // Define a base generator for the Svelte project structure + plop.setGenerator("init-svelte", { + description: "Create a basic Svelte-webpack project", + prompts: [ + { + type: "list", + name: "langType", + message: "Which of the following JS solutions do you want to use?", + choices: ["ES6", "Typescript"], + default: "ES6", + }, + { + type: "confirm", + name: "workboxWebpackPlugin", + message: "Do you want to add PWA support?", + default: true, + }, + { + type: "list", + name: "cssType", + message: "Which of the following CSS solution do you want to use?", + choices: ["none", "CSS only", "SASS", "LESS", "Stylus"], + default: "CSS only", + filter: (input, answers) => { + if (input === "none") { + answers.isCSS = false; + answers.isPostCSS = false; + answers.extractPlugin = "No"; + } else if (input === "CSS only") { + answers.isCSS = true; + } + return input; + }, + }, + { + type: "confirm", + name: "isCSS", + message: (answers) => + `Will you be using CSS styles along with ${answers.cssType} in your project?`, + when: (answers) => answers.cssType !== "CSS only", + default: true, + }, + { + type: "confirm", + name: "isPostCSS", + message: "Do you want to use PostCSS in your project?", + default: (answers: Answers) => answers.cssType == "CSS only", + }, + { + type: "list", + name: "extractPlugin", + message: "Do you want to extract CSS into separate files?", + choices: ["No", "Only for Production", "Yes"], + default: "No", + }, + { + type: "list", + name: "packageManager", + message: "Which package manager do you want to use?", + choices: ["npm", "yarn", "pnpm"], + default: "npm", + validate(input) { + if (!input.trim()) { + return "Package manager cannot be empty"; + } + return true; + }, + }, + ], + actions: function (answers: Answers) { + // setting some default values based on the answers + const actions: ActionType[] = []; + answers.htmlWebpackPlugin = true; + answers.devServer = true; + + switch (answers.langType) { + case "ES6": + devDependencies.push("babel-loader", "@babel/core", "@babel/preset-env"); + break; + case "Typescript": + devDependencies.push("typescript", "ts-loader", "@tsconfig/svelte"); + break; + } + + if (answers.isPostCSS) { + devDependencies.push("postcss-loader", "postcss", "autoprefixer"); + } + + if (answers.workboxWebpackPlugin) { + devDependencies.push("workbox-webpack-plugin"); + } + + if (answers.cssType === "none") { + answers.isCSS = false; + answers.isPostCSS = false; + answers.extractPlugin = "No"; + } else { + devDependencies.push("style-loader", "css-loader"); + switch (answers.cssType) { + case "CSS only": + answers.isCSS = true; + break; + case "SASS": + devDependencies.push("sass-loader", "sass"); + break; + case "LESS": + devDependencies.push("less-loader", "less"); + break; + case "Stylus": + devDependencies.push("stylus-loader", "stylus"); + break; + } + } + + if (answers.extractPlugin !== "No") { + devDependencies.push("mini-css-extract-plugin"); + } + + const files: Array = [ + { filePath: "./index.html", fileType: "text" }, + { filePath: "./src/assets/webpack.png", fileType: "binary" }, + { filePath: "webpack.config.js", fileType: "text" }, + { filePath: "package.json", fileType: "text" }, + { filePath: "README.md", fileType: "text" }, + { filePath: "./src/components/HelloWorld.svelte", fileType: "text" }, + { filePath: "./src/App.svelte", fileType: "text" }, + ]; + + switch (answers.langType) { + case "Typescript": + answers.entry = "./src/main.ts"; + files.push( + { filePath: "tsconfig.json", fileType: "text" }, + { filePath: "./src/index.d.ts", fileType: "text" }, + { filePath: answers.entry as string, fileType: "text" }, + ); + break; + case "ES6": + answers.entry = "./src/main.js"; + files.push({ filePath: answers.entry as string, fileType: "text" }); + break; + } + + if (answers.langType === "Typescript") { + files.push({ filePath: "./src/store/index.ts", fileType: "text" }); + } else { + files.push({ filePath: "./src/store/index.js", fileType: "text" }); + } + + switch (answers.cssType) { + case "CSS only": + files.push({ filePath: "./src/styles/global.css", fileType: "text" }); + break; + case "SASS": + files.push({ filePath: "./src/styles/global.scss", fileType: "text" }); + break; + case "LESS": + files.push({ filePath: "./src/styles/global.less", fileType: "text" }); + break; + case "Stylus": + files.push({ filePath: "./src/styles/global.styl", fileType: "text" }); + break; + } + + for (const file of files) { + actions.push({ + type: "fileGenerator", + path: join(answers.projectPath, file.filePath), + templateFile: join( + plop.getPlopfilePath(), + "../templates/init/svelte", + `${file.filePath}.tpl`, + ), + fileType: file.fileType, + data: answers, + }); + } + + actions.push({ + type: "pkgInstall", + path: answers.projectPath, + packages: devDependencies, + }); + + return actions; + } as DynamicActionsFunction, + }); +} diff --git a/packages/create-webpack-app/src/generators/init/vue.ts b/packages/create-webpack-app/src/generators/init/vue.ts new file mode 100644 index 00000000000..025daa80648 --- /dev/null +++ b/packages/create-webpack-app/src/generators/init/vue.ts @@ -0,0 +1,233 @@ +import { Answers, ActionType, FileRecord } from "../../types"; +import { dirname, join, resolve } from "path"; +import { NodePlopAPI, DynamicActionsFunction } from "node-plop"; +import { fileURLToPath } from "url"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +export default async function (plop: NodePlopAPI) { + // dependencies to be installed + const devDependencies: Array = [ + "webpack", + "webpack-cli", + "vue@3", + "webpack-dev-server", + "html-webpack-plugin", + "vue-loader@next", + "@vue/compiler-sfc", + "vue-router@4", + ]; + + await plop.load("../../utils/pkgInstallAction.js", {}, true); + await plop.load("../../utils/fileGenerator.js", {}, true); + + plop.setDefaultInclude({ generators: true, actionTypes: true }); + plop.setPlopfilePath(resolve(__dirname, "../../plopfile.js")); + + // Define a base generator for the Vue 3 project structure + plop.setGenerator("init-vue", { + description: "Create a basic Vue-webpack project", + prompts: [ + { + type: "list", + name: "langType", + message: "Which of the following JS solutions do you want to use?", + choices: ["ES6", "Typescript"], + default: "ES6", + }, + { + type: "confirm", + name: "useVueStore", + message: "Do you want to use Pinia for state management?", + default: true, + }, + { + type: "confirm", + name: "workboxWebpackPlugin", + message: "Do you want to add PWA support?", + default: true, + }, + { + type: "list", + name: "cssType", + message: "Which of the following CSS solution do you want to use?", + choices: ["none", "CSS only", "SASS", "LESS", "Stylus"], + default: "CSS only", + filter: (input, answers) => { + if (input === "none") { + answers.isCSS = false; + answers.isPostCSS = false; + answers.extractPlugin = "No"; + } else if (input === "CSS only") { + answers.isCSS = true; + } + return input; + }, + }, + { + type: "confirm", + name: "isCSS", + message: (answers) => + `Will you be using CSS styles along with ${answers.cssType} in your project?`, + when: (answers) => answers.cssType !== "CSS only", + default: true, + }, + { + type: "confirm", + name: "isPostCSS", + message: "Do you want to use PostCSS in your project?", + default: (answers: Answers) => answers.cssType == "CSS only", + }, + { + type: "list", + name: "extractPlugin", + message: "Do you want to extract CSS into separate files?", + choices: ["No", "Only for Production", "Yes"], + default: "No", + }, + { + type: "list", + name: "packageManager", + message: "Which package manager do you want to use?", + choices: ["npm", "yarn", "pnpm"], + default: "npm", + validate(input) { + if (!input.trim()) { + return "Package manager cannot be empty"; + } + return true; + }, + }, + ], + actions: function (answers: Answers) { + // setting some default values based on the answers + const actions: ActionType[] = []; + answers.htmlWebpackPlugin = true; + answers.devServer = true; + + switch (answers.langType) { + case "ES6": + devDependencies.push("babel-loader", "@babel/core", "@babel/preset-env"); + break; + case "Typescript": + devDependencies.push("typescript", "ts-loader"); + break; + } + + if (answers.useVueStore) { + devDependencies.push("pinia"); + } + + if (answers.isPostCSS) { + devDependencies.push("postcss-loader", "postcss", "autoprefixer"); + } + + if (answers.workboxWebpackPlugin) { + devDependencies.push("workbox-webpack-plugin"); + } + + if (answers.cssType === "none") { + answers.isCSS = false; + answers.isPostCSS = false; + answers.extractPlugin = "No"; + } else { + devDependencies.push("vue-style-loader", "style-loader", "css-loader"); + switch (answers.cssType) { + case "CSS only": + answers.isCSS = true; + break; + case "SASS": + devDependencies.push("sass-loader", "sass"); + break; + case "LESS": + devDependencies.push("less-loader", "less"); + break; + case "Stylus": + devDependencies.push("stylus-loader", "stylus"); + break; + } + } + + if (answers.extractPlugin !== "No") { + devDependencies.push("mini-css-extract-plugin"); + } + + const files: Array = [ + { filePath: "./index.html", fileType: "text" }, + { filePath: "./src/assets/webpack.png", fileType: "binary" }, + { filePath: "webpack.config.js", fileType: "text" }, + { filePath: "package.json", fileType: "text" }, + { filePath: "README.md", fileType: "text" }, + { filePath: "./src/App.vue", fileType: "text" }, + { filePath: "./src/components/Home.vue", fileType: "text" }, + { filePath: "./src/components/About.vue", fileType: "text" }, + { filePath: "./src/components/Layout.vue", fileType: "text" }, + { filePath: "./src/components/Navbar.vue", fileType: "text" }, + ]; + + switch (answers.langType) { + case "Typescript": + answers.entry = "./src/main.ts"; + files.push( + { filePath: "tsconfig.json", fileType: "text" }, + { filePath: answers.entry as string, fileType: "text" }, + ); + break; + case "ES6": + answers.entry = "./src/main.js"; + files.push({ filePath: answers.entry as string, fileType: "text" }); + break; + } + + if (answers.langType === "Typescript") { + files.push({ filePath: "./src/router/index.ts", fileType: "text" }); + } else { + files.push({ filePath: "./src/router/index.js", fileType: "text" }); + } + + if (answers.useVueStore) { + if (answers.langType === "Typescript") { + files.push({ filePath: "./src/store/index.ts", fileType: "text" }); + } else { + files.push({ filePath: "./src/store/index.js", fileType: "text" }); + } + } + + switch (answers.cssType) { + case "CSS only": + files.push({ filePath: "./src/styles/global.css", fileType: "text" }); + break; + case "SASS": + files.push({ filePath: "./src/styles/global.scss", fileType: "text" }); + break; + case "LESS": + files.push({ filePath: "./src/styles/global.less", fileType: "text" }); + break; + case "Stylus": + files.push({ filePath: "./src/styles/global.styl", fileType: "text" }); + break; + } + + for (const file of files) { + actions.push({ + type: "fileGenerator", + path: join(answers.projectPath, file.filePath), + templateFile: join( + plop.getPlopfilePath(), + "../templates/init/vue", + `${file.filePath}.tpl`, + ), + fileType: file.fileType, + data: answers, + }); + } + + actions.push({ + type: "pkgInstall", + path: answers.projectPath, + packages: devDependencies, + }); + + return actions; + } as DynamicActionsFunction, + }); +} diff --git a/packages/create-webpack-app/src/generators/loader/default.ts b/packages/create-webpack-app/src/generators/loader/default.ts new file mode 100644 index 00000000000..be50e990938 --- /dev/null +++ b/packages/create-webpack-app/src/generators/loader/default.ts @@ -0,0 +1,99 @@ +import { Answers, ActionType, FileRecord } from "../../types"; +import { dirname, join, resolve } from "path"; +import { NodePlopAPI, DynamicActionsFunction } from "node-plop"; +import { fileURLToPath } from "url"; +import { logger } from "../../utils/logger.js"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); + +export default async function (plop: NodePlopAPI) { + // dependencies to be installed + const devDependencies: Array = ["webpack-defaults"]; + + await plop.load("../../utils/pkgInstallAction.js", {}, true); + await plop.load("../../utils/fileGenerator.js", {}, true); + + // custom helper function + plop.setHelper("makeLoaderName", (name: string) => { + name = plop.getHelper("kebabCase")(name); + + if (!/loader$/.test(name)) { + name += "-loader"; + } + return name; + }); + + plop.setDefaultInclude({ generators: true, actionTypes: true }); + plop.setPlopfilePath(resolve(__dirname, "../../plopfile.js")); + + // Define a base generator for the project structure + plop.setGenerator("loader-default", { + description: "Create a basic webpack loader.", + prompts: [ + { + type: "input", + name: "name", + message: "Loader name?", + default: "my-loader", + validate: (str: string): boolean => str.length > 0, + }, + { + type: "list", + name: "packageManager", + message: "Pick a package manager:", + choices: ["npm", "yarn", "pnpm"], + default: "npm", + validate(input) { + if (!input.trim()) { + return "Package manager cannot be empty"; + } + return true; + }, + }, + ], + actions: function (answers: Answers) { + const actions: ActionType[] = []; + answers.projectPath = join(answers.projectPath, answers.name); + + logger.error(` + Your project must be inside a folder named ${answers.name} + I will create this folder for you. + `); + + const files: Array = [ + { filePath: "./package.json", fileType: "text" }, + { filePath: "./examples/simple/src/index.js", fileType: "text" }, + { filePath: "./examples/simple/src/lazy-module.js", fileType: "text" }, + { filePath: "./examples/simple/src/static-esm-module.js", fileType: "text" }, + { filePath: "./examples/simple/webpack.config.js", fileType: "text" }, + { filePath: "./src/cjs.js", fileType: "text" }, + { filePath: "./test/fixtures/simple-file.js", fileType: "text" }, + { filePath: "./test/functional.test.js", fileType: "text" }, + { filePath: "./test/test-utils.js", fileType: "text" }, + { filePath: "./test/unit.test.js", fileType: "text" }, + { filePath: "./src/index.js", fileType: "text" }, + ]; + + for (const file of files) { + actions.push({ + type: "fileGenerator", + path: join(answers.projectPath, file.filePath), + templateFile: join( + plop.getPlopfilePath(), + "../templates/loader/default", + `${file.filePath}.tpl`, + ), + fileType: file.fileType, + data: answers, + }); + } + + actions.push({ + type: "pkgInstall", + path: answers.projectPath, + packages: devDependencies, + }); + return actions; + } as DynamicActionsFunction, + }); +} diff --git a/packages/create-webpack-app/src/generators/plugin/default.ts b/packages/create-webpack-app/src/generators/plugin/default.ts new file mode 100644 index 00000000000..cd8f5330e9b --- /dev/null +++ b/packages/create-webpack-app/src/generators/plugin/default.ts @@ -0,0 +1,91 @@ +import { Answers, ActionType, FileRecord } from "../../types"; +import { dirname, join, resolve } from "path"; +import { NodePlopAPI, DynamicActionsFunction } from "node-plop"; +import { fileURLToPath } from "url"; +import { logger } from "../../utils/logger.js"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); + +export default async function (plop: NodePlopAPI) { + // dependencies to be installed + const devDependencies: Array = ["webpack-defaults"]; + + await plop.load("../../utils/pkgInstallAction.js", {}, true); + await plop.load("../../utils/fileGenerator.js", {}, true); + + plop.setDefaultInclude({ generators: true, actionTypes: true }); + plop.setPlopfilePath(resolve(__dirname, "../../plopfile.js")); + + // Define a base generator for the project structure + plop.setGenerator("plugin-default", { + description: "Create a basic webpack plugin.", + prompts: [ + { + type: "input", + name: "name", + message: "Plugin name?", + default: "my-webpack-plugin", + filter: (input) => plop.getHelper("kebabCase")(input), + validate: (str: string): boolean => str.length > 0, + }, + { + type: "list", + name: "packageManager", + message: "Pick a package manager:", + choices: ["npm", "yarn", "pnpm"], + default: "npm", + validate(input) { + if (!input.trim()) { + return "Package manager cannot be empty"; + } + return true; + }, + }, + ], + actions: function (answers: Answers) { + const actions: ActionType[] = []; + answers.projectPath = join(answers.projectPath, answers.name); + + logger.error(` + Your project must be inside a folder named ${answers.name} + I will create this folder for you. + `); + + answers.pluginIdentifier = plop.getHelper("pascalCase")(answers.name); + + const files: Array = [ + { filePath: "./package.json", fileType: "text" }, + { filePath: "./examples/simple/src/index.js", fileType: "text" }, + { filePath: "./examples/simple/src/lazy-module.js", fileType: "text" }, + { filePath: "./examples/simple/src/static-esm-module.js", fileType: "text" }, + { filePath: "./examples/simple/webpack.config.js", fileType: "text" }, + { filePath: "./src/cjs.js", fileType: "text" }, + { filePath: "./test/fixtures/simple-file.js", fileType: "text" }, + { filePath: "./test/functional.test.js", fileType: "text" }, + { filePath: "./test/test-utils.js", fileType: "text" }, + { filePath: "./src/index.js", fileType: "text" }, + ]; + + for (const file of files) { + actions.push({ + type: "fileGenerator", + path: join(answers.projectPath, file.filePath), + templateFile: join( + plop.getPlopfilePath(), + "../templates/plugin/default", + `${file.filePath}.tpl`, + ), + fileType: file.fileType, + data: answers, + }); + } + + actions.push({ + type: "pkgInstall", + path: answers.projectPath, + packages: devDependencies, + }); + return actions; + } as DynamicActionsFunction, + }); +} diff --git a/packages/create-webpack-app/src/index.ts b/packages/create-webpack-app/src/index.ts new file mode 100644 index 00000000000..1f67cc6c65d --- /dev/null +++ b/packages/create-webpack-app/src/index.ts @@ -0,0 +1,211 @@ +import { Command } from "commander"; +import { resolve, dirname } from "path"; +import { select } from "@inquirer/prompts"; +import nodePlop, { PlopGenerator } from "node-plop"; +import { fileURLToPath } from "url"; + +import { onSuccessHandler, onFailureHandler, logger } from "./utils/logger.js"; +import { Answers, InitOptions, LoaderOptions, PluginOptions } from "./types"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); + +const program = new Command(); + +const plop = await nodePlop(resolve(__dirname, "./plopfile.js")); + +const baseAnswers: Answers = { + projectPath: process.cwd(), + langType: "none", + devServer: true, + htmlWebpackPlugin: true, + workboxWebpackPlugin: true, + cssType: "none", + isCSS: false, + isPostCSS: false, + extractPlugin: "No", + packageManager: "npm", +}; +const initValues: Record = { + default: { + ...baseAnswers, + }, + react: { + ...baseAnswers, + langType: "ES6", + useReactState: true, + cssType: "CSS only", + }, + vue: { + ...baseAnswers, + langType: "ES6", + useVueStore: true, + cssType: "CSS only", + }, + svelte: { + ...baseAnswers, + langType: "ES6", + cssType: "CSS only", + }, +}; + +const initGenerators: Record = { + default: plop.getGenerator("init-default"), + react: plop.getGenerator("init-react"), + vue: plop.getGenerator("init-vue"), + svelte: plop.getGenerator("init-svelte"), +}; +const loaderGenerators: Record = { + default: plop.getGenerator("loader-default"), +}; + +const pluginGenerators: Record = { + default: plop.getGenerator("plugin-default"), +}; + +program + .version("1.0.0", "-v, --version") + .usage("[command] [options]") + .helpOption("-h, --help", "Display help for command") + .description("A CLI tool to generate a Webpack project"); + +program + .command("init", { isDefault: true }) + .aliases(["i", "n", "c", "create", "new"]) + .description("Initialize a new Webpack project") + .argument("[projectPath]", "Path to create the project") + .option("-f, --force", "Skip the prompt and use the default values", false) + .option("-t --template