Repository: electron/electron-packager (js)

Issue:
Add an option to use local electron binaries
Right now `electron-packager` can only be used with official releases (dowloaded with `electron-download`). I would be interested in using it with locally built binaries (instead of specifying `version`, we would specify `binary-path`?).


Pull request description:
feat: add electronZipDir option
* [x] I have read the [contribution documentation](https://github.com/electron/electron-packager/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:**

Fixes #187.

Patch:
diff --git a/docs/api.md b/docs/api.md
index 49194a5..6da8f55 100644
--- a/docs/api.md
+++ b/docs/api.md
@@ -228,6 +228,7 @@ parameters include, but are not limited to:
 - `rejectUnauthorized` (*Boolean* - default: `true`): Whether SSL certificates are required to be
   valid when downloading Electron.
 
+**Note:** `download` sub-options will have no effect if [`electronZipDir`](#electronzipdir) is set.
 
 ##### `electronVersion`
 
@@ -239,6 +240,17 @@ valid versions. If omitted, it will use the version of the nearest local install
 `electron`, `electron-prebuilt-compile`, or `electron-prebuilt`, defined in `package.json` in either
 `dependencies` or `devDependencies`.
 
+##### `electronZipDir`
+
+*String*
+
+The local path to a directory containing Electron ZIP files for Electron Packager to unzip, instead
+of downloading them. The ZIP filenames should be in the same format as the ones downloaded from the
+Electron releases site.
+
+**Note:** Setting this option prevents the [`download`](#download) sub-options from being used, as
+the functionality gets skipped over.
+
 ##### `extraResource`
 
 *String* or *Array* of *String*s
diff --git a/package.json b/package.json
index 9dd6826..d860e83 100644
--- a/package.json
+++ b/package.json
@@ -51,12 +51,12 @@
     "eslint-config-standard": "^14.0.0",
     "eslint-plugin-ava": "^9.0.0",
     "eslint-plugin-import": "^2.2.0",
-    "eslint-plugin-node": "^10.0.0",
+    "eslint-plugin-node": "^11.0.0",
     "eslint-plugin-promise": "^4.0.1",
     "eslint-plugin-standard": "^4.0.0",
-    "nyc": "^14.0.0",
+    "nyc": "^15.0.0",
     "pkg-up": "^3.0.1",
-    "sinon": "^7.0.0",
+    "sinon": "^8.0.1",
     "tempy": "^0.3.0"
   },
   "engines": {
diff --git a/src/index.js b/src/index.js
index b11c7b5..8919df4 100644
--- a/src/index.js
+++ b/src/index.js
@@ -105,8 +105,28 @@ class Packager {
     }
   }
 
+  async getElectronZipPath (downloadOpts) {
+    if (this.opts.electronZipDir) {
+      if (await fs.pathExists(this.opts.electronZipDir)) {
+        const zipPath = path.resolve(
+          this.opts.electronZipDir,
+          `electron-v${downloadOpts.version}-${downloadOpts.platform}-${downloadOpts.arch}.zip`
+        )
+        if (!await fs.pathExists(zipPath)) {
+          throw new Error(`The specified Electron ZIP file does not exist: ${zipPath}`)
+        }
+
+        return zipPath
+      }
+
+      throw new Error(`The specified Electron ZIP directory does not exist: ${this.opts.electronZipDir}`)
+    } else {
+      return download.downloadElectronZip(downloadOpts)
+    }
+  }
+
   async packageForPlatformAndArch (downloadOpts) {
-    const zipPath = await download.downloadElectronZip(downloadOpts)
+    const zipPath = await this.getElectronZipPath(downloadOpts)
     // Create delegated options object with specific platform and arch, for output directory naming
     const comboOpts = {
       ...this.opts,
diff --git a/usage.txt b/usage.txt
index 6ae570a..4f1e28d 100644
--- a/usage.txt
+++ b/usage.txt
@@ -48,6 +48,7 @@ download           a list of sub-options to pass to @electron/get. They are spec
                      checks.
 electron-version   the version of Electron that is being packaged, see
                    https://github.com/electron/electron/releases
+electron-zip-dir   the local path to a directory containing Electron ZIP files
 executable-name    the name of the executable file, sans file extension. Defaults to appname
 extra-resource     a file to copy into the app's resources directory
 icon               the local path to an icon file to use as the icon for the app.


Test patch:
diff --git a/test/basic.js b/test/basic.js
index d1326da..61d41ac 100644
--- a/test/basic.js
+++ b/test/basic.js
@@ -67,12 +67,7 @@ test('cannot build apps where the name ends in " Helper"', async t => {
     platform: 'linux'
   }
 
-  try {
-    await packager(opts)
-  } catch (err) {
-    return t.is(err.message, 'Application names cannot end in " Helper" due to limitations on macOS')
-  }
-  throw new Error('should not finish')
+  await t.throwsAsync(async () => packager(opts), 'Application names cannot end in " Helper" due to limitations on macOS')
 })
 
 test('deprecatedParameter moves value in deprecated param to new param if new param is not set', (t) => {
@@ -279,3 +274,32 @@ test.serial('dir: relative path', util.testSinglePlatform(async (t, opts) => {
   const finalPath = (await packager(opts))[0]
   t.is(path.join(t.context.workDir, 'ElectronTest-linux-x64'), finalPath, 'paths returned')
 }))
+
+test.serial('electronZipDir success', util.testSinglePlatform(async (t, opts) => {
+  const customDir = path.join(t.context.tempDir, 'download')
+  opts.dir = util.fixtureSubdir('basic')
+  opts.electronZipDir = customDir
+  await fs.ensureDir(customDir)
+  const zipPath = await download.downloadElectronZip(download.createDownloadOpts(opts, 'linux', 'x64'))
+  await fs.copy(zipPath, path.join(customDir, path.basename(zipPath)))
+
+  const paths = await packager(opts)
+  t.is(1, paths.length, '1 bundle created')
+}))
+
+test.serial('electronZipDir does not exist', util.testSinglePlatform(async (t, opts) => {
+  const customDir = path.join(t.context.tempDir, 'does-not-exist')
+  opts.dir = util.fixtureSubdir('basic')
+  opts.electronZipDir = customDir
+
+  await t.throwsAsync(async () => packager(opts), /Electron ZIP directory does not exist/)
+}))
+
+test.serial('electronZipDir: ZIP file does not exist', util.testSinglePlatform(async (t, opts) => {
+  const customDir = path.join(t.context.tempDir, 'download')
+  opts.dir = util.fixtureSubdir('basic')
+  opts.electronZipDir = customDir
+  await fs.ensureDir(customDir)
+
+  await t.throwsAsync(async () => packager(opts), /Electron ZIP file does not exist/)
+}))