@electron-forge/test-utils 8.0.0-alpha.9 → 8.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -10,6 +10,11 @@ export declare function ensureModulesInstalled(dir: string, deps: string[], devD
10
10
  export declare function ensureTestDirIsNonexistent(): Promise<string>;
11
11
  export declare function expectLintToPass(dir: string): Promise<void>;
12
12
  export declare function expectTypecheckToPass(dir: string): Promise<void>;
13
+ /**
14
+ * Runs the `typecheck` script and expects it to fail.
15
+ * Returns the script's stdout, where `tsc` reports its diagnostics.
16
+ */
17
+ export declare function expectTypecheckToFail(dir: string): Promise<string>;
13
18
  /**
14
19
  * Mutates the `package.json` file in a directory.
15
20
  * Use the return value to later restore the original `package.json` value
@@ -20,4 +25,5 @@ export declare function expectTypecheckToPass(dir: string): Promise<void>;
20
25
  * @returns The original `package.json` prior to mutation
21
26
  */
22
27
  export declare function updatePackageJSON(dir: string, callback: (packageJSON: PackageJSON) => Promise<PackageJSON>): Promise<any>;
23
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguZC50cyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uL3NyYy9pbmRleC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFNQSxNQUFNLE1BQU0sV0FBVyxHQUFHLE1BQU0sQ0FBQyxNQUFNLEVBQUUsT0FBTyxDQUFDLEdBQUc7SUFDbEQsTUFBTSxDQUFDLEVBQUU7UUFDUCxLQUFLLENBQUMsRUFBRSxNQUFNLENBQUMsTUFBTSxFQUFFLE9BQU8sQ0FBQyxDQUFDO0tBQ2pDLENBQUM7SUFDRixZQUFZLENBQUMsRUFBRSxNQUFNLENBQUMsTUFBTSxFQUFFLE1BQU0sQ0FBQyxDQUFDO0lBQ3RDLGVBQWUsQ0FBQyxFQUFFLE1BQU0sQ0FBQyxNQUFNLEVBQUUsTUFBTSxDQUFDLENBQUM7Q0FDMUMsQ0FBQztBQU1GLHdCQUFzQixhQUFhLENBQUMsR0FBRyxFQUFFLE1BQU0sRUFBRSxHQUFHLElBQUksRUFBRSxNQUFNLEVBQUUsaUJBRWpFO0FBRUQsd0JBQXNCLHNCQUFzQixDQUMxQyxHQUFHLEVBQUUsTUFBTSxFQUNYLElBQUksRUFBRSxNQUFNLEVBQUUsRUFDZCxPQUFPLEVBQUUsTUFBTSxFQUFFLEdBQ2hCLE9BQU8sQ0FBQyxJQUFJLENBQUMsQ0FHZjtBQUlELHdCQUFzQiwwQkFBMEIsSUFBSSxPQUFPLENBQUMsTUFBTSxDQUFDLENBTWxFO0FBRUQsd0JBQXNCLGdCQUFnQixDQUFDLEdBQUcsRUFBRSxNQUFNLEdBQUcsT0FBTyxDQUFDLElBQUksQ0FBQyxDQVVqRTtBQUVELHdCQUFzQixxQkFBcUIsQ0FBQyxHQUFHLEVBQUUsTUFBTSxHQUFHLE9BQU8sQ0FBQyxJQUFJLENBQUMsQ0FVdEU7QUFFRDs7Ozs7Ozs7R0FRRztBQUNILHdCQUFzQixpQkFBaUIsQ0FDckMsR0FBRyxFQUFFLE1BQU0sRUFDWCxRQUFRLEVBQUUsQ0FBQyxXQUFXLEVBQUUsV0FBVyxLQUFLLE9BQU8sQ0FBQyxXQUFXLENBQUMsZ0JBVTdEIn0=
28
+ export * from './template-tests.js';
29
+ //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAMA,MAAM,MAAM,WAAW,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG;IAClD,MAAM,CAAC,EAAE;QACP,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;KACjC,CAAC;IACF,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACtC,eAAe,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC1C,CAAC;AAMF,wBAAsB,aAAa,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,MAAM,EAAE,iBAEjE;AAED,wBAAsB,sBAAsB,CAC1C,GAAG,EAAE,MAAM,EACX,IAAI,EAAE,MAAM,EAAE,EACd,OAAO,EAAE,MAAM,EAAE,GAChB,OAAO,CAAC,IAAI,CAAC,CAGf;AAID,wBAAsB,0BAA0B,IAAI,OAAO,CAAC,MAAM,CAAC,CAMlE;AAED,wBAAsB,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAUjE;AAED,wBAAsB,qBAAqB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAUtE;AAED;;;;;;;;GAQG;AACH,wBAAsB,iBAAiB,CACrC,GAAG,EAAE,MAAM,EACX,QAAQ,EAAE,CAAC,WAAW,EAAE,WAAW,KAAK,OAAO,CAAC,WAAW,CAAC,gBAU7D"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAMA,MAAM,MAAM,WAAW,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG;IAClD,MAAM,CAAC,EAAE;QACP,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;KACjC,CAAC;IACF,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACtC,eAAe,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC1C,CAAC;AAMF,wBAAsB,aAAa,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,MAAM,EAAE,iBAEjE;AAED,wBAAsB,sBAAsB,CAC1C,GAAG,EAAE,MAAM,EACX,IAAI,EAAE,MAAM,EAAE,EACd,OAAO,EAAE,MAAM,EAAE,GAChB,OAAO,CAAC,IAAI,CAAC,CAGf;AAID,wBAAsB,0BAA0B,IAAI,OAAO,CAAC,MAAM,CAAC,CAMlE;AAED,wBAAsB,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAUjE;AAED,wBAAsB,qBAAqB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAUtE;AAED;;;GAGG;AACH,wBAAsB,qBAAqB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAUxE;AAED;;;;;;;;GAQG;AACH,wBAAsB,iBAAiB,CACrC,GAAG,EAAE,MAAM,EACX,QAAQ,EAAE,CAAC,WAAW,EAAE,WAAW,KAAK,OAAO,CAAC,WAAW,CAAC,gBAY7D;AAED,cAAc,qBAAqB,CAAC"}
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
+ import fs from 'node:fs/promises';
1
2
  import os from 'node:os';
2
3
  import path from 'node:path';
3
4
  import { ExitError, spawn } from '@malept/cross-spawn-promise';
4
- import fs from 'fs-extra';
5
5
  async function runNPM(dir, ...args) {
6
6
  await spawn('npm', args, { cwd: dir });
7
7
  }
@@ -16,7 +16,7 @@ let dirID = Date.now();
16
16
  export async function ensureTestDirIsNonexistent() {
17
17
  const dir = path.resolve(os.tmpdir(), `electron-forge-test-${dirID}`);
18
18
  dirID += 1;
19
- await fs.promises.rm(dir, { recursive: true, force: true });
19
+ await fs.rm(dir, { recursive: true, force: true });
20
20
  return dir;
21
21
  }
22
22
  export async function expectLintToPass(dir) {
@@ -43,6 +43,22 @@ export async function expectTypecheckToPass(dir) {
43
43
  throw err;
44
44
  }
45
45
  }
46
+ /**
47
+ * Runs the `typecheck` script and expects it to fail.
48
+ * Returns the script's stdout, where `tsc` reports its diagnostics.
49
+ */
50
+ export async function expectTypecheckToFail(dir) {
51
+ try {
52
+ await runNPM(dir, 'run', 'typecheck');
53
+ }
54
+ catch (err) {
55
+ if (err instanceof ExitError) {
56
+ return err.stdout.toString();
57
+ }
58
+ throw err;
59
+ }
60
+ throw new Error('Expected typecheck to fail, but it passed');
61
+ }
46
62
  /**
47
63
  * Mutates the `package.json` file in a directory.
48
64
  * Use the return value to later restore the original `package.json` value
@@ -53,9 +69,10 @@ export async function expectTypecheckToPass(dir) {
53
69
  * @returns The original `package.json` prior to mutation
54
70
  */
55
71
  export async function updatePackageJSON(dir, callback) {
56
- const packageJSON = await fs.readJson(path.resolve(dir, 'package.json'));
72
+ const packageJSON = JSON.parse(await fs.readFile(path.resolve(dir, 'package.json'), 'utf8'));
57
73
  const mutated = await callback(JSON.parse(JSON.stringify(packageJSON)));
58
- await fs.promises.writeFile(path.resolve(dir, 'package.json'), JSON.stringify(mutated, null, 2), 'utf-8');
74
+ await fs.writeFile(path.resolve(dir, 'package.json'), JSON.stringify(mutated, null, 2), 'utf-8');
59
75
  return packageJSON;
60
76
  }
61
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi9zcmMvaW5kZXgudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsT0FBTyxFQUFFLE1BQU0sU0FBUyxDQUFDO0FBQ3pCLE9BQU8sSUFBSSxNQUFNLFdBQVcsQ0FBQztBQUU3QixPQUFPLEVBQUUsU0FBUyxFQUFFLEtBQUssRUFBRSxNQUFNLDZCQUE2QixDQUFDO0FBQy9ELE9BQU8sRUFBRSxNQUFNLFVBQVUsQ0FBQztBQVUxQixLQUFLLFVBQVUsTUFBTSxDQUFDLEdBQVcsRUFBRSxHQUFHLElBQWM7SUFDbEQsTUFBTSxLQUFLLENBQUMsS0FBSyxFQUFFLElBQUksRUFBRSxFQUFFLEdBQUcsRUFBRSxHQUFHLEVBQUUsQ0FBQyxDQUFDO0FBQ3pDLENBQUM7QUFFRCxNQUFNLENBQUMsS0FBSyxVQUFVLGFBQWEsQ0FBQyxHQUFXLEVBQUUsR0FBRyxJQUFjO0lBQ2hFLE1BQU0sTUFBTSxDQUFDLEdBQUcsRUFBRSxTQUFTLEVBQUUsR0FBRyxJQUFJLENBQUMsQ0FBQztBQUN4QyxDQUFDO0FBRUQsTUFBTSxDQUFDLEtBQUssVUFBVSxzQkFBc0IsQ0FDMUMsR0FBVyxFQUNYLElBQWMsRUFDZCxPQUFpQjtJQUVqQixNQUFNLGFBQWEsQ0FBQyxHQUFHLEVBQUUsR0FBRyxJQUFJLENBQUMsQ0FBQztJQUNsQyxNQUFNLGFBQWEsQ0FBQyxHQUFHLEVBQUUsWUFBWSxFQUFFLEdBQUcsT0FBTyxDQUFDLENBQUM7QUFDckQsQ0FBQztBQUVELElBQUksS0FBSyxHQUFHLElBQUksQ0FBQyxHQUFHLEVBQUUsQ0FBQztBQUV2QixNQUFNLENBQUMsS0FBSyxVQUFVLDBCQUEwQjtJQUM5QyxNQUFNLEdBQUcsR0FBRyxJQUFJLENBQUMsT0FBTyxDQUFDLEVBQUUsQ0FBQyxNQUFNLEVBQUUsRUFBRSx1QkFBdUIsS0FBSyxFQUFFLENBQUMsQ0FBQztJQUN0RSxLQUFLLElBQUksQ0FBQyxDQUFDO0lBQ1gsTUFBTSxFQUFFLENBQUMsUUFBUSxDQUFDLEVBQUUsQ0FBQyxHQUFHLEVBQUUsRUFBRSxTQUFTLEVBQUUsSUFBSSxFQUFFLEtBQUssRUFBRSxJQUFJLEVBQUUsQ0FBQyxDQUFDO0lBRTVELE9BQU8sR0FBRyxDQUFDO0FBQ2IsQ0FBQztBQUVELE1BQU0sQ0FBQyxLQUFLLFVBQVUsZ0JBQWdCLENBQUMsR0FBVztJQUNoRCxJQUFJLENBQUM7UUFDSCxNQUFNLE1BQU0sQ0FBQyxHQUFHLEVBQUUsS0FBSyxFQUFFLE1BQU0sQ0FBQyxDQUFDO0lBQ25DLENBQUM7SUFBQyxPQUFPLEdBQUcsRUFBRSxDQUFDO1FBQ2IsSUFBSSxHQUFHLFlBQVksU0FBUyxFQUFFLENBQUM7WUFDN0IsT0FBTyxDQUFDLEtBQUssQ0FBQyxTQUFTLEVBQUUsR0FBRyxDQUFDLE1BQU0sQ0FBQyxRQUFRLEVBQUUsQ0FBQyxDQUFDO1lBQ2hELE9BQU8sQ0FBQyxLQUFLLENBQUMsU0FBUyxFQUFFLEdBQUcsQ0FBQyxNQUFNLENBQUMsUUFBUSxFQUFFLENBQUMsQ0FBQztRQUNsRCxDQUFDO1FBQ0QsTUFBTSxHQUFHLENBQUM7SUFDWixDQUFDO0FBQ0gsQ0FBQztBQUVELE1BQU0sQ0FBQyxLQUFLLFVBQVUscUJBQXFCLENBQUMsR0FBVztJQUNyRCxJQUFJLENBQUM7UUFDSCxNQUFNLE1BQU0sQ0FBQyxHQUFHLEVBQUUsS0FBSyxFQUFFLFdBQVcsQ0FBQyxDQUFDO0lBQ3hDLENBQUM7SUFBQyxPQUFPLEdBQUcsRUFBRSxDQUFDO1FBQ2IsSUFBSSxHQUFHLFlBQVksU0FBUyxFQUFFLENBQUM7WUFDN0IsT0FBTyxDQUFDLEtBQUssQ0FBQyxTQUFTLEVBQUUsR0FBRyxDQUFDLE1BQU0sQ0FBQyxRQUFRLEVBQUUsQ0FBQyxDQUFDO1lBQ2hELE9BQU8sQ0FBQyxLQUFLLENBQUMsU0FBUyxFQUFFLEdBQUcsQ0FBQyxNQUFNLENBQUMsUUFBUSxFQUFFLENBQUMsQ0FBQztRQUNsRCxDQUFDO1FBQ0QsTUFBTSxHQUFHLENBQUM7SUFDWixDQUFDO0FBQ0gsQ0FBQztBQUVEOzs7Ozs7OztHQVFHO0FBQ0gsTUFBTSxDQUFDLEtBQUssVUFBVSxpQkFBaUIsQ0FDckMsR0FBVyxFQUNYLFFBQTREO0lBRTVELE1BQU0sV0FBVyxHQUFHLE1BQU0sRUFBRSxDQUFDLFFBQVEsQ0FBQyxJQUFJLENBQUMsT0FBTyxDQUFDLEdBQUcsRUFBRSxjQUFjLENBQUMsQ0FBQyxDQUFDO0lBQ3pFLE1BQU0sT0FBTyxHQUFHLE1BQU0sUUFBUSxDQUFDLElBQUksQ0FBQyxLQUFLLENBQUMsSUFBSSxDQUFDLFNBQVMsQ0FBQyxXQUFXLENBQUMsQ0FBQyxDQUFDLENBQUM7SUFDeEUsTUFBTSxFQUFFLENBQUMsUUFBUSxDQUFDLFNBQVMsQ0FDekIsSUFBSSxDQUFDLE9BQU8sQ0FBQyxHQUFHLEVBQUUsY0FBYyxDQUFDLEVBQ2pDLElBQUksQ0FBQyxTQUFTLENBQUMsT0FBTyxFQUFFLElBQUksRUFBRSxDQUFDLENBQUMsRUFDaEMsT0FBTyxDQUNSLENBQUM7SUFDRixPQUFPLFdBQVcsQ0FBQztBQUNyQixDQUFDIn0=
77
+ export * from './template-tests.js';
78
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi9zcmMvaW5kZXgudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsT0FBTyxFQUFFLE1BQU0sa0JBQWtCLENBQUM7QUFDbEMsT0FBTyxFQUFFLE1BQU0sU0FBUyxDQUFDO0FBQ3pCLE9BQU8sSUFBSSxNQUFNLFdBQVcsQ0FBQztBQUU3QixPQUFPLEVBQUUsU0FBUyxFQUFFLEtBQUssRUFBRSxNQUFNLDZCQUE2QixDQUFDO0FBVS9ELEtBQUssVUFBVSxNQUFNLENBQUMsR0FBVyxFQUFFLEdBQUcsSUFBYztJQUNsRCxNQUFNLEtBQUssQ0FBQyxLQUFLLEVBQUUsSUFBSSxFQUFFLEVBQUUsR0FBRyxFQUFFLEdBQUcsRUFBRSxDQUFDLENBQUM7QUFDekMsQ0FBQztBQUVELE1BQU0sQ0FBQyxLQUFLLFVBQVUsYUFBYSxDQUFDLEdBQVcsRUFBRSxHQUFHLElBQWM7SUFDaEUsTUFBTSxNQUFNLENBQUMsR0FBRyxFQUFFLFNBQVMsRUFBRSxHQUFHLElBQUksQ0FBQyxDQUFDO0FBQ3hDLENBQUM7QUFFRCxNQUFNLENBQUMsS0FBSyxVQUFVLHNCQUFzQixDQUMxQyxHQUFXLEVBQ1gsSUFBYyxFQUNkLE9BQWlCO0lBRWpCLE1BQU0sYUFBYSxDQUFDLEdBQUcsRUFBRSxHQUFHLElBQUksQ0FBQyxDQUFDO0lBQ2xDLE1BQU0sYUFBYSxDQUFDLEdBQUcsRUFBRSxZQUFZLEVBQUUsR0FBRyxPQUFPLENBQUMsQ0FBQztBQUNyRCxDQUFDO0FBRUQsSUFBSSxLQUFLLEdBQUcsSUFBSSxDQUFDLEdBQUcsRUFBRSxDQUFDO0FBRXZCLE1BQU0sQ0FBQyxLQUFLLFVBQVUsMEJBQTBCO0lBQzlDLE1BQU0sR0FBRyxHQUFHLElBQUksQ0FBQyxPQUFPLENBQUMsRUFBRSxDQUFDLE1BQU0sRUFBRSxFQUFFLHVCQUF1QixLQUFLLEVBQUUsQ0FBQyxDQUFDO0lBQ3RFLEtBQUssSUFBSSxDQUFDLENBQUM7SUFDWCxNQUFN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
@@ -0,0 +1,42 @@
1
+ declare const supportedTemplates: readonly ['base', 'vite', 'webpack'];
2
+ type ModuleFormat = 'es' | 'cjs';
3
+ declare const supportedModuleFormats: ModuleFormat[];
4
+ type SupportedModuleFormats = typeof supportedModuleFormats;
5
+ export type TestForgeTemplateOptions = {
6
+ /**
7
+ * Note: `create-electron-app` always creates CommonJS projects; this option
8
+ * is only here to allow us to test projects that have been manually changed
9
+ * to ESM (either by programmatically updating the relevant project files in
10
+ * the temporary test directory or by manually updating them in the Forge
11
+ * checkout for local testing).
12
+ */
13
+ moduleFormats: SupportedModuleFormats;
14
+ templateName: (typeof supportedTemplates)[number];
15
+ };
16
+ /**
17
+ * Runs the local version of `create-electron-app` to create a project based on
18
+ * a given Forge template using all supported package managers. Because this
19
+ * test suite runs under Verdaccio, all ´@electron-forge/*` packages installed
20
+ * by the templates are served from the file system and rebuilt before every
21
+ * test run, which makes it easy to test local changes in the templates.
22
+ *
23
+ * At a high level, for each different package manager, this test suite:
24
+ *
25
+ * - creates a project with `create-electron-app`;
26
+ *
27
+ * - modifies the project's preload file so that it sends a "preload file ok"
28
+ * IPC message to the main process once it has successfully loaded;
29
+ *
30
+ * - modifies the project's main process entrypoint so that it logs a "main
31
+ * process ok" message. It also sets up a listener for the IPC message sent by
32
+ * the preload file that logs a "preload file ok" when that message is received;
33
+ *
34
+ * - runs the project's `start` script and checks whether its output contains
35
+ * the expected log messages;
36
+ *
37
+ * - checks whether the project includes a lock file that is consistent with the
38
+ * package-manager used by `create-electron-app`.
39
+ */
40
+ export declare function testForgeTemplate({ moduleFormats, templateName, }: TestForgeTemplateOptions): void;
41
+ export {};
42
+ //# sourceMappingURL=template-tests.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"template-tests.d.ts","sourceRoot":"","sources":["../src/template-tests.ts"],"names":[],"mappings":"AAUA,QAAA,MAAM,kBAAkB,YAAI,MAAM,EAAE,MAAM,EAAE,SAAS,CAAU,CAAC;AAEhE,KAAK,YAAY,GAAG,IAAI,GAAG,KAAK,CAAC;AAEjC,QAAA,MAAM,sBAAsB,EAAE,YAAY,EAAkB,CAAC;AAC7D,KAAK,sBAAsB,GAAG,OAAO,sBAAsB,CAAC;AAE5D,MAAM,MAAM,wBAAwB,GAAG;IACrC;;;;;;OAMG;IACH,aAAa,EAAE,sBAAsB,CAAC;IAEtC,YAAY,EAAE,CAAC,OAAO,kBAAkB,CAAC,CAAC,MAAM,CAAC,CAAC;CACnD,CAAC;AAyDF;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAgB,iBAAiB,CAAC,EAChC,aAAa,EACb,YAAY,GACb,EAAE,wBAAwB,QA6P1B"}
@@ -0,0 +1,265 @@
1
+ import { spawn } from '@malept/cross-spawn-promise';
2
+ import path from 'node:path';
3
+ import { pathToFileURL } from 'node:url';
4
+ import fs from 'node:fs';
5
+ import debug from 'debug';
6
+ import { afterEach, beforeEach, describe, expect, test } from 'vitest';
7
+ import os from 'node:os';
8
+ const supportedTemplates = ['base', 'vite', 'webpack'];
9
+ const supportedModuleFormats = ['cjs', 'es'];
10
+ const d = debug('electron-forge:testForgeTemplate');
11
+ /**
12
+ * Summarizes the layout a package manager installed into a project, which is
13
+ * what tells a flat `node_modules` (npm, Yarn, pnpm with `nodeLinker: hoisted`)
14
+ * apart from one where every package is a link into a store, and shows which
15
+ * packages a failing app could have resolved.
16
+ */
17
+ function describeDependencyTree(projectDir) {
18
+ const nodeModules = path.join(projectDir, 'node_modules');
19
+ let entries;
20
+ try {
21
+ entries = fs.readdirSync(nodeModules, { withFileTypes: true });
22
+ }
23
+ catch (error) {
24
+ return `no readable \`node_modules\` in ${projectDir} (${error})`;
25
+ }
26
+ const names = entries
27
+ .flatMap((entry) =>
28
+ // Scopes hold the packages we care about rather than being one themselves.
29
+ entry.name.startsWith('@')
30
+ ? fs
31
+ .readdirSync(path.join(nodeModules, entry.name))
32
+ .map((scoped) => `${entry.name}/${scoped}`)
33
+ : [entry.name])
34
+ .sort();
35
+ return `${names.length} packages into ${nodeModules}: ${names.join(' ')}`;
36
+ }
37
+ /**
38
+ * Everything the Verdaccio harness' pnpm shim recorded about the installs into
39
+ * one project, which is the only account there is of them: pnpm's own output.
40
+ */
41
+ function describePnpmInstalls(projectDir) {
42
+ const invocationLog = process.env.FORGE_PNPM_INVOCATION_LOG;
43
+ if (!invocationLog)
44
+ return 'no record of the pnpm runs was kept';
45
+ let records;
46
+ try {
47
+ records = fs.readFileSync(invocationLog, 'utf8').split(/^(?==== pnpm )/m);
48
+ }
49
+ catch (error) {
50
+ return `no readable record of the pnpm runs in ${invocationLog} (${error})`;
51
+ }
52
+ const forThisProject = records.filter((record) => record.includes(`in ${projectDir}`));
53
+ return forThisProject.length
54
+ ? forThisProject.join('')
55
+ : `nothing in ${invocationLog} mentions ${projectDir}`;
56
+ }
57
+ /**
58
+ * Runs the local version of `create-electron-app` to create a project based on
59
+ * a given Forge template using all supported package managers. Because this
60
+ * test suite runs under Verdaccio, all ´@electron-forge/*` packages installed
61
+ * by the templates are served from the file system and rebuilt before every
62
+ * test run, which makes it easy to test local changes in the templates.
63
+ *
64
+ * At a high level, for each different package manager, this test suite:
65
+ *
66
+ * - creates a project with `create-electron-app`;
67
+ *
68
+ * - modifies the project's preload file so that it sends a "preload file ok"
69
+ * IPC message to the main process once it has successfully loaded;
70
+ *
71
+ * - modifies the project's main process entrypoint so that it logs a "main
72
+ * process ok" message. It also sets up a listener for the IPC message sent by
73
+ * the preload file that logs a "preload file ok" when that message is received;
74
+ *
75
+ * - runs the project's `start` script and checks whether its output contains
76
+ * the expected log messages;
77
+ *
78
+ * - checks whether the project includes a lock file that is consistent with the
79
+ * package-manager used by `create-electron-app`.
80
+ */
81
+ export function testForgeTemplate({ moduleFormats, templateName, }) {
82
+ describe(`${templateName} template`, () => {
83
+ if (!moduleFormats.length) {
84
+ d('`moduleFormats` is empty, defaulting to `cjs` only');
85
+ moduleFormats = moduleFormats.concat('cjs');
86
+ }
87
+ let tmpDir;
88
+ beforeEach(async () => {
89
+ tmpDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'electron-forge-test-'));
90
+ });
91
+ const testCases = moduleFormats.reduce((acc, moduleFormat) => {
92
+ const packageManagers = [
93
+ 'npm',
94
+ 'pnpm',
95
+ 'yarn',
96
+ ];
97
+ packageManagers.forEach((packageManager) => {
98
+ acc.push([moduleFormat, packageManager]);
99
+ });
100
+ return acc;
101
+ }, []);
102
+ test.each(testCases)(`can launch a \`%s\` project created from \`template-${templateName}\` with \`%s\``, async (moduleFormat, packageManager) => {
103
+ if (!supportedTemplates.includes(templateName)) {
104
+ throw new Error(`unknown template ${templateName}`);
105
+ }
106
+ const createOutput = await spawn('node', [
107
+ path.resolve(__dirname, '../../../external/create-electron-app/dist/create-electron-app.js'),
108
+ tmpDir,
109
+ `--template=${templateName}`,
110
+ `--package-manager=${packageManager}`,
111
+ // Electron 41 is the last version that downloads its binary from a
112
+ // `postinstall` script. Yarn 4.18 disables install scripts by
113
+ // default (`enableScripts`), so on 41 the binary never gets
114
+ // downloaded and `electron-forge start` fails with "Electron failed
115
+ // to install correctly". Electron 42+ downloads the binary on demand
116
+ // the first time it's needed instead, so no install script is
117
+ // involved.
118
+ `--electron-version=43.4.0`,
119
+ ]);
120
+ d('tmpdir: ', pathToFileURL(tmpDir).toString());
121
+ const possiblePreloadFiles = ['preload.ts', 'preload.js'].map((item) => path.resolve(tmpDir, `src/${item}`));
122
+ const possibleMainProcessEntrypoints = [
123
+ 'main.ts',
124
+ 'main.js',
125
+ 'index.ts',
126
+ 'index.js',
127
+ ].map((item) => path.resolve(tmpDir, `src/${item}`));
128
+ const preloadPath = possiblePreloadFiles.find((item) => fs.existsSync(item));
129
+ const mainProcessEntrypoint = possibleMainProcessEntrypoints.find((item) => fs.existsSync(item));
130
+ let missingPreloadFileError = null;
131
+ let missingMainProcessEntrypointError = null;
132
+ if (!preloadPath) {
133
+ missingPreloadFileError = new Error(`"preload file not found in the following locations: ${JSON.stringify(possiblePreloadFiles, null, 2)}`);
134
+ }
135
+ if (!mainProcessEntrypoint) {
136
+ missingMainProcessEntrypointError = new Error(`"main process entrypoint not found in the following locations: ${JSON.stringify(possibleMainProcessEntrypoints, null, 2)}`);
137
+ }
138
+ if (missingPreloadFileError || missingMainProcessEntrypointError) {
139
+ throw new AggregateError([missingPreloadFileError, missingMainProcessEntrypointError], 'one or more files are missing');
140
+ }
141
+ const preloadOkMessage = '__FORGE_INTERNAL_PRELOAD_PROCESS_OK__';
142
+ const mainProcessOkMessage = '__FORGE_INTERNAL_MAIN_PROCESS_OK__';
143
+ await fs.promises.appendFile(preloadPath, [
144
+ '\n',
145
+ moduleFormat === 'es'
146
+ ? `import { ipcRenderer } from 'electron';`
147
+ : `const { ipcRenderer } = require('electron');`,
148
+ // If the preload file loads correctly, it will send this message to
149
+ // the main process, which will in turn log it. Once the app exits
150
+ // (either by calling `app.exit()` in our injected code upon
151
+ // receiving this message or after the test times out), we check if
152
+ // `electronForgeStartOutput` includes this string; if it doesn't,
153
+ // it means the preload file failed to load for some reason.
154
+ `ipcRenderer.send('${preloadOkMessage}')`,
155
+ ].join('\n'));
156
+ await fs.promises.appendFile(mainProcessEntrypoint, [
157
+ '\n',
158
+ moduleFormat === 'es'
159
+ ? `import { ipcMain } from 'electron';`
160
+ : `const { ipcMain } = require('electron');`,
161
+ // This runs at the top-level and below all other template code, so
162
+ // its presence in `electronForgeStartOutput` indicates that the
163
+ // main process entrypoint must have been correctly parsed /
164
+ // transpiled.
165
+ `console.log('${mainProcessOkMessage}');`,
166
+ // Logs the message that indicates that the preload file has been
167
+ // correctly loaded.
168
+ `
169
+ ipcMain.on('${preloadOkMessage}', () => {
170
+ console.log('${preloadOkMessage}');
171
+ app.exit(0);
172
+ });
173
+ `,
174
+ ].join('\n'));
175
+ const startApp = () => spawn(packageManager, ['run', 'start'], {
176
+ cwd: tmpDir,
177
+ env: {
178
+ PATH: process.env.PATH,
179
+ /**
180
+ * `start` makes the package manager check the lockfile it just
181
+ * wrote, and `XDG_CONFIG_HOME` is where the Verdaccio test harness
182
+ * puts the config that tells pnpm which registry to use, how old a
183
+ * release has to be, which packages are exempt, and to warn rather
184
+ * than fail when the check finds a difference. Dropping it would
185
+ * leave the check looking at the public registry under a policy
186
+ * the install never ran under.
187
+ */
188
+ XDG_CONFIG_HOME: process.env.XDG_CONFIG_HOME,
189
+ ...(process.platform === 'linux' && {
190
+ DISPLAY: process.env.DISPLAY,
191
+ XAUTHORITY: process.env.XAUTHORITY,
192
+ }),
193
+ ...(packageManager !== 'yarn' && {
194
+ /**
195
+ * HACK: when running the test script with Yarn on a npm/pnpm
196
+ * project created by `create-electron-app`,
197
+ * `process.env.npm_config_user_agent` can be something like
198
+ * `yarn/4.10.3 npm/? node/v24.14.1 win32 x64`, and Forge's
199
+ * `checkPackageManager` function takes this value to mean that
200
+ * the project _also_ uses Yarn, so `electron-forge start` ends
201
+ * up failing because there's no `yarn.lock` ([relevant
202
+ * code](https://github.com/electron/forge/blob/001f41befe2c049b6f54ce7d6c55e83435141055/packages/api/cli/src/util/check-system.ts#L108-L135)).
203
+ *
204
+ * Removing the `yarn/4.10.3` user agent causes Forge to
205
+ * correctly identify the project's package manager, but since
206
+ * the version information can be missing for npm in `npm/?`, it
207
+ * fails semver validation and Forge treats it like an
208
+ * unsupported npm version, so we also have to spoof a supported
209
+ * npm version number to work around that behavior.
210
+ */
211
+ npm_config_user_agent: process.env
212
+ .npm_config_user_agent.replace(/^yarn\/\d+\.\d+\.\d+ /i, '')
213
+ .replace(/\bnpm\/\?/, 'npm/99.99.99'),
214
+ }),
215
+ },
216
+ });
217
+ let electronForgeStartOutput;
218
+ try {
219
+ electronForgeStartOutput = await startApp();
220
+ }
221
+ catch (error) {
222
+ /**
223
+ * When `start` fails, it is usually because the package manager
224
+ * installed a dependency tree the app cannot resolve its own
225
+ * configuration from, and the failure alone doesn't say which tree it
226
+ * ended up with. `create-electron-app` runs its steps with listr2's
227
+ * `exitOnError: false`, so a failed install leaves a broken project
228
+ * behind and still exits 0; its output is the only place that failure
229
+ * is reported at all.
230
+ */
231
+ console.error([
232
+ `[template-tests] ${packageManager} installed ${describeDependencyTree(tmpDir)}`,
233
+ `[template-tests] from this package.json:\n${fs.readFileSync(path.join(tmpDir, 'package.json'), 'utf8')}`,
234
+ ...(packageManager === 'pnpm'
235
+ ? [
236
+ `[template-tests] pnpm was run like this:\n${describePnpmInstalls(tmpDir)}`,
237
+ ]
238
+ : []),
239
+ `[template-tests] create-electron-app said:\n${createOutput}`,
240
+ ].join('\n'));
241
+ throw error;
242
+ }
243
+ d({ electronForgeStartOutput });
244
+ const mainProcessOk = electronForgeStartOutput.includes(mainProcessOkMessage);
245
+ const preloadProcessOk = electronForgeStartOutput.includes(preloadOkMessage);
246
+ expect(mainProcessOk).toBe(true);
247
+ expect(preloadProcessOk).toBe(true);
248
+ const lockFile = {
249
+ npm: 'package-lock.json',
250
+ pnpm: 'pnpm-lock.yaml',
251
+ yarn: 'yarn.lock',
252
+ }[packageManager];
253
+ expect(fs.existsSync(path.resolve(tmpDir, lockFile))).toBe(true);
254
+ });
255
+ afterEach(async () => {
256
+ await fs.promises.rm(tmpDir, {
257
+ recursive: true,
258
+ force: true,
259
+ maxRetries: 5,
260
+ retryDelay: 500,
261
+ });
262
+ });
263
+ });
264
+ }
265
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidGVtcGxhdGUtdGVzdHMuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi9zcmMvdGVtcGxhdGUtdGVzdHMudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsT0FBTyxFQUFFLEtBQUssRUFBRSxNQUFNLDZCQUE2QixDQUFDO0FBQ3BELE9BQU8sSUFBSSxNQUFNLFdBQVcsQ0FBQztBQUM3QixPQUFPLEVBQUUsYUFBYSxFQUFFLE1BQU0sVUFBVSxDQUFDO0FBQ3pDLE9BQU8sRUFBRSxNQUFNLFNBQVMsQ0FBQztBQUN6QixPQUFPLEtBQUssTUFBTSxPQUFPLENBQUM7QUFDMUIsT0FBTyxFQUFFLFNBQVMsRUFBRSxVQUFVLEVBQUUsUUFBUSxFQUFFLE1BQU0sRUFBRSxJQUFJLEVBQUUsTUFBTSxRQUFRLENBQUM7QUFDdkUsT0FBTyxFQUFFLE1BQU0sU0FBUyxDQUFDO0FBSXpCLE1BQU0sa0JBQWtCLEdBQUcsQ0FBQyxNQUFNLEVBQUUsTUFBTSxFQUFFLFNBQVMsQ0FBVSxDQUFDO0FBSWhFLE1BQU0sc0JBQXNCLEdBQW1CLENBQUMsS0FBSyxFQUFFLElBQUksQ0FBQyxDQUFDO0FBZ0I3RCxNQUFNLENBQUMsR0FBRyxLQUFLLENBQUMsa0NBQWtDLENBQUMsQ0FBQztBQUVwRDs7Ozs7R0FLRztBQUNILFNBQVMsc0JBQXNCLENBQUMsVUFBa0I7SUFDaEQsTUFBTSxXQUFXLEdBQUcsSUFBSSxDQUFDLElBQUksQ0FBQyxVQUFVLEVBQUUsY0FBYyxDQUFDLENBQUM7SUFFMUQsSUFBSSxPQUFPLENBQUM7SUFDWixJQUFJLENBQUM7UUFDSCxPQUFPLEdBQUcsRUFBRSxDQUFDLFdBQVcsQ0FBQyxXQUFXLEVBQUUsRUFBRSxhQUFhLEVBQUUsSUFBSSxFQUFFLENBQUMsQ0FBQztJQUNqRSxDQUFDO0lBQUMsT0FBTyxLQUFLLEVBQUUsQ0FBQztRQUNmLE9BQU8sbUNBQW1DLFVBQVUsS0FBSyxLQUFLLEdBQUcsQ0FBQztJQUNwRSxDQUFDO0lBRUQsTUFBTSxLQUFL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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@electron-forge/test-utils",
3
- "version": "8.0.0-alpha.9",
3
+ "version": "8.0.0",
4
4
  "type": "module",
