diff --git a/index.js b/index.js index e9e3331..bcac6ef 100644 --- a/index.js +++ b/index.js @@ -1,304 +1,321 @@ -#!/usr/bin/env node - -const chalk = require('chalk'); -const fs = require('fs'); -const inquirer = require('inquirer'); -const meow = require('meow'); -const path = require('path'); -const spawn = require('cross-spawn'); - -const logger = require('./logger')("verbose"); -const { CHOICE_ORDER } = require('./constants'); -const { replace } = require('./utils'); - -const CURR_DIR = process.cwd(); -const TEMPLATE_DIR = path.join(__dirname, 'templates'); - -const CHOICES = getChoices(TEMPLATE_DIR); - -const cli = meow(` - Usage - $ generate-project - - Options - --dry-run, -n Do not generate project, but show directory and package.json - --yes, -y Use default values for package.json - --template, -t Name of the template to use - `, { - flags: { - "dry-run": { - type: 'boolean', - alias: 'n' - }, - "yes": { - type: 'boolean', - alias: 'y' - }, - "template": { - type: 'string', - alias: 't' - }, - "install": { - type: 'string', - alias: 'i' - } - } -}); - -const template = getTemplate(cli.flags.template); - -function createQuestions() { - const questions = []; - - if (!cli.input[0]) { - questions.push({ - name: 'name', - type: 'input', - message: 'Project Name:', - validate: input => /^([A-Za-z\-\_\d])+$/.test(input) || 'Project name may only include letters, numbers, underscores and dashes.' - }); - } - - if (!template) { - questions.push({ - name: 'project', - type: 'list', - message: 'Choose project', - choices: CHOICES - }); - } - - if (!cli.flags.yes) { - questions.push( - { - name: 'version', - type: 'input', - message: 'Version:', - default: '1.0.0' - }, - { - name: 'description', - type: 'input', - message: 'Description:' - }, - { - name: 'author', - type: 'input', - message: 'Author:' - }, - { - name: 'license', - type: 'input', - message: 'License:', - default: 'ISC' - }); - } - - if (!cli.flags.install) { - questions.push( - { - name: 'shouldInstall', - type: 'confirm', - message: 'Run npm install?' - } - ); - } - - questions.push( - { - name: 'shouldGitInit', - type: 'confirm', - message: 'Create Git Repository?' - } - ); - - return questions; -} - -async function askQuestions(questions) { - const answers = await inquirer.prompt(questions); - const { - project = template, - name = cli.input[0], - version = "1.0.0", - description = "", - author = "", - license = "ISC", - shouldInstall, - shouldGitInit } = answers; - - const templatePath = path.join(TEMPLATE_DIR, project) - const targetPath = path.join(CURR_DIR, name); - - if (!await confirmOptions(targetPath, { name, version, description, author, license, project })) { - return; - } - - if (!cli.flags.dryRun) { - const format = formatFromAnswers({ name, version, description, author, license }); - - await generateProject(templatePath, targetPath, format); - - if (shouldInstall) { - await installDependencies(targetPath); - } - - if (shouldGitInit) { - await initializeGitRepo(targetPath); - } - } - - logger.info(chalk`{green Project generated:} {underline ${targetPath}}`); -} - -async function confirmOptions(targetPath, { name, version, description, author, license, project }) { - logger.info(chalk`About to generate a {underline ${project}} project in {underline ${targetPath}}`); - - logger.info(` -{ - "name": "${name}", - "version": "${version}", - "description": "${description}", - "author": "${author}", - "license": "${license}" -}`); - - return promptShouldContinue() -} - -async function promptShouldContinue() { - const cont = await inquirer.prompt([ - { - name: 'confirm', - type: 'confirm', - message: 'Is this OK?' - } - ]); - - return cont.confirm; -} - -function formatFromAnswers({ name, version, description, author, license }) { - const format = [ - { - replace: /{title}/, - value: name - }, - { - replace: /{version}/, - value: version - }, - { - replace: /{description}/, - value: description - }, - { - replace: /{author}/, - value: author - }, - { - replace: /{license}/, - value: license - } - ] - - return format; -} - -async function generateProject(templatePath, targetPath, format) { - await fs.promises.mkdir(targetPath); - await copyDirectoryContents(templatePath, targetPath, format); -} - -function spawnPromise(command, args, options) { - return new Promise((resolve, reject) => { - spawn(command, args, options) - .on('close', code => code ? reject() : resolve()); - }); -} - -async function installDependencies(cwd) { - logger.info(`> npm install`); - - try { - await spawnPromise('npm', ['install'], { - stdio: 'inherit', - cwd - }); - logger.info(chalk`{blue Install finished}`); - } catch { - logger.info(chalk`{red There was an error installing the project}`); - logger.info(chalk`Install manually with {underline npm install} in {underline ${targetPath}`); - } -} - -async function initializeGitRepo(cwd) { - logger.info(`> git init`); - - try { - await spawnPromise('git', ['init'], { - stdio: 'inherit', - cwd - }); - logger.info(chalk`{blue Git repo created}`); - } catch (err) { - logger.info(chalk`{red There was an error initializing the git repo}`); - logger.info(chalk`Initialize repo manually with {underline git init} in {underline ${targetPath}`); - } -} - -async function formatFile(path, format) { - const file = await fs.promises.readFile(path, 'utf8'); - const content = replace(file, format); - - return content; -} - -async function copyFile(templateFilePath, targetFilePath, format) { - if (templateFilePath.includes('package.json')) { - // Format the package.json file to include options - const content = await formatFile(templateFilePath, format); - await fs.promises.writeFile(targetFilePath, content, 'utf8'); - } else { - // Copy all other files - await fs.promises.copyFile(templateFilePath, targetFilePath); - } -} - -async function copyDirectory(templateFilePath, targetFilePath, format) { - await fs.promises.mkdir(targetFilePath); - await copyDirectoryContents(templateFilePath, targetFilePath, format); -} - -async function copyDirectoryContents(templatePath, targetPath, format) { - const files = await fs.promises.readdir(templatePath); - - await Promise.all(files.map(async file => { - const templateFilePath = path.join(templatePath, file); - const targetFilePath = path.join(targetPath, file); - - const stats = await fs.promises.stat(templateFilePath); - - if (stats.isFile()) { - await copyFile(templateFilePath, targetFilePath, format); - } else if (stats.isDirectory()) { - await copyDirectory(templateFilePath, targetFilePath, format); - } - })); -} - -function getChoices(templatePath) { - const templates = fs.readdirSync(templatePath).sort((a, b) => CHOICE_ORDER.indexOf(b) - CHOICE_ORDER.indexOf(a)); - - return templates; -} - -function getTemplate(template) { - if (template && CHOICES.includes(template)) { - logger.debug("Using template: ", template); - return template; - } -} - -const questions = createQuestions(); -askQuestions(questions); \ No newline at end of file +#!/usr/bin/env node + +const chalk = require('chalk'); +const fs = require('fs'); +const inquirer = require('inquirer'); +const meow = require('meow'); +const path = require('path'); +const spawn = require('cross-spawn'); + +const logger = require('./logger')("verbose"); +const { CHOICE_ORDER } = require('./constants'); + +const CURR_DIR = process.cwd(); +const TEMPLATE_DIR = path.join(__dirname, 'templates'); + +const CHOICES = getChoices(TEMPLATE_DIR); + +const cli = meow(` + Usage + $ generate-project + + Options + --dry-run, -n Do not generate project, but show directory and package.json + --install, -i Install dependencies after project generation + --yes, -y Use default values for package.json + --template, -t Name of the template to use + --version, -v Get current version + `, { + flags: { + "dry-run": { + type: 'boolean', + alias: 'n' + }, + "yes": { + type: 'boolean', + alias: 'y' + }, + "template": { + type: 'string', + alias: 't' + }, + "install": { + type: 'boolean', + alias: 'i' + }, + "version": { + type: 'boolean', + alias: 'v' + } + } +}); + +const template = getTemplate(cli.flags.template); + +function createQuestions() { + const questions = []; + + if (!cli.input[0]) { + questions.push({ + name: 'name', + type: 'input', + message: 'Project Name:', + validate: input => /^([A-Za-z\-\_\d])+$/.test(input) || 'Project name may only include letters, numbers, underscores and dashes.' + }); + } + + if (!template) { + questions.push({ + name: 'project', + type: 'list', + message: 'Choose project', + choices: CHOICES + }); + } + + if (!cli.flags.yes) { + questions.push( + { + name: 'version', + type: 'input', + message: 'Version:', + default: '1.0.0' + }, + { + name: 'description', + type: 'input', + message: 'Description:' + }, + { + name: 'author', + type: 'input', + message: 'Author:' + }, + { + name: 'license', + type: 'input', + message: 'License:', + default: 'ISC' + }); + } + + if (!cli.flags.install) { + questions.push( + { + name: 'shouldInstall', + type: 'confirm', + message: 'Run install?' + } + ) + } + + return questions; +} + +async function askQuestions(questions) { + const answers = await inquirer.prompt(questions); + const { + project = template, + name = cli.input[0], + version = "1.0.0", + description = "", + author = "", + license = "ISC", + shouldInstall = true + } = answers; + + const templatePath = path.join(TEMPLATE_DIR, project) + const targetPath = path.join(CURR_DIR, name); + + const { installer } = shouldInstall ? await askAboutInstaller() : { installer: 'npm' }; + + const { shouldGitInit } = await inquirer.prompt([ + { + name: 'shouldGitInit', + type: 'confirm', + message: 'Create Git Repository?' + } + ]) + + if (!await confirmOptions(targetPath, { name, version, description, author, license, project })) { + return; + } + + if (!cli.flags.dryRun) { + await generateProject(templatePath, targetPath); + + await spawnPromise('npm', ['pkg', 'set', `name=${name}`, `version=${version}`, `description=${description}`, `author=${author}`, `license=${license}`], { + stdio: 'inherit', + cwd: targetPath + }); + + if (shouldInstall) { + await installDependencies(targetPath, installer); + } + + if (shouldGitInit) { + await initializeGitRepo(targetPath); + } + } + + logger.info(chalk`{green Project generated:} {underline ${targetPath}}`); +} + +async function confirmOptions(targetPath, { name, version, description, author, license, project }) { + logger.info(chalk`About to generate a {underline ${project}} project in {underline ${targetPath}}`); + + logger.info(` +{ + "name": "${name}", + "version": "${version}", + "description": "${description}", + "author": "${author}", + "license": "${license}" +}`); + + return promptShouldContinue() +} + +async function promptShouldContinue() { + const cont = await inquirer.prompt([ + { + name: 'confirm', + type: 'confirm', + message: 'Is this OK?' + } + ]); + + return cont.confirm; +} + +function askAboutInstaller() { + return inquirer.prompt([ + { + name: 'installer', + type: 'list', + message: 'Choose Installer', + choices: ['npm', 'yarn'] + } + ]) +} + +async function generateProject(templatePath, targetPath) { + await fs.promises.mkdir(targetPath); + await copyDirectoryContents(templatePath, targetPath); +} + +function spawnPromise(command, args, options) { + return new Promise((resolve, reject) => { + spawn(command, args, options) + .on('close', code => code ? reject() : resolve()); + }); +} + +async function installNpm(cwd) { + logger.info(`> npm install`); + + try { + await spawnPromise('npm', ['install'], { + stdio: 'inherit', + cwd + }); + logger.info(chalk`{blue Install finished}`); + } catch { + logger.info(chalk`{red There was an error installing the project}`); + logger.info(chalk`Install manually with {underline npm install} in {underline ${targetPath}`); + } +} + +async function installYarn(cwd) { + logger.info(`> yarn`); + + try { + await spawnPromise('yarn', { + stdio: 'inherit', + cwd + }); + logger.info(chalk`{blue Install finished}`); + } catch { + logger.info(chalk`{red There was an error installing the project}`); + logger.info(chalk`Install manually with {underline yarn} in {underline ${targetPath}`); + } +} + +async function installDependencies(cwd, installer) { + const installers = { + 'npm': installNpm, + 'yarn': installYarn + } + + await installers[installer]?.(cwd) ?? logger.error('Invalid installer') +} + +async function initializeGitRepo(cwd) { + logger.info(`> git init`); + + try { + await spawnPromise('git', ['init'], { + stdio: 'inherit', + cwd + }); + + logger.info(`> git add .`); + await spawnPromise('git', ['add', '.'], { + stdio: 'inherit', + cwd + }); + + logger.info(`> git commit -m "Initial commit"`); + await spawnPromise('git', ['commit', '-m', '"Initial commit"'], { + stdio: 'inherit', + cwd + }); + + logger.info(chalk`{blue Git repo created}`); + } catch (err) { + logger.info(chalk`{red There was an error initializing the git repo}`); + logger.info(chalk`Initialize repo manually with {underline git init} in {underline ${targetPath}`); + } +} + +async function copyDirectory(templateFilePath, targetFilePath) { + await fs.promises.mkdir(targetFilePath); + await copyDirectoryContents(templateFilePath, targetFilePath); +} + +async function copyDirectoryContents(source, dest) { + const files = await fs.promises.readdir(source); + + await Promise.all(files.map(async file => { + const sourcePath = path.join(source, file); + const destPath = path.join(dest, file); + + const stats = await fs.promises.stat(sourcePath); + + if (stats.isFile()) { + await fs.promises.copyFile(sourcePath, destPath); + } else if (stats.isDirectory()) { + await copyDirectory(sourcePath, destPath); + } + })); +} + +function getChoices(templatePath) { + const templates = fs.readdirSync(templatePath).sort((a, b) => CHOICE_ORDER.indexOf(b) - CHOICE_ORDER.indexOf(a)); + + return templates; +} + +function getTemplate(template) { + if (template && CHOICES.includes(template)) { + logger.debug("Using template: ", template); + return template; + } +} + +if (cli.flags.version) { + logger.info(process.env.npm_package_version) +} else { + const questions = createQuestions(); + askQuestions(questions); +} diff --git a/templates/webpack-typescript/.babelrc b/templates/webpack-typescript/.babelrc new file mode 100644 index 0000000..a4a89c0 --- /dev/null +++ b/templates/webpack-typescript/.babelrc @@ -0,0 +1,10 @@ +{ + "presets": [ + "@babel/preset-env", + "@babel/preset-typescript", + ], + "plugins": [ + "@babel/proposal-class-properties", + "@babel/proposal-object-rest-spread" + ] +} \ No newline at end of file diff --git a/templates/webpack-typescript/index.html b/templates/webpack-typescript/index.html index 943f2d4..d020c16 100644 --- a/templates/webpack-typescript/index.html +++ b/templates/webpack-typescript/index.html @@ -1,13 +1,13 @@ - - - - - - - Document - - - - - + + + + + + + Document + + + + + \ No newline at end of file diff --git a/templates/webpack-typescript/package.json b/templates/webpack-typescript/package.json index bc0d9b3..e5c7958 100644 --- a/templates/webpack-typescript/package.json +++ b/templates/webpack-typescript/package.json @@ -1,25 +1,31 @@ -{ - "name": "{title}", - "version": "{version}", - "description": "{description}", - "author": "{author}", - "license": "{license}", - "scripts": { - "test": "echo \"Error: no test specified\" && exit 1", - "start": "webpack-dev-server --config webpack.dev.js", - "build": "webpack --config webpack.prod.js" - }, - "keywords": [ - "webpack", - "typescript" - ], - "devDependencies": { - "ts-loader": "^5.2.1", - "typescript": "^3.1.3", - "webpack": "^4.20.2", - "webpack-cli": "^3.1.2", - "webpack-dev-server": "^3.1.9", - "webpack-merge": "^4.1.4", - "html-webpack-plugin": "^3.2.0" - } -} +{ + "name": "{title}", + "version": "{version}", + "description": "{description}", + "author": "{author}", + "license": "{license}", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1", + "start": "webpack-dev-server --config webpack.dev.js", + "build": "webpack --config webpack.prod.js" + }, + "keywords": [ + "webpack", + "typescript" + ], + "devDependencies": { + "@babel/core": "^7.15.5", + "@babel/plugin-proposal-class-properties": "^7.14.5", + "@babel/plugin-proposal-object-rest-spread": "^7.15.6", + "@babel/preset-env": "^7.15.6", + "@babel/preset-react": "^7.14.5", + "@babel/preset-typescript": "^7.15.0", + "babel-loader": "^8.2.2", + "typescript": "^4.4.4", + "webpack": "^5.60.0", + "webpack-cli": "^4.9.1", + "webpack-dev-server": "^4.4.0", + "webpack-merge": "^5.8.0", + "html-webpack-plugin": "^5.5.0" + } +} \ No newline at end of file diff --git a/templates/webpack-typescript/webpack.config.js b/templates/webpack-typescript/webpack.config.js index b3db234..d29def8 100644 --- a/templates/webpack-typescript/webpack.config.js +++ b/templates/webpack-typescript/webpack.config.js @@ -1,29 +1,29 @@ -const path = require('path'); -const HtmlWebpackPlugin = require('html-webpack-plugin'); - -module.exports = { - entry: './src/index.ts', - module: { - rules: [ - { - test: /\.tsx?$/, - use: 'ts-loader', - exclude: /node_modules/ - } - ] - }, - resolve: { - extensions: [ '.tsx', '.ts', '.js' ] - }, - plugins: [ - new HtmlWebpackPlugin({ - template: 'index.html', - inject: false - }) - ], - output: { - filename: 'bundle.js', - path: path.resolve(__dirname, 'dist'), - publicPath: '/dist/' - } +const path = require('path'); +const HtmlWebpackPlugin = require('html-webpack-plugin'); + +module.exports = { + entry: './src/index.ts', + module: { + rules: [ + { + test: /\.(ts|js)x?$/, + use: 'babel-loader', + exclude: /node_modules/ + } + ] + }, + resolve: { + extensions: [ '.tsx', '.ts', '.js' ] + }, + plugins: [ + new HtmlWebpackPlugin({ + template: 'index.html', + inject: false + }) + ], + output: { + filename: 'bundle.js', + path: path.resolve(__dirname, 'dist'), + publicPath: '/' + } }; \ No newline at end of file diff --git a/templates/webpack-typescript/webpack.dev.js b/templates/webpack-typescript/webpack.dev.js index 33c3af4..7e00d6f 100644 --- a/templates/webpack-typescript/webpack.dev.js +++ b/templates/webpack-typescript/webpack.dev.js @@ -1,7 +1,7 @@ -const merge = require('webpack-merge'); -const common = require('./webpack.config'); - -module.exports = merge(common, { - mode: 'development', - devtool: 'inline-source-map' +const { merge } = require('webpack-merge'); +const common = require('./webpack.config'); + +module.exports = merge(common, { + mode: 'development', + devtool: 'inline-source-map' }) \ No newline at end of file diff --git a/templates/webpack-typescript/webpack.prod.js b/templates/webpack-typescript/webpack.prod.js index 6fcb595..e954237 100644 --- a/templates/webpack-typescript/webpack.prod.js +++ b/templates/webpack-typescript/webpack.prod.js @@ -1,7 +1,7 @@ -const merge = require('webpack-merge'); -const common = require('./webpack.config'); - -module.exports = merge(common, { - mode: 'production', - devtool: 'source-map' +const { merge } = require('webpack-merge'); +const common = require('./webpack.config'); + +module.exports = merge(common, { + mode: 'production', + devtool: 'source-map' }) \ No newline at end of file