├── .editorconfig ├── .gitignore ├── LICENSE ├── README.md ├── bin └── itchy.js ├── commands ├── build.js ├── index.js ├── publish.js └── utils.js └── package.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | *.sublime-workspace 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright © 2016 erbridge 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Itchy Electron 2 | 3 | > Package your [Electron](http://electron.atom.io/) games for [itch.io](https://itch.io/) 4 | 5 | 6 | ## About 7 | 8 | This is a CLI app for building and publishing games and tools built using [Electron](http://electron.atom.io/) to [itch.io](https://itch.io/). It aims to handle everything from packaging your Electron app into distributables, to publishing it on itch.io. It wraps other tools into a single unified interface, with sensible defaults and simple commands that can easily be called manually or added as `package.json` scripts. 9 | 10 | Bug reports, feature requests, and pull requests are welcome! 11 | 12 | 13 | ## Installation 14 | 15 | Install into a project using `npm`: 16 | 17 | ``` 18 | $ npm install --save-dev itchy-electron 19 | ``` 20 | 21 | or globally: 22 | 23 | ``` 24 | $ npm install -g itchy-electron 25 | ``` 26 | 27 | Publishing also relies on [`butler`](https://github.com/itchio/butler), which needs to be manually installed. 28 | 29 | 30 | ## Usage 31 | 32 | Once installed, *Itchy Electron* is used through the CLI tool `itchy`. 33 | 34 | Refer to the help for an up to date command reference: 35 | 36 | ``` 37 | $ itchy help 38 | ``` 39 | 40 | 41 | ### Configuration 42 | 43 | *Itchy Electron* uses configuration files over command line arguments. To configure it, either add an object to your `package.json` called `itchyElectron`, or create a JavaScript or JSON file called `.itchyelectronrc`, `.itchyelectron.js`, or `.itchyelectron.json` (whatever takes your preference). 44 | 45 | The only "required" options are `electronVersion` (which is inherited from the `package.json` if possible - see below for more details) and `itchTargets`. 46 | 47 | A minimal `package.json` configuration looks like this: 48 | 49 | ```json 50 | { 51 | "name": "example", 52 | "version": "0.1.0", 53 | "itchyElectron": { 54 | "productName": "Example", 55 | "appDir": "./app", 56 | "itchTargets": { 57 | "release": "erbridge/example" 58 | } 59 | }, 60 | "devDependencies": { 61 | "electron-prebuilt": "1.0.2", 62 | "itchy-electron": "^0.1.0" 63 | } 64 | } 65 | ``` 66 | 67 | or `.itchyelectron.json`: 68 | 69 | ```json 70 | { 71 | "productName": "Example", 72 | "appVersion": "0.1.0", 73 | "electronVersion": "1.0.2", 74 | "itchTargets": { 75 | "beta": "erbridge/example-beta", 76 | "release": "erbridge/example" 77 | } 78 | } 79 | ``` 80 | 81 | 82 | #### Options 83 | 84 | ##### `appDir` 85 | 86 | The app source directory. Defaults to the current directory. 87 | 88 | 89 | ##### `appVersion` 90 | 91 | The release version of the application. Maps to the `ProductVersion` metadata property on Windows, and `CFBundleShortVersionString` on OS X. Defaults to the `version` from the `package.json`. 92 | 93 | 94 | ##### `buildDir` 95 | 96 | The directory to save builds into. Defaults to `./build`. 97 | 98 | 99 | ##### `buildVersion` 100 | 101 | The build version of the application. Maps to the `FileVersion` metadata property on Windows, and `CFBundleVersion` on OS X. Defaults to `appVersion`. 102 | 103 | 104 | ##### `electronVersion` 105 | 106 | The version of Electron to build against. If omitted, the pinned version in the `package.json` dependencies will be used. If there is no locally pinned version number, the build will fail. 107 | 108 | Accepted packages are: 109 | 110 | - `electron` 111 | - `electron-prebuilt` 112 | - `electron-prebuilt-compile` 113 | 114 | 115 | ##### `itchTargets` 116 | 117 | An object of key value pairs of target to project, used for publishing. 118 | 119 | 120 | ##### `productName` 121 | 122 | The application name. If omitted, `name` from the `package.json` will be used instead. If no name is present, it will default to `untitled`. 123 | 124 | 125 | ### Project Structure 126 | 127 | It is suggested that you structure your app in the following way, so as to minimize the overheads caused by packaging the `devDependencies`. 128 | 129 | ``` 130 | project/ 131 | 132 | app/ 133 | The entire app source is contained within here. 134 | 135 | package.json 136 | A minimal package.json containing a reference to the 137 | app entry point in "main" along with the runtime 138 | dependencies in "dependencies". Other values are optional. 139 | 140 | build/ 141 | 142 | dist/ 143 | 144 | package.json 145 | A more complete package.json with the development dependencies 146 | and other values. This is where the "itchyElectron" 147 | configuration belongs, if using the package.json option. 148 | ``` 149 | 150 | Setting `appDir` to `./app` in this case will enable building packages with only the runtime dependencies. It also has the bonus effect of excluding the various configuration files often found in a project's root. 151 | -------------------------------------------------------------------------------- /bin/itchy.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | 'use strict'; 4 | 5 | const path = require('path'); 6 | 7 | const chalk = require('chalk'); 8 | const Liftoff = require('liftoff'); 9 | const meow = require('meow'); 10 | const shell = require('shelljs'); 11 | const updateNotifier = require('update-notifier'); 12 | const winston = require('winston'); 13 | 14 | const commands = require('../commands'); 15 | const pkg = require('../package'); 16 | 17 | updateNotifier({ pkg }).notify(); 18 | 19 | winston.cli(); 20 | 21 | const cli = meow( 22 | ` 23 | Usage: 24 | 25 | ${chalk.cyan('itchy')} ${chalk.green('')} [${chalk.magenta('')}] [${chalk.yellow('')}] 26 | 27 | Commands: 28 | 29 | ${chalk.green('build')} ${chalk.magenta('')} Create a build for the target operating system 30 | 31 | ${chalk.magenta('')} Valid targets are ${chalk.bold('linux')}, ${chalk.bold('osx')}, or ${chalk.bold('win32')} 32 | 33 | ${chalk.green('help')} Display this help message 34 | 35 | ${chalk.green('publish')} ${chalk.magenta('')} ${chalk.magenta('')} Publish a build to itch.io 36 | 37 | ${chalk.magenta('')} One of the targets specified in the config 38 | ${chalk.magenta('')} Valid targets are ${chalk.bold('linux')}, ${chalk.bold('osx')}, or ${chalk.bold('win32')} 39 | 40 | Options: 41 | 42 | ${chalk.yellow('--config=')} Specify the config file to use 43 | ${chalk.yellow('--cwd=')} Specify the working directory 44 | ${chalk.yellow('--help')} Display this help message 45 | `, 46 | {} 47 | ); 48 | 49 | const ItchyElectron = new Liftoff({ 50 | moduleName: 'itchy-electron', 51 | configName: '.itchyelectron', 52 | processTitle: 'itchy-electron', 53 | extensions: { 54 | rc: null, 55 | '.json': null, 56 | '.js': null, 57 | }, 58 | }); 59 | 60 | ItchyElectron.launch( 61 | { 62 | cwd: cli.flags.cwd, 63 | configPath: cli.flags.config, 64 | completion: cli.flags.completion, 65 | }, 66 | function invoke(env) { 67 | if (shell.pwd() !== env.cwd) { 68 | shell.cd(env.cwd); 69 | } 70 | 71 | let appPackage; 72 | let config; 73 | 74 | try { 75 | appPackage = require(path.join(env.cwd, 'package')); // eslint-disable-line global-require 76 | } catch (err) { 77 | appPackage = {}; 78 | } 79 | 80 | if (env.configPath) { 81 | config = require(env.configPath); // eslint-disable-line global-require 82 | } else { 83 | config = appPackage.itchyElectron || {}; 84 | } 85 | 86 | try { 87 | switch (cli.input[0]) { 88 | case 'build': 89 | commands.build(cli.input[1], config, appPackage); 90 | break; 91 | case 'help': 92 | cli.showHelp(); 93 | break; 94 | case 'publish': 95 | commands.publish(cli.input[1], cli.input[2], config, appPackage); 96 | break; 97 | default: 98 | throw new Error('Unrecognized command'); 99 | } 100 | } catch (e) { 101 | // TODO: Show the help. 102 | winston.error(e.message); 103 | 104 | cli.showHelp(1); 105 | } 106 | } 107 | ); 108 | -------------------------------------------------------------------------------- /commands/build.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const chalk = require('chalk'); 4 | const packager = require('electron-packager'); 5 | const semverRegex = require('semver-regex'); 6 | const winston = require('winston'); 7 | 8 | const utils = require('./utils'); 9 | 10 | const getElectronVersion = function getElectronVersion(config, appPackage) { 11 | let electronVersion = config.electronVersion; 12 | 13 | if (!electronVersion) { 14 | const devDependencies = appPackage.devDependencies || {}; 15 | const dependencies = appPackage.dependencies || {}; 16 | 17 | const electronPackageNames = [ 18 | 'electron', 19 | 'electron-prebuilt', 20 | 'electron-prebuilt-compile', 21 | ]; 22 | 23 | electronPackageNames.find(function getVersion(name) { 24 | electronVersion = devDependencies[name]; 25 | 26 | if (!electronVersion) { 27 | electronVersion = dependencies[name]; 28 | } 29 | 30 | return electronVersion; 31 | }); 32 | 33 | if (electronVersion) { 34 | const v = semverRegex().exec(electronVersion); 35 | 36 | if (v) { 37 | electronVersion = v[0]; 38 | } 39 | } 40 | } 41 | 42 | return electronVersion; 43 | }; 44 | 45 | const getAppDir = function getAppDir(config) { 46 | return config.appDir || '.'; 47 | }; 48 | 49 | module.exports = function build(buildTarget, config, appPackage) { 50 | const electronVersion = getElectronVersion(config, appPackage); 51 | 52 | if (!electronVersion) { 53 | throw new Error('The electron version to use is missing'); 54 | } 55 | 56 | if (!buildTarget) { 57 | throw new Error('A build target must be specified'); 58 | } 59 | 60 | const buildTargetConfig = utils.getBuildTargetConfig(buildTarget); 61 | 62 | const packagerConfig = { 63 | dir: getAppDir(config), 64 | platform: buildTargetConfig.platform, 65 | arch: buildTargetConfig.arch, 66 | 67 | name: utils.getProductName(config, appPackage), 68 | 'app-version': utils.getAppVersion(config, appPackage), 69 | 'build-version': utils.getBuildVersion(config, appPackage), 70 | 71 | out: utils.getBuildDir(config), 72 | 73 | // TODO: Make these command flags. 74 | asar: true, 75 | overwrite: true, 76 | prune: true, 77 | }; 78 | 79 | packager( 80 | packagerConfig, 81 | function done(err, appPaths) { 82 | if (err) { 83 | throw err; 84 | } 85 | 86 | if (appPaths.length) { 87 | winston.info('Built the following packages:'); 88 | 89 | appPaths.forEach(function log(appPath) { 90 | winston.info('\t', chalk.yellow(appPath)); 91 | }); 92 | } else { 93 | winston.info('Build complete'); 94 | } 95 | } 96 | ); 97 | }; 98 | -------------------------------------------------------------------------------- /commands/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const build = require('./build'); 4 | const publish = require('./publish'); 5 | 6 | module.exports = { 7 | build, 8 | publish, 9 | }; 10 | -------------------------------------------------------------------------------- /commands/publish.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | 5 | const shell = require('shelljs'); 6 | 7 | const utils = require('./utils'); 8 | 9 | const getTarget = function getTarget(targetName, buildName, config) { 10 | if (!config.itchTargets) { 11 | throw new Error('No targets configured'); 12 | } 13 | 14 | const buildConfig = utils.getBuildTargetConfig(buildName); 15 | 16 | const target = { 17 | project: config.itchTargets[targetName], 18 | channel: `${buildName}-${buildConfig.arch}`, 19 | }; 20 | 21 | if (!target.project) { 22 | throw new Error(`Undefined target: ${targetName}`); 23 | } 24 | 25 | return target; 26 | }; 27 | 28 | const getBuildPath = function getBuildPath(buildName, config, appPackage) { 29 | const buildDir = utils.getBuildDir(config); 30 | const name = utils.getProductName(config, appPackage); 31 | const buildConfig = utils.getBuildTargetConfig(buildName); 32 | const build = `${name}-${buildConfig.platform}-${buildConfig.arch}`; 33 | 34 | return path.join(buildDir, build); 35 | }; 36 | 37 | module.exports = function publish(targetName, buildName, config, appPackage) { 38 | if (!shell.which('butler')) { 39 | throw new Error('butler needs to be installed and on the path'); 40 | } 41 | 42 | let command = 'butler push --fix-permissions'; 43 | 44 | const version = utils.getBuildVersion(config, appPackage); 45 | 46 | if (version) { 47 | command = `${command} --userversion=${version}`; 48 | } 49 | 50 | const target = getTarget(targetName, buildName, config); 51 | const buildPath = getBuildPath(buildName, config, appPackage); 52 | 53 | command = `${command} "${buildPath}" ${target.project}:${target.channel}`; 54 | 55 | shell.exec(command, { async: true }); 56 | }; 57 | -------------------------------------------------------------------------------- /commands/utils.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | getProductName(config, appPackage) { 5 | return config.productName || appPackage.name || 'untitled'; 6 | }, 7 | 8 | getAppVersion(config, appPackage) { 9 | return config.appVersion || appPackage.version; 10 | }, 11 | 12 | getBuildVersion(config, appPackage) { 13 | return config.buildVersion || this.getAppVersion(config, appPackage); 14 | }, 15 | 16 | getBuildDir(config) { 17 | return config.buildDir || './build'; 18 | }, 19 | 20 | getBuildTargetConfig(buildTarget) { 21 | const buildTargets = { 22 | linux: { 23 | platform: 'linux', 24 | arch: 'x64', 25 | }, 26 | osx: { 27 | platform: 'darwin', 28 | arch: 'x64', 29 | }, 30 | win32: { 31 | platform: 'win32', 32 | arch: 'ia32', 33 | }, 34 | }; 35 | 36 | return buildTargets[buildTarget]; 37 | }, 38 | }; 39 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "itchy-electron", 3 | "version": "0.2.1", 4 | "description": "Package your Electron games for itch.io", 5 | "repository": "erbridge/itchy-electron", 6 | "author": "erbridge (https://erbridge.co.uk)", 7 | "license": "MIT", 8 | "bin": { 9 | "itchy": "./bin/itchy.js" 10 | }, 11 | "dependencies": { 12 | "chalk": "^1.1.3", 13 | "electron-packager": "^7.0.1", 14 | "liftoff": "^2.2.1", 15 | "meow": "^3.7.0", 16 | "semver-regex": "^1.0.0", 17 | "shelljs": "^0.7.0", 18 | "update-notifier": "^0.7.0", 19 | "winston": "^2.2.0" 20 | } 21 | } 22 | --------------------------------------------------------------------------------