5
5
  "description": "Helper utilities for the Electron Forge testsuite",
6
6
  "repository": "https://github.com/electron/forge",
@@ -9,11 +9,10 @@
9
9
  "exports": "./dist/index.js",
10
10
  "typings": "dist/index.d.ts",
11
11
  "dependencies": {
12
- "@malept/cross-spawn-promise": "^2.0.0",
13
- "fs-extra": "^10.0.0"
12
+ "@malept/cross-spawn-promise": "^2.0.0"
14
13
  },
15
14
  "engines": {
16
- "node": ">= 22.12.0"
15
+ "node": ">= 22.13.0"
17
16
  },
18
17
  "publishConfig": {
19
18
  "access": "public"
@@ -22,5 +21,5 @@
22
21
  "dist",
23
22
  "src"
24
23
  ],
25
- "gitHead": "8d048358c5e5f0b5a09e03740687aa7f0c63d027"
24
+ "gitHead": "40b4adf8ebbbb09d4349079c65a0b7682bbb8565"
26
25
  }
package/src/index.ts CHANGED
@@ -1,8 +1,8 @@
1
+ import fs from 'node:fs/promises';
1
2
  import os from 'node:os';
2
3
  import path from 'node:path';
3
4
 
4
5
  import { ExitError, spawn } from '@malept/cross-spawn-promise';
