From d59695ecdb47a8a0865bc34c77c4bcc0c4302a32 Mon Sep 17 00:00:00 2001 From: Samuel Attard Date: Wed, 14 Feb 2018 20:54:53 +1100 Subject: [PATCH] feat(generic): remove electron-compile, make forge less opinionated and quite vanilla BREAKING CHANGE --- README.md | 7 +-- SUPPORT.md | 1 - src/api/import.js | 81 +------------------------ src/api/init.js | 25 ++------ src/api/make.js | 3 +- src/api/package.js | 18 +++--- src/api/start.js | 21 +++++-- src/electron-forge-init.js | 2 - src/init/init-custom.js | 4 +- src/init/init-npm.js | 53 ++-------------- src/init/init-standard-fix.js | 24 -------- src/init/init-starter-files.js | 5 +- src/util/compile-hook.js | 40 ------------ src/util/electron-version.js | 6 ++ src/util/forge-config.js | 6 +- src/util/hook.js | 4 +- src/util/plugin-interface.js | 50 +++++++++++++++ src/util/resolve-dir.js | 10 +-- test/fast/forge-config_spec.js | 14 ++++- test/fast/hook_spec.js | 12 +++- test/fast/publish_spec.js | 7 ++- test/fast/start_spec.js | 27 +++++++-- test/fixture/dummy_app/package.json | 2 +- test/fixture/dummy_js_conf/package.json | 2 +- test/slow/api_spec_slow.js | 61 ++++++------------- tmpl/_compilerc | 24 -------- tmpl/_eslintrc | 9 --- tmpl/index.js | 2 +- 28 files changed, 188 insertions(+), 332 deletions(-) delete mode 100644 src/init/init-standard-fix.js delete mode 100644 src/util/compile-hook.js create mode 100644 src/util/electron-version.js create mode 100644 src/util/plugin-interface.js delete mode 100644 tmpl/_compilerc delete mode 100644 tmpl/_eslintrc diff --git a/README.md b/README.md index 64a2bdd11b..e73d0c2234 100644 --- a/README.md +++ b/README.md @@ -62,9 +62,6 @@ npm start With these goals in mind, under the hood this project uses, among others: -* [`electron-compile`](https://github.com/electron/electron-compile): a tool - that lets you use modern and futuristic languages inside Electron without - worrying about transpiling or build tooling. * [`electron-rebuild`](https://github.com/electron/electron-rebuild): Automatically recompiles native Node.js modules against the correct Electron version. @@ -197,7 +194,7 @@ You can set `electronPackagerConfig` with any of the options from * `arch` (use the `--arch` Forge command line argument instead, so it's available to all of Forge) * `asar.unpack` (use `asar.unpackDir` instead) * `dir` (use the `cwd` Forge command line argument instead, so it's available to all of Forge) -* `electronVersion` (uses the exact version specified for `electron-prebuilt-compile` in your `devDependencies`) +* `electronVersion` (uses the exact version specified for `electron` in your `devDependencies`) * `out` * `platform` (use the `--platform` Forge command line argument instead, so it's available to all of Forge) * `quiet` @@ -205,7 +202,7 @@ You can set `electronPackagerConfig` with any of the options from You can set `electronRebuildConfig` with any of the options from [Electron Rebuild](https://github.com/electron/electron-rebuild#how-can-i-integrate-this-into-grunt--gulp--whatever), except: -* `electronVersion`/`--version` (uses the exact version specified for `electron-prebuilt-compile` in your `devDependencies`) +* `electronVersion`/`--version` (uses the exact version specified for `electron` in your `devDependencies`) * `arch`/`--arch` (use the `--arch` Forge command line argument instead, so it's available to all of Forge) * `buildPath`/`--module-dir` (uses your project's `node_modules`) diff --git a/SUPPORT.md b/SUPPORT.md index 94af6eca87..366c7da23d 100644 --- a/SUPPORT.md +++ b/SUPPORT.md @@ -10,7 +10,6 @@ running `electron-forge`. This will print debug information from the specified m value of the environment variable is a comma-separated list of modules which support this logging feature. Known modules include: -* `electron-compile:*` * `electron-download` * `electron-forge:*` (always use this one before filing an issue) * `electron-installer-debian` diff --git a/src/api/import.js b/src/api/import.js index d60652c4c9..f2b7840fd0 100644 --- a/src/api/import.js +++ b/src/api/import.js @@ -1,6 +1,5 @@ import debug from 'debug'; import fs from 'fs-extra'; -import inquirer from 'inquirer'; import path from 'path'; import initGit from '../init/init-git'; @@ -12,7 +11,6 @@ import { info, warn } from '../util/messages'; import installDepList from '../util/install-dependencies'; import readPackageJSON from '../util/read-package-json'; import confirmIfInteractive from '../util/confirm-if-interactive'; -import { yarnOrNpmSpawn, hasYarn } from '../util/yarn-or-npm'; const d = debug('electron-forge:import'); @@ -27,7 +25,6 @@ const d = debug('electron-forge:import'); /** * Attempt to import a given module directory to the Electron Forge standard. * - * - Replaces the prebuilt electron package with the one that integrates with `electron-compile` * - Sets up `git` and the correct NPM dependencies * - Adds a template forge config to `package.json` * @@ -68,18 +65,6 @@ export default async (providedOptions = {}) => { } } - // eslint-disable-next-line max-len - const shouldChangeMain = await confirmIfInteractive(interactive, '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.', 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 || {}; @@ -96,13 +81,8 @@ export default async (providedOptions = {}) => { '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]) { + if (buildToolPackages[key]) { const explanation = buildToolPackages[key]; // eslint-disable-next-line max-len const shouldRemoveDependency = await confirmIfInteractive(interactive, `Do you want us to remove the "${key}" dependency in package.json? Electron Forge ${explanation}.`); @@ -139,34 +119,13 @@ export default async (providedOptions = {}) => { }); }; - 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 () => { - d('attempting to prune node_modules in:', dir); - await yarnOrNpmSpawn(hasYarn() ? [] : ['prune'], { - cwd: dir, - stdio: 'ignore', - }); - }); - } - 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')); - if (electronName) { - await fs.remove(path.resolve(dir, 'node_modules', electronName)); - } - d('installing dependencies'); await installDepList(dir, deps); @@ -174,13 +133,7 @@ export default async (providedOptions = {}) => { await installDepList(dir, devDeps, true); d('installing exactDevDependencies'); - await installDepList(dir, exactDevDeps.map((dep) => { - if (dep === 'electron-prebuilt-compile') { - return `${dep}@${electronVersion || 'latest'}`; - } - - return dep; - }), true, true); + await installDepList(dir, exactDevDeps, true, true); }); packageJSON = await readPackageJSON(dir); @@ -205,39 +158,9 @@ export default async (providedOptions = {}) => { } }); - let babelConfig = packageJSON.babel; - const babelPath = path.resolve(dir, '.babelrc'); - if (!babelConfig && await fs.pathExists(babelPath)) { - babelConfig = await fs.readJson(babelPath, 'utf8'); - } - - if (babelConfig) { - await asyncOra('Porting original babel config', async () => { - let compileConfig = {}; - const compilePath = path.resolve(dir, '.compilerc'); - if (await fs.pathExists(compilePath)) { - compileConfig = await fs.readJson(compilePath, 'utf8'); - } - - await fs.writeJson(compilePath, Object.assign(compileConfig, { - 'application/javascript': babelConfig, - }), { spaces: 2 }); - }); - - info(interactive, 'NOTE: You might be able to remove your `.compilerc` file completely if you are only using the `es2016` and `react` presets'.yellow); - } - info(interactive, ` 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/init.js b/src/api/init.js index 6b9db589f7..2f295432a1 100644 --- a/src/api/init.js +++ b/src/api/init.js @@ -4,7 +4,6 @@ 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 asyncOra from '../util/ora-handler'; @@ -15,7 +14,6 @@ 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 {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 */ @@ -28,10 +26,9 @@ const d = debug('electron-forge:init'); */ export default async (providedOptions = {}) => { // eslint-disable-next-line prefer-const, no-unused-vars - let { dir, interactive, lintStyle, copyCIFiles, template } = Object.assign({ + let { dir, interactive, copyCIFiles, template } = Object.assign({ dir: process.cwd(), interactive: false, - lintStyle: 'airbnb', copyCIFiles: false, template: null, }, providedOptions); @@ -39,23 +36,11 @@ 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'; - } - } - await initDirectory(dir, interactive); await initGit(dir); - await initStarter(dir, { lintStyle: template ? undefined : lintStyle, copyCIFiles }); - await initNPM(dir, template ? undefined : lintStyle); - if (!template) { - if (lintStyle === 'standard') { - await initStandardFix(dir); - } - } else { - await initCustom(dir, template, lintStyle); + await initStarter(dir, { copyCIFiles }); + await initNPM(dir); + if (template) { + await initCustom(dir, template); } }; diff --git a/src/api/make.js b/src/api/make.js index e53e61589d..296d1c5a70 100644 --- a/src/api/make.js +++ b/src/api/make.js @@ -12,6 +12,7 @@ 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 getElectronVersion from '../util/electron-version'; import packager from './package'; @@ -129,7 +130,7 @@ export default async (providedOptions = {}) => { await runHook(forgeConfig, 'preMake'); - for (const targetArch of parseArchs(platform, arch, packageJSON.devDependencies['electron-prebuilt-compile'])) { + for (const targetArch of parseArchs(platform, arch, getElectronVersion(packageJSON))) { const packageDir = path.resolve(outDir, `${appName}-${actualTargetPlatform}-${targetArch}`); if (!(await fs.pathExists(packageDir))) { throw new Error(`Couldn't find packaged app at: ${packageDir}`); diff --git a/src/api/package.js b/src/api/package.js index 2d57f589af..178543a20f 100644 --- a/src/api/package.js +++ b/src/api/package.js @@ -11,12 +11,12 @@ import getForgeConfig from '../util/forge-config'; import runHook from '../util/hook'; import { warn } from '../util/messages'; import realOra, { fakeOra } from '../util/ora'; -import packagerCompileHook from '../util/compile-hook'; 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'; +import getElectronVersion from '../util/electron-version'; const d = debug('electron-forge:packager'); @@ -102,15 +102,15 @@ export default async (providedOptions = {}) => { prepareCounter += 1; prepareSpinner = 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) => { + }, async (buildPath, electronVersion, pPlatform, pArch, done) => { prepareSpinner.succeed(); - await packagerCompileHook(dir, ...args); + await runHook(forgeConfig, 'packageAfterCopy', buildPath, electronVersion, pPlatform, pArch); + done(); }, ]; @@ -136,6 +136,11 @@ export default async (providedOptions = {}) => { afterPruneHooks.push(...resolveHooks(forgeConfig.electronPackagerConfig.afterPrune, dir)); } + afterPruneHooks.push(async (buildPath, electronVersion, pPlatform, pArch, done) => { + await runHook(forgeConfig, 'packageAfterPrune', buildPath, electronVersion, pPlatform, pArch); + done(); + }); + const packageOpts = Object.assign({ asar: false, overwrite: true, @@ -147,12 +152,9 @@ export default async (providedOptions = {}) => { arch, platform, out: outDir, - electronVersion: packageJSON.devDependencies['electron-prebuilt-compile'], + electronVersion: getElectronVersion(packageJSON), }); packageOpts.quiet = true; - if (typeof packageOpts.asar === 'object' && packageOpts.asar.unpack) { - throw new Error('electron-compile does not support asar.unpack yet. Please use asar.unpackDir'); - } if (!packageJSON.version && !packageOpts.appVersion) { // eslint-disable-next-line max-len diff --git a/src/api/start.js b/src/api/start.js index 3915f4f4dd..e30c6c6696 100644 --- a/src/api/start.js +++ b/src/api/start.js @@ -8,6 +8,7 @@ import rebuild from '../util/rebuild'; import resolveDir from '../util/resolve-dir'; import getForgeConfig from '../util/forge-config'; import runHook from '../util/hook'; +import getElectronVersion from '../util/electron-version'; /** * @typedef {Object} StartOptions @@ -52,7 +53,21 @@ export default async (providedOptions = {}) => { const forgeConfig = await getForgeConfig(dir); - await rebuild(dir, packageJSON.devDependencies['electron-prebuilt-compile'], process.platform, process.arch, forgeConfig.electronRebuildConfig); + await rebuild(dir, getElectronVersion(packageJSON), process.platform, process.arch, forgeConfig.electronRebuildConfig); + + await runHook(forgeConfig, 'generateAssets'); + + // If a plugin has taken over the start command let's stop here + const spawnedPluginChild = await forgeConfig.pluginInterface.overrideStartLogic({ + dir, + appPath, + interactive, + enableLogging, + args, + runAsNode, + inspect, + }); + if (spawnedPluginChild) return spawnedPluginChild; const spawnOpts = { cwd: dir, @@ -75,10 +90,8 @@ export default async (providedOptions = {}) => { let spawned; - await runHook(forgeConfig, 'generateAssets'); - await asyncOra('Launching Application', async () => { - spawned = spawn(process.execPath, [path.resolve(dir, 'node_modules/electron-prebuilt-compile/lib/cli'), appPath].concat(args), spawnOpts); + spawned = spawn(process.execPath, [path.resolve(dir, 'node_modules/electron/cli'), appPath].concat(args), spawnOpts); }); return spawned; diff --git a/src/electron-forge-init.js b/src/electron-forge-init.js index a8143bf86f..94eef629b4 100644 --- a/src/electron-forge-init.js +++ b/src/electron-forge-init.js @@ -10,7 +10,6 @@ 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('-c, --copy-ci-files', 'Whether to copy the templated CI files (defaults to false)', false) .action((name) => { if (!name) return; @@ -25,7 +24,6 @@ import { init } from './api'; const initOpts = { dir, interactive: true, - lintStyle: program.lintStyle, copyCIFiles: !!program.copyCiFiles, }; if (program.template) initOpts.template = program.template; diff --git a/src/init/init-custom.js b/src/init/init-custom.js index 62485ffd7f..eb01e83c47 100644 --- a/src/init/init-custom.js +++ b/src/init/init-custom.js @@ -11,7 +11,7 @@ import ora from '../util/ora'; const d = debug('electron-forge:init:custom'); -export default async (dir, template, lintStyle) => { +export default async (dir, template) => { let templateModulePath; await asyncOra(`Locating custom template: "${template}"`, async () => { try { @@ -57,6 +57,6 @@ export default async (dir, template, lintStyle) => { }); if (typeof templateModule.postCopy === 'function') { - await Promise.resolve(templateModule.postCopy(dir, ora, lintStyle)); + await Promise.resolve(templateModule.postCopy(dir, ora)); } }; diff --git a/src/init/init-npm.js b/src/init/init-npm.js index 19533da6a2..b67237259e 100644 --- a/src/init/init-npm.js +++ b/src/init/init-npm.js @@ -10,31 +10,19 @@ import asyncOra from '../util/ora-handler'; const d = debug('electron-forge:init:npm'); -export const deps = ['electron-compile', 'electron-squirrel-startup']; -export const devDeps = ['babel-preset-env', 'babel-preset-react', 'babel-plugin-transform-async-to-generator', 'electron-forge']; -export const exactDevDeps = ['electron-prebuilt-compile']; -export const standardDeps = ['standard']; -export const airbnbDeps = ['eslint@^3', 'eslint-config-airbnb@^15', 'eslint-plugin-import@^2', - 'eslint-plugin-jsx-a11y@^5', 'eslint-plugin-react@^7']; +export const deps = ['electron-squirrel-startup']; +export const devDeps = ['electron-forge']; +export const exactDevDeps = ['electron']; -export default async (dir, lintStyle) => { +export default async (dir) => { await asyncOra('Initializing NPM Module', async () => { const packageJSON = await readPackageJSON(path.resolve(__dirname, '../../tmpl')); packageJSON.productName = packageJSON.name = path.basename(dir).toLowerCase(); packageJSON.author = await username(); setInitialForgeConfig(packageJSON); - switch (lintStyle) { - case 'standard': - packageJSON.scripts.lint = 'standard'; - break; - case 'airbnb': - packageJSON.scripts.lint = 'eslint src --color'; - break; - default: - packageJSON.scripts.lint = 'echo "No linting configured"'; - break; - } + packageJSON.scripts.lint = 'echo "No linting configured"'; + d('writing package.json to:', dir); await fs.writeJson(path.resolve(dir, 'package.json'), packageJSON, { spaces: 2 }); }); @@ -50,34 +38,5 @@ export default async (dir, lintStyle) => { for (const packageName of exactDevDeps) { await installDepList(dir, [packageName], true, true); } - - switch (lintStyle) { - case 'standard': - d('installing standard linting dependencies'); - await installDepList(dir, standardDeps, true); - break; - case 'airbnb': - d('installing airbnb linting dependencies'); - await installDepList(dir, airbnbDeps, true); - break; - default: - d('not installing linting deps'); - break; - } - - // NB: For babel-preset-env to work correctly, it needs to know the - // actual version of Electron that we installed - const content = await fs.readJson(path.join(dir, '.compilerc'), 'utf8'); - const electronPrebuilt = require( - path.join(dir, 'node_modules', 'electron-prebuilt-compile', 'package.json')); - - for (const profile of ['development', 'production']) { - const envTarget = content.env[profile]['application/javascript'].presets.find(x => x[0] === 'env'); - // parseFloat strips the patch version - // parseFloat('1.3.2') === 1.3 - envTarget[1].targets.electron = parseFloat(electronPrebuilt.version).toString(); - } - - await fs.writeJson(path.join(dir, '.compilerc'), content, { spaces: 2 }); }); }; diff --git a/src/init/init-standard-fix.js b/src/init/init-standard-fix.js deleted file mode 100644 index 7b5c8f2345..0000000000 --- a/src/init/init-standard-fix.js +++ /dev/null @@ -1,24 +0,0 @@ -import debug from 'debug'; -import { yarnOrNpmSpawn } from '../util/yarn-or-npm'; - -import asyncOra from '../util/ora-handler'; - -const d = debug('electron-forge:init:standard-fix'); - -const run = async (dir) => { - try { - await yarnOrNpmSpawn(['run', 'lint', '--', '--fix'], { - stdio: 'inherit', - cwd: dir, - }); - } catch (err) { - throw new Error(`Failed to fix JS to standard style (${err.message})`); - } -}; - -export default async (dir) => { - await asyncOra('Applying Standard Style to JS', async () => { - d('executing "standard --fix" in:', dir); - await run(dir); - }); -}; diff --git a/src/init/init-starter-files.js b/src/init/init-starter-files.js index 26240d9840..e54e662b0c 100644 --- a/src/init/init-starter-files.js +++ b/src/init/init-starter-files.js @@ -11,15 +11,14 @@ export const copy = async (source, target) => { await fs.copy(source, target); }; -export default async (dir, { lintStyle, copyCIFiles }) => { +export default async (dir, { 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']; + const rootFiles = ['_gitignore']; if (copyCIFiles) rootFiles.push(...['_travis.yml', '_appveyor.yml']); - if (lintStyle === 'airbnb') rootFiles.push('_eslintrc'); const srcFiles = ['index.js', 'index.html']; for (const file of rootFiles) { diff --git a/src/util/compile-hook.js b/src/util/compile-hook.js deleted file mode 100644 index 2b9ac9d67a..0000000000 --- a/src/util/compile-hook.js +++ /dev/null @@ -1,40 +0,0 @@ -import fs from 'fs-extra'; -import path from 'path'; - -import asyncOra from './ora-handler'; -import readPackageJSON from './read-package-json'; - -export default async(originalDir, buildPath, electronVersion, pPlatform, pArch, done) => { - await asyncOra('Compiling Application', async () => { - const compileCLI = require(path.resolve(originalDir, 'node_modules/electron-compile/lib/cli.js')); - - async function compileAndShim(appDir) { - for (const entry of await fs.readdir(appDir)) { - if (!entry.match(/^(node_modules|bower_components)$/)) { - const fullPath = path.join(appDir, entry); - - if ((await fs.stat(fullPath)).isDirectory()) { - const log = console.log; - console.log = () => {}; - await compileCLI.main(appDir, [fullPath]); - console.log = log; - } - } - } - - const packageJSON = await readPackageJSON(appDir); - - const index = packageJSON.main || 'index.js'; - packageJSON.originalMain = index; - packageJSON.main = 'es6-shim.js'; - - await fs.writeFile(path.join(appDir, 'es6-shim.js'), - await fs.readFile(path.join(path.resolve(originalDir, 'node_modules/electron-compile/lib/es6-shim.js')), 'utf8')); - - await fs.writeJson(path.join(appDir, 'package.json'), packageJSON, { spaces: 2 }); - } - - await compileAndShim(buildPath); - }); - done(); -}; diff --git a/src/util/electron-version.js b/src/util/electron-version.js new file mode 100644 index 0000000000..7e20cc6cd7 --- /dev/null +++ b/src/util/electron-version.js @@ -0,0 +1,6 @@ +export default (packageJSON) => { + if (!packageJSON.devDependencies) return null; + return (packageJSON.devDependencies['electron-prebuilt-compile'] + || packageJSON.devDependencies['electron-prebuilt'] + || packageJSON.devDependencies.electron); +}; diff --git a/src/util/forge-config.js b/src/util/forge-config.js index 74c998f528..120ccc212f 100644 --- a/src/util/forge-config.js +++ b/src/util/forge-config.js @@ -3,6 +3,7 @@ import path from 'path'; import _template from 'lodash.template'; import readPackageJSON from './read-package-json'; import yarnOrNpm from './yarn-or-npm'; +import PluginInterface from './plugin-interface'; const underscoreCase = str => str.replace(/(.)([A-Z][a-z]+)/g, '$1_$2').replace(/([a-z0-9])([A-Z])/g, '$1_$2').toUpperCase(); @@ -10,7 +11,7 @@ const proxify = (object, envPrefix) => { const newObject = {}; Object.keys(object).forEach((key) => { - if (typeof object[key] === 'object' && !Array.isArray(object[key])) { + if (typeof object[key] === 'object' && !Array.isArray(object[key]) && key !== 'pluginInterface') { newObject[key] = proxify(object[key], `${envPrefix}_${underscoreCase(key)}`); } else { newObject[key] = object[key]; @@ -76,6 +77,7 @@ export default async (dir) => { s3: {}, github_repository: {}, electronReleaseServer: {}, + plugins: [], }, forgeConfig); forgeConfig.make_targets = Object.assign({ win32: ['squirrel'], @@ -106,5 +108,7 @@ export default async (dir) => { template(forgeConfig); + forgeConfig.pluginInterface = new PluginInterface(dir, forgeConfig); + return proxify(forgeConfig, 'ELECTRON_FORGE'); }; diff --git a/src/util/hook.js b/src/util/hook.js index c053914dd4..255b5f56bc 100644 --- a/src/util/hook.js +++ b/src/util/hook.js @@ -4,10 +4,10 @@ const d = debug('electron-forge:hook'); export default async (forgeConfig, hookName, ...hookArgs) => { const hooks = forgeConfig.hooks || {}; + d(`hook triggered: ${hookName}`); if (typeof hooks[hookName] === 'function') { d('calling hook:', hookName, 'with args:', hookArgs); await hooks[hookName](forgeConfig, ...hookArgs); - } else { - d('could not find hook:', hookName); } + await forgeConfig.pluginInterface.triggerHook(hookName, hookArgs); }; diff --git a/src/util/plugin-interface.js b/src/util/plugin-interface.js new file mode 100644 index 0000000000..42f19db614 --- /dev/null +++ b/src/util/plugin-interface.js @@ -0,0 +1,50 @@ +import debug from 'debug'; + +import asyncOra from './ora-handler'; + +const d = debug('electron-forge:plugins'); + +export default class PluginInterface { + constructor(dir, forgeConfig) { + this.plugins = forgeConfig.plugins; + Object.defineProperty(this, 'config', { + value: forgeConfig, + enumerable: false, + configurable: false, + writable: false, + }); + + for (const plugin of this.plugins) { + plugin.init(dir, forgeConfig, asyncOra); + } + + this.triggerHook = this.triggerHook.bind(this); + this.overrideStartLogic = this.overrideStartLogic.bind(this); + } + + async triggerHook(hookName, hookArgs) { + for (const plugin of this.plugins) { + if (typeof plugin.getHook === 'function') { + const hook = plugin.getHook(hookName); + if (hook) await hook(...hookArgs); + } + } + } + + async overrideStartLogic(opts) { + let newStartFn; + const claimed = []; + for (const plugin of this.plugins) { + if (typeof plugin.startLogic === 'function') { + claimed.push(plugin.name); + newStartFn = plugin.startLogic; + } + } + if (claimed.length > 1) throw `Multiple plugins tried to take control of the start command, please remove one of them\n --> ${claimed.join(', ')}`; + if (claimed.length === 1) { + d(`plugin: "${claimed[0]}" has taken control of the start command`); + return await newStartFn(opts); + } + return false; + } +} diff --git a/src/util/resolve-dir.js b/src/util/resolve-dir.js index cd9ae871cd..66a70ba157 100644 --- a/src/util/resolve-dir.js +++ b/src/util/resolve-dir.js @@ -2,6 +2,7 @@ import debug from 'debug'; import fs from 'fs-extra'; import path from 'path'; import readPackageJSON from './read-package-json'; +import getElectronVersion from './electron-version'; const d = debug('electron-forge:project-resolver'); @@ -15,12 +16,13 @@ export default async (dir) => { if (await fs.pathExists(testPath)) { const packageJSON = await readPackageJSON(mDir); - if (packageJSON.devDependencies && packageJSON.devDependencies['electron-prebuilt-compile']) { - if (!/[0-9]/.test(packageJSON.devDependencies['electron-prebuilt-compile'][0])) { - throw 'You must depend on an EXACT version of "electron-prebuilt-compile" not a range'; + const electronVersion = getElectronVersion(packageJSON); + if (electronVersion) { + if (!/[0-9]/.test(electronVersion[0])) { + throw `You must depend on an EXACT version of electron not a range (${electronVersion})`; } } else { - throw 'You must depend on "electron-prebuilt-compile" in your devDependencies'; + throw 'You must depend on "electron" in your devDependencies'; } if (packageJSON.config && packageJSON.config.forge) { diff --git a/test/fast/forge-config_spec.js b/test/fast/forge-config_spec.js index 939a99ba1d..317650fa79 100644 --- a/test/fast/forge-config_spec.js +++ b/test/fast/forge-config_spec.js @@ -25,11 +25,14 @@ const defaults = { github_repository: {}, s3: {}, electronReleaseServer: {}, + plugins: [], }; describe('forge-config', () => { it('should resolve the object in package.json with defaults if one exists', async () => { - expect(await findConfig(path.resolve(__dirname, '../fixture/dummy_app'))).to.be.deep.equal(Object.assign({}, defaults, { + const config = await findConfig(path.resolve(__dirname, '../fixture/dummy_app')); + delete config.pluginInterface; + expect(config).to.be.deep.equal(Object.assign({}, defaults, { electronWinstallerConfig: { windows: 'magic' }, windowsStoreConfig: { packageName: 'test' }, github_repository: { @@ -39,6 +42,11 @@ describe('forge-config', () => { })); }); + it('should set a pluginInterface', async () => { + const config = await findConfig(path.resolve(__dirname, '../fixture/dummy_app')); + expect(config).to.have.property('pluginInterface'); + }); + it('should allow access to built-ins of proxied objects', async () => { const conf = await findConfig(path.resolve(__dirname, '../fixture/dummy_js_conf')); expect(conf.electronPackagerConfig.baz.hasOwnProperty).to.be.a('function'); @@ -63,7 +71,9 @@ describe('forge-config', () => { it('should resolve the JS file exports in config.forge points to a JS file', async () => { - expect(JSON.parse(JSON.stringify(await findConfig(path.resolve(__dirname, '../fixture/dummy_js_conf'))))).to.be.deep.equal(Object.assign({}, defaults, { + const config = JSON.parse(JSON.stringify(await findConfig(path.resolve(__dirname, '../fixture/dummy_js_conf')))); + delete config.pluginInterface; + expect(config).to.be.deep.equal(Object.assign({}, defaults, { electronPackagerConfig: { foo: 'bar', baz: {} }, })); }); diff --git a/test/fast/hook_spec.js b/test/fast/hook_spec.js index 6d231e7126..9484fb18c7 100644 --- a/test/fast/hook_spec.js +++ b/test/fast/hook_spec.js @@ -3,19 +3,25 @@ import { stub } from 'sinon'; import runHook from '../../src/util/hook'; +const fakeConfig = { + pluginInterface: { + triggerHook: async () => false, + }, +}; + describe('runHook', () => { it('should not error when running non existent hooks', async () => { - await runHook({}, 'magic'); + await runHook(Object.assign({}, fakeConfig), 'magic'); }); it('should not error when running a hook that is not a function', async () => { - await runHook({ hooks: { myHook: 'abc' } }, 'abc'); + await runHook(Object.assign({ hooks: { myHook: 'abc' } }, fakeConfig), 'abc'); }); it('should run the hook if it is provided as a function', async () => { const myStub = stub(); myStub.returns(Promise.resolve()); - await runHook({ hooks: { myHook: myStub } }, 'myHook'); + await runHook(Object.assign({ hooks: { myHook: myStub } }, fakeConfig), 'myHook'); expect(myStub.callCount).to.equal(1); }); }); diff --git a/test/fast/publish_spec.js b/test/fast/publish_spec.js index 503c6a7a38..67a4f6f3bc 100644 --- a/test/fast/publish_spec.js +++ b/test/fast/publish_spec.js @@ -52,11 +52,16 @@ describe('publish', () => { tag: 'my_special_tag', }); expect(publisherSpy.callCount).to.equal(1); + // pluginInterface will be a new instance so we ignore it + delete publisherSpy.firstCall.args[0].forgeConfig.pluginInterface; + const testConfig = await require('../../src/util/forge-config').default(path.resolve(__dirname, '../fixture/dummy_app')); + + delete testConfig.pluginInterface; expect(publisherSpy.firstCall.args).to.deep.equal([{ dir: resolveStub(), artifacts: ['artifact1', 'artifact2'], packageJSON: require('../fixture/dummy_app/package.json'), - forgeConfig: await require('../../src/util/forge-config').default(path.resolve(__dirname, '../fixture/dummy_app')), + forgeConfig: testConfig, authToken: 'my_token', tag: 'my_special_tag', platform: process.platform, diff --git a/test/fast/start_spec.js b/test/fast/start_spec.js index 7433e9b626..c6094bb4e0 100644 --- a/test/fast/start_spec.js +++ b/test/fast/start_spec.js @@ -11,14 +11,21 @@ describe('start', () => { let packageJSON; let resolveStub; let spawnStub; + let shouldOverride; beforeEach(() => { resolveStub = sinon.stub(); spawnStub = sinon.stub(); + shouldOverride = false; packageJSON = require('../fixture/dummy_app/package.json'); start = proxyquire.noCallThru().load('../../src/api/start', { - '../util/forge-config': async () => ({}), + '../util/forge-config': async () => ({ + pluginInterface: { + overrideStartLogic: async () => shouldOverride, + triggerHook: async () => false, + }, + }), '../util/resolve-dir': async dir => resolveStub(dir), '../util/read-package-json': () => Promise.resolve(packageJSON), '../util/rebuild': () => Promise.resolve(), @@ -36,11 +43,21 @@ describe('start', () => { }); expect(spawnStub.callCount).to.equal(1); expect(spawnStub.firstCall.args[0]).to.equal(process.execPath); - expect(spawnStub.firstCall.args[1][0]).to.contain(`electron-prebuilt-compile${path.sep}lib${path.sep}cli`); + expect(spawnStub.firstCall.args[1][0]).to.contain(`electron${path.sep}cli`); expect(spawnStub.firstCall.args[2]).to.have.property('cwd', __dirname); expect(spawnStub.firstCall.args[2].env).to.not.have.property('ELECTRON_ENABLE_LOGGING'); }); + it('should not spawn if a plugin overrides the start command', async () => { + resolveStub.returnsArg(0); + shouldOverride = true; + await start({ + dir: __dirname, + interactive: false, + }); + expect(spawnStub.callCount).to.equal(0); + }); + it("should pass electron '.' as the app path if not specified", async () => { resolveStub.returnsArg(0); await start({ @@ -48,7 +65,7 @@ describe('start', () => { }); expect(spawnStub.callCount).to.equal(1); expect(spawnStub.firstCall.args[0]).to.equal(process.execPath); - expect(spawnStub.firstCall.args[1][0]).to.contain(`electron-prebuilt-compile${path.sep}lib${path.sep}cli`); + expect(spawnStub.firstCall.args[1][0]).to.contain(`electron${path.sep}cli`); expect(spawnStub.firstCall.args[1][1]).to.equal('.'); }); @@ -60,7 +77,7 @@ describe('start', () => { }); expect(spawnStub.callCount).to.equal(1); expect(spawnStub.firstCall.args[0]).to.equal(process.execPath); - expect(spawnStub.firstCall.args[1][0]).to.contain(`electron-prebuilt-compile${path.sep}lib${path.sep}cli`); + expect(spawnStub.firstCall.args[1][0]).to.contain(`electron${path.sep}cli`); expect(spawnStub.firstCall.args[1][1]).to.equal('/path/to/app.js'); }); @@ -73,7 +90,7 @@ describe('start', () => { }); expect(spawnStub.callCount).to.equal(1); expect(spawnStub.firstCall.args[0]).to.equal(process.execPath); - expect(spawnStub.firstCall.args[1][0]).to.contain(`electron-prebuilt-compile${path.sep}lib${path.sep}cli`); + expect(spawnStub.firstCall.args[1][0]).to.contain(`electron${path.sep}cli`); expect(spawnStub.firstCall.args[2].env).to.have.property('ELECTRON_ENABLE_LOGGING', true); }); diff --git a/test/fixture/dummy_app/package.json b/test/fixture/dummy_app/package.json index 6e533c7d95..7d60780d49 100644 --- a/test/fixture/dummy_app/package.json +++ b/test/fixture/dummy_app/package.json @@ -29,6 +29,6 @@ } }, "devDependencies": { - "electron-prebuilt-compile": "9.9.9" + "electron": "9.9.9" } } diff --git a/test/fixture/dummy_js_conf/package.json b/test/fixture/dummy_js_conf/package.json index 522b2b0b49..ebd70b7b87 100644 --- a/test/fixture/dummy_js_conf/package.json +++ b/test/fixture/dummy_js_conf/package.json @@ -14,6 +14,6 @@ "forge": "./forge.config.js" }, "devDependencies": { - "electron-prebuilt-compile": "9.9.9" + "electron-prebuilt": "9.9.9" } } diff --git a/test/slow/api_spec_slow.js b/test/slow/api_spec_slow.js index e5b696fdbc..4e742b62a1 100644 --- a/test/slow/api_spec_slow.js +++ b/test/slow/api_spec_slow.js @@ -42,44 +42,31 @@ describe(`electron-forge API (with installer=${nodeInstaller})`, () => { expect(await fs.pathExists(path.resolve(dir, subPath)), `the ${subPath} ${pathType} should exist`).to.equal(true); }; - const forLintingMethod = (lintStyle) => { - describe(`init (with lintStyle=${lintStyle})`, () => { - beforeInitTest({ lintStyle }); + describe('init', () => { + beforeInitTest(); - 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); - expectProjectPathExists('package.json', 'file'); - }); - - it('should have initialized a git repository', async () => { - expectProjectPathExists('.git', 'folder'); - }); - - it('should have installed the initial node_modules', async () => { - 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 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); + expectProjectPathExists('package.json', 'file'); + }); - it('should have set the .compilerc electron version to be a string', async () => { - expectProjectPathExists('.compilerc', 'file'); - const compilerc = await fs.readJson(path.resolve(dir, '.compilerc')); - const electronVersion = compilerc.env.development['application/javascript'].presets[0][1].targets.electron; - expect(electronVersion).to.be.a('string'); - expect(electronVersion.split('.').length).to.equal(2); - }); + it('should have initialized a git repository', async () => { + expectProjectPathExists('.git', 'folder'); + }); - describe('lint', () => { - it('should initially pass the linting process', () => forge.lint({ dir })); - }); + it('should have installed the initial node_modules', async () => { + expectProjectPathExists('node_modules', 'folder'); + expect(await fs.pathExists(path.resolve(dir, 'node_modules/electron')), 'electron 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); + }); - after(() => fs.remove(dir)); + describe('lint', () => { + it('should initially pass the linting process', () => forge.lint({ dir })); }); - }; - forLintingMethod('airbnb'); - forLintingMethod('standard'); + after(() => fs.remove(dir)); + }); describe('init with CI files enabled', () => { beforeInitTest({ copyCIFiles: true }); @@ -267,16 +254,6 @@ describe(`electron-forge API (with installer=${nodeInstaller})`, () => { await fs.remove(path.resolve(dir, 'out')); }); - it('throws an error when asar.unpack is set', async () => { - let packageJSON = await readPackageJSON(dir); - packageJSON.config.forge.electronPackagerConfig.asar = { unpack: 'somedir/**' }; - await fs.writeJson(path.join(dir, 'package.json'), packageJSON); - await expect(forge.package({ dir })).to.eventually.be.rejectedWith(/electron-compile does not support asar\.unpack/); - packageJSON = await readPackageJSON(dir); - delete packageJSON.config.forge.electronPackagerConfig.asar; - await fs.writeJson(path.join(dir, 'package.json'), packageJSON); - }); - it('can package without errors with native pre-gyp deps installed', async () => { await installDeps(dir, ['ref']); await forge.package({ dir }); diff --git a/tmpl/_compilerc b/tmpl/_compilerc deleted file mode 100644 index 6c1a6c295c..0000000000 --- a/tmpl/_compilerc +++ /dev/null @@ -1,24 +0,0 @@ -{ - "env": { - "development": { - "application/javascript": { - "presets": [ - ["env", { "targets": { "electron": "1.4" } }], - "react" - ], - "plugins": ["transform-async-to-generator"], - "sourceMaps": "inline" - } - }, - "production": { - "application/javascript": { - "presets": [ - ["env", { "targets": { "electron": "1.4" } }], - "react" - ], - "plugins": ["transform-async-to-generator"], - "sourceMaps": "none" - } - } - } -} diff --git a/tmpl/_eslintrc b/tmpl/_eslintrc deleted file mode 100644 index 64f12522ef..0000000000 --- a/tmpl/_eslintrc +++ /dev/null @@ -1,9 +0,0 @@ -{ - "extends": "eslint-config-airbnb", - "rules": { - "import/extensions": 0, - "import/no-extraneous-dependencies": 0, - "import/no-unresolved": [2, { "ignore": ["electron"] }], - "linebreak-style": 0 - } -} diff --git a/tmpl/index.js b/tmpl/index.js index c3cea4e252..94a7e01e31 100644 --- a/tmpl/index.js +++ b/tmpl/index.js @@ -1,4 +1,4 @@ -import { app, BrowserWindow } from 'electron'; +const { app, BrowserWindow } = require('electron'); // Handle creating/removing shortcuts on Windows when installing/uninstalling. if (require('electron-squirrel-startup')) { // eslint-disable-line global-require