Repository: electron-userland/electron-forge
Pull request #309 (2017-08-26 20:19:32)

BREAKING CHANGE: Replace deprecated publish option
* [x] I have read the [contribution documentation](https://github.com/electron-userland/electron-forge/blob/master/CONTRIBUTING.md) for this project.
* [x] I agree to follow the [code of conduct](https://github.com/electron/electron/blob/master/CODE_OF_CONDUCT.md) that this project follows, as appropriate.
* [x] The changes are appropriately documented (if applicable).
* [x] The changes have sufficient test coverage (if applicable).
* [x] The testsuite passes successfully on my local machine (if applicable).

**Summarize your changes:**

Rename `target` option in the publish API to `publishTargets`, and only allow the param to be an Array of Strings, per the FIXMEs.

Also updated the docs to mention that multiple targets are allowed.

MarshallOfSound: @malept Before this goes in I want to add a breaking change into #300

Patch:
diff --git a/README.md b/README.md
index 0f60d0c5e..c7689ce38 100644
--- a/README.md
+++ b/README.md
@@ -79,6 +79,9 @@ You can also start a project with your
 [favorite framework](https://electronforge.io/templates) with the `--template`
 argument.  E.g. `--template=react`.
 
+If you'd like to have pre-made configuration files for Travis CI and AppVeyor CI to automatically
+build and deploy distributables to GitHub, use the `--copy-ci-files` argument.
+
 ## Importing an existing Project
 
 ```bash
@@ -130,7 +133,9 @@ electron-forge lint
 electron-forge publish
 ```
 
-This will `make` your project and publish any generated artifacts.  By default it will publish to GitHub but you can change the publish target with `--target=YourTarget`.
+This will `make` your project and publish any generated artifacts.  By default it will publish to
+GitHub, but you can change the publish target(s) with `--target=YourTarget,YourTarget2`, where the
+value is a comma-separated list of targets.
 
 # Config
 
diff --git a/src/api/init.js b/src/api/init.js
index df642b10e..6b9db589f 100644
--- a/src/api/init.js
+++ b/src/api/init.js
@@ -15,7 +15,8 @@ const d = debug('electron-forge:init');
  * @typedef {Object} InitOptions
  * @property {string} [dir=process.cwd()] The path to the app to be initialized
  * @property {boolean} [interactive=false] Whether to use sensible defaults or prompt the user visually
- * @property {string} [lintstyle=airbnb] The lintstyle to pass through to the template creator
+ * @property {string} [lintStyle=airbnb] The lintStyle to pass through to the template creator
+ * @property {boolean} [copyCIFiles=false] Whether to copy Travis and AppVeyor CI files
  * @property {string} [template] The custom template to use. If left empty, the default template is used
  */
 
@@ -27,10 +28,11 @@ const d = debug('electron-forge:init');
  */
 export default async (providedOptions = {}) => {
   // eslint-disable-next-line prefer-const, no-unused-vars
-  let { dir, interactive, lintstyle, template } = Object.assign({
+  let { dir, interactive, lintStyle, copyCIFiles, template } = Object.assign({
     dir: process.cwd(),
     interactive: false,
-    lintstyle: 'airbnb',
+    lintStyle: 'airbnb',
+    copyCIFiles: false,
     template: null,
   }, providedOptions);
   asyncOra.interactive = interactive;
@@ -38,22 +40,22 @@ export default async (providedOptions = {}) => {
   d(`Initializing in: ${dir}`);
 
   if (!template) {
-    lintstyle = lintstyle.toLowerCase();
-    if (!['airbnb', 'standard'].includes(lintstyle)) {
-      d(`Unrecognized lintstyle argument: '${lintstyle}' -- defaulting to 'airbnb'`);
-      lintstyle = 'airbnb';
+    lintStyle = lintStyle.toLowerCase();
+    if (!['airbnb', 'standard'].includes(lintStyle)) {
+      d(`Unrecognized lintStyle argument: '${lintStyle}' -- defaulting to 'airbnb'`);
+      lintStyle = 'airbnb';
     }
   }
 
   await initDirectory(dir, interactive);
   await initGit(dir);
-  await initStarter(dir, template ? undefined : lintstyle);
-  await initNPM(dir, template ? undefined : lintstyle);
+  await initStarter(dir, { lintStyle: template ? undefined : lintStyle, copyCIFiles });
+  await initNPM(dir, template ? undefined : lintStyle);
   if (!template) {
-    if (lintstyle === 'standard') {
+    if (lintStyle === 'standard') {
       await initStandardFix(dir);
     }
   } else {
-    await initCustom(dir, template, lintstyle);
+    await initCustom(dir, template, lintStyle);
   }
 };
diff --git a/src/api/publish.js b/src/api/publish.js
index c0fd35710..3968a5d68 100644
--- a/src/api/publish.js
+++ b/src/api/publish.js
@@ -2,7 +2,6 @@ import 'colors';
 import path from 'path';
 
 import asyncOra from '../util/ora-handler';
-import deprecate from '../util/deprecate';
 import getForgeConfig from '../util/forge-config';
 import readPackageJSON from '../util/read-package-json';
 import requireSearch from '../util/require-search';
@@ -16,7 +15,7 @@ import make from './make';
  * @property {boolean} [interactive=false] Whether to use sensible defaults or prompt the user visually
  * @property {string} [authToken] An authentication token to use when publishing
  * @property {string} [tag=packageJSON.version] The string to tag this release with
- * @property {string} [target=github] The publish target
+ * @property {Array<string>} [publishTargets=[github]] The publish targets
  * @property {MakeOptions} [makeOptions] Options object to passed through to make()
  */
 
@@ -28,16 +27,14 @@ import make from './make';
  */
 export default async (providedOptions = {}) => {
   // eslint-disable-next-line prefer-const, no-unused-vars
-  let { dir, interactive, authToken, tag, target, makeOptions } = Object.assign({
+  let { dir, interactive, authToken, tag, publishTargets, makeOptions } = Object.assign({
     dir: process.cwd(),
     interactive: false,
     tag: null,
     makeOptions: {},
-    target: null,
+    publishTargets: null,
   }, providedOptions);
   asyncOra.interactive = interactive;
-  // FIXME(MarshallOfSound): Change the method param to publishTargets in the next major bump
-  let publishTargets = target;
 
   const makeResults = await make(Object.assign({
     dir,
@@ -60,10 +57,6 @@ export default async (providedOptions = {}) => {
 
   if (publishTargets === null) {
     publishTargets = forgeConfig.publish_targets[makeOptions.platform || process.platform];
-  } else if (typeof publishTargets === 'string') {
-    // FIXME(MarshallOfSound): Remove this fallback string typeof check in the next major bump
-    deprecate('publish target as a string').replaceWith('an array of publish targets');
-    publishTargets = [publishTargets];
   }
 
   for (const publishTarget of publishTargets) {
diff --git a/src/electron-forge-init.js b/src/electron-forge-init.js
index d7d353a5b..a8143bf86 100644
--- a/src/electron-forge-init.js
+++ b/src/electron-forge-init.js
@@ -10,7 +10,8 @@ import { init } from './api';
     .version(require('../package.json').version)
     .arguments('[name]')
     .option('-t, --template [name]', 'Name of the forge template to use')
-    .option('-l, --lintstyle [style]', 'Linting standard to follow.  For the default template it can be "airbnb" or "standard"', 'airbnb')
+    .option('-l, --lintStyle [style]', 'Linting standard to follow.  For the default template it can be "airbnb" or "standard"', 'airbnb')
+    .option('-c, --copy-ci-files', 'Whether to copy the templated CI files (defaults to false)', false)
     .action((name) => {
       if (!name) return;
       if (path.isAbsolute(name)) {
@@ -24,7 +25,8 @@ import { init } from './api';
   const initOpts = {
     dir,
     interactive: true,
-    lintstyle: program.lintstyle,
+    lintStyle: program.lintStyle,
+    copyCIFiles: !!program.copyCiFiles,
   };
   if (program.template) initOpts.template = program.template;
 
diff --git a/src/electron-forge-publish.js b/src/electron-forge-publish.js
index 96f6935a5..2dd702770 100644
--- a/src/electron-forge-publish.js
+++ b/src/electron-forge-publish.js
@@ -13,7 +13,7 @@ import { getMakeOptions } from './electron-forge-make';
     .arguments('[cwd]')
     .option('--auth-token', 'Authorization token for your publisher target (if required)')
     .option('--tag', 'The tag to publish to on GitHub')
-    .option('--target [target]', 'The deployment target, defaults to "github"')
+    .option('--target [target[,target...]]', 'The comma-separated deployment targets, defaults to "github"')
     .allowUnknownOption(true)
     .action((cwd) => {
       if (!cwd) return;
@@ -31,7 +31,7 @@ import { getMakeOptions } from './electron-forge-make';
     authToken: program.authToken,
     tag: program.tag,
   };
-  if (program.target) publishOpts.target = program.target.split(',');
+  if (program.target) publishOpts.publishTargets = program.target.split(',');
 
   publishOpts.makeOptions = getMakeOptions();
 
diff --git a/src/init/init-starter-files.js b/src/init/init-starter-files.js
index 3bd4d88f4..26240d984 100644
--- a/src/init/init-starter-files.js
+++ b/src/init/init-starter-files.js
@@ -11,13 +11,14 @@ export const copy = async (source, target) => {
   await fs.copy(source, target);
 };
 
-export default async (dir, lintStyle) => {
+export default async (dir, { lintStyle, copyCIFiles }) => {
   await asyncOra('Copying Starter Files', async () => {
     const tmplPath = path.resolve(__dirname, '../../tmpl');
 
     d('creating directory:', path.resolve(dir, 'src'));
     await fs.mkdirs(path.resolve(dir, 'src'));
     const rootFiles = ['_gitignore', '_compilerc'];
+    if (copyCIFiles) rootFiles.push(...['_travis.yml', '_appveyor.yml']);
     if (lintStyle === 'airbnb') rootFiles.push('_eslintrc');
     const srcFiles = ['index.js', 'index.html'];
 
diff --git a/tmpl/_appveyor.yml b/tmpl/_appveyor.yml
new file mode 100644
index 000000000..728114826
--- /dev/null
+++ b/tmpl/_appveyor.yml
@@ -0,0 +1,26 @@
+platform:
+- x64
+environment:
+  nodejs_version: "6"
+cache:
+- '%APPDATA%\npm-cache'
+- '%USERPROFILE%\.electron'
+- node_modules
+branches:
+  only:
+  - master
+  - /^v\d+\.\d+\.\d+/
+install:
+- ps: Install-Product node $env:nodejs_version $env:platform
+- set PATH=%APPDATA%\npm;%PATH%
+- npm install
+- npm update
+
+test_script:
+- node --version
+- npm --version
+- npm run lint
+- if %APPVEYOR_REPO_TAG% EQU false npm run make
+
+build_script:
+- IF %APPVEYOR_REPO_TAG% EQU true npm run publish
diff --git a/tmpl/_travis.yml b/tmpl/_travis.yml
new file mode 100644
index 000000000..02fa627e3
--- /dev/null
+++ b/tmpl/_travis.yml
@@ -0,0 +1,33 @@
+language: node_js
+node_js: "6"
+os:
+- linux
+- osx
+dist: trusty
+osx_image: xcode8.3
+sudo: false
+
+cache:
+  directories:
+  - node_modules
+  - $HOME/.cache/electron
+
+addons:
+  apt:
+    packages:
+    - fakeroot
+    - rpm
+
+branches:
+  only:
+  - master
+  - /^v\d+\.\d+\.\d+/
+
+install:
+- npm install
+- npm update
+
+script:
+- npm run lint
+- if test -z "$TRAVIS_TAG"; then npm run make; fi
+after_success: if test -n "$TRAVIS_TAG"; then npm run publish; fi
diff --git a/tmpl/package.json b/tmpl/package.json
index 28e9a744c..9ac204578 100644
--- a/tmpl/package.json
+++ b/tmpl/package.json
@@ -7,7 +7,8 @@
   "scripts": {
     "start": "electron-forge start",
     "package": "electron-forge package",
-    "make": "electron-forge make"
+    "make": "electron-forge make",
+    "publish": "electron-forge publish"
   },
   "keywords": [],
   "author": "",


Test patch:
diff --git a/test/fast/publish_spec.js b/test/fast/publish_spec.js
index c1a15d0fb..504b0119f 100644
--- a/test/fast/publish_spec.js
+++ b/test/fast/publish_spec.js
@@ -72,7 +72,7 @@ describe('publish', () => {
     await publish({
       dir: __dirname,
       interactive: false,
-      target: 'void',
+      publishTargets: ['void'],
     });
     expect(requireSearchStub.firstCall.args[1][0]).to.equal('../publishers/void.js');
   });
@@ -81,7 +81,7 @@ describe('publish', () => {
     await publish({
       dir: __dirname,
       interactive: false,
-      target: ['void', 'nowhere', 'black_hole', 'everywhere'],
+      publishTargets: ['void', 'nowhere', 'black_hole', 'everywhere'],
     });
     expect(requireSearchStub.getCall(0).args[1][0]).to.equal('../publishers/void.js');
     expect(requireSearchStub.getCall(1).args[1][0]).to.equal('../publishers/nowhere.js');
diff --git a/test/slow/api_spec_slow.js b/test/slow/api_spec_slow.js
index 9a2a27f8d..a1cbfc04d 100644
--- a/test/slow/api_spec_slow.js
+++ b/test/slow/api_spec_slow.js
@@ -17,38 +17,51 @@ const forge = proxyquire.noCallThru().load('../../src/api', {
 });
 
 describe(`electron-forge API (with installer=${installer.substr(12)})`, () => {
+  let dir;
   let dirID = Date.now();
+
+  const ensureTestDirIsNonexistent = async () => {
+    dir = path.resolve(os.tmpdir(), `electron-forge-test-${dirID}`);
+    dirID += 1;
+    await fs.remove(dir);
+  };
+
+  const beforeInitTest = (params, beforeInit) => {
+    before(async () => {
+      await ensureTestDirIsNonexistent();
+      if (beforeInit) {
+        beforeInit();
+      }
+      await forge.init(Object.assign({}, params, { dir }));
+    });
+  };
+
+  const expectProjectPathExists = async (subPath, pathType) => {
+    expect(await fs.pathExists(path.resolve(dir, subPath)), `the ${subPath} ${pathType} should exist`).to.equal(true);
+  };
+
   const forLintingMethod = (lintStyle) => {
     describe(`init (with lintStyle=${lintStyle})`, () => {
-      let dir;
-
-      before(async () => {
-        dir = path.resolve(os.tmpdir(), `electron-forge-test-${dirID}`);
-        dirID += 1;
-        await fs.remove(dir);
-        await forge.init({
-          dir,
-          lintstyle: lintStyle,
-        });
-      });
+      beforeInitTest({ lintStyle });
 
       it('should create a new folder with a npm module inside', async () => {
         expect(await fs.pathExists(dir), 'the target dir should have been created').to.equal(true);
-        expect(await fs.pathExists(path.resolve(dir, 'package.json')), 'the package.json file should exist').to.equal(true);
+        expectProjectPathExists('package.json', 'file');
       });
 
       it('should have initialized a git repository', async () => {
-        expect(await fs.pathExists(path.resolve(dir, '.git')), 'the .git folder should exist').to.equal(true);
+        expectProjectPathExists('.git', 'folder');
       });
 
       it('should have installed the initial node_modules', async () => {
-        expect(await fs.pathExists(path.resolve(dir, 'node_modules')), 'node_modules folder should exist').to.equal(true);
+        expectProjectPathExists('node_modules', 'folder');
         expect(await fs.pathExists(path.resolve(dir, 'node_modules/electron-prebuilt-compile')), 'electron-prebuilt-compile should exist').to.equal(true);
         expect(await fs.pathExists(path.resolve(dir, 'node_modules/babel-core')), 'babel-core should exist').to.equal(true);
         expect(await fs.pathExists(path.resolve(dir, 'node_modules/electron-forge')), 'electron-forge should exist').to.equal(true);
       });
 
       it('should have set the .compilerc electron version to be a float', async () => {
+        expectProjectPathExists('.compilerc', 'file');
         const compilerc = JSON.parse(await fs.readFile(path.resolve(dir, '.compilerc')));
         expect(compilerc.env.development['application/javascript'].presets[0][1].targets.electron).to.be.a('number');
       });
@@ -63,29 +76,30 @@ describe(`electron-forge API (with installer=${installer.substr(12)})`, () => {
   forLintingMethod('airbnb');
   forLintingMethod('standard');
 
-  describe('init (with custom templater)', () => {
-    let dir;
+  describe('init with CI files enabled', () => {
+    beforeInitTest({ copyCIFiles: true });
 
-    before(async () => {
-      dir = path.resolve(os.tmpdir(), `electron-forge-test-${dirID}`);
-      dirID += 1;
-      await fs.remove(dir);
+    it('should copy over the CI config files correctly', async () => {
+      expect(await fs.pathExists(dir), 'the target dir should have been created').to.equal(true);
+      expectProjectPathExists('.appveyor.yml', 'file');
+      expectProjectPathExists('.travis.yml', 'file');
+    });
+  });
+
+  describe('init (with custom templater)', () => {
+    beforeInitTest({ template: 'dummy' }, () => {
       execSync('npm link', {
         cwd: path.resolve(__dirname, '../fixture/custom_init'),
       });
-      await forge.init({
-        dir,
-        template: 'dummy',
-      });
     });
 
     it('should create dot files correctly', async () => {
       expect(await fs.pathExists(dir), 'the target dir should have been created').to.equal(true);
-      expect(await fs.pathExists(path.resolve(dir, '.bar')), 'the .bar file should exist').to.equal(true);
+      expectProjectPathExists('.bar', 'file');
     });
 
     it('should create deep files correctly', async () => {
-      expect(await fs.pathExists(path.resolve(dir, 'src/foo.js')), 'the src/foo.js file should exist').to.equal(true);
+      expectProjectPathExists('src/foo.js', 'file');
     });
 
     describe('lint', () => {
@@ -101,13 +115,7 @@ describe(`electron-forge API (with installer=${installer.substr(12)})`, () => {
   });
 
   describe('init (with built-in templater)', () => {
-    let dir;
-
-    before(async () => {
-      dir = path.resolve(os.tmpdir(), `electron-forge-test-${dirID}`);
-      dirID += 1;
-      await fs.remove(dir);
-    });
+    before(ensureTestDirIsNonexistent);
 
     it('should succeed in initializing', async () => {
       await forge.init({
@@ -133,13 +141,7 @@ describe(`electron-forge API (with installer=${installer.substr(12)})`, () => {
   });
 
   describe('init (with a nonexistent templater)', () => {
-    let dir;
-
-    before(async () => {
-      dir = path.resolve(os.tmpdir(), `electron-forge-test-${dirID}`);
-      dirID += 1;
-      await fs.remove(dir);
-    });
+    before(ensureTestDirIsNonexistent);
 
     it('should fail in initializing', async () => {
       await expect(forge.init({
@@ -154,8 +156,6 @@ describe(`electron-forge API (with installer=${installer.substr(12)})`, () => {
   });
 
   describe('after init', () => {
-    let dir;
-
     before(async () => {
       dir = path.resolve(os.tmpdir(), `electron-forge-test-${dirID}/electron-forge-test`);
       dirID += 1;