diff --git a/packages/api/core/src/api/import.ts b/packages/api/core/src/api/import.ts index fc131b0b1c..9dfdc8e074 100644 --- a/packages/api/core/src/api/import.ts +++ b/packages/api/core/src/api/import.ts @@ -1,3 +1,4 @@ +import _merge from 'lodash.merge'; import { asyncOra } from '@electron-forge/async-ora'; import debug from 'debug'; import fs from 'fs-extra'; @@ -10,9 +11,29 @@ import { setInitialForgeConfig } from '../util/forge-config'; import { info, warn } from '../util/messages'; import installDepList, { DepType, DepVersionRestriction } from '../util/install-dependencies'; import { readRawPackageJson } from '../util/read-package-json'; +import upgradeForgeConfig, { updateUpgradedForgeDevDeps } from'../util/upgrade-forge-config'; const d = debug('electron-forge:import'); +function findElectronDep(dep: string): boolean { + return ['electron', 'electron-prebuilt', 'electron-prebuilt-compile'].includes(dep); +} + +function updateElectronDependency(packageJSON: any, dev: string[], exact: string[]): [string[], string[]] { + if (Object.keys(packageJSON.devDependencies).find(findElectronDep)) { + exact = exact.filter(dep => dep !== 'electron'); + } else { + const electronKey = Object.keys(packageJSON.dependencies).find(findElectronDep); + if (electronKey) { + d(`Moving ${electronKey} from dependencies to devDependencies`); + dev.push(`${electronKey}@${packageJSON.dependencies[electronKey]}`); + delete packageJSON.dependencies[electronKey]; + } + } + + return [dev, exact]; +} + export interface ImportOptions { /** * The path to the app to be imported @@ -72,25 +93,40 @@ export default async ({ await initGit(dir); + const importDeps = ([] as string[]).concat(deps); + let importDevDeps = ([] as string[]).concat(devDeps); + let importExactDevDeps = ([] as string[]).concat(exactDevDeps); + let packageJSON = await readRawPackageJson(dir); if (packageJSON.config && packageJSON.config.forge) { - warn(interactive, 'It looks like this project is already configured for "electron-forge"'.green); - if (typeof shouldContinueOnExisting === 'function') { - if (!await shouldContinueOnExisting()) { - process.exit(0); + if (packageJSON.config.forge.makers) { + warn(interactive, 'It looks like this project is already configured for Electron Forge'.green); + if (typeof shouldContinueOnExisting === 'function') { + if (!await shouldContinueOnExisting()) { + process.exit(0); + } } + } else if (typeof packageJSON.config.forge === 'string') { + warn(interactive, "We can't tell if the Electron Forge config is compatible because it's in an external JavaScript file, not trying to convert it and continuing anyway".yellow); + } else { + d('Upgrading an Electron Forge < 6 project'); + packageJSON.config.forge = upgradeForgeConfig(packageJSON.config.forge); + importDevDeps = updateUpgradedForgeDevDeps(packageJSON, importDevDeps); } } packageJSON.dependencies = packageJSON.dependencies || {}; packageJSON.devDependencies = packageJSON.devDependencies || {}; + [importDevDeps, importExactDevDeps] = updateElectronDependency(packageJSON, importDevDeps, importExactDevDeps); + const keys = Object.keys(packageJSON.dependencies).concat(Object.keys(packageJSON.devDependencies)); const buildToolPackages: { [key: string]: string | undefined; } = { 'electron-builder': 'provides mostly equivalent functionality', 'electron-download': 'already uses this module as a transitive dependency', + 'electron-forge': 'replaced with @electron-forge/cli', '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', @@ -152,13 +188,13 @@ export default async ({ await fs.remove(path.resolve(dir, 'node_modules/.bin/electron.cmd')); d('installing dependencies'); - await installDepList(dir, deps); + await installDepList(dir, importDeps); d('installing devDependencies'); - await installDepList(dir, devDeps, DepType.DEV); + await installDepList(dir, importDevDeps, DepType.DEV); d('installing exactDevDependencies'); - await installDepList(dir, exactDevDeps, DepType.DEV, DepVersionRestriction.EXACT); + await installDepList(dir, importExactDevDeps, DepType.DEV, DepVersionRestriction.EXACT); }); packageJSON = await readRawPackageJson(dir); @@ -169,8 +205,17 @@ export default async ({ packageJSON.config = packageJSON.config || {}; const templatePackageJSON = await readRawPackageJson(path.resolve(__dirname, '../../tmpl')); - packageJSON.config.forge = templatePackageJSON.config.forge; - setInitialForgeConfig(packageJSON); + if (packageJSON.config.forge) { + if (typeof packageJSON.config.forge !== 'string') { + packageJSON.config.forge = _merge(templatePackageJSON.config.forge, packageJSON.config.forge); + } + } else { + packageJSON.config.forge = templatePackageJSON.config.forge; + } + + if (typeof packageJSON.config.forge !== 'string') { + setInitialForgeConfig(packageJSON); + } await writeChanges(); diff --git a/packages/api/core/src/api/init-scripts/init-npm.ts b/packages/api/core/src/api/init-scripts/init-npm.ts index d9cfb60f0f..03cb23e0f3 100644 --- a/packages/api/core/src/api/init-scripts/init-npm.ts +++ b/packages/api/core/src/api/init-scripts/init-npm.ts @@ -11,7 +11,7 @@ import { readRawPackageJson } from '../../util/read-package-json'; const d = debug('electron-forge:init:npm'); const corePackage = fs.readJsonSync(path.resolve(__dirname, '../../../package.json')); -function siblingDep(name: string) { +export function siblingDep(name: string) { return `@electron-forge/${name}@${corePackage.version}`; } diff --git a/packages/api/core/src/util/upgrade-forge-config.ts b/packages/api/core/src/util/upgrade-forge-config.ts new file mode 100644 index 0000000000..174b280f75 --- /dev/null +++ b/packages/api/core/src/util/upgrade-forge-config.ts @@ -0,0 +1,142 @@ +import { + ForgeConfig, + ForgePlatform, + IForgeResolvableMaker, + IForgeResolvablePublisher, +} from '@electron-forge/shared-types'; +import path from 'path'; +import { siblingDep } from '../api/init-scripts/init-npm'; + +function mapMakeTargets(forge5Config: any): Map { + const makeTargets = new Map(); + if (forge5Config.makeTargets) { + // TODO: Use object.entries when dropping Node 6 + for (const platform in forge5Config.makeTargets) { + for (const target of forge5Config.makeTargets[platform]) { + let platforms = makeTargets.get(target); + if (platforms === undefined) { + platforms = []; + makeTargets.set(target, platforms); + } + platforms.push(platform as ForgePlatform); + } + } + } + + return makeTargets; +} + +const forge5MakerMappings = new Map([ + ['electronInstallerDebian', 'deb'], + ['electronInstallerDMG', 'dmg'], + ['electronInstallerFlatpak', 'flatpak'], + ['electronInstallerRedhat', 'rpm'], + ['electronInstallerSnap', 'snap'], + ['electronWinstallerConfig', 'squirrel'], + ['electronWixMSIConfig', 'wix'], + ['windowsStoreConfig', 'appx'], +]); + +/** + * Converts Forge v5 maker config to v6. + */ +function generateForgeMakerConfig(forge5Config: any): IForgeResolvableMaker[] { + const makeTargets = mapMakeTargets(forge5Config); + const makers: IForgeResolvableMaker[] = []; + + for (const [forge5Key, makerType] of forge5MakerMappings) { + const config = forge5Config[forge5Key]; + if (config) { + makers.push({ + name: `@electron-forge/maker-${makerType}`, + config: forge5Config[forge5Key], + platforms: makeTargets.get(makerType) || null, + } as IForgeResolvableMaker); + } + } + + const zipPlatforms = makeTargets.get('zip'); + if (zipPlatforms) { + makers.push({ + name: '@electron-forge/maker-zip', + platforms: zipPlatforms, + } as IForgeResolvableMaker); + } + + return makers; +} + +const forge5PublisherMappings = new Map([ + ['github_repository', 'github'], + ['s3', 's3'], + ['electron-release-server', 'electron-release-server'], + ['snapStore', 'snapcraft'], +]); + +/** + * Converts Forge v5 publisher config to v6. + */ +function generateForgePublisherConfig(forge5Config: any): IForgeResolvablePublisher[] { + const publishers: IForgeResolvablePublisher[] = []; + + for (const [forge5Key, publisherType] of forge5PublisherMappings) { + let config = forge5Config[forge5Key]; + if (config) { + if (publisherType === 'github') { + config = transformGitHubPublisherConfig(config); + } + publishers.push({ + config, + name: `@electron-forge/publisher-${publisherType}`, + platforms: null, + } as IForgeResolvableMaker); + } + } + + return publishers; +} + +/** + * Transforms v5 GitHub publisher config to v6 syntax. + */ +function transformGitHubPublisherConfig(config: any) { + const { name, owner, options, ...gitHubConfig } = config; + gitHubConfig.repository = { name, owner }; + if (options) { + gitHubConfig.octokitOptions = options; + } + + return gitHubConfig; +} + +/** + * Upgrades Forge v5 config to v6. + */ +export default function upgradeForgeConfig(forge5Config: any): ForgeConfig { + const forgeConfig: ForgeConfig = ({} as ForgeConfig); + + if (forge5Config.electronPackagerConfig) { + delete forge5Config.electronPackagerConfig.packageManager; + forgeConfig.packagerConfig = forge5Config.electronPackagerConfig; + } + if (forge5Config.electronRebuildConfig) { + forgeConfig.electronRebuildConfig = forge5Config.electronRebuildConfig; + } + forgeConfig.makers = generateForgeMakerConfig(forge5Config); + forgeConfig.publishers = generateForgePublisherConfig(forge5Config); + + return forgeConfig; +} + +export function updateUpgradedForgeDevDeps(packageJSON: any, devDeps: string[]): string[] { + const forgeConfig = packageJSON.config.forge; + devDeps = devDeps.filter(dep => !dep.startsWith('@electron-forge/maker-')); + devDeps = devDeps.concat(forgeConfig.makers.map((maker: IForgeResolvableMaker) => siblingDep(path.basename(maker.name)))); + devDeps = devDeps.concat(forgeConfig.publishers.map((publisher: IForgeResolvablePublisher) => siblingDep(path.basename(publisher.name)))); + + if (Object.keys(packageJSON.devDependencies).find((dep: string) => dep === 'electron-prebuilt-compile')) { + devDeps = devDeps.concat(siblingDep('plugin-compile')); + } + + return devDeps; +} diff --git a/packages/api/core/test/fast/upgrade-forge-config_spec.ts b/packages/api/core/test/fast/upgrade-forge-config_spec.ts new file mode 100644 index 0000000000..55c766437b --- /dev/null +++ b/packages/api/core/test/fast/upgrade-forge-config_spec.ts @@ -0,0 +1,173 @@ +import _merge from 'lodash.merge'; +import { IForgeResolvableMaker, IForgeResolvablePublisher } from '@electron-forge/shared-types'; +import { expect } from 'chai'; +import path from 'path'; + +import upgradeForgeConfig, { updateUpgradedForgeDevDeps } from '../../src/util/upgrade-forge-config'; + +describe('upgradeForgeConfig', () => { + it('converts Electron Packager config', () => { + const oldConfig = { + electronPackagerConfig: { + asar: true, + packageManager: 'npm', + }, + }; + const expected = { asar: true }; + + const newConfig = upgradeForgeConfig(oldConfig); + expect(newConfig.packagerConfig).to.deep.equal(expected); + }); + + it('converts electron-rebuild config', () => { + const rebuildConfig = { types: ['prod'] }; + const oldConfig = { electronRebuildConfig: _merge({}, rebuildConfig) }; + + const newConfig = upgradeForgeConfig(oldConfig); + expect(newConfig.electronRebuildConfig).to.deep.equal(rebuildConfig); + }); + + it('converts maker config', () => { + const oldConfig = { + makeTargets: { + linux: ['deb'], + }, + electronInstallerDebian: { + depends: ['liboath0'], + }, + }; + const expected = [ + { + name: '@electron-forge/maker-deb', + config: { + depends: ['liboath0'], + }, + platforms: ['linux'], + }, + ] as IForgeResolvableMaker[]; + + const newConfig = upgradeForgeConfig(oldConfig); + expect(newConfig.makers).to.deep.equal(expected); + }); + + it('adds the zip maker when specified in makeTargets', () => { + const oldConfig = { + makeTargets: { + darwin: ['zip'], + linux: ['zip'], + }, + }; + const expected = [ + { + name: '@electron-forge/maker-zip', + platforms: ['darwin', 'linux'], + }, + ] as IForgeResolvableMaker[]; + + const newConfig = upgradeForgeConfig(oldConfig); + expect(newConfig.makers).to.deep.equal(expected); + }); + + it('converts publisher config', () => { + const oldConfig = { + snapStore: { + release: 'beta', + }, + }; + const expected = [ + { + name: '@electron-forge/publisher-snapcraft', + config: { + release: 'beta', + }, + platforms: null, + }, + ] as IForgeResolvablePublisher[]; + + const newConfig = upgradeForgeConfig(oldConfig); + expect(newConfig.publishers).to.deep.equal(expected); + }); + + it('converts GitHub publisher config', () => { + const octokitOptions = { + timeout: 0, + }; + const repo = { + name: 'myapp', + owner: 'user', + }; + const oldConfig = { + github_repository: Object.assign({ + options: octokitOptions, + draft: true, + }, repo), + }; + const newConfig = upgradeForgeConfig(oldConfig); + expect(newConfig.publishers).to.have.lengthOf(1); + const publisherConfig = (newConfig.publishers[0] as IForgeResolvablePublisher).config; + expect(publisherConfig.repository).to.deep.equal(repo); + expect(publisherConfig.octokitOptions).to.deep.equal(octokitOptions); + expect(publisherConfig.draft).to.equal(true); + }); +}); + +describe('updateUpgradedForgeDevDeps', () => { + const skeletonPackageJSON = { + config: { + forge: { + makers: [] as IForgeResolvableMaker[], + publishers: [] as IForgeResolvablePublisher[], + }, + }, + devDependencies: {}, + }; + + it('removes unused makers from devDependencies', () => { + const packageJSON = _merge({}, skeletonPackageJSON); + const devDeps = updateUpgradedForgeDevDeps(packageJSON, ['@electron-forge/maker-squirrel']); + expect(devDeps).to.deep.equal([]); + }); + + it('adds makers to devDependencies', () => { + const packageJSON = _merge({}, skeletonPackageJSON); + packageJSON.config.forge.makers = [ + { + name: '@electron-forge/maker-zip', + platforms: ['darwin', 'linux'], + }, + { + name: '@electron-forge/maker-squirrel', + config: {}, + platforms: ['win32'], + }, + ] as IForgeResolvableMaker[]; + + const actual = updateUpgradedForgeDevDeps(packageJSON, []); + expect(actual).to.have.lengthOf(2); + expect(actual.find(dep => dep.startsWith('@electron-forge/maker-zip'))).to.not.equal(undefined); + expect(actual.find(dep => dep.startsWith('@electron-forge/maker-squirrel'))).to.not.equal(undefined); + }); + + it('adds publishers to devDependencies', () => { + const packageJSON = _merge({}, skeletonPackageJSON); + packageJSON.config.forge.publishers = [ + { name: '@electron-forge/publisher-github' }, + { name: '@electron-forge/publisher-snapcraft' }, + ]; + + const actual = updateUpgradedForgeDevDeps(packageJSON, []); + expect(actual).to.have.lengthOf(2); + expect(actual.find(dep => dep.startsWith('@electron-forge/publisher-github'))).to.not.equal(undefined); + expect(actual.find(dep => dep.startsWith('@electron-forge/publisher-snapcraft'))).to.not.equal(undefined); + }); + + it('adds electron-compile plugin to devDependencies when electron-prebuilt-compile is in devDependencies', () => { + const packageJSON = _merge({}, skeletonPackageJSON, { + devDependencies: { 'electron-prebuilt-compile': '2.0.0' }, + }); + + const actual = updateUpgradedForgeDevDeps(packageJSON, []); + expect(actual, JSON.stringify(actual)).to.have.lengthOf(1); + expect(actual[0]).to.match(/^@electron-forge\/plugin-compile/); + }); +});