From 93fb48f56c6f002b982617e3a246d8335d828912 Mon Sep 17 00:00:00 2001 From: Samuel Attard Date: Sun, 1 Jan 2017 17:09:24 +1300 Subject: [PATCH] feat(generic): expose top level methods as JS APIs ISSUES CLOSED: #54 --- .esdoc.json | 21 ++++ .gitignore | 1 + package.json | 5 +- src/api/import.js | 215 ++++++++++++++++++++++++++++++++++ src/api/index.js | 21 ++++ src/api/init.js | 56 +++++++++ src/api/install.js | 163 ++++++++++++++++++++++++++ src/api/lint.js | 73 ++++++++++++ src/api/make.js | 135 +++++++++++++++++++++ src/api/package.js | 97 +++++++++++++++ src/api/publish.js | 66 +++++++++++ src/api/start.js | 60 ++++++++++ src/electron-forge-import.js | 198 +------------------------------ src/electron-forge-init.js | 44 ++----- src/electron-forge-install.js | 141 ++-------------------- src/electron-forge-lint.js | 43 ++----- src/electron-forge-make.js | 119 +++---------------- src/electron-forge-package.js | 90 ++------------ src/electron-forge-publish.js | 56 +++------ src/electron-forge-start.js | 44 ++----- src/util/ora-handler.js | 38 ++++-- 21 files changed, 1018 insertions(+), 668 deletions(-) create mode 100644 .esdoc.json create mode 100644 src/api/import.js create mode 100644 src/api/index.js create mode 100644 src/api/init.js create mode 100644 src/api/install.js create mode 100644 src/api/lint.js create mode 100644 src/api/make.js create mode 100644 src/api/package.js create mode 100644 src/api/publish.js create mode 100644 src/api/start.js diff --git a/.esdoc.json b/.esdoc.json new file mode 100644 index 0000000000..3150ebb693 --- /dev/null +++ b/.esdoc.json @@ -0,0 +1,21 @@ +{ + "source": "./src/api", + "destination": "./docs", + "plugins": [ + { + "name": "esdoc-importpath-plugin", + "option": { + "replaces": [ + { + "from": ".js", + "to": "" + }, + { + "from": "api/", + "to": "dist/api/" + } + ] + } + } + ] +} diff --git a/.gitignore b/.gitignore index de4d1f007d..05f5a59104 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ dist node_modules +docs \ No newline at end of file diff --git a/package.json b/package.json index 336508d4c5..f729819be4 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "version": "2.4.0", "description": "A complete tool for building modern Electron applications", "repository": "https://github.com/electron-userland/electron-forge", - "main": "index.js", + "main": "dist/api/index.js", "bin": { "electron-forge": "dist/electron-forge.js", "forge": "dist/electron-forge.js" @@ -12,6 +12,7 @@ "build": "gulp build", "precommit": "npm run lint", "commit": "git-cz", + "docs": "esdoc", "lint": "eslint src test gulpfile.babel.js", "prepublish": "gulp build", "pretest": "gulp build", @@ -33,6 +34,8 @@ "chai": "^3.5.0", "commitizen": "^2.8.6", "cz-customizable": "4.0.0", + "esdoc": "^0.5.1", + "esdoc-importpath-plugin": "^0.1.0", "eslint": "^3.7.1", "eslint-config-airbnb-base": "^8.0.0", "eslint-plugin-import": "^1.16.0", diff --git a/src/api/import.js b/src/api/import.js new file mode 100644 index 0000000000..0baa48e209 --- /dev/null +++ b/src/api/import.js @@ -0,0 +1,215 @@ +import debug from 'debug'; +import fs from 'fs-promise'; +import inquirer from 'inquirer'; +import path from 'path'; +import { spawn as yarnOrNPMSpawn, hasYarn } from 'yarn-or-npm'; + +import initGit from '../init/init-git'; +import { deps, devDeps } from '../init/init-npm'; + +import asyncOra from '../util/ora-handler'; +import installDepList from '../util/install-dependencies'; +import readPackageJSON from '../util/read-package-json'; + +const d = debug('electron-forge:import'); + +/** + * @typedef {Object} ImportOptions + * @property {string} [dir=process.cwd()] The path to the module to import + * @property {boolean} [interactive=false] Boolean, whether to use sensible defaults or prompt the user visually. + */ + +/** + * Attempts to import a given module directory to the electron-forge standard. + * + * - Replaces the electron prebuilt with electron-compile + * - Sets up git and the correct NPM dependencies + * - Puts in a template forge config + * + * @param {ImportOptions} options - Options for the Import method + * @return {Promise} Will resolve when the import process is complete + */ +export default async (providedOptions = {}) => { + const { dir, interactive } = Object.assign({ + dir: process.cwd(), + interactive: false, + }, providedOptions); + asyncOra.interactive = interactive; + + d(`Attempting to import project in: ${dir}`); + if (!await fs.exists(dir) || !await fs.exists(path.resolve(dir, 'package.json'))) { + console.error(`We couldn't find a project in: ${dir}`.red); + process.exit(1); + } + + const confirm = !interactive || (await inquirer.createPromptModule()({ + type: 'confirm', + name: 'confirm', + message: `WARNING: We will now attempt to import: "${dir}". This will involve modifying some files, are you sure you want to continue?`, + })).confirm; + if (!confirm) { + process.exit(1); + } + + await initGit(dir); + + let packageJSON = await readPackageJSON(dir); + if (packageJSON.config && packageJSON.config.forge) { + console.warn('It looks like this project is already configured for "electron-forge"'.green); + const shouldContinue = !interactive || (await inquirer.createPromptModule()({ + type: 'confirm', + name: 'shouldContinue', + message: 'Are you sure you want to continue?', + })).shouldContinue; + if (!shouldContinue) { + process.exit(0); + } + } + + const shouldChangeMain = interactive ? (await inquirer.createPromptModule()({ + type: 'confirm', + name: 'shouldChangeMain', + message: 'Do you want us to change the "main" attribute of your package.json? If you are currently using babel and pointing to a "build" directory say yes.', // eslint-disable-line + })).shouldChangeMain : false; + if (shouldChangeMain) { + const { newMain } = await inquirer.createPromptModule()({ + type: 'input', + name: 'newMain', + default: packageJSON.main, + message: 'Enter the relative path to your uncompiled main file', + }); + packageJSON.main = newMain; + } + + packageJSON.dependencies = packageJSON.dependencies || {}; + packageJSON.devDependencies = packageJSON.devDependencies || {}; + + const keys = Object.keys(packageJSON.dependencies).concat(Object.keys(packageJSON.devDependencies)); + const buildToolPackages = { + 'electron-builder': 'provides mostly equivalent functionality', + 'electron-download': 'already uses this module as a transitive dependency', + 'electron-installer-debian': 'already uses this module as a transitive dependency', + 'electron-installer-dmg': 'already uses this module as a transitive dependency', + 'electron-installer-flatpak': 'already uses this module as a transitive dependency', + 'electron-installer-redhat': 'already uses this module as a transitive dependency', + 'electron-osx-sign': 'already uses this module as a transitive dependency', + 'electron-packager': 'already uses this module as a transitive dependency', + 'electron-winstaller': 'already uses this module as a transitive dependency', + }; + + let electronName; + for (const key of keys) { + if (key === 'electron' || key === 'electron-prebuilt') { + delete packageJSON.dependencies[key]; + delete packageJSON.devDependencies[key]; + electronName = key; + } else if (buildToolPackages[key]) { + const explanation = buildToolPackages[key]; + const shouldRemoveDependency = !interactive || (await inquirer.createPromptModule()({ + type: 'confirm', + name: 'shouldRemoveDependency', + message: `Do you want us to remove the "${key}" dependency in package.json? Electron Forge ${explanation}.`, + })).shouldRemoveDependency; + + if (shouldRemoveDependency) { + delete packageJSON.dependencies[key]; + delete packageJSON.devDependencies[key]; + } + } + } + + const writeChanges = async () => { + await asyncOra('Writing modified package.json file', async () => { + await fs.writeFile(path.resolve(dir, 'package.json'), `${JSON.stringify(packageJSON, null, 2)}\n`); + }); + }; + + let electronVersion; + if (electronName) { + const electronPackageJSON = await readPackageJSON(path.resolve(dir, 'node_modules', electronName)); + electronVersion = electronPackageJSON.version; + packageJSON.devDependencies['electron-prebuilt-compile'] = electronVersion; + } + + await writeChanges(); + + if (electronName) { + await asyncOra('Pruning deleted modules', async () => { + await new Promise((resolve) => { + d('attempting to prune node_modules in:', dir); + const child = yarnOrNPMSpawn(hasYarn() ? [] : ['prune'], { + cwd: dir, + stdio: 'ignore', + }); + child.on('exit', () => resolve()); + }); + }); + + await asyncOra('Installing dependencies', async () => { + d('deleting old dependencies forcefully'); + await fs.remove(path.resolve(dir, 'node_modules/.bin/electron')); + await fs.remove(path.resolve(dir, 'node_modules/.bin/electron.cmd')); + await fs.remove(path.resolve(dir, 'node_modules', electronName)); + + d('installing dependencies'); + await installDepList(dir, deps); + d('installing devDependencies'); + await installDepList(dir, devDeps, true); + d('installing electron-prebuilt-compile'); + await installDepList(dir, [`electron-prebuilt-compile@${electronVersion}`], false, true); + }); + } + + packageJSON = await readPackageJSON(dir); + + packageJSON.config = packageJSON.config || {}; + const templatePackageJSON = await readPackageJSON(path.resolve(__dirname, '../tmpl')); + packageJSON.config.forge = templatePackageJSON.config.forge; + + await writeChanges(); + + await asyncOra('Fixing .gitignore', async () => { + if (await fs.exists(path.resolve(dir, '.gitignore'))) { + const gitignore = await fs.readFile(path.resolve(dir, '.gitignore')); + if (!gitignore.includes('out')) { + await fs.writeFile(path.resolve(dir, '.gitignore'), `${gitignore}\nout/`); + } + } + }); + + let babelConfig = packageJSON.babel; + const babelPath = path.resolve(dir, '.babelrc'); + if (!babelConfig && await fs.exists(babelPath)) { + babelConfig = JSON.parse(await fs.readFile(babelPath, 'utf8')); + } + + if (babelConfig) { + await asyncOra('Porting original babel config', async () => { + let compileConfig = {}; + const compilePath = path.resolve(dir, '.compilerc'); + if (await fs.exists(compilePath)) { + compileConfig = JSON.parse(await fs.readFile(compilePath, 'utf8')); + } + + await fs.writeFile(compilePath, JSON.stringify(Object.assign(compileConfig, { + 'application/javascript': babelConfig, + }), null, 2)); + }); + + console.info('NOTE: You might be able to remove your `.compilerc` file completely if you are only using the `es2015` and `react` presets'.yellow); + } + + console.info(` + +We have ATTEMPTED to convert your app to be in a format that electron-forge understands. +Nothing much will have changed but we added the ${'"electron-prebuilt-compile"'.cyan} dependency. This is \ +the dependency you must version bump to get newer versions of Electron. + + +We also tried to import any build tooling you already had but we can't get everything. You might need to convert any CLI/gulp/grunt tasks yourself. + +Also please note if you are using \`preload\` scripts you need to follow the steps outlined \ +at https://github.com/electron-userland/electron-forge/wiki/Using-%27preload%27-scripts + +Thanks for using ${'"electron-forge"'.green}!!!`); +}; diff --git a/src/api/index.js b/src/api/index.js new file mode 100644 index 0000000000..b05b12a458 --- /dev/null +++ b/src/api/index.js @@ -0,0 +1,21 @@ +import 'colors'; + +import _import from './import'; +import init from './init'; +import install from './install'; +import lint from './lint'; +import make from './make'; +import _package from './package'; +import publish from './publish'; +import start from './start'; + +module.exports = { + 'import': _import, // eslint-disable-line + init, + install, + lint, + make, + 'pacakge': _package, // eslint-disable-line + publish, + start, +}; diff --git a/src/api/init.js b/src/api/init.js new file mode 100644 index 0000000000..932db27b5f --- /dev/null +++ b/src/api/init.js @@ -0,0 +1,56 @@ +import debug from 'debug'; + +import initCustom from '../init/init-custom'; +import initDirectory from '../init/init-directory'; +import initGit from '../init/init-git'; +import initNPM from '../init/init-npm'; +import initStandardFix from '../init/init-standard-fix'; +import initStarter from '../init/init-starter-files'; + +const d = debug('electron-forge:init'); + +/** + * @typedef {Object} InitOptions + * @property {string} [dir=process.cwd()] The path to the module to import + * @property {boolean} [interactive=false] Boolean, whether to use sensible defaults or prompt the user visually. + * @property {string} [lintstyle=airbnb] The lintstyle to pass through to the template creater + * @property {string} [template] The custom template to use, if left empty will use the default template. + */ + +/** + * Initializes a new electron-forge template project in the given directory + * + * @param {InitOptions} options - Options for the Import method + * @return {Promise} Will resolve when the initialization process is complete + */ +export default async (providedOptions = {}) => { + // eslint-disable-next-line prefer-const, no-unused-vars + let { dir, interactive, lintstyle, template } = Object.assign({ + dir: process.cwd(), + interactive: false, + lintstyle: 'airbnb', + template: null, + }, 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'; + } + } + + await initDirectory(dir); + await initGit(dir); + await initStarter(dir, template ? undefined : lintstyle); + await initNPM(dir, template ? undefined : lintstyle); + if (!template) { + if (lintstyle === 'standard') { + await initStandardFix(dir); + } + } else { + await initCustom(dir, template, lintstyle); + } +}; diff --git a/src/api/install.js b/src/api/install.js new file mode 100644 index 0000000000..6de28f7b78 --- /dev/null +++ b/src/api/install.js @@ -0,0 +1,163 @@ +import 'colors'; +import debug from 'debug'; +import fetch from 'node-fetch'; +import fs from 'fs-promise'; +import inquirer from 'inquirer'; +import nugget from 'nugget'; +import opn from 'opn'; +import os from 'os'; +import path from 'path'; +import pify from 'pify'; +import semver from 'semver'; + +import asyncOra from '../util/ora-handler'; + +import darwinDMGInstaller from '../installers/darwin/dmg'; +import darwinZipInstaller from '../installers/darwin/zip'; +import linuxDebInstaller from '../installers/linux/deb'; +import linuxRPMInstaller from '../installers/linux/rpm'; + +const d = debug('electron-forge:install'); + +const GITHUB_API = 'https://api.github.com'; + +/** + * @typedef {Object} InstallOptions + * @property {boolean} [interactive=false] Boolean, whether to use sensible defaults or prompt the user visually. + * @property {boolean} [prerelease=false] Whether to install prerelease versions + * @property {string} repo The GitHub repository to install from, in the format owner/name + * @property {function} chooseAsset A function that must return the asset to use/install from a provided array of compatible GitHub assets. + */ + +/** + * Installs an Electron application from GitHub. If you leave interactive as `false` you MUST provide a chooseAsset function. + * + * @param {InstallOptions} options - Options for the Install method + * @return {Promise} Will resolve when the install process is complete + */ +export default async (providedOptions = {}) => { + // eslint-disable-next-line prefer-const, no-unused-vars + let { interactive, prerelease, repo, chooseAsset } = Object.assign({ + interactive: false, + prerelease: false, + }, providedOptions); + asyncOra.interactive = interactive; + + let latestRelease; + let possibleAssets = []; + + await asyncOra('Searching for Application', async (searchSpinner) => { + if (!repo || repo.indexOf('/') === -1) { + // eslint-disable-next-line no-throw-literal + throw 'Invalid repository name, must be in the format owner/name'; + } + + d('searching for repo:', repo); + let releases; + try { + releases = await (await fetch(`${GITHUB_API}/repos/${repo}/releases`)).json(); + } catch (err) { + // Ignore error + } + + if (!releases || releases.message === 'Not Found' || !Array.isArray(releases)) { + // eslint-disable-next-line no-throw-literal + throw `Failed to find releases for repository "${repo}". Please check the name and try again.`; + } + + releases = releases.filter(release => !release.prerelease || prerelease); + + const sortedReleases = releases.sort((releaseA, releaseB) => { + let tagA = releaseA.tag_name; + if (tagA.substr(0, 1) === 'v') tagA = tagA.substr(1); + let tagB = releaseB.tag_name; + if (tagB.substr(0, 1) === 'v') tagB = tagB.substr(1); + return (semver.gt(tagB, tagA) ? 1 : -1); + }); + latestRelease = sortedReleases[0]; + + searchSpinner.text = 'Searching for Releases'; // eslint-disable-line + + const assets = latestRelease.assets; + if (!assets || !Array.isArray(assets)) { + // eslint-disable-next-line no-throw-literal + throw 'Could not find any assets for the latest release'; + } + + const installTargets = { + win32: [/\.exe$/], + darwin: [/OSX.*\.zip$/, /darwin.*\.zip$/, /macOS.*\.zip$/, /mac.*\.zip$/, /\.dmg$/], + linux: [/\.rpm$/, /\.deb$/], + }; + + possibleAssets = assets.filter((asset) => { + const targetSuffixes = installTargets[process.platform]; + for (const suffix of targetSuffixes) { + if (suffix.test(asset.name)) return true; + } + return false; + }); + + if (possibleAssets.length === 0) { + // eslint-disable-next-line no-throw-literal + throw `Failed to find any installable assets for target platform: ${`${process.platform}`.cyan}`; + } + }); + + console.info(`Found latest release${prerelease ? ' (including prereleases)' : ''}: ${latestRelease.tag_name.cyan}`); + + let targetAsset = possibleAssets[0]; + if (possibleAssets.length > 1) { + if (chooseAsset) { + targetAsset = await Promise.resolve(chooseAsset(possibleAssets)); + } else if (!interactive) { + const { assetID } = await inquirer.createPromptModule()({ + type: 'list', + name: 'assetID', + message: 'Multiple potential assets found, please choose one from the list below:'.cyan, + choices: possibleAssets.map(asset => ({ name: asset.name, value: asset.id })), + }); + + targetAsset = possibleAssets.find(asset => asset.id === assetID); + } else { + // eslint-disable-next-line no-throw-literal + throw 'expected a chooseAsset function to be provided but it was not'; + } + } + + const tmpdir = path.resolve(os.tmpdir(), 'forge-install'); + const pathSafeRepo = repo.replace(/[/\\]/g, '-'); + const filename = `${pathSafeRepo}-${latestRelease.tag_name}-${targetAsset.name}`; + + const fullFilePath = path.resolve(tmpdir, filename); + if (!await fs.exists(fullFilePath) || (await fs.stat(fullFilePath)).size !== targetAsset.size) { + await fs.mkdirs(tmpdir); + + const nuggetOpts = { + target: filename, + dir: tmpdir, + resume: true, + strictSSL: true, + }; + await pify(nugget)(targetAsset.browser_download_url, nuggetOpts); + } + + await asyncOra('Installing Application', async (installSpinner) => { + const installActions = { + win32: { + '.exe': async filePath => await opn(filePath, { wait: false }), + }, + darwin: { + '.zip': darwinZipInstaller, + '.dmg': darwinDMGInstaller, + }, + linux: { + '.deb': linuxDebInstaller, + '.rpm': linuxRPMInstaller, + }, + }; + + const suffixFnIdent = Object.keys(installActions[process.platform]).find(suffix => targetAsset.name.endsWith(suffix)); + await installActions[process.platform][suffixFnIdent](fullFilePath, installSpinner); + }); +}; diff --git a/src/api/lint.js b/src/api/lint.js new file mode 100644 index 0000000000..8ab946b33a --- /dev/null +++ b/src/api/lint.js @@ -0,0 +1,73 @@ +import 'colors'; +import debug from 'debug'; +import { spawn as yarnOrNPMSpawn } from 'yarn-or-npm'; + +import asyncOra from '../util/ora-handler'; +import resolveDir from '../util/resolve-dir'; + +const d = debug('electron-forge:lint'); + +/** + * @typedef {Object} LintOptions + * @property {string} [dir=process.cwd()] The path to the module to import + * @property {boolean} [interactive=false] Boolean, whether to use sensible defaults or prompt the user visually. + */ + +/** + * Lints a local Electron application. + * + * The promise will be rejected with the stdout+stderr of the linting process if linting fails or will be resolved + * if it succeeds. + * + * @param {LintOptions} options - Options for the Lint method + * @return {Promise} Will resolve when the lint process is complete + */ +export default async (providedOptions = {}) => { + // eslint-disable-next-line prefer-const, no-unused-vars + let { dir, interactive } = Object.assign({ + dir: process.cwd(), + interactive: false, + }, providedOptions); + asyncOra.interactive = interactive; + + let success = true; + let result = null; + + await asyncOra('Linting Application', async (lintSpinner) => { + dir = await resolveDir(dir); + if (!dir) { + // eslint-disable-next-line no-throw-literal + throw 'Failed to locate lintable Electron application'; + } + + d('executing "run lint -- --color" in dir:', dir); + const child = yarnOrNPMSpawn(['run', 'lint', '--', '--color'], { + stdio: process.platform === 'win32' ? 'inherit' : 'pipe', + cwd: dir, + }); + const output = []; + if (process.platform !== 'win32') { + child.stdout.on('data', data => output.push(data.toString())); + child.stderr.on('data', data => output.push(data.toString().red)); + } + await new Promise((resolve) => { + child.on('exit', (code) => { + if (code !== 0) { + success = false; + lintSpinner.fail(); + if (interactive) { + output.forEach(data => process.stdout.write(data)); + process.exit(code); + } else { + result = output.join(''); + } + } + resolve(); + }); + }); + }); + + if (!success) { + throw result; + } +}; diff --git a/src/api/make.js b/src/api/make.js new file mode 100644 index 0000000000..ab82bc3d71 --- /dev/null +++ b/src/api/make.js @@ -0,0 +1,135 @@ +import 'colors'; +import fs from 'fs-promise'; +import path from 'path'; + +import asyncOra from '../util/ora-handler'; +import electronHostArch from '../util/electron-host-arch'; +import getForgeConfig from '../util/forge-config'; +import readPackageJSON from '../util/read-package-json'; +import requireSearch from '../util/require-search'; +import resolveDir from '../util/resolve-dir'; + +import packager from './package'; + +/** + * @typedef {Object} MakeOptions + * @property {string} [dir=process.cwd()] The path to the module to import + * @property {boolean} [interactive=false] Boolean, whether to use sensible defaults or prompt the user visually. + * @property {boolean} [skipPackage=false] Whether to skip the pre-make packaging step + * @property {Array} [overrideTargets] An array of make targets to override your forge config + * @property {string} [arch=process.arch] The target arch + * @property {string} [platform=process.platform] The target platform. NOTE: This is limited to be the current platform at the moment + */ + +/** + * Makes distributables for an Electron application. + * + * @param {MakeOptions} options - Options for the Make method + * @return {Promise} Will resolve when the make process is complete + */ +export default async (providedOptions = {}) => { + // eslint-disable-next-line prefer-const, no-unused-vars + let { dir, interactive, skipPackage, overrideTargets, arch, platform } = Object.assign({ + dir: process.cwd(), + interactive: false, + skipPackage: false, + arch: electronHostArch(), + platform: process.platform, + }, providedOptions); + asyncOra.interactive = interactive; + + let forgeConfig; + await asyncOra('Resolving Forge Config', async () => { + dir = await resolveDir(dir); + if (!dir) { + // eslint-disable-next-line no-throw-literal + throw 'Failed to locate makeable Electron application'; + } + + forgeConfig = await getForgeConfig(dir); + }); + + if (platform && platform !== process.platform && !(process.platform === 'darwin' && platform === 'mas')) { + console.error('You can not "make" for a platform other than your systems platform'.red); + process.exit(1); + } + + if (!skipPackage) { + console.info('We need to package your application before we can make it'.green); + await packager({ + dir, + interactive, + arch, + platform, + }); + } else { + console.warn('WARNING: Skipping the packaging step, this could result in an out of date build'.red); + } + + const declaredArch = arch; + const declaredPlatform = platform; + + let targets = forgeConfig.make_targets[declaredPlatform]; + if (overrideTargets) { + targets = overrideTargets; + } + + console.info('Making for the following targets:', `${targets.join(', ')}`.cyan); + + let targetArchs = [declaredArch]; + if (declaredArch === 'all') { + switch (process.platform) { + case 'darwin': + targetArchs = ['x64']; + break; + case 'linux': + targetArchs = ['ia32', 'x64', 'armv7l']; + break; + case 'win32': + default: + targetArchs = ['ia32', 'x64']; + break; + } + } + + const packageJSON = await readPackageJSON(dir); + const appName = packageJSON.productName || packageJSON.name; + const outputs = []; + + for (const targetArch of targetArchs) { + const packageDir = path.resolve(dir, `out/${appName}-${declaredPlatform}-${targetArch}`); + if (!(await fs.exists(packageDir))) { + throw new Error(`Couldn't find packaged app at: ${packageDir}`); + } + + for (const target of targets) { + // eslint-disable-next-line no-loop-func + await asyncOra(`Making for target: ${target.cyan} - On platform: ${declaredPlatform.cyan} - For arch: ${targetArch.cyan}`, async () => { + const maker = requireSearch(path.resolve(__dirname, '..'), [ + `./makers/${process.platform}/${target}.js`, + `./makers/generic/${target}.js`, + `electron-forge-maker-${target}`, + ]); + if (!maker) { + // eslint-disable-next-line no-throw-literal + throw `Could not find a build target with the name: ${target} for the platform: ${declaredPlatform}`; + } + try { + outputs.push(await (maker.default || maker)(packageDir, appName, targetArch, forgeConfig, packageJSON)); + } catch (err) { + if (err) { + // eslint-disable-next-line no-throw-literal + throw { + message: `An error occured while making for target: ${target}`, + stack: `${err.message}\n${err.stack}`, + }; + } else { + throw new Error(`An unknown error occured while making for target: ${target}`); + } + } + }); + } + } + + return outputs; +}; diff --git a/src/api/package.js b/src/api/package.js new file mode 100644 index 0000000000..83872f8a77 --- /dev/null +++ b/src/api/package.js @@ -0,0 +1,97 @@ +import 'colors'; +import fs from 'fs-promise'; +import glob from 'glob'; +import path from 'path'; +import pify from 'pify'; +import packager from 'electron-packager'; +import ora from 'ora'; + +import electronHostArch from '../util/electron-host-arch'; +import getForgeConfig from '../util/forge-config'; +import packagerCompileHook from '../util/compile-hook'; +import readPackageJSON from '../util/read-package-json'; +import rebuildHook from '../util/rebuild'; +import resolveDir from '../util/resolve-dir'; + +/** + * @typedef {Object} PackageOptions + * @property {string} [dir=process.cwd()] The path to the module to import + * @property {boolean} [interactive=false] Boolean, whether to use sensible defaults or prompt the user visually. + * @property {string} [arch=process.arch] The target arch + * @property {string} [platform=process.platform] The target platform. NOTE: This is limited to be the current platform at the moment + */ + +/** + * Packages an Electron application into an platform dependent format. + * + * @param {PackageOptions} options - Options for the Package method + * @return {Promise} Will resolve when the package process is complete + */ +export default async (providedOptions = {}) => { + // eslint-disable-next-line prefer-const, no-unused-vars + let { dir, interactive, arch, platform } = Object.assign({ + dir: process.cwd(), + interactive: false, + arch: electronHostArch(), + platform: process.platform, + }, providedOptions); + + let prepareSpinner = ora.ora(`Preparing to Package Application for arch: ${(arch === 'all' ? 'ia32' : arch).cyan}`).start(); + let prepareCounter = 0; + + dir = await resolveDir(dir); + if (!dir) { + // eslint-disable-next-line no-throw-literal + throw 'Failed to locate compilable Electron application'; + } + + const packageJSON = await readPackageJSON(dir); + + const forgeConfig = await getForgeConfig(dir); + let packagerSpinner; + + const packageOpts = Object.assign({ + asar: false, + overwrite: true, + }, forgeConfig.electronPackagerConfig, { + afterCopy: [async (buildPath, electronVersion, pPlatform, pArch, done) => { + if (packagerSpinner) { + packagerSpinner.succeed(); + prepareCounter += 1; + prepareSpinner = ora.ora(`Preparing to Package Application for arch: ${(prepareCounter === 2 ? 'armv7l' : 'x64').cyan}`).start(); + } + await fs.remove(path.resolve(buildPath, 'node_modules/electron-compile/test')); + const bins = await pify(glob)(path.join(buildPath, '**/.bin/**/*')); + for (const bin of bins) { + await fs.remove(bin); + } + done(); + }, async (...args) => { + prepareSpinner.succeed(); + await packagerCompileHook(dir, ...args); + }, async (buildPath, electronVersion, pPlatform, pArch, done) => { + await rebuildHook(buildPath, electronVersion, pPlatform, pArch); + packagerSpinner = ora.ora('Packaging Application').start(); + done(); + }].concat(forgeConfig.electronPackagerConfig.afterCopy ? forgeConfig.electronPackagerConfig.afterCopy.map(item => + (typeof item === 'string' ? require(item) : item) + ) : []), + afterExtract: forgeConfig.electronPackagerConfig.afterExtract ? forgeConfig.electronPackagerConfig.afterExtract.map(item => + (typeof item === 'string' ? require(item) : item) + ) : [], + dir, + arch, + platform, + out: path.resolve(dir, 'out'), + version: packageJSON.devDependencies['electron-prebuilt-compile'], + }); + packageOpts.quiet = true; + if (typeof packageOpts.asar === 'object' && packageOpts.unpack) { + packagerSpinner.fail(); + throw new Error('electron-compile does not support asar.unpack yet. Please use asar.unpackDir'); + } + + await pify(packager)(packageOpts); + + packagerSpinner.succeed(); +}; diff --git a/src/api/publish.js b/src/api/publish.js new file mode 100644 index 0000000000..f61910bbd4 --- /dev/null +++ b/src/api/publish.js @@ -0,0 +1,66 @@ +import 'colors'; + +import asyncOra from '../util/ora-handler'; +import getForgeConfig from '../util/forge-config'; +import readPackageJSON from '../util/read-package-json'; +import requireSearch from '../util/require-search'; +import resolveDir from '../util/resolve-dir'; + +import make from './make'; + +/** + * @typedef {Object} PublishOptions + * @property {string} [dir=process.cwd()] The path to the module to publish + * @property {boolean} [interactive=false] Boolean, 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 {MakeOptions} [makeOptions] Options object to passed through to make() + */ + +/** + * Packages an Electron application into an platform dependent format. + * + * @param {PublishOptions} options - Options for the Publish method + * @return {Promise} Will resolve when the publish process is complete + */ +export default async (providedOptions = {}) => { + // eslint-disable-next-line prefer-const, no-unused-vars + let { dir, interactive, authToken, tag, target, makeOptions } = Object.assign({ + dir: process.cwd(), + interactive: false, + tag: null, + makeOptions: {}, + target: 'github', + }, providedOptions); + + const makeResults = await make(makeOptions); + + dir = await resolveDir(dir); + if (!dir) { + // eslint-disable-next-line no-throw-literal + throw 'Failed to locate publishable Electron application'; + } + + const artifacts = makeResults.reduce((accum, arr) => { + accum.push(...arr); + return accum; + }, []); + + const packageJSON = await readPackageJSON(dir); + + const forgeConfig = await getForgeConfig(dir); + + let publisher; + await asyncOra(`Resolving publish target: ${`${target}`.cyan}`, async () => { + publisher = requireSearch(__dirname, [ + `./publishers/${target}.js`, + `electron-forge-publisher-${target}`, + ]); + if (!publisher) { + throw `Could not find a publish target with the name: ${target}`; // eslint-disable-line + } + }); + + await publisher(artifacts, packageJSON, forgeConfig, authToken, tag); +}; diff --git a/src/api/start.js b/src/api/start.js new file mode 100644 index 0000000000..e339875683 --- /dev/null +++ b/src/api/start.js @@ -0,0 +1,60 @@ +import 'colors'; +import { spawn } from 'child_process'; +import path from 'path'; + +import asyncOra from '../util/ora-handler'; +import readPackageJSON from '../util/read-package-json'; +import rebuild from '../util/rebuild'; +import resolveDir from '../util/resolve-dir'; + +/** + * @typedef {Object} StartOptions + * @property {string} [dir=process.cwd()] The path to the module to publish + * @property {boolean} [interactive=false] Boolean, whether to use sensible defaults or prompt the user visually + * @property {boolean} [enableLogging=false] Enables advanced internal Electron debug calls + * @property {Array} [args] Arguments to pass through to the launched Electron application + */ + +/** + * Starts an Electron application + * + * @param {StartOptions} options - Options for the Publish method + * @return {Promise} Will resolve when the application is launched + */ +export default async (providedOptions = {}) => { + // eslint-disable-next-line prefer-const, no-unused-vars + let { dir, interactive, enableLogging, args } = Object.assign({ + dir: process.cwd(), + interactive: false, + enableLogging: false, + args: [], + }, providedOptions); + + await asyncOra('Locating Application', async () => { + dir = await resolveDir(dir); + if (!dir) { + // eslint-disable-next-line no-throw-literal + throw 'Failed to locate startable Electron application'; + } + }); + + const packageJSON = await readPackageJSON(dir); + + await rebuild(dir, packageJSON.devDependencies['electron-prebuilt-compile'], process.platform, process.arch); + + const spawnOpts = { + cwd: dir, + stdio: 'inherit', + env: Object.assign({}, process.env, enableLogging ? { + ELECTRON_ENABLE_LOGGING: true, + ELECTRON_ENABLE_STACK_DUMPING: true, + } : {}), + }; + await asyncOra('Launching Application', async () => { + if (process.platform === 'win32') { + spawn(path.resolve(dir, 'node_modules/.bin/electron.cmd'), ['.'].concat(args), spawnOpts); + } else { + spawn(path.resolve(dir, 'node_modules/.bin/electron'), ['.'].concat(args), spawnOpts); + } + }); +}; diff --git a/src/electron-forge-import.js b/src/electron-forge-import.js index 1c31cf400b..c80785d45c 100644 --- a/src/electron-forge-import.js +++ b/src/electron-forge-import.js @@ -1,22 +1,10 @@ -import debug from 'debug'; -import fs from 'fs-promise'; -import inquirer from 'inquirer'; import path from 'path'; import program from 'commander'; -import { spawn as yarnOrNPMSpawn, hasYarn } from 'yarn-or-npm'; - -import initGit from './init/init-git'; -import { deps, devDeps } from './init/init-npm'; - -import asyncOra from './util/ora-handler'; -import installDepList from './util/install-dependencies'; -import readPackageJSON from './util/read-package-json'; import './util/terminate'; +import importAPI from './api/import'; -const d = debug('electron-forge:import'); - -const main = async () => { +(async () => { let dir = process.cwd(); program .version(require('../package.json').version) @@ -31,182 +19,8 @@ const main = async () => { }) .parse(process.argv); - d(`Attempting to import project in: ${dir}`); - if (!await fs.exists(dir) || !await fs.exists(path.resolve(dir, 'package.json'))) { - console.error(`We couldn't find a project in: ${dir}`.red); - process.exit(1); - } - - const { confirm } = await inquirer.createPromptModule()({ - type: 'confirm', - name: 'confirm', - message: `WARNING: We will now attempt to import: "${dir}". This will involve modifying some files, are you sure you want to continue?`, - }); - if (!confirm) { - process.exit(1); - } - - await initGit(dir); - - let packageJSON = await readPackageJSON(dir); - if (packageJSON.config && packageJSON.config.forge) { - console.warn('It looks like this project is already configured for "electron-forge"'.green); - const { shouldContinue } = await inquirer.createPromptModule()({ - type: 'confirm', - name: 'shouldContinue', - message: 'Are you sure you want to continue?', - }); - if (!shouldContinue) { - process.exit(0); - } - } - - const { shouldChangeMain } = await inquirer.createPromptModule()({ - type: 'confirm', - name: 'shouldChangeMain', - message: 'Do you want us to change the "main" attribute of your package.json? If you are currently using babel and pointing to a "build" directory say yes.', // eslint-disable-line + await importAPI({ + dir, + interactive: true, }); - if (shouldChangeMain) { - const { newMain } = await inquirer.createPromptModule()({ - type: 'input', - name: 'newMain', - default: packageJSON.main, - message: 'Enter the relative path to your uncompiled main file', - }); - packageJSON.main = newMain; - } - - packageJSON.dependencies = packageJSON.dependencies || {}; - packageJSON.devDependencies = packageJSON.devDependencies || {}; - - const keys = Object.keys(packageJSON.dependencies).concat(Object.keys(packageJSON.devDependencies)); - const buildToolPackages = { - 'electron-builder': 'provides mostly equivalent functionality', - 'electron-download': 'already uses this module as a transitive dependency', - 'electron-installer-debian': 'already uses this module as a transitive dependency', - 'electron-installer-dmg': 'already uses this module as a transitive dependency', - 'electron-installer-flatpak': 'already uses this module as a transitive dependency', - 'electron-installer-redhat': 'already uses this module as a transitive dependency', - 'electron-osx-sign': 'already uses this module as a transitive dependency', - 'electron-packager': 'already uses this module as a transitive dependency', - 'electron-winstaller': 'already uses this module as a transitive dependency', - }; - - let electronName; - for (const key of keys) { - if (key === 'electron' || key === 'electron-prebuilt') { - delete packageJSON.dependencies[key]; - delete packageJSON.devDependencies[key]; - electronName = key; - } else if (buildToolPackages[key]) { - const explanation = buildToolPackages[key]; - const { shouldRemoveDependency } = await inquirer.createPromptModule()({ - type: 'confirm', - name: 'shouldRemoveDependency', - message: `Do you want us to remove the "${key}" dependency in package.json? Electron Forge ${explanation}.`, - }); - - if (shouldRemoveDependency) { - delete packageJSON.dependencies[key]; - delete packageJSON.devDependencies[key]; - } - } - } - - const writeChanges = async () => { - await asyncOra('Writing modified package.json file', async () => { - await fs.writeFile(path.resolve(dir, 'package.json'), `${JSON.stringify(packageJSON, null, 2)}\n`); - }); - }; - - let electronVersion; - if (electronName) { - const electronPackageJSON = await readPackageJSON(path.resolve(dir, 'node_modules', electronName)); - electronVersion = electronPackageJSON.version; - packageJSON.devDependencies['electron-prebuilt-compile'] = electronVersion; - } - - await writeChanges(); - - if (electronName) { - await asyncOra('Pruning deleted modules', async () => { - await new Promise((resolve) => { - d('attempting to prune node_modules in:', dir); - const child = yarnOrNPMSpawn(hasYarn() ? [] : ['prune'], { - cwd: dir, - stdio: 'ignore', - }); - child.on('exit', () => resolve()); - }); - }); - - await asyncOra('Installing dependencies', async () => { - d('deleting old dependencies forcefully'); - await fs.remove(path.resolve(dir, 'node_modules/.bin/electron')); - await fs.remove(path.resolve(dir, 'node_modules/.bin/electron.cmd')); - await fs.remove(path.resolve(dir, 'node_modules', electronName)); - - d('installing dependencies'); - await installDepList(dir, deps); - d('installing devDependencies'); - await installDepList(dir, devDeps, true); - d('installing electron-prebuilt-compile'); - await installDepList(dir, [`electron-prebuilt-compile@${electronVersion}`], false, true); - }); - } - - packageJSON = await readPackageJSON(dir); - - packageJSON.config = packageJSON.config || {}; - const templatePackageJSON = await readPackageJSON(path.resolve(__dirname, '../tmpl')); - packageJSON.config.forge = templatePackageJSON.config.forge; - - await writeChanges(); - - await asyncOra('Fixing .gitignore', async () => { - if (await fs.exists(path.resolve(dir, '.gitignore'))) { - const gitignore = await fs.readFile(path.resolve(dir, '.gitignore')); - if (!gitignore.includes('out')) { - await fs.writeFile(path.resolve(dir, '.gitignore'), `${gitignore}\nout/`); - } - } - }); - - let babelConfig = packageJSON.babel; - const babelPath = path.resolve(dir, '.babelrc'); - if (!babelConfig && await fs.exists(babelPath)) { - babelConfig = JSON.parse(await fs.readFile(babelPath, 'utf8')); - } - - if (babelConfig) { - await asyncOra('Porting original babel config', async () => { - let compileConfig = {}; - const compilePath = path.resolve(dir, '.compilerc'); - if (await fs.exists(compilePath)) { - compileConfig = JSON.parse(await fs.readFile(compilePath, 'utf8')); - } - - await fs.writeFile(compilePath, JSON.stringify(Object.assign(compileConfig, { - 'application/javascript': babelConfig, - }), null, 2)); - }); - - console.info('NOTE: You might be able to remove your `.compilerc` file completely if you are only using the `es2015` and `react` presets'.yellow); - } - - console.info(` - -We have ATTEMPTED to convert your app to be in a format that electron-forge understands. -Nothing much will have changed but we added the ${'"electron-prebuilt-compile"'.cyan} dependency. This is \ -the dependency you must version bump to get newer versions of Electron. - - -We also tried to import any build tooling you already had but we can't get everything. You might need to convert any CLI/gulp/grunt tasks yourself. - -Also please note if you are using \`preload\` scripts you need to follow the steps outlined \ -at https://github.com/electron-userland/electron-forge/wiki/Using-%27preload%27-scripts - -Thanks for using ${'"electron-forge"'.green}!!!`); -}; - -main(); +})(); diff --git a/src/electron-forge-init.js b/src/electron-forge-init.js index d2ea9a2da0..d7d353a5b9 100644 --- a/src/electron-forge-init.js +++ b/src/electron-forge-init.js @@ -1,19 +1,10 @@ -import debug from 'debug'; import path from 'path'; import program from 'commander'; -import initCustom from './init/init-custom'; -import initDirectory from './init/init-directory'; -import initGit from './init/init-git'; -import initNPM from './init/init-npm'; -import initStandardFix from './init/init-standard-fix'; -import initStarter from './init/init-starter-files'; - import './util/terminate'; +import { init } from './api'; -const d = debug('electron-forge:init'); - -const main = async () => { +(async () => { let dir = process.cwd(); program .version(require('../package.json').version) @@ -30,27 +21,12 @@ const main = async () => { }) .parse(process.argv); - d(`Initializing in: ${dir}`); - - if (!program.template) { - program.lintstyle = program.lintstyle.toLowerCase(); - if (!['airbnb', 'standard'].includes(program.lintstyle)) { - d(`Unrecognized lintstyle argument: '${program.lintstyle}' -- defaulting to 'airbnb'`); - program.lintstyle = 'airbnb'; - } - } - - await initDirectory(dir); - await initGit(dir); - await initStarter(dir, program.template ? undefined : program.lintstyle); - await initNPM(dir, program.template ? undefined : program.lintstyle); - if (!program.template) { - if (program.lintstyle === 'standard') { - await initStandardFix(dir); - } - } else { - await initCustom(dir, program.template, program.lintstyle); - } -}; + const initOpts = { + dir, + interactive: true, + lintstyle: program.lintstyle, + }; + if (program.template) initOpts.template = program.template; -main(); + await init(initOpts); +})(); diff --git a/src/electron-forge-install.js b/src/electron-forge-install.js index 5708aa8d1e..96ec7473b3 100644 --- a/src/electron-forge-install.js +++ b/src/electron-forge-install.js @@ -1,29 +1,9 @@ -import 'colors'; -import debug from 'debug'; -import fetch from 'node-fetch'; -import fs from 'fs-promise'; -import inquirer from 'inquirer'; -import nugget from 'nugget'; -import opn from 'opn'; -import os from 'os'; -import path from 'path'; -import pify from 'pify'; import program from 'commander'; -import semver from 'semver'; import './util/terminate'; -import asyncOra from './util/ora-handler'; +import { install } from './api'; -import darwinDMGInstaller from './installers/darwin/dmg'; -import darwinZipInstaller from './installers/darwin/zip'; -import linuxDebInstaller from './installers/linux/deb'; -import linuxRPMInstaller from './installers/linux/rpm'; - -const d = debug('electron-forge:install'); - -const GITHUB_API = 'https://api.github.com'; - -const main = async () => { +(async () => { let repo; program @@ -35,116 +15,9 @@ const main = async () => { }) .parse(process.argv); - let latestRelease; - let possibleAssets = []; - - await asyncOra('Searching for Application', async (searchSpinner) => { - if (!repo || repo.indexOf('/') === -1) { - // eslint-disable-next-line no-throw-literal - throw 'Invalid repository name, must be in the format owner/name'; - } - - d('searching for repo:', repo); - let releases; - try { - releases = await (await fetch(`${GITHUB_API}/repos/${repo}/releases`)).json(); - } catch (err) { - // Ignore error - } - - if (!releases || releases.message === 'Not Found' || !Array.isArray(releases)) { - // eslint-disable-next-line no-throw-literal - throw `Failed to find releases for repository "${repo}". Please check the name and try again.`; - } - - releases = releases.filter(release => !release.prerelease || program.prerelease); - - const sortedReleases = releases.sort((releaseA, releaseB) => { - let tagA = releaseA.tag_name; - if (tagA.substr(0, 1) === 'v') tagA = tagA.substr(1); - let tagB = releaseB.tag_name; - if (tagB.substr(0, 1) === 'v') tagB = tagB.substr(1); - return (semver.gt(tagB, tagA) ? 1 : -1); - }); - latestRelease = sortedReleases[0]; - - searchSpinner.text = 'Searching for Releases'; // eslint-disable-line - - const assets = latestRelease.assets; - if (!assets || !Array.isArray(assets)) { - // eslint-disable-next-line no-throw-literal - throw 'Could not find any assets for the latest release'; - } - - const installTargets = { - win32: [/\.exe$/], - darwin: [/OSX.*\.zip$/, /darwin.*\.zip$/, /macOS.*\.zip$/, /mac.*\.zip$/, /\.dmg$/], - linux: [/\.rpm$/, /\.deb$/], - }; - - possibleAssets = assets.filter((asset) => { - const targetSuffixes = installTargets[process.platform]; - for (const suffix of targetSuffixes) { - if (suffix.test(asset.name)) return true; - } - return false; - }); - - if (possibleAssets.length === 0) { - // eslint-disable-next-line no-throw-literal - throw `Failed to find any installable assets for target platform: ${`${process.platform}`.cyan}`; - } + await install({ + repo, + interactive: true, + prerelease: program.prerelease, }); - - console.info(`Found latest release${program.prerelease ? ' (including prereleases)' : ''}: ${latestRelease.tag_name.cyan}`); - - let targetAsset = possibleAssets[0]; - if (possibleAssets.length > 1) { - const { assetID } = await inquirer.createPromptModule()({ - type: 'list', - name: 'assetID', - message: 'Multiple potential assets found, please choose one from the list below:'.cyan, - choices: possibleAssets.map(asset => ({ name: asset.name, value: asset.id })), - }); - - targetAsset = possibleAssets.find(asset => asset.id === assetID); - } - - const tmpdir = path.resolve(os.tmpdir(), 'forge-install'); - const pathSafeRepo = repo.replace(/[/\\]/g, '-'); - const filename = `${pathSafeRepo}-${latestRelease.tag_name}-${targetAsset.name}`; - - const fullFilePath = path.resolve(tmpdir, filename); - if (!await fs.exists(fullFilePath) || (await fs.stat(fullFilePath)).size !== targetAsset.size) { - await fs.mkdirs(tmpdir); - - const nuggetOpts = { - target: filename, - dir: tmpdir, - resume: true, - strictSSL: true, - }; - await pify(nugget)(targetAsset.browser_download_url, nuggetOpts); - } - - await asyncOra('Installing Application', async (installSpinner) => { - const installActions = { - win32: { - '.exe': async filePath => await opn(filePath, { wait: false }), - }, - darwin: { - '.zip': darwinZipInstaller, - '.dmg': darwinDMGInstaller, - }, - linux: { - '.deb': linuxDebInstaller, - '.rpm': linuxRPMInstaller, - }, - }; - - const suffixFnIdent = Object.keys(installActions[process.platform]).find(suffix => targetAsset.name.endsWith(suffix)); - await installActions[process.platform][suffixFnIdent](fullFilePath, installSpinner); - }); -}; - -main(); +})(); diff --git a/src/electron-forge-lint.js b/src/electron-forge-lint.js index 33a287b34a..a23761588f 100644 --- a/src/electron-forge-lint.js +++ b/src/electron-forge-lint.js @@ -1,17 +1,11 @@ -import 'colors'; -import debug from 'debug'; -import fs from 'fs-promise'; +import fs from 'fs'; import path from 'path'; import program from 'commander'; -import { spawn as yarnOrNPMSpawn } from 'yarn-or-npm'; import './util/terminate'; -import asyncOra from './util/ora-handler'; -import resolveDir from './util/resolve-dir'; +import { lint } from './api'; -const d = debug('electron-forge:lint'); - -const main = async () => { +(async () => { let dir = process.cwd(); program .version(require('../package.json').version) @@ -26,31 +20,8 @@ const main = async () => { }) .parse(process.argv); - await asyncOra('Linting Application', async (lintSpinner) => { - dir = await resolveDir(dir); - if (!dir) { - // eslint-disable-next-line no-throw-literal - throw 'Failed to locate lintable Electron application'; - } - - d('executing "run lint -- --color" in dir:', dir); - const child = yarnOrNPMSpawn(['run', 'lint', '--', '--color'], { - stdio: process.platform === 'win32' ? 'inherit' : 'pipe', - cwd: dir, - }); - const output = []; - if (process.platform !== 'win32') { - child.stdout.on('data', data => output.push(data.toString())); - child.stderr.on('data', data => output.push(data.toString().red)); - } - child.on('exit', (code) => { - if (code !== 0) { - lintSpinner.fail(); - output.forEach(data => process.stdout.write(data)); - process.exit(code); - } - }); + await lint({ + dir, + interactive: true, }); -}; - -main(); +})(); diff --git a/src/electron-forge-make.js b/src/electron-forge-make.js index 3807b6a7cd..c8ae55cda3 100644 --- a/src/electron-forge-make.js +++ b/src/electron-forge-make.js @@ -1,18 +1,12 @@ -import 'colors'; import fs from 'fs-promise'; import path from 'path'; import program from 'commander'; import './util/terminate'; -import asyncOra from './util/ora-handler'; -import electronHostArch from './util/electron-host-arch'; -import getForgeConfig from './util/forge-config'; -import packager from './electron-forge-package'; -import readPackageJSON from './util/read-package-json'; -import requireSearch from './util/require-search'; -import resolveDir from './util/resolve-dir'; +import { make } from './api'; -const main = async () => { +// eslint-disable-next-line import/prefer-default-export +export const getMakeOptions = () => { let dir = process.cwd(); program .version(require('../package.json').version) @@ -20,7 +14,7 @@ const main = async () => { .option('--skip-package', 'Assume the app is already packaged') .option('-a, --arch [arch]', 'Target architecture') .option('-p, --platform [platform]', 'Target build platform') - .option('-t, --targets [targets]', 'Override your make targets for this run') + .option('--targets [targets]', 'Override your make targets for this run') .allowUnknownOption(true) .action((cwd) => { if (!cwd) return; @@ -32,99 +26,20 @@ const main = async () => { }) .parse(process.argv); - let forgeConfig; - await asyncOra('Resolving Forge Config', async () => { - dir = await resolveDir(dir); - if (!dir) { - // eslint-disable-next-line no-throw-literal - throw 'Failed to locate makeable Electron application'; - } + const makeOpts = { + dir, + interactive: true, + skipPackage: program.skipPackage, + }; + if (program.targets) makeOpts.overrideTargets = program.targets.split(','); + if (program.arch) makeOpts.arch = program.arch; + if (program.platform) makeOpts.platform = program.platform; - forgeConfig = await getForgeConfig(dir); - }); - - if (program.platform && program.platform !== process.platform && !(process.platform === 'darwin' && program.platform === 'mas')) { - console.error('You can not "make" for a platform other than your systems platform'.red); - process.exit(1); - } - - if (!program.skipPackage) { - console.info('We need to package your application before we can make it'.green); - await packager(); - } else { - console.warn('WARNING: Skipping the packaging step, this could result in an out of date build'.red); - } - - const declaredArch = program.arch || electronHostArch(); - const declaredPlatform = program.platform || process.platform; - - let targets = forgeConfig.make_targets[declaredPlatform]; - if (program.targets) { - targets = program.targets.split(','); - } - - console.info('Making for the following targets:', `${targets.join(', ')}`.cyan); - - let targetArchs = [declaredArch]; - if (declaredArch === 'all') { - switch (process.platform) { - case 'darwin': - targetArchs = ['x64']; - break; - case 'linux': - targetArchs = ['ia32', 'x64', 'armv7l']; - break; - case 'win32': - default: - targetArchs = ['ia32', 'x64']; - break; - } - } - - const packageJSON = await readPackageJSON(dir); - const appName = packageJSON.productName || packageJSON.name; - const outputs = []; - - for (const targetArch of targetArchs) { - const packageDir = path.resolve(dir, `out/${appName}-${declaredPlatform}-${targetArch}`); - if (!(await fs.exists(packageDir))) { - throw new Error(`Couldn't find packaged app at: ${packageDir}`); - } - - for (const target of targets) { - // eslint-disable-next-line no-loop-func - await asyncOra(`Making for target: ${target.cyan} - On platform: ${declaredPlatform.cyan} - For arch: ${targetArch.cyan}`, async () => { - const maker = requireSearch(__dirname, [ - `./makers/${process.platform}/${target}.js`, - `./makers/generic/${target}.js`, - `electron-forge-maker-${target}`, - ]); - if (!maker) { - // eslint-disable-next-line no-throw-literal - throw `Could not find a build target with the name: ${target} for the platform: ${declaredPlatform}`; - } - try { - outputs.push(await (maker.default || maker)(packageDir, appName, targetArch, forgeConfig, packageJSON)); - } catch (err) { - if (err) { - // eslint-disable-next-line no-throw-literal - throw { - message: `An error occured while making for target: ${target}`, - stack: `${err.message}\n${err.stack}`, - }; - } else { - throw new Error(`An unknown error occured while making for target: ${target}`); - } - } - }); - } - } - - return outputs; + return makeOpts; }; -if (process.mainModule === module) { - main(); -} +(async () => { + const makeOpts = getMakeOptions(); -export default main; + await make(makeOpts); +})(); diff --git a/src/electron-forge-package.js b/src/electron-forge-package.js index 06ca846af8..e5270271c9 100644 --- a/src/electron-forge-package.js +++ b/src/electron-forge-package.js @@ -1,21 +1,11 @@ -import 'colors'; import fs from 'fs-promise'; -import glob from 'glob'; import path from 'path'; -import pify from 'pify'; -import packager from 'electron-packager'; import program from 'commander'; -import ora from 'ora'; import './util/terminate'; -import electronHostArch from './util/electron-host-arch'; -import getForgeConfig from './util/forge-config'; -import packagerCompileHook from './util/compile-hook'; -import readPackageJSON from './util/read-package-json'; -import rebuildHook from './util/rebuild'; -import resolveDir from './util/resolve-dir'; +import packageAPI from './api/package'; -const main = async () => { +(async () => { let dir = process.cwd(); program @@ -31,76 +21,14 @@ const main = async () => { dir = path.resolve(dir, cwd); } }) - .on('--help', () => { - console.log('NOTE: All `electron-packager` arguments will be passed through to the packager'); - }) .parse(process.argv); - const arch = program.arch || electronHostArch(); - const platform = program.platform || process.platform; - - let prepareSpinner = ora.ora(`Preparing to Package Application for arch: ${(arch === 'all' ? 'ia32' : arch).cyan}`).start(); - let prepareCounter = 0; - - dir = await resolveDir(dir); - if (!dir) { - // eslint-disable-next-line no-throw-literal - throw 'Failed to locate compilable Electron application'; - } - - const packageJSON = await readPackageJSON(dir); - - const forgeConfig = await getForgeConfig(dir); - let packagerSpinner; - - const packageOpts = Object.assign({ - asar: false, - overwrite: true, - }, forgeConfig.electronPackagerConfig, { - afterCopy: [async (buildPath, electronVersion, pPlatform, pArch, done) => { - if (packagerSpinner) { - packagerSpinner.succeed(); - prepareCounter += 1; - prepareSpinner = ora.ora(`Preparing to Package Application for arch: ${(prepareCounter === 2 ? 'armv7l' : 'x64').cyan}`).start(); - } - await fs.remove(path.resolve(buildPath, 'node_modules/electron-compile/test')); - const bins = await pify(glob)(path.join(buildPath, '**/.bin/**/*')); - for (const bin of bins) { - await fs.remove(bin); - } - done(); - }, async (...args) => { - prepareSpinner.succeed(); - await packagerCompileHook(dir, ...args); - }, async (buildPath, electronVersion, pPlatform, pArch, done) => { - await rebuildHook(buildPath, electronVersion, pPlatform, pArch); - packagerSpinner = ora.ora('Packaging Application').start(); - done(); - }].concat(forgeConfig.electronPackagerConfig.afterCopy ? forgeConfig.electronPackagerConfig.afterCopy.map(item => - (typeof item === 'string' ? require(item) : item) - ) : []), - afterExtract: forgeConfig.electronPackagerConfig.afterExtract ? forgeConfig.electronPackagerConfig.afterExtract.map(item => - (typeof item === 'string' ? require(item) : item) - ) : [], + const packageOpts = { dir, - arch, - platform, - out: path.resolve(dir, 'out'), - version: packageJSON.devDependencies['electron-prebuilt-compile'], - }); - packageOpts.quiet = true; - if (typeof packageOpts.asar === 'object' && packageOpts.unpack) { - packagerSpinner.fail(); - throw new Error('electron-compile does not support asar.unpack yet. Please use asar.unpackDir'); - } - - await pify(packager)(packageOpts); - - packagerSpinner.succeed(); -}; - -if (process.mainModule === module) { - main(); -} + interactive: true, + }; + if (program.arch) packageOpts.arch = program.arch; + if (program.platform) packageOpts.platform = program.platform; -export default main; + await packageAPI(packageOpts); +})(); diff --git a/src/electron-forge-publish.js b/src/electron-forge-publish.js index ec3f600369..1a8747211b 100644 --- a/src/electron-forge-publish.js +++ b/src/electron-forge-publish.js @@ -1,26 +1,18 @@ -import 'colors'; import fs from 'fs-promise'; import path from 'path'; import program from 'commander'; import './util/terminate'; -import asyncOra from './util/ora-handler'; -import getForgeConfig from './util/forge-config'; -import readPackageJSON from './util/read-package-json'; -import requireSearch from './util/require-search'; -import resolveDir from './util/resolve-dir'; - -import make from './electron-forge-make'; - -const main = async () => { - const makeResults = await make(); +import { publish } from './api'; +import { getMakeOptions } from './electron-forge-make'; +(async () => { let dir = process.cwd(); program .version(require('../package.json').version) .arguments('[cwd]') .option('--auth-token', 'Authorization token for your publisher target (if required)') - .option('-t, --tag', 'The tag to publish to on GitHub') + .option('--tag', 'The tag to publish to on GitHub') .option('--target', 'The deployment target, defaults to "github"') .allowUnknownOption(true) .action((cwd) => { @@ -33,35 +25,15 @@ const main = async () => { }) .parse(process.argv); - dir = await resolveDir(dir); - if (!dir) { - // eslint-disable-next-line no-throw-literal - throw 'Failed to locate publishable Electron application'; - } - - const artifacts = makeResults.reduce((accum, arr) => { - accum.push(...arr); - return accum; - }, []); - - const packageJSON = await readPackageJSON(dir); - - const forgeConfig = await getForgeConfig(dir); - - if (!program.target) program.target = 'github'; - - let publisher; - await asyncOra(`Resolving publish target: ${`${program.target}`.cyan}`, async () => { - publisher = requireSearch(__dirname, [ - `./publishers/${program.target}.js`, - `electron-forge-publisher-${program.target}`, - ]); - if (!publisher) { - throw `Could not find a publish target with the name: ${program.target}`; // eslint-disable-line - } - }); + const publishOpts = { + dir, + interactive: true, + authToken: program.authToken, + tag: program.tag, + }; + if (program.target) publishOpts.target = program.target; - await publisher(artifacts, packageJSON, forgeConfig, program.authToken, program.tag); -}; + publishOpts.makeOptions = getMakeOptions(); -main(); + await publish(publishOpts); +})(); diff --git a/src/electron-forge-start.js b/src/electron-forge-start.js index 63746ec455..d0eb51c8a1 100644 --- a/src/electron-forge-start.js +++ b/src/electron-forge-start.js @@ -1,16 +1,11 @@ -import 'colors'; -import { spawn } from 'child_process'; import fs from 'fs-promise'; import path from 'path'; import program from 'commander'; import './util/terminate'; -import asyncOra from './util/ora-handler'; -import readPackageJSON from './util/read-package-json'; -import rebuild from './util/rebuild'; -import resolveDir from './util/resolve-dir'; +import { start } from './api'; -const main = async () => { +(async () => { let dir = process.cwd(); program .version(require('../package.json').version) @@ -26,33 +21,10 @@ const main = async () => { }) .parse(process.argv.slice(0, 2)); - await asyncOra('Locating Application', async () => { - dir = await resolveDir(dir); - if (!dir) { - // eslint-disable-next-line no-throw-literal - throw 'Failed to locate startable Electron application'; - } + await start({ + dir, + interactive: true, + enableLogging: program.enableLogging, + args: process.argv.slice(2), }); - - const packageJSON = await readPackageJSON(dir); - - await rebuild(dir, packageJSON.devDependencies['electron-prebuilt-compile'], process.platform, process.arch); - - const spawnOpts = { - cwd: dir, - stdio: 'inherit', - env: Object.assign({}, process.env, program.enableLogging ? { - ELECTRON_ENABLE_LOGGING: true, - ELECTRON_ENABLE_STACK_DUMPING: true, - } : {}), - }; - await asyncOra('Launching Application', async () => { - if (process.platform === 'win32') { - spawn(path.resolve(dir, 'node_modules/.bin/electron.cmd'), ['.'].concat(process.argv.slice(2)), spawnOpts); - } else { - spawn(path.resolve(dir, 'node_modules/.bin/electron'), ['.'].concat(process.argv.slice(2)), spawnOpts); - } - }); -}; - -main(); +})(); diff --git a/src/util/ora-handler.js b/src/util/ora-handler.js index 7162cd3ac2..b42a09e76c 100644 --- a/src/util/ora-handler.js +++ b/src/util/ora-handler.js @@ -1,23 +1,41 @@ import colors from 'colors'; import ora from 'ora'; -export default (initalOraValue, asyncFn) => { - const fnOra = ora.ora(initalOraValue).start(); - return new Promise((resolve) => { +class MockOra { + succeed() { return this; } + fail() { return this; } + start() { return this; } + stop() { return this; } +} + +const asyncOra = (initalOraValue, asyncFn) => { + let fnOra = new MockOra(); + if (asyncOra.interactive) { + fnOra = ora.ora(initalOraValue).start(); + } + return new Promise((resolve, reject) => { asyncFn(fnOra).then(() => { fnOra.succeed(); resolve(); }).catch((err) => { fnOra.fail(); - if (err && err.message && err.stack) { - console.error('\nAn unhandled error has occurred inside Forge:'.red); - console.error(colors.red(err.message)); - console.error(colors.red(err.stack)); + if (asyncOra.interactive) { + if (err && err.message && err.stack) { + console.error('\nAn unhandled error has occurred inside Forge:'.red); + console.error(colors.red(err.message)); + console.error(colors.red(err.stack)); + } else { + console.error('\nElectron forge was terminated:'.red); + console.error(colors.red(typeof err === 'string' ? err : JSON.stringify(err))); + } + process.exit(1); } else { - console.error('\nElectron forge was terminated:'.red); - console.error(colors.red(typeof err === 'string' ? err : JSON.stringify(err))); + reject(err); } - process.exit(1); }); }); }; + +asyncOra.interactive = true; + +export default asyncOra;