From 1bb565ddf46eb5483704efb5b5e970a2c36eb7d2 Mon Sep 17 00:00:00 2001 From: Erick Zhao Date: Mon, 20 Oct 2025 16:56:08 -0700 Subject: [PATCH 1/5] wip --- packages/api/cli/src/electron-forge-init.ts | 27 +++++++++++++++++++ .../fast/{ => init-scripts}/init-git.spec.ts | 2 +- .../spec/fast/init-scripts/init-npm.spec.ts | 5 ++++ .../fast/util/install-dependencies.spec.ts | 3 ++- .../slow/install-dependencies.slow.spec.ts | 6 +++-- packages/api/core/src/api/import.ts | 3 ++- .../api/core/src/api/init-scripts/init-npm.ts | 20 ++++++++++---- packages/api/core/src/api/init.ts | 13 +++++++-- .../api/core/src/util/install-dependencies.ts | 6 ++--- .../spec/ViteTypeScriptTemplate.slow.spec.ts | 1 + .../spec/WebpackTypeScript.slow.spec.ts | 1 + 11 files changed, 72 insertions(+), 15 deletions(-) rename packages/api/core/spec/fast/{ => init-scripts}/init-git.spec.ts (97%) create mode 100644 packages/api/core/spec/fast/init-scripts/init-npm.spec.ts diff --git a/packages/api/cli/src/electron-forge-init.ts b/packages/api/cli/src/electron-forge-init.ts index 934dce4454..f570d912eb 100644 --- a/packages/api/cli/src/electron-forge-init.ts +++ b/packages/api/cli/src/electron-forge-init.ts @@ -29,6 +29,10 @@ program '--skip-git', 'Skip initializing a git repository in the initialized project.', ) + .option( + '--electron-version [version]', + 'Set a specific Electron version for your Forge project. Can take in a version string (e.g. `38.3.0`) or `latest`, `beta`, or `nightly`.', + ) .action(async (dir) => { const options = program.opts(); const tasks = new Listr( @@ -117,6 +121,29 @@ program } initOpts.template = `${bundler}${language ? `-${language}` : ''}`; + + // TODO: add prompt for passing in an exact version as well + initOpts.electronVersion = await prompt.run>( + select, + { + message: 'Select an Electron release', + choices: [ + { + name: 'electron@latest', + value: 'latest', + }, + { + name: 'electron@beta', + value: 'beta', + }, + { + name: 'electron-nightly@latest', + value: 'nightly', + }, + ], + }, + ); + initOpts.skipGit = !(await prompt.run(confirm, { message: `Would you like to initialize Git in your new project?`, default: true, diff --git a/packages/api/core/spec/fast/init-git.spec.ts b/packages/api/core/spec/fast/init-scripts/init-git.spec.ts similarity index 97% rename from packages/api/core/spec/fast/init-git.spec.ts rename to packages/api/core/spec/fast/init-scripts/init-git.spec.ts index a20c3ec0e8..aefae37aad 100644 --- a/packages/api/core/spec/fast/init-git.spec.ts +++ b/packages/api/core/spec/fast/init-scripts/init-git.spec.ts @@ -5,7 +5,7 @@ import path from 'node:path'; import { beforeEach, describe, expect, it } from 'vitest'; -import { initGit } from '../../src/api/init-scripts/init-git'; +import { initGit } from '../../../src/api/init-scripts/init-git'; let dir: string; let dirID = Date.now(); diff --git a/packages/api/core/spec/fast/init-scripts/init-npm.spec.ts b/packages/api/core/spec/fast/init-scripts/init-npm.spec.ts new file mode 100644 index 0000000000..bbf53a1a6b --- /dev/null +++ b/packages/api/core/spec/fast/init-scripts/init-npm.spec.ts @@ -0,0 +1,5 @@ +import { describe } from 'vitest'; + +describe('init-npm', () => { + // Add your tests here +}); diff --git a/packages/api/core/spec/fast/util/install-dependencies.spec.ts b/packages/api/core/spec/fast/util/install-dependencies.spec.ts index fc029e7369..b89b7a1aae 100644 --- a/packages/api/core/spec/fast/util/install-dependencies.spec.ts +++ b/packages/api/core/spec/fast/util/install-dependencies.spec.ts @@ -4,9 +4,10 @@ import { } from '@electron-forge/core-utils'; import { describe, expect, it, vi } from 'vitest'; -import installDependencies, { +import { DepType, DepVersionRestriction, + installDepList as installDependencies, } from '../../../src/util/install-dependencies'; vi.mock(import('@electron-forge/core-utils'), async (importOriginal) => { diff --git a/packages/api/core/spec/slow/install-dependencies.slow.spec.ts b/packages/api/core/spec/slow/install-dependencies.slow.spec.ts index ff10d8d4b6..7a8877df27 100644 --- a/packages/api/core/spec/slow/install-dependencies.slow.spec.ts +++ b/packages/api/core/spec/slow/install-dependencies.slow.spec.ts @@ -5,7 +5,7 @@ import path from 'node:path'; import { PACKAGE_MANAGERS } from '@electron-forge/core-utils'; import { afterAll, beforeAll, describe, expect, it } from 'vitest'; -import installDeps from '../../src/util/install-dependencies'; +import { installDepList } from '../../src/util/install-dependencies'; describe.runIf(!(process.platform === 'linux' && process.env.CI))( 'install-dependencies', @@ -19,7 +19,9 @@ describe.runIf(!(process.platform === 'linux' && process.env.CI))( }); it('should install the latest minor version when the dependency has a caret', async () => { - await installDeps(PACKAGE_MANAGERS['npm'], installDir, ['debug@^2.0.0']); + await installDepList(PACKAGE_MANAGERS['npm'], installDir, [ + 'debug@^2.0.0', + ]); const packageJSON = await import( path.resolve(installDir, 'node_modules', 'debug', 'package.json') diff --git a/packages/api/core/src/api/import.ts b/packages/api/core/src/api/import.ts index 59f82f4cfa..1d00ac48fd 100644 --- a/packages/api/core/src/api/import.ts +++ b/packages/api/core/src/api/import.ts @@ -17,9 +17,10 @@ import fs from 'fs-extra'; import { Listr } from 'listr2'; import { merge } from 'lodash'; -import installDepList, { +import { DepType, DepVersionRestriction, + installDepList, } from '../util/install-dependencies'; import { readRawPackageJson } from '../util/read-package-json'; import upgradeForgeConfig, { 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 1c51f29274..b2636811a2 100644 --- a/packages/api/core/src/api/init-scripts/init-npm.ts +++ b/packages/api/core/src/api/init-scripts/init-npm.ts @@ -5,9 +5,10 @@ import { ForgeListrTask } from '@electron-forge/shared-types'; import debug from 'debug'; import fs from 'fs-extra'; -import installDepList, { +import { DepType, DepVersionRestriction, + installDepList, } from '../../util/install-dependencies'; const d = debug('electron-forge:init:npm'); @@ -35,23 +36,32 @@ export const exactDevDeps = ['electron']; export const initNPM = async ( pm: PMDetails, dir: string, + electronVersion: string = 'latest', task: ForgeListrTask, ): Promise => { d('installing dependencies'); task.output = `${pm.executable} ${pm.install} ${deps.join(' ')}`; await installDepList(pm, dir, deps); - d('installing devDependencies'); - task.output = `${pm.executable} ${pm.install} ${pm.dev} ${deps.join(' ')}`; + d(`installing devDependencies`); + task.output = `${pm.executable} ${pm.install} ${pm.dev} ${devDeps.join(' ')}`; await installDepList(pm, dir, devDeps, DepType.DEV); d('installing exact devDependencies'); for (const packageName of exactDevDeps) { - task.output = `${pm.executable} ${pm.install} ${pm.dev} ${pm.exact} ${packageName}`; + let packageInstallString = packageName; + if (packageName === 'electron') { + if (electronVersion === 'nightly') { + packageInstallString = `electron-nightly@latest`; + } else { + packageInstallString += `@${electronVersion}`; + } + } + task.output = `${pm.executable} ${pm.install} ${pm.dev} ${pm.exact} ${packageInstallString}`; await installDepList( pm, dir, - [packageName], + [packageInstallString], DepType.DEV, DepVersionRestriction.EXACT, ); diff --git a/packages/api/core/src/api/init.ts b/packages/api/core/src/api/init.ts index b38917f15a..7cae8f52ae 100644 --- a/packages/api/core/src/api/init.ts +++ b/packages/api/core/src/api/init.ts @@ -7,9 +7,10 @@ import debug from 'debug'; import { Listr } from 'listr2'; import semver from 'semver'; -import installDepList, { +import { DepType, DepVersionRestriction, + installDepList, } from '../util/install-dependencies'; import { readRawPackageJson } from '../util/read-package-json'; @@ -46,6 +47,13 @@ export interface InitOptions { * By default, Forge initializes a git repository in the project directory. Set this option to `true` to skip this step. */ skipGit?: boolean; + /** + * Set a specific Electron version for your Forge project. + * Can take in version numbers or `latest`, `beta`, or `nightly`. + * + * @defaultValue The `latest` tag on npm. + */ + electronVersion?: string; } async function validateTemplate( @@ -75,6 +83,7 @@ export default async ({ force = false, template = 'base', skipGit = false, + electronVersion, }: InitOptions): Promise => { d(`Initializing in: ${dir}`); @@ -178,7 +187,7 @@ export default async ({ { title: 'Installing common dependencies', task: async ({ pm }, task) => { - await initNPM(pm, dir, task); + await initNPM(pm, dir, electronVersion, task); }, exitOnError: false, }, diff --git a/packages/api/core/src/util/install-dependencies.ts b/packages/api/core/src/util/install-dependencies.ts index e966b7f575..c8a4810eed 100644 --- a/packages/api/core/src/util/install-dependencies.ts +++ b/packages/api/core/src/util/install-dependencies.ts @@ -14,13 +14,13 @@ export enum DepVersionRestriction { RANGE = 'RANGE', } -export default async ( +export async function installDepList( pm: PMDetails, dir: string, deps: string[], depType = DepType.PROD, versionRestriction = DepVersionRestriction.RANGE, -): Promise => { +): Promise { d( 'installing', JSON.stringify(deps), @@ -51,4 +51,4 @@ export default async ( throw err; } } -}; +} diff --git a/packages/template/vite-typescript/spec/ViteTypeScriptTemplate.slow.spec.ts b/packages/template/vite-typescript/spec/ViteTypeScriptTemplate.slow.spec.ts index ef53390223..79a3e228b5 100644 --- a/packages/template/vite-typescript/spec/ViteTypeScriptTemplate.slow.spec.ts +++ b/packages/template/vite-typescript/spec/ViteTypeScriptTemplate.slow.spec.ts @@ -35,6 +35,7 @@ describe('ViteTypeScriptTemplate', () => { dir, template: path.resolve(__dirname, '..'), interactive: false, + electronVersion: '38.2.2', }); }); diff --git a/packages/template/webpack-typescript/spec/WebpackTypeScript.slow.spec.ts b/packages/template/webpack-typescript/spec/WebpackTypeScript.slow.spec.ts index 00ae308704..700e0ce03a 100644 --- a/packages/template/webpack-typescript/spec/WebpackTypeScript.slow.spec.ts +++ b/packages/template/webpack-typescript/spec/WebpackTypeScript.slow.spec.ts @@ -25,6 +25,7 @@ describe('WebpackTypeScriptTemplate', () => { dir, template: path.join(__dirname, '..'), interactive: false, + electronVersion: '38.2.2', }); }); From c848046cf928fd06d98cd6d93848ba6d9befd004 Mon Sep 17 00:00:00 2001 From: Erick Zhao Date: Mon, 20 Oct 2025 17:00:26 -0700 Subject: [PATCH 2/5] `installDependencies` --- .../spec/fast/util/install-dependencies.spec.ts | 2 +- .../spec/slow/install-dependencies.slow.spec.ts | 4 ++-- packages/api/core/src/api/import.ts | 13 +++++++++---- packages/api/core/src/api/init-scripts/init-npm.ts | 8 ++++---- packages/api/core/src/api/init.ts | 6 +++--- packages/api/core/src/util/install-dependencies.ts | 2 +- 6 files changed, 20 insertions(+), 15 deletions(-) diff --git a/packages/api/core/spec/fast/util/install-dependencies.spec.ts b/packages/api/core/spec/fast/util/install-dependencies.spec.ts index b89b7a1aae..27cfe8f1e0 100644 --- a/packages/api/core/spec/fast/util/install-dependencies.spec.ts +++ b/packages/api/core/spec/fast/util/install-dependencies.spec.ts @@ -7,7 +7,7 @@ import { describe, expect, it, vi } from 'vitest'; import { DepType, DepVersionRestriction, - installDepList as installDependencies, + installDependencies, } from '../../../src/util/install-dependencies'; vi.mock(import('@electron-forge/core-utils'), async (importOriginal) => { diff --git a/packages/api/core/spec/slow/install-dependencies.slow.spec.ts b/packages/api/core/spec/slow/install-dependencies.slow.spec.ts index 7a8877df27..0c518c53c9 100644 --- a/packages/api/core/spec/slow/install-dependencies.slow.spec.ts +++ b/packages/api/core/spec/slow/install-dependencies.slow.spec.ts @@ -5,7 +5,7 @@ import path from 'node:path'; import { PACKAGE_MANAGERS } from '@electron-forge/core-utils'; import { afterAll, beforeAll, describe, expect, it } from 'vitest'; -import { installDepList } from '../../src/util/install-dependencies'; +import { installDependencies } from '../../src/util/install-dependencies'; describe.runIf(!(process.platform === 'linux' && process.env.CI))( 'install-dependencies', @@ -19,7 +19,7 @@ describe.runIf(!(process.platform === 'linux' && process.env.CI))( }); it('should install the latest minor version when the dependency has a caret', async () => { - await installDepList(PACKAGE_MANAGERS['npm'], installDir, [ + await installDependencies(PACKAGE_MANAGERS['npm'], installDir, [ 'debug@^2.0.0', ]); diff --git a/packages/api/core/src/api/import.ts b/packages/api/core/src/api/import.ts index 1d00ac48fd..997db79f13 100644 --- a/packages/api/core/src/api/import.ts +++ b/packages/api/core/src/api/import.ts @@ -20,7 +20,7 @@ import { merge } from 'lodash'; import { DepType, DepVersionRestriction, - installDepList, + installDependencies, } from '../util/install-dependencies'; import { readRawPackageJson } from '../util/read-package-json'; import upgradeForgeConfig, { @@ -307,15 +307,20 @@ export default autoTrace( d('installing dependencies'); task.output = `${pm.executable} ${pm.install} ${importDeps.join(' ')}`; - await installDepList(pm, dir, importDeps); + await installDependencies(pm, dir, importDeps); d('installing devDependencies'); task.output = `${pm.executable} ${pm.install} ${pm.dev} ${importDevDeps.join(' ')}`; - await installDepList(pm, dir, importDevDeps, DepType.DEV); + await installDependencies( + pm, + dir, + importDevDeps, + DepType.DEV, + ); d('installing devDependencies with exact versions'); task.output = `${pm.executable} ${pm.install} ${pm.dev} ${pm.exact} ${importExactDevDeps.join(' ')}`; - await installDepList( + await installDependencies( pm, dir, importExactDevDeps, 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 b2636811a2..917f089cbd 100644 --- a/packages/api/core/src/api/init-scripts/init-npm.ts +++ b/packages/api/core/src/api/init-scripts/init-npm.ts @@ -8,7 +8,7 @@ import fs from 'fs-extra'; import { DepType, DepVersionRestriction, - installDepList, + installDependencies, } from '../../util/install-dependencies'; const d = debug('electron-forge:init:npm'); @@ -41,11 +41,11 @@ export const initNPM = async ( ): Promise => { d('installing dependencies'); task.output = `${pm.executable} ${pm.install} ${deps.join(' ')}`; - await installDepList(pm, dir, deps); + await installDependencies(pm, dir, deps); d(`installing devDependencies`); task.output = `${pm.executable} ${pm.install} ${pm.dev} ${devDeps.join(' ')}`; - await installDepList(pm, dir, devDeps, DepType.DEV); + await installDependencies(pm, dir, devDeps, DepType.DEV); d('installing exact devDependencies'); for (const packageName of exactDevDeps) { @@ -58,7 +58,7 @@ export const initNPM = async ( } } task.output = `${pm.executable} ${pm.install} ${pm.dev} ${pm.exact} ${packageInstallString}`; - await installDepList( + await installDependencies( pm, dir, [packageInstallString], diff --git a/packages/api/core/src/api/init.ts b/packages/api/core/src/api/init.ts index 7cae8f52ae..b0ecc6d651 100644 --- a/packages/api/core/src/api/init.ts +++ b/packages/api/core/src/api/init.ts @@ -10,7 +10,7 @@ import semver from 'semver'; import { DepType, DepVersionRestriction, - installDepList, + installDependencies, } from '../util/install-dependencies'; import { readRawPackageJson } from '../util/read-package-json'; @@ -154,7 +154,7 @@ export default async ({ if (templateModule.dependencies?.length) { task.output = `${pm.executable} ${pm.install} ${pm.dev} ${templateModule.dependencies.join(' ')}`; } - return await installDepList( + return await installDependencies( pm, dir, templateModule.dependencies || [], @@ -171,7 +171,7 @@ export default async ({ if (templateModule.devDependencies?.length) { task.output = `${pm.executable} ${pm.install} ${pm.dev} ${templateModule.devDependencies.join(' ')}`; } - await installDepList( + await installDependencies( pm, dir, templateModule.devDependencies || [], diff --git a/packages/api/core/src/util/install-dependencies.ts b/packages/api/core/src/util/install-dependencies.ts index c8a4810eed..81bb3312a9 100644 --- a/packages/api/core/src/util/install-dependencies.ts +++ b/packages/api/core/src/util/install-dependencies.ts @@ -14,7 +14,7 @@ export enum DepVersionRestriction { RANGE = 'RANGE', } -export async function installDepList( +export async function installDependencies( pm: PMDetails, dir: string, deps: string[], From 75bb938428101704e196c79cef11c41d7e82442f Mon Sep 17 00:00:00 2001 From: Erick Zhao Date: Mon, 20 Oct 2025 17:02:20 -0700 Subject: [PATCH 3/5] fix imports --- packages/api/core/spec/slow/api.slow.spec.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/api/core/spec/slow/api.slow.spec.ts b/packages/api/core/spec/slow/api.slow.spec.ts index 5d1f82112a..f3c395ddfd 100644 --- a/packages/api/core/spec/slow/api.slow.spec.ts +++ b/packages/api/core/spec/slow/api.slow.spec.ts @@ -19,9 +19,8 @@ import { import { readMetadata } from 'electron-installer-common'; import { afterAll, beforeAll, beforeEach, describe, expect, it } from 'vitest'; -// eslint-disable-next-line n/no-missing-import -import { api, InitOptions } from '../../src/api'; -import installDeps from '../../src/util/install-dependencies'; +import { api, InitOptions } from '../../src/api/index'; +import { installDependencies } from '../../src/util/install-dependencies'; import { readRawPackageJson } from '../../src/util/read-package-json'; type BeforeInitFunction = () => void; @@ -400,7 +399,7 @@ describe.each([ describe('with prebuilt native module deps installed', () => { beforeAll(async () => { - await installDeps(pm, dir, ['ref-napi']); + await installDependencies(pm, dir, ['ref-napi']); return async () => { await fs.promises.rm(path.resolve(dir, 'node_modules/ref-napi'), { From f548a8d03848ec99cfe516d0c74fc7f830a77036 Mon Sep 17 00:00:00 2001 From: Erick Zhao Date: Mon, 20 Oct 2025 17:26:30 -0700 Subject: [PATCH 4/5] add tests --- .../spec/fast/init-scripts/init-npm.spec.ts | 74 ++++++++++++++++++- .../api/core/src/api/init-scripts/init-npm.ts | 2 +- packages/api/core/src/api/init.ts | 21 +++++- 3 files changed, 93 insertions(+), 4 deletions(-) diff --git a/packages/api/core/spec/fast/init-scripts/init-npm.spec.ts b/packages/api/core/spec/fast/init-scripts/init-npm.spec.ts index bbf53a1a6b..09aa5772f4 100644 --- a/packages/api/core/spec/fast/init-scripts/init-npm.spec.ts +++ b/packages/api/core/spec/fast/init-scripts/init-npm.spec.ts @@ -1,5 +1,75 @@ -import { describe } from 'vitest'; +import { PACKAGE_MANAGERS } from '@electron-forge/core-utils'; +import { ForgeListrTask } from '@electron-forge/shared-types'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +import { deps, devDeps, initNPM } from '../../../src/api/init-scripts/init-npm'; +import { + DepType, + DepVersionRestriction, + installDependencies, +} from '../../../src/util/install-dependencies'; + +vi.mock('../../../src/util/install-dependencies', async (importOriginal) => ({ + ...(await importOriginal()), + installDependencies: vi.fn(), +})); describe('init-npm', () => { - // Add your tests here + const mockTask = { + output: '', + } as ForgeListrTask; + + beforeEach(() => { + vi.clearAllMocks(); + }); + + describe('with regular electron version', () => { + it('should call installDependencies three times with correct parameters', async () => { + const pm = PACKAGE_MANAGERS['npm']; + const dir = '/test/dir'; + + await initNPM(pm, dir, 'latest', mockTask); + expect(vi.mocked(installDependencies)).toHaveBeenNthCalledWith( + 1, + pm, + dir, + deps, + ); + expect(vi.mocked(installDependencies)).toHaveBeenNthCalledWith( + 2, + pm, + dir, + devDeps, + DepType.DEV, + ); + expect(vi.mocked(installDependencies)).toHaveBeenNthCalledWith( + 3, + pm, + dir, + ['electron@latest'], + DepType.DEV, + DepVersionRestriction.EXACT, + ); + }); + }); + + describe('with `nightly`', () => { + it('should install electron-nightly@latest instead of electron', async () => { + const pm = PACKAGE_MANAGERS['npm']; + const dir = '/test/dir'; + const electronVersion = 'nightly'; + + await initNPM(pm, dir, electronVersion, mockTask); + + expect(installDependencies).toHaveBeenCalledTimes(3); + expect(vi.mocked(installDependencies)).toHaveBeenNthCalledWith( + 3, + pm, + dir, + ['electron-nightly@latest'], + DepType.DEV, + DepVersionRestriction.EXACT, + ); + }); + }); }); 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 917f089cbd..fecab1ebc5 100644 --- a/packages/api/core/src/api/init-scripts/init-npm.ts +++ b/packages/api/core/src/api/init-scripts/init-npm.ts @@ -36,7 +36,7 @@ export const exactDevDeps = ['electron']; export const initNPM = async ( pm: PMDetails, dir: string, - electronVersion: string = 'latest', + electronVersion: string, task: ForgeListrTask, ): Promise => { d('installing dependencies'); diff --git a/packages/api/core/src/api/init.ts b/packages/api/core/src/api/init.ts index b0ecc6d651..8744976922 100644 --- a/packages/api/core/src/api/init.ts +++ b/packages/api/core/src/api/init.ts @@ -83,7 +83,7 @@ export default async ({ force = false, template = 'base', skipGit = false, - electronVersion, + electronVersion = 'latest', }: InitOptions): Promise => { d(`Initializing in: ${dir}`); @@ -108,6 +108,25 @@ export default async ({ }, rendererOptions: { persistentOutput: true }, }, + { + title: `Resolving Electron version: ${chalk.cyan(electronVersion)}`, + task: async (_ctx, task) => { + if ( + electronVersion === 'latest' || + electronVersion === 'beta' || + electronVersion === 'nightly' + ) { + task.output = `Using Electron version tag: ${chalk.cyan(electronVersion)}`; + } else if (semver.valid(electronVersion)) { + task.output = `Using Electron version: ${chalk.cyan(electronVersion)}`; + } else { + throw new Error( + `Invalid Electron version: ${electronVersion}. Must be a valid semver version or one of 'latest', 'beta', or 'nightly'.`, + ); + } + }, + rendererOptions: { persistentOutput: true }, + }, { title: 'Initializing directory', task: async (_, task) => { From 4b9f338bd112fc52183d8f967a62b8d755c36c35 Mon Sep 17 00:00:00 2001 From: Erick Zhao Date: Tue, 21 Oct 2025 12:21:55 -0700 Subject: [PATCH 5/5] fixes and tests --- packages/api/cli/src/electron-forge-init.ts | 3 +- packages/api/core/spec/slow/api.slow.spec.ts | 63 +++++++++++++++++++ .../api/core/src/api/init-scripts/init-npm.ts | 3 + packages/api/core/src/api/init.ts | 24 ++++--- 4 files changed, 84 insertions(+), 9 deletions(-) diff --git a/packages/api/cli/src/electron-forge-init.ts b/packages/api/cli/src/electron-forge-init.ts index f570d912eb..f25ac25739 100644 --- a/packages/api/cli/src/electron-forge-init.ts +++ b/packages/api/cli/src/electron-forge-init.ts @@ -31,7 +31,7 @@ program ) .option( '--electron-version [version]', - 'Set a specific Electron version for your Forge project. Can take in a version string (e.g. `38.3.0`) or `latest`, `beta`, or `nightly`.', + 'Set a specific Electron version for your Forge project. Can take in a version string (e.g. `38.3.0`) or `latest`, `beta`, or `nightly` tags.', ) .action(async (dir) => { const options = program.opts(); @@ -45,6 +45,7 @@ program initOpts.force = Boolean(options.force); initOpts.skipGit = Boolean(options.skipGit); initOpts.dir = resolveWorkingDir(dir, false); + initOpts.electronVersion = options.electronVersion ?? 'latest'; }, }, { diff --git a/packages/api/core/spec/slow/api.slow.spec.ts b/packages/api/core/spec/slow/api.slow.spec.ts index f3c395ddfd..352882e436 100644 --- a/packages/api/core/spec/slow/api.slow.spec.ts +++ b/packages/api/core/spec/slow/api.slow.spec.ts @@ -17,6 +17,7 @@ import { expectLintToPass, } from '@electron-forge/test-utils'; import { readMetadata } from 'electron-installer-common'; +import semver from 'semver'; import { afterAll, beforeAll, beforeEach, describe, expect, it } from 'vitest'; import { api, InitOptions } from '../../src/api/index'; @@ -44,6 +45,68 @@ async function updatePackageJSON( ); } +// TODO: move more tests outside of the describe.each block +// if the actual package manager doesn't matter for the test +describe('init params', () => { + let dir: string; + describe('init (with electronVersion)', () => { + beforeEach(async () => { + dir = await ensureTestDirIsNonexistent(); + + return async () => { + await fs.promises.rm(dir, { recursive: true, force: true }); + }; + }); + + it('can define a specific Electron version with a version number', async () => { + await api.init({ + dir, + electronVersion: 'v38.0.0', + }); + const packageJSON = await import(path.resolve(dir, 'package.json')); + expect(packageJSON.devDependencies.electron).toEqual('38.0.0'); + }); + + it('can define a specific Electron nightly version with a version number', async () => { + await api.init({ + dir, + electronVersion: '40.0.0-nightly.20251020', + }); + const packageJSON = await import(path.resolve(dir, 'package.json')); + expect( + semver.valid(packageJSON.devDependencies['electron-nightly']), + ).not.toBeNull(); + expect(packageJSON.devDependencies.electron).not.toBeDefined(); + }); + + it('can define a specific Electron prerelease version with the beta tag', async () => { + await api.init({ + dir, + electronVersion: 'beta', + }); + const packageJSON = await import(path.resolve(dir, 'package.json')); + const prereleaseTag = semver.prerelease( + packageJSON.devDependencies.electron, + ); + expect(prereleaseTag).toEqual( + expect.arrayContaining([expect.stringMatching(/alpha|beta/)]), + ); + }); + + it('can define a specific Electron nightly version with the nightly tag', async () => { + await api.init({ + dir, + electronVersion: 'nightly', + }); + const packageJSON = await import(path.resolve(dir, 'package.json')); + expect( + semver.valid(packageJSON.devDependencies['electron-nightly']), + ).not.toBeNull(); + expect(packageJSON.devDependencies.electron).not.toBeDefined(); + }); + }); +}); + describe.each([ PACKAGE_MANAGERS['npm'], PACKAGE_MANAGERS['yarn'], 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 fecab1ebc5..442304d97b 100644 --- a/packages/api/core/src/api/init-scripts/init-npm.ts +++ b/packages/api/core/src/api/init-scripts/init-npm.ts @@ -4,6 +4,7 @@ import { PMDetails } from '@electron-forge/core-utils'; import { ForgeListrTask } from '@electron-forge/shared-types'; import debug from 'debug'; import fs from 'fs-extra'; +import semver from 'semver'; import { DepType, @@ -53,6 +54,8 @@ export const initNPM = async ( if (packageName === 'electron') { if (electronVersion === 'nightly') { packageInstallString = `electron-nightly@latest`; + } else if (semver.prerelease(electronVersion)?.includes('nightly')) { + packageInstallString = `electron-nightly@${electronVersion}`; } else { packageInstallString += `@${electronVersion}`; } diff --git a/packages/api/core/src/api/init.ts b/packages/api/core/src/api/init.ts index 8744976922..736b855b91 100644 --- a/packages/api/core/src/api/init.ts +++ b/packages/api/core/src/api/init.ts @@ -90,6 +90,7 @@ export default async ({ const runner = new Listr<{ templateModule: ForgeTemplate; pm: PMDetails; + parsedElectronVersion: string; }>( [ { @@ -110,19 +111,26 @@ export default async ({ }, { title: `Resolving Electron version: ${chalk.cyan(electronVersion)}`, - task: async (_ctx, task) => { + task: async (ctx, task) => { if ( electronVersion === 'latest' || electronVersion === 'beta' || electronVersion === 'nightly' ) { task.output = `Using Electron version tag: ${chalk.cyan(electronVersion)}`; - } else if (semver.valid(electronVersion)) { - task.output = `Using Electron version: ${chalk.cyan(electronVersion)}`; + ctx.parsedElectronVersion = electronVersion; } else { - throw new Error( - `Invalid Electron version: ${electronVersion}. Must be a valid semver version or one of 'latest', 'beta', or 'nightly'.`, - ); + // semver.clean allows us to accept `v` versions and trims whitespace + const maybeVersion = semver.clean(electronVersion); + + if (maybeVersion) { + task.output = `Using Electron version: ${chalk.cyan(maybeVersion)}`; + ctx.parsedElectronVersion = maybeVersion; + } else { + throw new Error( + `Invalid Electron version: ${electronVersion}. Must be a valid semver version or one of 'latest', 'beta', or 'nightly'.`, + ); + } } }, rendererOptions: { persistentOutput: true }, @@ -201,12 +209,12 @@ export default async ({ }, { title: 'Finalizing dependencies', - task: async (_, task) => { + task: async (ctx, task) => { return task.newListr([ { title: 'Installing common dependencies', task: async ({ pm }, task) => { - await initNPM(pm, dir, electronVersion, task); + await initNPM(pm, dir, ctx.parsedElectronVersion, task); }, exitOnError: false, },