Skip to content

Commit 2914cd1

Browse files
authored
feat(init): add --electron-version flag (#4037)
* wip * `installDependencies` * fix imports * add tests * fixes and tests
1 parent 84f76c8 commit 2914cd1

File tree

12 files changed

+253
-28
lines changed

12 files changed

+253
-28
lines changed

packages/api/cli/src/electron-forge-init.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,10 @@ program
2929
'--skip-git',
3030
'Skip initializing a git repository in the initialized project.',
3131
)
32+
.option(
33+
'--electron-version [version]',
34+
'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.',
35+
)
3236
.action(async (dir) => {
3337
const options = program.opts();
3438
const tasks = new Listr<InitOptions>(
@@ -41,6 +45,7 @@ program
4145
initOpts.force = Boolean(options.force);
4246
initOpts.skipGit = Boolean(options.skipGit);
4347
initOpts.dir = resolveWorkingDir(dir, false);
48+
initOpts.electronVersion = options.electronVersion ?? 'latest';
4449
},
4550
},
4651
{
@@ -117,6 +122,29 @@ program
117122
}
118123

119124
initOpts.template = `${bundler}${language ? `-${language}` : ''}`;
125+
126+
// TODO: add prompt for passing in an exact version as well
127+
initOpts.electronVersion = await prompt.run<Prompt<string, any>>(
128+
select,
129+
{
130+
message: 'Select an Electron release',
131+
choices: [
132+
{
133+
name: 'electron@latest',
134+
value: 'latest',
135+
},
136+
{
137+
name: 'electron@beta',
138+
value: 'beta',
139+
},
140+
{
141+
name: 'electron-nightly@latest',
142+
value: 'nightly',
143+
},
144+
],
145+
},
146+
);
147+
120148
initOpts.skipGit = !(await prompt.run(confirm, {
121149
message: `Would you like to initialize Git in your new project?`,
122150
default: true,

packages/api/core/spec/fast/init-git.spec.ts renamed to packages/api/core/spec/fast/init-scripts/init-git.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import path from 'node:path';
55

66
import { beforeEach, describe, expect, it } from 'vitest';
77

8-
import { initGit } from '../../src/api/init-scripts/init-git';
8+
import { initGit } from '../../../src/api/init-scripts/init-git';
99

1010
let dir: string;
1111
let dirID = Date.now();
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import { PACKAGE_MANAGERS } from '@electron-forge/core-utils';
2+
import { ForgeListrTask } from '@electron-forge/shared-types';
3+
import { beforeEach, describe, expect, it, vi } from 'vitest';
4+
5+
import { deps, devDeps, initNPM } from '../../../src/api/init-scripts/init-npm';
6+
import {
7+
DepType,
8+
DepVersionRestriction,
9+
installDependencies,
10+
} from '../../../src/util/install-dependencies';
11+
12+
vi.mock('../../../src/util/install-dependencies', async (importOriginal) => ({
13+
...(await importOriginal()),
14+
installDependencies: vi.fn(),
15+
}));
16+
17+
describe('init-npm', () => {
18+
const mockTask = {
19+
output: '',
20+
} as ForgeListrTask<unknown>;
21+
22+
beforeEach(() => {
23+
vi.clearAllMocks();
24+
});
25+
26+
describe('with regular electron version', () => {
27+
it('should call installDependencies three times with correct parameters', async () => {
28+
const pm = PACKAGE_MANAGERS['npm'];
29+
const dir = '/test/dir';
30+
31+
await initNPM(pm, dir, 'latest', mockTask);
32+
expect(vi.mocked(installDependencies)).toHaveBeenNthCalledWith(
33+
1,
34+
pm,
35+
dir,
36+
deps,
37+
);
38+
expect(vi.mocked(installDependencies)).toHaveBeenNthCalledWith(
39+
2,
40+
pm,
41+
dir,
42+
devDeps,
43+
DepType.DEV,
44+
);
45+
expect(vi.mocked(installDependencies)).toHaveBeenNthCalledWith(
46+
3,
47+
pm,
48+
dir,
49+
['electron@latest'],
50+
DepType.DEV,
51+
DepVersionRestriction.EXACT,
52+
);
53+
});
54+
});
55+
56+
describe('with `nightly`', () => {
57+
it('should install electron-nightly@latest instead of electron', async () => {
58+
const pm = PACKAGE_MANAGERS['npm'];
59+
const dir = '/test/dir';
60+
const electronVersion = 'nightly';
61+
62+
await initNPM(pm, dir, electronVersion, mockTask);
63+
64+
expect(installDependencies).toHaveBeenCalledTimes(3);
65+
expect(vi.mocked(installDependencies)).toHaveBeenNthCalledWith(
66+
3,
67+
pm,
68+
dir,
69+
['electron-nightly@latest'],
70+
DepType.DEV,
71+
DepVersionRestriction.EXACT,
72+
);
73+
});
74+
});
75+
});

packages/api/core/spec/fast/util/install-dependencies.spec.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,10 @@ import {
44
} from '@electron-forge/core-utils';
55
import { describe, expect, it, vi } from 'vitest';
66

7-
import installDependencies, {
7+
import {
88
DepType,
99
DepVersionRestriction,
10+
installDependencies,
1011
} from '../../../src/util/install-dependencies';
1112

1213
vi.mock(import('@electron-forge/core-utils'), async (importOriginal) => {

packages/api/core/spec/slow/api.slow.spec.ts

Lines changed: 66 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,11 @@ import {
1717
expectLintToPass,
1818
} from '@electron-forge/test-utils';
1919
import { readMetadata } from 'electron-installer-common';
20+
import semver from 'semver';
2021
import { afterAll, beforeAll, beforeEach, describe, expect, it } from 'vitest';
2122

22-
// eslint-disable-next-line n/no-missing-import
23-
import { api, InitOptions } from '../../src/api';
24-
import installDeps from '../../src/util/install-dependencies';
23+
import { api, InitOptions } from '../../src/api/index';
24+
import { installDependencies } from '../../src/util/install-dependencies';
2525
import { readRawPackageJson } from '../../src/util/read-package-json';
2626

2727
type BeforeInitFunction = () => void;
@@ -45,6 +45,68 @@ async function updatePackageJSON(
4545
);
4646
}
4747

48+
// TODO: move more tests outside of the describe.each block
49+
// if the actual package manager doesn't matter for the test
50+
describe('init params', () => {
51+
let dir: string;
52+
describe('init (with electronVersion)', () => {
53+
beforeEach(async () => {
54+
dir = await ensureTestDirIsNonexistent();
55+
56+
return async () => {
57+
await fs.promises.rm(dir, { recursive: true, force: true });
58+
};
59+
});
60+
61+
it('can define a specific Electron version with a version number', async () => {
62+
await api.init({
63+
dir,
64+
electronVersion: 'v38.0.0',
65+
});
66+
const packageJSON = await import(path.resolve(dir, 'package.json'));
67+
expect(packageJSON.devDependencies.electron).toEqual('38.0.0');
68+
});
69+
70+
it('can define a specific Electron nightly version with a version number', async () => {
71+
await api.init({
72+
dir,
73+
electronVersion: '40.0.0-nightly.20251020',
74+
});
75+
const packageJSON = await import(path.resolve(dir, 'package.json'));
76+
expect(
77+
semver.valid(packageJSON.devDependencies['electron-nightly']),
78+
).not.toBeNull();
79+
expect(packageJSON.devDependencies.electron).not.toBeDefined();
80+
});
81+
82+
it('can define a specific Electron prerelease version with the beta tag', async () => {
83+
await api.init({
84+
dir,
85+
electronVersion: 'beta',
86+
});
87+
const packageJSON = await import(path.resolve(dir, 'package.json'));
88+
const prereleaseTag = semver.prerelease(
89+
packageJSON.devDependencies.electron,
90+
);
91+
expect(prereleaseTag).toEqual(
92+
expect.arrayContaining([expect.stringMatching(/alpha|beta/)]),
93+
);
94+
});
95+
96+
it('can define a specific Electron nightly version with the nightly tag', async () => {
97+
await api.init({
98+
dir,
99+
electronVersion: 'nightly',
100+
});
101+
const packageJSON = await import(path.resolve(dir, 'package.json'));
102+
expect(
103+
semver.valid(packageJSON.devDependencies['electron-nightly']),
104+
).not.toBeNull();
105+
expect(packageJSON.devDependencies.electron).not.toBeDefined();
106+
});
107+
});
108+
});
109+
48110
describe.each([
49111
PACKAGE_MANAGERS['npm'],
50112
PACKAGE_MANAGERS['yarn'],
@@ -400,7 +462,7 @@ describe.each([
400462

401463
describe('with prebuilt native module deps installed', () => {
402464
beforeAll(async () => {
403-
await installDeps(pm, dir, ['ref-napi']);
465+
await installDependencies(pm, dir, ['ref-napi']);
404466

405467
return async () => {
406468
await fs.promises.rm(path.resolve(dir, 'node_modules/ref-napi'), {

packages/api/core/spec/slow/install-dependencies.slow.spec.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import path from 'node:path';
55
import { PACKAGE_MANAGERS } from '@electron-forge/core-utils';
66
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
77

8-
import installDeps from '../../src/util/install-dependencies';
8+
import { installDependencies } from '../../src/util/install-dependencies';
99

1010
describe.runIf(!(process.platform === 'linux' && process.env.CI))(
1111
'install-dependencies',
@@ -19,7 +19,9 @@ describe.runIf(!(process.platform === 'linux' && process.env.CI))(
1919
});
2020

2121
it('should install the latest minor version when the dependency has a caret', async () => {
22-
await installDeps(PACKAGE_MANAGERS['npm'], installDir, ['debug@^2.0.0']);
22+
await installDependencies(PACKAGE_MANAGERS['npm'], installDir, [
23+
'debug@^2.0.0',
24+
]);
2325

2426
const packageJSON = await import(
2527
path.resolve(installDir, 'node_modules', 'debug', 'package.json')

packages/api/core/src/api/import.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,10 @@ import fs from 'fs-extra';
1717
import { Listr } from 'listr2';
1818
import { merge } from 'lodash';
1919

20-
import installDepList, {
20+
import {
2121
DepType,
2222
DepVersionRestriction,
23+
installDependencies,
2324
} from '../util/install-dependencies';
2425
import { readRawPackageJson } from '../util/read-package-json';
2526
import upgradeForgeConfig, {
@@ -306,15 +307,20 @@ export default autoTrace(
306307

307308
d('installing dependencies');
308309
task.output = `${pm.executable} ${pm.install} ${importDeps.join(' ')}`;
309-
await installDepList(pm, dir, importDeps);
310+
await installDependencies(pm, dir, importDeps);
310311

311312
d('installing devDependencies');
312313
task.output = `${pm.executable} ${pm.install} ${pm.dev} ${importDevDeps.join(' ')}`;
313-
await installDepList(pm, dir, importDevDeps, DepType.DEV);
314+
await installDependencies(
315+
pm,
316+
dir,
317+
importDevDeps,
318+
DepType.DEV,
319+
);
314320

315321
d('installing devDependencies with exact versions');
316322
task.output = `${pm.executable} ${pm.install} ${pm.dev} ${pm.exact} ${importExactDevDeps.join(' ')}`;
317-
await installDepList(
323+
await installDependencies(
318324
pm,
319325
dir,
320326
importExactDevDeps,

packages/api/core/src/api/init-scripts/init-npm.ts

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,12 @@ import { PMDetails } from '@electron-forge/core-utils';
44
import { ForgeListrTask } from '@electron-forge/shared-types';
55
import debug from 'debug';
66
import fs from 'fs-extra';
7+
import semver from 'semver';
78

8-
import installDepList, {
9+
import {
910
DepType,
1011
DepVersionRestriction,
12+
installDependencies,
1113
} from '../../util/install-dependencies';
1214

1315
const d = debug('electron-forge:init:npm');
@@ -35,23 +37,34 @@ export const exactDevDeps = ['electron'];
3537
export const initNPM = async <T>(
3638
pm: PMDetails,
3739
dir: string,
40+
electronVersion: string,
3841
task: ForgeListrTask<T>,
3942
): Promise<void> => {
4043
d('installing dependencies');
4144
task.output = `${pm.executable} ${pm.install} ${deps.join(' ')}`;
42-
await installDepList(pm, dir, deps);
45+
await installDependencies(pm, dir, deps);
4346

44-
d('installing devDependencies');
45-
task.output = `${pm.executable} ${pm.install} ${pm.dev} ${deps.join(' ')}`;
46-
await installDepList(pm, dir, devDeps, DepType.DEV);
47+
d(`installing devDependencies`);
48+
task.output = `${pm.executable} ${pm.install} ${pm.dev} ${devDeps.join(' ')}`;
49+
await installDependencies(pm, dir, devDeps, DepType.DEV);
4750

4851
d('installing exact devDependencies');
4952
for (const packageName of exactDevDeps) {
50-
task.output = `${pm.executable} ${pm.install} ${pm.dev} ${pm.exact} ${packageName}`;
51-
await installDepList(
53+
let packageInstallString = packageName;
54+
if (packageName === 'electron') {
55+
if (electronVersion === 'nightly') {
56+
packageInstallString = `electron-nightly@latest`;
57+
} else if (semver.prerelease(electronVersion)?.includes('nightly')) {
58+
packageInstallString = `electron-nightly@${electronVersion}`;
59+
} else {
60+
packageInstallString += `@${electronVersion}`;
61+
}
62+
}
63+
task.output = `${pm.executable} ${pm.install} ${pm.dev} ${pm.exact} ${packageInstallString}`;
64+
await installDependencies(
5265
pm,
5366
dir,
54-
[packageName],
67+
[packageInstallString],
5568
DepType.DEV,
5669
DepVersionRestriction.EXACT,
5770
);

0 commit comments

Comments
 (0)