├── .gitignore ├── .npmignore ├── README.md ├── bin └── create-electron-react-app ├── index.ts ├── package-lock.json ├── package.json ├── src ├── cloneBase.ts ├── deleteGitDir.ts ├── done.ts ├── error.ts ├── generateReadme.ts ├── getAnswers.ts ├── index.ts ├── installDependencies.ts ├── makeDirectory.ts ├── spawnHelper.ts └── updatePackage.ts ├── tsconfig.json └── tslint.json /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | .DS_Store 3 | .vscode 4 | node_modules 5 | *.js 6 | *.js.map 7 | *.d.ts 8 | *.tgz 9 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | *.ts 2 | !*.d.ts 3 | *.tgz -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # create-electron-react-app 3 | ## An opinionated command-line utility for creating an electron app using react 4 | 5 | Builds apps using [electron-react-starter](https://github.com/kgroat/electron-react-starter) as a base. 6 | [![CircleCI](https://circleci.com/gh/kgroat/electron-react-starter.svg?style=shield&circle-token=18b44433f089413275cb90569f4aee3fc1d4a2ba)](https://circleci.com/gh/kgroat/workflows/electron-react-starter) 7 | [![Coverage Status](https://coveralls.io/repos/github/kgroat/electron-react-starter/badge.svg?branch=coveralls)](https://coveralls.io/github/kgroat/electron-react-starter?branch=coveralls) 8 | 9 | ### Usage 10 | Option 1 -- Global installation 11 | 12 | Install the CLI tool: 13 | `npm i -g create-electron-react-app` 14 | 15 | Run the tool: 16 | `create-electron-react-app` 17 | 18 | Option 2 -- Using `npx` (Requires NPM 5+) 19 | 20 | Install and run the tool: 21 | `npx create-electron-react-app` 22 | 23 | ### Features 24 | #### Tooling 25 | Generated applications will come with a configuration ready to be used with: 26 | * [`react`](https://facebook.github.io/react/) 27 | * [`redux`](http://redux.js.org/) 28 | * [`typescript`](https://www.typescriptlang.org/) 29 | * [`sass/scss`](http://sass-lang.com/) 30 | * [`jest`](https://facebook.github.io/jest/) 31 | * [`storybook`](https://storybook.js.org/) 32 | 33 | #### 34 | 35 | ### Prompts 36 | You will be prompted for a few pieces of information: 37 | 38 | `app name` (Required) 39 | * This is stored in the generated `package.json` as the `"appName"` property. It can be changed there at any time. 40 | * In MacOS builds, it is the name of the `.app` package, the name of the app as it appears in the menu bar and Activity Monitor, and by default the title of the main window. 41 | * In windows builds, it is the name of the `.exe` file, the name that appears in Task Manager, and by default the title of the main window. 42 | 43 | `directory name` (Required) 44 | * This is the name of the directory created that the app will be generated inside of. 45 | * This can only consist of lowercase letters, numbers, dashes, and underscores. 46 | * It is also used as the `"name"` property in the generated `package.json` 47 | 48 | `app identifier` (Required) 49 | * This is stored in the generated `package.json` as the `"identifier"` property. It can be changed there at any time. 50 | * In MacOS builds, this is used as the unique identifier for the package. 51 | * In windows builds, this serves no purpose. 52 | 53 | `description` (Optional) 54 | * This is used as the `"description"` property in the generated `package.json` 55 | 56 | `git repository` (Optional) 57 | * This is used in the `"repository"`, `"bugs"`, and `"homepage"` properties of the generated `package.json` 58 | 59 | `author` (Optional) 60 | * This is used in the `"author"` property of the generated `package.json` 61 | 62 | `lisence` (Optional) 63 | * This is used in the `"lisence"` property of the generated `package.json` 64 | -------------------------------------------------------------------------------- /bin/create-electron-react-app: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | require('../index.js') 4 | -------------------------------------------------------------------------------- /index.ts: -------------------------------------------------------------------------------- 1 | 2 | import execute from './src/index' 3 | 4 | execute() 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "create-electron-react-app", 3 | "version": "1.2.1", 4 | "description": "An opinionated command-line utility for creating an electron app using react", 5 | "main": "index.js", 6 | "bin": "./bin/create-electron-react-app", 7 | "scripts": { 8 | "prepare": "tsc", 9 | "test": "jest" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/kgroat/create-electron-react-app.git" 14 | }, 15 | "keywords": [ 16 | "electron", 17 | "react", 18 | "cli" 19 | ], 20 | "author": "kgroat (Kevin Groat)", 21 | "license": "ISC", 22 | "bugs": { 23 | "url": "https://github.com/kgroat/create-electron-react-app/issues" 24 | }, 25 | "homepage": "https://github.com/kgroat/create-electron-react-app#readme", 26 | "dependencies": { 27 | "bluebird": "^3.5.0", 28 | "colors": "^1.1.2", 29 | "ejs": "^2.5.7", 30 | "inquirer": "^3.3.0", 31 | "npm": "^6.5.0", 32 | "rimraf": "^2.6.2" 33 | }, 34 | "devDependencies": { 35 | "@types/bluebird": "^3.5.19", 36 | "@types/colors": "^1.1.3", 37 | "@types/ejs": "^2.5.0", 38 | "@types/inquirer": "0.0.36", 39 | "@types/node": "^8.5.2", 40 | "@types/npm": "^2.0.29", 41 | "@types/rimraf": "^2.0.2", 42 | "jest": "^21.1.0", 43 | "tslint": "^5.8.0", 44 | "tslint-config-standard": "^7.0.0", 45 | "typescript": "^2.6.2" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/cloneBase.ts: -------------------------------------------------------------------------------- 1 | 2 | import spawn from './spawnHelper' 3 | import { Answers } from './getAnswers' 4 | 5 | const gitUrl = 'https://github.com/kgroat/electron-react-starter' 6 | 7 | export default function (answers: Answers) { 8 | return spawn('git', ['clone', gitUrl, answers.dirName]) 9 | .then(function () { 10 | console.log('Starter cloned!') 11 | return answers 12 | }) 13 | } 14 | -------------------------------------------------------------------------------- /src/deleteGitDir.ts: -------------------------------------------------------------------------------- 1 | 2 | import * as rimraf from 'rimraf' 3 | import * as Bluebird from 'bluebird' 4 | import { Answers } from './getAnswers' 5 | 6 | const rmdir = Bluebird.promisify(rimraf) 7 | 8 | export default function (answers: Answers) { 9 | return rmdir('.git') 10 | .then(function () { 11 | return answers 12 | }) 13 | } 14 | -------------------------------------------------------------------------------- /src/done.ts: -------------------------------------------------------------------------------- 1 | 2 | import * as colors from 'colors/safe' 3 | import { Answers } from './getAnswers' 4 | 5 | const commands = [ 6 | ['npm start', 'Run the app in development mode'], 7 | ['npm test', 'Run the unit tests'], 8 | ['npm run lint', 'Run the linter'], 9 | ['npm run build', 'Build the application for publishing'], 10 | ] 11 | 12 | export default function (answers: Answers) { 13 | console.log() 14 | console.log(colors.green('Done!')) 15 | console.log(colors.green('To get to your project run ') + colors.blue('`cd ' + answers.dirName + '`')) 16 | console.log(colors.green('From there, you can issue the following commands:')) 17 | commands.forEach(function (command) { 18 | console.log(' * ' + colors.cyan(command[0]) + ' -- ' + colors.magenta(command[1])) 19 | }) 20 | console.log() 21 | console.log('For more information, see:') 22 | console.log(colors.blue('https://github.com/kgroat/electron-react-starter#readme')) 23 | console.log() 24 | } 25 | -------------------------------------------------------------------------------- /src/error.ts: -------------------------------------------------------------------------------- 1 | 2 | export default function (err: any) { 3 | console.error('An error occurred') 4 | console.error(err) 5 | process.exit(1) 6 | } 7 | -------------------------------------------------------------------------------- /src/generateReadme.ts: -------------------------------------------------------------------------------- 1 | 2 | import * as ejs from 'ejs' 3 | import * as fs from 'fs' 4 | import * as path from 'path' 5 | import * as rimraf from 'rimraf' 6 | import * as Bluebird from 'bluebird' 7 | import { Answers } from './getAnswers' 8 | 9 | const rm = Bluebird.promisify(rimraf) 10 | const readFile = Bluebird.promisify(fs.readFile) 11 | const writeFile = Bluebird.promisify(fs.writeFile) 12 | 13 | const templateFile = 'README_TEMPLATE.md.ejs' 14 | const writeTo = 'README.md' 15 | 16 | function replaceReadme (templatePath: string, outputPath: string, answers: Answers) { 17 | return readFile(templatePath) 18 | .then(function (buffer) { 19 | return buffer.toString() 20 | }) 21 | .then(ejs.compile) 22 | .then(function (template) { 23 | return template(answers) 24 | }) 25 | .then(function (output) { 26 | return writeFile(outputPath, output) 27 | }) 28 | .then(function () { 29 | return rm(templatePath) 30 | }) 31 | } 32 | 33 | function exists (path: string) { 34 | return new Promise(function (resolve, reject) { 35 | fs.exists(path, resolve) 36 | }) 37 | } 38 | 39 | export default function (answers: Answers) { 40 | console.log('Checking README.md') 41 | const templatePath = path.join(process.cwd(), templateFile) 42 | const outputPath = path.join(process.cwd(), writeTo) 43 | return exists(templatePath) 44 | .then(function (fileExists) { 45 | if (fileExists) { 46 | console.log('Generating README.md...') 47 | return replaceReadme(templatePath, outputPath, answers) 48 | .then(function () { 49 | console.log('README.md generated!') 50 | }) 51 | } else { 52 | console.log('Keeping README.md') 53 | } 54 | }) 55 | .then(function () { 56 | return answers 57 | }) 58 | } 59 | -------------------------------------------------------------------------------- /src/getAnswers.ts: -------------------------------------------------------------------------------- 1 | 2 | import { prompt, Question } from 'inquirer' 3 | import * as fs from 'fs' 4 | import { standardizeGitUri } from './updatePackage' 5 | import { dirname } from 'path'; 6 | 7 | function validateDirName (value: string) { 8 | var dirNameRgx = /^[a-z0-9\-_]*$/ 9 | if (!dirNameRgx.test(value)) { 10 | return 'Your directory name can only contain lowercase letters, numbers, dashes, and underscores.' 11 | } else if (fs.existsSync(value)) { 12 | return 'File or directory ' + value + ' already exists.' 13 | } else { 14 | return true 15 | } 16 | } 17 | 18 | function validateNotEmpty (value: string) { 19 | if (value && value.length > 0) { 20 | return true 21 | } else { 22 | return 'Please enter a value.' 23 | } 24 | } 25 | 26 | function defaultDirName (values: Answers) { 27 | var appName = values.appName 28 | var dirName = appName.toLowerCase().replace(/[ _]/g, '-').replace(/[^a-z0-9\-]/g, '') 29 | return dirName 30 | } 31 | 32 | function defaultIdentifier (values: Answers) { 33 | var dirName = values.dirName 34 | var identifier = 'com.github.' + dirName.replace(/-/g, '') 35 | return identifier 36 | } 37 | 38 | export interface GitAnswer { 39 | protocol: string 40 | user: string | null 41 | host: string 42 | repo: string 43 | } 44 | 45 | var sshRgx = /^(?:ssh:\/\/)?([^@]+)@([^:]+):(?:\/)?((?:(?!\.git).)+)(?:\.git)?$/i 46 | var httpRgx = /^(?:https?:\/\/)?([^/]+)\/((?:(?!\.git).)+)(?:\.git)?$/i 47 | 48 | function validateGit (value: GitAnswer | string | undefined) { 49 | var noValue = !value 50 | var isRepo = typeof value === 'object' 51 | if (noValue || isRepo) { 52 | return true 53 | } else { 54 | return 'Not a known git repository; use an ssh or https endpoint.' 55 | } 56 | } 57 | 58 | function filterGit (value: string): GitAnswer | string | undefined { 59 | if (!value) { 60 | return undefined 61 | } 62 | 63 | let protocol, user, host, repo 64 | if (sshRgx.test(value)) { 65 | const matches = sshRgx.exec(value) 66 | protocol = 'ssh' 67 | user = matches && matches[1] 68 | host = matches && matches[2] 69 | repo = matches && matches[3] 70 | } else if (httpRgx.test(value)) { 71 | const matches = httpRgx.exec(value) 72 | protocol = 'https' 73 | user = null 74 | host = matches && matches[1] 75 | repo = matches && matches[2] 76 | } else { 77 | return 'fail' 78 | } 79 | 80 | const output = { 81 | protocol: protocol, 82 | user: user, 83 | host: host, 84 | repo: repo, 85 | toString: (): string => standardizeGitUri(output) 86 | } as GitAnswer 87 | 88 | return output 89 | } 90 | 91 | export interface Answers { 92 | appName: string 93 | dirName: string 94 | identifier: string 95 | description: string 96 | git: GitAnswer 97 | author: string 98 | lisence: string 99 | } 100 | 101 | export default function(): Promise { 102 | return prompt([ 103 | { 104 | name: 'appName', 105 | message: 'app name (the name of the .exe or .app file)', 106 | validate: validateNotEmpty 107 | }, 108 | { 109 | name: 'dirName', 110 | message: 'directory name', 111 | validate: validateDirName, 112 | default: defaultDirName 113 | }, 114 | { 115 | name: 'identifier', 116 | message: 'app identifier', 117 | validate: validateNotEmpty, 118 | default: defaultIdentifier 119 | }, 120 | { 121 | name: 'description', 122 | message: 'description' 123 | }, 124 | { 125 | name: 'git', 126 | message: 'git repository', 127 | validate: validateGit, 128 | filter: filterGit 129 | }, 130 | { 131 | name: 'author', 132 | message: 'author' 133 | }, 134 | { 135 | name: 'lisence', 136 | message: 'lisence', 137 | default: 'ISC' 138 | } 139 | ] as Question[]) as Promise 140 | } 141 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | 2 | import getAnswers, { Answers } from './getAnswers' 3 | import makeDirectory from './makeDirectory' 4 | import cloneBase from './cloneBase' 5 | import updatePackage from './updatePackage' 6 | import generateReadme from './generateReadme' 7 | import installDependencies from './installDependencies' 8 | import deleteGitDir from './deleteGitDir' 9 | import done from './done' 10 | import error from './error' 11 | 12 | const dirName = 'dirName' 13 | 14 | function changeDirectory (answers: Answers) { 15 | process.chdir(answers.dirName) 16 | return answers 17 | } 18 | 19 | export default function () { 20 | return getAnswers() 21 | .then(makeDirectory) 22 | .then(cloneBase) 23 | .then(changeDirectory) 24 | .then(updatePackage) 25 | .then(generateReadme) 26 | .then(installDependencies) 27 | .then(deleteGitDir) 28 | .then(done) 29 | .catch(error) 30 | } 31 | -------------------------------------------------------------------------------- /src/installDependencies.ts: -------------------------------------------------------------------------------- 1 | 2 | import * as npm from 'npm' 3 | import * as Bluebird from 'bluebird' 4 | import { Answers } from './getAnswers' 5 | 6 | function installAllDeps () { 7 | const install = Bluebird.promisify(npm.commands.install) 8 | return install([]) 9 | } 10 | 11 | export default function (answers: Answers) { 12 | const load = Bluebird.promisify(npm.load) 13 | console.log('Installing dependencies...') 14 | return load() 15 | .then(installAllDeps) 16 | .then(function () { 17 | console.log('Dependencies installed!') 18 | return answers 19 | }) 20 | } 21 | -------------------------------------------------------------------------------- /src/makeDirectory.ts: -------------------------------------------------------------------------------- 1 | 2 | import * as fs from 'fs' 3 | import * as Bluebird from 'bluebird' 4 | import { Answers } from './getAnswers' 5 | 6 | const mkidr = Bluebird.promisify(fs.mkdir) 7 | 8 | export default function (answers: Answers) { 9 | return mkidr(answers.dirName) 10 | .then(function () { 11 | return answers 12 | }) 13 | } 14 | -------------------------------------------------------------------------------- /src/spawnHelper.ts: -------------------------------------------------------------------------------- 1 | 2 | import { spawn } from 'child_process' 3 | 4 | export default function (command: string, args: string[]) { 5 | return new Promise(function (resolve, reject) { 6 | function onExit (code: number, signal: string) { 7 | if (code !== null && code !== undefined && code !== 0) { 8 | reject(code) 9 | } else if (signal !== null && signal !== undefined && signal !== 'SIGINT' && signal !== 'SIGTERM') { 10 | reject(signal) 11 | } else { 12 | resolve(0) 13 | } 14 | } 15 | 16 | const child = spawn(command, args, { stdio: 'inherit' }) 17 | child.on('error', reject) 18 | child.on('exit', onExit) 19 | child.on('close', onExit) 20 | }) 21 | } 22 | -------------------------------------------------------------------------------- /src/updatePackage.ts: -------------------------------------------------------------------------------- 1 | 2 | import * as path from 'path' 3 | import * as fs from 'fs' 4 | import * as Bluebird from 'bluebird' 5 | import { Answers, GitAnswer } from './getAnswers' 6 | 7 | const readFile = Bluebird.promisify(fs.readFile) 8 | const writeFile = Bluebird.promisify(fs.writeFile) 9 | const packageFile = 'package.json' 10 | 11 | export function standardizeGitUri (git: GitAnswer): string { 12 | if (git.protocol === 'ssh') { 13 | return 'ssh://' + git.user + '@' + git.host + ':' + git.repo + '.git' 14 | } else if (git.protocol === 'https') { 15 | return 'https://' + git.host + '/' + git.repo 16 | } else { 17 | return '' 18 | } 19 | } 20 | 21 | function parsePackage (jsonBuffer: Buffer) { 22 | return JSON.parse(jsonBuffer.toString()) 23 | } 24 | 25 | function updatePackage (answers: Answers) { 26 | return function (pkg: any) { 27 | delete pkg.authors 28 | 29 | pkg.name = answers.dirName 30 | pkg.appName = answers.appName 31 | pkg.identifier = answers.identifier 32 | pkg.description = answers.description 33 | pkg.author = answers.author 34 | pkg.lisence = answers.lisence 35 | 36 | if (answers.git) { 37 | const git = answers.git 38 | const httpsUri = 'https://' + git.host + '/' + git.repo 39 | const standardizedUri = standardizeGitUri(git) 40 | pkg.repository = { 41 | type: 'git', 42 | url: 'git+' + standardizedUri, 43 | } 44 | pkg.bugs = { 45 | url: httpsUri + '/issues', 46 | } 47 | pkg.homepage = httpsUri + '#readme' 48 | } 49 | 50 | return pkg 51 | } 52 | } 53 | 54 | function writePackage (pkg: any) { 55 | const json = JSON.stringify(pkg) 56 | return writeFile(packageFile, json) 57 | } 58 | 59 | export default function (answers: Answers) { 60 | console.log('Updating package.json...') 61 | return readFile(packageFile) 62 | .then(parsePackage) 63 | .then(updatePackage(answers)) 64 | .then(writePackage) 65 | .then(function () { 66 | console.log('package.json updated!') 67 | return answers 68 | }) 69 | } 70 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": [ 4 | "es2015", 5 | "es2016", 6 | "es2017" 7 | ], 8 | "declaration": true, 9 | "strictNullChecks": true, 10 | "noImplicitAny": true, 11 | "sourceMap": true, 12 | "module": "commonjs", 13 | "moduleResolution": "node", 14 | "target": "es2015", 15 | "baseUrl": "./src" 16 | }, 17 | "include": [ 18 | "./src", 19 | "./index.ts" 20 | ], 21 | "exclude": [ 22 | "./node_modules" 23 | ], 24 | "compileOnSave": false, 25 | "atom": { 26 | "rewriteTsconfig": false 27 | } 28 | } -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tslint-config-standard", 3 | "rules": { 4 | "trailing-comma": { 5 | "options": [{ "multiline": "always", "singleline": "never" }], 6 | "severity": "default" 7 | } 8 | } 9 | } --------------------------------------------------------------------------------