5
- import fs from 'fs-extra';
6
6
 
7
7
  export type PackageJSON = Record<string, unknown> & {
8
8
  config?: {
@@ -34,7 +34,7 @@ let dirID = Date.now();
34
34
  export async function ensureTestDirIsNonexistent(): Promise<string> {
35
35
  const dir = path.resolve(os.tmpdir(), `electron-forge-test-${dirID}`);
36
36
  dirID += 1;
37
- await fs.promises.rm(dir, { recursive: true, force: true });
37
+ await fs.rm(dir, { recursive: true, force: true });
38
38
 
39
39
  return dir;
40
40
  }
@@ -63,6 +63,22 @@ export async function expectTypecheckToPass(dir: string): Promise<void> {
63
63
  }
64
64
  }
65
65
 
66
+ /**
67
+ * Runs the `typecheck` script and expects it to fail.
68
+ * Returns the script's stdout, where `tsc` reports its diagnostics.
69
+ */
70
+ export async function expectTypecheckToFail(dir: string): Promise<string> {
71
+ try {
72
+ await runNPM(dir, 'run', 'typecheck');
73
+ } catch (err) {
74
+ if (err instanceof ExitError) {
75
+ return err.stdout.toString();
76
+ }
77
+ throw err;
78
+ }
79
+ throw new Error('Expected typecheck to fail, but it passed');
80
+ }
81
+
66
82
  /**
67
83
  * Mutates the `package.json` file in a directory.
68
84
  * Use the return value to later restore the original `package.json` value
@@ -76,12 +92,16 @@ export async function updatePackageJSON(
76
92
  dir: string,
77
93
  callback: (packageJSON: PackageJSON) => Promise<PackageJSON>,
78
94
  ) {
79
- const packageJSON = await fs.readJson(path.resolve(dir, 'package.json'));
95
+ const packageJSON = JSON.parse(
96
+ await fs.readFile(path.resolve(dir, 'package.json'), 'utf8'),
97
+ );
80
98
  const mutated = await callback(JSON.parse(JSON.stringify(packageJSON)));
81
- await fs.promises.writeFile(
99
+ await fs.writeFile(
82
100
  path.resolve(dir, 'package.json'),
83
101
  JSON.stringify(mutated, null, 2),
84
102
  'utf-8',
85
103
  );
86
104
  return packageJSON;
87
105
  }
106
+
107
+ export * from './template-tests.js';
@@ -0,0 +1,366 @@
1
+ import { spawn } from '@malept/cross-spawn-promise';
2
+ import path from 'node:path';
3
+ import { pathToFileURL } from 'node:url';
4
+ import fs from 'node:fs';
5
+ import debug from 'debug';
6
+ import { afterEach, beforeEach, describe, expect, test } from 'vitest';
7
+ import os from 'node:os';
8
+
9
+ type SupportedPackageManager = 'npm' | 'pnpm' | 'yarn';
10
+
11
+ const supportedTemplates = ['base', 'vite', 'webpack'] as const;
12
+
13
+ type ModuleFormat = 'es' | 'cjs';
14
+
15
+ const supportedModuleFormats: ModuleFormat[] = ['cjs', 'es'];
16
+ type SupportedModuleFormats = typeof supportedModuleFormats;
17
+
18
+ export type TestForgeTemplateOptions = {
19
+ /**
20
+ * Note: `create-electron-app` always creates CommonJS projects; this option
21
+ * is only here to allow us to test projects that have been manually changed
22
+ * to ESM (either by programmatically updating the relevant project files in
23
+ * the temporary test directory or by manually updating them in the Forge
24
+ * checkout for local testing).
25
+ */
26
+ moduleFormats: SupportedModuleFormats;
27
+
28
+ templateName: (typeof supportedTemplates)[number];
29
+ };
30
+
31
+ const d = debug('electron-forge:testForgeTemplate');
32
+
33
+ /**
34
+ * Summarizes the layout a package manager installed into a project, which is
35
+ * what tells a flat `node_modules` (npm, Yarn, pnpm with `nodeLinker: hoisted`)
36
+ * apart from one where every package is a link into a store, and shows which
37
+ * packages a failing app could have resolved.
38
+ */
39
+ function describeDependencyTree(projectDir: string) {
40
+ const nodeModules = path.join(projectDir, 'node_modules');
41
+
42
+ let entries;
43
+ try {
44
+ entries = fs.readdirSync(nodeModules, { withFileTypes: true });
45
+ } catch (error) {
46
+ return `no readable \`node_modules\` in ${projectDir} (${error})`;
47
+ }
48
+
49
+ const names = entries
50
+ .flatMap((entry) =>
51
+ // Scopes hold the packages we care about rather than being one themselves.
52
+ entry.name.startsWith('@')
53
+ ? fs
54
+ .readdirSync(path.join(nodeModules, entry.name))
55
+ .map((scoped) => `${entry.name}/${scoped}`)
56
+ : [entry.name],
57
+ )
58
+ .sort();
59
+
60
+ return `${names.length} packages into ${nodeModules}: ${names.join(' ')}`;
61
+ }
62
+
63
+ /**
64
+ * Everything the Verdaccio harness' pnpm shim recorded about the installs into
65
+ * one project, which is the only account there is of them: pnpm's own output.
66
+ */
67
+ function describePnpmInstalls(projectDir: string) {
68
+ const invocationLog = process.env.FORGE_PNPM_INVOCATION_LOG;
69
+ if (!invocationLog) return 'no record of the pnpm runs was kept';
70
+
71
+ let records;
72
+ try {
73
+ records = fs.readFileSync(invocationLog, 'utf8').split(/^(?==== pnpm )/m);
74
+ } catch (error) {
75
+ return `no readable record of the pnpm runs in ${invocationLog} (${error})`;
76
+ }
77
+
78
+ const forThisProject = records.filter((record) =>
79
+ record.includes(`in ${projectDir}`),
80
+ );
81
+ return forThisProject.length
82
+ ? forThisProject.join('')
83
+ : `nothing in ${invocationLog} mentions ${projectDir}`;
84
+ }
85
+
86
+ /**
87
+ * Runs the local version of `create-electron-app` to create a project based on
88
+ * a given Forge template using all supported package managers. Because this
89
+ * test suite runs under Verdaccio, all ´@electron-forge/*` packages installed
90
+ * by the templates are served from the file system and rebuilt before every
91
+ * test run, which makes it easy to test local changes in the templates.
92
+ *
93
+ * At a high level, for each different package manager, this test suite:
94
+ *
95
+ * - creates a project with `create-electron-app`;
96
+ *
97
+ * - modifies the project's preload file so that it sends a "preload file ok"
98
+ * IPC message to the main process once it has successfully loaded;
99
+ *
100
+ * - modifies the project's main process entrypoint so that it logs a "main
101
+ * process ok" message. It also sets up a listener for the IPC message sent by
102
+ * the preload file that logs a "preload file ok" when that message is received;
103
+ *
104
+ * - runs the project's `start` script and checks whether its output contains
105
+ * the expected log messages;
106
+ *
107
+ * - checks whether the project includes a lock file that is consistent with the
108
+ * package-manager used by `create-electron-app`.
109
+ */
110
+ export function testForgeTemplate({
111
+ moduleFormats,
112
+ templateName,
113
+ }: TestForgeTemplateOptions) {
114
+ describe(`${templateName} template`, () => {
115
+ if (!moduleFormats.length) {
116
+ d('`moduleFormats` is empty, defaulting to `cjs` only');
117
+
118
+ moduleFormats = moduleFormats.concat('cjs');
119
+ }
120
+
121
+ let tmpDir: string;
122
+
123
+ beforeEach(async () => {
124
+ tmpDir = await fs.promises.mkdtemp(
125
+ path.join(os.tmpdir(), 'electron-forge-test-'),
126
+ );
127
+ });
128
+
129
+ const testCases: Array<
130
+ [SupportedModuleFormats[number], SupportedPackageManager]
131
+ > = moduleFormats.reduce(
132
+ (acc, moduleFormat) => {
133
+ const packageManagers: SupportedPackageManager[] = [
134
+ 'npm',
135
+ 'pnpm',
136
+ 'yarn',
137
+ ];
138
+
139
+ packageManagers.forEach((packageManager) => {
140
+ acc.push([moduleFormat, packageManager]);
141
+ });
142
+
143
+ return acc;
144
+ },
145
+ [] as typeof testCases,
146
+ );
147
+
148
+ test.each(testCases)(
149
+ `can launch a \`%s\` project created from \`template-${templateName}\` with \`%s\``,
150
+ async (moduleFormat, packageManager) => {
151
+ if (!supportedTemplates.includes(templateName)) {
152
+ throw new Error(`unknown template ${templateName}`);
153
+ }
154
+
155
+ const createOutput = await spawn('node', [
156
+ path.resolve(
157
+ __dirname,
158
+ '../../../external/create-electron-app/dist/create-electron-app.js',
159
+ ),
160
+ tmpDir,
161
+ `--template=${templateName}`,
162
+ `--package-manager=${packageManager}`,
163
+
164
+ // Electron 41 is the last version that downloads its binary from a
165
+ // `postinstall` script. Yarn 4.18 disables install scripts by
166
+ // default (`enableScripts`), so on 41 the binary never gets
167
+ // downloaded and `electron-forge start` fails with "Electron failed
168
+ // to install correctly". Electron 42+ downloads the binary on demand
169
+ // the first time it's needed instead, so no install script is
170
+ // involved.
171
+ `--electron-version=43.4.0`,
172
+ ]);
173
+
174
+ d('tmpdir: ', pathToFileURL(tmpDir).toString());
175
+
176
+ const possiblePreloadFiles = ['preload.ts', 'preload.js'].map((item) =>
177
+ path.resolve(tmpDir, `src/${item}`),
178
+ );
179
+
180
+ const possibleMainProcessEntrypoints = [
181
+ 'main.ts',
182
+ 'main.js',
183
+ 'index.ts',
184
+ 'index.js',
185
+ ].map((item) => path.resolve(tmpDir, `src/${item}`));
186
+
187
+ const preloadPath = possiblePreloadFiles.find((item) =>
188
+ fs.existsSync(item),
189
+ )!;
190
+ const mainProcessEntrypoint = possibleMainProcessEntrypoints.find(
191
+ (item) => fs.existsSync(item),
192
+ )!;
193
+
194
+ let missingPreloadFileError: Error | null = null;
195
+ let missingMainProcessEntrypointError: Error | null = null;
196
+
197
+ if (!preloadPath) {
198
+ missingPreloadFileError = new Error(
199
+ `"preload file not found in the following locations: ${JSON.stringify(possiblePreloadFiles, null, 2)}`,
200
+ );
201
+ }
202
+
203
+ if (!mainProcessEntrypoint) {
204
+ missingMainProcessEntrypointError = new Error(
205
+ `"main process entrypoint not found in the following locations: ${JSON.stringify(possibleMainProcessEntrypoints, null, 2)}`,
206
+ );
207
+ }
208
+
209
+ if (missingPreloadFileError || missingMainProcessEntrypointError) {
210
+ throw new AggregateError(
211
+ [missingPreloadFileError, missingMainProcessEntrypointError],
212
+ 'one or more files are missing',
213
+ );
214
+ }
215
+
216
+ const preloadOkMessage = '__FORGE_INTERNAL_PRELOAD_PROCESS_OK__';
217
+ const mainProcessOkMessage = '__FORGE_INTERNAL_MAIN_PROCESS_OK__';
218
+
219
+ await fs.promises.appendFile(
220
+ preloadPath,
221
+ [
222
+ '\n',
223
+ moduleFormat === 'es'
224
+ ? `import { ipcRenderer } from 'electron';`
225
+ : `const { ipcRenderer } = require('electron');`,
226
+
227
+ // If the preload file loads correctly, it will send this message to
228
+ // the main process, which will in turn log it. Once the app exits
229
+ // (either by calling `app.exit()` in our injected code upon
230
+ // receiving this message or after the test times out), we check if
231
+ // `electronForgeStartOutput` includes this string; if it doesn't,
232
+ // it means the preload file failed to load for some reason.
233
+ `ipcRenderer.send('${preloadOkMessage}')`,
234
+ ].join('\n'),
235
+ );
236
+
237
+ await fs.promises.appendFile(
238
+ mainProcessEntrypoint,
239
+ [
240
+ '\n',
241
+ moduleFormat === 'es'
242
+ ? `import { ipcMain } from 'electron';`
243
+ : `const { ipcMain } = require('electron');`,
244
+
245
+ // This runs at the top-level and below all other template code, so
246
+ // its presence in `electronForgeStartOutput` indicates that the
247
+ // main process entrypoint must have been correctly parsed /
248
+ // transpiled.
249
+ `console.log('${mainProcessOkMessage}');`,
250
+
251
+ // Logs the message that indicates that the preload file has been
252
+ // correctly loaded.
253
+ `
254
+ ipcMain.on('${preloadOkMessage}', () => {
255
+ console.log('${preloadOkMessage}');
256
+ app.exit(0);
257
+ });
258
+ `,
259
+ ].join('\n'),
260
+ );
261
+
262
+ const startApp = () =>
263
+ spawn(packageManager, ['run', 'start'], {
264
+ cwd: tmpDir,
265
+ env: {
266
+ PATH: process.env.PATH,
267
+ /**
268
+ * `start` makes the package manager check the lockfile it just
269
+ * wrote, and `XDG_CONFIG_HOME` is where the Verdaccio test harness
270
+ * puts the config that tells pnpm which registry to use, how old a
271
+ * release has to be, which packages are exempt, and to warn rather
272
+ * than fail when the check finds a difference. Dropping it would
273
+ * leave the check looking at the public registry under a policy
274
+ * the install never ran under.
275
+ */
276
+ XDG_CONFIG_HOME: process.env.XDG_CONFIG_HOME,
277
+ ...(process.platform === 'linux' && {
278
+ DISPLAY: process.env.DISPLAY,
279
+ XAUTHORITY: process.env.XAUTHORITY,
280
+ }),
281
+ ...(packageManager !== 'yarn' && {
282
+ /**
283
+ * HACK: when running the test script with Yarn on a npm/pnpm
284
+ * project created by `create-electron-app`,
285
+ * `process.env.npm_config_user_agent` can be something like
286
+ * `yarn/4.10.3 npm/? node/v24.14.1 win32 x64`, and Forge's
287
+ * `checkPackageManager` function takes this value to mean that
288
+ * the project _also_ uses Yarn, so `electron-forge start` ends
289
+ * up failing because there's no `yarn.lock` ([relevant
290
+ * code](https://github.com/electron/forge/blob/001f41befe2c049b6f54ce7d6c55e83435141055/packages/api/cli/src/util/check-system.ts#L108-L135)).
291
+ *
292
+ * Removing the `yarn/4.10.3` user agent causes Forge to
293
+ * correctly identify the project's package manager, but since
294
+ * the version information can be missing for npm in `npm/?`, it
295
+ * fails semver validation and Forge treats it like an
296
+ * unsupported npm version, so we also have to spoof a supported
297
+ * npm version number to work around that behavior.
298
+ */
299
+ npm_config_user_agent: process.env
300
+ .npm_config_user_agent!.replace(/^yarn\/\d+\.\d+\.\d+ /i, '')
301
+ .replace(/\bnpm\/\?/, 'npm/99.99.99'),
302
+ }),
303
+ },
304
+ });
305
+
306
+ let electronForgeStartOutput: string;
307
+ try {
308
+ electronForgeStartOutput = await startApp();
309
+ } catch (error) {
310
+ /**
311
+ * When `start` fails, it is usually because the package manager
312
+ * installed a dependency tree the app cannot resolve its own
313
+ * configuration from, and the failure alone doesn't say which tree it
314
+ * ended up with. `create-electron-app` runs its steps with listr2's
315
+ * `exitOnError: false`, so a failed install leaves a broken project
316
+ * behind and still exits 0; its output is the only place that failure
317
+ * is reported at all.
318
+ */
319
+ console.error(
320
+ [
321
+ `[template-tests] ${packageManager} installed ${describeDependencyTree(tmpDir)}`,
322
+ `[template-tests] from this package.json:\n${fs.readFileSync(path.join(tmpDir, 'package.json'), 'utf8')}`,
323
+ ...(packageManager === 'pnpm'
324
+ ? [
325
+ `[template-tests] pnpm was run like this:\n${describePnpmInstalls(tmpDir)}`,
326
+ ]
327
+ : []),
328
+ `[template-tests] create-electron-app said:\n${createOutput}`,
329
+ ].join('\n'),
330
+ );
331
+
332
+ throw error;
333
+ }
334
+
335
+ d({ electronForgeStartOutput });
336
+
337
+ const mainProcessOk =
338
+ electronForgeStartOutput.includes(mainProcessOkMessage);
339
+ const preloadProcessOk =
340
+ electronForgeStartOutput.includes(preloadOkMessage);
341
+
342
+ expect(mainProcessOk).toBe(true);
343
+ expect(preloadProcessOk).toBe(true);
344
+
345
+ const lockFile = (
346
+ {
347
+ npm: 'package-lock.json',
348
+ pnpm: 'pnpm-lock.yaml',
349
+ yarn: 'yarn.lock',
350
+ } as Record<SupportedPackageManager, string>
351
+ )[packageManager];
352
+
353
+ expect(fs.existsSync(path.resolve(tmpDir, lockFile))).toBe(true);
354
+ },
355
+ );
356
+
357
+ afterEach(async () => {
358
+ await fs.promises.rm(tmpDir, {
359
+ recursive: true,
360
+ force: true,
361
+ maxRetries: 5,
362
+ retryDelay: 500,
363
+ });
364
+ });
365
+ });
366
+ }