Repository: electron-userland/electron-forge
Pull request #406 (2018-01-08 03:25:21)

feat(generic): allow specifying a build identifier that segregates build artifacts
<!--
Thanks for filing a pull request!
Please check off all of the steps as they are completed by replacing [ ] with [x].
-->

* [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:**

Allows you to specify either a build identifier or a function that fetches a build identifier that will be used to uniquely identify a set of builds.  This allows you to build multiple "channels" for your app in the same folder without overwriting each other.

MarshallOfSound: Docs will be put up on the site 👍 

Patch:
diff --git a/src/api/make.js b/src/api/make.js
index 05ad4411d..e53e61589 100644
--- a/src/api/make.js
+++ b/src/api/make.js
@@ -11,6 +11,7 @@ import parseArchs from '../util/parse-archs';
 import readPackageJSON from '../util/read-package-json';
 import { requireSearchRaw } from '../util/require-search';
 import resolveDir from '../util/resolve-dir';
+import getCurrentOutDir from '../util/out-dir';
 
 import packager from './package';
 
@@ -49,7 +50,6 @@ export default async (providedOptions = {}) => {
     platform: process.platform,
   }, providedOptions);
 
-  const outDir = providedOptions.outDir || path.resolve(dir, 'out');
   asyncOra.interactive = interactive;
 
   let forgeConfig;
@@ -62,6 +62,8 @@ export default async (providedOptions = {}) => {
     forgeConfig = await getForgeConfig(dir);
   });
 
+  const outDir = providedOptions.outDir || getCurrentOutDir(dir, forgeConfig);
+
   const actualTargetPlatform = platform;
   platform = platform === 'mas' ? 'darwin' : platform;
   if (!['darwin', 'win32', 'linux', 'mas'].includes(actualTargetPlatform)) {
diff --git a/src/api/package.js b/src/api/package.js
index d6b68832b..2d57f589a 100644
--- a/src/api/package.js
+++ b/src/api/package.js
@@ -16,6 +16,7 @@ import readPackageJSON from '../util/read-package-json';
 import rebuildHook from '../util/rebuild';
 import requireSearch from '../util/require-search';
 import resolveDir from '../util/resolve-dir';
+import getCurrentOutDir from '../util/out-dir';
 
 const d = debug('electron-forge:packager');
 
@@ -67,7 +68,6 @@ export default async (providedOptions = {}) => {
 
   const ora = interactive ? realOra : fakeOra;
 
-  const outDir = providedOptions.outDir || path.resolve(dir, 'out');
   let prepareSpinner = ora(`Preparing to Package Application for arch: ${(arch === 'all' ? 'ia32' : arch).cyan}`).start();
   let prepareCounter = 0;
 
@@ -84,6 +84,7 @@ export default async (providedOptions = {}) => {
   }
 
   const forgeConfig = await getForgeConfig(dir);
+  const outDir = providedOptions.outDir || getCurrentOutDir(dir, forgeConfig);
   let packagerSpinner;
 
   const pruneEnabled = !('prune' in forgeConfig.electronPackagerConfig) || forgeConfig.electronPackagerConfig.prune;
diff --git a/src/api/publish.js b/src/api/publish.js
index f86d19d92..877ad789e 100644
--- a/src/api/publish.js
+++ b/src/api/publish.js
@@ -9,6 +9,7 @@ import readPackageJSON from '../util/read-package-json';
 import requireSearch from '../util/require-search';
 import resolveDir from '../util/resolve-dir';
 import PublishState from '../util/publish-state';
+import getCurrentOutDir from '../util/out-dir';
 
 import make from './make';
 
@@ -48,9 +49,6 @@ const publish = async (providedOptions = {}) => {
   }, providedOptions);
   asyncOra.interactive = interactive;
 
-  const outDir = providedOptions.outDir || path.resolve(dir, 'out');
-  const dryRunDir = path.resolve(outDir, 'publish-dry-run');
-
   if (dryRun && dryRunResume) {
     throw 'Can\'t dry run and resume a dry run at the same time';
   }
@@ -61,6 +59,8 @@ const publish = async (providedOptions = {}) => {
   let packageJSON = await readPackageJSON(dir);
 
   const forgeConfig = await getForgeConfig(dir);
+  const outDir = providedOptions.outDir || getCurrentOutDir(dir, forgeConfig);
+  const dryRunDir = path.resolve(outDir, 'publish-dry-run');
 
   if (dryRunResume) {
     d('attempting to resume from dry run');
diff --git a/src/util/out-dir.js b/src/util/out-dir.js
new file mode 100644
index 000000000..372395d8a
--- /dev/null
+++ b/src/util/out-dir.js
@@ -0,0 +1,14 @@
+const path = require('path');
+
+const BASE_OUT_DIR = 'out';
+
+export default (baseDir, forgeConfig) => {
+  if (forgeConfig.buildIdentifier) {
+    let identifier = forgeConfig.buildIdentifier;
+    if (typeof identifier === 'function') {
+      identifier = identifier();
+    }
+    if (identifier) return path.resolve(baseDir, BASE_OUT_DIR, identifier);
+  }
+  return path.resolve(baseDir, BASE_OUT_DIR);
+};


Test patch:
diff --git a/test/fast/out-dir_spec.js b/test/fast/out-dir_spec.js
new file mode 100644
index 000000000..eaf61256e
--- /dev/null
+++ b/test/fast/out-dir_spec.js
@@ -0,0 +1,26 @@
+import { expect } from 'chai';
+import path from 'path';
+
+import getCurrentOutDir from '../../src/util/out-dir';
+
+describe('out-dir', () => {
+  const DIR = __dirname;
+
+  describe('getCurrentOutDir', () => {
+    it('resolves to the default out directory when nothing extra is declared', () => {
+      expect(getCurrentOutDir(DIR, {})).to.equal(`${DIR}${path.sep}out`);
+    });
+
+    it('resolves to the provided identifier', () => {
+      expect(getCurrentOutDir(DIR, {
+        buildIdentifier: 'bar',
+      })).to.equal(`${DIR}${path.sep}out${path.sep}bar`);
+    });
+
+    it('resolves to the return value of provided identifier getter', () => {
+      expect(getCurrentOutDir(DIR, {
+        buildIdentifier: () => 'thing',
+      })).to.equal(`${DIR}${path.sep}out${path.sep}thing`);
+    });
+  });
+});